Raft协议理解

如果你不知道某个设计存在什么问题,可以看一下是否有人在这个设计上做了一些有优化
 本文分为如下部分

  • 各状态更新
  • 日志复制
  • 领导选举
  • 安全性理解
  • 工程实现及优化
  • 对比其他协议,自己的理解

论文第10章,讨论了Raft与paxos(多了领导者), raft与zab/viewstamped replication(日志只会从leader到follower单向流通) .

各状态更新

基于raft论文和etcd raft实现

  1. Term在Vote消息到达Follower,follower投票后,就会持久化

日志复制

日志匹配原则:如果两个日志在相同索引位置的entry的任期号相同,那么这两个日志从头到这个索引位置之前完全相同。
日志匹配原则可以解释为如下两条

  1. 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令。

这基于一个事实,领导者在一个任期中,在一个索引位置最多只会有一条entry

  1. 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也全部相同。

有复制日志RPC中一致性检查保证,AppendEntriesRpc 参数携带了prevLogTerm和prevLogIndex,follower收到请求时,如果在自己的日志中找不到相同索引位置和任期号的entry就拒绝接收。

日志复制过程

  1. 领导者维护了nextIndex[],记录要发给每个follower的索引位置;领导者刚起来时,nextIndex赋值为最后一条entry的index + 1;
  2. follower收到AppendEntry请求时检查发现prevLogIndex和prevLogTerm与自己不一致,会拒绝;leader收到拒绝响应后,会减小nextIndex,并重新发送AppendEntry;
  3. 重复上面两步,最终会找到follower与leader entry匹配的位置,并从这个地方leader将数据复制给follower,最终达到follower与leader日志一致。

领导选举

简单说,领导选举会日志完整性最高的节点作为新的leader

Raft 通过比较两份日志中最后一条日志条目的索引值和任期号定义谁的日志比较新。如果两份日志最后的条目的任期号不同,那么任期号大的日志更加新。如果两份日志最后的条目任期号相同,那么日志比较长的那个就更加新
Raft determins which of two logs is more up-to-date by comparing the index and term of the last entry in logs. if the logs have last entries with different term, then the log with later term is more up-to-date. if logs end with same term, then whichever log is longer is more up-to-date.

怎么理解日志完整性?
论文“安全性->选举约束”里,明确规定了,新选出的leader必须包含之前所有已提交的日志,看如下两个场景:

有6个节点a ~ f

  1. 开始6个节点日志都是一致的,且都commit了
  2. leader a收到新的写,记logX, 并复制logX到其中两个follower b , c
  3. leader挂掉,开始新的一轮领导选举,会出现两种情形:b/c当选 or d/e/f当选,都可以 ???

有5个节点a ~ e

同样上述场景,只会有b/c当选,d/e无法当选

如果d 尝试参选,发送自己的lastLogIndex和lastLogTerm,因为b/c的lastLogIndex都比d的大,所以不会投票给d

由这两个场景对比可知,日志完整性高,讲究的是拥有前面所有已提交的entry,并不一定要有最新的entry,最新的entry可能是新写的,只有少部分节点有

安全性分析

选举约束

  1. 在任何基于领导人的一致性算法中,领导人都必须存储所有已经提交的日志条目。在某些一致性算法中,例如 Viewstamped Replication,某个节点即使是一开始并没有包含所有已经提交的日志条目,它也能被选为领导者。这些算法都包含一些额外的机制来识别丢失的日志条目并把他们传送给新的领导人,要么是在选举阶段要么在之后很快进行。不幸的是,这种方法会导致相当大的额外的机制和复杂性。Raft 使用了一种更加简单的方法,它可以保证所有之前的任期号中已经提交的日志条目在选举的时候都会出现在新的领导人中,不需要传送这些日志条目给领导人。这意味着日志条目的传送是单向的,只从领导人传给跟随者,并且领导人从不会覆盖自身本地日志中已经存在的条目。

  2. Raft 使用投票的方式来阻止一个候选人赢得选举除非这个候选人包含了所有已经提交的日志条目。候选人为了赢得选举必须联系集群中的大部分节点,这意味着每一个已经提交的日志条目在这些服务器节点中肯定存在于至少一个节点上。如果候选人的日志至少和大多数的服务器节点一样新(这个新的定义会在下面讨论),那么他一定持有了所有已经提交的日志条目。请求投票 RPC 实现了这样的限制:RPC 中包含了候选人的日志信息,然后投票人会拒绝掉那些日志没有自己新的投票请求。

  3. Raft 通过比较两份日志中最后一条日志条目的索引值和任期号定义谁的日志比较新。如果两份日志最后的条目的任期号不同,那么任期号大的日志更加新。如果两份日志最后的条目任期号相同,那么日志比较长的那个就更加新。

提交前任的日志条目

这一点还是充满疑问的,等看一些开源实现之后再写。
针对“幽灵复现”问题

具体是怎么做的呢?

升为leader后,首先写一个nop entry到本地,这样在发送append entry给follower时,前一个任期的log也跟着被提交了
如果升为leader后,不写nop entry,而是将前一个任期的log复制到follower,这样即使复制到多数,也可能被复写掉。
前提:
If RPC request or response contains term T > currentTerm: set currentTerm = T, convert to follower
这个Term每次更新后都会持久化,所以能升为主,说明大多数的Term已经持久化为新的了
Raft never commits log entries from previous terms by counting replicas. Only log entries from the leader’s current term are committed by counting replicas
etcd-raft实现中 判断是否要更新committed时,判断下这个entry的term是否为当前term,如果是才会提交

  1. 升为leader后啥也不做,不发起复制

如果此时app来了push,那和nop entry一个效果;如果很久没有push,那么复制组里面的各个日志,会长时间不一致

  1. 升为leader后按照自己最大的日志往follower复制

那么把这部分复制到大多数后,committed/applied会增加,但最后可能committed的数据被覆盖

  1. 升为leader后写nop entry

也可能出现nop entry还没写下去,主就挂了的场景

一个场景

一开始,三个节点log是一致的,lastLogTerm=2, lastLogIndex=10
Leader挂掉,A节点升为Leader,写了Term=3, Index=11的entry,未append entry到其他节点,又挂了
B节点升Leader,也写了Term=3, index=11的entry,往节点A复制时会发生啥?

这个append请求,携带的prevTerm=2, prevIndex=10,在A上找到这条日志后,将后面的日志都删掉,然后开始复制b上的日志。

preVote

follower变成candidate时,会将TermID++,如果这个candidate一直无法获得多数派投票,又没有转变为follower会导致TermID一直增加。
为解决这个问题,提出preVote,candidate需确认自己可以获得多数派响应时,才将TermID++

脑裂

Log Read

如果把读当作更新来处理,走一遍日志复制流程,这个读一定能读到之前的更新。

ReadIndex

记录当前的 commit index,称为 ReadIndex
向 Follower 发起一次心跳,如果大多数节点回复了,那就能确定现在仍然是 Leader
等待状态机至少应用到 ReadIndex 记录的 Log
执行读请求,将结果返回给 Client

第 3 点中的“至少”是关键要求,它表明状态机应用到 ReadIndex 之后的状态都能使这个请求满足线性一致,不管过了多久,也不管 Leader 有没有飘走。为什么在 ReadIndex 只有就满足了线性一致性呢?之前 LogRead 的读发生点是 commit index,这个点能使 LogRead 满足线性一致,那显然发生这个点之后的 ReadIndex 也能满足。

Lease Read原理

  1. follower收到leader的heartbead, append消息时,会重新计算选举超时时间。
  2. leader周期(elect timeount时长)checkQuarom,如果不满足大多数,则变成follower
  3. 很多文章中提到clock drift问题,没有明白,不过如果leader的时间走得慢,follower时间走得快,lease read也还是有问题

更多阅读 线性一致性和 Raft

raft有哪些缺点?/可以做哪些优化?

心跳包优化

multiRaft讲的是raft group内交换心跳可以用node直接交换心跳替代,公用一条tcp链路。

log复制 pipline

Leader跟其他节点之间的Log同步是串行batch的方式,每个batch发送过程中之后到来的请求需要等待batch同步完成之后才能继续发送,这样会导致较长的延迟。这个可以通过Leader跟其他节点之间的PipeLine复制来改进,有效降低更新的延迟

可以按顺序一次发送多个请求,在follower端判断如果不连续,就拒绝,leader从丢掉的位置重发:leader发送了1,2,3,follow收到1, 3,其中3会被拒绝,此时leader需要继续发送2, 3。思路很想tcp的窗口。

group commit (batch思想无处不在,可以进行举例)

将client发送过来的多个请求打包成一个batch,作为一个entry去record、复制。

Leader写本地log和复制log并行

基于日志复制的多副本存储系统一致性分析

数据一致性分析一文中,我们分析了保证数据一致性具备的三个要素,结合本文raft协议理解,我们综合日志复制多副本存储一致性问题。

日志一致性

如果各副本上日志顺序一致,根据日志回放,最终各副本日志会一致。
日志一致性由日志复制的逻辑保证

例如raft协议中,日志只会由leader复制到follower,另外,针对AppendEntry会检查prevLogTerm和prevLogIndex,这就保证了follower是完全“顺从”leader的日志,保证了强领导者地位。
数据一致性分析中,可以看到备副本会检查<TermID, ReqID>是否连续,由此来保证与主副本日志顺序一致。

leader必须包含所有之前提交的entry

这个特性有领导选举保证,为什么呢?

  1. 如果没有切主,这个肯定可以保证,日志都是追加的(基于事实,索引位置和任期号都是自增的,leader只会在同一个索引位置上写一次数据,)
  2. 如果切主了,领导选举时,会选出日志完整性最高的follower升主

raft协议领导选举自解释
数据一致性分析中,选主有mds完成,而mds知道备副本正常与否,是否具有升主资格,这就保证了mds只会选出与老主数据一致的备升主。

相关阅读

有各种协议理解

posted @ 2020-08-02 10:43  holidays  阅读(495)  评论(0编辑  收藏  举报