使用快照进行 OM 引导
问题描述
当前 OM 的引导(bootstrapping)机制在处理带有快照的 OM RocksDB 时存在不一致的问题。引导过程没有使用任何加锁机制,在此期间活动事务仍可能修改快照 RocksDB。这可能导致引导完成后从节点 OM 上的 RocksDB 实例损坏。为了解决这一问题,引导过程必须在一致的系统状态上运行。
Jira 工单:HDDS-12090
快照背景知识
快照操作
当对 Ozone 存储桶执行快照操作时,会发生以下步骤:
- 为活动的
om.db创建一个 RocksDB 检查点(checkpoint)。 - 从活动对象存储(AOS)RocksDB 的
deletedKeyTable和deletedDirTable中移除已删除的条目。这样做只是为了防止在未先检查该键在快照链中对应快照里是否存在的情况下,直接清除这些数据块。 - 在 AOS RocksDB 的
snapshotInfoTable中新增一条记录。
当前的引导模型
当前的模型是由从节点 OM 向主节点 OM 发起 HTTP 请求,由主节点 OM 提供其状态的一致视图。在引入存储桶快照之前,该流程仅依赖 AOS RocksDB 检查点。然而,在引入快照之后,需要处理多个 RocksDB 实例(AOS RocksDB 加上各个快照 RocksDB),从而使流程变得复杂。
工作流程
从节点发起:
- 发送一个排除列表,其中包含在之前的批次中已经复制过的文件。
主节点的操作:
创建一个 AOS RocksDB 检查点。
执行目录遍历,涵盖:
- AOS RocksDB 检查点目录。
- 各个快照 RocksDB 目录。
- 备份 SST 文件目录(compaction 备份目录)。
确定下一批次中需要复制的唯一文件。
分批传输文件,并在从节点侧按需重建硬链接。
当前模型存在的问题
- 引导期间的活动事务可能会修改快照 RocksDB,从而导致数据不一致。
- 当双缓冲区(double-buffer)刷新或其他与快照相关的操作正在进行时,可能会发生部分数据复制。
- 快照数据量通常很大(往往达到 GB 级别),需要分多批次传输,增加了数据损坏的风险。
拟议的修复方案
对快照缓存加锁
Snapshot Cache 是负责维护与快照对应的所有 RocksDB 句柄的类。当系统中没有任何线程引用某个 RocksDB 时,快照缓存会不时关闭对应的 RocksDB 句柄。因此,对快照的任何操作都会经过快照缓存,从而增加该快照的引用计数。为该快照缓存实现一把锁,将阻止任何新线程从快照缓存请求快照 RocksDB 句柄。因此,持有该锁期间执行的任何操作都将获得整个快照的一致视图。唯一的缺点是它会阻塞双缓冲线程,因此在该线程下执行的任何操作都必须是轻量级的,以避免长时间运行。(附注:Sumit 实现的优化 Gatekeeping 模型以及从 OM 中移除双缓冲的方案,将只会阻塞快照操作,这应该是可以接受的,因为这些操作仅由后台线程触发。)
通过上述锁的实现,我们有了获取整个 OM 一致快照的方法。接下来让我们深入了解整体引导流程的各种方法。
方法 1(将文件分批打包到多个 tarball 中)
该方法在现有模型的基础上引入了大小阈值,以便更高效地管理锁和数据传输。
工作流程
Follower 发起请求:
- 发送已复制文件的排除列表(通过
inodeId标识)。
- 发送已复制文件的排除列表(通过
Leader 遍历目录:
- 遍历 AOS RocksDB、快照 RocksDB 和备份 SST 目录,以确定需要传输的文件。
- 与排除列表进行比对,避免重复传输。
如果待复制文件的总大小超过
ozone.om.ratis.snapshot.lock.max.total.size.threshold,则文件将直接以 tarball 形式通过流发送,其中文件名为该文件的 inodeId。如果待复制文件的总大小小于或等于
ozone.om.ratis.snapshot.lock.max.total.size.threshold,则在等待快照缓存完全清空(不应有任何快照 RocksDB 处于打开状态)后获取快照缓存锁。在锁内将执行以下操作:- 对 AOS RocksDB 创建检查点(checkpoint)。
- 对 AOS 检查点 RocksDB 目录 + 所有快照 RocksDB 目录 + 备份 SST 文件目录(compaction 日志目录)执行完整的目录遍历,以确定所有需要复制的文件,排除列表中已存在的文件将被跳过。
- 这些文件将被添加到 tarball 中,文件名同样为该文件的 inodeId。
在遍历文件的过程中,会记录每个文件的路径及其对应的 inodeId。当处理最后一批时,该映射也会作为文本文件写入最终的 tarball,以便在 follower 节点上重建所有硬链接。
缺点
这种方法唯一的缺点是,我们可能会在网络上发送更多的数据,因为通过网络发送的某些 sst 文件可能已经由于在活动对象存储上并发运行的压缩而被替换。但与此同时,由于整个引导操作预计在几分钟内完成,因此额外的数据量将非常少——假设我们最多写入 30 MB 数据,假设有 30000 个键在 2 分钟内写入,每个键大约 1 KB。
方法 1.1
这种方法在方法 1 的基础上构建,除了在锁管理下引入大小阈值之外,我们仅依赖快照目录下已更改的文件数量作为阈值。
工作流程
Follower 发起请求:
- 发送之前已复制文件(通过
inodeId标识)的排除列表。
- 发送之前已复制文件(通过
Leader 目录遍历:
- 遍历 AOS RocksDB、快照 RocksDB 和备份 SST 目录,以确定要传输的文件。
- 与排除列表进行比较,避免重复传输。
如果要复制的总大小或快照 RocksDB 目录下要复制的文件总数分别超过
ozone.om.ratis.snapshot.max.total.sst.size,则文件将直接通过流以 tarball 形式发送,其中文件名即为该文件的 inodeId。如果快照 RocksDB 目录下要复制的文件总大小小于或等于
ozone.om.ratis.snapshot.max.total.sst.size,则在等待快照缓存完全清空(不应有任何快照 RocksDB 处于打开状态)后获取快照缓存锁。在锁内将执行以下操作:- 对 AOS RocksDB 执行 checkpoint。
- 对所有快照 RocksDB 目录进行完整的目录遍历,以确定所有需要复制的文件,排除列表中已存在的文件将被跳过。
- 将这些文件的硬链接添加到 Leader 上的 tmp 目录。
- 退出锁。
- 退出锁后,需要将 tmp 目录、AOS RocksDB checkpoint 目录和压缩备份目录下的所有文件写入 tarball。在迭代文件的同时,会记录每个文件的路径及其对应的 inodeId。由于这是最后一批数据,该映射也会作为文本文件写入最终的 tarball 中,以便在 follower 节点上重新创建所有硬链接。
缺点
该方法的缺点与方法 1 相同,但这里我们通过在锁内执行非常轻量级的操作来优化锁的持有时间。因此这是一种更优的方法,因为它最大限度地减少了其他线程的锁等待时间。
方法 2(在锁内创建单个 tarball)
方法 2 提议在磁盘上创建一个单独的 tarball 文件,并通过 follower 的多个 HTTP 批量请求将 tarball 的分块流式传输。
以下是创建 tarball 的流程:
在等待快照缓存完全清空(不应有任何快照 RocksDB 处于打开状态)后,获取快照缓存锁。在锁下将执行以下操作:
- 对 AOS RocksDB 执行检查点(checkpoint)。
- 对 AOS RocksDB 目录 + 所有快照 RocksDB 目录 + 备份 SST 文件目录(compaction 日志目录)进行完整的目录遍历,以确定创建单个 tarball 所需复制的所有文件。
该 tarball 应按批次以分页方式流式传输给从节点。
缺点
这种方法的缺点是,如果需要打包的数据量很大,双缓冲区(double buffer)将被阻塞很长时间。如果 OM 各数据库的快照总大小为 1 TB,假设 tarball 写入目标为 NVMe 设备,且 NVMe 驱动器的写入吞吐量约为 5 GB/s,则 tarball 写入总共可能需要 1024/5 秒 = 3 分钟。将双缓冲区线程阻塞 3 分钟似乎不是一个好主意,但同时这种情况只会发生在快照操作正在进行中或已在双缓冲区队列中时。
方案三(在锁下对快照 RocksDB 创建检查点)
方案三提出在快照缓存锁下,为系统中的每一个快照 RocksDB 以及 AOS RocksDB 分别创建 RocksDB 检查点。在锁外,可以像方案二那样创建单个 tarball 文件,也可以像方案一那样将文件按批次以多个 tarball 文件的形式流式传输给从节点。
以下是创建 tarball 的流程:
在等待快照缓存完全清空(不应有任何快照 RocksDB 处于打开状态)后,获取快照缓存锁。在锁下将执行以下操作:
- 对 AOS RocksDB 执行检查点(checkpoint)。
- 通过遍历 AOS 检查点 RocksDB 的 snapshotInfo 表,对系统中的每一个快照执行 RocksDB 检查点。
现在需要将检查点目录中的文件以方案一或方案二的方式流式传输给从节点。
缺点
这种方法的缺点是会使文件系统中的硬链接数量翻倍,在引导过程中可能会对性能产生潜在影响,尤其考虑到在某些系统中,文件和硬链接的总数可达 500 万个。
建议
方法一是最优化的方案,它通过引入另一个阈值配置来跟踪锁内 IO 操作的数量,从而将其最小化,以此平衡持锁时间。此外,采用该方案所需的代码改动也最少,因为它与当前方案的差异并不大。方法二看起来虽然更简单,但这意味着要对现有的整个引导(bootstrap)逻辑进行彻底改造;而且该方案可能会增加锁内的总耗时,一旦出现这种情况,就会导致双缓冲(double buffer)线程被阻塞较长时间,而这正是方法一试图避免的。
最终采用的方案是方法 1.1。
评论
登录后参与评论
KnowForge