MySQL binlog 清理实战:从磁盘告警到安全释放空间
2026/9/11 3:16:07 网站建设 项目流程

凌晨两点四十,监控平台弹出一条告警:/data/mysql使用率 98%。我登上去看了一眼,数据目录里齐刷刷躺着一排mysql-bin.000xxx文件,光 binlog 就占了将近 190GB。按当时的业务写入量估算,这个量级的 binlog 大约能撑两周,但磁盘只剩 2% 可用,别说继续写数据,连 slow log 和 error log 都可能随时写不进去。那会儿脑子里冒出的第一个念头是“把 binlog 直接删了不就完事”,但干这行久了就明白,删除 binlog 从来不是一条rm命令那么简单。后面我会把这件事拆开讲清楚:什么时候能删、用命令怎么删、删完空间为什么没释放、以及手贱直接删物理文件之后要怎么补救。


1. binlog 不是普通日志:先搞懂它记录了什么,再谈删除

很多刚接触 MySQL 的人容易把 binlog 和 error log、slow log 混为一谈,觉得“日志嘛,删掉就行了”。实际上 binlog(二进制日志)的核心职责有两条,哪一条断了都会出大事。

1.1 一次磁盘告警引出的问题

我接手过的 MySQL 实例里,因为 binlog 撑爆磁盘的案例有不少。常见情况是:开发同学开启log_bin之后就没再管过,或者某个大事务一次性更新了上千万行数据,binlog 瞬间膨胀出几十 GB。等到磁盘告警响起来,数据库已经处于“写不进去、读也变慢”的边缘状态,这时候再去想怎么删,压力会大很多。

所以这篇文章的第一个结论就是:binlog 的清理策略,必须在部署阶段就定好,而不是等磁盘告警再去救火。

1.2 binlog 的双重职责:崩溃恢复与主从复制

先花两分钟搞清楚 binlog 为什么重要。它记录的是 MySQL 中所有数据变更操作,包括 INSERT、UPDATE、DELETE、DDL 等,但不记录 SELECT 查询。它主要有两个用途:

  • 崩溃恢复(时间点恢复):当数据库数据文件损坏,或者你想把数据恢复到某个特定时间点,binlog 记录了自全量备份以来的每一次变更。配合全量备份,可以用mysqlbinlog工具把增量部分重新执行一遍,做到接近零丢失的恢复。
  • 主从复制:主库生成 binlog,从库拉取 binlog 并在本地重放,从而保持主从数据一致。如果从库还没拉到某个 binlog 文件,主库就把这个文件删了,从库会直接复制中断,且报错信息往往比较难懂。

对于没有任何复制关系、也不做时间点恢复的纯单机测试库,binlog 的保留价值确实没那么高,删除的胆子可以大一些。但生产库基本都有备份策略和从库复制,动手之前必须先评估影响面。

提示:查看当前是否开启了 binlog,执行SHOW VARIABLES LIKE 'log_bin';。如果你至今没见过 binlog,大概率是没开,或者用的是云厂商默认托管实例。

1.3 什么情况让 binlog 快速膨胀

除了正常的业务写入,下面几类场景最容易让 binlog 增长失控:

  1. 大事务:比如一条UPDATE语句一次性修改一百万行,如果 binlog_format 是ROW,那么这百万行的变更前后值都会写进 binlog,文件体积可能瞬间增加几百 MB 到几 GB。MySQL 8.0 默认binlog_format=ROW,安全是安全,但体积确实比早期的STATEMENT格式大不少。
  2. 主从延迟期间主库持续写入:从库跟不上主库,主库的 binlog 文件只能一直保留,越长越占空间。
  3. 误把 binlog 当成归档:有人觉得 binlog 是“历史数据”,留着以后能当审计用,结果每小时的写入量又大,几个月下来积攒了几百个文件。
  4. 每次手动 FLUSH LOGS 产生新文件:频繁切换 binlog 会产生大量小文件,同样会占用不小空间。

我在实践中判断 binlog 是否需要清理,核心就看两点:从库是否还需要它,以及时间点恢复策略是否需要它。两者都不需要,那就可以放心清理。


2. 动手删除前,先把这三件事查清楚

删除 binlog 之前,我建议你先花五分钟执行几条查询命令,把现状摸清楚。这一步省不掉,否则很容易把复制链路弄断。

2.1 看现状:磁盘占用与 binlog 文件清单

先看文件列表和占用:

# 查看 binlog 文件列表及大小(binlog 目录通常在 datadir 下) ls -lh /data/mysql/mysql-bin.*
-- 在 MySQL 内查看 binlog 文件清单 SHOW BINARY LOGS;

这两条命令能让你立刻知道:现在有多少个 binlog 文件、最早的文件是什么时候的、整体占用多少空间。你还可以用SHOW VARIABLES LIKE 'max_binlog_size';查看单个 binlog 文件的最大尺寸,默认通常为 1GB,也就是说如果出现 100 个文件,那基本就是 100GB 起步。

2.2 确认复制链路:不能删到从库还没读的位置

如果这个实例有从库,那么这一步是重中之重。在从库上执行:

-- MySQL 8.0.22 之前的版本 SHOW SLAVE STATUS\G -- MySQL 8.0.22 及之后的版本 SHOW REPLICA STATUS\G

关注两个字段:

  • Master_Log_File/Source_Log_File:从库 IO 线程已经读到了哪个主库 binlog 文件。
  • Relay_Master_Log_File/Exec_Master_Log_Pos:从库 SQL 线程已经重放到了哪个文件、哪个位置。

安全清理的判断标准是:计划删除的 binlog 文件名,必须早于从库已经读取到的文件名。换句话说,从库不需要再读取的文件,删除风险才低。如果从库还在读mysql-bin.000010,而你删到了mysql-bin.000009,问题不大;但如果连000010一起删了,复制就会中断。

注意:从库的 IO 线程和 SQL 线程可能处于不同位置,以“更靠后的线程位置”为参考。有多个从库时,要以位置最落后的那个从库为准,不能只看最快的一个。

2.3 确认当前正在写入的文件

无论有多少历史 binlog,当前正在写入的 binlog 文件绝对不能删。查看方法:

SHOW MASTER STATUS;

返回结果里的File字段就是主库当前正在写入的 binlog。另外在删除之前,最好先记录一下这个文件名,后面验证的时候要用到。

你还可以顺手看一下从库复制是否健康:Slave_IO_RunningSlave_SQL_Running是否都是Yes。如果本来就是断开的,删除 binlog 更要谨慎,因为不知道从库落后了多少。


3. 长期解法:让 MySQL 自动过期清理,而不是手动删

手动清理是应急手段,长期来看必须靠参数让 MySQL 自己管理 binlog 的保留时间。这样不会出现“某一天忘了清理,磁盘又爆了”的情况。

3.1 自动过期机制与参数演变

MySQL 提供了两个控制 binlog 保留时间的参数:

  • expire_logs_days:按“天”设置,是 MySQL 5.7 及之前版本的主流参数。
  • binlog_expire_logs_seconds:按“秒”设置,MySQL 8.0 开始引入,精度更高,可以精确到小时甚至分钟。设置后expire_logs_days会进入废弃状态。

查看当前生效值:

SHOW VARIABLES LIKE 'expire_logs_days'; SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';

在 MySQL 8.0 中,如果同时把两个参数设置成非零值,系统会直接报错,要求你把其中一个设回0。这也是很多人在配置文件中同时写了两行,导致 MySQL 启动失败的原因。

3.2 修改配置的正确姿势

假设希望 binlog 保留 7 天,MySQL 8.0 的做法是:

[mysqld] binlog_expire_logs_seconds = 604800

对于 MySQL 5.7:

[mysqld] expire_logs_days = 7

修改配置文件后重启 MySQL 生效。如果不想重启,可以动态修改全局变量:

SET GLOBAL binlog_expire_logs_seconds = 604800;

这条命令在当前实例上立即生效,但不会持久化到配置文件。也就是说,下次重启后参数还会回到配置文件里的值。所以正确做法是先动态改,再把配置写进 my.cnf,两者缺一不可。

3.3 设置后不立即生效怎么办

有一个很常见的疑问:我明明设置了binlog_expire_logs_seconds = 604800,但过了一会儿去看SHOW BINARY LOGS;,老文件还在,空间也没减少,是不是设置没用?

这不是设置没用,而是自动清理不一定立刻触发。MySQL 对过期 binlog 的清理动作,通常发生在binlog 文件轮换时、实例启动时、或执行FLUSH LOGS。如果当前 binlog 还没写满 1GB,一直没轮换,那过期检查可能迟迟不会执行。

可以手动触发一次:

FLUSH LOGS;

这个命令会关闭当前 binlog,新建一个文件继续写入,同时触发一次过期清理。执行完再SHOW BINARY LOGS;,通常就能看到过期文件已经被自动删掉了。

提示:频繁执行FLUSH LOGS会产生很多 1GB 以下的小文件,反而增加管理成本。建议在需要验证参数时执行一次即可,不要做成定时任务。


4. 应急清理:PURGE 和 RESET MASTER 怎么选

如果磁盘空间已经告急,没时间等自动清理,那就需要手动下令。手动清理有两条命令,适用场景完全不同。

4.1 PURGE BINARY LOGS TO / BEFORE

PURGE是删除 binlog 最常用的命令,它只会删除指定范围之前的文件,保留当前文件,相对安全。

按文件名删除,删除mysql-bin.000010之前的所有文件(不包括000010本身):

PURGE BINARY LOGS TO 'mysql-bin.000010';

按时间删除,删除 2025-01-01 00:00:00 之前的所有 binlog:

PURGE BINARY LOGS BEFORE '2025-01-01 00:00:00';

也可以结合NOW()做相对时间:

PURGE BINARY LOGS BEFORE NOW() - INTERVAL 3 DAY;

这条命令的底层逻辑是:MySQL 会扫描 binlog index 文件,删除文件中记录的所有早于指定位置的 binlog 物理文件,同时更新 index 文件。这样 MySQL 内部记录和磁盘文件是一致的,不会出现“文件没了但索引还记着”的错乱状态。

删除后,可以立即验证:

SHOW BINARY LOGS;

确认目标文件确实消失,且剩余文件的最小编号符合预期。

4.2 RESET MASTER 的适用边界和风险

RESET MASTER的杀伤力比PURGE大得多——它会清空所有 binlog 文件,并把序号重新从000001开始编号。

RESET MASTER;

什么场景下能用?一般只建议在以下情况使用:

  • 刚搭建的全新实例,没有任何从库依赖,不需要保留历史 binlog。
  • 测试环境,binlog 已经没有价值,准备彻底重置。
  • 需要彻底清理 binlog 以释放磁盘,且能接受时间点恢复失效。

但生产环境要非常小心,尤其是开启了 GTID 的实例。执行RESET MASTER会同时清空gtid_executed信息,如果从库依赖主库的 GTID 集合做复制,很可能导致从库无法对齐。我在一个项目里见过有人为了清理 binlog 在主库执行了RESET MASTER,结果三个从库全部复制中断,最后只能通过重新备份恢复。

所以我的建议是:能不用就不要用,生产环境首选 PURGE。如果实在要用,先确认没有活跃从库,并提前备份mysql-bin.index内容和 SHOW MASTER STATUS 的输出,作为回退依据。

4.3 三种方式对比

方式命令/配置适用场景风险等级
自动过期binlog_expire_logs_seconds/expire_logs_days日常运维,长期生效
手动精准删除PURGE BINARY LOGS TO/BEFORE磁盘告警,需要保留部分 binlog中低
全量清空RESET MASTER全新实例、测试环境、彻底重置

实际运维中,我大多数情况下只用两种组合:平时靠自动过期,磁盘告警时先PURGE应急,再回头补查为什么自动过期没生效。


5. 最不建议的“省事”操作:直接 rm 掉 binlog 会怎样

我必须专门用一章聊这个问题,因为真的有人这么干过,而且我也这么干过一次,代价是支付了几个小时的排查成本和一顿检讨。

5.1 踩坑过程复盘

有一次,某台测试机的/data/mysql磁盘爆满,当时为了快速释放空间,我直接执行了:

rm -f /data/mysql/mysql-bin.0000*

文件确实没了,磁盘空间也释放了。但没过多久,业务侧反馈写入报错。我登上去执行SHOW BINARY LOGS;,结果直接报错,提示 binlog 索引文件里的某些文件找不到了。

原因很简单:MySQL 通过一个mysql-bin.index文件记录所有 binlog 文件清单。用rm删掉物理文件后,index 文件里还保留着这些文件名。MySQL 内部逻辑是“先查 index 再读文件”,当它发现索引指向的文件不存在,就会认为 binlog 文件损坏,影响后续的 binlog 写入和复制。

5.2 用 rm 删完可能出现的连锁问题

直接删物理文件的后果不止是条目错乱,还有更麻烦的情况:

  • 当前正在写入的 binlog 被删:如果你好巧不巧把SHOW MASTER STATUS里正在写的那一个也删了,MySQL 会一直尝试写入一个“已消失文件”的句柄,磁盘空间反而不会释放,直到你把 MySQL 重启。
  • index 文件与磁盘不一致:后续执行FLUSH LOGS、自动轮换、甚至重启都可能报错,严重时 MySQL 直接启动失败。
  • 从库彻底断链:从库按 index 顺序请求下一个 binlog,结果主库这边文件信息错乱,从库收到错误信息直接中断复制,报 1236 错误。

所以结论很明确:binlog 的删除必须通过 MySQL 自己来,因为它需要同时维护物理文件和 index 索引的一致性。rm只是删了文件,没有同步更新索引,自然会出现各种奇怪的问题。

5.3 补救方法

如果你已经手滑了,可以尝试按顺序做以下步骤:

  1. 停止 MySQL 服务(如果不影响业务,尽量停干净)。
  2. 打开mysql-bin.index文件,把其中已经不存在的文件名行删除。这个文件每行记录一个 binlog 文件名,内容大概长这样:
/data/mysql/mysql-bin.000001 /data/mysql/mysql-bin.000002 /data/mysql/mysql-bin.000003
  1. 把实际存在的 binlog 文件按顺序重命名补齐编号,确保 index 里的名称和磁盘上文件一一对应。
  2. 重新启动 MySQL,执行SHOW BINARY LOGS;验证。

这个过程说起来简单,实际操作非常繁琐,而且容易出错。如果 binlog 数量多、还有从库依赖,建议直接评估用备份重建实例,可能比重试 index 文件更快。


6. 主从复制环境下的删除纪律

上面提到的命令在单机环境里跑没问题,但大部分生产 MySQL 都是主从架构。删除 binlog 的时候,必须站在整个复制链路的角度想问题。

6.1 从库为什么依赖主库 binlog

主从复制的原理是:从库通过 IO 线程连接主库,按顺序拉取主库 binlog 文件,写入本地 relay log,再由 SQL 线程重放。如果主库某个 binlog 文件被提前删除,从库还没拉到它,相当于中间缺了一整段数据变更记录,复制必然中断。

最典型的一个场景是:某从库因为网络隔离或磁盘故障,和主库断开了两天。主库这边自动过期参数设置的是 24 小时,两天后重连时,从库发现需要的 binlog 已经被主库清理掉了。

6.2 安全删除的判断标准

在从库执行SHOW REPLICA STATUS\G,看两个关键位置:

  • IO 线程已经读到哪里(Master_Log_FileSource_Log_File
  • SQL 线程已经重放到哪里(Relay_Master_Log_FileExec_Master_Log_Pos

安全清理规则:只删除这个位置之前的所有 binlog 文件。比如从库 SQL 线程已经执行完mysql-bin.000050,那主库删除到000050之前都相对安全。如果从库落后比较多,就先别急着清。

另外,GTID 模式下还有个更稳妥的参考点。在从库执行:

SHOW VARIABLES LIKE 'gtid_executed';

看到gtid_executed集合后,可以在主库用SHOW MASTER STATUS确认哪些 binlog 文件对应的 GTID 范围已经被从库消费。不过这套判断对新手来说有点绕,最直接还是看Relay_Master_Log_File字段。

6.3 误删之后的重建思路

假设你已经误删了从库需要的 binlog,从库报错类似:

Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'Could not find first log file name in binary log index'

这说明当前主库已经没有从库需要的 binlog 文件了。最快捷的恢复方案是:重新指定从库的同步位点。

STOP REPLICA; CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000060', MASTER_LOG_POS=4; START REPLICA;

但这里有个问题:你要知道mysql-bin.000060MASTER_LOG_POS应该填多少。如果没有记录,或者主库这个文件里的 binlog 也不是从库缺失的那一段,数据还是会对不上。

所以更稳妥的恢复方式是直接重建从库:基于主库最新的全量备份(加上对应的 binlog position 或 GTID 信息),重新搭建一个从库。这个流程虽然耗时,但至少能保证数据一致性。

我个人经历过一次误删之后,养成了一个纪律:清理 binlog 之前,先确认所有从库的复制落后时间不超过一个文件的保留窗口;如果某个从库长期离线,先处理它,再清理。这句话值得每一位 MySQL 运维/开发记下来。


7. 一次磁盘告警的完整处理链路:从定位到验证

最后分享一个典型的处理过程,把前面所有知识点串起来。假设你现在遇到的就是文章开头说的场景:磁盘 98%,binlog 占到 190GB。

7.1 第一步:先看一眼全局

df -h du -sh /data/mysql/mysql-bin.*
SHOW BINARY LOGS; SHOW MASTER STATUS;

这一步的目标是回答三个问题:文件有多少、当前写到哪个文件、有没有从库依赖。先别急着删,信息不全就动手是事故的开端。

7.2 第二步:确认从库位置

如果有从库,去从库执行:

SHOW REPLICA STATUS\G

看到 IO 线程和 SQL 线程都停在mysql-bin.000060或更靠后的位置,那么主库删到000059或保留更保险的文件就相对安全。如果没有从库,这个检查跳过。

7.3 第三步:先止血,再长期治理

磁盘已经 98%,容不得慢慢等自动清理。先执行快速释放:

PURGE BINARY LOGS TO 'mysql-bin.000060';

注意:这里的000060是“要保留到的最早文件”。如果需要把所有 binlog 都清光,建议用RESET MASTER但需谨慎;常规做法是至少保留当前正在写的文件:

PURGE BINARY LOGS BEFORE NOW() - INTERVAL 1 DAY;

这样把一天前的 binlog 全部清掉,当前文件不受影响。执行完后观察磁盘:

df -h /data/mysql

空间释放后,再回过头去修改配置,设置合理的自动过期时间:

SET GLOBAL binlog_expire_logs_seconds = 604800;

并把binlog_expire_logs_seconds = 604800写进my.cnf[mysqld]段。

7.4 第四步:验证不反弹

改完参数后别直接走人。等几分钟后再次执行:

SHOW BINARY LOGS;

确认文件数量没有异常增长。如果后续发现自动过期没有按预期触发,可以手动执行一次FLUSH LOGS;触发检查。最后再把磁盘监控阈值从 80% 调整为“binlog 目录单独监控”,防止下次再被突增的 binlog 打爆。

我现在处理这类问题已经有固定套路:先判断有没有从库,再确认当前文件,然后PURGE止血,最后把自动过期参数补上并验证。这套流程虽然看起来步骤多,但每一步都在避免“删完才发现删错了”的尴尬。如果你在操作时能沉住气把这四步走完,binlog 清理这件事基本就稳了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询