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 高可用落地

image.png

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