背景
Spring boot 2.3 之后引入了优雅的停机之后,http 请求可以优雅的上下线,但是定时任务xxl-job,并不能优雅的上下线,具体问题如下
问题1
服务收到关闭信号之后,xxl-job 还继续为节点分配任务,节点并没有及时上报状态
问题2
关闭的途中,有正在跑的job,此时关闭,比如客户的升降级,比如客户资产的快照等等,这些问题,如果不能及时的发现,就有可能引起故障
如何解决问题
1. git clone xxl-job, 修改xxl-job-core的源码
分析源码
看下 com.xxl.job.core.executor.XxlJobExecutor#destroy() 的源码
public void destroy() {
// destory executor-server
stopEmbedServer();
// destory jobThreadRepository
if (jobThreadRepository.size() > 0) {
for (Map.Entry<Integer, JobThread> item : jobThreadRepository.entrySet()) {
JobThread oldJobThread = removeJobThread(item.getKey(), "web container destroy and kill the job.");
// wait for job thread push result to callback queue
if (oldJobThread != null) {
try {
oldJobThread.join();
} catch (InterruptedException e) {
logger.error(">>>>>>>>>>> xxl-job, JobThread destroy(join) error, jobId:{}", item.getKey(), e);
}
}
}
jobThreadRepository.clear();
}
jobHandlerRepository.clear();
// destory JobLogFileCleanThread
JobLogFileCleanThread.getInstance().toStop();
// destory TriggerCallbackThread
TriggerCallbackThread.getInstance().toStop();
}
其中 stopEmbedServer(), 就是停止服务,取消注册,可以满足问题1
com.xxl.job.core.server.EmbedServer#stop
public void stop() throws Exception {
// destroy server thread
if (thread!=null && thread.isAlive()) {
thread.interrupt();
}
// stop registry
stopRegistry();
logger.info(">>>>>>>>>>> xxl-job remoting server destroy success.");
}
其中com.xxl.job.core.executor.XxlJobExecutor#removeJobThread
是直接打断job的,不能满足问题2
public static JobThread removeJobThread(int jobId, String removeOldReason){
JobThread oldJobThread = jobThreadRepository.remove(jobId);
if (oldJobThread != null) {
oldJobThread.toStop(removeOldReason);
oldJobThread.interrupt();
return oldJobThread;
}
return null;
}
修改源码
增加一个优雅停机xxl-job的方法gracefulDestroy
public void gracefulDestroy() {
logger.info("开始取消注册job");
stopEmbedServer();
logger.info("等等job执行完毕");
// 一直等待job执行完毕,
//可以设置一个白名单,只对某些job,检查是否执行完毕,也可以设置一个最多等待时间
while (true) {
List<JobThread> collect = jobThreadRepository.values().stream().filter(JobThread::isRunningOrHasQueue).collect(Collectors.toList());
if (CollectionUtils.isEmpty(collect)) {
break;
}
//打印具体的为执行完的job
logger.info("job:{},还未执行完毕", collect.stream().filter(t -> t.getHandler() instanceof MethodJobHandler).map(t -> ((MethodJobHandler) t.getHandler()).toString()).collect(Collectors.joining(",")));
// 休眠一秒
try {
// 也可以设置一个最多等待时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
logger.info("job执行完毕!");
// destory JobLogFileCleanThread
JobLogFileCleanThread.getInstance().toStop();
// destory TriggerCallbackThread
TriggerCallbackThread.getInstance().toStop();
}
节点修改
修改配置文件application.yaml
将shutdown方式改为graceful
关闭超时最大改为2分钟,具体根据情况而定,注意超过此时间,就会立即关闭spring容器
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 120s
关闭时调用gracefulDestroy()
首先我们查看下Bean的生命周期销毁
从图中可见,我们不能通过指定 @Bean(destroyMethod = "gracefulDestroy") 方法,
因为此时可能MethodJobHandler,所依赖的bean有可能已经销毁了,比如数据库DataSource已经关闭。
实现DisposableBean 和注解PreDestroy,也是同样的问题
那么 只有ContextClosedEvent了,具体代码如下
@Component
@Log4j2
public class ApplicationShutdown implements ApplicationListener<ContextClosedEvent> {
@Resource
private XxlJobSpringExecutor xxlJobSpringExecutor;
@Override
public void onApplicationEvent(ContextClosedEvent event) {
log.info("getDisplayName:{}", event.getApplicationContext().getDisplayName());
xxlJobSpringExecutor.destroy();
}
}
后记
1 . 关闭进程的时候,不能kill -9 pid ,否侧上面都是无效,有两个方式,
一种
kill -15 pid
另一种
设置
management:
shutdown:
enabled: true
通过 curl 命令关闭
curl -X POST http://127.0.0.1:40001/actuator/shutdown
- 优雅关机系列中,还有很多
- request请求(spring 2.3之后,设置shutdown=graceful,就可以了)
- mq
- 线程池的任务,比如request 中,使用了线程池