RocksDB 远程压缩并发 MANIFEST 轮转导致的打开失败:OpenAndCompact 重试机制解析
【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb
导读
本文讲解 RocksDB 远程压缩(CompactionService/DB::OpenAndCompact)在 worker 以只读方式打开 primary 目录时,因 primary 并发轮转CURRENT/MANIFEST 文件而偶发失败的问题,以及 RocksDB 为此引入的“secondary 打开有限次重试”修复方案。读完本文,你将理解该竞态产生的文件系统语义背景、Status::TryAgain/PathNotFound/NotFound三类瞬态错误的含义、OpenAndCompactOptions::max_secondary_open_retries参数的正确用法,以及这一修复与remote_compaction_manifest_floor加固机制的关系。本文以 unreleased_history/bug_fixes/remote_compaction_secondary_open_retry.md 为骨架,并结合 db/db_impl/db_impl_secondary.cc 与 include/rocksdb/options.h 等源码展开。
一、问题背景:远程压缩如何打开 primary 目录
1.1 远程压缩的基本流程
RocksDB 的CompactionService允许把压缩任务卸载到另一台主机或另一个进程执行,从而把 primary 上的后台负载转移出去。核心入口是DB::OpenAndCompact(),它“以只读方式打开数据库并执行压缩,且不修改原始 DB”,压缩结果输出到源数据库目录下的子目录而非安装回原 LSM 树。相关声明见 include/rocksdb/db.h。
从 db/db_impl/db_impl_secondary.cc 的实现看,OpenAndCompact()的执行分为几个明确步骤:
- 反序列化压缩输入:
CompactionServiceInput::Read()从input中恢复 primary 序列化好的压缩任务信息(目标 CF、输入文件等); - 加载选项:通过
LoadOptionsFromFile()读取 primary 的 OPTIONS 文件(OptionsFileName(name, compaction_input.options_file_number)),再以CompactionServiceOptionsOverride覆盖相关配置; - 确定输出目录:
RemoteCompactionJobDir()依据use_session_tmp_dir_for_remote_compaction决定输出路径是<name>/session_tmp/<output_directory>还是<name>/<output_directory>; - 过滤列族:只打开 default CF 与目标 CF,因为远程压缩只需 MANIFEST 中的 LSM 状态,无需 WAL 重放;
- 以 secondary 方式打开 primary 目录(跳过 WAL 恢复),随后在该视图上执行压缩。
1.2 secondary 打开与 MANIFEST 跟踪
关键在第 5 步:worker 调用DBImplSecondary::OpenAsSecondaryImpl()打开primary 的实时目录(只读)。secondary 依赖ReactiveVersionSet跟踪 primary 的 MANIFEST 演进,其中MaybeSwitchManifest()(见 db/version_set.cc)负责在发现CURRENT指向新 MANIFEST 时切换文件句柄。
二、竞态根源:无 read-after-unlink 语义的文件系统
2.1 primary 如何轮转 MANIFEST
每次 MANIFEST 轮转,primary 都会:
- 写出新的 MANIFEST 文件;
- 把新写入的
CURRENT文件rename 覆盖到旧的CURRENT之上,完成指针切换; - 视清理策略删除旧 MANIFEST。
对运行中的 secondary 而言,它可能正处于“读CURRENT→ 打开对应 MANIFEST”的窗口期内,于是出现两种经典竞态:
- 读取
CURRENT时拿到了旧值,随后旧 MANIFEST 被 primary 删除,再打开文件即失败; - 读取
CURRENT后 primary 立刻切换,旧 MANIFEST 被删除,打开时同样失败。
2.2 POSIX 与远程/对象存储的语义差异
在 POSIX 文件系统上,“打开之后删除/替换”是安全的:已打开的文件描述符仍可继续读取,未打开但刚被 rename 覆盖的文件路径在删除前也有一段可用窗口,这就是 “read-after-unlink” 语义。
但远程/对象存储(remote/object stores)不具备该语义:被替换的对象会立即被报告为“不存在”。于是 secondary 打开时得到的不再是“文件不可读但进程可继续”,而是明确的错误状态:
| 状态 | 触发场景 |
|---|---|
Status::PathNotFound | 打开CURRENT或 MANIFEST 时目标对象已不存在(文件系统层的PathNotFound) |
Status::NotFound | 同样的“对象已消失”在另一条路径上的等价表现 |
Status::TryAgain | MANIFEST 层面的专门映射,见下文MaybeSwitchManifest() |
MaybeSwitchManifest()中专门做了这种映射:当FileExists返回IsNotFound,或打开 MANIFEST 返回IsPathNotFound时,统一构造Status::TryAgain("The primary may have switched to a new MANIFEST and deleted the old one.")(见 db/version_set.cc),把“primary 可能刚切换 MANIFEST 并删除了旧文件”这一瞬态竞态显式暴露出来。
2.3 为什么这是瞬态而不是损坏
关键在于:这只是读取时机撞上了轮转窗口,并不是数据库损坏。下一次尝试读取时,CURRENT已指向新 MANIFEST,打开即可成功。因此正确做法不是报错,而是在有限次数内重试。
三、修复方案:max_secondary_open_retries有限重试
3.1 参数定义
修复为OpenAndCompactOptions新增了max_secondary_open_retries字段,声明位于 include/rocksdb/options.h:
struct OpenAndCompactOptions { // Allows cancellation of an in-progress compaction. std::atomic<bool>* canceled = nullptr; // Maximum number of times to retry opening the source DB as a secondary // after the initial attempt fails because CURRENT or MANIFEST was replaced // concurrently. A value of zero disables retries. // // Default: 2 uint32_t max_secondary_open_retries = 2; // ... allow_resumption 等其余字段 };参数语义:
- 默认值为 2,即初始尝试之外最多再重试 2 次,共最多 3 次尝试;
- 设为 0 表示完全禁用重试,保持旧行为(一次失败即返回);
- 仅对瞬态的打开失败生效,见下方状态过滤逻辑。
3.2 重试逻辑的源码实现
重试循环位于 db/db_impl/db_impl_secondary.cc:
for (uint32_t retry_count = 0;; ++retry_count) { s = DBImplSecondary::OpenAsSecondaryImpl( db_options, name, output_path, column_families, &handles, &db, /*recover_wal=*/false, /*trust_manifest_recovery=*/floor_provided); if (s.ok() || !(s.IsTryAgain() || s.IsPathNotFound() || s.IsNotFound()) || retry_count == options.max_secondary_open_retries) { break; } ROCKS_LOG_WARN(db_options.info_log, "OpenAndCompact: secondary open of %s failed with %s " "(retry %" PRIu32 " of %" PRIu32 "); the primary may have replaced " "CURRENT/MANIFEST concurrently, retrying", name.c_str(), s.ToString().c_str(), retry_count + 1, options.max_secondary_open_retries); }逻辑要点:
- 每次循环调用
OpenAsSecondaryImpl()执行一次 secondary 打开(跳过 WAL 恢复,因为远程压缩只需要 MANIFEST 里的 LSM 状态); - 只有
Status::TryAgain、Status::PathNotFound、Status::NotFound三类瞬态状态才会触发重试;其他错误(如损坏、权限问题、配置错误)立即返回,不会被重试掩盖; - 重试次数达到
max_secondary_open_retries后停止,把最后一次的错误状态返回给调用方; - 每次重试前输出一条
ROCKS_LOG_WARN级别的日志,包含失败状态与“第几次/共几次重试”信息,便于在线上定位此类竞态发生的频率。
注意循环条件中retry_count == options.max_secondary_open_retries时也会 break,配合循环后对s.ok()的判断,保证重试次数严格有界。
3.3 配套计时统计
打开过程整体耗时被记录到OPEN_AND_COMPACT_DB_OPEN_MICROS直方图(db/db_impl/db_impl_secondary.cc),对应 include/rocksdb/statistics.h 中“Time spent opening the secondary DB insideDB::OpenAndCompact()”的说明。运维者可通过该指标观察重试对打开耗时的实际影响。
四、不重试的代价:primary 后台错误
4.1 失败如何传导回 primary
该修复之所以重要,是因为对不回退到本地压缩的CompactionService实现而言,worker 侧的任何失败都会成为primary 侧的后台错误(background error)。也就是说,一次纯粹由文件系统时序造成的瞬态失败,可能被放大为 primary 上的持续错误状态,进而触发后续的错误处理路径(如 stop writes)。
4.2 回退语义由集成方决定
CompactionService::Start()的返回值CompactionServiceJobStatus控制失败后的行为。测试实现给出了两种典型选择(见 db/compaction/compaction_service_test.cc):
- 返回
kUseLocal:任务回退到 primary 本地执行,是“有本地兜底”的集成方式; - 返回
kFailure:不本地兜底,失败直接暴露为后台错误,这是压力测试工具DbStressCompactionService的刻意选择。
对于后一种集成方式,max_secondary_open_retries的重试就是避免无谓后台错误的关键防线。
五、与 MANIFEST floor 加固的关系
5.1 两个不同层面的保护
本修复与既有的remote_compaction_manifest_floor机制(include/rocksdb/options.h)互为补充,解决的问题层面不同:
| 机制 | 解决的问题 | 失败处理 |
|---|---|---|
remote_compaction_manifest_floor(默认 true) | primary 调度压缩时,把当时的 MANIFEST 位置(文件号与大小)随请求下发;worker 若发现自己基于更旧的MANIFEST 视图(如最终一致文件系统返回了过期CURRENT、或 MANIFEST 被截断)重建压缩,则拒绝执行 | 回退到本地压缩,避免基于错误的 LSM 形状产出错误结果 |
max_secondary_open_retries(默认 2) | 打开瞬间撞上CURRENT/MANIFEST 替换的瞬态竞态 | 有限次重试后成功继续;重试耗尽才返回错误 |
floor 参数还隐含开启了 worker 侧的 “trust the MANIFEST” 恢复模式(floor_provided传给OpenAsSecondaryImpl的trust_manifest_recovery,见 db/db_impl/db_impl_secondary.cc),避免因某个并非压缩输入文件的文件瞬时不可用而失败整个任务。
5.2 本修复的定位
本文描述的重试逻辑处理的是floor 机制之前的打开阶段:先要成功打开并建立 secondary 视图(依赖ReactiveVersionSet的MaybeSwitchManifest),之后才谈得上 floor 检查。因此两者是同一链路中不同节点的加固:重试消化“打开时撞上轮转”的瞬时窗口,floor 拒绝“基于过期视图压缩”的正确性风险。
六、集成与验证方式
6.1 如何设置该参数
worker 侧调用DB::OpenAndCompact()时传入自定义的OpenAndCompactOptions:
OpenAndCompactOptions options; options.max_secondary_open_retries = 2; // 默认值,可显式指定 // options.canceled = &canceled_flag; // 可选:支持任务取消 DB::OpenAndCompact(options, db_path, scheduled_job_id, compaction_input, &result, options_override);若希望关闭重试以复现/调试竞态,可显式设为 0。
6.2 测试覆盖
测试侧已在 db/compaction/compaction_service_test.cc 中覆盖该参数:测试压缩服务把max_secondary_open_retries_传入OpenAndCompactOptions(见 db/compaction/compaction_service_test.cc),并提供接口调整重试次数(max_secondary_open_retries_字段定义见同文件 db/compaction/compaction_service_test.cc,默认值与OpenAndCompactOptions一致)。通过构造 concurrent CURRENT/MANIFEST 替换的时序(利用TEST_SYNC_POINT,如DBImplSecondary::OpenAndCompact::BeforeLoadingOptions:0/1、AfterOpenAsSecondary:0等同步点),可验证重试后成功与重试耗尽后返回错误的两种路径。
相关测试入口还包括 db/db_secondary_test.cc 与 db/compaction/compaction_job_test.cc(均引用了OpenAndCompact),可用于对照 secondary 打开与压缩任务行为。
七、小结与运维建议
- 根因:
OpenAndCompact()以只读方式打开 primary 实时目录时,primary 通过 rename 覆盖CURRENT轮转 MANIFEST;在无 read-after-unlink 语义的远程/对象存储上,被替换对象立即报告为不存在,表现为PathNotFound/NotFound/TryAgain。 - 修复:
OpenAndCompactOptions::max_secondary_open_retries(默认 2)对这三类瞬态状态做有界重试,避免并发轮转导致压缩失败进而演变为 primary 后台错误。 - 运维建议:线上若使用对象存储/远程文件系统承载 RocksDB 数据目录并启用
CompactionService,遇到TryAgain/PathNotFound类的偶发打开失败属正常竞态,可关注OPEN_AND_COMPACT_DB_OPEN_MICROS指标与 INFO/WARN 日志中的 “retry N of M” 信息确认重试生效;如竞态频率过高,可评估适当增大max_secondary_open_retries,但需权衡每次重试的开销。
【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考