数据库运维13 min read次阅读

磁盘被 mysql-bin.0000xx 吃光、误删数据靠它回滚:MySQL binlog 这套机制你至少得会管

一台 MySQL 半夜两点告警,磁盘使用率 100%,写入全挂。我连上去 du -sh /var/lib/mysql/* 一排,datadir 根下躺着三百多个 mysql-bin.000001mysql-bin.0003xx,每个正好 1G,加起来 300 多 G,把云盘吃没了。另一边,另一个项目里同事跑了个 DELETE FROM orders WHERE 忘了加条件,五千条当天订单瞬间没了,业务在问能不能恢复。两件事,一个毁在 binlog 没人管,一个救在 binlog 还在——同一个东西,既能坑你也能救你。

binlog 到底记了什么

binlog(binary log)是 MySQL 服务层的二进制日志,跟存储引擎无关,记录的是「数据发生了什么变化」的事件:一条 INSERT 写了什么行、一条 UPDATE 把哪些列从什么改成什么、一条 DDL 执行了什么。它和 InnoDB 自己的 redo log、undo log 不是一回事——redo 保证崩溃恢复时事务持久,undo 支撑回滚和 MVCC 读,而 binlog 是为了**复制和点位恢复(PITR)**而存在的,默认关,开了才生成文件。

也正是因为它和引擎无关,所以不管你用 InnoDB 还是 MyISAM,binlog 都记。复制的主从同步、canal/Debezium 这类 CDC 抓取,底层全靠消费 binlog。这也是为什么「binlog 炸了」和「靠 binlog 救数据」会同时发生在同一个人身上。

先看它开没开、在哪:

SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'log_bin_basename';
SHOW VARIABLES LIKE 'datadir';

log_bin = ON 才在记。MySQL 8.0 默认就开,5.7 及之前很多镜像默认是关的——这决定了你有没有资格谈后面的恢复。

三种格式,选错一行影响巨大

binlog_format 有三个值,这件事值得停下来想清楚,因为它直接关系到恢复能不能做、复制保不保真:

  • STATEMENT:记 SQL 原文。省空间,但带 NOW()UUID()RAND() 这类不确定函数的语句,在从库重放结果可能不一样,主从数据会漂移。触发器、函数里也容易翻车。
  • ROW:记每行实际改动的前后镜像。最安全,主从必然一致,误删恢复时也能精确到行。代价是体积大——一个大 UPDATE 改 100 万行,binlog 就记 100 万行变化,这也是 binlog 吃盘的隐形推手。
  • MIXED:MySQL 自己判断,能确定结果的用 STATEMENT,不确定的切 ROW。看似两全,但排障时你永远得先猜这一条到底记成了哪种,恢复解析时略烦。

现在新建实例基本无脑 ROW,5.7.7 之后默认就是 ROW。ROW 模式下还有个 binlog_row_image,设 FULL(默认,记前后镜像)能完整回滚,设 MINIMAL 只记必要的列,省空间但闪回能力打折。我的建议:生产用 ROW + FULL,别为了省那点盘赌恢复时的麻烦。

SHOW VARIABLES LIKE 'binlog_format';
SHOW VARIABLES LIKE 'binlog_row_image';

GTID 开了之后,玩法变了

GTID(Global Transaction ID)给每个事务一个全局唯一编号 server_uuid:事务序号,比如 3e11fa47-71ca-11e1-9e33-c80aa9429562:23。开了 GTID 模式,复制和 recovery 都用「已执行到哪个 GTID 集合」来表达进度,比数 binlog 文件名和 position 直观太多,也不容易因为手抖指错位点而重复执行。

开它需要:

SHOW VARIABLES LIKE 'gtid_mode';
SHOW VARIABLES LIKE 'enforce_gtid_consistency';

enforce_gtid_consistency = ON 会禁止那些 GTID 下无法安全复制的语句(比如 CREATE TABLE ... SELECT 不带 GTID 标记的写法),这是开启 GTID 的前提约束,迁移老库时经常在这里踩坑——业务里有这种语句会被拒绝执行。开了 GTID 之后,主从搭建用 MASTER_AUTO_POSITION = 1 自动对齐,不用再手算 position;恢复时也能用 GTID 区间精确跳过某个坏事事务。代价是运维心智模型得换,习惯了 position 的老手初期会别扭。

那些决定 binlog 命运的参数

把几个关键参数列清楚,它们直接决定你会不会半夜被磁盘叫醒:

SHOW VARIABLES LIKE 'server_id';             -- 复制必须的标识,8.0 仍建议显式设
SHOW VARIABLES LIKE 'max_binlog_size';        -- 单个 binlog 上限,默认 1G,写满切下一个
SHOW VARIABLES LIKE 'binlog_expire_logs_seconds'; -- 过期秒数,8.0 用它
SHOW VARIABLES LIKE 'expire_logs_days';       -- 老参数,8.0 已废弃,别再靠它
SHOW VARIABLES LIKE 'sync_binlog';            -- 刷盘策略,1 最安全

重点说 binlog_expire_logs_seconds。5.7 用 expire_logs_days(天),8.0 把它标废弃,换成秒级的 binlog_expire_logs_seconds,默认 2592000 即 30 天。很多人从 5.7 升 8.0 后照着老文档设 expire_logs_days=7,发现不生效,binlog 越堆越多——这就是我开头那台磁盘爆满机器的直接原因,它升过 8.0,旧参数失效,新参数没设,binlog 永久保留。正确姿势:

SET GLOBAL binlog_expire_logs_seconds = 604800;  -- 保留 7 天

永久生效写配置文件:

[mysqld]
binlog_expire_logs_seconds = 604800

sync_binlog 是另一个权衡点。sync_binlog=1 表示每次事务提交都刷 binlog 到磁盘,配合 innodb_flush_log_at_trx_commit=1 才是完整 ACID,断电不丢;但高并发写入下每次刷盘是实打实的 IO 开销。=0 交给操作系统调度,性能好但崩溃可能丢最后一点事务。金融、订单这类不能丢数据的库设 1,日志、埋点类可以放宽。

磁盘爆满的救急:别手贱直接 rm

binlog 把盘吃满时,第一反应千万别 rm mysql-bin.0000xx——直接删文件会破坏 mysql-bin.index 索引的一致性,MySQL 再写 binlog 或启动时可能报错,复制也会乱。正确做法是让 MySQL 自己清理,用 PURGE

-- 先看现在有哪些、多大
SHOW BINARY LOGS;

-- 删掉某个文件之前的所有(保留指定文件及之后)
PURGE BINARY LOGS TO 'mysql-bin.000250';

-- 或删某个时间之前的
PURGE BINARY LOGS BEFORE '2026-09-01 00:00:00';

盘已经 100% 导致 MySQL 自己都写不动时,PURGE 也可能执行不了(它要先记一条 binlog 事件)。那种绝境下,先腾一点空间——比如临时把 max_binlog_size 调小、或紧急 PURGE 最早几个文件——再让 MySQL 缓过来。我有次是先在别的挂载点 mv 走最近几个不重要的 binlog(确认从库不需要)腾出空间,MySQL 恢复写能力后立刻用 PURGE 正式清理,再把 binlog_expire_logs_seconds 设好,才算收尾。

监控 binlog 增长也能早发现问题:SHOW BINARY LOGS 看总大小,或 mysqlbinlog --base64-output=DECODE-ROWS 抽一个文件看它到底在记什么大事务:

mysqlbinlog --base64-output=DECODE-ROWS -v mysql-bin.000250 | head -100

如果发现某个 binlog 异常大,八成是有个大事务(批量 UPDATE/DELETE 整张表)在刷 ROW 格式的全量变更,这种该从业务侧拆分,而不是靠缩短过期时间来掩盖。

误删数据,靠 binlog 回滚

回到同事误删五千条订单。库开着 ROW 格式 binlog,数据能救。思路是:从 binlog 里把 DELETE 之前那一刻的数据「反向」生成出来,再插回去。

最细的活儿用 mysqlbinlog 按位置或时间切出那段:

# 先看 binlog 事件,定位误删发生的时间窗口
mysqlbinlog --base64-output=DECODE-ROWS -v mysql-bin.000248 | grep -n "DELETE FROM" 

# 导出误删前后的区间(用 position 更精确)
mysqlbinlog --start-position=12345 --stop-position=67890 mysql-bin.000248 > /tmp/rollback.sql

纯手工从 ROW 格式里抠出「反向 INSERT」很痛苦,因为 binlog 记的是「删了哪些行」,你得把它翻成「插回哪些行」。业界常用 binlog2sql(美团开源的 Python 工具)直接解析出回滚 SQL:

# 生成误删事件的回滚语句
python binlog2sql.py -h127.0.0.1 -P3306 -uroot -p \
  --start-file='mysql-bin.000248' \
  --start-position=12345 --stop-position=67890 \
  -B > /tmp/flashback.sql

mysql -uroot -p < /tmp/flashback.sql

-B 就是生成回滚(flashback)SQL。它把 DELETE 翻成 INSERT、把 UPDATE 翻成反向 UPDATE,精准还原那五千条。前提是 binlog_format=ROW 且 binlog_row_image=FULL,这也是我前面坚持生产用 FULL 的理由——MINIMAL 下回滚工具拿不到完整前镜像,救不回来。

如果误删发生在更早、且你需要的是「回到某个时间点」,那就是标准的 PITR:用最近的全量备份恢复,再用 binlog 追加重放到误删前一秒:

# 从全备恢复后,应用 binlog 到误删前
mysqlbinlog --stop-datetime="2026-09-13 10:29:59" \
  mysql-bin.0002[40-48] | mysql -uroot -p

--stop-datetime 卡在误操作前一刻,后面的坏事事务不重放,库就回到了干净状态。这就是 binlog 存在的核心意义——没有它,你只能退到上次全备,中间几小时的业务全没。

主从环境下,PURGE 前先确认从库跟上了

开了复制的库,PURGE BINARY LOGS 不能只看主库自己——从库可能还在读你正要删的文件。手滑 PURGE 掉从库还没消费的 binlog,复制直接断,且断了没法靠主库补(文件没了),只能重做从库。安全做法:

-- 在从库看它读到哪个文件了
SHOW SLAVE STATUS\G
# 看 Read_Master_Log_File / Exec_Master_Log_Pos

-- 主库 PURGE 时,确保保留的文件晚于所有从库已读取的
-- 或干脆用 GTID 模式下的自动清理,MySQL 会考虑从库进度

GTID 模式下,MySQL 的 binlog 自动清理会参考从库的已执行 GTID 集合,比手动 PURGE 安全。没上 GTID 的老架构,我习惯在脚本里先查各从库的 Read_Master_Log_File,取最小值再 PURGE,留足余量。

关 binlog 省下的盘,会加倍还回来

有些人为省磁盘和那点写入开销,直接 log_bin = OFF。短期看 datadir 干净了,但代价是:不能做主从复制、不能接 CDC、不能 PITR、误删只能认命。对订单、账户、配置类数据,关 binlog 等于赌自己永远不犯错、永远不丢数据。我的底线是,只要是「删了会有人找」的库,binlog 必开,过期时间设 7~14 天足够覆盖大多数恢复窗口,又不至于把盘吃爆;真担心体积,去查是不是有大事务在刷 ROW 格式,而不是关掉日志本身。

说回开头那台机器——把 binlog_expire_logs_seconds 设成 7 天、PURGE 掉历史残留,300 多 G 瞬间回来,磁盘告警解除。另一台误删的库,binlog 还在、格式是 ROW,binlog2sql 十几分钟就把五千条订单捞了回来。binlog 这套东西,平时觉得它占地方、添乱,真出事那天,它就是你唯一的后悔药。把它当日志管,而不是当垃圾堆,是 MySQL 运维里最划算的一笔投入。

分享:

相关文章

中文变问号、表情变乱码:MySQL 字符集那点事我踩了三次才搞透
数据库运维13 min read

中文变问号、表情变乱码:MySQL 字符集那点事我踩了三次才搞透

线上昵称里带个 emoji,存进库变成 「ðŸ」;一次迁移之后,原本正常的客户备注全成了 「???」。这种字符集乱码,八成不是「数据库坏了」,而是 server、database、table、column、连接层五层字符集没对齐。MySQL 的 utf8 其实是 3 字节的假 utf8,存不了 emoji,真正能存全 Unicode 的是 utf8mb4。从现象反推根因,顺手把已经乱掉的数据救回来。

给千万行大表加字段不锁表:pt-online-schema-change 与 gh-ost 实战
数据库运维11 min read

给千万行大表加字段不锁表:pt-online-schema-change 与 gh-ost 实战

一张 5000 万行的订单表要加个索引,直接 ALTER 一跑,写入全卡、连接堆成山、从库延迟飙到几分钟——这是每个 DBA 都挨过的打。MySQL 8.0 的 ALGORITHM=INSTANT 覆盖不了所有场景,真要大表变更得靠 pt-online-schema-change 和 gh-ost 这类影子表工具。一个用触发器、一个靠 binlog,把两条路都跑通,连同坑点一起摊开。

评论区