在解码中途缩减至零的自动伸缩器:当推理被视作无状态网络流量时
集群完全按照我们的指令行事。流量在 45 秒内降至零,队列深度指标也归于平寂,KEDA 将副本数从 1 改为 0,90 秒后,节点自动扩缩容工具回收了 H100 Pod。图表看起来很干净。Slack 频道一片寂静。成本看板上的数字跳低了半美分。
一小时十二分钟后,一封客户支持工单送达:一个长时间运行的文档分析任务——一个预算为 28 分钟解码时间的 180k-token 推理任务——消失了。客户端 SDK 没报错。应用程序日志没异常。只有网关访问日志中埋着的一行 499,时间戳大致就在调度器判定 Pod 空闲并将其回收的时候。
那个 499 状态码是压缩成三位数字的完整故事。当上游完成响应前客户端关闭连接时,NGINX(以及大多数继承其惯例的反向代理)会记录 499。从网关的角度看,客户放弃了。从客户的角度看,网关放弃了。从自动扩缩容工具的角度看,该 Pod 上根本没有发生任何事情——请求在几秒钟前就已经流式传入,没有新连接进入,而以成本驱动的缩容 策略完全执行了它被赋予的任务。
事后分析将该事件从“自动扩缩容 Bug”重新定义为“我们编码了错误的‘繁忙’定义”。下游的一切——优雅停机窗口、队列指标、PDB、成本模型——最终都以一种协同的方式出错了,因为团队将无状态 Web 服务的假设引入到了一个工作单元是 30 分钟流式解码的工作负载中。
在流式终结点上,“请求率”和“队列深度”是谎言
当你诚实地对推理 Pod 进行仪表化监控时,你会发现的第一件事是 requests/sec 是毫无意义的。一个在 25 分钟前接受了一个长上下文请求、目前正在产生预期 8000 个 token 中的第 4812 个 token 的 Pod,其请求率为零,队列深度也为零。HPA 抓取指标,看到没有需求,便开始了缩容倒计时。
Google Kubernetes Engine 推理文档和 SGLang 观测指南都直言不讳:原始 GPU 利用率百分比是 LLM 工作负载自动扩缩容的一个糟糕的首选信号,进入的请求数也是如此。你真正想要追踪的是引擎的 在途(in-flight) 状态:当前正在解码多少个序列、KV 缓存有多满、队列中积压了多少请求。SGLang 暴露了 new_token_ratio、eviction_duration_seconds 和 load_back_duration_seconds,正是为了让你能区分“这个 Pod 是空闲的”和“这个 Pod 正忙于为一个客户生成 8000 个 token”。vLLM 通过其 Prometheus 终结点暴露了类似的指标。
错误在于你抓取的指标的上游阶段。错误在于使用单个标量(利用率、队列深度、RPS)来总结一个工作单元具有无限时长的负载。自动扩缩容控制器是一个闭合回路系统,它需要知道何时被允许采取行动,而对于任何执行流式解码或连续批处理(continuous batching)的引擎来说,“没有新请求”并不等同于“没有正在进行的工作”。
一个有用的运维规则:缩容决策应该基于“没有在途序列 且 队列为空 且 已持续 N 秒”的断言,而不仅仅是 RPS 的平滑平均值。成本团队会反对,因为标量指标更容易绘制成图表,但图表完全隐藏了这种失效模式。
terminationGracePeriodSeconds 是你没意识到自己签署的 SLA
当调度器决定驱逐一个 Pod 时,它会发送 SIGTERM 并启动一个计时器。当计时器到期时,它会发送 SIGKILL。这个计时器就是 terminationGracePeriodSeconds,默认值为 30 秒。对于处理亚秒级请求的 HTTP API,30 秒是奢侈的。对于一个 180k-token 的推理任务,30 秒相对于 28 分钟的解码时间来说只是个舍入误差。
你共有三个选择,而且没有一个是免费的:
将优雅期限设置为可忍受的最长请求时长。 如果你 P99 的请求解码需要 20 分钟,就把优雅期限设置为 25 分钟。这是最简单的修复方法,也是大多数团队最终采取的方法。权衡在于,节点排空、OS 补丁、滚动发布和集群自动扩缩容回收 Pod 都会变成每 Pod 耗时 25 分钟的操作。平台团队没人会高兴。Kubernetes 文档规定了节点升级期间尊重 PDB 的优雅期限上限——例如,Google Kubernetes Engine 在自动节点升级期间仅尊重最多一小时的期限——因此这种方法是有上限的。
让引擎在收到 SIGTERM 时取消在途工作并传播结构化错误。 这是诚实的做法:客户端知道请求被杀掉了,Pod 迅速终止,调度器保持畅通。权衡在于,现在每次部署都会发生面向客户的故障,你必须围绕它们设计重试和幂等性机制。值得注意的是:vLLM 有一个悬而未决的问题(#24584),即运行时在流式传输期间无法响应 HTTP 上下文取消——在客户端断开连接后,Pod 仍会继续生成 token 长达三分钟或更久。如果你的引擎存在此类 Bug,“在 SIGTERM 时取消”会比听起来更难。
使长作业可恢复,并将驱逐视为预期事件。 这是唯一能真正与成本驱动的自动扩缩容完美结合的选项,但它要求工作内容是可以设置检查点(checkpointable)的。对于文档分析、批量摘要或任何工作单元可以从队列重新驱动的流水线,这是正确的答案:Worker 每隔 N 个 token 将中间状态写入持久存储,队列通过可见性超时将该工作标记为进行中,而替代 Pod 在抢占后从检查点恢复。批量推理平台多年来一直针对竞价实例(spot instances)这样做——工作队列追踪已完成和待处理的项目,中断只需将未完成的工作返回队列即可。
第一种选择是一种“税收”。第二种是对客户契约的更改。第三种是需要重写 Worker 的架构承诺。选择其中之一是一个伪装成配置按钮的组织决策。
