DataX核心流程之源码分析

257 阅读6分钟

DataX的同步过程实际可以抽象为一个生产者与消费者模型,在其基础之上做了很多扩展和封装。

整体流程

image.png 上图展示了Datax同步数据的整个流程。在官网给的流程上做了细化。整个同步工作分为如下几步:

  1. Job切分:这部分是在JobContaier中完成的

将任务构建的整个作业切分为单独的任务,每个任务只包含一个输入和输出源;

将任务重新打包组合到不同的任务组里,任务组负责管理它下面所有任务的执行和状态管理等;

2.任务执行:这部分是在TaskGroupContainer中完成的

每个任务的执行是有一个叫做TaskExecutor的对象来管理的。它内部维护了一个reader线程和writer线程,以及一个Channel。reader读取的数据写入channel,writer则从channle中读取数据。

切分流程

切分的方法入口是在JobContainer中的split方法。

private int split() {
    //代码一
    this.adjustChannelNumber();
    
    if (this.needChannelNumber <= 0) {
        this.needChannelNumber = 1;
    }
    //代码二
    List<Configuration> readerTaskConfigs = this
            .doReaderSplit(this.needChannelNumber);
    int taskNumber = readerTaskConfigs.size();
    List<Configuration> writerTaskConfigs = this
            .doWriterSplit(taskNumber);
    List<Configuration> transformerList = this.configuration.getListConfiguration(CoreConstant.DATAX_JOB_CONTENT_TRANSFORMER);
    LOG.debug("transformer configuration: "+ JSON.toJSONString(transformerList));
    /**
     * 输入是reader和writer的parameter list,输出是content下面元素的list
     */
     //代码三
    List<Configuration> contentConfig = mergeReaderAndWriterTaskConfigs(
            readerTaskConfigs, writerTaskConfigs, transformerList);


    LOG.debug("contentConfig configuration: "+ JSON.toJSONString(contentConfig));
    this.configuration.set(CoreConstant.DATAX_JOB_CONTENT, contentConfig);
    return contentConfig.size();
}

 

在代码一处,根据限流参数先计算一个切分数

 

  1. 先查看用户配置作业中有无配置字节限流,如果有配置的话,读取core.json配置中每个channel的字节限速配置
job.setting.speed.byte
core.transport.channel.speed.byte
  1. 计算针对字节限速配置需要的通道数
needChannelNumberByByte =
        (int) (globalLimitedByteSpeed / channelLimitedByteSpeed);
needChannelNumberByByte =
        needChannelNumberByByte > 0 ? needChannelNumberByByte : 1;
  1. 查看是否配置了记录数限流,如果有配置的话,读取core.json配置中的channel记录限速配置
job.setting.speed.record
core.transport.channel.speed.record
  1. 计算针对记录数限速配置需要的通道数
 needChannelNumberByRecord =
        (int) (globalLimitedRecordSpeed / channelLimitedRecordSpeed);
needChannelNumberByRecord =
        needChannelNumberByRecord > 0 ? needChannelNumberByRecord : 1;
  1. 取上述两者中的数值小的数作为限流的通道数
this.needChannelNumber = needChannelNumberByByte < needChannelNumberByRecord ?
        needChannelNumberByByte : needChannelNumberByRecord;

7.如果用户没有设置上述两种限速配置,则查看用户是否配置了通道数,如果有的话,则将通道数暂时设置为用户配置的。

boolean isChannelLimit = (this.configuration.getInt(
        CoreConstant.DATAX_JOB_SETTING_SPEED_CHANNEL, 0) > 0);
if (isChannelLimit) {
    this.needChannelNumber = this.configuration.getInt(
            CoreConstant.DATAX_JOB_SETTING_SPEED_CHANNEL);


    LOG.info("Job set Channel-Number to " + this.needChannelNumber
            + " channels.");


    return;
}

上述过程的完成代码如下:

private void adjustChannelNumber() {
    int needChannelNumberByByte = Integer.MAX_VALUE;
    int needChannelNumberByRecord = Integer.MAX_VALUE;
    boolean isByteLimit = (this.configuration.getInt(
            CoreConstant.DATAX_JOB_SETTING_SPEED_BYTE, 0) > 0);
    if (isByteLimit) {
        long globalLimitedByteSpeed = this.configuration.getInt(
                CoreConstant.DATAX_JOB_SETTING_SPEED_BYTE, 10 * 1024 * 1024);
        // 在byte流控情况下,单个Channel流量最大值必须设置,否则报错!
        Long channelLimitedByteSpeed = this.configuration
                .getLong(CoreConstant.DATAX_CORE_TRANSPORT_CHANNEL_SPEED_BYTE);
        if (channelLimitedByteSpeed == null || channelLimitedByteSpeed <= 0) {
            throw DataXException.asDataXException(
                    FrameworkErrorCode.CONFIG_ERROR,
                    "在有总bps限速条件下,单个channel的bps值不能为空,也不能为非正数");
        }
        needChannelNumberByByte =
                (int) (globalLimitedByteSpeed / channelLimitedByteSpeed);
        needChannelNumberByByte =
                needChannelNumberByByte > 0 ? needChannelNumberByByte : 1;
        LOG.info("Job set Max-Byte-Speed to " + globalLimitedByteSpeed + " bytes.");
    }
    boolean isRecordLimit = (this.configuration.getInt(
            CoreConstant.DATAX_JOB_SETTING_SPEED_RECORD, 0)) > 0;
    if (isRecordLimit) {
        long globalLimitedRecordSpeed = this.configuration.getInt(
                CoreConstant.DATAX_JOB_SETTING_SPEED_RECORD, 100000);
        Long channelLimitedRecordSpeed = this.configuration.getLong(
                CoreConstant.DATAX_CORE_TRANSPORT_CHANNEL_SPEED_RECORD);
        if (channelLimitedRecordSpeed == null || channelLimitedRecordSpeed <= 0) {
            throw DataXException.asDataXException(FrameworkErrorCode.CONFIG_ERROR,
                    "在有总tps限速条件下,单个channel的tps值不能为空,也不能为非正数");
        }
        needChannelNumberByRecord =
                (int) (globalLimitedRecordSpeed / channelLimitedRecordSpeed);
        needChannelNumberByRecord =
                needChannelNumberByRecord > 0 ? needChannelNumberByRecord : 1;
        LOG.info("Job set Max-Record-Speed to " + globalLimitedRecordSpeed + " records.");
    }
    // 取较小值
    this.needChannelNumber = needChannelNumberByByte < needChannelNumberByRecord ?
            needChannelNumberByByte : needChannelNumberByRecord;
    // 如果从byte或record上设置了needChannelNumber则退出
    if (this.needChannelNumber < Integer.MAX_VALUE) {
        return;
    }
    boolean isChannelLimit = (this.configuration.getInt(
            CoreConstant.DATAX_JOB_SETTING_SPEED_CHANNEL, 0) > 0);
    if (isChannelLimit) {
        this.needChannelNumber = this.configuration.getInt(
                CoreConstant.DATAX_JOB_SETTING_SPEED_CHANNEL);
        LOG.info("Job set Channel-Number to " + this.needChannelNumber
                + " channels.");
        return;
    }
    throw DataXException.asDataXException(
            FrameworkErrorCode.CONFIG_ERROR,
            "Job运行速度必须设置");
}

在代码代码二处,获取到通道数后,调用job的回调函数split,,将此通道数作为参数传递给方法。

List<Configuration> readerTaskConfigs = this
        .doReaderSplit(this.needChannelNumber);
int taskNumber = readerTaskConfigs.size();
List<Configuration> writerTaskConfigs = this
        .doWriterSplit(taskNumber);

 

进入doReaderSplit方法

private List<Configuration> doReaderSplit(int adviceNumber) {
    classLoaderSwapper.setCurrentThreadClassLoader(LoadUtil.getJarLoader(
            PluginType.READER, this.readerPluginName));
    //调用用户定义的split方法
    List<Configuration> readerSlicesConfigs =
            this.jobReader.split(adviceNumber);
    if (readerSlicesConfigs == null || readerSlicesConfigs.size() <= 0) {
        throw DataXException.asDataXException(
                FrameworkErrorCode.PLUGIN_SPLIT_ERROR,
                "reader切分的task数目不能小于等于0");
    }
    LOG.info("DataX Reader.Job [{}] splits to [{}] tasks.",
            this.readerPluginName, readerSlicesConfigs.size());
    classLoaderSwapper.restoreCurrentThreadClassLoader();
    return readerSlicesConfigs;
}

在该方法内将回调用户Job中的split方法

 

在代码三处,该段代码会对用户的read、write和transform任务进行合并,一个reader对应一个writer和transformer,它们组合成一个任务task。然后为每个task分配一个任务ID,任务ID从0开始。

private List<Configuration> mergeReaderAndWriterTaskConfigs(
        List<Configuration> readerTasksConfigs,
        List<Configuration> writerTasksConfigs,
        List<Configuration> transformerConfigs) {
    if (readerTasksConfigs.size() != writerTasksConfigs.size()) {
        throw DataXException.asDataXException(
                FrameworkErrorCode.PLUGIN_SPLIT_ERROR,
                String.format("reader切分的task数目[%d]不等于writer切分的task数目[%d].",
                        readerTasksConfigs.size(), writerTasksConfigs.size())
        );
    }


    List<Configuration> contentConfigs = new ArrayList<Configuration>();
    for (int i = 0; i < readerTasksConfigs.size(); i++) {
        Configuration taskConfig = Configuration.newDefault();
        taskConfig.set(CoreConstant.JOB_READER_NAME,
                this.readerPluginName);
        taskConfig.set(CoreConstant.JOB_READER_PARAMETER,
                readerTasksConfigs.get(i));
        taskConfig.set(CoreConstant.JOB_WRITER_NAME,
                this.writerPluginName);
        taskConfig.set(CoreConstant.JOB_WRITER_PARAMETER,
                writerTasksConfigs.get(i));


        if(transformerConfigs!=null && transformerConfigs.size()>0){
            taskConfig.set(CoreConstant.JOB_TRANSFORMER, transformerConfigs);
        }


        taskConfig.set(CoreConstant.TASK_ID, i);
        contentConfigs.add(taskConfig);
    }


    return contentConfigs;
}

 

在完成具体的任务切分后,每个任务都有了一个任务Id,接下来就是将这些任务分配到不同的TaskGroup中。该步骤是在JobContainer的schedule方法中执行的

private void schedule() {
int channelsPerTaskGroup = this.configuration.getInt(
        CoreConstant.DATAX_CORE_CONTAINER_TASKGROUP_CHANNEL, 5);
int taskNumber = this.configuration.getList(
        CoreConstant.DATAX_JOB_CONTENT).size();
this.needChannelNumber = Math.min(this.needChannelNumber, taskNumber);
PerfTrace.getInstance().setChannelNumber(needChannelNumber);
/**
 * 通过获取配置信息得到每个taskGroup需要运行哪些tasks任务
 */
List<Configuration> taskGroupConfigs = JobAssignUtil.assignFairly(this.configuration,
        this.needChannelNumber, channelsPerTaskGroup);
        
......
scheduler = initStandaloneScheduler(this.configuration);
......
scheduler.schedule(taskGroupConfigs);
}

JobAssignUtil.assignFairly就是执行具体执行分配的方法,从名字可以看出,采用的是一种公平的分配方式。何为公平呢?在该方法中就是将每个task均匀的分配到每个taskGroup中。并且给每个taskGroup分配一个Id,Id值从0开始。

public static List<Configuration> assignFairly(Configuration configuration, int channelNumber, int channelsPerTaskGroup) {
    Validate.isTrue(configuration != null, "框架获得的 Job 不能为 null.");
    List<Configuration> contentConfig = configuration.getListConfiguration(CoreConstant.DATAX_JOB_CONTENT);
    Validate.isTrue(contentConfig.size() > 0, "框架获得的切分后的 Job 无内容.");
    Validate.isTrue(channelNumber > 0 && channelsPerTaskGroup > 0,
            "每个channel的平均task数[averTaskPerChannel],channel数目[channelNumber],每个taskGroup的平均channel数[channelsPerTaskGroup]都应该为正数");
    //计算taskGroup的数量
    int taskGroupNumber = (int) Math.ceil(1.0 * channelNumber / channelsPerTaskGroup);
    Configuration aTaskConfig = contentConfig.get(0);
    //为每个task增加一个资源标识,用于任务的分配
    String readerResourceMark = aTaskConfig.getString(CoreConstant.JOB_READER_PARAMETER + "." +
            CommonConstant.LOAD_BALANCE_RESOURCE_MARK);
    String writerResourceMark = aTaskConfig.getString(CoreConstant.JOB_WRITER_PARAMETER + "." +
            CommonConstant.LOAD_BALANCE_RESOURCE_MARK);
    boolean hasLoadBalanceResourceMark = StringUtils.isNotBlank(readerResourceMark) ||
            StringUtils.isNotBlank(writerResourceMark);
    if (!hasLoadBalanceResourceMark) {
        // fake 一个固定的 key 作为资源标识(在 reader 或者 writer 上均可,此处选择在 reader 上进行 fake)
        for (Configuration conf : contentConfig) {
            conf.set(CoreConstant.JOB_READER_PARAMETER + "." +
                    CommonConstant.LOAD_BALANCE_RESOURCE_MARK, "aFakeResourceMarkForLoadBalance");
        }
        // 是为了避免某些插件没有设置 资源标识 而进行了一次随机打乱操作
        Collections.shuffle(contentConfig, new Random(System.currentTimeMillis()));
    }
    LinkedHashMap<String, List<Integer>> resourceMarkAndTaskIdMap = parseAndGetResourceMarkAndTaskIdMap(contentConfig);
    //将任务分配到taskGroup中,具体的算法就是将任务均匀的分配到taskGroup中
    List<Configuration> taskGroupConfig = doAssign(resourceMarkAndTaskIdMap, configuration, taskGroupNumber);
    // 调整 每个 taskGroup 对应的 Channel 个数(属于优化范畴)
    adjustChannelNumPerTaskGroup(taskGroupConfig, channelNumber);
    return taskGroupConfig;
    }

任务执行

将任务分配到对应的任务组后,通过StandAloneScheduler调度器StartAllTaskGroup方法来执行这些任务。

@Override
public void startAllTaskGroup(List<Configuration> configurations) {
    this.taskGroupContainerExecutorService = Executors
            .newFixedThreadPool(configurations.size());


    for (Configuration taskGroupConfiguration : configurations) {
        TaskGroupContainerRunner taskGroupContainerRunner = newTaskGroupContainerRunner(taskGroupConfiguration);
        this.taskGroupContainerExecutorService.execute(taskGroupContainerRunner);
    }


    this.taskGroupContainerExecutorService.shutdown();
}

从上面的代码可以看出,每个taskGroup任务被装进了叫做TaskGroupContainerRunner 的对象中,并且每个taskGroup会独立地运行在线程里面。而最终这些任务将会在TaskGroupContainer中的start方法中被执行。该方法会监控任务的运行状态,执行任务,失败重试等操作。该方法中的代码比较长,下面看看执行任务的流程:

public void start() {
  ......
  while(true){
    //状态获取
    ......
    while(true){
 //有任务未执行,且正在运行的任务数小于最大通道限制
Iterator<Configuration> iterator = taskQueue.iterator();
while(iterator.hasNext() && runTasks.size() < channelNumber){
    Configuration taskConfig = iterator.next();
    Integer taskId = taskConfig.getInt(CoreConstant.TASK_ID);
    int attemptCount = 1;
    TaskExecutor lastExecutor = taskFailedExecutorMap.get(taskId);
    if(lastExecutor!=null){
        attemptCount = lastExecutor.getAttemptCount() + 1;
        long now = System.currentTimeMillis();
        long failedTime = lastExecutor.getTimeStamp();
        if(now - failedTime < taskRetryIntervalInMsec){  //未到等待时间,继续留在队列
            continue;
        }
        if(!lastExecutor.isShutdown()){ //上次失败的task仍未结束
            if(now - failedTime > taskMaxWaitInMsec){
                markCommunicationFailed(taskId);
                reportTaskGroupCommunication(lastTaskGroupContainerCommunication, taskCountInThisTaskGroup);
                throw DataXException.asDataXException(CommonErrorCode.WAIT_TIME_EXCEED, "task failover等待超时");
            }else{
                lastExecutor.shutdown(); //再次尝试关闭
                continue;
            }
        }else{
            LOG.info("taskGroup[{}] taskId[{}] attemptCount[{}] has already shutdown",
                    this.taskGroupId, taskId, lastExecutor.getAttemptCount());
        }
    }
    Configuration taskConfigForRun = taskMaxRetryTimes > 1 ? taskConfig.clone() : taskConfig;
   TaskExecutor taskExecutor = new TaskExecutor(taskConfigForRun, attemptCount);
    taskStartTimeMap.put(taskId, System.currentTimeMillis());
   taskExecutor.doStart();
    iterator.remove();
    runTasks.add(taskExecutor);
    //上面,增加task到runTasks列表,因此在monitor里注册。
    taskMonitor.registerTask(taskId, this.containerCommunicator.getCommunication(taskId));
    taskFailedExecutorMap.remove(taskId);
    LOG.info("taskGroup[{}] taskId[{}] attemptCount[{}] is started",
            this.taskGroupId, taskId, attemptCount);
}
    }
  }
  ......
}

taskGroup中的所有的任务将放入一个队列中,在不超过taskGroup设置的最大并行数时,将任务封装进TaskExecutor中,在TaskExecutor的doStart方法中执行具体的read和write操作。

public void doStart() {
    this.writerThread.start();


    // reader没有起来,writer不可能结束
    if (!this.writerThread.isAlive() || this.taskCommunication.getState() == State.FAILED) {
        throw DataXException.asDataXException(
                FrameworkErrorCode.RUNTIME_ERROR,
                this.taskCommunication.getThrowable());
    }


    this.readerThread.start();


    // 这里reader可能很快结束
    if (!this.readerThread.isAlive() && this.taskCommunication.getState() == State.FAILED) {
        // 这里有可能出现Reader线上启动即挂情况 对于这类情况 需要立刻抛出异常
        throw DataXException.asDataXException(
                FrameworkErrorCode.RUNTIME_ERROR,
                this.taskCommunication.getThrowable());
    }


}

TaskExcutoer才是task真正的执行器,每个执行器内部都有一个readThread和writeThread来执行输入的读和写,以及一个channel作为数据传输缓冲。在Reader中有一个RecordSender对象,用来执行将数据从数据源发送channel。在Writer中有一个RecordReceiver对象,它从channel读取数据,以便下游端使用。channel的本质实际就是一个阻塞队列,它和reader和writer的模型就是生产者与消费者的模型。

总结

其实从Datax的整个同步流程来看,本质上就是一个生产者(Reader)和消费着(Writer)的模型,Channel充当了数据缓冲队列的作用。在这个模型基础之上,框架提供了插件话的加载机制,使得DataX扩展性有了很大的提高。在任务管理管理和执行方面,通过将任务拆分,再重新进行组合,以便能更好的监控和管理任务。