一、开篇暴击:你的数据库正在经历这些痛苦吗?
- 存储成本飙升:订单表3年增长到800GB,云盘费用月增2000+
- 查询卡成PPT:用户历史数据检索响应时间突破15秒
- 备份恢复噩梦:全库备份耗时从20分钟延长到2小时
- 开发效率骤降:WHERE create_time<"2020年" 的查询拖垮整个实例
真实案例:某电商平台清理1.2亿日志数据,传统DELETE方式导致:
主从延迟12小时锁等待超时237次磁盘空间未释放
二、核弹级解决方案:pt-archiver工作原理揭秘
2.1 与传统方式的降维对比
DELETE语句 | pt-archiver | |
执行方式 | 全事务提交 | 分批提交 (可配置) |
锁机制 | 行锁升级为表锁 | 最小化锁定范围 |
磁盘空间 | 不会立即释放 | 立即释放 |
执行耗时 | 随数据量线性增长 | 恒定速率 |
风险指数 | ????? | ? |
2.2 三维透视工作原理
三、手把手实战教学(关键操作配截图)
3.1 安装篇:2分钟极速部署
# 适用于CentOS的「一行流」安装
sudo yum install percona-toolkit -y && which pt-archiver
3.2 基础篇:清理3年前订单数据
pt-archiver \
--source h=localhost,D=test,t=orders,u=root,p=123456 \
--where "create_time < DATE_SUB(NOW(), INTERVAL 3 YEAR)" \
--purge \
--limit 1000 \
--commit-each
参数解析表
参数名 | 作用说明 | 推荐值/示例 |
连接配置类 | ||
--source | 指定源数据库连接(格式:h=主机,D=库名,t=表名,u=用户,p=密码) | h=192.168.1.2,D=order,t=logs |
--dest | 目标数据库连接(用于数据归档) | h=归档服务器IP,D=archive,t=logs |
数据过滤类 | ||
--where | 指定归档条件(需用引号包裹) | "create_time < '2022-01-01'" |
--limit | 每批次处理行数(影响锁持有时间) | 500-5000(根据内存调整) |
--progress | 进度报告间隔行数 | 5000 |
执行控制类 | ||
--purge | 直接删除数据不归档 | 清理日志表时使用 |
--no-delete | 仅复制数据不删除(用于数据迁移) | 数据迁移时启用 |
--txn-size | 事务提交间隔(每N行提交事务) | 1000(与limit值一致) |
性能优化类 | ||
--bulk-delete | 启用批量删除(提升删除效率) | 总行数>10万时建议启用 |
--bulk-insert | 启用批量插入(提升归档速度) | 配合--dest使用 |
--sleep | 批次间隔休眠时间(秒) | 0.1-1(高负载时增加) |
输出统计类 | ||
--statistics | 输出执行统计信息 | 必启用 |
--why-quit | 显示退出原因(调试用) | 异常终止时使用 |
其他实用类 | ||
--charset | 指定连接字符集 | utf8mb4 |
--no-check-charset | 跳过字符集验证(需确认兼容性) | 谨慎使用 |
--dry-run | 试运行模式(不实际执行操作) | 必先执行验证 |
参数调优指南:
- 高并发场景:--limit 500 --sleep 0.3
- 快速归档模式:--limit 5000 --bulk-insert --bulk-delete
- 敏感数据操作:--dry-run --why-quit
注意事项:
--limit值越大,单次事务时间越长,可能引发锁等待--sleep参数可有效降低主库压力,但会延长总执行时间务必先用--dry-run验证WHERE条件准确性
3.3 进阶篇:跨服务器归档
pt-archiver \
--source h=主库IP,D=生产库,t=订单表,u=archiver,p=密码 \
--dest h=归档库IP,D=历史库,t=订单表 \
--where "created_at < DATE_SUB(NOW(), INTERVAL 3 YEAR)" \
--limit 2000 \
--progress 5000 \
--bulk-delete \
--bulk-insert \
--statistics \
--txn-size 2000 \
--sleep 0.5 \
--no-check-charset
四、避坑指南:血泪经验总结
4.1 必查清单(执行前逐项确认)
- 已添加--dry-run参数试运行
- 目标表结构校验完成
- 从库延迟监控已开启
- 磁盘空间检查(至少保留20%)
- 业务低峰期窗口确认
4.2 高频报错解决方案
# 报错1:表结构不匹配
Error: Column count doesn't match
修复方案:添加--columns参数指定字段
# 报错2:主键缺失
No PRIMARY or UNIQUE index found
修复方案:临时添加索引
ALTER TABLE orders ADD INDEX idx_ctime(create_time);
# 报错3:外键约束
Cannot delete rows due to FOREIGN KEY
修复方案:按依赖顺序归档或SET foreign_key_checks=0
五、性能核弹:百万级数据实战报告
5.1 测试环境
- 机器配置:4C8G SSD云主机
- MySQL版本:8.0.28
- 数据量:`orders`表350万条(未分区)
5.2 性能对比数据
指标 | 传统DELETE | pt-archiver |
总耗时 | 6小时22分 | 11分钟 |
主库QPS波动 | 下降63% | 波动<5% |
磁盘空间释放 | 12小时后 | 立即释放 |
执行期间锁等待 | 89次 | 0次 |
六、专家私藏配置
结合Java代码以及shell脚本,是归档设置更加灵活
shell脚本
#!/bin/sh
set -e
#表结构列表
#echo $1
tableList=$1
sParams=$2
dParams=$3
oParams=$4
whereFrom=$5
whereTo=$6
operate=$7
IFS=";"
#历史数据处理
function doHistoryData() {
for tableName in $tableList
do
commandStr="pt-archiver --source ${tableName} --dest ${tableName} ${whereFrom} --no-delete $oParams"
echo "pt-archiver --source ${sParams}${tableName} --dest ${dParams}${tableName} ${whereFrom} --no-delete $oParams" | sh
echo ${commandStr}
commandStr="pt-archiver --source ${tableName} --dest ${tableName} ${whereTo} --bulk-delete $oParams"
echo "pt-archiver --source ${sParams}${tableName} --dest ${dParams}${tableName} ${whereTo} --bulk-delete $oParams" | sh
echo ${commandStr}
echo "succ"
done
}
# 参数检查
if [ -z ${operate} ]; then
echo "operate can not be null."
else
# 启动程序
if [ ${operate} == "doHistory" ]; then
doHistoryData
else
echo "Not supported the operate."
fi
fiJava代码
private Pair startBackUpSh(StringBuilder sBuilder, String tableName, String where, String filePath, String toFileName, String zipFileName, StringBuilder oBuilder, String method) throws IOException {
String command = System.getProperty("user.dir") + "/bin/BackUp.sh";
CommandLine cmdl = new CommandLine(command);
cmdl.addArgument(sBuilder.toString(), false);
cmdl.addArgument(tableName, false);
cmdl.addArgument(where, false);
cmdl.addArgument(filePath, false);
cmdl.addArgument(toFileName, false);
cmdl.addArgument(zipFileName, false);
cmdl.addArgument(oBuilder.toString(), false);
cmdl.addArgument(method, false);
DefaultExecutor executor = new DefaultExecutor();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
executor.setStreamHandler(new PumpStreamHandler(baos));
int execute = executor.execute(cmdl);
RUN_LOGGER.debug("execute result = {}", execute);
final String result = baos.toString().trim();
RUN_LOGGER.debug("execute result = {}", result);
if (StrUtil.containsIgnoreCase(result, "succ") || StrUtil.containsIgnoreCase(result, "no more rows")) {
return new Pair<>(true, result);
} else {
return new Pair<>(false, result);
}
} 