降龙十八掌练到第十八掌掌法反而越打越柔。这是我写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 idaliyunmaven/id mirrorOfcentral/mirrorOf namealiyun public/name urlhttps://maven.aliyun.com/repository/public/url /mirror /mirrors profiles profile idaliyun/id repositories repository idaliyun-spring/id urlhttps://maven.aliyun.com/repository/spring/url /repository /repositories pluginRepositories pluginRepository idaliyun-spring-plugin/id urlhttps://maven.aliyun.com/repository/spring/url /pluginRepository /pluginRepositories /profile /profiles activeProfiles activeProfilealiyun/activeProfile /activeProfiles /settingsmirrorOf配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 --frombuilder /build/target/*.jar app.jar EXPOSE 8080 ENV JAVA_OPTS-XX:MaxRAMPercentage75 -Dfile.encodingUTF-8 -XX:UseG1GC ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar app.jar]有几个细节我要特别说-XX:MaxRAMPercentage75而不是写死-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一直ImagePullBackOffdescribe pod一看Error提示pull access denied。生成这个Secret的命令如下kubectl create secret docker-registry acr-registry-secret \ --namespaceproduction \ --docker-serverregistry.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放敏感配置。系统提示词、智能审核规则、温度参数放ConfigMapapiVersion: 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、云服务账号凭据放SecretapiVersion: 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和limitsK8s调度器就无法感知这个Pod需要多少资源多个Pod堆在同一个节点上可能导致节点资源被吃尽。Spring AI应用在模型调用高峰期内存会明显上涨我见过太多不配limits导致OOMKilled的例子。requests和limits之间留多少余量有讲究。上面的配置requests是1Gilimits是2Gi意味着正常情况下Pod有1Gi的资源保障在节点空闲时可以冲到2Gi。如果limits和requests相等且写得过小比如都写512MiAI应用在并发上来时直接OOM。建议先压测再定值不要拍脑袋。imagePullPolicy: IfNotPresent的意思是本地有这个镜像就不重新拉取。开发调试阶段我建议改成Always不然你推了新镜像但Pod还跑着旧镜像排查半天都不知道问题在哪。4.2 Service和Ingress把AI服务暴露给外部Deployment只是把你的应用跑起来了外部还访问不到。接下来要建Service。我推荐用ClusterIP Ingress的组合而不是直接LoadBalancer。原因很简单LoadBalancer每个Service都会创建一个SLB实例按数量计费Service多了成本受不了Ingress用一台SLB做七层路由能复用域名和证书。Service YAMLapiVersion: 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证书服务可以申请免费证书为域名启用HTTPSapiVersion: 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应用的特点是流量可能会突然飙上来——比如你做智能审核功能业务方临时要批量审核一批旧数据请求量瞬间翻五倍。HPAHorizontalPodAutoscaler就是干这个的。最基础的配置是按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把日志输出到consolelogging: 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底层用NettyNetty的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_KEYsk-xxx或者把application-prod.yml带Key的配置COPY进镜像。这样做的后果是任何能拉取到你镜像的人docker history一下就能把Key翻出来。正确的姿势已经在3.3节写过了StringData放到Kubernetes SecretDeployment里用valueFrom.secretKeyRef注入。这个习惯必须从第一天就养成一旦Key泄漏到镜像仓库里转一圈可能就被刷爆了追责都无从追起。6. 神龙摆尾的最后一课——把能跑变成能扛写到这第18掌算是收势了。回过头看整个登云过程我最想强调的不是哪条命令、哪个YAML而是一句话容器化只是手段编排才是目的。神龙摆尾这招听名字气势汹汹其实是降龙十八掌里少有的防守式——但它防的不是敌人的进攻而是你自身的破绽。K8s也是一样它不能让你应用的逻辑变好也不能让模型调用变快但它能把你应用里人肉运维的那些破绽全部补上进程挂了自动拉起流量大了自动扩容配置改了不用重新出包密钥泄漏了不用改代码。对一个要长期运行的Spring AI应用来说这些短板补上之后你才有底气说它真正成为了一个产品而不只是一个demo。最后再分享一个我自己习惯的落地顺序先在测试环境里把YAML全套跑通然后用jmeter或者阿里云PTS打一轮压测把CPU和内存的真实水位摸清楚回过头再调整requests和limits最后才动生产集群。上生产的前三天盯紧SLS里的异常日志和HPA的扩缩容记录有问题第一时间处理三天之后基本就稳了。Spring AI这条路走到现在代码写得好不好是下限部署稳不稳才是上限。这份上限值得你花一个下午把它踩实。