2. Loki 核心架构:读写路径、Ingester、Querier、Distributor 组件详解

理解 Loki 的核心架构是进行性能调优和告警聚合设计的基础。Loki 采用微服务架构,各组件职责清晰,通过读写分离路径实现高效的日志存储与查询。本章将深入剖析 Distributor、Ingester、Querier 三大核心组件及其协作流程。

2.1 整体架构概览:读写分离与水平扩展

Loki 的架构设计围绕“低成本、高可用、易扩展”展开。其核心思想是将日志数据的写入路径与读取路径完全分离,每个路径上的组件均可独立水平扩展。

架构核心原则:
  • 写路径:Distributor → Ingester → 对象存储(如 S3、GCS)。
  • 读路径:Querier → Ingester(内存数据)+ 对象存储(历史数据)。
  • 无状态化:Distributor 和 Querier 为无状态组件,Ingester 为有状态组件。
Loki 核心读写架构图 日志客户端 / Agent Distributor(分发器) 验证、哈希、转发 Ingester 副本 A 内存 + WAL Ingester 副本 B 内存 + WAL Ingester 副本 C 内存 + WAL 对象存储(S3 / GCS) 长期持久化 Querier(查询器) 合并内存 + 存储结果 说明:实线为写路径,虚线为读路径。Querier 同时查询 Ingester 内存数据和对象存储历史数据。

2.2 Distributor:写入入口与数据分片

Distributor 是日志写入路径的第一站,负责接收来自 Agent(如 Promtail、Fluentd)的日志流。它的核心职责是验证、哈希和分发。

2.2.1 核心功能

  • 数据验证:检查日志流标签是否符合规范,时间戳是否在可接受范围内(默认 ±1 小时)。
  • 一致性哈希:根据日志流的标签集(Label Set)计算哈希值,通过一致性哈希环确定该日志流应被发送到哪个 Ingester 副本。
  • 负载均衡:将日志流均匀分发到多个 Ingester 实例,避免单点瓶颈。
  • 副本转发:根据配置的复制因子(默认 3),将同一份数据转发到多个 Ingester 副本,保证数据高可用。

2.2.2 性能调优关键点

参数 默认值 调优建议
distributor.ring.heartbeat-period 5s 集群规模较大时可适当增大至 10-15s,减少心跳网络开销。
distributor.ingestion-rate-limit-mb 10 MB/s 根据实际流量调整,避免限流导致写入失败。
distributor.ingestion-burst-size-mb 20 MB 允许短时突发流量,建议为 rate-limit 的 2 倍。
最佳实践:Distributor 是无状态组件,建议部署 2-3 个副本,并通过 K8s HPA 或手动扩容应对流量增长。监控 distributor_ingester_appends_total 和 distributor_ingester_append_failures_total 指标。

2.3 Ingester:内存写入与数据持久化

Ingester 是 Loki 中唯一的有状态组件,负责接收 Distributor 转发的日志数据,在内存中构建索引和块(Chunk),并最终将数据刷写到对象存储。

2.3.1 写入流程

  1. 接收数据:Ingester 通过 gRPC 接收来自 Distributor 的日志流。
  2. 内存构建:将日志流按时间顺序追加到内存中的 Chunk 里,同时构建倒排索引。
  3. WAL 预写日志:在数据写入内存前,先写入 WAL(Write-Ahead Log),防止进程崩溃导致数据丢失。
  4. Chunk 刷写:当 Chunk 达到最大大小(默认 1.5 MB)或最大时间跨度(默认 30 分钟)时,将 Chunk 压缩并上传到对象存储。
  5. 内存释放:刷写成功后,从内存中移除该 Chunk,释放资源。

2.3.2 关键数据结构

数据结构 作用 性能影响
Chunk 存储压缩后的日志数据块 Chunk 大小影响 I/O 和查询效率
WAL 预写日志,保证数据持久性 WAL 写入延迟影响写入吞吐
倒排索引 标签 → 日志流 ID 的映射 索引大小影响内存占用

2.3.3 性能调优关键点

  • Chunk 大小:增大 ingester.chunk-block-size(默认 256KB)可减少存储对象数量,但会增加内存占用。
  • WAL 路径:使用高性能 SSD 磁盘存放 WAL,避免与系统盘共用。
  • 内存限制:通过 ingester.max-chunk-age 和 ingester.chunk-idle-period 控制 Chunk 在内存中的驻留时间,防止 OOM。
  • 刷写策略:调整 ingester.chunk-target-size(默认 1.5 MB)平衡内存与存储成本。
注意:Ingester 重启或滚动更新会导致内存中的 Chunk 丢失(如果 WAL 未启用或未刷写)。务必启用 WAL(ingester.wal-enabled: true)并配置持久化卷。

2.4 Querier:查询执行与结果合并

Querier 是读取路径的核心,负责处理用户的 LogQL 查询请求。它需要从 Ingester(内存中的最新数据)和对象存储(历史数据)中获取结果,并进行合并与去重。

2.4.1 查询执行流程

  1. 接收查询:Querier 通过 HTTP API 接收 LogQL 查询请求。
  2. 并行查询:将查询拆分为多个子查询,并行发送到所有 Ingester 实例(查询内存数据)和对象存储(查询历史数据)。
  3. 结果合并:收集所有子查询结果,按时间戳排序,去除重复日志行。
  4. 返回结果:将合并后的结果返回给客户端。

2.4.2 性能调优关键点

参数 默认值 调优建议
querier.max-concurrent 10 根据 CPU 和内存资源调整,增大可提升并发查询能力。
querier.query-timeout 5m 对于大范围查询,适当增大超时时间。
querier.engine.timeout 5m 与 query-timeout 配合使用,控制查询引擎执行时间。
查询性能瓶颈:
  • Ingester 内存查询:如果 Ingester 内存中 Chunk 过多,查询延迟会显著增加。建议控制 ingester.max-chunk-age 在合理范围。
  • 对象存储查询:频繁的 S3 API 调用会增加延迟。使用 querier.index-cache-size 和 querier.chunk-cache-size 缓存热点数据。
  • 结果合并:大量日志行的排序和去重会消耗 CPU。建议在查询时使用 topk 或 limit 减少返回数据量。

2.5 组件协作与数据流总结

为了更直观地展示三个组件在读写路径中的协作关系,下图聚焦于一次完整的日志写入与查询生命周期。

组件协作时序图:写入与查询生命周期 Distributor Ingester 对象存储 Querier 1 转发日志流 2 写入 WAL + 内存 Chunk 3 Chunk 刷写 4 持久化存储 5 接收 LogQL 查询 查询内存数据 查询历史数据 6 合并结果并返回 关键说明: 1. 写入路径:Distributor → Ingester(WAL + 内存)→ 对象存储(异步刷写) 2. 查询路径:Querier 同时查询 Ingester(最新数据)和对象存储(历史数据),合并后返回

2.6 组件对比与选型建议

组件 状态 核心资源 扩展方式 故障影响
Distributor 无状态 CPU / 网络 水平扩展(增加副本) 写入中断,恢复后自动重试
Ingester 有状态 内存 / 磁盘(WAL) 水平扩展(增加副本 + 重新分片) 内存数据丢失(需 WAL 恢复)
Querier 无状态 CPU / 内存 水平扩展(增加副本) 查询中断,客户端重试
架构选型建议:
  • 小规模(< 1TB/天):单机模式或微服务模式,所有组件部署在同一节点。
  • 中等规模(1-10TB/天):微服务模式,Distributor 和 Querier 各 2-3 副本,Ingester 3-5 副本。
  • 大规模(> 10TB/天):微服务模式 + 分片(Zone Awareness),Ingester 按可用区分布,Querier 启用并行查询。