主从数据一致性校验与修复:chunk 对账、pt-table-checksum 与增量修复实战

“主从复制是自动的,所以主从数据一定一致”——这是运维里最大的错觉之一。binlog 丢失、复制中断、人为误操作、半同步降级、甚至程序 BUG,都会让从库与主库悄然分道扬镳。更隐蔽的是:复制通常不会报错,不一致像蛀虫一样潜伏,直到某个深夜的查询突然返回错误数据。数据一致性校验与修复,就是给这套 …

“主从复制是自动的,所以主从数据一定一致”——这是运维里最大的错觉之一。binlog 丢失、复制中断、人为误操作、半同步降级、甚至程序 BUG,都会让从库与主库悄然分道扬镳。更隐蔽的是:复制通常不会报错,不一致像蛀虫一样潜伏,直到某个深夜的查询突然返回错误数据。数据一致性校验与修复,就是给这套"自动复制"装上体检与治疗机制:周期性地对主从做逐块校验,发现差异后精确修复。本指南深度讲解一致性校验的原理(chunk 划分、checksum、校验时机)、工具实战(pt-table-checksum / pt-table-sync)、增量修复与对账平台,以及主从以外的通用一致性验证方法。

一、为什么主从会不一致

1.1 不一致的根源

主从不一致的常见根因:
  · binlog 格式问题(非 ROW,或 mixed 下函数非确定性)
  · 从库复制中断后手动补数错位
  · 从库被误写(人为/程序绕过只读)
  · 半同步降级为异步期间的主从窗口
  · 主库崩溃未完全 flush 导致的差异
  · 存储引擎差异 / 表结构不一致
  · 大事务 / 长延迟期间的并发写覆盖

关键事实:
  · MySQL 复制"尽力而为",不保证强一致
  · 不校验 = 不一致可能在未知时间点悄悄产生
  → 一致性校验是"复制健康"的体检,必须周期性做

ℹ️ 核心洞察:复制协议的语义是"主库执行什么,从库执行什么"(statement)或"主库改了哪些行,从库改哪些行"(row),但它不校验结果是否一致。校验必须由外部工具按"对账"思路完成。

1.2 校验的两种思路

① 抽样对比:随机抽行/按 id 抽样,速度快,有盲区
② 全量对账:逐块比对(chunk + checksum),彻底,慢

生产实践:全量对账为主(周期性),抽样为辅(日常快速巡检)

二、chunk 校验原理

2.1 为什么不能直接 COUNT

-- 错误示范:主从各 COUNT
SELECT COUNT(*) FROM orders;    -- 主库
SELECT COUNT(*) FROM orders;    -- 从库
-- 行数相等 ≠ 数据一致(可能内容不同,COUNT 不看内容)
-- 且大表 COUNT 全表扫描,代价极高

2.2 chunk 划分与 checksum

pt-table-checksum 的核心算法:
  1. 按主键把表切成多个 chunk(每 chunk 约 1000-2000 行)
  2. 对每个 chunk 计算 checksum(行内容哈希)
  3. 主从都算同一 chunk 的 checksum 并对比
  4. 差异 chunk → 精确到行定位

chunk 划分好处:
  · 内存可控(每次只处理一小块)
  · 锁影响小(只锁 chunk 范围)
  · 可增量/可断点续跑
  · 主从并行对比,吞吐高
-- pt-table-checksum 本质执行的 SQL(示意)
-- 主库计算 chunk 校验:
SELECT CRC32(CONCAT_WS('#', id, name, amount, ...))
FROM orders
WHERE id BETWEEN ? AND ?;
-- 从库同样计算并对比

2.3 checksum 的选择

checksum 函数:
  · CRC32:快,碰撞概率低(32bit)
  · MD5 / SHA:更稳但更慢
  · pt-table-checksum 默认组合算法(F CRC32)

注意:
  · 浮动/浮点列可能因平台差异产生不一致(用 round)
  · 时间列注意时区差异
  · 大字段(BLOB/TEXT)用 CRC32(CONCAT(bit_count...)) 处理

三、pt-table-checksum 实战

3.1 基本用法

# 全库校验(dsn 指向主库,工具自动感知从库)
pt-table-checksum \
  --host=mysql-master \
  --user=check \
  --password=*** \
  --databases=orders_db \
  --tables=orders \
  --replicate=percona.checksums \
  --no-check-binlog-format \
  --chunk-size=1000 \
  --empty-replicate-table

# 输出解读:每行一个表,TS/DIFFS/ROWS/CHUNKS
# DIFFS>0 表示有差异 chunk

3.2 校验结果表

-- 校验结果存在 percona.checksums 表
SELECT * FROM percona.checksums
WHERE master_cnt <> this_cnt OR master_crc <> this_crc
LIMIT 10;
-- 每个 chunk 一行:master_crc(主库校验值) vs this_crc(当前库)

3.3 常用参数与策略

# 控制负载与频率
--max-lag=1                    # 从库延迟超 1s 自动等待
--check-interval=1             # 检查从库延迟间隔
--chunk-size=1000              # 每 chunk 行数(越大越快但锁更久)
--chunk-size-limit=4           # 动态 chunk 上限
--max-load Threads_running=200 # 主库负载过高自动节流
--lock-wait-timeout=30
# 表过滤
--databases / --tables / --ignore-tables
# 跳过不确定列
--no-check-binlog-format / --nocheck-replication-filters

# 典型调度(低峰 + 大表分表跑)
0 3 * * 0  pt-table-checksum --databases=core_db --max-lag=2 > /var/log/consistency/checksum.log

3.4 权限要求

-- 校验账号所需权限
GRANT SELECT, PROCESS, SUPER, REPLICATION SLAVE ON *.* TO 'check'@'%';
-- SUPER/PROCESS 用于 SHOW SLAVE STATUS 检测延迟与锁等待

四、发现差异后的定位

4.1 定位差异行

-- 用 checksums 表定位差异 chunk 的 id 范围
SELECT db, tbl, chunk, lower_boundary, upper_boundary,
       master_cnt, this_cnt, master_crc, this_crc
FROM percona.checksums
WHERE db='orders_db' AND tbl='orders' AND master_crc <> this_crc;

-- 然后精确对账差异范围:
-- 主库
SELECT * FROM orders WHERE id BETWEEN ? AND ? ORDER BY id;
-- 从库
SELECT * FROM orders WHERE id BETWEEN ? AND ? ORDER BY id;
-- 逐行对比

4.2 差异类型判断

现象类型处理
主库有行,从库没有缺失行主→从 插入
从库有行,主库没有多余行从库删除
两库都有但内容不同值差异以主库为准覆盖
主库 index 差异结构不一致重建索引

五、pt-table-sync 修复实战

5.1 安全执行

# 先 dry-run 看会改什么(强烈建议)
pt-table-sync \
  --execute \
  --dry-run \
  --replicate=percona.checksums \
  --tables orders_db.orders \
  D=orders_db,h=mysql-master,u=check,p=***

# 确认无误后真正执行(仅在差异 chunk 上做最小 DML)
pt-table-sync \
  --execute \
  --replicate=percona.checksums \
  --tables orders_db.orders \
  D=orders_db,h=mysql-master,u=check,p=***

5.2 修复的两种方式

方式一:基于 checksums 表定位(推荐)
  只修有差异的 chunk → 最小化影响
  --replicate=percona.checksums 让工具"按差异点修复"

方式二:全表同步(慎用)
  不指定 replicate,逐表逐行对齐
  → 适合差异少、可短锁的表

原则:
  · 以主库为准(数据一致性锚点 = 主库)
  · 修复动作 = 把从库改到与主库一致
  · 先备份差异行再做修复(可回滚)

5.3 修复期间的注意事项

# 修复工具会持有锁(DML + 行锁),选择低峰执行
# 修复前:
#   1) 记录差异清单
#   2) 评估差异行数(大差异 = 先复盘根因再修)
#   3) 若差异来自"复制断流",先恢复复制
# 修复后:
#   1) 重新跑 checksum 验证
#   2) 检查复制延迟/错误
#   3) 分析根因并加防护(只读、延迟监控)

六、复制恢复与前置条件

6.1 修复前先恢复复制健康

-- 检查复制状态
SHOW REPLICA STATUS\G;
-- 常见问题:SQL 线程停止(错误码 1062 主键冲突等)
-- 恢复方式:
--   小额差异:STOP REPLICA; SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1; START REPLICA;
--   严重差异:需 re-position / 重建从库

⚠️ 硬规则:如果从库复制已中断,先恢复复制再对账。在断流状态下修复 = 修复结果会被随后的追赶写入再次覆盖,徒劳且制造新差异。

6.2 重建从库兜底

# 差异过大 / 无法增量修复时:重建从库
# 用备份(Xtrabackup)+ binlog 增量搭从库
xtrabackup --backup --target-dir=/backup
# 传到新从库,恢复后 change master 到主库

七、自动化对账平台

7.1 架构

对账平台组成:
  · 调度器:周期性触发校验(cron / 工作流)
  · 校验执行:pt-table-checksum 或自研 chunk 校验
  · 结果存储:差异明细入库(audit 表)
  · 告警:差异数 > 0 → 通知 DBA
  · 修复流程:人工确认 → pt-table-sync / 自研修复
  · 报表:校验覆盖率、差异趋势

触发策略:
  · 全量校验:每周/每月(低峰)
  · 增量校验:每日对变化大的表
  · 事件触发:复制异常恢复后立即校验

7.2 校验覆盖矩阵

校验对象分层:
  · 核心表(订单/账户/余额):高频率 + 全量
  · 普通表:中频率 + 抽样
  · 历史/归档表:低频 + 抽样
  · 结构一致性:定期对比 SHOW CREATE TABLE

7.3 指标化监控

# 对账平台指标
consistency_check_total                     # 校验次数
consistency_diff_chunks{db,tbl}             # 差异 chunk 数
consistency_diff_rows                        # 差异行数
consistency_repair_duration                  # 修复耗时
consistency_repair_failed                    # 修复失败
# 告警:任何 db.tbl 的 diff_rows > 0

八、超越 MySQL 的一致性校验

8.1 PostgreSQL / 其他引擎

· PostgreSQL 逻辑复制/流复制:
  可用 pg_stat_replication 观察 WAL 接收进度,
  数据一致性用自定义 checksum 查询或 pg_dump 对比抽样
· Redis 多副本:redis-check / keyspace 对比
· 消息队列(Kafka):offset 对齐是"消费进度"而非数据一致,
  需独立对账(去重 + 幂等消费)

8.2 通用数据对账设计

# verify_service.py — 通用 chunk 对账框架
def verify_table(source, target, table, chunk_size=1000):
    """按主键范围逐 chunk 对比两张表。"""
    min_id, max_id = source.range(table)
    diffs = []
    for lo in range(min_id, max_id + 1, chunk_size):
        hi = min(lo + chunk_size - 1, max_id)
        s = source.rows(table, lo, hi)     # 主库 chunk
        t = target.rows(table, lo, hi)     # 从库 chunk
        if s != t:
            diffs.append(find_diff_rows(s, t, table))
    return diffs

# 幂等修复(以 source 为准,可重放)
def repair(s, t, table, key, field):
    for row in s:
        if t.get(key=row[key]) is None or t.get(key=row[key])[field] != row[field]:
            t.upsert(table, row)            # 幂等:重复执行结果一致

九、实践清单与避坑

9.1 落地 Checklist

□ 校验账号与权限准备
□ 低峰窗口排期(全量 vs 增量)
□ pt-table-checksum 参数调优(max-lag/load 节流)
□ 差异定位流程(checksums 表 → 行级对比)
□ 修复前备份差异行
□ 修复后重校验
□ 对账结果入库 + 告警
□ 根因复盘(为何不一致)+ 防护加固

9.2 常见坑

坑现象对策
复制断流时校验差异持续增长先恢复复制
大表校验拖垮主库负载飙升chunk + max-load 节流
浮点/时区差异误报差异规范列类型/时区
修复中写冲突锁等待低峰 + 幂等重试
半同步降级未感知差异窗口监控 semi-sync 状态
校验一次就算完新差异积累周期化 + 覆盖矩阵

9.3 一致性黄金原则

· 主库是真相源,修复永远以主库为准
· 先恢复复制健康,再谈对账
· 校验要周期化,不是一次性
· 修复要可回滚、幂等
· 每个差异都要追根因(工具只治病,不治病根)

总结:一致性校验决策表

环节关键动作
时机周期全量 + 每日增量 + 事件触发
方法chunk 划分 + checksum 对账
定位checksums 表 → 行级 diff
修复pt-table-sync / 幂等修复,主库为准
兜底差异过大重建从库
平台调度 + 存储 + 告警 + 报表
根因复制健康 + 防护加固

主从不一致不是"复制系统坏了",而是复制的天然盲区被时间放大。一致性校验的意义在于把这个盲区变成可观测、可度量的指标——每周跑一遍对账,差异从"未知"变"已知",从"潜伏"变"已修复"。落地守住四件事:先恢复复制再对账、chunk 加节流防拖垮主库、修复以主库为准且可回滚、每次差异追根因。把"校验→定位→修复→复盘"做成周期化闭环,你的数据层就从"相信它一致"升级为"验证它一致"。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. 数据库安全加固与审计实战:权限最小化、加密、脱敏与合规
  2. 数据库容量规划与资源治理:从评估、监控到扩展路径
  3. 数据库字符集、排序规则与乱码实战:utf8mb4、Collation 选择与排查