赞
踩
Raft是分布式环境下的一致性算法,它通过少数服从多数的选举来维持集群内数据的一致性。它与RBFT算法名称有点像,然而Raft算法里不能存在拜占庭节点,而RBFT则能容忍BFT节点的存在。Raft非常类似于paxos协议(参见我的这篇文章《paxos算法如何容错的–讲述五虎将的实践》),然而它比paxos协议好理解许多(因为paxos协议难以具体实现,所以zookeeper参考paxos实现了它自己的Zab算法)。同样,Raft有一个用GO语言实现的etcd服务,它的功能与Zookeeper相同,在容器操作系统CoreOS作为核心组件被使用。
本文先从算法整体上说明其特点,再细说其实现。为方便大家理解,本文还是以图为主,没有过多涉及算法的细节。Raft易理解易实现,是我们入门分布式一致性算法的捷径!
Raft协议的性能并不比paxos的各种实现更高,它的优点主要在于协议的可理解性好,且非常具备可操作性,很容易照着协议就可以实现出稳定、健壮的算法。论文作者在斯坦福和加州大学做过测试,对本科及研究生分别学习paxos和Raft协议课程后测验,在总分60分的测试里其得分如下图所示:
从上图可见,Raft协议的得分(平均25.7)明显高于paxos(平均20.8)。我相信学习过paxos算法的人都有心得:很辛苦的理解后,一段时间后就完全不记得细节了,至少我本人是如此。而Raft协议非常简单清爽,这个测试也很能反映问题。在测试后,作者还发起了一个调查问卷,询问学生这两个算法哪个更容易实现?哪个更容易理解?其答案如下图所示:
可见,这个带有主观性的调查问卷呈现压倒性优势:Raft是一个容易理解、容易实现的算法。
Raft有很多语言的实现,包括C++、GO、Scala、Java等(详见https://raft.github.io/),但最有名的实现就是etcd了,它作为CoreOS生态的重要组件而闻名。我们可以通过etcd的性能数据看一看Raft算法的实际表现。
测试集群由3台服务器构成,其配置如下:
下面分别测试写和读的性能。写性能数据如下表所示:
Number of keys | Key size in bytes | Value size in bytes | Number of connections | Number of clients | Target etcd server | Average write QPS | Average latency per request | Average server RSS |
---|---|---|---|---|---|---|---|---|
10,000 | 8 | 256 | 1 | 1 | leader only | 583 | 1.6ms | 48 MB |
100,000 | 8 | 256 | 100 | 1000 | leader only | 44,341 | 22ms | 124MB |
100,000 | 8 | 256 | 100 | 1000 | all members | 50,104 | 20ms | 126MB |
而读性能数据如下表所示:
Number of requests | Key size in bytes | Value size in bytes | Number of connections | Number of clients | Consistency | Average read QPS | Average latency per request |
---|---|---|---|---|---|---|---|
10,000 | 8 | 256 | 1 | 1 | Linearizable | 1,353 | 0.7ms |
10,000 | 8 | 256 | 1 | 1 | Serializable | 2,909 | 0.3ms |
100,000 | 8 | 256 | 100 | 1000 | Linearizable | 141,578 | 5.5ms |
100,000 | 8 | 256 | 100 | 1000 | Serializable | 185,758 | 2.2ms |
在读性能数据中,Linearizable一致性高于Serializable,故性能稍差。其原始页面在这里:https://coreos.com/etcd/docs/latest/op-guide/performance.html。
事实上,在算法的正常运行中与paxos并无差异。而一旦leader宕机,从发现到重新选举出新leader、新leader开始工作的这段时间的长短,是影响性能的重要指标。下图中对5个节点构成的集群反复的让leader宕机,观察恢复的时间,其结果如下:
在上图中有以下几个关注点:
上图表明,增加随机化后,可以大幅减少选举的平均用时。下面的图表明,通过降低最短的超时时间,也可以减少宕机时间。Raft推荐的时间为150-300ms。
复杂的问题可以通过分解为多个简单的子问题来解决,Raft正是如此(paxos很难分解)。Raft首先定义自己是一个key/value数据库。那么,请求就分为读和写。Raft将问题分解为以下几个要点:
因此Raft将一致性问题转换为leader的选举上,以及leader与follower之间的数据同步。我们先来谈leader与 follower之间的数据同步问题。每一次写请求会修改数据,读请求则不会,所以把leader收到的写请求当作一次操作日志记录下来,且同时把操作日志同步给所有的follower节点,而有一个最终状态数据库记录某一个key的最终值,如下图所示(事实上这与fabric区块链里,多条交易日志构成的世界状态数据库非常相似,详情请参见《区块链开源实现hyperledger fabric架构详解》):
上图中其步骤含义如下:
当一个集群刚启动时,所有的节点都是follower,follower只能被动的接收leader的消息并响应。此时经过一段时间若follower节点发现全集群没有leader,开始把自己作为leader的候选人向大家征询投票,此时该节点叫做Candidate候选人。若多数follower节点同意后,则升级为leader节点。而leader节点有义务定时心跳通知所有的 follower节点,使follower节点知道此时集群中的leader是谁。如下图所示:
上图的状态变迁里,follower在一个随机定时器(例如150ms到300ms之间)内没有收到leader的心跳,则开始发起选举,而候选人就是自己,所以自己转化为Candidate,且自己首先投自己一票。若在投票未完成时,发现新的leader出现,则取消投票,由candidate转换为follower。
每次选举是一个任期,这个任期叫做term。每次任期有一个任期号,它是全局的、递增的,当网络中断时虽然会暂时不一致,但网络畅通后会很快同步,如下图所示:
如上图中,蓝色是选举期,绿色是产生leader后。如果不出现意外,这个leader会一直当下云,所以term周期会很长。出现宕机或者网络波动时,重新选举于是出现term2。在term3时也可能一直选举不出新的leader,此时很可能多个candidate发起了投票,票数被平摊后谁也没拿到大多数(由于每台follower的定时器时间是随机的,因此该情况发生概率很小,且发生后也能很快回归正常)。于是会进入term4。
leader需要把写日志同步到大多数follower后才能更新状态数据库,并向client回复写成功。如果没有得到多数follower的成功应答,leader会重复发送这条日志更新请求。下图中有8条日志3个任期5个节点,每条日志里除记录了操作行为外还记录了当时的任期:
上图中,绿色是第一个任期,其中3条日志条目中第3条日志y=9没有被第4个节点接收到。黄色是第2个任期。绿色与黄色任期内,所有的日志皆被多数节点收到,因此都是写入状态数据库的,这些日志的状态都是commited已提交状态。蓝色是第3个任期,其第8条日志x=4没有被多数节点收到,因此该日志不是committed状态。
leader与 follower之间的日志也可能存在不一致的情况,follower或者少了一些日志,或者多了一些日志,如下图所示:
上图中最上面一行是leader的日志,而follower的日志存在以下情况:
leader如果确定多数机器收到日志,自然可以提交。如果新leader刚被选出来,它会试图把多数机器上保存的日志(即使它自己没有这条日志)–也就是前任的日志也提交,但这未必保证一定成功,如下图所示:
在上图中,d和e就是提交前任日志努力下可能导致的两种状况:
通常我们把raft集群配置为3或者5个节点,特别是5个节点时可以容忍2个节点宕机。这给我们平滑升级时带来了好处:1台台升级时仍然可以容忍1台宕机。但若我们的集群原来是3个节点的组合,却改为5个节点,如果这个过程是不停止服务动态完成的,这可能出现问题,如下图所示:
在上图中,绿色的老配置只有1、2、3这三台server组成集群,而在蓝色的新配置里则在1、2、3、4、5这五台server组成的新集群。于是,存在红色箭头指标的点,在该点上,可能1、2这两台server根据老配置在它们2个中选出第1个leader,而3、4、5根据新配置在它们3个中选出了第2个leader。同一时刻出现了2个leader,这样数据就会不一致。
为了解决上述问题,Raft提出了一个共同一致状态,该状态处于老配置和新配置生效的中间阶段。首先,我们设C(old)为老配置,而新配置为C(new),欲从C(old)状态置C(new),必须经历C(old,new)状态。其中,更新到C(old,new)以及C(new)时,仍然以复制日志的方式进行,即:先进行日志复制,当确定多数节点收到该日志后,则该日志为commited已提交状态。如下图所示:
从上图中可以看到:
可以看到,Raft算法的核心就是leader选举以及日志复制。而日志的无限增长,必然带来性能问题,这是从工程角度必须解决的问题。日志表示的是过程,状态数据库表示的是结果;同样,我们可以定期把某一时间点之前的日志做成状态数据库,或者称为快照,仅保留该时间点后的日志,这样就可以大幅减少日志的数量。如下图所示:
在上图中,原先的已经被提交的5条日志最终导致的状态是x=0&&y=9,故可以被快照替代,这便减少了日志量。
Raft还有一个非常形象的算法演示动画,包含了一致性算法的由来、leader的选举、隔离网络下的leader选举、日志的复制等场景,请打开RaftUnderstandable Distributed Consensus链接观看。
学习Raft算法有助于我们理解分布式环境下的一致性解决方案,而且它确实比paxos好理解许多,可以作为我们的入门算法。
(转载本站文章请注明作者和出处 陶辉笔记 ,请勿用于任何商业用途)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。