从架构设计上来说,高可扩展性是一个设计的指标,它表示可以通过增加机器的方式来线性提高系统的处理能力,从而承担更高的流量和并发。
你可能会问:"在架构设计之初,为什么不预先考虑好使用多少台机器,支持现有的并发呢?" 答案是峰值的流量不可控。
通常情况下,在业务平稳期,我们会预留30%至50%的冗余资源,以处理运营活动或推广可能引发的峰值流量。然而,当突发事件发生时,流量可能会瞬间增加到2至3倍甚至更高的水平。以微博为例,这种情况是很常见的。
当出现鹿晗和关晓彤互相确认恋情等突发事件时,微博的流量会迅速增加,导致信息流无法及时刷新。在这种情况下,我们需要迅速应对流量激增,最快的方法是增加服务器。然而,需要确保增加了三倍的服务器后,系统能够支持三倍的流量。有人可能会觉得这很明显,但实际上,这个过程中存在一些挑战。让我们看看这些挑战是什么。
为什么提升扩展性会很复杂
增加处理核心是提高系统并行处理能力的一种方法,但它并不总是有效的。当任务并行数增加时,系统可能因为资源争用而性能下降,达到一个拐点。这不仅适用于单机系统,也适用于由多台机器组成的集群系统。集群系统中,不同的层次可能存在一些限制系统横向扩展的“瓶颈点”。
让我通过一个例子来解释这个概念。假设系统的每秒请求量是1000次,与数据库的请求量相同。如果流量增加了10倍,系统可以通过扩容来满足需求,但数据库可能成为瓶颈。
再举一个例子,单台服务器的网络带宽是50Mbps,如果扩展到30台服务器,前端负载均衡的带宽将超过千兆带宽的限制,也成为瓶颈。那么,系统中有哪些服务可能会成为限制系统扩展的关键因素呢?无状态的服务和组件更容易扩展,而像MySQL这样的有状态存储服务则难以扩展。
这是因为在向存储集群中添加或删除服务器时,涉及大量数据迁移,而传统的关系型数据库通常不支持这种操作。这是提高系统扩展性复杂性的主要原因之一。此外,从示例中可以看出,我们需要从整体架构的角度考虑系统的扩展性,而不仅仅是业务服务器的角度。因此,数据库、缓存、第三方依赖、负载均衡、网络带宽等都是需要考虑的因素。我们需要了解系统达到一定并发水平后,哪些因素将成为瓶颈,然后有针对性地扩展系统。
针对这些复杂的扩展性问题,我提炼了一些系统设计思路。
高可扩展性的设计思路
拆分是提高系统扩展性的关键策略,它将复杂的系统分解成独立的、具有单一职责的模块。与处理整个大系统相比,考虑如何扩展每个小模块显然更加简单。我们的方法是将复杂的问题分解为简单的部分。
在不同类型的模块拆分方面,我们需要遵循不同的原则。举个简单的例子,如果你要设计一个社区,可能会有以下5个模块:用户、关系、内容、评论/赞、搜索。通常,这些模块都会使用最简单的三层部署架构,其中负载均衡用于请求分发,应用服务器用于处理业务逻辑,数据库用于数据存储。然而,问题是所有这些模块的业务代码都混合在一起,数据也存储在同一个数据库中。
图片
1. 存储层的扩展性
不同的业务模块之间的数据量和并发访问量可能差异很大。以一个成熟的社区系统为例,关系模块的数据量可能远大于用户数据,但用户数据的访问量却更高。因此,如果存储容量成为瓶颈,我们可以选择仅拆分关系模块的数据,而不必拆分用户模块的数据。这种存储拆分是基于业务维度的,将系统拆分为用户库、内容库、评论库、点赞库和关系库等,从而隔离了故障。如果其中一个库出现问题,不会影响其他数据库的正常运行。
拆分之后,这个简单的社区系统就有了用户库、内容库、评论库、点赞库和关系库。这么做还能隔离故障,某一个库“挂了”不会影响到其它的数据库。
图片
按照业务拆分虽然在一定程度上提升了系统的扩展性,但随着系统运行时间的增长,单一的业务数据库可能会面临容量和并发请求量超过单机限制的问题。在这种情况下,我们需要考虑对数据库进行第二次拆分。
这次的拆分是基于数据特征进行水平拆分的。例如,我们可以向用户库添加两个节点,然后使用特定的算法将用户数据分布到这三个库中。
水平拆分后,数据库就可以突破单机的限制。但需要注意的是,不应随意增加节点,因为节点的增加涉及到数据的手动迁移,会增加成本。因此,从长远考虑,最好一次性增加足够数量的节点,以避免频繁扩容。
此外,在数据库按照业务和数据维度进行拆分后,应尽量避免使用事务。因为当一个事务涉及同时更新不同的数据库时,需要使用二阶段提交来协调,这会随着资源扩展而导致协调成本不断增加,最终可能无法承受。接下来,我们将讨论业务层如何实现易于扩展。
2. 业务层的扩展性
我们一般会从三个维度考虑业务层的拆分方案,它们分别是:业务维度,重要性维度和请求来源维度。
首先,我们需要把相同业务的服务拆分成单独的业务池,比方说上面的社区系统中,我们可以按照业务的维度拆分成用户池、内容池、关系池、评论池、点赞池和搜索池。
每个业务依赖独自的数据库资源,不会依赖其它业务的数据库资源。这样当某一个业务的接口成为瓶颈时,我们只需要扩展业务的池子,以及确认上下游的依赖方就可以了,这样就大大减少了扩容的复杂度。
图片
此外,我们还可以根据业务接口的重要性,将业务划分为核心池和非核心池。以关系池为例,关注和取消关注接口可能相对重要,因此它们可以放入核心池。而拉黑和取消拉黑等操作可能不那么重要,可以放入非核心池。
这种方法允许我们首先确保核心池的性能。当整体流量增加时,我们可以首先扩容核心池,并对一些非核心池的接口进行降级,以确保整体系统的稳定性。
图片
最后,你还可以根据接入客户端类型的不同做业务池的拆分。比如说,服务于客户端接口的业务可以定义为外网池,服务于小程序或者 HTML5 页面的业务可以定义为 H5 池,服务于内部其它部门的业务可以定义为内网池,等等。