10.1 容器化与 Kubernetes 部署:把 vLLM 送上生产集群
vLLM 容器化(镜像、非 root、共享内存)、K8s GPU 调度(nvidia.com/gpu)、探针配置(/health 与 gRPC)、vLLM Production Stack Helm 部署
前面的章节把服务”调快”,10.1 把服务”送上线”。容器化 + Kubernetes 编排是 LLM 推理服务的标准交付形态。这一节解决三个核心问题:镜像怎么打、GPU 怎么调度、探针怎么配——其中探针是 LLM 服务最容易踩坑的地方(模型加载要几分钟,探针阈值没放宽,Pod 会被反复杀死)。
📑 目录
- 1. 容器化:镜像、非 root 与共享内存
- 2. K8s 部署清单:GPU 调度与模型缓存
- 3. 探针配置:LLM 服务的生死线
- 4. 模型加载慢的两个配套方案
- 5. vLLM Production Stack:官方全家桶
- 6. KServe 与替代方案
- 📝 总结
- 🎯 自我检验清单
- 📚 参考资料
1. 容器化:镜像、非 root 与共享内存
1.1 官方镜像
# 预构建镜像(含 OpenAI 兼容 API)
docker pull vllm/vllm-openai:latest
docker run --runtime nvidia --gpus all \
-v ~/.cache/huggingface:/root/.cache/huggingface \
-p 8000:8000 \
--ipc=host \
vllm/vllm-openai:latest \
--model mistralai/Mistral-7B-Instruct-v0.3
- 镜像分层:vllm/vllm-openai 是基于 CUDA 的推理镜像(跑
vllm serve);另有 vllm/vllm(无 API 的库镜像) --ipc=host或--shm-size:必须——Tensor Parallel(第 6 章)依赖共享内存做进程间通信,默认 64MB 的 /dev/shm 会直接 OOM
1.2 非 root 运行(安全基线)
CUDA 镜像默认以 root 运行(历史兼容),生产建议非 root:
docker run --runtime nvidia --gpus all \
--user 1000:1000 \
--ipc=host \
-p 8000:8000 \
vllm/vllm-openai:latest \
--model meta-llama/Llama-3.1-8B-Instruct
📌 关键点:容器化的三件必备:共享内存(—ipc=host)+ 模型缓存挂载(HF 缓存目录)+ 非 root 用户。缺第一个 TP 起不来,缺第二个每次冷启动重新下载权重,缺第三个过不了安全审计。
2. K8s 部署清单:GPU 调度与模型缓存
2.1 最小 Deployment(NVIDIA GPU)
apiVersion: apps/v1
kind: Deployment
metadata:
name: mistral-7b
spec:
replicas: 1
selector: { matchLabels: { app: mistral-7b } }
template:
metadata:
labels: { app: mistral-7b }
spec:
volumes:
- name: cache-volume # 模型权重缓存(避免每次冷下载)
persistentVolumeClaim: { claimName: mistral-7b }
- name: shm # 共享内存(TP 通信必需)
emptyDir:
medium: Memory
sizeLimit: "2Gi"
containers:
- name: mistral-7b
image: vllm/vllm-openai:latest
command: ["/bin/sh", "-c"]
args: [
"vllm serve mistralai/Mistral-7B-Instruct-v0.3 \
--trust-remote-code --enable-chunked-prefill \
--max-num-batched-tokens 1024"
]
ports: [{ containerPort: 8000 }]
resources:
limits:
cpu: "10"; memory: 20G
nvidia.com/gpu: "1" # ← GPU 调度入口
requests:
cpu: "2"; memory: 6G
nvidia.com/gpu: "1"
volumeMounts:
- mountPath: /root/.cache/huggingface
name: cache-volume
- mountPath: /dev/shm
name: shm
2.2 GPU 调度的三个要点
| 要点 | 说明 |
|---|---|
| 资源声明 | NVIDIA:nvidia.com/gpu(需安装 NVIDIA device plugin);AMD:amd.com/gpu |
| request = limit | GPU 必须 requests=limits(K8s 不调度超额 GPU) |
| 整卡语义 | nvidia.com/gpu: "1" 是一整张卡(无 MIG 时),TP 场景一个 Pod 要 N 张卡 |
2.3 模型权重的两种加载方式
方式 A:PVC 挂载 HF 缓存(Deployment 直挂)
首次冷启动下载 → 后续从 PVC 读
优点:简单;缺点:多副本各挂各的 PVC → 每副本都要一份权重
方式 B:initContainer 预下载(Production Stack 默认)
下载 Job 把权重拉进共享 PVC(ReadWriteMany)
优点:副本共享一份权重,秒级启动;缺点:多一个存储依赖
💡 提示:多副本场景一定要用共享存储 + 预下载(方式 B)——否则每个副本冷启动各下载一份权重,扩容 10 个副本 = 10 次下载,扩缩容的延迟收益全被吃掉。这也是 vLLM Production Stack 把”模型预下载”做成默认 initContainer 的原因。
3. 探针配置:LLM 服务的生死线
3.1 探针的作用
| 探针 | 判定 | LLM 服务的特殊性 |
|---|---|---|
| startupProbe | 容器是否”开始工作” | 模型加载要 1-10 分钟——必须用 startup 探针兜住加载期 |
| readinessProbe | 是否可接收流量 | /health 就绪前不要转发请求(否则请求排队爆掉) |
| livenessProbe | 是否存活(死则重启) | 阈值太紧 → 加载慢被误杀;LLM 推理长请求不能设太激进 |
3.2 vLLM 官方示例的探针
livenessProbe:
httpGet: { path: /health, port: 8000 }
initialDelaySeconds: 60 # 留出模型加载时间
periodSeconds: 10
readinessProbe:
httpGet: { path: /health, port: 8000 }
initialDelaySeconds: 60
periodSeconds: 5
3.3 最经典的故障:KeyboardInterrupt: terminated
K8s 官方文档专门列出此坑:startup/readiness 探针的 failureThreshold 太低,容器还没加载完模型就被 K8s 判死杀掉。特征:
1. Pod 反复 CrashLoopBackOff
2. kubectl get events 显示 "failed startup probe, will be restarted"
3. 容器日志尾部是 KeyboardInterrupt: terminated
对策:去掉探针先量一次”从启动到 /health 就绪的真实耗时”,再把 failureThreshold 调到该耗时 + 裕量(而不是拍脑袋定 3 次 × 10s)。
📌 关键点:探针的唯一正确配置方法是用数据说话:先裸跑(无探针)测 readiness 时间 →
initialDelaySeconds与failureThreshold × periodSeconds都要覆盖它。LLM 服务的加载时间随模型大小、存储速度波动(磁盘慢时 10 倍差距),阈值必须留 2-3 倍裕量。
3.4 gRPC 探针(--grpc 模式)
vLLM 支持 --grpc 启动(标准 gRPC Health Checking Protocol),K8s 1.24+ 可用原生 gRPC 探针:
livenessProbe:
grpc: { port: 50051 }
initialDelaySeconds: 120
periodSeconds: 10
readinessProbe:
grpc: { port: 50051 }
initialDelaySeconds: 120
periodSeconds: 5
gRPC 健康服务每次探针都检查引擎状态:引擎不健康或正在关停 → 返回 NOT_SERVING(比 HTTP /health 更精细)。
4. 模型加载慢的两个配套方案
探针只能”容忍”加载慢,真正解决加载慢的是这两个方案:
| 方案 | 原理 | 收益 |
|---|---|---|
| 共享 PVC 预下载 | initContainer 提前把权重拉进共享存储 | 副本启动从分钟级 → 秒级 |
--model 用本地路径 + 只读挂载 | 权重常驻共享存储,Pod 直接读 | 同上,且省 Pod 内存储 |
💡 提示:vLLM 冷启动 = 权重下载(网络)+ 权重加载(磁盘/内存)+ 图捕获(CUDA Graph 预热)。生产经验:图捕获(约 30s-2min)是加载完成后才发生的事,探针的
/health在引擎 ready 后才返回——所以探针阈值必须覆盖”下载+加载+图捕获”全链路。
5. vLLM Production Stack:官方全家桶
5.1 是什么
vLLM 官方(Berkeley-UChicago 合作)的生产部署套件:Helm chart 一键部署”多模型服务引擎 + 路由 + 缓存 + 观测”全家桶。原则:不修改上游 vLLM,只做编排层。
sudo helm repo add vllm https://vllm-project.github.io/production-stack
sudo helm install vllm vllm/vllm-stack -f values.yaml
# values.yaml(最小示例)
servingEngineSpec:
modelSpec:
- name: "opt125m"
repository: "vllm/vllm-openai"
tag: "latest"
modelURL: "facebook/opt-125m"
replicaCount: 1
requestCPU: 6
requestMemory: "16Gi"
requestGPU: 1
pvcStorage: "10Gi"
5.2 全家桶成员
| 组件 | 职责 |
|---|---|
| Serving Engine | 多模型多副本的 vLLM 引擎(modelSpec 列表) |
| Router(lmstack-router) | 前缀感知/模型感知路由(10.3 详解) |
| LMCache Server | KV Cache 跨实例/CPU offload(--kv-offloading-backend lmcache) |
| LoRA Controller | 多 LoRA 适配器管理(8.3 的服务化形态) |
| Grafana Dashboards | 内置 vLLM + LMCache 看板(一键启用) |
| KEDA Autoscaling | 基于指标的扩缩容(10.3 详解) |
5.3 内置能力清单
- 多模型混合部署(一个 chart 管 N 个模型)
- 模型预下载 initContainer(共享 PVC,秒级启动)
vllmApiKey原生支持(API Key 挂到引擎,解决”裸奔”问题)- Grafana 看板:TTFT、缓存命中率、LMCache 检索速度等(
grafanaDashboards.enabled: true)
📌 关键点:有 Production Stack 就不建议手搓 Deployment——它把探针、预下载、路由、观测、扩缩容全部做成标准件,且随上游演进。学习顺序建议:先手写一遍 Deployment 理解原理(本节),再上 Production Stack 省事(本节第 5 节)。自建基础设施时,至少把它的设计抄走(预下载、探针、路由三件套)。
6. KServe 与替代方案
| 方案 | 定位 | 特点 |
|---|---|---|
| KServe | 云原生模型服务标准(Kubeflow 生态) | Serverless 自动扩缩容、多框架(vLLM/Triton/TEI)、InferenceGraph 路由 |
| Triton + K8s | NVIDIA 推理栈 | 动态批处理、模型集成(ensemble) |
| Seldon Core | 通用 ML 部署 | 多框架、解释性/监控扩展 |
| 自建 Deployment | 最小化场景 | 本节 2-3 节的模式,可控但缺生态 |
选择逻辑:
单模型、团队小 → 自建 Deployment(本节模式)或 Production Stack
多模型、要 Serverless 扩缩容 → KServe
NVIDIA 全栈(Triton 推理 + 引擎调优)→ Triton + K8s
要"官方支持 + 全家桶" → vLLM Production Stack(推荐起点)
💡 提示:别在”部署框架选型”上花太多时间——它们的差异远小于”有没有可观测性、有没有预下载、探针对不对”。先把本节的基础件做对,框架随时可换。选型评估表就三行:模型管理、扩缩容方式、与监控栈的集成度。
📝 总结
- 容器化三件套:
--ipc=host(共享内存)、模型缓存挂载、非 root - GPU 调度:
nvidia.com/gpu(device plugin 前置)、request=limit、整卡语义 - 探针是 LLM 服务的生死线:加载期由 startup/长阈值兜住;
KeyboardInterrupt: terminated= 阈值太紧 - 探针配置用数据说话:先裸跑测 readiness 耗时,再定阈值(留 2-3 倍裕量)
- 加载慢的正解:共享 PVC + initContainer 预下载(多副本秒级启动)
- vLLM Production Stack:官方 Helm 全家桶——引擎+路由+LMCache+LoRA+Grafana+KEDA
- 选型:默认 Production Stack;KServe(Serverless 多模型)、Triton(NVIDIA 全栈)
🎯 自我检验清单
- 容器跑 vLLM 为什么必须
--ipc=host?(TP 通信依赖共享内存) - K8s 里 GPU 资源声明的两个注意点?
- 多副本为什么必须共享 PVC + 预下载?
- 探针三兄弟各自判定什么?LLM 场景分别怎么配?
-
KeyboardInterrupt: terminated的成因与排查步骤? - 怎么用数据确定探针阈值?
- Production Stack 全家桶有哪些组件?和手搓 Deployment 的差异?
- 部署框架选型的评估标准?
📚 参考资料
- vLLM 官方文档:Using Kubernetes(GPU 部署 YAML、探针与故障排查)(https://docs.vllm.ai/en/latest/deployment/k8s.html)
- vLLM 官方文档:Using Docker(https://docs.vllm.ai/en/latest/deployment/docker.html)
- vLLM Production Stack 文档(https://docs.vllm.ai/en/latest/deployment/integrations/production-stack.html)
- Production Stack Helm Chart 值参考(https://github.com/vllm-project/production-stack/tree/main/helm)
- KServe 官方文档(https://kserve.github.io/website/)