springboot优雅关机方案分享:逻辑实现

23 阅读9分钟

前言

前两次分享,我们已经介绍过了k8s节点关机的流程和优雅关机要实现的流程,今天我们来一起来看下具体的代码实现,主要内容如下:

  • SIGTERM监听逻辑
  • 预关机逻辑
  • 各个组件的关机逻辑和监控逻辑

实现过程

前置要点

前面我们说了,本项目实际是个spring-boot-starter,所以你要先创建spring-boot-starter的项目,这里就不赘述具体过程了,具体可以参考之前的分享: [[编写你的第一个spring-boot-starter]]

这里提几个需要注意的点:

pom依赖

这块有两个标签要注意,scopeoptional,组合使用,可以确保依赖的灵活性,同时避免依赖冲突:

scope(作用域)

依赖的作用范围,控制依赖的使用范围,这里的provided表示编译和测试时有效,运行时由运行环境提供,也就是不会打进包里。这样可以确保我们的starter引入项目后不会影响原项目的pom依赖关系,带来未知风险。常见的作用域还有:

  • compile(默认):编译、测试、运行都有效,会打包
  • provided:编译和测试时有效,运行时由容器提供
  • runtime:运行时需要,编译时不需要
  • test:仅测试时使用
  • system:类似provided,但需要显式指定本地jar路径

optional(可选依赖)

当设置为 true 时:

  • 不会传递依赖:即使其他项目依赖本项目,也不会继承这个可选依赖
  • 需要显式声明:使用者必须主动声明才能使用
  • 场景:可选功能、有冲突的依赖、特定环境才需要的依赖

当然,这两个pom标签通常是配合spring-boot的条件配置来使用,或者你可以确保代码运行时一定包含对应的依赖,否则会导致运行时异常。

spring.factories

这个文件是starter的核心,当我们的项目被引入时,springboot会根据我们的spring.factories初始化我们的starter,并根据我们的配置类,完成加载和配置。这个项目通常位于src/main/resources/META-INF/spring.factories下,配置内容如下:

# src/main/resources/META-INF/spring.factories  
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  
io.syske.springboot.starter.config.GracefulShutdownAutoConfiguration,\  
io.syske.springboot.starter.compent.SpringContextUtilConfig

配置的内容就是我们的配置类,有多个配置类用逗号分隔开就行。因为配置类本身是支持import的,所以我们通常只需要配置一个主配置类即可,其他的直接@Import即可:

我这里配置两个是由于另一个类不能条件注入,所以单独配置了,具体大家根据自己实际情况看。

条件配置

这里简单说下本次用到的几个条件注解:

  • @ConditionalOnProperty:这个注解的作用是根据某个配置文件判断,类或者方法支付要执行,通常和@Configuration或者@Bean组合使用。在我们上面的截图中,实际就是通过判断优雅关机开关是否开启进行条件配置,如果条件缺失,我们给了默认值true
  • @ConditionalOnMissingBean:当缺少某个类的bean时配置,类似单例模式
  • @ConditionalOnClass:当有指定的class的时候触发配置。这个就是我前面说的要和pom那两个标签组合的条件配置。所有的关机组件逻辑都要加上这个判断,确保核心类存在(也就是依赖存在)时,才配置对应组件的关机逻辑。
  • @ConditionalOnWebApplication:这个注解是加在Tomcat的关机组件上的,确保它是WebApplication

SIGTERM逻辑

核心实现其实就是创建一个bean,并在bean初始化逻辑中定义一个处理TERM信号的Signal,并在Signalhandle逻辑中加入我们的预关机逻辑即可:

try {  
    Signal signal = new Signal("TERM");  
    Signal.handle(signal, sig -> {  
        logger.info("Received SIGTERM signal from K8s, starting graceful shutdown");  
        // 处理关机信号
        handleShutdownSignal();  
    });  
    logger.info("SIGTERM handler registered successfully");  
  
} catch (Exception e) {  
    logger.warn("Failed to register SIGTERM handler: {}, using shutdown hook as fallback", e.getMessage());  
    setupShutdownHook();  
}

这里我就直接放截图了,处理关机信号逻辑:

然后就是预关机逻辑:

这里简单说下:

  • 服务标记为不健康:这里我自定义了一个healthIndicator,实际没太大作用,因为我们的服务健康检查并不依赖这个,如果你们的服务健康检查依赖的是springboot的健康检查机制,那实现自定义健康检查则是需要考虑的。
  • 等待负载均衡感知:这个实际和网关有关系,在本项目中非必须,逻辑中只是睡了5秒。
  • 执行优雅关机:这里就是根据我们自定义的优先级,依次关机各个组件。

下面,我们逐步介绍各组件的实现逻辑。

Tomcat

监控逻辑

tomcat优雅关机要实现TomcatConnectorCustomizer接口,并实现customize方法,主要是为了关机时能获取到连接器,同时设置Executor实现统计活跃请求数的逻辑:

计数执行器逻辑:

组件核心逻辑

核心逻辑就三步:

// 1. 暂停接收新请求  
pauseConnector();  
  
// 2. 等待活跃请求完成  
waitForActiveRequests();  
  
// 3. 关闭连接器  
stopConnector();

停止接受新请求实际就是调用连接器的暂停方法:

等待活跃请求完成,实际就是等待线程池关闭:

关闭线程池实际就是调用shutdown方法,超时强制关闭实际就是超时后调用shutdownNow,然后再等待:

线程池考虑了org.apache.tomcat.util.threads.ThreadPoolExecutorjava.util.concurrent.ThreadPoolExecutor,是分别处理,实际两者是继承关系,其实可以省略第一个:

停止连接器也很简单,就是调用stop方法,不过关闭前需要判断下,避免重复关闭(springboot本身的关机逻辑也会触发关闭):

RPC提供者

监控逻辑

监控这里的逻辑是通过sofa-rpcFilter来实现的:

  • 统计请求数量
  • 服务注册:这里的注册实际是个辅助逻辑,确保没有正常注册的提供者在调用过程中注册。真正的服务提供中注册逻辑是基于ApplicationRunner实现的

关于拦截器的注册有几个点:

  • 拦截器只能通过SPI方式注入,且要正确配置@AutoActive@Extension,需要注意的是@Extension配置的名称要与com.alipay.sofa.rpc.filter.filter文件中配置的名称一致,否则不生效

  • com.alipay.sofa.rpc.filter.filter文件必须正确配置:配置的key@Extension配置的值,value是拦截器的全类名:

注意:这里还有个比较坑的点,sofa-boot的拦截器不能直接使用springbootbean,所以需要借助SpringContextUtil,这也是我的spring.factories里面还配置了SpringContextUtilConfig的原因。因为拦截器没法实现条件配置,所以SpringContextUtil也不能条件配置。

目标发现逻辑

Runer核心逻辑如下:

这里搞了好久一直没有搞定,最后是在sofa-boot的代码中找到的,然后直接解决了提供者发现问题:

组件核心逻辑

组件核心逻辑就三步:

  1. 标记为关闭状态:实际这一步并没有拦截请求
  2. 取消服务者注册:这一步直接调用unExport方法

  1. 等待处理中的请求完成:判断活跃请求的逻辑来自监控逻辑

ActiveMQ

监控逻辑

activeMQ的监控逻辑实际是通过DefaultMessageListenerContainer直接获取的,所以监控逻辑本身没什么特殊的:

目标发现逻辑

activeMQ的服务发现逻辑也比较简单,核心其实就是将JmsListenerEndpointRegistry注入我们的关机组件,关机组件要通过这个对象获取所有的DefaultMessageListenerContainer消费者容器:

组件核心逻辑

核心逻辑如下:

  1. 发现所有监听者,并打印容器状态
  2. 停止容器:调用stop方法

  1. 等待处理中的消息处理完成:

RocketMQ

监控逻辑

监控逻辑是根据容器是否是running状态判断的,比较简单:

目标发现逻辑

组件的发现逻辑是通过spring的后置处理器来完成的,将所有的DefaultRocketMQListenerContainer收集起来。

组件核心逻辑

核心逻辑如下:

  1. 停止监听器逻辑:这里是调用stop方法进行停止

  1. 等待处理中的容器停止:这里直接用的是容器状态判断的,因为消费中的消息数量,需要调用rpcketMQ的服务端接口,实际意义不大,而且要依赖外部服务,所以直接判断容器状态。

线程池

监控逻辑

没什么监控逻辑,实际就是直接打印线程池的活跃的线程数:

目标发现逻辑

发现逻辑也是基于spring的后置处理器完成的,根据不通的线程池注册到不通的集合中:

组件核心逻辑

核心逻辑:不同的线程池挨个关闭

关闭逻辑都差不多:

  1. 线程池关机:调用shutdown方法
  2. 等待完成:调用awaitTermination,超时后调用shutdownNow

ForkJoinPool比较特殊,只能等待:

RPC客户端

客户端也叫运行时环境,这个销毁就比较简单了,没有监控,直接通过反射调用RpcRuntimeContextdestroy方法即可。

至此,我们的k8s环境下的spring-boot服务优雅关机方案就完成了。下面我们简单总结下:

总结

我们用了三篇文章,和大家聊清楚了 K8s 环境下 Spring Boot 服务“优雅关机”这回事儿:

第一篇:先弄懂 K8s 是怎么关机的
带大家梳理了 K8s 节点和 Pod 关闭的基本流程。了解了它的“套路”,咱们自己设计方案时才能心里有底。

第二篇:咱们的方案长啥样?
知道了 K8s 的流程,那我们的 Spring Boot 服务该怎么配合着“优雅退场”呢?这一篇就讲了咱们的整体设计思路,关机要分几步、每一步要注意啥,都给大家掰扯明白了。

第三篇:动手!把代码写出来
光说不练假把式。最后一篇咱们直接上代码,手把手讲解了怎么监控组件、怎么发现需要关闭的目标,再把整个关闭流程像拼积木一样组合起来。让大家能从代码层面真正搞懂怎么写。

当然了,咱们现在这个方案还不是“终极完美版”。除了之前提到的(比如 Tomcat 和 RPC提供者可以同时关)这些优化点,其实还有一些地方可以做得更好。

比如说  “重复关闭”的问题:虽然代码里加了判断,不会因为重复调用而报错,但 Spring Boot 底层的关机钩子 (ShutdownHook) 逻辑其实还在,这里未来还有更优雅的解法。

我们特意把这个点提出来,其实就是想“抛砖引玉”。优雅关机这件事,细节很多,也欢迎大家一起来思考、讨论,看看还有哪些地方可以优化得更好。