nuraft源码笔记——(1)

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收到预投票的响应

  • 判断回复信息的有效性

    • 可能在集群内部会因为延迟,之前的预投票响应还没接收到,已经选举出了新的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虽然离开了,但连接还是)

    经过一轮的接收信息,来判断是否发起预投票

    • 获取完成投票的法定人数
    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;//该任期重置
      }
      
      • 设置了时钟,且时钟还没有超时,则阻止选举
      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;
      
      }
      
      我理解这里时钟的设置,是因为本node如果要发起选举,就需要将日志同步完成,这里就设置了一个定时器,这个定时器在当前任期都有效,如果时钟没有超时,那么说明日志同步没有成功,就不能参与选举,应该是针对新进节点的
    • 准备选举
      • 满足以下任一条件则可选举(优先级达到||集群内就一个节点||存在临时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);
      

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();//转移领导权
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值