Raft 也是一种分布式一致性算法(2PC、Proxy、ZAB)
一致性算法是从复制状态机的背景下提出的。
一致性算法管理着来自客户端指令的复制日志。状态机从日志中处理相同顺序的相同指令,所以产生的结果也是相同的
三个子问题
Raft 将分布式一致性问题分解成了三个相对独立的子问题:
- 选主:现存的领导人宕机,一个新的领导人需要被选举出来
- 日志复制:领导人必须从客户端接收日志然后复制到集群中的其他节点,并且强制要求其他节点的日志保持和自己相同
- 安全性:在 Raft 中安全性的关键是状态机安全:如果有任何的服务器节点已经应用了一个确定的日志条目到它的状态机中,那么其他服务器节点不能在同一个日志索引位置应用一个不同的指令
大体流程
集群选择出一个节点作为 leader,leader 节点负责接收客户端的请求(日志),并负责把请求复制给所有的从节点,保证节点之间数据的同步。如果 leader 节点出现问题挂掉,那么其他正常节点会重新选择 leader。
选主 leader election
每个节点在任意情况下,只能有三种状态可选:
- leader:领导节点,或者主节点,用来处理客户端发来的请求,并保证请求数据在整个集群的同步,需要用心跳和 follower 节点通信,通知它们自己的可用性
- follower:负责处理 leader 和 candidate 请求的节点。如果客户端把请求发送给 follower 节点,它需要把请求转发给 leader,由 leader 统一负责管理
- candidate:leader 的候选人,只是在选举过程中短暂出现的状态。如果通过选举,则会变成 leader;如果选举失败,还是会回到 follower 状态
任期(Term)的概念:任期是依次递增的编号,每次选举都是一个新的任期。在一个任期内最多只能有一个 leader,也就是说一个任期可以有一个 leader,表示正常工作;也可以没有 leader,表明选举失败。某个节点选举成功后,就成为当前任期的 leader,负责日志复制工作。
任期的主要目的是保证所有节点逻辑时间上的一致,而不会出现过期的请求导致逻辑混乱的情况。
每个节点都会保存一个当前任期的值,当节点通信时会交互当前任期的值,
- 如果节点发现其他节点的当前任期比自己的大,就更新自己当前任期的值;
- 如果 leader 节点发现有比自己大的任期值,则知道自己的任期过期了,集群中有更新的 leader 节点,它立即变成 follower 状态;
- 如果节点接收到历史任期的请求,则直接无视(这很可能是因为网络延迟或者报文重复导致的)。
当节点刚启动的时候,默认是在 follower 状态。
- 如果它能定时收到 leader 的心跳或者日志复制的请求,则会一直处于该状态;
- 如果在设定的超时时间(election timeout)内没有收到 leader 的消息,则认为当前集群没有 leader,或者 leader 以及失效,立即会发起重新投票。
投票
投票开始,节点会增加自己的当前任期值,转换成 candidate 状态,并向其他节点发送请求投票的消息(表示自己想成为下一个任期的 leader)。有三种情况:
- 节点收到大多数节点的投票,成为新任期的 leader。每个节点在每个任期只能投票一次,采取先到先得的原则,投票给最先收到的选主节点。大多数原则保证一个任期最多只能有一个节点
- 节点发现已经有另一个节点成为 leader。在等待选举结果的时候,节点收到了心跳或者日志复制的消息(也就是有 leader 了),如果这个 leader 合法(任期比自己的当前任期高),则当前节点会自动变成 follower 状态;否则会继续等待
- 一段时间过去,并没有任何节点成为 leader。比如有多个节点要选举 leader,而且都投票结果比较分散,没有节点获得过半的票数
如果不采取任何措施,那么第三种情况一直出现,会导致整个集群一直处于选举的状态,这当然是不可接受的。为此,raft 采取了随机时间的办法。
首先,选举超时时间(election timeout)是随机的,保证会有一个节点首先超时,率先选举,其他节点来不及和它竞选,它就会成为新的 leader,发送心跳和日志复制请求。
其次,在开始选举时,每个 candidate 节点重置它的超时计时器,等待计时器结束之后才会开始下一次选举,从而打乱下次选举的前后顺序,保证有一个节点先开始选举,并成为 leader。
事实证明,这两种方式能够保证选举在很短的时间里完成,而不会一直循环。
选举过期时间(election timeout)一般设置为 150-300ms,这是大量实验得到的经验值
日志复制 Log replication
一旦一个领导人被选举出来,他就开始为客户端提供服务。客户端的每一个请求都包含一条被复制状态机执行的指令。
log同步复制
- 领导人把这条指令作为一条新的日志条目附加到日志中去,
- 然后并行的发起附加条目 RPCs 给其他的服务器,让他们复制这条日志条目。
- 当这条日志条目被安全的复制(下面会介绍),领导人会应用这条日志条目到它的状态机中
- 然后把执行的结果返回给客户端。
- 如果跟随者崩溃或者运行缓慢,再或者网络丢包,领导人会不断的重复尝试附加日志条目 RPCs (尽管已经回复了客户端)直到所有的跟随者都最终存储了所有的日志条目。
每个日志记录都要保存
- 一个状态机的指令,
- 主节点接受请求时候的任期值
- 一个 index 表示它在日志文件中的位置
log提交
当日志记录被状态机执行后,就称它为已提交(commited)。当主节点知道日志记录已经复制到大多数节点时,会把当前记录提交到本地的状态机(因为日志已经更新到大多数节点,所有数据是安全的),也就是更改数据的值。
leader 节点会记录已经提交(commited)的最大日志 index,然后后续的心跳和日志复制请求会带上这个值,这样从节点就能知道哪些记录已经提交了,自己也会让状态机开始执行日志中的记录。从而达到所有状态机数据的一致性!
这样的日志机制保证了如果不同节点的日志文件某个记录的 index 和任期都相同,那么它们的内容也一定相同,而且之前的日志记录也一定是一样的。
当主节点发送日志复制的请求时,它会带上前一个日志记录的 index 和 term,如果从节点发现自己的日志中不存在这个记录,则会拒绝这个请求。
log不一致
领导人崩溃的情况会使得日志处于不一致的状态(老的领导人可能还没有完全复制所有的日志条目)。这种不一致问题会在领导人和跟随者的一系列崩溃下加剧。如跟随者可能会丢失一些在新的领导人中有的日志条目,他也可能拥有一些领导人没有的日志条目,或者两者都发生。丢失或者多出日志条目可能会持续多个任期。
解决办法:在 Raft 算法中,领导人处理不一致是通过强制跟随者直接复制自己的日志来解决了(先找到从节点日志和自己日志记录第一个不一致的地方,然后一直覆盖到最后。)。这意味着在跟随者中的冲突的日志条目会被领导人的日志覆盖。5.4 节会阐述如何通过增加一些限制来使得这样的操作是安全的。
安全性
raft 对选主做出了限制,从而实现算法的正确性。总的来说,这个限制只有一句话:只有保存了最新日志的节点(term最大 index最大的所有的已提交日志记录)才能选举成为 leader
跟随者和候选人崩溃
如果跟随者或者候选人崩溃了,那么后续发送给他们的 RPCs 都会失败。Raft 中处理这种失败就是简单的通过无限的重试;如果崩溃的机器重启了,那么这些 RPC 就会完整的成功。如果一个服务器在完成了一个 RPC,但是还没有响应的时候崩溃了,那么在他重新启动之后就会再次收到同样的请求。
Raft 的 RPCs 都是幂等的,所以这样重试不会造成任何问题。
集群成员变化
任何服务器直接从旧的配置直接转换到新的配置的方案都是不安全的。一次性自动的转换所有服务器是不可能的,所以在转换期间整个集群存在划分成两个独立的大多数群体的可能性
为了保证安全性,配置更改必须使用两阶段方法。
在 Raft 中,集群先切换到一个过渡的配置,我们称之为共同一致;一旦共同一致已经被提交了,那么系统就切换到新的配置上。共同一致是老配置和新配置的结合:
- 日志条目被复制给集群中新、老配置的所有服务器。
- 新、旧配置的服务器都可以成为领导人。
- 达成一致(针对选举和提交)需要分别在两种配置上获得大多数的支持。
共同一致允许独立的服务器在不影响安全性的前提下,在不同的时间进行配置转换过程。此外,共同一致可以让集群在配置转换的过程人依然响应客户端的请求。
日志压缩
Raft 的日志在正常操作中不断的增长,但是在实际的系统中,日志不能无限制的增长。
快照是最简单的压缩方法。
客户端交互
Raft 中的客户端发送所有请求给领导人。当客户端启动的时候,他会随机挑选一个服务器进行通信。
- 如果客户端第一次挑选的服务器不是领导人,那么那个服务器会拒绝客户端的请求并且提供他最近接收到的领导人的信息(附加条目请求包含了领导人的网络地址)。
- 如果领导人已经崩溃了,那么客户端的请求就会超时;客户端之后会再次重试随机挑选服务器的过程。
Raft 的目标是要实现线性化语义(每一次操作立即执行,只执行一次,在他调用和收到回复之间)
但是,如上述,Raft 是可以执行同一条命令多次的:例如,如果领导人在提交了这条日志之后,但是在响应客户端之前崩溃了,那么客户端会和新的领导人重试这条指令,导致这条命令就被再次执行了。解决方案就是客户端对于每一条指令都赋予一个唯一的序列号。然后,状态机跟踪每条指令最新的序列号和相应的响应。如果接收到一条指令,它的序列号已经被执行了,那么就立即返回结果,而不重新执行指令。
只读的操作可以直接处理而不需要记录日志。但是,在不增加任何限制的情况下,这么做可能会冒着返回脏数据的风险,因为领导人响应客户端请求时可能已经被新的领导人作废了,但是他还不知道。
线性化的读操作必须不能返回脏数据,Raft 需要使用两个额外的措施在不使用日志的情况下保证这一点。
- 首先,领导人必须有关于被提交日志的最新信息。领导人完全特性保证了领导人一定拥有所有已经被提交的日志条目,但是在他任期开始的时候,他可能不知道那些是已经被提交的。为了知道这些信息,他需要在他的任期里提交一条日志条目。Raft 中通过领导人在任期开始的时候提交一个空白的没有任何操作的日志条目到日志中去来实现。
- 第二,领导人在处理只读的请求之前必须检查自己是否已经被废黜了(他自己的信息已经变脏了如果一个更新的领导人被选举出来)。Raft 中通过让领导人在响应只读请求之前,先和集群中的大多数节点交换一次心跳信息来处理这个问题。可选的,领导人可以依赖心跳机制来实现一种租约的机制,但是这种方法依赖时间来保证安全性
Ref:
动画解释:http://thesecretlivesofdata.com/raft/
中文:https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md
参考:https://cizixs.com/2017/12/04/raft-consensus-algorithm/