本文接续上文《ceph recovery研究(1)》,继续讲解ceph的数据修复过程。
1. Recovery过程
在前面ReplicatedPG::start_recovery_ops()函数中我们讲到会调用:
-
recover_primary()修复PG主OSD上缺失的对象
-
recover_replicas()修复PG副本OSD上缺失的对象
-
recover_backfill()执行backfill过程
在恢复PG主OSD上缺失的对象时,我们看到又会调用ReplicatedBackend::recover_object()来实现PG Primary对象的修复。函数ReplicatedBackend::recover_object()其实实现的是pull操作
,另外在调用recover_replicas()进行副本对象恢复时,会调用ReplicatedBackend实现的push操作
。关于pgbackend我们会在后面再进行讲解,这里我们先来看看recover_replicas()的实现。
1.1 函数recover_replicas()
函数ReplicatedPG::recover_replicas()用于恢复PG副本上的对象:
我们来看具体的实现流程:
1)调用pgbackend->open_recovery_op()返回一个PG类型相关的PGBackend::RecoveryHandle。对于ReplicatedPG其对应的RecoveryHandle为RPGHandle
,内部有两个map,保存了Push和Pull操作的封装PushOp和PullOp。
2)遍历PG::actingbackfill中除PG Primary外的每一个PG副本:
2.1) 获取到对应副本的peer_missing以及peer_info信息
2.2) 遍历对应副本的peer_missing中的每一个object:
a) 如果该object大于peer_info.last_backfill,说明该对象是需要通过backfill来恢复,直接跳过;
b) 如果该对象已经正在进行恢复,直接跳过;
c) 如果该对象处于unfound状态,暂时无法进行恢复,跳过;
d) 如果该对象是snap对象,且其对应的head对象在PG Primary上仍然处于missing状态,则优先需要primary进行修复,直接跳过;
e) 如果该对象是snap对象,且其对应的snapdir对象在PG Primary上仍然处于missing状态,则优先需要primary进行修复,直接跳过;
f) 如果该对象在PG Primary上仍让处于missing状态,则应该先修复PG Primary,直接跳过;
g) 调用ReplicatedPG::prep_object_replica_pushes()为所需要的对象版本构造PushOp请求,放入RecoveryHandle中
3)调用函数pgbackend->run_recovery_op(),把PullOp或者PushOp封装的消息发送出去;
注:这里为PushOp消息
1.1.1 函数prep_object_replica_pushes()
下面我们来看一下函数ReplicatedPG::prep_object_replica_pushes()的实现:
具体流程如下:
1)本地获取所要恢复的对象的ObjectContext:
2)如果获取ObjectContext失败,则将该对象加入PG Primary的missing列表中。然后遍历PG::actingbackfill列表,看能否在peer_missing中找到该对象,之后程序结束,返回;
注:在进行Object恢复时,优先是恢复PG Primary,因此不管上面是否在peer_mising中找到该对象,都会返回
3)如果获取ObjectContext成功,但获取recovery read lock失败,直接返回
4)获取ObjectContext以及recovery read lock成功,执行如下步骤:
4.1) 调用PG::start_recovery_op()修改recovery_ops_active的个数
4.2) 将该所要恢复的对象加入ReplicatedPG::recovering列表中
4.3) 调用函数pgbackend->recover_object()把要修复的操作信息封装到PullOp或者PushOp对象中,并添加到RecoveryHandle结构中。
2. PGBackend
PGBackend封装了不同类型的pool的实现。ReplicatedBackend实现了replicate类型的PG相关的底层功能,ECBackend实现了Erasure code类型的PG相关的底层功能。
由前面的分析可知,不管是recover_primary()还是recover_replicas(),其都会调用到pgbackend->recover_object()函数来实现修复对象的信息封装。这里只介绍基于副本的。
在函数recover_object()中,调用get_parent()->get_local_missing()来判断是恢复自身还是恢复其他副本上的对象数据。对于PG Primary来说,如果要恢复其本身的数据,则调用ReplicatedBackend::prepare_pull()把请求封装成PullOp结构;否则调用ReplicatedBackend::start_pushes()把请求封装成PushOp的操作。
2.1 pull操作
prepare_pull()函数把要拉取的object相关的操作信息打包成PullOp类信息,如下所示:
难点在于snap对象的修复处理过程。下面我们来看具体的处理过程:
1) 通过调用函数get_parent()来获取PG对象的指针。pgbackend的parent就是相应的PG对象。通过PG获取missing、peer_missing、missing_loc等信息;
2) 从soid对象对应的missing_loc的map中获取该soid对象所在的OSD集合。把该集合保存在shuffle这个向量中。调用random_shuffle()操作对OSD列表随机排序,然后选择向量中首个OSD来为缺失对象拉取源OSD的值。从这一步可知,当修复主OSD上的对象,而多个从OSD上有该对象时,随机选择其中一个源OSD来拉取。
3)当选择了一个源shard之后,查看该shard对应的peer_missing来确保该OSD上不缺失该对象,即确实拥有该版本的对象。
4)确定拉取对象的数据范围:
a) 如果是head对象,直接拷贝对象的全部,在copy_subset()加入区间(0,-1),表示全部拷贝,最后设置size为-1:
b) 如果该对象是snap对象,确保head对象或者snapdir对象二者必须存在一个。如果headctx不为空,就可以获取SnapSetContext对象,它保存了snapshot相关的信息。调用函数calc_clone_subsets()来计算需要拷贝的数据范围。
5)设置PullOp的相关字段,并添加到RPGHandle中。此外,还会将当前soid添加到ReplicatedBackend::pull_from_peer和ReplicatedBackend::pulling中保存起来
2.1.1 函数ReplicatedBackend::calc_clone_subsets()
函数ReplicatedBackend::calc_clone_subsets()用于修复快照对象。在介绍它之前,这里需介绍SnapSet的数据结构和clone对象的overlap概念。
在SnapSet结构中,字段clone_overlap保存了clone对象和上一次clone对象的重叠部分:
下面通过一个示例来说明clone_overlap
数据结构的概念。
例11-2
clone_overlap数据结构如图11-2所示:
snap3从snap2对象clone出来,并修改了区间3和4,其在对象中范围的offset和length为(4,8)和(8,12)。那么在SnapSet的clone_overlap中就记录:
函数calc_clone_subset()用于修复快照对象时,计算应该拷贝的数据区间。在修复快照对象时,并不是完全拷贝快照对象,这里用于优化的关键在于:快照对象之间是有数据重叠,数据重叠的部分可以通过已存在的本地快照对象的数据拷贝来修复;对于不能通过本地快照对象拷贝修复的部分,才需要从其他副本上拉取对应的数据。
函数calc_clone_subsets()具体实现如下:
1) 首先获取该快照对象的size,把(0,size)加入到data_subset中:
2) 向前查找(oldest snap)和当前快照相交的区间,直到找到一个不缺失的快照对象,添加到clone_subset中。这里找的不重叠区间,是从不缺失快照对象到当前修复的快照对象之间从没修改过的区间,所以修复时,直接从已存在的快照对象拷贝所需区间数据即可。
3) 同理,向后查找(newest snap)和当前快照对象相重叠的对象,直到找到一个不缺失的对象,添加到clone_subset中。
4) 去除掉所有重叠的区间,就是需要拉取的数据区间;
对于上述算法,下面举例来说明:
例11-3
快照对象修复示例如图11-3所示:
要修复的对象为snap4,不同长度代表各个clone对象的size是不同的,其中深红色
的区间代表clone后修改的区间。snap2、snap3和snap5都是已经存在的非缺失对象。
算法处理流程如下:
1) 向前查找和snap4重叠的区间,直到遇到非缺失对象snap2为止。从snap4到snap2一直重叠的区间为1,5,8三个区间。因此,修复对象snap4时,修复1,5,8区间的数据,可以直接从已存在的本地非缺失对象snap2拷贝即可。
2) 同理,向后查找和snap4重叠的区间,直到遇到非缺失对象snap5为止。snap5和snap4重叠的区间为1,2,3,4,7,8六个区间。因此,修复对象4时,直接从本地对象snap4中拷贝区间1,2,3,4,7,8即可。
3) 去除上述本地就可修复的区间,对象snap4只有区间6需要从其他OSD上拷贝数据来修复。
2.2 push操作
函数ReplicatedBackend::start_pushes()获取actingbackfill的OSD列表,通过peer_missing查找缺失该对象的OSD,然后调用ReplicatedBackend::prep_push_to_replica()打包PushOp请求。
下面我们来看prep_push_to_replica()函数的实现:
处理流程如下:
1)如果需要push的对象是snap对象:检查如果head对象缺失,调用prep_push()推送head对象;如果headdir对象缺失,则调用prep_push()推送headdir对象;
2)如果是snap对象,调用函数calc_clone_subsets()来计算需要推送的快照对象的数据区间。
3)如果是head对象,调用calc_head_subsets()来计算需要推送的head对象的区间,其原理和计算快照对象类似,这里就不详细说明了。
4) 最后调用prep_push()封装PushInfo信息,在函数build_push_op()里读取要push的实际数据。
2.2.1 函数prep_push()
上面比较简单,就是构造一个PushInfo
数据结构,然后放入ReplicatedBackend::pushing中。
2.2.2 函数build_push_op()
具体过程如下:
1) 如果progress.first为true,就需要获取对象的元数据信息。通过store->omap_get_header()获取omap的header信息,通过store->getattrs()获取对象的扩展属性信息,并验证oi.version是否为recovery_info.version;否则返回-EINVAL值。如果成功,new_progress.first设置为false。
2) 上一步只是获取了omap的header信息,并没有获取omap信息。这一步首先判断progress.omap_complete是否完成(初始化设置为false),如果没有完成,就迭代获取omap的(key,value)信息,并检查一次获取信息的大小不能超过cct->_conf->osd_recovery_max_chunk设置的值(默认为8MB)。特别需要注意的是,当该配置参数的值小于一个对象的size时,一个对象的修复需要多次数据的push操作。为了保证数据的完整一致性,先把数据拷贝到PG的temp存储空间。当拷贝完成之后,再移动到该PG的实际空间中。
3) 开始拷贝数据:检查recovery_info.copy_subset,也就是拷贝的区间;
4) 调用函数store->fiemap()来确定有效数据的区间out_op->data_included的值,通过store->read()读取相应的数据到data里。
5) 设置PushOp的相关字段,并返回。
2.3 处理修复操作
从前面的代码分析中,我们看到不管是recover_primary()还是recover_replicas(),其都会调用函数run_recover_op()来发送PullOp和PushOp请求:
当主OSD把对象推送给缺失该对象的从OSD后,从OSD需要调用函数handle_push()来实现数据的写入工作,从而完成该对象的修复。同样,当主OSD给从OSD发起拉取对象的请求来修复自己缺失的对象时,需要调用函数handle_pulls()来处理该请求的应对。
PushOp处理流程
上面看到PushOp的处理流程非常长,在函数ReplicatedBackend::handle_push()中PushOp请求,主要调用ReplicatedBackend::submit_push_data()函数来写入数据。
PullOp处理流程
上面看到函数ReplicatedBackend::handle_pull()收到一个PullOp请求,返回PushOp操作,处理流程如下:
1)首先调用store->stat()函数,验证该对象是否存在,如果不存在,则调用函数prep_push_op_blank(),直接返回空值;
2)如果该对象存在,获取ObjectRecoveryInfo和ObjectRecoveryProgress结构。如果progress.first为true并且recovery_info.size为-1,说明是全拷贝修复:将recovery_info.size设置为实际对象的size,清空recovery_info.copy_subset,并把(0, size)区间添加到recovery_info.copy_subset的拷贝区间。
3)调用函数build_push_op(),构建PushOp结构。如果出错,调用prep_push_op_blank(),直接返回空值。
注:关于ReplicatedBackend::build_push_op()我们在前面已经讲述过,这里不再赘述。
2. Backfill过程
当PG完成了Recovery过程之后,如果backfill_targets不为空,表明有需要Backfill过程的OSD,就需要启动Backfill的任务,来完成PG的全部修复。可参看前面介绍的start_recovery_ops()函数:
下面我们介绍Backfill过程相关的数据结构和具体处理过程。
2.1 相关数据结构
数据结构BackfillInterval用来记录每个peer
上的Backfill过程:
其字段说明如下:
-
version: 记录扫描对象列表时,当前PG对象更新的最新版本,一般为last_update。由于此时PG处于active状态,可能正在进行写操作。其用来检查从上次扫描到现在是否有对象写操作。如果有,完成写操作的对象在已扫描的对象列表中,进行Backfill操作时,该对象就需要更新为最新版本。
-
objects: 扫描到的准备进行Backfill操作的对象列表
-
begin: 本次扫描的起始对象
-
end: 本次扫描的起始对象的结束对象,用于作为下次扫描对象的开始
注:如果begin==end==hobject_t(),表明在interval内没有对象要恢复
2.2 Backfill的具体实现
函数ReplicatedPG::recover_backfill()作为Backfill过程的核心函数,控制整个Backfill修复进程:
其具体工作流程如下:
1) 初始设置
在PG Peering完成进行激活时会调用到ReplicatedPG::on_activate(),在其中设置了PG的属性值new_backfill为true,设置了last_backfill_started值为earliest_backfill()的值。从上面我们看到earliest_backfill()就是计算需要backfill的OSD中,peer_info信息里保存的last_backfill的最小值。
peer_backfill_info的map中保存各个需要Backfill的OSD所对应的BackfillInterval对象信息。首先初始化begin和end都为peer_info.last_backfill,由PG的Peering过程可知,在PG::activate()里,设置该OSD的peer_info.last_backfill为hobject_t(),也就是MIN
对象。
backfills_in_flight保存了正在进行Backfill操作的对象,pending_backfill_updates保存了需要删除的对象。
2)设置backfill_info.begin为last_backfill_started,调用函数ReplicatedPG::update_range()来更新需要进行Backfill操作的对象列表
3)根据各个peer的peer_backfill_info信息进行trim操作。根据last_backfill_started来更新backfill_info里相关字段
4) 如果backfill_info.begin小于等于earliest_peer_backfill(),说明需要扫描更多的对象,backfill_info重新设置。这里特别注意的是,backfill_info的version字段也重新设置为(0,0),这会导致在随后调用的update_range()函数时再次调用scan_range()函数来扫描对象。
5) 进行比较, 如果pbi.begin小于backfill_info.begin,需要向各个OSD发送MOSDPGScan::OP_SCAN_GET_DIGEST消息来获取该OSD目前所拥有的对象列表
6) 当获取所有OSD的对象列表后,就对比当前主OSD的对象列表来进行修复
7) check对象指针,就当前OSD中最小的需要进行Backfill操作的对象:
a) 检查check对象,如果小于backfill_info.begin,就在各个需要Backfill操作的OSD上删除该对象,加入到to_remove队列中;
b) 如果check对象大于或者等于backfill_info.begin,检查拥有check对象的OSD,如果版本不一致,加入need_ver_targ中。如果版本相同,就加入keep_ver_targs中;
c) 那些begin对象不是check对象的OSD,如果pinfo.last_backfill小于backfill_info.begin,那么该对象缺失,加入missing_targs列表中;
d) 如果pinfo.last_backfill大于backfill_info.begin,说明该OSD修复的进度已经超越当前主OSD指示的修复进度,加入skip_targs中;
8)对于keep_ver_targs列表中的OSD,不做任何操作。对于need_ver_targs和missing_targs中的OSD,该对象需要加入到to_push中修复
9) 调用函数send_remove_op()给OSD发送删除的消息来删除to_remove中的对象;
10) 调用函数prep_backfill_object_push()把操作打包成PushOp,调用函数pgbackend->run_recovery_op()把请求发送出去。其流程和Recovery流程类似
11) 最后用new_last_backfill更新各个OSD的pg_info的last_backfill值。如果pinfo.last_backfill为MAX,说明backfill操作完成,给该OSD发送MOSDPGBackfill::OP_BACKFILL_FINISH消息;否则发送MOSDPGBackfill::OP_BACKFILL_PROGRESS来更新各个OSD上的pg_info的last_backfill字段。
2.2.1 recover_backfill()示例
下面举例说明recover_backfill()的处理过程。
例11-4
如下图11-4所示,该PG分布在5个OSD上(也就是5个副本,这里为了方便列出各种处理情况),每一行上的对象列表都是相应OSD当前对应backfillInterval的扫描对象列表。osd5为主OSD,是权威的对象列表,其他OSD都对照主OSD上的对象列表来修复。
下面举例来说明步骤7)中的不同的修复方法:
1)当前check对象指针为主OSD上保存的peer_backfill_info中begin的最小值。图中check对象应为obj4对象;
2)比较check对象和主osd5上的backfill_info.begin对象,由于check小于obj5,所以obj4为多余的对象,所有拥有该check对象的OSD都必须删除该对象。故osd0和osd2上的obj4对象被删除,同时对应的begin指针前移。
3) 当前各个OSD的状态如图11-5所示:此时check对象为obj5,比较check和backfill_info.begin的值:
a) 对于当前begin为check对象的osd0、osd1、osd4:
* 对于osd0和osd4,check对象和backfill_info.begin对象都是obj5,且版本号都为(1,4),加入到keep_ver_targs列表中,不需要修复;
* 对于osd1,版本号不一致,加入need_ver_targs列表中,需要修复;
b) 对于当前begin不是check对象的osd2和osd3:
* 对于osd2,其last_backfill小于backfill_info.begin,显然对象obj5缺失,加入missing_targs修复;
* 对于osd3,其last_backfill大于backfill_info.begin,也就是说其已经修复到obj6了,obj5应该已经修复了,加入skip_targs跳过;
4)步骤3处理完成后,设置last_backfill_started为当前的backfill_info.begin的值。backfill_info.begin指针前移,所有begin等于check对象的begin指针前移,重复以上步骤继续修复。
函数update_range()调用函数scan_range()更新BackfillInterval修复的对象列表,同时检查上次扫描对象列表中,如果有对象发生写操作,就更新该对象修复的版本。
具体实现步骤如下:
1)bi->version记录了扫描要修复的对象列表时PG最新更新的版本号,一般设置为last_update_applied或者info.last_update的值。初始化时,bi->version的默认值为(0,0),所以小于info.log_tail,就更新bi->version的设置,调用函数scan_range()扫描对象;
2)检查如果bi->version的值等于info.last_update,说明从上次扫描对象开始到当前时间,PG没有写操作,直接返回;
3)如果bi->version小于info.last_update,说明PG有写操作,需要检查从bi->version到log_head这段日志中的对象:如果该对象有更新操作,修复时就修复最新的版本;如果该对象已经删除,就不需要修复,在修复队列中删除。
下面举例说明ReplicatedPG::update_range()的处理过程。
2.2.2 update_range()示例
例11-5
update_range的处理过程
1) 日志记录如下图所示:
BackfillInterval的扫描的对象列表:bi->begin为对象obj1(1,3),bi->end为对象obj4(1,6),当前info.last_update的版本为(1,6),所以bi->version这只为(1,6)。由于本次扫描的对象列表不一定能修复完,只能等下次修复。
2)日志记录如下图所示:
第二次进入函数recover_backfill,此时begin对象指向了obj2对象。说明上次只完成了对象obj1的修复。继续修复时,期间有对象发生更新操作:
a) 对象obj3有写操作,版本更新为(1,7)。此时对象列表中要修复的对象obj3版本为(1,5),需要更新为版本(1,7)的值;
b) 对象obj4发送删除操作,不需要修复了,所以需从对象列表中删除。
综上所述可知,Ceph的Backfill过程是扫描OSD上该PG的所有对象列表,和主OSD做对比,修复不存在的或者版本不一致的对象,同时删除多余的对象。
3. 小结
本章介绍了ceph的修复数据的过程,有两个过程:Recovery过程和Backfill过程。Recovery过程根据missing记录,先完成主副本的修复,然后完成从副本的修复。对于不能通过日志修复的OSD,Backfill过程通过扫描各个部分上的对象来全量修复。整个Ceph的数据修复过程比较清晰,比较复杂的部分可能就是涉及快照对象的修复处理。
目前这部分代码是ceph最核心的代码,除非必要,都不会轻易修改。目前社区也提出修复时的一种优化方法。就是在日志里记录修改的对象范围,这样在Recovery过程中不必拷贝整个对象来修复,只修复修改过的对象对应的范围即可,这样在某些情况下可以减少修复的数据量。
[参看]
-
CEPH OBJECTSTORE API介绍
-
fiemap
-
Ceph 中最严重数据不一致 BUG