面试官:如何构建一个高可用的系统?

4,230 阅读9分钟

有的同学,虽然经常听说“系统可用性”、“系统可靠性”、“系统稳定性”这几个词,但却又傻傻分不清其中差别,我先来解释一下。

系统可用性(Availability) :高可用的系统,故障时间少,止损快,在任何给定的时刻都可以工作。一般公司对系统可用性的要求在99.9%——99.99%之间,即:宕机时长在50分钟——500分钟之间。

系统可靠性(Reliability) :高可靠的系统,故障次数少,频率低,在较长的时间内无故障地持续运行。

系统稳定性(Stability): 在系统可靠性和可用性之上,即降低故障频次和提升止损速度的情况下,要求系统的性能稳定,不要时快时慢。

换言之,不但要求系统尽可能地随时可以提供服务,并且希望系统提供有质量保障的服务。

为了便于大家理解“系统可用性”和“系统可靠性”的区别,举个例子:

  • 如果系统在每小时崩溃一毫秒,它的可用性就超过99.9999%,但它还是高度不可靠的。

  • 如果系统从来不崩溃,但每年的圣诞节前后停机两周,它是高度可靠的,但是系统的可用性只有96%。

我浅显地认为,问高可用相关问题的面试官,要比问高并发和高性能问题的面试官经验丰富一些。

至于原因,我引用朴树《平凡之路》中的歌词片段,来娱乐一下:

我曾经跨过山和大海(嗷嗷优化性能),也穿过人山人海(老想着提升并发量),我曾经拥有着的一切(绩效得S),转眼都飘散如烟(出了大故障),我曾经失落失望,失掉所有方向(被毕业了),直到看见平凡,才是唯一的答案(好好搞系统可用性)。

其实,真的是经历过系统出现大故障的切肤之痛,才真的会在任何时候,都把系统的可用性放在第一位。

下面我们来分析一下,构建一个高可用系统有哪些策略,其大类可以分为三类:减少故障次数、降低故障时长、

缩小故障范围,然后每大类中又有各自细分小类。

整体如下:

减少故障次数

限流

系统外部的请求流量是不受控的,有可能会在某个时间点,由于某个外部因素,导致了流量激增。

而限流的目的,则是要保护系统不被超出其处理能力的请求冲垮,通过拒绝请求的方式,保证系统的可用性。

在限流工具上,无论是Guava的RateLimiter,还是SpringCloud Alibaba的Sentinel,功能都非常完善好用。

但是,限流的关键点,从来不在工具上,而在于其阈值设置多级布控上。

(1)阈值设置

  • 如果设置限流的阈值高于系统的承载量,那限流动作就等于形同虚设了。
  • 如果设置限流的阈值过低,则拦住了一部分本可以正常处理的业务请求,或造成了服务器硬件资源的浪费。

那么问题来了,如何才能探查到系统合理的限流阈值呢?

我认为最好的方案是,在系统的业务低峰期,以真实流量回放、并递增加压的方式(1倍、1.5倍、2倍、2.5倍、3倍等)进行压测,探查系统所能承载的最大容量,然后将限流阈值设置为其峰值容量的50%——70%。

之所以设置50%——70%,就是为一些意外情况留有buffer,比如:两天后代码迭代上线,业务逻辑变复杂了,消耗了更多的硬件资源,或是压测所得到的峰值容量有些偏差,等等。

当然,这里所说的压测,如果是读写混压的话,还涉及到影子库影子表和路由策略的一些知识点,在这里就不进行展开了。

(2)多级布控

我们对A系统进行压测,得出来系统的峰值容量为1000 QPS,按照其50%——70%设置系统整体的限流阈值,这样就完全高枕无忧吗?

有些系统是可以的,但有些系统未必。

假如:

我们把A系统中的限流阈值设置为600 QPS,正常情况下,系统中有90%的接口耗时在20ms以下,另外10%的接口耗时则高达2000ms以上。

但忽然有一天,耗时2000ms以上接口比例,从10%增加到了50%,而耗时在20ms以下的接口比例,则从90%减少到了50%。

这种情况下,系统还是会出问题的。

除非,我们在系统整体限流的情况下,对这几个耗时奇高的接口,再case by case地单独设置限流阈值。即:总分多级布控。

除此场景外,如果系统为几个不同的外部公司,或者公司内不同的几个业务线同时提供服务,也需要根据其请求来源进行总分多级布控。

防刷

防刷,通过限制同一时间内单一用户(或IP)对特定接口的访问次数,以拒绝请求的方式,保证系统的可用性。

如果说限流,所限制的是正常的用户业务请求的话,那防刷策略,则更多的是防止恶意请求(DDoS攻击)、不正常请求(工具抢票),以及代码实现问题(接口内重复调用、循环调用)。

防刷策略的话,我认为越在上层进行拦截处理越好,优先在WAF(Web应用防火墙)进行拦截,其次在Nginx进行拦截,最后在应用服务器进行拦截。

常见解决方案:

(1)通过WAF进行IP围栏和DDoS速率限制。

(2)通过Nginx的limit_req_zone参数,限制单个IP的请求处理速率。

(3)通过用户名 + 方法名作为Redis Key,并把过期时间设置为间隔阈值,限制用户名的请求处理速率。

不过,这种方式适合于业务流程型的大接口(如:下单、约课、查询商品列表),而非By ID类的小接口,同时也需要跟上游团队约定规范,以免误杀正常业务使用。

超时设置

一般情况下,系统对于下游服务的依赖,分为强弱依赖。

  • 强依赖:假定服务A依赖于服务B,服务B出现故障不可用时,服务A也不可用。
  • 弱依赖:假定服务A依赖于服务B,服务B出现故障不可用时,服务A虽然受到了些许影响,但是仍然可用。

假设如下场景:

当服务A请求下游的强依赖服务B的某接口,该接口TP50的响应时间为10ms,TP99的响应时间为100ms,但TP999的响应时间为5000ms,相当于是处理TP50的500个请求的时间总和。

解释一下:TP99 = 100ms,标识这段时间99%的请求执行时间都在100毫秒以内,TP50和TP999也是相同计算策略。

为了防止高并发下,TP99以上,甚至是TP999这种长尾请求对系统造成的影响,都会给下游接口调用设置一个合理的超时时间。

此举是为了牺牲零星接口的处理结果,保证系统中的绝大绝大多数请求不被其所影响,确保业务正常运行。

一般接口的超时时间,会设置在TP99和TP999之间,比如:TP99的响应时间为100ms,则超时时间可以设置为200ms。

系统巡检

系统巡检一般是应用在代码上线后,或是系统业务高峰期以前进行的,旨在提前发现并处理系统中的潜在问题。

业务高峰期以前进行,适合于业务波峰和波谷比较明显的情况。

举个例子:在线教育类业务,周中的晚上(19点 —— 21点)为业务高峰期,那么可以选择16点以后禁止发布上线,17点进行巡检,一旦发现系统问题,还有两个小时的时间进行处理解决。

巡检内容包括:

(1)应用、数据库、中间件服务器的硬件指标,比如:负载、CPU、磁盘、网络、内存、JVM等。

(2)系统QPS、TPS、接口响应时间、错误率等。

(3)是否有新增的慢查SQL,以及SQL执行时间和次数等,这点尤为关键。

故障复盘

现在市面上故障复盘的方法论很多,比如:5 WHYS分析法、5W2H分析法、黄金三问分析法等,其本质都是围绕故障本身去进行深挖,用追根究底的精神去发掘问题的本质,而不是仅仅停留在“开发的时候没有想到”、“测试的时候没有覆盖到”、“巡检的时候遗漏了”等层面。

接下来,根据“重要紧急”、“重要不紧急”两个维度,制定短期和中期TODO,务必明确执行人以及完成时间,并持续地监督跟进,直到所有的TODO全部完成。

另外,TODO必须是可落地的,而不是“下次开发的时候多思考”、“下次测试的时候多重视”、“下次巡检的时候多注意”之类的口号流。

另外,从“减少故障次数”这个角度来说,故障复盘不但是通过流程规范和技术策略,保证在以后的开发迭代中,系统不再引入增量的同类问题,也是一种由点及面地去清理现有系统中的存量问题。

结语

本文已经将“减少故障次数”的内容大致讲了一下,后续的文章中再讲讲“降低故障时长”和“缩小故障范围”。

第二篇文章《面试官:如何构建一个高可用的系统?(2)》请移步这里。