痛点:大模型上了 K8s,但推理服务总是翻车
越来越多团队把 LLM 推理服务搬上 Kubernetes——听起来很美,实际落地全是坑。
典型场景:团队用 FastAPI 包了个 Hugging Face Transformers 模型,丢进 K8s 跑。结果呢?单个请求吃满 24GB 显存,并发一上来就 OOM;模型加载要 3 分钟,Pod 重启一次用户等到崩溃;GPU 利用率忽高忽低,A100 一个月烧几万刀却跑不满。
核心问题:传统的模型推理方式没有针对 LLM 的长序列、大显存、高并发做优化。你需要一个专为 LLM 设计的推理引擎,而 vLLM 就是目前最成熟的选择。
方案:vLLM + Kubernetes GPU 调度
vLLM 是一个高性能 LLM 推理引擎,核心优势:
- PagedAttention:像操作系统管理内存一样管理 KV Cache,显存利用率提升 2-4 倍
- Continuous Batching:动态合并请求,吞吐量比静态 Batching 高 5-20 倍
- OpenAI 兼容 API:零改动替换 OpenAI 后端,前端代码无需修改
- Tensor Parallel:多卡并行推理,支撑 70B+ 大模型
架构很简洁:NVIDIA GPU Operator 管理 GPU 驱动 → vLLM Pod 挂载 GPU → Service/Ingress 暴露 API → Prometheus 采集推理指标 → HPA 自动扩缩。
实操步骤
Step 1:准备 GPU 节点和 NVIDIA Device Plugin
前提:K8s 集群有 GPU 节点(推荐 A10G/A100/H100),已安装 NVIDIA GPU Operator。
# 安装 NVIDIA GPU Operator(如果尚未安装)
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update
helm install gpu-operator nvidia/gpu-operator \
--namespace gpu-operator \
--create-namespace \
--set driver.enabled=true \
--set toolkit.enabled=true
# 验证 GPU 节点就绪
kubectl get nodes -l nvidia.com/gpu.present=true
kubectl describe node <gpu-node> | grep -A5 "Allocatable"
# 应能看到 nvidia.com/gpu: 1(或更多)
确认 GPU 资源可被调度后,给 GPU 节点打标签,方便后续调度:
kubectl label nodes <gpu-node> workload-type=llm-inference
Step 2:部署 vLLM 推理服务
创建 Deployment,以部署 Qwen2.5-7B-Instruct 为例:
# vllm-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-qwen25-7b
namespace: llm-serving
labels:
app: vllm-qwen25-7b
spec:
replicas: 1
selector:
matchLabels:
app: vllm-qwen25-7b
template:
metadata:
labels:
app: vllm-qwen25-7b
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
prometheus.io/path: "/metrics"
spec:
nodeSelector:
workload-type: llm-inference
containers:
- name: vllm
image: vllm/vllm-openai:latest
args:
- "--model"
- "Qwen/Qwen2.5-7B-Instruct"
- "--tensor-parallel-size"
- "1"
- "--max-model-len"
- "8192"
- "--gpu-memory-utilization"
- "0.90"
- "--enable-prefix-caching"
- "--port"
- "8000"
ports:
- containerPort: 8000
name: http
protocol: TCP
resources:
limits:
nvidia.com/gpu: "1" # 7B 模型单卡即可
requests:
cpu: "4"
memory: "16Gi"
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 120 # 模型加载需要时间
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 180
periodSeconds: 30
volumeMounts:
- name: model-cache
mountPath: /root/.cache/huggingface
- name: shm
mountPath: /dev/shm
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: model-cache-pvc # 预下载模型,避免每次拉取
- name: shm
emptyDir:
medium: Memory
sizeLimit: 8Gi # PyTorch 共享内存,必须给够
---
apiVersion: v1
kind: Service
metadata:
name: vllm-qwen25-7b
namespace: llm-serving
spec:
selector:
app: vllm-qwen25-7b
ports:
- port: 8000
targetPort: 8000
protocol: TCP
type: ClusterIP
部署并验证:
kubectl create namespace llm-serving
kubectl apply -f vllm-deployment.yaml
# 等待 Pod Running(首次需要下载模型,可能 5-10 分钟)
kubectl -n llm-serving get pods -w
# 测试推理(OpenAI 兼容接口)
kubectl -n llm-serving port-forward svc/vllm-qwen25-7b 8000:8000 &
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-7B-Instruct",
"messages": [{"role": "user", "content": "用一句话解释 Kubernetes"}],
"max_tokens": 100
}'
Step 3:配置基于推理指标的 HPA 自动扩缩
vLLM 暴露 Prometheus 指标,我们可以基于 等待中的请求数 做弹性扩缩:
# vllm-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: vllm-qwen25-7b
namespace: llm-serving
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-qwen25-7b
minReplicas: 1
maxReplicas: 4
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 1
periodSeconds: 120 # GPU Pod 启动慢,扩容别太激进
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 180
metrics:
- type: Pods
pods:
metric:
name: vllm_num_requests_waiting # vLLM 排队请求数
target:
type: AverageValue
averageValue: "5" # 排队超过 5 个就扩容
需要配合 Prometheus Adapter 将 vLLM 指标暴露为 K8s Custom Metrics:
# prometheus-adapter 规则片段
rules:
- seriesQuery: 'vllm_num_requests_waiting{namespace!="",pod!=""}'
resources:
overrides:
namespace: {resource: "namespace"}
pod: {resource: "pod"}
name:
matches: "^(.*)$"
as: "vllm_num_requests_waiting"
metricsQuery: 'avg(<<.Series>>{<<.LabelMatchers>>}) by (<<.GroupBy>>)'
Step 4:监控大盘与关键告警
vLLM 内置 /metrics 端点,核心指标:
| 指标 | 含义 | 告警阈值建议 |
|---|---|---|
vllm_num_requests_running |
正在处理的请求数 | — |
vllm_num_requests_waiting |
排队等待的请求数 | > 20 持续 2min 告警 |
vllm_gpu_cache_usage_perc |
KV Cache 显存使用率 | > 95% 告警 |
vllm_avg_generation_throughput_toks_per_s |
生成吞吐(tokens/s) | < 基线 50% 告警 |
vllm_e2e_request_latency_seconds |
端到端延迟 | P99 > 10s 告警 |
Prometheus 告警规则示例:
groups:
- name: vllm-alerts
rules:
- alert: VLLMHighQueueDepth
expr: vllm_num_requests_waiting > 20
for: 2m
labels:
severity: warning
annotations:
summary: "vLLM 请求排队过长 ({{ $value }} 个请求等待)"
- alert: VLLMKVCacheNearFull
expr: vllm_gpu_cache_usage_perc > 0.95
for: 1m
labels:
severity: critical
annotations:
summary: "vLLM KV Cache 即将耗尽 ({{ $value | humanizePercentage }})"
避坑指南
坑 1:Pod 启动 OOMKilled,但 GPU 显存还有余量
原因:vLLM 模型加载阶段需要大量 CPU 内存(模型权重先加载到 CPU 再转到 GPU)。7B 模型需要约 14GB CPU 内存,70B 模型可能需要 140GB+。
解法:resources.requests.memory 至少设为模型参数量 × 2(FP16),同时 /dev/shm 的 sizeLimit 要给够,PyTorch 的 DataLoader 和 NCCL 通信依赖共享内存。
坑 2:模型每次 Pod 重启都重新下载,启动要 10 分钟
原因:默认模型从 Hugging Face Hub 下载到容器内,Pod 销毁后缓存丢失。
解法:用 PVC 持久化模型缓存目录 /root/.cache/huggingface,或预先把模型上传到 S3/MinIO,用 initContainer 拉取。大规模集群推荐用 Dragonfly 做 P2P 分发:
# 预下载模型到 PVC(一次性 Job)
kubectl -n llm-serving run model-download --rm -it \
--image=vllm/vllm-openai:latest \
--overrides='{"spec":{"containers":[{"name":"dl","image":"vllm/vllm-openai:latest","command":["python3","-c","from huggingface_hub import snapshot_download; snapshot_download(\"Qwen/Qwen2.5-7B-Instruct\")"],"volumeMounts":[{"name":"cache","mountPath":"/root/.cache/huggingface"}]}],"volumes":[{"name":"cache","persistentVolumeClaim":{"claimName":"model-cache-pvc"}}]}}' \
-- /bin/true
坑 3:并发上来后延迟飙升,吞吐反而下降
原因:--max-model-len 设太大,导致 KV Cache 预分配过多显存,实际可容纳的并发请求数反而减少。
解法:根据实际业务场景设置合理的 --max-model-len。如果 90% 的请求 input+output 不超过 4096 tokens,就别设 32768。用 --gpu-memory-utilization 0.90 控制显存上限(留 10% 安全余量),并开启 --enable-prefix-caching 让相同 system prompt 的请求共享 KV Cache。
总结
| 环节 | 关键配置 | 一句话建议 |
|---|---|---|
| GPU 调度 | NVIDIA GPU Operator + nodeSelector | 确保 Device Plugin 就绪再部署 |
| 推理引擎 | vLLM + PagedAttention | 比原生 Transformers 吞吐高 5-20x |
| 弹性扩缩 | HPA + vllm_num_requests_waiting |
扩容慢缩容稳,别用 CPU 指标 |
| 模型缓存 | PVC 持久化 + 预下载 | 冷启动从 10min 降到 1min |
| 监控 | KV Cache 使用率 + 排队深度 | 这两个指标比 GPU 利用率更有用 |
vLLM 在 K8s 上部署的核心思路:用 PagedAttention 最大化单卡并发 → 用 Continuous Batching 榨干吞吐 → 用自定义指标 HPA 按需扩缩 → 用 PVC 消灭冷启动。GPU 算力贵,每一点利用率都是钱。