
MySQL的binlog日志文件越攒越多一个不小心就把磁盘空间给撑爆了数据库直接进入只读模式线上业务全军覆没。这种事故我见过太多次也亲手处理过太多次今天把binlog删除这件事从头到尾好好捋一遍希望能帮你在磁盘报警之前就把问题解决掉。本文面向数据库管理员、后端开发、运维同学以及所有正在用MySQL但是对binlog管理没什么概念的朋友。1. binlog是什么为什么它会成为磁盘杀手1.1 记录了什么在什么场景下会产生binlog全称Binary Log也就是二进制日志。MySQL里每一次对数据有修改的操作——INSERT、UPDATE、DELETE、CREATE TABLE、ALTER TABLE这些都会记录到binlog里。SELECT查询不会记录。它记录的不只是哪一行变了而是整个变更事件本身包括执行时间、操作类型、涉及的表、变更前后的数据快照取决于binlog格式。你可能会想一个UPDATE语句在数据库里跑了一下日志能有多大事但你得知道binlog是追加写的文件会螺旋式增长。生产环境里如果有一张大表做批量更新一次更新几百万行ROW格式下这几百万行的前后镜像全部写进binlog一个文件瞬间就能涨到1GB甚至更大非常恐怖。1.2 binlog的实际用途决定了它不能随便删binlog主要有三个用途主从复制从库通过读取主库的binlog来同步数据删掉从库还没消费的binlog从库直接断链。数据恢复通过mysqlbinlog工具把某个时间点的binlog重放到数据库里找回误删的数据。审计追踪通过binlog查看某个时间窗口内发生了什么操作不过一般审计都走general log用binlog做审计的比较少。所以如果你在从库还在同步的情况下把binlog文件全部删除从库就会因为找不到下一个日志文件而报错——Got fatal error 1236 from master when reading data from binary log。这就是删binlog最典型的翻车现场。1.3 为什么每天的binlog增长量远超你的预期很多人的MySQL实例一跑就是几年从没看过binlog到底占了多少空间。我截过一个真实的例子一台普通的业务库日均写入量不算夸张但保留了整整30天的binlog占掉了差不多200GB磁盘。原因有几类ROW格式下一次大事务会产生极大的binlog体积。长事务会阻塞binlog文件的轮转binlog文件要等事务提交才算结束长事务期间所有日志都堆在同一文件里。频繁的小事务导致日志文件数量暴涨单文件虽然默认1GB但小文件几十上百个占的inode也很可观。总之不管哪种情况最终结果都是磁盘被binlog蚕食。所以管理binlog的空间是每个MySQL使用者必须掌握的技能不是DBA的专属任务。2. 删除binlog之前先把这三件事确认清楚这一节非常关键因为绝大多数误删事故都发生在没看状态就直接purge的情况下。就算你只是执行一条PURGE BINARY LOGS命令操作前也必须做下面三步。2.1 检查当前有哪些binlog文件先登录MySQL执行SHOW BINARY LOGS;输出结果是binlog文件的清单类似这样---------------------------------------- | Log_name | File_size | Encrypted | ---------------------------------------- | mysql-bin.000001 | 156 | No | | mysql-bin.000002 | 51234 | No | | mysql-bin.000003 | 98765 | No | ----------------------------------------Log_name是文件列表File_size是字节数。通过这个清单你能直观看到binlog占用的空间和文件数量。我在实际操作中会顺便用SELECT SUM(file_size) FROM performance_schema.file_summary_by_instance WHERE FILE_NAME LIKE %binlog%;这类方式汇总一下总占用不过大多数情况下SHOW BINARY LOGS已经足够说明问题了。2.2 确认主从复制的消费进度如果MySQL有从库这一步绝不跳过。在从库上执行SHOW SLAVE STATUS\G重点关注两个字段Master_Log_File从库当前正在读取的主库binlog文件名。Read_Master_Log_Pos从库已经读到该文件的哪个位置。翻译成人话就是从库已经消费到哪一个binlog文件了你删除的时候就必须保留到这个文件为止。比如说从库读到了mysql-bin.000003而你一上来就PURGE BINARY LOGS TO mysql-bin.000004那么从库在000003后面的所有数据全都拿不到了复制链路当场断裂后续只能重建从库。在MySQL 8.0里从库没有消费掉的binlog其实主库自己也知道——它有一个名为slave_worker_info的内部机制和dump thread的交互但保险起见我们永远不依赖主库自己去保护binlog而是手动确认。另外还要注意级联复制的情况。如果A库是B库的主库B库又是C库的主库那么删除A库binlog的时候不光要看B库的消费进度还要看C库的消费进度。因为B库作为中继它从A库拉取的binlog最终要传给C库。你不能只看直接从库的状态。2.3 检查是否开启了GTID模式MySQL 5.6及以后支持GTID全局事务标识符在GTID模式下事务的唯一标识是用GTID: 1-12345这样的形式表示的。GTID模式下清理binlog要注意一个问题如果你用PURGE BINARY LOGGS TO指定文件名MySQL会校验这个文件里的GTID是否已经被执行过正常来说它不会让你把未执行的GTID对应的binlog删掉。但如果你用PURGE BINARY LOGS BEFORE指定时间并且时间设置太激进就可能导致后续搭建新从库时找不到合适的binlog起点。所以操作前执行一下SHOW VARIABLES LIKE gtid_mode;如果结果是ON说明开启了GTID操作时谨慎一点如果不是保持默认。这个检查花不了十秒钟但是能让你对删哪些文件是安全的有一个更准确的判断。3. 三种主流删除方式与他们的适用场景3.1 用PURGE命令精确清理最推荐MySQL提供了一个专门用于清理binlog的命令PURGE BINARY LOGS。它有两种写法。写法一删除指定文件之前的所有binlog。PURGE BINARY LOGS TO mysql-bin.000010;意思就是保留mysql-bin.000010以及之后的所有文件000010之前的全部删掉。写法二删除指定时间之前的所有binlog。PURGE BINARY LOGS BEFORE 2024-12-20 00:00:00;意思是在2024-12-20 00:00:00之前创建并关闭的binlog文件会被删除。我来解释一下这两个命令背后的逻辑。PURGE TO的语义是基于文件名的它只知道文件名之间的顺序不会管你这个文件里的数据是不是还没被从库消费。所以之前强调的从库状态检查就非常重要了如果从库还在读000010之前的某个文件你把它带走了就出事。PURGE BEFORE的语义是基于时间戳的。MySQL会找到binlog文件里面记录的最新写入时间然后对比你给出的时间点把在这个时间点之前完成写入的文件删掉。注意是文件完整写入才算不是文件里有这个时间点的数据就算。如果一个binlog文件跨过了你指定的时间点那么这个文件就不会被删除。实际使用中我更喜欢用PURGE BINARY LOGS TO因为它更可控。你先把从库状态查出来找到从库正在读的那个文件然后指定一个比它大一些的文件名去purge即可逻辑很直观不容易产生歧义。时间版本容易踩坑因为文件完成写入的时间跟你直觉上理解的时间不完全一样。3.2 设置自动过期清理一劳永逸手动purge的问题在于你得定期去看磁盘空间然后手动执行命令。这真的是上个时代的玩法了。MySQL本身提供了过期自动清理机制。在MySQL 8.0里控制binlog保留时间的参数有两个不同版本有变化需要区分清楚expire_logs_days以天为单位默认值是0表示永不过期。MySQL 8.0.3及之后标记为废弃。binlog_expire_logs_seconds以秒为单位默认值是259200030天在MySQL 8.0.3及之后开始提供。MySQL 8.0.3之后的版本官方推荐使用binlog_expire_logs_seconds它比expire_logs_days控制得更细可以精确到秒。你设置一个合理的保留时长比如7天604800秒数据库会自动清理超过这个时间且已经关闭的binlog文件。-- 查看当前值 SHOW VARIABLES LIKE binlog_expire_logs_seconds; SHOW VARIABLES LIKE expire_logs_days; -- 动态设置临时生效重启后失效 SET GLOBAL binlog_expire_logs_seconds 604800; -- 持久化到配置文件永久生效修改配置文件[mysqld] binlog_expire_logs_seconds 604800修改配置文件后需要重启MySQL才生效。如果你用的是云数据库RDS等一般有控制台的参数设置入口改一下参数组就行不需要自己重启。从实际操作看自动清理能解决80%的binlog撑爆磁盘问题。你不需要知道binlog什么时候会被清掉只需要设置好磁盘容量和保留天数的关系就行。3.3 直接删除文件强烈不推荐有人习惯直接到磁盘目录下把binlog文件删了比如rm -f /var/lib/mysql/mysql-bin.000009这种做法绝对禁止。原因很简单MySQL的binlog文件是索引写入的直接删除磁盘文件不会同步更新mysql-bin.index索引文件MySQL在下次写入或者崩溃恢复时就会发现文件缺失整个binlog链路就乱了。更严重的情况下主从复制直接失败数据库某些内部状态也会异常。有人可能又说我删完重启MySQL就正常了——那是因为重启时MySQL重新扫描了磁盘文件并重建了索引有一种侥幸的成分。但如果碰上了复制关系里某个从库刚好要拉取你删掉的那个文件你连补救的机会都没有。真到了磁盘快满、来不及执行SQL命令的极端场景正确姿势是用PURGE而不是去rm。如果MySQL因为磁盘满已经写不了binlog了你还能不能执行PURGE答案是可以。虽然写binlog失败导致部分事务提交失败但清理binlog本身用的是独立的日志系统通常还是能执行的。如果真的连PURGE都执行不了那就先把磁盘上的归档文件比如老的mysql-bin.*压缩包挪走释放出空间再进MySQL执行purge。4. 三个高频场景的完整操作实例4.1 场景一磁盘剩余不足10%紧急清理假设我的服务器磁盘总共200GBbinlog占了180GB剩余空间不足10%。数据库可能随时因为磁盘满进入只读状态。我按下面这套流程操作。第一步先确认从库状态通过网络不行就在主库本机查SHOW PROCESSLIST;看有没有从这个主库拉binlog的从库连接。如果有去从库执行SHOW SLAVE STATUS看消费到哪个文件。第二步在从库上找到Master_Log_File字段的值假设是mysql-bin.000020。那么主库上能删的最多只能到mysql-bin.000020之前的文件。你可以在主库上执行PURGE BINARY LOGS TO mysql-bin.000021;把000021之前的所有文件清掉。如果连接正常这条命令执行得很快删除大文件可能需要点时间但不会锁表不会影响业务。第三步再次执行SHOW BINARY LOGS确认文件数量减少、磁盘空间释放了。如果释放后磁盘空间还是紧张继续查看binlog文件列表决定是否要把保留时间缩短或者手动再删多一点。实际上这种情况我遇到过不止一次按照这个顺序操作最惊险的一次距离数据库只读只剩不到1GB空间最终还是救回来了。4.2 场景二Docker容器里的MySQLbinlog把宿主机磁盘写满搜索关键词里看到好多人在问Docker里面MySQL的binlog恢复和清理问题这里专门说一下。Docker跑MySQLbinlog文件默认存在容器内部的数据目录一般是/var/lib/mysql/。如果你在启动容器时用了数据卷挂载-v /host/mysql-data:/var/lib/mysql那么binlog就在宿主机的/host/mysql-data目录下。如果没有挂载binlog就在容器可写层里删容器就没了但容器本身会占据宿主机磁盘空间。在Docker环境里执行清理最干净的方式还是进到MySQL里执行PURGE命令docker exec -it mysql-container mysql -uroot -p然后执行跟普通环境一样的PURGE BINARY LOGS TO mysql-bin.0000xx;。需要注意的事项Docker容器日志也可能撑爆磁盘不全是binlog的锅。清理binlog的同时建议看看/var/lib/docker/containers/container-id/*-json.log的大小这个文件才是Docker里经常背锅的磁盘杀手。容器里的MySQL会自动启用binlog过期配置很多时候生产环境拉起的容器参数没设置binlog_expire_logs_seconds默认值可能非常大甚至不清理所以Docker容器更要注意主动配置。如果容器配置文件没挂载出来修改参数会比较麻烦。建议启动容器时把/etc/mysql/配置目录和/var/lib/mysql数据目录都挂载出来方便调整。Docker的好处是环境隔离坏处是磁盘管理容易忽略。你容器随便跑一两个月binlog可能就把宿主机的根分区塞满所以如果你在用容器跑MySQL清binlog应该纳入周期性检查清单。4.3 场景三purge之后从库报1236错误先说结论如果从库还没消费的binlog被purge掉了那么从库复制就会报错错误码1236。表现形式是从库的SHOW SLAVE STATUS里的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 file这个错误只靠START SLAVE是解决不了的必须想办法把从库恢复到跟主库一致的状态。常见思路有两种如果从库上数据不太重要直接重建从库——从主库做一次全量备份恢复到从库然后重新设置复制位点。如果想尝试用mysqldump备份恢复然后设置位点为当前主库的binlog文件和position。这里特别提醒一主多从的架构下你删binlog前必须检查所有从库而不是只看一个。有些团队维护了多个从库A从库消费到了000021B从库因为网络问题只消费到000015。你按A的进度去purgeB就废了。5. 删除binlog之后数据恢复靠什么5.1 binlog删了还能恢复吗这个问题本质上是你删掉binlog之后某张表的数据还能不能通过binlog找回。如果你的恢复策略完全依赖binlog的连续文件那么被删掉的那部分binlog里的数据理论上无法通过binlog找回了。磁盘层的文件恢复工具另说但那是另一个领域不是MySQL层面能承诺的。所以很多生产环境的核心库binlog只是恢复链路上的一环真正的心脏其实是全量备份。全量备份比如mysqldump或物理备份必须周期性执行binlog是在全量备份基础上增量的补充。假设你每周日凌晨做一次全量备份那么恢复流程是用全量备份把数据库恢复到上周日凌晨的状态。把备份时间点之后到故障发生之前的binlog逐个用mysqlbinlog回放。数据库回到了故障前的某个时间点。如果这个链路里某一环的binlog被删了那么回放只能到被删位置之前数据恢复不完全。所以管理binlog保留时长要跟全量备份周期配起来。比如你每天凌晨做一次全量备份那么binlog留3~7天完全够用如果你一个月才做一次备份binlog只留一天恢复的时候就只能干瞪眼。5.2 从Docker里的MySQL恢复日志的正确打开方式热搜词里docker里面的mysql通过binlog恢复日志挺热门我顺便说下这个操作。假设你的MySQL跑在Docker容器里你需要把binlog从容器里弄出来或者直接在容器里用mysqlbinlog工具解析# 进入容器 docker exec -it mysql-container bash # 在容器内解析binlog mysqlbinlog --no-defaults /var/lib/mysql/mysql-bin.000008 | mysql -uroot -p或者把binlog先拷到宿主机再解析docker cp mysql-container:/var/lib/mysql/mysql-bin.000008 /tmp/ mysqlbinlog /tmp/mysql-bin.000008 | mysql -uroot -p解析出来的是什么是SQL语句或者事件数据。你可以通过--start-datetime和--stop-datetime参数只回放某个时间段的日志mysqlbinlog --start-datetime2024-11-01 09:00:00 --stop-datetime2024-11-01 09:30:00 /var/lib/mysql/mysql-bin.000008 | mysql -uroot -p注意一点如果你要恢复的那个时间段跨了多个binlog文件需要把这些文件按顺序全部传给mysqlbinlog比如mysqlbinlog mysql-bin.000008 mysql-bin.000009 mysql-bin.000010 | mysql -uroot -p而不是只恢复一个文件否则数据会缺一大块。5.3 为什么说清理binlog和保留binlog要一起考虑写到这里你可能会觉得我好唠叨——一会儿说binlog怎么删一会儿说binlog删了就没法恢复这不是自相矛盾吗不矛盾。清理是空间维度的需求保留是安全维度的需求两者商量出一个平衡点就是binlog管理的本质。我个人习惯的配置组合是这样的备份策略每天凌晨全量备份一次mysqldump或xtrabackup视数据量和存储引擎而定。binlog保留7天即binlog_expire_logs_seconds604800。磁盘规划binlog预留的空间约等于日均binlog增量乘以10留点余量。这样的配置下binlog占用的空间是可控的恢复能力窗口是7天完全覆盖了备份周期。如果有人误操作删了数据最多丢失当天凌晨备份之后到误操作之前的数据用binlog就能找回来。这个平衡对大多数中小业务足够了。6. 常见的坑和小技巧汇总6.1 大事务导致单个binlog文件异常庞大前面提过长事务会阻塞binlog文件轮转这里详细说一下。MySQL的binlog文件达到max_binlog_size参数设定的大小默认1GB之后会停止写入并切换到下一个文件。但这个切换的前提是当前没有未提交的事务。如果一个事务非常大大到超过1GB那么这个事务产生的日志就会冲破这个文件的上限binlog文件可能长到好几个GB。这会导致一个问题PURGE BINARY LOGS TO很难精细控制——一个超大文件就占了几GB空间但你没法只删掉这个文件里的一部分内容MySQL清理binlog的最小单位是文件。所以生产环境一定要注意控制大事务尽量分批提交既能减少锁竞争又能让binlog文件体积保持健康。6.2 binlog文件删不掉可能是从库还挂着有时候你执行PURGE BINARY LOGS TO发现报错ERROR 1794 (HY000): Slave is not configured or failed to initialize properly.或者删除不完全是因为有从库的连接还挂着。这个情况我刚才在确认从库进度部分讲过了但这里再补充一点判断技巧如果从库因为网络原因断连很久主库上dump线程可能已经退出了。你以为没有从库了但MySQL的binlog清理会去检查SHOW SLAVE HOSTS或者复制元数据发现还有未消费的binlog文件就不会删。此时要么等从库连上来消费完要么彻底解除这个从库在MySQL元数据里的注册关系才能继续purge。6.3 使用PURGE的语法要写对有同学会把命令写成PURGE MASTER LOGS TO这在MySQL 8.0以前的版本中是可用的PURGE MASTER LOGS是PURGE BINARY LOGS的同义词。但8.0之后官方文档明确推荐使用PURGE BINARY LOGSPURGE MASTER LOGS虽然还在但属于废弃状态。建议统一用PURGE BINARY LOGS避免以后版本升级遇到问题。同样查看binlog列表也可以用SHOW MASTER LOGS但更规范的是SHOW BINARY LOGS。把命令写规范看文档的时候也对得上少踩很多坑。6.4 删完binlog记得顺手检查一下binlog indexMySQL维护了一个mysql-bin.index文件里面记录所有binlog文件的路径。PURGE命令会自动更新这个文件但如果哪天因为断电、异常重启等导致索引和实际磁盘文件对不上可能会造成不必要的麻烦。执行完purge之后可以这样检查cat /var/lib/mysql/mysql-bin.index确认里面列出的文件都真实存在于磁盘目录里。如果索引里列了不存在的文件MySQL读取时可能会报错。这种情况修复起来比较麻烦好在正常purge不会遇到但检查完心里踏实。6.5 别忘了一种特殊binlogrelay log最后补充一下有的同学把relay log跟binlog混为一谈。relay log是从库上的中继日志它记录的是从主库拉取过来的binlog事件。从库执行完relay log里的内容后这些中继日志可以安全地自动清除由relay_log_purge参数控制默认是开启的。如果你发现从库磁盘也被日志占满了除了binlog之外还要看看relay log是不是堆积了。从库IO线程从主库拉取日志但SQL线程执行不及时relay log就会越积越多。清理思路是先排查SQL线程为什么执行慢大事务、缺少索引、复制延迟然后重启SQL线程或STOP SLAVE; START SLAVE;让它追赶依靠自动清理机制把relay log清掉。7. 写在最后我的binlog清理习惯每次有人问我binlog删不掉、磁盘满了怎么办我都会强调同一个观点binlog清理不是一次性的救火操作而是一个持续性的运维习惯。我的个人实践是这样的先在配置文件里设好binlog_expire_logs_seconds让MySQL自己去清理过期文件然后把备份周期和保留窗口匹配好确保数据恢复链路完整最后每个月手动检查一次SHOW BINARY LOGS确认一下文件数量是否正常。如果某天磁盘空间突然被大量占用第一时间不是想着删文件而是先查binlog生成速度为什么飙升看看是不是有大事务、是不是有从库断连、是不是备份脚本触发了大量更新。找到根因之后再做清理才能防止同样的事故反复出现。再分享一个小技巧可以把binlog目录放到独立的分区或者独立的磁盘上比如挂载到/data/mysql-bin/这样binlog即使疯长也不会把系统盘塞满影响操作系统的正常运行。这个设计在大数据量、高写入的场景下非常实用属于性价比极高的架构改进。binlog是MySQL数据安全的护城河但任何一条河都需要疏浚不然就会泛滥。希望这篇文章能帮你把binlog管理这面墙砌牢。