欢迎光临
我们一直在努力

AI 推理服务弹性扩容:从 HPA 到 GPU 感知调度的自动伸缩实践

AI 推理服务弹性扩容:从 HPA 到 GPU 感知调度的自动伸缩实践

一、AI 服务的扩容困境:CPU 规则在 GPU 场景的失效

Kubernetes 原生 HPA(Horizontal Pod Autoscaler)基于 CPU 利用率自动扩缩容,对传统微服务效果良好。但 AI 推理服务的瓶颈在 GPU 而非 CPU,CPU 利用率可能只有 30%,而 GPU 显存已接近满载。更关键的是,LLM 推理服务的延迟与批处理大小强相关——单请求延迟 200ms,10 个并发请求可能需要 2 秒,而 CPU 利用率变化不大。

某 AI 平台使用原生 HPA 管理 LLM 推理服务,CPU 阈值设为 70%,结果在高峰期 CPU 仅 40% 时服务 P99 延迟已达 5 秒,HPA 未触发扩容,用户体验严重劣化。手动将阈值调低到 30% 后,又出现低峰期过度扩容,GPU 资源浪费严重。AI 服务的弹性扩容需要一套基于 GPU 指标和推理延迟的自定义伸缩策略。

二、GPU 感知自动伸缩架构

2.1 整体架构

graph TD
A[Prometheus] –> B[GPU指标采集]
A –> C[推理延迟指标]
A –> D[队列深度指标]

B –> E[自定义指标适配器]
C –> E
D –> E

E –> F[GPU-Aware HPA Controller]
F –> G{伸缩决策引擎}

G –> H[扩容策略]
H –> H1[GPU利用率>80%]
H –> H2[P99延迟>阈值]
H –> H3[队列深度>上限]

G –> I[缩容策略]
I –> I1[GPU利用率<30%]
I –> I2[延迟低于阈值持续5分钟]
I –> I3[缩容冷却期5分钟]

F –> J[Kubernetes API]
J –> K[Deployment Scale]

2.2 伸缩决策流程

sequenceDiagram
participant M as Metrics Server
participant H as HPA Controller
participant D as 决策引擎
participant K as K8s API
participant P as Pod

loop 每15秒
M->>H: 拉取GPU/延迟指标
H->>D: 输入当前指标
D->>D: 计算期望副本数

alt 需要扩容
D–>>H: desiredReplicas=5(当前3)
H->>K: Scale Deployment to 5
K->>P: 创建2个新Pod
Note over P: 新Pod加载模型约30-60秒<br/>需预热后才能接收流量
else 需要缩容
D–>>H: desiredReplicas=2(当前3)
H->>H: 检查冷却期
H->>K: Scale Deployment to 2
K->>P: 优雅终止1个Pod
else 无需调整
D–>>H: desiredReplicas=3(当前3)
end
end

三、生产级 GPU 感知 HPA 实现

3.1 自定义指标采集与暴露

"""
GPU指标采集器 – 部署在每个推理节点
通过nvidia-smi采集GPU指标,暴露为Prometheus指标
"""
import subprocess
import time
import threading
from prometheus_client import Gauge, start_http_server

class GPUMetricsCollector:
"""GPU指标采集与Prometheus暴露"""

def __init__(self, scrape_interval: int = 5):
self.scrape_interval = scrape_interval

# Prometheus指标定义
self.gpu_utilization = Gauge(
'gpu_utilization_percent',
'GPU compute utilization percentage',
['node', 'gpu_index', 'model_name']
)
self.gpu_vram_usage = Gauge(
'gpu_vram_usage_percent',
'GPU VRAM usage percentage',
['node', 'gpu_index', 'model_name']
)
self.gpu_vram_used_mb = Gauge(
'gpu_vram_used_mb',
'GPU VRAM used in MB',
['node', 'gpu_index', 'model_name']
)
self.gpu_temperature = Gauge(
'gpu_temperature_celsius',
'GPU temperature in celsius',
['node', 'gpu_index']
)

def start(self, port: int = 9101):
"""启动指标采集与HTTP暴露"""
start_http_server(port)
collector_thread = threading.Thread(target=self._collect_loop, daemon=True)
collector_thread.start()

def _collect_loop(self):
"""定期采集GPU指标"""
while True:
try:
metrics = self._query_nvidia_smi()
for gpu in metrics:
labels = {
'node': gpu['node_name'],
'gpu_index': str(gpu['index']),
'model_name': gpu['name'],
}
self.gpu_utilization.labels(**labels).set(gpu['utilization'])
self.gpu_vram_usage.labels(**labels).set(gpu['vram_usage_percent'])
self.gpu_vram_used_mb.labels(**labels).set(gpu['vram_used_mb'])
self.gpu_temperature.labels(
node=gpu['node_name'],
gpu_index=str(gpu['index'])
).set(gpu['temperature'])
except Exception as e:
print(f"GPU指标采集异常: {e}")
time.sleep(self.scrape_interval)

def _query_nvidia_smi(self) -> list:
"""
调用nvidia-smi采集GPU指标
使用xml输出格式便于解析
"""
try:
result = subprocess.run(
['nvidia-smi', '–query-gpu=index,name,utilization.gpu,memory.used,memory.total,temperature.gpu',
'–format=csv,noheader,nounits'],
capture_output=True, text=True, timeout=5
)
metrics = []
for line in result.stdout.strip().split('\\n'):
parts = [p.strip() for p in line.split(',')]
if len(parts) >= 6:
vram_total = float(parts[4])
vram_used = float(parts[3])
metrics.append({
'index': int(parts[0]),
'name': parts[1],
'utilization': float(parts[2]),
'vram_used_mb': vram_used,
'vram_total_mb': vram_total,
'vram_usage_percent': (vram_used / vram_total * 100) if vram_total > 0 else 0,
'temperature': float(parts[5]),
'node_name': self._get_node_name(),
})
return metrics
except subprocess.TimeoutExpired:
return []
except Exception:
return []

def _get_node_name(self) -> str:
import os
return os.environ.get('NODE_NAME', 'unknown')

3.2 GPU 感知 HPA 控制器

package hpa

import (
"context"
"math"
"sync"
"time"
)

// ScalingPolicy 伸缩策略配置
type ScalingPolicy struct {
MinReplicas int32 // 最小副本数
MaxReplicas int32 // 最大副本数
CooldownPeriod time.Duration // 缩容冷却期

// 扩容触发条件(满足任一即触发)
ScaleUpGPUUtilization float64 // GPU利用率阈值
ScaleUpP99LatencyMs float64 // P99延迟阈值
ScaleUpQueueDepth int // 请求队列深度阈值

// 缩容触发条件(需全部满足)
ScaleDownGPUUtilization float64 // GPU利用率低于此值
ScaleDownP99LatencyMs float64 // P99延迟低于此值
}

// MetricsSnapshot 当前指标快照
type MetricsSnapshot struct {
AvgGPUUtilization float64
P99LatencyMs float64
QueueDepth int
CurrentReplicas int32
Timestamp time.Time
}

// GPUAwareHPAController GPU感知HPA控制器
type GPUAwareHPAController struct {
policy ScalingPolicy
metrics MetricsSnapshot
lastScaleUp time.Time
lastScaleDown time.Time
mu sync.Mutex
}

func NewGPUAwareHPAController(policy ScalingPolicy) *GPUAwareHPAController {
return &GPUAwareHPAController{
policy: policy,
}
}

// ComputeDesiredReplicas 计算期望副本数
// 核心伸缩算法:基于多指标加权计算
func (c *GPUAwareHPAController) ComputeDesiredReplicas(
ctx context.Context, current MetricsSnapshot) int32 {

c.mu.Lock()
defer c.mu.Unlock()
c.metrics = current

desired := current.CurrentReplicas

// 扩容判断:任一指标超过阈值即触发
needScaleUp := false
if current.AvgGPUUtilization > c.policy.ScaleUpGPUUtilization {
needScaleUp = true
}
if current.P99LatencyMs > c.policy.ScaleUpP99LatencyMs {
needScaleUp = true
}
if current.QueueDepth > c.policy.ScaleUpQueueDepth {
needScaleUp = true
}

if needScaleUp {
// 扩容计算:取各指标计算结果的最大值
gpuBased := c.calculateReplicasByGPU(current)
latBased := c.calculateReplicasByLatency(current)
queueBased := c.calculateReplicasByQueue(current)

desired = int32(math.Max(math.Max(float64(gpuBased), float64(latBased)),
math.Max(float64(queueBased), float64(current.CurrentReplicas))))

// 扩容步长限制:单次最多扩容当前副本数的50%
maxStep := current.CurrentReplicas + int32(math.Ceil(float64(current.CurrentReplicas)*0.5))
if desired > maxStep {
desired = maxStep
}

c.lastScaleUp = time.Now()
}

// 缩容判断:所有指标均低于阈值,且冷却期已过
needScaleDown := current.AvgGPUUtilization < c.policy.ScaleDownGPUUtilization &&
current.P99LatencyMs < c.policy.ScaleDownP99LatencyMs &&
time.Since(c.lastScaleDown) > c.policy.CooldownPeriod &&
time.Since(c.lastScaleUp) > c.policy.CooldownPeriod

if needScaleDown {
// 缩容步长:单次最多缩容1个副本,避免震荡
desired = current.CurrentReplicas – 1
c.lastScaleDown = time.Now()
}

// 边界限制
if desired < c.policy.MinReplicas {
desired = c.policy.MinReplicas
}
if desired > c.policy.MaxReplicas {
desired = c.policy.MaxReplicas
}

return desired
}

// calculateReplicasByGPU 基于GPU利用率计算所需副本数
func (c *GPUAwareHPAController) calculateReplicasByGPU(m MetricsSnapshot) int32 {
if m.AvgGPUUtilization <= 0 {
return m.CurrentReplicas
}
// 目标利用率设为扩容阈值的70%,留出缓冲空间
targetUtil := c.policy.ScaleUpGPUUtilization * 0.7
desired := float64(m.CurrentReplicas) * (m.AvgGPUUtilization / targetUtil)
return int32(math.Ceil(desired))
}

// calculateReplicasByLatency 基于P99延迟计算所需副本数
func (c *GPUAwareHPAController) calculateReplicasByLatency(m MetricsSnapshot) int32 {
if m.P99LatencyMs <= c.policy.ScaleUpP99LatencyMs {
return m.CurrentReplicas
}
// 延迟超标的扩容比例:延迟超标越多,扩容越激进
ratio := m.P99LatencyMs / c.policy.ScaleUpP99LatencyMs
desired := float64(m.CurrentReplicas) * ratio
return int32(math.Ceil(desired))
}

// calculateReplicasByQueue 基于队列深度计算所需副本数
func (c *GPUAwareHPAController) calculateReplicasByQueue(m MetricsSnapshot) int32 {
if m.QueueDepth <= c.policy.ScaleUpQueueDepth {
return m.CurrentReplicas
}
// 每个副本能处理的队列深度估算
perReplicaQueue := float64(c.policy.ScaleUpQueueDepth)
desired := float64(m.QueueDepth) / perReplicaQueue
return int32(math.Ceil(desired))
}

四、AI 弹性扩容的架构权衡

4.1 扩容延迟与模型加载时间

LLM 推理 Pod 从启动到就绪需要 30-60 秒(模型加载+预热),这意味着扩容不是即时的。在突发流量场景下,30 秒的扩容延迟可能导致大量请求超时。应对策略:预留缓冲副本(minReplicas 高于最低需求)、模型预热(启动时发送预热请求)、模型权重预加载到节点本地磁盘。

4.2 缩容震荡与冷却期

缩容后流量突然增加,又触发扩容,形成震荡。冷却期(通常 5-10 分钟)可缓解,但冷却期过长导致资源浪费,过短仍会震荡。更稳健的方案是:缩容采用逐步策略(每次只缩 1 个副本),扩容采用激进策略(可一次扩 50%),这与传统 HPA 的行为一致。

4.3 多模型混部的伸缩冲突

同一集群运行多个模型服务时,GPU 资源是共享的。模型 A 扩容可能挤占模型 B 的资源。需要引入全局资源配额(ResourceQuota)和优先级(PriorityClass),确保核心模型的扩容优先于非核心模型。

4.4 禁用场景

  • 流量稳定且可预测的服务,固定副本数更简单可靠
  • 模型加载时间超过 5 分钟的超大模型,弹性扩容的响应速度无法满足需求
  • GPU 资源固定的私有化部署环境,无法动态申请 GPU 节点

五、总结

AI 推理服务的弹性扩容需要从 CPU 指标驱动转向 GPU 指标与推理延迟联合驱动。GPU 利用率、P99 延迟和请求队列深度是三个核心伸缩指标,扩容策略需激进以应对突发流量,缩容策略需保守以避免震荡。模型加载延迟是弹性伸缩的关键瓶颈,需通过预留缓冲、模型预热等手段缓解。架构选型的核心原则是:根据流量波动特征和 SLA 要求,在资源利用率与服务稳定性之间找到平衡点。

赞(0)
未经允许不得转载:171主机测评 » AI 推理服务弹性扩容:从 HPA 到 GPU 感知调度的自动伸缩实践
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址