由于各种原因,MySQL 主从架构可能会出现数据不一致的情况出现,为此需要对主备复制的数据进行校验。
在此,简单介绍 Percona-Toolkits 提供的数据校验方式。
简介
由于各种原因,MySQL 主从架构可能会出现数据不一致的情况出现,大致归结为如下几类:
- 主备方式,且在备库写数据;
- 执行了 non-deterministic query,如用户自定义函数、rand() 等;
- 回滚掺杂事务表和非事务表的事务;
- binlog 或者 relaylog 数据损坏。
在主库执行基于 statement 的 SQL 语句生成主库数据块的 checksum,把相同的 SQL 语句传递到从库,并在从库上计算相同数据块的 checksum,最后,比较主从库上相同数据块的 checksum 值,由此判断主从数据是否一致。
这种校验是分表进行的,在每个表内部又是分块进行的,而且提供了非常多的限流选项,因此对线上服务的冲击较小。
环境搭建
在 CentOS 中可以通过如下方式安装依赖 RPM 包。
# rpm -ivh perl-DBD-MySQL perl-TermReadKey
# rpm -ivh percona-toolkit-x.x.x.el7.x86_64.rpm
三个包分别为 MySQL 驱动、交互密码输入、主备数据校验工具。如果是双主配置,只需要在一个节点部署即可。如果要测试,可以通过 PTDEBUG=1 pt-table-checksum > /tmp/checksum.log 2>&1
测试。
可以通过 pt-table-checksum --help
查看帮助信息,或者 perldoc /usr/local/bin/pt-table-checksum
查看详细帮助。
新建校验结果保存表
分别在主备新建数据校验表。
CREATE TABLE `zabbix`.`checksums` (
`db` char(64) NOT NULL,
`tbl` char(64) NOT NULL,
`chunk` int(11) NOT NULL,
`chunk_time` float DEFAULT NULL,
`chunk_index` varchar(200) DEFAULT NULL,
`lower_boundary` text,
`upper_boundary` text,
`this_crc` char(40) NOT NULL,
`this_cnt` int(11) NOT NULL,
`master_crc` char(40) DEFAULT NULL,
`master_cnt` int(11) DEFAULT NULL,
`ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`db`,`tbl`,`chunk`),
KEY `ts_db_tbl` (`ts`,`db`,`tbl`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
主机用户创建
首先需要创建如下用户。
脚本部署主机
CREATE USER 'checker' IDENTIFIED BY 'password';
GRANT SUPER, PROCESS, SHOW DATABASES ON *.* TO 'checker'@'localhost';
GRANT DELETE, INSERT, UPDATE ON zabbix.checksums TO 'checker'@'localhost';
权限配置解析:
- SUPER: SET @@binlog_format:=“STATEMENT” 设置会话级的 binlog 格式;
- PROCESS: SHOW GRANTS FOR 查看当前用户的权限信息;
- SHOW DATABASES: 查看是否存在数据库;
- DELETE,INSERT,UPDATE: 用于清理、REPLACE INTO、UPDATE checksums 的历史数据;
另外一台主机
CREATE USER 'checker' IDENTIFIED BY 'password';
GRANT PROCESS, SUPER, REPLICATION CLIENT ON *.* TO 'checker'@'IP-MASTER';
GRANT SELECT ON zabbix.* TO 'checker'@'IP-MASTER';
权限配置解析:
- SUPER, REPLICATION CLIENT: SHOW SLAVE STATUS 查看是否已经同步完成;
- SELECT: EXPLAIN 评估备库数据量;
数据校验命令
$ pt-table-checksum --nocheck-replication-filters --check-slave-tables --chunk-size-limit=1.5 \
--check-interval=3 --max-lag=1s --nocheck-binlog-format \
--empty-replicate-table --progress=time,30 \
--nocreate-replicate-table --replicate=zabbix.checksums \
--replicate-check --check-plan --replicate-database=zabbix \
--float-precision=2 --chunk-time=0.5 \
--user=checker --password=password --port=3306 --host=localhost \
--databases=somedb --tables=table1,table2,table3
数据不一致检查
可以直接通过如下命令查看数据不一致的表,需要在备库执行。
SELECT db, tbl, SUM(this_cnt) AS total_rows, COUNT(*) AS chunks
FROM checksums
WHERE (
master_cnt <> this_cnt
OR master_crc <> this_crc
OR ISNULL(master_crc) <> ISNULL(this_crc))
GROUP BY db, tbl;
执行流程
该工具每次只检测一个表,对于表中的数据会分割成 chunk,一般是 1000 条左右数据(默认会动态调整);这个系统的调用流程可以阐述如下:
设置超时时间
为了减小对线上事务的影响,在主库建立链接后,会先设置会话变量 innodb_lock_wait_timeout=1
和 wait_timeout=10000
,也可以通过 --set-vars wait_timeout=500
参数覆盖默认值。
检查 sql_mode
通过 SELECT @@SQL_MODE
命令,确认该变量中不包含 ONLY_FULL_GROUP_BY
,因为对于一些正常的函数,该模式仍可能会出错,因此需要通过 SET SQL_MODE
命令重新设置。
设置 binlog_mode
查看当前 SELECT @@binlog_mode
模式,如果不是 STATEMENT,默认会报错直接退出;当然,可以通过 --[no]check-binlog-format
参数不检查该参数。
然后,仍会设置会话变量 /*!50108 SET @@binlog_format := 'STATEMENT'*/
。
设置隔离级别
设置会话的隔离级别为 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
,主要因为在将 binog_format 设置为 STATEMENT 后,且隔离级别为 RR,那么会报如下的错误 Transaction level 'READ-COMMITTED' in InnoDB is not safe for binlog mode 'STATEMENT'
,当然,这正是我们需要的,所以直接忽略该报错信息。
查找备库
通过 get_slaves()
函数查找备库,当然可以在启动时设置不同的备库查找方式,详见 find_slave_hosts()
函数。
拼接 SQL
接下来就是拼接主库执行的 SQL 语句,主要的格式如下:
REPLACE INTO checksum_table(db, tbl, chunk, ...)
SELECT ..., COUNT(*) AS cnt, COALESCE(LOWER(CONV(BIT_XOR(
CAST(CRC32(CONCAT_WS('#',...)) AS UNSIGNED)), 10, 16)), 0) AS crc
FROM tables FORCE INDEX(`PRIMARY`)
WHERE ((col >= ?)) AND ((col <= ?));
简单介绍下拼接上述 SQL 时的注意内容:
- 拼接各个列时,会根据不同的数据类型进行处理,例如以
TIMESTAMP
类型使用UNIX_TIMESTAMP()
函数;可能为NULL
时,使用CONCAT(ISNULL())
等等; - 每次执行查询前会通过
EXPLAIN
生成执行计划,主要关注评估数据量、索引使用情况,执行时会强制使用索引; - 通过
WHERE
进行分区,也就是 chunk 块,默认会使执行结果尽量接近--chunk-time
参数指定的时间,所以每次 chunk 的大小会动态调整。
等待备库执行完成
在执行完上述的 SQL 后,需要等待备库执行完成上述的 SQL 语句,也即是计算备库的 CRC 值。
更新主库校验值
校验完成后在主库更新校验值,SQL 如下,该语句会将主库校验值同步到备库:
UPDATE checksum_table SET chunk_time = ?, master_crc = ?, master_cnt = ?
WHERE db = ? AND tbl = ? AND chunk = ?;
等待同步完成
接下来就是等待备库执行完成上述的 SQL 语句,也即将主库的校验值同步到备库。在按照 chunk 执行完成之后,接下来只需要去备库的 checksums 表中找 master_cnt <> this_cnt or OR master_crc <> this_crc
的记录即可。
校验函数
简单介绍下上述校验命令中的函数。
COALESCE()
类似于 Oracle 中的 NVL()
函数,语义略有区别,会返回参数中第一个非 NULL
表达式的值,从左到右:
----- 如下函数返回 1
mysql> SELECT COALESCE(NULL, NULL, 1);
通常使用场景为,如果某个字段默认是 NULL,如果不想返回 NULL,而是 0 或其它值,可以使用该函数:
mysql> SELECT COALESCE(field_name, 0) AS value FROM table;
BIT_XOR()
用于 Binary String 的检测,会字段的值逐位进行检测,如果 bit 相同则返回 0,否则返回 1;比较时会按照行逐行进行检测,示例如下:
mysql> CREATE TABLE foobar (id INT);
mysql> INSERT INTO foobar VALUES(100), (100), (120);
mysql> SELECT BIT_XOR(id) FROM foobar;
首先是第一行与第二行比较,结果是 0;然后结果 0 与第三行比较,结果是 120。
CAST()/CONVERT()
CAST()
和 CONVERT()
用来进行类型转换,只是语法不同,可用类型包括了 BINARY、CHAR(N)、DATE、TIME、DATETIME、DECIMAL、SIGNED、UNSIGNED 等:
CAST(value AS type);
CONVERT(value, type);
CONV()
语法为 CONV(N,from_base,to_base)
,该函数用于数字的基数转换,最小基数值是2,最大值为36,如果任一参数为NULL,则该函数返回NULL。
----- 将数字5从基数16转为基数2
SELECT CONV(5,16,2);
配置选项
接下来,看下执行命令时的常用参数。
安全选项
--[no]check-replication-filters
(可选)
是否检查复制过滤规则,默认会检查,如果检查到有设置过滤规则会直接退出,主要防治检查的表在主库存在,而备库不存在导致 SQL 异常,主备复制出错;--[no]check-slave-tables
(设置)
默认检查,查看是否所有从库都有被检查的表和列,防止由于改程序导致主备复制异常;每次同时会通过 EXPLAIN 检查备库的执行计划,评估行数;--chunk-size-limit=2.0
(设置,尽量确保有主键或者唯一索引)
每次执行校验 SQL 前,会通过 EXPLAIN 检查本次执行SQL的数据量,对于没有唯一索引的表,通过 EXPLAIN 查看的行数差异会比较大,该参数就用于设置最大可以超过 chunk-size 几倍;如果超过 chunk-size*chunk-size-limit,那么就不会对本次的 chunk 做校验。
如果是 1.0,就以 chunk-size 为准;是 0.0 则表示不对 chunk 大小做校验。
限速选项
--check-interval=1
(可选)
需要睡眠多久,再检查一次主从延迟是否超过 max-lag,默认是 1 秒;--max-load=Threads_running=25
(建议设置)
设置最大负载,当超过这个值时就等待,防止由于工具导致过载,默认是Threads_running=25
等价于Threads_running:25
,可以逗号分隔设置多个值;如果不指定值则会在开始检查变量对应的值,并将上限设置为120%
;
每次执行完一个 chunk 之后,会通过 SHOW GLOBAL STATUS
查看当前服务器的状态,如果超过了上述值,就会等待;如果校验 SQL 导致锁等待,则会导致队列增加,进而 Threads_running
增加,通过该参数可以减小系统负载。
--check-slave-lag="h=slave1"
(可选)
是否只检查这个从库的复制延迟,如果有些场景通过pt-slave-delay
特意设置了延迟复制,那么可以通过该参数指定需要关注哪些复制延迟。--max-lag=1s
(设置)
检查备库的最大延迟,也就是Seconds_Behind_Master
参数,当超过这个就等待,而且如果备库复制线程异常,同样会等待;默认会检查所有库,也可以通过 check-slave-lag 指定需要检查哪些库。
过滤选项
可以指定只检查哪些数据库、表、列、存储引擎等,也可以使用 Perl 正则表达式;除此之外,也可以指定忽略哪些。
--ignore-databases
--ignore-databases-regex
--ignore-tables
--ignore-tables-regex
--ignore-columns
--ignore-engines
--databases
--databases-regex
--tables
--tables-regex
--columns
--engines
主库登陆选项
--host
--user
--password
--port
--ask-pass
--socket
其它参数:
--[no]empty-replicate-table
默认yes,会在每次执行校验前清理要校验表对应的记录,注意该操作不会TRUNCATE TABLE,如果需要,则要手动执行;--progress=time,30
输出当前的进展,有三种输出方式,包括了两个参数值,默认是 time,30,还可以设置为 percentage,time,iterations;--explain
打印要执行的命令,但是不执行 (dry run);--[no]create-replicate-table
检查 replicate 参数指定的表,如果不存在是否创建;--replicate=percona.checksums
将校验结果写入到那个表中,默认是 percona.checksums,可以通过该参数指定;--binary-index
(默认不开启测试)
默认通过 replicate 指定的表的 lower_boundary、upper_boundary 字段格式为 text,有些情况下可能会存在不兼容,通过该参数将定义上述字段为 blob 格式;--[no]replicate-check
默认 yes,也就是每次校验完一个表之后,会等待各个备库同步完成,从而进行数据校验,如果关闭则需要通过 replicate-check-only 参数再检查一遍;--replicate-check-only
不执行数据校验的 SQL,只查询保存的结果;--run-time=NUM
要执行多长时间,每个循环会检测是否达到该参数设置值,可以指定单位,默认是秒,可以指定参数 s=seconds,m=minutes,h=hours,d=days;如果使用--resume
那么之前的时间也会计算在内;--recurse=NUM
在查找复制库时指定递归层次,可以查看recurse_to_slaves()
函数;--recursion-method=processlist
递归查找时的方法,包括了processlist(SHOW PROCESSLIST)
、hosts(SHOW SLAVE HOSTS)
、none
,详细查看recurse_to_slaves()
函数;默认在启动时会检查所有的级联备库,可以通过该参数确认递归方法,通过 recurse 设置检查级联数;
FAQs
1. binlog在row模式下是否支持?
工具在执行时会将会话变量binlog格式设置为statement;如果级联开启了–log-slave-updates选项,而binlog_format=ROW,那么此时仍有可能在会导致下一层的级联校验失败。
2. 如何做到流量控制?
提供了多个参数进行流量控制,每次执行完一个chunk之后,会检查参数是否满足限流要求,如果超过限制则睡眠等待,直到满足要求。详见参数配置中的 “限速选项” 。