AI 服务弹性伸缩:从资源浪费到精准调度的云原生架构实践

cover

一、当 GPU 账单失控:AI 服务的弹性伸缩困境

AI 推理服务的资源消耗远超传统 Web 服务。一个部署了大模型的推理服务,单实例可能需要 1-2 张 GPU,而 GPU 的成本是 CPU 的 10-50 倍。在流量低谷期,闲置的 GPU 资源造成了巨大的成本浪费;在流量高峰期,GPU 资源不足又导致请求排队和超时。

传统基于 CPU 利用率的 HPA(Horizontal Pod Autoscaler)策略在 AI 服务上失效了——GPU 利用率无法准确反映服务的负载状态。一个 GPU 利用率仅 30% 的推理实例,可能已经达到了其并发处理能力的上限,因为推理延迟随并发数的增长呈指数级上升。AI 服务的弹性伸缩需要全新的指标体系和调度策略。

二、AI 服务弹性伸缩的指标模型:从 CPU 利用率到推理延迟

AI 服务的弹性伸缩需要建立基于推理延迟和队列深度的指标体系,而非传统的 CPU/GPU 利用率。

flowchart TD
    A[请求入口] --> B[请求队列]
    B --> C[推理引擎]
    C --> D[响应返回]

    subgraph 伸缩指标
        E[队列深度: pending 请求数]
        F[P99 延迟: 推理响应时间]
        G[吞吐量: QPS/实例]
        H[GPU 显存占用率]
    end

    B --> E
    C --> F
    C --> G
    C --> H

    subgraph 伸缩决策
        I{队列深度 > 阈值?}
        J{P99 延迟 > SLA?}
        K{GPU 显存 > 80%?}
    end

    E --> I
    F --> J
    H --> K

    I -->|是| L[扩容]
    J -->|是| L
    K -->|否| M[可继续接收请求]
    I -->|否| N{队列深度 = 0?}
    N -->|是| O[缩容]

    style L fill:#ff6b6b,color:#fff
    style O fill:#51cf66,color:#fff

三、生产级 AI 服务弹性伸缩实现

3.1 自定义指标采集

// 自定义指标采集器:基于 Micrometer
@Component
public class AIInferenceMetrics {

    private final MeterRegistry registry;
    private final AtomicLong queueDepth = new AtomicLong(0);
    private final AtomicLong activeRequests = new AtomicLong(0);

    public AIInferenceMetrics(MeterRegistry registry) {
        this.registry = registry;
        // 注册自定义指标
        Gauge.builder("ai.inference.queue.depth", queueDepth, AtomicLong::get)
            .description("当前推理队列中的请求数")
            .register(registry);

        Gauge.builder("ai.inference.active.requests", activeRequests, AtomicLong::get)
            .description("当前正在处理的推理请求数")
            .register(registry);
    }

    // 请求入队
    public void onRequestEnqueued() {
        queueDepth.incrementAndGet();
    }

    // 请求开始处理
    public void onRequestStarted() {
        queueDepth.decrementAndGet();
        activeRequests.incrementAndGet();
    }

    // 请求处理完成
    public void onRequestCompleted(long durationMs, boolean success) {
        activeRequests.decrementAndGet();
        // 记录推理延迟分布
        Timer.builder("ai.inference.duration")
            .tag("success", String.valueOf(success))
            .register(registry)
            .record(durationMs, TimeUnit.MILLISECONDS);
    }

    public long getQueueDepth() {
        return queueDepth.get();
    }
}

3.2 K8s 自定义 HPA 策略

# 基于 Prometheus 自定义指标的 HPA 配置
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: ai-inference-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: ai-inference-service
  minReplicas: 2
  maxReplicas: 20
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # 缩容稳定窗口 5 分钟
      policies:
        - type: Percent
          value: 25        # 每次最多缩容 25%
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 30   # 扩容稳定窗口 30 秒
      policies:
        - type: Percent
          value: 100       # 快速扩容:翻倍
          periodSeconds: 30
        - type: Pods
          value: 4         # 或每次增加 4 个 Pod
          periodSeconds: 30
      selectPolicy: Max    # 取更激进的策略
  metrics:
    # 基于队列深度的伸缩
    - type: Pods
      pods:
        metric:
          name: ai_inference_queue_depth
        target:
          type: AverageValue
          averageValue: "5"   # 每个 Pod 平均队列深度不超过 5
    # 基于 P99 延迟的伸缩
    - type: Pods
      pods:
        metric:
          name: ai_inference_duration_seconds
          selector:
            matchLabels:
              quantile: "0.99"
        target:
          type: AverageValue
          averageValue: "2"   # P99 延迟不超过 2 秒

3.3 预测性伸缩:基于流量模式的预扩容

// 基于历史流量模式的预测性伸缩
@Service
public class PredictiveScaler {

    private final KubernetesClient k8sClient;
    private final TrafficPatternRepository patternRepo;

    // 每小时分析流量模式,预测下一时段的负载
    @Scheduled(cron = "0 0 * * * *")
    public void predictAndPreScale() {
        int currentHour = LocalTime.now().getHour();
        int nextHour = (currentHour + 1) % 24;

        // 获取过去 7 天同一时段的平均 QPS
        double predictedQps = patternRepo.getAverageQpsForHour(nextHour);

        // 根据预测 QPS 计算所需实例数
        int desiredReplicas = calculateDesiredReplicas(predictedQps);

        // 获取当前实例数
        int currentReplicas = getCurrentReplicas();

        // 仅在预测需要扩容时提前操作(缩容由 HPA 自然处理)
        if (desiredReplicas > currentReplicas) {
            preScale(desiredReplicas);
        }
    }

    private int calculateDesiredReplicas(double predictedQps) {
        // 每实例最大 QPS(通过基准测试确定)
        double maxQpsPerInstance = 15.0;
        // 保留 20% 的缓冲余量
        double safetyFactor = 1.2;

        return (int) Math.ceil(predictedQps / maxQpsPerInstance * safetyFactor);
    }

    private void preScale(int replicas) {
        k8sClient.apps().deployments()
            .inNamespace("production")
            .withName("ai-inference-service")
            .scale(replicas);

        log.info("预测性扩容: 目标实例数={}", replicas);
    }
}

3.4 GPU 感知的调度策略

# GPU 感知的 Pod 调度配置
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ai-inference-service
spec:
  template:
    spec:
      # 优先调度到 GPU 利用率较低的节点
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              preference:
                matchExpressions:
                  - key: gpu-utilization
                    operator: Lt
                    values:
                      - "50"   # 优先选择 GPU 利用率低于 50% 的节点
      containers:
        - name: inference
          image: ai-inference:latest
          resources:
            limits:
              nvidia.com/gpu: 1    # 请求 1 块 GPU
            requests:
              nvidia.com/gpu: 1
              memory: "8Gi"
              cpu: "4"
          env:
            - name: MAX_BATCH_SIZE
              value: "32"          # 最大批处理大小
            - name: MAX_CONCURRENT
              value: "64"          # 最大并发请求数

四、弹性伸缩的代价与架构权衡

AI 服务弹性伸缩方案的代价需要审慎评估:

GPU 实例的冷启动延迟:GPU 实例的启动时间远超 CPU 实例,通常需要 30-60 秒加载模型权重。这意味着扩容操作无法即时生效,在突发流量场景下仍可能出现服务降级。预热策略(Pre-warming)可以缓解,但预热实例本身就是资源浪费。

缩容的稳定性风险:GPU 实例缩容后,如果流量快速回升,重新扩容的延迟可能导致服务不可用。缩容稳定窗口设得太短会增加抖动风险,设得太长则浪费资源。需要根据业务流量特征调优,但流量模式可能随运营活动突变。

预测性伸缩的准确性依赖:基于历史模式的预测在常规时段表现良好,但无法应对突发流量(如营销活动、热点事件)。需要结合实时指标做二次修正,但修正逻辑的复杂度随策略数量增长。

适用边界:自定义 HPA 适合 QPS 波动明显、GPU 成本敏感的推理服务。预测性伸缩适合流量模式规律的服务(如 B 端 SaaS 工作时段集中)。

禁用场景:当推理服务使用超大模型(如 70B+ 参数),单实例需要多卡甚至多机时,弹性伸缩的粒度太粗,应采用固定资源 + 请求排队的策略。

五、总结

AI 服务的弹性伸缩需要从传统基于 CPU 利用率的模型转向基于推理延迟和队列深度的指标体系。自定义 HPA 实现了基于业务指标的精准伸缩,预测性伸缩通过历史流量模式提前预热实例,GPU 感知调度优化了资源分配效率。但 GPU 实例的冷启动延迟是弹性伸缩的天花板,缩容稳定性与资源利用率之间存在固有矛盾。在实际落地中,建议采用"预测性预扩容 + 自定义 HPA 微调 + 缩容保守策略"的组合方案,在成本与可用性之间找到适合业务特征的平衡点。

Logo

脑启社区是一个专注类脑智能领域的开发者社区。欢迎加入社区,共建类脑智能生态。社区为开发者提供了丰富的开源类脑工具软件、类脑算法模型及数据集、类脑知识库、类脑技术培训课程以及类脑应用案例等资源。

更多推荐