优雅停机


什么是优雅停机

优雅停机指的是Java项目在停机时需要做好断后工作。如果直接使用kill -9 方式暴力的将项目停掉,可能会导致正常处理的请求、定时任务、RMI、注销注册中心等出现数据不一致问题。

如何解决优雅停机呢?大致需要解决如下问题:

  1. 首先要确保不会再有新的请求进来,所以需要设置一个流量挡板
  2. 保证正常处理已进来的请求线程,可以通过计数方式记录项目中的请求数量
  3. 如果涉及到注册中心,则需要在第一步结束后注销注册中心
  4. 停止项目中的定时任务
  5. 停止线程池
  6. 关闭其他需要关闭资源等等等

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优雅停机源码分析

springboot使用actuator停机 springboot优雅停机原理_tomcat

上图中出现了两个重要的Bean:WebServerGracefulShutdownLifecycle、WebServerStartStopLifecycle

两个Bean都实现了SmartLifecycle接口,该接口在SpringBoot3.0出现。用于定义与关闭有关的生命周期方法。

WebServerStartStopLifecycle:

@Override
	public void start() {
		this.webServer.start();
		this.running = true;
		this.applicationContext
				.publishEvent(new ServletWebServerInitializedEvent(this.webServer, this.applicationContext));
	}

	@Override
	public void stop() {
		this.webServer.stop();
	}
	
WebServerGracefulShutdownLifecycle : 

	@Override
	public void start() {
		this.running = true;
	}
	
	@Override
	public 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方法关闭IOC
	super.doClose();
}
protected void doClose() {
		//启动IOC关闭状态
		if (this.active.get() && this.closed.compareAndSet(false, true)) {
			if (logger.isDebugEnabled()) {
				logger.debug("Closing " + this);
			}

			//注销JMX
			LiveBeansView.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();

			// 关闭IOC
			onClose();

			// 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);
		}
	}
自研优雅停机

springboot使用actuator停机 springboot优雅停机原理_SpringBoot_02

目前团队内部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进行后置处理。