日期:2026-07-24
本文为工程实践复盘,客户信息已脱敏;所述数据均为示意值,不对应任何具体合同或审计数字。
我们把多家模型统一收敛到网关之后,遇到过一个典型的"重试雪崩"事故。某天一家上游模型实例抖动,返回了大量超时和 5xx,业务侧为了"保险"在客户端做了无脑重试:一次请求失败就立刻再发三次,三次都打到了已经打满的下游,瞬间把原本只是局部抖动的故障放大成全链路拥塞。更糟的是,有些生成类请求被重复执行,用户侧收到了两份内容、账单上也被扣了两次 token。排查时我们发现,客户端根本不知道"这次调用到底成没成",只能赌一把重发。我们才意识到:在 AI 调用这种"慢且贵"的场景里,重试不是加个循环那么简单,它必须和幂等、退避、熔断绑在一起设计,否则重试本身就是一种攻击,而且会同时制造重复生成和重复计费。
我们在网关层统一收敛了重试与幂等治理:业务侧只调网关一个地址,网关负责分级重试、幂等去重、指数退避和熔断降级。幂等键由调用方显式携带(或网关在请求体稳定时自动生成),网关维护一个短窗口的结果缓存;同一幂等键的重复请求直接返回首次结果,不再穿透到模型。对明确非幂等的调用(如"生成一张新图""创作一段文案"),网关支持按调用类型选择关闭去重,保证语义正确。当上游错误率突破阈值,网关打开熔断器,返回降级答案或兜底提示,而不是把压力继续堆给已经不健康的模型。整套逻辑对业务透明:业务只感知"成功/失败/降级",不需要自己实现重试与幂等。
第一个难点是重试分级。我们区分"瞬时故障"(超时、429、503/504)与"永久故障"(400、401、403、内容合规拦截),只对前者重试,且重试次数与总时长受上限约束,避免无限循环把下游拖死。第二个难点是幂等键与去重。网关为携带幂等键的请求建立"键→响应"缓存,窗口内重复键直接短路返回;但去重必须是"可感知"的——调用方要知道自己被去重了(返回带幂等标记),而不是误以为两次都成功了,否则对账会乱。第三个难点是幂等冲突。同一个键却带着不同请求体时,网关判定为冲突并拒绝第二次,防止"借壳改内容"造成以旧覆新。第四个难点是退避与熔断。我们用指数退避加抖动,避免大量客户端在同一时刻重试造成的"重试风暴";错误率超阈值即熔断,给上游喘息窗口,熔断半开时按比例探活再恢复,期间请求走降级通道。
案例片段(已脱敏):重试与幂等策略配置片段:
yaml retry: on: [timeout, 429, 503, 504] # 仅瞬时故障重试 max_attempts: 3 backoff: exponential jitter: true idempotency: header: X-Idempotency-Key ttl_sec: 300 conflict: reject # 同键异体判冲突 opt_out_types: [image_generate] # 非幂等调用关闭去重 circuit_breaker: error_rate: 0.5 open_sec: 30
以脱敏示意口径看:幂等治理上线后,因重试导致的重复调用率(同一业务意图被模型执行多次)由事故期的约 18% 降至约 0.3%,重复计费工单基本归零。重试成功率(瞬时故障下最终拿到正确响应)提升到约 96%。引入退避与熔断后,同类上游抖动的"雪崩"事故从每月偶发降为约半年一起,且影响面被控制在单实例内、恢复时长由约 20 分钟缩短到约 3 分钟。网关整体调用成功率(含降级兜底)稳定在约 99.2%,其中约 0.6% 走的是熔断降级通道、用户无感。所有数值均为示意值,不对具体合同负责。
重试的前提是幂等——没有幂等键的去重,重试只会制造重复生成和重复扣费,雪崩之外还添乱。重试要先分级,只对瞬时故障重试,对永久故障(鉴权、合规拦截)重试毫无意义还浪费资源。退避要带抖动,否则所有客户端同步重试会合成新的流量尖峰,比原故障更猛。熔断是最后一道保险,错误率超阈值就开路,给上游恢复时间,半开探活后再恢复,别硬扛。非幂等调用要可显式关闭去重,否则"生成新内容"会被误去重成"返回旧内容",语义就错了。
落地后我们把重试与幂等的运行态也纳入可观测:网关对外暴露每次请求的“是否重试、重试几次、是否命中幂等、是否走降级”标签,调用方在日志里一目了然,排障时不用再猜“是不是重复发了”。一个容易踩的坑是幂等键 TTL 的选取——窗口太长占用缓存、且旧结果可能已过时;太短又会在业务重试间隙漏掉去重。我们的经验是先按业务最大容忍重试时长设基线,再按线上重复率微调,把“重复调用率”压到接近零又不浪费内存。最后是文化层面:我们明确“重试是网关的能力,不是业务的负担”,业务侧从此只管发一次,剩下的交给网关,整体调用代码反而更简洁,也少了各团队自己造轮子带来的不一致。值得强调,幂等键由调用方显式携带时,网关只负责缓存与去重,密钥空间与冲突判定完全可解释,便于事后对账与审计。
案例片段(已脱敏):幂等命中短路返回片段:
python key = req.headers.get("X-Idempotency-Key") if key and cache.exists(key): return cache.get(key), tagged("idempotent-hit") if key and cache.conflict(key, req.body_hash): raise Conflict("same key, different body") resp = upstream_call(req) if key: cache.set(key, resp, ttl=300)