news 2026/10/2 3:25:56

vLLM 与 K8s 实战:从 GPU 调度到弹性伸缩的推理服务部署指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
vLLM 与 K8s 实战:从 GPU 调度到弹性伸缩的推理服务部署指南

把一个大模型从“能跑”变成“能扛住生产流量”,中间隔着一整座 K8s 的坑。最近几个月我一直在折腾 vLLM 和 K8s 的组合:一边是当前大模型推理服务里最常见的开源框架,负责把 Qwen、GLM、DeepSeek 这类模型跑出高吞吐、低延迟;另一边是云原生时代的资源调度事实标准,负责让 GPU 显存、多副本、自动扩缩容这些事变得可控。这套组合解决的核心问题其实很直白——推理服务怎么在 GPU 资源有限的情况下,做到按流量弹性伸缩,同时让运维不至于天天睡不好觉。

我默认看这篇文章的你,至少已经手动部署过一次 vLLM 镜像,知道docker run能带--gpus all启动服务。至于 K8s 你是刚入门还是已经背过 CKA 题纲,都不影响阅读,我会把每一步为什么这么做讲清楚。下面这些内容不是我翻译文档得来的,是真实踩过0 Insufficient nvidia.com/gpu、apiserver not healthy、OOM killed之后倒逼出来的实战笔记,照着做能少走好几天的弯路。

1. 为什么偏偏是 vLLM + K8s:推理平台怎么选型

1.1 vLLM 到底解决了大模型推理的什么痛点

先说 vLLM。大模型推理和传统 Web 服务最大的区别,是它背后站着一块“显存围墙”。模型权重要占显存,每个请求的 KV Cache 也要占显存,而这两者之间存在一个动态博弈:请求越多,Cache 占得越多,能同时处理的并发就越少。早期方案里,显存是按请求预分配的,来了 8 个请求就预留 8 份固定大小的空间,结果真正用到的只有一半,其余全部浪费。

vLLM 之所以能在众多推理框架里冒头,核心是它提出了 PagedAttention 机制。这个思路和操作系统里的虚拟内存分页异曲同工:不再给每个请求分配一整块连续显存,而是把 KV Cache 切成固定大小的“页”,按需分配、按需回收。这样一来,显存利用率被大幅拉高,同样的 GPU 可以塞下更多并发请求。再加上 Continuous Batching,不等人齐就发车,来一个请求就插一个位置。这两个机制叠加,效果就是吞吐量比 naive 方案提升 2 到 4 倍并不夸张。

现在做推理服务选型,绕不开的其实就三个:Ollama、LM Studio、vLLM/SGLang。Ollama 和 LM Studio 更适合个人笔记本和前期试验,一条命令拉模型、跑对话,体验极好,但它们对高并发、动态批处理、生产监控的支持比较弱。SGLang 在部分长文本场景下非常强,性能甚至反超 vLLM,但它的周边生态和部署案例目前还是比 vLLM 少一截。我选 vLLM 的核心理由就一条:生产环境要的是“稳定可预期”,不是某个 benchmark 上高 5% 的分数。vLLM 的 OpenAI 兼容 API、Prometheus 指标、Docker 镜像成熟度、社区踩坑密度,都是当前最适合接生产的那一个。

1.2 K8s 在这里补上了什么能力

如果没有 K8s,vLLM 部署在单台裸机上其实也能跑,但一旦流量上来,问题就全冒出来了:GPU 显存不够要扩容怎么办?节点宕机了服务怎么恢复?深夜流量低谷时 8 张卡空转,费用谁兜?K8s 解决的正是这些问题背后的三个基本能力:调度、弹性、自愈。

调度层面,K8s 通过 Device Plugin 机制把 GPU 曝光成可调度的扩展资源nvidia.com/gpu,调度器会像分配 CPU 和内存一样分配显存;弹性层面,配合 HPA 或 KEDA 等机制,按流量指标自动增减容器副本;自愈层面,节点宕机后 Pod 会自动迁移到健康的节点。这些能力单独看都不新奇,但组合起来之后,一个推理平台才具备了“作为服务对外交付”的底气。

还有一个很多人容易忽略的好处:K8s 天然做多模型隔离。业务部门会同时跑对话模型、Embedding 模型、Reranker 模型,用裸机部署时这些模型经常互相干扰;在 K8s 里你只需要把它们拆成不同的 Deployment,用命名空间隔离,再用 ResourceQuota 控制每个团队能占用的 GPU 上限,运维口径一下就清晰了。

1.3 平台选型的现实约束

不过我要泼一盆冷水:vLLM + K8s 这套组合的复杂度是肉眼可见的。你自己docker run一条命令能起服务,搬到 K8s 上至少要写 Deployment、Service、HPA 三套 YAML,还要解决镜像拉取、模型预热、探针配置、日志采集一堆事。所以选型之前先想清楚你的场景:如果只是单机跑个 demo、或者团队没有专职运维,直接用 vLLM + systemd 或者 docker-compose 反而更务实。K8s 的价值在副本数和节点数起来之后才真正体现出来。

另外说句实在的,集群规模不到 10 台 GPU 节点的时候,K8s 的维护成本可能比收益还高。这套方案的甜蜜点,是 GPU 节点达到几十上百卡、需要做多团队资源隔离、并且有真实流量波动的时候。如果你正处在这个阶段的门口,继续往下看。

2. 实战前奏:镜像、模型与加载姿势

2.1 镜像选择:版本比想象中更重要

vLLM 官方镜像在 Docker Hub 上的路径是vllm/vllm-openai,比如热词里提到的vllm/vllm-openai:v0.27.1,这是一个带有 OpenAI 兼容 API 的现成镜像,拉下来直接跑就能对外提供服务。但镜像版本这件事,真的不能随手latest,因为模型框架的版本兼容问题会让你怀疑人生。

拿 GLM 系列来说,GLM 这类模型经常会用到较新的transformers特性,如果你的 vLLM 镜像内置的 transformers 版本太旧,加载模型时大概率报KeyError或者某个算子不存在的错。经验法则很简单:新发布的模型,尽量选择发布期前后的 vLLM 版本镜像。如果你的模型是半年前发布的,选择半年前的稳定版镜像通常最稳,而不是盲目追求最新。

我自己的习惯是先查docker hub上的 tag 列表,再去看官方 GitHub Release 页面里对模型支持的说明。千万别直接用latest,因为你不确定某个大版本升级会不会改掉 API 参数,比如--max-model-len默认值调整、--quantization参数名变化,这类 breaking change 在 vLLM 迭代中很常见。

2.2 模型加载的三种姿势

模型放到哪个位置决定了你上线一条新模型的耗时。我实际用下来,有三种主流方案,各有各的适用场景。

第一种,启动时从 Hugging Face 或 ModelScope 拉取。这种方式配置最简单,docker run的时候设好HF_HOME环境变量就行,模型首次启动会自动下载。缺点也很明显:启动时间可能是 10 分钟,也可能是 1 小时,取决于网络和模型体积。生产环境中这种不确定性非常致命,你的一次滚动更新可能因为模型下载慢导致整个服务几分钟内不可用。

第二种,提前拉取到本地/内网存储,容器启动时挂载。比如用hostPath或PVC把已下载好的模型目录挂进容器,模型文件已经在节点上,容器启动只需要几十秒完成加载。这是目前最推荐的生产方案。实际做的时候,可以先手动拉一个临时容器,把模型下载到某个节点目录,再通过 PV/PVC 的方式挂载给其他副本。

第三种,用对象存储做模型中转,节点按需缓存。适合模型很多、但每个节点不想都存一份全量模型的场景。举个例子,你维护一个内置 S3 客户端的自定义启动脚本,容器起来的时候先检查本地有没有模型缓存,没有则从远端下载。这种方案最灵活,但工程成本也最高。

这里顺便提一下qwen3-embedding-0.6b这种 Embedding 模型的加载。我们用vllm/vllm-openai:v0.27.1镜像加载它时,入口参数和生成模型基本一致,但要注意 embedding 模型对max-model-len的要求没那么高,反而对 batch size 更敏感。这类小模型特别适合做成常驻的 Embedding 服务,不需要上多卡,单卡就能扛很大的请求量。

2.3 加载效率的细节

模型加载慢这件事,往往不是模型本身的问题,而是镜像和模型文件分层的问题。如果你把模型文件放在镜像里,每次构建镜像都会把模型打进一个新的 layer,镜像体积会膨胀到十几 GB,集群里每个节点拉取镜像都会吃满网络。我的建议是模型文件和镜像解耦,镜像只装运行环境,模型通过 Volume 挂载进去。

另一个细节是环境变量HF_HUB_OFFLINE=1。如果模型文件已经通过挂载方式提供,一定要把这个变量设为 1,让 vLLM 启动时不尝试联网检查更新,否则会出现启动时卡在Downloading...的网络等待中,白白增加几十秒启动时间。类似地,从 ModelScope 加载模型的场景可以设MODELSCOPE_OFFLINE=1。

3. 集群准备:从零到 GPU 可调度

3.1 集群怎么搭不走弯路

K8s 集群搭建这件事,分两派。测试环境用Minikube或kind最快,一条命令起一个单节点集群,对于学习 K8s 基础、本地调试 YAML 完全够用。生产环境跑 GPU 负载,逃不掉kubeadm手动搭或者用云厂商托管的 K8s 服务。这里我只讲多节点 GPU 集群的注意点,因为单节点不管怎么折腾都不涉及调度问题。

kubeadm 初始化时最常见的坑就是那句让人头皮发麻的报错:the api server is not healthy after 4m0.00747357s。这个报错出现,十有八九是 kubelet 没起来,或者 apiserver 的静态 Pod 没跑起来。排查顺序:先systemctl status kubelet看 kubelet 状态;再看crictl ps -a看容器运行时里有没有 apiserver 的容器在反复崩溃;最后查journalctl -u kubelet -f的日志,看是否能拉到kube-apiserver镜像。最常见的 root cause 是 kubelet 的 cgroup driver 和容器运行时不一致——K8s 默认要求systemd,而 containerd 如果配成了cgroupfs,两者沟通不畅,apiserver 容器就会反复 CrashLoop。

还有两个前置条件被很多人忽略:swap 必须关闭,否则 kubelet 会报错拒绝启动;br_netfilter模块必须加载,否则后续节点通信会出问题。这两件事在kubeadm init之前就得处理好,等报错再回去补往往会更折腾。

3.2 GPU 要让 K8s 看见才算数

集群搭好只是第一步。K8s 默认是不知道 GPU 存在的,你必须在每个 GPU 节点上做三件事:装好 NVIDIA 驱动;装好 NVIDIA Container Toolkit;部署 NVIDIA Device Plugin。

驱动版本建议和容器运行时兼容,我踩过的版本错位问题基本都是因为驱动版本太新或太旧导致nvidia-smi在容器里不可用。装好 Container Toolkit 后,nvidia-smi应该能在容器里正常输出,这是验证的第一步。接着部署 Device Plugin 有两种方式:用官方 Helm 一键安装,或者直接 apply 官方 YAML。部署完成后,执行kubectl describe node,如果在Capacity里能看到nvidia.com/gpu这个资源,说明 GPU 已经被 K8s 纳管了。

这里有一个经常被问到的点:nvidia.com/gpu是什么?它叫扩展资源,是 K8s 自定义资源的一种。GPU 数量通过 Device Plugin 上报给 Kubelet,调度器在给 Pod 分配节点时会检查这个资源的剩余量。所以你在 Pod 的 YAML 里写resources.limits.nvidia.com/gpu: 1,调度器就有依据把 Pod 调度到一块还有空闲显存的卡上。

3.3 节点可运维性的前置检查

GPU 节点纳入集群后,我建议先做一轮“体检”再开始部署推理服务。第一项是查看节点上的 GPU 型号和驱动版本:kubectl get node --show-labels配合nvidia-smi确认哪些节点有卡、卡的型号是什么。第二项是给节点打上标签,比如gpu-type=a100、gpu-type=4090,这样后续做模型调度时可以做定向调度,比如大模型必须上 A100 节点,小模型允许上 4090 节点。

这些标签在后面的 GPU 调度里价值很大。K8s 调度器默认不感知 GPU 型号,它只知道你有几块卡、每块卡是一个nvidia.com/gpu,哪怕你的集群里同时有 A100 和 4090,调度器也不会区分。如果你希望不同的模型跑在不同型号的卡上,必须靠节点标签 +nodeSelector来手动约束。

4. 部署 vLLM 服务:从 Deployment 到对外可访问

4.1 Deployment YAML 的写法与背后逻辑

vLLM 在 K8s 里的 Deployment 写法,核心就两个地方:资源和参数。下面是一份我实际用过的模板,注释部分解释了每个字段为什么这样配:

apiVersion: apps/v1 kind: Deployment metadata: name: vllm-qwen3-embedding namespace: ai-platform spec: replicas: 2 selector: matchLabels: app: vllm-embedding template: metadata: labels: app: vllm-embedding spec: nodeSelector: gpu-type: 4090 containers: - name: vllm image: vllm/vllm-openai:v0.27.1 command: - python - -m - vllm.entrypoints.openai.api_server args: - --model - /models/qwen3-embedding-0.6b - --task - embedding - --port - "8000" - --max-model-len - "8192" - --gpu-memory-utilization - "0.9" env: - name: HF_HUB_OFFLINE value: "1" ports: - containerPort: 8000 name: http resources: limits: nvidia.com/gpu: 1 memory: 32Gi requests: nvidia.com/gpu: 1 memory: 24Gi volumeMounts: - name: models mountPath: /models readinessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 30 periodSeconds: 10 livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 60 periodSeconds: 30 volumes: - name: models hostPath: path: /data/models

这里说几个关键点。第一,nvidia.com/gpu: 1是硬性限制,不能拆成 requests 和 limits 两个值,在 Device Plugin 的模型里 GPU 是整体分配的,要么整卡独占,要么通过 MIG/Time-Slicing 切分。在这个 Deployment 里我们用了整卡独占。第二,--gpu-memory-utilization建议不要设满,0.9 已经是比较激进的数值,留给 CUDA context、driver 一部分余量。第三,探针路径建议用/health,这是 vLLM 官方暴露的健康检查端点,用它对流量的调度才准确。

4.2 Service 与 ExternalIPs:让外部打得进来

Deployment 只是把 Pod 建起来了,外部要访问还需要 Service。最简单的方式是 ClusterIP,只在集群内部可达;如果需要外部访问,有几个选择:LoadBalancer、NodePort、ExternalIPs。

热词里提到的 ExternalIPs,是一个很有意思、但也容易被忽略的选项。你可以在 Service 的配置里直接指定外部 IP,一条配置就能让集群外的流量直接通过这个 IP 打到你的 Pod:

apiVersion: v1 kind: Service metadata: name: vllm-embedding-svc namespace: ai-platform spec: selector: app: vllm-embedding ports: - port: 8000 targetPort: 8000 externalIPs: - 192.168.1.100

ExternalIPs 适合的场景是:你有固定的公网或内网 IP,但集群里没有云厂商负载均衡器,又不想用 NodePort 那种高位端口访问方式。要注意的是,192.168.1.100这个 IP 必须真实绑定到某个节点的网卡上,K8s 只是把流量引流到这个地址,真正的网络转发还是要靠节点本身的路由规则。如果没有现成的 LoadBalancer,ExternalIPs 比 NodePort 的可读性和运维可控性更好,生产环境里很实用。

4.3 滚动的优雅与不优雅

上线之后免不了更新镜像版本、调整模型参数。K8s 默认的滚动更新策略是RollingUpdate,它会先起一个新 Pod,等新的 ready 之后再杀掉一个旧的。但对于大模型推理服务,这个默认策略有点问题:新 Pod 启动加载模型可能需要一两分钟,在这段时间里 Service 的 endpoints 还包含旧 Pod,流量不会断;但如果你的maxSurge和maxUnavailable设得不好,比如maxUnavailable: 1,你会观察到旧 Pod 被快速杀掉,而新 Pod 又没 ready,服务出现短暂不可用。

我的建议是 Deployment 里显式配置strategy:

strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0

maxUnavailable: 0意味着旧 Pod 必须等新 Pod ready 之后才能被杀掉,这样可以保障服务不中断。代价是更新时需要多一块 GPU 的资源余量,因为新旧 Pod 会同时存在一会。如果你的 GPU 资源紧张到没有这块余量,也可以接受短暂中断,但一定要有心理预期。

还有一个细节:Pod 被删除时,vLLM 正在处理的请求会被直接切断。解决方案是给 Pod 配置terminationGracePeriodSeconds,给 vLLM 留出处理完当前请求的时间。实测下来,设置为 120 秒比较合理。再配合 K8s 1.20 之后默认开启的优雅关闭机制,Pod 进入 Terminating 状态时会被从 Service endpoints 里摘掉,新流量不会再来,老请求有足够时间跑完。

5. 弹性部署实战:HPA、自定义指标与 KEDA

5.1 为什么 CPU 指标在这里不够用

很多第一次做推理服务弹性伸缩的人,第一反应是:直接用 CPU 或内存利用率做 HPA 不就行了吗?在实际场景里,这个方案基本是失效的。原因很简单——大模型推理是 GPU 密集型而不是 CPU 密集型。服务的请求量和 GPU 利用率并不是线性对应的,一个请求在显存里排队等待时,CPU 可能基本是空闲的;但当并发批量到达时,GPU 才是真正的瓶颈。

所以做弹性伸缩,指标应该来自 GPU 状态和请求队列状态。vLLM 暴露了一组 Prometheus 指标,比如vllm_num_requests_running、vllm_num_requests_waiting、vllm_gpu_cache_usage_perc,这些才能真正反映服务压力。

5.2 用 Prometheus Adapter 把指标接进 HPA

生产上标准的做法是:Prometheus 抓取 vLLM 的 metrics,Prometheus Adapter 把它转成 K8s 自定义指标,然后 HPA 消费这个指标。链路看起来长,但每一步都是现成的组件。vLLM 默认支持通过环境变量打开 metrics,对应方式是把--metrics参数打开,然后暴露/metrics端点。Prometheus 里配置一个 scrape job 指向 vLLM Pod 的 8000 端口即可。

HPA 配置示例,指标用“当前排队等待的请求数”作为伸缩依据:

apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: vllm-embedding-hpa namespace: ai-platform spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: vllm-embedding minReplicas: 2 maxReplicas: 8 metrics: - type: Pods pods: metric: name: vllm_num_requests_waiting target: type: AverageValue averageValue: "16" behavior: scaleDown: stabilizationWindowSeconds: 600

这里要重点解释stabilizationWindowSeconds:模型推理的负载波动往往是很剧烈的,如果缩容太快,刚把一个副本缩掉,下一秒流量突然上来,新副本又要重新加载模型,几十秒内服务质量会明显下降。所以我建议缩容稳定窗口至少给 10 分钟,而扩容的稳定窗口可以短一点,让流量上来时能快速反应。

5.3 排队长度才是更稳的伸缩信号

用vllm_num_requests_waiting做伸缩依据,背后有一个逻辑:vLLM 的连续批处理机制决定了它在队列里的任务越多,资源利用率越高。如果这个指标持续超过某个阈值,说明当前副本数已经到达处理上限,继续堆请求只会增加延迟。这时候扩容是合理的。

与之对比,如果只看 GPU 利用率,你会发现模型推理服务往往在负载不高时 GPU 利用率就到 90% 以上,因为连续批处理会尽量填满 GPU 的计算单元,这个指标不具备区分“还能扛”和“已经扛不住”的能力。排队请求数则更直接反映服务的积压情况。

另外,如果你的推理服务前面还有一层业务队列,比如你在 K8s 里额外部署了 RabbitMQ 或 Kafka 做请求缓冲,那么用 KEDA 监听队列积压来做伸缩会是更好的选择。KEDA 本质上是一个事件驱动的伸缩控制器,配置起来比裸写 Prometheus Adapter 更简单,而且对“先到队列、再转发给推理服务”这种架构天然友好。

5.4 弹性伸缩里的隐性成本

弹性伸缩最容易被忽略的问题是:大模型服务的扩容成本远高于普通 Web 服务。普通服务扩容要做的就是起一个容器,几秒就绪;大模型服务扩容要先拉镜像、再加载模型,动辄几分钟。如果你把 HPA 的阈值设得太灵敏,流量微幅抖动就会触发扩容,然后扩容上来的 Pod 还没 ready 流量又退了,白占 GPU,还白白消耗了节点资源。

所以我有几个实测下来的建议:第一,HPA 的最小副本数不要低于 1,否则冷启动时流量直接打不进来;第二,扩容阈值要结合模型加载耗时来定,模型加载要 90 秒的话,扩容稳定窗口至少给 120 秒,留出足够的缓冲时间;第三,一定要设 maxReplicas 上限,防止某个突发流量把集群内所有 GPU 节点都打满,导致其他业务被挤出去。

6. GPU 调度深入:共享、隔离与碎片化

6.1 一块 GPU 能不能分给多个 Pod

默认情况下,nvidia.com/gpu: 1表示一整块 GPU 只能被一个 Pod 独占。这样做的好处是显存和算力完全隔离,一个 Pod 的显存泄漏不会影响别人;缺点是资源利用率不高,尤其是在部署了很多小模型的时候,每块卡只跑了一个模型,大部分显存都是闲置的。

解决 GPU 资源碎片化的方式有两个主要方向。第一是 MIG,在 A100、H100 这类卡上将物理 GPU 切分成多个独立实例,每个实例拥有独立的显存和计算单元,隔离性最好。第二是 Time-Slicing,让多个 Pod 共享同一块 GPU,按时间片切换使用。Time-Slicing 的隔离性弱,一个 Pod 跑满算力会影响同卡的其他 Pod,但胜在实现简单,适合跑延迟不敏感的批处理任务。

至于 vLLM 这个模型,它对接 MIG 和 Time-Slicing 都还算成熟。实际体验下来,我的建议是:如果跑的是 7B 以下的小模型,多个模型共享一块卡是可行的,但务必用 MIG 而不是 Time-Slicing,否则并发一高,同一个 batch 里的推理任务会被互相拖慢。如果跑的是 70B 以上的大模型,老老实实一卡一模型,别想着省。

6.2 多团队共享 GPU 时的资源治理

当多个团队共用一个 GPU 集群,运维的核心工作从“确保服务能跑”变成“确保资源不被某个人抢光”。K8s 提供的最基础工具是 ResourceQuota 和 LimitRange。给每个团队一个 Namespace,然后在 Namespace 上挂一个 ResourceQuota,限制这个团队能申请的nvidia.com/gpu总量上限:

apiVersion: v1 kind: ResourceQuota metadata: name: gpu-quota namespace: team-a spec: hard: nvidia.com/gpu: "8" requests.memory: 128Gi limits.memory: 256Gi

配合 PriorityClass 能进一步做优先级调度:核心业务团队的任务设置了高优先级,扩容时即使资源不够也能抢占非核心任务的 GPU。这个机制在集群资源紧张时特别重要,相当于给关键服务上了“资源保险”。

6.3 调度策略:Binpack 还是 Spread

K8s 默认调度器不感知 GPU 拓扑,只按资源量调度。你可以在节点上打标签,再用 nodeAffinity 做更细的约束。更进一步的调度策略调整,要关注两个方向:Binpack 和 Spread。

Binpack 意味着新 Pod 优先调度到已经有很多 GPU 被占用的节点上,这样能最大化利用单节点的 GPU,减少跨节点通信,适合推理这种延迟敏感的场景。Spread 则相反,优先分散到不同节点,容错性更高,整体服务不会被单个节点故障拖垮。K8s 内置调度器默认在两个策略间找平衡,如果你想强制 Binpack,可以给节点打分策略做调整,或者直接上支持更细 GPU 拓扑感知的调度器插件。

实际中我经常遇到的一个问题是:两个 vLLM Pod 被调度到了同一台机器上,结果其中一个 Pod 吃光了整机的显存,另一个 Pod 被 OOM Kill。防范方法是配置 Pod 反亲和性,让同一个 Deployment 的副本尽量分散到不同节点。但这个策略和 Binpack 有冲突,你需要根据自己的核心诉求做取舍——是更关心峰值性能,还是更关心服务的可用性。

7. 生产环境常见故障与排查实录

7.1 故障速查表

我把实际运维中遇到的高频故障整理成了一张速查表,对应现象、检查动作、常见解法一目了然:

故障现象排查动作常见原因与解法
Pod 一直 Pending,提示Insufficient nvidia.com/gpukubectl describe node查看 GPU 资源余量节点 GPU 被其他 Pod 占满,扩容节点或减少副本;也可能是 Device Plugin 没装好导致资源数为 0
容器内nvidia-smi报错docker exec进容器执行nvidia-smi驱动未装或 Container Toolkit 未配置,重建容器并挂载/usr/local/nvidia
/health探针一直失败kubectl logs查看 vLLM 启动日志模型加载失败、HF_HUB_OFFLINE 未开启导致卡在网络下载、显存不足
vLLM 日志报CUDA out of memory检查--gpu-memory-utilization和--max-model-len显存利用率设太高或 max-model-len 太大,调低后重启
服务间歇性 connection refusedkubectl get endpoints比较 Pod 列表Service 的 selector 没有匹配到 Pod,或 Pod 还没 ready 就被摘除
请求延迟巨大、GPU 利用率很高查看vllm_num_requests_waiting指标并发超出了当前副本的批处理能力,扩容副本或降低并发上限

7.2 三个典型问题的深度复盘

第一个是0/1 nodes are available: 0 Insufficient nvidia.com/gpu。这个问题在刚部署完 Device Plugin 的集群里尤其常见。检查顺序是:先确认节点上有没有 GPU 资源,kubectl describe node | grep nvidia,如果 Capacity 里是 0,说明 Device Plugin 没部署成功,或者它的 Pod 没起来。如果 Capacity 正常但可分配数为 0,说明这节点的 GPU 已经被占满。我遇到过一种隐蔽的情况:某个Running的旧 Pod 占着 GPU,但它的探针已经失败很久,服务实际不可用,K8s 却没把它杀掉。这时候看kubectl describe pod的 Conditions 就很容易发现端倪。

第二个是the api server is not healthy。这属于集群搭建期的问题,我已经在 3.1 节说了一部分。这里补充一个容易忽略的点:如果 apiserver 容器反复 CrashLoop,去查/etc/kubernetes/manifests/kube-apiserver.yaml里的--etcd-servers配置,因为 apiserver 和 etcd 通信失败时也会显示为 not healthy。另外,如果节点时间不同步,apiserver 的证书校验会失败,这也是一个容易忽略但很难排查的问题。

第三个是模型加载后显存瞬间爆掉。这个问题我踩得最惨。现象是:设置了--gpu-memory-utilization 0.9,但模型加载完成后没多久,容器就被 OOM Kill。检查下来发现,gpu-memory-utilization只是限制了 vLLM 内部的 KV Cache 和激活显存,但如果同一个 Pod 里还有别的进程占用显存,或者另一个也在跑 vLLM 的容器被调度到同一张卡上,显存自然不够用。这个问题的解法不是调低一个参数,而是保证“一张卡只有一个 Pod”的调度约束,并且给 Pod 的 limits.memory 留足余量。

7.3 监控体系:用数据体感替代玄学排查

故障排查的上层建筑是监控。我强烈建议在集群里一次性装好 Prometheus + Grafana 这套标准组合,然后重点盯四个指标:GPU 利用率、显存使用量、vLLM 排队请求数、Pod 重启次数。这四个指标能覆盖绝大多数性能问题的归因。

从 vLLM 的角度,/metrics端点暴露的指标里优先级最高的几个是:vllm_num_requests_running、vllm_num_requests_waiting、vllm_gpu_cache_usage_perc。其中vllm_gpu_cache_usage_perc非常直观地告诉你 KV Cache 的占用情况,如果持续在 95% 以上,同时排队请求数还在涨,说明当前副本的显存容量已经不够,要么扩副本,要么调大显存利用率上限。

Grafana 里我习惯把 GPU 利用率和排队请求数放到同一个面板,看它们的关系就能判断扩容阈值设得是否合理:如果 GPU 利用率已经打满,而排队请求数还没有超过扩容阈值,说明扩容阈值设高了,服务已经过载才开始扩容;反过来,如果排队请求数超过了阈值但 GPU 利用率只有 50%,说明扩容条件太敏感,副本数上来后资源并没有被有效利用。

8. 运维经验沉淀与后续扩展空间

8.1 从 nano-vllm 里学到的关键认知

如果你不是只想做个使用者,而是想深入理解 vLLM 的调度逻辑,我强烈建议花几天时间读一下 nano-vllm 这个项目。它是一个极简版 vLLM 实现,代码量很少,但把 PagedAttention 的显存分页、Continuous Batching 的调度循环、LLM Engine 的请求生命周期这几个核心模块都讲清楚了。

看完之后再回来看生产中的故障,理解完全不一样。比如为什么vllm_num_requests_waiting是比 GPU 利用率更准的伸缩依据,因为你明白了调度器在 Running 与 Waiting 之间的切换逻辑,就知道 Waiting 数量实际上代表了 GPU 算力没有及时处理的积压任务。这种从微观实现倒推宏观配置的思维模式,能让你在调参时不再靠猜。

8.2 一个比较完整的落地路径建议

这里我把自己在这一套组合上的落地路径梳理一下,顺序很重要。第一步是先在小规模集群里手动部署单副本 vLLM,跑通模型加载、健康检查、日志链路,不要一上来就写 HPA。第二步是补齐监控面板,重点看 GPU 利用率和排队请求数这两个指标。第三步是等监控数据跑一周,你对服务的负载特征有了基本判断之后,再配置 HPA 和弹性策略。

这套路径的核心思路是:弹性伸缩不是无中生有,它需要建立在稳定的基线上。很多团队一上来就铺 HPA,结果阈值拍脑袋设的,流量一波动副本数疯狂抖动,最后反而比固定副本更不稳定。先让它固定跑,用数据说话,再来谈弹性。

8.3 从 Deployment 到 Operator:推理平台的自研方向

Deployment 只是把服务跑起来的起点。如果你负责的是一个内部 AI 平台,后面大概率会遇到这些需求:不同的模型有不同的资源参数、不同的启动命令;模型上线要走审批流;模型出故障要能快速回滚。这些东西用裸 Deployment 也能做,但当模型数量到达几十上百个,YAML 的维护量就会变得非常痛苦。

K8s 社区的 Operator 模式就是为解决这类问题而生的。比如 KServe、KubeAI 这些项目,本质上都是把“部署一个模型服务”这个动作封装成一个个 CRD,用户只需要提交一份“模型服务描述”,Operator 自动完成滚动更新、扩缩容、监控配置。如果你团队有工程能力,也可以基于 operator-framework 写一个自己的简单 Operator,把 vLLM 的服务管理逻辑固化进去。热词里提到的“k8s operator案例”,在推理平台的语境下指的就是这种方向——把重复的运维操作代码化、自动化。

更进一步,GPU 池化、Serverless 推理、模型缓存预热、多集群联邦这些方向,都是 vLLM + K8s 这套架构后续可以演进的高级玩法。但每个方向都需要投入不小的工程成本,建议按照业务痛点的优先级逐个推进,不要为了技术而技术。

说回这套组合最打动我的地方,其实是它把“模型推理”这件事从一个需要手工盯着显存小心翼翼伺候的过程,变成了可以被标准化、被自动化、被度量的平台能力。我自己从裸机部署到 K8s 化改造,最深的体感是:故障从“半夜被叫起来看显存有没有爆”变成了“早上看一眼告警面板再安心开会”。这个变化背后,是资源抽象和自动调度带来的确定性。我给你的建议只有一条:不要盲目追求最复杂的方案,先把基础链路跑通,再逐步加弹性、加自动化,每一步都踩实了再往前挪,这套组合能给你的回报一定对得起你投入的时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 3:25:43

全国旅游景区数据集处理:JSON/Excel清洗与坐标转换实战

简介:全国旅游景区数据集收录了约12000条景区记录,时间节点为2022年6月,覆盖1A至5A等级景点,字段包含景点名称、所在城市、详细地址、景区等级、经度和纬度,可满足旅游数据分析、地图可视化、行程规划、景点检索等应用…

作者头像 李华
网站建设 2026/10/2 3:25:03

从零构建AI工程:数据契约、服务契约与最小可行实验追踪

1. 为什么“从零构建AI工程”不是写个模型就完事了“AI Engineering from Scratch”——这个标题乍看像极了某本技术书的副标题,或者某个开源项目的README第一行。但如果你真照着字面意思去干,十有八九会在第三天凌晨两点盯着GPU显存溢出报错、数据管道卡…

作者头像 李华
网站建设 2026/10/2 3:25:00

MySQL入门实战:从建表到增删查改的完整CRUD操作指南

MySQL入门绕不开的坎,就是增删查改这四个动作,也就是常说的CRUD。很多教程把增删查拆得七零八落,讲插入的只讲插入,讲查询的只讲查询,读者看完感觉自己什么都见过,可真到工作里要动手建表、写查询、改数据的…

作者头像 李华
网站建设 2026/10/2 3:24:59

MySQL删除操作详解:drop、delete、truncate的区别与实战避坑

干过几年数据库的人,基本都被问过一个问题:drop、delete、truncate到底有什么区别?前两天还有个朋友找我,说他在测试环境执行了一条不该执行的delete,结果整个表的数据全没了,幸好有备份,不然直…

作者头像 李华
网站建设 2026/10/2 3:24:57

K8S到底解决了什么问题?从容器编排到生产落地的核心原理

听到“K8S是用来解决什么问题的?”这种问题,第一反应通常是先立正,因为这问题看着基础,但真能一句话讲清楚的人并不多。网上铺天盖地都是安装部署教程、面试题、operator案例,反而把最核心的“它到底为什么存在”给说糊…

作者头像 李华
网站建设 2026/10/2 3:24:24

Java重载、重写与多态:从编译期到运行期的彻底解析

“重载(Overload)、重写(Override)、多态(Polymorphism)”这三个词,几乎每个学面向对象编程的人都绕不过去。但我发现一个很有趣的现象:网上搜这三个词,出来的资料有一半…

作者头像 李华