3. Loki 数据模型:Log Stream、Chunk、Index、Schema 版本、时间线 (Series)

Loki 的数据模型是其高性能、低成本存储的核心。与 Elasticsearch 等全文索引系统不同,Loki 的设计哲学是“仅索引元数据,不索引日志内容”。理解 Log Stream、Chunk、Index、Schema 版本以及时间线 (Series) 这五个核心概念,是掌握 Loki 架构与容量规划的基础。

3.1 核心概念总览

下表从定义、作用与关键特征三个维度,快速对比这五个核心概念:

概念 定义 作用与关键特征
Log Stream 由一组完全相同的标签 (Labels) 标识的日志流。 Loki 写入与读取的基本单位;每个 Stream 对应一个独立的追加日志文件。
Chunk 属于同一个 Stream 的一段连续日志数据块,经过压缩与存储。 Loki 的存储单元;包含原始日志行与元数据;默认压缩后大小约为 1.5 MB。
Index 将 Stream 的标签集合映射到 Chunk 列表的元数据索引。 用于快速定位查询涉及的 Chunk;支持基于标签的过滤与聚合。
Schema 版本 定义 Index 与 Chunk 的存储格式、编码方式及兼容性规则。 决定数据如何被写入、读取与迁移;版本升级通常需要重新索引。
时间线 (Series) 具有唯一标签组合的 Log Stream 在时间轴上的抽象表示。 与 Prometheus 的 Series 概念一致;高基数 (High Cardinality) 问题直接源于 Series 数量爆炸。

3.2 Log Stream:日志流的原子单位

Log Stream 是 Loki 中最核心的抽象。每个 Stream 由一组 唯一且有序的标签 (Labels) 定义。例如:{job="nginx", env="prod", instance="10.0.0.1:80"} 就是一个完整的 Stream 标识。

  • 写入路径:Promtail 或其他采集器将日志行与标签集一同推送到 Loki。Loki 根据标签集计算哈希值,将日志行路由到对应的 Stream。
  • 读取路径:查询时,LogQL 通过标签匹配器(如 {job="nginx"})选择一组 Stream,然后在这些 Stream 内搜索日志内容。
  • 性能影响:Stream 的数量直接影响内存中的 Index 大小与查询性能。过多的 Stream(高基数)会导致 Index 膨胀与查询延迟增加。
最佳实践: 保持每个 Stream 的标签基数在可管理范围内。通常建议每个应用或微服务使用 5-8 个标签,避免使用 request_id、user_ip 等具有无限可能值的标签。

3.3 Chunk:压缩存储的基本单元

Chunk 是 Loki 实际存储日志数据的容器。当采集器向一个 Stream 推送日志时,Loki 不会立即将每条日志写入持久化存储,而是先在内存中缓冲,直到满足以下任一条件时才会将当前 Chunk 封存并上传到对象存储(如 S3、GCS、MinIO):

  • Chunk 大小达到阈值:默认未压缩大小为 1.5 MB。
  • Chunk 空闲超时:默认 1 分钟内没有新日志写入该 Stream。
  • Chunk 最大生存时间:默认 2 小时,防止日志在内存中停留过久。

Chunk 内部结构包含:

  • 原始日志行:按时间戳排序的日志内容。
  • 元数据:该 Chunk 所属的 Stream 标识、时间范围、行数、CRC32 校验和等。
  • 压缩数据:使用 gzip、snappy 或 lz4 等算法压缩后的日志内容。
容量规划要点: 每个 Chunk 在对象存储中占用一个独立文件。Chunk 数量 = 总日志量 / 平均 Chunk 大小。如果 Stream 写入频率极低(如每小时仅几条日志),会产生大量小 Chunk,增加存储成本与查询开销。建议通过调整 chunk_idle_period 或 chunk_target_size 来优化。

3.4 Index:元数据索引的演进

Index 是 Loki 实现快速查询的关键。它不存储日志内容,只存储 标签集 → Chunk 列表 的映射关系。Loki 的 Index 经历了多个 Schema 版本的演进:

Schema 版本 Index 存储后端 特点
v9 (单 Store) BoltDB (本地文件) 单机模式,Index 存储在本地磁盘,不适合大规模集群。
v10 (单 Store) BoltDB (本地文件) 优化了 Chunk 元数据格式,减少 Index 大小。
v11 (单 Store) BoltDB (本地文件) 引入 Series 存储优化,进一步压缩 Index。
v12 (多 Store) BoltDB (Shipper) / TSDB 支持 Index 分片与分布式存储;TSDB 模式提供更好的压缩与查询性能。
v13 (多 Store) TSDB (推荐) 当前最新稳定版本;完全基于 TSDB 的 Index 格式,支持更高效的标签索引与聚合查询。
Schema 版本迁移警告: 升级 Schema 版本通常是一个破坏性操作。Loki 要求所有 Ingester 与 Querier 同时升级,并且旧数据不会自动重新索引。建议在升级前规划好数据保留策略,或使用 schema_config 中的 from 字段实现平滑过渡。

3.5 时间线 (Series):高基数的根源

在 Loki 中,时间线 (Series) 与 Log Stream 是等价的。每个唯一的标签组合定义一条时间线。例如:

{job="api", method="GET", status="200"}  → 一条时间线
{job="api", method="GET", status="404"}  → 另一条时间线
{job="api", method="POST", status="200"} → 第三条时间线

当标签中包含高基数属性(如 user_id、request_id、ip)时,时间线数量会呈指数级增长。例如,一个拥有 1000 个用户、每个用户产生 10 种不同状态码的应用,仅 user_id 与 status 两个标签组合就可能产生 10,000 条时间线。

高基数带来的问题:

  • Index 膨胀:每条时间线都需要在 Index 中维护一个条目,导致内存与磁盘占用激增。
  • 查询性能下降:查询时需要扫描更多的时间线,增加 CPU 与 I/O 开销。
  • Ingester 内存压力:每个 Ingester 需要为活跃的时间线维护内存中的 Chunk 缓冲区。
容量预测关键指标: 在压测沙盘中,务必监控 loki_ingester_memory_streams 与 loki_index_memory_series 指标。当 Series 数量超过 100 万时,建议立即审查标签设计或启用 max_global_streams_per_user 限制。

3.6 数据模型关系图

下图展示了 Log Stream、Chunk、Index 与时间线之间的核心关系:

Loki 数据模型核心关系 Loki 数据模型核心关系 时间线 (Series) 唯一标签组合 {job="api", env="prod"} {job="web", env="prod"} {job="db", env="staging"} ... Log Stream 日志流(写入/读取单位) Stream A: 标签集 A Stream B: 标签集 B Stream C: 标签集 C ... Chunk 压缩存储单元 Chunk_1: 时间范围 T1-T2 Chunk_2: 时间范围 T3-T4 Chunk_3: 时间范围 T5-T6 ... 1:1 映射 1:N 拆分 Index (元数据索引) 存储映射:标签集 → Chunk 列表 {job="api"} → [Chunk_1, Chunk_3, Chunk_5] {job="web"} → [Chunk_2, Chunk_4] 标签提取 每条时间线 (Series) 对应一个 Log Stream;每个 Stream 按时间拆分为多个 Chunk;Index 记录标签到 Chunk 的映射关系。

3.7 Schema 版本与数据生命周期

Schema 版本不仅定义了 Index 的存储格式,还决定了 Chunk 的编码方式与压缩算法。在压测沙盘中,选择合适的 Schema 版本直接影响容量预测的准确性。

下表对比了当前主流 Schema 版本的关键差异:

特性 v11 (单 Store) v12 (TSDB) v13 (TSDB 增强)
Index 后端 BoltDB (本地) TSDB (对象存储) TSDB (对象存储)
Chunk 编码 gzip / snappy gzip / snappy / lz4 gzip / snappy / lz4 / zstd
Series 存储 BoltDB Bucket TSDB 倒排索引 TSDB 倒排索引 + 布隆过滤器
查询性能 中等 高 极高 (支持快速标签过滤)
资源消耗 低 (单机) 中 (分布式) 中 (分布式)
推荐场景 开发/测试环境 生产环境 (中等规模) 生产环境 (大规模/高基数)
压测沙盘配置建议: 在沙盘环境中,建议从 Schema v12 开始测试,因为它平衡了性能与资源消耗。如果压测目标包含高基数场景(如 50 万+ Series),则必须升级到 v13 并启用布隆过滤器,否则查询延迟会显著增加。

3.8 数据模型对容量预测的影响

理解数据模型后,容量预测不再是简单的“日志量 × 存储单价”。以下关键因素必须纳入预测模型:

  • Series 数量:直接影响 Index 内存占用。每个 Series 在 Index 中约占用 256-512 字节。100 万 Series 约占用 256-512 MB 内存。
  • Chunk 大小与数量:影响对象存储的请求次数与存储成本。小 Chunk 过多会导致 GET/PUT 请求激增,增加 API 调用费用。
  • Schema 版本:不同版本的压缩率与索引效率不同。v13 的布隆过滤器可以显著减少查询时需要扫描的 Chunk 数量,从而降低 CPU 消耗。
  • 标签基数:高基数标签(如 trace_id)会导致 Series 爆炸,使 Index 无法完全放入内存,触发磁盘 I/O 瓶颈。

下图展示了从日志采集到存储的完整数据流,以及每个阶段对容量的影响:

Loki 数据流与容量影响点 Loki 数据流与容量影响点 1. 日志采集 Promtail / 采集器 2. 标签分配 定义 Series 基数 3. Stream 路由 Ingester 内存占用 4. Chunk 封存 对象存储写入频率 容量影响关键指标 • 采集速率 (bytes/s) • 标签基数 (Series 数量) • 平均 Chunk 大小 (bytes) • 压缩比 (原始/压缩) • Index 内存占用 (MB) • 对象存储 API 请求频率
压测沙盘关键验证点: 在变更门禁压测中,必须验证以下数据模型相关的容量阈值:
1. Ingester 内存上限:当活跃 Series 数量超过 Ingester 内存限制时,会导致 OOM 或拒绝写入。
2. Index 查询延迟:当 Series 数量超过 50 万时,Index 查询延迟可能从毫秒级上升到秒级。
3. Chunk 写入速率:对象存储的 PUT 请求频率不应超过后端服务的限流阈值(如 AWS S3 的 3500 PUT/s 分区限制)。