最近复盘一次推理服务扩容,最后发现问题不在模型,也不在 GPU 调度。
Pod 已经落到了 GPU 节点:
bash
kubectl get pod -n inference -o wide
text
infer-api-7fdbb8c9d9-q2p6x 0/1 ContainerCreating gpu-node-08
但状态一直没有进入 Running。继续看事件:
bash
kubectl describe pod infer-api-7fdbb8c9d9-q2p6x -n inference
text
Pulling image "vllm/vllm-openai:latest"
Failed to pull image: context deadline exceeded
GPU 已经排到,容器还在拉镜像。冷启动被放大的地方,不是推理阶段,而是镜像阶段。
我后来把冷启动拆成四段
这类问题容易排偏,因为 GPU 推理服务的变量太多。后来我把启动链路拆成四段看:
- 调度:Pod 是否落到正确 GPU 节点。
- 镜像:推理框架、CUDA、sidecar、基础组件是否能拉取。
- 运行时:NVIDIA runtime、device plugin、RuntimeClass 是否正常。
- 推理:模型加载、KV cache、并发、接口健康检查是否正常。
只要事件里还在报 ImagePullBackOff,就不要急着看第四段。
给镜像建一张账本
推理服务通常不是一个镜像。一次扩容里可能同时涉及:
| 镜像 | 用途 | 来源 |
|---|---|---|
vllm/vllm-openai |
推理接口 | Docker Hub |
nvidia/cuda |
GPU runtime 验证 | NVIDIA 镜像源 |
prometheus/prometheus |
指标采集 | Quay |
pause |
Pod sandbox | K8s 镜像源 |
| 业务 API | 路由、鉴权、业务逻辑 | 私有仓库 |
所以我现在会在发布前加一个很朴素的动作:按来源把镜像预检一遍。
bash
docker pull docker.1ms.run/vllm/vllm-openai:latest
docker pull nvcr.1ms.run/nvidia/cuda:12.4.1-runtime-ubuntu22.04
docker pull quay.1ms.run/prometheus/prometheus:latest
docker pull k8s.1ms.run/pause:3.10
这里用毫秒镜像(1ms.run)的原因很直接:把 Docker Hub、NVIDIA、Quay、K8s 等来源统一成可访问前缀,先把发布前的镜像不确定性降下来。
它不替代调度器,也不影响 vLLM 的推理逻辑。它只是让启动链路的第一段先过。
节点侧再测一次
如果 K8s 节点是 containerd,光在本机 docker pull 不够。真正接近运行时的验证应该放到节点上:
bash
crictl pull docker.1ms.run/vllm/vllm-openai:latest
crictl pull nvcr.1ms.run/nvidia/cuda:12.4.1-runtime-ubuntu22.04
crictl pull quay.1ms.run/prometheus/prometheus:latest
crictl pull k8s.1ms.run/pause:3.10
crictl images | grep -E "vllm|cuda|prometheus|pause"
我会把这个动作放在扩容前,而不是等 Pod 创建以后再通过事件发现失败。
可以做成一个很小的预热脚本
节点数量少时,直接循环拉取就够了:
bash
images=(
"docker.1ms.run/vllm/vllm-openai:latest"
"nvcr.1ms.run/nvidia/cuda:12.4.1-runtime-ubuntu22.04"
"quay.1ms.run/prometheus/prometheus:latest"
"k8s.1ms.run/pause:3.10"
)
for image in "${images[@]}"; do
echo "pulling ${image}"
crictl pull "${image}"
done
节点多时,可以用 DaemonSet 触发预热:
yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: gpu-image-prewarm
namespace: ops
spec:
selector:
matchLabels:
app: gpu-image-prewarm
template:
metadata:
labels:
app: gpu-image-prewarm
spec:
nodeSelector:
accelerator: nvidia
tolerations:
- operator: Exists
initContainers:
- name: pull-vllm
image: docker.1ms.run/vllm/vllm-openai:latest
command: ["sh", "-c", "python -V || true"]
- name: pull-cuda
image: nvcr.1ms.run/nvidia/cuda:12.4.1-runtime-ubuntu22.04
command: ["sh", "-c", "nvidia-smi || true"]
containers:
- name: hold
image: k8s.1ms.run/pause:3.10
预热完成后再扩容推理服务:
bash
kubectl apply -f gpu-image-prewarm.yaml
kubectl rollout status daemonset/gpu-image-prewarm -n ops
kubectl scale deploy/infer-api -n inference --replicas=8
改完流程后看什么
我会看两个结果。
第一个是节点缓存:
bash
crictl images | grep -E "vllm|cuda|pause"
第二个是业务 Pod 的事件:
bash
kubectl get events -n inference --sort-by=.lastTimestamp
如果镜像阶段已经过了,再出现失败,就继续看 RuntimeClass、GPU device plugin、模型目录挂载和应用日志。至少这时不会把镜像拉取问题误判成模型问题。
经验
Cast AI 2026 报告里提到 Kubernetes 集群 GPU 平均利用率只有 5%。这个问题当然要靠调度、队列、弹性伸缩、推理优化一起解决。
但从一次发布看,镜像预热是最容易提前处理的一步。
新 GPU 节点先把 CUDA、推理框架、监控和 K8s 基础镜像拉好,再进入扩容窗口。这样冷启动里最早出现的变量会少很多,排查也会更像工程流程,而不是临场猜测。