如何诊断并修复 Kubernetes 上的 AI 推理延迟
周四下午两点十五分左右。两周前,你们团队上线了一个内部助手。演示反响不错,财务部有人问它能不能看合同。消息就这么传开了。今天,所有人第一次同时用上了它。
支持频道里,同一个抱怨以六种不同语气刷屏。是挂了吗?我的怎么一直转圈。早上还好好的。有人发了一张加载动画的截图,什么都没配——你猜他发得还挺开心。
于是你打开了监控面板。
节点就绪。Pod 正常运行。CPU 占用 20%。没有重启,没有 CrashLoopBackOff,没有触发任何告警。从 Kubernetes 给出的所有指标看,系统一切正常。
但它并不正常。有人已经等了十一秒还没看到回答的第一个字,他们马上就要回去用老办法干活了,而且不会再回来。
这就是本文要讲的故障。不是那种宕机,而是那种安静的故障:一切技术上都在正常运行,产品却已经没法用了。
Table of Contents
你的第一直觉是错的
此刻的本能让你去怀疑模型:版本不对、量化质量差、上下文太长,或者有人改了 system prompt。
问题几乎从未出在模型上。
追一个请求的完整链路,你会发现它经历四个阶段:被路由到某个副本、在队列中等待、模型读取 prompt、模型流式输出回答。只有后两步真正消耗了你付费购买的模型算力,前两步纯粹是排队和路由,属于基础设施层面的事。
当人们说模型慢的时候,模型本身通常没问题,瓶颈在别处。
你不孤单
在开始排查之前,值得先知道:这是该领域最常见的抱怨,不是你配置侥幸撞上的特例。
一项针对 200 名生产环境运行 AI 系统的从业者的调查显示,近半数(49.5%)的人将峰值负载下的延迟列为最难解决的扩展性问题。不是准确性,不是幻觉,不是单 token 成本,而是当用户正试图使用该功能的那一刻出现的延迟。
同一研究还有两组数据值得对照。59.5% 的人认为将推理部署在离用户或决策点更近的地方至关重要或非常重要,而 45.5% 的人仍然只在单一云区域提供服务。人们知道就近部署很重要,却没能付诸行动,这恰恰说明了搭建多区域 GPU 容量的成本有多高。
同一项研究还提到,一些机构要求响应时间低于 250 毫秒,同时保证 99.9% 的可用性。仔细看看这个要求,因为它在字面意义上是无法实现的。没有任何有意义的 LLM 响应能在四分之一秒内完成。这应该是指首个 Token 的到达时间(即答案的第一个词出现前的等待时长),而这恰恰是你应当设定的正确标准。一旦 Token 以可读的速度流动,用户就不再计算时间了。正是首个 Token 出现前的那段沉默让人失去耐心。
部署时没人告诉你的事
这一点解释了其他所有问题。
普通 Web 请求短小,成本与上一个请求大致相同。Kubernetes 的调度器基于此假设。服务负载均衡基于此假设。Horizontal Pod Autoscaler(HPA)基于此假设。你的 Ingress Controller 默认超时设置也基于此假设。没人把这些假设写下来,因为在过去的十五年里,它们确实都是真的。
LLM 请求在三个方面打破了这些假设。
它分两个阶段运行,且两个阶段的特征截然不同。预填充阶段(Prefill):模型一次性读取整个 Prompt。这是计算密集型任务,具有突发性,决定了用户面对空白屏幕的时间长短。然后是解码阶段(Decode):逐 Token 生成,每个 Token 都需要之前所有 Token 累积的状态。这种状态就是 KV 缓存。它存储在 GPU 内存中,紧挨着模型权重,并随着回答长度的增加而增大。
因此,请求并不短小,可能运行长达一分钟。成本也不相同,因为粘贴了一整篇文档的 Prompt,其成本可能是单行指令的五十倍。而真正稀缺的资源是 GPU 内存,但它并不会出现在你继承的某一块单一仪表盘上。
这就是为什么你的监控系统显示一切正常。因为它测量的是错误的机器。
那十一秒去哪儿了
跟随请求穿过这四个阶段,故障模式会按顺序浮现。以下是从业者最常提到的六种情况,它们能清晰地对应到请求路径上。
第一阶段,路由:你的负载均衡器在盲猜
一旦请求成本出现巨大差异,轮询(Round-robin)就不再公平了。三个重度 Prompt 落到同一个 Pod 上,而旁边的 Pod 只处理单行指令。那个负载重的 Pod 队列变长,尾延迟上升,但集群平均指标看起来依然非常合理,这就是为什么今天之前没人注意到问题。
长连接会让情况更糟。使用 HTTP/2 或 keepalive 时,客户端只开一条连接,所有请求都走这条连接,而 Kubernetes Service 是按连接而非按请求做负载均衡的。于是全部流量都钉在一个后端上,不会分散。
还有一个大多数团队从未想过的成本问题。如果某个副本的 KV cache 里已经有这条 prompt 的前缀,把请求路由过去就能省掉真正的 prefill 计算。生产环境中的 prompt 大多共享一段很长的 system 前缀,所以这不是边缘情况,而是你大部分流量的常态。而轮询调度等于每次都随机扔掉这个好处。
第二阶段,排队:没有人会来救你
你的 autoscaler 可以扩容,但它总是来得太晚,原因很朴实:新副本要拉取数 GB 的服务镜像,再通过网络下载 20GB 到 70GB 的权重,最后加载进 GPU 显存。这是分钟级而非秒级的事,而且前提是集群里已经有带空闲显卡的节点。如果没有,还得加上节点开通时间,再加一个变量:你的云厂商今天在这个可用区有没有货。
在这之上还叠着第二个问题:扩容信号通常是错的。CPU 在这里毫无参考价值,因为 GPU 干活时 CPU 是空闲的。GPU 利用率也强不到哪去:它只反映采样窗口内有 kernel 在执行,处理 1 个请求的服务器和处理 60 个请求的服务器读数都是 100%。它无法告诉你有没有人在排队等待。
队列深度可以。vLLM 已经把 running 和 waiting 的请求数以 Prometheus 指标的形式暴露出来了,可以据此扩缩容:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: llm-server
spec:
scaleTargetRef:
name: llm-server
minReplicaCount: 2
maxReplicaCount: 8
cooldownPeriod: 600
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring:9090
query: sum(vllm:num_requests_waiting{app="llm-server"})
threshold: "5"
请对照你的 vLLM 版本核对指标名称。多个版本之间有些指标被重命名过,而且改错了不会有任何报错。
配置里有两个值是刻意设置的。最小副本数为 2,因为冷启动意味着不能缩到零还指望能正常服务。冷却时间为 10 分钟,是因为激进缩容意味着很快又要再付一次冷启动的代价。
由此引出一个令人不安的结论:针对推理服务的自动扩缩容,响应式扩容永远滞后于冷启动时长,没有任何触发机制能解决这个根本问题。解决办法是维持比直觉更宽的冗余容量:始终运行额外的副本,比预估更早扩容,以及更慢地缩容。这也意味着 scale to zero 并不适合面向用户的推理场景,它只适用于批量任务——那种没人坐在那儿等响应的场景。
第三阶段,Prefill:GPU 就在那儿,但毫无用处
Kubernetes 以毫核分配 CPU,GPU 则是整卡分配。一个 Pod 申请一张卡,无论它只需要 10% 还是 100% 的资源,它拿到的都是整张卡。
资源浪费是最明显的问题,但真正让人崩溃的是碎片化。一个 70B 模型的 16-bit 权重大约需要 140GB 显存,因此一个副本需要单个节点上的多张 GPU。你可能有四台节点,空闲 GPU 分布为两、二、一、一,副本却永远卡在 Pending 状态。账面容量充裕,实际无一可用,而且不会触发任何告警,因为系统没有坏。
共享单卡有三种方案,每种都有必须知晓的坑:Time-slicing 开启容易,但没有内存隔离,一个贪心的 Pod 可能拖垮邻居,只适合开发环境,不能用于生产;MIG 提供真实的硬件隔离,但切片尺寸固定,意味着你得在业务量出现之前就预测好负载组合;Dynamic Resource Allocation 才是结构上正确的答案,它将加速器请求正式纳入了核心 Kubernetes API。
这些功能默认均未开启。启用它们需要决策者理解工作负载特性,而这个人通常不在集群搭建时现场。
第四阶段,Decode:Ingress 正在掐断用户连接
许多 Ingress 控制器默认配备 60 秒的读取超时和响应缓冲。
超时机制会在长文本生成中途强行终止,用户报告为崩溃,而你无法复现,因为测试用的 Prompt 太短。缓冲机制会积攒 Token 再成批释放,破坏了流式体验,尽管模型运行完全正常。
两行配置,是为一个早已不存在的负载场景写的。
贯穿四个阶段的底层问题:突发流量,以及无人认领的故障
内部 AI 工具面临的流量并非平稳持续,而是突发尖峰:早上九点全员开工、公司全员大会结束后的一个小时内,或是有人把链接甩到繁忙的 Slack 频道里并称赞“这东西真不错”的时候。固定的副本数只能应对上述场景之一。多数团队只在业务清淡的一周设定一次参数,之后再未回顾,导致同一天内既遭受资源浪费又发生服务崩溃。 还有第六种故障模式,它并非技术问题,恰恰是前五种故障能存续数个季度的根源。 平台工程团队负责集群。集群指标一片绿。在他们看来,工作已经完成且完成得很好。应用开发者能察觉到延迟很高,却看不清原因,因为所有问题都隐藏在调度、扩缩容和路由中——这些既非他们所能控制,也非他们所能观测。MLOps 团队拥有模型制品,却对决定周四下午两点十五分性能表现的因素知之甚少。 三个团队,各自陈述事实,而问题恰恰存活在他们排班轮值之间的缝隙里:那里没有配置任何告警,也没有任何仪表盘指向它。与此同时,平台并未停滞
这个故事令人鼓舞的部分在于,上述缺口是已知问题,且修复方案正在逐步交付。 动态资源分配(Dynamic Resource Allocation)将加速器硬件请求纳入核心 Kubernetes API,使调度器能够基于设备本身进行推理,而非计算不透明的资源单位。Kueue 在 CPU 和内存之外,增加了作业排队和多租户 GPU 配额机制,这能阻止研究作业抢占用户依赖的服务端点。Gateway API 推理扩展和 llm-d 引入了模型感知路由:支持请求优先级判断、基于实时模型指标的动态负载均衡,以及对缓存状态的感知。最后一点直接回应了第一阶段中“盲目猜谜”的负载均衡器问题。 已发布的基准测试提供了规模感:Kueue 将多阶段作业的完成时间(makespan)缩短最多 15%,动态加速器切片将平均作业完成时间减少 36%,Gateway API 加 llm-d 在高负载下将首字延迟(time to first token)的尾部改善最高达 90%。把这些数字当作提升空间来看,而不是预测。最高 90% 是在一个刻意选择用来展示改进效果的配置上测得的,你实际能获得多少提升,完全取决于你的基线有多差。如果你已经在用 least-outstanding-requests 负载均衡并预热了缓存,那提升会小得多。但如果你用的是默认 ingress 下的原生 round-robin,提升可能会非常可观,因为那个基线确实很糟糕。
关键在于方向。最大的公开提升出现在长尾的 time to first token 上,而这恰恰是受访从业者中半数人指出的最头疼的问题。有人已经为人们抱怨的痛点造出了修复方案。
这一切是怎么演变到今天的
值得稍微绕开讲一下,因为它解释了为什么相关工具来得这么晚。
当生成式 AI 出现在企业的规划文档里时,一种流行的论断是:Kubernetes 扛不住它。容器编排是为小型、可互换的 CPU 和内存单元设计的,讲究廉价重启、副本可替换。而 AI 需要的是承载庞大规模状态、调度起来耗时数分钟的专用芯片。一个专门构建的平台即将出现——当时大家都这么认为。
但那个平台从未出现。AI 进入了微服务技术栈,Kubernetes 把它吸收了。
调查数据说明了原因。超过半数的企业根本不训练模型,每天实际部署模型的只占 7%。与此同时,82% 的容器用户在生产环境运行 Kubernetes,66% 托管生成式 AI 的组织已经在那里提供服务。整个行业花了好几年为一个几乎没人拥有的工作负载做设计,而其他人则默默地下载模型权重,把它们接到一个 endpoint 后面。
真正收敛的不是某个运行 AI 的专用场所,而是一套运行 AI 的标准方式,至于是什么场所则留待协商。这也正好解释了为什么你今天面对的问题是放置、路由和容量,而不是任何与模型本身相关的难题。
火扑灭之后该做什么
从"能跑通"到"可靠",归结起来就是五件事:容量、性能、放置、韧性和所有权。
容量
以每秒请求数衡量性能毫无意义,因为一个请求可能包含 50 个 token,下一个却高达 8,000 个。应按每秒 token 数做容量规划,并分别统计 prompt 和 completion 的吞吐,因为它们对系统不同部分造成不同的压力。Prompt token 在 prefill 阶段会引发计算峰值,而 completion token 在 decode 阶段则是持续的低强度输出,并在整个响应期间占用 GPU 显存。
用最坏但现实中的 prompt 和并发度做负载测试,并以此为基准规划容量。基于短 prompt 的压测会得出一个令人放心的数字,但在上线第一周就会彻底失效。
在资源供给方面,GPU 应提前预留,而不是临时申请。保持一个稳定的基线资源池,并在此基础上允许突发。在真正需要资源之前,务必确认供应商的配额和各区域库存情况,并使用 Kueue 防止某个团队悄悄耗尽整个资源池。
性能
最关键的两个指标是:首字响应时间(TTFT),即用户从发送 prompt 到看到答案第一个字所等待的时间,它决定了用户是否信任该产品;以及 token 间延迟,即答案中每个词之间的间隔,它决定了阅读体验是否流畅。
这两个指标都应追踪 p95 和 p99 分位数,而不是平均值。p95 表示 95% 的请求在此时间内完成,反映的是最慢的 1/20 用户体验到的情况;p99 同理,对应最慢的 1/100 用户。平均值可能看起来很健康,但这部分慢速用户的等待时间却长得离谱,而他们恰恰是最容易放弃的人。
接下来,优化推理服务层。这是负责加载模型和处理请求的软件,例如 vLLM,也是能以最少投入获得最大收益的地方。持续批处理(Continuous batching)能让 GPU 保持忙碌,同时避免早期请求因等待批处理填满而久候。量化(Quantization)在质量可接受的前提下可将显存占用大致减半,腾出的显存就能支撑更多并发请求。将最大上下文长度设置为产品实际所需的值,而非模型支持的上限。如果模型支持 128K 上下文,而你的最长真实 prompt 仅 6K,那等于为永远不发生的场景预留了 KV cache。只需改一行配置,就能显著提升有效并发数。
资源放置
给 GPU 节点添加污点(Taint),使普通工作负载无法调度到上面,只有带对应容忍(Tolerations)的推理 Pod 才能部署在这些节点上。日志 sidecar 绝不应成为模型副本无法调度的原因。
让权重数据靠近节点。使用节点本地 NVMe 或同可用区缓存,可将 40GB 的网络拉取转化为快速的本地读取,这能直接改善冷启动速度、自动扩缩容的滞后以及周四下午让你头疼的峰值延迟。这是清单中投入产出比最高但最不起眼的优化手段。
尊重硬件拓扑。通过 NVLink 互联的 GPU 与仅共享 PCIe 总线的 GPU 表现截然不同。对于分片模型而言,这种互联结构处于每个生成 token 的关键路径上。
对地理位置保持诚实。如果用户位于伦敦,而 GPU 部署在弗吉尼亚,每个 token 都要跨越大洋。即使模型调优得当,漫长的网络路径仍会导致产品响应迟缓。
韧性
就绪探针(Readiness)应在模型完成加载并真正具备服务能力时才通过。使用具有宽松失败阈值的启动探针,否则存活探针(Liveness)可能会在权重加载中途杀死 Pod,导致重启循环,这看起来像个谜,却会耗费你一小时的排查时间。
设置 PodDisruptionBudget(PDB),防止常规节点升级一次性移除半数副本。
将终止宽限期(termination grace period)延长至默认的 30 秒以上,并添加 preStop 钩子。推理服务的单轮生成时间很容易超过 30 秒,否则每次滚动更新都会切断正在进行的流式请求,用户会将部署感知为随机故障。
有策略地卸载流量。当队列深度超过阈值时,快速返回“忙,请重试”而不是接受一个将挂起两分钟的请求。这对工程师来说感觉不对劲,但对用户是正确的。人们能原谅快速且坦诚的重试,但不会原谅冻结的界面。
准备兜底方案:第二个区域、覆盖常见用例的较小模型,或在灾难边缘使用的外部 API。鉴于 45.5% 的从业者运行在单一区域,这是清单中最常被忽略的项目,因此也是下次事故最可能的罪魁祸首。
职责归属
确定指标并记录在案:首字延迟(time to first token)、字间延迟(inter-token latency)、错误率、排队时间。将这些指标放在一个仪表盘上,并列展示集群和模型指标,确保两个团队打开的是同一个链接,而不是各自维护一套不同的现实版本。
明确划分职责。平台团队负责"能不能达到目标":容量、调度、路由、扩缩容、部署位置。应用团队负责"模型怎么配置和调用":上下文长度、batching 参数、prompt 大小、重试策略。指标一旦掉下来,双方的问题都会暴露出来,大家就能基于同一张图表展开讨论,而不是各说各话。
你的监控系统仍然没告诉你的四件事
延迟、流量、错误、饱和度这四大黄金指标依然必要,但已经不够了。还有四项应该被纳入监控面板。
Token 消耗速率,并区分 prompt 和 completion 两部分,因为这才是真正的容量单位。按租户和查询类型统计的单请求成本,因为"我们跑了八块 GPU"回答不了某个功能在单个用户身上花多少钱。输出质量,跟踪幻觉率和 guardrail 偏移,因为以牺牲质量为代价的延迟优化不算赢。模型归因,记录每次响应来自哪个 checkpoint、哪个 prompt 版本,因为质量一旦变化,你得知道是哪里改了。
最后这一点呼应了一个值得关注的趋势。Model registry 已经和容器镜像仓库平起平坐,prompt 和 agent 配置也越来越多地作为 GitOps 工件纳入版本管理,走和其他代码一样的交付流水线。
Prompt 现在就是代码。它影响生产行为,也可能把事情搞坏,所以应该像其他代码一样经过评审、版本管理和回滚。可很多团队还在网页控制台里直接改 prompt,然后纳闷为什么质量在某个周二突然变了。
仍未定论的部分
底层已经收敛了,收敛的速度就是证据。Kubernetes AI 一致性认证计划于 2025 年底启动时只有 18 个平台,短短四个月左右就增长到 31 个,并将范围扩展到 agentic 工作负载,覆盖了主流超大规模云厂商、企业级和私有云发行版、GPU neocloud 以及边缘网络。这些在几乎所有事情上都谈不拢的公司,在这件事上达成了一致。
而上面这一层完全没有收敛。AI 网关、评测平台、agent 控制平面、agent 可观测性——这些领域在商业产品中的推进速度远超任何标准化组织,而且它们恰好都是业务逻辑正在涌向的地方。
有必要直言不讳地指出这种风险。你可以在 Kubernetes 层面实现完全的可移植性,但在实际构建的层面却完全陷入锁定。你的 Pod 可以在云之间优雅迁移,而你的代理定义、评估套件和网关策略却会原封不动地留在原地,因为根本不存在标准的地方供它们迁移。
这并不意味着要避免使用这些工具。它们确实解决了当下的实际问题。这意味着你需要清楚哪些组件具备退出机制,哪些没有,并将此作为书面决策,而不是在续约谈判时才恍然大悟。
周一早晨
从一项测量开始。记录冷启动的端到端耗时,从 Pod 创建到输出首个 token,记下这个数字。该数值即为你的自动扩缩容下限,而最小副本数直接由此推导得出。
在主要仪表盘上将 P99 分位的“首个 token 响应时间”和“队列深度”可视化。用队列深度触发器和考虑了冷启动特性的最小值,取代基于 CPU 的自动扩缩容。对照真实的长流式响应而非测试提示词,检查你的 Ingress 读取超时和缓冲设置。修复终止宽限期,以阻止部署时强行切断连接。
随后,在接下来的几周内:给你的 GPU 节点添加污点,将权重迁移到节点本地或可用区本地存储,将最大上下文长度设置为实际使用量,并确认是否仍在轮询调度。大概率是的。
在季度结束前,决定谁对端到端延迟指标负责,并在其他团队在场的情况下大声说出来。
回到周四
仪表盘从未说谎。它只是回答了另一个问题。
它告诉你容器是否存活,而它们确实活着。但它无法告诉你请求正在因路由不良的批处理而排队,无法告诉你有四分钟没有新副本上线,无法告诉你有六块 GPU 闲置且无法使用,也无法告诉你 Ingress 正在缓冲本应流式输出的 token。标准工具包中的任何指标都不指向这些方面。
让模型在 Kubernetes 上跑起来,一个下午就够;但要让它在所有人同时登录时依然响应流畅,则是另一套功夫——这活儿其实更像传统的 infra 工程,而非当下 AI 圈的讨论所暗示的那般玄乎。调度、路由、容量、责任归属,这些痛点在贴 AI 标签之前我们就已经在解了。相应的技能早就在团队里,只差把指标对准正确的地方。
集群一片绿,才是真正开干的起点。关键看正在敲 prompt 的用户看到什么。
如果这些内容对你有用、或有新收获,欢迎在 LinkedIn 上给我提建议、抛想法,或私信聊聊。
如果我的文字帮到了你,还想继续支持我更新,不妨去 GitHub 点个 star,或在 LinkedIn 上帮我 endorse 相关技能。
下期见,祝探索愉快!