ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MySQL高可用切换全解:从传统主从复制到组复制机制演进

MySQL高可用切换全解:从传统主从复制到组复制机制演进 如果你现在负责的 MySQL 主从集群还在靠 MHA 脚本做故障切换而 MySQL Group ReplicationMGR只是偶尔在文档里见过那我建议你先别急着给生产环境上这套组件花 20 分钟把它的故障检测、选主、故障转移这三件事彻底搞清楚。MGR 看起来只是把复制从一主多从改成了多节点互通但它背后的工作机制已经不是普通复制那套玩法理解不到位等真出故障时你连排查方向都会找错。这篇文章不打算复述官方文档而是结合我在几套生产集群上的实际排障经历和被坑过的记录把 MGR 从发现节点离线到新主真正能写这件事的完整链路讲透。适合正在评估高可用方案的架构师、维护 MGR 集群的 DBA以及刚接触组复制想弄懂原理的开发同学。1. 先搞清楚MGR 相对传统主从到底改了什么1.1 传统主从复制为什么让你半夜爬起来传统一主多从搭配 MHA 的方案里故障切换依赖的是外部监测器。MHA 不断探测主库是否可用一旦连接超时就尝试通过 SSH 连到备库检查然后尝试在备用节点上补拉二进制日志最后再通过 VIP 或 DNS 切换让应用指向新主。这套流程能跑但问题很明显探测的是主库还有没有响应而不是组内所有节点是否认可这个结论。如果主库只是网络抖动MHA 可能误判把活的主库踢掉如果主库进程瞬间崩溃判断的准确性还行但因为要经历判定-检查日志-补拉-提权-切换 VIP多个环节通常忙完已经是几十秒甚至一两分钟之后。更麻烦的是半同步复制在极端情况下依然可能丢数据。主库刚把 binlog 发给一个从库还没落库就宕机MHA 接管时只能选最近的从库但它不一定包含最后那条事务。我见过不止一次切换完成后业务发现最近几秒的订单数据查不到最后只能靠人工从 binlog 里手工补。这种切换后还要修数据的体验经历过一次就够让人刻骨铭心了。1.2 MGR 的三个核心承诺有序、一致、自动MGR 的设计思路换了一种让每个成员实时参与一个组协商过程节点状态、主节点归属、事务提交顺序都由多数派成员通过一致的协议来决策。传统方案是外部裁判判定谁活着MGR 是组内成员共同认可谁活着。用稍微抽象的话总结MGR 承诺了三件事消息全局有序任何一条组内广播的消息在所有成员那里的到达顺序一致。这是靠基于 Paxos 思想的一致性协议保证的。成员视图一致任何时候所有存活成员对当前组由哪些节点组成、谁是主持有完全相同的看法。故障转移自动完成成员被驱逐后剩余节点自动推进视图、选出新主不需要外部脚本介入。这三点就是 MGR 高可用的理论基础。为什么视图一致这么重要因为在分布式系统里最可怕的事情不是某个节点挂了而是不同节点对谁挂了持有不同意见。MHA 时代协调这个意见靠脚本逻辑和人MGR 把这件事下沉到协议层靠多数派投票完成。我习惯用一个类比来理解它把 MGR 想象成一个小团队的线上会议每个成员定期在群里报平安。如果有人一直不说话大家不会立刻把他踢出去而是先等几秒确认没有动静后再投票把他移出会议群。投票必须超过半数才有效新的主持人在投票结果出来之后才产生。这个模型和 MHA 的外部巡检员模式完全不同。1.3 MGR 的代价从运维切换变成分布式一致性MGR 不是没有代价的。事务需要经过组内协商提交延迟至少增加一轮网络通信跨机房部署时这个延迟会非常明显组内所有成员都要保留 binlog 并应用事务慢节点会拖累整体吞吐配置、排障复杂度远高于普通复制排错时你要同时具备网络、数据库、分布式协议三方面知识。我把选型前的判断列成了一张简易表给正在纠结要不要上 MGR 的团队参考判断维度适合 MGR 的场景不适合 MGR 的场景一致性要求需要自动化切换、最小化人工介入可以接受较长时间手动切换团队对脚本维护很熟网络条件节点间带宽稳定、延迟低同机房/同城跨洋链路、频繁抖动、丢包率较高写入模型单主写为主多主写能严格控制冲突极高并发写入且无法拆分流量MGR 会放大提交延迟节点规模3~7 个节点组成的高可用域超过 9 个节点的大集群组通信成本太高这张表的核心意思是MGR 用分布式一致性的思路解决了传统复制方案里最头疼的自动切换和防脑裂问题代价是它更挑剔环境尤其是网络。接下来从故障检测开始拆。2. 故障检测一场多数派投票而不是简单的心跳超时2.1 组通信引擎里的心跳与故障检测器MGR 的通信核心是一个叫 XCom 的组件官方文档里通常称作组通信系统Group Communication System简称 GCS。它负责在成员之间传送消息、维持连接、管理视图。每个成员内部还有一个故障检测器Failure Detector但它不单独发心跳包而是复用消息通道每个成员在收到其他成员任何一条消息时都会记录这个成员最后活跃时间。如果一段时间内没收到某个成员的任何消息故障检测器会把该成员的状态标记为可疑suspicious。在performance_schema.replication_group_members表里你能看到这种状态转换ONLINE、RECOVERING、OFFLINE/ERROR、UNREACHABLE。其中ONLINE是正常服务状态RECOVERING是正在追赶数据准备重新加入而UNREACHABLE就是进入怀疑阶段但没有最终确认。这里有个容易误解的点UNREACHABLE不等于已经故障。它只代表在本地故障检测器的视角里这个节点暂时联系不上。后续到底怎么处理要看接下来的协商流程不是发现超时就立刻踢人。2.2 从可疑到驱逐的完整链路当一个成员被标记为可疑后MGR 内部会走一条完整链路我按顺序拆给你看成员 A 连续一段时间没有收到成员 B 的消息将 B 标记为UNREACHABLE。A 启动一个等待计时器等待时间由group_replication_member_expel_timeout控制。如果等待时间内 B 的通信恢复A 会把 B 标记回ONLINE整个过程无事发生。如果等待时间耗尽A 会发起驱逐 B的提议这个提议被当作一条组内消息广播出去。组内其他成员对这条提议进行表决。根据多数派原则超过半数的成员同意后驱逐生效。驱逐生效后组内广播一个新的视图所有存活成员安装这个新视图B 被排除在外。被驱逐的 B 在本地也会检测到自己无法与组通信或者收到视图变化通知随即进入恢复流程。第 5 步是整个链路里最关键的一环并不是一个成员认为 B 挂了B 就被踢掉而是需要在存活的成员中形成多数派共识。这保证了网络分区场景下不会出现两边同时在踢人的局面。为什么需要等待时间在分布式环境里网络瞬时拥塞、长时间 GC 停顿、负载高峰都可能导致心跳延迟。如果听说你挂了就立刻踢你误杀会成为常态被误杀的成员恢复回来又要重新追数据代价比多等几秒大得多。2.3 一次网络抖动误杀案例默认参数下的真实代价我在生产环境里真实遇到过这样一次事故某机房出口交换机做策略变更三个节点之间的网络出现约 2 秒的间歇性丢包节点 C 大概有四五秒没有和 A、B 互通。当时group_replication_member_expel_timeout用的是默认值 0A 和 B 在发现 C 可疑后立刻发起驱逐C 被踢出组。虽然网络随后恢复正常但 C 进入了RECOVERING流程通过其他成员的 binlog 追平数据后才重新加回组。整个过程持续了十几分钟。这十几分钟里原本可以在 C 上分担的读查询全部落到 A、B 上高峰期主库负载直接翻倍业务延迟明显上升。事后复盘发现只要把group_replication_member_expel_timeout设置成 5 到 10 秒这次误驱逐完全可以避免业务无感。这里要强调一个观念设置一个合理的宽限期并不是降低故障检测速度而是承认大多数真实故障都不是节点死了而是节点暂时联系不上。节点真的进程崩溃时即使等待 5 秒再驱逐也还是在几秒内完成切换业务几乎无感但如果因为 1 秒抖动就把节点踢出组恢复成本反而高得多。这是很多刚开始维护 MGR 的团队最容易踩的坑。3. 单主模式的选主规则确定性优先能力靠后3.1 默认选主算法最小 UUID 胜出单主模式Single-Primary下MGR 的选主规则比很多人想的简单得多在能够参与选举的存活成员中server_uuid字典序最小的那个会成为新的主节点。server_uuid是每个 MySQL 实例在初始化数据目录时自动生成的全局唯一标识它与节点配置、负载、加入顺序都没有关系。完整逻辑大致是这样的故障成员被驱逐后新视图产生。所有存活成员拿到同一个候选列表候选范围是视图中所有ONLINE状态的节点不包括还在RECOVERING的节点。每个成员独立对候选列表排序取字典序最小的 UUID。因为候选列表一致、排序规则一致所有成员算出来的新主必然是同一个不需要额外协商。新主在确保本地已经应用所有已提交事务后正式对外提供写服务。有意思的是MGR 里并不存在一个单独的选主投票环节。选主是通过确定性规则从视图里直接推导出来的。这也是它和很多人在理解上最大的差异点。3.2 为什么 MGR 选主不像 MHA 那样挑重点我经常被问到一个问题能不能让某台配置高、业务资历老的节点优先当主答案是目前原生 MGR 在单主模式下没有权重机制不会看节点配置、不会看数据新旧、也不会看谁先响应。潜在的影响手段很有限控制节点加入顺序。初始主通常就是第一个引导组的节点但单个节点故障后这个方法就失效了。修改server_uuid来影响排序结果。不建议这么做UUID 是节点身份标识手工修改很容易引发复制链路混乱。在应用层或 Router 层做读写分离让业务实际只访问你希望访问的节点。但无论谁当选主MySQL Router 只会把写流量路由到当前主节点这改变不了谁是主的事实。如果你真的对哪台机器成为主有强烈偏好MGR 原生机制其实不擅长这个。你更应该在架构层面考虑要么所有节点配置足够对称要么接受选主结果由 UUID 排序决定这个现实。这不是缺陷而是设计取舍——确定性优先能力靠后。3.3 并发发现故障时如何保证结果唯一这里值得多写几句。假设 A 是主B、C、D 是从A 崩溃瞬间B 在 t1 发现 A 可疑C 在 t2 发现 A 可疑只相差毫秒。B 先发起了驱逐提议C 随后也发起。由于组通信保证消息全局有序这两个提议会被排到同一个顺序上最终生效的视图只有一个。无论 B 的提议先到达还是 C 的先到达所有成员对已经剔除 A这个事实达成一致然后在同一份视图上用同一个规则算主。所以不会出现 B 认为自己是主、C 认为 D 是主的双主情况。这正是基于 Paxos 的共识协议的价值它保证的不是选举过程公平而是选举结果在并发场景下仍然唯一。那如果 A 没死只是被网络切断了呢比如 A 连不上 B/C/D但 B/C/D 之间通信正常。B/C/D 发现 A 可疑把 A 驱逐选出新主继续服务A 自己发现自己联系不上其他成员无法达到多数派就会按配置进入只读或退出。这个场景我放到第五章再详细讲。4. 故障转移全过程拆解从驱逐到新主真正可写4.1 一条完整时间线看故障切换的每个环节从故障发生到新主真正能够接受写入中间有若干阶段我列了一张时间线表阶段主要动作典型耗时故障发生主节点进程崩溃或网络断开0故障检测其他成员超时未收到消息标记UNREACHABLE毫秒级到秒级宽限期等待expel_timeout计时若通信恢复则取消驱逐0~N 秒按配置驱逐与共识发出驱逐提议多数派确认新视图毫秒级到几十毫秒选举新主视图安装后按最小 UUID 推导新主与视图确认几乎同步新主恢复写入新主应用全局已提交位置之前的事务转为可写毫秒级到秒级注意最后一行里全局已提交位置这个说法官方概念叫低水位标记low water markLWM。很多人会忽略这一环。MGR 里事务提交分两个层面一个成员本地提交不代表所有节点都已经应用了该事务。共识协议会把每个事务放在一个全局有序的日志序列里每条日志对应一个安全位置。新主被推出后不能立刻打开写入开关。它要先看自己本地的日志已经推进到全局序列的哪个位置如果还差几条事务需要先补上确保组内所有已提交事务在自己的 binlog 里完整存在再对外提供写服务。简单说新主先追上全局进度再开门营业。这就是 MGR 在故障转移时通常不会丢已提交事务的根本原因比异步复制和传统 MHA 强很多。4.2 视图变化期间客户端和应用层看到什么故障发生时客户端此刻正在执行的写事务会收到什么最常见的表现是连接断开、查询超时、或者报Lost connection to MySQL server during query事务回滚需要应用层重试。切换到新主后如果应用还是用同一个 VIP 或 DNS 连接可能还会有十几秒的连接拒绝或认证失败随后逐步恢复。要想让客户端快速感知新主常见有两个办法一是使用 MySQL Router。Router 会订阅 MGR 的视图变化主节点变更后自动更新路由目标把读写流量切换到新主。它对应用层是透明的应用只连 Router 的地址不需要感知后端节点变化。最小实践是把 Router 部署在两台机器上做冗余避免 Router 自身成为单点。二是在应用层做失败重试。捕获连接异常后不立即放弃而是重新查询performance_schema.replication_group_members找到状态为ONLINE且角色为PRIMARY的节点建立新连接重试。一个健壮的重试策略通常是第一次重试延迟 1 秒第二次 3 秒之后递增最多尝试 5 次防止应用端重试风暴压垮刚刚恢复的集群。4.3 被驱逐成员的重新加入RECOVERING、Clone 与状态补偿被驱逐的节点如果只是网络问题数据没有被清掉恢复网络后它会启动重新加入流程。这个流程里节点会先尝试与其他成员建立通信、读取组视图然后进入RECOVERING状态而不是直接变成ONLINE。恢复数据的方式取决于被驱逐期间的差距大小如果差距不大节点从其他成员的 binlog 继续同步追平追平后自动变成ONLINE。如果差距过大或者本地 binlog 已经被清理只有两个选择从备份恢复数据后重新加入或者使用 Clone Plugin 从现有成员克隆一份数据。8.0 里 clone 是最常规的手段克隆过程中节点数据会被完整覆盖完成后重新以新身份加入组再追平 clone 结束点之后的事务。这里提醒一下RECOVERING状态长时间挂着不结束多半是下面三种情况——对端成员本身也在RECOVERING无法提供 binlog 流本地relay log或 binlog 与组内其他成员差异过大导致追平困难网络虽然恢复但组成员之间的连接没有完全建立。排查顺序建议先看错误日志再看复制状态最后考虑 clone 或重建。5. 生产环境实测中的坑与调优建议5.1 expel_timeout 到底该设多少参数调整的取舍group_replication_member_expel_timeout这个参数直接影响故障检测的敏感度。默认值是 0也就是一旦怀疑就立即发起驱逐生产环境我基本不会这么用。根据网络质量不同我通常这样设置部署场景建议值说明同机房、网络质量好、希望最快切换3~5 秒误杀概率低切换速度有保障同城跨机房、偶尔有抖动10~15 秒给瞬态网络问题留出恢复时间跨地域、链路不太稳定15~30 秒更保守但故障切换也会变慢同时建议把group_replication_exit_state_action调成符合业务预期的方式。这个参数决定节点被驱逐后的动作ABORT_SERVER会让被驱逐的节点直接关闭 mysqld 进程优先保证不产生孤立读READ_ONLY则让节点继续以只读方式存活。我个人的选择是ABORT_SERVER因为被驱逐的节点继续开着一旦被应用误连读到的是落后数据风险比直接宕机更大。但如果你有意让孤立节点继续承担只读流量并且业务能容忍陈旧数据也可以选READ_ONLY这是个需要想清楚再做取舍的点。5.2 多数派与脑裂网络分区的真实表现MGR 的多数派机制天然防脑裂这是它比传统双主强的地方。但很多人因此产生了一个错误预期以为只要还有节点活着集群就能写。实际上不是这么回事。以 3 节点 MGR 为例正常状态A、B、C 三者互相通信。最多容忍 1 个节点故障剩下 2 个节点构成多数派可以继续选主和写。A 被网络隔离B 和 C 互通正常B、C 把 A 驱逐集群继续服务。节点坏掉两个只剩一个节点这个节点无法构成多数派写服务会中断直到有节点恢复或人工干预。换句话说3 节点 MGR 的可用性是「最多允许 1 个节点故障」而不是3 个节点坏 2 个还剩 1 个照样写。这跟高可用三个字给人的直觉很不一样。更需要注意的部署陷阱是 2 节点 MGR。2 个节点中任何一个故障剩余 1 个都不足半数集群直接不可写。除非你的业务对偶尔不可写完全无所谓否则不要把 2 节点 MGR 当高可用方案。生产环境最基础的选择应该是 3 节点条件允许就用 5 节点。5.3 故障切换后的检查清单每次故障转移完成之后我都会按固定清单确认一遍防止切换流程有遗漏。这套清单不只手动故障演练时用真实故障恢复后同样适用检查项具体动作期望结果节点状态SELECT * FROM performance_schema.replication_group_members;所有节点状态为ONLINE角色为PRIMARY或SECONDARY主节点归属查看MEMBER_ROLE列单主模式下有且只有一个PRIMARY数据追平查询performance_schema.replication_group_member_stats对比事务相关计数各成员已应用事务数一致或差距极小应用层路由检查 MySQL Router 或 VIP 指向已指向当前PRIMARY写入验证在PRIMARY上执行一条写事务成功被驱逐节点确认exit_state_action是否触发按预期进入恢复或被拉起告警确认检查监控系统没有持续的报错或告警这里面最容易忽视的是数据追平这一项。MGR 的多数派保证了切到新主后不丢已提交事务但它在某些极端场景下依然需要你亲自确认各节点的事务进度。尤其是节点经历RECOVERING重新加入后如果它追平到ONLINE时间点比较晚组内其他成员可能已经产生了大量新事务此时它对外的读流量可能会读到相对旧的数据。业务如果对读一致性敏感这种情况要提前设计好路由策略。说完这些我最后说一点个人体会。MGR 这套机制的协议设计确实干净但越干净的东西越容不得脏环境。它把故障检测、选主、故障转移的复杂度吞进协议层同时把对网络质量、对部署规划、对参数精细度的要求全吐到了运维侧。如果你正准备上 MGR第一件事不是搭环境而是先把你机房网络到底稳不稳定搞清楚。
返回列表