6.1 节点配置变更
流程详解
流程概览
用户通过
add_peer/remove_peer/change_peer接口向 Leader 提交配置变更若当前 Leader 有配置变更正在进行,则返回
EBUSY;若新老配置相同,则直接返回成功进入日志追赶(
CaughtUp)阶段:3.1 将新老配置做
diff获得新加入的节点列表;若没有新增节点,则直接进入下一阶段3.2 为每个新节点创建
Replicator,并对所有新节点定期广播心跳3.3 为每个新节点安装最新的快照,每当一个节点完成快照的安装,则进行步骤 3.4 的判断
3.4 判断该节点日志与当前 Leader 之间的差距:
3.4.1 若小于一定值(默认 1000 条日志),则将其从追赶列表中移除
3.4.2 若追赶列表为空,即所有新节点都追赶上了 Leader 的日志,则进入下一阶段
3.5 向每个新节点同步日志,每当一个节点成功同步一部分日志,重复步骤 3.4
3.6 若追赶阶段任意节点失败,则本次配置变更标记为失败
进入联合共识(
Joint-Consensus)阶段:4.1 Leader 应用联合配置(即
C{old,new})为当前配置,以该配置视角进行选举和日志复制:4.1.1 在该阶段,选举需要同时在新老集群都达到
Qourum4.1.2 在该阶段,日志复制需要同时在新老集群都达到
Qourum才能提交
4.2 Leader 将联合配置日志(即
C{old,new})同时复制给新老集群;当然也复制其他日志4.3 待联合配置日志在新老集群都达到
Qourum,则提交并应用该日志:4.3.1 调用该日志的
Closure,进入下一阶段
进入同步新配置(
Stable)阶段:5.1 Leader 应用新配置(即
C{new})为当前配置,以该配置视角进行选举和日志复制:5.1.1 在该阶段,选举只需在新集群达到
Qourum5.1.2 在该阶段,日志只需在新集群达到
Qourum即可提交5.1.3 在该阶段,日志仍会被复制给老集群,因为老集群节点的
Replicator还未停止
5.2 Leader 将新配置日志(即
C{new})同时复制给新老集群;当然也复制其他日志5.3 待新配置日志在新集群中达到
Qourum,则提交并应用该日志:5.3.1 以新配置作为参数,调用用户状态机的
on_configuration_committed5.3.2 调用该配置的
Closure,进入下一阶段
进行清理工作:
6.1 Leader 移除不在集群中的
Replicator,不再向它们发送心跳和日志6.2 回调接口传入的
Closure6.3 若当前 Leader 不在新配置中,则
step_down变为 Follower:6.3.1 调用用户状态机的
on_leader_stop6.3.2 向拥有最长日志的节点发送
TimeoutNow请求,让其立马进行选举
对于不在集群中的节点:
7.1 若其拥有了新配置日志,则其发现自身不在集群中,则不会发起选举
7.2 若其未拥有新配置日志,则会超时发起选举,但由于日志落后,其不会通过
PreVote

干扰集群

上图来自 Raft 作者的博士论文 4.2.3 Disruptive servers,描述的是一个被移除的节点干扰集群,造成集群重新选举的场景:
由节点
S1,S2,S3,S4组成的集群中,S4为 Leader用户执行配置变更,调用
remove_peer将节点S1移除集群当
S4同步新配置时,已将新配置持久化到本地,但未复制给S2,S3由于
S1不再接收S4的心跳,触发其选举,并在PreVote阶段获得S1,S2,S3的支持S1增加自身term进行RequestVote,S4发现其term比自己高,则会step down
在<3.2 选举优化>中提到过,这种场景 PreVote 无法阻止,但是可以利用 Check Quorum 解决。但是其实在目前 braft 的实现中,这种场景是不会出现的,因为 Leader 是将新配置(即 C{new})复制到新集群的大多数集群后,才开始移除不在新集群中的 Replicator,并停止广播心跳。
相关接口
阶段一:追赶日志
调用接口
用户调用各接口要求 Leader 进行配置变更。在这些接口中,会生成新配置和老配置,最终都会调用 on_configuration_committed 执行变更:
开始变更
而在 unsafe_register_conf_change 函数中首先做进行一些判断,决定是否要执行变更。若要进行变更,则调用 ConfigurationCtx::start 正式开始变更:
ConfigurationCtx::start 的主要流程详见以下注释:
创建 Replicator
ReplicatorGroup::add_replicator 会为每个节点创建 Replicator,并将其启动,Replicator 负责发送心跳和同步日志。
关于 Replicator 的创建,我们已经在<3.1 选举流程>中详细介绍过了,见创建 Replicator:
保存 Closure
在<开始变更>的主干函数中会调用 ReplicatorGroup::wait_caughtup 为每个新增节点保存 OnCaughtUp,其会在安装快照或同步日志后被调用,来判断追赶进度是否可以进入下一阶段。具体流程见以下注释:
安装快照
当 Replicator 被创建后,其就会调用 _send_entries 开始复制日志。由于新加入的节点需要的日志很大概率已经被快照压缩了,所以需要向其安装快照。安装快照的流程我们已经在<5.2 安装快照>中详细介绍过来,这里只阐述相关流程:
同步日志
如果日志差距仍大于配置值,则先继续调用 _send_entries 同步日志:
判断日志差距
成功安装快照或每次成功同步一批日之后,都会调用 _notify_on_caught_up 判断新加入节点日志与当前 Leader 的差距。若差距小于一定值(默认为 1000),
阶段二:联合共识
进入新阶段
当所有新增节点都追赶上了 Leader 的日志,则调用 next_stage 进入下一阶段,详见以下注释:
开始变更
unsafe_apply_configuration 具体会执行以下这些工作,详见以下注释,其中的每一项工作我们将在之后逐一介绍:
初始化投票规则
应用联合配置
Leader 调用 check_and_set_configuration 将当前配置变为联合配置,即 C{old,new}:
复制日志
调用 LogManager::append_entries 追加配置日志(即 C{old,new}) 后,Replicator 就会被唤醒向 Follower 同步日志。
Leader 会将普通日志和配置日志(即 C{old,new})复制给新老集群,每收到一个节点的成功响应,都会调用 BallotBox::commit_at 将对应日志的计算加一,如果其达到了 Quorum 则提交并应用该日志。具体的日志复制流程我们在之前的章节都详细介绍过了,详见<4.1 复制流程>。
提交日志
Leader 本地持久化成功或每成功复制日志给一个 Follower,都会调用 BallotBox::commit_at 将对应日志的复制计数加一,如果达到 Quorum,则更新 commitIndex,并将其应用:
granted 中的 _quorum 和 _old_quorum 都在以上的<初始化投票规则>中设为相应的值了:
应用日志
FSMCaller::on_committed 最终会调用 FSMCaller::do_committed 开始应用日志,具体见以下注释:
回调 Closure
当配置日志被应用后,其会调用 ConfigurationChangeDone::Run,而该函数最终会调用 ConfigurationCtx::next_stage 进入下一阶段:
阶段三:同步新配置
进入新阶段
开始变更
应用新配置
复制日志
提交日志
应用日志
回调 Closure
以上这些步骤跟<阶段二:联合共识>的步骤是一样的,你可以从<开始变更>开始重走一遍逻辑。
阶段四:清理工作
进入新阶段
待新配置被提交后,会调用 next_stage 进入清理阶段:
停止 Replicator
回调 Closure
reset 函数会执行一些清理工作以及回调接口的 Closure,详见以下注释:
stop_replicator 函数会清理不在新配置中 Follower 对应的 Replicator,具体流程见以下注释:
step_down
在 step_down 函数中会向一个日志最长的节点发送一个 TimeoutNow 请求,让其立马进行选举,该优化我们曾在<3.2 选举优化>中提到过。step_down 的具体流程见以下注释:
其他:故障恢复
当执行配置变更的 Leader 中途 Crash 了,新当选的 Leader 继续执行配置变更:
Last updated