raft学习笔记

  在被paxos虐了两周之后,再一次来阅读raft,突然感觉茅塞顿开的感觉,感觉写的真是贴心的细致的感觉,所有之前想到的问题这里都能找到答案,开心,raft的官方原文已经写的很好了,中文翻译也写的很棒,如果想要了解详情还是直接阅读原文就好,我这里仅仅算是类似学生时代的一个读后感吧,记录一些自己在学习官方文档的时候因为自己知识背景不够造成的一些理解上的困难点。
  如果要类比paxos和raft的话,我觉得可以这样说: paxos就是相对论中的质能转换方程,raft则是如何造出原子弹的一系列系统工程,原子弹的诞生必要条件是质能转换方程的正确性,但是仅仅有质能转化方程还远远不够。raft和paxos的本质支撑也是共识,多个节点之间要达成共识(在raft中文中翻译成了一致性,需要注意一下)

1. 复制状态机

  一致性算法是从复制状态机的背景下提出的。在这种方法中,一组服务器上的状态机产生相同状态的副本,并且在一些机器宕掉的情况下也可以继续运行。复制状态机在分布式系统中被用于解决很多容错的问题。例如,大规模的系统中通常都有一个集群领导者,像 GFS、HDFS 和 RAMCloud,典型应用就是一个独立的的复制状态机去管理领导选举和存储配置信息并且在领导人宕机的情况下也要存活下来。比如 Chubby 和 ZooKeeper就可以认为是一个复制状态机。复制状态机就是一组状态始终保持一致的状态机。
图1

  复制状态机通常都是基于复制日志实现的,如上图。每一个服务器存储一个包含一系列指令的日志,并且按照日志的顺序进行执行。每一个日志都按照相同的顺序包含相同的指令,所以每一个服务器都执行相同的指令序列。因为每个状态机都是确定的,每一次执行操作都产生相同的状态和同样的序列。

  其实这里理解状态机就是一个应用存储的状态,比如MYSQL的master/slave,每个mysql实例都是一个状态机,然后状态机之间通过binlog进行复制同步,如果master和slave能够保持一致,那么可以称为复制状态机,MYSQL实际上不是哈,因为他没有满足容错一致性,在master宕掉后有可能导致数据不一致,这中间的原因就是没有保证日志复制的一致性。

  保证复制日志相同就是一致性算法的工作了。在一台服务器上,一致性模块接收客户端发送来的指令然后增加到自己的日志中去。它和其他服务器上的一致性模块进行通信来保证每一个服务器上的日志最终都以相同的顺序包含相同的请求,尽管有些服务器会宕机。一旦指令被正确的复制,每一个服务器的状态机按照日志顺序处理他们,然后输出结果被返回给客户端。因此,服务器集群看起来形成一个高可靠的状态机。

  这就是复制状态机的概念,这里的日志并不是程序运行日志,而是和binlog类似,代表了用户的一个个操作。每个状态机都执行已经提交的日志,然后让状态机的状态保持一致,然后数据系统堆外提供线性一致性的服务。所以一定要认识到,最终维护的是状态机的一致性,而且状态机是一个抽象的数据模型,可以代表任意的数据存储结构,只要他满足状态机的定义。

  所以在raft日志复制完成后分为commit和apply两个步骤,commit只是代表日志被复制到集群中的大多数了,完成了持久化,可以commit了,apply则是将这个commit的日志应用到状态机中,一个日志只有先完成commit才能够被applied。

  通过这个也可以看出,raft算法实际上更算是构建一个线性一致性的分布式存储系统的理论指导和详细设计,而paxos是更基础的,想要表达共识的概念,比如说构建分布式的事务也是需要共识的,这个是多个节点的共识需求,但是这种需求使用raft来进行解释的话可能有些困难,但是使用paxos来解释的话则相对轻松。感觉分布式存储是分布式算法的一个分支(这里都是个人的想法哈,慎重参考)。

2. raft特点概述

  要理解分布式算法,其实需要很多知识,因为分布式算法也是一个大家已经研究了很久的领域,所以是有很多的设计理念在里面的,raft是遵循这些设计理念来完成了其算法设计,其论文的表述很多时候都是对其遵循这些设计理念的阐述或者是证明,所以如果不了解分布式系统存在面临的问题等,也就很难理解raft的论文为什么要写这么些东西。多年前第一次看raft算法就是这样,那个时候也看了paxos和raft,其实基本上可以认为完全没有看懂😢,当时还以为自己看懂了一些😢,当时看raft的感觉就是他比paxos多了很多内容,而且这些内容好像都很有道理,但是却记不住,而且也无法关联起来。这次重新学习分布式相关的概念也看了不少相关的资料,可以先参考这篇来了解分布式算法的一些场景和问题。

  Raft 算法在许多方面和现有的一致性算法都很相似(主要是 Oki 和 Liskov 的 Viewstamped Replication),但是它也有一些独特的特性:

  1. 强领导者:和其他一致性算法相比,Raft 使用一种更强的领导能力形式。比如,日志条目只从领导者发送给其他的服务器。这种方式简化了对复制日志的管理并且使得 Raft 算法更加易于理解。
  2. 领导选举:Raft 算法使用一个随机计时器来选举领导者。这种方式只是在任何一致性算法都必须实现的心跳机制上增加了一点机制。在解决冲突的时候会更加简单快捷。
  3. 成员关系调整:Raft 使用一种共同一致的方法来处理集群成员变换的问题,在这种方法下,处于调整过程中的两种不同的配置集群中大多数机器会有重叠,这就使得集群在成员变换的时候依然可以继续工作。

  raft论文首先给出了一个使用raft构建的分布式存储一致性的系统的使用的数据模型以及在集群中通信使用的消息类型。然后他将系统的正常运行分为leader选举,log复制,集群节点变更三个过程,然后仔细的分析了在这个三个过程中可能存在的异常以及发生异常之后如何满足安全特性(safety)

3. raft的数据模型和消息类型

1. 服务器上存储的数据模型

数据模型包括持久化的和内存态的,他们的区别就是进程重启之后这些数据是否还存在

1. 所有服务器上的持久性状态 (在响应RPC请求之前 已经更新到了稳定的存储设备)
参数解释
currentTerm服务器已知最新的任期(在服务器首次启动的时候初始化为0,单调递增)
votedFor当前任期内收到选票的候选者id 如果没有投给任何候选者 则为空
log[]日志条目;每个条目包含了用于状态机的命令,以及领导者接收到该条目时的任期(第一个索引为1)
2. 所有服务器上的易失性状态
参数解释
commitIndex已知已提交的最高的日志条目的索引(初始值为0,单调递增),这里的commitIndex是指log已经被复制到了大多数的机器上,但是这个操作有可能还没有应用到状态机,只有commit之后的log才能被applied
lastApplied已经被应用到状态机的最高的日志条目的索引(初始值为0,单调递增)
3. 领导者(服务器)上的易失性状态 (选举后已经重新初始化)

主要存储每个follower上的日志状态,通过nextIndex,leader知道要发给每个follower的下一条log是什么,通过matchIndex,leader知道了当前能够提交的log index,也就是上面的的commitIndex。

参数解释
nextIndex[]对于每一台follower服务器,即将发送到该服务器的下一个日志条目的索引(初始值为领导者最后的日志条目的索引+1),用于向follower同步日志
matchIndex[]对于每一台follower服务器,已知的已经复制到该服务器的最高日志条目的索引(初始值为0,单调递增),通过对这个的统计可以得出可以commit的log的index

2. 集群通信消息模型

1. 追加条目RPC

被领导者调用 用于日志条目的复制 同时也被当做心跳使用

参数解释
term领导者的任期
leaderId领导者ID 因此跟随者可以对客户端进行重定向(译者注:跟随者根据领导者id把客户端的请求重定向到领导者,比如有时客户端把请求发给了跟随者而不是领导者)
prevLogIndex紧邻新日志条目之前的那个日志条目的索引
prevLogTerm紧邻新日志条目之前的那个日志条目的任期
entries[]需要被保存的日志条目(被当做心跳使用是 则日志条目内容为空;为了提高效率可能一次性发送多个)
leaderCommit领导者的已知已提交的最高的日志条目的索引

对应的响应

返回值解释
term当前任期,对于领导者而言 它会更新自己的任期
success结果为真 如果跟随者所含有的条目和preLogIndex以及preLogTerm匹配上了

处理逻辑

  1. 如果领导者者的任期 小于 接收者的当前任期(译者注:这里的接收者是指跟随者或者候选者),返回false
  2. 在接收者日志中 如果能找到一个和preLogIndex以及prevLogTerm一样的索引和任期的日志条目 则返回true 否则返回false
  3. 如果一个已经存在的条目和刚刚接收到的日志条目,发生了冲突(因为索引相同,任期不同),那么就删除这个已经存在的条目以及它之后的所有条目 (5.3 节)
  4. 追加日志中尚未存在的任何新条目
  5. 如果领导者的leaderCommit 大于 follower的commitIndex,则把 follower的commitIndex 重置为 min{leaderCommit, 当前follower接受玩log之后的最后的log index}, 取两者的最小值
2. 请求投票 RPC

由候选人负责调用用来征集选票

参数解释
term候选人的任期号
candidateId请求选票的候选人的 Id
lastLogIndex候选人的最后日志条目的索引值
lastLogTerm候选人最后日志条目的任期号

对应的响应

返回值解释
term当前任期号,以便于候选人去更新自己的任期号
voteGranted候选人赢得了此张选票时为真

处理逻辑

对于接受者来说

  1. 如果term < currentTerm返回 false
  2. 如果 votedFor 为空或者为 candidateId,并且候选人的日志至少和自己一样新,那么就投票给他

这里的votedFor在什么时候会被清空呢,是在term+1的时候会被清空么,在下面注意一下

4. 服务器在运行中需要遵循的通用规则和安全保证

依赖于上面的数据模型和消息,集群中的节点遵循下面的运行规则进行运行

1. 运行规则

1. 对所有服务器有效的规则
  1. 如果commitIndex > lastApplied,那么就 lastApplied 加一,并把log[lastApplied]应用到状态机中
  2. 如果接收到的 RPC 请求或响应中,任期号T > currentTerm,那么就令 currentTerm 等于 T,并切换状态为跟随者,这个时候votedFor应该会被重置,因为term变了,而且raft要求每个term每个node只能投票一次
2. follower需要遵循的规则
  1. 响应来自候选人和领导者的请求
  2. 如果在超过选举超时时间的情况之前没有收到当前领导人(即该领导人的任期需与这个跟随者的当前任期相同)的心跳/附加日志,而且没有收到其他候选人的投票请求,就自己变成候选人
3. candidate 候选人运行规则
  1. 在转变成候选人后就立即开始选举过程
    1. 自增当前的任期号(currentTerm)
    2. 给自己投票
    3. 重置选举超时计时器
    4. 发送请求投票的 RPC 给其他所有服务器
  2. 如果接收到大多数服务器的选票,那么就变成领导人
  3. 如果接收到来自新的领导人的附加日志 RPC,转变成跟随者
  4. 如果选举过程超时,再次发起一轮选举
4. 领导人的运行规则
  1. 一旦成为领导人:发送空的附加日志 RPC(心跳)给其他所有的服务器;在一定的空余时间之后不停的重复发送,以阻止跟随者超时(5.2 节)
  2. 如果接收到来自客户端的请求:附加条目到本地日志中,在条目被应用到状态机后响应客户端(5.3 节)
  3. 如果对于一个跟随者,最后日志条目的索引值大于等于 nextIndex,那么:发送从 nextIndex 开始的所有日志条目:
    1. 如果成功:更新相应跟随者的 nextIndex 和 matchIndex
    2. 如果因为日志不一致而失败,减少 nextIndex 重试
  4. 如果存在一个满足N > commitIndex的 N,并且大多数的matchIndex[i] ≥ N成立,并且log[N].term == currentTerm成立,那么令 commitIndex 等于这个 N (5.3 和 5.4 节),这个就是log的commit,而且是只提交当前term的log,在规则一中,之前term产生的log也会被applied到状态机当中。

2. 安全特性

通过以上数据模型和和消息机制,应用对应的运行规则,raft可以保证一下的安全特性

特性解释
选举安全特性对于一个给定的任期号,最多只会有一个领导人被选举出来
领导人只附加原则领导人绝对不会删除或者覆盖自己的日志,只会增加
日志匹配原则如果两个日志在相同的索引位置的日志条目的任期号相同,那么我们就认为这个日志从头到这个索引位置之间全部完全相同
领导人完全特性如果某个日志条目在某个任期号中已经被提交,那么这个条目必然出现在更大任期号的所有领导人中
状态机安全特性如果一个领导人已经将给定的索引值位置的日志条目应用到状态机中,那么其他任何的服务器在这个索引位置不会应用一个不同的日志

5. leader选举概述

  raft的容错能力是多数存活原则,比如5个节点的集群最多能够容忍2个节点失败。每一个服务器节点都处于这三个状态之一:领导人、跟随者或者候选人。在通常情况下,系统中只有一个领导人并且其他的节点全部都是跟随者。跟随者都是被动的:他们不会发送任何请求,只是简单的响应来自领导者或者候选人的请求。领导人处理所有的客户端请求(如果一个客户端和跟随者联系,那么跟随者会把请求重定向给领导人)。第三种状态,候选人,在选举新领导人时使用。
raft的状态转化图
在这里插入图片描述

  每个leader的工作的时间周期被划分成了一个一个的term,每个log也是和term关联的,每个当选的leader会在leader的任期里面保持一个term。每个term中也只能有一个leader,下面的选举逻辑摘自raft原文

  1. Raft 使用一种心跳机制来触发领导人选举。当服务器程序启动时,他们都是跟随者身份。一个服务器节点继续保持着跟随者状态只要他从领导人或者候选者处接收到有效的 RPCs。领导者周期性的向所有跟随者发送心跳包(即不包含日志项内容的附加日志项 RPCs)来维持自己的权威。如果一个跟随者在一段时间里没有接收到任何消息,也就是选举超时,那么他就会认为系统中没有可用的领导者,并且发起选举以选出新的领导者。

  2. 要开始一次选举过程,跟随者先要增加自己的当前任期号并且转换到候选人状态。然后他会并行的向集群中的其他服务器节点发送请求投票的 RPCs 来给自己投票。候选人会继续保持着当前状态直到以下三件事情之一发生:(a) 他自己赢得了这次的选举,(b) 其他的服务器成为领导者,© 一段时间之后没有任何一个获胜的人。这些结果会分别的在下面的段落里进行讨论。

  3. 当一个候选人从整个集群的大多数服务器节点获得了针对同一个任期号的选票,那么他就赢得了这次选举并成为领导人。每一个服务器最多会对一个任期号投出一张选票,按照先来先服务的原则,并且要满足候选者的日志至少和自己的一样新,同时一个候选者不会给其他的候选者投票,因为他已经给自己投过票了,vateFor中的id是自己的id 。要求大多数选票的规则确保了最多只会有一个候选人赢得此次选举(图 3 中的选举安全性)。一旦候选人赢得选举,他就立即成为领导人。然后他会向其他的服务器发送心跳消息来建立自己的权威并且阻止新的领导人的产生。

  4. 在等待投票的时候,候选人可能会从其他的服务器接收到声明它是领导人的附加日志项 RPC。如果这个领导人的任期号(包含在此次的 RPC中)不小于候选人当前的任期号,那么候选人会承认领导人合法并回到跟随者状态。 如果此次 RPC 中的任期号比自己小,那么候选人就会拒绝这次的 RPC 并且继续保持候选人状态。

  5. 第三种可能的结果是候选人既没有赢得选举也没有输:如果有多个跟随者同时成为候选人,那么选票可能会被瓜分以至于没有候选人可以赢得大多数人的支持。当这种情况发生的时候,每一个候选人都会超时,然后通过增加当前任期号来开始一轮新的选举。然而,没有其他机制的话,选票可能会被无限的重复瓜分。

1. 使用随机超时来避免导致活性丧失

  Raft 算法使用随机选举超时时间的方法来确保很少会发生选票瓜分的情况,就算发生也能很快的解决。为了阻止选票起初就被瓜分,选举超时时间是从一个固定的区间(例如 150-300 毫秒)随机选择。这样可以把服务器都分散开以至于在大多数情况下只有一个服务器会选举超时;然后他赢得选举并在其他服务器超时之前发送心跳包。同样的机制被用在选票瓜分的情况下。每一个候选人在开始一次选举的时候会重置一个随机的选举超时时间,然后在超时时间内等待投票的结果;这样减少了在新的选举中另外的选票瓜分的可能性。9.3 节展示了这种方案能够快速的选出一个领导人。

  领导人选举这个例子,体现了可理解性原则是如何指导我们进行方案设计的。起初我们计划使用一种排名系统:每一个候选人都被赋予一个唯一的排名,供候选人之间竞争时进行选择。如果一个候选人发现另一个候选人拥有更高的排名,那么他就会回到跟随者状态,这样高排名的候选人能够更加容易的赢得下一次选举。但是我们发现这种方法在可用性方面会有一点问题(如果高排名的服务器宕机了,那么低排名的服务器可能会超时并再次进入候选人状态。而且如果这个行为发生得足够快,则可能会导致整个选举过程都被重置掉)。我们针对算法进行了多次调整,但是每次调整之后都会有新的问题。最终我们认为随机重试的方法是更加明显和易于理解的。

2. 安全性分析

1. leader要包含最新的日志

  在任何基于领导人的一致性算法中,领导人都必须存储所有已经提交的日志条目。注意这里是已提交的,并不包含未提交的。
  Raft 使用投票的方式保证了只有这个候选人包含了所有已经提交的日志条目他才有可能赢得选举。候选人为了赢得选举必须联系集群中的大部分节点,这意味着每一个已经提交的日志条目在这些服务器节点中肯定存在于至少一个节点上。
  Raft 通过比较两份日志中最后一条日志条目的索引值和任期号定义谁的日志比较新。如果两份日志最后的条目的任期号不同,那么任期号大的日志更加新。如果两份日志最后的条目任期号相同,那么日志比较长的那个就更加新。

2. leader不会直接提交之前任期的日志

  这个异常还是很有意思的,在raft中分析的很好,新的leader总是使用直接提交当前term的日志从而间接提交之前term的日志的方式进行日志提交。也就是leader不会直接提交之前任期的日志,因为这样可能会导致一些异常。leader在选举成功之后会立刻发送一个包含空指令的log,只有这个log被commit之后,leader才能开始接收客户端的请求。

4. 跟随者失败

  到目前为止,我们都只关注了领导人崩溃的情况。跟随者和候选人崩溃后的处理方式比领导人要简单的多,并且他们的处理方式是相同的。如果跟随者或者候选人崩溃了,那么后续发送给他们的 RPCs 都会失败。Raft 中处理这种失败就是简单的通过无限的重试;如果崩溃的机器重启了,那么这些 RPC 就会完整的成功。如果一个服务器在完成了一个 RPC,但是还没有响应的时候崩溃了,那么在他重新启动之后就会再次收到同样的请求。Raft 的 RPCs 都是幂等的,所以这样重试不会造成任何问题。例如一个跟随者如果收到附加日志请求但是他已经包含了这一日志,那么他就会直接忽略这个新的请求。

6. 日志复制

  一旦一个领导人被选举出来,他就开始为客户端提供服务。客户端的每一个请求都包含一条被复制状态机执行的指令。领导人把这条指令作为一条新的日志条目附加到日志中去,然后并行的发起附加条目 RPCs 给其他的服务器,让他们复制这条日志条目。当这条日志条目被安全的复制(下面会介绍),领导人会应用这条日志条目到它的状态机中然后把执行的结果返回给客户端。如果跟随者崩溃或者运行缓慢,再或者网络丢包,领导人会不断的重复尝试附加日志条目 RPCs (尽管已经回复了客户端)直到所有的跟随者都最终存储了所有的日志条目。

  每一个日志条目存储一条状态机指令和从领导人收到这条指令时的任期号。日志中的任期号用来检查是否出现不一致的情况,每一条日志条目同时也都有一个整数索引值来表明它在日志中的位置。
领导人来决定什么时候把日志条目应用到状态机中是安全的;这种日志条目被称为已提交。Raft 算法保证所有已提交的日志条目都是持久化的并且最终会被所有可用的状态机执行。在领导人将创建的日志条目复制到大多数的服务器上的时候,日志条目就会被提交(例如在图 6 中的条目 7)。同时,领导人的日志中之前的所有日志条目也都会被提交,包括由其他领导人创建的条目。

  领导人跟踪了最大的将会被提交的日志项的索引,并且索引值会被包含在未来的所有附加日志 RPCs (包括心跳包),这样其他的服务器才能最终知道领导人的提交位置。一旦跟随者知道一条日志条目已经被提交,那么他也会将这个日志条目应用到本地的状态机中(按照日志的顺序)。

日志的复制特性满足

  1. 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令。
  2. 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也全部相同。

领导人从来不会覆盖或者删除自己的日志,在跟随者中的冲突的日志条目会被领导人的日志覆盖。

7. 集群成员变化

这一块儿感觉论文中说的相对比较模糊一些,我这边专门写了一篇文章进行分析,参看这里

8. 和client交互的时候实现线性一致性

  这一节将介绍客户端是如何和 Raft 进行交互的,包括客户端如何发现领导人和 Raft 是如何支持线性化语义的。这些问题对于所有基于一致性的系统都存在,并且 Raft 的解决方案和其他的也差不多。

  Raft 中的客户端发送所有请求给领导人。当客户端启动的时候,他会随机挑选一个服务器进行通信。如果客户端第一次挑选的服务器不是领导人,那么那个服务器会拒绝客户端的请求并且提供他最近接收到的领导人的信息(附加条目请求包含了领导人的网络地址)。如果领导人已经崩溃了,那么客户端的请求就会超时;客户端之后会再次重试随机挑选服务器的过程。

  我们 Raft 的目标是要实现线性化语义(每一次操作立即执行,只执行一次,在他调用和收到回复之间)。但是,如上述,Raft 是可以执行同一条命令多次的:例如,如果领导人在提交了这条日志之后,但是在响应客户端之前崩溃了,那么客户端会和新的领导人重试这条指令,导致这条命令就被再次执行了。解决方案就是客户端对于每一条指令都赋予一个唯一的序列号。然后,状态机跟踪每条指令最新的序列号和相应的响应。如果接收到一条指令,它的序列号已经被执行了,那么就立即返回结果,而不重新执行指令。

  只读的操作可以直接处理而不需要记录日志。但是,在不增加任何限制的情况下,这么做可能会冒着返回脏数据的风险,因为领导人响应客户端请求时可能已经被新的领导人作废了,但是他还不知道。线性化的读操作必须不能返回脏数据,Raft 需要使用两个额外的措施在不使用日志的情况下保证这一点。首先,领导人必须有关于被提交日志的最新信息。领导人完全特性保证了领导人一定拥有所有已经被提交的日志条目,但是在他任期开始的时候,他可能不知道哪些是已经被提交的。为了知道这些信息,他需要在他的任期里提交一条日志条目。Raft 中通过领导人在任期开始的时候提交一个空白的没有任何操作的日志条目到日志中去来实现。第二,领导人在处理只读的请求之前必须检查自己是否已经被废黜了(他自己的信息已经变脏了如果一个更新的领导人被选举出来)。Raft 中通过让领导人在响应只读请求之前,先和集群中的大多数节点交换一次心跳信息来处理这个问题。可选的,领导人可以依赖心跳机制来实现一种租约的机制,但是这种方法依赖时间来保证安全性(假设时间误差是有界的)。

9. 附一张脑图辅助记忆

在这里插入图片描述

raft可能存在的活性问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值