- Core engine with MemTable, SST, WAL - B+Tree indexing for SST files - Leveled compaction strategy - Multi-table database management - Schema validation and secondary indexes - Query builder with complex conditions - Web UI with HTMX for data visualization - Command-line tools for diagnostics
10 KiB
10 KiB
CLAUDE.md
本文件为 Claude Code (claude.ai/code) 提供在本仓库中工作的指导。
项目概述
SRDB 是一个用 Go 编写的高性能 Append-Only 时序数据库引擎。它使用简化的 LSM-tree 架构,结合 WAL + MemTable + mmap B+Tree SST 文件,针对高并发写入(200K+ 写/秒)和快速查询(1-5ms)进行了优化。
模块: code.tczkiot.com/srdb
构建和测试
# 运行所有测试
go test -v ./...
# 运行指定包的测试
go test -v ./engine
go test -v ./compaction
go test -v ./query
# 运行指定的测试
go test -v ./engine -run TestEngineBasic
# 构建示例程序
go build ./examples/basic
go build ./examples/with_schema
架构
两层存储模型
与传统的多层 LSM 树不同,SRDB 使用简化的两层架构:
- 内存层: WAL + MemTable (Active + Immutable)
- 磁盘层: 带 B+Tree 索引的 SST 文件,分为 L0-L4+ 层级
核心数据流
写入路径:
- Schema 验证(如果定义了)
- 生成序列号 (
_seq) - 追加写入 WAL(顺序写)
- 插入到 Active MemTable(map + 有序 slice)
- 当 MemTable 超过阈值(默认 64MB)时,切换到新的 Active MemTable 并异步将 Immutable 刷新到 SST
- 更新二级索引(如果已创建)
读取路径:
- 检查 Active MemTable(O(1) map 查找)
- 按顺序检查 Immutable MemTables(从最新到最旧)
- 使用 mmap + B+Tree 索引扫描 SST 文件(从最新到最旧)
- 第一个匹配的记录获胜(新数据覆盖旧数据)
查询路径(带条件):
- 如果是带
=操作符的索引字段:使用二级索引 → 通过 seq 获取 - 否则:带过滤条件的全表扫描(MemTable + SST)
关键设计选择
MemTable: map[int64][]byte + sorted []int64
- 为什么不用 SkipList?实现更简单(130 行),Put 和 Get 都是 O(1) vs O(log N)
- 权衡:插入时需要重新排序 keys slice(但实际上仍然更快)
- Active MemTable + 多个 Immutable MemTables(正在刷新中)
SST 格式: 4KB 节点的 B+Tree
- 固定大小的节点,与 OS 页面大小对齐
- 支持高效的 mmap 访问和零拷贝读取
- 内部节点:keys + 子节点指针
- 叶子节点:keys + 数据偏移量/大小
- 数据块:Snappy 压缩的 JSON 行
mmap 而非 read() 系统调用
- 对 SST 文件的零拷贝访问
- OS 自动管理页面缓存
- 应用程序内存占用 < 150MB,无论数据大小
Append-only(无更新/删除)
- 简化并发控制
- 相同 seq 的新记录覆盖旧记录
- Compaction 合并文件并按 seq 去重(保留最新的,按时间戳)
目录结构
srdb/
├── database.go # 多表数据库管理
├── table.go # 带 schema 的表
├── engine/ # 核心存储引擎(583 行)
│ └── engine.go
├── wal/ # 预写日志
│ ├── wal.go # WAL 实现(208 行)
│ └── manager.go # 多 WAL 管理
├── memtable/ # 内存表
│ ├── memtable.go # MemTable(130 行)
│ └── manager.go # Active + Immutable 管理
├── sst/ # SSTable 文件
│ ├── format.go # 文件格式定义
│ ├── writer.go # SST 写入器
│ ├── reader.go # mmap 读取器(147 行)
│ ├── manager.go # SST 文件管理
│ └── encoding.go # Snappy 压缩
├── btree/ # B+Tree 索引
│ ├── node.go # 4KB 节点结构
│ ├── builder.go # B+Tree 构建器(125 行)
│ └── reader.go # B+Tree 读取器
├── manifest/ # 版本控制
│ ├── version_set.go # 版本管理
│ ├── version_edit.go # 原子更新
│ ├── version.go # 文件元数据
│ ├── manifest_writer.go
│ └── manifest_reader.go
├── compaction/ # 后台压缩
│ ├── manager.go # Compaction 调度器
│ ├── compactor.go # 合并执行器
│ └── picker.go # 文件选择策略
├── index/ # 二级索引
│ ├── index.go # 字段级索引
│ └── manager.go # 索引生命周期
├── query/ # 查询系统
│ ├── builder.go # 流式查询 API
│ └── expr.go # 表达式求值
└── schema/ # Schema 验证
├── schema.go # 类型定义和验证
└── examples.go # Schema 示例
运行时数据目录(例如 ./mydb/):
database_dir/
├── database.meta # 数据库元数据(JSON)
├── MANIFEST # 全局版本控制
└── table_name/ # 每表目录
├── schema.json # 表 schema
├── MANIFEST # 表级版本控制
├── wal/ # WAL 文件(*.wal)
├── sst/ # SST 文件(*.sst)
└── index/ # 二级索引(idx_*.sst)
常见模式
使用 Engine
Engine 是核心存储层。修改引擎行为时:
- 所有写入都经过
Insert()→ WAL → MemTable → (异步刷新到 SST) - 读取经过
Get(seq)→ 检查 MemTable → 检查 SST 文件 switchMemTable()创建新的 Active MemTable 并异步刷新旧的flushImmutable()将 MemTable 写入 SST 并更新 MANIFEST- 后台 compaction 通过
compactionManager运行
Schema 和验证
Schema 是可选的,但建议在生产环境使用:
schema := schema.NewSchema("users").
AddField("name", schema.FieldTypeString, false, "用户名").
AddField("age", schema.FieldTypeInt64, false, "用户年龄").
AddField("email", schema.FieldTypeString, true, "邮箱(索引)")
table, _ := db.CreateTable("users", schema)
- Schema 在
Insert()时验证类型和必填字段 - 索引字段(
Indexed: true)自动创建二级索引 - Schema 持久化到
table_dir/schema.json
Query Builder
对于带条件的查询,始终使用 QueryBuilder:
qb := query.NewQueryBuilder()
qb.Where("age", query.OpGreater, 18).
Where("city", query.OpEqual, "Beijing")
rows, _ := table.Query(qb)
- 支持操作符:
OpEqual、OpNotEqual、OpGreater、OpLess、OpPrefix、OpSuffix、OpContains - 支持
WhereNot()进行否定 - 支持
And()和Or()逻辑 - 当可用时自动使用二级索引(对于
=条件) - 如果没有索引,则回退到全表扫描
Compaction
Compaction 在后台自动运行:
- 触发条件: L0 文件数 > 阈值(默认 10)
- 策略: 合并重叠文件,从 L0 → L1、L1 → L2 等
- 安全性: 删除前验证文件是否存在,以防止数据丢失
- 去重: 对于重复的 seq,保留最新记录(按时间戳)
- 文件大小: L0=2MB、L1=10MB、L2=50MB、L3=100MB、L4+=200MB
修改 compaction 逻辑时:
picker.go: 选择要压缩的文件compactor.go: 执行合并操作manager.go: 调度和协调 compaction- 删除前始终验证输入/输出文件是否存在(参见
DoCompaction)
版本控制(MANIFEST)
MANIFEST 跟踪跨版本的 SST 文件元数据:
VersionEdit: 记录原子变更(AddFile/DeleteFile)VersionSet: 管理当前和历史版本LogAndApply(): 原子地应用编辑并持久化到 MANIFEST
添加/删除 SST 文件时:
- 分配文件编号:
versionSet.AllocateFileNumber() - 创建带变更的
VersionEdit - 应用:
versionSet.LogAndApply(edit) - 清理旧文件:
compactionManager.CleanupOrphanFiles()
错误恢复
- WAL 重放: 启动时,所有
*.wal文件被重放到 Active MemTable - 孤儿文件清理: 不在 MANIFEST 中的文件在启动时删除
- 索引修复:
verifyAndRepairIndexes()重建损坏的索引 - 优雅降级: 表恢复失败会被记录但不会使数据库崩溃
测试模式
测试按组件组织:
engine/engine_test.go: 基本引擎操作engine/engine_compaction_test.go: Compaction 场景engine/engine_stress_test.go: 并发压力测试compaction/compaction_test.go: Compaction 正确性query/builder_test.go: Query builder 功能schema/schema_test.go: Schema 验证
为多线程操作编写测试时,使用 sync.WaitGroup 并用多个 goroutine 测试(参见 engine_stress_test.go)。
性能特性
- 写入吞吐量: 200K+ 写/秒(多线程),50K 写/秒(单线程)
- 写入延迟: < 1ms(p99)
- 查询延迟: < 0.1ms(MemTable),1-5ms(SST 热数据),3-5ms(冷数据)
- 内存使用: < 150MB(64MB MemTable + 开销)
- 压缩率: Snappy 约 50%
优化时:
- 批量写入以减少 WAL 同步开销
- 对经常查询的字段创建索引
- 监控 MemTable 刷新频率(不应太频繁)
- 根据写入模式调整 compaction 阈值
重要实现细节
序列号
_seq是单调递增的 int64(原子操作)- 充当主键和时间戳排序
- 永不重用(append-only)
- compaction 期间,相同 seq 值的较新记录优先
并发
Engine.mu: 保护元数据和 SST reader 列表Engine.flushMu: 确保一次只有一个 flushMemTable.mu: RWMutex,支持并发读、独占写VersionSet.mu: 保护版本状态
文件格式
WAL 条目:
CRC32 (4B) | Length (4B) | Type (1B) | Seq (8B) | DataLen (4B) | Data (N bytes)
SST 文件:
Header (256B) | B+Tree Index | Data Blocks (Snappy compressed)
B+Tree 节点(4KB 固定):
Header (32B) | Keys (8B each) | Pointers/Offsets (8B each) | Padding
常见陷阱
- Schema 验证仅在向
Engine.Open()提供 schema 时才应用 - 索引必须通过
CreateIndex(field)显式创建(非自动) - 带 schema 的 QueryBuilder 需要调用
WithSchema()或让引擎设置它 - Compaction 可能会暂时增加磁盘使用(合并期间旧文件和新文件共存)
- MemTable flush 是异步的;关闭时可能需要等待 immutable flush 完成
- mmap 文件可能显示较大的虚拟内存使用(这是正常的,不是实际 RAM)