1. 数据同步选型:为什么最终定下 DataX
做数据开发的人早晚都会遇到一个场景:业务库在 MySQL 里,数仓在 HDFS 上,每天要把订单表、用户表、行为日志同步过去;又或者数仓算完的结果要回写到 MySQL,给报表或线上服务用。两边互相倒腾数据这件事,看起来简单,真要自己写一套同步程序就折腾了——要处理并发、断点、脏数据、类型转换,还要能监控。
我最早用的是 Sqoop,后来也试过写 Flume 自定义 Source 硬推,但综合下来,DataX 成了我目前最顺手的选择。DataX 是阿里开源的一款离线数据同步工具,核心就是“读插件 + 框架 + 写插件”三件套,不同数据源之间的迁移本质上是把 Reader 读出来的数据经过框架的通道(Channel)交给 Writer。它不像 Flume 那样偏流式实时,也不像 Flume 那样主要解决日志采集,DataX 走的是离线批量同步路线,适合定时调度、全量或增量搬运的场景。
这个工具解决的最大痛点是"异构数据源之间的映射"。MySQL 和 HDFS 之间不仅仅是从一个库导到另一个文件系统,还涉及字段类型、字符编码、空值处理、文件格式(TextFile、ORC、Parquet)的转换。如果这些全靠手写 MapReduce 或 Spark 作业,开发成本根本不划算,维护成本更高。DataX 把这些问题抽象成了插件规范和 json 配置,改个配置就能换表换路径,对于绝大多数数据同步需求来说是性价比最高的方案。
适合什么读者?如果你是数据仓库工程师、ETL 开发,或者日常要和 MySQL、HDFS 打交道的平台开发,这篇内容能帮你少走不少弯路。下面所有配置都是我在真实同步作业中使用过的,我尽量把参数背后的道理也讲清楚。
2. 整体设计思路与核心概念拆解
2.1 DataX 的运行模型:Reader、Channel、Writer 是怎么协作的
先把 DataX 的底层模型说清楚。一次同步任务由 Job 和 Task 组成:Job 是总任务,负责拆分和调度;Task 是实际干活的最小单位。你可以把 Job 理解成一个项目经理,Task 就是具体执行的工人。Reader 负责从 MySQL 里查出数据,Writer 负责往 HDFS 写文件,中间的 Channel 是数据流的管道,负责把 Reader 吐出来的记录搬运给 Writer。
这个模型的好处是天然支持并发。比如 MySQL 全量导出一张 5000 万行的表,DataX 可以根据配置把表按主键范围或某个字段切分成多个分片,每个分片对应一个 Task,Task 并行跑。Channel 的数量决定了同时有多少个 Task 在跑,也就直接决定了吞吐量。我曾经在一个不规范的库上见过有人把 channel 调到 32,结果源库连接数直接爆掉,这是个非常常见的坑。
Channel 里还有一个关键参数叫byteCapacity和capacity,前者控制管道的字节缓冲,后者控制记录缓冲条数。如果读得快写得慢,数据会在 Channel 里积压;如果写得快读得慢,Channel 会空闲。调优的方向就是让两者速率匹配,而不是一味加大缓冲区,否则内存占用会高得吓人。
2.2 MySQL 到 HDFS 与 HDFS 到 MySQL 的差异对比
别看都是"互相迁移",两个方向的技术挑战完全不同。MySQL 往 HDFS 写,本质上是一个"并行查询 + 文件写出"的过程,难点在于分片策略、文件格式、字段类型映射;HDFS 往 MySQL 写,本质上是一个"分布式读取 + 批量写入关系库"的过程,难点在于解析速度、事务控制、主键冲突、目标库写入压力。
用生活类比来讲:MySQL 到 HDFS 相当于把一个大仓库的货物搬到空地上分堆堆放,搬的时候要考虑从哪个门出去、堆成什么码放格式;HDFS 到 MySQL 相当于把堆放的货物按规格重新装进仓库货架,装的时候要考虑货架的大小、送货的顺序、会不会把货架压塌。DataX 对这两个方向的处理逻辑是分开的,JSON 配置里reader和writer插件完全不同,所以不能拿一个配置改两下就两头用,必须分别设计。
另外,表结构信息也很关键。MySQL 到 HDFS 时,DataX 建议把 MySQL 表的元数据(字段名、类型)提前确保清晰,因为 HDFS 上如果是 TextFile,默认只有字段顺序没有字段名,下游读数据时全靠列位置对应。HDFS 到 MySQL 时则反过来,必须明确 HDFS 上每一列的类型,否则会出现把数字读成字符串导致入库失败。很多时候"数据同步没问题,下游跑数出错",根因就在字段映射环节。
3. 环境准备与核心配置逐项解析
3.1 安装与基本验证
DataX 的安装极其简单,本质上是解压一个发行包。前提是你机器上有 JDK 8 或更高版本,注意不要用过于新的 JDK,某些插件对高版本 Java 的模块化限制处理得不好,我踩过 JDK 17 的坑,后来老老实实装回 JDK 8。安装后进入目录,直接跑自检脚本验证:
cd /opt/datax python bin/datax.py job/job.json如果能看到类似 “任务启动时刻” 和 “任务结束时刻” 以及统计信息,说明安装没问题。官方自带的job/job.json是一个小 demo,从内存读取写内存,不是真实场景,但用来验证环境足够了。
这里忠告一句:别一上来就配 MySQL 同步任务,先把 demo 跑通,再用python bin/datax.py -r mysqlreader -w hdfswriter查看插件模板,能免去很多拼写错误。
3.2 理解 Job 配置里每个核心参数
一个标准的 MySQL 到 HDFS 任务,JSON 结构大概是:
{ "job": { "content": [ { "reader": { "name": "mysqlreader", "parameter": { "username": "sync_user", "password": "******", "connection": [ { "jdbcUrl": ["jdbc:mysql://192.168.1.10:3306/business?useUnicode=true&characterEncoding=utf8"], "table": ["orders"] } ], "column": ["id", "order_no", "user_id", "amount", "create_time"], "splitPk": "id", "where": "create_time >= '2024-01-01 00:00:00'" } }, "writer": { "name": "hdfswriter", "parameter": { "defaultFS": "hdfs://nameservice1", "fileType": "text", "path": "/warehouse/ods/orders/date=20240101", "fileName": "orders", "column": [ {"name": "id", "type": "bigint"}, {"name": "order_no", "type": "string"}, {"name": "user_id", "type": "bigint"}, {"name": "amount", "type": "double"}, {"name": "create_time", "type": "string"} ], "writeMode": "append", "fieldDelimiter": "\t" } } } ], "setting": { "speed": { "channel": 4, "byte": 10485760 } } } }逐个拆解关键字。
splitPk是分片字段,含义是 DataX 根据这个字段的取值范围把查询拆成多个子查询并行执行。通常选主键或者唯一递增字段,类型为数字最佳。如果表没有主键也没有合适的数字字段,写splitPk会报错,此时只能设 channel=1,全表串行读,性能大打折扣。所以建表时留一个自增主键不仅是为了业务,也是为数据同步准备的。
where参数非常灵活。增量同步时利用它加时间条件;全量同步时可以直接不写,但不写就会扫全表,千万小心。我一般建议全量也加一个where 1=1之类的恒真条件来明确表达意图,避免别人看配置时产生歧义。
fileType有两种常用值:text和orc。用了orc就必须在column里给出完整的字段列表和类型,因为 ORC 文件自带 schema;用了text则字段类型只用于 DataX 内部类型转换,写出来的文件本身是文本,字段分隔符由fieldDelimiter控制,通常用\t。下游如果用 Hive 建表,分隔符必须对应上,否则你同步完发现 Hive 查出来全是 null,第一个怀疑对象就是分隔符。
writeMode在 HDFS Writer 里只有append和nonConflict,没有overwrite那种乱来的模式。append表示往已有目录追加文件,nonConflict表示如果目录下已有文件则报错。很多同学第一次用会问"我每次全量同步怎么覆盖旧数据?"——正确做法不是在 DataX 配置里选 overwrite,而是在调度系统里先执行 HDFS 删除目录的命令,再跑 DataX。如果你非要在 DataX 里控制覆盖,可以写一个 shell 脚本包一层:先hdfs dfs -rm -r目标目录,再执行 DataX。我们生产环境就是这么干的,简单直接。
3.3 Channel 数与速度限制的调优逻辑
setting.speed里有几个参数控制速度:channel表示并发通道数,byte表示每秒总字节限制,record表示每秒总记录条数限制。很多人以为这些都是调得越大越好,实际上它更像一个"水位控制"。
先看 channel。MySQL 的并发读取决于查询能否拆分。如果splitPk是自增主键,DataX 会自动做区间分片,channel 数量可不是越多越好,因为每个 task 都会占用一个数据库连接。我实测过一张 1 亿行的订单表,channel 从 1 调到 4,速度大概提升 3 倍;从 4 调到 8,只提升了 30%;从 8 调到 16,速度反而下降。原因是 MySQL 侧的行锁、网络带宽、HDFS 小文件数都成了瓶颈,并发太多还会产生大量小文件,对 Hive 查询极不友好。
再说byte。如果你设置了byte为 10485760(10MB/s),那么 DataX 会根据这个值动态调整每个 task 的读取速率。这个参数特别适合在源库是业务库的时候使用,防止同步任务把数据库 IO 打满,影响线上业务。我见过一个团队没做限速,结果 DBA 找上门,说 MySQL 的 QPS 涨了十倍。最终他们设置byte: 5242880,也就是 5MB/s,同步时间从半小时拉长到两小时,但源库完全无感。
关于 HDFS 侧的小文件问题,后面实操里还会展开。这里先记住一个经验值:channel不超过 8 时,单表同步产生的小文件通常还能接受;如果超过 8,强烈建议下游用 Hive 的MERGE或定期做小文件合并。
4. MySQL 到 HDFS 方向:全量与增量实操
4.1 全量同步的一个稳妥套路
全量同步适合小表或维度表,比如用户维度表、地区表。我常用的套路是"先删目录再同步"。调度系统里大致是这样的:
hdfs dfs -rm -r /warehouse/dim/user 2>/dev/null python /opt/datax/bin/datax.py job/mysql2hdfs_user.jsonJSON 里的 path 写/warehouse/dim/user,fileName 写user。DataX 会在目标目录下生成多个文件,命名类似user__a10c3f2d_xxxx,后面带随机串,保证文件不重名。表结构在 Hive 里的建表语句要和 JSON 里的column完全对应,顺序一致。
这里有个关键点:HDFS 上如果建的是分区表,比如date=20240101,path 应该写到分区列之前,也就是/warehouse/ods/orders/date=20240101中 date 是分区列。DataX 不会自动创建分区目录,但只要你提前在 Hive 里执行了ALTER TABLE ADD PARTITION,或者用脚本创建好目录,DataX 就能直接写进去。有一种错误做法是把分区列写在 column 里,导致 Hive 表分区字段和普通字段重复,查数据时错乱。分区列不应该出现在 column 里,数据文件里只保存普通字段,分区值由目录名决定。
4.2 增量同步的三种常用方式
增量同步是日常开发里占比最高的需求。我见过有人直接用where create_time >= '${start}',这种方式简单但有两个问题:一是时间字段如果没走索引,扫描会很慢;二是边界重复或丢失问题,比如同一秒内写入的数据可能被切到两个批次里。
比较稳妥的方式有三种。
第一种是基于自增主键增量。维护一个last_max_id,在 where 里写id > ${last_max_id}。这种方式适合业务表有自增主键且不会物理删除数据的场景。优点是准确,缺点是拿不到被删的数据。
第二种是基于修改时间增量。在业务表里加上update_time字段并建索引,同步时where update_time >= '${last_time}'。这种方式适合更新频繁的表。
第三种是基于 binlog 或日志的增量,本质上是把 DataX 只当作回溯全量或者定期补数的工具,实时的部分交给 Canal 或 Flink CDC。DataX 的定位是离线,不要试图用它做实时同步,它是"拉取"模型,不是"监听"模型。
具体到 DataX 配置,增量同步就是动态生成 where 参数。我通常用 shell 包一层:
last_id=$(hive -e "select max(id) from dwd.orders" | tail -1) sed "s/__LAST_ID__/${last_id}/g" job/mysql2hdfs_orders.template.json > job/mysql2hdfs_orders.json python /opt/datax/bin/datax.py job/mysql2hdfs_orders.json模板里的 where 写id > __LAST_ID__。这样做的优势是不需要每次手改 JSON,调度平台跑定时任务时传参即可。如果你用的是 Azkaban 或 DolphinScheduler,可以把last_id通过参数传入,灵活得多。
4.3 类型映射与空值处理经验
MySQL 到 HDFS 最常见的报错都出在类型上。MySQL 的datetime、timestamp在 DataX 内部会被读成Date,写入 HDFS 时如果 column 里写string,没问题,DataX 会自动转成字符串;如果写date,就要看格式。HDFS Writer 的date类型只接受yyyy-MM-dd格式,如果原字段是2024-01-01 12:00:00,你配置date就会报错。所以我的习惯是时间字段一律映射成string,让数据保持原样,下游 Hive 里再用函数解析。这样做看似"不规范",但能避免同步任务频繁失败,而且对下游足够友好。
空值也要专门处理。JSON 里可以配nullFormat,默认是null。也就是说源 MySQL 里字段为 NULL 时,DataX 写到 HDFS 文本文件里的值就是字符串"null"。Hive 读这个文件时也会认为是字符串 null,不是 SQL 的 NULL。如果你想存成 Hive 可识别的 NULL,可以配置"nullFormat": "\\N",这是 Hive 的默认空值表示法。我自己习惯统一成\\N,这样在 Hive 里查is null才能查出来。如果你不设置,Hive 里拿where col is null一查,毛都查不到,还以为是数据丢了。
5. HDFS 到 MySQL 方向:回写与落地实践
5.1 读取 HDFS 文件的配置细节
HDFS 到 MySQL 的核心配置里,Reader 常用hdfsreader,Writer 用mysqlwriter。一个典型配置如下:
{ "job": { "content": [ { "reader": { "name": "hdfsreader", "parameter": { "defaultFS": "hdfs://nameservice1", "path": "/warehouse/ads/user_analysis", "fileType": "text", "column": [ {"index": 0, "type": "string"}, {"index": 1, "type": "string"}, {"index": 2, "type": "long"}, {"index": 3, "type": "double"} ], "fieldDelimiter": "\t", "encoding": "UTF-8", "nullFormat": "\\N" } }, "writer": { "name": "mysqlwriter", "parameter": { "username": "sync_user", "password": "******", "writeMode": "insert", "column": ["user_id", "user_name", "order_cnt", "total_amount"], "preSql": ["delete from count_result where stat_date = '2024-01-01'"], "connection": [ { "jdbcUrl": "jdbc:mysql://192.168.1.20:3306/report", "table": ["count_result"], "jdbcUrl": "jdbc:mysql://192.168.1.20:3306/report" } ] } } } ], "setting": { "speed": { "channel": 4 } } } }这里要细说column的index参数。HDFS 的文本文件没有字段名,只有列顺序。index 从 0 开始,表示读取文件中的第几列,type 表示把它解析成什么类型。这个顺序和文件实际列顺序必须一致,否则数据错位。我踩过最痛的坑是 Hive 表结构变更后(中间加了一列),下游忘了改 DataX 配置,结果所有数据全部错位,整整跑了三小时才发现。所以每次上游表结构变更,必须同步审 DataX 配置。
fileType同样支持text和orc。如果用orc,column 里的index就不需要了,DataX 会根据 ORC 文件的 schema 读取,此时column里只要type即可。这一点是很多人的知识盲区,因为hdfsreader对 ORC 的支持并不像 text 那么直观,出错时建议先确认版本。
5.2 写入 MySQL 的几种模式和幂等性处理
mysqlwriter 的writeMode有insert和replace两种。insert就是普通插入,如果主键冲突会报错;replace是REPLACE INTO,遇到主键或唯一键冲突时先删除原记录再插入新记录。很多同步任务希望“跑批后数据正确”,这本质上是幂等性问题。
幂等性最简单可靠的方案不是依赖 replace,而是先 delete 再 insert。配置里有一个preSql数组,写入前会先执行这些 SQL。我常用的写法是:
"preSql": [ "delete from count_result where stat_date = '${date}'" ]这样不管上次同步成功还是失败,每次跑批先把目标分区数据清掉,再插入最新数据。只有 delete 和 insert 都在同一个事务里才能保证完全不出现中间状态,DataX 默认是一次性提交,如果中途失败可能导致部分数据残留。为了规避风险,我的经验是给目标表加上任务批次字段,或者跑批前先手动 delete,再跑 DataX,跑完检查影响行数。
批量插入的性能由batchSize控制。默认值比较保守,建议调到 1024 到 2048。batchSize太大会导致单条 SQL 过长或内存溢出;太小又频繁网络交互。MySQL 侧还可以在 JDBC URL 上加参数rewriteBatchedStatements=true,让批量插入走预处理语句,性能提升非常明显,实测同样 100 万行数据,不开启要 3 分钟,开启后不到 40 秒。这个参数网上很多人不知道,我把配置贴在下面,直接加在jdbcUrl尾部即可:
jdbc:mysql://192.168.1.20:3306/report?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true5.3 回写 MySQL 的目标库保护策略
HDFS 到 MySQL 最需要注意的不是数据能不能读出来,而是不要把目标 MySQL 打挂。数仓跑批经常是多个任务同时回写,如果每个任务 channel 都开 16,MySQL 根本扛不住。我有一个习惯:回写任务的 channel 一律不超过 4,并且设置speed.record限速。
另外,写入目标表前,尽量让目标表去掉所有非必要的索引和触发器。比如报表目标表只需要主键和一个统计日期索引,其他索引全部在导入完成后再创建。很多团队忽略这个细节,导致 1000 万行数据写入时因为索引维护太慢而超时。我的做法是调度系统里串三步:第一步alter table drop index,第二步跑 DataX,第三步alter table add index。虽然多花了一点时间,但整体跑批时间反而降低了一半。
6. 调优实录与参数组合经验
6.1 我的一组性能对比测试数据
这里放一组我自己的实测数据,环境是 8 核 16G 的同步节点,MySQL 源表 2000 万行,目标 HDFS 路径为 ORC 格式分区表。
| channel | byte 限制 | 耗时 | 产生文件数 | 备注 |
|---|---|---|---|---|
| 1 | 不限制 | 28 分钟 | 1 | 最慢,文件少 |
| 4 | 不限制 | 9 分钟 | 6 | 均衡 |
| 8 | 不限制 | 6 分钟 | 15 | 文件偏多 |
| 8 | 10MB/s | 12 分钟 | 15 | 源库压力小 |
| 4 | 不限制 + ORC | 8 分钟 | 4 | 推荐 |
从这组数据可以明显看出,channel=4 是一个性价比很高的档位。文件数从 1 变为 6,对一个分区来说完全可以接受。继续加 channel 到 8,时间只减少了 3 分钟,文件数却翻倍。HDFS 里小文件太多,NameNode 内存占用高,下游 Hive 查询时 map 数暴增,得不偿失。
如果是把数据从 MySQL 同步到 ORC 分区表,建议不要直接让 DataX 写 ORC 文件,而是让 DataX 先写 text 临时目录,再用 Hive 的INSERT OVERWRITE ... SELECT转换成 ORC。原因很简单:DataX 写 ORC 时一旦字段类型和数量有偏差,错误信息往往不直观,排查成本高;而 text 文件容易检查,出问题一眼就能看出来。用 Hive 做转换,还能顺便做数据分布、压缩、小文件合并,一举多得。
6.2 生产环境的调度与监控整合
DataX 本身没有调度和监控界面,生产上要配合调度平台使用。我所在的团队用 DolphinScheduler 比较多,每个 DataX 任务封装成一个 shell 节点,关键逻辑是:
#!/bin/bash source /etc/profile start_time=$(date +%s) python /opt/datax/bin/datax.py /opt/etl/job/${job_name}.json if [ $? -ne 0 ]; then echo "datax job failed: ${job_name}" exit 1 fi end_time=$(date +%s) cost=$((end_time - start_time)) echo "job ${job_name} success, cost ${cost}s"这里的退出码判断很重要。DataX 任务里如果有脏数据超过了阈值,进程会以非 0 退出,所以一定要检查$?。日志采集上,DataX 自带logPath设置,默认在$DATAX_HOME/log下,可以按日期归档。我习惯在调度任务结束后用grep "任务结束时刻"提取耗时,再把它作为 metrics 上报,方便观察同步任务是否变慢。
jobId参数也可以利用起来。DataX 支持在运行时传入-DjobId=xxx,配合monitor可以上报到自定义监控系统。如果你们公司没有现成的数据同步平台,建议至少做到“失败重试 + 超时告警”。DataX 没有内置重试机制,所以需要在调度平台里配重试次数。我见过很多跑批失败的原因是网络抖动或 NameNode 切换,这种问题重试一次就好,不需要人工介入。
6.3 常用调优技巧清单
- MySQL Reader 端加上
useCursorFetch?实际上 DataX 的 MySQL Reader 是基于 JDBC 的 ResultSet 循环读取,默认不会一次性把全表 load 到内存。不需要额外配置大内存。 - HDFS Writer 的
fileName不要用固定名称,用默认生成的随机名,避免多个 Task 写同一个文件冲突。 - 同步大文本字段(如
text、varchar(1000))时,文本文件的行分隔符不要和字段内容冲突。如果 MySQL 某字段里包含换行符,写到 HDFS 后,后续用 Hive 读会出现行错位。解决办法是给 Hive 表开启serde.properties里的quote设置,或者同步前预处理把换行符替换掉。我在订单备注字段上踩过这个坑,查了大半天才发现是备注里有人工输入的换行符。 - 跑批高峰期,HDFS 写入速度会受 NameNode 负载影响,可以把 DataX 调度时间错开与其他 Hive 任务的高峰。
- 对数据质量要求高的场景,开启
“dryRun”: true,先跑一个空同步,检查配置是否能连通,字段映射是否正常。DataX 并非所有版本都支持该参数,需要确认所使用版本。
7. 常见问题与排查速查表
7.1 我踩过的那些典型故障
故障一:MySQL 连接数被打满。现象是源库大量Too many connections,连带着业务接口超时。原因是channel设了 16,每 Task 复用一个 Connection 吗?实际上每个 Task 都会创建自己的 JDBC 连接,所以 channel 越大连接数越多。解决办法是把 channel 降到 4 或 8,并设置byte限速,同时检查 MySQL 的max_connections是否过小。
故障二:HDFS 同步完发现 Hive 查不到数。排查链路一般是:先看目标目录下是否有文件,有文件再看文件里的内容,如果内容有数据,再检查 Hive 表的 SerDe 和分隔符。最常见的分叉点是分隔符不一致。DataX 默认fieldDelimiter是\t,Hive 建表如果写的是ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t',没问题;但如果 Hive 表是用 Parquet 建的,DataX 写的是 text 文件,必然查不到。记住:DataX 的文件格式必须和 Hive 表存储格式严格一致。
故障三:增量同步丢数。表现为某一天的数据比业务库少。原因大概率是where条件里时间边界没处理好。假设上次同步到 12:00:00,本次条件create_time > '上次12:00:00',那么 12:00:00 之后的数据包含在本次范围内,没问题;但如果业务系统在 12:00:00 前写入、事务提交时间晚于 12:00:00,数据就可能漏掉。解决方案是用主键增量而不是时间增量,或者把时间条件往前多取 5 分钟,再配合目标表去重。
故障四:mysqlwriter 写入慢。很多情况下是目标表索引太多,或者没有开启rewriteBatchedStatements。其次是每次写入没有走preSql删除,导致主键冲突重试频繁。目标表尽量精简索引,使用批量预处理,能解决 80% 的写入性能问题。
7.2 问题排查速查表
| 现象 | 可能原因 | 快速检查方式 | 解决方案 |
|---|---|---|---|
| 任务启动即失败,提示找不到插件 | 环境变量DATAX_HOME未配置 | echo $DATAX_HOME | export DATAX_HOME=/opt/datax |
| MySQL 连接超时 | JDBC URL 缺少connectTimeout | 查看完整 URL | 加上connectTimeout=60000&socketTimeout=60000 |
| HDFS 路径不存在 | path 没提前建目录 | hdfs dfs -ls目标目录 | 先建目录或使用 Hive 创建分区 |
| 数据都变成 null | nullFormat 不匹配 | 查看 text 文件里空值表示 | DataX 配置nullFormat与 Hive 保持一致 |
| 写入 MySQL 报主键冲突 | writeMode 选了 insert 且未 preSql | 查看目标表主键 | 改为 preSql 先删或 replace mode |
| ORC 文件同步报类型错误 | ORC schema 与 column 不匹配 | 用orc-tools查看文件元数据 | 调整 column 字段顺序与类型 |
| 任务成功但影响行数为 0 | where 条件过滤掉了数据 | 手动执行 reader 的 SQL 查 count | 检查时间分区参数是否传对 |
7.3 几个防坑心得
不要把 DataX 当作万能工具,它有边界。比如 MySQL 到 HDFS 时,如果源表有JSON类型字段,DataX 默认读出来是字符串,写入 HDFS 时如果你的 Hive 表对应列是 string,没有问题;但如果 Hive 表用STRUCT或MAP类型,DataX 不会自动转换,必须提前在同步前处理成 JSON 字符串,再由 Hive 的get_json_object解析。
不同版本的 DataX 插件行为有差异。开源版和阿里内部版不完全一样,如果你从 GitHub 上拉下来的发行包,遇到某些问题最好先查一下对应版本的文档。很多网上教程基于旧版本,我用 2023 年某版本时hdfsreader对 ORC 的支持就已经比较完善了,但老版本可能会不支持nullFormat。
多环境场景下,defaultFS一定要写对。如果有 HA 集群,defaultFS写的是hdfs://nameservice1,但 DataX 节点上必须在hdfs-site.xml和core-site.xml里配置好 HA 地址。否则会报 UnknownHostException。我在容器化部署 DataX 时反复遇到这个问题,最后把 HDFS 客户端的配置文件放到 DataX 的conf目录下才解决。具体路径是/opt/datax/conf下新建hdfs-site.xml与core-site.xml,内容复制集群里的即可。
8. 一个完整场景:订单表每日增量入数仓再回写报表
为了让你把这些配置串起来,我描述一个完整的小场景。某业务系统有订单表orders,每天新增约 50 万行,字段包括id、order_no、user_id、amount、status、pay_time。我们需要每天把这些增量数据同步到 HDFS 的ods_orders分区表,接着数仓计算每小时的成交额,把结果回写到 MySQL 的report_hour_sales表。
第一步,在 DataX 的模板里配置 MySQL 到 HDFS,where条件写成id > ${last_id}。调度系统每天获取前一天的最大id,作为变量传入。
第二步,任务跑完后,Hive 里用 SQL 做分区修复,因为 DataX 直接写目录不会自动更新 Hive 分区元数据:
ALTER TABLE ods_orders ADD IF NOT EXISTS PARTITION (dt='${date}');如果你的 Hive 版本是 3.0 以上,可以开启MSCK REPAIR TABLE自动修复,但生产上还是显式ADD PARTITION更可控。
第三步,进入数仓计算,产出结果表ads_hour_sales,数据在 HDFS 上。然后配置第二个 DataX 任务,从 HDFS 读取当天的计算结果目录,写入 MySQL。写入前执行preSql: delete from report_hour_sales where dt='${date}',保证幂等。channel 设 2,并开启rewriteBatchedStatements。整个流程跑下来约 15 分钟,其中同步只占不到 5 分钟。
这个场景几乎涵盖了两边迁移的所有核心操作。把它拆开看,每一步都不难,但串联起来很容易出现“数据对不上”的情况。我见过不少团队在使用 DataX 时因为缺少分区修复,最终报表数据和业务库差了一天,这个问题排查起来非常隐蔽。
9. 最后再分享一个实用小技巧
DataX 默认通过python启动,如果你所在的机器上没有 Python 环境(比如精简容器镜像),会直接卡在启动阶段。可以看看发行包里是否有bin/datax.py,其实它只是引导脚本,真正运行逻辑在lib/datax-core-*.jar里。你完全可以用 Java 命令启动:
java -Xms1g -Xmx2g \ -Ddatax.home=/opt/datax \ -Dlogback.configurationFile=/opt/datax/conf/logback.xml \ -cp '/opt/datax/lib/*' \ com.alibaba.datax.core.Engine -job /opt/etl/job/sync.json这种方式对内存控制更精细,而且不需要 Python。很多容器化部署的团队就是这么用的,把 DataX 打成一个镜像,每次作为任务 Pod 拉起,跑完即焚。
回到开头说的,MySQL 和 HDFS 两个方向的数据同步,DataX 给我的最大感受是"规则明确、配置胜过写代码"。它不是一个性能天花板特别高的工具,但它在 90% 的场景里足够稳定、足够简单。只要把splitPk、channel、nullFormat、writeMode这几个核心参数吃透,再结合调度和监控做好兜底,它完全能长期扛住生产环境的日常同步需求。以上都是我在真实业务里验证过的做法,如果你按着配置一步步走,大部分坑提前就能绕开。