侧边栏壁纸
  • 累计撰写 238 篇文章
  • 累计创建 245 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

vLLM 上 K8S:服务部署、对外暴露、监控与验证

zhanjie.me
2025-10-01 / 0 评论 / 0 点赞 / 2 阅读 / 0 字

上一篇里,我们已经把 vLLM 的单机路径跑通了:

  • 本地 Python/Conda 可以启动
  • Docker 容器可以启动
  • OpenAI 兼容接口也已经能返回结果

但从团队使用和平台化角度看,这还只是“单机工具可用”,并不等于“集群服务可用”。

因为一旦你把问题换成下面这些,单机部署很快就会显得不够:

  • 这套服务能不能给团队其他人共用
  • 模型文件怎么挂到运行实例上
  • GPU 资源怎么在集群里被调度
  • 服务怎么统一暴露出去
  • 运行中怎么知道是 GPU 有问题,还是推理接口有问题

这就是为什么 vLLM 的下一步自然会走到 Kubernetes。

不过,vLLM 上 K8S 真正要解决的,并不是“换个地方启动同一条命令”,而是:

把一个单机上能跑的推理 API,变成一个有资源约束、可共享访问、可观测、可运维的集群推理服务。

这一篇就围绕这件事来展开。

和前面几篇一样,下面会尽量保持可操作写法:

  • 先讲为什么要做
  • 再给命令
  • 关键地方补输出
  • 明确什么样才算真的成功

文中的命名空间、Pod 名称、PVC 名称、Prometheus chart 版本、Pod 输出和时间戳基于课程环境整理,与你的环境不同属于正常现象。
重点是部署链路和判断方式。

摘要

如果你只想先拿结论,可以先记住这条链路:

  1. 先确认 K8S 集群已经具备 GPU 运行基础,并部署 NVIDIA Device Plugin
  2. 再准备模型文件目录,并通过 PV/PVC 挂载给 vLLM
  3. 为 GPU 节点打标签,让工作负载只落到可用节点
  4. 部署 vLLM Deployment,通过 Service 暴露接口
  5. Gateway API 或等价入口把服务暴露到集群外
  6. 再补 DCGM Exporter + Prometheus + Grafana,把 GPU 和 vLLM 的状态看见

这一篇的核心验收点有四个:

  • NVIDIA Device Plugin Pod 正常 Running
  • vLLM 的 PVC 成功 Bound,Pod 从 ContainerCreating 变成 Running
  • kubectl logs 能看到 vLLM API server version ...
  • 通过 Service/Gateway 能调通 /v1/models/v1/chat/completions

系列导航

这是“大模型私有化部署实践”系列的第七篇。当前系列顺序如下:

  1. 本地、Docker、K8S:大模型私有化部署路线怎么选
  2. 大模型推理环境准备实战:GPU、驱动、CUDA、容器运行时
  3. 基于 Ubuntu 24.04 搭建 AI 推理用原生 K8S 集群
  4. 为 K8S 补齐入口与存储:MetalLB、Gateway API、NFS 动态供给
  5. 用 Ollama + Open WebUI 快速搭建本地 AI 体验环境
  6. vLLM 私有化部署实战:本地部署、Docker 部署、接口验证
  7. vLLM 上 K8S:服务部署、对外暴露、监控与验证
  8. SGLang 私有化部署实战:本地部署、Docker 部署、能力体验
  9. SGLang 上 K8S:接入 Open WebUI、服务发布与 GPU 运维
  10. 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 生效,你通常会在 CapacityAllocatable 里看到:

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 对应前面已经绑定好的 PVC
  • prometheus.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 Service
  • Gateway
  • HTTPRoute

这样后面和 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-pvcBound

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

确认:

  • PrometheusGrafanaDCGM 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 gatewaykubectl 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 的差异,以及它为什么更适合复杂推理编排场景。

0

评论区