Skip to content

使用 Pebble 替换 full_sync.reader.oplog_store_disk 的 go-diskqueue 本地落盘实现 #982

Description

@SisyphusSQ

背景

full_sync.reader.oplog_store_disk=true 当前依赖 github.com/vinllen/go-diskqueue,用于在全量同步期间把增量 oplog 临时写入本地磁盘,并在全量完成后回放这些 oplog。

这条路径近期已经暴露出一些可靠性问题:

#981 已经修复了 Persister.retrieve() 状态机层面的主要问题,但底层仍然保留 go-diskqueue

这个 issue 建议进一步把本地临时 oplog 队列替换成 Pebble-backed spool。

目标

直接使用 Pebble 替换 go-diskqueue,用户侧配置保持不变:

full_sync.reader.oplog_store_disk = true

不新增 storage backend 选择项,也不保留 go-diskqueue fallback。

替换后仍保持现有运行语义:

  • 全量同步期间,增量 oplog 写入本地临时 spool。
  • 全量同步完成后,StartDiskApply() 切换到磁盘回放阶段。
  • 本地 spool 中的 oplog 按 FIFO 顺序回放到现有 pending queue。
  • 回放完成后切换到内存 apply。
  • OplogDiskQueueFinishTs 继续表示本地落盘 oplog 的最后 timestamp;只有 worker checkpoint 追上这个 timestamp 后,才认为磁盘回放真正完成。

为什么直接移除 go-diskqueue

我建议直接移除 go-diskqueue,而不是做成可选后端,原因是:

  1. full_sync.reader.oplog_store_disk=true 本来就是低频、特殊的可靠性路径。
  2. 当前 go-diskqueue 路径已经有明确 bug 和生产不稳定经验。
  3. 如果保留两个后端,测试矩阵会变大,但仍然保留了一条已知高风险路径。
  4. Pebble 是成熟的嵌入式有序 KV,可以把一致性、批量写入和有序 scan 交给存储层处理,避免继续维护自定义队列文件语义。

兼容性策略

新实现不需要兼容读取旧版本遗留的 go-diskqueue 文件。

如果 checkpoint 仍然指向旧版本创建的本地 queue,新版本启动时应直接 fail fast,并给出明确错误提示,例如:

oplog spool <name> is not a Pebble spool. Legacy go-diskqueue files are not supported.
Please either finish replay with the old MongoShake version, or clear the stale checkpoint/local queue after confirming source oplog can cover the restart position.

不做自动迁移的原因:

  • 旧格式本身就是这次要移除的风险点。
  • 自动迁移需要理解旧 queue 文件格式、读取游标、部分回放状态和异常损坏场景,复杂度较高。
  • 错误迁移比显式失败更危险,可能导致 oplog 丢失或重复回放。

设计草案

建议新增一个很窄的内部 spool 抽象,例如放在 collector/spoolPersister 只依赖这个接口,不直接依赖 Pebble API。

type OplogSpool interface {
    Put(data []byte) error
    ReadBatch(max int) ([][]byte, error)
    Advance(n int) error
    ReadAll() ([][]byte, error)
    LastWriteData() ([]byte, error)
    Depth() (uint64, error)
    Stats() Stats
    Close() error
    Delete() error
}

本地目录建议为:

<log.dir>/spool/<queue-name>/

其中 <queue-name> 继续由 checkpoint 字段 OplogDiskQueue 保存。字段名可以不改,虽然内部格式已经不是 go-diskqueue

Pebble key schema

Pebble 内部建议分两个 key namespace:

m/...  meta key
d/...  data key

具体 schema:

m/version             -> uint32,schema version,第一版为 1
m/write_seq           -> uint64,已经成功写入的最大 seq
m/read_seq            -> uint64,下一条需要回放的 seq
m/last_write_ts       -> int64,最近写入 oplog 的 timestamp 编码值
m/last_write_data     -> []byte,最近写入 oplog 的原始 BSON bytes

d/<uint64_be(seq)>    -> []byte,seq 对应的原始 oplog BSON bytes

这里 d/<uint64_be(seq)> 使用 big-endian uint64 编码,是为了让 Pebble 的字典序 scan 等价于 seq 数值升序。例如:

d/0000000000000001
d/0000000000000002
d/0000000000000003

这样 ReadBatch 只需要从 d/<read_seq> 开始用 iterator 顺序扫描,就能得到 FIFO 回放顺序。

meta 字段含义

write_seq 表示本地 spool 已经持久化到哪个位置。

read_seq 表示下一条应该回放的位置。

因此当前未回放条数可以按下面计算:

depth = write_seq >= read_seq ? write_seq - read_seq + 1 : 0

last_write_data 用来保持当前 GetQueryTsFromDiskQueue() 的语义:回放完成后仍然能拿到最后一条本地落盘 oplog,并解析出 diskQueueLastTs

last_write_ts 是同一信息的结构化缓存,便于恢复、日志和后续指标使用;第一版仍可保留从 last_write_data 解析 timestamp 的路径,以减少 Persister 改动面。

写入语义

Put(data) 使用 Pebble batch 原子写入:

d/<next_seq>        = raw oplog BSON
m/write_seq         = next_seq
m/last_write_ts     = oplog timestamp
m/last_write_data   = raw oplog BSON

这样可以避免 “data 写入成功但 meta 没更新” 或 “meta 更新了但 data 不存在” 这类部分状态。

默认不对每条 oplog 强制 fsync。原因是当前 go-diskqueue 路径也不是严格断电事务日志语义;每条 fsync 会显著影响全量期间写入性能。第一版可以依赖 Pebble batch 原子提交,并在正常关闭 / 阶段切换时 close 或 flush。

读取和推进语义

ReadBatch(max) 只读取,不修改 read_seq

从 d/<read_seq> 开始 iterator scan,最多读取 max 条

Advance(n) 在数据已经推入 pending queue 后推进:

m/read_seq = old_read_seq + n

这保持当前 go-diskqueue.Next() 的语义:本地 spool 的 read pointer 表示“已经交给 pending queue”,不是“已经写入目标端”。真正端到端确认仍然由 worker ack 和 checkpoint 负责。

清理语义

回放完成后删除整个 Pebble spool 目录,而不是长期保留已读 key。

第一版可以不对每批已读数据做 point delete,只推进 read_seq。原因是这个 spool 本来就是全量期间的临时目录,最终会整体删除;逐条删除会增加写放大。

Pebble 参考资料

Pebble 是 CockroachDB 使用的 Go 实现嵌入式 KV 存储,设计上类似 LevelDB / RocksDB。它支持 ordered key iteration、batch 写入、DeleteRange 等能力,适合这里的本地临时有序 spool 场景。

参考资料:

这些资料里提到的能力与本设计的对应关系:

  • ordered key/value store:用于 d/<uint64_be(seq)> 的 FIFO scan。
  • Iterator:用于从 read_seq 开始顺序读取待回放 oplog。
  • Batch:用于把 d/<seq>m/write_seqm/last_write_tsm/last_write_data 原子写入。
  • Delete / DeleteRange:可作为后续优化,用于批量清理已读 key;第一版先整体删除 spool 目录。

可观测性

建议新增 Prometheus 指标:

  • spool_depth{name,stage}
  • spool_write_seq{name,stage}
  • spool_read_seq{name,stage}
  • spool_write_total{name,stage}
  • spool_read_total{name,stage}
  • spool_errors_total{name,stage,op}

现有 /persist API 建议继续保留 disk_write_countdisk_read_count,避免破坏已有调试入口;可以额外增加:

  • spool_depth
  • spool_read_seq
  • spool_write_seq

范围

本 issue 建议包含:

  • 替换 collector/persister.go 中的 go-diskqueue 调用。
  • 新增 Pebble-backed spool 实现和单元测试。
  • 保持现有 checkpoint 字段和 stage 状态机。
  • 增加 Prometheus 指标和关键日志。
  • 对旧 go-diskqueue 本地文件 fail fast。

不包含:

  • change stream spool 支持。
  • 跨 shard / replset 全局排序。
  • storage backend 配置项。
  • go-diskqueue 文件迁移。
  • Grafana dashboard 或 alert rule 调整。

验证建议

建议覆盖以下命令:

go test ./collector/spool
go test ./collector -run 'TestPersister|TestCheckpoint'
go test ./common -run 'Test.*Prom'
go build ./cmd/collector

建议覆盖以下用例:

  • Put / ReadBatch / Advance / Depth
  • 关闭后重新打开 Pebble spool,确认 read_seq / write_seq / last_write_data 能恢复。
  • Delete 后本地 spool 目录被删除。
  • 非 Pebble 目录或旧 go-diskqueue 目录启动失败。
  • schema version 不匹配时启动失败。
  • [read_seq, write_seq] 中间缺失 data key 时启动或读取失败。
  • /metrics 中能看到 spool 相关指标。

相关 issue / PR

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions