大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。
有一次对账,两边总额差了一笔。我先怀疑是数据出错,翻了半天流水,主库那笔是好端端的。问题不在主库,在从库:同一个订单号,从库读出来的状态,跟主库写进去的不是一回事。奇怪的是,主从延迟曲线一直很平,SHOW SLAVE STATUS 也没报错。延迟没红,数据却早就对不上了。我盯了那么久的那个指标,根本没看住这件事。
后面我才搞懂,「同步得上」和「数据对得上」,本来就是两回事。前者是性能,后者是正确性。延迟正常,不代表数据一样。今天这篇就把我在这上面踩过的坑分享出来,希望帮你少走弯路少踩坑。
不一致从哪来
知道了它会发生,接下来就得问:好端端的复制,怎么会跑歪。主从不一致,常见四个源头。
第一类,非确定性语句。STATEMENT 格式存的是 SQL 原文。从库要把它重放一遍,函数就在从库上重新求值。UUID()、RAND() 每次结果都不同,主从自然不一样。最容易被误会的是 SYSDATE()。很多人以为它跟 NOW() 一样。其实 NOW() 会被 binlog 里的 SET TIMESTAMP 固定住,重放时值一致。SYSDATE() 不认这个,取的是从库的真实时间,主从就会差。还有 LIMIT 不带 ORDER BY,取哪几行全凭引擎顺序,主从各取各的。
第二类,GTID 空洞。复制报错时,为了尽快恢复,很多人会用 sql_slave_skip_counter 跳过那笔事务。跳过的代价,是从库的 GTID 集合里少了一条,数据永远缺着。它的隐蔽在于,跳完之后复制继续跑,SHOW SLAVE STATUS 一切正常。如果你不去比对数据,根本不知道会少了这一笔。
第三类,并行复制冲突。MySQL 开了多线程复制(slave_parallel_workers > 0),主库提交的一组事务,从库按依赖关系并行回放。顺序一旦和主库不一样,比如两笔交易交叉写了同一行,从库的 SQL 线程就会停住报错。常见的是 1062(Duplicate entry)和 1032(Cannot find record),背后多半就是回放顺序对不上。5.7 的 LOGICAL_CLOCK 和 8.0 的 WRITESET 都在改善,但冲突窗口没法完全关死。
第四类,手工改从库。从库没开 super_read_only,有人上去直接改数据。改完复制继续跑,没人报警。
校验:pt-table-checksum 怎么用
四个源头,有的能提前防,有的只能事后抓。但不管你怀疑哪一个,第一步都一样,先确认到底哪里不一致。我用的工具是 pt-table-checksum,Percona Toolkit 里的分片校验工具。
它的工作原理是这样的。工具连上主库,把每张表切成若干 chunk,对每个 chunk 计算 CRC32 校验和。这个校验和通过 binlog 复制到从库,从库也存一份同样的结果。然后工具去从库查那份校验表,把主库和从库的 CRC 逐 chunk 对比。不一样的那个 chunk,就是数据不一样的地方。
# 在主库上跑,把结果写进 percona.checksums 表
pt-table-checksum \
--replicate=percona.checksums \
--databases=app \
--chunk-size=1000 \
--sleep=0.1 \
h=master_host,u=checksum_user,p=xxx
两个参数要盯紧。--chunk-size 是每次校验的行数,太大拖库,太小切太多片。一般一千到五千,看表大小。--sleep 是每次 chunk 之间的间隔秒数,给主库喘口气。别在高峰跑,或者 --sleep 设大点。
还有一个自动限流的参数,--max-load,很容易被漏掉。它盯的是活跃线程数(Threads_running),默认超过 25 就自动暂停,等压力降下来再接着跑。这才是真正的兜底——前面两个参数是你自己估出来的,它是盯着主库的实时水位动态刹车。
跑完之后,它会给一份输出。关键看 DIFFS 列。
TS ERRORS DIFFS ROWS DIFF_ROWS CHUNKS SKIPPED TIME TABLE
10-15T14:20:00 0 1 2345678 0 234 0 123.4 app.t_order
10-15T14:20:05 0 0 123456 0 12 0 5.6 app.t_user
(输出格式为示意,实机以 Percona Toolkit 实际输出为准。)
DIFFS 非零,说明这张表的至少一个 chunk 在主从两边不一致。上面 t_order 的 DIFFS = 1,就是不一致。
这里有个前提别忘了。校验和是先在主库算好,再靠 binlog 传到从库去比对的。也就是说,复制本身得是通的。要是复制已经停了、卡着,工具拿到的比对也会失真。所以别拿校验去判断复制通不通,那个因果是反的。
定位:从差异到具体行
知道哪张表有问题了,下一步是找具体差在哪。去从库查校验表。
-- 从库上查:哪些 chunk 的 CRC 对不上
SELECT db, tbl, chunk, master_cnt, this_cnt, master_crc, this_crc
FROM percona.checksums
WHERE master_crc <> this_crc
AND this_crc IS NOT NULL;
拿到 db、tbl、chunk 之后,就知道是哪块分片出了问题。要看到底差哪行,有两种办法。一是把主从两边的这个 chunk 分别导出来 diff。二是用 pt-table-sync 加 --print 预览,看它打算生成哪些 corrective 语句。
# pt-table-sync 预览:它会输出在主库上执行的 corrective SQL
pt-table-sync \
--print \
--replicate=percona.checksums \
h=master_host,u=sync_user,p=xxx \
h=replica_host,u=sync_user,p=xxx
注意:--print 只打印,不执行。先看清它要改什么,再决定下一步。
修复:pt-table-sync 的安全边界
pt-table-sync 能自动把从库拉回和主库一致。但它是个危险操作,用错了会雪上加霜。
# 确认无误后执行修复(必须在主库上跑,变更通过 binlog 传播到从库)
pt-table-sync \
--execute \
--replicate=percona.checksums \
h=master_host,u=sync_user,p=xxx \
h=replica_host,u=sync_user,p=xxx
三条安全线,一条都不能省。
第一,执行前必做备份。不管是 mysqldump 还是物理快照,先把当前状态留底。万一修错了,能退回去。
第二,必须在主库上跑,变更通过 binlog 正常复制到从库。如果直接连从库跑,变更不复制,两边又会差。
第三,别在高峰期盲目 sync。它会生成大量的 UPDATE / INSERT / DELETE,吃 IO,也吃复制带宽。选低峰窗口跑。
预防:别等出事再查
修复只是止血,预防才是根治。
binlog_format=ROW,MySQL 5.7 起默认就是 ROW,但老库还是 STATEMENT 或 MIXED 的,尽早切。ROW 格式存的是行级变更,不依赖函数重算,能挡掉大部分非确定性语句的问题。
super_read_only=1,从库开只读。注意 read_only 挡不住有 SUPER 权限的用户,要开到 super_read_only。业务账号本身没写权限,从库再锁一层,基本杜绝手工改从库。
定期跑 checksum 校验。不是出了事才跑,是当成周常或月常任务。设个 cron,低峰自动跑,结果进告警。不一致要能在监控里先看见,不是靠用户投诉才发现。
GTID + 变更管控。开了 GTID 后,别再用 sql_slave_skip_counter 跳过事务。遇到需要跳的场景,先分析原因、留档、再操作。每次 GTID 操作都要记录,留痕可追溯。
同步得上,不等于对得上
主从健康,从来不是一条延迟曲线能说清的。延迟正常,只说明从库在追,还追得挺快。它一个字都没保证,追到的东西对不对。同步得上是性能,对得上才是正确性。性能能靠监控盯,正确性只能靠校验换。你省掉的那次 checksum,不会消失。它只是变成某天对不上、又查不出从哪来的那笔账。
避坑清单
想少欠几笔这样的账,日常把这几点盯牢。
非确定性语句是主从不一致的常见源头。NOW()、UUID()、RAND() 在 STATEMENT 格式下各算各的,业务 SQL 要尽量确定性,或确保用 ROW 格式。LIMIT 不带 ORDER BY 也是坑。
校验要在低峰跑,并且限流。pt-table-checksum 不轻,--chunk-size、--sleep、--max-load 设好了再跑,别把主库拖垮。
最后一条是我自己踩的。我有一次跑 pt-table-sync --execute,没先备份也没用 --print 预览,结果一行数据的修复把关联表也带歪了。那次之后我定了条死规矩:sync 之前先 --print,--print 之后先备份,备份完才 --execute。多花十分钟,少救一次火。
写在最后
主从数据不一致,最难的不是修,是它发生了你却不知道。延迟曲线太好看,反而让人放松警惕。数据不像延迟会喊疼,它只会安安静静地,主库存一份,从库存一份,各是各的。
定期校验、从库只读、binlog 用 ROW、GTID 操作留痕,这几条听着老生常谈。可每一条,都是真踩过之后才肯老老实实补上的。复制能跑不稀奇,跑得对才值钱。
你们的主从校验是怎么跑的?多久一次,用的什么工具?评论区聊聊。
我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋