Files
go-kv/docs/phase1-wal-plan.md
T
dailz cf913b1d52 feat: initialize project structure and error types
- go.mod with github.com/dailz/go-kv, Go 1.26.3, testify
- config/config.go with WalConfig, Validate() with checked arithmetic
- errors.go with sentinel errors (ErrCommitUnknown, ErrWriteStopped, etc.)
- wal/constants.go with all WAL format constants and enums
- wal/header.go with WAL File Header encode/decode (CRC32 IEEE)
- wal/record.go with Physical Record codec, block boundary, SplitIntoRecords
- wal/entry.go with WAL Entry codec (varint keys/values, OpType, ValueKind)
- wal/sequence.go with SequenceManager (atomic, CAS, overflow-safe)
- manifest/manifest.go with MANIFEST stub (Load/Save atomic)
- manifest/current.go with CURRENT file (WriteCurrent/ReadCurrent)
- Comprehensive tests for all modules
- .golangci.yml configuration
2026-06-12 13:23:27 +08:00

21 KiB
Raw Blame History

Phase 1: WAL 子系统开发方案

基于 docs/design.md §3.2 设计文档。

目标

实现完整的 WAL(预写日志)子系统,使其能够支撑单 key autocommit 的写入、崩溃恢复和读可见性语义。

开发阶段总览

Phase 1A: 项目骨架 + WAL 编码格式层
Phase 1B: WAL 文件写入 + Segment 管理
Phase 1C: WAL WriterGroup Commit
Phase 1D: WAL Recovery
Phase 1E: MemTableSkipList + 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/      # MemTablePhase 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)

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 配置结构体:

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 打开时验证不变量:

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 WriterGroup 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. 分配 sequencebaseSequence
  7. 在私有缓冲区编码 WAL Batch
  8. 检查/触发 segment 轮转
  9. Append WAL Batch 到 segment 文件
  10. 写入 MemTablepending/unpublished
  11. fsyncAlways 模式)
  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 定期 fsyncwrite 成功即可 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: MemTableSkipList + 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)

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
  → fsyncAlways 模式)
  → 原子发布 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

type GetResult struct {
    Value []byte
    Found bool
}

func (db *DB) Get(key []byte) (GetResult, error)

1G-2: 读路径实现

  • 读取 publishedSequenceatomic load
  • 遍历 MemTable → Immutable MemTables
  • 只返回 sequence <= publishedSequence 且非 aborted 的 entry
  • Delete (tombstone) 返回 Found=false

1G-3: 辅助 API

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 只在 checkpointMemTable flush)后推进

1H-2: 首次创建 DB 流程

  • 创建目录结构
  • 创建初始 MANIFESTrecoverySegmentID=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
  • publishedSequenceatomic.Uint64 store(在所有 entry 节点发布后)
  • 读者先 load publishedSequenceacquire),再遍历 skiplist

WAL 副作用边界

  • 未调用 write() → 普通错误
  • 已调用 write() → ErrCommitUnknown + write-stopped
  • 私有缓冲区编码,不共享 bufio.Writer

ErrCommitUnknown 语义

  • maybe committed,不是 definitely failed
  • 不盲目重试
  • 第一阶段为弱确认

WAL Batch 资源前置校验

  • sequence 分配之前完成所有可失败校验
  • 减少 write-stopped 触发机会