背景
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,而不是做成可选后端,原因是:
full_sync.reader.oplog_store_disk=true 本来就是低频、特殊的可靠性路径。
- 当前
go-diskqueue 路径已经有明确 bug 和生产不稳定经验。
- 如果保留两个后端,测试矩阵会变大,但仍然保留了一条已知高风险路径。
- 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/spool。Persister 只依赖这个接口,不直接依赖 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_seq、m/last_write_ts、m/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_count 和 disk_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
背景
full_sync.reader.oplog_store_disk=true当前依赖github.com/vinllen/go-diskqueue,用于在全量同步期间把增量 oplog 临时写入本地磁盘,并在全量完成后回放这些 oplog。这条路径近期已经暴露出一些可靠性问题:
Persister.retrieve()等待循环无法正确跳出,导致磁盘中的 oplog 永远不会进入 apply 阶段。Persister.retrieve()里还有 polling 延迟和 ticker 生命周期问题。full_sync.reader.oplog_store_disk=true后,后续回放本地磁盘 oplog 时仍可能出现 panic。当前 disk queue 实现不太适合作为长期可靠的本地落盘层。#981 已经修复了
Persister.retrieve()状态机层面的主要问题,但底层仍然保留go-diskqueue。这个 issue 建议进一步把本地临时 oplog 队列替换成 Pebble-backed spool。
目标
直接使用 Pebble 替换
go-diskqueue,用户侧配置保持不变:full_sync.reader.oplog_store_disk = true不新增 storage backend 选择项,也不保留
go-diskqueuefallback。替换后仍保持现有运行语义:
StartDiskApply()切换到磁盘回放阶段。OplogDiskQueueFinishTs继续表示本地落盘 oplog 的最后 timestamp;只有 worker checkpoint 追上这个 timestamp 后,才认为磁盘回放真正完成。为什么直接移除 go-diskqueue
我建议直接移除
go-diskqueue,而不是做成可选后端,原因是:full_sync.reader.oplog_store_disk=true本来就是低频、特殊的可靠性路径。go-diskqueue路径已经有明确 bug 和生产不稳定经验。兼容性策略
新实现不需要兼容读取旧版本遗留的
go-diskqueue文件。如果 checkpoint 仍然指向旧版本创建的本地 queue,新版本启动时应直接 fail fast,并给出明确错误提示,例如:
不做自动迁移的原因:
设计草案
建议新增一个很窄的内部 spool 抽象,例如放在
collector/spool。Persister只依赖这个接口,不直接依赖 Pebble API。本地目录建议为:
其中
<queue-name>继续由 checkpoint 字段OplogDiskQueue保存。字段名可以不改,虽然内部格式已经不是go-diskqueue。Pebble key schema
Pebble 内部建议分两个 key namespace:
具体 schema:
这里
d/<uint64_be(seq)>使用 big-endian uint64 编码,是为了让 Pebble 的字典序 scan 等价于 seq 数值升序。例如:这样
ReadBatch只需要从d/<read_seq>开始用 iterator 顺序扫描,就能得到 FIFO 回放顺序。meta 字段含义
write_seq表示本地 spool 已经持久化到哪个位置。read_seq表示下一条应该回放的位置。因此当前未回放条数可以按下面计算:
last_write_data用来保持当前GetQueryTsFromDiskQueue()的语义:回放完成后仍然能拿到最后一条本地落盘 oplog,并解析出diskQueueLastTs。last_write_ts是同一信息的结构化缓存,便于恢复、日志和后续指标使用;第一版仍可保留从last_write_data解析 timestamp 的路径,以减少Persister改动面。写入语义
Put(data)使用 Pebble batch 原子写入:这样可以避免 “data 写入成功但 meta 没更新” 或 “meta 更新了但 data 不存在” 这类部分状态。
默认不对每条 oplog 强制 fsync。原因是当前
go-diskqueue路径也不是严格断电事务日志语义;每条 fsync 会显著影响全量期间写入性能。第一版可以依赖 Pebble batch 原子提交,并在正常关闭 / 阶段切换时 close 或 flush。读取和推进语义
ReadBatch(max)只读取,不修改read_seq:Advance(n)在数据已经推入 pending queue 后推进:这保持当前
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 场景。
参考资料:
这些资料里提到的能力与本设计的对应关系:
d/<uint64_be(seq)>的 FIFO scan。read_seq开始顺序读取待回放 oplog。d/<seq>和m/write_seq、m/last_write_ts、m/last_write_data原子写入。可观测性
建议新增 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}现有
/persistAPI 建议继续保留disk_write_count和disk_read_count,避免破坏已有调试入口;可以额外增加:spool_depthspool_read_seqspool_write_seq范围
本 issue 建议包含:
collector/persister.go中的go-diskqueue调用。go-diskqueue本地文件 fail fast。不包含:
go-diskqueue文件迁移。验证建议
建议覆盖以下命令:
建议覆盖以下用例:
Put/ReadBatch/Advance/Depth。read_seq/write_seq/last_write_data能恢复。Delete后本地 spool 目录被删除。go-diskqueue目录启动失败。[read_seq, write_seq]中间缺失 data key 时启动或读取失败。/metrics中能看到 spool 相关指标。相关 issue / PR