Raft 协议分析:选举

Raft协议是现在使用最广泛的分布式一致性协议,下面从问题出发,看Raft怎么解决分布式中的种种问题。
分布式一致性
先问为什么,再问怎么做:“为什么需要分布式一致性协议”? 首先,需要分布式一致性协议的系统肯定是分布式的。分布式系统是由多台机器组成的集群来实现的。既然是集群,就必然引入额外的复杂度,比如网络故障、某个节点宕机等。这些是一个完整的分布式协议必须要解决的问题。 其次,系统有一致性要求。在单机场景下,最常用的一致性系统就是数据库了。它保证了写入成功的数据在多次读取的情况下结果是一致的,并且操作失败的情况下也不会影响之前的数据。单机情况下,操作系统可以保证内存和硬盘读写的原子性,所以串行操作的一致性很容易得到保证。而数据库通常在不同的事务隔离机制下,通过算法来保证并发的情况下也可以做到一致性。而在分布式系统下,无论是写入还是读取,都需要在多个节点上进行,可能面临读取的节点数据不是最新的或者部分节点写入失败的情况。这是分布式一致性协议要解决的第二类问题。 总的来说,分布式一致性算法就是为了让一组机器像一个整体一样工作,即使部分节点出现故障也能够继续工作下去,并且保证数据在集群内的一致性。 当前,基于分布式一致性算法典型应用场景有,1)提供分布式一致性的服务,比如zookeeper、etcd,业务系统基于它可实现分布式锁及master选举等功能。2)分布式数据库,比如mangoDB、TiDB等。
Raft协议背景
"这个世界上只有一种一致性算法,那就是Paxos", ---- MikeBurrows, Google Chubby的作者
Paxos是最先提出和被证明的分布式一致性算法,后续的算法都是基于它发展而来的。Paxos算法最大的问题就是难于理解,这个不是我们普通人难于理解,而是专门研究算法的专家们也很难按照协议的要求做出一个工程实现方案。这样导致一个问题,很多系统自己认为是Paxos的实现,但实际上在实现过程中做了很多妥协之后,最终的系统建立在一种没有经过证明的算法之上。 以上就是Raft出现的初衷,与其说Raft是一个全新的分布式一致性算法,不如说是Paxos的工程化扩展,是一个经过证明的,工程实现可行性更高的一致性协议。举个不太恰当的例子,如果把实现分布式一致性系统比作造一辆汽车,Paxos协议的就是告诉你需要发动机,变速箱,车架、轮胎等等这些东西,然后证明了这样造出的车是能跑的。而Raft协议的意义就是告诉你,具体需要多少个零件,这些零件怎么组装,能够最快的造出一辆好车。 还有一点需要提及,Raft协议出现的时候,已经有很多支持分布式一致性的系统在大规模部署的生产环境验证了可靠性。所以,Raft协议跟Paxos不同,它是先有了实现,后提炼的协议。从协议中也可以发现跟Zookeeper的ZAB协议有很多类似的设计。这从侧面证明了它的可靠性和可实现性。 后续我们从问题出发,看看Raft协议,还是要强调一下,这些问题不可能覆盖协议的所有情况,更大的意义是做笔记帮助理解,对照协议原文查看会更有帮助
Raft基础
Raft包含的模块
Raft协议分成3个部分来增加可理解性,分别是领导人选举、日志复制和安全性。
  • 领导人选举,Raft协议规定集群中必须有且只有一个领导者,客户端所有的读写请求最终都是发给领导人来处理的。在没有领导人的情况下,集群不对外提供服务。所以领导人选举是首要要解决的问题。
  • 日志复制,一致性算法是从复制状态机的背景下提出的,复制状态机通常都是基于复制日志实现的。在整个集群中,通过日志按顺序执行,来保证集群中节点上的数据最终都是一致的,简单点可以理解成数据库中常用的通过binlog进行主从数据同步,只是数据库的主从同步时一对一的。保证复制日志相同就是一致性算法的工作,所以协议针对日志复制,定义了一系列需遵守的规范。
  • 安全性,为了更好的保证数据一致性和容易实现,Raft额外定义了很多需遵守的安全性规范
Raft集群节点状态
集群中节点的角色及状态变换如下图:
 
0
集群中节点可能处于3种角色中的一种:
  • Follower(追随者): 接收Leader的指令保存数据,接收Candiate的投票请求,然后投票
  • Candidate (候选人): 发拉票请求给Follower,请求选自己为Leader
  • Leader (领导人): 在同一个任期(term)内,只会存在一个Leader节点
在集群处于稳定状态时,只会存在Leader和Follower;当Leader宕机后,集群进入重新选举阶段,只会存在Follower和Candidate,一旦新的Leader被选出,没选上的Candidate就转成Follower。
本篇和后续2篇文章将按如下顺序详细解释Raft:
  • 领导人选举
  • 日志复制
  • 集群扩容&缩容
这篇文章先来看下领导人选举
领导人选举
为什么需要选举
Raft将整个集群的运行过程,分成一个个任期(term),任期通过任期号来标识,是一个自增的数字。Raft规定每个任期内只能有且只有一个Leader,所有发给集群的请求都由Leader来处理。就是说对于客户端来说,虽然我面对的是一个集群,但是我只需要跟Leader交互就可以了,虽然不同的任期内Leader可能变化,但是同一时间面对的仍然只有一个节点。这样做可以大大降低整个系统实现的复杂性,Zookeeper的ZAB协议也采用了同样的设计。 而选举就是选Leader的过程,所以,每一个新的任期生成,一定伴随着一次重新选举。
选举达成的条件
决议通过都必需要超过半数的节点同意,这是Raft、Paxos及其它所有分布式一致性协议成立的基础。对于领导人选举来说,就是一个候选人收到了包括它自己在内的超过半数节点的选票。比如一个5台机器的集群,一个候选人如果收到除了它自己之外2台机器的选票,那它就可以成功当选。
选举过程
选举前的状态 在一个正常运行中的任期中,集群中有两种节点,Leader节点和Follower节点。Leader节点通过定时发送心跳给Follower来维持领导者状态,Follower通过接收到日志包,来接收数据。所有节点都持久化和临时保存一些属性用来维持状态和重启时恢复数据。 Raft节点属性列表:
属性名
定义
实时持久化
currentTerm
当前任期,任何节点都必须保存当前任期号,
在收到其它节点的rpc请求后,都会先对比任期,然后决定丢弃还是做出合理的response
votedFor
上次投票投给的候选人的 Id,一个任期内不会变化
log[]
日志集合, 每个日志条目包含一个指令和任期号
集群内部通过复制日志来同步数据
commitIndex
已经被提交的最后一条日志的索引
lastApplied
最后被应用到状态机的日志索引
nextIndex[]
仅Leader节点使用
记录了需要发送给其它节点的最后一条日志的索引号,每个Follower一条记录
matchIndex[]
仅Leader节点使用
记录了已经发送给其它节点最高索引号
跟nextIndex的区别讲到日志复制的部分再讲
 
再来看一下日志复制RPC的格式
参数
定义
term
领导人的任期号
leaderId
领导人的 Id,以便于跟随者重定向请求
prevLogIndex
新的日志条目紧随之前的索引值
prevLogTerm
prevLogIndex 条目的任期号
entries[]
准备存储的日志条目(表示心跳时为空;一次性发送多个是为了提高效率)
leaderCommit
领导人已经提交的日志的索引值
 
心跳包和日志复制使用同一个RPC调用,只是其中的entries属性为空 Follower对于日志复制rpc的Response
返回值
定义
success
Follower匹配上 prevLogIndex 和 prevLogTerm 的日志时为true,否则为false
term
Follower当前的任期号,如果Follower发现Leader发来的包任期号比自己小,说明领导人已经过期了,则返回false和自己的任期号
 
通过以上的RPC定义可以知道,在集群运行正常的时候,Leader不断地发送日志复制(心跳)包给Follower,而Follower接收日志然后更新本地数据和索引Index,这时候任期号(term)一直不会发生变化。 选举触发 有两种情况会触发集群的重新选举
  • 首次启动
第一次启动后,所有节点默认都是Follower,自然不会有Leader发心跳,所以等待一段时间后,就会有Follower将自己转换成Candidate,给其它节点发请求投票RPC。
  • 未收到Leader心跳
集群运行一段时间后,Leader因为宕机或者网络分区导致Follower收不到Leader的心跳,Follower也会变成Candidate
以上两种情况,Follower等待直到变成Candidate的时长称为选举超时。Follower在等待过程中,收到任何一种请求,都会将等待时长清零。包括Leader的心跳或者其它Candidate的投票请求。
Follower变成Candidate后,就会立即发送拉票的RPC给其他所有节点,RPC格式如下:
参数
定义
term
候选人的当前任期号,是原任期号+1
candidateId
候选人的 Id
lastLogIndex
候选人的最后日志条目的索引值
lastLogTerm
候选人最后日志条目的任期号
 
拉选票的rpc除了携带任期号和自己的id外,还需要带自己已经接收的日志索引,这是Raft为了在选举时能够选出数据最新的节点作为新的Leader,原因会在复制日志的部分解释。 RPC发出前,需要将当前任期号加1,作为新的任期号,放到term字段中。前面说过,一次新的选举必然对应一个新的任期。 Follower在收到拉票的请求后,会回复如下格式的Repsonse:
返回值
定义
voteGranted
true代表同意候选人当选新Leader
term
如果Follower发现自己的任期号比候选人大,回false,并且带上自己的任期号。
 
投票结果 候选人的拉票请求发出去后可能会出现3种结果
  • 赢得选举
说明该候选人收到了超过半数Follower节点的选票。这个时候候选人变成新Leader,一个新的任期开始,往外分发日志和心跳,直到它宕机
  • 输掉选举
候选人把拉票请求发出去之后,没有收到大部分Follower的选票,相反收到了新Leader的心跳。这种情况下,说明有别的候选人当选了,这时候候选人主动变成 Follower,承认新的Leader。
怎么判断这个Leader的心跳是新当选的而不是老的包呢?就是用心跳里的任期号,如果心跳包里的任期号大于等于自己的,则说明是新Leader,否则直接丢弃,继续等自己的选票。
  • 选举超时 候选人把拉票请求发出去之后,等啊等,既没有等到大部分Follower的选票,也没有等到新Leader的心跳。则认为选举超时了,处理方法就是把自己的任期号再加1,重新发拉票请求,开启新一轮选举。
以上就是一个完整的选举过程了,上面的循环会一直进行,直到一个新的Leader选出来为止。 对于选举超时的情况,我们可以想一下原因。首先有可能是大家都没得到足够多的选票,比如一个5个节点的集群,Leader挂了后还剩4个,这时候其中两个节点发出投票请求,分别得到2张选票,所以没超过半数,则没人当选。其次,当前候选人在发出选票后因网络问题,导致Follower投给它的票一直没收到,也会造成超时。 带着上面的问题进入下一节,看看选举过程中会出哪类问题,Raft是怎么从协议角度来规避和解决这些问题的。
选举安全性
  1. 如何保证只有一个候选人当选 Raft规定一个节点当选必须要在一个任期内收到超过半数Follower的投票。这个很容易理解,比如有5个节点,其中3个节点投票给我,其它候选人最多也就能得到2张选票,所以超过半数肯定是可以保证获胜的。其实这里面隐含了Raft协议的2点规定:
  • 同一任期内,Follower只能投给一个候选人,就是说同一个term内只有一次投票机会,不得改变自己的主意
  • 节点只能响应任期号大于或等于自己任期的请求,这个规定其实面向所有请求的,不止是针对投票RPC。比如当前Follower的任期号为5,这个时候来了一个投票的请求任期号是6,Follower就可以投票给它,同时会把自己的任期号改为6。从此以后只接收任期号大于等于6的请求。这样可以处理一种情况,就是我任期5的票投给了节点A,之后又收到节点B的任期6的请求,因为它的任期更大,所以又把票投给了B。如果A之前没有收到回复,重新发送了任期5的票,这时候会直接得到拒绝的答复。
  1. Follower如何决定是否投票给候选人
  • 票中带的候选人任期号不小于Follower的任期号
  • 任期号符合条件则先到先得, 这个和Zookeeper有一点区别,zookeeper会拒绝serverId比自己小的节点
  • 候选人必须拥有最新日志比自己新,这个在后面日志复制的时候解释原因
  1. 如何保证不进入选举超时-重新投票-选举超时的循环 进入以上的循环可能是如下的原因:
  • Leader宕机后,所有Follower都变成候选人,都投票给自己
  • 多个候选人瓜分选票,一直分不出胜负。比如一共4个节点A、B、C、D,A一直收到A和B的票,C一直收到C和D的票
首先对于上面第一种情况,Raft规定选举超时时间是从一个固定的区间(例如 150-300 毫秒)随机选择,这样在Leader宕机后,大部分情况下只有一个节点会选举超时并变成候选人,而不是大家同时变成候选人。 其次,如果第一步还是有多个节点变成候选人,然后大家又瓜分了选票导致超时,这个超时也是随机的(其实跟Leader宕机导致超时是一个),这样多个候选人不会同时发起新一轮投票,第一个发起新一轮的节点会把新任期号的投票率先给到其它候选人,其他候选人收到比自己大的任期号的拉票请求,就会自动转成Follower。所以每一次失败都随机,进一步减少了瓜分的概率。
4.如何保证在一个候选人被选为Leader后,集群内其它节点迅速达成一致 选举过程可能出现一种情况,候选人A收到了超过半数选票成为Leader,候选人B未收到足够多的选票而超时,所以发起新的一轮选举,因为B的任期号更高,所以A收到B的拉票请求后被迫又变成Follower。这样导致的结果就是拉长了选举的时间。 理论上Raft协议无法规避这种情况,而只能减少这种情况发生的概率:
    1. 候选人成为Leader后会以最快的速度发出心跳,在其它候选人超时之前,这样其它候选人会变成Follower
    2. 合理设置超时时间。一般来说在大部分网络情况下,广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障间隔时间(MTBF),每个时间都差至少一个数量级。Raft 的 RPCs 广播时间大约是 0.5 毫秒到 20 毫秒。因此,建议选举超时时间可能需要在 100 毫秒到 500 毫秒之间。大多数的服务器的平均故障间隔时间都在几个月甚至更长,所以上面的时间要求很容易满足。
5.Follower宕机,会触发选举吗 集群中一个Follower宕机,Leader仍然可以收到超半数节点的请求确认,所以不会触发选举。
6.如果一次性加入多个节点到集群中,会不会出现多个Leader的情况 在集群发生大规模变化时,比如如下的集群:
 
0
开始只有Server1-3,server1 通过1和2的选票当选Leader,而后加入Server4和5,Server5通过3、4、5的选票也当选Leader 针对这种配置更改情况,Raft协议有专门的定义,防止出现上面多Leader的状态。在第3篇文章中会有解析
总结
Raft协议采用任期加单个Leader的方式,大大降低分布式一致性的复杂性。把Leader选举作为一个单独的阶段提取出来,虽然这样会导致选举期间集群不可用,但是选举毕竟是小概率事件,相对来说还是利远大于弊。下一篇文章继续解析Raft对于日志复制的定义。
posted @ 2021-11-16 15:09  晨煦风清  阅读(406)  评论(0编辑  收藏  举报