Datax调度与数据传输流程
调度指的是 Datax 根据数据(任务执行情况)来进行任务执行的顺序以及优先级;数据传输是指 reader 和 writer 是如何配合进行数据之间的交互,以及 Datax 的一些特性例如速率把控、并行操作等是如何实现的。
1. 调度流程
调度流程的代码入口在 JobContainer.java 的 schedule() 方法。
首先会获取全局的channel数、每个TaskGroup的channel数以及计算需要的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 使用了 TaskGroupContainerRunner 将 Configuration 封装成一个 TaskGroupContainer,同时 TaskGroupContainerRunner 的 state 属性代表其 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,在TaskExecutor的dostart()中可以看到两个执行线程writerThread和readerThread。
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 中,实际上 Reader 和 Writer 是通过 channel 来链接。插件不必关心channel的具体实现。插件通过 RecordSender 往 channel 写入数据,通过 RecordReceiver 从 channel 读取数据。
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_SQL、username、password 等参数, 进行连接,然后执行 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 实现了 RecordSender 和 RecordReceiver, 同时它有两个实现类 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;
本文详细介绍了Datax的调度流程,从调度代码入口到任务执行,以及数据传输流程,包括初始化、读写任务的具体实现和Exchanger交互。Datax采用生产者-消费者模型,通过Exchanger进行数据交互,实现高效并行和速率控制。
4082

被折叠的 条评论
为什么被折叠?



