基本原理
整体概念:
官方文档:CEPH BLOCK DEVICE
rbd总体架构和原理:《ceph设计原理与实现》第六章
rbd快照和克隆补充:《ceph源码分析》第九章
其他一些不错的资料:
ceph rbd快照原理解析
理解 OpenStack + Ceph (3):Ceph RBD 接口和工具 [Ceph RBD API and Tools]
理解 OpenStack + Ceph (4):Ceph 的基础数据结构 [Pool, Image, Snapshot, Clone]
对外接口
rbd使用方法有两种:
- 通过librbd,通过函数接口来操作。
- 通过kernel module,走kernel 的路径,使用时类似于普通块设备,可以mkfs、mount。
kernel module的方式可以参考相关文档。
目前应用比较广泛的是librbd的方式,接入openstack、iscsi等都是使用的这种方式。下面对librbd进行介绍。
librbd
librbd大部分操作通过librados来实现,部分元数据相关的操作通过cls模块直接注册在osd上。
librbd使用方法
作为一个对外接口库,librbd默认支持c和cpp,其对应的头文件在src/include/rbd下,针对c和cpp分别是librdb.h和librbd.hpp。具体的实现在src/rbd目录下。cls相关的代码在cls/rbd目录下,具体见附录A。
src/include/rbd/librbd.hpp中最主要的两个类是class CEPH_RBD_API RBD
和class CEPH_RBD_API Image
。RBD 主要负责创建、删除、克隆镜像等操作, 而Image 类负责镜像的读写,以及快照相关的操作等等。
要使用librbd,需要先安装下面两个包。可以通过yum安装,也可以通过下载ceph源码编译后,通过make install
进行安装。
$ yum list | grep librbd
librbd1.x86_64 1:0.80.7-3.el7 base
librbd1-devel.x86_64 1:0.80.7-3.el7 base
至于如何使用librbd来编程,请参考下面的代码,这是使用librbd的一般流程。
编译时记得加上链接参数:g++ librbdtest.cpp -lrados -lrbd
。
更多函数的使用请参考 librbd.hpp。另外 这里 有一些不错的示例。
#include <rbd/librbd.hpp>
#include <rados/librados.hpp>
#include <cstring>
#include <iostream>
#include <string>
void err_msg(int ret, const std::string &msg = ""){
std::cerr<< "[error] msg:" << msg << " strerror: " << strerror(-ret) << std::endl;
}
void err_exit(int ret, const std::string &msg = ""){
err_msg(ret, msg);
exit(EXIT_FAILURE);
}
int main(int argc, char* argv[]) {
int ret = 0;
// rados
librados::Rados rados;
// use client.admin keyring
ret = rados.init("admin");
if (ret < 0)
err_exit(ret,"failed to initialize rados");
// read ceph.conf
ret = rados.conf_read_file("/path/to/ceph.conf");
if (ret < 0)
err_exit(ret, "failed to parse ceph.conf");
// connect to cluster
ret = rados.connect();
if (ret < 0)
err_exit(ret, "failed to connect to rados cluster");
std::string pool_name = "rbd";
librados::IoCtx io_ctx;
ret = rados.ioctx_create(pool_name.c_str(), io_ctx);
if (ret < 0) {
rados.shutdown();
err_exit(ret, "failed to create ioctx");
}
// rbd
librbd::RBD rbd;
std::string image_name = "image1";
librbd::Image image;
// open image
ret = rbd.open(io_ctx, image, image_name.c_str());
if (ret < 0) {
io_ctx.close();
rados.shutdown();
err_exit(ret, "failed to open rbd image");
}
// now, you can operate image
// check the image info
librbd::image_info_t info;
ret = image.stat(info, sizeof info);
if (ret < 0) {
err_msg(ret, "get image stat failed");
} else {
std::cout << "info.size:" << info.size << std::endl;
std::cout << "info.obj_size:" << info.obj_size << std::endl;
std::cout << "info.num_objs:" << info.num_objs << std::endl;
std::cout << "info.order:" << info.order << std::endl;
std::cout << "info.block_name_prefix:" << info.block_name_prefix << std::endl;
}
done:
image.close();
io_ctx.close();
rados.shutdown();
exit(EXIT_SUCCESS);
}
librbd代码小窥
这里只是librbd最简单的代码流程,只是为了告知你各个功能的实现函数在哪,至于执行这个功能所使用的异步机制等过程没有提及。
让我们通过一些函数来看一下librbd的代码流程。rbd是基于rados实现的,但rados中并没有rbd的逻辑,可以说,librbd就是rbd的完整实现,rados只是作为存储。
另外,在librbd中,使用了一种独特的代码风格。操作对外提供的api在class RBD和class Image中,但这些函数的实现仅仅是一些参数的解析和传递,其最终的功能实现,都会封装到一个名为<operation>Request
的类中,这个类往往包含完成该操作所必须的数据和功能。在类的注释中还会包含该操作执行逻辑的流程图,或者状态图。
所以当你需要寻找某个功能的实现代码时,直接寻找以这个功能命名的<operation>Request
类吧。
以RBD::create为例:
1) 首先在librbd.hpp/librbd.cc中,定义了对外的create函数接口。其函数实现,简单调用了internal.h/internal.cc中定义的librbd::create函数。
2)librbd::create函数做的主要工作就是准备create需要的各种参数,准备create操作可能用到的工具(线程池等),然后将这些数据封装到image::CreateRequest
对象中。最后调用对象的send()
函数开始执行流程。最后通过cond.wait()
来等待操作的完成。代码如下:
int create(IoCtx& io_ctx, const std::string &image_name,
const std::string &image_id, uint64_t size,
ImageOptions& opts,
const std::string &non_primary_global_image_id,
const std::string &primary_mirror_uuid,
bool skip_mirror_enable)
{
// 此处省略代码内容:
// 根据参数准备image id、order、format等属性,没有则设默认值
...
if (old_format) {
// 如果是旧版的format,调用format v1的create函数,现在很少使用
r = create_v1(io_ctx, image_name.c_str(), size, order);
} else {
// 从ceph ctx中获得全局的线程池和队列
ThreadPool *thread_pool;
ContextWQ *op_work_queue;
ImageCtx::get_thread_pool_instance(cct, &thread_pool, &op_work_queue);
C_SaferCond cond;
// 创建image::CreateRequest对象,
// 其实就是new 了一个image::CreateRequest对象。
image::CreateRequest<> *req = image::CreateRequest<>::create(
io_ctx, image_name, id, size, opts, non_primary_global_image_id,
primary_mirror_uuid, skip_mirror_enable, op_work_queue, &cond);
req->send();
// 等待创建请求完成
r = cond.wait();
}
int r1 = opts.set(RBD_IMAGE_OPTION_ORDER, order);
assert(r1 == 0);
return r;
}
3)image::CreateRequest
对象中给出的流程图,或者说状态图。这幅图描述了send()函数执行后的逻辑,其中的状态转移是通过if判断和函数调用来实现的。
Image在rados中的创建过程如下:
- 创建一个
rbd_id.<name>
对象,映射image name到image id。 - 增加
name_<name>->image id
和id_<id>->name
的映射到rbd_directorty
对象的omap。 - 创建
rbd_header.<id>
对象,在其omap和xattr中记录该image的metadata。 - 如果开启了object map特性,创建
rbd_object_map.<id>
对象,记录该image所有data object的情况 - 数据对象不会被创建,直到有数据写入
/**
* @verbatim
*
* <start> . . . . > . . . . .
* | .
* v .
* VALIDATE POOL v (pool validation
* | . disabled)
* v .
* VALIDATE OVERWRITE .
* | .
* v .
* (error: bottom up) CREATE ID OBJECT. . < . . . . .
* _______<_______ |
* | | v
* | | ADD IMAGE TO DIRECTORY
* | | / |
* | REMOVE ID OBJECT<-------/ v
* | | NEGOTIATE FEATURES (when using default features)
* | | |
* | | v (stripingv2 disabled)
* | | CREATE IMAGE. . . . > . . . .
* v | / | .
* | REMOVE FROM DIR<--------/ v .
* | | SET STRIPE UNIT COUNT .
* | | / | \ . . . . . > . . . .
* | REMOVE HEADER OBJ<------/ v /. (object-map
* | |\ OBJECT MAP RESIZE . . < . . * v disabled)
* | | \ / | \ . . . . . > . . . .
* | | *<-----------/ v /. (journaling
* | | FETCH MIRROR MODE. . < . . * v disabled)
* | | / | .
* | REMOVE OBJECT MAP<--------/ v .
* | |\ JOURNAL CREATE .
* | | \ / | .
* v | *<------------/ v .
* | | MIRROR IMAGE ENABLE .
* | | / | .
* | JOURNAL REMOVE*<-------/ | .
* | v .
* |_____________>___________________<finish> . . . . < . . . .
*
* @endverbatim
*/
C_InvokeAsyncRequest
相关的流程之后补充。
附录A cls/rbd介绍
元数据相关的操作,通过cls注册到osd上。
cls是ceph的一个模块扩展,用户可以自定义对象的接口的实现方法,通过动态链接的形式加入osd中,在osd上直接执行。在此不做展开,具体可以参考这里。
这部分rbd代码主要包含两部分,cls_rbd.h/cc
和 cls_rbd_client.h/cc
。类似于服务端和客户端的关系,前者定义了具体在osd上执行的函数,后者在客户端执行,将函数参数封装后发送给服务端(osd),然后在osd上执行。
以snapshot_add
函数为例,该函数主要负责在rbd_header
对象中增加新的snapshot元数据信息:
- 在
cls_rbd.cc
函数中,对函数进行定义和注册。下面的代码注册了rbd模块,以及snapshot_add
函数。
cls_register("rbd", &h_class);
cls_register_cxx_method(h_class, "snapshot_add",
CLS_METHOD_RD | CLS_METHOD_WR,
snapshot_add, &h_snapshot_add);
-
cls_rbd_client.h/cc
定义了通过客户端访问osd注册的cls函数的方法。以snapshot_add
函数为例,这个函数将参数封装进bufferlist,通过ioctx->exec方法,把操作发送给osd处理。
void snapshot_add(librados::ObjectWriteOperation *op, snapid_t snap_id,
const std::string &snap_name, const cls::rbd::SnapshotNamespace &snap_namespace)
{
bufferlist bl;
::encode(snap_name, bl);
::encode(snap_id, bl);
::encode(cls::rbd::SnapshotNamespaceOnDisk(snap_namespace), bl);
op->exec("rbd", "snapshot_add", bl);
}
-
cls_rbd.cc
定义了方法在服务端的实现,其一般流程是:从bufferlist将客户端传入的参数解析出来,调用对应的方法实现,然后将结果返回客户端。
/**
* Adds a snapshot to an rbd header. Ensures the id and name are unique.
*/
int snapshot_add(cls_method_context_t hctx, bufferlist *in, bufferlist *out)
{
bufferlist snap_namebl, snap_idbl;
cls_rbd_snap snap_meta;
uint64_t snap_limit;
// 从bl中解析参数
try {
bufferlist::iterator iter = in->begin();
::decode(snap_meta.name, iter);
::decode(snap_meta.id, iter);
if (!iter.end()) {
::decode(snap_meta.snapshot_namespace, iter);
}
} catch (const buffer::error &err) {
return -EINVAL;
}
// 判断参数合法性,略
......
// 完成操作,在rbd_header对象中增加新的snapshot元数据,并更新sanp_seq。
map<string, bufferlist> vals;
vals["snap_seq"] = snap_seqbl;
vals[snapshot_key] = snap_metabl;
r = cls_cxx_map_set_vals(hctx, &vals);
if (r < 0) {
CLS_ERR("error writing snapshot metadata: %s", cpp_strerror(r).c_str());
return r;
}
return 0;
}