绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
MySQL Group Replication之Arbiter节点实现方案分析
2020-03-04 15:00:31

上一篇文章中,我们探讨了在MGR中引入arbiter节点的必要性和可行性,本文重点分析如何实现。

一、arbiter角色拆分

围绕arbiter节点是否记录binlog信息这个关键点。我们做了些需求收集,并在完成arbiter功能原型实现后,在网易云环境上进行了测试。终决定进行角色拆解:

- 从arbiter角色中拆分出日志节点角色(log_member),日志节点会回放其他节点提交的事务并将事务信息记录到binlog文件中;

- 保留下来的arbiter角色变为纯用于投票的角色(arbiter_member),不回放事务,当然也就不会写binlog。下面分别展开进行描述。

对于一个secondary节点来说,回放事务是大的资源开销,如上图中b~d所示,需要将认证通过并确定了回放次序的事务日志写入到relaylog文件中,然后由mysql复制的sql线程(coordinater和workers线程)读取relaylog中的事务日志进行并行回放,终写入binlog文件中。一个事务会被写2次(b,d),读取一次(c),回放一次(group_replication_applier)。写入和读取是IO开销,回放是计算资源开销,包括cpu和内存。

了解了secondary事务处理的简单流程后,再来说明下两种角色的实现。

二、arbiter_member实现

arbiter角色纯粹基于成本考虑,该角色是为了尽可能得降低计算和存储资源开销。

实现方案

在之前的文章-MySQL事务在MGR中的漫游记 - 路线图-中有个章节“事务在Applier_module中的处理”从不同角度分析了事务进入Applier_module后的处理流程,后汇总如下图:

从图一和图二我们很容易发现,如果将通过认证的事务直接丢弃掉,那么就没有事务数据读写操作,也不再需要事务回放。从而极大节省所需系统资源。

具体实现上,我们去掉了Applier_handler中2.3和3.2两步。

社区兼容性考虑

可能有部分同学会问,为什么不把整个pipeline去掉? 主要原因是我们需要解决arbiter_member节点的状态同步问题。因为不回放事务,那么gtid_executed就无法更新,这对于节点自身无影响。但会影响其他节点的流控(flow control)和冲突检测数据库清理(certification_info gc)等多个流程。

其实解决方法有多种,但基于简化系统设计,保持兼容性考虑,同时也为了减少代码改动量。我们选择将arbiter_member设计成一个除了member_weight为0之外,与MGR正常节点无差别的节点。

这样处理的好处是大程度保证版本兼容性,也就是说,如果MGR集群使用的是Oracle或Percona的MySQL版本,也完全可以使用InnoSQL版本来为现有集群新增一个arbiter节点,或将一个正常的secondary节点替换成arbiter节点。

为了达到这个目的,就需要保留图二中2.2a这步。这一步会给每个事务确定gtid和提交次序,而我们需要的正是gtid。certification_handler给事务分配gtid后,会将其加入到group_gtid_executed集合中,而我们在实现时就是使用group_gtid_executed替代gtid_executed发送给集群中其他节点。

三、log_member实现

日志节点角色除了节省成本,还希望保留binlog日志文件。在MySQL生态中,binlog文件非常重要,基于redo复制和共享存储的PolarDB也为了MySQL生态考虑重新增加了产生binlog文件的逻辑。具体做什么用,这里不进行赘述。

实现方案

既然希望保留binlog日志,那么前述的arbiter_member实现方案就不适用了。在上一篇文章中已经说明我们是基于blackhole存储引擎来实现的。

对于指定了存储引擎为innodb的ddl语句,通过源码处理在ddl回放时动态将其修改为blackhole引擎,当写入binlog文件时,仍维持原来的ddl语句,即不修改ddl语句内容。

容忍复制错误

在该方案的测试过程中,我们发现从库在某些情况下update语句会提示更新记录不存在而复制出错。由于非常偶现,未找到复现规律,还在定位中,非常怀疑是blackhole引擎的bug。(2020-2-19 经分析,复制出错是由于产生binlog的primary节点启用了参数binlog_rows_query_log_events=ON导致,已向mysql提了个bug)

由于是日志节点,只关心binlog而不关心数据本身,所以规避方法也很简单,只需要设置slave_skip_errors并去掉对应的错误日志即可。而且客观上说设置slave_skip_error=all可以增加日志节点可用性,规避复制潜在的其他bug。

这个问题给我们一种启示,其实我们可以将复制错误作为一种常态,这可以简化日志节点的创建和故障恢复,使之与arbiter节点的创建和故障恢复操作相一致。

四、节点初始化和故障恢复设计

故障恢复分为2种情况:上述2种节点故障恢复,集群中存在arbiter/log节点时其他节点的故障恢复。

MGR的故障恢复实现可参考文章:MySQL MGR成员管理与故障恢复实现 。这里通过一张见图来说明影响。

从图三可以了解到节点加入MGR集群时,会有全局恢复阶段:即通过group_replication_recovery这个复制通道从集群中其他节点(donor)异步拉取和回放本节点缺失的事务。

arbiter节点

由于arbiter节点的gtid_executed不会更新,所以退出集群后重新加入时需要回放非常多的缺失事务,导致全局故障恢复阶段耗时太长且极可能因为binlog文件被purge而失败。更重要的是全局恢复阶段对arbiter节点是无用的。针对这个问题,处理方法也很简单:

在节点加入集群前,先获取primary节点的gtid_executed信息xxx,然后执行“reset master; set global gtid_purged='xxx'”来将arbiter节点的gtid_executed初始化成跟primary一样。

但即使这样,只要primary节点在提交读写事务,arbiter节点还是会复制一些事务信息并回放,显然会出错。针对这个问题,经过权衡,我们没有做全局恢复阶段代码适配,而是通过开启slave_skip_errors来规避。

log节点

在上一篇文章提到,初始化日志节点的方法是通过mysqldump --no-data来初始化schema信息。但如果我们把复制错误作为常态,可以采用跟arbiter节点完全相同的故障恢复方案。即

在节点加入集群前,先获取primary节点的gtid_executed信息xxx,然后执行“reset master; set global gtid_purged='xxx'”来将arbiter节点的gtid_executed初始化成跟primary一样。

其他secondary节点故障恢复

正常节点需要在故障恢复时绕过arbiter节点。处理方法也很简单,在构建donor列表时,通过判断member_weight=0来剔除arbiter。

分享好友

分享这个小栈给你的朋友们,一起进步吧。

数据库内核开发
创建时间:2019-12-11 16:43:06
网易数据库内核技术专家 8年多数据库和存储系统开发经验,《MySQL内核:InnoDB存储引擎 卷1》作者之一,申请技术专利10+,已授权5+。曾主导了网易公有云RDS、MongoDB等数据库云服务建设 现负责网易MySQL分支InnoSQL开发和维护。专注于数据库内核技术和分布式系统架构,擅长分析解决疑难问题。
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

栈主、嘉宾

查看更多
  • 温正湖
    栈主

小栈成员

查看更多
  • xzh1980
  • else
  • Jack2k
  • at_1
戳我,来吐槽~