不管你是准备把 MySQL 老库迁到 TiDB 做分布式扩容,还是 TiDB 集群想回迁精简成 MySQL,第一关永远是全量数据怎么搬得又快又稳。这个问题我在技术预研阶段拿一套 1.6TB 的压测数据来回折腾过好几轮,也帮人排查过不少导入报错,今天把 MySQL 和 TiDB 互相导入全量数据的工具链、参数和坑位一次性讲透。
先说结论:MySQL 到 TiDB 的正解是 Dumpling 导出加 TiDB Lightning 物理导入;TiDB 回迁 MySQL 则没有类似的"一键神装",只能靠 Dumpling 导出加 MySQL 侧的分片、并发和参数调优。后面我会把两套链路拆开讲清楚,连同校验方法和排障经验一起给出。
1. 迁移前先摸清两边底细:兼容性边界与检查清单
1.1 协议兼容,但底层格式完全不通用
MySQL 和 TiDB 的 SQL 兼容性确实做得很好,TiDB 就是按 MySQL 协议和语法实现的,所以很多人第一反应是"既然兼容,拿 mysqldump 备份文件直接 source 就行",这是最大的误区。
TiDB 底层是分布式 KV 存储,官方备份工具备的是逻辑数据或者更底层的 KV 快照,MySQL 的物理备份(备份 datadir 里的 ibd、frm 这些文件)拿过去完全读不了;反过来 TiDB 的存储文件 MySQL 也不认识。所以两边互相导全量数据的唯一通用路径是逻辑导出导入,也就是把数据变成 SQL 文本或者 CSV 文件再灌入目标端。
理解这一点,你就知道迁移的瓶颈大概率出在"导出端序列化"和"导入端回放"两个环节,而不是简单的网络传输或磁盘拷贝。后面所有优化都围绕这两个环节展开。
1.2 一张清单摸清迁移难度
动工之前,我建议先把下面这张表过一遍。很多导入失败不是工具不行,而是源库本身就埋着雷。
| 检查项 | 怎么查 | 不处理的风险 |
|---|---|---|
| 数据库版本 | select version() | 语法差异、Dumpling / Lightning 版本兼容问题 |
| 字符集 | show variables like 'character%' | 中文乱码、索引长度超限 |
| 最大表数据量 | information_schema.tables 按大小排序 | 导入耗时规划完全失准 |
| 无主键表 | information_schema.columns 关联查询 | 导入热点、后续查询性能差 |
| 分区表 | show create table | 导出后分区结构可能丢失或变化 |
| 自增列 | show create table | 业务主键冲突、ID 语义被改变 |
| 外键约束 | information_schema.referential_constraints | 导入顺序错误、约束报错 |
| 视图/存储过程/触发器 | information_schema 中查询 | 逻辑工具不一定默认导出 |
| 磁盘空间 | df -h | 中间文件写满磁盘,任务中途退出 |
| 目标端负载 | CPU、内存、磁盘 IO | 导入性能翻车 |
1.3 磁盘空间预估与迁移边界
中间文件到底需要多大,很多人拍脑袋就开始了,结果导到一半磁盘爆了。我的估算公式很简单:
- 导出成 SQL 格式时,文件体积一般会比源库逻辑数据量大,保守按源库的 1.2~1.5 倍估算;
- 如果走 TiDB Lightning 物理导入,除了导出目录,还需要一个 sorted-kv-dir 临时目录来排 KV 文件,这块建议再按源库体积预留 1.5~2 倍空间;
- MySQL 侧导入时,binlog、redo log 在导入期间会明显膨胀,特别是调低刷盘参数后 binlog 增长可能更快,空间和 IO 都要留余量。
再说清楚一点,本文只讨论全量存量数据。表结构、数据、一部分元数据怎么搬过去;增量同步、双写追平 binlog 属于后续阶段。但全量导入的产出质量,直接决定增量同步能不能顺利启动,所以这一步值得做扎实。
2. MySQL 到 TiDB 的正确姿势:Dumpling 导出 + Lightning 物理导入
2.1 为什么不建议 mysqldump 硬灌
先把方案摆在一起对比:
| 对比项 | mysqldump + mysql/source | Dumpling + TiDB Lightning |
|---|---|---|
| 导出并发 | 单线程 | 可多线程分片导出 |
| 导入方式 | 在 TiDB SQL 层逐条执行 | 物理写入 TiKV,绕过 SQL 层 |
| 大表(1TB+) | 基本不可用 | 主流方案 |
| 断点续传 | 无 | 有 checkpoint |
| 表结构处理 | 自带 DDL | Dumpling 导出自带 DDL |
| 适合场景 | 小库、临时实验 | 生产迁移、大库 |
mysqldump 导出的 SQL 文件拿到 TiDB 里 source,每一条 INSERT 都要走 SQL parser、事务提交,TiDB 的分布式事务在大 batch 下会把内存压力和事务冲突都放大。几千万行的大表往往越导越慢,最后慢到你怀疑人生。
Lightning 物理导入模式完全是另一条路:它先把数据源拆成并发读取,转换成分好序的 SST 文件,再通过 PD 把文件路由、散列到各个 TiKV 节点,最后 ingest 进底层存储。整个过程 SQL 层基本不参与,吞吐自然不在一个量级。
2.2 Dumpling 导出 MySQL 的实操命令
Dumpling 是 TiDB 官方生态里的逻辑导出工具,但连 MySQL 也连得很稳:
# SQL 格式导出,适合后续给 Lightning 消费 dumpling \ -h 127.0.0.1 -P 3306 \ -u root -p 'yourpass' \ --threads 16 \ --filetype sql \ --output-dir /data/backup/mysql_dump \ --filter 'dbname.*' \ --rows 100000 \ --where 'true'几个参数值得展开:
--threads:并发线程数,建议 8~32。加并发不是线性的,磁盘 IO 先被打满之后再加线程只会增加争抢,执行时用iostat盯一下 util。--filetype sql:导出 SQL 格式,自带CREATE TABLE和INSERT,后面 Lightning 直接消费最省事。也可以导 CSV,但 MySQL 到 TiDB 场景下 SQL 格式更顺。--filter 'dbname.*':只导业务库,默认会带出系统库,最好显式过滤掉。--rows 100000:按十万行分片导出,避免单文件过大,也能让并发更均匀。--where 'true':全量导出,整库迁移直接这样写。
如果源库有只读从库,建议从从库上跑导出,Dumpling 的 SELECT 会消耗连接、buffer 和 CPU,生产主库不该背这个压力。
2.3 TiDB Lightning 物理导入配置
导出完成后准备一个tidb-lightning.toml:
[lightning] level = "info" file = "tidb-lightning.log" check-requirements = true [mydumper]>tidb-lightning --config tidb-lightning.toml三个关键点必须注意:
backend = "local"是物理导入模式,backend = "tidb"是走 SQL 层导入,慢很多。数据量小可以用 tidb 模式,全量迁移一定要 local。sorted-kv-dir必须放在高吞吐磁盘上,Lightning 会把源文件排成有序 KV 后写到这里,这块磁盘慢,整个任务都被拖住,SSD 优先。- Lightning 需要连 PD 和 TiKV,安全组或防火墙记得放行 2379、4000、10080 这几个端口。
2.4 性能参考:1.6TB 压测数据的完整耗时拆解
我拿一套 1.6TB 的压测数据完整压过一次:任务机 8 核 16G,千兆网络,SSD。Dumpling 导出阶段实际大约 25 分钟,Lightning 物理导入大约 50 分钟,最后 ANALYZE 加抽样校验大约半小时,全程控制在 2 小时以内。如果换 mysqldump 加 source,这个时间可能只够导出阶段。
如果速度没起来,按下面顺序排查:
- 磁盘 IO 是不是先打满了,
iostat一眼就能看出来; - Lightning 日志里是不是有大量 region scatter 重试,这通常指向 PD 和 TiKV 之间网络不稳定;
- 任务机 CPU 是不是只有单核在忙,并发参数没真正生效。
table-concurrency、index-concurrency这类参数我用默认值居多,除非目标 TiDB 集群确实很闲,否则调高了不一定划算。
3. TiDB 回迁 MySQL:没有 Lightning 神器时的提速打法
3.1 反向迁移为什么注定更麻烦
TiDB 往 MySQL 回迁时,一个最常被问的问题就是"TiDB 有没有对应的 Lightning"?答案是没有。
Lightning 之所以快,是因为 TiDB 集群自己就是分布式存储,可以并发向多个 TiKV ingest 文件;MySQL 是单机或主从架构,再快也得在 SQL 层执行并落盘。方向反了之后,优化目标就变成:在 MySQL 能承受的范围内,把导入拆成尽量多的并行流,同时降低每条 SQL 的固定成本。
3.2 Dumpling 从 TiDB 导出
Dumpling 连 TiDB 的命令和连 MySQL 几乎一样:
dumpling \ -h 127.0.0.1 -P 4000 \ -u root \ --threads 16 \ --filetype sql \ --output-dir /data/backup/tidb_dump \ --filter 'dbname.*'这里有几个 TiDB 特有的坑:
- 使用
AUTO_RANDOM主键的表,导出的 SQL 会把主键值显式写死,但 MySQL 这张表如果从 1 开始自增,两边的 ID 语义就对不上。迁移前先确认这类表的主键策略。 - TiDB 分区表导到 MySQL 后很有可能变成普通表,分区信息会丢。如果业务依赖分区裁剪,到了 MySQL 要手动重建分区。
- 表特别多时,导出后目录里每个库一个子目录,先看目录结构再写导入脚本,不要一股脑
*.sql全部灌进去。
3.3 MySQL 侧加速导入的三板斧
第一板斧:降低事务提交成本。
SET GLOBAL innodb_flush_log_at_trx_commit = 0; SET GLOBAL sync_binlog = 0; SET GLOBAL max_allowed_packet = 1073741824;这几个参数有代价:导入期间如果 MySQL 崩溃,可能丢失最近几秒到几分钟的数据,所以只在能接受这个风险的前提下用。导入完成之后必须改回来,不然会把数据安全的水位拉低。
第二板斧:并行导入不同表。
mkdir -p /data/import_log for db_tb in $(cat tables.list); do ( mysql -h 127.0.0.1 -uroot -p'pass' \ --default-character-set=utf8mb4 \ "$db_tb" < "/data/backup/tidb_dump/${db_tb}.sql" \ > "/data/import_log/${db_tb}.log" 2>&1 ) & done wait并行度控制在 4~8 个比较稳,开太多会把 MySQL 的线程池和磁盘 IO 打崩。每个文件不要超过 1GB,Dumpling 默认按行数切片,单表超大时继续用--rows压缩文件体积。
第三板斧:先删二级索引,导完再建。
ALTER TABLE big_table DROP INDEX idx_xxx, DROP INDEX idx_yyy; -- 导入数据完成后 ALTER TABLE big_table ADD INDEX idx_xxx (col1), ADD INDEX idx_yyy (col2);为什么排队建索引更快?插入时 InnoDB 要为每一行实时维护所有二级索引,索引越多插入越慢;事后建索引是一次性扫描加排序建树,整体 IO 更可控。高并发导入时,这部分收益经常超过 50%。
3.4 单表超过 50GB 时的分片导入思路
单表超过 50GB,即使拆文件依然会碰到问题:事务越来越大,undo log 膨胀,二级索引维护压力集中。我的做法是再分片:
- 先算主键范围:
select min(id), max(id) from t; - Dumpling 导出时用
--where 'id between ...'拆成多个文件; - 每个文件用
LOAD DATA LOCAL INFILE并行导入。
在一套压测环境里,一张主键均匀分布的 80GB 表,单文件 source 导入大概要 4 小时;按 8 个分片并行后压到 50 分钟内。这背后的原理很简单:并行分片避免了锁冲突,InnoDB 的插入缓冲和 redo 吞吐能同时吃满。
4. 导入只算完成一半:数据校验与元数据收尾
4.1 行数和聚合校验
全量导入结束,第一件事不是切流量,而是做三组快速统计,把每张表在两端跑一遍:
SELECT count(*), sum(id), max(id), min(id) FROM dbname.table_name;- 有主键或唯一键的表,
sum(id)和max(id)对不上,说明有缺行或重复; - 大表的精确
count(*)在两端都是开销很大的操作,TiDB 可以先看information_schema.tables里的table_rows做粗筛,有差异再精确 count; - 写个小脚本把两端的
information_schema.tables导出成文件做 diff,能快速圈出可疑表。
4.2 抽样内容核对
行数一致不代表字段内容一致,尤其要小心数据类型转换带来的隐式截断。我的做法是写临时脚本:
- 在源库随机抽 1000 个主键,或者用
id % 1000 = 1这种采样条件; - 把每行关键字段拼接成字符串,两端分别算 MD5;
- 比对哈希列表。
如果嫌全文比对太慢,可以只比对数值列的合计、时间字段的最大最小值、若干代表性字符串字段,开销小很多。跨异构数据库的场景,很多通用的 checksum 工具不一定能跑通,这种临时脚本是最靠谱的兜底。
4.3 统计信息、视图、存储过程与权限
Lightning 物理导入完成后,TiDB 的统计信息往往是空的或者严重不准,优化器很可能选错执行计划,全表扫就来了。所以:
ANALYZE TABLE t1, t2, ...;MySQL 导入后同样要 ANALYZE。这一步别省,我在 5.5 里专门记录过一次翻车。
视图、存储过程、触发器和自定义函数,Dumpling 默认不一定带全,要额外处理:
- 用
mysqldump --routines --events --triggers --no-data只导元数据,再到目标端执行。注意 TiDB 对存储过程支持有限,需要业务侧做适配。 - 用户权限用
SHOW GRANTS FOR 'user'@'host';生成CREATE USER和GRANT语句,在目标端逐条执行。
自增列也要检查。TiDB 的AUTO_INCREMENT迁移后不会自动重置,业务如果依赖插入后返回的自增 ID 做逻辑,需要手动ALTER TABLE ... AUTO_INCREMENT=xxx对齐。
5. 这些坑我替你们踩过了
5.1 中文乱码基本都是字符集不一致
源库是 latin1,导出文件是 UTF-8,目标库默认 utf8mb4,中间任何一环SET NAMES没指定,就会出现乱码或者incorrect string value报错。建议导出后先看 SQL 文件头部有没有SET NAMES utf8mb4,导入端命令统一加--default-character-set=utf8mb4。
5.2 max_allowed_packet 和超大字段
遇到过一条 INSERT 包含几十 MB 的 base64 字段,直接报Got a packet bigger than 'max_allowed_packet'。解决办法是导出端和导入端同时调大max_allowed_packet,我一般直接设 1GB。Dumpling 导出时单行大小由字段本身决定,只能靠目标端参数接住。
5.3 无主键大表在两端都不省心
MySQL 无主键表导入 MySQL 还能忍,导入 TiDB 就麻烦了:TiDB 内部会生成_tidb_rowid作为隐式主键,写入容易形成热点,Lightning 物理导入时 region 拆分也可能不均匀。迁移前我习惯建议业务方补一个业务唯一键或代理主键,收益非常明显。
5.4 默认时间戳和零值日期
MySQL 的 timestamp 允许0000-00-00这种零值日期,TiDB 默认 sql_mode 不一定允许,导入时直接报错。可以在 TiDB 侧调整 sql_mode,或者在导出前把源库零值改成合法日期。反向导入 MySQL 也有同样问题,MySQL 5.7+ 默认NO_ZERO_DATE,遇到 TiDB 导出的零值日期要提前处理。
5.5 Lightning 导入完没跑 ANALYZE
有次压测时导完直接切了读流量,某条 join 查询走了错误索引,接口延迟从几十毫秒飙到几秒。查了半天发现是统计信息缺失导致优化器选了全表扫。教训是导入完成到切流之间必须留 ANALYZE 时间窗,大表 ANALYZE 也可能要跑几分钟,别等出事再补救。
5.6 sorted-kv-dir 空间预估失败
Lightning 中途退出,日志写着no space left,原因就是 sorted-kv-dir 只留了 100G,而源库有 800G。物理导入的排序中间文件体积和源数据量是同一量级,加上原始导出文件,两处都要按源库体量的 1.5~2 倍预留。最好把 sorted-kv-dir 放到单独挂载点,避免和导出目录互相挤占。
最后说个我自己的习惯:无论哪个方向,我都会在导入前先导出一份全量表的清单文件,记录表名、预估行数、主键类型,导入过程中每完成一批就打个勾。数据迁移不怕慢,怕的是不知道哪张表有问题、也不知道从哪开始查。希望你下次搬数据时,手里有这份清单能少熬夜。