Skip to content

🐯 [并发] ScanRange 读路径未与删文件互斥 · engine.go #297

Description

@github-actions

来自 #296 的架构级评审建议。不阻塞合入,仅供参考是否有更好的架构解法。

⚠️ [重要 · 并发] ScanRange 读路径未与删文件互斥 storage/engine.go:520

问题根因:ReclaimUpTo 与 CompactSSTable 通过 fileMu 互斥删文件,但 Engine.ScanRange 的读路径在 newSSTableIteratorFrom(meta.Filepath, start) 时并未持 fileMu。删除动作(ReclaimUpTo)可能在 ScanRange 已经取得 meta 后、尚未 open 文件前,把文件 Unlink 掉(POSIX 下已打开 fd 仍可读,但这里是在 open 之前 被删)。若删除发生在 open 之前,newSSTableIteratorFrom 将拿到一个已不存在的路径,返回错误后被 ScanRange 跳过——这本身不算数据损坏,但引入了:扫描这一批会丢掉该文件的数据。对投递而言,丢弃刚被回收的文件的旧快照数据尚可接受(游标已过);但若删除发生在 compaction 与扫描交错时,可能出现扫描读到「半回收」状态的一致性弱化。真正的风险点是:ScanRange 用的 meta 快照与删文件之间没有同步,删文件只与 compaction 互斥、未与读路径互斥。当前 meta 快照的 copy-on-write 机制保证元数据层面的一致性,但文件实体与读迭代器之间没有建立锁关系。

为什么低级解法不够:给 ScanRange 也套 fileMu 会把这个高频读路径与删除/compaction 全串行化,违背了「读路径无锁」的既有架构——低修(加同一把锁)以热路径吞吐为代价换取一致性,方向不对。

架构级方案:明确所有权分层:删除文件(mutation)与扫描(reader)之间应通过「元数据快照即判定」的不变式而非互斥锁来协调。具体做法:在 ReclaimUpTo 删文件之前先发布 metas 快照(已剔除被删文件);ScanRange 通过 atomic 快照拿到的是删除后的元数据视图,就不会再指向已删除文件。这样删除与扫描之间不存在数据竞争,靠 copy-on-write 发布顺序天然保证「scan 见到的 metas 不含已被删除的文件」——无需在热读路径加锁。若担心删除发生在快照发布与 open 之间,可将 open 推迟到快照取自的同一临界区内完成,或用 refcount 而非互斥锁(open 时计数、close 时递减,删除需等 refcount 归零)。

代价/收益:收益:热读路径保持无锁,删除与扫描在元数据层面天然有序,消除竞争又不动读吞吐。代价:需要把「删文件」拆成「发布新快照 → 等活跃 reader 释放 → unlink」两阶段,或引入 per-file refcount,实现略复杂;但这是把当下依赖 fileMu 才能保障的对读的安全,固化进所有权模型。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions