Spring Boot 的优雅启停:确保停机不影响交易

459 阅读6分钟

Spring Boot 的优雅启停:确保停机不影响交易

在实际生产环境中,项目上线、发版或版本升级时,服务直接关停可能导致正在执行的交易失败,尤其是在分布式事务和异步调用场景下。许多人对“优雅启停”的理解不够深入,导致问题频发。本文将详细介绍如何实现 Spring Boot 的优雅启停,确保停机时已发起的请求能够顺利完成,同时新请求自动切换到其他可用节点,从而保证业务连续性。

背景

微服务架构中,分布式事务与异步调用非常普遍。例如,审批流程完成后异步调用支付模块扣款(失败会重试)。如果服务急停,可能导致支付模块因服务关闭、线程中断或队列积压而扣款失败。如果缺乏有效的异常处理策略,用户体验会受到严重影响。

问题分析

本质上,微服务架构下的分布式事务容易因远程调用失败而受影响。这种情况只能采用相关策略保证最终一致性。

有观点认为 MQ 能够解决该问题,但实际上,单纯 MQ 解决不了分布式事务问题,还是要解决幂等、超时失效等问题以及 MQ 的消息必达等问题。必须配合优雅启停、幂等设计、补偿机制等组合策略才能实现可靠交易。

当前方案

目前,我们的方案是在发版时先将流量引导至其他可用中心,但这一流程涉及流量控制、会话保持等问题,容易引入新的风险。本文旨在介绍如何实现“优雅启停”,确保停机时已发起请求能顺利完成,新请求切换到其他可用节点。

1. 解决方案概述

  1. 多中心并行发版:各中心并行升级,确保始终有可用实例。
  2. 服务下线前的流量切换:使用服务注册中心(如 Eureka)将待下线实例置为“DOWN”。
  3. 进程关闭信号选择:使用 kill -15(SIGTERM)触发优雅关闭,而非 kill -9(SIGKILL)。
  4. 线程池的优雅停止:配置线程池等待任务完成,避免强制中断。
  5. Spring Boot 2.3 内置优雅关机:启用 server.shutdown=graceful,设置超时时间。
  6. 启动时的优雅准备:检测依赖、初始化资源,确保服务就绪后再接受流量。

2. 多中心并行发版

  • 方案说明:多中心架构下,各中心并行发版,中心内顺序升级,确保任何时刻每个中心至少一台机器运行,避免单中心全部下线。

3. 服务下线前的流量切换

  • 实现方式:发版前,通过服务注册中心(如 Eureka)将待下线实例状态置为“DOWN”,使流量自动切换。例如:

    curl -X POST "http://localhost:8080/actuator/service-registry?status=DOWN" -H "Content-Type: application/json"
    
    • 检测方法:
      • 请求 Eureka API,检查待下线实例是否已从注册列表中移除。
      • 通过负载均衡器或网关的监控界面,确认流量已不再路由到该实例。
  • 注意事项:

    • 此方法模拟流量切换,先下线再等待交易完成。
    • Actuator 接口存在安全风险,生产环境应限制访问或配置认证。
  • 优化:确保线程池中任务执行完成后再做启停。增加对线程池资源的监控(检查方法同下边启动时检查)

    • 手动将 ThreadPoolExecutor 注册到 Micrometer 以便在 /actuator/metrics 获取线程池信息

      @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 自动注册线程池指标:

      @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 关闭钩子,执行资源释放、线程池关闭、完成未结束请求等清理工作,降低任务中断风险。
  • 如果kill -15后发现进程一直在,可通过jstack排查

5. 线程池的优雅停止

  • ThreadPoolTaskExecutor:配置 waitForTaskToCompleteOnShutdown=true,设置 awaitTerminationSeconds,确保等待任务完成。
  • ExecutorService:使用 shutdown() 发起平滑关闭,调用 awaitTermination() 等待任务结束,而非 shutdownNow()
  • 非 Spring 管理的 Bean:监听 ApplicationListener 事件,在 ContextClosedEvent 中统一处理资源关闭。
示例代码
//场景 1:注册线程池为 Spring Bean
@Configuration
public class ThreadPoolConfig {

    @Bean(destroyMethod = "shutdown")  // 关键注解
    public ExecutorService myPool() {
        return Executors.newFixedThreadPool(4);
    }
}

//场景 2:手动注册关闭钩子
@Bean
public ApplicationListener<ContextClosedEvent> contextClosedEventListener(){
  return new ApplicationListener<ContextClosedEvent>(){

    private ExecutorService executorService;
    
    @Override
    public void onApplicationEvent(ContextClosedEvent contextClosedEvent){
      executorService.shutdown();
      try {
          if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {
              executorService.shutdownNow();
          }
      } catch (Exception e) {
          log.warn("线程池停止异常", e);
      }
    }
  }
}

6. Spring Boot 2.3 内置优雅关机

  • 配置:

    server.shutdown=graceful
    spring.lifecycle.timeout-per-shutdown-phase=30s
    
  • 作用:嵌入式服务器停止接收新请求,允许已建立连接完成处理,降低服务中断风险。

7. 启动时的优雅准备

  • 检测方法:

    • 请求应用的 /actuator/health 或自定义健康检查端点,确认所有依赖(数据库、缓存、远程服务)均正常。
    • 请求应用的 /actuator/info 或自定义信息端点,确认所有必要的初始化操作已完成。
    • 请求 Eureka API,检查自身实例是否已成功注册,且状态为 UP。
  • 确保:

    • 各项依赖已正常建立连接。
    • 所有初始化操作已完成。
    • 注册成功后才处理外部请求,避免流量打到未就绪实例。
  • 检测脚本: 检测服务在注册中心状态及检测health(如检测所有组件需配置health显示所有组件状态)的主要脚本如下,同样你可以配置在actuator的info信息后检测部署的git分支号是否和预期一致:

    关键脚本
    EUREKA_URL="http://your-eureka-server:8761/eureka/instances/实例ID"
    SERVICE_NAME=USER-SERVICE
    IP="192.168.1.100"
    PORT="8080"
    
    eureka_response=$(curl -s $EUREKA_URL)
    echo 实例状态${eureka_response}
    status=$(echo "$eureka_response" | perl -nle 'print $1 if /<status>(UP|DOWN)<\/status>/i')
    
    # 判断并输出结果
    case "$status" in
      "UP")
          echo "实例状态正常 (UP)"
          exit 0
          ;;
      "DOWN")
          echo "实例状态异常 (DOWN)"
          exit 1
          ;;
      *)
          echo "无法获取实例状态"
          exit 3
          ;;
    esac
    

    检测health状态

    health=$(echo $(curl -s http://$IP:$PORT/actuator/health/) | grep DOWN)
    if [ X"$health" = "X" ]; then
      echo "无 DOWN 组件,服务正常"
      exit 0
    else
      exit 1
    fi
    

优雅启停理解的澄清

很多人认为,只要使用了 Spring Boot 2.3 的优雅关机功能,或者配置了线程池的优雅停止,就能实现优雅启停。但实际上,优雅启停是一个综合性的概念,涉及多个方面的配合。

  • 流量切换:即使应用内部能优雅停止,如果流量没有切换,新的请求仍然会打到即将下线的实例上,导致失败。
  • 依赖项的关闭: 优雅的关闭,需要保证依赖项的正常关闭,例如数据库连接,消息队列连接等。
  • 启动时的就绪状态:同样,启动时也需要确保所有依赖项都已就绪,才能开始处理请求。

总结

优雅启停是系统稳定运行的关键。通过多中心并行发版、流量切换、优雅关闭信号、线程池管理、内置支持和启动准备等措施,可有效降低服务中断风险,确保停机不影响交易。