
我接手过一套三节点的MySQL MGR集群多主模式业务量不算大但这套集群差一点让我在线上“翻车”。现象很有规律每天下午业务高峰时段整个集群的写入TPS会从正常的两三千瞬间跌到两三百甚至更低持续几十秒后又一跳一跳地恢复。应用层没有任何报错MySQL错误日志里也没出现异常从监控上看CPU、磁盘IO都还说得过去。我先是怀疑网络抖动后来又觉得是磁盘IO问题折腾了将近一周最后才发现元凶不是我最初猜的任何一个而是MGR自带的流控功能flow control被触发了。而触发它的根源正是多主模式下某个节点的relay log事务积压。这篇文章就是我在整治这套集群过程中对MGR流控机制从原理到实战的一次完整复盘多主模式为什么容易事务积压、流控到底在底层做什么、参数怎么调才有效、积压发生时的完整排查链路以及最终我做了哪些改动才把TPS救回来。如果你也在用MGR尤其是多主模式建议认真看看这套“刹车机制”。理解不到位轻则业务TPS莫名其妙被削平重则节点被驱逐出集群高可用失效。1. 多主模式的事先看懂事务在节点间的完整旅行1.1 一条事务在多主MGR里究竟怎么流转多主模式下每个节点都能接收写请求但一条事务并不会在你写入的那个节点本地提交就算完事。MGR底层靠的是Group Communication SystemXCom协议来保证数据在所有节点上的一致性。完整链路是这样的客户端把事务提交到节点A节点A在执行后把binlog中的事务通过XCom广播给集群内所有节点。每个节点收到后都会对事务做一次冲突认证certification检查它与集群中其他已提交事务是否存在读写集合或写写集合上的冲突。认证通过后节点A源节点可以立即提交并把成功结果返回给客户端其他节点则将这个事务追加到自己的relay log里。远端节点由group_replication_applier通道的applier线程按顺序把relay log里的事务回放到本地数据中。这里面有个特别容易让人误判的点MGR并不要求所有节点把事务应用完才向客户端确认提交。只要大多数节点在认证环节通过了源节点就可以返回成功。这意味着业务的写入响应时间看起来很好但真正决定集群整体吞吐上限的是最慢那个节点上applier的回放速度。打个比方一个车队头车跑得再快整个车队的实际前进速度也取决于最后一辆车能跑多快。如果最后一辆车跟不上车队的距离会越拉越大直到有人掉队。MGR的事务积压就是“距离越拉越大”的那个过程。1.2 积压到底发生在哪个环节认证队列与应用队列事务积压不是凭空冒出来的它只发生在两个明确的位置认证队列certifier queue事务已经广播到某个节点但还没完成冲突认证在等待被处理。这个队列正常情况下非常小因为certification是在内存里做集合操作速度极快。但如果CPU跑满或者无主键表导致的认证效率下降这个队列也会堆积。应用队列applier queue认证已经通过、事务也已经写入relay log但applier线程还没来得及回放。多主模式下最容易暴涨的就是这个队列尤其在某个节点正在回放一个大事务、或者该节点硬件明显弱于其他节点时。我实际见过的一个典型案例就是典型的后者。那套集群里两台新机器是128G内存、8核CPU但第三台是几年前的旧机器64G内存、4核CPU磁盘还是机械盘。业务方图省事把将近三分之二的写请求通过负载均衡打到了那台旧机器上。旧机器既要处理本地写又要回放新机器广播过来的事务applier长期处于高负载但就是跟不上relay log大小从几百KB一路涨到几个GB。等到积压突破了MGR内部预设的阈值流控直接接管了集群所有人的写入都被压了下来。所以理解了事务在MGR里的流转路径再谈流控才有意义。流控不是无端削你的TPS它只是在执行保护机制。2. 流控是刹车而且是一套带配额的刹车系统2.1 流控解决的不是“堵”而是“被堵到驱逐”很多人一听到流控就觉得是MySQL在“拖后腿”恨不得赶紧关掉。但了解MGR的成员管理机制之后你会发现流控其实是MGR的自保手段。MGR集群对节点有一个容忍机制如果一个节点在group_replication_member_expel_timeout设置的时间内默认5秒最大可以调整没有正常响应集群的心跳或者长时间无法追平数据它会被集群“驱逐”。被驱逐意味着什么节点脱离集群需要人工干预才能重新加入。在多主模式下这还意味着该节点上的读写全部中断如果正好打到它上面就是一次小型的可用性故障。流控的作用就是让集群里所有节点在“某个节点跟不上”的时候一起降速给慢节点追平的时间。它不是治疗积压的药而是一套防止情况恶化到节点被驱逐的刹车系统。MGR流控的判断逻辑一句话就能说清楚所有节点周期性默认是每1秒地把自己的认证队列长度、应用队列长度广播到组内一旦有任何一个节点探测到某个队列超过了阈值集群内所有节点就同时进入流控状态。注意是所有节点一起被限流而不是只有慢节点被限流。这在多主场景下非常关键你不能因为一个节点慢就让写它的那个客户端承担全部代价。MGR采用的方式是“连坐”让全部写入方都减速确保慢节点有时间喘息。2.2 配额机制为什么流控一开启TPS会掉成一条直线进入流控状态后MGR不是简单地拒绝所有写请求而是给每个节点计算一个写配额quota。在下一个流控周期内每个节点最多只能写入一定字节数的事务。配额用完之后新的写事务就得等到下一个周期才能放行。所以你在监控上看到的典型流控特征就是TPS曲线不再是平稳的波浪线而是变成了一段段的平台和断崖——一个周期内配额用完随后等待下一周期然后配额重新分配再写一点。整体看就像被削成了一条锯齿状的直线。这也是我判断集群是否触发流控的第一个信号。这里有两个参数直接决定“刹车力度”group_replication_flow_control_min_quota单个周期内节点至少可以写入的字节数下限。group_replication_flow_control_max_quota单个周期内节点最多可以写入的字节数上限默认0表示不限制。生产环境里我见过最糟糕的配置就是有人把max_quota设成了0并且没有设置合理的min_quota。流控一触发配额可能瞬间跌到接近0整个集群写入几乎完全停摆几十秒然后慢慢恢复。这让业务方完全无法接受最后被逼着直接把流控关了。这种操作我明确不建议因为它把保护机制拆掉了后面风险更大。2.3 自动流控与手动流控的取舍MGR的group_replication_flow_control_mode参数控制流控模式。默认值是QUOTA也就是上面说的自动配额机制这也是绝大多数生产环境应该保持的模式。另一个可选值是DISABLED即完全关闭流控。什么情况下会考虑临时把mode改成DISABLED我只有在做短期验证时才这么做。比如已经确认某个大事务正在导致全集群积压但这个大事务不是业务常态我需要它在几分钟内快速应用完人为关掉流控给applier加速等追平后再立刻恢复QUOTA模式。但长期关闭流控就是拿集群性命开玩笑。我见过最惨的现场某团队为了追求峰值TPS把流控永久关闭结果某天一个节点的relay log积压到几十GB最终节点被驱逐。由于自动故障转移并不会马上发生业务在很长时间内其实都在访问一个“已经不在集群里”的节点数据可靠性已经出问题了而没人知道。所以我的结论很明确流控这个刹车该踩的时候必须让它踩我们要做的是把刹车调得更平滑而不是把刹车拆掉。3. 流控参数怎么理解、怎么调才有实战价值3.1 六个关键参数的定位与默认行为MGR流控相关的参数主要以group_replication_flow_control_开头我在实际运维中会重点关注以下六个参数名作用默认值备注group_replication_flow_control_mode是否开启流控以及流控模式QUOTA正常保持即可group_replication_flow_control_applier_thresholdapplier队列字节数阈值25000000约25MB触发流控的核心阈值group_replication_flow_control_certifier_threshold认证队列字节数阈值25000000约25MB极端情况下才触发group_replication_flow_control_period节点间同步队列状态、计算配额的周期1秒生产环境很少改group_replication_flow_control_min_quota每周期至少放行的写入字节数0可根据业务设置下限group_replication_flow_control_max_quota每周期最多放行的写入字节数0不限制建议根据峰值推算先说两个阈值参数。它们都是字节数描述的是“队列里积压了多少数据”的概念。默认25MB看起来不大但对于大事务多、业务高峰期写入密集的集群来说很容易就被触及。group_replication_flow_control_applier_threshold的触发频率远高于certifier_threshold因为大多数积压都发生在relay log应用阶段。再说配额参数。min_quota和max_quota默认都是00意味着“不限制”或“自动计算”。自动计算出来的配额不一定适合你的业务形态。比如我遇到过流控触发后配额被算得极小导致集群写入几乎全停的情况。这时候手动给min_quota设置一个合理下限就能保证最坏情况下仍然有部分写入流量通过。3.2 按业务场景给参数的做法和我踩过的坑调流控参数没有“万能公式”但有一个靠谱的思考框架先确定你对积压的容忍度和对延迟的敏感度再决定阈值是调大还是调小。如果业务对数据实时性要求很高比如需要立刻读到刚写入的数据那就不应该让relay log积压太多。这时候保持默认25MB甚至更小能让流控更早触发、更早干预减少慢节点追平时间。代价是流控会更频繁地介入TPS波动会更剧烈。反过来如果业务能容忍几秒甚至十几秒的短暂延迟但对TPS的稳定性极度敏感那就可以把applier_threshold调大。我记得有一套做数据汇聚的集群业务方明确说“我可以等十秒但别让我TPS掉到0”。我直接把group_replication_flow_control_applier_threshold从默认的25000000调到了100000000100MB同时把max_quota设置成根据日常峰值推算的一个周期写入字节数的1.2倍。这样即使流控触发也只是把峰值稍微削平而不是直接清零。效果立竿见影从那次以后业务方再没来找我投诉过“断崖式掉点”。再分享一个我踩过的坑有一段时间我把applier_threshold调得很大想着让积压多攒一会儿、少触发流控结果发现慢节点的延迟一直在涨最终relay log积压到几十GB磁盘空间都报警了。这时候我才意识到阈值调大只是推迟了流控的触发时机并没有提高慢节点的消化能力。如果慢节点的问题是结构性的比如硬件太差、大事务太多调参数就是治标不治本必须配合第5节讲的根治手段。提示修改流控参数是动态的不需要重启集群直接SET GLOBAL即可生效。但要注意这是全局变量所有节点最好保持一致的配置否则不同节点的流控触发阈值不同行为会变得难以预测。4. 事务积压出现后的完整排查链路4.1 第一眼判断是不是真的在流控当你看到TPS曲线出现异常的平台和锯齿时第一步不是去调参数而是确认“到底是不是流控干的”。我总结了一套从现象到结论的快速判断方法。首先看监控曲线。流控造成的TPS形态非常典型原本平稳的写入量突然变成接近水平的直线持续几个周期后恢复到一个较低的水平再往上爬。和网络故障的“完全归零”不同流控一般不会让写入彻底停掉而是压在一个较低的水平上跳动。其次查performance_schema.replication_group_member_stats视图这个是判断积压的核心。执行SELECT MEMBER_ID, COUNT_TRANSACTIONS_IN_QUEUE AS certifier_queue, COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE AS applier_queue, COUNT_TRANSACTIONS_CHECKED, COUNT_CONFLICTS_DETECTED FROM performance_schema.replication_group_member_stats\G重点关注每个节点的applier_queue字段。如果在高峰期它持续大于0并且接近或者超过了group_replication_flow_control_applier_threshold设定的值那基本就可以断定流控正在被触发。正常情况下applier_queue应该是0或者一个很小的数字毕竟applier的消费速度足够快。4.2 定位轧点是认证慢还是应用慢确认是流控之后下一步要回答一个关键问题积压的瓶颈到底在哪个环节我把排查思路整理成了一张对照表现象瓶颈环节进一步检查certifier_queue持续增长认证环节慢检查是否存在大量无主键表、冲突检测比例是否过高applier_queue持续增长应用环节慢检查applier线程状态、当前回放SQL、磁盘IO两者都在增长节点整体资源不足看CPU负载、内存、IO/util如果是applier_queue的问题继续往下钻取。查询performance_schema.replication_applier_status_by_worker可以看到每个applier worker线程的实时状态SELECT WORKER_ID, THREAD_ID, SERVICE_STATE, LAST_APPLIED_TRANSACTION, APPLYING_TRANSACTION FROM performance_schema.replication_applier_status_by_worker WHERE CHANNEL_NAME group_replication_applier\G如果某个worker的APPLYING_TRANSACTION长时间不变多半就是正在回放一条特别大的事务。这时候去源节点的binlog里找到这个事务用mysqlbinlog --base64-outputdecode-rows -vv解析一下看看它涉及多少行数据就能非常直观地判断大事务对集群的冲击。4.3 不同积压现场的应对清单排查之后不同积压程度和成因的处理方式完全不同。我按实战中遇到的概率整理了以下应对清单轻度积压relay log堆积小于100MB且只是偶发直接微调group_replication_flow_control_applier_threshold比如从25MB调到50MB观察一两个高峰周期。如果不再触发就算解决。中度积压上百MB到GB级且高峰必现这通常不是阈值问题而是某个节点消化能力不足。检查是不是大事务集中出现或者applier单线程跟不上优先考虑开启并行回放。重度积压十GB以上或已经出现节点被驱逐的迹象不要纠结参数了先保集群稳定。如果有条件临时把部分非核心业务的写入切到别的节点给慢节点留出追赶窗口如果积压实在追不上考虑在低峰期重启该节点的MGR通道让它走增量恢复重新追赶。反复积压解决了又出现这属于结构性问题。要么是慢节点硬件长期不达标要么是大事务频繁出现要么是流量分配不均。这一类问题不是调参能解决的必须回到架构和业务侧去做调整。提示遇到积压时最忌讳的就是反复重启节点。MGR节点重启后要做增量恢复这个过程本身也会消耗组内其他节点的性能反而可能让积压更严重。除非明确判断节点状态异常否则优先通过降速、分流来缓解。5. 比调参更重要的从根上让集群“不快不慢”5.1 多线程并行回放是性价比最高的开关流控参数只是刹车灵敏度真正决定cluster能跑多快的是applier的回放能力。MGR的applier默认是单线程回放——这在5.7时代还能凑合放到8.0多主模式下就是妥妥的瓶颈。MySQL 8.0提供了group_replication_applier_parallelism这个参数可以控制group replication applier通道的并行回放线程数。默认值是1也就是单线程。把它调大到4或者8慢节点的回放吞吐会有非常明显的提升。我一般会先在测试环境摸一下底。以那套“2台新机器1台老机器”的集群为例把group_replication_applier_parallelism从1调整为4之后老机器上relay log的积压增长速度明显放缓流控触发的频率从一分钟好几次降到高峰也就一两次。配合前面说的阈值调整业务侧TPS的锯齿基本消失。需要注意两个坑第一这个参数对已经存在的channel不会立即生效。改完SET GLOBAL之后并不会像流控参数那样动态更新。稳妥的做法是在低峰期执行STOP GROUP_REPLICATION; START GROUP_REPLICATION;重启通道。重启期间这个节点会短暂不可用需要确保业务有连接容错。第二并行度不是越大越好。MGR的relay log是经过认证排序的applier并行回放时仍要处理事务之间的锁依赖并行线程之间可能互相等待。我实验过把并行度调到16结果因为冲突率和锁等待问题回放性能反而下降。对于绝大多数业务4到8是合理的区间具体值要看你的事务冲突比例。5.2 大事务才是流控被触发的头号元凶我处理过的MGR积压案例里十个有九个背后都藏着大事务的影子。所谓大事务不一定是事务代码写得有问题而是单条SQL影响的数据量太大。比如一条UPDATE更新了500万行源节点执行可能只要几十秒但这条SQL传到远端节点后applier要一条条应用到本地数据可能需要几分钟。几分钟内其他事务全被堵在后面relay log自然越积越多。解决大事务的核心思路只有一个拆。我在生产环境用过的拆分策略是按主键范围分批提交。比如原本是一条更新500万行的SQL改成循环1000行一批去更新每批单独提交-- 伪代码示意按主键范围分批更新 WHILE (1) DO UPDATE big_table t JOIN ( SELECT pk FROM big_table WHERE update_time NOW() - INTERVAL 1 DAY AND pk last_pk ORDER BY pk LIMIT 1000 ) tmp ON t.pk tmp.pk SET t.status closed; IF ROW_COUNT() 0 THEN LEAVE; END IF; COMMIT; END WHILE;拆分之后每个小事务在远端应用的时间都在秒级以内applier队列不会出现长时间不消费的情况流控的触发概率会大幅下降。总执行时间可能比一条大SQL要长一些但对整个集群的稳定性是质的改善。另外还必须检查一下所有参与MGR复制的表是否有主键。MGR在做冲突认证时依赖主键来快速判定事务的读写集合。如果一个表没有主键认证阶段就要走全表扫描级别的判断效率极低同时会大幅拉高认证队列的长度。MySQL官方对无主键表在MGR里的态度一直是“不支持的高风险操作”但在实际运维里总有历史表漏掉了。我的建议是巡检一遍所有没有主键的表都补上主键哪怕是用一个自增ID也远比没有强。5.3 多主模式的流量分配决定积压的方向多主模式给了你“每个节点都能写”的能力但如果你把所有写流量都压到同一个节点上那这个节点既是源节点又是回放节点双重压力下必然成为集群中最慢的那个节点积压也就不可避免地指向它。我之前提到的那套集群就是典型的流量分配失衡。业务方用负载均衡把写请求打过去但负载均衡是轮询加会话保持大部分写请求都落在了节点1上。结果节点1的写入TPS很高同时还要回放节点2、节点3的写事务压力山大节点2和节点3资源空闲却接不到足够流量。这种局面下流控再怎么调都是拆东墙补西墙。正确的做法是在应用层或者代理层做更精细的流量拆分。比如按照业务模块拆分订单写节点1库存写节点2用户写节点3。或者如果业务无法按模块拆至少要让写流量在三个节点上尽量均匀分布尤其不要形成“一个主节点两个从节点”的错觉。多主的意义就在于并行写如果压在一个节点上不仅享受不到多主的吞吐优势还给自己制造了不必要的积压风险。另外还有一点容易被忽略多主模式对硬件对称性的要求很高。不管你怎么分配流量每台节点都既可能当源节点也可能当远端回放节点。硬件弱的那台节点在高峰期必然成为整个集群吞吐的短板。如果条件允许尽量让MGR集群内的节点硬件规格保持一致。我见过太多用“旧的机器先凑合跑着”的集群最后都因为这台旧机器拖垮了整个集群的高可用体验。最后讲一个我自己的运维习惯。MGR集群上线初期我会把performance_schema.replication_group_member_stats里的COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE写进监控告警阈值就参考group_replication_flow_control_applier_threshold设成百分之八十。这样在流控真正把TPS削成直线之前我就能提前收到积压预警。流控这个“刹车”是MGR保护集群不被自己拖垮的最后一道防线你要学会控制它而不是拆掉它。上面这几板斧下来我这套集群的TPS曲线终于恢复了正常每天下午的“定时抽搐”也彻底消失了。