MySQL Binlog磁盘清理:自动过期、PURGE及避坑指南
发布时间:2026/10/16 13:02:15 锦皓数字建站

1. 为什么Binlog会悄悄吃满磁盘1.1 Binlog写入链路与磁盘消耗来源先聊一个我经常被问到的问题明明业务没怎么变磁盘怎么就满了十有八九罪魁祸首就是MySQL的Binlog二进制日志。Binlog是MySQL Server层维护的一种逻辑日志它记录的是所有导致数据变化的SQL语句或行变更信息比如INSERT、UPDATE、DELETE甚至DDL。它的用途主要有两个一是主从复制从库靠拉取主库的binlog来重放变更二是时间点恢复PITR当全量备份之后出现误操作你可以用binlog把数据恢复到某个具体时间点。但Binlog有个特性——它默认是持续追加的。只要实例在跑主库有写操作binlog文件就会一直增长。MySQL不会主动帮你删除旧文件除非你设置了过期清理参数或者手动执行清理命令。在默认配置下很多新手部署MySQL时根本没管这个参数结果就是binlog像一个无底洞一天几GB甚至几十GB地涨。我见过一个真实的案例某个系统的MySQL数据目录总共才200GB结果binlog占了160GB而且还在以每天5GB的速度增长。因为业务方根本没有配置过期策略MySQL默默攒了几百个binlog文件最后磁盘报警数据库直接进入只读保护状态业务全线受影响。这个场景我相信不少人都经历过所以binlog清理不是要不要做的问题而是必须提前规划的问题。1.2 清除前必须想清楚的三大问题在你动手清理之前我建议先停下来想清楚三件事否则很容易出现清了日志、丢了数据的悲剧。第一个问题你的备份策略是什么Binlog并不仅仅是日志它还是数据恢复的重要一环。如果你的全量备份是每周一次那么从上次全量备份到现在的binlog都不能删否则一旦数据损坏你只能恢复到上周的状态中间这几天的数据全没了。正确的做法是先确认全量备份成功且可恢复binlog保留天数覆盖了备份周期通常建议保留时间至少是备份间隔的两倍。第二个问题你的从库是否跟得上主库如果主库有从库在异步复制而主库的binlog已经被清理了从库正好还没拉到那个位置从库会永久性地追不上主库只能重建从库。所以清理binlog之前一定要确认所有从库的IO线程都已经把需要的binlog拉走了。这一点我再往后细说这里先记住一个判断原则主库binlog清理的下限必须晚于所有从库已拉取的位置。第三个问题最近的业务高峰是否已经过去如果你在业务高峰期清理binlog新产生的binlog会迅速重新填满磁盘你会陷入清完立刻又满的循环。而且清理过程中产生的磁盘IO和锁竞争虽然不大但没必要在高峰期给自己的数据库添堵。我会建议在业务低峰期做清理操作并且把清理脚本放入定时任务而不是等磁盘报警了才手动处理。想清楚这三个问题你再往下看具体的三种方法思路会清晰很多。2. 方法一自动过期——最省心的兜底方案2.1 expire_logs_days与binlog_expire_logs_seconds的区别与选择自动过期是最常用的方式核心思路就是告诉MySQLbinlog只保留N天超过N天的文件自动删除。在MySQL 5.7及更早版本里参数是expire_logs_days默认值是0表示不自动清理。这个参数的单位是天比如设置7就表示保留7天。在MySQL 8.0里官方做了一个升级增加了binlog_expire_logs_seconds参数单位是秒精度更高。默认值是2592000也就是30天。同时expire_logs_days在MySQL 8.0里已经被标记为废弃虽然还能用但会有告警而且这两个参数同时存在时MySQL会优先使用binlog_expire_logs_seconds。我的建议很简单MySQL 5.7就用expire_logs_daysMySQL 8.0就直接用binlog_expire_logs_seconds别混着配。还有个细节容易被忽略expire_logs_days的清理不是实时触发的也不是精确到秒级执行的。MySQL在binlog写入时、以及某些特定时机才会检查是否需要删除旧的binlog文件。所以你会发现即使设置了保留7天某些binlog文件可能已经超过了7天才被删除这是正常现象不必恐慌。2.2 配置示例与验证方法配置文件方式一般在my.cnf或my.ini的[mysqld]段下追加# MySQL 5.7及以下 expire_logs_days 7 # MySQL 8.0推荐 binlog_expire_logs_seconds 604800修改后需要重启MySQL服务生效或者在运行时用SET GLOBAL动态修改不需要重启-- 动态修改立即生效但重启后失效需要同时写入配置文件 SET GLOBAL binlog_expire_logs_seconds 604800;修改完怎么验证执行SHOW VARIABLES LIKE binlog_expire_logs_seconds; SHOW VARIABLES LIKE expire_logs_days;再查看当前binlog列表SHOW BINARY LOGS;你会看到类似这样的输出最后一列是日志大小但没有显示创建时间。想看得更清楚可以直接看磁盘文件ls -lh /var/lib/mysql/mysql-bin.*如果文件时间已经超过了保留期但还没有被清理不用急MySQL会在合适的时机触发清理你也可以手动执行一次PURGE来加速下一章会讲。2.3 为什么有人配置了却不管用这是我遇到的坑里比较高频的一个明明设置了expire_logs_days7磁盘上的binlog文件却越来越多一个都不删。排查下来发现原因大多是下面几种。第一种参数名写错了。我看到过有人写成expire-logs-days横线或者binlog_expired_logs_seconds这些名字MySQL根本识别不了等于没配。第二种主从复制状态异常。假如从库的IO线程一直没追上主库主库会认为从库还需要某些binlog即使这些binlog已经超过了过期时间MySQL也不会删除。这是一种保护机制。所以当你发现binlog没有按预期清理时不要只想参数先看一下从库状态SHOW SLAVE STATUS\G重点关注Slave_IO_Running和Master_Log_File。如果IO线程是Yes但是Master_Log_File还停留在很久之前的文件那说明从库拉取进度滞后binlog被保护了。这种情况的解决办法是修复从库复制或者如果你能接受从库重建可以重新配置从库然后超期的binlog会被自动清理掉。第三种在MySQL 8.0中同时设置了两个过期参数。前面说过8.0优先用binlog_expire_logs_seconds如果你只设置了expire_logs_days虽然MySQL会显示该参数已废弃但清理逻辑并不生效。这时候必须显式设置binlog_expire_logs_seconds。第四种binlog的默认保留期本来就是30天。很多人在MySQL 8.0里以为没配就不会自动清理其实8.0默认配置就是30天过期只是如果30天积累的数据太大磁盘仍然可能爆。所以真正的问题不是没清理而是清理的频率和保留时长不匹配你的磁盘容量。配好自动过期之后磁盘水位会趋于稳定但别以为这就万事大吉了。你还需要监控binlog的总大小和磁盘水位防止出现单日写入量大导致保留期内binlog总大小就超过磁盘容量的情况。这种场景下自动过期解决不了燃眉之急就需要下面的手动清理方法来应急。3. 方法二PURGE BINARY LOGS——手动精准清理3.1 核心语法与常规用法手动清理的核心命令是PURGE BINARY LOGS。它比直接删除文件安全得多因为MySQL会同步更新binlog索引文件就是记录所有binlog文件名的那个.index文件不会出现目录里有文件但MySQL不知道的情况。常规用法有两种写法-- 删除指定文件之前的所有binlog不包含该文件 PURGE BINARY LOGS TO mysql-bin.000010; -- 删除指定时间点之前的所有binlog PURGE BINARY LOGS BEFORE 2024-01-01 00:00:00;第一种TO语法很好理解就是保留到某一个文件这个文件之前的全部删掉。第二种BEFORE语法稍微需要小心时间点之前生成的binlog都会被删除但这个时间点不是文件创建时间而是binlog里记录的事件时间。实际工作中我用TO语法更多一些因为它的语义更明确。我通常先执行SHOW BINARY LOGS;看看当前有哪些文件再决定保留哪些。比如输出是mysql-bin.000001 mysql-bin.000002 ... mysql-bin.000018如果磁盘快满了你决定只保留最近3个文件那就执行PURGE BINARY LOGS TO mysql-bin.000016;这样000001到000015会被删除000016到000018保留。注意是截止到某个文件它之前全删所以你要保留N个文件就应该把TO后面的文件写为从最新文件往前数第N1个。3.2 BEFORE语法与时区陷阱PURGE BINARY LOGS BEFORE有一个容易出现偏差的地方时间参数。binlog内部记录时间使用的是UTC标准时间但你在命令行里输入的时间通常被MySQL按会话时区解析。如果你的服务器时区恰好是UTC8而你没做转换直接写PURGE BINARY LOGS BEFORE 2024-01-01 08:00:00;实际上MySQL会把你输入的时间解析后按UTC记录处理结果可能是你想删8点之前的日志但实际删除了UTC时间8点即本地时间16点之前的日志。范围比你预想的大8个小时如果这8个小时内有你需要的数据就很难受了。我建议的方式是先查看当前时区配置SHOW VARIABLES LIKE %time_zone%;然后在执行清理时明确指定与UTC对齐的时间。更稳妥的办法是先查询binlog里的事件时间比如执行SHOW BINLOG EVENTS IN mysql-bin.000016 LIMIT 1;看事件里的时间戳确认好时间界线再清理。让我说得更具体一点。假设你想删除本地时间2024年1月1日00:00:00之前的所有binlog先看看时区如果system_time_zone和time_zone都是UTC8那BEFORE后面直接写2024-01-01 00:00:00就是本地时间没问题。如果服务器时区是UTC但你业务上习惯用北京时间那你要写2023-12-31 16:00:00因为这样才是UTC 2024-01-01 00:00:00。这个坑我在自己维护的一台云服务器上踩过一次。当时服务器默认时区是UTC业务方让我清理今天之前的日志我用本地概念写了BEFORE时间结果多删了几个小时的日志还好后续通过备份完成了数据补齐但那段经历让我现在清理之前一定会先看时区。3.3 主从复制场景下清理的监测与自我保护如果你有从库使用PURGE之前一定要先确认从库的同步状态否则会造成从库断粮。从库有一个Relay_Log中继日志用于暂存从主库拉取的binlog但它的最终数据源还是主库的binlog文件。如果主库已经PURGE了某个binlog而从库的IO线程还没拉到这个文件那从库就会一直报错relay log read failure或者Received error reading packet之类的问题等你发现的时候从库已经落后主库十万八千里只能重建。所以在任何一次手动清理之前我都建议执行这个检查流程-- 在主库上执行 SHOW MASTER STATUS;记录下当前正在写的binlog文件名和位置。-- 在从库上执行 SHOW SLAVE STATUS\G重点看三个地方Slave_IO_Running是否等于YesSlave_SQL_Running是否等于YesMaster_Log_File是否等于主库上看到的当前binlog文件或者至少是在你准备PURGE的截止文件之后只有确认所有从库的Master_Log_File都已经读到了你准备保留的那个文件或更新的位置你才可以放心地执行清理。如果有从库落后严重建议先处理从库问题而不是强行清理主库binlog。4. 方法三直接删文件的误解与正解4.1 为什么直接rm是高危操作很多人觉得清理binlog最直接的方式就是rm -f /var/lib/mysql/mysql-bin.000001试过之后你会发现当时看起来空间释放了但没过多久MySQL报了各种怪异的错误比如找不到binlog文件、复制中断、甚至实例启动失败。问题出在哪因为MySQL的binlog索引文件mysql-bin.index里仍然记录着已被删除的文件名。MySQL每次写binlog、恢复binlog列表、或者启动实例时都会去读这个索引文件。如果索引文件指向的文件在磁盘上不存在MySQL就会报错。更麻烦的是如果你删除的binlog恰好是某个从库正在拉取的文件从库会立刻断连而且很难无痕续上。如果你删除的是当前正在写的binlog文件MySQL会继续往一个已被删除的文件里写数据虽然磁盘上文件消失了但空间不会真正释放因为进程还持有这个文件的文件句柄。所以我的结论很明确不要直接对binlog文件执行rm除非你同时准备手动修改binlog索引文件而这一步对绝大多数人来说风险太高不值得。唯一一种可以考虑直接操作文件的场景是MySQL已经彻底停止服务而且你确定要丢弃所有未同步的binlog。即便如此也需要谨慎处理好索引文件否则启动一样会失败。4.2 用RESET MASTER清空所有Binlog的适用场景如果你确实想一次性清空所有binlogMySQL提供了一个命令RESET MASTER;这个命令会删除所有现有的binlog文件并重新从mysql-bin.000001开始编号。注意它威力极大执行后当前实例的binlog将全部丢失主从复制中从库的复制位置信息也会失效因为binlog文件名和位置都重置了。适用场景其实非常有限重新初始化一个全新的主从复制架构所有从库都将从主库的全量备份重新开始旧binlog没有保留价值。排障时确认所有binlog文件都已没有保留价值的情况。某个实例要重新作为新的主库并且不依赖旧binlog做恢复。常规生产环境中我会尽量避免使用RESET MASTER。因为一旦你后面意识到还需要某个时间点之前的binlog或者从库恢复需要特定位点你已经无法找回。相比之下PURGE至少还能保留最新的一部分文件。还有一点需要注意如果你在从库上执行了RESET MASTER它会清空从库的master.info中的复制元数据信息实际上从库的复制位置通常记录在relay-log.info和应用状态里但无论如何在从库上执行RESET MASTER都不是一个好主意我见过有同学在从库上误执行RESET MASTER导致复制配置被搞乱的情况。4.3 磁盘空间不释放的排查与处理有同学会遇到这样的情况执行了PURGE或设置了自动清理但磁盘空间依然没有减少。排除掉binlog文件还在别的目录这种低级问题之后最常见的原因是MySQL进程还持有已删除文件的文件句柄。这通常发生在你直接rm了正在使用的binlog文件或者文件被删除但某个线程仍然在读取它。解决办法是先查看MySQL进程打开的已删除文件lsof | grep deleted | grep mysql如果看到一堆(deleted)的文件说明这些文件虽然从目录里看不到了但空间并没有释放。一个应急手段是重启MySQL服务重启后句柄释放空间才会真正归还给操作系统。但重启数据库不是小事必须在业务低峰期做而且要先确认实例能正常关闭、正常启动。还有一种可能你删了binlog文件但是没删干净——比如磁盘上还有大量未归档的其他日志慢查询日志、错误日志、undo表空间等。这种情况要单独排查不能只盯着binlog。我维护的一个实例遇到过类似情况binlog清理后磁盘空间只释放了一小部分一查发现是慢查询日志因为某个业务查询失控几天内涨了20GB。所以遇到清完还不释放的情况我建议用du逐个目录排查du -sh /var/lib/mysql/* | sort -rh | head -20找出真正的大头而不是盲目重复清理binlog。5. 避坑指南那些年我们在Binlog清理上踩过的坑5.1 从库还在读就清理了Binlog这是我重点想强调的一个场景。某个实例的主库配置了自动过期清理也没人注意从库的同步进度结果有一天从库突然报错复制线程一直处于Connecting状态IO线程断掉错误日志提示无法从主库读取binlog文件。原因就是在某个时间点主库的expire_logs_days已经把所有从库还没拉取的binlog自动删了。MySQL的自动清理并不会智能判断从库是否已消费它只会在主从复制还健康的前提下判断从库的Master_Log_File是否落后于保留窗口。如果落后太多超出保留范围它照样会清理因为对主库来说那已经是过期文件了。教训是主库和从库的配置不能完全割裂主库binlog保留时间必须考虑到从库可能的最长落后时间。建议给主从同步加监控告警从库Seconds_Behind_Master超过阈值要能第一时间收到报警。从库落后超过保留期后修复从库不是一个简单的继续追就能解决的大概率需要重新拉取全量备份。5.2 Binlog与数据文件同目录的空间危机默认安装的MySQL会把binlog放在datadir下也就是和数据文件、ibdata等放在同一块磁盘上。这本身没问题但你得对总容量有概念。之前遇到过一个案例某个实例的数据目录磁盘是1TB数据文件占了700GBbinlog又积累了400GB直接把磁盘干满了。因为binlog的自动清理参数当时没有配置也没人及时发现磁盘水位的上涨趋势。磁盘满了之后MySQL写入失败事务无法提交服务直接不可用。更要命的是因为磁盘满MySQL自己也无法正常启动恢复过程相当痛苦最后是在维护模式下移动了部分文件才救回来。建议做法是要么把log-bin放到独立的磁盘/分区上要么至少给binlog目录设置独立的监控告警磁盘使用率达到70%就要提醒达到85%就要强告警。而且binlog预留空间要根据业务的日写入量来评估比如日写入量5GB你至少预留3天的量也就是15GB如果还想保留7天那就是35GB。把这些数据算清楚你才有底气设定binlog_expire_logs_seconds。5.3 清理Binlog后误以为数据丢失新手容易把binlog和数据文件混淆觉得binlog被清理了我的数据是不是就没了一部分严格来说binlog清理不会影响你在数据库里能查到的最新数据。数据库的当前数据保存在表空间文件里binlog只是用来做增量恢复和复制的日志。清掉旧binlog丢失的是回到过去某个时间点的能力而不是当前数据。所以如果你整天频繁做全量备份对binlog的依赖会小很多反之如果你全量备份很少binlog是唯一的时间点恢复手段那就要额外谨慎。但也有一个微妙场景某个实例开启了GTID模式如果你清理了binlog而之前的一些GTID事务没有被其他方式记录下来那么在使用某些工具做基于GTID的复制恢复时主库已经没有对应的GTID信息从库可能无法自动定位。这个属于进阶话题但提醒你注意到GTID模式下binlog清理策略和基于文件名的复制模式下恢复思路是略有差异的。5.4 常见问题速查表为了让你以后排查更方便我把常见问题整理成了一张表现象可能原因处理建议设置了过期参数但不生效参数名称错误或8.0同时设置了废弃参数检查变量名统一用binlog_expire_logs_secondsbinlog不清理且从库IO线程正常从库拉取进度落后于保留窗口检查Master_Log_File位置追上或重建从库PURGE后磁盘空间没释放文件句柄未释放或有其他大文件占空间lsof查看deleted文件必要时重启实例du排查目录删除binlog后实例启动失败索引文件记录了不存在的文件不要直接rm用PURGE需要恢复时参考错误日志重建索引磁盘满导致实例不可写未配置自动过期或保留窗口内binlog体积过大紧急purge配置自动过期把binlog目录独立清理后从库复制中断主库删除了从库尚未读取的binlog重建从库并配置主库保留周期覆盖最大从库延迟这张表里的内容基本涵盖了我在生产环境中遇到的绝大多数binlog清理相关问题。建议你把这个表保存下来遇到对应现象可以直接按图索骥。5.5 一套可落地的清理监控方案最后分享一套我自己比较常用的落地思路不一定适合所有人但至少能避免磁盘满了才想起来清理的被动局面。在配置层面我会在my.cnf里写[mysqld] # 启用binlog log-bin mysql-bin # 8.0保留5天按你们的业务写入量自行调整 binlog_expire_logs_seconds 432000 # 控制单个binlog文件大小默认1G可调 max_binlog_size 1G在运维层面我会写一个简单的shell脚本每天检查binlog总大小和磁盘使用率超过阈值时自动执行PURGE BINARY LOGS TO并记录日志。脚本核心逻辑大概是这样#!/bin/bash DISK_THRESHOLD85 BINLOG_DIR/var/lib/mysql CURRENT_USAGE$(df -h $BINLOG_DIR | awk NR2 {print $5} | sed s/%//) if [ $CURRENT_USAGE -ge $DISK_THRESHOLD ]; then # 找到当前活跃的binlog往前推N个删除之前的文件 mysql -e SHOW BINARY LOGS | tail -n 2 | head -n -5 | \ while read binlog _; do mysql -e PURGE BINARY LOGS TO $binlog done fi注意这个脚本只是示例生产环境你要加上更严谨的错误处理和告警通知。脚本的思路是保留最近5个binlog文件如果磁盘水位超过阈值就删到只剩5个。不过我要提醒一句用脚本自动purge属于应急手段不能替代合理的自动过期配置。如果你发现自己的环境已经时不时需要手动purge说明保留天数设得太大了或者磁盘容量本身就不够要想办法扩容或缩短保留时长。我自己比较推荐的方式是双保险自动过期策略作为日常清理主力手动purge作为磁盘告警时的应急手段脚本只用于释放磁盘日常不依赖脚本删除binlog。这样做的好处是既有兜底又不会因为脚本bug把binlog误删太多。一点个人的体会做数据库运维的时间越久我越觉得清理binlog这件事看似操作简单真正要打理好却需要把备份策略、主从状态、磁盘容量、业务写入几个维度都串联起来。很多事故并不是某一条命令执行失败而是清理前少看了一眼状态或者清理后没有及时观察磁盘水位的变化。如果你现在还没有任何binlog清理策略我建议今天就去检查一下你实例的expire_logs_days或者binlog_expire_logs_seconds顺手看一眼SHOW BINARY LOGS;的输出再随手量一下binlog目录一共占了多少空间。十几分钟的时间可能会帮你避免一次磁盘爆满导致的生产事故。这比任何高深的技术技巧都更实在。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。