每天一道题头像
微服务为什么会发生级联故障?从 Timeout、Retry 到熔断、限流与 Backpressure封面图

微服务为什么会发生级联故障?从 Timeout、Retry 到熔断、限流与 Backpressure

前面我们已经学习了微服务之间如何通过 RPC 通信,以及 Kafka 如何让不同服务异步协作。

在正常情况下,这些机制都比较容易理解。

例如用户提交订单:

Client
   |
   v
Gateway
   |
   v
Order Service
   |
   v
Inventory Service
   |
   v
MySQL

Order Service 调用 Inventory Service,检查库存并预留商品。

Inventory Service 返回成功以后,Order Service 继续创建订单。

但假设某一天,Inventory Service 的 MySQL 查询突然变慢了。

原本只需要几十毫秒的库存请求,现在需要几秒才能返回。

这时候 Order Service 应该怎么办?

最直接的做法是设置超时时间。如果请求超时,就重新调用一次。

但问题是:如果下游服务本来就处理不过来,继续重试会不会让它变得更慢?

更麻烦的是,Order Service 在等待 Inventory Service 时,也会消耗自身的线程、连接或者并发执行资源。

如果大量请求都在等待,Order Service 也可能逐渐无法处理新的订单。

最后,故障会从 MySQL 扩散到 Inventory Service,再扩散到 Order Service,甚至影响 Gateway。

这种现象就是微服务中的级联故障(Cascading Failure)。

它通常不是因为所有服务同时发生了故障,而是一个局部问题通过调用链、资源等待和流量放大不断向上传播。

这一篇我们就围绕这条故障链,依次理解 Timeout、Retry、Circuit Breaker、Rate Limiting、Bulkhead 和 Backpressure。


一、为什么一个服务变慢,会影响整条调用链?

假设订单系统有三个服务:

Order Service
      |
      v
Inventory Service
      |
      v
MySQL

正常情况下:

Inventory Service:

平均响应时间 = 20ms

Order Service 发起库存预留请求,很快就能得到结果。

但现在 MySQL 出现慢查询。

库存服务的响应时间变成:

Inventory Service:

响应时间 = 3s

从 Inventory Service 自身来看,它可能只是处理速度下降了。

但对于 Order Service 来说,影响可能更大。

1. 等待本身也会消耗资源

假设 Order Service 使用同步 RPC 调用:

请求进入 Order Service
          |
          v
调用 Inventory Service
          |
          v
等待响应
          |
          v
继续处理订单

在等待期间,执行该请求的线程或者其他并发资源可能一直被占用。

假设 Order Service 每秒接收 200 个请求。

正常情况下,库存调用平均只需要 20ms。

但如果库存调用持续需要 3 秒,那么同时等待库存响应的请求数量就可能显著增加。

即使 Order Service 自身的业务逻辑没有变慢,它也会因为大量请求等待下游而积累并发压力。

这里可以联系一个常见的性能关系:

当请求到达速率保持不变,而请求在系统中的停留时间增加时,系统内同时存在的请求数量通常也会增加。

这就是为什么下游延迟上升,会影响上游的线程池、连接池和请求队列。

2. 等待资源耗尽后会发生什么?

例如 Order Service 只有一定数量的工作线程。

Order Service

Worker 1 → 等待库存
Worker 2 → 等待库存
Worker 3 → 等待库存
Worker 4 → 等待库存
...

当大量线程都处于等待状态时,新请求可能只能进入队列。

新订单请求
     |
     v
等待队列
     |
     v
可用 Worker 不足

随着队列积压:

请求等待时间上升
       |
       v
客户端超时
       |
       v
部分客户端重试
       |
       v
更多请求进入系统

最终 Order Service 也开始变慢。

此时即使 Inventory Service 恢复了一部分能力,上游已经积累的请求、超时和重试仍然可能继续造成压力。

所以级联故障的核心是:

一个服务的延迟问题,通过资源占用和请求积压,逐渐转变成多个服务的容量问题。

3. 为什么增加实例不一定立即解决?

我们之前学习 Kubernetes HPA 时,讨论过根据 CPU 等指标增加 Pod 数量。

假设 Order Service 出现大量等待。

HPA 将其从三个 Pod 扩容到六个 Pod。

这可能提高 Order Service 的并发处理能力。

但如果真正的瓶颈在 Inventory Service 或 MySQL,那么新增的 Order Service 实例也可能向下游发送更多请求。

于是:

Order Service 扩容
        |
        v
上游并发能力增加
        |
        v
更多请求进入 Inventory Service
        |
        v
MySQL 压力进一步增加

所以 HPA 解决的是工作负载容量扩展问题,不能代替下游流量控制。

理解了故障传播以后,我们再讨论最基本的保护机制:Timeout。


二、Timeout:为什么所有远程调用都应该有时间边界?

假设 Order Service 调用 Inventory Service:

POST /inventory/reserve

请求内容:

{
  "request_id": "R10001",
  "order_id": 10001,
  "product_id": 1001,
  "quantity": 1
}

如果 Inventory Service 一直没有响应,Order Service 不能无限等待。

否则线程、连接或者其他请求资源可能长期被占用。

因此,需要为远程调用设置超时。

例如:

Inventory RPC Timeout = 2 秒

如果超过相应时间边界,调用方就结束等待,并进入超时处理流程。

1. Timeout 的作用是什么?

Timeout 主要解决:

一次远程调用最多允许占用多少等待时间。

它能够限制单次调用的等待范围,防止请求无限期挂起。

但需要注意,实际工程中可能有不同类型的超时:

类型主要限制
Connect Timeout建立网络连接的等待时间
Read Timeout等待读取响应数据的时间
Overall Timeout / Deadline整次调用或请求链路允许的总时间

其中,Read Timeout 和 Overall Timeout 不能简单混为一谈。

例如某些读取超时配置可能限制的是两次网络读取之间的等待时间,而不是整次业务调用的总耗时。

因此,重要的业务请求通常还需要一个明确的整体时间预算。

2. Timeout 不等于操作失败

这是我们之前学习 Kafka、Outbox 和分布式锁时,多次讨论过的一个结论。

假设库存预留请求已经到达 Inventory Service。

Order Service              Inventory Service
      |                            |
      |----- ReserveStock -------->|
      |                            |
      |                        MySQL 扣减成功
      |                            |
      |                        COMMIT 成功
      |                            |
      |<------ Timeout -----------X

Order Service 看到的是:

ReserveStock Timeout

但 Inventory Service 实际上已经完成了库存预留。

因此:

调用超时只能说明调用方没有在规定时间内获得确定结果,不能证明远端操作没有执行。

如果 Order Service 直接重新调用一次库存扣减:

第一次:库存已经扣减

第二次:重新扣减

就可能产生重复业务副作用。

所以对于修改状态的远程操作,尤其需要小心处理超时。

3. 超时以后应该怎么做?

首先要判断操作属于哪一类。

例如:

GET /products/1001

这是普通查询。

超时以后,如果业务允许,可以安全地尝试有限重试。

但对于:

POST /inventory/reserve

这种可能修改业务状态的操作,不能简单假设它失败了。

可以使用稳定的业务请求标识:

request_id = R10001

让库存服务根据该标识识别重复操作。

同时提供状态查询能力:

GET /inventory/reservations/R10001

超时后,Order Service 可以:

调用库存预留超时
        |
        v
查询 R10001 的执行状态
        |
   +----+----+-----------+
   |         |           |
   v         v           v
已成功    明确失败     状态未知
   |         |           |
   v         v           v
继续流程   业务处理    保留待确认状态

如果状态仍然未知,就需要根据幂等与恢复策略决定是否重新发起操作。

这里的关键不是“超时必须先查询一次”,而是:

不能把结果未知直接解释为操作未执行。


三、Retry:为什么重试有时能恢复故障,有时却会放大故障?

重试(Retry)是微服务中非常常见的恢复机制。

例如:

Order Service
      |
      v
Inventory Service
      |
      X
临时网络错误

如果只是一次短暂网络抖动,稍后重新发送请求可能就会成功。

所以重试本身不是错误设计。

问题在于:不是所有失败都值得重试,也不是任何时刻都应该立即重试。

1. 先区分临时性错误和确定性错误

例如:

网络短暂中断

可能适合重试。

服务暂时返回 503

在具备恢复条件且受到流量控制时,可以考虑稍后重试。

但:

库存不足

通常是明确的业务失败。

重复请求并不会凭空增加库存。

又例如:

参数格式错误

如果不修改参数,重试一百次仍然可能失败。

因此,重试前首先应该识别失败类型。

失败情况是否适合直接重试
短暂连接失败可考虑有限重试
下游暂时不可用结合负载与重试策略
请求超时且执行状态未知先考虑幂等与状态确认
参数错误通常不应重试
业务条件不满足通常不应盲目重试
明确的资源冲突根据业务设计决定

2. 立即重试为什么危险?

假设 Inventory Service 正常每秒能够处理约 1000 个请求。

但某个时刻数据库变慢,有效处理能力下降。

此时上游仍然持续发送请求。

如果所有失败请求都立即重试两次,那么一次原始业务请求最多可能产生:

第一次调用

第一次重试

第二次重试

共三次下游调用。

当大量请求同时重试时,原本已经过载的服务会收到更多请求。

例如:

原始请求量:1000 次/秒

每个请求最多调用 3 次

在极端情况下,短时间内产生的调用需求可能接近原始请求量的三倍。

这并不代表实际观测到的 QPS 一定精确变成三倍,因为超时、并发限制和请求到达时间会影响结果。

但机制很清楚:

重试会增加下游需要处理的调用次数。

而此时下游最缺少的恰恰是处理能力。

3. 为什么多层重试更加危险?

假设:

Gateway
   |
   v
Order Service
   |
   v
Inventory Service

Gateway 允许一次请求最多尝试三次。

Order Service 调用 Inventory Service 时,也允许最多三次尝试。

那么一条原始业务请求,在某些故障路径下,可能引发最多九次库存服务调用。

Gateway 第一次尝试
    |
    +-- Inventory 调用 3 次

Gateway 第二次尝试
    |
    +-- Inventory 调用 3 次

Gateway 第三次尝试
    |
    +-- Inventory 调用 3 次

因此:

3 × 3 = 9

如果每一层都独立添加重试,而没有统一预算,很容易造成请求放大。

所以重试策略不应该只是每个服务随手配置一个:

retry = 3

而应该明确:

哪一层负责重试、哪些错误允许重试、允许增加多少额外调用,以及是否还在整体请求时间预算内。


四、指数退避与随机抖动:为什么重试不能一起发生?

假设 Inventory Service 暂时不可用。

有一千个 Order Service 请求同时失败。

如果所有请求都:

失败后等待 1 秒
然后重试

那么一秒以后,可能有大量请求再次同时进入 Inventory Service。

T = 0s:

大量请求失败

T = 1s:

大量请求同时重试

这会形成新的流量峰值。

即使服务本来已经开始恢复,也可能再次被打满。

1. 指数退避(Exponential Backoff)

一种常见思路是随着重试次数增加,逐步拉长等待时间。

例如:

第一次重试前:等待 200ms

第二次重试前:等待 400ms

第三次重试前:等待 800ms

这样可以避免客户端持续以极高频率重试。

但指数退避还有一个问题:

如果所有请求使用完全相同的等待时间,它们仍然可能在相同时间一起重试。

2. 随机抖动(Jitter)

因此,可以给等待时间增加随机性。

例如:

请求 A:等待 230ms

请求 B:等待 370ms

请求 C:等待 190ms

请求 D:等待 410ms

使请求分散到不同时间点。

这样能够减轻同步重试造成的瞬时压力。

不过,退避与抖动并不意味着可以无限重试。

实际设计中通常还需要限制:

最大重试次数

最大重试等待时间

整体请求 Deadline

重试并发量

例如,如果客户端只愿意等待两秒,那么在剩余时间不足时继续安排一个很长的重试,就没有太大意义。

重试的目标是恢复短暂故障,而不是让一个已经过载的系统承受更多无意义工作。
在这里插入图片描述


五、熔断:为什么有时候应该暂时停止调用下游?

前面讨论的 Timeout 和 Retry,仍然主要从单次调用角度处理故障。

但假设 Inventory Service 已经持续出现大量超时。

这时 Order Service 每次都要:

发起请求
    |
    v
等待两秒
    |
    v
超时

即使我们限制了重试次数,仍然会有大量请求浪费时间等待一个明显不可用的服务。

所以我们需要一种机制:

当下游持续表现异常时,暂时停止向它发送部分请求。

这就是 Circuit Breaker(熔断器)的核心思想。

1. 熔断器的三个状态

熔断器常见的状态包括:

Closed

Open

Half-Open

它们可以理解为:

Closed:闭合状态。

正常允许请求访问下游,同时统计错误率、超时等指标。

Open:打开状态。

熔断条件触发后,暂时不再把普通请求转发到对应下游,而是快速返回失败或进入允许的降级路径。

Half-Open:半开状态。

经过一定恢复等待后,允许少量探测请求访问下游,判断服务是否已经恢复。

状态转换:

            正常调用
                |
                v
             CLOSED
                |
       错误达到触发条件
                |
                v
              OPEN
                |
         暂停正常调用
                |
         恢复等待结束
                |
                v
            HALF-OPEN
             /      \
            /        \
        探测成功    探测失败
           |            |
           v            v
         CLOSED        OPEN

2. 熔断有什么价值?

假设 Inventory Service 已经连续超时。

如果没有熔断:

每个订单请求
     |
     v
等待库存服务超时
     |
     v
占用上游资源

如果熔断器打开:

订单请求
    |
    v
检查熔断状态
    |
    v
发现下游不可用
    |
    v
快速失败或安全降级

这样能够减少对下游的无效调用,也避免上游每次都付出完整的等待成本。

3. 熔断不是修复下游

这里需要注意:

熔断器不会让 MySQL 自动恢复,也不会修复 Inventory Service 的程序错误。

它主要用于限制故障传播。

同时,如果熔断条件设置不合理,也可能出现误触发。

例如短时间内只有极少数请求,但其中一次失败,就直接打开熔断器,可能过于敏感。

因此,实际设计通常需要结合足够的请求样本、错误比例、慢调用比例和恢复窗口,而不是简单地看到一次错误就熔断。

4. 熔断以后应该返回什么?

对于商品推荐接口,可以允许降级:

推荐服务不可用
      |
      v
返回普通商品列表

但对于库存预留:

Inventory Service 不可用

不能随意返回:

库存预留成功

因为这会造成虚假的业务成功。

正确做法可能是:

返回暂时无法下单

或者将订单保留在可恢复的待处理状态,前提是业务明确支持这种异步流程。

所以:

熔断之后能否降级,以及如何降级,取决于业务正确性要求。

不能为了接口成功率,把无法确认的业务操作当成已经成功。


六、限流:为什么必须控制进入系统的请求量?

熔断主要关注下游已经出现明显异常时,是否继续调用。

但如果请求量本身已经超过系统处理能力,还需要在更早的位置进行保护。

例如:

Client
   |
   v
Gateway
   |
   v
Order Service
   |
   v
Inventory Service

假设订单系统稳定处理能力是每秒 1000 个请求。

但促销活动开始后,突然有每秒 10000 个请求进入。

如果所有请求都放行,就可能导致:

线程池耗尽

数据库连接池等待

库存服务过载

请求队列不断增长

这时候可以使用 Rate Limiting(限流)。

1. 限流主要解决什么?

限流用于控制某个时间范围内允许通过的请求数量或速率。

例如:

最多允许 1000 QPS

超过限制的请求可以根据业务策略:

快速拒绝

返回 429

进入有界等待队列

而不是全部进入下游执行。

需要注意,1000 QPS 只是示例。实际阈值需要通过压测、监控以及服务容量评估确定。

2. 为什么 Gateway 是常见的限流位置?

因为 Gateway 是很多外部请求进入微服务系统的统一入口。

Client
   |
   v
Gateway
   |
   +-- Order Service
   |
   +-- Product Service
   |
   +-- User Service

在 Gateway 进行限流,可以在请求进入内部服务之前拦截部分流量。

例如:

某用户每秒最多请求 10 次

某接口每秒最多接收 1000 次请求

这样可以减少不必要的内部处理开销。

但 Gateway 限流也存在边界。

它主要控制经过该 Gateway 的流量。

如果还有 Kafka Consumer、内部 RPC、后台任务等其他入口,仅靠外部 Gateway 限流无法保护所有下游资源。

因此,重要的下游服务有时还需要在内部增加自己的并发限制和容量保护。

3. Gateway 限流和 HPA 是不是一回事?

不是。

这是我们之前讨论 Kubernetes 时专门区分过的。

Gateway 限流:控制允许多少请求进入。

Kubernetes HPA:根据指标调整工作负载的副本数量。

例如:

Gateway
   |
   | 控制进入 Order Service 的请求
   v
Order Service Pods
   |
   | HPA 根据指标调整 Pod 数量
   v
更多或更少的 Pod

两者解决的问题不同。

机制核心问题
Gateway 限流当前是否应该继续接收这些请求?
HPA当前工作负载是否需要更多实例?
熔断某个下游持续异常时,是否暂停调用?
超时单次调用最多允许等待多久?

而且 HPA 扩容需要时间,也受到节点容量和下游处理能力的限制。

不能认为:

请求量突然增加
       |
       v
HPA 马上扩容
       |
       v
所有请求都能成功

这不是可靠的容量模型。
在这里插入图片描述


七、隔离:为什么不能让一个故障服务占满所有资源?

除了 Timeout、Retry 和限流,还有一个常见问题:

假设 Order Service 需要调用两个下游。

Order Service
      |
      +-- Inventory Service
      |
      +-- Coupon Service

库存服务正常,但优惠券服务突然变慢。

如果所有调用共用同一个有限线程池:

Shared Thread Pool
       |
       +-- Inventory 请求
       |
       +-- Coupon 请求

那么大量等待 Coupon Service 的调用可能占满线程池。

结果是:

库存服务虽然正常,但订单服务已经没有足够资源继续调用它。

这种情况下,故障就从一个下游传播到了其他业务能力。

Bulkhead:隔离不同资源池

Bulkhead(舱壁隔离)的思想,就是不要让某一类操作无限消耗所有共享资源。

例如:

Order Service
      |
      +-- Inventory Thread Pool
      |
      +-- Coupon Thread Pool

或者对于异步调用,使用不同的并发限制:

Inventory:最多 100 个并发请求

Coupon:最多 20 个并发请求

假设 Coupon Service 变慢:

Coupon 的 20 个并发名额被占满

后续 Coupon 调用可以被拒绝、降级或有限排队。

但 Inventory 的并发资源不会因此全部被占用。

这里不一定要为每个下游创建独立线程池。

对于异步框架,也可以使用 Semaphore(信号量)等方式限制特定调用的同时执行数量。

真正重要的是:

让某一个下游的异常,只能消耗它被分配的资源预算,而不是无限占用整个上游服务的执行能力。

当然,资源隔离也会引入容量配置问题。

如果隔离得过细,可能导致某些资源池空闲,而另一些资源池一直排队。

因此,应当根据调用量、延迟和业务优先级进行设计。


八、Backpressure:为什么处理不过来时,应该让上游减速?

前面讲到的限流主要强调控制进入系统的请求速率。

Backpressure(背压)则更关注:

当下游消费或处理能力不足时,如何把这种容量限制向上游传递,避免请求或消息无限积压。

例如:

Producer
    |
    v
Queue
    |
    v
Worker

假设:

Producer 每秒产生 5000 个任务

Worker 每秒只能处理 2000 个任务

如果一直维持这个状态:

队列每秒新增积压约 3000 个任务

队列会越来越长。

Kafka 可以缓冲任务和事件,但 Kafka 不能自动让 Worker 拥有无限处理能力。

如果积压长期超过可管理范围,就会带来:

业务处理延迟增加

消息保存压力增加

故障恢复时间变长

消费者长时间追不上生产速度

1. 为什么不能让队列无限增长?

假设应用使用内存队列处理请求。

Request Queue
      |
      v
Worker Pool

如果队列没有边界,在下游持续变慢时,大量等待任务就可能占用越来越多内存。

最终造成应用内存压力、GC 停顿甚至进程崩溃。

所以背压的一个常见做法是使用:

有界队列(Bounded Queue)。

例如:

最多允许 1000 个任务排队

当队列满了以后,系统不再无条件接受新任务。

而是根据业务选择:

拒绝

等待

通知上游降低发送速率

将任务可靠转移到其他可承载的位置

需要注意,对于必须处理的业务任务,不能简单把队列满了的请求直接丢弃,而不留下任何可靠记录。

如果业务无法接受任务丢失,就需要设计持久化任务记录、可靠消息或其他恢复机制。

2. Backpressure 和 Retry 有什么关系?

它们的方向可以这样区分。

Retry:

失败后尝试再次执行

Backpressure:

处理能力不足时,减少新的输入压力

如果系统已经持续过载,继续重试只会增加输入压力。

此时更需要的是背压、限流和容量隔离。

所以:

重试解决的是部分暂时性失败,背压解决的是输入速度与处理能力不匹配。

3. Backpressure 和 Kafka 有什么关系?

我们之前学习 Kafka 时,讨论过 Consumer Lag。

Kafka Topic
      |
      v
Consumer Group
      |
      v
MySQL

如果 MySQL 变慢,Consumer 处理消息的速度就会下降。

Kafka 中的消费积压随之增加。

此时不能只依靠增加 Kafka Consumer 数量。

如果真正的瓶颈是 MySQL,增加 Consumer 并发可能把更多请求打到数据库。

更合理的处理方向可能包括:

限制数据库写入并发

控制每批消息的处理量

优化 MySQL SQL 与索引

使用有界执行队列

在容量允许时增加消费并行度

但必须注意:暂停消费或者降低读取速度,只是防止 Consumer 继续加重下游压力,并不能自动消除 Kafka 中已经积累的消息。

如果生产速度长期超过消费速度,还需要从生产端、业务处理能力和系统整体容量上解决问题。


九、把这些保护机制放在一起,应该怎么理解?

到这里,我们已经讨论了六类机制:

Timeout

Retry

Circuit Breaker

Rate Limiting

Bulkhead

Backpressure

它们并不是六种互相竞争的方案。

它们分别处理故障传播的不同阶段。

机制主要保护什么典型问题
Timeout单次调用的等待时间下游迟迟不返回
Retry短暂失败后的恢复机会一次网络抖动导致请求失败
Circuit Breaker上游免受持续异常下游拖累大量调用持续超时
Rate Limiting系统入口与下游容量请求量突然超过处理能力
Bulkhead不同操作的资源隔离一个故障下游占满所有线程
Backpressure输入速率与处理能力匹配队列无限积压

可以用一个完整请求链来理解它们的位置。

                    Client
                       |
                       v
                 Gateway 限流
                       |
                       v
                 Order Service
                       |
                 并发资源隔离
                       |
                       v
                    熔断判断
                       |
                       v
                 Inventory RPC
                       |
                  Timeout
                       |
                 有条件 Retry
                       |
                       v
               Inventory Service
                       |
                 并发 / 队列控制
                       |
                       v
                     MySQL

这里并不代表所有系统都必须严格按这个顺序实现每个机制。

例如重试和熔断可以由同一客户端调用框架协调,具体执行顺序也需要结合调用策略设计。

真正的重点是:

每个服务应该知道自己能够承受多少工作,以及在下游已经无法处理时,应该怎样及时减少额外压力。


十、一个完整案例:库存服务变慢以后,系统如何避免级联故障?

现在把前面的机制放回订单系统。

假设正常情况下:

Client
   |
   v
Gateway
   |
   v
Order Service
   |
   v
Inventory Service
   |
   v
MySQL

Inventory Service 响应稳定。

突然,MySQL 因为某条慢 SQL 导致查询延迟明显增加。

第一阶段:下游延迟开始上升

MySQL 查询变慢
       |
       v
Inventory Service 响应变慢

Order Service 开始观察到 RPC 延迟上升。

这时合理的 Timeout 可以避免某个请求无限等待。

第二阶段:限制重试造成的额外压力

部分库存请求开始超时。

如果调用方立即大量重试,Inventory Service 会收到更多请求。

所以需要:

只对允许重试的错误重试

保持稳定的 request_id

限制最大尝试次数

设置退避与 Jitter

遵守整体 Deadline

对于库存预留这种有状态操作,还需要根据幂等标识和执行状态避免重复扣减。

第三阶段:触发熔断

如果库存服务持续异常,熔断器可以暂停大量无效调用。

Order Service
      |
      v
Circuit Breaker
      |
      v
OPEN
      |
      v
快速返回不可用

这样 Order Service 不必让每个请求都等待库存 RPC 超时。

但不能把库存服务不可用伪装成库存预留成功。

第四阶段:限制入口请求和内部并发

Gateway 可以对下单接口进行限流。

Order Service 还可以控制自己同时向 Inventory Service 发起多少请求。

例如:

Gateway:

限制下单请求进入速率
Order Service:

限制库存 RPC 并发数量

这样可以防止库存服务恢复期间再次被瞬间打满。

第五阶段:保护其他业务功能

假设订单服务还需要调用优惠券服务。

通过 Bulkhead 或按下游设置 Semaphore,使库存故障不会占满所有下游调用资源。

这样至少可以减少库存故障对其他请求路径的连带影响。

第六阶段:恢复时逐步放开

当 Inventory Service 开始恢复:

Circuit Breaker
      |
      v
HALF-OPEN
      |
      v
允许少量探测请求

如果探测稳定,再逐步恢复正常调用。

同时观察:

错误率

P99 延迟

线程池使用率

数据库连接池等待

请求积压

避免刚恢复就把大量积压请求和重试一起释放给下游。

这里需要强调:系统恢复不是简单地把熔断器从 OPEN 改回 CLOSED,而是要确认真实的下游处理能力已经恢复。
在这里插入图片描述


十一、为什么快速失败有时候比一直等待更可靠?

前面讲到熔断和限流时,多次出现了一个词:

快速失败

对于刚接触分布式系统的人来说,这可能有些反直觉。

既然我们的目标是让请求成功,为什么要主动拒绝请求?

原因在于:

当系统已经无法处理全部请求时,继续接受所有请求,可能让原本可以成功的请求也一起失败。

例如:

Inventory Service 每秒只能稳定处理 1000 次请求

但上游持续发送:

每秒 5000 次请求

如果全部放进队列,系统可能出现:

等待时间越来越长

客户端纷纷超时

客户端继续重试

请求进一步积压

最后,大部分请求都无法在可接受时间内完成。

但如果系统在入口就限制部分流量:

允许处理可承载的请求

对超出容量的请求快速拒绝

那么被允许的请求反而有更高机会及时完成。

这就是过载保护的一个基本思想。

但快速失败并不是所有业务的最终处理方式。

例如:

普通商品推荐查询可以直接返回稍后重试提示,或者降级返回默认推荐。

订单提交如果没有创建任何业务状态,可以明确拒绝请求并让用户重新提交。

已经开始执行、但状态未知的支付请求则不能直接当成没有发生过。它需要通过业务 ID、状态查询和恢复机制确认最终结果。

因此,真正需要区分的是:

尚未开始执行的请求

和:

可能已经产生副作用的请求

前者可以根据容量快速拒绝。

后者需要按照相应业务协议确认状态,避免快速失败掩盖已经发生的业务操作。


十二、实际排查级联故障时,应该先看什么?

假设某一天,订单接口的错误率突然升高。

我们可以按照调用链逐步排查。

1. 先看错误从哪里开始出现

例如:

Order Service:

P99 从 200ms 上升到 5s

但 Order Service 自身 CPU 并不高。

这时应该检查它是否正在等待下游。

如果发现:

Inventory RPC:

超时比例明显增加

再继续排查 Inventory Service。

如果 Inventory Service 又大量等待 MySQL,就可能找到真正的初始瓶颈。

2. 看重试是否放大了调用量

例如正常情况下:

Order Service 每秒接收 1000 个请求

但 Inventory Service 突然收到:

每秒 2500 个 RPC 调用

这时就应该检查是否有大量自动重试。

还需要区分:

原始请求 QPS

和:

实际下游调用 QPS

两者不一定相同。

3. 看资源是否被等待占满

例如:

线程池活跃线程数

线程池排队数量

HTTP 连接池等待

数据库连接池等待

CPU 使用率

内存与 GC

这些指标能帮助判断问题是 CPU 计算能力不足,还是大量请求正在等待外部资源。

4. 看熔断、限流与拒绝是否正常触发

例如:

Circuit Breaker Open 次数

429 返回比例

并发限制拒绝数量

降级次数

半开探测成功率

这些指标不能单独代表系统质量好坏。

例如限流拒绝数量增加,可能说明系统正在主动保护下游,而不一定是限流器出现故障。

需要结合下游延迟和真实成功率判断。

5. 看故障恢复后是否仍然存在积压

如果系统使用 Kafka 或后台任务队列,还应该关注:

Consumer Lag

任务队列长度

最旧任务等待时间

重试队列数量

即使 MySQL 已经恢复,消息积压也可能需要一段时间才能处理完成。

所以排查级联故障不能只看到:

服务进程重新启动成功

就认为问题已经完全结束。

还需要确认:

调用延迟、业务错误、积压任务和下游负载是否真正恢复到可接受水平。


十三、总结:微服务可靠性不是靠无限重试实现的

这一篇从库存服务变慢开始,逐步解释了级联故障如何发生,以及不同故障治理机制分别解决什么问题。

整个过程可以归纳为:

下游服务变慢
       |
       v
上游请求等待
       |
       v
并发资源被占用
       |
       v
请求开始超时
       |
       v
错误重试增加下游压力
       |
       v
故障向更多服务传播

针对这条故障链,我们学习了几种保护机制。

Timeout: 限制单次调用的等待时间,避免资源长期占用。

Retry: 在合适的条件下恢复短暂故障,但需要控制次数、时间和额外流量。

Circuit Breaker: 当下游持续异常时,暂时减少无效调用,防止故障继续向上游传播。

Rate Limiting: 控制进入系统的请求速率,避免流量超过处理能力。

Bulkhead: 隔离不同业务路径或下游调用的资源,避免一个故障占满全部执行能力。

Backpressure: 当处理能力不足时,减少新的输入压力或进行有界缓冲,避免无限积压。

其中最重要的是以下三个结论。

第一,Timeout 不等于失败

如果调用涉及数据库修改、支付或库存预留,超时以后必须考虑操作已经执行成功但响应丢失的可能性。

不能简单地用重复调用替代状态确认和业务幂等。

第二,重试不是免费的

每一次重试都会占用额外的网络、线程、连接或者下游计算资源。

当下游已经过载时,不受限制的重试只会进一步放大故障。

第三,扩容与流量治理必须一起考虑

HPA 可以增加工作负载实例,但不能自动解决数据库瓶颈,也不能阻止客户端重试风暴。

Gateway 限流、服务内部并发限制、熔断和背压仍然具有独立价值。

最后,可以把这一篇的核心观点总结为:

一个可靠的微服务系统,不是保证每个请求在任何情况下都必须完成,而是在资源有限、下游故障和网络不确定时,仍然能够控制工作量、明确业务状态,并阻止局部故障扩散到整个系统。

下一篇我们继续学习微服务性能与可观测性,重点介绍 QPS、P99、Trace,以及如何从一条跨服务调用链中定位真正的性能瓶颈。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_51711289/article/details/169460832

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--