Skip to content

feat: add distributed owner SSD backing tier for KV cache storage#37

Merged
ActivePeter merged 26 commits into
mainfrom
SSD
Jul 21, 2026
Merged

feat: add distributed owner SSD backing tier for KV cache storage#37
ActivePeter merged 26 commits into
mainfrom
SSD

Conversation

@zTz01

@zTz01 zTz01 commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

背景

当 KV 工作集超过 DRAM 容量时,内存副本驱逐会增加缓存 miss。本 PR 将 owner 本地 SSD 接入现有 key-version 副本体
系,用作 DRAM 的运行期回填层,从而扩大可命中的工作集。

单机最高Mooncake 6x 吞吐:让 Fluxon KV 支持 DRAM + SSD 多级缓存 https://zhuanlan.zhihu.com/p/2060716360108397913

主要改动

  • 保持 putgetdelete 公共 API 不变,get 仍返回 MemHolder
  • put 在内存副本发布后返回,SSD 持久化在后台异步完成,不增加同步写入路径的等待时间。
  • get 优先读取内存;没有可读内存副本时才从 SSD 回填。
    • 本地回填可直接读取到 requester target,不满足对齐条件时使用 scratch buffer。
    • 远端回填采用分块读取和主动 push,使 SSD IO 与网络传输能够重叠。
  • master 维护 key-version 级逻辑路由和 allocation 生命周期;owner 管理 SSD shard、ring、文件 offset、驱逐和
    IO。
  • SSD 引擎使用 O_DIRECTio_uring、512-byte 对齐、per-device 队列和有界背压。
  • 分别上报 DRAM 与 SSD 的容量和使用量,并在 CLI/UI 中展示。
  • 增加 CPU、GPU、Mooncake 和 Foyer 对比 benchmark,以及对应结果分析和设计文档。

配置

通过 fluxonkv_spec.large_limit_size 启用 SSD,其配置项与 large_file_paths 一一对应。未配置或设置为 null
时保持纯内存模式。

容量支持整数 bytes 和 "1.5GB" 等可读格式。

兼容性与边界

  • SSD 默认关闭,无公共 API breaking change。
  • SSD 是运行期缓存,不提供 WAL、checkpoint 或冷启动恢复;owner 重启后会重建空 shard 和 ring。
  • 单个 value 位于一块 device,不支持跨盘条带化。
  • master 只保存逻辑副本信息,不保存 SSD 物理 offset。
  • SSD commit 是异步 late commit,失败不会回滚已经发布的内存副本。

验证

  • 补充了配置解析、ring 状态、direct/scratch IO、多设备选择、分块回填、背压、驱逐和生命周期相关测试。
  • 当前 package-wheel、文档镜像、双节点 CI 和大规模 MQ GitHub checks 均通过。
  • 在文档所述单机 H100 SSD-pressure 测试中,Fluxon 开启 SSD 后达到 100% 命中率。
  • c16 下,4/8/16 MiB hit-payload 吞吐分别达到两种 Mooncake 拓扑中较快者的 3.83×、5.61× 和 6.73×。

上述数据是限定环境下的端到端逻辑吞吐,不代表裸 SSD 带宽。

当前分支包含的其他改动

该分支同时包含:

  • FluxonFS VideoReader 与视频 dataloader benchmark。
  • MQ/etcd client pool、重试和 consumer rebind 改进。
  • CI artifact 清理及测试栈稳定性改进。
  • 开发、Code Review、事件订阅和数据结构设计规范文档。

修复与加固(按提交顺序)

  1. SSD IO 正确性与测试覆盖(b477e02

    • 小 value 或未对齐 target 不再被提前拒绝,改由 scratch buffer 完成回填。
    • io_uring 补充 EINTR 重试、EBUSY completion drain,以及致命错误下的 outstanding IO 收敛。
    • 写入前按 value 大小过滤可承载的 SSD device,避免 round-robin 选中容量不足的 shard。
    • 修复 VideoReader 中 FFmpeg AVIOContext 的 64 KiB buffer 泄漏。
    • 将 SSD P2P round 和 KV Rust unit tests 纳入 CI。
  2. KV 语义、etcd 与 MQ 稳定性(585da30b43ce92

    • 为 create-only put 增加原子 admission,区分 KeyAlreadyExistsKeyBeingWritten,修复并发创建竞态。
    • 修复 lease 重新绑定后的版本隔离,避免旧 lease 过期误删新版本;同时收紧 lease expiry 测试时序。
    • 将 etcd 连接收敛到进程级、generation-aware client pool,避免重复建连以及旧连接失败误清理新连接。
    • 为 MQ metadata 和 producer offset 写入增加有界、shutdown-aware 的瞬时 etcd 错误重试。
    • 修复 consumer commit sequencer 失败后 waiter 无法唤醒、继续接收新任务的问题。
    • 加强 producer 持续运行、consumer 多轮退出与重新绑定的 MPMC 回归测试。
    • 清理成功 CI case 的大体积 artifact,并将环境相关主机名、IP 和路径替换为通用示例。
  3. SSD shutdown、背压与远端回填生命周期(8dd0b02cc13dcf

    • 引入模块级 shutdown gate,按“停止接收新任务 → 等待在途操作 → 关闭队列 → join worker → 释放 io_uring”完
      成退出。
    • SSD 驱逐通知在队列满时传播背压,不再静默丢弃。
    • 使用无碰撞的实例目录组件,并阻止 ... 等路径逃逸。
    • 为远端 SSD stage 增加 begin/finalize 生命周期,使 requester revoke 等待 source 停止访问 staging/target。
    • 由 SSD source 负责远端回填的 GetDone/GetRevoke terminal report,并支持丢失响应后的幂等结果重放。
    • benchmark 密码改为从权限受限的文件读取,不再通过命令行参数暴露。
  4. 错误收敛与热路径扩展性(de5dbc2

    • 一个 SSD chunk 失败或 ready channel 关闭后,仍等待所有已提交 read 完成。
    • 一个网络 transfer 失败后,保留首个错误并等待已启动的 sibling transfer 全部结束。
    • writer blocked 时停止继续抽空 bounded channel,防止本地 pending 队列无界增长。
    • 使用增量 used_bytes 和 busy-offset 索引,移除写入与指标采集热路径中的 SSD entry 全表扫描。
    • SSD replica commit 使用 transport 认证的 caller identity,不再信任请求体中的 owner ID。
    • 按 member 建立 holding 索引,修复 MemberLeft 与 holding 提交竞态,并避免成员离开时全表扫描。
  5. 代际、地址范围与并发边界(7039918

    • ring 顺序项记录不可变的 (begin, key) generation,避免同 key 的失败 reservation 与后续重试混淆。
    • 为 SSD read 增加 operation 和 byte 双重 admission budget,防止 reader 绕过 bounded queue。
    • 对本地 raw address 增加完整范围与读写权限校验,并在 IO 期间阻止对应 segment slot 被移除。
    • 将 inflight GET 改为 member-generation scoped admission,原子处理成员离开、重连和 terminal claim。
    • 为 source terminal RPC 和 SSD commit 增加有界 deadline及 shutdown 中断处理。
    • 将 holding 重构为 member/holder 两级索引,进一步消除清理路径上的全表扫描

English

Background

When the KV working set exceeds available DRAM, memory replica eviction increases cache misses. This PR
integrates owner-local SSDs into the existing key-version replica model as a runtime backing tier for DRAM,
expanding the effective cacheable working set.

Key changes

  • Preserve the existing put, get, and delete APIs; get continues to return MemHolder.
  • Return from put after the memory replica is published, while persisting to SSD asynchronously.
  • Keep reads memory-first and use SSD only when no readable memory replica is available.
    • Local refills can read directly into the requester target, with a scratch-buffer fallback for unaligned
      IO.
    • Remote refills use chunked reads and source-side pushes to overlap SSD IO with network transfer.
  • Keep key-version routing and allocation lifetimes on the master, while owners manage SSD shards, rings, file
    offsets, eviction, and IO.
  • Implement the SSD engine with O_DIRECT, io_uring, 512-byte alignment, per-device queues, and bounded
    backpressure.
  • Report DRAM and SSD capacity and usage separately in metrics and the CLI/UI.
  • Add CPU, GPU, Mooncake, and Foyer benchmark coverage, together with result analysis and design
    documentation.

Configuration

SSD storage is enabled through fluxonkv_spec.large_limit_size, mapped one-to-one to large_file_paths.

Omitting the field or setting it to null keeps the existing memory-only behavior. Capacity values accept raw
bytes and human-readable strings such as "1.5GB".

Compatibility and limitations

  • Disabled by default, with no public API breaking changes.
  • The SSD tier is a runtime cache and does not provide WAL, checkpoint, or cold-start recovery. Owners rebuild
    empty shards and rings after restart.
  • Each value is stored on one device; cross-device striping is not supported.
  • The master stores logical replica metadata only, not physical SSD offsets.
  • SSD persistence is an asynchronous late commit. A persistence failure does not roll back an already
    published memory replica.

Validation

  • Added coverage for configuration parsing, ring states, direct/scratch IO, multi-device selection, chunked
    refills, backpressure, eviction, and lifecycle handling.
  • The current package-wheel, documentation-image, two-node CI, and large-scale MQ GitHub checks are green.
  • In the documented single-node H100 SSD-pressure setup, Fluxon reached a 100% hit rate with SSD enabled.
  • At c16, hit-payload throughput for 4/8/16 MiB values was 3.83×/5.61×/6.73× that of the faster Mooncake
    topology.

These are scoped end-to-end logical-throughput results, not raw SSD bandwidth.

Other changes currently included in this branch

The branch also contains:

  • FluxonFS VideoReader and video dataloader benchmarks.
  • MQ/etcd client-pool, retry, and consumer-rebind improvements.
  • CI artifact cleanup and test-stack stability improvements.
  • Additional development, code-review, event-subscription, and data-structure design guidelines.

Fixes and hardening, in chronological order

  1. SSD IO correctness and test coverage (b477e02)

    • Allow small or unaligned targets to use the scratch-buffer refill path instead of rejecting them early.
    • Handle EINTR, drain completions on EBUSY, and explicitly settle outstanding IO after fatal io_uring
      failures.
    • Select SSD devices according to value and shard capacity.
    • Fix the 64 KiB FFmpeg AVIOContext buffer leak in VideoReader.
    • Add SSD P2P rounds and KV Rust unit tests to CI.
  2. KV semantics, etcd, and MQ stability (585da30b43ce92)

    • Add atomic admission for create-only puts, distinguishing committed keys from concurrent in-flight
      writes.
    • Preserve key-version isolation across lease rebinding and make lease-expiration tests deterministic.
    • Consolidate etcd connections into a process-wide, generation-aware client pool.
      retries MQ-offset failures a failure and stop further admission.
    • Expand MPMC regression coverage for continuously running producers and repeated consumer rebinding.
    • Clean successful CI artifacts and replace environment-specific hosts, addresses, and paths with generic
      examples.
  3. SSD shutdown, backpressure, and remote-refill lifecycle (8dd0b02cc13dcf)

    • Introduce a module-scoped shutdown gate that stops admission, drains active work, closes queues, joins
      workers, and then releases io_uring.
    • Propagate backpressure when the SSD eviction queue is full instead of dropping invalidations.
    • Use collision-resistant instance directory components and reject dot-segment path escapes.
    • Add an explicit begin/finalize lifecycle for remote SSD stages.
    • Make the SSD source responsible for remote-refill terminal reporting and support idempotent replay after
      a lost GetDone response.
    • Load benchmark credentials from a permission-restricted file instead of command-line arguments.
  4. Failure draining and hot-path scalability (de5dbc2)

    • Drain every submitted SSD read after a chunk failure or ready-channel closure.
    • Preserve the first transfer error while waiting for all started sibling transfers to finish.
    • Stop draining the bounded writer channel while blocked, preventing unbounded local pending growth.
    • Replace full-table scans with incremental per-shard usage and busy-offset indexes.
    • Derive SSD replica ownership from the authenticated transport caller.
    • Index holdings by member to close MemberLeft races without scanning the complete holding table.
  5. Generation, address-range, and concurrency edge cases (7039918)

    • Store immutable (begin, key) generations in ring order entries.
    • Add operation and byte budgets to SSD read admission.
    • Validate complete local address ranges and access permissions, keeping segment slots alive during IO.
    • Scope in-flight GET admission to member generations and atomically handle leave, rejoin, and terminal

let (source_allocation, source_base, source_addr, response_ssd_stage_len) =
if local_direct_read {
let target_capacity = target_allocation.capcity();
if target_capacity < ssd_stage_len {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 允许未对齐容量走 scratch 读取

这里把本地 SSD 回填强制要求为 target_capacity >= align(len, 512),但 KvSsdStorage::load_into_addr 已经会在容量不满足 direct I/O 时选择 scratch,并且最终只复制真实的 len。当前 buddy allocator 对 100 B 请求只会给 128 B capacity,因此这类小 value 的内存副本被驱逐后,本地 SSD GET 会在这里直接失败,尽管 target 足以容纳 payload。这里应只要求 target_capacity >= len,并把实际 capacity 传下去,让存储层自行选择 direct/scratch。

self.submit_ctx(ctx, &mut read_inflight, &mut write_inflight);
continue;
}
if let Err(err) = self.uring.submit_and_wait(1) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 不要因可重试的 io_uring 错误退出 worker

submit_and_wait 可能瞬时返回 EINTR,CQ overflow 时也可能返回 EBUSY。当前分支只处理已经出现在 CQ 中的项就直接退出;仍在内核中的 SQE 对应的 IoCtx 只剩 user_data 裸指针,其 oneshot 不会完成,随后该 ring 上的读写会挂住或因 receiver 关闭持续失败。请对 EINTR 重试、对 EBUSY drain 后重试;若确实是致命错误,也需要显式完成/取消全部 outstanding context 后再终止 worker。

avformat_close_input(&fmt_ctx);
}
if (avio_ctx != NULL) {
avio_context_free(&avio_ctx);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 释放 AVIOContext 使用的 buffer

成功调用 avio_alloc_context 后,上面的局部 avio_buffer 被置为 NULL;而 FFmpeg 的 avio_context_free 只释放 context 本身(实现中仅调用 av_freep(ps)),不会释放 avio_ctx->buffer。因此每次 read_frames_numpy 都会泄漏这里分配的 64 KiB。应在 avio_context_free 前调用 av_freep(&avio_ctx->buffer)

detail: "kv ssd storage has no active device".to_string(),
}));
}
let idx = self.next_write_device.fetch_add(1, Ordering::Relaxed) % self.devices.len();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 按 entry 大小选择可承载的 device

这里在不知道 entry_len 的情况下直接 round-robin 到单个 device,而该 device 的 writer 之后只会在自己的 shard_ids 中分配。配置不同大小的多个 root 时,一个只放得进大盘 shard 的 value 若轮询到小盘会直接返回 InvalidArgument,即使其他 device 有足够空间也不会重试,导致 SSD persist 是否成功取决于计数器奇偶。建议先按 aligned length 过滤可承载的 device,并在候选 device 间做 fallback。

key,
data_len as u64,
);
self.get_revoke_ssd_source(get_id).await?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R2][blocking] revoke 前必须等待远端 SSD stage 静默

SsdStageRead RPC 的 60 秒 timeout 只结束 requester 的等待;source 侧 rpc_ssd_stage_read 是独立 spawn,可能仍在 load_and_push_kv_from_ssd 中读写 stage_addr / target_addr。这里立即发 GetRevoke 后,master 会移除 inflight_gets,source 和 target Allocation 的最后一个 Arc 可能被释放并复用,旧 source task 随后就会把数据写进另一个 value;inflight 的 60 秒 TTL 也有同一竞态。请按 get_id 增加 cancel + quiescence ACK(或等价的 ownership barrier),确认 source 不再访问这些裸地址后再释放 allocation。

put_id: key.put_id,
})
.collect();
if let Err(err) = eviction_tx.try_send(replicas) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] SSD 淘汰通知不能在队列满时丢弃

这不是可丢的观测事件:本地 ring 已经删除 entry,而 master 仍把它作为有效 SSD replica。队列 Full(例如 master 暂时不可达、batch actor 正在重试)后,这条状态变化没有任何后续恢复来源,get_meta/is_exist 会长期返回 true,GET 也会先路由到不存在的数据后才被动自愈。请对通知施加 backpressure,或用按 key/put_id 合并且不会丢失的 pending 状态;不要让 queue full 永久分裂本地存储和 master route。

help="FluxonFS username used to sign file RPC tokens.",
)
fluxon_group.add_argument(
"--fluxon-request-password",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R6][major] 不要通过 argv 接收真实密码

传给这个参数的值会暴露在 shell history、CI 命令日志以及同机进程可读的 /proc//cmdline / ps 中;后面的 sanitized_args 只能保护应用自己写出的结果,无法覆盖这些渠道。请复用项目已有的 secret/config 文件机制,或提供 password-file / 受控 stdin 之类不把秘密放进 argv 的入口,并同步修改 README 示例。

let (write_tx, write_rx) = tokio_mpsc::channel(DEFAULT_WRITE_QUEUE_DEPTH);
let (read_tx, read_rx) = tokio_mpsc::channel(DEFAULT_READ_QUEUE_DEPTH);

task::spawn(ssd_writer_loop(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R2][blocking] SSD worker 必须进入 shutdown completion barrier

这两个原生 tokio::spawn 的 JoinHandle 被直接丢弃,也没有注册到 framework task registry;ClientKvApi::shutdown 又没有关闭读写队列或等待它们。因此 framework shutdown 返回后,worker 仍会阻塞在 recv,UringIoEngine 的线程和 fd 也继续存活。更危险的是 runtime 强制销毁 task 时,in-flight future 持有的 buffer 可能先释放,而内核 I/O 仍保留其裸指针。请由 KvSsdStorage 保存 task handles,按“停止 admission → 关闭队列/取消 → drain → join worker → drop io engine”收敛,并增加 close 返回后无后台活动的测试。

)
.await
{
Ok(()) => inner.get_done(req.get_id).await,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R1/R3][blocking] 不要在 holder_id 可交付前不可逆地提交 holding

这里的 GetDone 会先让 master 把 inflight 项移入 get_holding,但 holder_id 还要经过外层 SsdStageReadResp 才能到 requester。若该响应发送失败或 requester 在 60 秒处超时,requester 只会拿 get_id 去 revoke;master 已经找不到 inflight,requester 又从未创建 MemHolder,因而不会发送 DeleteAck,这块 allocation 只能等 member 离开时清理。请为 get_id 保留幂等的完成结果和可补偿的 revoke/query 映射,或把完成提交移到 requester,并确保响应丢失后仍能恢复 holder_id 或回收 holding。

for ch in raw.chars() {
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
out.push(ch);
} else {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R1][blocking] 目录组件必须无碰撞且不能保留 “.” / “..”

当前替换规则会把 owner/a:b、owner_a_b 等不同 instance_key 映射到同一目录,同时 “.” 和 “..” 会原样通过。instance_key 目前只校验非空;一旦两个 owner 共享 large_file_path,前者会让不同实例共用同一批 shard 文件,后者还能让路径跳出实例目录,而 open_cache_files 启动时会 truncate 这些文件。请使用可逆编码或“安全前缀 + 稳定哈希”的一一映射,并显式拒绝 dot segments,补充碰撞和路径逃逸测试。

@ActivePeter ActivePeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at cc13dcf: prior findings were rechecked. The remaining inline findings below focus on source-owned SSD finalization, raw-address quiescence, bounded backpressure, and avoiding full-table scans.

let Some(chunk) = inflight.next().await else {
break;
};
let chunk = chunk?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R2][blocking] 返回错误前必须收敛所有已提交的 SSD read。

触发场景:任一 chunk read 失败,或下一行 ready_tx.send 因 consumer 提前退出而失败。这里的 ? 会直接 drop FuturesUnordered 中其余 future,但 submit_read_command 已把 ReadCommand 放进 worker;drop oneshot receiver 不会取消 io_uring,worker 仍会向裸 stage_addr 写数据。随后 source 会发 GetRevoke,master 可能释放并复用 source allocation,形成越界写/数据破坏。

请记录首个错误后继续 drain/join 所有已提交 read,再允许 source terminal Revoke;并增加“一个 chunk 失败/ready receiver 关闭后,函数返回前所有 I/O 已完成”的故障注入测试。

}
}
Some(result) = inflight.next(), if !inflight.is_empty() => {
result?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R2][blocking] transfer 失败不能通过 drop sibling futures 来证明静默。

这里首个 result? 会丢弃其他已发起的 transfer_data_no_copy future。closed runtime 的 async invoke 是 callback + oneshot:drop receiver 不会撤销 provider 中已经提交的 transfer,所以函数可能先返回,source 随后 GetRevoke 释放 stage/target allocation,而后台 transfer 仍在访问裸地址。

请保留首错但等待全部已提交 transfer 完成,或者实现具有 completion barrier 的 cancel;验收标准是 source 发 Done/Revoke 前不存在仍访问该 get_id 地址的 transfer。

Ok(()) => match inner.get_done(req.get_id).await {
Ok(done) => Ok(done),
Err(done_err) => {
if let Err(revoke_err) = inner.get_revoke(req.get_id).await {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R2/R3][blocking] source 的 terminal RPC 失败后不能只记录日志并退出。

GetDone 失败,fallback GetRevoke 也失败,这个 handler 会返回;下面 transfer-error 分支也一样。requester 随后的 Revoke 在 lifecycle=Active 时只会记 pending,而 inflight_gets 已改为无 TTL,source 又仍是存活成员,因此 allocation 和 durable slot 会永久滞留,没有后续 writer 能完成状态转换。

需要由 source 持有可重试的 terminal-report 状态,直到 master ACK,或进入明确的 shutdown/member-left 回收契约;不要恢复一条无 shutdown 收敛条件的无限循环。

let inflight_ids = view
.master_kv_router()
.inner()
.take_member_inflight_get_ids(node_id);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R2/R3][blocking] MemberLeft 与 source-owned GetDone 存在漏清理竞态。

当前先 cleanup_node,再处理 inflight index。source 可在 cleanup 之后获得 get transition lock,提交新的 holding 并从 member index 删除 get_id;这里随后 take 到空集合(或拿到 id 后发现 inflight 已消失),新 holding 就永久留给已离群 requester。

请先在每个 get_id 的 transition lock 下建立 requester-left fence,再做最后的 holding cleanup;同时给 holding 建 member_id -> holder_id 索引,避免 cleanup_node 底层对整个 DashMap 做全表扫描。

if new_tail <= self.shards[shard_id].tail {
return false;
}
let writing_busy = self.entries.values().any(|state| match state {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] SSD 写入热路径不能扫描全部 entry/read pin。

ring 填满后,几乎每次 allocate_contiguous 都会在全局 inner mutex 下走这里,对 entriesread_pins 做 O(N) 扫描;cache key 增长后会把稳定写吞吐退化成 O(N²) 并阻塞并发读写。

请维护按 shard 的 busy frontier/有序计数索引,在 write submit/complete、pin/unpin 时增量更新,使 wrap 判定只查询目标 shard,而不是扫描整张表。


fn used_bytes_by_shard(&self) -> Vec<u64> {
let mut out = vec![0u64; self.shards.len()];
for state in self.entries.values() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] 指标采集不应每 30 秒在全局 storage mutex 下扫描全部 SSD entry。

device_usage_snapshot 由 owner metric loop 周期调用,这里会造成与 key 数量成正比的 I/O 停顿,也违反本 PR 的 no-full-scan 约束。请在 commit/remove/advance_tail 时维护 per-shard used-bytes 计数,snapshot 只读取计数。

Some(completion) = inflight.next(), if !inflight.is_empty() => {
finish_write_completion(&inner, &space_notify, completion);
}
Some(cmd) = rx.recv() => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] pending 会绕过 bounded channel 变成无界的大对象队列。

当队首因 read/persist pin 返回 BlockedByBusyIo 时,本循环仍持续从 rx 接收并把含完整 AlignedBuffer 的命令放进 VecDeque;只要 producer 持续写,原本有容量上限的 channel 会被反复抽空,pending 可无限增长并 OOM。

请在 blocked 状态停止接收,或给 pending 施加与 channel 一致的硬上限,让背压传播回调用者。

let view_task = view2.clone();
let _ = view.spawn("rpc_ssd_replica_commit", async move {
let t0 = Utc::now().timestamp_micros();
let mut ack = handle_ssd_replica_commit(view_task, msg).await;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R1][major] SSD replica 的 owner 身份必须来自已认证的 RPC caller。

当前 handler 丢弃 resp.node_id(),转而信任 request 内可伪造的 node_id。任一 cluster member 都能把 SSD replica 提交到另一个已有 memory replica 的节点,master 随后会把 GET 路由到并不存在该 entry 的 owner;同文件的 eviction handler 已正确使用 caller identity。

请把 resp.node_id() 传给 handle_ssd_replica_commit 并移除冗余字段,或至少强制校验二者相等。

@ActivePeter ActivePeter left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at de5dbc2:前一轮的 worker join、淘汰通知、read/transfer drain、全表扫描、writer backpressure、holding MemberLeft 竞态和 commit caller 身份均已复核修复。最新 head 仍有以下 raw-address authority、失败代际、bounded backpressure 与 shutdown completion-barrier 问题;存在 blocking/major finding,当前不建议合并。

);

let view_ext = inner.view.clone_view();
RPCHandler::<SsdStageReadReq>::new().regist(inner.view.p2p_module(), move |resp, msg| {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R1][blocking] SsdStageReadReq 的 raw-address descriptor 必须绑定 transport caller 和 master authority。

这里丢弃了 resp.node_id();后续 SsdStageBegin 只让 source 用 get_id 向 master 证明自己是 source,master 没有验证本次 StageRead 的真实 caller 是 inflight.req_node_id,也没有核对 request 中的 key/put_id/stage_addr/stage_len/target_node_id/target_addr/len。任一 cluster member 都可以借 active get_id 让 source 对 request 指定的地址发起 io_uring read,或向任意 member segment 推送数据,并可在失败后让有效 SSD replica 被 revoke。

请把 transport-derived caller 绑定到 inflight requester,并让 master 校验或返回权威 descriptor;source 只能使用 master 授权的 tuple。不要把 caller id 放进可伪造 payload 当作修复。

});

let view_ext = inner.view.clone_view();
RPCHandler::<SsdReplicaPersistReq>::new().regist(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R1][blocking] 这个 persist raw-address RPC 必须验证真实调用方。

当前同样丢弃 resp.node_id(),而 handler 会把 payload 中的 target_addr/len 直接交给 persist_from_addr;native backend 在 AlignedBuffer::copy_from_addr 解引用该地址,Foyer backend 用 from_raw_parts(...).to_vec() 解引用。任一 cluster member 因而可以让 owner 读取任意映射地址、申请超大 buffer,甚至触发进程崩溃,并把伪造内容提交为 SSD replica。

该 RPC 目前只有 master 会调用,请至少只接受 transport 识别出的当前 master,并把请求绑定到 master 持有的 put/allocation descriptor;不能信任 payload 自报身份。

view_task.p2p_module(),
node_id.clone(),
req,
Some(Duration::from_secs(60)),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R2][blocking] RPC timeout 不能作为远端 raw-address reader 已静默的证明。

owner 的 handler 是收到请求后独立 spawn 的;caller 在这里 60 秒超时/重试只停止等待,不会取消那个 handler。尤其 Foyer handler 可能先长时间等待 persist_gate,尚未执行 raw copy。两次 attempt 都超时后本 task 会结束并释放 _allocation_guard;若 route 同时被 delete、替换或 MemberLeft 清理,迟到的 owner handler 随后会解引用已释放/复用的 target_addr

请为 persist 建立明确的 begin → raw-copy-quiescent terminal 生命周期(或让 owner 持有可验证的 allocation/capability),由 master 在 terminal ack 或 owner MemberLeft 后释放。不要用更长 timeout 或 TTL 替代 quiescence。

tail: u64,
used_bytes: u64,
busy_begins: BTreeMap<u64, usize>,
order: VecDeque<KvSsdKey>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] order 只保存 key,会把同 key 的旧 reservation 和新 reservation 混成一个 generation。

可复现:capacity=2048、每项 512;A@0 写失败后 entry 被删但旧 A 留在 order,提交 B@512,再重试并提交 A@1024C@1536。wrap 后推进 tail 时,队首旧 A 会通过 entries.get(A) 解析成新 A@1024 并提前 break;下一次覆盖 B 时仍不淘汰它,used_bytes 超 capacity(debug assert),release 下还会让 B 指向已覆盖的数据。

请让 order 保存不可变 generation,例如 (begin, key);pop 时只在当前 entry 的 begin 与该 generation 相同时删除/发布 eviction。不要通过扫描 order 或 entries 修复。

};
let _ = completion.done_tx.send(result);
}
Some(cmd) = rx.recv() => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] reader 仍会绕过 bounded channel,把 pending 变成无界队列。

inflight.len() == max_inflight 时,这个 recv 分支仍然启用,actor 会不断清空容量 16 的 channel 到无界 VecDequeReadCommand 在 send 前已经可能分配整块 scratch buffer,并持有 raw target/read pin;并发读取持续到达时,原本的 channel backpressure 因而失效并可 OOM。

请只在 inflight.len() < max_inflight 时 recv;reader 没有 writer 的 Blocked 状态,通常可以直接删除 pending。补一个 inflight 被阻塞时验证 read_tx.capacity()==0 的回归测试。

Ok(()) => match inner.get_done(req.get_id).await {
Ok(done) => Ok(done),
Err(done_err) => {
if let Err(revoke_err) = inner.finish_ssd_source_revoke(req.get_id, false).await {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R2][blocking] source 的 terminal phase 仍不能被 shutdown 中断。

上面的 get_done 和这里的 fallback 都先调用无 deadline 的 find_or_wait_master_node()finish_ssd_source_revoke 内部的 get_revoke_inner 还使用 timeout=None。它虽然检查 poller/gate,但只会在当前 await 返回后检查。若 master 消失,handler 一直持有 ssd_stage_readShutdownGuardClientKvApi::prepare_shutdown 先 stop admission 再等 gate quiescence,而 framework poller 要等所有 prepare hooks 返回后才置 false,于是形成永久 shutdown deadlock。

请让每次 master lookup/RPC attempt 有有限 deadline或与模块 shutdown 信号竞争,并只对 transient error 重试。仍须保留“master ack 或 source MemberLeft 才能最终释放 raw allocation”的单一 release authority。

},
raw_bytes: Vec::new(),
};
let master_node_id = self

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R2][blocking] SSD persist handler 也会在这里把 shutdown completion barrier 卡死。

handle_ssd_replica_persist 持有外层 ShutdownGuard 和已提交 entry 的 persist guard,再进入无 deadline 的 find_or_wait_master_node()。若 master 在本地写盘后离开,ClientKvApi shutdown 会等待这个 guard,但 global poller 尚不会关闭,lookup 也没有事件可以结束。后面的 60 秒 RPC timeout 覆盖不到 master lookup。

请给 lookup/commit attempt 有界 deadline或与模块 shutdown 竞争;失败时不要向 master 发布 replica,释放本地 guard 后让 entry 走正常 eviction/后续回收,不要让外层直接改 storage 内部状态。

self.view.get().unwrap()
}

pub(crate) fn insert_inflight_get(&self, get_id: u64, info: InflightGetInfo) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[R2/R3][blocking] MemberLeft 必须原子关闭该 member 的 inflight admission;当前 take 与这里的 insert 仍可漏项。

两个具体 interleaving:① MemberLeft 先 take 到空集合,正在执行的 GetStart 随后插入;② 这里先把 get_id 放进 member index,MemberLeft take 后拿 transition lock时 main inflight_gets 尚未执行 entry.insert(info),于是看到 None 并丢掉 id,随后 main entry 才发布。inflight_gets 已是无 TTL DashMap,请求方又已离群,这会永久持有 allocation/durable slot。

请像 MasterOwnerMemMgr 一样在 per-member DashMap state 中保存 departed + get_ids,MemberLeft 先关闭 admission,MemberJoined 再重开,并提供 insert_if_active;同时保证 index/main publication 不会出现“id 已被消费但 main 尚不可见”。只按离开的 member 处理其集合,不能回退成全表扫描。

@ActivePeter
ActivePeter merged commit 37bf8fc into main Jul 21, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants