主题
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 WebUI | http://117.50.188.237/ | Web 聊天界面 |
| vLLM Qwen2.5-0.5B API | http://117.50.188.237/api/qwen25/v1 | API 接口 (3090) |
| vLLM Qwen3-0.6B API | http://117.50.188.237/api/qwen3/v1 | API 接口 (3090) |
注意: 云厂商 NAT 仅放行 22/80 端口,NodePort 不可从外部直接访问,需通过 traefik Ingress 代理。
vLLM 模型部署总览(7 个 GPU 实例)
| 部署名 | 模型 | 节点 | GPU | vGPU 显存 | Service 端口 |
|---|---|---|---|---|---|
| vllm-qwen25-05b | Qwen2.5-0.5B | 10-60-18-8 (3090) | 6GB | :8001 | |
| vllm-qwen3-06b | Qwen3-0.6B | 10-60-18-8 (3090) | 6GB | :8002 | |
| vllm-qwen25-05b-3080ti | Qwen2.5-0.5B | 10-60-205-41 (3080Ti) | 6GB | :8003 | |
| vllm-qwen3-06b-3080ti | Qwen3-0.6B | 10-60-205-41 (3080Ti) | 6GB | :8004 | |
| vllm-qwen3-06b-2080 | Qwen3-0.6B | 10-60-10-196 (2080) | 4GB | :8006 | |
| vllm-qwen25-05b-3090-2 | Qwen2.5-0.5B | 10-60-18-8 (3090) | 6GB | :8007 | |
| vllm-qwen3-06b-3090-2 | Qwen3-0.6B | 10-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
EOF2.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
done2.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.yaml2.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 calico2.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.2 | 117.50.188.237:30000/rancher/mirrored-pause:3.10.2 |
| rancher/mirrored-coredns-coredns:1.14.6 | 117.50.188.237:30000/rancher/mirrored-coredns-coredns:1.14.6 |
| rancher/mirrored-metrics-server:v0.9.0 | 117.50.188.237:30000/rancher/mirrored-metrics-server:v0.9.0 |
| rancher/local-path-provisioner:v0.0.36 | 117.50.188.237:30000/rancher/local-path-provisioner:v0.0.36 |
| rancher/klipper-helm:v0.13.3-build20260727 | 117.50.188.237:30000/rancher/klipper-helm:v0.13.3-build20260727 |
| rancher/klipper-lb:v0.4.17 | 117.50.188.237:30000/rancher/klipper-lb:v0.4.17 |
| rancher/mirrored-library-traefik:3.7.8 | 117.50.188.237:30000/rancher/mirrored-library-traefik:3.7.8 |
4. vLLM 和 Open WebUI 镜像(已在 Registry 中)
| 镜像 | Registry 路径 |
|---|---|
| vllm/vllm-openai:v0.8.4 | 117.50.188.237:30000/vllm/vllm-openai:v0.8.4 |
| open-webui/open-webui:main | 117.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 -ra5.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 /mnt5.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-modelscopebash
k3s kubectl apply -f nfs-pv-pvc.yaml
k3s kubectl get pv,pvc nfs-modelscope5.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.0 | 117.50.188.237:30000/projecthami/hami:v2.9.0 |
| google_containers/kube-scheduler:v1.36.3 | 117.50.188.237:30000/google_containers/kube-scheduler:v1.36.3 |
| liangjw/kube-webhook-certgen:v1.1.1 | 117.50.188.237:30000/liangjw/kube-webhook-certgen:v1.1.1 |
| jettech/kube-webhook-certgen:v1.5.2 | 117.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.yaml6.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 float32 | — | CPU 不支持 bfloat16,需手动指定 |
--swap-space 1 | 1GB | 默认 4GB 会 OOM,4GB 节点改为 1 |
--max-num-seqs 4 | 4 | 限制并发序列数 |
--enforce-eager | — | 禁用 CUDA graph(CPU 无意义) |
--disable-async-output-proc | — | 减少内存占用(回退 V0 引擎) |
VLLM_CPU_KVCACHE_SPACE=1 | 1GB | 限制 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: NodePortbash
sudo mkdir -p /opt/open-webui-data
sudo chmod 777 /opt/open-webui-data
k3s kubectl apply -f /tmp/vllm-deploy.yaml8. 部署 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: NodePortbash
k3s kubectl apply -f /tmp/openwebui-deploy.yaml9. 配置 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: 8080bash
k3s kubectl apply -f /tmp/ingress.yaml10. 验证
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_FOUND | Ubuntu 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 Loaded | monitor 容器非特权且无 /dev 访问 | Patch DaemonSet: 设置 privileged: true + 挂载 /dev |
| HAMi 调度: node unregistered | 强制删除 Pod 后 HAMi 节点注册过期 | 重启 HAMi device-plugin Pod 触发重新注册 |
| vLLM: Bfloat16 not supported | RTX 2080 计算能力 7.5 < 8.0 | 添加 --dtype half 参数 |
| exec: "python" not found | vLLM 旧版镜像用 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 ImagePullBackOff | klipper-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 table | image 字段用了简单字符串而非结构化对象 | 改用 {registry, repository, tag, pullPolicy, pullSecrets} 结构 |
| HAMi device-plugin CrashLoopBackOff | GPU 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 range | OPENAI_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 Pending | HAMi GPU 资源未释放 | 手动 kubectl scale rs <old-rs> --replicas=0 释放旧 Pod |