From 5c3adb33909548447f9188a8771c038dfb32b5fa Mon Sep 17 00:00:00 2001 From: dailz Date: Sat, 6 Jun 2026 22:18:48 +0800 Subject: [PATCH] design doc --- docs/design.md | 785 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 785 insertions(+) create mode 100644 docs/design.md diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..12d3f9c --- /dev/null +++ b/docs/design.md @@ -0,0 +1,785 @@ +# go-kv 存储引擎设计文档 + +## 1. 项目定位 + +### 1.1 目标 + +构建一个高性能、功能完善的 KV 存储引擎,使用 Go 语言实现。 + +### 1.2 聚焦方向 + +- **写吞吐极致** — 写入路径优化到极致,这是核心竞争力 +- **读性能不拖后腿** — 不追 B+Tree 的延迟,但不应比同类 LSM 引擎差超过 20% + +### 1.3 具体数字目标 + +- 待引擎跑出 benchmark 后确定 +- 方向:持久化写吞吐尽可能高,非持久化写 20 万 ops/s 量级 + +### 1.4 架构风格 + +- **架构简洁** — 以最少的代码实现最强的性能 +- **可配置** — 关键参数可调,不同场景灵活适配 + +### 1.5 功能范围 + +#### 包含 + +- 有序 KV(LSM-Tree) +- 范围扫描(Range Scan) +- 可串行化事务,多写者并发(SSI) +- MVCC 内置在存储引擎 +- KV 分离(小值内联,大值走 Value Log) +- 嵌入模式(Go 库) +- 网络层(自定义 TCP 二进制协议)— 后续 + +#### 不包含(后续扩展) + +- Watch / 变更通知 +- TTL / 过期机制 +- 快照 / 备份 +- Redis 数据类型(String/List/Hash/Set) +- 分布式 + +--- + +## 2. 整体架构 + +``` +┌─────────────────────────────────────────────┐ +│ 嵌入式 API / 网络层(后续) │ +├─────────────────────────────────────────────┤ +│ 事务层(MVCC + SSI) │ +├─────────────────────────────────────────────┤ +│ 存储引擎核心 │ +│ │ +│ 写入路径: │ +│ Client → WAL → MemTable → Immutable │ +│ MemTable → SSTable │ +│ │ +│ 读取路径: │ +│ Client → MemTable → Immutable → SSTable │ +│ │ +│ 后台任务: │ +│ Compaction + Value Log GC │ +│ │ +│ 加速组件: │ +│ Bloom Filter + Block Cache │ +├─────────────────────────────────────────────┤ +│ Value Log(大值分离) │ +├─────────────────────────────────────────────┤ +│ 文件系统(SSTable / WAL / VLog) │ +└─────────────────────────────────────────────┘ +``` + +--- + +## 3. 存储引擎核心 + +### 3.1 底层结构 + +**LSM-Tree(Log-Structured Merge Tree)** + +选择理由: +- MVCC 多版本:追加写天然支持,B+Tree 需要在页面内维护版本链 +- 多写者并发:写入路径简单(WAL → 内存),无需复杂的页面级锁协议 +- KV 分离:SSTable 不可变,compaction 只搬 key 和指针,不搬大 value +- 高写吞吐:顺序写 WAL + 追加写 Value Log,避免随机写 + +--- + +### 3.2 WAL(预写日志) + +**职责:** 崩溃恢复。写入数据前先追加到日志文件,确保数据不丢失。 + +#### 持久化策略 + +- 默认采用 **durable group commit**:多条写入合并为一个 WAL Batch,Batch 写入并 fsync 成功后,Batch 内每条写入才分别返回成功 +- 默认语义下,`Put` 返回成功表示该写入所在 WAL Batch 已经持久化;进程崩溃或机器掉电后,可通过 WAL 恢复 +- 提供配置项允许用户显式选择异步刷盘;异步模式追求吞吐,但允许丢失最近尚未 fsync 的写入 + +#### 写入流程 + +``` +① 写请求进入 commit queue +② WAL writer 收集当前队列中的多条写入,组成 WAL Batch +③ 等待组提交触发(500µs 或 32KB,先到者触发) +④ 为 Batch 内每条写入分配递增的 sequence number +⑤ 编码 WAL Batch,并拆分为一个或多个 Physical Record 写入 WAL 文件 +⑥ 写入 MemTable,但标记为 pending / unpublished +⑦ WAL Batch fsync 落盘 +⑧ 发布 publishedSequence,使 Batch 内写入对普通读可见 +⑨ 唤醒 Batch 内每个调用方,分别返回客户端"写入成功" +``` + +默认模式下,写入成功发生在 WAL fsync 之后;但 fsync 的粒度是 WAL Batch,不是单条 `Put`。因此多条写入共享一次 fsync 成本,同时每条写入仍然只有在自身所在 WAL Batch 持久化后才算成功。 + +如果 WAL write 失败或 WAL bytes 未完整写入,该 WAL Batch 不发布;对应 pending entries 保留在 MemTable 中,但保持 unpublished / aborted 状态,不做原地删除,Batch 内调用方返回错误。由于 sequence 已经分配且不复用,引擎必须进入 write-stopped 状态,不再接受新写入,直到关闭并完成恢复;否则 `publishedSequence` 无法跳过失败 Batch 推进,后续成功 Batch 也会因为 sequence 空隙而不可见。这样避免在无锁读和 Arena / SkipList 分配模型下引入并发删除与内存回收复杂度,同时保持 `publishedSequence` 始终是连续 high-water mark。 + +如果 WAL bytes 已写入但 fsync 返回错误,该 WAL Batch 在当前运行期仍不发布、不确认成功,Batch 内调用方返回 `ErrCommitUnknown`,而不是普通失败错误;因为它的持久化状态是 unknown,而不是 definitely lost。sequence 一旦分配不复用;引擎进入 write-stopped 状态,不再接受新写入,直到关闭并完成恢复。因此 `publishedSequence` 始终保持连续 high-water mark,普通读仍可使用 `sequence <= publishedSequence` 判断可见性,失败 Batch 的 entries 在当前运行期永远不会对普通读可见。重启后,如果 recovery 在 WAL 中发现该 Batch 完整、CRC 合法且 sequence 连续,可以按正常 WAL 规则重放。 + +`ErrCommitUnknown` 表示提交结果不确定:调用方不能把它当作“写入一定失败”并盲目重试。恢复完成后,调用方必须通过读取 key 或后续事务层提供的事务 ID / commit record 查询提交状态,再决定是否重试。后续 MVCC / SSI 事务层必须为事务提交提供幂等标识,避免 fsync 不确定结果导致非幂等事务重复提交。 + +#### 可见性语义 + +- `publishedSequence` 表示普通读的逻辑可见 high-water mark;普通读可以读取 MemTable,但只返回 `sequence <= publishedSequence` 的 entry +- fsync 前,写入可以已经存在于 MemTable 中,但处于 pending / unpublished 状态,仅供内部提交流程使用,对普通读不可见 +- `Always` 默认策略下,`publishedSequence` 也是 durable high-water mark;普通读能读到的数据,必须是崩溃恢复后仍可恢复的数据 +- `Periodic` / `Never` 策略下,`publishedSequence` 可以领先于 durable high-water mark;普通读能读到当前进程内已发布的数据,但这些数据不保证机器掉电后仍可恢复 +- 如果 WAL Entry 引用外部持久化对象(例如 Value Log record),发布 WAL Batch 前,被引用对象也必须满足当前落盘策略对应的持久化要求;在 `Always` 下这意味着被引用对象必须已持久化,避免恢复后出现悬空指针 +- 该模型为后续 MVCC / 事务层提供统一的 read timestamp / commit sequence 基础 +- `Always` 下该选择牺牲的是写入进入 MemTable 后到 fsync 发布前的短暂全局可见性延迟,不是已发布数据的读路径性能 + +#### WAL 落盘策略 + +| 策略 | 行为 | 语义 | +| ---------------- | ------------------------------------------ | ------------------------------ | +| `Always`(默认) | WAL Batch fsync 后发布 sequence 并返回成功 | 不丢已确认写入 | +| `Periodic` | 按固定时间间隔 fsync,可提前发布 sequence | 崩溃可能丢失最近一段已确认写入 | +| `Never` | 不主动 fsync,仅依赖 OS page cache | 吞吐最高,崩溃风险最大 | + +非默认落盘策略必须由用户显式开启。内部仍使用同一套 sequence / publish 机制,区别只在于 `publishedSequence` 是在 fsync 后推进,还是在 WAL write 成功后提前推进。`Periodic` / `Never` 下,已经发布并返回成功的写入仍可能在机器掉电后丢失。 + +#### 设计决策 + +| 决策 | 选择 | 理由 | +| -------- | ---------------------------------- | --------------------------------------------------------------------------- | +| 批量窗口 | 时间 + 大小双触发(500µs 或 32KB) | 高负载靠大小触发效率最大化,低负载靠时间触发不会卡住 | +| 日志格式 | 固定 Block(32KB) | 恢复时按 block 读取校验,比逐条快;和 SSTable block 设计一致 | +| 文件管理 | 分段日志 | 精确删除已刷盘的旧 WAL 段;恢复可并行读多文件;CURRENT 文件指向当前活跃 WAL | + +#### WAL Block 格式 + +WAL 使用分层格式: + +```text +WAL Segment File +├── WAL File Header +└── Block (32KB) + └── Physical Record + └── WAL Batch + └── Entry +``` + +术语定义: + +| 名称 | 含义 | +|------|------| +| Entry | 一条用户写操作,例如 Put / Delete | +| WAL Batch | 一次 group commit 的逻辑提交单元,包含多条 Entry | +| Physical Record | WAL 文件中的物理片段,负责承载 WAL Batch 的完整或部分 bytes | +| Block | 固定 32KB 的顺序读写容器,Physical Record 必须完整位于单个 Block 内 | + +除 varint 字段外,WAL 中固定宽度整数统一使用 little-endian 编码。Block 区域从 `headerSize` offset 开始,Block offset 相对于 Block 区域计算。 + +##### WAL File Header + +每个 WAL segment 文件开头写入 32 bytes header: + +```text +┌────────────────┬──────┐ +│ magic │ u32 │ +│ formatVersion │ u16 │ +│ headerSize │ u16 │ +│ blockSize │ u32 │ +│ segmentID │ u64 │ +│ startSequence │ u64 │ +│ headerCRC │ u32 │ +└────────────────┴──────┘ +``` + +字段说明: + +| 字段 | 含义 | +|------|------| +| magic | 文件类型标识,用于确认这是 go-kv WAL segment | +| formatVersion | WAL 文件格式版本,第一版为 1 | +| headerSize | Header 长度,第一版为 32;后续扩展时可跳过新增字段 | +| blockSize | WAL Block 大小,第一版为 32KB | +| segmentID | WAL segment 编号,用于恢复时校验文件顺序 | +| startSequence | 当前 segment 理论上的起始全局 sequence | +| headerCRC | 校验 `magic` 到 `startSequence` 字段,不包含 `headerCRC` 自身,避免 header 损坏后误解析 | + +##### Physical Record + +Block 只是固定大小容器,CRC 放在每个 Physical Record 上,而不是放在 Block 尾部。 + +``` +Physical Record +┌────────┬────────┬──────┬─────────┐ +│ crc32c │ length │ type │ payload │ +│ u32 │ u16 │ u8 │ bytes │ +└────────┴────────┴──────┴─────────┘ +``` + +字段说明: + +| 字段 | 含义 | +|------|------| +| crc32c | 校验 `length + type + payload`,用于识别 torn write、partial write 和数据损坏 | +| length | payload 长度;使用 u16,因为单个 Physical Record 不跨 32KB Block | +| type | fragment 类型 | +| payload | WAL Batch 的完整 bytes 或部分 fragment bytes | + +fragment 类型(u8): + +| type | 名称 | 含义 | +|------|------|------| +| 0 | Invalid | 非法值,用于损坏检测 | +| 1 | Full | 一个完整 WAL Batch | +| 2 | First | 跨多个 Physical Record 的 WAL Batch 的第一个 fragment | +| 3 | Middle | 中间 fragment,可重复出现任意多次 | +| 4 | Last | 最后一个 fragment | + +完整 WAL Batch 的 Physical Record 组合只有两种合法形式: + +```text +Full +First + Middle* + Last +``` + +Physical Record 的顺序由 WAL 文件的顺序追加和顺序扫描保证,不额外存储 fragment index。 + +##### Block 边界处理 + +- Physical Record 必须完整位于单个 Block 内,不能跨 Block +- WAL Batch 可以拆成多个 Physical Record,从而跨多个 Block +- 如果当前 Block 剩余空间 `<= 7 bytes`,写入 zero padding 到 Block 末尾,并从下一个 Block 开始写新的 Physical Record +- 如果当前 Block 剩余空间 `> 7 bytes`,可以写入一个 Physical Record +- Physical Record 的 `length` 必须 `> 0` +- 读取时,Block 尾部 padding 必须全为 0;如果 padding 区出现非 0 bytes,视为 WAL 损坏 + +##### WAL Batch + +WAL Batch 是逻辑提交单元,对应一次 group commit batch。恢复时必须拼出完整 WAL Batch 后才能重放,不能重放半个 batch。 + +```text +WAL Batch +┌──────────────┬─────────┐ +│ Batch Header │ Entries │ +└──────────────┴─────────┘ +``` + +Batch Header: + +```text +┌────────┬──────────────┬────────────┬─────────────┐ +│ flags │ baseSequence │ entryCount │ entriesSize │ +│ u16 │ u64 │ u32 │ u32 │ +└────────┴──────────────┴────────────┴─────────────┘ +``` + +字段说明: + +| 字段 | 含义 | +|------|------| +| flags | batch 扩展标记,第一版为 0;后续可用于压缩、加密、事务标记、Value Log pointer 等 | +| baseSequence | 当前 batch 第一条 Entry 的全局 sequence | +| entryCount | 当前 batch 内 Entry 数量 | +| entriesSize | Entries 区域总字节数,不包含 Batch Header,用于解析边界校验 | + +Entry 的 sequence 由 batch 内位置推导: + +```text +entry[i].sequence = baseSequence + i +``` + +##### Entry + +Entry 使用 row-based 编码,每条 Entry 自带自己的 key/value 长度。`opType` 表示操作语义,`valueKind` 表示 value 的存储形态。 + +```text +Entry +┌────────┬───────────┬────────┬────────┬─────┬───────┐ +│ opType │ valueKind │ keyLen │ valLen │ key │ value │ +│ u8 │ u8 │ varint │ varint │ ... │ ... │ +└────────┴───────────┴────────┴────────┴─────┴───────┘ +``` + +第一版 OpType: + +| opType | 含义 | +|--------|------| +| 0 | Invalid,非法值,用于损坏检测 | +| 1 | Put | +| 2 | Delete | + +第一版 ValueKind: + +| valueKind | 含义 | +|-----------|------| +| 0 | None,用于 Delete | +| 1 | Inline,value 字段存用户 value bytes | +| 2 | ValueLogPointer,value 字段存 encoded Value Log pointer | + +合法性规则: + +- `keyLen > 0` +- `Put` 要求 `valueKind ∈ {Inline, ValueLogPointer}` +- `Put + Inline` 允许 `valLen = 0`,表示 key 存在且 value 为空 bytes +- `Put + ValueLogPointer` 的 value 字段存 opaque encoded pointer bytes,具体格式由 Value Log 模块定义 +- `Delete` 编码为 tombstone,要求 `valueKind = None`、`valLen = 0` 且 value 为空 +- Delete 恢复到 MemTable 后写入 tombstone,而不是直接移除 key + +##### WAL Recovery 规则 + +恢复目标是恢复到最后一个完整、CRC 校验通过、sequence 连续的 WAL Batch。恢复过程不能重放半个 batch;尾部半写可以截断,中间损坏必须报错。 + +###### Recovery 扫描流程 + +```text +1. 从 MANIFEST 指定的 recovery 起点开始,按 segmentID 从小到大打开 WAL segment +2. 校验 WAL File Header:magic、formatVersion、headerSize、blockSize、segmentID、headerCRC +3. 对 recovery 起点 segment,使用 segment.startSequence 初始化 expectedSequence +4. 对后续 segment,校验 segment.startSequence == expectedSequence +5. 从 File Header 之后开始按 Block 顺序扫描 +6. 在每个 Block 内按 Physical Record 顺序解析 +7. 将 Full 或 First/Middle/Last 拼成完整 WAL Batch +8. 校验并重放 WAL Batch 中的 Entries +9. 更新 expectedSequence,并继续扫描下一个 batch 或下一个 segment +``` + +###### Segment 连续性校验 + +从 MANIFEST 指定的 recovery 起点开始,多 segment 恢复必须同时校验 `segmentID` 和 `startSequence` 的连续性: + +```text +expectedSegmentID = manifest.recoverySegmentID +expectedSequence = recoveryStartSegment.startSequence + +for segment in segmentID ascending order: + require segment.segmentID == expectedSegmentID + require segment.startSequence == expectedSequence + + recover all complete batches in segment + + expectedSegmentID += 1 + expectedSequence = next sequence after last recovered batch +``` + +如果 segmentID 不连续、文件名与 header 中的 segmentID 不匹配,或后续 segment 的 `startSequence` 不等于上一 segment 恢复后的 `expectedSequence`,说明 WAL segment 缺失、乱序或损坏,默认报错。 + +已被 MANIFEST 证明不再需要的旧 WAL segment 可以删除,不参与连续性校验;连续性要求只适用于 recovery 起点之后仍需恢复的 WAL segment。 + +###### Physical Record 解析规则 + +解析器在 Block 内顺序读取 Physical Record: + +```text +while blockRemaining >= 7: + read crc32c, length, type + if header 全 0: + 要求当前 Block 剩余 bytes 全 0 + 跳到下一个 Block + + require length > 0 + require length <= blockRemaining - 7 + read payload + verify crc32c(length + type + payload) +``` + +当 `blockRemaining < 7` 时,剩余 bytes 必须全为 0,然后进入下一个 Block。 + +错误分类: + +这里的 “WAL 尾部” 有严格定义:只指最后一个需要恢复的 WAL segment 的物理 EOF 附近。非最后恢复 segment 中的 header 半写、`length` 越界、CRC 错误、incomplete batch 或非法 padding,即使发生在该 segment 文件尾,也视为 WAL 中间损坏。 + +| 情况 | 位于 WAL 尾部 | 位于 WAL 中间 | +|------|---------------|---------------| +| header 半写 | 丢弃尾部并截断 | 报错 | +| `length` 越界 | 丢弃尾部并截断 | 报错 | +| CRC 校验失败 | 丢弃尾部并截断 | 报错 | +| padding 区出现非 0 bytes | 丢弃尾部并截断 | 报错 | + +尾部损坏通常来自进程崩溃或机器掉电时最后一次写入的 partial write。若后面还有需要恢复的 segment,前一个 segment 的尾部异常不能按尾部损坏截断,否则可能跳过已经持久化历史并破坏 sequence 连续性。中间损坏说明已持久化 WAL 文件被破坏,默认不静默跳过,不默认 repair。 + +###### Fragment 状态机 + +恢复器维护两个状态: + +```text +Idle +CollectingFragments +``` + +状态转移: + +| 当前状态 | 输入 type | 行为 | +|----------|-----------|------| +| Idle | Full | 解析并重放该完整 WAL Batch | +| Idle | First | 开始收集 fragment,进入 CollectingFragments | +| Idle | Middle | 非法 fragment 顺序 | +| Idle | Last | 非法 fragment 顺序 | +| CollectingFragments | Middle | 追加 payload 到当前 fragment buffer | +| CollectingFragments | Last | 追加 payload,拼出完整 WAL Batch,解析重放后回到 Idle | +| CollectingFragments | Full | 非法 fragment 顺序 | +| CollectingFragments | First | 非法 fragment 顺序 | + +如果扫描到 WAL 尾部时仍处于 `CollectingFragments` 状态,说明最后一个 WAL Batch 未写完整,丢弃该 incomplete batch,并截断到最后一个完整 WAL Batch 结束位置。 + +###### WAL Batch 校验与重放 + +拼出完整 WAL Batch 后,先校验 Batch Header: + +```text +require flags 合法 +require entryCount > 0 +require entriesSize > 0 +require entriesSize == 实际 Entries bytes 长度 +require batch.baseSequence == expectedSequence +``` + +然后顺序解析 Entries: + +```text +for i in 0..entryCount: + sequence = batch.baseSequence + i + parse opType, valueKind, keyLen, valLen, key, value + require opType ∈ {Put, Delete} + require keyLen > 0 + require Entry 不越界 + + if opType == Put: + require valueKind ∈ {Inline, ValueLogPointer} + if valueKind == Inline: + valLen 可以为 0 + replay PutInline(key, value, sequence) + if valueKind == ValueLogPointer: + require valLen > 0 + require value 可由 Value Log 模块解析为 encoded pointer + replay PutPointer(key, value, sequence) + + if opType == Delete: + require valueKind == None + require valLen == 0 + replay Tombstone(key, sequence) +``` + +一个 WAL Batch 内所有 Entry 都校验通过后,才算该 batch 可重放。重放成功后: + +```text +expectedSequence += entryCount +lastCompleteBatchEnd = 当前 WAL offset +``` + +如果 Batch Header 或 Entry 解析错误发生在 WAL 尾部,可以丢弃该 incomplete batch;如果发生在 WAL 中间,默认报错。 + +###### 恢复完成状态 + +恢复完成后: + +```text +recoveredSequence = expectedSequence - 1 +nextSequence = expectedSequence +publishedSequence = recoveredSequence +``` + +因为 WAL 中恢复出来的数据都来自已经持久化的完整 batch,所以恢复后可以全部视为 published。 + +--- + +### 3.3 MemTable(内存表) + +**职责:** 内存中的有序索引,支持快速读写。 + +#### 设计决策 + +| 决策 | 选择 | 理由 | +| ---------- | ----------------- | ------------------------------------------------------------------------------------- | +| 数据结构 | 跳表(Skip List) | 实现简单(~200 行核心代码);节点大小固定,Arena 分配天然适配;BadgerDB/Pebble 已验证 | +| 最大层数 | 20 层 | 容量 ~10 亿条,额外内存开销极小 | +| Arena 大小 | 可配置,默认 64MB | 不同场景灵活调整;64MB 是 BadgerDB 验证过的安全值 | +| 并发策略 | Mutex 写 + 无锁读 | 写入已被 WAL 组提交串行化,无锁写优势用不上;读是并发的,无锁读有价值 | + +#### MemTable 生命周期 + +``` +MemTable (可写, 64MB) + ↓ 写满 +冻结为 Immutable MemTable (只读) + ↓ 后台线程 +刷盘为 SSTable 文件 + ↓ 刷盘完成 +释放 Immutable MemTable +``` + +#### Flush 过滤规则 + +MemTable 可能包含已写入内存但尚未发布的 pending entry,也可能包含 WAL write / fsync 失败后保留的 aborted entry。Flush 到 SSTable 时必须过滤这些 entry: + +- 只刷 `sequence <= publishedSequence` 且非 aborted 的 entry +- pending / unpublished entry 不进入 SSTable +- aborted entry 不进入 SSTable +- 失败 WAL Batch 中遗留在 MemTable 的 entry 只作为运行期内部状态存在,关闭后不会从 WAL 恢复 + +该规则保证失败写入不会因为 MemTable flush 进入持久化 SSTable,也保持无锁读场景下无需原地删除 skiplist / arena 节点。 + +#### 多 Immutable MemTable 策略 + +- 上限 2 个 Immutable MemTable 排队等刷盘 +- 超过上限时阻塞新写入 +- 大部分情况不阻塞,极端情况兜底 + +--- + +### 3.4 读路径 + +读取时按优先级查,从新到旧,找到第一个就返回: + +``` +读请求 + ↓ +1. 查 MemTable(最新的数据) + ↓ 没找到 +2. 查 Immutable MemTable #1 + ↓ 没找到 +3. 查 Immutable MemTable #2 + ↓ 没找到 +4. 查 SSTable(L0 → L1 → L2 → ...) +``` + +SSTable 层级的读取通过布隆过滤器加速,避免无谓的磁盘 I/O。 + +--- + +### 3.5 SSTable(Sorted String Table) + +**职责:** 磁盘上的有序不可变文件,存储 MemTable 刷盘后的数据。 + +#### 文件格式 + +``` +┌──────────────┐ +│ Data Block 0 │ ← key+value 有序排列,块内前缀压缩 +├──────────────┤ +│ Data Block 1 │ +├──────────────┤ +│ ... │ +├──────────────┤ +│ Index Block │ ← 每个 Data Block 的最小 key 和文件偏移量 +├──────────────┤ +│ Filter Block │ ← 布隆过滤器 +├──────────────┤ +│ Footer │ ← 指向 Index Block 和 Filter Block 的指针 +└──────────────┘ +``` + +#### 设计决策 + +| 决策 | 选择 | 理由 | +| ---------- | ------------------------------- | -------------------------------------- | +| Block 大小 | 16KB | 读取粒度适中;压缩效率好;缓存命中率高 | +| 刷盘策略 | 不阻塞写入,上限 2 个 Immutable | 大部分情况不阻塞,极端情况兜底 | + +--- + +### 3.6 Compaction(压实合并) + +**职责:** 后台合并多个 SSTable,去除重复 key、删除废弃值、整理成有序的新文件。 + +#### 层级结构 + +``` +L0: 刚刷下来的 SSTable,key 范围可能重叠,最多 8 个文件 +L1: Compaction 后,key 范围不重叠,总大小上限约 64MB +L2: 比 L1 大 10 倍,约 640MB +L3: 比 L2 大 10 倍,约 6.4GB +... +每层大小上限 = 上一层的 10 倍 +``` + +#### 触发条件 + +| 条件 | 阈值 | 行为 | +| ------------------ | --------------- | --------------------- | +| L0 文件数 ≥ 4 | 触发 Compaction | 后台开始 L0 → L1 合并 | +| L0 文件数 ≥ 8 | 阻塞写入 | 等待 Compaction 赶上 | +| 某层总大小超过上限 | 触发 Compaction | 该层 → 下一层合并 | + +#### 限速 + +- 默认限制 Compaction 使用 50% 磁盘带宽 +- 可配置 +- 保护前台读写延迟不受 Compaction 影响 + +--- + +### 3.7 布隆过滤器(Bloom Filter) + +**职责:** 快速判断 key 是否"肯定不在"某个 SSTable 里,跳过不必要的磁盘读取。 + +#### 设计决策 + +| 决策 | 选择 | 理由 | +| -------- | ----------- | ---------------------------------------------- | +| 哈希函数 | xxhash | 速度最快(~2 bytes/ns);RocksDB/Pebble 已验证 | +| 精度 | 10 bits/key | 误判率 ~0.8%,1000 次查询约误判 8 次,可接受 | +| 层级区分 | 所有层统一 | 保持简洁,后续如有需要再分 | + +--- + +### 3.8 Block Cache(块缓存) + +**职责:** 将热点 SSTable 数据块缓存在内存,避免重复磁盘读取。 + +#### 设计决策 + +| 决策 | 选择 | 理由 | +| -------- | --------------------------------------- | ------------------------------------------------------ | +| 缓存内容 | Data Block + Index Block + Filter Block | 缓存索引和过滤器后,点查可能完全不需要磁盘 I/O | +| 淘汰策略 | S3FIFO | 抗扫描污染(范围扫描不会挤掉热点数据);实现复杂度适中 | +| 分片 | 256 个分片 | 每个分片独立锁,并发度高 256 倍 | +| 大小 | 可配置,用户必须显式指定 | 根据可用内存灵活分配 | + +#### S3FIFO 队列结构 + +``` +小队列 (Small, ~10%) + → 新数据先进这里 + → 短时间被再次访问 → 晋升到主队列 + → 自然被淘汰 → 进入幽灵队列 + +主队列 (Main, ~80%) + → 存放经过验证的热点数据 + → 被淘汰时检查幽灵队列历史决定是否保留 + +幽灵队列 (Ghost, ~10%) + → 不存数据,只记录被淘汰 key 的指纹 + → 防止被扫描数据反复进入主队列 +``` + +--- + +### 3.9 Value Log(KV 分离) + +**职责:** 大值存储在独立的追加写文件中,SSTable 只存指针,降低 compaction 写放大。 + +#### 写入流程 + +``` +写入请求(key, value) + ↓ +value > 4KB? + ├── 是 → value 追加写入 Value Log,得到指针 [文件ID, 偏移, 长度] + │ 默认 Always 策略下,Value Log record 必须先 fsync + │ WAL Batch 记录 Put + ValueLogPointer,value 字段存 encoded pointer + │ MemTable 存:key → [vlog指针](pending / unpublished) + │ + └── 否 → WAL Batch 记录 Put + Inline,value 字段存用户 value bytes + MemTable 存:key → value(pending / unpublished) + ↓ +WAL Batch fsync 成功后发布 sequence,写入对普通读可见并返回成功 +``` + +#### 与 WAL 的崩溃一致性 + +Value Log 指针只有在被引用的 Value Log record 已满足当前落盘策略后,才能随 WAL Batch 发布。 + +默认 `Always` 策略下,顺序为: + +```text +1. append Value Log record +2. fsync Value Log record 所在文件 +3. 写入 WAL Batch(记录 key + vlog 指针) +4. fsync WAL Batch +5. 发布 sequence +6. 返回写入成功 +``` + +如果 WAL 已持久化但 Value Log record 未持久化,恢复时会得到指向不存在或半写 value 的悬空指针,因此默认模式必须禁止这种状态。`Periodic` / `Never` 策略可以放宽 fsync 时机,但必须同时放宽 WAL 和 Value Log 的崩溃保证,并明确可能丢失最近已发布写入。 + +WAL Entry 通过 `valueKind` 区分内联值和 Value Log 指针: + +```text +Put + Inline: + value = 用户 value bytes + +Put + ValueLogPointer: + value = opaque encoded ValueLogPointer bytes +``` + +第一版 ValueLogPointer 编码: + +```text +ValueLogPointer v1 +┌────────┬────────┬────────┬──────────┐ +│ fileID │ offset │ length │ valueCRC │ +│ u64 │ u64 │ u32 │ u32 │ +└────────┴────────┴────────┴──────────┘ +``` + +字段说明: + +| 字段 | 含义 | +|------|------| +| fileID | Value Log 文件编号 | +| offset | value record 在文件中的起始偏移 | +| length | value bytes 长度 | +| valueCRC | value bytes 校验值,用于读取时发现悬空、半写或损坏 value | + +ValueLogPointer v1 使用 little-endian 固定宽度编码,长度为 24 bytes。WAL 层只把它当作 opaque bytes 承载,具体解析和校验由 Value Log 模块负责。 + +#### 读取流程 + +``` +读请求 + ↓ +MemTable 找到 entry + ├── 内联 value → 直接返回 + └── vlog 指针 → 去 Value Log 文件读取实际 value +``` + +#### 设计决策 + +| 决策 | 选择 | 理由 | +| -------- | ------------------------------- | -------------------------------------------------- | +| 分离阈值 | 4KB | 小值内联保范围扫描性能,大值分离保 compaction 效率 | +| 文件格式 | 带校验的追加写 | CRC 校验防止磁盘损坏返回错误数据 | +| 文件大小 | 固定 128MB | GC 粒度固定可预期,足够大不频繁切换 | +| GC 策略 | 按文件存活率,低于 50% 触发重写 | 精准回收,只处理需要的文件 | + +#### Value Log 记录格式 + +``` +┌────────┬────────┬────────┬─────┬─────────┬────────┐ +│ CRC(4) │key_len │val_len │ key │ value │ CRC(4) │ +│ bytes │ varint│ varint│ │ │ bytes │ +└────────┴────────┴────────┴─────┴─────────┴────────┘ +``` + +#### Value Log GC 流程 + +``` +1. 统计每个 Value Log 文件的存活率 + 存活率 = 有效 value 数量 / 总 value 数量 + ↓ +2. 存活率 < 50% 的文件触发 GC + ↓ +3. 读取该文件中的有效 value,重写到新文件 + ↓ +4. 向 LSM 写入新的 pointer 版本 + ↓ +5. 等旧 pointer 被 compaction 淘汰后删除旧文件 +``` + +注意:SSTable 不可变,GC 不能原地更新 SSTable 中的旧指针。Value Log GC 通过写入新的 pointer 版本来更新引用,旧 pointer 由 compaction 按 MVCC / snapshot 规则淘汰。 + +Value Log GC 计算存活率时,只能把已发布且非 aborted 的 LSM entry 视为可达引用: + +- `sequence > publishedSequence` 的 pending / unpublished pointer 不计入存活 value +- aborted pointer 不计入存活 value +- 失败 WAL Batch 遗留在 MemTable 中的 pointer 永不进入 SSTable,也不参与 Value Log GC 可达性计算 +- GC 判断 value 是否有效时,必须以 LSM 当前已发布版本和后续 MVCC / snapshot 规则为准 + +--- + +## 4. 待设计模块 + +以下模块尚未讨论,将在后续补充: + +- **MVCC + 事务(SSI)** — 多版本并发控制,可串行化隔离级别 +- **范围扫描** — 多层 Merge Iterator +- **网络层** — 自定义 TCP 二进制协议 +- **嵌入式 API** — Go 库接口设计 +- **崩溃恢复** — WAL 重放 + MANIFEST 恢复 +- **文件管理** — MANIFEST、CURRENT、文件生命周期