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 机器,可以在公有云上临时申请。
- 阿里云: https://aliyun.com/
- 腾讯云: https://cloud.tencent.com/
- 华为云: https://www.huaweicloud.com/
rockylinux9.8,24GB显存,10MB公网固定带宽,100GB磁盘
1.1.2 驱动安装
下载地址: https://www.nvidia.com/en-us/drivers/unix/
选择最新版下载即可(Linux x64/AMD64/EM64T 或 Linux aarch64 对应版本)。
下载完成后,把安装包上传到 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
# 安装完成后,重启服务器
reboot
驱动安装完成后,执行 nvidia-smi 命令检查是否安装成功:
nvidia-smi
1.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 -y
1.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 containerd
1.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.* kubelet-1.36.* kubectl-1.36.* -y
systemctl enable --now kubelet
kubeadm config images pull \
--image-repository registry.cn-hangzhou.aliyuncs.com/google_containers --kubernetes-version 1.36.0
# 初始化集群
kubeadm init --apiserver-advertise-address xxxx --image-repository registry.cn-hangzhou.aliyuncs.com/google_containers --cri-socket "unix:///var/run/containerd/containerd.sock" --kubernetes-version 1.36.0
# 加入其它节点
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/
kubectl create -f .
解除污点:
kubectl taint node node-role.kubernetes.io/control-plane- --all
查看集群状态:
kubectl top node
kubectl get node
1.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
1.2.2.2 部署 GPU Operator
添加仓库:
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia \
&& helm repo update
下载安装包:
helm pull nvidia/gpu-operator
tar xf gpu-operator-*.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 重复安装驱动,避免覆盖现有驱动或产生版本冲突。当前 Kubernetes 使用 Containerd,因此同时明确指定 operator.defaultRuntime=containerd:
helm install gpu-operator . \
-n gpu-operator \
--create-namespace \
--set driver.enabled=false \
--set operator.defaultRuntime=containerd
查看 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 实例数已达到上限。先确认限制和当前用量,再提高限制并重建 device-plugin Pod:
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-cuda11.7.1-ubuntu20.04"
resources:
limits:
nvidia.com/gpu: 1
EOF
kubectl create -f test.yaml
查看日志:
kubectl logs cuda-vectoradd
1.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
EOF
kubectl 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: registry.cn-beijing.aliyuncs.com/dotbalo/nginx:1.15.12
imagePullPolicy: IfNotPresent
volumeMounts:
- name: volv
mountPath: /usr/share/nginx/html
ports:
- containerPort: 80
volumes:
- name: volv
persistentVolumeClaim:
claimName: longhorn-redis-pvc
EOF
kubectl create -f longhorn-pod-test.yaml
写入数据测试:
kubectl get po
kubectl exec -ti volume-test -- bash
1.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: izuf62nmmrsj356g2ju67ez
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 并等待完成:
kubectl apply -f model-import-job.yaml
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的第一个位置参数。 - 单节点只有一块 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:latest
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: CUDA_VISIBLE_DEVICES
value: "0"
- name: NVIDIA_VISIBLE_DEVICES
value: "all"
- name: VLLM_USE_MODELSCOPE
value: "true"
- name: MODELSCOPE_CACHE
value: "/data/modelscope"
- 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,在标准输出管道中替换占位符并直接提交给 API Server。由于生成的是十六进制字符串,可以安全地作为 sed 替换值;不要直接执行 kubectl apply -f qwen35-vllm.yaml,否则会把占位符保存为真实密钥:
DEPLOYMENT_EXISTS=false
kubectl get deployment qwen3-5-4b-deployment -n models >/dev/null 2>&1 \
&& DEPLOYMENT_EXISTS=true
VLLM_API_KEY="$(openssl rand -hex 32)"
sed "s/VLLM_API_KEY_PLACEHOLDER/${VLLM_API_KEY}/g" qwen35-vllm.yaml \
| kubectl apply -f -
unset VLLM_API_KEY
# 现有部署轮换 Secret 后,必须重建 Pod 才能读取新的环境变量
if [ "${DEPLOYMENT_EXISTS}" = true ]; then
kubectl rollout restart deployment/qwen3-5-4b-deployment -n models
fi
unset DEPLOYMENT_EXISTS
kubectl rollout status deployment/qwen3-5-4b-deployment \
-n models \
--timeout=15m
查看启动状态:
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_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://127.0.0.1:${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_KEY
1.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 缓存(general_settings.cache)。redis-single:单节点 Redis,用作 LiteLLM 路由限流(router_settings)。当前限流 Redis 按单节点方式接入;后续 LiteLLM 支持 Sentinel 后,可再评估共用缓存 Redis。
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 > "${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 10m
global.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:
REDIS_PASSWORD=$(kubectl get secret redis-sentinel-auth -n litellm \
-o jsonpath='{.data.redis-password}' | base64 -d)
kubectl exec redis-node-0 -n litellm -c redis -- \
env REDISCLI_AUTH="${REDIS_PASSWORD}" redis-cli --raw \
-h redis -p 26379 SENTINEL get-master-addr-by-name mymaster
kubectl exec redis-node-0 -n litellm -c redis -- \
env REDISCLI_AUTH="${REDIS_PASSWORD}" redis-cli --raw \
-h redis -p 26379 SENTINEL ckquorum mymaster
kubectl exec redis-node-0 -n litellm -c redis -- \
env REDISCLI_AUTH="${REDIS_PASSWORD}" sh -c \
'redis-cli --raw -h redis -p 26379 SENTINEL replicas mymaster | grep -c "^name$"'
kubectl exec redis-node-0 -n litellm -c redis -- \
env REDISCLI_AUTH="${REDIS_PASSWORD}" redis-cli --raw \
-h redis -p 6379 SET litellm-sentinel-test ok
kubectl exec redis-node-0 -n litellm -c redis -- \
env REDISCLI_AUTH="${REDIS_PASSWORD}" redis-cli --raw \
-h redis -p 6379 GET litellm-sentinel-test
kubectl exec redis-node-0 -n litellm -c redis -- \
env REDISCLI_AUTH="${REDIS_PASSWORD}" redis-cli --raw \
-h redis -p 6379 DEL litellm-sentinel-test
unset REDIS_PASSWORD
供 LiteLLM 使用的集群内地址为:
- Sentinel:
redis.litellm.svc.cluster.local:26379 - Sentinel master set:
mymaster - Redis 服务:
redis.litellm.svc.cluster.local:6379
单节点 Redis - 限流用
使用同一个 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
REDIS_PASSWORD=$(kubectl get secret redis-standalone-auth -n litellm \
-o jsonpath='{.data.redis-password}' | base64 -d)
kubectl exec redis-single-master-0 -n litellm -c redis -- \
env REDISCLI_AUTH="${REDIS_PASSWORD}" redis-cli --raw \
SET litellm-rate-limit-test ok
kubectl exec redis-single-master-0 -n litellm -c redis -- \
env REDISCLI_AUTH="${REDIS_PASSWORD}" redis-cli --raw \
GET litellm-rate-limit-test
kubectl exec redis-single-master-0 -n litellm -c redis -- \
env REDISCLI_AUTH="${REDIS_PASSWORD}" redis-cli --raw \
DEL litellm-rate-limit-test
unset REDIS_PASSWORD
供 LiteLLM 限流使用的集群内地址为 redis-single-master.litellm.svc.cluster.local:6379。
1.4.2 高可用 Postgresql 部署
在 Kubernetes 集群中按照高可用 PG 数据库:
tar xf postgresql-ha.tar.gz
cd postgresql-ha/
查看状态:
kubectl get po -n litellm
链接测试:
kubectl exec -ti pg-postgresql-ha-pgpool-74f76c59fd-gkgs9 -n litellm -- bash
I have no name!@pg-postgresql-ha-pgpool-74f76c59fd-gkgs9:/$ psql -U postgres -h pg-postgresql-ha-pgpool
Password for user postgres: postgres_litellm
postgres=# \l
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | ...
----------+----------+----------+------------------+------------+------------+-----
litellm | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 |
1.4.3 部署 LiteLLM
部署 LiteLLM:
kubectl create -f litellm-deploy.yaml -n litellm
查看状态:
kubectl get po -n litellm
访问测试:
kubectl get svc -n litellm
登录 LiteLLM 后台后,接下来就可以添加之前部署的模型服务,和基础课程添加过程一致,将 API Base 填写为 K8s Service 地址(如 http://qwen3-5-4b-service.models:8080/v1),API Key 填写为 xxxx。
使用 LiteLLM 调用模型测试:
kubectl get svc -n litellm
curl -H "Authorization: Bearer sk-tvE6JKo-Q54_dkQIdXYnlg" -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": "介绍下你自己"}]}'
1.4.4 限流测试
LiteLLM 支持如下三种限流方案:
- TPM:每分钟 Token 数量限制
- RPM:每分钟请求数量限制
- Max Parallel Requests:最大并发量限制
其中 TPM 和 RPM 分为了三个限制方法:
- Best effort:系统会尽可能多地处理请求,不强制拦截超出限制的请求
- Guaranteed:系统会严格进行限制,确保不超过设定的限制值
- Dynamic:动态调整,系统根据实时负载、历史调用等状态,动态调整限制规则
比如限制某个 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 sk-tvE6JKo-Q54_dkQIdXYnlg" -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 sk-tvE6JKo-Q54_dkQIdXYnlg" -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 虚拟化
1.5.1 HAMI 部署
官方文档: https://project-hami.io/zh/docs/installation/prerequisites
HAMI 管理 GPU 机器,需要先给 GPU 机器添加 gpu=on 的标签:
kubectl label nodes {nodeid} gpu=on
HAMI 的安装依赖当前 K8s 的版本,所有先获取当前的 K8s 版本:
kubectl version
安装 HAMI:
helm repo add hami-charts https://project-hami.github.io/HAMi/
helm pull hami-charts/hami
# 解压安装包,修改镜像配置
sed -i 's#docker.io#m.daocloud.io/docker.io#g' values.yaml
helm install hami . --set scheduler.kubeScheduler.imageTag=v1.35.3 -n kube-system
查看启动状态:
kubectl get pods -n kube-system | grep hami
查看注册的 GPU 资源信息:
kubectl describe node | grep nvidia.com/gpu:
nvidia.com/gpu: 20 # 每个 GPU 卡*10
1.5.2 GPU 调度测试
创建测试 Pod:
tee 1-gpu-test.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: hami-gpu-test
labels:
app: hami-gpu-test
spec:
containers:
- name: hami-gpu-test
imagePullPolicy: IfNotPresent
image: registry.cn-beijing.aliyuncs.com/dotbalo/vllm-openai:latest
command:
- sh
- -c
- nvidia-smi
restartPolicy: OnFailure
env:
- name: TZ
value: "Asia/Shanghai"
- name: LANG
value: "C.UTF-8"
- name: LC_ALL
value: "C.UTF-8"
resources:
limits:
nvidia.com/gpu: 1
EOF
查看日志:
kubectl logs -f hami-gpu-test
上述分配了 1 个 vGPU,但是为指定显存和核心数,所以会独占一个显卡,和无虚拟化一致。
1.5.3 分配指定显存
假如需要控制每个显卡使用的显存大小,可以添加 gpumem 参数:
resources:
limits:
nvidia.com/gpu: 2
nvidia.com/gpumem: 3000
以上代表分配两个显卡,每个显卡分配 3000Mi 显存。创建后查看日志:
nvidia-smi
注意:nvidia.com/gpu 和 nvidia.com/gpumem 不能超过物理机真实显卡数量和显存大小。
显存大小也可以按照百分比分配:nvidia.com/gpumem-percentage: 50。
1.5.4 指定显卡类型调度
HAMI 支持不同显卡级别的调度,比如某个任务需要 A100,可以添加如下注释:
metadata:
annotations:
nvidia.com/use-gputype: "A100,V100" # 多种类型逗号分割
# nvidia.com/nouse-gputype: "1080,2080" # 指定黑名单
设备类型可以通过 nvidia-smi 命令查看:
nvidia-smi -L
GPU 0: NVIDIA A10 Ada Generation
GPU 1: NVIDIA A10 Ada Generation
1.5.5 VLLM 测试
修改之前的 vllm 启动文件,添加显存配置:
resources:
limits:
nvidia.com/gpu: 1
nvidia.com/gpumem: 30720
requests:
nvidia.com/gpu: 1
nvidia.com/gpumem: 30720
修改显存使用率为 0.95:
- "--gpu_memory_utilization"
- "0.95"
查看启动状态:
kubectl get po -n models -owide
查看显存占用:
nvidia-smi