本人最近仔细研究ceph 恢复部分的源代码,这个阅读分析的过程比较艰难,分享在此,希望大家能互相交流学习,有不正确的地方,希望大家指正!
Peering的作用
Peer的过程,是使一个PG内的OSD达成一个一致状态,当主从副本完成达成一个一致的状态,peering 的状态就结束,PG处于active状态。但此时,该PG的三个OSD的数据副本并非完全一致。
基本概念
acting set 和 up set
acting set 就是一个pg对应的副本所在的osd 列表,列表是有序的,第一个osd 为 primary. 在通常情况下,up set和acting set 相同,要理解不同之处,需要理解pg_temp
pg temp
如果pg的acting set 为[0,1,2], 此时如果osd.0出现故障,导致CRUSH算法重新分配该pg的acting set 为[3,1,2]. 此时osd.3为该pg的主osd,但是osd.3并不能负担该pg的读操作,因为其上现在还没有数据。所以向monitor申请一个临时的pg,osd.1为临时主osd,这时acting set依然为[0,1,2],up set 变为[1,3,2],此时就出来acting set 和up set的不同。当osd.3 backfill完成之后,该pg的up set恢复为acting set, 也就是acting set 和 up set都为[0,1,2]
current interval 和 past_interval
都是epoch的一个序列,在这个序列之内,一个pg的acting set 和 up set不会变化。current是当前的序列,past是上一个阶段的序列。
last_epoch_started: pg peering完成之后的epoch
last_epoch_clean: pg recovery完成,处于clean状态的epoch
例如:
ceph 系统当前的epoch值为20, pg1.0 的 acting set 和 up set 都为[0,1,2]
- osd.3失效导致了osd map变化,epoch变为 21
- osd.5失效导致了osd map变化,epoch变为 22
- osd.6失效导致了osd map变化,epoch变为 23
上述三次epoch的变化都不会改变pg1.0的acting set和up set
- osd.2失效导致了osd map变化,epoch变为 24
此时导致pg1.0的acting set 和 up set变为 [0,1,8],若此时 peering过程成功完成,则last_epoch_started 为24
- osd.12失效导致了osd map变化,epoch变为 25
此时如果pg1.0完成了recovery,处于clean状态,last_epoch_clean就为25
- osd13失效导致了osd map变化,epoch变为 26
epoch 序列 21,22,23,23 就为pg1.0的past interval
epoch 序列 24,25,26就为 pg1.0的current interval
pg log
保存了该pg内所有更新操作的日志记录,具体记录的字段,可以查看struct pg_log_entry_t,这里列出关键的字段:
- __s32 op; 操作的类型
- hobject_t soid; 操作的对象
- eversion_t version, prior_version, reverting_to; 操作的版本
需要注意的是,pg log的记录是以整个PG 为单位,而不是以对象为单位
PG info
关于pg在osd上的一些元数据信息,具体保存struct pg_info_t 数据结构里
列举一些重要的字段:
- eversion_t last_update; //pg 最后一次更新的eversion
- eversion_t last_complete; //recovery完成后的最后一个everson,也就是pg处于clean状态的最后一次操作的 eversion
- epoch_t last_epoch_started;// last epoch at which this pg started on this osd 这个pg在这个osd上的最近的的开始的epoch,也就是最近一次peering完成后的epoch
- version_t last_user_version; // last user object version applied to store
- eversion_t log_tail; // 日志的尾,也就是日志最老的日志记录
- hobject_t last_backfill; // objects >= this and < last_complete may be missing
最后一个backfill 操作的对象,如果该osd的backfill没有完成,那么last_backfill 和last_complete之间的对象就丢失
Authoritative History
权威日志,在代码里简写为olog. 某个pg的一个完整的,顺序的操作日志。
eversion_t head; // 日志头,保存最新写入的日志
eversion_t tail; // 日志尾,最后早写入的日志
up_thru
这个实在不适合特别理解的概念
Peering的时机
当系统初始化时,或者有OSD 失效,或者OSD增加或者删除,导致OSD map发生变化,这引起PG的 acting set的变化,该PG会发起Peering过程。
Peering的基本过程
Peering的过程,基本分为三个步骤
- GetInfo : pg的主osd通过发送消息获取各个从OSD的pg_info信息
- GetLog:根据pg_info的比较,选择一个拥有权威日志的osd(auth_log_shard) , 如果主osd不是拥有权威日志的osd,就去该osd上获取权威日志
- GetMissing:拉取其它从OSD 的pg log(或者部分获取,或者全部获取FULL_LOG) , 通过本地的auth log对比,来判别从OSD 上缺失的object 信息。以用于后续recovery过程的依据.
- Active: 激活主osd,并发想通知notify消息,激活相应的从osd
GetInfo
pg_info_t
struct pg_info_t {
spg_t pgid;
eversion_t last_update;
eversion_t last_complete;
epoch_t last_epoch_started;
version_t last_user_version;
eversion_t log_tail;
hobject_t last_backfill;
interval_set<snapid_t> purged_snaps;
pg_stat_t stats;
pg_history_t history;
pg_hit_set_history_t hit_set;
....
}
get_infos
generate_past_intervals
计算past intervals
build_prior
根据past inerval 计算probe_targes
- 首先把当前PG中的 acting 和 up 的OSD 加入到probe 列表中
检查每个past_intervals 里
- 如果interval.last < info.history.last_epoch_started,这种情况下,根本就是不care
- 如果该interval 的act为空
- 如果该interval 没有rw操作
Probe: 现在 up 和acting的osd,以及在past interval 期间up的osd,用于获取权威日志和后续数据恢复
up_now 保存在该interval 内 up的osd
down 保存现在已经down的osd
调用pcontdec 函数检查该interval的up_now 的OSD是否有足够的OSD,对应replicated类,就是否有一正常的OSD,对应EC(n+m),是否有n个OS
- 检查是否需要need_up_thru
- 设置probe_targets
get_infos
void PG::RecoveryState::GetInfo::get_infos()
函数get_infos 向prior_set的probe 集合中的每个osd发送pg_query_t::INFO的消息,来获取pg_info信息
context< RecoveryMachine >().send_query(
peer,
pg_query_t(pg_query_t::INFO,
it->shard, pg->pg_whoami.shard,
pg->info.history,
pg->get_osdmap()->get_epoch())
);
peer_info_requested.insert(peer);
pg->blocked_by.insert(peer.osd)
发送消息的过程调用RecoveryMachine类的send_query 函数
收到查询peer_info的ack处理
在主osd 收到pg_info的ack时,包装成MNotifyRec的事件发送给状态机。
boost::statechart::result PG::RecoveryState::GetInfo::react(const MNotifyRec& infoevt)
- 首先从peer_info_requested 里删除该peer,同时从blocked_by队列里删除
调用函数bool PG::proc_replica_info(pg_shard_t from, const pg_info_t &oinfo)来处理副本的pg_info
- 首先检查该osd的pg_info是否已经存储,并且last_update参数相同,则说明已经处理过,返回false
- 确定自己是primary,把该osd的peer_info信息保存peer_info数组,并加入might_have_unfound数组里,该数组的osd保存一些对象,在后续的恢复操作中使用
- 调用函数unreg_next_scrub (目前不少特别清楚)
- 调用info.history.merge 函数处理slave osd发过来的pg_info信息,基本就是更新为最新的字段。设置dirty_info 为true
- 调用 reg_next_scrub()
- 如果该osd既不在up数组中也不再acting数组中,那就就是加入的stray_set列表中,如果pg处于clean状态,就调用purge_strays函数删除stray状态的pg以及其上的对象数据
- 如果是一个新的osd,就调用函数update_heartbeat_peers 更新需要heartbeat的osd列表
old_start保存了调用proc_replica_info前主osd的pg->info.history.last_epoch_started,如果该epoch值小于合并后的值,说明该值更新,从osd上的epoch值比较新
- 则调用pg->build_prior重新构建prior_set
- 从peer_info_requested队列中去掉上次构建的prior_set中存在osd,这最新构建中不存在的osd
- 调用get_infos函数重新发送查询peer_info请求
调用pg->apply_peer_features更新features(具体什么features,不太清楚)
- 当peer_info_requested队列为空,并且prior_set不处于pg_down的状态时,说明收到所有的osd的peer_info并处理完成
- 最后检查 past_interval阶段至少有一个osd处于up并且非incomplete的状态
- 最后完成处理 ,调用函数post_event(GotInfo())抛出GetInfo事件进入状态机的下一个状态。
PG_GetLog
当pg的主osd获取各个从osd(以及past interval期间的参与的osd)的pg_info的信息后选取一个权威的日志的osd
PG::RecoveryState::GetLog::GetLog
- 调用函数pg->choose_acting(auth_log_shard)选取拥有权威日志的osd,输出保存在auth_log_shard里
- 如果auth_log_shard == pg->pg_whoami,也就是选择的拥有权威日志的log就是自己,直接抛出事件GotLog()完成操作
- 如果自己不是,则需要去拥有权威日志的osd上去拉取权威日志,收到权威日志后,触发GetLog事件
boost::statechart::result PG::RecoveryState::GetLog::react(const GotLog&)
收到GetLog事件的处理
proc_master_log
void PG::proc_master_log
合并权威日志到本地pg log
find_best_info
函数find_best_info完成根据各个osd的pg_info_t的信息,选取一个拥有权威日志的osd,选择的优先顺序:
- Prefer newer last_update
- Prefer longer tail if it brings another info into contiguity
- Prefer current primary
代码实现具体的过程如下:
- 首先计算min_last_update_acceptable 和 max_last_epoch_started
- 如果min_last_update_acceptable == eversion_t::max(),也就是没有效的min_last_update, 直接返回infos.end()的空iterator (这种情况下,pg可能处于初始化或者pg没有写操作)
- 根据以下条件选择一个osd
- 首先把last_update小于min_last_update_acceptable或者last_epoch_started小于 max_last_epoch_started_found 或者处于incomplete的osd去掉
- 如是ec,选择最新的last_update, 如果是replicate,选择最大的last_update的osd
- 选择longer tail和 current primary
calc_replicated_acting
- 首先选择primay osd. 如果up_primary 处于非incomplete状态,并且last_update >= auth_log_shard->second.log_tail, 那么优先选择up_pirmray为primray,否则选择auth_log_shard为primray
- 以下过程选择可用的size个和 primay完全,分别添加到队列:
backfill:需要backfill的osd集合
acting_backfill: backfill + acting
want: 和acting_backfill 集合相同,主要是在compat_mode模式下兼容Erasure Code
- 首先在up里面选择,如果处于incomplete的状态, cur_info.last_update小于primary和 auth_log_shard的最小值,该pg 无法根据pg log 来进行部分对象的的拷贝修复,需要backfill,把该osd分别加入backfill 和 acting_backfill集合,否则只加入acting_backfill集合
- 同样的流程,在acting 和 all_info 中选择
choose_acting
- 首先调用find_best_info函数获取拥有权威日志的osd
- 如果没有选举出权威日志的osd,如何处理 ?目前这个逻辑不是特别清楚
- 如果当前的primary
- 计算是否是compat_mode
- 调用calc_replicated_acting,计算出backfill 和 acting_backfill(want)集合
- 计算num_want_acting数量,检查如果小于min_size,对于EC处于incomplete状态,对于Replicae 处于Peered状态,并返回flase
- 检查是否有足够的osd来进行recovery
- 检查如果acting不等于up,需要申请pg_temp
- 最后验证,如果backfill_targets为空,strary_set里面应该没有want_backfill的osd;否则如果backfill_targets不为空,需要backfill的osd不在strary_set中
PG_GetMissing
PG::RecoveryState::GetMissing::GetMissing(my_context ctx)
- 遍历pg->actingbackfill集合的osd
- 如果osd为backfill或者日志完整,只添加到pg->peer_missing
- 如果日志不全,向该osd发送请求,获取log+missing 或者 fulllog+missing
boost::statechart::result PG::RecoveryState::GetMissing::react(const MLogRec& logevt)
当收到获取日志的ack应对,包装成MLogRec事件,GetMissing状态处理该事件
- 调用proc_replica_log处理日志
- 如果peer_missing_requested为空,即所有的获取日志的请求返回并处理,如果需要pg->need_up_thru,抛出 post_event(NeedUpThru()); 否则,直接 post_event(Activate(pg->get_osdmap()->get_epoch())); 进入Activate状态
proc_replica_log
- 调用pg_log.proc_replica_log来处理日志,输出为omissing,也就是该osd缺失的对象
- 加入might_have_unfound.insert(from);
- 加入peer_missing[from].swap(omissing)
Activate
PG::RecoveryState::Active::Active(my_context ctx)
- 在构造函数里初始化了remote_shards_to_reserve_recovery 和remote_shards_to_reserve_backfill,需要recovery和backfill的
- pg->start_flush 这个目前不清楚干啥用的
- 调用pg->activate
Active
PG::activate 这个函数是peering过程的最后一步。 这个函数在ReplicaActive中激活slave osd时也调用本函数,所有里面有分别对primay osd和 slave osd 的两种情况的分别处理。
- 如果有replay,处理replay
- 如当前osd是primary, 并且acting.size() >= pool.info.min_size时更新info.last_epoch_started;如果是slave osd,确保该osd是acting状态,更新info.last_epoch_started。
更新一些字段
find out when we commit
- initialize snap_trimq
- 处理complete pointer
如果 missing.num_missing() == 0,表明没有miss的object,处于clean 状态。直接更新info.last_complete = info.last_update,并调用pg_log.reset_recovery_pointers() 调整指针
否则pg_log.activate_not_complete(info)
- 以下都是primay的操作
- pi.last_update == info.last_update
- 否则需要backfill
- Set up missing_loc
- needs_recovery,把该pg加入到 osd->queue_for_recovery(this);
- 标记pg的状态为 PG_STATE_ACTIVATING
AllReplicasActivated
boost::statechart::result PG::RecoveryState::Active::react(const AllReplicasActivated &evt)
当所有的replica 处于activated 状态时:
- 取消PG_STATE_ACTIVATING和PG_STATE_CREATING状态, 如果该pg的acting的osd的数量大于等于pool的min_size,设置该pg为PG_STATE_ACTIVE的状态,否则设置为PG_STATE_PEERED状
- share_pg_info
- ReplicatedPG::check_local,检查本地的stray对象都被删除了
- 如果有读写请求在等待peering完成,则把该请求添加到处理队列 pg->requeue_ops(pg->waiting_for_peered);
- 调用void ReplicatedPG::on_activate
- 如果需要recovery, 触发DoRecovery事件,如果需要backfill, 触发RequestBackfill事件,否则触发AllReplicasRecovered事件
- 初始化Cache Tier 需要的hist_set, hit_set_setup
- 初始化 Cache Tier需要的agent,agent_setup