下游只是发布时抖了一下,我的服务却因为疯狂重试把它彻底打死、还让它久久无法恢复,我对着重试风暴和指数退避加抖动排查了大半天的复盘

一次本该轻描淡写的小故障:依赖的下游因发布短暂抖动几秒,本来加了重试就该平稳扛过。我也确实加了重试——失败就重试最多 3 次,以为很稳健。结果这"几秒小抖动"被重试放大成持续十几分钟的大故障:下游不但没恢复反而被打得更死,抖动结束后还久久缓不过来。百思不得其解:重试不是为了提高成功率吗怎么把下游搞死了?排查大半天才理解"重试风暴"这个反直觉陷阱:我的立即重试(无退避)+ 无抖动 + 大并发,在下游整体故障时,每个原始请求瞬间变 4 个、且几乎同时涌来,形成同步洪峰把下游彻底打垮;更糟的是惊群——下游刚要恢复积压重试又同时砸来再次打趴。根因是重试针对偶发独立失败是良药,但下游整体故障时无退避无抖动的重试会放大流量N倍、形成失败→重试→更过载的正反馈。这篇从重试风暴的形成、指数退避+随机抖动+只重试幂等可重试+熔断+控制总预算的正解、为何抖动是防惊群的关键(退避防频繁/抖动防集中)、重试策略要素速查、容错四件套(超时/重试/熔断/降级/限流/隔离)、决策图与铁律,到附上一个带退避抖动+熔断的健壮重试封装。核心领悟:判断机制好坏不能只看正常场景,更要看异常/前提被打破时;容错机制设计不周自己会成为故障放大器;好机制应在最坏情况安全失败而非灾难性反噬。

下游只是抖了一下,我的服务却因为疯狂重试把它彻底打死、还久久无法恢复,我对着重试风暴和指数退避排查了大半天的复盘

那是一次本该轻描淡写的小故障。我们依赖的一个下游服务,因为一次发布,短暂地抖动了几秒——这种程度,本来加了重试就该平稳扛过去。我也确实给调用下游的地方加了重试:失败了就重试,最多 3 次。我以为这很稳健。可结果是,这次"几秒的小抖动",被我的重试放大成了一场持续十几分钟的大故障:下游不但没因为重试而恢复,反而被打得更死了,而且抖动结束后,它还迟迟缓不过来。我百思不得其解:重试不是为了提高成功率、增强稳定性吗?怎么反而把下游搞死了?排查了大半天,我才真正理解了"重试风暴"这个反直觉的陷阱,以及"指数退避 + 抖动"的价值。这篇就把这场"好心重试办坏事"的事故,从头复盘一遍。

故障现场:重试,反而成了压垮下游的最后一根稻草

先看现场。问题就藏在我那段"看起来很稳健"的重试代码里:

// 我的重试代码: 失败立即重试, 最多3次 —— 我以为这很稳健
public Response callDownstream(Request req) {
    int maxRetries = 3;
    for (int i = 0; i <= maxRetries; i++) {
        try {
            return httpClient.call(downstream, req);
        } catch (Exception e) {
            if (i == maxRetries) throw e;
            // ✗ 立即重试! 没有任何等待
            // (而且: 没有判断"这个错误该不该重试")
        }
    }
}

// 故障是怎么被放大的:
// 1. 下游发布, 抖动几秒, 开始大量返回失败/超时。
// 2. 我的服务有大量并发请求, 每个失败的请求【立即】重试。
// 3. 重试也大概率失败(下游还在抖) → 立即再重试 → 再失败...
//    → 每个原始请求, 瞬间变成 4 个请求(1次 + 3次重试)打向下游!
// 4. 关键: 这些重试是【几乎同时】发生的(都失败、都立即重试),
//    形成一波又一波"同步的"重试洪峰。
// 5. 下游本来只是小抖动, 现在却要承受【4倍】的、且高度集中的流量
//    → 被彻底打垮(本来能自愈的抖动, 被重试雪上加霜压死了)。
// 6. 更糟的"惊群效应": 等下游刚要缓过来一点, 积压的大量请求/重试
//    又【同时】涌上来(因为大家都在同一时刻重试)→ 再次把它打死。
//    → 下游反复"刚要起来就被打趴", 久久无法恢复。

// 现象拼图:
//   - 重试的本意: 应对"偶发、独立"的失败(如单个网络丢包), 是好的。
//   - 但"立即重试 + 无退避 + 无抖动 + 大并发", 在下游【整体性故障】时,
//     会瞬间制造数倍的、同步的流量洪峰 → 把抖动放大成雪崩。
//   - ★ 重试在"个体偶发失败"时是良药, 在"下游整体故障"时却是毒药 ——
//     它把"本可自愈的抖动", 变成了"被流量洪峰压垮的雪崩"。

看清真相后,我惊出一身冷汗。我那段"看起来稳健"的重试,在下游整体性故障时,变成了压垮它的元凶。问题出在几个叠加的因素:立即重试(无等待)、无退避、无抖动、大并发下游一抖,我的大量并发请求每个失败都立即重试,每个原始请求瞬间变成 4 个(1 次 + 3 次重试)打向下游;而且这些重试几乎同时发生(都失败、都立即重试),形成一波波"同步的"洪峰;下游本来只是小抖动,现在却要承受 4 倍且高度集中的流量,被彻底打垮。更糟的是"惊群效应":下游刚要缓过来,积压的请求/重试又同时涌上来(大家都在同一时刻重试),再次把它打死,导致它反复"刚要起来就被打趴"、久久无法恢复。归根结底:重试在"个体偶发失败"时是良药,在"下游整体故障"时却是毒药——它把"本可自愈的抖动",放大成了"被流量洪峰压垮的雪崩"

第一件事:搞懂重试为什么会变成"风暴"

要解决它,得先搞懂"重试风暴"是怎么形成的,以及它和"健康的重试"差在哪。

重试风暴(Retry Storm)的形成

# 重试本是好东西 —— 针对"偶发、独立"的失败:
#   - 单个请求碰巧网络丢包、碰巧遇到一次GC停顿 → 重试一下就好了。
#   - 这类失败是"孤立的、随机的", 重试能有效提高成功率。

# 但"重试风暴"是怎么形成的? —— 当失败是"整体性、相关的":
#   - 下游整体故障/过载时, 失败不再是"个别请求"的, 而是"大面积"的。
#   - 此时所有失败的请求都重试 → 流量瞬间放大 N 倍(N=重试次数+1)。
#   - 而下游本就过载, 再来 N 倍流量 → 更过载 → 更多失败 → 更多重试...
#   - 形成正反馈的恶性循环: 失败→重试→更多流量→更多失败。这就是风暴。

# 三个让风暴更猛的"帮凶":
#   1. 立即重试(无退避): 失败后马上重发, 流量毫无缓冲地砸过去。
#   2. 固定间隔(无抖动): 大家都"失败后等固定1秒"再重试 → 重试在
#      同一时刻【同步】涌来 → 形成尖峰(惊群 thundering herd)。
#   3. 多层重试叠加: A调B、B调C, 每层都重试3次 → 重试次数【相乘】!
#      A的1次 → B重试3次 → 每次B又让C重试3次 → C被放大 9倍、16倍...

# 健康重试 vs 风暴重试:
#   健康: 退避(越失败等越久) + 抖动(错开时间) + 限次 + 判断可重试 + 熔断。
#   风暴: 立即 + 固定间隔 + 无脑重试 + 多层叠加。

# 核心: 重试针对偶发独立失败是良药; 但下游整体故障时, 无退避无抖动的重试
#   会放大流量N倍、形成"失败→重试→更过载"的正反馈恶性循环, 即重试风暴。

原来,重试是良药还是毒药,取决于失败的性质重试针对"偶发、独立"的失败是好的——单个请求碰巧丢包、碰巧遇到 GC 停顿,重试一下就好;这类失败孤立随机,重试能有效提高成功率。但"重试风暴"形成于失败是"整体性、相关的"时——下游整体故障/过载时,失败是大面积的,所有失败请求都重试 → 流量瞬间放大 N 倍 → 下游本就过载、再来 N 倍流量 → 更过载 → 更多失败 → 更多重试,形成正反馈的恶性循环三个"帮凶"让风暴更猛:立即重试(无退避,流量毫无缓冲砸过去)、固定间隔(无抖动,重试在同一时刻同步涌来形成尖峰=惊群)、多层重试叠加(A 调 B、B 调 C 每层都重试,重试次数相乘,被放大 9 倍、16 倍)对比就很清晰:健康重试 = 退避 + 抖动 + 限次 + 判断可重试 + 熔断;风暴重试 = 立即 + 固定间隔 + 无脑重试 + 多层叠加

第二件事:正解——指数退避 + 抖动 + 熔断 + 只重试该重试的

搞懂了原理,正解就清晰了:重试要有指数退避(越失败等越久)、加随机抖动(错开时间)、配熔断(整体故障就别重试了)、只重试幂等且可重试的请求

// ====== 正解一: 指数退避 + 随机抖动(Exponential Backoff + Jitter)======
public Response callWithBackoff(Request req) {
    int maxRetries = 3;
    long baseMs = 200;
    for (int i = 0; i <= maxRetries; i++) {
        try {
            return httpClient.call(downstream, req);
        } catch (Exception e) {
            if (i == maxRetries || !isRetryable(e)) throw e;  // 不可重试就直接抛
            // ✓ 指数退避: 等待时间随重试次数指数增长(200, 400, 800ms)
            long backoff = baseMs * (1L << i);          // 2^i 倍
            // ✓ 抖动: 在退避基础上加随机, 错开各请求的重试时刻(打散尖峰)
            long jitter = ThreadLocalRandom.current().nextLong(backoff);
            sleep(backoff + jitter);   // 越失败等越久, 且各请求错开
        }
    }
    throw new IllegalStateException();
}
// → 指数退避: 给下游"喘息"的时间, 越失败说明问题越严重、越该多等。
// → 抖动: 让原本会"同步涌来"的重试, 在时间上被随机打散, 削平尖峰(防惊群)。

// ====== 正解二: 只重试"幂等 + 可重试"的请求 ======
boolean isRetryable(Exception e) {
    // ✓ 可重试: 网络超时、连接失败、503/429(下游明确说"稍后再试")
    // ✗ 不可重试: 400参数错误、401/403鉴权(重试也没用, 纯属浪费)
    //            非幂等写操作(重试可能导致重复下单/重复扣款!)
    return e instanceof TimeoutException || isStatus(e, 503, 429, 502);
}
// → 别无脑重试! 4xx客户端错误重试无意义; 非幂等操作重试会出大问题。

// ====== 正解三: 熔断器(Circuit Breaker), 整体故障时"快速失败"别再重试 ======
// 当下游错误率超过阈值(如50%), 熔断器"打开": 后续请求【直接失败】, 不再
//   真正去调下游(也就不会重试) → 给下游彻底"卸载", 让它有机会自愈。
// 过一段时间"半开": 放少量请求试探, 成功了再"闭合"恢复。
if (circuitBreaker.isOpen()) {
    return fallback();   // 快速失败/降级, 不打下游
}
// → 熔断是重试风暴的"终极刹车": 识别到下游整体挂了, 就别再试了, 让它休息。

// ====== 正解四: 限制总重试预算 + 别多层叠加 ======
// - 整个调用链, 重试要有"全局预算"(如令牌桶), 别每层都重试3次相乘。
// - 通常: 只在【最接近用户的一层】或【最了解幂等性的一层】重试, 其余不重试。

// 核心: 重试要 指数退避(给下游喘息)+ 随机抖动(打散尖峰防惊群)+ 只重试幂等可重试的
//   + 熔断(整体故障快速失败别再试)+ 控制总重试预算(别多层相乘)。

修复的核心,是"让重试'温柔且克制',而不是'立即且无脑'"正解一:指数退避 + 随机抖动——指数退避让等待时间随重试次数指数增长(200、400、800ms),给下游喘息时间(越失败说明问题越严重、越该多等);抖动在退避基础上加随机,让原本"同步涌来"的重试在时间上被打散、削平尖峰(防惊群)。正解二:只重试"幂等 + 可重试"的请求——可重试的如超时、503/429;不可重试的如 400 参数错(重试也没用)、非幂等写操作(重试可能重复下单/扣款)正解三:熔断器——下游错误率超阈值就"打开"熔断、后续请求直接快速失败不再打下游,给下游彻底卸载让它自愈,过段时间"半开"试探;熔断是重试风暴的终极刹车正解四:限制总重试预算 + 别多层叠加——整个调用链重试要有全局预算,通常只在最接近用户或最了解幂等性的一层重试。归根结底:重试要指数退避 + 随机抖动 + 只重试幂等可重试的 + 熔断 + 控制总预算。

第三件事:为什么"抖动"如此关键

排查时我对"抖动"(Jitter)这个看似不起眼的细节,有了全新的认识。它是防"惊群"的关键。

抖动(Jitter): 防"惊群"的点睛之笔

# 问题场景: 即使有了"指数退避", 没有抖动还是会惊群
#   - 假设下游挂了, 1000个请求【同时】失败。
#   - 都用"指数退避": 第1次都等200ms, 第2次都等400ms...
#   - 但因为它们"同时失败", 所以会"同时等待"、"同时重试"!
#   - → 1000个重试在 200ms 那个点【同时】砸向下游 → 又一个尖峰!
#   - 退避只是"推迟"了尖峰, 没有"削平"尖峰。

# 抖动的作用: 把"同步"打散成"分散"
#   - 在退避时间上加随机: 等待 = 退避 ± 随机量。
#   - 于是1000个请求, 有的等180ms、有的220ms、有的350ms... 错开了!
#   - → 重试流量被"摊平"在一段时间里, 不再是一个尖峰, 下游能从容处理。

# 几种抖动策略:
#   - Full Jitter:   sleep = random(0, backoff)        (完全随机, 最分散)
#   - Equal Jitter:  sleep = backoff/2 + random(0, backoff/2)
#   - Decorrelated:  sleep = random(base, prev*3)       (AWS推荐之一)
#   AWS的经典文章实测: Full Jitter 在多数场景下削峰效果最好。

# 一个深刻的洞见:
#   "退避"解决的是"重试太频繁(强度)"; "抖动"解决的是"重试太集中(时机)"。
#   两者缺一不可: 只退避不抖动, 尖峰只是被推后, 依然成灾。

# 核心: 只有指数退避、没有抖动, 同时失败的请求仍会"同步重试"形成尖峰(惊群);
#   抖动给退避加随机, 把同步的重试打散到一段时间, 削平尖峰 —— 退避防频繁, 抖动防集中。

排查让我对"抖动"这个细节肃然起敬。问题在于:即使有了指数退避,没有抖动还是会惊群——假设 1000 个请求同时失败,都用指数退避(第 1 次都等 200ms),但因为它们"同时失败",所以会"同时等待、同时重试",1000 个重试在 200ms 那个点同时砸向下游、又是一个尖峰;退避只是"推迟"了尖峰,没有"削平"它而抖动把"同步"打散成"分散":在退避时间上加随机(等待 = 退避 ± 随机),于是 1000 个请求有的等 180ms、有的 350ms,错开了,重试流量被摊平在一段时间里、不再是尖峰常见策略有 Full Jitter(random(0, backoff),最分散)、Equal Jitter、Decorrelated(AWS 实测 Full Jitter 削峰最好)。一个深刻的洞见:"退避"解决的是"重试太频繁(强度)","抖动"解决的是"重试太集中(时机)",两者缺一不可——只退避不抖动,尖峰只是被推后、依然成灾下面这张图,是这次重试风暴的成因与解法:

第四件事:重试策略要素速查

这次踩坑后,我把"一个健康的重试策略该具备什么"整理成一张表,以后写重试就逐项对照。

要素 不做的后果 正确做法
退避策略 立即重试,流量毫无缓冲 指数退避,越失败等越久
抖动 同步重试形成尖峰(惊群) 加随机抖动错开时间
重试上限 无限重试拖垮自己和下游 限次(如3次)
可重试判断 4xx/非幂等也重试,浪费或出错 只重试超时/5xx/幂等操作
熔断 下游整体挂了还硬试 错误率超阈值就熔断快速失败
总预算 多层重试相乘,放大数十倍 全局重试预算/只一层重试
超时 没超时,请求堆积 每次调用设合理超时

这张表,把"健康重试"的要素拆解清楚了。它告诉我:"加重试"绝不是写个 for 循环重发那么简单——一个生产级的重试,要同时考虑退避、抖动、限次、可重试判断、熔断、总预算、超时这一整套。它给我的最大启发是:"重试"这个看似简单的"容错"手段,本身其实是一把双刃剑:用对了,它提升系统的韧性;用错了,它会成为放大故障的"故障放大器"而区分这两者的,正是这些"看似琐碎的细节"(有没有退避、有没有抖动、会不会熔断)。这让我领悟到一个关于"容错设计"的深刻道理:容错机制(重试、降级、熔断……),如果设计得不够周全,它们自己就可能成为新的故障源,甚至比它们想防的故障还可怕;"为了提高可用性而加的东西",必须自己先足够"克制和智能",否则它会"帮倒忙"所以,加任何"容错"机制时,都要追问一句:"当故障真的发生时,我这个'容错机制'本身,会不会让事情变得更糟?"

第五件事:重试、超时、熔断、降级——容错四件套

这次让我把分布式容错的几个核心机制串起来理解了。它们各司其职,配合使用。

机制 解决什么 关键
超时 Timeout 防止请求无限等待、堆积 每次调用都设,且要合理
重试 Retry 应对偶发、独立的失败 退避+抖动+限次+可重试判断
熔断 Circuit Breaker 下游整体故障时止损 错误率超阈值快速失败,给下游卸载
降级 Fallback 失败时给个兜底结果 返回缓存/默认值/友好提示
限流 Rate Limit 保护自己不被过量请求压垮 令牌桶/漏桶,超了拒绝
隔离 Bulkhead 防止一个依赖拖垮整个服务 线程池/信号量隔离

这张表,把分布式系统的"容错四件套"(及更多)串成了一个整体。它们各司其职:超时防无限等待、重试应对偶发失败、熔断在整体故障时止损、降级给失败兜底、限流保护自己、隔离防止一个依赖拖垮全局而这次事故让我看清了它们之间的配合关系:重试和熔断,是一对必须搭配的"矛与盾"——重试是"积极地再试一次"(应对偶发),熔断是"识别到没救了就别再试"(应对整体故障);只有重试没有熔断,就会像我这次一样,在下游整体挂掉时,把重试变成压垮它的风暴它给我的最大启发是:分布式系统的稳定性,不是靠某一个"银弹",而是靠这一整套机制的协同配合;每个机制都有它"擅长应对的故障类型"和"失灵甚至帮倒忙的场景",必须把它们组合起来,才能覆盖各种故障情况真正健壮的系统设计,是清楚每个容错机制的边界、并让它们互相补位——重试管偶发、熔断管整体、降级管兜底、限流隔离管自保。理解了这套组合拳,才算真正入门了分布式系统的稳定性建设。

第六件事:要加重试时,我现在的决策习惯

现在每当我要给一个调用加重试,我都会先过一遍这张图,确保它是"健康的重试"而非"风暴的种子":

这张图的精髓,是"加重试前,把它设计成'温柔、克制、智能'的"先问 "操作幂等吗":非幂等(写/扣款)要慎重、需配幂等键去重;再问 "这错误该重试吗":4xx 别重试、超时/5xx 才重试。确定要重试后,逐项加上:指数退避、随机抖动、限次、熔断;并检查 调用链是否有多层重试(有就只保留一层,别相乘)最后一步是我现在的硬习惯:压测时主动模拟下游故障,看重试会不会演变成风暴(这次的坑正是因为没测下游故障时的重试行为)。这套习惯,让我加重试时,从"失败就重试以为很稳健"变成了"设计一个不会帮倒忙的重试"——核心始终是:重试要温柔克制智能,且必须配熔断,否则下游整体故障时它会变成风暴。

我立下的几条规矩

这场"重试把下游打死"的事故,换来了我做分布式调用时,刻进骨子里的几条铁律:

  1. 重试必须有指数退避。别立即重试,给下游喘息时间,越失败等越久。
  2. 退避必须配抖动。否则同时失败的请求会同步重试形成尖峰(惊群)。
  3. 只重试幂等且可重试的。4xx 别重试,非幂等写操作重试要配幂等键。
  4. 重试必须配熔断。下游整体故障就快速失败、别再试,给它卸载自愈。
  5. 控制总重试预算,别多层叠加。每层都重试会相乘放大数十倍。
  6. 容错机制本身要克制。设计不周的重试/降级,自己会成为故障放大器。
  7. 压测要模拟下游故障。专门测"下游挂了时我的重试会不会帮倒忙"。

附:一个带退避抖动+熔断的健壮重试封装

口说无凭。下面把指数退避、Full Jitter、熔断、可重试判断合到一个封装里,生产可直接参考:

// 一个健壮的"带退避抖动+熔断"的调用封装
public class ResilientCaller {
    private final int maxRetries = 3;
    private final long baseMs = 200;
    private final long maxBackoffMs = 3000;       // 退避上限, 别等太久
    private final CircuitBreaker breaker;          // 熔断器

    public  T call(Supplier action, Supplier fallback) {
        // 1. 熔断器打开: 直接降级, 根本不去打下游(防风暴的终极刹车)
        if (breaker.isOpen()) {
            return fallback.get();
        }
        Exception last = null;
        for (int attempt = 0; attempt <= maxRetries; attempt++) {
            try {
                T result = action.get();
                breaker.recordSuccess();           // 成功, 反馈给熔断器
                return result;
            } catch (Exception e) {
                last = e;
                breaker.recordFailure();           // 失败, 反馈给熔断器
                // 2. 不可重试的错误: 立即放弃(别浪费重试)
                if (!isRetryable(e) || attempt == maxRetries) break;
                // 3. 熔断器在重试过程中打开了: 立即停止重试
                if (breaker.isOpen()) break;
                // 4. 指数退避 + Full Jitter
                long backoff = Math.min(baseMs * (1L << attempt), maxBackoffMs);
                long sleep = ThreadLocalRandom.current().nextLong(backoff + 1);
                //          ↑ Full Jitter: random(0, backoff), 削峰效果最好
                sleepQuietly(sleep);
            }
        }
        // 5. 重试用尽 / 不可重试 / 熔断: 走降级兜底
        return fallback != null ? fallback.get() : rethrow(last);
    }

    private boolean isRetryable(Exception e) {
        if (e instanceof TimeoutException) return true;
        if (e instanceof HttpException he) {
            int code = he.statusCode();
            return code == 502 || code == 503 || code == 429;  // 网关/过载/限流
        }
        return false;   // 默认不重试(4xx等)
    }
}

// 用法:
// resilientCaller.call(
//     () -> httpClient.call(downstream, req),   // 主逻辑
//     () -> Response.cachedOrDefault()          // 降级兜底
// );

// 核心: 熔断先行(打开就降级不打下游) + 只重试可重试错误 + 指数退避Full Jitter
//   + 退避上限 + 重试中途熔断也停 + 最终降级兜底; 一个封装兜住"重试不成风暴"。

这个 ResilientCaller,把这篇文章所有的教训,落成了一个生产可用的封装。它的精妙,在于把"重试"和"熔断"这对矛与盾,以及"退避、抖动、降级"几个要素,有机地编织在一次调用里:熔断先行(打开就直接降级、根本不打下游);只重试可重试的错误;指数退避配 Full Jitter(削峰);退避有上限(别等太久);重试中途如果熔断打开了也立即停止;最终走降级兜底而不是裸抛异常尤其是那个"重试过程中也检查熔断"的细节——它保证了即使在一次调用的重试循环里,一旦发现下游整体挂了(熔断打开),也会立刻停止重试,而不是傻傻地把 3 次重试都用完这,正是我想用这个封装,留给每个做分布式系统的人的最后一课:"容错",从来不是单个机制的事,而是多个机制(重试、熔断、退避、抖动、降级)协同配合的系统工程;而把这套协同固化进一个统一的、经过深思熟虑的封装里,让团队的每一次远程调用都默认享有这套保护,远比"靠每个人每次手写正确的重试"可靠得多因为分布式系统的稳定性,经不起"个别地方写错了一个重试"——而一个好的封装,正是把"正确的容错",从一件"需要时时小心的难事",变成一件"调用即得的默认"把对抗复杂故障的智慧,沉淀成人人可用的基础设施——这,是工程团队真正的护城河。

写在最后

回头看,这场由"重试风暴"引发的、把下游小抖动放大成大故障的事故,真正教给我的,远不止"重试要加退避和抖动"这一套技巧。它让我对"好意"和"好结果"之间的关系,有了一次深刻的反思。我加重试,是出于一个绝对正确的"好意"——提高系统的稳定性和成功率。可这个好意,因为缺少了对"它在最坏情况下会如何表现"的考量,最终办成了一件大坏事——把一个本可自愈的小抖动,亲手放大成了一场大故障这让我领悟到一个在系统设计中至关重要、却极易被忽视的道理:判断一个机制的好坏,不能只看它"在正常情况下、在它被设计来应对的场景下"表现如何;更要看它"在异常情况下、在它的前提假设被打破时"会如何表现我的重试,在"下游偶发抖动(个别请求失败)"这个它被设计的场景下,工作得很好;但在"下游整体故障(大面积失败)"这个它没考虑到的场景下,它就从"帮手"变成了"凶手"。这正是"反直觉"之所在:一个旨在增强稳定性的机制,在特定条件下反而摧毁了稳定性所以,设计任何系统机制(尤其是容错、自动化、反馈类的机制)时,我都会强迫自己做一次"最坏情况推演":"如果它面对的情况,极端到超出了我的设计预期,它会优雅地降级,还是会失控地放大灾难?"——一个好的机制,应该在最坏情况下"安全地失败",而不是"灾难性地反噬"。这,是我用一次"好心重试办坏事"的事故,换来的、关于网络、也关于"机制的最坏情况推演"的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次加重试时,先想一想"下游整体挂了时,我这重试会不会变成压垮它的风暴",那我对着那个被重试打死、久久不恢复的下游熬的这大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我的列表接口前几页飞快、翻到几十万页却慢到超时,同样是查 20 条,凭什么越往后越慢,我对着深分页的 LIMIT offset 排查了大半天的复盘

2026-6-2 6:31:44

技术教程

我的服务平稳跑了整整三个月却突然全线崩溃,排查到最后发现根因竟是磁盘被一个日志文件写满了,我对着日志轮转和磁盘监控告警排查了大半天的复盘

2026-6-2 6:43:25

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索