文章目录
RBD 读写流程
librdb 中提供块设备的用户空间实现, 让用户可以直接操作 Ceph RBD, 在 RBD 上直接读写数据.
本文主要介绍 librbd 的块设备读写接口, 包括基本的使用方法和基本的读写流程(源码角度), 本文内容基于 Ceph 10.2.11 版本的源码
API 介绍
读写相关的 API 一共 8 个, 属于 Image
类, 这 8 个 API 的原型如下
ssize_t Image::read(uint64_t ofs, size_t len, bufferlist& bl)
ssize_t Image::read2(uint64_t ofs, size_t len, bufferlist& bl, int op_flags)
int Image::aio_read(uint64_t off, size_t len, bufferlist& bl, RBD::AioCompletion *c)
int Image::aio_read2(uint64_t off, size_t len, bufferlist& bl, RBD::AioCompletion *c, int op_flags)
ssize_t Image::write(uint64_t ofs, size_t len, bufferlist& bl)
ssize_t Image::write2(uint64_t ofs, size_t len, bufferlist& bl, int op_flags)
int Image::aio_write(uint64_t off, size_t len, bufferlist& bl, RBD::AioCompletion *c)
int Image::aio_write2(uint64_t off, size_t len, bufferlist& bl, RBD::AioCompletion *c, int op_flags)
其中:
- ofs 代表读写的起始位置,
- len 代表读写的长度,
- bl 是要写入 image 的数据或者要存放读取到的数据的空间,
- op_flags 是一些读写的标志未, 一般传 0 即可
- *c 是异步 IO 的回调函数
- 前缀为
aio_
的 API 为异步读写, 没有aio_
前缀的为同步读写. - 后缀为
2
的 API 支持指定op_flags
参数
实际上以上 8 个 API 分别调用了 AioImageRequestWQ
中的异步读写方法, 其函数原型如下
void AioImageRequestWQ<I>::aio_write(AioCompletion *c, uint64_t off, int64_t len, const char *buf, int op_flags, bool native_async)
void AioImageRequestWQ<I>::aio_read(AioCompletion *c, uint64_t off, uint64_t len, char *buf, bufferlist *pbl, int op_flags, bool native_async)
因为这两个方法都是异步操作函数, 所以 8 个 API 中的同步函数 Image::read() Image::write()
会在内部创建一个条件变量, 作为回调函数, 再调用AioImageRequestWQ<I>
的异步读写, 然后阻塞等待读/写 完成.
8 个 API 中没有后缀 2
的 API 则会默认使用 0
作为 op_flags
所以这 8 个 API 实际上就是对 AioImageRequestWQ
中异步读写方法的封装, 所以只要了解 AioImageRequestWQ
中的两个异步读写方法即可.
源码片段:
ssize_t Image::read(uint64_t ofs, size_t len, bufferlist& bl){
...
int r = ictx->aio_work_queue->read(ofs, len, bl.c_str(), 0);
...
return r;
}
template <typename I>
ssize_t AioImageRequestWQ<I>::read(uint64_t off, uint64_t len, char *buf, int op_flags) {
...
C_SaferCond cond;
AioCompletion *c = AioCompletion::create(&cond);
aio_read(c, off, len, buf, NULL, op_flags, false);
return cond.wait();
}
API 用法
下面的代码调用了 Image::aio_read2
和 Image::aio_write2
两个接口, 再 Image 上写入一段数据后再读出
#include <rbd/librbd.hpp>
#include <rados/librados.hpp>
#include <cstring>
#include <iostream>
#include <string>
using namespace std;
using namespace librbd;
const char *config = "/etc/ceph/ceph.conf";
const char *POOL = "rbd"; // pool name
const char *IMAGE= "image"; // RBD name
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 rados_connect(librados::Rados &rados, const char *user = "admin") {
int ret = 0;
ret = rados.init(user);
if (ret < 0)
err_exit(ret, "failed to initialize rados");
ret = rados.conf_read_file(config);
if (ret < 0)
err_exit(ret, "failed to parse %s" + string(config));
ret = rados.connect();
if (ret < 0)
err_exit(ret, "failed to connect to rados cluster");
return 0;
}
//简单的回调函数,用于librbd::RBD::AioCompletion
void simple_cb(librbd::completion_t cb, void *arg) {
std::cout << "read completion cb called!" << std::endl;
}
int main(int argc, char *argv[]) {
// connect rados
librados::Rados rados;
int ret = 0;
ret = rados_connect(rados);
if (ret < 0) {
err_exit(ret, "failed to connect to rados cluster");
}
// open pool
librados::IoCtx io_ctx;
ret = rados.ioctx_create(POOL, io_ctx);
if (ret < 0) {
rados.shutdown();
err_exit(ret, "failed to create ioctx");
}
// open image(RBD)
RBD rbd;
Image image;
ret = rbd.open(io_ctx, image, IMAGE);
if (ret < 0) {
io_ctx.close();
rados.shutdown();
err_exit(ret, "failed to open rbd image");
}
ceph::bufferlist bl_in;
ceph::bufferlist bl_out;
// 定义回调函数
auto *cbw = new RBD::AioCompletion(nullptr, (librbd::callback_t) simple_cb);
auto *cbr = new RBD::AioCompletion(nullptr, (librbd::callback_t) simple_cb);
bl_in.append("12345678910");
image.aio_write2(0, bl_in.length(), bl_in, cbw, 0);
cbw->wait_for_complete();
image.aio_read2(0, bl_in.length(), bl_out, cbr, 0);
cbr->wait_for_complete();
image.close();
io_ctx.close();
rados.shutdown();
exit(EXIT_SUCCESS);
}
类
librbd
读写的过程主要的代码位于源码根目录的 src/librbd
, 和 osdc
两个目录中, 主要的业务逻辑在 src/librbd
中.
源码分析
以 Image::aio_read2
和 Image::aio_write2
这两个异步读写接口为例
Image::aio_read2
#### 流程图:
读写流程
Image::aio_read2
Image::aio_read2
是 librbd
提供给用户的接口, 具备了异步读写的能力, 函数中通过 ImageCtx
调用 AioImageRequestWQ<I>::aio_read
, ictx->aio_work_queue
是AioImageRequestWQ
类型的成员变量 .
int Image::aio_read2(uint64_t off, size_t len, bufferlist& bl, RBD::AioCompletion *c, int op_flags)
{
ImageCtx *ictx = (ImageCtx *)ctx; // todo init?
....
ictx->aio_work_queue->aio_read(get_aio_completion(c), off, len, NULL, &bl, op_flags);
return 0;
}
AioImageRequestWQ::aio_read
AioImageRequestWQ<I>::aio_read
中判断 IO 的类型, 如果是非阻塞IO,或者有rbd mirror,或者有其它阻塞的IO请求,就调用函数 AioImageReques
对象,加入到AioImageRequestWQ
的工作队列里.
否则如果是阻塞IO请求就直接调用 AioImageRequest<I>::aio_read
处理读请求
本函数实现了阻塞 IO 请求与非阻塞 IO 请求的分离. 通过线程池的 queue
实现了异步的 IO
template <typename I>
void AioImageRequestWQ<I>::aio_read(AioCompletion *c, uint64_t off, uint64_t len, char *buf, bufferlist *pbl, int op_flags, bool native_async) {
...
RWLock::RLocker owner_locker(m_image_ctx.owner_lock);
if (m_image_ctx.non_blocking_aio || writes_blocked() || !writes_empty() || require_lock_on_read()) {
queue(AioImageRequest<I>::create_read_request(m_image_ctx, c, off, len, buf, pbl, op_flags));
} else {
c->start_op();
AioImageRequest<I>::aio_read(&m_image_ctx, c, off, len, buf, pbl, op_flags);
finish_in_flight_io();
}
}
AioImageRequest::aio_read
AioImageRequest<I>::aio_read
中创建一个 AioImageRead<I> req
请求. 然后调用 req.send()
发送请.
本函数实现了将一个请求转换为具体的针对于镜像的 IO 请求.
如果是在上一步中的非阻塞 IO, 即放入线程池的 queue
中的请求, 线程池最终也会调用 req.send
, 可以看到在 AioImageRequestWQ<I>::aio_read
将请求放入 queue
时调用的 AioImageRequest<I>::create_read_request
实际上就是创建了一个 req
, 然后通过线程池实现了异步的 req.send
AioImageRequest
继承自 AioImageRequest
并且并没有实现 send 函数, 所以这里的 req.send()
最终会调用父类的 void AioImageRequest<I>::send
.
template <typename I>
void AioImageRequest<I>::aio_read(I *ictx, AioCompletion *c, uint64_t off, size_t len, char *buf, bufferlist *pbl, int op_flags) {
AioImageRead<I> req(*ictx, c, off, len, buf, pbl, op_flags);
req.send();
}
template <typename I>
AioImageRequest<I>* AioImageRequest<I>::create_read_request( I &image_ctx, AioCompletion *aio_comp, uint64_t off, size_t len, char *buf, bufferlist *pbl, int op_flags) {
return new AioImageRead<I>(image_ctx, aio_comp, off, len, buf, pbl, op_flags);
}
void AioImageRequest::send
AioImageRequest
是一个虚类, 此时的 send 是通过子类调用的, 子类实现了 send_request()
所以这里最终将会调用子类的 send_request
即 AioImageRead
本函数实际上就是通过抽象层, 实现了部分不同的业务中相同的逻辑
template <typename I>
void AioImageRequest<I>::send() {
I &image_ctx = this->m_image_ctx;
...
CephContext *cct = image_ctx.cct;
AioCompletion *aio_comp = this->m_aio_comp;
...
aio_comp->get();
send_request();
}
AioImageRead::send_request
AioImageRead
是整个读流程中的核心函数, 其实现了 预读, 回调函数处理,Image 到 Object 的转换等功能, 并且实现了直接在 RBD cache 中读取数据的功能
其中 Image 到 Object 的转换是通过 file_to_extents
实现的, 将 Image
读写转换为对象的读写后, 逐个对象的进行读写, 如果开启了缓存就进入 ImageCtx::aio_read_from_cache
函数, 否则就调用 void AioObjectRead<I>::send
直接通过 librados 来读取数据
部分代码以及函数解析:
template <typename I>
void AioImageRead<I>::send_request() {
I &image_ctx = this->m_image_ctx;
CephContext *cct = image_ctx.cct;
// 开启了缓存, 并且预读 bytes 大于 0 , 并且需要预读, 则调用 readahead 进行预读操作
if (image_ctx.object_cacher && image_ctx.readahead_max_bytes > 0 && !(m_op_flags & LIBRADOS_OP_FLAG_FADVISE_RANDOM)) {
readahead(get_image_ctx(&image_ctx), m_image_extents);
}
// 回调函数处理
AioCompletion *aio_comp = this->m_aio_comp;
librados::snap_t snap_id;
map<object_t,vector<ObjectExtent> > object_extents;
uint64_t buffer_ofs = 0;
{
for (vector<pair<uint64_t,uint64_t> >::const_iterator p = m_image_extents.begin(); p != m_image_extents.end(); ++p) {
uint64_t len = p->second;
//验证参数合法, 有必要则裁剪 io
int r = clip_io(get_image_ctx(&image_ctx), p->first, &len);
...
// image 读写 --> Object 的过程
Striper::file_to_extents(cct, image_ctx.format_string, &image_ctx.layout, p->first, len, 0, object_extents, buffer_ofs);
buffer_ofs += len;
}
}
....
// issue the requests, 处理每一个 Object
for (auto &object_extent : object_extents) {
for (auto &extent : object_extent.second) {
...
C_AioRead<I> *req_comp = new C_AioRead<I>(aio_comp);
AioObjectRead<I> *req = AioObjectRead<I>::create( &image_ctx, extent.oid.name, extent.objectno, extent.offset, extent.length, extent.buffer_extents, snap_id, true, req_comp, m_op_flags);
req_comp->set_req(req);
if (image_ctx.object_cacher) {
C_CacheRead<I> *cache_comp = new C_CacheRead<I>(image_ctx, req);
image_ctx.aio_read_from_cache(extent.oid, extent.objectno, &req->data(), extent.length, extent.offset, cache_comp, m_op_flags);
} else {
req->send();
}
}
}
aio_comp->put();
...
}
Striper::file_to_extents
本函数的主要作用就是将对于 Image 的读写转换为相对于 Object 的读写操作, 因为 Ceph 底层是以对象的形式存储数据的, 而对于块设备来说通常是以off, len
, 即: 从 off
位置, 读取长度为 len
的数据,.
这些数据可能分布在底层多个对象的不同位置上, 这就需要将这个一维的读写转换为针对底层对象的读写操作即一个 3 维的读写, 3 维指的是 objectset,stripeno,stripepos
.
并且实际上上层的 librdb 是以条带的形式来读写底层的对象的, 有关于条带的概念可见 // todo
void Striper::file_to_extents(CephContext *cct, const char *object_format, const file_layout_t *layout, uint64_t offset, uint64_t len, uint64_t trunc_size, map<object_t,vector<ObjectExtent> >& object_extents, uint64_t buffer_offset)
{
assert(len > 0);
...
__u32 object_size = layout->object_size; // 对象大小默认是 4MiB
__u32 su = layout->stripe_unit; // 条带大小默认 1
__u32 stripe_count = layout->stripe_count; // 条带数量.默认 1
assert(object_size >= su);
if (stripe_count == 1) {
ldout(cct, 20) << " sc is one, reset su to os" << dendl;
su = object_size;
}
...
uint64_t stripes_per_object = object_size / su;
uint64_t cur = offset;
uint64_t left = len;
// 把一维的读写信息, 循环计算出每一个 Object 的坐标信息
while (left > 0) {
// layout into objects
uint64_t blockno = cur / su; // which block , su 默认是 4MiB cur 就是要读写的 offset ??, blockno 就是 rbd data 对象的 编号
// which horizontal stripe (Y)
uint64_t stripeno = blockno / stripe_count;
// which object in the object set (X)
uint64_t stripepos = blockno % stripe_count;
// which object set
uint64_t objectsetno = stripeno / stripes_per_object;
// object id
uint64_t objectno = objectsetno * stripe_count + stripepos;
// find oid, extent
char buf[strlen(object_format) + 32];
// 拼接出一个完整的 Object name 如 rbd_data.04b73a65f737b2.000000000000328e
snprintf(buf, sizeof(buf), object_format, (long long unsigned)objectno);
object_t oid = buf;
// map range into object
uint64_t block_start = (stripeno % stripes_per_object) * su;
uint64_t block_off = cur % su;
uint64_t max = su - block_off;
uint64_t x_offset = block_start + block_off;
uint64_t x_len;
if (left > max)
x_len = max;
else
x_len = left;
// 赋值给 object_extents 相应的 oid
ObjectExtent *ex = 0;
vector<ObjectExtent>& exv = object_extents[oid];
if (exv.empty() || exv.back().offset + exv.back().length != x_offset) {
exv.resize(exv.size() + 1);
ex = &exv.back();
ex->oid = oid;
ex->objectno = objectno;
ex->oloc = OSDMap::file_to_object_locator(*layout);
...
ex->offset = x_offset;
ex->length = x_len;
ex->truncate_size = object_truncate_size(cct, layout, objectno, trunc_size);
...
} else {
// add to extent
ex = &exv.back();
ex->length += x_len;
}
ex->buffer_extents.push_back(make_pair(cur - offset + buffer_offset, x_len)); // 这里是计算每个 Object 中读写数据的位置和长度, pair<offset, object>
...
left -= x_len;
cur += x_len;
}
}
object_extents
Striper::file_to_extents
最终将转换的结果保存在 object_extents
中, object_extents
结构为 map<object_t,vector<ObjectExtent> > object_extents
. 其中:
obect_t
是一个object_t
结构体, 其最主要的参数就是 就是 name, 即对象名称如 rbd_data.519646b8b4567.000000000000000ObjectExtent
就是object_t
对象有关的读写信息
obect_t
和ObjectExtent
定义如下
struct object_t {
string name;
object_t() {}
// cppcheck-suppress noExplicitConstructor
object_t(const char *s) : name(s) {}
// cppcheck-suppress noExplicitConstructor
object_t(const string& s) : name(s) {}
void swap(object_t& o) {
name.swap(o.name);
}
void clear() {
name.clear();
}
void encode(bufferlist &bl) const {
::encode(name, bl);
}
void decode(bufferlist::iterator &bl) {
::decode(name, bl);
}
};
class ObjectExtent {
public:
object_t oid; // object id
uint64_t objectno;
uint64_t offset; // in object
uint64_t length; // in object
uint64_t truncate_size; // in object
object_locator_t oloc; // object locator (pool etc)
vector<pair<uint64_t,uint64_t> > buffer_extents; // off -> len. extents in buffer being mapped (may be fragmented bc of striping!)
ObjectExtent() : objectno(0), offset(0), length(0), truncate_size(0) {}
ObjectExtent(object_t o, uint64_t ono, uint64_t off, uint64_t l, uint64_t ts) : oid(o), objectno(ono), offset(off), length(l), truncate_size(ts) { }
};
image_ctx.aio_read_from_cache
该函数会调用 readx
来进行缓存的读取
void ImageCtx::aio_read_from_cache(object_t o, uint64_t object_no, bufferlist *bl, size_t len, uint64_t off, Context *onfinish, int fadvise_flags) {
snap_lock.get_read();
ObjectCacher::OSDRead *rd = object_cacher->prepare_read(snap_id, bl, fadvise_flags);
snap_lock.put_read();
ObjectExtent extent(o, object_no, off, len, 0);
extent.oloc.pool = data_ctx.get_id();
extent.buffer_extents.push_back(make_pair(0, len));
rd->extents.push_back(extent);
cache_lock.Lock();
int r = object_cacher->readx(rd, object_set, onfinish);
cache_lock.Unlock();
if (r != 0)
onfinish->complete(r);
}
readx
readx 的函数的工作就是去处理 file_to_exnts
中分片出来的每一个 objectextent
中的 Object(这里的 Object 就是 Ceph 标准意义上的, 直接存储在底层的 Object), 一个简单的方案就是将整个 Object 一次性读取出来, 但是底层的 Object 默认大小为 4MiB, 如果这样直接的读取, 如果读取的数据本身就比较小, 那么可能会读取到大量无关的数据, 所以这里每次读/写并不一定是读/写整个 Object 中的内容, 可能只是读/写 Object 中的部分数据(即根据 objectextent
中的 offset, len
确定 Object 中真正要读取的数据) , 并且由于有些数据片段已经存在在本地缓存, 有些不在, 所以还要对此进行处理, 最大限度的利用已有的缓存.
readx 函数很长, 这里只摘抄了部分代码
get_object()
这里会根据 objectno
来从本地内存中查询是否存在对于 Obejct
的缓存, 存在则直接返回相应缓存, 不存在就在内存上创建该缓存空间让后续填充map_read()
这里是将要读取的数据分为 hits, missing, rx, hits
就是内存中已经存在的数据, missing
就是不存在的, rx
应该是指接受缓冲区的数据
bh_it->second->waitfor_read
这个队列中放入的是未命中的缓存, 在队列中注册一个回调函数, 回调函数倍调用后, 会和 osd 通讯读取对应的数据, 然后 readx
会二次读取, 这时就可以读取到相应的数据了
int ObjectCacher::_readx(OSDRead *rd, ObjectSet *oset, Context *onfinish, bool external_call)
{
...
uint64_t bytes_in_cache = 0;
uint64_t bytes_not_in_cache = 0;
uint64_t total_bytes_read = 0;
map<uint64_t, bufferlist> stripe_map; // final buffer offset -> substring
...
for (vector<ObjectExtent>::iterator ex_it = rd->extents.begin(); ex_it != rd->extents.end(); ++ex_it) {
total_bytes_read += ex_it->length;
// get Object cache
Object *o = get_object(soid, ex_it->objectno, oset, ex_it->oloc, ex_it->truncate_size, oset->truncate_seq);
......
// map extent into bufferheads
map<loff_t, BufferHead*> hits, missing, rx, errors;
o->map_read(*ex_it, hits, missing, rx, errors);
......
if (!missing.empty() || !rx.empty()) {
// 处理 miss 和 rx 的数据
for (map<loff_t, BufferHead *>::iterator bh_it = missing.begin(); bh_it != missing.end(); ++bh_it) {
// 处理 miss 的数据
......
if (!waitfor_read.empty() || (stat_rx > 0 && rx_bytes > max_size)) {
if (success) {
......
waitfor_read.push_back(new C_RetryRead(this, rd, oset, onfinish)); // 注册一个重读回调
}
bh_remove(o, bh_it->second);
delete bh_it->second;
} else {
bh_it->second->set_nocache(nocache);
bh_read(bh_it->second, rd->fadvise_flags); // 调用 LibrbdWriteback::read() 发送读请求
if ((success && onfinish) || last != missing.end())
last = bh_it;
}
success = false;
}
......
for (map<loff_t, BufferHead *>::iterator bh_it = rx.begin(); bh_it != rx.end(); ++bh_it) {
// 处理 rx 的数据
......
if (success && onfinish) {
......
bh_it->second->waitfor_read[bh_it->first].push_back(new C_RetryRead(this, rd, oset, onfinish)); // 注册一个重读回调
}
......
success = false;
}
......
}else{
// 处理 hit 的数据
for (map<loff_t, BufferHead*>::iterator bh_it = hits.begin(); bh_it != hits.end(); ++bh_it) {
// 提升 hit 数据的 LRU 中的热度
......
BufferHead *bh = bh_it->second;
if (bh->get_nocache() && bh->is_clean())
bh_lru_rest.lru_bottouch(bh);
else
touch_bh(bh);
.....
}
......
while (1) {
// 将 hit 的数据放到 stripe_map 中,
.....
BufferHead *bh = bh_it->second;
uint64_t len = MIN(f_it->second - foff, bh->length() - bhoff);
bufferlist bit;
......
if (bh->is_zero()) {
stripe_map[f_it->first].append_zero(len);
} else {
bit.substr_of(bh->bl, opos - bh->start(), len);
stripe_map[f_it->first].claim_append(bit);
}
......
if (rd->bl && !error) {
// 将 stripe_map 数据放到 rd->bl 中
rd->bl->clear();
for (map<uint64_t, bufferlist>::iterator i = stripe_map.begin(); i != stripe_map.end(); ++i) {
pos += i->second.length();
rd->bl->claim_append(i->second);
assert(rd->bl->length() == pos);
}
} else if (!error) {
map<uint64_t, bufferlist>::reverse_iterator i = stripe_map.rbegin();
pos = i->first + i->second.length();
}
}
}
return ret;
}
AioObjectRead::send
如果没有开启缓存, 或者缓存的空间为 0 , 那么就直接调用 send 去去读取相应的数据
template<typename I>
void AioObjectRead<I>::send() {
ImageCtx *image_ctx = this->m_ictx;
......
{
RWLock::RLocker snap_locker(image_ctx->snap_lock);
// 处理镜像是快照生成的情况, 需要去父镜像读取数据. 如果该镜像是由快照生成的, 因为 Ceph 快照采用的是 COW (写时复制), 所以如果快照上的某段数据从未写过, 那么此段数据实际上会和父镜像重用, 所以需要到父快照上读取
if (image_ctx->object_map != nullptr && !image_ctx->object_map->object_may_exist(this->m_object_no)) {
image_ctx->op_work_queue->queue(util::create_context_callback <AioObjectRequest < I > > (this), -ENOENT);
return;
}
}
// 通过 libados 相关 api 读取 objetct 上相应的数据
librados::ObjectReadOperation op;
int flags = image_ctx->get_read_flags(this->m_snap_id);
if (m_sparse) {
op.sparse_read(this->m_object_off, this->m_object_len, &m_ext_map, &m_read_data, nullptr);
} else {
op.read(this->m_object_off, this->m_object_len, &m_read_data, nullptr);
}
op.set_op_flags2(m_op_flags);
librados::AioCompletion *rados_completion = util::create_rados_ack_callback(this);
int r = image_ctx->data_ctx.aio_operate(this->m_oid, rados_completion, &op, flags, nullptr);
rados_completion->release();
}