先说个我上周刚处理完的事。线上一个 MySQL 实例磁盘报警,登录上去一看,数据目录里 mysql-bin.0xxxxx 从 000001 排到了 000350,每个文件 1G,加起来快 400G。同事第一反应是直接 rm 掉前面那些老文件,我赶紧拦住——他要是真删了,这个实例大概率连 show binary logs 都会报错,更别提后面我们还要靠 binlog 恢复误删的数据。
这个场景太典型了:大家遇到 binlog 占空间,第一反应永远是删文件、调 expire_logs_days,却很少有人认真想过,为什么自己的 binlog 会比别人长得快那么多、写库性能为什么越来越差。答案通常不在日志上,而在配置里。这篇就把我实际排查过的几类“配错了”的情况从头捋一遍。读完你自己能定位问题,也能学会怎么用 binlog 做恢复、迁移和操作审计。
1. 先搞清楚 Binlog 在记什么,再谈为什么占空间
1.1 Binlog 不是备份,它是数据变更的“行车记录仪”
MySQL 的 binlog(binary log)是所有写操作事件的记录流。凡是让数据发生变化的行为——INSERT、UPDATE、DELETE、DDL,都会按执行顺序写入 binlog。它不像 InnoDB 的 redolog 那样用来做崩溃恢复,也不像 undolog 那样用来做事务回滚。binlog 的核心作用是两件事:主从复制时,从库拉取它来重放数据变更;数据恢复时,配合全量备份做增量回放。
打个比方:全量备份等于给数据拍了一张快照,binlog 则是从快照那一刻开始的行车记录仪。没有 binlog,你只能回到快照的时间点;有了 binlog,才能把数据推进到出问题前的最后一秒。
但很多人对 binlog 有一个误解:以为它只记录“我写了什么 SQL”。在 STATEMENT 格式下确实是这样,但 MySQL 5.7.7 开始默认格式变成了 ROW。ROW 格式记录的是每一行数据改动前、改动后的完整镜像,日志量天然比 STATEMENT 大得多。再加上 MySQL 8.0 默认保留 30 天的 binlog,写量大的实例如果不主动干预,占满磁盘只是时间问题。
1.2 为什么 Binlog 会无上限增长?它和 redolog 有本质区别
InnoDB 的 redolog 是循环写的,文件大小固定,写满就覆盖旧数据,所以它不会导致磁盘爆满。Binlog 完全不同:它是追加写的顺序日志,默认情况下只会一直新增文件,没有任何“自动覆盖”机制。
这意味着一个残酷的事实:只要磁盘不被写满,binlog 就会一直长下去。 你的 MySQL 不会自己判断“这个 binlog 已经没用了,我删掉吧”,它只会老老实实把文件写下去,直到 max_binlog_size 达到阈值后滚动生成下一个文件。
所以 binlog 的空间治理,从来不是 MySQL 帮你解决的,而是 DBA 的规划和配置决定的。很多人把“配置 expire_logs_days”当成唯一手段,但后面你会发现,这个参数只是兜底,真正让 binlog 膨胀的通常是另外几个更隐蔽的配置。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 90% 的坑都出在这五处配置上
2.1 默认的 ROW + FULL:日志量翻倍的“元凶”
如果你用的 MySQL 5.7.7 以上版本,binlog_format 默认是 ROW,binlog_row_image 默认是 FULL。这两个默认值叠加,是 binlog 体积异常的第一个大坑。
binlog_row_image=FULL 意味着,ROW 格式下每一条 UPDATE 事件,都要同时记录变更行的所有列在修改前和修改后的镜像。举个例子,user 表有 30 个字段,你只改了 nickname 一列,FULL 模式下 binlog 会把 30 列的前值、后值全部写进去。一个本来只有几百字节的 UPDATE,落在 binlog 里就是几 KB 甚至几十 KB。
大部分表都有十几个字段以上,线上更新又频繁,日志量就是这么翻倍的。
正确做法是改成 binlog_row_image=MINIMAL。这个模式下,binlog 只记录被修改的列和能唯一定位行的主键/唯一索引。对于“只改了 1 列”的常见场景,日志体积能下降 50% 以上,update 密集的业务甚至能降 80%。
我见过一个实际的案例:某订单系统把 binlog_row_image 从 FULL 改成 MINIMAL 后,binlog 日均生成量从 60G 降到 15G,效果立竿见影。改动前记得确认从库版本支持,5.6.2 之前的老版本不支持这个参数。
2.2 从不过期:expire_logs_days 和 binlog_expire_logs_seconds
这是最直接导致磁盘爆满的配置,也是“90% 的人配错了”的重灾区。
MySQL 5.7 的 expire_logs_days 默认值是 0,表示 binlog 永不过期。很多生产实例一路跑下来,binlog 攒了几个月,磁盘爆了才想起来清理。MySQL 8.0 改成了 binlog_expire_logs_seconds,默认 2592000 秒(30 天),比 5.7 安全,但对大促、批量任务频繁的库,30 天的 binlog 也能轻松堆出几百 GB。
这里要提醒三个细节:
- 5.7 里同时也有
binlog_expire_logs_seconds,默认 0。如果你同时设置了两个参数,服务器会优先取binlog_expire_logs_seconds的值,expire_logs_days会被忽略。 - 8.0 已经废弃了
expire_logs_days,再设置会产生警告。 - 过期不是实时删除的。binlog 会在切换文件或服务启动时触发清理逻辑。所以就算设了过期时间,磁盘空间也不会立刻释放,碰到紧急告警还是得手动 PURGE。
建议的保留周期怎么定?我的习惯是:至少覆盖两个全量备份周期。例如你每天凌晨 2 点做全量备份,那 binlog 保留 3 到 7 天就够了。这样做完一次全备,老的 binlog 就可以安全过期,磁盘占用是可控的。保留太久没有实际价值,除非有审计合规的硬性要求。
2.3 sync_binlog 和刷盘策略:性能差很可能不是 SQL 慢
很多人遇到“开了 binlog 之后写性能暴跌”就开始怀疑磁盘、怀疑 SQL,其实大概率是刷盘策略没调好。
sync_binlog 控制 binlog 多久同步一次到磁盘,默认值是 1,即每个事务提交前都把 binlog 刷到磁盘。这保证即使 MySQL 宕机,binlog 也不会丢,主从数据不会出现空洞。但代价是每次提交都承担一次磁盘 fsync 的开销,TPS 高的业务能明显感觉到吞吐量下降。
如果把 sync_binlog 设为 0,MySQL 把刷盘决定权交给操作系统,事务提交变快,但 mysqld 崩溃或主机断电时可能丢 binlog,后果是从库缺失一段事务,甚至需要重建从库。
这中间不是只有 0 和 1 两个选择。sync_binlog=N(比如 100)表示每 N 个事务刷一次盘,是很多高并发业务的折中方案。不过要注意,N 越大,崩溃时可能丢失的事务数越多。如果你们有金融级一致性要求,sync_binlog=1 不能动;如果只是内部系统,可以结合 innodb_flush_log_at_trx_commit 一起调。
另外,MySQL 5.7 之后提供了组提交相关参数:binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count。简单说,让多个事务凑一批再一起刷盘,在牺牲毫秒级延迟的前提下大幅提升吞吐量。对写并发高的库,这两个参数有时比直接调 sync_binlog=0 更值得先试。
2.4 binlog 和数据文件同盘:IO 资源互相争抢
这是一个经常被忽略、但影响非常明显的部署级配置错误。binlog 是顺序写,数据文件是随机读写为主。两者放在同一块磁盘上时,InnoDB 的随机 IO 和 binlog 的顺序 IO 会互相干扰。表现就是:看起来磁盘 IO 使用率不高,但写库延迟忽高忽低,TPS 上不去。
MySQL 允许把 binlog 放到独立目录,通过 log_bin 参数指定路径。比如:
ini复制[mysqld]
log_bin = /data/binlog/mysql-bin
binlog_format = ROW
binlog_row_image = MINIMAL
sync_binlog = 1
binlog_expire_logs_seconds = 604800
max_binlog_size = 1G
将 binlog 挪到单独磁盘后,顺序写不跟随机写抢 IO,写入性能会有明显改善。如果公司预算有限,至少要把 binlog 放到和数据文件不同的 LUN 或者云盘上。这个调整对主从复制延迟也有正向作用。
2.5 大事务、长事务:Binlog 分分钟撑爆盘
上面说的都是静态配置,还有一种动态的“配置错误”:允许大批量更新在线上执行。
ROW 格式下,一个 UPDATE 影响 50 万行,binlog 就会产生 50 万行的前后镜像。我用 MySQL 8.0 测试过,一次性更新 50 万行的订单状态,binlog 瞬间增加 1GB 左右。如果业务每天都在跑这种任务,磁盘再大也扛不住。
更麻烦的是长事务。事务长时间不提交,binlog 会缓存在事务私有的 binlog cache 里;超过 binlog_cache_size 后,溢出的部分会落到磁盘临时文件。等到提交时,瞬间把这一大坨日志刷到 binlog。这会在某个时间点产生巨大的 IO 毛刺,并拖慢从库回放进度。
应对方式不在“压缩 binlog”,而在拆事务:
- 批量更新按主键范围分片,比如一次只更新 5000 行,循环跑完。
- 控制单个事务的执行时间,别让事务跨秒级甚至分钟级。
- 监控
performance_schema里的事务持续时间,发现长事务及时 kill。
这些是写入侧的习惯问题,但真到了磁盘告警时,追根溯源往往都能查到几个“贪方便跑大 SQL”的接口。
3. 删、留、限:Binlog 清理的正确姿势
3.1 “Binlog 可以删除吗”:能,但先回答三个问题
搜索引擎里“binlog 可以删除吗”这个问题被问烂了。答案是:可以删,但得搞清楚场景再动手。
删 binlog 之前,先回答三个问题:
- 有没有最近一次全量备份? 增量恢复依赖全备点之后的 binlog。如果连全备都没有,binlog 全删了,等于数据只能停在当前时刻,之前的任何误操作都救不回来。
- 有没有从库还没消费完? 从库的 IO 线程会从主库拉 binlog。如果你把从库还没拉取的文件删了,从库会直接报 1236 错误,复制中断,这是个非常经典的故障。
- 公司的审计和合规要求是什么? 有些行业要求数据库操作留痕至少 N 个月。binlog 是重要的审计依据,删除前得权衡合规风险。
总结成一句话:binlog 可以被删除,但它是一份“后悔药”,在确认没有消费者之前,不要随便扔。
3.2 三种清理方式:自动过期、PURGE、RESET MASTER(危险)
清理 binlog 有三种方式,我按推荐度排个序:
第一种:靠自动过期参数清理。 前面说了,配置 binlog_expire_logs_seconds(或 5.7 的 expire_logs_days),让 MySQL 在日志滚动或实例重启后自动删除过期文件。这是最安全的方式,不用人为干预。
第二种:手动 PURGE BINARY LOGS。 适用场景是磁盘告警、等不到自动清理。语法很直观:
sql复制-- 删除 mysql-bin.000123 之前的所有 binlog(不含该文件本身)
PURGE BINARY LOGS TO 'mysql-bin.000123';
-- 删除 2025-03-01 之前的所有 binlog
PURGE BINARY LOGS BEFORE '2025-03-01 10:00:00';
注意几点:
- 不能删除当前正在写入的 binlog 文件。执行会报错或无效。
- 删除前一定要确认从库状态。查看主库的
SHOW MASTER STATUS拿到当前位点,再到从库执行SHOW SLAVE STATUS\G,确认Relay_Master_Log_File对应的文件比你要删的文件更早。否则从库会断。 - PURGE 删除后,磁盘空间是立即释放的,不需要重启。
第三种:RESET MASTER。 清空所有 binlog 并重新从 mysql-bin.000001 开始编号。这个命令在测试环境随便用,生产环境极度不建议。它会把所有 binlog 都删掉,如果配置了 GTID,还会影响 gtid_executed 集合,可能导致主从复制关系错乱。有从库的时候千万慎重。
另外奉劝一句:永远不要用 rm 命令直接删 binlog 文件。 虽然 binlog 是普通文件,但 MySQL 有 mysql-bin.index 索引文件在管理它。你 rm 掉文件,索引里还留着记录,执行 SHOW BINARY LOGS 就会报错“Could not find target log file”,整个日志系统就乱了。正确做法永远是让 MySQL 自己通过 PURGE 或过期机制删。
3.3 空间上限:没有一步到位的参数,要靠监控加策略
有人问:MySQL 能不能设置“binlog 最多占用 100G,超过就自动清理”?很遗憾,官方没有这样的参数。控制 binlog 空间,只能靠“过期时间”这个间接手段。
所以合理做法是建立监控和预测:
- 每天统计 binlog 增量趋势。比如通过脚本记录
SHOW MASTER STATUS中的文件大小,算出日均增长量。 - 根据全备周期和磁盘容量反推保留天数。假设磁盘给 binlog 预留 200G,日均增长 10G,最多保留 20 天。那么把
binlog_expire_logs_seconds设为低于 20 天的值,留出安全余量。 - 设置磁盘空间告警,比如 binlog 目录使用率达到 70% 就提前人工干预。
这套逻辑跑起来后,binlog 空间问题就从“救火”变成了“日常巡检”。
4. 把 Binlog 变成生产力:数据恢复与数据迁移实战
4.1 基于 Binlog 的增量恢复:误操作后回到任意时间点
这是 binlog 最核心的用途之一。流程不复杂,但每一步都有坑。
假设你每天凌晨 2 点用 Xtrabackup 做全量备份,今天上午 11 点有人误删了一张核心表。恢复思路是:
- 把最近一次全量备份恢复到一台临时实例。
- 找到备份结束时的 binlog 坐标。Xtrabackup 会在备份目录里生成
xtrabackup_binlog_info文件,内容类似:code复制表示备份起始点对应的 binlog 文件和位点。mysql-bin.000089 12345678 - 用
mysqlbinlog把备份点之后、误操作时刻之前的 binlog 事件提取出来,重放到临时实例:
bash复制mysqlbinlog \
--start-position=12345678 \
--stop-datetime="2025-03-10 10:59:59" \
/data/binlog/mysql-bin.000089 > /tmp/recover.sql
mysqlbinlog \
--start-datetime="2025-03-10 11:00:00" \
--stop-datetime="2025-03-10 11:00:05" \
/data/binlog/mysql-bin.000090 >> /tmp/recover.sql
mysql -h 临时实例IP -uroot -p < /tmp/recover.sql
这里有个非常关键的细节:停止时间点要精确到误操作发生之前的瞬间,不要只精确到分钟,否则可能把误操作本身也重放进去。如果能从应用日志里定位到具体位点,用 --stop-position 比 --stop-datetime 更可靠。
重放完成后,在临时实例上确认数据完整,再把需要的表导出,导入正式库。注意不要直接在正式库上重放 binlog,风险太高。
4.2 如何利用 Binlog 做数据迁移
再看“如何利用 binlog 做数据迁移”。这个场景的典型诉求是:把线上实例的数据同步到新的实例,同时保证迁移窗口内新增的增量数据不丢。
常见的做法有两种。
方案 A:全量恢复 + mysqlbinlog 追增量。 适合迁移窗口内可以短暂停写或容忍短暂不一致的业务。
流程是先做全量备份恢复到新实例,然后把备份点之后的 binlog 通过 mysqlbinlog 输出成 SQL,在目标实例重放。比如目标实例全量恢复到了 mysql-bin.000089 的位点 12345678,迁移窗口结束于 mysql-bin.000092,那就把这三个文件的后续事件都追过去:
bash复制mysqlbinlog \
--start-position=12345678 \
/data/binlog/mysql-bin.000089 \
/data/binlog/mysql-bin.000090 \
/data/binlog/mysql-bin.000091 \
/data/binlog/mysql-bin.000092 \
| mysql -h 新实例IP -uroot -p
方案 B:解析 binlog 做实时同步。 适合需要持续在线同步的场景。常见工具是 canal、Maxwell、Debezium。它们的原理都是伪装成从库,通过复制协议拉取主库的 binlog,再把解析出来的行变更写到目标端(另一个 MySQL、ES、Kafka 等)。
以 canal 为例,它可以精确解析出每一条 INSERT、UPDATE、DELETE 对应的数据行,通过适配器同步到目标。如果目标库是异构存储,比如要同步到 Elasticsearch,这种方案是主流。
迁移过程中有几个容易踩的坑:
- 表结构必须一致。 binlog 只记录数据变更,不会校验目标表结构。源表有
id列,目标表缺了,重放就会报错。 - SQL_MODE 和字符集要尽量一致。 否则重放时可能出现字符串截断、默认值不一致等问题。
- 迁移前最好
FLUSH LOGS,让 binlog 文件边界干净。 这样指定文件范围更清晰,不容易把迁移窗口拉得过长。
4.3 一个容易被忽略的原则:Binlog 只做增量,别当全量用
我要强调一个原则:binlog 是增量日志,不是全量备份的替代品。任何数据迁移方案,都必须先做全量数据同步,再用 binlog 追增量。指望“只靠拷 binlog 就把几 TB 的数据迁移过去”是不现实的,因为 binlog 从头到尾回放一遍,代价比全量备份还高。
我在实际项目里见过有人为了省事,把一个月前的 binlog 一股脑重放到新库,结果跑了十几个小时,中间遇到乱序、重复主键等问题,最后全部推倒重来。正确的姿势永远是:全量打底,增量补齐。
5. 追查“谁动了数据”:如何通过 Binlog 还原操作
5.1 先校准预期:Binlog 不记录 SELECT,查不到“读”操作
很多业务方找到 DBA 说“帮我查下哪天谁查了这张表”,这里有个常见误区:binlog 只记录数据变更操作,纯 SELECT 查询不会写入 binlog。
所以如果你需要审计“谁执行了哪些 SELECT”,binlog 帮不了你,得靠另外两套方案:
- 开启慢查询日志
slow_query_log,把超过阈值的 SELECT 记录下来。 - 使用 MySQL 审计插件或 Performance Schema 的语句事件表,记录所有 SQL。
但如果你要查的是“谁在什么时间改了哪些数据、改成了什么”,binlog 是首选。ROW 格式下,binlog 里有每行数据的前后镜像,足以还原出完整的写操作过程。需要说明的是,binlog 本身不直接记录执行者账号,定位到具体用户需要结合应用日志、连接审计等外部信息。
5.2 mysqlbinlog 解析:把二进制日志变成人话
mysqlbinlog 是官方自带的解析工具,要把 binlog 变成可读内容,这是最基础的一步。
ROW 格式的 binlog 默认输出是 BASE64 编码,加 --base64-output=DECODE-ROWS -v 才能看到可读的伪 SQL:
bash复制mysqlbinlog \
--no-defaults \
--base64-output=DECODE-ROWS \
-v \
--start-datetime="2025-03-10 10:00:00" \
--stop-datetime="2025-03-10 10:30:00" \
/data/binlog/mysql-bin.000090 > /tmp/decode.sql
解析出来的内容长这样:
sql复制# at 4123
#250310 10:15:22 server id 3306101 end_log_pos 4188 CRC32 0x1a2b3c4d
### UPDATE `shop`.`orders`
### WHERE
### @1=10001 /* INT meta=0 nullable=0 is_null=0 */
### @2='PENDING' /* STRING(20) meta=65028 nullable=1 is_null=0 */
### SET
### @2='PAID' /* STRING(20) meta=65028 nullable=1 is_null=0 */
可以看到这是一条更新了 orders 表第 2 列(从 PENDING 改为 PAID)的操作。注意 --database 参数在 ROW 格式下有历史性的过滤坑,跨库操作时可能过滤不干净。如果只想定位某张表,解析完后用 grep 筛表名通常更可靠。
还有个实用技巧:如果要定位某个时间段内某张表的操作,可以先按时间段解析,再 grep 表名:
bash复制grep -n "UPDATE \`shop\`.\`orders\`" /tmp/decode.sql
5.3 用 binlog2sql / my2sql 还原出可读 SQL,甚至生成回滚语句
mysqlbinlog 输出的伪 SQL 毕竟不是真实可执行 SQL,可读性一般。如果要还原成原始 SQL,甚至生成回滚 SQL,业界常用的两个工具是 binlog2sql(Python)和 my2sql(Go)。
这两个工具的使用前提是:binlog_format 必须是 ROW,binlog_row_image 最好保持 FULL。 因为还原原始 SQL 和生成回滚 SQL,都需要完整的前后镜像。如果你为了省空间把 binlog_row_image 设成了 MINIMAL,回滚脚本可能生成不出来,只能看到部分列。这是个天然的取舍:FULL 占空间但可回溯性强,MINIMAL 省空间但牺牲了细节。
以 binlog2sql 为例,它的思路是先解析 binlog 事件,再根据事件类型组装成可读 SQL。基本用法类似:
bash复制python binlog2sql.py \
-h 127.0.0.1 -P 3306 -u root -p \
-d shop -t orders \
--start-file='mysql-bin.000090' \
--start-datetime='2025-03-10 10:00:00' \
--stop-datetime='2025-03-10 10:30:00' \
--stop-file='mysql-bin.000090'
输出就是带注释的原始 SQL:
sql复制INSERT INTO shop.orders(id, status, created_at) VALUES (10001, 'PENDING', '2025-03-10 10:15:00');
UPDATE shop.orders SET status = 'PAID' WHERE id = 10001 AND status = 'PENDING';
这类工具最常见的用途是闪回。比如误删数据后,用工具把 DELETE 事件反向生成 INSERT 语句,再执行回正式库,比手动重放安全得多。要注意的是,工具解析出的 SQL 不一定 100% 等价于原始语义,例如事务里依赖了 NOW()、自增主键冲突等情况,复盘时都要人工确认。
我的建议是:这类工具先在测试环境完整跑一遍流程,再上生产。尤其生成回滚 SQL 时,必须在临时实例上验证结果,确认不影响其他业务表。
最后再分享一个我自己的习惯。每次做完全量备份,我都会顺手看一眼 binlog 的日均增长量和磁盘余量,再对照 binlog_expire_logs_seconds 的设置,看保留周期是否合理。平时这些小动作看不出什么,但真等磁盘报警、从库断开的深夜里,你会感谢那个提前想了三步的自己。
如果你现在正被 binlog 占空间困扰,别急着删文件,先去查 SHOW MASTER STATUS、SHOW BINARY LOGS,对照全量备份的时间和从库消费位点,再决定清到哪一步。顺序对了,问题就解决了一半。
