K8s部署大模型
优势:
- 资源调度与优化
- 多租户隔离与资源分配
- 弹性伸缩和负载均衡
- 高可用性和故障恢复
- 生态集成和工具链支持
K8s管理GPU的技术架构
k8s原生不支持调度GPU的,cpu、memory等资源是kubelet从主机上采集之后发给api-server的,默认是不涉及GPU的。 想要k8s支持GPU调度,需要借助工具:
- NVIDIA Device Plugin(把GPU信息发送给kubelet,kubelet发给api-server)
- NVIDIA Container Toolkit(让容器内的应用可以访问主机上的GPU)
- NVIDIA Driver Manager(安装和管理节点上的NVIDIA驱动)
- NVIDIA DCGM(采集GPU性能指标)
K8s GPU operator
由NVIDIA开发,自动管理所有用于配置GPU需要的工具。
1.1 GPU 环境准备
1.1.1 GPU 机器申请
若没有 GPU 机器,可以在公有云上临时申请。
选择 NVIDIA GPU 时,先检查计算能力(Compute Capability,CC)。vLLM 官方 GPU 安装要求明确要求:GPU: compute capability 7.5 or higher,即计算能力至少为 7.5。
比如:
- NVIDIA V100:CC 为
7.0,低于7.5,不满足当前官方 vLLM 的最低要求,不能直接用于本文的部署方案。参见 NVIDIA 旧款 GPU 计算能力表。 - NVIDIA A10:CC 为
8.6,满足≥ 7.5的最低要求。参见 NVIDIA GPU 计算能力表。
CC 描述 GPU 硬件架构能力,不是显存大小,也不是 CUDA Toolkit 版本号;升级驱动或 CUDA 不会提高这项硬件能力。CC 达标后,还需检查模型显存需求,以及所用 vLLM 镜像与驱动的兼容性。
rockylinux9.8,24GB显存,10MB公网固定带宽,100GB磁盘
1.1.2 驱动安装
下载地址: https://www.nvidia.com/en-us/drivers/unix/
先确认驱动版本支持目标 GPU,再选择对应的 CPU 架构(Linux x64/AMD64/EM64T 或 Linux aarch64)。本文实验使用 A10 和 595.104.02 驱动;不要在不同 GPU 型号上直接照搬驱动版本。
先检查现有环境。已经安装的依赖、已经禁用的 nouveau 可以跳过;dnf update 不是安装驱动的必要条件。若当前内核与默认启动内核不同,需要为重启后的内核也安装驱动模块,避免重启后 nvidia-smi 失效:
uname -r
grubby --default-kernel
rpm -q kernel-devel
lsmod | grep nouveau下载完成后,把安装包上传到 GPU 服务器,之后执行如下命令即可:
# 安装依赖包
dnf install gcc make kernel-devel wget -y
# 按需升级系统,非安装驱动的必要步骤;升级内核后应先重启再安装匹配的驱动
# dnf update -y
# reboot
# 禁用 Linux 驱动 nouveau
grep "blacklist nouveau" /etc/modprobe.d/blacklist.conf || echo "blacklist nouveau" >> /etc/modprobe.d/blacklist.conf
grep "options nouveau modeset=0" /etc/modprobe.d/blacklist.conf || echo "options nouveau modeset=0" >> /etc/modprobe.d/blacklist.conf
dracut --force /boot/initramfs-$(uname -r).img
dracut --force --omit-drivers nouveau
reboot
# 检查 nouveau 是否关闭
lsmod | grep nouveau # 没有结果说明已经关闭
# 安装驱动
chmod +x NVIDIA-Linux-*.run && ./NVIDIA-Linux-*.run -q -s --no-dkms --no-x-check
# 未先切换内核、且默认启动内核与当前内核不同时,补装重启后使用的模块
NEXT_KERNEL="$(basename "$(grubby --default-kernel)" | sed 's/^vmlinuz-//')"
if [ "${NEXT_KERNEL}" != "$(uname -r)" ]; then
dnf install "kernel-devel-${NEXT_KERNEL}" -y
bash NVIDIA-Linux-x86_64-595.104.02.run --silent --no-dkms --no-x-check \
--kernel-modules-only --kernel-name="${NEXT_KERNEL}"
fi
# 安装完成后,重启服务器
reboot驱动安装完成后,执行 nvidia-smi 命令检查是否安装成功:
nvidia-smi1.2 K8s 环境准备
1.2.1 K8s 集群部署
1.2.1.1 基础配置
基本环境配置:所有节点关闭防火墙、selinux、dnsmasq、swap。服务器配置如下:
systemctl disable --now firewalld
systemctl disable --now dnsmasq
setenforce 0
sed -i 's#SELINUX=enforcing#SELINUX=disabled#g' /etc/sysconfig/selinux
sed -i 's#SELINUX=enforcing#SELINUX=disabled#g' /etc/selinux/config
# 关闭 swap 分区
swapoff -a && sysctl -w vm.swappiness=0
sed -ri '/^[^#]*swap/s/^/#/' /etc/fstab配置 yum 源,安装必备工具:
sed -e 's|^mirrorlist=|#mirrorlist=|g' \
-e 's|^#baseurl=http://dl.rockylinux.org/$contentdir|baseurl=https://mirrors.aliyun.com/rockylinux|g' \
-i.bak \
/etc/yum.repos.d/*.repo
dnf makecache必备工具安装:
yum install wget jq psmisc vim net-tools telnet yum-utils device-mapper-persistent-data lvm2 git -y1.2.1.2 Runtime 安装
配置安装源:
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo安装 Containerd:
yum install containerd.io -y
cat <<EOF | sudo tee /etc/modules-load.d/containerd.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
cat <<EOF | sudo tee /etc/sysctl.d/99-kubernetes-cri.conf
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
sudo sysctl --system
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
sed -i 's#SystemdCgroup = false#SystemdCgroup = true#g' /etc/containerd/config.toml
sed -i 's#k8s.gcr.io/pause#registry.cn-hangzhou.aliyuncs.com/google_containers/pause#g' /etc/containerd/config.toml
sed -i 's#registry.gcr.io/pause#registry.cn-hangzhou.aliyuncs.com/google_containers/pause#g' /etc/containerd/config.toml
sed -i 's#registry.k8s.io/pause#registry.cn-hangzhou.aliyuncs.com/google_containers/pause#g' /etc/containerd/config.toml
# 启动 Containerd
systemctl daemon-reload
systemctl enable --now containerd1.2.1.3 Kubernetes 部署
配置安装源:
cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.36/rpm/
enabled=1
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.36/rpm/repodata/repomd.xml.key
EOF安装 Kubernetes 组件:
yum install kubeadm-1.36.0 kubelet-1.36.0 kubectl-1.36.0 -y
systemctl enable --now kubelet
kubeadm config images pull \
--image-repository registry.cn-hangzhou.aliyuncs.com/google_containers --kubernetes-version 1.36.0
# 初始化集群
NODE_IP="$(ip -4 -o addr show dev eth0 | awk '{print $4}' | cut -d/ -f1)"
test -n "${NODE_IP}"
kubeadm init --apiserver-advertise-address "${NODE_IP}" \
--image-repository registry.cn-hangzhou.aliyuncs.com/google_containers \
--cri-socket unix:///run/containerd/containerd.sock \
--kubernetes-version 1.36.0 \
--pod-network-cidr 192.168.0.0/16
# 加入其它节点
kubeadm join 192.168.181.134:6443 --token xxx \
--discovery-token-ca-cert-hash sha256:xxx配置 kubeconfig:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config安装 Addons:
yum install git -y
git clone https://gitee.com/dukuan/k8s-ha-install.git
cd k8s-ha-install/
git checkout manual-installation-v1.36.x
cd single/
# 本文只需要 CNI 和 metrics-server,不批量安装目录中的 Dashboard 等额外组件。
sed -i 's#quay.io/calico/#m.daocloud.io/quay.io/calico/#g' calico.yaml
kubectl apply --server-side -f calico.yaml
kubectl apply --server-side -f comp.yaml镜像代理返回 502 时,先确认同版本官方镜像是否能访问,不要直接换成其他版本。例如本次上海节点可以从 Quay 拉取 Calico controller,使用以下命令回退到官方地址:
kubectl set image deployment/calico-kube-controllers -n kube-system \
calico-kube-controllers=quay.io/calico/kube-controllers:v3.32.1
kubectl rollout status deployment/calico-kube-controllers -n kube-system --timeout=5m解除污点:
kubectl taint nodes --all node-role.kubernetes.io/control-plane-查看集群状态:
kubectl top node
kubectl get node1.2.2 GPU Operator 部署
1.2.2.1 Helm 安装
官方安装文档: https://helm.sh/docs/intro/install/ Helm 安装包: https://github.com/helm/helm/releases
安装 Helm:
# 首先下载安装包
mkdir helm
cd helm
wget https://get.helm.sh/helm-v4.2.2-linux-amd64.tar.gz
ls
tar xf helm-v4.2.2-linux-amd64.tar.gz
mv linux-amd64/helm /usr/local/bin/
helm version创建 Namespace:
kubectl create ns gpu-operator
kubectl label --overwrite ns gpu-operator pod-security.kubernetes.io/enforce=privileged1.2.2.2 部署 GPU Operator
添加仓库:
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia \
&& helm repo update下载安装包:
helm pull nvidia/gpu-operator --version v26.7.2
tar xf gpu-operator-v26.7.2.tgz
cd gpu-operator/国内机器需要修改 values 文件中的仓库地址:
vim charts/node-feature-discovery/values.yaml
image:
repository: m.daocloud.io/registry.k8s.io/nfd/node-feature-discovery如果服务器已经按照 1.1.2 安装了 NVIDIA 驱动,需要通过 driver.enabled=false 禁止 GPU Operator 重复安装驱动,避免覆盖现有驱动或产生版本冲突。本文固定使用 GPU Operator v26.7.2;该版本会识别节点上的 Containerd,Chart 已不再使用旧的 operator.defaultRuntime 参数:
helm install gpu-operator . \
-n gpu-operator \
--create-namespace \
--set driver.enabled=false查看 Pod 状态:
kubectl get po -n gpu-operator所有 Pod 启动完成后,即可在 Kubernetes 的节点状态中,看到 GPU 资源:
kubectl get po -n gpu-operator查看 GPU 资源:
kubectl describe node | grep Allocatable: -A 10重启后 GPU 资源变为 0 的处理: 如果 nvidia-device-plugin 日志出现 couldn't initialize inotify: too many open files,优先检查主机的 inotify 实例上限。本次重启后上限为 128、相关 FD 统计约为 131;提高到 1024 并重建插件 Pod 后,GPU 可分配数恢复为 1。先确认限制和当前用量,再执行调整:
fs.inotify.max_user_instances 限制的是同一用户可创建的 inotify 实例数。在没有启用用户命名空间隔离的常见配置下,多个以 root 运行的系统服务和容器会共同消耗宿主机 UID 0 的额度。这与 GPU 显存无关,也不能仅凭 too many open files 就认定是 ulimit -n 太小;应结合日志触发点、inotify 用量和进程文件描述符限制判断。下面的 FD 统计用于辅助排查,不是精确的去重实例数。
sysctl fs.inotify.max_user_instances
find /proc/[0-9]*/fd -lname anon_inode:inotify 2>/dev/null | wc -l
echo 'fs.inotify.max_user_instances = 1024' > /etc/sysctl.d/99-kubernetes-inotify.conf
sysctl --system
kubectl delete pod -n gpu-operator -l app=nvidia-device-plugin-daemonset
kubectl wait -n gpu-operator \
--for=condition=Ready pod \
-l app=nvidia-device-plugin-daemonset \
--timeout=180s
kubectl get node -o jsonpath='{.items[0].status.allocatable.nvidia\.com/gpu}{"\n"}'创建 GPU 测试服务:
tee test.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: cuda-vectoradd
spec:
restartPolicy: OnFailure
containers:
- name: cuda-vectoradd
image: "nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0-ubuntu22.04"
resources:
limits:
nvidia.com/gpu: 1
EOFkubectl create -f test.yaml查看日志:
kubectl logs cuda-vectoradd
# 确认日志包含 Test PASSED 后删除临时测试 Pod
kubectl delete pod cuda-vectoradd1.2.3 动态存储配置
官方文档: https://longhorn.io/docs/1.12.0/deploy/install/install-with-helm/
1.2.3.1 Longhorn
Longhorn 依赖 iSCSI 和 NFS 客户端。Rocky Linux 需要安装 iscsi-initiator-utils 和 nfs-utils,启用 iscsid,并加载 dm_crypt 内核模块。将模块写入 modules-load.d 后,服务器重启时也会自动加载:
dnf install iscsi-initiator-utils nfs-utils -y
systemctl enable --now iscsid
modprobe dm_crypt
echo dm_crypt > /etc/modules-load.d/longhorn.conf
# 检查服务和模块状态
systemctl is-active iscsid
lsmod | grep dm_crypt添加 Longhorn Helm 仓库:
helm repo add longhorn https://charts.longhorn.io
helm repo update
helm pull longhorn/longhorn --version 1.12.0安装 Longhorn:
tar xf longhorn-1.12.0.tgz
cd longhorn/
# 国内机器,将 global.imageRegistry 设置为 m.daocloud.io/docker.io
# 针对单节点,把默认卷副本数和默认 StorageClass 副本数设为 1
helm install longhorn . \
-n longhorn-system \
--create-namespace \
--set global.imageRegistry=m.daocloud.io/docker.io \
--set defaultSettings.defaultReplicaCount=1 \
--set persistence.defaultClassReplicaCount=1查看服务状态:
kubectl get po -n longhorn-system等待服务启动后,创建 PVC 和 Pod 测试。首先创建 PVC:
tee longhorn-pvc-test.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: longhorn-redis-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 1Gi
EOFkubectl create -f longhorn-pvc-test.yaml
kubectl get pvc创建测试 Pod:
tee longhorn-pod-test.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: volume-test
spec:
containers:
- name: volume-test
image: m.daocloud.io/docker.io/library/busybox:1.36.1
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "sleep 3600"]
volumeMounts:
- name: volv
mountPath: /data
volumes:
- name: volv
persistentVolumeClaim:
claimName: longhorn-redis-pvc
EOFkubectl create -f longhorn-pod-test.yaml写入、读取并验证 Pod 重建后的持久化。以下 PVC 仅用于本次测试,不应存放业务数据;测试完成后删除 Pod 和专用测试 PVC:
kubectl wait --for=condition=Ready pod/volume-test --timeout=5m
kubectl exec volume-test -- sh -ceu \
'printf LONGHORN_OK > /data/test.txt; sync; test "$(cat /data/test.txt)" = LONGHORN_OK'
kubectl delete pod volume-test
kubectl create -f longhorn-pod-test.yaml
kubectl wait --for=condition=Ready pod/volume-test --timeout=5m
kubectl exec volume-test -- sh -ceu \
'test "$(cat /data/test.txt)" = LONGHORN_OK; echo LONGHORN_PERSISTENCE_OK'
kubectl delete pod volume-test
kubectl delete pvc longhorn-redis-pvc1.3 模型部署
1.3.1 模型文件预下载
环境准备好后,就可以通过 VLLM 部署模型。当前模型已经下载到 Kubernetes 节点的 /data/models/Qwen3.5-4B,实际大小约为 8.8 GiB,因此创建一个 12 GiB 的 Longhorn PVC:
tee qwen35-4b-pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: qwen35-4b-pvc
namespace: models
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 12Gi # 根据模型实际大小和预留空间进行调整
EOF创建 PVC:
kubectl create namespace models --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f qwen35-4b-pvc.yaml
kubectl wait --for=jsonpath='{.status.phase}'=Bound \
pvc/qwen35-4b-pvc -n models --timeout=5m模型文件本地导入
如果本地已经有下载好的模型文件:
不要直接向 /var/lib/longhorn 写入文件。应创建一个临时 Job,同时将节点上的模型目录以只读 hostPath 挂载为源目录,并将 PVC 挂载为目标目录。Job 完成复制。
tee model-import-job.yaml <<'EOF'
apiVersion: batch/v1
kind: Job
metadata:
name: qwen35-4b-model-import
namespace: models
spec:
backoffLimit: 0
template:
spec:
nodeSelector:
kubernetes.io/hostname: MODEL_NODE_PLACEHOLDER
restartPolicy: Never
containers:
- name: importer
image: m.daocloud.io/docker.io/library/busybox:1.36.1
command: ["sh", "-ceu"]
args:
- |
target=/target/Qwen/Qwen3.5-4B
if [ -e "${target}" ] && [ -n "$(ls -A "${target}" 2>/dev/null)" ]; then
echo "目标目录非空,停止以避免覆盖已有模型"
exit 1
fi
mkdir -p "${target}"
cp -a /source/. "${target}/"
sync
(
cd /source
find . -type f -exec sha256sum {} \; | sort
) > /tmp/source.sha256
(
cd "${target}"
find . -type f -exec sha256sum {} \; | sort
) > /tmp/target.sha256
cmp /tmp/source.sha256 /tmp/target.sha256
echo "模型复制及 SHA256 校验完成"
du -sh "${target}"
find "${target}" -type f | wc -l
volumeMounts:
- name: local-model
mountPath: /source
readOnly: true
- name: model-storage
mountPath: /target
volumes:
- name: local-model
hostPath:
path: /data/models/Qwen3.5-4B
type: Directory
- name: model-storage
persistentVolumeClaim:
claimName: qwen35-4b-pvc
EOF创建导入 Job 并等待完成:
# 单节点实验:从实际节点标签读取 hostname,不要照搬其他 ECS 的节点名。
MODEL_NODE="$(kubectl get nodes -o jsonpath='{.items[0].metadata.labels.kubernetes\.io/hostname}')"
test -n "${MODEL_NODE}"
sed "s/MODEL_NODE_PLACEHOLDER/${MODEL_NODE}/g" model-import-job.yaml \
| kubectl apply -f -
kubectl wait --for=condition=complete \
job/qwen35-4b-model-import -n models --timeout=30m查看复制和 SHA256 校验结果:
kubectl logs job/qwen35-4b-model-import -n models确认成功后删除临时 Job 及其 Pod,但保留 PVC 中的模型文件:
kubectl delete job qwen35-4b-model-import -n models
kubectl get pvc qwen35-4b-pvc -n models后续部署将该 PVC 挂载到 /data/modelscope 后,模型路径仍为 /data/modelscope/Qwen/Qwen3.5-4B。本地源目录应保留到模型服务启动和推理验证成功后,再根据磁盘空间情况决定是否删除。
1.3.2 部署模型
文档: https://docs.vllm.ai/en/latest/deployment/k8s/
- 仅部署一个模型时,用 YAML 部署即可
- 如果有分布式计算、模型微调、调度等需求,可以用一些开源框架比如KServe,llm-d等: https://docs.vllm.ai/en/latest/deployment/integrations/kserve/
确认模型 PVC 已经处于 Bound 状态:
kubectl get pvc qwen35-4b-pvc -n models- 创建
qwen35-vllm.yaml,在同一个多文档 YAML 中定义 Secret、Deployment 和 Service。Secret 中只保留VLLM_API_KEY_PLACEHOLDER占位符,应用时在内存中替换,真实 API Key 不写入文章或 YAML 文件。 - 镜像入口点已经是
vllm serve,因此通过args传入参数。vLLM 0.23.0 弃用了--model,模型路径必须作为args的第一个位置参数。 - 本地模型通过 PVC 挂载,并设置
HF_HUB_OFFLINE=1、TRANSFORMERS_OFFLINE=1,避免启动时重新从网络下载模型。GPU 设备交给 Device Plugin 分配,不硬编码CUDA_VISIBLE_DEVICES。 - 单节点只有一块 GPU,且 PVC 为
ReadWriteOnce,Deployment 使用Recreate,避免 RollingUpdate 时新旧 Pod 争用同一块 GPU 和 PVC:
apiVersion: v1
kind: Secret
metadata:
name: vllm-api-key
namespace: models
type: Opaque
stringData:
api-key: "VLLM_API_KEY_PLACEHOLDER" # 应用时替换,不要把真实密钥提交到 Git
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: qwen3-5-4b-deployment
namespace: models
labels:
app: qwen3-5-4b
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: qwen3-5-4b
template:
metadata:
labels:
app: qwen3-5-4b
spec:
containers:
- name: qwen3-5-4b
image: registry.cn-beijing.aliyuncs.com/dotbalo/vllm-openai@sha256:3a1e7f5904e1a1192a02aa0086ceaffc33985d7044c7bb25b3a43d61bdbe3ac0
imagePullPolicy: IfNotPresent
args:
- "/data/modelscope/Qwen/Qwen3.5-4B" # PVC 挂载点下的模型目录
- "--port"
- "8080"
- "--served-model-name"
- "Qwen3.5-4B"
- "--gpu-memory-utilization"
- "0.9"
- "--max-model-len"
- "16384"
- "--max-num-batched-tokens"
- "16384"
ports:
- name: http
containerPort: 8080
env:
- name: TZ
value: "Asia/Shanghai"
- name: LANG
value: "C.UTF-8"
- name: LC_ALL
value: "C.UTF-8"
- name: HF_HUB_OFFLINE
value: "1"
- name: TRANSFORMERS_OFFLINE
value: "1"
- name: VLLM_API_KEY
valueFrom:
secretKeyRef:
name: vllm-api-key
key: api-key
resources:
limits:
nvidia.com/gpu: 1
requests:
nvidia.com/gpu: 1
volumeMounts:
- name: model-storage
mountPath: /data/modelscope
readOnly: true
- name: dshm
mountPath: /dev/shm
startupProbe:
httpGet:
path: /health
port: http
# 给予最长 15 分钟(900 秒)的启动时间,每 10 秒探测一次
# 如果 90 次(900/10)探测都失败,容器才会被重启
failureThreshold: 90
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /health
port: http
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: http
periodSeconds: 15
timeoutSeconds: 10
failureThreshold: 3
volumes:
- name: model-storage
persistentVolumeClaim:
claimName: qwen35-4b-pvc
- name: dshm
emptyDir:
medium: Memory
sizeLimit: 4Gi
---
apiVersion: v1
kind: Service
metadata:
name: qwen3-5-4b-service
namespace: models
spec:
selector:
app: qwen3-5-4b
ports:
- name: http
protocol: TCP
port: 8080
targetPort: http
type: NodePort # 非必须首次部署时生成随机 API Key;重复执行时复用已有 Secret,不自动轮换。通过管道替换占位符并直接提交给 API Server,不把真实密钥写入 YAML 文件。本文生成的是十六进制字符串,可以安全地作为 sed 替换值;不要直接执行 kubectl apply -f qwen35-vllm.yaml,否则会把占位符保存为真实密钥:
if kubectl get secret vllm-api-key -n models >/dev/null 2>&1; then
VLLM_API_KEY="$(kubectl get secret vllm-api-key -n models \
-o jsonpath='{.data.api-key}' | base64 -d)"
else
VLLM_API_KEY="$(openssl rand -hex 32)"
fi
test -n "${VLLM_API_KEY}"
sed "s/VLLM_API_KEY_PLACEHOLDER/${VLLM_API_KEY}/g" qwen35-vllm.yaml \
| kubectl apply -f -
unset VLLM_API_KEY
kubectl rollout status deployment/qwen3-5-4b-deployment \
-n models \
--timeout=15m后续若显式轮换 API Key,需要同步更新 litellm 命名空间中的同名 Secret,并重启 vLLM 和 LiteLLM Deployment;已启动 Pod 的环境变量不会自动读取新值。
查看启动状态:
kubectl get po,pvc,svc -n models -o wide
kubectl logs deployment/qwen3-5-4b-deployment -n models --tail=100模型访问测试。NodePort 由 Kubernetes 动态分配,不要写死示例端口;enable_thinking=false 用于让这次连通性测试只返回最终答案:
NODE_IP="$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')"
NODE_PORT="$(kubectl get service qwen3-5-4b-service \
-n models \
-o jsonpath='{.spec.ports[0].nodePort}')"
VLLM_API_KEY="$(kubectl get secret vllm-api-key \
-n models \
-o jsonpath='{.data.api-key}' | base64 -d)"
curl -sS "http://${NODE_IP}:${NODE_PORT}/v1/chat/completions" \
-H "Authorization: Bearer ${VLLM_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen3.5-4B",
"stream": false,
"temperature": 0,
"max_tokens": 64,
"chat_template_kwargs": {"enable_thinking": false},
"messages": [
{"role": "user", "content": "只回复:K8S_VLLM_OK"}
]
}'
unset VLLM_API_KEY1.4 LiteLLM 高可用落地

1.4.1 高可用 Redis 部署
参考文档:
- LiteLLM 配置: https://docs.litellm.com.cn/docs/proxy/config_settings
- Bitnami Redis Helm Chart: https://github.com/bitnami/charts/tree/main/bitnami/redis
需要部署两个相互独立的 Redis Release:
redis:3 个 Redis + Sentinel Pod,用作 LiteLLM 响应缓存(litellm_settings.cache和cache_params)。redis-single:单节点 Redis,用作 LiteLLM 路由限流(router_settings)和跨 Pod 协调。本文将它与响应缓存分开配置。
NOTE
本文是单节点 K8s 集群。3 个 Sentinel Pod 可以处理 Redis 进程或 Pod 故障,但都运行在同一台 ECS 上,无法提供节点级高可用;Longhorn 卷副本数同样应保持为
1。
先创建命名空间和两个独立的认证 Secret。下面的命令只在 Secret 不存在时生成密码,重复执行不会静默轮换已有密码:
kubectl create namespace litellm --dry-run=client -o yaml | kubectl apply -f -
for secret_name in redis-sentinel-auth redis-standalone-auth; do
if ! kubectl get secret "${secret_name}" -n litellm >/dev/null 2>&1; then
password_file=$(mktemp)
chmod 600 "${password_file}"
openssl rand -hex 32 | tr -d '\n' > "${password_file}"
kubectl create secret generic "${secret_name}" -n litellm \
--from-file=redis-password="${password_file}"
shred -u "${password_file}"
fi
done只有出现磁盘调度失败时才检查以下设置。之前的实验节点根盘仅剩约 17 GiB,低于 Longhorn 默认的 25% 最小剩余空间阈值,创建卷时出现了 ReplicaSchedulingFailure: disks are unavailable。磁盘空间充足的新节点不需要降低阈值;遇到这种情况应先确认剩余容量并优先扩容,下面的 10% 仅是经确认后的实验环境示例,不是必做步骤:
kubectl -n longhorn-system get settings.longhorn.io \
storage-minimal-available-percentage
kubectl -n longhorn-system patch settings.longhorn.io \
storage-minimal-available-percentage \
--type=merge -p '{"value":"10"}'WARNING
该设置影响整个 Longhorn 集群,不是 Redis Helm 参数。只有确认磁盘仍有足够空间、且卷确实因 25% 阈值无法调度时才应降低;生产环境应优先扩容磁盘并保留更高的安全余量。
Redis Sentinel - 缓存用
使用 Bitnami Redis Chart 28.1.0 部署 3 个 Redis + Sentinel Pod,Sentinel master set 为 mymaster、quorum 为 2。每个 Redis 数据 PVC 均从默认的 8Gi 调整为 1Gi:
helm upgrade --install redis \
oci://m.daocloud.io/docker.io/bitnamicharts/redis \
--version 28.1.0 \
--namespace litellm \
--set global.imageRegistry=m.daocloud.io/docker.io \
--set global.defaultStorageClass=longhorn \
--set global.security.allowInsecureImages=true \
--set image.digest=sha256:ffa455a3ad00bccc24dfde113ef55329dde687da242e9feb6ccd2b30eb93e8f3 \
--set sentinel.image.digest=sha256:97433fd14ea945c164ac514c162e2d2a017e9044fdc7ec46abf12a92f4660770 \
--set architecture=replication \
--set sentinel.enabled=true \
--set sentinel.masterSet=mymaster \
--set sentinel.quorum=2 \
--set replica.replicaCount=3 \
--set master.persistence.storageClass=longhorn \
--set master.persistence.size=1Gi \
--set replica.persistence.storageClass=longhorn \
--set replica.persistence.size=1Gi \
--set auth.existingSecret=redis-sentinel-auth \
--set auth.existingSecretPasswordKey=redis-password \
--wait --timeout 10mglobal.security.allowInsecureImages=true 是 Bitnami Chart 使用自定义镜像仓库时所需的镜像校验开关,并不表示关闭 Redis 密码认证。镜像仍固定到了本次验证通过的不可变 digest。
查看 Pod、Service 和 PVC 状态:
helm status redis -n litellm
kubectl get sts,pod,svc,pvc -n litellm \
-l app.kubernetes.io/instance=redis验证 Sentinel master、quorum、replica 数量和读写;测试键会在最后删除,不需要创建临时测试 Pod:
kubectl exec redis-node-0 -n litellm -c redis -- sh -ceu '
export REDISCLI_AUTH="$(cat "$REDIS_PASSWORD_FILE")"
redis-cli --raw -h redis -p 26379 SENTINEL ckquorum mymaster
master=$(redis-cli --raw -h redis -p 26379 \
SENTINEL get-master-addr-by-name mymaster | head -n 1)
test -n "$master"
printf "Sentinel master: %s\n" "$master"
redis-cli --raw -h redis -p 26379 SENTINEL replicas mymaster | grep -c "^name$"
redis-cli --raw -h "$master" SET litellm-sentinel-test ok
redis-cli --raw -h "$master" GET litellm-sentinel-test
redis-cli --raw -h "$master" DEL litellm-sentinel-test
'供 LiteLLM 使用的集群内地址为:
- Sentinel:
redis.litellm.svc.cluster.local:26379 - Sentinel master set:
mymaster - Redis 服务:
redis.litellm.svc.cluster.local:6379
redis:6379 的 Service 可能命中 replica。写入应使用 Sentinel 发现的 master 地址,LiteLLM 响应缓存也应按 Sentinel 方式连接。
单节点 Redis - 限流用
限流用的Redis,liteLLM暂时不支持哨兵或者集群模式(https://docs.litellm.ai/docs/proxy/config_settings),只能先部署单节点
使用同一个 Chart 的 standalone 架构部署独立 Redis,数据 PVC 同样设置为 1Gi:
helm upgrade --install redis-single \
oci://m.daocloud.io/docker.io/bitnamicharts/redis \
--version 28.1.0 \
--namespace litellm \
--set global.imageRegistry=m.daocloud.io/docker.io \
--set global.defaultStorageClass=longhorn \
--set global.security.allowInsecureImages=true \
--set image.digest=sha256:ffa455a3ad00bccc24dfde113ef55329dde687da242e9feb6ccd2b30eb93e8f3 \
--set architecture=standalone \
--set master.persistence.storageClass=longhorn \
--set master.persistence.size=1Gi \
--set auth.existingSecret=redis-standalone-auth \
--set auth.existingSecretPasswordKey=redis-password \
--wait --timeout 10m验证状态和读写,随后删除测试键:
helm status redis-single -n litellm
kubectl get sts,pod,svc,pvc -n litellm \
-l app.kubernetes.io/instance=redis-single
kubectl exec redis-single-master-0 -n litellm -c redis -- sh -ceu '
export REDISCLI_AUTH="$(cat "$REDIS_PASSWORD_FILE")"
redis-cli --raw SET litellm-rate-limit-test ok
redis-cli --raw GET litellm-rate-limit-test
redis-cli --raw DEL litellm-rate-limit-test
'供 LiteLLM 限流使用的集群内地址为 redis-single-master.litellm.svc.cluster.local:6379。
1.4.2 高可用 Postgresql 部署
使用 Bitnami postgresql-ha Chart 16.3.2,部署 3 个 PostgreSQL/repmgr 实例和 2 个 Pgpool。数据库名为 litellm,每个 PostgreSQL PVC 为 8Gi,使用单副本 Longhorn。
WARNING
本次经确认使用
bitnamilegacy的 PostgreSQL17.6.0和 Pgpool4.6.3镜像,仅用于实验。这些公开旧镜像不再获得安全更新,生产环境应选用仍在维护的方案。所有实例都在同一台 ECS 上,不能提供节点级高可用。
先生成认证 Secret。密码不带尾部换行,也不写入 Helm values;已有 Secret 不会被重复生成的密码覆盖:
kubectl create namespace litellm --dry-run=client -o yaml | kubectl apply --server-side -f -
if ! kubectl get secret postgresql-ha-auth -n litellm >/dev/null 2>&1; then
secret_dir=$(mktemp -d)
for key in password repmgr-password admin-password sr-check-password; do
openssl rand -hex 32 | tr -d '\n' > "${secret_dir}/${key}"
done
kubectl create secret generic postgresql-ha-auth -n litellm \
--from-file="${secret_dir}"
for key in password repmgr-password admin-password sr-check-password; do
shred -u "${secret_dir}/${key}"
done
rmdir "${secret_dir}"
fi保存为 postgresql-ha-values.yaml。单节点使用软反亲和性,不能设置必须分散到不同节点的硬反亲和性:
global:
imageRegistry: m.daocloud.io/docker.io
security:
allowInsecureImages: true
postgresql:
image:
repository: bitnamilegacy/postgresql-repmgr
tag: 17.6.0-debian-12-r2
digest: sha256:9336b076c7f846043872a9eacf8496967c5c3ba9028c80dcd8a81a148a8cfa65
replicaCount: 3
podAntiAffinityPreset: soft
username: postgres
database: litellm
existingSecret: postgresql-ha-auth
pgpool:
image:
repository: bitnamilegacy/pgpool
tag: 4.6.3-debian-12-r0
digest: sha256:663b1cbe74bd10ff963ef43e8cc5dfc4032e11c10c6e72297b6c312434fdef17
replicaCount: 2
podAntiAffinityPreset: soft
existingSecret: postgresql-ha-auth
persistence:
enabled: true
storageClass: longhorn
size: 8Gi
volumePermissions:
enabled: false
metrics:
enabled: false下载 Chart 并部署;离线准备时,可将相同的 .tgz 和 values 文件上传后直接使用:
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update bitnami
helm pull bitnami/postgresql-ha --version 16.3.2
helm upgrade --install pg ./postgresql-ha-16.3.2.tgz \
-n litellm -f postgresql-ha-values.yaml
kubectl rollout status statefulset/pg-postgresql-ha-postgresql -n litellm --timeout=15m
kubectl rollout status deployment/pg-postgresql-ha-pgpool -n litellm --timeout=15m
kubectl get sts,deploy,pod,svc,pvc -n litellm通过 Pgpool 连接数据库。密码在 Pod 内读取挂载的 Secret 文件,不作为 kubectl exec 的参数传入:
kubectl exec pg-postgresql-ha-postgresql-0 -n litellm -c postgresql -- bash -ceu '
export PGPASSWORD="$(cat "$POSTGRES_PASSWORD_FILE")"
psql -v ON_ERROR_STOP=1 -U postgres -h pg-postgresql-ha-pgpool -d litellm \
-c "SELECT current_database(), version();"
'再分别查看 3 个实例的实时角色和复制状态。pg_is_in_recovery() 为 f 的是 primary,为 t 的是 standby;primary 的 pg_stat_replication 应显示 2 个 streaming 连接。这里使用 SQL 验证,避免旧镜像中直接 kubectl exec repmgr 时因 UID 缺少 passwd 映射而出现 could not get current user name:
for ordinal in 0 1 2; do
echo "postgresql-${ordinal}"
kubectl exec "pg-postgresql-ha-postgresql-${ordinal}" -n litellm -c postgresql -- bash -ceu '
export PGPASSWORD="$(cat "$POSTGRES_PASSWORD_FILE")"
psql -U postgres -d postgres \
-c "SELECT pg_is_in_recovery();" \
-c "SELECT application_name, state, sync_state FROM pg_stat_replication;"
'
doneLiteLLM 使用的数据库入口为 pg-postgresql-ha-pgpool.litellm.svc.cluster.local:5432。
1.4.3 部署 LiteLLM
部署 2 个 LiteLLM v1.105.1 Pod,共用 PostgreSQL 数据库、Sentinel 响应缓存和独立 Redis 路由限流实例。模型后端使用 1.3.2 的集群内 Service,不再填写示例 IP 或 API Key。
1.4.3.1 认证 Secret 准备
先创建 litellm-auth,并把模型服务的 API Key 复制到 litellm 命名空间。Secret 不能跨命名空间直接引用;真实密钥只提交给 Kubernetes,不写入文章或部署 YAML。LITELLM_SALT_KEY 用于加密数据库中的凭据,部署后不要随意轮换:
if ! kubectl get secret litellm-auth -n litellm >/dev/null 2>&1; then
secret_dir=$(mktemp -d)
db_password=$(kubectl get secret postgresql-ha-auth -n litellm \
-o jsonpath='{.data.password}' | base64 -d)
printf 'postgresql://postgres:%s@pg-postgresql-ha-pgpool.litellm.svc.cluster.local:5432/litellm' \
"${db_password}" > "${secret_dir}/DATABASE_URL"
unset db_password
printf 'sk-%s' "$(openssl rand -hex 32)" > "${secret_dir}/LITELLM_MASTER_KEY"
printf 'sk-%s' "$(openssl rand -hex 32)" > "${secret_dir}/LITELLM_SALT_KEY"
printf admin > "${secret_dir}/UI_USERNAME"
openssl rand -hex 24 | tr -d '\n' > "${secret_dir}/UI_PASSWORD"
kubectl create secret generic litellm-auth -n litellm --from-file="${secret_dir}"
for key in DATABASE_URL LITELLM_MASTER_KEY LITELLM_SALT_KEY UI_USERNAME UI_PASSWORD; do
shred -u "${secret_dir}/${key}"
done
rmdir "${secret_dir}"
fi
if ! kubectl get secret vllm-api-key -n litellm >/dev/null 2>&1; then
kubectl get secret vllm-api-key -n models -o json \
| jq 'del(.metadata.uid,.metadata.resourceVersion,.metadata.creationTimestamp,.metadata.managedFields,.metadata.annotations) | .metadata.namespace="litellm"' \
| kubectl apply -f -
fi1.4.3.2 模型与缓存配置(ConfigMap)
保存以下配置为 litellm-config.yaml。响应缓存位于 litellm_settings.cache_params,不要写成 general_settings.cache;Sentinel 自身和 Redis 数据连接都需要密码。路由限流和跨 Pod 协调使用独立的单节点 Redis:
model_list:
- model_name: Qwen3.5-4B
litellm_params:
model: openai/Qwen3.5-4B
api_base: http://qwen3-5-4b-service.models.svc.cluster.local:8080/v1
api_key: os.environ/VLLM_API_KEY
litellm_settings:
cache: true
cache_params:
type: redis
service_name: mymaster
sentinel_nodes:
- [redis-node-0.redis-headless.litellm.svc.cluster.local, 26379]
- [redis-node-1.redis-headless.litellm.svc.cluster.local, 26379]
- [redis-node-2.redis-headless.litellm.svc.cluster.local, 26379]
sentinel_password: os.environ/CACHE_SENTINEL_PASSWORD
password: os.environ/CACHE_SENTINEL_PASSWORD
ttl: 600
router_settings:
redis_host: redis-single-master.litellm.svc.cluster.local
redis_port: 6379
redis_password: os.environ/REDIS_PASSWORD
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
database_url: os.environ/DATABASE_URL
database_connection_pool_limit: 10
use_redis_transaction_buffer: true
coordination_redis:
host: redis-single-master.litellm.svc.cluster.local
port: 6379
password: os.environ/REDIS_PASSWORDuse_redis_transaction_buffer: true 开启费用更新缓冲:各 Pod 先将费用更新写入 Redis,再由取得锁的 Pod 汇总后写入 PostgreSQL,减少并发更新同一记录的竞争。它与响应缓存分别配置,不能替代数据库。参见 Redis transaction buffer 官方说明。
创建 ConfigMap:
这里使用自定义环境变量 CACHE_SENTINEL_PASSWORD。在 v1.105.1 中,保留名称 REDIS_SENTINEL_PASSWORD 会被所有 Redis 客户端读取;与单节点 Redis 共存时,可能触发 Redis.__init__() got an unexpected keyword argument 'sentinel_password'。通过 cache_params 显式引用自定义变量,可以避免把 Sentinel 参数带入单节点客户端。
kubectl create configmap litellm-config -n litellm \
--from-file=config.yaml=litellm-config.yaml \
--dry-run=client -o yaml | kubectl apply -f -1.4.3.3 执行数据库迁移
核心原因是:
- PostgreSQL 已安装,不代表里面已经有 LiteLLM 需要的表;数据库迁移负责创建或升级这些表。
- LiteLLM 默认可以在服务启动时执行迁移。但我们部署了两个副本,如果都允许自动迁移,启动、滚动升级时就可能同时尝试修改同一个数据库。
数据库迁移会根据 LiteLLM 版本创建或更新表、字段、索引和约束,用于保存虚拟 Key、用户、团队、UI 添加的模型配置及费用记录。
执行迁移是为了让数据库结构与应用版本一致:首次部署时准备所需表结构,升级时应用新版本的结构变更。否则,即使 PostgreSQL 可以连接,LiteLLM 仍可能因缺少表或字段而报错。参见 LiteLLM 官方迁移说明。
这样还有两个好处:
- 迁移失败时,可以先停止发布,不让新服务在旧表结构上运行。
- 可以单独检查迁移 Job 的日志和结果,不必混在服务启动日志里排查。 这也是 LiteLLM 官方部署建议采用的分工。 https://docs.litellm.ai/docs/proxy/deploy
本文先通过独立 Job 完成迁移,再启动 LiteLLM 服务。迁移 Job 和服务使用相同的镜像 digest,确保迁移文件与应用版本一致。(独立 Job 是我们为双副本部署选择的执行方式,并不是唯一方式)
保存以下内容为 litellm-migrate.yaml:
apiVersion: batch/v1
kind: Job
metadata:
name: litellm-db-migrate
namespace: litellm
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: ghcr.io/berriai/litellm@sha256:b7b9e2c306343721802253d0dd9436b9634a2eb4e14da7c7cf06c5c7b7530802 # v1.105.1
imagePullPolicy: IfNotPresent
args: [--skip_server_startup]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: litellm-auth
key: DATABASE_URL
- name: DISABLE_SCHEMA_UPDATE
value: "false"
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
memory: 2Gikubectl apply -f litellm-migrate.yaml
kubectl wait --for=condition=complete job/litellm-db-migrate -n litellm --timeout=15m1.4.3.4 部署 Deployment 和 Service
保存以下内容为 litellm-deploy.yaml。服务 Pod 禁止自动更新数据库 schema,通过 Secret 注入 API Key 和认证信息:
apiVersion: apps/v1
kind: Deployment
metadata:
name: litellm
namespace: litellm
spec:
replicas: 2
selector:
matchLabels:
app: litellm
template:
metadata:
labels:
app: litellm
spec:
containers:
- name: litellm
image: ghcr.io/berriai/litellm@sha256:b7b9e2c306343721802253d0dd9436b9634a2eb4e14da7c7cf06c5c7b7530802 # v1.105.1
imagePullPolicy: IfNotPresent
args: [--config, /app/config.yaml, --port, "4000", --num_workers, "1"]
ports:
- name: http
containerPort: 4000
envFrom:
- secretRef:
name: litellm-auth
env:
- name: VLLM_API_KEY
valueFrom:
secretKeyRef:
name: vllm-api-key
key: api-key
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-standalone-auth
key: redis-password
- name: CACHE_SENTINEL_PASSWORD
valueFrom:
secretKeyRef:
name: redis-sentinel-auth
key: redis-password
- name: STORE_MODEL_IN_DB
value: "True"
- name: DISABLE_SCHEMA_UPDATE
value: "true"
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
memory: 2Gi
startupProbe:
httpGet:
path: /health/liveliness
port: http
periodSeconds: 10
failureThreshold: 60
readinessProbe:
httpGet:
path: /health/readiness
port: http
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /health/liveliness
port: http
periodSeconds: 30
timeoutSeconds: 5
volumeMounts:
- name: config
mountPath: /app/config.yaml
subPath: config.yaml
readOnly: true
volumes:
- name: config
configMap:
name: litellm-config
---
apiVersion: v1
kind: Service
metadata:
name: litellm-service
namespace: litellm
spec:
selector:
app: litellm
ports:
- name: http
port: 4000
targetPort: http
type: NodePort部署并检查:
kubectl apply -f litellm-deploy.yaml
kubectl rollout status deployment/litellm -n litellm --timeout=15m
kubectl get pods,svc -n litellm
# 确认迁移和服务启动成功后清理临时迁移 Job
kubectl delete job litellm-db-migrate -n litellm1.4.3.5 管理后台与调用验证
使用实际节点内网 IP 和动态 NodePort 访问。管理后台入口为 http://<节点内网IP>:<NodePort>/ui,用户名为 admin,密码保存在 litellm-auth 的 UI_PASSWORD 中;后台创建的虚拟 Key 可用于后续调用。模型 API Base 已在 ConfigMap 中配置,API Key 来自 vllm-api-key。
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
LITELLM_NODE_PORT=$(kubectl get svc litellm-service -n litellm -o jsonpath='{.spec.ports[0].nodePort}')
read -rs -p 'LiteLLM virtual key: ' LITELLM_VIRTUAL_KEY; echo
curl -fsS "http://${NODE_IP}:${LITELLM_NODE_PORT}/v1/chat/completions" \
-H "Authorization: Bearer ${LITELLM_VIRTUAL_KEY}" \
-H 'Content-Type: application/json' \
-d '{"model":"Qwen3.5-4B","stream":false,"temperature":0,"max_tokens":32,"chat_template_kwargs":{"enable_thinking":false},"messages":[{"role":"user","content":"只回复:K8S_LITELLM_OK"}]}'
unset LITELLM_VIRTUAL_KEY1.4.3.6 模型配置来源与更新维护
本文的 Qwen3.5-4B 已通过 litellm 命名空间的 litellm-config ConfigMap 接入:model_list 定义模型,Deployment 将其中的 config.yaml 挂载到 /app/config.yaml,LiteLLM 启动时通过 --config /app/config.yaml 加载,无需再到 UI 添加同一份后端定义。
ConfigMap 和 UI 是两种不同的模型配置来源:
- ConfigMap 中的模型由配置文件管理,修改时应更新
litellm-config.yaml和对应的 ConfigMap,不能通过 UI 直接修改这份文件。 - 当前已开启
STORE_MODEL_IN_DB=True。通过 UI/API 添加或修改的数据库模型配置保存在 PostgreSQL 中,不会自动回写 ConfigMap;开启该参数也不会自动将model_list中的模型转为数据库管理。
两种来源可以同时加载,但同一份后端定义应选择一种方式维护,避免在 UI 和 ConfigMap 中重复配置后产生不一致。参见 LiteLLM 模型管理说明。
当前 Deployment 使用 subPath: config.yaml 挂载配置文件。此类挂载不会自动接收 ConfigMap 更新,因此只执行 kubectl apply 更新 ConfigMap,不会让正在运行的 Pod 加载新配置;需要滚动重启 LiteLLM,让新 Pod 重新挂载文件并读取配置。参见 Kubernetes ConfigMap 更新说明。
后续修改模型时,先更新完整的 litellm-config.yaml,保留现有缓存、路由及数据库配置,再在已配置目标集群管理员 kubeconfig 的 ECS 终端中执行:
kubectl create configmap litellm-config -n litellm \
--from-file=config.yaml=litellm-config.yaml \
--dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deployment/litellm -n litellm
kubectl rollout status deployment/litellm -n litellm --timeout=15m更新完成后,按 管理后台与调用验证 重新确认模型可用。
1.4.4 限流测试
LiteLLM 支持如下三种限流方案:
- TPM:每分钟 Token 数量限制
- RPM:每分钟请求数量限制
- Max Parallel Requests:最大并发量限制
其中 TPM 和 RPM 分为了三个限制方法:
- Best effort:系统会尽可能多地处理请求,不强制拦截超出限制的请求
- Guaranteed:系统会严格进行限制,确保不超过设定的限制值
- Dynamic:动态调整,系统根据实时负载、历史调用等状态,动态调整限制规则
在Key Setting中设置。比如限制某个 Key,每分钟最大 Token 为 2000,请求数为 3:
- TPM Limit:
2000 - TPM Rate Limit Type:
Dynamic - RPM Limit:
3 - RPM Rate Limit Type:
Guaranteed throughput
测试每分钟请求数量限制:
curl -H "Authorization: Bearer <litellm-virtual-key>" -X POST http://127.0.0.1:31398/v1/chat/completions -H "Content-Type: application/json" -d '{"model": "Qwen3.5-4B","stream": true, "messages": [{"role": "user", "content": "介绍下你自己"}]}'{"error":{"message":"Rate limit exceeded for api_key: 7b41ef1ecb31b286fdb9815a5266c8a232edf059cc02dfdf05a1a38d85f87ab3. Limit type: requests. Current limit: 3, Remaining: 0.","type":"throttling_error","param":null,"code":"429"}}由于请求数量是 Guaranteed,所以会直接拒绝,token 限制为动态的,所以不会被拒绝。调整为 Guaranteed 后会拒绝连接:
{"error":{"message":"Rate limit exceeded for api_key: 7b41ef1ecb31b286fdb9815a5266c8a232edf059cc02dfdf05a1a38d85f87ab3. Limit type: tokens. Current limit: 200, Remaining: 0.","type":"throttling_error","param":null,"code":"429"}}1.4.5 缓存命中
同一个问题,连续问会直接返回结果(命中 Redis 缓存,无需重新推理):
curl -H "Authorization: Bearer <litellm-virtual-key>" -X POST http://127.0.0.1:31398/v1/chat/completions -H "Content-Type: application/json" -d '{"model": "Qwen3.5-4B","stream": true, "messages": [{"role": "user", "content": "介绍下你自己 2"}]}'流式返回示例(节选):
data: {"id":"chatcmpl-a1c275d854c9c722","object":"chat.completion.chunk","created":1783764514,"model":"Qwen3.5-4B","choices":[{"index":0,"delta":{"role":"assistant","content":"对于用户的问题,首先需要明确我是 Qwen3.5,是通义实验室最新推出的大语言模型。..."}}]}
data: {"id":"chatcmpl-a1c275d854c9c722","object":"chat.completion.chunk","created":1783764514,"model":"Qwen3.5-4B","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
1.5 GPU 虚拟化
前面我们通过GPU operator实现了大模型申请GPU资源,但是这是独占模式。如果模型很小,卡很大,就会造成资源浪费。
HAMi(异构AI计算虚拟化中间件)是一个用于管理K8s集群中异构AI计算设备的开源平台,前身为8s-vGPU-scheduler(第四范式),目前为CNCF正在孵化的开源项目。
HAMi可以在多个容器和工作负载之间实现设备共享,解决AI算力集群中GPU资源碎片化、利用率低的问题。支持多个厂商的GPU虚拟化,比如NVIDIA、华为、Intel、AMD、沐曦等。
简单说明:

1.5.1 HAMI 部署
官方文档: https://project-hami.io/zh/docs/installation/prerequisites
以下命令针对本文的单节点 A10 环境。先给 GPU 节点添加 gpu=on 标签:
GPU_NODE="$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')"
kubectl label node "${GPU_NODE}" gpu=on --overwriteHAMi 使用的 kube-scheduler 镜像版本需要匹配集群版本:
kubectl versionHAMi 和 GPU Operator 的设备插件不能同时管理同一节点的 nvidia.com/gpu。首次切换前,先停掉本文的单副本 vLLM,再关闭 GPU Operator 的设备插件;驱动、Toolkit 和 DCGM 保留。完成后续测试,在 1.5.5 恢复模型服务。已经完成切换的集群不要重复停机安装。
先备份当前 Helm 的全部生效值,再用同版本 Chart 更新,避免丢失之前修改过的镜像仓库配置。--no-hooks 仅用于这次同版本、只关闭设备插件的更新,不应直接套用于版本升级:
kubectl scale deployment/qwen3-5-4b-deployment -n models --replicas=0
kubectl wait --for=delete pod -n models -l app=qwen3-5-4b --timeout=3m
helm get values gpu-operator -n gpu-operator --all -o yaml \
> gpu-operator.before-hami.values.yaml
helm upgrade gpu-operator nvidia/gpu-operator \
--version v26.7.2 -n gpu-operator \
-f gpu-operator.before-hami.values.yaml \
--set devicePlugin.enabled=false \
--no-hooks --wait --timeout 5m
kubectl get clusterpolicy cluster-policy \
-o jsonpath='{.spec.devicePlugin.enabled}{"\n"}'
kubectl get daemonset -n gpu-operator确认设备插件已关闭,且原 nvidia-device-plugin-daemonset 已退出,再安装 HAMi:
helm repo add hami-charts https://project-hami.github.io/HAMi/
helm repo update hami-charts
helm pull hami-charts/hami --version 2.9.0 --untar
cd hami
K8S_VERSION="$(kubectl version -o json | jq -r '.serverVersion.gitVersion')"
GPU_NODE="$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')"
helm install hami . -n kube-system \
--set-string scheduler.kubeScheduler.image.tag="${K8S_VERSION}" \
--set-string scheduler.nodeName="${GPU_NODE}" \
--set-string scheduler.extender.image.registry=m.daocloud.io/docker.io \
--set-string scheduler.patch.image.registry=m.daocloud.io/docker.io \
--set-string scheduler.patch.imageNew.registry=registry.cn-hangzhou.aliyuncs.com \
--set-string scheduler.patch.imageNew.repository=google_containers/kube-webhook-certgen \
--set-string scheduler.patch.imageNew.tag=v1.1.1 \
--set devicePlugin.enabled=true \
--set-string devicePlugin.image.registry=m.daocloud.io/docker.io \
--set-string devicePlugin.monitor.image.registry=m.daocloud.io/docker.io \
--set-string devicePlugin.runtimeClassName=nvidia-legacy \
--set devicePlugin.gpuOperatorToolkitReady.enabled=true \
--set devicePlugin.deviceSplitCount=10 \
--set devicePlugin.deviceMemoryScaling=1 \
--set devicePlugin.deviceCoreScaling=1 \
--set-string mockDevicePlugin.image.registry=m.daocloud.io/docker.io \
--wait --timeout 10m本机部署时需要保留的兼容配置:
nvidia-legacy:本机使用 GPU Operator 26.7.2。使用nvidiaRuntimeClass 时,按 GPU UUID 分配的容器曾报unresolvable CDI devices management.nvidia.com/gpu=GPU-...;改用已有的nvidia-legacy后正常。这里只选择容器运行时,不关闭集群的 CDI,也不修改驱动或 Toolkit。scheduler.nodeName:仅将 HAMi 调度器自身固定到当前单节点,避免 Chart 2.9.0 的硬反亲和规则阻塞滚动更新。多节点环境应重新评估此设置;业务 GPU Pod 不要设置nodeName,否则会绕过 HAMi 调度。- 证书 Job 使用阿里云
kube-webhook-certgen:v1.1.1,其镜像清单摘要已与原镜像核对一致,避免原 DaoCloud 路径返回 502。 deviceSplitCount=10表示每张物理卡最多提供 10 个逻辑分配份额;显存和算力缩放均为1,本次不做超卖。
查看启动状态:
kubectl rollout status deployment/hami-scheduler -n kube-system --timeout=3m
kubectl rollout status daemonset/hami-device-plugin -n kube-system --timeout=3m
kubectl get pods -n kube-system -l app.kubernetes.io/name=hami查看注册的 GPU 资源信息:
kubectl get node "${GPU_NODE}" \
-o jsonpath='{.status.allocatable.nvidia\.com/gpu}{"\n"}'本文单张 A10 输出 10。这不是 10 张物理卡,也不是把显存平均切成了 10 份;每个 Pod 的显存额度通过 gpumem 单独指定。
查看物理卡信息:
nvidia-smi --query-gpu=name,uuid,memory.total --format=csv1.5.2 GPU 调度测试
创建测试 Pod。沿用已缓存的 vLLM 镜像,并显式申请 1 GiB 显存,避免测试任务申请整卡显存。首次切换时 vLLM 仍保持停止;已经完成 1.5.5 的集群,也可以在有剩余显存额度时运行此测试。
tee 1-gpu-test.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: hami-gpu-test
namespace: models
labels:
app: hami-gpu-test
spec:
schedulerName: hami-scheduler
runtimeClassName: nvidia-legacy
restartPolicy: Never
containers:
- name: hami-gpu-test
imagePullPolicy: IfNotPresent
image: registry.cn-beijing.aliyuncs.com/dotbalo/vllm-openai@sha256:3a1e7f5904e1a1192a02aa0086ceaffc33985d7044c7bb25b3a43d61bdbe3ac0
command: ["nvidia-smi"]
resources:
limits:
nvidia.com/gpu: 1
nvidia.com/gpumem: 1024
EOF
kubectl create -f 1-gpu-test.yaml
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded \
pod/hami-gpu-test -n models --timeout=3m查看日志:
kubectl logs hami-gpu-test -n models
kubectl get pod hami-gpu-test -n models \
-o jsonpath='{.spec.schedulerName}{"\n"}{.metadata.annotations.hami\.io/vgpu-devices-allocated}{"\n"}'
kubectl delete pod hami-gpu-test -n models注意:
- Pod 中的
nvidia.com/gpu: 1表示申请访问一张物理 GPU、消耗其一个逻辑份额,不代表固定大小的“十分之一张卡”。本机只写gpu: 1而不指定显存时,默认申请整卡显存;本节通过显式设置gpumem实现显存共享。 resources必须放在容器下面,不能与containers平级;环境变量同样属于容器字段。- GPU 扩展资源的 request 和 limit 应相同;只写 limit 时,Kubernetes 会将其作为 request。
nvidia-smi用于检查驱动、设备可见性;隔离是否生效,还要结合下面的 CUDA 可见显存和超额分配测试判断。
1.5.3 分配指定显存
用 nvidia.com/gpumem 指定每张卡的显存额度,单位是 MiB,值写整数,不加 Gi 后缀。例如:
resources:
limits:
nvidia.com/gpu: 1
nvidia.com/gpumem: 1024本机只有一张 A10,因此不能写 gpu: 2。多卡机器上的 gpu: 2, gpumem: 3000 才表示访问两张不同的物理卡,每张分配 3000 MiB。Node 上显示的 10 个逻辑份额不改变这个约束。
创建两只持续运行的测试 Pod,每只申请 1024 MiB 显存,验证它们可以共用一张卡。测试会执行 CUDA 矩阵乘法,并尝试申请 1536 MiB,预期被 1024 MiB 限制拒绝。
tee hami-share-test.py <<'PY'
import json
import os
import time
import torch
x = torch.ones((1024, 1024), device="cuda")
y = x @ x
torch.cuda.synchronize()
assert y[0, 0].item() == 1024.0
rejected = False
try:
oversized = torch.empty(1536 * 1024 * 1024, dtype=torch.uint8, device="cuda")
except torch.OutOfMemoryError:
rejected = True
assert rejected, "HAMi memory limit not enforced"
print(json.dumps({
"gpu_uuid": os.environ.get("NVIDIA_VISIBLE_DEVICES"),
"visible_memory_mib": torch.cuda.get_device_properties(0).total_memory // (1024 ** 2),
"matmul": y[0, 0].item(),
"over_limit_rejected": rejected,
}), flush=True)
time.sleep(600)
PY
TEST_CODE="$(<hami-share-test.py)"
for POD_NAME in hami-share-a hami-share-b; do
jq --arg name "${POD_NAME}" --arg code "${TEST_CODE}" '
.metadata.name = $name |
.spec.terminationGracePeriodSeconds = 5 |
.spec.containers[0].command = ["python3", "-u", "-c", $code]
' < <(kubectl create -f 1-gpu-test.yaml --dry-run=client -o json) \
| kubectl create -f -
done
kubectl wait --for=condition=Ready -n models \
pod/hami-share-a pod/hami-share-b --timeout=3m
kubectl logs hami-share-a -n models
kubectl logs hami-share-b -n models
kubectl get pod hami-share-a hami-share-b -n models \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.annotations.hami\.io/vgpu-devices-allocated}{"\n"}{end}'Pod Ready 只表示进程已启动,还应等待两份日志都输出 JSON:两只 Pod 的 GPU UUID 相同,visible_memory_mib 都为 1024,matmul 为 1024.0,over_limit_rejected 为 true。日志中的 HAMI-core ... OOM 是主动申请超额显存触发的预期结果,不是模型服务故障。测试完成后及时清理,避免占用配额:
kubectl delete pod hami-share-a hami-share-b -n models负向测试:下面的 Pod 申请 32768 MiB,超过本机 A10 的 24564 MiB,不应被调度。检查到 FailedScheduling 和 FilteringFailed: CardInsufficientMemory,Pod 仍为 Pending 且未绑定节点后删除:
kubectl create -f 1-gpu-test.yaml --dry-run=client -o json \
| jq '.metadata.name = "hami-too-much-memory" |
.spec.containers[0].resources.limits["nvidia.com/gpumem"] = "32768"' \
| kubectl create -f -
kubectl wait --for=condition=PodScheduled=false \
pod/hami-too-much-memory -n models --timeout=1m
kubectl describe pod hami-too-much-memory -n models
kubectl delete pod hami-too-much-memory -n models注意:
- 本文没有开启显存超卖,同卡上所有活动任务的显存申请总量不能超过卡的可分配显存。
nvidia.com/gpumem-percentage: 50可申请半张卡的显存,但不能与gpumem同时配置。本文使用固定 MiB 值,便于核算。- 本节只验证调度、CUDA 运算和显存限制,不验证算力隔离或性能。
1.5.4 指定显卡类型调度
HAMi 支持用 Pod annotation 选择 GPU 型号。本文机器是 A10,使用白名单 A10:
metadata:
annotations:
nvidia.com/use-gputype: "A10"
# nvidia.com/use-gputype: "A10,A100" # 多个允许的型号用逗号分隔
# nvidia.com/nouse-gputype: "V100" # 排除指定型号Deployment 中的 annotation 要放在 spec.template.metadata.annotations,只写在 Deployment 自身的 metadata.annotations 不会传给 Pod。GPU 型号筛选也不能绕过 vLLM 自身的硬件要求,例如 V100 仍不满足前文的 CC ≥ 7.5。
先查看真实型号,再做正向、负向测试:
nvidia-smi -L本机实际只有 GPU 0: NVIDIA A10,不是两张卡,也不是 Ada Generation。
kubectl create -f 1-gpu-test.yaml --dry-run=client -o json \
| jq '.metadata.name = "hami-type-a10" |
.metadata.annotations = {"nvidia.com/use-gputype": "A10"}' \
| kubectl create -f -
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded \
pod/hami-type-a10 -n models --timeout=3m
kubectl logs hami-type-a10 -n models
kubectl delete pod hami-type-a10 -n models
kubectl create -f 1-gpu-test.yaml --dry-run=client -o json \
| jq '.metadata.name = "hami-type-a100" |
.metadata.annotations = {"nvidia.com/use-gputype": "A100"}' \
| kubectl create -f -
kubectl wait --for=condition=PodScheduled=false \
pod/hami-type-a100 -n models --timeout=1m
kubectl describe pod hami-type-a100 -n models
kubectl delete pod hami-type-a100 -n models预期 A10 测试成功;A100 测试保持 Pending、未绑定节点,并出现 FailedScheduling 和 FilteringFailed: CardTypeMismatch。仅看到 Pending 不足以证明型号筛选生效,应结合事件及 HAMi 调度器日志确认原因。
1.5.5 VLLM 测试
在已有 Deployment 上做局部更新,保留模型 PVC、Secret、镜像、探针和 Service。本文配置为 20 GiB 显存,给其他小任务留出显存空间,不设置显式算力配额;--gpu-memory-utilization 0.95 按容器可见的 20 GiB 计算,vLLM 的目标显存预算约为 19 GiB,并非物理卡显存的 95%。
单卡服务继续使用 Recreate,更新时会短暂不可用。先保存当前配置,再创建战略合并补丁;容器按名称匹配,resources 使用 $patch: replace 完整替换,避免已有的旧资源配额残留。若已额外配置 CPU、内存等资源,需要在下面的资源块中一并保留:
kubectl get deployment qwen3-5-4b-deployment -n models -o json \
> qwen35-vllm.before-hami.json
tee qwen35-vllm-hami.patch.yaml <<'EOF'
spec:
strategy:
type: Recreate
template:
metadata:
annotations:
nvidia.com/use-gputype: "A10"
spec:
schedulerName: hami-scheduler
runtimeClassName: nvidia-legacy
containers:
- name: qwen3-5-4b
args:
- "/data/modelscope/Qwen/Qwen3.5-4B"
- "--port"
- "8080"
- "--served-model-name"
- "Qwen3.5-4B"
- "--gpu-memory-utilization"
- "0.95"
- "--max-model-len"
- "16384"
- "--max-num-batched-tokens"
- "16384"
resources:
$patch: replace
limits:
nvidia.com/gpu: 1
nvidia.com/gpumem: 20480
requests:
nvidia.com/gpu: 1
nvidia.com/gpumem: 20480
EOF
kubectl patch deployment qwen3-5-4b-deployment -n models \
--type=strategic --patch-file=qwen35-vllm-hami.patch.yaml
kubectl scale deployment/qwen3-5-4b-deployment -n models --replicas=1
kubectl rollout status deployment/qwen3-5-4b-deployment -n models --timeout=15m查看调度结果、注入的额度和容器可见显存:
POD="$(kubectl get pod -n models -l app=qwen3-5-4b -o jsonpath='{.items[0].metadata.name}')"
kubectl get pod "${POD}" -n models \
-o jsonpath='{.spec.schedulerName}{"\n"}{.spec.runtimeClassName}{"\n"}{.metadata.annotations.hami\.io/vgpu-devices-allocated}{"\n"}'
kubectl exec -n models "${POD}" -- sh -c \
'env | grep -E "^(CUDA_DEVICE_MEMORY_LIMIT|NVIDIA_VISIBLE_DEVICES)"'
kubectl exec -n models "${POD}" -- python3 -c \
'import torch; print(torch.cuda.get_device_properties(0).total_memory // (1024 ** 2))'
kubectl exec -n models "${POD}" -- nvidia-smi
nvidia-smi --query-gpu=name,memory.total,memory.used --format=csv预期 scheduler 为 hami-scheduler,runtime 为 nvidia-legacy,分配注解包含 NVIDIA,20480,0(默认不申请固定算力配额),CUDA 可见显存为 20480 MiB。宿主机仍能看到物理卡的完整显存。两者不是同一层的统计,不应混用。
模型 Ready 后,重新执行 vLLM 调用验证 和 LiteLLM 调用验证。还可重跑 1.5.3 的两只共享测试 Pod,同时调用模型:20 GiB 的模型加两只 1 GiB 测试任务应能共用同一张 A10;完成后删除测试 Pod,只保留模型服务。