一、环境和问题
这次记录一个 GPU 推理服务扩容时遇到的启动问题。
环境大致如下:
| 项目 | 内容 |
|---|---|
| 编排 | Kubernetes |
| 节点运行时 | containerd |
| 推理服务 | vLLM OpenAI API |
| GPU 节点 | 新增节点池 |
| 现象 | Pod 已调度,但容器一直没有 Running |
先看 Pod 状态:
bash
kubectl get pod -n inference -o wide
text
infer-api-7fdbb8c9d9-q2p6x 0/1 ContainerCreating gpu-node-08
继续看事件:
bash
kubectl describe pod infer-api-7fdbb8c9d9-q2p6x -n inference
text
Pulling image "vllm/vllm-openai:latest"
Failed to pull image: context deadline exceeded
Warning Failed kubelet Error: ErrImagePull
Warning Failed kubelet Error: ImagePullBackOff
这里不要直接排查模型文件和推理参数。Pod 还没进入运行阶段,当前问题在镜像拉取。
二、先把启动链路拆开
GPU 推理服务启动可以拆成四段:
| 阶段 | 检查点 | 常见命令 |
|---|---|---|
| 调度 | Pod 是否落到 GPU 节点 | kubectl get pod -o wide |
| 镜像 | 镜像是否能拉取 | kubectl describe pod、crictl pull |
| 运行时 | NVIDIA runtime、device plugin、RuntimeClass | kubectl describe node |
| 推理 | 模型加载、接口健康检查、batch 参数 | kubectl logs |
如果事件里已经出现 ImagePullBackOff,优先处理第二段。
三、整理镜像来源
推理服务通常不只有业务镜像,还会包含 CUDA、监控、K8s 基础组件和 initContainer。可以先把 Deployment / Helm values 里的镜像列出来:
bash
kubectl get deploy infer-api -n inference -o jsonpath="{..image}"
按来源分组后,做一次预检:
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)在这里解决的是多源镜像入口和拉取稳定性问题。它不参与 GPU 调度,也不改变 vLLM 的推理行为。
这一步通过后,至少说明 Docker Hub、NVIDIA、Quay、K8s 这些来源的基础镜像链路可以先过。
四、containerd 节点上用 crictl 验证
Kubernetes 节点实际由 containerd 拉镜像,所以建议在目标节点上再执行:
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
查看本地缓存:
bash
crictl images | grep -E "vllm|cuda|prometheus|pause"
如果这里失败,Deployment 扩容后仍然大概率进入 ImagePullBackOff。
如果这里通过,再继续看:
bash
kubectl describe node gpu-node-08
kubectl get events -n inference --sort-by=.lastTimestamp
kubectl logs -n inference deploy/infer-api
五、预热前后对比
这次调整后,我把镜像预热放到了扩容前:
| 项目 | 预热前 | 预热后 |
|---|---|---|
| 新节点首次扩容 | Pod 卡在 ContainerCreating |
镜像阶段提前验证 |
| 排查方向 | 模型、GPU、网络混在一起看 | 先排除镜像拉取 |
| 回滚演练 | 新节点仍可能首次拉镜像 | 关键镜像已有缓存 |
| 值班处理 | 看到 ImagePullBackOff 才介入 |
发布前暴露镜像来源问题 |
这个对比不是为了证明推理更快,而是为了让问题边界更明确。
六、DaemonSet 预热示例
GPU 节点多的时候,可以用一个临时 DaemonSet 触发预热。示例只演示思路,生产环境建议固定 tag 或 digest。
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"]
- name: pull-metrics
image: quay.1ms.run/prometheus/prometheus:latest
command: ["sh", "-c", "prometheus --version || 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 get pod -n ops -o wide
预热完成后,再进行推理服务扩容。
七、排查顺序
遇到 GPU 推理服务启动失败,可以按下面顺序排查:
kubectl get pod -o wide确认 Pod 是否已经调度到 GPU 节点。kubectl describe pod查看是否是ErrImagePull或ImagePullBackOff。- 按来源整理镜像,先做
docker pull预检。 - 在目标节点上用
crictl pull复现真实拉取路径。 - 镜像阶段通过后,再看 RuntimeClass、GPU device plugin、模型目录和应用日志。
- 生产发布前固定 tag 或 digest,不要长期依赖
latest。
八、总结
GPU 推理服务冷启动慢,不一定是模型慢,也不一定是调度慢。
如果 Pod 已经调度到 GPU 节点,但事件里反复出现 Pulling image、context deadline exceeded、ImagePullBackOff,先把镜像阶段拆出来处理。
镜像预热解决的是启动链路最前面的不确定性。先让 Docker Hub、NVIDIA、Quay、K8s 这些来源的镜像稳定通过,再排查 GPU 运行时和推理参数,问题会清楚很多。