如何让系统易于扩展?

高可扩展性是架构设计中的关键指标,它意味着系统能够通过增加服务器数量来线性提升处理能力,从而应对更高的流量和并发需求。有人可能会问:“为什么在设计之初不直接规划好所需机器数量,以支持预期的并发量呢?”实际上,这样的方式在实践中行不通,原因在于流量的峰值往往是不可控的。

通常,我们会基于成本考虑,在业务平稳期预留大约 30%-50% 的冗余,来应对日常运营活动或推广带来的流量高峰。然而,在某些突发事件中,流量可能瞬间暴涨至平时的 2-3 倍,甚至更高。例如,当明星突然发布恋情等热点事件发生时,用户纷纷涌入评论、互动,导致平台流量瞬间激增。以微博为例,当年鹿晗和关晓彤公布恋情时,微博的流量在短时间内猛增,甚至导致信息流加载缓慢或中断。

遇到这样的突发流量,架构改造显然来不及了,最快的应对方式是增加机器数量。然而,扩容三倍的机器是否就能确保系统支持三倍的流量?很多人可能认为这很简单,但实际上并非如此。那么,挑战究竟在哪里呢?

为什么提升扩展性会很复杂

在之前的讨论中提到,虽然通过增加处理核心可以在单机系统中提升并行处理能力,但这个方法在并行任务量大时并不总是有效。因为当并行任务增多,系统可能因资源争夺而遇到性能瓶颈,导致处理能力不增反降。这种情况在多台机器组成的集群系统中同样存在。在集群架构中,不同系统层级往往会有一些“瓶颈点”,这些瓶颈限制了系统的横向扩展能力。

举个例子更容易理解。如果系统当前每秒承载 1000 次请求,对数据库的请求量也达到每秒 1000 次。那么当流量增加 10 倍时,虽然业务服务器可以通过扩容应对更高流量,但数据库可能成为瓶颈。同样地,如果单机网络带宽为 50Mbps,扩展到 30 台机器后,总带宽可能超出负载均衡器的千兆带宽限制,这也会成为瓶颈。

在系统扩展性设计中,哪些服务会限制扩展能力呢?一般而言,无状态服务和组件更容易扩展,而像 MySQL 这样的有状态存储服务扩展难度较大,因为在向存储集群增减机器时,会涉及大量数据迁移,而传统关系型数据库通常不具备自动扩展支持。这就是系统扩展性设计复杂的主要原因之一。

因此,在扩展系统时,我们需要从整体架构的角度来看,而不仅是业务服务器的角度。数据库、缓存、第三方依赖、负载均衡器、交换机带宽等都是系统扩展时需要关注的因素。重要的是,我们要明确系统并发量达到某一水平后,哪个因素会成为瓶颈,以便有针对性地进行扩展。

高可扩展性的设计思路

提升系统扩展性的关键思路之一是拆分,它将庞大的系统划分为单一职责的独立模块。相比于处理一个复杂的系统,逐一考虑小模块的扩展性要简单得多。这种拆分思路的核心就是将复杂问题简单化。不过,不同类型的模块在拆分时遵循的原则有所不同。

举个例子,假如你要设计一个社区系统,那么可能会有以下几个模块:

  • 用户模块:负责管理用户信息,包括注册、登录等。
  • 关系模块:用于维护用户之间的关注、好友、拉黑等关系。
  • 内容模块:负责社区内发布的内容管理,比如朋友圈或微博内容。
  • 评论与点赞模块:管理用户的评论和点赞等常见的互动行为。
  • 搜索模块:支持用户和内容的搜索功能。

在最简单的三层架构下,这些模块的设计可以是:负载均衡器负责分发请求,应用服务器处理业务逻辑,数据库负责数据存储。但是在初始设计中,所有模块的业务代码可能混合在一起,所有数据也都存储在同一个数据库中。

  1. 存储层的扩展性

在存储数据量和并发访问量上,不同业务模块的差异往往非常明显。例如,在一个成熟的社区中,关系模块的数据量通常远大于用户模块的数据量,但用户数据的访问量却远超关系数据。因此,如果当前存储遇到容量瓶颈,我们可以优先对关系模块的数据进行拆分,而不必拆分用户模块的数据。

因此,存储拆分的首要考虑因素是业务维度。拆分后,这个简单的社区系统将拥有独立的用户库、内容库、评论库、点赞库和关系库。这种设计还可以实现故障隔离,即使某个库出现故障,也不会影响到其他数据库的正常运行。

通过业务拆分,系统的扩展性得到一定提升,但随着系统运行时间的增长,单一业务数据库的容量和并发请求量仍可能超过单机的上限。这时,就需要进行数据库的第二次拆分。此时,我们会根据数据特征进行水平拆分,比如在用户库中增加两个节点,并通过某种算法将用户数据分散存储到这三个库中。这种水平拆分可以让数据库突破单机限制,相关算法会在后续讨论数据库分库分表时详细说明。

需要注意的是,节点的增加不宜过于随意,因为增加节点往往伴随手动的数据迁移,成本较高。为了避免频繁扩容,最好在规划时一次性增加足够的节点,从而减少后续调整的频率。此外,在数据库完成业务和数据维度的拆分后,尽量避免使用跨库事务。因为跨库事务需要二阶段提交,以确保所有数据库的更新要么全部成功,要么全部失败,而这种协调成本会随着节点的增加逐步攀升,最终可能难以承受。

存储层的扩展性讨论到此,接下来看看业务层是如何实现易于扩展的。

  1. 业务层的扩展性

业务层的拆分通常从三个维度入手:业务维度、重要性维度以及请求来源维度。首先,可以按业务维度将服务划分为独立的业务池。例如,对于社区系统,可以拆分出用户池、内容池、关系池、评论池、点赞池和搜索池。每个业务池依赖自身的数据库资源,不再依赖其他业务的数据库资源。

这样设计的好处是,当某个业务接口成为瓶颈时,我们只需扩展该业务池,同时确保其上下游依赖满足需求,从而大幅降低了扩容的复杂度。

此外,还可以根据业务接口的重要性,将业务分为核心池和非核心池。以关系池为例,关注和取消关注接口相对重要,可以放入核心池;而拉黑和取消拉黑操作的重要性相对较低,可以归入非核心池。通过这种划分,我们能够优先保障核心池的性能。当整体流量上升时,可以优先扩容核心池,同时降级部分非核心池接口,从而确保系统的整体稳定性。

最后,你还可以根据接入客户端类型的不同做业务池的拆分。比如说,服务于客户端接口的业务可以定义为外网池,服务于小程序或者 HTML5 页面的业务可以定义为 H5 池,服务于内部其它部门的业务可以定义为内网池,等等。

2