目录
- 前言
- 数据结构
- 标识一个文件
- 文件系统的全局记录
- 事务记录
- 超级块
- 启动流程
- 磁盘管理
- 读写流程
- 创建文件流程
- 为文件写数据
- 把数据下刷到磁盘
- 读流程
- 参考资料
前言
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
读写流程
创建文件流程
本次分析的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 创建文件句柄
为文件写数据
其实只是把数据写到了缓存
BlueRocksWritableFile::Append->
BlueFS::append_try_flush->
FileWriter::append->
ceph::buffer::list::page_aligned_appender::append
再强调一点,BlueFS的使用者是Rocksdb。所以append的最开始也是来源于rocksdb的append。
把数据下刷到磁盘
BlueRocksWritableFile::Sync->
BlueFS::fsync->
BlueFS::_flush_F->
BlueFS::_flush_range_F->
BlueFS::_flush_data->
KernelDevice::write
特殊的在上面的_flush_range_F里面如果发现为文件分配的大家不够了,那就需要找Allocate重新从磁盘上申请空间。
读流程
由于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