Raft图文详解

Raft图文详解

refer to:

  1. Raft lecture (Raft user study) - YouTube

  2. Raft PDF

  3. Raft算法详解 - 知乎 (zhihu.com)

今天来详细介绍一下Raft协议

Raft是来解决公式问题的协议,那么什么是共识呢?

在分布式系统里面,consensus指的是多个节点对某个状态达成一致结果,而共识算法则是用来保障系统一致性的,比如说对于某个事件发生的顺序某个key对应的值、谁是领导等


那么如何实现共识呢?

现在主要有两种方法,第一种是对称的、无领导的方式,即server之间是平等的,都可以进行日志的添加或者复制,client可以和任何一个server交互

第二种方法是:非对称的,有领导者的。集群中有一台server负责统筹管理,其他server只是被动的接受她的决定,而client是直接与leader进行交互的

Raft则是leader-based,它将问题分解成了两个东西,第一个就是在有leader下normal operation,第二部分就是当leader crash的时候要做些什么去选出一个新leader。这样的好处就是在正常运行的时候整个系统非常简单,不需要考虑不同leader之间的冲突,效率也更高


Raft的整体目标是进行集群之间的日志的多副本复制,然后将log应用到状态机。假设你有一个程序或者应用想要可靠运行reliable,一种方式是把这个程序执行在一堆机器上,并且确保他们以相同方式执行程序。这就是复制状态机的概念。

log可以帮助确保这些状态机以相同的顺序执行相同的命令。

如果client想要执行一个命令z = 6,将会给这些任意一个server的共识模块发送这个命令,然后server会将命令存在本地的log中,除此之外,他还会将这个command传给其他servers,其他机器也进行本地存储。当这个command被安全复制到logs之后,将会把这个log传给状态机执行,执行完毕,返回结果给clients。所以我们可以看出来,只要logs和机器是相同的,这些不同机器上的状态机就会以相同顺序执行相同的命令,并且产生相同的结果

所以共识模块的任务就是管理这些logs,确保他们能正确的复制,并且决定什么时候能安全地把这些command传给状态机执行,之所以叫共识,就是不需要全部节点都在特定时间运行,只需要大多数节点即可。

image-20230305233533291


主要从六个方面介绍,

首先是leader election,我们如果在多个server里面选出一个作为leader,当崩溃之后,我们如何选,依据什么选一个新的leader呢?

第二点是在有leader是情况下系统是怎么正常运行的,比如接受client请求,log replication等等

第三点是leader change,领导人变更,这是能保障系统运行的关键部分,首先将说一下raft safe意味着什么,并且如何保证safe,然后将讨论一下leader如何解决一log一致性

第四点将说明一下关于leader changes的另一件事,即如何处理并没有真正死亡的旧leader回到集群的问题

第五点将讨论一下clients如何和集群交互,客户端在server crash的时候如何处理,raft是怎么保证客户端的每个命令只被执行一次的

最后将讨论一下,系统成员变更,例如增加或者减少servers

image-20230305235014376


在具体说Raft之前,首先来看一下server states,在任何时候,server都处于以下三种状态之一。分别是

  • leader state,即管理整个集群和客户端交互、日志复制的状态
  • follower state,这是一个完全消极的状态,被动接受RPCs,然后做的只有处理RPCs
  • candidate state,是两者的中间态,用于选举leader。

当系统正常运行的时候,只会有1 leader N-1 followers,下面这张图则是说明了三种状态之间互相转化的条件,这里就不仔细说了,在后面的内容中会体现这点。

image-20230306100405097


时间被分成一个个term,每个term有一个数字,并且这个数字是递增的。每个term 有两个部分,term的开始则是进行选举,即对当前term选出一个leader,如果选举成功则进入第二部分,即正常对外服务。可以看出每个term只有一个leader,并且这里有一些term是没有leader的,这一般是发生了split vote,分票,导致没有leader赢得多数票,当这种情况发生的时候,系统会马上重试,进入一个新的term。每个server都会维护一个current term,作为一个全序的值,用来识别过时消息非常有用。

image-20230306100654141


这张图其实已经基本完全的介绍了整个raft协议,

image-20230306101742231


OK,现在来看一下raft六个部分的第一个部分,选主。Raft需要保证在任何时候,最多有一台机器是真正的leader。当一个server启动的时候,他的状态是follower,在这个状态,它不会和其他任何followers交流,仅仅回复rpc。那么为了维持follower保持follower状态,必须让follower相信现在是有leader的,实现的方法就是它收到candidate或者leader发送的消息。所以,leader为了维护自己的存活就需要不断和其他servers交流,当没有具体任务的时候,就通过heartbeats来交互。如果一段时间follower没收到rpc,将认为没有leader,然后发起选举看看是不是需要自己当leader。而这段等待的时间,就是election timeout,通常在100-500 ms之间。初始化的时候,整个集群们没有leader,所以都等待election timeout时间,然后开始选举。

当一个server开始选举的时候, 首先就是增加自己的current term,进入一个新的term,当然新term第一件事就是开始选举,然后将自己从follower转变成candidate,在候选者状态要做的事就是让自己变成leader,即需要赢得大多数选票。candidate第一件事就是先给自己投票,然后发送request vote rpcs 给其他server请求选票。最后将会有三件事情发生,第一个就是candidate收到了多数选票,然后将自己转变成leader状态,立即向其他servers发送heartbeats。第二个就是candidate收到了一个有效的leader发来的RPC,然后就会step down成follower。第三种就是,没有一个人赢得了选举,有可能两个follower同时发起了选举,最后出现了分票,没人赢得大多数选票。当超过vote timeout时间还没选出leader,candidate就会重新增加自己的term,然后再次发起选举。


image-20230306104404679

这是一个选举的demo,初始时有三个节点,随机一段election timeout后,node c首先到时间发起选举,term 0->1 vote++,然后向node a,b发送request vote rpc,a,b收到之后更新自己的current Term,记录vote for给谁投票,然后回复node c,node c收到超半数投票当选为leader,然后发送心跳信号,node a,b收到心跳信号之后就会重置自己的计时器,并且更新leader,然后给node c肯定回复。


election过程需要满足这两个属性来确保安全,safety和liveness。safety说的是每个term最多只有一个leader当选。Raft是通过要求每个server每个term只能投一票来实现的,这样就能确保即使有两个不同的candidate,也无法同时赢得多数选票。第二个是liveness,必须要保证最后会有人赢得选举。如果一直重复split vote,是可能一直都没有leader的。Raft解决这个的办法是增大election timeout,当split vote之后下一次将在【T,2T】之间timeout,通过增大timeout,来减少两个servers同时醒来的可能性,当有一个先醒来的时候,他会有足够的时间向其他server发送请求完成选举。特别是T >> broadcast time的时候。


Raft的第二部分则是在正常运行情况下leader是如何进行log replication的。首先来看一下log 结构。每个server都独立保存着自己的log,leader…followers… log是由Log entry组成的有序列,而log entry是由index来索引的,在entry内部,是一个二元组(t,c) 分别是term和command,command是client发出的命令,term则是该log entry首次被创建的时候的leader term的值。log存储在稳定存储器上,例如磁盘,当server对log改变的时候,最后必须要在disk上做拷贝。像entry 7,被存储到了大多数server中,这时entry认为是committed,如果entry是committed,那么它可以安全地传给状态机来执行,raft可以保证该entry的持久化,不久之后,该entry就会被每一个server上的状态机执行。其实这个committed定义还不够完全的安全,在后面维护log的一致性的时候,会稍作调整

image-20230306111223812

Normal operation的过程非常简单:client向leader发出请求,想要将command执行到所有状态机中。而leader会首先将command追加到自己log后面,然后向followers发送append entries RPCs,一旦收到majority的response来认为entry已经committed,leader会将command应用到状态机并且给客户端返回执行结果,leader告知followers entries committed,follower得知已提交之后进行状态机执行。如果follower crash或者很慢,导致leader没收到回复,则超过timeout之后再次发送即可,当然leader也不需要每一个server都回复,只需要收到大多数节点回复即可。这样就使得整体效率比较高,不会因一个slow server导致整个system slow


Raft维护logs的高度一致性,这一页列出了一些任何时候都成立的属性。第一个是,index 和 term的结合 可以唯一标识一条log entry,两个在不同server上的log entries有相同的index和term,一定存着相同的command。此外,除了这两个,他们之前的所有entries也都相同,所以进一步index和term的结合可以唯一表示一整条log。 第二条,如果一个entry是committed,那么它之前的所有entries也都是committed。比如entry 5 committed,根据1.1前面也都存储在majority了。

image-20230306134350013

那么,是如何保证这些特性的呢? 是由Append entries consistency check来强制检查实现的。当一个leader向followers发送append entries rpc时,里面除了新的log entry,还会包含两个值,次新log entry的index和term;当follower收到rpc的时候,只会接受次新log entry能够匹配上的rpc。让我们看个例子,leader刚收到z<-0,然后发送RPC给followers,里面将会包含该entry 以及 (term,index) = (2,4), follower进行相应的检查上面的这个接收下面的拒绝。

这个一致性检查很重要,通过简单的归纳演绎就可以证明出一个新entry只有前面entry匹配的时候才能添加以此类推,推出如果一个follower接受了leader的一个log entry,那么该follower的log从开始到该entry和leader的log是完全匹配的。

image-20230306134358377

正常状态下的运行到此就讨论完了,下面讨论一下leader changes


Leader changes: 当一个新的leader被选出来的时候,它面对的log可能不是那么整齐,因为前一个leader可能在完成复制之前就挂掉了。raft并不需要采取特别的清理步骤来解决这个问题,只需要正常运行而log修复就在正常运行中完成。为什么不采取特殊措施呢?因为new leader来的时候有可能存在一些宕机的server很久才修复好回来,即使执行了clean up,也很难立即同步好所有logs。所以raft必须设计的正常运行的日志复制,必须能达到最终一致性。首先一点,raft认为leader的log永远是最新最完整的。leader要做的就是让followers和自己的日志同步上、匹配上。当然,在这个过程中新的leader可能会crash,反复几次,会出现很多繁杂的log entries。

因为前面说过term和index可以唯一标识log,所以可以只用这两者来代替log entry。如下图server 4,5当选了term 2 3 4的leader,但是出于某种原因没有向外复制log,然后挂了,并且4,5和123分区了一段时间,123 作为term 5 6 7 leader又复制了一些日志,最后导致了杂乱的log。但是关键的只有圈出来的这部分log,log entry 1 2 3,只有这些才是committed,需要维护保存的。其他的即没有应用到状态机也没有返回客户端结果,所以不重要。如何server 2 是 term 7 leader,最终它会让其他server log与他相同。在讨论如何修复这种凌乱的log之前,首先讨论一下安全性。我们如何能确保在这种凌乱的log下,我们丢弃添加log但是没有丢掉某些重要的信息,并且让系统一直正确地运行下去呢?这就是安全性问题

image-20230306135736536

Safety:所有做log replication的系统都需要保证的一件事,即某个状态机收到并apply了一条log entry就必须保证其他状态机对于该log entry不能apply一个不同的值。考虑到前面raft协议的复制过程,该安全性需求等价于任意两个位于相同index的committed entry必须是相同的。为解决这个问题,raft实现了一个safety property,即如果leader决定某个log entry是committed,那么raft将保证该entry将出现在所有未来leader的log中。如果可以让raft满足这个safe property,就能够保证上面的safety requirement。具体怎么做的呢?

  1. Leaders从不覆写日志条目,只会追加写
  2. entries要想提交,必须在leader的log里面才可能。// 这样其他值就不会提交
  3. entries在apply之前必须要committed

这几条共同作用来满足上面的需求。但是前面所讲的raft过程还不足以满足安全性需要,就如下面这张图,committed->present in future leader’s logs,还需要修改raft协议的内容,首先是修改一下选举过程,保证选到的新leader是最完整的,第二修改一下提交策略,有些时候可能需要延迟提交,直到确保安全之后。

image-20230306142949739

Pick Up-to-Date Leader: 如何选出拥有所有已经提交的log 的leader呢?其实,我们是无法判断哪个server有所有已提交的logs的,因为无法判断哪个entry是已提交的,如图,假设server 3 不可用了,entry 5是否提交取决于是否存储在这个已经不可用的server上。所以我们要做的是尽可能选最可能有最完整已提交entries的candidate当leader,

image-20230306144155324

具体的方式,是通过比较log;所以当一个candidate发送Request vote RPCs的时候,需要包含上自己最后一个日志项的index和term,这样可以唯一标识一整条log。当进行投票的server V收到RPC之后,会将candidate的log和自己本地的log进行比对,看哪个更完整。如果投票者的last term > 候选者last term,拒绝;如果last term相等,但是投票者的index更大,同样也拒绝,除此之外都是认为candidate的log更完整,将会进行投票。这样就能保证,不管谁嬴得选举,都是集群中log最完整的server。

image-20230306145925086


接着我们看一下新的选主方式的实践过程中。第一种是新leader提交了一个当前term的entry,例如s1 作为term2的leader,在server 3上复制完了entry 4,已经majority了,然后说committed,这样可以安全地apply到状态机。这样为什么是安全的呢?因为entry 4一定会出现在未来的leader中,比如s4,5都不可能当选leader了,一个是因为term一个是因为index。假设s1挂了 s2 s3会成功当选leader,完成日志复制。

image-20230306151107939

第二种情况,leader尝试提交之前term的entry。这种情况下,term 2的leader entry 3只复制了s1 s2就挂了,s5因为某些原因没有复制entry 3,创建了一些自己本地的entries就挂了。然后term 4 s1又成为了leader,想要同步log,所以让s3复制term 2的entry 3,这个时刻,leader直到entry 3存到了大多数节点,可以committed;但是,entry 3是不安全提交的。原因是,假设提交完entry 3,s1又挂了,很有可能s5会当选为leader,然后同步日志,把已经提交的term 2的entry 3 overwrite了。

image-20230306151324586

为了解决这个问题,修改了新的提交规则。除了已经存储到majority节点之外,还需要至少有一条来自当前leader term的entry已经存到了majority节点,才能提交;或者说新的leader只有提交过自己当前term的entry之后,才能将旧的entries committed。 再看这个例子,当完成复制到多数结点之后,entry 3也不能提交,需要等待entry 4提交,才能提交。这样,s5就没有机会当选leader了,不存在安全隐患。所以,Election rules 和 commitment rules 的结合使得Raft协议满足安全性,也就是满足一旦一个leader提交entry,该entry一定会出现在未来下一个leader的log中,以此类推,一定出现在未来任何一个leader中。

image-20230306152658244

现在已经讨论完了安全性,我们知道leader log永远是正确的完整的,最新的,那么如何让followers的log 和leader的log 匹配呢?首先,先看一下log有多种不一致情况;follower可能entries确实、如follower a&b&e;c,d,f是存在冗余entries,我们要做的就是删除多余无关的entries,填补缺失的entries。

image-20230306154009963

Repairing Follower Logs: 具体的方法是,leader为每一个follower维护一个next index,代表着每个follower下一个要写的entry的index,新leader当选后初始化都等于leader’s last log index + 1;图中next index = 10 + 1 = 11;在RPC的时候,发送的(term,index)其实就是log[nextindex-1]的term,index,当Append Entries consistency check失败的时候,将next Index–并再次发送RPCs请求;比如一开始将index=11的时候拒绝,减为10,以此类推,直到next index变成5,然后term和index匹配上了,然后就可以写entry 5了,以此类推完成A的复制。

image-20230306154609045

对于follower B,当替换了一个不一致的log之后,会自动将后续的无用log删除。这就是第三部分leader changes的全部内容,我们关注了两个问题,第一个问题是确保安全性,主要是如果选新leader以及怎么提交才是安全的;第二个问题是,当新leader选出来之后如何让followers和自己的log匹配,这个过程主要依靠了Append entries consistency check来实现。

image-20230306165651723

Raft第四部分也是关于leader change的一个问题,old leaders可能没有真正死亡;比如说,现在出现了一个网络分区的情况,当前leader短暂的和整个集群断开了连接,然后其他servers选出了一个新的leader,但是过了一会,old leader又重连回来了,他并不知道发生了选举和新的leader,只会和之前一样把自己当作leader去运行,比如说尝试复制日志,和client交互。如何阻止呢? 我们用Term来解决,每一个RPC都要包含上发送者的term,接收者接手之后会把该term和自己的当前term进行比较,如果sender比较老,则拒绝,发回包含自己term的response,sender收到后就会step down,反之一样。而选举过程,恰好是更新了majority server的term,而即使old server回来,也无法完成大多数共识,无法提交log entry。总之,如果有什么过时的东西,term会发现的。


接下来是第五部分,主要讨论一下client如何和系统交互的。client向leader发command,然后得到回复。如果不知道谁是leader,可以随意发,然后会返回client谁是leader,重发就行了。leader完成command的logged和committed和executed之后回复client。比较难的地方是如果这个过程中leader crash了怎么办?Client会认为leader crash,然后向其他server重新发出命令,其他server最终将返回一个新leader的地址,Client向这个新leader发起request就解决了。这保证了client的一条命令最终都会被执行。

下面是log replication的过程,这个过程的任何一个阶段,都有可能leader crash,

image-20230306172441492

特别需要注意以下这种情况:数据到达了leader,复制到 (0,all] follower,但是leader未收到响应,在这种情况下command可能被执行两次。因为leader挂掉之后,选的主肯定是包含v=3这个log的follower,然后再次完成复制和提交执行,这个过程中因为client不知道是否成功,可能会超时重发,而leader也无法分辨是否是相同还是连续两个命令,会再次复制提交执行,就导致相同命令执行两次。

为了解决这个问题:client需要将每一个command和一个唯一的id绑定,server会将这个id放在log entry中,在接受command之前,leader会检查自己的log有没有这个id,如果有,只需要忽视命令,返回之前的结果就行了。这样就实现了幂等性,解决重复执行的问题。

image-20230306172558231

最后一部分,我们需要一些能够改变系统配置的方法。这里的系统配置主要是指:server id network address;这决定了到底哪些是集群的大多数。为什么要有成员变更、或者说配置的改变,主要是为了替换failed机器,或者改变副本因子等等。

首先我们需要意识到,不能够直接切换集群的成员配置。如下面的例子,Cold是123,Cnew是12345我们想要从状态Cold切换到Cnew,在这个过程中很难做到同步切换,所以可能会出现这样一个阶段,server12可以组成old cluster的majority,而同时server123可以组成new cluster的majority,那么会发生什么,这个时间的选主或者复制都会出现问题,可能选出两个leader,也可能错误提交log。

image-20230306173809744

解决方法就是使用一个两阶段协议来完成这个过程。Raft协议规定首先将旧的成员状态,转移到一个叫joint consensus共同一致的过渡状态,这种状态的成员组成是old∪new,这时候的majority需要old的大多数和new的大多数同时达成。具体流程可以看这张图,我们开始的配置叫做Cold,然后同别的请求一样,client向leader发出请求,leader将这种共同一致的配置存到log里,即Cold U Cnew,然后向其他follower发送RPC,和普通的log replication过程一样,唯一的区别是它是立即起作用的,当配置log到本地,server就会立即把这当作当前的集群配置来用,不需要等待提交。Cold+new 什么时候committed的呢?即前面说的majority of old and majority of new; 这里在cold起作用的阶段可能有Cold里面的新leader当选,但是不影响正确性,因为根据log index,当选的肯定是包含这条成员变更log的机器;当中间态committed,整个集群就是在joint consensus下运行的,这个时候leader可以成员变更到Cnew,与之前一样,log然后复制,经过一段时间,Cnew committed了,就完成了成员变更,以后的决定的大多数都是看的Cnew了。

一阶段:使用两阶段是因为没有对C_old 和C_new 做出限制, C_old 和C_new可以各自形成不相交的majority选出两个Leader。而两阶段过程保证了Cold和Cnew不可能产生没有冲突的两个majority。或者另外一种方法,限制每次只允许增加或删除一个成员,这样一定无法形成两个不相交的majority,同时限制一次成员变更成功之前不允许开始下一次成员变更。


这就是六个部分的全部内容。简单回顾一下,第一个leader election,我们确保了某个term内最多只有一个server可以当选leader;第二部分主要介绍了以下选主之后的正常运行,包括接受客户端请求以及日志复制,提到了一个重要的一致性检测,Append Entries 一致性检测,从而证明了index和term能唯一标识log;第三部分,讨论了leader change,带来的两个问题,第一个是如何确保安全性,即当一个log entry committed他就会一直出现在后面的leader中。还有一致性问题,即如何让follower的log和leader log变得相同。第四部分,如何保证没真死的旧leader不会影响系统。第五部分,简单说了client做些什么,以及在leader crash情况下的高可用。最后,讨论了如何安全进行成员变更的方法。
分,讨论了leader change,带来的两个问题,第一个是如何确保安全性,即当一个log entry committed他就会一直出现在后面的leader中。还有一致性问题,即如何让follower的log和leader log变得相同。第四部分,如何保证没真死的旧leader不会影响系统。第五部分,简单说了client做些什么,以及在leader crash情况下的高可用。最后,讨论了如何安全进行成员变更的方法。

猜你喜欢

转载自blog.csdn.net/qq_47865838/article/details/129368121