Datax-web 源码阅读记录

Datax-web 源码阅读;搭建Datax-web开发环境可以参考上一篇博客Datax-web 二次开发环境配置

一、Datax-web简介

DataX Web是在DataX之上开发的开源分布式数据同步工具,提供简单易用的 操作界面,降低用户使用DataX的学习成本,缩短任务配置时间,避免配置过程中出错。

Datax Web基于xxl-job;Datax Web核心是拼接Datax的json、执行Datax脚本部分代码。

项目地址

Datax-web https://gitee.com/WeiYe-Jing/datax-web
Datax-web https://github.com/alibaba/DataX

软件版本:

Datax 3.0
Datax-web 2.1.2

commit信息

commit f0aac36b6f3c5c6182b8985bd0bcf1470201e92f (HEAD -> master, origin/master, origin/HEAD)
Author: WeiYe <33245094+WeiYe-Jing@users.noreply.github.com>
Date:   Tue Mar 23 21:42:58 2021 +0800

    Update README.md

框架版本:

Spring Boot 2.1.4.RELEASE
Mybatis Plus 3.3.1

二、源码阅读

按照调用顺序阅读

1 datax-admin(调度中心)启动 执行流程

1.1 调度中心启动类 DataXAdminApplication.main()

    public static void main(String[] args) throws UnknownHostException {
        Environment env = new SpringApplication(DataXAdminApplication.class).run(args).getEnvironment();
        String envPort = env.getProperty("server.port");
        String envContext = env.getProperty("server.contextPath");
        // 配置文件,默认端口是8080
        String port = envPort == null ? "8080" : envPort;
        String context = envContext == null ? "" : envContext;
        String path = port + "" + context + "/doc.html";
        String externalAPI = InetAddress.getLocalHost().getHostAddress();
        // 打印3个url,第一个和第二个我这里打开是一样的都是swagger接口文档,第三个是页面的访问地址
        logger.info(
                "Access URLs:\n----------------------------------------------------------\n\t"
                        + "Local-API: \t\thttp://127.0.0.1:{}\n\t"
                        + "External-API: \thttp://{}:{}\n\t"
                        + "web-URL: \t\thttp://127.0.0.1:{}/index.html\n\t----------------------------------------------------------",
                path, externalAPI, path, port);
    }

1.2 配置类 JobAdminConfig.afterPropertiesSet()

读取配置文件,初始化bean时会自动调用afterPropertiesSet()初始化JobScheduler类。InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候会自动执行该方法。这个方法里初始化JobScheduler对象,同时调用init方法

	@Override
    public void afterPropertiesSet() throws Exception {
        adminConfig = this;

        xxlJobScheduler = new JobScheduler();
        // 初始化任务调度器
        xxlJobScheduler.init();
    }

1.3 调度器初始化 JobScheduler.init()

public void init() throws Exception {
        // 1 初始化页面国际化的工具
        initI18n();

        // 2 启动注册监控线程
        JobRegistryMonitorHelper.getInstance().start();

        // 3 启动失败监控线程
        JobFailMonitorHelper.getInstance().start();

        // 4 初始化触发线程池,创建快慢线程池
        JobTriggerPoolHelper.toStart();

        // 5 启动日志线程
        JobLogReportHelper.getInstance().start();

        // 6 启动作业调度器,这个类是主要逻辑.启动一个死循环,不断的遍历任务触发执行。为避免cpu飙升,隔一会睡一会(这里是重点)
        JobScheduleHelper.getInstance().start();

        logger.info(">>>>>>>>> init datax-web admin success.");
    }

1.5 调度器真正初始化 JobScheduleHelper.getInstance().start()

public void start() {

        // 启动一个调度线程
        scheduleThread = new Thread(new Runnable() {
            @Override
            public void run() {

                try {
                	// 睡大约4秒
                    TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis() % 1000);
                } catch (InterruptedException e) {
                    if (!scheduleThreadToStop) {
                        logger.error(e.getMessage(), e);
                    }
                }
                logger.info(">>>>>>>>> init datax-web admin scheduler success.");

                // 预读取数量,等于快线程池大小加慢线程池大小的和再乘以20,默认的话是(200+100)*20 = 6000
                int preReadCount = (JobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + JobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;
				//死循环,进程退出时修改变量为true
                while (!scheduleThreadToStop) {

                    // 从数据库查询job
                    long start = System.currentTimeMillis();

                    Connection conn = null;
                    Boolean connAutoCommit = null;
                    PreparedStatement preparedStatement = null;

                    boolean preReadSuc = true;
                    try {

                        conn = JobAdminConfig.getAdminConfig().getDataSource().getConnection();
                        connAutoCommit = conn.getAutoCommit();
                        conn.setAutoCommit(false);
						// 获取一个悲观锁
                        preparedStatement = conn.prepareStatement("select * from job_lock where lock_name = 'schedule_lock' for update");
                        preparedStatement.execute();

                        // tx start

                        // 1、pre read
                        long nowTime = System.currentTimeMillis();
                        // 查询要执行的任务,如何判断要执行呢,即下次执行时间小于等于当前时间+5s
                        List<JobInfo> scheduleList = JobAdminConfig.getAdminConfig().getJobInfoMapper().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);
                        if (scheduleList != null && scheduleList.size() > 0) {
                            // 2、push time-ring
                            for (JobInfo jobInfo : scheduleList) {

                                // 判断下次执行时间是否小于(nowTime-5s),如果为true说明任务还没到执行时间跳过此次执行,刷新下次执行时间
                                if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
                                    // 2.1、trigger-expire > 5s:pass && make next-trigger-time
                                    logger.warn(">>>>>>>>>>> datax-web, schedule misfire, jobId = " + jobInfo.getId());

                                    // fresh next
                                    refreshNextValidTime(jobInfo, new Date());
								// 判断下次执行时间是否刚过去5s内,如果是,将任务添加到触发执行线程池,刷新下次执行时间;计算执行秒数;将秒和任务id添加到ringData;刷新下次执行时间
                                } else if (nowTime > jobInfo.getTriggerNextTime()) {
                                    // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time

                                    // 触发任务
                                    JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null);
                                    logger.debug(">>>>>>>>>>> datax-web, schedule push trigger : jobId = " + jobInfo.getId());

                                    // 刷新下次执行时间
                                    refreshNextValidTime(jobInfo, new Date());

                                    // next-trigger-time in 5s, pre-read again
                                    if (jobInfo.getTriggerStatus() == 1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {

                                        // 1、make ring second
                                        int ringSecond = (int) ((jobInfo.getTriggerNextTime() / 1000) % 60);

                                        // 2、push time ring
                                        pushTimeRing(ringSecond, jobInfo.getId());

                                        // 3、fresh next
                                        refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                    }

                                } else {
                                    // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time

                                    // 1、make ring second
                                    int ringSecond = (int) ((jobInfo.getTriggerNextTime() / 1000) % 60);

                                    // 2、push time ring
                                    pushTimeRing(ringSecond, jobInfo.getId());

                                    // 3、fresh next
                                    refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                }

                            }

                            // 3、update trigger info
                            for (JobInfo jobInfo : scheduleList) {
                                JobAdminConfig.getAdminConfig().getJobInfoMapper().scheduleUpdate(jobInfo);
                            }

                        } else {
                            preReadSuc = false;
                        }

                        // tx stop


                    } catch (Exception e) {
                        if (!scheduleThreadToStop) {
                            logger.error(">>>>>>>>>>> datax-web, JobScheduleHelper#scheduleThread error:{}", e);
                        }
                    } finally {

                        // 手动提交修改,关闭连接
                        if (conn != null) {
                            try {
                                conn.commit();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            try {
                                conn.setAutoCommit(connAutoCommit);
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            try {
                                conn.close();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                        }

                        // close PreparedStatement
                        if (null != preparedStatement) {
                            try {
                                preparedStatement.close();
                            } catch (SQLException e) {
                                if (!scheduleThreadToStop) {
                                    logger.error(e.getMessage(), e);
                                }
                            }
                        }
                    }
                    long cost = System.currentTimeMillis() - start;


                    // Wait seconds, align second
                    if (cost < 1000) {  // 扫描时间超过1秒就不sleep,小于1秒才sleep
                        try {
                            // 如果前面预读成功,睡1秒,预读失败睡大约4秒
                            TimeUnit.MILLISECONDS.sleep((preReadSuc ? 1000 : PRE_READ_MS) - System.currentTimeMillis() % 1000);
                        } catch (InterruptedException e) {
                            if (!scheduleThreadToStop) {
                                logger.error(e.getMessage(), e);
                            }
                        }
                    }

                }

                logger.info(">>>>>>>>>>> datax-web, JobScheduleHelper#scheduleThread stop");
            }
        });
        // 设置守护进程
        scheduleThread.setDaemon(true);
        scheduleThread.setName("datax-web, admin JobScheduleHelper#scheduleThread");
        scheduleThread.start();


        // 启动处理之前加入到ringData中数据的线程(前面只是保存到ringData,这里是真正开始触发执行)
        ringThread = new Thread(() -> {

            // align second
            try {
            	// 睡(0~1)秒
                TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
            } catch (InterruptedException e) {
                if (!ringThreadToStop) {
                    logger.error(e.getMessage(), e);
                }
            }
			// 死循环,只有程序退出时才修改标志为true
            while (!ringThreadToStop) {

                try {
                    // second data
                    List<Integer> ringItemData = new ArrayList<>();
                    int nowSecond = Calendar.getInstance().get(Calendar.SECOND);   // 避免处理耗时太长,跨过刻度,向前校验一个刻度;
                    // 这里就是处理之前添加到ringData里的任务;前面存ringData时,key是秒(从1到60),value是由jobid组成的list;每次从ringData里取2秒的数据。
                    for (int i = 0; i < 2; i++) {
                        List<Integer> tmpData = ringData.remove((nowSecond + 60 - i) % 60);
                        if (tmpData != null) {
                            ringItemData.addAll(tmpData);
                        }
                    }

                    // ring trigger
                    logger.debug(">>>>>>>>>>> datax-web, time-ring beat : " + nowSecond + " = " + Arrays.asList(ringItemData));
                    if (ringItemData.size() > 0) {
                        // do trigger
                        for (int jobId : ringItemData) {
                            // 依次调用JobTriggerPoolHelper.trigger()触发任务
                            JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null);
                        }
                        // 执行完后清空列表
                        ringItemData.clear();
                    }
                } catch (Exception e) {
                	// 打印非停止状态下的异常
                    if (!ringThreadToStop) {
                        logger.error(">>>>>>>>>>> datax-web, JobScheduleHelper#ringThread error:{}", e);
                    }
                }

                // 睡(0-1)秒
                try {
                    TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
                } catch (InterruptedException e) {
                    if (!ringThreadToStop) {
                        logger.error(e.getMessage(), e);
                    }
                }
            }
            logger.info(">>>>>>>>>>> datax-web, JobScheduleHelper#ringThread stop");
        });
        ringThread.setDaemon(true);
        ringThread.setName("datax-web, admin JobScheduleHelper#ringThread");
        ringThread.start();
    }

到这里调度中心就启动成功了,下面再看执行器的执行流程

2 datax-executor(执行器)启动 执行流程

2.1 启动类 DataXExecutorApplication

package com.wugui.datax.executor;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author xuxueli 2018-10-28 00:38:13
 */
@SpringBootApplication
public class DataXExecutorApplication {

	public static void main(String[] args) {
        SpringApplication.run(DataXExecutorApplication.class, args);
	}

}

2.2 JobSpringExecutor.afterSingletonsInstantiated()

实现SmartInitializingSingleton的接口后,当所有单例 bean 都初始化完成以后, Spring的IOC容器会回调该接口的 afterSingletonsInstantiated()方法。afterSingletonsInstantiated里调用了父类JobExecutor的start方法

// start
    @Override
    public void afterSingletonsInstantiated() {

        // 遍历所有添加了@JobHandler注解的class,将这些class添加到名为jobHandlerRepository的ConcurrentMap中
        initJobHandlerRepository(applicationContext);

        // 兼容spring-glue
        GlueFactory.refreshInstance(1);


        // 调用父类的start()
        try {
            super.start();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

2.3 启动任务执行器 JobExecutor.start()

public void start() throws Exception {

        // 1.初始化日志。创建执行器日志和glue日志
        JobFileAppender.initLogPath(logPath);

        // 2.将调度中心的ip、端口、accessToken封装成AdminBizClient对象;有几个ip,就封装几个AdminBizClient对象,然后把这些对象都放到adminBizList中
        initAdminBizList(adminAddresses, accessToken);


        // 3.启动清理日志线程。默认删除30天以前的日志,隔一天执行一次
        JobLogFileCleanThread.getInstance().start(logRetentionDays);

        // 4.初始化回调线程。从队列callBackQueue中获取回调对象HandleCallbackParam;callBackQueue是LinkedBlockingQueue,获取对象的方法是take,所以没有对象时会一直阻塞;当获取到回调对象后,调用doCallback方法;
        TriggerCallbackThread.getInstance().start();

        // 5.这里处理过程和4很像,不同的地方在于多了一些关于processId的处理
        ProcessCallbackThread.getInstance().start();

        // 6. 获取当前的ip和配置的执行器注册端口,然后以此端口和类中的方法发布为TCP服务端;创建完服务后,往所有调度中心发送注册服务的http请求(注册接口是 api/registry);参数内容包括:执行器名称、当前TCP服务端的ip和端口;当系统关闭时,还会往所有调度中心发送移除注册的http请求(移除接口是	   		api/registryRemove);这个方法执行完执行器端就启动完成了
        port = port > 0 ? port : NetUtil.findAvailablePort(9999);
        ip = (ip != null && ip.trim().length() > 0) ? ip : IpUtil.getIp();
        initRpcProvider(ip, port, appName, accessToken);
    }

4 任务运行 执行流程

4.1 定时执行

定时执行的任务,要从创建任务说起。创建好的任务就有一个字段是cron表达式,调度中心有一个类JobScheduleHelper会启动一个线程死循环不断去扫描任务表,满足执行条件的任务就会调用JobTriggerPoolHelper.trigger()方法去执行。

以上这一段在datax-admin(调度中心)启动 源码执行流程部分已经分析过,这里就不重复分析了。

因为定时执行调用方法和手动执行都是调用的JobTriggerPoolHelper.trigger()方法,所以就放在下面一起说了

4.2 手动执行

1.页面点击运行任务

2.请求后台接口对应的是JobInfoController.triggerJob()方法,这个方法又调用了JobTriggerPoolHelper.trigger()方法。

@PostMapping(value = "/trigger")
    @ApiOperation("触发任务")
    public ReturnT<String> triggerJob(@RequestBody TriggerJobDto dto) {
        // force cover job param
        String executorParam=dto.getExecutorParam();
        if (executorParam == null) {
            executorParam = "";
        }
        JobTriggerPoolHelper.trigger(dto.getJobId(), TriggerTypeEnum.MANUAL, -1, null, executorParam);
        return ReturnT.SUCCESS;
    }
4.2.1 JobTriggerPoolHelper.trigger()

前面初始化的时候创建了1快1慢两个线程池,这里就排上用场了所有任务开始时都放到快线程池里执行,1分钟内执行超过500毫秒的请求大于10次的任务放入慢线程池处理,线程池中调用JobTrigger.trigger()

public void addTrigger(final int jobId, final TriggerTypeEnum triggerType, final int failRetryCount, final String executorShardingParam, final String executorParam) {

        // choose thread pool
        ThreadPoolExecutor triggerPool_ = fastTriggerPool;
        AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
        if (jobTimeoutCount != null && jobTimeoutCount.get() > 10) {      // job-timeout 10 times in 1 min
            triggerPool_ = slowTriggerPool;
        }
        // trigger
        triggerPool_.execute(() -> {
            long start = System.currentTimeMillis();
            try {
                // do trigger
                JobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam);
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            } finally {
                // check timeout-count-map
                long minTim_now = System.currentTimeMillis() / 60000;
                if (minTim != minTim_now) {
                    minTim = minTim_now;
                    jobTimeoutCountMap.clear();
                }
                // incr timeout-count-map
                long cost = System.currentTimeMillis() - start;
                if (cost > 500) {       // ob-timeout threshold 500ms
                    AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1));
                    if (timeoutCount != null) {
                        timeoutCount.incrementAndGet();
                    }
                }
            }
        });
    }
4.2.2 JobTrigger.trigger()
public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam) {
		// 根据任务id查询到任务信息
        JobInfo jobInfo = JobAdminConfig.getAdminConfig().getJobInfoMapper().loadById(jobId);
        if (jobInfo == null) {
            logger.warn(">>>>>>>>>>>> trigger fail, jobId invalid,jobId={}", jobId);
            return;
        }
        // 解密账号密码
        if (GlueTypeEnum.BEAN.getDesc().equals(jobInfo.getGlueType())) {
            //解密账密
            String json = JSONUtils.changeJson(jobInfo.getJobJson(), JSONUtils.decrypt);
            jobInfo.setJobJson(json);
        }
        // 设置执行参数
        if (StringUtils.isNotBlank(executorParam)) {
            jobInfo.setExecutorParam(executorParam);
        }
        // 重试次数,如果有值就用该值,否则就用默认值
        int finalFailRetryCount = failRetryCount >= 0 ? failRetryCount : jobInfo.getExecutorFailRetryCount();
        // 获取执行器信息
        JobGroup group = JobAdminConfig.getAdminConfig().getJobGroupMapper().load(jobInfo.getJobGroup());

        // 处理分片参数
        int[] shardingParam = null;
        if (executorShardingParam != null) {
            String[] shardingArr = executorShardingParam.split("/");
            if (shardingArr.length == 2 && isNumeric(shardingArr[0]) && isNumeric(shardingArr[1])) {
                shardingParam = new int[2];
                shardingParam[0] = Integer.valueOf(shardingArr[0]);
                shardingParam[1] = Integer.valueOf(shardingArr[1]);
            }
        }
        // 循环执行器配置的服务地址列表,调用JobTrigger.processTrigger()
        if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null)
                && group.getRegistryList() != null && !group.getRegistryList().isEmpty()
                && shardingParam == null) {
            for (int i = 0; i < group.getRegistryList().size(); i++) {
                processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size());
            }
        } else {
        // 非广播模式调用JobTrigger.processTrigger()
            if (shardingParam == null) {
                shardingParam = new int[]{0, 1};
            }
            processTrigger(group, jobInfo, finalFailRetryCount, triggerType, shardingParam[0], shardingParam[1]);
        }

    }
4.2.3 JobTrigger.processTrigger()
private static void processTrigger(JobGroup group, JobInfo jobInfo, int finalFailRetryCount, TriggerTypeEnum triggerType, int index, int total) {

        TriggerParam triggerParam = new TriggerParam();

        // 设置阻塞策略、路由策略、分片参数
        ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), ExecutorBlockStrategyEnum.SERIAL_EXECUTION);  // block strategy
        ExecutorRouteStrategyEnum executorRouteStrategyEnum = ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null);    // route strategy
        String shardingParam = (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) ? String.valueOf(index).concat("/").concat(String.valueOf(total)) : null;

        // 1 保存任务id
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        calendar.set(Calendar.MILLISECOND, 0);
        Date triggerTime = calendar.getTime();
        JobLog jobLog = new JobLog();
        jobLog.setJobGroup(jobInfo.getJobGroup());
        jobLog.setJobId(jobInfo.getId());
        jobLog.setTriggerTime(triggerTime);
        jobLog.setJobDesc(jobInfo.getJobDesc());

        JobAdminConfig.getAdminConfig().getJobLogMapper().save(jobLog);
        logger.debug(">>>>>>>>>>> datax-web trigger start, jobId:{}", jobLog.getId());

        // 2、初始化触发参数
        triggerParam.setJobId(jobInfo.getId());
        triggerParam.setExecutorHandler(jobInfo.getExecutorHandler());
        triggerParam.setExecutorParams(jobInfo.getExecutorParam());
        triggerParam.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy());
        triggerParam.setExecutorTimeout(jobInfo.getExecutorTimeout());
        triggerParam.setLogId(jobLog.getId());
        triggerParam.setLogDateTime(jobLog.getTriggerTime().getTime());
        triggerParam.setGlueType(jobInfo.getGlueType());
        triggerParam.setGlueSource(jobInfo.getGlueSource());
        triggerParam.setGlueUpdatetime(jobInfo.getGlueUpdatetime().getTime());
        triggerParam.setBroadcastIndex(index);
        triggerParam.setBroadcastTotal(total);
        triggerParam.setJobJson(jobInfo.getJobJson());

        // 设置增量参数,id或者时间
        Integer incrementType = jobInfo.getIncrementType();
        if (incrementType != null) {
            triggerParam.setIncrementType(incrementType);
            if (IncrementTypeEnum.ID.getCode() == incrementType) {
                long maxId = getMaxId(jobInfo);
                jobLog.setMaxId(maxId);
                triggerParam.setEndId(maxId);
                triggerParam.setStartId(jobInfo.getIncStartId());
            } else if (IncrementTypeEnum.TIME.getCode() == incrementType) {
                triggerParam.setStartTime(jobInfo.getIncStartTime());
                triggerParam.setTriggerTime(triggerTime);
                triggerParam.setReplaceParamType(jobInfo.getReplaceParamType());
            } else if (IncrementTypeEnum.PARTITION.getCode() == incrementType) {
                triggerParam.setPartitionInfo(jobInfo.getPartitionInfo());
            }
            triggerParam.setReplaceParam(jobInfo.getReplaceParam());
        }
        //jvm参数
        triggerParam.setJvmParam(jobInfo.getJvmParam());

        // 3、设置执行器服务地址
        String address = null;
        ReturnT<String> routeAddressResult = null;
        if (group.getRegistryList() != null && !group.getRegistryList().isEmpty()) {
            if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) {
                if (index < group.getRegistryList().size()) {
                    address = group.getRegistryList().get(index);
                } else {
                    address = group.getRegistryList().get(0);
                }
            } else {
                routeAddressResult = executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList());
                if (routeAddressResult.getCode() == ReturnT.SUCCESS_CODE) {
                    address = routeAddressResult.getContent();
                }
            }
        } else {
            routeAddressResult = new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("jobconf_trigger_address_empty"));
        }

        // 4 调用JobTrigger.runExecutor()方法触发远程执行器执行
        ReturnT<String> triggerResult = null;
        if (address != null) {
            triggerResult = runExecutor(triggerParam, address);
        } else {
            triggerResult = new ReturnT<String>(ReturnT.FAIL_CODE, null);
        }

        // 5 搜集触发信息
        StringBuffer triggerMsgSb = new StringBuffer();
        triggerMsgSb.append(I18nUtil.getString("jobconf_trigger_type")).append(":").append(triggerType.getTitle());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IpUtil.getIp());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regtype")).append(":")
                .append((group.getAddressType() == 0) ? I18nUtil.getString("jobgroup_field_addressType_0") : I18nUtil.getString("jobgroup_field_addressType_1"));
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regaddress")).append(":").append(group.getRegistryList());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorRouteStrategy")).append(":").append(executorRouteStrategyEnum.getTitle());
        if (shardingParam != null) {
            triggerMsgSb.append("(" + shardingParam + ")");
        }
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorBlockStrategy")).append(":").append(blockStrategy.getTitle());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_timeout")).append(":").append(jobInfo.getExecutorTimeout());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorFailRetryCount")).append(":").append(finalFailRetryCount);

        triggerMsgSb.append("<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>" + I18nUtil.getString("jobconf_trigger_run") + "<<<<<<<<<<< </span><br>")
                .append((routeAddressResult != null && routeAddressResult.getMsg() != null) ? routeAddressResult.getMsg() + "<br><br>" : "").append(triggerResult.getMsg() != null ? triggerResult.getMsg() : "");

        // 6 保存日志到日志表
        jobLog.setExecutorAddress(address);
        jobLog.setExecutorHandler(jobInfo.getExecutorHandler());
        jobLog.setExecutorParam(jobInfo.getExecutorParam());
        jobLog.setExecutorShardingParam(shardingParam);
        jobLog.setExecutorFailRetryCount(finalFailRetryCount);
        jobLog.setTriggerCode(triggerResult.getCode());
        jobLog.setTriggerMsg(triggerMsgSb.toString());
        JobAdminConfig.getAdminConfig().getJobLogMapper().updateTriggerInfo(jobLog);

        logger.debug(">>>>>>>>>>> datax-web trigger end, jobId:{}", jobLog.getId());
    }
4.2.4 JobTrigger.runExecutor()

public static ReturnT<String> runExecutor(TriggerParam triggerParam, String address) {
        ReturnT<String> runResult = null;
        try {
        	// 根据地址创建代理类ExecutorBiz实例
            ExecutorBiz executorBiz = JobScheduler.getExecutorBiz(address);
            // 调用代理类的ExecutorBiz.run()方法
            runResult = executorBiz.run(triggerParam);
        } catch (Exception e) {
            logger.error(">>>>>>>>>>> datax-web trigger error, please check if the executor[{}] is running.", address, e);
            runResult = new ReturnT<String>(ReturnT.FAIL_CODE, ThrowableUtil.toString(e));
        }

        StringBuffer runResultSB = new StringBuffer(I18nUtil.getString("jobconf_trigger_run") + ":");
        runResultSB.append("<br>address:").append(address);
        runResultSB.append("<br>code:").append(runResult.getCode());
        runResultSB.append("<br>msg:").append(runResult.getMsg());

        runResult.setMsg(runResultSB.toString());
        return runResult;
    }
4.2.5 ExecutorBiz.run()

这里是调用的执行器启动时发布的http服务,也就是执行器端ExecutorBiz的实现类的ExecutorBizImpl.run()方法

4.2.6 ExecutorBizImpl.run()
@Override
public ReturnT<String> run(TriggerParam triggerParam) {
        // load old:jobHandler + jobThread
        JobThread jobThread = JobExecutor.loadJobThread(triggerParam.getJobId());
        IJobHandler jobHandler = jobThread != null ? jobThread.getHandler() : null;
        String removeOldReason = null;

        // valid:jobHandler + jobThread
        GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType());
        // datax-web调用data使用的是BEAN模式。根据任务参数里executorHandler从ConcurrentMap缓存里获取对应的类。
        if (GlueTypeEnum.BEAN == glueTypeEnum) {

            // new jobhandler
            IJobHandler newJobHandler = JobExecutor.loadJobHandler(triggerParam.getExecutorHandler());

            // valid old jobThread
            if (jobThread != null && jobHandler != newJobHandler) {
                // change handler, need kill old thread
                removeOldReason = "change jobhandler or glue type, and terminate the old job thread.";

                jobThread = null;
                jobHandler = null;
            }

            // valid handler
            if (jobHandler == null) {
                jobHandler = newJobHandler;
                if (jobHandler == null) {
                    return new ReturnT<>(ReturnT.FAIL_CODE, "job handler [" + triggerParam.getExecutorHandler() + "] not found.");
                }
            }

        } else if (GlueTypeEnum.GLUE_GROOVY == glueTypeEnum) {

            // valid old jobThread
            if (jobThread != null &&
                    !(jobThread.getHandler() instanceof GlueJobHandler
                            && ((GlueJobHandler) jobThread.getHandler()).getGlueUpdatetime() == triggerParam.getGlueUpdatetime())) {
                // change handler or gluesource updated, need kill old thread
                removeOldReason = "change job source or glue type, and terminate the old job thread.";

                jobThread = null;
                jobHandler = null;
            }

            // valid handler
            if (jobHandler == null) {
                try {
                    IJobHandler originJobHandler = GlueFactory.getInstance().loadNewInstance(triggerParam.getGlueSource());
                    jobHandler = new GlueJobHandler(originJobHandler, triggerParam.getGlueUpdatetime());
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                    return new ReturnT<String>(ReturnT.FAIL_CODE, e.getMessage());
                }
            }
        } else if (glueTypeEnum != null && glueTypeEnum.isScript()) {

            // valid old jobThread
            if (jobThread != null &&
                    !(jobThread.getHandler() instanceof ScriptJobHandler
                            && ((ScriptJobHandler) jobThread.getHandler()).getGlueUpdatetime() == triggerParam.getGlueUpdatetime())) {
                // change script or gluesource updated, need kill old thread
                removeOldReason = "change job source or glue type, and terminate the old job thread.";

                jobThread = null;
                jobHandler = null;
            }

            // valid handler
            if (jobHandler == null) {
                jobHandler = new ScriptJobHandler(triggerParam.getJobId(), triggerParam.getGlueUpdatetime(), triggerParam.getGlueSource(), GlueTypeEnum.match(triggerParam.getGlueType()));
            }
        } else {
            return new ReturnT<>(ReturnT.FAIL_CODE, "glueType[" + triggerParam.getGlueType() + "] is not valid.");
        }

        // executor block strategy
        if (jobThread != null) {
            ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(triggerParam.getExecutorBlockStrategy(), null);
            if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) {
                // discard when running
                if (jobThread.isRunningOrHasQueue()) {
                    return new ReturnT<>(ReturnT.FAIL_CODE, "block strategy effect:" + ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle());
                }
            } else if (ExecutorBlockStrategyEnum.COVER_EARLY == blockStrategy) {
                // kill running jobThread
                if (jobThread.isRunningOrHasQueue()) {
                    removeOldReason = "block strategy effect:" + ExecutorBlockStrategyEnum.COVER_EARLY.getTitle();

                    jobThread = null;
                }
            } else {
                // just queue trigger
            }
        }

        // 注册新线程取代旧线程
        if (jobThread == null) {
            jobThread = JobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);
        }

        // push data to queue
        ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam);
        return pushResult;
    }
4.2.7 JobExecutor.registJobThread()

注册任务线程

public static JobThread registJobThread(int jobId, IJobHandler handler, String removeOldReason) {
		// 根据任务id和handler创建任务线程
        JobThread newJobThread = new JobThread(jobId, handler);
        // 启动任务线程
        newJobThread.start();
        logger.info(">>>>>>>>>>> datax-web regist JobThread success, jobId:{}, handler:{}", new Object[]{jobId, handler});

        JobThread oldJobThread = jobThreadRepository.put(jobId, newJobThread);    // putIfAbsent | oh my god, map's put method return the old value!!!
        if (oldJobThread != null) {
            oldJobThread.toStop(removeOldReason);
            oldJobThread.interrupt();
        }

        return newJobThread;
    }
4.2.8 JobThread.run()
@Override
public void run() {

        // init
        try {
            handler.init();
        } catch (Throwable e) {
            logger.error(e.getMessage(), e);
        }

        // execute
        while (!toStop) {
            running = false;
            idleTimes++;

            TriggerParam tgParam = null;
            ReturnT<String> executeResult = null;
            try {
                // to check toStop signal, we need cycle, so wo cannot use queue.take(), instand of poll(timeout)
                tgParam = triggerQueue.poll(3L, TimeUnit.SECONDS);
                if (tgParam != null) {
                    running = true;
                    idleTimes = 0;
                    triggerLogIdSet.remove(tgParam.getLogId());

                    // log filename, like "logPath/yyyy-MM-dd/9999.log"
                    String logFileName = JobFileAppender.makeLogFileName(new Date(tgParam.getLogDateTime()), tgParam.getLogId());
                    JobFileAppender.contextHolder.set(logFileName);
                    ShardingUtil.setShardingVo(new ShardingUtil.ShardingVO(tgParam.getBroadcastIndex(), tgParam.getBroadcastTotal()));

                    // 执行任务
                    JobLogger.log("<br>----------- datax-web job execute start -----------<br>----------- Param:" + tgParam.getExecutorParams());

                    if (tgParam.getExecutorTimeout() > 0) {
                        // limit timeout
                        Thread futureThread = null;
                        try {
                            final TriggerParam tgParamT = tgParam;
                            // 如果是datax-web任务,这里调用的就是ExecutorJobHandler.execute()如果是其他类的任务调用的是创建任务时指定的JobHandler.execute()
                            FutureTask<ReturnT<String>> futureTask = new FutureTask<>(() -> handler.execute(tgParamT));
                            futureThread = new Thread(futureTask);
                            futureThread.start();

                            executeResult = futureTask.get(tgParam.getExecutorTimeout(), TimeUnit.MINUTES);
                        } catch (TimeoutException e) {

                            JobLogger.log("<br>----------- datax-web job execute timeout");
                            JobLogger.log(e);

                            executeResult = new ReturnT<>(IJobHandler.FAIL_TIMEOUT.getCode(), "job execute timeout ");
                        } finally {
                            futureThread.interrupt();
                        }
                    } else {
                        // just execute
                        executeResult = handler.execute(tgParam);
                    }

                    if (executeResult == null) {
                        executeResult = IJobHandler.FAIL;
                    } else {
                        executeResult.setMsg(
                                (executeResult != null && executeResult.getMsg() != null && executeResult.getMsg().length() > 50000)
                                        ? executeResult.getMsg().substring(0, 50000).concat("...")
                                        : executeResult.getMsg());
                        executeResult.setContent(null);    // limit obj size
                    }
                    JobLogger.log("<br>----------- datax-web job execute end(finish) -----------<br>----------- ReturnT:" + executeResult);

                } else {
                    if (idleTimes > 30) {
                        if (triggerQueue.size() == 0) {    // avoid concurrent trigger causes jobId-lost
                            JobExecutor.removeJobThread(jobId, "executor idel times over limit.");
                        }
                    }
                }
            } catch (Throwable e) {
                if (toStop) {
                    JobLogger.log("<br>----------- JobThread toStop, stopReason:" + stopReason);
                }

                StringWriter stringWriter = new StringWriter();
                e.printStackTrace(new PrintWriter(stringWriter));
                String errorMsg = stringWriter.toString();
                executeResult = new ReturnT<>(ReturnT.FAIL_CODE, errorMsg);

                JobLogger.log("<br>----------- JobThread Exception:" + errorMsg + "<br>----------- datax-web job execute end(error) -----------");
            } finally {
                // 终止操作暂不监控状态
                if (tgParam != null && tgParam.getJobId() != -1) {
                    // callback handler info
                    if (!toStop) {
                        // commonm
                        TriggerCallbackThread.pushCallBack(new HandleCallbackParam(tgParam.getLogId(), tgParam.getLogDateTime(), executeResult));
                    } else {
                        // is killed
                        ReturnT<String> stopResult = new ReturnT<String>(ReturnT.FAIL_CODE, stopReason + " [job running, killed]");
                        TriggerCallbackThread.pushCallBack(new HandleCallbackParam(tgParam.getLogId(), tgParam.getLogDateTime(), stopResult));
                    }
                }
            }
        }

        // callback trigger request in queue
        while (triggerQueue != null && triggerQueue.size() > 0) {
            TriggerParam triggerParam = triggerQueue.poll();
            if (triggerParam != null) {
                // is killed
                ReturnT<String> stopResult = new ReturnT<String>(ReturnT.FAIL_CODE, stopReason + " [job not executed, in the job queue, killed.]");
                TriggerCallbackThread.pushCallBack(new HandleCallbackParam(triggerParam.getLogId(), triggerParam.getLogDateTime(), stopResult));
            }
        }

        // destroy
        try {
            handler.destroy();
        } catch (Throwable e) {
            logger.error(e.getMessage(), e);
        }

        logger.info(">>>>>>>>>>> datax-web JobThread stoped, hashCode:{}", Thread.currentThread());
    }
4.2.9 ExecutorJobHandler.execute()
@Override
public ReturnT<String> execute(TriggerParam trigger) {

        int exitValue = -1;
        Thread errThread = null;
        String tmpFilePath;
        LogStatistics logStatistics = null;
        //生成json临时文件
        tmpFilePath = generateTemJsonFile(trigger.getJobJson());

        try {
        	// 构建执行命令
            String[] cmdarrayFinal = buildDataXExecutorCmd(trigger, tmpFilePath,dataXPyPath);
            // 执行datax任务
            final Process process = Runtime.getRuntime().exec(cmdarrayFinal);
            String prcsId = ProcessUtil.getProcessId(process);
            JobLogger.log("------------------DataX process id: " + prcsId);
            jobTmpFiles.put(prcsId, tmpFilePath);
            //update datax process id
            HandleProcessCallbackParam prcs = new HandleProcessCallbackParam(trigger.getLogId(), trigger.getLogDateTime(), prcsId);
            ProcessCallbackThread.pushCallBack(prcs);
            // log-thread
            Thread futureThread = null;
            FutureTask<LogStatistics> futureTask = new FutureTask<>(() -> analysisStatisticsLog(new BufferedInputStream(process.getInputStream())));
            futureThread = new Thread(futureTask);
            futureThread.start();

            errThread = new Thread(() -> {
                try {
                    analysisStatisticsLog(new BufferedInputStream(process.getErrorStream()));
                } catch (IOException e) {
                    JobLogger.log(e);
                }
            });

            logStatistics = futureTask.get();
            errThread.start();
            // process-wait
            exitValue = process.waitFor();      // exit code: 0=success, 1=error
            // log-thread join
            errThread.join();
        } catch (Exception e) {
            JobLogger.log(e);
        } finally {
            if (errThread != null && errThread.isAlive()) {
                errThread.interrupt();
            }
            //  删除临时文件
            if (FileUtil.exist(tmpFilePath)) {
                FileUtil.del(new File(tmpFilePath));
            }
        }
        if (exitValue == 0) {
            return new ReturnT<>(200, logStatistics.toString());
        } else {
            return new ReturnT<>(IJobHandler.FAIL.getCode(), "command exit value(" + exitValue + ") is failed");
        }
    }

5 查看执行日志执行流程

页面点击日志查看按钮,请求的api是/api/log/logDetailCat 对应的是JobLogController.logDetailCat()

5.1 JobLogController.logDetailCat()

@RequestMapping(value = "/logDetailCat", method = RequestMethod.GET)
    @ApiOperation("运行日志详情")
    public ReturnT<LogResult> logDetailCat(String executorAddress, long triggerTime, long logId, int fromLineNum) {
        try {
        	// 根据指定地址获取远程服务代理类ExecutorBiz的实例ExecutorBizImpl
            ExecutorBiz executorBiz = JobScheduler.getExecutorBiz(executorAddress);
            // 调用ExecutorBizImpl.log()
            ReturnT<LogResult> logResult = executorBiz.log(triggerTime, logId, fromLineNum);

            // is end
            if (logResult.getContent() != null && fromLineNum > logResult.getContent().getToLineNum()) {
                JobLog jobLog = jobLogMapper.load(logId);
                if (jobLog.getHandleCode() > 0) {
                    logResult.getContent().setEnd(true);
                }
            }

            return logResult;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return new ReturnT<>(ReturnT.FAIL_CODE, e.getMessage());
        }
    }

5.2 ExecutorBizImpl.log()

@Override
    public ReturnT<LogResult> log(long logDateTim, long logId, int fromLineNum) {
        // log filename: logPath/yyyy-MM-dd/9999.log
        // 生成日志文件名
        String logFileName = JobFileAppender.makeLogFileName(new Date(logDateTim), logId);
		// 调用JobFileAppender.readLog() 一行一行读取文件
        LogResult logResult = JobFileAppender.readLog(logFileName, fromLineNum);
        return new ReturnT<>(logResult);
    }

6 任务增删改查

JobInfoController

	//添加
    JobInfoController.add()
    //查询
    JobInfoController.list()
    //修改
    JobInfoController.update()
    //删除
    JobInfoController.remove()

参考资料

datax-web swagger接口文档(接口文档需要启动服务才可以看到)
http://127.0.0.1:8381/doc.html#/default/%E4%BB%BB%E5%8A%A1%E9%85%8D%E7%BD%AE%E6%8E%A5%E5%8F%A3/addUsingPOST

xxl-job 参考文档
https://www.xuxueli.com/xxl-job/

xxl-job 原理系列博客
https://www.jianshu.com/p/d70b78a704e5

xxl-job 任务执行流程博客
https://blog.csdn.net/god_86/article/details/114650439

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
DataX Web是在DataX之上开发的分布式数据同步工具,提供简单易用的操作界面,降低用户使用DataX的学习成本,缩短任务配置时间,避免配置过程中出错。用户可通过页面选择数据源即可创建数据同步任务,RDBMS数据源可批量创建数据同步任务,支持实时查看数据同步进度及日志并提供终止同步功能,集成并二次开发xxl-job可根据时间、自增主键增量同步数据。 任务"执行器"支持集群部署,支持执行器多节点路由策略选择,支持超时控制、失败重试、失败告警、任务依赖,执行器CPU.内存.负载的监控等等。后续还将提供更多的数据源支持、数据转换UDF、表结构同步、数据同步血缘等更为复杂的业务场景。 DataX Web安装环境: Language: Java 8(jdk版本建议1.8.201以上) Python2.7(支持Python3需要修改替换datax/bin下面的三个python文件,替换文件在doc/datax-web/datax-python3下) Environment: MacOS, Windows,Linux Database: Mysql5.7 DataX Web功能特点: 1、通过Web构建DataX Json; 2、DataX Json保存在数据库中,方便任务的迁移,管理; 3、Web实时查看抽取日志,类似Jenkins的日志控制台输出功能; 4、DataX运行记录展示,可页面操作停止DataX作业; 5、支持DataX定时任务,支持动态修改任务状态、启动/停止任务,以及终止运行中任务,即时生效; 6、调度采用中心式设计,支持集群部署; 7、任务分布式执行,任务"执行器"支持集群部署; 8、执行器会周期性自动注册任务, 调度中心将会自动发现注册的任务并触发执行; 9、路由策略:执行器集群部署时提供丰富的路由策略,包括:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等; 10、阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度; 11、任务超时控制:支持自定义任务超时时间,任务运行超时将会主动中断任务; 12、任务失败重试:支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试; 13、任务失败告警;默认提供邮件方式失败告警,同时预留扩展接口,可方便的扩展短信、钉钉等告警方式; 14、用户管理:支持在线管理系统用户,存在管理员、普通用户两种角色; 15、任务依赖:支持配置子任务依赖,当父任务执行结束且执行成功后将会主动触发一次子任务的执行, 多个子任务用逗号分隔; 16、运行报表:支持实时查看运行数据,以及调度报表,如调度日期分布图,调度成功分布图等; 17、指定增量字段,配置定时任务自动获取每次的数据区间,任务失败重试,保证数据安全; 18、页面可配置DataX启动JVM参数; 19、数据源配置成功后添加手动测试功能; 20、可以对常用任务进行配置模板,在构建完JSON之后可选择关联模板创建任务; 21、jdbc添加hive数据源支持,可在构建JSON页面选择数据源生成column信息并简化配置; 22、优先通过环境变量获取DataX文件目录,集群部署时不用指定JSON及日志目录; 23、通过动态参数配置指定hive分区,也可以配合增量实现增量数据动态插入分区; 24、任务类型由原来DataX任务扩展到Shell任务、Python任务、PowerShell任务; 25、添加HBase数据源支持,JSON构建可通过HBase数据源获取hbaseConfig,column; 26、添加MongoDB数据源支持,用户仅需要选择collectionName即可完成json构建; 27、添加执行器CPU、内存、负载的监控页面; 28、添加24类插件DataX JSON配置样例 29、公共字段(创建时间,创建人,修改时间,修改者)插入或更新时自动填充 30、对swagger接口进行token验证 31、任务增加超时时间,对超时任务kill datax进程,可配合重试策略避免网络问题导致的datax卡死。 32、添加项目管理模块,可对任务分类管理; 33、对RDBMS数据源增加批量任务创建功能,选择数据源,表即可根据模板批量生成DataX同步任务; 34、JSON构建增加ClickHouse数据源支持; 35、执行器CPU.内存.负载的监控页面图形化; 36、RDBMS数据源增量抽取增加主键自增方式并优化页面参数配置; 37、更换MongoDB数据源连接方式,重构HBase数据源JSON构建模块; 38、脚本类型任务增加停止功能; 39、rdbms json构建增加postSql,并支持构建多个preSq

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值