Loki 架构基础:读写路径与核心组件
在深入蓝绿发布门禁下的日志压缩策略之前,必须先理解 Loki 的读写路径与核心组件。Loki 的设计哲学与 Prometheus 一脉相承:不全文索引日志内容,仅索引日志的元数据(标签)。这一设计大幅降低了存储成本,但也对查询模式和数据压缩提出了特定要求。本节将拆解 Distributor、Ingester、Querier 三大组件及其协作流程。
2.1 整体架构概览
Loki 采用微服务架构,核心组件通过 gRPC 通信。下图展示了日志从采集到存储、再到查询的完整路径:
架构要点:Loki 的写入路径是 采集器 → Distributor → Ingester → 对象存储;查询路径是 Querier → Ingester(内存)+ 对象存储(历史)→ 返回结果。理解这两条路径是后续压缩策略优化的基础。
2.2 Distributor:写入入口与数据分片
Distributor 是 Loki 写入路径的第一站,负责接收来自采集器(Promtail、Fluentd 等)的日志流。其主要职责包括:
- 数据验证:检查日志流的时间戳是否在可接受范围内(默认 ±30 分钟),标签格式是否合法。
- 哈希分片:对日志流的标签集合计算哈希值,通过一致性哈希环决定该流应由哪个 Ingester 实例处理。
- 负载均衡:将写入请求均匀分发到后端 Ingester 实例,避免单点瓶颈。
- 副本转发:根据配置的复制因子(默认 3),将同一份数据转发到多个 Ingester 实例,保证高可用。
| 特性 | 说明 | 成本影响 |
|---|---|---|
| 无状态 | Distributor 不存储任何数据,可随意水平扩展 | 扩展成本低,仅消耗 CPU 和网络带宽 |
| 一致性哈希 | 使用 Ring 协议(基于 Consul / etcd)确定数据归属 | 减少数据迁移,降低存储碎片 |
| 复制因子 | 默认 3,可调整为 2 或 1 以降低成本 | 降低副本数可节省 33%~66% 存储成本 |
| 速率限制 | 可配置每租户每秒最大日志量(bytes/s) | 防止突发流量导致 Ingester OOM |
成本优化提示:在蓝绿发布场景中,Distributor 的速率限制配置尤为重要。如果蓝环境或绿环境同时推送大量日志,可能导致 Ingester 内存压力骤增。建议为每个环境设置独立的租户 ID,并配置不同的速率限制。
2.3 Ingester:内存缓冲与数据压缩
Ingester 是 Loki 架构中最核心的组件,负责将接收到的日志数据在内存中缓冲、压缩并最终刷入对象存储。Ingester 的工作状态直接影响查询性能和存储成本。
2.3.1 写入流程
- 接收日志流:从 Distributor 接收日志条目,按标签组合(Stream)分组。
- 构建 Chunk:每个 Stream 对应一个或多个 Chunk(数据块),Chunk 是 Loki 的最小存储单元。
- 内存压缩:在 Chunk 写入对象存储前,使用 gzip / snappy / lz4 等算法进行压缩。
- 刷盘(Flush):当 Chunk 达到最大大小(默认 1.5MB)或最大时长(默认 30 分钟)时,异步写入对象存储。
- 内存释放:刷盘成功后,Ingester 释放该 Chunk 的内存,仅保留元数据索引。
2.3.2 关键参数与成本关系
| 参数 | 默认值 | 优化建议 | 成本影响 |
|---|---|---|---|
chunk_target_size |
1.5MB | 增大至 2~3MB 可减少存储文件数 | 降低对象存储 API 调用次数 |
chunk_idle_period |
30m | 缩短至 15m 可减少内存占用 | 降低 Ingester 内存需求 |
chunk_compression |
gzip | 使用 snappy 可提升写入速度 | 压缩比略低,但 CPU 开销更小 |
max_chunk_age |
2h | 保持默认,避免 Chunk 过大 | 平衡查询性能与存储效率 |
压缩原理:Loki 的 Chunk 压缩是在内存中完成的。Ingester 将同一 Stream 的多条日志合并为一个块,然后整体压缩。这种“先合并、后压缩”的策略比逐条压缩的压缩比高出 3~5 倍。在蓝绿发布场景中,如果两个环境的日志流标签一致,可以考虑合并推送以提升压缩效率。
2.4 Querier:查询引擎与数据合并
Querier 负责处理用户的 LogQL 查询请求。它需要从两个数据源获取结果:
- Ingester(内存数据):查询尚未刷入对象存储的最新日志。
- 对象存储(历史数据):查询已持久化的 Chunk 文件。
2.4.1 查询执行流程
- 接收查询请求:解析 LogQL 语句,提取标签选择器和时间范围。
- 并行查询:同时向所有 Ingester 实例发送查询(获取内存数据),并向对象存储发送索引查询(获取历史 Chunk 列表)。
- 数据合并:将来自 Ingester 和对象存储的结果按时间戳排序、去重。
- 过滤与聚合:执行 LogQL 中的过滤表达式(如
|= "error")和聚合函数(如rate())。 - 返回结果:将最终结果返回给 Grafana 或其他客户端。
2.4.2 查询性能优化
| 优化手段 | 原理 | 适用场景 |
|---|---|---|
| 索引加速 | 通过标签索引快速定位 Chunk,避免全表扫描 | 标签基数较低的查询 |
| Chunk 缓存 | 将热数据 Chunk 缓存在内存或 SSD 中 | 频繁查询的近期日志 |
| 查询并行度 | 将大时间范围拆分为多个子查询并行执行 | 长时间范围的聚合查询 |
| 预聚合 | 使用 Recording Rules 提前计算常用指标 | 仪表盘常用查询 |
蓝绿发布场景建议:在蓝绿发布期间,建议为每个环境创建独立的 Querier 实例或使用租户隔离。这样可以避免蓝环境的查询流量影响绿环境的查询性能,同时便于分别监控查询延迟和资源消耗。
2.5 组件协作与数据流总结
下表总结了三个核心组件在读写路径中的角色和协作关系:
| 组件 | 写入角色 | 查询角色 | 状态 | 扩展方式 |
|---|---|---|---|---|
| Distributor | 接收、验证、分片、转发 | 不参与 | 无状态 | 水平扩展(CPU 密集型) |
| Ingester | 缓冲、压缩、刷盘 | 提供内存数据 | 有状态(内存数据) | 水平扩展(内存密集型) |
| Querier | 不参与 | 合并、过滤、聚合 | 无状态 | 水平扩展(CPU + IO 密集型) |