日期:2026-08-17
我们的模型推理服务早期做版本升级,图省事直接把旧进程 kill 掉,新进程起来接流量。平时没事,一到模型体积大、生成时间长就出乱子。有一次大促期间升级,半截正在生成的回答直接断在中间,调用方收到空响应就重试,重试又撞上新进程预热没好,雪崩一样把上游拖挂了快十分钟。复盘会上一句话点透,问题不在升级本身,在我们把实例当一次性容器用,没给它把在途活干完的机会。
现在升级走的是标准下线流程。调度系统先把这个实例从服务发现里摘掉,新的请求不再路由进来。已经在手里的请求继续跑完,这叫 draining,长连接也逐步关,不强行断。等到在途请求清零或者超过一个保护时长,进程才真正退出。健康检查在这期间上报摘流状态,上游看到的是实例慢慢变少而不是突然全没。重试和退避策略也配合改了,新进程没就绪前上游不会疯狂重试。
首先要解决的是在途请求怎么算完。生成式模型一个请求可能跑十几秒,我们给每个实例配了在途计数器,draining 阶段盯着它归零。接着要解决的是长连接平滑关闭,WebSocket 那种流式会话不能一刀切,我们发了个关闭帧让客户端优雅收尾,再等一小段才断。还有一块难啃的是摘流和下线的时序,摘早了新请求进不来但旧请求还没清,摘晚了又有新请求塞进来拖长窗口,我们用健康检查的两种状态把这个时序卡准,摘流和 draining 是两段明确分开的信号。
案例片段(已脱敏): 实例下线流程配置片段(字段示意):
lifecycle: drain_timeout: 30s # 在途请求最多再跑 30s shutdown_delay: 2s # 发关闭帧后等 2s 再断连接 health: "draining" # 摘流期间上报,不再接新流量 force_exit_after: 45s # 兜底强杀,防卡死一次演练:某大模型实例在途 12 个长生成请求,摘流后 28 秒内全部跑完,连接平滑关闭,期间上游零新请求进入,升级窗口未触发重试风暴。
升级窗口期间的调用报错率从之前的两位数百分点降到近乎为零,因为再也不断在途请求。在途请求完成率稳定在接近百分之百,长连接在关之前都收到了完整响应。单次升级窗口时长从不可控的十几分钟收到了一分钟内,主要是 draining 时限让流程可预期。上游重试量在升级时段下降明显,雪崩那类事故没再发生。得承认,drain_timeout 我们一开始设成 10 秒,结果长生成请求被强杀,后来才放宽到 30 秒。
优雅关闭这事,说白了就是把实例当活物而不是一次性容器,给它把活干完的余地。draining 加健康检查两段式信号,在我们所有推理服务里都是标配了,不只是大模型。force_exit 兜底一定要留,不然遇到卡死的请求会拖垮整个升级。重试策略那块我后来觉得,上游在目标没就绪前就该安静,乱重试只会把小故障放大,这点和网关的取消传播其实是一回事。
优雅关闭这事说白了就是把实例当活物而不是一次性容器,给它把在途活干完的余地。我们那次大促雪崩,根子不是升级,是升级时把正在生成的请求硬掐了,上游重试又撞预热,小故障被放大成十分钟事故。draining 加健康检查两段式信号,现在是我们所有推理服务的标配,不只大模型。force_exit 兜底一定要留,不然遇到卡死的请求会拖垮整轮升级。重试策略那块我后来想明白,上游在目标没就绪前就该安静,乱重试只放大故障,这点和网关的取消传播其实一回事。如果重来,我会把 drain_timeout 一开始就设宽松,我们曾设成十秒把长生成请求强杀,反而制造错误,后来放宽到三十秒才稳。我们把这套和发布系统的健康检查打通后,升级窗口从不可控变成可预期,运维终于敢在大促前做小版本更新,不用非等到深夜。在途计数器我们接了监控面板,升级时谁都能看见还有几个长请求在跑,心里有底就不慌。摘流信号和 draining 信号我们拆成两段,调度先摘流再等清零,时序错了就会出现新请求混进来的情况,这一步调过才顺。团队终于不怕升级了,这比省时间更值钱,之前那次事故我们记了很久。在途计数归零的判定我们最初只看请求数,后来发现长连接也算在途,才把连接数也纳入,否则 draining 会提前结束。升级演练我们每月做一次,不演练真出事就手忙脚乱,这套现在写进了上线手册,新人照着做就行。
我们还把 draining 状态接进了发布系统看板,值班的人不用登机器就能看见每个实例是摘流中还是已退出,哪个卡在 draining 超时直接告警。这层可见性让我们敢把升级放进白天做,不必再深夜偷偷摸摸。drain_timeout 设成三十秒后,连最长的生成请求也能在窗口内收尾,强杀次数从每周几次降到零,升级这件事终于不再是心理负担。