
干 DBA 这行快十年被 binlog 撑爆磁盘的求助至少接过二十回。剧本几乎一模一样半夜告警平台疯狂弹磁盘使用率超过 85%登上服务器一看数据目录下 mysql-bin.000xxx 排得整整齐齐最早的能追溯到三个月前单文件 1G 起步目录总大小轻松上百 G。MySQL binlog 日志保留策略这件事平时没人关心但真等磁盘满了才想起来处理轻则实例进入只读保护重则直接宕机业务方半夜打电话的语气可一点都不温柔。这篇文章就把 binlog 保留策略这件事讲透从根本原理、主流配置方案、实操步骤到高频故障排查让新手能直接照着配也让老手对上几处平时容易忽略的细节。1. binlog 日志的本质与保留策略的底层逻辑1.1 binlog 到底在记录什么binlog 是 MySQL 的二进制日志记录的是所有会改变数据库内容的操作也就是 DML 和 DDL。注意它和 redo log 有本质区别redo log 是 InnoDB 存储引擎层面的物理日志主要服务于崩溃恢复数据写盘之后就可以循环覆盖binlog 是 Server 层逻辑日志以事件形式记录 SQL 或行变更主要服务于主从复制、基于时间点的恢复和数据审计。可以这么理解redo log 是引擎内部的草稿纸binlog 是给外部看的流水账两者缺一不可。binlog 有三种记录格式ROW、STATEMENT 和 MIXED。ROW 格式记录每一行数据变化前后的值最精确但也最占空间STATEMENT 记录原始 SQL省空间但主从复制时受函数、存储过程等不确定因素影响MIXED 由 MySQL 自动判断。8.0 默认就是 ROW这也意味着 binlog 的体积比早年 STATEMENT 时代明显更大保留策略必须把这个体积增长考虑进去否则沿用老经验配置磁盘迟早出问题。很多刚接触 MySQL 的朋友会把 binlog 误认为可以随手删的临时文件这个认知风险很大。binlog 除了用于主从同步还承载着基于时间点恢复的核心功能。假设你每天凌晨做一次全量备份白天某张业务表被误删恢复路径就是全量备份加上备份之后产生的全部 binlog 重放。如果 binlog 只保留一天那就只能恢复到昨天凌晨的状态中间一整天的数据全部丢失。保留策略本质上是在可恢复窗口和磁盘成本之间做权衡这也是整个方案设计的出发点。1.2 保留时间长与短分别要付出什么代价保留时间设太长最直接的代价是磁盘。这里要提醒一个很多人忽略的点binlog 的写入速度和业务活跃度正相关和保留天数本身没有关系。不管你怎么配保留策略binlog 该写多少还是写多少只是保留多久决定它占多少磁盘。一个日写入量 100G 的实例保留 7 天就是 700G 占用留 30 天就是 3T这个账必须提前算而不是等磁盘告警再算。很多团队把保留时间拍脑袋设成 30 天结果 2T 数据盘三个月就飘红最后只能半夜起来删日志。保留时间设太短的代价更隐蔽。刚才说了恢复窗口依赖 binlog主从复制中的从库如果因为网络抖动或长时间停机落后主库而主库的 binlog 已经按保留策略被清掉了从库就会报 1236 错误也就是常见的 Could not find first log file name in binary log index这种情况下只能重新搭建从库代价非常大。所以主从环境里保留策略必须覆盖从库最大可能的落后时间这个我在后面故障排查部分还会专门讲。还有一类代价容易被忽略长事务。如果在保留期内执行了一个超长事务binlog 里对应的大事务事件会长期占用空间甚至导致清理被卡住。MySQL 在清理 binlog 时如果一个 binlog 文件里包含了尚未提交的事务这个文件就不能被删除。所以保留策略叠加长事务在一起的时候实际占用可能远超预期这也是我在监控实践里会特别关注大事务相关指标的原因。2. 主流保留方案盘点参数、命令与思路2.1 expire_logs_days老派但仍在服役expire_logs_days 是最早的自动清理参数单位是天含义是 binlog 文件的最长保留天数。5.7 及更早版本里它几乎是唯一的选择。5.7 的一个大坑是默认值是 00 表示永远不清理很多 5.7 实例磁盘被写满就是因为压根没人配这个参数。虽然 5.7 已经引入了 binlog_expire_logs_seconds但 expire_logs_days 直到 8.0 才被标记为废弃8.0 里如果两个参数同时设置会以 binlog_expire_logs_seconds 为准。这个参数的粒度是天这在很多场景下太粗了。比如你的备份周期是 24 小时理论上 binlog 保留 30 小时就够但 expire_logs_days 最小粒度就是 1 天等于强制多留 18 小时的数据。单个实例可能无所谓但如果是几十套实例组成的数据库集群多留出来的空间加起来就是个惊人的数字。2.2 binlog_expire_logs_seconds更精细的控制binlog_expire_logs_seconds 的单位是秒精度比天高得多。为什么需要秒级因为一天的粒度太粗了。举个例子我管理过一个核心库全量备份每天凌晨 2 点执行理论上 binlog 保留 30 小时就足够覆盖两次备份之间的增量区间但如果用 expire_logs_days2就得白白多留 18 小时的数据多占几十 G 磁盘。改成 binlog_expire_logs_seconds108000也就是 30 小时就精准多了。8.0 版本里如果两个参数同时存在但值不一致会以 binlog_expire_logs_seconds 为准但官方文档并不建议这么混用因为容易出现我明明改了 expire_logs_days 为什么没生效的困惑。我的建议是 8.0 直接忽略 expire_logs_days统一用 binlog_expire_logs_seconds。5.7 如果想用秒级参数也可以它在 5.7 里就已经支持了。2.3 PURGE BINARY LOGS手动清理的时机与边界自动清理之外还有一种手动方式PURGE BINARY LOGS TO mysql-bin.000123;或PURGE BINARY LOGS BEFORE 2025-01-01 00:00:00;。手动清理适合两种场景一是磁盘突然吃紧等自动清理线程的调度周期来不及二是你想精准删到某个文件或某个时间点。但这里必须强调两条红线。第一绝对不要直接去操作系统层面 rm binlog 文件。删完文件但没更新 binlog.index 索引MySQL 启动时会报错主从复制也会因为找不到文件而中断。第二PURGE 操作要避开主从复制断开的窗口否则从库还没拉到对应位置的 binlog主库这边就把它清掉了从库会直接报错。还有一点如果某个 binlog 文件正被从库读取PURGE 会被阻塞或只清理从库安全位置之后的文件MySQL 5.7 之后系统会自动判断但理解这个机制有助于你解释为什么我配了保留策略文件还留着。2.4 MySQL 8.0/8.4 的默认值变化版本差异是很多人踩坑的重灾区。MySQL 8.0 里 binlog_expire_logs_seconds 默认值是 2592000 秒也就是 30 天同时 expire_logs_days 默认值是 0。8.4 直接移除了 expire_logs_days 参数binlog_expire_logs_seconds 默认值同样是 30 天。这意味着从 5.7 升到 8.x 之后如果你沿用原来的 my.cnf 把 expire_logs_days 写进去8.0 会告警但还能启动8.4 里直接报 unknown variable启动就失败。升级前务必要把配置文件里的 expire_logs_days 清理掉。这里给个参数速查表方便收藏参数单位默认值适用版本说明expire_logs_days天0不清理5.7 及以下为主8.0 废弃早期的保留策略参数粒度粗binlog_expire_logs_seconds秒259200030天5.7 起引入8.x 沿用目前主推的保留参数精度高max_binlog_size字节10737418241G所有版本单个 binlog 文件体积上限注意 max_binlog_size 说的是目标大小实际文件可能因为事务跨文件写入而略超这属于正常现象不要看到 1.2G 的 binlog 就以为参数没生效。另外binlog 文件命名规则是主机名加数字序号删除旧文件后序号不会回填这是正常设计别去手动改文件名。3. 实操一套可落地的保留策略配置3.1 先算清楚该保留多久配置之前先回答一个问题你的恢复目标是什么给一个我常用的计算模板。假设业务要求故障时可恢复到最近 1 小时全量备份每天凌晨 2 点执行备份耗时 30 分钟那么 binlog 保留时间等于备份周期 24 小时加上备份耗时 0.5 小时加上安全余量建议 2 到 4 小时再加上从库允许的最大落后时间比如 2 小时。这样算下来大概是 29 到 31 小时取整设成 36 小时比较稳妥也就是 129600 秒。对你没看错不是越久越好而是刚刚好覆盖恢复窗口加容错余量最好。很多人机械地设成 7 天原因是拍脑袋结果磁盘压力全在 binlog 上。先算需求再配置是保留策略最核心的思路。当然如果磁盘非常充裕、也不想承担任何恢复窗口缩水的风险保留 7 天甚至 14 天也无可厚非毕竟运维的本质是风险偏好选择我只是告诉你大多数业务真的不需要留那么久。3.2 动态配置与持久化配置配置分为动态和持久化两步。动态配置直接执行 SQLSET GLOBAL binlog_expire_logs_seconds 129600;注意 SET GLOBAL 只对当前实例生效重启后失效必须写进配置文件。Linux 下编辑 /etc/my.cnf 或 /etc/mysql/conf.d/ 下的自定义配置文件在 [mysqld] 段加一行binlog_expire_logs_seconds129600然后重启 MySQL或者在 MySQL 8.0 及以上版本里用 SET PERSIST 直接持久化到 mysqld-auto.cnfSET PERSIST binlog_expire_logs_seconds 129600;SET PERSIST 的好处是无需重启而且会把配置写入数据目录下的 mysqld-auto.cnf重启照样生效比手改 my.cnf 省事得多。如果想撤销某个 PERSIST 配置用SET PERSIST binlog_expire_logs_seconds DEFAULT;就行。这里提醒一句SET PERSIST 写进去的配置优先级高于 my.cnf如果你后面在 my.cnf 里改了同一个参数却发现不生效先排查 mysqld-auto.cnf。3.3 配置成功与否怎么验证改完配置别急着走要验证三件事。第一看参数是否生效SHOW VARIABLES LIKE binlog_expire_logs_seconds;。第二观察现有 binlog 文件的清理情况SHOW BINARY LOGS;看最早的日志文件结合当前时间判断清理是否按预期推进。第三看主从状态SHOW SLAVE STATUS\G里的 Master_Log_File 和 Relay_Master_Log_File确保从库读取位置远早于清理位置。这里有一个我在实践中反复确认过的细节自动清理不是一到时间立刻删而是按 binlog 文件为单位清理。MySQL 不会把一个 binlog 文件删掉一半文件内所有事件对应的时间点只要在保留期限之后整个文件就会在下次检查时被删除。所以 SHOW BINARY LOGS 里最老的文件时间可能比预期略老这是正常的别误以为策略没生效。4. 高频故障与排查实录4.1 配了自动清理binlog 还是没被删这个问题在社区里被问烂了原因就那几类。最常见的是从库还在读取如果从库的 IO 线程正在拉取某个 binlog 文件主库不会删除它。解决办法是确认从库状态如果从库已经永久下线就在从库或主库层面先清理复制关系从库执行 STOP SLAVE 和 RESET SLAVE ALL或主库回收复制账号权限让主库放开该文件的清理。还有一种情况是 binlog 里有尚未提交或尚未结束的临时表相关事件清理被阻塞长事务同理事务没提交关联的 binlog 文件就不能动。另外别忘了 MySQL 对 binlog 的清理检查是在写日志时触发的。用 PURGE 手动清理会立即执行但自动清理机制存在调度窗口如果你配置后立刻想看到效果SHOW BINARY LOGS 可能没变化等下一个 binlog 切换或写入动作触发检查后再看。我见过有人配置完十分钟没看到文件减少就在群里喊没生效其实等半小时再看就有了。4.2 磁盘告警时的应急处理如果磁盘已经红了先做应急别先想策略。我的标准流程是这样第一步立刻确认实例是否还活着SELECT 1 都返回失败的话优先考虑是重启还是扩容没有足够空间时先清理其他日志文件比如慢查询日志和错误日志。MySQL 的错误日志常常被忽略一个 10G 以上的 error log 并不罕见。第二步用 SHOW BINARY LOGS 和 SHOW MASTER STATUS 组合起来确认当前正在写的文件以及最老的可用文件。第三步用PURGE BINARY LOGS BEFORE NOW() - INTERVAL 2 HOUR;立刻清掉大部分过期文件。第四步检查从库状态这个放在清理前做先看 SHOW SLAVE STATUS\G从库落后太多就不要贸然 PURGE。这个顺序是有讲究的为什么先查从库再 PURGE因为一旦 PURGE 删掉了从库还没拉取的文件从库就废了需要重建那可比磁盘告警的代价大得多。宁可临时扩容磁盘也不要赌从库已经同步到位。磁盘告警的处理原则是先止血、再排查、后优化顺序反了容易把小事搞成事故。4.3 误删文件与 binlog.index 修复还有一种场景是有人直接在系统层 rm 了 binlog 文件然后 MySQL 报错或者复制中断。这个问题的根源就是 binlog.index 里还记录着已经被删除的文件名。修复思路是把被删文件恢复到原路径或者从 binlog.index 中移除对应行并重启。最稳妥的做法是别删提前规划好保留策略。但真遇到已经 rm 了索引还没更新的情况可以这样处理先停掉 MySQL备份 binlog.index把其中指向不存在文件的那些行删掉再启动 MySQL。注意操作前确认备份链是否还完整——如果删掉的 binlog 恰好是备份之后唯一的增量来源那么这个时间窗口的数据恢复基本就无解了。这也是为什么我强调所有对 binlog 的操作都要先想备份链。处理这类问题我通常会用一张速查表帮助定位症状可能原因排查顺序binlog 文件没被清理从库还在读取、长事务未提交、调度窗口未到先查从库状态再查事务磁盘突然写满保留策略未配置、错误日志过大、临时表空间膨胀先看 binlog 目录大小再看 error log从库报 1236 错误主库 binlog 被清从库落后太远确认复制断点准备重建从库实例启动报找不到 binlog 文件有人 rm 了文件binlog.index 未更新恢复文件或修复索引4.4 我遇到过的两个典型场景第一个场景来自一个日写入量很大的电商库binlog 每天产生约 80G运维把保留时间设成 7 天磁盘 2T 眼看就要满了。后来我把保留时间改成了 50 小时配合每天凌晨 3 点全量备份磁盘占用从 560G 降到 170G 左右恢复窗口完全没受影响。这个案例说明保留策略的优化空间很多时候比想象中大得多关键是先算需求。第二个场景是在一个主从复制环境里从库因为机房断网停了 6 天主库 binlog 保留 5 天恢复网络后从库直接 1236 报错。最后只能重建从库期间业务读压力全部打在主库上主库 CPU 飙到 90%。这个案例一正一反说明保留策略不是简单填个天数必须结合备份策略和复制拓扑一起设计。我后来把主库保留时间统一改成了备份周期加从库最大容忍延迟再加 24 小时余量这套口径沿用至今没再出过问题。5. 监控体系与长期维护心得5.1 监控什么指标保留策略配好只是开始后续要盯监控。第一磁盘使用率按实例维度监控binlog 目录单独统计通过 du -sh 查看数据目录下 binlog 子目录的大小或者用 pt-diskstats 类工具辅助观察。第二binlog 文件数量与磁盘增速建立基线连续几天偏离基线就要排查业务变更或大事务。第三SHOW BINARY LOGS 里最老文件的时间戳理论上它应该稳定在当前时间减保留时间附近如果明显偏老说明清理被阻塞正好对应 4.1 节的那些坑。我平时会把这三个指标做成一个简单的巡检脚本每天定时跑一次输出实例名、binlog 目录大小、最老文件、当前写入文件、磁盘使用率、从库延迟。脚本逻辑不复杂十几分钟就能写完但省掉的是半夜爬起来救火的痛苦。这里顺手分享一个小习惯每次变更保留策略我都会在变更记录里写清楚三个数字旧的保留秒数、新的保留秒数、备份策略允许的最小保留秒数。这样半年后回看变更记录还能还原当时的决策逻辑。5.2 几个值得长期坚持的习惯最后说几条我在实操中沉淀下来的习惯。第一binlog 目录和数据目录尽量分开挂载数据盘满了 binlog 不至于带着业务一起死如果条件允许binlog 放 SSD 上写入性能对高并发实例的 TPS 有肉眼可见的影响。第二主从环境保留策略一律按从库最大容忍延迟加备份窗口加余量计算绝不用拍脑袋天数。第三大版本升级前用 SHOW VARIABLES LIKE %binlog% 和 SHOW VARIABLES LIKE %expire% 把当前配置记录下来升级后逐项对照。第四也是最容易被忽略的一点binlog 保留策略要写进运维文档和故障预案。磁盘告警的应急处理流程里必须明确第一步做什么、第二步做什么而不是让值班同学临场翻手册。我经历过太多次告警后值班同学在服务器上手忙脚乱找命令的场面提前把命令固化到预案里几十秒就能把事情处理完。binlog 保留策略不是什么高深技术但它和备份、复制、监控串成一条完整的链路之后就是数据库稳定运行的基本盘之一。这套链路我建议每个团队都认真梳理一遍别等到磁盘告警了再回头补课。