简介: 成员变更是一致性系统实现绕不开的难题,对于提升运维能力以及服务可用性都有很大的帮助。 本文从Raft成员变更理论出发,介绍了Raft成员变更和单步成员变更的问题,其中包括Raft著名的Bug。 对于Raft成员变更的工程实现上需要考虑的问题,本文给出了一些工程实践经验。

一  引言

成员变更是一致性系统实现绕不开的难题,对于提升运维能力以及服务可用性都有很大的帮助。

本文从Raft成员变更理论出发,介绍了Raft成员变更和单步成员变更的问题,其中包括Raft著名的Bug。

对于Raft成员变更的工程实现上需要考虑的问题,本文给出了一些工程实践经验。

二  Raft成员变更简介

分布式系统运行过程中节点经常会出现故障,需要支持节点的动态增加和删除。

成员变更是在集群运行过程中改变运行一致性协议的节点,如增加、减少节点、节点替换等。成员变更过程不能影响系统的可用性。

成员变更也是一个一致性问题,即所有节点对新成员达成一致。但是成员变更又有其特殊性,因为在成员变更的过程中,参与投票的成员会发生变化。

如果将成员变更当成一般的一致性问题,直接向Leader节点发送成员变更请求,Leader同步成员变更日志,达成多数派之后提交,各节点提交成员变更日志后从旧成员配置(Cold)切换到新成员配置(Cnew)。

因为各个节点提交成员变更日志的时刻可能不同,造成各个节点从旧成员配置(Cold)切换到新成员配置(Cnew)的时刻不同。可能在某一时刻出现Cold和Cnew中同时存在两个不相交的多数派,进而可能选出两个Leader,形成不同的决议,破坏安全性。

图1 成员变更的某一时刻Cold和Cnew中同时存在两个不相交的多数派

如图1是3个节点的集群扩展到5个节点的集群,直接扩展可能会造成Server1和Server2构成老成员配置的多数派,Server3、Server4和Server5构成新成员配置的多数派,两者不相交从而可能导致决议冲突。
由于成员变更的这一特殊性,成员变更不能当成一般的一致性问题去解决。为了解决这个问题,Raft提出了两阶段的成员变更方法Joint Consensus。

1  Joint Consensus成员变更

Joint Consensus成员变更让集群先从旧成员配置Cold切换到一个过渡成员配置,称为联合一致成员配置(Joint Consensus),联合一致成员配置是旧成员配置Cold和新成员配置Cnew  的组合Cold,new,一旦联合一致成员配置Cold,new提交,再切换到新成员配置Cnew  。

图2 Joint Consensus成员变更

Leader收到成员变更请求后,先向Cold和Cnew同步一条Cold,new日志,此后所有日志都需要Cold和Cnew两个多数派的确认。Cold,new日志在Cold和Cnew都达成多数派之后才能提交,此后Leader再向Cold和Cnew同步一条只包含Cnew的日志,此后日志只需要Cnew的多数派确认。Cnew日志只需要在Cnew达成多数派即可提交,此时成员变更完成,不在Cnew中的成员自动下线。

成员变更过程中如果发生Failover,老Leader宕机,Cold,new中任意一个节点都可能成为新Leader,如果新Leader上没有Cold,new日志,则继续使用Cold,Follower上如果有Cold,new日志会被新Leader截断,回退到Cold,成员变更失败;如果新Leader上有Cold,new日志,则继续将未完成的成员变更流程走完。

Joint Consensus成员变更比较通用且容易理解,但是实现比较复杂,之所以分为两个阶段,是因为对  与  的关系没有做任何假设,为了避免  和  各自形成不相交的多数派而选出两个Leader,才引入了两阶段方案。

如果增强成员变更的限制,假设Cold与Cnew任意的多数派交集不为空,Cold与Cnew就无法各自形成多数派,则成员变更就可以简化为一阶段。

2  单步成员变更
实现单步的成员变更,关键在于限制Cold与Cnew,使之任意的多数派交集不为空。方法就是每次成员变更只允许增加或删除一个成员。

图3 增加或删除一个成员

增加或删除一个成员时的情形,如图3所示,可以从数学上严格证明,只要每次只允许增加或删除一个成员,Cold与Cnew不可能形成两个不相交的多数派。因此只要每次只增加或删除一个成员,从Cold可直接切换到Cnew,无需过渡成员配置,实现单步成员变更。

单步成员变更一次只能变更一个成员,如果需要变更多个成员,可以通过执行多次单步成员变更来实现。

单步成员变更理论虽然简单,但却埋了很多坑,实际用起来并不是那么简单。

三  Raft单步成员变更的问题

Raft单步成员变更的问题,最著名的莫过于Raft著名的正确性问题,另外单步成员变更还有潜在的可用性问题。

1  Raft单步成员变更的正确性问题

Raft单步变更过程中如果发生Leader切换会出现正确性问题,可能导致已经提交的日志又被覆盖。Raft作者(Diego Ongaro)早在2015年就发现了这个问题,并且在Raft-dev详细的说明了这个问题[1]。

下面是一个Raft单步变更出问题的例子, 初始成员配置是abcd这4节点,节点u和V要加入集群, 如果中间出现Leader切换, 就会丢失已提交的日志:

图4 Raft单步成员变更的正确性问题

  • t0:节点abcd的成员配置为C0;
  • t1 :节点abcd在Term 0选出a为Leader,b和c为Follower;
  • t2:节点a同步成员变更日志Cu,只同步到a和u,未成功提交;
  • t3:节点a宕机;
  • t4:节点d在Term 1被选为Leader,b和c为Follower;
  • t5:节点d同步成员变更日志Cv,同步到c、d、V,成功提交;
  • t6:节点d同步普通日志E,同步到c、d、V,成功提交;
  • t7:节点d宕机;
  • t8:节点a在Term 2重新选为Leader,u和b为Follower;
  • t9:节点a同步本地的日志Cu给所有人,造成已提交的Cv和E丢失。

为什么会出现这样的问题呢?根本原因是上一任Leader的成员变更日志还没有同步到多数派就宕机了,新Leader一上任就进行成员变更,使用新的成员配置提交日志,之前上一任Leader重新上任之后可能形成另外一个多数派集合,产生脑裂,将已提交的日志覆盖,造成数据丢失。

Raft作者在发现这个问题之后,也给出了修复方法。修复方法很简单, 跟Raft的日志Commit条件类似:新任Leader必须在当前Term提交一条日志之后,才允许同步成员变更日志。也即Leader在当前Term还未提交日志之前,不允许同步成员变更日志。

按照这个修复方法,最简单的实现就是Leader上任后先提交一条no-op日志,然后再同步成员变更日志。这条no-op日志可以保证跟上一任Leader未提交的成员变更日志至少有一个节点交集,这样可以发现上一任Leader的日志是旧的,从而阻止上一任Leader重新选为Leader,进而阻止了脑裂的产生。

对应上面这个例子,就是L1当选Leader后必须先提交一条no-op日志,然后才能开始同步Cv和E,以便能发现L2的日志是旧的,从而阻止L2当选Leader。

另一种方法是使用Joint Consensus成员变更,没有这样的正确性问题。

2  Raft单步成员变更的可用性问题

单步成员变更每次只能增加或者减少一个成员,在做成员替换的时候需要分两次变更,第一次变更先将新成员加入进来,第二次变更再将老成员删除,中间如果如果网络分区,有可能会导致服务不可用。

考虑a、b、c三个成员部署在三个机房,现在因为a发生故障要将a替换为同机房的d。按照单步成员变更,abc要先变为abcd,再变为bcd。

中间经历的4节点abcd的状态, 有可能在出现二分的网络分区(ad|bc)时导致整个集群不可用。因为a与d位于同一机房,这种二分网络分区的情况在实际情况中还是不容忽视的。

怎么解决这个问题呢?一种方法是做成员替换的时候,先删除老成员,再加入新成员,即abc先变为bc,再变为bcd,这样可以避免abcd的状态。

另一种方法是使用Joint Consensus成员变更,abc先变为abc U bcd  ,再变为bcd,也不会经历abcd的状态。

四  Raft成员变更的工程实践

Raft成员变更的理论虽简单,但实际工程实现上还是有很多地方要考虑。因为Raft单步成员变更有正确性问题及可用性问题,工程上建议尽量使用Joint Consensus成员变更,这里主要讨论一些Joint Consensus成员变更工程实现上必须考虑的问题。

1  新成员先加入再同步数据还是先同步数据再加入

因为Raft需要严格保证顺序,而新成员上还没有任何数据,因此新成员加入集群后需要先同步数据才能正常工作。工程实现时就有两种选择,一种是让新成员先加入再同步数据,另一种是先给新成员同步数据,同步完成后再加入。这两种方式各有利弊。

表1 新成员先加入再同步数据和先同步数据再加入的优缺点

新成员先加入再同步数据,成员变更可以立即完成,并且因为只要大多数成员同意即可加入,甚至可以加入还不存在的成员,加入后再慢慢同步数据。但在数据同步完成之前新成员无法服务,但新成员的加入可能让多数派集合增大,而新成员暂时又无法服务,此时如果有成员发生Failover,很可能导致无法满足多数成员存活的条件,让服务不可用。因此新成员先加入再同步数据,简化了成员变更,但可能降低服务的可用性。

新成员先同步数据再加入,成员变更需要后台异步进行,先将新成员作为Learner角色加入,只能同步数据,不具有投票权,不会增加多数派集合,等数据同步完成后再让新成员正式加入,正式加入后可立即开始工作,不影响服务可用性。因此新成员先同步数据再加入,不影响服务的可用性,但成员变更流程复杂,并且因为要先给新成员同步数据,不能加入还不存在的成员。

2  成员变更日志使用什么配置

成员变更日志本身是为了改变成员配置,处在成员配置变更的临界点上,因此成员变更日志使用什么配置就很关键。

表2 Joint Consensus成员变更日志使用的成员配置

对于Joint Consensus成员变更,成员变更日志使用什么配置是确定的。Cold,new日志使用联合一致成员配置Cold,new,需要老成员配置Cold和新成员配置Cnew两个多数派确认才能提交,Cnew日志使用新成员配置Cnew,只需要新成员配置Cnew的多数派确认即可提交,但Cnew日志也会同步给老成员配置Cold,主要是为了让Cold中不在Cnew中的成员自动退出。

3  成员变更日志什么时候生效

成员变更通过成员变更日志来完成,让各成员对成员配置达成一致,但成员变更日志与普通日志不同,并不一定要等到提交后Apply生效。

表3 成员变更日志的生效时机

对于Joint Consensus成员变更,成员变更日志什么时候生效是确定的。在Leader上开始同步成员变更日志之前就需要生效,在Follower上成员变更日志持久化完成后就需要生效。成员变更日志还未提交就先生效了,因此在Leader切换后可能会回滚。

4  成员变更期间日志是否需要严格按序提交

考虑这样一种情况,成员变更减少了成员数量,进而减小了多数派集合,而更小的多数派更容易达成,造成成员变更之后的日志比之前的日志先达成多数派。

按照Raft论文中的commitIndex的推进算法:

If there exists an N such that N > commitIndex, a majority of matchIndex[i] ≥ N, and log[N].term == currentTerm:

set commitIndex = N

一条日志达成多数派就往前推进commitIndex至该日志,如果该日志之前有日志按照老成员配置还未达成多数派,也一并提交了。

这种情况是否会出问题呢?实际上并不会,因为成员变更之后,已经有日志使用新成员配置提交了,不在新成员配置中的节点不可能再当选Leader了,进而不会覆盖之前的日志,因此就算之前的日志按照老成员配置未达成多数派也可以安全的提交。

hashicorp raft的实现还是严格按序提交的,即只有前面的日志都达成多数派之后才能提交。

5  只有少数成员存活时怎么恢复服务

Raft只能在大多数成员存活的情况下才能正常工作,实际可能会遇到只有少数成员存活的情况,这个时候要怎么恢复服务呢。

因为只有少数成员存活,已经不能达成多数派,不能写入数据,也不能做正常的成员变更。需要提供一个强制更改成员配置的接口,通过它设置每个成员的成员配置列表,便于从大多数成员故障中恢复。

比如只剩一个成员S1存活的时候,强制更改成员配置设置成员列表为{S1},这样形成一个只有S1的成员列表,让S1继续提供读写服务,后续再调度其他节点通过成员变更加入。通过强制修改成员列表,可以实现最大可用模式。

五  单步成员变更的工程实践

单步成员变更虽然不推荐在工程中使用,这里还是总结一下单步成员变更的一些工程实践,供研究讨论。

1  单步成员变更日志使用什么配置

对于单步成员变更,成员变更日志是使用新成员配置 还是老成员配置Cnew呢?实际上单步成员变更日志无论使用新成员配置Cold还是老成员配置Cnew都不会破坏Cold与Cnew的多数派至少有一个节点相交,因此单步成员变更日志既可以使用新成员配置Cold也可以使用老成员配置Cnew,两种方式各有利弊。

表4 单步成员变更日志使用老成员配置和使用新成员配置的优缺点

单步成员变更日志使用老成员配置Cold,可以避免单步成员变更的正确性问题,因此可以省略掉Leader上任后的no-op日志,同时在增加成员时可能只需要更小的多数派集合,但在减少成员时可能需要更大的多数派集合。

单步成员变更日志使用新成员配置Cnew,需要Leader上任后先提交一条no-op日志,以避免单步成员变更的正确性问题,同时在减少成员时可能只需要更小的多数派集合,但在增加成员时可能需要更大的多数派集合。

单步成员变更日志不管使用新成员配置还是老成员配置,最好都同步给新老成员配置中的所有成员,这样在增加成员时可以让新成员迟早收到通知,在减少成员时也可以让被删除的成员收到通知而自动退出。

Raft论文中单步成员变更日志使用新成员配置Cnew,etcd中单步成员变更日志使用老成员配置Cold。

2  单步成员变更日志什么时候生效

表5 单步成员变更日志的生效时机

对于单步成员变更,如果成员变更日志使用新成员配置,则与Joint Consensus成员变更一样,Leader上开始同步成员变更日志之前就需要生效,在Follower上成员变更日志持久化完成后就需要生效。如果成员变更日志使用老成员配置,理论上只需要在下一次成员变更开始之前生效即可,但实际为了让新加入的节点尽快开始服务,一般在成员变更日志提交后就生效。

Raft论文中单步成员变更日志使用新成员配置Cnew,本地持久化完成就生效;etcd中单步成员变更日志使用老成员配置Cold,提交后再生效。

六  总结

Raft提供了Joint Consensus成员变更和单步成员变更,极大的推动了成员变更在工程中的应用。本文总结了一些Raft单步成员变更的问题,以及成员变更的工程实践。Joint Consensus通用并且不容易踩坑,一阶段成员变更坑比较多。工程上建议尽量使用Joint Consensus成员变更。

原文链接

本文为阿里云原创内容,未经允许不得转载。

Raft成员变更的工程实践相关推荐

  1. Joint Consensus两阶段成员变更的单步实现

    简介: Raft提出的两阶段成员变更Joint Consensus是业界主流的成员变更方法,极大的推动了成员变更的工程应用.但Joint Consensus成员变更采用两阶段,一次变更需要提议两条日志 ...

  2. Raft 集群成员变更、日志压缩、客户端交互

    Raft 集群成员变更.日志压缩.客户端交互 集群成员变更 在集群服务器发生变化时,不能一次性的把所有的服务器配置信息从老的替换为新的,因为,每台服务器的替换进度是不一样的,可能会导致出现双主的情况, ...

  3. 一致性协议raft详解(四):raft在工程实践中的优化

    一致性协议raft详解(四):raft在工程实践中的优化 前言 性能优化 client对raft集群的读写 参考链接 前言 有关一致性协议的资料网上有很多,当然错误也有很多.笔者在学习的过程中走了不少 ...

  4. 【Raft】学习九:成员变更ConfChangeV2

    前言 在分布式系统中,节点的增删是常见也是必须的操作,对于实用的共识算法Raft自然也提供了关于节点变更的理论基础.在Raft算法中一次变更一个节点是天然支持的,如果一次涉及多个节点的变更,对于一个稳 ...

  5. 5000 万行以上大型代码仓库工程实践

    作者:marinewu,腾讯 PCG 工程效能平台部专家 腾讯 PCG 工程效能平台部自 2020 年开始进行大仓基本能力建设,并在 2021 年与工蜂合作成立了代码大仓研效联合项目组.在此, 我们想 ...

  6. 工程实践规模化推进要点分析

    本文纲要 [引言] [技术教练团队] [持续集成] [哪些实践更加优先] [复杂的自动化测试] L0自动化测试 L1自动化测试 L2自动化测试 L3自动化测试 [组织级工程实践氛围建设] [小结] [ ...

  7. C++接口工程实践:有哪些实现方法?

    简介:程序开发的时候经常会使用到接口.众所周知,C++语言层面并没有接口的概念,但并不意味着C++不能实现接口的功能.相反,正是由于C++语言没有提供标准的接口,导致实际实现接口的方法多种多样.那么C ...

  8. 大规模 Node.js 网关架构设计与工程实践

    作者:王伟嘉,腾讯云 CloudBase 前端负责人. 本文是王伟嘉在 GMTC 2021 全球大前端技术大会(深圳站)上的演讲内容:<十亿级 Node.js 网关的架构设计与工程实践>. ...

  9. c++ STL 工程实践的15条建议

    STL是c++非常重要的一部分,它是很多大神的杰作,高效,稳定,可扩展性好,虽然STL确实存在难以调试,内存碎片的问题(现在机器的内存越来越大,内存碎片的问题基本不太可能成为系统瓶颈,但只要你使用恰当 ...

最新文章

  1. ASP.NET 2.0 - 如何把上传的文件保存到数据库字段 (转自章立民CnBlogs)
  2. 使用机器学习检测TLS 恶意加密流——业界调研***有开源的数据集,包括恶意证书的,以及恶意tls pcap报文***...
  3. python基础知识整理-Python 重点知识整理(基于Python学习手册第四版)
  4. Java环境的安装与配置
  5. IT项目管理中如何应对预算削减的难题?
  6. Windows Server 2008 IIS7.0 发布html和Asp.net网站
  7. JS,JQ 格式化小数位数
  8. 腾讯急招.NET,但你准备好了吗?
  9. 64位CentOS6.2安装erlang及rabbitmqServer
  10. 【HDU - 1412】 {A} + {B} (STL + set)
  11. jav中什么是组织java程序_Java程序的执行过程中用到一套JDK工具,其中javaprof.exe是指()。A.Java调试器B.Java剖析工具C.Jav...
  12. Java-Runoob-高级教程-实例-方法:11. Java 实例 – enum 和 switch 语句使用
  13. 95-33-020-ChannelHandler-ChannelHandler简介
  14. Ruby设计模式透析之 —— 适配器(Adapter)
  15. winform keydown 等待按下另外一个键_真是没想到,手机电源键还有4个隐藏技巧,今天算是学到了...
  16. JavaScript文档DOM对象处理HTML→document属性方法、write、getElementBy**、getsetAttribute、节点操作方法、innerHTML、操作CSS样式属性
  17. (1)快速了解Redis
  18. jsp中空格字符怎么写_jsp多个空格符号怎么打
  19. 自己做量化交易软件(40)小白量化实战13--Alpha101及自编因子公式
  20. P2P分布式搜索引擎YaCy

热门文章

  1. Java开发中快速提升编码能力的方法有哪些?
  2. 如何给python升级_python升级后,如何给virtualenv里的python进行升级
  3. python训练手势分类器_python-Keras分类器的准确性在训练过程中稳定...
  4. linux 重定向 不换行,Ada:重定向到stdout时省略换行符(测试Put)
  5. MySQL故障检测_检测MySQL的表的故障的方法
  6. linux删除了mount目录,Linux记录-分区(df/fdisk/mount/umount/fuser)
  7. 京东开普勒php接口,IOS菜鸟初学第十五篇:接入京东开普勒sdk,呼起京东app打开任意京东的链接-Go语言中文社区...
  8. git配置全局用户名和密码_还在手动打包,手动传jar包?那你确实应该学一下jekins配置了...
  9. tail将输出的日志放到文件中_如何将Spring Boot应用中日志输出格式改为JSON?
  10. linux安装python3.7的步骤_Linux 安装python3.7.3