一致性算法之Raft

718 阅读9分钟

概述

一致性算法

一致性算法允许多台机器作为一个集群协同工作,并且在其中的某几台机器出故障时集群仍然能正常工作。 正因为如此,一致性算法在建立可靠的大规模软件系统方面发挥了关键作用。 在之前,Paxos 几乎主导了关于一致性算法的讨论:大多数一致性的实现都是基于 Paxos 或受其影响,Paxos 已成为用于教授学生一致性相关知识的主要工具。

不幸的是,Paxos 实在是太难以理解,为了使 Raft 协议更易懂,Raft 将一致性的关键元素分开,如 leader 选举、日志复制和安全性,并且它实施更强的一致性以减少必须考虑的状态的数量。接下来就让我们进入“Raft的世界”吧!

Raft 基本概念

  • 首先,Raft 集群必须存在一个主节点(leader),我们作为客户端向集群发起的所有操作都必须经由主节点处理。所以 Raft 核心算法中的第一部分就是选主(Leader election)——没有主节点集群就无法工作,先票选出一个主节点,再考虑其它事情。
  • 其次,主节点需要承载什么工作呢?它会负责接收客户端发过来的操作请求,将操作包装为日志同步给其它节点,在保证大部分节点都同步了本次操作后,就可以安全地给客户端回应响应了。这一部分工作在 Raft 核心算法中叫日志复制(Log replication)
  • 然后,因为主节点的责任是如此之大,所以节点们在选主的时候一定要谨慎,只有符合条件的节点才可以当选主节点。此外主节点在处理操作日志的时候也一定要谨慎,为了保证集群对外展现的一致性,不可以覆盖或删除前任主节点已经处理成功的操作日志。所谓的“谨慎处理”,其实就是在选主和提交日志的时候进行一些限制,这一部分在 Raft 核心算法中叫安全性(Safety)。

Raft 核心算法其实就是由这三个子问题组成的:选主(Leader election)、日志复制(Log replication)、安全性(Safety)。这三部分共同实现了 Raft 核心的共识和容错机制。

选主

raft选举

在包含若干节点Raft集群中,存在几个重要的角色:Leader、Candidate、Follower。每种角色负责的任务也不一样,正常情况下,集群中的节点只存在 Leader 与 Follower 两种角色。

  1. Leader(领导者):处理所有客户端交互,日志复制等,一般一次只有一个Leader

  2. Follower(追随者):响应 Leader 的日志同步请求,响应 Candidate 的邀票请求,以及把客户端请求到 Follower 的事务转发(重定向)给 Leader;

  3. Candidate(候选者):负责选举投票,集群刚启动或者 Leader 宕机时,角色为 Follower 的节点将转为 Candidate 并发起选举,选举胜出(获得超过半数节点的投票)后,从 Candidate 转为 Leader 角色。

image.png

每个群众手里都分配了一个倒计时器,这个倒计时器设置的时间是随机的,在150ms~300ms之间,谁的倒计时器先结束谁就开始有优先拉票权。比如张三倒计时结束,张三会先投自己一票,然后向集群中的其他人发起拉票(RequestVote RPC),当集群中大多数人(N/2+1)都把票投给了张三,那么张三就成了领袖,这时候张三会向集群中所有人发起心跳来表示自己的权威,告诉大家,你们都要听我的。 image.png

我们来看一个实际的一个过程,如果是用极简化思维来分析,那么一个最小的Raft集群至少有三个节点(A、B、C),假设A的倒计时先结束,这个时候A向B、C发起拉票请求。

image.png

第一种情形:B、C都把票投给了A,这个时候A就成为了Leader

第二种情形:B把票投给了A,C投给了自己,这个时候A也能成为了Leader,因为获得了大多数票

第三种情形:A、B、C都把票投给了自己,这个时候没有选出Leader

第三种情况则表明本轮投票无效(Split Votes),每方都投给了自己,结果没有任何一方获得多数票。之后每个参与方会继续随机休息一阵(Election Timeout)重新发起投票直到一方获得多数票,理论上,如果每次都是平票,选举将会一直往下执行。

==选举完成后,Leader会向所有的Follower节点发起心跳,若 Follower 一段时间未收到 Leader 的心跳则认为 Leader 可能已经挂了会再次发起新选主过程。==

任期

每开始一次新的选举,称为一个任期(term),每个 term 都有一个严格递增的整数与之关联。

每当 candidate 触发 leader election 时都会增加 term,如果一个 candidate 赢得选举,他将在本 term 中担任 leader 的角色。但并不是每个 term 都一定对应一个 leader,有时候某个 term 内会由于选举超时导致选不出 leader,这时 candicate 会递增 term 号并开始新一轮选举。

image.png

Term 更像是一个逻辑时钟(logic clock)的作用,有了它,就可以发现哪些节点的状态已经过期。每一个节点都保存一个 current term,在通信时带上这个 term 号。

节点间通过 RPC 来通信,主要有两类 RPC 请求:

  • RequestVote RPCs: 用于 candidate 拉票选举。
  • AppendEntries RPCs: 用于 leader 向其它节点复制日志以及同步心跳。

日志复制

日志复制概述

共识算法通常基于状态复制机(Replicated State Machine)模型,所有节点从同一个 state 出发,经过一系列同样操作 log 的步骤,最终也必将达到一致的 state。也就是说,只要我们保证集群中所有节点的 log 一致,那么经过一系列应用(apply)后最终得到的状态机也就是一致的。

Raft 负责保证集群中所有节点 log 的一致性。

此外,Raft 赋予了 leader 节点更强的领导力(Strong Leader)。那么 Raft 保证 log 一致的方式就很容易理解了,即所有 log 都必须交给 leader 节点处理,并由 leader 节点复制给其它节点。

这个过程,就叫做日志复制(Log replication)。

Raft 日志复制机制解析

复制过程

  • 客户端的每一个请求都包含被复制状态机执行的指令。
  • leader 把这个指令作为一条新的日志条目添加到日志中,然后并行发起 RPC 给其他的服务器,让他们复制这条信息。
  • 假如这条日志被安全的复制,Leader就应用这条日志到自己的状态机中,并返回给客户端
  • 如果 follower 宕机或者运行缓慢或者丢包,领导人会不断的重试直到所有的 follower 最终都存储了所有的日志条目

image.png

日志的数据结构

  • 创建日志时的任期号(用来检查节点日志是否出现不一致的情况, term 相同的 log 是由同一个 leader 在其任期内发送的
  • 状态机需要执行的指令(真正的内容)
  • 索引:整数索引表示日志条目在日志中位置

image.png

在发送 AppendEntries RPC 的时候,leader 会将前一个日志条目的索引位置和任期号包含在里面。如果 follower 在它的日志中找不到包含相同索引位置和任期号的条目,那么他就会拒绝该新的日志条目。一致性检查就像一个归纳步骤:一开始空的日志状态肯定是满足 Log Matching Property(日志匹配特性) 的,然后一致性检查保证了日志扩展时的日志匹配特性。因此,每当 AppendEntries RPC 返回成功时,leader 就知道 follower 的日志一定和自己相同(从第一个日志条目到最新条目)。

Raft 对日志有以下保证:如果 2 个日志的相同的索引位置的日志条目的任期号相同,那么 Raft 就认为这个日志从头到这个索引之间全部相同。

依据这个保证,当 leader 和 follower 日志冲突的时候,leader 将校验 follower 最后一条日志是否和 leader 匹配,如果不匹配,将递减查询,直到匹配,匹配后,删除所有冲突的日志。这样就实现了主从日志的一致性。

正常操作期间,leader 和 follower 的日志保持一致,所以 AppendEntries RPC 的一致性检查从来不会失败。然而,leader 崩溃的情况会使日志处于不一致的状态(老的 leader 可能还没有完全复制它日志里的所有条目)。这种不一致会在一系列的 leader 和 follower 崩溃的情况下加剧。Follower 可能缺少一些在新 leader 中有的日志条目,也可能拥有一些新 leader 没有的日志条目,或者同时发生。缺失或多出日志条目的情况可能会涉及到多个任期。

下图展示了Leader和Follower日志冲突的情况:

image.png

上面这种情况Raft 如何处理呢?

leader 为每一个 follower 维护一个下标,称之为 nextIndex,表示下一个需要发送给 follower 的日志条目的索引。

当一个新 leader 刚获得权力的时候,他将自己的最后一条日志的 index + 1,如果一个 follower 的日志和 leader 不一致,那么在下一次 RPC 附加日志请求中,一致性检查就会失败(不会插入数据)。

当这种情况发生,leader 就会把 nextIndex 递减进行重试,直到遇到匹配到正确的日志。 最终一定会存在一个 next index 使得 leader 和 follower 在这之前的日志都保持一致。极端情况下 next index 为1,表示 follower 没有任何日志与 leader 一致,leader 必须从第一条日志开始同步

当匹配成功之后,follower 就会把冲突的日志全部删除,此时,follower 和 leader 的日志就达成一致。

日志复制的完整过程

image.png

参考:

【分布式系列】一致性算法之Raft

深度解析 Raft 分布式一致性协议

raft动画