Back to Rustfs

Heal 并发安全说明(对象级 healing 标记对标审计结论)

docs/operations/heal-concurrency-safety-notes-zh.md

1.0.0-rc.38.9 KB
Original Source

Heal 并发安全说明(对象级 healing 标记对标审计结论)

对应 backlog rustfs/backlog#1874(父 #1862,HS-12)。本文回答一个问题:MinIO 在 heal 期间对对象打 x-minio-healing:true 元数据标记以防"heal 提交与并发删除/版本清理互毁" (cmd/xl-storage.go RenameData 的 healing 分支),RustFS 是否需要同款防御。

结论:不需要。 RustFS 不存在 MinIO 用 healing 标记防御的那类竞争:所有会触达同一 (bucket, object) 提交面的路径都在同一把对象级 namespace 写锁上互斥,且 heal 的锁 guard 覆盖 rename 提交全程;MinIO 需要标记的根因(RenameData 提交内部与版本清理逻辑 交错)在 RustFS 的提交模型中不存在。RustFS 已有一个瞬态 healing 旗标用于另一目的 (见下文 §2),并有并发不变量回归测试锁定本结论(§5)。

1. 两个防御模型的对照

MinIO:heal 时对对象写 x-minio-healing:true(持久元数据标记),后续任何 RenameData 提交看到该标记就跳过版本清理/legacy purge 逻辑——防御发生在锁外,靠元数据让路。

RustFS:三层防御,全部不依赖持久对象标记:

  1. 锁内互斥:heal 与一切前台/后台写路径的提交点在同一把 (bucket, object) ns 写锁 上串行(分布式部署为 quorum 锁 RPC,单机为进程内锁管理器;锁粒度是对象级,version 恒为 None)。
  2. 提交模型隔离:rename_data 提交内没有会与 heal 交错的版本清理逻辑;被替换旧版本 的 data_dir 物理删除被移出提交临界区(commit tail),且只删已被新提交替换的 unshared 目录。
  3. 瞬态 healing 旗标FileInfo::set_healing(crates/filemeta/src/fileinfo.rs)在 heal 提交的内存 FileInfo 上打 "healing" 内部键,rename_data 据此允许先清空 stale 目标 data_dir 再 rename——解决 heal 复用 data_dir 做 in-place 修复时 rename(2) 无法 替换非空目录的文件系统语义冲突(EEXIST/ENOTEMPTY)。该键是瞬态的,不落盘 (is_skip_meta_key),与 MinIO 的持久标记目的不同。非 heal 提交撞上非空目标 data_dir 会显式失败,有测试锁定两个方向的行为。

2. 交点矩阵

中心路径:heal_object_with_explicit_version_regen(crates/ecstore/src/set_disk/ops/heal.rs, 下称 heal.rs)在入口取 (bucket, object) ns 写锁,guard 绑定到函数作用域末尾,覆盖 quorum 元数据读取 → EC 重建 → 逐盘 rename 提交 → tmp 清理 → HEAL_RENAME_INCOMPLETE 部分提交返回 → 孤儿 data_dir 回收的全过程。并发侧逐交点判定:

#并发路径并发侧锁判定关键证据
1PUT 对象提交put_object_commit 对象写锁,rename_data 在锁内同锁串行ops/object.rs 提交锁段 + rename 调用点
2PUT 旧 data_dir tail 清理drop 对象锁后的 commit_rename_data_dir,无锁无锁并发,语义安全(见 §3.1)object.rs drop 后 tail 段;io_primitives.rs
3DELETE 单对象/版本delete_object 对象写锁,delete_version 在锁内同锁串行object.rs delete_object 锁段
4DELETE 批量批量逐对象写锁(dist 走批量锁 RPC)同锁串行object.rs delete_objects 锁段
5CompleteMultipart对象写锁 + upload 路径锁双锁,rename 在锁内同锁串行ops/multipart.rs 提交锁段
6CompleteMultipart tail 清理drop 对象锁后的旧 data_dir 删除无锁并发,语义安全(见 §3.1)multipart.rs drop 后 tail 段
7AbortMultipart仅 multipart bucket 的 upload 路径锁锁 key 不相交,但资源不相交(abort 不触对象 data_dir/xl.meta)→ 无实际交点multipart.rs abort 锁段
8ILM expiry(含 DeleteAllVersions)DeleteAllVersions 走 delete_prefix_object=true → 仍取对象锁;FreeVersionTask 显式取锁;noncurrent 批量走批量锁同锁串行bucket_lifecycle_ops.rs 消费端链路
9纯 prefix 删除(绕锁能力面)delete_prefix-only 不取子对象锁无锁并发,但生产调用方为零(见 §3.2)object.rs delete_object 锁条件
10孤儿 data_dir 回收 reclaim_orphan_data_dirs函数本体无锁;唯一生产调用方在 heal 锁内heal 流程内=锁内串行heal.rs 收尾调用;io_primitives.rs
11旧清理 receipt 对账 reconcile_old_data_cleanup_receipts函数本体无锁;调用点在 heal 锁内 + epoch fence 防误删锁内串行object.rs 对账函数
12replication数据面为远端 HTTP 写(不落本地盘);本地元数据回写走对象锁同锁串行 / 无交点replication_resyncer.rs 链路
13data_movement / rebalance / decommission 源清理显式取对象锁 + 版本未变复核 + guard 复用(no_lock 只是复用已持锁)同锁串行data_movement/mod.rs 源清理
14copy_object目标对象锁 / 走 put 链锁同锁串行object.rs copy_object 锁段
15另一 heal 任务(跨 HealType/force_start)dedup key 跨类型不相交 + force_start 跳过去重 → 任务级可并发最终在 ns 写锁上串行heal/manager.rs dedup key 构成
16admin no_lock=true heal客户端可控绕锁无锁并发,明示运维选项(见 §3.3)admin/handlers/heal.rs 透传
17stale multipart 清理multipart bucket 的 upload 路径锁资源不相交 → 无交点bucket_lifecycle_ops.rs 清理链路

3. 残留窗口定性

3.1 PUT/CompleteMultipart commit tail(交点 2/6)

写路径提交成功、释放对象锁之后,才 best-effort 删除被替换的旧 data_dir(注释明示有意 不阻塞下一操作)。该删除与并发 heal 对同一旧 data_dir 的读取/重建存在竞态窗口,但语义 安全:

  • 删除目标是已被新提交替换的 unshared data_dir;heal 的 canonical 元数据来自 quorum 仲裁(ETag/mod_time),此时 quorum 已指向新版本,heal 不会把已替换版本当作 canonical 复活;
  • 竞态最坏后果 = heal 当轮对旧版本的一次 transient 失败/空转,重试轮自然收敛;清理 residue 会上报并重新入队 heal(report_old_data_dir_cleanup);
  • 换盘重建等长 heal 走 per-version 显式版本请求,quorum 元数据在锁内读取,不受 tail 影响。

3.2 纯 prefix 删除(交点 9)

delete_prefix && !delete_prefix_object 的路径不取子对象锁(对象名空间锁无法保护前缀 递归删除),与并发 heal 存在理论复活窗口(heal 在 prefix 删除进行中依据旧 quorum 元 数据重建某版本)。全仓库核对结论:该路径的生产调用方为零——所有生产 delete_prefix: true 调用点均同时设置 delete_prefix_object: true(从而取对象锁)或在测试模块内。这 是 API 能力面的暴露而非行为风险。若未来有调用方需要纯 prefix 删除,须在调用点证明与 heal/scanner 的隔离(例如 bucket 级停扫围栏)。

3.3 admin no_lock=true(交点 16)

admin heal 请求可透传客户端 nolock 参数绕过 ns 锁(与 MinIO madmin 的同名选项对齐)。 这是运维明示选项:使用即自负与并发写的竞争责任。文档化即可,不建议收紧。

4. heal 侧自身的不变量保障

  • dedup key 跨 HealType 不相交(object/metadata/mrf/ecdecode/prefix 各自键面)+ admin force_start 可跳过去重 → 同对象可能同时存在多个 heal 任务,但它们的执行体全部在 heal_object 入口的 ns 写锁上串行(生产入口均 no_lock=false);
  • read-repair 的本地 TTL 预留只去重自身来源,不拦截其他来源的 heal——同样由 ns 锁兜底;
  • healing 旗标不落盘,故不存在"标记残留导致后续提交错误让路"的反向风险。

5. 回归测试

以下两个并发不变量测试随本审计加入 crates/ecstore/src/set_disk/ops/heal.rs 测试模块:

  • heal_racing_version_delete_never_resurrects_the_deleted_version:注入 doomed 版本 shard 损坏后,版本化 DELETE 与 Deep heal 真并发(同一把锁争用),断言已删除版本不被 复活、存活版本完好;
  • heal_racing_unversioned_overwrites_preserves_the_last_commit:非版本化覆盖提交(激活 commit tail 旧 data_dir 删除)与 Deep heal 循环竞态,断言最终 current 恰为最后一次 提交(etag 级一致)。

6. 结论

MinIO 的 x-minio-healing 是锁外元数据防御,前提是其 RenameData 提交内部存在与 heal 交错的版本清理逻辑;RustFS 的提交模型把这类交错从根上消除(提交面锁内互斥 + 清理外 移到 tail + tail 只删 unshared 旧目录),因此引入持久对象级 healing 标记没有对应的竞争 可防,反而会引入 FileInfo 落盘格式变更与标记残留清理两类新成本。维持现状,本对标疑点 关闭。