
前阵子有个朋友问我MySQL如何删除binlog日志文件我第一反应不是告诉他命令而是反问了一句你想删的是哪一部分、删完怎么恢复、现在有没有从库在跑。binlog这玩意儿看着只是一堆mysql-bin.0000xx文件背后牵着的却是复制和恢复这两条命脉。删对了是释放磁盘、恢复清爽删错了就是从库断供、恢复无门。这篇文章我就把binlog清理这件事掰开揉碎讲一遍从原理、判断、实操到排障把我这些年踩过的坑一并交代清楚。1. 先说透binlog到底是什么为什么清理它要这么谨慎1.1 binlog记录了什么为什么体积膨胀快binlog是MySQL的二进制日志全称Binary Log。它记录的是数据库层面“已经发生的变化”比如某条INSERT插入了一行、某条UPDATE改了哪些行、某条DELETE删掉了哪些数据。它不是记录SQL文本那么简单在ROW格式下它会把每一行的前镜像和后镜像都写进去。举个例子一条UPDATE语句批量更新了一万行在STATEMENT格式下可能只是一条语句文本体积很小但在ROW格式下它要把这一万行更新前后的完整数据都写进日志文件体积直接暴涨几十倍甚至上百倍。我见过不少业务库正常数据量也就几十GBbinlog却堆到了几百GB。原因往往就是这么几个一是binlog_format设置成了ROW这是现在最主流也最推荐的安全配置但代价就是日志体积大二是业务高峰期写入量太大每秒几万次变更binlog的写入速度跟得上文件数量也涨得快三是清理策略缺失默认配置下binlog可能永久保留时间一长磁盘就吃不消了。还有一个容易忽略的点binlog不仅主库会写从库如果开启了log_slave_updates也会生成自己的binlog。很多公司架构是一主多从每个节点的binlog都在各自磁盘上占空间巡检的时候只看主库不看从库结果从库磁盘悄悄满了直到同步报错才发现。1.2 为什么不能直接删文件索引、复制与恢复的隐性依赖很多人第一次遇到binlog占满磁盘第一反应是用rm命令直接删掉mysql-bin.0000xx文件。这绝对是大忌我用一次惨痛教训换来的结论。MySQL维护了一个binlog索引文件默认叫mysql-bin.index存放在数据目录下。这个文件就像一本目录册每一行记录一个binlog的文件名。MySQL启动、执行PURGE、进行复制拉取时都要靠这个索引来定位日志文件。如果你直接用rm删掉了某个binlog但索引文件里还留着它的记录那么整个日志链就断掉了。轻则SHOW BINARY LOGS列出的文件与磁盘实际文件对不上重则MySQL在切换日志或重启时报错甚至从库过来拉日志时发现要拉的文件不见了直接报1236错误复制中断。更关键的是binlog不只是用来做复制。它是数据恢复体系里不可或缺的一环。日常全量备份可能一天一次但如果某个时刻需要做时间点恢复光靠昨天的全量备份是不够的需要从备份时间点到故障时间点的binlog来补齐增量。binlog一旦被删这个时间窗口就永久缺失了数据只能恢复到备份时刻。所以删除binlog从来不是一个简单的磁盘清理动作它牵涉到恢复能力的边界。1.3 不适用场景哪些情况先别急着清理在动手之前先对照一下自己是不是下面这些情况。如果你正处在主从复制架构中从库还没有把某个binlog文件消费完那么这个文件就不能清理。判断标准不是“这个文件有多老”而是“从库的复制位点是否已经越过这个文件”。如果你最近没有做过全量备份那也要慎重。binlog加全量备份才构成完整的恢复链缺了binlog恢复能力就会出现空窗。还有一种情况你开启了GTID模式清理binlog时对GTID执行历史的处理会更加敏感。后面我会专门讲这一块。总之清理binlog前先想清楚三件事从库消费到哪了、最近的备份在什么时候、删掉之后如果出问题能不能恢复。想清楚了再动手永远不迟。2. 清理前必须做的三个判断从库、备份和GTID状态2.1 从库消费位点缺了这段日志从库就“断供”了主从复制的基础原理就是从库主动来主库拉取binlog。主库的binlog被清理得太激进从库还没拉到那个文件复制就会中断。这种中断不像网络抖动那样会自动恢复它是永久性的只能通过重建从库或者手动指定位点来恢复非常麻烦。所以每次清理前我都习惯在主库执行一下SHOW MASTER STATUS或者在从库执行SHOW REPLICA STATUS老版本是SHOW SLAVE STATUS重点看几个字段Master_Log_File表示IO线程已经拉取到的主库binlog文件Relay_Master_Log_File表示SQL线程已经执行到的主库binlog文件。两个字段里SQL线程的位置更关键因为relay log只是一个缓冲最终消费完成的标志是SQL线程的位点。有一个更容易理解的办法直接看主库当前正在写的binlog文件名再对比从库Exec_Master_Log_Pos对应的文件名。如果从库已经执行到了mysql-bin.000015而你想清理mysql-bin.000012以前的文件那基本是安全的如果你想清理mysql-bin.000014那从库马上就会断。GTID模式下可以更直观。主从各自维护着Executed_Gtid_Set如果从库的Executed_Gtid_Set已经包含了主库所有已执行的事务那么从库就已经追上了主库。不过GTID的判断在生产环境里偶尔会因为人为清空gtid_purged造成混乱所以我更推荐以文件位点为基准同时参考GTID双保险。2.2 备份与恢复窗口binlog是最便宜的时间点恢复工具很多人平时从来不关心备份只关心磁盘。但binlog清理一旦做下去你的数据恢复能力就会实打实地被切割掉一段。举个例子某天中午12点你清理了MySQL所有binlog但今天凌晨3点才做过全量备份。如果下午2点有人误删了一张核心业务表你能恢复到什么时间最多恢复到凌晨3点从凌晨3点到中午12点之间的所有数据变更全部丢失。那9个小时的增量数据本来全部存在binlog里现在没了。所以我的习惯是清理binlog之前先确认最近一次全量备份的时间并且保证要清理掉的binlog时间段已经有全量备份覆盖。更稳妥的做法是双保险先把要清理的binlog用mysqlbinlog工具拷贝到备份服务器或对象存储然后再在本地执行清理。这样即使以后有审计需求或者异常情况还能从备份里找回这段日志。这一步我称之为“先备份、再清理”是binlog生命周期管理里性价比最高的习惯。别嫌麻烦真出事的时候你会感谢自己多拷贝的那一步。2.3 GTID模式下的额外约束清错日志会让整个复制拓扑瘫痪GTID模式下清理binlog需要额外留意gtid_purged这个变量。gtid_purged表示已经清理掉的GTID集合。如果一台实例的gtid_purged和gtid_executed不一致从库在连接时可能会因为找不到事务的GTID而拒绝同步。我遇到过一次非常典型的问题某同事在主库执行了RESET MASTER想一次性清空所有binlog。结果主库的gtid_executed被清空gtid_purged也被重置从库那边还记录着旧的GTID集合重新连接后从库去请求主库拉取GTID结果两边集合对不上复制直接起不来。当时不得不重建整个从库耽误了大半天。所以在GTID模式下清理binlog优先使用PURGE BINARY LOGS系列命令不要轻易使用RESET MASTER。PURGE只会删除日志文件会把gtid_purged更新到合理的值RESET MASTER则是刀切豆腐连执行历史一起重置副作用大得多。如果你确实想清空所有日志我的建议是先在从库确认完全追平再短暂停掉复制链路然后在主库执行RESET MASTER之后重新配置复制。这个操作必须写在变更单里一步步按流程走绝不能在线上随意执行。3. 手把手实操三种删除binlog的正确姿势3.1 姿势一PURGE MASTER LOGS 精准清理最常用的方法也是我推荐的首选方案。语法很简单有两种写法-- 按文件名清理删除指定文件之前的所有binlog PURGE BINARY LOGS TO mysql-bin.000010; -- 按时间清理删除指定时间之前的所有binlog PURGE BINARY LOGS BEFORE 2024-06-01 00:00:00;按文件清理时系统保留你指定的这个文件和之后的所有文件清理掉更早的。按时间清理时保留该时间点之后产生的日志。实际运维中我会先执行SHOW BINARY LOGS看一下当前有哪些文件再决定保留到哪个文件。接着确认从库的消费位点已经越过了要清理的文件然后执行PURGE。操作完成后再执行SHOW BINARY LOGS确认磁盘上的文件已经减少。这里有一个细节值得注意PURGE命令不会触发磁盘空间立即释放吗实际上它确实会立即删除文件空间马上腾出来。这跟自动清理不一样自动清理依赖后台purge线程可能有几分钟的延迟。还有一点PURGE尽量不要在主库业务高峰执行虽然它本身很快但涉及文件删除和索引更新磁盘IO会有轻微波动。稳妥起见放到凌晨低峰期做配合脚本巡检一点问题都没有。3.2 姿势二RESET MASTER 全量重置仅限单机场景RESET MASTER的作用是清空所有binlog文件重置binlog索引让日志从头开始编号。执行后第一个新生成的binlog会变成mysql-bin.000001。RESET MASTER;这个命令我只能说慎用。它适用于两种场景。一种是全新的单机环境没有任何从库依赖你只是想清空日志重新开始另一种是刚刚做完全量重建整个复制体系全部重新搭建旧的日志确实没有保留价值了。在有多从库或者有历史恢复诉求的架构中千万别执行RESET MASTER。前面提到过GTID模式下它会造成gtid_executed和gtid_purged被清空而且从库的复制元数据如果还指向旧文件重启后复制就断了。另外注意RESET MASTER不会停掉日志写入MySQL会立刻生成新的binlog所以“清空”只是相对概念。如果你连binlog功能本身都想关掉请单独去修改log-bin配置不要指望RESET MASTER能替代。3.3 姿势三自动清理参数从源头控制磁盘占用相比手动清理我更推荐在MySQL层面配置自动过期清理。这样binlog达到过期时间后后台线程会自动删除大部分时候根本不用人工干预。MySQL 5.7时代用expire_logs_days这个参数单位是天# my.cnf [mysqld] expire_logs_days 7MySQL 8.0以后官方推荐用binlog_expire_logs_seconds单位是秒。默认值是2592000也就是30天。8.4版本开始expire_logs_days被彻底移除所以新环境建议直接用秒这个参数[mysqld] binlog_expire_logs_seconds 604800动态调整也很方便SET GLOBAL binlog_expire_logs_seconds 604800;注意动态设置只对后续产生的binlog生效已有的binlog也会根据新过期时间在purge线程的下一次扫描中判断删除。想让旧日志立即按新策略清理可以手动执行一次PURGE BINARY LOGS BEFORE NOW() - INTERVAL 7 DAY;这里我踩过一个小坑在老版本5.6、5.7里如果我只在my.cnf里改了expire_logs_days但忘了重启全局变量一直没生效日志照样无限增长。后来我养成了一个习惯改完配置先执行SHOW VARIABLES LIKE %expire%确认参数确实变化了再接着做下一步操作。3.4 磁盘已满的紧急处理流程如果数据库已经因为磁盘满而报错甚至无法写入这时候没法心平气和地分析。但越紧急越要稳住按以下顺序操作。第一步先确认从库状态。如果从库离主库很远那要先看有没有空间可以临时扩容或者能不能暂停从库的拉取来争取时间。但暂停从库并不能解决主库磁盘问题只能延缓复制压力。第二步查找最老的binlog文件确认它已经被所有从库消费完毕。快速判断的方法是从库上SHOW REPLICA STATUS看Relay_Master_Log_File字段找出所有从库中最低的那个位点以它为准。第三步用PURGE BINARY LOGS TO定位到该文件之后的位置执行强制清理。这时候已经没有时间考虑“要不要备份”了只能先保写入。但一旦腾出空间要立刻安排全量备份把恢复窗口补齐。紧急切记一条绝对不要直接rm。原因前面说过了索引文件还在MySQL内部会默认为某个文件存在实际磁盘却没有这种不一致状态比磁盘满还难处理。我见过有人rm之后MySQL进程一直报找不到日志文件最后只能通过备份重搭实例才恢复代价极大。4. 常见问题与排查实录4.1 为什么PURGE报错找不到文件执行PURGE有时会报错提示Could not find first log file name in binary log index file。这个错误通常有三种原因。第一种binlog索引文件和磁盘文件已经不一致。比如有人之前直接rm过某些binlog导致索引文件里列出的文件在磁盘上不存在。这种只能手工修复索引把索引文件里不存在的那几行删掉再重启MySQL。第二种指定的文件名本身存在问题。比如你写了PURGE BINARY LOGS TO mysql-bin.000020但当前索引里根本没有这个文件或者这个文件已经是被purge过的后续文件命令就会报错。执行之前一定要先用SHOW BINARY LOGS确认文件名列表。第三种在执行PURGE的同时后台purge线程也在自动清理两个操作产生竞争。这种情况下等几秒重试通常就好。修复索引文件的操作不能乱来。最稳妥的做法是先把数据目录下的binlog文件列表导出来与mysql-bin.index逐行对比缺失的行手工从索引里移除。操作前务必先停掉MySQL实例或者至少确认没有新的binlog写入否则索引文件还在被MySQL持有手工编辑会导致新的不一致。4.2 磁盘满了还没法写事务磁盘满时MySQL会拒绝所有需要写日志的事务表现是卡住、报错严重时整个实例呆滞。有人问我明明只满了几百MB为什么事务全卡因为binlog需要预写、双写、redo log也要同时写任何一个环节空间不足整个提交链路都没法完成。这时候应急办法是什么如果实例还能登录立刻执行SHOW BINARY LOGS找最老的binlog评估是否能PURGE。如果连登录都困难说明磁盘满到连会话都创建不了这时候只能靠操作系统的空间来救急了比如清理其他非MySQL目录或者临时挂一块新的磁盘。等MySQL缓过来再执行PURGE释放binlog空间。我在实际运维中很早就给binlog目录单独规划了一块分区并且设置磁盘使用率监控。binlog目录和其他数据目录分离的好处是binlog满了不会拖累整个数据盘清理时目标也明确。这个我在后面的运维习惯里细说。4.3 直接从文件系统删了binlog现在重启报错这个问题出现频率比我预想的高。原因很典型总觉得binlog不是“数据”删了就删了结果重启后MySQL找不到日志文件或者从库过来拉日志失败。如果只是删了部分文件且实例还没重启你还有补救机会。方法是用PURGE BINARY LOGS TO指定一个仍然存在的文件让MySQL把那些已经不存在的文件从索引中清掉。执行后索引和磁盘就能恢复一致。如果已经重启了实例起不来或者起来后一直报错找不到某个日志文件那就要手工编辑mysql-bin.index。操作流程是这样先停实例备份原索引文件然后用ls命令列出当前磁盘上的所有binlog文件按名称顺序重写mysql-bin.index每行一个文件名最后启动实例。这一步很危险务必小步慢走改完立刻验证。更稳妥的做法是如果这个实例不重要且数据已经被全量备份覆盖直接重建实例比修复索引更省心。最怕的是既删了文件又没有备份那已经超出“运维操作”范畴进入“灾难恢复”流程了。所以再次强调binlog的删除永远走PURGE不走rm。4.4 从库跟不上但主库磁盘又实在顶不住了这是运维里最让人头疼的场景空间告急清理脚本已经按检测逻辑判断“安全”但从库因为大事务延迟、网络抖动等原因消费进度差了一截。怎么办首先千万别硬删。从库一旦断不是小事。应对思路是“拆解压力”。第一加大单个binlog文件的大小减文件数量。调整max_binlog_size的值比如从128M改为1Gbinlog切分的节奏慢了文件数量自然减少磁盘紧张能稍微缓解。但文件数量减少不代表空间减少只是风险窗口变小。第二检查从库延迟原因。如果是并行复制能力不足调整从库的slave_parallel_workers参数加快SQL线程消费速度。如果是网络带宽问题看看主从中间有没有加速通道可以临时切换。第三如果从库延迟确实追不上而主库磁盘只剩几百MB只能做一个临时取舍确认从库的relay log是否已经缓存了一段时间的数据尝试把主库的PURGE点定位到离从库消费位点最近的一个文件边界。宁可漏掉一部分还没消费的日志触发从库重建也不能在主库磁盘满的情况下停摆。重建从库虽然费时间但比整个主库挂掉影响面小得多。这个取舍要提前和团队沟通不要一个人拍板毕竟从库重建期间读写压力会全部落在主库和现有从库上风险要提前评估。5. 自动清理之外的几个运维习惯5.1 给binlog建一个独立挂载点我很早就把binlog独立出来了这个习惯帮我在多次事故中避免了大麻烦。binlog目录、数据目录、系统盘、备份盘物理隔离或至少逻辑隔离各占各的容量。binlog增长再快也不会把系统盘塞满导致整个实例崩溃。而且独立目录之后清理、迁移、封盘都变得很灵活。如果你用的云数据库做不到物理隔离至少可以在建实例时确认binlog的存储空间是不是单独计量的。很多云厂商现在已经是独立日志盘了如果还不是建议升级配置或者至少把监控加上。5.2 把清理策略和备份策略绑定在一起binlog生命周期管理不是一个孤立话题。它的上游是全量备份下游是恢复演练。我习惯在规划备份策略时把binlog保留时间定为全量备份间隔的1.5到2倍以上。比如全量备份一天一次binlog至少保留48小时以上如果全量备份一周一次binlog至少保留半个月。这么做逻辑上非常顺任何一次恢复操作都需要从最近一次全量备份开始重放后续全部binlog直到目标时间点。binlog保留太短恢复窗口就不完整保留太长磁盘压力大。绑定起来之后清理策略就不是拍脑袋定天数而是从备份和恢复的需求倒推出来更经得起评审。5.3 用计划任务加一道巡检保险就算配置了自动清理我仍然会写一个定时巡检脚本每天检查一次binlog目录使用量、文件总数、过期参数是否生效。脚本逻辑不复杂统计目录大小超过阈值发出告警检查SHOW BINARY LOGS的文件列表与磁盘文件是否一致顺带在日志里记录当前主从复制位点。这个脚本我自己维护了很多年几次在自动清理失效时提前发现苗头避免走到磁盘告警那一步。自动清理是常态机制巡检是兜底保险两者配合才是完整的binlog治理方案。最后分享一个我在实际使用中的体会binlog的管理功夫全在删之前。判断清楚从库位点、备份窗口、GTID状态再加上正确的PURGE姿势和自动过期配置磁盘空间基本不会失控。最怕的永远是图省事直接rm。如果你之前已经踩过类似的坑现在修复索引还来得及如果没有踩过记住这句话就够了binlog这个东西规则用对了是护身符删错了一瞬间就是事故。