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)
日志写入路径是典型的“先验证、再分片、后缓冲”三步走:
- 客户端推送:Promtail / Fluent Bit / SDK 通过 HTTP/gRPC 将日志流推送到 Distributor。
- Distributor 预处理:校验标签合法性(如保留标签
__name__、job)、检查速率限制、使用一致性哈希环(Consistent Hash Ring)决定目标 Ingester 副本。 - 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 与外部存储之间的完整数据流关系:
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)
}
理解这些接口,有助于在蓝绿发布期间排查组件间连接超时、流控错误等问题。