1. 先搞清楚:binlog 到底“存”在哪儿
这个问题在搜索框里出现的频率高得离谱,但真正问的人,心里想的往往不是同一件事。有人是想在服务器上ls出一个文件,有人是想知道主从同步现在推进到第几个字节,还有人是在做数据恢复,需要把某天下午三点之后的所有变更捞出来。所以“mysql binlog 在哪里”这句话,至少可以拆成三层含义:物理上它落在哪个目录的哪个文件里、逻辑上当前的写入位点是多少、语义上我怎么把这些二进制内容翻译成人能看懂的操作。这三层不搞清楚,你就算把文件路径背下来,真出事的时候照样抓瞎。
我先把最直接的答案摆出来:在绝大多数自建实例上,binlog 就躺在数据目录(datadir)下面,文件名由log_bin_basename决定,加上一串六位数字后缀,比如binlog.000037。同时同一目录下还会有一个.index结尾的索引文件,里面按顺序列出当前所有可用的 binlog 文件名。这个答案对 MySQL 5.6 之后的所有版本基本成立,差异主要在变量名和查询语句上。但如果你用的是云上的托管实例,或者跑在容器编排平台里,这个“标准答案”立刻就会失效——前者你根本没有文件系统权限,后者容器一重建文件就没了。
所以这篇文章我打算按“从物理到逻辑、从查到用、从用到排错”的顺序讲一遍。适合谁看?正在准备 DBA 面试的、做后端开发要写数据订阅或审计的、搞运维要背容量规划的、以及哪天手滑DELETE少了WHERE需要抢救数据的。不需要你已经是老手,但你至少得能登上一台 MySQL 敲命令。我下面提到的所有参数和语句都是可复现的,你可以边看边在测试环境验证。
1.1 一句话答案和它背后的三个前提
把 binlog 当成一个“流水账本”来理解最省事。数据库每执行一条会改变数据的语句(或者每一行变更),就往账本上记一笔,记完才算数。账本放在仓库(datadir)的第几排货架上,这就是物理位置;账本翻到第几页第几行,这就是位点(position);账本用的是速记符号,需要专门的翻译工具(mysqlbinlog)才能读懂,这就是格式问题。
但这个类比有个前提必须说清楚:binlog 是“写”给谁看的?它主要服务三个对象——从库(复制)、备份恢复、以及外部消费程序(比如用 Canal、Maxwell 这类工具做数据同步)。理解了服务对象,你才能明白为什么它的位置信息既有文件维度又有位点维度:文件是给存储和传输用的,位点是给“续传”用的。从库断线重连,必须告诉主库“我从某个文件的某个偏移量继续”,少一个都不行。
第二个前提是binlog 只记录“逻辑变更”,不记录物理页。这跟 InnoDB 的 redo log 是两回事。redo log 记录的是“某个数据页的某个偏移量被改成了什么”,是物理层面的,循环覆盖写,位置在datadir下的#innodb_redo目录(8.0)或ib_logfile0/1(5.7)。binlog 记录的是“执行了什么”,是逻辑层面的,追加写,不会覆盖(除了被清理)。很多人在搜“binlog 在哪里”的时候,脑子里的对象其实是 redo log,这两者混淆是最高频的认知错误。
第三个前提:binlog 的存在不是默认保证的。从 MySQL 8.0 开始,log_bin默认是ON,但 5.7 及更早版本默认是OFF。所以你在一台老库上怎么找都找不到,第一件事应该是确认开关,而不是怀疑路径。这个我会在第 5 章展开。
1.2 binlog、redo log、undo log 的位置对照
既然提到了混淆,干脆一次性把三种日志的“住址”和职责摊开。我见过太多人把ib_logfile0当成 binlog 拷走,然后在另一台机器上mysqlbinlog解析出一堆乱码,回头在群里问“为什么我的 binlog 是坏的”。这种事故完全可以避免。
| 日志类型 | 典型物理位置 | 写入方式 | 主要职责 | 能否用 mysqlbinlog 解析 |
|---|---|---|---|---|
| binlog | datadir下的basename.0000NN | 追加写,事务提交时落盘(受sync_binlog控制) | 主从复制、按时间点恢复、变更捕获 | 能,这是它唯一的用途 |
| redo log | 8.0 在datadir/#innodb_redo/;5.7 在datadir/ib_logfile0、ib_logfile1 | 循环覆盖写 | 崩溃恢复,保证已提交事务不丢 | 不能 |
| undo log | 存放在 undo 表空间(undo_001、undo_002等),位于datadir | 随事务生命周期管理 | 事务回滚、MVCC 一致性读 | 不能 |
这张表看着简单,但第 4 章讲实操时你会发现,很多人把relay log也混进来。relay log 是从库上的东西,位置同样在datadir,命名靠relay_log_basename控制,格式跟 binlog 一样,也能用mysqlbinlog解析。但它记录的是“从主库拉过来、尚未执行完的事件”,语义完全不同。你如果在一台从库上执行SHOW BINARY LOG STATUS,看到的位点是从库自己写 binlog 的进度(如果开了log_slave_updates),而不是它从主库同步到哪了。想知道后者,得看SHOW REPLICA STATUS里的Relay_Master_Log_File和Exec_Master_Log_Pos字段。这一条我在第 5.3 节还会再强调一次,因为它坑过的人实在太多。
1.3 从“文件”和“位点”两个维度理解“在哪里”
我建议在脑子里建立这样一个双层模型,后面所有操作都会顺畅很多。
物理层回答的是:文件叫什么、放在哪个目录、多大、什么时候被删。这一层用文件系统命令和SHOW VARIABLES就能搞定。它决定了你做备份时该拷哪些文件、磁盘什么时候会满、容器重启后数据还在不在。
逻辑层回答的是:现在正写到哪个文件的第几个字节、从库追到哪了、我想恢复的那个时间点落在哪个区间。这一层用SHOW BINARY LOG STATUS、SHOW BINLOG EVENTS、以及mysqlbinlog的时间过滤来处理。它决定了你写复制位点配置、写数据同步工具断点续传逻辑时该填什么值。
两层之间是映射关系但不对等。一个 1GB 的文件里可能塞了几十万个事件,位点是一个字节一个字节往前数的,没有人会去心算“第 37 个文件第 8 亿字节对应几点几分”。所以实际工作中,我们总是先用时间戳定位到大概的文件,再用位点做精确截取。这个流程在第 4.2 节我会给出完整的命令组合。
2. 物理文件位置:从参数查到具体目录
现在进入动手环节。假设你已经登录到一台 MySQL 实例,第一步该敲什么?很多教程上来就说“去/var/lib/mysql下面找”,这个说法在 Debian/Ubuntu 的 apt 安装下大概率对,在 CentOS 的 yum 安装下也是默认值,但只要你装的时候改过my.cnf、或者用的是编译安装、或者跑在容器里,这个路径就未必成立。永远不要靠猜路径,让数据库自己告诉你。
2.1 三个必查参数:log_bin、datadir、log_bin_basename
这三条语句是我每次接手一个陌生实例时最先敲的:
-- 确认 binlog 是否开启,以及 basename 是绝对路径还是相对路径 SHOW VARIABLES LIKE 'log_bin'; SHOW VARIABLES LIKE 'log_bin_basename'; -- 拿数据目录,binlog 通常就在这里 SHOW VARIABLES LIKE 'datadir';log_bin在 MySQL 8.0 里返回ON,在 5.7 里如果没开就是OFF,如果开了可能会返回ON或者具体路径(早期版本有这个差异,看到OFF就不用往下找了)。log_bin_basename是 5.6 之后才有的变量,它的值是 binlog 文件名的完整前缀,可能形如/var/lib/mysql/binlog,也可能形如./binlog。注意这个细节:如果 basename 是相对路径,它是相对于datadir的,不是相对于你当前 shell 的工作目录。我遇到过有同学在/root下面ls binlog.*然后说文件不存在,问题就出在这儿。
拿到 basename 之后,物理路径就是basename加上.000001这样的后缀。你可以直接用一条 SQL 把路径拼出来,省得手工对齐:
SELECT @@datadir AS datadir, @@log_bin_basename AS binlog_basename, CONCAT(@@datadir, '/', SUBSTRING_INDEX(@@log_bin_basename, '/', -1)) AS binlog_prefix;还有一种更直接的办法,某些版本支持通过SHOW BINARY LOGS拿到文件名列表,但注意它返回的是文件名不是完整路径:
SHOW BINARY LOGS;输出长这样:
+------------------+-----------+ | Log_name | File_size | +------------------+-----------+ | binlog.000035 | 1073742016| | binlog.000036 | 1073742201| | binlog.000037 | 4026531 | +------------------+-----------+看到File_size这个字段了吗?这是判断“当前正在写哪一个”的关键线索。正在写的那个文件通常尺寸小于上限(默认 1GB,由max_binlog_size控制),而其他文件基本都逼近上限。当然这只是个经验判断,一个事务如果特别大,写完可能直接超过 1GB,那时候文件尺寸会超过max_binlog_size,这是个容易被算错的细节——max_binlog_size是“事务边界检查”而不是硬性截断,事务不被拆开。
2.2 文件命名规则与 .index 文件的作用
binlog 文件名 =basename+.+ 六位数字序号,从000001开始递增。到999999之后会怎样?答案是有可能回绕,带来重用风险,不过一个文件 1GB、每秒写一个文件也要写 11 天以上才能绕一圈,实际生产里几乎碰不到。真有这种极端写入量的场景,建议把max_binlog_size调大或者缩短保留时间,别指望编号永远不冲突。
.index文件是很多人忽略的存在。它的内容就是一行一个 binlog 文件名,按创建顺序排列。从库和mysqlbinlog在批量读取时会参考这个索引。你可以直接cat它:
cat /var/lib/mysql/binlog.index输出类似:
./binlog.000035 ./binlog.000036 ./binlog.000037注意这个文件是可以手工编辑的,但我强烈建议不要随手改。如果索引和实际文件对不上,某些版本在启动时会报错甚至拒绝启动,或者SHOW BINARY LOGS的输出与实际磁盘不一致,排查起来非常费劲。清理 binlog 请统一用PURGE BINARY LOGS TO 'binlog.000038'或PURGE BINARY LOGS BEFORE '2024-05-01 00:00:00',让数据库自己维护索引。手工rm文件是最常见的自找麻烦的姿势,我第 5 章会专门讲这个坑。
另外还有一个参数值得记住:log_bin_index。默认情况下索引文件叫basename.index,但这个参数可以单独指定索引文件的位置。如果你发现 datadir 里没有.index文件,先查一下这个参数,别急着下结论说“binlog 不完整”。
2.3 不同部署形态下的路径差异
这是“mysql binlog 在哪里”这个问题最需要分场景回答的地方。同一句提问,答案可能完全不同。
场景一:包管理器安装的单机实例。绝大多数情况下datadir是/var/lib/mysql,binlog 就在这个目录下,属主是mysql:mysql。你以 root 身份ls能看到,但直接用 root 身份cat一个正在写的 binlog 文件,理论上不会损坏数据,但毫无意义,而且如果文件正在被写入,你读到的可能是半个事务。读 binlog 一律用mysqlbinlog,不要用cat或strings。这是我的第一条铁律。
场景二:源码编译或自定义安装。datadir往往被改到了/data/mysql/data这类位置。这时候你必须在配置文件里确认datadir和log-bin两行。补充一句,配置项写的是log-bin(连字符),SQL 变量名是log_bin(下划线),这俩是同一个东西的不同写法,别被绕晕。
场景三:容器化部署。binlog 在容器内部的datadir下,但容器一删,文件全没。所以生产环境必须把datadir挂载到宿主机卷或者持久化存储上。如果你在宿主机上docker inspect看到个匿名卷,赶紧改成具名卷或绑定挂载。我见过一个团队把数据库跑在容器里,binlog 没做持久化,容器因为健康检查失败被编排系统重建,等他们想按时间点恢复数据的时候,历史 binlog 一个都不剩,只剩一个空的000001。这件事的教训不是“记得备份”,而是“先想清楚你的日志到底存在哪个生命周期里”。
场景四:云上托管实例。你连不上文件系统,mysqlbinlog也没法在服务器上跑。这类实例通常提供“日志下载”或者“binlog 备份”的能力,通过控制台或者 API 把文件拉到本地,再在本地用mysqlbinlog解析。同时SHOW BINARY LOGS一般还是能用的,能看到有哪些文件、多大,只是拿不到本地路径。这种情况下问“在哪里”,答案是“在一个你只有下载权、没有读权限的对象存储里”,心态上要接受这一点。
场景五:主从架构。主库和从库各有一份 binlog(从库是否写取决于log_slave_updates,8.0 默认开启)。所以如果你登录的是一台从库,看到的 binlog 是从库自己的,跟主库的位点没有直接对应关系。很多人在这里绕圈,本质是没搞清自己登的是哪台机器。
2.4 文件权限与属主:看不到不等于不存在
这一节讲一个非常具体、但出现频率高到离谱的问题:ls报No such file or directory,但SHOW BINARY LOGS明明列出了一堆文件。
原因通常是权限或命名空间。第一种可能,你用的系统用户没有那个目录的执行权限,ls一个目录需要x权限,不只是r。第二种可能,datadir挂的是一个 overlay 文件系统或者网络存储,路径在容器内和宿主机上不是同一个。第三种可能,你看到的binlog.000037是 basename 展开的形式,而实际文件名可能带了你的实例标识前缀,比如mysql-bin.000037,你ls binlog.*当然找不着。
我一般会让同学先跑这个组合:
mysql -e "SELECT @@datadir, @@log_bin_basename, @@hostname" ls -lh $(mysql -N -e "SELECT @@datadir")-N参数去掉表头,方便后面接管道。如果ls权限不够,用sudo ls -lh,或者干脆用mysql客户端以有权限的账号执行SHOW BINARY LOGS来确认真实文件名。这里还有一个隐藏细节:查 binlog 列表需要的权限。SHOW BINARY LOGS需要REPLICATION CLIENT权限(8.0 里叫REPLICATION CLIENT,某些版本对应SUPER或BINLOG MONITOR)。如果你用一个只有业务库读写权限的账号去查,可能直接报权限错误,很多人误以为“binlog 没开”,其实只是账号权限不够。MySQL 8.0.22 之后新增了更细粒度的BINLOG MONITOR权限,专门用于查看复制和 binlog 状态,做最小权限分配的时候可以用它。
3. 逻辑位置:位点、GTID 与“当前写到哪儿了”
物理路径解决了“文件在哪”,接下来是“进度在哪”。这一块的重要性体现在两个场景:配置从库时必须填对,否则复制起不来;写数据同步工具时必须能断点续传,否则重启就要全量重来。
3.1 SHOW BINARY LOG STATUS 的输出怎么读
老版本的经典命令是SHOW MASTER STATUS,这个命令在 MySQL 8.0.22 之后被SHOW BINARY LOG STATUS取代,8.4 版本里SHOW MASTER STATUS已经被移除。如果你在一台新库上敲老命令报语法错误,别怀疑自己记错了,是版本变了。反过来,老版本不认新命令,写脚本的时候要做版本兼容判断。
SHOW BINARY LOG STATUS;输出大概是这样:
+------------------+----------+--------------+------------------+-------------------------------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | +------------------+----------+--------------+------------------+-------------------------------------------+ | binlog.000037 | 4026531 | | | 3e1a5b88-9c2f-11ee-8a7b-0242ac110002:1-8821 | +------------------+----------+--------------+------------------+-------------------------------------------+几个字段的含义要逐个说清楚。File是当前正在写的 binlog 文件名,这个就是第 2 章里你ls出来的那个。Position是当前写入位置,单位是字节,从文件开头算起。Binlog_Do_DB和Binlog_Ignore_DB是过滤规则,现在大多数生产环境都是空的,用binlog_do_db做过滤反而容易踩坑(跨库事务的语句会被意外丢弃),建议保持为空,需要过滤就在下游消费端做。Executed_Gtid_Set是已执行的 GTID 集合,只有开启gtid_mode=ON才有意义,如果没有开启,这一列是空的。
这里有个非常容易误读的点:从SHOW BINARY LOG STATUS拿到的位点,是“当前写入位置”,不是“最近一次事务结束的位置”。如果你拿这个位点去配置从库的CHANGE REPLICATION SOURCE TO ... SOURCE_LOG_POS=...,严格来说应该取一个事务边界上的位点。实践中的常见做法是:在源库上执行FLUSH TABLES WITH READ LOCK(或者用备份工具的一致性快照),再记录位点,这样位点处在一个确定的事务边界上。直接拿SHOW BINARY LOG STATUS的值去配从库,如果那一刻正好有事务在写,可能出现“从库从半个事务开始回放”的奇怪状况。这个细节在文档里不会特别强调,但你配一次带写入压力的主从就会印象深刻。
3.2 position 到底是怎么算的
位点这个数字,本质是“文件内的字节偏移量”。binlog 文件开头有一个 4 字节的 magic number(固定是fe 62 69 6e,也就是\xfe加bin三个字符),后面每隔一段就是一个事件(event)。每个事件有自己的头部,包含时间戳、事件类型、服务器 ID、事件长度、下一个事件的位点等信息。所以 position 的推进不是按事件个数,而是按字节数。
这意味着两个推论。第一,同一个事务在不同实例上的位点不可能相同。哪怕你从主库克隆了一份数据到从库,两边的 binlog 位点也没有可比性,能对比的只有 GTID。第二,你不能手工“修”位点。我见过有人为了让复制跳过某个报错,直接把SOURCE_LOG_POS往后加了 1000,结果从库回放到了一个事件中间,直接 crash。跳过错误有专门的手段(sql_slave_skip_counter或者注入空事务),位点不是那么用的。
如果你想看某个文件里的所有事件及其位点,用这个:
SHOW BINLOG EVENTS IN 'binlog.000037' FROM 0 LIMIT 20;输出会给你Log_name、Pos、Event_type、Server_id、End_log_pos、Info这几列。End_log_pos就是这个事件结束后的位点,跟Pos的差值就是这个事件的字节长度。多翻几页你就能看出规律:Format_desc事件在文件开头,Previous_gtids紧跟其后,然后是一串Gtid、Query(或者Write_rows/Update_rows/Delete_rows)、Xid。Xid出现意味着一个事务提交了,这个位置就是一个安全的截取点。
这里给一个实操心得:用SHOW BINLOG EVENTS定位时间点,比用mysqlbinlog全量解析快得多,因为它支持FROM和LIMIT,你可以先粗定位再精定位。缺点是它不会显示每个事件的具体时间戳(Info列里可能有部分信息),而且大文件上翻页仍然慢。真要按时间找,还是mysqlbinlog的--start-datetime好用,下面第 4.2 节会讲。
3.3 GTID 视角下的位置表达
从 MySQL 5.6 开始引入 GTID,本质上是用“全局事务 ID”替代“文件 + 位点”来标识复制进度。GTID 的格式是server_uuid:transaction_id,比如3e1a5b88-9c2f-11ee-8a7b-0242ac110002:8821。Executed_Gtid_Set显示的是一组区间,表示这个实例已经执行过哪些事务。
用 GTID 的好处是显而易见的:换文件、换位点都不影响,从库只要报出自己执行到哪了,主库就能算出该补哪些。坏处是调试的时候数字不直观,而且一旦出现“同一个 GTID 在两台机器上被不同事务占用”的情况(通常是手工操作不当导致的),修复起来比位点问题麻烦得多。
查 GTID 是否开启:
SHOW VARIABLES LIKE 'gtid_mode'; SHOW VARIABLES LIKE 'enforce_gtid_consistency';两个都是ON才算完整开启。如果gtid_mode是OFF,那Executed_Gtid_Set那列就是空的,你只能靠文件和位点。生产环境里我倾向于开 GTID,理由很实际:故障切换的时候不用手工算位点,运维同学半夜被叫起来的机会少一半。
3.4 常用定位命令速查表
把这一章涉及的查询汇总一下,方便你直接抄:
| 目的 | 命令 | 需要的权限 | 备注 |
|---|---|---|---|
| 看 binlog 是否开启 | SHOW VARIABLES LIKE 'log_bin'; | 无特殊要求 | 返回ON或OFF |
| 看文件前缀 | SHOW VARIABLES LIKE 'log_bin_basename'; | 无特殊要求 | 可能是相对路径,相对于 datadir |
| 看文件列表与大小 | SHOW BINARY LOGS; | REPLICATION CLIENT或BINLOG MONITOR | 只有文件名,没有完整路径 |
| 看当前写入位点 | SHOW BINARY LOG STATUS; | 同上 | 8.0.22 之前用SHOW MASTER STATUS |
| 看文件内事件 | SHOW BINLOG EVENTS IN 'xxx'; | 同上 | 支持 FROM 和 LIMIT |
| 看从库同步进度 | SHOW REPLICA STATUS\G | 同上 | 关注Relay_Master_Log_File、Exec_Master_Log_Pos |
| 看 binlog 格式 | SHOW VARIABLES LIKE 'binlog_format'; | 无特殊要求 | 8.0 默认ROW |
这张表我建议存下来,尤其是权限那一列。很多“查不到”的问题,根子都在权限上,而不是配置上。
4. 找到文件之后:怎么读、怎么用
找到了位置,绝大多数人的下一步是“我想看看里面到底写了什么”。这一章讲解析工具和实操技巧。先重申一遍铁律:不要用cat、strings、hexdump去读 binlog。里面除了少量可读字符串(库名、表名、SQL 文本片段),大部分是二进制编码,硬看只会浪费时间。唯一的正解是mysqlbinlog,这是 MySQL 官方自带的命令行工具,装在数据库服务器的bin目录下。
4.1 mysqlbinlog 的基础用法与必加参数
最朴素的用法:
mysqlbinlog /var/lib/mysql/binlog.000037这会把整个文件的事件翻译成伪 SQL 打印出来。但我几乎不会只敲这一条,因为输出里最关键的“行数据变更”默认是被编码的。如果你用的是ROW格式(8.0 默认),你会看到类似这样的东西:
### INSERT INTO `shop`.`orders` ### SET ### @1=1001 ### @2='2024-05-01 10:00:00' ### @3=199.00这是可读的。但如果表里有BLOB、JSON、GEOMETRY类型,或者你用的是老版本的ROW格式,可能会看到BINLOG '...'这样的 base64 块。要把它解码成可读形式,需要加参数:
mysqlbinlog --base64-output=DECODE-ROWS -v /var/lib/mysql/binlog.000037--base64-output=DECODE-ROWS的作用是:把原本以 base64 字符串形式存储的行事件解码出来,以###注释的形式展示。-v(verbose)则是把列值以带注释的方式打印,方便你对照列序号。两个都要加,只加一个的效果会让你怀疑人生。
再给几个我常用参数的组合说明:
--no-defaults:忽略my.cnf,避免本地配置里的字符集等设置干扰解析。跨机器解析时很有用。--database=dbname:只输出指定库的事件。注意它是按“事件所属库”过滤,跨库操作时可能漏掉部分,别完全依赖它做过滤。--skip-gtids:在导出 SQL 时要保留原始 GTID 就不加,要去掉 GTID 信息以便在别的实例上重放就加上。这个参数在恢复场景下至关重要,加错了会导致重放时 GTID 冲突。--flashback:这不是官方参数,是部分分支版本(比如某些兼容分支)提供的功能,用于生成“反向 SQL”做误操作回滚。官方版没有这个参数,别照抄网上的命令然后发现报错。
还有一点要注意:mysqlbinlog的版本最好和服务器版本一致或更新。用 5.7 的客户端解析 8.0 的文件,可能因为事件格式新增字段而失败或输出错乱。
4.2 按时间或位点截取片段
生产环境里的 binlog 动辄几十 GB,你不大可能全量解析再grep。常见的做法是先用时间窗口缩范围,再用位点精确截取。
按时间:
mysqlbinlog --no-defaults \ --start-datetime='2024-05-01 09:00:00' \ --stop-datetime='2024-05-01 10:00:00' \ -v --base64-output=DECODE-ROWS \ /var/lib/mysql/binlog.000036 /var/lib/mysql/binlog.000037 \ > /tmp/incident.sql--start-datetime和--stop-datetime是基于事件时间戳过滤的,时间戳来自事件头,是主库产生该事件的时间。注意它只过滤事件的输出,不会改变位点语义。所以如果你要拿这个输出做恢复,先在一个临时实例上验证,别直接往生产库灌。
按位点:
mysqlbinlog --no-defaults \ --start-position=4026531 \ --stop-position=4073000 \ -v --base64-output=DECODE-ROWS \ /var/lib/mysql/binlog.000037 > /tmp/seg.sql这里的位点必须对应到真实的事件边界,否则会报ERROR: Error in Log_event::read_log_event()。判断边界的办法就是用第 3.2 节的SHOW BINLOG EVENTS,找Xid事件的End_log_pos。我一般会多留一点余量,宁可多解析一个事务,事后在 SQL 里手工删掉多余的语句,也不要因为位点差几个字节反复重试。
顺带提一个提高效率的技巧:如果你只需要某一个表的数据,可以在mysqlbinlog输出后接grep快速筛查表名,再针对性精读。
mysqlbinlog -v --base64-output=DECODE-ROWS /var/lib/mysql/binlog.000037 \ | grep -n -A 20 'INSERT INTO `shop`.`orders`' | head -100这招比全量读快得多,但它只是“看”,不要基于grep的输出直接做恢复,因为过滤会丢掉上下文(比如事务边界、BEGIN/COMMIT)。
4.3 读内容时容易误判的地方
这里列几个我踩过或者看别人踩过的坑,都属于“看懂了每个字但结论全错”的类型。
第一,ROW格式下的@1、@2是列的序号,不是列名。序号按表定义顺序排,遇到加列、删列,历史 binlog 里的@N和现在的表结构就对不上了。想搞清楚含义,只能去查当时那一刻的表结构,或者看 binlog 事件里附带的Table_map信息(mysqlbinlog -v会打印列类型)。这也是为什么我强调“恢复必须在匹配版本的实例上做”。
第二,伪 SQL 不可直接执行。### INSERT INTO ...这类带###的行是注释,不是可执行语句。你可以手工转换,但一定要注意字符集和转义。有些字段里的内容包含单引号、反斜杠,转写的时候极易出错。
第三,binlog_format是MIXED时,同一个文件里可能有STATEMENT也有ROW。你在看一段连续的Query事件,突然出现Table_map和Write_rows,不要以为是文件损坏,是格式切换导致的。这种混合格式给解析工具带来不少麻烦,如果下游有消费程序,建议统一成ROW。
第四,Xid事件出现不代表事务一定在业务库上生效了。它只表示 binlog 里写入了提交记录。真正的数据落地靠的是 InnoDB 的事务提交,两者通过两阶段提交协调。如果你只看 binlog 就断言“这单交易成功了”,遇到极端场景(binlog 写完但引擎提交失败)可能会误判。当然这是极小概率,但做审计类需求的时候得知道这个边界。
4.4 从 binlog 恢复数据的正确姿势
既然讲到这儿,就把恢复流程说清楚,因为这是“找 binlog”最常见的最终目的。
标准流程是:先恢复最近的物理全备,再重放全备之后到目标时间点的 binlog。缺了全备只有 binlog,是恢复不出完整数据的——binlog 里只有增量。
具体步骤:
- 在隔离环境(绝对不要在生产实例上)恢复最近的全备,记录备份时的位点或 GTID 集合。
- 用
mysqlbinlog从备份位点开始,按--stop-datetime截取到误操作发生前一刻。 - 在隔离环境重放这段 SQL,验证数据状态是否符合预期。
- 确认无误后,再考虑把数据导回生产,或者把生产切换到恢复后的实例。
这里有三个必须强调的点。第一,--stop-datetime的时间一定要取在误操作之前,因为误操作本身也在 binlog 里,重放会把错误再执行一遍。想精确定位,得先用mysqlbinlog找出误操作那条DELETE或UPDATE的位点,再用--stop-position截在它前面。第二,重放时要注意 GTID。如果目标实例开了 GTID,重放带 GTID 的 SQL 会冲突,需要--skip-gtids或者把gtid_mode临时关掉(后者在 8.0 里需要按步骤切换,不建议随便动)。第三,别在业务高峰期做这件事,重放本身是串行执行,对 IO 有压力,而且你未必一次就对,可能要重来几次。
关于误操作回滚,还有一个更轻量的思路:既然ROW格式记录了变更前后的值,理论上可以生成反向 SQL。但官方工具链里没有这个能力,需要借助第三方脚本或者自己按事件类型转换。我个人的建议是:如果数据量不大,优先从备份恢复加 binlog 补增量;如果只是误删了几百行,从解析结果里把DELETE事件对应的行值手工捞出来重建插回去,反而更快更安全。别一上来就想搞全自动逆向,那玩意儿的调试成本经常超过手工处理。
5. 找不到 binlog 的排查路径
终于到了最容易让人抓狂的部分。这一章我按“从概率高到概率低”的顺序排,你按这个顺序走一遍,90% 的情况能定位。
5.1 排查顺序:开关、路径、权限、生命周期
第一步,确认开关。SHOW VARIABLES LIKE 'log_bin'返回OFF,那就是没开,后面都不用看。5.7 及更早版本默认关闭,需要改配置加log-bin再重启。有些托管实例允许在线开启,有些必须重启,看具体能力的支持情况。这一条排除掉,问题就少了一半。
第二步,确认路径。拿到@@datadir和@@log_bin_basename,在服务器上ls -lh对应目录。如果 basename 是相对路径,记得拼上datadir。这一步能排除“找错目录”的问题。
第三步,确认权限。用有REPLICATION CLIENT或BINLOG MONITOR权限的账号执行SHOW BINARY LOGS。如果报权限错误,说明账号不够,不是日志不在。同时确认系统用户对datadir有读权限,ls时报 permission denied 和报 no such file 是两回事。
第四步,确认生命周期。前面三步都通过了,SHOW BINARY LOGS也列出了文件,但你要找的那一个不在列表里——那就是被清理了。看expire_logs_days(8.0 里是binlog_expire_logs_seconds)和是否有人执行过PURGE BINARY LOGS。这一步在事故复盘中非常关键,因为它决定了你能不能做时间点恢复。
5.2 常见现象与原因对照表
| 现象 | 最可能的原因 | 处置方式 |
|---|---|---|
SHOW VARIABLES LIKE 'log_bin'返回OFF | 未开启 binlog | 修改配置加log-bin,重启后确认 |
ls报 no such file,但SHOW BINARY LOGS有文件 | 路径算错,basename 是相对路径 | 用@@datadir拼接后再查 |
ls报 permission denied | 系统用户对 datadir 无读权限 | 换有权限的用户,或用 sudo |
SHOW BINARY LOGS报权限错误 | 账号缺REPLICATION CLIENT/BINLOG MONITOR | 授予对应权限 |
| 文件比预期少很多 | 到期自动清理 | 查binlog_expire_logs_seconds |
| 文件突然清空或索引对不上 | 手工rm或手工编辑 index | 检查操作历史,重建索引需谨慎 |
| 容器重启后 binlog 全没了 | datadir 未持久化 | 改用具名卷或绑定挂载 |
解析报read_log_event错误 | 位点不在事件边界,或版本不匹配 | 调整位点到End_log_pos,核对客户端版本 |
| 从库位点跟主库对不上 | 在看从库自己的 binlog | 改看SHOW REPLICA STATUS |
| 有 binlog 但没记录某些操作 | binlog_format=STATEMENT且用了不确定函数,或 session 级sql_log_bin=0 | 检查格式与 session 设置 |
这张表我建议对着现象逐条排。经验上,前三条能覆盖绝大部分提问。
5.3 几个反直觉的坑
坑一:以为expire_logs_days设成 0 就是永久保留。在 5.7 里,expire_logs_days=0确实表示“不自动清理”。但 8.0 改用binlog_expire_logs_seconds之后,如果这个值是 0 且expire_logs_days也是 0,行为是“不自动清理”;可一旦binlog_expire_logs_seconds有非零值,就按秒数清理。升级版本后没检查这一项,日志被提前清掉的事故我见过不止一次。迁移前一定要把这个参数对齐确认。
坑二:手工rm了 binlog 文件,索引文件没更新。后果是SHOW BINARY LOGS可能仍然列出已经被删掉的文件,从库去拉取时直接报错找不到文件。补救办法是PURGE BINARY LOGS TO 'xxx'让数据库重建索引,但如果你删的是最后一个之后的文件,重建逻辑可能不符合预期。结论就是永远别手工删,一律用PURGE。
坑三:在从库上找主库的位点。前面提过,这里再强调。从库的SHOW BINARY LOG STATUS给的是从库自己写 binlog 的进度。你想知道它同步到主库哪里了,必须看SHOW REPLICA STATUS里的Relay_Master_Log_File(主库的哪个文件)、Exec_Master_Log_Pos(主库的哪个位点已执行完)、Read_Master_Log_Pos(已读到哪)。Read和Exec两个值不一样的时候,说明从库正在追,中间有延迟。
坑四:大事务把 binlog 缓存挤爆。报错信息类似Multi-statement transaction required more than 'max_binlog_cache_size' bytes of storage。这说明单个事务产生的 binlog 缓存超过上限。此时SHOW BINARY LOG STATUS看位点可能没推进,看起来像“卡住了”。处置方式是调大max_binlog_cache_size,或者把大事务拆小。这个参数和binlog_cache_size是两个东西,前者是上限,后者是每个会话的初始缓冲,别配反了。
坑五:容器里mysqlbinlog找不到文件。因为mysqlbinlog要在能访问datadir的命名空间里跑。如果数据库跑在容器里,你得exec进去执行,或者把 binlog 文件拷出来再在宿主机解析(注意拷贝正在写的文件要用--read-from-remote-server或者先FLUSH LOGS让当前文件切换)。
6. 运维层面的经验:位置管理该怎么做
聊完“在哪”和“怎么读”,最后说说“怎么管”。这部分是区分“会用”和“用得住”的地方,也是很多团队出问题的地方。
6.1 保留策略与磁盘估算
binlog 占磁盘是个慢性的问题。默认max_binlog_size是 1GB,binlog_expire_logs_seconds默认是 30 天(2592000 秒)。如果你的库写入量是每天 20GB 的 binlog,30 天就是 600GB,加上数据本身,磁盘规划立刻变得紧张。
我的估算方法是:先看SHOW BINARY LOGS里几个文件的尺寸和生成时间,算出日均增量,再乘以保留天数,最后留 30% 余量。比如日增 20GB、保留 7 天,就需要 140GB 加上余量约 180GB 专门给 binlog。这个数字要写进容量规划文档,不能拍脑袋。
保留天数怎么定?取决于两个约束:最长的全备周期和能接受的恢复点目标。如果每天一次全备,那 binlog 至少要保留 1 天多一点,保证你总能用“昨天全备 + 今天 binlog”恢复到任意时间点。保守一点给 7 天,出了事故有回旋余地。如果你有下游消费程序依赖 binlog(比如数据同步、审计),那保留时间还得满足它们的追平窗口,工具挂了两天修好,日志还在才行。
清理方式统一用:
PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY);或者按文件:
PURGE BINARY LOGS TO 'binlog.000045';执行前一定确认没有从库还在依赖更早的文件。如果从库已经断了好几天,你一清理,它就连不上了,只能重建。所以清理前先看一眼SHOW REPLICA STATUS的Relay_Master_Log_File,这个习惯能救你一次。
6.2 binlog 与备份的关系
很多人以为“有 binlog 就等于有备份”,这是个危险的理解。binlog 是增量,没有全备做基点,它恢复不出任何完整状态。反过来,只有全备没有 binlog,你只能恢复到备份那一刻,出了事故会丢掉备份之后的所有数据。
两者配合的完整链路是:全备记录位点或 GTID 集合 → 增量 binlog 从该位点开始重放 → 恢复到目标时间点。所以每次做全备,务必记下当时的 binlog 位点,写进备份元数据里。我用过的一个简单做法是在全备完成后立刻执行SHOW BINARY LOG STATUS并把输出存成文本文件,跟备份放一起。这个动作花不了两秒钟,但恢复的时候能省掉大量推算。
还有一点:sync_binlog参数直接决定了你能恢复到什么程度。sync_binlog=1表示每次事务提交都刷盘,最安全但性能有损耗;sync_binlog=0交给操作系统刷,机器掉电可能丢最近的 binlog。大部分生产环境我会设成 1。这个参数和innodb_flush_log_at_trx_commit一起构成“双一”配置,是两个不同的持久化保证,别只配一个。
6.3 该监控哪些指标
最后列几个必须上监控的项,都是能提前发现问题的。
- binlog 目录占用空间:占满分区是硬故障,数据库直接写不进去。
- binlog 文件增长速度:增速异常往往意味着有批量操作或者事务没控制好。
- 主从复制延迟:
SHOW REPLICA STATUS里的Seconds_Behind_Source,虽然有不准的时候,但趋势有参考价值。 - binlog 写入错误数:
SHOW GLOBAL STATUS LIKE 'Binlog_cache_disk_use',这个值持续增长说明大事务太多,binlog_cache_size不够,事务在往磁盘溢写,性能会明显下降。 Binlog_cache_use与Binlog_cache_disk_use的比例:前者是用了内存缓存的事务数,后者是溢写到磁盘的。如果后者占比超过 10%,就该调参数或者优化业务事务了。
这些指标在大多数监控系统里都能直接采到,成本不高,但能让你在磁盘写满之前两周就收到告警。
我个人在实际操作中的体会是,这个问题之所以问的人多,不是因为答案难,而是因为提问的人常常没有先分清自己到底想知道哪一层。先跑一遍SELECT @@datadir, @@log_bin_basename和SHOW BINARY LOG STATUS,把物理路径和当前位点这两个坐标拿到手,剩下的问题基本都能顺着往下推。真要我给一句最实用的建议,那就是:在你还没出事的时候,先去自己的库上把这几条命令跑一遍,把输出记下来。等到凌晨三点手抖删错表的时候,你是没有心情现学参数的。