题干与适用场景
某订单服务经压测可在 p99 延迟 250 ms 下持续处理 3,000 请求/秒。大促期间,到达流量升至 8,000 请求/秒。库存依赖变慢,在途请求数与队列年龄同时上升,客户端开始重试,而自动扩容的新实例要三分钟才能就绪。关键下单、交互式库存查询和内部批量对账共用这个服务。
请设计一套过载保护策略,让进程保持可响应,并尽量保住有价值的工作。回答需要覆盖检测信号、并发与队列边界、准入控制、请求优先级、优雅降级、重试、恢复和验证。题中数字是面试假设,不代表某个真实生产系统。
这道题归入后端,因为核心决策是单个服务如何保护自己的 CPU、内存、线程、连接和下游调用。分布式限流器负责按租户或时间窗口执行流量策略;本题处理的是合法流量仍然超过服务当前容量的情况。超时、重试和熔断器保护某一次依赖调用,过载控制则决定服务当前还负担得起哪些新工作。
面试官考察点
强回答会用本地工作量识别饱和,而不会只看请求速率。当一次请求变成原来的五倍昂贵,或依赖变慢导致连接占用更久时,固定的每秒请求阈值会失效。在途工作量、队列年龄、可用工作线程或连接、CPU、内存压力和剩余 deadline,才能暴露真正即将耗尽的资源。
第二个信号是系统是否有明确边界。无界队列只会把超额需求变成内存增长和过期请求。候选人应在瓶颈处限制并发,保持小队列或不排队,并在昂贵的解析或下游调用前低成本拒绝。目标是完成更多有用工作,不是接受所有请求,也不是追求更高的原始尝试数。
面试官还会看策略是否理解业务。关键下单可以获得预留份额;库存查询可以在契约允许时使用短期缓存;批量对账可以暂停。优先级内部仍要保证租户公平,避免大客户耗尽所有预留槽位。最后还需要一个恢复闭环:抑制重试放大,把扩容当作较慢的容量响应,逐步恢复准入,并在事故前持续演练降级路径。
回答前需要澄清的问题
- 最先饱和的资源是什么? CPU 饱和适合本地并发或成本上限;慢依赖需要独立的连接/并发舱壁;内存增长或队列年龄过高需要更短队列和更早拒绝。瓶颈不同,准入信号也不同。
- 哪些操作必须保住,哪些可以降级? 本题假设下单关键、库存允许轻微陈旧、批量对账可暂停。如果法规或业务要求每个操作都读取最新库存,缓存回退就不成立,应明确失败。
- 端到端 deadline 是多少? 排队时间同样消耗 deadline。服务应丢弃已经不可能按时完成的工作,并把取消信号传到下游。
- 请求成本是否相近? 只有成本相近时,一个请求一个令牌才合理。昂贵接口可能需要加权许可或独立资源池,防止便宜的关键工作被批任务堵住。
- 客户端能否安全重试? 被拒绝的读请求可以稍后再试;写订单需要幂等键和结果查询。服务必须区分“稍后重试”和“不要重试”,各层还要共享重试预算。
- 是否能迁移流量? 只有目标区域确有经验证的余量时,跨区接流才有帮助。盲目转移过载会制造第二个故障。
30 秒回答框架
“我会围绕瓶颈建立一个小型闭环。先用压测确定单实例并发和队列上限,再观察本地在途工作、队列年龄、CPU、内存、连接池和剩余 deadline。饱和上升时,在昂贵处理前拒绝新工作,为下单预留容量,在同一优先级内保证公平;库存查询走经过批准的缓存降级,批任务暂停。过载拒绝返回明确的临时不可用信号,客户端只对幂等操作使用退避、随机抖动和共享重试预算。自动扩容能补容量,但来不及充当第一道防线。恢复时使用滞回和逐步放量,并通过过载压测验证内存有界、有效吞吐、关键请求成功率、公平性、重试放大和恢复过程。”
分步骤深入解答
先建立经过测量的容量边界。3,000 请求/秒只对压测时的请求组合、依赖延迟、实例数和 250 ms p99 目标有效。记录这个边界对应的单实例在途量、CPU、内存、工作线程利用率和下游连接占用。请求速率是输入,准入决策应跟随最接近失效的资源。例如库存变慢后,相同到达速率会产生更多在途调用,纯速率控制器会反应过晚。
围绕稀缺资源设置相互独立的并发限制。订单处理器有整体上限,库存调用和批任务再使用更小的舱壁。分配昂贵资源前先获取许可,并在成功、失败、超时或取消时释放。请求成本差异明显时,使用加权许可或独立端点资源池。根据压测得出的静态硬上限是安全起点;自适应上限可能提升利用率,但必须有稳定反馈、护栏和快速回滚方式。
队列必须显式且有界。短队列可以吸收已知突发,但长度应由 deadline 余量决定,不能按剩余内存决定。队列已满,或预计等待后已经没有足够时间执行时,直接拒绝。无界队列不会创造容量,只会拉高尾延迟、占用内存,并让客户端重试仍在等待的请求。既要监控深度,也要监控最老任务年龄,因为少量昂贵任务同样可能已经过期。
准入控制要尽早、低成本且前后一致。网关执行租户契约配额和粗粒度流量上限;每个服务实例根据本地饱和度保护自己拥有的资源。下游微服务已经过载时,上游应在完成注定被丢弃的工作前拒绝。请求关键等级需要沿调用链传递,避免同一个订单在前层获准,却在已经消耗大量工作后被后层随机丢弃。
用明确的容量预留实现优先级:
| 类别 | 过载动作 | 原因 |
|---|---|---|
| 下单 | 预留并发;达到自身上限后仍要拒绝 | 保护关键路径,同时不授予无限容量 |
| 库存查询 | 优先返回带明确新鲜度契约的短期缓存;否则拒绝 | 在不伪造答案的前提下降低下游工作量 |
| 批量对账 | 暂停拉取,之后从持久进度恢复 | 工作重要,但不需要交互式 deadline |
没有公平性的优先级会饿死小租户。在同一类别内应用租户配额或公平调度,并为恢复与控制流量保留最小份额。不要设计几十个优先级;事故期间,运维人员必须能预测某类请求是否会被准入。
达到上限后,要在数据库或依赖调用前返回。HTTP 503 表示临时过载,可附带 Retry-After;它不等于允许所有客户端同时重试。客户端需要带上限的指数退避、随机抖动、deadline 和重试预算。只在一个合适层级重试,不让每层都试。创建订单复用幂等键;超时结果不明确时先查询原结果。调用方 deadline 已过的请求应取消下游工作,避免完成无人需要的响应。
优雅降级通过减少工作量止损。若产品契约允许,库存查询可以跳过可选扩展,或使用有最大年龄的缓存。批量消费者可以停止拉消息。把陈旧库存标成最新库存属于不诚实回退;必须新鲜时,应返回明确的不可用结果。平时要让一小部分流量持续经过降级路径,因为从未使用的应急路径很可能在事故时失效。
自动扩容、跨区接流和增配仍有价值,但应位于准入控制之后。按原始请求数扩容可能在便宜流量到来时多加实例,却在昂贵请求下反应过慢;指标应包含并发、队列年龄或资源饱和度。新实例应预热连接后再接满流量。故障转移前必须验证目标容量。这些机制都不能替代本地边界。
恢复阈值应低于进入过载的阈值。在途量、队列年龄和依赖健康度持续低于退出线一段时间后,再分阶段提高准入量。正常延迟稳定前继续保留优先级份额。这种滞回和渐进放量可以避免系统在正常与过载间来回抖动,也防止把只恢复了一部分的依赖再次压垮。
验证不能停在标称容量测试。按生产请求成本组合回放流量,在库存变慢且自动扩容延迟三分钟的同时,将到达速率从 3,000 提高到 8,000 请求/秒。再加入同步客户端重试、一个滥用租户、过期 deadline 和依赖恢复。断言队列与内存有界、线程和连接稳定、拒绝成本足够低、下单获得承诺份额、租户公平、降级诚实、重试放大受控,并且能逐步恢复。要分别统计完成的有用操作、接受请求数和下游尝试数。
高质量示范回答
“3,000 请求/秒是某种请求组合下的压测结果,我会先确认边界处到底是哪种资源耗尽。库存变慢期间,在途调用数和连接占用比请求速率更能反映风险。我会为服务设置经过压测的单实例并发上限,为库存调用再设一个更小的舱壁,并只保留一个仍能满足请求 deadline 的短队列。触达任一边界后,在昂贵工作前拒绝。
流量分为下单、库存查询和对账。下单有预留容量,但仍有硬上限;库存只有在 API 明确新鲜度契约时才能使用短期缓存;对账暂停并从持久进度恢复。每一类内部都做租户公平,不能让一个客户拿走全部份额。我还会把关键等级传给下游,避免请求在调用链末端才被随机丢弃。
临时过载返回可识别的 503;能够估计时附带 Retry-After。客户端仍要使用有上限的退避、随机抖动、deadline 和共享重试预算。只让一个层级重试,订单写入复用幂等键。调用方已经超时的工作要向下游取消。
由于新实例要三分钟,自动扩容是较慢的容量环。我会按饱和信号扩容并预热新实例,而本地准入负责保住现有实例。恢复时使用更低的退出阈值和分阶段放量。
最后用 8,000 请求/秒、慢库存、延迟扩容、重试、混合请求成本和噪声租户做过载测试。预期看到内存和队列有界、有效吞吐稳定、下单份额和公平性成立、拒绝成本低、没有重试风暴,并在库存恢复后受控回到正常状态。”
常见错误
- 不断提高队列上限直到错误消失 → 接受的请求等待更久、占用内存、过期并引发重试,却没有增加执行能力 → 按 deadline 余量限制队列并尽早拒绝。
- 只按每秒请求数检测过载 → 请求成本和依赖延迟会变化,相同速率可能安全,也可能致命 → 观察本地在途量、队列年龄、资源饱和和依赖池。
- 让自动扩容充当第一道防线 → 三分钟延迟足以让队列和重试破坏现有实例 → 先保持本地准入边界,再扩容恢复余量。
- 给关键流量无限优先级 → 它仍会耗尽相同资源,并饿死恢复工作 → 预留容量,同时保留硬上限和公平性。
- 每个微服务都随机卸载 → 请求在后层被拒绝前已经消耗上游工作,端到端有效成功率下降 → 传递关键等级,并在知道瓶颈后尽早拒绝。
- 返回 503 后让所有客户端重试 → 同步重试会放大超额流量 → 使用随机抖动、deadline、单层重试和重试预算。
- 返回未标注的陈旧数据 → 表面可用,却破坏库存语义 → 公开新鲜度契约,或明确失败。
- 恢复后立刻接回全部流量 → 刚恢复的依赖再次过载 → 使用滞回、有界探测和逐步放量。
- 只统计接受流量 → 高接受率可能掩盖超时、浪费工作和重试 → 统计有效完成、拒绝成本、deadline 浪费和尝试放大。
追问及应对
追问 1:为什么不能只用分布式限流器解决?
分布式限流器适合执行契约配额、防滥用和请求进入服务前的流量整形。它无法单独感知库存延迟已经提高了每个合法请求的成本。保留网关限流,再由资源拥有者使用本地并发和队列保护。如果请求成本稳定且服务只有一个瓶颈,保守的速率限制可能就是更简单且足够的方案。
追问 2:并发上限如何确定?
先用生产请求组合压测,找到仍满足延迟和资源目标且保有余量的最高并发,再在依赖变慢时重复测试。稳态下并发、吞吐和耗时之间的关系可以用于合理性检查,但无法保证突发流量或混合成本下的容量。先以只观察模式上线,再灰度拒绝,最后强制执行;代码、实例规格或依赖变化后重新评估。
追问 3:如果 90% 的请求都声称自己关键呢?
此时标签已经不能帮助准入。关键等级应来自业务操作,由可信入口设置,并且每类都有上限,只预留经过测量的份额。关键类内部继续做租户公平或用户稳定优先级,避免只偏爱最吵的客户端。如果真正关键的需求也超过物理容量,仍然会有关键工作失败,契约必须说明取舍。
追问 4:自适应并发会不会更好?
它能比静态上限更快跟随服务耗时变化,但有噪声的延迟和滞后反馈可能引起振荡或误卸载。先从压测后的静态边界开始。只有同时具备最小/最大限制、平滑信号、滞回、稳定回退值,以及覆盖快速恶化和缓慢恢复的回放测试时,再引入自适应控制。
追问 5:深调用链应该在哪一层卸载?
资源拥有者必须保留本地最后一道保护。它一旦发出过载信号,上游应更早停止工作,并沿调用链保持相同关键等级。只在边缘卸载无法精确知道下游状态,只在叶子卸载会浪费上游工作。实用方案是粗粒度边缘策略、本地保护,以及上游可消费的过载信号三者结合。
追问 6:哪些生产指标能证明策略有效?
按操作和优先级统计有效完成量与延迟;准入、排队、降级和拒绝数;最老队列年龄;在途工作;CPU、内存、工作线程和连接饱和度;deadline 过期工作;租户公平性;每个逻辑请求的物理尝试数;扩容就绪延迟;以及过载模式持续时间。关键份额失守、长期过载、拒绝成本接近正常处理成本,或恢复放量反复退回时都应告警。