braft 是一个较多人使用的 C++ raft 框架。开发者基于其抽象接口实现自己的业务逻辑,方便实现 raft 高可用的服务。本文从 metrics 入手,梳理开发者应该持续关注哪些监控变量。以其为线索,阅读源码探究其实现原理。力争做到心里有谱,不惧异常。
Table of Contents
0 背景
raft 算法和 braft 框架的解析,网络上已经有众多优秀的文章。项目 example 下也提供了完整的示例,服务可参考实现自己的业务逻辑。
相比快速实现一个基本可用服务,在长尾运行中应对各类异常情况更具挑战性。因此,本文主要回答以下问题:
- braft 中,已经暴露了哪些监控 metrics 变量?
- 每个变量对应 raft 背后的实现、以及为什么它很重要,需要监控?
- 根据源码和实验,特定异常情况下,braft 框架表现出哪些行为?
braft 提供了 2 种观测的方法。
- 自行获取
node status
- 更加偏向于 node 最基本的状态,包括
Follower / Leader
,commit log Index
等
- 更加偏向于 node 最基本的状态,包括
- 内置 bvar 暴露的
metrics
- 更多地是每个模块内部的详细状态,比如
snapshot store
写入速度,apply log
速率等
- 更多地是每个模块内部的详细状态,比如
而这些监控指标,正是我们入手了解 braft 内部实现的好线索。
本文梳理源码版本 v1.1.2
。
1 概览
braft 要求用户将自己服务逻辑实现在 node
类中,比如实现其中的 on_apply
, on_leader_start
等接口,本文不再赘述。
其中 node
提供了 get_status
方法,用于获取 raft node 内部状态。
class NodeImpl
: public butil::RefCountedThreadSafe<NodeImpl> {
...
public:
...
void describe(std::ostream& os, bool use_html);
// Get the internal status of this node, the information is mostly the same as we
// see from the website, which is generated by |describe| actually.
void get_status(NodeStatus* status);
...
}
NodeStatus
的定义如下:
// Status of Node
struct NodeStatus {
...
State state;
PeerId peer_id;
PeerId leader_id;
bool readonly;
int64_t term;
int64_t committed_index;
int64_t known_applied_index;
int64_t pending_index;
int64_t pending_queue_size;
int64_t applying_index;
int64_t first_index;
int64_t last_index;
int64_t disk_index;
PeerStatusMap stable_followers;
PeerStatusMap unstable_followers;
};
braft 会自动注册到 brpc 服务中,用户可以访问 http://brpc_server_addr:port/raft_stat
直接获取状态。不过相比于程序中的 get_status
,直接访问调用的是 node.describe
,相比单独的 NodeStatus
更加详细一些。
比如:
[Region_0]
peer_id: 127.0.0.1:30000:0
state: LEADER
readonly: 0
term: 12
conf_index: 279754448
peers: 127.0.0.1:30000:0 127.0.0.1:30001:0 127.0.0.1:30002:0
changing_conf: NO stage: STAGE_NONE
election_timer: timeout(5000ms) STOPPED
vote_timer: timeout(6000ms) STOPPED
stepdown_timer: timeout(5000ms) SCHEDULING(in 3749ms)
snapshot_timer: timeout(30000ms) SCHEDULING(in 17529ms)
storage: [279752493, 279755268]
disk_index: 279755268
known_applied_index: 279755268
last_log_id: (index=279755268,term=12)
state_machine: Idle
last_committed_index: 279755268
last_snapshot_index: 279754448
last_snapshot_term: 12
snapshot_status: IDLE
replicator_6597069766661@127.0.0.1:30001:0: next_index=279714316 flying_append_entries_size=0 blocking consecutive_error_times=41 hc=15897 ac=2676 ic=1355
replicator_7709466296321@127.0.0.1:30002:0: next_index=279755269 flying_append_entries_size=0 idle hc=15897 ac=529862 ic=0
2 state
state 是 raft node 状态机最最基本的状态,相信读过 raft 论文的开发者都不陌生。除了我们的业务逻辑,raft 状态也是个有限状态机:
enum State {
STATE_LEADER = 1,
STATE_TRANSFERRING = 2,
STATE_CANDIDATE = 3,
STATE_FOLLOWER = 4,
STATE_ERROR = 5,
STATE_UNINITIALIZED = 6,
STATE_SHUTTING = 7,
STATE_SHUTDOWN = 8,
STATE_END,
};
STATE_FOLLOWER
根据源码,有 2 种情况会流转进入。
- node init, node 初始化
- 初始化时,做完各项前置检查后,会先将
state
设置为FOLLOWER
。
- 初始化时,做完各项前置检查后,会先将
- leader step down, leader 下台
梳理一下所有 leader 下台的原因:
情景 | code | msg | 说明 |
---|---|---|---|
node init | Status::OK() |
ok |
节点初始化时,会触发一次下台,做必要的内部 ctx, timer 更新 |
stepdown_timer | ERAFTTIMEDOUT |
Majority of the group dies |
leader 上台后,开启 stepdown timer 定期检查 check_dead_nodes 。连接超时超过节点半数,则主动下台 |
reset_peers | ESETPEER |
Set peer from empty configuration 或 Raft node set peer normally" |
通过接口强行 reset_peers 后,会调用 step_down , 做必要的 ctx 和 timer 更新 |
shutdown | ESHUTDOWN |
Raft node is going to quit |
自行调用 shutdown 时下台 |
handle_timeout_now | EHIGHERTERMREQUEST |
Raft node receives higher term request |
接收到其他节点的立即超时 rpc,且请求中 term 大于自身 term,主动下台 |
on_error | EBADNODE |
Raft node(leader or candidate) is in error |
leader 在 snapshot 操作和用户 fsm 发生错误时,主动下台。并设置为 ERROR 状态。(注:若 Follower 发生错误,也会停止 follower,转为 ERROR 状态) |
vote_timeout | ERAFTTIMEDOUT |
Fail to get quorum vote-granted |
开启 FLAGS_raft_step_down_when_vote_timedout 时, vote 超时会自动下台。默认为 true 。 |
handle_request_vote_response | EHIGHERTERMRESPONSE |
Raft node receives higher term request_vote_response |
处理 vote resp 时发现 term 更大 |
handle_pre_vote_response | EHIGHERTERMRESPONSE |
Raft node receives higher term pre_vote_response |
处理 pre-vote resp 时发现 term 更大 |
handle_request_vote_request | EHIGHERTERMREQUEST |
Raft node receives higher term request_vote_request |
接收 vote resp 时发现 term 更大 |
handle_request_vote_request | EVOTEFORCANDIDATE |
Raft node votes for some candidate tep down to restart election_timer |
接收 vote resp 时发现投票给了其他 candidate |
handle_install_snapshot_request / handle_append_entries_request | ENEWLEADER |
Raft node receives message from new leader with higher term. 或 Candidate receives message from new leader with the same term 或 Follower receives message from new leader with the same term |
接收 append entries 和 install snapshot 时发现了更高 term 的 leader。可能是网络分区后更新的 leader 发过来的。 |
handle_append_entries_request / handle_append_entries_request | ELEADERCONFLICT |
More than one leader in the same term |
处理 append entries 时发现同一 term 有不同的 leader |
rpc_returned / heartbeat_returned / timeout_now_returned | EHIGHERTERMRESPONSE |
Leader receives higher term timeout_now_response(rpc/heartbeat) from peer:%s |
回包发现产生了更新的 term,直接设置成更高的 term increase_term_to ,下台 |
remove peer | ELEADERREMOVED |
This node was removed |
管理节点移除,且该节点为 leader |
lease expired | ERAFTTIMEDOUT |
Leader lease expired |
leader 令牌过期 |
当然,所有的状态流转都离不开 raft + 一些优化。了解所有可能的情况有助于我们应对复杂的网络分区故障。
Tips:
on_leader_stop
中有个 status 传入,err 中有很详细的原因解释,别忘了记录。
STATE_LEADER
根据源码,有 2 种情况会流转进入。
- node became leader
- transfer leader timeout
- transfer leader 超时,如果 term 未改变且仍为
STATE_TRANSFERRING
,会尝试调用on_leader_start
并恢复STATE_LEADER
状态
- transfer leader 超时,如果 term 未改变且仍为
STATE_TRANSFERRING
调用 NodeImpl::transfer_leadership_to
后进入,直到成功选出新 leader 或者超时。
STATE_CANDIDATE
节点选举自己时 elect_self
。
STATE_ERROR
一般是用户业务状态机报错时,或者 log/ load snapshot 发现存储有错误的时候,有以下枚举
enum ErrorType : int {
ERROR_TYPE_NONE = 0,
ERROR_TYPE_LOG = 1,
ERROR_TYPE_STABLE = 2,
ERROR_TYPE_SNAPSHOT = 3,
ERROR_TYPE_STATE_MACHINE = 4
};
STATE_UNINITIALIZED, STATE_SHUTTING, STATE_SHUTDOWN
顾名思义
3 term
该部分即 raft 论文中 term。不再赘述。
4 configuration 相关: conf_index / peers / changing_conf / stage
示例输出:
conf_index: 279754448
peers: 127.0.0.1:30000:0 127.0.0.1:30001:0 127.0.0.1:30002:0
changing_conf: NO stage: STAGE_NONE
configuration & peers 管理接口
我们先来明确 Configuration
这个术语: 即 raft 组的所有节点集合。
// A set of peers.
class Configuration {
......
std::set<PeerId> _peers;
};
而整个 raft 组的 config,是以 raft 日志的形式存储、也要以日志的形式 apply。我们进行 raft 节点管理和变更时,也需要取得 raft 组的共识。源码可以看到,ConfigurationEntry
实际上也是一个 LogEntry
。
struct ConfigurationEntry {
LogId id;
Configuration conf;
Configuration old_conf;
......
// ConfigurationEntry 实际上也是一个 LogEntry
ConfigurationEntry(const LogEntry& entry) {
id = entry.id;
conf = *(entry.peers);
if (entry.old_peers) {
old_conf = *(entry.old_peers);
}
}
......
};
影响到 conf 的变更,主要是 node 管理接口的调用。关于节点变更,官方文档解释得非常详细且权威。应用开发者提供运维文档时非常有必要参考。
braft 官方文档 - server 篇
以下接口可以对正常的 raft 组执行变更。
// Add a new peer to the raft group. done->Run() would be invoked after this
// operation finishes, describing the detailed result.
void add_peer(const PeerId& peer, Closure* done);
// Remove the peer from the raft group. done->Run() would be invoked after
// this operation finishes, describing the detailed result.
void remove_peer(const PeerId& peer, Closure* done);
// Gracefully change the configuration of the raft group to |new_peers| , done->Run()
// would be invoked after this operation finishes, describing the detailed
// result.
void change_peers(const Configuration& new_peers, Closure* done);
以下接口变更是强制性的,可能会破坏数据一致性。仅在紧急时飞线使用。
// Reset the configuration of this node individually, without any repliation
// to other peers before this node beomes the leader. This function is
// supposed to be inovoked when the majority of the replication group are
// dead and you'd like to revive the service in the consideration of
// availability.
// Notice that neither consistency nor consensus are guaranteed in this
// case, BE CAREFULE when dealing with this method.
butil::Status reset_peers(const Configuration& new_peers);
chaning_conf / stage
此两项状态只会在节点为 leader 时输出。config stage 指的是节点 conf 变换的各个阶段,枚举如下。除了 STAGE_NONE
,都视为 changing_conf=true
。
enum Stage {
// Don't change the order if you are not sure about the usage
STAGE_NONE = 0,
STAGE_CATCHING_UP = 1,
STAGE_JOINT = 2,
STAGE_STABLE = 3,
};
官方文档 [1] 对节点变更配置提供了接口和描述。
https://github.com/baidu/braft/blob/master/docs/cn/server.md
追赶阶段: 如果新的节点配置相对于当前有新增的一个或者多个节点,leader对应的Replicator, 向把最新的snapshot再这个这些中安装,然后开始同步之后的日志。等到所有的新节点数据都追的差不多,就开始进入一下一阶段。
追赶是为了避免新加入的节点数据和集群相差过远而影响集群的可用性. 并不会影响数据安全性.
在追赶阶段完成前, 只有leader知道这些新节点的存在,这个节点都不会被记入到集群的决策集合中,包括选主和日志提交的判定。追赶阶段任意节点失败,则这次节点变更就会被标记为失败。联合选举阶段: leader会将旧节点配置和新节点配置写入Log, 在这个阶段之后直到下一个阶段之前,所有的选举和日志同步都需要在新老节点之间达到多数。 这里和标准算法有一点不同, 考虑到和之前实现的兼容性,如果这次只变更了一个节点, 则直接进入下一阶段。
新配置同步阶段: 当联合选举日志正式被新旧集群接受之后,leader将新节点配置写入log,之后所有的log和选举只需要在新集群中达成一致。 等待日志提交到新集群中的多数节点中之后, 正式完全节点变更。
清理阶段: leader会将多余的Replicator(如果有)关闭,特别如果当leader本身已经从节点配置中被移除,这时候leader会执行stepdown并且唤醒一个合适的节点触发选举。
值得注意的是,在追赶阶段,只有 leader 知道新节点的存在。
5 log_manager 相关 index
在整个 raft 组,index 是日志递增的序列号。通过 index 指标,我们可以明确当前的状态机情况,以及日志存储的范围。
raft_stat 的返回如下,由 log_manager
输出。
_log_manager->describe(os, use_html);
...
storage: [279752493, 279755268]
disk_index: 279755268
known_applied_index: 279755268
last_log_id: (index=279755268,term=12)
node status 结构体 index 包括:
LogManagerStatus log_manager_status;
_log_manager->get_status(&log_manager_status);
status->known_applied_index = log_manager_status.known_applied_index;
status->first_index = log_manager_status.first_index;
status->last_index = log_manager_status.last_index;
status->disk_index = log_manager_status.disk_index;
总结成表格如下
http 接口输出 | node.state 结构体 | 注释 |
---|---|---|
storage [first, last] | first_index, last_index | storage 指 raft log store。在我们初始化 node_options.log_uri 时指定。是当前日志存储的范围。该范围会随着 apply 和 snapshot 不断前进。包含内存和磁盘中的。 |
disk_index | disk_index | 磁盘上最新的 log。(log 可能在磁盘或者内存中) |
known_applied_index | known_applied_index | 已知 apply 的 index |
last_log_id | 获取最新的 log id。注意,其会等待 disk 刷盘结束后才能获得。 | |
last_index | 最新的 log index,包括磁盘和内存中的。 |
6 ballot_box 相关 index
ballot_box 是用来管理投票的组件。
raft_stat 的返回如下,由 ballot_box
输出。
_ballot_box->describe(os, use_html);
...
os << "last_committed_index: " << _last_committed_index << newline;
os << "pending_index: " << _pending_index << newline;
os << "pending_queue_size: " << _pending_meta_queue.size() << newline;
node status 结构体对应内容
BallotBoxStatus ballot_box_status;
_ballot_box->get_status(&ballot_box_status);
status->committed_index = ballot_box_status.committed_index;
status->pending_index = ballot_box_status.pending_index;
status->pending_queue_size = ballot_box_status.pending_queue_size;
对应的含义总结成表格:
http 接口输出 | node.state 结构体 | 注释 |
---|---|---|
last_committed_index | last_committed_index | 最新被 commit 的 log index |
pending_index | pending_index | 通常为 last_committed_index + 1 |
pending_queue_size | pending_queue_size | 顾名思义 pending 队列长度 |
Q: apply 与 commit
在 Raft 论文 In Search of an Understandable Consensus Algorithm (Extended Version) 中,多次提到 commit 和 apply 两词。其中,commit (提交)或 committed (已提交)针对的是日志,即日志项被成功复制到集群中大多数节点后,日志项处于 committed 状态,;apply (应用)或 applied (已应用)针对的是状态机,即节点将日志项应用到状态机,真正改变节点变量的。[2]
7 state_machine 相关
由 FSMCaller
输出。
_fsm_caller->describe(os, use_html);
在一个 node 中,执行各种回调实际上是由一个串行的 FSMCaller
执行的。
状态中的 state_machine
即标志着当前状态机的执行状态。
这也意味着,在我们应用层实现的各种回调中,一定是串行执行的。它们类似中断机制,一定不能长时间阻塞。一旦阻塞,将阻塞整个状态机的执行。
switch (cur_task) {
case IDLE:
os << "Idle";
break;
case COMMITTED:
os << "Applying log_index=" << applying_index;
break;
case SNAPSHOT_SAVE:
os << "Saving snapshot";
break;
case SNAPSHOT_LOAD:
os << "Loading snapshot";
break;
case ERROR:
os << "Notifying error";
break;
case LEADER_STOP:
os << "Notifying leader stop";
break;
case LEADER_START:
os << "Notifying leader start";
break;
case START_FOLLOWING:
os << "Notifying start following";
break;
case STOP_FOLLOWING:
os << "Notifying stop following";
break;
}
8 snapshot 相关
http stat 输出
_snapshot_executor->describe(os, use_html);
...
os << "last_snapshot_index: " << last_snapshot_index << newline;
os << "last_snapshot_term: " << last_snapshot_term << newline;
if (m && is_loading_snapshot) {
CHECK(!is_saving_snapshot);
os << "snapshot_status: LOADING" << newline;
os << "snapshot_from: " << request.uri() << newline;
os << "snapshot_meta: " << meta.ShortDebugString();
} else if (m) {
CHECK(!is_saving_snapshot);
os << "snapshot_status: DOWNLOADING" << newline;
os << "downloading_snapshot_from: " << request.uri() << newline;
os << "downloading_snapshot_meta: " << request.meta().ShortDebugString();
} else if (is_saving_snapshot) {
os << "snapshot_status: SAVING" << newline;
} else {
os << "snapshot_status: IDLE" << newline;
}
node status
node status 未包含 snapshot 相关的监控
9 Replicator 相关
当 node 为 leader 时,可以在 http stat 中看到 replicator 状态。
源码的输出比较直观。
os << "replicator_" << id << '@' << peer_id << ':';
os << " next_index=" << next_index << ' ';
os << " flying_append_entries_size=" << flying_append_entries_size << ' ';
if (readonly_index != 0) {
os << " readonly_index=" << readonly_index << ' ';
}
switch (st.st) {
case IDLE:
os << "idle";
break;
case BLOCKING:
os << "blocking consecutive_error_times=" << consecutive_error_times;
break;
case APPENDING_ENTRIES:
os << "appending [" << st.first_log_index << ", " << st.last_log_index << ']';
break;
case INSTALLING_SNAPSHOT:
os << "installing snapshot {" << st.last_log_included
<< ", " << st.last_term_included << '}';
break;
}
os << " hc=" << heartbeat_counter << " ac=" << append_entries_counter << " ic=" << install_snapshot_counter << new_line;
小结
本文从一个使用者视角,通过源码总结了 braft 的观测指标。同时以它们为线索,探索了 raft 的一些基本原理和术语。
参考
[1] braft 文档 - https://github.com/baidu/braft/blob/master/docs/cn/server.md
[2] Raft 共识算法学习笔记 二:日志复制 - https://leehao.me/Raft-%E5%85%B1%E8%AF%86%E7%AE%97%E6%B3%95%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-%E4%BA%8C%EF%BC%9A%E6%97%A5%E5%BF%97%E5%A4%8D%E5%88%B6/
赞
ping