系统设计五大心法:水平扩张、快取、非同步、避免单点故障、监控
2024年1月26日
要做好系统设计,或是在系统设计面试中有突出的表现,必须深入细节。虽然说面对不同的系统,会需要针对需求与特性,去做特别的调整,不过有许多心法是在各类系统都适用的。在这篇文章,我们会谈 5 个系统设计心法,分别是水平扩张、快取、非同步、避免单点故障、监控
水平扩张 (Horizontal Scaling)
第一个要谈的心法是水平扩张。要扩张一个系统通常有两种方式,一种是垂直扩张 (Vertical Scaling),也就是去提升单体机器的能力,例如增加 CPU 的核心数,或是增加记忆体,或是把硬碟升级成 SSD。 但是单一硬体总是有其极限所在,因此一直升级到最后仍会遇到瓶颈。这也是为什么多数时候,在系统设计时,会选择水平扩张 (Horizontal Scaling)。
水平扩张做的事情,就是透过复制或拆分伺服器与资料库,来分散系统负载的压力。而要做到这件事,会透过均衡负载器 (load balancer) 来实现。当请求进到系统时,均衡负载器会将请求转去给不同的伺服器处理。这也是几乎在所有系统设计面试中,平衡负载器基本上一定会在你的系统设计图当中。
常见的均衡负载方式包含随机 (random) 分配给伺服器、轮询 (round robin) 分配、最少连接 (least connection),以及一致性杂凑 (consistent hashing) 等各类方法。在实务执行上,通常也会加上权重 (weight),每种方式都有其要考量的点,推荐大家可以多深入去理解不同方法的取舍。
当谈到水平扩张,就不能不提“状态”这件事。有状态会导致服务被受限,因为请求打过来,只能去找具有状态的那个伺服器;这时如果具有状态的伺服器的单点挂了,要处理就比较麻烦。这也是为什么在水平扩张的系统设计中,会让各个微服务无状态。当你在设计系统,或是在面试时被面试官问到要不要有状态时,你的雷达要记得响起来,然后说“不”。
当选择无状态的设计,某一台伺服器挂了,均衡负载器可以把请求导向其他台伺服器。在实务上,我们经常会使用心跳检测 (heartbeats) 来判断伺服器是否还正常运作。简单来说,就是会定期发心跳请求给伺服器,如果伺服器在一定时间没有回应,则会被判断为出问题,这时就会把请求转去备用的伺服器。这做法也适用在均衡负载起本身,来确保如果均衡负载器挂了,随时有备用的均衡负载器可以接手。
快取 (cache)
所谓的快取是指将运算完的结果,存放在可以更快取得的地方。当使用快取时,请求资料时会比从主要储存位置来得快,同时可以免去重复已经做过的运算。因此,快取可以达到两个显而易见的好处 — (i.) 加快响应速度 (ii.) 避免重复运算,借此能提高资料源头的乘载量
在实际的应用程式开发中,通常是会采用多层快取的策略,来大幅增加应用程式的并发量与响应速度。举例来说,可以在资料库前放一个快取,这样伺服器可以直接跟快取拿资料,这会比跟资料库拿还快,而且可以降低资料库的负载;同时我们可以在 HTTP 层做快取,这样客户端要跟伺服器端请求时,可以透过快取来加速,同时减少伺服器的负载。甚至我们可以直接在客户端 (例如浏览器) 快取,更进一步加速响应速度。
虽说快取好处很明显,但也有必须注意的问题。在软体工程界有个名言是“电脑科学只有两件困难的事,一个是快取失效管理,另一个是命名 There are only two hard things in Computer Science: cache invalidation and naming things”。快取失效 (cache invalidation) 的意思是指判断什么时候不该继续用快取,而是要再去源头取资料,然后更新快取 (revalidate cache)。这个困难点在于,每个应用有不同的属性,所以没有一个通用的标准,需要工程师随着应用的特性去做判断。
除此之外,在系统中加入快取时,千万不要把快取当成资料库。在快取背后,务必要有一个最终的资料来源 (source of truth),因为你没办法保证负责快取的机器会不会故障或出问题,因此要有带着“快取的资料可能会丢失”的角度思考,这时就会有意识要有一个最终的资料来源。
非同步 (async)
在系统与程式设计中,当发起某个请求或操作时,可以是同步 (synchronous) 也可以是非同步 (asynchronous) 进行。如果是同步的方式,该直到操作完成,操作会阻塞其他部分的进行。反之,如果选择非同步的方式,就不需用等该操作完成,也可以避免阻塞的问题。
在讨论非同步前,有两个概念要先厘清,一个是编排 (orchestration),意即依赖某个中枢的指挥,来驱动整个系统的流程;另一个是协同 (choreography),意即定义好系统中各个部分的职责,但具体要怎么运行,则由该部分自行负责。
如果选择编排的方式,可能遇到的问题会是,当中枢节点出问题,需要有相对应的处理机制,不然会导致整个系统的运作出问题;而选择用协同的形式,虽然可以有效把各部分与中枢解耦,但是需要有一个额外的系统来监控,确保每个部分的运作都是良好的。从系统的角度来看,除非是关键的请求,不然现在业界会更偏向协同的模式。
从协同的角度进一步讨论,最常见的非同步模式,就是透过消息对列 (message queue),发送方 (producer) 只需要把消息 (message) 送往消息对列,让接收方(consumer) 依序从消息对列中取出讯息并处理,透过这种方式达到非同步处理。此架构不仅能达到解耦,也可以轻易扩展,只需要在发送方、接收方分别增加机器,就可以扩展。
总的来说,想要让系统解藕,同时能轻易扩展系统,使用消息对列进行非同步处理,是非常有效的方法。
避免单点故障
所谓的单点故障 (Single Point of Failure,简称 SPOF) 指的是系统中的某个单一节点失效时,导致整个系统无法运作进而崩溃。这对于系统来说,最直接的影响就是可用性 (availability) 会降低,而对任何系统来说,这都是不乐见的事情。也因此,在系统设计时,这个概念特别重要。
在往下讲之前,先来了解一下可用性,以及一些关于可用性大家需要知道的概念。假如要用白话来理解,可用性是指系统可供使用的时间;通常我们会用 系统可供使用的时间 / 总时间
这个比例来衡量。举例来说,业界常用的 99.99% 可用性,即是指一年只有 52 分钟系统是不可用的 (365 天 x 24 小时 x 60 分钟 x 0.0001 会是约 52 分钟)。
在业界,大家常会看到 SLA (Service Level Agreement) 即是针对可用性的协议。而常听人说的“几个 9”则是在描述 SLA 上的比例。以上面的 99.99% 来说,会被说四个 9;如果是 99.999% 则会被说五个 9,往下以此类推。
在业界通常违反 SLA,都是会有相对应的赔偿,举例来说,假如你用某个 AWS 的服务,SLA 写四个 9,结果该年 AWS 挂掉超过 52 分钟,AWS 的合约上会有他们相对应要做的赔偿。身为设计系统的人,你肯定不想系统挂掉超过 SLA 协议的规范,不然系统挂掉造成客户的损失,也是需要自己来承担。
要做到高可用,避免单点故障,就会是非常重要的。要避免单点故障,有几个重要的概念,首先要避免系统中的模块有过高的相互依赖,减少依赖就能避免“一个地方坏掉,造成整个系统崩掉”的状况。
然而,假如有某些关键依赖,就是没办法不依赖,这时可以则务必要避免某个模组有热点,意即被大量地请求,先前提的均衡负载就能来做到这件事。除此之外,有备案也是非常重要的。常听人说的冗余 (redundancy) 即使如此,如果某个模块挂掉了,备案可以马上接替。
监控 (monitoring)
监控在真实世界的系统中,是非常重要的环节,其核心在于能预测并协助系统在问题发生前主动发出警告,让问题发生时相关人员能立即处理。
监控要设计的好,需要确保故障的检测、警告的发出,以及问题定位的容易程度都能被照顾到。设计的好的监控,将让维运的人,能快速上手,并能低门槛地接手 oncall 的任务。
针对上面提的点,我们一个个来谈,首先是故障的检测,要能够有效检测故障,需要定义好明确的指标,该要追踪的都有追踪到。在这个环节如果有缺失,很可能会导致在问题发生时,系统没有办法立即发出警告,导致出问题了没人知道。在准备系统设计面试时,推荐针对每个题目,都练习发想在该系统下,有什么是一定要监控的指标。
接着是警告的发出,在设计监控时,要确保过多的警告噪音 (无效的警告)。因为噪音不仅会造成团队的疲劳,还可能导致真正的问题被忽视。就像狼来了的故事,如果野狼一直没出现,大家就会开始觉得警告是无效的,这样等狼真的出现时还以为是没事,那将错过问题的黄金处理时间。
因此,在系统设计中,适当的警告阈值设计和过滤机制,都是是至关重要的,它们能协助减少错误警告发出。在实务上,通常会针对每次发出的无效警告,都进行审查,确保未来同样的无效警告会被过滤掉。
最后是问题定位的容易程度。当监控系统发出警告说有系统错误,这时开发与维运人员,需要第一时间定位问题,并处理问题。有效定位出问题,是能够协助加速处理问题的关键。要有效定位,就需要确保分级有做好,在实务上,会需要在应用层 (例如 QPS、可用性的监控)、系统层 (例如 CPU、RAM 的监控),以及基础设施层 (例如机房与网络的监控),都有监控。
举例来说,如果在基础设施层没有监控,很可能应用层的监控发出警告,但因为问题不是出在应用层,所以应用层排查不出所以然,这会浪费很多不必要的时间。
总的来说,好的监控设计,应该要能有效检测出错误,同时让开发者与维运人员,能轻易定位出问题所在,协助开发者迅速发现线上代码的问题。此外,要避免发出不必要的噪音,从而提高报警系统的准确性。