本章我们介绍Ceph的客户端实现。客户端是系统对外提供的功能接口,上层应用通过它来访问ceph存储系统。本章首先介绍librados和Osdc两个模块,通过它们可以直接访问RADOS对象存储系统。其次介绍Cls扩展模块,使用它们可方便地扩展现有的接口。最后介绍librbd模块。由于librados和librbd的多数实现流程都比较类似,本章在介绍相关数据结构后,只选取一些典型的操作流程介绍。
1. librados
librados是RADOS对象存储系统访问的接口库,它提供了pool的创建、删除,对象的创建、删除、读写等基本操作接口。架构如下图5-5所示:
最上层是类RadosClient,它是Librados的核心管理类,处理整个RADOS系统层面以及pool层面的管理。类IoctxImpl实现单个pool层对象的读写等操作。OSDC模块实现了请求的封装和通过网络模块发送请求的逻辑,其核心类Objecter完成对象的地址计算、消息的发送等工作。
1.1 RadosClient
代码如下(src/librados/radosclient.h):
通过RadosClient的成员函数,可以了解RadosClient的实现功能如下:
1) 网络连接
connect()函数是RadosClient的初始化函数,完成了许多的初始化工作:
a) 调用函数monclient.build_initial_monmap(),从配置文件里检查是否有初始的Monitor地址信息;
b) 创建网络通信模块messenger,并设置相关的Policy信息;
c) 创建Objecter对象并初始化;
d) 调用monclient.init()函数初始化monclient
e) Timer定时器初始化,Finisher对象初始化
2) pool的同步和异步创建
a) 函数pool_create同步创建pool。其实现过程为调用Objecter::create_pool()函数,构造PoolOp操作,通过monitor的客户端monc发送请求给Monitor创建一个pool,并同步等待请求的返回;
b) 函数pool_create_async异步创建。与同步方式的区别在于注册了回调函数,当创建成功后,执行回调函数通知完成。
3) pool的同步和异步删除
函数delete_pool完成同步删除,函数delete_pool_async异步删除。其过程和pool的创建过程相同,向Monitor发送删除请求。
4) 查找pool和列举pool
函数lookup_pool()用于查找pool,函数pool_list用于列出所有的pool。pool相关的信息都保存在OsdMap中。
5) 获取pool和系统的信息
函数get_pool_stats()用于获取pool的统计信息,函数get_fs_stats()用于获取系统的统计信息。
6) 命令处理
函数mon_command()处理Monitor相关的命令,它调用函数monclient.start_mon_command()把命令发送给monitor处理;函数osd_command处理OSD相关的命令,它调用函数objecter->osd_command()把命令发送给对应OSD处理。函数pg_command()处理PG相关命令,它调用函数objecter->pg_command()把命令发送给该PG的主OSD来处理。
7) 创建IoCtxImpl对象
函数create_ioctx()创建一个pool相关的山下文信息IoCtxImpl对象。
1.2 IoCtxImpl
类IoCtxImpl(src/librados/IoCtxImpl.h)是pool操作相关的上下文信息,一个IoCtxImpl对象对应着一个pool(注: 对于一个pool,我们可以创建多个IoCtxImpl对象),可以在该pool里创建、删除对象,完成对象的数据读写等各种操作,包括同步和异步的实现。其处理过程都比较简单,而且过程类似:
1) 把请求封装成ObjectOperation类(该类定义在src/osdc/Objecter.h中)
2) 然后再添加pool的地址信息,封装成Objecter::Op对象
3) 调用函数objecter->op_submit发送给相应的OSD。如果是同步操作,就等待操作完成;如果是异步操作,就不用等待,直接返回。当操作完成后,调用相应的回调函数通知。
2. OSDC
OSDC是客户端比较底层的模块,其核心在于封装操作数据,计算对象的地址,发送请求和处理超时。代码位于src/osdc目录下:
# ls
Filer.cc Filer.h Journaler.cc Journaler.h Makefile.am ObjectCacher.cc ObjectCacher.h Objecter.cc Objecter.h Striper.cc Striper.h WritebackHandler.h
2.1 ObjectOperation
类ObjectOperation用于将操作相关的参数统一封装在该类里,该类可以一次封装多个对象的操作(src/osdc/objecter.h):
封装的对象操作主要包括如下几类:
-
object的创建、读写、遍历
-
pg的遍历
-
object的xattr的创建、遍历
-
object的omap的创建、遍历
类OSDOp封装对象的一个操作。结构体ceph_osd_op封装一个操作的操作码和相关的输入和输出参数(src/osd/osd_types.h):
注: 这里OSDOp通常只是操作的封装,具体操作的对象名可能不会在这里设置,一般需要搭配Objecter一起使用
2.2 op_target
结构op_target_t封装了对象所在的PG,以及PG对应的OSD列表等地址信息(src/osdc/objecter.h):
2.3 Op
结构Op封装了完成一个操作的相关上下文信息,包括target地址信息、链接信息等(src/osdc/objecter.h):
2.4 Objecter
类Objecter主要完成对象的地址计算、消息的发送等工作(src/osdc/objecter.h):
注:关于crush_location的使用,可以参看https://ceph.com/planet/ceph%E6%A0%B9%E6%8D%AEcrush%E4%BD%8D%E7%BD%AE%E8%AF%BB%E5%8F%96%E6%95%B0%E6%8D%AE/
2.4 Striper
对象有分片(stripe)时,类Stripe用于完成对象分片数据的计算。数据结构ceph_file_layout用来保存文件或者image的分片信息(src/include/fs_types.h):
对象ObjectExtent用来记录对象内的分片信息(src/osd/osd_types.h):
函数file_to_extents()完成了file到对象stripe后的映射。只有了解清楚了每个概念,计算方法都比较简单。下面举例说明。
例5-1
file_to_extents示例
如上图5-5所示,要计算的文件的offset为2KB,length为16KB。文件的分片信息: stripe unit为4KB,stripe count为3,object size为8KB。则对象Object 0对应的ObjectExtent为:
其中,oid就是映射对象的id,objectno为stripe对象的序号, offset为映射的数据段在对象内的起始偏移,length为对象内的长度。buffer_extents为映射的数据在buffer内的偏移和长度。
2.5 ObjectCacher
类ObjectCacher提供了客户端的基于LRU算法的对象数据缓存功能,其实比较简单,这里就不深入分析了(src/osdc/ObjectCacher.h)。
2.6 小结
librados层的整体架构如下:
对于librados我们可以分如下两个层面来看:
1) 从大的接口封装层面来说,提供了C语言的封装rados_t,以及C++语言的抽象librados::Rados(注意: Objecter处于全局名称空间,而其他处于librados名称空间)
2) 从RadosClient、IoCtxImpl、Objecter层面来说,是一个抽象到具体的一个实现的过程。RadosClient是粗粒度的Rados操作接口;IoCtxImpl是针对某一个pool的较为细粒度的操作接口;而Objecter则是object层面的更为细粒度的操作实现。
3. 客户端写操作分析
以下代码是通过librados库的接口写入数据到对象中的典型例程,对象的其他操作过程都类似:
上述代码是C语言接口完成的,其流程如下:
1) 首先调用rados_create()函数创建一个RadosClient对象,输出类型为rados_t,它是一个void类型的指针,通过librados::RadosClient对象的强制转换产生。第二个参数id为一个标识符,一般传入为NULL。
2) 调用函数rados_conf_read来读取配置文件。第二个参数为配置文件的路径,如果是NULL,就搜索默认的配置文件。
3) 调用rados_connect()函数,它调用了RadosClient的connect()函数,做相关的初始化工作。
4) 调用函数rados_ioctx_create(),它调用RadosClient的create_ioctx()函数,创建pool相关的IoCtxImpl类,其输出类型为rados_ioctx_t,它也是void类型的指针,有IoCtxImpl对象转换而来;
5) 调用函数rados_write()函数,向该pool的名为foo
的对象写入数据。此调用IoCtxImpl的write()操作
3.1 写操作消息封装
本函数完成具体的写操作,代码如下(src/librados/IoCtxImpl.cc):
其实现过程如下:
1) 创建ObjectOperation对象,封装写操作的相关参数;
2) 调用函数operate()完成处理:
a) 调用函数objecter->prepare_mutate_op()把ObjectOperation类型封装成Op类型,添加了object_locator_t相关的pool信息;
b) 调用objecter->op_submit()把消息发送出去;
c) 等待操作完成;
3.2 发送数据op_submit
函数op_submit()用来把封装好的操作Op通过网络发送出去:
函数_op_submit_with_budget()用来处理Throttle相关的流量限制。如果osd_timeout大于0,就设置定时器,当操作超时,就调用定时器回调函数op_cancel取消操作:
函数_op_submit完成了关键的地址寻址和发送工作,其处理过程如下:
1) 调用函数_calc_target来计算对象的目标OSD
2) 调用函数_get_session获取目标OSD的链接,如果返回值为-EAGAIN,就升级为写锁,重新获取;
3) 检查当前的状态标志,如果当前是CEPH_OSDMAP_PAUSEWR
或者OSD空间满
(注: 因为这里我们讲述的是对象的写操作,因此flag中CEPH_OSD_FLAG_WRITE肯定被设置),就暂时不发送请求;否则调用函数_prepare_osd_op()准备请求的消息,然后调用函数_send_op()发送出去。
3.3 对象寻址_calc_target
函数_calc_target用于完成对象到OSD的寻址过程:
其处理过程如下:
1) 首先根据t->base_oloc.pool
的pool信息,获取pg_pool_t对象;
2) 检查如果强制重发,force_resend设置为true;
3) 检查cache tier,如果是读操作,并且有读缓存,就设置target_oloc.pool为该pool的read_tier值;如果是写操作,并且有写缓存,就设置target_oloc.pool为该pool的write_tier值
4) 调用函数osdmap->object_locator_to_pg()获取目标对象所在的PG
5) 调用函数osdmap->pg_to_up_acting_osds(),通过CRUSH算法,获取该PG对应的OSD列表;
6) 如果是写操作,target的OSD就设置为主OSD;如果是读操作,如果设置了CEPH_OSD_FLAG_BALANCE_READS标志,就随机选择一个副本读取;如果设置了CEPH_OSD_FLAG_LOCALIZE_READS标志,就尽可能选择本地副本来读取;否则选择主OSD来进行读取
3.4 查询object对应的PG
object到PG的映射是通过object_locator_to_pg()函数来实现的(src/osd/osdmap.cc):
从上面来看,过程比较简单,首先计算ps(placement seed),然后根据pool id就可以获得PG。
3.5 通过pg求OSD列表
pg到OSD列表的映射是通过pg_to_up_acting_osds()来实现的(src/osd/osdmap.cc):
pg_to_up_acting_osds()函数实现将pg映射为acting set。我们在进行对象读写时,都需要通过acting set来进行(对于pg_temp等少数情况,我们可能只需要知道PG对应的up set即可,也可以通过本函数来返回)。其实现步骤如下:
1) _pg_to_osds()会根据pool所对应的crush rule来计算出PG所对应的OSD
此时计算出来的OSD其实只是原始的仍在osd map列表中存在的OSD(这里在调用该函数时我们传递的是raw),之间是没有primary、secondary、tertiary这样区分的。
2) _raw_to_up_osds()函数从raw中返回当前仍处于UP状态的OSD,把处于down状态的进行标记
或移除
。同时这里简单的将返回的OSD列表中的第一个作为up_primary
3) _apply_primary_affinity()用于计算up_primary的亲和性,就是根据一定的规则从up osds里面选出其中一个作为primay。
4) _get_temp_osds()用于求出PG对应的acting set
上面我们看到首先调用pool.raw_pg_to_pg(pg)将raw pg转换,然后再在pg_temp中查找转换后的PG所对应的OSD列表即为acting set。
3.5 写操作的应答
上面讲写请求发送出去之后,就会等待相应的应答。当从网络收到应答之后,首先会回调:
对于对象的读写操作,应答都是CEPH_MSG_OSD_OPREPLY
消息,其会调用handle_osd_op_reply()来进行处理。
4. Cls
Cls是Ceph的一个扩展模块,它允许用户自定义对象的操作接口和实现方法,为用户提供了一种比较方便的接口扩展方式。目前rbd和lock等模块都采用了这种机制(即通过加载额外的动态链接库的方式来进行处理,类似于Nginx中的dynamic module)。
4.1 模块以及方法的注册
类ClassHandler用来管理所有的扩展模块。函数register_class用来注册模块(src/osd/ClassHandler.h):
一个ClassData代表一个模块,一个模块中可以有很多方法。可以在一个ClassHandler中注册多个模块。
类ClassData描述了一个模块相关的元数据信息。它描述了一个扩展模块的相关信息,包括模块名、模块相关的操作方法以及依赖的模块:
ClassMethod定义一个模块具体的方法名,以及函数类型:
在src/objclass/Objectclass.h, src/objclass/class_api.c里定义了一些辅助函数用来注册模块以及方法:
4.2 cls模块的工作原理
1) ClassHandler的初始化
在src/osd/OSD.cc的init()函数中有如下:
通过上面我们看到是调用cls_initialize()来完成初始化,现在我们来看看该函数(src/objclass/Class_api.cc):
这里可以看到,是将OSD::init()中创建的ClassHandler对象赋给了一个静态全局变量。
2) 加载动态链接库
通过上面创建出的ClassHandler对象调用open_all_classes()来加载ceph配置文件中以libcls_
开头的动态链接库,加载步骤如下:
从上面我们可以看到步骤较为简单:
从上面我们可以看到,其首先调用cls_register()向ClassHandler注册该模块,其实就是在ClassHandler中创建了一个ClassData。然后,调用cls_register_cxx_method()来向该模块注册C++方法,这样该ClassData中就有该模块所有相关的方法了。
4.3 模块的方法执行
模块方法的执行在类ReplicatedPG的函数do_osd_ops()里实现(src/osd/ReplicatedPG.cc)。执行方法对应的操作码为CEPH_OSD_OP_CALL值:
[参看]
-
Ceph 的物理和逻辑结构
-
小甲陪你一起看Ceph