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

9.0 KiB
Raw Blame History

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)

索引文件管理器,负责索引的构建、加载、追加和查询。

主要方法

// 创建或加载索引(自动重建)
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() 确保持久化

错误处理

  • 日志文件不存在 → 返回错误
  • 写入失败 → 返回错误,不更新索引
  • 索引文件损坏 → 启动时自动重建(无影响)

未来优化方向

  1. 稀疏索引:每 N 条记录建一个索引点,减少内存占用
  2. 分段索引:大文件分段存储,支持并发查询
  3. 压缩索引:使用差值编码减少存储空间
  4. mmap 映射:大索引文件使用内存映射优化加载
  5. 布隆过滤器:快速判断记录是否存在

总结

索引文件设计要点:

持久化:索引保存到磁盘,重启后快速加载 增量更新:写入时自动追加,避免重建 向后兼容:不影响现有 API可选启用 自动恢复:损坏时自动重建,确保可用性 高效查询:二分查找 + O(1) 元数据读取 测试完备:全面的单元测试覆盖