一、我们要干什么
学数仓的时候,很容易陷入一个误区:一上来就研究分层、建模、拉链表。但真把项目跑起来才发现,最容易卡住的其实是第一步——数据怎么进来。
我这次要做的链路很朴素:
业务数据库(MySQL) --DataX--> HDFS --Hive外部表--> 数仓ODS层大体分三步走:
- 构建数仓:规划 ODS / DWD / DWS / ADS 分层,确定每层放什么
- 导入数据到业务数据库:准备模拟源数据(刷卡记录表、用户信息表)
- 数据采集:装 DataX,写配置,把两张表同步到 Hive
前两步是体力活,真正有技术含量的是第三步。所以这篇博客的重点放在数据采集上,把 DataX 的原理拆开讲清楚。
二、为什么是 DataX
离线同步工具其实不少:Sqoop、Flume、Kettle、DataX……选 DataX 的理由很实际:
| 对比项 | Sqoop | Flume | DataX |
|---|---|---|---|
| 定位 | 关系库 ↔ Hadoop | 日志流式采集 | 异构数据源离线同步 |
| 数据源 | 以 RDBMS 为主 | 以文件/日志为主 | 插件化,几十种 |
| 同步粒度 | 任务级 | 事件级 | 单机多线程,任务级 |
| 学习成本 | 中 | 中 | 低(一个 JSON 搞定) |
DataX 最舒服的一点是:换数据源只需要换插件、改 JSON,不用改代码。业务库里是 MySQL、Oracle、SQLServer,目标端是 HDFS、Hive、HBase、ES,写法几乎一样。
三、原理:DataX 的架构到底是什么样
这是本文最想讲清楚的部分。很多教程直接甩一个 JSON 让你抄,抄完了也不知道为什么长这样。
3.1 一句话概括
DataX 是一个Framework + Plugin的架构。Framework 负责调度、缓冲、流控、错误处理;Plugin 负责「读」和「写」。
[图1:DataX 整体架构图,标注 Reader 插件 / Writer 插件 / Framework]
┌──────────────── Framework ────────────────┐ │ 调度 · 缓冲 · 流控 · 脏数据 · 统计 │ └───────────────────────────────────────────┘ ▲ │ ┌──────────┴──────────┐ ┌────────┴────────┐ │ Reader Plugin │ │ Writer Plugin │ │ (mysqlreader…) │ │ (hdfswriter…) │ └─────────────────────┘ └─────────────────┘ ▲ │ 业务数据库 MySQL HDFS / Hive一个 Job 里,Reader 和 Writer 是一一对应的:一个 mysqlreader 配一个 hdfswriter。中间的所有搬运、排队、限速工作,全由 Framework 兜住。
3.2 关键:Job → Task → TaskGroup 三层拆分
这是 DataX 并发的核心,也是「channel 数配错了会出事」的根源。
| 层级 | 含义 | 数量由谁决定 |
|---|---|---|
| Job | 一次同步作业,对应一个 JSON 配置文件 | 你写几个 JSON 就有几个 Job |
| Task | Job 按分片规则切出来的最小执行单元 | 由splitPk和切分策略决定 |
| TaskGroup | Task 的容器,直接对应并发度 | 由setting.speed.channel决定 |
数据流向是这样的:
Reader 切分出的 Task ──► Channel(内存队列) ──► Writer Task (N 个) (限速/背压) (N 个)Channel 是理解 DataX 性能的钥匙:它本质是一个有界阻塞队列。
- 队列满了 → Reader 线程阻塞 → 不会把内存撑爆(这就是「背压」)
- 队列空了 → Writer 线程等待 → 不会空转浪费 CPU
channel配几个,就有几个并发的「流水线」
所以调channel不是无脑调大就行:channel 数 ≈ 并发线程数 ≈ 需要的内存和数据库连接数。
3.3 线程模型:单进程,多线程
DataX 是单机单进程多线程模型,不是分布式的。一个 DataX 进程内部:
JobContainer(主线程) ├── 切分 Task、调度、统计、收尾 └── TaskGroupContainer × N(每个一个线程) └── Task × M(每个一个线程,内含 Reader + Writer 两个执行体)这就是为什么 DataX单机跑得快、但上限取决于机器资源。想要更高吞吐,标准做法是「多机部署 + 用调度器切分任务」,而不是把 channel 无限调大。
3.4 一次执行的完整流程
你敲下python bin/datax.py job.json之后,背后发生了这些事:
- 解析:读取 JSON,校验
job.setting和job.content结构是否合法 - 加载插件:根据
reader.name/writer.name去plugin/reader/、plugin/writer/下找对应插件,装载 jar - preHandler(重要):Reader 根据
splitPk或切分策略,把一次大读取切成多个 Task - schedule:按
channel数把 Task 分配到 TaskGroup,启动线程开始搬运 - 流控:Channel 队列 + RateLimiter 双重控速
- postHandler:收尾、flush、统计并打印报告
- 输出报告:总记录数、读写速度、耗时、脏数据量
[图2:datax.py 执行完成后终端打印的统计报告]
那个报告非常有用,尤其最后一行任务启动时刻 / 结束时刻 / 总计耗时,是后面调优的唯一依据。
3.5 限速与脏数据:两个容易被忽略的机制
限速:在setting.speed里可以配byte(字节/秒)和record(记录/秒)。底层是 RateLimiter,通过让线程睡眠来削峰。为什么需要它?——因为线上库经不起你把 IO 打满,采集任务必须「让路给业务」。
脏数据:指 Reader 读到了、但 Writer 写不进去的记录(比如字段超长、类型不匹配、编码错误)。默认行为是直接丢弃,只在统计里计个数。调试阶段建议打开errorLimit并把脏数据落到文件里,否则你会遇到「任务成功但数据少了」这种最折磨人的问题。
四、动手:刷卡主数据采集
4.1 编写 DataX 配置文件
DataX 的配置就是一个 JSON,结构永远是三段:
{"job":{"setting":{"speed":{...},"errorLimit":{...}},"content":[{"reader":{...},"writer":{...}}]}}setting管全局行为,content管「谁读、谁写」。核心是content里的 reader/writer。
[图3:编写好的刷卡主数据 JSON 配置文件]
几个必踩的配置点:
splitPk:必须选一个数值型且分布均匀的字段(比如自增 id),DataX 靠它把大查询切片,并发才有效。选字符串主键或者分布倾斜的字段,切片会失效,等于白配并发。column:显式列出字段,不要图省事用*。显式列字段能减少网络传输、避免源表加字段后同步错位。writeMode:append(追加)/nonConflict(重名报错)/truncate(先清空)。首次全量用 truncate,日常增量用 append,选错就是数据翻倍。fieldDelimiter:HDFS 文本文件的分隔符。别用逗号——业务字段里出现逗号会让 Hive 直接串列,推荐\u0001这类不可见字符。
4.2 在 Hive 中创建表
这里有个新手最容易懵的顺序问题:
DataX 的 hdfswriter 是往 HDFS 写文件,它不知道 Hive 表的存在。
所以真实顺序是:
- 先执行 DataX 任务 → 数据以文件形式落在 HDFS 某个目录
- 再在 Hive 建外部表,
LOCATION指向那个目录
CREATEEXTERNALTABLEods_swipe_master(idBIGINT,card_no STRING,...)ROWFORMAT DELIMITEDFIELDSTERMINATEDBY'\u0001'LOCATION'/warehouse/ods/swipe_master';建外部表的好处是:删表不删数据,重跑任务不会因为 DROP TABLE 把辛苦采集的数据一起干掉。
4.3 执行脚本
python bin/datax.py job/swipe_master.json看到退出码 0 且报告里「读出记录总数 == 写出记录总数 == 脏数据 0」,这一步才算真的过。
[图4:执行脚本后 DataX 打印的成功报告]
五、用户信息表:换汤不换药
用户信息表的流程完全一样,只是reader 的 SQL 和 writer 的路径/表名不同。
编写 DataX 脚本 → 在 Hive 建表 → 执行脚本这里真正值得记录的差异是:如果源表没有合适的数值型主键怎么办?
比如用户表主键是user_id这种 UUID 字符串,splitPk就没法用,只能退化成单 Task 串行读,速度会慢一个数量级。对策有两个:
- 找表里其它数值型且有索引的字段当
splitPk(比如create_time的时间戳) - 接受单通道,靠调大
batchSize和压缩来换吞吐
六、优化:脚本跑通之后才是开始
跑通只是能出数,优化才是让链路能上生产。前两节脚本的第 3、4 条优化,本质是同一类思路。
6.1 吞吐上不去
现象:任务能跑完,但速度只有几 MB/s,数据库 CPU 却很闲。
原因:默认channel = 1,等于单线程在搬;或者splitPk没配/配置无效,Reader 根本切不出多个 Task。
对策:
channel提到 4~8(不要超过数据库承受能力)- 确认
splitPk是数值型且分布均匀 - 增大
batchSize(MySQL 建议 1024~2048),减少往返次数 - 调大 JVM 内存:
bin/datax.py里的-Xms/-Xmx,默认值偏小,大表容易 OOM
6.2 全量太重,想改成增量
现象:每天整表重刷,源库压力大,目标端文件越滚越多。
原因:配置里没有 where 条件,每次都是select全表。
对策:在 reader 的 SQL 上加时间窗口,配合调度器传参:
select...fromt_swipewherecreate_time>='${bizdate}'andcreate_time<'${bizdate_next}'再配合 Hive 端按天分区,做到「只读当天、只写当天分区」。
6.3 HDFS 上一堆小文件
现象:一次任务结束后,目标目录里散落大量几十 KB 的小文件。
原因:文件数 ≈ Task 数 × 每个 Task 产生的文件数。channel 调大了、切片变多了,小文件自然就多。小文件一多,Hive 查询时元数据开销和 NameNode 压力都会爆炸。
对策:
- 控制
channel数,别为了速度把切片切得太碎 - 写 ORC/Parquet 时开启小文件合并
- 加一层「小文件合并」的定时任务(数仓里几乎是标配)
6.4 数据对不上
现象:任务显示成功,但 Hive 里查出来条数比源表少,或者字段串位了。
原因:八成是脏数据被默认丢弃,或者分隔符和业务字段内容冲突。
对策:
- 打开
errorLimit,把脏数据输出到文件排查 - 确认 Hive 建表的分隔符与 DataX 的
fieldDelimiter完全一致(这是最常见的一处失误) - 检查空值处理:源端
null与 Hive 端\N的映射
七、小结
回头再看整条链路,其实就三件事:
- 构建数仓——先想清楚分层,别急着写代码
- 准备业务数据——模拟源数据,方便反复测试
- 数据采集——用 DataX 把数据搬进来
而 DataX 的原理,浓缩成三句话:
- 架构是 Framework + Plugin,读写的差异被插件吃掉,剩下的调度和流控由框架兜底;
- 并发靠 Task 切分,
splitPk决定能不能并行,channel决定并行多少个; - 性能的本质是取舍,速度、源库压力、目标端文件数三者互相牵制,调优就是在这三者之间找平衡点。
理解了这三点,再回头看那个 JSON,就不是「抄来的配置」了,而是「我知道每一行为什么这么写」。