nuraft源码解读——(2)

raft

梳理一遍raft和F和L之间同步日志的过程
在日志中,会有两个部分同步日志,一个是在提交时,会发送一个同步请求,另一个是server会在后台维护一个同步日志的线程

commits时的信息分发

  • 当提交leader提交日志后,会主动向Follower分发信息
    if (role_ == srv_role::leader) {
        for (peer_itor it = peers_.begin(); it != peers_.end(); ++it) {
            ptr<peer> pp = it->second;
            if (!request_append_entries(pp)) {
                pp->set_pending_commit();
            }
        }
    }
    

node发送request:append_entries

handle_append_entry

后台维护的日志线程

  • 一个节点启动时,会创建一个append_entries的线程,来实现同步日志的需求(如果server没有global_manager就会启动这个线程)
bg_append_thread_ = std::thread(std::bind(&raft_server::append_entries_in_bg, this));
  • append_entries_in_bg中维护着一个循环,这个循环会持续调用request_append_entries来发送同步日志的请求,request_append_entries则会对该node中的所有连接循环发送通过请求
  for (peer_itor it = peers_.begin(); it != peers_.end(); ++it) {
      request_append_entries(it->second);
  }

在进行分发日志前,会需要做一些集群管理设置,这个时候会需要调用集群内的参数raft_params,下面则是一些会用到的成员变量

int32 heart_beat_interval_; // 心跳的间隔时间
int32 log_sync_batch_size_;//用于追赶,rpc最多能装载的日志数
int32 log_sync_stop_gap_; //用于记录新节点加入后,与leaders相差的日志间隙,如果值为0,就可以立即加入peer list中
int32 snapshot_distance_;//创建raft快照的日志间隔
int32 max_append_size_;//rpc连接中日志中可以追加的最大日志数
int32 reserved_log_items_;//(防止日志被压缩)记录上一次raft快照后被保存的最小日志数量
int32 fresh_log_gap_;//如果一个F和L的日志差距超过这个值,则认定其为新鲜节点
int32 stale_log_gap_;//如果超过这个值,就认定其为过时节点
int32 custom_commit_quorum_size_;//指定一个commmit的法定数量,如果为0,就使用默认值
bool auto_adjust_quorum_for_small_cluster_;//针对小集群的,如果该值为true,且集群中个数为2,一旦有节点离线,那么quorum_size就会为1
int32 grace_period_of_lagging_state_machine_;//该值表明新节点catch-up的间隔大小,如果不为0,则该节点是不能参与选举的
  • 对集群规模进行检测(针对小集群的情况)
    • 满足以下条件则需要调整集群:auto_adjust_quorum_for_small_cluster_为true,且集群的投票人数只有2
      • 两个节点的情况,则需要进一步的进行调整
        size_t num_not_responding_peers = get_not_responding_peers();//获取未响应人数
        size_t cur_quorum_size = get_quorum_for_commit();//获取法定投票人数(意思是如果如果有的F在处理快照,我们就暂时不向其发送?)
        size_t num_stale_peers = get_num_stale_peers();//陈旧的peer(即对方log和server的log差的有点多)
        if (cur_quorum_size >= 1) {//法定人数还是大于1(这三个数的设定需要继续看看)
        bool do_adjustment = false;//默认不调整
        if (num_not_responding_peers) {//如果还是存在没有响应的人数,则应调整
            p_wn("2-node cluster's follower is not responding long time, "
                 "adjust quorum to 1");
            do_adjustment = true;
        } else if (num_stale_peers) {//如果有老旧的连接
            p_wn("2-node cluster's follower is lagging behind, "
                 "adjust quorum to 1");
            do_adjustment = true;//进行调整
        }
        if (do_adjustment) {//调整:经过上面处理,认定存在失效的情况,就设定为1,即如果有不响应的,有老旧连接,都对人数更新
            ptr<raft_params> clone = cs_new<raft_params>(*params);
            clone->custom_commit_quorum_size_ = 1;
            clone->custom_election_quorum_size_ = 1;
            ctx_->set_params(clone);//将选举人数修改为1,这里应该就是更新一下数据
        }
        
        } else if ( num_not_responding_peers == 0 &&
                num_stale_peers == 0 &&
                params->custom_commit_quorum_size_ == 1 ) {//如果法定人数只有1,且没有其他无效人数,说明集群也没啥F,设为0(默认值)
                // Recovered, both cases should be clear.
                p_wn("2-node cluster's follower is responding now, "
                     "restore quorum with default value");
                ptr<raft_params> clone = cs_new<raft_params>(*params);
                clone->custom_commit_quorum_size_ = 0;
                clone->custom_election_quorum_size_ = 0;
                ctx_->set_params(clone);
        }
        
        • 上面会进一步检测集群中的成员情况,主要检测是不是只有一个节点可用
  • 对peer连接情况进行检测
    上面针对小集群的情况做出调整后,下一步就找peer连接是否有效
    • 如果F的上次活跃时间大于了心跳间隔*重连限制,且未确认离开集群,则重连
    • 重新建立rpc连接后,会对peer中的日志idx进行重新设置
  • 占用peer连接,准备发送
    • 判断peer中是否有保留未发送的信息?
      • 如果有,就采用,如果没有,则create一个request消息
      ptr<req_msg> msg = p->get_rsv_msg();//如果有响应,先处理响应
      if (msg) {//如果peer保留有信息
        // Clear the reserved message.
        p->set_rsv_msg(nullptr, nullptr);//清空
        p_in("found reserved message to peer %d, type %d",
             p->get_id(), msg->get_type());
      } else {//如果没有,就造一个
        // Normal message.
        msg = create_append_entries_req(p);
        m_handler = resp_handler_;
        }
      
      • 发送之前,检测该连接是否已离开
        • 如果确认是同意离开但未离开的节点,则会step_down(如果节点正规离开节点,貌似要流程)
        • 然后进行删除
      if ( srv_to_leave_ &&//同意离开的peer list
       srv_to_leave_->get_id() == p->get_id() &&
       msg->get_commit_idx() >= srv_to_leave_target_idx_ &&
       !srv_to_leave_->is_stepping_down() ) {
      srv_to_leave_->step_down();
      p_in("srv_to_leave_ %d is safe to be erased from peer list, "
           "log idx %" PRIu64 " commit idx %" PRIu64 ", set flag",
           srv_to_leave_->get_id(),
           msg->get_last_log_idx(),
           msg->get_commit_idx());
      }
      

create_append_entries_req

创建同步日志的请求

  • 获取Follower和Leader的日志索引

    Leader:
    starting_idx = log_store_->start_index();//L的第一个日志索引
    cur_nxt_idx = precommit_index_ + 1;//L的下一个预提交索引(L的最新索引)
    commit_idx = quick_commit_index_;//L的目标提交索引
    term = state_->get_term();//当前任期
    Follower:
    last_log_idx = p.get_next_log_idx() - 1;//连接中复制给F的最后一个日志索引
    
  • 获取一个日志有效性标志

    bool entries_valid = (last_log_idx + 1 >= starting_idx);
    

    如果给Follower的索引,大于了Leader的起始日志索引,那么认定还是有效的,这个值后面会用到

  • 确定这次传输中可传递的最大索引值

      ulong end_idx = std::min( cur_nxt_idx,
                                  last_log_idx + 1 + ctx_->get_params()->max_append_size_ );
      //如果L的最新的log索引在传输的最大值范围内,就选择其作为end_idx,否则就选择rpc能传输的最大值
    
  • 装载日志

    • 根据F的日志索引情况,分别去获取不同的日志
      • 如果F的最大索引值>L的最大索引值
        • 暂时不装载
      • 如果F最大索引值<L的最大索引值,且entries_vaild未true(即在L的log范围中)
        • 装载日志
    if ((last_log_idx + 1) >= cur_nxt_idx) {//超过了起始
        log_entries = ptr<std::vector<ptr<log_entry>>>();//F的大于了L,日志就放空(因为没有相同的了)
    } else if (entries_valid) {//(但没完全超过)F的没有超过L的index,但是超过了L的起始,意思是二者还存在一些相同的
        log_entries = log_store_->log_entries_ext(last_log_idx + 1, end_idx,
                                                  p.get_next_batch_size_hint_in_bytes());//给它补齐
        if (log_entries == nullptr) {
            p_wn("failed to retrieve log entries: %" PRIu64 " - %" PRIu64,
                 last_log_idx + 1, end_idx);
            entries_valid = false;//如果一切正常,但是log没取到,就说明这段不存在
        }
    }  
    
    • 如果F的日志索引没有在L的日志范围内,即entries_vaild为false
      • 如果本地有快照,且快照的最大值>F的日志最大值,那么可以从快照中获取日志进行同步
    if ( snp_local && // 但是快照中的idx也不是最新(还是要进一步弄清楚日志中log和快照的处理,什么时候nuraft会整个snop)
       last_log_idx < starting_idx &&
       last_log_idx < snp_local->get_last_log_idx() 
      bool succeeded_out = false;
      return create_sync_snapshot_req( pp, last_log_idx, term,
                                       commit_idx, succeeded_out );}
    
    • 如果快照中的日志也不能满足,可能是存在越界情况,向Follower发送提醒
      ptr<req_msg> req = cs_new<req_msg>
                     ( term, msg_type::custom_notification_request,
                       id_, p.get_id(), 0, last_log_idx, commit_idx );
      
  • 走到这,经过对相关日志的有效性判断和处理,继续向下说明日志已经有效的复制了

    • 创建request
    ptr<req_msg> req
      ( cs_new<req_msg>
        ( term, //L的当前期号
        msg_type::append_entries_request, //消息类型
        id_, //src
        p.get_id(),//dest
          last_log_term, //F最后一个log_index的期号(应该是F的期号)
          last_log_idx, //F的最后一个日志索引
          commit_idx //L提交的索引大小
          ) );
      if (log_entries) {
          v.insert(v.end(), log_entries->begin(), log_entries->end());//这里创建了一个append请求,将日志内容进行插入
      }
      p.set_last_sent_idx(last_log_idx + 1);//更新p发送的索引大小    
    

node接受到一个日志复制请求

handle_append_entries

  • 如果在执行过程中,出现了选举,保存以下结构

    struct ServingReq {
      ServingReq(std::atomic<bool>* _val) : val(_val) { val->store(true); }
      ~ServingReq() { val->store(false); }
      std::atomic<bool>* val;
      } _s_req(&serving_req_);
    
  • 检测任期

    • 如果req的任期和本node的状态机一致
      • 如果本node当前是candidate,则更改为follower
      • 如果本node是leader
        • 那一定是出错
    if (req.get_term() == state_->get_term()) {//二者日期一致
        if (role_ == srv_role::candidate) {//看下srv_role怎么来的
            become_follower();//这里也会触发选举
        } else if (role_ == srv_role::leader) {
            p_wn( "Receive AppendEntriesRequest from another leader (%d) "//出现两个leader,bug
                  "with same term, there must be a bug. Ignore it instead of exit.",
                  req.get_src() );
            return nullptr;
        } else {
            update_target_priority();//如果都不是,就更新优先级
        }
    }
    
  • 对日志信息进行检测

    • 如果req中的last_log_idx<本node的最大索引的下一个
      • 正常情况下,req中的索引应该等于log_store_->next_slot()-1
      • 这里处理,从而获得对应的期号,方便下面的处理
      if (req.get_last_log_idx() < log_store_->next_slot()) {//如果req的log小于了本地log
      log_term = term_for_log( req.get_last_log_idx() );//根据req中的索引号,找到对应在本地的一个term
      }
      
    • 判断日志是否ok,满足以下条件,就算log_okay
      • req中的last_log_idx为0,表明这是首次连接,算ok
      • req中的last_log_idx能在本node下找到期号且该期号与req中的期号一致说明,还是一个老连接
      • 存在本地快照且本地快照中的最后一个索引、期号和req中的last_log_idx、期号一致
    • log_okay的情况:
      • 新连接
      • 老连接
      • 最新的快照中满足req的索引要求
      • 如果都不存在,就直接返回,填上本机最大日志索引对比
  • 如果req的期号比本node的期号更小||log_okay为否,则直接返回

    • 如果req期号更小,但是log_okay为true,说明本node出现了一些差错,还是可以通过回滚的方式调整回来,log_okay中第三种情况都不满足,那么说明F和L之间需要重新建立一致性。
  • 经过上面处理,说明这里是一个有效连接,可以进行日志复制了

    • 找到可插入的日志索引值
      • 获取req上次插入node日志的位置
      • 通过任期号的对比,来跳过已复制过的日志
    ulong log_idx = req.get_last_log_idx() + 1;//
    while ( log_idx < log_store_->next_slot() &&
            cnt < req.log_entries().size() ){
        if ( log_store_->term_at(log_idx) ==//通过任期号的对比,来跳过以复制的
                 req.log_entries().at(cnt)->get_term() ) {
            log_idx++;
            cnt++;
        } else {
            break;
        }
    }  
    
    • 判断是否需要回滚
      • 获取本node的索引最大值
    ulong my_last_log_idx = log_store_->next_slot() - 1;
    
    • 如果req中可插入的log_index,比本机node的log的日志最大值还大(说明,本node还有更多的日志,例如论文中提到的,本node可能自己突然离线自己成为领导,并同步更新日志,后来恢复连线,则需要重新同步一遍日志,将本node的日志进行回滚)
      • 更新commit的索引值
        • 如果quick_commit_index比log_index大,则将qm设置log_index-1
        • 如果sm_commit_index比log_index大,则将sm设置log_index-1
      • 然后将日志回滚到,将日志回滚到log_idx
    bool rollback_in_progress = false;//默认不回滚
    if ( my_last_log_idx >= log_idx &&
         cnt < req.log_entries().size() ) {
        rollback_in_progress = true;//则进行回滚
        if ( quick_commit_index_ >= log_idx ) {//更新quick_commit_index_
            p_wn( "rollback quick commit index from %" PRIu64 " to %" PRIu64,
                  quick_commit_index_.load(),
                  log_idx - 1 );
            quick_commit_index_ = log_idx - 1;
        }
        if ( sm_commit_index_ >= log_idx ) {//更新sm_commit_index_值
            p_er( "rollback sm commit index from %" PRIu64 " to %" PRIu64 ", "
                  "it shouldn't happen and may indicate data loss",
                  sm_commit_index_.load(),
                  log_idx - 1 );
            sm_commit_index_ = log_idx - 1;
        }
        //进行日志回滚
        for ( uint64_t ii = 0; ii < my_last_log_idx - log_idx + 1; ++ii ) {
            uint64_t idx = my_last_log_idx - ii;//这里进行回退
            ptr<log_entry> old_entry = log_store_->entry_at(idx);
            if (old_entry->get_val_type() == log_val_type::app_log) {
                ptr<buffer> buf = old_entry->get_buf_ptr();
                buf->pos(0);
                state_machine_->rollback_ext
                    ( state_machine::ext_op_params( idx, buf ) );
                p_in( "rollback log %" PRIu64 ", term %" PRIu64,
                      idx, old_entry->get_term() );
    
            } else if (old_entry->get_val_type() == log_val_type::conf) {
                p_in( "revert from a prev config change to config at %" PRIu64,
                      get_config()->get_log_idx() );
                config_changing_ = false;
            }
        }
    }
    
    • 开始写入日志
      • 如果本node中存在log_store的超过了log_idx的部分,则先对这部分日志进行覆写,重新提交
    while ( log_idx < log_store_->next_slot() && 
           cnt < req.log_entries().size() )
    {
       ptr<log_entry> entry = req.log_entries().at(cnt);//获取req中的日志
       store_log_entry(entry, log_idx);//覆盖写一次
       if (entry->get_val_type() == log_val_type::app_log) {
           ptr<buffer> buf = entry->get_buf_ptr();
           buf->pos(0);
           state_machine_->pre_commit_ext
               ( state_machine::ext_op_params( log_idx, buf ) );//cal中也啥都没做
    
       } else if(entry->get_val_type() == log_val_type::conf) {
           p_in("receive a config change from leader at %" PRIu64, log_idx);
           config_changing_ = true;
       }
       log_idx += 1;
       cnt += 1;
       if (stopping_) return resp;
    }
    
    • 这里就到正常部分了,执行操作和上面是一样的,但是在nuraft中将其分开处理
    while (cnt < req.log_entries().size()) {//这里应该就是在加新日志了
      ptr<log_entry> entry = req.log_entries().at( cnt++ );
      p_tr("append at %" PRIu64 ", term %" PRIu64 ", timestamp %" PRIu64 "\n",
           log_store_->next_slot(), entry->get_term(), entry->get_timestamp());
      ulong idx_for_entry = store_log_entry(entry);
      if (entry->get_val_type() == log_val_type::conf) {//继续写入
          p_in( "receive a config change from leader at %" PRIu64,
                idx_for_entry );
          config_changing_ = true;
    
      } else if(entry->get_val_type() == log_val_type::app_log) {
          ptr<buffer> buf = entry->get_buf_ptr();
          buf->pos(0);
          state_machine_->pre_commit_ext
              ( state_machine::ext_op_params( idx_for_entry, buf ) );//如果是日志文件,就应该向状态机写入,但是这里cal中状态机是空
      }
      if (stopping_) return resp;
    }
    

    上面在对日志处理中,并没有直接涉及到对log_store,而是在重新对状态机进行提交,上面整理完之后,再将日志同意存储log_stroe中

    log_store_->end_of_append_batch( req.get_last_log_idx() + 1,
                                           req.log_entries().size() );
    
  • 再确认leader

    leader_ = req.get_src();
    
  • 更新提交的所有索引值 & 选择合适的索引值进行提交

    leader_commit_index_.store(req.get_commit_idx());
    ulong target_precommit_index = req.get_last_log_idx() + req.log_entries().size();
    commit( std::min( req.get_commit_idx(), target_precommit_index ) );
    resp->accept(target_precommit_index + 1);
    
  • (附)resp中的日志内容

    ptr<resp_msg> resp = cs_new<resp_msg>( 
      state_->get_term(),//本node的任期号
      msg_type::append_entries_response,//消息内容
      id_,//src
      req.get_src(),//dest
      log_store_->next_slot()//本机最大日志 
      );
      //回复的时候
      resp->accept(target_precommit_index + 1);//这里会重新更新resp中的next_index,即本node收到的最大索引大小
    
  • 小总结

    • 先同步日志索引,再进行提交

node接受到响应

  • 首先看链接是否有效,遍历一下

    peer_itor it = peers_.find(resp.get_src());
    if (it == peers_.end()) {
        p_in("the response is from an unknown peer %d", resp.get_src());
        return;
    }
    
    • 查看一下,这个node是不是离开状态了
    if ( srv_to_leave_ &&
       srv_to_leave_->get_id() == resp.get_src() &&
       srv_to_leave_->is_stepping_down() &&
       resp.get_next_idx() > srv_to_leave_target_idx_ ) {
      // Catch-up is done.
      p_in("server to be removed %d fully caught up the "
           "target config log %" PRIu64,
           srv_to_leave_->get_id(),
           srv_to_leave_target_idx_);
      remove_peer_from_peers(srv_to_leave_);
      reset_srv_to_leave();
      return;
    }
    
  • 如果对方接受了请求

    • 更新peer中的相关索引值
    if (resp.get_accepted()) {//这里接受了请求,就更新了匹配的索引
      uint64_t prev_matched_idx = 0;
      uint64_t new_matched_idx = 0;
      {
          std::lock_guard<std::mutex> l(p->get_lock());
          p->set_next_log_idx(resp.get_next_idx());
          prev_matched_idx = p->get_matched_idx();
          new_matched_idx = resp.get_next_idx() - 1;
          p->set_matched_idx(new_matched_idx);
          p->set_last_accepted_log_idx(new_matched_idx);
      }
      cb_func::Param param(id_, leader_, p->get_id());
      param.ctx = &new_matched_idx;
      CbReturnCode rc = ctx_->cb_func_.call
                        ( cb_func::GotAppendEntryRespFromPeer, &param );
      (void)rc;
      ulong committed_index = get_expected_committed_log_idx();
      commit( committed_index );
      need_to_catchup = p->clear_pending_commit() ||
                        resp.get_next_idx() < log_store_->next_slot();
    }
    
    • 判断下对方是否需要catchup
    need_to_catchup = p->clear_pending_commit() ||
                        resp.get_next_idx() < log_store_->next_slot();
    
    • 如果同时满足以下条件,leader接收到信息后,就需要重新触发选举
      • 如果正处于选举的第一阶段
      • 如果连接的对方是下一任的候选人
      • 且以完成匹配
      • (完成日志同步后,且受到了选举指令,则停止leader权限,转向Follower)
      • 并发送一个custom_notification_request的信息
    if ( write_paused_ &&
         p->get_id() == next_leader_candidate_ &&
         p_matched_idx &&
         p_matched_idx == log_store_->next_slot() - 1 &&
         p->make_busy() ) {
        leader_ = -1;
        rand_timeout_ = [this]() -> int32 {
            return this->ctx_->get_params()->election_timeout_upper_bound_ +
                   this->ctx_->get_params()->election_timeout_lower_bound_;
        };
        become_follower();
        update_rand_timeout();
        hb_alive_ = false;
        ptr<req_msg> req = cs_new<req_msg>
                           ( state_->get_term(),
                             msg_type::custom_notification_request,
                             id_, p->get_id(),
                             term_for_log(log_store_->next_slot() - 1),
                             log_store_->next_slot() - 1,
                             quick_commit_index_.load() );
        ptr<custom_notification_msg> custom_noti =
            cs_new<custom_notification_msg>
            ( custom_notification_msg::leadership_takeover );
        ptr<log_entry> custom_noti_le =
            cs_new<log_entry>(0, custom_noti->serialize(), log_val_type::custom);
        req->log_entries().push_back(custom_noti_le);
        p->send_req(p, req, resp_handler_);
        return;
    }
    
  • 如果need_to_catchup为true,则进行追加

    if (role_ == srv_role::leader) {
      if (need_to_catchup) {
          p_db("reqeust append entries need to catchup, p %d\n",
               (int)p->get_id());
          request_append_entries(p);}
    }
    
  • custom_notification_request

    • 这是一个自定提醒消息,其中内含各种细节上的请求,接收方会进行解析并处理
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值