一致性协议raft详解(三):raft中的消息类型

本文详细介绍了Raft一致性算法中的节点角色(Follower、Candidate、Leader)、日志同步过程以及快照机制。在选举过程中,节点根据term值和日志的新旧来决定投票。Leader通过AppendEntries RPC进行日志同步,并维护每个节点的nextIndex和matchIndex。当日志达到多数派提交条件时,Leader会推进commitIndex。快照用于压缩日志,防止存储空间耗尽,创建快照时需与常规操作并发进行,确保服务可用性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

有关一致性协议的资料网上有很多,当然错误也有很多。笔者在学习的过程中走了不少弯路。现在回过头来看,最好的学习资料就是Leslie LamportDiego Ongaro的数篇论文、Ongaro在youtube上发的三个视频讲解,以及何登成的ppt。

本系列文章是只是笔者在学习一致性协议过程中的摘抄和总结,有疏漏之处敬请谅解,欢迎讨论。

raft 节点

Raft算法中服务器有三种角色

  1. Follower
  2. Candidate
  3. Leader

每个服务器上都会存储的持久状态:

  1. currentTerm: 当前节点所能看到的最大的term值, 初始化为0并单调递增
  2. votedFor: 当前term里将票投给对象的candidateId, 如果尚未投票则为空(我实现时置为-1)
  3. log[]: 日志条目(每条日志条目包含命令和任期), 会按顺序作用于状态机, 第一个索引Index为1

每个服务器上都会存储的易失状态:

  1. commitIndex: 当前服务器已知已提交的最高的日志条目的索引(每次选举之后leader将其初始为0,单调递增)(这个代表了整个raft集群的最后一个index,根据figure8,这个参数有可能因为其他节点而被改变)
    1. 所谓的CommitIndex,就是已经达成多数派,可以应用的最新日志位置
  2. lastApplied: 当前服务器已经被应用到状态机的最高的日志条目的索引(初始值为0,单调递增)(这个参数代表了自己这个节点目前到底持久化了多少日志)

上面两个index只是索引,可能会有空挡,比如某个log entry没有commit上

在状态为Leader的服务器上会额外存储的易失状态:

  1. nextIndex[]: 针对每个其他节点, 下一个需要发送的日志条目的索引, 初始化为leader最后一个日志索引+1
  2. matchIndex[]: 针对每个其他节点, 当前所知的和Leader匹配的最高日志索引, 初始化为0并单调递增

Raft中RPC的种类

RequestVote

candidate节点请求其他节点投票给自己

请求参数:

  1. term: 当前candidate节点的term值
  2. candidateId: 当前candidate节点的编号
  3. lastLogIndex: 当前candidate节点最后一个日志的索引
  4. lastLogTerm: 当前candidate节点最后一个日志的term值

返回值:

  1. term: 接受投票节点的term值, 主要用来更新当前candidate节点的term值
  2. voteGranted: 是否给该申请节点投票

一个节点(无论当前是什么状态)在接收到RequestVote(term, candidateId, lastLogIndex, lastLogTerm)消息时, 其会做如下判断:

  1. 如果参数携带的term < currentTerm, 则返回currentTerm并拒绝投票请求: (currentTerm, false), 并保持当前节点状态不变
  2. 如果当前term voteFor=null,做以下检查:
    1. 如果参数携带的term > currentTerm
      1. leader会stepdown,并且提升term,然后重新选主(这点可以通过Leader Stickiness进行优化)
      2. follower会拒绝leader的请求,提升term,然后重新选主
      3. 经过以上的过程之后,节点仍需要将request lastLogIndex和自己的最后一条日志的index进行比较(leader就是最后一条日志(比如lastapplied或者最后一个log的index),follower就是commitIndex),确保candidate节点的日志至少和自己一样新,才可以同意RequestVote RPC
    2. 如果参数携带的term = currentTerm,直接判断candidate的日志是否至少和自己一样新,如果是则同意RequestVote RPC
leader选举成功后

领导人:

  • 一旦成为领导人:发送空的附加日志 RPC(心跳)给其他所有的服务器;在一定的空余时间之后不停的重复发送,以阻止follower超时(5.2 节)
  • 如果接收到来自客户端的请求:附加条目到本地日志中,在条目被应用到状态机后响应客户端(5.3 节)
  • 如果对于一个follower,如果leader发现自己的最后日志条目的索引值大于等于 nextIndex,那么:发送从 nextIndex 开始的所有日志条目:
    • 如果成功:更新相应follower的 nextIndex 和 matchIndex
    • 如果因为日志不一致而失败,减少 nextIndex 重试
  • 如果存在一个满足N > commitIndex的 N,并且大多数的matchIndex[i] ≥ N成立,并且log[N].term == currentTerm成立,那么令 commitIndex 等于这个 N (5.3 和 5.4 节) (figure 8),这样的话,leader就可以把漏下的日志补上
    • 之所以这么做,是因为在新的leader选举的过程中,老的leader是可以继续生效的,那么也就导致新的leader可能确实了一部分老leader最后commit的日志,或者network partition了,某个节点的term很大,导致其一定是主,但是这个主上有很多漏掉的leader

AppendEntries

leader节点使用该消息向其他节点同步日志, 或者发送空消息作为心跳包以维持leader的统治地位

请求参数
  1. term: 当前leader节点的term值
  2. leaderId: 当前leader节点的编号(注:follower根据领导者id把客户端的请求重定向到领导者,比如有时客户端把请求发给了follower而不是leader)
  3. prevLogIndex: 当前发送的日志的前面一个日志的索引
  4. prevLogTerm: 当前发送的日志的前面一个日志的term值 (这个和上一个作用是follower日志有效性检查)
  5. entries[]: 需要各个节点存储的日志条目(用作心跳包时为空, 可能会出于效率发送超过一个日志条目)
  6. leaderCommit: 当前leader节点最高的被提交的日志的索引(就是leader节点的commitIndex)
返回值
  1. term: 接收日志节点的term值, 主要用来更新当前leader节点的term值
  2. success: 如果接收日志节点的log[]结构中prevLogIndex索引处含有日志并且该日志的term等于prevLogTerm则返回true, 否则返回false

一个节点(无论当前是什么状态)接收到AppendEntries(term, leaderId, prevLogIndex, prevLogTerm, entries[], leaderCommit)消息时, 其会做如下判断(条件从上往下依次判断):

  1. 如果参数携带的term < currentTerm, 则返回当前term并返回: (currentTerm, false), 并保持当前节点状态不变
  2. 如果参数携带的term >= currentTerm, 则设置currentTerm = term, voteFor = leaderId, 转换当前节点为Follower状态, 重置随机定时器, 进入下一步判断:
    1. 如果当前节点log[]结构中prevLogIndex索引处不含有日志, 则返回(currentTerm, false)
    2. 如果当前节点log[]结构中prevLogIndex索引处含有日志但该日志的term不等于prevLogTerm, 则返回(currentTerm, false)
    3. 如果当前节点log[]结构中prevLogIndex索引处含有日志并且该日志的term等于prevLogTerm, 则执行存储日志, 然后应用日志到状态机并返回(currentTerm, true)
    4. 以上三点说明了,log在一个节点上是顺序append的 (日志提交的顺序:先append再apply)
存储日志(日志同步过程)
  1. Leader上为每个节点维护NextIndex、MatchIndex,NextIndex表示待发往该节点的Entry index,MatchIndex表示该节点已匹配的Entry index,同时每个节点维护CommitIndex表示当前已提交的Entry index。转为Leader后会将所有节点的NextIndex置为自己最后一条日志index+1,MatchIndex全置0,同时将自身CommitIndex置0
  2. Leader节点不断将user_data转为Entry追加到日志文件末尾,Entry包含index、term和user_data,其中index在日志文件中从1开始顺序分配,term为Leader当前的term。
  3. Leader通过AppendEntry RPC将Entry同步到Followers,Follower收到后校验该Entry之前的日志是否已匹配。如匹配则直接写入Entry,返回成功;否则删除不匹配的日志,返回失败。校验是通过在AppendEntry RPC中携带待写入Entry的前一条entry信息完成。
  4. 当Follower返回成功时,leader更新对应节点的NextIndex和MatchIndex,继续发送后续的Entry。如果MatchIndex更新后,大多数节点的MatchIndex已大于CommitIndex,则更新CommitIndex。Follower返回失败时回退NextIndex继续发送,直到Follower返回成功。
  5. Leader每次AppendEntry RPC中会携带当前最新的LeaderCommitIndex,Follower写入成功时会将自身CommitIndex更新为Min(LastLogIndex,LeaderCommitIndex)。

leader会将commit index置为0 --> 大部分follower将commitindex推进之后 --> leader才会推进自己的commit index --> leader代表整个系统推进commit index

InstallSnapshot RPC

该rpc主要用于leader将集群的快照同步给其他节点。这里主要讲一下快照的机制:

本节主要参考文章条分缕析 Raft 算法(续):日志压缩和性能优化

log过多就需要做快照,最初设计 LogCabin 的时候没有考虑日志压缩,因此代码上假定了如果 entry i 在日志中,那么 entry 1 到 i - 1 也一定在日志中。有了日志压缩,这就不再成立了,前面的 entry 可能已经被丢弃了。

和配置变化不同,不同的系统有不同的日志压缩方式,取决于你的性能考量,以及基于硬盘还是基于内存。日志压缩的大部分责任都落在状态机上。

不同的压缩方法有几个核心的共同点:

  1. 不将压缩决定集中在 Leader 上,每个服务器独立地压缩其已提交的日志。这就避免了 Leader 将日志传递给已有该日志的 Follower,同时也增强了模块化,减少交互,将整个系统的复杂性最小化。(对于非常小的状态机,基于 Leader 的日志压缩也许更好。)
  2. 将之前的 log 的维护责任从 Raft 转移到状态机。Raft 要保存最后被丢弃的记录的index和term,用于 AppendEntries RPC一致性检查。同时,也需要保存最新的配置信息:成员变更失败需要回退配置,最近的配置必须保存。
  3. 一旦丢弃了前面部分的日志,状态机就承担两个新的责任:
    1. 如果服务器重启了,需要将最新的快照加载到状态机后再接受 log;此外,
    2. 需要向较慢的 follower(日志远落后于 Leader)发送一致的状态镜像。(InstallSnapshot RPC)

memory-based 状态机的快照的大部分工作是序列化内存中的数据结构。

快照的并发性

创建一个快照需要耗费很长时间,包括序列化和写入磁盘。**因此,序列化和写快照都要与常规操作并发进行,避免服务不可用。**copy-on-write 技术允许进行新的更新而不影响写快照。有两个方法来实现:

  • 状态机可以用不可变的(immutable)数据结构来实现。因为状态机命令不会 in-place 的方式来修改状态(通常使用追加的方式),快照任务可以引用之前状态的并把状态一致地写入到快照。
  • 另外,也可以使用操作系统的 copy-on-write。例如,在 Linux 上可以使用 fork 来复制父进程的整个地址空间,然后子进程就可以把状态机的状态写出并退出,整个过程中父进程都可以持续地提供服务。LogCabin中当前使用的就是这种方法。
快照实现以及何时做快照

服务器需要决定什么时候做快照。太过频繁地做快照,将会浪费磁盘带宽和其他资源太不频繁地做快照,则有存储空间耗尽的风险,并且重启服务需要更长的重放日志时间。

**一个简单的策略是设置一个阈值,当日志大小超过阈值则做快照。**然而,这会导致对于小型状态机时有着不必要的大日志。

一个更好的方法是引入快照大小和日志大小的对比,如果日志超过快照好几倍,可能就需要做快照。但是在做快照之前计算快照的大小是困难并且繁重的,会引入额外负担。所以使用前一个快照的大小是比较合理的行为,一旦日志大小超过之前的快照的大小乘以扩展因子(expansion factor),服务器就做快照。

这个扩展因子权衡空间和带宽利用率。例如,扩展因子为 4 的话会有 20% 的带宽用于快照(每1byte 的快照写入有对应的 4bytes 的 log 写入)和大约 6 倍的硬盘空间使用(旧的快照+日志+新的快照)。

快照仍然会导致 CPU 和磁盘的占用率突发,可以增加额外的磁盘来减轻该现象。

**同时,可以通过调度使得做快照对客户端请求没有影响。**服务器需要协调保证在某一时刻集群只有小部分成员集同时在做快照。由于 Raft 是多数派成员构成的 commit,所以这样就不会影响请求的提交了。当 Leader 想做快照的时候,首先要先下台,让其他服务器选出另一个 Leader 接替工作。如果这个方法充分地可行,就可能消除快照的并发,服务器在快照期间其实是不可用的(这可能会造成集群的容错能力降低的问题)。这是一个令人兴奋的提升集群性能并降低实现机制的机会。(这里其实可以通过实现指定服务器做快照来优化,braft 里就有提到这点。

快照实现

根据log的实现方式不同(分为memory-based和disk-based),快照也有不同的实现方式

disk-based

对于几十或上百 GB 的状态机,需要使用磁盘作为主要存储。对于每一条记录,当其被提交并应用到状态机后,其实就可以被丢弃了,因为磁盘已经持久化存储了,可以理解为每条日志就做了一个快照。

Disk-based 状态机的主要问题是,磁盘会导致性能不佳。在没有写缓冲的情况下,每应用一条命了都需要进行一次或多次随机磁盘写入,这会限制系统的整体吞吐量。

Disk-based 状态机仍然需要支持向日志落后的 Follower 提供最新的快照,而写快照也要继续提供服务,所以仍然需要 copy-on-write 技术以在一定期间内保持一个一致地快照传输。幸运的是,磁盘总是被划分为逻辑块,因此在状态机中实现应该是直接的。基于磁盘的状态机也可以依靠操作系统的支持,例如 Linux 的 LVM 也可以用来创建快照。或者是使用系统的COW支持,Linux的fork,或者是ZFS的Snapshot等。

memory-based

memory-based日志主要有Log-structured File System 或 LSM tree方式做快照

参考链接

分布式计算是计算机科学中一个研究方向,它研究如何把一个需要非常巨大的计算能力才能解决的问题分成许多小的部分,然后把这些部分分配给多个计算机进行处理,zui后把这些计算结果综合起来得到zui终的结果。分布式网络存储技术是将数据分散地存储于多台独立的机器设备上。分布式网络存储系统采用可扩展的系统结构,利用多台存储服务器分担存储负荷,利用位置服务器定位存储信息,不但解决了传统集中式存储系统中单存储服务器的瓶颈问题,还提高了系统的可靠性、可用性和扩展性。 分布式计算与互联网的普及随着计算机的普及,个人电脑开始进入千家万户。与之伴随产生的是电脑的利用问题。越来越多的电脑处于闲置状态,即使在开机状态下CPU的潜力也远远不能被完全利用。我们可以想象,一台家用的计算机将大多数的时间花费在“等待”上面。即便是使用者实际使用他们的计算机时,处理器依然是寂静的消费,依然是不计其数的等待(等待输入,但实际上并没有做什么)。互联网的出现, 使得连接调用所有这些拥有闲置计算资源的计算机系统成为了现实。  分布式计算项目那么,一些本身非常复杂的但是却很适合于划分为大量的更小的计算片断的问题被提出来,然后由某个研究机构通过大量艰辛的工作开发出计算用服务端和客户端。服务端负责将计算问题分成许多小的计算部分,然后把这些部分分配给许多联网参与计算的计算机进行并行处理,zui后将这些计算结果综合起来得到zui终的结果。 当然,这看起来也似乎很原始、很困难,但是随着参与者和参与计算的计算机的数量的不断增加, 计算计划变得非常迅速,而且被实践证明是的确可行的。一些较大的分布式计算项目的处理能力已经可以达到甚而超过世界上速度zui快的巨型计算机。 您也可以选择参加某些项目以捐赠的 CPU内核处理时间,您将发现您所提供的 CPU 内核处理时间将出现在项目的贡献统计中。您可以和其他的参与者竞争贡献时间的排名,您也可以加入一个已经存在的计算团体或者自己组建一个计算小组。这种方法很利于调动参与者的热情。  参与计算随着民间的组队逐渐增多, 许多大型组织(例如公司、学校和各种各样的网站)也开始了组建自己的战队。同时,也形成了大量的以分布式计算技术和项目讨论为主题的社区,这些社区多数是翻译制作分布式计算项目的使用教程及发布相关技术性文章,并提供必要的技术支持。 那么谁可能加入到这些项目中来呢? 当然是任何人都可以! 如果您已经加入了某个项目,而且曾经考虑加入计算小组, 您将在中国分布式计算总站及论坛里找到您的家。任何人都能加入任何由我站的组建的分布式计算小组。希望您在中国分布式总站及论坛里发现乐趣。 参与分布式计算——一种能充分发挥您的个人电脑的利用价值的zui有意义的选择——只需要下载有关程序,然后这个程序会以zui低的优先度在计算机上运行,这对平时正常使用计算机几乎没有影响。如果你想利用计算机的空余时间做点有益的事情,还犹豫什么?马上行动起来吧,你的微不足道的付出或许就能使你在人类科学的发展史上留下不小的一笔呢。 raft算法之所以容易理解,其一是他将一致性问题划分成几个子问题,这几个子问题都是独立、可理解和解释的。从传统的思维来讲,对于一个复杂的系统或者工程,都是大化小,分解实现,然后去尝试融合解决整体逻辑。包括CS系统的设计也是如此。 一致性算法的目标 1.安全性:在非拜占庭错误情况下,包括网络延迟、分区、丢包、冗余和乱序等错误都可以保证正确。2.可用性:只要集群中大多数节点处于runing,并且不分区,和客户端能通信,那么我们需要保证这个集群可用。3.对于数据同步,小部分慢节点的不会影响系统性能。因为对于日志复制,我们如果等待所有节点响应,那么系统的性能会存在短板效应。 说白了,就是如果一个集群中,如果大多数节点可用(网络、服务),那么通过raft算法,我们就能保证整个系统可用(可处理请求,数据一致性)。后面我们主要研究的就是raft是如何做到的。首先我们要知道,Raft算法将其问题划分为 领导选举 日志复制 安全性 对于一个集群只有一个leader(领导),那么我们就很容易理解。只要领导操作同步到对应的followers(跟随者),数据必然一致。当leader宕机,需要进行领导选举。 日志复制其实就是同步操作数据的过程。leader将操作日志同步到其他节点。安全性:如何安全的同步,在不同的情况,我们都能保证一致性,这也就是安全性需要考虑的问题。 其实就是如此,raft首先假设了领导选举。然后实现了日志复制,zui后在安全问题上解决上面的漏洞问题。 
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值