文章目录
腾讯文档
简介
Raft是一种用来管理复制日志的分布式一致性算法,提供了与Paxos相同的功能,但是比Paxos更好理解和搭建环境
Raft一致性问题包含三个子问题:
- Leader选举:集群启动或Leader失效时,需要选出一个新的Leader
- 日志复制:Leader需要接收客户端提交的日志,并将其复制到集群中的其他节点,强制其他节点的日志与leader一致
- 安全性:主要是状态机的安全性;如果有任何集群节点已经在其状态机中apply了一条日志,那么节点不能在同一个日志索引位置apply一个不同的日志
状态
所有服务器上的持久化状态:
currentTerm
:已知的最新的任期(即当前任期,服务器第一次启动时初始化为0,单调递增)votedFor
:当前任期内该节点选举的候选人,保存候选人id(candidateId),为空则表示没有投给任何人log[]
: 日志条目,每条日志包含应用到状态机的命令,以及Leader接收到该日志时的任期(初始index为1)
所有服务器上的易失性状态:
commitIndex
:已知已提交最新日志的index(初始为0,单调递增)lastApplied
:应用到状态机的最新日志的index(初始为0,单调递增)
Leader的易失性状态:
nextIndex[]
:对于每一台服务器,发送到该服务器的下一条日志的索引(初始值为Leader最后一条日志的index + 1)matchIndex[]
:对于每一台服务器,已知的已经复制到该服务器的最新日志的index(初始为0,单调递增)
追加条目RPC
AppendEntries RPC,由Leader调用,用于日志条目的复制,同时也被当做心跳使用
参数
term
:Leader的任期leaderId
:Leader id,Follower可以将客户端的请求重定向到LeaderprevLogIndex
:最新日志的前一条日志的indexprevLogTerm
:最新日志的前一条日志的termentries[]
:需要保存的日志条目(为空的话就当做心跳使用,为了提高效率可能一次性发送多个)leaderCommit
:Leader已提交的最新日志的index
返回
term
:当前任期,对于Leader而言,用于更新自己的任期success
:如果Follower所包含的条目和prevLogIndex、prevLogTerm匹配上了,则返回true
接收者实现:
- 如果leader任期小于接收者任期,返回false
- 如果接收者没有包含一个和prevLogIndex、prevLogTerm匹配的日志,返回false
- 如果一个已存在的日志条目和接收到的新的日志条目发生冲突(index相同但是term不同),则删除该已存在日志条目后的所有条目
- 追加日志中不含新的日志
- 如果leaderCommit > commitIndex,则设置commitIndex = min(leaderCommit, 接收者最新日志的index)
请求投票RPC
由候选者调用,用来征集选票
参数
term
:候选者任期号candidateId
:候选者idlastLogIndex
:候选者最新日志的indexlastLogTerm
:候选者最新日志的term
返回
term
:当前任期,对于候选者而言,用于更新自己的任期voteGranted
:返回true,表示候选者得到了选票
接受者实现
- 如果候选者任期小于接收者任期,返回false
- 如果接受者votedFor为空(当前任期没有投给任何人)或者为候选者id,且候选者日志至少与自身一样新,则投票给它
所有服务器需要遵守的规则
所有服务器:
- 如果commitIndex > lastApplied:则递增lastApplied,并将log[lastApplied]应用到状态机
- 如果请求或者回复的rpc里的term T > currentTerm:则currentTerm = T,并且切换成Follower
Follower:
- 响应Candidates和leaders的RPC请求
- 如果在选举超时时间内,没有收到Leader的心跳或者附加日志或者候选者的投票请求:则将自己变成候选者
Candidates:
- 在转变成候选者时立即开始选举过程
- 自增当前任期currentTerm
- 投票给自己
- 重置选举超时计时器
- 发送请求投票RPC给所有服务器,征集选票
- 如果收到大多数服务器的选票,则变成Leader
- 如果接收到新Leader的附加日志(AppendEntries)RPC,则转变成Follower
- 如果选举过程超时,则再次发起一轮选举
Leader:
- 转变成Leader后,马上发送空的附加日志(AppendEntries)RPC(心跳)给其他所有服务器,间隔一定时间重复发送,避免Follower超时
- 接收客户端请求,附加日志到本地日志中,在日志应用到状态机后响应客户端
- 对应Follower,如果最新的日志index(lastLogIndex) >= nextIndex,则发送从nextIndex开始的所有日志到跟随者
- 如果成功:更新相应Follower的nextIndex和matchIndex
- 如果因为日志不一致而失败,则递减nextIndex并重试
- 如果存在N > commitIndex,使得大多数matchIndex[i] >= N,以及log[N].term == currentTerm成立,则使commitIndex = N
Raft在任何时候都保证以下特性
-
选举安全性
一个任期最多只能选出一个Leader
-
Leader Append-Only
Leader的日志只附加
-
日志匹配原则
如果两条日志的term和index都相同,则认为这两个日志集合从头到该日志都相同
-
领导人完全性
如果某条日志在某一任期中被提交,则这条日志必然存在于更大任期的Leader中
-
状态机安全性
如果一个服务器已将指定index位置的日志应用到状态机中,则其他任何服务器在该index位置不会应用不同的日志
基础
一个raft集群包含若干个服务器节点,5个服务器是比较典型的,可以容忍2个节点故障
每个节点都处于这三个状态之一:Leader、Candidate、Follower
正常情况下,只有一个Leader,且其他节点全是Follower
Follower不会主动发起请求,只会简单响应Leader和Candidate的请求
Leader处理客户端的请求,如果客户端访问Follower,则会将请求重定向到Leader
Candidate状态用来选出Leader
Raft将时间分割成一个一个任期,每个任期从一次选举开始,选举出一个Leader来管理集群;若由于一些故障导致没有选出Leader(如选票被瓜分),则会很快开启一个新的任期
term在Raft中起到逻辑时钟的作用,用于检测过期的Leader。
在节点间进行通信时,都会交换Term,如果一个节点的currentTerm小于另一个,则会更新为较大值;如果Leader或者Candidate发现自己的term过期时,会切换成Follower状态;如果一个节点会拒绝一个含有过期term的请求
节点间通过RPC通信,Raft假定request和response都会丢失,所有需要请求者有重试的机制;为了性能,RPC请求会并行发出,基本的一致性算法只需要两种RPC:
- RequestVote RPC由candidate在选举过程中发出
- AppendEntries RPC由leader发出,用于复制日志和提供心跳
Leader选举
Raft通过心跳机制来触发选举,节点服务启动时处于follower状态,当其可以正常接收到Leader或Candidate有效的请求时(即请求中的term大于等于自身),就会保持follower状态。Leader周期性的发送心跳(不包含日志的AppendEntries RPC)给所有follower来确保自己的Leader状态,如果一个follower在指定时间内没有收到心跳,就会认为Leader过期,以此来开启一个新的选举
Follower开启新一轮选举,会将term加一并转为Candidate状态,先给自身投一票,然后向集群中的其他节点发出RequestVote RPC请求,Candidate会保持这个转态直到以下条件达成:
-
获取到了大多数选票成为Leader。每一个节点在指定的term内至多只能投票给一个Candidate,先到先得;一旦一个Candidate赢得选举,成为Leader,会发送心跳申明其Leader的身份,阻止新的选举
-
另一个Candidate成为Leader。在等待投票期间,Candidate可能收到其他节点的AppendEntries RPC,声明其已晋选成为Leader了,如果这个请求中的term大于等于Candidate的term,Candidate就会认为这个Leader是合法的,并转为Follower状态;如果term比当前的小,则会拒绝该请求并保持Candidate状态
-
没有Leader产生,选举超时。如果在一定时间内存在多个多个Follower变成Candidate,选票就会被瓜分,形不成多数派,这种情况下,Candidate就会超时并触发新一轮选举,此时如果不采取措施的话,可能会一直选不出Leader
Raft使用随机选举超时的方法来避免选票被瓜分导致一直选不出Leader情况,election timeout的值会在一个固定区间内随机的选取(比如150-300ms),这样大部分情况下存在一个节点先超时,并在其它节点超时前赢得选举并发送心跳。Candidate在发起选举前会重置自己的随机election timeout,减少选票被瓜分的情况
日志复制
leader选出来后,开始为客户端提供服务。每一个客户端请求会包含一个待状态机执行的命令,leader会将这个命令作为一条日志追加到日志中,然后将日志通过AppendEntries RPC并行的复制给其他节点,当日志复制完成后,leader就可以将该日志应用到状态机中,并将执行结果返回给客户端。如果Follower宕机或者执行很慢,甚至出现丢包的情况,Leader会无线重试RPC(即使已经将结果报告给客户端了),直到所有Follower最终都存储了相同的日志
leader会决定何时apply一条日志是安全的,即commited。Raft确保Commited日志是持久化的且最终被所有节点的状态机执行。一旦Leader将日志复制到多数节点后上,就会Commited,这表示在此之前的所有日志都被Commit了,包括之前其他Leader创建的日志。
leader会将最新的commited的日志index更新到之后的AppendEntries RPC中,以便于其他节点可以发现,一旦Follower发现一条日志被committed了,就会应用到自身的状态机中
Raft日志机制可以保证不同server上的日志具有很高的一致性。
日志匹配特性:
- 如果在不同节点的日志中存在两个记录有相同的index和term,则日志存储着相同的命令
- 如果在不同节点的日志中存在两个记录有相同的index和term,则表明在该日志之前的记录都是一样的
Leader在指定的index和term处只会创建一条记录,并且新纪录不会改变之前的记录,这保证了第一条;第二条是通过AppendEntries的一致性检查实现的,当发送AppendEntries RPC时,Leader会将之前最新日志的term和index包含在请求中,如果Follower在日志条目中没有找到相同的记录,就会拒绝该请求,所以只要AppendEntries RPC返回成功,Leader就知道Follower从头到此条日志都是一样的
在正常的操作中,Leader和Follower的日志是一致的,但是也存在一些不一样的情况,这种情况下,在Raft中,通过Leader强制Follower复制自己的日志来解决这种不一致的情况,这表示Follower日志将变得和Leader一致,存在冲突的地方则以Leader为准进行重写
Leader也是通过AppendEntries RPC的一致性检查来找到Leader和Follower日志一致的地方,Leader保存着每个Follower的nextIndex值,即下一条要发生到Follower的日志index,如果Leader和Follower日志不一致,则AppendEntries一致性检查就会失败,Leader就会递减nextIndex值并重试,直到找到一致的地方,这条AppendEntries RPC就会执行成功,并覆盖follower在这之后原有的日志,之后follower的日志会保持和leader一致,直到这个term结束
上边寻找index的过程可以进行优化减少AppendEntries RPC的次数,例如,当AppendEntries请求被拒绝时,Follower可以返回发生冲突日志所在的term以及该term的第一个index,通过这些信息,Leader可以直接跳过这个term中的全部index;Leader也可以通过二分搜索来查找第一个和Follower不同的日志。但是实际上这些优化不是很有必要的,因为正常情况下故障不会频繁发生,且不太可以存在太多不一致的日志
安全性
前面的描述不能充分的保证每一个状态机会按照相同的顺序执行相同的指令,所以需要增加一些特性来确保状态机做出正确的行为
选举限制
通过限制Candidate获取选票的条件来保证Leader存储所有已经提交的日志条目,如果Candidate的日志和绝大多数节点一样新,那么其一定存储了所有已提交的日志条目。所以在RequestVote RPC中存在限制:请求中包含了Candidate的日志信息,Follower会拒绝向日志没有自己新的Candidate投票。
Raft通过比较两份日志中最后一条日志的index和term来比较谁的日志较新,如果term不同,则大的较新;如果任期号相同,则日志index大的较新
提交之前任期的日志条目
Leader在将日志复制到多数节点后,就可以提交该日志了,但是如果在提交之前崩溃了,新leader就会尝试完成复制这条日志,然后在提交当前term的日志时,之前term的日志也间接被提交了
Follower和Candidate节点崩溃
崩溃后,Leader发送的RPC请求就会失败,raft会不断重试
如果节点在回复的时候崩溃了,Leader会重试这条RPC,该节点会收到重复的RPC,这个是没有影响的,如收到了重复的日志条目,就会忽略这些日志
持久状态和服务重启
Raft服务必须将一些数据进行持久化,避免重启后丢失
- 持久化当前term及投出的选票,避免一个节点在一个term内投出两票
- commit前持久化最新的日志,可以防止commited日志丢失
对于状态机来说,它可以使持久的,也可以是易失的;易失性的则需在重启后重新apply日志;持久的为了避免重复apply日志,则需要记录最后apply日志的index
如果一个节点丢失了所有持久化数据,则需要以一个新的节点加入到集群中
如果大部分节点都丢失了数据则需要人工干预了
时间和可用性
Raft的一项要求是它不能依赖于精确的时间,整个系统不能因为某些事件提前或者延后完成而不可用
广播时间 << 选举超时时间 << 单个节点故障间隔的平均时间(平均故障时间MTBF)
广播时间要比选举超时时间小一个数量级,如广播时间为100ms,则选举超时时间最好为1000ms
选举超时时间需要比MTBF小几个数量级,大多数的服务器的MTBF都在几个月甚至更长,很容易满足这条需求
转移领导权
raft允许一个节点将Leader权转给其他节点,如下列两种情况:
- Leader主动下线。服务器重启或者移出集群时
- 存在更适合担任Leader的节点,如性能更好
转移领导权之前,当前leader会将日志发送给目标节点,确保目标节点拥有了当前Leader的全部commited日志,然后该目标节点提前触发一轮选举
详细步骤:
- 当前Leader停止接收客户端请求
- 当前Leader通过复制日志将目标节点的日志更新为和自己完全一样
- 当前Leader发送一个TimeoutNow的请求给目标节点。这个请求会使目标节点立刻触发超时并开启新一轮选举,该目标节点有极大可能在其他节点超时之前完成选举,且他的下一条消息会包含新的term,导致当前Leader下线,完成转移
如果转移失败,当前Leader必须中断转移过程,并重新开始接收客户端的请求