1.1.1 选举投票可能会多次轮番上演,为了区分,所以需要定义你的投票是属于哪个轮次的。
- Raft定义了term来表示选举轮次
- ZooKeeper定义了electionEpoch来表示
他们都需要在某个轮次内达成过半投票来结束选举过程
1.1.2 投票PK的过程,更多的是日志越新越多者获胜
选举出来的leader至少包含之前全部已提交的日志
有2种选择,一种就是所有日志中的最后一个日志,另一种就是所有已提交中的最后一个日志。目前Raft和ZooKeeper都是采用前一种方式。日志的越新越大表示:轮次新的优先,然后才是同一轮次下日志记录大的优先
r
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ZooKeeper是不会出现这种情况的,因为ZooKeeper在每次leader选举完成之后,都会进行数据之间的同步纠正,所以每一个轮次,大家都日志内容都是统一的
而Raft在leader选举完成之后没有这个同步过程,而是靠之后的AppendEntries RPC请求的一致性检查来实现纠正过程,则就会出现上述案例中隔了几个轮次还不统一的现象
leader选举区别:
Raft中的每个server在某个term轮次内只能投一次票,哪个candidate先请求投票谁就可能先获得投票,这样就可能造成split vote,即各个candidate都没有收到过半的投票,Raft通过candidate设置不同的超时时间,来快速解决这个问题,使得先超时的candidate(在其他人还未超时时)优先请求来获得过半投票
ZooKeeper中的每个server,在某个electionEpoch轮次内,可以投多次票,只要遇到更大的票就更新,然后分发新的投票给所有人。这种情况下不存在split vote现象,同时有利于选出含有更新更多的日志的server,但是选举时间理论上相对Raft要花费的多。
如何发现已经完成选举的leader
Raft:比较简单,该server启动后,会收到leader的AppendEntries RPC,这时就会从RPC中获取leader信息,识别到leader,即使该leader是一个老的leader,之后新leader仍然会发送AppendEntries RPC,这时就会接收到新的leader了(因为新leader的term比老leader的term大,所以会更新leader)
ZooKeeper:该server启动后,会向所有的server发送投票通知,这时候就会收到处于LOOKING、FOLLOWING状态的server的投票(这种状态下的投票指向的leader),则该server放弃自己的投票,判断上述投票是否过半,过半则可以确认该投票的内容就是新的leader。
加入过程是否阻塞整个请求?
如果是连续的,则leader中只需要保存每个follower上一次的同步位置,这样在同步的时候就会自动将之前欠缺的数据补上,不会阻塞整个请求过程;Raft的日志是依靠index来实现连续的
如果不是连续的,则在确认follower和leader当前数据的差异的时候,是需要获取leader当前数据的读锁,禁止这个期间对数据的修改。差异确定完成之后,释放读锁,允许leader数据被修改,每一个修改记录都要被保存起来,最后一一应用到新加入的follower中。目前ZooKeeper的日志zxid并不是严格连续的,允许有空洞.
leader选举完成后,超时检测的区别?
Raft:目前只是follower在检测。follower有一个选举时间,在该时间内如果未收到leader的心跳信息,则follower转变成candidate,自增term发起新一轮的投票,leader遇到新的term则自动转变成follower的状态
ZooKeeper:leader和follower都有各自的检测超时方式,leader是检测是否过半follower心跳回复了,follower检测leader是否发送心跳了。一旦leader检测失败,则leader进入LOOKING状态,其他follower过一段时间因收不到leader心跳也会进入LOOKING状态,从而出发新的leader选举。一旦follower检测失败了,则该follower进入LOOKING状态,此时leader和其他follower仍然保持良好,则该follower仍然是去学习上述leader的投票,而不是触发新一轮的leader选举
上一轮次leader的残留数据如何处理?
Raft:对于之前term的过半或未过半复制的日志采取的是保守的策略,全部判定为未提交,只有当当前term的日志过半了,才会顺便将之前term的日志进行提交
ZooKeeper:采取激进的策略,对于所有过半还是未过半的日志都判定为提交,都将其应用到状态机中
Raft的保守策略更多是因为Raft在leader选举完成之后,没有同步更新过程来保持和leader一致(在可以对外处理请求之前的这一同步过程)。而ZooKeeper是有该过程的, 每次leader选举后,都会进行数据的同步纠正
如何阻止上一轮次leader假死
Raft的copycat实现为:每个follower开通一个复制数据的RPC接口,谁都可以连接并调用该接口,所以Raft需要来阻止上一轮次的leader的调用。每一轮次都会有对应的轮次号,用来进行区分,Raft的轮次号就是term,一旦旧leader对follower发送请求,follower会发现当前请求term小于自己的term,则直接忽略掉该请求,自然就解决了旧leader的干扰问题
ZooKeeper:一旦server进入leader选举状态则该follower会关闭与leader之间的连接,所以旧leader就无法发送复制数据的请求到新的follower了,也就无法造成干扰了
请求处理流程区别?
先来看下Raft:
- client连接follower或者leader,如果连接的是follower则,follower会把client的请求(写请求,读请求则自身就可以直接处理)转发到leader
- leader接收到client的请求,将该请求转换成entry,写入到自己的日志中,得到在日志中的index,会将该entry发送给所有的follower(实际上是批量的entries)
- follower接收到leader的AppendEntries RPC请求之后,会将leader传过来的批量entries写入到文件中(通常并没有立即刷新到磁盘),然后向leader回复OK
- leader收到过半的OK回复之后,就认为可以提交了,然后应用到leader自己的状态机中,leader更新commitIndex,应用完毕后回复客户端
- 在下一次leader发给follower的心跳中,会将leader的commitIndex传递给follower,follower发现commitIndex更新了则也将commitIndex之前的日志都进行提交和应用到状态机中
再来看看ZooKeeper:
- client连接follower或者leader,如果连接的是follower则,follower会把client的请求(写请求,读请求则自身就可以直接处理)转发到leader
- leader接收到client的请求,将该请求转换成一个议案,写入到自己的日志中,会将该议案发送给所有的follower(这里只是单个发送)
- follower接收到leader的议案请求之后,会将该议案写入到文件中(通常并没有立即刷新到磁盘),然后向leader回复OK
- leader收到过半的OK回复之后,就认为可以提交了,leader会向所有的follower发送一个提交上述议案的请求,同时leader自己也会提交该议案,应用到自己的状态机中,完毕后回复客户端
- follower在接收到leader传过来的提交议案请求之后,对该议案进行提交,应用到状态机中
日志连续性问题?
- 如果是连续性日志,则leader在分发给各个follower的时候,只需要记录每个follower目前已经同步的index即可,如Raft
- 如果是非连续性日志,如ZooKeeper,则leader需要为每个follower单独保存一个队列,用于存放所有的改动,如ZooKeeper,一旦是队列就引入了一个问题即顺序性问题,即follower在和leader进行同步的时候,需要阻塞leader处理写请求,先将follower和leader之间的差异数据先放入队列,完成之后,解除阻塞,允许leader处理写请求,即允许往该队列中放入新的写请求,从而来保证顺序性
还有在复制和提交的时候:
- 连续性日志可以批量进行
- 非连续性日志则只能一个一个来复制和提交
如何保证顺序?
正常同步过程的顺序
- Raft对请求先转换成entry,复制时,也是按照leader中log的顺序复制给follower的,对entry的提交是按index进行顺序提交的,是可以保证顺序的
- ZooKeeper在提交议案的时候也是按顺序写入各个follower对应在leader中的队列,然后follower必然是按照顺序来接收到议案的,对于议案的过半提交也都是一个个来进行的
异常过程的顺序保证
- Raft:重启之后,由于leader的AppendEntries RPC调用,识别到leader,leader仍然会按照leader的log进行顺序复制,也不用关心在复制期间新的添加的日志,在下一次同步中自动会同步
-
ZooKeeper:重启之后,需要和当前leader数据之间进行差异的确定,同时期间又有新的请求到来,所以需要暂时获取leader数据的读锁,禁止此期间的数据更改,先将差异的数据先放入队列,差异确定完毕之后,还需要将leader中已提交的议案和未提交的议案也全部放入队列
然后再释放读锁,允许leader进行处理写数据的请求,该请求自然就添加在了上述队列的后面,从而保证了队列中的数据是有序的,从而保证发给follower的数据是有序的,follower也是一个个进行确认的,所以对于leader的回复也是有序的
如果是leader挂了之后,重新选举出leader,会不会有乱序的问题?
- Raft:Raft对于之前term的entry被过半复制暂不提交,只有当本term的数据提交了才能将之前term的数据一起提交,也是能保证顺序的
- ZooKeeper:ZooKeeper每次leader选举之后都会进行数据同步,不会有乱序问题
请求处理流程异常的区别
- Raft:会不断重试,并且接口是幂等的
- ZooKeeper:follower会断开与leader之间的连接,重新加入该集群,加入逻辑前面已经说了
目前ZooKeeper和Raft都是过半即可,所以对于分区是容忍的。如5台机器,分区发生后分成2部分,一部分3台,另一部分2台,这2部分之间无法相互通信
其中,含有3台的那部分,仍然可以凑成一个过半,仍然可以对外提供服务,但是它不允许有server再挂了,一旦再挂一台则就全部不可用了。
含有2台的那部分,则无法提供服务,即只要连接的是这2台机器,都无法执行相关请求。
所以ZooKeeper和Raft在一旦分区发生的情况下是是牺牲了高可用来保证一致性,即CAP理论中的CP。但是在没有分区发生的情况下既能保证高可用又能保证一致性,所以更想说的是所谓的CAP二者取其一,并不是说该系统一直保持CA或者CP或者AP,而是一个会变化的过程。在没有分区出现的情况下,既可以保证C又可以保证A,在分区出现的情况下,那就需要从C和A中选择一样。ZooKeeper和Raft则都是选择了C。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Paxos 协议 最令人诟病的一点就是 描述了 可能会存在 多个 Proposer, 而 多个Propser 的协调这点,并没有很好的描述。
而 Zab , Raft 都放弃了多 Proposer , 都设计为 单 Leader , 并由该 Leader 进行协调
-
Zk集群中的client和任意一个Node建立TCP的长连接,完成所有的交互动作,而Raft第一次随机的获取到一个节点,然后找到Leader后,后续直接和leader交互
-
Zk中的读请求,直接由连接的Node处理,不需要和leader汇报,也就是Consul中的stale模式。这种模式可能导致读取到的数据是过时的,但是可以保证一定是半数节点之前确认过的数据
-
为了避免Follower的数据过时,Zk有sync()方法,可以保证读取到最新的数据。可以调用sync()之后,再查询,确保所有的数据一致后再返回结果
角色Zk引入了 Observer的角色来提升性能,既可以大幅提升读取的性能,也可以不影响写的速度和选举的速度,同时一定程度上增加了容错的能力
集群之间投票消息是单向、网状的,类似于广播,比如A广播A投票给自己,广播出去,然后B接收到A的这个消息之后,会PK A的数据,如果B更适合当leader(数据更新或者myid更大),B会归档A的这个投票,但是不会更新自己的数据,也不会广播任何消息。除非发现A的数据比B当前存储的数据更适合当leader,就更新自己的数据,且广播自己的最新的投票消息。
而Raft集群之间的所有消息都是双向的,发起一个RPC,会有个回复结果。比如A向B发起投票,B要么反馈投票成功,要么反馈投票不成功。
ZK集群中,因为引入了myid的概念,系统倾向让数据最新和myid最大的节点当leader,所以即使有半数节点都投票给同一个Node当leader,这个Node也不一定能成为leader,需要等待200ms,看是不是有更适合的leader产生,当然如果可能因为网络原因 数据最新 myid最大的节点也不一定能当选为leader。但是在Raft系统中,只要某个candidate发现自己投票过半了,就一定能成为leader
ZK集群中,每一轮的选举一定会选出一个leader,因为每个节点允许多次投票,而且会广播自己的最新投票结果。而Raft系统可能涉及选票瓜分,需要重新发起一轮选举才能选出leader,是通过选举超时时间的随机来降低选票瓜分的概率。所以ZK的选举理论上一般需要花费更多的时间,但是只需要一轮。Raft每一轮选举的时间是大致固定的,但是不一定一轮就能选出leader。
-
ZK集群中,成为公认的leader条件更苛刻,raft模式下,只要新leader发一个命令为空的Log出来,大家就会认同这个节点为leader,但是在ZK集群中,追随leader的2种条件都很苛刻
- 要么recvset中半数节点的选举following投票给A,才会认可A为自己的leader
- 要么outofelection中半数节点都认可A为leader,自己才会认可A为自己的leader
Raft系统中的事务消息是通过双向的RPC来完成的,而Zab中,则是单向的,一来一回两个消息来完成的。明显Zab的性能更加快,不需要浪费leader一个线程去等待follower完成业务操作。
Zab中leader发送一个Proposal消息给follower,发送完成。当follower完成proposal过程时,再发送一个消息ACK到leader,发送完成。leader统计ACK数量过半时,触发广播commit