etcd raft 处理流程图系列3-wal的存储和运行

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: etcd raft 处理流程图系列3-wal的存储和运行

存储和节点的创建

raftexample中的存储其实有两种,一个是通过raft.NewMemoryStorage()进行创建的raft.raftStorage,关联到单个raft节点,另一个是通过newKVStore创建的kv存储,用于服务来自外部的访问。

节点启动时raft.raftStorage的加载

上一篇中主要围绕replayWAL介绍wal的读写,到本文为止可以完整拼接出该函数的处理逻辑。其中snapshot的作用是通过index限定了加载的wal日志的范围。

  1. 一开始会通过loadSnapshot函数找出一个有效且最新的snapshot。保证有效的方式是:从raftNode.waldir中读取所有snapshotType类型的数据(snaps),以及最新的stateType类型的数据(state),然后过滤出所有index<=state.Commit的snapshot(state.Commit表示已提交的日志的index,从本地加载的snapshot的index不能超过state.Commit,否则被认为是无效的历史数据);保证最新的方式是:通过对比raftNode.snapdir(注意对比时文件是倒序的)和上一步获取到的snaps(即通过对比Snapshot.TermSnapshot.Index),找出最新的snapshot。
  2. openWAL函数根据第1步中获取到的snapshot的index找出符合条件(即index<=snapshot.index)的wal文件
  3. ReadAll()会从上一步的wal文件中读取index大于snapshot.index的entryType类型的表项(wal的start就是snapshot),以及stateType类型的状态信息。
case entryType:
  e := mustUnmarshalEntry(rec.Data)
  // 0 <= e.Index-w.start.Index - 1 < len(ents)
  if e.Index > w.start.Index {
   // prevent "panic: runtime error: slice bounds out of range [:13038096702221461992] with capacity 0"
    up := e.Index - w.start.Index - 1
    if up > uint64(len(ents)) {
      // return error before append call causes runtime panic
      return nil, state, nil, ErrSliceOutOfRange
    }
    // The line below is potentially overriding some 'uncommitted' entries.
    ents = append(ents[:up], e)
  }
  w.enti = e.Index

总结一下,上面的关系如下,state限定了读取的snapshot的范围(起点),而snapshot限定了读取的ents的范围。

三者的大小关系如下:snapshot.Index<=state.Commit,ents>snapshot.Index。

后续就是将snapshot、state和ents保存到raftStorage中(raftexample的存储实际并不支持持久化,下面以"落库"表示将数据保存到存储中),需要注意的是,落库的内容也受snapshot的限制,即只保存index>snapshot.index的表项(ApplySnapshot函数会使用snapshot的覆盖原有数据,由此可见raftStorage中并没有保存完整的数据)。从上图可以看出所有处理逻辑都以state为起点进行的,state表示raft的处理结果,可以直接落库。当保存snapshot时,需要与存储中的snapshot进行比较,只有当index大于存储中的snapshot的index时才会被保存。在保存ents时,由于ReadAll接口(见上)对读取的ents的范围作了限制,因此只可能出现以下3种情况:1)ents的数据的最大index小于存储中的最小index,这种情况不做任何处理;2)ents的数据的最小index等于存储中的最小index,这种情况直接追加到存储即可;3)ents的数据和存储的数据有交叉,这种情况需要剔除重叠的数据,并追加新的数据。

snapshot中除了保存了索引相关的内容,还保存了与集群状态有关的信息(snapshot.Metadata.ConfState),用于恢复集群状态。

kvStore的存储

节点运行中主要通过raftNode.commitC来通知kvStore。一种是在接收到snapshot时先保存snapshot,然后通过给rc.commitC传递nil来触发kvStore从snap目录中加载snapshot;另一种就是直接通过rc.commitC将接收到的entries保存到kvStore。

raftLog

raft通过raftLog与raftStorage存储进行交互,unstable可以看作是storage的缓存,保存着未进入raftStorage的数据(entires和snapshot)。当需要获取raftLog的首末索引时,会优先从unstable中查找,若找不到,再从raftStorage中查找。

raftNode的创建

下图展示了raft节点的创建过程,从下图可以看到,raft节点启动时主要涉及的是raftLogProgressTrackermsgs。上一节已经讲过raftlog,它作为raft的存储,包括unstable和storage。ProgressTracker是raft中用来跟踪集群(config)以及各个节点的状态(prs)。raft集群中的节点分为Voters,Learners和LearnersNext三大类,其中前两个是互斥的,即一个raft节点只能属于其中一类。最后一类存在的原因是为了在voters(outgoing)向learner转换过程中保证一致性(受raft的joint行为限制,raft的状态变更是通过joint方式运作的),是个临时状态。Voters又分为incoming(Voters[0])和outgoing(Voters[1])两种,分别表示新状态和原始状态。当发生状态变更时,会将当前状态拷贝到outgoing中,根据新状态类型(ConfChangeAddNode/ConfChangeAddLearnerNode/ConfChangeRemoveNode/ConfChangeUpdateNode)来执行joint操作,如果raft处于joint状态(outgoing长度大于0),则不能再次执行joint(有可能存在中间状态)。


raftLog中有两个标识:committed和applied,committed标识已经进入storage中的最大日志位置,而applied表示已经处理过的消息(如配置变更等)。其中applied <= committed。


ProgressTracker中保存的节点状态有如下三种:

  • StateProbe:每个心跳周期只能发送一个复制消息,同时用来探测follower的进度。
  • StateReplicate:为follower可以快速接收赋值日志的理想状态
  • StateSnapshot:在发送赋值消息前需要发送snapshot,来让follower转变为StateReplicate状态

原图地址

raft的joint操作以及LearnersNext存在的原因见下:

// When we turn a voter into a learner during a joint consensus transition,
  // we cannot add the learner directly when entering the joint state. This is
  // because this would violate the invariant that the intersection of
  // voters and learners is empty. For example, assume a Voter is removed and
  // immediately re-added as a learner (or in other words, it is demoted):
  //
  // Initially, the configuration will be
  //
  //   voters:   {1 2 3}
  //   learners: {}
  //
  // and we want to demote 3. Entering the joint configuration, we naively get
  //
  //   voters:   {1 2} & {1 2 3}
  //   learners: {3}
  //
  // but this violates the invariant (3 is both voter and learner). Instead,
  // we get
  //
  //   voters:   {1 2} & {1 2 3}
  //   learners: {}
  //   next_learners: {3}
  //
  // Where 3 is now still purely a voter, but we are remembering the intention
  // to make it a learner upon transitioning into the final configuration:
  //
  //   voters:   {1 2}
  //   learners: {3}
  //   next_learners: {}
  //
  // Note that next_learners is not used while adding a learner that is not
  // also a voter in the joint config. In this case, the learner is added
  // right away when entering the joint configuration, so that it is caught up
  // as soon as possible.

上图中的涉及新raft节点的创建,整个过程也比较简单。首先根据replayWAL获取到的snapshot来恢复集群和节点的状态,然后根据本节点是否是leader来执行更新raftLog的提交记录和消息发送。

raftNode的运行

下图给出了raftNode的基本运作流程,仅含初始的选举流量,但后续处理方式也大体类似,区别在于传递和处理的消息类型不同。在raftExample的讲解中可以看到启动了一个serveChannels的服务,该服务用于从n.readyc中接收封装好的ready消息,然后进行保存、应用(配置)和发送等操作,最后通过n.advancec通知node节点处理结果,node以此执行acceptReady操作,更新提交记录和应用记录等。


serveChannels中会创建一个100ms的时钟,定时向n.tickc发送触发消息。一开始,所有节点角色都是follower,此时会触发tickElection(follower和candidate为tickElection,leader为tickHeartbeat),尝试将角色转变为candidate。此外ProgressTracker.Votes中保存了支持本节点作为leader的票数,超过一半节点同意时会转变为leader。


n.readyc中传递的ready消息封装了本节点的状态变更以及待发送的信息,n.readyc中的消息进而会传递给serveChannels,后续通过transport.send发送给其他节点,反之亦然。

TIPs

  • etcd的raft角色有三种:leader、follower、learner。在etcd 3.4之前出现可能会出现如下问题:
  • 新加入一个节点,leader会将快照同步到该节点,但如果快照数量过大,可能会导致超时,导致节点加入失败
  • 新加一个节点时,如果新的节点配置错误(如url错误),可能会导致raft选举失败,集群不可用
  • 为了避免如上问题,加入了一个新的角色learner,它作为一个单独的节点,在日志同步完成之前不参与选举,etcd中需要通过member promote命令来让learner参与选举。

参考

存储模块源码简析

etcd的raft实现之tracker&quorum

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
3月前
|
缓存 JSON 数据格式
etcd raft 处理流程图系列3-wal的读写
etcd raft 处理流程图系列3-wal的读写
41 1
|
3月前
|
存储
etcd raft 处理流程图系列1-raftexample
etcd raft 处理流程图系列1-raftexample
41 2
|
3月前
etcd raft 处理流程图系列2-transport
etcd raft 处理流程图系列2-transport
32 2
|
4月前
|
存储 缓存 监控
Etcd/Raft 原理问题之Etcd-Raft节点故障问题如何解决
Etcd/Raft 原理问题之Etcd-Raft节点故障问题如何解决
|
4月前
|
索引
Etcd/Raft 原理问题之follower会进入StateReplicate状态时的问题如何解决
Etcd/Raft 原理问题之follower会进入StateReplicate状态时的问题如何解决
Etcd/Raft 原理问题之follower会进入StateReplicate状态时的问题如何解决
|
4月前
|
存储 索引
Etcd/Raft 原理问题之应用层收到Ready结构体后执行操作时的问题如何解决
Etcd/Raft 原理问题之应用层收到Ready结构体后执行操作时的问题如何解决
|
4月前
|
存储 算法 开发工具
Etcd/Raft 原理问题之Etcd-Raft是什么
Etcd/Raft 原理问题之Etcd-Raft是什么
|
4月前
Etcd/Raft 原理问题之etcd/raf配置变更t问题如何解决
Etcd/Raft 原理问题之etcd/raf配置变更t问题如何解决
|
存储 关系型数据库 块存储
带你读《存储漫谈:Ceph原理与实践》——2.3.2 PG 的状态机
带你读《存储漫谈:Ceph原理与实践》——2.3.2 PG 的状态机
|
存储 安全 算法
Raft 共识算法3-日志复制
一旦领导者被选出,它就开始为客户请求提供服务。 每个客户端请求都包含要由复制状态机执行的命令。 领导者将该命令作为新条目附加到其日志中,然后向每个其他服务器并行发出 AppendEntries RPC 以复制该条目。 当条目已被安全复制(如下所述)后,领导者将条目应用于其状态机并将该执行的结果返回给客户端。 如果跟随者崩溃或运行缓慢,或者网络数据包丢失,领导者会无限期地重试 AppendEntries RPC(即使在它已经响应客户端之后)直到所有跟随者最终存储所有日志条目。
133 0