ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

GPU推理服务扩容不生效?从Pod状态到推理框架的完整排查指南

GPU推理服务扩容不生效?从Pod状态到推理框架的完整排查指南 你有没有遇到过这种情况线上推理服务告警QPS 已经顶到阈值HPA 也正常触发Deployment 副本数从 2 个一路涨到 6 个kubectl get pods看过去清一色 Running。结果呢QPS 还是那个 QPS延迟照样往上爬GPU 利用率纹丝不动。这个“GPU Pod 明明 Running但扩容后推理容量没变”的问题我前前后后排查过好几次也在不同团队的集群里见过完全相同的一幕。这篇文章就把这条排查路径完整拆开讲从 Pod 状态、GPU 设备、Service 流量、HPA 指标到推理框架自身的并发瓶颈一层层往下翻看完你基本就能照着定位自己的问题。这篇文章适合谁看部署过 GPU 推理服务vLLM、Triton、TorchServe 这一类用过或正在用 Kubernetes HPA 做弹性扩容并且被“扩容不生效”“加了副本没提性能”这类现象困扰的同学。如果你是刚上手的小白也别急着划走前半部分会把概念补齐后面才是实操。1. 先对齐概念Pod Running 和“推理容量就绪”不是一回事1.1 Running 只是进程活着不代表模型能接流量先说一个最根本的认知问题。在 Kubernetes 里Pod 状态变成 RunningAPI Server 告诉你的信息只有一条这个 Pod 已经被调度到某个节点容器镜像拉取成功容器进程被创建并且没有立刻退出。它完全没有回答另一个问题容器里的推理服务到底能不能处理请求。GPU 推理服务从进程启动到真正具备推理能力中间有一段不短的路要走。以 vLLM 为例启动后要加载模型权重权重文件动不动几个 GB 甚至几十 GB要从磁盘或对象存储拉下来再反序列化进显存接着要初始化 CUDA context分配 KV cache预热推理引擎最后才是监听端口、响应/health或/ready探针。这一步在模型大、显存慢、冷启动的情况下花上两三分钟非常正常。也就是说Pod 显示 Running 的那一瞬间很可能推理服务还在“加载中”根本没有接流量的能力。这里打个比方餐厅门口挂上了“营业中”的牌子后厨的菜还没从仓库搬进来锅都还没热这时候你进去点单人家是接待不了的。Kubernetes 的 Running 就是那块“营业中”的牌子真正的容量是“后厨已经能出菜”两件事之间隔着一个完整的就绪过程。1.2 推理容量到底是什么并发、时延和副本数之间的关系我们常说的“推理容量”其实是一个结果指标不是一个状态指标。它等于单位时间内服务能完成的请求数往细了拆就两个变量并发处理能力和单请求时延。用最简单的公式理解吞吐量 并发数 / 平均时延。并发数取决于推理框架能同时处理多少请求时延取决于单次前向计算要多久。GPU 推理和传统 Web 服务不一样的地方在于它是典型的批处理密集型负载。一个 GPU 里面有很多计算单元具体技术术语叫 SM / CUDA Core软件层面你会看到 thread、warp、CTA 这些概念一次前向推理如果只喂一个请求GPU 里绝大部分计算单元都是空闲的。推理框架做的 continuous batching、dynamic batching本质上就是把多个请求攒成一批喂给同一个 GPU让计算单元尽量满载。所以 GPU 推理容量的关键参数不是副本数而是“每个副本在当前显存和 batch 策略下能同时处理多少请求”。这就是为什么有时候你看到副本增加了容量却上不去——新副本可能根本没有参与流量分配或者参与进来的新副本因为显存、batch 限制只能处理极少的并发。1.3 为什么 HPA 扩容了推理容量却没变化HPA 做的是“扩副本”这个动作它不负责“把流量切过去”。从 HPA 修改 Deployment 的 replicas 开始到新 Pod 真正开始扛请求中间隔了好几步调度器选节点、容器启动、模型加载、探针通过、Pod 被写入 Service 的 Endpoints、负载均衡把请求转发过来。这几步任何一步卡住你看到的表象都是“扩容了但容量没涨”。更隐蔽的一种情况是HPA 扩容的触发条件本身就跟 GPU 推理容量的瓶颈不匹配。比如你用的指标是 CPU 利用率而 GPU 推理服务的特点是 CPU 占用很轻真正的压力全在 GPU 上。CPU 可能一直在 20% 以下HPA 自然不扩等到你手动或者基于 QPS 触发扩容后新副本能不能用 GPU 又是个未知数。这种“指标错位”导致的现象就是扩容动作发生了但扩出来的副本对容量没有正向贡献甚至因为 GPU 资源不足一直卡在初始化阶段。2. 排障第一步从 Pod 到 GPU 设备的链路体检2.1 先看 Pod 本身状态、事件和日志的三件套遇到“Pod Running 但容量没涨”我建议你先别急着看 HPA 配置先把 Pod 的状态看全。kubectl get pods只看一列 READY 是不够的要仔细看kubectl get pods -o wide -l appyour-inference-service kubectl describe pod pod-name kubectl logs -f pod-name --tail200describe里的 Events 会告诉你调度阶段发生了什么。最常见的几种情况Pending 是因为 GPU 资源不足、节点没有可用 GPU 或者配额被限制ContainerCreating 可能是镜像拉取慢、GPU runtime 配置不对Running 但 READY 显示 0/1那就是探针没通过这个最坑人——它表面上是 Running实际流量根本没进来。日志信息同样关键vLLM 加载模型卡住、CUDA 初始化失败、显存不足日志里都会留痕。我见过太多人只贴一句“Pod 是 Running 的”就下结论说服务正常一眼没看 READY 列结果问题就藏在那里。以后排查这类问题请把 READY 这一列当作第一优先级1/1才是真的活0/1只是“没死透”。2.2 GPU 设备到底通不通驱动、runtime 和设备映射Pod 起来了GPU 不一定可用。GPU 要真正被容器里的推理框架使用链路比 CPU 长得多。宿主机要装好 NVIDIA 驱动容器运行时要装 nvidia-container-toolkitKubernetes 才能通过 device plugin 把/dev/nvidia0这类设备挂载进容器。这条链路任何一环断了推理框架都会报 CUDA 错误或者干脆降级到 CPU 上跑。进到容器里先执行三条命令nvidia-smi ls -l /dev/nvidia* python -c import torch; print(torch.cuda.is_available(), torch.cuda.device_count())nvidia-smi 能显示 GPU 型号、显存占用、驱动版本说明宿主机驱动和 runtime 基本是通的。如果设备文件不存在或者权限不足推理进程会报/dev/nvidia0: No such file or directory。torch.cuda.is_available()返回 False 则说明 PyTorch 层面没拿到 GPU要么是驱动问题要么是 CUDA 版本不匹配要么是容器没映射设备。这里提醒一个很容易踩的多显卡问题。机器上如果既有核显又有独立显卡比如 Intel UHD Graphics 加 NVIDIA RTX 4060 Laptop GPU 这种组合推理框架默认选显卡时不一定会选到 NVIDIA 那张。你需要在容器环境里显式指定CUDA_VISIBLE_DEVICES0或者对应的 GPU 序号不然 CUDA 可能找不到设备或者跑在核显上性能惨不忍睹。这也是为什么我排查时一定会问一句“你进容器跑过 nvidia-smi 吗跑出来的结果对不对”2.3 显存是真的被占了还是被“僵尸”占着GPU 显存和 CPU 内存不一样进程退出后显存不一定立刻释放干净。尤其在 K8s 环境里上一个任务被 kill 之后留下僵尸进程或者 CUDA context 没回收新 Pod 调度到同一个节点上就会碰到一个诡异的现象nvidia-smi显示显存已经被用掉一大半但你找不到是哪个进程在占。排查方法也简单在宿主机上看nvidia-smi ps aux | grep -i python如果显存占用高、进程列表里却无主基本就是僵尸占用。这种时候新 Pod 虽然起来了但初始化模型时申请显存失败反复报CUDA out of memory一直无法进入 ready 状态。有一些平台会配置 GPU 配额预冻结机制申请 GPU 时先把配额冻结住如果任务迟迟不启动配额冻结时间到了会自动释放。这就是热词里提到的“预冻结时间 5.00 min”的真实含义。如果上层平台不清理僵尸任务冻结的配额一多后续扩容申请 GPU 就直接被拒Pods 会卡在 Pending或者起来了也拿不到足够显存。遇到这种情况最直接的做法是把节点上的残留进程清掉或者把调度策略改成显存分离模式让新任务只调度到显存充足的节点。K8s 原生对 GPU 只有“数量”没有“显存分片”的概念所以在扩容前检查目标节点的剩余显存是在 GPU 集群里需要反复强调的日常操作。3. 流量这一侧新副本为什么没接到推理请求3.1 Ready 状态与 Service Endpoints 的关系这里必须讲清楚 Kubernetes 的一个关键机制Service 的负载均衡只把流量转发给 Endpoints 列表里的 Pod而 Endpoints 列表只会收录通过 readiness 探针的 Pod。换句话讲Pod 是 Running 也好进程在跑也好只要 readiness 探针没通过它就被 Service 排除在流量路由之外。怎么确认直接看kubectl get endpoints service-name输出里 READY 这一列会明确告诉你当前到底有几个 Pod 真正在接收流量。如果你看到 Pod 是 6 个但 Endpoints 里 READY 只有 2 个那问题就很清晰了——其他 4 个新副本根本没有被路由到扩容对容量当然没有影响。这个机制我强调再多都不为过。它是这类问题里最高频的根因而且表面极具迷惑性人去kubectl get pods看到全是 Running第一反应就是“Pod 没问题”如果不知道看 Endpoints就会去折腾 HPA、折腾网络、折腾一堆不相关的东西白白浪费时间。3.2 探针配置和模型加载时间赛跑那 readiness 探针为什么会失败最经典的场景就是探针的超时和模型加载时间赛跑而且必输。默认情况下很多人会写一个简单的 ready 探针readinessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 3这个配置的潜台词是容器启动 10 秒后就开始探测每 5 秒探一次连续失败 3 次就判定为不健康。可你的模型加载可能要 60 秒甚至 120 秒探针在服务还没监听端口的时候就已经连续失败 3 次了。Kubernetes 会怎么处理不是把 Pod 杀掉而是把 Pod 从 Endpoints 里摘掉同时继续探。虽然 Pod 最终加载完模型后探针会通过但在通过之前这段黄金扩容时间它完全没有接到流量。解决思路是给模型加载留出足够的安全边际startupProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 60 readinessProbe: httpGet: path: /health port: 8000 periodSeconds: 5 failureThreshold: 3startupProbe 负责在启动阶段慢慢等最多能等 300 秒5 秒一次 × 60 次等应用完全起来之后readinessProbe 才开始正常接手。这是我在 GPU 推理部署里最常用的配置之一把探针问题解决掉扩容生效的概率直接提升一大截。3.3 Service 选择器和路由注册新副本不是自动就能接流量还有一个很低级但发生率极高的坑Service 的 selector 和新扩容 Pod 的 label 不匹配。你可能把 Deployment 的 replicas 通过 HPA 扩上去了但新 Pod 的 label 是app: inferenceService 的 selector 写的是app: infer或者因为发布历史原因某个老 Service 只关联了一个固定 Endpoints。这种情况下新 Pod 无论怎么 ready流量也不会过来。排查命令kubectl get svc service-name -o yaml | grep selector kubectl get pods --show-labels把 Service 的 selector 和 Pod 的 labels 对一下确保 key-value 完全匹配。这个问题在手工创建 Service 和 Deployment 时特别容易犯一旦发生扩容自然完全无效。还有一类情况和应用层路由有关。如果你用的是支持多实例推理的框架比如 vLLM 的多节点张量并行新副本起来后需要向路由注册中心注册自己的地址推理网关才会往它身上分流量。如果注册流程依赖环境变量、启动参数或者外部数据库新副本虽然 CPU、GPU、模型都正常但没有注册进去同样接不到请求。这种时候要去看推理网关的路由表或注册中心的实例列表。4. 容量这一侧HPA 指标和推理框架的并发瓶颈4.1 HPA 用的指标对不对CPU 还是 GPU 还是 QPSHPA 扩容不生效很多时候问题不在扩容动作本身而在触发扩容的指标选错了。GPU 推理服务和普通 Web 服务的资源画像完全不同。普通 Web 服务吃 CPUCPU 利用率是合理的扩容指标GPU 推理服务主要压力在 GPU 上CPU 常常闲得很。如果你 HPA 配的是 CPU 利用率可能在 QPS 已经到顶、GPU 显存快满的时候CPU 利用率才 30%HPA 根本不会触发。手动扩了副本后新副本同样 CPU 空闲但 GPU 显存可能不足卡在初始化。正确的做法是尽量用与推理容量直接相关的指标如果平台支持 GPU 指标DCGM 这类用 GPU 利用率或显存利用率如果不支持 GPU 指标用业务指标比如每秒请求数QPS、队列中的请求数实在不行用并发请求数和 GPU 显存余量的组合。下面是一个基于 QPS 扩展的 HPA 示例指标来自 Prometheus 适配器apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: inference-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: inference-deployment minReplicas: 2 maxReplicas: 10 metrics: - type: Pods pods: metric: name: inference_qps target: type: AverageValue averageValue: 200这个配置的含义是当每个 Pod 的平均 QPS 超过 200 时扩容。扩容出来后如果新 Pod 没有流量它自己的 QPS 是 0平均 QPS 又会被拉低HPA 甚至可能认为压力缓解而开始缩容。所以指标选对只是第一步确保新 Pod 真的有流量进来才是闭环。4.2 配额冻结、资源上限与冷启动的连锁反应在云原生的 GPU 集群里GPU 资源不像 CPU 那样按核数随便分配它通常以“卡”为单位由调度器或上层平台统一管控。很多平台在调度 GPU 任务时会有配额预冻结机制你申请一张 GPU平台先从你的配额里冻结掉一份资源任务成功启动后转成正式占用如果任务卡住、启动超时冻结的资源到期会释放。这个机制本身没什么问题但在扩容场景下会形成一个连锁反应HPA 把副本数从 2 扩到 6一次性要申请 4 张 GPU。如果集群里只有 3 张可用或者你的配额已经预冻结了 2 张迟迟没释放那新 Pod 就会一直 Pending或者调度到没有 GPU 的节点上然后初始化失败。表面上是“扩了”实际上新的推理副本一个都起不来。排查这种问题看两处一是kubectl describe pod里的调度事件有没有Insufficient nvidia.com/gpu二是节点的可分配资源kubectl describe node看Allocated resources里的 GPU 数量。如果在平台界面上看配额中心的“已冻结”和“已占用”分别多少。很多时候你以为的“GPU 不够”其实是“上一批没启动干净的冻结配额还没释放”。4.3 推理框架自己的并发限制掩盖了扩容效果就算上面所有环节都正常新 Pod ready、流量进来了、GPU 也分配到了还有一个更隐蔽的层面推理框架自身的并发模型可能限死了单副本的吞吐导致多副本也没有体现出叠加效果。拿 vLLM 来说关键参数是max_num_seqs它决定了一个实例最多同时处理多少个序列。如果你的请求本身很长、很占显存或者每个请求要求的并发很低那么即使有 6 个副本每个副本实际能跑的并发可能远小于预期。更麻烦的是如果请求调度策略把所有流量都倾向性地打到老副本上比如负载均衡模式选择不当、连接复用、session affinity新副本只分到一小部分流量容量自然上不去。这种情况下GPU 利用率会呈现出“老副本跑满、新副本空闲”的极端不均衡。再看 GPU 计算层面的细节GPU 计算的基本调度单位是 warp一个 warp 是一组一起执行的线程CTACooperative Thread Array是更上层的线程块概念可以理解成任务在 GPU 上的一个基本组织单位。推理框架的 batch size 最终决定了 GPU 上有多少 warp 和 CTA 在真正干活。如果 batch 太小GPU 大部分执行单元处于空转你在 nvidia-smi 里会看到利用率很高但有效吞吐很低——因为很多线程在等数据。反过来如果 batch 太大显存和延迟又会爆。所以多副本扩容要考虑的不仅是“加机器”还有“每个副本的 batch 策略是否跟着流量压力在动态调整”。用更通俗的话说你开了 6 家店每家店本来每小时只能做 100 杯咖啡扩容成 6 家后门面上是 600 杯的产能但如果你前台接单的速度只有 200 杯或者原料采购只支持 200 杯的量那后面 4 家店就是空转。扩容的真正收益只有当流量能够被均匀、有效地分配到新副本且新副本的显存、batch、并发参数匹配时才会体现出来。5. 一份完整的排查实录从“6/6 Running”到 QPS 翻倍5.1 现场现象与第一轮误判把上面这些机制串起来分享一个我实际处理过的案例。线上 vLLM 推理服务高峰时 QPS 阈值告警HPA 从 2 个副本一路扩到 6 个。我看一眼 Pod 列表清一色 Running第一反应是负载均衡或者网关出问题了。检查了 Ingress、Service、网关配置全部正常。然后我拉出 Endpoints 看了一眼发现只有 2 个 READY其余 4 个都是 0/1。这时候第一轮误判被纠正——问题根本不在网关而在新 Pod 一直没能进入流量路由。这个观察非常关键它把所有排查方向都收拢到“新副本为什么没有 ready”这一个问题上。5.2 定位根因readiness 探针失败 GPU 显存碎片继续看新 Pod 的describe输出Events 里写满了Readiness probe failed: HTTP probe failed with statuscode: 503。进容器看日志前几行是模型权重加载中间开始报CUDA error: out of memory。也就是说新副本被调度到的节点上显存已经被其他任务的残留进程占了大半模型加载到一半就申请不到显存一直无法完成初始化。但 Kubernetes 只看探针失败并不管你是加载慢还是显存不够于是新 Pod 就在“Running 探针失败”的状态里卡住不动。检查节点资源时发现那台 GPU 节点上有一个残留的 Python 进程占着接近 20GB 显存但已经不在任何 K8s Pod 里了。这是典型的任务被强杀后 CUDA context 未释放的场景。再加上探针的initialDelaySeconds配置太短模型加载还没完成探针就连续失败进一步加剧了问题。5.3 修复动作与前后对比修复分三步。第一步清掉节点上的残留进程把显存释放干净。第二步调整探针配置增加 startupProbe给模型加载留足 300 秒。第三步给 Deployment 加上显存相关的调度约束避免新任务再被调度到显存不足的节点。修复之后再观察新 Pod 从启动到 1/1 READY 大约需要 90 秒模型加载时间Endpoints 里的 READY 数量从 2 变成 6随后 QPS 从瓶颈的 500 左右涨到 1200 以上P95 延迟也明显回落。整个过程没有改一行推理代码就是让“扩容出来的副本”真正变成“能接流量的副本”。这个案例最大的教训就是不要相信 Running只相信 READY 和 Endpoints。这是排查一切“Pod 在跑但服务不好使”类问题的第一信条。6. 实战避坑速查表遇到同类问题照着做就行下面这张表是我在排查 GPU 推理扩容问题时固定会走一遍的检查清单。建议收藏遇到同类问题按顺序过一遍大多数情况十分钟内能定位。检查项排查命令期望结果失败时可能原因Pod 真实就绪状态kubectl get pods所有副本 READY 为 1/1探针失败、模型未加载完Pod 调度事件kubectl describe pod name无 Pending、无 FailedSchedulingGPU 资源不足、配额冻结GPU 设备可用性kubectl exec -it pod -- nvidia-smi能看到 GPU 和显存驱动/runtime/设备映射问题CUDA/PyTorch 识别容器内torch.cuda.is_available()True版本不匹配、设备选择错误残留显存占用宿主机nvidia-smips aux显存有主、进程可识别僵尸进程占用显存Service 路由端点kubectl get endpoints svcREADY 数等于可用副本数selector 不匹配、探针失败Service selectorkubectl get svc svc -o yaml与 Pod labels 完全匹配标签不一致HPA 指标来源kubectl get hpa name -o yaml指标能反映 GPU/QPS 压力指标错位、采集延迟推理框架并发参数检查 vLLM/Triton 的 max_num_seqs 等与流量压力匹配batch 上限太低上层平台配额冻结配额中心或调度事件冻结/占用比例合理冻结配额未释放几个补充提醒探针配置里startupProbe和readinessProbe分开写不要图省事只写一个模型加载越大的模型越要留足 startup 时间。多显卡节点上推理容器最好显式设置CUDA_VISIBLE_DEVICES避免选到非目标 GPU。HPA 缩容策略默认是扩容后等 5 分钟才缩因为刚 ready 的副本还在承接流量马上缩掉会引起抖动这个默认值尽量不要乱改。如果一次扩了多个副本但只有一个成功 ready优先怀疑显存容量和调度策略而不是网络问题。路由层面如果用了 session affinity 或者保持连接新副本可能要等新连接进来才会被分到流量短时间观察可能显得“没生效”等一两分钟再看才是真实状态。我个人在实际操作中的体会是这类问题 80% 以上都出在“探针没过”和“显存不够”这两件事上而这两件事又经常同时发生。先看 READY 和 Endpoints再看 GPU 显存基本上不会跑偏。最后再分享一个小技巧把kubectl get pods -o wide和kubectl get endpoints这两条命令做成一个脚本每次排查时一秒出结果事实摆在面前误判率会低很多。希望这篇梳理能帮你少走几趟弯路。
返回列表