ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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 在这里补上了什么能力如果没有 K8svLLM 部署在单台裸机上其实也能跑但一旦流量上来问题就全冒出来了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_OFFLINE1。如果模型文件已经通过挂载方式提供一定要把这个变量设为 1让 vLLM 启动时不尝试联网检查更新否则会出现启动时卡在Downloading...的网络等待中白白增加几十秒启动时间。类似地从 ModelScope 加载模型的场景可以设MODELSCOPE_OFFLINE1。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-typea100、gpu-type4090这样后续做模型调度时可以做定向调度比如大模型必须上 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 打到你的 PodapiVersion: 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.100ExternalIPs 适合的场景是你有固定的公网或内网 IP但集群里没有云厂商负载均衡器又不想用 NodePort 那种高位端口访问方式。要注意的是192.168.1.100这个 IP 必须真实绑定到某个节点的网卡上K8s 只是把流量引流到这个地址真正的网络转发还是要靠节点本身的路由规则。如果没有现成的 LoadBalancerExternalIPs 比 NodePort 的可读性和运维可控性更好生产环境里很实用。4.3 滚动的优雅与不优雅上线之后免不了更新镜像版本、调整模型参数。K8s 默认的滚动更新策略是RollingUpdate它会先起一个新 Pod等新的 ready 之后再杀掉一个旧的。但对于大模型推理服务这个默认策略有点问题新 Pod 启动加载模型可能需要一两分钟在这段时间里 Service 的 endpoints 还包含旧 Pod流量不会断但如果你的maxSurge和maxUnavailable设得不好比如maxUnavailable: 1你会观察到旧 Pod 被快速杀掉而新 Pod 又没 ready服务出现短暂不可用。我的建议是 Deployment 里显式配置strategystrategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0maxUnavailable: 0意味着旧 Pod 必须等新 Pod ready 之后才能被杀掉这样可以保障服务不中断。代价是更新时需要多一块 GPU 的资源余量因为新旧 Pod 会同时存在一会。如果你的 GPU 资源紧张到没有这块余量也可以接受短暂中断但一定要有心理预期。还有一个细节Pod 被删除时vLLM 正在处理的请求会被直接切断。解决方案是给 Pod 配置terminationGracePeriodSeconds给 vLLM 留出处理完当前请求的时间。实测下来设置为 120 秒比较合理。再配合 K8s 1.20 之后默认开启的优雅关闭机制Pod 进入 Terminating 状态时会被从 Service endpoints 里摘掉新流量不会再来老请求有足够时间跑完。5. 弹性部署实战HPA、自定义指标与 KEDA5.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 的 metricsPrometheus 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 还是 SpreadK8s 默认调度器不感知 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 化改造最深的体感是故障从“半夜被叫起来看显存有没有爆”变成了“早上看一眼告警面板再安心开会”。这个变化背后是资源抽象和自动调度带来的确定性。我给你的建议只有一条不要盲目追求最复杂的方案先把基础链路跑通再逐步加弹性、加自动化每一步都踩实了再往前挪这套组合能给你的回报一定对得起你投入的时间。
返回列表