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

目 录CONTENT

文章目录

SGLang 上 K8S:接入 Open WebUI、服务发布与 GPU 运维

zhanjie.me
2025-10-07 / 0 评论 / 0 点赞 / 4 阅读 / 0 字

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

  • 本地环境能启动
  • Docker 容器能启动
  • /v1/models/v1/chat/completions 能返回结果
  • Open WebUI 也能直接对接它的 OpenAI 兼容接口

但只要你把目标从“单机体验”切换成“团队共享”,问题就会立刻变得不一样:

  • 模型文件怎么挂进 Pod
  • GPU 资源怎么在集群里稳定分配
  • Open WebUI 怎么和 SGLang 服务连起来
  • 服务怎么对外暴露
  • 运行中的 GPU 和推理链路怎么监控

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

不过,SGLang 上 K8S 真正要解决的,不是“把上一篇命令写进 Deployment”,而是:

把一个单机可用的复杂推理框架,变成一个在集群里可访问、可观察、可维护的正式服务。

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

为了延续前面几篇的实战风格,下面仍然尽量保持:

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

文中的命名空间、Pod 名称、NFS 地址、NodePort、Prometheus 版本、日志时间戳和指标示例基于课程环境整理,与你的环境不同属于正常现象。
重点是部署链路、验收方式和运维思路。

摘要

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

  1. 先准备好模型共享目录,并让 K8S 能通过 PV/PVC 把它挂进 SGLang Pod
  2. 再为 GPU 节点打标签,确保 SGLang 只被调度到可运行 GPU 的节点
  3. 通过 ConfigMap + Deployment + Service 跑起 SGLang
  4. 把 Open WebUI 接进来,让团队先能在浏览器里用起来
  5. 先用 NodePort 快速验证,再用 Gateway API 做更规整的服务发布
  6. 最后补 kube-prometheus-stack + DCGM Exporter + ServiceMonitor + PrometheusRule,把 GPU 和 SGLang 的状态真正看见

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

  • sglang-model-pvc 成功 Bound
  • sglang-deployment Pod 从 ContainerCreating 进入 Running
  • kubectl logs 能看到 Application startup complete
  • curl http://<sglang-pod-ip>:30000/v1/models 能返回模型列表
  • 浏览器打开 Open WebUI 后已经能通过 SGLang 对话
  • Prometheus 里能查到 sglang_*DCGM_* 指标

系列导航

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

  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 到底怎么选

如果说上一篇解决的是“单机层面把 SGLang 跑通”,这一篇解决的就是:

如何把 SGLang 做成一套能在 K8S 里承接团队使用、界面访问和 GPU 运维的完整服务。

1. 为什么 SGLang 上 K8S,重点不再是“能启动”

SGLang 和普通聊天接口框架不太一样,它的价值更多体现在复杂推理、多步骤执行和长上下文复用。

这意味着一旦你把它放到团队环境里,关注点会自然转向下面这些问题:

  • 一台 GPU 机器上能跑,不代表集群里就能稳定跑
  • API 能返回结果,不代表浏览器侧的 WebUI 已经可用
  • 服务暴露出来,不代表监控链路已经补齐
  • GPU 使用率高,不代表请求一定成功,可能只是排队严重

所以,SGLang 上 K8S 的重点不只是部署,而是把下面几层一起补起来:

  • 模型存储
  • GPU 调度
  • 服务访问
  • 界面接入
  • 指标监控
  • 告警规则

2. 开始之前,先确认 K8S 底座已经具备这些能力

真正开始前,建议先确认前 3 到前 7 篇涉及的基础已经稳定:

  • K8S 集群节点都已经 Ready
  • Calico 正常
  • MetalLB 正常
  • Gateway API 正常
  • NFS 动态供给或等价共享存储已经可用
  • NVIDIA Device Plugin 已经把 GPU 注册进 K8S

你可以先跑下面这组检查命令:

kubectl get nodes
kubectl get pods -n kube-system | grep nvidia-device-plugin
kubectl get pods -n envoy-gateway-system
kubectl get sc
kubectl get pv

一个正常的参考输出大致像这样:

NAME            STATUS   ROLES           AGE   VERSION
k8s-master01    Ready    control-plane   5h    v1.35.0
k8s-worker01    Ready    <none>          5h    v1.35.0
k8s-worker02    Ready    <none>          5h    v1.35.0

nvidia-device-plugin-daemonset-pbrxz   1/1   Running   0   3h

NAME          PROVISIONER                                         RECLAIMPOLICY   VOLUMEBINDINGMODE
nfs-client    cluster.local/nfs-subdir-external-provisioner       Delete          Immediate

如果这里都不稳,就先别急着上 SGLang 工作负载。

3. 先准备模型目录、命名空间和 GPU 节点标签

课件里这一步做得很直接,也很实用:
先把模型复制到共享目录,再创建独立命名空间,并明确告诉调度器哪些节点可以跑 GPU 任务。

3.1 把模型放到共享目录

mkdir -p /netshare/Deepseek-Qwen
cp -r /data/ModelScope/Deepseek-Qwen/* /netshare/Deepseek-Qwen/
ls /netshare/Deepseek-Qwen | head

如果目录准备正常,通常会看到类似:

config.json
generation_config.json
model.safetensors
tokenizer.json
tokenizer_config.json

这里最关键的是:

  • 共享目录对 K8S 节点可访问
  • 模型文件已经提前准备好
  • 至少存在权重文件和 tokenizer 相关文件

3.2 创建命名空间

kubectl create namespace sglang
kubectl get ns sglang

预期输出类似:

namespace/sglang created

NAME     STATUS   AGE
sglang   Active   5s

3.3 为 GPU 节点打标签

课件里直接给 k8s-master01 打了标签:

kubectl label nodes k8s-master01 nvidia.com/gpu=true
kubectl get nodes --show-labels | grep nvidia.com/gpu

预期输出类似:

node/k8s-master01 labeled

k8s-master01   Ready   control-plane   162m   v1.35.0   ...,nvidia.com/gpu=true

这一步的意义很明确:

  • 后面的 Deployment 可以通过 nodeSelector 或亲和性控制,限制只调度到 GPU 节点
  • 避免 Pod 被调度到没有 GPU 的节点上再启动失败

4. 用 PV/PVC 把模型挂进 SGLang Pod

和第 7 篇 vLLM on K8S 一样,模型文件最好不要在 Pod 启动时临时下载。
更稳的做法,是提前放到共享存储,再通过 PV/PVC 只读挂载进去。

4.1 创建 PV

下面这个例子基本沿用了课件里的思路:

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
  name: sglang-model-pv
  labels:
    type: nfs
    app: sglang
    model: deepseek-qwen
spec:
  capacity:
    storage: 100Gi
  volumeMode: Filesystem
  accessModes:
    - ReadOnlyMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs-client
  mountOptions:
    - nfsvers=4.1
    - hard
    - timeo=600
    - retrans=2
    - rsize=1048576
    - wsize=1048576
  nfs:
    server: 172.17.84.155
    path: /netshare/Deepseek-Qwen
EOF

如果你的 NFS 地址不同,把 serverpath 替换掉即可。

4.2 创建 PVC

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sglang-model-pvc
  namespace: sglang
spec:
  accessModes:
    - ReadOnlyMany
  storageClassName: nfs-client
  resources:
    requests:
      storage: 100Gi
  selector:
    matchLabels:
      app: sglang
      model: deepseek-qwen
EOF

4.3 检查绑定状态

kubectl get pv sglang-model-pv
kubectl get pvc -n sglang

正常应该看到类似结果:

NAME              CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   AGE
sglang-model-pv   100Gi      ROX            Retain           Available           nfs-client     7s

再过几秒:

NAME               STATUS   VOLUME            CAPACITY   ACCESS MODES   STORAGECLASS   AGE
sglang-model-pvc   Bound    sglang-model-pv   100Gi      ROX            nfs-client     14s

这一层只要不通,后面的 SGLang Pod 大概率会卡在:

  • ContainerCreating
  • 挂载失败
  • 或启动脚本提示 /models 下没有文件

5. 通过 ConfigMap + Deployment 跑起 SGLang

课件这里的做法很值得借鉴:
不是把超长启动命令直接塞到 YAML 里,而是先用 ConfigMap 提供启动脚本,再在 Deployment 里引用它。

5.1 创建 SGLang ConfigMap

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: sglang-config
  namespace: sglang
data:
  start.sh: |
    #!/bin/bash
    echo "Starting SGLang server..."
    if [ -f "/models/model.safetensors" ]; then
      echo "Model found at /models"
    else
      echo "Error: Model file not found at /models"
      ls -la /models/
      exit 1
    fi
    echo "Loading model from /models"
    python3 -m sglang.launch_server \
      --model-path /models \
      --host 0.0.0.0 \
      --port 30000 \
      --trust-remote-code \
      --served-model-name deepseek-qwen
  env.sh: |
    export CUDA_VISIBLE_DEVICES=0,1
    export HF_HOME=/cache
    export PYTHONUNBUFFERED=1
    if [ -d "/usr/local/lib/python3.12/dist-packages" ]; then
      export PYTHONPATH=/usr/local/lib/python3.12/dist-packages:$PYTHONPATH
    fi
EOF

执行后确认一下:

kubectl get configmap -n sglang

预期输出类似:

NAME            DATA   AGE
kube-root-ca.crt 1     5m
sglang-config    2     12s

5.2 创建 Deployment

下面这个版本把课件中的关键项都保留下来了:

cat <<'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sglang-deployment
  namespace: sglang
  labels:
    app: sglang
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sglang
  template:
    metadata:
      labels:
        app: sglang
    spec:
      nodeSelector:
        nvidia.com/gpu: "true"
      containers:
        - name: sglang
          image: lmsysorg/sglang:latest
          imagePullPolicy: IfNotPresent
          command: ["/bin/bash", "/config/start.sh"]
          env:
            - name: CUDA_VISIBLE_DEVICES
              value: "0,1"
            - name: HF_HOME
              value: "/cache"
            - name: PYTHONUNBUFFERED
              value: "1"
          ports:
            - containerPort: 30000
              name: sglang-api
          resources:
            limits:
              nvidia.com/gpu: 2
              memory: "32Gi"
              cpu: "8"
            requests:
              nvidia.com/gpu: 2
              memory: "16Gi"
              cpu: "4"
          volumeMounts:
            - name: model-storage
              mountPath: /models
              readOnly: true
            - name: cache-volume
              mountPath: /cache
            - name: config-volume
              mountPath: /config
          livenessProbe:
            httpGet:
              path: /model_info
              port: 30000
            initialDelaySeconds: 120
            periodSeconds: 30
            timeoutSeconds: 10
          readinessProbe:
            httpGet:
              path: /model_info
              port: 30000
            initialDelaySeconds: 60
            periodSeconds: 10
            timeoutSeconds: 5
          startupProbe:
            httpGet:
              path: /model_info
              port: 30000
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 30
      volumes:
        - name: model-storage
          persistentVolumeClaim:
            claimName: sglang-model-pvc
        - name: cache-volume
          emptyDir:
            sizeLimit: 10Gi
        - name: config-volume
          configMap:
            name: sglang-config
            defaultMode: 0755
EOF

5.3 观察 Pod 从创建到可用

kubectl get pods -n sglang -w

典型过程大致是:

sglang-deployment-7f948b868f-qmmbg   0/1   ContainerCreating   0   18s
sglang-deployment-7f948b868f-qmmbg   0/1   Running             0   65s
sglang-deployment-7f948b868f-qmmbg   1/1   Running             0   2m25s

5.4 看启动日志,别只盯着 Pod 状态

kubectl logs -n sglang deploy/sglang-deployment -f

课件里的关键日志大致像这样:

Starting SGLang server...
Model found at /models
Loading model from /models
[2026-01-23 11:52:09] INFO: Application startup complete.
[2026-01-23 11:52:09] INFO: Uvicorn running on http://0.0.0.0:30000
[2026-01-23 11:52:12] The server is fired up and ready to roll!

你至少要看到这几个信号,才算真的可用了:

  • Model found at /models
  • Application startup complete
  • Uvicorn running on http://0.0.0.0:30000
  • The server is fired up and ready to roll!

6. 给 SGLang 建 Service,并先做集群内验收

6.1 创建 ClusterIP Service

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: sglang-service
  namespace: sglang
  labels:
    app: sglang
spec:
  selector:
    app: sglang
  ports:
    - name: api
      port: 30000
      targetPort: 30000
  type: ClusterIP
EOF

这里的 nvidia.com/gpu: 2 是基于课件中的双 A10 环境。
如果你的节点只有 1 张 GPU,请把 requests/limits 都改成 1,否则 Pod 会一直处于不可调度状态。

查看结果:

kubectl get svc -n sglang

预期输出类似:

NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)       AGE
sglang-service   ClusterIP   10.99.171.72    <none>        30000/TCP     12s

6.2 先通过 Pod IP 验证 SGLang 是否真能服务

kubectl get pods -n sglang -o wide

课件里的 Pod IP 类似:

NAME                                READY   STATUS    RESTARTS   AGE   IP             NODE
sglang-deployment-7f948b868f-qmmbg  1/1     Running   0          11m   10.244.32.144  k8s-master01

然后直接测模型接口:

curl http://10.244.32.144:30000/v1/models

参考返回类似:

{
  "object": "list",
  "data": [
    {
      "id": "/models",
      "object": "model",
      "owned_by": "sglang",
      "max_model_len": 131072
    }
  ]
}

再测一个补全文本接口:

curl http://10.244.32.144:30000/v1/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek",
    "prompt": "Once upon a time",
    "max_tokens": 20
  }'

返回里只要能看到:

{
  "object": "text_completion",
  "choices": [
    {
      "text": "..."
    }
  ]
}

就说明 SGLang 后端已经不只是“进程活着”,而是真的可以接请求。

7. 接入 Open WebUI,让团队先在浏览器里用起来

SGLang 在 K8S 里跑起来之后,最实用的一步就是接 Open WebUI。
这样你就从“API 服务可用”进一步变成了“团队成员打开浏览器就能用”。

7.1 为 Open WebUI 准备数据 PVC

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: webui-data-pvc
  namespace: sglang
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: nfs-client
  resources:
    requests:
      storage: 10Gi
EOF

查看状态:

kubectl get pvc -n sglang

典型输出类似:

NAME              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
webui-data-pvc    Bound    pvc-8e5c3f04-97f2-41a7-8fcc-59dd0a007508   10Gi       RWX            nfs-client     12s

7.2 部署 Open WebUI

下面这份 YAML 基本沿用了课件配置,并保留了最关键的 SGLang 对接项:

cat <<'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: open-webui-deployment
  namespace: sglang
  labels:
    app: open-webui
spec:
  replicas: 1
  selector:
    matchLabels:
      app: open-webui
  template:
    metadata:
      labels:
        app: open-webui
    spec:
      containers:
        - name: open-webui
          image: dyrnq/open-webui:main
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
              name: webui
          env:
            - name: WEBUI_NAME
              value: "DeepSeek Chat (SGLang)"
            - name: WEBUI_SECRET_KEY
              value: "your-secret-key-please-change"
            - name: WEBUI_AUTH
              value: "false"
            - name: OPENAI_API_BASE_URL
              value: "http://sglang-service:30000/v1"
            - name: OPENAI_API_KEY
              value: "sk-no-key-required"
            - name: DEFAULT_MODEL
              value: "deepseek-qwen"
            - name: ENABLE_OLLAMA_API
              value: "false"
            - name: ENABLE_TEXT_GENERATION_WEBUI_API
              value: "false"
            - name: ENABLE_OPENAI_API
              value: "true"
            - name: DISABLE_RAG
              value: "true"
            - name: ENABLE_MODEL_DOWNLOAD
              value: "false"
            - name: HF_HUB_OFFLINE
              value: "1"
            - name: TRANSFORMERS_OFFLINE
              value: "1"
            - name: HF_DATASETS_OFFLINE
              value: "1"
            - name: HF_EVALUATE_OFFLINE
              value: "1"
            - name: HF_MODULES_CACHE
              value: "/tmp/huggingface"
            - name: WORKERS
              value: "1"
          resources:
            limits:
              memory: "2Gi"
              cpu: "1"
            requests:
              memory: "1Gi"
              cpu: "500m"
          volumeMounts:
            - name: webui-data
              mountPath: /app/backend/data
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 60
            periodSeconds: 30
          readinessProbe:
            httpGet:
              path: /
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
      volumes:
        - name: webui-data
          persistentVolumeClaim:
            claimName: webui-data-pvc
EOF

这里最关键的三项是:

  • OPENAI_API_BASE_URL=http://sglang-service:30000/v1
  • OPENAI_API_KEY=sk-no-key-required
  • DEFAULT_MODEL=deepseek-qwen

其中 DEFAULT_MODEL 最好和 SGLang 启动参数里的 --served-model-name 一致。

7.3 给 Open WebUI 建 Service

课件里先用了最容易验证的 NodePort:

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: open-webui-service
  namespace: sglang
spec:
  selector:
    app: open-webui
  ports:
    - name: http
      port: 8080
      targetPort: 8080
      nodePort: 30010
  type: NodePort
EOF

查看状态:

kubectl get svc -n sglang

你应该会看到类似:

NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)           AGE
open-webui-service   NodePort    10.96.88.52     <none>        8080:30010/TCP    18s
sglang-service       ClusterIP   10.99.171.72    <none>        30000/TCP         6m

这时就可以直接在浏览器里访问:

http://<任意节点IP>:30010

如果页面能打开,并且在对话界面里能正常调用模型,说明这条链路已经打通:

Browser -> Open WebUI -> sglang-service -> SGLang -> DeepSeek 模型

8. 服务发布:先用 NodePort 验证,再用 Gateway API 正式暴露

课件里 Open WebUI 的访问验证采用的是 NodePort,这很适合快速确认。

但如果你前面已经按第 4 篇和第 5 篇部署好了 Gateway API,那么这里更推荐把它整理成一个更像正式服务的入口。

8.1 确认 Gateway 已就绪

kubectl get gatewayclass
kubectl get gateway -A
kubectl get svc -n envoy-gateway-system

如果你沿用前面的 eg 示例,通常至少会看到:

NAME   CONTROLLER
eg     gateway.envoyproxy.io/gatewayclass-controller

以及类似的 Envoy Service:

envoy-default-eg-xxxxx   LoadBalancer   192.168.10.240   80:xxxxx/TCP

如果你的 Gateway 只允许同命名空间的 HTTPRoute 绑定,那么这里有两种处理方式:

  • 要么把 Gateway 也创建在 sglang 命名空间
  • 要么在 Gateway 的 listener 里把 allowedRoutes 放开到 All 或指定命名空间

8.2 为 Open WebUI 创建 HTTPRoute

cat <<'EOF' | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: open-webui-route
  namespace: sglang
spec:
  parentRefs:
    - name: eg
      namespace: default
  hostnames:
    - "sglang.kubemsb.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: open-webui-service
          port: 8080
EOF

查看路由状态:

kubectl get httproute -n sglang
kubectl describe httproute open-webui-route -n sglang

如果绑定成功,Parents 里应该能看到 Accepted=True 之类的状态。

8.3 从集群外验证访问

curl -H "Host: sglang.kubemsb.com" http://192.168.10.240/

如果返回的是 Open WebUI 的 HTML 或登录页内容,就说明外部入口已经通了。

这一步的价值在于:

  • NodePort 更适合快速验证
  • Gateway API 更适合后续统一入口、域名和路由治理

9. GPU 运维第一步:先把 Prometheus 全家桶装起来

如果你只是把服务跑起来,不做监控,那么后面排障会很痛苦。
尤其是 SGLang 这种复杂推理框架,很多问题既可能是服务层,也可能是 GPU 层。

9.1 安装 Helm 并准备 kube-prometheus-stack

课件里使用的是:

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

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

确认 Helm 正常:

version.BuildInfo{Version:"v4.0.5", ...}

9.2 用 values 文件安装监控栈

这里给一个比课件更紧凑、但仍然实用的版本:

cat <<'EOF' > prometheus-values.yaml
grafana:
  adminPassword: admin
  service:
    type: NodePort
    nodePort: 30001
alertmanager:
  enabled: true
prometheus:
  prometheusSpec:
    resources:
      requests:
        memory: 400Mi
      limits:
        memory: 800Mi
EOF

然后安装:

helm install kube-prometheus-stack ./kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  -f prometheus-values.yaml \
  --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false \
  --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false

查看 Pod:

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-operator-7f696fbb8d-zhztv         1/1   Running   0   53m
prometheus-kube-prometheus-stack-prometheus-0           2/2   Running   0   53m

10. GPU 运维第二步:部署 DCGM Exporter,把 GPU 指标暴露出来

课件里这一步使用了 nvidia/dcgm-exporter:4.4.2-4.7.1-ubuntu22.04,并通过 DaemonSet 方式在 GPU 节点上跑起来。

10.1 部署 DCGM Exporter

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nvidia-dcgm-exporter
  namespace: monitoring
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nvidia-dcgm-exporter-config
  namespace: monitoring
data:
  dcp-metrics-included.csv: |
    DCGM_FI_DEV_GPU_UTIL, gauge, GPU utilization (in %).
    DCGM_FI_DEV_MEM_COPY_UTIL, gauge, Memory utilization (in %).
    DCGM_FI_DEV_FB_USED, gauge, GPU Frame Buffer memory used (in MiB).
    DCGM_FI_DEV_FB_FREE, gauge, GPU Frame Buffer memory free (in MiB).
    DCGM_FI_DEV_POWER_USAGE, gauge, GPU power usage (in W).
    DCGM_FI_DEV_GPU_TEMP, gauge, GPU temperature (in C).
---
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
          securityContext:
            runAsNonRoot: false
            runAsUser: 0
            capabilities:
              add: ["SYS_ADMIN"]
          env:
            - name: NVIDIA_DRIVER_CAPABILITIES
              value: compute,utility
            - name: NVIDIA_VISIBLE_DEVICES
              value: all
          volumeMounts:
            - name: config
              mountPath: /etc/dcgm-exporter
            - name: nvidia-sock
              mountPath: /var/run/nvidia
      volumes:
        - name: config
          configMap:
            name: nvidia-dcgm-exporter-config
        - name: nvidia-sock
          hostPath:
            path: /var/run/nvidia
---
apiVersion: v1
kind: Service
metadata:
  name: nvidia-dcgm-exporter
  namespace: monitoring
  labels:
    app: nvidia-dcgm-exporter
spec:
  selector:
    app: nvidia-dcgm-exporter
  ports:
    - name: metrics
      port: 9400
      targetPort: 9400
EOF

10.2 检查 GPU 指标是否已经可见

kubectl get pods -n monitoring
kubectl get svc -n monitoring nvidia-dcgm-exporter
curl http://10.110.169.16:9400/metrics | head -30

课件里的参考输出像这样:

# HELP DCGM_FI_DEV_GPU_TEMP GPU temperature (in C).
# TYPE DCGM_FI_DEV_GPU_TEMP gauge
DCGM_FI_DEV_GPU_TEMP{gpu="0",modelName="NVIDIA A10",Hostname="k8s-master01"} 31
DCGM_FI_DEV_GPU_TEMP{gpu="1",modelName="NVIDIA A10",Hostname="k8s-master01"} 45

# HELP DCGM_FI_DEV_GPU_UTIL GPU utilization (in %).
# TYPE DCGM_FI_DEV_GPU_UTIL gauge
DCGM_FI_DEV_GPU_UTIL{gpu="0",modelName="NVIDIA A10",Hostname="k8s-master01"} 0
DCGM_FI_DEV_GPU_UTIL{gpu="1",modelName="NVIDIA A10",Hostname="k8s-master01"} 0

如果这里能拿到 DCGM_* 指标,说明 GPU 层监控已经通了。

11. 打开 SGLang 自身指标,再让 Prometheus 自动采集

SGLang 在 K8S 里真正有价值的监控,不只是 GPU 温度和利用率,还包括它自己暴露出来的服务指标。

11.1 更新 SGLang ConfigMap,开启 metrics

课件里的做法是在启动参数中加入 --enable-metrics

cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: sglang-config
  namespace: sglang
data:
  start.sh: |
    #!/bin/bash
    echo "Starting SGLang server with Prometheus metrics..."
    if [ -f "/models/model.safetensors" ]; then
      echo "Model found at /models"
    else
      echo "Error: Model file not found at /models"
      ls -la /models/
      exit 1
    fi
    python3 -m sglang.launch_server \
      --model-path /models \
      --host 0.0.0.0 \
      --port 30000 \
      --trust-remote-code \
      --served-model-name deepseek-qwen \
      --enable-metrics
  env.sh: |
    export CUDA_VISIBLE_DEVICES=0,1
    export HF_HOME=/cache
    export PYTHONUNBUFFERED=1
EOF

然后重启 Deployment:

kubectl rollout restart deployment sglang-deployment -n sglang
kubectl rollout status deployment sglang-deployment -n sglang

11.2 直接访问 /metrics

kubectl get pods -n sglang -o wide
curl http://10.244.32.152:30000/metrics | head -40

如果开启成功,你会看到类似:

# HELP sglang:num_running_reqs The number of running requests.
# TYPE sglang:num_running_reqs gauge
sglang:num_running_reqs{model_name="deepseek-qwen"} 0.0

# HELP sglang:token_usage The token usage.
# TYPE sglang:token_usage gauge
sglang:token_usage{model_name="deepseek-qwen"} 0.0

# HELP sglang:num_queue_reqs The number of requests in the waiting queue.
# TYPE sglang:num_queue_reqs gauge
sglang:num_queue_reqs{model_name="deepseek-qwen"} 0.0

11.3 创建 ServiceMonitor

这里最实用的方式,就是让 Prometheus 自动发现 SGLang 和 GPU 服务。

cat <<'EOF' | kubectl apply -f -
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: sglang-monitor
  namespace: monitoring
  labels:
    release: kube-prometheus-stack
    app: sglang
spec:
  selector:
    matchLabels:
      app: sglang
  namespaceSelector:
    matchNames:
      - sglang
  endpoints:
    - port: api
      path: /metrics
      interval: 15s
      scrapeTimeout: 10s
      honorLabels: true
      metricRelabelings:
        - sourceLabels: [__name__]
          regex: 'sglang:(.+)'
          targetLabel: __name__
          replacement: 'sglang_$1'
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: nvidia-gpu-monitor
  namespace: monitoring
  labels:
    release: kube-prometheus-stack
spec:
  selector:
    matchLabels:
      app: nvidia-dcgm-exporter
  namespaceSelector:
    matchNames:
      - monitoring
  endpoints:
    - port: metrics
      interval: 15s
      honorLabels: true
EOF

注意这里做了一件很重要的事:

  • 把原始 sglang:* 指标名转换成 sglang_*

这样后面 Prometheus 查询和告警规则会更顺手。

11.4 验证指标已经进入 Prometheus

kubectl get servicemonitor -n monitoring

kubectl exec -n monitoring prometheus-kube-prometheus-stack-prometheus-0 -- \
  wget -q -O- 'http://localhost:9090/api/v1/query?query=sglang_prompt_tokens_total' | python3 -m json.tool

kubectl exec -n monitoring prometheus-kube-prometheus-stack-prometheus-0 -- \
  wget -q -O- 'http://localhost:9090/api/v1/query?query=DCGM_FI_DEV_GPU_TEMP' | python3 -m json.tool

只要返回里能看到 status: success 和结果向量,说明自动采集已经生效。

12. 最后一层:为 SGLang 和 GPU 写告警规则

监控能看见还不够,真正能帮运维减负的是告警。

12.1 创建 SGLang 告警规则

cat <<'EOF' | kubectl apply -f -
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: sglang-alerts
  namespace: monitoring
  labels:
    release: kube-prometheus-stack
    app: sglang
spec:
  groups:
    - name: sglang.rules
      rules:
        - alert: SGLangServiceDown
          expr: up{job=~".*sglang.*"} == 0
          for: 1m
          labels:
            severity: critical
          annotations:
            summary: "SGLang 服务不可用"
            description: "SGLang 服务已经超过 1 分钟无法访问"
        - alert: SGLangHighLatency
          expr: histogram_quantile(0.95, rate(sglang_e2e_request_latency_seconds_bucket[5m])) > 5
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "SGLang 请求延迟过高"
            description: "SGLang 服务 P95 请求延迟超过 5 秒"
        - alert: SGLangHighQueueLength
          expr: sglang_num_queue_reqs > 10
          for: 2m
          labels:
            severity: warning
          annotations:
            summary: "SGLang 请求队列过长"
            description: "SGLang 等待队列中的请求数量超过 10"
EOF

12.2 创建 GPU 告警规则

cat <<'EOF' | kubectl apply -f -
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: gpu-alerts
  namespace: monitoring
  labels:
    release: kube-prometheus-stack
    app: kube-prometheus-stack
spec:
  groups:
    - name: gpu-alerts
      interval: 30s
      rules:
        - alert: GPUTemperatureHigh
          expr: DCGM_FI_DEV_GPU_TEMP > 80
          for: 3m
          labels:
            severity: critical
          annotations:
            summary: "GPU 温度过高"
            description: "GPU 温度已经超过 80°C"
        - alert: GPUUtilizationHigh
          expr: DCGM_FI_DEV_GPU_UTIL > 90
          for: 3m
          labels:
            severity: warning
          annotations:
            summary: "GPU 使用率过高"
            description: "GPU 使用率超过 90%"
        - alert: GPUMemoryUsageHigh
          expr: (DCGM_FI_DEV_FB_USED / (DCGM_FI_DEV_FB_USED + DCGM_FI_DEV_FB_FREE)) * 100 > 90
          for: 3m
          labels:
            severity: warning
          annotations:
            summary: "GPU 显存使用率过高"
            description: "GPU 显存使用率超过 90%"
EOF

12.3 验证规则已经被 Prometheus 加载

PROM_POD=prometheus-kube-prometheus-stack-prometheus-0

kubectl exec -n monitoring $PROM_POD -c prometheus -- ls -la /etc/prometheus/rules/
kubectl exec -n monitoring $PROM_POD -c prometheus -- grep -r "gpu-alerts\\|sglang-alerts" /etc/prometheus/

如果能查到相应规则文件,就说明 PrometheusRule 已经被接收。

13. 最常见的几类问题,通常卡在这些地方

13.1 Pod 一直起不来

优先检查:

  • kubectl describe pod -n sglang <pod-name>
  • PVC 是否 Bound
  • nodeSelector 是否把 Pod 限死到一个没有可用 GPU 的节点
  • nvidia.com/gpu 的请求数量是否超过节点可分配值

13.2 Pod Running,但 /v1/models 访问失败

优先检查:

  • kubectl logs -n sglang deploy/sglang-deployment
  • 是否看到 Application startup complete
  • Serviceselector 是否和 Pod label 一致
  • 探针是否过早失败,导致 Pod 反复重启

13.3 Open WebUI 页面能打开,但对话失败

优先检查:

  • OPENAI_API_BASE_URL 是否写成了 http://sglang-service:30000/v1
  • DEFAULT_MODEL 是否与 --served-model-name 一致
  • 先在 Pod 内或集群内确认 curl http://sglang-service:30000/v1/models 正常

13.4 GPU 指标有了,但 SGLang 指标没有

通常是下面几个原因:

  • 启动参数里没有 --enable-metrics
  • ServiceMonitor 里端口名、路径或 selector 不匹配
  • Prometheus selector 没有匹配到你的 ServiceMonitor

13.5 告警规则创建了,但一直不生效

优先检查:

  • PrometheusRule 是否带了 Prometheus 选择器需要的 label
  • 查询表达式里用的是不是转换后的 sglang_* 指标名
  • Prometheus 是否真的已经采集到对应时间序列

14. 结语:到这里,SGLang 才真正变成“可用服务”

把这一篇走完之后,你会很明显地感受到:

SGLang 上 K8S 的价值,不只是“集群里多起一个 Pod”,而是把复杂推理框架真正变成一套可以被团队使用、被浏览器访问、被 Prometheus 看见、被告警系统接管的服务能力。

到这里为止,这一篇解决的是:

  • 如何把模型文件挂进 SGLang Pod
  • 如何让 SGLang 在 K8S 里稳定启动
  • 如何接上 Open WebUI 并给团队提供界面入口
  • 如何用 NodePort 和 Gateway API 对外发布
  • 如何把 GPU 指标和 SGLang 指标纳入统一监控
  • 如何通过 PrometheusRule 把常见异常提前暴露出来

如果把整个系列串起来看,你现在已经有了一条比较完整的大模型私有化部署路线:

  • 从 GPU 和容器运行时准备开始
  • 到原生 K8S 集群和底座组件搭建
  • 再到 Ollama、vLLM、SGLang 的单机和集群实践

再往后最自然的收尾文章,就是把两条主路线做一次真正的对照:

vLLM 和 SGLang 到底怎么选,分别更适合哪些团队、哪些负载和哪些应用形态。

0

评论区