記錄定時任務遷移xxl-job的過程和理解

         一般定時任務使用的是基於quartz或者spring-scheduler的,能夠滿足大部分的開發需求。但是像手動執行一次,執行情況監測,進程阻塞停止等維護需求就顯得無能爲力了。無意間在gitee.com上發現了一個很好滿足以上需求的項目,來自許雪裏開源的一個輕量級分佈式任務調度平臺xxl-job。gitee地址:https://gitee.com/xuxueli0323/xxl-job,主頁爲:http://www.xuxueli.com/xxl-job/。本文只觸及到其簡單的功能使用,更多功能和更深層次的理解請參考源碼。

       通過其demo使用和源碼的梳理,基本搞明白了其運行原理。其主要分爲任務執行器和任務調度器。任務執行器接收調度指令,執行相應的方法。任務調度器根據事先設定的每個job handler 的cron表達式調度任務執行器。如果調度失敗可以根據配置再調度其他的執行器(集羣模式下)。在集羣模式下,相同appname的多個執行器組成一個執行器集羣,Handler與執行器集羣之間通過appname綁定。一個執行器相當於一個runner實體,一個runner可以有多個Handler(job),他們都由調度器管理,在調度器中指定Handler與執行器之間的關係。

       執行器項目一般集成到自己的項目中,調度器是一個後臺管理項目,二者分別部署到兩個主機,通過rpc交互(xxl-rpc):調度器通過rpc的9999端口調用執行器,執行器回調走8080http端口調用調度中心。任務調度中心通過數據庫管理執行器和job等實體信息,所以需要一個數據庫。直接通過

java -jar xxl-job-admin-2.1.1-SNAPSHOT.jar &

的方式運行調度中心。配置執行器和任務。

路由策略

執行器參考其示例項目,引入xxl-job-core,初始化XxlJobExecutor,註冊jobhandler即可。

我們以前的項目是基於quartz的,框架基於vertx,所以選擇了FrameLess這種集成方式。

Set<Class<?>> jobHandlerClasses = ClassUtil.scanPackageBySuper("xx.xx.xxx.xxx.package", true, IJobHandler.class);

        for (Class<?> jobHandlerClass : jobHandlerClasses) {
            String name = StrUtil.lowerFirst(jobHandlerClass.getSimpleName());
            if(jobHandlerClass.isAnnotationPresent(JobHandler.class)){
                String value = jobHandlerClass.getAnnotation(JobHandler.class).value();
                if(!"".equals(value)){
                    name = value;
                }
            }
            IJobHandler jobHandler = BeanUtil.newInstance(jobHandlerClass);
            XxlJobExecutor.registJobHandler(name , jobHandler);
        }

        // load executor prop
        //Properties xxlJobProp = loadProperties("xxl-job-executor.properties");
        Prop prop = PropertiesUtils.use(CONFIG_PATH + "xxl-job-executor.properties");

        // init executor
        xxlJobExecutor = new XxlJobExecutor();
        xxlJobExecutor.setAdminAddresses(prop.get("xxl.job.admin.addresses"));
        xxlJobExecutor.setAppName(prop.get("xxl.job.executor.appname"));
        xxlJobExecutor.setIp(prop.get("xxl.job.executor.ip"));
        xxlJobExecutor.setPort(prop.getInt("xxl.job.executor.port"));
        xxlJobExecutor.setAccessToken(prop.get("xxl.job.accessToken"));
        xxlJobExecutor.setLogPath(prop.get("xxl.job.executor.logpath"));
        xxlJobExecutor.setLogRetentionDays(prop.getInt("xxl.job.executor.logretentiondays"));

        // start executor
        try {
            xxlJobExecutor.start();
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }

以前的job不動,重新寫jobhandler,然後將以前的job關閉,註冊這個新的handler到調度器中。jobhandler中調用以前的service方法,先改幾個不太重要的job,比如清理類的job,執不執行和多次執行都沒啥影響的,運行無誤之後再遷移其他所有的job。如此,可以非常平滑地遷移,遷移的風險也降到最低。job和jobhandler之間其實相當於兩個馬甲,都是調用service來完成具體的事情,從此也能看出代碼規範的重要性。

public class DemoJobHandler extends IJobHandler {

	@Override
	public ReturnT<String> execute(String param) throws Exception {
		XxlJobLogger.log("XXL-JOB, Hello World.");

		for (int i = 0; i < 5; i++) {
			XxlJobLogger.log("beat at:" + i);
			TimeUnit.SECONDS.sleep(2);
		}
        
        
        xxxxx.service.doSomething();

		return SUCCESS;
	}

}

 

FrameLess這種集成方式有一個非常重要的問題,就是如何保持程序的不退出?

第一種方式:利用quartz的job線程,即至少運行一個job,這種方式比較簡單,但是不夠優雅。

第二種方式:寫一個hold住程序的類,jvm退出的兩個理由是1)不再擁有前臺進程和2)程序調用了exit,所以只要保證程序有一個前臺進程,jvm就不會退出。

/**
 * 使程序不退出,保證至少一個前臺進程
 * @see https://dubbo.apache.org/zh-cn/blog/spring-boot-dubbo-start-stop-analysis.html
 * @author xiongshiyan at 2019/10/16 , contact me with email [email protected] or phone 15208384257
 */
public class HoldProcessor {
    private volatile boolean stopAwait = false;
    /**
     * Thread that currently is inside our await() method.
     */
    private volatile Thread awaitThread = null;

    /**
     * 開始等待
     */
    public void startAwait(){
        Thread awaitThread = new Thread(this::await,"hold-process-thread");
        awaitThread.setContextClassLoader(getClass().getClassLoader());
        //這一步很關鍵,保證至少一個前臺進程
        awaitThread.setDaemon(false);
        awaitThread.start();
    }

    /**
     * 停止等待,退出程序,一般放在shutdown hook中執行
     * @see Runtime#addShutdownHook(Thread)
     */
    public void stopAwait() {
        //此變量
        stopAwait=true;
        Thread t = awaitThread;
        if (null != t) {
            t.interrupt();
            try {
                t.join(1000);
            } catch (InterruptedException e) {
                // Ignored
            }
        }
    }

    private void await(){
        try {
            awaitThread = Thread.currentThread();
            while(!stopAwait) {
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch( InterruptedException ex ) {
                    // continue and check the flag
                }
            }
        } finally {
            awaitThread = null;
        }
    }
}
FrameLessXxlJobConfig.getInstance().initXxlJobExecutor();

HoldProcessor holdProcessor = new HoldProcessor();
holdProcessor.startAwait();
logger.info("程序開始等待");
Runtime.getRuntime().addShutdownHook(new Thread(()->{
      logger.info("收到kill 信號,執行清理程序");
      //在關閉的時候釋放資源
      FrameLessXxlJobConfig.getInstance().destroyXxlJobExecutor();
      holdProcessor.stopAwait();
}));
參考:https://dubbo.apache.org/zh-cn/blog/spring-boot-dubbo-start-stop-analysis.html


基於以上的原因,程序不要使用kill -9 pid的方式關閉,這樣會收不到關閉信號無法執行鉤子程序。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章