(第三篇)Spring AI 架构设计与优化:容器化与云原生部署,基于 K8s 的 AI 应用全生命周期管理

摘要

大家好,我是深耕 Spring AI 架构的后端开发。在这个系列的前两篇文章里,我给大家分享了万级 QPS 分布式架构搭建、全链路可观察性体系落地的实战经验。但架构和监控搭完,真正的线上运维难题才刚刚开始:本地能跑的 AI 服务,到线上就因为环境不一致报错;高峰期要扩容,手动装 JDK、配环境搞了半个多小时,错过了流量峰值;发布新版本要一台台重启虚拟机,回滚慢、风险高,还经常出现 "一半成功一半失败" 的尴尬局面。

痛定思痛,我们带着团队把 Spring AI 服务做了完整的云原生容器化改造,用 Docker 做镜像打包,K8s 做编排管理,GitLab CI/CD 做自动部署,还深度整合了 Spring Cloud Kubernetes,实现了配置热更新、原生服务发现。改造完成后,我们的服务发布时间从 1 小时缩短到 5 分钟,扩容从手动半小时变成秒级自动伸缩,环境一致性问题彻底解决,资源利用率从 30% 提升到了 70%。

这篇文章我会把整个改造过程、核心优化方案、踩过的坑全部分享给大家,从 Docker 镜像优化、K8s 全资源配置、弹性伸缩策略,到完整的 CI/CD 流水线落地,全程配套可直接复制的代码和插图,看完就能直接落地到你的 Spring AI 项目里。

1. 引言:为什么 Spring AI 服务必须做云原生容器化改造?

先给大家看看我们改造前的运维噩梦,相信做 AI 服务的兄弟们都有共鸣:

  • 环境一致性问题:本地开发用 JDK17,线上虚拟机是 JDK11,本地跑的好好的 RAG 功能,线上直接报错;本地模型依赖的 CUDA 版本,线上和本地不一致,服务直接起不来;
  • 扩容效率极低:业务搞活动,流量峰值是平时的 10 倍,手动开虚拟机、装 JDK、配环境、拉代码、启动服务,一套流程下来半个多小时,流量峰值都过去了;
  • 发布风险极高:每次发布要一台台登录虚拟机重启服务,不仅慢,还经常出现 "一半实例更新成功,一半更新失败" 的情况,回滚要一台台操作,线上故障恢复时间长达几十分钟;
  • 资源利用率极低:为了应对高峰期,每台虚拟机都按峰值配置了 8C16G,平时低峰期 CPU 利用率只有 10% 不到,大量资源被浪费;
  • 配置管理混乱:模型 API Key、向量数据库地址、Prompt 模板这些配置,要么写死在代码里,要么散落在每台虚拟机的配置文件里,改个配置要重启所有服务,还经常出现漏改、错改的情况。

这些问题,靠传统的虚拟机部署根本解决不了,而云原生容器化就是最佳解决方案。Docker 把应用和依赖一起打包成镜像,彻底解决了环境一致性问题;K8s 实现了应用的自动化编排、秒级扩容、滚动发布、一键回滚;Spring Cloud Kubernetes 让 Spring 应用能无缝对接 K8s 的原生能力,不用再依赖 Eureka、Nacos 这些中间件,架构更轻量。

对于 Spring AI 服务来说,云原生改造的价值更是被放大了:AI 服务有明显的流量波峰波谷,弹性伸缩能极大降低资源成本;AI 服务依赖的模型、环境复杂,镜像打包能彻底解决依赖问题;AI 服务的配置变更频繁,ConfigMap 能实现热更新,不用重启服务。

2. 核心架构概览:Spring AI 云原生部署全景架构

先给大家看一下我们最终落地的 Spring AI 云原生部署全景架构图,这套架构稳定运行了半年,支撑了多次大流量活动,零线上故障:

整个架构从上到下分为四大核心层,完全贴合云原生最佳实践:

  1. 研发与 CI/CD 层:从代码提交到自动部署的全流程自动化,实现 AI 应用的持续集成与持续交付;
  2. 镜像仓库层:存储 Spring AI 应用的 Docker 镜像,做版本管理、安全扫描,是环境一致性的核心保障;
  3. K8s 集群层:整个架构的核心,负责应用的编排、调度、服务发现、配置管理、弹性伸缩,是 AI 服务稳定运行的底座;
  4. 可观察性层:和前两篇文章的监控体系联动,实现容器、Pod、应用全维度的监控、日志、链路追踪。

接下来的章节,我会给大家拆解每一层的落地细节,从代码到配置,从优化到踩坑,全部分享给大家。

3. Docker 镜像优化:多阶段构建 + 分层缓存,极致压缩镜像体积

3.1 AI 服务镜像的核心痛点:为什么你的镜像又大又慢?

很多人第一次打包 Spring AI 的 Docker 镜像,都会遇到同样的问题:镜像体积太大,动辄 1G 以上,不仅拉取慢,占用大量磁盘空间,还会导致扩容、发布的时间变长。

Spring AI 服务的镜像之所以容易变大,核心原因有三个:

  1. 直接用 fat jar 打包:把 JDK、所有依赖、编译代码、源码都打包到了一个镜像里,大量无用的构建环境依赖被带到了运行环境;
  2. 基础镜像选型错误:用了完整的 openjdk 镜像,甚至是 ubuntu 镜像,自带了大量无用的系统工具和依赖;
  3. AI 服务额外依赖多:如果部署本地大模型,还会带 CUDA、Python 环境、模型文件,镜像体积会进一步膨胀。

镜像太大带来的直接问题就是:发布慢、扩容慢、拉取镜像容易超时,尤其是高峰期扩容的时候,镜像还没拉完,流量峰值已经过去了。所以,镜像优化是云原生改造的第一步,也是最关键的一步。

3.2 多阶段构建:分离构建与运行环境,从源头减体积

Docker 多阶段构建的核心思想,就是把构建环境和运行环境彻底分离:用一个完整的 JDK 镜像做构建,编译代码、打包 jar 包;然后用一个精简的 JRE 镜像做运行环境,只把构建好的 jar 包和运行需要的依赖复制过来,构建环境里的所有东西都不会被带到最终的镜像里。

先给大家看一下优化前的错误 Dockerfile,相信很多人都是这么写的:

bash 复制代码
# 错误示例:单阶段构建,镜像体积大,有安全风险
FROM openjdk:17-jdk
WORKDIR /app
COPY . .
# 直接在镜像里构建,把maven、源码、编译缓存都带到了最终镜像里
RUN ./mvnw clean package -DskipTests
EXPOSE 8080
# 用完整的JDK运行,自带了大量无用的开发工具
CMD ["java", "-jar", "target/spring-ai-service.jar"]

这个 Dockerfile 打出来的镜像,体积直接到了 1.2G,里面包含了 maven、源码、编译缓存、完整的 JDK,大量无用的内容。

再给大家看优化后的多阶段构建 Dockerfile,也是我们生产环境在用的版本:

bash 复制代码
# 第一阶段:构建阶段,用完整的JDK + Maven,只负责编译打包
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /build
# 先复制pom.xml,下载依赖,利用Docker缓存,不用每次构建都重新下载依赖
COPY pom.xml .
COPY mvnw .
COPY .mvn .mvn
RUN ./mvnw dependency:go-offline -DskipTests
# 再复制源码,编译打包
COPY src ./src
RUN ./mvnw clean package -DskipTests

# 第二阶段:运行阶段,用精简的JRE镜像,只复制构建好的jar包
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# 设置时区,避免日志时间不对
RUN apk add --no-cache tzdata
ENV TZ=Asia/Shanghai
# 从构建阶段复制打包好的jar包,只复制这一个文件
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
# 启动参数优化,适配容器环境
CMD ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

这个多阶段构建的 Dockerfile,打出来的镜像体积直接降到了 400M,比优化前缩小了 2/3。

3.3 极致优化:JRE 裁剪 + 分层构建 + 基础镜像选型

如果想把镜像体积压缩到极致,还可以做这三个优化,我们就是靠这几步,把镜像从 400M 进一步压缩到了 180M。

JDK9 之后提供了 jlink 工具,可以把 JRE 裁剪成只包含项目需要的模块,不用带完整的 JRE。Spring AI 项目只需要 java.base、java.logging、java.sql 等核心模块,裁剪后的 JRE 体积只有几十 M。

给大家看一下裁剪 JRE 的多阶段构建 Dockerfile:

bash 复制代码
# 第一阶段:构建JRE + 编译打包
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /build
# 第一步:用jlink裁剪JRE,只保留需要的模块
RUN jlink \
    --module-path $JAVA_HOME/jmods \
    --add-modules java.base,java.logging,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument \
    --output /minimal-jre \
    --strip-debug \
    --no-man-pages \
    --no-header-files \
    --compress=2

# 第二步:编译打包Spring AI项目
COPY pom.xml .
COPY mvnw .
COPY .mvn .mvn
RUN ./mvnw dependency:go-offline -DskipTests
COPY src ./src
RUN ./mvnw clean package -DskipTests

# 第二阶段:运行阶段,用alpine基础镜像 + 裁剪后的JRE
FROM alpine:3.19
WORKDIR /app
# 安装必要的依赖
RUN apk add --no-cache tzdata libc6-compat
ENV TZ=Asia/Shanghai
# 从构建阶段复制裁剪后的JRE
COPY --from=builder /minimal-jre /opt/minimal-jre
ENV JAVA_HOME=/opt/minimal-jre
ENV PATH=$JAVA_HOME/bin:$PATH
# 复制构建好的jar包
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
CMD ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

用 jlink 裁剪后,镜像体积直接从 400M 降到了 200M 以内。

2. 分层构建,利用 Docker 缓存加速构建

Spring Boot 的 jar 包里,依赖包占了 90% 以上的体积,而我们的代码变更频率远高于依赖变更频率。所以我们可以把 jar 包分层,把依赖包和代码分开,依赖包单独放一层,代码放另一层,这样只要依赖不变,构建的时候就会复用缓存,不用每次都重新复制依赖包,构建速度能提升 80% 以上。

Spring Boot 2.3 + 已经内置了分层打包的能力,只需要在 pom.xml 里开启:

XML 复制代码
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <!-- 开启分层打包 -->
                <layers>
                    <enabled>true</enabled>
                </layers>
            </configuration>
        </plugin>
    </plugins>
</build>

然后在 Dockerfile 里分层复制:

bash 复制代码
# 从构建好的jar包里提取分层内容
COPY --from=builder /build/target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
# 先复制依赖层,这层很少变,会被缓存
COPY --from=builder /build/dependencies/ ./
COPY --from=builder /build/spring-boot-loader/ ./
COPY --from=builder /build/snapshot-dependencies/ ./
# 最后复制应用层,代码变更只会影响这一层
COPY --from=builder /build/application/ ./
3. 基础镜像选型避坑

给大家总结一下 Spring AI 服务的基础镜像选型建议,按优先级排序:

  1. 生产环境首选 :eclipse-temurin:17-jre-alpine,体积小、安全、稳定,适配 Spring Boot 3.x;
  2. 极致体积首选 :alpine + jlink 裁剪后的 JRE,体积最小,适合对镜像大小有极致要求的场景;
  3. 绝对不要用 :ubuntu、centos这类完整的系统镜像,体积大,有安全风险;也不要用过时的openjdk镜像,已经停止维护了。

3.4 GPU 镜像专项优化:本地模型部署踩坑指南

如果你的 Spring AI 服务需要部署本地大模型,需要用到 GPU,镜像优化会更复杂,这里给大家分享两个核心踩坑经验:

  1. 基础镜像必须用官方 CUDA 镜像 :NVIDIA 官方提供的nvidia/cuda镜像,已经适配好了显卡驱动、CUDA、cuDNN,不要自己在 ubuntu 里装 CUDA,很容易出现驱动版本不兼容的问题;
  2. 多阶段构建分离模型文件:大模型的文件动辄几个 G,不要把模型文件直接打包到镜像里,否则镜像体积会爆炸。应该用 K8s 的 PV/PVC 挂载模型文件,或者用对象存储在服务启动时拉取,镜像里只放代码和运行环境。

给大家看一下 GPU 镜像的极简 Dockerfile 示例:

bash 复制代码
# 构建阶段
FROM nvidia/cuda:12.1.1-cudnn8-devel-ubuntu22.04 AS builder
WORKDIR /build
# 安装JDK、Maven,编译打包Spring AI项目
RUN apt-get update && apt-get install -y openjdk-17-jdk maven
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

# 运行阶段,用runtime镜像,体积比devel镜像小很多
FROM nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04
WORKDIR /app
RUN apt-get update && apt-get install -y openjdk-17-jre tzdata
ENV TZ=Asia/Shanghai
# 只复制构建好的jar包,模型文件用PVC挂载
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]

用 runtime 镜像代替 devel 镜像,镜像体积能缩小一半以上。

3.5 踩坑实录:1.2G 镜像优化到 180M 的全过程

我第一次打包 Spring AI 镜像的时候,用了单阶段构建,打出来的镜像 1.2G,每次拉取镜像要 5 分钟,高峰期扩容的时候,镜像还没拉完,流量峰值就过去了。

后来我们一步步优化:

  1. 改成多阶段构建,分离构建和运行环境,镜像体积降到 400M;
  2. 开启 Spring Boot 分层构建,利用 Docker 缓存,构建时间从 10 分钟降到 1 分钟;
  3. 用 jlink 裁剪 JRE,只保留需要的模块,镜像体积降到 200M 以内;
  4. 把基础镜像换成 alpine,清理无用的系统依赖,最终镜像体积稳定在 180M。

优化完成后,镜像拉取时间从 5 分钟降到了 30 秒以内,高峰期扩容的速度提升了 10 倍,再也没出现过镜像拉取超时的问题。

4. K8s 部署实战:Deployment+Service+ConfigMap 全配置落地

4.1 资源规划:AI 服务在 K8s 里的核心资源选型

在部署之前,我们先要做好资源规划,AI 服务和普通 Java 服务的资源需求有很大的区别,核心要关注这几点:

  1. CPU / 内存资源:Spring AI 服务是 IO 密集型服务,普通的 API 调用场景,单 Pod 配置 2C4G 足够;如果有本地向量化、轻量模型推理,需要配置 4C8G 以上;
  2. GPU 资源:如果部署本地大模型,需要给 Pod 配置 GPU 资源,K8s 支持 NVIDIA GPU 调度,需要提前安装 NVIDIA GPU Operator;
  3. 存储资源:模型文件、向量索引文件需要用 PV/PVC 做持久化存储,不要放在容器的本地存储里,容器重启后数据会丢失;
  4. 副本数规划:生产环境最少配置 2 个副本,避免单点故障,用 HPA 做弹性伸缩,应对流量波动。

4.2 完整部署 YAML:Deployment+Service+ConfigMap+Secret

我把我们生产环境在用的 Spring AI 服务 K8s 部署 YAML 整理好了,大家可以直接复制修改使用,包含了所有核心资源:

1. ConfigMap:管理应用配置,不用写死在镜像里
bash 复制代码
apiVersion: v1
kind: ConfigMap
metadata:
  name: spring-ai-service-config
  namespace: ai-service
data:
  application.yml: |
    spring:
      application:
        name: spring-ai-service
      # AI模型配置,从ConfigMap管理,不用重启服务就能修改
      ai:
        openai:
          api-key: ${OPENAI_API_KEY}
          base-url: https://api.openai.com
          chat:
            options:
              model: gpt-3.5-turbo
              temperature: 0.7
      # 向量数据库配置
      milvus:
        client:
          address: milvus-service.ai-service.svc.cluster.local:19530
    # 服务端口
    server:
      port: 8080
    # 监控端点配置
    management:
      endpoints:
        web:
          exposure:
            include: prometheus,health,info
2. Secret:管理敏感信息,比如 API Key、数据库密码
bash 复制代码
apiVersion: v1
kind: Secret
metadata:
  name: spring-ai-service-secret
  namespace: ai-service
type: Opaque
# 敏感数据用base64编码,不要明文写在这里
data:
  openai-api-key: eW91ci1vcGVuYWktYXBpLWtleQ==
3. Deployment:管理 Spring AI 应用的 Pod,核心部署配置
bash 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-ai-service
  namespace: ai-service
  labels:
    app: spring-ai-service
spec:
  # 副本数,生产环境最少2个
  replicas: 3
  # 滚动发布策略
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  # 标签选择器
  selector:
    matchLabels:
      app: spring-ai-service
  # Pod模板
  template:
    metadata:
      labels:
        app: spring-ai-service
    spec:
      # 容器配置
      containers:
        - name: spring-ai-service
          # 镜像地址,改成你自己的镜像仓库地址
          image: harbor.your-domain.com/ai-service/spring-ai-service:latest
          imagePullPolicy: Always
          # 端口配置
          ports:
            - containerPort: 8080
              name: http
          # 环境变量,从Secret里读取敏感信息
          env:
            - name: OPENAI_API_KEY
              valueFrom:
                secretKeyRef:
                  name: spring-ai-service-secret
                  key: openai-api-key
          # 配置文件挂载,从ConfigMap读取
          volumeMounts:
            - name: config
              mountPath: /app/config
            # 模型文件挂载,用PVC持久化
            - name: model-data
              mountPath: /app/model
          # 资源限制,避免Pod占用过多资源
          resources:
            requests:
              cpu: "2"
              memory: "4Gi"
            limits:
              cpu: "4"
              memory: "8Gi"
          # 存活探针:检测Pod是否正常运行,异常则重启
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: http
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 3
          # 就绪探针:检测Pod是否可以接收流量,没就绪不会加入Service负载
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: http
            initialDelaySeconds: 20
            periodSeconds: 5
            failureThreshold: 2
      # 数据卷配置
      volumes:
        - name: config
          configMap:
            name: spring-ai-service-config
        - name: model-data
          persistentVolumeClaim:
            claimName: model-data-pvc
4. Service:服务发现,给 Pod 提供统一的访问入口
bash 复制代码
apiVersion: v1
kind: Service
metadata:
  name: spring-ai-service
  namespace: ai-service
spec:
  selector:
    app: spring-ai-service
  ports:
    - port: 80
      targetPort: http
      name: http
  type: ClusterIP

把这四个 YAML 文件应用到 K8s 集群里,你的 Spring AI 服务就成功部署到 K8s 里了。

4.3 核心配置详解:健康检查 + 资源限制 + 生命周期管理

上面的 YAML 里,有几个核心配置是 AI 服务稳定运行的关键,很多人踩坑都是因为这几个配置没做好,这里给大家详细解释:

1. 健康检查:存活探针 + 就绪探针,避免发布故障

这是我踩过的最大的坑之一:一开始没配就绪探针,发布的时候,Pod 刚启动就被加入了 Service 负载,结果请求进来,Spring 上下文还没初始化完,直接返回 503,线上告警直接炸了。

  • 存活探针(livenessProbe):检测 Pod 是否正常运行,如果检测失败,K8s 会重启 Pod。AI 服务启动慢,initialDelaySeconds 要设长一点,建议 30 秒以上;
  • 就绪探针(readinessProbe):检测 Pod 是否可以接收流量,只有检测成功,才会把 Pod 加入 Service 的负载。这个是发布零故障的关键,一定要配置,initialDelaySeconds 建议 20 秒以上。
2. 资源限制:requests 和 limits,避免资源争抢

K8s 里的资源配置,requests 是 Pod 的最低资源需求,K8s 会把 Pod 调度到满足 requests 的节点上;limits 是 Pod 的最大资源限制,超过会被 OOM killed。

对于 Spring AI 服务,给大家几个配置建议:

  • CPU:requests 设为 2 核,limits 设为 4 核,避免 CPU 争抢导致服务卡顿;
  • 内存:requests 设为 4G,limits 设为 8G,一定要比 JVM 的最大堆内存大,不然会出现容器 OOM,但是 JVM 没有堆内存溢出的情况;
  • 一定要配置 requests 和 limits,不要让 Pod 的资源无限制使用,否则一个 Pod 出问题,会把整个节点的资源耗尽。
3. 滚动发布策略:零停机发布

Deployment 的 strategy 配置,我们用的是 RollingUpdate 滚动发布,maxSurge=1,maxUnavailable=0,意思是:发布的时候,先启动 1 个新版本的 Pod,等新版本 Pod 就绪了,再销毁 1 个旧版本的 Pod,全程不会有不可用的 Pod,实现零停机发布。

这个配置对于 AI 服务来说非常重要,因为 AI 服务启动慢,如果用默认的配置,可能会出现大量 Pod 同时重启,导致服务不可用。

4.4 核心亮点:Spring Cloud Kubernetes 深度整合实践

这是整个云原生改造的核心亮点:Spring Cloud Kubernetes 可以让 Spring AI 应用无缝对接 K8s 的原生能力,不用再依赖 Eureka、Nacos、Spring Cloud Config 这些中间件,架构更轻量,更贴合云原生最佳实践。

1. 核心能力

Spring Cloud Kubernetes 提供了这些核心能力,完美适配 Spring AI 服务:

  • 配置管理:直接用 K8s 的 ConfigMap 和 Secret 管理配置,修改 ConfigMap 后,应用能自动刷新配置,不用重启 Pod;
  • 服务发现:直接用 K8s 的 Service 实现服务发现,不用再部署注册中心,服务之间直接通过 Service 名称调用;
  • 负载均衡:集成 Ribbon,实现客户端负载均衡;
  • 健康检查:对接 K8s 的健康检查机制,实现应用生命周期管理。
2. 整合步骤

第一步:引入依赖(Spring Cloud 2023.x 版本)

XML 复制代码
<!-- Spring Cloud Kubernetes 核心依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-kubernetes-fabric8-all</artifactId>
</dependency>
<!-- 配置热刷新依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

第二步:配置 bootstrap.yml,开启 K8s 配置管理

bash 复制代码
spring:
  application:
    name: spring-ai-service
  cloud:
    kubernetes:
      # 开启ConfigMap配置
      config:
        enabled: true
        name: spring-ai-service-config
        namespace: ai-service
      # 开启Secret配置
      secrets:
        enabled: true
        name: spring-ai-service-secret
        namespace: ai-service
      # 开启配置热刷新
      reload:
        enabled: true
        mode: event
        strategy: refresh

第三步:给需要热刷新的配置类加上 @RefreshScope 注解

java 复制代码
@RestController
@RequestMapping("/ai/config")
@RefreshScope
public class AiConfigController {

    @Value("${spring.ai.openai.chat.options.model}")
    private String modelName;

    @GetMapping("/model")
    public String getModelName() {
        return modelName;
    }
}

配置完成后,你修改 K8s 里的 ConfigMap,Spring AI 应用会自动刷新配置,不用重启 Pod,对于需要频繁调整模型参数、Prompt 模板的 AI 服务来说,这个能力太实用了。

3. 权限配置

这里有个坑:Spring Cloud Kubernetes 需要访问 K8s 的 API,所以要给 Pod 配置对应的 ServiceAccount 和 RBAC 权限,否则会报权限不足的错误。

给大家提供完整的 RBAC 配置:

bash 复制代码
apiVersion: v1
kind: ServiceAccount
metadata:
  name: spring-ai-service-sa
  namespace: ai-service
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: ai-service
  name: spring-ai-service-role
rules:
  - apiGroups: [""]
    resources: ["configmaps", "secrets", "services", "endpoints", "pods"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: spring-ai-service-rolebinding
  namespace: ai-service
subjects:
  - kind: ServiceAccount
    name: spring-ai-service-sa
    namespace: ai-service
roleRef:
  kind: Role
  name: spring-ai-service-role
  apiGroup: rbac.authorization.k8s.io

然后在 Deployment 里指定这个 ServiceAccount:

bash 复制代码
spec:
  template:
    spec:
      serviceAccountName: spring-ai-service-sa

5. 弹性伸缩:HPA + 自定义指标,从容应对 AI 流量洪峰

AI 服务的流量有非常明显的波峰波谷:白天业务高峰期 QPS 是平时的 10 倍,晚上低峰期只有不到 10% 的流量。如果按峰值配置固定的副本数,低峰期会浪费大量资源;如果按低峰期配置,高峰期又会扛不住。

而 K8s 的 HPA(Horizontal Pod Autoscaler)水平自动伸缩,完美解决了这个问题:它可以根据 CPU 利用率、QPS、GPU 利用率等指标,自动增加或减少 Pod 的副本数,高峰期自动扩容,低峰期自动缩容,既保证了服务稳定,又降低了资源成本。

5.1 基础 HPA:基于 CPU / 内存的弹性伸缩配置

最基础的 HPA 是基于 CPU 和内存利用率的伸缩,配置非常简单,适合刚入门的场景。

首先,要确保你的 K8s 集群里安装了 Metrics Server,它是 HPA 获取 CPU / 内存指标的基础,大部分 K8s 发行版都默认安装了。

然后,给大家看一下基础 HPA 的配置 YAML:

bash 复制代码
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: spring-ai-service-hpa
  namespace: ai-service
spec:
  # 要伸缩的目标Deployment
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: spring-ai-service
  # 副本数范围:最小2个,最大10个
  minReplicas: 2
  maxReplicas: 10
  # 伸缩指标
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          # 目标CPU利用率:平均利用率超过60%就扩容
          averageUtilization: 60
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          # 目标内存利用率:平均利用率超过70%就扩容
          averageUtilization: 70
  # 伸缩行为配置,避免频繁伸缩
  behavior:
    scaleUp:
      # 扩容冷却时间:30秒
      stabilizationWindowSeconds: 30
      # 一次最多扩容5个Pod
      maxPods: 5
    scaleDown:
      # 缩容冷却时间:5分钟,避免流量波动导致频繁缩容扩容
      stabilizationWindowSeconds: 300
      # 一次最多缩容2个Pod
      maxPods: 2

这个配置的意思是:

  • Pod 的副本数最少 2 个,最多 10 个;
  • 当所有 Pod 的平均 CPU 利用率超过 60%,或者平均内存利用率超过 70%,就自动扩容;
  • 当利用率低于阈值,就自动缩容;
  • 扩容冷却 30 秒,缩容冷却 5 分钟,避免频繁伸缩。

5.2 进阶实战:基于 Prometheus 自定义指标的 HPA(QPS/GPU 利用率)

对于 Spring AI 服务来说,只靠 CPU 和内存做伸缩是远远不够的。很多时候,CPU 利用率只有 30%,但是模型调用的 QPS 已经到了瓶颈,请求开始排队,响应变慢;如果是 GPU 推理场景,GPU 利用率才是核心的伸缩指标。

这时候,我们就需要用基于 Prometheus 自定义指标的 HPA,通过 Prometheus Adapter 把 Prometheus 里的指标暴露给 K8s,让 HPA 可以根据这些指标做伸缩。

1. 前置条件
  • 你的 K8s 集群里已经安装了 Prometheus,并且已经采集了 Spring AI 服务的指标(比如 QPS、GPU 利用率);
  • 安装了 Prometheus Adapter,把 Prometheus 指标转换成 K8s 的自定义指标。
2. 核心配置:基于 QPS 的 HPA

我们在前两篇文章里,用 Micrometer 采集了 Spring AI 服务的模型调用 QPS 指标ai_model_call_total,现在我们就用这个指标做 HPA。

首先,配置 Prometheus Adapter,把 QPS 指标转换成自定义指标:

bash 复制代码
apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-adapter-config
  namespace: monitoring
data:
  config.yaml: |
    rules:
      - seriesQuery: 'ai_model_call_total{namespace!="",pod!=""}'
        resources:
          overrides:
            namespace: {resource: "namespace"}
            pod: {resource: "pod"}
        name:
          matches: "^(.*)_total$"
          as: "${1}_qps"
        metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>}[1m])) by (<<.GroupBy>>)'

然后,配置基于 QPS 的 HPA:

bash 复制代码
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: spring-ai-service-qps-hpa
  namespace: ai-service
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: spring-ai-service
  minReplicas: 2
  maxReplicas: 20
  metrics:
    # 自定义指标:单Pod每秒请求数超过100就扩容
    - type: Pods
      pods:
        metric:
          name: ai_model_call_qps
        target:
          type: AverageValue
          averageValue: 100
    # 保留CPU指标作为兜底
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
      maxPods: 10
    scaleDown:
      stabilizationWindowSeconds: 300
      maxPods: 3

这个配置的意思是:当每个 Pod 的平均模型调用 QPS 超过 100,就自动扩容,最多扩容到 20 个副本,完美应对流量洪峰。

同样的,如果你是 GPU 推理场景,可以用 GPU 利用率指标做 HPA,核心逻辑是一样的。

5.3 AI 场景专项优化:伸缩冷却 + 副本保底 + 预热策略

针对 AI 服务的特点,我们给 HPA 做了三个专项优化,避免踩坑:

  1. 伸缩冷却时间优化:AI 服务的 Pod 启动慢,启动时间要 30 秒以上,所以扩容冷却时间设为 30 秒,保证新的 Pod 能正常启动;缩容冷却时间设为 5 分钟,避免流量波动导致的频繁缩容扩容,防止 "抖动";
  2. 副本保底策略:生产环境最小副本数最少设为 2 个,避免单点故障;如果是核心业务,建议设为 3 个以上,即使某个节点宕机,也不会影响服务;
  3. 预热策略:AI 服务的第一次模型调用、向量检索会比较慢,我们可以在 Pod 启动后,做一次预热请求,让 Spring AI 初始化模型客户端、向量数据库连接,等预热完成后,再把 Pod 加入 Service 负载,避免用户请求出现慢响应。

预热可以用 K8s 的生命周期钩子实现,在 Deployment 的 Pod 模板里加上:

bash 复制代码
spec:
  containers:
    - name: spring-ai-service
      # 启动后钩子,执行预热请求
      lifecycle:
        postStart:
          exec:
            command: ["/bin/sh", "-c", "sleep 20 && curl -X GET http://localhost:8080/actuator/health/readiness"]

5.4 踩坑实录:为什么我的 HPA 始终不生效?

很多人配置了 HPA,但是发现不生效,这里给大家总结了最常见的几个坑,90% 的问题都在这里:

  1. 没安装 Metrics Server/Prometheus Adapter:HPA 获取不到指标,自然不会伸缩,一定要先确认指标能正常获取;
  2. 资源 requests 没配置:HPA 的 CPU / 内存利用率是基于 requests 计算的,如果没配置 requests,利用率就无法计算,HPA 不会生效;
  3. 伸缩阈值设的太高:比如 CPU 利用率阈值设为 90%,平时 CPU 利用率只有 30%,永远不会触发扩容;
  4. 副本数达到了 maxReplicas 上限:已经扩容到了最大副本数,自然不会再扩容;
  5. 指标标签不对:自定义指标的标签和 HPA 的配置不匹配,HPA 获取不到指标;
  6. 冷却时间设的太长:比如扩容冷却时间设了 10 分钟,一次扩容后,要等 10 分钟才会再次扩容,高峰期跟不上流量增长。

6. 全流程实战:GitLab CI/CD 实现 AI 应用自动部署

手动打包镜像、推送镜像、修改 K8s 配置、执行部署,不仅效率低,还很容易出错。我们用 GitLab CI/CD 实现了 Spring AI 应用的全流程自动化部署:代码提交到 GitLab 后,自动执行代码检查、单元测试、镜像构建、镜像推送、K8s 部署,全程不用人工干预,发布时间从 1 小时降到了 5 分钟,还避免了人工操作的失误。

6.1 流水线整体设计:从代码提交到线上部署的全流程

我们的 CI/CD 流水线分为 5 个阶段,完全贴合 Spring AI 服务的发布流程:

整个流水线的逻辑是:

  1. 代码提交到 GitLab 的指定分支,自动触发流水线;
  2. 先执行代码检查和单元测试,不通过则直接终止流水线,避免有问题的代码被发布;
  3. 测试通过后,执行多阶段构建,打包 Docker 镜像,做安全扫描;
  4. 镜像构建成功后,推送到 Harbor 镜像仓库,用 commit hash 做版本号,方便追溯;
  5. 最后自动更新 K8s 集群里的 Deployment 镜像,执行滚动发布,完成部署。

6.2 完整.gitlab-ci.yml 配置:分阶段详解

我把我们生产环境在用的.gitlab-ci.yml 整理好了,大家可以直接复制使用,适配 Spring AI 服务:

bash 复制代码
# 全局变量
variables:
  # 镜像仓库地址
  REGISTRY_URL: harbor.your-domain.com
  # 镜像仓库项目名
  IMAGE_PROJECT: ai-service
  # 镜像名称
  IMAGE_NAME: spring-ai-service
  # K8s命名空间
  K8S_NAMESPACE: ai-service
  # K8s Deployment名称
  K8S_DEPLOYMENT: spring-ai-service

# 流水线阶段
stages:
  - code-check
  - test
  - build
  - push
  - deploy

# 阶段1:代码检查
code-check:
  stage: code-check
  image: maven:3.9.6-eclipse-temurin-17
  script:
    - echo "开始代码静态检查"
    - ./mvnw spotbugs:check checkstyle:check
  only:
    - main
    - dev
  tags:
    - k8s-runner

# 阶段2:单元测试
unit-test:
  stage: test
  image: maven:3.9.6-eclipse-temurin-17
  script:
    - echo "开始执行单元测试"
    - ./mvnw test -DskipTests=false
  only:
    - main
    - dev
  tags:
    - k8s-runner

# 阶段3:镜像构建
build-image:
  stage: build
  image: docker:24.0.6-dind
  services:
    - docker:24.0.6-dind
  script:
    - echo "开始构建Docker镜像"
    # 用Git commit hash作为镜像版本号,唯一标识
    - export IMAGE_VERSION=$CI_COMMIT_SHORT_SHA
    - export FULL_IMAGE_NAME=$REGISTRY_URL/$IMAGE_PROJECT/$IMAGE_NAME:$IMAGE_VERSION
    # 登录镜像仓库
    - docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $REGISTRY_URL
    # 构建镜像,用多阶段构建
    - docker build -t $FULL_IMAGE_NAME .
    # 镜像安全扫描
    - docker scan $FULL_IMAGE_NAME --severity high
    # 把镜像版本号写入临时文件,传递给后续阶段
    - echo $FULL_IMAGE_NAME > image_version.txt
  artifacts:
    paths:
      - image_version.txt
  only:
    - main
  tags:
    - k8s-runner

# 阶段4:镜像推送
push-image:
  stage: push
  image: docker:24.0.6-dind
  services:
    - docker:24.0.6-dind
  script:
    - echo "开始推送镜像到仓库"
    - export FULL_IMAGE_NAME=$(cat image_version.txt)
    - docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $REGISTRY_URL
    - docker push $FULL_IMAGE_NAME
    # 给镜像打latest标签
    - docker tag $FULL_IMAGE_NAME $REGISTRY_URL/$IMAGE_PROJECT/$IMAGE_NAME:latest
    - docker push $REGISTRY_URL/$IMAGE_PROJECT/$IMAGE_NAME:latest
  only:
    - main
  tags:
    - k8s-runner
  dependencies:
    - build-image

# 阶段5:自动部署到K8s
deploy-to-k8s:
  stage: deploy
  image: bitnami/kubectl:1.29
  script:
    - echo "开始部署到K8s集群"
    - export FULL_IMAGE_NAME=$(cat image_version.txt)
    # 配置K8s集群访问凭证,存在GitLab的变量里
    - echo "$K8S_KUBE_CONFIG" > ~/.kube/config
    - chmod 600 ~/.kube/config
    # 更新Deployment的镜像,执行滚动发布
    - kubectl set image deployment/$K8S_DEPLOYMENT $IMAGE_NAME=$FULL_IMAGE_NAME -n $K8S_NAMESPACE
    # 等待滚动发布完成
    - kubectl rollout status deployment/$K8S_DEPLOYMENT -n $K8S_NAMESPACE --timeout=300s
    - echo "部署完成,镜像版本:$FULL_IMAGE_NAME"
  only:
    - main
  tags:
    - k8s-runner
  dependencies:
    - build-image
    - push-image
  # 手动确认部署,避免误操作,生产环境建议开启
  when: manual

6.3 流水线优化:缓存加速 + 权限管控 + 密钥安全

上面的基础流水线可以直接跑,但是我们还做了几个优化,让构建速度更快,安全性更高:

1. Maven 缓存加速,构建时间从 10 分钟降到 1 分钟

每次构建都要重新下载 Maven 依赖,非常慢,我们用 GitLab Runner 的缓存功能,把 Maven 本地仓库缓存起来,不用每次都重新下载:

bash 复制代码
# 全局缓存配置
cache:
  paths:
    - .m2/repository
  key: ${CI_PROJECT_NAME}-maven-cache

在 maven 命令里加上-Dmaven.repo.local=.m2/repository,指定本地仓库路径,就能复用缓存了。

2. 密钥安全管理,绝对不要把密钥写在配置文件里

镜像仓库的账号密码、K8s 的 kubeconfig 这些敏感信息,绝对不要写在.gitlab-ci.yml 里,要存在 GitLab 的项目变量里,也就是上面配置里的$REGISTRY_USER、$REGISTRY_PASSWORD、$K8S_KUBE_CONFIG,这些变量在 GitLab 项目的「设置」→「CI/CD」→「变量」里配置,不会被泄露。

3. 权限管控:不同分支对应不同环境

我们配置了多环境隔离:dev 分支提交自动部署到测试环境,main 分支合并后,需要手动确认才能部署到生产环境,避免误操作导致线上故障。

6.4 发布策略:滚动发布 + 一键回滚 + 多环境隔离

1. 滚动发布

我们用的是 K8s 的滚动发布策略,流水线里的kubectl rollout status命令会等待发布完成,如果发布失败,会自动回滚,不会影响线上服务。

2. 一键回滚

如果发布后出现问题,我们可以在 GitLab 里一键执行回滚流水线,或者直接用 kubectl 命令回滚:

bash 复制代码
# 查看Deployment的发布历史
kubectl rollout history deployment/spring-ai-service -n ai-service
# 回滚到上一个版本
kubectl rollout undo deployment/spring-ai-service -n ai-service

回滚是秒级完成的,比虚拟机部署的回滚速度快了几十倍。

3. 多环境隔离

我们用不同的 K8s 命名空间隔离开发、测试、生产环境:

  • 开发环境:ai-service-dev,dev 分支提交自动部署;
  • 测试环境:ai-service-test,测试分支合并自动部署;
  • 生产环境:ai-service,main 分支合并后手动确认部署。

不同环境的配置用不同的 ConfigMap 管理,完全隔离,不会出现测试环境的配置跑到生产环境的情况。


7. 踩坑总结:Spring AI 云原生改造的 8 个避坑指南

最后,给大家总结一下我们这半年云原生改造踩过的 8 个核心坑,大家可以直接避坑:

  1. 镜像体积太大,拉取超时:一定要用多阶段构建,分离构建和运行环境,用 jlink 裁剪 JRE,不要把模型文件打包到镜像里;
  2. 健康检查配置错误,发布时出现 503:一定要配置就绪探针,等 Spring 应用完全启动后,再把 Pod 加入 Service 负载,initialDelaySeconds 要设长一点;
  3. 容器 OOM,但是 JVM 没有堆溢出 :容器的内存 limits 一定要比 JVM 的最大堆内存大,JVM 的-XX:MaxRAMPercentage参数要设为 75% 左右,不要让 JVM 把容器的内存占满;
  4. Spring Cloud Kubernetes 权限不足:一定要给 Pod 配置 ServiceAccount 和 RBAC 权限,允许 Pod 访问 K8s 的 API,否则配置热更新、服务发现都用不了;
  5. HPA 不生效:先确认 Metrics Server/Prometheus Adapter 正常运行,Pod 配置了 resources.requests,指标能正常获取,阈值设置合理;
  6. CI/CD 流水线构建慢:一定要配置 Maven、Docker 的缓存,避免每次构建都重新下载依赖;
  7. 配置热更新不生效:给配置类加上 @RefreshScope 注解,开启 Spring Cloud Kubernetes 的 reload 配置,ConfigMap 的名称要和配置里的一致;
  8. GPU 调度失败:一定要提前安装 NVIDIA GPU Operator,用官方的 CUDA 基础镜像,Pod 的 GPU 资源配置要正确,不要用 limits 和 requests 不同的配置。

8. 总结与展望

总结

这篇文章里,我给大家完整分享了 Spring AI 服务从虚拟机部署到云原生容器化改造的全流程实战,从 Docker 镜像优化、K8s 全资源部署、弹性伸缩配置,到完整的 CI/CD 流水线落地,还有核心的 Spring Cloud Kubernetes 整合实践,所有的代码和配置都是我们生产环境验证过的,大家可以直接复制落地。

完成云原生改造后,我们的服务获得了这些核心收益:

  • 环境一致性问题彻底解决:镜像里包含了所有运行依赖,彻底告别了 "本地能跑线上不行" 的问题;
  • 发布效率提升 10 倍以上:从手动 1 小时发布,变成了自动化 5 分钟发布,滚动发布零停机,一键回滚;
  • 扩容效率从半小时变成秒级:HPA 自动根据流量伸缩,高峰期自动扩容,低峰期自动缩容,从容应对流量洪峰;
  • 资源利用率提升一倍以上:从原来的 30% 提升到了 70%,极大降低了服务器成本;
  • 配置管理更规范:用 ConfigMap 和 Secret 管理配置,敏感信息加密存储,配置热更新不用重启服务。

展望

未来我们还会在云原生架构的基础上,做进一步的优化:

  1. 引入 Serverless K8s:用 Knative 实现基于请求的自动扩缩容,甚至可以缩容到 0,进一步降低资源成本;
  2. GPU 虚拟化与池化:用 GPU 虚拟化技术,实现多个 Pod 共享一张 GPU,提升 GPU 资源利用率,降低大模型部署成本;
  3. 模型预热与懒加载:实现模型的按需加载、预热,加快 Pod 启动速度,降低内存占用;
  4. GitOps 全流程管理:用 Argo CD 实现 GitOps,把 K8s 配置全部存在 Git 仓库里,实现配置的版本管理、审计、自动同步,进一步提升发布的安全性和规范性。

9. 参考文献

结语:以上就是 Spring AI 服务云原生容器化改造的全量实战分享,所有的代码和配置都可以直接复制到你的项目里使用。如果大家有任何问题,或者需要完整的配置文件,欢迎在评论区留言,我会一一回复。如果觉得这篇文章对你有帮助,别忘了点赞、收藏、转发三连,后续我还会分享更多 Spring AI 架构优化的实战干货,感谢大家的支持!

相关推荐
一 乐36 分钟前
动漫书销售商城|基于springboot + vue动漫书销售商城(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕业设计
卓怡学长1 小时前
w214基于jsp知道特产网
java·intellij-idea
步行cgn1 小时前
Spring 注解使用详解
java·spring
一条小小yu1 小时前
Spring IoC的理解
java·后端·spring
落魄实习生2 小时前
Agent Scope Java 2.x 系列【7】工具使用
java·开发语言·ai
旺仔学长 哈哈2 小时前
springboot钓鱼爱好者交流平台APP设计与实现
java·spring boot·mysql·充电桩管理系统
bloglin999993 小时前
vmware部署centos7启动docker容器后无法访问服务
运维·docker·容器
自强的小白3 小时前
核心功能(Service接口)
java·mybatis
乌暮4 小时前
深入理解 Java 泛型:把「万能盒子」用对、用稳
java·开发语言·后端·学习