1. 概述
1.1 Librados
Ceph rados分布式存储提供了多种语言的api接口,封装在librados库中。客户可以在客户端调用librados,完成对远端的rados分布式存储系统的访问。我们可以在源码目录中打开librados中的api文件夹,查看相关接口。
librados就是操作rados对象存储的接口。 其接口分为两种:一个是c 接口,其定义在include/librados.h 中。 一个是 c++接口,定义在include/librados.hpp中,实现都在librados.cc中实现。
接口主要分为五类[^1]:
- ceph集群句柄(rados client类的实例)的创建和销毁,配置,连接等,pool的创建和销毁,io上下文(ioctx)的创建和销毁等。
- 创建一个集群句柄
- 根据配置文件,命令行参数,环境变量配置集群句柄
- 连接集群,相当于使rados client能够实时通信
- 创建pool,配置不同的 crush分布策略,复制级别,位置策略等等
- io上下文的创建及获取
- 快照相关接口,librados支持对于整个pool的快照,接口包括快照的创建和销毁,到对应快照版本的回滚,快照查询等等。
- 同步IO操作接口,包括读,写,覆盖写,追加写,对象数据克隆,删,截断,获取和设置指定的扩展属性,批量获取扩展属性,迭代器遍历扩展属性,特殊键值对获取等等
- 异步IO操作接口包括异步读,异步写,异步覆盖写,异步追加写,异步删,librados还提供了对象的监视功能,通过rados_watch可以注册回调,当对象发生变化时会回调通知上层。
- io操作组原子操作,即可以把对同一个对象的一系列io操作放到一个组里面,最后保证加入到组里的所有io操作保持原子性,要么全部成功,要么全部失败,而不会给用户呈现出文件系统不一致的问题。
下述是客户端使用librados连接集群并读写对象的一个实例[^2]:
- 获得集群的句柄,并连接到集群的某个Monitor中.以获得Cluster Map;
1 | std::string cluster_name("ceph"); |
- 创建IO上下文,并绑定一个已经存在的存储池;
1 | librados::IoCtx io_ctx ; |
- 同步写入一个对象;
1 | librados::bufferlist bl; |
- 为该对象添加扩展属性;
1 | librados::bufferlist lang_bl; |
- 异步读取对象;
1 | librados::bufferlist read_buf; |
- 断开连接
1 | io_ctx.close(); |
1.2 librbd
Librbd是Ceph提供块存储的库,它实现了RBD接口,基于Librados实现了对块设备的基本操作。[^3]librbd的基本架构及功能如下图所示。
Ceph librbd通过调用librados的接口,完成了块设备的接口封装。下面将讲解librbd中的具体实现情况。
2. librbd接口实现
首先,librbd中调用的rados类如下:,包括:
名称 | 声明 | 定义 | 注释 |
---|---|---|---|
librados::(v14_2_0::)IoCtx | \include\rados\librados.hpp | src\librados\librados_cxx.cc | rados的上下文实例 |
librados::(v14_2_0::)AioCompletion | \include\rados\librados.hpp | src\librados\librados_cxx.cc | rados异步操作的回调实现 |
librados::(v14_2_0::)Rados | \include\rados\librados.hpp | src\librados\librados_cxx.cc | rados实例 |
其中IoCtx是rados的上下文实例,创建时需要绑定一个存储池(pool),封装了大量对存储池的操作函数,比如:
1 | int create(const std::string& oid, bool exclusive); //创建object |
从以上源码可知,IoCtx中提供了对rados中对象的创建、删除、读写等同步操作接口以及除了同步操作,其中也提供异步操作的接口。
而AioCompletion这个类十分特别。他提供了回调函数的相关接口,即可以简单的理解成,AioCompletion表示我们需要进行的回调函数。
2.1 librbd使用实例
Ceph的rbd设备使用方法与对象存储的使用有很大的相关性,原因在于rbd本质上是对于rados的再次封装。相关接口可以直接查看*/include/rbd/librbd.hpp*查看。
- 获得集群的句柄,并连接到集群的某个Monitor中.以获得Cluster Map;
1 | std::string cluster_name("ceph"); |
- 创建IO上下文,并绑定一个已经存在的存储池;
1 | librados::IoCtx io_ctx; |
- 创建rbd设备,即我们需要的虚拟块设备,并创建image结构,这里该结构将myimage与ioctx 联系起来,后面可以通过image结构直接找到ioctx。这里会将ioctx复制两份,分为为data_ioctx和md_ctx。见明知意,一个用来处理rbd的存储数据,一个用来处理rbd的管理数据。
1 | rbd_inst.create(ioctx,'myimage',size); |
再次之后,我们就可以通过调用image的相关接口,如aio_write,aio_read对该块设备进行读写操作。
2.2 librbd读写流程
- image.read(data,0),通过image开始了一个写请求的生命的开始。这里指明了request的两个基本要素 buffer=data 和 offset=0。image.read(data,0)将会转化为librbd.cc文件中的Image::read() 函数,该函数中调用了ImageRequestWQ中的read的函数。
1 | ssize_t Image::read(uint64_t ofs, size_t len, bufferlist& bl) |
- ImageRequestWQ::read中的实现。该函数的具体实现在ImageRequestWQ.cc文件中。
1 | ssize_t ImageRequestWQ<I>::read(uint64_t off, uint64_t len, |
- a. 创建了一个等待机制的上下文。
- b. 根据上下文创建回调函数,即aio_read完成后会调用的函数。
- c. 该函数aio_read会继续处理这个读请求。
- d. 知道cond.wait()结束,即aio_read调用回调函数时,程序结束。
由上述步骤可知,ceph的同步读写实际上是在异步读写的基础上,加上同步机制实现的。
- 再来看看aio_write 拿到了 请求的offset和buffer会做点什么呢?
1 | template <typename I> |
这里对输入的情况进行讨论。
- a. 如果输入后,发现写入队列不为空/日志被其他程序打开/写入区域已被阻塞/需要锁定当前数据,就会将当前写请求加入读写队列中。
- b. 否则直接调用ImageRequet::aio_read操作进行读取。
这里直接查看第二处的执行流程。实际上,在ImageRequest::aio_read函数中,读取请求按照下图的次序进入ImageReadRequest::send_reques*中,在该函数将对于块设备的读写请求转化为对于对象的读写请求。
1 | graph TB |
- ImageReadRequest::send_request这个函数主要完成了块设备分割的功能。
1 | void ImageReadRequest<I>::send_request() { |
- a. 根据请求的大小需要将这个请求按着object进行划分,由函数file_to_extents进行处理,处理完成后按着object进行保存在extents中。该函数完成了原始请求的拆分。
一个rbd设备是有很多的object组成,也就是需要将rbd设备进行切块,每一个块叫做object,每个object的大小默认为4M,也可以自己指定。file_to_extents函数将这个大的请求分别映射到object上去,拆成了很多小的请求如下图。最后映射的结果保存在ObjectExtent中。
原本的offset是指在rbd内的偏移量(写入rbd的位置),经过file_to_extents后,转化成了一个或者多个object的内部的偏移量offset0。这样转化后处理一批这个object内的请求。
- b. 调用ObjectDispatchSpec::send,将分割后的对象读请求进行分发、处理。
- ObjectDispatchSpac::send函数将会按照下图次序进入函数*ImageReadRequest::send_request()*:
1 | graph TB |
函数ImageReadRequest::send_request将创建一个SendVistor,由观察者继续读写流程。
1 | void ObjectDispatcher<I>::send(ObjectDispatchSpec* object_dispatch_spec) { |
- 进入ObjectDispatcher::SendVisitor函数,发现读流程如下图,最终调用了read_object。
1 | graph TB |
最终在函数read_object中调用了rados对于对象的读写接口。
1 | void ObjectReadRequest<I>::read_object() { |
- a. 创建对象操作。这里将根据具体情况选择进行read或sparse_read操作。
- b. 创建回调函数。这里创建了读取操作的回调函数,回调函数将会返回读取道德结果
- c.
int r = image_ctx->data_ctx.aio_operate
语句中,data_ctx即为当前image所在存储池的IoCtx对象。通过调用IoCtx对象的接口,最终将读写请求交付到了librados的相关接口。
2.3 librbd小结
上文已经介绍,librbd的基本功能是将块存储的请求转化为对象存储。事实上,librbd实现的功能远远不止这些。包括调用rados的snap机制完成快照,借助journal完成镜像功能,并能在故障后进行故障恢复,完成回滚操作等等。
为了保证块设备的正常运行,librbd中还需要管理大量的其他数据,这些数据都会以对象的形式存储在rados分布式存储系统中。包括:
- 元数据:rbd _dircetory,rbd_id,rbd_head,rbd_object_map等
- cache数据:cache_object,cache_parent,cache_writeAround等
- 日志数据:journal
因为需要实现的功能太过繁杂,librbd的代码十分复杂。对librbd进行全文件夹搜索,发现其对librados的接口调用多达1042处。
[^1]: ceph的librados api解释
[^2]: ceph librados接口说明
[^3]: Ceph学习——Librbd块存储库与RBD读写流程源码分析
[^4]: librbd 架构分析
[^5]: ceph的数据存储之路(4) —– rbd client 端的数据请求处理
[^6]: Ceph Tiering官方文档