初步学习etcdraft
1 何为raft?为什么我们要学习etcdraft?
介绍raft前,我们先说说Paxos,Paxos与raft一样,都是一致性算法。但是由于Paxos算法协议难以理解,让很多程序员学起来很困难,且开发者难以去扩展。那么raft又如何呢?raft也可以保证强一致性,且很容易让人理解。让很多程序员甚是喜欢。接下来介绍raft的神秘之处。
2 raft怎么去保证一致性?
自然是通过复制状态机,通常情况下都是使用日志实现,去保持复制日志的一致性。每台服务器上的一致性模块接收来自客户端的命令,并将它们添加到其日志中。 它与其他服务器上的一致性模块通信,以确保每个日志最终以相同的顺序包含相同的命令,即使有一些服务器失败。 一旦命令被正确复制,每个服务器上的状态机按日志顺序处理它们,并将输出返回给客户端。 这样就形成了高可用的复制状态机。
- 在保证没有网络延迟,分区胡总和数据丢失,重复乱序等情况下的安全性
- 保证过半的服务器可以运行,保证服务器与客户端之间能狗互相通信。当服务器宕机时,能够稍后恢复过来并且加入集群。
- 它们不依赖于时序来确保日志的一致性:错误的时钟和极端消息延迟可能在最坏的情况下导致可用性问题。
- 在通常情况下,只要集群的大部分已经响应了单轮远程过程调用,命令就可以完成; 少数(一半以下)慢服务器不需要影响整个系统性能。
附带Raft的动画图网址:
链接: http://thesecretlivesofdata.com/raft/
3 leader选举
通常我们建立raft的服务节点都是奇数个【防止出现脑裂、资源浪费】
下图展示了这些状态和他们之间的转换关系。
3.1 任期term:
保证leader的单一,raft它将选举时间分割成任期term时间。在一段term时间内candidate进行选举时,如果candidate赢得选举成为leader,将在剩下的term时间内作为leader。但如果在term内没有选举上leader,将继续下一个term任期时间开始【新的leader选举也随之开始】。所以raft保证了在term内最多只能有一个leader。
Raft 算法中服务器节点之间使用 RPC 进行通信,提供了三种。
- 请求投票(RequestVote)
- 追加条目(AppendEntries)
- 快照(Snapshot)
Raft通过心跳机制来选举leader。在leader的角度来看,leader会周期性地向follower发送心跳来证明自己还存在。在follower的角度来看,如果在选举超时时间内没有接受到任何消息,就会假设没有leader,该follower就会变成candidate。开始RequestVote操作。
3.2 RequestVote
当follower准备选举之前,它会先增加自己当前的任期号再转到candidate状态。然后先投票给自己并且向集群的塔器服务器发送RequestVoteRPC操作,直到保持以下三个事情其中之一发生为止。
- 它自己收到过半的投票成为了新的leader
- 其他服务器成为了leader
- 一段时间之后没有任何获胜者。
当有一个candidate获取过半的投票机会成为新的leader。因为要过半的投票,所以保证了只有一个candidate成为新的leader。(选举的安全性),一旦成为新的leader,就会发送心跳消息去阻止别的节点成为leder,其他节点将再次成为follower。在对于同一个term,每个服务器节点只会投给一个 candidate ,按照先来先服务(first-come-first-served)的原则。
那么会有个问题,如果在candidate收到leader【又可能是其他的candidate抢先成为leader】发来的AppendEntries RPC,candidtion会怎么做?
将会比较两者的任期号,如果candidate的任期号比leader的大,将会拒绝这次PRC的请求。反之,则会退回follower。
那如果多个candidate再抢票,都没有赢得过半的投票怎么办?那么就会超时。然后通过增加任期号来进行下一轮term。可能就会直到出现leader为止
所以为了出现以上的情况,Raft算法使用随机选举超时时间来确保出现没有leader出现的情况,超时时间是从一个固定的区间[150,300]毫秒,可以自行配置。目的就是为了把各个candiate选举时间分开来,这样一来就可以更有效的防止第三种情况。
4 日志复制
在客户端的每一个请求都包含一条被复制状态机的指令。leader把指令作为日志条目加到日志中去,然后发起AppendEntriesPRC给其他服务器,让其他复制该条目。leader 会应用该条目到它的状态机中(状态机执行该指令)然后把执行的结果返回给客户端。如果 follower 崩溃或者运行缓慢,或者网络丢包,领导人会不断地重试 AppendEntries RPC(即使已经回复了客户端)直到所有的 follower 最终都存储了所有的日志条目。
日志以下图展示的方式组织。每个日志条目存储一条状态机指令和 leader 收到该指令时的任期号。任期号用来检测多个日志副本之间的不一致情况,同时也用来保证选举中的某些性质。每个日志条目都有一个整数索引值来表明它在日志中的位置。
Leader 决定什么时候把日志条目应用到状态机中是安全的;这种日志条目被称为已提交的。Raft 算法保证所有已提交的日志条目都是持久化的并且最终会被所有可用的状态机执行。一旦创建该日志条目的 leader 将它复制到过半的服务器上,该日志条目就会被提交(例如在上图中的条目 7)leader 日志中该日志条目之前的所有日志条目也都会被提交,包括由其他 leader 创建的条目。 Leader 追踪将会被提交的日志条目的最大索引,未来的所有 AppendEntries RPC 都会包含该索引,这样其他的服务器才能最终知道哪些日志条目需要被提交,Follower 一旦知道某个日志条目已经被提交就会将该日志条目应用到自己的本地状态机中(按照日志的顺序)。
4.1日志匹配特性
raft为了维持不同服务器之间的日志一致性与安全性。raft有以下两个特性
- 如果不同日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令。
- 如果不同日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也都相同。
Leader 在特定的任期号内的一个日志索引处最多创建一个日志条目,同时日志条目在日志中的位置也从来不会改变。该点保证了上面的第一条特性。第二个特性是由 AppendEntries RPC 执行一个简单的一致性检查所保证的。在发送 AppendEntries RPC 的时候,leader 会将前一个日志条目的索引位置和任期号包含在里面。如果 follower 在它的日志中找不到包含相同索引位置和任期号的条目,那么他就会拒绝该新的日志条目。
4.2 leader崩溃
正常操作期间,leader与follower日志保持一致,AppendEntriesRPC的一致性检查从来不会失败。但是leader崩溃的情况会使日志处于不一致的状态。会出现follower与leader日志不同。可能会出现follower少leader的日志条目,或者leader少follower日志条目。
在Raft算法中,leader通过强行follower复制自己的日志来解决日志不一致的问题。这也是唯一一个删除日志条目的地方
那么Raft是如何保持leader与follower之间的日志一致的呢?【Leader Append-Only 属性】
首先,leader必须找到两种达成一致的最大日志条目【也就是索引值】,然后删除掉之后所有不同日志条,follower再追加leader的日志条目,保证两者日志条目一致。所有的操作都发生在对AppendEntries RPCs 中一致性检查的回复中。leader针对所有follower维护着一个nextIndex,表示 leader 要发送给 follower 的下一个日志条目的索引。当选出一个新 leader 时,该 leader 将所有 nextIndex 的值都初始化为自己最后一个日志条目的 index 加1。如果follower与leader日志不一致,那么AppendEntriesPRC一致性检查就会失败。在follower每一次拒绝之后,nextIndex就会减一,然后再次重试AppendEntries RPC 。直到nextIndex在某个位置一致,将之后的不一致全部删除,然后再追加leader之后的日志条目,保持一致。AppendEntries RPC 成功,follower 的日志就和 leader 一致,并且在该任期接下来的时间里保持一致。【以上图leader与(f)为例】
优化:
如果想要的话,该协议可以被优化来减少被拒绝的 AppendEntries RPC 的个数。例如,当拒绝一个 AppendEntries RPC 的请求的时候,follower 可以包含冲突条目的任期号和自己存储的那个任期的第一个 index 。借助这些信息,leader 可以跳过那个任期内所有冲突的日志条目来减小 nextIndex;这样就变成每个有冲突日志条目的任期需要一个 AppendEntries RPC 而不是每个条目一次。在实践中,我们认为这种优化是没有必要的,因为失败不经常发生并且也不可能有很多不一致的日志条目。
这样一来就可以保证leader不用删除日志条目,也能保证与follower日志条目相同。也就是AppendEntries里的一致性检查。
4.3 提交之前任期内的日志条目
还有一些特殊情况:
当前任期内的某个日志条目已经存储到过半的服务器节点上,leader 就知道该日志条目已经被提交了。如果某个 leader 在提交某个日志条目之前崩溃了,以后的 leader 会试图完成该日志条目的复制。然而,如果是之前任期内的某个日志条目已经存储到过半的服务器节点上,leader 也无法立即断定该日志条目已经被提交了。下图展示了一种情况,一个已经被存储到过半节点上的老日志条目,仍然有可能会被未来的 leader 覆盖掉。
如图的时间序列展示了为什么 leader 无法判断老的任期号内的日志是否已经被提交。在 (a) 中,S1 是 leader ,部分地复制了索引位置 2 的日志条目。在 (b) 中,S1 崩溃了,然后 S5 在任期 3 中通过 S3、S4 和自己的选票赢得选举,然后从客户端接收了一条不一样的日志条目放在了索引 2 处。然后到 ©,S5 又崩溃了;S1 重新启动,选举成功,继续复制日志。此时,来自任期 2 的那条日志已经被复制到了集群中的大多数机器上,但是还没有被提交。如果 S1 在 (d) 中又崩溃了,S5 可以重新被选举成功(通过来自 S2,S3 和 S4 的选票),然后覆盖了他们在索引 2 处的日志。但是,在崩溃之前,如果 S1 在自己的任期里复制了日志条目到大多数机器上,如 (e) 中,然后这个条目就会被提交(S5 就不可能选举成功)。 在这种情况下,之前的所有日志也被提交了。
因为有这种情况发生,所以raft永远不会1计算副本数目的方式来提交之前任期内的日志条目。只有 leader 当前任期内的日志条目才通过计算副本数目的方式来提交;一旦当前任期的某个日志条目以这种方式被提交,那么由于日志匹配特性,之前的所有日志条目也都会被间接地提交。在某些情况下,领导人可以安全地断定一个老的日志条目已经被提交(例如,如果该条目已经存储到所有服务器上)
所以raft不会删除原来的任期号。
通过 Leader Completeness 特性,我们就能证明状态机安全特性,即如果某个服务器已经将某个给定的索引处的日志条目应用到自己的状态机里了,那么其他的服务器就不会在相同的索引处应用一个不同的日志条目。在一个服务器应用一个日志条目到自己的状态机中时,它的日志和 leader 的日志从开始到该日志条目都相同,并且该日志条目必须被提交。现在考虑如下最小任期号:某服务器在该任期号中某个特定的索引处应用了一个日志条目;日志完整性特性保证拥有更高任期号的 leader 会存储相同的日志条目,所以之后任期里服务器应用该索引处的日志条目也会是相同的值。因此,状态机安全特性是成立的。
最后,Raft 要求服务器按照日志索引顺序应用日志条目。再加上状态机安全特性,这就意味着所有的服务器都会按照相同的顺序应用相同的日志条目到自己的状态机中。
4.4 Follower 和 candidate 崩溃
两者的处理方式相同,如果两者都崩溃了,那么RequestVote 和 AppendEntries RPCs 都会失败。Raft会通过无限重试来处理这种失败。如果他们重启了,PRC就会成功完成,如果它成功完成了PRC,这时又崩溃了没来得及响应,那它重启之后还会再次收到同样的请求,直到响应成功。Raft 的 RPCs 都是幂等的,一个 follower 如果收到 AppendEntries 请求但是它的日志中已经包含了这些日志条目,它就会直接忽略这个新的请求中的这些日志条目。
4.5 定时时间的完善(timing)
Raft 的要求之一就是安全性不能依赖定时。例如,当有服务器崩溃时,消息交换的时间就会比正常情况下长,candidate 将不会等待太长的时间来赢得选举;没有一个稳定的 leader ,Raft 将无法工作。所以leader有个稳定的时间规定:
广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障间隔时间(MTBF)
广播时间和平均故障间隔时间是由系统决定的,但是选举超时时间是我们自己选择的。Raft 的 RPCs 需要接收方将信息持久化地保存到稳定存储中去,所以广播时间大约是 0.5 毫秒到 20 毫秒之间,取决于存储的技术。因此,选举超时时间可能需要在 10 毫秒到 500 毫秒之间。大多数的服务器的平均故障间隔时间都在几个月甚至更长,很容易满足时间的要求。
5 快照(Snapshot)
优点:节省空间内存,且让太落后的follower可以追得上leader
缺点:第一,发送快照会浪费网络带宽并且延缓了快照过程。每个 follower 都已经拥有了创建自己的快照所需要的信息,而且很显然,follower 从本地的状态中创建快照远比通过网络接收别人发来的要来得经济。第二,leader 的实现会更加复杂。例如,leader 发送快照给 follower 的同时也要并行地将新的日志条目发送给它们,这样才不会阻塞新的客户端请求。
首先,服务器必须决定什么时候创建快照。如果快照创建过于频繁,那么就会浪费大量的磁盘带宽和其他资源;如果创建快照频率太低,就要承担耗尽存储容量的风险,同时也增加了重启时日志回放的时间。一个简单的策略就是当日志大小达到一个固定大小的时候就创建一次快照。如果这个阈值设置得显著大于期望的快照的大小,那么快照的磁盘带宽负载就会很小。
第二个性能问题就是写入快照需要花费一段时间,并且我们不希望它影响到正常的操作。解决方案是通过写时复制的技术,这样新的更新就可以在不影响正在写的快照的情况下被接收。例如,具有泛函数据结构的状态机天然支持这样的功能。
raft采用的方法是写时复制的snapshot(写是复制在linux中可以通过fork来完成)来防止log过长或日志回放时间过长。
引用/转载 : https://www.jianshu.com/p/2a2ba021f721
https://www.jianshu.com/p/3c6a4fd6a7cc