资讯动态

MySQL主从数据不一致?用pt-table-checksum快速定位修复

发布时间:2026/10/22 16:23:56 来源:尧图企业网站定制
凌晨一点多手机开始疯狂震动。某报表库的监控群刷出几十条消息当天凌晨的统计结果比另一个来源少了整整几万行。第一反应是主从切换导致的数据丢失结果从库状态、复制线程、主从位点全部正常没有一条报错。开发同事在群里问了一句主库和从库的数据到底还一不一致全表几条语句 count 下来数量差不多但没人敢说某一行某一条数据百分百对得上。那天晚上我做了两件事先锁定了问题的大致时间窗口再用 pt-table-checksum 对核心大表做了一次逻辑校验。二十分钟后差异清单已经明明白白摆在面前原因也很快定位——有人在从库上绕过业务系统执行过手工 DML从库数据被改了复制链路却一直“健康”。这篇就把我使用 pt-table-checksum 做主从数据校验的完整经验写下来包括它的原理、部署、实操命令、结果解读、遇到线上高负载怎么调优以及校验完成之后怎么收尾。1. pt-table-checksum 是干什么的一次让我记到现在的线上事故1.1 半夜的告警报表数据为什么对不上了那次事故的前因其实很简单。白天有开发同事反馈线上某个查询很慢顺手就在从库上执行了几条 DML 语句想“临时修一下数据”。从库的复制链路没有断主库正常写入从库也在正常回放但已经提前被手工改过的行后续主库的 binlog 事件再过来时要么产生冲突、要么被跳过最终结果就是从库和主库在部分行上出现了静默的数据不一致。更麻烦的是MySQL 本身不会主动告诉你这件事binlog 里也没有任何错误日常的复制状态监控完全发现不了。这种问题最典型的特点就是主从同步正常但数据已经悄悄分叉。等报表、统计、下游应用真的读取到差异数据时问题已经发酵了很久。那天夜里我试着做的校验其实解决的就是一个问题——在不影响主库写入、不锁大表的前提下快速找出主从之间存在差异的表、差异行范围甚至精确到块的边界。1.2 它和“手工对表”的本质区别很多 DBA 在没有专门工具时会自己写 SQL 在主从两边分别执行SELECT COUNT(*), SUM(id), MAX(id) FROM big_table;这种粗粒度比对只能说明“表面数量对得上”对于中间某几条被改动、总数恰好一致的情况完全无感。更细一点的做法是把数据按主键分段每条 SQL 算出该段的聚合值再拿到从库对比。听起来可行实际上有两个大问题第一手工分段很难对边界做到精确稳定分少了查询次数爆炸分多了单条 SQL 掏空内存第二主从两端执行的时间点不一致主库这边查完从库还没来得及回放那几秒的写入比对结果就会出现大量“伪差异”根本无法收场。pt-table-checksum 解决的就是这两个痛点。它在主库上按主键把表拆成多个 chunk每一个 chunk 独立计算校验和同时把同样的计算过程发给从库执行并在执行前充分考虑复制延迟确保从库“追平”到和主库一致的时间点再计算。最后结果写入一张复制表每个 chunk 一行主库的 CRC 值和从库的 CRC 值摆在一起DIFFS 一列就把差异数量直接标出来。你可以把它理解成一种“对账机制”不是比总额而是比每一笔明细的指纹。2. 掌握原理再动手分块、校验和与对比链路2.1 主键分块一块一块吃才不会噎死先解释为什么它敢在几亿行的大表上跑。核心思路是“分块”英文里叫 chunk。工具有两种分块策略一种基于行数默认--chunk-size1000行左右另一种基于时间通过--chunk-time控制单个 chunk 的执行耗时。实际运行时会先在主库执行一个估算查询根据表的统计信息拿到大致行数和主键分布再动态决定每个 chunk 的边界。分块的意义有两个。第一单条校验 SQL 只需要扫描一个主键范围比如WHERE id BETWEEN 10001 AND 11000对这个范围内的数据计算校验和内存和 IO 压力都被限制在可控范围内。第二主库执行一个 chunk 后从库也执行同一个范围的 chunk因为两边在主键边界上是完全一致的所以比对才有意义。假如整表一把梭SQL 执行时间拉得很长从库的读取点又一直在推后比对必然失真。我把这个机制理解成“切牛排”。一整块牛排很难一次入口切成小块之后每一块的大小固定咀嚼时间也固定。工具内部还会根据上一块的执行耗时自动调整下一块的大小如果某个范围内的数据长度特别大、算得很慢下一块就自动变小如果表很小、算得飞快块就放大最终目标是让每个 chunk 的执行时间维持在一个合理区间。2.2 校验和怎么算拼起来算一下比一比每个 chunk 的校验 SQL本质上就是把这一块内的每一行数据拼成一个字符串然后对字符串算一个哈希值。工具会先读取表结构获取所有列的信息把各列的值通过CONCAT拼接拼接前还会做各种精细化处理包括 NULL 值处理、浮点数格式统一、字符集转换、二进制字段的十六进制化等等。这些细节非常关键因为如果不统一浮点数的字符表示两边算出来的字符串不一样哪怕实际数据一模一样也会被判为差异。拼接完成之后再使用校验和函数把长字符串压缩成一个固定长度的数字这个数字通常被称为 crc。主库算出一个值从库对同样范围的数据算出一个值两个值放进 checksums 表里面对比。this_crc和master_crc的字段名就是这么来的——master_crc是在主库计算的值this_crc是当前连接看到的副本中计算的值。这个方案看起来不复杂但工程上难在细节如何让主从两边对同一行数据生成完全一致的字符串。比如DECIMAL类型的尾随零、VARCHAR类型的末尾空格、CHAR类型的填充空格以及不同sql_mode下的隐式转换任何一个偏差都会造成误报。工具在构造校验 SQL 时会根据列的元数据生成专门的转换表达式这也是它比手工写CONCAT可靠得多的原因。2.3 结果写到复制表为什么不是临时表校验过程中每个 chunk 的结果都会被写入一张复制表默认库名是percona表名是checksums。可能有人会问结果为什么不直接打印在终端或者写到本地临时文件非要落到数据库里关键在于这张表本身也会被复制到所有从库。每一行代表一个 chunk主库写入这个 chunk 的结果后binlog 会把这次的 INSERT 也推给从库。由于 checksums 表是复制表从库上也存在同样的 chunk 记录工具只需要在从库这边查这张表把主库相关行和从库相关行进行比较就能知道差异情况。这个设计等于把“比对结果”本身也纳入了复制链路天然地保证了比对数据不会因为时间差而错乱。而且这张表的存在让“多次校验”变成了有历史记录的审计每次跑完边界、CRC、行数都留在表里。第一次发现问题时把结果表备份起来修复之后再次校验还可以对比两次的 chunk 记录。我习惯把这类结果表保留一段时间防止下次需要追溯问题时发现之前的证据已经没了。3. 部署和前置条件跑通的前提与权限模型3.1 安装percona-toolkit 与 Perl 依赖pt-table-checksum 是 Percona Toolkit 工具集里的一员安装方式非常简单。在 CentOS/RHEL 系系统上配置好 Percona 官方软件源之后一条命令即可yum install -y percona-toolkitDebian/Ubuntu 系则是apt install -y percona-toolkit离线环境下可以找一台同架构且有网络的主机下载好 rpm 或 deb 包再拷贝进内网安装。工具本身是 Perl 脚本不需要编译但依赖perl-DBI和perl-DBD::MySQL两个模块装完 toolkit 之后可以用pt-table-checksum --version验证一下。安装完成后的验证我每次都会跑pt-table-checksum --help不是为了看帮助而是为了确认 Perl 模块加载没有报错。很多机器上 toolkit 装好了但 DBD 模块没装命令一执行就报Cant locate DBD/mysql.pm这时候补装对应的 Perl 模块就行。这个小坑我在不少环境里都遇到过。3.2 账号权限给多了危险给少了报错很多第一次用这个工具的同学经常被各种权限报错劝退。先明确它需要的权限模型在源端和目标端都需要SELECT权限用于正常读取数据和执行校验 SQL需要INSERT、UPDATE、DELETE权限用于在percona.checksums表里写入和清理校验结果需要CREATE、DROP、ALTER权限用于自动创建 checksums 表及其索引需要PROCESS权限用于查看连接信息和线程状态需要REPLICATION CLIENT权限用于查找主从拓扑关系在旧版本 MySQL 上还需要SUPER权限用于设置会话级变量MySQL 8.0 之后根据具体版本可能需要拆分出的动态权限。我这里给出一个常用的最小权限授权语句实际使用可以按需收敛GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, PROCESS, REPLICATION CLIENT ON *.* TO checksum_user% IDENTIFIED BY your_password;这里必须用*.*因为校验过程可能要读取任意库的表并且需要访问 MySQL 内部的 replication 信息。有人问过能不能只给部分库权限从安全角度更好但从工具实现角度它会去读取 master 信息、进程列表、服务器变量这些都属于实例级权限没法精确定位到库。3.3 运行库的选择主从拓扑里怎么下命令pt-table-checksum 的正确使用姿势是在从库上发起让它自动去找主库然后分别在主库和从库上执行校验。为什么不在主库上直接跑因为工具会在主库执行 chunk 计算也会在从库执行 chunk 计算两边的时间差控制非常重要。在从库上发起时工具能更准确地判断从库的复制延迟状态并且通过--max-lag参数控制追平节奏。如果你从主库发起天然就缺少了“从库视角”延迟控制也就无从谈起。具体命令形如pt-table-checksum \ --host192.168.1.101 \ --userchecksum_user \ --passwordxxx \ --databasesapp_db \ --tablest_order \ --replicatepercona.checksums这里的--host指向从库。工具会向上递归找到主库然后自动完成双向比对。需要强调的是从库必须在复制拓扑中是正常连接的而且连接用户需要具备读取主库信息的权限。如果你的从库设置了replicate-ignore-db或replicate-wild-ignore-table之类的过滤规则工具会检测到并给出警告比如--nocheck-replication-filters这个参数就是用来跳过这类检查的但跳过之前你自己要先想清楚后果。4. 实战跑一遍命令拆解与输出解读4.1 最小可用命令和最关键参数实际项目中我一般会按库和表粒度分批跑不会一上来就全实例梭哈。第一轮先跑所有业务库但跳过明显不需要校验的大日志表。命令如下pt-table-checksum \ h192.168.1.101,uchecksum_user,pxxx,P3306 \ --databasesapp_db \ --ignore-tablesapp_db.t_log_archive \ --replicatepercona.checksums \ --nocheck-replication-filters \ --max-lag1 \ --max-loadThreads_running20 \ --chunk-time0.5 \ --empty-replicate-table参数逐个说清楚--replicatepercona.checksums指定校验结果表。如果不存在工具会自动创建--empty-replicate-table每次运行前先清空结果表。注意这个参数对从库的 checksums 表也会生效但因为是复制表从库的清空动作由 binlog 复制完成无需额外处理--max-lag1当从库复制延迟超过 1 秒时暂停校验等延迟追上再继续。这是保护线上环境最重要的参数之一--max-loadThreads_running20当主库并发线程数超过 20 时工具会主动放缓执行避免给业务造成冲击--chunk-time0.5目标是让每个 chunk 的执行耗时控制在 0.5 秒左右工具动态调整 chunk 行数来满足这个目标--nocheck-replication-filters跳过复制过滤检查。如果从库配置了复制过滤工具默认会拒绝执行加了这个参数才允许跑具体风险后面说。4.2 终端输出与 checksums 表字段逐行看懂命令跑起来之后终端会实时输出一张进度表大致长这样TS ERRORS DIFFS ROWS CHUNKS SKIPPED TIME TABLE 01-12T01:05:32 0 0 145323 15 0 6.321 app_db.t_user 01-12T01:06:01 0 0 882341 30 0 29.884 app_db.t_order 01-12T01:06:28 0 2 190032 22 0 26.512 app_db.t_payment每一列的含义TS校验执行的时间点ERRORS执行过程中出现的错误次数DIFFS主从之间差异的 chunk 数ROWS该表累计校验过的行数CHUNKS该表被切分成的 chunk 数SKIPPED因异常跳过未校验的 chunk 数TIME处理整张表消耗的时间TABLE当前处理的表名。我最关注的就是DIFFS。只要DIFFS大于 0说明这张表存在主从不一致的 chunk。但这只是“有差异”的信号具体差异在哪一行还得去 checksums 表里看。checksums 表结构里几个关键字段db、tbl库名和表名chunk第几个 chunklower_boundary、upper_boundary当前 chunk 的主键下界和上界this_crc、this_cnt当前连接所在从库计算出的校验值和行数master_crc、master_cnt主库计算出的校验值和行数chunk_time执行这个 chunk 所花费的时间。如果this_crc与master_crc不一致或者this_cnt与master_cnt不一致就说明这个主键范围内存在差异。差异的定位粒度是 chunk不是行。对于一个几百万行的表一个 chunk 可能覆盖几千行范围已经很小接下来可以直接在主从两端用主键范围查数据逐条比对或者借助后面要讲的 pt-table-sync 做行级修复。4.3 把校验接入日常巡检脚本跑一次只能解决眼前的问题真正的价值是把它变成固定巡检。我在实际项目中会把这次命令包成一个脚本每天凌晨低峰期自动执行并把结果按 DIFFS 数量输出到监控系统。下面是一个很简单的巡检脚本骨架#!/bin/bash source /etc/profile export PATH$PATH:/usr/local/bin:/usr/bin DB_HOST192.168.1.101 DB_USERchecksum_user DB_PASSxxx REPLICATE_TBLpercona.checksums LOG_FILE/var/log/pt-checksum.log pt-table-checksum \ h$DB_HOST,u$DB_USER,p$DB_PASS,P3306 \ --databasesapp_db \ --replicate$REPLICATE_TBL \ --max-lag5 \ --max-loadThreads_running30 \ --chunk-time1 \ --empty-replicate-table \ --quiet \ $LOG_FILE 21 EXIT_CODE$? if [ $EXIT_CODE -ne 0 ]; then echo pt-table-checksum 执行异常退出码$EXIT_CODE $LOG_FILE fi工具退出码非零时往往说明有差异或执行异常可以在监控平台配置一个基于退出码和日志内容的告警规则。脚本里的--quiet参数可以减少输出噪音只输出错误和关键进度。实际上我更推荐在脚本里把DIFFS大于 0 的表单独提取出来发送到 IM 告警群输出过去人工排查的时间成本会小很多。5. 面对大数据量和线上负载常用调优与报错排查5.1 chunk-size 和 chunk-time 怎么搭配很多人在大表上跑 pt-table-checksum 后第一反应是“太慢了”。慢的根源往往不是工具本身不行而是 chunk 参数的默认值不一定适配你的环境。默认的--chunk-time0.5表示单 chunk 执行目标不超过 0.5 秒对秒级延迟敏感的压力环境很友好但在 IO 能力很强的机器上会显得过于保守导致 chunk 数量非常多整体耗时长。我的习惯是先在小范围表上跑一次观察CHUNKS和TIME的比例再决定调参方向。如果单表几千万行、机器负载很低可以把--chunk-size固定为一个较大行数同时把--chunk-time调大到 2 秒让每个 chunk 处理更多行减少调度开销。反之如果线上业务很敏感--chunk-time0.2甚至更小让每个校验 SQL 快速结束虽然总耗时变长但对业务影响最小。再强调一次这两个参数本质是“时间换负载”还是“负载换时间”的选择题。所有建议的前提都是先大致确认你业务库的峰值时段和 IO 水位再动手。5.2 max-load 与 max-lag压不住负载就别硬跑线上校验最怕的不是慢而是把主库线程数打到告警线。--max-load的作用就在于此它的默认值是 25也就是Threads_running超过 25 时工具会等一等。实际使用中我会把阈值调到业务低峰期正常运行线程数的 1.5 倍左右既能充分利用空闲资源又不会冲垮主库。--max-lag的作用前面说过是控制从库延迟。但这里有个隐藏细节一次校验如果涉及多个从库工具默认只会监控最慢的那个从库。如果你的拓扑里有一个开发用的测试从库延迟长期上百秒工具会因为--max-lag而被“拖住”大部分时间都在等待而不是在干活。这时候可以用--recurse0关闭递归查找从库或者用--slave参数明确指定要校验的从库。这个坑比较隐蔽遇到过好几位同行拿着配置来找我最后定位到的是测试从库拖后腿。5.3 高频报错和处理从“No slaves found”到“Would be auto-replicated”我遇到的几千次执行里最有代表性的报错就那么几类列在这里报错信息含义处理方法Cannot connect to MySQL连接不上目标实例检查端口、账号、网络和安全组DBD::mysql server has gone away校验 SQL 过大导致连接中断调小--chunk-size或者检查 max_allowed_packetNo slaves found当前连接上找不到从库拓扑确认是否用的是从库地址发起且连接用户有 REPLICATION CLIENT 权限A slave is found but it is not replicating从库 IO 或 SQL 线程停了先修复复制链路再跑校验Would be auto-replicated结果表会被自动复制到从库这不是 error是提示表明 checksums 表会经过 binlog 复制最后一条值得认真说。有些同学看到Would be auto-replicated很慌以为有什么副作用其实它是工具在确认--replicate指定的结果表会被正常复制到从库。反过来如果从库上设置了replicate-ignore-tablepercona.checksums结果表到不了从库工具就会明确报错因为比对工作根本无法完成。另外必须提醒的是如果主库的binlog_format是STATEMENT工具在向 checksums 表写入结果时可能会因为执行环境的差异生成不确定的 checksum 值。官方做法是在执行时自动临时调整binlog_formatROW前提是连接用户有SUPER或者对应的权限。这也是为什么前面权限模型里要包含SUPER的原因之一。6. 校验完成之后同步修复与二次校验闭环6.1 pt-table-sync 的基本用法和边界找到差异只是第一步更关键的是修复。Percona Toolkit 里配套的工具有pt-table-sync它可以直接读取 checksums 表里记录的 chunk 边界再到主从两边对差异行做更精细的逐行比对最终生成修复 SQL把从库的数据改回和主库一致。基本用法是pt-table-sync \ --replicatepercona.checksums \ --databasesapp_db \ --tablest_payment \ h192.168.1.101,uchecksum_user,pxxx \ --execute执行之后它会打印修复的语句条数但不直接显示具体内容除非加--print。我的建议是第一次运行时一定要先加--print看生成的 SQL确认修复方式符合预期再真正--execute尤其是涉及从库数据被手工改过的场景。同时要明确它的边界它修复的是“从库向主库对齐”这个方向适合修复复制链路中从库数据被篡改或复制异常导致的数据不一致。如果主库本身的数据已经损坏或者你想用主库的某一时间点备份恢复某张表那就不该用它应该走备份恢复流程。另一个边界是它不适合直接修复主库数据也不建议在两边数据都存在“各自有效”的业务口径时盲跑因为修复方向是单向的。6.2 修复再校验DIFFS 从 1 回到 0修复完成后闭环的最后一步是再跑一遍同样的 pt-table-checksum。这时候看终端输出的DIFFS是否归零即可。归零不代表万事大吉至少说明在这个时间点主从双方被校验表的指纹一致。如果DIFFS依然大于 0要看是留下了哪些 chunk再结合 checksums 表里对应的this_crc和master_crc判断是不是有写入一直在发生导致两次校验之间又有新差异。高并发写入的大表上这也是常见现象——不是没修好而是校验窗口内又有新写入需要结合业务低峰期再确认一轮。6.3 我的实际建议什么时候别急着跑修复吃过大亏之后我总结了几个“先别急着跑 pt-table-sync”的场景。第一如果差异数据本身有业务含义比如财务流水、库存扣减不要贸然用主库覆盖从库。先把差异行导出来人工确认甚至让业务方确认哪一边才是对的。从库上被业务逻辑改过的数据有时候可能才是真实的主库反而因为 bug 丢失了更新这种场景靠工具无脑对齐是危险的。第二如果怀疑主库上存在未记录的手工 DML不先排查清楚就执行同步等于把不确定的差异扩散到所有从库。我的习惯是先暂停相关从库的复制再导出差异数据做审计确认主库数据可信后再恢复复制并执行修复。第三修复动作本身会产生压力。pt-table-sync在逐行比对和生成修复语句时也会查询全表大表上耗时和负载都不低新手容易忽略这一点在白天业务高峰直接跑结果是把线上库跑出慢查询告警。修复尽量安排在低峰期并且在执行过程中盯住主库的Threads_running负载指标。好了最后分享一个实际心得。我第一次拿 pt-table-checksum 的时候满脑子都是“我要让它把数据不一致全找出来”结果一跑就是一堆疑似差异整整排查了一个通宵后来发现大半是浮点字段和字符串尾部空格造成的误报。从那以后我拿到一个新环境第一件事是先找一张数据量适中、确信一致的业务表跑一遍把工具的行为基准摸清楚再对全库铺开。数据校验这门手艺不怕查不出问题就怕查出的问题没法判断真假。先把工具的行为吃透再让它替你冲锋陷阵才算真正把它用顺手了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑