技术博客

vLLM on Kubernetes 生产部署与 KEDA 弹性伸缩实战

完整讲解 vLLM 0.21 在 Kubernetes 上的生产级部署方案,包括多 GPU Tensor Parallel 配置、Readiness/Liveness 探针设置、KEDA 基于 Prometheus 队列深度自动弹性伸缩,以及 FP8 精度与 Prefix Caching 生产调优。

vLLMKubernetesKEDAGPULLM推理弹性伸缩

vLLM 已成为 2026 年最主流的开源 LLM 推理引擎,0.21 版本新增 KV Offload 与 Hybrid Memory Allocator、Speculative Decoding 对 Reasoning 模型的支持,以及默认开启 Prefix Caching。生产环境部署 vLLM 的难点不在于单机启动,而在于如何在 Kubernetes 上做好健康探针、弹性伸缩与资源隔离。本文以 H100 节点为例,提供完整的生产部署配方。

vLLM 0.21 核心特性速览

特性 说明
KV Cache Offload 将 KV Cache 卸载到 CPU 内存,支持超长上下文
Prefix Caching(默认开启) 多轮对话显存复用,降低 TTFT 最高 60%
Speculative Decoding 对 Reasoning 模型支持思考预算约束
TOKENSPEED_MLA 后端 Blackwell GPU 专用加速后端
FP8 默认路径 Hopper/Blackwell 原生 FP8,几乎无质量损失

系统依赖

基础 Deployment 配置

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-qwen3-32b
  namespace: llm-serving
  labels:
    app: vllm
    model: qwen3-32b
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vllm
      model: qwen3-32b
  template:
    metadata:
      labels:
        app: vllm
        model: qwen3-32b
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/metrics"
    spec:
      # GPU 节点专属污点容忍
      tolerations:
      - key: dedicated
        operator: Equal
        value: gpu
        effect: NoSchedule
      nodeSelector:
        node-role: gpu-inference

      # 终止等待时间(等当前请求处理完毕)
      terminationGracePeriodSeconds: 120

      containers:
      - name: vllm
        image: vllm/vllm-openai:v0.21.0
        ports:
        - containerPort: 8080
          name: http
        resources:
          # 请求值等于限制值,防止 GPU 超售
          requests:
            nvidia.com/gpu: "4"
            cpu: "16"
            memory: "64Gi"
          limits:
            nvidia.com/gpu: "4"
            cpu: "16"
            memory: "64Gi"

        args:
        - --model=/models/qwen3-32b-instruct
        - --host=0.0.0.0
        - --port=8080
        - --tensor-parallel-size=4
        - --dtype=fp8                      # H100 原生 FP8
        - --max-model-len=32768
        - --gpu-memory-utilization=0.90
        - --enable-prefix-caching          # 默认已开启,显式声明更清晰
        - --max-num-seqs=256
        - --served-model-name=qwen3-32b

        env:
        - name: VLLM_WORKER_MULTIPROC_METHOD
          value: spawn
        - name: CUDA_VISIBLE_DEVICES
          value: "0,1,2,3"
        # 开启 KV Offload(CPU 内存 >= 128GB 时推荐)
        - name: VLLM_CPU_KVCACHE_SPACE
          value: "40"   # 40GB CPU 内存用于 KV Offload

        # 健康探针:启动探针(模型加载可能需要数分钟)
        startupProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 60
          periodSeconds: 15
          failureThreshold: 40    # 最多等待 60 + 40*15 = 660 秒
          successThreshold: 1

        # 就绪探针:确认服务可接受请求
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 10
          failureThreshold: 3

        # 存活探针:检测服务是否挂死
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 120
          periodSeconds: 30
          failureThreshold: 5

        volumeMounts:
        - name: models
          mountPath: /models
        - name: dshm
          mountPath: /dev/shm

      volumes:
      - name: models
        nfs:
          server: 192.168.1.100
          path: /data/models
      # Tensor Parallel 需要大共享内存
      - name: dshm
        emptyDir:
          medium: Memory
          sizeLimit: 32Gi

Service 与 Ingress

apiVersion: v1
kind: Service
metadata:
  name: vllm-qwen3-32b
  namespace: llm-serving
spec:
  selector:
    app: vllm
    model: qwen3-32b
  ports:
  - port: 80
    targetPort: 8080
    name: http
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: vllm-ingress
  namespace: llm-serving
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
  ingressClassName: nginx
  rules:
  - host: llm.internal.company.com
    http:
      paths:
      - path: /v1
        pathType: Prefix
        backend:
          service:
            name: vllm-qwen3-32b
            port:
              number: 80

Prometheus 指标采集

vLLM 内置 /metrics 端点暴露 Prometheus 指标,核心指标包括:

指标 含义
vllm:num_requests_waiting 等待中的请求数(队列深度)
vllm:num_requests_running 正在处理的请求数
vllm:gpu_cache_usage_perc KV Cache GPU 使用率
vllm:cpu_cache_usage_perc KV Cache CPU Offload 使用率
vllm:avg_generation_throughput_toks_per_s 平均生成吞吐量(tok/s)
vllm:time_to_first_token_seconds 首 Token 延迟分布
# Prometheus ServiceMonitor
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: vllm-metrics
  namespace: monitoring
spec:
  namespaceSelector:
    matchNames:
    - llm-serving
  selector:
    matchLabels:
      app: vllm
  endpoints:
  - port: http
    path: /metrics
    interval: 15s

KEDA 弹性伸缩

基于 vllm:num_requests_waiting(队列深度)驱动 HPA,实现按需弹性扩缩:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: vllm-scaledobject
  namespace: llm-serving
spec:
  scaleTargetRef:
    name: vllm-qwen3-32b
  pollingInterval: 15       # 每 15 秒轮询一次
  cooldownPeriod: 120       # 缩容冷却 120 秒(防止频繁缩容)
  minReplicaCount: 1        # 至少保留 1 个副本
  maxReplicaCount: 4        # 最多扩到 4 个副本(按 GPU 节点数量设置)

  triggers:
  - type: prometheus
    metadata:
      serverAddress: http://prometheus-operated.monitoring.svc:9090
      metricName: vllm_requests_waiting
      # 当队列深度超过 10 时开始扩容
      threshold: "10"
      query: |
        sum(vllm:num_requests_waiting{namespace="llm-serving", model="qwen3-32b"})

扩容验证

# 观察 ScaledObject 状态
kubectl get scaledobject vllm-scaledobject -n llm-serving
kubectl describe scaledobject vllm-scaledobject -n llm-serving

# 模拟高负载(使用 vegeta 压测)
echo "POST http://llm.internal.company.com/v1/chat/completions" | \
  vegeta attack -rate=50/s -duration=60s \
  -header="Content-Type: application/json" \
  -body='{"model":"qwen3-32b","messages":[{"role":"user","content":"解释量子纠缠"}]}' | \
  vegeta report

# 观察 Pod 副本数变化
watch kubectl get pods -n llm-serving

生产调优建议

GPU 显存利用率

# --gpu-memory-utilization 根据模型大小调整:
# Qwen3-7B  on 40GB A100:0.85~0.90
# Qwen3-32B on 4×H100 80GB(TP=4):0.88~0.92
# Llama4 Scout 17B on 2×H100:0.90

并发与批处理

# 高并发场景(在线 API 服务)
--max-num-seqs=512
--max-num-batched-tokens=32768

# 低并发但高吞吐(离线批处理)
--max-num-seqs=64
--max-num-batched-tokens=131072
--chunked-prefill-enabled

多模型实例隔离

不同模型部署在不同 Namespace,通过 ResourceQuota 限制 GPU 配额:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-inference-quota
  namespace: team-a
spec:
  hard:
    requests.nvidia.com/gpu: "4"
    limits.nvidia.com/gpu: "4"

常见问题

OOM Killed

# 降低 --gpu-memory-utilization 或 --max-num-seqs
# 检查是否有其他进程占用 GPU 显存
nvidia-smi -q -d MEMORY

首 Token 延迟高(TTFT > 500ms)

# 检查 Prefix Cache 命中率
curl http://<vllm-svc>/metrics | grep prefix_cache_hit_rate

# 若命中率低,检查 System Prompt 是否一致(相同 System Prompt 才能命中缓存)
# 同时检查 --max-model-len 设置是否过大,压缩了可用 KV Cache 空间

Pod 重启循环(Liveness 探针失败)

kubectl describe pod <vllm-pod>
# 若是模型加载时间过长导致 Liveness 超时,增大 initialDelaySeconds
# 或改用 startupProbe 替代 livenessProbe 在启动阶段的判断

小结

vLLM on Kubernetes 的生产核心三件套:合理的健康探针防止流量打到未就绪实例 + KEDA 基于队列深度弹性扩缩 + Prefix Caching 降低长上下文 TTFT。GPU 资源 requests 等于 limits 是防止 OOM 的基础原则,不要设置 requests < limits。多副本场景下注意模型加载时间(大模型从 NFS 加载可能需要 3-5 分钟),startupProbe 的 failureThreshold 要足够大。