高峰期上游 API 限流了,你的应用怎么优雅降级?
谁在问:二面·稳定性考察;有过线上事故的团队、平台组、SRE 背景的面试官
口语化问法
- 早高峰上游返回 429 了,你的应用会怎么表现?怎么让用户少受影响?
- 如果加了自动重试,429 率反而更高了,为什么?
- 讲一次你们线上因为上游不可用导致的事故,怎么处理的?
考察意图
这是稳定性题,面试官在判断:
- 你会不会加重雪崩。 这道题有一个明确的陷阱——盲目重试。有一条反直觉的官方规则:失败的请求同样计入你的每分钟配额。
- 你能不能区分错误类型。 429 和 529 完全不是一回事,动作也应该相反,把它们一律当限流重试是典型的没做过生产。
- 你的"降级"有没有代价意识。 说"切到备用厂商"很容易,但跨厂商 fallback 的代价有三重,且第二天的账单会告诉你。
参考答案
60 分答案(及格线)
429 是上游限流,处理上主要做这几件事:
- 重试:用指数退避加随机抖动,避免所有客户端同时重试;同时读响应里的
retry-after头,按它给的时间等。 - 限流前置:在自己这边先做一层限流,把超出上游配额的请求提前拦下来,别都打到上游去。
- 降级:配置备用模型或备用厂商,主链路失败就切过去。
- 兜底:如果都不行,给用户一个友好的提示,或者转人工,而不是直接抛 500。
另外可以做削峰填谷——把不着急的请求放进队列异步处理,高峰期只保证核心链路。
为什么只有 60 分:动作都对,退避加抖动和读
retry-after也说明看过文档。但缺三样——没有区分 429 和 529(对 529 重试是有害的)、没提失败请求也消耗配额(这是"重试反而更糟"的直接原因)、降级只说"切过去",没有任何代价陈述。
90 分答案(有生产经验的回答)
我先按错误类型分叉,因为不同的码对应完全相反的动作:
| 状态码 | 含义 | 动作 |
|---|---|---|
| 429 | 你超限了 | 读 retry-after 指数退避 + 随机抖动,同厂商重试 |
| 529 / 服务过载 | 上游过载,不是你的问题 | 重试无用甚至加重雪崩,正确动作是切 fallback |
| 403 配额耗尽 / 413 请求过大 | 不可重试 | 降级或直接拒绝 |
| 504 超时 | 处理超时 | 官方建议改用流式或批处理,而不是重试 |
把 429 和 529 一律当限流重试,是典型的没做过生产。 529 说明上游整体容量吃紧,重试只会在已经很挤的时候再挤一下。
然后是那条反直觉的规则:失败的请求同样计入每分钟配额。 盲目重试就是用配额买配额——我们踩过这个坑,加了自动重试后五分钟内 429 率不降反升。官方在批处理场景给的建议反而是主动注入延迟:限额 20 RPM 就给每个请求加 3 到 6 秒延迟,在天花板附近平稳运行,而不是把配额浪费在重试上。
我的降级策略是分层的,按"代价从小到大"排:
第一层:预防,让 429 少发生。
- 网关侧做前置限流,量纲要对——上游按 TPM/ITPM/OTPM 计,就不能只按 QPS 限;并且要配并发闸门,因为 token 数要等响应回来才知道,并发数是唯一在请求发出前生效的硬约束。
- 把默认
max_tokens压到贴近真实需求——收益最大的一个动作。限流按估算的max_tokens扣配额,设 4096 但实际只输出 200,配额就浪费了 20 倍。我们从 4096 压到 512,429 率降了一个数量级。这也解释了"用量指标看着很低却一直 429"——不是 bug,是设计。 - 能异步的负载迁到批处理,既五折又不占实时配额。
第二层:削峰,用排队换成功率。 非核心场景进队列异步处理、前端给"处理中"状态,核心链路优先;同时要有优先级和公平性——核心保底配额,非核心只能借用空闲配额,高峰期先被挤出去。
第三层:降级到备用能力。 这一层最容易被说得太轻松,实际有三重代价:
① 缓存归零 跨厂商必然 100% 未命中;
即使同厂商降级到小模型,也可能因最小可缓存长度门槛跳档
(旗舰 512 token → 小模型 4096 token),
让一批短 prompt 从 0.1× 掉回 1.0× —— 输入成本涨 10 倍
② 契约破裂 tool schema、结构化输出格式、流式事件语义都可能不兼容
③ 合规越界 主链路走的是签了协议的厂商,
故障时自动降到另一家 —— 数据在无人察觉时流出了合规边界我们的做法是给降级链单独跑一遍输出契约测试、单独过一遍合规评审,敏感流量的降级目标只能在同等合规级别内选——宁可返回错误也不跨界降级。
第四层:优雅失败。 都不行时正确的表现不是抛 500,而是:返回明确说明"当前繁忙、请稍后"的可读响应、保留用户输入不丢、提供转人工或稍后通知的出口。流式场景还要注意——已经流出一半再失败,要在 UI 上标明这是不完整的回答,否则用户会把半截答案当成完整结论。
最后是熔断。 重试和降级都要配熔断:连续失败到阈值就跳闸走降级,避免每个请求都白等一次超时。要有冷却时间,且熔断状态要能观测——否则会出现"上游好了但还在降级"。
还有一件容易被忽略的事:限流计数存储本身是隐藏的单点。 Redis 挂了,限流器是 fail-open 还是 fail-close?我们的做法是分层——全局成本兜底走 fail-close(钱是不可逆的),单租户公平性限流走 fail-open(它保护的是体验不是钱),同时进程内保留一个粗粒度计数器兜底。
追问链
指数退避的参数怎么设?为什么必须加抖动?
期望初始延迟 1 秒、倍数 2、重试次数个位数,且必叠加随机抖动 —— 防惊群:同一条曲线会让所有客户端一起回来,把刚恢复的上游再打挂一次。两条边界:retry-after优先于自己的曲线;总重试预算封顶(2–3 次)。加分:加速度限制 —— 突增本身被罚,扩量要爬坡信号能说出「惊群」和「retry-after优先」→ 真配过;只甩「指数退避」一个词 → 背的降级到备用厂商会让账单涨,怎么提前评估?
期望三件事:① 缓存归零:跨厂商必然,同厂商跨模型也可能因最小可缓存长度跳档(短 prompt 从 0.1× 掉回 1.0×);② 单价差,输出是输入的五六倍;③ 行为差异 —— 更啰嗦、工具调用次数不同,不看轨迹发现不了。把降级链当正式配置验收:评测集上跑质量、成本、契约合规率信号主动提「同厂商跨模型也会因门槛跳档而涨价」→ 被账单教育过;说「备用更便宜所以没问题」→ 没算缓存那一笔怎么判断该重试、该排队、还是直接降级?
期望判据是时效性与可撤销性,不是技术偏好:同步交互(对话、搜索)短重试 + 快速降级 —— 能接受换模型答,不接受转圈 30 秒;异步任务排队 + 长退避或迁批处理;写操作(下单、扣款)必须有幂等键;强合规宁可失败也不跨界。判断做成网关层策略配置,不散在 try-catch 里信号单独拎出「写操作要幂等」→ 踩过重复执行的坑;只按「重要不重要」分 → 还停在直觉层面熔断怎么设计?阈值怎么定?
期望三态(正常 / 跳闸 / 半开)+ 三参(阈值、冷却、试探流量)。阈值看错误率加最小样本量,只看错误数会在低流量时被一两个错误误跳闸;按错误码分熔断器:429跳闸后降速不切走,529与5xx切 fallback(后者加告警);状态要可观测可干预,否则「上游好了还在降级」信号说得出「不同错误码走不同熔断器」→ 真设计过;只描述三态模型 → 教科书背的早高峰持续 429、降级链也满了,产品说「宁可慢也不能报错」,客服说「用户投诉转圈太久」,怎么决策?
期望① 点破两个诉求物理互斥:不报错就得排队,不慢就得拒量,是分配不是优化 → ② 做双方受益的可逆动作:压max_tokens、摘非核心与批量任务、重试转保守(失败重试也吃配额)→ ③ 「慢」要分层:核心保底配额,次要排队给预计时间,最低档「稍后重试」留输入 → ④ 给三档量化(零报错 / 18% 收到提示 / 成本涨 X 倍)交两边同表选,事后沉淀成配置信号第一句就点破「两个诉求物理互斥」→ 有系统思维;只说「扩容」或「跟产品说做不到」→ 两头都不合格
评分要点
- 429 与 529 分叉处理,且知道对 529 重试是有害的
- 知道失败请求同样消耗配额,因此盲目重试会加重雪崩
- 退避要有随机抖动,且
retry-after优先于自己的曲线 - 知道并发闸门是唯一在请求发出前生效的硬约束
- 说得出压
max_tokens能显著降 429,并解释"用量低却一直 429"的成因 - 降级有代价陈述:缓存归零、契约破裂、合规越界
- 有分层策略:预防 → 削峰 → 降级 → 优雅失败,且写操作要幂等
- 熔断按错误码分类,且状态可观测可干预
- 知道限流计数存储是隐藏单点,并对 fail-open / fail-close 有分层判断
- 压力面下能识别诉求互斥、给分层方案、量化后交回决策
常见错误
max_tokens 会影响限流。这是这道题最容易拿分、也最容易被漏掉的一条。