面对程序故障,我们该做些什么
“容错性设计”(Design for Failure)是微服务的另一个核心原则,也是架构反复强调的开发观念的转变。
流量治理
流量治理所要解决的问题
1.某一个服务的崩溃,会导致所有用到这个服务的其他服务都无法正常工作,一个点的错误经过层层传递,最终波及到调用链上与此有关的所有服务,这便是雪崩效应。
比如全链路,外域的一个查询不可用,导致我们本应用服务大面积不可用。
2.服务虽然没有崩溃,但由于处理能力有限,面临超过预期的突发请求时,大部分请求直至超时都无法完成处理。 这就需要消息队列削峰填谷。
我们将围绕着如何解决这两个问题,提出服务容错、流量控制、服务质量管理等一系列解决方案。
服务容错
其实前面的讲解又发现,在分布式服务中,很多设计比如一致性设计都有妥协的成分在,但是容错性设计却不能妥协,不能妥协的原因在于,分布式系统的本质是不可靠的,一个大的服务集群中,程序可能崩溃、节点可能宕机、网络可能中断,这些“意外情况”其实全部都在“意料之中”。
容错策略
第一种容错策略,是故障转移(Failover)。
第二种容错策略,是快速失败(Failfast)。
第三种容错策略,是安全失败(Failsafe)。
第四种容错策略,是沉默失败(Failsilent)。
第五种容错策略,是故障恢复(Failback)。
第六种容错策略,是并行调用(Forking)。
第七种容错策略,是广播调用(Broadcast)。
那么为了实现各种各样的容错策略,开发人员总结出了一些被实践证明有效的服务容错设计模式。这些设计模式,包括了这一讲我们要学习的,微服务中常见的断路器模式、舱壁隔离模式和超时重试模式等,以及我们下一讲要学习的流量控制模式,比如滑动时间窗模式、漏桶模式、令牌桶模式,等等。
实现容错策略
断路器模式
快速失败策略
断路器模式是微服务架构中最基础的容错设计模式,以至于像 Hystrix 这种服务治理工具,我们往往会忽略了它的服务隔离、请求合并、请求缓存等其他服务治理职能,直接把它叫做微服务断路器或者熔断器。
断路器的思路很简单,就是通过代理(断路器对象)来一对一(一个远程服务对应一个断路器对象)地接管服务调用者的远程请求。那怎么实现的呢?
断路器会持续监控并统计服务返回的成功、失败、超时、拒绝等各种结果,当出现故障(失败、超时、拒绝)的次数达到断路器的阈值时,它的状态就自动变为“OPEN”。之后这个断路器代理的远程访问都将直接返回调用失败,而不会发出真正的远程服务请求。
断路器的状态CLOSED、OPEN 和 HALF OPEN 这三种状态的转换逻辑和条件
那判断断路器应该开着还是关闭?一个可行的办法是,当一次调用失败后,如果还同时满足下面两个条件,断路器的状态就变为 OPEN:
- 一段时间(比如 10 秒以内)内,请求数量达到一定阈值(比如 20 个请求)。这个条件的意思是,如果请求本身就很少,那就用不着断路器介入。
- 一段时间(比如 10 秒以内)内,请求的故障率(发生失败、超时、拒绝的统计比例)到达一定阈值(比如 50%)。这个条件的意思是,如果请求本身都能正确返回,也用不着断路器介入。
举个例子:你女朋友有事儿想召唤你,打你手机没人接,响了几声气冲冲地挂断后(快速失败),又打了你另外三个不同朋友的手机号(故障转移),都还是没能找到你(重试超过阈值)。这时候她生气地在微信上给你留言“三分钟不回电话就分手”,以此来与你取得联系。在这个不是太吉利的故事里,女朋友给你留言这个行为便是服务降级逻辑。
舱壁隔离模式
静默失败策略
解决方法1
局部线程池,避免全局线程池短时间被打满然后全局瘫痪,但是增大了CPU开销,频繁切换上下文
解决方法2
为应对这种情况,还有一种更轻量的控制服务最大连接数的办法,那就是信号量机制(Semaphore)。
具体做法是,当服务开始调用时计数器加 1,服务返回结果后计数器减 1;一旦计数器的值超过设置的阈值就立即开始限流,在回落到阈值范围之前都不再允许请求了。因为不需要承担线程的排队、调度和切换工作,所以单纯维护一个作为计数器的信号量的性能损耗,相对于局部线程池来说,几乎可以忽略不计。
以上介绍的是从微观的、服务调用的角度应用舱壁隔离设计模式,实际上舱壁隔离模式还可以在更高层、更宏观的场景中使用,不按调用线程,而是按功能、按子系统、按用户类型等条件来隔离资源都是可以的。比如,根据用户等级、用户是否是 VIP、用户来访的地域等各种因素,将请求分流到独立的服务实例去,这样即使某一个实例完全崩溃了,也只是影响到其中某一部分的用户,把波及范围尽可能控制住。
一般来说,我们会选择将服务层面的隔离实现在服务调用端或者边车代理上,将系统层面的隔离实现在 DNS 或者网关处。
重试模式
服务因为网络抖动等原因没有成功,自恢复的。
重试实现起来简单,所以面临的最大风险就是滥用。
那怎么避免滥用呢?在实践中,我们判断是否应该且是否能够对一个服务进行重试时,要看是否同时满足下面 4 个条件。
第一,仅在主路逻辑的关键服务上进行同步的重试。
第二,仅对由瞬时故障导致的失败进行重试。比如,当发出的请求收到了 401 Unauthorized 响应时,说明服务本身是可用的,只是你没有权限调用,这时候再去重试就没有什么意义。
第三,仅对具备幂等性的服务进行重试。
第四,重试必须有明确的终止条件,常用的终止条件有超时终止和次数终止两种。
小结
熔断、隔离、重试、降级、超时等概念,都是建立具有韧性的微服务系统的必须的保障措施。那么就目前来说,这些措施的正确运作,主要还是依靠开发人员对服务逻辑的了解,以及根据运维人员的经验去静态地调整、配置参数和阈值。
目前仅仅做到容错,只让故障不扩散是远远不够的,我们还希望系统或者至少系统的核心功能能够表现出最佳的响应的能力,不受或少受硬件资源、网络带宽和系统中一两个缓慢服务的拖累。在下一节课,我们还将会面向如何解决集群中的短板效应,去讨论服务质量、流量管控等话题。