背景最近项目在jenkins部署的时候发现部署很慢,查看部署日志发现kill命令执行后应用pid还存在,导致必须在60秒等待期后kill -9杀死springboot进程
应用环境
- springboot
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.6.3</version>
</dependency>
- springcloud
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
- 监控
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.6.3</version>
</dependency>
原因分析
- 通过将全部日志调整为debug级别,观察到有个定时任务线程在不断执行,例子如下
java
@SpringBootApplication
@MapperScan("com.test.test.mapper")
public class TestApplication implements CommandLineRunner {
static ScheduledExecutorService executor;
public static void main(String[] args) {
executor = Executors.newScheduledThreadPool(1);
SpringApplication.run(TestApplication.class, args);
}
private static void run(ScheduledExecutorService executor) {
executor.scheduleAtFixedRate(() -> {
System.out.println("run");
}, 0, 1, TimeUnit.SECONDS);
@Override
public void run(String... args) throws Exception {
run(executor);
}
}
上述代码中,由于线程定义默认是非守护线程,执行优雅停机后,在用户线程停止后,非守护线程不会自动停止
解决办法
- 定义为守护线程
对于非业务逻辑,例如监控数据上传,日志记录,这样做非常方便,但对于系统业务,这么做会导致未执行完成任务被丢弃。 - 将线程池定义为springbean,交予spring容器管理其生命周期
java
@SpringBootApplication
@MapperScan("com.test.test.mapper")
public class TestApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
private static void run(ScheduledExecutorService executor) {
executor.scheduleAtFixedRate(() -> {
System.out.println("run");
}, 0, 1, TimeUnit.SECONDS);
}
@Bean
public ScheduledExecutorService executor() {
return Executors.newScheduledThreadPool(1);
}
@Override
public void run(String... args) throws Exception {
ScheduledExecutorService executor = SpringUtil.getBean(ScheduledExecutorService.class);
run(executor);
}
}
效果
弊端:此类方式中,由于线程池的工作线程属于非守护线程,应用会等待所有任务执行完成后才关闭。由于容器已经关闭,数据库连接池已经释放,这时候任务再获取spring容器内容会报错,因此这种方案只适用于用户日志记录,监控等非业务功能,效果如下:
java
@SpringBootApplication
@MapperScan("com.test.test.mapper")
@Slf4j
public class TestApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
private static void run(ExecutorService executor) {
executor.execute(() -> {
log.info("=====start");
try {
TimeUnit.SECONDS.sleep(25);
User user = SpringUtil.getBean(IUserService.class).findById(10L);
log.info("用户信息:" + user);
} catch (Exception ex) {
ex.printStackTrace();
}
log.info("=========end");
});
}
@Bean
public ExecutorService executor() {
return new ThreadPoolExecutor(
10, 10, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1),
r -> {
Thread thread =new Thread(r);
return thread;
},
new ThreadPoolExecutor.DiscardOldestPolicy());
}
@Override
public void run(String... args) throws Exception {
ExecutorService executor = SpringUtil.getBean(ExecutorService.class);
run(executor);
}
}
3.使用spring提供的ThreadPoolTaskExecutor线程池
java
@SpringBootApplication
@MapperScan("com.test.test.mapper")
@Slf4j
public class TestApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
private static void run(ThreadPoolTaskExecutor executor) {
executor.execute(() -> {
log.info("=====start");
try {
TimeUnit.SECONDS.sleep(25);
User user = SpringUtil.getBean(IUserService.class).findById(10L);
log.info("用户信息:" + user);
} catch (Exception ex) {
ex.printStackTrace();
}
log.info("=========end");
});
}
@Bean
public ThreadPoolTaskExecutor executor() {
int core = Runtime.getRuntime().availableProcessors();
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(core > 3 ? core >> 1 : core);
int maxSize = core + 2;
executor.setMaxPoolSize(maxSize);
//使用同步队列,避免任务进入等待队列排队导致耗时过长
executor.setQueueCapacity(0);
executor.setKeepAliveSeconds(30);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(25);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public void run(String... args) throws Exception {
ThreadPoolTaskExecutor executor = SpringUtil.getBean(ThreadPoolTaskExecutor.class);
run(executor);
}
}
从上图可以看到,应用会等待线程池任务执行完毕后才选择优雅关闭,因此对于异步业务任务,ThreadPoolTaskExecutor才是首选。
spring已经内置了ThreadPoolTaskExecutor 线程池实例,我们可以通过修改其配置参数,简化代码,例如:
bash
spring:
task:
execution:
pool:
queue-capacity: 0
core-size: 2
max-size: 16
keep-alive: 30s
thread-name-prefix: 'async-'
shutdown:
await-termination: true
await-termination-period: 25s
java
@SpringBootApplication
@MapperScan("com.test.test.mapper")
@Slf4j
public class TestApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
private static void run(ThreadPoolTaskExecutor executor) {
executor.execute(() -> {
log.info("=====start");
try {
TimeUnit.SECONDS.sleep(25);
User user = SpringUtil.getBean(IUserService.class).findById(10L);
log.info("用户信息:" + user);
} catch (Exception ex) {
ex.printStackTrace();
}
log.info("=========end");
});
}
@Override
public void run(String... args) throws Exception {
ThreadPoolTaskExecutor executor = SpringUtil.getBean(ThreadPoolTaskExecutor.class);
run(executor);
}
}
效果与上述手动创建效果一样