- 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
21 KiB
21 KiB
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-stoppedErrSequenceExhausted— 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, headerCRCEncodeWalHeader(h *WalFileHeader) [WalFileHeaderSize]byteDecodeWalHeader(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, PayloadEncodePhysicalRecord(recType uint8, payload []byte) []byte— 返回编码后的 bytesDecodePhysicalRecord(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, EntriesEncodeWalBatch(batch *WalBatch) ([]byte, error)— 编码 Batch Header + EntriesDecodeWalBatch(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, ValueEncodeEntry(e *WalEntry) ([]byte, error)— 编码为 varint 长度 + bytesDecodeEntry(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() errorRemainingPayload() 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 协议:
- create segment-N+1.wal.tmp
- write WAL File Header(含 startSequence = nextExpectedSequence)
- fsync segment-N+1.wal.tmp
- rename → segment-N+1.wal
- fsync WAL directory
- 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 sequenceatomic.Uint64存储 nextSequence、publishedSequence、durableSequenceAllocateBatch(count uint32) (baseSequence uint64, err error)— checked arithmetic 检查溢出Publish(sequence uint64)— release 语义 store publishedSequenceMarkDurable(snapshot SegmentEndState)— 推进 durableSequencePublished() uint64— load publishedSequenceDurable() 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 / NeverAlways: 每次 batch fsync 后再 publishPeriodic: 后台 goroutine 定期 fsync,write 成功即可 publishNever: 不主动 fsyncPeriodic的 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.Pointerstore-release - 可见性判断:
entry.sequence <= loadedPublishedSequence && !aborted
1E-4: MemTable (memtable/memtable.go)
- 封装 SkipList + Arena
Put(key, value, sequence) error— 写入 pending entryPublishEntries(upToSequence)— 批量发布 pending entriesAbortEntries(fromSequence)— 标记 abortedGet(key, publishedSequence) (GetResult, error)— 无锁读,只返回 sequence <= publishedSequence 且非 aborted 的 entryNewIterator(publishedSequence) Iterator— 无锁有序遍历ApproximateSize() uint64— 近似内存使用量IsFull() boolReserve(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
→ 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
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
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.Pointerstore-release publishedSequence:atomic.Uint64store(在所有 entry 节点发布后)- 读者先 load publishedSequence(acquire),再遍历 skiplist
WAL 副作用边界
- 未调用
write()→ 普通错误 - 已调用
write()→ ErrCommitUnknown + write-stopped - 私有缓冲区编码,不共享 bufio.Writer
ErrCommitUnknown 语义
- maybe committed,不是 definitely failed
- 不盲目重试
- 第一阶段为弱确认
WAL Batch 资源前置校验
- sequence 分配之前完成所有可失败校验
- 减少 write-stopped 触发机会