背景
Spring boot 2.3 之后引入了優(yōu)雅的停機之后,http 請求可以優(yōu)雅的上下線,但是定時任務(wù)xxl-job,并不能優(yōu)雅的上下線,具體問題如下
問題1
服務(wù)收到關(guān)閉信號之后,xxl-job 還繼續(xù)為節(jié)點分配任務(wù),節(jié)點并沒有及時上報狀態(tài)
問題2
關(guān)閉的途中,有正在跑的job,此時關(guān)閉,比如客戶的升降級,比如客戶資產(chǎn)的快照等等,這些問題,如果不能及時的發(fā)現(xiàn),就有可能引起故障
如何解決問題
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(), 就是停止服務(wù),取消注冊,可以滿足問題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;
}
修改源碼
增加一個優(yōu)雅停機xxl-job的方法gracefulDestroy
public void gracefulDestroy() {
logger.info("開始取消注冊job");
stopEmbedServer();
logger.info("等等job執(zhí)行完畢");
// 一直等待job執(zhí)行完畢,
//可以設(shè)置一個白名單,只對某些job,檢查是否執(zhí)行完畢,也可以設(shè)置一個最多等待時間
while (true) {
List<JobThread> collect = jobThreadRepository.values().stream().filter(JobThread::isRunningOrHasQueue).collect(Collectors.toList());
if (CollectionUtils.isEmpty(collect)) {
break;
}
//打印具體的為執(zhí)行完的job
logger.info("job:{},還未執(zhí)行完畢", collect.stream().filter(t -> t.getHandler() instanceof MethodJobHandler).map(t -> ((MethodJobHandler) t.getHandler()).toString()).collect(Collectors.joining(",")));
// 休眠一秒
try {
// 也可以設(shè)置一個最多等待時間
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
logger.info("job執(zhí)行完畢!");
// destory JobLogFileCleanThread
JobLogFileCleanThread.getInstance().toStop();
// destory TriggerCallbackThread
TriggerCallbackThread.getInstance().toStop();
}
節(jié)點修改
修改配置文件application.yaml
將shutdown方式改為graceful
關(guān)閉超時最大改為2分鐘,具體根據(jù)情況而定,注意超過此時間,就會立即關(guān)閉spring容器
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 120s
關(guān)閉時調(diào)用gracefulDestroy()
首先我們查看下Bean的生命周期銷毀

從圖中可見,我們不能通過指定 @Bean(destroyMethod = "gracefulDestroy") 方法,
因為此時可能MethodJobHandler,所依賴的bean有可能已經(jīng)銷毀了,比如數(shù)據(jù)庫DataSource已經(jīng)關(guān)閉。
實現(xiàn)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 . 關(guān)閉進程的時候,不能kill -9 pid ,否側(cè)上面都是無效,有兩個方式,
一種
kill -15 pid
另一種
設(shè)置
management:
shutdown:
enabled: true
通過 curl 命令關(guān)閉
curl -X POST http://127.0.0.1:40001/actuator/shutdown
- 優(yōu)雅關(guān)機系列中,還有很多
- request請求(spring 2.3之后,設(shè)置shutdown=graceful,就可以了)
- mq
- 線程池的任務(wù),比如request 中,使用了線程池