谈谈BlueFS

4 篇文章 0 订阅

前言

BlueFS具体是个什么东西呢?
在这里插入图片描述
如上图,在Ceph里,使用BlueStore作为默认的存储引擎。
作为存储引擎,它说白了就是存储文件的,但是一般情况下,文件分为原始数据与元数据。如上图,对于原始数据,BlueStore是直接操作裸盘进行存储的;对于元数据则使用RocksDB来进行保存。
单独看RocksDB,它是一个kv存储引擎,它里面存储的数据是需要进行持久化的,理论上来说吧Rocksdb的数据也直接写到裸盘上就行,但是实际上,Rocksdb本身不能直接操作裸盘,它需要一个文件系统来作为自己和裸盘的中间层,来提供文件的读写服务。
而咱们得BlueFS就是上面提到的中间层,向下把数据通过Allocator写到磁盘上,向上通过BlueRocksENV为Rocksdb提供文件的存储与读取。
至此,咱们对BlueFC就可以下一个定义了:BlueFC是一个专门为RocksDB提供文件读写支持的文件系统。
去掉各种定语,BlueFC就是一个文件系统

数据结构

咱们从下往上讲:

标识一个文件

// 物理磁盘的位移和长度,代表块设备的一个存储区域
class AllocExtent {
	public:
		uint64_t offset; // BlockDevice的物理地址
		uint32_t length; // 长度
};

class bluefs_extent_t : public AllocExtent{
	public:
	    // 属于哪个block device
	    // block device 包括以下几种:
	    /*
	      static constexpr unsigned BDEV_WAL = 0;
		  static constexpr unsigned BDEV_DB = 1;
		  static constexpr unsigned BDEV_SLOW = 2;
		  static constexpr unsigned BDEV_NEWWAL = 3;
		  static constexpr unsigned BDEV_NEWDB = 4;
		 */
		uint8_t bdev; 
};

// 文件的inode
truct bluefs_fnode_t {
	uint64_t ino; // inode编号
	uint64_t size; // 文件大小
	utime_t mtime; // 修改时间
	uint8_t prefer_bdev; // 优先使用哪个block device
	// 注意这是一个vector! 一块数据在磁盘上,可能因为碎片的问题
	// 分布在多个并不相连的区域上
	mempool::bluefs::vector<bluefs_extent_t> extents; // 文件对应的磁盘空间
	uint64_t allocated; // 文件实际占用的空间大小,extents的length之和。应该是小于等于size
};

文件系统的全局记录

上面的代码说明了一个文件在BlueFC里面的标识,那具体作为一个文件系统,还得知道他自己管理的系统下,都有哪些文件,哪些文件夹嘛,这部分代码如下:

class BlueFS {
	public:
		// 文件系统支持不同种类的块设备
		static constexpr unsigned MAX_BDEV = 3;
		static constexpr unsigned BDEV_WAL = 0;
		static constexpr unsigned BDEV_DB = 1;
		static constexpr unsigned BDEV_SLOW = 2;

		enum {
			WRITER_UNKNOWN,
			WRITER_WAL, // RocksDB的log文件
			WRITER_SST, // RocksDB的sst文件
		};

		// 文件
		struct File : public RefCountedObject {
			bluefs_fnode_t fnode; // 文件inode
			int refs; // 引用计数
			uint64_t dirty_seq; // dirty序列号
			bool locked;
			bool deleted;
			boost::intrusive::list_member_hook<> dirty_item;

			// 读写计数
			std::atomic_int num_readers, num_writers;
			std::atomic_int num_reading;
		};

		// 目录
		struct Dir : public RefCountedObject {
			mempool::bluefs::map<string,FileRef> file_map; // 目录包含的文件
		};

		// 文件系统的内存映像
		// dir_map的可以就是文件夹路径 DirRef里面放着一个file_map
		mempool::bluefs::map<string, DirRef> dir_map; // 所有的目录
		// file_map 的key就是文件的fnode
		mempool::bluefs::unordered_map<uint64_t,FileRef> file_map; // 所有的文件

		map<uint64_t, dirty_file_list_t> dirty_files; // 脏文件,根据序列号排列

		// 文件系统超级块和日志
		......

		// 结构体FileWriter/FileReader/FileLock,用来对一个文件进行读写和加锁
		......

		vector<BlockDevice*> bdev; // BlueFS能够使用的所有BlockDevice,包括wal/db/slow
		vector<IOContext*> ioc; // bdev对应的IOContext
		vector<interval_set<uint64_t> > block_all;  // bdev对应的磁盘空间
		vector<Allocator*> alloc; // bdev对应的allocator
		......
};

事务记录

这里关于数据结构还需要一个逻辑,就是假定一个空的文件系统启动了,然后新建了3个目录,再加了5个文件。然后系统断电关闭了。那启动的时候,那之前新建的目录和文件去哪里找呢?
BlueFC针对上面的问题,使用了WAL的方式来解决。具体来说,就是把用户的每次操作都记录成日志写到磁盘上,然后每次系统启动的时候,就读一下那文件,就知道系统之前都有哪些文件了。
那具体的操作记录是什么样子的呢?

struct bluefs_transaction_t {
	typedef enum {
		OP_NONE = 0,
		OP_INIT,        ///< initial (empty) file system marker

		// 给文件分配和释放空间
		OP_ALLOC_ADD,   ///< add extent to available block storage (extent)
		OP_ALLOC_RM,    ///< remove extent from availabe block storage (extent)

		// 创建和删除目录项
		OP_DIR_LINK,    ///< (re)set a dir entry (dirname, filename, ino)
		OP_DIR_UNLINK,  ///< remove a dir entry (dirname, filename)

		// 创建和删除目录
		OP_DIR_CREATE,  ///< create a dir (dirname)
		OP_DIR_REMOVE,  ///< remove a dir (dirname)

		// 文件更新
		OP_FILE_UPDATE, ///< set/update file metadata (file)
		OP_FILE_REMOVE, ///< remove file (ino)

		// bluefs日志文件的compaction操作
		OP_JUMP,        ///< jump the seq # and offset
		OP_JUMP_SEQ,    ///< jump the seq #
	} op_t;

	uuid_d uuid;          ///< fs uuid
	uint64_t seq;         ///< sequence number
	bufferlist op_bl;     ///< encoded transaction ops
};

对了有一个问题,需要说明大家想想,如果我把所有的操作记录一直保存着,那不管系统多大,都放不下这么多日志。那怎么办呢?假定对一个文件,首先创建,然后多次append,最终删除了。最开始肯定是有多个操作记录的,那请问文件最终都已经删除了,还保留那么多操作有意义么?
答案是肯定的,保留已经删除的问题的操作记录是没有意义的。所有每当操作记录的体积大于某个阈值,系统就会进行操作记录的合并。所谓合并就是删除哪些不需要的操作记录。那什么是不需要的操作n呢,上面说的已经删除的文件之前的操作记录就是不需要保留的操作。

超级块

再往前追一步,系统重启了,磁盘的什么地方存放上面的操作日志呢?
如果熟悉文件系统的小伙伴应该知道,就是超级块。他固定就存放在BlueFC接管的第二个4K块里。其结构如下:

struct bluefs_super_t {
  uuid_d uuid; // 唯一的uuid
  uuid_d osd_uuid; // 对应的osd的uuid
  uint64_t version; // 版本
  uint32_t block_size; // 块大小

  bluefs_fnode_t log_fnode; // 记录文件系统日志的文件
};

看完之后,咱们就可以看一下整个文件系统的静态数据结构图了:
在这里插入图片描述
journal就是我们的操作流水的记录。

启动流程

刚才已经提到超级块了,那大家就算猜也能猜出来,那这个BlueStore启动到BlueFS的时候,第一步就是去读那个超级块,然后读之前用户的操作记录流水,一步步恢复全局的dir_map和file_map。
代码基本的大致调用流程是:

BlueStore::mkfs()->
BlueStore::_open_db->
BlueFS::mount()

BlueFS::mount()的具体代码如下:

int BlueFS::mount()
{
	// 读取超级块
	int r = _open_super();
	......

	// 初始化allocator为磁盘所有的空间
	_init_alloc();
	......

	// 回放文件系统日志,日志项即为上面的事务OP,针对每个事务进行回放,文件系统的dir_map/file_map就会被更新
	r = _replay(false);

	for (auto& p : file_map) {
		for (auto& q : p.second->fnode.extents) {
			alloc[q.bdev]->init_rm_free(q.offset, q.length); // 将文件已经占用的内容从allocator中删除
		}
	}
	......
}


上面的get_super_offset 和get_super_length 代码如下:
看 写死了,都是4K

  // always put the super in the second 4k block.  FIXME should this be
  // block size independent?
  unsigned get_super_offset() {
    return 4096;
  }
  unsigned get_super_length() {
    return 4096;
  }

磁盘管理

文件系统对外的功能就是文件的读写与删除。但是从内部讲,来了一段数据,系统把它应该放在磁盘的哪个位置呢?这部分逻辑说白了就是文件系统的磁盘管理功能。
从逻辑上来说,BlueStore的磁盘管理使用的是位图法。

所谓位图法,就是借助一系列的比特流来表示磁盘上一个块的占据情况,例如0表示空闲,1表示已经被占用。

从实现上来说:

空闲空间列表用BitmapFreeListManager来管理
已分配空间列表用BitmapAllocator来管理

磁盘管理这块的内容比较细碎,这就不过多涉及了,大家可以参考:
https://zhuanlan.zhihu.com/p/643938193
https://zhuanlan.zhihu.com/p/91018497

读写流程

创建文件流程

本次分析的BlueFS的源码来自Ceph 17.2.5。
我们知道BlueFS虽然是一个文件系统,但是他的用户很单一,就只有一个RocksDB。所以写流程的起点就在Rocksdb里面。
具体调用链如下:

BlueRocksEnv::NewWritableFile
BlueFS::open_for_write

我尽量不直接贴代码,就用语言描述这个流程吧。
首先看,头文件的描述,英文写的很简单,我就不翻译了。

  // Create an object that writes to a new file with the specified
  // name.  Deletes any existing file with the same name and creates a
  // new file.  On success, stores a pointer to the new file in
    // *result and returns OK.  On failure, stores nullptr in *result and
  // returns non-OK.
  //
  // The returned file will only be accessed by one thread at a time.
    rocksdb::Status NewWritableFile(
    const std::string& fname,
    std::unique_ptr<rocksdb::WritableFile>* result,
    const rocksdb::EnvOptions& options) override;

NewWritableFile里面本身很薄,就是把fname分割成目录名和文件名,再就是把open_for_write的返回结果包装成BlueRocksWritableFile。
open_for_write里面
1 判断目录和文件是否存在,然后更新file_map
2 在操作流水里记录一个OP_DIR_LINK
3 创建文件句柄

为文件写数据

BuildTable
	BlueRocksEnv::NewWritableFile
	BlueRocksWritableFile

BlockBasedTableBuilder::Add
BlockBasedTableBuilder::Flush
BlockBasedTableBuilder::WriteBlock
BlockBasedTableBuilder::WriteRawBlock
BlueRocksWritableFile::Append

其实只是把数据写到了缓存

BlueRocksWritableFile::Append->
BlueFS::append_try_flush->
FileWriter::append->
ceph::buffer::list::page_aligned_appender::append
再强调一点,BlueFS的使用者是Rocksdb。所以append的最开始也是来源于rocksdb的append。

上面的append_try_flush,如果发现内存累计的数量已经超过了512KB(bluefs_min_flush_size)就会调用_flush_F给磁盘上刷
另外
bluestore里面
env = new BlueRocksEnv(bluefs);
env里面的fs 是BlueFs
在SanitizeOptions里面把BlueRocksEnv 传给了 ImmutableDBOptions
NewWritableFile最终调用的BlueRocksEnv::NewWritableFile
最终的WritableFile是 BlueRocksWritableFile

把数据下刷到磁盘

BlueRocksWritableFile::Sync->
BlueFS::fsync->
BlueFS::_flush_F->
BlueFS::_flush_range_F->
BlueFS::_flush_data->
KernelDevice::write

特殊的在上面的_flush_range_F里面如果发现为文件分配的大家不够了,那就需要找Allocate重新从磁盘上申请空间。
这里更详细的可以看
https://blog.csdn.net/u014104588/article/details/87886764

读流程

由于BlueFS的元数据都在内存中,所以读流程很简单,从内存中获取请求数据的物理位置和物理设备进行读取即可,不存在读放大。
代码流程如下:

BlueRocksRandomAccessFile::Read->
BlueFS::read_random->
BlueFS::_read_random
BlueFS::_bdev_read_random
KernelDevice::read_random

参考资料

https://blog.wjin.org/posts/ceph-bluestore-bluefs.html
https://blog.csdn.net/u014104588/article/details/87886764
https://zhuanlan.zhihu.com/p/46362124
https://zhuanlan.zhihu.com/p/643938193

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值