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 膨胀与查询延迟增加。
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_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_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 与时间线之间的核心关系:
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 倒排索引 + 布隆过滤器 |
| 查询性能 | 中等 | 高 | 极高 (支持快速标签过滤) |
| 资源消耗 | 低 (单机) | 中 (分布式) | 中 (分布式) |
| 推荐场景 | 开发/测试环境 | 生产环境 (中等规模) | 生产环境 (大规模/高基数) |
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 瓶颈。
下图展示了从日志采集到存储的完整数据流,以及每个阶段对容量的影响:
1. Ingester 内存上限:当活跃 Series 数量超过 Ingester 内存限制时,会导致 OOM 或拒绝写入。
2. Index 查询延迟:当 Series 数量超过 50 万时,Index 查询延迟可能从毫秒级上升到秒级。
3. Chunk 写入速率:对象存储的 PUT 请求频率不应超过后端服务的限流阈值(如 AWS S3 的 3500 PUT/s 分区限制)。