注意

本文档适用于 Ceph 开发版本。

SeaStore

目标和基础

  • 目标是 NVMe 设备。主要不关注 pmem 或 HDD。

  • 利用 SPDK 进行用户空间驱动的 IO

  • 使用 Seastar 未来编程模型来促进运行至完成和分片内存/处理模型

  • 当与基于 Seastar 的信使结合使用 DPDK 时,允许在读写路径上实现零(或最小)数据复制

动机和背景

所有闪存设备在内部都以段的形式结构化,可以高效地写入,但必须完整擦除。NVMe 设备通常对段中哪些数据仍然是“活动”(尚未逻辑丢弃)知之甚少,导致设备内部的垃圾回收效率低下。我们可以设计一个对较低层级的垃圾回收友好的磁盘布局,并在较高层级上驱动垃圾回收。

从原则上讲,细粒度的丢弃可以向我们设备传达意图,但在实践中,丢弃在设备中实现不佳,并且中间的软件层也难以实现。

基本思想是所有数据将按顺序流出到设备上的大段。在 SSD 硬件中,段的大小可能在 100 MB 到几十 GB 的范围内。

SeaStore 的逻辑段理想情况下应与硬件段完全对齐。在实践中,确定几何形状并充分向设备提示要写入的 LBA 应与底层硬件对齐可能具有挑战性。在最坏的情况下,我们可以将逻辑段结构化为例如物理段大小的 5 倍,以便我们大约有 20% 的数据未对齐。

当我们达到某个利用率阈值时,我们将清理工作与正在进行的写入工作混合在一起,以便从先前写入的段中清除活动数据。一旦它们完全空闲,我们可以丢弃整个段,以便它被擦除并由设备回收。

关键是将一点清理工作与每个写入事务混合在一起,以避免写入延迟的峰值和差异。

数据布局基础

一个或多个核心/分片将同时读取和写入设备。每个分片将对其操作的处理的数据有自己的独立数据,并将其流到自己的打开段。支持流的设备可以相应地提示,以便来自不同分片的数据不会在底层介质上混合。

持久内存

随着上述初始顺序设计的成熟,我们将为元数据和缓存结构引入持久内存支持。

设计

该设计严重依赖于 f2fs 和 btrfs。每个反应堆管理自己的根。在重用段之前,我们将任何活动块重写到一个打开的段。

由于我们只顺序写入到打开的段,因此对于每个字节写入,我们必须“清理”现有段中的一个字节。通常,我们需要预留一部分可用容量,以确保写入放大保持可接受的低水平(例如 2 倍的 20%?-- TODO:查找先前的作品)。作为设计选择,我们希望避免后台垃圾回收方案,因为它往往使估计操作成本变得复杂,并且倾向于引入非确定性的延迟行为。因此,我们希望有一组结构,允许我们在正在进行客户端 IO 的情况下将块从现有段中重新定位。

为此,从高层次来看,我们将维护两个基本元数据树。首先,我们需要一个映射 ghobject_t->onode_t(onode_by_hobject)的树。其次,我们需要一种查找段内活动块的方法,以及一种将内部引用与物理位置解耦的方法(lba_tree)。

每个 onode 包含直接作为 xattr 的 xattr,以及 omap 和范围树的顶部(优化:我们应该能够将足够小的对象放入 onode)。

段布局

背景存储被抽象为一组段。每个段可以处于三种状态之一:空、打开、关闭。段的字节内容是一系列记录。记录由一个标题前缀(包括长度和校验和)和一系列增量或/和块组成。每个增量描述了对某些块的逻辑更改。每个包含的块是可以通过 <segment_id_t, segment_offset_t> 对齐的范围地址。事务可以通过构建一个结合增量并更新块的记录并将其写入打开段来实现。

注意,段通常很大(例如 >=256MB),因此通常不会有太多的段。

记录:[ 标题 | 增量 | 增量… | 块 | 块 … ]

请参阅 src/crimson/os/seastore/journal.h 以获取 Journal 实现。请参阅 src/crimson/os/seastore/seastore_types.h 以获取大多数 seastore 结构。

每个分片将保持打开 N 个段用于写入

  • HDD:N 可能在一个分片上是 1

  • NVMe/SSD:N 可能是每个分片 2,一个用于“日志”,一个用于完成的记录,因为它们的生命周期不同。

我认为保持打开的确切数量以及如何在这些之间划分写入将是一个调整问题 -- gc/layout 应该是灵活的。在实用情况下,目标可能是根据预期寿命划分块,以便一个段要么有长寿命块,要么有短寿命块。

背景物理层通过基于段的接口暴露。请参阅 src/crimson/os/seastore/segment_manager.h

日志和原子性

一个打开的段被指定为日志。事务由原子写入的记录表示。记录将包含作为事务一部分写入的块以及指向现有物理范围的逻辑更改的增量。事务增量始终写入日志。如果事务与写入到其他段的块相关联,则应仅在其他块持久化后写入包含增量的最终记录。崩溃恢复是通过找到包含当前日志开头的段,加载根节点,重放增量,并根据需要将块加载到缓存中来完成的。

请参阅 src/crimson/os/seastore/journal.h

块缓存

每个块处于以下两种状态之一:

  • 清洁:可能位于缓存中或不在缓存中,读取可能会导致缓存驻留或不会

  • 脏:当前记录的版本需要覆盖来自日志的增量。必须在缓存中完全存在。

定期需要修剪日志(否则,我们将不得不从那时起重放日志增量)。为此,我们需要通过重写根块和所有当前脏块来创建一个检查点。注意,我们可以相对不频繁地进行日志检查点,它们不必阻塞写入流。

注意,增量可能不是字节范围修改。考虑一个具有左侧键和右侧值的 btree 节点(提高点查询/键扫描性能的常见技巧)。在最小值处将键/值插入该节点将涉及移动大量字节,这纯粹以字节操作序列表示将很昂贵(或冗长)。因此,每个增量都指示相应范围的类型和位置。因此,每种块类型都可以相应地实现 CachedExtent::apply_delta。

请参阅 src/os/crimson/seastore/cached_extent.h。请参阅 src/os/crimson/seastore/cache.h。

GC

在重用段之前,我们必须重新定位所有活动块。因为我们只顺序写入到空的段,对于每个写入到当前打开段的字节,我们需要清理现有关闭段中的一个字节。作为设计选择,我们希望避免后台工作,因为它使估计操作成本变得复杂,并且倾向于产生非确定性的延迟峰值。因此,在正常操作中,每个 seastore 反应堆将插入足够的工作来以与传入操作相同的速率清理段。

为了使这对稀疏段来说很便宜,我们需要一种方法来正面识别已死的块。因此,对于每个写入的块,将向 lba 树添加一个条目,其中包含指向段中先前 lba 的指针。任何移动块或修改现有块引用集的事务都将包括增量/块,以更新 lba 树以更新或删除先前的块分配。因此,gc 状态只需要维护一个迭代器(某种形式)到当前正在清理的段 lba 树段链表,以及一个指向下一个要检查的记录的指针——分配树中不存在的记录可能仍然包含根(如分配树块),因此必须检查记录元数据以指示根块。

对于每个事务,我们评估当前可用空间和当前活动空间的一个启发式函数,以确定是否需要执行清理工作(可能只是一个活动/使用空间比率的范围)。

TODO:目前还没有 GC 实现

逻辑布局

使用上述块和增量语义,我们构建了两个根级别的树:

上述每个结构都由包含在增量中变化的块组成。上述树的每个节点都映射到一个块。每个块要么是物理寻址的(根块和 lba_tree 节点),要么是逻辑寻址的(其他所有内容)。物理寻址的块通过 paddr_t:<segment_id_t, segment_off_t> 元组定位,并在记录中标记为物理寻址。逻辑块通过 laddr_t 寻址,需要查找 lba_tree 进行寻址。

由于缓存/事务机制位于 lba 树的下方,我们可以通过简单地将其包含在事务中来表示 lba 树和其他结构的原子更改。

LBAManager/BtreeLBAManager

LBAManager 接口实现的职责是管理逻辑到物理的映射——请参阅 crimson/os/seastore/lba_manager.h。

BtreeLBAManager 直接在 Journal 和 SegmentManager 之上实现此接口,使用游走树的方法。

由于 SegmentManager 不允许我们预测已提交记录的位置(SMR 和 Zone 设备的属性),同一事务内创建的块引用必然是相对relative地址。BtreeLBAManager 通过一个不变性来维护,即当 !is_pending() 时,任何内存中的块副本将只包含绝对地址——on_commit 和 complete_load 根据实际块地址和 on_delta_write 根据刚提交的记录填充绝对地址。当 is_pending() 时,如果 is_initial_pending 引用内存是块相对的(因为它们将被写入原始块位置),否则是记录相对的(值将被写入增量)。

事务管理器

事务管理器负责在 Journal、SegmentManager、Cache 和 LBAManager 之上提供一个统一的接口。用户可以根据逻辑地址分配和更改范围,段清理在后台处理。

请参阅 crimson/os/seastore/transaction_manager.h

下一步

日志

  • 支持扫描段以查找物理寻址的块

  • 添加支持修剪日志和释放段

缓存

  • 支持重写脏块

    • 需要在 CachedExtent 中添加查找/更新依赖块的支持

    • 需要在 try_construct_record 中添加脏块写出到支持

LBAManager

  • 添加支持固定

  • 添加段 -> laddr 以用于 GC

  • 支持定位段中剩余使用的块

GC

  • 初始实现

  • 在 BtreeLBAManager 中支持跟踪段中使用的块

  • 识别要清理段的启发式算法

其他

  • 添加支持定期生成日志检查点

  • onode 树

  • 范围树

  • 剩余对象存储集成

对象存储的考虑

分割、合并和分片

当前对象存储要求能够在 O(1) 时间内分割集合(PG)。从 mimic 开始,我们还需要能够将两个集合合并为一个(即分割的完全相反)。

然而,我们分割成的 PG 将哈希到当前分片方案中 OSD 的不同分片中。可以想象用临时映射替换该分片方案,将较小的子 PG 指向正确的分片,因为我们通常会将该 PG 迁移到另一个 OSD,但这在合并情况下不会帮助我们,因为在合并的情况下,组成部分可能从不同的分片开始,最终需要在同一集合中处理(并通过单个事务操作)。

This suggests that we likely need a way for data written via one shard to “switch ownership” and later be read and managed by a different shard.

由 Ceph 基金会带给您

Ceph 文档是一个社区资源,由非盈利的 Ceph 基金会资助和托管Ceph Foundation. 如果您想支持这一点和我们的其他工作,请考虑加入现在加入.