## 主要变更 ### 架构改进 - 明确索引(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>
9.0 KiB
9.0 KiB
Seqlog 索引设计文档
概述
seqlog 现已支持持久化索引文件,实现高效的日志记录查询和检索。
设计原则
- 职责分离:数据文件只存储数据,索引文件负责 offset 管理
- 启动时重建:每次启动都从日志文件重建索引,确保一致性
- 最小化存储:移除冗余字段,优化存储空间
索引文件格式
文件命名
{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)
索引文件管理器,负责索引的构建、加载、追加和查询。
主要方法
// 创建或加载索引(自动重建)
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 集成
写入器支持可选的索引自动更新。
// 创建带索引的写入器
writer, err := seqlog.NewLogWriterWithIndex(logPath, true)
// 写入时自动更新索引
offset, err := writer.Append([]byte("log data"))
// 关闭写入器和索引
writer.Close()
3. RecordQuery 集成
查询器优先使用索引文件进行高效查询。
// 创建带索引的查询器
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:高频查询
// 使用索引,避免每次查询都扫描日志
query, _ := seqlog.NewRecordQueryWithIndex(logPath, true)
for i := 0; i < 1000; i++ {
count, _ := query.GetRecordCount() // O(1)
// ...
}
场景 2:向后查询
// 向后查询需要索引(否则需全文扫描)
backward, _ := query.QueryAt(currentPos, -1, 10, startPos, endPos)
场景 3:断点续传
// 程序重启后,索引自动加载,无需重建
index, _ := seqlog.NewRecordIndex(logPath)
count := index.Count()
lastOffset := index.LastOffset()
场景 4:大文件处理
// 索引文件远小于日志文件,快速加载
// 100 万条记录的索引文件仅 ~7.6 MB
// (24B header + 1,000,000 * 8B = 8,000,024 字节)
API 兼容性
向后兼容
- 现有 API 保持不变(
NewLogWriter,NewRecordQuery) - 默认不启用索引,避免影响现有代码
选择性启用
// 旧代码:不使用索引
writer, _ := seqlog.NewLogWriter(logPath)
// 新代码:启用索引
writer, _ := seqlog.NewLogWriterWithIndex(logPath, true)
测试覆盖
所有索引功能均有完整测试覆盖:
go test -v -run TestIndex
测试用例:
TestIndexBasicOperations- 基本操作(构建、加载、查询)TestIndexRebuild- 索引重建TestQueryWithIndex- 带索引的查询TestIndexAppend- 索引追加TestIndexHeader- 头部信息验证
文件示例
运行示例程序:
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()确保持久化
错误处理
- 日志文件不存在 → 返回错误
- 写入失败 → 返回错误,不更新索引
- 索引文件损坏 → 启动时自动重建(无影响)
未来优化方向
- 稀疏索引:每 N 条记录建一个索引点,减少内存占用
- 分段索引:大文件分段存储,支持并发查询
- 压缩索引:使用差值编码减少存储空间
- mmap 映射:大索引文件使用内存映射优化加载
- 布隆过滤器:快速判断记录是否存在
总结
索引文件设计要点:
✅ 持久化:索引保存到磁盘,重启后快速加载 ✅ 增量更新:写入时自动追加,避免重建 ✅ 向后兼容:不影响现有 API,可选启用 ✅ 自动恢复:损坏时自动重建,确保可用性 ✅ 高效查询:二分查找 + O(1) 元数据读取 ✅ 测试完备:全面的单元测试覆盖