Files
srdb/CLAUDE.md

292 lines
10 KiB
Markdown
Raw Normal View History

# 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`
## 构建和测试
```bash
# 运行所有测试
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 使用简化的两层架构:
1. **内存层**: WAL + MemTable (Active + Immutable)
2. **磁盘层**: 带 B+Tree 索引的 SST 文件,分为 L0-L4+ 层级
### 核心数据流
**写入路径**:
1. Schema 验证(如果定义了)
2. 生成序列号 (`_seq`)
3. 追加写入 WAL顺序写
4. 插入到 Active MemTablemap + 有序 slice
5. 当 MemTable 超过阈值(默认 64MB切换到新的 Active MemTable 并异步将 Immutable 刷新到 SST
6. 更新二级索引(如果已创建)
**读取路径**:
1. 检查 Active MemTableO(1) map 查找)
2. 按顺序检查 Immutable MemTables从最新到最旧
3. 使用 mmap + B+Tree 索引扫描 SST 文件(从最新到最旧)
4. 第一个匹配的记录获胜(新数据覆盖旧数据)
**查询路径**(带条件):
1. 如果是带 `=` 操作符的索引字段:使用二级索引 → 通过 seq 获取
2. 否则带过滤条件的全表扫描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 # MemTable130 行)
│ └── 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 是可选的,但建议在生产环境使用:
```go
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`
```go
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 文件时:
1. 分配文件编号:`versionSet.AllocateFileNumber()`
2. 创建带变更的 `VersionEdit`
3. 应用:`versionSet.LogAndApply(edit)`
4. 清理旧文件:`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 写/秒(单线程)
- **写入延迟**: < 1msp99
- **查询延迟**: < 0.1msMemTable1-5msSST 热数据3-5ms冷数据
- **内存使用**: < 150MB64MB MemTable + 开销
- **压缩率**: Snappy 约 50%
优化时:
- 批量写入以减少 WAL 同步开销
- 对经常查询的字段创建索引
- 监控 MemTable 刷新频率(不应太频繁)
- 根据写入模式调整 compaction 阈值
## 重要实现细节
### 序列号
- `_seq` 是单调递增的 int64原子操作
- 充当主键和时间戳排序
- 永不重用append-only
- compaction 期间,相同 seq 值的较新记录优先
### 并发
- `Engine.mu`: 保护元数据和 SST reader 列表
- `Engine.flushMu`: 确保一次只有一个 flush
- `MemTable.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