先交代一句:这篇东西不是纸上谈兵,是我在好几套生产环境里真刀真枪用出来的。如果你手头有张 MySQL 表,已经跑到几亿行,每次 DELETE 一下历史数据就锁表、慢查询报警、主从延迟飙升,那你大概率需要认识一下 pt-archiver 这个工具。它是 Percona Toolkit 里的老牌成员,专门解决“在线清理和归档 MySQL 大表数据”这个痛点。写这篇文章,是想把 pt-archiver 从安装、参数理解、实战命令到自动化调度一次讲透,给正在被大表膨胀折磨的 DBA、后端开发、运维朋友一条能直接照做的路子。
我最早接触这个工具也是被逼的:一张订单流水表 3 亿多行,磁盘快满了,业务还不能停,用传统 DELETE 删数据删到晚上,主库直接扛不住。后来用 pt-archiver 做分批归档加删除,才把数据库从生死线上拉回来。下面所有内容,都是围绕“怎么安全地把大表里的历史数据挪走”这个核心展开的,你不需要有很深的MySQL基础,只要会跑命令行,按顺序看完就能上手。
1. 为什么清理大表数据非它不可
1.1 传统 DELETE 在大表场景下的致命伤
很多人的第一反应是:清理数据不就是 DELETE FROM table WHERE create_time < '2022-01-01' 吗?这句话在几百万行的小表上没错,但到了几亿行的表上,就是事故现场。
普通 DELETE 在执行时,InnoDB 要把满足条件的行逐条标记删除,同时记录到 undo log 和 redo log,而且这整个过程是在一个大事务里完成的。也就是说,在语句执行期间,这几十万、几百万行数据一直被锁住,其他业务的 SELECT、UPDATE 只能排队等。你还会看到另一个问题:主库上一个大 DELETE 运行 20 分钟,从库为了追主库的 binlog,要连续执行 20 分钟的单线程回放,主从延迟瞬间拉到几分钟甚至更久。
还有个容易被忽略的坑:按 create_time 这种普通索引条件去 DELETE,没有主键顺序的约束,InnoDB 删除的时候是随机扫描二级索引再回表,每删一批 IO 都很差,删到一半系统负载就上去了。更别提大事务回滚时的灾难——如果 DELETE 没执行完被你手动 kill,MySQL 需要回滚整个事务,这个回滚比删除还慢,等于数据库当场“罢工”。我在生产环境见过一次误操作,一个 delete 共影响 2000 万行,跑了 25 分钟后发现条件写错想取消,回滚用了将近 1 小时,这期间那台主库的其他查询全部超时。
1.2 pt-archiver 的核心思路:小事务、分批、可暂停
pt-archiver 解决这个问题的思路特别清晰。它不会一次性锁住所有满足条件的行,而是顺着表的主键或者唯一索引,用高效的范围查询一次只取一小批数据,比如默认一次 400 行,处理完这一批就提交一个事务。然后继续往下跑,直到把所有满足条件的行都处理完。
这样做有几个实打实的好处:
- 锁粒度极小。每个事务只涉及几百行,别的事务在该表上的插入、更新基本不受影响,只有在处理到特定主键范围时会短时间持有行锁。
- 主从延迟可控。每处理一批之后可以 sleep 若干秒,给从库留出追 binlog 的时间,让延迟保持在一个安全水位。
- 可以随时中断。因为是分批提交,你随时 Ctrl+C 杀掉进程,已经提交的事务会保留,没跑的不会产生大回滚,下次再跑还能接着来。
我经常打一个比方:传统 DELETE 像是用挖掘机在小区里拆楼,轰隆一下全砸了,动静大还震坏周边的邻居;而 pt-archiver 像一支装修队,按楼层一间一间拆,拆完一间运走一间,虽然工期长一点,但全楼的人照常生活。这个特质,决定了它天生就是为“线上大表清理”这个场景设计的。
另外它还自带一个很关键的“归档”能力,不光是删,还能在删除的同时把数据写到文件、或者插入到另一张历史表。这样你的数据不会真的消失,只是从“热表”挪到了“冷库”,审计、数据分析、合规留档的需求全都照顾到了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 安装与参数体系,动手前的必修课
2.1 Percona Toolkit 的安装和验证
pt-archiver 是 Percona Toolkit 中的一个工具,不是 MySQL 自带的,所以先要装这个工具包。不同操作系统安装方式略有差异,我常用的两种:
Debian/Ubuntu 系:
bash复制wget https://repo.percona.com/apt/percona-release_latest.$(lsb_release -sc)_all.deb
dpkg -i percona-release_latest.$(lsb_release -sc)_all.deb
apt-get update
apt-get install percona-toolkit
CentOS/RHEL 系:
bash复制yum install https://repo.percona.com/yum/percona-release-latest.noarch.rpm
yum install percona-toolkit
macOS 上做开发测试的话可以直接 brew install percona-toolkit。装完之后用 pt-archiver --version 验证一下,能看到类似 pt-archiver 3.5.5 的输出就说明环境就绪。
这里插一句,Percona Toolkit 是一整套 MySQL 运维工具集,除了 pt-archiver,还有常用的 pt-query-digest、pt-online-schema-change、pt-table-checksum 等等。你只装一个包,等于这些工具都备齐了,后面做数据库巡检、大表 DDL 变更都用得上。
2.2 核心参数速查,每个参数都别乱用
pt-archiver 的参数非常多,但日常归档根本用不着全部,记住下面这些就够了。我按功能分了几组,方便你对照排查。
表连接与认证:
- --host、--port、--user、--password:连接数据库的基础信息。注意密码不要直接写在命令行里,可以用 --ask-pass 手动输入,或者写到 ~/.my.cnf,避免密码泄露到 shell history。
- --socket:走本地 socket 连接时使用,效率更高。
核心归档条件:
- --source:必填,指定要操作的源表,格式是 D=库名,t=表名。
- --dest:选填,指定归档目标表,格式一样。如果不指定,就只把数据导出到文件或者直接删除。
- --where:必填,归档条件,相当于 SELECT 的 WHERE 子句,决定你要把哪些行挪走。这个条件非常关键,必须能用到索引,否则性能会打折扣。
- --limit:每个批次取多少行,默认是 400。调大这个值可以提高吞吐,但会增加单批锁范围,生产环境建议 500~2000 之间做取舍。
- --txn-size:每个事务处理多少行,默认是 1。注意这个参数和 --limit 是配合的,也就是说 --txn-size 决定了每个事务内包含的批次数。如果 --limit 100 --txn-size 10,那就是每个事务 1000 行。我通常直接设置 --txn-size 1000,--limit 1000,语义清晰。
- --sleep:处理完一批后休眠多少秒,单位是秒,支持小数。这是控制从库延迟和降低主库压力的关键旋钮。
输出与处理方式:
- --file:把归档的数据写入文件,格式类似 --file '/data/archive/%Y-%m-%d-orders.txt',支持日期变量。如果不加 --file 也没加 --dest,那数据就是纯删除。
- --purge:只删除源表数据,不归档。这个参数要和 --dest、--file 二选一使用。
- --output-format:配合 --file 使用,可以指定 csv、tab 等格式。默认是 tab 分隔。
- --header:配合 --file 使用,导出时把列名作为首行写入。
批量与加速:
- --bulk-delete:把每批删除操作改成一条多值 DELETE,语句形如 DELETE FROM t WHERE id IN (...),能显著降低 SQL 执行次数,但对锁和从库压力会增大。
- --bulk-insert:类似于 --bulk-delete,把插入目标表的操作合并成一条 INSERT 多值语句。
- --bulk-size:配合 bulk 系列使用,指定每条批量 SQL 包含多少行。
防从库延迟与自我保护:
- --max-lag:当从库延迟超过这个秒数时,pt-archiver 会暂停等待。比如 --max-lag 5,就是延迟大于 5 秒时先休息。
- --check-slave-lag:指定检查哪个从库的延迟,不写的话会自动找。如果不指定,默认只检查所有能找到的从库。
- --max-flow-control:只针对 MySQL 5.6+ 的组复制/流控场景,一般用不到。
辅助性参数:
- --dry-run:只打印要执行的 SQL,不实际执行,强烈建议每次跑之前先试一遍。
- --statistics:结束的时候打印详细的统计信息,包括扫描行数、删除行数、耗时、每秒处理行数等,用来评估性能。
- --analyze:处理完之后对源表执行 ANALYZE TABLE,更新统计信息。
- --optimize:处理完之后执行 OPTIMIZE TABLE,会重建表,适合归档量很大、表碎片严重的场景,但要在业务低峰用。
- --charset:指定连接字符集,避免中文乱码。
参数看着多,其实多数命令只需要组合其中五六个。下面给个速查表,后面实战会逐个演示。
| 参数 | 作用 | 我的建议 |
|---|---|---|
| --source | 指定源表 | 必填,D=库,t=表 |
| --where | 筛选归档数据 | 必填,必须走索引 |
| --dest | 指定归档目标表 | 归档到表时必填 |
| --file | 归档到文件 | 导文件时必填 |
| --purge | 只删不导 | 与 dest/file 互斥 |
| --limit | 每批取数行数 | 500~2000 |
| --txn-size | 事务总行数 | 1000 左右 |
| --sleep | 批间暂停秒数 | 0.1~2 |
| --max-lag | 从库延迟上限 | 5~10 |
| --bulk-delete | 批量删除加速 | 低峰期可以开 |
| --dry-run | 演练模式 | 上线前必须 |
| --statistics | 输出统计 | 建议开启 |
3. 从零开始的三类典型归档场景
我把话放这儿:不管你的归档需求多复杂,基本都是下面三种场景的组合。场景一是归档到文件,适合彻底清理历史数据并做离线备份;场景二是归档到另一个库的历史表,适合保留在线查询能力;场景三是只清理不保留,适合日志数据直接丢弃。我们把每一种都完整跑一遍。
实际操作前,我先建一张模拟用的订单表,这是生产环境最常见的数据类型,SQL 如下:
sql复制CREATE DATABASE IF NOT EXISTS test_archive;
USE test_archive;
CREATE TABLE orders (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_create_time (create_time)
) ENGINE=InnoDB;
注意 create_time 上有索引,这个非常关键,后面细说。如果你手头没有真实大表,可以通过一条递归 CTE 往表里灌几百万测试数据。有了表,我们开始。
3.1 场景一:归档到文件,并删除源表数据
需求:把 orders 表中 2022 年之前的订单导出到 /data/archive 目录下的 CSV 文件,然后从源表删除。
正式跑之前,务必先做一次 dry-run,别嫌麻烦,这一步能帮你发现权限、连接、SQL 语法等各种问题,而且不会产生任何实际变更。
bash复制pt-archiver \
--source D=test_archive,t=orders \
--where "create_time < '2022-01-01'" \
--file '/data/archive/orders-archive-%Y-%m-%d.txt' \
--output-format csv \
--header \
--dry-run
正常你会看到一堆 SELECT 语句从源表里抓取数据,而不会有 DELETE 输出。确认没问题,把 --dry-run 去掉,加上 --limit 和 --txn-size,正式执行:
bash复制pt-archiver \
--source D=test_archive,t=orders \
--where "create_time < '2022-01-01'" \
--file '/data/archive/orders-archive-%Y-%m-%d.txt' \
--output-format csv \
--header \
--limit 1000 \
--txn-size 1000 \
--sleep 0.5 \
--max-lag 5 \
--statistics
执行期间,你可以另开一个终端看 MySQL 的 processlist,会看到 pt-archiver 一直在发起类似这样的查询:
sql复制SELECT id, order_no, user_id, amount, status, create_time FROM test_archive.orders FORCE INDEX(`PRIMARY`) WHERE (create_time < '2022-01-01') AND (id < 某个值) ORDER BY id LIMIT 1000;
DELETE FROM test_archive.orders WHERE id IN (1001, 1002, ..., 2000);
这个查询模式很有特征:每次都是记住上一批的最大主键,然后往后取下一批,而不是从头开始扫。这意味着它不会重复扫描旧数据,效率随时间保持稳定,这也是 pt-archiver 能处理几亿行数据而不拖垮数据库的根本原因。
跑完后,/data/archive 下会出现类似 orders-archive-2025-01-18.txt 的文件,以 tab 或 csv 分隔,第一行是列名。打开看一眼确认数据正常,再继续下一步。这里我建议你不要急着删源表数据,先对比文件行数和源表满足条件的行数,确认一致后再考虑清理。
3.2 场景二:归档到另一张历史表,保留在线查询
需求:不止要清理,还要把历史数据挪到 test_archive.orders_history 表,业务上偶尔还能查旧单子。这种情况就需要 --dest 参数。
先建一张和 orders 表结构基本一致的历史表,注意如果未来要做分区,可以在这个表上直接设计分区策略。为了演示,我直接复制一张普通表:
sql复制CREATE TABLE test_archive.orders_history LIKE test_archive.orders;
然后执行归档命令:
bash复制pt-archiver \
--source D=test_archive,t=orders \
--dest D=test_archive,t=orders_history \
--where "create_time < '2022-01-01'" \
--limit 1000 \
--txn-size 1000 \
--bulk-insert \
--sleep 0.5 \
--max-lag 5 \
--statistics
这里我加了 --bulk-insert,让插入历史表那一步变成一条多值 INSERT,减少 SQL 交互次数,速度会快很多。但不加 --bulk-delete,因为删除源表还是要分批走,锁更小。
执行完后,查询历史表:
sql复制SELECT COUNT(*) FROM test_archive.orders_history;
正常情况下应该等于 orders 表中 create_time < '2022-01-01' 的行数。这个场景最适合“线上保留近期数据,历史数据降级到冷表”的架构,应用层如果还需要查历史,可以直接把查询路由到 history 表,避免所有查询都打在大热表上。
3.3 场景三:只清理不保留,直接 purge
需求:比如日志表、流水表,公司规定只保留最近 90 天,此前的数据无须留任何副本。这种场景用 --purge 就行,它只做删除操作。
bash复制pt-archiver \
--source D=test_archive,t=orders \
--where "create_time < '2022-01-01'" \
--purge \
--limit 2000 \
--txn-size 2000 \
--sleep 0.2 \
--statistics
这个命令会一批一批地删除满足条件的行。我特意把 --limit 和 --txn-size 都调到 2000,因为纯删除没有插入目标表的开销,可以稍微激进一点。不过,如果你发现主库压力大,或者从库延迟开始飙,第一件事就是把这俩值调回 500,并加大 --sleep。
需要注意,--purge 和 --file、--dest 是冲突的,你不能一边删一边又想着写文件,语法上 pt-archiver 也不会允许。真要用文件留底,先按 3.1 跑一遍,再按 3.3 删。
3.4 归档完必须做的事:验证、清碎片、备份
很多教程讲到这里就结束了,但真实生产环境还差三步。
第一步,验证数据一致性。文件归档的要对比文件行数和源表满足条件行数;表归档的要对比源表和历史表的行数。我一般先跑一条 COUNT,再抽查几个主键在两边都存在,确认没有丢数据。
第二步,处理表碎片。归档删掉大量行后,InnoDB 表文件不会自动变小,而是产生大量空洞。如果不做任何操作,下一次全表扫描依然要扫这么大的文件,性能上没占到便宜。常用两种处理方式:
sql复制ALTER TABLE test_archive.orders ENGINE=InnoDB;
或者:
sql复制OPTIMIZE TABLE test_archive.orders;
这两条都会重建表并释放碎片,但都会把表锁住一段时间。所以我只在业务低峰期执行,或者直接用 pt-online-schema-change 做在线重建。如果公司有按周、按月清理的习惯,我建议每季度做一次重建就够了,不用每次删完都做。
第三步,也是很多人容易漏的一步:在删除完大量历史数据后,立即做一次物理备份。这时候表文件最小,备份时间最短,恢复也最快,是性价比最高的备份窗口。配合归档文件一起留存,等于双保险。
4. 自动化实战,把归档流程交给机器
手工跑一次 pt-archiver 不难,难的是让它在没人盯着的时候,每周或者每天定时、安全地把数据归档掉,而且失败了还要能告警。这部分是很多人卡住的地方,我把自己生产环境的方案拆开讲。
4.1 一个可以直接套用的归档脚本
我习惯把归档逻辑封装成一个 shell 脚本,固定放目录,比如 /opt/scripts/archive_orders.sh。脚本需要完成这几件事:加载配置、检查是否有上次任务还在跑、执行 pt-archiver、记录日志、失败时告警。
bash复制#!/bin/bash
# 订单表归档脚本,归档90天前的数据
set -e
DB_HOST="127.0.0.1"
DB_PORT="3306"
DB_USER="archive_user"
DB_PASS="YourPassword"
DB_NAME="test_archive"
SOURCE_TABLE="orders"
DEST_TABLE="orders_history"
ARCHIVE_DIR="/data/archive"
KEEP_DAYS=90
LOCK_FILE="/tmp/archive_orders.lock"
LOG_FILE="/var/log/archive/orders_archive_$(date +%Y%m%d).log"
# 防止重复执行
if [ -f "$LOCK_FILE" ]; then
echo "$(date '+%F %T') 已有归档任务正在执行,本次退出。" >> "$LOG_FILE"
exit 1
fi
touch "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT
# 动态计算归档时间点,只归档比当前时间早90天的数据
ARCHIVE_POINT=$(date -d "$KEEP_DAYS days ago" '+%Y-%m-%d')
WHERE_CLAUSE="create_time < '$ARCHIVE_POINT'"
echo "$(date '+%F %T') 开始归档 $DB_NAME.$SOURCE_TABLE,条件: $WHERE_CLAUSE" >> "$LOG_FILE"
# 执行归档,异常时退出并返回非零码
pt-archiver \
--source D=$DB_NAME,t=$SOURCE_TABLE \
--dest D=$DB_NAME,t=$DEST_TABLE \
--where "$WHERE_CLAUSE" \
--limit 1000 \
--txn-size 1000 \
--bulk-insert \
--sleep 0.5 \
--max-lag 5 \
--statistics >> "$LOG_FILE" 2>&1
if [ $? -eq 0 ]; then
echo "$(date '+%F %T') 归档完成" >> "$LOG_FILE"
else
echo "$(date '+%F %T') 归档失败,请检查日志" >> "$LOG_FILE"
# 这里可以接告警,比如 curl 一个 webhook 到钉钉、飞书或企业微信
fi
几个细节说说我为什么这么写:
- 用 date 命令动态算时间点,而不是写死日期。这样脚本每周跑都能自动把窗口往前推,不需要人工每月改一次。
- 用 LOCK_FILE 做互斥。如果上次归档因为某些原因卡住了,新的任务不会叠加上去,避免两批删除互相干扰。
- 输出重定向到日志文件,日志名按日期命名,方便后面排查。我一般还会配合 logrotate 或定期清理脚本把 30 天前的日志清掉。
如果你没有历史表,而是需要导出文件,把 --dest 参数换成 --file 路径和 --output-format csv 即可,脚本骨架完全一样。
4.2 定时任务:crontab 与 systemd timer 的选择
归档脚本需要的定时调度,Linux 上最简单的方式就是 crontab。比如每周日凌晨 2 点执行一次:
cron复制0 2 * * 0 /opt/scripts/archive_orders.sh
crontab 的好处是简单直接,适合绝大多数团队;缺点是没有自带的重试、依赖管理、运行状态查询。如果你的服务器用的 systemd,也可以考虑用 systemd timer 做定时器,这样能用 journalctl 统一看日志。我这边简单给一个 service 和 timer 的示例,你按需选择。
先写 service 文件 /etc/systemd/system/archive-orders.service:
ini复制[Unit]
Description=Archive orders table
After=network.target mysql.service
[Service]
Type=oneshot
ExecStart=/opt/scripts/archive_orders.sh
User=root
再写 timer 文件 /etc/systemd/system/archive-orders.timer:
ini复制[Unit]
Description=Run archive orders weekly
[Timer]
OnCalendar=Sun 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
然后启动:
bash复制systemctl daemon-reload
systemctl enable archive-orders.timer
systemctl start archive-orders.timer
用 systemd 的好处是如果上次任务错过执行时间,系统重启后 Persistent=true 会立即补跑一次,比 crontab 更可靠。但对大多数场景,crontab 已经足够了,不用为了“高级”刻意换。
4.3 自动化不等于甩手,配套监控要跟上
定时任务跑起来之后,你千万别以为万事大吉。我见过太多团队配了 crontab,结果某天磁盘满了、密码改了、表结构变了,归档任务悄悄失败了一个月没人发现,最后大表膨胀把实例拖垮。归档自动化必须配套监控,至少盯住四个指标:
- 任务执行状态。最简单的方式是脚本里失败时输出非零退出码,然后给监控系统上报。我们内部会给 webhook 发一个告警消息。
- 主从延迟。pt-archiver 自己有 --max-lag 兜底,但你要监控延迟整体趋势,如果归档一跑延迟就飙升,说明参数需要调。
- 磁盘空间和表大小。归档的核心目标是释放空间,定期查看源表大小是否真在下降,归档文件所在目录是否快满。
- 慢查询和锁等待。看看归档期间有没有业务 SQL 被阻塞,锁等待时间是否变长。
如果你的监控体系比较齐全,比如有 Prometheus + Grafana,可以直接在仪表盘上把 pt-archiver 每次执行的行数、耗时、延迟曲线都展示出来。没有的话,哪怕每天看一眼日志也算数,关键是“有人盯着”。别忘了定期演练归档数据的恢复流程,光归档不验证可恢复性,等于白干。
5. 高频踩坑记录与性能调优思路
5.1 我踩过的坑,按出现频率排序
先给大家排一下我在真实环境里见过的、以及自己踩过的坑,每一项都能从日志或现场症状反推。
where 条件没走索引。 最常见的问题,没有之一。如果 where 条件里的字段没有索引,pt-archiver 会退化成全表扫描。表面上它能跑,但每批取数都要扫全表找符合条件的行,随着数据量增加,性能直线下降,最终把磁盘 IO 打满。解决方法是先 EXPLAIN SELECT COUNT(*) FROM orders WHERE create_time < '2022-01-01';,确认用了 idx_create_time,再跑归档。
归档任务和业务高峰重叠。 即使 pt-archiver 已经控制得很优雅,它毕竟还是会在源表上产生持续的读和写操作。如果在白天业务高峰跑大量归档,一样会有性能影响。我通常安排在凌晨 2 点到 4 点,并且用 --sleep 把节奏放慢。
归档后表文件大小没有下降。 这个前面提过,DELETE 只是打标记,不还给操作系统,必须重建表或 OPTIMIZE。不少人归档完看磁盘没变化,以为工具没生效,其实是误解。归档释放的是“逻辑空间”,立即反映在行数减少上,物理文件需要重建才收缩。
主从延迟飙升。 即使有 --max-lag 兜底,如果你 --limit 和 --txn-size 调得太大,或者 --sleep 设成 0,从库回放 binlog 还是可能跟不上。我的建议是先小参数跑一轮,观察延迟变化曲线,再逐步加码。
字符集不一致导致乱码或报错。 源表、目标表、连接层如果字符集不一致,中文数据容易变成问号,甚至直接报 Illegal mix of collations 错误。连接时加上 --charset=utf8mb4,并用 SHOW CREATE TABLE 确认两表字符集一致。
目标表有重复数据。 如果你用 --dest 归档到历史表,但历史表已经有相同主键的行,INSERT 会直接报主键冲突。这种要看业务容忍度:如果允许重复,就把目标表主键去掉;如果不允许,得先去重再归档。
DELETE 触发器导致意外操作。 如果源表定义了 DELETE 触发器,pt-archiver 的每一批删除都会触发它。归档一个 DELETE 触发器,可能导致大量额外开销,甚至误改其他表数据。遇到这种表,我在归档前会和业务方确认触发器逻辑,必要的时候先临时禁用触发器,跑完再恢复。
内存和临时文件暴涨。 当 --bulk-insert 插入的目标表有大量二级索引时,构建索引的临时数据会占用较多内存。若实例内存本来吃紧,建议关掉 bulk 参数,走普通逐行插入。
下面是几个最典型问题的速查表:
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| 执行极慢 | where 无索引 | 给条件字段加二级索引 |
| 磁盘空间没变化 | 有碎片 | ALTER TABLE ENGINE=InnoDB 重建 |
| 主从延迟高 | limit 太大/sleep 太小 | 调小 limit,加大 sleep |
| 中文乱码 | 字符集不一致 | 加 --charset=utf8mb4 |
| 主键冲突 | 目标表已有重复数据 | 先清理目标表重复数据 |
| 任务自动失败 | 密码过期/权限不足 | 检查账号权限和密码有效期 |
| 归档后业务变慢 | 重建表期间锁表 | 低峰执行或使用在线改表工具 |
5.2 从慢到快,参数调优的实战节奏
很多人跑 pt-archiver 觉得慢,就开始盲目调大 --limit。我劝你千万别这么干,调优得一步步来,观察指标再动参数。我一般按这个节奏来:
第一步,用小参数试跑 5 分钟,比如 --limit 500 --txn-size 500 --sleep 1,同时盯着主库的 CPU、QPS、从库延迟。如果一切平稳,进入第二步。
第二步,把 --sleep 降为 0.5,跑 10 分钟,观察延迟是否上升。如果延迟平稳,进入第三步。
第三步,把 --limit 和 --txn-size 提到 1000,保持 --sleep 0.5,继续观察。如果延迟开始上涨,退回上一档。如果依然稳定,可以再尝试 2000。
第四步,确认安全后,加上 --bulk-insert 和 --bulk-delete,重新观察。开启 bulk 后处理速度会明显提升,但主库可能短暂出现单个语句执行时间变长的情况,这个需要接受。
还有一个取巧的方式:把归档任务切成多个子任务并行跑,每个子任务负责不同 id 范围。比如 id 1 到 1 亿归第一个进程跑,1 亿到 2 亿归第二个进程跑。因为每个进程都在用主键范围扫描,互不冲突,可以更好利用多核 CPU。不过并行度要控制,我一般不超过 4 个并发,否则从库压力会叠加。
5.3 归档文件落盘时也要动脑子
归档到文件时,文件生成位置对性能也有影响。如果你把归档文件写到源数据库所在盘的同一块磁盘,归档期间的磁盘 IO 会跟 MySQL 的读写混在一起,彼此拖累。我一般把归档文件放到独立的磁盘或挂载点,比如 /data2/archive,并用 tmpfs 做临时缓冲的场景也有人用,但最终落盘还是得独立存储。
还有一个小技巧:归档文件建议用日期变量做命名,而不是固定一个文件名。这样每次任务都生成新文件,方便追溯,也避免因为并发写同一个文件导致数据错乱。比如:
bash复制--file '/data/archive/orders_archive_%Y_%m_%d_%H_%i_%s.txt'
秒级的时间戳能保证每天多次执行时也不会覆盖。如果你之后要加载到数仓,这个命名规则也能直接当作分区字段用。
6. 几个我坚持推荐的最佳实践
工具用熟之后,真正拉开差距的是使用习惯和整体方案设计。这里分享几个我坚持了很久的实践,希望对你有参考价值。
一是归档条件尽量用主键或唯一索引范围,而不是纯时间范围。虽然 where 里写 create_time < '2022-01-01' 完全可行,但底层扫描和分批逻辑是按主键走的。如果时间条件和主键顺序没有相关性,每次取一批数据都可能需要回表查二级索引。我通常会在表里设计一个自增主键,并且让 create_time 随插入顺序递增,这样归档时 where 条件和主键顺序天然一致,性能会好很多。
二是冷热数据分层要提前设计,不要等表快爆了才动手。如果业务上能预见数据增长,从一开始就把历史表设计成分区表,比如按年份分区,归档时直接用 pt-archiver 把数据插入对应分区,甚至更激进一点,用分区裁剪直接 DROP PARTITION 清理数据。分区表的归档方案比单表灵活得多,但要注意分区键的选择要符合查询习惯。
三是把归档当成“数据生命周期管理”的一部分,而不是单纯的清理。这意味着你要想清楚:哪些数据保留多久、归档后存哪里、谁能查、保留多少年、到期怎么销毁。在这个框架下,pt-archiver 只是执行层的一个工具,前面要接制度,后面要接存储和备份。能让这一步跑顺的团队,数据库治理水平基本都不会差。
四是日常运维留好后路。每次做大规模归档前,先备份源表的数据,不需要完整备份,只要确认可恢复即可。归档任务结束后,花几分钟抽查几行数据,看看源表、目标表、文件三者是否一致。这些习惯几分钟就能完成,但能避免绝大多数数据事故。
五是别把 pt-archiver 当成唯一方案。除了它,还有分区表、归档表/存储引擎(比如改用归档引擎)、或者直接把冷数据迁移到 ClickHouse、对象存储等外部系统。pt-archiver 适合保留在 MySQL 内部做高低峰之间的挪移,但如果你要长期存放海量历史数据,外部系统或者数据湖是更好的归宿。工具是死的,思路是活的,搞清楚每种方案的适用边界才是最重要的。
回到开头那句话:我是被大表清理逼着认识 pt-archiver 的。用到现在几年了,它给我的感受是,哪怕你完全没读过源码,只要把参数吃透,它就能帮你把最危险的大数据量清理变成一件可以预期、可以监控、可以自动化的日常任务。这套流程跑顺之后,我再也不用半夜爬起来处理磁盘告警,也希望这篇文章能让你少走我当年走过的弯路。
