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

  • 前言
  • raft 节点
  • Raft中RPC的种类
    • RequestVote
      • leader选举成功后
    • AppendEntries
      • 请求参数
      • 返回值
        • 存储日志(日志同步过程)
    • InstallSnapshot RPC
      • 快照的并发性
      • 快照实现以及何时做快照
      • 快照实现
        • disk-based
        • memory-based
  • 参考链接

前言

有关一致性协议的资料网上有很多,当然错误也有很多。笔者在学习的过程中走了不少弯路。现在回过头来看,最好的学习资料就是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方式做快照

参考链接

  • MIT 6.824 Raft 设计文档

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

  1. 一致性协议-ChainPaxos详解

    一致性协议-ChainPaxos详解 一.背景 二.算法实现 流程概况 主流程实现细节 故障处理以及reconfiguration 选举 新增副本 线性读 三.总结 一.背景 该paxos变体的主要特 ...

  2. 一致性协议Paxos详解(一):Basic Paxos协议详解

    一致性协议Paxos详解(一):Basic Paxos协议详解 前言 Paxos是什么 Paxos算法原理与推导 Basic Paxos Proposal Numbers prepare阶段 prep ...

  3. CPU处理器一致性协议MESI详解

    CPU处理器缓存一致性协议MESI详解 缓存一致性的由来 MESI协议简介 Exclusive状态 Shared状态 Modified和Invalid状态 MESI状态切换 Modified状态跳转 ...

  4. 一致性协议raft详解(四):raft在工程实践中的优化

    一致性协议raft详解(四):raft在工程实践中的优化 前言 性能优化 client对raft集群的读写 参考链接 前言 有关一致性协议的资料网上有很多,当然错误也有很多.笔者在学习的过程中走了不少 ...

  5. 一致性协议raft详解(二):安全性

    一致性协议raft详解(二):安全性 前言 安全性 log recovery 为什么no-op能解决不一致的问题? 成员变更 Single mempership change raft用到的随机时间 ...

  6. 一致性协议raft详解(一):raft整体介绍

    一致性协议raft详解(一):raft介绍 前言 概述 raft独特的特性 raft集群的特点 raft中commit何意? raft leader election log replication ...

  7. Nacos如何实现Raft算法与Raft协议原理详解

    前言 大名鼎鼎的Paxos算法可能不少人都听说过,几乎垄断了一致性算法领域,在Raft协议诞生之前,Paxos几乎成了一致性协议的代名词.但是对于大多数人来说,Paxos算法太难以理解了,而且难以实现 ...

  8. MQTT协议详解 三、MQTT控制包(CONNECT)

    文章目录 系列文章目录 前言 CONNECT(客户端请求连接服务端) 一.固定包头(2字节) 二.可变包头(10字节) 协议名字(6字节) 协议等级(1字节) 连接标识(1字节) Clean Sess ...

  9. HTTPS协议详解(三):PKI 体系

    本文大部分内容摘自:http://www.wosign.com/faq/faq2016-0309-03.htm 尊重知识产权,转载注明Wosign -------------------------- ...

最新文章

  1. 为什么我们需要比特币(BCH)
  2. 每天学一点flash(71)折纸
  3. 【Android RTMP】Android Camera 视频数据采集预览 ( 视频采集相关概念 | 摄像头预览参数设置 | 摄像头预览数据回调接口 )
  4. 流媒体技术的应用与发展前景
  5. linux上samba+本地yum源最简单的配置
  6. Python学习入门基础教程(learning Python)--5.1 Python下文件处理基本过程
  7. android jni示例_Android CollapsingToolbarLayout示例
  8. bert模型可以做文本主题识别吗_BERT模型可以使用无监督的方法做文本相似度任务吗?...
  9. symantec BESR 异机恢复
  10. GD32VF103启动流程分析
  11. 编程中实例是什么?什么是实例?实例化又是什么?什么是类?什么是对象?
  12. 数字人事系统 java_市国税局“数字人事”信息系统正式上线
  13. VS中更改exe程序图标
  14. 好看的照片要怎么拍?
  15. 今年是嵌入式香还是互联网香?
  16. vue-cli之加载ico文件
  17. 【2022-08-27】美团秋招笔试前四道编程题
  18. java.lang.NullPointerException出现原因及解决办法
  19. python 读取邮件内容_python获取邮件内容(邮件内容为html)
  20. 东方博宜题解1075

热门文章

  1. png变成矢量图_[PPT]如何将图片背景变成透明
  2. 多媒体文件格式之TS
  3. 直接运行内存中的代码
  4. cocos2d-x游戏实例(2)-主角根据输入移动
  5. 解决延迟有 Wi-Fi 6 就够了!
  6. 掌握这 20 个容器实战技巧!
  7. 原创 | 分布式事务科普(终结篇)
  8. 滴滴为啥值3600亿?看它的数据中台就知道了
  9. Python中的多线程
  10. VVC专利池最新进展:MC-IF正在召集专利拥有者