ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL数据同步方案详解:从CDC到主从复制的实操指南

MySQL数据同步方案详解:从CDC到主从复制的实操指南 上周刚处理完一个挺典型的同步需求业务库在异地机房本地分析团队要拿到远程库里的订单表做日报。最开始他们的做法是每天凌晨跑一次全量导出导入结果第二天早上领导问数据怎么还是昨天的一查发现增量数据丢了外键关联也乱了。后来我把方案换成了基于binlog的增量同步从源头抓变更实时落到本地表问题才算彻底解决。“MySQL 数据出海之数据同步方案”说的就是这类场景把MySQL里的数据从一个环境同步到另一个环境常见的有本地机房到云上RDS、总部分支机构之间跨地域同步、生产库到分析库/数仓/ES等异构目标。这篇文章我会把方案选型、工具选择、具体配置和实操细节展开讲重点覆盖CDCChange Data Capture和主从复制两条主流路线并给出“远程库单表同步到本地”的完整操作步骤。适合正在做数据同步、数据集成、异地容灾的后端开发和DBA参考。1. 先把方案选型的逻辑理清楚同步一张表和同步整个库不是一回事很多人一上来就搜工具、抄配置结果同步跑了几天才发现方向选错了。做数据同步之前你得先回答几个问题同步的是整库还是几张表是全量一次性搬家还是长期增量跟进目标端是MySQL还是MySQL之外的分析系统允许的延迟是秒级、分钟级还是小时级这些答案直接决定你该走哪条路线。1.1 “数据出海”到底在说什么场景这里说的“出海”不是指访问外网而是指数据离开它当前所在的环境进入另一个环境。不必把概念想复杂实际工作中无非就这几种业务系统在本地机房或者私有云数据需要同步到公有云上的分析平台公司有多地部署需要把总部库的数据同步到分支机构的只读库合规或灾备要求需要跨地域保留一份实时副本临时需求比如把远程库的某张核心表同步到本地分析库避免每次都要全量导出数据从OLTP库同步到数据仓库、Elasticsearch、Kafka等异构下游。这类需求的共性是不能影响源库业务、数据要尽量实时、重复要可控、出问题要能恢复。理解了场景之后再看工具你就不容易被带偏。1.2 三条主流路线定时ETL、主从复制、CDC解析我遇到过不少开发同学只知道mysqldump一种办法。其实根据同步的实时性和粒度主流路线有三条各有利弊。方案实现方式延迟适合场景主要缺点定时ETL每天/每小时mysqldump或DataX全量抽取小时级离线数仓、报表增量难做删除操作难同步主从复制MySQL原生复制binlog relay log秒级整库热备、读写分离单表过滤弱下游必须是MySQLCDC解析Canal、Debezium或客户端库解析binlog秒级单表/多表精细同步、异构落地需要开启binlog有一定开发成本定时ETL的思路最简单就是定时把数据全量拉一遍。问题是每次全量对源库压力大而且源库里被delete的数据在目标端很难感知除非你每次全量重建。这个方案只适合表不大、容忍小时级延迟的报表场景。主从复制是MySQL原生的能力本质上就是目标库伪装成源库的一个从库实时接收binlog并回放。它最稳、对开发者最透明但有个前提目标端必须也是MySQL而且你同步的粒度一般是整个实例或整个库。如果你只要同步一张表原生复制也能配但后面会讲到坑很多。CDC解析是目前处理“把远程库这张表同步到本地”这类需求最好的方式。Canal这类工具把自己伪装成MySQL从库拉取binlog然后把行变更解析成结构化事件你可以自由决定这些事件落到哪里。相比主从复制CDC对下游没有限制也支持只订阅某几张表。代价是需要引入一套新的组件或者你自己写消费逻辑。1.3 一套组合拳的推荐架构实际项目里我不会只依赖一种手段而是按阶段组合全量初始化用mysqldump或者DataX先把历史数据搬到目标端增量阶段用Canal或python-mysql-replication监听binlog实时消费变更最后再配套一个定时的数据对账任务确保任何时候双方数据是一致的。这套组合的核心思路是全量解决“历史数据”增量解决“实时变更”对账解决“万一漏了怎么办”。链路看起来长但每一段都很成熟。后面我会把每个环节的具体配置和代码写出来你照着搭就能跑。2. 远程库单表同步到本地一套可以抄作业的CDC实操先说结论如果你只需要把远程库的一张表同步到本地而且源库能开binlog优先用CDC方式不要用MySQL主从复制去做单表过滤。CDC只订阅这张表的变更事件不关心其他表目标端也可以灵活决定是否加字段、改类型。2.1 准备源库要打开binlog并给最小权限CDC的数据源是binlog所以第一步是确认源库开启了binlog并且格式是ROW。我的建议配置如下server_id 100 log_bin mysql-bin binlog_format ROW binlog_row_image FULL expire_logs_days 7 max_binlog_size 256M几个参数我解释一下binlog_format必须设为ROW。只有ROW模式才记录每一行被改成了什么STATEMENT模式只记录SQL语句你无法从语句里还原出具体变更了哪些行CDC基本没法用。binlog_row_image设为FULL让binlog里同时包含变更前和变更后的完整镜像。做数据对账、做回滚时旧值非常有用。expire_logs_days根据你网络故障容忍度来定。如果只保留1天一旦网络断了大半天binlog被清掉增量日志就断了只能重新全量。我一般至少保留7天。改完配置后重启MySQL然后创建一个专用同步账号权限要够小但必须包含复制协议需要的权限CREATE USER sync_user% IDENTIFIED BY sync_pass; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO sync_user%; FLUSH PRIVILEGES;注意REPLICATION SLAVE不是给从库用的给Canal这类伪装从库的工具也一样需要。REPLICATION CLIENT权限用于查询主库状态比如执行SHOW MASTER STATUS。很多同学只给SELECT结果Canal一启动就报权限不足。2.2 方案A用Canal做单表解析推荐用于生产Canal是阿里巴巴开源的项目专门用来解析MySQL binlog。部署其实不复杂简单几步第一步下载Canal Deployer解压到服务器。第二步修改conf/canal.properties主要关注canal.port和canal.destinations。默认不需要大改但你要知道自己端口配的是什么后面客户端连接要用。第三步修改conf/example/instance.propertiescanal.instance.master.address源库IP:3306 canal.instance.dbUsernamesync_user canal.instance.dbPasswordsync_pass canal.instance.filter.regextest_db\.tb_order这里的正则要特别注意库名和表名之间的分隔符是“.”不是“.”。如果写错了Canal看着日志里正常连接但就是收不到任何数据。test_db\.tb_order表示只订阅test_db库的tb_order表。第四步启动bin/startup.sh查看日志tail -f logs/example/example.log看到类似“successful”或者“start successful”的日志说明Canal已经伪装成从库开始拉binlog了。第五步消费端接入。Canal支持TCP模式直连消费也支持MQ模式把事件扔到Kafka或RocketMQ里再消费。我建议在正式环境用MQ模式因为Canal重启、网络抖动时MQ可以缓冲事件消费端不用跟着频繁断连。如果只是临时同步TCP模式更轻量伪代码大致这样while True: message client.get_without_ack(1024) for entry in message.entries: if entry.entryType ! ROWDATA: continue for row_data in entry.executeResult.rowData: # 根据eventType判断是INSERT还是UPDATE/DELETE # 然后执行对应的目标库SQL client.ack(message)这个场景下你其实是把Canal当成一个“增量事件源”真正干活的逻辑都在自己的消费程序里。2.3 方案Bpython-mysql-replication快速落地适合小表和临时任务如果你的场景没那么重不希望为了同步一张表就部署一套Java服务加MQ可以试试python-mysql-replication这个库。它把binlog解析包装成了Python接口很适合中小团队快速落地。先安装pip install mysql-replication下面这段代码可以处理单表的增删改事件from pymysqlreplication import BinLogStreamReader from pymysqlreplication.row_event import WriteRowsEvent, UpdateRowsEvent, DeleteRowsEvent stream BinLogStreamReader( connection_settings{ host: 源库IP, port: 3306, user: sync_user, passwd: sync_pass }, server_id101, blockingTrue, only_events[WriteRowsEvent, UpdateRowsEvent, DeleteRowsEvent], only_tables[tb_order], only_schemas[test_db] ) for event in stream: if isinstance(event, WriteRowsEvent): for row in event.rows: insert_local(row[values]) elif isinstance(event, UpdateRowsEvent): for row in event.rows: update_local(row[before_values], row[after_values]) elif isinstance(event, DeleteRowsEvent): for row in event.rows: delete_local(row[values]) stream.close()我自己实际用这个方案处理过一张几百万行的订单表效果很稳定。但有几个坑必须提醒你。第一server_id不能和源库已有的从库重了。如果重了源库会把旧连接踢掉表现是其他从库突然复制中断而你这边日志还正常。建议启动前先查一下SHOW SLAVE HOSTS;然后挑一个没被占用的server_id。第二这个库默认从你连上那一刻开始解析新产生的binlog历史数据它不管。所以一定要先做全量初始化再启动增量消费。第三only_tables和only_schemas要配合使用而且表名大小写是否敏感取决于你数据库的lower_case_table_names配置。如果你库表名有大小写混用建议先在测试环境跑一遍看看能不能收到事件。2.4 全量初始化加增量追平中间不能有缝这是整个实操里最需要细心的一步。我推荐的做法是第一步获取当前binlog位点SHOW MASTER STATUS;记录下File和Position。第二步用mysqldump全量导出并导入目标库。注意做全量导入时源库业务还在写这两步之间会有新增数据。第三步启动增量消费时不直接从SHOW MASTER STATUS拿到的位点开始而是往前推或者在这段时间里临时积攒binlog事件。一个比较稳的简单做法是先全量导完后立刻执行一次SHOW MASTER STATUS然后从该位点开始增量。这样全量导出期间产生的数据会被增量线程补上但因为全量快照本身不包含导出期间的数据所以不会重复。唯一的代价是全量导出期间源库如果有DDL可能导致快照和binlog衔接不齐这种情况比较少见遇到了就对账发现。如果你用Canal它本身就持久化位点重启后能从上一次消费的位置继续。如果你用python-mysql-replication需要自己在消费循环里定期记录当前处理到的binlog文件名和位置with open(offset.txt, w) as f: f.write(f{stream.log_file}:{stream.log_pos})这样进程重启后可以在BinLogStreamReader里传入log_file和log_pos参数从上次位置继续。这很重要否则同步任务一重启就是漏数据事故。2.5 落地端写入的细节幂等是第一原则增量事件到了目标端之后写入逻辑要非常小心。Canal或者python-mysql-replication都可能因为网络、重启等原因重复投递事件如果写入不幂等就会产生重复数据。我的经验是目标表一定要有唯一键最好就是源表的主键。没有唯一键的同步方案都是耍流氓。对于INSERT事件不要先查再插直接用INSERT INTO tb_order (id, ...) VALUES (...) ON DUPLICATE KEY UPDATE col1VALUES(col1), col2VALUES(col2);这样即使重复消费也只是覆盖写不会报错。对于UPDATE事件用主键做定位更新其他业务字段。对于DELETE事件用主键DELETE。如果源表的delete是物理删除消费端也必须物理删除不能心软只做标记。目标表如果还想记录同步时间可以额外加一个sync_time字段用本地写入时间填充。这样排查“数据是什么时候到目标库的”特别方便。3. 整库出海主从复制方案怎么落地如果你的目标是完整同步整个MySQL实例而且目标端也是MySQL那用原生主从复制是性价比最高的选择。它不需要额外引入组件MySQL自己就把binlog传输、回放、断线重连这些事都干了。3.1 什么时候该用主从复制我在这些场景下会毫不犹豫选择主从复制异地容灾热备目标库要随时准备接管流量读写分离把一部分读请求放到从库需要完整保留所有库表、存储过程、触发器等数据库对象希望用GTID管理复制位点简化故障切换。反过来如果下游不是MySQL或者只需要几张表就不要勉强用主从直接走CDC。3.2 主库配置和GTID现代MySQL复制建议开启GTID因为它让主从位点对齐变得非常简单。主库配置server_id 1 log_bin mysql-bin binlog_format ROW gtid_mode ON enforce_gtid_consistency ON log_slave_updates ON几个参数的作用gtid_mode打开后每个事务都有全局唯一的事务ID从库用GTID自动定位不用人工记binlog文件名和位置。enforce_gtid_consistency保证所有事务都能被安全复制。log_slave_updates让从库也记录自己的binlog。如果从库下面还有从库或者要从从库再做数据同步这个必须开。然后用mysqldump做全量备份并记录位点mysqldump -u root -p --single-transaction --master-data2 --all-databases backup.sql--single-transaction保证备份期间不锁表--master-data2会在备份文件注释里记录当时的binlog位点。把backup.sql传到目标库导入mysql -u root -p backup.sql在目标库执行CHANGE MASTER TO MASTER_HOST源库IP, MASTER_PORT3306, MASTER_USERsync_user, MASTER_PASSWORDsync_pass, MASTER_AUTO_POSITION 1; START SLAVE;然后查看复制状态SHOW SLAVE STATUS\G重点关注三个字段Slave_IO_Running: Yes表示IO线程能正常从主库拉binlogSlave_SQL_Running: Yes表示SQL线程能正常回放Seconds_Behind_Master: 0表示延迟为0数值越大说明追赶越慢。3.3 跨机房/公网场景下的注意事项主从复制在同一个内网里很容易跑稳但一旦跨地域、跨云、走公网问题就多了。我踩过的坑集中在这几点第一网络延迟会直接体现在Seconds_Behind_Master上。跨地域专线的延迟通常在几十毫秒内勉强能接受如果走公网延迟可能到几百毫秒。这时候可以考虑开半同步复制让主库等待至少一个从库确认收到binlog再提交事务。半同步能在一定程度上保证数据不丢但也会增加写入延迟需要业务侧接受。第二公网环境一定要开SSL传输。MySQL从库的CHANGE MASTER语句里可以指定CHANGE MASTER TO MASTER_SSL1, MASTER_SSL_CA/path/to/ca.pem, ...不开SSL的话binlog内容在网络上明文传输风险很大。等出问题再补配置代价远高于一开始就做好。第三binlog保留时间要拉长。跨地域网络不可能永远不抖动一旦断连时间超过binlog保留时长从库就只能重新全量备份。我之前把expire_logs_days设成3天结果一次专线断了2天勉强活着但已经吓得够呛。现在跨地域场景我至少保留7天。第四源库的DDL操作要收敛。大表加字段、改表结构在主库可能秒级完成但在从库上可能因为表数据量大、需要重建表而执行很久。期间从库的SQL线程会卡住所有后续事务都会排队延迟瞬间飙升。应对办法是DDL尽量安排在业务低峰并且提前评估目标库执行所需时间。3.4 单表过滤为什么我不推荐有人试图在主从复制里只同步一张表用replicate-do-table配置。从配置本身看很简单replicate-do-tabletest_db.tb_order但实际用起来很麻烦。一方面主库binlog里其他表的事务也会传输到从库从库要花时间过滤跳过效率并不高另一方面如果这张表和其他表存在跨库事务或者你在源库对该表做过DDL从库回放可能直接报错而且排查起来很痛苦。更麻烦的是从库上还会残留其他表的数据这些表不更新时间一长容易让人误以为它们也是新的。所以只要你的需求是单表或几张小表就老老实实走CDC。4. 一致性校验与延迟监控同步任务跑了三天我怎么确认没丢数据数据同步和写业务代码不一样业务代码有输入输出可以测试数据同步跑起来之后如果你不主动校验很难发现数据是不是悄悄丢了。所以从第一天起就要建立一套对账机制。4.1 先做一个能落地的对账方案对账不需要一上来就搞分布式校验那样太重了。我的习惯是从粗到细分三个层次第一层行数对比。每个整点对一次COUNT虽然慢但能发现大的增删差异。对超大表不要全表COUNT可以对主键范围抽样统计。第二层分片校验。把表按主键切成若干段每段算一个校验值对比源库和目标库。比如10万行一分片每片用SELECT COUNT(*), COALESCE(SUM(CRC32(CONCAT_WS(#, id, field1, field2))), 0) FROM db.tb_order WHERE id BETWEEN 0 AND 100000;把两边的count和校验值都算出来任何一边不一致就说明这段分片有问题再逐行定位。第三层逐行对比。只在分片校验不一致时才做用主键把差异行拉出来看。工具方面熟悉Percona Toolkit的同学可以直接用pt-table-checksum和pt-table-sync。但要注意这两个工具是为MySQL主从复制设计的如果你用的是Canal自研同步链路它们不一定能直接套用这时候更适合写脚本自己算分片校验。4.2 监控同步延迟的几种方式同步延迟是数据同步最重要的指标没有之一。我见过很多人搭完同步就不管了等到数据对不上才发现。对于MySQL主从复制最直接的是采集SHOW SLAVE STATUS里的Seconds_Behind_Master。用mysqld_exporter配合Prometheus就能做到秒级告警。对于Canal链路Canal自带metrics接口可以暴露当前消费到的最新binlog位点然后用程序对比源库的SHOW MASTER STATUS差值就是积压量。对于python-mysql-replication这种自研方案可以在消费循环里定期记录当前处理到的binlog文件名和位置同时起一个定时任务查询源库的SHOW MASTER STATUS;两个位点一对比就能估算延迟秒数(源库当前位点 - 本地已消费位点) / 每秒binlog平均字节数 延迟秒数这个延迟值最好落到监控系统里超过阈值就告警。针对数据出海场景我个人建议报表类同步延迟超过60秒告警容灾类超过5秒就要盯紧。4.3 故障恢复的三板斧同步任务挂了不要慌按照固定套路恢复先看位点再看binlog是否还在最后决定是续传还是重灌。位点很好理解消费端定期把binlog文件名和位置写到一个状态表。恢复时从最近一次记录的位置继续消费。但这有一个前提源库binlog还在。如果binlog早被清理了续传就不可能了只能全量重灌。全量重灌听起来粗暴但在数据严重不一致时是最快的恢复手段。比你在那里一点点补数要省心得多。正式环境我的原则是先保一致再谈效率。只要确认增量日志断了直接全量再来一遍然后重新接增量。第三板斧是切换演练。如果目标端最终要承担读写不要只在源库正常时演练要定期在生产低峰期做一次主从切换把流量切到目标端跑几小时再切回来。这个动作能提前暴露权限、网络、延迟、应用连接串等各种问题。我参与过的每次切换演练都能发现点新问题演练过几次后真出事的时候大家都心里有底。5. 踩坑记录这些坑我基本都踩过一遍前面讲了很多正确做法下面把实际运维中最容易翻车的问题集中列一下希望能帮你少走弯路。5.1 binlog相关源库binlog_format不是ROW。Canal能连上、不报错但解析不出数据或者只拿到BEFORE镜像没有AFTER镜像。我排查过一次查了半天才发现是历史配置遗留下来的问题。max_binlog_size设得太大。binlog文件太大网络抖动后Canal重新连接要从断点继续拉由于单个文件过大重放耗时很长。建议控制在256M以内。开启了GTID之后传统基于文件和位置的复制语句可能会报错。如果你两种复制方式混用注意看报错信息不要凭空猜测。5.2 网络与权限同步账号权限不足时报错信息往往不直接说“缺权限”而是告诉你“连接被关闭”或者“认证失败”。直接按最小权限原则把SELECT、REPLICATION SLAVE、REPLICATION CLIENT都给了能省很多排查时间。多个从库或消费者共用server_id导致旧连接被踢掉。这个问题最难排查因为日志里不明显要跑到源库上查SHOW SLAVE HOSTS才能确认。目标库和源库的表结构不完全一致。比如源库是utf8mb4目标库建表时用了utf8mb3某些特殊字符会变成乱码或者直接报错。建议同步前对比一下两边的字符集和排序规则。5.3 数据层面大事务会放大同步延迟。一条UPDATE影响100万行的SQL在源库执行只要几秒但binlog会产生100万条行事件Canal消费这一大批事件可能要十几分钟甚至更久。所以同步链路的健壮性不只是工具和网络的事业务侧也要尽量拆小事务。UPDATE主键的坑。源库把某一行主键从1改成2binlog事件里before是旧主键after是新主键。消费端如果只按新主键去更新就会多出一行。这个很容易漏处理很多同步工具其实没处理好主键变更。DATETIME时区问题。源库服务器是CST时区目标库容器是UTC时区binlog解析出来的时间会差8小时。统一的做法是在消费端明确指定解析时区比如都按Asia/Shanghai解析目标库存的就是业务时间明文。5.4 我自己的几个土办法说几个不入流但很管用的习惯。第一我会在源库放一张心跳表每5秒更新一次时间戳同步程序同时消费心跳表的变更和目标端本地时间对比这个“心跳差值”比任何延迟指标都直观。第二我会在目标表上加一个sync_time字段哪天业务反馈数据有问题先看同步时间就知道问题发生在哪个环节。第三每次调整同步配置前先做一次全量备份回滚起来才有底。结尾跑数据同步这些年我的体会是同步方案技术上不算难难的是把每个细节都想到前面。选型之前先搞清楚自己要的是整库还是单表实时性要求多高选了CDC就认真做好幂等和位点记录选了主从就要把GTID、SSL、binlog保留时间这些基础工作做扎实最后不管用哪条路对账和监控都不能省。你把这套东西搭好之后同步任务反而成了整个系统里最让人放心的部分。希望这篇基于实际操作经验的分享能帮你少踩几个我踩过的坑。
返回列表