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); }
- 上面会进一步检测集群中的成员情况,主要检测是不是只有一个节点可用
- 两个节点的情况,则需要进一步的进行调整
- 满足以下条件则需要调整集群:auto_adjust_quorum_for_small_cluster_为true,且集群的投票人数只有2
- 对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()); }
- 判断peer中是否有保留未发送的信息?
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范围中)
- 装载日志
- 如果F的最大索引值>L的最大索引值
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 );
- 根据F的日志索引情况,分别去获取不同的日志
-
走到这,经过对相关日志的有效性判断和处理,继续向下说明日志已经有效的复制了
- 创建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的任期和本node的状态机一致
-
对日志信息进行检测
- 如果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中的last_log_idx<本node的最大索引的下一个
-
如果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
- 更新commit的索引值
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, ¶m ); (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
- 这是一个自定提醒消息,其中内含各种细节上的请求,接收方会进行解析并处理