LLM 面试备考
首页/分布式与可靠性

超时、重试与幂等(Timeout, Retry & Idempotency)

所属:01-fundamentals / 07-distributed-systems
面试权重:★★★
来源:《深入理解 AI Agent:设计原理与工程实践》第 4 章「幂等性与取消语义」(唯一标识去重、先查询后变更、「预检—确认」两段式)与第 1 章「Harness 的纠正 / 熔断」;《深入理解 AI Infra:量化分析与系统设计》第 11 章「工具失败后的恢复」(幂等定义、消息去重 ≠ 实际执行两次)。通用重试 / 退避 / 抖动 / 熔断三态 / 隔离舱的原理(System Design 101、DDIA 等公开资料)未做一手考证 —— 这一部分书里没有系统论述,按渠道记录,不当作一手基线
相关知识点:../06-agent-infra/10-agent-runtime.md §6(分工:那篇讲 Agent 工具执行层「一轮怎么从故障接着跑」与持久性语义;本篇讲通用机制层 —— 超时 / 重试 / 幂等 / 熔断本身,不依赖 Agent 语境,Agent 只是它的一个应用场景)、rpc-http-grpc.md(通信与 deadline 传递)、queue-pubsub.md(投递语义里的「至少一次 / 恰好一次」)、state-machine.md(状态机怎么让重试可判定)、../04-agent-architecture/tool-use.md(工具层:可重试 vs 不可重试)、../04-agent-architecture/human-in-the-loop.md(中断前后副作用的幂等)、../../02-interview/design/05-routing-gateway.md §4.7(网关自身的高可用)、../../02-interview/projects/quewi-chatbot.md(网关故障复盘)

一句话#

分布式调用里没有超时就没有资源上界,而超时并不等于失败 —— 它只是「结果未知」;围绕这个「未知」,工程上要同时给出三样东西:调用链上递减的超时预算、只在瞬态错误上做且有上限带抖动的重试、以及让重复调用安全的幂等协议。三者缺一个,重试就从「恢复手段」变成「放大器」。

为什么重要(面试角度)#

  • 它是最容易被答成口号的一块。 「失败就重试」几乎人人会答;能说清超时后结果是未知的、分层重试会相乘、消息去重防不住实际执行两次的人很少,而这三条恰好是生产事故的直接成因。
  • 它有一批能当场推的量。 超时预算怎么沿调用链递减、3 层各重试 3 次能放大到几倍、退避为什么必须加抖动 —— 都是可推的,不是形容词。
  • 它把「可靠性」从「多试几次」推进到协议设计。 幂等键的粒度、去重表的有效期、外部副作用怎么补偿 —— 这些是设计阶段就要定的,不是出了问题再加的补丁。
  • 和你的项目接得上:QueWI 的 AI 网关做的是模型调用层的超时 / 重试 / 熔断 / 降级;这是你唯一有真实锚点的一层。

⚠️ 边界声明:除 QueWI 网关(模型调用层)外,你没有在生产里运维过分布式系统。本篇是通用机制的梳理,面试用「这套账我会这么算」的口径讲;除明确标注的 QueWI 那一节,不要说成「我们线上就是这么做的」。

核心内容#

1. 超时为什么必须有:它是唯一的上界#

一次没有超时的调用,会把调用方的一个线程 / 连接 / 协程无限期占住。下游卡死时,调用方不是「慢慢变慢」,而是资源被逐个占满:先占连接池,再占工作线程,最后整个服务只剩在等下游。这就是「一次下游卡死放大成上游资源枯竭」的机制 —— 放大的不是错误,是等待。

所以超时的作用不是「失败了早点报错」,而是给每层调用一个资源占用的上界。它和连接池大小、线程数、并发上限是同一件事的三个参数:只要有一层没有上界,其他层的上界就形同虚设(上游可以无限堆积在等这一层)。

一个常见误区:把超时设得很长当「保险」。超时值本身就是容量参数 —— 超时 60 秒意味着一个坏掉的下游能在 60 秒里吸走本层全部并发;超时 2 秒则最坏只损失 2 秒的产能。怎么定:从 SLO 反推(用户能接受多久?),而不是从「下游一般要多久」反推。

2. 超时 ≠ 失败:结果是未知的(整篇的枢纽)#

这一条是整篇的地基。 超时之后你不知道下游到底做没做:

  • 可能没做 —— 请求还在路上,或下游还没开始处理;
  • 可能做完了但确认丢了 —— 下游已经执行并提交,返回的响应在网络里丢了。

../06-agent-infra/10-agent-runtime.md §6 把这条讲得很清楚:「没做完」和「做完了但确认丢了」是两种失败,处理完全不同。把它们当成同一种(都当「失败」)去重试,就会在第二种情形里把同一件事做两遍 —— 重复下单、重复扣款、重复发消息,而这类副作用往往不可逆。

正确动作不是「重试」,而是先查询:查已持久化的结果、核对操作标识与输入版本,结果在就接续,不在才判断能不能重试。而且这个查询本身要幂等(第 6 节)。

一句话记法:超时是「不确定」,不是「没发生」。 只有幂等操作才允许把「不确定」直接当「没发生」处理。

3. 超时预算(deadline)沿调用链递减#

一次用户请求往往穿过多层:网关 → 编排服务 → 工具服务 → 外部 API。如果每层各自用「自己的超时」,最内层的超时可能比最外层还长 —— 最外层早已放弃,内层还在烧资源。

正确做法是传一个 deadline 而不是每层设一个 timeout:上游总共给 3 秒,那它调下游最多留 2.5 秒,下游再往下最多 2 秒 —— 每层留出给自己处理和返回的时间,越往下越短。

  • 不递减的后果:上游 3 秒放弃时,下游可能才跑到第 2.8 秒,然后继续跑到自己的 5 秒 —— 上游的线程早就释放了,下游还在为一个已经没人要的结果消耗资源。故障时这会叠加成资源雪崩。
  • 实现要点:deadline 绝对时间随请求传递(不是相对值),或者上下文里带「剩余预算」;每层进入时先算「我还剩多少」,剩得不够就直接放弃,不再往下发。
  • 和重试的关系:重试也要服从同一个 deadline —— 不是「重试 3 次」就完事,而是「在剩余预算内最多重试 3 次」(第 4 节)。

4. 重试三问:重试什么、几次、怎么退避#

重试不是「出错就再来一次」,而是三个独立决策。

① 重试什么(可重试性分类)—— 这步错了后面全错。

该重试(瞬态) 不该重试(确定性)
超时、连接重置、连接建立失败 参数非法 / schema 不合法
429 限流、503 服务不可用、5xx 鉴权失败、权限不足
网络抖动、临时 DNS 失败 业务规则拒绝(余额不足、风控拦截)
—— 工具不存在 / 幻觉调用(重试多少次都一样)

判据是「同样的输入再来一次,结果会不会不同」:瞬态错误的答案可能变,确定性错误的答案不变。对确定性错误,重试是纯浪费,正确路径是把结构化错误回灌给调用方去改输入或改策略(Agent 场景见 ../04-agent-architecture/09-coding-agent.md:工具层错误绝不能静默重试,要回灌给模型自纠)。

② 重试几次 —— 要有两个上限,不是只有一个。

  • 次数上限(如 3 次);
  • 总时长上限(如「最多再花 2 秒」)—— 这个常被漏掉。指数退避下第 5 次的等待已经很长,光有次数上限,总耗时仍可能超出上游 deadline。所以两者都要,取先到的那个。

还要区分前台 / 后台调用:主链路的请求值得重试,标题生成、输入建议这类辅助调用失败就直接放弃 —— 否则后台重试会挤占主链路配额(见 ../04-agent-architecture/09-coding-agent.md 的「重试放大」)。

③ 怎么退避 —— 指数退避 + 抖动(jitter),抖动不是可选项。

固定间隔重试会把「顺时抖动」变成「周期性冲击」:一批客户端同时失败,如果都等同样的 1 秒、2 秒、4 秒再重试,它们会永远保持同步 —— 每次重试都是同一时刻的齐步冲击,下游刚缓过来又被同一批请求打垮。抖动就是给每个客户端的等待加随机量,把这种同步打散:等待从「2n2^n」变成「2n2^n 区间内的随机值」。

退避方式 等待 效果
固定间隔 恒定 最差,同步冲击
指数退避 b⋅2nb\cdot2^n 更差:等待越长,同步的重试波峰越集中
指数退避 + 全抖动 random(0, b⋅2n)\text{random}(0,\ b\cdot2^n) 打散重试波峰,是常见默认
尊重服务端提示 用 Retry-After 上游明确说了等多久就听它的(如 429)

抖动为什么必须:不做抖动时,失败是「相关」的 —— 大家都被同一件事打倒,也就都会在同一时刻站起来。指数的增长会让重试波峰更集中而不是更分散,这就是「同步重试把瞬时故障变成周期性冲击」的机制。

5. 重试放大与重试风暴#

每一层各自重试,倍数会相乘。 3 层调用链,每层失败都重试 3 次:

3 层×3 次=33=27 倍流量3\ \text{层}\times3\ \text{次}=3^3=27\ \text{倍流量}

即:本来 1 个请求失败,最坏会对最内层下游产生 27 倍的请求量。下游本来只是「慢」,会被重试打成「挂」。 这就是重试放大 / 重试风暴。

三条对症措施,缺一不可:

  1. 只有一层重试。 通常选最靠近调用方、掌握最多上下文的那层做重试;其余层失败就向上抛,不要各自「兜底重试一次」。
  2. 重试要有预算。 给整条链一个重试总量 / 速率上限(即使按比例也好),别让重试无限占配额。
  3. 重试要服从上游 deadline。 内层重试不能超过外层给的剩余时间;外层已经放弃,内层不要再开新一轮。

这一条在这个仓库里有一条真实复盘。 ../../02-interview/projects/quewi-chatbot.md 的线上故障复盘第 2 条:「上游故障被重试放大」 —— 某家模型超时后整体延迟被拉爆,甚至把其他模型 / 配额的额度也吃掉,演成雪崩;缓解是超时预算 + 有上限且带退避的重试 + 熔断降级到备用模型,复盘结论一句话:「重试是最容易写成放大器的机制」。把这句话在本篇的框架里展开,就是上面三条。

6. 幂等:让重复调用安全#

定义(../06-agent-infra/10-agent-runtime.md 的书中口径):幂等操作 = 以同一操作标识重复调用时,最终业务效果与调用一次相同。

注意是业务效果相同,不是「返回同一个响应」—— 重复调用可以返回不同的时间戳,只要没有产生第二次副作用。这个定义比「函数式幂等」宽,正好覆盖「写操作 + 唯一标识」的场景。

幂等键(idempotency key)的实现套路:

  1. 调用方生成唯一键 —— 在第一次尝试之前生成,重试时复用同一个键(这点最关键:每次重试新生成一个键,就等于没做幂等)。键通常放进请求头(如 Idempotency-Key),因为它是传输元数据而非业务数据;放 header 也便于网关 / 中间件统一处理。
  2. 服务端去重表 + 唯一约束 —— 收到请求先按键查表:
    • 键已存在且有结果 → 直接返回存下来的那个结果(不重新执行);
    • 键已存在但还在处理中 → 返回「处理中」(或让调用方稍后查),不要并发执行第二次;
    • 键不存在 → 执行,并把「键 → 结果」写入去重表。唯一约束是防并发重复的兜底:两个几乎同时到达的同键请求,只有一个能插入成功。
  3. 结果落库(不止落「已处理的标记」) —— 只记「做过了」而不记结果,调用方拿不到结果还是得再问一次;把首次的响应体也存下来,重放时原样返回。

键的粒度(设计阶段就要定):谁生成 —— 客户端(重试复用同一个,服务端生成就形同虚设);作用域 —— 按「用户 + 操作类型」加前缀,避免跨用户撞键;放哪 —— header 为主,跨语言中间件好处理;有效期 —— 去重记录至少覆盖可能重试的时间窗,太短则迟到的重试又会执行一次。

一句硬话:幂等性是重试的前提,不是重试的补充。不幂等的写操作「先重试再想办法」,顺序就反了。

7. 必须点破的一条:消息去重 ≠ 操作不重复执行#

这一条 ../06-agent-infra/10-agent-runtime.md §6 讲得最透,也是区分「背过」和「想清楚」的分水岭:

现象 靠什么解决 代价
同一份结果被传送两次 消息层去重就够了 无额外副作用
同一项操作实际执行两次 只能靠持久化结果 + 目标系统的幂等协议 已经烧了两份资源

书里的推演:一次工具执行 3 CPU·秒,结果被发两次 —— 若接收方按操作标识去重,累计 CPU 时间仍是 3 秒;但若控制器因确认丢失又执行了一次,累计就涨到 6 秒。两种情况最后都可能只有一条结果,却消耗了不同资源。

消息去重是「传输层」的,操作不重复是「执行层」的 —— 方向完全不同,前者管不到后者。所以「我们加了消息去重」不能推出「不会重复执行」。

8. 熔断 / 降级 / 隔离舱:什么时候该停止重试#

重试的前提是「失败是瞬态的」。 如果下游是持续故障(不是抖动,是挂了),重试只是把流量反复砸向一个已经倒下的服务 —— 这时正确的动作是停止重试、快速失败、让请求走别的路。这就是熔断。

熔断器的三个状态(通用机制,System Design 101 一类公开资料;未做一手考证):

状态 行为 怎么切换
闭合 Closed 正常放行;统计失败率 / 连续失败数 超过阈值 → 打开
打开 Open 快速失败,请求不再打到下游(直接走降级 / 报错) 经过冷却时间 → 半开
半开 Half-open 放少量探测请求过去 探测成功 → 闭合;失败 → 再次打开

半开是关键:不能等冷却时间一到就全量恢复 —— 下游可能还没好,全量恢复等于再来一次冲击;半开是「先放几个探子」,成了再逐步放开。

隔离舱(bulkhead):给不同依赖 / 不同租户分独立的资源池(各自的连接池、线程池)。一个依赖坏了,最多耗尽它自己的池子,不会把整个服务的线程吃光 —— 这正是本篇开头「一次下游卡死放大成资源枯竭」的正解。熔断按时间维度挡住坏依赖,隔离舱按空间维度限制坏依赖的影响范围,两者常配合用。

降级之后怎么对用户交代是个产品问题。 ../../02-interview/design/05-routing-gateway.md §4.7 与 ../../02-interview/projects/quewi-chatbot.md 都强调:降级可以是静默(换更小的模型 / 更短的上下文)也可以是显式提示;要说清这次降级质量掉到什么程度、用户能不能感知、有没有兜底话术。含糊答「就降级了」等于没答。

什么时候停止重试而不是继续重试 —— 三句判据:下游整体不可用(连续失败 / 大面积超时)→ 熔断;重试已吃掉 deadline(再试也交不出结果)→ 停止、走降级;重试会放大故障(下游已过载,重试等于加压)→ 停止 —— 过载不是靠重试补救的。

9. 接回项目:QueWI 网关这一层(真实锚点)#

我做过的是「模型调用层」,不是整条分布式系统。 具体在这三层做了事,讲的时候要分清:

  • 超时:给上游模型调用设超时,不设的话一次卡死会占住网关的并发;超时值不是拍的,是按「用户能接受的延迟 - 下游处理时间」倒推,并给重试留出预算。
  • 重试:只对瞬态错误(超时、429、5xx)重试;有次数上限 + 指数退避 + 抖动;尊重服务端 Retry-After。这正是复盘的结论来源 —— 「重试是最容易写成放大器的机制」。
  • 熔断 + 降级:某家模型连续失败时快速失败,降级到备用模型 / 供应商,而不是继续把请求砸给它。同时明确降级后的质量损失怎么和用户沟通。

要主动划的界:网关的降级是「换一个模型接着答」,不涉及支付、下单这类不可逆副作用 —— 所以网关层的「重复调用」代价主要是多花钱,而不是「把钱转错两次」。那一类副作用的幂等要求高得多(../04-agent-architecture/tool-use.md、../04-agent-architecture/human-in-the-loop.md 讲的写操作幂等),我没有在生产里处理过,只能按本篇的协议讲「应该怎么做」。

常见面试问法#

Q:为什么调用必须设超时? A:因为超时是这一层资源占用的唯一上界。 没有超时,一次下游卡死会逐个占满调用方的连接 / 线程 / 协程,服务不是变慢而是资源枯竭。超时值本身是容量参数:超时 60 秒意味着一个坏下游能在 60 秒里吸走本层全部并发。所以超时要从 SLO 反推,不是从「下游一般多快」反推。还要说清:光设超时不够,超时值要沿调用链递减(下一问)。

Q:超时了直接重试不行吗? A:不行 —— 超时之后结果是「未知」,不是「失败」。 下游可能没做,也可能做完了但确认丢了。对第二种直接重试,就是把同一件事做两遍(重复下单 / 扣款 / 发消息)。正确动作是先查询:查持久化结果、核对操作标识与输入版本,结果在就接续,不在再判断能不能重试;而且这个查询本身必须幂等。只有确定幂等的操作,才允许把「未知」当「没发生」处理。这一条就是 ../06-agent-infra/10-agent-runtime.md §6 的「两种失败」之分。

Q:重试要怎么设计才算不闯祸? A:三件事,缺一不可。 ① 重试什么 —— 只重试瞬态错误(超时、连接重置、429、5xx),不重试参数错误 / 鉴权失败 / 业务拒绝(判据:同样输入再来一次结果会不会变);② 重试几次 —— 次数上限 + 总时长上限两个都要,取先到的;并区分前台 / 后台调用,辅助调用失败直接放弃;③ 怎么退避 —— 指数退避必须叠加抖动,否则失败是相关的、重试会同步,指数增长反而让重试波峰更集中。再加两条全局约束:只有一层重试、重试服从上游 deadline。

Q:为什么指数退避还要加抖动? A:因为不加抖动时,所有客户端会永远保持同步。 一批客户端被同一件事打倒,如果都等同样的 1、2、4 秒再重试,就会在相同时刻齐步冲击下游 —— 把一次瞬时故障变成周期性冲击,而且指数增长让波峰更集中。抖动把每个客户端的等待随机化,打散这个同步(全抖动:random(0,b⋅2n)\text{random}(0,b\cdot2^n))。另外,上游明确给了 Retry-After 就按它来。

Q:多层调用链都做了重试,有什么风险? A:倍数会相乘。 3 层各重试 3 次 = 33=273^3=27 倍流量,下游本来只是「慢」,会被重试打成「挂」。所以:只有一层重试(通常最靠近调用方那层)、重试要有预算、服从上游 deadline。QueWI 网关的真实复盘就是这条 —— 「上游故障被重试放大」,整体延迟被拉爆还吃掉其他配额,缓解靠的是超时预算 + 有上限带退避的重试 + 熔断降级。「重试是最容易写成放大器的机制」。

Q:幂等怎么做?幂等键有什么讲究? A:幂等的定义是「以同一操作标识重复调用,业务效果与调用一次相同」。 实现是幂等键套路:调用方在第一次尝试前生成键、重试时复用(每次重试新生成键就等于没做)→ 服务端去重表 + 唯一约束(防并发重复的兜底)→ 结果也落库(不能只记「做过了」)→ 重放时返回同一结果。讲究在粒度:谁生成(客户端)、作用域(按用户 / 操作类型加前缀)、放哪(header 为主)、有效期(至少覆盖可能重试的时间窗)。一句话:幂等是重试的前提,不是补充。

Q:消息去重了,是不是就不会重复执行了? A:不是,方向不同。 消息去重只管「同一份结果被传送两次」;「同一项操作实际执行两次」是执行层的事,只能靠持久化结果 + 目标系统的幂等协议。书里的推演:工具执行 3 CPU·秒,结果发两次按操作标识去重 → 还是 3 秒;控制器因确认丢失又执行一次 → 变成 6 秒。两种情况最后都可能只有一条结果,却烧了不同资源。

Q:什么时候该停止重试? A:当下游是持续故障、或重试本身在加剧故障时。 三类信号:下游整体不可用(连续失败 / 大面积超时)→ 熔断;重试已吃掉 deadline(再试也交不出结果)→ 停止、走降级;下游已过载(重试等于加压)→ 停止。熔断是三态的(闭合 / 打开 / 半开),半开时必须只放少量探测请求,不能冷却时间一到就全量恢复。配合隔离舱把坏依赖的影响限制在它自己的资源池里。降级还要回答产品问题:这次降级质量掉多少、用户能不能感知、怎么交代。

易错点#

易错说法 问题 应该说
「设个超时就行了」 只设一层、不递减 超时是资源上界,要沿调用链递减传递 deadline;超时值本身是容量参数
「超时 = 调用失败」 丢掉了「未知」 超时后结果是未知(可能没做、可能做完确认丢了);先查询再决定
「超时了就重试」 对不幂等操作会重复执行 只有幂等操作(或目标系统支持操作标识 / 补偿)才可重试
「出错就重试」 最常见、最扣分 先分类:只重试瞬态错误;参数 / 鉴权 / 业务拒绝重试无意义,要改输入
「重试次数设好就够了」 漏了总时长上限 次数 + 总时长两个上限取先到;并服从上游 deadline;区分前台 / 后台
「指数退避就是全部」 漏了抖动 必须叠加抖动;否则客户端同步重试,把瞬时故障变成周期性冲击
「每层都兜底重试最稳」 倍数相乘 3 层 × 3 次 = 27 倍流量;只有一层重试
「我们做了消息去重,不会重复执行」 方向错 去重只防「同一结果传两次」;实际执行两次要靠持久化结果 + 幂等协议
「重试时重新生成一个幂等键」 等于没做幂等 幂等键在第一次尝试前生成,重试复用同一个
「重试能救下游过载」 反向加压 下游过载时停止重试;上游故障用熔断(三态 + 半开探测)挡住
「熔断冷却时间到就全量恢复」 少了半开 必须半开只放少量探测,成了再逐步放开
「降级是纯技术问题」 漏了产品面 降级要说清质量损失、用户是否可感、有无兜底话术 —— 静默还是显式提示
把 QueWI 网关讲成「整条分布式系统都做过」 触碰仓库硬规矩 只讲模型调用层的超时 / 重试 / 熔断;不可逆副作用的幂等没在生产里做过

自测#

1. 你调用一个下游服务,3 秒超时了。直接重试有什么问题?应该怎么设计才算对?

答案

直接重试的问题:它把「不确定」当成了「没发生」。 超时之后你不知道下游做没做 —— 可能没做,也可能做完了但确认丢了。对不幂等的操作(下单、扣款、发消息),盲目重试会让同一件事发生两次,而这类副作用往往不可逆。

正确顺序是「先查询,再决定」:

  1. 查已持久化的结果,核对操作标识与输入版本 —— 有结果就接续,不重试(这一步本身也要幂等);
  2. 没有结果时,判断操作是否幂等:只读查询或幂等写可以安全重试;不幂等的操作需要目标系统支持操作标识 / 幂等接口 / 补偿才敢重试;
  3. 设计侧:给写操作配幂等键(调用方在首次尝试前生成、重试复用同一个键),服务端用去重表 + 唯一约束兜底并发重复,并把结果一并落库,重放时返回同一结果。

再往上:报告「完成」时的持久性语义要定义清楚(只保证答案已生成,还是保证消息 / 工具结果 / 恢复位置都已保存)——这属 ../06-agent-infra/10-agent-runtime.md §6 的范围。本题的枢纽就是一句话:超时是「不确定」,不是「没发生」。

2. 一条 3 层调用链,每层失败各自重试 3 次。最坏情况下最内层下游会收到多少倍的请求?怎么改?

答案

最坏 33=273^3=27 倍。 每层各自重试,倍数相乘:外层一次失败触发 3 次中层调用,中层每次失败又触发 3 次内层调用,依次下去。下游本来只是「慢」,会被重试打成「挂」 —— 这就是重试放大 / 重试风暴。

三条改法(缺一不可):

  1. 只有一层重试 —— 通常选最靠近调用方、掌握最多上下文的那层;其余层失败就向上抛,不要各自兜底重试。
  2. 重试要有预算 —— 给整条链一个重试总量 / 速率上限,别让重试无限占配额。
  3. 服从上游 deadline —— 内层重试不超过外层给的剩余时间;外层放弃了,内层不要开新一轮。

还有两个配套细节: 退避要指数 + 抖动(否则客户端同步重试,把瞬时故障变周期性冲击);区分前台 / 后台调用,辅助调用失败直接放弃。仓库里的真实结论:QueWI 网关复盘第 2 条「上游故障被重试放大」——「重试是最容易写成放大器的机制」。

3. 为什么指数退避一定要加抖动?不加会怎样?

答案

因为不加抖动时,失败是「相关」的,客户端会永远保持同步。 一批客户端被同一件事打倒,如果都等同样的 1、2、4 秒再重试,它们就会在相同时刻齐步重试 —— 下游刚缓过来又被同一批请求打垮。指数增长会让问题更严重:等待越长,重试时刻越集中成一个尖峰,结果就是「把一次瞬时故障变成周期性冲击」。

抖动(jitter)的作用是给每个客户端的等待加随机量,打散这个同步。 常见做法是全抖动:等待取 random(0, b⋅2n)\text{random}(0,\ b\cdot2^n) 区间内的值,而不是固定的 b⋅2nb\cdot2^n;上游若明确给了 Retry-After(如 429)则按它来。一句话:退避管「等多久」,抖动管「别一起等」。

4. 一个调用超时了,你加了两层消息去重,能保证这件事不会被做两次吗?

答案

不能。方向不同。 消息去重处理的是「同一份结果被传送两次」;「同一项操作实际执行两次」是执行层的问题,消息层去重管不到。

书里的推演最能说清:一次工具执行要 3 CPU·秒,若结果被发两次、接收方按操作标识去重,累计仍是 3 秒;但若控制器因确认丢失又执行了一次,累计就涨到 6 秒 —— 两种情况最后都可能只有一条结果,却消耗了不同资源。 避免「实际执行两次」只能靠持久化结果 + 目标系统的幂等协议(即第 1 题那套)。「我们加了去重」必须说清是消息层还是执行层,否则等于没说。

5. 下游模型服务大面积超时(不是抖动,是挂了)。你还继续重试吗?接下来怎么做?

答案

不继续重试 —— 这时重试只会把流量反复砸向一个已经倒下的服务,等于加压。 判据:「同样的输入再来一次,结果会不会不同」 —— 大面积超时是持续故障,答案不会变,重试没有意义。

正确动作是熔断 + 降级 + 隔离:

  • 熔断:连续失败 / 失败率超阈值 → 打开,快速失败;冷却后进半开,只放少量探测请求,成了再闭合。半开是关键 —— 不能冷却时间一到就全量恢复。
  • 降级:走备用模型 / 备用供应商 / 更短的上下文,而不是直接报错;网关不可用时也要有安全默认。
  • 隔离舱:不同依赖 / 租户的资源池分开,坏依赖最多耗尽它自己的池子。

最后必须回答产品问题:降级质量掉多少、用户能不能感知、有没有兜底话术、静默还是显式提示(见 ../../02-interview/design/05-routing-gateway.md §4.7)。说「就降级了」等于没答。