健康、就绪与优雅启停:实例怎样安全接入和退出流量
实例从创建到退出,至少要完成三次交接:初始化完成后接收请求,暂时无法服务时通知路由方避开自己,关闭时处理已经接收的工作。应用负责报告状态和结束工作,容器平台负责探测、路由与进程管理;这两部分需要相互配合。
健康检查要对应明确的平台动作
三类探针回答不同的问题
| 探针 | 要判断的事情 | Kubernetes 中达到失败阈值后的主要动作 |
|---|---|---|
| startup | 启动初始化是否已完成到可检查的程度 | 按配置处理容器重启;成功前抑制 liveness/readiness 检查 |
| liveness | 进程内部是否进入无法自行恢复的状态 | 终止并按重启策略处理容器 |
| readiness | 当前实例是否适合接收新的服务流量 | 将实例标记为未就绪,从通常的 Service 可用后端中排除 |
readiness 失败本身不会触发重启。TCP 探测检查地址是否接受连接;HTTP 健康端点还可以根据应用状态返回结果,检查了哪些状态由端点实现决定。探针配置与各类检查方式见 Kubernetes 探针文档。
一个实例可能存活且未就绪:例如正在加载只属于本实例的必要索引,或已经准备退出。也可能 HTTP 端口已监听,初始化任务尚未完成。把“端口开放”直接当成“可以处理业务”,容易在滚动发布时接入半初始化实例。
外部依赖放在哪里
数据库故障通常无法靠重启所有应用实例修好。若把共享数据库可达性直接并入 liveness,所有实例可能同步重启,恢复时又同时建立大量连接。liveness 更适合反映应用内部无法恢复的状态。
readiness 是否包含数据库检查,需要看该依赖影响哪些请求、是否所有实例同时受影响、摘除后还剩多少可用后端。全体实例共享同一个坏依赖时,把它们全部摘除可能导致服务没有可选地址;若仍有无需该数据库的查询可提供,应用内部的降级或按路由减载往往更合适。
Spring Boot 默认的 readiness/liveness 组不会随意纳入所有外部健康指标。可以按业务需要加入其他指标,但应先确定其失败会引发什么动作,见 Actuator Kubernetes Probes。
探针自身也要轻量、有时限。每个实例每秒执行复杂 SQL,既增加共享依赖负载,也会让健康判断被锁等待拖住。访问探针的身份、网络策略和限流规则要与平台配置一致,避免平台因 401、403 或自身限流误判服务状态。
检查主服务端口
管理端口独立部署时,它可能在业务端口拥塞、线程池耗尽或监听失败后仍然返回健康。因此至少要有一条检查经过真实业务端口的路径。Boot 可把探针路径额外暴露到主端口,具体配置见前面的 Actuator 文档;下面的实验直接让健康端点和业务接口共用 8080。
Docker 的 HEALTHCHECK 会更新容器健康状态,但普通 Docker restart policy 主要响应容器退出,不会仅因 unhealthy 就自动重启。实际重启动作需要对应的编排或管理逻辑,见 Docker 自动启动容器。
在 Spring Boot 中观察就绪状态变化
从初始化到接受流量
Boot 使用 LivenessState、ReadinessState 表达可用性,应用可以通过 ApplicationAvailability 读取,并发布状态变化事件。启动期间,必要的 ApplicationRunner、CommandLineRunner 任务完成后,应用才进入接受流量的阶段。状态与事件顺序见 SpringApplication Application Availability。
下载 健康与启停实验,解压到 reliability-availability。版本为 Spring Boot 4.1.1、Java 17/25、Maven 3.9.12;运行使用嵌入式 Tomcat,没有数据库和 Kubernetes 集群。
server.port=8080
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=15s
management.endpoints.web.exposure.include=health
management.endpoint.health.probes.enabled=true
management.endpoint.health.show-details=never工程用一个可配置的启动任务模拟必要初始化,在任务开始和完成时各输出一条标记。实验中延迟为 8 秒,期间可以访问已启动的 HTTP 服务器,readiness 为 503、liveness 为 200;任务完成后 readiness 转为 200。
Linux Bash 下,当前用户需要 Docker 使用权限,项目目录和缓存需要可写。构建容器显式使用当前 UID/GID:
mkdir -p .m2-lab
docker run --rm --user "$(id -u):$(id -g)" \
--entrypoint mvn -e HOME=/tmp -e MAVEN_CONFIG=/tmp/.m2 \
-v "$PWD:/work" -v "$PWD/.m2-lab:/m2" -w /work \
maven:3.9.12-eclipse-temurin-17 \
-B -Dmaven.repo.local=/m2 -Duser.home=/tmp clean verify应得到可执行 JAR,并有一项真实 HTTP 集成测试通过。改用 Java 25 的 Maven 镜像可重跑同一套测试;不要在运行中的实验容器仍挂载该 JAR 时执行 clean 重建。
readiness 是信号,直接请求仍然可能进入
实验管理入口会发布下列事件:
AvailabilityChangeEvent.publish(context, ReadinessState.REFUSING_TRAFFIC);随后 /actuator/health/readiness 返回 503,/actuator/health/liveness 保持 200。但直接请求 /work 仍返回 completed,因为只修改可用性状态,并没有添加拒绝业务请求的过滤器,也没有外部路由器参与实验。
readiness=503 liveness=200 directWork=completed在 Kubernetes 中,平台探测到状态变化并更新路由后,新连接通常会避开该实例;传播过程、已有连接和客户端直连仍需考虑。如果业务要求应用一旦进入排空状态就拒绝某类新请求,应在入口增加明确的准入检查,并保留探针、管理接口和已有工作所需路径。
/lab/readiness 和 /lab/active 是无鉴权的本地实验控制入口,只允许回环发布。生产系统应保护管理操作,不能让任意调用者把实例摘流或读取内部执行信息。
用真实关闭信号检查在途请求
正常排空与强制终止分别验证
构建完成后,在同一目录运行:
bash verify.sh脚本创建随机名称的本地容器,用非 root 用户、只读文件系统和可写 /tmp 运行 JAR,随机端口仅绑定宿主回环。它依次检查初始化探针、就绪切换、正常排空和超短宽限。典型结果为:
duringRunner readiness=503 liveness=200
refusing readiness=503 liveness=200 directWork=completed
graceful result=completed exit=143 (SIGTERM)
forcedKill transport=52 exit=137 completedBody=falsetransport 的具体非零值可能因连接终止方式变化;脚本检查它非零、响应不是 completed,并检查容器退出码 137。不会仅凭一个空响应判断强杀。
正常路径发起一个 4 秒请求,确认服务端在途计数已为 1,才执行 docker stop -t 10。请求得到完整结果后,容器退出。这里 Java 进程响应 SIGTERM 的退出码为 143,即 128 + 15;结合完整响应和已退出状态,它对应本次有序关闭,不应按“非零一定是崩溃”解释。
负例发起 10 秒请求,却只给 Docker 1 秒宽限。容器被 SIGKILL 结束,退出码 137,即 128 + 9,客户端未得到完整响应。仅看 137 不能区分所有场景下的强杀与 OOM;此实验由明确的停止命令触发,其他环境还要检查 OOMKilled 和平台事件。
Java 17 的容器复验方式为:
IMAGE=eclipse-temurin:17.0.20_8-jdk bash verify.sh脚本结束只清理它创建的容器,保留临时响应文件目录供检查。若失败在探针等待阶段,先查看打印前的 curl 错误、JAR 是否存在、镜像版本与容器启动日志;若失败在排空阶段,检查服务端是否确实已经接收请求,以及实际停止宽限。
两层关闭预算
Docker 默认向容器主进程发送配置的停止信号,通常是 SIGTERM;宽限到期仍未退出时再强制结束。超时参数见 docker container stop。应用应以 exec 形式运行 Java,让信号到达正确的主进程;多包一层不转发信号的 shell,可能使应用来不及处理关闭。
Boot 的优雅关闭在应用上下文停止时启动,让 Web 服务器停止接收新工作并等待活动请求完成。Tomcat、Jetty、Reactor Netty 的连接处理细节并非完全一致,需按实际服务器验证。Boot Graceful Shutdown说明了服务器行为及配置方式。
spring.lifecycle.timeout-per-shutdown-phase=15s 表示每个关闭阶段的时间上限。若还有其他 SmartLifecycle 组件在不同阶段停止,应用总退出时间可能超过 15 秒。平台预算应覆盖退流等待、必要关闭阶段和安全余量,不能只比一个配置数字。
同时,15 秒是最多允许等待的额度;4 秒工作可以提前完成,应用无需等满 15 秒。本实验的 Docker 10 秒足以容纳那条已知 4 秒请求,却不适合据此作为其他服务的固定配置。
将启停行为接入实际部署
Pod 终止包含并行变化
删除 Pod 后,控制面和节点上的 kubelet 会推进各自的终止步骤。服务端点的 terminating/ready 状态更新,与节点执行 preStop、发终止信号、等待进程退出之间,不存在一个可以假设为零延迟的串行事务。
控制面与路由:Pod 进入终止 → 端点状态更新 → 各路由组件逐步获知
节点与应用: 宽限开始 → preStop(若有)→ TERM → 等待退出 → 必要时 KILL
在途请求: 已接受的请求继续执行,受应用及平台剩余时间约束终止中的端点会带上 terminating 信息,通常不再以 ready=true 接受常规流量;需要区分排空时仍可服务的消费者可查看 serving 条件。具体行为见 Pod Lifecycle。
preStop 消耗同一段终止宽限,完成后才发送正常终止信号。给钩子加固定 sleep 可以留出传播时间,但数值应来自路由实际表现,而不是把 sleep 当作路由已完成的确认。钩子超时还存在平台规定的短暂延长等处理,见 Container Lifecycle Hooks。
下面是将已有 Boot 容器接到探针上的配置片段,需合并到真实 Deployment 的对应容器与 Pod 配置中;它不是可直接创建完整应用的清单:
spec:
terminationGracePeriodSeconds: 40
containers:
- name: app
startupProbe:
httpGet: {path: /actuator/health/liveness, port: 8080}
periodSeconds: 2
failureThreshold: 30
livenessProbe:
httpGet: {path: /actuator/health/liveness, port: 8080}
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet: {path: /actuator/health/readiness, port: 8080}
periodSeconds: 2
timeoutSeconds: 1
failureThreshold: 2startup 复用 liveness 路径,表示先让服务达到存活检查条件;初始化任务尚未完成时,readiness 仍能继续挡住平台接流。periodSeconds × failureThreshold 可以帮助估算连续失败的容忍窗口,但实际检测时刻还受调度、超时和探针起点影响。上面的数值需要按应用启动与恢复时间调整。
HTTP 之外的工作也要停止入口
消息消费者通常先停止拉取新消息,再等待正在处理的消息结束,并按照实际处理结果提交确认或允许重投。定时任务需要停止产生新任务;线程池需要拒绝新提交并等待已接收任务,超时后处理未完成部分。数据库连接和客户端应在依赖它们的工作结束后关闭。
一项长任务若无法在终止宽限内结束,可以保存进度并由下一实例恢复,或者缩小任务单位。只在 shutdown hook 里写一句“正在保存”,无法保证强杀前已持久化;实际完成条件应来自存储结果和恢复后的读取。
服务间调用也可能在关闭过程中超时。已经提交但来不及响应的写入,仍按幂等查询与重放处理;优雅关闭只能降低这种窗口,无法消除节点断电或强杀。
常见故障的检查顺序
| 现象 | 优先确认 |
|---|---|
| 应用一直重启 | 退出原因、OOM、liveness 是否错误依赖共享服务、startup 是否过短 |
| readiness 503 但仍有请求进入 | 是否直连、已有长连接、路由传播、应用是否实现准入拒绝 |
| 健康端点正常但业务不通 | 探针是否经过业务端口、业务线程/连接资源是否独立耗尽 |
| 发布时频繁 502 或连接重置 | TERM 是否到达 Java、在途时间、路由摘除速度和外层宽限 |
| 实例迟迟不能退出 | 哪个生命周期阶段未结束、未消费的流、线程池或消息消费者 |
| 退出后出现重复处理 | 消息确认时机、原动作是否已提交、去重记录是否可用 |
接流前的检查应覆盖初始化完成和必要资源可用;少量请求进入后,再观察成功率、延迟与在途数量。如果全体未就绪源于共享依赖故障,恢复工作要从依赖和流量入口开始。实例重新连接、缓存加载和积压处理应错开,降低同时接流产生的峰值。
权威资料与规范地址
- Kubernetes 探针:startup、liveness、readiness 与检测参数。https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
- Spring Boot Actuator:健康组、探针和主端口配置。https://docs.spring.io/spring-boot/reference/actuator/endpoints.html
- SpringApplication:可用性状态与启动事件。https://docs.spring.io/spring-boot/reference/features/spring-application.html
- Spring Boot Graceful Shutdown:Web 服务器关闭与阶段超时。https://docs.spring.io/spring-boot/reference/web/graceful-shutdown.html
- Docker restart policy:容器退出后的重启条件。https://docs.docker.com/engine/containers/start-containers-automatically/
- Docker stop:停止信号与宽限。https://docs.docker.com/reference/cli/docker/container/stop/
- Kubernetes Pod Lifecycle:终止流程与端点状态。https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/
- Kubernetes Container Lifecycle Hooks:preStop 和终止宽限。https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/
