2.1 启动流程

流程详解

流程概览

  1. 在启动节点前,用户需要创建 BRPC Server,并调用 braft::add_service 将 Raft 相关的 Service 加入到 BRPC Server 中,以及启动 BRPC Server

  2. 用户创建 braft::Node,并调用 Node::init 启动节点,此后启动流程将由 braft 接管

  3. 启动任务队列 ApplyTaskQueue

  4. 遍历一遍日志,读取每条日志的 Header (24 字节):

    • 4.1 若 Header 中的类型显示其为配置,则读取完整日志以获取配置

    • 4.2 构建索引(logIndex 到文件 offset),便于读取时快速定位

    • 4.3 获得当前节点的 firstLogIndexlastLogIndex

    • 4.4 删除 firstLogIndex 之前的日志文件(快照遗留)

  5. 加载快照:

    • 5.1 打开本地快照,并返回 SnapshotReader

    • 5.2 以 SnapshotReader 作为参数回调用户状态机 on_snapshot_load 来加载快照

    • 5.3 等待快照加载完成,将快照元数据中的节点配置设为当前节点配置

  6. 将日志中的配置(步骤 4.1)或用户指定配置(initial_conf)设为当前节点配置

  7. 读取 Raft 元数据文件,即 termvotedFor

    • 7.1 将节点 currentTerm 设置为 term

    • 7.2 恢复投票状态 votedFor

  8. 启动快照定时器

  9. 将自身角色变为 Follower,并启动选举定时器

  10. 将节点加入 Raft Group

  11. 至此,启动完成,节点将等待选举超时后发起选举

从上述流程可以看出,启动工作可以大致分为以下 2 类:加载持久化存储来恢复节点状态(步骤 4、5、6、7),以及启动算法(步骤 8、9)。

图 2.1 Raft Node

Raft Service

当用户调用 braft::add_service 时,braft 会增加以下 4 个 Raft 相关的 Service 至 BRPC Server:

  • FileService:用于 Follower 安装快照时,向 Leader 下载快照中的文件。

  • RaftService:核心服务,用于处理 Raft 算法,如选举投票、复制日志、下发安装快照指令等。

  • RaftStat:可观测性的一部分,用户可通过访问 http://${your_server_endpoint}/raft_stat 查看当前这个进程上 Node 的列表,以及每个 Node 的内部状态,详见查看节点状态

  • CliService:允许用户通过发送 RPC 请求来控制节点,如配置变更、重置节点列表、转移 Leader;当然用户也可以通过 API 来控制节点。

ApplyTaskQueue

这是一个串行执行的任务队列,由 BRPC ExecutionQueue 实现,所有需要回调用户状态机的任务都会进入该队列,并被依次串行回调:

任务类型
说明
回调函数

COMMITTED

当已被提交的日志,需被应用到状态机时

若日志类型为节点配置,则回调 on_configuration_committed,否则回调 on_apply

SNAPSHOT_SAVE

创建快照

on_snapshot_save

SNAPSHOT_LOAD

加载快照

on_snapshot_load

LEADER_START

当节点成为 Leader 时

on_leader_start

LEADER_STOP

当节点不再是 Leader 时

on_leader_stop

START_FOLLOWING

当 Follower 或 Candidate 开始跟随 Leader 时;此时其 leader_id 为 Leader 的 PeerId

on_start_following

STOP_FOLLOWING

当节点不再跟随 Leader;此时其 leader_id 将变为空

on_stop_following

ERROR

当节点出现出错,此时任何 apply 任务都将失败

on_error

持久化存储

Raft 拥有以下 3 个持久化存储,这些都需要在节点重启时进行重建:

  • RaftMetaStorage:保存 Raft 算法自身的状态数据(即 termvotedFor

  • LogStorage:存储用户日志以及日志元数据(即 firstLogIndex

  • SnapshotStorage:存储用户快照以及元数据

比较容易忽略的是,其实节点的配置也是持久化的,其会保存在快照元数据(SnapshotStorage)和日志(LogStorage)中。

Raft 元数据 StablePBMeta

日志元数据 LogPBMeta

快照元数据 LocalSnapshotPbMeta

日志回放

当节点刚启动时,其不会回放日志,因为 commitIndex 并没有持久化,所以节点在启动时并不知道自己的 commitIndex,也就不知道该 apply 哪些日志。只有当集群产生 Leader 后集群中的节点才开始回放日志,Leader 的 commitIndex 由其当选 Leader 后,提交一条本任期 no-op 日志后确定,其 commitIndex 就等于该 no-op 日志的 index,而 Follower 的 commitIndex 由 Leader 在之后的心跳或 AppendEntries 请求中告知,详见 3.1 选主流程

而对于快照来说,其代表的都是 applied 的数据,所以可以安全的加载。

节点配置

节点在启动时,其配置取决如下:

  • 优先读取日志中的配置

  • 若当前节点不存在日志或日志中没有配置,则读取快照元数据中保存的配置

  • 若当前节点为新节点,既没有日志,也没有快照,则使用用户指定的 initial_conf

  • 若用户没有指定配置,则该节点配置为空

从以上流程可以看出,只有当节点以空节点启动时,用户指定的配置才会生效。

具体实现

braft:add_service

用户需调用 braft::add_service 将 braft 相关 Service 加入到 BRPC Server 中:

braft::add_service 会调用 NodeManager::add_service 来增加以下 4 个 Service:

braft::Node

此外,用户在启动节点前,需要构建一个 braft::Node

  • GroupId:一个字符串, 表示这个复制组的 ID

  • PeerId:结构是一个 EndPoint,表示对外服务的端口,外加一个 Index (默认为 0)用于区分同一进程内的不同副本

Node::init

最后,用户需调用 Node::init 来启动节点,在 init 函数中主要完成以下几项工作:

参考

Last updated