☰
Kubernetes上生成式AI生产级落地:GPU调度、vLLM推理与自动伸缩实战
2026/10/15 12:16:04 网站建设 项目流程

做生成式 AI 的同学,这两年应该都有一个共同感受:模型越来越重,推理链路越来越复杂,单机玩不转了。前四篇我们聊了容器化基础、GPU 驱动、模型镜像、以及简单的推理服务部署,这一篇我打算把视角拉高,聊聊真正把生成式 AI 工作负载稳定跑在 Kubernetes 上的完整实战经验。

这篇文章的定位是“进阶落地篇”。你如果已经有一个 Kubernetes 集群,或者正准备搭建,想在上面跑 LLM 推理、微调、甚至多模型服务,那这篇文章就是给你写的。我会从集群初始化开始,讲到推理服务部署、自动伸缩、调度策略,最后是排障实录。整个过程都是我这几年在真实环境里一点一点踩出来的,不是官方文档的复读。

还是老规矩,先交代一下我用的环境:Kubernetes v1.26.0,容器运行时是 containerd,GPU 是 NVIDIA A100 和 L40S 混布,推理框架以 vLLM 和 TGI 为主。这套组合在 2024 到 2025 年这个时间点,基本就是生产环境的标配了。

1. 整体设计与方案选型思路

1.1 为什么非要用 Kubernetes 跑生成式 AI

很多人一开始会觉得,跑大模型不就是一个 GPU 服务器 + 一个推理进程吗?直接 systemd 拉起来不就行了?我承认,单机单卡跑一个小模型,确实不用 Kubernetes。但只要你进入生产环境,事情就变味了。

第一,生成式 AI 服务天然有流量波动。白天业务高峰,可能几百个并发请求,晚上可能就寥寥无几。如果用裸机,你就得按峰值预留 GPU,平时大部分时间都在浪费。Kubernetes 配合自动伸缩,可以按需申请 GPU 资源,省下的都是真金白银。

第二,模型版本迭代太快。LLM 几乎每周都有新 checkpoint,你不可能每次更新都手动登录服务器改配置。Kubernetes 的滚动更新机制,配合镜像仓库,可以把“模型发布”变成一个标准的 DevOps 流程。

第三,多模型、多租户的场景,资源隔离和调度是硬需求。一个集群里可能同时跑着 ChatGLM、Llama、Stable Diffusion、Embedding 模型,它们的优先级、资源配额、显存需求都不一样。裸机环境下你想做这种资源池化,基本靠人肉运维,Kubernetes 天生就是干这个的。

所以我一直跟团队说一句话:Kubernetes 不是银弹,但它是当前跑生成式 AI 工作负载最成熟的“操作系统”。你可以不用它的某些能力,但没必要从零造轮子。

1.2 系列前四篇的铺垫与这一篇的定位

这里简单回顾一下前四篇做了什么,方便新读者接上。

前四篇我们依次解决了这几个问题:第一篇是容器化基础,把 PyTorch 和 CUDA 环境装进镜像;第二篇是 GPU 驱动和容器运行时打通,让容器能真正用到宿主机显卡;第三篇是模型镜像的构建与仓库管理,解决“模型和代码一起发布”的问题;第四篇是单推理服务的 Deployment 部署,以及简单的 Service 暴露。

这一篇我们要做的是“从单服务到生产级平台”的跨越。需要把之前零散的能力串起来,形成一个能应对真实业务压力的整体架构。

具体来说,这一篇会重点解决四件事:集群初始化时 GPU 节点怎么加进去、推理服务怎么优雅地跑起来并对外提供稳定能力、流量变化时怎么自动伸缩而不抖动、以及出问题时怎么快速定位。这四件事,恰好对应我后面的四个章节。

1.3 整体架构:控制平面与 GPU 节点池分离

生产环境的架构,我强烈建议控制平面和 GPU 工作节点分离,不要搞 all-in-one。

控制平面节点跑 kube-apiserver、etcd、controller-manager、scheduler、coreDNS 这些系统组件,它们对 CPU 和内存的稳定性要求很高。GPU 节点则专注跑推理、微调任务,需要安装额外的驱动和运行时。

我当前的集群架构大概是这样:

  • 3 台控制平面节点:8C16G 虚拟机,跑 kubeadm 高可用,etcd 独立在三块 SSD 上。
  • 4 台 GPU 节点:每台 2 张 A100 或 4 张 L40S,512G 内存,系统盘和模型盘分离。
  • 3 台 CPU 节点:跑日志收集、Ingress Controller、监控组件、对象存储网关等辅助服务。
  • 共享存储:JuiceFS + 对象存储,用来放模型文件和训练数据。

这个架构的核心思路是“各司其职”。GPU 节点不承担控制面压力,控制面节点不装 GPU 驱动,辅助服务不占用 GPU 节点资源。后续扩展时,只需要往 GPU 节点池里加机器即可。

1.4 方案选型:推理框架与运行时

推理框架的选择上,目前生产环境主流是两个:vLLM 和 Hugging Face TGI。我两个都用过,说说实际感受。

vLLM 胜在吞吐量高、社区活跃、兼容 OpenAI API 协议,PagedAttention 对显存利用率的提升非常明显。如果你主要是做 LLM 的 OpenAI 兼容接口,vLLM 是首选。

TGI 的优点是部署简单、和 Hugging Face 生态集成度极高,但性能和 vLLM 相比,在长上下文场景下略有差距。

我的选择是:核心对话模型用 vLLM,小规模的 Embedding 和重排模型用 TorchServe 或者直接 Python 服务。Stable Diffusion 类的文生图服务用专属镜像,不用 vLLM——因为 vLLM 目前还是以文本模型为主。

另外,微调场景我用的是 SGLang 和 DeepSpeed,调度模板我会在后面的章节里展开说。

2. 集群初始化与 GPU 节点接入实录

2.1 版本选型:为什么锁在 v1.26.0 不升

先把最容易忽视的版本问题说透。很多新手上来就装最新版 Kubernetes,结果发现和 GPU 驱动、CSI 驱动、CRI 兼容性对不上,折腾一星期。

我当前集群用的是 v1.26.0,这是经过大量生产验证的稳定版本。kubeadm init 时的输出里能看到[init] using kubernetes version: v1.26.0,这个版本对 NVIDIA GPU 插件和 containerd 的兼容性都很好。

选版本有一个非常朴素的原则:不要追新,看主流云厂商托管集群的默认版本。比如当时主要云厂商托管版大量停留在 1.24 到 1.28 之间,说明这个区间的组件生态是最成熟的。v1.26.0 刚好在这个区间里,而且 CRI 已经是 containerd 的天下了,Docker 时代的兼容包袱已经卸掉了。

2.2 kubeadm init 的 preflight 检查与常见坑

第一次跑kubeadm init的人,大概率会被 preflight 检查卡住。这一步是 kubeadm 在正式初始化前做的系统预检,检查项非常细。我遇到过的问题可以列个表:

检查项失败原因解决方案
swap 检查系统开了 swapswapoff -a并注释 /etc/fstab 里的 swap 行
CRI 检查containerd 未配置或 socket 路径不对确认 containerd 已启动,crictl info能正常输出
端口占用6443、2379、2380 等端口被占ss -lntp查占用进程,释放端口
内核模块检查br_netfilter、overlay 未加载modprobe overlay br_netfilter,写进 /etc/modules-load.d/
系统参数检查net.bridge.bridge-nf-call-iptables = 0写入 /etc/sysctl.d/k8s.conf 并sysctl --system

preflight 失败不可怕,可怕的是你忽略它强行继续。--ignore-preflight-errors=all这个参数我强烈不建议在生产环境使用——它只是把问题藏起来了,后面大概率会在更糟糕的时机爆出来。

2.3 控制平面初始化与工作节点加入

控制平面初始化,我是用配置文件方式执行的,方便后续高可用扩展。核心配置节选如下:

apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.26.0 controlPlaneEndpoint: "10.10.0.10:6443" networking: podSubnet: "10.244.0.0/16" serviceSubnet: "10.96.0.0/16"

执行kubeadm init --config=kubeadm-config.yaml,然后按输出执行 kubeconfig 配置命令,再安装 Calico 网络插件。这里注意,podSubnet 要和 Calico 的 IP 池保持一致,否则 Pod 网络会出问题。

工作节点加入集群的命令是kubeadm join,token 有效期默认 24 小时。我习惯把 token 信息保存到本地文件,避免过期后还要重新生成:

kubeadm token create --print-join-command > join-command.txt

GPU 节点加入集群前,我一定会先做一件事:给节点打标签gpu-node=true,并且加上 GPU 资源相关的污点nvidia.com/gpu=:NoSchedule。这样后续只有声明了 GPU 需求的 Pod 才能调度到这些节点上,普通业务 Pod 不会跑上来抢资源。

2.4 NVIDIA 容器运行时:让 Pod 用上 GPU

Kubernetes 本身不认识 GPU,它只认扩展资源。要让 Pod 能使用 GPU,需要安装 NVIDIA Device Plugin,同时把容器运行时切换成 nvidia-container-runtime。

具体步骤我简化成三条:

  1. 安装 NVIDIA 驱动(宿主机层面),执行nvidia-smi能正常输出。
  2. 安装 nvidia-container-toolkit,配置 containerd 使用 nvidia 运行时。
  3. 部署 NVIDIA Device Plugin,让每个 GPU 变成 Kubernetes 的扩展资源nvidia.com/gpu。

这里最容易踩的坑,是 containerd 的配置文件修改后没有重启生效。你改了 config.toml,必须systemctl restart containerd,然后再把 kubelet 重启,让 kubelet 重新上报资源。

还有一个隐蔽的坑:GPU 节点上有多个驱动版本,或者驱动与 CUDA 版本不匹配,会导致 Device Plugin 报错。排查时先不要看 Kubernetes,先在宿主机上确认nvidia-smi是否正常,再crictl run一个含 CUDA 的测试容器验证。我手头常备一个nvidia/cuda:12.1.0-base镜像,专门用来做这个验证。

2.5 存储规划:模型文件放哪里

生成式 AI 工作负载对存储的要求,和传统业务不一样。最大的特点就是:模型文件巨大,动辄几十 GB 甚至上百 GB,而且加载进显存需要高吞吐的读性能。

我的存储分层策略是这样的:

  • 模型文件主存储:JuiceFS 挂载到 GPU 节点,底层是对象存储,本地可用内存/磁盘做缓存。好处是多个节点共享同一份模型,不会每台机器都存一份副本。
  • 临时训练数据:本地 NVMe 临时目录,用emptyDir 挂载进 Pod。训练 checkpoint 定期同步到共享存储。
  • 日志与指标:直接打到宿主机磁盘,通过 Filebeat 采集,不进共享存储。

共享存储的挂载,我用的是 CSI Driver。JuiceFS 的 CSI 插件配置起来不算复杂,但要注意挂载参数里cache-dir的路径要落在 SSD 上,否则下载模型的速度会受限于磁盘 IO。

3. 生成式 AI 工作负载的部署实践

3.1 推理服务的镜像构建

模型推理镜像和普通业务镜像最大的区别是:镜像体积巨大,基础镜像动辄几个 GB,加上依赖和模型文件,可能超过 10GB。所以镜像构建的优化空间非常大。

先说我的分三层构建策略:

第一层,基础运行环境层。Python 3.10 + CUDA 12.1 + PyTorch 2.1,这一层基本不变,单独打一个镜像 tag,比如llm-base:cu121-py310。所有推理服务都基于它构建。

第二层,推理框架层。把 vLLM 或者 TGI 装好,锁版本。这一步依赖较多,单独一层可以充分利用镜像缓存,避免每次改代码都重新装框架。

第三层,业务代码层。只 COPY 推理脚本、配置文件、prompt 模板。这一层变化最频繁,但基础层缓存命中后,构建时间能控制在半分钟以内。

模型文件我不建议打进镜像,而是通过共享存储挂载。原因有两个:一是镜像仓库传输 20GB 的镜像是灾难,二是模型更新频率和代码不一致,强行绑在一起会导致镜像版本爆炸。

3.2 Deployment 还是 StatefulSet

第一个问题:跑推理服务,用 Deployment 还是 StatefulSet?

我见过不少人直接用 Deployment,结果发现一个问题:Pod 重建后 IP 变了,而且没有稳定的标识。对于无状态的 HTTP 推理服务,Deployment 确实够用,配合 Service 负载均衡即可。

但如果你有 WebSocket 长连接、或者需要稳定的 RPC 通信(比如 gRPC),我建议用 StatefulSet。它给每个 Pod 一个稳定的序号和网络标识,比如model-llm-0.model-llm-headless.namespace.svc,客户端可以做精确的连接管理。

还有一类场景:微调任务。这种短生命周期、需要跑完就退出的工作负载,应该用 Job 而不是 Deployment。比如用 DeepSpeed 跑 LoRA 微调,提交一个 Job,跑完看日志,然后删掉即可。

我的实际选择是:在线推理用 Deployment,离线批量推理用 Job,需要稳定连接的用 StatefulSet。

3.3 关键配置:vLLM 推理服务的 Deployment 示例

下面这个配置是我生产环境里一个 Llama-3-8B 服务的实际 Deployment,做了必要脱敏。可以直接参考改改参数就能用。

apiVersion: apps/v1 kind: Deployment metadata: name: llama3-8b-server namespace: ai labels: app: llama3-8b spec: replicas: 1 selector: matchLabels: app: llama3-8b template: metadata: labels: app: llama3-8b spec: nodeSelector: nvidia.com/gpu.present: "true" tolerations: - key: nvidia.com/gpu operator: Exists effect: NoSchedule containers: - name: vllm image: registry.internal/ai/vllm-llama3-8b:20240512 command: ["python3", "-m", "vllm.entrypoints.openai.api_server"] args: - --model=/models/llama3-8b - --served-model-name=llama3-8b - --tensor-parallel-size=1 - --max-model-len=8192 - --gpu-memory-utilization=0.85 - --port=8000 ports: - containerPort: 8000 name: http resources: requests: cpu: "8" memory: "32Gi" nvidia.com/gpu: "1" limits: cpu: "8" memory: "32Gi" nvidia.com/gpu: "1" volumeMounts: - name: model-storage mountPath: /models readinessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 120 periodSeconds: 10 lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 30"] volumes: - name: model-storage csi: driver: juicefs-csi volumeAttributes: path: /models/llama3-8b

这个配置里有两个细节值得解释。

第一个是resources里的nvidia.com/gpu: "1"。这个写法是请求一张 GPU,不是占满一张卡。如果你用的是 NVIDIA MIG(多实例 GPU),也可以写成小数或者整数标识,但这里我们先不展开。

第二个是readinessProbe的initialDelaySeconds: 120。为什么是 120 秒而不是默认的几秒钟?因为大模型加载权重到显存通常需要 1 到 3 分钟,8B 模型可能快一点,70B 模型甚至更久。如果探针设得太激进,Pod 一直在“未就绪”和“重启”之间反复横跳,反而更糟。

3.4 服务暴露:Ingress 与 Gateway

推理服务跑起来之后,怎么暴露给外部?最直接的方式是 Service 的 LoadBalancer 类型,但私有化部署的环境里,我更喜欢用 Ingress。

我在集群里部署了 NGINX Ingress Controller,配置了一个支持大体积请求的 Ingress:

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: llm-ingress namespace: ai annotations: nginx.ingress.kubernetes.io/proxy-body-size: "50m" nginx.ingress.kubernetes.io/proxy-read-timeout: "300" nginx.ingress.kubernetes.io/proxy-send-timeout: "300" spec: ingressClassName: nginx tls: - hosts: - llm.example.com secretName: llm-tls rules: - host: llm.example.com http: paths: - path: / pathType: Prefix backend: service: name: llama3-8b-service port: number: 8000

这里最关键的注解是proxy-read-timeout: "300"。因为 LLM 生成式推理的响应时间通常要几十秒甚至几分钟,Nginx 默认的超时时间只有 60 秒,如果不改,长文本生成到一半就会被网关切断。这也是容易被忽略的坑。

3.5 GPU 资源的调度细节

Kubernetes 调度 GPU 资源时,默认行为是“不可超卖”。也就是说,你请求了 1 张 GPU,调度器就认为你独占一张卡。但现实里,一个小模型可能只用半张卡,大模型又要多卡并行,调度策略需要细分。

我的做法是混合调度 + 自定义优先级。

先按显存需求,把 GPU 节点池分成两类:

  • 大显存池:A100 80G,调度给 70B 级别的大模型。
  • 中显存池:L40S 48G,调度给 7B 到 13B 模型。

然后通过 nodeSelector 和 nodeAffinity 控制调度目标。比如大模型的 Deployment 会指定nvidia.com/gpu.memory: "80G"这样的节点标签。

还有一个重要概念是tolerations。我给 GPU 节点默认打了污点,只允许需要 GPU 的 Pod 调度上来。这样普通业务不会试图占用 GPU 节点,调度器也省心很多。

3.6 模型存储与加载优化

模型文件放在 JuiceFS 上,有一个现实问题:节点冷启动时,模型文件不在本地缓存,首次加载需要从对象存储拉取,速度取决于网络。一个 8B 模型大约是 16GB,千兆网络下要两三分钟,这还算能接受;70B 模型 130GB,那就要十几分钟了。

我做了两个优化。

第一个优化是节点预热,或者说“模型预取”。在部署推理服务前,先在 GPU 节点上跑一个 DaemonSet,把常用的模型文件从 JuiceFS 预读取到本地缓存。这个过程不会占用 GPU 显存,只占磁盘。实测下来,预热过的节点拉起一个 8B 模型,从 Pod 创建到就绪只需要 30 秒左右。

第二个优化是 hook 脚本自动预热。我在 CSI 插件的挂载参数里设置了缓存预热目录,配合 GPU 节点上的 cron job,每半小时拉取一次模型文件变更列表。模型发布后,节点缓存最多延迟半小时就能拿到新版本。

4. 自动伸缩与弹性保障

4.1 为什么不能用传统的 HPA

生成式 AI 服务的自动伸缩,比普通 Web 服务难很多。难在两点:一是模型服务的资源消耗和请求并发强相关,但指标采集周期长;二是 GPU 资源伸缩不像 CPU 那样灵活,新 Pod 启动要几秒钟到几分钟。

传统 HPA 基于 CPU 和内存指标,对 LLM 推理服务来说基本不可用。因为模型推理时 CPU 利用率通常不高,GPU 利用率才是瓶颈。而默认的 metrics-server 根本不采集 GPU 指标。

我的做法是:基于 GPU 利用率和队列深度的组合指标,使用 KEDA 来驱动伸缩。

4.2 KEDA 集成:按 GPU 利用率和请求队列伸缩

KEDA 可以监听外部指标源,再驱动 HPA 进行伸缩。我用了 Prometheus 作为指标源,采集每个 Pod 的 GPU 利用率、请求排队长度、每秒请求数等指标。

核心配置是一个 KEDA 的 ScaledObject 示例:

apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: name: llama3-8b-scaledobject namespace: ai spec: scaleTargetRef: name: llama3-8b-server minReplicaCount: 1 maxReplicaCount: 4 triggers: - type: prometheus metadata: serverAddress: http://prometheus.monitoring.svc:9090 query: | sum(rate(vllm:requests_total{service="llama3-8b"}[1m])) threshold: "50" - type: prometheus metadata: serverAddress: http://prometheus.monitoring.svc:9090 query: | avg(vllm:gpu_utilization{service="llama3-8b"}) threshold: "50"

这里要特别说一个经验:自动伸缩阈值不要只设一个,要设置差别很大的上下阈值。比如缩放上去的阈值是 GPU 利用率超过 80%,缩下来的阈值是低于 20%。如果上下阈值靠得太近,就会出现 Pod 反复创建删除的抖动,每次创建都要加载模型,浪费 GPU 资源。

4.3 伸缩风暴的抑制:禁止频繁缩容

KEDA 默认的缩容冷却时间不够长,对推理服务来说,频繁缩容是灾难。

我的配置里一定会改这几个参数:

spec: advanced: horizontalPodAutoscalerConfig: behavior: scaleUp: stabilizationWindowSeconds: 120 policies: - type: Percent value: 100 periodSeconds: 60 scaleDown: stabilizationWindowSeconds: 1800 policies: - type: Pods value: 1 periodSeconds: 300

scaleDown的 stabilizationWindowSeconds 我设成了 1800 秒,也就是半小时内不允许连续缩容。scaleUp则尽量激进,因为流量上来时你要尽快扩容,而缩容则需要谨慎,晚缩几分钟不会有问题,但缩错了就要承受排队的流量尖峰。

4.4 节点级别的 GPU 弹性伸缩

Pod 级别的自动伸缩解决的是“单节点内多副本”的问题,如果整个节点池的 GPU 都被占满了,Pod 伸缩也白搭。

节点级别的 GPU 弹性伸缩,需要配合集群的 Cluster Autoscaler 或 Karpenter 来做。我目前用的是 Cluster Autoscaler,它监听调度失败的 Pod,当 Pod 因为 GPU 资源不足而 pending 时,自动为节点池扩容新的 GPU 节点。

但我要提醒一句:Cluster Autoscaler 对 GPU 节点的伸缩周期通常比较长,从触发扩容到节点 ready,可能 5 到 10 分钟。所以它是应对“小时级”流量波动的,不能应对“分钟级”突发。如果业务确实有分钟的尖峰,你得保留一个 GPU 节点的最小余量。

5. 可观测性:日志、指标与链路追踪

5.1 推理服务的核心指标

生成式 AI 服务的可观测性,比传统 Web 服务多了几个关键指标,它们是排障和容量规划的基石:

  • GPU 显存利用率:模型推理时显存占用是否接近上限,决定能否承受更大 batch。
  • GPU 算力利用率:SM 利用率多少,判断是计算密集还是推理等待密集。
  • 首 token 延迟(TTFT):用户发出请求到收到第一个 token 的时间,体验指标。
  • 每 token 生成延迟(TPOT):后续每个 token 生成速度,影响输出总时长。
  • 请求队列深度:排队中的请求数量,判断是否需要扩容。
  • 每秒请求数(RPS)与并发数:评估整体吞吐。

这些指标,vLLM 原生会暴露一部分,比如vllm:gpu_cache_usage_perc,vllm:num_requests_running等。我通过 Prometheus 采集 vLLM 的指标端点,再配合 DCGM 采集 GPU 硬件指标,最终在 Grafana 上统一展示。

5.2 日志的采集与管理

大模型服务的日志有一个特点:prompt 和响应结果可能包含敏感数据。我不建议把完整 prompt 打进日志,而是做脱敏处理。我们实际做的是在日志采集端过滤掉prompt字段和output字段,只记录 URL、状态码、耗时、token 用量。

日志采集用 Filebeat + Elasticsearch 的经典组合。每个 GPU 节点上跑一个 Filebeat DaemonSet,采集容器标准输出和挂载的日志文件。这里要特别注意 Filebeat 对多行日志的处理——Python 的 traceback 是多行的,如果没配多行匹配,一条异常会被拆成无数条,排障时简直要疯掉。

5.3 链路追踪

生成式 AI 链路,从客户端到网关到推理服务,中间还可能有 RAG 检索、向量数据库查询,链路比传统 API 长得多。如果不做链路追踪,出了问题只能靠猜。

我的方案是 OpenTelemetry 链路追踪。在推理服务里埋点,记录每个阶段耗时。Grafana Tempo 作为后端存储,Grafana 里可视化。其中最重要的一个链路节点,是“RAG 检索耗时”和“LLM 生成耗时”的拆分开。因为很多用户觉得“生成式 AI 响应慢”,实际慢在向量数据库检索而不是模型推理。

5.4 告警策略

告警我这里只设了四个高优先级的,因为告警太多等于没告警:

  1. GPU 显存利用率超过 95% 持续 5 分钟。说明可能 OOM,需要扩容或减小并发。
  2. TTFT 超过 10 秒持续 3 分钟。说明模型排队严重,需要扩容副本。
  3. 请求错误率超过 5%。说明服务异常或模型故障。
  4. GPU 节点掉线或 Device Plugin 异常。这是集群层面的故障。

告警的收敛很重要。比如模型加载失败可能触发一连串的 5xx 告警,但根因只有一个。我在告警规则里加了 “单个时间窗口内同一服务只触发一次” 的收敛策略,避免告警风暴。

6. 常见问题与排查技巧实录

6.1 GPU 显存溢出(OOM)实战排查

我在生产环境里碰到最多的一个问题就是:模型推理到一半,容器直接被杀,dmesg里看到Out of memory: Killed process。

这个问题的根因通常在 vLLM 的--gpu-memory-utilization参数上。如果设成 0.9,意味着预留 10% 的显存给 KV cache 的碎片和其他开销。但有些操作(比如长上下文生成、大 batch)会临时申请更多显存,超过预留阈值,就 OOM。

我的排查思路是三步走:

  1. 看 Pod 状态和重启次数:kubectl describe pod看 Last State 和退出码。如果退出码为 137,基本可以确认是被 OOM Killer 杀掉。
  2. 看 vLLM 的日志:重点关注CUDA out of memory或Killed字样,能看到具体失败的显存申请大小。
  3. 计算显存平衡:gpu-memory-utilization和max-model-len相互影响。max-model-len 越大,KV cache 占用越多。

我最后的解决办法是把gpu-memory-utilization从 0.9 降到 0.8,同时把max-model-len从 8192 降到 6144,实测下来既满足了业务需求,也留出了足够的显存余量。

6.2 模型加载慢导致 Pod 一直重启

另一个高频问题是:Pod 创建后一直处于CrashLoopBackOff,看起来是 readinessProbe 失败被反复杀掉。

我的排查过程是这样的:

  1. kubectl logs看容器日志,确认模型是否加载成功。如果日志显示加载到一半就退出,往往是显存不够或存储读取异常。
  2. kubectl exec进容器,直接跑一遍python -c "import torch; print(torch.cuda.is_available())",确认 CUDA 可用。
  3. 检查 JuiceFS 挂载是否正常:到节点上df -h看挂载点。

这类问题还有一个隐蔽原因:Pod 启动时模型文件在远程存储上,首次读取慢,超过了 readinessProbe 的时间窗。我最后是把 initialDelaySeconds 调到 300 秒,并且用生命周期 hook 在容器启动前预加载模型到本地缓存。

6.3 自动伸缩不生效的秘密:指标没采集到

KEDA 配置好了,看 Grafana 上指标也有,但 Pod 就是不扩。这个问题我排查了一个下午,最后发现是 Prometheus 的指标采集频率和 HPA 的同步周期不匹配。

Kubernetes HPA 默认每 15 秒同步一次指标,而 Prometheus 的抓取间隔是 30 秒。如果 KEDA 从 Prometheus 查询到的数据是旧值,或者指标的rate()函数窗口太短,导致指标值一直为 0,HPA 就认为不需要扩容。

我的解决方法是:单独为推理服务设置一个抓取任务,把 scrape_interval 调到 10 秒,并且在 Prometheus 查询里使用max_over_time来平滑指标波动。改完后,扩容响应时间从几分钟缩短到了 30 秒内。

6.4 滚动更新时服务中断

推理服务滚动更新,默认策略是“先创建新 Pod,等就绪后删旧 Pod”。但对于大模型推理,这个默认策略有一个致命问题:新 Pod 启动加载模型要 1 到 3 分钟,旧 Pod 早已被删除,整个服务在加载期间对外不可用。

我用的方案有两个:

一是maxSurge: 1和maxUnavailable: 0,保证更新期间始终有一个旧 Pod 在提供服务。

二是 PreStop 钩子里加一个优雅退出逻辑:收到 SIGTERM 时,先等待正在处理的请求完成,再真正退出。vLLM 框架对这个支持得不错,具体实现是:

lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 30"]

这个sleep 30是给 vLLM 预留 30 秒完成正在处理的请求。如果请求耗时更长,可以适当增加。经验上 60 秒足以覆盖绝大多数情况。

6.5 多模型共存的资源隔离

同一个集群里跑多个模型,最大的问题是互相干扰。一个大模型的显存占用冲到 90%,可能把旁边小模型的显存挤爆。

我的做法是:

  • 按模型大小划分 nodeSelector 标签。
  • 大模型和小模型用不同的节点池。
  • 每个模型设置严格的 Limits,禁止超卖。
  • 使用 PriorityClass 区分业务优先级,核心模型优先调度。

如果不做隔离,我建议至少配置 LimitRange 和 ResourceQuota,限制每个命名空间的 GPU 总量。

7. 一点经验总结与后续规划

这些年的实操下来,我的体感是:Kubernetes 跑生成式 AI,最大的难点不是部署本身,而是“资源效率”。同样的 GPU 集群,调度策略和伸缩策略不同,最终吞吐量和成本可能差出两三倍。

如果你现在正要上手,我给三个最实用的建议:

第一,版本稳妥为主,不要追新。先把一套成熟版本跑透,再考虑升级。我们的集群从 v1.26.0 平稳运行了大半年,才逐步测试升级到更高版本。

第二,一定要做 GPU 资源监控。没有 DCGM 和 Prometheus,你排查显存问题就像闭着眼睛开车。

第三,自动伸缩的参数要反复调优。先调容忍的抖动范围,再调 HPA 上下阈值,不要一上来就追求全自动。

后续我计划在这个系列里再聊聊多集群管理、GPU 共享与 MIG、以及 RAG 架构在 Kubernetes 上的落地,欢迎继续关注。如果你在部署过程中遇到这里没有写到的坑,也欢迎在评论区和大家交流,说不定你的问题就是下一篇的主题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询