Spring AI部署到Kubernetes后频繁重启和503?Liveness、Readiness、启动探针与优雅停机完整排查
文章摘要
Spring AI应用部署到Kubernetes后,经常出现一组看似矛盾的问题:Pod明明已经启动,Ingress却持续返回503;模型Provider短暂抖动后,Kubernetes开始反复重启应用;知识索引加载需要两分钟,Pod还没准备好就被Liveness杀死;滚动发布期间旧Pod收到SIGTERM,但仍有流式回答、长任务和Tool Calling正在执行,结果被强制中断。
根因通常不是Kubernetes不稳定,而是健康检查把“进程是否活着”“实例能否接收新流量”“依赖是否暂时可用”“启动是否完成”混成了一个/actuator/health。对于AI应用,模型Provider、向量库、Reranker和对象存储都可能短暂不可用,但这些外部依赖不应该轻易进入Liveness,否则一次第三方故障会触发所有Pod同时重启,形成重启风暴。
Spring Boot Actuator提供Liveness和Readiness状态,并能在启动和优雅停机阶段反映应用可用性;Kubernetes则分别使用Startup、Liveness和Readiness Probe控制启动保护、重启和流量摘除。本文给出AI应用的探针分层、依赖健康策略、冷启动、连接池预热、流式请求排空、长任务租约释放、preStop、terminationGracePeriodSeconds和滚动发布参数的完整配置。
一、先看一个典型事故
Deployment配置:
livenessProbe:httpGet:path:/actuator/healthport:8080periodSeconds:10readinessProbe:httpGet:path:/actuator/healthport:8080periodSeconds:5/actuator/health中包含:
- PostgreSQL;
- Redis;
- VectorStore;
- 模型Provider;
- Reranker;
- 对象存储。
某个模型Provider发生30秒抖动。
结果:
/actuator/health = DOWN ↓ Liveness失败 ↓ Kubernetes重启Pod ↓ 所有Pod同时重新建立连接 ↓ 冷启动和依赖压力升高 ↓ 更多探针失败 ↓ 服务持续503真正应该发生的是:
Provider暂时不可用 → 路由到备用模型 或 → Readiness按策略拒绝部分新请求 或 → 返回可解释降级 而不是 → 重启整个JVM二、四种状态必须分开
1. Process Alive
JVM和事件循环仍然工作,应用没有进入不可恢复死锁。
对应:
Liveness2. Startup Completed
应用是否完成初始化:
- 配置加载;
- Bean创建;
- 数据库迁移;
- 必要模型Profile加载;
- 本地词典或规则加载;
- 连接池预热。
对应:
Startup Probe3. Ready for New Traffic
实例是否可以接受新的业务请求。
对应:
Readiness4. Business Dependency Capability
某个具体能力是否可用:
- 主模型可用;
- 备用模型可用;
- 向量检索可用;
- Tool Gateway可用;
- 异步队列可用。
这不一定映射到Pod级探针,更适合能力矩阵、业务健康端点、指标和降级策略。
三、Liveness应该检查什么
Liveness的目标是回答:
重启这个进程 是否可能恢复?适合进入Liveness的情况:
- JVM不可响应;
- 事件循环严重阻塞;
- 核心线程死锁;
- 应用内部状态不可恢复;
- 必需本地资源损坏;
- 自检确认必须重启。
不适合:
- 模型Provider 5xx;
- Redis短暂超时;
- VectorStore延迟升高;
- 数据库主从切换;
- Reranker限流;
- 外部Tool不可用。
外部依赖故障通常不会因为重启Pod而恢复。
四、Readiness应该检查什么
Readiness回答:
这个实例现在是否应该接收新流量?适合考虑:
- 应用是否完成启动;
- 本实例是否正在优雅停机;
- 核心配置是否加载;
- 必需数据库是否可用;
- 当前实例是否具备至少一个可用模型路由;
- 是否达到严重过载阈值;
- 是否正在执行不可兼容迁移。
但也不能把所有依赖直接加入Readiness。若所有实例同时Unready,Service将无法接收新连接,因此外部依赖是否进入Readiness,需要结合系统的降级能力判断。
五、AI应用的推荐健康分层
Liveness: JVM内部可恢复性 Readiness: 当前实例是否接受新的AI请求 Capability Health: chat/rag/tool/embedding等能力矩阵 Dependency Metrics: Provider、VectorStore、Redis、DB详细指标能力状态:
publicrecordAiCapabilityStatus(booleanchatAvailable,booleanragAvailable,booleantoolAvailable,booleanembeddingAvailable,Set<String>availableModelProfiles,Set<String>degradedReasons){}六、Spring Boot探针配置
management:endpoint:health:probes:enabled:trueshow-details:neverhealth:livenessstate:enabled:truereadinessstate:enabled:trueserver:shutdown:gracefulspring:lifecycle:timeout-per-shutdown-phase:45s典型端点:
/actuator/health/liveness /actuator/health/readiness管理端口可独立:
management:server:port:9090但管理端口健康不代表业务端口一定可用。生产中应验证两者共享的关键线程池、网络栈和进程状态。
七、Startup Probe解决什么
AI应用冷启动可能很慢:
- 大量Bean;
- 数据源初始化;
- 本地模型或Tokenizer加载;
- Prompt模板编译;
- 知识库元数据缓存;
- 证书加载;
- 远程配置;
- 连接预热。
如果只设置Liveness:
应用尚未启动完成 → Liveness失败 → 重启 → 永远无法启动Startup Probe在成功前保护应用,不执行普通Liveness判定。
startupProbe:httpGet:path:/actuator/health/livenessport:9090periodSeconds:5failureThreshold:36最大启动窗口:
5秒 × 36 = 180秒窗口应来自真实冷启动P99,而不是凭经验随意设置。
八、推荐Kubernetes探针
startupProbe:httpGet:path:/actuator/health/livenessport:9090periodSeconds:5timeoutSeconds:2failureThreshold:36livenessProbe:httpGet:path:/actuator/health/livenessport:9090periodSeconds:10timeoutSeconds:2failureThreshold:3readinessProbe:httpGet:path:/actuator/health/readinessport:9090periodSeconds:5timeoutSeconds:2failureThreshold:2successThreshold:1不要把timeoutSeconds设得比健康检查内部调用的依赖超时还短,否则会制造随机失败。
九、不要在探针里调用真实模型
错误实现:
@ComponentpublicclassModelHealthIndicatorimplementsHealthIndicator{publicHealthhealth(){Stringanswer=chatClient.prompt().user("ping").call().content();returnanswer!=null?Health.up().build():Health.down().build();}}问题:
- 每几秒产生Token费用;
- Provider限流时探针失败;
- 模型延迟拖慢探针;
- 所有Pod同时发请求;
- 健康检查增加生产负载;
- 真实Prompt可能进入日志。
更合理的方式:
- 后台主动探测并缓存结果;
- 检查路由器是否至少有一个可用Profile;
- 探针只读本地聚合状态;
- Provider探测使用短TTL和熔断;
- 业务流量仍执行独立重试和Fallback。
十、自定义Readiness策略
@ComponentpublicclassAiReadinessPolicy{privatefinalProviderCapabilityRegistryproviderRegistry;privatefinalOverloadGuardoverloadGuard;privatefinalMigrationStatemigrationState;publicReadinessDecisionevaluate(){if(migrationState.blocksTraffic()){returnReadinessDecision.refuse("MIGRATION");}if(overloadGuard.isSeverelyOverloaded()){returnReadinessDecision.refuse("OVERLOAD");}if(!providerRegistry.hasAnyUsableChatProfile()){returnReadinessDecision.refuse("NO_CHAT_MODEL");}returnReadinessDecision.accept();}}通过ApplicationAvailability发布状态:
@ComponentpublicclassAiReadinessPublisher{privatefinalApplicationEventPublisherpublisher;privatefinalAiReadinessPolicypolicy;@Scheduled(fixedDelay=5000)publicvoidrefresh(){ReadinessDecisiondecision=policy.evaluate();AvailabilityChangeEvent.publish(publisher,this,decision.accepting()?ReadinessState.ACCEPTING_TRAFFIC:ReadinessState.REFUSING_TRAFFIC);}}十一、Readiness抖动治理
外部依赖可能一秒好、一秒坏。若每次都立即切换Readiness,Endpoint会频繁加入和移出Service。
publicfinalclassHysteresisHealthGate{privatefinalintfailureThreshold;privatefinalintsuccessThreshold;privateintconsecutiveFailures;privateintconsecutiveSuccesses;privatebooleanready=true;publicsynchronizedbooleanupdate(booleancurrentHealthy){if(currentHealthy){consecutiveSuccesses++;consecutiveFailures=0;if(!ready&&consecutiveSuccesses>=successThreshold){ready=true;}}else{consecutiveFailures++;consecutiveSuccesses=0;if(ready&&consecutiveFailures>=failureThreshold){ready=false;}}returnready;}}还可以增加最短保持时间,避免秒级来回切换。
十二、数据库应该进入Readiness吗
强依赖数据库
如果每个请求都必须完成鉴权、读取租户配置、写审计和扣预算,数据库不可用时无法服务,可以进入Readiness。
可短时降级
如果只读缓存仍能回答部分FAQ,可以保持Ready但标记Capability降级。
不要把“依赖Down”机械等价为“整个Pod Unready”。
十三、模型Provider故障的正确策略
Primary失败 ↓ 路由Fallback ↓ Fallback也失败 ↓ 按任务类型: 返回降级 转异步 拒绝新请求只有所有关键Profile不可用且没有可接受降级时,才考虑Readiness拒绝AI流量。
Provider故障更适合进入:
provider_availability provider_error_rate routing_fallback_rate而不是Liveness。
十四、向量库故障怎么处理
对于RAG专用接口:
VectorStore不可用 → RAG Capability不可用对于同时支持非RAG任务的应用:
Chat仍可用 → 整个Pod不必Unready可以按路径拆分服务,也可以由API Gateway根据能力状态路由。
十五、滚动发布期间为什么会503
常见时序:
新Pod启动 ↓ Readiness过早变为Ready ↓ 流量进入 ↓ Prompt、连接池、索引元数据尚未预热 ↓ 超时与503另一个方向:
旧Pod收到SIGTERM ↓ 仍保持Ready数秒 ↓ 继续收到新请求 ↓ 容器被终止 ↓ 流式回答中断需要同时处理新Pod进入与旧Pod退出。
十六、预热与Readiness
启动完成不等于业务已预热。
可以预热:
- 数据库连接;
- Redis连接;
- 模型路由配置;
- Prompt模板;
- Tokenizer;
- 向量库Collection元数据;
- HTTP连接池和DNS;
- 必需证书。
@ComponentpublicclassAiWarmupRunnerimplementsApplicationRunner{privatefinalWarmupCoordinatorwarmupCoordinator;privatefinalApplicationEventPublisherpublisher;@Overridepublicvoidrun(ApplicationArgumentsargs){warmupCoordinator.warmup();AvailabilityChangeEvent.publish(publisher,this,ReadinessState.ACCEPTING_TRAFFIC);}}预热不要调用高费用的真实生成,可使用Metadata接口、低成本连接测试和专用测试Profile。
十七、优雅停机的正确时序
Pod进入Terminating ↓ preStop执行 ↓ 应用切换Readiness=REFUSING_TRAFFIC ↓ Endpoint从Service摘除 ↓ 等待流量传播 ↓ SIGTERM ↓ Spring Boot Graceful Shutdown ↓ 停止接收新请求 ↓ 排空短请求与流式请求 ↓ 释放任务租约 ↓ 退出Spring Boot在优雅停机阶段拒绝新流量,并允许在途请求在配置窗口内完成。
十八、preStop
lifecycle:preStop:exec:command:-/bin/sh--c->wget --post-data='' -qO- http://127.0.0.1:9090/internal/drain || true; sleep 10/internal/drain应:
- 只允许本机或管理网络;
- 切换Readiness;
- 禁止新长任务;
- 通知Worker停止拉取;
- 不暴露公网。
十九、Drain Manager
@ServicepublicclassDrainManager{privatefinalApplicationEventPublisherpublisher;privatefinalAtomicBooleandraining=newAtomicBoolean();publicvoidbegin(){if(!draining.compareAndSet(false,true)){return;}AvailabilityChangeEvent.publish(publisher,this,ReadinessState.REFUSING_TRAFFIC);}publicbooleanisDraining(){returndraining.get();}}新任务入口先检查:
if(drainManager.isDraining()){thrownewServiceDrainingException();}二十、异步长任务如何停机
AI任务中心不能依赖HTTP请求完成。Pod停机时:
- Worker停止领取新任务;
- 当前步骤尽量Checkpoint;
- 不可快速完成时停止续租;
- 让租约到期;
- 新Worker接管;
- 不要直接把任务标记FAILED。
@EventListenerpublicvoidonShutdown(ContextClosedEventevent){workerAdmission.stopAccepting();taskCheckpointCoordinator.flushInProgress();leaseManager.stopRenewingAfterCheckpoint();}二十一、流式回答如何排空
SSE或Flux流可能持续几分钟。
spring:lifecycle:timeout-per-shutdown-phase:60sKubernetes:
terminationGracePeriodSeconds:75Kubernetes窗口应大于应用优雅停机窗口,并预留preStop时间。
若允许无限长流,任何固定窗口都可能截断请求。产品应提供Response ID、断线重连、结果持久化或异步任务模式。
二十二、Tool Calling停机风险
工具可能正在:
- 发邮件;
- 提交订单;
- 发权益;
- 写ERP。
SIGTERM后不能盲目重试。工具执行记录必须包含:
toolCallId;- 幂等键;
- 状态;
- 外部操作ID;
- UNKNOWN状态。
新Pod恢复时先查询外部结果,不要直接重做。
二十三、Deployment滚动参数
strategy:type:RollingUpdaterollingUpdate:maxUnavailable:0maxSurge:1minReadySeconds:20progressDeadlineSeconds:600revisionHistoryLimit:10含义:
maxUnavailable: 0:尽量不减少现有可用实例;maxSurge: 1:额外启动一个新Pod;minReadySeconds:新Pod持续Ready一段时间才视为可用;progressDeadlineSeconds:发布长期无进展时标记失败;revisionHistoryLimit:保留旧ReplicaSet用于回滚。
资源紧张时要确认集群能容纳Surge。
二十四、PodDisruptionBudget
apiVersion:policy/v1kind:PodDisruptionBudgetmetadata:name:spring-ai-apispec:minAvailable:2selector:matchLabels:app:spring-ai-apiPDB主要约束自愿中断,不替代副本数、探针和发布策略。
二十五、资源配置
AI编排应用常见瓶颈:
- HTTP连接;
- 内存中的Prompt;
- 流式连接;
- 线程池;
- JSON序列化;
- 本地Tokenizer;
- Tool并发。
resources:requests:cpu:"500m"memory:"1Gi"limits:cpu:"2"memory:"2Gi"CPU Limit过低时,冷启动和GC可能导致探针超时。探针失败应同时排查CPU throttling、GC pause、线程池饱和、连接池和DNS。
二十六、探针端点安全
Actuator不要全部公网暴露。
management:endpoints:web:exposure:include:-health-prometheus详细健康信息可能泄露数据库、Provider、区域和内部版本。公网只返回最小状态,详细信息放内部监控。
二十七、监控指标
kube_pod_container_status_restarts_total kube_pod_status_ready spring_ai_readiness_transition_total{ reason } spring_ai_drain_in_progress spring_ai_inflight_requests spring_ai_streaming_requests spring_ai_worker_active_tasks spring_ai_task_lease_released_total spring_ai_provider_available{ profile } spring_ai_routing_fallback_total spring_ai_startup_duration_seconds spring_ai_graceful_shutdown_duration_seconds spring_ai_forced_termination_total二十八、告警
Pod重启率突然上升 Readiness频繁抖动 新版本Ready后错误率升高 Terminating Pod仍有大量新请求 强制终止流式请求 租约过期任务上升 Provider故障导致全Pod重启二十九、自动化测试
1. Provider故障不影响Liveness
Fake Provider返回503,断言:
liveness=UP2. 无可用模型时Readiness拒绝
主备Profile全部不可用,断言Readiness拒绝流量。
3. Startup保护
Warmup未完成时不接收业务请求,但不会被Liveness重启。
4. Drain
调用Drain后:
- Readiness拒绝;
- 新长任务返回503或429;
- 在途短请求继续;
- Worker停止领取任务。
5. SIGTERM
发送SIGTERM,验证Checkpoint和租约状态。
6. Rolling Update
发布新镜像,确认:
- 可用副本不低于目标;
- 无请求丢失;
- 旧Pod排空;
- 新Pod满足
minReadySeconds。
三十、完整Deployment示例
apiVersion:apps/v1kind:Deploymentmetadata:name:spring-ai-apispec:replicas:3revisionHistoryLimit:10minReadySeconds:20progressDeadlineSeconds:600strategy:type:RollingUpdaterollingUpdate:maxUnavailable:0maxSurge:1selector:matchLabels:app:spring-ai-apitemplate:metadata:labels:app:spring-ai-apispec:terminationGracePeriodSeconds:75containers:-name:appimage:registry.example.com/spring-ai-api:release-v42ports:-name:httpcontainerPort:8080-name:managementcontainerPort:9090startupProbe:httpGet:path:/actuator/health/livenessport:managementperiodSeconds:5timeoutSeconds:2failureThreshold:36livenessProbe:httpGet:path:/actuator/health/livenessport:managementperiodSeconds:10timeoutSeconds:2failureThreshold:3readinessProbe:httpGet:path:/actuator/health/readinessport:managementperiodSeconds:5timeoutSeconds:2failureThreshold:2lifecycle:preStop:exec:command:-/bin/sh--c->wget --post-data='' -qO- http://127.0.0.1:9090/internal/drain || true; sleep 10resources:requests:cpu:"500m"memory:"1Gi"limits:cpu:"2"memory:"2Gi"三十一、最终排查清单
□ Startup、Liveness和Readiness已分离 □ 外部模型故障不会直接触发Liveness失败 □ 探针不会调用真实付费模型 □ Readiness使用本地聚合能力状态 □ Readiness具有失败和恢复滞回 □ 冷启动P99用于计算Startup窗口 □ 新Pod完成必要预热后才Ready □ 管理端口与业务端口可用性关系已验证 □ server.shutdown=graceful □ preStop先切换Readiness并等待流量摘除 □ terminationGracePeriod大于preStop+停机窗口 □ Worker停机前Checkpoint并停止续租 □ 流式回答具备断线恢复策略 □ Tool副作用具备幂等和UNKNOWN状态 □ RollingUpdate设置合理的Surge和Unavailable □ minReadySeconds和progressDeadline已配置 □ 详细Actuator信息不对公网暴露 □ 已进行Provider故障、SIGTERM和滚动发布测试总结
Spring AI应用在Kubernetes中频繁重启和503,根本原因通常不是“探针不够多”,而是健康语义设计错误。
正确分层是:
Startup 保护慢启动 Liveness 只判断进程是否需要重启 Readiness 决定是否接收新流量 Capability Health 表达模型、RAG和工具的细粒度能力再配合预热、Readiness滞回、优雅停机、流量排空、任务Checkpoint和工具幂等,才能避免外部Provider故障触发重启风暴,也避免滚动发布中断流式回答和异步任务。
