文章目录
背景
在BlueStore存储引擎中,数据和元数据的存储不再经过操作系统层,OSD直接管理裸盘空间,通过libaio对裸盘进行读写。为了更好的兼容性,BlueStore提供了BlockDevice层作为写盘IO的调度层,也管理不同类型的盘,如HDD、Sata SSD和Nvme SSD。
本文后续基于N版本进行介绍,主要介绍HDD场景,HDD场景BlockDevice会抽象为KernelDevice类。
本文结论
- Device使用流程为
- 异步写:aio_write(异步写入队)、aio_submit(遍历队列写盘)、aio_thread(检查写盘完成)
- 同步写:write(同步写盘)
- 同步刷盘:flush(同步刷盘)
- 同步trim:discard(trim)
- 后台trim:queue_discard(trim extent入队)、discard_thread(执行队列中trim任务)
- Device设备文件主要有两种(block和block.db),需要在osd创建的时候将本地磁盘软链接到Device设备文件。
- block:用于存储数据。
- block.db:用于存储rocksdb的元数据。
- BlockDevice模块提供块设备文件的访问接口,空间的管理需要上层使用者控制。
- 异步写的过程是并发执行的,两个相互覆盖的IO,是无法保证顺序性的,需要使用者进行互斥的控制。
代码分析
数据结构
下面只解释部分关键字段
class BlockDevice {
uint64_t size; // 设备总大小
uint64_t block_size; // 块大小
}
class KernelDevice : public BlockDevice {
std::vector<int> fd_direct, fd_buffered; // 裸设备以direct、buffered两种方式打开的fd
std::string path; // 设备路径
bool aio, dio; // 是否启用Libaio
interval_set<uint64_t> discard_queued; // interval_set是offset+length, discard_queued 存放需要做Discard的Extent。
interval_set<uint64_t> discard_finishing; // discard_finishing 和 discard_queued 交换值,存放完成Discard的Extent
// Libaio线程,收割完成的事件
struct AioCompletionThread : public Thread {
KernelDevice *bdev;
explicit AioCompletionThread(KernelDevice *b) : bdev(b) {}
void *entry() override {
bdev->_aio_thread();
return NULL;
}
} aio_thread;
// Discard线程,用于SSD的Trim
struct DiscardThread : public Thread {
KernelDevice *bdev;
explicit DiscardThread(KernelDevice *b) : bdev(b) {}
void *entry() override {
bdev->_discard_thread();
return NULL;
}
} discard_thread;
// 同步IO
int read(uint64_t off, uint64_t len, bufferlist *pbl, IOContext *ioc,
bool buffered) override;
int write(uint64_t off, bufferlist &bl, bool buffered) override;
// 异步IO
int aio_read(uint64_t off, uint64_t len, bufferlist *pbl,
IOContext *ioc) override;
int aio_read(uint64_t off, uint64_t len, bufferlist *pbl,
IOContext *ioc) override;
void aio_submit(IOContext *ioc) override;
// sync数据
int flush() override;
// 对SSD指定offset、len的数据做Trim
int discard(uint64_t offset, uint64_t len) override;
};
打开device
create
BlueFS用户态文件系统会使用BlockDevice存放文件系统的数据,BlueStore也会使用BlockDevice存放object相关的数据,创建BlockDevice的时候,通过工厂函数create,根据不同的设备类型,创建不同的设备。
- 通过上层调用可以看到创建的块设备有两种,见BlueStore::_minimal_open_bluefs等函数。
- /pathname/block:用于存放数据。
- /pathname/block.db:用于存放rocksdb元数据。
- 设备初始化主要是一种内存的操作,真正的块设备是本地磁盘,存储池创建后可以使用软链接的方式将本地磁盘挂载到block或block.db文件,后续对其读写就是对盘的读写了。
BlockDevice *BlockDevice::create(CephContext* cct, const string& path,
aio_callback_t cb, void *cbpriv) {
// 通过设备的path,判断出设备的类型
string type = "kernel";
char buf[PATH_MAX + 1];
int r = ::readlink(path.c_str(), buf, sizeof(buf) - 1);
if (r >= 0) {
buf[r] = '\0';
char *bname = ::basename(buf);
if (strncmp(bname, SPDK_PREFIX, sizeof(SPDK_PREFIX)-1) == 0)
type = "ust-nvme";
}
if (type == "kernel") {
return new KernelDevice(cct, cb, cbpriv); // kernel
}
......
if (type == "ust-nvme") {
return new NVMEDevice(cct, cb, cbpriv); // nvme
}
......
}
open
创建好设备后,接下来就是打开设备并对设备的基础参数进行初始化,后续就可以进行读写了。
int KernelDevice::open(const string& p)
{
// 分别以fd_direct/fd_buffered方式打开块设备
fd_direct = ::open(path.c_str(), O_RDWR | O_DIRECT);
fd_buffered = ::open(path.c_str(), O_RDWR);
// 读取block size等参数
block_size = cct->_conf->bdev_block_size;
......
// 如果是aio,初始化aio相关参数,并启动aio线程
r = _aio_start();
......
}
异步IO处理
aio_write
设备初始化完成后,就可以调用相应接口进行IO的读写操作。KernelDevice提供同步读写接口read/write和异步读写接口aio_read/aio_write。如果是异步的,调用aio_write准备数据到buffer,后续还要调用aio_submit将请求提交,io执行完成后会由线程aio thread执行回调函数。这里以最复杂的流程aio_write为例介绍。
aio接口是通过libaio完成的,libaio怎么使用可以参考网上的文章,Ceph将其封装在类IOContext中,每个device对应一个IOContext。
struct IOContext {
private:
std::mutex lock;
std::condition_variable cond;
public:
void *priv;
std::list<aio_t> pending_aios; // 待执行的aio
std::list<aio_t> running_aios; // 正在执行的aio
// 计数
std::atomic_int num_pending = {0};
std::atomic_int num_running = {0};
};
struct aio_t {
struct iocb iocb; // libaio相关的结构体
......
void pwritev(uint64_t _offset, uint64_t len) {
offset = _offset;
length = len;
io_prep_pwritev(&iocb, fd, &iov[0], iov.size(), offset); // 准备数据
}
......
};
执行aio_write,实际上是在Device对应的IOContext结构体的成员变量pending_aios中追加了一个和libaio相关的aio_t结构.
int KernelDevice::aio_write(uint64_t off, bufferlist &bl, IOContext *ioc, bool buffered)
{
......
if (aio && dio && !buffered) {
ioc->pending_aios.push_back(aio_t(ioc, fd_direct)); // 放入IOContext的pending队列,等待执行
++ioc->num_pending;
// 将待写入的数据准备在aio的buffer中
aio_t& aio = ioc->pending_aios.back();
bl.prepare_iov(&aio.iov);
for (unsigned i=0; i<aio.iov.size(); ++i) {
aio.bl.claim_append(bl);
aio.pwritev(off, len); // 写buffer
}
}
}
aio_submit
使用方调用aio_write准备数据后,紧接着会调用aio_submit提交IO请求.
void KernelDevice::aio_submit(IOContext *ioc)
{
if (ioc->num_pending.load() == 0) {
return;
}
// 获取pending的aio
list<aio_t>::iterator e = ioc->running_aios.begin();
ioc->running_aios.splice(e, ioc->pending_aios);
......
// 批量提交aio
r = aio_queue.submit_batch(ioc->running_aios.begin(), e,
ioc->num_running.load(), priv, &retries);
}
int aio_queue_t::submit_batch(aio_iter begin, aio_iter end,
uint16_t aios_size, void *priv,
int *retries)
{
......
while (left > 0) {
int r = io_submit(ctx, left, piocb + done); // 调用libaio相关的api提交io
}
......
}
aio_thread
提交aio请求后,device的使用方就完成了,需要单独的线程来检查io的完成情况,当真正完成的时候,执行回调函数通知调用方,此线程即为设备对应的aio thread线程,线程入口如下.
void KernelDevice::_aio_thread()
{
......
while (!aio_stop) {
int r = aio_queue.get_next_completed(cct->_conf->bdev_aio_poll_ms, // 调用libaio相关的api,检查io是否完成
aio, max);
if (ioc->priv) {
if (--ioc->num_running == 0) {
aio_callback(aio_callback_priv, ioc->priv); // 执行回调
}
}
}
......
}
int aio_queue_t::get_next_completed(int timeout_ms, aio_t **paio, int max)
{
......
do {
r = io_getevents(ctx, 1, max, event, &t); // 调用libaio相关的api,获取已经完成aio请求
} while (r == -EINTR);
}
同步IO处理
write
int KernelDevice::write(uint64_t off, bufferlist &bl,
bool buffered, int write_hint)
{
......
return _sync_write(off, bl, buffered, write_hint);
}
int KernelDevice::_sync_write(uint64_t off, bufferlist &bl, bool buffered) {
uint64_t len = bl.length();
vector<iovec> iov;
bl.prepare_iov(&iov);
// 调用pwritev写入PageCache
auto r = ::pwritev(choose_fd(buffered, write_hint),
&iov[idx], iov.size() - idx, o);
if (buffered) {
// initiate IO and wait till it completes
auto r = ::sync_file_range(fd_buffereds[WRITE_LIFE_NOT_SET], off, len, SYNC_FILE_RANGE_WRITE|SYNC_FILE_RANGE_WAIT_AFTER|SYNC_FILE_RANGE_WAIT_BEFORE);
if (r < 0) {
r = -errno;
derr << __func__ << " sync_file_range error: " << cpp_strerror(r) << dendl;
return r;
}
}
io_since_flush.store(true);
}
刷盘
sync
BlueStore往往采用异步IO,同步数据到磁盘上调用flush函数。
先通过Libaio写入数据,然后在kv_sync_thread里面调用flush函数,把数据和元数据同步到磁盘上。
int KernelDevice::flush() {
// protect flush with a mutex. note that we are not really protecting
// data here. instead, we're ensuring that if any flush() caller
// sees that io_since_flush is true, they block any racing callers
// until the flush is observed. that allows racing threads to be
// calling flush while still ensuring that *any* of them that got an
// aio completion notification will not return before that aio is
// stable on disk: whichever thread sees the flag first will block
// followers until the aio is stable.
// 保证并发性
std::lock_guard<std::mutex> l(flush_mutex);
// 如果 io_since_flush 为false,则没有要sync的数据,直接退出
bool expect = true;
if (!io_since_flush.compare_exchange_strong(expect, false)) {
dout(10) << __func__ << " no-op (no ios since last flush), flag is "
<< (int)io_since_flush.load() << dendl;
return 0;
}
// 调用 fdatasync 同步数据到磁盘
......
int r = ::fdatasync(fd_direct);
......
return r;
}
数据Trim
discard
BlueStore针对SSD的优化之一就是添加了Discard操作。Discard(Trim)的主要作用是提高GC效率以及减小写入放大。
int KernelDevice::discard(uint64_t offset, uint64_t len) {
int r = 0;
if (!rotational) {
......
r = block_device_discard(fd_direct, (int64_t)offset, (int64_t)len);
}
return r;
}
int block_device_discard(int fd, int64_t offset, int64_t len)
{
uint64_t range[2] = {(uint64_t)offset, (uint64_t)len};
return ioctl(fd, BLKDISCARD, range);
}
discard_thread
KernelDevice会启动一个Discard线程,不断的从discard_queued里面取出Extent,然后做Discard。
void KernelDevice::_discard_thread() {
std::unique_lock<std::mutex> l(discard_lock);
......
while (true) {
// 如果没有需要做Discard的Extent就等待
if (discard_queued.empty()) {
if (discard_stop) break;
......
} else {
discard_finishing.swap(discard_queued);
.....
// 对需要做Discard的Extent依次调用discard函数
for (auto p = discard_finishing.begin();
p != discard_finishing.end(); ++p) {
discard(p.get_start(), p.get_len());
}
discard_callback(discard_callback_priv,
static_cast<void *>(&discard_finishing));
discard_finishing.clear();
......
}
}
}
queue_discard
由BlueFS和BlueStore在涉及到数据删除的时候调用queue_discard将需要做Discard的Extent传入discard_queued。
int KernelDevice::queue_discard(interval_set<uint64_t> &to_release) {
if (rotational) return -1;
if (to_release.empty()) return 0;
std::lock_guard<std::mutex> l(discard_lock);
// 插入需要做Discard的Extent
discard_queued.insert(to_release);
discard_cond.notify_all();
return 0;
}
参考文献
https://zhuanlan.zhihu.com/p/91020703
http://blog.wjin.org/posts/ceph-bluestore-blockdevice.html