大模型服务弹性伸缩:从 GPU 利用率到 K8s HPA 的全链路实战
大模型服务弹性伸缩从 GPU 利用率到 K8s HPA 的全链路实战一、Token 洪峰与 GPU 空转LLM 服务弹性伸缩的容量困局在大模型服务落地的工程实践中流量模型与传统 Web 服务存在本质差异。传统 HTTP 服务的请求耗时通常在毫秒到百毫秒量级而一次 LLM 推理请求的响应时间可能从数百毫秒到数十秒不等且与输入/输出 Token 数量强相关。这种长尾延迟特性使得传统的 QPS 驱动扩缩容策略完全失效——当 GPU 利用率已经达到 90% 时排队等待的请求可能已经堆积了数百条而 HPA 仍在等待平均 CPU 利用率超过阈值才开始扩容。更严峻的现实是 GPU 资源的成本。一张 A100 显卡的月租成本在万元级别如果在业务低峰期维持与高峰期相同的推理副本数仅 GPU 空转带来的资源浪费就足以让运维预算失控。通过压测验证在典型的对话式 LLM 服务场景中工作时段与凌晨时段的请求量差异可达 15:1这意味着如果没有弹性伸缩能力要么在高峰期承受严重的请求超时要么在低峰期承受巨额的资源浪费。核心痛点可以归纳为三点第一传统指标CPU 利用率无法准确反映 GPU 推理负载第二LLM 推理冷启动耗时长达 30-60 秒导致扩容滞后第三缩容策略过于激进会引发请求中断过于保守则无法回收资源。二、从指标采集到伸缩决策LLM 弹性伸缩的底层机制要实现精准的 LLM 服务弹性伸缩必须建立一套从硬件指标到编排调度的完整链路。这套链路的核心在于用正确的指标驱动正确的决策并在决策与执行之间插入合理的缓冲。flowchart TD A[GPU Metrics Exporter] --|GPU利用率/显存占用| B[Prometheus] C[vLLM Metrics Endpoint] --|KV Cache利用率/请求队列深度| B D[Application Metrics] --|平均Token延迟/P99延迟| B B --|PromQL 查询| E[Custom Metrics Adapter] E --|转换指标| F[K8s HPA Controller] F --|扩缩容决策| G[Deployment Controller] G --|创建/删除 Pod| H[GPU Node Pool] H --|节点状态反馈| A subgraph 冷启动优化 I[预热池 Warm Pool] --|预加载模型权重| J[待命 Pod] J --|秒级就绪| G end subgraph 缩容保护 K[优雅终止 Graceful Shutdown] --|等待推理完成| L[Pod 终止] M[请求排空 Drain] --|拒绝新请求| K end指标层GPU 利用率和显存占用是基础指标但仅靠这两个指标无法反映推理服务的真实负载状态。vLLM 等推理框架暴露的 KV Cache 利用率是更精准的负载指标——当 KV Cache 利用率超过 80% 时意味着 PagedAttention 的内存池即将耗尽新请求将被迫排队等待。请求队列深度则直接反映了当前积压的推理任务数量。决策层K8s HPA 的默认行为是 30 秒采样一次指标计算期望副本数后等待冷却窗口。对于 LLM 服务这个延迟链条太长。通过 Custom Metrics Adapter 将 Prometheus 中的自定义指标注册到 HPA配合behavior字段中的快速扩容策略和保守缩容策略可以在 60 秒内完成从指标异常到新 Pod 就绪的全流程。执行层LLM 推理 Pod 的冷启动是最大的延迟瓶颈。加载一个 70B 参数模型的权重到 GPU 需要 30-60 秒这还不算模型初始化和预热的时间。解决方案是维护一个 Warm Pool——始终保持 1-2 个已加载模型权重的待命 Pod当扩容触发时直接将请求路由到待命 Pod将就绪时间从分钟级压缩到秒级。三、生产级弹性伸缩配置与代码实现以下是基于 vLLM K8s HPA 的完整生产级配置包含自定义指标、伸缩策略和优雅终止的全部实现。Prometheus 自定义指标采集配置# prometheus-llm-rules.yaml groups: - name: llm_inference_metrics interval: 10s # 10秒采集一次比默认60秒更频繁 rules: # KV Cache 利用率反映推理引擎内存压力 - record: llm:kv_cache_utilization:ratio expr: | vllm_num_active_requests / vllm_max_num_seqs # 请求队列深度反映积压程度 - record: llm:request_queue_depth:gauge expr: | vllm_waiting_requests # 平均 Token 生成延迟 - record: llm:token_latency:avg expr: | rate(vllm_generation_time_sum[1m]) / rate(vllm_generation_time_count[1m]) # GPU 显存利用率 - record: llm:gpu_memory_utilization:ratio expr: | DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTALCustom Metrics Adapter 注册# custom-metrics-adapter.yaml apiVersion: apps/v1 kind: Deployment metadata: name: custom-metrics-adapter spec: template: spec: containers: - name: adapter image: registry.k8s.io/prometheus-adapter/prometheus-adapter:v0.11.2 args: - --prometheus-urlhttp://prometheus:9090 - --metrics-relist-interval30s - --v4 # 注册自定义指标规则 - --config/etc/adapter/config.yaml volumeMounts: - name: config mountPath: /etc/adapter volumes: - name: config configMap: name: adapter-config --- apiVersion: v1 kind: ConfigMap metadata: name: adapter-config data: config.yaml: | rules: - seriesQuery: llm:kv_cache_utilization:ratio resources: overrides: namespace: {resource: namespace} pod: {resource: pods} metricsQuery: llm:kv_cache_utilization:ratio - seriesQuery: llm:request_queue_depth:gauge resources: overrides: namespace: {resource: namespace} pod: {resource: pods} metricsQuery: llm:request_queue_depth:gaugeHPA 弹性伸缩核心配置# llm-hpa.yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: llm-inference-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: llm-inference minReplicas: 2 # 最少保持2个副本确保高可用 maxReplicas: 20 # 最大20副本受GPU节点池容量约束 metrics: # 核心指标KV Cache利用率超过70%触发扩容 - type: Pods pods: metric: name: kv_cache_utilization_ratio target: type: Utilization averageUtilization: 70 # 辅助指标队列深度超过10触发扩容 - type: Pods pods: metric: name: request_queue_depth_gauge target: type: AverageValue averageValue: 10 # 兜底指标GPU显存利用率超过85% - type: Pods pods: metric: name: gpu_memory_utilization_ratio target: type: Utilization averageUtilization: 85 behavior: scaleUp: # 快速扩容30秒内最多扩容5个副本 stabilizationWindowSeconds: 30 policies: - type: Pods value: 5 periodSeconds: 60 - type: Percent value: 100 periodSeconds: 60 selectPolicy: Max scaleDown: # 保守缩容5分钟观察窗口每次最多缩1个副本 stabilizationWindowSeconds: 300 policies: - type: Pods value: 1 periodSeconds: 120 selectPolicy: Min优雅终止与请求排空# graceful_shutdown.py import signal import sys import time import threading from http.server import HTTPServer, BaseHTTPRequestHandler # 全局标志是否接受新请求 accepting_requests True # 当前正在处理的推理请求数 active_requests 0 active_lock threading.Lock() class HealthHandler(BaseHTTPRequestHandler): 健康检查处理器配合K8s探针实现请求排空 def do_GET(self): if self.path /health: # 终止信号后健康检查失败K8s将新请求路由到其他Pod if not accepting_requests: self.send_response(503) self.end_headers() self.wfile.write(bdraining) return self.send_response(200) self.end_headers() self.wfile.write(bok) elif self.path /ready: # 就绪探针排空期间标记为不可就绪 if not accepting_requests: self.send_response(503) self.end_headers() return self.send_response(200) self.end_headers() def handle_shutdown(signum, frame): 处理SIGTERM信号启动优雅终止流程 global accepting_requests print(f收到终止信号 {signum}开始排空请求...) # 第一步停止接受新请求 accepting_requests False # 第二步等待活跃请求完成最多等待120秒 wait_start time.time() while time.time() - wait_start 120: with active_lock: if active_requests 0: print(所有推理请求已完成安全退出) sys.exit(0) time.sleep(2) # 第三步超时后强制退出 print(f等待超时仍有 {active_requests} 个请求未完成强制退出) sys.exit(1) # 注册信号处理器 signal.signal(signal.SIGTERM, handle_shutdown) if __name__ __main__: server HTTPServer((0.0.0.0, 8080), HealthHandler) server.serve_forever()Warm Pool 预热 Pod 配置# warm-pod-priority.yaml apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: warm-pool-low-priority value: -1 # 低优先级资源紧张时优先被驱逐 globalDefault: false preemptionPolicy: Never description: 预热池Pod使用低优先级不抢占其他工作负载 --- apiVersion: apps/v1 kind: Deployment metadata: name: llm-warm-pool spec: replicas: 2 # 始终保持2个预热Pod template: spec: priorityClassName: warm-pool-low-priority containers: - name: llm-inference image: llm-server:v2.1 # 启动时预加载模型权重到GPU command: [python, -m, vllm.entrypoint.openai.api_server] args: - --model/models/qwen-72b - --preload-model # 启动时即加载模型不等待首个请求 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 45 # 模型加载需要时间 periodSeconds: 5四、弹性伸缩的代价延迟、成本与一致性的三重博弈弹性伸缩并非银弹在 LLM 服务场景中引入弹性能力的同时必须正视以下 Trade-offs冷启动延迟的不可消除性。即使有 Warm Pool预热 Pod 从就绪到真正能处理请求仍需 5-10 秒的模型预热时间首次推理的 KV Cache 分配和 CUDA Kernel 编译。在突发流量场景下这 5-10 秒的延迟可能导致请求超时。实测数据显示70B 模型的首次推理延迟比稳态高出 3-5 倍。如果业务对 P99 延迟有严格要求如低于 2 秒则必须维持更高的基础副本数这直接削弱了弹性伸缩的成本收益。缩容震荡风险。LLM 服务的流量模式往往呈现锯齿形——对话式应用的请求间隔不均匀可能在几分钟内出现多次波峰波谷。如果缩容冷却窗口设置过短Pod 会被反复创建和销毁不仅浪费计算资源还会导致 GPU 节点的频繁分配与释放。通过压测验证在冷却窗口为 300 秒时缩容震荡率从 120 秒窗口的 35% 降低到 8%但资源回收效率也相应下降了约 20%。指标采集的精度与开销矛盾。10 秒的采集间隔虽然能更快感知负载变化但高频采集本身会消耗 Prometheus 的存储和查询资源。在管理 100 推理 Pod 的集群中自定义指标的高频采集可能导致 Prometheus 内存占用增加 30% 以上。需要在采集精度和监控基础设施成本之间找到平衡点。适用边界弹性伸缩最适合流量波动幅度大峰谷比 5:1且对延迟容忍度较高的离线推理场景。对于延迟敏感的在线对话场景建议采用固定基线 弹性缓冲的混合策略——维持足够处理稳态流量的基础副本数弹性副本仅用于吸收突发流量。五、总结LLM 服务的弹性伸缩是一项系统工程从指标定义、决策策略到执行保障每个环节都需要针对 GPU 推理的特殊性进行定制化设计。核心落地步骤如下指标体系建设部署 DCGM Exporter 采集 GPU 硬件指标启用 vLLM 的 Prometheus Metrics 端点采集推理引擎指标重点关注 KV Cache 利用率和请求队列深度。HPA 策略调优采用多指标联合触发机制扩容策略设置快速响应窗口30 秒缩容策略设置保守冷却窗口300 秒避免震荡。冷启动优化维护 Warm Pool 预热 Pod使用低优先级 PriorityClass 确保不影响正常工作负载启动参数开启--preload-model。优雅终止保障实现 SIGTERM 信号处理器终止时先标记不可就绪排空新请求等待活跃推理完成后再退出超时阈值建议设为 120 秒。容量规划兜底弹性伸缩不能替代容量规划必须根据业务 SLA 确定合理的基础副本数弹性能力仅作为流量波动的缓冲层。