背景
Seastore 作为 OSD 下一代存储引擎,相比 Bluestore 做了诸多改进。Bluestore 在 NVMe 上最明显的瓶颈是 kv-sync-thread,即顺序提交元数据到 RocksDB 的线程,Seastore 则完全去除了 RocksDB,并且实现了 extent 粒度的写时复制,避免了上一代 OSD 中一个 read 请求阻塞整个 pg 的情况。
Seastore 相比 Bluestore 的明显变化:
- 使用 seastar 框架。全新的,更简洁的异步编程模式,通过 RTC 模式去除传统多线程模式带来的损耗。
- 独立管理元数据,不再依赖 RocksDB。
- 读不再影响写入。
- 实现了两种类型的磁盘管理后端:Segement Backend(Append 方式) 和 RandomBlock Backend(传统方式),且 Segment Backend 支持 ZNS 和 SMR 磁盘。
实现介绍
一、如何去除的 RocksDB?
上一代的 OSD 中对 RocksDB 的需求来自两方面:一是对外提供 kv 接口存储 omap、xattr,二是内部记录一些元数据,比如每个 object 对应的物理地址。
SeaStore 仍然保留了处理 omap、xattr 的接口,但针对不同 kv 场景,将 kv 数据分为了 3 种:
- Onode
- Omap、xattr
- lba、backref
这三种数据都是通过 B+ tree 管理,但每一种数据的 B+ tree 具体实现有区别。对于 Onode 实现了一个 Staged FLTree;对于 LBA 和 Backref 这类信息则实现了一个 FixedKVBtree;而对于 omap、xattr 这种 kv 场景,则实现了一个 kv 长度可变的B+ tree。抛开具体实现,三类数据每次的操作都是从树的 root 节点开始查找,最终在叶子结点上更新数据,中间可能会有分裂合并发生。
下面针对每一种做进一步说明。
1. 关于 Onode
每个 object 对应一个 onode,每个 onode 有唯一的 key,根据 key 可寻址到 onode,一个 onode key 由 3 部分组成:
shard (ghobject.shard_id
), pool (ghobject.hobj.pool
), crush (ghobject.hobj.hash_reverse_bits
)
oid (object name), ns (obj name space)
gen, snap
这 3 部分信息都是从 ghobject 中获得,这也是 OSD层的 object 到 Seastore 层的 onode 的桥梁。
onode 中(onode value)记录了一个 object 的元信息,其中比较重要的有 3 个:
- data 的逻辑地址
- xattr root 的逻辑地址
- omap root 的逻辑地址
每次操作 object 都要先获得 onode,根据 onode 中记录的逻辑地址再去处理相关数据。onode 是由单独的树(FLTree)管理,树的根节点地址记录在 super block 中。 FLTree 名字来源从代码中没有找到,不过整个 onode 的管理本质上仍是一棵 B+ 树,比较特别的地方是FLTree 使用了 “staged” 方式管理:将 key 分层,通过类似前缀树的方式减少冗余信息。
如果不使用“staged”方式,那么每个 val 都要记录一个独立完整的 key,显然前两层会有大量冗余,毕竟 object 会很多,而 shard 、pool 这些变化并不多。下面的图片展示了对于同一个 object 的不同 snap,普通管理方式和 staged 方式区别,其中大框表示一个 extent,对应一块物理磁盘空间,所有数据顺序写入 extent,每个小框表示相应类型的数据:
2. 关于 Omap
每个 onode 有两个独立的 omap-root,分别用于记录 omap 和 xattr,均以 B+ 树方式组织。不同 object 的元数据组成相互独立的树,相比 Bluestore 每次 kv 操作都是在全局的 RocksDB 中进行,Seastore 中 kv 操作有对象级别的隔离,预期性能会更好。
目前 LeafNode 对应的 extent 长度 64KB(OMAP_LEAF_BLOCK_SIZE
),InternalNode 长度 8KB(OMAP_INNER_BLOCK_SIZE
),当新放入的 k-v 导致 node 超过这个阈值后则需要分裂、合并的一系列调整。
3. 关于 lba、backref
Seastore 提供了一个统一的逻辑地址空间,所有数据均是以 laddr 进行寻址;物理磁盘地址则抽象成 paddr。
LBA Manager 负责记录逻辑地址 laddr_t
到物理地址 paddr_t
的映射,Backref Manager 则相反。
Backref Manager 的存在是为了确认一个 paddr 是否仍在被使用,如果没在使用则可以回收对应物理空间。这主要是为了支持 Segment Backend 实现空间清理。对于 Random Block Backend 其主要用途是启动阶段遍历 Backref tree,初始化 RBM 的 allocator。
LBA 和 Backref 都是以 FixedKVNode
作为 B+ 树节点基类,他们的 InternalNode 的 value 都是 paddr_t,用于沿树逐层寻址到最终的 LeafNode。LBA 的 LeafNode 中的 value 记录的是paddr(lba_map_val_t
),作用就是记录 laddr -> paddr 这样一个 kv;而 Backref 的 LeafNode 中的 value 则是 laddr(backref_map_val_t
)。
二、Transaction 举例
事务通过 Transaction Manager 管理,其内部会调用 epm、cache、lba/backref manager 和 journal 等组件实现不同功能。下面仅以写入数据为例做简要说明。
写入数据的流程在 Transaction Manager 这一层主要是通过 TransactionManager::do_submit_transaction
实现数据的落盘。
- 写新数据。调用该接口前上层已经分配好新的 extent,此时将 extent 数据落盘。此处和 Bluestore 中的“先写数据”模式一样,都是写到新地址。
- prepare_record。此处是将本次事务相关的元数据改动打包成 Record 格式,用来后续写到 Journal 中。这里打包的数据分为两类:完整的 extent 和 delta_info。完整的 extent 来自新分配元数据 extent 场景,比如新分配的 root 节点或者 segment backend 下新分配的 lba 或 backref 的节点;delta_info 也有两种,一种来自对现有元数据节点的改动,比如步骤一中为写数据分配了新 extent,那么就需要在 LBA tree 中新增一个 k-v 记录新分配的 extent 的 laddr->paddr,这时 LBA tree 中至少有一个结点发生了变动,这个变动会在此时被记录成一个 delta 类型的信息,这就暂时避免了把 LBA 节点对应的 extent 进行重写/原地修改;另一种 delta_info则是 alloc 信息,用于记录一个 paddr 已被分配,这就避免了立即对 backref tree 进行改动,而是交由由后台程序将这个信息更新到 backref tree。omap 和 onode 的修改也是类似的处理方式,即避免马上修改存在磁盘上的整个节点对应的 extent,而是追加一个 delta 信息到 journal。
- submit_record。这一步直接调用 journal 接口,将上一步打包好的 Record 以追加方式落盘到 journal 区。
- complete_commit。比如步骤一中新分配的 extent 此时状态设置为
CLEAN
且在 Cache 中加入一个 backref entry(journal trim 时才会更新至 Backref tree),用于记录相应 paddr 的使用信息。对于步骤 2 中产生了 delta_info 的 extent,例如相关的 LBA node,则置为DIRTY
状态,等待 journal trimer 回收。
三、Extent Placement Manager
1. 管理 extent
数据分类:epm 会将相同生命周期/类型数据放在一起。通过使用 category(data_category_t
: DATA 或 METADATA),type(extent_types_t
),hint(placement_hint_t
: HOT, COLD, REWRITE),gen (rewrite_gen_t
)这四个维度做数据分类,同类数据在物理空间上尽量放在一起。不过这个目标实际主要是针对 Segment Backend,因为 Random Backend 只有 journal 和 data 两个区域,对 data 区没有做进一步细分。segment backend 因为将整个磁盘划分成了多个 segment,可以做到将不同类型数据放到不同 segment。其中 REWRITE 是因为空间清理时会 generation+1,因此归类成不同类别数据,如果启用了 cold segment,会有更多 generation。
2. background trim
backgroud trim 分两类:trim journal 和 clean space。
2.1 trim journal
journal 一直追加写,而划分给 journal 的磁盘大小是固定的,所以需要 trim。trim 包括两类:trim_dirty 和 trim_alloc。
trim_dirty 的过程就是通过rewrite_extent把 journal 中的 delta extent 信息写到 extent 中。举例来说,对于TransactionManager::do_submit_transaction
中产生的对于 LBA node 的修改,此时会发生一次完整的 extent 重写:先分配一个新的元数据 extent,将 journal 中的 delta 信息加上原 extent 中未改动的信息重新写到新的 extent,当然,这个过程又需要一次TransactionManager::do_submit_transaction
。对于 Random Block backend,除 root 类节点外,重写的 extent 都会放到 data 区,而 segment 略微不同,对于 LBA 和 Backref node,重写时依然放到 journal 类的 segment 中,这个逻辑被封装在 EPM 中。
trim_alloc 的过程就是将记录在 journal 中的delta alloc 信息即物理磁盘空间分配信息更新到 Backref tree 中。这是因为 transaction 在提交时并不会立刻把空间分配信息更新到 Backref Tree,而是先记录到 journal,由 trim_alloc 后台更新到 Backref Tree。更新的过程也是通过再提交一次 transaction 完成。
经过 trim_dirty 和 trim_alloc 后,journal 中 记录的 delta 信息便被清除了。但对于 segment backend,此时并不会回收 segment,回收放在 clean space 流程中,因为这时 journal 中可能还有 INLINE 类型的数据。
2.2 clean space
显而易见,对于 Segment backend 这种 LFS 式的空间管理,肯定需要一个 cleaner 来及时整理空间,消除空洞,此过程刚好也会处理掉journal 上的 INLINE 数据。对于 Random Block backend 则不需要这一步。
clean space 分六步:
-
get_next_reclaim_segment
:找出一个CLOSED状态的且收益最大的 segment 准备回收。 -
backref_manager.get_mappings
:根据第一步中的 segment 的(物理)地址,查询 Backref tree,得到 alive 的 extent,这些 extents 需要挪到新地址。 -
backref_manager.retrieve_backref_extents_in_range
:收集处于待回收地址上的 Backref tree node,这些 extents 也需要挪到新地址。 -
backref_manager.get_cached_backref_entries_in_range
:收集内存中记录的处于待回收地址的 extents。执行这一步是因为有些 alloc 信息位于 journal 还未更新到 Backref tree中。 -
rewrite_extent
重写步骤 2~4 中找到的有效 extent。数据写入到新地址。 -
release_segment
。将 segment 标记为空闲,后续可被重新使用。
以上的数据更新仍然会通过 submit transaction 方式进行。
四、后端磁盘管理
Seastore 中同时实现了两种磁盘管理:segment 方式(LFS 方式,只能追加写)和 Random Block 方式(可以随机写)。
由于使用了 seastar 框架,服务层面可以轻易扩展,一个 shard 即对应一个 CPU 核。但目前实现中一旦 mkfs 后 shard 数就无法再变化。
1. Segment Backend
Segment Backend 将磁盘分成多个 segment,写入数据时在 segment 上追加写入。目前有两种实现,分别支持传统 block 接口磁盘和 ZNS/SMR 磁盘。
1.1 传统磁盘
上图中 tracker 用于记录本 shard 上 segment 的状态,比如打开一个 segment 时需要将tracker 中对应数据更新,并落盘。不过该功能目前在代码中未看到实际作用。
每个 shard 管理一段区域,这段区域被分成多个 segment,segment 大小默认64MB(seastore_segment_size
)。
1.2. ZNS/SMR磁盘
这类磁盘除了不支持覆盖写外,读写接口和传统磁盘一样,但有一套额外的 ioctl 接口管理磁盘,这套管理接口以 zone 为单位,目前 ZBDSegmentManager 实现中是一个 segment 等价一个 zone,zone 大小则通过磁盘管理接口读出。
2. Random Block Backend
Random Block Backend 在命名上直接使用了NVMeBlockDevice
,可以看出 Seastore 对于 HDD 只建议使用 Segment Backend。
五、Journal
关于磁盘管理最后还有一部分是关于 journal。为了支持事务,journal 不可避免。Bluestore 中没有明显的 journal 是因为先写数据,再将元数据存到 RocksDB 中,最终是由 RocksDB 提供了对事务的保证,实际上Bluestore 的 wal 分区即为 RocksDB 的 journal。
Seastore 中的两种 Backend 各自实现了一种 Journal:CircularBoundedJourna和SegmentedJournal。SegementedJournal 写满后会分配新的 segment 继续写,通过 trim 和 clean 来回收空间,而CircularBoundedJournal则是将 journal 分区当做 ring buffer 使用。
1. SegmentedJournal
- JournalTrimmer:负责 journal 清理,由 epm 调用
- RecordSubmitter:以 Record 格式管理数据
- SegmentAllocator:一个 segment 用完后分配新的 segment
- SegmentSeqAllocator:新 segment 需要新的 seq
- SegmentGroupManager:负责除 record submit 以外的 IO
2. CircularBoundedJournal
- JournalTrimmer:负责 journal 清理,由 epm 调用
- RecordSubmitter:以 Record 格式管理数据
- CircularJournalSpace:负责除 record submit 以外的 IO,管理 write_pointer 等信息
总结
以上对 Seastore 的部分组件进行了简要介绍,从设计上可以看出确实针对上一代 Bluestore 中的问题做了针对性处理,且引入了 LSF 和 RTC 模型,但目前 Seastore 仍不支持 SPDK,这也算一个遗憾了。DPDK 的话通过 seastar 应该能直接使用,但 SPDK 功能 seastar 还未集成。根据社区开发者的说法是希望等 seastar 完成对 SPDK 的封装,而不是在 Seastore 中支持 SPDK。