分布式一致性Raft算法

参考文档:
https://raft.github.io
https://raft.github.io/raft.pdf

Raft是一个分布式的一致性协议算法,放弃了CAP的可用性,保证严格的一致性
sentinel使⽤的就是raft协议,raft在redis内并没有⽤来实现⼀些分布式锁以及分布式事务,仅仅是⽤来做master宕机时的选主、
腾讯云 CMQ、注册中心consul、go语言的etcd、等实际场景应用

一、角色状态

  • leader 领导者:接受客户端请求,并向Follower同步请求日志
  • candidate 候选者:Leader选举过程中的临时角色
  • follower 跟随者:接受并持久化Leader同步的日志

在leader的任期内,谁是leader谁就是老大,follower的日志都向leader看齐,多删少补。
在leader期间,leader的日志不会被删除,若leader被干下来了,新任期内成follower了,其日志向新leader看齐。

在这里插入图片描述

二、leader选举

Raft采用心跳机制触发Leader选举。
当系统启动时,所有节点初始化为Follower状态,设置任期为0,每个Follower有一个随机时间的计时器,在计时器超时时仍然没有接受到来自Leader的心跳RPC,Follower节点转化为Candidate节点,一旦转化为Candidate节点,立即开始做以下几件事情:

(1)增加自己的任期数
(2)启动一个新的计时器

任期的作用:raft中的term、zab中的epoch等,目的都是识别过期信息
任期与计时器是绑定到一起的。任何新任期的启动,必然伴随一个新计时器的启动

(3)给自己投一票
(4)向所有其他节点发送RequestVote RPC请求,并等待其他节点回复。

candidate在发送RequestVoteRPC时,会带上自己的term, lastLogTerm, lastLogIndex,follower在接收到RequestVoteRPC消息时,
1、若RPC中的term<当前follower的term,follower拒绝投票,且返回term信息,RPC发送者收到后更新自己的term
2、若当前follower未进行过投票,通过投票,更新自己term
3、若当前follower进行过投票,进行日志比较,如果发现自己的日志比RPC中的新,就拒绝投票。
日志比较的原则是:

  • 与本地的最后一条log entry的term id比较,谁更大谁就更新,
  • 如果term id一样大,则日志更多的更大(index大的更新)。

4、若候选者在等待选举期间,收到leader发送的AppendEntries RPC,比较term,

  • 若leader中的term大,则承认leader合法性,自己成为follower,更新term;
  • 若leader中的term小,拒绝leader AppendEntries RPC,继续维持候选者状态,old leader更新term

如果在计时器超时前接收到多数节点的同意投票,则转换为Leader。
如果接受到其他节点的AppendEntries心跳RPC,说明其他节点已经被选为Leader, 则转换为Follower。如果计时器超时的时候还没有接受到以上两种信息中的任何一种,则重复步骤1-4,进行新的选举。

Candidate节点在接受到多数节点的投票成为Leader后,会立即向所有节点发送AppendEntries 心跳RPC。所有Candidate收到心跳RPC后,转换为Follower,选举结束。

每个Follower在一个任期内只能投一票,采取先到先得的策略。 如果多个节点同时发起投票,每个节点都没有拿到多数票(这种情况成为Split Vote),则增加任期数,在新的任期内重新进行投票。

有没有可能Split Vote反复发生,永远都选不出Leader呢?

不会的。因为Raft采取随机超时时间,Raft系统有一个选举超时配置项,Follower和Candidate的计时器超时时间每次重新计算,随机选取配置时间的1倍到2倍之间。即使所有节点同时启动,由于随机超时时间的设置,各个节点一般不会同时转为Candidate,先转为Candidate的节点会先发起投票,从而获得多数票。因而在每个任期内,多个节点同时请求投票并且都只获得少数票的几率很小,连续多次发生这种情况几率更小,实际上可以认为完全不可能发生。一般情况下,基本上都在1-2个任期内选出Leader。

三、RPC

Raft核心算法部分只需要用到2个RPC:RequestVote和AppendEntries。每个Raft节点将会根据自己节点的状态数据来对这两种RPC请求进行处理。Follower不会发起任何RPC。
在这里插入图片描述

RequestVote RPC

是由Candidate发送给其他节点,请求其他节点为自己投票,如果一个Candidate获得了多数节点的投票,则该Candidate转变为Leader。

过程见上文

AppendEntries RPC

是由Leader节点发送给其他节点,有两个作用,当其entries域为空时,该RPC作为Leader的心跳;当entries域不为空时,请求其他节点将entries域中的日志添加到自己的日志中。同时进行log之间的一致性检查。

过程:

  1. 客户端向leader发送command请求;
  2. leader将command存入自己的log中;
  3. leader向所有follower发送AppendEntries RPC请求;典型场景下leader是并发请求的,即会同时向所有follower发送相同的请求,并等待接收响应结果;

假如follower宕机了,leader会一直不断地重试、重试,直到follwer重启后leader仍会接着发请求,直到请求成功;
leader并不需要等待所有follower都响应,只需要收到大多数server的响应以确保log被复制到大多数server即可;

  1. 一旦leader接收到足够多的响应,即至少一半(加上自己刚好过半,形成大多数),即认为这条entry已经被committed,则认为可以安全地执行这条command了(过半即提交,提交即可执行)

    • a. leader一旦认为某条entry已committed,则将对应的command传给它的state machine执行,执行完成之后返回结果给client;

    • b. leader一旦认为某条entry已committed,则会通知其他所有follower;即通过发送AppendEntries请求通知其他所有follower这条entry已经被committed了;最终集群中所有机器都知道这条entry已经被提交了;

    • c. 一旦followers知道这条entry已经被提交了,也会将对应的command传递给自己的state machine执行;

InstallSnapshot RPC

在这里插入图片描述

Leader节点使用该RPC来发送快照给太落后的追随者。

四、数据结构

currentTerm当前任期,默认为0,任期用连续的整数进行标号,每次+1
votedForcandidateId that received vote in currentterm (or null if none)
log[]log entries
termlog Entry:term when entry was received by leader 日志写入时任期号
indexlog Entry:position of entry in the log日志下标
commandlog Entry:command for state machine命令
leader维护
nextIndex[]for each server, index of the next log entry to send to that server (initialized to leader last log index + 1)
matchIndex[]for each server, index of highest log entry known to be replicated on server (initialized to 0, increases monotonically)

五、三个超时

  • 心跳超时

心跳是Leader发送心跳给Follower的时间。这个时间一旦超时,Follower会觉得Leader挂了,这样会开始新的选举周期。

  • 选举超时

就是新一轮选举开始时,每个节点准备转成候选者的随机的超时时间,这个时间一般100-到200ms。
假设集群由3个节点组成,为了防止3个节点同时发起投票,Raft会给每个节点分配一个随机的选举超时时间(Election Timeout)。在这个时间内,节点必须等待,不能成为Candidate状态。现在假设节点a等待168ms,节点b等待210ms,节点c等待200ms。由于a的等待时间最短,所以它会最先成为Candidate,并向另外两个节点发起投票请求,希望它们能选举自己为Leader。另外两个节点收到请求后,假设将它们的投票返回给Candidate状态节点a,节点a由于得到了大多数节点的投票,就会从Candidate变为Leader。

  • 最小选举超时

在分布式系统中,有时候需要对集群中的成员数量进行更新的操作。对于被下线的服务器而言,如果它们没有及时关闭,那么它们将不会接收到心跳信息和日志信息,从而不断发生超时,最后导致任期不断增加(高于集群中所有成员的任期),然后不断向集群中发送请求投票消息。集群中的Leader将变为Follower,集群中将不断开始新的选举,从而扰乱集群的正常运行。

六、client与raft交互

  1. client请求集群中的任意server,如果server不是leader,它会拒绝client请求并告诉谁是leader;若此时leader挂掉了,则client会不断重试,一直到发现新的leader
  2. 然后client可以重新请求leader
  3. leader执行client的command,但并不会立即响应。 command被记录到log中,并复制到大多数server上,即被committed,接着leader的state machine执行该command
  4. leader向client返回请求结果
问题:command是否有重复执行风险,幂等问题

leader可能在执行完某个command,但是还未向client发送结果时挂掉。而一旦出现这种情况,client不可能知道这个command到底是否被执行了,所以它会不断重试,并最终请求到新leader上,而这会导致该command被执行两次。

client为每个command生成一个唯一id,并在发送command时候带上该id

  1. 当leader记录command时,会将command的id也记录到log entry中;
  2. 在leader接受command之前,会先检查log中是否有带该id的entry;
  3. leader一旦发现log中已有该id的entry,则会忽略这个new command,并将old command的结果返回给client(如果此时old command还没执行完,会等待其完成再返回);

七、安全性规则

Raft以什么规则保证安全性

  • 拥有最新的已提交的log entry的Follower才有资格成为leader。

  • 什么规则下的log entry能被视为已提交的:

    • a、leader看到某条entry存在于majority of servers(过半即可)
    • b、leader必须看到至少一条来自其current term的entry也存在于大多数server上
leader完整性

leader完整性保证了leader拥有所有的已提交的entry,那leader是如何知道哪些entry是否已被提交,
To find out, it needs to commit an entry from its term.
Raft handles this by having each leader commit a blank no-op entry into the log at the start of its term
Raft让leader在其开始时向日志提交一个空白的无操作条目来处理这个问题

只读操作

只读操作不需要写任何日志,这可能会引发过期数据风险。响应请求的leader可能已不是集群中最新的leader,读取数据可能返回过期数据
首先:领导者必须掌握最新的信息提交了哪些log(leader完整性,见上)
其次:在处理只读请求之前,领导者必须检查它是否被废黜(让leader在响应只读请求之前与大多数集群交换心跳消息来处理这个问题)

旧term日志的提交要等到提交当前term的日志来间接提交

当新leader看到某条来自之前term的entry已经存在于大多数server上时,它不能直接认为这条entry是committed了,必须等到来自其自身term的第一条entry也存在于大多数server上,才能认为之前term的那条entry是committed的;
这其实相当于一种leader change场景下的延迟commitment吧,或者换种说法,相当于将上一个term时期的entry的commitment与当前term时期的commitment绑定在了一起,旧term日志的提交要等到提交当前term的日志来间接提交

提交规则b比较难理解,举个栗子:
在这里插入图片描述
有5个服务器节点,假设先不加b限制:
1、s1为 term2时的leader,将entry2复制到s2时服务器挂掉了,entry2这时只有s1、s2服务器上存在,
2、此时s5比s2提前发出RequestVote RPC,s5能获取到s3、s4的选票,加上自己一票满足选举条件,成为term3任期的leader
3、client向leader发送请求,产生了3条logEntry,s5假如因各种原因导致其entry在复制到其他follower上
之前又挂掉了
4、选举再次进行,假设这次s1重新被选举上成为term4任期的leader,进行日志复制时,s1上的entry3、entry4将都要复制到其他follower上,当entry3复制到s3时,此时entry3就已经满足存在于大多数服务器上,直接满足条件,被视为committed,entry3的command就会被state machines执行,client能够收到entry3的结果
5、仍在s1是term4的leader时,在准备将entry4进行复制时又挂掉了,只有s1本地存在entry4
6、再次进行选举,s5的任期是term3,为最高,成为term5任期的leader,再次进行日志复制,会将其term3时的entry3、entry4、entry5复制到其他follower上
7、这就导致了term2时期的entry3被执行后又被覆盖丢失了,刚才还是小甜甜,现在成了牛夫人。芭比Q了。。。

同样还是这5个服务器,现在加上b限制:

1~3步骤同上
4、选举再次进行,假设这次s1重新被选举上成为term4任期的leader,进行日志复制时,s1上的entry3、entry4将都要复制到其他follower上,当entry3复制到s3时,此时entry3就已经满足存在于大多数服务器上,满足条件a,还不满足条件b,是不会被视为committed,entry3的command现在还不会被state machines执行,client也还不能收到entry3的结果

s1在同步entry4这里分成功和未成功的两种场景进行分析

  • 5a、仍在s1是term4的leader时,在准备将entry4进行复制时又挂掉了,只有s1本地存在entry4
    6a、再次进行选举,s5的任期是term3,为最高,成为term5任期的leader,再次进行日志复制,会将其term3时的entry3、entry4、entry5复制到其他follower上
    7a、这就导致了term2时期的entry3被覆盖丢失了,但这有问题吗? 当然没问题,因为entry3是没有被执行,没被确认的,client也没有收到执行结果,不会产生错误的信号,丢了就丢了呗

在这里插入图片描述
回到第4步

  • 5b、仍在s1是term4的leader时,entry4也被复制到大多数上,如图所示,s2、s3都已完成复制entry3、entry4,此时满足条件b,entry3、entry4都会被committed,假如这个时候s1挂掉了
    6b、选举开始,s5能被选上吗? 毫无疑问的,选s5,s2、s3明显不同意啊

八、日志复制

raft是通过日志复制实现数据一致性,leader将请求指令作为一条新的日志条目添加到日志中,然后发起RPC
给所有的follower,进行日志复制,递减查询匹配,多删少补

leader为了log的一致性,为集群中的所有follower保存一个状态变量,nextIndex
1)nextIndex是leader准备向某个follower发送的下一个log entry的index;
2)当leader刚刚即位后,nextIndex的初始值是(1+leader’s last index);

举个栗子:
在这里插入图片描述
1、当某个节点成为term7时的新leader时,log中的最后一个是entry10,所以这个leader会将所有follower的nextIndex都设为11;
2、当leader发送AppendEntries RPC请求到集群中的follower时,会带上nextIndex前面的一个entry的index和term,(10,6)
3、follower a收到后,与自己的log比较,没有匹配的,会直接拒绝这个请求
4、leader发现请求被拒绝,nextIndex-1,再次尝试
5、如此往复,直到follower a有与之相匹配(4,4)的entry,接收请求,最终补充丢失的entry

在这里插入图片描述
1-2同上
3b、follower b收到后,与自己的log比较,没有匹配的,会直接拒绝这个请求
4b、leader发现请求被拒绝,nextIndex-1,再次尝试
5b、如此往复,直到follower b有与之相匹配(3,1)的entry,接收请求,
6b、当leader将entry4发送给follower,follower发现的对应位置的entry(4,2)与之(4,4)冲突,因此entry4会覆盖follower的entry,且会将follower中entry4后面的所有entries都删除掉(即图中的before变成after)

九、集群成员变更

在这里插入图片描述
假设目前系统配置中有3台机器,即server1/server2/server3;现在想再添加2台机器,server4和server5。我们没办法做到所有server同时更改配置,变更过程总要花点时间,而这会导致conflicting majoritys。

例如图中某个时刻,server1、server2仍是旧配置,它两不知道有新配置变更,仍认为集群是哥三个,这样server1、server2能够形成旧集群中的大多数,它们可以基于此进行leader election、commit log entry等;与此同时,另外3台机器,即server3、server4、server5已经是新配置了,占集群中五分之三,也可以形成新配置集群中的大多数,也可能commit与前2台server相同位置的log entry,而这很可能会产生冲突。(类似脑裂)

在这里插入图片描述

虚线:新的logEntry 创建还未提交
实线:最新的已提交的logEntry

raft采用2-phase(两阶段)的方式进行配置变更

raft先立即进入一个中间状态,叫joint-consensus(联合共识)

  1. 集群最初的已有配置是configuration-old(简写成Cold),而在某个时刻,由client发起集群配置变更,即client向leader发送配置变更请求(类似普通的operation请求),由Cold更换成Cnew;
  2. 当leader接收到配置变更请求时,它储存一个joint-consensus状态下的配置,Cold+Cnew,向自己的log中插入一条entry(跟普通的log entry是一样的),用于描述配置变更;

这个状态下leader election和log commitment需同时满足旧配置的大多数和新配置的大多数

  1. 然后leader通过AppendEntries RPC请求将该配置变更log entry复制到其他server上(跟普通的AppendEntries RPC复制log entry一样);

需要特别注意的是,配置变更立即生效!即一旦server将新配置的log
entry写入自己的log,它就立即生存在新的配置之下;它不需要等待大多数server都有这条entry,即不需要等待这条entry提交,一旦写入自己的log中就立即生效(这点与普通entry不同,普通的entry必须等到已提交才能被执行而生效);

  1. 一旦Cold+Cnew被提交了,即使当前leader挂掉了,触发选主,leader的安全性规则确保了只有有Cold+Cnew的server服务能被选择作为leader,
  2. leader再向自己的log中插入Cnew的配置日志,然后在集群中进行广播复制
  3. 当Cnew提交后,老配置无关紧要的了,不再新配置中的server可以shut down

集群变更的三个问题:

1、新的server没有任何日志

加入到集群后,需花点时间同步日志,可能一段时间内是不能提交新的日志,为了解决这个问题,raft增加了额外的阶段,新加入集群的server没有选票权,也不能成为候选者;当新server赶上来了,按上述重新配置(新来的先同步下项目进度InstalledSnapshot RPC,别着急上手)

2、集群的leader不在新配置的server中

当Cnew logEntry提交后,leader将会下台,成为follower,在创建和提交Cnew logEntry的这个过程中,它要同步日志给其他server。
当leader接收到足够多的响应,即至少一半(加上自己刚好过半 ,形成大多数,这种情况就不加自己了),即认为这条entry已经被committed

3、下线server,当然这个移除的不在新配置中

Raft引入了一个最小选举超时时间,意思是如果集群中存在Leader时,并且接收到心跳信息之后在最小选举超时时间内接受到请求投票消息,那么将会忽略掉该投票消息。

对于被下线的服务器而言,如果它们没有及时关闭,那么它们将不会接收到心跳信息和日志信息,从而不断发生超时,最后导致任期不断增加(高于集群中所有成员的任期),然后不断向集群中发送请求投票消息。集群中的Leader将变为Follower,而它又收不到回复,一直发,集群中将不断开始新的选举,从而扰乱集群的正常运行。

十、日志压缩

Raft的日志在正常运行期间随着client的请求增长,但是在实际的系统中,Raft的日志无法不受限制地增长。随着日志的增长,日志会占用更多空间,并且需要花费更多时间进行重放。如果没有某种机制可以丢弃日志中累积的过时信息,这最终将导致可用性问题。

在这里插入图片描述

  • 每个服务器都独立拍摄快照,仅覆盖其日志中的已提交条目
  • 状态机将其当前状态写入快照
  • 最后包含的索引是快照替换的日志中最后一个条目的索引(状态机已应用的最后一个log)
  • 服务器完成快照的写入后,它可能会删除最后一个包含的索引中的所有日志条目以及所有以前的快照。
  • 异常慢的follower或加入集群的新服务器,领导者可以通过向其发送快照InstallSnapshot RPC使其同步最新状态的

这种快照方法背离了Raft强大的领导者原则,但是,我们认为这种偏离是合理的。快照时已经达成共识,因此没有决策冲突。

考虑另一种基于领导者的方法,其中只有领导者会创建快照,然后将快照发送给其每个follower

有以下缺点

  1. 将快照发送给每个follower会浪费网络带宽并减慢快照过程。每个follower其本地已经具有生成自己的快照所需的所有信息,对于服务器而言,从其本地状态生成快照比通过网络发送和接收快照要方便得多
  2. 领导者的将更加复杂。例如,领导者会需要向快照者发送快照,同时向其复制新的日志log,以免阻止新的客户端请求

快照性能影响

  • 服务器必须决定何时进行快照。

如果服务器快照太多,浪费磁盘带宽和性能;如果快照太慢,则可能会堆积到存储容量上限,并增加了重启期间重放日志所需的时间。
一种简单的策略是 在日志达到固定大小(以字节为单位)时拍摄快照。如果此尺寸设置为明显大于 快照的预期大小,则用于快照的磁盘带宽开销将很小

  • 写入快照可能要花费大量时间

可能会延迟正常操作,解决方案是使用写时复制技术,以便可以接受新的更新而不影响正在写入的快照
使用操作系统的写时复制支持(例如,Linux上的fork)来创建整个内存的快照 状态机

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值