凌晨两点四十,监控平台弹出一条告警:/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 增长失控:
- 大事务:比如一条
UPDATE语句一次性修改一百万行,如果 binlog_format 是ROW,那么这百万行的变更前后值都会写进 binlog,文件体积可能瞬间增加几百 MB 到几 GB。MySQL 8.0 默认binlog_format=ROW,安全是安全,但体积确实比早期的STATEMENT格式大不少。 - 主从延迟期间主库持续写入:从库跟不上主库,主库的 binlog 文件只能一直保留,越长越占空间。
- 误把 binlog 当成归档:有人觉得 binlog 是“历史数据”,留着以后能当审计用,结果每小时的写入量又大,几个月下来积攒了几百个文件。
- 每次手动 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_Running和Slave_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 补救方法
如果你已经手滑了,可以尝试按顺序做以下步骤:
- 停止 MySQL 服务(如果不影响业务,尽量停干净)。
- 打开
mysql-bin.index文件,把其中已经不存在的文件名行删除。这个文件每行记录一个 binlog 文件名,内容大概长这样:
/data/mysql/mysql-bin.000001 /data/mysql/mysql-bin.000002 /data/mysql/mysql-bin.000003- 把实际存在的 binlog 文件按顺序重命名补齐编号,确保 index 里的名称和磁盘上文件一一对应。
- 重新启动 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_File或Source_Log_File) - SQL 线程已经重放到哪里(
Relay_Master_Log_File或Exec_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.000060的MASTER_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 清理这件事基本就稳了。