快照碎片整理
功能文档来自 apache/ozone#10131(HDDS-15113)。
概述
Ozone 快照是以活跃 OM 数据库的 RocksDB 检查点(checkpoint)形式创建的。创建新快照的开销很小,因为其 SST 文件是指向活跃数据库 SST 文件的硬链接。随着时间推移,活跃数据库的压缩(compaction)会重写 SST 文件。较旧的快照目录会继续固定其原始的 SST 文件,而较新的快照则固定同一批元数据的新版本。当存在大量长期存在的快照且元数据变更频繁时,快照检查点目录下的磁盘占用会大致随快照数量增长,而不是随存活的唯一键数量增长。
快照碎片整理(snapshot defragmentation)会将每个快照重写为一个版本化的检查点,其中只包含该快照所需的数据。它利用同一存储桶路径链中的上一个快照,以及当前快照发生变化的 SST/键范围,因此最新的已整理副本无需保留每个历史 SST 文件的完整独立副本。
快照碎片整理在设计阶段曾被称为快照压缩(snapshot compaction)。快照碎片整理并不等同于 RocksDB 对快照数据库的自动压缩。快照数据库的自动压缩仍保持禁用状态,因为快照差异(diff)路径依赖于稳定的 SST 元数据。
当前实现
实现围绕以下这些类展开:
SnapshotDefragService:后台及按需服务,负责重写快照检查点目录。OmSnapshotLocalData与OmSnapshotLocalDataYaml:以 YAML 旁挂文件形式持久化的本地(每个 OM 一份)元数据。OmSnapshotLocalDataManager:加载 YAML 文件,维护(snapshotId, version)节点的内存依赖图,解析上一个快照版本,并清除孤立的版本元数据。CompositeDeltaDiffComputer、RDBDifferComputer和FullDiffComputer:计算两个快照之间可能包含差异的 SST 文件。SstFileSetReader和TableMergeIterator:以有序流的形式从增量 SST 文件中读取候选键,并比较当前与上一个快照的表,而无需为每个候选键单独执行点查询。OmSnapshotManager:打开当前快照版本,并在版本切换后删除旧的检查点目录。
碎片整理服务是每个 OM 本地的。重写出的检查点目录和 YAML 文件不属于通过 Ratis 复制的状态。在高可用(HA)部署中,每个 OM 都有自己本地的快照数据库目录,必须对自己的副本执行碎片整理。管理命令可以指定任意 OM 节点。
磁盘布局
活跃的 OM 数据库位于 ozone.om.db.dirs 所指定的 OM 元数据目录下。如果未设置该属性,OM 会回退使用 ozone.metadata.dirs。
对于 OM 元数据目录 <om-meta-dir>,快照检查点目录位于:
<om-meta-dir>/db.snapshots/checkpointState/当前实现不会将碎片整理后的 DB 放置在单独的 checkpointStateDefragged 目录下。原始版本与整理后的版本是 checkpointState 中互为同级的目录:
<om-meta-dir>/db.snapshots/checkpointState/om.db-<snapshot_uuid>
<om-meta-dir>/db.snapshots/checkpointState/om.db-<snapshot_uuid>-<version>版本 0 是原始的、未经碎片整理的检查点,没有版本后缀。大于 0 的版本由快照碎片整理产生。通常在碎片整理清理成功之后,只保留当前版本的目录。以下路径展示了在快照的生命周期中目录名称如何变化;在正常的稳态下,它们并不会同时存在:
# Before first defrag:
/var/lib/ozone/om/db.snapshots/checkpointState/om.db-3d0a...9f62
# After first successful defrag:
/var/lib/ozone/om/db.snapshots/checkpointState/om.db-3d0a...9f62-1
# After the next successful defrag:
/var/lib/ozone/om/db.snapshots/checkpointState/om.db-3d0a...9f62-2较旧的目录可能在版本切换期间或清理被中断后短暂存在,但正常的碎片整理后续流程会删除该快照数据库对应的较旧检查点目录。
每个快照还会在版本 0 目录旁边生成一个本地 YAML sidecar 文件:
<om-meta-dir>/db.snapshots/checkpointState/om.db-3d0a...9f62.yaml临时工作在以下位置创建:
<om-meta-dir>/db.snapshots/checkpointState/tmp_defrag/
<om-meta-dir>/db.snapshots/checkpointState/tmp_defrag/differSstFiles/SnapshotDefragService 在服务启动时会删除并重新创建 tmp_defrag,并在关闭时将其删除。
当向其他 OM 提供 OM DB 检查点时,检查点代码会使用本地 YAML 元数据中的当前版本,并包含该快照 DB 目录。引导传输还会包含所需的 om.db-<snapshot_uuid>.yaml 边车文件。基于 inode 的传输路径会显式地归档检查点中存在的快照的 YAML 文件,以及它们所依赖的任何先前的本地数据节点的 YAML 文件;基于目录遍历的传输路径在只选择当前快照 DB 目录的同时包含边车文件。引导写锁会在收集文件之前等待 OM 双缓冲区完成刷新,而基于 inode 的路径在解析快照目录和 YAML 路径期间还会持有快照缓存锁和本地数据管理器锁。处于中间 SNAPSHOT_DELETED 状态的快照仍然可以被复制,因为它们仍保留在 SnapshotInfo 中;已完全清除的快照则不再存在于其中。
本地快照元数据
快照碎片整理元数据存储在 OmSnapshotLocalData YAML 中,而不是存储在 SnapshotInfo 中,也不存储在 Ratis 日志中。重要字段包括:
| 字段 | 含义 |
|---|---|
snapshotId | 快照 UUID,必须与检查点目录名一致。 |
checksum | YAML 表示形式的校验和,用于检测本地元数据是否损坏。 |
previousSnapshotId | 同一桶路径链中位于该本地数据之前的上一个快照,本地数据依据它进行解析。 |
version | 要打开的当前版本。0 表示原始检查点;> 0 表示经过碎片整理的版本。 |
needsDefrag | 显式的本地标志,用于强制服务对该快照执行碎片整理。 |
isSSTFiltered | 供旧版 SstFilteringService 路径使用的 YAML 标记。启用碎片整理时会禁用该服务。 |
versionSstFileInfos | 从快照版本到 VersionMeta 的映射,取代了此前 notDefraggedSstFileList 与 defraggedSstFileList 的拆分方式。 |
VersionMeta.previousSnapshotVersion | 当前所依赖的 previousSnapshotId 对应的版本。 |
VersionMeta.sstFiles | keyTable、directoryTable 和 fileTable 的 SST 文件元数据。每个嵌套的 SstFileInfo 包含 fileName、startKey、endKey 和 columnFamily;fileName 不带 .sst 扩展名存储。 |
dbTxSequenceNumber | 创建原始快照 YAML 时,在被跟踪的 SST 文件中观察到的最大 RocksDB 序列号,供检查点差异比较器(checkpoint differ)使用。 |
transactionInfo | 清理事务标记,用于在清理操作刷写到磁盘之后才删除本地元数据。 |
lastDefragTime | 由 YAML 类序列化,但当前的碎片整理决策依据 version、needsDefrag 和 versionSstFileInfos 作出。 |
每次创建快照时,OM 都会生成 YAML 辅助文件,并将 keyTable、directoryTable 和 fileTable 的活跃 SST 文件元数据以版本 0 记录下来。该元数据读取自新创建的快照检查点数据库,而非活跃 OM 数据库,因此检查点创建后立即进行的活跃数据库压缩不会破坏快照的本地 SST 跟踪信息。即使周期性快照碎片整理服务被禁用,这一过程也会发生。新创建的快照以 needsDefrag = true 提交。在升级/最终确定过程中,OmSnapshotLocalDataManager 还会为 SnapshotInfo 中已存在的快照补齐缺失的 YAML 文件:活跃快照会获取其被跟踪的 SST 元数据,而合成生成的 YAML 会被标记为 needsDefrag = true。当新增一个已整理的碎片版本时,当前版本号会递增,新版本的 SST 列表会从 RocksDB 中捕获,同时清除 needsDefrag 标记。
OmSnapshotLocalDataManager 在内存中维护一个本地版本依赖图。图中的每个节点是一个 (snapshotId, version) 二元组,并指向其所依赖的 (previousSnapshotId, previousSnapshotVersion)。该图在 OM 启动时根据 YAML 重建,用于:
- 拒绝删除仍被其他快照版本引用的版本;
- 在清除(purge)导致路径链变化后,解析快照的前一版本依赖;
- 识别清除后可以删除的孤立版本和孤立 YAML 文件。
服务配置
快照碎片整理默认处于禁用状态。
| 属性 | 默认值 | 含义 |
|---|---|---|
ozone.snapshot.defrag.service.interval | -1 | 后台执行间隔。取值 <= 0 时禁用该服务。 |
ozone.snapshot.defrag.limit.per.task | 1 | 单次服务运行中最多整理碎片的快照数量。 |
ozone.snapshot.defrag.service.timeout | 300s | 单次服务运行的超时时间。 |
ozone.om.snapshot.local.data.manager.service.interval | 5m | 本地 YAML/版本孤立文件清理线程的执行间隔。取值 <= 0 时禁用该清理线程。 |
该服务由 SNAPSHOT_DEFRAG OM 布局特性进行控制,同时还需要 Rocks 工具原生库;如果该库不可用,按需执行的运行会直接返回,不会对快照进行碎片整理。
如果启用了碎片整理,KeyManagerImpl 就不会启动 SstFilteringService,即使配置了 SST 过滤间隔也是如此。碎片整理在构建重写的检查点时,已经按存储桶前缀对被跟踪的快照表进行了过滤。如果禁用了碎片整理并启用了 SST 过滤,旧的 SST 过滤服务仍会从版本 0 的快照中移除无关的 SST 文件,并写入 sstFiltered 标记文件。
手动碎片整理通过以下方式提供:
ozone admin om snapshot defrag --service-id=<om-service-id> --node-id=<om-node-id>
ozone admin om snapshot defrag --service-id=<om-service-id> --node-id=<om-node-id> --no-wait该命令要求在目标 OM 上初始化碎片整理服务。HA 服务中的任意 OM 都可以运行它,因为重写后的快照数据库状态位于该 OM 本地。
碎片整理工作流程
SnapshotDefragService 按正序遍历全局快照链,且只处理活跃快照。对于每个快照,它会解析同一桶中路径上的前一个快照。增量式碎片整理基于路径链,而不仅仅是全局创建顺序。
当满足以下任一条件时,该服务会判定某个快照需要进行碎片整理:
- 本地
needsDefrag标志为 true;或 - 快照当前版本所依赖的、其解析出的前一个快照的版本,比该前一个快照的当前版本更旧。
第二个条件是当前一个快照被重写,或快照清除(purge)改变路径链之后,碎片整理得以传导的机制。
主要工作流程如下:
获取引导读锁(bootstrap read lock),并加载
SnapshotInfo以及本地 YAML。在
tmp_defrag中创建临时检查点(checkpoint)。- 如果这是路径链中的第一个快照,则对当前快照创建检查点。
- 否则,对路径上前一个快照的当前版本创建检查点。
从临时检查点中删除非增量列族(column family)。稍后会从当前快照中重新加载它们。
对于路径链中的第一个快照,对
keyTable、directoryTable和fileTable执行完整碎片整理:- 删除桶前缀之外的范围;
- 使用强制最底层(bottommost-level)压实对每个受跟踪的表进行压实,从而将范围墓碑(range tombstone)从重写后的检查点中移除。
对于后续快照,对相同的受跟踪表执行增量式碎片整理:
- 计算路径上前一个快照与当前快照之间的增量 SST 文件;
- 将增量按列族分组;
- 从增量 SST 文件中读取候选键,将其与前一个快照和当前快照的表合并,并仅将发生变化的键或墓碑写入临时 SST 文件;
- 将生成的 SST 文件摄取(ingest)到临时检查点中。
为当前快照获取
SNAPSHOT_DB_CONTENT_LOCK写锁。这是在服务重新加载非增量表并切换版本期间,防止并发更改快照内容的锁。快照读取和深度清理(deep-clean)写入使用SNAPSHOT_DB_LOCK,或在同一锁层级中以读方式获取SNAPSHOT_DB_CONTENT_LOCK。基于 DAG 的锁获取顺序允许在快照数据库锁和本地数据锁之前获取内容锁;代码路径会避免在已持有本地数据锁时再获取内容锁。将当前快照中的非增量表转储并摄取到检查点中。受跟踪的表(
keyTable、directoryTable、fileTable)会被跳过,因为它们已经被重建。关闭临时检查点元数据管理器,并将检查点目录移动到下一个版本路径:
<om-meta-dir>/db.snapshots/checkpointState/om.db-<snapshot_uuid>-<next_version>- 打开新版本,将其活跃的 SST 元数据添加到
versionSstFileInfos,更新version,并清除needsDefrag。 - 版本切换成功后,在获取该快照的数据库缓存写锁之后,删除该快照的旧检查点目录版本。例如,从版本
1切换到版本2后,当该快照数据库不再有打开的缓存句柄时,本地的om.db-<snapshot_uuid>-1会被删除。版本0通常在首次成功执行碎片整理并创建版本1后被删除;如果之前某次清理被中断后仍残留旧目录,同一条删除路径也可以将其移除。当仍有其他快照版本引用 YAML 版本元数据时,该元数据可能比目录存留更久。OmSnapshotLocalDataManager会稍后移除孤立的版本元数据。此目录删除操作特意保留在SNAPSHOT_DB_CONTENT_LOCK之下,以确保过期的缓存句柄无法在旧版本被删除时向其写入数据。 - 释放
SNAPSHOT_DB_CONTENT_LOCK。
Delta 计算
碎片整理仅使用检查点差异器(checkpoint differ)所跟踪的列族:keyTable、directoryTable 和 fileTable。
CompositeDeltaDiffComputer 首先尝试使用 RDBDifferComputer。该差异器利用本地的 versionSstFileInfos 元数据以及活跃数据库的压缩 DAG。当当前快照版本为 0 时,可以利用 DAG 路径找出自上一个快照以来发生变化的 SST 文件。对于大于 0 的版本,差异器会回退到按版本比较 SST 文件元数据,因为经过碎片整理的版本已经是重写后的快照数据库,而不是原始的活跃数据库检查点。
如果基于 DAG 的差异器无法给出完整的结果,代码会回退到 FullDiffComputer。完整差异器在 inode 元数据可用时按 inode 比较相关 SST 文件,当 inode 比较失败时则回退到比较完整的文件列表。它会识别仅存在于某一端的文件,并且只有在文件标识能够证明两者是同一个 SST 时,才会跳过公共文件。
Delta 计算器在将候选 SST 返回给碎片整理服务之前,会先将其以硬链接形式物化到 tmp_defrag/differSstFiles 目录下。这样即使原始源路径随后变为可清理状态,服务在读取时也能保持源 SST 内容的稳定。
Delta 文件标识的是候选 SST,而非最终的行级变更。碎片整理服务仍然会从这些文件中读取键,比较当前与上一个快照的表值,仅将发生变化的记录或墓碑写入新的 SST 文件,并将该文件摄入检查点。如果某个表只有一个 delta 文件,且当前快照版本已经大于 0,服务可以直接摄入该 delta 文件。
SstFileSetReader 以有序合并流的形式返回候选键,并可通过原始 SST 读取器读取墓碑。整理路径尽可能采用仅按键迭代、CodecBuffer 和直接缓冲区。由于候选键是有序的,TableMergeIterator 可以使用前向迭代器和 seek 遍历当前和先前的 RocksDB 表,而不必为每个候选键发起独立的点查询。
快照整理前后的快照对比
快照整理后,快照对比 API 和报告生成流程保持不变。SnapshotDiffManager 依然提交对比作业,通过 OmSnapshotManager 打开当前快照的 DB 版本,向 CompositeDeltaDiffComputer 请求候选 SST 文件,使用 SstFileSetReader 读取候选键,用 TableMergeIterator 比较 from/to 快照表,并构建用于生成最终对比报告的对象 ID 映射。
内部的 SST 候选路径会根据 to 快照的当前本地版本发生变化:
- 整理前,to 快照为版本
0,即最初的 OM DB 检查点。RDBDifferComputer可以请求RocksDBCheckpointDiffer遍历活动 DB 的压缩 DAG,并结合 YAML 中的dbTxSequenceNumber与版本0的 SST 元数据来识别发生变化的 SST。如果 DAG 无法给出完整答案,CompositeDeltaDiffComputer会回退到FullDiffComputer。 - 整理后,to 快照的当前版本大于
0,该版本是重写的快照 DB,而不是由常规 RocksDB 压缩产生的活动 DB 检查点。对比器通过OmSnapshotLocalDataManager解析 from 快照的依赖关系,将 YAML 中的versionSstFileInfos版本映射传入RocksDBCheckpointDiffer,并比较相关快照版本的 SST 元数据,而不是使用压缩 DAG 遍历。全量对比回退仍然可用,--forceFullDiff也仍会绕过 DAG 路径。
快照读取
快照读取通过 OmSnapshotManager 和 SnapshotCache 完成。缓存加载器从 OmSnapshotLocalDataManager 读取快照的当前版本,并打开:
<om-meta-dir>/db.snapshots/checkpointState/om.db-<snapshot_uuid>[-<version>]读取路径不会扫描磁盘上最大的目录后缀。YAML 的 current 版本才是事实依据:在 YAML 的 current 版本提交之前,移动一个新的检查点目录对读取方不可见。
在打开一个快照缓存条目之前,加载器会等待 SnapshotInfo.createTransactionInfo 中记录的快照创建事务刷新到 OM DB。这样可以防止从节点或快速读取方在相应的创建事务尚未持久化之前,就打开其检查点目录或 YAML 伴随文件已存在于内存或磁盘上的快照。
快照清理与孤儿数据回收
删除快照时,首先会将 SnapshotInfo 标记为 SNAPSHOT_DELETED。随后,SnapshotDeletingService 会提交一个内部清理请求。清理操作会更新后续快照 SnapshotInfo 中的路径/全局前置 ID,将被清理的快照从链中移除,在被清理快照的本地 YAML 中记录清理的 transactionInfo,使快照缓存条目失效,并删除被清理快照的检查点目录。
清理路径不会直接向后续快照的 YAML 中写入 needsDefrag = true。相反,当下次为碎片整理而打开该快照的本地数据时,OmSnapshotLocalDataManager 会解析更新后的 pathPreviousSnapshotId。如果该解析结果改变了依赖关系,或者所引用的前置快照版本已过期,提供器就会将该快照标记为需要碎片整理,或上报这一情况。
某个快照的旧检查点目录会在该快照成功碎片整理到更新版本之后立即删除。旧版本的元数据和 YAML 文件由 OmSnapshotLocalDataManagerService 与检查点目录分开清理,该服务是一个由 OmSnapshotLocalDataManager 拥有的单线程调度器。
启动时,本地数据管理器会加载所有 om.db-<snapshot_uuid>.yaml 文件,在内存中重建版本依赖图,并将每个已加载的快照 ID 加入孤儿检查队列。后续的提交也可能将额外的快照 ID 加入队列:
- 当某个快照增加或移除了本地版本;
- 当某个快照解析出的
previousSnapshotId在清理操作更新路径链之后发生变化; - 当清理操作在某个快照的 YAML 中记录了
transactionInfo。
每次清理遍历都会检查队列中的快照 ID。当没有其他本地版本节点依赖某个版本,且满足以下条件之一时,可以从 YAML 中移除该版本条目:
- 该版本不是
0,也不是该快照的当前版本;或 - 该快照本身已被清理。
对于活跃快照,即使版本 0 没有任何依赖者也会被保留,因为新创建或尚未解析的快照仍可能依赖原始版本。如果某个快照包含清理的 transactionInfo,但清理事务尚未刷新到 OM DB,清理线程会保留该 YAML,并将该快照重新加入队列,留待后续遍历处理。当清理事务已刷新且不再存在任何版本时,YAML 文件会被删除。
指标与日志
OmSnapshotInternalMetrics 记录自上次 OM 重启以来的碎片整理进度:碎片整理操作总数、失败总数、跳过的快照数、全量碎片整理操作数及失败数、增量碎片整理的快照数及失败数、全量碎片整理压缩的表数,以及处理的增量 delta 文件数。
OMPerformanceMetrics 以毫秒为单位记录最近一次全量碎片整理操作和最近一次增量碎片整理操作的延迟。启用 trace 级日志后,SnapshotDefragService 还会记录每个被整理快照的整理前后目录统计信息,包括文件总数、SST 文件数和目录字节占用量。
预期效果
完成一次全量处理后,每个活动快照的当前版本都是一个紧凑的、作用域为 bucket 的检查点,它复用上一个路径快照以及该快照自身的变更。这种方式减少了长快照链中重复的 SST 保留,同时仍基于普通的 RocksDB 检查点和 SST 元数据进行快照读取与 snapshot-diff 计算。
关于原始规模问题的背景以及早期设计讨论,请参阅 HDDS-13003。
评论
登录后参与评论
KnowForge