资讯动态

MySQL组复制+Router:云原生环境下数据库高可用架构实践

发布时间:2026/10/17 9:34:00 来源:尧图企业网站定制
好的。1. 为什么在云原生架构里我最终选定了组复制加路由这一套这两年不少团队上云原生数据库却还是老一套玩法——一个主库带几个从库配合脚本做自动切换。这种架构在物理机和虚拟机时代能凑合一旦丢进 Kubernetes 这种环境问题就变得格外刺眼Pod 重建意味着 IP 漂移StatefulSet 重新调度意味着节点身份变化原本写死在脚本里的主库地址往往第一时间失效。我见过不止一个项目在容器环境里做过主从切换演练结果不是从库追不上主库就是主库地址已经不再是当初那个节点脚本直接失联。后来我把目光放到了 MySQL 组复制加上 MySQL 路由这套组合上。组复制MySQL Group Replication相比传统异步复制最大的差异在于它引入了组成员管理和分布式共识每个节点都能知道当前集群里有谁、谁是主、谁是只读副本而且主节点的变更由组内投票机制决定不需要外部脚本干预。MySQL Router 则充当连接层的路由器和负载均衡器应用只知道自己连的是一个逻辑地址至于背后哪个节点在提供服务由 Router 动态发现。我得先给出一个明确结论这套组合并不适合所有场景但对大多数云原生化改造的 MySQL 业务来说它是最接近把数据库也云原生化的路径。它能解决三件最核心的事主库切换不再依赖人肉改配置只读流量可以自动分发到你想要的副本上成员状态变化能够被连接层及时感知。你不再需要为每个客户端的连接串去维护一份当前主库 IP的清单。这篇文章不是官方文档的翻译而是一个在实际项目里踩过坑、推过重建、被凌晨三点报警电话叫醒过的人的经验记录。我会按照部署顺序来讲先在容器环境里把组复制跑起来再理解 Router 的路由逻辑然后是持久化、自愈、排障和调优。每个环节我都会说明为什么这样做而不是简单甩一个启动命令。2. 组复制部署从物理机思维切换到容器思维的几个关键差异2.1 身份绑定server_id 和 server_uuid 在 Pod 重建后还能保住吗这是我在容器环境里踩过的第一个坑而且是最隐蔽的一个。组复制要求集群里每台实例有一个全局唯一的 server_id同时各节点通过 server_uuid 互相识别。物理机上这从来不是问题机器不换配置不丢一切都是稳定的。但容器环境里Pod 一被销毁重建数据目录如果是临时盘那么整个实例的 server_uuid 就会被重新生成。等新 Pod 起来之后它想重新加入原集群原集群的成员表里还挂着那个 uuid 的旧条目两边对不上节点直接进入 RECOVERING 状态然后超时回滚为 ERROR。这个问题我在某项目的模拟环境里复现过多次现象就是节点怎么也加不进集群。网上一搜全是重启大法但根本原因就是身份信息没有保持住。解决办法并不复杂别用临时存储用持久卷挂载数据目录同时确保 MySQL 的 auto.cnf 文件存放在持久卷里。auto.cnf 里记录的就是这台实例的 server_uuid只要数据目录不丢身份就能保住。还要特别留意StatefulSet 中每个 Pod 的主机名必须是稳定的因为组复制的节点名可以用 hostname 或自定义变量标识一旦变化成员列表的辨识度会下降排查问题时很容易混淆。另一层身份是 server_id。建议在 StatefulSet 的 yaml 里用 index 计算比如 server-id 100 序号。这样每个 Pod 的 server_id 固定重建后不会变后续加节点也清晰。2.2 引导节点的纪律bootstrap_group 只能开一次组复制有两种启动方式第一种是全新集群的引导启动需要在一台实例上打开 group_replication_bootstrap_group第二种是后续节点以常规模式加入。很多新手犯的错误是引导节点的 MySQL 一重启就把 bootstrap 参数带着一起启动结果它认为自己要开启一个新集群原来的集群元数据反而被抛到脑后。容器环境里这个错误带来的连锁反应更明显一旦引导节点用相同的 group name 拉起了新集群其他存活的旧节点会陷入混乱甚至可能短期出现双主写入。云原生架构讲求自动化重启如果你在启动命令里简单粗暴地加了一行 bootstrap 配置每次节点一重启就踩雷。我的做法是在 entrypoint 脚本里加一个判断逻辑只有当你检测到数据目录里还没有集群元数据时才允许本节点作为引导节点启动一旦数据目录里已经存在组复制的元数据就强制以常规模式加入。这个判断不能靠环境变量一次定型因为 Pod 重建的次数和环境变量的初始值不一定一致最靠谱的还是数据目录里有没有 gr_state 相关表或文件这个事实条件。2.3 新节点加入集群分布式恢复不是等一等就好组复制的新节点上有一个分布式恢复阶段它需要从一个在线节点拉取全量数据快照再通过 binlog 追平后续变更。这段逻辑在物理机环境通常只有几分钟但容器环境里却很容易卡住。卡住的高频原因有三个第一新节点访问 seed 节点用的是 Kubernetes Service 的 ClusterIP但这个 IP 在新节点自己的网络命名空间里不一定通第二恢复用的用户如 rpl_user在复制链路上传输的数据包被网络策略或者 TLS 配置挡住第三新节点的 join 超时设置太短全量备份数据较大时还没传完就被判定为失败。云原生环境的网络模型比较特殊我一般建议组复制的内部通信不要走 ClusterIP而是用每个 Pod 的独立 DNS 地址StatefulSet 默认支持再配合 headless service 做到集群内可达。同时要把 group_replication_recovery_get_public_key 和 TLS 参数明确配好不要依赖默认值。这里的核心思维是节点之间必须能通过稳定的、不经过 NAT 的地址互相访问否则你后面所有故障恢复都会变得玄学。2.4 单主还是多主云原生环境我为什么偏好单主组复制支持单主模式和多主模式。从官方定义看多主模式可以在多个节点上同时执行写事务听起来非常吸引人但代价是每个写入都要在组内做冲突检测事务冲突率一高整体吞吐量立刻跳水。而且多主模式下如果业务没有做好分片同一行的并发修改极易触发认证失败应用层会看到大量死锁和重试。我见过一个团队盲目选了多主结果压测一上冲突率接近 30%整个集群性能比单节点还差。后来切回单主业务侧把只读流量交给副本写流量集中在主节点压力分布反而合理得多。云原生环境下我更推荐单主模式。理由不只是性能还有排障和心理负担单主模式下谁拥有当前主节点的判断非常明确Router 也更容易分流业务侧的理解成本最低。多主模式不是不能用而是要求团队对业务的事务访问模式有极深的理解这通常不是大多数项目在前中期能承受的复杂度。3. MySQL Router 的路由策略这一步决定你的读写流量分配是否合理3.1 先搞清楚 Router 的定位它不是一个普通代理很多人第一次接触 MySQL Router总以为它就是一个端口转发器。其实它的核心能力是感知组复制集群的成员状态并维护一份元数据缓存告诉每个连接现在谁是主、谁是只读副本。Router 会周期性向组复制节点查询 membership 信息并且在主节点变化时更新自己的路由表。订阅这个机制非常重要应用不会直接连 MySQL 的某个集群节点而是连 Router 的端口。这样一来当主节点发生切换时应用不需要修改连接串只需要重试新连接Router 就会把新连接分发到新的主节点上。在云原生架构里Router 通常部署为独立 Service或者以 Sidecar 模式挂在应用 Pod 旁边。我的经验是如果应用数量不多用独立 Deployment 加几个副本跑 Router 就够如果应用数量很多且每个应用的连接池行为和超时设置差异很大才考虑 Sidecar 模式。Sidecar 更灵活但每个 Pod 多一个进程占用和运维成本也随之上升。3.2 路由模式选择first-available 和 round-robin 的差异Router 支持静态路由和动态路由。组复制场景下我们通常使用动态路由其中两个策略最常用first-available从候选节点列表里按顺序选取第一个可用的目标一旦一个目标变成主节点后续写流量都会集中在它这里。这个策略的好处是行为可预期适合大多数业务。round-robin在多个满足条件的节点间轮流分发适合批量读流量可以把压力均匀分散。实际项目里读写分离通常这样设计写端口走主节点读端口按 round-robin 分发到所有节点或者只分到只读副本。要注意如果你希望读流量只去只读副本在 Router 的配置里要明确设置 destinations 的 metadata 标签否则 Router 默认可能把读流量分到主节点上造成主节点白白承担读压力。3.3 元数据缓存刷新切换主库后为什么应用还在连老地址这是 Router 使用中最常见的坑主节点切换后新的写连接没有正确指向新主。原因通常在两个层面。第一Router 的元数据缓存刷新周期太长。默认的 ttl 在某些版本里可能较长或者 refresh 机制没有正确触发导致切换发生后的一段时间内Router 依然认为旧主可用。第二应用连接池里的老连接没有被及时销毁应用侧觉得连接还活着就继续在旧主上执行写操作。旧主在组复制里已经变成只读于是写操作直接报错。解决思路也比较明确把 Router 的元数据刷新间隔调小比如几秒同时设置 max_transactions_history 等参数确保应用侧对写失败的感知足够快。应用连接池那边要设置合理的连接存活检测比如每次从连接池取连接前执行一个轻量的 ping。3.4 Router 自身的可用性不要让它成为单点Router 本身没有管理节点它的元数据来源于组复制的节点。这意味着 Router 是无状态的可以放心多副本部署。我在某项目里就是用一个 Deployment 跑了三个 Router 副本前端挂一个 Kubernetes Service 做负载均衡。客户端连接 Service 的虚拟 IP由 Service 把流量分发到 Router 副本。这样即使一个 Router Pod 被杀掉另外两个副本依然能提供连接。这里唯一的注意点是多个 Router 的配置要保持一致最好用 ConfigMap 挂载避免某一次发布只改了部分副本。4. 持久化、探针与故障自愈让整套方案真正云原生化4.1 为什么必须用 StatefulSet而不是 Deployment组复制的节点需要有稳定的网络标识和有状态的数据目录这决定了它不能使用 Deployment。Deployment 管理的 Pod 名称是随机的重建后主机名会变化这对组复制的节点识别和 Router 的元数据感知都很不友好。StatefulSet 则保证了 Pod 的顺序和稳定名称每个 Pod 永远叫 mysql-0、mysql-1、mysql-2。这带来的直接好处是组复制节点的地址可以在配置里固定下来不管 Pod 被重新调度多少次它的对外 DNS 都是不变的。还有个细节StatefulSet 的滚动更新策略默认是逐个更新的。对于组复制来说这个特性恰好合适——你可以在更新一个节点前先将其设置为只读状态更新完成后等它重新加入集群再更新下一个节点。如果使用 Deployment系统可能同时更新多个副本一转眼集群多数节点都离线了分布式共识直接失效。4.2 持久化卷的选择本地 SSD 和网络存储的取舍组复制对写入延迟高度敏感因为一个事务在主节点写入后至少要经过组内通信和认证才能提交。持久化卷本身的行为会直接影响这条链路的稳定性。云原生环境里常见的存储有两种网络块存储和本地 SSD。网络存储的优点是支持跨节点迁移缺点是延迟不稳定尤其在流量高峰期网络 I/O 抖动会被组复制放大。本地 SSD 的性能好但 Pod 一旦被调度到另一个节点数据卷没法直接跟随。我的建议是如果集群规模不大且你愿意接受节点重建后的数据重灌成本优先用本地 SSD 加节点亲和如果集群规模较大运维要求高用支持快照的网络存储把备份和恢复流程做扎实。不要把组复制完全依赖在存储虚拟化层的高可用上组复制本身已经提供了一份数据冗余存储层主要保证的是性能和恢复速度。4.3 探针设计不要用端口的存活状态而要查复制状态Kubernetes 的探针决定了一个 Pod 是否被纳入 Service 的端点列表。默认的 TCP readiness 探针只能说明 MySQL 进程在监听端口但无法说明它是否还是组复制的在线成员。更合理的设计是使用自定义探针脚本定期执行SELECT member_state FROM performance_schema.replication_group_members WHERE member_id server_uuid只有返回 ONLINE 时才认为就绪。如果一个节点处于 RECOVERING说明它还在追赶数据此时不该接流量如果返回 ERROR说明它已经掉出集群应该触发告警甚至自动重建。我还在某些项目里额外增加了对 Router 的健康检查Router 本身可以用 TCP 探针但如果要更严谨可以用 MySQL 客户端尝试发起一个真实连接。这个成本很低但能避免 Router 进程活着、却因为元数据问题拒绝连接的情况。4.4 故障自愈的恢复顺序先恢复仲裁者再恢复数据节点组复制遵循多数派原则。如果三节点集群里有两个节点同时宕机剩下的一个节点会失去仲裁能力进入只读状态无法继续做写入。此时如果自动恢复机制盲目重启单个节点很可能写入能力依然无法恢复。我建议在集群自动化恢复脚本里做一层顺序控制先确认存活节点数如果少于多数派优先拉起一个节点组成临时多数再滚动恢复其他节点。这里的细节是别用 Kubernetes 原生的 ReplicaSet 自愈逻辑直接替你做决策——它只看 Pod 数量不会理解组复制的仲裁算法。你需要一个更懂数据库的控制器或者至少一个带有预检查的运维脚本。这也是我后来在某高可用项目里引入 Operator 类组件的原因它能感知组复制的状态并做出恢复决策而不是盲目扩缩容。5. 排障实录一次主库连接全部超时的完整排查链路下面这一段我记录一次真实发生过的排障过程。现象是某业务系统突然大量报错应用连数据库的连接全部超时表面上看起来像是数据库挂掉了但实际上去每个节点上看MySQL 的端口都正常监听。5.1 排查第一步区分故障层级我习惯先把问题拆成两层数据库集群层和连接路由层。先用mysql -h某节点ip -P3306直连其中一台节点观察能不能登录再查询performance_schema.replication_group_members。结果显示三个节点都在线member_state都是 ONLINE。直连没有问题说明数据库集群本身没挂问题大概率出在 Router 上。接着连接 Router 的端口发现 Router 能建立 TCP 连接但执行任何查询都会超时。这就很有意思TCP 通应用层不通典型的 Router 内部状态异常。5.2 定位根因元数据缓存指向了一个不存在的节点查看 Router 的日志发现大量连接被路由到一台已经不在集群成员列表里的旧节点。原因是之前我们做过一次节点替换旧节点被移出了组复制集群但 Router 的元数据缓存里还残留了它的信息。正常情况下 Router 会周期性刷新但由于那次替换完成后某个组节点的group_replication_exit_states参数没有正确反馈Router 一直以为旧节点还健康。这个场景很有代表性Router 本身没有问题是脏数据。它的元数据来源是组复制节点上报的信息如果组内某个节点迟迟没有把成员变更广播清楚Router 就会基于过期信息做出错误路由。我临时修改了 Router 配置里的元数据刷新参数强制触发一次全量重建让 Router 重新从在线节点拉取成员列表问题立刻缓解。这次事故之后我要求团队在节点替换后必须执行一次 Router 元数据的主动刷新验证不能只依赖它自动周期刷新。5.3 类似场景的快速对照表如果读者时间紧可以直接参照这张表做快速排查现象可能原因快速验证解决方案连接 Router 超时但直连节点正常Router 元数据缓存过期查看 Router 日志看路由目标节点是否在线强制刷新元数据缓存或重启 Router主库切换后应用无法写入应用连接池未释放旧连接连接池里检查活跃连接的目标地址配置连接池的存活检测与最大空闲时间节点数据恢复卡在 RECOVERING分布式恢复网络不通查看 group_replication_recovery 的通道日志检查 seed 节点可达性和 rpl_user 配置节点被自动拉起后加入失败server_uuid 变化检查 auto.cnf 是否持久化使用持久卷保存数据目录两个节点都能写入误开启多主模式或引导配置残留查看 group_replication_mode改回单主模式清理 bootstrap 配置6. 组复制在云原生环境的性能调优与容量规划6.1 先接受一个事实组复制不是零损耗的组复制的写入路径比普通异步复制多了一个步骤主节点写完 binlog 后还需要将事务内容广播给组内多数节点并完成冲突认证确认没有其他节点同时在写同一个记录才能提交。这个额外的通信和认证开销是客观存在的单线程写入性能不可能和单机版 MySQL 拉到同一水平。所以在容量规划上我通常要求团队做减法先按单节点写入性能的 70% 左右预估组复制的写入上限再结合业务读写比决定需要几台只读节点。如果业务对写入吞吐的要求极高超过了一个主节点的安全承载范围组复制本身不是灵丹妙药你需要考虑分片。6.2 让 flow control 拉起减速带组复制内置了流量控制机制flow control它的作用是监视各节点的积压情况。当某个节点的应用线程追不上主节点的 binlog 时它会通过反馈机制让主节点放慢写入速度从而避免慢节点被彻底拖垮。曾经有团队觉得这个机制影响吞吐直接把它关掉。结果某个慢节点一旦落后主节点还在全速写入两者差距越来越大最终慢节点的数据落后太多分布式恢复重启失败整个集群失去冗余。我个人建议保留 flow control但它的目标不是把性能调到最高而是保证集群的稳态运行。可以在参数层面设置 flow control 的触发阈值让它的干预尽可能晚一些。但不要完全关闭。云原生环境里网络抖动更频繁这个机制是保护集群底线的关键。6.3 网络和资源分配上的实际建议组复制的节点间通信最好单独跑在低延迟的内网网络上不要在公网或跨地域网络上部署一个组复制集群。延迟超过二三十毫秒后写入性能会非常难看而且超时风险显著上升。资源方面CPU 建议把组复制认证相关的线程绑定到性能核上内存要给足 innodb_buffer_pool_size因为组复制的写缓存和认证信息也在内存里处理磁盘方面除了数据盘binlog 尽量放在独立的存储路径上避免写入放大互相干扰。网络策略上注意别让 Kubernetes 的网络策略把组复制的心跳消息给挡了。很多节点掉线检测超时是因为网络策略过于严格拦截了组内自定义端口的数据包。排查时先看节点之间是否可以互相 ping 通组复制端口再看防火墙和网络策略。6.4 事务和写入模式设计从应用侧减少冲突最后说一个常被忽略的点组复制对大事务非常敏感。一个大事务意味着认证阶段的比较开销更大其他事务的提交可能要等其他事务的认证完成再继续整个集群的并发度会被压下来。在实际项目里我遇到过一条批量 update 更新十万行的情况它在主节点上只花了零点几秒但组复制里因为要同步记录这么多行的 binlog整个集群卡了十几秒。后来团队把大事务拆成小批每批几千行集群的稳定性立刻好转。这类事务设计要前置到开发和压测阶段不要等到上线后再调试。建议在每次发布前做一轮组复制环境下的写入压测重点看两条线大批量写入下的吞吐曲线以及节点故障时的恢复时间。这两条线是判断这套架构是否适合你业务的最直观依据。最后分享一个小技巧组复制的节点恢复时间很大程度上受制于新节点加入时的全量快照大小。快照越大恢复越慢节点处于 RECOVERING 状态的时间越长期间它还无法对外提供读写服务。我目前用的方案是定期对节点做一次全量备份然后新节点加入时从最近的备份文件做物理导入而不是完全依赖组复制内置的分布式恢复。这个流程虽然多了一步但把节点加入集群从几小时压缩到几十分钟在节点频繁重建的容器环境里非常值得。另外记得在每次集群变更完成后主动验证一次 Router 的路由结果别等用户报障。半年了这套流程在几个项目里一直运转得还算稳希望这篇记录能帮你少踩几个坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑