ceph rbd:总览

基本原理

整体概念:
官方文档: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使用方法有两种:

  1. 通过librbd,通过函数接口来操作。
  2. 通过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 RBDclass 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 
#include 

#include 
#include 
#include 

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中,但这些函数的实现仅仅是一些参数的解析和传递,其最终的功能实现,都会封装到一个名为Request的类中,这个类往往包含完成该操作所必须的数据和功能。在类的注释中还会包含该操作执行逻辑的流程图,或者状态图。

所以当你需要寻找某个功能的实现代码时,直接寻找以这个功能命名的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.对象,映射image name到image id。
  • 增加name_->image idid_->name的映射到rbd_directorty对象的omap。
  • 创建rbd_header.对象,在其omap和xattr中记录该image的metadata。
  • 如果开启了object map特性,创建rbd_object_map.对象,记录该image所有data object的情况
  • 数据对象不会被创建,直到有数据写入
  /**
   * @verbatim
   *
   *                                   . . . . > . . . . .
   *                                     |                      .
   *                                     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                     .
   * |_____________>___________________ . . . . < . . . .
   *
   * @endverbatim
   */

C_InvokeAsyncRequest相关的流程之后补充。

附录A cls/rbd介绍

元数据相关的操作,通过cls注册到osd上。

cls是ceph的一个模块扩展,用户可以自定义对象的接口的实现方法,通过动态链接的形式加入osd中,在osd上直接执行。在此不做展开,具体可以参考这里。

这部分rbd代码主要包含两部分,cls_rbd.h/cccls_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 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;
}

你可能感兴趣的:(ceph rbd:总览)