数据要“出海”本质上是把 MySQL 里的核心数据从业务库释放出去送到它应该去的下一个地方——可能是备库、数仓、异构存储、下游微服务甚至是另一套数据库。这个动作在圈子里常被叫做“数据出海”。干这活儿最绕不开的就是“MySQL 数据同步方案”。这几年我经手过不少同步项目从最基础的 binlog 主从复制到 Canal 监听增量再到把表结构转到 TDengine 超级表踩过的坑能排成一串。这篇就把我实际用过的方案、选型逻辑和排查经验完整梳理一遍给正在做数据分发的同学一个能直接参考的路线图。1. 数据出海场景与同步方案选型1.1 数据出海到底在说什么“数据出海”不是把数据发到海外而是指业务数据从源端数据库“流出去”输送到不同目标端的过程。常见场景有几类一是同构容灾比如 MySQL 主库复制到从库做读写分离和故障切换二是异构迁移比如 MySQL 同步到 TDengine、ClickHouse、Elasticsearch 等专用存储三是业务数据分发比如订单表变更后同步给下游搜索服务或缓存系统四是离线数仓采集把线上 MySQL 的增量数据定期或实时灌入数据仓库。不同场景对同步的要求差异很大。容灾场景要求延迟低、链路稳最好能不丢数据异构迁移不仅要同步数据本身还要处理表结构映射和类型转换业务分发往往只需要部分表、部分字段最好能按规则过滤数仓采集则要兼顾存量全量同步和增量实时同步。没有一套方案能通吃所有场景这也是选型最容易被忽视的地方。很多团队一开始图省事直接用定时任务导出 CSV 再导入数据量小的时候确实能跑但一旦表过千万行或者业务要求分钟级延迟这种批处理方式就完全撑不住。所以做同步方案的第一步不是选工具而是先明确你的目标场景和数据规模。1.2 三类主流同步方式我把实际项目中用过的同步方式归成三大类每一类都有明确的适用边界。第一类是 MySQL 原生的主从复制。这是最“正统”的同步方式源库开启 binlog从库通过 I/O 线程拉取 binlog 并写入 relay log再由 SQL 线程回放完成同步。优点是不需要额外引入中间件MySQL 自带机制非常成熟事务一致性有保障也是官方文档里最推荐的复制方案。缺点是同构限制目标端必须是 MySQL 或兼容 MySQL 协议的数据库而且默认异步复制存在主从延迟极端情况下可能丢数据。第二类是日志抓取型 CDC 工具比如 Canal、Debezium、Maxwell。这类工具伪装成 MySQL 的从库实时解析 binlog 并输出为结构化事件可以投递到 Kafka、RocketMQ或直接写入其他存储。Canal 在 Java 生态里用得最多De bezium 则在 Kafka Connect 生态里更常见。这类方案解决了异构同步的痛点同时天然支持表级、行级过滤。缺点是要多维护一套中间件部署和运维成本明显高于主从复制。第三类是定时批量同步工具比如 DataX、Kettle、Sqoop。它们以查询方式从源库抽取数据再写入目标端。优点是实现简单、支持任意异构存储缺点是只能做准实时甚至离线同步无法捕获删除操作同时频繁全量查询会对源库产生额外压力。这类方案适合 T1 数仓不适合在线业务分发。选型时我习惯用一个简单的判断框架同构数据库且需要高可用和读写分离优先主从复制异构实时同步优先 Canal/Debezium 这类 CDC离线数仓批量抽取用 DataX 这类工具如果既要异构又要准实时那就组合使用——存量用批量工具灌入增量用 CDC 工具接力。1.3 选型背后的四个判断标准很多人在 Canal 和主从复制之间纠结其实把下面四个问题想清楚答案自然就出来了。第一目标端是不是 MySQL。是的话主从复制永远是最省心的选择不要为了“技术先进”而引一套 Canal。第二允许多大的数据延迟。秒级延迟才能叫实时分钟级可以接受的话定时任务加增量字段也能凑合。第三是否需要对源库透明。主从复制不侵入业务但 CDC 工具需要源库开 binlog 且格式为 ROW对 DBA 来说多一个运维项。第四团队有没有能力维护中间件。Canal 本身不是重组件但它依赖的 ZooKeeper、Kafka、监控告警一套下来需要专门的精力。我见过最典型的反面案例是小团队为了同步几张表到 Elasticsearch直接上了 Canal Kafka Logstash 全家桶结果每天光盯组件健康就花两小时。后来换了 Canal Adapter 直连目标端砍掉 Kafka 这一层维护成本立刻降了一个量级。选型不是在选最强大的工具而是在选最匹配团队运维能力的组合。2. 同步的底层基础从 binlog 说起2.1 binlog 就是 MySQL 的“流水账”不管用主从复制还是 Canal所有增量同步都绕不开 binlog。binlog 是 MySQL Server 层维护的二进制日志记录了对数据库产生变更的操作包括 INSERT、UPDATE、DELETE也包括表结构变更 DDL。它就像是数据库的“流水账”每一笔账都按发生顺序记录下来同步工具只需要跟着流水账的进度往后放账就行。binlog 有三种记录格式STATEMENT、ROW 和 MIXED。STATEMENT 记录的是原始 SQL 语句日志量小但某些非确定性函数在不同库上执行结果可能不一致容易导致主从数据偏差。ROW 记录的是每一行变更前后的镜像日志量大但信息最完整行级变更一目了然是 CDC 工具的硬性要求。MIXED 是混合模式MySQL 自动判断使用哪种格式。做同步方案时我强烈建议直接把 binlog_format 设置为 ROW虽然日志会膨胀但换来的是可靠性和可解析性。这也是官方对数据一致性要求高的场景给出的推荐配置。2.2 GTID 让位点管理变得省心理解同步就不能不理解位点。传统主从复制里从库通过 MASTER_LOG_FILE 和 MASTER_LOG_POS 两个参数记录自己在主库 binlog 上的读取位置。这两个参数组合起来就是一个位点从库每次拉取日志都从位点开始同步完成后再更新位点。一旦从库异常重启只要位点没丢就能接着断点继续同步这就是同步的“书签”机制。位点管理虽然有效但手工运维很痛苦。主库切换、从库重建时找到正确的 binlog 文件名和偏移量是 DBA 最头疼的事。GTID 解决的就是这个问题。GTID 是全局事务标识符每个事务在生成时就被分配一个唯一 ID格式类似uuid:sequence。开启 GTID 之后从库不需要手动指定位点只需要告诉主库“我要从哪个 GTID 开始同步”主库会自动定位。这不仅简化了切换流程还避免了传统复制模式下位点定位不准导致重复或丢失事务的问题。我在实际操作中只要不是特别老的 MySQL 版本都建议直接开启 GTID 模式。需要注意的是开启 GTID 要求 binlog_format 和相关配置做配套调整并且已经存在的复制链路需要先停掉再重新配置。存量环境改造前一定要在测试库先演练一遍否则主从切换时容易出幺蛾子。2.3 主从复制与 CDC 的本质区别很多人误以为 Canal 只是“把主从复制搬到了非 MySQL 目标端”其实两者的实现位置完全不同。主从复制是 MySQL 原生机制从库本身就是 MySQL 实例靠 I/O 线程和 SQL 线程协作完成 relay log 的拉取和回放。Canal 这类工具则完全模拟了一个“假从库”它只做一件事向主库请求 binlog然后在内存里把 binlog 解析成结构化事件再交给下游。这个区别带来几个关键影响。一是主从复制只能同步到 MySQL 实例而 Canal 可以把事件输出到 Kafka、消息队列、文件或直接写入任意存储。二是主从复制的 SQL 回放是从库原生执行的遇到存储过程、触发器、函数时行为更贴近源库而 Canal 解析的是行变更事件天然绕过了存储过程只认最终数据结果。三是主从复制在回放失败时会报错并停止Canal 则可以通过消费者重新消费事件容错机制更灵活。但 Canal 也不是银弹。它解析 ROW 格式 binlog 时会屏蔽掉原始的库名、表名只输出 schema 和 table 信息下游消费者需要自己维护表映射关系。同时 DDL 事件解析后只是拿到 SQL 文本目标端如果要应用 DDL得自行处理和转换。这些边界在实际项目中都要提前评估。3. 实操从主从复制到 CDC 再到异构同步3.1 动手前的源库准备不管最后选哪条路源库的准备工作都绕不开。先检查 binlog 是否开启登录 MySQL 执行SHOW VARIABLES LIKE log_bin; SHOW VARIABLES LIKE binlog_format;如果 log_bin 是 OFF或者 binlog_format 不是 ROW需要修改配置文件并重启 MySQL。以 MySQL 8.0 为例在my.cnf的[mysqld]段加server-id100 log-binmysql-bin binlog_formatROW gtid_modeON enforce_gtid_consistencyON binlog_row_imageFULL这里每个参数都有讲究。server-id 在整个复制拓扑里必须唯一主从不能相同。log-bin 开启 binlog文件名前缀可以自定义。binlog_formatROW 是 CDC 的前提也是保证数据一致性的关键。gtid_mode 和 enforce_gtid_consistency 是配套的开启后复制位点管理更简单。binlog_row_imageFULL 确保 ROW 格式下记录完整的前后镜像Truncate 和 Delete 等操作才能被正确解析。接着创建同步专用账号。主从复制只需要 REPLICATION SLAVE 权限Canal 则需要 REPLICATION SLAVE 和 REPLICATION CLIENT 权限。建议单独建账号不要用 root 跑同步链路避免权限过大带来安全隐患CREATE USER repl% IDENTIFIED BY StrongPass123; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO repl%; FLUSH PRIVILEGES;3.2 经典主从复制搭建步骤假设有两台 MySQL 8.0 实例主库 IP 为 10.0.1.10从库 IP 为 10.0.1.11。主库配置完成后先在主库查看当前 binlog 位点这个位点就是从库开始复制的起点SHOW MASTER STATUS;执行结果里会有 File 和 Position 两列例如mysql-bin.000003和154。记住这两个值接下来去从库配置。在从库的my.cnf里也要设置一个不同的 server-idserver-id101然后重启从库 MySQL执行 CHANGE MASTER 语句建立复制关系CHANGE MASTER TO MASTER_HOST10.0.1.10, MASTER_USERrepl, MASTER_PASSWORDStrongPass123, MASTER_LOG_FILEmysql-bin.000003, MASTER_LOG_POS154;执行完成后启动复制线程START SLAVE;用状态命令验证复制是否正常SHOW SLAVE STATUS\G重点关注两个字段Slave_IO_Running和Slave_SQL_Running这两个都应该是 Yes。如果出现Slave_SQL_Running: No可以查看Last_SQL_Error定位问题。Seconds_Behind_Master字段表示从库延迟秒数正常应该在个位数。数据量大时只有运行中的增量同步还不够需要先做存量数据迁移。我常用的做法是在从库上直接使用mysqldump导出时指定--master-data2这样导出的文件里会自动包含起始位点信息导入从库后再配置位点比手动记录位点更可靠。命令参考mysqldump -h 10.0.1.10 -u root -p --single-transaction --master-data2 --all-databases all.sql导入到从库后从all.sql头部的注释里找到CHANGE MASTER TO语句按其中的位点信息配置即可。--single-transaction配合 InnoDB 可以在不锁表的情况下拿到一致性快照。3.3 用 Canal 监听 binlog 做增量同步当目标端不是 MySQL 时主从复制就无能为力了这时候 Canal 是首选。Canal 本身是 Java 写的官方提供了多种部署方式最简单的是直接下载 release 包或者用 Docker 运行。我常用 Docker 方式一条命令就能拉起服务端docker run --name canal-server -p 11111:11111 \ -e canal.instance.master.address10.0.1.10:3306 \ -e canal.instance.dbUsernamecanal_user \ -e canal.instance.dbPasswordCanalPass123 \ -e canal.instance.connectionCharsetUTF-8 \ -e canal.instance.filter.regextest_db\..* \ -d canal/canal-server:v1.1.7这里canal.instance.filter.regex是表过滤规则test_db\..*表示只监听 test_db 库下的所有表。如果要监听指定表可以写成test_db\\.order_info或者用逗号分隔多个规则。注意反斜杠的转义在 Shell 环境变量里尤其容易出错我踩过不少次坑。Canal Server 起来后还需要一个客户端消费事件。官方提供 Canal Adapter可以通过配置直接同步到关系型数据库、ES、HBase 等目标端。以同步到 MySQL 目标库为例在application.yml中配置 canal Server 地址和 adapter 的线程数再在conf/rdb/mytest_user.yml里配置源表与目标表的映射dataSourceKey: defaultDS destination: example groupId: outerAdapterKey: mysql concurrent: true dbMapping: database: target_db table: order_info targetTable: target_order_info targetPk: id: id mapAll: true启动 Adapter 后源库 test_db 下的 order_info 表的任何变更都会被实时写入 target_db.target_order_info。Canal Adapter 的更新逻辑默认以目标表主键为更新条件所以源表和目标表必须有对应主键否则 UPDATE 事件无法正确路由。这一点在映射表结构时就要提前统一。Canal 在解析 DDL 事件时只会输出 SQL 文本Adapter 默认不自动执行 DDL。所以表结构变更需要人工同步到目标端或者自己在消费端实现 DDL 执行逻辑。很多团队在这里栽跟头源库加了字段目标库没跟上后面所有行变更解析出来的字段数对不上导致同步任务整体报错。我的习惯是源库 DDL 变更必须和同步链路联调验证不能只在源库执行就完事。3.4 MySQL 表结构转 TDengine 超级表子表TDengine 的同步场景比较特殊它不是简单地把表搬过来而是需要做模型转换。TDengine 有两种核心表模型超级表STable和子表。超级表定义 schema 和标签TAG子表是超级表在具体标签值下的实例。比如你有一张 MySQL 设备采集表CREATE TABLE sensor_data ( id INT PRIMARY KEY AUTO_INCREMENT, device_id VARCHAR(64) NOT NULL, ts DATETIME NOT NULL, temperature FLOAT, humidity FLOAT, INDEX idx_device_ts (device_id, ts) );在 TDengine 里合理的建模是把device_id作为 TAGts作为时间戳主列temperature和humidity作为普通列。对应的超级表定义CREATE STABLE sensor_data_stable ( ts TIMESTAMP, temperature FLOAT, humidity FLOAT ) TAGS (device_id VARCHAR(64));每个设备会对应一张子表命名一般是设备标识例如sensor_data_stable_dev001。在同步时不需要预先创建所有子表TDengine 支持在写入数据时用INSERT INTO 子表 USING 超级表 TAGS(...) VALUES(...)自动建表。MySQL 表结构要自动转成 TDengine 超级表和子表核心就是做两件事识别主键或唯一键中用于分片的业务标识作为 TAG识别时间字段转为 TIMESTAMP其余字段原样映射。我写过一个简单的转换思路先读取 MySQL 的information_schema.COLUMNS确认字段类型再把 DATETIME 映射成 TIMESTAMP数值类型按精度映射到 FLOAT/DOUBLE/INT字符串映射到 VARCHARTDengine 里是 NCHAR 或 VARCHAR注意长度限制。转换脚本的大致逻辑如下import pymysql # 读MySQL表结构 conn pymysql.connect(host10.0.1.10, userroot, passwordxxx, databasetest_db) cur conn.cursor() cur.execute( SELECT COLUMN_NAME, DATA_TYPE, COLUMN_KEY FROM information_schema.COLUMNS WHERE TABLE_SCHEMAtest_db AND TABLE_NAMEsensor_data ORDER BY ORDINAL_POSITION ) cols cur.fetchall() tag_col device_id ts_col ts fields [] for col_name, data_type, col_key in cols: if col_name tag_col or col_name ts_col: continue td_type FLOAT if float in data_type or double in data_type else \ INT if int in data_type else VARCHAR(100) fields.append(f{col_name} {td_type}) # 生成TDengine建表语句 td_sql fCREATE STABLE sensor_data_stable ( {ts_col} TIMESTAMP, {, .join(fields)} ) TAGS ({tag_col} VARCHAR(64)); print(td_sql)这个脚本能把 MySQL 表结构生成对应的超级表定义。同步数据时实时链路我会用 Canal 解析 binlog 的 INSERT 事件然后把事件内容拼装成 TDengine 的自动建表写入语句存量数据则直接用 DataX 或脚本分批查询 MySQL按设备分组写入。TDengine 建表时需要注意TAG 字段不能出现在普通列里否则写入会报错。4. 常见的坑与排查实录4.1 连不上源库Error 2002 这类连接问题“数据出海”最先遇到的不一定是同步逻辑问题而是连接问题。最常见的报错是ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock (2)这个错误基本分两种情况。一种是 MySQL 服务根本没起来用systemctl status mysqld或者service mysql status看看进程状态。另一种是客户端连接时没有走 TCP而是用了默认 socket 文件但 MySQL 实例的 socket 路径不在默认位置。解决办法是显式指定连接方式比如mysql -h 127.0.0.1 -P 3306 -u root -p这段命令强制走 TCP 协议绕过 socket 文件问题。如果 TCP 连接也失败检查 MySQL 是否监听了 3306 端口使用netstat -tlnp | grep 3306确认。远程同步场景下还需要检查防火墙和安全组是否放通了 3306 端口云厂商的安全组规则经常被忽略导致从库或 Canal 所在的机器连不上主库。连接报 SSL 错误也很常见比如ERROR 2026 (HY000): SSL connection error: protocol version mismatch。MySQL 8.0 默认开启 SSL 连接但老版本客户端或驱动可能不兼容 TLS 版本。如果同步链路是内网环境可以在不敏感的前提下关闭 SSL 要求来解决ALTER USER repl% IDENTIFIED WITH mysql_native_password BY StrongPass123; ALTER USER repl% REQUIRE NONE;4.2 主从同步中断怎么救复制链路最怕的不是慢而是断。Slave_IO_Running: No表示从库无法从主库拉取 binlogSlave_SQL_Running: No表示从库 SQL 线程回放失败。SQL 线程失败最典型的原因是主键冲突报错一般是1062 Duplicate entry或1032 Cant find record。1062 通常发生在线下补数据之后又开启同步的场景从库已经存在主键对应的数据binlog 里的 INSERT 再次执行就冲突了。处理思路是先定位从库数据与主库的差异把多出来的数据清理干净然后跳过错误事务继续同步。用 GTID 模式下可以通过设置空事务来跳过传统位点模式下可以用STOP SLAVE; SET GLOBAL SQL_SLAVE_SKIP_COUNTER 1; START SLAVE;这个操作是跳过一条错误事务使用前必须确认错误事务不会影响后续数据一致性否则跳过去反而埋下更大的雷。1236 错误则是从库的位点已经超出主库 binlog 的保留范围也就是主库 binlog 被清理了。遇到这个别慌先把主从差距评估清楚如果从库落后不多可以基于最近的全量备份重建从库如果数据差距不大且业务允许也可以直接从主库当前位点重新开始复制但这意味着丢失中间变更必须和业务确认后果。预防为主的手段是给主库设置足够长的 binlog 保留时间比如 MySQL 8.0 的binlog_expire_logs_seconds设置成 86400 或更长。4.3 数据不一致与延迟问题主从复制默认是异步的主库提交事务后不等待从库确认就返回成功。网络抖动或从库负载高时Seconds_Behind_Master会飙升。如果业务对延迟敏感可以考虑半同步复制也就是主库提交后至少等待一个从库确认再返回。MySQL 8.0 自带半同步插件配置不复杂能显著降低数据丢失窗口代价是主库写入延迟略有增加。Canal 链路的数据不一致排查思路和主从复制不太一样。先确认 Canal Server 有没有丢事件看canal.properties里的canal.zkServers和内存 store 的配置接着确认消息队列或 Adapter 有没有重复消费重复消费需要通过目标端主键幂等来规避最后溯源源库的 binlog看事件是否完整。Debug 时我习惯先在小表上做 INSERT、UPDATE、DELETE 三类操作观察目标端是否一一对应能快速定位是哪一环丢了逻辑。表结构变更导致的同步失败是比我上面任何一个问题都更隐蔽的坑。某次源库一个ALTER TABLE新增了字段结果目标端字段顺序对不上Canal 解析出来的行事件在 Adapter 里批量更新报错连带着后面所有变更全部阻塞。从那以后我定了一条规矩任何源端 DDL 变更必须提前在同步链路的测试环境验一遍再上生产。4.4 问题速查表我把实践中最常遇到的同步问题和对应处理方式整理成了一张速查表排查时按图索骥能省不少时间现象可能原因处理方式Slave_IO_Running: No网络不通/账号权限不足检查防火墙、账号 REPLICATION SLAVE 权限Slave_SQL_Running: No主键冲突或记录不存在清理数据差异或 SQL_SLAVE_SKIP_COUNTER 跳过1236 日志找不到binlog 被清理重建从库设置合理的 binlog 保留时间Seconds_Behind_Master 很大从库磁盘慢/大事务回放优化从库硬件拆分大事务考虑并行复制Canal 收不到事件binlog_format 不是 ROW 或过滤规则写错确认 binlog_format检查 filter.regexAdapter 同步报字段数不匹配DDL 只改了源库没同步目标端检查表结构差异同步 DDL 后重启任务Error 2002 socket 连接失败服务未启动或 socket 路径不对用 -h 127.0.0.1 -P 3306 强制 TCP 连接这类问题排查时最忌讳一上来就瞎猜先看日志再看状态最后才动手改配置。主从复制看SHOW SLAVE STATUSCanal 看canal.log和 Adapter 的日志文件日志里通常已经给出了足够明确的线索只要按照线索逐步缩小范围大部分问题都能在十几分钟内定位。5. 个人经验与扩展建议做同步方案这些年我最深的一个体会是同步链路开始跑通不算完成能持续稳定运行、出问题能快速恢复才算真正的完成。上生产之前先把数据校验脚本写好定期对账源端和目标端的数据量和关键字段校验值比临时抱佛脚强太多。我常用的一种方式是同时对源端和目标端跑SELECT COUNT(*)和CHECKSUM TABLE数值差异一比对不一致的表立刻能发现。另外有一个实操细节存量数据灌入和增量同步启动的先后顺序一定要严格把控。一般是先做全量导出灌入等目标端数据追齐后记录下全量导出的位点再启动增量同步。如果顺序反了先启动增量再灌存量增量事件会被存量数据覆盖或者产生大量主键冲突数据一致性直接就破了。用mysqldump --master-data2导出的文件天然带有位点信息配合 Canal 的位点设置可以做到无缝衔接。如果团队规模不大我建议一开始不要上太复杂的组件链路。MySQL 主从复制能解决的问题就用主从复制解决等确实需要异构同步了再从 Canal 这类工具里挑最轻量的一款上车。配置能写在文件里就不要硬编码监控能用一个脚本解决就不要上全家桶工具越少出问题的面就越小。同步链路本身不产生业务价值它的价值在于让数据可靠地流动到需要的地方稳永远比炫重要。