如何在 Spring Boot 优雅关闭加入一些自定义机制(上)

本文涉及的产品
网络型负载均衡 NLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
传统型负载均衡 CLB,每月750个小时 15LCU
简介: 如何在 Spring Boot 优雅关闭加入一些自定义机制(上)
个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 issue,谢谢支持~

我们知道从 Spring Boot 2.3.x 这个版本开始,引入了优雅关闭的机制。我们也在线上部署了这个机制,来增加用户体验。虽然现在大家基本上都通过最终一致性,以及事务等机制,来保证了就算非优雅关闭,也可以保持业务正确。但是,这样总会带来短时间的数据不一致,影响用户体验。所以,引入优雅关闭,保证当前请求处理完,再开始 Destroy 所有 ApplicationContext 中的 Bean。


优雅关闭存在的问题


ApplicationContext 的关闭过程简单来说分为以下几个步骤(对应源码 AbstractApplicationContext 的 doClose 方法):

  1. 取消当前 ApplicationContext 在 LivBeanView 的注册(目前其实只包含从 JMX 上取消注册)
  2. 发布 ContextClosedEvent 事件,同步处理所有这个事件的 Listener
  3. 处理所有实现 Lifecycle 接口的 Bean,解析他们的关闭顺序,并调用他们的 stop 方法
  4. Destroy 所有 ApplicationContext 中的 Bean
  5. 关闭 BeanFactory

简单理解优雅关闭,其实就是在上面的第三步中加入优雅关闭的逻辑实现的 Lifecycle,包括如下两步:

  1. 切断外部流量入口:具体点说就是让 Spring Boot 的 Web 容器直接拒绝所有新收到的请求,不再处理新请求,例如直接返回 503.
  2. 等待承载的 Dispatcher 的线程池处理完所有请求:对于同步的 Servlet 进程其实就是处理 Servlet 请求的线程池,对于异步响应式的 WebFlux 进程其实就是所有 Web 请求的 Reactor 线程池处理完当前所有 Publisher 发布的事件。


首先,切断外部流量入口保证不再有新的请求到来,线程池处理完所有请求之后,正常的业务逻辑也是正常走完的,在这之后就可以开始关闭其他各种元素了。


但是,我们首先要保证,优雅关闭的逻辑,需要在所有的 Lifecycle 的第一个最保险。这样保证一定所有请求处理完,才会开始 stop 其他的 Lifecycle。如果不这样会有啥问题呢?举个例子,例如某个 Lifecycle 是负载均衡器的,stop 方法会关闭负载均衡器,如果这个 Lifecycle 在优雅关闭的 Lifecycle 的 stop 之前进行 stop,那么可能会造成某些在 负载均衡器 stop 后还没处理完的请求,并且这些请求需要使用负载均衡器调用其他微服务,执行失败。


优雅关闭还有另一个问题就是,默认的优雅关闭功能不是那么全面,有时候我们需要在此基础上,添加更多的关闭逻辑。例如,你的项目中不止 有 web 容器处理请求的线程池,你自己还使用了其他线程池,并且线程池可能还比较复杂,一个向另一个提交,互相提交,各种提交等等,我们需要在 web 容器处理请求的线程池处理完所有请求后,再等待这些线程池的执行完所有请求后再关闭。还有一个例子就是针对 MQ 消费者的,当优雅关闭时,其实应该停止消费新的消息,等待当前所有消息处理完。这些问题可以看下图:


微信图片_20220625194937.jpg


源码分析接入点 - Spring Boot + Undertow & 同步 Servlet 环境

我们从源码触发,分析在 Spring Boot 中使用 Undertow 作为 Web 容器并且是同步 Servlet 环境下,如果接入自定义的机制。首先,在引入 spring boot 相关依赖并且配置好优雅关闭之后:

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <!--不使用默认的 tomcat 容器-->
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--使用 undertow 容器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

application.yml

server:
  # 设置关闭方式为优雅关闭
  shutdown: graceful
management:
  endpoint:
    health:
      show-details: always
    # actuator 暴露 /actuator/shutdown 接口用于关闭(由于这里开启了优雅关闭所以其实是优雅关闭)
    shutdown:
      enabled: true
  endpoints:
    jmx:
      exposure:
        exclude: '*'
    web:
      exposure:
        include: '*'

在设置关闭方式为优雅关闭之后,Spring Boot 启动时,在创建基于 Undertow 实现的 WebServer 的时候,会添加优雅关闭的 Handler,参考源码:

UndertowWebServerFactoryDelegate

static List<HttpHandlerFactory> createHttpHandlerFactories(Compression compression, boolean useForwardHeaders,
      String serverHeader, Shutdown shutdown, HttpHandlerFactory... initialHttpHandlerFactories) {
  List<HttpHandlerFactory> factories = new ArrayList<>(Arrays.asList(initialHttpHandlerFactories));
  if (compression != null && compression.getEnabled()) {
    factories.add(new CompressionHttpHandlerFactory(compression));
  }
  if (useForwardHeaders) {
    factories.add(Handlers::proxyPeerAddress);
  }
  if (StringUtils.hasText(serverHeader)) {
    factories.add((next) -> Handlers.header(next, "Server", serverHeader));
  }
  //如果指定了优雅关闭,则添加 gracefulShutdown
  if (shutdown == Shutdown.GRACEFUL) {
    factories.add(Handlers::gracefulShutdown);
  }
  return factories;
}

添加的这个 Handler 就是 Undertow 的 GracefulShutdownHandlerGracefulShutdownHandler 是一个 HttpHandler,这个接口很简单:

public interface HttpHandler {
    void handleRequest(HttpServerExchange exchange) throws Exception;
}

其实就是对于收到的每个 HTTP 请求,都会经过每个 HttpHandler 的 handleRequest 方法。GracefulShutdownHandler 的实现思路也很简单,既然每个请求都会经过这个类的 handleRequest 方法,那么我就在收到请求的时候将一个原子计数器原子 + 1,请求处理完后(注意是返回响应之后,不是方法返回,因为请求可能是异步的,所以这个做成了回调),将原子计数器原子 - 1,如果这个计数器为零,就证明没有任何正在处理的请求了。源码是:

GracefulShutdownHandler:

@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
    //原子更新,请求计数器加一,返回的 snapshot 是包含是否关闭状态位的数字
    long snapshot = stateUpdater.updateAndGet(this, incrementActive);
    //通过状态位判断是否正在关闭
    if (isShutdown(snapshot)) {
        //如果正在关闭,直接请求数原子减一
        decrementRequests();
        //设置响应码为 503
        exchange.setStatusCode(StatusCodes.SERVICE_UNAVAILABLE);
        //标记请求完成
        exchange.endExchange();
        //直接返回,不继续走其他的 HttpHandler
        return;
    }
    //添加请求完成时候的 listener,这个在请求完成返回响应时会被调用,将计数器原子减一
    exchange.addExchangeCompleteListener(listener);
    //继续走下一个 HttpHandler
    next.handleRequest(exchange);
}

那么,是什么时候调用的这个关闭呢?前面我们说过 ApplicationContext 的关闭过程的第三步:处理所有实现 Lifecycle 接口的 Bean,解析他们的关闭顺序,并调用他们的 stop 方法,其实优雅关闭就在这里被调用。当 Spring Boot + Undertow & 同步 Servlet 环境启动时,到了创建 WebServer 这一步,会创建一个优雅关闭的 Lifecycle,对应源码:

相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
96 2
|
16天前
|
XML Java 数据格式
使用idea中的Live Templates自定义自动生成Spring所需的XML配置文件格式
本文介绍了在使用Spring框架时,如何通过创建`applicationContext.xml`配置文件来管理对象。首先,在resources目录下新建XML配置文件,并通过IDEA自动生成部分配置。为完善配置,特别是添加AOP支持,可以通过IDEA的Live Templates功能自定义XML模板。具体步骤包括:连续按两次Shift搜索Live Templates,配置模板内容,输入特定前缀(如spring)并按Tab键即可快速生成完整的Spring配置文件。这样可以大大提高开发效率,减少重复工作。
使用idea中的Live Templates自定义自动生成Spring所需的XML配置文件格式
|
16天前
|
设计模式 XML Java
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
本文详细介绍了Spring框架的核心功能,并通过手写自定义Spring框架的方式,深入理解了Spring的IOC(控制反转)和DI(依赖注入)功能,并且学会实际运用设计模式到真实开发中。
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
|
23天前
|
NoSQL Java Redis
Spring Boot 自动配置机制:从原理到自定义
Spring Boot 的自动配置机制通过 `spring.factories` 文件和 `@EnableAutoConfiguration` 注解,根据类路径中的依赖和条件注解自动配置所需的 Bean,大大简化了开发过程。本文深入探讨了自动配置的原理、条件化配置、自定义自动配置以及实际应用案例,帮助开发者更好地理解和利用这一强大特性。
81 14
|
1月前
|
缓存 Java 数据库连接
深入探讨:Spring与MyBatis中的连接池与缓存机制
Spring 与 MyBatis 提供了强大的连接池和缓存机制,通过合理配置和使用这些机制,可以显著提升应用的性能和可扩展性。连接池通过复用数据库连接减少了连接创建和销毁的开销,而 MyBatis 的一级缓存和二级缓存则通过缓存查询结果减少了数据库访问次数。在实际应用中,结合具体的业务需求和系统架构,优化连接池和缓存的配置,是提升系统性能的重要手段。
71 4
|
2月前
|
Java 开发者 Spring
深入解析:Spring AOP的底层实现机制
在现代软件开发中,Spring框架的AOP(面向切面编程)功能因其能够有效分离横切关注点(如日志记录、事务管理等)而备受青睐。本文将深入探讨Spring AOP的底层原理,揭示其如何通过动态代理技术实现方法的增强。
81 8
|
2月前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
55 2
|
3月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
103 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
2月前
|
安全 Java 应用服务中间件
如何将Spring Boot应用程序运行到自定义端口
如何将Spring Boot应用程序运行到自定义端口
68 0
|
3月前
|
架构师 Java 开发者
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
在40岁老架构师尼恩的读者交流群中,近期多位读者成功获得了知名互联网企业的面试机会,如得物、阿里、滴滴等。然而,面对“Spring Boot自动装配机制”等核心面试题,部分读者因准备不足而未能顺利通过。为此,尼恩团队将系统化梳理和总结这一主题,帮助大家全面提升技术水平,让面试官“爱到不能自已”。
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?