raft协议是multi paxos协议的实现.Etcd、Consu都使用了raft
1.角色
raft协议中包含这几种角色
领导者:带头大哥1.提出提议,但是不需要确认,因为我是大哥;2.复制日志,数据以大哥为准,3,领导者会定时发送心跳,确定自己的位置.告诉小弟老实呆着,一旦心跳超时,小弟就会重新选举大哥.
跟随者:只要大哥发送心跳,我就老实的同步日志.一旦没有心跳,我就变成候选人,开始发起投票,争取做大哥
候选人:心跳超时后,变成候选人,这是个中间状态.
2.选举
2.1选举时机:
1.心跳超时后,变成候选人,开始进行选举.为了避免节点全都一起选举,不同节点会有随机心跳超时时间.
2.例如机器刚启动时,大家都是follower,心跳超时后,就开始选举
3.优化.为了避免瓜分选票,心跳超时时间随机,当选举超时后,会进入第二轮选举,这个间隔时间也是随机的
2.2 选举原则:
1.每个节点,对每个任期(trem)只能投出一张票.与zab不同,zab是不断修改自己的选票,然后每个节点都有个投票箱,自己计算
2.收到选举请求后,如果该请求的事务日志编号小于自己的机器,那么就不会投票. 要当leader的机器,最起码掌握的东西得比我多吧(log)
3.收到选举请求后,如果该选举编号(term)小于自己的term,那么就不会投票. 我们都在老xi时代了,你还在选举第二任?
4.当一台机器收到多数投票,代表选举成功.
注意一个点.当集群中一个节点出现网络分区.那么他会去选举,增加自己的term.但是还是会超时,因此一直去增加term,发起更多轮的选举.一旦.这个节点网络恢复.那么其他节点就会收到选举请求,发下term大于自己,那么会触发重新选举. 由于事务id小,当然不会成为leader,但是还是影响了集群.
因此.有一个preVote阶段,
在PreVote算法中,Candidate首先要确认自己能赢得集群中大多数节点的投票,这样才会把自己的term增加,然后发起真正的投票。其他投票节点同意发起选举的条件是(同时满足下面两个条件):
- 没有收到有效领导的心跳,至少有一次选举超时。
- Candidate的日志足够新(Term更大,或者Term相同raft index更大)。
2.3 rpc消息:
raft中有两个rpc:
1.选举rpc:是由候选人在选举期间发起,通知各节点进行投票
2.日志复制rpc:是由领导者发起,用来复制日志和提供心跳消息.
2.4 term
1.如果一个机器收到,任期编号大的机器发来的心跳,那么他就应该知道自己已经out了.要赶紧同步数据.化为follower.
2.当节点收到任期编号比我小的怎么办?丢弃.
2.5 总结:
与multiPaxos相比
1.通过日志编号确定数据是否最新,因此要求数据是连续的
当确认leader之后,leader会同步数据给follower.
3.日志复制:
流程
正常情况:
- client向leader发出一个添加请求
- leader写入appendEntryLog.并标记为uncommit状态.
- 调用appendEntryRpc,拷贝日志项到follower.
- follower接收到日志项,并append到本机.返回给leader success
- leader收到success后,将日志置为committed状态.并apply到状态机.返回给client success.
异常流程:
当拷贝到follower,follower没有响应成功时,有可能是没有拷贝过去,也有可能ack丢失了. leader会不断重试,直到满足过半. 当然这时候,client可能会超时.这条日志可能最后会丢失,也可能会committed,客户端需要实现幂等
先说问题:
1.leader把数据复制到follower,follower大部分返回成功后,leader会apply此条数据到状态机.并返回给client成功的响应.
注意这里:状态机是对于上层来说的,比如你写成功后,再来读取数据,就是从状态机的数据中读,如果你返回给用户成功,但是状态机数据还是旧的,那么就不对了.
那我能不能在leader复制后,就update状态机呢?
不能,如果直接update,返回成功给用户,但是此时leader挂了.咋整.所以需要等大部分都提交,才返回成功.因为大部分提交后,即使此时leader挂了.重新选举时,根据raft协议.只有提议id大的才可以当leader,
就可以保证,之后当选的leader会有这条数据.因此这里也可以知道.选举时比较的日志id,是复制的日志id,而不是commitedId.
2.raft将两阶段提交,优化为1阶段提交.
当一条请求发送给leader时,leader会写一条日志(uncommit),之后复制给follower(uncommit),当过半后,leader会变为committed,那么follower啥时候变为committed,并apply到状态机呢???
如果不是实时的,岂不是在读取的时候,读不出来??
a:通过AppendEntriesRPC同步数据,细节前面已经说了.此时就会把数据应用到状态机.大概逻辑就是:心跳/同步数据 rpc,会携带最新日志id,如果少的话,那么需要进行同步.appendEntryRpc互携带最新日志以及前一条日志(preEntryTerm,preEntryLogId),就可以回溯到 到底从哪条日志不一样了,然后再同步给follower.
b:注意:一致性是说 对于client,发出的读请求,可以获取到集群最新的数据,而不是说,集群每个节点数据都是一致的!!.这个是很容易错误理解的!! 例如etcd,早期是通过读写都走leader节点来保证一致性.3.2版本后,通过readIndex来保证.readIndex是什么,可以参考etcd源码分析系列;
所以一般来讲, leader append log(uncommit)--->copy to followers-->leader apply log &commit the log--->下次心跳,同步数据
4.raft算法的缺陷
我们可以在网上看到很多比较 Paxos 和 Raft 的文章,它们都会提到在复制效率上 Raft 会差一些,主要原因就是 Raft 必须“顺序投票”,不允许日志中出现空洞。
- Leader 收到客户端的请求。
- Leader 将请求内容(即 Log Entry)追加(Append)到本地的 Log。
- Leader 将 Log Entry 发送给其他的 Follower。
- Leader 等待 Follower 的结果,如果大多数节点提交了这个 Log,那么这个 Log Entry 就是 Committed Entry,Leader 就可以将它应用(Apply)到本地的状态机。
- Leader 返回客户端提交成功。
- Leader 继续处理下一次请求。
以上是单个事务的运行情况。那么,当多事务并行操作时,又是什么样子的呢?我画了张图来演示这个过程。我们设定这个 Raft 组由 5 个节点组成,T1 到 T5 是先后发生的 5 个事务操作,被发送到这个 Raft 组。
事务 T1 的操作是将 X 置为 1,5 个节点都 Append 成功,Leader 节点 Apply 到本地状态机,并返回客户端提交成功。
事务 T2 执行时,虽然有一个 Follower 没有响应,但仍然得到了大多数节点的成功响应,所以也返回客户端提交成功。
现在,轮到 T3 事务执行,没有得到超过半数的响应,这时 Leader 必须等待一个明确的失败信号,比如通讯超时,才能结束这次操作。因为有顺序投票的规则,T3 会阻塞后续事务的进行。
T4 事务被阻塞是合理的,因为它和 T3 操作的是同一个数据项,但是 T5 要操作的数据项与 T3 无关,也被阻塞,显然这不是最优的并发控制策略。
同样的情况也会发生在 Follower 节点上,第一个 Follower 节点可能由于网络原因没有收到 T2 事务的日志,即使它先收到 T3 的日志,也不会执行 Append 操作,因为这样会使日志出现空洞。
Raft 的顺序投票是一种设计上的权衡,虽然性能有些影响,但是节点间日志比对会非常简单。在两个节点上,只要找到一条日志是一致的,那么在这条日志之前的所有日志就都是一致的。这使得选举出的 Leader 与 Follower 同步数据非常便捷,开放 Follower 读操作也更加容易。要知道,我说的可是保证一致性的 Follower 读操作,它可以有效分流读操作的访问压力。
性能优化方法
缺点:顺序投票,只能串行apply,因此高并发场景下性能差。
TiDB:
1.批操作(Batch)。Leader 缓存多个客户端请求,然后将这一批日志批量发送给 Follower。Batch 的好处是减少的通讯成本。牺牲了写操作的耗时
2.流水线(Pipeline)。Leader 本地增加一个变量(称为 NextIndex),每次发送一个 Batch 后,更新 NextIndex 记录下一个 Batch 的位置,然后不等待 Follower 返回,马上发送下一个 Batch。如果网络出现问题,Leader 重新调整 NextIndex,再次发送 Batch。当然,这个优化策略的前提是网络基本稳定。
3.并行追加日志(Append Log Parallelly)。Leader 将 Batch 发送给 Follower 的同时,并发执行本地的 Append 操作。因为 Append 是磁盘操作,开销相对较大,而标准流程中 Follower 与 Leader 的 Append 是先后执行的,当然耗时更长。改为并行就可以减少部分开销。当然,这时 Committed Entry 的判断规则也要调整。在并行操作下,即使 Leader 没有 Append 成功,只要有半数以上的 Follower 节点 Append 成功,那就依然可以视为一个 Committed Entry,Entry 可以被 Apply。
4.异步应用日志(Asynchronous Apply)。Apply 并不是提交成功的必要条件,任何处于 Committed 状态的 Log Entry 都确保是不会丢失的。Apply 仅仅是为了保证状态能够在下次被正确地读取到,但多数情况下,提交的数据不会马上就被读取。因此,Apply 是可以转为异步执行的,同时读操作配合改造. 这里是牺牲了读取的一致性了
其实,Raft 算法的这四项优化并不是 TiDB 独有的,CockroachDB 和一些 Raft 库也做了类似的优化。比如,SOFA-JRaft 也实现了 Batch 和 Pipeline 优化。
参考:
https://zhuanlan.zhihu.com/p/27207160
https://raft.github.io/raft.pdf
https://time.geekbang.org/column/article/277028
http://blog.sina.com.cn/s/blog_3fc85e260102wr0c.html