跳转到主要内容

在解码中途缩减至零的自动伸缩器:当推理被视作无状态网络流量时

阅读需 2 分钟Tian PanTian Pan

集群完全按照我们的指令行事。流量在 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_ratioeviction_duration_secondsload_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 的架构承诺。选择其中之一是一个伪装成配置按钮的组织决策。

PodDisruptionBudgets 仅覆盖由集群发起的干扰

会员专享

余下内容仅对会员开放。

会员可读完整内容 —— 每一个公开发布的观点背后,那些框架、决策与推理的全貌。

  • 完整文章,包含未公开存档的部分
  • 可落地的工作框架,附带权衡与决策依据
  • 新文章抢先看,先于公开发布

随时取消 · 一次订阅,畅读全部

参考资料

保持联系,关注我获取更多内容

阅读需 10 分钟

当硬件说谎时:静默数据损坏遇上随机性软件

大约每千个加速器中就有一个会静默地计算出错误答案,而 LLM 推理是第一个硬件故障与采样噪声看起来完全一致的大规模工作负载。本文将探讨位翻转如何隐藏在随机输出中,以及如何利用金丝雀通道和单机统计数据来捕捉说谎的 GPU。

insider
ai-engineering
阅读需 9 分钟

混合 LLM 工作负载的 GPU 调度:那个没人解决好的装箱问题

在共享 GPU 集群上服务多个 LLM 模型会浪费 30–50% 的可用算力。本文解析 Kubernetes GPU 调度为何不适用于 LLM 推理,以及真正有效的解决方案。

insider
llm
阅读需 9 分钟

批处理负载挤占了你的实时路径:GPU 预留的惨痛教训

在夜间训练和晨间推理之间共享同一个 GPU 池看起来是提高了利用率,直到 p99 仪表板揭示了其负外部性的代价。为什么 GPU 分区必须是物理的,资源核算必须遵循延迟类别,以及早晨的尾部延迟问题无法通过软件层面修复。

gpu
scheduling
阅读需 10 分钟

配额窗口机制重写后,这批夜间脚本是如何拖垮你的交互流量的

一个夜间 LLM 批处理任务稳定运行了 10 个月,直到供应商重写了每日窗口的计算方式——将 00:05 UTC 的 Cron 任务变成了交互式流量的自杀式 429 异常。本文探讨为什么负载隔离、抖动和桶语义契约测试才是结构化的修复方案。

insider
rate-limits
阅读需 9 分钟

那个因等待另一个 Agent 而死锁的 Agent

两个能力出色的 Agent 可能会在你的账单飞涨时永远互相等待。为什么是协作——而非模型能力——导致了 Agent 群体的崩溃,以及分布式系统准则如何修复这一问题。

insider
ai-agents