分布式一致性算法-Raft翻译和注解

分布式一致性问题

假设有一个单节点的系统,这个系统是一个数据库服务器,存储单个值。一个客户端往这个服务器发送一个值。在这个单节点系统中,这个值是很容易维护的,不会产生二义性,客户端设置的值是多少,服务器就存多少。

假设现在有多个节点的服务器组成的系统,就会有分布式一致性问题。这些节点服务器是对等的,客户端可以连接其中任意一个节点服务器来获取服务。考虑这样的场景:一个客户端往其中一个节点服务器设置了一个值;之后,客户端无论访问哪一个节点服务器,都能获取到这个值。

按照常理,客户端往其中一个节点服务器设置了值以后,这个节点服务器应该把这个值拷贝到其他节点服务器,那么客户端再访问任意一个服务器节点就能获取到这个值了。
但是,有很多种情况会导致上述情况不会如你所愿。假设三个服务器节点,分别为服务器A、B、C:

  • 情况1,客户端往A写了一个值5,那么A会通过网络向B和C拷贝这个值,在这个拷贝刚刚开始或者还未完成之前,客户端就向B去请求这个值,得到的值就可能是错误的。
  • 情况2,服务器B故障了(离线),此时客户端往A写了一个值,A通过网络把这个值拷贝给C,无法拷贝给B。之后B修复后上线,在数据还未同步给B之前,客户端向B请求这个值,得到错误的值。还有,这个值应该由谁同步给B呢?假设是A负责同步给B,而与此同时,客户端请求C将这个值改变了,在这个值还未同步给A之前,A将该值拷贝给B,B得到的就不是最新的值,此时向B请求到的值就不对。

上述的情况,就是分布式一致性问题范畴,即一个共识问题,所有服务器节点要对某个状态,某个值达成共识,给客户返回一样的结果(一致的结果)。
带着这些问题,下面看Raft算法的实现细节,想想它是如何解决这些问题的。

Raft算法细节

Raft算法是一种分布式系统中的一致性算法,提供和Paxos(点击了解Paxos)相同级别的功能和性能,2014年首次发表。Raft因其易于理解和实现的特性,已经被广泛应用于多种分布式系统中,包括分布式数据库、分布式文件系统、服务发现系统等。一些知名的使用Raft算法的系统和项目包括:

  • Etcd:一个高可用的键值存储,用于配置共享和服务发现,广泛用于Kubernetes。
  • Consul:一种服务网格解决方案,提供服务发现、配置和分段功能。
  • CockroachDB:一个分布式SQL数据库,设计目标是提供全球数据强一致性。

下面详细介绍Raft算法实现细节

节点状态

用一个实心圆表示一个节点,如图:
          实心圆
每个节点有三种状态

  1. 追随者(Follower),无边框的实心圆表示,如图:
    在这里插入图片描述
  2. 候选者(candidate),领导者的候选者,虚线边框的实心圆表示,如图:
    在这里插入图片描述
  3. 领导者(leader),实现边框的实心圆表示,如图:
    在这里插入图片描述

节点状态演变

选举leader过程

领导者选举(Leader Election),本小节只是简单描述领导者(leader)的选举过程,大家在本小节中只需大致了解选举领导者(leader)的过程,重点在关注节点的状态变化。后续有专门的章节详细描述领导者(leader)选举的细节。

  1. 所有节点初始都为追随者(follower)状态(无边框的实心圆):
    在这里插入图片描述
  2. 如果追随者(follower)发现没有领导者(leader)存在,那么就会变为候选者(candidate):
    在这里插入图片描述
  3. 候选者(candidate)向其他节点发出投票请求:请投票给我,让我成为领导者(leader):
    在这里插入图片描述
  4. 其他节点回复投票,如果候选者(candidate)节点得到大多数的投票,它就成为了领导者(leader):
    在这里插入图片描述

日志复制过程

在本小节中,只需大致了解日志复制(Log Replication)的过程。

所有对这个分布式系统的更改都经过领导者(leader)节点。下面以更改值5为例:

  • 每个更改会被作为一个日志项添加到领导者(leader)节点。并且这个新加的日志项处于uncommitted(未提交)状态,因此它不会更新领导者节点的值。下图中,红色的SET 5表示状态是未提交的更改:
    在这里插入图片描述
  • 为了提交这个日志项,领导者(leader)节点首先将它复制到所有追随者(follower)节点。日志项复制到追随者(follower)节点后也是保持未提交的状态(红色表示):
    在这里插入图片描述
  • 领导者(leader)进入等待状态,直到大部分节点回复已经记录该日志项为止。此时领导者(leader)节点上提交这个日志项(状态变为committed,SET 5颜色从红色变为黑色来表示),现在领导者节点的值为5了。
    在这里插入图片描述
  • 领导者(leader)通知所有追随者(follower)该日志项已被提交,收到这个通知的追随者(follower)立即提交自己的该日志项。
    在这里插入图片描述
    此时,这个集群关于这个系统状态就达成共识了(或者说这个值/状态在集群节点之间就一致了)。这个过程就是日志复制(Log Replication)

选举leader

本节详细描述领导者选举(Leader Election)的细节。Raft的选举过程设计得非常巧妙,下面开始慢慢体会。

初始的选举

在Raft中,有2个重要的控制选举的超时设置。

第一个是选举超时(Election Timeout)。选举超时时间,是追随者(follower)成为候选者(candidate)之前等待的时间。更详细一点,是追随者(follower)本身一直会有这个选举超时时间的倒计时,如果一直到倒计时结束系统中没有领导者(leader),那么这个追随者(follower)就会成为候选者(candidate)。

每个追随者的选举超时都是150~300ms之间的随机值,即大概率上,每个节点的这个选举超时时间是不相同的(后面“拆分投票”小节会描述当这个超时时间出现相同时的情况)。

以初始状态为例,所有节点都是追随者(follower),集群中没有领导者(leader),每个节点都有各自的随机的选举超时时间。如下图所示:
在这里插入图片描述
图中描述了在初始的选举周期(Election Term)(初始选举周期Term=0)中的情况,图中圆缺陷的部分表示当前选举超时时间的流逝,可以看到节点B流逝的要快一些,说明B节点的选举超时这个随机值最短,那么它会首先成为候选者(candidate),如下图:
在这里插入图片描述
B在选举超时时间之后,成为候选者(candidate),并且开启了一个新的选举周期(Term=1),向其他节点发起投自己成为领导者的投票请求,与此同时自己给自己投了1票,如下图:
在这里插入图片描述
收到投票请求的节点会检查自己在这个选举周期中是否已经投过票,如果已经投过了,那么啥事儿不做;如果还没投过,就会给发起投票的候选者(candidate)节点投一票。并且重置自己的“选举超时”时间(重新从0开始,经历一个150~300ms之间的随机时间)。如下图所示:
在这里插入图片描述
一旦一个候选者(candidate)拥有大多数(超过一半)节点的投票,它就会成为领导者(leader)节点。注意上图中A和C此时都更新了自己的选举周期Term值。

这个新的领导者节点(leader)开始给它的追随者(follower)们发送“追加条目(Append Entries)”消息。这个消息每隔一段时间就会发送,这个间隔由心跳超时(Heartbeat Timeout)指定。心跳超时也是第2个重要的控制选举的超时设置。
追随者(follower)们响应每个追加条目消息,并重置自己的选举超时时间

小结一下:

  • 追随者(follower)一直通过领导者(leader)发来的心跳(即追加条目消息)来检测领导者(leader)的存在。
  • 每次收到领导者(leader)的心跳检测时就将自己的选举超时时间重置,重新从0开始经历一个150~300ms之间的随机超时时间。
  • 当追随者(follower)在选举超时时间之内没有收到领导者(leader)的心跳检测,那么它将成为候选者(candidate),开启一个新的选举周期,发起新的选举自己为领导者(leader)的投票。
  • 上述过程是不断进行中的,从未停歇。

领导者故障后的选举

当领导者(leader)故障以后,会发生重新选举。流程如下:

以上面的示例继续,假设现在B故障了。

  • 此时A和C都有自己的一个随机选举超时时间,假设A的选举超时时间值比C的小,那么A会提前完成自己的选举超时,这期间由于B故障了,不会有来自领导者(leader)的心跳检测(追加条目消息)。
  • A成为候选者,并开启一个新的选举周期(Term=2,上一个Term+1得到),并为自己先投一票。然后向B和C发出投票请求;
  • B仍处于故障状态,无法作出响应。
  • C发现自己在新的Term=2选举周期中未给A投过票,于是给A回复“给你投一票”,并更新自己的Term=2。
  • 此时A的得票数为2。于是在这个新的投票周期中,A得到了“大多数”的投票(3个中的2个,自己的+C的投票),成为了领导者(leader)。
  • 之后A开始持续向B和C发送追加条目消息,做心跳检测。C不断收到A的心跳检测,不断重置自己的选举超时时间。

如图所示:
在这里插入图片描述
“需要大多数投票”和“每个选举周期只投一次票”的设定,保证了每个选举周期中只会选举出一个领导者(leader)。

拆分投票

因为每个节点的选举超时时间是一个150~300之间的随机数,理论上会出现多个节点之间的这个值相同的情况。如果领导者(leader)是存在的,出现节点之间的选举超时相同也不会有问题,因为会被领导者的心跳消息给重置。如果领导者(leader)不存在,那么这种情况就需要考虑了,会出现同时多个节点成为候选者(candidate)。如果多个节点成为候选者(candidate),那么会发生拆分投票(Split Vote)。如下图所示,是4个节点在选举周期为3时,都为追随者(follower)的场景:
在这里插入图片描述
假设A和D的选举超时时间恰好相等,那么A和D在会同时成为候选者,如下图所示:
在这里插入图片描述
如上图所示,恰巧节点A和D的选举超时时间相同,它们同时成为候选者(candidate),并都开启了选举周期Term=4的投票,其中绿色小圆点表示它们发出的为自己投票的请求,分别发向另外的3个节点。

假设B先收到A的投票请求,B为A投一票,当B再收到D的请求后,发现自己在该选举周期(Term=4)已经给A投过票了,将不再给D投票。同理假设C先收到D的投票请求,C为D投票,而不会给A投票。与此同时,B和C在收到投票请求的时候,都重置了自己的选举超时时间,不会成为候选者(candidate)。A和D两个节点已经给自己投了票,也不会出现彼此相互投票的情况。如下图所示:
在这里插入图片描述
当投票请求返回到A和D的时候,A和D在当前选举周期中各自有2个投票数。如下图:
在这里插入图片描述
由于A和D的得票相同,都没有“大多数”的得票,所以都不能成为领导者(leader)。

与此同时A、B、C和D还有自己的选举超时在倒计时(虽然B和C选举超时在收到投票请求时被重置了,但它俩新的选举超时也可能小于A和D的选举超时,也有可能成为候选者)。我们假设D先完成选举超时(当然可以是A、B、C、D中的任意一个先完成这个选举超时,这里以D为例),成为候选者(candidate),那么D将开始一个新的选举周期,向另外三个节点发起投票,最终D收到了4个投票成为新的领导者(leader)。

如果选举超时相同的节点还是超过1个,那么又会轮回到下一个选举超时中,直至选举超时都不相等为止。这里有3个说明:

  • 如果是偶数个节点,理论最坏情况下就会一直选举循环下去,无法选举出领导者(leader)。这也是推荐奇数个节点原因之一。
  • 即使奇数个节点,在故障的时候也会出现理论最坏情况。
  • 由于每个节点的超时时间是一个随机数,所以
    • 出现相同的概率很小。
    • 而且相同必须是选举超时时间最短的2个及以上个节点的时间值相同,这个概率就更小了。
    • 再进一步出现一直相同的概率就更更小了。
    • 最坏情况只会出现在初始偶数节点,或者故障部分节点的系统中。综合这些因素,概率就非常非常小了。

日志复制

日志复制(Log Replication)是指对分布式系统的所有修改都会作为日志项进行记录,首先在领导者(leader)节点记录修改日志,然后领导者(leader)节点将修改日志拷贝到所有追随者(follower)节点上,使得每次修改都在集群中达成一致性。

一旦有了选举出的领导者(leader)节点,对系统的所有修改都需要复制到所有节点上,这是通过领导者(leader)节点发出的追加条目消息(Append Entries)来完成的,该消息还用于心跳检测。

考虑下面这个三节点的分布式系统,C是领导者节点:
在这里插入图片描述
现在有一个客户端发起设置值5的请求,这个请求会发送到领导者(leader)节点,追加到领导者(leader)的日志中,作为一个日志项,该日志项处于未提交状态(uncommitted,红色字体表示)。如图所示:
在这里插入图片描述
该修改会随下次心跳(也就是追加条目消息)发送到所有追随者(follower)节点
在这里插入图片描述
追随者(follower)节点记录该修改日志之后,会回复领导者(leader)节点告知它“我已记录该修改”。领导者(leader)节点收到“大多数”追随者(follower)的回复后,就会提交这个修改日志项(状态变为已提交,committed,黑色字体表示)。如图:
在这里插入图片描述
然后,领导者(leader)节点回复客户端:已修改成功。如图所示:
在这里插入图片描述
然后,领导者(leader)节点向追随者(follower)们发送本次修改已提交的消息(通过追加条目消息),追随者(follower)们收到该消息后也会提交自己的这条修改的日志项,如下图:
在这里插入图片描述
(注:上图中节点B和A的SET 5应该会变为黑色,表示已提交的状态)

让我们再考虑:再发送将该值5增加2的修改命令,流程会是怎样的?

  1. 客户端向领导者(leader)节点C发送ADD 2的修改请求
  2. 节点C记录一个ADD 2的日志项,其状态为未提交。
  3. 领导者(leader)节点C用下一次心跳向所有追随者(follower)发送这次修改。
  4. 追随者(follower)收到心跳后,把修改记录到日志中,状态为未提交。然后回复领导者(leader)已记录修改。
  5. 当领导者(leader)节点C收到大多数追随者(follower)回复的已记录修改消息之后,将这个修改日志项提交,其状态变为已提交(committed)。
  6. 领导者(leader)回复客户端:已经完成这次修改.
  7. 领导者(leader)向所有追随者(follower)发送已经提交日志项的消息(也是通过心跳完成)
  8. 追随者(follower)收到消息后,将自己的这次修改日志项提交。


    这样,系统中所有节点都达成一致,值都更新成7了。如图所示:
    在这里插入图片描述

网络分区

Raft可以在网络分区(Network Partition)中保持一致性。
在这里插入图片描述
如上图所示,原本的领导者(leader)为B节点。此时由于网络故障导致了网络分区,将节点A、B和C、D、E隔离,即网络分区下半部分中A和B网络是互通的,网络分区上半部分中C、D和E之间网络是互通的,但是网络上下两区域之间不连通。

在网络分区上半本部分中,C的选举超时最快,很快就成为了候选者(candidate),并发起了新的Term=2的选举周期,并会得到D、E以及自己的合计3个选票,由于是5个中的3个,是大多数,所以节点C成为新的领导者(leader),如下图所示:
在这里插入图片描述
这样,每个网络分区中各自有一个领导者(leader),并且它们的选举周期是不同的。

假设一个客户端尝试设置B节点值为3,由于B节点无法收到大多数追随者(follower)的日志记录回复,所以B节点无法完成值修改(一致处于uncommitted的未提交状态)。如图所示:
在这里插入图片描述
假设另一个客户端尝试设置节点C的值为8,由于C能收到大多数追随者(follower)(包括自己在内共3个)的回复,所以完成值8的设置,并且D和E节点也能完成该修改(提交状态)。如图:
在这里插入图片描述
现在,修改好了这个网络分区故障。
B节点会看到更高的选举周期(自己的Term=1,而C、D和E的Term=2),那么B会回滚它没有提交的日志项,并且通过接收新领导者(leader)C节点的心跳,将自己的选举周期设置为2,且完成设置值8的日志提交操作。同理,节点A也跟B一样。如图:
在这里插入图片描述
现在,整个集群数据都一致了。

再看分布式一致性问题

现在再回过头来看第一节里描述的问题

  • 情况1,客户端往A写了一个值5,那么A会通过网络向B和C拷贝这个值,在这个拷贝刚刚开始或者还未完成之前,客户端就向B去请求这个值,得到的值就可能是错误的。

在Raft算法中,A写了一个值5,这个操作真正完成后,任意节点都能获取到最新的值5。因为只有在A向B和C拷贝写值状态成功以后,A上的值5才会提交,然后才会反馈给客户端写入成功。此时虽然B和C上的值5还未提交,但是都能读取到这个最新的值5。

  • 情况2,服务器B故障了(离线),此时客户端往A写了一个值,A通过网络把这个值拷贝给C,无法拷贝给B。之后B修复后上线,在数据还未同步给B之前,客户端向B请求这个值,得到错误的值。还有,这个值应该由谁同步给B呢?假设是A负责同步给B,而与此同时,客户端请求C将这个值改变了,在这个值还未同步给A之前,A将该值拷贝给B,B得到的就不是最新的值,此时向B请求到的值就不对。

情况2在Raft算法中是这样保障的:

  1. 故障节点B上线后,首先是要进行故障恢复流程的,因为B会检查到自己处于数据“过时”状态,需要从当前的leader那里同步最新的日志条目,然后才能向客户端提供服务。
  2. 故障恢复后的节点,最新的数据都是由leader节点负责同步给这些节点的。
  3. 修改数据请求都是由唯一的领导者来负责领衔处理的,即所有的写操作都需要经过领导者节点。

写在最后

  • 文章大概完成于2018年,一直躺在电脑的某个角落。最近想回顾过去的十年,重新翻了出来。
  • 最重要的一点:本文是对benbjohnson的杰出工作的翻译和增加了部分我对Raft的理解。benbjohnson对Raft的动画互动讲解是我看到过的Raft算法讲解中最直观易懂的,没有之一。大家伙可忽略文章内容,直接去观看动画演示点击观看Raft原理动画演示。有问题再来看本文,或者来这里跟我一起探讨Raft。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值