Skip to content

RTX 2080 虚拟机 - vLLM + Open WebUI 部署 (k3s + HAMi vGPU)

集群信息

Server (control-plane)

  • 公网 IP: 117.50.188.237 | 内网 IP: 10.60.10.196
  • SSH: ssh ubuntu@117.50.188.237 (密码: xxx
  • GPU: RTX 2080 8GB
  • 角色: control-plane + worker
  • k3s: v1.36.3+k3s1

Worker 1 (117.50.215.183)

  • 公网 IP: 117.50.215.183 | 内网 IP: 10.60.38.105
  • SSH: ssh ubuntu@117.50.215.183 (密码: xxx
  • 角色: worker (agent)
  • k3s: v1.36.3+k3s1
  • 磁盘: 100G

Worker 2 (117.50.190.159)

  • 公网 IP: 117.50.190.159 | 内网 IP: 10.60.156.30
  • SSH: ssh ubuntu@117.50.190.159 (密码: xxx
  • 角色: worker (agent)
  • k3s: v1.36.3+k3s1
  • 磁盘: 100G

Worker 3 - GPU Node (117.50.213.129)

  • 公网 IP: 117.50.213.129 | 内网 IP: 10.60.18.8
  • SSH: ssh ubuntu@117.50.213.129 (密码: xxx
  • GPU: RTX 3090 24GB × 1(HAMi vGPU: 4 × 6GB)
  • CPU: 32 核 | RAM: 125GB
  • 角色: worker (agent) + GPU 推理节点

Worker 4 - GPU Node (117.50.185.68)

  • 公网 IP: 117.50.185.68 | 内网 IP: 10.60.205.41
  • SSH: ssh ubuntu@117.50.185.68 (密码: xxx
  • GPU: RTX 3080 Ti 12GB × 1(HAMi vGPU: 2 × 6GB)
  • 角色: worker (agent) + GPU 推理节点

通用

  • OS: Ubuntu 22.04
  • CNI: Calico v3.28.0 (VXLAN, 非 BGP)
  • HAMi: v2.10.0 (per-node vGPU 分片: 3090=4×6GB, 3080Ti=2×6GB, 2080=2×4GB)

访问地址(通过 Traefik Ingress 走 80 端口)

服务URL用途
Open WebUIhttp://117.50.188.237/Web 聊天界面
vLLM Qwen2.5-0.5B APIhttp://117.50.188.237/api/qwen25/v1API 接口 (3090)
vLLM Qwen3-0.6B APIhttp://117.50.188.237/api/qwen3/v1API 接口 (3090)

注意: 云厂商 NAT 仅放行 22/80 端口,NodePort 不可从外部直接访问,需通过 traefik Ingress 代理。

vLLM 模型部署总览(7 个 GPU 实例)

部署名模型节点GPUvGPU 显存Service 端口
vllm-qwen25-05bQwen2.5-0.5B10-60-18-8 (3090)6GB:8001
vllm-qwen3-06bQwen3-0.6B10-60-18-8 (3090)6GB:8002
vllm-qwen25-05b-3080tiQwen2.5-0.5B10-60-205-41 (3080Ti)6GB:8003
vllm-qwen3-06b-3080tiQwen3-0.6B10-60-205-41 (3080Ti)6GB:8004
vllm-qwen3-06b-2080Qwen3-0.6B10-60-10-196 (2080)4GB:8006
vllm-qwen25-05b-3090-2Qwen2.5-0.5B10-60-18-8 (3090)6GB:8007
vllm-qwen3-06b-3090-2Qwen3-0.6B10-60-18-8 (3090)6GB:8008

每个 vLLM Pod 使用 schedulerName: hami-scheduler + nvidia.com/gpumem 请求绝对显存值。 RTX 2080 (4GB vGPU) 只部署 Qwen3-0.6B,因为 Qwen2.5-0.5B 在 4GB 下 OOM。


1. 安装 k3s(国内镜像)

bash
curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn sh -

2. 配置 k3s

2.1 kubeconfig 权限(安装前写入,k3s 重启后仍生效)

bash
sudo mkdir -p /etc/rancher/k3s
sudo tee /etc/rancher/k3s/config.yaml > /dev/null << 'EOF'
write-kubeconfig-mode: "0644"
flannel-backend: "none"
disable-network-policy: true
EOF

2.2 安装 Calico CNI(使用本地 Registry)

2.2.1 拉取并推送 Calico 镜像到本地仓库

bash
CALICO_VERSION="v3.28.0"
REGISTRY="117.50.188.237:30000"
HUAWEI_MIRROR="swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io"

# 从华为云拉取 Calico 镜像
for img in cni node kube-controllers pod2daemon-flexvol csi node-driver-registrar; do
  docker pull $HUAWEI_MIRROR/calico/$img:$CALICO_VERSION
done

# 打标签并推送到本地仓库
for img in cni node kube-controllers pod2daemon-flexvol csi node-driver-registrar; do
  SRC="$HUAWEI_MIRROR/calico/$img:$CALICO_VERSION"
  DST="$REGISTRY/calico/$img:$CALICO_VERSION"
  docker tag $SRC $DST
  docker push $DST
done

2.2.2 下载并修改 Calico manifest

bash
# 下载 Calico manifest
curl -sSL https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/calico.yaml -o calico.yaml

# 替换镜像地址为本地 Registry
sed -i "s|docker.io/calico/|117.50.188.237:30000/calico/|g" calico.yaml

# 验证替换结果
grep -E "image:.*calico" calico.yaml

2.2.3 部署 Calico

bash
# 应用 Calico
k3s kubectl apply -f calico.yaml

# 等待 Calico Pod 就绪
k3s kubectl wait --for=condition=Ready pods -l k8s-app=calico-node -n kube-system --timeout=120s
k3s kubectl wait --for=condition=Ready pods -l k8s-app=calico-kube-controllers -n kube-system --timeout=120s

# 验证 Calico 状态
k3s kubectl get pods -n kube-system | grep calico

2.2.4 验证网络

bash
# 检查 Calico 网络接口
ip addr show | grep -E "cali|tunl|vxlan"

# 测试 Pod 网络连通性
k3s kubectl run test-pod --image=busybox --restart=Never --rm -it -- sh -c "ping -c 2 8.8.8.8"

2.3 容器镜像加速(本地 Registry 优先)

bash
sudo tee /etc/rancher/k3s/registries.yaml > /dev/null << 'EOF'
mirrors:
  docker.io:
    endpoint:
      - "http://117.50.188.237:30000"
      - "https://registry.cn-hangzhou.aliyuncs.com"
  registry.k8s.io:
    endpoint:
      - "http://117.50.188.237:30000"
      - "https://registry.cn-hangzhou.aliyuncs.com"
  gcr.io:
    endpoint:
      - "http://117.50.188.237:30000"
      - "https://registry.cn-hangzhou.aliyuncs.com"
  ghcr.io:
    endpoint:
      - "http://117.50.188.237:30000"
      - "https://registry.cn-hangzhou.aliyuncs.com"
  quay.io:
    endpoint:
      - "http://117.50.188.237:30000"
      - "https://registry.cn-hangzhou.aliyuncs.com"
  registry.cn-hangzhou.aliyuncs.com:
    endpoint:
      - "http://117.50.188.237:30000"
  "117.50.188.237:30000":
    endpoint:
      - "http://117.50.188.237:30000"
EOF
sudo systemctl restart k3s

说明:所有镜像已预推送到本地 Registry (117.50.188.237:30000),k3s 拉取时优先走本地,秒级完成。

3. 系统镜像(已在 Registry 中)

以下镜像已预推送,k3s 通过 mirror 自动从本地 Registry 拉取,无需手动操作:

镜像Registry 路径
rancher/mirrored-pause:3.10.2117.50.188.237:30000/rancher/mirrored-pause:3.10.2
rancher/mirrored-coredns-coredns:1.14.6117.50.188.237:30000/rancher/mirrored-coredns-coredns:1.14.6
rancher/mirrored-metrics-server:v0.9.0117.50.188.237:30000/rancher/mirrored-metrics-server:v0.9.0
rancher/local-path-provisioner:v0.0.36117.50.188.237:30000/rancher/local-path-provisioner:v0.0.36
rancher/klipper-helm:v0.13.3-build20260727117.50.188.237:30000/rancher/klipper-helm:v0.13.3-build20260727
rancher/klipper-lb:v0.4.17117.50.188.237:30000/rancher/klipper-lb:v0.4.17
rancher/mirrored-library-traefik:3.7.8117.50.188.237:30000/rancher/mirrored-library-traefik:3.7.8

4. vLLM 和 Open WebUI 镜像(已在 Registry 中)

镜像Registry 路径
vllm/vllm-openai:v0.8.4117.50.188.237:30000/vllm/vllm-openai:v0.8.4
open-webui/open-webui:main117.50.188.237:30000/open-webui/open-webui:main

5. 模型文件(NFS 共享)

5.1 NFS Server(117.50.188.237)

bash
# 安装 NFS Server
sudo apt-get install -y nfs-kernel-server

# 配置导出
sudo tee /etc/exports > /dev/null << 'EOF'
/model/ModelScope *(rw,sync,no_subtree_check,no_root_squash)
EOF

# 启动
sudo systemctl enable --now nfs-kernel-server
sudo exportfs -ra

5.2 NFS Client(Worker 节点)

bash
# 安装 nfs-common
sudo apt-get install -y nfs-common

# 验证挂载(使用内网 IP)
sudo mount -t nfs 10.60.10.196:/model/ModelScope /mnt
ls /mnt/
sudo umount /mnt

5.3 NFS PV/PVC

yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-modelscope
spec:
  capacity:
    storage: 200Gi
  accessModes:
    - ReadWriteMany
  nfs:
    server: 10.60.10.196
    path: /model/ModelScope
    readOnly: false
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs-modelscope
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-modelscope
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 200Gi
  storageClassName: nfs-modelscope
  volumeName: nfs-modelscope
bash
k3s kubectl apply -f nfs-pv-pvc.yaml
k3s kubectl get pv,pvc nfs-modelscope

5.4 模型目录结构

/model/ModelScope/Qwen/       ← 所有节点通过 NFS PVC 挂载到 /models
├── Qwen2.5-0.5B-Instruct/
└── Qwen3-0.6B/

vLLM Pod 挂载路径: claimName: nfs-modelscope → 容器内 /models

6. 安装 HAMi vGPU(2 × 4GB 分片)

RTX 2080 8GB 通过 HAMi 切分为 2 个 4GB vGPU,每个 vLLM 实例独占 1 个 vGPU。

6.1 HAMi 镜像(已在 Registry 中)

以下镜像已预推送,无需手动操作:

镜像Registry 路径
projecthami/hami:v2.9.0117.50.188.237:30000/projecthami/hami:v2.9.0
google_containers/kube-scheduler:v1.36.3117.50.188.237:30000/google_containers/kube-scheduler:v1.36.3
liangjw/kube-webhook-certgen:v1.1.1117.50.188.237:30000/liangjw/kube-webhook-certgen:v1.1.1
jettech/kube-webhook-certgen:v1.5.2117.50.188.237:30000/jettech/kube-webhook-certgen:v1.5.2

6.2 标记节点 & 安装 HAMi

bash
# 标记 GPU 节点
k3s kubectl label node 10-60-10-196 gpu=on --overwrite

# Helm 安装 HAMi
helm repo add hami https://project-hami.github.io/HAMi/
helm repo update hami

创建 /tmp/hami-values.yaml(注意 image 使用结构化格式,不能用简单字符串):

yaml
global:
  imageTag: "v2.9.0"
  gpuHookPath: /usr/local

devicePlugin:
  deviceSplitCount: 2      # 将 8GB GPU 切分为 2 个 vGPU
  deviceMemoryScaling: 1
  deviceCoreScaling: 1
  nvidiaNodeSelector:
    gpu: "on"
  nvidiaDriverRoot: "/"    # Ubuntu apt 安装驱动,根目录即驱动根
  image:
    registry: "docker.io"
    repository: "projecthami/hami"
    tag: "v2.9.0"
    pullPolicy: IfNotPresent
    pullSecrets: []

scheduler:
  nodeName: "10-60-10-196"
  kubeScheduler:
    image:
      registry: "117.50.188.237:30000"
      repository: "google_containers/kube-scheduler"
      tag: "v1.36.3"
      pullPolicy: IfNotPresent
      pullSecrets: []
  extender:
    image:
      registry: "117.50.188.237:30000"
      repository: "projecthami/hami"
      tag: "v2.9.0"
      pullPolicy: IfNotPresent
      pullSecrets: []
bash
helm install hami hami/hami \
  -n hami-system --create-namespace -f /tmp/hami-values.yaml

6.3 修补 HAMi DaemonSet(Ubuntu apt 驱动兼容)

Ubuntu apt 安装的 NVIDIA 驱动库在 /usr/lib/x86_64-linux-gnu/,HAMi monitor 容器默认无法访问,需 patch:

bash
k3s kubectl patch ds hami-device-plugin -n hami-system --type=json -p='[
  {"op":"add","path":"/spec/template/spec/containers/1/env/-","value":{"name":"LD_LIBRARY_PATH","value":"/driver-root/usr/lib/x86_64-linux-gnu:/driver-root/usr/lib32:/usr/local/vgpu"}},
  {"op":"add","path":"/spec/template/spec/containers/1/volumeMounts/-","value":{"name":"driver-root","mountPath":"/driver-root","readOnly":true}},
  {"op":"replace","path":"/spec/template/spec/containers/1/securityContext","value":{"privileged":true,"allowPrivilegeEscalation":true}},
  {"op":"add","path":"/spec/template/spec/containers/1/volumeMounts/-","value":{"name":"dev","mountPath":"/dev"}},
  {"op":"add","path":"/spec/template/spec/volumes/-","value":{"name":"dev","hostPath":{"path":"/dev"}}}
]'

6.4 验证 HAMi

bash
k3s kubectl get pods -n hami-system
# 应显示 hami-device-plugin 2/2 Running, hami-scheduler 2/2 Running

k3s kubectl get node -o jsonpath='{.items[0].status.allocatable}' | python3 -m json.tool | grep nvidia
# "nvidia.com/gpu": "2"

7. 部署 vLLM

7.0 CPU 模式(GPU 离线时的临时方案)

场景: GPU 被云厂商 detach 后,vLLM 只能跑 CPU 推理。需要修补 vLLM 0.8.4 的平台检测 bug。

7.0.1 vLLM 0.8.4 CPU 平台补丁

vLLM 0.8.4 的 get_vllm_version() 调用 importlib.metadata.version("vllm"),Docker 安装的包元数据返回 "unspecified",导致 CPU 平台无法识别并抛出 UnspecifiedPlatform

修复方案: 在容器启动前注入 .pth 文件,通过 Python import hook 拦截 importlib.metadata.version 调用,将版本号追加 +cpu

bash
# 创建补丁脚本
cat << 'PATCHEOF' | sudo tee /tmp/fix_cpu.pth
import importlib.metadata as _m; _m.version = (lambda _o: (lambda n, _f=_o, _c=_o("vllm"): (_c + "+cpu") if n == "vllm" else _f(n)))(_m.version)
PATCHEOF

# 通过 ConfigMap 注入容器
k3s kubectl create configmap vllm-cpu-patch \
  --from-file=fix_cpu.pth=/tmp/fix_cpu.pth -o yaml --dry-run=client \
  | k3s kubectl apply -f -

7.0.2 CPU 模式内存调优(4GB 节点)

参数说明
--device cpu强制 CPU 推理
--dtype float32CPU 不支持 bfloat16,需手动指定
--swap-space 11GB默认 4GB 会 OOM,4GB 节点改为 1
--max-num-seqs 44限制并发序列数
--enforce-eager禁用 CUDA graph(CPU 无意义)
--disable-async-output-proc减少内存占用(回退 V0 引擎)
VLLM_CPU_KVCACHE_SPACE=11GB限制 KV cache 内存

7.0.3 CPU 模式 Deployment(去除 GPU resources)

yaml
# /tmp/vllm-deploy-cpu.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-qwen25-05b
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vllm-qwen25-05b
  template:
    metadata:
      labels:
        app: vllm-qwen25-05b
    spec:
      tolerations:
      - key: "nvidia.com/gpu"
        operator: "Exists"
        effect: "NoSchedule"
      - key: "node.kubernetes.io/unschedulable"
        operator: "Exists"
        effect: "NoSchedule"
      containers:
      - name: vllm
        image: vllm/vllm-openai:latest
        command: ["/bin/bash", "-c"]
        args:
        - |
          vllm serve /models/Qwen/Qwen2.5-0.5B-Instruct \
            --served-model-name qwen2.5-0.5b \
            --port 8000 \
            --device cpu \
            --dtype float32 \
            --max-model-len 2048 \
            --enforce-eager \
            --max-num-seqs 4 \
            --disable-async-output-proc \
            --swap-space 1
        ports:
        - containerPort: 8000
          name: http
        volumeMounts:
        - name: nfs-storage
          mountPath: /models
        - name: cpu-patch
          mountPath: /usr/local/lib/python3.12/dist-packages/fix_cpu.pth
          subPath: fix_cpu.pth
          readOnly: true
        resources:
          requests:
            cpu: "2"
            memory: "3Gi"
          limits:
            cpu: "4"
            memory: "4Gi"
        env:
        - name: VLLM_CPU_KVCACHE_SPACE
          value: "1"
        - name: VLLM_LOGGING_LEVEL
          value: "INFO"
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 60
          periodSeconds: 10
          failureThreshold: 12
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 120
          periodSeconds: 30
          failureThreshold: 6
      volumes:
      - name: nfs-storage
        persistentVolumeClaim:
          claimName: nfs-pvc
      - name: cpu-patch
        configMap:
          name: vllm-cpu-patch
---
# Qwen3-0.6B 同理,修改 name/labels/model 路径
---
apiVersion: v1
kind: Service
metadata:
  name: vllm-qwen25-05b
spec:
  type: ClusterIP
  selector:
    app: vllm-qwen25-05b
  ports:
  - port: 8001
    targetPort: 8000
    protocol: TCP
    name: http

注意: 同时跑两个 vLLM float32 实例(每个 ~2GB 模型 + KV cache)在 4GB 节点上会 OOM。建议先只启一个验证稳定后再考虑第二个。

7.0.4 验证 CPU 模式

bash
# 1. 查看日志 — 确认 "Automatically detected platform cpu"
k3s kubectl logs deploy/vllm-qwen25-05b --tail=20 | grep -i "platform\|engine\|cpu"

# 2. 测试 /v1/models
k3s kubectl run api-test --rm -it --restart=Never --image=busybox \
  -- sh -c "wget -qO- http://vllm-qwen25-05b:8001/v1/models"

# 3. 对话测试
k3s kubectl run api-test --rm -it --restart=Never --image=busybox -- sh -c \
  "wget -qO- --post-data='{\"model\":\"qwen2.5-0.5b\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}],\"max_tokens\":50}' \
   --header='Content-Type: application/json' \
   http://vllm-qwen25-05b:8001/v1/chat/completions"

7.1 GPU 模式(HAMi vGPU,2 个实例)

场景: GPU 可用时使用,资源配置含 hami.io/vgpu: 4000

创建 /tmp/vllm-deploy.yaml

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-qwen25-05b
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vllm-qwen25-05b
  template:
    metadata:
      labels:
        app: vllm-qwen25-05b
    spec:
      schedulerName: hami-scheduler
      runtimeClassName: nvidia
      containers:
      - name: vllm
        image: 117.50.188.237:30000/vllm/vllm-openai:v0.8.4
        imagePullPolicy: IfNotPresent
        args:
        - --model
        - /model
        - --served-model-name
        - qwen2.5-0.5b
        - --port
        - "8000"
        - --gpu-memory-utilization
        - "0.85"
        - --max-model-len
        - "2048"
        - --enforce-eager
        - --dtype
        - half
        ports:
        - containerPort: 8000
        env:
        - name: HF_ENDPOINT
          value: "https://hf-mirror.com"
        - name: NVIDIA_VISIBLE_DEVICES
          value: "all"
        resources:
          limits:
            nvidia.com/gpu: "1"
            nvidia.com/gpumem: "4000"
        volumeMounts:
        - name: model
          mountPath: /model
          readOnly: true
      volumes:
      - name: model
        hostPath:
          path: /model/ModelScope/Qwen/Qwen2.5-0.5B-Instruct
---
apiVersion: v1
kind: Service
metadata:
  name: vllm-qwen25-05b
spec:
  selector:
    app: vllm-qwen25-05b
  ports:
  - port: 8001
    targetPort: 8000
    nodePort: 30001
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-qwen3-06b
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vllm-qwen3-06b
  template:
    metadata:
      labels:
        app: vllm-qwen3-06b
    spec:
      schedulerName: hami-scheduler
      runtimeClassName: nvidia
      containers:
      - name: vllm
        image: 117.50.188.237:30000/vllm/vllm-openai:v0.8.4
        imagePullPolicy: IfNotPresent
        args:
        - --model
        - /model
        - --served-model-name
        - qwen3-0.6b
        - --port
        - "8000"
        - --gpu-memory-utilization
        - "0.85"
        - --max-model-len
        - "2048"
        - --enforce-eager
        - --dtype
        - half
        ports:
        - containerPort: 8000
        env:
        - name: HF_ENDPOINT
          value: "https://hf-mirror.com"
        - name: NVIDIA_VISIBLE_DEVICES
          value: "all"
        resources:
          limits:
            nvidia.com/gpu: "1"
            nvidia.com/gpumem: "4000"
        volumeMounts:
        - name: model
          mountPath: /model
          readOnly: true
      volumes:
      - name: model
        hostPath:
          path: /model/ModelScope/Qwen/Qwen3-0.6B
---
apiVersion: v1
kind: Service
metadata:
  name: vllm-qwen3-06b
spec:
  selector:
    app: vllm-qwen3-06b
  ports:
  - port: 8002
    targetPort: 8000
    nodePort: 30002
  type: NodePort
bash
sudo mkdir -p /opt/open-webui-data
sudo chmod 777 /opt/open-webui-data
k3s kubectl apply -f /tmp/vllm-deploy.yaml

8. 部署 Open WebUI

创建 /tmp/openwebui-deploy.yaml

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: open-webui
spec:
  replicas: 1
  selector:
    matchLabels:
      app: open-webui
  template:
    metadata:
      labels:
        app: open-webui
    spec:
      containers:
      - name: open-webui
        image: 117.50.188.237:30000/open-webui/open-webui:main
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
          name: http
        env:
        - name: OPENAI_API_BASE_URLS
          value: "http://vllm-qwen25-05b:8001/v1;http://vllm-qwen3-06b:8002/v1"
        # 注意:当配置多个 OPENAI_API_BASE_URLS 时,必须使用 OPENAI_API_KEYS(复数)
        # 并用分号提供与 URL 数量一致的 key,否则会触发 index out of range 错误。
        # 若 vLLM 未启用 API Key 校验,填入任意占位符即可。
        - name: OPENAI_API_KEYS
          value: "not-needed;not-needed"
        - name: WEBUI_AUTH
          value: "false"
        - name: HF_ENDPOINT
          value: "https://hf-mirror.com"
        - name: TRANSFORMERS_OFFLINE
          value: "0"
        - name: ENABLE_RAG_WEB_SEARCH
          value: "false"
        - name: RAG_EMBEDDING_MODEL
          value: ""
        - name: CHUNK_SIZE
          value: "0"
        volumeMounts:
        - name: data
          mountPath: /app/backend/data
      volumes:
      - name: data
        hostPath:
          path: /opt/open-webui-data
          type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
  name: open-webui
spec:
  selector:
    app: open-webui
  ports:
  - port: 8080
    targetPort: 8080
    nodePort: 30080
  type: NodePort
bash
k3s kubectl apply -f /tmp/openwebui-deploy.yaml

9. 配置 Traefik Ingress(外网访问)

云厂商 NAT 仅放行 80 端口,需通过 Traefik IngressRoute 暴露服务:

创建 /tmp/ingress.yaml

yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: strip-qwen25
spec:
  stripPrefix:
    prefixes:
      - /api/qwen25
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: strip-qwen3
spec:
  stripPrefix:
    prefixes:
      - /api/qwen3
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: vllm-ingress
spec:
  entryPoints:
    - web
  routes:
  - match: PathPrefix(`/api/qwen25`)
    kind: Rule
    services:
    - name: vllm-qwen25-05b
      port: 8001
    middlewares:
    - name: strip-qwen25
  - match: PathPrefix(`/api/qwen3`)
    kind: Rule
    services:
    - name: vllm-qwen3-06b
      port: 8002
    middlewares:
    - name: strip-qwen3
  - match: PathPrefix(`/`)
    kind: Rule
    services:
    - name: open-webui
      port: 8080
bash
k3s kubectl apply -f /tmp/ingress.yaml

10. 验证

bash
# 检查 HAMi 状态
k3s kubectl get pods -n hami-system
k3s kubectl get node -o jsonpath='{.items[0].status.allocatable}' | python3 -m json.tool | grep nvidia
# "nvidia.com/gpu": "2"  (2 个 vGPU)

# 检查 Pod 状态(应全部 Running)
k3s kubectl get pods -A

# 检查 GPU 使用(约 4.5GB / 8GB,每个模型约 2.2GB)
nvidia-smi

# 内网测试 API
curl http://localhost:30001/v1/models
curl http://localhost:30002/v1/models
curl http://localhost:30001/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"qwen2.5-0.5b","messages":[{"role":"user","content":"你好"}]}'

# 外网测试
curl http://117.50.188.237/                                          # Open WebUI
curl http://117.50.188.237/api/qwen25/v1/models                     # Qwen2.5 API
curl http://117.50.188.237/api/qwen3/v1/models                      # Qwen3 API
curl http://117.50.188.237/api/qwen25/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"qwen2.5-0.5b","messages":[{"role":"user","content":"1+1=?"}]}'

# Open WebUI:浏览器打开 http://117.50.188.237/

11. 新增 Worker 节点

bash
# 1. 从 server 获取 join token
SERVER_NODE="ubuntu@117.50.188.237"
NEW_NODE="ubuntu@117.50.215.183"
SERVER_INTERNAL_IP="10.60.10.196"

TOKEN=$(ssh $SERVER_NODE "sudo cat /var/lib/rancher/k3s/server/node-token")

# 2. 拷贝 k3s 二进制(比从 GitHub 下载快得多)
ssh $SERVER_NODE "scp /usr/local/bin/k3s $NEW_NODE:/tmp/k3s"
ssh $NEW_NODE "sudo install -m 755 /tmp/k3s /usr/local/bin/k3s"

# 3. 写入配置
ssh $NEW_NODE "sudo mkdir -p /etc/rancher/k3s"
ssh $NEW_NODE "cat << EOF | sudo tee /etc/rancher/k3s/config.yaml
server: https://$SERVER_INTERNAL_IP:6443
token: $TOKEN
EOF"

# 4. 写入 registries.yaml(同 server)
ssh $NEW_NODE "cat << 'EOF' | sudo tee /etc/rancher/k3s/registries.yaml
mirrors:
  docker.io:
    endpoint:
      - \"http://117.50.188.237:30000\"
      - \"https://registry.cn-hangzhou.aliyuncs.com\"
  gcr.io:
    endpoint:
      - \"http://117.50.188.237:30000\"
  ghcr.io:
    endpoint:
      - \"http://117.50.188.237:30000\"
  quay.io:
    endpoint:
      - \"http://117.50.188.237:30000\"
  registry.cn-hangzhou.aliyuncs.com:
    endpoint:
      - \"http://117.50.188.237:30000\"
  \"117.50.188.237:30000\":
    endpoint:
      - \"http://117.50.188.237:30000\"
EOF"

# 5. 创建 systemd 服务
ssh $NEW_NODE "cat << 'EOF' | sudo tee /etc/systemd/system/k3s-agent.service
[Unit]
Description=Lightweight Kubernetes
Documentation=https://k3s.io
Wants=network-online.target
After=network-online.target

[Install]
WantedBy=multi-user.target

[Service]
Type=notify
KillMode=process
Delegate=yes
LimitNOFILE=1048576
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
TimeoutStartSec=0
Restart=always
RestartSec=5s
EnvironmentFile=-/etc/default/k3s-agent
EnvironmentFile=-/etc/sysconfig/k3s-agent
ExecStartPre=/bin/sh -xc \"! /usr/sbin/getent passwd k3s > /dev/null 2>&1 || ( /usr/sbin/usermod -L k3s && /bin/touch /var/lib/k3s/.k3s_locked )\"
ExecStartPre=/sbin/modprobe br_netfilter
ExecStartPre=/sbin/modprobe overlay
ExecStart=/usr/local/bin/k3s agent
EOF"

# 6. 启动服务
ssh $NEW_NODE "sudo systemctl daemon-reload && sudo systemctl enable --now k3s-agent"

# 7. 验证(在 server 上)
k3s kubectl get nodes

注意: 如果端口 10250 被占用,先 sudo pkill -9 k3s; sudo pkill -9 containerd 清理残留进程。


排错记录

问题原因解决
HAMi device-plugin CrashLoopBackOff: NVML ERROR_LIBRARY_NOT_FOUNDUbuntu apt 驱动库在 /usr/lib/x86_64-linux-gnu/,容器内找不到设置 nvidiaDriverRoot: "/" 并在 device-plugin 容器添加 LD_LIBRARY_PATH=/driver-root/usr/lib/x86_64-linux-gnu
HAMi monitor: Driver Not Loadedmonitor 容器非特权且无 /dev 访问Patch DaemonSet: 设置 privileged: true + 挂载 /dev
HAMi 调度: node unregistered强制删除 Pod 后 HAMi 节点注册过期重启 HAMi device-plugin Pod 触发重新注册
vLLM: Bfloat16 not supportedRTX 2080 计算能力 7.5 < 8.0添加 --dtype half 参数
exec: "python" not foundvLLM 旧版镜像用 command 而非 ENTRYPOINT改用 args(v0.8.4 已修复)
Qwen3 不识别vLLM v0.6.1 太旧不支持 qwen3 架构升级到 vLLM v0.8.4
系统镜像拉取超时 (rancher/*)Docker Hub 国内不通华为云 SWR 手动拉取 + ctr images tag
Open WebUI: huggingface 连不上需下载 sentence-transformers 嵌入模型设置 HF_ENDPOINT=https://hf-mirror.com + 禁用 RAG
svclb-traefik ImagePullBackOffklipper-lb 版本不匹配确认正确版本 v0.4.17 后重新拉取
NodePort 外网不可达云厂商 NAT 仅放行 22/80 端口通过 Traefik IngressRoute 走 80 端口暴露所有服务
新节点 k3s-agent 启动失败: bind: address already in use (10250)残留 k3s 进程占用端口sudo pkill -9 k3s; sudo pkill -9 containerd 后重启
Calico Init 卡在新节点registries.yaml 未在节点上配置配置 registries.yaml 后删除 Pod 重建
k3s 重启后 Pod sandbox 创建失败: registry.k8s.io/pause 超时registries.yaml 缺少 registry.k8s.io mirror添加 registry.k8s.io mirror 到 registries.yaml
HAMi Helm 安装失败: cannot overwrite table with non tableimage 字段用了简单字符串而非结构化对象改用 {registry, repository, tag, pullPolicy, pullSecrets} 结构
HAMi device-plugin CrashLoopBackOffGPU detached,nvidia 内核模块未加载GPU 重挂后自动恢复
NFS provisioner 镜像拉取超时registry.k8s.io 国内不通改用静态 NFS PV/PVC 方案(无需额外镜像)
vLLM 启动报 UnspecifiedPlatform / 无法检测 CPU 平台vLLM 0.8.4 的 get_vllm_version() 调用 importlib.metadata.version("vllm") 返回 "unspecified",CPU 分支无法匹配注入 .pth 补丁文件(详见 §7.0.1),拦截 importlib.metadata.version 并在版本号后追加 +cpu,通过 ConfigMap 挂载到 site-packages
vLLM CPU 模式 OOM / 节点 SSH 无响应4GB 节点同时跑两个 float32 vLLM 实例,每个模型 ~2GB + KV cache 超限单实例部署:--swap-space 1 --max-num-seqs 4 --enforce-eager --disable-async-output-proc + VLLM_CPU_KVCACHE_SPACE=1;先只跑一个实例验证稳定
vllm serve 报错 "provide the model as a positional argument"vLLM v0.8.4 起 serve 子命令禁止 --model 选项把模型路径作为 vllm serve /path/to/model 位置参数(第一个参数),不要写 --model
Open WebUI 启动报 OPENAI_API_KEYS index out of rangeOPENAI_API_BASE_URLS 有多个 URL,但只提供了一个 key(使用单数 OPENAI_API_KEY改用 OPENAI_API_KEYS(复数)+ 用分号分隔与 URL 数量一致的占位 key,例如 "not-needed;not-needed"
containerd 自定义模板导致 k3s 默认配置丢失config.toml.tmpl 完全覆盖默认配置删除 tmpl 文件,仅用 registries.yaml 管理镜像加速
vLLM 在 RTX 2080 4GB vGPU 下 OOM (CUDA out of memory)Qwen2.5-0.5B 模型 + KV cache 在 4GB 显存下不够用(需 3.91GB + 116MB)2080 只部署 Qwen3-0.6B,或降低 --max-model-len 512 --max-num-seqs 1
新节点 (10-60-18-8) 的 GPU 0 (00:03.0) nvidia-smi 不可见VM GPU 透传问题,PCI 设备存在但固件 N/A、UUID 全为 ?仅使用 GPU 1 (00:04.0),1 张可用 RTX 3090
HAMi device-plugin 重启后 node handshake 显示 "Deleted"HAMi scheduler 缓存过期同时重启 hami-scheduler + 对应节点的 device-plugin Pod
HAMi 全局 deviceSplitCount 无法满足异构 GPU 需求3090/3080Ti/2080 需要不同切分数使用 hami-device-plugin ConfigMap 的 nodeconfig 按节点设置不同 devicesplitcount
滚动更新时旧 Pod 占 GPU 槽位导致新 Pod PendingHAMi GPU 资源未释放手动 kubectl scale rs <old-rs> --replicas=0 释放旧 Pod