BlueStore 架构及原理分析
Ceph 底层存储引擎经过了数次变迁,目前最常用的是 BlueStore,在 Jewel 版本中引入,用来取代 FileStore。与 FileStore 相比,Bluesore 越过本地文件系统,直接操控裸盘设备,使得 I/O 路径大大缩短,提高了数据读写效率。并且,BlueStore 在设计之初就是针对固态存储,对目前主力的 SATA SSD 有着更好的支持(相比 FileStore),同时也支持 Nvme SSD 超高速固态。在数据的处理上,BlueStore 选择把元数据和对象数据分开存储,使用高速设备来保存元数据,能够起到性能优化作用。
1. BlueStore 架构总览
图1.1 ceph_bluestore 架构图
- BlockDevice:物理块设备,使用 Libaio 操作裸设备,AsyncIO。
- RocksDB:存储 WAL 、对象元数据、对象扩展属性 Omap、磁盘分配器元数据。
- BlueRocksEnv:抛弃了传统文件系统,封装 RocksDB 文件操作的接口。
- BlueFS:小型的 Append 文件系统,实现了 RocksDB::Env 接口,给 RocksDB 用。
- Allocator:磁盘分配器,负责高效的分配磁盘空间。
根据上图,可以很直观的了解到:BlueStore 把数据分成两条路径。一条是 data 直接通过 Allocator(磁盘空间分配器)分配磁盘空间,然后写入 BlockDevice。另一条是 metadata 先写入 RocksDB(内存数据库),通过 BlueFs(BlueStore 专用文件系统)来管理 RocksDB 数据,经过 Allocator 分配磁盘空间后落入 BlockDevice。
先树立第一层的概念:BlueStore 把元数据和对象数据分开写,对象数据直接写入硬盘,而元数据则先写入超级高速的内存数据库,后续再写入稳定的硬盘设备,这个写入过程由 BlueFS 来控制。
为什么这样设计呢?背后的原因令人暖心。
这里直接引入《Ceph 设计原理与实现》一文中给出的观点(concept)。
在所有的存储系统中,读操作一般都是同步的(前端发出读指令,后端必须返回待读取的数据后,才算读成功)。写操作则可以是异步的(前端发出写指令,后端先接收数据后,直接返回成功写,后续再慢慢把数据写到磁盘中),一般为了性能考虑,所有写操作都会先写内存缓存Page-Cache便返回客户端成功,然后由文件系统批量刷新。但是内存是易失性存储介质,掉电后数据便会丢失,所以为了数据可靠性,我们不能这么做。
通常,将数据先写入性能更好的非易失性存储介质(SSD、NVME等)充当的中间设备,然后再将数据写入内存缓存,便可以直接返回客户端成功,等到数据写入到普通磁盘的时候再释放中间设备对应的空间。
传统文件系统中将数据先写入高速盘,再同步到低速盘的过程叫做“双写”,高速盘被称为日志盘,低速盘被成为数据盘。每个写操作实际都要写两遍,这大大影响了效率,同时浪费了存储空间。
BlueStore 针对上述这一问题重新设计了自身的日志系统,尽可能避免双写,但同时也要保证数据的一致性。其使用增量日志,即针对大范围的覆盖写,只在前后非磁盘块大小对齐的部分使用日志,其他部分不需要改写的(RMW),则使用重定向写(不写入日志)。
书中介绍了块、COW、RWM 等内容,这里也直接拿来主义。
BlockSize:磁盘IO操作的最小单元(原子操作)。HDD为512B,SSD为4K。即读写的数据就算少于 BlockSize,磁盘I/O的大小也是 BlockSize,是原子操作,要么写入成功,要么写入失败,即使掉电不会存在部分写入的情况。
RWM(Read-Modify-Write):指当覆盖写发生时,如果本次改写的内容不足一个BlockSize,那么需要先将对应的块读上来,然后再内存中将原内容和待修改内容合并Merge,最后将新的块写到原来的位置。但是RMW也带来了两个问题:一是需要额外的读开销;二是RMW不是原子操作,如果磁盘中途掉电,会有数据损坏的风险。为此我们需要引入Journal,先将待更新数据写入Journal,然后再更新数据,最后再删除Journal对应的空间。
COW(Copy-On-Write):指当覆盖写发生时,不是更新磁盘对应位置已有的内容,而是新分配一块空间,写入本次更新的内容,然后更新对应的地址指针,最后释放原有数据对应的磁盘空间。理论上COW可以解决RMW的两个问题,但是也带来了其他的问题:一是COW机制破坏了数据在磁盘分布的物理连续性。经过多次COW后,读数据的顺序读将会便会随机读。二是针对小于块大小的覆盖写采用COW会得不偿失。是因为:一是将新的内容写入新的块后,原有的块仍然保留部分有效内容,不能释放无效空间,而且再次读的时候需要将两个块读出来Merge操作,才能返回最终需要的数据,将大大影响读性能。二是存储系统一般元数据越多,功能越丰富,元数据越少,功能越简单。而且任何操作必然涉及元数据,所以元数据是系统中的热点数据。COW涉及空间重分配和地址重定向,将会引入更多的元数据,进而导致系统元数据无法全部缓存在内存里面,性能会大打折扣。
图1.2 覆盖写示意图
基于以上设计,BlueStore 的写策略综合运用直接写、COW 和 RMW 策略。
注意:
-
非覆盖写直接分配空间写入即可;
-
块大小对齐的覆盖写采用 COW 策略;小于块大小的覆盖写采用 RMW 策略。
再结合图1.1 ceph_bluestore 架构图,我们需要引入第二层观念:
-
非覆盖写直接通过 Allocater 分配空间后写入硬盘设备。
-
覆盖写分为两种:一种是可以 COW,也是直接通过 Allocater 分配空间后写入硬盘设备。另一种是需要 RMW,先把数据写入 Journal,在 BlueStore 中就是 RocksDB,再后续通过 BlueFS 控制,刷新写入硬盘设备。
在 ceph 配置中,有 bluestore_prefer_deferred_size 可以设定:当对象大小小于该值时,该对象总是使用延迟写(即先写入 Rocks DB,再落入 BlockDevice)。
2. BlockDevice
Ceph 较新版本中,把 设备 模块单独放到 blk 文件夹中。
[root@localhost ceph]# cd src/blk
[root@localhost blk]# tree
.
├── aio
│ ├── aio.cc # 封装 aio 操作
│ └── aio.h
├── BlockDevice.cc # 块设备基类
├── BlockDevice.h
├── CMakeLists.txt
├── kernel # kernel 设备,目前常用的 HDD、SATA 都是此类设备
│ ├── io_uring.cc # bluestore 或者 存在 libaio 的情况下,不适用 libio_uring
│ ├── io_uring.h
│ ├── KernelDevice.cc
│ └── KernelDevice.h
...
# 后续还有 pmem、spdk、zoned 设备,这里不再列出
2.1 aio
aio 封装了 libaio 相关操作,具体有三个结构:
- aio_t:一次 io 操作,pwrite,pread,不写到磁盘
- io_queue_t:io 提交队列,基本不使用,只用作基类
- aio_queue_t:继承自 io_queue_t,提交 io 操作,真正在此写入磁盘
简单介绍 ceph 中 libaio 使用流程:
- init():io_setup(int maxevents, io_context_t *ctxp);
- 构造 aio_t
- 调用 aiot_t->pwritev 或者 aio_t->preadv
- 把 aio_t 加入队列:std::list<aio_t>
- int submit_batch(aio_iter begin, aio_iter end, uint16_t aios_size, void *priv, int *retries)
- shutdown: io_destroy(io_context_t ctx)
相关方法有:
init():初始化
shutdown():关闭
pwritev():预备写,后续通过 submit_batch() 完成实际写入磁盘操作
preadv():预备读,后续通过 submit_batch() 完成实际写入磁盘操作
submit_batch():按批次提交 io 任务
get_next_completed():获取已经完成 IO 的 aio_t,用于 aio_thread() 回调函数
2.2 从 BlockDevice 到 KernelDevice
BlockDevice 是所有块设备的基类,定义了块设备必备的一些接口,比如 read、write等。在 src/blk/BlockDevice.h 中还定义了 IOContext,用作封装传递 io 具体操作,其中保存着每次 aio 操作的list:pending_aios 和 running_aios,前者表示等待队列,后者表示正在 io 操作的队列,同时还创建两个原子数分别用于表示这两个队列中 aio_t 的数量。
BlockDevice提供创建块设备实例的接口,通过识别 device_type 来创建不同的块设备,如 kernel device。
BlockDevice *BlockDevice::create(
CephContext* cct, const string& path, aio_callback_t cb,
void *cbpriv, aio_callback_t d_cb, void *d_cbpriv);
->
create_with_type(device_type, cct, path, cb, cbpriv, d_cb, d_cbpriv);
-> 根据类型创建 device
new KernelDevice(cct, cb, cbpriv, d_cb, d_cbpriv)
-> 创建以下线程
aio_thread:
discard_thread
-
aio_thread:作用1. 感知已经完成的 io。2. 唤醒等待的 io 或者调用回调函数。具体根据IOContext创建时是否提供了 priv 参数(BlueStore 指针)来决定使用哪种操作,如果提供了,则调用回调函数BlueStore::TransContext::aio_finish()或者BlueStore::DeferredBatch::aio_finish()(根据AioContext 类型)。
-
discard_thread:这是针对 SSD discard 操作的线程,可以通过 cat /sys/block/sda/queue/discard_granularity 命令查看设备是否支持 discard 操作。discard 在 bluestore 中 分为两步:第一步:BlkDev{fd_directs[WRITE_LIFE_NOT_SET]}.discard((int64_t) offset, (int64_t) len); 调用系统函数对指定位置做 discard。第二步:discard_callback() 回调 shared_alloc.a->release(),告诉 Allocater 需要回收的部分(标记为 free)。
KernelDevice 支持同步、异步读写操作,具体实现方式:在 open操作时,对同一个设备首先创建两种文件描述符fd_direct、fd_buffered(通过O_DIRECT,O_DIRECT不同打开权限来实现),同时告知内核fd_buffered的数据以随机形式访问。后续顺序读写操作统一用 fd_direct,随机读写使用 fd_buffered;同步读写操作调用系统函数pwrite()和pread(),异步读写操作使用aio_t::preadv()和pwritev()。open 块设备的同时也会开启 aio_thread 和 discard_thread。
以下简单介绍KernelDevice部分关键操作:
同步读写:
- read:同步读要求块对齐,调用系统函数 pread() 实现,使用fd_directed。
- read_random:分为对齐和不对齐两种情况。对齐使用fd_buffered(open()时告知内核,数据以随机形式访问)。不对齐则强制转成对齐操作,再使用同步读的方式,使用fd_directed+pread(),后续返回时再裁剪为原本不对齐的bl大小。
- write:同步写要求块对齐。同时支持使用fd_direct或者fd_buffered,每次写完后,立刻调用系统函数sync_file_range()(仅刷新offset~length的数据,不更新元数据,提高性能)。
同步写需要刷新脏页和元数据。
- flush():调用系统函数fdatasync()。
异步读写:
异步读写时一般只用 fd_direct,因为libaio要求打开方式为O_DIRECT。
aio_read:要求使用!buffered方式(使用fd_direct),否则转为同步读。调用aio_t->preadv()函数。
aio_write:要求块对齐。要求使用!buffered方式(使用fd_direct),否则转为同步写。调用aio_t->pwritev()函数。
异步读写需要通过submit()把io操作提交到磁盘。
aio_submit():调用io_queue_t::submit_batch()。
3. BlueStore
3.1 mkfs
mkfs 作用是在磁盘第一次使用 bluestore 时,写入一些用户指定的配置到磁盘第一个块——超级块(大小可配置,一般为: BDEV_LABEL_BLOCK_SIZE 4096),这样后续使用该磁盘时,可以直接读取配置项。 之所以需要固化这些配置项,是因为 bluestore 使用不同的配置项对于磁盘数据的组织形式不同,如果前后两次上电使用不同的配置项访问磁盘数据有可能导致数据发生永久损坏。对已经 mkfs 过的磁盘再次使用该函数,则会对磁盘做一次 meta 数据检查。
以下为 LABEL_BLOCK 块中存储的数据,主要有 osd id、设备大小、生日时间、设备描述以及一组元数据 map。
// label for block device
struct bluestore_bdev_label_t {
uuid_d osd_uuid; ///< osd uuid
uint64_t size; ///< device size
utime_t btime; ///< birth time
string description; ///< device description
map<string,string> meta; ///< {read,write}_meta() content from ObjectStore
...
};
mkfs() 主要是初始化 bluestore_bdev_label_t,并写入到磁盘的第一个4K块中,同时建立块设备链并预分配指定大小的空间。同时BlueStore层次的mkfs()里面调用了Bluefs的Bluefs::mkfs(),并且也固化了bluefs的信息位于磁盘的第二个4K块。
以下给出mkfs详细步骤。
- read_meta() 读取 “mkfs_done” value,验证是否曾经完成过mkfs,如果之前做过 mkfs 则做一次 fsck 然后 return。
- 验证 type 是否是 bluestore,如果 meta 中没有 tpye,说明是第一次 mkfs,需要写入 {type,bluestore}。
- 指定了 freelist_type。
- 获取路径的 fd:path_fd = TEMP_FAILURE_RETRY(::open(path.c_str(), O_DIRECTORY|O_CLOEXEC));
- 创建 fsid 文件 /var/lib/ceph/osd/ceph-0/fsid
- 文件锁 fsid: int r = ::fcntl(fsid_fd, F_SETLK, &l);
- 读取fsid文件中的 fsid 号,如果等于0,则生成 fsid
- 建立 block 和实际块设备的软连接,并截取 block_size 大小,这里调用系统函数::symlinkat()和::ftruncate()。
- 根据配置,创建 wal 和 db 的软链接,这个可以没有
- 创建设备实例,详见 3.1.1 BlueStore::_open_bdev
- 设置 min_alloc_size,默认 4K
- 验证 block_size > round_up_to(max(SUPER_RESERVED, min_alloc_size),min_alloc_size)
- 保证 min_alloc_size 是2的倍数
- 创建 Allocator,详见 Allocator::create
- 在 Allocator 中指定 预留区域 为 free 未使用,一般就是 SUPER_RESERVED 大小,默认 8K
- 打开 kvdb,_open_db()
- 向 kvdb 写入配置数据:{S, nid_max, 0},{S, blobid max, 0},{S, min_alloc_size, ${min_alloc_size}},{S, per_pool_omap, 2},{S, ondisk_format, ${ lastest_ondisk_format = 4}},{S, min_compat_ondisk_format, 3}
- 向 path/block superblock 区域写入 meta 数据:{kv_backend, bluestore_kvbackend(默认 rocksdb)}
- meta 写入 {bluefs,1|0},是否启用 bluefs,1为启用,0为不启用。
- 更新fsid
- 关闭上文打开的所有文件或设备
3.1.1 _open_bdev
开启块设备。此函数用于创建block设备实例(slow设备),并初始化某些配置。WAL和DB设备由BlueFS自行管理,不在此处开启。
BlockDevice:create()函数在2.2节中详细介绍过了,主要作用是打开块设备,获取句柄,用于后续的数据读写操作。
discard用于SSD设备的空间回收,其作用在2.2节中介绍过。
_set_max_defer_interval()设置了max_defer_interval参数,此参数的作用是在MempoolThread线程中定时提交延迟写(try->deferred_try_submit()),默认为3。
set_cache_size()从配置中获取所有cache相关的参数,包括cache总空间,onode内存使用比率、kvdb内存使用比率、hdd、ssd内存使用比率等。
3.1.2 fsck
在mkfs中,可以看到fsck操作,通常在bluestore格式化或者挂载时使用,可以通过bluestore_fsck_on_umount、bluestore_fsck_on_mkfs配置来开启或关闭。
-
_fsck()函数中首先开启了相关kvdb、block、Allocator、FreelistManager等相关设备,通过_open_db_and_around()。
-
在_upgrade_super()中,根据ondisk_format与atest_ondisk_format比较,判断当前设备的bluestore格式是否最新,若不是最新版本,则向其kvdb中添加 super 信息(super为rocksdb前缀),具体有:ondisk_format、min_alloc_size、min_min_alloc_size、min_readable_ondisk_format、min_compat_ondisk_format。向block超级块添加freelistmanager信息:bfm_blocks、bfm_size、bfm_bytes_per_block、bfm_blocks_per_key。_
-
open_collection()中从kvdb还原coll_map,collection在kvdb中使用PREFIX_COLL前缀。
-
deferred_replay()把PREFIX_DEFERRED前缀的所有deferred_transaction延迟事务重放一遍,保证所有数据真正写入block设备。
-
_fsck_on_open()是真正执行fsck操作的函数。根据注释,有以下操作:
检测共享blob是否有不可解码的keys或者records,如果有则删除该blob
修正错误引用的pextent
尝试创建丢失的共享blob
删除不可解码的 deferred_transaction
修正 freelistmanager错误标记(free or used)
更新 statfs,bluefs文件系统信息
3.2 queue_transaction
Transaction 是bluestore 中用于保证pg上的事务,是一批有顺序的操作的集合。以下简单介绍Transaction中变量的意义及用途:
Op:代表一次操作(增删改),至少有三个参数:ghobject、coll_t、op(此次操作含义,如OP_CREATE)。在调用 touch、write等函数时写入参数。
data:TransactionData,保存transaction的一些关键参数,如ops(操作数量)largest_data_len(最大数据长度)等。注:fadvise_flags作用?
coll_index:coll_t索引。这是每个transaction单独保存的一份用过或者即将使用的collection集合。通过bluestore中coll_map<coll_t, collection>可以查到对应collection。Op.oid即是此索引中的value,这样可以通过两次查询,找到oid对应的collection。
object_index:ghobject索引。Op.oid即此索引value。
coll_id和object_id:分别是两个对应索引中键值对数量。
data_bl:写入的数据。注:如何确定某个op的data在data_bl中的位置?
op_bl:Op集合。
on_applied:事务提交完成之后调用的回调函数。
on_applied_sync:同步,事务应用完成之后调用的回调函数,由bluestore线程调用。
on_applied:异步,事务完成之后调用的回调函数,在Finisher线程中调用。
这里给出osd、collection、transaction等的关系:
osd作为rados存储层面的基石,实际上可以由三种块设备组合而成(data、wal、slow)。pg则是pool用来划分对象到不同osd上的工具,一个osd上有多个pg,这些pg可以来自不同的或相同的pool。在bluestore 中,用collection来表示pg,每次在向osd做io操作时,需要提供对象的归属pg(CollectionHandle)以及至少一个Transaction,而每一个Transaction中可以有多个io操作(op_bl)。
小结:
1个osd——多个pg
1个pg——多个Transactions,1个Transaction——多个pg(每个op不同pg)
1个Transaction——多个op
1个TransContext——同一批次Transactions(后续会添加到对应collection的OpSequencer)
queue_transaction则把一组Transactions提交到bluestore的操作。
queue_transaction(CollectionHandle &ch,
vector<Transaction> &tls,
TraskedOpRef op,
ThreadPool::TPHandle *handle)
1)收集Transaction中的三种回调函数:on_applied,on_commit,on_applied_sync。
2)根据ch参数获取Collection,ch为Collection的侵入式指针。
3)获取Collection对应的OpSequencer。
4)创建TransContext,从kvdb获取KeyValueDB::Transaction,把TransContext加入到OpSequencer. q_list_t。
5)解析transaction操作,根据op类型,调用对应的函数。如OP_WRITE->_write()。这里给出查询onode的顺序:onode_map -> kvdb -> new Onode()。
6)计算每条txc(TransContext)大致io消耗,便于后续限流操作。
7)更新onode
8)对于延迟写,提交事务到kvdb中。
9)更新空间分配(freelistmanager),删除失效kv数据。
10)throttle限流
11)进入_txc_state_proc状态机。
上图流程中_txc_add_transaction()完成了transaction到txc的转化,根据提交时设定的transaction的操作,进一步调用不同的处理函数。主要分为对coll的操作和对obj的操作这两类,如:touch、remove、delete、read、write等。
以下为_txc_add_transaction中支持的操作
op操作类型(coll) | 用途 |
---|---|
OP_RMCOLL | 删除指定collection,必须保证collection为空(没有对象)。 |
OP_MKCOLL | 创建一个collection。 |
OP_SPLIT_COLLECTION2 | 分裂集合,用于增加pg数量。pg总数必须为2的倍数,用于保证分裂操作能够作用到每个pg上。 |
OP_MERGE_COLLECTION | 合并集合,用于减少pg数量。 |
OP_COLL_HINT | 给予collection一个hint。 |
op操作类型(obj) | 用途 |
OP_CREATE | 创建一个新obj(ghobj,对象)。 |
OP_TOUCH | 优先从kvdb中查询该obj,若失败则创建一个新obj。 |
OP_WRITE | 向一个obj写入数据。 |
OP_ZERO | 清空一个obj。 |
OP_TRIMCACHE | 截取obj的指定长度,删除超出长度的所有内容。 |
OP_REMOVE | 删除指定obj。 |
OP_SETATTRS | 设置attr属性,支持同时添加多条attr。 |
OP_RMATTRS | 清空指定obj所有attr属性。 |
OP_CLONE | 克隆obj。 |
OP_CLONERAN | 克隆一个obj的指定范围内容到另一个obj中。 |
OP_COLL_MOVE_RENAME | 把一个对象转移到新集合中。 |
OP_TRY_RENAME | 把一个对象转移到同一个集合的不同对象中。 |
OP_OMAP_CLEAR | 清空指定obj所有map属性。 |
OP_OMAP_SETKEYS | 设置指定obj的omap key |
OP_OMAP_RMKEYS | 删除omap 指定key属性 |
OMAP_RMKEY_RANGE | 批量删除omap |
OMAP_SETHEADER | 设置header omap 的属性 |
SET_ALLOC_HINT | Set allocation hint for an object. make 0 values(expected_object_size, expected_write_size) noops for all implementations |
3.3.1 write
write操作涉及bluestore中经典的大小写问题,这里主要介绍_write()。源码中可以看出_write()进一步调用了_do_write() ,并在此完成对象写入操作。
_do_write(TransContext *txc, CollectionRef &c, OnodeRef o, uint64_t offset, uint64_t length, bufferlist &bl, uint32_t fadvise_flags)
- 根据配置设置写操作参数:csum_order(校验的单位块大小 = 1 << csum_order)、target_blob_size、compress、buffered。这里涉及到的pool参数,是在PG::init_collection_pool_opts()函数中完成初始化的。
- 根据obj的大小、偏移量对obj进行切割,随后对min_alloc_size倍数大小的对象块使用_do_write_big()方式写,小于min_alloc_size大小的对象块调用_do_write_small()方式写。网上关于大小写策略分析很多,这里不再展开,需要注意的是实际落盘的动作并不在此发生,而是在后续的状态机中。
- 对于新blob写,需要分配磁盘空间,此步骤在_do_alloc_write中进行,并且进行了压缩、计算校验和、更新extent_map等额外操作。bluestore配置文件中定义了prefer_deferred_write参数,凡是obj.size小于此参数的,统一使用deferred_op(延迟写),目的是小对象写操作不需要等到落盘后才返回成功,这样能够快速响应前端。
- 更新对象onode信息,包括blob、shared_blob、extent_map等。
- 压缩的blob需要进行gc评估,以此来合并blob或重新分配blob空间。
3.3.2 状态机
为了保证思考的连续性,这一小节介绍的是状态机。状态机中涉及到io保序、延迟写和简单写的提交、kvdb数据的更新等操作,是控制bluestore中数据落盘的幕后黑手。上文介绍的write()函数实际不过是真正写操作的前置工作,write仅仅表示其操作含义是写或改写一个对象。后文介绍的所有对象、pg操作的真正落盘环节都是在此函数中。状态机也是一次queue_transaction的最后一步,当程序跳出状态机也就意味着这次io写操作完成。
需要注意是,状态机中用到大量异步线程,因此前端在queue_transaction时,无需等到整个状态机到达完成状态。一般直接写在STATE_PREPARE中调用bdev->aio_submit()后就返回前端,延迟写在STATE_AIO_WAIT中调用_txc_finish_io()进行保序处理(根据osr顺序依次进入状态机处理)后直接返回前端。
具体过程不一一叙述,网上参考资料较多,或者直接看下述流程图。
这里给出每个状态迁移到下一状态的延时参数:
状态 | 参数 |
---|---|
STATE_PREPARE | l_bluestore_state_prepare_lat |
STATE_AIO_WAIT | l_bluestore_state_aio_wait_lat |
STATE_IO_DONE | l_bluestore_state_io_done_lat |
STATE_KV_QUEUED | l_bluestore_state_kv_queued_lat |
STATE_KV_SUBMITTED | l_bluestore_state_kv_committing_lat |
STATE_KV_DONE | l_bluestore_state_kv_done_lat |
STATE_DEFERRED_QUEUED | l_bluestore_state_deferred_queued_lat |
STATE_DEFERRED_CLEANUP | l_bluestore_state_deferred_cleanup_lat |
STATE_FINISHING | l_bluestore_state_finishing_lat |
STATE_DONE | l_bluestore_state_done_lat |
3.3 BlueFS
BlueStore把元数据统统用RocksDB来保存,包括Collection、Onode、omap等,可以通过查看kv store prefixes了解数据类型。RocksDB不支持裸设备,需要给它提供文件系统接口,其本身提供RocksEnv来支持跨平台操作,BlueStore在BlueRocksEnv.cc中实现了RocksDB所要求的全部接口,调用BlueFS api实现功能。
BlueFS是一个简易文件系统,同时管理三种存储空间:Slow、DB、WAL,它们的含义这里不再赘述,需要注意的是:如果三者各自分配一块磁盘,WAL满了会往DB写,DB满了会往Slow写;又或者,WAL、DB可以不使用单独设备而是统一在Slow设备上,此时由BlueStore动态管理,其空间大小根据使用情况动态调整。
除了在BlusStore::mkfs时,bluefs在实际使用中通常是在bluestore挂载的时候打开的,调用流程:_mount() -> _open_db_and_around() -> _prepare_db_environment() -> _open_bluefs()。
BlueFS的mkfs()在第一次使用bluefs时执行,其中最为重要的是写入了super信息(因为其他操作都会在mount()中重做一遍,比如init_alloc等,这些操作的目的都是为了构建一个简单的环境,便于写如super信息)。
bluefs_super_t中保存了osd.fsid、bluefs.uuid、log_fnode等。其中,log_fnode是bluefs的索引日志,记录了bluefs文件系统的所有操作,通过它可以还原出bluefs的元数据:file_map和dir_map。
bluefs的文件组织形式比较简单:一级目录+二级文件的形式。其定位某个具体文件只需要通过两次查找,第一次根据dir_map找到对应目录,第二次通过该目录的file_map找到对应文件。
由于bluefs中存储的文件数量不多(主要是用于rocksdb的存储,一般也就rocksdb的日志文件,sst文件等),所以bluefs的元数据不另外存储,而是通过append日志形式记录操作,每次mount时,回放所有的事务,并根据事务内容,还原出整个bluefs元数据。同样的,其已用空间的freelist也是如此还原。
通过bluefs,我们可以看到文件系统的本质:磁盘空间分配管理+调用磁盘读写操作+所有文件在磁盘的位置。
3.4 cache
BlueStore中cache主要存放两种数据:Onode和Obj_data(用户数据),分别用两种分片类型来管理:OnodeCacheShard和BufferCacheShard。每个bluestore(objectstore)实例中包含多个Cache实例,为了使得不同PG之间的客户端请求可以并发的执行,每个OSD会设置多个PG工作队列(op_sharededwq),cache实例个数与之对应。
collection中的onode_map和cache中分别保存了两类cache实例指针。通过osd_num_cache_shards配置修改cache总数。这就意味着一个cache可能被多个pg享有,为了便于快速判断在cache中是否有某个pg的缓存数据,就需要在pg层引入新的对象,用于保存pg的cache实例以及该cache中保存的数据对象id,即Onodespace和BufferSpace。
对于Onode来说,使用LRU算法维护数据列表,cache实例为LruOnodeCacheShard,指针保存在OnodeSpace中,而OnodeSpace指针保存在Collection中的onode_map变量里。OnodeSpace中保存了指定cache中的onode_map,通过查询该map可以快速知道cache中是否有某个Onode缓存。
data用户数据的缓存有两种实现:LruBufferCacheShard和TwoBufferCacheShard,用户数据使用Buffer封装,并保存在list链表中。BufferSpace用于管理Buffer,其中保存了Buffer表,便于快速查明内存中的buffer,通过Blob->shared_blob->bc调用。
set_cache_shards()在BlueStore构造函数中调用。OSD在init的时候也会创建多个cache。osd_num_cache_shards配置参数可以设置OSD初始化时创建的cache数量,默认32个。bluestore_cache_type配置参数可以设置Buffer_Cache_Shards类型,默认2q。
set_cache_shards()中循环创建多个cache,并在构造或打开collection时被其获取cache实例,从cache_shards数组中。
具体功能代码解析,可以参考以下网址:https://blog.csdn.net/lzw06061139/article/details/105656938/
4. BlueStore中的几个重要线程
BlueStore构造函数中初始化了多个线程,如下所示。本节内容主要探究这几个线程的作用。
以下提及的所有线程都在在bluestore mount时或者fsck时开启:_kv_start()(开启finisher、kv_sync_thread、kv_finalize_thread)、mempool_thread.init()(开启mempool_thread)。
BlueStore::BlueStore(CephContext *cct,
const string &path,
uint64_t _min_alloc_size)
: ObjectStore(cct, path),
throttle(cct),
finisher(cct, "commit_finisher", "cfin"),
kv_sync_thread(this),
kv_finalize_thread(this),
zoned_cleaner_thread(this),
min_alloc_size(_min_alloc_size),
min_alloc_size_order(ctz(_min_alloc_size)),
mempool_thread(this) {
_init_logger();
cct->_conf.add_observer(this);
set_cache_shards(1);
}
这里给出一张bluestore运行时线程图。
4.1 finisher
finisher线程是用来处理异步回调函数的线程。我们先从线程的entry开始,研究线程执行过程。
由上图可知,finisher线程作用就是不断的从finisher_queue中获取要执行的context,每当向finisher_queue插入对象时,就会唤醒该线程。
通过分析bluestore.cc代码,发现finisher.queue()函数在三处被调用。第一处在queue_transactions()中,用于处理on_applied回调在一批事务的所有写操作完成后。第二处在_txc_committed_kv()(状态机中调用)中,用于处于on_commit回调。第三处在_deferred_aio_finish()(状态机中调用)中,用于处理未完成的延迟写操作(C_DeferredTrySubmit())。
4.2 kv_sync_thread
kv_sync_thread做四件事:
- 处理kv_committing(kv_queue),把它的kvdb事务提交了。kvdb层面。
- 处理kv_submitting(kv_queue_unsubmitted)。kv_queue_unsubmitted是状态机中STATE_IO_DONE状态中,发现last_nid>=nid_max或者last_blobid>blob_max的异常,需要更新这两项配置的,因此该次提交不成功,所有事务移交至kv_queue_unsubmitted队列。这里就是更新配置。kvdb层面。
- 处理deferred_done(deferred_done_queue),把它的io给落盘了。bdev层面。
- 处理deferred_stable(deferred_stable_queue),把它对应kvdb中的wal写给删除了,清洗日志数据。kvdb层面。
具体流程见下图。
4.3 kv_finalize_thread
kv_finalize_thread主要是处理kv_sync线程中的队列,返回状态机。
具体流程看下图。
4.4 mempool_thread
5. ceph-objectstore-tool 功能解析
ceph-objectstore-tool是专门用来控制对象和pg的工具,接下来将以这个工具的功能为切入点深入bluestore引擎。
5.1list
ceph-objectstore-tool --data-path $PATH_TO_OSD [–pgid P G I D ] [ PG_ID] [ PGID][OBJECT_ID] --op list
此命令中包含3条子命令,分别是:列出osd所有对象、列出pg所有对象、列出指定对象的xattr信息。因为这三条命令在逻辑上是递进关系,代码上也是互相关联因此放到一起讨论。
上图中用到了blustore的三个方法:list_collections()(获取ObjectStore所有collection)、collection_list()(获取collection的指定范围[start, end)的所有ghobj)、getattr()(获取对象的xattr属性)。
list_collection()实现很简单,把coll_map中所有key值返回即可。coll_map保存了该OSD的所有{coll_t,CollectionRef}映射,在挂载mkfs存储引擎的时候从kvdb中获取,获取方式为db->get_iterator(PREFIX_COLL)->value()。
collection_list()同样通过kvdb查询db->get_iterator(PREFIX_OBJ),获取osd所有对象,再使用get_coll_range()方法计算出指定pg和范围对应的首尾ghobj对象,通过kv迭代器获取首尾ghobj对象范围的所有ghobj并加入vector。
getattr()获取的是保存在onode->onode.attrs集合。第二个onode是bluestore_onode_t,保存在kvdb中对象的元数据,在get_onode()方法中,从kvdb加载到内存中store->db->get(PREFIX_OBJ, key.c_str(), key.size(), &v)。
注意:以PREFIX_OBJ为前缀的数据中,除了onode外还有extent_shard(extent_map分片),它们通过不同的key值结尾来区分,onode的key末尾最后一个是“o”,extent_shard末尾是“x”。在BlueSotre.cc源码注释中有介绍
/*
\* object name key structure
*
\* encoded u8: shard + 2^7 (so that it sorts properly)
\* encoded u64: poolid + 2^63 (so that it sorts properly)
\* encoded u32: hash (bit reversed)
*
\* escaped string: namespace
*
\* escaped string: key or object name
\* 1 char: '<', '=', or '>'. if =, then object key == object name, and
\* we are done. otherwise, we are followed by the object name.
\* escaped string: object name (unless '=' above)
*
\* encoded u64: snap
\* encoded u64: generation
\* 'o'
*/
\#define ONODE_KEY_SUFFIX 'o'
/*
\* extent shard key
*
\* object prefix key
\* u32
\* 'x'
*/
\#define EXTENT_SHARD_KEY_SUFFIX 'x'
也可以使用kvstore工具查看,以下分别是RocksDB中onode和extent_shard的key值。
[root@localhost bin]# ./ceph-kvstore-tool bluestore-kv /var/lib/ceph/osd/ceph-admin/ dump O | awk '{print $2}'
%7f%7f%ff%ff%ff%ff%ff%ff%ff%99%1f%bdw%21osbench%21%3d%ff%ff%ff%ff%ff%ff%ff%fe%ff%ff%ff%ff%ff%ff%ff%ffo
%7f%7f%ff%ff%ff%ff%ff%ff%ff%99%1f%bdw%21osbench%21%3d%ff%ff%ff%ff%ff%ff%ff%fe%ff%ff%ff%ff%ff%ff%ff%ffo%00%00%00%00x
…
5.2 get-bytes、set-bytes
bluestore中read()函数主要分为两步,第一步_read_cache(),尝试在cache中读取obj数据;失败则需要去dev读取,调用_prepare_read_ioc() -> bdev->aio_read()方法读取数据,并且需要调用bdev->aio_submit()函数真正把请求提交到块设备。此步骤中,还会对数据进行解压、校验等操作。
write()函数封装了op的一系列参数,包括op类型、oid、偏移量、长度等,最重要的obj数据则保存在data_bl中。
set_bytes涉及到改写数据,所有非幂等的操作,都需要通过事务来完成。本文第三节中详细介绍了queue_transaction(),这里不再赘述。
5.3 list-omap、get-omap、set-omap
这些都是关于对象omap的操作。omap是对象的大属性,用来弥补xattr容量太小的缺陷,omap属性以对象的形式保存在kvdb中,可以通过。注意着三条命令都需要指明pg与oid。
list-omap与set-omap都是通过kvdb查询对应的key值,ghobj的key值通过十六进制编码的pool+oid+“.”+key(可没有)拼接而成。在kvdb中omap的前缀为O,key名称中的“.”的位置如果显示为“~”,表示该key的边界。通过这些信息可以通过db->get_iterator()函数找到指定对象的所有kv键值对。
set-omap则是构造一个op操作,通过queue_transaction()来提交到bluestore内部。
5.4 list-attrs、get-attr、set-attr
ceph中使用encode和decode来序列化和反序列化。
encode:把一个对象转成“长度+对象”的16进制字符并append到data_bl中。注意这个“长度“的字节长度是相对固定的,根据不同的对象类型,有8位、16位、32位和64位。也就是说目前最大支持的对象长度是(264 - 1)个字节,转换后(264 - 1)* 8 bit ≈ 247GB ,一个超大的数字。
decode:从data_bl中还原出一个对象,需要指定对象的原本类型,并根据data_bl中记录的数据长度来截取字符流。
6. ceph-bluestore-tool 功能解析
6.1 show-label、set-label-key、rm-label-key
label是bluestore中的block(SLOW)设备的超级块信息,可以通过show-label命令查看信息,或者通过set-label-key、rm-label-key增改信息。block的超级块信息储存在设备的第一个4kb的块中,可以通过系统读写函数来操控它。