人工智能实战:GPU 服务如何上 Kubernetes?从单机部署到 K8s + NVIDIA Device Plugin + HPA 的生产级改造
一、问题场景:单机部署能跑,但一上线就难维护
前面我们已经把大模型服务做到了:
text
1. vLLM 高吞吐推理
2. Redis 队列削峰
3. Prometheus 监控
4. 限流、熔断、降级
单机部署时,启动方式大概是:
bash
CUDA_VISIBLE_DEVICES=0 python -m vllm.entrypoints.openai.api_server --model xxx --port 8001
CUDA_VISIBLE_DEVICES=1 python -m vllm.entrypoints.openai.api_server --model xxx --port 8002
uvicorn app:app --port 8000
刚开始这套方式很方便。
但当服务进入准生产环境后,问题逐渐暴露:
text
1. 某个进程挂了,需要人工重启
2. GPU 资源分配靠人记
3. 多台机器部署容易混乱
4. 服务扩容要手动改端口、改配置
5. 发布新版本时容易影响正在运行的服务
6. 监控、日志、重启策略都不统一
这时问题已经不再是"模型怎么跑",而是:
text
大模型服务如何工程化运维。
Kubernetes 的价值就在这里。
它不是为了让部署变复杂,而是为了解决:
text
调度、隔离、重启、扩容、发布、观测
这篇文章就从真实部署问题出发,讲清楚如何把一个 GPU 大模型服务迁移到 Kubernetes。
二、真实问题:为什么不能一直用裸机部署?
裸机部署有一个致命缺点:
text
资源和服务强绑定。
例如:
text
A 模型占 GPU0
B 模型占 GPU1
C 服务监听 8000
D 服务监听 8001
随着服务增多,你会开始维护各种表格:
text
哪台机器
哪个 GPU
哪个端口
哪个模型
哪个进程
谁负责重启
这类系统早期能跑,但后期一定会乱。
典型事故包括:
text
1. 两个模型抢同一张 GPU
2. 服务挂了没人发现
3. 升级版本后旧进程没杀干净
4. GPU 显存被僵尸进程占用
5. 手动扩容后负载均衡没更新
所以,大模型系统到一定规模后,必须从:
text
进程管理
升级到:
text
资源调度
Kubernetes 解决的正是这个问题。
三、K8s 部署 GPU 服务的核心概念
很多人第一次在 K8s 上跑 GPU 服务会踩坑,主要是因为把它当成普通 Deployment。
普通 CPU 服务只需要:
yaml
resources:
requests:
cpu: "1"
memory: "2Gi"
但 GPU 不是普通资源。
Kubernetes 默认并不知道你的节点上有 NVIDIA GPU。
你需要安装:
text
NVIDIA Device Plugin
安装后,K8s 才能识别:
text
nvidia.com/gpu
然后你才能在 Pod 中声明:
yaml
resources:
limits:
nvidia.com/gpu: 1
这句话的意思是:
text
这个 Pod 需要独占 1 张 GPU。
注意,是独占,不是共享。
四、目标架构
迁移后的目标架构如下:
text
Client
↓
Ingress / Nginx
↓
LLM Gateway Deployment
↓
LLM vLLM Deployment
↓
GPU Node
如果加上队列:
text
Client
↓
API Gateway
↓
Redis Queue
↓
Worker Deployment
↓
vLLM Service
↓
GPU Pod
这里建议把系统拆成两类服务:
text
1. Gateway:轻量 CPU 服务,负责鉴权、限流、路由、日志
2. vLLM:重型 GPU 服务,负责模型推理
不要把业务逻辑和模型推理写在一个 Pod 里。
这样后续才能做到:
text
业务服务独立扩容
GPU 服务独立扩容
模型服务独立升级
五、可复现前置条件
需要准备:
text
1. 一个 Kubernetes 集群
2. 至少一个带 NVIDIA GPU 的节点
3. 节点已安装 NVIDIA Driver
4. kubectl 可正常访问集群
5. 容器镜像仓库
检查 GPU 节点:
bash
nvidia-smi
检查节点:
bash
kubectl get nodes -o wide
六、安装 NVIDIA Device Plugin
执行:
bash
kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.5/nvidia-device-plugin.yml
查看插件状态:
bash
kubectl get pods -n kube-system | grep nvidia
查看节点是否识别 GPU:
bash
kubectl describe node <your-gpu-node-name> | grep nvidia.com/gpu
正常应该看到类似:
text
nvidia.com/gpu: 1
如果没有看到,通常是:
text
1. NVIDIA Driver 没装好
2. nvidia-container-toolkit 没装
3. device plugin 没启动成功
七、构建一个可部署的 vLLM 镜像
创建 Dockerfile:
dockerfile
FROM nvidia/cuda:12.1.1-runtime-ubuntu22.04
WORKDIR /app
RUN apt-get update && apt-get install -y \
python3 \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
RUN pip3 install vllm fastapi uvicorn
EXPOSE 8000
CMD ["python3", "-m", "vllm.entrypoints.openai.api_server", \
"--model", "Qwen/Qwen2.5-0.5B-Instruct", \
"--host", "0.0.0.0", \
"--port", "8000", \
"--trust-remote-code", \
"--gpu-memory-utilization", "0.80", \
"--max-model-len", "2048"]
构建镜像:
bash
docker build -t your-registry/llm-vllm:qwen-0.5b .
推送镜像:
bash
docker push your-registry/llm-vllm:qwen-0.5b
注意:
text
生产环境建议把模型提前下载到镜像或挂载持久化缓存目录。
否则每次 Pod 启动都要重新下载模型,会非常慢。
八、Deployment 部署 vLLM
创建 vllm-deployment.yaml:
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-qwen
labels:
app: vllm-qwen
spec:
replicas: 1
selector:
matchLabels:
app: vllm-qwen
template:
metadata:
labels:
app: vllm-qwen
spec:
containers:
- name: vllm
image: your-registry/llm-vllm:qwen-0.5b
ports:
- containerPort: 8000
resources:
limits:
nvidia.com/gpu: 1
memory: "24Gi"
cpu: "4"
requests:
memory: "16Gi"
cpu: "2"
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 60
periodSeconds: 10
failureThreshold: 12
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 120
periodSeconds: 20
failureThreshold: 3
部署:
bash
kubectl apply -f vllm-deployment.yaml
查看:
bash
kubectl get pods -l app=vllm-qwen
九、Service 暴露 vLLM
创建 vllm-service.yaml:
yaml
apiVersion: v1
kind: Service
metadata:
name: vllm-qwen
spec:
selector:
app: vllm-qwen
ports:
- protocol: TCP
port: 8000
targetPort: 8000
type: ClusterIP
应用:
bash
kubectl apply -f vllm-service.yaml
集群内部访问地址:
text
http://vllm-qwen:8000
测试:
bash
kubectl run curl-test --image=curlimages/curl -it --rm -- sh
在容器中执行:
bash
curl http://vllm-qwen:8000/health
十、部署 Gateway 服务
Gateway 不需要 GPU,它负责调用 vLLM。
gateway.py:
python
import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
app = FastAPI(title="LLM Gateway")
VLLM_URL = "http://vllm-qwen:8000/v1/chat/completions"
MODEL_NAME = "Qwen/Qwen2.5-0.5B-Instruct"
class ChatRequest(BaseModel):
prompt: str = Field(..., min_length=1, max_length=2000)
max_tokens: int = Field(default=128, ge=1, le=512)
@app.post("/chat")
async def chat(req: ChatRequest):
payload = {
"model": MODEL_NAME,
"messages": [
{"role": "user", "content": req.prompt}
],
"max_tokens": req.max_tokens,
"temperature": 0.7
}
try:
async with httpx.AsyncClient(timeout=60) as client:
resp = await client.post(VLLM_URL, json=payload)
resp.raise_for_status()
data = resp.json()
return {
"answer": data["choices"][0]["message"]["content"]
}
except Exception as e:
raise HTTPException(500, f"LLM call failed: {str(e)}")
@app.get("/health")
def health():
return {"status": "ok"}
Gateway Deployment:
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: llm-gateway
spec:
replicas: 2
selector:
matchLabels:
app: llm-gateway
template:
metadata:
labels:
app: llm-gateway
spec:
containers:
- name: gateway
image: your-registry/llm-gateway:latest
ports:
- containerPort: 8000
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
十一、为什么 vLLM Pod 不建议一开始多副本?
很多人会直接写:
yaml
replicas: 4
但 GPU 服务不能这样随便扩。
因为:
text
1 个 vLLM Pod = 1 张 GPU
如果你的节点只有 2 张 GPU,却写了 4 个副本,结果就是:
text
2 个 Running
2 个 Pending
查看:
bash
kubectl get pods
Pending 原因:
bash
kubectl describe pod <pod-name>
通常会看到:
text
Insufficient nvidia.com/gpu
这不是错误,而是资源不够。
十二、HPA 为什么不能直接按 GPU 扩容?
K8s 默认 HPA 支持:
text
CPU
Memory
但不直接支持:
text
GPU Utilization
Queue Size
P99 Latency
而大模型服务真正应该按什么扩容?
通常不是 CPU,而是:
text
1. 队列长度
2. P95 / P99 延迟
3. GPU 利用率
4. 请求等待时间
所以生产环境更推荐:
text
KEDA + Prometheus
例如按队列长度扩容 Worker。
十三、Worker 按队列扩容思路
如果你的架构是:
text
Gateway → Redis Queue → Worker → vLLM
那么 Worker 可以用 KEDA 根据 Redis 队列长度扩容。
示意配置:
yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: llm-worker-scaler
spec:
scaleTargetRef:
name: llm-worker
minReplicaCount: 1
maxReplicaCount: 5
triggers:
- type: redis
metadata:
address: redis.default.svc.cluster.local:6379
listName: llm_queue
listLength: "20"
这表示:
text
当 Redis 队列长度变长时,自动扩容 Worker。
注意:
text
Worker 扩容不等于 vLLM 扩容。
如果 GPU 已经满了,盲目增加 Worker 只会让请求争抢更严重。
十四、验证部署是否成功
1. 查看 Pod
bash
kubectl get pods
2. 查看 GPU 分配
bash
kubectl describe node <gpu-node-name> | grep -A 5 nvidia.com/gpu
3. 查看日志
bash
kubectl logs -f deploy/vllm-qwen
4. 端口转发测试
bash
kubectl port-forward svc/llm-gateway 8080:8000
请求:
bash
curl -X POST "http://127.0.0.1:8080/chat" \
-H "Content-Type: application/json" \
-d '{
"prompt": "解释一下Kubernetes中GPU调度的原理",
"max_tokens": 128
}'
十五、踩坑记录
坑 1:Pod 一直 Pending
现象:
text
Pod Pending
查看:
bash
kubectl describe pod <pod-name>
如果看到:
text
Insufficient nvidia.com/gpu
说明 GPU 不够,或者 Device Plugin 没生效。
坑 2:容器里无法访问 GPU
进入容器:
bash
kubectl exec -it <pod-name> -- bash
nvidia-smi
如果提示找不到 GPU,检查:
text
1. 节点驱动
2. nvidia-container-toolkit
3. NVIDIA Device Plugin
4. Pod 是否声明 nvidia.com/gpu
坑 3:readinessProbe 设置太短
大模型服务启动慢,尤其是首次加载模型。
如果 readinessProbe 太激进,K8s 会认为服务不可用。
建议:
yaml
initialDelaySeconds: 60
failureThreshold: 12
大模型越大,这个时间越要加长。
坑 4:每次 Pod 重启都重新下载模型
这会导致:
text
Pod 启动非常慢
镜像拉取正常但模型下载卡住
解决方案:
text
1. 使用持久化模型缓存
2. 将模型预置到镜像
3. 使用 initContainer 预拉模型
坑 5:Gateway 和 vLLM 混在一个容器
短期方便,长期麻烦。
问题包括:
text
1. 业务逻辑无法独立升级
2. 模型服务无法独立扩容
3. 故障边界不清晰
4. CPU 服务被 GPU 服务拖累
建议拆开。
十六、适合收藏的 K8s GPU 部署 Checklist
text
基础环境:
[ ] GPU 节点能执行 nvidia-smi
[ ] 已安装 nvidia-container-toolkit
[ ] 已安装 NVIDIA Device Plugin
[ ] kubectl describe node 能看到 nvidia.com/gpu
Deployment:
[ ] Pod 声明 nvidia.com/gpu
[ ] readinessProbe 时间足够长
[ ] livenessProbe 不要过于激进
[ ] 模型缓存已处理
[ ] 资源 requests / limits 合理
架构:
[ ] Gateway 和 vLLM 分离
[ ] vLLM 通过 ClusterIP 暴露
[ ] 外部只暴露 Gateway
[ ] GPU 服务有独立监控
[ ] Pod Pending 有排查流程
十七、验证结果
迁移到 Kubernetes 后,系统变化主要体现在运维能力上:
text
1. 进程挂了可以自动重启
2. 服务发布更规范
3. Gateway 可以水平扩容
4. GPU 资源分配更清晰
5. Pod 状态可观测
6. 后续接入 Prometheus、KEDA 更方便
但也要注意:
text
Kubernetes 不会自动让模型更快。
它解决的是:
text
资源调度和服务治理问题。
如果你的推理本身很慢,仍然要从模型、vLLM、batch、KV Cache 角度优化。
十八、经验总结
这次迁移给我的核心经验是:
text
裸机部署适合验证,Kubernetes 适合生产治理。
大模型服务进入生产后,需要的不是简单启动一个 Python 进程,而是:
text
1. 资源隔离
2. 自动重启
3. 滚动发布
4. 服务发现
5. 监控告警
6. 弹性扩容
K8s 的价值不在于让部署更酷,而在于让系统可维护。
十九、优化建议
后续可以继续做:
text
1. 使用 Helm 管理部署模板
2. 使用 KEDA 按队列长度扩容 Worker
3. 使用 Prometheus Adapter 按自定义指标扩容
4. 使用 NodeSelector / Taints 精确调度 GPU 节点
5. 使用模型缓存 PVC 加速 Pod 启动
6. 使用蓝绿发布降低模型升级风险
7. 使用多 vLLM 实例做模型路由
一句话总结:
text
当大模型服务从 Demo 走向生产,K8s 不是加分项,而是工程治理的起点。