3. Loki 架构速览:Ingester、Querier、Distributor、Compactor 组件职责

在深入压测沙盘之前,必须建立对 Loki 核心组件的清晰认知。Loki 采用微服务架构,各组件各司其职,共同完成日志的接收、存储、查询与压缩。理解这些组件的职责边界,是后续分析写入放大与查询限流问题的前提。

3.1 组件全景与数据流

下图展示了 Loki 核心组件之间的数据流转关系。日志从左侧进入,经过分发、写入、查询与压缩四个阶段,最终持久化到对象存储。

Loki 核心组件数据流图 Loki 核心组件数据流 数据流方向:左 → 右 Distributor 分发 & 校验 Ingester 写入 & 缓存 Querier 查询 & 聚合 Compactor 压缩 & 索引 对象存储 S3/GCS/MinIO gRPC 推送 内存查询 读取 Flush 落盘 读写 说明:实线为同步数据流,虚线为异步数据流(Flush / 后台压缩)
架构要点:Loki 的写入路径(Distributor → Ingester → 对象存储)与查询路径(Querier → Ingester + 对象存储)是分离的,这种设计使得写入放大主要发生在 Ingester 侧,而查询限流则与 Querier 的并发控制密切相关。

3.2 组件职责详解

下表从职责、关键行为、压测关注点三个维度,逐一拆解四个核心组件:

组件 核心职责 关键行为 压测关注点
Distributor 接收日志推送,校验合法性,按哈希分片转发给 Ingester gRPC 接收 → 标签校验 → 一致性哈希路由 → 批量转发 接收吞吐、转发延迟、限流触发阈值
Ingester 缓存日志流,构建内存索引,定期 Flush 到对象存储 追加写入 → 构建 Chunk → 内存索引 → Flush 落盘 写入放大系数、Chunk 切割策略、OOM 风险
Querier 接收 LogQL 查询,合并 Ingester 内存数据与对象存储数据 解析 LogQL → 并行读取 Ingester + 对象存储 → 去重聚合 查询并发数、超时控制、数据一致性
Compactor 合并对象存储中的小 Chunk,构建/更新倒排索引 扫描 Chunk → 合并 → 生成新索引 → 清理旧数据 压缩频率、索引膨胀、对象存储请求量

3.3 组件交互细节

3.3.1 Distributor:写入入口的守门人

Distributor 是 Loki 写入路径的第一站,负责以下关键操作:

  • 标签校验:检查日志流标签是否符合命名规范,拒绝空标签或超长标签。
  • 速率限制:基于每个租户(Tenant)的写入速率配额,超出时直接返回 429 限流响应。
  • 一致性哈希路由:根据日志流标签计算哈希值,将同一流的所有日志转发到同一个 Ingester 副本,保证写入顺序。
  • 副本转发:默认配置下,Distributor 会将每条日志转发到 replication_factor 个 Ingester 副本(通常为 3),这是写入放大的第一个来源。
# Distributor 限流配置示例(loki.yaml)
distributor:
  ring:
    kvstore:
      store: memberlist
  rate_store:
    max_global_rate: 50MB   # 全局写入速率上限
    max_global_burst: 100MB # 突发写入上限
  ingester_client:
    grpc_client_config:
      max_recv_msg_size: 10485760  # 10MB

3.3.2 Ingester:写入放大的核心战场

Ingester 是写入放大问题的集中爆发点。它维护着内存中的日志流(Stream)和 Chunk 结构:

  • Stream 与 Chunk:每个唯一的标签组合对应一个 Stream,每个 Stream 包含多个 Chunk。Chunk 是 Loki 的最小存储单元,默认大小约为 1.5MB(未压缩)。
  • 写入放大来源:
    • 副本写入:每个日志条目被写入 replication_factor 个 Ingester,放大系数 = replication_factor。
    • Chunk 切割:当 Chunk 达到容量上限或时间窗口(默认 30 分钟)时,Ingester 会关闭当前 Chunk 并创建新 Chunk,关闭的 Chunk 会被 Flush 到对象存储。频繁切割会导致大量小 Chunk,增加对象存储请求量。
    • 内存索引:Ingester 在内存中维护倒排索引,用于快速定位 Stream。当 Stream 数量巨大时,索引本身也会消耗大量内存。
压测警示:在 etcd 压测沙盘中,如果模拟大量短生命周期 Pod 的日志(即高频创建/删除 Stream),Ingester 的内存索引会急剧膨胀,同时产生大量小 Chunk,导致写入放大系数从正常的 3 倍飙升到 10 倍以上。这是课程后续重点分析的现象。

3.3.3 Querier:查询路径的调度中枢

Querier 负责处理所有 LogQL 查询请求,其工作流程如下:

  1. 查询解析:将 LogQL 语句解析为抽象语法树(AST),提取标签选择器、时间范围、聚合函数等。
  2. 分片查询:根据时间范围和标签选择器,将查询拆分为多个子查询,并行发送到所有 Ingester(查询内存数据)和对象存储(查询已落盘数据)。
  3. 结果合并:收集所有子查询结果,进行去重、排序、聚合,返回给客户端。

Querier 的并发控制直接影响查询限流行为:

# Querier 并发配置示例
querier:
  max_concurrent: 10          # 最大并发查询数
  query_timeout: 5m           # 查询超时时间
  engine:
    max_look_back_period: 72h # 最大回溯时间范围
限流触发条件:当并发查询数超过 max_concurrent 时,后续查询请求会被排队或直接拒绝(返回 429)。在压测沙盘中,可以通过调整并发数观察 Querier 的限流行为与系统吞吐量的关系。

3.3.4 Compactor:存储效率的优化器

Compactor 是一个后台组件,定期执行以下任务:

  • Chunk 合并:将对象存储中的多个小 Chunk 合并为更大的 Chunk(默认目标大小 10MB),减少对象存储中的文件数量。
  • 索引构建:基于合并后的 Chunk 生成倒排索引(Boltdb-shipper 模式),加速查询时的 Chunk 定位。
  • 数据清理:删除超过保留期限的 Chunk 和索引。

Compactor 的合并效率直接影响查询性能:

指标 合并前(小 Chunk 多) 合并后(大 Chunk 少)
对象存储文件数 高(10,000+) 低(1,000-)
查询时索引查找次数 高 低
Compactor CPU 消耗 低 高(合并期间)

3.4 组件职责总结

下图以矩阵形式总结四个组件在写入与查询两条路径中的角色定位:

Loki 组件职责矩阵 组件职责矩阵:写入路径 vs 查询路径 写入路径 Distributor Ingester Compactor Querier 写入 存储 查询 主要 主要 次要 无 无 主要 主要 无 无 次要 无 主要 主要职责 次要职责 不参与