ceph的Peering过程是一个十分复杂的流程,其主要的目的是使一个PG内的OSD达成一个一致的状态。当主从副本达成一个一致的状态后,PG处于active状态,Peering过程的状态就结束了。但此时PG的三个OSD副本上的数据并非完全一致。
PG在如下两种情况下触发Peering过程:
1. 基本概念
在具体的讲解Peering之前,我们先对其中的一些概念做个介绍。
1.1 acting set和up set
acting set是一个PG对应副本所在的OSD列表,该列表是有序的,列表中第一个OSD为主OSD。在通常情况下,up set与acting set列表是完全相同的,只有当存在pg temp
时两者会不同。
下面我们先来看acting set与up set的计算过程:
从上面的注释中我们了解到:acting set主要用于数据映射的目的;而up set只是根据crush map计算出来的,有些用户也会发现其具有重要的作用,通过对比acting set与up set我们就知道pg_temp是什么。
下面我们来看_pg_to_up_acting_osds()函数的实现:
从上面我们可以看到,对于PG的映射有多个步骤:
1) _pg_to_osds()
_pg_to_osds()函数的实现比较简单,其从crushmap中获取PG所映射到的OSD。这里注意,从crushmap获取到到的映射与OSD的运行状态无关(与权重有关,当osd out出去之后,其权重变为了0,在进行crush->do_rule()时就不会再映射到该OSD上了)。之后调用_remove_nonexistent_osds()函数移除在该OSDMap下不存在的OSD。
_pg_to_osds()函数返回了最原始的PG到OSD的映射,并且第一个即为primary。
2) _raw_to_up_osds()
通过_pg_to_osds()函数,我们获取到了原始的PG到OSD的映射关系。而_raw_to_up_osds()函数主要是移除了在该OSDMap下处于down状态的OSD,即获取到了原始的up osds,并且将第一个设置为up primary。
3) _apply_primary_affinity()
此函数主要是处理primary亲和性的设置,这里不做介绍(默认不做设置)。
4) _get_temp_osds()
_get_temp_osds()用于获取指定OSDMap下,指定PG所对应的temp_pg所映射到的OSD。
5)完成PG的up set、acting set的映射
我们再回到_pg_to_up_acting_osds()函数的最后,如果当前没有temp_pg,那么该PG的acting set即为up set。
针对up set与acting set我们可以总结为:up set是直接根据crushmap算出来的,与权威日志等没有任何关系;而acting set是我们真正读写数据时所依赖的一个osd序列,该序列的第一个osd作为primary来负责数据读写,因此必须拥有权威日志。
1.2 临时PG
假设初始状态一个PG的up set
与acting set
均为[0,1,2]列表。此时如果OSD0出现故障,重新发起Peering操作,根据CRUSH算法得到up set为[3,1,2], acting set也为[3,1,2],但是在OSD::choose_acting()时发现osd3为新添加的OSD,并不能负担该PG上的读操作,不能成为该PG的主OSD。所以PG向Monitor申请一个临时的PG,osd1为临时的主OSD,这时up set
为[3,1,2],acting set
变为[1,2,3],这就出现了up set
与acting set
不同的情况。当osd3完成Backfill过程之后,临时PG被取消,该PG的acting set被修改为[3,2,1],此时up set与acting set就又相等了。
通过阅读ceph相关源码,发现如下两种情况下会产生pg_temp:
下面我们分别介绍这两种情况。
1.2.1 peering过程主动申请pg_temp
我们搜索OSD::queue_pg_temp()函数,发现有如下两个地方调用:
上面的代码中,会调用OSD::queue_want_pg_temp()将对应PG所要请求的acting set放入到队列中。
注:如果所要请求的acting set为空,则表示清除对应的pg temp。
下面我们来看OSD::queue_want_pg_temp()的实现,以及对应的请求是如何发送出去的:
其中OSDService::pg_temp_pending用于存放当前已经
向Monitor发出过pg_temp请求的映射表;而OSDService::pg_temp_wanted表示将要
发起pg_temp请求的映射表。下面我们来看具体的发送函数:
在OSDService::send_pg_temp()函数中,将pg_temp_wanted中的所有映射表都发送到OSDMonitor。查找该函数,发现会在两个地方被调用:
-
OSD::ms_handle_connect(): 当OSD重新连接上monitor时,会requeue以前的pg_temp,然后再重新发送。这时因为,对于以前的pg_temp我们并不确定其是否已经发送出去,因此这里会重新进行发送。
-
OSD::process_peering_events(): 发送Peering过程中所要求的pg_temp
1.2.2 OSDMonitor检测到OSD主动下线时自动产生pg_temp
当OSD主动下线时会通过如下向Monitor发送MOSDMarkMeDown消息:
当Monitor收到该消息后,会做响应的处理(Monitor类继承自Dispatcher,因此可以分发消息),下面我们来看这一过程:
此时其会调用OSDMonitor的dispatch()函数来分发消息:
下面我们就来看看具体的产生pg_temp的过程:
1)处理MSG_OSD_MARK_ME_DOWN消息
如上,在函数中构造了一个proposal: C_AckMarkedDown,然后插入到waiting_for_finished_proposal列表中,准备对提议进行表决。因为Paxos需要对每一个提议进行持久化,因此这里会调用:
跟踪到这里,我们发现maybe_prime_pg_temp()可能会为该主动关闭的OSD预先产生好pg_temp。下面我们来看看该函数的实现。
2)OSDMonitor::maybe_prime_pg_temp()
上面函数中调用prime_pg_temp()来为指定的OSD构建pg_temp:
上面首先会遍历该OSD上的每一个PG,然后根据相应的条件产生pg_temp。
1.3 pg_history_t数据结构
pg_history_t数据结构在PG运行过程中具有重要的作用,其一般用来记录已经发生的且具有一定权威性的内容,可以在OSD之间来进行共享。
-
epoch_created: PG创建时的epoch值,由PGMonitor生成
-
last_epoch_started:PG上一次激活(activate)时的epoch值
-
last_epoch_clean:PG上一次进入clean状态时的epoch值
-
same_up_since:记录当前up set出现的最早时刻
-
same_interval_since: 表示的是某一个interval的第一个osdmap版本号
1.4 past_interval介绍
所谓past_interval
就是osdmap版本号epoch的一个序列,在该序列内一个PG的acting set与up set不会发生变化。
每一个PG都会维护一个past_intervals的映射表,其中key为该past_interval
的第一个epoch值。下面简要介绍一下pg_interval_t各字段的值:
-
pg_interval_t::up => PG在该interval期间的up set;
-
pg_interval_t::acting => PG在该interval期间的acting set
-
pg_interval_t::first => 该interval的第一个epoch值
-
pg_interval_t::last => 该interval的最后一个epoch值
-
pg_interval_t::maybe_went_rw => 表示在该interval期间可能执行了写操作
-
pg_interval_t::primary => PG在该该interval期间的acting primary
-
pg_interval_t::up_primary =>PG在该interval期间的up primary
1.4.1 pg_interval_t的几个重要函数
1) is_new_interval(): 版本1
从上面我们看出,只要该PG的up set与acting set任何一个发生变化,或者PG发生了分裂,都会产生一个新的interval。
2) is_new_interval():版本2
从上面可以看出,如果lastmap中不含有当前PG所在的pool,那么为一个新的interval;另外PG的up set与acting set任何一个发生变化,或者PG发生了分裂,都会产生一个新的interval。
3)check_new_interval()
注: same_interval_since表示的是某一个interval的第一个osdmap版本号
本函数通过对比lastmap
与osdmap
,判断当前是否是一个新的interval,如果是则将lastmap放入past_intervals
中,然后再判断past_interval是否可能执行了写操作。如果past_interval中有acting set,并且数量达到了对应pool的min_size,且通过该old_acting_shards足以进入active状态:
-
如果该PG的primary osd在lastmap时的up_thru
值大于等于past_interval.first,且该OSD的up_from值小于等于past_interval.first,则可能发生了写入操作,将past_interval.maybe_went_rw设置为true;
-
如果last_epoch_clean值在[past_interval.first, past_interval.last]之间时,则当前的past_interval肯定是由于recovery恢复完成,然后产生acting change事件从而改变acting set而生成的,在此一过程中也是可能进行写入操作,因此将past_interval.maybe_went_rw设置为true;
-
其他情况,不可能会执行写入操作,将past_interval.maybe_went_rw设置为false;
1.4.2 PG::past_intervals的产生
past_intervals的产生主要有两个地方,下面我们来分析:
1.4.2.1 PG::generate_past_intervals()
我们来分析一下该函数的实现:
1) 调用_calc_past_interval_range()计算当前PG的past_interval可能的范围
a) info.history.same_interval_since是指当前interval(current_interval
)的第一个osdmap版本号。因此,如果same_interval_since值不为0,那就将当前要计算的past_intervals
的end设置为该值。
b) 查看past_intervals里已经计算的past_interval的第一个epoch,如果已经比info.history.last_epoch_clean小,就不用计算了,直接返回false。否则设置end为其first值。
a) start设置为info.history.last_epoch_clean,从最后一次last_epoch_clean算起
b)当PG为新建时,从info.history.epoch_started开始计算
c)oldest_map值为保存的最早osd map值,如果start小于这个值,相关的osdmap信息缺失,所以无法计算
所以将start设置为三者的最大值:
下面我们给出一个示例来说明计算past_intervals的过程
如上表所示:一个PG有4个interval。past_interval 1,开始epoch为4,结束的epoch为8;past_interval 2的epoch区间为(9,11);past_interval 3的区间为(12,13);current_interval的区间为(14,16)。最新的epoch为16,info.history.same_interval_since为14,意指是从epoch 14开始,之后的epoch值和当前的epoch值在同一个interval内。info.history.last_epoch_clean为8,就是说在epoch值为8时,该PG处于clean状态。
计算start 和 end的方法如下:
a)start的值设置为info.history.last_epoch_clean值,其值为8
b)end值从14开始计算,检查当前已经计算好的past_intervals的值。past_interval的计算是从后往前计算。如果第一个past_interval的first小于等于8,也就是past_interval 1已经计算过了,那么后面的past_interval 2和past_interval 3都已经计算过,就直接退出。否则就继续查找没有计算过的past_interval值。
1.4.2.2 OSD::build_past_intervals_parallel()
本函数与PG::generate_past_intervals()类似,只是为了快速的计算所有PG(注:pg primary为本OSD的pgs)的past_intervals,并在计算完成后保存。
1.4.3 PG::past_intervals使用场景
搜寻PG::generate_past_intervals(),我们发现主要在如下地方被调用:
1) Reset状态接收到AdvMap事件
正常情况下,基本上这是第一次产生past_interval的地方。
2) GetInfo阶段产生past_interval
暂时未知为何要重新计算past_intervals。
3)Primary OSD恢复期间构建might_have_unfound集合时
1.4.3 PG::past_intervals的清理
PG::past_intervals在运行过程中不可能一直大规模保留,肯定是需要清理的:
从上面的代码可以看出,最后保留的第一个past_interval的last必须要大于等于info.history.last_epoch_clean。
下面我们来什么时候会触发调用trim_past_intervals()呢?
在进入clean状态时,首先会将当前osdmap的版本号赋值给info.history.last_epoch_clean,之后调用trim_past_intervals()来清除掉一些不必要的past_intervals。
2. up_thru
大家都知道,OSDMap的作用之一便是维护Ceph集群OSD的状态信息,所以基于此想先提出一个疑问:Ceph集群中有1个osd down了,那么osdmap会发生什么变化?osdmap会更新几次?带着这个问题,本文深入探讨up_thru。
参看:ceph之up_thru分析
2.1 引入up_thru的目的
up_thru的引入是为了解决如下这类极端场景:
比如集群有两个osd(osd.1, osd.2)功能承载一批PG来服务业务IO。如果osd.1在某个时刻down了,并且随后osd.2也down了,再随后osd.1又up了,那么此时osd.1是否能提供服务?
如果osd.1 down掉期间,osd.2有数据更新,那么显然osd.1再次up后是不能服务的;但是如果osd.2没有数据更新,那么osd.1再次up后是可以提供服务的。
2.2 up_thru到底是啥?
要想知道up_thru到底是啥,可以先通过其相关数据结构感受一下,如下:
通过数据结构我们便可以知道,up_thru是作为osd的信息保存在osdmap里的,其类型便是osdmap的版本号(epoch_t)。
既然其是保存在osdmap里面的,那么我们可以通过把osdmap dump出来看看,如下:
usrname@hostname:# ceph osd dump
epoch ***
fsid ***
// 如下便看到了up_thru,277便是osdmap的版本号
osd.1 up in weight 1 up_from 330 up_thru 277 down_at 328 ...
osd.2 up in weight 1 up_from 329 up_thru 277 down_at 328 ...
2.3 up_thru的来龙去脉
up_thru的整个生命周期以及是如何起作用的?整个流程是非常长的,为了让文章变得短小精悍一点,与up_thru不是强相关的流程就不加入代码分析了,只是一笔带过。
为了形象说明up_thru的来龙去脉,我们就沿着上文那两个OSD的例子展开说。
3.3.1 up_thru的更新
osd.1挂了后发生了什么?
osd.1挂了之后,整个集群反应如下:
osd.1挂了后,或是osd.1主动上报,或是其他osd向mon上报osd.1挂了,此时mon已经感知到osd.1挂了
该osdmap中标记该down掉的osd状态为down,并将新的osdmap发送给osd.2
osd.2收到新的osdmap后,相关PG开始peering,若PG发现需要向mon申请更新up_thru信息,那么PG状态变为WaitUpThru;
osd.2判断是否需要向mon申请更新up_thru消息,若需要,则向mon发送该信息。
mon收到消息后,更新osdmap里面osd.2的up_thru信息,并将新的osdmap发送给osd.2
osd.2收到新的osdmap后,相关PG开始peering,相关PG状态由WaitUpThru变为active,可以开始服务业务IO。
具体的up_thru更新相关流程如下
以下是osd.2收到新的osdmap(第一次)后相关操作:
1) PG判断其对应的主OSD是否需要更新up_thru
PG开始peering,以下是peering相关函数:
2) osd判断其本次做Peering的所有PG中是否有需要申请up_thru的。若有,该OSD向monitor申请up_thru
3) monitor接受osd的申请
PG状态的转变
osd.2收到新的osdmap(第一次)后,PG进入peering后如果需要mon更新up_thru,其会先进入NeedUpThru状态:
等到osd.2收到新的osdmap(第二次)后,该osdmap也即是更新了up_thru后的新osdmap,PG当前处于WaitUpThru状态,当处于WaitUpThru状态的PG收到OSDMap,会把pg->need_up_thru置为false,如下:
wait_up_thru状态的PG收到ActMap事件后,由于need_up_thru标记已经设为false了,所以开始进入active状态,进入active状态后,也就可以开始io服务了:
2.4 up_thru应用
上文描述了当osd.1挂掉后,整个集群的响应,其中包括up_thru的更新。本节描述一下up_thru的应用,也即本文开始的那个例子,当osd.1再次up后,其能否提供服务?
场景说明
假设初始时刻,也即osd.1
、osd.2
都健康,对应PG都是active + clean时osdmap的版本号epoch是80,此时osd.1以及osd.2的up_thru必然都小于80。
1)不能提供服务的场景
osd.1 down掉,osdmap版本号epoch变为81;
如果osd.2向mon申请更新up_thru成功,此时osdmap版本号epoch为82(osd.2的up_thru变为81),由于osd.2收到新的osdmap后,PG的状态就可以变为active,也即可以提供io服务了,所以如果up_thru更新成功了,可以判定osd.2有新的写入了(当然,可能存在虽然up_thru更新了,但是osd.2进入到active之前就挂了,那也是没有数据更新的);
osd.2 down掉,osdmap版本号变为83;
osd.1进程up,osdmap版本号变为84;
2)能提供IO服务的场景
osd.1 down掉,osdmap版本号epoch变为81;
osd.2没有peering成功,没有向monitor上报相应的up_thru,没有更新操作(比如,osd.2向mon申请更新up_thru失败,在上报之前就挂了)
osd.2 down掉,osdmap版本号epoch变为82;
osd.1 进程up后,osdmap版本号变为83;
IO能否提供服务的代码逻辑判断
通过如下流程判断对应PG能否提供IO服务:
1) 生成past_interval
在peering阶段会调用如下函数生成past_interval(osd启动时也会从底层读到相关信息进而生成past_interval):
这里还是以上述两种场景来描述past_interval的更新:
如上所述,osdmap版本号epoch分别是80~84,这几个osdmap版本号会对应如下4个past_interval:
{80}、{81,82}、{83}、{84}
并且在{81,82}这个past_interval中,maybe_went_rw会被设置为true,因为在osdmap版本号为82时,osd.2对应的up_thru为81,这样就等于本past_interval的第一个osdmap版本号81.
如上所述,osdmap版本号epoch分别是80~83,这几个osdmap版本号分别对应如下4个past_interval:
{80}、{81}、{82}、{83}
2) 根据past_interval判断IO能否服务
这里以不能服务IO的场景为例,如下:
综上所述,当osd.1再次up后,最终到{84}这个interval时,根据上面一系列函数调用,此时的PG状态最终会变为peering+down,此时便无法响应服务IO了。
3.5 写在后面
在本章的开头我们提出了一个疑问,即Ceph集群中有一个osd down了,那么osdmap会发生什么变化?osdmap会更新几次?
答案是:不一定,主要分如下两种情况
此时,OSDMap只会更新一次,变化便是osdmap中该OSD的状态从up更新为了down。因为都不存在相关PG,也就不存在PG Peering,也就没有up_thru更新了,所以osdmap是变化1次。
此时osdmap会至少更新2次,其中第一次是更新osdmap中osd的状态,第二次便是更新相关osd的up_thru。
这里通过实例来说明osdmap变化情况:
1) 集群初始状态信息如下
# ceph osd tree
ID CLASS WEIGHT TYPE NAME STATUS REWEIGHT PRI-AFF
-1 1.74658 root default
-5 0.87329 host pubt2-ceph1-dg-163-org
0 ssd 0.87329 osd.0 up 1.00000 1.00000
-3 0.87329 host pubt2-ceph2-dg-163-org
1 ssd 0.87329 osd.1 up 1.00000 1.00000
# ceph osd dump | grep epoch
epoch 416 // osdmap版本号
2) down掉osd.0后集群信息如下
# ceph osd tree
ID CLASS WEIGHT TYPE NAME STATUS REWEIGHT PRI-AFF
-1 1.74658 root default
-5 0.87329 host pubt2-ceph1-dg-163-org
0 ssd 0.87329 osd.0 down 1.00000 1.00000
-3 0.87329 host pubt2-ceph2-dg-163-org
1 ssd 0.87329 osd.1 up 1.00000 1.00000
# ceph osd dump | grep epoch
epoch 418 // osdmap版本号
如上所示,当down掉osd.0后,osdmap版本号增加了2个。那我们分别看看这两个osdmap有啥变化,可以通过ceph osd dump epoch
把相应的osdmap打印出来:
# ceph osd dump 416
# ceph osd dump 417
# ceph osd dump 418
通过对比,我们可以发现osdmap.416
与osdmap.417
的差别就是osd.0的状态变化;osdmap.417
与osdmap.418
的差别就是更新了osd.1的up_thru。
[参看]
-
ceph osd heartbeat 分析
-
boost官网
-
在线编译器
-
boost在线编译器
-
ceph官网
-
PEERING