In Search of an Understandable Consensus Algorithm 论文
推荐视频:看戌米的论文笔记up主的视频
复制状态机
相同的初始状态,执行相同命令序列后,那么他们得结束状态也会一致
状态简化
raft只考虑吧状态切换不考虑像paxos那样状态之间的共存和影响,raft中只有leader follower candidate三种状态。
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/be450fc4d45e4e30a8e7150ddd390875.png#pic_center)
raft时间以任期作为分割每一个,一个任期包括最开始的选举操作,选举期间会选出leader,而选举失败这个任期会以没有leader结束此次任期并且会很快进行下一次选举。一个任期只能有一个leader。我们可以通过观察日志查看有无某个任期内的日志来判断这个任期内该节点是否宕机。
服务器节点直接使用RPC通信 raft主要有两种RPC
1.Requestvote RPC(请求投票RPC) 由候选者(candidate)发起
2.AppendEntires RPC(追加条目) 由leader发起并提供一种心跳机制
服务器RPC通信时会交换任期号如果服务器间任期号不一致,小的会替换成大的
如果服务器节点为leader或者candidate服务器会选择发现自己任期号过期会退回到follower状态
如果一个结点收到了携带过期的任期号请求会直接拒绝该请求
领导者选举
Raft内部有一种心跳机制,如果存在leader,leader会周期性的向所有follower发送心跳来位置自己的地位。如果follower有单时间没有收到心跳,该follower会认为系统中没有可用leader就会进行选举,选举开始后follower会增加自己任期号,并且并行的向集群中其他服务器发起投票(RequestVoteRpc)的进行选举。
超过半数票赢得选举成为leader
其他节点成为leader后任期号不小于自己 候选者candidate 会 回到follower状态
一段时间之后没有任何获胜者,每个candidate都在一个自己的随机选举超时时间(随机选举超时时间为150-300ms)后增加任期号开始新一轮投票。不使用随机时间可能会导致持续没有获胜者因为选举的总包含那几个
没有获胜者产生的原因候选者过多得票分散导致没有人得票超过半数
```go
//请求投票RPC Request
type RequestVoteRequest struct{
term int //自己当前任期
candidateld int //自己的ID
IastLogIndex int //自己最后一个日志号
lastLogTerm int //自己最后一个日志任期
}
//follower 请求投票的Response
type RequestVoteResponse struct{
term int //当前自己的任期号
voteGrabted bool //是否投票
}
对于没有成为candidate的follower节点,对于同一个任期,会按照先来先得的原则投出自己的选票 并且请求投票的节点要大于本follower的任期号的节点
RequestVote RPC要有candidate最后一个日志信息是为了解决安全性子问题
日志复制
Leader被选取出来之后,开始为客户端请求提供服务
客户端怎么知道新leader是哪个节点呢?
1.发送的老leader正好为leader
2.follower可以通过心跳获得leader的Id
3.如果leader正好处于宕机 那么client需要重复请求因为raft算法会选举出新的leader
一条日志所需的信息
1.状态机指令
2.leader的任期号
3.日志号(日志索引) [日志号+任期号两个因素才能唯一确定一个日志,因为leader宕机时候可能会造成日志号相同日志内容不同所以要使用任期号组合来确定一个日志]
leader并行发送给ApeendEntries RPC(日志放入这个RPC当中)给follower 让它们复制该条目 当该条目被超过半数的follower复制后,leader就可以在本地执行
我们把本地执行指令,也就是leader应用日志与状态机这不称作提交
可以看到图中有的follower当中日志落后太多
raft必须要在有宕机的情况下支持日志复制并且保证每个副本日志顺序一致
1.如果有follower因为某些原因没有给leader响应,那么leader会不断地重发追加条目请求,哪怕leader已经回复了客户端。
2.如果有follower崩溃后恢复,这时raft追加条目一致性检查实效,保证follower能按顺序恢复崩溃后缺失的日志
3.raft的一致性检查:leader在每一个发往follower的追加条目RPC中,会放入前一个日志条目的索引位置和任期号。如果follower在它的日志中找不到前一个日志,那么它就会拒绝此日志,leader收到follower的拒绝后会发送前一个日志条目,从而逐渐定位到follower第一个缺失的日志。
leader宕机了
如果leader崩溃那么崩溃的leader可能已经复制了日志但部分follower但还没有提交而被选出的新leader又可能不具备这些日志,这样就有部分follower中的日志和新leader的日志不同。
raft在这种情况下leader通过强制follower复制他的日志来解决不一致问题,这意味着follower中跟leader冲突的日志条目会被新的leader的日志条目覆盖(因为没有提交,所以不违背外部一致性,橙色部分事未提交部分,leader宕机了都没提交的部分因此follower更不可能提交)
type AppendEntiesRequest struct{
term int //自己当前的任期号
leaderId int //leader的ID
prevLogIndex int //前一个日志的日志号
prevLogTerm int //前一个日志的任期号
//两个日志的日志号和任期号进行一致性检查(这俩都与follower相同才会认为follower日志和leader一致)
entries []byte //当前日志体
leaderCommit int //leader的已提交日志
}
//追加日志RPC Response
type AppendEntriesResponse struct{
term int //自己当前任期号
success bool //如果follower包括前一个
//日志则返回ture
}
如果只有日志号相同而任期号不同
可能是上页中f情况需要向前回溯
如果leaderCommit > commitIndex 那么把commitIndex设为min(leaderCommit,index of last new entry)
安全性
因为有允许乱序复制所以中途会有空洞 例如后面的已经复制了而8还没有进行复制
领导者选举和日志复制两个子问题实际上已经覆盖了共识算法的全过程,但这两点还不能完全保证每一个状态机会按照相同顺讯执行相同的命令
所以raft通过几个补充规则完善整个算法,使算法可以各类宕机问题下都不出错。
这些规则包括
1.leader宕机处理:选举限制
如果一个follower落后了leader若干条日志(但是没有漏一整个任期),那么下次选举中后按照领导者选举里的规则,它依旧有可能当选leader。它在当选新leader后就永远也无法补上之前缺失的那部分日志,从而造成状态机之间的不一致。(已经提交的操作无法撤销所以必须包含所有已经提交操作的,但是可以不包含那些未被提交的为什么呢因为在日志复制节曾经讲过leader的日志可以让follower进行复制所以新的leader只要保证有所有提交的就行)
type RequestVoteRequest struct{
term int //自己当前任期号
candidateld int //自己的ID
IastLogIndex int //自己最后一个日志号
lastLogTerm int //自己最后一个日志任期
}
新日志的定义
如果两份日志最后条目的任期号不同,那么任期号大的日志更新
如果两份日志最后条目任期号相同,那么日志较长的那个更新
如何实现包含所有已经提交的
每次选举投票者会拒绝掉没有自己日志新的投票请求
2.leader宕机处理: 新leader是否提交之前任期内的日志条目
日志不能直接提交之前任期内的条目,只能跟随本任期内的条目提交时一起进行提交
以下将会对一个例子进行解析 :
1.在S1为leader时刻产生第二个日志时,s5s4 s3宕机 这时s1无法得到大多数投票无法提交
2.随后s1s2宕机然后s5通过竞选获得leader资格,并且这次任期内s5产生了新的命令日志
3.s5 s4宕机s1成为新leader并且把自己日志复制给了其他follower但是不能直接提交因为他的任期是4不能直接提交上一个任期内的日志
4.S1接受新命令并且提出4随后S1立刻宕机并未到提交4的那一步
5.S1宕机后随后其他恢复 然后S5当选 当选后把所有日志复制给未宕机的节点并且把2节点给覆盖掉了。可以想到如果之前直接把任期2内的日志提交了,那S5这次就会覆盖掉已经提交的S2并且S5这个节点并未提交任期2时刻的日志因此造成了不一致问题。
因此只有提交本任期内节点时刻才能顺带提交之前任期内未提交的日志
3.follower和Candidate宕机处理
Follower和Candidate崩溃后的处理方式比leader崩溃要简单的多,并且两者处理方式是相同的
如果Follower或者Candidate崩溃了,那么后续发给他们的RequestVote和Apeendentries PRCs都会失败
Raft通过无限重试来处理这种失败如果崩溃的机器重启了那么这些RPC就会成功地完成
如果一个服务器在完成了一个RPC但是还没有响应的时候崩溃了那么它重启之后会再次受到同样的请求(Raft和RPC都是幂等的)
4.时间与可用性限制
raft算法整体不依赖客观时间,也就是说,哪怕因为网络或其他因素,造成后发的RPC先到也不会影响Raft正确性
只要整个系统满足下面的时间要求 Raft就可以选举出并维持一个稳定leader
广播时间 <<选举超时时间<<平均故障时间
广播时间和平均故障时间由系统决定,但是选举超时时间使我们自己选择的。raft的ROC需要接受并将信息系落盘,所以广播时间大约0.5ms到20ms,取决于存储的技术因此,选举超时时间可能需要10ms到500ms大多数服务器平均故障间隔时间都在几个月甚至更长时间。
集群成员变更
在problem时间点Leader宕机,这时有三个节点为新配置 而另外两个节点为老配置但是因此可能会分别选举出新leader产生脑裂问题
配置信息作为一个日志体包装为一个普通的AppendEntires RPC 发送给所有Follower
type AppendEntiesRequest struct{
term int //自己当前的任期号
leaderId int //leader的ID
prevLogIndex int //前一个日志的日志号
prevLogTerm int //前一个日志的任期号
//两个日志的日志号和任期号进行一致性检查(这俩都与follower相同才会认为follower日志和leader一致)
entries []byte //当前日志体
leaderCommit int //leader的已提交日志
}
所以采用了两阶段方法
集群先切换到了一个过渡配置,称之为联合一致.
第一阶段leader发起了Coldnew使整个集群达到联合一致状态,这时RPC要在新旧两个配置都达到大多数才算成功
第二阶段leader发起Cnew使整个集群进入新配置状态这时所有RPC只要在新配置下达到大多数就算成功
一旦将新配置日志条目增加到自己的日志中,他就会用该配置来做出蔚来所有决策(服务器总是用最新配置无论该配置是否已经提交)
1.leader在coldnew未提交时宕机
s1s2超时开始选举,并且可以产生一个老配置leader
但是在联合一致状态下s3必须在老配置和新配置下都超过半数才能当选
所以s3无法当选leader集群中只能选出一个s1 s2中一个leader
这样集群成员变更就失败了但不会产生两个leader
2.leader在Coldnew已提交但Cnew未发起时宕机
这种情况下leader(S3)在两种配置集群中都超过了半数 Coldnew就可以被提交了
Coldnew提交就发起Cnew 这时只要leader满足新配置条件就可以提交日志
因为只要配置只要日志满足条件就行 而Cnew发起就意味着Coldnew就已经复制到大多数节点就不需要管老配置了
Cnew发起占大多数而且并未提交的时候,就可以只用Cnew来决定。而在Cold提交之后Cnew为占据大多数之前的时刻由联合一致性规则负责
在Coldnew发起并且未提交的时,raft集群还未进入联合一致的状态。这时leader宕机时,可以仅靠老配置选出新的leader
一旦Coldnew提交,raft集群就进入了联合一状态,这时leader宕机选出的新leader也要符合联合一致的投票规则
Coldnew提交后,leader就可以发起Cnew,从发起Cnew开始集群就可以仅靠新配置进行选举和日志复制了
如果缩减集群的情况下,leader可能自身就是缩减对象,那么它会在Cnew复制完成后自动退位
3.集群成员变更三个补充规则
1.新增节点时,需要等新增的节点完成日志同步再开始集群成员变更
这点事防止集群在新增节点还未同步日志时就进入联合一致状态或新配置状态,影响正常命令日志提交。
2.缩减节点时,leader本身可能就是要缩减的节点,这时它会在完成Cnew的提交后自动退位
在发起Cnew后,要退出集群的leader就会处在一个操纵一个不包含它本身的raft集群状态下。这时它可以发送Cnew日志但是日志计数时刻不计算自身。
3.为了避免下线的节点超时选举而影响集群运行,服务器会在它确信集群中有leader存在时拒绝Request Vote RPC
因为Cnew的新leader不会再发送心跳给要退出的节点,如果这些节点没有及时下线,他们会超时增加任期号后发送Request Vote RPC 虽然他们不可能当选leader,但是会导致raft集群进去投票选举阶段,影响集群的正常运行
为了解决这个问题,Raft在Request Vote RPC上补充了一个规则:一个节点如果在最小超时时间之内收到了Request VoteROC 那么他会拒绝此RPC
这样只要follower 连续收到leader的心跳,那么退出集群节点的Request Vote RPC就不会影响到raft集群的正常运行了
深入理解复制状态机
1.构建分布式存储系统是为了更大的存储容量
2.为了获取更大存储容量,我们把数据进行分片
3.而更多机器带来了更高的出错频率
4.为了容错我们为每个分片建立了副本
5.为了维持副本间的一致性引入了共识算法
6.共识算法有增加了资源和心跟那个的开销
数据量小的情况下 ,可以使用无leader共识算法 实现Chubby zookeeper
数据量大的情况下,使用有leader的共识算法Raft和Multi Paxos 实现由GFS HDFS
数据量大并且数据之间还存在关联的时候,一个共识算法集群容纳不了所有的数据。这种情况下,就要把数据分片到多个状态机中,状态机之间通过两阶段提交来保证一致性
raft算法改进
raft引入no-op解决了什么问题
- 问题的由来
- 引入no-op之后
- 总结
- 参考链接
- 问题的由来
raft论文In Search of an Understandable Consensus Algorithm中的figure8描述了如下场景,可能会违反raft协议的一致性保证。
(a):S1是Term2的Leader,将LogEntry部分复制到S1和S2的2号位置,然后Crash。
(b):S5被S3、S4和S5选为Term3的Leader,并只写入一条LogEntry到本地,然后Crash。
©S1被S1、S2和S3选为Term4的Leader,并将2号位置的数据修复到S3,达到多数;并在本地写入一条Log Entry,然后Crash。
这个时候2号位置的Log Entry虽然已经被复制到多数节点上,但是并不是Committed。
(d1):S5被S3、S4和S5选为Term5的Leader,将本地2号位置Term3写入的数据复制到其他节点,覆盖S1、S2、S3上Term2写入的数据,这里引用何登成老师的原话:这违背consensus协议原则
(d2):S1被S1、S2和S3选为Term5的Leader,将3号位置Term4写入的数据复制到S2、S3,使得2号位置Term2写入的数据变为Committed
在这个场景中,虽然logEntry被写入多数节点上,但是这条日志并没有被commit。在这种情况下,写入多数节点并没有推进raft集群的commitIndex。
这里代表了raft的一个隐含保证:前一轮Term未Commit的LogEntry的Commit依赖于高轮Term LogEntry的才能Commit。raft这个隐含的特点,不过不认真对待,会导致违背线性一致性。因此,我们需要引入no-op。
- 引入no-op之后
no-op是和普通的heartbeat不一样,no-op是一个log entry,是一条需要落盘的log,只不过其只有term、index,没有额外的value信息。
在leader刚选举成功的时候,leader首先发送一个no-op log entry。从而保证之前term的log entry提交成功。并且通过no-op,新当选的leader可快速确认自己的CommitIndex,来保证系统迅速进入可读状态。(raft协议的线性一致性读和写也有很多讲究,可以另写一遍文章)
具体是怎么做的呢?我们看下图:
(a):S1是Term2的leader,选为主后,将no-op LogEntry复制到S1和S2之后crash。
(b):S5被S3、S4和S5选为Term3的leader,并只写入一条no-op LogEntry到本地后crash。
©:S1被S1、S2和S3选为Term4的leader。
后面有两种可能:
(c1):S1作为leader,继续做了以下几件事:
写一条no-op LogEntry
在写no-op的过程中间接提交Term2的no-op,对S5而言,会覆盖Term3的no-op日志。
提交新的日志4
最终整个系统达成状态(c2),所有的节点对日志达成一致
(d2):S1写入一条no-op LogEntry之后就crash了。S5被S3、S4和S5选为Term5的leader。
写一条no-op LogEntry
在no-op提交的过程中间接提交Term3提交的no-op,对S1、S2和S3而言,会覆盖不一致的日志。
提交新的日志3
最终整个系统达成状态(d2),所有节点对日志达成一致。
可见,我们通过引入no-op,修复了之前可能存在的问题,提高了系统的可用性。
那么是否引入no-op之后,之前的违反一致性的情况就不会发生了呢?我们看下面的对比图。
引入no-op之前,如博士论文所述,包含value信息的LogEntry有可能被覆盖掉。
引入no-op之后,如果当前leader已经开始提交含有value信息的LogEntry,那么它一定将之前的LogEntry全部提交了,就算它crash了:
系统也会选拥有最新最全日志的candidate为leader,比如上图,S5就不可能像之前一样成为leader
就算有日志覆盖,覆盖的也是no-op,或者没有复制到多数节点的LogEntry。不会是已经复制到多数节点的包含value的LogEntry。
3. 总结
通过no-op,我们解决了raft在实践中遇到的违反consensus的问题。另外可以保证新当选的leader迅速获取系统的CommitIndex,方便提供读服务。
当然,引入no-op会让系统复杂化,产生额外的落盘开销。但是,工程上可以通过Leader Stickiness,增加pre-vote等方式避免leader频繁切换。另外raft本身的幂等性保证也决定了,LogEntry可能会被raft系统commit多次,这些重复的log也可以被认为是no-op,可以被RSM状态机过滤掉。
本人对raft的理解也仅限于6.824(这个不用no-op也可以过,更不要提各种工程上的优化了)、博士论文和各种文章的解读,并没有工程上大规模的实践。有理解不对之处欢迎探讨。
- 参考文章
一文看尽 Raft 一致性协议的关键点
raft在处理用户请求超时的时候,如何避免重试的请求被多次应用?
braft文档 RAFT介绍
线性一致性:什么是线性一致性?
关于Paxos "幽灵复现"问题看法
CAP理论是指分布式系统在一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)三个方面无法同时满足,只能选择两个1。根据这个理论,我们可以对Redis、RocketMQ和ZooKeeper进行分类:
Redis:Redis是一个基于内存的键值数据库,支持主从复制和集群模式。Redis的主从复制是异步的,也就是说主节点写入数据后不会等待从节点的同步,而是立即返回给客户端。这样做的好处是提高了可用性,但是也牺牲了一致性,因为在某个时间点,不同的节点可能有不同的数据。Redis的集群模式也是采用了AP原则,当出现网络分区时,集群会保证大多数节点可用,但是无法保证数据的强一致性。因此,Redis是一个典型的AP系统23。
RocketMQ:RocketMQ是一个分布式的消息中间件,支持同步和异步的消息发送模式,以及事务消息和顺序消息等特性。RocketMQ的消息发送是基于TCP协议的,保证了消息的可靠传输。RocketMQ的消息存储是基于文件系统的,支持同步和异步的刷盘策略,以及主从复制和集群模式。RocketMQ的消息消费是基于拉取的,消费者可以根据自身的消费能力来拉取消息,也可以设置消费偏移量来控制消费进度。RocketMQ在不同的场景下,可以选择不同的一致性和可用性的权衡。例如,如果要求消息的强一致性,可以采用同步发送和同步刷盘的方式,但是这样会降低可用性和性能;如果要求消息的高可用性,可以采用异步发送和异步刷盘的方式,但是这样会增加消息丢失的风险。因此,RocketMQ是一个既可以是AP系统,也可以是CP系统的系统,具体取决于业务需求和配置 。
ZooKeeper:ZooKeeper是一个分布式的协调服务,提供了分布式锁、配置管理、服务发现等功能。ZooKeeper采用了ZAB协议来保证数据的一致性,ZAB协议是一个基于Paxos算法的变种,要求集群中的大多数节点(超过一半)正常工作,才能对外提供服务。ZooKeeper的写操作是通过Leader节点来协调的,Leader节点会向Follower节点发送提议,等待Follower节点的响应,如果收到超过半数的响应,就会发送提交命令,完成写操作。ZooKeeper的读操作是直接从本地节点返回的,不需要经过Leader节点,但是可以通过sync命令来强制和Leader节点同步数据。ZooKeeper在出现网络分区时,会牺牲可用性,保证一致性,因为只有能和Leader节点通信的那部分节点才能继续提供服务,而另一部分节点会进入查找Leader的状态。因此,ZooKeeper是一个典型的CP系统 。