# Phase 1: WAL 子系统开发方案 基于 `docs/design.md` §3.2 设计文档。 ## 目标 实现完整的 WAL(预写日志)子系统,使其能够支撑单 key autocommit 的写入、崩溃恢复和读可见性语义。 ## 开发阶段总览 ``` Phase 1A: 项目骨架 + WAL 编码格式层 Phase 1B: WAL 文件写入 + Segment 管理 Phase 1C: WAL Writer(Group Commit) Phase 1D: WAL Recovery Phase 1E: MemTable(SkipList + Arena) Phase 1F: 写入路径集成(WAL → MemTable 完整流水线) Phase 1G: 读路径 + 嵌入式 API Phase 1H: MANIFEST + 文件管理 Phase 1I: 集成测试 + Benchmark ``` --- ## Phase 1A: 项目骨架 + WAL 编码格式层 **目标**: 建立 Go 项目结构,实现 WAL 物理格式(Block / Physical Record / WAL Batch / Entry)的编码与解码。 ### 任务 #### 1A-1: 项目初始化 - `go.mod` 初始化(模块名 `github.com/dailz/go-kv`) - 目录结构: ``` go-kv/ ├── go.mod ├── wal/ # WAL 子系统 │ ├── wal.go # 公共类型、常量、配置 │ ├── record.go # Physical Record 编解码 │ ├── batch.go # WAL Batch 编解码 │ ├── entry.go # Entry 编解码 │ ├── header.go # WAL File Header 编解码 │ └── wal_test.go ├── memtable/ # MemTable(Phase 1E) ├── config/ # 全局配置 ├── errors.go # 公共错误类型 └── db.go # DB 入口 ``` - `.golangci.yml` 配置(参考 golang-lint skill) #### 1A-2: 公共错误类型 (`errors.go`) - `ErrCommitUnknown` — maybe committed 语义 - `ErrWriteStopped` — 引擎 write-stopped - `ErrSequenceExhausted` — sequence 耗尽 - `ErrWALCorrupted` — WAL 损坏 - `ErrInvalidConfig` — 配置不合法 #### 1A-3: WAL 常量与配置 (`wal/wal.go`) ```go const ( WalMagic uint32 = 0x... // 待定 WalFormatVersion uint16 = 1 WalFileHeaderSize = 32 WalBlockSize = 32 * 1024 // 32KB PhysicalRecordHeaderSize = 7 WalBatchHeaderSize = 18 MaxWalBatchEntryCount = 10_000 MaxWalBatchEntriesSize = 4 * 1024 * 1024 // 4MB MaxWalKeyBytes = 4 * 1024 // 4KB MaxWalInlineValueBytes = 4 * 1024 // 4KB MaxWalVarintBytes = 5 DefaultMaxWalSegmentSize = 64 * 1024 * 1024 // 64MB DefaultImmutableCount = 2 ) // Fragment types const ( RecInvalid uint8 = 0 RecFull uint8 = 1 RecFirst uint8 = 2 RecMiddle uint8 = 3 RecLast uint8 = 4 ) // OpType const ( OpInvalid uint8 = 0 OpPut uint8 = 1 OpDelete uint8 = 2 ) // ValueKind const ( VKNone uint8 = 0 VKInline uint8 = 1 VKValueLogPointer uint8 = 2 ) ``` WAL 配置结构体: ```go type WalConfig struct { MaxSegmentSize uint64 // default 64MB BlockSize uint32 // default 32KB SyncMode SyncMode // Always/Periodic/Never PeriodicSyncMs uint32 // Periodic 模式的 fsync 间隔 MaxBatchEntries uint32 // default 10000 MaxBatchSize uint32 // default 4MB MaxKeyBytes uint32 // default 4KB MaxInlineValue uint32 // default 4KB } ``` 配置校验函数 — 必须在 DB 打开时验证不变量: ```text maxWalSegmentPayload >= maxEncodedWalBatchSize + worstCasePhysicalRecordOverhead + worstCaseBlockPadding ``` #### 1A-4: WAL File Header 编解码 (`wal/header.go`) - `WalFileHeader` 结构体:magic, formatVersion, headerSize, blockSize, segmentID, startSequence, headerCRC - `EncodeWalHeader(h *WalFileHeader) [WalFileHeaderSize]byte` - `DecodeWalHeader(data []byte) (*WalFileHeader, error)` — 校验 magic、formatVersion、headerSize、headerCRC - CRC 覆盖范围:magic 到 startSequence,不包含 headerCRC 自身 - 字节序:little-endian #### 1A-5: Physical Record 编解码 (`wal/record.go`) - `PhysicalRecord` 结构体:CRC, Length, Type, Payload - `EncodePhysicalRecord(recType uint8, payload []byte) []byte` — 返回编码后的 bytes - `DecodePhysicalRecord(data []byte) (*PhysicalRecord, error)` — CRC 校验 - Block 边界处理辅助函数: - `PaddingNeeded(blockOffset, blockSize uint32) int` — 剩余空间 <= 7 时返回需要 padding 的字节数 - `CanFitRecord(blockOffset, blockSize, payloadLen uint32) bool` #### 1A-6: WAL Batch 编解码 (`wal/batch.go`) - `WalBatch` 结构体:Flags, BaseSequence, EntryCount, EntriesSize, Entries - `EncodeWalBatch(batch *WalBatch) ([]byte, error)` — 编码 Batch Header + Entries - `DecodeWalBatch(data []byte) (*WalBatch, error)` — 校验 flags、entryCount、entriesSize - Batch 分片:`SplitIntoRecords(encodedBatch []byte, blockSize uint32) [][]byte` — 将编码后的 Batch 拆分为 Physical Record payloads - Batch 重组:`FragmentCollector` — 收集 fragments 并重组成完整 Batch FragmentCollector 状态机: ``` Idle → 收到 Full → 重放 batch → Idle Idle → 收到 First → CollectingFragments CollectingFragments → 收到 Middle → 追加 CollectingFragments → 收到 Last → 重组 → 重放 → Idle ``` #### 1A-7: Entry 编解码 (`wal/entry.go`) - `WalEntry` 结构体:OpType, ValueKind, Key, Value - `EncodeEntry(e *WalEntry) ([]byte, error)` — 编码为 varint 长度 + bytes - `DecodeEntry(data []byte) (*WalEntry, int, error)` — 解码,返回 entry 和 consumed bytes - 校验规则: - keyLen > 0 && keyLen <= maxKeyBytes - Put 要求 valueKind ∈ {Inline, ValueLogPointer} - Put + Inline: valLen <= maxInlineValueBytes (允许 valLen = 0) - Put + ValueLogPointer: valLen > 0 - Delete: valueKind == None, valLen == 0 #### 1A-8: WAL Batch 资源校验 - `ValidateBatchLimits(entries []*WalEntry) error` — 在 sequence 分配之前检查: - entryCount <= maxBatchEntries - 每个 keyLen <= maxKeyBytes - 每个 inline valLen <= maxInlineValueBytes - entries 编码后总大小 <= maxBatchSize - 单个 Batch 的最坏 Physical Record overhead 不超过 segment capacity ### 验收标准 - [ ] 所有编解码函数有 table-driven test - [ ] CRC 校验正确 - [ ] Fragment 分片/重组 round-trip 正确 - [ ] 资源限制校验覆盖所有边界条件 - [ ] `go vet` / `golangci-lint` 通过 --- ## Phase 1B: WAL 文件写入 + Segment 管理 **目标**: 实现 WAL segment 文件的写入、轮转和持久化协议。 ### 任务 #### 1B-1: Segment 文件格式写入器 (`wal/segment_writer.go`) - `SegmentWriter` — 封装 WAL segment 文件的追加写入 - 状态:当前 segment fd、当前 block offset、当前 segmentID、payload written bytes - `NewSegmentWriter(dir string, segmentID uint64, startSequence uint64, cfg *WalConfig) (*SegmentWriter, error)` - 创建 segment-N.wal.tmp - 写入 WAL File Header - fsync - rename → segment-N.wal - fsync directory - 进入 durable-ready 状态 - `AppendBatch(batch *WalBatch) error` — 编码 batch → split into records → 按 block 边界写入 - `Sync() error` — fsync 当前 segment 文件 - `Close() error` - `RemainingPayload() uint64` — 当前 segment 剩余可用 payload 空间 - `CurrentOffset() uint64` — 当前写入偏移 #### 1B-2: Block 写入缓冲 (`wal/block_writer.go`) - 管理 32KB block 的填充和 padding - `BlockWriter` — 封装 block 内的 Physical Record 写入 - 自动处理 block 边界:剩余 <= 7 bytes 时 padding - 跨 block 的 batch fragment 自动拆分 #### 1B-3: Segment 轮转逻辑 - 写入 batch 前检查 `RemainingPayload()` 是否足够容纳整个 batch - 不足时:当前 segment 完成(在 batch 边界)、创建新 segment - 新 segment 的 durable-ready 协议: 1. create segment-N+1.wal.tmp 2. write WAL File Header(含 startSequence = nextExpectedSequence) 3. fsync segment-N+1.wal.tmp 4. rename → segment-N+1.wal 5. fsync WAL directory 6. segment-N+1 进入 durable-ready - 旧的 active segment 密封 #### 1B-4: CURRENT 文件管理 - best-effort 更新 CURRENT 文件 - temp + rename 模式 - 更新失败不影响已 durable-ready 的 segment #### 1B-5: WAL 目录管理工具 - 扫描 WAL 目录中的 segment 文件 - 按 segmentID 排序 - 解析文件名中的 segmentID - 文件名格式:`segment-{id}.wal` ### 验收标准 - [ ] Segment 创建遵循 durable-ready 协议 - [ ] Batch 不跨 segment - [ ] Block padding 正确 - [ ] Segment 轮转在 batch 边界发生 - [ ] 多 segment 写入后,每个 segment 的 header 可以正确解析 - [ ] 测试覆盖:正常写入、跨 block batch、segment 轮转触发 --- ## Phase 1C: WAL Writer(Group Commit) **目标**: 实现完整的 WAL 写入路径,包括 group commit、sequence 管理、fsync 策略和错误分类。 ### 任务 #### 1C-1: Sequence 管理器 (`wal/sequence.go`) - `SequenceManager` — 管理 WAL 物理 mutation sequence - `atomic.Uint64` 存储 nextSequence、publishedSequence、durableSequence - `AllocateBatch(count uint32) (baseSequence uint64, err error)` — checked arithmetic 检查溢出 - `Publish(sequence uint64)` — release 语义 store publishedSequence - `MarkDurable(snapshot SegmentEndState)` — 推进 durableSequence - `Published() uint64` — load publishedSequence - `Durable() uint64` — load durableSequence #### 1C-2: Commit Queue (`wal/commit_queue.go`) - 写请求进入的队列 - 每个写请求关联一个 `*sync.Cond` 或 channel 用于等待/唤醒 - `CommitBatch` 结构体:entries、完成 channel、错误结果、baseSequence #### 1C-3: WAL Writer 主循环 (`wal/writer.go`) 核心写入循环: ``` loop: 1. 从 commit queue 收集一批写入 2. 等待触发条件(500µs 或 32KB)或 queue 非空 3. 组装 WAL Batch 4. 校验 batch 资源限制 5. 预留 MemTable Arena 容量 6. 分配 sequence(baseSequence) 7. 在私有缓冲区编码 WAL Batch 8. 检查/触发 segment 轮转 9. Append WAL Batch 到 segment 文件 10. 写入 MemTable(pending/unpublished) 11. fsync(Always 模式) 12. 发布 publishedSequence 13. 唤醒所有等待的调用方 ``` 错误分类逻辑: - 步骤 4-7 失败(未分配 sequence)→ 普通错误,可继续 - 步骤 6 后失败(sequence 已分配)→ write-stopped - 步骤 9 后失败(WAL write 已尝试)→ ErrCommitUnknown + write-stopped - 步骤 11 失败(fsync)→ ErrCommitUnknown + write-stopped #### 1C-4: Fsync 策略实现 (`wal/fsync.go`) - `SyncMode` 类型:Always / Periodic / Never - `Always`: 每次 batch fsync 后再 publish - `Periodic`: 后台 goroutine 定期 fsync,write 成功即可 publish - `Never`: 不主动 fsync - `Periodic` 的 fsync worker: - 快照当前 append high-water mark: (segmentID, endOffset, endSequence) - fsync 成功后按连续 batch 推进 durableSequence - fsync 失败 → write-stopped #### 1C-5: durableSequence 推进逻辑 - 每个 batch 记录 `(segmentID, endOffset, endSequence)` - fsync snapshot 后只推进满足条件的最大连续 batch - 跨 segment 推进需要 segment 已 durable-ready #### 1C-6: Write-Stopped 状态管理 - `atomic.Bool` 存储 writeStopped - 进入 write-stopped 后拒绝新写入 - 已存在的 MemTable / Immutable MemTable 可继续后台处理 - 提供 `IsWriteStopped() bool` 查询接口 ### 验收标准 - [ ] Group commit 正确合并多个写请求 - [ ] 双触发(时间/大小)工作正常 - [ ] Sequence 分配无溢出 - [ ] Always 模式下 publish 在 fsync 之后 - [ ] 错误分类准确(普通错误 / write-stopped / ErrCommitUnknown) - [ ] 并发写入正确(多 goroutine 同时 Put) - [ ] Write-stopped 后新写入被拒绝 --- ## Phase 1D: WAL Recovery **目标**: 实现 WAL 崩溃恢复,包括 segment 扫描、fragment 重组、batch 校验和尾部截断。 ### 任务 #### 1D-1: Segment 扫描器 (`wal/scanner.go`) - 从 WAL 目录扫描 segment 文件 - 按 segmentID 排序 - 从 MANIFEST 指定的 recoverySegmentID 开始 - 过滤掉 segmentID < recoverySegmentID 的旧 segment - 校验连续性:segmentID 和 startSequence 都必须连续 #### 1D-2: Physical Record 解析器 (`wal/record_parser.go`) - Block 级别的顺序解析 - 处理 padding(全 0 校验) - Physical Record header 解析和 CRC 校验 - 错误分类:尾部 vs 中间损坏 #### 1D-3: Fragment 重组器 (`wal/fragment_collector.go`) - 实现 Idle / CollectingFragments 状态机 - 收集 First / Middle / Last fragments - Buffer 大小限制(Batch Header 长度 + entriesSize 上限) - Fragment 顺序合法性检查 #### 1D-4: Batch 校验与重放 (`wal/recovery.go`) - Batch Header 校验:flags、entryCount、entriesSize - Batch sequence 连续性:batch.baseSequence == expectedSequence - Entry 逐条校验:opType、valueKind、keyLen、valLen - 重放回调:对每个合法 entry 调用 replay 函数 - Sequence 推进:expectedSequence += entryCount #### 1D-5: 尾部截断持久化 (`wal/truncation.go`) - 识别最后一个完整 batch 的结束位置 - ftruncate segment 文件 - fsync 被截断的 segment - 删除不含任何 complete batch 的后续空 segment - fsync WAL directory - 任一步失败 → recovery 报错 #### 1D-6: Recovery 主流程 (`wal/recovery.go`) ``` 1. 读取 MANIFEST → recoverySegmentID 2. 扫描 WAL 目录 → 过滤出 recovery segments 3. 排序并校验连续性 4. 逐 segment 扫描: a. 校验 File Header b. 逐 Block 解析 Physical Records c. Fragment 重组 → 完整 Batch d. Batch 校验 → 重放 e. 更新 expectedSequence 5. 处理尾部异常 6. 持久化截断(如需要) 7. 返回恢复结果:recoveredSequence, nextSequence, publishedSequence ``` ### 验收标准 - [ ] 正常 WAL 完整恢复 - [ ] 尾部 partial write 正确截断 - [ ] 中间损坏正确报错 - [ ] 跨 segment 恢复正确 - [ ] Fragment 重组 round-trip 正确 - [ ] Segment 连续性校验 - [ ] Sequence 溢出检测 - [ ] 资源限制校验(recovery 侧) --- ## Phase 1E: MemTable(SkipList + Arena) **目标**: 实现基于 Arena 的 SkipList,支持 pending/unpublished/aborted 状态,容量预留,和原子发布。 ### 任务 #### 1E-1: Arena 分配器 (`memtable/arena.go`) - 固定大小 Arena(默认 64MB) - 线程安全的内存分配 - 对齐分配 - 剩余容量查询 - 支持预留(reserve)操作 #### 1E-2: SkipList (`memtable/skiplist.go`) - 最大 20 层 - Mutex 写 + 无锁读 - `atomic.Pointer` 发布 next 指针(release 语义) - 有序遍历(Iterator) - key 比较(bytes comparison) #### 1E-3: Entry 状态管理 (`memtable/entry.go`) - Entry 结构:key、value、sequence、pending/aborted 标记 - 原子发布:`atomic.Pointer` store-release - 可见性判断:`entry.sequence <= loadedPublishedSequence && !aborted` #### 1E-4: MemTable (`memtable/memtable.go`) - 封装 SkipList + Arena - `Put(key, value, sequence) error` — 写入 pending entry - `PublishEntries(upToSequence)` — 批量发布 pending entries - `AbortEntries(fromSequence)` — 标记 aborted - `Get(key, publishedSequence) (GetResult, error)` — 无锁读,只返回 sequence <= publishedSequence 且非 aborted 的 entry - `NewIterator(publishedSequence) Iterator` — 无锁有序遍历 - `ApproximateSize() uint64` — 近似内存使用量 - `IsFull() bool` - `Reserve(entries []ReserveEntry) (uint64, error)` — 容量预留(最坏情况计算) #### 1E-5: 容量预留计算 - 每个 entry 的预留大小 = key bytes + value bytes + skiplist node overhead + next 指针数组(最大层高)+ arena 对齐 padding - 批量预留必须覆盖整个 batch - checked arithmetic 检查单个 batch 是否超过空 MemTable 容量 #### 1E-6: Immutable MemTable 管理 - Freeze 流程:当前 MemTable → Immutable - Immutable 队列(上限 2) - 队列满时阻塞 ### 验收标准 - [ ] SkipList 正确性:插入、查找、有序遍历 - [ ] Arena 分配无泄漏 - [ ] 并发读写正确(racetest) - [ ] pending/unpublished entry 对读不可见 - [ ] 发布后 entry 可见 - [ ] aborted entry 对读不可见 - [ ] 容量预留准确 - [ ] 内存序正确(go test -race 通过) --- ## Phase 1F: 写入路径集成 **目标**: 将 WAL Writer 和 MemTable 连通,实现完整的写入流水线。 ### 任务 #### 1F-1: DB 写入 API (`db.go`) ```go type DB struct { ... } func Open(dir string, opts ...Option) (*DB, error) func (db *DB) Close() error func (db *DB) Put(key, value []byte) error func (db *DB) Delete(key []byte) error ``` #### 1F-2: 写入路径集成 完整写入流程: ``` Put(key, value) → commit queue → group commit 组装 batch → 校验 batch limits → 预留 MemTable Arena → 分配 sequence → 私有缓冲编码 → 检查 segment 轮转 → WAL append → MemTable pending write → fsync(Always 模式) → 原子发布 MemTable entries → 推进 publishedSequence → 唤醒调用方 ``` #### 1F-3: MemTable Freeze + Switch - 写入前检查容量,不足时 freeze + switch - Immutable 队列满时阻塞等待 - Freeze 时确保当前 MemTable 已完成所有 pending 发布 #### 1F-4: 恢复启动集成 - Open 时执行 recovery - 恢复的 entries 写入 MemTable 并标记为 published - 设置 nextSequence、publishedSequence ### 验收标准 - [ ] 单条 Put 写入成功 - [ ] 并发 Put 正确 - [ ] 写入后读取可见(Always 模式) - [ ] WAL crash recovery 后数据完整 - [ ] MemTable freeze/switch 正确 - [ ] Sequence 连续无间隙 - [ ] `go test -race` 通过 --- ## Phase 1G: 读路径 + 嵌入式 API **目标**: 实现完整的读路径和嵌入式 API。 ### 任务 #### 1G-1: Get API ```go type GetResult struct { Value []byte Found bool } func (db *DB) Get(key []byte) (GetResult, error) ``` #### 1G-2: 读路径实现 - 读取 publishedSequence(atomic load) - 遍历 MemTable → Immutable MemTables - 只返回 `sequence <= publishedSequence` 且非 aborted 的 entry - Delete (tombstone) 返回 `Found=false` #### 1G-3: 辅助 API ```go func (db *DB) GetDurableSequence() uint64 func (db *DB) IsWriteStopped() bool ``` ### 验收标准 - [ ] Put 后 Get 返回正确值 - [ ] Delete 后 Get 返回 Found=false - [ ] 空 value 正确区分(Found=true, Value=[]byte{}) - [ ] 并发读写正确 - [ ] 未发布 entry 对 Get 不可见 --- ## Phase 1H: MANIFEST + 文件管理 **目标**: 实现 MANIFEST 持久化和 WAL segment 生命周期管理。 ### 任务 #### 1H-1: MANIFEST 格式 - 记录 recovery 起点的 recoverySegmentID - temp + rename 原子更新 - MANIFEST 只在 checkpoint(MemTable flush)后推进 #### 1H-2: 首次创建 DB 流程 - 创建目录结构 - 创建初始 MANIFEST(recoverySegmentID=0) - 创建初始 WAL segment #### 1H-3: WAL Segment 生命周期 - 旧 segment 删除条件:已被 MANIFEST checkpoint 覆盖 - 删除顺序:先删除文件,再 fsync directory ### 验收标准 - [ ] 首次创建 DB 成功 - [ ] 重复打开 DB 正确恢复 - [ ] MANIFEST 原子更新 - [ ] 旧 WAL segment 正确清理 --- ## Phase 1I: 集成测试 + Benchmark **目标**: 端到端测试和性能基准。 ### 任务 #### 1I-1: 集成测试 - 正常写入 + 读取 round-trip - 并发写入 + 读取一致性 - 崩溃恢复(kill -9 模拟) - WAL 尾部损坏恢复 - Write-stopped 后的行为 - Sequence 耗尽处理 - 配置校验拒绝非法配置 - 空 value 写入/读取 - 大量数据写入(触发 segment 轮转) #### 1I-2: Benchmark - 单线程 Put 吞吐 - 多线程 Put 吞吐 - 单线程 Get 延迟 - 多线程 Get 延迟 - WAL Recovery 时间 - 写入放大测量 #### 1I-3: Race Condition 测试 - `go test -race -count=100` - 并发 Put + Get - 并发 Put + Close ### 验收标准 - [ ] 所有集成测试通过 - [ ] Benchmark 数字可作为后续优化基线 - [ ] Race test 无 data race --- ## 依赖关系与并行度 ``` 1A ─────┐ ├── 1B ─────┐ │ ├── 1C ─────┐ │ │ ├── 1F ── 1G ── 1I │ │ │ 1A ── 1E ──────────┘ │ │ │ ├── 1D ─────────────────┘ │ └── 1H ────────────────────────────── 1I ``` 可并行开发的模块: - 1A 完成后,1B/1D/1E/1H 可以并行开发 - 1B 完成后,1C 可以开始 - 1C + 1D + 1E 完成后,1F 可以集成 - 1F + 1H 完成后,1G 可以集成 - 所有完成后,1I 集成测试 ## 技术要点备忘 ### 内存序(最关键) - skiplist next 指针:`atomic.Pointer` store-release - `publishedSequence`:`atomic.Uint64` store(在所有 entry 节点发布后) - 读者先 load publishedSequence(acquire),再遍历 skiplist ### WAL 副作用边界 - 未调用 `write()` → 普通错误 - 已调用 `write()` → ErrCommitUnknown + write-stopped - 私有缓冲区编码,不共享 bufio.Writer ### ErrCommitUnknown 语义 - maybe committed,不是 definitely failed - 不盲目重试 - 第一阶段为弱确认 ### WAL Batch 资源前置校验 - sequence 分配之前完成所有可失败校验 - 减少 write-stopped 触发机会