2. Loki 基础架构:多租户、日志流、Ingester、Querier、Distributor 组件详解
在生产环境中,Loki 并非单进程运行,而是由多个职责分明的组件构成一个水平可扩展的日志系统。理解这些组件及其协作方式,是构建可靠 CI/CD 流水线 SLI 监控与故障排查能力的前提。本章将深入剖析 Loki 的核心架构,重点讲解多租户隔离、日志流模型以及 Distributor、Ingester、Querier 三大关键组件。
2.1 多租户与日志流模型
Loki 从设计之初就原生支持多租户。每个租户拥有独立的日志存储空间和查询视图,租户之间完全隔离。在 CI/CD 场景中,通常将每个微服务团队或每条流水线视为一个租户,从而实现权限与数据的逻辑分离。
X-Scope-OrgID 标识。如果未配置多租户,Loki 会使用默认的 fake 租户。
日志流(Log Stream) 是 Loki 中最核心的数据组织单元。一组具有完全相同标签(Label)组合的日志条目构成一个日志流。标签是键值对,用于索引和快速检索日志。
| 概念 | 说明 | CI/CD 场景示例 |
|---|---|---|
| 租户 | 数据隔离边界,每个租户拥有独立的索引和数据 | team-alpha、pipeline-build |
| 日志流 | 具有相同标签集合的日志序列 | {job="deploy", env="staging", service="api-gateway"} |
| 标签 | 键值对,用于标识和查询日志流 | pipeline_id、stage、commit_sha |
| 日志条目 | 单行日志内容 + 时间戳 + 元数据 | 2025-03-15T10:00:00Z [INFO] Build started |
pipeline_id、stage、job_name 作为标签。标签基数(Cardinality)不宜过高,否则会导致索引膨胀。避免将 request_id、trace_id 等高频变化的值作为标签。
2.2 Distributor:日志写入的入口网关
Distributor 是 Loki 接收日志写入请求的第一站。它负责验证、转发和负载均衡,确保日志数据被正确路由到对应的 Ingester 实例。
2.2.1 核心职责
- 请求验证: 检查日志流标签格式、时间戳合法性、租户身份。
- 数据分片: 根据标签哈希将日志流分配到特定的 Ingester 实例(一致性哈希)。
- 速率限制: 防止单个租户或日志流过度写入,保护后端组件。
- 预聚合(可选): 在写入前对日志条目进行简单校验或预处理。
2.2.2 工作流程
- 客户端(Promtail、Fluentd 等)通过 HTTP API 推送日志。
- Distributor 解析请求,提取租户 ID 和日志流标签。
- 使用一致性哈希环计算目标 Ingester 实例。
- 将日志条目转发至目标 Ingester 的 gRPC 接口。
- 如果目标 Ingester 不可用,Distributor 会重试或返回错误。
loki_distributor_ingester_appends_total 和 loki_distributor_ingester_append_failures_total 指标。
2.3 Ingester:日志数据的临时存储与持久化
Ingester 是 Loki 中负责接收、缓存和持久化日志数据的核心组件。它维护着内存中的日志流数据,并定期将数据刷写到对象存储(如 S3、GCS、MinIO)。
2.3.1 内存中的日志流管理
每个 Ingester 实例维护一个内存中的日志流映射表。当 Distributor 推送日志时,Ingester 将日志条目追加到对应日志流的缓存中。日志流在内存中按时间顺序排列,形成 Chunk(数据块)。
| Ingester 状态 | 说明 | 对查询的影响 |
|---|---|---|
| PENDING | 刚启动,正在等待加入哈希环 | 不接收写入,不响应查询 |
| JOINING | 正在从其他 Ingester 转移数据 | 不接收写入,不响应查询 |
| ACTIVE | 正常运行,接收写入和查询 | 正常服务 |
| LEAVING | 正在退出,将数据转移给其他 Ingester | 不接收写入,仍响应查询 |
2.3.2 Chunk 与持久化
- Chunk: 日志数据的最小存储单元,包含多个日志条目和元数据。默认压缩后大小约为 1.5 MB。
- Flush 策略: 当 Chunk 达到最大大小或最大时间窗口(默认 1 小时)时,Ingester 将其刷写到对象存储。
- 数据可靠性: 在刷写前,数据仅存在于内存中。如果 Ingester 崩溃,未刷写的数据会丢失。建议部署多个副本并启用 WAL(Write-Ahead Log)来增强可靠性。
loki_ingester_memory_streams 和 loki_ingester_chunks_flushed_total。如果内存使用过高,可以调整 ingester.max-chunk-age 或 ingester.chunk-target-size 参数。
2.4 Querier:日志查询的执行引擎
Querier 负责处理用户的 LogQL 查询请求。它需要从多个数据源(Ingester 内存中的最新数据 + 对象存储中的历史数据)合并结果,返回给客户端。
2.4.1 查询执行流程
- 接收 LogQL 查询请求,解析标签选择器和时间范围。
- 根据标签匹配找到对应的日志流。
- 并行查询所有 Ingester 实例,获取内存中的最新数据。
- 查询对象存储,获取已持久化的历史 Chunk 数据。
- 对两部分结果进行合并、去重、排序,返回最终结果。
2.4.2 查询性能优化
| 优化手段 | 说明 | 适用场景 |
|---|---|---|
| 标签索引 | 使用高基数的标签缩小查询范围 | 所有查询 |
| 时间范围限制 | 尽量缩小查询的时间窗口 | 历史日志查询 |
| 并行查询 | Querier 自动并行查询多个 Ingester 和存储 | 大规模集群 |
| 缓存 | 启用结果缓存和 Chunk 缓存 | 重复查询、仪表盘 |
loki_querier_query_duration_seconds 指标。常见原因:标签基数过高导致索引扫描慢、Ingester 内存中数据过多、对象存储延迟高。使用 topk 和 rate 函数时注意数据量。
2.5 组件间通信与数据一致性
Loki 组件之间通过 gRPC 进行高效通信,并使用 一致性哈希环(Consistent Hash Ring) 来管理 Ingester 实例的发现和数据分布。哈希环存储在 etcd 或 Consul 中,所有组件共享该环。
- Distributor 视角: 根据日志流标签的哈希值,在环上找到负责该日志流的 Ingester 实例。
- Querier 视角: 查询时遍历所有 Ingester 实例,获取内存中的最新数据。
- Ingester 视角: 注册到哈希环,并在状态变化时更新环信息。
2.6 组件故障场景与排查思路
| 故障场景 | 影响 | 排查方向 |
|---|---|---|
| Distributor 不可用 | 日志写入失败,客户端返回 5xx | 检查 Distributor 进程、网络、负载均衡器 |
| Ingester 崩溃 | 未刷写的数据丢失,写入延迟增加 | 检查 Ingester 内存、WAL 状态、哈希环 |
| Querier 查询超时 | 用户查询缓慢或失败 | 检查查询复杂度、Ingester 负载、存储延迟 |
| 哈希环分裂 | 数据分布不均,部分日志流不可用 | 检查 etcd/Consul 健康状态、网络分区 |
loki_distributor_ingester_clients 和 loki_ingester_flush_queue_length 指标作为 SLI 的组成部分,提前发现写入瓶颈。