上一篇里,我们已经把 vLLM 的单机路径跑通了:
- 本地 Python/Conda 可以启动
- Docker 容器可以启动
- OpenAI 兼容接口也已经能返回结果
但从团队使用和平台化角度看,这还只是“单机工具可用”,并不等于“集群服务可用”。
因为一旦你把问题换成下面这些,单机部署很快就会显得不够:
- 这套服务能不能给团队其他人共用
- 模型文件怎么挂到运行实例上
- GPU 资源怎么在集群里被调度
- 服务怎么统一暴露出去
- 运行中怎么知道是 GPU 有问题,还是推理接口有问题
这就是为什么 vLLM 的下一步自然会走到 Kubernetes。
不过,vLLM 上 K8S 真正要解决的,并不是“换个地方启动同一条命令”,而是:
把一个单机上能跑的推理 API,变成一个有资源约束、可共享访问、可观测、可运维的集群推理服务。
这一篇就围绕这件事来展开。
和前面几篇一样,下面会尽量保持可操作写法:
- 先讲为什么要做
- 再给命令
- 关键地方补输出
- 明确什么样才算真的成功
文中的命名空间、Pod 名称、PVC 名称、Prometheus chart 版本、Pod 输出和时间戳基于课程环境整理,与你的环境不同属于正常现象。
重点是部署链路和判断方式。
摘要
如果你只想先拿结论,可以先记住这条链路:
- 先确认 K8S 集群已经具备 GPU 运行基础,并部署
NVIDIA Device Plugin - 再准备模型文件目录,并通过
PV/PVC挂载给 vLLM - 为 GPU 节点打标签,让工作负载只落到可用节点
- 部署 vLLM
Deployment,通过Service暴露接口 - 用
Gateway API或等价入口把服务暴露到集群外 - 再补
DCGM Exporter + Prometheus + Grafana,把 GPU 和 vLLM 的状态看见
这一篇的核心验收点有四个:
NVIDIA Device PluginPod 正常 Running- vLLM 的
PVC成功Bound,Pod 从ContainerCreating变成Running kubectl logs能看到vLLM API server version ...- 通过
Service/Gateway能调通/v1/models或/v1/chat/completions
系列导航
这是“大模型私有化部署实践”系列的第七篇。当前系列顺序如下:
- 本地、Docker、K8S:大模型私有化部署路线怎么选
- 大模型推理环境准备实战:GPU、驱动、CUDA、容器运行时
- 基于 Ubuntu 24.04 搭建 AI 推理用原生 K8S 集群
- 为 K8S 补齐入口与存储:MetalLB、Gateway API、NFS 动态供给
- 用 Ollama + Open WebUI 快速搭建本地 AI 体验环境
- vLLM 私有化部署实战:本地部署、Docker 部署、接口验证
- vLLM 上 K8S:服务部署、对外暴露、监控与验证
- SGLang 私有化部署实战:本地部署、Docker 部署、能力体验
- SGLang 上 K8S:接入 Open WebUI、服务发布与 GPU 运维
- vLLM 和 SGLang 到底怎么选
如果说上一篇解决的是“单机层面把 vLLM 跑通”,这一篇解决的就是:
如何把单机上可用的 vLLM 变成一个真正放进 K8S、可以被团队共享访问和持续观测的推理服务。
1. 为什么单机可用,不等于集群可用
很多团队第一次把单机上的 vLLM 往 K8S 搬时,容易低估这里真正增加的复杂度。
单机时,你只需要关心几件事:
- GPU 是否可用
- 模型路径是否存在
- 端口 8000 是否监听成功
但到了集群里,这些问题会被拆成更多层:
- GPU 资源有没有真正注册到 K8S
- 模型目录怎么挂进 Pod
- 调度器怎么知道这个 Pod 必须去 GPU 节点
- 其他服务怎么通过 Service 或 Gateway 访问它
- 失败时到底是模型没加载好,还是入口没打通
所以从单机走向 K8S,本质上不是“把命令搬到 YAML 里”,而是把原来由人手动保证的那些条件,变成平台对象和运行时约束。
2. 部署前要先确认:前 3 篇和前 4 篇的底座已经到位
在真正写 vLLM 的 Deployment 之前,至少要先确认下面这些已经成立:
- 集群节点都是
Ready Calico正常MetalLB正常Gateway API正常NFS动态供给或等价存储已经可用
你可以先跑这组命令做一个前置检查:
kubectl get nodes
kubectl get pods -n metallb-system
kubectl get pods -n envoy-gateway-system
kubectl get sc
如果这些底座都还没稳定,就先不要急着部署 vLLM 工作负载。
3. 第一层:部署 NVIDIA Device Plugin,让 K8S 认识 GPU
这是 vLLM 上 K8S 最容易被忽略、但又最不能跳过的一层。
3.1 为什么仅仅宿主机有 GPU 还不够
宿主机上能跑 nvidia-smi,并不意味着 K8S 调度器已经知道“这台节点有几个 GPU 可以分配”。
要让 Pod 里写的:
nvidia.com/gpu: 1
真正生效,K8S 需要先通过 NVIDIA Device Plugin 把 GPU 注册成可调度资源。
3.2 部署 Device Plugin
课程里使用的是官方静态清单:
kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.17.1/deployments/static/nvidia-device-plugin.yml
也可以先下载下来再看:
wget https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.17.1/deployments/static/nvidia-device-plugin.yml
kubectl apply -f nvidia-device-plugin.yml
3.3 查看 Pod 状态
kubectl get pods -n kube-system
刚创建出来时,课程里的典型状态会先经历:
nvidia-device-plugin-daemonset-pbrxz 0/1 ContainerCreating 0 11s
稍等一会再看,正常应该会变成:
nvidia-device-plugin-daemonset-pbrxz 1/1 Running 0 2m
这里最重要的不是 Pod 名称,而是状态从 ContainerCreating 进入 Running。
3.4 确认节点已经注册 GPU 资源
你可以进一步看节点资源:
kubectl describe node k8s-master01 | grep -A5 Capacity
kubectl describe node k8s-master01 | grep -A5 Allocatable
如果 Device Plugin 生效,你通常会在 Capacity 或 Allocatable 里看到:
nvidia.com/gpu: 1
如果这里没有,说明 GPU 还没有真正成为 K8S 可调度资源。
4. 第二层:准备模型存储和 GPU 节点约束
在 K8S 里,vLLM 的工作负载如果要稳定运行,通常至少要解决两件事:
- 模型文件怎么挂进去
- Pod 应该落到哪台 GPU 节点
4.1 先准备模型目录
课程环境里,先把模型复制到了 NFS 共享目录,例如:
mkdir -p /netshare/Deepseek-Qwen
cp -r /data/ModelScope/Deepseek-Qwen/* /netshare/Deepseek-Qwen/
这一步的目标很简单:
- 让 vLLM Pod 启动时不用临时再下载模型
- 让模型目录能通过共享存储挂进去
4.2 创建命名空间
课程里使用的是:
kubectl create namespace vllm-production
kubectl get ns
正常输出中应该能看到:
vllm-production Active 3s
4.3 给 GPU 节点打标签
这是一个非常实用的动作,因为它能让你明确控制:
- 哪些节点允许跑推理服务
- 哪些节点只是控制平面或普通工作负载节点
课程里的示例:
kubectl label nodes k8s-master01 nvidia.com/gpu=true
kubectl get nodes --show-labels | grep nvidia.com/gpu
预期输出类似:
k8s-master01 Ready control-plane 162m v1.35.0 ... nvidia.com/gpu=true
如果你实际的 GPU 节点不是 k8s-master01,就把标签打到真正的 GPU worker 上。
5. 第三层:为模型目录准备 PV/PVC
课程里采用的是 NFS 后端,并手动创建一组专门给 vLLM 用的 PV/PVC。
5.1 为什么这里不用“临时挂个 hostPath”
因为后面一旦进入正式服务阶段,你很快就会碰到这些问题:
- Pod 重建后模型目录丢失
- 节点一变,路径不一致
- 无法声明式管理
对于系列路线来说,这里更值得直接走 K8S 原生的卷声明方式。
5.2 创建 PV
课程里的结构是先定义一个带标签的 PV,例如:
apiVersion: v1
kind: PersistentVolume
metadata:
name: vllm-models-pv
labels:
type: nfs
app: vllm
spec:
capacity:
storage: 100Gi
accessModes:
- ReadOnlyMany
storageClassName: nfs-client
persistentVolumeReclaimPolicy: Retain
nfs:
server: 192.168.10.143
path: /netshare/Deepseek-Qwen
5.3 创建 PVC
对应的 PVC 类似这样:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vllm-models-pvc
namespace: vllm-production
spec:
accessModes:
- ReadOnlyMany
resources:
requests:
storage: 100Gi
selector:
matchLabels:
type: nfs
app: vllm
storageClassName: nfs-client
5.4 应用并检查状态
kubectl apply -f vllm-pv.yaml
kubectl apply -f vllm-pvc.yaml
kubectl get pvc -n vllm-production
kubectl get pv
课程里的典型成功输出如下:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
vllm-models-pvc Bound vllm-models-pv 100Gi ROX nfs-client 7s
以及:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS AGE
vllm-models-pv 100Gi ROX Retain Bound vllm-production/vllm-models-pvc nfs-client 3m37s
只要 PVC 还在 Pending,就先不要继续部署 vLLM。
6. 第四层:把 vLLM 真正部署成 K8S 工作负载
到了这一步,前面的准备才真正开始发挥作用:
- GPU 资源可调度
- 模型目录可挂载
- 命名空间和节点约束也准备好了
6.1 一个适合起步的 Deployment 结构
课程里使用的是 Deployment,并用 vllm.entrypoints.openai.api_server 作为启动命令。
一个简化后的结构大致如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-deployment
namespace: vllm-production
spec:
replicas: 1
selector:
matchLabels:
app: vllm
template:
metadata:
labels:
app: vllm
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
prometheus.io/path: "/metrics"
spec:
nodeSelector:
nvidia.com/gpu: "true"
containers:
- name: vllm
image: vllm/vllm-openai:latest
imagePullPolicy: IfNotPresent
securityContext:
runAsUser: 0
runAsGroup: 0
command: ["python3"]
args:
- "-m"
- "vllm.entrypoints.openai.api_server"
- "--model"
- "/models"
- "--served-model-name"
- "deepseek"
- "--max-model-len"
- "8192"
- "--dtype"
- "auto"
- "--gpu-memory-utilization"
- "0.9"
- "--port"
- "8000"
- "--host"
- "0.0.0.0"
- "--trust-remote-code"
- "--load-format"
- "safetensors"
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: model-volume
mountPath: /models
readOnly: true
volumes:
- name: model-volume
persistentVolumeClaim:
claimName: vllm-models-pvc
这里要特别关注四个点:
nodeSelector让 Pod 只去 GPU 节点nvidia.com/gpu: 1真正声明了 GPU 资源/models对应前面已经绑定好的 PVCprometheus.io/*注解给后面的监控做铺垫
6.2 应用 Deployment
kubectl apply -f vllm-deployment.yaml
kubectl get pods -n vllm-production
课程里的典型状态变化是:
先看到:
NAME READY STATUS RESTARTS AGE
vllm-deployment-5bf95cd79-mdxv9 0/1 ContainerCreating 0 24s
过一会再看:
NAME READY STATUS RESTARTS AGE
vllm-deployment-5bf95cd79-mdxv9 1/1 Running 0 119s
这一步非常有代表性。
在 K8S 里,vLLM 从 ContainerCreating 变成 Running,通常意味着:
- 镜像拉取正常
- 卷挂载正常
- GPU 资源调度成功
6.3 看日志确认 API 真的起来了
kubectl logs -n vllm-production deploy/vllm-deployment
课程里的日志中会出现类似:
(APIServer pid=1) INFO ... vLLM API server version 0.13.0
只要这里能看到 vLLM API server version 这类关键字,基本就说明应用进程已经真正起来了。
7. 第五层:给 vLLM 创建 Service,并做集群内验证
Pod 能跑不代表服务已经可用。
接下来要把它通过 K8S Service 稳定暴露出来。
7.1 创建 ClusterIP Service
你可以为 vLLM 创建一个最基础的 Service:
apiVersion: v1
kind: Service
metadata:
name: vllm
namespace: vllm-production
spec:
selector:
app: vllm
ports:
- name: http
port: 8000
targetPort: 8000
type: ClusterIP
应用后查看:
kubectl apply -f vllm-svc.yaml
kubectl get svc -n vllm-production
预期输出类似:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
vllm ClusterIP 10.102.44.120 <none> 8000/TCP 10s
7.2 用临时 Pod 做集群内验证
先不要急着上 Gateway,先确认 Service 本身通了:
kubectl run curl-test -n vllm-production --rm -it --image=curlimages/curl -- sh
进入临时容器后测试:
curl http://vllm:8000/v1/models
如果服务正常,返回通常类似:
{
"object": "list",
"data": [
{
"id": "deepseek",
"object": "model",
"owned_by": "vllm"
}
]
}
再做一次推理接口验证:
curl http://vllm:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek",
"messages": [
{"role": "user", "content": "请用一句话介绍 vLLM 的优势"}
],
"max_tokens": 128
}'
如果这里已经通了,说明:
- Pod 本身正常
- Service 也正常
- 集群内部调用链路成立
8. 第六层:通过 Gateway 对外暴露 vLLM
前面第 4 篇已经把 MetalLB + Gateway API 补齐了。
这一步就是把那些底座能力真正用到 vLLM 上。
8.1 为什么这里不建议直接先上 NodePort
NodePort 当然能用,但对后面多服务、多域名、多入口治理来说会越来越粗糙。
在这套系列里,我更建议一开始就走:
ClusterIP ServiceGatewayHTTPRoute
这样后面和 Open WebUI、SGLang 的入口模型也能保持一致。
8.2 创建 Gateway 和 HTTPRoute
可以参考前面的 Gateway API 写法,为 vLLM 准备类似资源:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: vllm-gateway
namespace: vllm-production
spec:
gatewayClassName: eg
listeners:
- name: http
protocol: HTTP
port: 80
hostname: "vllm.kubemsb.com"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: vllm-route
namespace: vllm-production
spec:
parentRefs:
- name: vllm-gateway
hostnames:
- "vllm.kubemsb.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: vllm
port: 8000
应用并查看:
kubectl apply -f vllm-gateway.yaml
kubectl get gateway -n vllm-production
kubectl get httproute -n vllm-production
kubectl get svc -n envoy-gateway-system | grep LoadBalancer
如果 Gateway 正常工作,Envoy 入口层的 LoadBalancer Service 应该已经拿到 MetalLB 分配的外部 IP。
8.3 用 Host 头或域名做外部访问验证
如果你已经做好本地 hosts 解析,可以直接访问:
http://vllm.kubemsb.com/v1/models
也可以先用 curl 验证:
curl -H "Host: vllm.kubemsb.com" http://192.168.10.240/v1/models
如果返回模型列表,就说明从集群外访问 vLLM 的入口链路已经打通。
9. 第七层:把 GPU 和服务状态真正看见
很多部署文章到 Pod Running 就结束了。
但对正式服务来说,这远远不够。
因为后续你迟早会碰到这些问题:
- 是 GPU 不够了,还是请求量上来了
- 是 Pod 卡住了,还是模型加载慢
- 是接口错误率高,还是节点本身有波动
这时候,监控不是“加分项”,而是运维基础。
10. 用 Prometheus + Grafana + DCGM Exporter 做基础监控
课程里的监控思路分两层:
kube-prometheus-stack:负责 K8S 和服务层监控DCGM Exporter:负责 NVIDIA GPU 指标暴露
10.1 先准备 Helm
课程里的准备方式类似这样:
wget https://get.helm.sh/helm-v4.0.5-linux-amd64.tar.gz
tar xf helm-v4.0.5-linux-amd64.tar.gz
mv linux-amd64/helm /usr/local/bin/helm
helm version
典型输出:
version.BuildInfo{Version:"v4.0.5", ...}
10.2 准备并安装 kube-prometheus-stack
课程中使用的是:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm pull prometheus-community/kube-prometheus-stack --version 81.0.0
tar xf kube-prometheus-stack-81.0.0.tgz
然后通过 values 文件安装:
helm install kube-prometheus-stack /root/kube-prometheus-stack \
--namespace monitoring \
--create-namespace \
-f prometheus-values.yaml \
--set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false \
--set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false
安装完成后查看:
kubectl get pods -n monitoring
课程里的典型成功输出类似:
alertmanager-kube-prometheus-stack-alertmanager-0 2/2 Running 0 53m
kube-prometheus-stack-grafana-957ffd8df-wf628 3/3 Running 0 53m
kube-prometheus-stack-kube-state-metrics-6ff49f8969-74m62 1/1 Running 0 53m
prometheus-kube-prometheus-stack-prometheus-0 2/2 Running 0 53m
10.3 部署 NVIDIA DCGM Exporter
课程里使用的是 DaemonSet 方式在 GPU 节点上采集指标。
核心结构类似这样:
apiVersion: v1
kind: ServiceAccount
metadata:
name: nvidia-dcgm-exporter
namespace: monitoring
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nvidia-dcgm-exporter
namespace: monitoring
spec:
selector:
matchLabels:
app: nvidia-dcgm-exporter
template:
metadata:
labels:
app: nvidia-dcgm-exporter
spec:
serviceAccountName: nvidia-dcgm-exporter
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
containers:
- name: nvidia-dcgm-exporter
image: nvidia/dcgm-exporter:4.4.2-4.7.1-ubuntu22.04
args:
- "-f"
- "/etc/dcgm-exporter/dcp-metrics-included.csv"
应用后查看:
kubectl apply -f dcgm-exporter.yaml
kubectl get pods -n monitoring
只要 GPU 节点上对应 Pod 已经 Running,说明 GPU 指标已经有机会被 Prometheus 抓取。
10.4 利用 vLLM 自带 /metrics
课程里给 vLLM Pod 加了这些注解:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
prometheus.io/path: "/metrics"
这一步很有价值,因为 vLLM 本身就能暴露一部分服务指标。
也就是说,监控层至少可以同时覆盖两类数据:
- GPU 侧:来自 DCGM Exporter
- 服务侧:来自 vLLM
/metrics
11. 一次完整验收:什么样才算这套 vLLM K8S 服务真的跑成了
为了避免“Pod Running 就算结束”的误判,我建议至少把下面这些点都走一遍。
11.1 资源层
kubectl get pods -n kube-system | grep nvidia-device-plugin
kubectl describe node <gpu-node-name> | grep -A5 nvidia.com/gpu
确认:
- Device Plugin Pod 在跑
- 节点已注册
nvidia.com/gpu
11.2 存储层
kubectl get pvc -n vllm-production
kubectl get pv
确认:
vllm-models-pvc已Bound
11.3 应用层
kubectl get pods -n vllm-production
kubectl logs -n vllm-production deploy/vllm-deployment
确认:
- Pod 是
Running - 日志里出现
vLLM API server version ...
11.4 接口层
kubectl run curl-test -n vllm-production --rm -it --image=curlimages/curl -- sh
curl http://vllm:8000/v1/models
以及:
curl -H "Host: vllm.kubemsb.com" http://192.168.10.240/v1/models
确认:
- 集群内接口能通
- 集群外入口也能通
11.5 监控层
kubectl get pods -n monitoring
确认:
Prometheus、Grafana、DCGM Exporter都在运行
12. 最常见的问题,通常卡在这几层
到了 K8S 这一步,问题往往比单机更像“层与层之间没接起来”,而不是单一组件故障。
12.1 Pod 一直 ContainerCreating
最常见原因:
- PVC 没绑定
- 镜像没拉下来
- GPU 资源没调度到
优先看:
kubectl describe pod -n vllm-production <pod-name>
12.2 Pod Running,但接口没起来
这通常说明应用进程本身还没成功初始化。
优先看:
kubectl logs -n vllm-production deploy/vllm-deployment
12.3 Service 正常,但 Gateway 不通
这类问题要分层查:
- 集群内
curl http://vllm:8000/v1/models通不通 kubectl get gateway、kubectl get httproute正不正常- Envoy 的
LoadBalancer是否拿到 IP
12.4 GPU 有,但 Pod 调度不上
优先检查:
nvidia.com/gpu是否注册成功nodeSelector标签是不是打到了正确节点- 节点上的 GPU 是否已经被别的 Pod 占满
13. 结语:vLLM 上 K8S,真正变化的是“服务化能力”
从单机到 K8S,表面上看只是把启动命令搬进了 Deployment。
但真正发生变化的,其实是 vLLM 开始拥有了更完整的服务化能力:
- 有明确的资源声明
- 有模型目录挂载
- 有集群内访问入口
- 有集群外暴露方式
- 有 GPU 和服务层的监控
也就是说,这时候的 vLLM 已经不再只是“一个在某台机器上跑着的推理进程”,而更像一项真正的团队基础能力。
如果把系列往后接,这一篇解决的是:
- 把 vLLM 从单机 API 变成 K8S 服务
- 把访问、存储、监控串起来
- 为更复杂的推理框架上集群建立模板
下一篇文章,我们就继续沿着这条路往前走:
进入 SGLang 路线,先从本地部署和 Docker 部署开始,理解它和 vLLM 的差异,以及它为什么更适合复杂推理编排场景。
评论区