此文主要是对周志明老师相关文章的总结和搬运
在微服务架构中,服务不是永远稳定的,我们要接受服务总会出错的现实。因此,在微服务的设计中,要有自动的机制能够对其依赖的服务进行快速故障检测,在持续出错的时候进行隔离,在服务恢复的时候重新联通。所以“断路器”这类设施,对实际生产环境的微服务来说,并不是可选的外围组件,而是一个必须的支撑点。如果没有容错性的设计,系统很容易就会因为一两个服务的崩溃带来的雪崩效应而被淹没。
流量治理要解决的两个问题:
- 某一个服务的崩溃,会导致所有用到这个服务的其他服务都无法正常工作,一个点的错误经过层层传递,最终波及到调用链上与此有关的所有服务,这便是雪崩效应。
- 服务虽然没有崩溃,但由于处理能力有限,面临超过预期的突发请求时,大部分请求直至超时都无法完成处理。这种现象产生的后果跟交通堵塞是类似的,如果一开始没有得到及时地治理,后面就会需要很长时间才能使全部服务都恢复正常。
服务容错和流量控制就是解决上述两个问题的方案。
服务容错
容错策略
容错策略,指的是“面对故障,我们该做些什么”。容错策略大致分为两类:一类是针对错误发生后,应该怎样处理;另一类是在调用之前就开始考虑如何获得最大的成功概率。下面的前5种属于第一类,后2种属于第二类。
故障转移(Failover)
故障转移是指,如果调用的服务器出现故障,系统不会立即向调用者返回失败结果,而是自动切换到其他服务副本,尝试其他副本能否返回成功调用的结果,从而保证了整体的高可用性。
这种策略需要服务具有幂等性。缺点是故障转移会增加调用时间。
快速失败(Failfast)
尽快让服务报错并抛出异常,坚决避免重试,由调用者自行处理。
这种策略不需要服务具有幂等性。
安全失败(Failsafe)
在一个调用链路中的服务,通常也有主路和旁路之分,并不见得每个服务都是不可或缺的,属于旁路逻辑的一个显著特点是,服务失败了也不影响核心业务的正确性。例如日志。
属于旁路逻辑的另一个显著特征是,后续处理不会依赖其返回值,或者它的返回值是什么都不会影响后续处理的结果。
对这类逻辑,一种理想的容错策略是,即使旁路逻辑调用失败了,也当作正确来返回,如果需要返回值的话,系统就自动返回一个符合要求的数据类型的对应零值,然后自动记录一条服务调用出错的日志备查即可。
沉默失败(Failsilent)
如果大量的请求需要等到超时(或者长时间处理后)才宣告失败,很容易因为某个远程服务的请求堆积而消耗大量的线程、内存、网络等资源,进而影响到整个系统的稳定性。面对这种情况,一种合理的失败策略是当请求失败后,就默认服务提供者一定时间内无法再对外提供服务,不再向它分配请求流量,并将错误隔离开来,避免对系统其他部分产生影响。
故障恢复(Failback)
故障恢复是指,当服务调用出错了以后,将该次调用失败的信息存入一个消息队列中,然后由系统自动开始异步重试调用。
故障恢复也是需要服务具有幂等性,由于它的重试是后台异步进行,即使最后调用成功了,原来的请求也早已经响应完毕。所以,故障恢复策略一般用于对实时性要求不高的主路逻辑,也适合处理那些不需要返回值的旁路逻辑。
并行调用(Forking)
并行调用策略,是指一开始就同时向多个服务副本发起调用,只要有其中任何一个返回成功,那调用便宣告成功。
广播调用(Broadcast)
广播调用与并行调用是相对应的,都是同时发起多个调用,但并行调用是任何一个调用结果返回成功便宣告成功,而广播调用则是要求所有的请求全部都成功,才算是成功。
容错设计模式
容错设计模式,指的是“要实现某种容错策略,我们该如何去做”。
断路器模式
断路器做的事情是自动进行服务熔断,属于一种快速失败的容错策略的实现方法。断路器一般可以设置为 CLOSED、OPEN 和 HALF OPEN 三种状态。
在断路器刚刚建立的时候,默认是CLOSED状态。此时的远程请求会真正发送给服务提供者,此后将持续监视远程请求的数量和执行结果,决定是否要进入 OPEN 状态。
当断路器监控到单位时间内,达到一定数量的请求的故障率超过阈值,断路器会变为OPEN状态。此时不会进行远程请求,直接给服务调用者返回调用失败的信息。直接进行熔断。Netflix Hystrix对着三个参数的默认值分别是:10 秒、20 个请求、50%。
断路器必须带有自动的故障恢复能力,当进入 OPEN 状态一段时间以后,将“自动”(一般是由下一次请求而不是计时器触发的,所以这里的自动是带引号的)切换到 HALF OPEN 状态。在中间状态下,会放行一次远程调用,然后根据这次调用的结果成功与否,转换为 CLOSED 或者 OPEN 状态,来实现断路器的弹性恢复。
服务熔断和服务降级
这两个是服务治理中非常容易混淆的概念。断路器做的事情是自动进行服务熔断,将故障信息反馈给上游服务,而上游服务必须能够主动处理调用失败的后果,而不是坐视故障扩散。这里的“处理”,指的就是一种典型的服务降级逻辑。
服务降级不一定是在出现错误后才被动执行的,我们在很多场景中谈论的降级更可能是指,需要主动迫使服务进入降级逻辑的情况。
舱壁隔离模式
除了服务熔断和服务降级,服务治理中还有另一个很常见的概念:服务隔离。
舱壁隔离模式,是常用的实现服务隔离的设计模式。服务隔离,就是避免某一个远程服务的局部失败影响到全局,而设置的一种止损方案。这种思想,对应的就是容错策略中的沉默失败策略。
以一个Java Web应用为例,整个应用的所有接口共享的是Tomcat配置的线程池,一般在200到400之间。如果某个接口由于某些原因发生了超时的问题,如果调用者短时间内对这个接口发起了“疯狂”的请求,那么很快,整个应用的线程都会被这个接口“吃掉”。此时整个应用都被这个发生问题的接口拖累的无法对外提供服务了。
发生这种问题的根本原因在于,整个应用共享了一个线程池。如果针对各个接口,设置独立的线程池。这样因为某个接口产生的超时问题,便不会影响到整个应用。
但是,局部线程池有一个显著的弱点,那就是它额外增加了 CPU 的开销,每个独立的线程池都要进行排队、调度和下文切换工作。根据 Netflix 官方给出的数据,一旦启用 Hystrix 线程池来进行服务隔离,每次服务调用大概会增加 3~10 毫秒的延时。如果调用链中有 20 次远程服务调用的话,那每次请求就要多付出 60 毫秒至 200 毫秒的代价,来换取服务隔离的安全保障。
为应对这种情况,还有一种更轻量的控制服务最大连接数的办法,那就是信号量机制(Semaphore)。
如果不考虑清理线程池、客户端主动中断线程这些额外的功能,仅仅是为了控制单个服务并发调用的最大次数的话,我们可以只为每个远程服务维护一个线程安全的计数器,并不需要建立局部线程池。
服务隔离的思想不仅可以按照某个接口这样进行划分,可以根据各种各样的划分逻辑,站在更高的层次去保障整个系统的可用性。
重试模式
故障转移和故障恢复这两种策略都需要对服务进行重复调用,差别就在于这些重复调用有可能是同步的,也可能是后台异步进行;有可能会重复调用同一个服务,也可能会调用服务的其他副本。但是重点都在于重试。
重试模式适合解决系统中的瞬时故障,简单地说就是有可能自己恢复(Resilient,称为自愈,也叫做回弹性)的临时性失灵,比如网络抖动、服务的临时过载(比如返回了 503 Bad Gateway 错误)这些都属于瞬时故障。
实现重试很简单,如果是Spring应用,使用Spring Retry一个注解就可以搞定了,但是在实现重试模式时要注意:
- 是否有必要重试
- 是否应该重试
- 是否可以重试(幂等性)
- 何时终止(超时终止、次数终止)
流量控制
流量统计指标
- 每秒事务数(Transactions per Second,TPS):TPS是衡量信息系统吞吐量的最终标准。“事务”可以理解为一个逻辑上具备原子性的业务操作。
- 每秒请求数(Hits per Second,HPS):HPS是指每秒从客户端发向服务端的请求数。如果只要一个请求就能完成一笔业务,那HPS与TPS是等价的。
- 每秒查询数(Queries per Second,QPS):QPS是指一台服务器能够响应的查询次数。如果只有一台服务器来应答请求,那QPS和HPS是等价的。QPS和HPS相比,QPS是针对某个服务器来说的,HPS是站在客户端角度,是针对整个系统而言。
目前来说,主流系统大多倾向于使用 HPS 作为首选的限流指标,因为它相对容易观察统计,而且能够在一定程度上反映系统当前以及接下来一段时间的压力。
限流设计模式
限流也大致分为两类:否决式限流、阻塞式限流。前两种模式就适用于否决式限流。
流量计数器模式
设置一个计算器,根据当前时刻的流量计数结果是否超过阈值来决定是否限流。它的缺点是只是针对时间点进行离散的统计。比如一个80TPS的系统,可能在每一秒内都没有超过80TPS,将两秒的流量连续起来看,很有可能在中间时间段内超过了80TPS。
滑动时间窗模式
为了解决上面的问题,很自然想到可以实现平滑的基于时间片段统计。滑动时间窗口模式就是如此。和TCP的滑动窗口很像。
漏桶模式
漏桶在代码实现上非常简单,它其实就是一个以请求对象作为元素的先入先出队列(FIFO Queue),队列长度就相当于漏桶的大小,当队列已满时便拒绝新的请求进入。
漏桶实现起来很容易,困难在于如何确定漏桶的两个参数:桶的大小和水的流出速率。现实中系统的处理速度往往受到其内部拓扑结构变化和动态伸缩的影响,所以能够支持变动请求处理速率的令牌桶算法往往可能会是更受程序员青睐的选择。
令牌桶模式
它与漏桶一样都是基于缓冲区的限流算法,只是方向刚好相反,漏桶是从水池里往系统出水,令牌桶则是系统往排队机中放入令牌。当桶中的令牌没有了,就应该马上失败或进入服务降级逻辑。与漏桶类似,令牌桶同样有最大容量,这意味着当系统比较空闲时,桶中令牌累积到一定程度就不再无限增加,预存在桶中的令牌便是请求最大缓冲的余量。