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

641 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`)
```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 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`)
```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
```go
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
```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 只在 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
- `publishedSequence``atomic.Uint64` store(在所有 entry 节点发布后)
- 读者先 load publishedSequenceacquire),再遍历 skiplist
### WAL 副作用边界
- 未调用 `write()` → 普通错误
- 已调用 `write()` → ErrCommitUnknown + write-stopped
- 私有缓冲区编码,不共享 bufio.Writer
### ErrCommitUnknown 语义
- maybe committed,不是 definitely failed
- 不盲目重试
- 第一阶段为弱确认
### WAL Batch 资源前置校验
- sequence 分配之前完成所有可失败校验
- 减少 write-stopped 触发机会