欢迎来到啾啾的博客🐱。
这是一个致力于构建完善 Java 程序员知识体系的博客📚。
它记录学习点滴,分享工作思考和实用技巧,偶尔也分享一些杂谈💬。
欢迎评论交流,感谢您的阅读😄。
本篇主要讨论服务集群故障时的处理方案设计。
目录
引言
我们的微服务在经历过注册、通信、负载均衡之后,可以发挥出集群性能进行通信了。
但是很快,因为各种各样的原因如网络异常、程序异常、黑客攻击等,我们的部分服务挂掉了,服务集群高可用性随之降低了。
没挂掉也可能影响服务的性能、数据的持久化丢失等因性能下降或操作链时序混乱导致的一些列问题。
“故障无处不在”
当微服务成规模时,发生故障的可能是成倍增加的。在本系列的第2篇拓展阅读的“通过网络进行分布式运算的八宗罪”也有提到——网络是不可靠的。
那么,遇到异常导致服务故障时,我们应该怎么保证我们服务的高可用呢?
以下是微服务通信故障处理的核心要点及主流中间件的设计思想解析⬇️。
微服务通信的故障处理思路
要分析故障,首先需要理解故障的种类与造成的影响、期待处理后的目标,不清晰目标时可以反问倒推——“如果通信的服务发生故障时会发生什么”。
这里故障分为两种情况:
一种是集群整个宕掉,集群不可用。此时,重要的是保障发起通信的服务的可用。
一种是集群部分宕掉,集群部分不可用。此时,重要的是保障服务对外仍然可用。
这两种故障情况在没有处理时,都会导致调用服务的上游服务一定程度上出现故障,或是响应变慢,或是报错异常,这种导致调用链路的上游服务出现故障的现象也被称为“级联故障”或“雪崩”。
当然,故障都需要被检测、可以记录,这里暂且不论。
下面,我们就故障的两种情况一一分析处理。
故障情况一:集群整个不可用
图示是服务B崩溃导致服务A无法正常工作,在微服务架构中经常有这样的情况出现。
当这个链路边长,错误层层传播,便会造成服务雪崩。
对于此情况,需要保障对上游服务(服务A)的可用以保障系统整体可用,即,发起通信的服务不应该一直重复请求与等待宕掉的服务的响应。
这种情况下,需要有容错性设计来保证整体系统架构的可用,有以下的解决方案。
合适的超时时间
逐步的调试等待下游服务响应的时间,时间太长整个调用链路时间都会变长,时间太短,可能下游还正在处理会引发结果不一致。
如果完全没有超时时间,则会引起级联故障,整个系统宕掉都有可能。
设置和计算超时时,记得重试时间也计算进去哦。
不管采用什么架构方案,什么场景,都应该有超时时间。
熔断器Circuit Breaker(断路器)
熔断器的本质是完全阻断,一种隔离技术。主动卸载负载和快速失败,而不是排队。
设计概要
熔断器类似电路中的断路器,当服务的错误率超过阈值时,熔断器会打开,阻止进一步的请求,避免系统资源被耗尽。打开效果为快速失败 (Fail Fast),防止故障扩散。
那么,熔断器具体是怎么做的呢?
熔断器通常有3种状态:关闭Closed、打开Open(拦截所有发向下游的请求)、半开HalfOpen(拦截部分发向下游的请求)。
1.当请求下游服务发生一定数量(阈值)的失败后,熔断器打开;
2.熔断器打开时,不再向下游发送请求,服务快速失败;
3.在打开状态时,偶尔发送健康检查给下游,检查是否恢复;
4.恢复则重置状态。
- 熔断器中间件代表:Hystrix、Resilience4j
- 熔断器设计思想:
- 状态机:Closed(正常)→ Open(熔断)→ Half-Open(试探恢复)
- 阈值控制:基于内存计数器判断状态,无需远程协调。如Hystrix默认阈值为5秒内20次故障
- 自动恢复:熔断后周期性允许少量请求试探
- 零网络开销:不创建真实请求
熔断器的设计总的来说,是记录并计算请求状态,使用拦截的方式处理发向故障服务的请求,较每次请求都进行重试,拦截处理能显著减少开销。
其设计核心是服务状态探测,每一次请求都算做探测,不同的熔断器对其实现都不同,服务状态的探测会影响熔断器的开启、恢复。服务状态探测有可能遇到一种情况,即通信的集群也就波动一会,在熔断开始后立马恢复正常,这个时候需要提升探测的准确率。
探测与恢复方案
要实现可靠熔断恢复,建议:
- 采用渐进式恢复策略而非二进制开关
- 实现多维度的健康评估体系
- 引入异常类型识别提升判断准确率
- 在关键业务场景实施跨区域验证
后续做源码分析时展开。
服务降级(Fallback)
服务降级的本质是柔性处理而非完全阻断,可以继续尝试请求。目的是保证核心链路可用。
设计概要
同样是故障处理容错机制,服务降级较熔断相比,会尝试降级数据或采用其他服务链路。
服务降级有以下3种常见降级方式:
- 配置化静态降级
通过配置返回默认值(如"系统繁忙") ,这个效果和熔断类似。 - 动态降级
读取缓存旧数据(如商品详情页),使用动态场景需要业务支持,如商品详情页可以返回之前的缓存版本数据也可以。 - 异步化降级(慎用)
将请求写入队列延迟处理 ,以消息积压的方式进行处理。这种方式需要有消息队列的监控与补偿机制,如定时任务扫描。
服务降级设计起来没有熔断那么绝对——都返回失败。而是更柔和,可以直接返回失败,也可以使用动态降级这样体验稍微没那么好的方式继续服务。
熔断+降级
设计概要
实际生产中,仅采用熔断(拦截请求直接返回失败的设计)会使得业务的连续性变差、用户体验骤降。
服务降级用户体验好一些,但是服务降级还是每次都会去请求宕掉的服务,然后再基于失败的结果进行降级逻辑,开销并没有减少反而可能增加。
因此,实际生产中方案一般为熔断与服务降级融合,且设置多层降级。
方案简单来说,就是熔断开启后,使用服务降级。
在《微服务设计》一书中,作者建议“对所有同步的下游调用都使用断路器”即熔断,全面一点的说应该是熔断+服务降级。
Hystrix设计实现简单概述
以Hystrix为例,Hystrix的核心功能包括资源隔离、熔断、降级等。
其中资源隔离是日常控制流量的手段,主要有两种方式:线程池隔离和信号量隔离。
线程池隔离通过为每个依赖分配独立的线程池,避免因某个依赖的高延迟或故障耗尽所有线程资源,影响其他依赖。
比如在高并发下,线程池满了,请求被拒绝,触发降级,此时熔断器可能还未打开。
我们日常使用线程池时也应参考Hystrix这个为不同资源设置不同线程池的做法,做到保证核心服务的可用。
// HystrixCommand用于执行同步命令
public abstract class HystrixCommand<T> extends HystrixObservableCommand<T> {
protected HystrixCommand(Setter setter) {
super(setter);
}
@Override
protected Observable<T> construct() {
try {
return Observable.just(run());
} catch (Exception e) {
return Observable.error(e);
}
}
protected abstract T run();
protected T getFallback() {
// 实现服务降级逻辑
}
}
// 简化的熔断器核心逻辑
class HystrixCircuitBreaker {
enum State { CLOSED, OPEN, HALF_OPEN }
private AtomicInteger failureCount = new AtomicInteger(0);
private State state = State.CLOSED;
public boolean allowRequest() {
if (state == State.OPEN) {
return false; // 直接拒绝
} else if (state == State.HALF_OPEN) {
return Math.random() < 0.5; // 概率试探
}
return true;
}
public void recordSuccess() {
if (state == State.HALF_OPEN) {
state = State.CLOSED; // 恢复
}
failureCount.set(0);
}
public void recordFailure() {
if (failureCount.incrementAndGet() > threshold) {
state = State.OPEN; // 触发熔断
scheduleHalfOpen(); // 定时切换半开
}
}
}
Hystrix的对应类为com.netflix.hystrix包下的HystrixCircuitBreaker。
这一块尚有不清晰的地方,带我看完源码昨晚分析再论。
故障情况二:服务集群部分不可用
使用微服务架构的方式其实很大程度上可以避免情况一的出现。
情况二是集群内部部分服务不可用,对内来说健康服务实例需要处理的请求变多了,对外来说可能服务响应时间增加了,在《微服务设计》一书中,作者认为“处理系统缓慢要比处理系统快速失败困难得多”。
对于系统整体来说,突如其来的流量洪峰和集群内部部分服务不可用一样,都会导致健康服务实例需要处理的请求变多,说起流量,我们很自然的就能想到下面的方案,流量控制。
流量控制
当流量超出服务能处理的最大上限,服务的表现在上游看来就是如同崩溃,大量失败。
这个时候需要进行流量控制。
流量控制的三个问题
- 1.依据什么限流
什么流量需要限制,限制的粒度有多大,即限流的条件策略与期待效果是什么?这点同架构设计、同JVM调优一样,都需要结合实际运行状况,逐步调整。 - 2.具体如何限流
限流操作怎么抽象具现?服务限流算法和设计模式是怎样的? - 3.超额流量如何处理
被限制的流量,即请求,需要怎么处理?排队还是丢弃?
流量统计指标
流量控制的本质是拆分、控制流量,保护服务、平滑突发流量,核心对象就是流量(废话)。
要想针对流量进行控制,首先需要统计流量。
哪些指标能反映系统的流量压力大小呢?
- 每秒事务数-TPS (Transaction per Second)
TPS是衡量信息系统吞吐量的最终标准。每秒钟原子操作的次数。 - 每秒请求数-HPS (Hit per Second)
HPS从指客户端发向服务端的请求数,client->server的次数。
不等同于web点击数,一个web点击可能向server发送多个请求。 - 每秒查询数-QPS (Query per Second)
QPS是指一台服务器能够响应的查询次数。
流量控制多数时候是针对用户实际操作场景来限流的,因此HPS更适合作为限流指标。
限流设计模式(限流算法)
对于具体如何限流,也有一些常用的设计模式可以参考使用,本节将介绍流量计数器、滑动时间窗、漏桶和令牌桶四种限流设计模式。
-
1.流量计数器模式
做限流最容易想到的一种方法就是设置一个计算器,根据当前时刻的流量计数结果是否超过阈值来决定是否限流。譬如在前面的场景应用题中,我们计算得出该系统能承受的最大持续流量是80 TPS,那就可以通过控制任何一秒内的业务请求次数来限流,超过80次就直接拒绝掉超额部分。这种做法很直观,也确实有些简单的限流是这样实现的,但它并不严谨,以下两个结论就可以证明这个观点。
1)即使每一秒的统计流量都没有超过80 TPS,也不能说明系统没有遇到过大于80 TPS的流量压力。可以想象如下场景,如果系统连续两秒都收到60 TPS的访问请求,但这两个60 TPS请求分别是在前1秒里面的后0.5s,以及后1s中的前面0.5s所发生的。这样虽然每个周期的流量都不超过80 TPS请求的阈值,但是系统确实曾经在1s内发生了超过阈值的120 TPS请求。
2)即使连续若干秒的统计流量都超过了80 TPS,也不能说明流量压力就一定超过了系统的承受能力。可以想象如下场景,如果在10s的时间片段中,前3s TPS平均值到了100,而后7s的平均值是30左右,此时系统是否能够处理完这些请求而不产生超时失败呢?答案是可以的,因为条件中给出的超时时间是10s,而最慢的请求也能在8s左右处理完毕。如果只基于固定时间周期来控制请求阈值为80 TPS,反而会误杀一部分请求,导致部分请求出现原本不必要的失败。
流量计数器的缺陷根源在于它只是针对时间点进行离散的统计,为了弥补该缺陷,一种名为“滑动时间窗”的限流模式被设计出来,它可以实现平滑的基于时间片段统计。
-
2.滑动时间窗模式
滑动窗口算法(Sliding Window Algorithm)在计算机科学的很多领域中都有成功的应用,譬如编译原理中的窥孔优化(PeepholeOptimization)、TCP协议的阻塞控制(Congestion Control)等都使用到滑动窗口算法。
对分布式系统来说,无论是服务容错中对服务响应结果的统计,还是流量控制中对服务请求数量的统计,都经常要用到滑动窗口算法。
关于这个算法的运作过程,建议你发挥想象力,在脑海中构造如下场景:在不断向前流淌的时间轴上,漂浮着一个固定大小的窗口,窗口与时间一起平滑地向前滚动。任何时刻静态地通过窗口内观察到的信息,都等价于一段长度与窗口大小相等、动态流动中时间片段的信息。
由于窗口观察的目标都是时间轴,所以它被形象地称为“滑动时间窗模式”。
主要解决“流量计数器模式”按时间节点带来的固定边界限制。
以sentinel的滑动窗口为例,窗口的时间长度可以设置,窗口划分的格子数目也可以设置,格子越多精度越高,内存占用也越多。
图示为1s内划分5个格子——>6个格子。
- 3.漏桶模式
这里先解释一个专业术语,流量整形(Traffic Shaping):用于描述如何限制网络设备的流量突变,使得网络报文以均匀的速度向外发送。
流量整形通常都需要用到缓冲区来实现,当报文的发送速度过快时,首先在缓冲区中暂存,然后在控制算法的调节下均匀地发送这些被缓冲的报文。
常用的控制算法有漏桶算法(Leaky Bucket Algorithm)和令牌桶算法(Token Bucket Algorithm)两种,这两种算法的思路截然相反,但达到的效果又是相似的。
“漏桶”即一端进一端出,这个描述让人想起单向队列先进先出,FIFO Queue。队列的长度相当于“漏桶”的大小。
“漏桶”实现的关键在于“桶的大小”和“水的流出速率”,即改缓存多少请求,请求处理速度又该怎么确认。
- 4.令牌桶模式
“令牌桶”和“漏桶”一样,都是基于缓冲区的限流算法,只是方向刚好相反,“漏桶”是将请求先放置在缓冲区,“令牌桶”是在缓冲区放“准入令牌”,有“令牌”才能调用下游请求,当请求进入缓冲区发现没有令牌可以使用,则请求宣告失败或进入服务降级逻辑。
那么很明显,“令牌桶”的实现关键在于“令牌投放的数量”,令牌的数量由下游的处理时间决定。
分布式流量统计
在进行流量统计时,微服务本身很容易统计出流量指标TPS等,但是微服务架构中,一般调用服务是经过负载均衡面对服务集群的,又或者说,一些流量所请求的是整个服务链路。
那么,该怎么统计整个集群、或是服务链路的流量呢?
我们很容易想到使用第三方服务来进行统计,如使用集中式缓存Redis来实现统计数据的共享,并通过分布式锁、信号量等机制,解决这些数据读写访问时并发控制的问题。
但是这样每次统计都会有一次额外网络开销(只要集中存储统计信息,就不可避免地会产生网络开销)。
可以通过按照优先级给服务发放“令牌”的方式,保障优先级高的核心服务,一般发放令牌出为API网关。
Sentinel简单概述
前面有提过Hystrix的线程池隔离可以做一定程度上的流量控制,本质是服务自身通过编码形式控制具体依赖的流量,不具备扩展性、资源开销大且与业务耦合。
而且,控制的对想也只是API级的具体依赖。
下面以Sentinel为例,简单介绍“基于资源统计的多维度流量控制”。
- 基于资源统计的流控
Sentinel将服务或接口定义为“资源”,通过实时统计资源的请求量、响应时间等指标,动态调整流量控制策略。- 动态性:阈值可动态调整(如根据QPS、线程数、异常比例等)。
- 细粒度控制:支持按接口、方法、参数(如热点参数)等维度进行限流。
- 多维度流量控制算法
Sentinel支持多种限流算法,覆盖不同场景需求:- 直接拒绝(固定窗口):简单阈值触发后直接拒绝请求。
- 滑动窗口(Adaptive):通过滑动窗口算法统计流量,避免临界点流量突增。
- 令牌桶(Token Bucket):允许短时突发流量,但平均速率受控。
- 漏桶(Leaky Bucket):平滑流量,严格控制输出速率。
- 自适应熔断与降级
Sentinel不仅限流,还能根据异常比例自动熔断服务,并提供降级逻辑(如返回默认值)。- 动态熔断:熔断阈值可随系统负载动态调整。
关于Sentinel内容为AI生成,去官网做了简单验证。
总结
服务故障处理、微服务架构的容错是个很大的话题,有很多内容可以聊,不仅仅是超时、熔断、服务降级、流量控制,还有幂等、事务、补偿机制等等内容。
但是总的来说,我们要避免因微服务故障导致的服务雪崩,整个系统架构要自洽就就必须能容错,设计容错需要考虑资源利用、可用性、用户体验等等因素,也需要考虑后期维护,功能迭代的可拓展性。
原则上,我们认为服务必然会出错,但也认为,不是所有服务都重要。
拓展阅读
混沌工程(故障演练)
我们可以对我们的系统进行“故障发生”演练,演练项目中有一个著名的项目是“混乱猴子(Chaos Monkey)”。
Chaos Monkey 的原则:避免大多数失效的主要方式就是经常失效。失效一定会发生,并且无法避免。在大多数情况下,我们的应用设计要保证当服务的某个实例下线时仍能继续工作,但是在那些特殊的场景下,我们需要确保有人在值守,以便解决问题,并从问题中进行经验学习。基于这个想法,Chaos Monkey 仅会在工作时间内被使用,以保证工程师能发现警告信息,并作出适当的回应。
常见中间件的设计思想
中间件 | 核心设计思想 | 适用场景 |
---|---|---|
Hystrix | 线程池隔离、熔断降级 | Netflix系微服务 |
Sentinel | 实时监控、流量整形与系统自适应保护 | 阿里生态、高并发控流 |
Ribbon | 客户端负载均衡 + 超时重试 | 服务调用 |
Envoy | 服务网格边车代理 + 全局流量管控 | Istio服务网格 |
Kafka | 高吞吐消息持久化 + 事务消息 | 异步解耦与最终一致性 |
资料引用
《微服务设计》
《SpringCloud中文文档》
《凤凰架构:构建可靠的大型分布式系统》
Hystrix官方wiki