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