“本篇文章内容来自 In Search of an Understandable Consensus Algorithm (Raft) 的英文论文,算是一篇读书笔记。文档中保留了和论文基本一致的顺序和章节结构,部分英文名词做了标注解释和引用,方便对照阅读理解。”
状态机 Raft 基本认识 Leader 选举 日志复制 介绍分布式系统中日志的复制机制 安全性保障 Leader 选举和日志复制组成了分布式协议的核心部分,但是可靠的一致性保障还需要额外的机制来保障 配置变更 分布式系统中节点数量等配置信息不是一成不变的,配置的变更也需要满足分布式系统的要求
论文下载:https://raft.github.io/raft.pdf
演示工具:https://raft.github.io/
01
—
状态机

处理过程:
一致性处理模块接收到客户端的请求(也就是操作命令),会将这些操作命令记录在该服务器节点的日志中;
该节点会与集群中其他节点通信,将新增的操作命令同步到集群中的其他节点(即便是有部分服务器节点宕机);
当这次的操作命令同步完成,各个服务器节点state machine就可以自行按序处理日志;
最后便可以响应客户端的这次请求。
安全可靠性,如在 non-Byzantine 发生:网络延迟、网络分区、丢包等情况下的安全可靠;
可用性,只要大多数的服务器节点可以正常工作,就需要保证集群可用;
去“时间”依赖,算法不能依赖自然时间系统来保证一致性;
低延时,不能因为集群中存在小部分慢节点,而影响集群响应速度;
脚注:
non-Byzantine: 把出现故障(crash 或 fail-stop,即不响应)但不会伪造信息的情况称为“非拜占庭错误”( non-byzantine fault)或“故障错误”( Crash Fault); Byzantine: 伪造信息恶意响应的情况称为“拜占庭错误”( Byzantine Fault),对应节点为拜占庭节点。
02
—
Raft 基本认识
leader 不需要和其他节点协商log entries的处理方式;
数据流始终是从 leader 到 follower;
脚注:
log entries:可以抽象理解成客户端发起的请求中包含的操作命令,如果是存储系统,则可以是对数据的写操作。 command:log entry 中包含的,可以应用于 state machine 中的操作命令,如存储系统中,将某个数值更新等。 log:日志,也即是节点用于存储 log entries 的地方。
节点状态
Leader:处理所有的客户端请求,即便是部分请求到达了 Follower,Follower 也不会处理而是将请求转发到 Leader 来处理;
Candidate:这类节点是 Leader 的候选节点,新的 Leader 会从这一类节点中选出;
Follower:这类节点不处理任何请求,只会响应来自 Leader 和 Candidate 的请求,是被动型状态;

脚注:
election timeout:follower 达到该时间后,会推断当前集群没有可用 leader,自身状态变为 candidate,并主动发起下一轮 leader 的选举。 心跳请求:不带 log entries 的空 AppendEntries RPC。
Term

State-表注:
几种不同状态节点中的属性信息
表注:
Raft 核心特性,协议需要保证它们的成立。
03
—
Leader Election
递增自己的 term (current term);
将自己状态变为 Candidate;
给自己投票成为 Leader ,并同时向集群中其他节点发起 Vote RPC 请求来拉票;
而该 "Follower" 节点发起的这一轮的 Leader 选举,无外乎如下三种结果:
脚注:
FCFS: first-come-first-served,也就是在 Leader 选举时期,集群中节点会给它收到的第一个满足条件的 Vote RPC 投票。 Election Safety Property:会在后面的小结具体解释。
2. 集群中其他节点赢得选举,成为 Leader;
如果“新Leader”的 term 不小于该节点的 current term。那么,该节点就会承认并接受“新Leader”,自己再次变回到 Follower 态。
如果“新Leader”的 term 小于该节点的 current term。那么,该节点会忽略这次请求,继续选举计票。
3. 无节点胜出,没有产生新的 Leader;
04
—
Log Replication
基本认识
一次完整的客户端请求处理大致过程:
leader 收到客户端请求(通常包含 command);
leader 会为这个操作命令,创建 log entry,并加入到自己的 log 中;
leader 通过 AppendEntries RPC 来并行的复制这些 log entry 到集群中其他节点;
当这些 log entry 被安全可靠的(Raft 中叫做 safely,集群中多数节点)复制,leader 就会将 log entry 应用到自己的 state machine 中,最后响应客户端。
集群中 follower 节点出现 crash、网络丢包、运行缓慢,leader 都是通过 AppendEntries RPC 来保持和其他节点的状态同步,直至 follower 最终和 leader 保持一致。
Log index

Committed entry
Raft Property: Log Matching Property
如果两份不同的 logs 中存在相同 term 和 log index 的两个 entries,那么这两个 entries 一定包含相同的 command;
上述的这两份 logs 中,该 log entry 之前的所有 log entries 也一定是完全一致的;
在初始状态,logs 为空的情况下,是满足 Log Matching 特性的;
当 logs 在追加扩充时,只要 AppendEntries RPC 成功,Log Matching 特性就会一直满足;
Inconsistency handle

nextIndex:
新选举出来的 leader 会将该 nextIndex 初始化为自身上一个 log index + 1 的位置;
如果某个 follower 与 leader 的 log 不一致,在 AppendEntries 请求到达 follower 时,一致性校验失败 follower 会拒绝该请求;
leader 发现请求被拒绝后,会对对应的 nextIndex 做递减操作,然后重新发起 AppendEntries 请求。
05
—
Safety
Election restriction
restriction-1: log entries only flow in one direction, from leader to followers
Raft 是如何保证新 leader 一定包含所有已提交的日志的呢?
通过 "Leader election" 小节阐述的 leader 选举过程,我们知道,candidate 能够胜出,必须获得集群中多数节点的投票,我们可以将这个多数节点记作集合:C<lead>;
通过 "Log replication" 小节阐述的日志提交过程,我们同样知道,log entry 要变为 committed,必须通知到集群中的多数节点,也就是集群中的多数节点记录着当前最新的 committed entry 的 log index,我们可以将这个多数节点记作集合:C<committed>;
两个集合取交集得到 C<v>:
C<lead> ∩ C<committed> = C<v>
Replication restriction
restriction-2: never commit log entries from previous terms by counting replicas
如下图:S1~S5 是集群中的5个节点,a~d 是顺序的5个不同 term。
(a):S1 为 leader,并且正在提交/复制 entry<term-2, index-2>;
(b):S1 发生崩溃,S5 获得 S3、S4、S5 的投票成为 term-3 的 leader,但是收到了一条不同于 term-2 时的 entry<term-3, index-2>;
(c):S5 发生崩溃,S1 已经重启并在此成为 term-4 的 leader,S1 继续提交/复制 entry<term-2, index-2>,并存储在了多数节点中,但还未变为 committed 状态;
(d):S1 再次崩溃,S5 再次变为 leader,并开始继续复制 entry<term-3, index-2>,这时已经复制到集群中多数节点上的 entry<term-2, index-2> 就会被覆盖。
虽然 entry<term-2, index-2> 并没有变为提交态,但是由于这个 log entry 已经在多数节点上上存储,只是提交状态未被记录,本质上是已提交态,在(d)中发生的覆盖行为是不可接受的,实质上破坏了前面一致性的约定。

根据上面的约束条件,我们知道问题其实发生在 (c) 。我们重新梳理上图的场景:a、b、e
(a) (b) :同上不变;
(e):S5 发生崩溃,S1 已经重启并在此成为 term-4 的 leader,这时 S1 不需要继续提交entry<term-2, index-2>,而是只提交/复制entry<term-4, index-3>,如果此时该 entry 复制到了多数节点上,后面 S5 没有条件被选举为 leader;(反之,如果该 entry 没有成功同步到多数节点,又有了新的leader, entry<term-2, index-2> 和entry<term-4, index-3> 都可能被覆盖,是符合预期的 )
Raft Property: Leader Completeness Property
leader-T:term-T 的 leader;
leader-U:term-U 的 leader;
U > T;
term-T 中有一个已提交的 log entry(entry-T),而它不存在与 term-U;

根据上面的假设场景,我们做出如下推断:
在 term-U 选举阶段,entry-T 一定不会出现在 leader-U 的 log 中(因为 leader 不允许删除或者覆盖 entries)。
如上图,leader-T 将 entry-T复制到了集群中的多数节点。而 leader-U 获得了集群中多数节点的投票成为 leader。这样至少就会有一个节点,如:S3,即拥有提交态 entry-T,也给 leader-U 投了票。
S3 一定是先接受 entry-T,再给 leader-U 投票。否则,它将会拒绝 leader-T 的 AppendEntries 请求,无法接受 entry-T(因为S3的 current term 已经大于 T 了)。
在 term-U 选举阶段,S3 一定还包含 entry-T,因为:
在上面的假设中 T~U 之间的 leader 还是会包含 entry-T
S3 如果中途成为 leader,也不会删除 entry-T
S3 在 term-U 阶段只有在 AppendEntries 发生日志冲突时,才会被删除
S3 要投票给 leader-U,那么 leader-U 的 log 一定不能比 S3 的旧,而这恰好是矛盾的地方。这里也可以分为两种情况考虑:
最近的 log term 相同
最近的 log term 不相同且 leader-U > S3
Raft Property: State Machine Safety Property
有 「Log Matching Property」和「Leader Completeness Property」的保证,该特性也能保证,也很容易理解。
06
—
Cluster membership changes

为了保证安全可靠性(Safety),配置变更需要采用两阶段提交(two-phase approach):
集群进入到配置转换的过渡阶段(joint consensus);
joint consensus 被提交后,集群就转换到全新的配置;
joint consensus 新旧配置同时生效,也即是:
不论是处于哪种配置,log entries 会被复制到所有节点;
不论是处于哪种配置,集群中节点都可以成为 leader;
上面提案的通过,必须同时得到新旧两部分中各自的大多数通过;
有了 joint consensus,既可以保证安全可靠性,也能够在配置变更过程中继续提供服务。
如下图描述了集群配置的变更过程:
第一阶段:joint consensus,创建C-old,new
集群配置实际会被存储在一个特殊的 log entry(C-old,new) 中。
leader 接收到配置变更请求后,在第一阶段(joint consensus)添加该 C-old,new,并依照日志复制机制来进行同步复制。
一旦某个节点接收到了新的配置,不论它是否已经 committed,该节点都会使用该配置。也就意味着,leader 会在应用 C-old,new的规则来决定 C-old,new 什么时候应该被提交。
如果 leader 崩溃,新的 leader 是C-old 还是 C-old,new 完全取决于 candidate 是否接收到了 C-old,new.
在该阶段 C-new 子集群是无法独立做决策的(前面提到的 joint consensus 特点)。
第二阶段:提交 C-old,new
C-old,new 一旦处于提交状态,C-old 和 C-new 子集群就可以独立决策了;
由于前面讲到的「Leader Completeness Property」的保证,只有拥有 C-old,new 的节点才能被选举为 leader;此时就可以创建 C-new entry 在集群中执行同步复制,新的配置就可以在整个集群中应用生效了。
一旦 C-new 生效,C-old 节点就可以被摘除,不会有影响。
从上面的过程中可以看出,C-old 和 C-new 子集群没有时机能够同时独立决策,这样就保证了安全可靠性(Safety)。






