1. 一次发布引发的线上事故:我们为什么需要“优雅下线”
先看一个真实场景。假设你负责一个订单服务,深夜发布新版本。你直接在注册中心把实例下线,然后执行 kill 命令。监控却立刻弹出告警:该实例在停止过程中处理了 300 多笔订单,其中 40 笔因数据库连接被强制关闭而失败,用户收到“系统繁忙,请稍后重试”。更糟的是,下游支付回调打到该实例时连接被拒,引发了重试风暴。这次事故的根源是什么?因为你“粗暴地杀掉进程”,没有给应用一个缓冲期来完成它正在处理的请求,也没有提前通知网关和负载均衡器“我要离开了”。
这就是为什么需要“优雅下线”。它不是一个单一开关,而是一条从流量入口到应用内部的完整动作链。只有理解了这条链,你才能在发布、扩缩容、故障恢复时做到用户无感知。
2. 先记一个最小模型:摘流量 → 停新 → 处理存量 → 释放资源
你可以把优雅下线理解成一场有序的“退场仪式”。先记住四步主流程:
- 摘流量:让负载均衡器、网关、注册中心不再把新请求发给当前实例,并等待“在途通知”完成。
- 停新:拒绝或缓冲新进入的请求,避免处理一半时进程退出。
- 处理存量:给正在处理的请求一个宽限期,让它们完成事务、返回响应。
- 释放资源:关闭线程池、数据库连接池、消息消费者,最后退出进程。
这四个环节由不同的组件协作完成:负载均衡器 / 网关 / 注册中心负责“摘流量”,停机钩子(shutdown hook) 负责“停新”和“处理存量”,健康检查负责向外部汇报状态,而 spring 容器 负责释放资源。它们通过操作系统信号、http 探针、注册中心的元数据等手段连接起来。
整个过程的角色与连接关系可以用下面的 ascii 图表示:
客户端/用户
|
v
[负载均衡器/网关] ---- 健康检查探针 ----> spring 应用 (端口 8080)
| |
| 摘除流量(注册中心反注册/api下线) | 1.sigterm
v v
[注册中心 (nacos/eureka)] [停机钩子] ---> 线程池停止接收新任务
| 等待存量任务完成
v
[spring 容器销毁] ---> 关闭数据源/连接池
|
v
进程退出
注意:健康检查并不直接参与停机流程,但它决定了负载均衡器何时开始摘除流量。所以,你必须把健康检查、负载均衡摘除和停机钩子放在一起设计,而不是各自为政。
3. 停机钩子:应用内部的“总指挥”
3.1 停机钩子是什么,它解决什么问题
当操作系统向 java 进程发送中断信号(通常是 sigterm,即 kill 命令默认信号)时,jvm 会触发已注册的关闭钩子(shutdown hook)。它允许你在进程退出前执行清理代码。spring boot 的 applicationcontext 本身也注册了一个钩子,用于调用 close() 方法,从而触发 bean 的销毁和容器关闭。
这个机制解决的问题是:让应用有机会“体面地”处理完正在进行的工作,而不是被系统强行终止。
3.2 最小示例:利用 spring@predestroy感知停机
先看一个能直接运行的最小示例。创建一个 spring boot 项目,添加 spring-boot-starter-web,然后写一个带 @predestroy 方法的 bean。目标:当进程收到 sigterm 时,观察控制台输出,证明钩子被调用。
代码 1: 最小停机钩子示例(完整可运行)
前置环境:jdk 8+,maven 3.6+,spring boot 2.7+。创建一个 maven 项目,pom.xml 中加入:
<parent>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-parent</artifactid>
<version>2.7.18</version>
</parent>
<dependencies>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>
</dependencies>主类 shutdowndemoapplication.java:
import org.springframework.boot.springapplication;
import org.springframework.boot.autoconfigure.springbootapplication;
import org.springframework.context.annotation.bean;
import javax.annotation.predestroy;
@springbootapplication
public class shutdowndemoapplication {
public static void main(string[] args) {
springapplication.run(shutdowndemoapplication.class, args);
}
@bean
public shutdownlistener shutdownlistener() {
return new shutdownlistener();
}
public static class shutdownlistener {
@predestroy
public void onshutdown() {
system.out.println("[优雅停机] 正在清理资源,当前时间: " + system.currenttimemillis());
}
}
}
操作步骤:
- 使用
mvn spring-boot:run或打包后java -jar target/demo.jar启动应用。 - 找到进程 pid(例如
ps -ef | grep demo.jar)。 - 执行
kill <pid>(发送 sigterm)。
预期输出:在应用退出前,控制台打印 [优雅停机] 正在清理资源...。
这个示例证明了 @predestroy 方法会被调用。但它并没有真正处理请求。你需要在真实场景中,在 @predestroy 里关闭线程池、等待任务完成。
常见误区:@predestroy 只在容器正常关闭时触发,如果你用 kill -9 强杀,钩子不会执行。另外,如果你在钩子里执行耗时的清理操作,而默认超时时间到了,jvm 会强制退出,所以不要做太多重活。
3.3 spring boot 2.3+ 的优雅停机配置:行为与取舍
从 spring boot 2.3 开始,内置了优雅停机支持,针对 web 服务器(tomcat、jetty、reactor netty)和消息监听器。你只需要在 application.properties 中设置:
# 开启优雅停机(默认 immediate) server.shutdown=graceful # 最长等待时间,默认 30 秒(注意:这是 spring boot 的全局限制,不同服务器有不同覆盖) spring.lifecycle.timeout-per-shutdown-phase=20s
这里有两个概念需要区分:
server.shutdown=graceful告诉 web 容器在收到停机信号后停止接受新请求,并等待已接收请求完成。spring.lifecycle.timeout-per-shutdown-phase是 spring 容器每个关闭阶段(如 bean 销毁)的超时时间,不是等请求的总时间,总时间以 web 服务器的实现为准,例如 tomcat 默认等待 30 秒(可配置)。
设计取舍:优雅停机能提高可用性,但也延长了实例的退出时间——如果请求处理很慢,你可能要等满超时。所以,你需要平衡“等待时长”和“发布速度”。一般设置 10~30 秒就够,如果业务经常有长事务,需要更长且配合流量摘除策略。
小例子:假设你的下单接口平均耗时 200ms,但偶尔有 5 秒的报表请求。设置 server.shutdown=graceful 和 spring.lifecycle.timeout-per-shutdown-phase=10s,在停机时 tomcat 会等所有活跃请求完成,但最多等多久?实际上 tomcat 自己也有超时,默认 30 秒。所以 10 秒是 spring 容器销毁阶段的限制,而 tomcat 等待请求完成可能超过 10 秒?这里需要看依赖:如果你的 tomcat 版本高于 9.0.33,spring boot 2.3+ 会设置 tomcat 的 gracefulshutdowntimeout 为 spring.lifecycle.timeout-per-shutdown-phase 的值?实际上 spring boot 把 server.shutdown 映射到 tomcat 的 gracefulshutdown 方法,等待时间为 spring.lifecycle.timeout-per-shutdown-phase?? 不,更准确的是:spring boot 2.3 中,tomcat 的优雅停机等待时间取 spring.lifecycle.timeout-per-shutdown-phase 的值作为每个阶段,但同时 tomcat 的 gracefulshutdown 也有它的超时配置(可通过 server.tomcat.graceful-shutdown-timeout 设置,默认 30 秒)。所以两者可能共同作用,你需要了解细节以避免陷阱。
为了避免版本混淆,建议测试你的环境。
4. 健康检查:让外部知道“我还能不能用”
4.1 健康检查的角色
健康检查是负载均衡器或编排平台(如 k8s)用来判断实例是否存活和就绪的机制。在引入 spring boot actuator 后,/actuator/health 暴露实例的健康状态。kubernetes 等平台通过探测这个端点决定是否将流量路由到该实例。
4.2 两种探针:存活与就绪
k8s 有两种探针:
- 存活探针(livenessprobe):检测进程是否死锁或崩溃,失败会重启容器。
- 就绪探针(readinessprobe):检测应用是否准备好接收流量,失败会将 pod 从 service 的 endpoint 中摘除,但不会重启。
优雅下线主要用就绪探针。当你收到停机信号时,希望先让就绪探针失败,这样 service 会停止向该 pod 转发新请求。
4.3 完整示例:基于 spring boot actuator 与 k8s 就绪探针的下线流程
示例 2:结合健康检查的优雅下线(贴近生产)
前置环境:需要 docker 和可用的 kubernetes 集群,或者你可以用 minikube。同样使用前面的 spring boot 项目,添加 actuator 依赖和业务模拟接口。
代码 2.1:pom.xml 添加依赖
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-actuator</artifactid>
</dependency>代码 2.2:配置 health 端点开放和停机设置
# 开启优雅停机 server.shutdown=graceful spring.lifecycle.timeout-per-shutdown-phase=20s # 暴露健康端点 management.endpoints.web.exposure.include=health,info # 让健康检查包含 db 和 diskspace(默认已有) management.endpoint.health.show-details=always
代码 2.3:模拟长耗时请求的接口
在主类中增加一个 controller,模拟一个耗时 15 秒的处理:
import org.springframework.web.bind.annotation.getmapping;
import org.springframework.web.bind.annotation.restcontroller;
@restcontroller
class longtaskcontroller {
@getmapping("/slow")
public string slow() throws interruptedexception {
thread.sleep(15000); // 模拟长任务
return "done";
}
}
代码 2.4:定义就绪探针,并在停机信号时切换为 not_accepting_traffic
这里的关键是如何让就绪探针失败?spring boot 没有内置“停止接收流量”的状态,但我们可以自定义 healthindicator 来反映实例状态。实现思路:监听停机信号,设置一个标志位,让 /actuator/health 返回 down。
更好的做法是注册一个停机钩子,在收到 sigterm 时先改变健康状态,再等待请求完成。但注意:钩子执行时 spring 容器还在,但 actuator 的 healthendpoint 可能已经失效?不,容器还没销毁,但端点可能还在。但更安全的方式是利用 k8s prestop 钩子,在容器停止前调用一个 api 来设置状态。
下面用一个简化方案:使用 spring boot 的 applicationlistener<applicationpreparedevent>?不,还是使用自定义 healthindicator 并用一个静态变量控制。
为了可运行,我们展示一个独立的完整示例:自定义 readinesshealthindicator 和 shutdownsignallistener。
import org.springframework.boot.actuate.health.health;
import org.springframework.boot.actuate.health.healthindicator;
import org.springframework.stereotype.component;
@component
public class readinessindicator implements healthindicator {
private volatile boolean acceptingtraffic = true;
public void setacceptingtraffic(boolean acceptingtraffic) {
this.acceptingtraffic = acceptingtraffic;
}
@override
public health health() {
if (acceptingtraffic) {
return health.up().build();
}
return health.down().withdetail("reason", "shutting down").build();
}
}
在 applicationrunner 或主类中注册一个线程监听信号,实际生产中可使用 sun.misc.signal,但为了跨平台更推荐使用 spring 的停机钩子?但 spring 钩子执行时机在容器关闭前,可能来不及把状态设为 down。事实上,k8s 发送 sigterm 后,同时执行 prestop 钩子(如果有)。更常见的做法是设置 prestop 调用一个接口:
假设你有一个 /internal/readiness/off 端点,仅供内部调用:
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.web.bind.annotation.postmapping;
import org.springframework.web.bind.annotation.restcontroller;
@restcontroller
public class admincontroller {
@autowired
private readinessindicator readinessindicator;
@postmapping("/internal/readiness/off")
public string turnoff() {
readinessindicator.setacceptingtraffic(false);
return "off";
}
}
在 k8s deployment 中,配置 prestop 钩子:
apiversion: apps/v1
kind: deployment
metadata:
name: demo-app
spec:
replicas: 1
selector:
matchlabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
containers:
- name: demo
image: demo:1.0
ports:
- containerport: 8080
readinessprobe:
httpget:
path: /actuator/health
port: 8080
initialdelayseconds: 5
periodseconds: 5
lifecycle:
prestop:
exec:
command: ["sh", "-c", "curl -x post http://localhost:8080/internal/readiness/off"]当删除 pod 时,k8s 先执行 prestop(这里把就绪状态设为 down),然后等待一段时间(默认 30 秒)才发送 sigterm。这个等待期内,service 端点已经摘除该 pod,新流量不再进来,存量请求得以继续。然后 sigterm 触发 spring 优雅停机,继续等待存量请求完成。
可观察结果:在滚动发布期间,旧 pod 的 /actuator/health 变为 down,新 pod 就绪后,流量平滑切换,无 502 报错。
适用场景:适合 k8s 部署的微服务。
容易改错的地方:健康检查的 periodseconds 不要太短;prestop 执行时间受 terminationgraceperiodseconds 限制(默认 30 秒),如果你需要更长的存量处理时间,要同步增加该值。另外,prestop 是阻塞的,它执行完前 k8s 不会发 sigterm,所以不要在里面做太长时间的操作。
4.4 误区:健康检查等于就绪状态?
很多人误以为只要健康检查成功,流量就一直可达。实际上,健康检查分为存活和就绪,存活失败会重启容器,就绪失败才会摘除流量。如果你的健康检查只有 up 状态,而你的应用因为内存或线程池耗尽无法处理新请求,探针可能依然为 up,但流量雪崩。所以最好在健康检查中增加自定义指标,如线程池剩余容量、数据库连接池使用率。
5. 负载均衡摘除:从入口切断新流量
5.1 摘除的两种方式:被动摘除与主动摘除
负载均衡器(如 nginx、spring cloud gateway)或注册中心(如 nacos、eureka)需要在上线前将实例从候选列表中移除。有两种思路:
- 被动摘除:靠健康检查失败让负载均衡器自动摘除。
- 主动摘除:应用主动调用注册中心的下线 api,或者通过脚本通知网关。
被动摘除简单,但存在健康检查间隔(通常几秒到几十秒)导致流量仍可能打过来。主动摘除更即时,但需要额外的编排。
5.2 spring cloud 场景:通过注册中心实现灰度摘除
如果你的服务使用 nacos 或 eureka 做注册发现,可以在停机前调用 nacosserviceregistry.deregister() 将实例注册删除。下面是用 spring cloud alibaba nacos 的示例:
示例 3:spring cloud nacos 下的主动摘除(贴近生产)
前置环境:一个可运行的 nacos 服务(可用 docker 启动),spring boot 2.7 + spring cloud alibaba 2021.0.5.0。
代码 3.1:依赖
<dependency>
<groupid>com.alibaba.cloud</groupid>
<artifactid>spring-cloud-starter-alibaba-nacos-discovery</artifactid>
</dependency>代码 3.2:配置 nacos 地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 spring.cloud.nacos.discovery.service-name=demo-service
代码 3.3:调用注销 api 实现主动摘除(伪代码)
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.web.bind.annotation.postmapping;
import org.springframework.web.bind.annotation.restcontroller;
import com.alibaba.cloud.nacos.registry.nacosserviceregistry;
import org.springframework.cloud.client.serviceregistry.registration;
@restcontroller
public class registrycontroller {
@autowired
private nacosserviceregistry nacosserviceregistry;
@autowired
private registration registration;
@postmapping("/deregister")
public string deregister() {
// 注意:nacosserviceregistry 的 deregister 需要 registration 对象
nacosserviceregistry.deregister(registration);
return "deregistered";
}
}
注意:deregister 调用后,实例在 nacos 中消失,但本地应用还在运行。因此,通常在发布脚本中先调用 /deregister,等待几秒(让 lb 同步),再发送 sigterm。
可观察结果:调用后,nacos 控制台该实例下线,客户端在下次拉取时不再获得该实例 ip,新请求不再分发到该实例。
适用场景:使用 spring cloud 且没有 k8s 时,手动控制发布流程。
容易改错的地方:不要误用 registration 类型,如果是 nacos 的 nacosregistration 必须正确注入。另外,如果在钩子里调用 deregister,需要确保网络可用,否则会因异常导致停机失败。
5.3 摘除时机的选择:先摘除,后停机,再等待
正确的顺序是:摘除流量 → 等待(例如 10 秒) → 发送 sigterm。其中等待时间取决于负载均衡器同步周期。对于 nacos,默认有 3 秒定时拉取?实际上客户端会缓存服务列表,最长可能延迟 30 秒。所以,如果你的负载均衡器是有状态的(如 ribbon 客户端缓存),摘除后必须等待足够长的时间。
6. 线程池与请求队列:停新与存量的核心
6.1 web 容器如何实现“停新等待完成”
在优雅停机开启后,spring boot 的 web 容器会停止接收新连接/新请求,但继续处理已经进入的请求。这一般通过“先关闭服务端套接字,不再 accept,然后等待活跃请求计数归零”实现。
6.2 自定义业务线程池:如何优雅停止
很多应用有自己的业务线程池(如执行异步任务)。当停机时,你需要停止提交新任务,并 shutdown() 等待任务结束。
示例 4:业务线程池优雅停止(可独立运行)
下面是一个纯 java 示例,模拟线程池停止。
代码 4.1:完整示例
import java.util.concurrent.*;
public class poolshutdowndemo {
public static void main(string[] args) throws interruptedexception {
executorservice pool = executors.newfixedthreadpool(4);
// 提交 10 个耗时任务
for (int i = 0; i < 10; i++) {
final int id = i;
pool.submit(() -> {
try {
system.out.println("task " + id + " started");
thread.sleep(2000);
system.out.println("task " + id + " finished");
} catch (interruptedexception e) {
thread.currentthread().interrupt();
system.out.println("task " + id + " interrupted");
}
});
}
// 模拟停机:停止接受新任务
pool.shutdown();
system.out.println("执行 shutdown, 拒绝新任务");
// 等待已提交任务完成,最多等 5 秒
if (!pool.awaittermination(5, timeunit.seconds)) {
system.out.println("还有任务未完成,强制关闭");
pool.shutdownnow(); // 中断剩余任务
}
system.out.println("线程池已完全停止");
}
}
运行预期:看到任务按顺序开始、完成;如果有任务超过 5 秒,会看到“强制关闭”。
解释:shutdown() 不再接受新任务,已提交的任务继续执行。awaittermination 轮询等待。如果超时,可调用 shutdownnow() 中断。
边界:如果你的任务不响应中断,shutdownnow 也无法终止它们,所以最好让任务支持中断。
6.3 常见错误:在@predestroy里直接调用system.exit
有些开发者会在钩子里写 system.exit(0),这会绕过 spring 的销毁顺序,导致资源没被释放。不要在钩子里调用 system.exit,应让容器正常关闭。
7. 完整过程:k8s 滚动更新的端到端时序
现在把前面的机制串起来,看一次典型的 k8s 滚动更新。
文本时序图:
k8s master old pod new pod user traffic | | | | |-- 触发滚动更新 --| | | | | | | |-- 启动新 pod ----|----------------->| | | | 等待就绪 | | | | | |-- 准备终止旧pod->| | | |-- 执行 prestop ->| /health down | | | |--- 摘除流量 --------------------> x |-- 等待终止宽限->| 继续处理存量请求 | | | | | |-- 发送 sigterm->| spring 钩子 | | | | 停止新请求 | | | | 等待在途请求完成 | | |-- 等待退出-------| | | |<-- 进程退出 ------| | | | | | | |---------------- 流量已切到新pod -------------------->
步骤拆解:
- k8s 创建新 pod,等待它就绪(readiness 探针通过)。
- 此时旧 pod 仍在服务。
- 滚动更新策略(默认 rollingupdate)会选一个旧 pod 删除。
- 在删除过程中,k8s 先执行
prestop钩子,把健康状态置 down,service 摘除该 pod。 - k8s 等待
terminationgraceperiodseconds(默认 30 秒)后发送 sigterm。 - sigterm 触发 spring 优雅停机:web 容器停止接收新请求,已接收请求快速完成,spring 容器释放资源。
- 进程退出,滚动更新继续下一台。
关键点:prestop 和 sigterm 之间有一段等待,这是典型的“优雅停机窗口”。你需要确保这段时间足够让流量列表同步,且存量请求能完成。如果你的服务需要长耗时处理,就应增加 terminationgraceperiodseconds 并相应调整 spring 的 timeout-per-shutdown-phase。
8. 配置对比与选型指南
8.1 不同部署方式下的配置一览表
| 部署方式 | 摘除流量手段 | 健康检查 | 优雅停机配置 | 主要注意点 |
|---|---|---|---|---|
| 虚拟机/裸机 + nginx | nginx upstream 手动剔除或脚本 | 依赖 nginx 健康检查 | server.shutdown=graceful | 需脚本配合 nginx reload,停机钩子需等待 nginx 同步 |
| docker + k8s | service endpoint 自动摘除(readiness 失败) | k8s 探针 | server.shutdown=graceful, prestop | 需定义 prestop 生效,注意 terminationgraceperiodseconds |
| spring cloud + nacos | 调用 deregister | spring boot actuator | server.shutdown=graceful | 需在停机前主动注销,并等待客户端缓存刷新 |
| 单机测试 | 无 | 无 | server.shutdown=graceful | 直接 kill 观察钩子 |
8.2 停机参数对比表
| 参数 | 默认值 | 作用 | 建议值 |
|---|---|---|---|
server.shutdown | immediate | 是否优雅停机 | graceful |
spring.lifecycle.timeout-per-shutdown-phase | 30s | spring 容器各阶段超时 | 10-20s |
server.tomcat.graceful-shutdown-timeout | 30s (tomcat 9.0.33+) | tomcat 等待请求完成的超时 | 与 spring 超时相一致或更大 |
terminationgraceperiodseconds (k8s) | 30s | k8s 发送 sigterm 前的等待 | 足够覆盖 prestop 和请求处理 |
8.3 常见误区与陷阱
| 误区/陷阱 | 说明 | 正确做法 |
|---|---|---|
kill -9 也能优雅停机 | 强杀不会执行钩子 | 只有 kill (sigterm) 才能触发 |
| 健康检查一直为 up | 不会摘除流量 | 需自定义状态,结合停机标记 |
只配置 server.shutdown=graceful 就够了 | 若负载均衡未摘除,新流量仍可能涌入 | 需设计完整的摘除流程 |
@predestroy 中可以执行大量清理 | 钩子执行也受总超时限制 | 将耗时操作移出或异步处理 |
| 停机钩子执行顺序与声明顺序相同 | 实际上 spring 容器销毁有特定顺序 | 了解 bean 生命周期 |
在线程池 shutdown 后调用 submit 会抛异常 | 但应避免提交新任务 | 统一停新接口 |
9. 工程落地:一步到位的发布脚本与配置示例
下面提供一个可复制的生产级发布脚本框架(适用于 vm + nacos)及配置。
9.1 停止应用前,先摘除流量并等待
#!/bin/bash
# 假设服务名和端口
export service_name=my-service
export port=8080
# 1. 调用内部摘除接口
curl -x post http://localhost:${port}/internal/readiness/off
# 或调用 nacos 注销接口(如果有)
# curl -x put "http://localhost:8848/nacos/v1/ns/instance?servicename=${service_name}&ip=127.0.0.1&port=${port}&enabled=false"
# 2. 等待负载均衡器同步(默认 10 秒)
sleep 10
# 3. 优雅停止应用
kill $(pgrep -f "java -jar my-service.jar")
# 等待进程退出
wait $(pgrep -f "java -jar my-service.jar") # 注意:需要捕获 pid9.2 验证健康检查的 curl 命令
# 检查应用健康状态 curl http://localhost:8080/actuator/health | jq . # 模拟摘除后健康检查 curl -x post http://localhost:8080/internal/readiness/off curl http://localhost:8080/actuator/health # 应返回 down
10. 生产实践建议
- 先摘流量,后发 sigterm:确保没有新流量再来。
- 预留缓冲时间:摘除操作后至少等待 10 秒,具体取决于注册中心/负载均衡器的同步周期。
- 合理设置超时:根据你的业务处理时长设置 spring 和 k8s 的超时,避免过长拖慢发布。常见生产配置:
server.shutdown=graceful,spring.lifecycle.timeout-per-shutdown-phase=20s, k8sterminationgraceperiodseconds=35s。 - 将健康检查与停机联动:用自定义
healthindicator或prestop让探针失败。 - 监控停机过程:在日志中记录请求数、活跃线程数、关闭阶段耗时,便于排障。
- 使用新的 spring boot 版本:老版本(2.3 之前)需要自己实现,强烈建议升级。
10.1 各类异常对应的排查思路(排障清单)
| 现象 | 可能原因 | 排查和解决 |
|---|---|---|
| 停机时 502/504 | 负载均衡未摘除或摘除太晚 | 检查健康检查是否失败;缩短摘除等待;增加等待时间 |
| 停机时数据库连接异常 | 在请求未完成时连接池已关闭 | 确保容器关闭顺序正确;调整 spring.datasource.hikari... 的关闭顺序;在 @predestroy 中先关闭业务线程池再关闭数据源 |
| 钩子未执行 | 使用了 kill -9 或钩子里有异常 | 改用 sigterm;捕获异常并记录日志 |
| 进度时间长导致容器被杀 | 超时设置过短 | 增加 terminationgraceperiodseconds 和 spring 超时 |
| 摘除后仍有新请求 | 客户端缓存了旧的实例列表 | 增加等待时间;强制清空本地 dns 或 ribbon 缓存 |
11. 面试/复盘问题
- spring boot 2.3+ 优雅停机需要设置哪些参数?底层原理是什么?
- 什么是停机钩子?它在 jvm 中如何触发?与
@predestroy的关系? - 在 kubernetes 中,如何利用探针优雅摘除流量?
prestop和 sigterm 的配合? - 如果业务中有一个 10 分钟的任务,你怎么设计优雅下线方案?
- 健康检查返回 down 一定会摘除流量吗?取决于什么?
- 为什么要在停机前先注销实例?直接 kill 会有什么风险?
12. 总结:优雅下线决策清单
最后,给你一张实用决策清单,当你要配置优雅下线时:
- 是否使用了 spring boot 2.3+? → 是:开启
server.shutdown=graceful;否:升级或手动实现。 - 是否部署在 k8s? → 是:配置
prestop和就绪探针;并在prestop中使就绪状态为 down;再发送 sigterm。 - 是否使用注册中心(nacos/eureka)? → 是:在发布脚本中先调用下线 api,并等待 10 秒。
- 是否有长时间运行的任务? → 是:增加
terminationgraceperiodseconds和 spring 超时,并确保任务可中断。 - 是否需要自动恢复? → 如果是滚动发布,k8s 自动完成;否则需脚本等待进程退出。
记住最小模型:摘流量 → 停新 → 处理存量 → 释放资源。只要每一步都明确由哪个组件负责,执行顺序正确,就能让用户无感。优雅下线不是银弹,但它是微服务生命周期中不可省略的一环。
以上就是spring应用优雅下线的完整指南的详细内容,更多关于spring应用优雅下线的资料请关注代码网其它相关文章!
发表评论