一周前的凌晨,一台跑了三个 WordPress 站的服务器弹出磁盘告警:使用率 97%,剩余空间不到 3GB。追查下去,吃掉磁盘的既不是日志也不是上传文件,而是 MySQL 的二进制日志——一天净增 2.8GB。更蹊跷的是,配置文件里明明设置了自动过期,binlog 却只增不减。这篇文章完整复盘这次排障:从取证、止血、定位元凶,到修复后的增速复测方法。所有命令都在 MySQL 5.7 + 宝塔面板环境实测通过。

一、取证:先看清楚是谁在吃磁盘
磁盘告急不要第一反应就删文件,先做归因。一条命令看清大头:
du -ch /srv/mysql/data/mysql-bin.* | sort -h | tail
ls /srv/mysql/data/ | grep -c mysql-bin
这次实测结果:binlog 文件 400+ 个,总量占满分区大头。再核对配置:
# my.cnf
expire_logs_days = 3
设置了 3 天过期,文件却越积越多——这就是本案第一个知识点。
二、为什么 expire_logs_days 形同虚设
MySQL 5.7 的经典坑:expire_logs_days 只在两个时机被真正执行——MySQL 重启时,或下一次 binlog rotate 由持有 SUPER 权限的客户端触发时。
宝塔环境里,PHP 站点全部用业务账号连库,业务账号没有 SUPER 权限。结果就是:过期策略永远没有执行者,binlog 无限堆积。
止血方案用官方命令,绝不允许直接 rm mysql-bin.*(会破坏 binlog index,MySQL 下次 rotate 直接起不来):
PURGE BINARY LOGS BEFORE NOW() - INTERVAL 1 DAY;
PURGE 会自动维护 index 文件,这是它和手删的本质区别。
PURGE 前的三个安全前提(缺一不可)
SHOW SLAVE STATUS为空——确认没有复制下游在消费 binlog;- 备份策略只做
mysqldump全量,不依赖 binlog 做 PITR(时间点恢复); - 保留窗口内的业务低峰执行。
这次实测:业务账号在该 5.7 环境有执行 PURGE 的权限,无需 root;执行完磁盘 97% → 84%,回收 9.3GB。

三、止血只是开始:2.8G/天 的增量必须找到凶手
PURGE 完第二天,binlog 又开始肉眼可见地涨。purge 是抽血,凶手还在造血。定位方法是对当前正在写入的文件做事件统计:
mysqlbinlog --no-defaults --base64-output=decode-rows -v /srv/mysql/data/mysql-bin.000471 \
| grep -E "^UPDATE |^### (UPDATE|INSERT)" \
| cut -c1-60 | sort | uniq -c | sort -rn | head
输出立刻见分晓:wp_options 表的 rewrite_rules 一行以压倒性频次排第一。
回到代码层,WordPress 站的高频凶手长得很有辨识度——某个插件把 flush_rewrite_rules() 挂在了 init 钩子上:
// 反面教材:每请求重写 39KB 的 rewrite_rules option
add_action('init', function () {
add_rewrite_endpoint('quiz-iframe', EP_PERPOST);
flush_rewrite_rules(); // ← 每刷新一次页面,39KB 全量 UPDATE 进 binlog
});
一行代码,每次页面请求产生一次 39KB 的全字段 UPDATE;行格式 binlog 记录整行前后镜像,日均五万 PV 就是 2.8GB。用 grep 扫插件目录直接点名:
grep -rn "add_action('init'" /srv/app/wp-content/plugins/ \
| xargs -I{} sh -c 'grep -l flush_rewrite_rules {}'
四、修复:删之前先确认规则已在库里
flush_rewrite_rules() 不是无害代码,它的职责就是把内存里算好的重写规则写回数据库。直接删的前提是:规则已经在 wp_options 里躺着了。先验证:
SELECT option_value LIKE '%quiz-iframe%' FROM wp_options WHERE option_name = 'rewrite_rules';
返回 1,说明该插件的 endpoint 规则已在库中;此后 WordPress 每次请求本来就会从库里读取这份缓存,删掉函数体内那行 flush_rewrite_rules();、保留 add_rewrite_endpoint(...) 调用,行为完全不受影响。
操作纪律:改动前 cp -a 留备份(class-plugin.php.bak-noflush-日期),改完 php -l 语法复验,回滚 = 还原备份文件,十秒收工。

五、修复后怎么复测:别采样错对象
这是最容易翻车的一步:复测必须对着”当前写入文件”采样,而不是目录里最大的文件。旧的大文件是历史尸体,数字再大也不增长。
# SHOW BINARY LOGS 最后一行 = 正在写的文件
SIZE1=$(stat -c %s /srv/mysql/data/mysql-bin.000472)
for i in $(seq 1 50); do curl -s -o /dev/null https://example.com/; done
SIZE2=$(stat -c %s /srv/mysql/data/mysql-bin.000472)
echo "增量: $((SIZE2 - SIZE1)) bytes / 50 请求"
修复前:每请求约 80KB 写放大(含前后镜像与元数据),50 请求就是 4MB 级增量。修复后:应接近 0。这次实测从”每请求 80KB”降到平均不足 500 字节,按日均流量折算,日产量从 2.8GB 降到 22MB——128 倍的降幅。
六、顺手项的边界:别贪刀
排障时看到旁边还有几处”看起来能优化”的东西,克制很重要:
- 碎片整理:全库
data_free只有 27MB,其中超过 5MB 阈值的表没有——OPTIMIZE 收益为零,跳过; - 大表慎动:一张 610MB 的副标题表,OPTIMIZE 意味着长时间锁风险,收益不到 100MB,不动;
- slow log 画像:用 Python 正则聚类
Query_time,不要用 bash grep 抽指纹——会被SET timestamp噪音骗得一塌糊涂; - 再生缓存:
/tmp下的搜索缓存目录可清,但先确认它不是业务写入目标。

七、验收三连
- 站点核心页面 200;
- 业务 API 返回真实数据(不是空壳 200);
df -h前后对比确认回收量,且第二天 binlog 目录不再涨。
小结
| 环节 | 关键动作 | 陷阱 |
|---|---|---|
| 止血 | PURGE BINARY LOGS |
严禁 rm 物理文件 |
| 前提 | 无下游复制 + 备份不依赖 binlog | 缺一不可 |
| 定位 | mysqlbinlog 事件频次聚类 | 采样对象必须是当前写入文件 |
| 修复 | 确认规则已入库再删 flush | 先备份、php -l 复验 |
| 复测 | 50 请求增量法 | 别对旧文件测增长 |
binlog 暴涨类问题的本质通常是”某个每请求执行的重量级写操作”,PURGE 给你争取时间,找到并拔掉那行代码才是治本。



