在这里插入图片描述

👋 大家好,欢迎来到我的技术博客!
💻 作为一名热爱 Java 与软件开发的程序员,我始终相信:清晰的逻辑 + 持续的积累 = 稳健的成长
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕一个云原生相关话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!


AI 推理服务云原生:LLM 推理服务弹性伸缩配置 🌐🤖

“智能的未来,不在于算力的堆砌,而在于资源的智慧调度。”
大型语言模型(Large Language Models, LLMs)正在重塑人机交互的边界。从 ChatGPT 到 Llama 3,从通义千问到 Claude,LLM 已成为企业智能服务的核心引擎。然而,LLM 推理服务(Inference Service)对计算资源的需求极为苛刻——高显存、高并发、低延迟。在流量波动剧烈的生产环境中,如何实现 高效、低成本、弹性可扩展 的推理服务部署,成为云原生架构的关键挑战。

传统的静态部署方式(如固定 GPU 实例)在面对突发流量时容易过载,而空闲时又造成资源浪费。通过 Kubernetes 的 Horizontal Pod Autoscaler (HPA)Custom MetricsKEDA(Kubernetes Event Driven Autoscaling),我们可以构建一个真正智能的 LLM 推理平台,实现按需扩缩容、秒级响应、成本优化。

本文将深入探讨如何在云原生环境下为 LLM 推理服务配置弹性伸缩策略,涵盖 Prometheus 指标采集、自定义扩缩容逻辑、Java 客户端监控、GPU 资源调度、服务网格集成等核心内容,并提供大量可运行的 Java 代码示例与 mermaid 架构图,助你打造生产级的 AI 推理平台。


🔹 引入:为什么 LLM 推理需要弹性伸缩?

LLM 推理不同于传统 Web 服务。其特点包括:

  • 高延迟敏感性:用户期望秒级响应,延迟超过 2s 即不可接受;
  • GPU 资源密集:单个模型实例可能占用 16GB 以上显存;
  • 请求不均衡:白天高峰 vs 夜间低谷,突发流量(如营销活动)频发;
  • 冷启动代价高:GPU 实例启动 + 模型加载可能耗时数十秒。

若采用固定实例部署,将面临:

  • ❌ 流量高峰时:请求排队、超时、用户体验下降;
  • ❌ 流量低谷时:GPU 空转,资源浪费,成本飙升。

弹性伸缩正是为解决这一矛盾而生。它允许系统根据实时负载动态调整实例数量,实现 性能与成本的最优平衡

graph LR
    A[用户请求] --> B{流量高峰?}
    B -->|是| C[自动扩容]
    B -->|否| D[维持或缩容]
    C --> E[新增推理 Pod]
    E --> F[负载均衡]
    D --> F
    F --> G[LLM 推理服务]
    G --> H[响应用户]
    style C fill:#4CAF50,color:white
    style D fill:#FF9800,color:black
    style G fill:#2196F3,color:white

📘 参考:AWS - Scaling Machine Learning Inference


🔹 组件一:LLM 推理服务基础架构

我们以 Hugging Face Transformers + Triton Inference Server 为例,构建一个支持批量推理的 Java 后端服务。

✅ 技术栈

  • 推理引擎:NVIDIA Triton Inference Server(支持 TensorRT、ONNX、PyTorch)
  • 后端服务:Spring Boot + Java 17
  • 模型格式:ONNX 或 TorchScript
  • 部署平台:Kubernetes + GPU 节点(如 AWS p3.2xlarge)

🐳 Triton Inference Server 部署

# triton-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: triton-inference
spec:
  replicas: 1
  selector:
    matchLabels:
      app: triton
  template:
    metadata:
      labels:
        app: triton
    spec:
      containers:
      - name: triton
        image: nvcr.io/nvidia/tritonserver:23.12-py3
        args:
        - tritonserver
        - --model-repository=/models
        - --strict-model-config=false
        ports:
        - containerPort: 8000 # HTTP
        - containerPort: 8001 # GRPC
        - containerPort: 8002 # Metrics
        volumeMounts:
        - name: model-volume
          mountPath: /models
        resources:
          limits:
            nvidia.com/gpu: 1
      volumes:
      - name: model-volume
        hostPath:
          path: /opt/triton-models
---
apiVersion: v1
kind: Service
metadata:
  name: triton-service
spec:
  selector:
    app: triton
  ports:
    - protocol: TCP
      port: 8000
      targetPort: 8000
  type: ClusterIP

💻 Java 代码:调用 Triton 推理服务

使用 gRPC 客户端调用 Triton(推荐,性能更高)。

// src/main/java/com/ai/inference/client/TritonGrpcClient.java
package com.ai.inference.client;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import org.tensorflow.framework.DataType;
import org.tensorflow.framework.TensorShapeProto;
import com.google.protobuf.Int64Value;
import com.google.protobuf.Value;
import com.google.protobuf.Struct;
import com.nvidia.triton.TritonGrpc;
import com.nvidia.triton.ModelInferRequest;
import com.nvidia.triton.ModelInferResponse;
import com.nvidia.triton.ModelMetadataRequest;
import com.nvidia.triton.ModelMetadataResponse;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class TritonGrpcClient {

    private final ManagedChannel channel;
    private final TritonGrpc.TritonStub asyncStub;

    public TritonGrpcClient(String host, int port) {
        this.channel = ManagedChannelBuilder.forAddress(host, port)
                .usePlaintext() // 生产环境应启用 TLS
                .build();
        this.asyncStub = TritonGrpc.newStub(channel);
    }

    public void infer(String modelName, String inputText) {
        // 构建输入张量
        ModelInferRequest.InferInputTensor inputTensor = ModelInferRequest.InferInputTensor.newBuilder()
                .setName("input_text")
                .setDatatype("BYTES")
                .addShape(1)
                .build();

        // 编码输入
        byte[] textBytes = inputText.getBytes(StandardCharsets.UTF_8);
        ModelInferRequest.InferTensorContents contents = ModelInferRequest.InferTensorContents.newBuilder()
                .addBytesContents(ByteString.copyFrom(textBytes))
                .build();

        // 构建请求
        ModelInferRequest request = ModelInferRequest.newBuilder()
                .setModelName(modelName)
                .addInputs(inputTensor)
                .addInputContents(contents)
                .build();

        // 异步调用
        asyncStub.modelInfer(request, new StreamObserver<ModelInferResponse>() {
            @Override
            public void onNext(ModelInferResponse response) {
                System.out.println("推理结果: " + response.getRawOutputContents(0).toStringUtf8());
            }

            @Override
            public void onError(Throwable t) {
                System.err.println("推理失败: " + t.getMessage());
            }

            @Override
            public void onCompleted() {
                System.out.println("推理完成");
            }
        });
    }

    public void shutdown() {
        channel.shutdown();
    }
}

📘 学习 gRPC:gRPC 官方文档


🔹 组件二:指标采集与监控(Prometheus + Micrometer)

要实现智能扩缩容,必须先能“看见”系统状态。我们使用 Prometheus 采集 LLM 服务的关键指标。

✅ 关键指标

指标名称 说明
llm_request_count 请求总数
llm_request_duration_seconds 请求延迟
llm_gpu_utilization GPU 利用率
llm_pending_requests 待处理请求数(队列长度)
llm_active_workers 活跃工作线程数

💻 Java 代码:自定义指标注册

// src/main/java/com/ai/inference/metrics/LLMMetrics.java
package com.ai.inference.metrics;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;

@Component
public class LLMMetrics {

    private final Counter requestCounter;
    private final DistributionSummary requestDuration;
    private final DistributionSummary gpuUtilization;

    public LLMMetrics(MeterRegistry registry) {
        this.requestCounter = Counter.builder("llm_request_count")
                .description("Total number of LLM inference requests")
                .register(registry);

        this.requestDuration = DistributionSummary.builder("llm_request_duration_seconds")
                .description("Duration of LLM inference requests in seconds")
                .register(registry);

        this.gpuUtilization = DistributionSummary.builder("llm_gpu_utilization")
                .description("GPU utilization percentage")
                .register(registry);
    }

    public void incrementRequest() {
        requestCounter.increment();
    }

    public void recordDuration(double seconds) {
        requestDuration.record(seconds);
    }

    public void recordGPUUtil(double utilization) {
        gpuUtilization.record(utilization);
    }
}

📈 暴露指标端点

// src/main/java/com/ai/inference/controller/MetricsController.java
package com.ai.inference.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MetricsController {

    @Autowired
    private LLMMetrics llmMetrics;

    @GetMapping("/infer")
    public String inference() {
        long start = System.nanoTime();
        // 模拟推理
        try {
            Thread.sleep(800); // 模拟耗时
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        double duration = (System.nanoTime() - start) / 1_000_000_000.0;

        // 记录指标
        llmMetrics.incrementRequest();
        llmMetrics.recordDuration(duration);
        llmMetrics.recordGPUUtil(Math.random() * 100); // 模拟 GPU 利用率

        return "Response from LLM";
    }
}

application.yml 配置:

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  metrics:
    export:
      prometheus:
        enabled: true

📊 Prometheus 配置

# prometheus.yml
scrape_configs:
  - job_name: 'llm-inference'
    static_configs:
      - targets: ['llm-service:8080']

🔹 组件三:基于 HPA 的 CPU/内存扩缩容

Kubernetes 原生的 Horizontal Pod Autoscaler(HPA)支持基于 CPU 和内存的自动扩缩容。

🐳 HPA 配置示例

# hpa-cpu.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: llm-hpa-cpu
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: llm-inference-service
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

⚠️ 问题:CPU 利用率 ≠ 推理负载

LLM 推理的瓶颈通常在 GPU,而非 CPU。单纯依赖 CPU 利用率可能导致:

  • GPU 已满载,但 CPU 仅 30%,无法触发扩容;
  • 或 CPU 因数据预处理高负载,但 GPU 空闲,导致误扩。

因此,必须引入自定义指标


🔹 组件四:基于 KEDA 的事件驱动扩缩容

KEDA 是 CNCF 毕业项目,支持基于外部事件源(如 Kafka、RabbitMQ、Prometheus 指标)的自动扩缩容。

✅ 架构图

Prometheus
KEDA Metrics Server
KEDA Operator
Kubernetes API
LLM Deployment
Pods

🐳 安装 KEDA

helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace

🐳 KEDA ScaledObject 配置

# keda-scaledobject.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: llm-scaledobject
  namespace: default
spec:
  scaleTargetRef:
    name: llm-inference-service
  minReplicaCount: 1
  maxReplicaCount: 20
  triggers:
  - type: prometheus
    metadata:
      serverAddress: http://prometheus-server.default.svc.cluster.local:9090
      metricName: llm_request_count
      threshold: "10"  # 每秒请求数 > 10 时扩容
      query: |
        sum(rate(llm_request_count[2m])) by (job)
  - type: prometheus
    metadata:
      serverAddress: http://prometheus-server.default.svc.cluster.local:9090
      metricName: llm_pending_requests
      threshold: "5"   # 队列长度 > 5 时扩容
      query: |
        llm_pending_requests

💻 Java 代码:模拟请求队列

// src/main/java/com/ai/inference/service/InferenceQueue.java
package com.ai.inference.service;

import com.ai.inference.metrics.LLMMetrics;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

@Service
public class InferenceQueue {

    private final BlockingQueue<String> queue = new LinkedBlockingQueue<>(100);
    private final LLMMetrics llmMetrics;

    @Autowired
    public InferenceQueue(LLMMetrics llmMetrics) {
        this.llmMetrics = llmMetrics;
    }

    public boolean submit(String prompt) {
        boolean offered = queue.offer(prompt);
        if (offered) {
            // 更新待处理请求数
            llmMetrics.recordPendingRequests(queue.size());
        }
        return offered;
    }

    @Scheduled(fixedDelay = 100)
    public void process() {
        String prompt = queue.poll();
        if (prompt != null) {
            // 调用推理服务
            simulateInference(prompt);
            llmMetrics.recordPendingRequests(queue.size());
        }
    }

    private void simulateInference(String prompt) {
        try {
            Thread.sleep(500 + (long)(Math.random() * 1000)); // 模拟耗时
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

📘 KEDA 官方文档:keda.sh/docs


🔹 组件五:GPU 感知调度与资源优化

Kubernetes 支持 GPU 资源调度,但需正确配置。

🐳 节点打标与容忍

# 手动打标(通常由 device plugin 自动完成)
kubectl label nodes gpu-node-1 accelerator=nvidia-tesla-t4
kubectl taint nodes gpu-node-1 accelerator=nvidia-tesla-t4:NoSchedule

🐳 推理服务 Pod 配置

# llm-deployment-gpu.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: llm-inference-service
spec:
  replicas: 1
  selector:
    matchLabels:
      app: llm
  template:
    metadata:
      labels:
        app: llm
    spec:
      containers:
      - name: inference
        image: your-llm-service:1.0
        ports:
        - containerPort: 8080
        resources:
          limits:
            nvidia.com/gpu: 1
            memory: "16Gi"
            cpu: "4"
          requests:
            nvidia.com/gpu: 1
            memory: "8Gi"
            cpu: "2"
        env:
        - name: MODEL_NAME
          value: "llama-3-8b"
      tolerations:
      - key: accelerator
        operator: Equal
        value: nvidia-tesla-t4
        effect: NoSchedule

📊 监控 GPU 使用

使用 dcgm-exporter 将 GPU 指标暴露给 Prometheus:

helm install dcgm-exporter gpu-helm-charts/dcgm-exporter

查询 GPU 利用率:

DCGM_FI_DEV_GPU_UTIL{container="triton"}

🔹 高级策略:预测性扩缩容与成本优化

✅ 基于时间的预测扩缩容

使用 KEDA 的 cron 触发器,在已知高峰前预热实例。

triggers:
- type: cron
  metadata:
    timezone: Asia/Shanghai
    start: 0 8 * * 1-5  # 工作日 8:00
    end: 0 18 * * 1-5   # 工作日 18:00
    desiredReplicas: "5"

✅ 分层部署:CPU vs GPU 实例

  • GPU 实例:处理实时推理;
  • CPU 实例:处理异步任务、批量推理、预热缓存。
// 根据负载类型路由
public String routeInference(String prompt, boolean isRealTime) {
    if (isRealTime) {
        return gpuClient.infer(prompt);
    } else {
        return asyncQueue.submit(prompt);
    }
}

✅ 成本监控

使用 Kubecost 监控 GPU 资源成本。

helm install kubecost kubecost/cost-analyzer --namespace kubecost --create-namespace

📘 Kubecost 免费版:kubecost.com


🔹 服务网格集成:Istio + 流量管理

在多版本推理服务(如 A/B 测试)场景下,Istio 可实现精细化流量控制。

✅ 架构图

客户端
Istio Ingress Gateway
Istio VirtualService
LLM v1
LLM v2
Prometheus

🐳 Istio 配置

# virtual-service.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: llm-routing
spec:
  hosts:
  - llm.example.com
  http:
  - route:
    - destination:
        host: llm-inference-service-v1
      weight: 90
    - destination:
        host: llm-inference-service-v2
      weight: 10

🔹 总结:构建智能的 LLM 推理平台

本文系统阐述了在云原生环境下为 LLM 推理服务配置弹性伸缩的完整方案:

  1. 基础架构:使用 Triton + Spring Boot 构建高性能推理服务;
  2. 指标采集:通过 Micrometer + Prometheus 暴露关键性能指标;
  3. 原生扩缩:HPA 适用于 CPU/内存负载;
  4. 事件驱动:KEDA 实现基于自定义指标(如请求率、队列长度)的精准扩缩;
  5. GPU 调度:合理配置资源请求与容忍,确保 GPU 实例正确调度;
  6. 高级策略:结合预测性扩缩、分层部署与成本监控,实现极致优化。

弹性伸缩不仅是技术实现,更是一种 资源治理哲学。通过智能化的调度,我们让 AI 服务既能应对洪峰,又能静享低谷,真正实现“按需付费、随用随扩”的云原生愿景。

🌐 拓展阅读:

让 AI 推理,如呼吸般自然。💨🧠


🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐