优雅停机
什么是优雅停机
优雅停机指的是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:
@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);
}
}
自研优雅停机
目前团队内部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进行后置处理。