Files
seqlog/INDEX_DESIGN.md
bourdon de39339620 重构:统一使用索引(Index)替代位置(Position)进行状态判断
## 主要变更

### 架构改进
- 明确索引(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>
2025-10-03 23:50:53 +08:00

322 lines
9.0 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.

# 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) 元数据读取
**测试完备**:全面的单元测试覆盖