DataX可以支持基本所有常用数据库作为数据源。具体支持的数据源,可查:https://help.aliyun.com/document_detail/137670.html
可以在github上看 具体插件的源码和使用文档:https://github.com/alibaba/DataX
强烈建议在 仔细浏览官方使用文档 对DataX有初步认识后,再看本文。
如果是仅仅简单了解DataX,可以直接下载dataX的工具包,然后执行
cd {YOUR_DATAX_HOME}/bin
python datax.py {YOUR_JOB.json}
而无需下载源码编译运行。
这是第一次我亲手debug工具源码。充满好奇与欣喜,所以想这记录下此时此情。
首先我在DataX的github的文档 userGuid.md文档可以知道,DataX有工具包和DataX源码。如果你想深入了解DataX,建议将这两者都下载来。DataX源码建议使用git下拉,这样方便项目直接识别为多模块的Maven项目。下拉后,建议直接对父项目进行 mvn clean,然后 mvn install。
项目的目录结构如上图,我们可以看到项目中最多就是 各种数据源对应的writer、reader。首先我们可能需要先了解DataX的核心模块 core。因为这可能帮助我们了解DataX的实现原理。
core模块下的 core/src/main/bin/datax.py 就是我们调用DataX开始任务的python脚本。
可以看出其实该脚本还是调用 java 来执行 主函数 com.alibaba.datax.core.Engine 。知道 命令行运行时候输入的参数 及入口函数后,就可以去看看入口函数,进行debug。
在Engine的main函数中
try {
//这里我设置的是 我另外下载的 DataX工具包的位置
System.setProperty("datax.home", "E:\\development\\datax");
//设置datax的运行脚本信息 这里的最后一个入参 是我自定义的 job.json ,如果这里你没有编写的话,可以使用 job 目录下自带的 job.json
args = new String[]{"-mode", "standalone", "-jobid", "-1", "-job", "D:\\learning\\DataX\\core\\src\\main\\job\\mysql_mysql_job.json"};
Engine.entry(args);
} catch (Throwable e) {
//下面代码不用改动
// ******
}
这里看到了我在路径 D:\learning\DataX\core\src\main\job\mysql_mysql_job.json 中准备了 mysql_mysql_job.json 这是一个作业配置文件,该文件说了 本次作业数据传输的双方的地址、传输的内容等等。这里我给出我的 作业json内容,你可以当作一个模板去使用。
{
"job": {
"content": [
{
"reader": {
"name": "mysqlreader",
"parameter": {
"connection": [
{
"jdbcUrl": ["jdbc:mysql://localhost:3306/data1?serverTimezone=Asia/Shanghai&useLegacyDatetimeCode=false&useSSL=false&nullNamePatternMatchesAll=true&useUnicode=true&characterEncoding=UTF-8"],
"querySql":["SELECT c2.name AS countyName,c1.name AS cityName FROM county c2 LEFT JOIN city c1 ON c2.cityId = c1.id"]
}
],
"password": "111111",
"username": "root"
}
},
"writer": {
"name": "mysqlwriter",
"parameter": {
"column": [
"countyName","cityName"
],
"connection": [
{
"jdbcUrl": "jdbc:mysql://localhost:3306/data2?serverTimezone=Asia/Shanghai&useLegacyDatetimeCode=false&useSSL=false&nullNamePatternMatchesAll=true&useUnicode=true&characterEncoding=UTF-8",
"table": ["county_info"]
}
],
"password": "111111",
"preSql": [],
"session": [],
"username": "root",
"writeMode": "insert"
}
}
}
],
"setting": {
"speed": {
"channel": "10"
}
}
}
}
如果你没有理解我的意思,或者觉得debug步骤繁琐,可以直接拉去我的dataX代码,在其中我已经做好了这些工作。当然其中也包括了一些我自己的注释。 gitee地址:https://gitee.com/kxrofearch/dataX.git
这可以说是core模块中最重要的类。job实例运行在jobContainer容器中,它是所有任务的master,负责初始化、拆分、调度、运行、回收、监控和汇报。但它并不做实际的数据同步操作。
// 可以观察一下,他的部分成员变量
private ClassLoaderSwapper classLoaderSwapper = ClassLoaderSwapper
.newCurrentThreadClassLoaderSwapper();
private long jobId;
private String readerPluginName;
private String writerPluginName;
/**
* reader和writer jobContainer的实例
*/
private Reader.Job jobReader;
private Writer.Job jobWriter;
从成员变量中:
private long startTimeStamp;
private long endTimeStamp;
private long startTransferTimeStamp;
private long endTransferTimeStamp;
其实我们也可以看到很多 时间信息 这是用于各种信息统计的。datax提供了完善了监控和汇报功能。
而在类中start方法,可以看出JobContainer的总体运行流程:
//从源码中截取,去除debug输出
userConf = configuration.clone();
this.preHandle();
this.init();
this.prepare();
this.schedule();
this.post();
this.postHandle();
preHandle:前置处理(预处理)。 根据配置文件中job.preHandler.pluginName 加载已经存在的插件,并执行插件的preHandler方法。这里目前官方的mysqlReader也未实现,所以暂不讨论。
init :reader和writer的初始化。 生成 Reader.Job、Writer.Job 实例,并设置 reader 的 jobConfig、readerConfig 、job监控 以及调用 reader插件自己的 init 方法。同理,也会对 writer 进行类似操作。
MysqlReader 的内部类 Job 中有成员变量 CommonRdbmsReader.Job,而 init 方法 主要就是调用 CommonRdbmsReader 的 init 方法。该方法将会对 配置信息进行校验(数据库账户密码、数据库url、采用的是table还是querySql模式、column配置等等 )。说明一下:如果使用的是 querySql,如果语句的错误,在执行时可能才被发现。
与此相类似,MysqlWriter初始化时 也会调用CommonRdbmsWriter的 init 方法,该方法也会 对配置信息进行校验,主要包括(bathchsize,数据库url、表名、写入模式 [ 仅支持replace,update 或 insert 方式],将写入的列名 等等)
**prepare:进行准备工作。**这里 MySqlReader 未做准备操作。但是MysqlWriter 调用了commonRdbmsWriterJob 的 prepare 方法:在单表情况下,执行 自己配置的 preSql (写入数据到目的表前,会先执行这里的preSql,比如写入前 先清空表)
split: 这个是及其重要的部分:它会计算作业需要的channel数目及将作业拆分成多个task。
1.划分的首要是确认 needchannelNumber(需要使用的线程总数)。你可以在配置文件中配置byte、record或者channel 来调整 needchannelNumber。如果你使用 byte或record方式,则也必须配置 单通道的最大byte或record大小,通过公式 通道数量 = 全局速度/单通道最高速度。如果两者都配置了,则通过计算之后,谁的 needchannelNumber 小就采用何种方式。如果前两者都未配置,则由配置项 channel 决定。如果channel都不存在,则直接报异常
2.计算得到的 needChannelNumber 并不代表最终的作业切分数目,其最终还是由 reader 插件的来实现作业的具体切分,框架只给一个建议切分的任务数。 之所以采取这个建议是为了给用户最好的实现,例如框架根据计算认为用户数据存储可以支持100个并发连接,并且用户认为需要100个并发。 此时,插件开发人员如果能够根据上述切分规则进行切分并做到>=100连接信息,DataX就可以同时启动100个Channel,这样给用户最好的吞吐量。例如用户同步一张Mysql单表,但是认为可以到10并发吞吐量,插件开发人员最好对该表进行切分,比如使用主键范围切分,并且如果最终切分任务数到>=10,我们就可以提供给用户最大的吞吐量。
3.这里看一下 MysqlReader 是如何具体划分的。MysqlReader 采用调用 CommonRdbmsReader的splite 方法:
- 如果为 table模式(初步猜测,是json文件中是否声明了 将要读的表名 ),则会通过表的数量,首先确定单表划分的个数 eachTableShouldSplittedNumber = (adviceNumber / tableNumber)(向上取整)。如果table = 1,且 SplitFactor未配置的话,则 将单表划分为 eachTableShouldSplittedNumber * splitFactor 份,然后尝试对单表切分。如果table>1,则直接将多表都切分成 eachTableShouldSplittedNumber 份。
- 如果不为 table模式,如果为通过SQL语句查询,则切分是看 配置文件中 querySql中sql的数量。这里说明一下:在切分时,时通过轮询 connect 时,会抽取jdbcUrl 中的 ip/port 进行资源使用的打标,以提供给 core 做有意义的 shuffle操作。
3.当读插件计算后,写插件必须使用相同的taskNumber,以保证1:1的通道模型。
schedule: schedule首先完成的工作是把上一步reader和writer split的结果整合到具体taskGroupContainer中,同时不同的执行模式,会使用不同的调度策略将所有任务调度起来。
1.默认每个任务组的并发量控制为5个,总task数小于总并发数,则按task数目进行并发。最终组数 = 总并发数/任务组最大并发数。通过获取各task的配置信息,公平的分配 task 到对应的 taskGroup中。公平体现在:会考虑 task 中对资源负载做 load 标识进行更加均衡的作业分配。
2.如何标记资源标识呢?DataX是分别让 reader、writer 建立资源名称–> taskId(List)的map映射关系。谁对资源做的标记最多(即map.size()最大),则使用 谁的资源标记。使用不同资源的 tasks 按轮询方式尽量分配到不同的 taskGroup。这样以保证 某一个时刻 所有 taskGroup 内进行都有较好并发性能。
/** * 需要实现的效果通过例子来说是: * * a 库上有表:0, 1, 2 * b 库上有表: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 * * */ private static List<Configuration> doAssign(LinkedHashMap<String, List<Integer>> resourceMarkAndTaskIdMap, Configuration jobConfiguration, int taskGroupNumber)
3.因为 taskGroupNumber = channel/组内最大并发 ,所以可能会产生余数(代表有多余的并发 未使用)。这是 DataX 也会针对 needChannel % taskGroupNumber !=0 的情况进行优化,将多余 channel 分配给 编号靠前的 taskGroup。
**post:**读写完成后。这里 MysqlReader 未进行任何操作。MysqlWriter 调用 CommonRdbmsWriter 的 post方法,执行 postSql(如果i是单表或者未配置 postSql ,则不执行)
destory: 任务彻底结束前。这里 MysqReader、MySqlWrite 都未做实现。
介绍:RDBMSReader插件实现了从RDBMS读取数据。在底层实现上,RDBMSReader通过 JDBC连接远程RDBMS数据库,并执行相应的sql语句将数据从RDBMS库中 SELECT出来。目前支持达梦、db2、PPAS、Sybase数据库的读取。RDBMSReader是一个通用的关系数据库读插件,您可以通过注册数据库驱动等方式增加任意多样的关系数据库读支持。
实现原理:
简而言之,RDBMSReader通过 JDBC 连接器连接到远程的 RDBMS 数据库,并根据用户配置的信息生成查询 SELECT SQL语句并发送到远程 RDBMS数据库,并将该 SQL执行返回结果使用 DataX自定义的数据类型拼装为抽象的数据集,并传递给下游 Writer处理。
对于用户配置 Table、Column、Where的信息,RDBMSReader将其拼接为 SQL语句发送到RDBMS数据库;对于用户配置 querySql信息,RDBMS直接将其发送到RDBMS数据库。
事实上,MySqlReader 自身的实现内容并不多,其很多功能实现是 直接采用 CommonRdbmsReader 中的函数。
CommonRdbmsReader.startRead 方法:
创建Connection、Session,执行sql语句,得到查询的结果集。轮询结果集时,将数据构造成一条记录(也会处理数据类型 [buildRecord 方法]),并将记录发送到缓冲区。
Connection conn = DBUtil.getConnection(this.dataBaseType, jdbcUrl,
username, password);
DBUtil.dealWithSessionConfig(conn, readerSliceConfig,
this.dataBaseType, basicMsg);
rs = DBUtil.query(conn, querySql, fetchSize)
/** **/
while (rs.next()) {
rsNextUsedTime += (System.nanoTime() - lastTime);
this.transportOneRecord(recordSender, rs,
metaData, columnNumber, mandatoryEncoding, taskPluginCollector);
lastTime = System.nanoTime();
}
// buildRecord 方法中部分内容:
case Types.DECIMAL:
record.addColumn(new DoubleColumn(rs.getString(i)));
break;
case Types.BIGINT:
record.addColumn(new LongColumn(rs.getString(i)));
break;
// record 结构:
{
"data": [{
"byteSize": 3,
"rawData": "滨湖区",
"type": "STRING"
}, {
"byteSize": 3,
"rawData": "合肥市",
"type": "STRING"
}],
"size": 2
}
这里发送数据的类 有多种,这里介绍的是 BufferedRecordExchanger。sendToWriter方法中可以 了解记录会被积累下来,直到 积累的记录(buffer)大小大于 byteCapacity ,才将 buffer 通过 Channel类的 pullAll 方法将数据发送到队列,如果队列已经满了,则将会堵塞在这里。说明:内存Channel的具体实现,底层其实是一个ArrayBlockingQueue
public void sendToWriter(Record record) {
if(shutdown){
throw DataXException.asDataXException(CommonErrorCode.SHUT_DOWN_TASK, "");
}
Validate.notNull(record, "record不能为空.");
if (record.getMemorySize() > this.byteCapacity) {
this.pluginCollector.collectDirtyRecord(record, new Exception(String.format("单条记录超过大小限制,当前限制为:%s", this.byteCapacity)));
return;
}
boolean isFull = (this.bufferIndex >= this.bufferSize || this.memoryBytes.get() + record.getMemorySize() > this.byteCapacity);
if (isFull) {
flush();
}
this.buffer.add(record); //缓存下来的 record
this.bufferIndex++;
memoryBytes.addAndGet(record.getMemorySize());
}
protected void doPullAll(Collection<Record> rs) {
assert rs != null;
rs.clear();
try {
long startTime = System.nanoTime();
lock.lockInterruptibly();
while (this.queue.drainTo(rs, bufferSize) <= 0) {
notEmpty.await(200L, TimeUnit.MILLISECONDS);
}
waitReaderTime += System.nanoTime() - startTime;
int bytes = getRecordBytes(rs);
memoryBytes.addAndGet(-bytes);
notInsufficient.signalAll();
} catch (InterruptedException e) {
throw DataXException.asDataXException(
FrameworkErrorCode.RUNTIME_ERROR, e);
} finally {
lock.unlock();
}
}
因为这里的调用链较长,这里说明一下: CommonRdbmsReader.startRead -> this.transportOneRecord -> this.buildRecord -> RecordSender.sendToWriter -> BufferedRecordExchanger.sendToWriter -> this.flush -> Channel.pushAll -> this.doPushAll
介绍:RDBMSWriter 插件实现了写入数据到 RDBMS 主库的目的表的功能。在底层实现上, RDBMSWriter 通过 JDBC 连接远程 RDBMS 数据库,并执行相应的 insert into … 的 sql 语句将数据写入 RDBMS。 RDBMSWriter是一个通用的关系数据库写插件,您可以通过注册数据库驱动等方式增加任意多样的关系数据库写支持。RDBMSWriter 面向ETL开发工程师,他们使用 RDBMSWriter 从数仓导入数据到 RDBMS。同时 RDBMSWriter 亦可以作为数据迁移工具为DBA等用户提供服务。
实现原理:RDBMSWriter 通过 DataX 框架获取 Reader 生成的协议数据,RDBMSWriter 通过 JDBC 连接远程 RDBMS 数据库,并执行相应的 insert into … 的 sql 语句将数据写入 RDBMS。
ReaderSplitUtil是公共的工具类,他可以帮助CommonRdbmsReader去做主要是帮助reader插件 切分单表 进行并发读取。
因为可以配置多数据库连接,支持多数据库同时连接。多个url中的不同的ip/port就可以视为不同物理资源,以提供给core做有意义的shuffle操作。
DataX中三大主体——Job、TaskGroup、Task三者之间或者上下级是如何通信呢?
DataX中提供了一个基类AbstractContainerCommunicator来处理 JobContainer、TaskGroupContainer和Task的通讯。AbstractContainerCommunicator提供了注册、收集信息等接口,信息的单位是Communication(一个类)。AbstractContainerCommunicator主要将其功能委托给其主要包含两个模块:
AbstractContainerCommunicator#collector
collector 负责管理下级注册到上级,搜集并合并下级所有的信息。dataX提供一个基类AbstractCollector和一个实现类ProcessInnerCollector。实现类ProcessInnerCollector只实现了一个方法collectFromTaskGroup。AbstractCollector同时包含将Task注册到TaskGroupContainer和将TaskGroupContainer注册到JobContainer的功能。
AbstractContainerCommunicator#reporter
reporter 负责将下级信息上报到上级。dataX提供一个基类AbstractReporter及其实现类ProcessInnerReporter。实现类ProcessInnerReporter 只实现了一个方法reportTGCommunication 将 TaskGroup的Communication更新到taskGroupCommunicationMap。
前面说,CommonRdbmsReader与CommonRdbmsWriter的工作流程,但是可能有一个疑问,他们是如何交互的呢?这里可能就需要TaskGroupContainer中内容:
在TaskGroupContainer初始化时,注册了StandaloneTGContainerCommunicator,用于taskGroup与task之间的通信。这也是TaskGroupContainer run 的前提。同时
JobContainer将所有的task分配各个TaskGroupContainer中执行,TaskGroupContainer启动5个线程去消费这些task。TaskGroupContainer的入口方法是 start()
TaskExecutor:TaskExecutor是一个完整task的执行器,其中包括1:1的reader和writer。
1.获取task配置 来初始化该 task的 taskCommunication,并将 taskCommunication 传给 readerRunner和writerRunner以及channel使用。
2.生成 writerRunner、readerRunner 以及 writerThread、readerThread:
/** * 生成writerThread */ writerRunner = (WriterRunner) generateRunner(PluginType.WRITER); // * this.writerThread = new Thread(writerRunner, String.format("%d-%d-%d-writer", jobId, taskGroupId, this.taskId)); //通过设置thread的contextClassLoader,即可实现同步和主程序不通的加载器 this.writerThread.setContextClassLoader(LoadUtil.getJarLoader( PluginType.WRITER, this.taskConfig.getString( CoreConstant.JOB_WRITER_NAME))); /** * 生成readerThread */ readerRunner = (ReaderRunner) generateRunner(PluginType.READER,transformerInfoExecs); this.readerThread = new Thread(readerRunner, String.format("%d-%d-%d-reader", jobId, taskGroupId, this.taskId)); /** * 通过设置thread的contextClassLoader,即可实现同步和主程序不通的加载器 */ this.readerThread.setContextClassLoader(LoadUtil.getJarLoader( PluginType.READER, this.taskConfig.getString( CoreConstant.JOB_READER_NAME)));
3.深入代码,你可以发现在 readerThread、writerThread被实例化前,readerRunner、writerRunner的过程中分别生成了recordSender、recordReceiver,这两个实例共同持有一个channel,可以查看channel的具体实现-MemoryChannel,其底层其实是一个ArrayBlockingQueue-基于数组的堵塞式队列。channel作为reader和writer的通信组件,要求reader与writer保持一对一关系,reader向channel写入数据(channel提供push方法),writer从channel获取数据(channel提供pull方法)。
额外说明一下:
public void push(final Record r) { Validate.notNull(r, "record不能为空."); this.doPush(r); this.statPush(1L, r.getByteSize()); }
在push时,channel会检查读取速度(它通过Communication记录总的写入数据大小和数据条数。然后每隔一段时间,检查速度),如果速度过快,就会sleep一段时间,把速度降下来(前提:设置了channel字节速度限制)。但是在pull时,速度是不受限制的。
篇外:
优化mysqlwriter:https://blog.csdn.net/Shadow_Light/article/details/100749537
主要是针对 mysql客户端每次从服务端拿数据时,每次都只拿1条数据,这会在取数据时造成严重的网络开销。其实针对mysql drive5.0以上,已经有了解决方法
python ./bin/datax.py -r mysqlreader -w hdfswriter //通过命令即可查询 该读写插件默认的 json文档了
{
"job": {
"content": [
{
"reader": {
"name": "mysqlreader", //本次所读数据库 对应的插件
"parameter": {
"column": [], //需要同步的列名集合,使用JSON数组声明,可用*代表所有列
"connection": [
{
"jdbcUrl": [], //数据库的JDBC连接的数据,使用JSON数组声明,可支持多个连接地址
"table": [], //支持同步的表,可支持多个
"querySql":[] //自定义SQL,配置之后将通过SQL语句进行查询.会选择忽略已经配置
//的table、column、where (可选)
}
],
"password": "",
"username": "",
"where": "", //筛选条件:通过where字段实现 全量、增量同步 (可选)
"querySql":[] //数据分片字段,一般是主键,仅支持整型.作业一般切分成多个Task,该字段可
//作为切分的凭据,让Task切分更加均匀 (可选)
}
},
"writer": {
"name": "hdfswriter", //本次所写数据库 对应的插件
"parameter": {
"column": [], //写入数据的字段,其中name指定字段名,type指定类型
"compress": "",
"defaultFS": "",
"fieldDelimiter": "",
"fileName": "",
"fileType": "",
"path": "",
"writeMode": ""
}
}
}
],
"setting": {
"speed": {
"channel": ""
}
}
}
}
// * to ElasticSearch
{
"job": {
"setting": {
"speed": {
"channel": 1
}
},
"content": [
{
"reader": {
...
},
"writer": {
"name": "elasticsearchwriter",
"parameter": {
"endpoint": "http://xxx:9999", //ElasticSearch的连接地址
"accessId": "xxxx", //http auth中的user
"accessKey": "xxxx", //http auth中的password
"index": "test-1", //elasticsearch中的index名
"type": "default", //elasticsearch中index的type名
"cleanup": true, //是否删除原表
"settings": {"index" :{"number_of_shards": 1, "number_of_replicas": 0}},//创建index时候的settings,
//与elasticsearch官方相同
"discovery": false, //启用节点发现将(轮询)并定期更新客户机中的服务器列表。
"batchSize": 1000, //每次批量数据的条数
"splitter": ",", //如果插入数据是array,就使用指定分隔符
"column": [
{"name": "pk", "type": "id"},
{ "name": "col_ip","type": "ip" },
{ "name": "col_double","type": "double" },
{ "name": "col_long","type": "long" },
{ "name": "col_integer","type": "integer" },
{ "name": "col_keyword", "type": "keyword" },
{ "name": "col_text", "type": "text", "analyzer": "ik_max_word"},
{ "name": "col_geo_point", "type": "geo_point" },
{ "name": "col_date", "type": "date", "format": "yyyy-MM-dd HH:mm:ss"},
{ "name": "col_nested1", "type": "nested" },
{ "name": "col_nested2", "type": "nested" },
{ "name": "col_object1", "type": "object" },
{ "name": "col_object2", "type": "object" },
{ "name": "col_integer_array", "type":"integer", "array":true},
{ "name": "col_geo_shape", "type":"geo_shape", "tree": "quadtree", "precision": "10m"}
]
}
}
}
]
}
}
@Author:[email protected]
该文档还会持续更新内容
如何有疑问可以在评论区询问