分布式存储系统通常通过维护多个副本来进行fault-tolerance,提高系统的availability,带来的代价就是分布式存储系统的核心问题之一:维护多个副本的一致性。一致性协议就是用来干这事的,即使在部分副本宕机的情况下。Raft是一种较容易理解的一致性协议。一致性协议通常基于replicated state machines,即所有结点都从同一个state出发,都经过同样的一些操作序列,最后到达同样的state。

为了便于理解,Raft大概将整个过程分为三个阶段,leader election,log replication和commit(safety)。

每个server处于三个状态:leader,follower,candidate。正常情况下,所有server中只有一个是leader,其它的都是follower。server之间通过RPC消息通信。follower不会主动发起RPC消息。leader和candidate(选主的时候)会主动发起RPC消息。

Leader election

时间被分为很多连续的随机长度的term(一段时间),一个term由一个唯一的id标识。每个term一开始就进行leader election:

1. followers将自己维护的current_term_id加1。

2. 然后将自己的状态转成candidate。

3. 发送RequestVoteRPC消息(带上current_term_id) 给 其它所有server

这个过程会有三种结果:

1. 自己被选成了主。当收到了majority的投票后,状态切成leader,并且定期给其它的所有server发心跳消息(其实是不带log的AppendEntriesRPC)以告诉对方自己是current_term_id所标识的term的leader。每个term最多只有一个leader,term id作为logical clock,在每个RPC消息中都会带上,用于检测过期的消息,比如自己是一个过期的leader(term id更小的leader)。当一个server收到的RPC消息中的rpc_term_id比本地的current_term_id更大时,就更新current_term_id为rpc_term_id,并且如果当前state为leader或者candidate时,将自己的状态切成follower。如果rpc_term_id比本地的current_term_id更小,则拒绝这个RPC消息。

2. 别人成为了主。如1所述,当candidate在等待投票的过程中,收到了大于或者等于本地的current_term_id的声明对方是leader的AppendEntriesRPC时,则将自己的state切成follower,并且更新本地的current_term_id。

3. 没有选出主。当投票被瓜分,没有任何一个candidate收到了majority的vote时,没有leader被选出。这种情况下,每个candidate等待的投票的过程就超时了,接着candidates都会将本地的current_term_id再加1,发起RequestVoteRPC进行新一轮的leader election。

投票策略:

每个server只会给每个term投一票,具体的是否同意和后续的Safety有关。

当投票被瓜分后,所有的candidate同时超时,然后有可能进入新一轮的票数被瓜分,为了避免这个问题,Raft采用一种很简单的方法:每个candidate的election timeout从150ms-300ms之间随机取,那么第一个超时的candidate就可以发起新一轮的leader election,带着最大的term_id给其它所有server发送RequestVoteRPC消息,从而自己成为leader,然后给他们发送心跳消息以告诉他们自己是主。

Log Replication

当leader被选出来后,leader就可以接受客户端发来的请求了,每个请求包含一条需要被replicated state machines执行的命令。leader会把它作为一个log entry,append到它的日志中,然后给其它的server发AppendEntriesRPC。当leader确定一个log entry被safely replicated了,就apply这条log entry到状态机中然后返回结果给客户端。如果某个follower宕机了或者运行的很慢,或者网络丢包了,则会一直给这个follower发AppendEntriesRPC直到日志一致。

当一条日志是commited时,leader才能决定将它apply到状态机中。Raft保证一条commited的log entry已经持久化了并且会被所有的server执行。

当一个新的leader选出来的时候,它的日志和其它的follower的日志可能不一样,这个时候,就需要一个机制来保证日志是一致的。如下图所示,一个新leader产生时,集群状态可能如下:

最上面这个是新leader,a~f是follower,每个格子代表一条log entry,格子内的数字代表这个log entry是在哪个term上产生的。

新leader产生后,log就以leader上的log为准。其它的follower要么少了数据比如b,要么多了数据,比如d,要么既少了又多了数据,比如f。

需要有一种机制来让leader和follower对log达成一致,leader会为每个follower维护一个nextIndex,表示leader给各个follower发送的下一条log entry在log中的index,初始化为leader

的最后一条log entry的下一个位置。leader给follower发送AppendEntriesRPC消息,带着(term_id, (nextIndex-1)), term_id即(nextIndex-1)这个槽位的log entry的term_id,follower接收到AppendEntriesRPC后,会从自己的log中找是不是存在这样的log entry,如果不存在,就给leader回复拒绝消息,然后leader则将nextIndex减1,再重复,知道AppendEntriesRPC消息被接收。

以leader和b为例:

初始化,nextIndex为11,leader给b发送AppendEntriesRPC(6,10),b在自己log的10号槽位中没有找到term_id为6的log entry。则给leader回应一个拒绝消息。接着,leader将nextIndex减一,变成10,然后给b发送AppendEntriesRPC(6, 9),b在自己log的9号槽位中同样没有找到term_id为6的log entry。循环下去,直到leader发送了AppendEntriesRPC(4,4),b在自己log的槽位4中找到了term_id为4的log entry。接收了消息。随后,leader就可以从槽位5开始给b推送日志了。

Safety

1.哪些follower有资格成为leader?

Raft保证被选为新leader的server拥有所有的已经committed的log entry,这与ViewStamped Replication不同,后者不需要这个保证,而是通过其他机制从follower拉取自己没有的commited的log entry。

这个保证是在RequestVoteRPC阶段做的,candidate在发送RequestVoteRPC时,会带上自己的最后一条log entry的term_id和index,server在接收到RequestVoteRPC消息时,如果发现自己的日志比RPC中的更新,就拒绝投票。日志比较的原则是,如果本地的最后一条log entry的term id更大,则更新,如果term id一样大,则日志更多的更大(index更大)。

2. 哪些log entry被认为是commited?

两种情况:

1. leader正在replicate当前term即term2的log entry给其它follower,一旦leader确认了这条log entry被majority写盘了,这条log entry就被认为是committed。如图a,S1作为当前term即term2的leader,log index为2的日志被majority写盘了,这条log entry被认为是commited

2. leader正在replicate更早的term的log entry给其它follower。图b的状态是这么出来的:

S1作为term2的leader,给S1和S2 replicate完log index=2的日志后crash,当前状态为:

S1 1 2 宕机

S2 1 2

S3 1

S4 1

S5 1

S5被选为term3的leader(由于S5的最后一条log entry比S3,S4的最后一条log entry更新或一样新,接收到S3,S4,S5的投票),自己产生了一条term3的日志,没有给任何人复制,就crash了,当前状态如下:

S1 1 2

S2 1 2

S3 1

S4 1

S5 1 3 宕机

接着S1重启后,又被选为term4的leader(接收到S1,S2,S3的投票,文中没有指出S4?),然后S1给S3复制了log index为2的log entry,当前状态如下:

S1 1 2

S2 1 2

S3 1 2

S4 1

S5 1 3 宕机

这个时候S5重启,被选为了term5的主(接收了S2,S3,S4,S5的投票),那么S5会把log index为2的日志3复制给其它server,那么日志2就被overwrite了。

所以虽然这里日志2被majority的server写盘了,但是并不代表它是commited的。

对commit加一个限制:主的当前term的至少一条log entry被majority写盘

如:c图中,就是主的当前term 4的一条log entry被majority写盘了,假设这个时候S1宕机了,S5是不可能变成主的。因为S2和S3的log entry的term为4,比S5的3大。

关于算法的正确性证明见:Raft implementations

Log Compaction

在实际的系统中,不能让日志无限增长,否则系统重启时需要花很长的时间进行回放,从而影响availability。Raft采用对整个系统进行snapshot来处理,snapshot之前的日志都可以丢弃。

snapshot技术在Chubby和ZooKeeper系统中都有采用。

每个server独立的对自己的系统状态进行snapshot,并且只能对已经committed log entry(已经apply到了状态机)进行snapshot,snapshot有一些元数据,包括last_included_index,即snapshot覆盖的最后一条commited log entry的 log index,和last_included_term,即这条日志的termid。这两个值在snapshot之后的第一条log entry的AppendEntriesRPC的consistency check的时候会被用上,之前讲过。一旦这个server做完了snapshot,就可以把这条记录的最后一条log index及其之前的所有的log entry都删掉。

snapshot的缺点就是不是增量的,即使内存中某个值没有变,下次做snapshot的时候同样会被dump到磁盘。

当leader需要发给某个follower的log entry被丢弃了(因为leader做了snapshot),leader会将snapshot发给落后太多的follower。或者当新加进一台机器时,也会发送snapshot给它。

发送snapshot使用新的RPC,InstalledSnapshot。

做snapshot有一些需要注意的性能点,1. 不要做太频繁,否则消耗磁盘带宽。 2. 不要做的太不频繁,否则一旦server重启需要回放大量日志,影响availability。系统推荐当日志达到某个固定的大小做一次snapshot。3. 做一次snapshot可能耗时过长,会影响正常log entry的replicate。这个可以通过使用copy-on-write的技术来避免snapshot过程影响正常log entry的replicate。

Cluster membership changes

Raft将有server加入集群或者从集群中删除也纳入一致性协议中考虑,避免由于下线老集群上线新集群而引起的不可用。集群的成员列表重配置也是一条log entry,log内容包含了集群成员列表。

老集群配置用Cold表示,新集群配置用Cnew表示。

当集群成员配置改变时,leader收到人工发出的重配置命令从Cold切成Cnew,leader 给其它server复制一条特殊的log entry给其它的server,内容包括Cold∪Cnew,一旦server收到了这条特殊的配置log entry,其后的log entry会被replicate到Cold∪Cnew中,一条log entry被认为是committed的需要满足这条日志既被Cold的majority写盘,也被Cnew的majority写盘。一旦Cold∪Cnew这条log entry被确认为committed,leader就会产生一条只包含了Cnew的log entry,同样复制给所有server,server收到log后,老集群的server就可以自动下线了。

Performance

横坐标代表没有leader的ms数,每条线代表election timeout的随机取值区间。

上图说明只要给个5ms的区间,就能避免反复的投票被瓜分。超过10s没有leader的情况都是因为投票被瓜分的情况。

150-150ms的election timeout区间,没有主的时间平均287ms。

系统推荐使用150ms~300ms

参考资料:

In Search of an Understandable Consensus Algorithm

Raft一致性协议的更多相关文章

  1. 一文看尽 Raft 一致性协议的关键点

    本文由  网易云 发布. 作者:孙建良 Raft 协议的发布,对分布式行业是一大福音,虽然在核心协议上基本都是师继 Paxos 祖师爷(Lamport) 的精髓,基于多数派的协议.但是 Raft 一致 ...

  2. 一文带你了解 Raft 一致性协议的关键点

    此文已由作者孙建良授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. Raft 协议的发布,对分布式行业是一大福音,虽然在核心协议上基本都是师继 Paxos 祖师爷(lampor ...

  3. Raft 一致性协议算法 《In search of an Understandable Consensus Algorithm (Extended Version)》

    <In search of an Understandable Consensus Algorithm (Extended Version)>   Raft是一种用于管理日志复制的一致性算 ...

  4. 分布式一致性协议Raft原理与实例

    分布式一致性协议Raft原理与实例 1.Raft协议 1.1 Raft简介 Raft是由Stanford提出的一种更易理解的一致性算法,意在取代目前广为使用的Paxos算法.目前,在各种主流语言中都有 ...

  5. 分布式一致性协议之:Raft算法

    一致性算法Raft详解 背景 熟悉或了解分布性系统的开发者都知道一致性算法的重要性,Paxos一致性算法从90年提出到现在已经有二十几年了,而Paxos流程太过于繁杂实现起来也比较复杂,可能也是以为过 ...

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

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

  7. [转帖]分布式一致性协议介绍(Paxos、Raft)

    分布式一致性协议介绍(Paxos.Raft) https://www.cnblogs.com/hugb/p/8955505.html  两阶段提交 Two-phase Commit(2PC):保证一个 ...

  8. 分布式一致性协议 Raft

    分布式领域,CP模型下 数据一致性协议至关重要,不然两边数据不一致容易出现数据读混乱问题.像Etcd Consul  zookeeper Eureka ,Redis集群方案这些中间件 都有一致性算法来 ...

  9. 浅谈 Raft 分布式一致性协议|图解 Raft

    前言 本篇文章将模拟一个KV数据读写服务,从提供单一节点读写服务,到结合分布式一致性协议(Raft)后,逐步扩展为一个分布式的,满足一致性读写需求的读写服务的过程. 其中将配合引入Raft协议的种种概 ...

随机推荐

  1. dijit样式定制(三)Button、RadioButton、CheckBox

    dijit.form.DropDownButton dijit的button中除了ComboButton使用table布局外,其他的button都是用span嵌套布局,下图中可看一下button的主要 ...

  2. 熟用js中的Date

    js中的Date类型是使用UTC(国际协调时间)自1970年1月1日午夜(零时)开始,经过的毫秒数来保存日期. 1. 创建日期对象  ---> 获得当前日期和时间  var now = new ...

  3. html表格相关

    <html> <head> <style type="text/css"> thead {color:green} tbody {color:b ...

  4. python 模块加载

    python 模块加载 本文主要介绍python模块加载的过程. module的组成 所有的module都是由对象和对象之间的关系组成. type和object python中所有的东西都是对象,分为 ...

  5. JavaScript 常用功能总结

    小编吐血整理加上翻译,太辛苦了~求赞! 本文主要总结了JavaScript 常用功能总结,如一些常用的JS 对象,基本数据结构,功能函数等,还有一些常用的设计模式. 目录: 众所周知,JavaScri ...

  6. 深入CSS,让网页开发少点“坑”

    通常我们在学习CSS的时候,感觉语法很容易掌握,实际应用中却碰到各式各样难以填补的“坑”,为避免大家受到同样的困惑与不解,本文详细讲解了CSS中优先级和Stacking Context等高级特性.让你 ...

  7. Linux多线程系列-2-条件变量的使用(线程安全队列的实现)

    多线程情况下,往往需要使用互斥变量来实现线程间的同步,实现资源正确共享. linux下使用如下变量和函数 //条件变量 pthread_cond_t int pthread_cond_init (pt ...

  8. struts2学习笔记之七:Result类型

    一:关于Struts2的type类型,也就是Result类型,他们都实现了共同的接口Result,都实现了execute方法 他们体现了策略模式,具体Result类型参见:struts-default ...

  9. Atitit 泛型原理与理解attilax总结

    Atitit 泛型原理与理解attilax总结 1. 泛型历史11.1.1. 由来11.2. 为什么需要泛型,类型安全21.3. 7.泛型的好处22. 泛型的机制编辑22.1.1. 机制32.1.2. ...

  10. ASP.NET Web API 2框架揭秘

    ASP.NET Web API 2框架揭秘(.NET领域再现力作顶级专家精讲微软全新轻量级通信平台) 蒋金楠 著   ISBN 978-7-121-23536-8 2014年7月出版 定价:108.0 ...