Datax调度与数据传输流程

本文详细介绍了Datax的调度流程,从调度代码入口到任务执行,以及数据传输流程,包括初始化、读写任务的具体实现和Exchanger交互。Datax采用生产者-消费者模型,通过Exchanger进行数据交互,实现高效并行和速率控制。

Datax调度与数据传输流程

调度指的是 Datax 根据数据(任务执行情况)来进行任务执行的顺序以及优先级;数据传输是指 readerwriter 是如何配合进行数据之间的交互,以及 Datax 的一些特性例如速率把控、并行操作等是如何实现的。

1. 调度流程

调度流程的代码入口在 JobContainer.javaschedule() 方法。

首先会获取全局的channel数、每个TaskGroupchannel数以及计算需要的channelNumber个数

    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);

接着调用 JobAssignUtil 对每个 TaskGroup 进行 channel的轮询 式分配。

    List<Configuration> taskGroupConfigs = JobAssignUtil.assignFairly(this.configuration,
            this.needChannelNumber, channelsPerTaskGroup); 

然后实例化AbstractScheduler 进行调度。

    private void schedule() { 
        //...
        scheduler.schedule(taskGroupConfigs);
        //...
    }

Scheduler主要完成两个事情:

  • 监控流程信息
  • 启动所有任务
    int totalTasks = calculateTaskCount(configurations);  //计算任务总数
    startAllTaskGroup(configurations);      //开启所有任务

这里的startAllTaskGroup调用的是AbstractScheduler 的接口ProcessInnerScheduler#startAllTaskGroup方法

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

        for (Configuration taskGroupConfiguration : configurations) {
            TaskGroupContainerRunner taskGroupContainerRunner = newTaskGroupContainerRunner(taskGroupConfiguration);        //实例化 TaskGroupContainerRunner -> TaskGroupContaine
            this.taskGroupContainerExecutorService.execute(taskGroupContainerRunner);
        }

        this.taskGroupContainerExecutorService.shutdown();
    }

可以看出Datax 使用了 TaskGroupContainerRunnerConfiguration 封装成一个 TaskGroupContainer,同时 TaskGroupContainerRunnerstate 属性代表其 TaskGroupContainer 的状态。然后交给固定线程池去执行。

接下来是任务启动代码.

	@Override
	public void run() {
		try {
            Thread.currentThread().setName(
                    String.format("taskGroup-%d", this.taskGroupContainer.getTaskGroupId()));
            this.taskGroupContainer.start();
			this.state = State.SUCCEEDED;
		} catch (Throwable e) {
			this.state = State.FAILED;
			throw DataXException.asDataXException(
					FrameworkErrorCode.RUNTIME_ERROR, e);
		}
	}

这里的start主要完成两个事情

  • 初始化task执行相关的状态信息。包括代运行队列、失败队列、运行队列等等。
  • 循环检测所有任务的执行状态。当所有的执行完成从while中退出之后,再次全局汇报当前的任务状态

最终任务交给TaskGroup的执行员TaskExecutor,在TaskExecutordostart()中可以看到两个执行线程writerThreadreaderThread

        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());
            }

        }

至此,调度的流程结束,接下来是数据传输流程。

2. 数据传输流程

2.1 初始化

数据传输流程都在 TaskGroupContainer 里面,所以一些控制的指标参数、重试策略等配置会在这里体现和处理的更加细致。Datax 采用的生产者-消费者模型,这样做可以解耦、提高并行的效率以及控制处理数据的速率。在 TaskGroupContainer 中,实际上 ReaderWriter 是通过 channel 来链接。插件不必关心channel的具体实现。插件通过 RecordSenderchannel 写入数据,通过 RecordReceiverchannel 读取数据。

TaskGroupCountainer#start()是传输的入口,代表TaskGroup的启动。

start()方法主要完成:

  • 获取参数,配置监控器
  • 给各个参数封装成可执行的任务,并执行任务以及注册监控
  • 对任务的状态进行监控,进行重试机制的执行

具体的任务封装为TaskGroupContainer的内部类TaskExecutor

  class TaskExecutor {
        private Configuration taskConfig;

        private int taskId;

        private int attemptCount;

        private Channel channel;

        private Thread readerThread;

        private Thread writerThread;
        
        private ReaderRunner readerRunner;
        
        private WriterRunner writerRunner;

        private Communication taskCommunication;
        
        // method area
   }

TaskGroupContainer会遍历配置将切分的配置封装为一个TaskExecutor

                	TaskExecutor taskExecutor = new TaskExecutor(taskConfigForRun, attemptCount);
                    taskStartTimeMap.put(taskId, System.currentTimeMillis());
                	taskExecutor.doStart();

dostart()代码为

    public void doStart() {
        this.writerThread.start(); 
        
        if (!this.writerThread.isAlive() || this.taskCommunication.getState() == State.FAILED) {
            //throw exception
        }
        this.readerThread.start();  

        
        if (!this.readerThread.isAlive() && this.taskCommunication.getState() == State.FAILED) {
             //throw exception
        }
    }

分别开启读写线程,两个线程均是在TaskExecutor实例化的时候通过generateRunner多态来生成。

    newRunner = LoadUtil.loadPluginRunner(pluginType,
            this.taskConfig.getString(CoreConstant.JOB_WRITER_NAME)); 
    newRunner.setJobConf(this.taskConfig
            .getConfiguration(CoreConstant.JOB_WRITER_PARAMETER));
    pluginCollector = ClassUtil.instantiate(
            taskCollectorClass, AbstractTaskPluginCollector.class,
            configuration, this.taskCommunication,
            PluginType.WRITER);
    ((WriterRunner) newRunner).setRecordReceiver(new BufferedRecordExchanger(
            this.channel, pluginCollector));
    newRunner.setTaskPluginCollector(pluginCollector);
    
    // reader类似 但会多出下面部分
    RecordSender recordSender;
    if (transformerInfoExecs != null && transformerInfoExecs.size() > 0) {
        recordSender = new BufferedRecordTransformerExchanger(taskGroupId, this.taskId, this.channel,this.taskCommunication ,pluginCollector, transformerInfoExecs);
    } else {
        recordSender = new BufferedRecordExchanger(this.channel, pluginCollector);
    }

if -else判断这里会判断使用哪种 交换. 交换可以理解为外部与 channel 交互的中间件.BufferedRecordTransformerExchanger 指的是可以根据特定的格式转换。 BufferedRecordExchanger 仅仅是根据 Datax 的格式进行传输。

2.2 读写任务具体实现

当读写任务完成初始化,接下来就是其 具体的start 方法里面的内容了。先从读任务开始。读任务定义于 ReaderRunner,为了更加灵活,在 ReaderRunner 内也是使用插件机制的.如果我们需要拓展,只拓展 Reader.Task 即可。

 Reader.Task taskReader = (Reader.Task) this.getPlugin();

这里面包括具体的生命过程init()prepare()startRead()post()以及destory(),具体的执行逻辑在startRead()中.

    taskReader.startRead(recordSender);
   recordSender.terminate();

调用startRead方法需要传入交换内容.接下来就是具体的数据源实现,以Mysql的commonRdbmsReaderTask通用关系型数据库举例:

    		   String querySql = readerSliceConfig.getString(Key.QUERY_SQL);
    		   String table = readerSliceConfig.getString(Key.TABLE);
    		   Connection conn = DBUtil.getConnection(this.dataBaseType, jdbcUrl, username, password);


                rs = DBUtil.query(conn, querySql, fetchSize);

		       while (rs.next()) {
                    rsNextUsedTime += (System.nanoTime() - lastTime);
                    this.transportOneRecord(recordSender, rs,
                            metaData, columnNumber, mandatoryEncoding, taskPluginCollector);
                    lastTime = System.nanoTime();
                }

首先是获取 QUERY_SQLusernamepassword 等参数, 进行连接,然后执行 QUERY_SQL 获取数据,最后通过结果集rs执行transportOneRecord方法开始写入.

TransportOneRecord方法 实际上是通过 交互内容 进行交互.

    protected Record transportOneRecord(RecordSender recordSender, ResultSet rs, 
        ResultSetMetaData metaData, int columnNumber, String mandatoryEncoding, 
        TaskPluginCollector taskPluginCollector) {
        Record record = buildRecord(recordSender,rs,metaData,columnNumber,mandatoryEncoding,taskPluginCollector); 
        recordSender.sendToWriter(record);
        return record;
    }

写任务和读任务类似,需要关注缓存

    //使用 buffer 缓存, batchSize 是控制每次发送的数量
    List<Record> writeBuffer = new ArrayList<Record>(this.batchSize);
    
    while ((record = recordReceiver.getFromReader()) != null) {
        if (record.getColumnNumber() != this.columnNumber) {
            // throw error... 
        }
    }

如果符合条件的,即可添加进缓存. 最后写进数据库里

   doBatchInsert(connection, writeBuffer);
    writeBuffer.clear();
    bufferBytes = 0;
2.3 Exchanger交互

Exchanger 实现了 RecordSenderRecordReceiver, 同时它有两个实现类 BufferedRecordExchanger 以及 BufferedRecordTransformerExchanger.

RecordSender.java

public interface RecordSender {
    public Record createRecord();
    public void sendToWriter(Record record);
    public void flush();
    public void terminate();
    public void shutdown();
}

RecordReceiver.java

public interface RecordReceiver {
    public Record getFromReader();
    public void shutdown();
}

sendToWriter()getFromReader()是交互的重要方法,flush()控制缓冲区写入channel

SendToWriter 方法是负责将记录放进 channel. 首先会校验记录的大小是否超出限制

    if (record.getMemorySize() > this.byteCapacity) {
        this.pluginCollector.collectDirtyRecord(record, new Exception(String.format("单条记录超过大小限制,当前限制为:%s", this.byteCapacity)));
        return;
    }

然后判断channel是否满了。如果满了就刷新发送到channel

    boolean isFull = (this.bufferIndex >= this.bufferSize || this.memoryBytes.get() + record.getMemorySize() > this.byteCapacity);
    if (isFull) {
        flush();
    }

不满就加入到缓冲池队列中

    this.buffer.add(record);
    this.bufferIndex++;
    memoryBytes.addAndGet(record.getMemorySize());

GetFromReader 方法是给 writer 获取数据,进行下一步操作. 首先是排查队列是否为空. 如果为空, 然后调用方法将数据批量写入 writer 的缓冲区

    boolean isEmpty = (this.bufferIndex >= this.buffer.size());
    if (isEmpty) {
       receive();
    }

如果不为空, 根据索引读取,返回缓冲区中的一条.

    Record record = this.buffer.get(this.bufferIndex++);
    if (record instanceof TerminateRecord) {
            record = null;
    }
    return record;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值