Raft算法,从学习到忘记

--Raft算法阅读笔记。

--Github

概述

说到分布式一致性算法,可能大多数人的第一反应是paxos算法。但是paxos算法一直以来都被认为是难以理解,难以实现。So...Stanford的Diego Ongaro和John Ousterhout提出了Raft算法,这是一个更容易理解的分布式一致性算法,在算法的论文中,不仅详细描述了算法,甚至给出了RPC接口定义和伪代码,这显然更加容易应用到工程实践中。这两个算法在一定程度上是相通的,个人觉得Raft是加了更多约束的Poxas衍生算法。

log entry:每个entry包含状态机的command term:用来标示是那一轮选举,每次选举都会递增 index:log entry的标示,term和index唯一标示一个entry commit:将log entry应用到状态机中

服务器状态

图中是Raft中的Repliated status machine architecture,每个server维护一份log,在这些log中记录的是应用到state machine中的commands。各个服务器中的状态机被apply一致的log的命令,从而保持集群中服务器的最终的一致性。

如下图所示,在Raft算法中,服务器有三种状态:Follower, Candidate, Leader。

  1. 在服务器刚启动的时候,都属于Follower状态,它接收Leader的RPC请求。如果经过一定时间没有发现Leader,那么它转换到Candidate状态,进而开始Leader的选举。
  2. Candidate发现集群中已经有Leader的时候便会转换到Follower状态,又或者它在选举中获得大多数节点(majority of servers)的选票变成Leader。
  3. Leader只有在发现比自己高term的Leader的时候才会转换成Follower。

RPC Interface

为了保证各个服务器的一致性,Raft算法有一些不变式是要保证的。

  • Election Safety: 在每一个term中最多只有一个leader会被选举出来。
  • Leader Append-Only: 每个leader只能append自己的log,而不能覆盖或者删除它。
  • Log Matching: 如果两条log具有相同的index和term,那么它们之前的logs应该是一致的。
  • Leader Completeness: 如果一个log在一个term中被committed,那么它会出现在所有更高term的leader的log中。
  • State Machine Safety: 如果一个server已经把一个log entry应用到她的状态机,那么没有其他的server可以应用一个具有同样term和index的不同的log entry。

Raft算法给出了服务器状态的变量,以及所所用到的RPC接口的定义和伪代码:

Status

Persistent state on all servers
在RPC调用返回之前会被更新到稳定的存储中
currentTerm: server发现的最高的term
voteFor: 在当前term中投票给了哪一个candidate
log[]: log entries,entry包含应用到状态机的命令和从leader得来的term和command Volatile state on all servers
commitIndex: 已尽被提交的log的最大log entry。
lastApplied: 已经被应用到status的最大log entry。 Volatile state on leaders
在选举后重新初始化
nextIndex[]: 要发给各个follower的下一个log entry的index。
matchIndex[]: 各个服务器当前已被replicated的log entry的最大index。

AppendEntriesRPC

leader用于replicate log entries,也用作心跳。

Arguments
term: leader's term
leaderId: follower用于让client重定向
prevLogIndex: 新log entry的前一个entry的index
preLogTerm: 新log entry前一个entry的term
entries[]: 需要同步的log entries,如果用作心跳,那么这个参数为空
leaderCommit: leader's commitIndex Results
term: 当前term
success: 如果follower有entry符合preLogIndex和preLogTerm,则返回true,否则返回false。 被调用方的实现:
1. replay false if term < currentTerm
2. replay false 如果没有entry符合preLogIndex和preLogTerm
3. 如果存在与新entry冲突的entry,那么把现有的冲突entry和往后的entries删除
4. Append any new entries not already in the log
5. if leaderCommit > commitIndex, set commitIndex = min(leaderCommit, index of last new entry)

RequestVote RPC

candidate用于收集选票
Arguments
term: candidate's term
candidateId: candidate requesting vote
lastLogIndex: index of candidate's last log entry
lastLogTerm: term of candidate's last log entry Results
term: currentTerm, for candidate to update itself
voteGranted: true means candidate received vote 被调用方实现:
1. Reply false if term < currentTerm
2. If voitedFor is null or candidateId, and candidate's log is at least as up-to-date as receiver's log, grant vote.

Rules for Servers

All Servers

  • If commitIndex > lastApplied: increment lastApplied, apply log[lastApplied] to state machine
  • If RPC request or response contains term T > currentTerm: set currentTerm = T, convert to follower

Followers

  • Respond to RPCs from candidates and leaders
  • If election timeout elapses without receiving AppendEntries RPC from current leader or granting vote to candidate: convert to candidate

Candidates

  • On conversion to candidate, start election:

    • Increment currentTerm
    • Vote for self
    • Reset election timer
    • Send RequestVote RPCs to all other servers
  • If votes received from majority of servers: become leader
  • If AppendEntries RPC received from new leader: convert to follower
  • If election timeout elapses: start new election

Leaders

  • Upon election: send initial empty AppendEntries RPCs (heartbeat) to each server; repeat during idle periods to prevent election timeouts
  • If command received from client: append entry to local log, respond after entry applied to state machine
  • If last log index ≥ nextIndex for a follower: send AppendEntries RPC with log entries starting at nextIndex
    • If successful: update nextIndex and matchIndex for follower
    • If AppendEntries fails because of log inconsistency:decrement nextIndex and retry
    • If there exists an N such that N > commitIndex, a majority of matchIndex[i] ≥ N, and log[N].term == currentTerm: set commitIndex = N

Leader Election

在一个server转换成candidate后就会++term,并且用RequestVoteRPC发起选举。对于一个server来说,一轮选举的结果有三种:

  1. 赢得选举变成leader
  2. 另一个server赢得选举,自己变成follower
  3. 没有winner,开始新一轮选举。

在一轮选举中:

  1. 选票是先到先得,也就是说一个server收到一个RequestVoteRPC,如果请求投票的server满足voitedFor is null or candidateId, and candidate's log is at least as up-to-date as receiver's log, 那么就将选票给它。
  2. 当一个server获得大多数选票的时候,它成为leader,并向其他服务器发送心跳,告知新的leader已经产生。
  3. 如果一个server收到不低于自己term的心跳,说明已经产生了leader,这个server转换成follwer。
  4. 如果一轮选举没有winner,那么心跳超时以后在随机延时之后将发起新一轮的选举。以避免形成活锁

Log Replicate

一个log entry由leader接收client的请求而产生,通过appendEntryRPC同步集群中的其他服务器,在多数节点返回以后,leader向client返回处理结果。并且commit已经完成replicated的log entry。

在leader对一个entry做commite操作的时候,同时也会将这个entry之前的entrise一并做commit。在AppendEntries RPC中,会带上leader已知被commite的entries的最大index,这样follower就根据这个index将自己的log entrise中在index之前的entrise应用到状态机中。

当AppendEntriesRPC中的preLog不存在于follower的log中,那么follower返回false,然后leader递减该follower的nextIndex,并重新发送AppendEntriesRRC直到找到leader跟follower共同拥有的log entry。如果follower中存在leader中没有的log entries,那么讲使用leader的log进行覆盖。

对于AppendEntriesRPC的实现,也有人提出一些优化的方案,比如说在返回false的时候附带follower的last entry的信息,但是论文作者认为在实践中失败并不是经常发生,并且增加这些优化机制会增加算法实现的复杂度,index递减的机制已经可以保证在实践中算法的性能,所以没有必要去使用更加复杂的机制。

对于上一个term的entries,如果它们没有被commite,那么新的leader也不会去计算它们备份的数量来判断它们是否应该commite,而是通过commite新leader自己提出的entry,从而把之前的entries也提交了,见Log Matching

Cluster membership

这个小节讨论集群配置变化的情况。在实践中,集群的配置不可能完全没有变化,那么如何处理配置变化的情况就是一个需要解决的问题。最简单的方式莫过于停止所有的服务器,更改配置,然后重启。但是这样在实际的工程实践中一般是很难被接受的,因为需要完全停止服务。

为了配置变更的安全性,那么必须保证在任意的时间里在同一个term中不能有两个server同时被选举成为leader,否则就产生了分布式系统中所谓的脑裂。不幸的是任何直接将server从老的配置变更到新的配置的方式都是不安全的。因为在转变的过程中,集群又可能分化为两个(majorities)集群。

如上图所示,由于每个服务器的转变时间不一样在某个时刻可能会有两个leader选出。在这个例子中就是集群中的服务器从三台增加到五台的情况,在同一个term中就有可能会出现以老的配置Cold的一个集群和使用新的配置的一个集群。

为了保证配置更新的安全性,配置的变化必须使用两段的策略。必入有一些系统使用第一阶段停止老的配置,使之不能响应客户端的请求,第二阶段使新的配置生效。

在Raft算法中,集群首先会进入一个过渡的配置,我们称之为joint consensus,在这个阶段新老配置同时存在。一旦joint consensus被committed,那么系统将转换到新的配置。

在joint consensus中,新老配置同时存在:

  1. Log entries are replicated to all servers in both con- figurations.
  2. Any server from either configuration may serve as leader.
  3. Agreement(forelectionsandentrycommitment)re- quires separate majorities from both the old and new configurations. 这个指的是一个agreement需要新老Configuration两个集群中的各自的大多数都同意。

集群的configuration使用一个特殊的entries来保存和传输,当leader收到configuration从Cold到Cnew的变更的请求,它将configuration保存成Cold,new,并且将它replicate到其他server中。当server将new configuration entry保存到它的log中,它将使用新的configuration来做将来的决策(server都会使用最新的configuration,不管它是否被提交)。也就是说leader使用Coldnew的规则来判断什么时候Cold,new被提交。如果这时候leader挂掉了,那么新的leader有可能具有Cold或者Cold,new,这要取决于它是否已经收到了Cold,new。在这种情况中,Cnew不能单方面的做出决定。

当Cold,new已经被提交,那么Cold和Cnew都不能在没有对方同意的情况下单方面的作决定。在这之后leader就可以产生一个描述Cnew的entry,并且将他replicate到集群中。当Cnew被commite,就可以将Cnew中没有包括的server安全的关闭了。

在任何时候Cold和Cnew都不能单方面的作出决定。这就保证了安全性。

不过还有3个延伸的问题:

  1. 新的server可能没有任何日志,那么在与leader同步之前它可能并不能commite任何entry。Raft为这种情况添加了一个额外的phase,这个阶段让新加入的server与leader同步,但是它不被认为是majorities(non-voting members)。
  2. 如果新选出的leader不是new configuration的一员,那么在它commite完Cnew,他将主动卸任,转换成follower。也就是说他要管理一个不包括它自己的集群,直到Cnew已经被commmite。
  3. 被下限的机器会因为收不到机器而超时,他会增加自己的term,并向集群中的机器发起选举。因为它的term会比集群中的机器高,但是preLog却不够up-to-date,所以Cnew的机器总是会被选举成新的leader。但是下线的机器又回超时而重复触发选举,从而降低可用性。为了解决这个问题,Raft使用了一个简单而有效的策略,如果server确认leader还存活,也就是在心跳的超时时间之内,它将忽略RequestVoteRPC请求。这个策略既没有修改选举的核心策略,却能完美的解决了下线机器的困扰。

Log compaction

Raft的log会随着时间的推移而越来越大,但是在实际的实践中并不能让日志没有限制的增长,这会使得占用空间和replay的时间增加。与Chubby和ZooKeeper等很多分布式系统一样,Raft也使用Snapshot来压缩自己的log。在Snapshot中,当前的系统状态会被记录并存储下来,而之前的log就可以删掉了。当然,使用log cleaning和LSM树也是可以的,log clean需要对Raft算法做一定的修改,当然也会给算法带来额外的复杂度;状态机可以实现LSM树并用相同的snapshot接口。

如图,在snapshot中,会包括当前的状态机状态,并且会包括snapshot中的最后一个entry的index和term,这是为了让AppendEntriesRPC调用的时候能够检查到preLog的信息。snapshot中还会包涵最新的configuration。 虽然各个server是独立的进行snapshot的操作,但是当一个节点落后太多的时候leader还是需要向该节点发送snapshot来进行一致性的同步。这种情况发生在leader生成snapshot的时候把要发送给该节点的nextLog从log中删除掉了。 下面是Leader给其他节点发送snapshot的RPC接口:

InstallSnapshot RPC

Arguments
term: leader's term
leaderId: 让follower能够重定向请求
lastIncludedIndex: the snapshot replaces all entries up through and including this index
lastIncludedTerm: term of lastIncludedIndex
offset: byte offset where chunk is positioned in the snapshot file
data[]: raw bytes of the snapshot chunk, starting at offset
done: true if this is the last chunk Results
term:currentTerm, for leader to update itself Receiver impleamentation
1. Reply immediately if term < currentTerm
2. Create new snapshot file if first chunk (offset is 0)
3. Write data into snapshot file at given offset
4. Reply and wait for more data chunks if done is false
5. Save snapshot file, discard any existing or partial snapshot with a smaller index
6. If existing log entry has same index and term as snapshot’s last included entry, retain log entries following it and reply
7. Discard the entire log
8. Reset state machine using snapshot contents (and load snapshot’s cluster configuration)

另外,如果server收到一个snapshot指向之前的entries,那么它将覆盖snapshot指向的entries,并且保留之后的log entries,这种情况发生在网络不稳定而重传的时候。

生成snapshot也是需要消耗io和时钟周期的,所以snapshot的时机也是需要考虑的。一个简单的策略就是文件达到一定的大小就进行snapshot的生成。而对于磁盘io性能的消耗,一般使用,一般使用copy-on-write的技术,使得在snapshot的时候还是可以接收客户端请求。例如把状态机以支持写时复制的数据结构实现。另外,一些操作系统的写时复制机制也可以加以利用(Alternatively, the operat- ing system’s copy-on-write support (e.g., fork on Linux) can be used to create an in-memory snapshot of the entire state machine (our implementation uses this approach))

Client interaction

与其他主从结构的分布式系统一样,Raft算法中写请求的操作全都由leader来进行,如果follower接收到了写请求,那么它将拒绝请求,并且提供leader的地址,供client访问。在leader crash的时候有可能会使得client以为之前的请求没有执行而发起重复的请求,从而使得请求被执行两次,这个问题的解决办法是客户端在请求中附带一个线性递增的序列号,而状态机则追踪各个client的最新的序列号。

读请求可以不经过leader,但是如果不加额外限制读请求可能会读到过期的数据,这种情况发生在读请求由leader处理,并且这个leader是一个新的leader,它可能还不知道哪些entries已经被commite了,或者leader正好与集群断开了连接。对于第一种情况,Raft算法要求leader在自己成为leader之后先commite一个空的entries(no-op entry)这样它就拥有了哪些entries已经被commite的信息。对于第二种情况,Raft算法要求leader在处理只读请求的时候,需要先获得大多数节点的心跳成功。

小结

在读完Raft算法的论文以后,最大的感受就是Raft算法就如它提出的理由一样,简单、易于理解(KISS原则有没有),并且可以很好的指导工程实践。但是还是需要看一下Paxos,可以借助Raft去理解它,毕竟是用讲故事的方式描述的算法~

另外,突然觉得log就是分布式的核心啊。。(别问我为什么,我就是那么觉得的)

引用

Raft算法,从学习到忘记的更多相关文章

  1. 学习Raft算法的笔记

    Raft是一种为了管理日志复制的一致性算法.它提供了和Paxos算法相同的功能和性能,但是它的算法结构和Paxos不同,使得Raft算法更加容易理解并且更容易构建实际的系统.为了提升可理解性,Raft ...

  2. 分布式一致性算法:Raft 算法(论文翻译)

    Raft 算法是可以用来替代 Paxos 算法的分布式一致性算法,而且 raft 算法比 Paxos 算法更易懂且更容易实现.本文对 raft 论文进行翻译,希望能有助于读者更方便地理解 raft 的 ...

  3. 关于raft算法

    列出一些比较好的学习资料, 可以经常翻一番,加深印象 0 raft官方git 1  raft算法动画演示 2    Raft 为什么是更易理解的分布式一致性算法 3  raft一致性算法 4  Raf ...

  4. 一文搞懂Raft算法

      raft是工程上使用较为广泛的强一致性.去中心化.高可用的分布式协议.在这里强调了是在工程上,因为在学术理论界,最耀眼的还是大名鼎鼎的Paxos.但Paxos是:少数真正理解的人觉得简单,尚未理解 ...

  5. 【转】分布式一致性算法:Raft 算法(Raft 论文翻译)

    编者按:这篇文章来自简书的一个位博主Jeffbond,读了好几遍,翻译的质量比较高,原文链接:分布式一致性算法:Raft 算法(Raft 论文翻译),版权一切归原译者. 同时,第6部分的集群成员变更读 ...

  6. 搞懂分布式技术2:分布式一致性协议与Paxos,Raft算法

    搞懂分布式技术2:分布式一致性协议与Paxos,Raft算法 2PC 由于BASE理论需要在一致性和可用性方面做出权衡,因此涌现了很多关于一致性的算法和协议.其中比较著名的有二阶提交协议(2 Phas ...

  7. leetcode 刷500道题,笔试/面试稳过吗?谈一谈这些年来算法的学习

    想要学习算法.应付笔试或者应付面试手撕算法题,相信大部分人都会去刷 Leetcode,有读者问?如果我在 leetcode 坚持刷它个 500 道题,以后笔试/面试稳吗? 这里我说下我的个人看法,我认 ...

  8. 10分钟弄懂Raft算法

    分布式系统在极大提高可用性.容错性的同时,带来了一致性问题(CAP理论).Raft算法能够解决分布式系统环境下的一致性问题. 我们熟悉的ETCD注册中心就采用了这个算法:你现在看的这篇微信公众号文章, ...

  9. 推荐一个算法编程学习中文社区-51NOD【算法分级,支持多语言,可在线编译】

    最近偶尔发现一个算法编程学习的论坛,刚开始有点好奇,也只是注册了一下.最近有时间好好研究了一下,的确非常赞,所以推荐给大家.功能和介绍看下面介绍吧.首页的标题很给劲,很纯粹的Coding社区....虽 ...

随机推荐

  1. Express 3.0新手指南入门教程

    在确认已经安装了node之后(下载), 在你的机器上创建一个目录,让我们来开始你的第一个应用程序吧 $ mkdir hello-world 在这个目录中你首先得定义一下你的应用程序“包”文件,它和其它 ...

  2. Oracle中 Instr 这个函数

    http://www.jb51.net/article/42369.htm sql :charindex('字符串',字段)>0 charindex('administrator',MUserI ...

  3. table固定前两列和最后一列,其他滑动显示

    网上搜的基本都是4个table做的,数据处理比较麻烦,写了个一个table的,此示例只固定了前两列和最后一列,和网上的不太一样. 网上搜的基本都是4个table做的,数据处理比较麻烦,写了个一个tab ...

  4. 第三章 Python 的容器: 列表、元组、字典与集合

    列表是Python的6种内建序列(列表,元组,字符串,Unicode字符串,buffer对象,xrange对象)之一, 列表内的值可以进行更改,操作灵活,在Python脚本中应用非常广泛 列表的语法格 ...

  5. 一步一步学Java IO

    1.基本概念 1.1.InputStream 最基本的字节输入流,抽象类,定义了读取原始字节的所有基本方法1.1.1.public abstract int read() throws IOExcep ...

  6. 转载:MAT Memory Analyzer Tool使用示例

    地址:http://blog.csdn.net/yanghongchang_/article/details/7711911 以下是一个会导致java.lang.OutOfMemoryError: J ...

  7. 使用PMD进行代码审查

    很久没写博客了,自从上次写的设计模式的博客被不知名的鹳狸猿下架了一次之后兴趣大减,那时候就没什么兴致写博客了,但是这段时间还没有停下来,最近也在研究一些其他的东西,目前有点想做点东西的打算,但好像也没 ...

  8. 蓝桥网试题 java 基础练习 十六进制转八进制

    - -------------------------------------------------------------------------------------------------- ...

  9. gevent调度流程解析

    gevent是目前应用非常广泛的网络库,高效的轮询IO库libev加上协程(coroutine),使得gevent的性能非常出色,尤其是在web应用中.本文介绍gevent的调度流程,主要包括geve ...

  10. 初识PHP遗留下来的问题?

    待解决的问题: 1.写一个PHP脚本,显示用户输入的名称. 提示: <?php echo $_POST["username"];?>! <?php echo &q ...