Spring Boot 的优雅启停:确保停机不影响交易
在实际生产环境中,项目上线、发版或版本升级时,服务直接关停可能导致正在执行的交易失败,尤其是在分布式事务和异步调用场景下。许多人对"优雅启停"的理解不够深入,导致问题频发。本文将详细介绍如何实现 Spring Boot 的优雅启停,确保停机时已发起的请求能够顺利完成,同时新请求自动切换到其他可用节点,从而保证业务连续性。
背景
微服务架构中,分布式事务与异步调用非常普遍。例如,审批流程完成后异步调用支付模块扣款(失败会重试)。如果服务急停,可能导致支付模块因服务关闭、线程中断或队列积压而扣款失败。如果缺乏有效的异常处理策略,用户体验会受到严重影响。
问题分析
本质上,微服务架构下的分布式事务容易因远程调用失败而受影响。这种情况只能采用相关策略保证最终一致性。
有观点认为 MQ 能够解决该问题,但实际上,单纯 MQ 解决不了分布式事务问题,还是要解决幂等、超时失效等问题以及 MQ 的消息必达等问题。必须配合优雅启停、幂等设计、补偿机制等组合策略才能实现可靠交易。
当前方案
目前,我们的方案是在发版时先将流量引导至其他可用中心,但这一流程涉及流量控制、会话保持等问题,容易引入新的风险。本文旨在介绍如何实现"优雅启停",确保停机时已发起请求能顺利完成,新请求切换到其他可用节点。
1. 解决方案概述
- 多中心并行发版:各中心并行升级,确保始终有可用实例。
- 服务下线前的流量切换:使用服务注册中心(如 Eureka)将待下线实例置为"DOWN"。
- 进程关闭信号选择:使用
kill -15
(SIGTERM)触发优雅关闭,而非kill -9
(SIGKILL)。 - 线程池的优雅停止:配置线程池等待任务完成,避免强制中断。
- Spring Boot 2.3 内置优雅关机:启用
server.shutdown=graceful
,设置超时时间。 - 启动时的优雅准备:检测依赖、初始化资源,确保服务就绪后再接受流量。
2. 多中心并行发版
- 方案说明:多中心架构下,各中心并行发版,中心内顺序升级,确保任何时刻每个中心至少一台机器运行,避免单中心全部下线。
3. 服务下线前的流量切换
-
实现方式:发版前,通过服务注册中心(如 Eureka)将待下线实例状态置为"DOWN",使流量自动切换。例如:
bashcurl -X POST "http://localhost:8080/actuator/service-registry?status=DOWN" -H "Content-Type: application/json"
- 检测方法:
- 请求 Eureka API,检查待下线实例是否已从注册列表中移除。
- 通过负载均衡器或网关的监控界面,确认流量已不再路由到该实例。
- 检测方法:
-
注意事项:
- 此方法模拟流量切换,先下线再等待交易完成。
- Actuator 接口存在安全风险,生产环境应限制访问或配置认证。
-
优化:确保线程池中任务执行完成后再做启停。增加对线程池资源的监控(检查方法同下边启动时检查)
- 手动将
ThreadPoolExecutor
注册到 Micrometer 以便在/actuator/metrics
获取线程池信息
java@Bean public ThreadPoolExecutor threadPoolExecutor(ThreadPoolTaskExecutor executor, MeterRegistry registry) { ThreadPoolExecutor threadPoolExecutor = executor.getThreadPoolExecutor(); registry.gauge("executor.pool.size", threadPoolExecutor, ThreadPoolExecutor::getPoolSize); registry.gauge("executor.active.count", threadPoolExecutor, ThreadPoolExecutor::getActiveCount); registry.gauge("executor.queue.size", threadPoolExecutor, e -> e.getQueue().size()); registry.gauge("executor.completed.tasks", threadPoolExecutor, ThreadPoolExecutor::getCompletedTaskCount); return threadPoolExecutor; }
-
如果你使用的是 Spring Boot 3.x,Micrometer 3 以及 Actuator 自带
ExecutorService
监控功能,你可以直接用ExecutorServiceMetrics
自动注册线程池指标:java@Bean public ExecutorService monitoredExecutor(ThreadPoolTaskExecutor executor, io.micrometer.core.instrument.MeterRegistry registry) { ThreadPoolExecutor threadPoolExecutor = executor.getThreadPoolExecutor(); return ExecutorServiceMetrics.monitor(registry, threadPoolExecutor, "custom-executor", "task"); }
- 手动将
4. 进程关闭信号的选择
- 推荐做法:使用
kill -15
(SIGTERM)通知应用平滑关闭,而非kill -9
(SIGKILL)。 - 优势:SIGTERM 触发 Spring Boot 关闭钩子,执行资源释放、线程池关闭、完成未结束请求等清理工作,降低任务中断风险。
5. 线程池的优雅停止
- ThreadPoolTaskExecutor:配置
waitForTaskToCompleteOnShutdown=true
,设置awaitTerminationSeconds
,确保等待任务完成。 - ExecutorService:使用
shutdown()
发起平滑关闭,调用awaitTermination()
等待任务结束,而非shutdownNow()
。 - 非 Spring 管理的 Bean:监听
ApplicationListener
事件,在ContextClosedEvent
中统一处理资源关闭。
6. Spring Boot 2.3 内置优雅关机
-
配置:
propertiesserver.shutdown=graceful spring.lifecycle.timeout-per-shutdown-phase=30s
-
作用:嵌入式服务器停止接收新请求,允许已建立连接完成处理,降低服务中断风险。
7. 启动时的优雅准备
-
检测方法:
- 请求应用的
/actuator/health
或自定义健康检查端点,确认所有依赖(数据库、缓存、远程服务)均正常。 - 请求应用的
/actuator/info
或自定义信息端点,确认所有必要的初始化操作已完成。 - 请求 Eureka API,检查自身实例是否已成功注册,且状态为 UP。
- 请求应用的
-
确保:
- 各项依赖已正常建立连接。
- 所有初始化操作已完成。
- 注册成功后才处理外部请求,避免流量打到未就绪实例。
优雅启停理解的澄清
很多人认为,只要使用了 Spring Boot 2.3 的优雅关机功能,或者配置了线程池的优雅停止,就能实现优雅启停。但实际上,优雅启停是一个综合性的概念,涉及多个方面的配合。
- 流量切换:即使应用内部能优雅停止,如果流量没有切换,新的请求仍然会打到即将下线的实例上,导致失败。
- 依赖项的关闭: 优雅的关闭,需要保证依赖项的正常关闭,例如数据库连接,消息队列连接等。
- 启动时的就绪状态:同样,启动时也需要确保所有依赖项都已就绪,才能开始处理请求。
总结
优雅启停是系统稳定运行的关键。通过多中心并行发版、流量切换、优雅关闭信号、线程池管理、内置支持和启动准备等措施,可有效降低服务中断风险,确保停机不影响交易。