本章我们会先讲述一下PriorSet以及pg_info_t数据结构,然后开始正式的讲解Ceph的Peering流程。
1. PriorSet
PriorSet是运行于PG Primary上,用于构建一个Recovery状态。下面我们来看看其数据结构:
各字段含义如下:
-
ec_pool: 当前PG所在的pool是否是ec_pool
-
probe: current_interval以及past_intervals中我们需要probe的OSD,也就是需要去获取pg_info的OSD列表
-
down: 保存了probe中down掉的osd,以及其他感兴趣的处于down状态的osd
-
blocked_by: 用于记录当前osd的lost_at值。如果lost_at值发生改变,会影响当前所构建出来的PriorSet
-
pg_down: 如果当前存在一些down OSDs导致我们不能进行数据恢复,会将此标志设置为true
-
pcontdec: 断言PG是否可以进行恢复。函数IsPGRecoverablePredicate实际上是一个类的运算符重载。对于不同类型的PG有不同的实现。对于ReplicatedPG对应的实现类为RPCRecPred,其至少保证有一个处于Up状态的OSD;对应ErasureCode(n+m)类型的PG,至少有n各处于up状态的OSD。
1.1 PriorSet构造函数
下面我们看具体的实现步骤:
1)把当前PG的acting set和up set中的OSD加入到probe列表中(这里时current_interval)
2) 遍历past_intervals阶段:
a) 如果interval.last小于info.history.last_epoch_started,这种情况下past_interval就没有意义,直接跳过;
b) 如果该interval的acting set为空,就跳过;
c) 如果该interval没有读写操作,就跳过
d) 对于当前interval的每一个处于acting状态的OSD进行检查:
* 如果该OSD当前处于up状态,就加入到up_now列表中。并同时加入到probe列表中,用于获取权威日志以及后续数据恢复;
* 如果该OSD当前已不在OSDMap中,那么就将其加入到down列表中;
* 如果该OSD当前不是up状态,但是在该past_interval期间还处于up状态,且该OSD的lost_at值大于该interval.first,说明是后期人工将其置为lost状态的,就将该OSD加入到up_now列表,并将其加入到down列表。(注:因为该OSD当前实际并不是处于up状态,这里加入up_now列表仅仅是为了可以通过pcontdec的检测,从而有机会跳过该OSD,使peering不会因为该OSD被阻塞,但是这存在丢失数据的风险)
* 否则,加入到down列表,并且将any_down_now置为true;
e) 如果当前interval确实有宕掉的OSD,就调用函数pcontdec,也就是IsPGRecoverablePredicate函数。该函数判断该PG在该interval期间的数据是否可以恢复,如果无法恢复,就直接设置pg_down为true值。同时会将对应down掉的OSD加入到block_by中,表明是由于哪些OSD阻塞了当前的恢复。(注:这里将prior_set的pg_down设置为true之后,并不是马上就结束Peering动作,而仍然会向probe中的OSD发起get_info动作,但是在进入GetLog阶段前会判断本标志,就会阻塞Peering的进一步推进)
1.2 build_prior()函数
build_prior()函数是用来构建PriorSet:
这里代码比较简单,就是构建PriorSet,然后检查当前OSD的up_thru是否小于info.history.same_interval_since,如果是则设置need_up_thru标志为true。
1.3 判断一个新的OSDMap是否会影响PriorSet
affected_by_map()函数用于判断一个新的OSDMap是否会影响当前的PriorSet,如果不会影响则在Peering阶段收到AdvMap事件时,就不必再重新回到Reset阶段开启一个新的Peering动作。
下面我们来看其实现:
1) 遍历prior.probe列表:
2) 遍历prior.down列表
-
如果down列表中的OSD重新up,肯定会改变PriorSet,返回true
-
如果down列表中的OSD被移出osdmap,也会影响PriorSet,返回true
-
如果被block_by的osd被重新设置lost_at,那么也会影响PriorSet,返回true(因为重新构建PriorSet时,可能可以绕过该OSD)
2. pg_info_t数据结构
数据结构pg_info_t
保存了PG在OSD上的一些描述信息。该数据结构在Peering的整个过程,以及后续的数据修复中都发挥了重要的作用,理解该数据结构的各个关节字段的含义可以更好地理解相关的过程。pg_info_t
数据结构如下:
结构pg_history_t
保存了PG的一些历史信息:
2.1 last_epoch_started介绍
last_epoch_started字段有两个地方出现,一个是pg_info_t
结构里的last_epoch_started,代表最后一次Peering成功后的epoch值,是本地PG完成Peering后就设置的。另一个是pg_history_t
结构里的last_epoch_started,是PG里所有的OSD都完成Peering后设置的epoch值。
2.2 last_complete和last_backfill的区别
在这里特别指出last_update和last_complete、last_backfill之间的区别。下面通过一个例子来讲解,同时也可以大概了解PG数据恢复的流程。在数据恢复过程中先进行Recovery过程,再进行Backfill过程(我们会在后面的章节进行讲解)。
情况1: 在PG处于clean状态时,last_complete就等于last_update的值,并等于PG日志中的head版本。它们都是同步更新,此时没有区别。last_backfill设置为MAX值。例如: 下面的PG日志里有三条日志记录。此时last_update和last_complete以及pg_log.head都指向版本(1,2)。由于没有缺失的对象,不需要恢复,last_backfill设置为MAX值。示例如下所示:
情况2: 当该osd1发生异常之后,过一段时间又重新恢复,当完成了Peering状态后的情况。此时该PG可以继续接受更新操作。例如:下面的灰色字体的日志记录为该osd1崩溃期间缺失的日志,obj7为新的写入的操作日志记录。last_update指向最新的更新版本(1,7),last_complete依然指向版本(1,2)。即last_update指的是最新的版本,last_complete指的是上次的更新版本。过程如下:
注:obj7的epoch似乎不应该为1了
last_complete为Recovery修复进程完成的指针。当该PG开始进行Recovery工作时,last_complete指针随着Recovery过程推进,它指向完成修复的版本。例如:当Recovery完成后last_complete指向最后一个修复的对象版本(1,6),如下图所示:
last_backfill为Backfill修复进程的指针。在Ceph Peering的过程中,该PG有osd2无法根据PG日志来恢复,就需要进行backfill过程。last_backfill初始化为MIN对象,用来记录Backfill的修复进程中已修复的对象。例如:进行Backfill操作时,扫描本地对象(按照对象的hash值排序)。last_backfill随修复的过程不断推进。如果对象小于等于last_backfill,就是已经修复完成的对象。如果对象大于last_backfill且对象的版本小于last_complete,就是处于缺失还没有修复的对象。过程如下所示:
当恢复完成之后,last_backfill设置为MAX值,表明恢复完成,设置last_complete等于last_update的值。
3. Peering的触发
通常在如下两种情形下会触发PG的Peering过程:
对于上面第一种情况,这里不做介绍。这里主要讲述当OSD接受到新的OSDMap时,是如何触发Peering流程的。如下图所示:
3.1 接收新的OSDMap
OSD继承自Dispatcher,因此其可以作为网络消息的接收者:
ms_dispatch()所分发的一般是实时性不需要那么强的消息,因此这里我们看到其会调用do_waiters()来等待阻塞在osdmap上的消息分发完成。
如下是对osdmap的分发:
3.2 handle_osd_map()实现对新OSDMap的处理
handle_osd_map()的实现比较简单,其主要是对接收到的Message中的相关OSDMap信息进行处理。如下图所示:
1)如果接收到的osdmaps中,最后一个OSDMap小于等于superblock.newest,则直接跳过
2) 如果收到的osdmaps中,第一个OSDMap大于superblock.newest +1,那么中间肯定存在缝隙,可分如下几种情况处理:
-
如果Monitor上最老的osdmap小于等于superblock.newest+1,那么说明我们仍然可以获取到OSD所需的所有OSDMap,此时只需要发起一个订阅请求即可;
-
如果Monitor上最老的osdmap大于superblock.newest+1,且monitor.oldest小于m.first,则当前OSD是无法从Monitor获得所有的OSDMap了,此时只能尝试从Monitor获取尽可能多的osdmap,因此也发起一个订阅请求;
-
如果Monitor上最老的osdmap大于superblock.newest+1,且monitor.oldest等于m.first,则此时虽然存在缝隙,但是我们也不能从Monitor获取到更多的OSDMap,此时将skip_maps置为true;
3)遍历Message中的所有OSDMap,然后存入缓存以及硬盘
注: 当skip_maps置为true时,我们要将superblock上保存的最老的OSDMap设置为m.first,确保OSD上所保存的OSDMap是连续的
3.3 处理已提交的OSDMaps
当接收到的OSDMap保存成功之后,就会回调_committed_osd_maps(),下面我们来看该函数的实现:
1)遍历接收到的osdmaps,如果有OSD在新的osdmap中不存在了或者不为up状态了,那么在发布新的OSDMap之前,必须等待阻塞在当前osdmap上的请求处理完成
2)若当前所设置的最新的OSDMap合法,且当前OSD处于active状态,那么检测当前OSD状态看是否符合OSDMap要求:
3)消费并激活OSDMap
对于consume_map()函数,我们放到后面来讲解,现在我们来看一下activate_map()的实现:
我们知道waiting_for_osdmap里面存放的是一些因等待新osdmap
而阻塞的请求,现在新的osdmap已经发布了,因此这里将相关的请求放入到finished队列里。
注:waiting_for_osdmap里面存放的是不需要绑定session的消息
3.4 消费OSDMap
在OSD::consume_map()中主要做如下事情:
1) 遍历pg_map,检查是否有PG需要移除,或者是否有PG需要分裂
2) 将OSDMap发布到OSDService
这里注意,我们在_committed_osd_maps()函数里只是将新收到的OSDMap发布给了OSD,在这里才将该最新的OSD发布到OSDService里。我们在进行数据读写操作时用的都是OSDService::osdmap。在真正发布之前,我们需要等到前一个OSDMap上的请求都执行完成。
session_waiting_for_map中所存放的一般是需要绑定session且因等待osdmap而阻塞的消息,因此这里我们调用dispatch_sessions_waiting_on_map()来对阻塞的消息进行分发。
注:要绑定session,一般是说明请求与OSDMap严重相关,且一般需要对相关的请求做响应
3)移除session_waiting_for_pg中不符合条件的PG
由于OSDMap发生变化,当前OSD上的一些PG可能会由pg primary变为pg replica,因此这里将session_waiting_for_pg中一些不再符合条件的PG移除。
4) 向当前OSD上的所有PG发送CephPeeringEvt事件
这里我们看到,OSD会遍历pg_map上的所有PG(包括pg primary以及pg replica),然后向其发送NullEvt事件。事件最终会添加进PG::peering_queue中,并且会将该PG添加到OSDService::peering_wq(即OSD::peering_wq,因为OSDService::peering_wq只是一个引用)
这里我们注意,CephPeeringEvt::epoch_sent以及CephPeeringEvt::epoch_requested都设置为了当前OSD::osdmap的版本号。
[参看]
-
ceph博客
-
ceph官网
-
PEERING