新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring Boot优雅停机全解析:从SIGTERM到K8s滚动发布

发布时间:2026/10/2 9:25:43来源:尧图网络
Spring Boot优雅停机全解析:从SIGTERM到K8s滚动发布
2. 优雅停机机制的前世今生前置知识SpringBoot应用部署在Tomcat等Web容器中正常的停止流程是收到停止信号如kill命令后直接结束进程正在处理的HTTP请求会被中断已放入消息队列的任务可能丢失数据库事务可能处于不确定状态。问题场景某次生产环境发布运维执行kill -9强制杀掉进程导致正在写入订单表的请求直接断掉数据库里出现十几条只有半截数据的记录。此后每次发版前都要先把流量摘掉等几分钟再用停止脚本重启。这种先摘流量再重启的土办法虽然能解决问题但流程很重、人工操作极易出错。优雅停机要解决的就是这件事让应用收到停止信号后先停止接收新请求、处理完正在进行的请求、释放资源然后才真正退出进程。整个流程对调用方完全透明不需要额外的人工干预。3. 优雅停机的硬核技术拆解3.1 三种常见停机方式对比SpringBoot应用常见的停机方式有三种停机方式处理方式风险程度kill -9内核直接杀掉进程高风险请求中断、数据不一致kill不带参数发送SIGTERM信号需要应用配合处理否则等同于直接退出shutdown endpoint旧方案SpringBoot提供的/actuator/shutdown接口需要手动调用且存在安全隐患kill -9的杀伤力在于它直接绕过应用的清理逻辑任何资源都没机会释放。kill命令不带参数发送的是SIGTERM信号这是应用可以捕获并处理的信号。SpringBoot 2.3版本之前即便收到SIGTERM默认行为也是直接退出和kill -9区别不大。真正意义上的优雅停机是从SpringBoot 2.3起才正式支持。3.2 优雅停机机制的执行流程一套完整的优雅停机流程实际操作中分为下面这几个阶段接收停止信号通常来自kill命令、Docker停止容器或者Kubernetes的Pod终止流程触发SIGTERM信号。停止接收新请求Web容器如Tomcat不再接受新建连接已经建立的连接继续工作。这一步通过Tomcat的Connector内部状态切换实现外部请求在负载均衡层面会自然感知到实例下线。等待线程池处理完存量请求Tomcat的Executor线程池不会立即销毁而是给工作者线程一个宽限期让它们在超时前完成手头的任务。处理已接收但未完成的异步请求包括Servlet异步请求、WebFlux的响应式流等等待异步结果返回或超时。关闭Spring容器执行PreDestroy注解标注的方法、发布ContextClosedEvent事件、销毁Bean实例此时数据库连接池、消息队列连接、Redis连接池等资源会被依次释放。退出JVM进程所有清理工作完成后进程自动退出退出码为0。整个流程的核心在于超时时间的把控。如果超时时间设得太短请求还没处理完就被强杀优雅停机等于白配设得太长发布流程会被拖慢尤其在滚动发布场景中会导致整体发布时间拉长。需要根据业务接口的最大耗时评估一个合理值我一般用P99响应时间乘以2到3作为参考线上环境设置为30秒以内比较稳妥。3.3 优雅停机在流量调度场景中的作用业务量稍大的系统服务实例前面都有负载均衡或者注册中心。如果没有优雅停机会有这两类典型问题注册中心还没摘除实例新请求仍然打过来网关或Nginx继续往这个节点转发流量。调用方已经拿到了服务地址本地缓存了实例列表无论注册中心状态如何请求都会直接打到正在终止的实例上。优雅停机通过停止接收新连接但不中断存量连接的设计给负载均衡层留出了一个流量摘除的窗口。存量请求处理完毕后新请求也会因为连接建立失败而自动重试到其他实例。所以这个机制和注册中心、负载均衡的配合是天然互补的而不是替代关系。4. 开启优雅停机的完整配置与代码实现4.1 Spring Boot 2.3版本的YAML配置SpringBoot 2.3起内置的优雅停机配置在application.yml中只需要两行server: shutdown: graceful如果觉得默认的超时时间不够用还可以加一行spring: lifecycle: timeout-per-shutdown-phase: 30stimeout-per-shutdown-phase这个参数控制的是每个停机阶段的等待时间而不是总时长。停机过程中Tomcat关闭、Spring容器关闭、JVM退出等都属于不同阶段如果一个阶段超时了就会强制进入下一阶段。所以这个值实际上约等于应用处理存量请求加释放资源的总宽限期。如果仍在使用SpringBoot 2.3以下的版本也可以通过自定义TomcatConnectorCustomizer和Spring事件监听来模拟优雅停机大体思路是监听ContextClosedEvent先置Connector为暂停状态再等待线程池中的任务完成。不过这种方案有很多细节要处理能用新版本尽量用新版本毕竟这些都是框架底层优化过的成熟逻辑。4.2 WebFlux响应式Web场景下的配置如果项目用的是WebFlux而非传统Servlet配置会有一些区别。WebFlux基于Netty优雅停机需要额外引入spring: lifecycle: timeout-per-shutdown-phase: 30s同时在pom.xml中确保有actuator依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependencyWebFlux场景下timeout-per-shutdown-phase的语义略有差异。Netty的事件循环线程和Reactor的执行线程都需要等待响应式编程中一个请求可能被拆分到多个线程上执行超时控制比Servlet模型更复杂。实际使用中WebFlux应用对超时时间更敏感我建议设置得更保守一些起步可以用60秒试运行观察。4.3 Undertow、Jetty等容器配置要点除了默认的TomcatSpringBoot也支持切到Undertow或Jetty。这两种容器的优雅停机都是走同一套server.shutdown: graceful配置但需要注意各自的特殊性Undertow底层是XNIO连接处理模型和Tomcat不同优雅停机时对长连接的处理存在差异需要关注server.undertow相关配置是否符合业务预期。Jetty停止时默认会尝试等待请求完成但Jetty的线程池是自适应的超时控制不如Tomcat直观。因为公司内部技术栈基本都是Tomcat我建议如果没有特殊的性能或内存诉求不要在这上面折腾Tomcat经过SpringBoot官方优化适配是兼容性最稳妥的选择。4.4 数据源、消息队列等组件的收尾处理优雅停机框架层面负责了大部分工作但有些组件的清理需要业务代码配合。核心注意点在于数据源和消息队列数据源HikariCP连接池在Spring容器关闭时会自动关闭但如果在停机阶段有请求正在执行数据库操作需要确保这个操作能正常完成。如果有慢SQL要特别小心建议把数据库操作相关的超时时间如HikariCP的connectionTimeout压到合理范围。spring: datasource: hikari: connection-timeout: 3000 maximum-pool-size: 20消息队列如果应用使用了RabbitMQ或Kafka消费者线程在停机时需要确保已经拉取的消息处理完成并正确地提交offset或应答。Spring的PreDestroy可以用来做兜底Component public class MQShutdownHandler { PreDestroy public void onShutdown() { // 确保消息处理线程完成当前任务 // 等待pending消息落库 } }4.5 配置正确性的验证方法配置完成后需要实测验证而不是看一眼配置就觉得没问题。标准验证步骤放在K8s或Docker环境里启动一个服务发起一个耗时较长的请求比如写一个Thread.sleep(15000)的测试接口。执行docker stop或kubectl delete pod。观察请求是否在15秒后正常返回而不是被直接中断。确认日志中出现了Commencing graceful shutdown. Waiting for active requests to complete之类的提示。一定要在真实容器环境里验证本地IDE启动的进程信号处理方式与容器存在差异测出来的结果不能完全代表线上表现。5. 容器化与K8s部署中的优雅停机实战5.1 Dockerfile中的优雅停机配置Docker默认向容器内PID 1进程发送SIGTERM信号这一点对SpringBoot应用是友好的。但有一个常见的坑如果在Dockerfile用了java -jar直接启动Java进程是PID 1如果用了sh -c启动PID 1变成shell进程SIGTERM不会转发给Java进程结果就是容器停止时Java进程被直接终止优雅停机完全不生效。推荐的启动方式如下FROM eclipse-temurin:17-jre WORKDIR /app COPY target/app.jar /app/app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]直接用ENTRYPOINT的exec形式保证Java进程是PID 1。如果用CMD配合shell形式务必确认信号传递链路或者用tini这类init进程来转发信号。另外需要设置JVM参数推荐在启动命令中加入java -jar -Xms512m -Xmx512m -XX:UseG1GC -XX:MaxMetaspaceSize256m app.jar内存参数根据服务实际情况调整这些参数和优雅停机本身没有直接关系但合理的启动参数能保证停机时GC不会成为瓶颈。停机阶段如果发生Full GC会显著减慢请求线程的收尾速度分配太少堆内存反而拖慢优雅停机过程。5.2 Kubernetes中的preStop钩子与terminationGracePeriodSecondsK8s对Pod终止流程有明确定义Pod被标记为Terminating。preStop钩子执行如果配置了。K8s向容器主进程发送SIGTERM。等待terminationGracePeriodSeconds指定的宽限期默认30秒。超时后发送SIGKILL强制杀掉。所以K8s环境下有两层超时控制应用内部的timeout-per-shutdown-phase和K8s的terminationGracePeriodSeconds。前者必须小于后者否则应用还在优雅停机Pod就被强杀了。spec: terminationGracePeriodSeconds: 60 containers: - name: app image: app:latest lifecycle: preStop: exec: command: [sh, -c, sleep 10]为什么要加preStop的sleep 10因为Pod被标记为Terminating后Endpoints和Service的规则可能尚未更新完毕。如果有注册中心或负载均衡还在往这个Pod转发流量这10秒提供了一个缓冲窗口让流量摘除完成后再关闭应用。这个技巧是线上实践经验沉淀出来的尤其是配合Spring Cloud或自研注册中心时效果很明显。terminationGracePeriodSeconds不是越长越好。Pod的终止时间是滚动发布耗时的重要组成部分过长会拖慢发布节奏过短又可能杀活。建议使用60秒起步观察一段时间再根据实际场景调整。5.3 滚动发布场景下的优雅停机策略K8s的滚动发布RollingUpdate策略默认是先启动新Pod等就绪后再终止旧Pod。优雅停机在其中扮演的角色决定了发布过程中不会出现请求失败。但要特别注意就绪检查Readiness Probe和优雅停机是两回事。就绪检查通过后Pod会被加入Endpoints停机时Pod先移出Endpoints然后开始处理存量请求。如果两个过程衔接不好就绪探针的initialDelaySeconds配置不当会导致新Pod还没准备好就接流量或旧Pod流量还没摘完就进入停机。我习惯这样组合配置readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 15 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3failureThreshold: 3表示连续3次失败后才认为Pod不健康这个容错空间能避免偶发GC或网络抖动导致Pod被过早摘除。而initialDelaySeconds要给Spring容器启动预留充足时间启动缓慢的应用可以适当加大这个值。5.4 容器环境下的信号传递问题排查容器环境下的优雅停机失效90%以上是信号传递问题。排查时按这个顺序检查进程是否为PID 1docker exec 容器名 ps -ef看到PID 1不是Java进程优先怀疑启动方式有问题。是否被shell包裹java -jar直接执行和sh -c java -jar执行的信号行为完全不同后者会吞掉信号。日志中有无优雅停机提示SpringBoot优雅停机启动时会在控制台输出Commencing graceful shutdown...如果没有这一行说明根本没进入优雅停机流程。实际遇到过的一种情况是docker stop后应用立即退出查日志发现完全没有优雅停机记录。最后定位到是Docker Compose里用了command: bash -c ...导致Java不是PID 1调整成exec形式的ENTRYPOINT后就正常了。6. 踩坑记录与调优实践6.1 常见问题清单现象原因解决方案docker stop后请求仍被中断Java进程不是PID 1修改Dockerfile使用exec形式启动kubectl delete pod后服务报错terminationGracePeriodSeconds小于应用超时时间调大K8s宽限期同时检查preStop配置优雅停机很慢发布耗时长线程池任务长时间未完成缩小超时时间排查是否有长耗时任务停机时数据库连接未释放容器关闭顺序有问题在PreDestroy中显式关闭数据源重试请求反而造成雪崩调用方重试策略不合理调整sentinel或Feign的重试配置注意幂等性设计6.2 关闭SpringBoot内置优雅停机的时机有些场景下SpringBoot内置的优雅停机反而不适用。以下这些情况我建议不要开启优雅停机状态ful服务如集群模式的定时任务调度器如果服务维护着分布式锁或任务队列存量请求处理完可能产生重复或冲突。接口耗时极短、流量极低的内部工具优雅停机的意义有限反而增加停机时间。明确依赖外部队列消息的应用消费者拉取消息后优雅停机期间不能确认消息但外部系统可能已经超时重发产生重复消费。配置方式也很简单server: shutdown: immediate # 或不配置默认就是immediate6.3 优雅停机与定时任务的兼容性问题集群中多个实例同时运行Scheduled任务时优雅停机可能会导致一个问题其中一个实例正在执行定时任务停机流程触发后任务会被强制中断。Spring的Scheduled本身没有和优雅停机做联调所以有两种处理方式在Scheduled方法内部手动判断停机状态发现正在停机就退出执行。使用TaskScheduler的preShutdown机制在停机前先暂停调度器。比较推荐的是通过Spring事件感知停机状态。代码示例Component public class ShutdownFlag { private volatile boolean shuttingDown false; EventListener(ContextClosedEvent.class) public void onShutdown() { this.shuttingDown true; } public boolean isShuttingDown() { return shuttingDown; } }然后在定时任务中加上判断Scheduled(fixedDelay 5000) public void runTask() { if (shutdownFlag.isShuttingDown()) { return; } // 执行业务逻辑 }在定时任务场景下还有一个需要留意的细节停机过程中Tomcat线程池已经停止接收新请求但Scheduled的线程池是独立的不受影响。所以如果不做上述判断定时任务可能还会在优雅停机期间继续触发等到Spring容器关闭时才被强杀任务的状态和结果可能处于未知状态。加上标志位判断是最简单的兜底方案。6.4 响应超时时间与优雅停机超时时间的关系假设业务接口的最长响应时间是30秒优雅停机的超时时间至少要大于30秒。但这里还有一个隐藏问题网关或调用方的超时时间可能比30秒更短。比如下游服务最长处理30秒但上游网关的超时时间是10秒。那么即使优雅停机成功让请求处理完了调用方在10秒时已经断开连接前端拿到的是请求失败的结果。这个请求注定是失败的优雅停机只是让它多占用了一段时间的线程资源。所以在设计超时时间时需要考虑一条链路客户端超时时间 网关超时时间 服务端接口P99响应时间 优雅停机超时时间 K8s宽限期如果发现优雅停机过程中大量请求在停机阶段被客户端超时打断需要先优化的是接口响应速度而不是盲目调大停机超时时间。6.5 性能调优大流量场景下的停机优化大流量场景下优雅停机有一个比较棘手的点存量请求太多宽限期内处理不完。这个问题的本质是线程池设置和优雅停机超时时间不匹配。假设服务QPS是2000平均响应时间500ms那么在任意一个时刻正在处理的请求数量大约是2000 * 0.5 1000个。如果把Tomcat的max-threads设为200那还有800个请求在等待队列里。停机时这些等待中的请求也会被处理但它们并不会被Tomcat的控制机制保护。调优思路有两个方向调大max-threads让更多请求并发处理减少等待队列长度但会增加机器资源消耗。调低停机超时时间如果服务间调用链设计合理直接让排队中的请求超时失败由调用方重试到其他实例。实际操作中我更喜欢在负载均衡层配合处理。比如Nginx设置了max_fails和fail_timeoutK8s滚动发布配合maxSurge: 1、maxUnavailable: 0的策略服务实例逐个替换而不是同时替换一大批存量请求的压力就大大降低了。6.6 测试技巧如何模拟优雅停机场景本地开发时也可以模拟优雅停机# 获取进程PID jps -l # 发送SIGTERM信号 kill -15 PID更好的做法是在测试环境引入Spring Boot Actuator的/actuator/shutdown端点但注意这个端点默认是关闭的需要显式配置management: endpoint: shutdown: enabled: true endpoints: web: exposure: include: health,info,shutdown生产环境务必关掉这个端点或者加上严格的权限控制。它只适合测试阶段模拟停机线上还是要靠真实的K8s或Docker信号流程来验证。7. 基于个人经验的功能扩展与细节补充最后再分享一个细节。SpringBoot 2.3的优雅停机配置刚推出来的时候很多团队直接改配置就上线结果发现部分长连接服务如WebSocket、SSE、gRPC在优雅停机时表现异常。长连接本身没有明确的请求完成概念服务端停止接收新连接存量连接却可能一直挂着最终等到超时被强制断开。如果业务涉及WebSocket或SSE建议在停机时主动向前端推送一个服务即将下线的消息让客户端主动重连到其他实例。这个逻辑可以挂在PreDestroy或ContextClosedEvent监听器里。另外spring.lifecycle.timeout-per-shutdown-phase这个配置的语义在不同版本中可能会有细微差别升级SpringBoot版本后建议回归验证一遍优雅停机流程。Spring Boot 3.x对Jakarta EE的迁移也影响了部分容器内部的实现细节但实际验证下来3.x的优雅停机比2.x更稳定尤其对于虚拟线程Virtual Threads项目来说优雅停机期间虚拟线程的回收机制比平台线程更可控。如果你的项目已经升级到Spring Boot 3.2并且启用了虚拟线程建议在停机时特别关注任务调度的收尾行为。根据我个人经历优雅停机上线后最明显的变化就是发版不再需要等到凌晨。以往每次发版都要掐着流量低峰、摘流量、等请求跑完、再杀进程整套流程最少半小时。现在直接走流水线所有请求安全交接完毕日志里也没有惊心动魄的报错信息了。这就是优雅停机的价值所在。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

钉钉群消息自动转发怎么搞? 2026/10/2 12:42:52

钉钉群消息自动转发怎么搞?

做企业运营的朋友,经常碰到这种需求:一个钉钉群里的消息,自动同步到另外一个群里。比如项目群消息同步到通知群、跨部门信息传递、运营群消息汇总——这种活儿纯靠手动复制粘贴实在太累,自己跑一遍就懂了。今天就把这事一次讲透。…

阅读更多 →
网址导航系统源码评测:个性 UI 与轻量化架构实战 2026/10/2 12:42:52

网址导航系统源码评测:个性 UI 与轻量化架构实战

在接手一个全新的 Web 项目时,开发者往往面临两难选择:是选用功能庞大但臃肿的商业框架,还是寻找轻量灵活却可能文档缺失的开源源码?很多时候,我们需要的不是一个“大而全”的解决方案,而是一个能够快速上手…

阅读更多 →
玄铁语言与 Python/Go/Rust 对比:中文关键字、静态类型与自举定位 2026/10/2 12:42:51

玄铁语言与 Python/Go/Rust 对比:中文关键字、静态类型与自举定位

对比定位 本文把玄铁(XuanTie-Lang)与三门主流语言(Python、Go、Rust)按关键维度对比,供选型与学习参考。玄铁是 2026 年公开的新语言(当前版本 v1.0-rc.2),中文关键字、编译到原生…

阅读更多 →
2027国考省考资料合集 2026/10/2 12:42:51

2027国考省考资料合集

老用户要把资源转存到自己网盘,不然只有2分钟观看。没有会员的话一次少转存几个文件,分多次转存即可! 链接:https://pan.quark.cn/s/ea30023b30c3 链接里包含下面这些课程资源~ 行测申论 语言理解 数量资料 判断推理 图形推理 政治理论等

阅读更多 →
Agent长期记忆六大方案对比,彻底解决AI失忆问题 2026/10/2 12:42:50

Agent长期记忆六大方案对比,彻底解决AI失忆问题

文章目录前言一、先搞清楚:Agent 为什么会"失忆"1. 不是智商问题,是没地方记2. 上下文窗口再大,也是临时工3. 所以真正要解决的是四个问题二、四条路线,四个门派三、Mem0:给老应用装个外挂硬盘1. 它到底是啥…

阅读更多 →
Agent Skills实战指南:从安装到自建,让AI编程工具按标准流程干活 2026/10/2 12:42:43

Agent Skills实战指南:从安装到自建,让AI编程工具按标准流程干活

最近一段时间,各个技术社群里讨论密度最高的话题,已经从“哪个模型更强”变成了“skills”。这个词在Claude Code、Codex、OpenCode这类AI编程工具的用户圈里火得不行,前端开发skills、superpower skills、数学建模skills、AI漫剧skills&…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉