什么是Replicated state machines(复制状态机):
一致性算法之所以可以保证在有节点挂掉时也能够继续服务, 就是因为有Replicated state machines的存在。 在分布式系统中, 有两种方式来实现这个复制状态机
- 用一个专门节点负责记录整个集群的元信息, 如HDFS, GFS
- 每个节点都会记录replicated log,确保每个节点的log最终都包含了相同的请求, 并且是同样的顺序
一致性算法一般有一下特点:
- 保证安全(不会返回错误数据)
- 只有半数以上的节点存活就可以继续提供服务
- 不依赖时钟来提供一致性, 最多会造成不可用
- 通常只要半数以上节点完成请求就算成功, 小部分慢节点不会影响整个系统的性能
为啥不用Paxos?
- 不清晰, 难以理解
- 不好实现, 只提供了single-decree Paxos的细节, 没有给出multi-Paxos的细节
Raft一致性算法
Raft算法首先会选举一个leader, 然后又leader来管理replicated log。 leader会从client处接收请求, 然后转发给别的节点, 并且告诉这些节点什么时候可以把这些请求应用在状态机上(落地)。 当一个leader挂了的时候, 会马上选举出一个新的leader。 由上可知, Raft将一致性问题拆分成了3个独立的子问题:
- Leader选举
- 日志的复制(Log replication)
- Safty: 不能有错误数据
Raft的实践基础
一个Raft集群会有多个节点, 一般至少5个, 这样可以忍受2个节点fail。 这些节点在任何时间点都会是以下3个状态之一: leader, follower, candidate。
Raft将时间分为了多个term, 选举会产生一个新的term, 每个term的时长不定,但是每个term都至多有一个leader
不同的节点会在不同时间感知到变化,有些节点甚至在一个term完结时都没有感知到。 Terms是一个逻辑时钟, 每个节点都会存储当前term, 并且会一直跟别的节点交换信息, 一旦发现自己的term不是最新的, 就会更新term, 如果一个candidate或者leader发现他的term过时了,就会马上回退成follower。 如果一个节点收到一个过期term的请求, 那么他会拒绝这个请求
Raft节点间通过RPC请求进行通信, 基本的算法只要求两种RPC请求类型。 一种是RequestVote RPCs, 由candidate在选举时发出。 一种是AppendEntries RPCs, 由leader发出, 用来复制log和发送心跳-
leader选举
Raft用心跳来触发leader选举, 当节点启动时, 都是follower的状态。只要节点能够收到来自leader或者candidate的RPC, 节点就会保持follower状态。
Leader会定期发送RPC给所有的follower来维持他的leader地位, 但是一旦有一个节点在一段时间没有收到心跳, 那么节点就会认为没有存活的leader并且触发新一次的选举。
选举开始时,一个follower会给当前term加1并且转换成candidate状态。 然后这个节点会给自己投票并且给所有其他节点发送RequestVote RPC。
Candidate会保持这个状态直到以下3个事件中的一个发生:- 这个节点赢得选举
如果这个节点赢得了半数以上的vote就会成为leader,每个节点会按照first-come-first-served的原则进行投票,并且一个term中只能投给一个节点, 这样就保证了一个term最多有一个节点赢得半数以上的vote。
当一个节点赢得选举, 他会成为leader, 并且给所有节点发送这个信息, 这样所有节点都会回退成follower。 - 另外一个节点赢得选举
在等待vote时, 如果这个candidate收到了别的节点请求vote的RPC, 那么他会检查是否这个节点的term大于等于自己的term, 如果是那么这个candidate就会回退成follower。 如果不是就会reject这个RPC。 - 一个选举周期结束并且没有leader选出来
这种情况是由于可能有很多follower都成为了candidate, 那么vote就会很分散, 最后没有一个节点拿到半数以上的vote。最后这个term超时并且进入下个term继续选举。
为了防止这种情况一直重复,每个节点的election 超时时间是(150~300ms)的随机数, 这样在一个term就会有一个节点很快超时进入下一个term, 然后就能在别的节点timeout之前赢得选举
- 这个节点赢得选举
- Log replication
当leader选出来后,他就可以正式给客户端提供服务。 leader会将请求以entry的形式追加到自己的log中,并且将这个entry以AppendEntries RPC并发发送给所有的follower节点。 当这个entry成功的复制后(怎么算成功之后会谈到), leader会将这个entry 应用到自己的state machine并且给client返回成功。 如果复制失败, leader会一直重试AppendEntries RPC直到所有log entry都被成功复制。
Logs 组织的形式如图所示。 当leader收到一个Log entry时, 每个log entry都会存储一个带有term number的state machine命令。 Term number是用来探测log之间的不一致性并且保证Figure3的一些特性。 每个log也还带有他们在log里的索引数字。
当leader认为这个entry是可以成功应用到状态机上, 那么这个entry就被成为commited。Raft保证所有commited entry最终都会被应用到所有的节点上。
当一个entry被成功复制到半数以上的节点后, 这个log就可以认为成功写入了。并且会将leader之前的写入也视为commit。
设计Raft日志机制不仅简化了系统的行为, 也保证了正确性。 保证了Figure3的以下特性
1. 当不同log中的两个entry有相同的term和index, 那么这两个entry就是相同的
2. 当不同log中的两个entry有相同的term和index, 那么这两个entry之前的所有entry也都是相同的
当leader发现follower的log跟自己的不同时, 他会针对每个follower维护一个nextIndex。 这个nextIndex就是Leader下次会发第几个entry给这个follower。 而follower也会拒绝这个append entry的rpc。
Safety
- 如何保证数据不丢失和数据的正确性是分布式数据库最重要的两点, Raft协议也设计了相应的算法来保证这两点。
- 所有的entry都只能从leader流向follower,而要成为leader必须有所有的committed entries。 如果有server发现candidate的数据没有自己更update-to-date, 那么他会拒绝这个vote request。 如果两个server的log term不同, 那么term number更大的更update-to-date, 如果term相同, 那么index更大的更update-to-date。
-
仅有第一点的保证我们仍旧可能会遇到问题。 如上图所示, a) s1成为leader,部分拷贝了index 2; b) s1挂了, s5成为leader, 写入index 3; c)s5又挂了, s1再次成为leader并且继续将index 2拷贝到其余follower; d) s1又一次挂了, s5成为leader(获得2,3,4的选票), 将自己所有的s3拷贝到别的机器并且覆盖掉了index 2; e)如果c阶段没有挂那么日志会被拷贝到s2,s3, 之后s5就不会被选为leader。
上图演示了写入数据可能会被覆盖的问题, 那么如何避免这个问题。 Raft规定绝不通过计算副本数的方式commit 之前term的log entries。 只有当前term的log enties使用计算副本的方式来提交。 对于之前的term来说,当一个log通过这个方式提交成功了,那么他之前的所有log都算commit成功了。如何证明这个结论的正确性, 论文用反证法进行了论证:
如Figure 4所示, 如果term T commit的数据在之后的term U丢失了, 那么
1. 这个log一定不在Leader-U的log中(因为leader不会覆盖旧数据)
2. Leader-T把这个日志复制给了大多数节点并且Leader-U收到了大多数节点的投票, 所以至少有一个节点是既收到了这个entry并且又给Leader-U投票了
3. 那么这个投票的节点也一定收到了所有Leader-T的commited entries
4. 因为这个投票节点把票投给了U, 那么说明Leader-U也至少有所有这个节点的entries
由此可知, 如果投票节点和Leader-U 共享了之前的log, 那么Leader-U肯定会有所有投票节点的entry。 另外, Leader-U的last-log-term必须比T大, 而投票节点的term至少是T。之前term的leader在复制entry给Leader-U时也必须包含了之前的这个entry。 投票节点和之前term的leader都会有这个entry, 所以Leader-U也不会没有这个entry。可下结论所有大于T的term的leader绝对包含了所有term T commited的entries。