"降龙十八掌练到第十八掌,掌法反而越打越柔。"这是我写Spring AI系列以来,第18篇落笔时的第一个念头。前17篇我们一步步把模型接入、提示词模板、智能审核链路都调通了,应用在本地和单机服务器上跑得欢脱,可真到要交付给正经业务用的时候,问题就来了:并发一上来JVM就开始喘,API Key和系统提示词散落在各个配置文件里,上线要靠ssh上去kill进程换jar包,手一抖就是五分钟故障。所以这一篇,我决定带大家把Spring AI应用真正"登云"——部署到阿里云Kubernetes集群上,用容器编排把这套应用从"能跑"变成"能扛"。这篇适合已经写完Spring AI应用、想走通云上部署的开发者,内容涵盖镜像构建、ACR仓库、ACK集群部署、配置管理和上线后的运维排错,全程按真实操作顺序来。
1. 为什么Spring AI应用最终要走上K8s这条路
1.1 上云之前,AI应用比普通Web应用更难扛
很多人觉得K8s是运维的事,开发写完接口丢给运维就行。但Spring AI应用和普通CRUD接口有一个本质区别:它的大多数请求要调外部模型接口,一次响应可能拉长到几秒甚至几十秒,而且模型结果有不确定性,导致接口耗时波动非常大。
这种长尾耗时对单机部署是致命的。Tomcat默认的连接池就那么多,一个请求堵在模型调用上,后面排队的请求全部饿死。我在第12篇里用CompletableFuture做过并发优化,但那只是把线程池打满的时限往后推了一点——机器总有上限。到了业务真正起来那天,你会发现不管怎么调JVM参数,单机就是单机的命。
而且AI应用还有个特点:配置特别多。模型名称、温度参数、系统提示词、安全审核的关键词规则、不同渠道的API Key……这些配置如果每次发布都要手动改,或者干脆写死在代码里,那几乎等于给自己埋雷。K8s的ConfigMap和Secret就是为这种场景准备的。
1.2 K8s不是Docker的替代品,它管的是"Docker们"
搜索"k8s和docker区别"的人特别多,这里用最直白的话讲清楚:Docker解决的是"怎么把应用和它的环境打包"的问题,K8s解决的是"这一堆打包好的容器怎么协作、怎么调度、怎么在挂了以后自动拉起"的问题。
你可以把Docker镜像想象成一个标准化的集装箱,里面装着Spring AI应用和它的运行时;K8s则是那个港口调度系统,负责决定集装箱放哪个泊位、什么时候放、坏了怎么办。没有港口,单个集装箱也能跑,但几十个集装箱一起到货,靠人搬是搬不过来的。
阿里云ACK(容器服务Kubernetes版)就是帮你把这个港口搭好的托管服务。Master节点、etcd、控制平面这些K8s最复杂的组件都由云平台管了,你只需要关心Worker节点和应用本身。对于大部分中小团队,我不建议自己用rocky之类的系统去裸装K8s——不是不行,而是你省下的那点机器成本,会在etcd备份、证书轮转、控制平面高可用这些事上十倍找回来。
1.3 从"本地能跑"到"云上稳定",差距在哪
本地跑通Spring AI只需要三样东西:JDK、一个能访问模型接口的网络、一个application.yml。云上稳定运行则需要回答这些问题:
- 应用挂了我的进程会自动重启吗——K8s的Deployment会保证副本数,Pod挂了自动拉起。
- 流量突增时能快速扩出几台实例吗——HPA可以按CPU或自定义指标自动扩缩容。
- 模型API Key和提示词能不进代码仓库吗——Secret和ConfigMap配合,配置与镜像分离。
- 发新版本能不能不中断服务——滚动更新策略,先起新的、再摘旧的。
- 日志、监控能不能集中看——标准输出对接日志服务,Metrics接入Prometheus。
这五件事,才是K8s对Spring AI这类应用真正的价值。容器化只是第一步,编排才是目的。
2. 登云前的最后一公里——镜像构建与阿里云基础设施准备
2.1 先让Maven飞起来:阿里云仓库镜像配置
Spring AI的依赖很多还在里程碑仓库里,直接从中央仓库拉会慢到怀疑人生。第一步先把Maven的镜像源切到阿里云,这是一切构建提速的基础。
编辑~/.m2/settings.xml,把镜像和仓库地址都配置好:
<settings> <mirrors> <mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>aliyun public</name> <url>https://maven.aliyun.com/repository/public</url> </mirror> </mirrors> <profiles> <profile> <id>aliyun</id> <repositories> <repository> <id>aliyun-spring</id> <url>https://maven.aliyun.com/repository/spring</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>aliyun-spring-plugin</id> <url>https://maven.aliyun.com/repository/spring</url> </pluginRepository> </pluginRepositories> </profile> </profiles> <activeProfiles> <activeProfile>aliyun</activeProfile> </activeProfiles> </settings>mirrorOf配central就够了,所有中央仓库的依赖都会走阿里云加速;Spring里程碑仓库单独配在profile里,这样Spring AI的spring-ai-starter相关依赖才能正常解析。实测下来,原来拉一个Spring AI依赖可能要三五分钟,配完基本十几秒搞定,尤其在后面自动化构建里,这一步省下来的时间会被反复放大。
还有一个容易被忽略的点:dependency:go-offline一定要在COPY源码之前跑。因为改了pom之后Maven Core类的依赖下载是最耗时的,把它单独拎出来做一层缓存,后续改动代码重新构建时Docker会直接命中缓存层,构建时间能从三分钟压到三十秒。
2.2 多阶段构建Spring AI应用的Docker镜像
Spring AI应用本质还是Spring Boot应用,镜像构建推荐多阶段方式:第一个阶段用带Maven的镜像编译打包,第二个阶段只拷贝jar包和JRE运行。
FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn -B dependency:go-offline -DskipTests COPY src ./src RUN mvn -B package -DskipTests FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=builder /build/target/*.jar app.jar EXPOSE 8080 ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -Dfile.encoding=UTF-8 -XX:+UseG1GC" ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]有几个细节我要特别说:
-XX:MaxRAMPercentage=75而不是写死-Xmx2g。K8s会给容器设置内存限制,JVM默认只认识宿主机的内存,如果不用这个参数,可能出现容器限制1G而JVM以为有32G可用的情况,然后就是无穷无尽的OOM。让JVM动态感知容器内存是云上部署的底线操作。整个构建过程不要拷贝
.env、application-local.yml这些带Key的配置进镜像。我在4.3节还会专门讲Key泄漏的坑,这里先记住一句话:镜像里只放代码和jar包,配置全部在运行时注入。阶段一里
COPY pom.xml .和COPY src ./src分开写,是为了利用Docker layer缓存。只改代码不会触发pom相关层重建,构建速度提升非常明显。
2.3 推送镜像到阿里云ACR,配好拉取凭据
镜像要在ACK集群里跑起来,得先放到一个集群能拉取的地方。阿里云的容器镜像服务ACR就是干这个的,个人版免费,够用。
先把镜像打好tag并推送:
docker login --username=你的账号 registry.cn-hangzhou.aliyuncs.com docker tag springai-app:v1.0.0 registry.cn-hangzhou.aliyuncs.com/你的命名空间/springai-app:v1.0.0 docker push registry.cn-hangzhou.aliyuncs.com/你的命名空间/springai-app:v1.0.0记住推送成功后,在K8s里拉这个私有镜像需要一个Secret。很多人第一次部署失败都卡在这——Pod一直ImagePullBackOff,describe pod一看Error提示pull access denied。
生成这个Secret的命令如下:
kubectl create secret docker-registry acr-registry-secret \ --namespace=production \ --docker-server=registry.cn-hangzhou.aliyuncs.com \ --docker-username=你的账号 \ --docker-password=你的密码然后在Deployment的spec.template.spec里显式引用:
imagePullSecrets: - name: acr-registry-secret这条配置漏掉的比例非常高。很多人镜像推上去了,Deployment也写了image地址,结果就是拉不下来,折腾半天发现是少了这一行。
另外提一句:ACK集群也可以直接绑定CRD加速器或者配置免密拉取,如果你是terraform或者ACK托管集群,可以在集群创建时开启"免密拉取"组件,这个可以省掉Secret,但机制上还是建议把Secret写上,后续如果换仓库也能平滑切换。
3. 把Spring AI应用"翻译"成K8s对象——从Deployment到Service的完整映射
3.1 先花三分钟搞懂Deployment、Pod、Service的关系
新人看K8s文档最容易懵的是这堆概念,我尝试用一句话把它们串起来:
- Pod:一个或一组紧密相关的容器,是K8s调度的最小单位。你的Spring AI应用在Pod里以一个容器形式运行。
- Deployment:管着Pod的"声明",描述"我要几个副本、用什么镜像、健康检查怎么做"。它会帮你保证Pod数量始终符合预期,挂了就新建。
- Service:给一组Pod提供稳定访问入口的东西。因为Pod是随时可能被销毁重建的,它的IP不固定。Service用Label Selector找到那些Pod,给它一个固定的虚拟IP和DNS名字。
打个比方:Pod是演出的演员,Deployment是确保演员阵容齐整的经纪公司,Service是剧院的对外售票窗口——观众不用管今天具体是哪个演员上场,窗口买票进去看就行。
3.2 Namespace:给生产环境隔一道墙
部署之前先创建Namespace,这是很多人图省事跳过、后面后悔到拍大腿的步骤。
kubectl create namespace productionNamespace的作用是资源隔离。你把生产环境的Spring AI应用放在production这个Namespace里,测试环境放staging,两边的配置、密钥、Pod互不干扰。尤其在你要测试不同版本的提示词或模型参数的时候,Namespace隔离能让你安心地在测试环境乱搞而不会误伤生产。
有个注意点:ConfigMap和Secret默认也是分Namespace的。你在production里创建了springai-secret,切换到default命名空间里去引用是引不到的,会报Secret not found。这个坑我见太多人了。
3.3 系统提示词、智能审核规则、模型Key的正确存放姿势
这部分是Spring AI应用和普通Web应用配置管理差异最大的地方。普通应用的配置就几个字符串,AI应用的配置里还有大段大段的系统提示词和审核规则,怎么放、放哪里直接决定你迭代效率。
推荐组合是:ConfigMap放非敏感配置,Secret放敏感配置。
系统提示词、智能审核规则、温度参数放ConfigMap:
apiVersion: v1 kind: ConfigMap metadata: name: springai-config namespace: production data: application-prod.yml: | spring: ai: model: chat: temperature: 0.7 max-tokens: 1024 prompt: system: | 你是一名内容安全审核助手。 请对用户输入的文本进行以下维度审核: 1. 是否包含违法违规内容 2. 是否包含诱导性、欺诈性表述 3. 是否包含恶意攻击或骚扰信息 如果发现风险,请返回风险等级和具体原因。模型API Key、云服务账号凭据放Secret:
apiVersion: v1 kind: Secret metadata: name: springai-secret namespace: production type: Opaque stringData: model-api-key: sk-你的模型服务Key oss-access-key-id: your-access-key-id oss-access-key-secret: your-access-key-secret然后在Deployment里通过环境变量引用:
env: - name: SPRING_PROFILES_ACTIVE value: "prod" - name: AI_MODEL_API_KEY valueFrom: secretKeyRef: name: springai-secret key: model-api-key这样做的好处是:改提示词不用重新构建镜像,改完kubectl apply一下再滚动重启Pod就生效;换API Key不用动代码,Secret改了之后Pod重启自动拿到新值。我专门写过一篇怎么配置Spring AI系统提示词的文章,但到了K8s环境,核心思路就一句话:配置跟着环境走,不跟着镜像走。
3.4 把application.yml和ConfigMap对接起来
Spring Boot读取外部配置的方式很多,在K8s里我最推荐的是把ConfigMap里的配置挂载成文件,然后通过spring.config.additional-location指向它。
在Deployment里挂载:
volumeMounts: - name: config mountPath: /config volumes: - name: config configMap: name: springai-config启动参数改成:
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS --spring.config.additional-location=/config/ -jar app.jar"]Spring Boot会自动读取/config目录下的application-prod.yml,并且和jar包里的配置做合并。这就是ConfigMap的优雅之处,环境特有配置全部外置,jar包在任何环境都能跑。
4. 登云实操——Spring AI应用部署到ACK的完整清单
4.1 一份能直接用的Deployment YAML
理论讲再多不如一份能跑的清单。下面的Deployment覆盖了镜像、资源限制、探针、日志、凭据,基本上照着改镜像地址就能用:
apiVersion: apps/v1 kind: Deployment metadata: name: springai-app namespace: production labels: app: springai-app spec: replicas: 2 selector: matchLabels: app: springai-app strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app: springai-app spec: imagePullSecrets: - name: acr-registry-secret containers: - name: springai-app image: registry.cn-hangzhou.aliyuncs.com/你的命名空间/springai-app:v1.0.0 imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http env: - name: SPRING_PROFILES_ACTIVE value: "prod" - name: AI_MODEL_API_KEY valueFrom: secretKeyRef: name: springai-secret key: model-api-key resources: requests: cpu: 500m memory: 1Gi limits: cpu: "2" memory: 2Gi readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 60 periodSeconds: 20 timeoutSeconds: 3 failureThreshold: 3几个值得展开的细节:
resources一定要写。不写requests和limits,K8s调度器就无法感知这个Pod需要多少资源,多个Pod堆在同一个节点上可能导致节点资源被吃尽。Spring AI应用在模型调用高峰期内存会明显上涨,我见过太多不配limits导致OOMKilled的例子。
requests和limits之间留多少余量有讲究。上面的配置requests是1Gi,limits是2Gi,意味着正常情况下Pod有1Gi的资源保障,在节点空闲时可以冲到2Gi。如果limits和requests相等且写得过小,比如都写512Mi,AI应用在并发上来时直接OOM。建议先压测再定值,不要拍脑袋。
imagePullPolicy: IfNotPresent的意思是本地有这个镜像就不重新拉取。开发调试阶段我建议改成Always,不然你推了新镜像但Pod还跑着旧镜像,排查半天都不知道问题在哪。
4.2 Service和Ingress:把AI服务暴露给外部
Deployment只是把你的应用跑起来了,外部还访问不到。接下来要建Service。
我推荐用ClusterIP + Ingress的组合,而不是直接LoadBalancer。原因很简单:LoadBalancer每个Service都会创建一个SLB实例,按数量计费,Service多了成本受不了;Ingress用一台SLB做七层路由,能复用域名和证书。
Service YAML:
apiVersion: v1 kind: Service metadata: name: springai-app-svc namespace: production spec: selector: app: springai-app ports: - port: 80 targetPort: 8080 protocol: TCP type: ClusterIP然后在ACK里创建Ingress,绑定域名和SSL证书。这一步能用到阿里云SSL证书服务,可以申请免费证书为域名启用HTTPS:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: springai-app-ingress namespace: production annotations: cert-manager.io/cluster-issuer: "letsencrypt-prod" spec: ingressClassName: nginx tls: - hosts: - ai.yourdomain.com secretName: ai-tls-secret rules: - host: ai.yourdomain.com http: paths: - path: / pathType: Prefix backend: service: name: springai-app-svc port: number: 80注意这里cert-manager.io/cluster-issuer的注解,我建议在ACK集群里装好cert-manager,它可以自动为你的域名申请并续期SSL证书,申请下来的证书就存在ai-tls-secret里。证书续期这个事真是能自动就别手动,我见过手动续期忘掉导致线上HTTPS告警的情况,心脏受不了。
4.3 探针没配好,滚动更新就是灾难现场
你可能觉得探针就是两行配置,实际上一半的发布事故都出在探针上。
Spring Boot Actuator在Spring Boot 2.4之后提供了两组健康端点:/actuator/health/readiness就绪探针,/actuator/health/liveness存活探针。把两组端点分别接到K8s的readinessProbe和livenessProbe上,逻辑才正确。
我踩过最惨的一次坑是这样的:第一版只配了readiness,没配liveness。然后某天上线一个新版本,应用启动时Redis连接不上,进程活着但请求全部超时。这时候readiness探针会把它从Service里摘掉,请求不往这个Pod打了,但Pod一直"半死不活"地杵在那,永远不会被重启。流量全压在另一台Pod上,直接被打爆。
后来补上liveness探针,逻辑变成:如果进程活着但内部状态已经坏了(比如连续多次健康检查失败),K8s就强制杀掉这个Pod重新拉起。这才是K8s"自愈"能力的正确打开方式。
另外要提醒的是liveness的initialDelaySeconds不要设太小。Spring AI应用启动时要做模型客户端的初始化、提示词模板加载,如果60秒的延迟启动还不够,适当调到90秒。探针延迟设置太短,Pod还在启动中就被liveness杀掉,会出现没完没了的CrashLoopBackOff。
5. 上线之后那点事——弹性、可观测与三个经典坑
5.1 HPA弹性伸缩:AI应用如何应对流量洪峰
Deployment把副本数固定成2,这在业务平稳时没问题,但AI应用的特点是流量可能会突然飙上来——比如你做智能审核功能,业务方临时要批量审核一批旧数据,请求量瞬间翻五倍。
HPA(HorizontalPodAutoscaler)就是干这个的。最基础的配置是按CPU使用率扩缩容:
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: springai-app-hpa namespace: production spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: springai-app minReplicas: 2 maxReplicas: 8 behavior: scaleDown: stabilizationWindowSeconds: 300 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60CPU到了60%就扩容,低于这个值持续5分钟才缩容。stabilizationWindowSeconds: 300一定要配,不然流量一抖动Pod就疯狂扩缩,对模型服务商API的并发连接数忽高忽低,很容易触发限流。
但我必须说:纯CPU指标对AI应用不太够。一个应用如果卡在外部模型API响应慢上,CPU可能还很低但QPS已经上不去。这时候更好的方案是用自定义指标,比如通过Micrometer暴露http_server_requests_seconds_max或者resilience4j的调用指标,再用Prometheus Adapter接到HPA上。
这里我不展开Prometheus Adapter的安装细节,只给方向:阿里云ACK自带Prometheus监控组件,装上之后就能在HPA里配置按请求量扩缩容。对大多数场景,CPU + 内存两个基础指标已经能解决80%的问题,先把基础的用好再上复杂的。
5.2 日志别写文件,都打到标准输出
Spring Boot默认日志是输出的logs目录的,在K8s里这是大忌。
容器是随时会被销毁重建的,写在Pod本地文件系统里的日志,Pod一删就没了。而且kubectl logs只能看标准输出,你写文件就看不到Pod的实时日志,排查问题像盲人摸象。
正确的做法是在生产配置里让Logback把日志输出到console:
logging: file: name: /dev/stdout或者直接在logback-spring.xml里把root日志输出到STDOUT。然后如果是阿里云ACK,集群可以装日志服务组件,直接采集标准输出到SLS。配好之后,日志检索、告警、看板都集中在一处,不用一台台机器连过去tail -f。
这里有个小经验:Spring AI应用会打印比较多的模型调用日志和令牌消耗日志,最好在logback里给org.springframework.ai单独设一个INFO级别,既能看到关键调用信息又不被刷屏。如果开了DEBUG,光日志就能把你SLS的存储费用顶上去。
5.3 三个绕不开的坑:内存、外连、Key泄漏
坑一:内存OOM。
Spring AI应用的内存大头不只是堆内存,还有Metaspace和直接内存。很多AI SDK底层用Netty,Netty的Direct Memory不在堆内,-XX:MaxRAMPercentage管不到它。如果Pod内存限制不够,会出现进程明明没到堆上限却被OOMKilled的情况。我的建议是:limits给到压测峰值的1.5倍以上,同时加上-XX:MaxDirectMemorySize相关的监控,先跑两天看真实用量再收敛配置。
坑二:Pod无法访问RDS和Redis。
Spring AI应用通常要配合业务数据做检索增强,所以会连RDS或者Redis集群。这里有个网络层面的天坑:ACK集群的Pod网段和VPC的ECS网段可能是隔离的。如果你的RDS和Redis只开了内网地址,而ACK集群的Pod网段没有加入它们的安全组白名单,那Pod里就是访问不通。
排查链路通常是:先kubectl exec进Pod里curl一下RDS的内网地址,不通再看安全组。阿里云的RDS控制台里可以配置白名单,你需要把ACK集群的Pod CIDR网段加进去。这个网段在创建集群时能看到,是一个类似/16的地址段。配完之后Pod到RDS的内网访问就通了。注意是Pod网段不是节点网段,这两个东西不一样。
坑三:模型API Key被构建进镜像层。
这个坑特别隐蔽。有人图方便,在Dockerfile里直接ENV AI_API_KEY=sk-xxx,或者把application-prod.yml带Key的配置COPY进镜像。这样做的后果是:任何能拉取到你镜像的人,docker history一下就能把Key翻出来。
正确的姿势已经在3.3节写过了:StringData放到Kubernetes Secret,Deployment里用valueFrom.secretKeyRef注入。这个习惯必须从第一天就养成,一旦Key泄漏到镜像仓库里,转一圈可能就被刷爆了,追责都无从追起。
6. 神龙摆尾的最后一课——把"能跑"变成"能扛"
写到这,第18掌算是收势了。回过头看整个登云过程,我最想强调的不是哪条命令、哪个YAML,而是一句话:容器化只是手段,编排才是目的。
神龙摆尾这招,听名字气势汹汹,其实是降龙十八掌里少有的防守式——但它防的不是敌人的进攻,而是你自身的破绽。K8s也是一样,它不能让你应用的逻辑变好,也不能让模型调用变快,但它能把你应用里"人肉运维"的那些破绽全部补上:进程挂了自动拉起,流量大了自动扩容,配置改了不用重新出包,密钥泄漏了不用改代码。对一个要长期运行的Spring AI应用来说,这些短板补上之后,你才有底气说它真正成为了一个产品,而不只是一个demo。
最后再分享一个我自己习惯的落地顺序:先在测试环境里把YAML全套跑通,然后用jmeter或者阿里云PTS打一轮压测,把CPU和内存的真实水位摸清楚,回过头再调整requests和limits,最后才动生产集群。上生产的前三天,盯紧SLS里的异常日志和HPA的扩缩容记录,有问题第一时间处理,三天之后基本就稳了。Spring AI这条路走到现在,代码写得好不好是下限,部署稳不稳才是上限。这份上限,值得你花一个下午把它踩实。