nuraft源码笔记——(3)

nuraft3

关于快照的处理和分发

  1. leader什么时候进行一次快照
  2. 什么时候向follower发送快照

leader什么时候制作快照(when/how)

  • when
    • 在nuraft在后台会运行一个commit_in_bg_exec的线程,进行日志提交(后面整理),nuraft中在每次提交之后,会进行一次快照处理
  • how
构建日志1-snapshot_and_compact

制作快照

  • 如果是在日志提交处调用,forced_creation为false
bool raft_server::snapshot_and_compact(ulong committed_idx, bool forced_creation) 
  • 首先提交nuraft的配置文件,检测有效性
    • 如果配置文件中的索引和log_store不一致,则不能创建
    if ( conf->get_prev_log_idx() >= log_store_->next_slot() ) {
        // The latest config and previous config is not in log_store,
        // so skip the snapshot creation.
        return false;
    }
    
    • 获取snapshot_distance:若快照,则committed_idx和log->start_index()需要满足的阈值
    auto snapshot_distance = (ulong)params->snapshot_distance_;
    
    • 如果是第一次进行快照 && 参数设置为随机化distance
    if ( params->enable_randomized_snapshot_creation_ &&
       !snp_in_progress_.load(std::memory_order_relaxed) &&
       !get_last_snapshot() &&
       params->snapshot_distance_ != 0 ) {
      snapshot_distance = first_snapshot_distance_;
      }
    
    • 如果不是强制进行快照
      • 检测commit_index是否满足distance
    if (!forced_creation) {//如果并不是强制创建
      if ( params->snapshot_distance_ == 0 ||
           ( committed_idx - log_store_->start_index() + 1 ) < snapshot_distance )  {
          return false;
          }
      }
    
  • 快照制作(distance满足)
    • 满足以下任一条件,即可进行快照处理
      • (||)forced_creation为true
      • (||)loca_snp为空,即这是第一张快照制作
      • (||)loca_snp不为空,则上一次快照间隔达到距离要求(即要满足和log_store_->start_index的距离,还要满足和上一次快照的距离要求)
      • (&&)没有线程在进行快照处理
      if ( ( forced_creation 
      ||!local_snp 
      ||( committed_idx - local_snp->get_last_log_idx() ) >= snapshot_distance ) 
        &&  snp_in_progress_.compare_exchange_strong(f, true) )
      
    • 配置文件设置为最新,该日志要满足
      (why:可能是conf并不是按顺序存储的,我们要找到一个conf和log_store一致,committed_idx肯定是在log_start_index之前的)
      • 筛选1
        • conf的上一个索引值小于当前log_start_index
        • 同时conf的索引不能超过当前提交的索引
        while ( conf->get_log_idx() > committed_idx 
        && conf->get_prev_log_idx() >= log_store_->start_index() ) 
        {
          ptr<log_entry> conf_log( log_store_->entry_at( conf->get_prev_log_idx() ) );
          conf = cluster_config::deserialize(conf_log->get_buf());
          }
        
        • 这里做了一次检测,后面会继续处理
      • 筛选2
        • while循环中,上一个conf的日志索引小于log_start_index,但conf的日志索引大于commited_idx
        • 就获取本地快照的config
        if ( conf->get_log_idx() > committed_idx &&
           conf->get_prev_log_idx() > 0 &&
           conf->get_prev_log_idx() < log_store_->start_index() ) {//上一个conf比本机的起始还小,但这一个比comi大
          if (!local_snp) {
              ctx_->state_mgr_->system_exit(raft_err::N6_no_snapshot_found);
              ::exit(-1);
              return false;
              // LCOV_EXCL_STOP
          }
          conf = local_snp->get_last_config();
          }
        
        • 我理解这里是想要一个最新的conf对象,
    • 创建snap_shot
      • 索引号
      • 期号
      • 配置文件
          ulong log_term_to_compact = log_store_->term_at(committed_idx);
          ptr<snapshot> new_snapshot( cs_new<snapshot>(committed_idx, log_term_to_compact, conf) );
        cmd_result<bool>::handler_type handler =
                    (cmd_result<bool>::handler_type)
                    std::bind( &raft_server::on_snapshot_completed,
                               this,
                               new_snapshot,
                               std::placeholders::_1,
                               std::placeholders::_2 );//绑定函数
          timer_helper tt;
          state_machine_->create_snapshot(*new_snapshot, handler);//向状态机创建一个快照                        
        
      • 交给状态机进行处理
        state_machine_->create_snapshot(*new_snapshot, handler);
        
构建日志2——快照的后续处理

将快照写入,重新给一个线程保存日志,我理解是不是防止阻塞(这里以cal的例子)

  • cal中对快照的处理方式

    • cal会对create_snapshot重构,根据是否同步来调用(这里我们看阻塞同步)
      void create_snapshot(snapshot& s,
                         async_result<bool>::handler_type& when_done)
    {
        if (!async_snapshot_) {
            // Create a snapshot in a synchronous way (blocking the thread).
            create_snapshot_sync(s, when_done);
        } else {
            // Create a snapshot in an asynchronous way (in a different thread).
            create_snapshot_async(s, when_done);
        }
    }
    
  • create_snapshot_sync

        void create_snapshot_sync(snapshot& s,
                         async_result<bool>::handler_type& when_done)
    
    • 收到最新的快照,进行序列化,并向本机中存储
    ptr<buffer> snp_buf = s.serialize();
    ptr<snapshot> ss = snapshot::deserialize(*snp_buf);
    create_snapshot_internal(ss);
    
  • create_snapshot_internal(ss)

    • 获取快照中的内容信息,并将其放入cal_machine的快照map中
    • 在cal中会存储最近的三张快照
    ptr<snapshot_ctx> ctx = cs_new<snapshot_ctx>(ss, cur_value_);
    snapshots_[ss->get_last_log_idx()] = ctx;
    
    // Maintain last 3 snapshots only.
    const int MAX_SNAPSHOTS = 3;
    int num = snapshots_.size();
    auto entry = snapshots_.begin();
    for (int ii = 0; ii < num - MAX_SNAPSHOTS; ++ii) {
        if (entry == snapshots_.end()) break;
        entry = snapshots_.erase(entry);
    } 
    
  • 在快照制作中,创建了on_snapshot_completed回调函数,其会等到状态机将日志处理,然后在raft_server中设置最新的快照

    void raft_server::on_snapshot_completed
       ( ptr<snapshot>& s, bool result, ptr<std::exception>& err ){
                ptr<snapshot> new_snp = state_machine_->last_snapshot();//将新的快照拿出来
          set_last_snapshot(new_snp);//设置一个新快照
       }
    
    • 同时,如果日志够大,就会压缩log(当创建一次快照,且新快照够新)
  • 疑惑的暂时解答

    • why:为什么Leader更新了一次快照后,follower也会同步一次快照,并同步打印信息
      • 因为每个日志的分发是同步,raft中server的服务设置一样,则leader和follower制作快照的间隔一样,当日志被同步提交后,follower也会同步被进行快照处理。

向follower分发快照

  • 分发快照时,有两个部分会进行follower进行快照分发
    1. 日志同步时,follower的日志和leader的日志之间差距大,leader会先发送一个快照给follower进行同步(例如:某个follower因离线脱离集群,但自身日志在增长,后面又加入集群,这时候leader发送同步日志的请求,可能和L上次连接相比last_log_index过小,这时候就需要follower重新同步一遍,L会先发送快照给F)
    2. 当Follow进行刚加入集群时,需要和对方同步日志
      下面针对第二种情况进行讨论

新节点加入集群,L发送同步快照请求

当F的日志索引间隔超过了L参数设置,则发出快照同步请求

新节点向L发送了接受join的响应,L则调用handle_join_cluster_resp进行处理

  • handle_join_cluster_resp
    • F接受join,L需要则向其同步日志,resp携带了F可以接受的下一个索引
      if (resp.get_accepted()) {
        p_in("new server (%d) confirms it will join, "
             "start syncing logs to it", srv_to_join_->get_id());
        sync_log_to_new_srv(resp.get_next_idx());
    }
    
  • sync_log_to_new_srv
    void raft_server::sync_log_to_new_srv(ulong start_idx)
    
    • 判断一下qm(L提交的目标索引)和start_idx之间的差距
      • 如果qm比start_index,则返回差距大小
      • 如果qm更小,则返回0
      ulong gap = ( quick_commit_index_ > start_idx )
                ? ( quick_commit_index_ - start_idx )
                : 0;
      
      如果差距过大,则会发送快照,如果不大,则直接通过日志同步的方式来实现
      • 检测差距大小,满足以下任何条件则不用发送快照
        • params设置的同步间隔大于0 && F的间隔小于params设置的同步间隔
        • params设置的同步大小为0
        • 满足以上任一条件,则直接生成配置文件,放入conf队列中,并使用日志进行同步
      if ( ( params->log_sync_stop_gap_ > 0 &&
               gap < (ulong)params->log_sync_stop_gap_ ) ||
             params->log_sync_stop_gap_ == 0 ) 
      
      • 如果无法通过日志同步方式实现,则同过其他方式同步
        • 当start_idx比log_start_index还小,则同步日志
        if (/* start_idx > 0 && */ start_idx < log_store_->start_index()) {
          srv_to_join_snp_retry_required_ = false;
          bool succeeded_out = false;
          req = create_sync_snapshot_req( srv_to_join_,//待加入的peer
                                          start_idx,//F的可写入索引值
                                          state_->get_term(),//L的期号
                                          quick_commit_index_,//L的提交日志期号
                                          succeeded_out );}
      
      • 如果start_idx大于log_start_index
        • 则发送"msg_type::sync_log_request"请求
          req = cs_new<req_msg>( state_->get_term(),//L的期号
                               msg_type::sync_log_request,//消息类型
                               id_,//src
                               srv_to_join_->get_id(),//dest
                               0L,
                               start_idx - 1,//F的日志last_idx
                               quick_commit_index_.load() );
        req->log_entries().push_back
            ( cs_new<log_entry>
              ( state_->get_term(), log_pack, log_val_type::log_pack) );
        
  • create_sync_snapshot_req
    ptr<req_msg> raft_server::create_sync_snapshot_req(ptr<peer>& pp,
                                                     ulong last_log_idx,
                                                     ulong term,
                                                     ulong commit_idx,
                                                     bool& succeeded_out)
    
    • 判断现在是否peer中是否有内容,如果有则取出来
      ptr<snapshot_sync_ctx> sync_ctx = p.get_snapshot_sync_ctx();
      
      • 如果peer中存在快照了,就直接使用,进一步获取sync_ctx中的快照
            if (sync_ctx) {
          snp = sync_ctx->get_snapshot();//获取peer中这个快照
          p_db( "previous sync_ctx exists %p, offset %" PRIu64 ", snp idx %" PRIu64
                ", user_ctx %p",
                sync_ctx.get(),
                sync_ctx->get_offset(),
                snp->get_last_log_idx(),
                sync_ctx->get_user_snp_ctx() );
          prev_sync_snp_log_idx = snp->get_last_log_idx();//获取这个快照的idx,并记录"上一个快照日志记录"
        }
        
    • 如果快照中不存
      • 从本地获取最新的快照内容
      • 重新设置peer中的同步快照
      if ( !snp || sync_ctx->get_offset() == 0 ){
        snp = get_last_snapshot();
          if (sync_ctx) {
              destroy_user_snp_ctx(sync_ctx);//销毁
          }
        p.set_snapshot_in_sync(snp, snp_timeout_ms);//重新向peer中放置快照
      }
      
    • 进一步获取日志
      • 获取peer中的日志内容
      • 获取偏移(我理解因为cal中使用了一个map进行存放)
      • 从cal中将快照中获取数据
        • (暂时的理解:snp中存放的是管理数据,read_logical_snp_obj会读取cal中对应索引的数据)
        ptr<buffer> data = nullptr;
        ulong data_idx = 0;
        sync_ctx = p.get_snapshot_sync_ctx();//获取peer中的日志内容
        ulong obj_idx = sync_ctx->get_offset();//快照的索引
        void*& user_snp_ctx = sync_ctx->get_user_snp_ctx();
        int rc = state_machine_->read_logical_snp_obj( *snp, user_snp_ctx, obj_idx,data, last_request );//读取快照内容
        
    • 制作快照同步请求并装入request中
        std::unique_ptr<snapshot_sync_req> sync_req
          ( new snapshot_sync_req(snp, data_idx, data, last_request) );
        ptr<req_msg> req( cs_new<req_msg>
                        ( term,//期号
                          msg_type::install_snapshot_request,
                          id_,//src
                          p.get_id(),//dest
                          snp->get_last_log_term(),//snp的期号
                          snp->get_last_log_idx(),//snp的索引号
                          commit_idx ) );
       req->log_entries().push_back( cs_new<log_entry>
                                    ( term,
                                      sync_req->serialize(),
                                      log_val_type::snp_sync_req    ) );
      

F收到同步快照的请求

  • handle_install_snapshot_req(处理快照同步请求)
    • 对本机角色做判断
      • 如果是候选人,则变为follower
      • 如果是leader,则报错
      • 如果是follower就重启一次选举时钟
    • 制作回复信息
      ptr<resp_msg> resp = cs_new<resp_msg>
                         ( state_->get_term(),//本机期号
                           msg_type::install_snapshot_response,
                           id_,//src
                           req.get_src(),//dest
                           log_store_->next_slot() );//本机的下一个可用索引
      
    • 如果不存在追赶 && 本机期号更大,则退出
      if (!catching_up_ && req.get_term() < state_->get_term())   {
          return resp;
      }
      
    • 获取req中日志信息
      std::vector<ptr<log_entry>>& entries(req.log_entries());
      ptr<snapshot_sync_req> sync_req = snapshot_sync_req::deserialize(entries[0]->get_buf());
      
    • 如果快照中的索引没有本机目标提交索引大,则退出(说明目前的够新了)
      if (sync_req->get_snapshot().get_last_log_idx() <= quick_commit_index_){
        return resp
      }
      
    • 检测完毕,则开始处理快照同步的请求
    if (handle_snapshot_sync_req(*sync_req))
    
  • handle_snapshot_sync_req(*sync_req)

    这里会将快照中的信息,存放到本机中

    • 读取req中的内容,并将其存放到状态机的快照容器中
      ulong obj_id = req.get_offset();//获取偏移
      buffer& buf = req.get_data();//获取数据
      buf.pos(0);
      state_machine_->save_logical_snp_obj(req.get_snapshot(),
                                           obj_id,
                                           buf,
                                           is_first_obj,
                                           is_last_obj);//向本node的cal(state_machine中存储)
      req.set_offset(obj_id);
      
    • 如果这是最后一个对象,还会进行一些处理…(其中就有压缩日志的过程)

L收到F的快照同步的回复

raft_server::handle_install_snapshot_resp(resp_msg& resp),L接受后,会根据收到的消息状态,来判断对方是否需要追赶

  • handle_install_snapshot_resp
    • 如果F接收成功
      • 获取peer中快照
     ptr<snapshot> snp = sync_ctx->get_snapshot();
    
    • 如果同步成功,则进一步判断是否需要追赶
      • 我理解如果peer中F的日志最大索引还是比L小,则需要追赶
      need_to_catchup = p->clear_pending_commit() ||
                                      p->get_next_log_idx() < log_store_->next_slot();
      
    • 如果需要追赶,且本node还是leader(因为可能存在延时的情况,本机不再是leader,就没必要让对方追赶),Leader分发日志,让F同步
      if (role_ == srv_role::leader && need_to_catchup) {
        request_append_entries(p);
      }
      

压缩过程

inmem_log_store::compact(ulong last_log_index)

  • 大概过程,就是只保留last_log_index对应的索引文件
    std::lock_guard<std::mutex> l(logs_lock_);
    for (ulong ii = start_idx_; ii <= last_log_index; ++ii) {
      auto entry = logs_.find(ii);
      if (entry != logs_.end()) {
          logs_.erase(entry);
      }
    }
    if (start_idx_ <= last_log_index) {
      start_idx_ = last_log_index + 1;
    }
    return true;
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值