源码调试
VM Options:
-Ddatax.home=D:\workspaces\DataX\target\datax\datax
Args Options:
-mode standalone -jobid -1 -job D:\datax-jobs\stream2stream.json
Main Lancher:
com.alibaba.datax.core.Engine
- mysql2stream.json
{
"job": {
"content": [
{
"reader": {
"name": "streamreader",
"parameter": {
"sliceRecordCount": 10,
"column": [
{
"type": "long",
"value": "10"
},
{
"type": "string",
"value": "hello,你好,世界-DataX"
}
]
}
},
"writer": {
"name": "streamwriter",
"parameter": {
"encoding": "UTF-8",
"print": true
}
}
}
],
"setting": {
"speed": {
"channel": 5
}
}
}
}
DataX执行总体步骤
- preHandle()
- init() 需重点关注
- prepare()
- split() 需重点关注
- schedule() 需重点关注
- post()
- postHandle()
- invokeHooks()
init() 初始化过程
Job的init()过程主要做了两个事情,分别是:
1、创建reader的job对象。
2、创建writer的job对象。
通过 组件类型【read/write/transformer】、组件名称、ContainerType【Job/task】三要素来确定唯一组件,通过ClassLoader 获取对应Class
e.g mysqlreader 可以找到路径为 d a t a x . h o m e / p l u g i n s / m y s q l r e a d e r / 并 加 载 J o b L o a d e r c l a s s p a t h , 从 中 查 找 名 为 c o m . a l i b a b a . d a t a x . p l u g i n . r e a d e r . m y s q l r e a d e r . M y s q l R e a d e r {datax.home}/plugins/mysqlreader/ 并加载 JobLoader classpath, 从中查找名为 com.alibaba.datax.plugin.reader.mysqlreader.MysqlReader datax.home/plugins/mysqlreader/并加载JobLoaderclasspath,从中查找名为com.alibaba.datax.plugin.reader.mysqlreader.MysqlReaderJob 的 类, 并调用 getInstance() 方法进行实例的创建。init() 方法进行初始化
split() 任务切分
DataX的job的split过程主要是根据限流配置计算channel的个数,进而计算task的个数,主要过程如下:
1、adjustChannelNumber的过程根据按照字节限流和record限流计算channel的个数。
2、reader的个数根据channel的个数进行计算。
3、writer的个数根据reader的个数进行计算,writer和reader实现1:1绑定。
4、通过mergeReaderAndWriterTaskConfigs()方法生成reader+writer的task的configuration,至此我们生成了task的配置信息。
任务切分的主要目的是得到关键结果 totalStage
执行reader和writer最细粒度的切分,需要注意的是,writer的切分结果要参照reader的切分结果,达到切分后数目相等,才能满足1:1的通道模型,所以这里可以将reader和writer的配置整合到一起,然后,为避免顺序给读写端带来长尾影响,将整合的结果shuffler掉
- 计算所需通道数
com.alibaba.datax.core.job.JobContainer#adjustChannelNumber()
关键参数
属性 | 配置参数 | 默认值 | 作用 | 备注 |
---|---|---|---|---|
globalLimitedByteSpeed | job.setting.speed.byte | 10 * 1024 * 1024 | 总bps值 | |
globalLimitedRecordSpeed | job.setting.speed.record | 100000 | 总tps值 | |
channelLimitedByteSpeed | core.transport.channel.speed.byte | 单个channel的bps值 | ||
channelLimitedRecordSpeed | core.transport.channel.speed.record | 单个channel的tps值 | ||
needChannelNumber | job.setting.speed.channel | 在以上四个参数的计算结果结果大于 231-1 时生效 | ||
/* com.alibaba.datax.core.job.JobContainer#adjustChannelNumber */
// 总bps限速 除以 单个channel的bps值 后取整
needChannelNumberByByte = (int) (globalLimitedByteSpeed / channelLimitedByteSpeed);
needChannelNumberByByte = needChannelNumberByByte > 0 ? needChannelNumberByByte : 1;
needChannelNumberByRecord = (int) (globalLimitedRecordSpeed / channelLimitedRecordSpeed);
needChannelNumberByRecord = needChannelNumberByRecord > 0 ? needChannelNumberByRecord : 1;
// 取较小值
needChannelNumber = needChannelNumberByByte < needChannelNumberByRecord ?
needChannelNumberByByte : needChannelNumberByRecord;
-
进行Reader/Writer分割
List<Configuration> com.alibaba.datax.core.job.JobContainer#doReaderSplit(int needChannelNumber); List<Configuration> com.alibaba.datax.core.job.JobContainer#doWriterSplit(int readerTaskNumber)
主要是通过调用加载的组件的split()方法进行各自的分割
与读分割操作不同的是,写分割的分割因子是 读分割其分割后的配置数量
List<Configuration> readerTaskConfigs = this.doReaderSplit(this.needChannelNumber);
int taskNumber = readerTaskConfigs.size();
List<Configuration> writerTaskConfigs = this.doWriterSplit(taskNumber);
- 合并Reader / Writer / transformer 分割后得到的 Configuration集合
List<Configuration> mergeReaderAndWriterTaskConfigs(
List<Configuration> readerTasksConfigs,
List<Configuration> writerTasksConfigs,
List<Configuration> transformerConfigs) {
//...
}
代码完成的事情主要是:
- 校验 Reader 分割后配置集合 与 Writer 分割后配置集合是否数量相同
- 合并配置
- 返回合并后配置长度作为 totalStage
关键合并参数:
配置参数 | 备注 |
---|---|
reader.name | Reader名称 |
reader.parameter | Reader配置信息 |
writer.name | Writer名称 |
writer.parameter | Writer配置信息 |
transformer | Transformer配置信息 |
schedule()
schedule首先完成的工作是把上一步reader和writer split的结果整合到具体taskGroupContainer中, 同时不同的执行模式调用不同的调度策略,将所有任务调度起来
关键参数
属性 | 配置参数 | 默认值 | 作用 | 备注 |
---|---|---|---|---|
channelsPerTaskGroup | core.container.taskGroup.channel | 5 | ||
taskNumber | job.content | 配置的content个数 | ||
readerResourceMark | reader.parameter.loadBalanceResourceMark | aFakeResourceMarkForLoadBalance | 资源名称 | 第一个Job Configuration 内的配置 |
writerResourceMark | writer.parameter.loadBalanceResourceMark | 资源名称 | 第一个Job Configuration 内的配置 | |
- 任务分组
法:com.alibaba.datax.core.container.util.JobAssignUtil#assignFairly(Configuration configuration, int channelNumber, int channelsPerTaskGroup)
公平的分配 task 到对应的 taskGroup 中。
公平体现在:会考虑 task 中对资源负载作的 load 标识进行更均衡的作业分配操作。
// 该任务分组数量计算
int taskGroupNumber = (int) Math.ceil(1.0 * needChannelNumber / channelsPerTaskGroup);
- 根据task 配置,获取到:资源名称 --> taskId(List) 的 map 映射关系, 返回根据资源名称拆分数量较多的集合
/**
* 根据task 配置,获取到:
* 资源名称 --> taskId(List) 的 map 映射关系
*/
private static LinkedHashMap<String, List<Integer>> parseAndGetResourceMarkAndTaskIdMap(List<Configuration> contentConfig) {
// ...
if (readerResourceMarkAndTaskIdMap.size() >= writerResourceMarkAndTaskIdMap.size()) {
// 采用 reader 对资源做的标记进行 shuffle
return readerResourceMarkAndTaskIdMap;
} else {
// 采用 writer 对资源做的标记进行 shuffle
return writerResourceMarkAndTaskIdMap;
}
}
- 分配任务
/**
* /**
* 需要实现的效果通过例子来说是:
* <pre>
* a 库上有表:0, 1, 2
* a 库上有表:3, 4
* c 库上有表:5, 6, 7
*
* 如果有 4个 taskGroup
* 则 assign 后的结果为:
* taskGroup-0: 0, 4,
* taskGroup-1: 3, 6,
* taskGroup-2: 5, 2,
* taskGroup-3: 1, 7
*
* </pre>
*
* 任务分配
* @param resourceMarkAndTaskIdMap 根据资源名称拆分数量较多的集合
* @param jobConfiguration 任务配置
* @param taskGroupNumber 任务分组数量
* @return
*/
private static List<Configuration> doAssign(LinkedHashMap<String, List<Integer>> resourceMarkAndTaskIdMap, Configuration jobConfiguration, int taskGroupNumber) {
// ...
}
- 任务调度
初始化调度器 (com.alibaba.datax.core.job.scheduler.processinner.StandAloneScheduler)
方法:com.alibaba.datax.core.job.scheduler.AbstractScheduler#schedule
主要做的事情:
- 构建计数器 ErrorRecordChecker
- 给 taskGroupContainer 的 Communication 注册(监听者模式,可能用于信息上报等操作)
- 计算任务数
- 执行组任务(com.alibaba.datax.core.job.scheduler.processinner.ProcessInnerScheduler#startAllTaskGroup(List configurations))
- 任务上报/异常监控
- checkLimit() 检查任务执行情况
通过之前注册的 Communication,进行异常检查
检查最终结果是否超出阈值,如果阈值设定小于1,则表示百分数阈值,大于1表示条数阈值。
post()
注意:此方法每个 Task 都会执行一次。
最佳实践:如果 Task 中有需要进行数据同步之后的后续处理,可以在此处完成。
postHandle()
从源代码看出,这个操作主要是将 DefaultJobPluginCollector 对象注入到了插件当中,后进行了 postHandler 操作,应用场景暂无。
invokeHooks() 调用外部hook
主要原理是 JavaSPI
扩展路径: ${datax.home}/hook
参考文档
-
DataX的执行流程分析 https://www.jianshu.com/p/b10fbdee7e56
-
一看你就懂,超详细java中的ClassLoader详解 https://blog.youkuaiyun.com/briblue/article/details/54973413