nuraft源码笔记——(1)
根据mit6.824的思路,来梳理nuraft中的实现过程
1 预投票
1.1 进行预投票
- 触发预投票的条件
- 每个node都会设置一个定时器election_timer,当timeout,则回触发选举
handle_election_timeout()
- 需要满足以下条件,node才可能开始prevote的准备
- 是否是新来的节点
if (catching_up_)
- 是否存在越界日志
if (out_of_log_range_)
- 是否正常接收快照
if (receiving_snapshot_ && et_cnt_receiving_snapshot_ < 20)
- 定时器的时间间距够不够
if ( serving_req_ || time_ms < ctx_->get_params()->election_timeout_lower_bound_ )
- 不是leader,也不是follower
if (role_ == srv_role::leader) if (!im_learner_)
- 进行准备
- 在预投票中,维护了要给pre_vote来维护预投票的状态
struct pre_vote_status_t { pre_vote_status_t() : quorum_reject_count_(0) , failure_count_(0) { reset(0); } void reset(ulong _term) { term_ = _term; done_ = false;//pre_vote_是否完成 live_ = dead_ = abandoned_ = 0; } ulong term_;//pre_vote_的任期号 std::atomic<bool> done_;//pre_vote_是否完成 std::atomic<int32> live_;//与当前leader有联系的node个数 std::atomic<int32> dead_;//与当前leader没有联系的node个数 std::atomic<int32> abandoned_;//离开集群的node个数 /** * Number of pre-vote rejections by quorum. */ std::atomic<int32> quorum_reject_count_; /** * Number of pre-vote failures due to not-responding peers. */ std::atomic<int32> failure_count_;//没有响应的个数 };
- 对pre_vote进行检测和维护
- 如果pre_vote期号和当前状态机不一致(pre_vote还保存了之前期号的pre_vote状态)
if (pre_vote_.term_ != state_term) { p_in("pre-vote term (%" PRIu64 ") is different, reset it to %" PRIu64 "", pre_vote_.term_, state_term); pre_vote_.reset(state_term); }
- 如果满足以下任一要求,就直接初始化准备选举
- peer为0(该集群中就这个node一个节点)
- 法定投票人数为0
- pre_vote.done_为true(预投票这个流程已完成)
if ( !peers_.size() || pre_vote_.done_ || get_quorum_for_election() == 0 ) {//选举需要的法定人数 initiate_vote();
- 都不满足,则要发起一次预投票
request_prevote();
- 开始进行预投票的请求——request_prevote
- 这里会对peer链接做一些合法性检测
- 更改本node的相关状态
hb_alive_ = false;// leader_ = -1;//重置leader role_ = srv_role::candidate;//修改角色 pre_vote_.reset(state_->get_term());//重置预投票的状态 // Count for myself. pre_vote_.dead_++;//本node已离开集群
- 开始分发消息
for (peer_itor it = peers_.begin(); it != peers_.end(); ++it){ pp->send_req(pp, req, resp_handler_); }
1.2 其他node收到pre_vote的处理
- handle_prevote_req
收到请求后先进行检测- 检测是否二者建立有链接,即本node是否有资格进行投票,如果没有资格,就将索引设置为无效值
ulong next_idx_for_resp = 0; auto entry = peers_.find(req.get_src());//判断两个有无建立连接 if (entry == peers_.end()) {//没有连接 // This node already has been removed, set a special value. next_idx_for_resp = std::numeric_limits<ulong>::max();//最大值(非法),用于后面接受发处理 }
- 制定回复信息,这个msg中并没有携带太多信息
ptr<resp_msg> resp ( cs_new<resp_msg> ( req.get_term(), msg_type::pre_vote_response, id_, req.get_src(), next_idx_for_resp ) );
- 是否接收pre_vote的处理
- 满足以下条件之一就接受
- 是否还有hb(如果没有,则表明其已经看不到leader了,可以接受投票)
- 是否是新进节点(如果是新进入的节点,具有预投票资格,说明其和现任leader不是很近)
- 满足以下条件之一就接受
if (!hb_alive_ || catching_up_) { p_in("pre-vote decision: O (grant)"); resp->accept(log_store_->next_slot()); }
- 检测是否二者建立有链接,即本node是否有资格进行投票,如果没有资格,就将索引设置为无效值
node收到预投票的响应
-
判断回复信息的有效性
- 可能在集群内部会因为延迟,之前的预投票响应还没接收到,已经选举出了新的leader,这个时候就需要拒收
if (resp.get_term() != pre_vote_.term_) { // Vote response for other term. Should ignore it. return; }
- 根据接收情况更新pre_vote中的结构
- 如果resp接受预投票:
- pre_vote.dead_++(看不到当前leader的个数加一)
- 如果resp未接受
- 如果resp返回的索引不是最大值,说明二者建立有链接,都为集群内成员,则pre_vote_.live_++(接受当前leader管理的人数)
- 如果索引是最大值,说明二者没建立集群内连接,即该node已经leave了,则pre_vote_.abandoned_++;(因为有的node虽然离开了,但连接还是)
- 如果resp接受预投票:
经过一轮的接收信息,来判断是否发起预投票
- 获取完成投票的法定人数
int32 election_quorum_size = get_quorum_for_election() + 1;
- 判断看不见的node的个数够不够,如果够了,pre_vote.done还是有效,可以发起新一轮的选举
if (pre_vote_.dead_ >= election_quorum_size){ initiate_vote();//发起选举 }
- 如果当前接受leader领导的人数大于了选举人数要求,且拒绝选举的人数也超过了阈值,说明本node可能是离线了,则和leader重新建立一下连接
if (pre_vote_.live_ >= election_quorum_size){ if ( pre_vote_.quorum_reject_count_ >= raft_server::raft_limits_.pre_vote_rejection_limit_ ){ send_reconnect_request(); } }
- 如果离开集群的node个数也超过了选举个数阈值,则本node也可以准备离开该集群
if (pre_vote_.abandoned_ >= election_quorum_size) { steps_to_down_ = 2; }
进行选举前准备(nitiate_vote)
- 本node是否还在catch-up,如果还有需要同步的节点,则获取需要同步的任期数
int grace_period = ctx_->get_params()->grace_period_of_lagging_state_machine_;
- 以下条件判断本node是不是还需要同步日志(都需要满足)
- 进行以下判断
- 不需要强制进行投票
- 存在追赶的日志区间
- 当前日志值小于了目标值
- 如果满足,则阻止本次投票(设定投票阻止投票时钟vote_init_timer_term_),并检测vote_init_timer_term_是否是当前任期的,如果不是,则重新设置一次
if (vote_init_timer_term_ != cur_term) {//vote的timer重置的任期如果不是当前任期,那么就重置一下 p_in("grace period: %d, term increment detected %" PRIu64 " vs. %" PRIu64 ", reset timer", grace_period, vote_init_timer_term_.load(), cur_term); vote_init_timer_.set_duration_ms(grace_period); vote_init_timer_.reset(); vote_init_timer_term_ = cur_term;//该任期重置 }
- 设置了时钟,且时钟还没有超时,则阻止选举
我理解这里时钟的设置,是因为本node如果要发起选举,就需要将日志同步完成,这里就设置了一个定时器,这个定时器在当前任期都有效,如果时钟没有超时,那么说明日志同步没有成功,就不能参与选举,应该是针对新进节点的if ( vote_init_timer_term_ == cur_term &&//如果任期相同了,并且选举器还没有超时,则不选举 !vote_init_timer_.timeout() ) { // Grace period, do not initiate vote. p_in("grace period: %d, term %" PRIu64 ", waited %" PRIu64 " ms, skip initiating vote", grace_period, cur_term, vote_init_timer_.get_ms()); return; }
- 准备选举
- 满足以下任一条件则可选举(优先级达到||集群内就一个节点||存在临时leader)
- 优先级达到
- 强制进行选举
- 是否存在临时leader
- 集群中就本node一个节点
if ( my_priority_ >= target_priority_ ||//优先级达到 force_vote ||//强迫进行选举 check_cond_for_zp_election() ||//存在临时leader的情况 get_quorum_for_election() == 0 )
- 开始选举相关设置初始化
state_->inc_term();//增加任期 state_->set_voted_for(-1);//设定投票人选 role_ = srv_role::candidate;//角色更改 votes_granted_ = 0;//初始化投票相关值 votes_responded_ = 0; election_completed_ = false;
- 开始发起投票请求
request_vote(force_vote);
- 满足以下任一条件则可选举(优先级达到||集群内就一个节点||存在临时leader)
- 进行以下判断
2 发起投票
当前假定pre_vote已经通过,进入投票环节(实现condiate向leader的转换)
2.1 node发起投票
- raft_server
raft中相关日志索引,存放一个节点需要的管理细节
class raft_server {
/*********本机的相关处理文件*********/
std::thread bg_commit_thread_;//线程:提交日志
std::thread bg_append_thread_;//线程:发送日志添加请求
std::atomic<bool> initialized_;//当前服务器是否初始化完毕
/*********投票相关***************/
std::atomic<int32> leader_;//leader的id号
int32 id_;//本机的id号
int32 my_priority_;//本机当前的优先级
int32 target_priority_;//为实现投票的目标优先级
int32 votes_responded_;//响应投票的节点数量
int32 votes_granted_;//给本节点投票的数量
/************提交日志相关******************/
std::atomic<ulong> precommit_index_;//pre_index
/*
当节点是follower时有效,存放leader的commit_index
*/
std::atomic<ulong> leader_commit_index_;
std::atomic<ulong> quick_commit_index_;//提交的目标index,一般和leader_commmit相同
std::atomic<ulong> sm_commit_index_;//状态机中的实际index
/*other*/
std::atomic<bool> hb_alive_;//leader是否存活
bool config_changing_;//如果true,则表明存在未提交的日志
std::atomic<bool> catching_up_;//是否追赶
ptr<state_machine> state_machine_;//本节点的状态机
ptr<cluster_config> config_;//本节点的集群配置文件
ptr<peer> srv_to_join_;
ptr<peer> srv_to_leave_;//确认离开的peer连接
int32 steps_to_down_;//默认为0-node离开节点的剩余步骤数,一旦集群离开节点,就触发一次(后期注意哪里设置的)
std::atomic<bool> out_of_log_range_;//如果置位,表明收到了超出日志范围的日志,则该节点不会发起选举
std::atomic<bool> receiving_snapshot_;//true,正在接收日志
td::atomic<ulong> et_cnt_receiving_snapshot_;//接收日志的超时时间数
std::atomic<bool> serving_req_;//true,还在处理handle_entries中
}
- req_msg
nuraft中请求信息的格式
class msg_base{
ulong term_;//期号
msg_type type_;//消息类型
int32 src_;//发送方ID
int32 dst_;//接收方ID
}
class req_msg : public msg_base {
public:
req_msg(ulong term,
msg_type type,
int32 src,
int32 dst,
ulong last_log_term,
ulong last_log_idx,
ulong commit_idx)
: msg_base(term, type, src, dst)
, last_log_term_(last_log_term)
, last_log_idx_(last_log_idx)
, commit_idx_(commit_idx)
, log_entries_()
{ }
private:
ulong last_log_term_;//最后一个日志的期号
ulong last_log_idx_;//当前拥有的最后一个日志索引(指dest的,例如follower的)
ulong commit_idx_;//指src(ie.leader)当前提交的日志索引
std::vector<ptr<log_entry>> log_entries_; //日志实体(如果不为空,其start_index = last_log_idx_+1)
}
- request_vote
这里会发起投票
- request_vote_request消息的制作
ptr<req_msg> req = cs_new<req_msg>(
state_->get_term(),//本节点的任期号
msg_type::request_vote_request,//消息类型
id_,//本node`s id
pp->get_id(),//目标id号
term_for_log(log_store_->next_slot() - 1),//本日志中的最后一个日志的任期号
log_store_->next_slot() - 1,//本节点中最后一个日志索引
quick_commit_index_.load() //本node的目标(提交)索引
);
2.2 其他node收到投票请求——handle_vote_req
- 判断req中的日志是否满足要求(满足两者情况中之一即有效)
- req中的任期号更加新
- req和本node的任期号相同 && req的日志索引号相等或更大
//req的日志内容要够新
bool log_okay =
req.get_last_log_term() > log_store_->last_entry()->get_term() ||//req的任期号更新
( req.get_last_log_term() == log_store_->last_entry()->get_term() &&//或者(req的任期号一样&&req的索引更新)
log_store_->next_slot() - 1 <= req.get_last_log_idx() );
- 满足以下要求,则接受req请求方的投票要求
- req和本node的节点任期相同
- 日志有效
- 本机还没有投票 || 投过且就投的req的src
bool grant =//接受
req.get_term() == state_->get_term() &&//任期相同
log_okay &&//日志有效
( state_->get_voted_for() == req.get_src() ||//且(本节点投票id与req的发送相同||本节点还没投票)
state_->get_voted_for() == -1 );
- 对req的src投票后还需要做一次检测
- 即集群中的所有节点,是否达到了选举的目标优先级,如果没有,则拒绝投票(为什么这里要这样处理?)
ptr<cluster_config> c_conf = get_config(); for (auto& entry: c_conf->get_servers()) { srv_config* s_conf = entry.get(); if ( !ignore_priority && s_conf->get_id() == req.get_src() && s_conf->get_priority() && s_conf->get_priority() < target_priority_ ) {// p_in("I (%d) could vote for peer %d, " "but priority %d is lower than %d", id_, s_conf->get_id(), s_conf->get_priority(), target_priority_); p_in("decision: X (deny)\n"); return resp; }
- 接受投票,并进行设置
resp->accept(log_store_->next_slot());
state_->set_voted_for(req.get_src());//设置投票id
node收到投票后的响应——handle_vote_resp
- node会统计以下
- resp的响应数量
- resp的接受数量
votes_responded_ += 1;//响应投票数量+1。不管投还是不投,都会回复 if (resp.get_accepted()) {//如果接受了,+1 votes_granted_ += 1; }
- 如果响应人数大于了投票人数,就选举完成
if (votes_responded_ >= get_num_voting_members()) { election_completed_ = true;//如果响应数量达到投票数量要求,投票结束 }
- 如果投票人数达到了法定人数,就可以转换状态了
int32 election_quorum_size = get_quorum_for_election() + 1; if (votes_granted_ >= election_quorum_size) {//如果投本节点的大于这个 p_in("Server is elected as leader for term %" PRIu64, state_->get_term()); election_completed_ = true; become_leader(); p_in(" === LEADER (term %" PRIu64 ") ===\n", state_->get_term()); }
become_leader
- 暂停选举
stop_election_timer();//暂停选举
- 成为leader后,node会对相关标记量进行重置
role_ = srv_role::leader;//更换角色 leader_ = id_;//本node_id为leader`s id srv_to_join_.reset();//重置 leadership_transfer_timer_.set_duration_ms (params->leadership_transfer_min_wait_time_);//成为leader后重新设置成为领导者的最小等待时间 leadership_transfer_timer_.reset();//时钟重新设置
- 更新peer中所有日志索引
- 查询本节点中日志中未提交的索引最大的日志,拿出来存储
- 开始向follower发送日志
request_append_entries();//向F分发日志
- 如果本机节点是一个临时节点,就转移管理权
if (my_priority_ == 0 && get_num_voting_members() > 1) {//如果本node的优先级为0,且满足投票的个数大于1(说明该节点是临时掌管) yield_leadership();//转移领导权 }