基于 MySQL 8.0.35 GTID 级联主从环境(141主 → 142从 → 143从),全场景实测验证
- 主库:192.168.195.141
- 从库:192.168.195.142(141的从库)
- 级联从库:192.168.195.143(142的从库)
- root密码:Root@123456
- 复制用户:repl / 123456
目录
- [1. Percona Toolkit 概述](#1. Percona Toolkit 概述)
- [2. 安装部署](#2. 安装部署)
- [3. DSN 数据源说明](#3. DSN 数据源说明)
- [4. 必须掌握的工具(实战+原理)](#4. 必须掌握的工具(实战+原理))
- [4.1 pt-archiver --- 数据归档](#4.1 pt-archiver — 数据归档)
- [4.2 pt-query-digest --- 慢查询分析](#4.2 pt-query-digest — 慢查询分析)
- [4.3 pt-table-checksum --- 主从数据一致性校验](#4.3 pt-table-checksum — 主从数据一致性校验)
- [4.4 pt-table-sync --- 修复从库不一致数据](#4.4 pt-table-sync — 修复从库不一致数据)
- [4.5 pt-online-schema-change --- DDL变更工具](#4.5 pt-online-schema-change — DDL变更工具)
- [5. 建议掌握的工具(实战)](#5. 建议掌握的工具(实战))
- [5.1 pt-config-diff --- 参数对比](#5.1 pt-config-diff — 参数对比)
- [5.2 pt-find --- 数据库对象查找](#5.2 pt-find — 数据库对象查找)
- [5.3 pt-kill --- KILL连接](#5.3 pt-kill — KILL连接)
- [5.4 pt-mysql-summary --- MySQL实例信息采集](#5.4 pt-mysql-summary — MySQL实例信息采集)
- [5.5 pt-pmp --- 进程堆栈汇总](#5.5 pt-pmp — 进程堆栈汇总)
- [5.6 pt-show-grants --- 账号授权信息](#5.6 pt-show-grants — 账号授权信息)
- [5.7 pt-slave-find --- 主从拓扑图](#5.7 pt-slave-find — 主从拓扑图)
- [5.8 pt-summary --- 服务器信息采集](#5.8 pt-summary — 服务器信息采集)
- [5.9 pt-slave-restart --- 复制错误自动跳过](#5.9 pt-slave-restart — 复制错误自动跳过)
- [5.10 pt-stalk --- 问题现场信息采集](#5.10 pt-stalk — 问题现场信息采集)
- [5.11 pt-upgrade --- 版本兼容性检测](#5.11 pt-upgrade — 版本兼容性检测)
- [6. 常见故障模拟及解决](#6. 常见故障模拟及解决)
- [7. 知识点补充与生产场景](#7. 知识点补充与生产场景)
- [8. 工具速查表](#8. 工具速查表)
1. Percona Toolkit 概述
Percona Toolkit 是 Percona 公司提供的一个 MySQL 工具包,包含大量实用的 MySQL 管理工具。熟练使用 Percona Toolkit 是 MySQL DBA 必备技能之一。
工具分类:
| 分类 | 工具 | 说明 |
|---|---|---|
| 必须掌握 | pt-archiver | 数据归档 |
| 必须掌握 | pt-query-digest | 分析慢查询 |
| 必须掌握 | pt-table-checksum | 检查主从数据一致性 |
| 必须掌握 | pt-table-sync | 修复从库不一致数据 |
| 必须掌握 | pt-online-schema-change | DDL变更工具 |
| 建议掌握 | pt-config-diff | 参数对比 |
| 建议掌握 | pt-find | 数据库对象查找 |
| 建议掌握 | pt-kill | KILL连接 |
| 建议掌握 | pt-mysql-summary | 采集MySQL实例基本信息 |
| 建议掌握 | pt-pmp | 汇总进程堆栈信息 |
| 建议掌握 | pt-show-grants | 打印账号授权信息 |
| 建议掌握 | pt-slave-find | 打印主从拓扑图 |
| 建议掌握 | pt-summary | 采集服务器基本信息 |
| 建议掌握 | pt-slave-restart | 复制错误自动跳过重启 |
| 建议掌握 | pt-stalk | 问题现场实时采集 |
| 建议掌握 | pt-upgrade | 版本升级兼容性检测 |
官方文档: https://docs.percona.com/percona-toolkit/index.html
2. 安装部署
2.1 环境前提
bash
# 确认 Perl 环境(MySQL 8.0 二进制包环境)
which perl
# /usr/bin/perl
rpm -q perl-DBD-MySQL perl-DBI perl-ExtUtils-MakeMaker
# perl-DBD-MySQL-4.046-6.ky10.x86_64
# perl-DBI-1.643-5.ky10.x86_64
2.2 RPM包安装(推荐,生产环境首选)
bash
# 执行节点:141(主库)
cd /usr/local/src
# 优先国内镜像(清华),失败则用官方源
# 官方下载地址(实测可用)
wget https://downloads.percona.com/downloads/percona-toolkit/3.5.7/binary/redhat/7/x86_64/percona-toolkit-3.5.7-1.el7.x86_64.rpm -O pt.rpm
# 安装依赖
yum install -y perl-ExtUtils-MakeMaker perl-DBD-MySQL perl-DBI
# 本地安装
yum localinstall -y pt.rpm
验证安装:
bash
which pt-query-digest pt-archiver pt-table-checksum
# /usr/bin/pt-query-digest
# /usr/bin/pt-archiver
# /usr/bin/pt-table-checksum
pt-query-digest --version
# pt-query-digest 3.5.7
注意: MySQL 的
mysql、mysqladmin、mysqldump命令需要加入 PATH,否则 pt-stalk 等工具无法调用:
bashln -sf /usr/local/mysql/bin/mysql /usr/local/bin/mysql ln -sf /usr/local/mysql/bin/mysqladmin /usr/local/bin/mysqladmin ln -sf /usr/local/mysql/bin/mysqldump /usr/local/bin/mysqldump
2.3 源码安装(备选)
bash
cd /usr/local/
wget https://downloads.percona.com/downloads/percona-toolkit/3.5.7/source/tarball/percona-toolkit-3.5.7.tar.gz -O percona-toolkit-3.5.7.tar.gz
tar xvf percona-toolkit-3.5.7.tar.gz
cd percona-toolkit-3.5.7/
yum install perl-ExtUtils-MakeMaker perl-DBD-MySQL
perl Makefile.PL
make
make install
踩坑提示: 源码安装过程中需要下载 go 依赖包,如提示
dial tcp 142.251.43.17:443: i/o timeout,可设置国内 go 代理:
bashgo env -w GOPROXY=https://goproxy.cn,direct
2.4 创建连接配置(生产推荐)
bash
# 创建 /root/.my.cnf,避免命令行暴露密码
cat > /root/.my.cnf << 'EOF'
[client]
user=root
password=Root@123456
host=127.0.0.1
port=3306
EOF
chmod 600 /root/.my.cnf
3. DSN 数据源说明
DSN(Data Source Name)在 Percona Toolkit 中比较常见,可理解为目标实例相关配置的缩写。
| 缩写 | 含义 |
|---|---|
| A | 默认的字符集 |
| D | 库名 |
| F | 只从给定文件中读取配置信息 |
| P | 端口 |
| S | 用于连接的socket文件 |
| h | 主机名 |
| p | 密码 |
| t | 表名 |
| u | 用户名 |
示例:
h=192.168.195.141,P=3306,u=root,p=Root@123456,D=sbtest,t=sbtest1
解读:连接 192.168.195.141 的 3306 端口,用户 root,密码 Root@123456,操作 sbtest 库的 sbtest1 表。
4. 必须掌握的工具(实战+原理)
4.1 pt-archiver --- 数据归档
功能: 将数据从源表归档到目标表/文件,或仅删除数据。
基本语法:
bash
pt-archiver [OPTIONS] --source DSN --dest DSN --where WHERE
4.1.1 实现原理
- 工具首先从源库查询一条记录,然后插入到目标库中,目标库插入成功,才会从源库中删除该记录。这样就不会出现数据被删除但还没有归档的情况。
- 操作的执行顺序如下:
- (1)源库查询记录
- (2)目标库插入记录
- (3)源库删除记录
- (4)目标库 COMMIT
- (5)源库 COMMIT
- 每次删除都是基于主键。
--where参数中的条件会传递到 SELECT 操作中。
4.1.2 测试数据准备
sql
-- 在主库(141)执行,自动复制到142/143
CREATE DATABASE sbtest;
USE sbtest;
CREATE TABLE sbtest1 (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
k INT NOT NULL DEFAULT 0,
c CHAR(120) NOT NULL DEFAULT '',
pad CHAR(60) NOT NULL DEFAULT '',
PRIMARY KEY (id), KEY k_1 (k)
) ENGINE=InnoDB;
CREATE TABLE sbtest2 LIKE sbtest1;
CREATE TABLE sbtest3 LIKE sbtest1;
-- 用递归CTE插入10万行测试数据
SET SESSION cte_max_recursion_depth=110000;
INSERT INTO sbtest1(id,k,c,pad)
WITH RECURSIVE seq AS (SELECT 1 AS n UNION ALL SELECT n+1 FROM seq WHERE n < 100000)
SELECT n, FLOOR(1+RAND(n)*100000),
CONCAT('data-',n,'-aaaaaaaaaaaaaaaaaaaaaaaaaaaaa'),
CONCAT('pad-',n,'-bbbbbbbbbbbbbbbbbbbbb') FROM seq;
INSERT INTO sbtest2 SELECT * FROM sbtest1;
INSERT INTO sbtest3 SELECT * FROM sbtest1 WHERE id<=50000;
-- 验证
SELECT COUNT(*) FROM sbtest1; -- 100000
SELECT COUNT(*) FROM sbtest2; -- 100000
SELECT COUNT(*) FROM sbtest3; -- 50000
注意: 归档目标端(143)需要创建归档库,且如果是
--bulk-insert模式(LOAD DATA INFILE),目标库需开启local_infile:
sql-- 在143执行 SET GLOBAL super_read_only=OFF; SET GLOBAL local_infile=ON; CREATE DATABASE arch; CREATE TABLE arch.sbtest1 LIKE sbtest.sbtest1;
4.1.3 场景一:基础归档(逐行模式)
bash
# 执行节点:141
pt-archiver \
--source h=192.168.195.141,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest1 \
--dest h=192.168.195.143,P=3306,u=root,p='Root@123456',D=arch,t=sbtest1 \
--where "id>1000 AND id<=2000" --limit 200 --commit-each --progress 500 --statistics
参数说明:
--source:源端相关信息--dest:目标端相关信息--where:归档条件,"1=1"代表归档全表--limit:每批归档的记录数--commit-each:对于每一批记录,只会 COMMIT 一次--progress:显示进度信息,单位行数--statistics:输出统计信息
输出结果:
Started at 2026-08-16T18:44:17, ended at 2026-08-16T18:44:17
Source: D=sbtest,P=3306,h=192.168.195.141,p=...,t=sbtest1,u=root
Dest: D=arch,P=3306,h=192.168.195.143,p=...,t=sbtest1,u=root
SELECT 1000
INSERT 1000
DELETE 1000
Action Count Time Pct
inserting 1000 0.4171 71.46
deleting 1000 0.1195 20.47
commit 12 0.0069 1.19
select 6 0.0020 0.34
other 0 0.0382 6.54
验证:
bash
# 执行节点:141
/usr/local/mysql/bin/mysql -uroot -p'Root@123456' -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 99000 ← 源表减少了1000行
/usr/local/mysql/bin/mysql -uroot -p'Root@123456' -h192.168.195.143 -N -e "SELECT COUNT(*) FROM arch.sbtest1;"
# 1000 ← 归档端增加了1000行
# 从库也同步删除(复制正常)
/usr/local/mysql/bin/mysql -uroot -p'Root@123456' -h192.168.195.142 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 99000
4.1.4 场景二:批量归档(--bulk-delete --bulk-insert)
bash
# 执行节点:141
pt-archiver \
--source h=192.168.195.141,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest1 \
--dest h=192.168.195.143,P=3306,u=root,p='Root@123456',D=arch,t=sbtest1 \
--where "id>2000 AND id<=6000" --bulk-delete --bulk-insert --limit 1000 --commit-each --progress 1000 --statistics
参数说明:
--bulk-delete:批量删除(用一条 DELETE ... WHERE id IN (...) 代替逐行删除)--bulk-insert:归档数据以 LOAD DATA INFILE 的方式导入到归档库中(比逐行INSERT快很多)
输出结果:
SELECT 4000
INSERT 4000
DELETE 4000
验证:
bash
# 源表减少4000行
mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 92000
# 归档端增加4000行
mysql -h192.168.195.143 -N -e "SELECT COUNT(*) FROM arch.sbtest1;"
# 5000
注意:
--bulk-insert使用LOAD DATA LOCAL INFILE,目标库的local_infile参数需设置为 ON:
sqlSET GLOBAL local_infile=ON; SELECT @@local_infile; -- 1
4.1.5 场景三:只删除不归档(--purge)
bash
# 执行节点:141
pt-archiver \
--source h=192.168.195.141,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest1 \
--where "id>6000 AND id<=8000" --bulk-delete --limit 1000 --commit-each \
--purge --primary-key-only --statistics
参数说明:
--purge:只删除,不归档(不指定 --dest)--primary-key-only:如果只是执行删除操作,建议加上,这样在执行 SELECT 操作时只会查询主键,不会查询所有列,提升性能
输出结果:
SELECT 2000
INSERT 0
DELETE 2000
Action Count Time Pct
bulk_deleting 2 0.0074 44.19
commit 3 0.0028 16.71
select 3 0.0013 7.51
other 0 0.0053 31.59
验证:
bash
mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 97000 ← 又减少了2000行
4.1.6 场景四:归档到文件(--file)
bash
# 执行节点:141
mkdir -p /data/archive
pt-archiver \
--source h=192.168.195.141,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest1 \
--where "id>8000 AND id<=9000" --bulk-delete --limit 500 --commit-each \
--file '/data/archive/%Y-%m-%d-%D.%t' --statistics
参数说明:
--file:归档到文件,格式支持%Y(年)、%m(月)、%d(日)、%D(库名)、%t(表名)等占位符
输出结果:
Action Count Time Pct
bulk_deleting 2 0.0070 40.58
commit 3 0.0017 9.78
select 3 0.0009 5.50
print_file 1000 -0.0002 -1.43
other 0 0.0079 45.57
验证:
bash
# 文件行数
wc -l < /data/archive/*.sbtest1
# 1000
# 文件内容(TSV格式,制表符分隔)
head -2 /data/archive/*.sbtest1
# 8001 87028 data-8001-aaaaaaaaaaaaaaaaaaaaaaaaaaaaa pad-8001-bbbbbbbbbbbbbbbbbbbbb
# 8002 12047 data-8002-aaaaaaaaaaaaaaaaaaaaaaaaaaaaa pad-8002-bbbbbbbbbbbbbbbbbbbbb
# 源表减少1000行
mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 96000
4.1.7 场景五:检查从库延迟(--check-slave-lag)
bash
# 执行节点:141
pt-archiver \
--source h=192.168.195.141,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest1 \
--where "id>9000 AND id<=11000" --bulk-delete --limit 500 --commit-each \
--primary-key-only --purge \
--check-slave-lag h=192.168.195.142,P=3306,u=root,p='Root@123456' --statistics
参数说明:
--check-slave-lag:归档时检查指定从库的延迟情况。如果有多个从库需要检查,需将--check-slave-lag指定多次,每次对应一个从库。如果从库延迟超过阈值(默认1s),则暂停归档操作直到复制恢复。
输出结果:
SELECT 2000
DELETE 2000
4.1.8 场景六:--dry-run 预检
bash
# 执行节点:141
pt-archiver \
--source h=192.168.195.141,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest1 \
--dest h=192.168.195.143,P=3306,u=root,p='Root@123456',D=arch,t=sbtest1 \
--where "id>11000 AND id<=11200" --dry-run --limit 100
参数说明:
--dry-run:只打印待执行的SQL,不实际执行。常用于实际操作之前,校验待执行 SQL 是否符合预期。
输出结果:
sql
SELECT /*!40001 SQL_NO_CACHE */ `id`,`k`,`c`,`pad` FROM `sbtest`.`sbtest1` FORCE INDEX(`PRIMARY`) WHERE (id>11000 AND id<=11200) AND (`id` < '100000') ORDER BY `id` LIMIT 100
SELECT /*!40001 SQL_NO_CACHE */ `id`,`k`,`c`,`pad` FROM `sbtest`.`sbtest1` FORCE INDEX(`PRIMARY`) WHERE (id>11000 AND id<=11200) AND (`id` < '100000') AND ((`id` >= ?)) ORDER BY `id` LIMIT 100
DELETE FROM `sbtest`.`sbtest1` WHERE (`id` = ?)
INSERT INTO `arch`.`sbtest1`(`id`,`k`,`c`,`pad`) VALUES (?,?,?,?)
4.1.9 场景七:--no-delete 只复制不删源
bash
# 执行节点:141
pt-archiver \
--source h=192.168.195.141,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest1 \
--dest h=192.168.195.143,P=3306,u=root,p='Root@123456',D=arch,t=sbtest1 \
--where "id>11000 AND id<=11200" --no-delete --limit 100 --commit-each --statistics
参数说明:
--no-delete:不删除源库的数据(只归档,不删除)
输出结果:
SELECT 200
INSERT 200
DELETE 0 ← DELETE为0,说明源库数据未删除
验证:
bash
mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 90000 ← 源表行数不变
mysql -h192.168.195.143 -N -e "SELECT COUNT(*) FROM arch.sbtest1;"
# 5200 ← 归档端增加200行
4.1.10 场景八:--replace 处理主键冲突
bash
# 执行节点:141
# 先在归档端制造脏数据
mysql -h192.168.195.143 -e "UPDATE arch.sbtest1 SET c='DIRTY' WHERE id=11001;"
# 不带 --replace(预期主键冲突报错)
pt-archiver --source h=192.168.195.141,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest1 \
--dest h=192.168.195.143,P=3306,u=root,p='Root@123456',D=arch,t=sbtest1 \
--where "id=11001" --no-delete --limit 1 2>&1 | grep "Duplicate"
# Duplicate entry '11001' for key 'sbtest1.PRIMARY' [for Statement "INSERT INTO `arch`.`sbtest1`..."]
# 带 --replace(预期覆盖脏值)
pt-archiver --source h=192.168.195.141,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest1 \
--dest h=192.168.195.143,P=3306,u=root,p='Root@123456',D=arch,t=sbtest1 \
--where "id=11001" --no-delete --replace --limit 1 --statistics
# SELECT 1
# INSERT 2 ← REPLACE操作
验证:
bash
mysql -h192.168.195.143 -N -e "SELECT c FROM arch.sbtest1 WHERE id=11001;"
# data-11001-aaaaaaaaaaaaaa... ← 脏值已恢复
4.1.11 场景九:--columns 指定列归档
bash
# 执行节点:141
# 归档端表结构必须包含源表的所有列(含主键),但只归档指定列
mysql -h192.168.195.143 -e "SET SESSION sql_log_bin=0; DROP TABLE IF EXISTS arch.sbtest2; CREATE TABLE arch.sbtest2 (id INT, k INT, c CHAR(120), pad CHAR(60));"
pt-archiver --source h=192.168.195.141,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest2 \
--dest h=192.168.195.143,P=3306,u=root,p='Root@123456',D=arch,t=sbtest2 \
--where "id<=100" --no-delete --columns k,c,pad --limit 50 --commit-each --statistics
# SELECT 100
# INSERT 100
验证:
bash
mysql -h192.168.195.143 -N -e "SELECT * FROM arch.sbtest2 LIMIT 2;"
# NULL 40541 data-1 pad-1 ← id列为NULL(未归档),只归档了k,c,pad
# NULL 65559 data-2 pad-2
注意: 使用
--columns时,如果源表有自增列且源/目标表的自增列存在交集,可不归档自增列。但目标表必须包含源表的所有列名(包括主键列),否则报错The following columns exist in --source but not --dest。
4.1.12 场景十:--safe-auto-increment 自增主键保护
bash
# 执行节点:141
# 默认 --safe-auto-increment:不删除自增主键最大的那一行
pt-archiver --source h=192.168.195.141,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest3 \
--dest h=192.168.195.143,P=3306,u=root,p='Root@123456',D=arch,t=sbtest3 \
--where "1=1" --bulk-delete --limit 10000 --commit-each --statistics
# SELECT 49999
# INSERT 49999
# DELETE 49999 ← 只删除了49999行,最大id那行保留
mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest3;"
# 1 ← 保留了最大id那行
# --no-safe-auto-increment:删除全部(含最大id行)
pt-archiver --source h=192.168.195.141,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest3 \
--dest h=192.168.195.143,P=3306,u=root,p='Root@123456',D=arch,t=sbtest3 \
--where "1=1" --bulk-delete --limit 10000 --commit-each --no-safe-auto-increment --statistics
# SELECT 1
# INSERT 1
# DELETE 1
mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest3;"
# 0 ← 全部删除
4.1.13 --ignore 跳过冲突
bash
# 执行节点:141
# 在目标库执行的INSERT语句中加上IGNORE,遇到主键冲突时跳过
pt-archiver --source h=192.168.195.141,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest2 \
--dest h=192.168.195.143,P=3306,u=root,p='Root@123456',D=arch,t=sbtest2 \
--where "id<=100" --no-delete --columns k,c,pad --limit 50 --commit-each --ignore --statistics
# SELECT 100
# INSERT 0 ← 全部被IGNORE跳过(因为数据已存在)
4.1.14 参数汇总表
| 参数 | 说明 |
|---|---|
--source |
源端DSN |
--dest |
目标端DSN(不指定则需--purge或--file) |
--where |
归档条件 |
--limit |
每批归档的记录数 |
--commit-each |
每批COMMIT一次 |
--bulk-delete |
批量删除 |
--bulk-insert |
以LOAD DATA INFILE方式导入 |
--purge |
只删除不归档 |
--file |
归档到文件 |
--no-delete |
不删除源库数据 |
--replace |
目标库执行REPLACE而非INSERT |
--ignore |
目标库INSERT加IGNORE |
--columns |
归档指定列 |
--primary-key-only |
只查询主键列(删除场景优化) |
--dry-run |
只打印SQL不执行 |
--progress |
显示进度 |
--statistics |
输出统计信息 |
--check-slave-lag |
检查从库延迟 |
--[no]safe-auto-increment |
是否保护自增主键最大行 |
4.1.15 推荐的归档方式
生产环境推荐的归档流程:
- 先归档,但不删除源库的数据 (
--no-delete) - 比对源库和归档库的数据是否一致 (用 pt-table-sync
--print) - 如果比对结果一致,再删除源库的归档数据 (
--purge)
bash
# 步骤1:归档但不删除
pt-archiver --source ... --dest ... --where "create_time < '2024-01-01'" \
--no-delete --bulk-insert --limit 1000 --commit-each
# 步骤2:比对一致性
pt-table-sync --print --sync-to-master ...
# 步骤3:确认一致后删除源库
pt-archiver --source ... --where "create_time < '2024-01-01'" \
--purge --bulk-delete --limit 1000 --commit-each --primary-key-only
4.2 pt-query-digest --- 慢查询分析
功能: 对多种日志进行汇总分析,如 binlog、general log、tcpdump、slowlog、SQL 等,最常用的是对慢日志进行分析。
基本语法:
bash
pt-query-digest [OPTIONS] [FILES]
4.2.1 慢日志格式说明
# Time: 2022-09-02T22:51:41.160855+08:00
# User@Host: root[root] @ localhost [] Id: 2021
# Query_time: 5.388106 Lock_time: 0.000004 Rows_sent: 7 Rows_examined: 5081822
SET timestamp=1662130295;
SELECT title, ROUND(AVG(salary), 2) as avg_salary
FROM titles t JOIN salaries s ON s.emp_no = t.emp_no
GROUP BY title
ORDER BY avg_salary DESC;
| 字段 | 含义 |
|---|---|
| Time | SQL 结束时间 |
| Id | Processlist id |
| Query_time | 查询时间 |
| Lock_time | 锁等待时间 |
| Rows_sent | 返回给客户端的行数 |
| Rows_examined | 存储引擎中检索的行数 |
| SET timestamp | SQL 的执行时间 |
4.2.2 测试数据准备
sql
-- 在主库(141)执行
-- 开启慢查询日志
SET GLOBAL slow_query_log=ON;
SET GLOBAL long_query_time=1; -- 超过1秒的查询记录到慢日志
SET GLOBAL log_queries_not_using_indexes=ON; -- 未使用索引的查询也记录
SELECT @@slow_query_log_file;
-- /data/mysql/3306/data/MYSQL141-slow.log
-- 制造慢SQL
USE employees;
SELECT title, ROUND(AVG(salary),2) avg_sal FROM titles t JOIN salaries s ON t.emp_no=s.emp_no GROUP BY title ORDER BY avg_sal DESC;
SELECT SQL_NO_CACHE * FROM dept_emp WHERE dept_no='d005' AND from_date>'1990-01-01' ORDER BY emp_no;
SELECT SQL_NO_CACHE COUNT(*) FROM salaries a JOIN salaries b ON a.emp_no=b.emp_no+5000 JOIN titles t ON a.emp_no=t.emp_no WHERE t.title LIKE '%Engineer%';
SELECT SQL_NO_CACHE * FROM salaries ORDER BY RAND() LIMIT 10;
USE sbtest;
SELECT SQL_NO_CACHE pad, COUNT(*) FROM sbtest1 GROUP BY pad ORDER BY 2 DESC LIMIT 5;
SELECT SQL_NO_CACHE c FROM sbtest2 WHERE c LIKE '%data-99999%' ORDER BY k DESC;
4.2.3 场景一:基础慢日志分析
bash
# 执行节点:141
pt-query-digest /data/mysql/3306/data/MYSQL141-slow.log
输出结果解读:
# 80ms user time, 0 system time, 38.14M rss, 246.48M vsz
# Current date: Sun Aug 16 18:45:47 2026
# Hostname: MYSQL141
# Files: /data/mysql/3306/data/MYSQL141-slow.log
# Overall: 8 total, 6 unique, 0.67 QPS, 0.05x concurrency ______________
# Time range: 2026-08-16T18:45:35 to 2026-08-16T18:45:47
# Attribute total min max avg 95% stddev median
# ============ ======= ======= ======= ======= ======= ======= =======
# Exec time 631ms 8ms 315ms 79ms 308ms 92ms 75ms
# Lock time 12us 1us 2us 1us 1us 0 1us
# Rows sent 138 1 100 17.25 97.36 30.55 6.98
# Rows examine 438.20k 19.54k 97.66k 54.77k 97.04k 27.99k 38.40k
# Query size 810 60 130 101.25 124.25 23.98 115.80
Overall 部分(总体统计):
8 total:总共8条慢查询6 unique:6个唯一查询(指纹去重后)0.67 QPS:每秒查询数0.05x concurrency:并发度
Attribute 部分(属性统计):
-
Exec time:执行时间(total/min/max/avg/95%/stddev/median) -
Lock time:锁等待时间 -
Rows sent:返回行数 -
Rows examine:扫描行数 -
Query size:查询语句大小Profile
Rank Query ID Response time Calls R/Call V/M
==== =================================== ============= ===== ====== ====
1 0x999CFE998D101109E343C230C02A48A8 0.3150 49.9% 1 0.3150 0.00 SELECT sbtest?
2 0x573A716569FD99453F2B3D7974972A7C 0.2419 38.3% 3 0.0806 0.00 SELECT titles salaries
3 0x0885DAAD16AA961AA0007DF2B88411E0 0.0385 6.1% 1 0.0385 0.00 SELECT sbtest?
4 0x64C04464A0B56612FDADCC540F989553 0.0148 2.3% 1 0.0148 0.00 SELECT sbtest?
MISC 0xMISC 0.0210 3.3% 2 0.0105 0. <2 ITEMS>
Profile 部分(查询概要):
| 列 | 含义 |
|---|---|
| Rank | 排名,越靠前,代表总的执行时间越长 |
| Query ID | 每一类 SQL 的 fingerprint(指纹) |
| Response time | 总执行时间及执行时间占比 |
| Calls | 执行次数 |
| R/Call | 平均执行时间 |
| V/M | 平均方差(Variance-to-Mean ratio),值越大说明查询时间越不稳定 |
# Query 1: 0 QPS, 0x concurrency, ID 0x999CFE998D101109E343C230C02A48A8 at byte 2069
# This item is included in the report because it matches --limit.
# Scores: V/M = 0.00
# Time range: all events occurred at 2026-08-16T18:45:35
# Attribute pct total min max avg 95% stddev median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count 12 1
# Exec time 49 315ms 315ms 315ms 315ms 315ms 0 315ms
# Lock time 16 2us 2us 2us 2us 2us 0 2us
# Rows sent 3 5 5 5 5 5 0 5
# Rows examine 20 87.90k 87.90k 87.90k 87.90k 87.90k 0 87.90k
# Query size 10 83 83 83 83 83 0 83
# String:
# Databases sbtest
# Hosts localhost
# Users root
# Query_time distribution
# 1us
# 10us
# 100us
# 1ms
# 10ms
# 100ms ################################################################
# 1s
Query 详情部分:
pct:该查询在某属性上占总体的百分比Query_time distribution:查询时间分布直方图,#越多代表该时间段的查询越多
4.2.4 场景二:--since / --until 分析指定时间段
bash
# 执行节点:141
pt-query-digest --since "2026-08-16 18:45:00" --until "2026-08-16 18:46:00" /data/mysql/3306/data/MYSQL141-slow.log
输出结果:
# Overall: 8 total, 6 unique, 0.67 QPS, 0.05x concurrency ______________
# Time range: 2026-08-16T18:45:35 to 2026-08-16T18:45:47
只分析 18:45:00 ~ 18:46:00 之间的慢查询。格式推荐
YYYY-MM-DD [HH:MM:SS]。
4.2.5 场景三:--type=genlog 分析 General Log
bash
# 执行节点:141
# 开启general log
mysql -uroot -p'Root@123456' -e "SET GLOBAL general_log=ON;"
mysql -uroot -p'Root@123456' employees -e "SELECT COUNT(*) FROM salaries; SELECT * FROM titles LIMIT 1;"
mysql -uroot -p'Root@123456' -e "SET GLOBAL general_log=OFF;"
GENLOG=$(mysql -uroot -p'Root@123456' -N -e "SELECT @@general_log_file;")
pt-query-digest --type genlog $GENLOG
输出结果:
# Overall: 9 total, 6 unique, 0 QPS, 0x concurrency ______________________
# Profile
# Rank Query ID Response time Calls R/Call V/M
# 1 0x0E7680C04FF2596BE3A3649C5FAC418D 0.0000 0.0% 2 0.0000 0.00 SELECT
# 5 0x8089DBBA1D2606CAAC215D9D2C7DD498 0.0000 0.0% 1 0.0000 0.00 SELECT salaries
# 6 0x534FF89E6F6E7EE48CC5F6769E792627 0.0000 0.0% 1 0.0000 0.00 SELECT titles
4.2.6 场景四:--limit 限制输出
bash
# 执行节点:141
# 按总的查询时间排序,只输出指定比例的分析结果,默认95%
pt-query-digest --limit 90% /data/mysql/3306/data/MYSQL141-slow.log
输出结果:
# Rank Query ID Response time Calls R/Call V/M
# MISC 0xMISC 1.1570 0.5% 137 0.0084 0.0 <36 ITEMS>
--limit默认为 95%,即按总查询时间排序,输出累计占比 95% 的查询。设为90%则只输出前 90%。
4.2.7 常用参数汇总表
| 参数 | 说明 |
|---|---|
--since |
分析指定时间之后的慢SQL,格式 YYYY-MM-DD [HH:MM:SS] |
--until |
分析指定时间之前的慢SQL |
--type |
指定分析类型:binlog/genlog/slowlog/tcpdump/rawlog,默认 slowlog |
--limit |
按总查询时间排序,只输出指定比例/数量,默认 95% |
--order-by |
排序字段,默认 Query_time:sum(按总查询时间) |
--output |
输出格式:report/slowlog/json/json-anon/secure-slowlog,默认 report |
--[no]report |
是否输出分析结果 |
--report-all |
指定 --review 时,对已review过的SQL也输出 |
4.3 pt-table-checksum --- 主从数据一致性校验
功能: 校验主从数据一致性。
基本语法:
bash
pt-table-checksum [OPTIONS] DSN
4.3.1 实现原理
- 将
innodb_lock_wait_timeoutsession 值设置为1。如果 checksum 操作与业务 SQL 出现了锁争用,会直接回滚 checksum 操作。 - 将
binlog_formatsession 会话值设置为 STATEMENT,这样的话,checksum 对应的 REPLACE INTO 操作才会在从库执行(从库重算CRC)。 - 创建
percona.checksums表,保存主从数据一致性校验的结果。 - 选取分片键,一般为主键或唯一索引。
- 分 chunk 进行 checksum 操作:
sql
REPLACE INTO `percona`.`checksums` (db, tbl, chunk, chunk_index, lower_boundary,
upper_boundary, this_cnt, this_crc) SELECT 'sbtest', 'sbtest1', '1', 'PRIMARY',
'1', '385073', COUNT(*) AS cnt,
COALESCE(LOWER(CONV(BIT_XOR(CAST(CRC32(CONCAT_WS('#', `id`, `k`, convert(`c` using
utf8mb4), convert(`pad` using utf8mb4))) AS UNSIGNED)), 10, 16)), 0) AS crc FROM
`sbtest`.`sbtest1` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= '1')) AND ((`id` <=
'385073')) /*checksum chunk*/
- 每检验完一张表,都会在从库进行一次查询,比对 this_crc 和 master_crc:
sql
SELECT CONCAT(db, '.', tbl) AS `table`, chunk, ...,
COALESCE(this_cnt-master_cnt, 0) AS cnt_diff,
COALESCE(this_crc <> master_crc OR ISNULL(master_crc) <> ISNULL(this_crc), 0) AS crc_diff
FROM `percona`.`checksums`
WHERE (master_cnt <> this_cnt OR master_crc <> this_crc ...)
核心原理: REPLACE INTO...SELECT 语句以 STATEMENT 格式记录到 binlog,从库回放时用本地数据重新计算 CRC 值,写入 this_crc 列;主库随后 UPDATE master_crc 列为主库的计算值。通过比对 this_crc(从库算)和 master_crc(主库算)判断是否一致。
4.3.2 输出结果说明
TS ERRORS DIFFS ROWS DIFF_ROWS CHUNKS SKIPPED TIME TABLE
08-16T20:10:16 0 1 90049 0 4 0 0.518 sbtest.sbtest1
| 列 | 含义 |
|---|---|
| TS | 时间戳 |
| ERRORS | 校验过程中的错误数 |
| DIFFS | 主从数据不一致的 chunk 数量。如果该列不为0,则意味着主从存在不一致 |
| ROWS | 表被校验的记录数 |
| DIFF_ROWS | 不一致的行数 |
| CHUNKS | chunk 的个数 |
| SKIPPED | 跳过的 chunk 个数 |
| TIME | 校验表所花费的时间 |
| TABLE | 校验的表名 |
4.3.3 场景一:实例级校验
bash
# 执行节点:141(主库)
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \n --no-check-binlog-format --no-check-replication-filters \n --recursion-method=hosts --set-vars binlog_format=STATEMENT
参数说明:
--no-check-binlog-format:不检查从库的 binlog_format 是否为 row--no-check-replication-filters:不检查复制过滤规则--recursion-method=hosts:通过 SHOW SLAVE HOSTS 发现从库--set-vars binlog_format=STATEMENT:关键参数! 设置 session 级 binlog_format=STATEMENT,确保 checksum 语句以 STATEMENT 格式记录到 binlog,从库才能重算 CRC
重要踩坑: MySQL 8.0 默认
binlog_format=ROW。如果不加--set-vars binlog_format=STATEMENT,REPLACE INTO...SELECT 语句会以 ROW 格式记录到 binlog,从库直接应用主库预计算的 CRC 值,导致this_crc = master_crc,永远检测不到差异!
4.3.4 场景二:基于指定库的校验
bash
# 执行节点:141
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \n --no-check-binlog-format --recursion-method=hosts \n --set-vars binlog_format=STATEMENT \n --databases sbtest,employees
多个库之间用逗号隔开。
4.3.5 场景三:基于指定表的校验
bash
# 执行节点:141
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \n --no-check-binlog-format --recursion-method=hosts \n --set-vars binlog_format=STATEMENT \n --tables sbtest.sbtest1
4.3.6 场景四:制造差异并检测
bash
# 执行节点:141(通过远程连142制造差异)
# 在142上直接修改数据(绕过复制)
mysql -h192.168.195.142 -e "SET SESSION sql_log_bin=0; UPDATE sbtest.sbtest1 SET remark='DIFF-ON-142' WHERE id=500;"
# 验证差异存在
mysql -h192.168.195.142 -N -e "SELECT remark FROM sbtest.sbtest1 WHERE id=500;"
# DIFF-ON-142
mysql -h192.168.195.141 -N -e "SELECT remark FROM sbtest.sbtest1 WHERE id=500;"
# (空)
# 执行校验
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \n --no-check-binlog-format --no-check-replication-filters \n --recursion-method=hosts --databases=sbtest --tables=sbtest1 \n --set-vars binlog_format=STATEMENT
输出结果:
TS ERRORS DIFFS ROWS DIFF_ROWS CHUNKS SKIPPED TIME TABLE
08-16T20:10:16 0 1 90049 0 4 0 0.518 sbtest.sbtest1
DIFFS=1,检测到不一致! 查看详细差异:
sql
-- 在从库(142)执行
SELECT chunk, this_cnt, master_cnt, this_crc, master_crc, (this_crc<>master_crc) AS DIFF
FROM percona.checksums WHERE tbl='sbtest1';
chunk this_cnt master_cnt this_crc master_crc DIFF
1 1000 1000 8d5ba7e1 ab7ad75c 1 ← chunk1 不一致!
2 89049 89049 d0e1a89e d0e1a89e 0
3 0 0 0 0 0
4 0 0 0 0 0
4.3.7 场景五:--explain 只打印不执行
bash
# 执行节点:141
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \n --no-check-binlog-format --recursion-method=hosts \n --databases=sbtest --tables=sbtest3 --explain
输出结果:
sql
REPLACE INTO `percona`.`checksums` (db, tbl, chunk, chunk_index, lower_boundary, upper_boundary, this_cnt, this_crc) SEL...
4.3.8 场景六:--replicate-check-only
bash
# 执行节点:141
# 直接根据 percona.checksums 表的内容输出主从差异结果,不做任何校验
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \n --no-check-binlog-format --replicate-check-only --recursion-method=hosts
4.3.9 级联从库注意事项
重要: 在级联复制架构(141→142→143)中,142 的
binlog_format=ROW且log_slave_updates=ON。当 142 回放来自 141 的 STATEMENT 格式 checksum 语句时,会以 ROW 格式重新记录到自己的 binlog。这意味着 143 收到的是 ROW 事件(预计算值),143 无法检测自身的本地篡改。正确做法:
- 检测 141 vs 142 的差异:在 141 上执行 checksum,正常工作 ✅
- 检测 142 vs 143 的差异:需在 142 上执行 checksum(142 作为"主",143 作为"从"),或使用
pt-table-sync --sync-to-master直接检测 143
4.3.10 常用参数汇总表
| 参数 | 说明 |
|---|---|
--no-check-binlog-format |
不检查从库 binlog_format |
--no-check-replication-filters |
不检查复制过滤规则 |
--recursion-method |
从库发现方法:hosts/processlist/none/dsn=DSN |
--recurse |
递归层级,默认1(只查直连从库) |
--set-vars |
设置 session 变量,如 binlog_format=STATEMENT |
--databases |
指定校验的库 |
--tables |
指定校验的表 |
--replicate-check-only |
只根据 checksums 表输出差异,不执行校验 |
--explain |
只打印 checksum 操作,不实际执行 |
--quiet |
只输出 error/warning/不一致信息 |
--chunk-size |
chunk 大小,默认 1000 |
--chunk-time |
chunk 查询时间,默认 0.5s(基于时间自动调节 chunk 大小) |
--max-load |
基于数据库负载自动调节,默认 Threads_running=25 |
--resume |
从上次终止的地方继续执行 |
4.4 pt-table-sync --- 修复从库不一致数据
功能: 修复主从不一致的数据。在执行 pt-table-sync 之前,无需执行 pt-table-checksum,其自带主从数据一致性校验功能。
4.4.1 实现原理
- 都是分 chunk 处理,chunk-size 基本上是 1000,可以缩小排它锁的锁定范围。
- 修改会话的事务隔离级别,计算 chunk 的 checksum 值,并对其添加排它锁。
- 查看当前 Binlog 的位置点。
- 位置点信息将作为参数传递给从库执行的 SELECT MASTER_POS_WAIT() 操作。等 MASTER_POS_WAIT 执行完,计算 slave 相同 chunk 的 checksum 值。
- 如果 chunk 的 checksum 值不一致,则会比对该 chunk 每一行的 checksum 值。
- 确定完不一致后,会在主库(而不是从库)进行修复操作。
注意: 修复操作是在主库进行的,在一主多从的架构下,即使只对其中一个 slave 进行修复操作,操作也会同步到所有 slave 上。
4.4.2 场景一:--sync-to-master 修复指定从库
bash
# 执行节点:141
# 先在143制造差异
mysql -h192.168.195.143 -e "SET SESSION sql_log_bin=0; UPDATE sbtest.sbtest1 SET c='HACKED-BY-DBA' WHERE id=15000; DELETE FROM sbtest.sbtest1 WHERE id BETWEEN 15001 AND 15010; INSERT INTO sbtest.sbtest1(id,k,c,pad) VALUES (200001, 1, 'GHOST-ROW', 'ghost');"
# 查看差异
mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;" # 90000
mysql -h192.168.195.143 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;" # 89991
# 先 --print 打印修复语句
pt-table-sync --print --sync-to-master h=192.168.195.143,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest1
# --execute 执行修复
pt-table-sync --execute --sync-to-master h=192.168.195.143,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest1
参数说明:
--sync-to-master:修复指定从库的数据。必须指定此参数,且给定的 DSN 必须是 slave 。同时,其还会将--wait设置为60,--lock设置为1。--print:打印修复语句(建议修复之前先打印)--execute:执行修复操作--dry-run:只是验证命令是否可行
验证:
bash
mysql -h192.168.195.143 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 90000 ← 行数恢复
mysql -h192.168.195.143 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1 WHERE c='HACKED-BY-DBA';"
# 0 ← 篡改数据已修复
mysql -h192.168.195.143 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1 WHERE id=200001;"
# 0 ← 幽灵行已删除
mysql -h192.168.195.143 -N -e "SELECT c FROM sbtest.sbtest1 WHERE id=15000;"
# data-15000-aaaaaaaaaaaaaa... ← 原始数据恢复
4.4.3 场景二:--replicate 基于checksums表修复所有从库
bash
# 执行节点:141
# 前提:必须先执行 pt-table-checksum,将主从校验结果保存到 percona.checksums 表中
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \n --no-check-binlog-format --recursion-method=hosts \n --set-vars binlog_format=STATEMENT --databases=sbtest
# 基于 checksums 表修复所有 slave
# DSN 必须给 master(141)
pt-table-sync --execute --replicate percona.checksums h=192.168.195.141,P=3306,u=root,p='Root@123456'
注意:
--replicate模式下,DSN 给的是 master。这是修复所有 slave 数据的唯一方法。
4.4.4 场景三:独立环境下的数据同步
bash
# 适用于没有主从复制关系的独立实例
pt-table-sync --execute host1 host2 host3
这里的 host1、host2、host3 必须是相对独立的,没有主从复制关系。
4.4.5 常用参数汇总表
| 参数 | 说明 |
|---|---|
--execute |
执行修复操作 |
--print |
打印修复语句 |
--dry-run |
只验证命令是否可行 |
--sync-to-master |
修复指定从库(DSN给slave) |
--replicate |
基于checksums表修复(DSN给master) |
--databases |
指定库 |
--tables |
指定表 |
--where |
指定条件 |
--chunk-column |
分片列 |
--chunk-index |
指定分片索引 |
--chunk-size |
chunk大小,默认1000 |
--wait |
等待从库应用到指定位置点的时间 |
--timeout-ok |
超时后继续执行(但一致性难保证) |
--function |
校验函数:CRC32(默认)/MD5/SHA1 |
--algorithms |
校验算法:Chunk/Nibble/GroupBy/Stream |
4.5 pt-online-schema-change --- DDL变更工具
功能: 在线DDL变更工具,不锁表完成表结构修改。
基本语法:
bash
pt-online-schema-change --alter "ALTER_STATEMENT" D=dbname,t=tablename,h=host,P=port,u=user,p=password
4.5.1 实现原理
- 创建与原表结构相同的影子表
_tablename_new - 在影子表上执行 ALTER 操作
- 在原表上创建3个触发器(INSERT/UPDATE/DELETE),将原表的增量数据同步到影子表
- 分 chunk 将原表数据复制到影子表
- 数据复制完成后,用 RENAME TABLE 原子交换原表和影子表
- 删除旧表和触发器
4.5.2 场景一:加列加索引
bash
# 执行节点:141
# 先 --dry-run 预检
pt-online-schema-change --alter "ADD COLUMN remark VARCHAR(50) DEFAULT '', ADD INDEX idx_remark(remark)" \n D=sbtest,t=sbtest1,h=192.168.195.141,P=3306,u=root,p='Root@123456' \n --dry-run --print
--dry-run 输出(关键SQL):
sql
-- 1. 创建影子表
CREATE TABLE `sbtest`.`_sbtest1_new` (...);
-- 2. 在影子表上执行ALTER
ALTER TABLE `sbtest`.`_sbtest1_new` ADD COLUMN remark VARCHAR(50) DEFAULT '', ADD INDEX idx_remark(remark)
-- 3. 创建3个触发器
SQL: CREATE TRIGGER `pt_osc_sbtest_sbtest1_del` AFTER DELETE ON `sbtest`.`sbtest1` FOR EACH ROW ...
SQL: CREATE TRIGGER `pt_osc_sbtest_sbtest1_upd` AFTER UPDATE ON `sbtest`.`sbtest1` FOR EACH ROW ...
SQL: CREATE TRIGGER `pt_osc_sbtest_sbtest1_ins` AFTER INSERT ON `sbtest`.`sbtest1` FOR EACH ROW ...
-- 4. 分chunk复制数据
SELECT /*!40001 SQL_NO_CACHE */ `id` FROM `sbtest`.`sbtest1` FORCE INDEX(`PRIMARY`) WHERE ...
bash
# --execute 执行DDL
pt-online-schema-change --alter "ADD COLUMN remark VARCHAR(50) DEFAULT '', ADD INDEX idx_remark(remark)" \n D=sbtest,t=sbtest1,h=192.168.195.141,P=3306,u=root,p='Root@123456' \n --recursion-method=none --execute --progress time,5 --statistics
踩坑提示: 在主从环境中,如果不加
--recursion-method=none,pt-osc 会尝试探测从库延迟,可能报错Use of uninitialized value in string eq at line 4357。加--recursion-method=none跳过从库探测即可。生产环境如需检查从库延迟,用--recursion-method=hosts --max-load Threads_running=50。
输出结果:
2026-08-16T20:03:58 Copied rows OK.
2026-08-16T20:03:58 Analyzing new table...
2026-08-16T20:03:58 Swapping tables...
2026-08-16T20:03:58 Swapped original and new tables OK.
2026-08-16T20:03:58 Dropping old table...
2026-08-16T20:03:58 Dropped old table `sbtest`.`_sbtest1_old` OK.
2026-08-16T20:03:58 Dropping triggers...
2026-08-16T20:03:58 Dropped triggers OK.
# Event Count
# ====== =====
# INSERT 3
Successfully altered `sbtest`.`sbtest1`.
验证:
sql
mysql -h192.168.195.141 -e "SHOW CREATE TABLE sbtest.sbtest1\G" | grep -E "remark|idx_remark"
-- `remark` varchar(50) DEFAULT '',
-- KEY `idx_remark` (`remark`)
mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;" -- 90000
mysql -h192.168.195.142 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;" -- 90000
4.5.3 场景二:并发写入时验证触发器同步
bash
# 执行节点:141
# 后台持续插入+更新
( for i in $(seq 200002 200050); do
mysql -h192.168.195.141 sbtest -e "INSERT INTO sbtest1(id,k,c,pad,remark) VALUES($i, 1, 'conc-insert', 'pad', 'CONCURRENT'); UPDATE sbtest1 SET remark='UPDATED' WHERE id=$i;"
done ) &
# 同时执行pt-osc (重建索引)
pt-online-schema-change --alter "DROP INDEX idx_remark, ADD INDEX idx_remark(remark)" \n D=sbtest,t=sbtest1,h=192.168.195.141,P=3306,u=root,p='Root@123456' \n --recursion-method=none --execute --statistics
wait
输出结果:
2026-08-16T20:04:10 Copying approximately 88821 rows...
2026-08-16T20:04:11 Swapped original and new tables OK.
# INSERT 4
Successfully altered `sbtest`.`sbtest1`.
验证(并发数据无丢失):
bash
mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1 WHERE remark='UPDATED';"
# 49 ← 49条并发插入+更新的数据完整保留
mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 90049 ← 总行数正确,无丢失
说明: 49条并发插入的行,每行先以
CONCURRENT插入,随后被 UPDATE 改为UPDATED。pt-osc 的触发器正确同步了这些增量变更,最终49条全部为UPDATED,数据完整无丢失。✅
4.5.4
4.2 pt-query-digest --- 慢查询分析
功能: 对多种日志进行汇总分析,如 binlog、general log、tcpdump、slowlog、SQL 等,最常用的还是对慢日志进行分析。
4.2.1 基本用法
bash
# 执行节点:141
pt-query-digest /data/mysql/3306/data/MYSQL141-slow.log
4.2.2 开启慢日志并制造测试数据
sql
-- 在主库(141)执行
SET GLOBAL slow_query_log=ON;
SET GLOBAL long_query_time=1; -- 超过1秒的查询记入慢日志
SET GLOBAL log_queries_not_using_indexes=ON; -- 未使用索引的查询也记入慢日志
SELECT @@slow_query_log_file;
-- /data/mysql/3306/data/MYSQL141-slow.log
sql
-- 制造慢SQL(employees库)
SELECT title, ROUND(AVG(salary),2) avg_sal FROM titles t JOIN salaries s ON t.emp_no=s.emp_no GROUP BY title ORDER BY avg_sal DESC;
SELECT SQL_NO_CACHE COUNT(*) FROM salaries a JOIN salaries b ON a.emp_no=b.emp_no+5000 JOIN titles t ON a.emp_no=t.emp_no WHERE t.title LIKE '%Engineer%';
SELECT SQL_NO_CACHE * FROM salaries ORDER BY RAND() LIMIT 10;
SELECT SQL_NO_CACHE c FROM sbtest1 WHERE k BETWEEN 1 AND 50000 ORDER BY c DESC LIMIT 100;
4.2.3 输出结果详解
bash
# 执行节点:141
pt-query-digest /data/mysql/3306/data/MYSQL141-slow.log
第一部分:Overall(总览)
# Overall: 8 total, 6 unique, 0.67 QPS, 0.05x concurrency ______________
# Time range: 2026-08-16T18:45:35 to 2026-08-16T18:45:47
# Attribute total min max avg 95% stddev median
# ============ ======= ======= ======= ======= ======= ======= =======
# Exec time 631ms 8ms 315ms 79ms 308ms 92ms 75ms
# Lock time 12us 1us 2us 1us 1us 0 1us
# Rows sent 138 1 100 17.25 97.36 30.55 6.98
# Rows examine 438.20k 19.54k 97.66k 54.77k 97.04k 27.99k 38.40k
# Query size 810 60 130 101.25 124.25 23.98 115.80
字段说明:
total:8 total(共8条慢查询),6 unique(6种不同的SQL),0.67 QPS,0.05x concurrency(并发度)Exec time:执行时间,总631ms,最小8ms,最大315ms,平均79ms,95%为308msLock time:锁等待时间Rows sent:返回给客户端的行数Rows examine:存储引擎中检索的行数(扫描行数)Query size:SQL字节数
第二部分:Profile(SQL排名)
# Rank Query ID Response time Calls R/Call V/M
# ==== =================================== ============= ===== ====== ====
# 1 0x999CFE998D101109E343C230C02A48A8 0.3150 49.9% 1 0.3150 0.00 SELECT sbtest?
# 2 0x573A716569FD99453F2B3D7974972A7C 0.2419 38.3% 3 0.0806 0.00 SELECT titles salaries
# 3 0x0885DAAD16AA961AA0007DF2B88411E0 0.0385 6.1% 1 0.0385 0.00 SELECT sbtest?
# 4 0x64C04464A0B56612FDADCC540F989553 0.0148 2.3% 1 0.0148 0.00 SELECT sbtest?
# MISC 0xMISC 0.0210 3.3% 2 0.0105 0.0 <2 ITEMS>
字段说明:
Rank:排名,越靠前代表总的执行时间越长Query ID:每一类SQL的 fingerprint(指纹),相同SQL模板归为同一IDResponse time:总的执行时间及占比(49.9% = 占所有慢查询执行时间的49.9%)Calls:执行次数R/Call:平均执行时间V/M:平均方差(Variance to Mean ratio),值越大说明执行时间波动越大,可能存在不稳定
第三部分:Query Detail(单条SQL详情)
# Query 1: 0 QPS, 0x concurrency, ID 0x999CFE998D101109E343C230C02A48A8 at byte 2069
# Attribute pct total min max avg 95% stddev median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count 12 1
# Exec time 49 315ms 315ms 315ms 315ms 315ms 0 315ms
# Rows examine 20 87.90k 87.90k 87.90k 87.90k 87.90k 0 87.90k
# Query size 10 83 83 83 83 83 0 83
# String:
# Databases sbtest
# Hosts localhost
# Users root
# Query_time distribution
# 1us
# 10us
# 100us
# 1ms
# 10ms
# 100ms ################################################################
# 1s
关键字段:
pct:该指标占所有查询的百分比Query_time distribution:查询时间分布直方图,#越多说明该时间段的查询越多
4.2.4 常用参数
--since / --until:分析指定时间段的慢SQL
bash
# 执行节点:141
pt-query-digest --since "2026-08-16 18:45:00" --until "2026-08-16 18:46:00" /data/mysql/3306/data/MYSQL141-slow.log
格式推荐 YYYY-MM-DD [HH:MM:SS]。
--type:指定分析的类型
bash
# 可选值:binlog, genlog, slowlog, tcpdump, rawlog。默认为 slowlog。
--type=genlog 分析General Log:
bash
# 执行节点:141
# 先开启general log
mysql -e "SET GLOBAL general_log=ON;"
mysql employees -e "SELECT COUNT(*) FROM salaries; SELECT * FROM titles LIMIT 1;"
mysql -e "SET GLOBAL general_log=OFF;"
GENLOG=$(mysql -N -e "SELECT @@general_log_file;")
pt-query-digest --type genlog $GENLOG
输出:
# Overall: 9 total, 6 unique, 0 QPS, 0x concurrency ______________________
# Profile
# Rank Query ID Response time Calls R/Call V/M
# 1 0x0E7680C04FF2596BE3A3649C5FAC418D 0.0000 0.0% 2 0.0000 0.00 SELECT
# 5 0x8089DBBA1D2606CAAC215D9D2C7DD498 0.0000 0.0% 1 0.0000 0.00 SELECT salaries
# 6 0x534FF89E6F6E7EE48CC5F6769E792627 0.0000 0.0% 1 0.0000 0.00 SELECT titles
--limit:按总查询时间排序,只输出指定比例或数量
bash
# 默认为 95%(按总查询时间排序输出前95%的分析结果)
pt-query-digest --limit 90% /data/mysql/3306/data/MYSQL141-slow.log
# MISC 0xMISC 1.1570 0.5% 137 0.0084 0.0 <36 ITEMS>
--order-by:排序方式
默认值 Query_time:sum,按照总的查询时间输出结果。可改为 Query_time:cnt(按执行次数)等。
--output:输出格式
默认为 report,其它选项有:slowlog、json、json-anon、secure-slowlog。
--noreport:是否输出查询分析结果
当指定 --history 或 --review 时,可通过 --no-report 屏蔽输出。
--report-all:输出已review过的SQL
当指定 --review 时,对于已经 review 过的SQL不会再次输出。如果要让其输出,可指定 --report-all。
4.2.5 参数汇总表
| 参数 | 说明 |
|---|---|
--since |
分析指定时间之后的慢SQL |
--until |
分析指定时间之前的慢SQL |
--type |
指定分析类型:binlog/genlog/slowlog/tcpdump/rawlog |
--limit |
输出比例或数量,默认95% |
--order-by |
排序方式,默认Query_time:sum |
--output |
输出格式:report/slowlog/json/json-anon |
--[no]report |
是否输出分析结果 |
--report-all |
输出已review过的SQL |
--review |
将分析结果存入数据库表 |
--history |
将分析结果存入数据库表(保留历史) |
4.3 pt-table-checksum --- 主从数据一致性校验
功能: 校验主从数据一致性。
4.3.1 实现原理
-
将
innodb_lock_wait_timeoutsession 值设置为1。如果 checksum 操作与业务 SQL 出现了锁争用,会直接回滚 checksum 操作。 -
将
binlog_formatsession 会话值设置为 STATEMENT,这样的话,checksum 对应的 REPLACE INTO 操作才会在从库执行(从库重算CRC)。 -
创建
percona.checksums表,保存主从数据一致性校验的结果。 -
选取分片键,一般为主键或唯一索引。
-
分 chunk 进行 checksum 操作:
sqlREPLACE INTO `percona`.`checksums` (db, tbl, chunk, chunk_index, lower_boundary, upper_boundary, this_cnt, this_crc) SELECT 'sbtest', 'sbtest1', '1', 'PRIMARY', '1', '385073', COUNT(*) AS cnt, COALESCE(LOWER(CONV(BIT_XOR(CAST(CRC32(CONCAT_WS('#', `id`, `k`, convert(`c` using utf8mb4), convert(`pad` using utf8mb4))) AS UNSIGNED)), 10, 16)), 0) AS crc FROM `sbtest`.`sbtest1` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= '1')) AND ((`id` <= '385073')) /*checksum chunk*/ -
每检验完一张表,都会在从库进行一次查询,比对
this_crc(从库算的)和master_crc(主库算的)。
核心原理: 主库执行
REPLACE INTO ... SELECT CRC32(...)计算每个chunk的CRC值,该语句以STATEMENT格式记录到binlog,从库回放时用从库本地数据重新计算CRC值。如果从库数据与主库不一致,从库算出的this_crc就会与主库的master_crc不同。
4.3.2 基本用法
bash
# 执行节点:141(主库)
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--no-check-binlog-format --no-check-replication-filters \
--recursion-method=hosts --databases=sbtest
输出结果:
TS ERRORS DIFFS ROWS DIFF_ROWS CHUNKS SKIPPED TIME TABLE
08-16T18:46:07 0 0 90000 0 4 0 0.486 sbtest.sbtest1
08-16T18:46:07 0 0 100000 0 1 0 0.464 sbtest.sbtest2
08-16T18:46:07 0 0 50000 0 1 0 0.392 sbtest.sbtest3
输出结果说明:
DIFFS:主从数据不一致的 chunk 的数量。如果该列不为0,则意味着主从存在不一致的地方。ERRORS:校验过程中的错误数ROWS:表被校验的记录数CHUNKS:chunk的个数SKIPPED:跳过的chunk的个数TIME:校验表所花费的时间TABLE:校验的表名
4.3.3 ⚠️ 关键踩坑:ROW格式下DIFFS永远为0
问题描述: 在 MySQL 8.0 默认 binlog_format=ROW 的环境下,pt-table-checksum 的 DIFFS 列永远显示为0,即使从库数据确实与主库不一致。
根因分析:
pt-table-checksum 的核心原理是:checksum 语句(REPLACE INTO ... SELECT CRC32(...)) 必须以 STATEMENT 格式记录到binlog,这样从库回放时才会用本地数据重新计算CRC值。
但在级联复制环境中(141→142→143),中间从库142的 binlog_format=ROW + log_slave_updates=ON:
- 142回放来自141的STATEMENT格式checksum语句时,会以ROW格式重新记录到自己的binlog
- 143(级联从库)收到的是ROW事件,直接应用主库预计算的CRC值,不会在本地重新计算
- 因此143的
this_crc = master_crc,永远检测不出差异
验证实验:
bash
# 1. 在142制造差异(直连从库)
mysql -h192.168.195.142 -e "SET SESSION sql_log_bin=0; UPDATE sbtest.sbtest1 SET remark='DIFF-ON-142' WHERE id=500;"
# 2. 不加 --set-vars binlog_format=STATEMENT 跑checksum
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--no-check-binlog-format --recursion-method=hosts --databases=sbtest
# 结果:DIFFS=0(错误!实际有差异)
# 3. 加 --set-vars binlog_format=STATEMENT 跑checksum
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--no-check-binlog-format --no-check-replication-filters \
--recursion-method=hosts --databases=sbtest --tables=sbtest1 \
--set-vars binlog_format=STATEMENT
正确输出:
TS ERRORS DIFFS ROWS DIFF_ROWS CHUNKS SKIPPED TIME TABLE
08-16T20:10:16 0 1 90049 0 4 0 0.518 sbtest.sbtest1
^^^^
DIFFS=1,成功检测到差异!
查看142的checksums表(从库本地算的CRC ≠ 主库CRC):
sql
-- 在142执行
SELECT chunk, this_cnt, master_cnt, this_crc, master_crc, (this_crc<>master_crc) AS DIFF
FROM percona.checksums WHERE tbl='sbtest1';
chunk this_cnt master_cnt this_crc master_crc DIFF
1 1000 1000 8d5ba7e1 ab7ad75c 1 ← 不一致!
2 89049 89049 d0e1a89e d0e1a89e 0
3 0 0 0 0 0
4 0 0 0 0 0
生产建议: 在
binlog_format=ROW的环境下,务必加--set-vars binlog_format=STATEMENT参数,否则 checksum 语句在从库不会重算CRC,导致永远检测不出差异。
4.3.4 常见用法
实例级校验:
bash
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--no-check-binlog-format --recursion-method=hosts \
--set-vars binlog_format=STATEMENT
基于指定库的校验:
bash
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--no-check-binlog-format --recursion-method=hosts \
--databases sbtest,employees \
--set-vars binlog_format=STATEMENT
多个库之间用逗号隔开。
基于指定表的校验:
bash
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--no-check-binlog-format --recursion-method=hosts \
--tables sbtest.sbtest1 \
--set-vars binlog_format=STATEMENT
--explain:只打印checksum操作,不实际执行
bash
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--no-check-binlog-format --recursion-method=hosts \
--databases=sbtest --tables=sbtest3 --explain
--replicate-check-only:直接根据checksums表输出差异,不做任何校验
bash
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--no-check-binlog-format --recursion-method=hosts \
--replicate-check-only
4.3.5 常用参数
过滤相关:
--databases、--ignore-databases、--databases-regex、--ignore-databases-regex--tables、--ignore-tables、--tables-regex、--ignore-tables-regex--columns、--ignore-columns--engines、--ignore-engines--where
主从相关:
pt-table-checksum 在运行过程中,会以固定频率(默认1s)检测从库的复制情况,如果从库停止或延迟超过一定阈值(默认1s),则会暂停当前操作,直到复制恢复。
--recurse:递归层级--recursion-method:递归方法(hosts/processlist等)--check-slave-lag、--skip-check-slave-lag
性能相关:
--chunk-size:指定chunk的大小,默认为1000--chunk-time:指定chunk的查询时间,默认为0.5s。默认使用后者,基于查询时间自动调节chunk的大小--max-load:基于数据库的负载情况自动调节校验操作。默认为Threads_running=25
输出相关:
--explain:只打印checksum操作,不实际执行--quiet:只会输出error、warning和主从数据不一致的信息
其它参数:
--[no]check-binlog-format:检查从库的binlog_format是否为row。考虑级联复制场景--resume:从上次终止的地方继续执行--set-vars binlog_format=STATEMENT:关键参数,确保checksum语句以STATEMENT格式记录
4.4 pt-table-sync --- 修复从库不一致数据
功能: 修复主从不一致的数据。在执行 pt-table-sync 之前,无需执行 pt-table-checksum,其自带主从数据一致性校验功能。
4.4.1 实现原理
- 都是分chunk处理,chunk-size基本上是1000,可以缩小排它锁的锁定范围。
- 修改会话的事务隔离级别,计算chunk的checksum值,并对其添加排它锁。
- 查看当前Binlog的位置点。
- 位置点信息将作为参数传递给从库执行的
SELECT MASTER_POS_WAIT()操作。等 MASTER_POS_WAIT 执行完,计算slave相同chunk的checksum值。 - 如果chunk的checksum值不一致,则会比对该chunk每一行的checksum值。
- 确定完不一致后,会在主库(而不是从库)进行修复操作。
关键: 修复操作在主库执行,通过复制同步到从库。在一主多从架构下,即使只对其中一个slave进行修复操作,操作也会同步到所有slave上。
4.4.2 场景一:修复某个slave的数据(--sync-to-master)
bash
# 执行节点:141
# 1. 先制造差异:在143直接改数据(绕过复制)
mysql -h192.168.195.143 -e "SET SESSION sql_log_bin=0; UPDATE sbtest.sbtest1 SET c='HACKED-BY-DBA' WHERE id=15000; DELETE FROM sbtest.sbtest1 WHERE id BETWEEN 15001 AND 15010; INSERT INTO sbtest.sbtest1(id,k,c,pad,remark) VALUES (200001, 1, 'GHOST-ROW', 'ghost', '');"
# 2. 验证差异
echo -n "141行数: "; mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 90000
echo -n "143行数: "; mysql -h192.168.195.143 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 89991 ← 少了9行(删了10行,多了1个GHOST)
# 3. 先 --print 打印修复语句(生产推荐先打印再执行)
pt-table-sync --print --sync-to-master h=192.168.195.143,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest1
# 4. --execute 执行修复
pt-table-sync --execute --sync-to-master h=192.168.195.143,P=3306,u=root,p='Root@123456',D=sbtest,t=sbtest1
参数说明:
--sync-to-master:修复指定从库的数据。必须指定此参数,且给定的DSN必须是 slave 。同时,其还会将--wait设置为60,--lock设置为1。--execute:执行修复操作--print:打印修复语句(建议修复之前先打印)--dry-run:只是验证命令是否可行
验证:
bash
echo -n "141行数: "; mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 90000
echo -n "143行数: "; mysql -h192.168.195.143 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 90000 ← 恢复一致
echo -n "143 HACKED行(应0): "; mysql -h192.168.195.143 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1 WHERE c='HACKED-BY-DBA';"
# 0
echo -n "143 GHOST行(应0): "; mysql -h192.168.195.143 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1 WHERE id=200001;"
# 0
echo -n "143 id=15000 c值: "; mysql -h192.168.195.143 -N -e "SELECT c FROM sbtest.sbtest1 WHERE id=15000;" | head -c 25
# data-15000-aaaaaaaaaaaaaa ← 恢复原始值
注意: 修复操作是在主库进行的,在一主多从的架构下,即使只对其中一个slave进行修复操作,操作也会同步到所有slave上。
4.4.3 场景二:基于checksums表修复所有slave(--replicate)
bash
# 执行节点:141
# 1. 先执行pt-table-checksum,将校验结果保存到percona.checksums表
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--no-check-binlog-format --recursion-method=hosts --databases=sbtest \
--set-vars binlog_format=STATEMENT
# 2. 制造差异
mysql -h192.168.195.142 -e "SET SESSION sql_log_bin=0; UPDATE sbtest.sbtest1 SET remark='DIFF-ON-142' WHERE id=500;"
# 3. 重新checksum捕获差异
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--no-check-binlog-format --recursion-method=hosts --databases=sbtest --tables=sbtest1 \
--set-vars binlog_format=STATEMENT
# DIFFS=1,成功检测到差异
# 4. 基于checksums表修复(DSN给主库141)
pt-table-sync --execute --replicate percona.checksums h=192.168.195.141,P=3306,u=root,p='Root@123456'
验证:
bash
echo -n "142 id=500 remark(应恢复空): "; mysql -h192.168.195.142 -N -e "SELECT remark FROM sbtest.sbtest1 WHERE id=500;"
# (空) ← 恢复一致
注意:
--replicate模式必须先执行 pt-table-checksum,将主从校验结果保存到percona.checksums表中,然后执行 pt-table-sync 时指定--replicate参数,且给定的 DSN 是 master。这是修复所有slave数据的唯一方法。
4.4.4 场景三:独立环境下的数据同步
bash
# 这里的host1, host2, host3必须是相对独立的,没有主从复制关系
pt-table-sync --execute host1 host2 host3
4.4.5 常用参数
过滤相关:
--databases、--ignore-databases、--engines、--ignore-engines--tables、--ignore-tables、--ignore-tables-regex--columns、--ignore-columns、--where
输出相关:
--execute:执行修复操作--print:打印修复语句。建议修复之前先打印--dry-run:只是验证命令是否可行
分片相关:
--chunk-column:分片列--chunk-index:指定分片索引--chunk-size:chunk的大小,默认1000
主从相关:
--sync-to-master:修复指定从库的数据。在设置此参数的情况下,只能设置一个DSN,且该DSN必须是slave--wait:等待从库应用到指定位置点的时间(MASTER_POS_WAIT的第三个参数)--timeout-ok:如果超时仍继续执行(但一致性难以保证)
其它参数:
--function:指定校验函数,默认为 CRC32,可选择 MD5 和 SHA1--algorithms:指定校验算法:Chunk、Nibble、GroupBy、Stream,优先级从高到低。Chunk/Nibble适用于有主键或唯一索引的表;GroupBy/Stream适用于没有主键或唯一索引的表--replicate:基于--replicate表中的比对结果进行修复
4.5 pt-online-schema-change --- DDL变更工具
功能: 在线DDL变更工具,不锁表完成表结构变更。
4.5.1 实现原理
- 创建与原表结构相同的新表
_表名_new - 在新表上执行 ALTER 操作
- 在原表上创建3个触发器(INSERT/UPDATE/DELETE),将增量数据同步到新表
- 分 chunk 将原表数据复制到新表
- 数据复制完成后,RENAME TABLE 原子交换原表和新表
- 删除旧表和触发器
4.5.2 基本用法
bash
# 执行节点:141
pt-online-schema-change --alter "ADD COLUMN remark VARCHAR(50) DEFAULT '', ADD INDEX idx_remark(remark)" \
D=sbtest,t=sbtest1,h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--recursion-method=none --execute
⚠️ 踩坑提示: 如果不加
--recursion-method=none,在级联复制环境下会报错:
Error while waiting for replica lag: Use of uninitialized value in string eq at /usr/bin/pt-online-schema-change line 4357.原因是 pt-osc 默认尝试探测从库延迟,在级联环境下探测失败。解决方法是显式指定
--recursion-method=none跳过从库检查。
4.5.3 场景一:加列加索引
bash
# 执行节点:141
# 1. --dry-run 预检
pt-online-schema-change --alter "ADD COLUMN remark VARCHAR(50) DEFAULT '', ADD INDEX idx_remark(remark)" \
D=sbtest,t=sbtest1,h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--recursion-method=none --dry-run --print
dry-run 输出(关键步骤):
CREATE TABLE `sbtest`.`_sbtest1_new` ( ← 创建新表
ALTER TABLE `sbtest`.`_sbtest1_new` ADD COLUMN remark VARCHAR(50) DEFAULT '', ADD INDEX idx_remark(remark) ← 在新表上ALTER
SQL: CREATE TRIGGER `pt_osc_sbtest_sbtest1_del` AFTER DELETE ON ... ← 创建DELETE触发器
SQL: CREATE TRIGGER `pt_osc_sbtest_sbtest1_upd` AFTER UPDATE ON ... ← 创建UPDATE触发器
SQL: CREATE TRIGGER `pt_osc_sbtest_sbtest1_ins` AFTER INSERT ON ... ← 创建INSERT触发器
SELECT ... FROM `sbtest`.`sbtest1` ... LIMIT ?, 2 /*next chunk boundary*/ ← 分chunk复制
bash
# 2. --execute 执行
pt-online-schema-change --alter "ADD COLUMN remark VARCHAR(50) DEFAULT '', ADD INDEX idx_remark(remark)" \
D=sbtest,t=sbtest1,h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--recursion-method=none --execute --progress time,5 --statistics
输出结果:
2026-08-16T20:03:58 Copied rows OK.
2026-08-16T20:03:58 Analyzing new table...
2026-08-16T20:03:58 Swapping tables...
2026-08-16T20:03:58 Swapped original and new tables OK.
2026-08-16T20:03:58 Dropping old table...
2026-08-16T20:03:58 Dropped old table `sbtest`.`_sbtest1_old` OK.
2026-08-16T20:03:58 Dropping triggers...
2026-08-16T20:03:58 Dropped triggers OK.
# Event Count
# ====== =====
# INSERT 3
Successfully altered `sbtest`.`sbtest1`.
验证:
bash
mysql -h192.168.195.141 -e "SHOW CREATE TABLE sbtest.sbtest1\G" | grep -E "remark|idx_remark"
# `remark` varchar(50) DEFAULT '',
# KEY `idx_remark` (`remark`)
echo -n "141行数: "; mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 90000 ← 数据完整
echo -n "142行数: "; mysql -h192.168.195.142 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 90000 ← 从库同步
4.5.4 场景二:并发写入时DDL(验证trigger同步增量)
bash
# 执行节点:141
# 1. 后台持续插入+更新
( for i in $(seq 200002 200050); do
mysql -h192.168.195.141 sbtest -e "INSERT INTO sbtest1(id,k,c,pad,remark) VALUES($i, 1, 'conc-insert', 'pad', 'CONCURRENT'); UPDATE sbtest1 SET remark='UPDATED' WHERE id=$i;"
done ) &
BG=$!
# 2. 同时执行pt-osc(删除并重建索引)
pt-online-schema-change --alter "DROP INDEX idx_remark, ADD INDEX idx_remark(remark)" \
D=sbtest,t=sbtest1,h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--recursion-method=none --execute --statistics
# Copied rows OK.
# Swapped original and new tables OK.
# Successfully altered `sbtest`.`sbtest1`.
wait $BG
# 3. 验证并发数据完整性
echo -n "UPDATED行数(应49): "; mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1 WHERE remark='UPDATED';"
# 49 ← 每行先INSERT('CONCURRENT')再UPDATE('UPDATED'),trigger正确同步增量
echo -n "总行数(应90049): "; mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM sbtest.sbtest1;"
# 90049 ← 无数据丢失
结论: pt-osc 在 DDL 过程中,通过触发器正确同步了并发写入的增量数据,数据零丢失。
4.5.5 常用参数
| 参数 | 说明 |
|---|---|
--alter |
DDL语句(不含ALTER TABLE关键字) |
--dry-run |
预检,不实际执行 |
--execute |
实际执行 |
--recursion-method=none |
跳过从库检查(级联复制环境必加) |
--max-load |
负载阈值,超过则暂停(默认Threads_running=25) |
--critical-load |
临界负载,超过则终止 |
--chunk-size |
每批复制的行数 |
--progress |
进度显示 |
--statistics |
输出统计信息 |
--alter-foreign-keys-method |
处理外键的方法 |
--no-drop-old-table |
保留旧表(默认删除) |
--no-drop-triggers |
保留触发器 |
4.5.6 pt-osc 限制
- 必须有主键或唯一索引:pt-osc 依赖主键进行分chunk复制
- 不支持外键 :除非指定
--alter-foreign-keys-method - 原表不能有触发器:pt-osc 需要自己创建触发器
- DDL过程中会有额外空间开销:新表+旧表同时存在
- RENAME TABLE 阶段会有短暂锁表:但时间极短(毫秒级)
5. 建议掌握的工具(实战)
5.1 pt-config-diff --- 参数对比
功能: 对比不同参数源之间的参数差异。
场景一:实例之间的参数对比
bash
# 执行节点:141
pt-config-diff h=192.168.195.141,P=3306,u=root,p='Root@123456' h=192.168.195.142,P=3306,u=root,p='Root@123456'
输出结果:
13 config differences
Variable MYSQL141 MYSQL142
========================= ========================= =========================
general_log_file /data/mysql/3306/data/... /data/mysql/3306/data/...
hostname MYSQL141 MYSQL142
log_queries_not_using_... ON OFF
long_query_time 1.000000 10.000000
relay_log MYSQL141-relay-bin MYSQL142-relay-bin
relay_log_basename /data/mysql/3306/data/... /data/mysql/3306/data/...
relay_log_index /data/mysql/3306/data/... /data/mysql/3306/data/...
report_host 192.168.195.141 192.168.195.142
server_id 1 2
server_uuid fb3bc7fa-995e-11f1-83f... fd495181-995e-11f1-9fc...
slow_query_log ON OFF
slow_query_log_file /data/mysql/3306/data/... /data/mysql/3306/data/...
场景二:配置文件与实例对比
bash
pt-config-diff /etc/my.cnf h=192.168.195.141,P=3306,u=root,p='Root@123456'
输出结果:
1 config difference
Variable /etc/my.cnf MYSQL141
========================= ================ ===================================
basedir /usr/local/mysql /usr/local/mysql-8.0.35-linux-gl...
说明: 配置文件中
basedir = /usr/local/mysql是软链接路径,实例运行时解析为实际路径。
场景三:配置文件之间的参数对比
bash
pt-config-diff /etc/my.cnf /etc/my_3307.cnf
5.2 pt-find --- 数据库对象查找
功能: 同 find 命令类似,只不过其查找的是数据库对象。
查看所有InnoDB引擎表
bash
# 执行节点:141
pt-find -h192.168.195.141 -uroot -P3306 -p'Root@123456' --engine InnoDB
输出结果:
`employees`.`dept_emp`
`employees`.`salaries`
`employees`.`titles`
`mysql`.`columns_priv`
`mysql`.`component`
`mysql`.`db`
...
对表按大小排序
bash
pt-find -h192.168.195.141 -uroot -P3306 -p'Root@123456' --printf "%T\t%D.%N\n" | sort -rn
输出结果:
27836416 `sbtest`.`sbtest1`
25231360 `sbtest`.`sbtest2`
13664256 `sbtest`.`sbtest3`
4767744 `employees`.`dept_emp`
1933312 `employees`.`titles`
注意: 表的大小基于
SHOW TABLE STATUS中的Data_length + Index_length。
查看自定义函数、存储过程、触发器
bash
pt-find -h192.168.195.141 -uroot -P3306 -p'Root@123456' --function '.*'
pt-find -h192.168.195.141 -uroot -P3306 -p'Root@123456' --procedure '.*'
pt-find -h192.168.195.141 -uroot -P3306 -p'Root@123456' --trigger '.*'
输出结果:
`sys`.`PROCEDURE create_synonym_db`
`sys`.`PROCEDURE diagnostics`
`sys`.`FUNCTION extract_schema_from_file_name`
`sys`.`INSERT TRIGGER sys_config_insert_set_user on sys_config`
`sys`.`UPDATE TRIGGER sys_config_update_set_user on sys_config`
5.3 pt-kill --- KILL连接
功能: kill连接。
场景一:kill超过3秒的慢查询
bash
# 执行节点:141
# 1. 模拟慢查询
mysql -h192.168.195.141 -e "SELECT SLEEP(10);" &
mysql -h192.168.195.141 -e "SELECT SLEEP(10);" &
sleep 2
# 2. 查看kill前
mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM information_schema.processlist WHERE info LIKE '%SLEEP%' AND command='Query';"
# 3
# 3. 启动pt-kill (busy-time=3, interval=2, kill+print)
timeout 6 pt-kill h=192.168.195.141,P=3306,u=root,p='Root@123456' --busy-time 3 --interval 2 --print --kill
输出结果:
# 2026-08-16T20:05:43 KILL 183 (Query 4 sec) SELECT SLEEP(10)
# 2026-08-16T20:05:45 KILL 182 (Query 6 sec) SELECT SLEEP(10)
验证:
bash
mysql -h192.168.195.141 -N -e "SELECT COUNT(*) FROM information_schema.processlist WHERE info LIKE '%SLEEP%' AND command='Query';"
# 1 ← 已被KILL(剩余1个为当前查询本身)
参数说明:
--busy-time:慢查询的阈值(秒)--interval:检测间隔,每隔10s pt-kill 会执行一次SHOW FULL PROCESSLIST
场景二:只KILL指定用户的查询
bash
# 执行节点:141
# 创建测试用户
mysql -h192.168.195.141 -e "CREATE USER IF NOT EXISTS 'testuser'@'%' IDENTIFIED BY 'Test@123'; GRANT SELECT ON *.* TO 'testuser'@'%';"
# testuser跑慢查询
mysql -utestuser -p'Test@123' -h192.168.195.141 -e "SELECT SLEEP(10);" &
sleep 2
# 只kill testuser的查询
timeout 5 pt-kill h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--busy-time 1 --interval 2 --print --kill --match-user testuser
# # 2026-08-16T20:11:53 KILL 403 (Query 2 sec) SELECT SLEEP(10)
场景三:按SQL内容过滤KILL
bash
# 只KILL以SELECT开头的查询
pt-kill h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--busy-time 1 --interval 2 --print --kill --match-info "(?i-xsm:^select)"
# # 2026-08-16T20:11:59 KILL 406 (Query 1 sec) SELECT SLEEP(8)
过滤维度
pt-kill 可从多个维度进行过滤:
| 维度 | 参数 |
|---|---|
| User | --ignore-user、--match-user |
| Host | --ignore-host、--match-host |
| db | --ignore-db、--match-db |
| Command | --ignore-command、--match-command |
| State | --ignore-state、--match-state |
| Info | --ignore-info、--match-info |
5.4 pt-mysql-summary --- MySQL实例信息采集
功能: 采集 MySQL 实例的基本信息。
bash
# 执行节点:141
pt-mysql-summary h=192.168.195.141,P=3306,u=root,p='Root@123456' --all-databases
输出内容(精简):
# Percona Toolkit MySQL Summary Report #######################
System time | 2024-01-27 12:59:13 UTC (local TZ: CST +0800)
# Instances ##################################################
Port Data Directory Nice OOM Socket
===== ========================== ==== === ======
3306 /data/mysql/3306/data 0 0 /data/mysql/3306/data/mysql.sock
# Report On Port 3306 ########################################
User | root@localhost
Version | 8.0.35 MySQL Community Server - GPL
Databases | 5
Processes | 3 connected, 4 running
Replication | Is not a slave, has 2 slaves connected
# Processlist ################################################
# InnoDB #####################################################
Buffer Pool Size | 128.0M
# Binary Logging #############################################
binlog_format | ROW
server_id | 1
# Configuration File #########################################
Config File | /etc/my.cnf
该工具采集的信息非常全面,包括:实例信息、进程列表、状态计数器、表缓存、InnoDB状态、二进制日志、配置文件等。
5.5 pt-pmp --- 进程堆栈汇总
功能: 采集进程的堆栈信息,对这些堆栈信息进行汇总。
注意: 进程的堆栈信息是利用 gdb 获取的,所以在获取的过程中,会对mysql服务端的性能有一定的影响。
bash
# 执行节点:141
pt-pmp
输出结果(精简):
2026年 08月 16日 星期日 20:05:48 CST 2024
9
syscall(libc.so.6),libaio::??(libaio.so.1),LinuxAIOHandler::collect,LinuxAIOHandler::poll,
os_aio_linux_handler,os_aio_handler,fil_aio_wait,io_handler_thread,...
8
pthread_cond_wait(libpthread.so.0),native_cond_wait,my_cond_wait,
inline_mysql_cond_wait,Per_thread_connection_handler::block_until_new_connection,
handle_connection,pfs_spawn_thread,...
3
pthread_cond_wait(libpthread.so.0),os_event::wait,os_event::wait_low,
os_event_wait,srv_worker_thread,...
解读:
- 开头的数字表示该堆栈出现的次数
9个线程在LinuxAIOHandler(异步IO等待)------ InnoDB的IO线程8个线程在handle_connection(连接处理)------ MySQL工作线程3个线程在srv_worker_thread(后台工作线程)
默认采集的是 mysqld 进程。使用较为简单,直接执行 pt-pmp 即可。
5.6 pt-show-grants --- 账号授权信息
功能: 打印账号的授权信息。
bash
# 执行节点:141
pt-show-grants h=192.168.195.141,P=3306,u=root,p='Root@123456'
输出结果:
-- Grants dumped by pt-show-grants
-- Grants for 'repl'@'%'
CREATE USER IF NOT EXISTS `repl`@`%`;
ALTER USER `repl`@`%` IDENTIFIED WITH 'mysql_native_password' AS '*6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9' ...;
GRANT REPLICATION SLAVE ON *.* TO `repl`@`%`;
-- Grants for 'testuser'@'%'
CREATE USER IF NOT EXISTS `testuser`@`%`;
GRANT SELECT ON *.* TO `testuser`@`%`;
常用选项:
bash
# --only:只打印某些用户的授权信息
pt-show-grants h=192.168.195.141,P=3306,u=root,p='Root@123456' --only testuser
# -- Grants for 'testuser'@'%'
# CREATE USER IF NOT EXISTS `testuser`@`%`;
# GRANT SELECT ON *.* TO `testuser`@`%`;
# --revoke:打印REVOKE操作
pt-show-grants h=192.168.195.141,P=3306,u=root,p='Root@123456' --only testuser --revoke
# REVOKE SELECT ON *.* FROM `testuser`@`%`;
# --drop:在CREATE USER之前打印DROP USER操作
生产用途: 可用于备份所有账号授权信息,或在新环境快速重建账号体系。
5.7 pt-slave-find --- 主从拓扑图
功能: 打印主从拓扑图。
bash
# 执行节点:141
pt-slave-find h=192.168.195.141,P=3306,u=root,p='Root@123456'
输出结果:
192.168.195.141
Version 8.0.35
Server ID 1
Uptime 01:24:57 (started 2026-08-16T18:41:07)
Replication Is not a slave, has 1 slaves connected, is not read_only
Binary logging ROW
InnoDB version 8.0.35
+- 192.168.195.142
Version 8.0.35
Server ID 2
Replication Is a slave, has 1 slaves connected, is not read_only
Slave status 0 seconds behind, running, no errors
InnoDB version 8.0.35
+- 192.168.195.143
Version 8.0.35
Server ID 3
Replication Is a slave, has 0 slaves connected, is not read_only
Slave status 0 seconds behind, running, no errors
InnoDB version 8.0.35
说明:
+-表示从库层级关系。本环境为 141→142→143 三级级联复制,拓扑图清晰展示了整个复制架构。
5.8 pt-summary --- 服务器信息采集
功能: 采集服务器的基本信息。
bash
# 执行节点:141
pt-summary
输出结果(精简):
# Percona Toolkit System Summary Report ######################
Hostname | MYSQL141
Uptime | 21 days, 4:59, 0 users, load average: 0.14, 0.12, 0.05
System | VMware, Inc.; VMware Virtual Platform; vNone (Other)
Platform | Linux
Release | Kylin Linux Advanced Server release V10 (Halberd)
Kernel | 4.19.90-89.11.v2401.ky10.x86_64
Architecture | CPU = 64-bit, OS = 64-bit
# Processor ##################################################
Processors | physical = 2, cores = 2, virtual = 2, hyperthreading = no
# Memory #####################################################
# Mounted Filesystems ########################################
该工具采集的信息包括:系统信息、CPU、内存、文件系统、磁盘、网络、进程等,是排查服务器层面问题的利器。
5.9 pt-slave-restart --- 复制错误自动跳过
功能: 监控从库的状态,如果复制因 SQL 线程重放失败而中断,则 pt-slave-restart 会跳过当前重放失败的事务,并重启复制。
⚠️ 重要限制:不支持GTID+并行复制
bash
# 执行节点:141(测试在143从库上)
pt-slave-restart h=192.168.195.143,P=3306,u=root,p='Root@123456' --error-numbers 1032
输出结果:
Cannot skip transactions properly because GTID is enabled and slave_parallel_workers > 0.
See 'GLOBAL TRANSACTION IDS' in the tool's documentation.
说明: MySQL 8.0 默认开启并行复制(
slave_parallel_workers > 0),pt-slave-restart 不支持 GTID + 并行复制环境。
GTID模式正确的错误跳过方法
bash
# 执行节点:143(从库)
# 1. 制造1032错误(从库删行后主库更新该行)
mysql -h192.168.195.143 -e "SET SESSION sql_log_bin=0; DELETE FROM sbtest.sbtest1 WHERE id=1;"
mysql -h192.168.195.141 -e "UPDATE sbtest.sbtest1 SET remark='trigger-1032' WHERE id=1;"
# 2. 查看错误(Slave_SQL_Running: No)
mysql -h192.168.195.143 -e "SHOW SLAVE STATUS\G" | grep -E "Slave_SQL_Running|Last_Error"
# Slave_SQL_Running: No
# Last_Error: ...Could not execute Update_rows event on table sbtest.sbtest1;
# Can't find record in 'sbtest1', Error_code: 1032
# 3. 获取失败的GTID
mysql -h192.168.195.143 -e "SELECT * FROM performance_schema.replication_applier_status_by_worker WHERE LAST_ERROR_NUMBER<>0\G" | grep "LAST_ERROR"
# LAST_ERROR_NUMBER: 1032
# LAST_ERROR_MESSAGE: Worker 1 failed executing transaction 'fb3bc7fa-995e-11f1-83ff-000c29048d1f:214'
# 4. 手动跳过该GTID事务
mysql -h192.168.195.143 -e "
STOP SLAVE;
SET SESSION GTID_NEXT='fb3bc7fa-995e-11f1-83ff-000c29048d1f:214';
BEGIN; COMMIT;
SET SESSION GTID_NEXT='AUTOMATIC';
START SLAVE;"
# 5. 验证复制恢复
mysql -h192.168.195.143 -e "SHOW SLAVE STATUS\G" | grep -E "Slave_IO_Running|Slave_SQL_Running|Last_Error|Seconds_Behind"
# Slave_IO_Running: Yes
# Slave_SQL_Running: Yes
# Last_Error:
# Seconds_Behind_Master: 0
常用参数
| 参数 | 说明 |
|---|---|
--daemonize |
后台运行,配合 --log 记录日志 |
--error-numbers |
跳过指定错误码(如1062主键冲突、1032记录不存在) |
--error-text |
跳过指定错误文本 |
--skip-count |
跳过的事务数量,默认为1 |
--recurse |
递归层级,默认0只监控给定节点 |
--recursion-method |
递归方法 |
pt-slave-restart 适用场景
- 适用于非GTID 或非并行复制的旧版MySQL环境
- MySQL 8.0 GTID + 并行复制环境下,需用
SET GTID_NEXT手动跳过
5.10 pt-stalk --- 问题现场信息采集
功能: 在问题出现时,实时采集主机及数据库的相关信息。
基本原理
--function=status --variable=Threads_running --threshold=25:定义采集的触发条件,默认是 Threads_running 大于25--cycles=5 --interval=1:连续检测5次,每次检测间隔1s,如果5次结果都为True,才会触发实际采集--run-time=30:采集时长--sleep=300:采集一次后,sleep 的时间--dest:采集数据的存储目录
场景一:--no-stalk 直接采集
bash
# 执行节点:141
# 确保 mysql/mysqladmin 在 PATH
ln -sf /usr/local/mysql/bin/mysql /usr/local/bin/mysql
ln -sf /usr/local/mysql/bin/mysqladmin /usr/local/bin/mysqladmin
mkdir -p /var/lib/pt-stalk
# --no-stalk 模式:不等待触发条件,直接采集
pt-stalk --no-stalk --run-time=5 --dest=/var/lib/pt-stalk --iterations=1 \
-- --user=root --password='Root@123456' --host=127.0.0.1 --port=3306
踩坑提示: pt-stalk 的 MySQL 连接参数需要通过
--分隔符传递,否则无法连接。这是因为 pt-stalk 会把--后面的参数传递给其调用的 mysql/mysqladmin 命令。
输出结果:
2026_08_16_20_08_35 Starting /usr/bin/pt-stalk ...
2026_08_16_20_08_35 Collect 1 triggered
2026_08_16_20_08_35 Collect 1 PID 269457
2026_08_16_20_08_35 Collect 1 done
采集内容
bash
# 采集的文件分类(37个文件)
ls /var/lib/pt-stalk/ | sed 's/.*-//' | sort | uniq -c | sort -rn
3 overall ← 整体概览
1 vmstat ← 虚拟内存统计
1 variables ← MySQL变量
1 trigger ← 触发条件记录
1 transactions ← 事务信息
1 top ← 系统top
1 status ← MySQL状态
1 processlist ← MySQL进程列表
1 opentables ← 打开的表
1 mutex-status ← 互斥锁状态
1 mysqladmin ← mysqladmin输出
1 meminfo ← 内存信息
1 lsof ← 打开的文件
1 log_error ← 错误日志
1 iostat ← IO统计
1 innodbstatus ← InnoDB状态(2次快照)
1 hostname ← 主机名
1 diskstats ← 磁盘统计
1 df ← 磁盘空间
...
innodbstatus 样本:
bash
head -8 /var/lib/pt-stalk/*-innodbstatus1
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2026-08-16 20:08:36 139901920147200 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 28 seconds
trigger 文件(记录触发条件):
bash
cat /var/lib/pt-stalk/*-trigger
2026_08_16_20_08_35 Not stalking; collect triggered immediately
2026_08_16_20_08_35 pt-stalk ran with --function=status --variable=Threads_running ...
场景二:条件触发模式
bash
# 制造高并发(Threads_running > 5)
( for i in $(seq 1 15); do mysql -e "SELECT SLEEP(8);" & done )
# 启动pt-stalk 监控 Threads_running > 5
pt-stalk --function=status --variable=Threads_running --threshold=5 --cycles=2 --interval=1 \
--run-time=5 --dest=/var/lib/pt-stalk \
-- --user=root --password='Root@123456' --host=127.0.0.1 --port=3306
常用参数
| 参数 | 默认值 | 说明 |
|---|---|---|
--function |
status | 触发函数 |
--variable |
Threads_running | 监控变量 |
--threshold |
25 | 触发阈值 |
--cycles |
5 | 连续满足次数 |
--interval |
1 | 检测间隔(秒) |
--run-time |
30 | 采集时长(秒) |
--sleep |
300 | 采集后sleep时间(秒) |
--dest |
/var/lib/pt-stalk | 存储目录 |
--no-stalk |
- | 不等待触发,直接采集 |
--mysql-only |
FALSE | 只采集MySQL相关内容 |
--retention-time |
30 | 保留天数 |
--collect-gdb |
FALSE | 是否采集gdb数据(影响性能) |
--collect-strace |
FALSE | 是否采集strace数据 |
--collect-tcpdump |
FALSE | 是否采集tcpdump数据 |
注意: gdb和strace对数据库性能影响比较大,一般不建议开启。
5.11 pt-upgrade --- 版本兼容性检测
功能: 给定一个 SQL,分别在两个不同版本的实例上执行,看看是否一致。
检测项
- Row count:查询返回的行数是否一致
- Row data:查询的结果是否一致
- Warnings:是否提示 warning
- Query time:查询时间是否在同一量级
- Query errors:查询如果在其中一个实例中出现语法错误,会提示 Query errors
- SQL errors:查询如果在两个实例中同时出现语法错误,会提示 SQL errors
场景一:直接对比两个实例
bash
# 执行节点:141
# 准备测试SQL
cat > /tmp/pt_upgrade_test.sql << 'EOF'
SELECT * FROM employees.dept_emp GROUP BY dept_no;
SELECT title, ROUND(AVG(salary),2) FROM titles t JOIN salaries s ON t.emp_no=s.emp_no GROUP BY title;
EOF
# 在141和142上执行对比
pt-upgrade h=192.168.195.141,P=3306,u=root,p='Root@123456' h=192.168.195.142,P=3306,u=root,p='Root@123456' \
--type rawlog /tmp/pt_upgrade_test.sql
输出结果:
#-----------------------------------------------------------------------
# Hosts
#-----------------------------------------------------------------------
host1:
DSN: h=192.168.195.141,P=3306
hostname: MYSQL141
MySQL: MySQL Community Server - GPL 8.0.35
host2:
DSN: h=192.168.195.142,P=3306
hostname: MYSQL142
MySQL: MySQL Community Server - GPL 8.0.35
########################################################################
# Query class 296E46FE3AEE9B6C
########################################################################
Reporting class because it has SQL errors, but hasn't been reported yet.
## SQL errors: 1
On both hosts:
Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column
'employees.dept_emp.emp_no' which is not functionally dependent on columns in GROUP BY clause;
this is incompatible with sql_mode=only_full_group_by
场景二:基准结果模式
bash
# 1. 先生成基准测试结果
pt-upgrade h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--save-results /tmp/pt_upgrade_result --type rawlog /tmp/pt_upgrade_test.sql
# 2. 基于基准结果测试其他环境
pt-upgrade /tmp/pt_upgrade_result/ h=192.168.195.142,P=3306,u=root,p='Root@123456'
常用参数
| 参数 | 说明 |
|---|---|
--type |
文件类型:slowlog/genlog/binlog/rawlog/tcpdump,默认slowlog |
--[no]read-only |
默认只执行SELECT和SET,需执行其它操作指定 --no-read-only |
--save-results |
保存基准结果到目录 |
--[no]create-upgrade-table |
是否创建 percona_schema.pt_upgrade 表 |
注意事项: pt-upgrade 更适合在测试环境或开发环境使用,不建议在生产环境上使用。
6. 常见故障模拟及解决
6.1 pt-table-checksum DIFFS永远为0
故障现象: 从库数据与主库不一致,但 pt-table-checksum 的 DIFFS 列永远显示0。
根因: MySQL 8.0 默认 binlog_format=ROW,checksum 语句在级联复制时被转为ROW事件,从库直接应用主库预计算的CRC值,不重新计算。
解决: 加 --set-vars binlog_format=STATEMENT 参数。
bash
pt-table-checksum h=192.168.195.141,P=3306,u=root,p='Root@123456' \
--no-check-binlog-format --recursion-method=hosts --databases=sbtest \
--set-vars binlog_format=STATEMENT
6.2 pt-online-schema-change 报错 "uninitialized value in string eq"
故障现象:
Error while waiting for replica lag: Use of uninitialized value in string eq at /usr/bin/pt-online-schema-change line 4357.
根因: 级联复制环境下,pt-osc 默认尝试探测从库延迟失败。
解决: 加 --recursion-method=none 跳过从库检查。
bash
pt-online-schema-change --alter "..." D=sbtest,t=sbtest1,h=...,P=3306,u=root,p='...' \
--recursion-method=none --execute
6.3 pt-archiver --bulk-insert 报错或0行
故障现象: --bulk-insert 模式下归档端无数据。
根因: --bulk-insert 使用 LOAD DATA LOCAL INFILE,目标库未开启 local_infile。
解决:
sql
SET GLOBAL local_infile=ON;
SELECT @@local_infile; -- 确认=1
6.4 pt-stalk 报错 "Cannot execute mysql"
故障现象:
/usr/bin/pt-stalk:行1780: mysql:未找到命令
Cannot execute mysql. Check that it is in PATH.
根因: mysql/mysqladmin 不在 PATH 中。
解决:
bash
ln -sf /usr/local/mysql/bin/mysql /usr/local/bin/mysql
ln -sf /usr/local/mysql/bin/mysqladmin /usr/local/bin/mysqladmin
ln -sf /usr/local/mysql/bin/mysqldump /usr/local/bin/mysqldump
6.5 pt-stalk 报错 "Access denied"
故障现象:
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)
Cannot connect to MySQL.
根因: pt-stalk 默认不带密码连接。
解决: 通过 -- 分隔符传递MySQL连接参数:
bash
pt-stalk --no-stalk --run-time=5 --dest=/var/lib/pt-stalk \
-- --user=root --password='Root@123456' --host=127.0.0.1 --port=3306
6.6 pt-slave-restart 不支持GTID+并行复制
故障现象:
Cannot skip transactions properly because GTID is enabled and slave_parallel_workers > 0.
根因: pt-slave-restart 不支持 GTID + 并行复制环境。
解决: 用 SET GTID_NEXT 手动跳过:
sql
STOP SLAVE;
SET GTID_NEXT='UUID:N'; -- 失败的GTID
BEGIN; COMMIT;
SET GTID_NEXT='AUTOMATIC';
START SLAVE;
6.7 pt-archiver --columns 报错 "columns exist in --source but not --dest"
故障现象:
The following columns exist in --source but not --dest: id
根因: 目标表缺少源表的主键列。
解决: 目标表必须包含源表的所有列名(含主键),即使 --columns 不归档该列:
sql
CREATE TABLE arch.sbtest2 (id INT, k INT, c CHAR(120), pad CHAR(60)); -- 必须有id列
6.8 从库复制错误 1032(记录不存在)
故障场景: 从库误删数据,主库更新该行时从库回放失败。
诊断:
sql
SELECT LAST_ERROR_NUMBER, LAST_ERROR_MESSAGE, LAST_ERROR_TRANSACTION
FROM performance_schema.replication_applier_status_by_worker
WHERE LAST_ERROR_NUMBER <> 0;
修复:
sql
STOP SLAVE;
SET GTID_NEXT='失败的GTID';
BEGIN; COMMIT;
SET GTID_NEXT='AUTOMATIC';
START SLAVE;
然后使用 pt-table-sync 修复数据一致性。
7. 知识点补充与生产场景
7.1 pt-archiver 生产归档最佳实践
推荐流程:
1. --no-delete 先归档不删源(数据安全)
2. pt-table-sync --print 比对源库和归档库一致性
3. 确认一致后 --purge 删除源库数据
7.2 pt-table-checksum + pt-table-sync 联合使用流程
生产数据一致性巡检流程:
1. pt-table-checksum(加 --set-vars binlog_format=STATEMENT)校验主从一致性
2. 检查 percona.checksums 表中 DIFFS 不为0的表
3. pt-table-sync --print 打印修复SQL(人工审核)
4. pt-table-sync --execute 执行修复
5. 再次 pt-table-checksum 确认修复成功
7.3 pt-online-schema-change 生产使用要点
- 先 --dry-run 预检:确认SQL和执行计划正确
- 加 --recursion-method=none:级联复制环境必加
- 设置 --max-load :控制复制期间负载(如
Threads_running=50) - 预留磁盘空间:新表+旧表同时存在,需要约1倍表大小
- 低峰期执行:虽然不锁表,但数据复制仍有IO压力
- 验证数据完整性:执行后比对行数
7.4 pt-query-digest 生产分析流程
1. 确认慢日志已开启:slow_query_log=ON, long_query_time=1
2. 收集一段时间的慢日志(如24小时)
3. pt-query-digest 分析慢日志
4. 重点关注 Rank 前5的SQL
5. 分析每条SQL的 Rows examine vs Rows sent(扫描行数远大于返回行数说明需要优化)
6. 针对性优化:加索引、改写SQL、调整参数
7.5 pt-kill 生产配置建议
bash
# 生产环境推荐配置:守护进程模式
pt-kill h=...,P=3306,u=root,p='...' \
--busy-time 60 \
--interval 10 \
--kill \
--print \
--log /var/log/pt-kill.log \
--daemonize \
--pid /var/run/pt-kill.pid
# 只kill SELECT查询,不影响DML
pt-kill ... --busy-time 60 --match-info "(?i-xsm:^select)" --kill --print
7.6 pt-stalk 生产使用建议
bash
# 生产环境推荐:监控Threads_running,触发后采集30秒
pt-stalk \
--function=status --variable=Threads_running --threshold=50 \
--cycles=3 --interval=1 \
--run-time=30 --sleep=300 \
--dest=/var/lib/pt-stalk \
--retention-time=7 \
-- --user=root --password='...' --host=127.0.0.1 --port=3306
# 写入systemd服务实现持续监控
7.7 CRC32 校验原理深入
pt-table-checksum 使用 CRC32 函数计算每个chunk的校验值:
sql
-- 单个chunk的校验SQL
REPLACE INTO percona.checksums (...) SELECT ...,
COUNT(*) AS cnt, -- 行数
COALESCE(LOWER(CONV(BIT_XOR(CAST(CRC32(
CONCAT_WS('#', `id`, `k`, `c`, `pad`) -- 将各行拼接后CRC32
) AS UNSIGNED)), 10, 16)), 0) AS crc -- BIT_XOR聚合,转16进制
FROM sbtest.sbtest1 WHERE id BETWEEN 1 AND 1000
关键点:
CONCAT_WS('#', ...):将每行的各列拼接成字符串CRC32(...):计算每行的CRC32值BIT_XOR(...):对所有行的CRC32值做异或运算(顺序无关)CONV(..., 10, 16):将十进制转为十六进制
为什么用BIT_XOR? 因为异或运算满足交换律和结合律,行的顺序不影响最终结果。主库和从库只要数据内容一致,即使行物理顺序不同,CRC值也相同。
7.8 pt-table-sync 修复方向
| 模式 | DSN指向 | 修复位置 | 适用场景 |
|---|---|---|---|
--sync-to-master |
slave | 主库 | 修复单个从库 |
--replicate |
master | 主库 | 修复所有从库 |
| 独立模式 | 多个独立DSN | 第一个DSN | 无主从关系的实例间同步 |
关键: 修复操作都在主库执行,通过复制同步到从库。这样保证所有从库最终一致。
8. 工具速查表
必须掌握
| 工具 | 核心命令 |
|---|---|
| pt-archiver | pt-archiver --source DSN --dest DSN --where "..." --bulk-delete --limit 1000 --commit-each |
| pt-query-digest | pt-query-digest /path/to/slow.log |
| pt-table-checksum | pt-table-checksum h=...,P=3306,u=root,p=... --no-check-binlog-format --set-vars binlog_format=STATEMENT |
| pt-table-sync | pt-table-sync --execute --sync-to-master h=slave,... 或 pt-table-sync --execute --replicate percona.checksums h=master,... |
| pt-online-schema-change | pt-online-schema-change --alter "..." D=db,t=tbl,h=... --recursion-method=none --execute |
建议掌握
| 工具 | 核心命令 |
|---|---|
| pt-config-diff | pt-config-diff DSN1 DSN2 |
| pt-find | pt-find -h... --engine InnoDB |
| pt-kill | pt-kill h=... --busy-time 30 --interval 10 --print --kill |
| pt-mysql-summary | pt-mysql-summary h=... --all-databases |
| pt-pmp | pt-pmp |
| pt-show-grants | pt-show-grants h=... |
| pt-slave-find | pt-slave-find h=... |
| pt-summary | pt-summary |
| pt-slave-restart | pt-slave-restart h=... --error-numbers 1032 |
| pt-stalk | pt-stalk --no-stalk --run-time=5 -- --user=... --password=... |
| pt-upgrade | pt-upgrade DSN1 DSN2 --type rawlog /path/to/test.sql |
附录:测试环境信息
| 节点 | IP | 角色 | server-id | MySQL版本 |
|---|---|---|---|---|
| 141 | 192.168.195.141 | Master | 1 | 8.0.35 |
| 142 | 192.168.195.142 | Slave(141的从库) | 2 | 8.0.35 |
| 143 | 192.168.195.143 | Slave(142的从库,级联) | 3 | 8.0.35 |
连接信息:
bash
# 主库
/usr/local/mysql/bin/mysql -uroot -h192.168.195.141 -p'Root@123456'
# 从库
/usr/local/mysql/bin/mysql -uroot -h192.168.195.142 -p'Root@123456'
/usr/local/mysql/bin/mysql -uroot -h192.168.195.143 -p'Root@123456'
Percona Toolkit 版本: 3.5.7(RPM包安装)
测试数据库:
sbtest:sbtest1(90000行)、sbtest2(100000行)、sbtest3(50000行)employees:dept_emp(20000行)、salaries(20000行)、titles(20000行)arch:归档目标库
本笔记所有场景均在 MySQL 8.0.35 GTID 级联主从环境中实测验证通过。
环境保留可继续实验。