ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL binlog开启与配置:从原理到生产实践

MySQL binlog开启与配置:从原理到生产实践 1. 为什么要开启binlog它远不只是“日志”那么简单不管你是刚接触MySQL的开发者还是正在从单机库走向主从架构的运维新手binlog这几个字几乎绕不开。它不像数据文件那样直接决定服务能不能启动也不像缓存参数那样能立刻带来性能提升但一旦遇到主从同步、误删数据恢复、数据审计这类问题没有binlog的你基本只能干瞪眼。我自己的感受是一个规范化管理的MySQL实例binlog是标配绝不是可选项。简单来说binlog是MySQL的二进制日志记录的是所有对数据产生变更的操作比如insert、update、delete、create table、drop table等。它不会记录select、show这类查询语句。如果你希望把所有访问都记录下来那是另一张叫general log的普通查询日志跟binlog完全不是一回事。很多人会问那binlog和MySQL的redo log、undo log有什么区别这里用生活化的方式来理解redo log像记账员的草稿本专门负责数据库崩溃后恢复保证“账不能错”它是物理级的记录的是页的修改undo log则像是修改前的留底主要用于事务回滚和MVCC多版本快照。而binlog是“正式公布出去的流水账”它是逻辑级的记录的是“做了哪些改变”跟着这条流水其他人可以把数据复制一份也可以在灾难后用这条流水重新“做一遍账”。所以binlog的实际价值集中在三块主从复制和读写分离时从库靠它拿到主库的变更然后重放数据恢复时配合全量备份可以把数据库恢复到某个时间点或者跳过误操作做数据审计和追溯时binlog能告诉你某个表在某个时间段发生了什么变更。如果你是刚搭了一套MySQL还没开过binlog那我强烈建议你尽快补上。后面我会把从原理到实操、再到生产中踩过的坑完整讲一遍。2. 开启binlog前必须搞懂的一组参数网上很多教程直接丢一行log_binmysql-bin但也正是因为少了几个关键参数很多新手重启后都发现没生效或者一开就出问题。开启binlog不是一个参数的事而是一组参数协同工作。我们先逐个搞明白。2.1 server-id能不能开起来的硬性前提server-id是一个全局标识符用来区分集群里的不同MySQL实例。取值范围是1到4294967295每个实例必须设置成不一样的值。当你启用binlog时MySQL内部会自动要求配置一个server-id如果没配置或者配置成了0很有可能在启动时直接报错或者相关功能不正常。我之前接手过一个生产库同事只是在[mysqld]段加了log_binmysql-bin忘写server-id结果MySQL直接拒绝启动日志里写着相关的错误。后来补上server-id1一切正常。所以这台机器不管是不是要做集群只要开启binlog就先给它一个唯一的server-id养成好习惯。2.2 log_bin指定日志文件的基础名称这个参数负责打开binlog并定义二进制日志文件的名称前缀。可以配置相对路径也可以写绝对路径。相对路径一般会被写到默认数据目录下比如log_bin mysql-bin如果不写绝对路径生成的日志文件会像mysql-bin.000001、mysql-bin.000002这样递增。我个人建议在生产环境里给一个明确的绝对路径比如log_bin /var/lib/mysql/mysql-bin注意这个参数一旦在配置文件中被设置MySQL就会开启binlog。即使服务已经启动想通过set global log_binON来动态开启是不行的这是个只读系统变量必须重启MySQL才能生效。很多初学的朋友会踩这个坑在网上搜到“动态开启”的教程其实那不是同一个变量。2.3 binlog_format决定binlog到底记录成什么格式这是非常关键的一步。binlog支持三种格式STATEMENT记录SQL语句本身日志量小但某些带有随机函数或不确定事件的语句在主从复制时会导致数据不一致ROW记录每一行数据的具体变更日志量大但最可靠推荐使用MIXEDMySQL自动判断一般情况下用STATEMENT遇到不确定性操作时自动切成ROW。从MySQL 5.7.7开始默认格式就是ROW。为什么推荐ROW因为它在恢复、复制时最不容易出错。比如你执行一把UPDATE t SET nameabc WHERE id 100STATEMENT格式只记录这条SQL从库执行时如果表的索引或者扫描方式有差异可能导致更新的行数不一样而ROW格式会明确记录每一行被如何改动不管从库怎么执行最终数据是一致的。代价也很明显ROW格式的binlog文件膨胀得快特别是大量更新、删除操作。为了改善这一点还有一个配套参数叫binlog_row_image设为MINIMAL的话只记录被修改的那一列和唯一键不走全字段快照能大大缩小日志量。不过要谨慎只有在确认不需要读取旧字段值时才用MINIMAL否则默认的FULL最稳。2.4 sync_binlog安全性和性能的平衡开关这个参数决定MySQL往二进制日志缓存写入多少事务后再强制地把缓存同步到磁盘。它有三个常见取值0不主动刷新交给操作系统决定何时落盘性能最好但进程崩溃可能丢掉最后一部分binlog1每个事务提交前都刷一次磁盘最安全能最大程度保证binlog不丢但性能略差N每N个事务提交后刷一次磁盘是相对的折中方案。如果只开binlog而sync_binlog0主从复制一旦遇到MySQL异常崩溃从库拿到的binlog可能落后于主库甚至主库自己恢复时也会发现binlog记录不完整。对于大多数在线业务sync_binlog1仍然是一个比较推荐的默认值尤其搭配innodb_flush_log_at_trx_commit1时能让事务在崩溃恢复期间保持相对一致。如果对实时性要求不高的批处理场景可以适当放宽。2.5 日志大小与保留时长不做好清理迟早被磁盘塞满还有一些参数看起来不起眼但到了生产环境很容易出事。比如max_binlog_size 512M expire_logs_days 14max_binlog_size表示单个binlog文件到达多大时就切换生成下一个文件默认是1G。配置得小一点比如512M配合定期flush logs会更容易管理。expire_logs_days是控制自动清理的默认是0不清理这意味着日志文件会无限增长直到把你的磁盘写满。很多线上事故就是这么来的。在MySQL 8.0里官方更推荐使用binlog_expire_logs_seconds来设置保留秒数比如保留14天binlog_expire_logs_seconds 86400 * 14不过这个参数在部分旧版本里不存在需要先确认你自己的版本。3. 实操在不同环境下安全开启binlog前面都是理论铺路现在来说动手。生产环境修改binlog配置必须重启实例所以操作前最好申请一个维护窗口并且在测试环境先跑一遍。以下是我自己反复验证过的一套流程可以照着抄。3.1 确认当前实例状态第一步永远是检查当前的binlog是否开启避免重复操作或者误判SHOW VARIABLES LIKE log_bin;如果结果是OFF说明没开。还可以看一下当前的参数情况SHOW VARIABLES LIKE server_id; SHOW VARIABLES LIKE binlog_format;顺手把当前数据目录大小和磁盘剩余空间都看一眼df -h这一步很关键因为一旦开启binlog日志文件会持续增长如果你预留空间不足可能撑不过半天。3.2 备份并修改配置文件找到MySQL的配置文件。Linux常见路径是/etc/my.cnf也有的在/etc/mysql/mysql.conf.d/mysqld.cnfWindows是my.ini。改之前先备份cp /etc/my.cnf /etc/my.cnf.bak.$(date %F)然后在[mysqld]段下添加或调整配置。我给出的生产环境常用模板如下[mysqld] server-id 1 log_bin /var/lib/mysql/mysql-bin binlog_format ROW binlog_row_image FULL sync_binlog 1 max_binlog_size 512M binlog_expire_logs_seconds 1209600如果你想做基于GTID的复制还可以加gtid_mode ON enforce_gtid_consistency ON但要注意GTID参数在单机热切换时也有限制如果你的架构还没准备好暂时不要一起开。3.3 执行重启并验证配置改好以后重启MySQL服务。不同发行版略有不同systemctl restart mysqld或者service mysql restart重启后先检查版本和基本状态SHOW VARIABLES LIKE log_bin;如果返回ON说明大功告成。接着查看当前正在写入的日志文件和位置SHOW MASTER STATUS; SHOW BINARY LOGS;前者会输出类似mysql-bin.000001和Position值后者会列出当前所有binlog文件和大小。到这里binlog就算彻底开起来了。不过要说清楚这里没有一步是能动态完成的log_bin这门“门”必须通过重启打开。如果你实在无法接受重启带来的连接中断可以考虑在低峰期做或者把MySQL做成多实例滚动切换那属于另一个话题了。3.4 用启动参数临时模拟不推荐有人会问能不能先在启动命令行加上--log-bin/tmp/mysql-bin不起服务重启实际上这也会触发实例重启而且假如你是通过服务管理的命令行参数会被配置覆盖。我建议在正式环境别走这条路老老实实改配置文件最稳妥。4. binlog的验证与日志内容速查开了binlog不能只看到log_binON就放心。真正要确认的是数据库的每一次变更都已经稳定写入到了binlog文件里。4.1 做一个最小实验验证写入先创建一个临时库和表然后插入几条数据CREATE DATABASE IF NOT EXISTS test_binlog; USE test_binlog; CREATE TABLE t_demo (id INT PRIMARY KEY, name VARCHAR(20)); INSERT INTO t_demo VALUES (1, alice), (2, bob);接着刷新或者直接查看当前最新的binlogSHOW BINLOG EVENTS IN mysql-bin.000001 LIMIT 10;这个命令会看到类似下面的输出-------------------------------------------------------------------------------------------------------- | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | -------------------------------------------------------------------------------------------------------- | mysql-bin.000001 | 4 | Format_desc | 1 | 125 | Server ver: 8.0.32, Binlog ver: 4 | | mysql-bin.000001 | 125 | Previous_gtids | 1 | 158 | | | mysql-bin.000001 | 158 | Anonymous_Gtid | 1 | 227 | SET SESSION.GTID_NEXTANONYMOUS | | mysql-bin.000001 | 227 | Query | 1 | 306 | use test_binlog; CREATE DATABASE ... | ...看到Query或Write_rows事件说明binlog确实把变更动作记下来了。4.2 使用mysqlbinlog解析内容如果你配置的是binlog_formatROW直接用cat打开binlog会是一堆乱码因为它里面存储的是二进制行数据。需要借助MySQL自带工具mysqlbinlog来解码。下面这条命令最常用可以解析出可读的SQL格式内容mysqlbinlog --base64-outputDECODE-ROWS --verbose /var/lib/mysql/mysql-bin.000001加--verbose之后ROW格式默认会在注释里以伪SQL形式展示每一行的变更。如果你只想看某个时间段的日志可以加--start-datetime和--stop-datetime。如果想跳过某些GTID可以加--skip-gtids这在从库奔溃点恢复时非常有用。有一点要注意如果binlog里有敏感数据解析结果也会包含明文。所以binlog文件本身和它的备份文件权限一定要控制好。生产环境里我通常会把/var/lib/mysql目录设置为MySQL用户可读其他用户一律拒绝访问。4.3 从库视角的验证思路如果你是为主从复制开启的binlog光看主库写入还不够从库会需要另外两个配置relay_log和read_only。不过从库不一定要开启自己的binlog。如果你想做级联复制即A - B - CB作为中转节点就必须也开启binlog。怎么判断B是否把收到的变更继写存到binlog里可以先在B上执行一些写操作然后SHOW BINLOG EVENTS如果看不到对应事件那就说明B没有生成自己的binlog或者设置不正确。5. 开启后最常见的坑和处理方案我自己和身边朋友在实际维护中几乎把这里容易踩的坑踩了一遍。列成一张速查表你遇到了可以直接对照。现象可能原因处理方式重启后log_bin仍然OFF配置写错了段路径无权限确认写在[mysqld]段下检查日志目录属主和权限MySQL启动直接失败缺少server-id日志路径不存在设置唯一server-id创建目录并授权给mysql用户磁盘空间快速增长没设保留时间单文件太大设置expire参数调小max_binlog_sizepurge binary logsROW格式解析出来是乱码没有加--base64-outputDECODE-ROWS加参数并配合--verbose主从复制报错server_id冲突各实例server-id重复给每个实例分配不同IDbinlog不切换文件未到max_binlog_size未触发flush手动执行FLUSH LOGS切分再展开说几个高频坑。5.1 日志目录权限不对服务起不来我曾经在配置里写了log_bin /data1/mysql-bin但/data1目录属于rootMySQL用户没有写权限。重启时实例直接failed排查日志才看到“failed to open log”之类的错误。解决方式很直接mkdir -p /data1 chown mysql:mysql /data1 chmod 750 /data1权限这块不要给得太宽松数据库文件和工作目录必须掌握在最小权限范围内。5.2 expire_logs_days 不生效很多同学设置了expire_logs_days 7但过了一个月发现binlog还在。原因是如果binlog文件被某个从库还正在使用中MySQL不会自动清理此外如果你的从库长期连接不上主库可能认为这个文件“还有人要”从而拒绝删除。还有一个点expire_logs_days只在文件发生轮转或被访问时才会扫描不一定会做到分钟级精确清理。想立刻清理已经写完的文件可以先FLUSH LOGS把当前文件轮转再执行PURGE BINARY LOGS TO mysql-bin.000099;这条命令会删除指定编号之前的binlog。注意如果某个从库还在从更早的位置读取强制清理会导致复制中断所以执行前一定要先确认所有从库的Relay_Master_Log_File和Exec_Master_Log_Pos都已越过要删的文件。5.3 大事务容易让binlog缓存溢出如果你一次性执行一个几十万行的update事务产生的binlog会先被写入缓冲区binlog_cache_size默认只有32KB。一旦超过MySQL会把这部分日志临时写到磁盘的tmp目录。所以在大量写库场景下这个参数值得调大比如设置到1M或者更大。不过也不建议无限调大因为每个连接都可能占用这份缓存独立线程会按需分配过大容易吃掉内存。binlog_cache_size 1M max_binlog_cache_size 16M后面这个max_binlog_cache_size是限制所有线程缓存的总和。如果设置为0代表不限制但生产环境强烈建议设一个合理上限防止内存被打爆。6. binlog在真实场景中的三个高价值用法开启binlog之后它能在很多关键场景里帮上大忙。这里挑三个我实际用过、也能立刻见效的方向。6.1 玩转主从复制position和GTID两种方式主从复制最基础的原理就是从库去主库拉binlog然后写入从库自己的relay log再按顺序执行。开启binlog只是第一步要真正跑起来还需要配置复制账号CREATE USER repl% IDENTIFIED BY 强密码; GRANT REPLICATION SLAVE ON *.* TO repl%;然后在从库上执行CHANGE MASTER TO MASTER_HOST主库IP, MASTER_USERrepl, MASTER_PASSWORD强密码, MASTER_LOG_FILEmysql-bin.000001, MASTER_LOG_POS158; START SLAVE;如果是MySQL 8.0且开启了GTID直接用SET GLOBAL.GTID_PURGED然后指定MASTER_AUTO_POSITION1就方便很多。GTID是全局事务标识符可以让主从自动找位置不用手算binlog文件名和position故障切换时的体验好非常多。6.2 从误删到恢复基于binlog做时间点回放有一天凌晨某个业务开发执行了一条不带where条件的删除等发现再来找你全表已经被清空了。如果这个库开启了binlog并且你有最近的全量备份那么恢复路径是用全量备份恢复到事故发生前的那个时间点用binlog回放全量备份之后到误删之前的那部分操作避开误删语句本身把业务需要的后续数据找回来。具体操作类似这样mysqlbinlog --start-datetime2024-05-20 00:00:00 --stop-datetime2024-05-20 03:30:00 mysql-bin.000018 | mysql -uroot -p test_db注意恢复前一定要提前看binlog里到底有哪些语句尤其是drop、truncate这类DDL它们无法被普通事务回滚必须手动处理。如果binlog里夹杂了一些别的库的表操作原样灌入会导致同名的表冲突这时就要用到--databasetest_db之类的过滤参数。这个场景想真正省心光靠“开启binlog”还不够还需要配套定期全备。binlog只能保证“全备之后”的增量部分不丢如果没有任何基础备份binlog回放也无从谈起。我在做模拟演练时经常说binlog是肉全备是骨架单独一个都不够。6.3 追踪数据变更做简单审计有些业务需要知道某个关键表在某个时间段发生了哪些修改。如果没有开启binlog你只能依赖数据库自身的一般日志低频、大体积、不可控。而有了ROW格式的binlog就可以用mysqlbinlog把变更检索出来。举一个实际例子线上某张订单表的价格被改错了需要定位是谁在什么时候改的。虽然binlog默认不记录用户账号MySQL audit plugin可以增强但至少能看到变更发生的时间点、执行事务的执行server-id、修改前后的具体字段值。配合应用日志或代理层日志就能追到具体请求。对于更严格的审计要求可以考虑MySQL Enterprise Audit或者第三方插件不过在中小团队用binlog做基础的变更回溯成本低且效果立竿见影。7. 生产环境维护binlog的几条实践心得最后聊几点我真正干活时沉淀下来的经验希望能帮你在部署时少走弯路。第一个心得是如果没有监控就不要开启binlog这句话可能有点极端但很实在。因为我在实际维护中见过太多次“打开后忘记清理磁盘爆满”的案例。binlog文件的增长速率取决于你业务的写频繁程度绝不能想当然。上线后要盯住两个指标一是binlog目录的磁盘使用率二是SHOW BINARY LOGS里文件的总大小。哪怕只是写一个简单的定时任务每天把binlog文件列表和大小输出到监控日志里都能提前发现问题。第二个心得是把binlog和全量备份放到同一个方案里去设计而不是“先开了再说”。我推荐的原则是开启binlog后全量备份频率可以适当放宽因为从全备到故障那一刻之间的数据理论上都能通过binlog补回来。但前提是你必须验证过“全备binlog回放”的流程。光在测试库上试成功还不够最好能定期做一次恢复演练尤其是在大版本升级之后binlog的格式可能有细微变化旧版本工具读新版本日志偶尔会遇到兼容性问题。第三个心得是flush logs这个命令在关键时刻很有用。比如做一次全量备份之前先执行FLUSH LOGS;它会关闭当前binlog并创建一个新编号的日志文件。这样全量备份和binlog的衔接就会非常干净备份完成后你只需要记住备份之前最后一个binlog的position就能轻松确定增量恢复的起点。这类细节在遇到真实事故时能帮你少掉很多头发。第四个心得是binlog的权限要像密码文件一样去保护。ROW格式下binlog里几乎能看到所有行的完整字段包括手机号、邮箱、地址等敏感数据。如果有第三方角色需要读取日志做分析尽量通过mysqlbinlog工具分发解析后的必要内容不要把原始文件随意拷贝到其他服务器。更稳妥的做法是单独准备一台内网跳板机做解析并限制访问人员。第五个心得是不同MySQL版本之间的默认值和变量名有差异。如果你是从旧版本迁移到8.0要重点检查binlog_expire_logs_seconds和expire_logs_days这两个参数的兼容关系8.0已经将后者标记为废弃但为了安全起见升级后我会先用SHOW VARIABLES LIKE %expire%确认最终生效的是哪个值。类似地log_bin变量从5.6之后变成了只读的不要试图在运行中去改它。最后说一个小技巧想快速确认某个binlog文件里包含哪些库和表的变更可以不用把整个文件解析出来先用mysqlbinlog --no-defaults --verbose --base64-outputDECODE-ROWS mysql-bin.000030 | grep -E INSERT INTO|UPDATE|DELETE FROM | head先看个大概再按需深入。这种粗筛思路在处理超大binlog时比一上来就整文件解码要高效得多。我自己用过一阵子ROW格式后确实能明显感觉到日志文件比STATEMENT大不少但用过它做过一次精确恢复后我从此再也不想切回STATEMENT。数据库这件事最怕的就是“看似节省一点空间关键时刻给不了确定性”。如果你问我现在该不该开binlog我的答案是开并且认真对待它。它不是一颗解百病的灵丹但它是你在数据库事故里最后一根可靠的安全绳。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进