嘉峪关市网站建设_网站建设公司_React_seo优化
2025/12/30 10:37:07 网站建设 项目流程

上一章我们梳理了微服务下的全链路日志,接下来我们聊聊每个微服务系统都躲不开的第二个关键环节——熔断。你可能会想:熔断不是高并发大流量时才用得上的吗?前面提到的业务场景看起来流量并不惊人,这还需要考虑熔断吗?其实啊,这是一个挺常见的认知偏差,实际情况并非如此。

在展开讨论之前,我们先简单说明一下所涉及的业务场景。

1 业务场景:如何预防一个服务故障影响整个系统

在一个典型的新零售系统架构里,存在一个通用用户服务——它就像很多关键页面的“水电煤”,使用频率极高。该服务主要提供两个核心接口,而它们各自都带着需要警惕的“小脾气”。

接口一:用户状态查询接口

  • 功能:获取用户状态,其中包含诸如用户车辆实时位置等动态信息。
  • 出场场景:所有需要展示用户信息页面的地方,例如客服系统里的用户详情页,它都会频繁登场。
  • 潜在特点:调用量大,属于基础信息展示。

接口二:用户权限列表接口

  • 功能:返回一个针对当前用户的、可操作权限清单。这份列表既有通用标配权限,也包含用户的个性化定制权限。
  • 出场场景:每次用户打开App的启动环节,几乎都会调用它,决定了用户能看到和操作什么。
  • 潜在特点:位于关键路径(启动链路上),且逻辑较复杂。

正是这两个接口不同的“脾性”和重要地位,让它们可能遭遇不同的问题。下面我们就来分别拆解。

1.1 问题一:请求慢

用户状态接口的调用链路如图所示。问题的症结在于:Basic Data Service 中的 /currentCarLocation 接口,需要调用一个第三方系统来获取车辆位置数据

这个第三方服务时不时“闹点脾气”出故障,导致响应时间更加不可预测。其结果就是,我们的接口频繁超时、报错。

但这还不是最糟糕的。有一次,用户集体投诉App慢到令人无法忍受。运维人员紧急介入,查看了几个 Thread Dump(线程转储),发现了一个可怕的场景:
User APIBasic 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。两者的对比见表。

Sentinel和Hystrix对比

在这里插一句题外话,有些同学总是觉得限流和熔断极为类似分不清楚,这里给出一些核心特征的区别

特性 熔断 限流
核心目标 故障隔离与恢复 流量整形与过载保护
触发条件 错误率、超时率 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的时间内,便不再对外调用接口,而是直接调用本地的一个降级方法,代码如下所示。

@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):

  1. 封装命令:首先,将你的请求意图(调用哪个接口、参数是什么)封装成一个 HystrixCommand 对象。这是后续所有管控的起点。
  2. 执行命令:开始执行这个封装好的命令。
  3. 请求缓存检查(可选):如果启用了请求缓存(Request Cache),Hystrix会先尝试用相同的参数从缓存中直接获取结果。若命中,则立刻返回,省去后续所有步骤。
  4. 熔断器状态检查:这是第一道关键“闸门”。系统会检查针对该依赖的断路器是否已打开。如果已打开(处于熔断状态),则流程直接短路,跳至降级方法(fallback方法),不再尝试真实调用。
  5. 资源隔离检查:这是第二道“闸门”。根据配置的隔离机制(线程池或信号量),判断当前是否有可用的资源(如线程池是否有空闲线程、信号量是否有剩余许可)。如果资源已满,请求会被立即拒绝,同样跳至降级方法(fallback方法),并记录一次“拒绝数”。

至此,所有快速失败路径结束。若能通过以上检查,请求才被允许尝试真正的远程调用。

分叉路径:

  • 路径一:调用成功
    1. 执行真实调用:在隔离的线程或信号量管控下,发起对依赖接口的实际网络调用。
    2. 上报成功:调用成功返回后,除了将结果返回给调用方,Hystrix还会向断路器报告一次成功,并在当前滚动时间窗口的统计桶中增加成功计数。这有助于熔断器判断是否应恢复闭合。
  • 路径二:调用失败(超时或异常)
    1. 执行真实调用:同上,发起真实调用。
    2. 上报失败并判断:当调用发生超时或抛出异常时,系统会上报一次失败,并更新统计窗口。此时会执行核心逻辑:判断最新的失败率等指标是否已达到预设的熔断阈值。如果满足条件,则会立即打开断路器,以便在短期内保护系统。
    3. 执行降级:无论断路器是否因此次失败被打开,最终都会执行预设的降级方法(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 用户体验

触发熔断后,用户端体验必须被妥善处理,不能仅仅满足于“服务没宕机”。通常有以下三类情况:

  1. 读操作降级:部分数据无法获取。应在UI上做到无感降级(如隐藏相关模块、显示默认值)或友好提示(“信息暂不可用”),避免页面错误或空白。
  2. 写操作转异步:请求被接收并转为后台异步处理。必须向用户提供明确预期,如提示“请求已提交,正在处理中”,而非“操作失败”。
  3. 写操作被放弃/回滚:操作确实无法执行。必须清晰、及时地告知用户“操作未成功,请稍后重试”,并提供重试途径。

因此,服务调用触发了熔断降级时需要把这些情况都考虑到,以此来保证用户体验,而不是仅仅保证服务器不宕机。

5.4 熔断监控

Hystrix是一个基于静态阈值的事前配置框架。参数(如超时时间、错误比例阈值)是否合理,必须通过生产流量验证。因此,上线后必须结合其监控面板,持续观察各服务的熔断次数、请求量、延迟百分位数、线程池使用率等核心指标。只有通过数据驱动的持续调优,才能使熔断机制精准发挥作用,避免误伤或保护不足,真正将系统损失降至最低。

6 小结

在项目中引入Hystrix后,两个核心问题迅速得到解决:

  1. 通过线程隔离,下游依赖的延迟或故障被限制在独立资源池内,不再会拖垮整个服务。
  2. 通过熔断机制,对持续故障的下游依赖进行快速断路,防止请求积压和故障蔓延,避免了级联雪崩。

系统因此获得了显著的弹性。然而,Hystrix存在一个固有局限:其效果高度依赖于对流量和系统容量的精准事前预测和参数配置。当实际情况偏离预测时,其保护效果会打折扣。这正是一直以来的主要运维负担——需要根据监控反复调整参数。

也正是由于这一局限性,其创造者Netflix乃至整个社区都在寻求更动态、更自适应的解决方案。这推动了如Resilience4j等新一代容错库的发展。Hystrix自2018年起已进入维护模式,这标志着静态配置熔断时代逐步向更智能的动态系统演进。

尽管具体技术在迭代,但熔断的思想和核心原理已成为分布式系统的基石。理解其如何通过隔离、断路、降级和统计来构建韧性,远比掌握某个特定库的API更重要。本章旨在厘清这些基本原理,为深入探索更现代的容错模式打下基础。

既然熔断是构建高可用服务的核心策略之一,那么另一个与其紧密相关、面试中同样高频出现的主题——限流,自然不可或缺。接下来,我们将探讨如何为系统设置合理的流量“闸门”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询