ceph-librbd-源码分析

整体介绍

Ceph可以同时提供对象存储RGW、块存储RBD、文件系统存储Ceph FS。 RBD(RADOS Block Device)块设备类似磁盘可以被挂载。RBD块设备具有快照、多副本、克隆和一致性等特性,数据可以以条带化的方式存储到底层的Rados集群中。如果想从理论方面全面的了解rbd运作机制可以看:”Ceph设计原理与实现” 一书

上层应用访问RBD块设备有两种途径,librbd 和 krbd:

  • krbd: 集成在GNU/Linux内核的一个内核模块,用户使用用户态的rbd命令行工具可以将RBD块设备映射为本地的一个块设备文件。

  • Librbd: 一个基于librados的用户态接口库(支持c, c++, python等),实现了对RBD的基本操作。Librbd 包含了rbd的相关操作,可能有人会问librbd如何将一个 Image 对应到后端的R安东森集群中,librba通过将Image进行分片(将一个 块 分解成 对象 进行处理储)成一个个的对象,然后再存储到Rados集群中。

下图为rbd 架构图:

rbd 架构图

核心概念介绍

RBD是ceph中提供块存储的客户端服务,RBD基于librados API开发。Ceph底层的实现是对象存储,通过librbd实现了块存储的接口,对外提供块存储的服务。

名词介绍

  • Image: 对应于LVM的Logical Volume,是能被attach/detach到VM的载体。在RBD中,Image的数据有多个Object组成。(image 镜像 可以作是块存储的呈现形式)

  • Snapshot: Image的某一个特定时刻的状态,只能读不能写但是可以将Image回滚到某一个Snapshot状态,Snapshot必定属于某一个Image。

  • object: 后端Rados集群存储的单个对象,一个image会分拆成多个后端rados的对象,默认一个对象为4M,例如:一个大小为100M的image,后端对应的对象个数为25个。

  • objectcacher: 一个object级别的缓存

操作

  • Clone:为Image的某一个Snapshot的状态复制变成一个Image。如ImageA有一个Snapshot-1,clone是根据ImageA的Snapshot-1克隆得到ImageB。ImageB此时的状态与Snapshot-1完全一致,区别在于ImageB此时可写,并且拥有Image的相应能力。

RBD元数据

RBD中的元数据包括image元数据和RBD管理元数据两种数据。相关元数据在RADOS对象中有三种存储方式,根据RBD所支持或启动的功能特性的改变,RBD用于存储元数据的RADOS对象也会有所增减,所存储的元数据信息也会有所变化。

  • 二进制(data): 将元数据编码后以二进制文件的形式存储在RADOS对象的数据部分

  • xattr: 将元数据以键值对的形式存储在RADOS对象的扩展属性中

  • omap: 将元数据以键值对的形式存储在RADOS对象omap中

image元数据

单个image的元数据信息,这些数据记录的是单个image相关的元信息。

Rados对象名 元数据类型 描述
rbd_id.( name ) data 记录image名称到image id的单项映射关系,image内部元数据和数据的名称已id为基础,这样即使image重命名,内部的结构也基本不用发生改变。
rbd_header.( id ) omap/xattr 记录image所支持的功能特性、容量大小等基本信息以及配置参数、自定义元数据、锁信息等
rbd_object_map.( id ) data 记录组成image的所有数据对象的存在状态
rbd_header元数据记录

rbd_header对象是image最主要的元数据对象,除了记录容量大小等基本信息之外,为单个image配置的参数、用户自定义的元数据以及为了支持image互斥访问所记录的锁信息等也都在记录在该对象中。

key type 描述
data_pool_id omap 指定将数据对象存储在与元数据对象不同的存储池,EC纠删码存储池不支持omap, EC存储时会用到这个字段
features omap 已启用的功能特性
object_prefix data 数据对象名称前缀
order omap 组成 image 的数据对象容量大小,以 2 为底的指数
parent omap 当存在克隆关系时,克隆 image 记录的关联的父 image 快照信息
size omap 容量大小
snap_seq omap 最近一次创建的快照的快照id
snapshot_(snap_id) omap id为snap_id的快照的基本信息
stripe_count omap 条带宽度
strip_unit omap 条带大小
lock_rbd_lock xattr 控制 image 互斥访问的锁信息,有两种类型:分别是LOCK_EXCLUSIVE(在同一时刻只允许一个客户端对image上锁)、LOCK_SHARED(允许多个客户端同时对image上锁)
image 特性

features所记录的元数据是一个64位的整型数据,用于记录image已启用的功能特性。每个特性占用整型中的一位。

特性 bit位 描述
layering 0 是否支持image克隆操作,克隆 image 与关联的父image快照之间通过COW实现数据共享
striping 1 是否进行数据对象间的数据条带化,类似于RAID.0, 在创建 image 时如果指定了条带化参数,数据会在多个 image 数据对象之间进行条带化
exclusive-lock 2 是否支持分布式锁,即 image 自带互斥访问锁机制以限制同时只能有一个客户端访问 image, 主要应用于虚机的热迁移
object-map 3 是否记录组成 image 的数据对象存在状态位图,通过查表加速类似于导入、导出、克隆分离、已使用容量计算等操作,同时有助于减小COW机制带来的克隆 image 的I/O时延,依赖于exclusive-lock特性
fast-diff 4 用于计算快照间增量数据等操作加速,依赖于object-map特性
deep-flatten 5 克隆分离时是否同时解除克隆 image 创建的快照与父 image 之间的关联关系。该特性只是为了阻止老的RBD客户端访问 image 而设置,从 Ceph I 版本开始librbd的内部克隆分离实现已经不区分是否开启该特性
journaling 6 是否记录 image 修改操作到日志对象,用于远程异步镜像功能,依赖于 exclusive-lock 特性
data-pool 7 是否将数据对象存储于与元数据不同的存储池,用于支持将 image 的数据对象存储于 EC 纠删码存储池

RBD管理元数据

存储池级别用来记录和管理多个Image的元数据。

Rados对象名 元数据类型 描述
rbd_directory omap 记录存储池中所有的image列表
rbd_children omap 记录父image快照到克隆image之间的单向映射关系(parent –> children)
rbd_directory元数据记录

每个创建了 image 的存储池下都有一个 rbd_directory 元数据对象用于记录当前存储池中的image列表。image 列表信息以键值对的形式记录在rbd_directory对象的omap中。

Key type 描述
name_(name) omap 记录 image 名称所对应的 image id
id_(id) omap 记录 image id 所对应的 image 名称

条带化介绍

rbd会将一个大的image分拆成多个object,数据会条带化的存储到后端Rados集群中,ceph默认不开启对象条带化。条带化类似 raid0,就是将raid0中的磁盘换成了对象。

下图为条带化示意图:

条带化 示意图

上图概念介绍

  • 蓝色柱状:代表一个个rados底层对象,默认为4M
  • 绿色块su:代表一个个条带化单元
  • 红色框stripe: 代表一个个条带
  • objectset代表对象组: 在cephfs中一般一个对象组属于同一个文件,在rbd中没有这种限制。

条带化事例

  • 概念介绍
object_size:对象的大小,就是rados底层对象的大小,一般默认是4M

stripe_unit: 条带每个su的大小

stripe_count:每个条带有多少个su, 跨域多少个底层object

stripes_per_object: 每个底层object对应多少个条带
  • 条带化案例 rbd的源码中使用 file_to_extent 这个函数完成数据的条带化。file_to_extent函数把一维坐标转化成三维坐标(objectset,stripeno,stripepos),这三维坐标分别表示哪一个objectset,哪一个stripe(条带),条带中的哪一个su(对象分片)。

下图为条带化案例图:

条带化案例

上图假设一个对象的大小是3M,一个对象分片大小是1M,上图两个objectset占用6个rados的对象,总共有18个条带化对象分片。我们读取1M ~ 6M的相关数据,上图所示就需要读取分片su1 ~ su6 六个分片。具体变量如下:

offset: 1M 表示读偏移量
len: 6M 表示要读取的大小

object_size: 3M
stripe_unit: 1M
stripe_count: 3
stripes_per_object: 3

条带化后就会得到对应后端每个对象需要写入或者读取的偏移和长度,并将相关请求通过librados发送到后端集群。

源码介绍

源码介绍

  • librbd.cc: 这个文件里实现了rbd对外提供接口的实现,其实大部分接口的具体实现是在internal.cc文件中。

  • ImageCtx.cc:定义了image的上下文,包括image layout的初始化、卷/snap相关元数据的获取接口以及后来引入的rbd mirror的相关元数据结构(object map、journal 等)的创建。

  • ImageRequestWQ: 这个类的详细定义在ImageRequestWQ.cc文件中,用户的读写请求都是传到这个类中进行处理,有两种处理方式:1:直接读写。2:丢入到相应的队列中,有专门的线程从队列中获取请求进行读写。

  • image request 这一系列类的实现主要在ImageRequest.cc文件中,主要用来处理image请求相关的操作。涉及的相关类如下图所示:

rbd image request类图

  • object request 这一系列类的实现主要在ObjectRequest.cc文件中,主要用来处理对象请求的相关操作,这里的对象来自于上层对单image的切分。涉及的主要相关类如下图所示:

rbd object request类图

读写流程

总体介绍

RBD中包含几个数据操作的关键的类,其中 ObjectRequest用于对某一个对象的处理的基类,而ImageRequest则是处理Image的基类。图中可以看到他们都有不少子类,其中 AbstractImageWrite下面还有 imageWrite、ImageDiscard等子类,当一个Image的操作跨越了多个对象的时候(一个块会被拆成多个对象进行操作,Ceph本质是对象存储),在每个对象中产生一个ObjectRequest 请求,用于在各自的对象中各自处理。

rbd 读写流程

写流程

  • Image::aio_write2 处于Librbd里面,在librbd的c++接口中,这个接口可用来写入数据,其它的接口如:Image::aio_write, rbd_aio_write等函数也可用来写入数据,其在调用流程上大同小异
    int Image::aio_write2(uint64_t off, size_t len, bufferlist& bl,
                RBD::AioCompletion *c, int op_flags)
    {
      ...
      //调用ImageRequestWQ<I>::aio_write  来生成ImageRequest对象
      ictx->io_work_queue->aio_write(get_aio_completion(c), off, len,
                                     bufferlist{bl}, op_flags);
      ...
    }
    
  • ImageRequestWQ::aio_write:处于 ImageRequestWQ中,程序会根据用户的配置自动选择将image请求放入队列中还是直接调用。ImageRequestWQ初始化时会启动一个线程专门用于处理队列中的数据,具体的入口函数为ImageRequestWQ::process()
template <typename I>
void ImageRequestWQ<I>::aio_write(AioCompletion *c, uint64_t off, uint64_t len,
                                  bufferlist &&bl, int op_flags,
                                  bool native_async) {
    ...
    if (m_image_ctx.non_blocking_aio || writes_blocked()) {
        //将写请求 的 ImageRequest 加入队列中
        queue(ImageRequest<I>::create_write_request(
            m_image_ctx, c, , std::move(bl), op_flags, trace));
    } else {
        c->start_op();
        //直接调用ImageRequest<I>::aio_write处理写请求
        ImageRequest<I>::aio_write(&m_image_ctx, c, , std::move(bl),
                                   op_flags, trace);
        finish_in_flight_io();
    }
    ...
}
  • ImageRequestWQ::process 如果上一步是将相关数据放入队列中,ImageRequestWQ的线程会自动处理队列中的数据,process是其入口函数
    template <typename I>
    void ImageRequestWQ<I>::process(ImageRequest<I> *req) {
      ...
    
      //调用imageRequest的具体请求,send方法具体调用send_request方法
      req->send();
    
      ...
    }
    
  • AbstractImageWriteRequest::send_request, 类 ImageWriteRequest 继承自 AbstractImageWriteRequest, 最后调用的是AbstractImageWriteRequest的send_request()函数。并且,后面调用的函数都类似,都是在ImageWriteRequest类的函数中调用AbstractImageWriteRequest对应的函数。因此直接看AbstractImageWriteReques对应的函数。
template <typename I>
void AbstractImageWriteRequest<I>::send_request() {
  ...
 /!!!!!!!将image 分片,块->对象的映射(块存储 的image 依然要用对象存储来处理),具体可以参考条带化哪一章
 Striper::file_to_extents(cct, image_ctx.format_string, &image_ctx.layout,
                               extent.first, extent.second, 0, object_extents);
  ...

  //将分成的对象 分别生成 ObjectRequests 对象 调用send_object_requests 各个对象各自处理
  send_object_requests(object_extents, snapc,
                         (journaling ? &requests : nullptr));
  ...
}
  • AbstractImageWriteRequest::send_object_requests: 用于发送请求,如果 journaling 则 将请求加入队列中 否则直接 调用AbstractObjectWriteRequest::send() 发送请求。

  • AbstractObjectWriteRequest::send: 先处理object_map相关 ,然后调用AbstractObjectWriteRequest::pre_write_object_map_update(),然后最终调用AbstractObjectWriteRequest::write_object()发送请求,将请求发送到librados处理。

读流程

  • Image::aio_read2 处于Librbd里面,在librbd的c++接口中,这个接口可用来写入数据,其它的接口如:Image::aio_read, rbd_aio_read等函数也可用来写入数据,其在调用流程上大同小异
    int Image::aio_read2(uint64_t off, size_t len, bufferlist& bl,
              RBD::AioCompletion *c, int op_flags)
    {
      ...
    
    //调用ImageRequestWQ<I>::aio_read  来生成ImageRequest对象
      ictx->io_work_queue->aio_read(get_aio_completion(c), off, len,
                                    io::ReadResult{&bl}, op_flags);
        
      ...
    }
    
  • ImageRequestWQ::aio_read:处于 ImageRequestWQ中,程序会根据用户的配置自动选择将image请求放入队列中还是直接调用。ImageRequestWQ初始化时会启动一个线程专门用于处理队列中的数据,具体的入口函数为ImageRequestWQ::process()
void ImageRequestWQ<I>::aio_read(AioCompletion *c, uint64_t off, uint64_t len,
                                 ReadResult &&read_result, int op_flags,
                                 bool native_async) {
    ...

    // if journaling is enabled -- we need to replay the journal because
    // it might contain an uncommitted write
    RWLock::RLocker owner_locker(m_image_ctx.owner_lock);
    if (m_image_ctx.non_blocking_aio || writes_blocked() || !writes_empty() ||
        require_lock_on_read()) {
        //将读请求 的 ImageRequest 加入队列中
        queue(ImageRequest<I>::create_read_request(m_image_ctx, c, ,
                                                   std::move(read_result),
                                                   op_flags, trace));
    } else {
        c->start_op();
        //直接调用 ImageRequest 的读请求
        ImageRequest<I>::aio_read(&m_image_ctx, c, ,
                                  std::move(read_result), op_flags, trace);
        finish_in_flight_io();
    }
    trace.event("finish");
}
  • ImageRequestWQ::process 如果上一步是将相关数据放入队列中,ImageRequestWQ的线程会自动处理队列中的数据,process是其入口函数
    template <typename I>
    void ImageRequestWQ<I>::process(ImageRequest<I> *req) {
      ...
    
      //调用imageRequest的具体请求,send方法具体调用send_request方法
      req->send();
    
      ...
    }
    
  • ImageReadRequest::send_request, 发送image的读请求,这个函数其实就是将相关数据拆分为不同的object,然后调用object的读请求。
void ImageReadRequest<I>::send_request() {
  ...
 /!!!!!!!将image 分片,块->对象的映射(块存储 的image 依然要用对象存储来处理),具体可以参考条带化哪一章
 Striper::file_to_extents(cct, image_ctx.format_string, &image_ctx.layout,
                               extent.first, extent.second, 0, object_extents,
                               buffer_ofs);
  ...

  //将分成的对象 分别生成 ObjectRequests 对象 调用send_object_requests 各个对象各自处理
  ObjectReadRequest<I> *req = ObjectReadRequest<I>::create(
        &image_ctx, extent.oid.name, extent.objectno, extent.offset,
        extent.length, snap_id, m_op_flags, false, this->m_trace, req_comp);
      req_comp->request = req;
      req->send();
  ...
}
  • void ObjectReadRequest::send, 该函数具体调用 object 的读请求,如果开启的objectcache就会优先从缓存中读取,否则直接读取
    void ObjectReadRequest<I>::send() {
    ...
    
    if ((!m_cache_initiated && image_ctx->object_cacher != nullptr &&
      image_ctx->object_cacher->is_cache_enabled()) &&
      ((!image_ctx->local_cache) || (image_ctx->local_cache && image_ctx->snap_id == CEPH_NOSNAP))) {
      // 尝试从缓存中读取
      read_cache();
    } else {
      //直接读取数据
      read_object();
    }
    }
    

尾记

转载请注明原地址,冯杰的博客:http://Lfengjie.github.io 谢谢!

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦