
上周接到一个临时任务把生产环境Oracle 11g里的一张订单表T_ORDER完整同步到MySQL 8.0表里大概1200万行数据。这类库表同步以前我也用DataX做过不少次但以前的表都比较小配置JSON基本是照着官方文档抄跑通就行很少去深究背后的机制。这次数据量一上来问题全冒出来了——调大channel速度没变化、Oracle端时不时卡顿、日志里一堆Task和TaskGroup概念看得一头雾水。于是我用DataX-Web把这个同步任务从头到尾重新配置了一遍边跑边验证对channel、splitPk、TaskGroup、Task这几个概念的理解。这篇文章就是这次实践的完整记录先讲清楚DataX把一个任务切碎并发执行的原理再给出可以直接抄的Oracle到MySQL同步配置最后把日志解读和常见问题一起说完希望对正在用DataX做数据同步的朋友有帮助。1. 先把DataX执行一个大任务的过程捋一遍1.1 一次同步在DataX内部是什么样子DataX里一次同步任务叫JobJob的完整配置就是那一份JSON。我们通常以为同步一张表就是启动一个进程把它当成一条SQL去执行实际上完全不是这么回事。DataX执行Job的过程更像是一条流水线先把任务登记好然后把数据范围切碎再根据切出来的小块去调度执行。具体流程大致是Job启动后先读取reader和writer插件的配置并完成初始化然后进入切分阶段split。切分阶段DataX会调用reader插件的split方法尝试把要读的数据切碎。切碎之后生成一个或多个Task这些Task才是真正干活的最小单元。接着调度器把Task分配到TaskGroup里由TaskGroup负责并发执行。最后每个Task跑起来之后通过内部的channel把Reader读到的数据流转给Writer。刚开始接触DataX的同学往往只看得到JSON里那几个参数以为调大channel就是调快速度其实channel只是其中一个环节。你至少得同时理解splitPk、TaskGroup和Task才知道整个流程的瓶颈在哪。为了更容易理解我用切香肠打个比方1200万行数据是一整根香肠splitPk决定按什么刻度切channel是同时拿几把刀来切、来搬Task是被切出来的那一段段香肠TaskGroup就是把切好的香肠分到几个筐里交给不同的小工去搬。后面所有配置都是围绕这个场景展开的。1.2 splitPk没有它channel就是摆设splitPk直译过来就是分片字段作用就是告诉DataX用表里哪一列作为数据切分的依据。切分过程其实不复杂oraclereader在split阶段会先对splitPk字段做一次SELECT MIN(splitPk), MAX(splitPk)拿到最小值和最大值之后再结合配置的channel数把字段值区间均匀切成若干段每一段数据交给一个Task去读。所以splitPk能不能发挥作用有几个前提条件字段在表里真实存在而且值类型稳定值大致均匀分布否则很容易出现数据倾斜某些Task很快就跑完某些Task还在那慢慢磨最好有索引否则切分时查MIN/MAX也会全表扫一遍实际生产里最优的splitPk就是整数型主键。以T_ORDER表为例主键叫ORDER_ID数值型分布均匀拿它当splitPk最合适。如果一张表没有主键或者主键是UUID这种字符串也别太担心。DataX的oraclereader也支持字符串、日期类型的splitPk只是要做更细的评估。字符串字段如果基数大且分布均匀也是能用的。真正的问题是没有splitPk——这种情况下oraclereader会把整张表当做一个Task处理就算配置channel10实际跑的时候也只有一个Task在执行其他channel全是摆设。我再强调一遍这个逻辑channel是并发通道Task是被切出来的子任务。子任务只有一个通道再多也没东西可运。很多新手跑同步发现速度上不去第一反应就是调大channel但如果日志里显示的task splits只有1那调多大都白搭。1.3 channel并发通道不是越大越好channel参数写在job.setting.speed.channel里作用是决定整个Job能有几个并发通道同时搬运数据。在DataX的实现里一个channel承载一条reader读取数据并写入writer的数据链路Task在被调度执行时会占用channel所以可以简单理解为并行的Task数量最多不超过channel总数。channel到底设多大合适没有固定答案。机器配置是一方面4核8G的机器建议先给2到4个channel8核16G可以给到4到8个。源库和目标库的承载能力是另一方面channel每增加1Oracle端就多一个查询会话MySQL端就多一批写入连接连接数会直接翻倍。还要留意DataX进程的内存每个channel都会维护自己的数据缓冲队列单行数据如果包含CLOB/BLOB这种大字段内存消耗会非常快。调channel时我常用的方法是从2起步逐步翻倍观察两端数据库的状态。比如先channel2跑一小段看看Oracle的会话数和IO等待再看看MySQL的写入快不快如果都还轻松再往4、8甚至16去试。一旦在源库侧看到明显的IO等待或者目标库出现锁等待就不要继续加了再加只会把问题转移给数据库。还有一点容易被忽略如果你在speed里同时配置了byte或record限速逻辑会优先按字节数或记录数执行。举个例子设置channel10、byte1048576那每秒最大流量被限制在1MB就算并发通道再宽松整体速度也被卡死。之前有人问我为什么channel调到16了速度还是没变化一查就是JSON里还留着一个老的byte限速参数。1.4 TaskGroup夹在Task和调度器之间的管理层切分完生成的Task不会直接被丢出去执行而是先被分配到TaskGroup里。TaskGroup是任务执行层面的一个容器也是DataX调度器的调度单位。DataX的默认策略是每个TaskGroup最多管理5个channel。这个5是社区版本沿用的默认值来自配置项core.container.taskGroup.channel。所以在channel总数固定的情况下TaskGroup的数量基本等于channel总数除以5后向上取整。举例来说channel4TaskGroup数量就是1个4个channel全部归它管。channel10TaskGroup数量是2个两个组各分到5个channel。channel12TaskGroup数量还是3个因为ceil(12/5)3分配上会尽量平均其中一组可能只有2个channel。为什么要设计TaskGroup这一层因为如果几十个Task全部无脑并发跑系统线程数量会爆炸数据库连接数也扛不住调度开销和上下文切换成本都会非常高。加上TaskGroup这一层之后可以控制单位时间内的并发规模同时当某个Task失败时影响会被限制在组内不至于拖垮整个Job。TaskGroup内部的工作方式是拿到分配给自己的channel后从任务队列里取出Task来跑跑完一个再取一个。这跟线程池的原理很像任务总量可以不少但真正同时在跑的数量受channel控制。1.5 一张表把四个概念串起来到这里概念都讲完了我把它们放在一起列个表方便记忆概念作用配置位置比喻Job一次同步任务的整体定义整个JSON一整根香肠splitPk决定数据按哪个字段切分reader.parameter.splitPk切香肠的刻度标记channel控制最大并发通道数job.setting.speed.channel同时有几把刀在切和搬Task切分后产生的子任务最小执行单元框架自动生成切好的一段香肠TaskGroup装载并调度Task的容器默认最多5个channel框架自动生成放香肠的筐一个筐最多放5根再看一个具体例子假设给T_ORDER表配置了splitPkORDER_ID、channel10split阶段oraclereader执行MIN/MAX查询把ORDER_ID的范围切分成10段生成10个Taskschedule阶段TaskGroup数量ceil(10/5)2运行阶段TaskGroup0和TaskGroup1各分到5个channel分别从自己的任务队列里取Task执行如果是channel4同样会被切成4个TaskTaskGroup数量ceil(4/5)14个Task在同一个TaskGroup里排队执行。如果是没配splitPk只生成1个TaskTaskGroup数量1即使channel10也只有一个Task在跑其余9个通道空转。这下应该很清楚了channel是并发上限splitPk是能不能切碎的前提Task是要干的活TaskGroup是管活的容器。四者配合好了DataX才能把一台机器的资源真正用起来。2. 在DataX-Web上配置Oracle到MySQL同步任务2.1 动手前的环境准备我用的环境是这样的DataX-Web 2.1.2DataX 3.0部署在同一台4核8G的Linux服务器上Oracle和MySQL分布在两台独立机器上。DataX-Web本质上是一个包了一层壳的调度平台底层还是要靠DataX执行任务。所以第一步是保证DataX本身能跑。如果你是从头开始装大致流程把官方DataX的release包解压放到比如/opt/datax验证一份最小JSON能跑通比如MySQL到MySQL再装DataX-Web下载release后解压到/opt/datax-web准备一个MySQL库作为DataX-Web的元数据库执行它自带的初始化SQL创建表修改配置里的数据库连接然后启动服务DataX-Web启动后默认端口8080浏览器登录即可初始账号密码一般是admin/123456登录之后建议马上改掉。Oracle插件是这里比较坑的一个点。DataX官方包里oraclereader自带了一个JDBC驱动但如果Oracle版本比较新或者驱动版本不匹配连接时会报各种奇怪的错误。我的习惯是先从maven仓库拉一个对应版本的ojdbc8.jar放到/opt/datax/plugin/reader/oraclereader/libs/目录下顺便把目录里其他版本的驱动jar清一清避免classpath冲突。2.2 在数据源管理里添加Oracle和MySQLDataX-Web左侧菜单有数据源管理点进去可以新增数据源。我们要添加两个Oracle数据源数据库类型选Oracle填主机IP、端口1521、服务名orcl、用户名SCOTT、密码MySQL数据源类型选MySQL填目标库的IP、端口3306、数据库实例名orders、用户名、密码有个小细节容易踩坑DataX-Web里配置的Oracle数据库名其实要填的是Oracle的SERVICE_NAME而不是SID。比如SID是orcl但服务名可能是orcl或orcl.example.com具体要看tnsnames.ora配置。如果填错了表面上数据源管理能保存但实际跑任务时jdbcUrl很可能连不上日志报ORA-12514或者ORA-12541。还要说清楚一点DataX-Web的数据源管理只负责保存元数据真正执行时任务JSON里的连接串还是要自己保证正确。我见过有人以为加了数据源就不用在JSON里写jdbcUrl了结果生成的JSON里连接信息为空任务跑直接报错。2.3 新建任务填好JSON在任务管理页新建任务时DataX-Web会让你选reader和writer这里选的是oraclereader和mysqlwriter。界面会自动生成一个JSON模板可以直接在模板上改。我当时用的配置如下{ job: { setting: { speed: { channel: 4 }, errorLimit: { record: 0, percentage: 0.02 } }, content: [ { reader: { name: oraclereader, parameter: { username: SCOTT, password: tiger, column: [ ORDER_ID, CUSTOMER_NAME, AMOUNT, ORDER_DATE, STATUS ], splitPk: ORDER_ID, where: STATUS VALID, connection: [ { table: [ T_ORDER ], jdbcUrl: [ jdbc:oracle:thin://192.168.1.20:1521/orcl ] } ] } }, writer: { name: mysqlwriter, parameter: { username: root, password: 123456, column: [ ORDER_ID, CUSTOMER_NAME, AMOUNT, ORDER_DATE, STATUS ], preSql: [ truncate table t_order ], connection: [ { table: [ t_order ], jdbcUrl: jdbc:mysql://192.168.1.30:3306/orders?useUnicodetruecharacterEncodingutf8 } ], writeMode: insert } } } ] } }这份JSON有几个地方要重点解释一下。splitPk就是前面讲的ORDER_ID这是这个案例能并发起来的关键。where加了一个过滤条件只同步状态为VALID的订单既减少了同步量也让后续切分都建立在过滤后的数据集合上。preSql里写了truncate table t_order这是为了确保任务可以重复执行。每次全量同步前先把目标表清空避免上一次跑了一部分、这次又追加一部分导致数据重复。只有全量替换的场景才适合这么干增量同步千万别用truncate。writeMode用的是insert。如果目标表有唯一键并且想用源库数据覆盖目标记录可以考虑replace。insert模式在遇到主键冲突时直接报错对于全量同步而言配合preSql truncate反而更可控。2.4 每个参数为什么这么定可能有人觉得channel4定得太随意我解释下当时怎么算出来的。环境是4核8G源表1200万行单行数据量不大字段平均下来大概一百字节左右。这类场景下我一般先按CPU核数等于channel数起步也就是4。理由很简单每个channel本质上是一条独立的数据链路会占用一个reader查询和一个writer写入的资源4个channel相当于同时让4个并发查询从Oracle拉数据对4核机器来说刚好匹配。如果机器核数更多或者源库和目标库负载都比较低可以把channel逐步提到8甚至16。但调速时一定要盯着两端数据库的负载而不是只看DataX日志里的速度数字。Oracle的会话数、MySQL的Threads_running、磁盘IO这些才是决定性的指标。另外关于errorLimit我设了record0、percentage0.02。意思是一旦有单条记录失败先尝试重试但如果失败记录数超过0条同时失败比例超过2%任务就直接失败。生产上尽量严格要求成功给2%的失败比例是为了容忍一些偶发的死锁或网络抖动。拿不准可以保持默认真遇到报错再看错误日志决定。3. 运行任务、读日志、验证并发机制3.1 跑起来之后先看这几行日志在DataX-Web上点执行任务会通过执行器Agent去调用DataX进程。日志查看界面可以直接看到stdout输出。如果本地执行也可以直接用命令行跑本质是一样的。我习惯一启动就盯住前面几行关键日志类似下面这样JobContainer - DataX Reader.Job [oraclereader] do split work ... JobContainer - DataX Reader.Job [oraclereader] with 4 task splits. JobContainer - DataX JobContainer starts to schedule... AbstractScheduler - Scheduler starts 1 taskGroups.with 4 task splits意味着oraclereader确实把整张表切成了4个Task说明splitPk生效了。Scheduler starts 1 taskGroups说明因为channel4没超过5所以只创建了1个TaskGroup。如果配置channel10这两行会变成oraclereader with 10 task splits和Scheduler starts 2 taskGroups。通过这两行日志就能直观验证前面讲的Task和TaskGroup计算逻辑非常有用。3.2 从TaskGroup日志里看并发执行继续往下翻日志会看到类似这样的片段TaskGroupContainer - taskGroupId0 is started TaskGroupContainer - taskGroupId0 taskId0 is running TaskGroupContainer - taskGroupId0 taskId1 is running TaskGroupContainer - taskGroupId0 taskId2 is running TaskGroupContainer - taskGroupId0 taskId3 is running每个taskIdx is running代表一个子任务在跑。日志里会分别打印每个Task的读写记录数、平均速度、成功失败数。这些信息可以用来判断哪个Task出现了数据倾斜比如taskId0跑了很久taskId1早就结束说明切分时各段的数据量很不均衡。任务结束之后DataX会输出一个汇总日志重点看这几行任务总计耗时: 683s 任务平均流量: 1.72MB/s 记录写入速度: 17334rec/s 读出记录总数: 12000000 读写失败总数: 0这个案例里1200万行数据channel4的情况下读完大约花了11分钟左右平均每秒读出一万七千多行整体速度符合预期Oracle端和MySQL端负载都正常。3.3 改变参数对比效果为了验证channel的作用我做了一组对比同样是T_ORDER表、同样有splitPkchannelTask数量TaskGroup数总耗时平均速度111约28分钟0.7MB/s221约16分钟1.2MB/s441约11分钟1.7MB/s882约8分钟2.3MB/s16164约7.5分钟2.5MB/s从8到16提升就非常有限了。这时候再看Oracle端的监听日志会话数和等待事件明显增多瓶颈从DataX自身转移到了源库的读取链路。这种实验也印证了一个观点channel不是无限加的决定速度上限的往往是源库或目标库而不是DataX。跑的过程中可以在Oracle里执行select count(*) from v$session where usernameSCOTT能直接看到DataX打开的并发会话数在MySQL里执行show processlist也能看到并发插入的连接。这些实时状态比DataX日志更早暴露问题。4. 常见问题与排查技巧实录4.1 高频问题速查表我把实践和社区里常见的问题汇总成一张速查表问题现象常见原因解决办法只有1个Task速度很慢日志显示1 task splits没有配置splitPk或splitPk字段无法切分配置有效的splitPk优先用整形主键channel调大但速度不变流量被卡死speed里同时写了byte或record限速检查并去掉byte/record参数只保留channelJVM内存溢出日志出现OutOfMemoryErrorchannel过大或单行包含大字段降低channel、调大DataX的JVM堆内存、用byte限速Oracle报ORA-12514连接失败jdbcUrl里的SERVICE_NAME填错用SELECT name FROM v$database或tnsnames.ora确认服务名Oracle报ORA-28001连接失败密码过期登录Oracle重置密码或让DBA修改密码有效期策略MySQL端写入慢速度提不上去目标表二级索引过多、锁竞争同步前先删索引跑完再重建数据数量对不上任务成功但总量差异大同步期间源表有DML变更全量同步前锁定快照或使用preSql truncate后重跑CLOB字段写入报错数据超长Oracle CLOB映射到MySQL text可能不够建表时用longtext或在SQL里做截断/转换4.2 排查路径按这个顺序来遇到同步慢的问题我一般按照这样的顺序排查先看日志里task splits数量确认数据有没有被切分再看每个Task的吞吐和耗时判断是否有数据倾斜然后看源库和目标库的会话数、IO、锁等待确认是哪一边的瓶颈最后才考虑调整channel而不是一上来就加并发遇到任务失败的问题顺序也差不多先看异常栈里的关键错误码是ORA还是MySQL驱动报错用where条件拉少量数据比如只查一天的先验证字段映射和类型转换确认preSql是否带了truncate保证可重复执行修好之后重跑全程盯着日志直到跑通全量4.3 几个实测有效的经验值最后分享几个我实测有用的细节都比较冷门但很管用。第一大表同步前把目标表的非主键索引全部删掉。MySQL在insert的时候要维护二级索引索引越多代价越大。我之前有一次同步带6个二级索引的目标表速度只有同配置下无索引表的50%左右。全量数据导入后再一次性重建索引整个过程会快非常多。第二选splitPk时要看数据分布。如果选的是时间字段而业务上有明显的时间峰值切分出来的块大小会差距很大。可以用select min(字段), max(字段), count(*) from 表 where ...先看看分布再决定splitPk选哪个字段。第三DataX-Web的定时调度不要排太密。全量同步任务消耗很大如果前一次还没跑完下一次调度就触发了两个任务同时对一个MySQL表做truncate和insert结果会非常难看。设置调度间隔时至少要大于任务平均耗时的1.5到2倍。第四如果目标是MySQL且遇到主键冲突先确认是不是重复跑。很多所谓的同步失败最后发现就是同一批数据因为上次没清干净又导了一遍。固定用preSql truncate能治好一半的疑难杂症。这篇DataX实践记录写到这里核心内容基本讲完了。我个人最大的感受是DataX这种工具配置本身并不难难的是真正理解它背后的调度模型。没有splitPk并发通道就是一纸空谈不理解TaskGroup看日志就会一脸懵。建议拿到一个新任务时别急着把channel调大先花几分钟打开DataX日志确认task splits和taskGroup数再决定参数方向。多跑几次、多看几次两端数据库的状态这些概念自然就变成肌肉记忆了。后面如果遇到增量同步的典型坑我再来补充一篇。