2.3 降级与排队:过载时不硬扛,保住核心体验 限流是「提前按计划挡」,但再精密的限流也挡不住一切:一次未曾预料的爆款事件、一个下游模型的集体降速、一段网络分区,都能让系统进入「过载」状态。这一节要解决的,正是限流之后的「后手」问题。读者读完,应该能一句话说清楚:过载时优先降级而非硬扛,用优先级队列保住付费与核心用户体验,让系统在「少服务一点」和「全崩」之间,永远选前者。 2.3.1 过载时的两条路:硬扛 vs 优雅降级 系统过载时有两条截然不同的路。一条是「硬扛」:继续接收所有请求,让它们在内存里排队、让延迟一路飙升,直到某一层 OOM、连接表打满、连锁雪崩。另一条是「优雅降级」:主动承认「我现在能力有限」,关闭或替换掉非核心能力,用更便宜的方式满足用户,保住核心链路不挂。
限流是「提前按计划挡」,但再精密的限流也挡不住一切:一次未曾预料的爆款事件、一个下游模型的集体降速、一段网络分区,都能让系统进入「过载」状态。这一节要解决的,正是限流之后的「后手」问题。读者读完,应该能一句话说清楚:过载时优先降级而非硬扛,用优先级队列保住付费与核心用户体验,让系统在「少服务一点」和「全崩」之间,永远选前者。
系统过载时有两条截然不同的路。一条是「硬扛」:继续接收所有请求,让它们在内存里排队、让延迟一路飙升,直到某一层 OOM、连接表打满、连锁雪崩。另一条是「优雅降级」:主动承认「我现在能力有限」,关闭或替换掉非核心能力,用更便宜的方式满足用户,保住核心链路不挂。
我强烈建议选后者。 因为对话系统的过载往往有「自我强化」特性:延迟升高 → 用户不耐烦重发 → 请求更多 → 更慢。这种正反馈一旦形成,硬扛只会加速崩。降级则是「主动打破正反馈」——我少做点、做快点,先把核心体验保住。
降级不是「随便砍功能」,而是「砍非核心、保核心」。所以降级的起点,是先给能力分级。以 AI 对话引擎为例,能力大致可分三档:
经验法则:从增强到重要,逐级降级,永远不动核心。 这样用户感知是「回答变简单了」,而不是「系统坏了」。
当模型集群过载,最实用的两个降级手段是「缓存答案」和「轻量模型顶上」。
手段一:语义缓存兜底。 第 3 章会系统讲缓存,但这里要先点出它的降级价值:当大模型慢或不可用时,如果用户的请求和近期某次请求语义相近,直接返回那条缓存答案(带「答案可能不完全匹配」的提示)。这比让用户干等或报错好得多。缓存命中率每高一点,降级时的兜底能力就强一点。
手段二:小模型/蒸馏模型顶上。 准备一条「降级专用小模型链路」:平时它承接简单闲聊,过载时让它临时接管更多流量。小模型单卡并发高一个数量级、成本低得多,用它顶上能极大缓解大模型压力。代价是答案质量下降——但这是「可接受的质量降级」,总好过无答案。
手段三:静态兜底与排队提示。 极端情况下,返回一句「服务繁忙,已为你排到队列第 N 位,预计等待 X 秒」,并给出预估,比冷冰冰的 5xx 体验好太多。用户知道「系统在管我」,流失率会明显下降。
降级解决「怎么答得省」,排队解决「先答谁的」。当 capacity 实在有限,必须决定请求的处理顺序——这就是排队策略。
为什么用优先级队列而非 FIFO:先进先出看似公平,实则粗糙。在商业系统里,付费用户、VIP、核心 API 调用方,理应比匿名游客、刷量请求优先级更高。用单一 FIFO,等于让付费用户和最劣质流量「同等待遇」,既不合理也伤收入。
优先级怎么划:最常见是按「用户等级 / 租户 SLA / 请求价值」分档:
做法是:一个带优先级的队列,调度器总是先取高优先级请求去消耗有限的容量。当容量触底,低优先级请求直接进降级或拒绝,高优先级始终有路可走。
排队救急,但用错了比不排队更糟。两个必须守住的约束:
约束一:队列必须有上限,且必须计时。 无限队列 = 把「拒绝」推迟成「无限等待」,用户感知更差,内存也会被堆积请求吃光。队列要设最大长度,超出的低优先级请求直接降级/拒。同时每个请求在队列中要有 TTL(生存时间),超时未处理就移除并降级返回,绝不让孩子「排队排到天荒地老」。
约束二:给排队用户明确预期。 一旦决定让用户排队,就要回传「你在第几位、预计等几秒」。这种「可预期」能显著降低焦虑和流失。技术上可以在响应头或首帧里带 X-Queue-Position 和预估等待。没有预期的排队,用户只会反复刷新,反而制造更多请求(又回到硬扛的正反馈)。
90% 的人会在这里踩坑:把「排队」当成「万能缓冲」,设一个巨大队列,心想「先收着总能处理」。结果队列里堆了 200 万请求,平均等待 30 秒,用户全跑了,而系统还在「认真处理」早已没人等的请求。队列是缓冲,不是黑洞——它必须有出口、有上限、有超时。
最后补一个和降级、排队配合的关键机制——熔断(Circuit Breaker)。它和限流、降级是三兄弟。
原理:持续监测某个下游的健康度(错误率、延迟)。当错误率超过阈值,熔断器「跳闸」,后续请求不再打向该下游,而是直接走降级路径,给下游喘息时间。过一段冷却期后,放少量探活请求试探,恢复了再闭合。
为什么必须和降级联动:熔断跳闸后,请求不能「无家可归」,必须有降级答案接住——这正是 2.3.3 那一整套降级手段的价值。没有降级的熔断,等于「跳闸即报错」;有降级的熔断,等于「跳闸即优雅兜底」。
把 2.2 的限流和本节三件套合起来,一个完整的过载防护网是这样的:
这张图是接入与限流层的终极全景:限流按计划挡、熔断按实况隔离、降级与排队按优先级兜底。三者协同,系统就能在「全崩」之前,先「少服务一点」,把核心体验守到最后。
降级策略绝不能写死在代码里。正确的做法是把所有降级开关做成可动态下发的配置:特性开关或配置中心里的一项项布尔值或数值,运维在监控发现过载时一键切换,无需发版。至少要把这几项做成热更新:
这样,一次降级就是「改几个配置、推送到全网」的事,从决策到生效以秒计。没有这层工程化,降级就只能是「紧急发版」,等你发完版,雪崩早已发生。
举个具体的演练场景,体会降级链路怎么接力。某对话产品晚高峰流量达到平峰的 3 倍,模型集群 P99 从 2 秒涨到 8 秒,连接池占用率触顶、背压开始频繁触发。监控告警后,运维依次做了四件事:
结果:核心链路可用率维持在 99.5%,付费用户体验基本无损,普通用户体验「变简单了」但全程无 5xx。这就是「少服务一点、而非全崩」的精髓。这类演练应当定期用混沌工程做,别等真故障才第一次跑。
人们常忘一件事:过载解除后的「恢复」也能打挂系统。如果系统在过载时堆积了海量请求,一旦容量恢复就瞬间全量放行,相当于制造第二次洪峰——这叫恢复风暴。正确做法是渐进恢复(warm-up):容量回来后,先放 30% 流量、观察延迟与错误率稳定,再逐步提到 60%、100%。熔断器从「打开」到「闭合」也应经历「半开探活」阶段,先放少量请求试探下游是否真恢复。恢复要像限流一样被设计,而不是简单地「把闸一开」。
降级不只是后端动作,也是一种「用户沟通」。当系统进入降级,应在 UI 上坦诚提示,比如「当前为高峰期,回答由轻量模型生成、可能更简洁」。这种透明反而提升信任、降低焦虑,比悄悄降级被用户发现「质量变差了」更得体。把降级当成一次「体面的妥协」而非「偷偷减配」,是产品成熟度的体现。
读完 2.3,请带走这些结论:
至此,第 2 章三节全部讲完。下一章(第 3 章·上下文与缓存)我们会看到:接入层整形放行后的流量,如何被「缓存」这一最大杠杆进一步降本——它正是 2.3 里反复提到的「语义缓存兜底」的真正主场。