一、开篇暴击:你的数据库正在经历这些痛苦吗?
- 存储成本飙升:订单表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
fi
Java代码
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);
}
}