为什么选择 Loki

传统 ELK(Elasticsearch + Logstash + Kibana)方案虽然功能强大,但存在两个核心痛点:

  • 存储成本高:Elasticsearch 将日志全文索引化,每条日志的索引膨胀可达原始数据的 3-5 倍
  • 运维复杂:ES 集群扩缩容、分片再平衡、索引生命周期管理复杂,生产集群维护成本高

Loki 由 Grafana Labs 开源,设计理念是"像 Prometheus 那样做日志"。它只对日志的标签(Labels)做索引,不对日志正文建索引,通过 LogQL 进行全文检索。这种设计使存储成本降低 10 倍以上。

本文基于 Loki 3.x,参考 Loki 官方文档

Loki vs ELK 对比

维度ELK (Elasticsearch)Loki
索引方式全文倒排索引仅索引标签,正文不索引
存储成本高(索引膨胀 3-5x)低(标签索引 + 压缩正文)
查询语言Lucene Query / KQLLogQL(类 PromQL 语法)
扩展性水平扩展,分片复杂微服务模式,组件独立扩展
适用场景全文检索、复杂分析日志监控、指标化查询、与 Grafana 联动
资源消耗高(JVM,内存大)低(Go 编写,内存友好)

Loki 并非要完全替代 ES。如果你的核心需求是全文检索和复杂文本分析,ES 仍是更好的选择。但对于 SRE 日志监控、指标告警、排障定位这类场景,Loki + Grafana 的组合在成本和效率上优势明显。

Loki 架构原理

日志存储模型

Loki 的核心设计:标签索引 + 压缩日志流

┌─────────────────────────────────────────────┐
│              Label Index (倒排)               │
│  app=nginx  →  stream_id_1, stream_id_2     │
│  env=prod   →  stream_id_1, stream_id_3     │
└──────────────────┬──────────────────────────┘
                   │ 查到 stream_id 后
┌─────────────────────────────────────────────┐
│           Chunk Store (压缩日志块)           │
│  stream_id_1: [ts] log line 1               │
│              [ts] log line 2               │
│              ... 压缩存储(Snappy/ZSTD)      │
└─────────────────────────────────────────────┘
  • Stream(日志流):由一组唯一标签集合定义,如 {app="nginx", env="prod"}。同一 Stream 的日志按时间排序追加。
  • Chunk(日志块):Stream 中的日志被切成固定大小的 Chunk(默认 1MB 或 1h),压缩后存储到对象存储(S3/GCS/本地文件)。
  • Label Index(标签索引):仅索引标签到 Stream 的映射关系,体积极小。

这种设计让索引数据量与标签基数成正比,而与日志量无关——每天 1TB 日志,只要标签不变,索引大小几乎不增长。

微服务架构组件

                    ┌──────────┐
   Promtail ──────► │ Distributor │ ── 写入 ──► Ingester ──► Chunk Store (S3)
                    └──────────┘                    │
   Grafana ────►  ┌──────────┐                Query Frontend
                   │  Querier  │ ◄─── 读取 ──── Ingester + S3
                   └──────────┘
                   ┌──────────┐
                   │ Compactor │ ─── 合并/过期清理
                   └──────────┘
组件职责
Distributor接收日志写入,校验标签,按 Stream 哈希分发给 Ingester
Ingester缓存最近写入的日志,满块后 flush 到对象存储
Querier处理查询请求,先查 Ingester 缓存再查对象存储
Query Frontend查询前置,拆分大查询、并行化、缓存结果
Compactor合并 Chunk、执行日志保留策略(TTL)

单机模式(Single Binary)将所有组件打包到一个进程,适合开发和小规模部署。微服务模式各组件独立部署,适合大规模生产环境。

Promtail 配置

Promtail 是 Loki 的日志采集 Agent,功能类似于 Logstash/Filebeat:采集、解析、打标签、推送到 Loki。

基础配置

# promtail-config.yaml
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml  # 记录每个日志文件的读取位置

clients:
  - url: http://loki:3100/loki/api/v1/push
    tenant_id: default           # 多租户场景下区分租户

scrape_configs:
  # 采集 Nginx 访问日志
  - job_name: nginx
    static_configs:
      - targets:
          - localhost
        labels:
          job: nginx
          app: nginx
          env: prod
          __path__: /var/log/nginx/*.log

  # 采集 Docker 容器日志(按容器名打标签)
  - job_name: docker
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 5s
        filters:
          - name: label
            values: ["logging=loki"]
    relabel_configs:
      - source_labels: ['__meta_docker_container_name']
        regex: '/(.*)'
        target_label: container_name
      - source_labels: ['__meta_docker_container_log_stream']
        target_label: stream
      - source_labels: ['__meta_docker_container_label_com_docker_compose_service']
        target_label: service

  # 采集 Kubernetes Pod 日志
  - job_name: kubernetes-pods
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels: [__meta_kubernetes_namespace]
        target_label: namespace
      - source_labels: [__meta_kubernetes_pod_name]
        target_label: pod
      - source_labels: [__meta_kubernetes_pod_label_app]
        target_label: app
      - source_labels: [__meta_kubernetes_pod_container_name]
        target_label: container

日志解析与标签提取

通过 pipeline_stages 对原始日志行进行解析,提取结构化字段作为标签或日志内的结构化数据:

scrape_configs:
  - job_name: app-json-logs
    static_configs:
      - targets: [localhost]
        labels:
          job: app
          app: my-service
          __path__: /var/log/app/*.log
    pipeline_stages:
      # 1. 解析 JSON 格式日志
      - json:
          expressions:
            level: level
            trace_id: trace_id
            method: http.method
            status: http.status_code
            duration_ms: duration_ms

      # 2. 将提取的字段设为标签
      - labels:
          level:
          status:
          method:

      # 3. 只保留 level=error 和 status>=500 的日志
      - match:
          selector: '{job="app"} |~ "ERROR"'
          action: keep

      # 4. 对耗时字段做数值化处理
      - template:
          source: duration_ms
          template: '{{ .ToFloat .duration_ms | printf "%.1f" }}'

      # 5. 输出结构化输出
      - output:
          source: message

Promtail pipeline 配置详见 Promtail 文档

日志位置管理

positions.yaml 记录每个日志文件的已读位置(offset),Promtail 重启后从断点继续读取。生产环境建议将此文件放在持久化卷上:

positions:
  filename: /data/promtail/positions.yaml

如果日志被轮转(如 logrotate 后旧文件被删除),Promtail 通过文件 inode 追踪,仍能正确续读新文件。

LogQL 查询语法

LogQL 是 Loki 的查询语言,语法设计参考 Prometheus 的 PromQL,分为日志查询指标查询两类。

流选择器(Stream Selector)

用花括号选择日志流,类似 PromQL 的标签选择器:

# 精确匹配
{app="nginx", env="prod"}

# 正则匹配
{app=~"nginx|gateway|api.*"}

# 排除标签值
{namespace="default", container!="istio-proxy"}

日志管道(Log Pipeline)

在流选择器后追加管道操作符,对日志行进行过滤和解析:

# 包含关键词
{app="nginx"} |= "error"

# 排除关键词
{app="nginx"} != "timeout"

# 正则匹配
{app="app"} |~ "ERROR|WARN|PANIC"

# JSON 解析后按字段过滤
{app="my-service"}
  | json
  | level="error"
  | status >= 500

# 日志格式化(提取字段后重组显示)
{app="my-service"}
  | json
  | line_format "{{.trace_id}} [{{.level}}] {{.message}}"

# 正则提取(非 JSON 日志)
{app="nginx"}
  | regexp `(?P<method>\w+) (?P<path>\S+) (?P<status>\d+) (?P<duration>\d+)`
  | status >= 500

指标查询(Metric Query)

将日志流转换为时间序列指标,这是 Loki 最强大的功能——直接从日志生成 Grafana 指标面板:

# 1. 日志行计数率(QPS)
sum(rate({app="nginx"}[5m])) by (status)

# 2. 提取字段后统计 P99 延迟
quantile_over_time(0.99,
  {app="my-service"} | json | unwrap duration_ms [5m]
) by (method)

# 3. 错误率
sum(rate({app="my-service"} | json | level="error" [5m]))
  /
sum(rate({app="my-service"} [5m]))

# 4. Top 10 耗时请求
topk(10,
  sum by (path) (
    rate({app="my-service"} | json | unwrap duration_ms [5m])
  )
)

unwrap 关键字用于从日志中提取数值字段进行聚合计算,支持 rateavg_over_timequantile_over_timesum_over_time 等聚合函数。

Grafana 集成

添加 Loki 数据源

在 Grafana → Configuration → Data Sources 中添加 Loki,URL 填 http://loki:3100。Grafana 10+ 支持自动检测 Loki 版本和功能。

日志面板

在 Dashboard 中新建 Log Panel,写入 LogQL:

{app="my-service", env="prod"}
  | json
  | line_format "{{.trace_id}} [{{.level}}] {{.method}} {{.path}} {{.duration_ms}}ms"

设置 VisualizationLogs,开启 Color lines based on field(按 level 字段着色)。

指标面板与日志面板联动

利用 Grafana 的 Dashboard 变量和面板链接,实现"指标异常 → 一键跳转日志"的排障工作流:

  1. 变量定义:创建 $trace_id 变量,从日志查询中获取值列表
# Variable query
label_values({app="my-service"} | json | level="error", trace_id)
  1. 指标面板:展示 P99 延迟趋势
quantile_over_time(0.99,
  {app="my-service"} | json | unwrap duration_ms [5m]
) by (path)
  1. 日志面板联动:在日志面板中使用变量过滤
{app="my-service"} | json | trace_id="$trace_id"
  1. 面板链接:在指标面板的 Data Links 中配置跳转链接,将当前时间范围和 Trace ID 传递给日志面板:
/d/abc123/my-dashboard?from=$__from&to=$__to&var-trace_id=$__field.trace_id

这样当指标面板上某个路径的 P99 飙高时,点击该数据点即可直接跳转到对应时间窗口的日志。

生产部署

完整 docker-compose.yml

以下是一个适合中小规模生产环境的完整部署方案,包含 Loki(单机模式)、Promtail、Grafana 和 MinIO(S3 兼容存储):

# docker-compose.yml
version: '3.8'

networks:
  loki-net:
    driver: bridge

volumes:
  minio-data:
  grafana-data:
  promtail-data:

services:
  # ===== MinIO: S3 兼容对象存储 =====
  minio:
    image: minio/minio:RELEASE.2024-01-01T00-00-00Z
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      - MINIO_ROOT_USER=loki
      - MINIO_ROOT_PASSWORD=loki123456
    command: server /data --console-address ":9001"
    volumes:
      - minio-data:/data
    networks: [loki-net]
    restart: unless-stopped

  # 初始化 MinIO bucket
  minio-init:
    image: minio/mc:RELEASE.2024-01-01T00-00-00Z
    depends_on: [minio]
    networks: [loki-net]
    entrypoint: >
      /bin/sh -c "
      sleep 5;
      mc alias set local http://minio:9000 loki loki123456;
      mc mb local/loki-chunks --ignore-existing;
      mc mb local/loki-ruler --ignore-existing;
      mc anonymous set download local/loki-chunks;
      "      
    restart: "no"

  # ===== Loki: 日志存储与查询 =====
  loki:
    image: grafana/loki:3.0.0
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/loki-config.yaml
    volumes:
      - ./loki-config.yaml:/etc/loki/loki-config.yaml:ro
    networks: [loki-net]
    restart: unless-stopped
    depends_on: [minio]

  # ===== Promtail: 日志采集 =====
  promtail:
    image: grafana/promtail:3.0.0
    ports:
      - "9080:9080"
    command: -config.file=/etc/promtail/promtail-config.yaml
    volumes:
      - ./promtail-config.yaml:/etc/promtail/promtail-config.yaml:ro
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - promtail-data:/data
    networks: [loki-net]
    restart: unless-stopped

  # ===== Grafana: 可视化 =====
  grafana:
    image: grafana/grafana:11.0.0
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin123
      - GF_INSTALL_PLUGINS=grafana-loki-datasource
    volumes:
      - grafana-data:/var/lib/grafana
    networks: [loki-net]
    restart: unless-stopped
    depends_on: [loki]

Loki 配置文件

# loki-config.yaml
auth_enabled: false  # 单租户模式

server:
  http_listen_port: 3100

common:
  path_prefix: /tmp/loki
  storage:
    filesystem:
      chunks_directory: /tmp/loki/chunks
      rules_directory: /tmp/loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

# 使用 S3 兼容存储(MinIO)
storage_config:
  aws:
    endpoint: minio:9000
    bucketnames: loki-chunks
    access_key_id: loki
    secret_access_key: loki123456
    insecure: true
    sse_encryption: false
  ruler:
    storage:
      type: local
      local:
        path: /tmp/loki/rules

# Schema 配置:定义日志的索引结构
schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: aws
      schema: v13
      index:
        prefix: index_
        period: 24h

# 保留策略
limits_config:
  retention_period: 720h          # 日志保留 30 天
  max_query_series: 5000
  max_query_parallelism: 16
  ingestion_rate_mb: 10           # 每租户每秒最大写入 10MB
  ingestion_burst_size_mb: 20
  reject_old_samples: true
  reject_old_samples_max_age: 168h # 拒绝超过 7 天的旧日志

compactor:
  working_directory: /tmp/loki/compactor
  compaction_interval: 10m
  retention_enabled: true
  retention_delete_delay: 2h
  retention_delete_worker_count: 150
  delete_request_store: filesystem

ruler:
  storage:
    type: local
    local:
      path: /tmp/loki/rules
  rule_path: /tmp/loki/rules-temp
  alertmanager_url: http://alertmanager:9093
  enable_api: true

Loki 微服务模式部署

大规模生产环境推荐微服务模式(Distributed),各组件独立扩展。Helm 部署:

helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

helm install loki grafana/loki-distributed \
  --namespace observability \
  --create-namespace \
  --set storage.type=s3 \
  --set storage.s3.endpoint=s3.us-east-1.amazonaws.com \
  --set storage.s3.bucket=loki-chunks-prod

微服务模式下每个组件都可以独立水平扩展:

# values.yaml 关键配置
loki:
  structuredConfig:
    ingester:
      max_chunk_age: 2h
      chunk_idle_period: 30m
      chunk_target_size: 1572864    # 1.5MB
    query_scheduler:
      max_outstanding_requests_per_tenant: 4096

  # Ingester 副本数(写入吞吐瓶颈时扩容)
  ingester:
    replicas: 3

  # Querier 副本数(查询并发瓶颈时扩容)
  querier:
    replicas: 4

  # Query Frontend 拆分大查询
  queryFrontend:
    replicas: 2

S3 存储后端配置

生产环境使用 AWS S3 作为存储后端时,注意以下配置项:

storage_config:
  aws:
    endpoint: s3.us-east-1.amazonaws.com
    bucketnames: loki-chunks-prod
    access_key_id: ${S3_ACCESS_KEY}     # 建议通过环境变量注入
    secret_access_key: ${S3_SECRET_KEY}
    region: us-east-1
    insecure: false
    sse_encryption: true               # 启用 SSE 加密
    http_config:
      idle_conn_timeout: 90s
      response_header_timeout: 30s

# 存储生命周期:利用 S3 Lifecycle 自动归档
# 在 S3 控制台配置 bucket lifecycle:
# - 30 天后转为 Glacier 归档
# - 90 天后删除

日志告警

Loki 内置 Ruler 组件,支持基于 LogQL 的告警规则:

# /tmp/loki/rules/alerts.yaml
groups:
  - name: app-alerts
    interval: 30s
    rules:
      # 错误日志突增
      - alert: HighErrorRate
        expr: |
          sum(rate({app="my-service"} | json | level="error" [5m]))
            by (app)
          /
          sum(rate({app="my-service"} [5m]))
            by (app)
          > 0.05          
        for: 5m
        labels:
          severity: critical
          team: sre
        annotations:
          summary: "{{ $labels.app }} error rate > 5%"
          description: "Current error rate: {{ $value }}"

      # 特定关键词告警
      - alert: OOMKilled
        expr: sum(count_over_time({container="my-service"} |~ "OOMKilled|OutOfMemoryError" [5m])) > 0
        labels:
          severity: critical
        annotations:
          summary: "Container OOMKilled detected"

总结

Loki + Promtail + Grafana 构建了一套轻量高效的日志监控体系,与 Prometheus 指标监控形成互补。落地建议:

  • 标签设计先行:标签基数(cardinality)直接影响索引大小和查询性能。避免将 user_idrequest_id 等高基数值作为标签,它们应放在日志正文中通过管道提取
  • 控制标签基数:单个标签的不同值不超过数千。container_namepod_name 在大规模 K8s 集群中基数可能很高,需评估
  • 合理设置保留期:核心业务日志保留 30-90 天,访问日志保留 7-14 天,通过 limits_config.retention_period 控制
  • 日志结构化:应用输出 JSON 格式日志(含 leveltrace_idduration_ms 等字段),Promtail 可直接解析提取,大幅提升查询效率
  • 存储成本优化:S3 存储比 ES 存储成本低一个数量级。配合 S3 Lifecycle 策略(热数据标准存储 → 冷数据 Glacier 归档 → 自动删除),进一步压缩长期日志成本
  • 告警联动:利用 Loki Ruler 将日志异常转为告警,与 Prometheus Alertmanager 统一管理告警分发

Grafana Loki 官方文档:https://grafana.com/docs/loki/latest/

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. loki:3100 — loki:3100,参考了loki:3100相关内容
  2. [loki:3100。Grafana](http://loki:3100。Grafana) — loki:3100。Grafana,参考了loki:3100。Grafana相关内容
  3. Loki 官方文档 — Grafana,参考了Loki 官方文档相关内容
  4. Promtail 文档 — Grafana,参考了Promtail 文档相关内容