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