整体介绍
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是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请求相关的操作。涉及的相关类如下图所示:
- object request 这一系列类的实现主要在ObjectRequest.cc文件中,主要用来处理对象请求的相关操作,这里的对象来自于上层对单image的切分。涉及的主要相关类如下图所示:
读写流程
总体介绍
RBD中包含几个数据操作的关键的类,其中 ObjectRequest用于对某一个对象的处理的基类,而ImageRequest则是处理Image的基类。图中可以看到他们都有不少子类,其中 AbstractImageWrite下面还有 imageWrite、ImageDiscard等子类,当一个Image的操作跨越了多个对象的时候(一个块会被拆成多个对象进行操作,Ceph本质是对象存储),在每个对象中产生一个ObjectRequest 请求,用于在各自的对象中各自处理。
写流程
- 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 谢谢!