nuraft3
关于快照的处理和分发
- leader什么时候进行一次快照
- 什么时候向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对象,
- 筛选1
- 创建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也会同步被进行快照处理。
- why:为什么Leader更新了一次快照后,follower也会同步一次快照,并同步打印信息
向follower分发快照
- 分发快照时,有两个部分会进行follower进行快照分发
- 日志同步时,follower的日志和leader的日志之间差距大,leader会先发送一个快照给follower进行同步(例如:某个follower因离线脱离集群,但自身日志在增长,后面又加入集群,这时候leader发送同步日志的请求,可能和L上次连接相比last_log_index过小,这时候就需要follower重新同步一遍,L会先发送快照给F)
- 当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) );
- 判断一下qm(L提交的目标索引)和start_idx之间的差距
- 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中存在快照了,就直接使用,进一步获取sync_ctx中的快照
- 如果快照中不存
- 从本地获取最新的快照内容
- 重新设置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 ) );
- 判断现在是否peer中是否有内容,如果有则取出来
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);
- 如果这是最后一个对象,还会进行一些处理…(其中就有压缩日志的过程)
- 读取req中的内容,并将其存放到状态机的快照容器中
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); }
- 如果F接收成功
压缩过程
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; }