Ceph BlueStore:BlockDevice代码详解。

背景

在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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值