之前搭建过jenkins发版项目,启动脚本中的判断进程是否存在,然后杀死进程的方式是kill -9的,这样的做法是很不优雅的,执行复杂的业务的时候突然中断可能导致一系列的问题。哎,包括我目前所在的公司处理生产的重启也是这样,我想到了之前无意中看到的优雅停机的方式
项目已上传gitee
https://gitee.com/gangye/springboot_mutiDemos/tree/shutdown-gentle-branch/
常规的java项目优雅停机
Java语言本身是支持优雅停机的,先写一个demo来看看普通的java项目是如何优雅停机的。搭建一个项目,为了下面继续使用springboot项目展示,我就不创建普通的java项目而是一个maven项目
<dependencies>
<!--引入另一个项目工程-->
<dependency>
<groupId>com.csrcb</groupId>
<artifactId>common_demo</artifactId>
<version>1.0.0</version>
</dependency>
<!--若使用了spring-boot-starter-web依赖,则无需引入此依赖,以免冲突,否则引入此日志依赖-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!--<mainClass>com.csrcb.ShutdownGentleNativeTest</mainClass>-->
<mainClass>com.csrcb.SpringAppShutdown</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
然后配置log4j.properties配置文件
#此配置适用于非springboot项目引入slf4j配置,首先引入slf4j-log4j12依赖
#也适用于springboot项目(前提springboot是1.3及之前的版本,1.3之后版本有所区别,
# 我使用的2.1.6版本不再支持properties格式文件支持,支持xml格式的,当然之前的logback-spring.xml是毫无疑问支持的并且无需在applications.yml中指定配置
# 为了兼容之前写的模拟优雅停机的非spring项目而引入的日志配置log.properties,我做了些调整,
# 将springboot-web-start中去除了spring-boot-starter-logging依赖,然后额外引入slf4j-log4j12依赖,接着在application.yml配置文件中配置好要使用的日志配置文件路径logging.config: classpath:log4j.properties)
#config root logger
log4j.rootLogger = INFO,system.out
log4j.appender.system.out=org.apache.log4j.ConsoleAppender
log4j.appender.system.out.layout=org.apache.log4j.PatternLayout
log4j.appender.system.out.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} [%p] [%t] %c - %m%n
#config this Project.file logger
#log4j.logger.thisProject.file=INFO,thisProject.file.out
#log4j.appender.thisProject.file.out=org.apache.log4j.DailyRollingFileAppender
#log4j.appender.thisProject.file.out.File=logContentFile.log
#log4j.appender.thisProject.file.out.layout=org.apache.log4j.PatternLayout
#log4j.appender.thisProject.file.out.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} [%c]-[%p] %m%n
在application.yml中指定log4j.properties配置文件
测试
Runtime.getRuntime().addShutdownHook();
功能
@Slf4j
public class ShutdownHook extends Thread{
private Thread mainThread;
private boolean shutdownFlag;
public ShutdownHook(Thread mainThread) {
this.mainThread = mainThread;
this.shutdownFlag = false;
Runtime.getRuntime().addShutdownHook(this);
}
@Override
public void run() {
log.info("Shut down signal received.");
this.shutdownFlag = true;
mainThread.interrupt();
try {
mainThread.join();//当收到停止信号时,等待mainThread的执行完成
} catch (InterruptedException e) {
log.error("中断后join异常",e);
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
log.error("睡眠中断异常");
}
log.info("Shut down excute all complete.");
}
public boolean getShutownFlag(){
return shutdownFlag;
}
}
编写启动类,测试优雅停机功能是否实现
@Slf4j
public class ShutdownGentleNativeTest {
private ShutdownHook shutdownHook;
public ShutdownGentleNativeTest(){
this.shutdownHook = new ShutdownHook(Thread.currentThread());
}
public static void main(String[] args) {
ShutdownGentleNativeTest shutdownTest = new ShutdownGentleNativeTest();
log.info("app start...");
shutdownTest.businessDemoExcute();
log.info("main Thread end...");
}
public void businessDemoExcute(){
while (!shutdownHook.getShutownFlag()){
log.info("do something start...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.error("睡眠中断异常捕获:",e);
}
log.info("do something end...");
}
log.info("end of businessExcute...");
}
}
idea启动然后停机有点类似断点的效果,无法模拟,然后我就打包部署发版,然后测试使用Ctrl+C(与kill -2没什么区别)还有kill -15,复合预期的优雅停机案例
Springboot项目的优雅停机
由于大部分web项目都是使用的tomcat做容器,而且springboot自带的默认容器也是tomcat,所以就以常规的springboot项目做案例,我这使用的是springboot2.1的版本没有2.3以后的版本,没有引入spring-boot-starter-actuator组件
若引入了spring-boot-starter-actuator组件,优雅停机就简单多了
以2.3以后的版本为例,配置这样即可
server.shutdown: graceful
spring.lifecycle.timeout-per-shutdown-phase: 30s
下面回归正题
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Connector;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @Classname GracefulShutdownTomcat
* @Description tomcat容器的优雅停机
* @Date 2021/9/3 16:39
* @Created by gangye
*/
@Slf4j
@Component
public class GracefulShutdownTomcat implements TomcatConnectorCustomizer , ApplicationListener<ContextClosedEvent> {
private volatile Connector connector;
//设置收到中断(终结)信号后等待时间,超出时间将强制关机
private final int waitTime = 30;
@Override
public void customize(Connector connector) {
this.connector = connector;
}
@Override
public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
this.connector.pause();
Executor executor = this.connector.getProtocolHandler().getExecutor();
if (executor instanceof ThreadPoolExecutor) {
try {
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
threadPoolExecutor.shutdown();
if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
log.warn("Tomcat thread pool did not shut down gracefully within " + waitTime + " seconds. Proceeding with forceful shutdown");
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}
在启动类中配编写
import com.csrcb.config.GracefulShutdownTomcat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
/**
* @Classname SpringAppShutdown
* @Description SpringBoot的优雅停机(单体应用)
* @Date 2021/8/31 17:06
* @Created by gangye
*/
@SpringBootApplication
public class SpringAppShutdown {
public static void main(String[] args) {
SpringApplication.run(SpringAppShutdown.class,args);
}
@Autowired
private GracefulShutdownTomcat gracefulShutdownTomcat;
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
tomcat.addConnectorCustomizers(gracefulShutdownTomcat);
return tomcat;
}
}
同样,打包部署mvn clean install,注意将pom文件的mainClass里面的类改下,然后将打包的jar包,使用java -jar XXX.jar启动,注意
不要使用nohup后台启动,不然kill -2关闭无效(不会关闭)
,但是kill -15还是正常有优雅停机的效果
请求一个,然后中断,再请求一个
查看日志
复合优雅停机效果,但是此应用还是单体的,在微服务上,复杂应用中,消息队列,服务下线,定时任务关闭,等等一系列都要进行处理
参考文章:
https://www.jianshu.com/p/0c49eb23c627