Ceph BlueStore BlockDevice

January 24, 2018
Author:Eric
Source:http://blog.wjin.org/posts/ceph-bluestore-blockdevice.html
Declaration: this work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License. Creative Commons License

Introduction

Ceph新的存储引擎BlueStore在Luminous版本已经变成默认的存储引擎,这个存储引擎替换了以前的FileStore存储引擎,彻底抛弃了对文件系统的依赖,由Ceph OSD进程直接管理裸盘的存储空间,通过libaio的方式进行读写操作。实现的时候抽象出BlockDevice基类类型,统一管理各种类型的设备,如Kernel, NVME和NVRAM等,为裸盘的使用者(BlueFS/BlueStore)提供统一的操作接口。同时,为了紧跟存储技术的最新进展,将支持NVME的spdk集成进来,完全通过用户态程序操作NVME磁盘,提升iops的同时大大缩短io操作的延时。

Source Code

BlockDevice类图继承关系如下:

img

Data Structure

鉴于目前大多数部署还是使用的hdd和sata ssd,故以此为例作介绍,对应的派生类是KernelDevice,主要数据成员如下:

class KernelDevice : public BlockDevice {
	int fd_direct, fd_buffered; // 分别存放以direct和buffered两种方式打开裸设备时的fd
	uint64_t size; // 设备总的大小
	uint64_t block_size; // 块的大小
	std::string path; // 路径
	bool aio, dio;

	// libaio相关的线程
	struct AioCompletionThread : public Thread {
		KernelDevice *bdev;
		explicit AioCompletionThread(KernelDevice *b) : bdev(b) {}
		void *entry() override {
			bdev->_aio_thread();
			return NULL;
		}
	} aio_thread;

	// aio操作相关的队列和回调函数
	aio_queue_t aio_queue;
	aio_callback_t aio_callback;
	void *aio_callback_priv;
	bool aio_stop;
};

Init Device

BlueFS用户态文件系统会使用BlockDevice存放文件系统的数据,BlueStore也会使用BlockDevice存放object相关的数据,创建BlockDevice的时候,通过工厂函数create,根据不同的设备类型,创建不同的设备,并提供callback方法及其参数:

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
	}
	......
}

创建设备后,接下来就是打开设备并对设备的基础参数进行初始化:

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();
	......
}

此时device已经ready,可以进行读写操作。需要说明的是,设备的空间怎么管理,是由设备的使用方,比如BlueFS和BlueStore完成,设备只提供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);
}

Config

除了debug相关参数外,主要配置参数与libaio相关,目前看来,基本不需要做调整:

bdev_aio # 默认为true。不能修改,现在只支持aio方式操作磁盘
bdev_aio_poll_ms # libaio API io_getevents的超时时间,默认为250
bdev_aio_max_queue_depth # libaio API io_setup的最大队列深度, 默认为1024
bdev_aio_reap_max # libaio API io_getevents每次请求返回的最大条目数
bdev_block_size # 磁盘块大小,默认4096字节

# nvme相关参数
bdev_nvme_unbind_from_kernel
bdev_nvme_retry_count

Summary