## 主要变更 ### 架构改进 - 明确索引(Index)与偏移(Offset)的职责分离 - Index: 记录序号(逻辑概念),用于状态判断 - Offset: 文件字节位置(物理概念),仅用于 I/O 操作 ### API 变更 - 删除所有 Position 相关方法: - `LogCursor.StartPos()/EndPos()` - `LogTailer.GetStartPos()/GetEndPos()` - `TopicProcessor.GetProcessingPosition()/GetReadPosition()` - `Seqlog.GetProcessingPosition()/GetReadPosition()` - 新增索引方法: - `LogCursor.StartIndex()/EndIndex()` - `LogTailer.GetStartIndex()/GetEndIndex()` - `TopicProcessor.GetProcessingIndex()/GetReadIndex()` - `Seqlog.GetProcessingIndex()/GetReadIndex()` - `Seqlog.GetProcessor()` - 获取 processor 实例以访问 Index ### 查询接口变更 - `RecordQuery.QueryOldest(startIndex, count, startIdx, endIdx)` - 使用索引参数 - `RecordQuery.QueryNewest(endIndex, count, startIdx, endIdx)` - 使用索引参数 - `RecordQuery.QueryAt(position, direction, count, startIdx, endIdx)` - startIdx/endIdx 用于状态判断 ### 性能优化 - 状态判断改用整数比较,不再需要计算偏移量 - 减少不必要的索引到偏移的转换 - 只在实际文件 I/O 时才获取 offset ### 测试更新 - 更新所有测试用例使用新的 Index API - 更新示例代码(topic_processor_example.go, webapp/main.go) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
322 lines
9.0 KiB
Markdown
322 lines
9.0 KiB
Markdown
# Seqlog 索引设计文档
|
||
|
||
## 概述
|
||
|
||
seqlog 现已支持**持久化索引文件**,实现高效的日志记录查询和检索。
|
||
|
||
## 设计原则
|
||
|
||
1. **职责分离**:数据文件只存储数据,索引文件负责 offset 管理
|
||
2. **启动时重建**:每次启动都从日志文件重建索引,确保一致性
|
||
3. **最小化存储**:移除冗余字段,优化存储空间
|
||
|
||
## 索引文件格式
|
||
|
||
### 文件命名
|
||
|
||
```
|
||
{logfile}.idx
|
||
```
|
||
|
||
例如:`app.log` 对应的索引文件为 `app.log.idx`
|
||
|
||
### 数据文件结构
|
||
|
||
```
|
||
每条记录:[4B len][4B CRC][16B UUID][data]
|
||
头部大小:24 字节
|
||
|
||
示例:
|
||
00000000 0f 00 00 00 8b 54 b3 a5 a5 9b fb 59 dd d5 45 2c |.....T.....Y..E,|
|
||
↑ Len=15 ↑ CRC ↑ UUID 开始...
|
||
00000010 a1 82 6f 16 5c 54 94 8d e6 97 a5 e5 bf 97 e8 ae |..o.\T..........|
|
||
↑ ...UUID 继续 ↑ 数据开始
|
||
```
|
||
|
||
### 索引文件结构
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────┐
|
||
│ Header (8 字节) │
|
||
│ ┌─────────────────────────────────────────────┐ │
|
||
│ │ Magic: [4B] 0x53494458 ("SIDX") │ │
|
||
│ │ Version: [4B] 1 │ │
|
||
│ └─────────────────────────────────────────────┘ │
|
||
├─────────────────────────────────────────────────┤
|
||
│ Index Entries (每条 8 字节) │
|
||
│ ┌─────────────────────────────────────────────┐ │
|
||
│ │ Offset1: [8B] 第 1 条记录的偏移 │ │
|
||
│ │ Offset2: [8B] 第 2 条记录的偏移 │ │
|
||
│ │ ... │ │
|
||
│ │ OffsetN: [8B] 第 N 条记录的偏移 │ │
|
||
│ └─────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────┘
|
||
|
||
RecordCount = (文件大小 - 8) / 8
|
||
LastOffset = 读取最后一条索引条目
|
||
```
|
||
|
||
### 实际示例
|
||
|
||
15 条记录的文件:
|
||
|
||
**数据文件** (591 字节):
|
||
```
|
||
00000000 0f 00 00 00 8b 54 b3 a5 a5 9b fb 59 dd d5 45 2c |.....T.....Y..E,|
|
||
00000010 a1 82 6f 16 5c 54 94 8d e6 97 a5 e5 bf 97 e8 ae |..o.\T..........|
|
||
↑ 数据:"日志记录 #1"
|
||
```
|
||
|
||
**索引文件** (128 字节):
|
||
```
|
||
00000000 53 49 44 58 01 00 00 00 00 00 00 00 00 00 00 00 |SIDX............|
|
||
↑ Magic="SIDX" ↑ Ver=1 ↑ Offset[0]=0
|
||
00000010 27 00 00 00 00 00 00 00 4e 00 00 00 00 00 00 00 |'.......N.......|
|
||
↑ Offset[1]=39 (0x27) ↑ Offset[2]=78 (0x4E)
|
||
|
||
文件大小:128 字节 = 8B header + 15 × 8B entries
|
||
记录总数:(128 - 8) / 8 = 15 条
|
||
```
|
||
|
||
## 核心组件
|
||
|
||
### 1. RecordIndex (index.go)
|
||
|
||
索引文件管理器,负责索引的构建、加载、追加和查询。
|
||
|
||
#### 主要方法
|
||
|
||
```go
|
||
// 创建或加载索引(自动重建)
|
||
index, err := seqlog.NewRecordIndex(logPath)
|
||
|
||
// 追加索引条目(写入时调用)
|
||
err := index.Append(offset)
|
||
|
||
// 根据索引位置获取记录偏移
|
||
offset, err := index.GetOffset(index)
|
||
|
||
// 二分查找:根据偏移量查找索引位置
|
||
idx := index.FindIndex(offset)
|
||
|
||
// 获取记录总数
|
||
count := index.Count()
|
||
|
||
// 获取最后一条记录偏移
|
||
lastOffset := index.LastOffset()
|
||
|
||
// 关闭索引
|
||
index.Close()
|
||
```
|
||
|
||
### 2. LogWriter 集成
|
||
|
||
写入器支持可选的索引自动更新。
|
||
|
||
```go
|
||
// 创建带索引的写入器
|
||
writer, err := seqlog.NewLogWriterWithIndex(logPath, true)
|
||
|
||
// 写入时自动更新索引
|
||
offset, err := writer.Append([]byte("log data"))
|
||
|
||
// 关闭写入器和索引
|
||
writer.Close()
|
||
```
|
||
|
||
### 3. RecordQuery 集成
|
||
|
||
查询器优先使用索引文件进行高效查询。
|
||
|
||
```go
|
||
// 创建带索引的查询器
|
||
query, err := seqlog.NewRecordQueryWithIndex(logPath, true)
|
||
|
||
// 获取记录总数(从索引,O(1))
|
||
count, err := query.GetRecordCount()
|
||
|
||
// 向后查询(基于索引,O(log n) 定位 + O(n) 读取)
|
||
backward, err := query.QueryAt(offset, -1, count, startPos, endPos)
|
||
|
||
query.Close()
|
||
```
|
||
|
||
## 性能优化
|
||
|
||
### 1. 启动时重建
|
||
|
||
- **每次启动都重建**:从日志文件扫描构建索引,确保索引和日志完全一致
|
||
- **无损坏风险**:索引文件即使损坏也会自动重建
|
||
- **简化设计**:无需在头部保存 RecordCount 和 LastOffset
|
||
|
||
### 2. 增量更新
|
||
|
||
- 写入记录时同步追加索引条目
|
||
- 避免每次查询都重新扫描日志文件
|
||
|
||
### 3. 二分查找
|
||
|
||
- `FindIndex()` 使用二分查找定位偏移量
|
||
- 时间复杂度:O(log n)
|
||
|
||
### 4. 自动恢复
|
||
|
||
- 索引文件损坏时自动重建
|
||
- 写入器打开时检查并同步索引
|
||
|
||
## 使用场景
|
||
|
||
### 场景 1:高频查询
|
||
|
||
```go
|
||
// 使用索引,避免每次查询都扫描日志
|
||
query, _ := seqlog.NewRecordQueryWithIndex(logPath, true)
|
||
for i := 0; i < 1000; i++ {
|
||
count, _ := query.GetRecordCount() // O(1)
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 场景 2:向后查询
|
||
|
||
```go
|
||
// 向后查询需要索引(否则需全文扫描)
|
||
backward, _ := query.QueryAt(currentPos, -1, 10, startPos, endPos)
|
||
```
|
||
|
||
### 场景 3:断点续传
|
||
|
||
```go
|
||
// 程序重启后,索引自动加载,无需重建
|
||
index, _ := seqlog.NewRecordIndex(logPath)
|
||
count := index.Count()
|
||
lastOffset := index.LastOffset()
|
||
```
|
||
|
||
### 场景 4:大文件处理
|
||
|
||
```go
|
||
// 索引文件远小于日志文件,快速加载
|
||
// 100 万条记录的索引文件仅 ~7.6 MB
|
||
// (24B header + 1,000,000 * 8B = 8,000,024 字节)
|
||
```
|
||
|
||
## API 兼容性
|
||
|
||
### 向后兼容
|
||
|
||
- 现有 API 保持不变(`NewLogWriter`, `NewRecordQuery`)
|
||
- 默认**不启用**索引,避免影响现有代码
|
||
|
||
### 选择性启用
|
||
|
||
```go
|
||
// 旧代码:不使用索引
|
||
writer, _ := seqlog.NewLogWriter(logPath)
|
||
|
||
// 新代码:启用索引
|
||
writer, _ := seqlog.NewLogWriterWithIndex(logPath, true)
|
||
```
|
||
|
||
## 测试覆盖
|
||
|
||
所有索引功能均有完整测试覆盖:
|
||
|
||
```bash
|
||
go test -v -run TestIndex
|
||
```
|
||
|
||
测试用例:
|
||
- `TestIndexBasicOperations` - 基本操作(构建、加载、查询)
|
||
- `TestIndexRebuild` - 索引重建
|
||
- `TestQueryWithIndex` - 带索引的查询
|
||
- `TestIndexAppend` - 索引追加
|
||
- `TestIndexHeader` - 头部信息验证
|
||
|
||
## 文件示例
|
||
|
||
运行示例程序:
|
||
|
||
```bash
|
||
cd example
|
||
go run index_example.go
|
||
```
|
||
|
||
示例输出:
|
||
|
||
```
|
||
=== 示例 1:带索引的写入器 ===
|
||
写入: offset=0, data=日志记录 #1
|
||
写入: offset=47, data=日志记录 #2
|
||
...
|
||
索引文件已创建: test_seqlog/app.log.idx
|
||
|
||
=== 示例 2:带索引的查询器 ===
|
||
记录总数: 10
|
||
第 5 条记录的偏移: 235
|
||
向后查询 3 条记录:
|
||
[0] 状态=StatusProcessing, 数据=日志记录 #3
|
||
...
|
||
```
|
||
|
||
## 技术细节
|
||
|
||
### 存储开销
|
||
|
||
**数据文件**:
|
||
- 每条记录头部:24 字节(原 32 字节,节省 25%)
|
||
- 格式:`[4B len][4B CRC][16B UUID][data]`
|
||
|
||
**索引文件**:
|
||
- 头部:8 字节(固定)
|
||
- 每条记录:8 字节
|
||
- 总大小:`8 + recordCount * 8` 字节
|
||
|
||
**示例对比**(1 万条记录,每条 100 字节数据):
|
||
|
||
| 组件 | 旧格式 (32B 头) | 新格式 (24B 头) | 节省 |
|
||
|------|----------------|----------------|------|
|
||
| 数据文件 | 1.32 MB | 1.24 MB | **80 KB (6%)** |
|
||
| 索引文件 | 128 字节 | 128 字节 | 0 |
|
||
| 总计 | 1.32 MB | 1.24 MB | **80 KB** |
|
||
|
||
### 性能对比
|
||
|
||
| 操作 | 无索引 | 有索引 |
|
||
|------|--------|--------|
|
||
| 获取记录总数 | O(n) 全文扫描 | O(1) 读取头部 |
|
||
| 向后查询定位 | 不支持 | O(log n) 二分查找 |
|
||
| 启动时间 | 快(无需加载) | 中(加载索引) |
|
||
| 内存占用 | 低 | 中(索引数组) |
|
||
|
||
### 数据一致性
|
||
|
||
- **启动时重建**:确保索引永远和日志文件一致
|
||
- 运行时:写入日志后立即追加索引
|
||
- 索引文件使用 `Sync()` 确保持久化
|
||
|
||
### 错误处理
|
||
|
||
- 日志文件不存在 → 返回错误
|
||
- 写入失败 → 返回错误,不更新索引
|
||
- 索引文件损坏 → 启动时自动重建(无影响)
|
||
|
||
## 未来优化方向
|
||
|
||
1. **稀疏索引**:每 N 条记录建一个索引点,减少内存占用
|
||
2. **分段索引**:大文件分段存储,支持并发查询
|
||
3. **压缩索引**:使用差值编码减少存储空间
|
||
4. **mmap 映射**:大索引文件使用内存映射优化加载
|
||
5. **布隆过滤器**:快速判断记录是否存在
|
||
|
||
## 总结
|
||
|
||
索引文件设计要点:
|
||
|
||
✅ **持久化**:索引保存到磁盘,重启后快速加载
|
||
✅ **增量更新**:写入时自动追加,避免重建
|
||
✅ **向后兼容**:不影响现有 API,可选启用
|
||
✅ **自动恢复**:损坏时自动重建,确保可用性
|
||
✅ **高效查询**:二分查找 + O(1) 元数据读取
|
||
✅ **测试完备**:全面的单元测试覆盖
|