ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL 9主从复制核心链路:binlog流转、延迟排查与GTID切换实战

MySQL 9主从复制核心链路:binlog流转、延迟排查与GTID切换实战 开头不用提示符直接输出。下面开始写正文。前阵子帮一个朋友处理线上的主从切换事故Prometheus从凌晨两点开始报警主库负载飙到接近百分之百切换命令敲下去之后从库迟迟赶不上主库的位点业务被迫只读了将近半个小时。那个场景让我再次确认了一件事MySQL的主从复制架构平时看起来就是几行配置文件、一条CHANGE REPLICATION SOURCE TO真正出问题的时候决定你能不能稳住局面的全在于你对复制原理和优化手段的理解有多深。MySQL 9发布之后很多人跑过来问我这套复制架构是不是又变了。我的答案是核心链路没变但版本演进把一些旧习惯逼到了墙角。这篇文章就围绕MySQL 9从binlog如何流转、复制线程如何协作到延迟怎么定位、半同步怎么配、GTID怎么做切换再到我实际维护中踩过的坑和优化清单一次讲透。1. MySQL 9的复制底座从版本号聊起认清哪些机制没变1.1 为什么MySQL 9值得关注MySQL 9属于Innovation版本Oracle的版本节奏已经改成了LTS加Innovation的双轨制8.4是LTS9.x是创新版本。很多人一听Innovation就觉得不稳定的测试版其实这个定位更多是生命周期上的区别8.4会维护很多年9.x的维护窗口短下个大版本出来就要跟着升。但在功能层面MySQL 9拿到了不少新东西比如新的向量数据类型、部分系统变量的动态调整能力以及一些跟复制相关的默认行为变化。对我这种长期做数据库运维的人来说MySQL 9最需要关注的不是某个酷炫新功能而是它把过去容易踩坑的默认配置给改了。举个例子mysql_native_password插件在9.0里默认被禁用所有新建用户必须走caching_sha2_password。这在复制场景里有个直接影响如果从库连接主库的用户还是老的native密码复制线程会直接认证失败。我见过不少升级后复制断掉的案例根因就这一个。回到复制本身主从复制的核心骨架从MySQL 5.5、5.6一路到现在本质上没有翻天覆地的变化主库产生binlog从库拉取binlog写入relay log然后回放。MySQL 9只是在这个骨架上做优化和清理但理解这套链路的基础逻辑依然是排查所有复制问题的前提。1.2 binlog与复制链路核心结构binlog是复制的绝对核心。它记录的是主库上所有可能导致数据变更的事件包括数据行变更、DDL、事务提交等。MySQL 9的主从复制依然依赖binlog这一点没有变。这里有一个很多人忽略的点binlog不是InnoDB自己产生的而是MySQL Server层产生的。InnoDB引擎层也有自己的redo log二者是两套独立的日志体系。为了让它们在崩溃恢复后保持一致MySQL专门设计了两阶段提交事务在InnoDB里进入prepare状态然后写binlog最后在InnoDB里进入commit状态。这套机制保证了binlog里有这个事务InnoDB里就一定有反过来也一样。正因为两阶段提交的存在复制的一致性才有了基础。从库回放的binlog事件对应的就是主库上已经提交成功的事务不会出现半截事务。这个点一会儿讲半同步和数据丢失的时候还会用到。从库这边复制由三个角色协作完成主库的dump线程把binlog内容通过网络推给从库。从库的IO线程负责接收binlog并写入本地的relay log。从库的SQL线程在并行复制模式下是coordinator线程加worker线程负责读取relay log并应用。这个链路从MySQL 3.23就有了到现在依然是复制的地基。我在给团队做培训时经常说你什么时候能把这三个线程各自的职责、状态字段和常见故障背清楚什么时候才算真正入了复制的门。2. 一条事务从主库到从库的完整旅程binlog、relay log与复制链路拆解2.1 binlog格式的选择ROW、STATEMENT与MIXED的取舍MySQL 9的默认binlog格式是ROW这也是我强烈建议所有线上环境使用的格式没有之一。为什么因为ROW格式记录的是每一行数据变更前后的完整镜像从库回放时按主键定位目标行不依赖当前会话变量、不依赖函数结果、不依赖存储引擎状态。而STATEMENT格式记录的是SQL语句本身看起来省空间但一个UPDATE ... WHERE create_time NOW()在主从两边执行出来的结果可能完全不同UUID()、RAND()这种非确定性函数更是复制的一致性问题制造机。MIXED格式看起来两全其美MySQL自己判断用哪种事件但实际运维中很难精准控制边界一旦判断失效问题依旧。ROW格式不是没有代价最直接的就是binlog体积变大。一个全表UPDATESTATEMENT可能就几百字节ROW可能生成几十MB。所以配套的参数是binlog_row_image默认FULL记录完整前像后像可以设为MINIMAL只记录变更列和主键。我的建议是不要为了省那点磁盘空间而全局改成MINIMAL因为它会影响一些工具对binlog的解析比如某些CDC组件需要完整前像来做数据比对。除非你的binlog消费方确认支持MINIMAL否则保持FULL更稳妥。2.2 dump线程、IO线程与SQL线程的协作方式一条事务在主库提交之后会被写入binlog文件。从库发起复制连接后主库的dump线程开始从指定位置读取binlog并推送给从库。这里有个容易忽略的细节dump线程是每个从库一个推流是基于TCP协议主库不会做任何持久化确认这也就是异步复制丢数据的根源所在。从库的IO线程拿到binlog事件后会把它追加写入本地的relay log。注意relay log的格式和binlog基本一致只是命名不同。IO线程写入relay log是顺序写所以正常情况下非常快性能瓶颈很少出现在这里。之后SQL线程开始干活。在没有并行复制的情况下SQL线程串行读取relay log并回放事务。这个串行模型意味着主库并发再高从库应用的速度上限就是单线程的速度。这也是MySQL 5.6时代主从延迟那么常见的原因。到了MySQL 5.7并行复制出现8.0又做了不少改进但从本质上说从库的回放能力取决于并行度能不能匹配主库的写入并发度。2.3 并行复制MTS与协调器/工作者线程目前从库并行复制的工作方式是这样的coordinator线程读取relay log把可以并行的事务分发给多个worker线程执行。并行度怎么来早期是按database分库并行也就是不同库的事务并行执行。但一个库内有大量写入的场景下这种并行度等于没用。MySQL 8.0之后引入了基于逻辑时钟LOGICAL_CLOCK的并行复制以及基于writeset的依赖追踪。简单说主库上同时进入组提交阶段的事务在从库上大概率可以并行回放。控制这个行为的关键参数是binlog_transaction_dependency_tracking可选值是COMMIT_ORDER、WRITESET、WRITESET_SESSION。默认是COMMIT_ORDER也就是只按组提交的commit事件来判定依赖关系。改成WRITESET之后MySQL会利用writeset判断事务之间是否存在行冲突没有冲突的事务即使提交时间有先后在从库上也能并行执行。对于高并发、热点行不明显的业务这个参数能非常明显地提升从库回放速度。配套参数还有两个replica_parallel_workers从库并行的worker线程数一般设4到8设太高会加剧从库的锁竞争和上下文切换。replica_preserve_commit_order默认为ON保证并行执行的事务在从库上的提交顺序和主库一致。这个需要开启否则遇到异常情况从库的数据状态可能和主库不一致。3. 主从延迟的三大根因与定位思路从秒级到分钟级的实战排查3.1 大事务与DDL导致的复制停滞主从延迟是最常见的复制问题。我见过最夸张的一次从库延迟到了六千多秒业务方已经决定要删库跑路了。排查下来根因就是主库上有个业务跑了个全表UPDATE一次更新了两亿行。ROW格式的binlog把每一行的前后镜像都写下来了dump线程推给从库从库的worker线程执行这个事务时要一行一行更新单事务内又不能拆开提交延迟直接爆炸。这种延迟本质上是复制停滞不是复制变慢而是单个事务太大从库必须一口气执行完中间没法并行也没法跳过。定位方法很简单打开SHOW REPLICA STATUS\G看Replica_SQL_Running_State如果长时间停留在某个事务的执行状态再配合SHOW PROCESSLIST看SQL线程正在跑什么SQL基本就能确认。处理办法从根源上杜绝大事务。应用侧把一次性更新拆成批次每批几千行分批提交DDL用gh-ost或pt-osc这类工具做在线变更而不是直接在主库上跑原生ALTER TABLE。这个经验说起来容易真正落实要靠日常巡检去抓主库上的长事务和长执行语句。3.2 从库负载过高与单线程瓶颈另一种常见的延迟场景主库写入并不大但从库的机器负载很高SQL线程回放本身就慢。这种往往是从库上承载了太多分析类查询导致的。从库既是复制的目的地又是报表、数据抽取、备份的读取源一堆慢查询把CPU和IO耗掉SQL线程拿不到资源延迟自然就起来了。定位这种问题不要只看延迟秒数要看从库的整体资源画像。用top看CPU、用iostat看磁盘util、再看SHOW ENGINE INNODB STATUS里有没有明显的信号量等待。如果是从库查询拖垮的就需要做读写分离的物理隔离也就是所谓的一主多从各司其职——一台从库专门做复制容灾不接任何业务查询其他从库各自承担不同的分析任务。还有一个方向是检查SQL线程是否真的并行起来了。如果replica_parallel_workers设了8但业务写入几乎都带同一个TRX_ID前缀或者大量操作同一张表并行度根本打不上去。这时候要回到binlog_transaction_dependency_tracking和业务表设计层面去看。3.3 Seconds_Behind_Source 的局限与更精准的延迟度量SHOW REPLICA STATUS里的Seconds_Behind_Source旧版叫Seconds_Behind_Master是大家最依赖的延迟指标但它真的不是精确值。它的计算逻辑是当前系统时间减去relay log里最后执行事件的时间戳所以至少有四个问题从库和主库系统时钟不一致时偏差直接反映到指标里。如果SQL线程没有在跑任何事务这个值可能显示为0实际relay log里还有一堆没执行完的。主库某段时间没有写入从库执行完最后事件后指标会停在某个值直到下个事务进来才更新。大事务执行期间它只能反映当前正在执行的事务起点距现在的时间不能反映事务内部完成度。更靠谱的做法是用pt-heartbeat这类工具在主库上周期更新一张心跳表从库去读这张表通过时间差计算真实延迟精度可以达到秒级以下。尤其是在做主从切换决策时不要相信Seconds_Behind_Source0一定要用心跳表数据确认从库已经追平。这是我踩过的坑后面会细说。4. 半同步复制与GTID把不丢数据从口号变成配置4.1 半同步复制的原理与参数落地异步复制的最大风险是主库提交完事务、还没等binlog推给从库就宕机了这个事务永远到不了从库业务希望不丢数据就成了一句空话。半同步复制为了解决这个问题加了一个握手环节主库的dump线程在推binlog时会等待从库的IO线程写relay log之后给一个ACK收到ACK主库的提交才算真正完成。注意是等从库的IO线程写完relay log就ACK并不是等SQL线程回放完成。这个区别后面会展开。落地半同步需要装插件并设置参数主库和从库都要装INSTALL PLUGIN rpl_semi_sync_source SONAME semisync_source.so; INSTALL PLUGIN rpl_semi_sync_replica SONAME semisync_replica.so; SET GLOBAL rpl_semi_sync_source_enabled ON; SET GLOBAL rpl_semi_sync_replica_enabled ON;MySQL 8.0之后主库有两个重要参数rpl_semi_sync_source_timeout主库等待ACK的超时时间默认1000毫秒。超时后会降级为异步复制继续提供服务。rpl_semi_sync_source_wait_no_replica当没有可用从库时是否继续等待默认ON。处理ACK的方式上MySQL 8默认是AFTER_SYNC也就是先把事务的binlog刷到磁盘再等待从库ACK然后才给客户端返回提交成功。这个顺序保证了任何返回客户端已提交的事务至少已经有一份binlog躺在从库的relay log里。AFTER_COMMIT模式则相反事务在本库先提交成功返回再补等一个ACK这种模式下从库理论上是有可能缺失最近提交的事务的所以不推荐也没必要主动切过去。4.2 半同步降级与数据丢失的实际场景半同步复制不是万能的这个必须讲清楚。首先它能保证的是从库收到了binlog不代表从库成功应用了事务。如果从库在写完relay log后、SQL线程还没执行时宕机重启后binlog事件可能还在relay log里但如果relay log损坏或者从库被重建这部分数据就丢了。其次半同步有降级机制。主库等ACK超过timeout后会默默切回异步复制。这种降级本身没有报错只是日志里多了几条warning。如果恰好降级后主库宕机数据照样丢。所以说半同步不是保险箱它只能把丢数据的窗口缩小到主库已经提交、但binlog还没推给从库且半同步尚未生效或已降级的那一小段。对于核心业务更稳的方案是走MySQL Group Replication组复制用Paxos协议在多数派节点上确认数据从机制上把多数派没收到就不算提交固化下来。如果一定要用主从复制那就必须接受半同步的边界并配套做好备份和延迟监控。4.3 GTID与AUTO_POSITION让主从切换不再靠手工定位GTID全局事务标识符是我进入任何一个MySQL环境后第一个检查的东西。没有GTID的主从复制每次切换都要去翻主库的binlog文件名和position稍微一疏忽就配错位点然后从库要么重复执行事务要么漏执行事务。GTID的格式是UUID:事务序号比如59f1a7c2-0c4b-11ef-9a3e-00155d0f8300:1-458。每个事务在主库提交时都会被分配一个全局唯一的GTIDbinlog里记录了GTID事件。由于GTID全局唯一从库天然知道自己已经执行过哪些事务新连上主库时可以通过SOURCE_AUTO_POSITION1自动协商binlog拉取位点不再需要人为指定file和position。配置GTID模式的复制核心步骤是CHANGE REPLICATION SOURCE TO SOURCE_HOST192.168.10.11, SOURCE_USERrepl, SOURCE_PASSWORDxxx, SOURCE_AUTO_POSITION1, GET_SOURCE_PUBLIC_KEY1; START REPLICA;GET_SOURCE_PUBLIC_KEY1是MySQL 8/9里连接caching_sha2_password认证用户时的必要参数不加这一句复制线程会卡在等待公钥上这也是升级后常见的问题点。做了主从切换时GTID的优势就体现出来了。新主库上执行RESET MASTER之后从库重新CHANGE REPLICATION SOURCE TO ... SOURCE_AUTO_POSITION1它会自动跳过自己已经执行过的GTID集合只从缺的地方开始拉取。这套机制比我早年用文件名加position做切换时省了太多事也避免了人为算错位点。5. 实战优化清单从参数到架构的三层调优5.1 参数层优化主库组提交与从库并行度优化主从复制主库侧的思路是提升一批能提交的事务数。MySQL的组提交机制把多个准备提交的事务合并成一组一次fsync刷盘从而提升binlog写入吞吐。相关参数binlog_group_commit_sync_delay延迟多少微秒再执行组提交默认0可以设成几十到几百微秒让更多事务凑进同一组。binlog_group_commit_sync_no_delay_count攒够多少个事务就立刻提交不用等延迟时间。sync_binlog1每次组提交都刷盘binlog保证不丢。innodb_flush_log_at_trx_commit1每次事务提交都刷redo log。这四个参数组合起来是主库数据安全的基本盘也是复制不丢数据的源头保障。有些性能优化文章建议把sync_binlog改成0或者innodb_flush_log_at_trx_commit改成2来提升性能我只能说在核心业务上这么做就是在拿数据安全换吞吐出事的时候哭都来不及。从库侧的核心参数我刚才提到了这里给出一套我常用的组合replica_parallel_workers 8 replica_parallel_type LOGICAL_CLOCK binlog_transaction_dependency_tracking WRITESET replica_preserve_commit_order ON relay_log_recovery ONrelay_log_recoveryON也很关键从库异常重启后会自动丢弃未应用的relay log并重新从主库拉取避免SQL线程和IO线程位点不一致导致的复制错乱。5.2 拓扑层优化级联复制、读写分离与容灾选择复制拓扑不是越多越好也不是越复杂越好。最常见的拓扑是一主多从主库承担写入从库分摊读和备份。这个拓扑简单清晰切换逻辑也容易理解。但当从库数量多了主库的dump线程压力会上升每个从库都要单独推一份binlog流。这个场景下可以考虑用级联复制主库只接一个中转从库中转从库开log_slave_updatesON把自己的relay log应用结果继续记录下来再往下挂其他从库。要注意中转从库本身又增加了一跳延迟会被放大所以别级联太多层。更高阶的容灾方案是组复制Group Replication它提供多节点强一致故障时自动选主。但组复制的网络要求高运维复杂度也高不是所有业务都适合。我的建议是核心支付类业务用组复制或云数据库自带的高可用方案一般业务用半同步加一主一从再加一个备份性价比最高。读写分离方面应用层自己做判断容易出错建议用中间件如ProxySQL、ShardingSphere或者云上的读写分离地址。中间件只做SQL路由真正的复制链路和一致性保障还得靠数据库层的架构设计。5.3 监控与巡检复制健康度的日常功课监控复制不能只看有没有报错要形成一张日常巡检清单每天看一次SHOW REPLICA STATUS\G重点字段Replica_IO_Running、Replica_SQL_Running、Last_IO_Error、Last_SQL_Error、Retrieved_Gtid_Set、Executed_Gtid_Set。对比Retrieved_Gtid_Set和Executed_Gtid_Set差集就是还没应用的GTID范围比Seconds_Behind_Source更可靠。接prometheus类监控时用pt-heartbeat计算真实延迟阈值一般设10秒核心业务可以更严。关注从库的relay log大小IO线程拉得快、SQL线程执行慢时relay log会持续增长极端情况下会占满磁盘。巡检主库的大事务通过SHOW PROCESSLIST观察是否有持续很久的写语句提前干预。6. 两个我踩过的坑6.1 relay log损坏导致复制中断的恢复过程有一年我们一台从库磁盘满了结果relay log写入失败SQL线程报错停掉。当时没开GTID我试图去拼master position折腾了两个小时最后还是从主库重新做了一遍数据同步才恢复。这个坑的本质是没开GTID时SQL线程的位点丢失或和relay log不一致后人工很难精确定位该从哪里继续。最稳妥的恢复流程反而是直接重建复制关系STOP REPLICA; RESET REPLICA ALL; CHANGE REPLICATION SOURCE TO SOURCE_HOST..., SOURCE_USERrepl, SOURCE_PASSWORD..., SOURCE_AUTO_POSITION1, GET_SOURCE_PUBLIC_KEY1; START REPLICA;执行之前要从主库做一次备份恢复或者用xtrabackup的方式把从库补齐到某个一致位点。这套流程在GTID模式下非常干净因为从库的Executed_Gtid_Set告诉它自己已经执行到哪了剩下的自己拉。那次之后我把所有环境的复制都切到了GTID模式再也没在恢复复制的时侯熬夜。6.2 并行复制中字符集不一致导致的报错与修复还有一次某台从库被人偷偷改了默认字符集主库的binlog是按utf8mb4写入的从库在回放一个包含中文的行更新时由于列校验规则不一致报了字符集相关的错误SQL线程直接卡死。当时更麻烦的是从库已经执行了一部分事务不能简单重建。处理思路分两步先让复制继续走下去再修复数据差异。跳过当前报错事务可以用SET GLOBAL sql_replica_skip_counter 1; START REPLICA;sql_replica_skip_counter1会让SQL线程跳过一个事件注意只跳报错的那一个不要跳多否则很容易跳掉正常事务。跳过之后复制继续追赶再用pt-table-checksum和pt-table-sync对比修复主从不一致的数据。这个坑的教训是不要因为一时方便在从库上改字符集、排序规则更不要随意改表结构。从库的配置应尽量复刻主库环境差异越小复制越稳。7. 最后的一些运维习惯复盘这么多年管MySQL的经历主从复制出问题大部分时候不是因为原理多复杂而是日常操作中积攒的小差异在故障时刻集中爆发。我自己现在会坚持几件事每天手工看一眼复制状态不只是靠监控每周对比一下主库和从库的GTID集合每次发版前确认没有大事务和长DDL每半年做一次主从切换演练把CHANGE REPLICATION SOURCE TO的流程练到不用查文档。MySQL 9带来的新特性确实让人兴奋但复制这套东西决定成败的仍然是基本功。把binlog怎么来、线程怎么跑、延迟怎么量、半同步怎么守、GTID怎么用这些底层逻辑吃透不管版本怎么升级心里都有底。最后提醒一句如果你现在还有环境在纯异步复制、没开GTID听我一句劝尽快找个维护窗口把它升级掉别等事故来了再追悔。
返回列表