优雅停机
什么是优雅停机
优雅停机指的是Java项目在停机时需要做好断后工作。如果直接使用kill -9 方式暴力的将项目停掉,可能会导致正常处理的请求、定时任务、RMI、注销注册中心等出现数据不一致问题。
如何解决优雅停机呢?大致需要解决如下问题:
- 首先要确保不会再有新的请求进来,所以需要设置一个流量挡板
- 保证正常处理已进来的请求线程,可以通过计数方式记录项目中的请求数量
- 如果涉及到注册中心,则需要在第一步结束后注销注册中心
- 停止项目中的定时任务
- 停止线程池
- 关闭其他需要关闭资源等等等
SpringBoot优雅停机出现之前,一般需要通过自研方式来保证优雅停机。我也见过有项目组使用 kill -9 或者执行 shutdown脚本直接停止运行的项目,当然这种方式不够优雅。SpringBoot在最新的2.X.X版本中新增了优雅停机功能,该功能解决了之前 kill -9的暴力停机问题。我们一起来剖析一下SpringBoot提供的优雅停机
SpringBoot优雅停机使用方式
以SpringBoot2.3.4-RELEASE为例
创建好项目后引入 :;
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
SpringBoot优雅停机有两种使用方式:
方式一:
spring-boot-starter-actuator 模块提供了一个 restful 接口 /actuator/shutdown (POST) 用于优雅停机。一般需要限制内网关IP访问权限,而且最好使用Secrety进行登录验证。
#### 使用endpoints方式需要在配置文件中添加如下配置 server.shutdown=graceful ## 开启优雅停机
spring.lifecycle.timeout-per-shutdown-phase=20s ##设置优雅停机关闭流量挡板后最多等待时间management.server.port=9090 ## 指定endpoints的访问端口,最好不与server.port一致
management.endpoint.shutdown.enabled=true ## 开启/actuator/shutdown路由
management.endpoints.web.exposure.include=shutdown ## 暴露/actuator/shutdown路由
发出一个需要30秒才能完成的请求,然后另一个线程执行 ip:port/actuator/shutdown,可以发现项目会等待20秒之后关闭容器。如果没有正在处理的请求则会立即停机。如果请求处理时间超过配置的20秒则会丢弃处理,进行关机。
方式二:
使用 kill -15 pid 发送停机通知进行优雅停机
kill -9 pid 可以理解为操作系统从内核级别强行杀死某个进程,直接模拟了一次系统宕机,系统断电,这对于应用来说太不友好.kill -15 pid 则可以理解为发送一个通知,告知应用主动关闭。
SpringBoot优雅停机源码分析
上图中出现了两个重要的Bean:WebServerGracefulShutdownLifecycle、WebServerStartStopLifecycle
两个Bean都实现了SmartLifecycle接口,该接口在SpringBoot3.0出现。用于定义与关闭有关的生命周期方法。
WebServerStartStopLifecycle:@Overridepublic void start() {this.webServer.start();this.running = true;this.applicationContext.publishEvent(new ServletWebServerInitializedEvent(this.webServer, this.applicationContext));}@Overridepublic void stop() {this.webServer.stop();}WebServerGracefulShutdownLifecycle : @Overridepublic void start() {this.running = true;}@Overridepublic void stop(Runnable callback) {this.running = false;this.webServer.shutDownGracefully((result) -> callback.run());}
优雅停机最关键的类是GracefulShutdown。WebServerGracefulShutdownLifecycle的stop方法最终会委托给GracefulShutdown。
final class GracefulShutdown {private static final Log logger = LogFactory.getLog(GracefulShutdown.class);private final Tomcat tomcat;private volatile boolean aborted = false;GracefulShutdown(Tomcat tomcat) {this.tomcat = tomcat;}//优雅停机核心方法void shutDownGracefully(GracefulShutdownCallback callback) {logger.info("Commencing graceful shutdown. Waiting for active requests to complete");new Thread(() -> doShutdown(callback), "tomcat-shutdown").start();}private void doShutdown(GracefulShutdownCallback callback) {List<Connector> connectors = getConnectors();connectors.forEach(this::close);try {for (Container host : this.tomcat.getEngine().findChildren()) {for (Container context : host.findChildren()) {while (isActive(context)) {if (this.aborted) {logger.info("Graceful shutdown aborted with one or more requests still active");callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);return;}Thread.sleep(50);}}}}catch (InterruptedException ex) {Thread.currentThread().interrupt();}logger.info("Graceful shutdown complete");callback.shutdownComplete(GracefulShutdownResult.IDLE);}private List<Connector> getConnectors() {List<Connector> connectors = new ArrayList<>();for (Service service : this.tomcat.getServer().findServices()) {Collections.addAll(connectors, service.findConnectors());}return connectors;}private void close(Connector connector) {connector.pause();connector.getProtocolHandler().closeServerSocketGraceful();}private boolean isActive(Container context) {try {//判断关闭挡板后剩余请求数if (((StandardContext) context).getInProgressAsyncCount() > 0) {return true;}for (Container wrapper : context.findChildren()) {if (((StandardWrapper) wrapper).getCountAllocated() > 0) {return true;}}return false;}catch (Exception ex) {throw new RuntimeException(ex);}}void abort() {this.aborted = true;}}
以客户端发出 /actuator/shutdown请求后,SpringBoot接受到请求会进入ShutdownEndpoint的shutdown方法
该方法最终调用了IOC容器的AbstractApplicationContext.close方法,该方法又会委托到它的子类ServletWebServerApplicationContext中的doClose方法
@Override
protected void doClose() {//判断IOC容器是否是运行状态if (isActive()) {//发布一个AvailabilityChangeEvent事件,用于通知Tomcat关闭请求挡板//tomcat中有一个定时任务会维护一个状态,该状态决定了是否接受请求,Tomcat收到时间后会关闭挡板AvailabilityChangeEvent.publish(this, ReadinessState.REFUSING_TRAFFIC);}//调用父类AbstractApplicationContext的doClose方法关闭IOCsuper.doClose();
}
protected void doClose() {//启动IOC关闭状态if (this.active.get() && this.closed.compareAndSet(false, true)) {if (logger.isDebugEnabled()) {logger.debug("Closing " + this);}//注销JMXLiveBeansView.unregisterApplicationContext(this);try {//发布shutdown事件publishEvent(new ContextClosedEvent(this));}catch (Throwable ex) {logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);}//调用WebServerGracefulShutdownLifecycle和WebServerStartStopLifecycle两个Bean生命周期stop方法进行优雅停机if (this.lifecycleProcessor != null) {try {this.lifecycleProcessor.onClose();}catch (Throwable ex) {logger.warn("Exception thrown from LifecycleProcessor on context close", ex);}}//调用Bean的destroy生命销毁方法destroyBeans();// 关闭Bean工厂closeBeanFactory();// 关闭IOConClose();// Reset local application listeners to pre-refresh state.if (this.earlyApplicationListeners != null) {this.applicationListeners.clear();this.applicationListeners.addAll(this.earlyApplicationListeners);}//关闭IOC状态this.active.set(false);}}
自研优雅停机
目前团队内部Devops流程: 上传代码分支----> gitalb合并master分支 ------> jenkins打包版本 -------> 自研管理台拉取nexus中打包的最新版本 --------> 自研管理台选择要升级的版本
团队自研了一套管理台部署系统,本质上是调用shell脚本和提供界面操作。服务要使用自研平台的功能,需要使用封装好的通用的jar包: app-health.jar. 该jar主要包含(省略代码) :
//HealthStatus : 维护一个状态,该状态主要控制流量挡板//started 来源于app启动后的状态,当after_start后,该值设置为true。//closing 来源于servlet的请求,当触发closing时,需要确保started状态不能被设置,并且将started状态设置为false。
//HealthListener主要用于监听tomcat信号,用于开启流量挡板
// HealthHttpFilter会拦截所有请求,用于记录当前接受的请求数量、当流量挡板关闭后还可以起到拒绝请求目的
//HealthServlet用于接受自研系统发出的shutdown请求,该类只是关闭了挡板,并未做注销注册中心、停止线程池等操作。HealthServlet是jar默认提供的,不同的项目可以自行覆盖并定制服务的shutdown请求。通常shutdown请求会包括注销注册中心、等待剩余请求处理、休眠指定秒数、停止线程池、停止定时任务等
团队内部的优雅停机本质上是借助了自研的部署平台。当在管理台上停止某服务时,管理台会向服务发出一个shutodown请求通知服务下线。该shutodown请求可以在管理台上进行配置。既然暴露了shutdown请求那是不是会遭到有心人乱调用呢? 肯定不会的,shutdown请求会限制指定ip等。服务接受到shutdown请求后首先会关闭流量挡板、然后注销注册中心、等待剩余请求处理、休眠指定秒数、停止线程池、停止定时任务等。shutdown结束之后会返回响应给管理台系统。管理台收到响应后会调用shell脚本关闭Tomcat容器从而实现优雅停机。
服务的部署也是调用shell脚本启动tomcat容器,容器启动好后,通用jar中的HealthListener会监听到Tomcat的发出的Lifecycle中不同的sign信号,当HealthListener收到Lifecycle.AFTER_START_EVENT信号之后说明容器部署成功。然后会将流量挡板打开正常运行服务。
总结
SprungBoot2.3.版本提供的新特性皆在融合docker/k8s.比如actuator新增的两个地址:/actuator/health/liveness和/actuator/health/readiness,前者用作kubernetes的存活探针,后者用作kubernetes的就绪探针;以及maven-plugin-starter支持打包docker镜像、提供spring-boot-jarmode-layertools工具提供镜像分层功能。
本质上SpringBoot的优雅停机与团队自研的优雅停机没有太大区别。都是先关闭流量挡板再处理剩余请求。但是两者都需要通过定制关闭挡板后的操作。SpringBoot并没有提供关闭线程池、定时任务、注册中心下线等操作。所以还是需要封装一个通用的starter进行后置处理。