ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

给DataX套Web界面:异构数据库迁移可视化与增量对账实践

给DataX套Web界面:异构数据库迁移可视化与增量对账实践

1. 先搞清楚:为什么要给 DataX 套一层 Web 界面

异构数据库迁移工具 DataX 本身是个挺有意思的东西——它是个纯命令行的、插件化的离线数据同步引擎,reader 从源库抽数据,writer 往目标库灌数据,中间靠一套框架把读写解耦开,理论上只要两端各有一个插件,任意两种数据源之间都能搬数据。我第一次用它做一次跨库迁移,做法特别朴素:写一份 job.json,扔到服务器上,python datax.py job.json敲回车,然后盯着滚动的日志看进度。小表没事,一旦碰上几十张表、几百 GB 的数据,命令行这套玩法就开始暴露问题了:配置靠手写、错误靠眼睛找、进度靠日志猜、多任务靠人肉排队,最要命的是这事只有我一个人会干,换个同事接手,光看懂那份 JSON 就得半天。

这就是我给 DataX 做一层 Web 界面的原始动机。它不是一个“为了好看”的包装,而是把命令行工具里那些隐性的、靠人记忆维持的知识,固化成一个可视化的任务配置、执行、监控闭环。做完之后的效果是:业务方的数据同学自己就能在页面上选源库、选目标库、勾表、填过滤条件,点提交,跑完看报告,中间不需要我在旁边解释splitPk是干嘛的。适合谁参考呢?我总结大概是三类人:一是手上有一堆异构库要搬迁、还在用命令行硬扛的后端或数据开发;二是想搞内部数据集成平台但不想从零造轮子的团队;三是需要把数据同步能力开放给业务方、又不放心让他们直接碰服务器的人。

异构这块要特别强调一下。所谓的“异构”,不只是 MySQL 到 Oracle 这种品牌不同,还包括字段类型体系不同(比如一边是tinyint(1)当布尔用,另一边没有布尔类型)、字符集不同(utf8mb4 到 GBK)、标识符大小写规则不同、事务与锁的行为不同、时间类型带不带时区也不同。DataX 的插件把这层差异抽象成了 reader 和 writer 两张参数表,但抽象不等于消灭,很多细节仍然得靠配置去兜。Web 界面能做的,是把这些容易漏的细节变成表单上的必填项和默认值,而不是让每个人每次重新踩一遍。

1.1 命令行版 DataX 在真实项目里卡在哪

我把当时遇到的痛点列了一遍,基本可以归成四类,这四类直接决定了 Web 层要做什么功能。

第一类是配置成本高且易错。一份完整的 job.json 有 setting、content、reader、writer 四大块,字段名是驼峰和下划线混着的,写错一个 key 不报错,只是不生效,跑起来才发现速度慢得离谱或者过滤条件根本没加上。手工维护二三十份 JSON,改一个库地址要全文替换,替换漏一处就是连错库。

第二类是执行过程不可见。DataX 的日志会打印每个 TaskGroup 的进度、每秒记录数、脏数据条数,但这些信息在终端里滚过去就没了,事后想复盘只能翻日志文件。更要命的是任务卡住的时候,你没法快速判断是源库在慢慢吐数据,还是网络在重传,还是 writer 那边在等锁。

第三类是多任务没有统一编排。几十张表要么串行跑(慢),要么自己写 shell 脚本并发(崩了没人管)。表之间有外键依赖的还得手工排序,漏一张表就是数据不完整,而且这种缺失往往在业务侧报错时才被发现。

第四类是权限与审计缺失。命令行模式下,谁能登上服务器谁就能跑任务,数据库密码明文躺在 JSON 里。生产环境这套做法基本过不了内审。

Web 界面的价值就在这四点上:把 JSON 变成表单加校验,把终端日志变成实时回传加落库,把 shell 并发变成有状态的任务编排,把明文密码变成加密存储加权限控制。想清楚这个对应关系,后面的功能设计就不会跑偏——每一个页面元素都应该能回答“它解决的是上面哪一条”。

1.2 这套 Web 工具的定位与架构分层

我最终落地的架构是四层,从上往下说。

展示层是前后端分离的页面,负责数据源管理、任务配置、执行监控、日志查看。服务层是核心,干三件事:把页面提交的表单翻译成 DataX 能读的 job.json;管理任务和执行实例的生命周期(排队、运行、成功、失败、终止);采集执行过程中的日志和统计指标。执行层就是 DataX 进程本身,服务层不直接连数据库搬数据,而是通过子进程方式调起datax.py,这一点很关键——DataX 是 JVM 上的东西,硬塞进 Web 应用的进程里会带来类加载冲突和内存互相拖累,独立进程反而更稳。元数据层存储数据源连接信息(密码加密)、任务定义、执行历史、日志索引。

这里有个设计取舍值得说一下:要不要自己重写一个同步引擎。我评估过,答案是不值得。DataX 的插件生态已经把主流关系型库、大数据组件的读写都覆盖了,自己写等于重新解决一遍分片、限流、脏数据统计、类型转换这些脏活。Web 层的定位应该是“编排器和外壳”,不是“引擎”。所以整个设计的核心原则是:能通过 job.json 表达的,绝不改引擎。这条原则后面帮我省了大量麻烦——引擎升级时 Web 层基本不用动。

另一个取舍是同步还是异步。页面点“执行”之后,如果同步等结果,一个跑两小时的任务会把 HTTP 连接拖死。所以执行接口只负责创建执行实例、把任务丢进队列,立刻返回实例 ID,前端拿着 ID 去轮询或者订阅实时进度。这个模式在数据同步场景里几乎是必须的,因为任务时长从几十秒到几小时不等,跨度太大。

1.3 技术选型:哪些自研、哪些直接借力

选型上我踩过一次弯路,值得展开讲讲。一开始我想把调度也自己写,用一个内存队列加线程池,简单粗暴。跑了两个月发现不行:服务重启后队列里的任务全丢,多实例部署时任务会重复执行,失败重试策略也没地方配。后来换成一套成熟的开源分布式调度组件,把“什么时候跑”这件事交给它,把“怎么跑”留在自己手里,边界一下就清楚了。

具体分工是这样的:调度组件负责定时触发和失败重试,它只需要回调我一个 HTTP 接口,接口里创建执行实例;执行器负责调起 DataX 进程、读日志、更新状态。这样即使调度组件挂掉,已经在跑的任务不受影响;服务重启后,执行实例的状态能从数据库恢复,不会出现“任务跑完了但状态还是运行中”的幽灵记录。

进程管理这块我用的是 Java 的ProcessBuilder,关键点有三个。一是必须重定向错误流,DataX 的 Python 启动脚本有些错误是打到 stderr 的,不合并的话日志里会缺一大块,排查时非常难受。二是必须自己读输出流,子进程的输出缓冲区满了之后会阻塞,如果父进程不消费,DataX 就会卡在那里不动,表现为“任务假死”。我专门起了一个线程按行读 stdout,边读边解析边推送。三是必须能杀得掉,DataX 启动的是python datax.py,它自己又拉起一个 Java 进程,直接 kill 父进程可能留下孤儿 Java 进程继续跑,占着连接不放。我的做法是先记下进程 PID 树,终止时按树逐个杀,再兜底扫一遍命令行里带任务标识的残留进程。

密码存储这块没什么花活,对称加密后落库,密钥放配置中心或者环境变量,不写进代码仓库。页面上密码框只写不回显,编辑时留空表示不修改。这一条看着朴素,但它是内审能过的最低门槛。

2. 异构迁移的骨架:Job JSON 与插件模型

DataX 的所有能力最终都收敛到一份 JSON 上,Web 界面的本质就是这台 JSON 生成器加执行器。所以想把这套工具做扎实,得先把 JSON 的每一块吃透,知道哪些字段是必须显式指定的、哪些有默认值会坑人、哪些组合会互相冲突。这一章我把结构拆开讲,顺便把几个关键参数的计算逻辑摊开。

2.1 一份能跑的 job.json 长什么样

先看一份最小的、从 MySQL 到 PostgreSQL 的配置,我把它当成模板用了很久:

{ "job": { "setting": { "speed": { "channel": 4, "byte": 1048576 }, "errorLimit": { "record": 0, "percentage": 0.02 } }, "content": [ { "reader": { "name": "mysqlreader", "parameter": { "username": "sync_user", "password": "******", "column": ["id", "order_no", "amount", "created_at"], "splitPk": "id", "connection": [ { "table": ["t_order"], "jdbcUrl": ["jdbc:mysql://10.0.0.11:3306/biz?useUnicode=true&characterEncoding=utf8"] } ] } }, "writer": { "name": "postgresqlwriter", "parameter": { "username": "sync_user", "password": "******", "column": ["id", "order_no", "amount", "created_at"], "writeMode": "insert", "batchSize": 1024, "connection": [ { "jdbcUrl": "jdbc:postgresql://10.0.0.21:5432/dw", "table": ["t_order"] } ] } } } ] } }

几个必须理解的细节。column在 reader 和 writer 里是按位置对应的,不是按名字匹配的,所以两边顺序必须严格一致,少一个多一个都会导致数据错列。我见过最典型的事故是源表加了一个字段,reader 的 column 里补上了,writer 忘了补,结果后面所有字段整体错位一格,金额写进了备注字段,跑完才发现。Web 界面里我把这个做成了强约束:选完表之后自动带出字段列表,两端字段数量不一致直接拦住不让提交。

splitPk是分片键,DataX 用它把一张表切成多个 Task 并发拉取,不给的话整张表就是一个 Task,速度上不去。它要求这个字段是数值型或者能比较大小的类型,字符串主键(比如 UUID)不能直接用,得改用where条件手工切分,或者干脆放弃并发。这一点后面还会细说。

writeMode的取值依 writer 插件而定,MySQL 系一般支持insert、replace、update,PostgreSQL 支持insert、update、replace(部分版本),Oracle 支持insert、update、replace。这个参数直接决定重复数据怎么处理,选错就是主键冲突或者数据被覆盖。

errorLimit是脏数据容忍度,record是绝对条数,percentage是比例。注意这两个是或的关系,先到哪个阈值就停。我一般设record: 0加一个很小但非零的percentage,原因在 5.1 节展开。

2.2 channel、byte、record:三个速度参数到底怎么配

这三个参数是新手最容易拍脑袋填的,但它们的含义和作用范围完全不同,混着填会导致限流失效或者资源打满。

channel是并发数,一个 channel 对应一个 Task,一个 Task 对应一条独立的读写链路。它决定了同时有多少个连接在抽数据和灌数据。byte和record是限流阈值,作用在全局或者 channel 上,控制每秒最多传输多少字节或者多少条记录。

这三个的优先级关系是:如果配了byte或record,速度会被限住;如果两个都不配,只配channel,那就是不限速、能跑多快跑多快。我见过不少配置写成这样:

"speed": { "channel": 8, "byte": 1048576, "record": 2000 }

看起来配得很全,实际上byte: 1048576相当于全局每秒 1MB,这个速度对现代数据库来说慢得离谱,8 个 channel 全都堵在限流上,等于白开。这就是典型的“以为配了就是优化了”。

我的经验算法是这样的:先算目标吞吐。假设要迁 200GB 数据,窗口期 4 小时,那需要200 * 1024 / (4 * 3600) ≈ 14.2 MB/s,也就是大概 15MB/s 的持续吞吐。单个 channel 在普通千兆网络、库压力正常的情况下,实测大概能到 3~5MB/s,取 4MB/s 算,需要 4 个 channel 左右。再考虑目标库写入可能成为瓶颈(索引多、触发器多、单表写入有热点),我会先保守开 4~6 个 channel 跑一次,观察源库 CPU、目标库 IOPS、网络带宽三个指标,哪个先到 70% 就停在那,不再往上加。

record这个参数更适合“小行大表”的场景。如果一行只有几十字节,用byte限流会导致每秒记录数高到吓人,数据库单条插入的压力反而更大,这时候用record限流更合适。反过来,如果表里有大文本字段,一行几 MB,用record限流会瞬间打满网络,那就该用byte。

至于byte和record的粒度,还有个容易搞混的点:在speed这一层配置的是全局阈值,DataX 会把它平均分给每个 channel。如果你想让单个 channel 限速,得写在core.transport.channel.speed.byte这种更细的配置里。Web 界面里我只暴露顶层全局配置,因为绝大多数用户的心智模型就是“整个任务跑多快”,暴露细粒度参数只会增加困惑。

提示:channel不是越多越好。每个 channel 都占一个数据库连接、一份内存缓冲区。源库连接数打满、目标库锁等待上升、JVM 频繁 GC,都是 channel 开过头的典型信号。

2.3 类型映射:异构数据库之间最容易翻车的地方

这一节是我认为整篇文章里最有价值的部分之一,因为类型问题在测试环境往往测不出来,到了生产才爆,而且爆得毫无规律。

布尔类型的陷阱。MySQL 里常用tinyint(1)表示布尔,tinyint(4)表示小整数。有些 reader 插件会把tinyint(1)自动映射成布尔,写到一个没有布尔类型的目标库时,可能被转成'true'/'false'字符串,也可能被转成1/0,取决于 writer 的实现。如果目标库里这个字段是char(1),那就变成了't'或者'1',业务侧一读就懵。我的处理办法是在配置里显式写查询语句,用CAST把类型定死:

SELECT id, order_no, CAST(is_deleted AS SIGNED) AS is_deleted, created_at FROM t_order

用querySql替代table+column的方式,代价是失去了splitPk的自动分片(部分插件支持在 querySql 外再指定 splitPk,但兼容性参差),所以我一般只在字段类型有歧义的表上这么干,其他表还是走标准配置。

数值精度丢失。从 Oracle 的NUMBER迁到 MySQL,如果没指定精度,NUMBER默认可能被当成浮点处理,超过 15 位有效数字就开始丢精度,而且是静默丢。企业库里身份证号、订单号、金额字段一旦丢精度,后果是数据不可逆。做法很直接:目标端建表时把字段定义成DECIMAL(38, 10)或者VARCHAR,不要留给自动映射。金额字段我一般直接定成DECIMAL,绝不用FLOAT/DOUBLE。同理,浮点类型在 JDBC 层来回转换本身就不可靠,能不用就不用。

时间与时区。JDBC 连接串里不带serverTimezone参数时,驱动会按 JVM 默认时区解释时间,服务器时区是 UTC、业务方在 GMT+8,那迁移完所有时间字段统一差 8 小时。这个坑的隐蔽性在于它不报错,数据看着也挺“正常”,只是全错。我的标准做法是:连接串强制带上时区参数,源端和目标端统一用同一个时区解释,容器镜像里也把时区固定住。迁移完成后必然要做一次时间字段抽样比对,不能只看行数。

字符集与大小写。源库是 utf8mb4、目标库是 GBK,或者反过来,中文和特殊符号必然出问题。连接串里的characterEncoding参数必须和目标库的实际字符集对上,建表时也要显式指定。大小写这块,PostgreSQL 对不加引号的标识符默认转小写,Oracle 默认转大写,迁移时表名和字段名可能全部变形,导致后续的查询语句全挂。解决方式是迁移前统一标识符命名规范,建表脚本里该加引号的加引号,别指望引擎帮你猜。

大字段的内存问题。表里有TEXT、BLOB、LONGTEXT字段时,如果batchSize还按默认的 1024 走,一批数据可能几百 MB,DataX 进程直接 OOM。这种情况我会把batchSize降到 64 甚至 32,channel 数也相应减少,牺牲速度换稳定。或者更彻底一点,把大字段表单独拆成一个任务跑,和其他表错开时间。

3. Web 层落地:从建任务到跑通的完整链路

前面讲的是引擎侧,这一章讲外壳侧。我做这套界面的顺序是:先做数据源管理,再做任务配置,再做执行监控,最后补权限和审计。这个顺序不是随便定的,因为后一步都依赖前一步的产出——任务配置依赖数据源里的连接信息,执行监控依赖任务定义,权限又依赖前面所有实体的用户归属。

3.1 数据源管理:连接测试与元数据采集

数据源这块的核心不是“存一个连接串”,而是“连接测试要准、元数据采集要快”。连接测试看着简单,实际有坑:只测 TCP 端口通不通是不够的,端口通但账号密码错、库不存在、账号没权限,都会在真正跑任务时才暴露。我的测试逻辑分三步走:先建 TCP 连接(3 秒超时),再建 JDBC 连接(10 秒超时),最后执行一条SELECT 1或者查一下information_schema里的表清单。三步都过才算“可用”,任何一步失败就把具体原因回显到页面上,别只说“连接失败”。

元数据采集我做了惰性加载加缓存。一次把所有库的所有表的所有字段拉回来,在几百张表的库上要几十秒,页面直接卡住。改成用户先选库,再拉表清单,再选表,再拉字段,每一步都是一个轻量请求,体验立刻不一样。缓存这块要注意失效策略,表结构是会变的,缓存时间设太长会导致配置里带出已经删掉的字段。我的做法是缓存 10 分钟,同时提供一个“刷新元数据”按钮,用户觉得不对就手动刷。

字段清单拿回来之后,页面要能自动做两件事:一是默认勾选全部字段并保持顺序,二是标记出类型有风险字段。比如源端是tinyint(1)、datetime、decimal且精度未知的,在字段旁边打个黄色标记,鼠标悬停显示建议的处理方式。这个小功能看着不起眼,但它把前面 2.3 节的知识点直接送到了用户面前,比写十页文档都管用。

数据源还得分读写方向。源库账号通常只需要读权限,目标库账号需要写权限,页面里我把这两个分开配置,避免有人图省事用一个高权限账号两边通用。密码的加密存储前面说过,这里补一个细节:加密后的密文也要能支持轮换密钥,所以密文里要带密钥版本号,不然换密钥时全量数据都得重来。

3.2 表单到 JSON:任务配置的生成逻辑

从表单生成 JSON 这段逻辑,我建议不要用字符串拼接,老老实实用对象序列化。原因很实在:JSON 里有大量可选字段,拼接的方式很难处理“字段没填就不输出”这件事,而 DataX 对某些字段的存在与否很敏感——比如splitPk传了空字符串和完全不给,行为可能不一样。用对象映射,没填的字段就是 null,序列化时配一个忽略空值的策略,干净利落。

生成的 JSON 我在落库前会做三层校验。第一层是结构校验:reader 和 writer 的 column 数量必须相等,connection 里的 table 和 jdbcUrl 必须非空,channel 必须在 1 到 32 之间。第二层是语义校验:如果选了splitPk,去元数据里确认这个字段是不是数值型;如果writeMode是replace,确认目标库的插件支持这个模式;如果配置了preSql,简单做一下语法层面的检查。第三层是预演校验:拿配置去两端各执行一条SELECT ... WHERE 1=0,确认字段真的存在、账号真的有权限。这三层下来,能拦掉八成的低级错误。

这里有个经验:校验的报错信息要具体到字段名。写“配置有误”等于没写,写“writer 的 column 数量是 3,reader 是 4,请检查字段映射”才是有效信息。我见过太多内部工具因为报错太笼统,最后没人用,大家还是回去手写 JSON 了——因为手写至少知道自己错在哪。

还有一个设计细节:任务定义和执行实例要分开存。任务是配置,可以反复执行;实例是某一次执行,有自己的状态、开始时间、日志。很多人图省事把两者合成一张表,结果就是任务一跑,配置表被状态字段污染,想再跑一次还得手动重置状态。分开之后,一个任务可以有 N 个实例,历史记录清清楚楚,失败重跑也只是新建一个实例。

3.3 执行与日志回传:进程管理这块最容易出事

执行这块我踩过的坑最多,逐条讲。

子进程的输出必须实时消费。前面提过,不消费会阻塞。我的实现是起一个守护线程,按行读BufferedReader,每读到一行就做两件事:追加写本地日志文件,同时判断是不是关键进度行,是的话解析出数值推给前端。日志文件按“任务 ID + 实例 ID”命名,保留最近 N 个实例,超出的按时间清理,不然磁盘迟早被撑爆。

前端进度用 WebSocket 推,不用轮询。轮询看起来简单,但几十个任务同时跑的时候,定时器的请求量很难看,而且进度更新有延迟。WebSocket 的话,连接建立之后服务端主动推,体验好很多。要注意的是连接断开重连的处理,我的做法是前端重连后主动拉一次当前快照,再接着收增量,避免断连期间丢进度。

超时必须自己管。DataX 本身没有全局超时,任务卡死在某个网络等待上可以挂一整天。我在执行器里加了一个看门狗,任务启动时记下开始时间,另外每读到一条进度就更新一次“最后活跃时间”,看门狗定期扫描,超过阈值(比如 30 分钟没有新进度)就判定为疑似卡死,先告警,再超时就强杀并标记失败。

终止要杀干净。前面说过 PID 树的问题,再补一点:杀进程之后要等一会儿确认真的退出了,然后才更新状态,不然会出现“状态已终止但进程还在写数据”的诡异情况。我的做法是杀完之后轮询检查进程是否存在,最多等 30 秒,还在就再杀一次,两次都杀不掉就记一条需要人工介入的告警。

并发执行要限流。如果不管不顾地让用户点,很容易出现十几个任务同时跑,源库直接被抽干,业务查询全部超时。我加了一个执行队列,按数据源维度限制同时运行的任务数,超出的排队等待。这个限制值是可配的,默认给每个数据源 2 个并发。这个功能上线之后,源库告警数量直接降了一个量级。

4. 增量同步与数据一致性校验

全量迁移只是第一步,真正难的是迁移窗口期的增量数据,以及迁移完之后怎么证明数据是对的。这两块如果没做好,前面所有的性能和体验优化都是白搭。

4.1 全量加增量的组合拳

常见的做法是“全量跑一遍,再追增量”。增量的实现方式我推荐两种,按源库能力选。

如果源库有可靠的自增主键或者更新时间戳字段,用水位线方式最省事。全量跑完后记下当前最大 ID 或者最大更新时间,增量任务带上WHERE id > ${watermark}或者WHERE updated_at > '${last_time}'反复跑,每次跑完更新水位线。这个方案的优点是实现简单、对源库几乎无侵入;缺点是删不掉的数据同步不了,物理删除在目标端永远存在。所以如果业务有删除操作,得配合逻辑删除字段,或者单独跑一轮删除对齐。

如果源库能开启日志订阅(很多商业库和开源库都支持),那就用日志订阅方式,把 INSERT/UPDATE/DELETE 都捕获到,实时写到目标端。这个方案一致性最好,但实现复杂度高一个数量级,涉及到日志位点管理、消息有序性、幂等写入。我的建议是:数据量不大、删除操作少、迁移窗口短的场景,别上日志订阅,水位线够用了;数据量大、要求秒级一致、业务不能停的场景,再考虑上。

水位线这块有几个坑要提。时间戳的精度很重要,如果源表updated_at只精确到秒,同一秒内更新多条,水位线取最大值会漏数据。解决办法是用>=加上去重(配合主键),或者改用自增 ID 做水位线。时钟回拨也会造成漏数据,所以不要用应用服务器时间,一定要用数据写入数据库的时间。水位线的存储要持久化,别放内存,服务重启就丢了。

还有一个容易被忽略的点:增量任务和全量任务不能同时跑同一张表。全量在灌数据,增量也在灌,两边写同一个主键,结果取决于写入模式,非常不可控。我的做法是任务上加一个互斥锁,同一张目标表在同一时刻只允许一个写任务。

4.2 对账:迁移完不算完,对得上才算完

我见过太多迁移“跑完了”就宣布成功,结果业务上线第一天发现少了三百万条数据。对账这件事必须做成标准流程,而且要自动化。

最基础的是行数对账。源端COUNT(*),目标端COUNT(*),不相等就告警。这个只能发现大问题,源端删了 1000 条、目标端多了 1000 条,行数一样但数据完全不同,它发现不了。

进一步是聚合对账。选几个关键数值字段,做SUM、MAX、MIN,两端比对。金额字段的SUM对不上,基本就是精度或者丢行的问题。这个方法成本低、覆盖率高,我一般每张表都跑。

再进一步是抽样明细对账。按主键随机抽几百条,源端目标端各查一次,逐字段比对。抽样比例可以按表的重要性调整,核心表抽 1000 条,边缘表抽 50 条。抽样对账能发现类型转换错误、编码错误这类行数看不出问题的情况。

最严格的是全量校验,两端按主键排序后全量比对。这个成本很高,一般只在核心表、迁移收尾阶段跑一次,跑完签字确认。

对账的时机也有讲究。全量迁移完要跑一次,增量追平后再跑一次,业务切换前再跑一次。三次对账的结果都留档,出问题时有据可查。我在界面上把对账做成了一个独立功能,可以按任务、按表发起,结果以报告形式展示,对不上的表直接标红,附上差异样本。

注意:对账查询本身是有压力的。COUNT(*)在大表上可能扫全表,SUM也差不多。对账任务一定要限速、错峰,别在业务高峰期跑,也别和迁移任务同时跑。

5. 常见问题排查实录

这一章是纯经验,把我踩过的、同事踩过的、用户反馈过的问题整理出来。大部分报错信息看着吓人,实际原因都很朴素。

5.1 高频报错速查表

现象大概率原因处理方式
任务启动即失败,日志报找不到插件插件名拼错,或插件目录不在plugin下用python datax.py -r 插件名 -w 插件名先生成模板验证插件存在
卡在“启动任务”不动子进程输出缓冲区满,父进程没消费检查父进程是否按行读取 stdout/stderr
速度极慢,CPU 很低配了byte限流且值过小,或splitPk没配导致单并发去掉 byte/record 限流先压测,补上数值型 splitPk
报连接超时网络不通、防火墙、连接串 host 错先用同一台机器上的客户端工具连一次
报Too many connectionschannel 数超过源库连接上限调小 channel,或临时调大源库max_connections
报主键冲突writeMode是insert,目标端已有数据改replace/update,或先清空目标表
报 OOM大字段表batchSize过大,或 JVM 堆太小调小 batchSize,调大datax.py -j参数
中文乱码连接串字符集与库实际字符集不一致两端连接串统一characterEncoding
时间全部差 8 小时JDBC 时区参数缺失,JVM 时区不对连接串补时区参数,容器固定时区
脏数据条数暴涨类型不匹配、字段超长、非空约束看脏数据日志文件,定位具体行和字段
任务成功但数据量少源表有where条件,或column与目标端不匹配核对行数,做对账
状态显示运行中但进程已退进程管理没做状态同步看门狗定时校对进程存活状态

表格里我特别想展开的是脏数据这一条。DataX 会把写失败的记录单独落一个文件,文件名里带任务标识,路径在日志里能看到。很多人看到“脏数据 200 条”就不管了,觉得比例很低。我的做法是把errorLimit.record设成 0,只要有一条脏数据就停,逼着自己去看那几条到底是什么。实践下来,脏数据往往是同一类问题重复出现,比如某个字段长度不够、某个值超出目标库枚举范围,修一个能解决一片。设成 0 会牺牲一点“跑完的爽感”,但能换来数据质量。

5.2 几条踩出来的经验

先把小表跑通,再上大表。我做每个迁移项目都先挑三张最小的表,跑全量、跑增量、跑对账,全流程走通之后再批量上。这个习惯救过好几次——有次在小表上发现目标库的默认排序规则和源库不同,主键比较行为不一致,导致update模式写入了重复数据。如果直接上大表,等发现时已经跑了几亿条。

日志一定要落盘并保留。DataX 的控制台输出信息量很大,包括每个 TaskGroup 的启动、每个 channel 的读写速率、脏数据样本。只推前端不落盘,事后复盘就没素材了。我按实例落文件,保留最近 30 天,重要项目手动归档。有了日志,很多问题不用重现就能定位。

不要在业务高峰期做全量。这条听起来像废话,但实际项目里总有人赶进度硬上。全量迁移本质是全表扫描加全量写入,对源库的 IO 和连接资源占用很大。我的原则是全量必须放在低峰窗口,且提前和业务方确认。如果窗口不够,就分表分批次跑,宁可多花几天,也别把生产库拖垮。

配置模板比文档有用。我最后在界面上做了“从模板创建”的功能,把几个典型场景(全量覆盖、增量追加、大字段表、分片表)做成预置模板,用户选模板改连接信息就能跑。文档写了几千字没人看,模板一放上去,配置错误率下降了七成。

留一个“原始 JSON”视图。不管你表单做得多完善,总有覆盖不到的老插件参数。我在任务配置页加了一个折叠的 JSON 编辑器,高级用户可以在这里直接改原始配置,改完再做一遍校验。这个功能是给老手的逃生通道,也是给自己省事——新插件上线时不用改前端就能用。

迁移项目的成败在沟通,不在技术。最后说个不那么技术的。数据迁移这事,技术方案再漂亮,只要业务方不知道你在干什么、什么时候干、干完会怎样,就会出问题。我的习惯是迁移前把“哪些表、什么时间、停多久、失败回滚方案”做成一张表发出去,迁移中每完成一个阶段同步一次进度,迁移后把对账报告发出来。这套流程看着繁琐,但它是把一个技术任务变成一次可交付的项目,也是这套 Web 工具真正能被人信任的原因——页面上每一行状态,背后都对应着业务方能看懂的一句话。

我在实际使用中体会最深的一点是,工具的价值不在于它替你做了多少事,而在于它把原本只存在于某个人脑子里的判断,变成了所有人看得见、改得动、查得到的东西。DataX 引擎本身很稳,真正脆弱的是人对配置的理解和记忆,Web 界面解决的正是这个脆弱环节。后续如果还要扩展,我会优先做两件事:一是把对账做成任务执行的自动后置步骤,跑完自动出报告;二是把数据源的血缘关系画出来,让下游任务能感知上游变更。这两件事都不难,难的是先把眼下这套跑稳、跑顺、跑得让人放心。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进