【Ceph】Ceph Client
librados
librados是RADOS对象存储系统访问的接口层,它提供了pool的创建、删除、对象的创建、删除、读写等基本操作接口
在最上层是RadosClient,它是librados的核心管理类,处理整个RADOS系统层面以及pool层面的管理。类IoctxImpl实现单个pool 层的对象读写等操作
OSDC模块实现了请求的封装和通过网络模块发送请求的逻辑,其核心类Objecter完成对象的地址计算、消息的发送等工作
librados主要接口:
1.集群句柄创建
librados::Rados对象是用来操纵ceph集群的句柄,使用init来创建RadosClient,之后读取指定的ceph配置文件,获取monitor的 ip和端口号。RadosClient里面有与monitor通信的MonClient 和用于与OSD通信的Messenger。
init a.创建一个RadosClient对象 reinterpret_cast<rados_t>(new librados::RadosClient(cct));
2.集群连接
初始化集群句柄之后,就可以使用这个句柄来连接集群了,RadosClient::connect完成了连接操作:
a)调用monclient.build_initial_monmap,从配置文件中检查是否有初始化的monito的地址信息
b)创建网络通信模块messenger,并设置相关的Policy信息
c)创建Objecter对象并初始化
d)调用monclient.init()函数初始化monclient
e)Timer定时器初始化,Finisher对象初始化
3.IO上下文环境初始化
使用句柄创建好存储池后,还需要创建与存储池相关联的IO上下文句柄
rados.ioctx_create(pool_name, io_ctx)
4.对象读写
创建对象并写入数据:
io_ctx.create_full(object_name, bl)
读取对象中的数据到bufferlist中。对象读取有同步读取和异步读取两种接口,分别是io_ctx.read和io_ctx.aio_read
同步读取 ... io_ctx.read(object_name, read_bl, read_len, 0); ... 异步读取(需要指定完成读取数据后的回调,用于检查读取是否完成) ... librados::AioCompletion *read_completion = librados::Rados::aio_create_completion(); io_ctx.aio_read(object_name, read_completion, &read_buff, read_len, 0); read_completion->wait_for_complete(); 同时,还要获取返回值,得到读取对象的字节数: ...
5.IO上下文关闭
io_ctx.close()
6.集群句柄关闭
rados.shutdown()
OSDC
OSDC是客户端底层的模块,其核心在于封装操作数据,计算对象地址、发送请求和处理超时。 OSDC是osd client的意思,该目录下封装了各类操作osd的方法。objecter最终会创建一个操作,并且把它提交。 可以从代码中看出,检查pool是否已经存在,是使用osdmap, 也就是说创建的pool信息是至少有部分在osdmap中的。最后会交由 MonClient发送消息给Monitor,走的是PaxosService。如果要继续追溯,前提条件是对Ceph的消息发送机制熟悉,这样才能在 Monitor端接收消息的地方,继续追踪下去。
Objecter:
RadosClient::connect: set_balanced_budget init set_client_incarnation start
RadosClient::shutdown Objecter: shutdown
RadosClient::watch_flush objecter: linger_callback_flush
RadosClient::wait_for_osdmap objecter: get_osdmap_read put_osdmap_read
RadosClient::wait_for_latest_osdmap objecter: wait_for_latest_osdmap
RadosClient::get_pool_stats objecter: get_pool_stats
RadosClient::get_fs_stats objecter: get_fs_stats
RadosClient::create_pool objecter create_pool
RadosClient::delete_pool objecter delete_pool
RadosClient::blacklist_self objecter blacklist_self
RadosClient::osd_command objecter osd_command
RadosClient::pg_command objecter pg_command
cls
cls是ceph的一个扩展模块,它允许自定义对象的操作接口和实现方法,为用户提供了一种比较方便的接口扩展方式。
#define CLS_NAME(name) \ int __cls_name__## name = 0; \ const char *__cls_name = #name; int __cls_name__name = 0; const char *__cls_name = "name";
cls提供了服务端接口和客户端接口,以共享库的形式提供接口。编译后生成libcls_xx.so和libcls_client_xx.so
,服务端的 接口主要供OSD调用,在OSD初始化的时候会注册共享库中指定要注册的模块和以及C或者C++接口,然后以名称和函数指针的形式存在。 当客户端需要调用服务端提供的接口时,必须先实例化一个IoCtx对象,通过IoCtx类提供的exec方法去调用服务端提供的接口。比如 cls中的hello这个例子,在cls_hello.cc中的__cls_init()这个初始化函数中,注册了hello这个模块和say_hello这个函数, 注册完后,客户端就能够实用IoCtx实例的exec方法来调用这个模块所提供的方法了。
通过这种方式,提高了OSD模块的可扩展性,要想实现操作rados对象或者rbd块设备的扩展功能,只需要在cls中添加相应的实现即可。 这样,就不必改动OSD模块原有的功能,同时也降低了耦合度。
cls中主要的类: 服务端: ClassHandler ClassHandler这个类用来管理所有的扩展模块,其中包含了两个主要的内部类ClassData和ClassMethod以及加载和注册扩展库模块 的方法。
客户端: 要调用cls提供的扩展模块的方法很简便,在使用librados库时初始化IoCtx对象,然后执行IoCtx::exec方法即可。
librbd
librbd是ceph对外提供的块存储接口的抽象,可以供qemu虚拟机, fio测试程序的rbd engine等程序使用。它提供c和c++两种接口, 对于c++,最主要的两个类就是RBD和Image。RBD主要负责创建,删除,克隆镜像等操作, 而Image类负责镜像的读写等操作。
{ librbd::RBD rbd; librbd::Image image; int order = 22; unsigned long long size = 512 * (1<<20lu); const char *image_name = "hello_format2.1.img"; //int ret = rbd.create(io_ctx, image_name, size, &order); int ret = rbd.create3(io_ctx, image_name, size, 3, &order, 0, 0); if (ret < 0) { std::cerr << "failed create image: " << image_name << std::endl; goto out; } ret = rbd.open(io_ctx, image, image_name); if (ret < 0) { std::cerr << "rbd: failed to open image" << std::endl; goto out; } std::string image_data("hello_world!"); bufferlist outbl; outbl.append(image_data); librbd::RBD::AioCompletion *write_comp = new librbd::RBD::AioCompletion(NULL, NULL); if (!outbl.is_zero()) { image.aio_write(0, outbl.length(), outbl, write_comp); } std::vector<std::string> image_list; ret = rbd.list(io_ctx, image_list); if (ret < 0) { std::cerr << "failed to list image" << std::endl; goto out; } std::cout << "all images:" << std::endl; for (auto& item : image_list) { std::cout << item << " "; } std::cout << std::endl; }
RBD元数据:
rbd_directory
- 映射镜像名称至id,反之亦然
rbd_children
- pool中克隆的列表,通过parent来索引
rbd_id.$image_name
- 内部id,只需要使用用户指定的镜像名称定位
rbd_header.$image_name
- 每个镜像的元数据
RBD数据:
rbd_data.*objects
-
基于在镜像中偏移来命名
-
不存在从哪开始
-
对象中的纯数据
-
rados负责快照
-
通常是稀疏的
RBD的相关对象: rbd_directory: 每个pool都有该对象,保存该pool下所有RBD的目录信息 format1时:rbd_directory文件中保存的是该pool下所有的RBD信息 format2时:可以用rados listomapvals rbd_directory列出RBD信息
Format1: Hammer和Hammer之前默认都是这种格式,rbd_default_features=3 Format2: Jewel及之后的版本默认的格式, rbd_default_features=61
rbd镜像的features详情请看后面一节。
image_name --> oid --> fd_cache(the collection of fd) --> lfn_open(*path->path())
librbd简介 RBD image internal in Ceph Ceph librbd create image Ceph librbd flatten rbd块设备驱动揭秘 QEMU QoS特性及原理分析和Librbd QoS定义 Object的attr和omap操作
在ceph中,不管是块设备、对象存储还是文件存储,最后都转化成了底层的Object。这个Object包含有三个元素:data、xattr和omap。 data是保存对象的数据;xattr是保存对象的扩展属性,每个对象文件都可以设置文件的扩展属性
块存储设备的元数据管理: 对于块存储,在存储过程中主要是块的相关信息。块的元数据管理,创建一个块设备后会创建一个名为.rbd的元数据文件,它不是 用来存储数据的,而是用来保存元数据信息的。NOTE: ?真实的元数据信息不回保存在这个文件中,而是保存在这个文件的xattr或者omap中?
rbd_directory对象保存了所有创建的块设备名字。
format1: 写镜像时使用librados的write接口 format为1时,striping不可用,features为0
uint64_t features = old_format ? 0 : cct->_conf->rbd_default_features
数据结构: ceph_file_layout:
format2: 写镜像时使用cls扩展模块
Create: format1:rbd.create format2:rbd.create3
rbd扩展模块: 客户端编码,服务端解码:这样设计是为了让客户端传递参数时可以将多个参数编码到一个bufferlist中,然后传递给服务端,服务端 再将输入的bufferlist解码获得客户端传递的参数。还有个好处是保持了服务端接口的一致性,所有服务端的接口都设计为接收3个参数 (method上下文,传入参数和传出参数)
RBD import Test 1.创建池后
__head_00000000__12c rbd\udirectory__head_30A98C1C__12c
2.导入rbd镜像后
a.img.rbd__head_D43CB243__12c rb.0.1d5c19.74b0dc51.000000000000__head_84EDD092__12c rbd\udirectory__head_30A98C1C__12c __head_00000000__12c rb.0.1d5c19.74b0dc51.000000000001__head_06A3542D__12c
3.新增加的rados对象为
a.img.rbd rb.0.1d5c19.74b0dc51.000000000000 rb.0.1d5c19.74b0dc51.000000000001
4.a.img.rbd__head_D43CB243__12c中的内容
[root@cephnode2 300.0_head]# hexdump -C a.img.rbd__head_D43CB243__12c 00000000 3c 3c 3c 20 52 61 64 6f 73 20 42 6c 6f 63 6b 20 |<<< Rados Block | 00000010 44 65 76 69 63 65 20 49 6d 61 67 65 20 3e 3e 3e |Device Image >>>| 00000020 0a 00 00 00 00 00 00 00 72 62 2e 30 2e 31 64 35 |........rb.0.1d5| 00000030 63 31 39 2e 37 34 62 30 64 63 35 31 00 00 00 00 |c19.74b0dc51....| 00000040 52 42 44 00 30 30 31 2e 30 30 35 00 16 00 00 00 |RBD.001.005.....| 00000050 50 ca 7f 00 00 00 00 00 00 00 00 00 00 00 00 00 |P...............| 00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000070
5.rbd_directory中的内容为
[root@cephnode2 300.0_head]# hexdump -C rbd\\udirectory__head_30A98C1C__12c 00000000 00 00 00 00 01 00 00 00 05 00 00 00 61 2e 69 6d |............a.im| 00000010 67 00 00 00 00 |g....| 00000015
librbd主要数据结构
1.AioCompletion
AioCompletion是单个rbd I / O请求的全部completion。它可能会包含多个AioObjectRequest, 每个都会请求单个object。
单独的一个请求重试在更低一层已经被处理,所以所有的AioCompletion关心的只是未完成的请求数量。 期望的请求数量应该在请求之前使用set_request_count
进行初始化。这确保了completion不会在 调用者的线程执行中被完成(而是通过librados上下文或者线程池上下文进行缓存读取命中)。
aio的请求类型:
-
AIO_TYPE_NODE
-
Aio_TYPE_OPEN
-
AIO_TYPE_CLOSE
-
AIO_TYPE_READ
-
AIO_TYPE_WRITE
-
AIO_TYPE_DISCARD
-
AIO_TYPE_FLUSH
aio的一些状态:
-
STATE_PENDING
-
STATE_CALLBACK
-
STATE_COMPLETE
2.AioImageRequest
3.AioObjectRequest
该类表示对单个RBD数据对象的IO操作,它的子类封装了处理layering特性的IO逻辑。
快照
snap_info_t
typedef struct { uint64_t id; uint64_t size; std::string name; } snap_info_t;
获取snap信息
创建snap:
添加一个snap到一个rbd header上面 创建snap时,会创建header对象的omap属性,snap_seq为当前snapset的最大id,snapshot的具体 key为snapshot_+snap_id
,value为snapname。
Test
create pool: [root@cephnode2 45.0_head]# ls __head_00000000__2d use ceph-kvstore-tool get omap info: _HOBJTOSEQ_:...head.2d.00000000 _SYS_:HEADER _USER_0000000000000057_USER_:_biginfo _USER_0000000000000057_USER_:_epoch _USER_0000000000000057_USER_:_info _USER_0000000000000057_USER_:_infover _USER_0000000000000057_USER_:can_rollback_to _USER_0000000000000057_USER_:rollback_info_trimmed_to rbd import a 4M image [root@cephnode2 45.0_head]# ls __head_00000000__2d rb.0.d2f63.74b0dc51.000000000000__head_2FED9A61__2d rbd\udirectory__head_30A98C1C__2d zealoussnow.rbd__head_78F40C0D__2d get omap info: _HOBJTOSEQ_:...head.2d.00000000 _SYS_:HEADER _USER_0000000000000057_USER_:0000000391.00000000000000000001 _USER_0000000000000057_USER_:0000000391.00000000000000000002 _USER_0000000000000057_USER_:0000000391.00000000000000000003 _USER_0000000000000057_USER_:0000000391.00000000000000000004 _USER_0000000000000057_USER_:0000000391.00000000000000000005 _USER_0000000000000057_USER_:_biginfo _USER_0000000000000057_USER_:_epoch _USER_0000000000000057_USER_:_info _USER_0000000000000057_USER_:_infover _USER_0000000000000057_USER_:can_rollback_to _USER_0000000000000057_USER_:rollback_info_trimmed_to
omap属性里,保存所有的RBD的设备名和id信息,key值为“name“和设备名的拼接,value为“id_”和RBD的id的拼接 (如何获取这些值呢?)
每个RBD设备所创建的对象
format v2
rbd_id.<name>: RBD的id_obj对象,其名字保存了RBD的name信息,其对象的内容保存了该RBD的id信息 [root@cephnode2 303.0_head]# hexdump -C "rbd\\uid.hello\\uformat2.img__head_57893838__12f" 00000000 0e 00 00 00 31 64 38 33 33 37 37 34 62 30 64 63 |....1d833774b0dc| 00000010 35 31 |51| 00000012 rbd_header.<id>: RBD的header_obj对象,其omap保存了RBD相关的元数据信息 [root@cephnode2 src]# ./rados -c ceph.conf -p hello_world_pool listomapvals rbd_header.1d833774b0dc51 2>/dev/null features value (8 bytes) : 0000 : 03 00 00 00 00 00 00 00 : ........ object_prefix value (27 bytes) : 0000 : 17 00 00 00 72 62 64 5f 64 61 74 61 2e 31 64 38 : ....rbd_data.1d8 0010 : 33 33 37 37 34 62 30 64 63 35 31 : 33774b0dc51 order value (1 bytes) : 0000 : 16 : . size value (8 bytes) : 0000 : 00 00 00 20 00 00 00 00 : ... .... snap_seq value (8 bytes) : 0000 : 00 00 00 00 00 00 00 00 : ........ rbd_object_map.<id>: 保存了其对象和父iamge对象的映射信息 数据对象: rbd.data.<id>.00000000 rbd.data.<id>.00000001 ...
Rados可以同步或异步的方式交互,一旦应用有了IO上下文,读写操作只需要你知道对象/扩展文件属性的名字。Crush算法封装在librados 中,使用ClusterMap来确认合适的OSD。
librados初始化: librados.h rados_t (typedef void *rados_t) 同rados集群交互的句柄,封装了所有RADOS客户端配置,包括用户名,认证key,日志和调试
rados_ioctx_t (typedef void *rados_ioctx_t)
Rados::init rados_create rados_create_cct 新建一个RadosClient对象
monclient发送给monitor的消息(helloworld): 认证: auth(proto 0 30 bytes epoch 0) v1 auth(proto 2 32 bytes epoch 0) v1 auth(proto 2 165 bytes epoch 0) v1
monmap和osdmap订阅: mon_subscribe({monmap=0+}) v2 mon_subscribe({monmap=2+,osdmap=0}) v2 mon_subscribe({monmap=2+,osdmap=0}) v2
创建pool:
pool_op(create pool 0 auid 0 tid 1 name hello_world_pool v0) v4 mon_subscribe({monmap=2+,osdmap=337}) v2 OSDMonitor::prepare_new_pool librados::Rados::pool_create librados::RadosClient::pool_create Objecter::create_pool // op->pool_op = POOL_OP_CREATE Objecter::pool_op_submit new MPoolOp --> CEPH_MSG_POOLOP Objecter::_pool_op_submit monc->send_mon_message MonClient::send_mon_message MonClient::_send_mon_message cur_con->send_message PipeConnection::send_message static_cast<SimpleMessenger*>(msgr)->send_message SimpleMessenger::send_message SimpleMessenger::_send_message SimpleMessenger::submit_message Pipe::_send cond.Signal() ---> 激活写线程,发送消息
OSDMonitor接收消息:
删除池:
pool_op(delete pool 38 auid 0 tid 4 name delete v337) v4 mon_subscribe({monmap=2+,osdmap=339}) v2 Objecter::delete_pool Objecter::_do_delete_pool // op->pool_op = POOL_OP_DELETE
写入对象: osd_op(client.754098.0:2 hello_object [writefull 0~12] 38.bbbfcc6c ondisk+write+known_if_redirected e337) v5
1.IoCtx的创建 Rados::ioctx_create rados_ioctx_create RadosClient::create_ioctx new librados::IoCtxImpl 最终创建一个IoCtxImpl实例
从对象到pg的寻址: hash 从pg到到OSD: crush算法
crush算法:
Write Object
write_full:
ObjectOperation op prepare_assert_ops ==> op op.write_full(bl) IoCtxImpl::operate objecter->prepare_mutate_op objecter->op_submit Objecter::op_submit _op_submit_with_budget _op_submit Objecter::_op_submit ==> 完成了关键的地址寻址和发送工作 _calc_target _get_session ==> 获取目标OSD连接 _send_op_account _session_op_assign _send_op Objecter::_send_op op->session->con->send_message(m) PipeConnection::send_message SimpleMessenger::_send_message
读取对象: op.op = CEPH_OSD_OP_READ Rados::aio_create_completion
设置对象属性: osd_op(client.754098.0:3 hello_object [setxattr helloworld_version (1)] 38.bbbfcc6c ondisk+write+known_if_redirected e337) v5
所有的pool,对象和属性操作都是通过调用Objecter::op_submit来完成的
Entity NAME: MON 1 MDS 2 OSD 4 CLIENT 8 AUTH 20
rbd镜像属性详解
配置项为rbd_default_features=[3 or 61]
,这个值是由几个属性加起来的:
Features编号:
Only applies to format 2 images: +1 for layering +2 for striping v2 +4 for exclusive lock +8 for object map +16 for fast-diff +32 for deep-flatten +64 for journaling
在J版中,rbd镜像的默认属性是61(1+4+8+16+32),即开启了layering
,exclusive lock
,objct map
, fast-diff
, deep-flatten
这几个属性。可以新建一个rbd块设备来查看其属性:
# rbd info test/test rbd image 'test': size 500 GB in 128000 objects order 22 (4096 kB objects) block_name_prefix: rbd_data.104b2a6b8b4567 format: 2 features: layering, exclusive-lock, object-map, fast-diff, deep-flatten flags:
layering
Ceph支持创建针对一个块设备快照的许多COW克隆。快照layering可以让rbd客户端非常快速地创建镜像。 比如:可以创建一个Linux VM的块设备镜像;然后对这个镜像做快照,保护快照,创建许多COW克隆。一个 快照是只读的,因此,克隆一个快照从语义上来说是很精简的——使得快速克隆称为可能。
+--------------------+ +----------------+ | Snapshot | Child refers | COW Clone | | of Image | <----------------| of Snapshot | | (read only) | to Parent | (Writeable) | +--------------------+ +----------------+
注意:上图中的parent表示的意思是一个块设备的快照,child表示的是从快照克隆的相关镜像。
每个克隆的镜像(child)存储了它的父镜像的一个引用,允许克隆的镜像打开父快照并读取它。
举一个实际的例子,比如ceph里有一个base image
,我们要克隆到一个新的instance:
-
先给这个
base image
创建一个快照 -
对这个快照进行克隆
快照在克隆之前要设置为protect状态。
如果这个快照被克隆了,那么在child没有被删除或者没有进行flatten操作之前,这个快照的protect 状态是取消不掉的,因此它不会被删除。只有它的children都被删除或flatten之后(即不再被依赖), 它才可以删除。
layering的实现中,并没有track每个object在克隆中是否存在,而是读到不存在的object的时候就去 parent那里找。对于写操作,首先要检查object是否存在,不存在的话要先从parent那里读出来再做写 操作。
striping
exclusive-lock
object-map
ObjectMap这个属性自从J版开始默认支持,它是一个位图实现,每个镜像都会有一个ObjectMap,根据object_no 来记录每个object的存在状态。
一个Image的ObjectMap对象的名称为rbd_object_map.<oid>
,如果有快照,快照的ObjectMap 名称为rbd_object_map.<oid>.<snap_id>
。
数据结构:
class ObjectMap { ... private: ImageCtx &m_image_ctx; ceph::BitVector<2> m_object_map; uint64_t m_snap_id; };
字段解释:
-
m_image_ctx: 使用该ObjectMap的镜像上下文
-
m_object_map: 位图结构
-
m_snap_id: 镜像的snap id