1. 为什么我坚持给 DataX 套一层 Web 界面
第一次接触Web 界面的异构数据库迁移工具 DataX这个组合,是在一个需要把几十张业务表从一套关系型库搬到另一套关系型库的项目里。当时团队里没人愿意接这个活,原因很简单:DataX 本身是个很扎实的同步引擎,但它的使用方式是写一份 JSON 配置,然后在命令行里跑python datax.py job.json。单次迁移没问题,一旦表多了、人要换、要重跑、要查历史,麻烦就全冒出来了。于是我们决定在 DataX 外面加一层 Web 界面,把它从“个人脚本工具”变成“团队能共用的迁移平台”。
这篇文章想聊的就是这件事:一个 Web 化的 DataX 迁移工具到底该怎么做,中间哪些设计是必须的,哪些坑是躲不开的。它适合正在做数据迁移、数据同步平台、异构库割接的同行看,也适合刚接触 DataX、只会照着模板改 JSON 的同学。我会把架构拆解、配置生成、并发调优、类型映射、日志解析、故障排查这些内容都摊开讲,代码和配置能直接抄的部分我会尽量给全,踩过的坑也会原样写出来,省得你再去踩一遍。
需要说明的是,这里讲的“异构”不只是 MySQL 到 PostgreSQL 这种跨产品,还包括跨版本、跨字符集、跨时区、跨字段语义。真正的难点往往不在“能不能跑通”,而在“跑完之后数据对不对、下次还能不能跑、出了问题能不能查”。
1.1 命令行版 DataX 在团队协作里的三个硬伤
DataX 的设计思路很清晰:Reader 插件读、Writer 插件写,中间靠框架做缓冲和流控,配置全放在一份 JSON 里。这套模型单机跑非常稳,但放到团队里就会暴露三个问题。
第一个硬伤是配置不可维护。一份典型的 job.json 里,reader 和 writer 各有一大段参数,column 列表、splitPk、preSql、连接串全挤在一起。表结构一变,得手动改 JSON,改错一个逗号就整份解析失败。几十张表就是几十份 JSON,谁改的、改了什么、有没有备份,全靠人记。
第二个硬伤是运行状态不可见。命令行跑起来之后,屏幕上刷的是日志,任务成功还是失败、读了多少条、跳过多少条脏数据,要等人跑完再去日志里翻。跑批集中在夜里,第二天早上才发现失败,重跑窗口已经过了。
第三个硬伤是权限和审计缺失。数据库账号密码明文写在 JSON 里,谁都能看到;谁在什么时候跑了哪张表、导出了多少数据,没有任何记录。对于有合规要求的场景,这一条基本就否决了命令行方案。
Web 化要解决的就是这三件事:把配置变成结构化数据、把运行变成可观测状态、把操作变成有记录的账号行为。至于 DataX 引擎本身,一行都不用改——它只是个被调用的执行器。
1.2 Web 化不是做皮肤,是补齐工程能力
很多人以为给 DataX 做个界面,就是画几个表单、拼一个 JSON、点一下按钮。真做下来会发现,界面只是最外面那层,真正的工作量在中间。
我的理解是,Web 化要补的能力有四块。配置管理:数据源、表映射、字段映射要能存下来、能复制、能版本化,而不是每次从零写 JSON。任务编排:一个迁移任务可能包含多张表,表之间可能有顺序依赖,要能串起来跑。运行管控:启动、停止、重跑、并发控制、失败重试,这些操作要能在界面上完成。结果追溯:每次运行读了多少、写了多少、失败多少、耗时多少,要能查、能对比。
这四块能力合起来,才是一个“工具”。只做表单拼 JSON,那叫“配置生成器”,遇到失败重跑、断点续传、增量同步这些场景立刻就露怯了。
还有一个容易忽略的点:DataX 是单机进程模型。它不是常驻服务,每跑一个任务就起一个 JVM 进程,跑完退出。Web 后端要做的是管理这些进程的生命周期,而不是把 DataX 当成一个库去调用。这个认知很关键,后面讲进程管理和内存时会反复用到。
1.3 技术选型:引擎、后端、前端怎么定
引擎层面没什么可纠结的,既然标题已经定了 DataX,就围绕它做。选它的理由也很实在:插件生态覆盖主流关系型库和部分非关系型库,Reader/Writer 组合基本够用;流控和脏数据阈值是内置的,不用自己实现;社区里 JSON 模板多,遇到冷门库还能找到参考。
后端我选的是 Python 的 FastAPI。原因有三个:一是 DataX 启动脚本本身就是 Python 的,同一套环境里调 subprocess 最顺手;二是 FastAPI 写异步接口省事,任务下发、状态轮询这类场景很合适;三是团队里做数据的人对 Python 更熟,维护成本低。如果你的团队是 Java 栈,用 Spring Boot 也完全没问题,核心逻辑是一样的。
前端没太多讲究,表单多、表格多,用现成的组件库搭就行。真正需要花心思的是字段映射那块交互——源表字段和目标表字段要能左右对照、能自动匹配、能手改,这个体验做不好,配置效率反而不如手写 JSON。
存储用一套关系型库存元数据,数据源信息、任务定义、运行记录、字段映射都放里面。注意数据源密码不能明文存,至少要做对称加密,密钥放环境变量里,别写进代码仓库。
2. 整体架构拆解:四层结构与元数据设计
把架构画清楚,后面写代码才不会乱。我最后落地的结构是四层:浏览器端、Web 服务端、执行层、引擎层。每一层的职责要划清楚,尤其是“谁生成配置”和“谁执行配置”这两件事必须分开,否则后面做重跑、做参数覆盖会非常别扭。
执行层是我单独抽出来的一个模块,它只做一件事:接收一份已经生成好的 job.json 路径,起进程、拿输出、解析结果、回写状态。这样即使以后换引擎、换版本,上层的任务管理逻辑不用动。
2.1 分层:从表单到执行器的完整链路
完整链路是这样的:用户在浏览器里选源数据源、目标数据源、源表、目标表,系统读出双方的字段元信息,自动做一次字段匹配,用户确认或调整后保存成任务定义。点“运行”时,服务端根据任务定义渲染出 job.json,写到临时目录,然后交给执行层。
执行层用subprocess.Popen起一个独立进程跑datax.py,同时把 stdout 和 stderr 重定向到日志文件。进程启动后,服务端立刻返回一个 run_id,前端拿着这个 id 轮询状态。执行层在进程结束后读取日志,用正则把关键指标抽出来,写回运行记录表。
这里有个细节值得强调:不要把 DataX 的日志直接实时推到前端。DataX 日志量很大,一个任务跑下来几十兆很常见,实时推送既费带宽又没意义。我的做法是只推“阶段变化”和“每分钟一行的进度摘要”,完整日志留在文件里,需要时按 run_id 下载。
另外,job.json 一定要落盘保留。每次运行生成一份带 run_id 的配置文件,别覆盖。出了问题第一时间就是拿这份文件手工重跑,比在界面上反复点强得多。
2.2 元数据表怎么设计才扛得住
元数据表我一开始设计得很简单,后来被现实打脸了两次,才补成了现在的样子。核心有四张表:数据源、任务、字段映射、运行记录。
CREATE TABLE ds_datasource ( id BIGINT PRIMARY KEY AUTO_INCREMENT, ds_name VARCHAR(64) NOT NULL, ds_type VARCHAR(32) NOT NULL, -- mysql / postgresql / sqlserver host VARCHAR(128) NOT NULL, port INT NOT NULL, db_name VARCHAR(64) NOT NULL, username VARCHAR(64) NOT NULL, password_enc VARCHAR(256) NOT NULL, jdbc_url VARCHAR(512) NOT NULL, extra_json TEXT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE ds_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_name VARCHAR(128) NOT NULL, src_ds_id BIGINT NOT NULL, dst_ds_id BIGINT NOT NULL, src_table VARCHAR(128) NOT NULL, dst_table VARCHAR(128) NOT NULL, write_mode VARCHAR(16) NOT NULL, -- insert / replace / truncate split_pk VARCHAR(64), channel INT NOT NULL DEFAULT 3, where_clause VARCHAR(512), pre_sql TEXT, post_sql TEXT, status TINYINT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE ds_task_run ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id BIGINT NOT NULL, run_id VARCHAR(64) NOT NULL, job_path VARCHAR(512) NOT NULL, log_path VARCHAR(512) NOT NULL, state VARCHAR(16) NOT NULL, -- RUNNING / SUCCESS / FAILED / KILLED read_records BIGINT DEFAULT 0, write_records BIGINT DEFAULT 0, error_records BIGINT DEFAULT 0, start_time DATETIME, end_time DATETIME, cost_seconds INT, message TEXT, UNIQUE KEY uk_run (run_id) );字段映射单独一张表,存源字段名、目标字段名、是否启用、转换表达式。这张表是异构迁移的核心资产,后面讲类型映射时会细说。
被现实打脸的两次是这样的:第一次是没存 job_path,任务失败后想手工重跑,发现配置是按临时文件生成的,进程退出就被清理了,只能重新拼一遍。第二次是没给 run_id 建唯一索引,前端轮询超时重发,同一个任务起了两个进程,两边同时往目标表写,主键冲突报了一屏。这两条后来都补进去了。
注意:密码字段一定要加密存储,加密密钥通过环境变量注入,不要和代码一起提交。界面上回显时用固定掩码,不要返回真实密码。
2.3 配置生成器:把表单翻译成 job.json
配置生成器是整个系统的心脏。它的输入是任务定义加双方字段映射,输出是一份合法的 job.json。我把它做成独立的函数,不依赖数据库,方便单元测试——给它一组参数,断言输出的 JSON 结构正确。
一个最小可用的生成结果大概是这样:
{ "job": { "setting": { "speed": { "channel": 3 }, "errorLimit": { "record": 0, "percentage": 0.02 } }, "content": [ { "reader": { "name": "mysqlreader", "parameter": { "username": "reader_user", "password": "******", "column": ["id", "order_no", "amount", "created_at"], "splitPk": "id", "where": "created_at >= '2024-01-01 00:00:00'", "connection": [ { "table": ["t_order"], "jdbcUrl": ["jdbc:mysql://10.0.0.11:3306/biz?useUnicode=true&characterEncoding=utf8"] } ] } }, "writer": { "name": "postgresqlwriter", "parameter": { "username": "writer_user", "password": "******", "column": ["id", "order_no", "amount", "created_at"], "preSql": ["truncate table t_order"], "batchSize": 1024, "connection": [ { "jdbcUrl": "jdbc:postgresql://10.0.0.21:5432/warehouse", "table": ["t_order"] } ] } } } ] } }生成时有几处必须做校验,否则错误会被推迟到运行时才暴露。column 数组长度两边必须一致,顺序也要一致——DataX 是按位置对应列的,不是按列名。writer 的 jdbcUrl 在多数插件里是字符串,reader 的是数组,这个差异写错了会直接报配置解析失败。preSql 和 postSql 都是数组,哪怕只有一条也要包在数组里。
还有一处容易忽略:where 条件里不要塞太复杂的子查询。DataX 会把它拼进分片 SQL,复杂条件在多个 channel 下会被重复执行,效率很低。需要复杂过滤时,优先在源端建视图,让 DataX 读视图。
3. 异构迁移真正难的地方:类型、分片与限流
前面两章讲的是“怎么把系统搭起来”,这章讲的是“怎么让数据是对的”。异构迁移里,跑通只是起点,类型对得上、分片分得匀、限流限得稳,才是真正决定成败的部分。我见过太多任务:日志显示成功,读写的记录数也对得上,但下游一用就发现小数精度丢了、时间多了八小时、中文字段变成问号。
3.1 字段映射与类型映射的对照表
字段映射分两层。第一层是名字映射,源表叫order_no,目标表叫order_code,界面上左右对照,手工拖一下就行。第二层是类型映射,这层不能靠人拍脑袋,得有一张对照表兜底。
我在系统里内置了一份常见类型的映射规则,用户确认字段匹配时自动套用,也可以手工覆盖。下面这张表是从实际项目里整理出来的,覆盖了 MySQL 到 PostgreSQL 和 SQL Server 的主要场景:
| 源类型(MySQL) | 目标类型(PostgreSQL) | 目标类型(SQL Server) | 注意事项 |
|---|---|---|---|
| tinyint(1) | boolean | bit | 语义是布尔,不要当数字迁 |
| tinyint / smallint | smallint | smallint | |
| int | integer | int | |
| bigint | bigint | bigint | 超过 2^53 时前端展示会失真 |
| decimal(p,s) | numeric(p,s) | decimal(p,s) | 精度必须显式带上 |
| float / double | double precision | float | 浮点比较不要用等号 |
| datetime | timestamp | datetime2 | 重点看时区 |
| date | date | date | |
| varchar(n) | varchar(n) | nvarchar(n) | 目标端长度要留余量 |
| text | text | nvarchar(max) | |
| blob | bytea | varbinary(max) | 大字段慎迁 |
这张表里有三个坑,我逐个说。
第一个是tinyint(1)。在源库里它经常被用来表示布尔,值是 0 和 1。目标端如果是 PostgreSQL 的 boolean,DataX 不会自动把 0/1 转成 false/true,会直接报类型不匹配。解决办法是在 writer 前加一层转换,或者在源端 SQL 里用case when改写。
第二个是decimal 精度。目标端如果建成numeric不带精度,PostgreSQL 会按任意精度存,看起来没问题;但如果建成numeric(10,2),而源数据里有三位小数,写入时会被静默截断。这类问题最难查,因为任务不报错。
第三个是字符集和排序规则。源端是 utf8mb4、目标端是 utf8 的情况下,四字节字符(比如某些生僻字、特殊符号)写进去会报错或者变成问号。这个不是 DataX 的问题,是两端库的编码不一致,迁移前必须先把目标库的字符集对齐。
实操心得:迁移前跑一遍“空表结构比对”,把源端表结构导出来,和目标端逐列对比类型、长度、精度、是否可空。这一步花二十分钟,能省掉后面几天的排查。
3.2 splitPk 与并发通道数的取舍
splitPk 是 DataX 做数据分片的依据。它会把select * from t where splitPk >= ? and splitPk < ?拆成若干段,交给不同 channel 并行读。选得好,速度线性提升;选得不好,要么分片不均,要么直接报错。
选择 splitPk 的原则我总结成三条。优先用数值型主键,最好是自增 id,分片均匀且索引友好。没有数值主键时,退而求其次用时间字段,但要注意数据倾斜——如果历史数据集中在某几个月,分片会有的大小、有的空。绝对不要用字符串 UUID做主键分片,字符串比较在分片 SQL 里效率很低,而且分布不均。
channel 的数量也不是越大越好。我的经验公式是:
channel = min(源库可用连接数 - 2, CPU核数 * 2, 分片数)举例来说,源库最大连接数是 100,业务占用了 30,那留给迁移的就不宜超过 20;机器是 8 核,乘 2 是 16;如果 splitPk 只能分出 4 片,那 channel 超过 4 也没有意义,多出来的通道会空转。三个值取最小,才是合理的并发数。
还有一个隐性约束:目标端的写入能力往往是瓶颈。源端读得再快,目标端一次只能承受这么多事务。我在项目里遇到过一次,源端分片调到 16 通道,读取飞快,结果目标库的写入队列堆满,连接池被打爆,业务查询跟着超时。后来把 channel 降到 6,整体耗时反而短了。
3.3 限流、脏数据、错误阈值
DataX 的流控在setting.speed里配,两种方式:按字节限byte,按记录数限record。按字节限更适合大字段多的表,按记录数限更直观。我的做法是默认不限,只在目标库压力大的时段开启限流,比如白天跑增量时限制到 2MB/s,夜里跑全量时放开。
错误阈值errorLimit是最需要谨慎对待的参数。它有两个值:record表示允许失败的绝对条数,percentage表示允许失败的百分比。默认是 0 和 0.02,也就是一条都不许错,或者错的比例不超过 2%。
这里有个非常容易踩的坑:percentage 是相对于已读记录数的。任务刚开始读了几百条就错了五条,比例可能瞬间超过 2%,任务直接失败。所以我一般把record设成一个合理的绝对值,比如 100,percentage设成 0,让阈值判断变成确定性的。真正需要容忍少量脏数据的场景,再把 percentage 打开。
脏数据本身也要收集。DataX 支持把失败记录写到errorLimit相关的通道里,配置方式是在 setting 中指定脏数据目录。这条一定要开,因为失败记录里往往藏着根因——是某个字段超长、还是某个值类型不对,看几条就能定位。
3.4 增量同步与断点续传的设计
全量迁移是一次性的,增量同步是长期的。这两件事在 Web 平台上的实现方式完全不同。
全量比较简单:清空目标表,全量读,全量写。为了安全,我一般用 preSql 做 truncate,但只在用户明确选择“覆盖模式”时才加,默认是 append,避免误操作把目标表清了。
增量的关键在 where 条件。最常见的做法是按时间戳字段过滤,比如update_time > '${last_max_time}'。这里有两个细节:时间戳字段必须有索引,否则每次同步都是全表扫;时间边界要用>而不是>=,并且要考虑同一秒内的多条记录,最好配合主键做二次去重。
断点续传我用的方案是记录水位。每次运行成功后,把本次读到的最大主键或最大时间戳写回任务表,下次运行时自动读出来拼进 where。这里要注意只在任务成功时更新水位,失败的运行不能推进,否则中间那段数据就永久丢了。
还有一种场景是源表没有可靠的自增主键和时间字段,这种情况我一般建议加一个同步标记字段,或者在源端做逻辑删除+全量覆盖。硬做增量很容易漏数据,不如老老实实全量。
4. 从零搭一套能用的迁移平台
这一章讲落地。假设你现在有一台 Linux 机器,想从零搭起来,下面的步骤可以直接照着做。我会把依赖、命令、配置、代码片段都给出来,中间的关键判断也会说明理由。
4.1 环境与依赖清单
DataX 的运行依赖 JDK 和 Python。JDK 建议 1.8,Python 版本要注意——早期版本的 DataX 启动脚本是 Python 2 写的,新版本已经兼容 Python 3。装之前先看一眼bin/datax.py开头的 shebang 和语法,确认版本,否则会卡在第一步。
# 检查基础环境 java -version python3 --version # 建虚拟环境,避免污染系统 Python python3 -m venv /opt/dwplatform/venv source /opt/dwplatform/venv/bin/activate # 后端依赖 pip install fastapi uvicorn sqlalchemy pymysql cryptography jinja2 python-multipartDataX 本体解压到一个固定目录,比如/opt/datax,确认python3 /opt/datax/bin/datax.py --help能正常输出。如果这一步报错,先解决它,别往下走。
Web 服务用一个 systemd 单元托管,保证重启后自动拉起。日志目录和临时配置目录要提前建好,权限给运行账号,不要让服务以 root 跑。
注意:DataX 每个任务会起一个 JVM,默认堆大小可能不够。大表迁移时在启动脚本里加 JVM 参数,把
-Xms和-Xmx调到 2G 以上,否则很容易在中途 OOM。
4.2 数据源管理与连通性测试
数据源管理看着简单,其实是使用频率最高的功能。新建数据源时要填的不只是连接信息,还有 JDBC 参数。几个必填项:useUnicode=true、characterEncoding=utf8、serverTimezone=Asia/Shanghai、useSSL=false。前两个管编码,第三个管时区,第四个避免握手阶段的证书问题。
保存前要做一次连通性测试。实现方式很简单:用对应驱动建一个连接,执行select 1,超时设 5 秒,把异常信息原样返回给前端。这一步能挡掉八成的配置错误。
def test_connection(ds): url = build_jdbc_url(ds) try: engine = create_engine(url, connect_args={"connect_timeout": 5}) with engine.connect() as conn: conn.execute(text("select 1")) return {"ok": True, "message": "连接成功"} except Exception as e: return {"ok": False, "message": str(e)[:500]}测试通过之后,还要能读元数据:表列表、字段列表、字段类型、是否主键。这一步用information_schema查,不同数据库的字段名略有差异,做一层适配就行。读到字段元信息后,字段映射界面才有数据可填。
这里有个小技巧:把元数据缓存起来。每次打开任务配置都去查一遍源库和目标库的表结构,表多的时候会明显卡顿。缓存加个手动刷新按钮,用户改完表结构自己点一下就行。
4.3 任务配置与下发执行
任务配置页面的交互我改了三四版才顺手。最终形态是三步:选源和目标、选表、配字段。选表之后自动拉字段,按名字相同优先匹配,匹配不上的留空让用户手动选。字段列表要显示类型和是否主键,方便判断 splitPk。
保存任务时做一次静态校验:两边字段数量是否一致、目标表是否已存在、splitPk 是否在字段列表里、preSql 是否为空。校验不过直接拦住,别等到运行时才报错。
运行任务用异步接口,提交后立刻返回 run_id,实际执行放到后台线程或任务队列里。
import subprocess, uuid, os, datetime def run_task(task, job_json): run_id = uuid.uuid4().hex job_path = f"/opt/dwplatform/jobs/{run_id}.json" log_path = f"/opt/dwplatform/logs/{run_id}.log" with open(job_path, "w", encoding="utf-8") as f: f.write(job_json) cmd = ["python3", "/opt/datax/bin/datax.py", job_path] log_file = open(log_path, "w", encoding="utf-8") proc = subprocess.Popen( cmd, stdout=log_file, stderr=subprocess.STDOUT, cwd="/opt/datax", start_new_session=True ) save_run(run_id, task.id, job_path, log_path, proc.pid, "RUNNING") return run_id注意start_new_session=True这一行。它让子进程脱离父进程的进程组,服务重启时不会把正在跑的任务一起带走。停止任务时要用进程组 ID 去 kill,而不是 kill 单个 pid,否则 JVM 的子线程杀不干净。
4.4 日志解析与运行状态回写
DataX 的日志末尾有固定的统计段落,格式大致是“读出记录总数”“读写失败总数”“任务启动时刻”“任务结束时刻”。用正则抽这几个值就够了,不用解析全部日志。
import re PATTERNS = { "read_records": r"读出记录总数\s*:\s*(\d+)", "error_records": r"读写失败总数\s*:\s*(\d+)", "start_time": r"任务启动时刻\s*:\s*([\d\-\s:]+)", "end_time": r"任务结束时刻\s*:\s*([\d\-\s:]+)", } def parse_log(log_path): text = open(log_path, encoding="utf-8", errors="ignore").read() result = {} for key, pat in PATTERNS.items(): m = re.search(pat, text) result[key] = m.group(1) if m else None result["state"] = "SUCCESS" if "任务结束" in text and "错误" not in text[-2000:] else "FAILED" return result状态判断不能只看日志里有没有“错误”两个字,因为正常日志里也可能出现这个词。我的做法是优先看退出码,退出码为 0 且能解析到“任务结束时刻”才算成功,其余一律失败,把日志最后 200 行作为 message 存下来,前端展示时截断。
前端轮询间隔我设的是 5 秒,任务结束后停止轮询。别设太快,数据库扛不住几十个页面同时查状态。
5. 踩坑实录与排查速查表
这一章是整篇文章里我最想写的部分。前面讲的都是“应该怎么做”,这里讲的是“实际会怎么坏”。下面这些坑,有的是我在自己项目里踩的,有的是同事迁移时遇到的,都做过脱敏处理。
5.1 速度上不去的四种典型情况
情况一:channel 调了但速度没变。八成是 splitPk 没配或者配错。没有 splitPk 时,DataX 只会起一个 channel 单线程读,配置里的 channel 数形同虚设。检查方法很简单,看日志里有没有多个 reader 线程在跑。如果没有,回去确认 splitPk 字段名拼写和类型。
情况二:读得快写得慢。日志里 reader 的速率很高,writer 的速率上不去。这通常是目标端的问题:目标表没建索引、有大量触发器、或者 batchSize 设得太小。batchSize 默认是 512,往 1024 或 2048 调通常有改善,但要注意别超过驱动允许的单次批量上限。
情况三:越跑越慢。前几分钟很快,后面逐渐掉速。常见原因是目标表的主键或唯一索引在写入时不断膨胀,B 树分裂导致写入变慢。这种场景下先删索引、导完再建,往往比调参数有效得多。
情况四:网络是瓶颈。源库和目标库跨机房时,单通道带宽可能就几十兆。这时候加 channel 有用,但要先确认中间链路没有限速。有个简单的验证方法:用scp传一个大文件过去,看看实际带宽,心里就有数了。
5.2 类型、编码、时区问题速查
| 现象 | 可能原因 | 排查方式 | 处理办法 |
|---|---|---|---|
| 中文变问号 | 目标库字符集不是 utf8 | show create table看字符集 | 建表时指定 utf8mb4 |
| 时间差 8 小时 | JDBC 时区未指定 | 对比源和目标的时间字段值 | 连接串加 serverTimezone |
| 小数被截断 | 目标列精度不足 | 对比源和目标列定义 | 扩大 precision 和 scale |
| 布尔字段报错 | tinyint(1) 迁到 boolean | 看报错列名 | 源端 case when 转换 |
| 主键冲突 | 目标表有残留数据 | 查目标表 count | 覆盖模式或先清表 |
| 字段超长 | 目标列长度不足 | 看脏数据文件 | 扩长度或截断策略 |
| 空字符串变 NULL | 目标库严格模式 | 查目标列是否可空 | 建表时给默认值 |
这张表建议直接贴到项目文档里,出问题时先对照一遍,能省掉大量沟通成本。
5.3 内存溢出与进程管理
DataX 的 JVM 内存溢出有两个典型表现:日志里出现OutOfMemoryError,或者进程直接消失、退出码非 0 但没有错误日志。前者通常是单批次数据太大,调小 batchSize 或者调大堆内存;后者更麻烦,多半是被系统 OOM Killer 杀掉了,用dmesg能看到记录。
我的处理原则是:单任务堆内存不超过机器内存的一半,同时运行的 DataX 进程数不超过 CPU 核数。这两条是硬约束,超了就等着出事。Web 平台要做并发控制,不能让用户想点多少点多少。
进程管理还有个坑:用户点“停止”之后,进程可能进入僵尸状态。原因是 kill 了父进程,子进程还在。正确做法是用os.killpg杀整个进程组,杀完等几秒再检查,还活着就kill -9。停止之后要把运行状态改成 KILLED,别让它一直挂在 RUNNING。
5.4 数据一致性怎么校验
迁移完成不等于数据正确。我的做法是分三层校验。
第一层是行数比对:源表count(*)和目标表count(*)是否一致。这一层最快,能挡掉大部分明显问题。
第二层是聚合校验:对数值字段做sum和max/min,对关键维度做group by计数,两边对比。这一层能发现行数对但内容错的情况。
第三层是抽样比对:按主键随机抽几百条,逐字段对比。这一步最慢但最可靠,尤其是文本字段和精度字段。
校验脚本我一般写成独立的定时任务,跑完把结果写进一张校验记录表,界面上能看到每次迁移的校验结论。校验不通过就告警,别等人自己发现。
6. 最后分享几条实操心得
做完这套东西,我最大的感受是:DataX 本身很稳,坏事的都是外围。引擎的读写逻辑几乎没出过问题,出问题的永远是配置拼错、类型没对齐、并发没控住、日志没看全。所以做 Web 平台的价值,不在于把命令行包一层,而在于把这些容易出错的地方用系统约束住。
几条具体的建议。第一,先做只读的配置生成器,再做执行。生成器可以先在本地跑通,确认 JSON 结构正确,再接执行链路,能少走很多弯路。第二,每次运行都保留 job.json,这是排障时最值钱的东西,比日志还重要。第三,类型映射表要沉淀成资产,每遇到一个新库、一个新类型就补一条,用久了它就是团队最厚的家底。第四,别迷信“任务成功”四个字,成功只代表进程退出码是 0,数据对不对得靠校验兜底。
如果后面还要继续扩展,我想到两个方向:一是把表级依赖做成 DAG,支持一张表跑完再触发下一张;二是把校验做成任务的后置步骤,跑完自动校验、自动告警。这两块目前还是半手工,等有空了再补。