3. Loki 架构组件剖析:读写路径与核心组件详解
Loki 的架构设计借鉴了 Prometheus 的分层思想,但针对日志数据的特点进行了深度优化。理解其核心组件及交互流程,是构建高可用、高性能日志监控系统的基础。本章将拆解 Loki 的读写路径,详解 Distributor、Ingester、Querier、Compactor 等关键组件的作用与协作方式。
3.1 Loki 整体架构概览
Loki 采用微服务架构,各组件可独立部署和扩展。其核心设计理念是“仅索引元数据,不索引日志内容”,通过将日志数据压缩存储到对象存储(如 S3、GCS、MinIO)中,大幅降低存储成本。下图展示了 Loki 的核心组件及其关系:
3.2 写入路径详解
Loki 的写入路径负责将采集到的日志数据可靠地接收、验证并持久化。整个流程设计为无状态、可水平扩展的管道。
3.2.1 写入路径核心流程
- 日志采集:Promtail、Fluentd 或 Grafana Agent 等采集器从目标(如文件、Journald、容器 stdout)收集日志,并添加标签(Labels)。
- 推送至 Distributor:采集器通过 HTTP/gRPC 将日志流(Log Stream)推送到 Distributor 的
/loki/api/v1/push接口。 - 验证与分片:Distributor 对日志流进行合法性校验(标签格式、时间戳范围),然后使用一致性哈希环(Consistent Hash Ring)决定该日志流应路由到哪个 Ingester。
- Ingester 接收:Ingester 接收日志流后,先在内存中构建 Chunk(数据块),并定期将 Chunk 刷新到对象存储。
- 持久化:Ingester 将 Chunk 写入对象存储(如 S3),同时更新索引信息。
3.2.2 Distributor 组件详解
| 功能维度 | 说明 |
|---|---|
| 职责 | 作为写入入口,负责接收、验证、转发日志流 |
| 验证规则 | 检查标签名/值是否合法、时间戳是否在可接受范围内(默认 ±1 小时) |
| 路由策略 | 基于标签哈希和一致性哈希环,将日志流均匀分发到 Ingester 副本 |
| 高可用 | 无状态,可多副本部署;通过 gRPC 与 Ingester 通信 |
| 限流 | 支持基于租户(Tenant)的速率限制,防止单个租户打满集群 |
3.2.3 Ingester 组件详解
| 功能维度 | 说明 |
|---|---|
| 职责 | 接收日志流,构建 Chunk,管理内存中的日志数据 |
| Chunk 构建 | 将同一标签集的日志压缩为 Chunk(默认使用 gzip/snappy),每个 Chunk 最大约 1.5MB |
| 内存管理 | Chunk 在内存中积累,达到阈值(时间或大小)后刷新到对象存储 |
| 数据可靠性 | 支持 WAL(Write-Ahead Log),在崩溃时恢复未刷新的 Chunk |
| 生命周期 | Ingester 关闭前会执行 Flush 操作,确保所有数据持久化 |
最佳实践:Ingester 是写入路径中最关键的“有状态”组件。建议为 Ingester 分配充足的内存,并启用 WAL 以防止数据丢失。在生产环境中,Ingester 通常以 StatefulSet 方式部署。
3.3 查询路径详解
查询路径负责处理用户的 LogQL 查询请求,从 Ingester(热数据)和对象存储(冷数据)中检索日志。
3.3.1 查询路径核心流程
- 接收查询:Query Frontend 接收用户的 HTTP 查询请求,进行拆分、缓存和排队。
- 分发子查询:Query Frontend 将大查询拆分为多个子查询(按时间范围),并分发给多个 Querier。
- 并行查询:Querier 同时向 Ingester(内存中的热数据)和对象存储(持久化的冷数据)发起查询。
- 合并结果:Querier 将来自不同数据源的结果去重、合并,返回给 Query Frontend。
- 返回响应:Query Frontend 将最终结果返回给用户。
3.3.2 Querier 组件详解
| 功能维度 | 说明 |
|---|---|
| 职责 | 执行 LogQL 查询,从 Ingester 和对象存储中检索日志 |
| 查询范围 | 同时查询 Ingester(热数据,最近几小时)和对象存储(冷数据,历史数据) |
| 去重机制 | 通过 Chunk 的唯一标识(fingerprint + 时间戳)去除重复数据 |
| 无状态 | Querier 不存储数据,可水平扩展以提升查询并发能力 |
| 缓存 | 支持结果缓存和索引缓存,加速重复查询 |
3.3.3 Query Frontend 组件详解
| 功能维度 | 说明 |
|---|---|
| 职责 | 作为查询入口,负责查询拆分、缓存、限流和结果合并 |
| 查询拆分 | 将大时间范围的查询拆分为多个小时间片(如 1 小时/片),并行执行 |
| 缓存策略 | 缓存查询结果和元数据,减少重复计算和存储访问 |
| 排队机制 | 当 Querier 繁忙时,请求进入队列,避免雪崩 |
| 限流 | 支持基于租户的查询并发限制,保障集群稳定性 |
3.4 Compactor 组件详解
Compactor 是 Loki 的“后台管家”,负责优化对象存储中的索引数据,提升查询效率并降低存储成本。
3.4.1 Compactor 核心职责
- 索引合并:将多个小的索引文件合并为更大的文件,减少文件数量,提升查询时的文件扫描效率。
- 数据去重:删除重复的索引条目,确保查询结果的准确性。
- 数据过期:根据配置的保留期(Retention),删除过期的索引数据,释放存储空间。
- 多租户隔离:按租户分别处理索引,避免租户间数据干扰。
3.4.2 Compactor 工作流程
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 扫描存储 | 定期扫描对象存储中的索引目录,发现待合并的小文件 |
| 2 | 下载索引 | 将需要合并的索引文件下载到本地临时目录 |
| 3 | 合并去重 | 在内存中合并索引,去除重复条目,生成新的索引文件 |
| 4 | 上传替换 | 将合并后的索引文件上传到对象存储,并更新索引清单 |
| 5 | 清理旧文件 | 删除已被替换的旧索引文件,释放存储空间 |
注意:Compactor 是单实例运行的组件(Leader 选举模式),以避免多个 Compactor 同时操作同一份索引导致冲突。如果 Compactor 故障,Loki 的写入和查询功能不受影响,但索引优化和过期清理会暂停。
3.5 组件交互总结
下表总结了 Loki 核心组件的角色、状态和关键交互关系:
| 组件 | 角色 | 状态 | 主要交互对象 |
|---|---|---|---|
| Distributor | 写入入口 | 无状态 | 采集器 → Ingester |
| Ingester | 数据写入与热存储 | 有状态 | Distributor → 对象存储;Querier |
| Querier | 查询执行 | 无状态 | Ingester + 对象存储 → Query Frontend |
| Query Frontend | 查询入口与调度 | 无状态 | 用户 → Querier |
| Compactor | 索引优化与清理 | 有状态(单实例) | 对象存储 |
| 对象存储 | 数据持久化 | 外部依赖 | Ingester、Querier、Compactor |
架构要点:Loki 的读写分离设计使得写入和查询可以独立扩展。在发布监控场景中,写入压力通常集中在发布窗口期,而查询压力则分布在发布后的验证阶段。通过合理配置 Distributor 和 Querier 的副本数,可以轻松应对这两种不同的负载模式。