3. Loki 基础:日志聚合系统架构、LogQL 查询语言与标签索引机制

Loki 是 Grafana Labs 受 Prometheus 启发设计的一款水平可扩展、高可用、多租户的日志聚合系统。与 Elasticsearch 等全文索引方案不同,Loki 的核心设计哲学是仅对日志元数据(标签)建立索引,而不对日志内容建立全文索引。这一设计大幅降低了存储成本和运维复杂度,使其在云原生环境中成为日志可观测性的首选方案。

3.1 Loki 核心架构与组件拓扑

Loki 采用微服务架构,各组件可独立扩展。其数据流遵循 写入路径 与 查询路径 两条主线。

架构要点:Loki 将日志数据压缩存储于对象存储(如 S3、GCS、MinIO)中,内存中仅维护索引与最近写入的日志块(Chunk),以此实现低成本与高性能的平衡。
Loki 核心组件拓扑与数据流 写入路径 Distributor Ingester 对象存储 查询路径 Querier Query Frontend 索引与元数据 Index Gateway Compactor 关键设计原则 • 仅索引标签,不索引日志内容 • 日志块压缩后存储于对象存储 • 各组件可独立水平扩展

3.1.1 核心组件职责

组件 职责 扩展性
Distributor 接收日志写入请求,验证数据合法性,基于哈希环将日志流分发至对应 Ingester 无状态,可水平扩展
Ingester 接收日志并构建 Chunk(压缩块),将 Chunk 刷入对象存储;维护内存中的最近日志 有状态,通过一致性哈希分片
Querier 处理 LogQL 查询,从 Ingester(热数据)和对象存储(冷数据)中获取日志 无状态,可水平扩展
Query Frontend 查询缓存、拆分与合并;将大查询拆分为小查询并行执行 无状态,可水平扩展
Index Gateway 管理标签索引,加速查询时的标签匹配 有状态,可分区
Compactor 合并与重排对象存储中的索引和 Chunk,提升查询效率 无状态,单实例运行

3.2 LogQL 查询语言

LogQL 是 Loki 的查询语言,语法受 PromQL 启发,但专门针对日志数据设计。LogQL 查询由 日志流选择器 和 日志管道 两部分组成。

3.2.1 基本查询结构

{label="value"} | logfmt | line_format "{{.message}}" | unwrap duration | rate()

查询从左至右执行:

  1. 日志流选择器(花括号内):通过标签匹配筛选日志流
  2. 管道操作符(竖线后):对匹配的日志行进行过滤、解析、格式化、计算

3.2.2 常用管道操作符

操作符 用途 示例
|= 行包含(字符串匹配) {job="api"} |= "error"
!= 行不包含 {job="api"} != "debug"
|~ 行匹配正则表达式 {job="api"} |~ "5[0-9]{2}"
logfmt 解析 logfmt 格式日志为结构化字段 {job="api"} | logfmt
json 解析 JSON 格式日志为结构化字段 {job="api"} | json
line_format 重写日志行内容(支持模板语法) | line_format "{{.status}}"
unwrap 提取数值字段用于指标计算 | unwrap duration
rate() 计算日志速率(每秒条数) rate({job="api"}[5m])
实战案例:查询最近 1 小时内,API 服务中状态码为 500 的日志,并提取响应时间字段计算 P99 延迟。
quantile_over_time(0.99,
  {job="api"} |= "500" | logfmt | unwrap response_time_ms
  [1h]
)

3.3 标签索引机制

Loki 的索引机制是其性能与成本优势的核心。与 Elasticsearch 对日志全文建立倒排索引不同,Loki 仅对标签(Labels)建立索引,日志内容以压缩块形式存储,不参与索引。

3.3.1 标签设计原则

  • 高基数标签需谨慎:如 user_id、request_id、ip 等取值过多的标签会导致索引膨胀,影响性能。建议将高基数信息放入日志内容而非标签。
  • 推荐的低基数标签:job、namespace、pod、container、level、service。
  • 标签命名规范:使用小写字母、下划线,避免特殊字符。推荐遵循 Prometheus 标签命名惯例。
Loki 标签索引与日志存储结构 标签索引(倒排索引) job="api" → [chunk_1, chunk_3, chunk_5] namespace="prod" → [chunk_1, chunk_2, chunk_4] level="error" → [chunk_1, chunk_5] pod="web-1" → [chunk_1, chunk_3] 标签 → Chunk 列表的映射关系 Chunk 存储(对象存储) Chunk 1 时间范围: 10:00:00 - 10:05:00 | 压缩日志行 Chunk 2 时间范围: 10:05:00 - 10:10:00 | 压缩日志行 Chunk 3 时间范围: 10:10:00 - 10:15:00 | 压缩日志行

3.3.2 索引查询流程

  1. 标签匹配:查询中的标签选择器(如 {job="api", level="error"})在倒排索引中查找匹配的 Chunk 列表
  2. Chunk 定位:取多个标签匹配结果的交集,得到候选 Chunk 集合
  3. Chunk 扫描:从对象存储中读取候选 Chunk,在解压后的日志内容中执行管道操作符过滤
  4. 结果聚合:Querier 合并来自多个 Ingester 和对象存储的结果,返回给客户端
性能陷阱:如果查询未指定时间范围,Loki 将扫描所有匹配标签的 Chunk,导致查询缓慢。始终在查询中显式指定时间范围(如 [1h] 或通过 Grafana 时间选择器),以限制扫描范围。

3.4 标签与 LogQL 协同优化

在 OPA 与 Loki 协同的场景中,标签设计直接影响安全策略的生效粒度与查询性能。以下为推荐的最佳实践:

场景 推荐标签 说明
多租户隔离 tenant_id 每个租户使用独立标签,OPA 策略可基于此标签限制查询范围
服务拓扑 service、namespace 快速定位特定服务的日志,配合 OPA 实现服务级访问控制
安全审计 user、action 低基数用户标签(如角色),避免使用高基数的 user_id
异常检测 level、status_code 配合 LogQL 的 rate() 和 quantile_over_time() 实现告警
OPA 集成提示:在 OPA 策略中,可以通过 input.labels 获取查询中的标签选择器,从而动态判断用户是否有权访问特定标签组合。例如,限制非管理员用户只能查询 namespace="dev" 的日志。