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]:

  1. 获得集群的句柄,并连接到集群的某个Monitor中.以获得Cluster Map;
1
2
3
4
5
6
std::string cluster_name("ceph");
std::string user_name("client.admin");
librados::Rados cluster ;
cluster.init2(user_name.c_str(), cluster_name.c_str(), 0);
cluster.conf_read_file("/etc/ceph/ceph.conf");
cluster.connect();
  1. 创建IO上下文,并绑定一个已经存在的存储池;
1
2
3
librados::IoCtx io_ctx ;
std::string pool_name("data");
cluster.ioctx_create(pool_name.c_str(), io_ctx);
  1. 同步写入一个对象;
1
2
3
4
5
librados::bufferlist bl;
std::string objectId("hw");
std::string objectContent("Hello World!");
bl.append(objectContent);
io_ctx.write(objectId, bl, objectContent.size(), 0);
  1. 为该对象添加扩展属性;
1
2
3
librados::bufferlist lang_bl;
lang_bl.append("en_US");
io_ctx.setxattr(objectId, "lang", lang_bl);
  1. 异步读取对象;
1
2
3
4
librados::bufferlist read_buf;
int read_len = 4194304;
librados::AioCompletion *read_completion = librados::Rados::aio_create_completion();
io_ctx.aio_read(objectId, read_completion, &read_buf, read_len, 0 );
  1. 断开连接
1
2
io_ctx.close();
cluster.shutdown();

1.2 librbd

Librbd是Ceph提供块存储的库,它实现了RBD接口,基于Librados实现了对块设备的基本操作。[^3]librbd的基本架构及功能如下图所示。

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
2
3
4
5
6
7
8
9
10
11
int create(const std::string& oid, bool exclusive); //创建object

int write(const std::string& oid, bufferlist& bl, size_t len, uint64_t off) //从偏移处修改某个对象

int append(const std::string& oid, bufferlist& bl, size_t len);// 在对象末尾追加

int aio_read(const std::string& oid, AioCompletion *c,
bufferlist *pbl, size_t len, uint64_t off, uint64_t snapid);// 异步读
int aio_write(const std::string& oid, AioCompletion *c, const bufferlist& bl,
size_t len, uint64_t off);//异步写
...

从以上源码可知,IoCtx中提供了对rados中对象的创建、删除、读写等同步操作接口以及除了同步操作,其中也提供异步操作的接口。

而AioCompletion这个类十分特别。他提供了回调函数的相关接口,即可以简单的理解成,AioCompletion表示我们需要进行的回调函数。

2.1 librbd使用实例

Ceph的rbd设备使用方法与对象存储的使用有很大的相关性,原因在于rbd本质上是对于rados的再次封装。相关接口可以直接查看*/include/rbd/librbd.hpp*查看。

  1. 获得集群的句柄,并连接到集群的某个Monitor中.以获得Cluster Map;
1
2
3
4
5
6
std::string cluster_name("ceph");
std::string user_name("client.admin");
librados::Rados cluster ;
cluster.init2(user_name.c_str(), cluster_name.c_str(), 0);
cluster.conf_read_file("/etc/ceph/ceph.conf");
cluster.connect();
  1. 创建IO上下文,并绑定一个已经存在的存储池;
1
2
3
4
librados::IoCtx io_ctx;
std::string pool_name("data");
cluster.ioctx_create(pool_name.c_str(), io_ctx);

  1. 创建rbd设备,即我们需要的虚拟块设备,并创建image结构,这里该结构将myimage与ioctx 联系起来,后面可以通过image结构直接找到ioctx。这里会将ioctx复制两份,分为为data_ioctxmd_ctx。见明知意,一个用来处理rbd的存储数据,一个用来处理rbd的管理数据。
1
2
3
rbd_inst.create(ioctx,'myimage',size);
image = rbd.Image(ioctx,'myimage')

再次之后,我们就可以通过调用image的相关接口,如aio_write,aio_read对该块设备进行读写操作。

2.2 librbd读写流程

  1. image.read(data,0),通过image开始了一个写请求的生命的开始。这里指明了request的两个基本要素 buffer=data 和 offset=0。image.read(data,0)将会转化为librbd.cc文件中的Image::read() 函数,该函数中调用了ImageRequestWQ中的read的函数。
1
2
3
4
5
6
7
8
9
ssize_t Image::read(uint64_t ofs, size_t len, bufferlist& bl)
{
ImageCtx *ictx = (ImageCtx *)ctx;
...
int r = ictx->io_work_queue->read(ofs, len, io::ReadResult{&bl}, 0);
tracepoint(librbd, read_exit, r);
return r;
}

  1. ImageRequestWQ::read中的实现。该函数的具体实现在ImageRequestWQ.cc文件中。
1
2
3
4
5
6
7
8
9
10
11
12
ssize_t ImageRequestWQ<I>::read(uint64_t off, uint64_t len,
ReadResult &&read_result, int op_flags) {
CephContext *cct = m_image_ctx.cct;
ldout(cct, 20) << "ictx=" << &m_image_ctx << ", off=" << off << ", "
<< "len = " << len << dendl;

C_SaferCond cond; //---a
AioCompletion *c = AioCompletion::create(&cond); //---b
aio_read(c, off, len, std::move(read_result), op_flags, false); //---c
return cond.wait(); //---d
}

  • a. 创建了一个等待机制的上下文。
  • b. 根据上下文创建回调函数,即aio_read完成后会调用的函数。
  • c. 该函数aio_read会继续处理这个读请求。
  • d. 知道cond.wait()结束,即aio_read调用回调函数时,程序结束。

由上述步骤可知,ceph的同步读写实际上是在异步读写的基础上,加上同步机制实现的。

  1. 再来看看aio_write 拿到了 请求的offset和buffer会做点什么呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename I>
void ImageRequestWQ<I>::aio_read(AioCompletion *c, uint64_t off, uint64_t len,
ReadResult &&read_result, int op_flags,
bool native_async) {
CephContext *cct = m_image_ctx.cct;
...
RWLock::RLocker owner_locker(m_image_ctx.owner_lock);
if (m_image_ctx.non_blocking_aio || writes_blocked() || !writes_empty() ||
require_lock_on_read()) { //---a
queue(ImageDispatchSpec<I>::create_read_request(
m_image_ctx, c, {{off, len}}, std::move(read_result), op_flags,
trace));
} else { //---b
c->start_op();
ImageRequest<I>::aio_read(&m_image_ctx, c, {{off, len}},
std::move(read_result), op_flags, trace);
finish_in_flight_io();
}
trace.event("finish");
}

这里对输入的情况进行讨论。

  • a. 如果输入后,发现写入队列不为空/日志被其他程序打开/写入区域已被阻塞/需要锁定当前数据,就会将当前写请求加入读写队列中。
  • b. 否则直接调用ImageRequet::aio_read操作进行读取。

这里直接查看第二处的执行流程。实际上,在ImageRequest::aio_read函数中,读取请求按照下图的次序进入ImageReadRequest::send_reques*中,在该函数将对于块设备的读写请求转化为对于对象的读写请求。

1
2
3
4
graph TB
D[ImageRequestWQ::aio_read] --> A
A[ImageRequest::aio_read] -->B[ImageRequest::send]
B --> C[ImageWriteRequest::send_request]
  1. ImageReadRequest::send_request这个函数主要完成了块设备分割的功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void ImageReadRequest<I>::send_request() {
I &image_ctx = this->m_image_ctx;
CephContext *cct = image_ctx.cct;
...
Striper::file_to_extents(cct, image_ctx.format_string, &image_ctx.layout,
extent.first, extent.second, 0, object_extents,
buffer_ofs); // ---a
...
for (auto &object_extent : object_extents) {
for (auto &extent : object_extent.second) {
auto req_comp = new io::ReadResult::C_ObjectReadRequest(
aio_comp, extent.offset, extent.length,
std::move(extent.buffer_extents));
auto req = ObjectDispatchSpec::create_read(
&image_ctx, OBJECT_DISPATCH_LAYER_NONE, extent.oid.name,
extent.objectno, extent.offset, extent.length, snap_id, m_op_flags,
this->m_trace, &req_comp->bl, &req_comp->extent_map, req_comp);
req->send(); // ---b
}
}

aio_comp->put();

image_ctx.perfcounter->inc(l_librbd_rd);
image_ctx.perfcounter->inc(l_librbd_rd_bytes, buffer_ofs);
}

  • a. 根据请求的大小需要将这个请求按着object进行划分,由函数file_to_extents进行处理,处理完成后按着object进行保存在extents中。该函数完成了原始请求的拆分。

一个rbd设备是有很多的object组成,也就是需要将rbd设备进行切块,每一个块叫做object,每个object的大小默认为4M,也可以自己指定。file_to_extents函数将这个大的请求分别映射到object上去,拆成了很多小的请求如下图。最后映射的结果保存在ObjectExtent中。

img

原本的offset是指在rbd内的偏移量(写入rbd的位置),经过file_to_extents后,转化成了一个或者多个object的内部的偏移量offset0。这样转化后处理一批这个object内的请求。

  • b. 调用ObjectDispatchSpec::send,将分割后的对象读请求进行分发、处理。
  1. ObjectDispatchSpac::send函数将会按照下图次序进入函数*ImageReadRequest::send_request()*:
1
2
3
4
graph TB
D[ObjectDispatchSpac::send] --> A[ObjectDispatcherInterface::send]
A -->B[ObjectDispatcher::send]
B --> C[ImageReadRequest::send_request]

函数ImageReadRequest::send_request将创建一个SendVistor,由观察者继续读写流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void ObjectDispatcher<I>::send(ObjectDispatchSpec* object_dispatch_spec) {
auto cct = m_image_ctx->cct;
...
bool handled = boost::apply_visitor(
SendVisitor{object_dispatch, object_dispatch_spec},
object_dispatch_spec->request);
object_dispatch_meta.async_op_tracker->finish_op(); // 创建SendVistor

// handled ops will resume when the dispatch ctx is invoked
if (handled) {
return;
}
}
object_dispatch_spec->dispatcher_ctx.complete(0);
}

  1. 进入ObjectDispatcher::SendVisitor函数,发现读流程如下图,最终调用了read_object
1
2
3
4
5
6
graph TB
D[ObjectDispatcher::SendVisitor] --> A[ObjectDispatchInterface::read]
A -->B[ObjectDispatch::read]
B --> C[ObjectReadRequest::read]
C --> E[ObjectReadRequest::read_object]

最终在函数read_object中调用了rados对于对象的读写接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void ObjectReadRequest<I>::read_object() {
I *image_ctx = this->m_ictx;
...
librados::ObjectReadOperation op; // ---a
...
librados::AioCompletion *rados_completion = util::create_rados_callback<
ObjectReadRequest<I>, &ObjectReadRequest<I>::handle_read_object>(this); // ---b

int flags = image_ctx->get_read_flags(this->m_snap_id);
int r = image_ctx->data_ctx.aio_operate(
data_object_name(this->m_ictx, this->m_object_no), rados_completion, &op,
flags, nullptr,
(this->m_trace.valid() ? this->m_trace.get_info() : nullptr)); // ---c
ceph_assert(r == 0);

rados_completion->release();
}

  • a. 创建对象操作。这里将根据具体情况选择进行readsparse_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官方文档