上一章我们梳理了微服务下的全链路日志,接下来我们聊聊每个微服务系统都躲不开的第二个关键环节——熔断。你可能会想:熔断不是高并发大流量时才用得上的吗?前面提到的业务场景看起来流量并不惊人,这还需要考虑熔断吗?其实啊,这是一个挺常见的认知偏差,实际情况并非如此。
在展开讨论之前,我们先简单说明一下所涉及的业务场景。
1 业务场景:如何预防一个服务故障影响整个系统
在一个典型的新零售系统架构里,存在一个通用用户服务——它就像很多关键页面的“水电煤”,使用频率极高。该服务主要提供两个核心接口,而它们各自都带着需要警惕的“小脾气”。
接口一:用户状态查询接口
- 功能:获取用户状态,其中包含诸如用户车辆实时位置等动态信息。
- 出场场景:所有需要展示用户信息页面的地方,例如客服系统里的用户详情页,它都会频繁登场。
- 潜在特点:调用量大,属于基础信息展示。
接口二:用户权限列表接口
- 功能:返回一个针对当前用户的、可操作权限清单。这份列表既有通用标配权限,也包含用户的个性化定制权限。
- 出场场景:每次用户打开App的启动环节,几乎都会调用它,决定了用户能看到和操作什么。
- 潜在特点:位于关键路径(启动链路上),且逻辑较复杂。
正是这两个接口不同的“脾性”和重要地位,让它们可能遭遇不同的问题。下面我们就来分别拆解。
1.1 问题一:请求慢
用户状态接口的调用链路如图所示。问题的症结在于:Basic Data Service 中的 /currentCarLocation 接口,需要调用一个第三方系统来获取车辆位置数据。
这个第三方服务时不时“闹点脾气”出故障,导致响应时间更加不可预测。其结果就是,我们的接口频繁超时、报错。
但这还不是最糟糕的。有一次,用户集体投诉App慢到令人无法忍受。运维人员紧急介入,查看了几个 Thread Dump(线程转储),发现了一个可怕的场景:
User API 和 Basic Data Service 的可用线程数几乎被耗尽,而所有这些线程,竟然全部卡在等待那个慢吞吞的第三方接口上。
由于连接池和线程池被完全占满,没有多余的资源来处理其他任何请求。于是,一个慢速的外部依赖,成功让整个App的页面都陷入了卡顿——这就好比高速公路因为一辆车抛锚,导致了全线大瘫痪。问题的严重性,从一次局部故障升级为全局性雪崩。

此前,运维同事针对接口响应慢的问题,采取了一个直观的“缓兵之计”:大幅调长超时时间。这招确实立竿见影——超时错误提示变少了,其他页面也看似正常。但代价是,所有调用这个慢接口的地方(比如客服后台查看用户信息)都会等得更久,用户体验像陷入了泥潭。
1.2 问题二:流量洪峰缓存超时
用户权限的接口、服务间的调用关系与上面类似,如图10-2所示。服务间的调用流程具体分为以下3个步骤。

1)APP访问User API。
2)User API访问Basic Data Service接口/commonAccesses。
3)Basic Data Service提供一个通用权限列表。因为权限列表对所有用户都一样,所以把它放在了Redis中,如果通用权限在Redis中找不到,再去数据库中查找。
接下来聊聊服务间的调用流程中笔者遇到过的一些问题。有一次,因为历史代码的原因,在流量高峰时Redis中的通用权限列表超时了,那一瞬间所有的线程都需要去数据库中读取数据,导致数据库的CPU使用率升到了100%。
数据库崩溃后,紧接着Basic Data Service也停了,因为所有的线程都堵塞了,获取不到数据库连接,导致Basic Data Service无法接收新的请求。
而User API因调用Basic Data Service的线程而出现了堵塞,以至于User API服务的所有线程都出现堵塞,即User API也停止工作,使得App上的所有操作都不能使用,后果比较严重。
2 覆盖场景
为了解决以上两个问题,需要引入一种技术,这种技术还要满足以下两个条件。
1.线程隔离
首先针对第一个问题进行举例说明。假设User API中每个服务配置的最大连接数是1000,每次API调用Basic Data Service的/currentCarLocation时速度会很慢,所以调用/currentCarLocation的线程就会很慢,一直不释放。那么原因可能是,User API这个服务中的1000个连接线程全部都在调用/currentCarLocation这个服务。这就像一艘船的底舱破了个洞,进水却会蔓延到所有船舱,导致整船沉没。
因此,我们需要的,是给这个慢接口单独隔出一个“小舱室”。希望控制/currentCarLocation的调用请求数,保证不超过50条,以此保证至少还有950条连接可用于处理常规请求。如果请求超过50个,则可以快速失败并返回兜底结果(如给用户一个友好提示),避免排队等待。
2.熔断(快速失败与恢复)
针对第二个问题,当时数据库本身并无死锁,只是因瞬间压力过大而“喘不过气”。理想的情况是:当 Basic Data Service 发现下游数据库异常或自身线程池快被占满时,能主动、迅速地“熔断”。
- “断”:暂时停止接收新的请求(或立即返回降级结果),给下游服务(数据库)一个喘息的机会,让缓存得以重新填充,让连接数降下来。
- “探”:稍后,再智能地尝试放少量请求过去,探测下游是否已恢复。如果恢复了,则逐步闭合电路,恢复正常调用。
总结一下,这套机制的核心逻辑就两点:
异常不访问:当发现某个接口近期频繁出错(如超时、抛异常),系统应能敏锐察觉,并暂时停止调用它,避免做无用功并拖垮自己。
超时不硬等:当发现某个接口响应时间持续异常,应能判断其可能已不堪重负,转而快速执行备用方案(如返回缓存旧数据、默认值或提示信息),而不是让线程无限期等待。
简单说,它的行为准则就是:“惹不起,躲得起;等情况好了,我再回来试试。” 这,便是熔断与隔离的精髓。了解了这些需求,我们接下来就可以有的放矢地进行技术选型了。
3 Sentinel和Hystrix
目前可以解决以上需求的比较流行的开源框架有两个:一个是Netflix开源的Hystrix,Spring Cloud默认使用这个组件;另一个是阿里开源的Sentinel。两者的对比见表。

在这里插一句题外话,有些同学总是觉得限流和熔断极为类似分不清楚,这里给出一些核心特征的区别
| 特性 | 熔断 | 限流 |
|---|---|---|
| 核心目标 | 故障隔离与恢复 | 流量整形与过载保护 |
| 触发条件 | 错误率、超时率 | QPS、并发数 |
| 行为 | 状态切换(开/关/半开) | 直接拒绝/排队/延迟 |
| 关键作用 | 避免雪崩、快速失败 | 平滑流量、防止资源耗尽 |
回到正题,这两个框架都能满足需求,但项目组最终使用了Hystrix,具体原因如下。
1)满足需求。
2)团队里有人用过Hystrix,并通读了它的源代码。
3)它是Spring Cloud默认自带的,项目组很多人都看过相关文档。
4 Hystrix的设计思路
4.1 线程隔离机制
在微服务架构中,服务间常存在强依赖调用。Hystrix的核心设计之一,便是为这类每个关键依赖建立独立的资源隔离区。如图所示,例如当前服务调用外部接口A时,其最大并发线程数被限制为10;而调用接口M时,则被限制为5。

若不进行隔离,当某个依赖接口响应变慢时,处理请求的线程会因等待其响应而被大量占用且无法释放。这将迅速耗尽服务的整体连接线程池,导致其他正常请求也无法处理,引发系统级阻塞。
为此,Hystrix的解决方案是:为每个依赖接口(或可共享的一组接口)单独维护一个受限的线程池。通过线程池大小、队列长度等参数,严格限制对每个依赖的并发调用量。这样,即使接口A的线程池被慢请求占满,也不会影响服务内其他线程资源,从而保障系统其他部分的可用性。
除了线程池隔离,Hystrix还提供了一种更轻量级的方案:信号量隔离。同样以限制并发数10为例,信号量模式并非维护一个包含10个线程的池子,而是使用一个计数器(如semaphoresA)。在每次调用接口A前执行semaphoresA++(申请许可),调用完成后执行`semaphoresA--(释放许可)。一旦计数器值超过10,后续请求将立即被拒绝,而无需等待。
两种模式的选型考量如下:
- 线程池隔离的缺点在于存在线程切换的开销,资源消耗相对较高。
- 信号量隔离的优势正是开销极低、速度极快,因为它不涉及线程切换。但其有一个重要缺陷:一旦调用开始便无法被中断。这是因为在信号量模式下,执行远程调用的就是请求本身的线程,而非像线程池模式那样由专门的线程池线程负责。在线程池模式下,主请求线程可以设置超时并中断隔离线程;而在信号量模式下,调用线程自身被阻塞后,则无法从外部强制取消。
通过引入上述线程隔离机制,我们有效解决了第一个核心问题:确保单个下游依赖的故障或延迟,不会耗尽当前服务的所有连接资源。然而,如果某个依赖接口不仅慢,而且持续失败,我们是否应该让所有请求继续尝试并快速失败?这就需要一个更高级的、具备状态判断的机制——熔断机制。
4.2 熔断机制
1.在哪种条件下会触发熔断
熔断判断规则是某段时间内调用失败数超过特定的数量或比例时,就会触发熔断。那这个数据是如何统计出来的呢?
在Hystrix机制中,会配置一个不断滚动的统计时间窗口metrics.rollingStats.timeInMilliseconds,在每个统计时间窗口中,若调用接口的总数量达到circuitBreakerRequestVolumeThreshold,且接口调用超时或异常的调用次数与总调用次数之比超过circuitBreakerErrorThresholdPercentage,就会触发熔断。
2.熔断了会怎么样
如果熔断被触发,在circuitBreakerSleepWindowInMilliseconds的时间内,便不再对外调用接口,而是直接调用本地的一个降级方法,代码如下所示。
/* by 01130.hk - online tools website : 01130.hk/zh/color.html */
@HystrixCommand (fallbackMethod ="getCurrentCarLocationFallback")
3.熔断后怎么恢复
到达circuitBreakerSleepWindowInMilliseconds的时间后,Hystrix首先会放开对接口的限制(断路器状态为HALF-OPEN),然后尝试通过一个请求,如果调用成功,则恢复正常(断路器状态为CLOSED),如果调用失败或出现超时等待,就需要重新等待circuitBreakerSleepWindowInMilliseconds的时间,之后再重试。
4.3 滚动(滑动)时间窗口
Hystrix的熔断判断依赖于对近期请求结果的精确统计。其采用的滚动时间窗口机制,绝非简单的定时快照。
举个例子,若将滚动时间窗口设置为10秒,这并不意味着系统只在每分钟的第10秒、20秒进行统计。相反,它需要持续不断地统计任何时刻为止的、最近10秒内的数据。
为了实现这种持续滚动的统计,Hystrix引入了“桶”的概念。通过配置 metrics.rollingStats.numBuckets(例如设为10),将整个时间窗口(10秒)划分为10个连续的、时长相等的小区间(每个桶代表1秒)。
其运作方式如图所示:系统会维护一个按时间推进的桶队列。

- 在
1分0秒~1分10秒这个区间统计一次。 - 紧接着,在
1分1秒~1分11秒这个区间再统计一次。 - 随后是
1分2秒~1分12秒……以此类推。
实际上,系统每秒钟都会生成一个基于最新10个桶(即最近10秒)的聚合统计数据。
在每个独立的桶内,Hystrix会分别记录该秒内发生的请求成功数、失败数、超时数和被拒绝数。当进行统计时,系统会自动累加最近10个桶(即一个完整时间窗口)内的各类计数。当第11个桶的数据产生时,最旧的第1个桶的数据将被排除在统计之外,计算范围随之滚动到第2至第11个桶,始终保持对最近10秒状态的跟踪。
基于这套精确的统计机制,我们便能清晰地梳理Hystrix处理每次请求的完整决策流程。
4.4 Hystrix调用接口的请求处理流程
当你的代码发起一个被Hystrix托管的调用时,它会经历一套设计精巧的决策流程,其严谨程度堪比机场安检。无论是成功还是失败,大部分检查步骤都是共通的,我们将其合并梳理以便理解。
通用流程 (步骤1-5):
- 封装命令:首先,将你的请求意图(调用哪个接口、参数是什么)封装成一个
HystrixCommand对象。这是后续所有管控的起点。 - 执行命令:开始执行这个封装好的命令。
- 请求缓存检查(可选):如果启用了请求缓存(Request Cache),Hystrix会先尝试用相同的参数从缓存中直接获取结果。若命中,则立刻返回,省去后续所有步骤。
- 熔断器状态检查:这是第一道关键“闸门”。系统会检查针对该依赖的断路器是否已打开。如果已打开(处于熔断状态),则流程直接短路,跳至降级方法
(fallback方法),不再尝试真实调用。 - 资源隔离检查:这是第二道“闸门”。根据配置的隔离机制(线程池或信号量),判断当前是否有可用的资源(如线程池是否有空闲线程、信号量是否有剩余许可)。如果资源已满,请求会被立即拒绝,同样跳至降级方法
(fallback方法),并记录一次“拒绝数”。
至此,所有快速失败路径结束。若能通过以上检查,请求才被允许尝试真正的远程调用。
分叉路径:
- 路径一:调用成功
- 执行真实调用:在隔离的线程或信号量管控下,发起对依赖接口的实际网络调用。
- 上报成功:调用成功返回后,除了将结果返回给调用方,Hystrix还会向断路器报告一次成功,并在当前滚动时间窗口的统计桶中增加成功计数。这有助于熔断器判断是否应恢复闭合。
- 路径二:调用失败(超时或异常)
- 执行真实调用:同上,发起真实调用。
- 上报失败并判断:当调用发生超时或抛出异常时,系统会上报一次失败,并更新统计窗口。此时会执行核心逻辑:判断最新的失败率等指标是否已达到预设的熔断阈值。如果满足条件,则会立即打开断路器,以便在短期内保护系统。
- 执行降级:无论断路器是否因此次失败被打开,最终都会执行预设的降级方法
(fallback方法),向主调方返回一个可控的备用结果。
理解这套流程后,在Spring Cloud等框架中集成Hystrix就变得直观了(具体集成步骤此处不展开)。此外,Hystrix还提供了requestcaching(请求缓存)和requestcollapsing(请求合并) 等提升性能的高级功能,鉴于它们与熔断核心逻辑相对独立,我们在此不作深入探讨。
5 注意事项
引入熔断,实质上是引入了受控的、策略性的失败。这带来了新的设计挑战,必须在架构层面予以考量。
5.1 数据一致性
熔断降级可能破坏跨服务的操作原子性。考虑以下场景:
- 简单场景:服务A在本地数据库更新成功后,调用服务B时触发熔断并降级。此时,服务A已完成的数据库更新是否需要回滚?
- 链式场景:服务A更新DB后调用服务B成功,服务B继续调用服务C时触发熔断降级。问题更复杂:服务B应向服务A返回成功还是失败?服务A的DB更新又该如何处置?
核心洞察:这本质是分布式事务问题,没有普适的解决方案。设计取决于业务语义。常见思路包括:
- 最终一致性:通过异步补偿、对账或事务消息机制,在后期修复状态。
- 强一致性尝试:将关键步骤封装为Saga等长事务,或在降级时选择回滚,但这可能牺牲可用性。
- 业务折中:评估操作是否可以接受中间状态,或通过设计避免此类跨服务写事务。
5.2 超时降级
这是一个典型陷阱:服务A调用服务B,因超时触发熔断并执行降级。然而,服务B的线程并未中止,它可能最终会处理成功。这将导致:
- 服务A认为失败,使用了降级逻辑或提示用户失败。
- 服务B侧却成功变更了状态。
结果是双方状态不一致。这再次印证了熔断场景下,数据一致性是需要首要设计的核心问题。
5.3 用户体验
触发熔断后,用户端体验必须被妥善处理,不能仅仅满足于“服务没宕机”。通常有以下三类情况:
- 读操作降级:部分数据无法获取。应在UI上做到无感降级(如隐藏相关模块、显示默认值)或友好提示(“信息暂不可用”),避免页面错误或空白。
- 写操作转异步:请求被接收并转为后台异步处理。必须向用户提供明确预期,如提示“请求已提交,正在处理中”,而非“操作失败”。
- 写操作被放弃/回滚:操作确实无法执行。必须清晰、及时地告知用户“操作未成功,请稍后重试”,并提供重试途径。
因此,服务调用触发了熔断降级时需要把这些情况都考虑到,以此来保证用户体验,而不是仅仅保证服务器不宕机。
5.4 熔断监控
Hystrix是一个基于静态阈值的事前配置框架。参数(如超时时间、错误比例阈值)是否合理,必须通过生产流量验证。因此,上线后必须结合其监控面板,持续观察各服务的熔断次数、请求量、延迟百分位数、线程池使用率等核心指标。只有通过数据驱动的持续调优,才能使熔断机制精准发挥作用,避免误伤或保护不足,真正将系统损失降至最低。
6 小结
在项目中引入Hystrix后,两个核心问题迅速得到解决:
- 通过线程隔离,下游依赖的延迟或故障被限制在独立资源池内,不再会拖垮整个服务。
- 通过熔断机制,对持续故障的下游依赖进行快速断路,防止请求积压和故障蔓延,避免了级联雪崩。
系统因此获得了显著的弹性。然而,Hystrix存在一个固有局限:其效果高度依赖于对流量和系统容量的精准事前预测和参数配置。当实际情况偏离预测时,其保护效果会打折扣。这正是一直以来的主要运维负担——需要根据监控反复调整参数。
也正是由于这一局限性,其创造者Netflix乃至整个社区都在寻求更动态、更自适应的解决方案。这推动了如Resilience4j等新一代容错库的发展。Hystrix自2018年起已进入维护模式,这标志着静态配置熔断时代逐步向更智能的动态系统演进。
尽管具体技术在迭代,但熔断的思想和核心原理已成为分布式系统的基石。理解其如何通过隔离、断路、降级和统计来构建韧性,远比掌握某个特定库的API更重要。本章旨在厘清这些基本原理,为深入探索更现代的容错模式打下基础。
既然熔断是构建高可用服务的核心策略之一,那么另一个与其紧密相关、面试中同样高频出现的主题——限流,自然不可或缺。接下来,我们将探讨如何为系统设置合理的流量“闸门”。
1486

被折叠的 条评论
为什么被折叠?



