Loki 架构基础:核心组件及数据流

在深入日志治理与压缩优化之前,必须理解 Loki 的读写分离架构。Loki 的设计灵感源自 Prometheus,采用“标签索引 + 对象存储”模式,将日志的元数据与原始内容解耦。本章聚焦三大核心组件——Distributor、Ingester、Querier,并梳理一条完整的日志生命周期数据流。

2.1 核心组件职责速览

组件 角色 关键职责 状态
Distributor 写入入口 / 负载均衡 接收日志流、验证合法性、哈希分片、转发至 Ingester 无状态
Ingester 写入节点 / 内存缓冲 接收并缓存日志流、构建倒排索引、批量刷写到对象存储 有状态(含 WAL)
Querier 读取入口 / 查询引擎 解析 LogQL、从 Ingester 和对象存储中拉取日志、聚合返回 无状态
架构要点:Distributor 和 Querier 都是无状态组件,可以水平扩展;Ingester 是有状态组件,依赖 WAL(Write-Ahead Log)保证数据不丢。理解这个状态差异,是后续设计蓝绿发布期间日志不中断的基础。

2.2 写入数据流(Distributor → Ingester)

日志写入路径是典型的“先验证、再分片、后缓冲”三步走:

  1. 客户端推送:Promtail / Fluent Bit / SDK 通过 HTTP/gRPC 将日志流推送到 Distributor。
  2. Distributor 预处理:校验标签合法性(如保留标签 __name__、job)、检查速率限制、使用一致性哈希环(Consistent Hash Ring)决定目标 Ingester 副本。
  3. Ingester 接收与缓冲:Ingester 将日志追加到内存中的 chunk(压缩块),同时写入 WAL 防止宕机丢失。当 chunk 达到阈值(如 512 KB 或 30 分钟),Ingester 将其刷写到对象存储(S3 / GCS / MinIO)。
写入路径关键指标:
loki_distributor_ingester_appends_total —— 写入请求总数
loki_ingester_chunks_flushed_total —— 刷写到存储的 chunk 数
loki_ingester_chunks_flush_failures_total —— 刷写失败次数(治理重点)

2.3 读取数据流(Querier → Ingester + 对象存储)

查询路径采用“Ingester 内存 + 对象存储”双源合并策略:

  • 实时数据:Querier 直接从 Ingester 内存中拉取尚未刷盘的日志(最近 30 分钟内的数据)。
  • 历史数据:Querier 从对象存储中读取已刷盘的 chunk,通过倒排索引(Index)快速定位。
  • 合并去重:Querier 对来自 Ingester 和存储的结果进行时间戳去重与排序,返回完整日志流。
蓝绿发布场景注意:当 Ingester 滚动重启时,内存中的实时数据会丢失吗?不会——WAL 在重启后回放,且 Querier 会通过 Ring 感知 Ingester 状态变化,自动切换到可用副本。这是 Loki 高可用的核心保障。

2.4 组件协作架构图

下图展示了 Distributor、Ingester、Querier 与外部存储之间的完整数据流关系:

Loki 核心组件数据流架构图 Loki 核心组件数据流架构 日志客户端 (Promtail) Distributor Ingester (内存 + WAL) 对象存储 (S3/MinIO) Querier 用户 / Grafana 推送日志流 哈希分片转发 写入索引 刷写 chunk 实时数据 历史数据 返回日志结果 图例说明 🔴 Distributor:写入入口 🟢 Ingester:内存缓冲 🟣 对象存储:持久化层 🟠 Querier:查询引擎

2.5 数据流中的关键治理点

从上述架构可以看出,日志治理与压缩优化的机会点集中在三个环节:

环节 治理对象 优化手段 影响
Ingester 内存缓冲 chunk 大小与压缩率 调整 chunk_target_size、chunk_block_size 减少刷写次数,降低存储成本
对象存储刷写 chunk 生命周期与过期策略 配置 retention_period、compactor 压缩 控制存储增长,提升查询性能
Querier 查询路径 索引大小与标签基数 限制标签数量、使用 structured_metadata 降低查询延迟,避免 OOM
实战建议:在蓝绿发布场景下,Ingester 的 WAL 目录需要挂载独立持久卷,避免因 Pod 重建导致 WAL 丢失。同时,Distributor 的 ingester_client 超时时间应适当调大(如 10s),以应对发布期间的流量抖动。

2.6 组件间通信协议

Loki 组件之间使用 gRPC 进行高效通信,关键接口如下:

  • Distributor ↔ Ingester:logproto.Pusher 接口,批量推送日志流。
  • Querier ↔ Ingester:logproto.Querier 接口,拉取实时数据。
  • Querier ↔ 对象存储:通过 storage.Client 接口,读取已刷盘的 chunk 和索引。
  • 组件间心跳:通过 ring.Ring 维护成员状态,支持自动故障转移。
// 示例:Distributor 推送日志的 gRPC 调用(简化)
// 实际代码位于 pkg/distributor/distributor.go
func (d *Distributor) Push(ctx context.Context, req *logproto.PushRequest) (*logproto.PushResponse, error) {
    // 1. 验证标签合法性
    // 2. 计算哈希,选择目标 Ingester
    // 3. 通过 gRPC 调用 Ingester.Push()
    return d.ingesterClient.Push(ctx, req)
}

理解这些接口,有助于在蓝绿发布期间排查组件间连接超时、流控错误等问题。