技术博客

Loki 日志聚合实战:Promtail 采集、LogQL 查询与生产部署

系统讲解 Grafana Loki 日志系统的生产部署:Loki 架构与存储原理(标签索引 vs 全文索引)、Promtail 多场景日志采集配置(Kubernetes/文件/系统日志)、LogQL 查询语言从基础到进阶、Loki 日志告警配置、Loki 分布式部署与存储配置(S3/阿里云 OSS 后端)。

Loki日志LogQLPromtail可观测性监控Grafana

Loki 是 Grafana Labs 开发的日志聚合系统,定位是“为日志设计的 Prometheus”。与 ELK 全文索引不同,Loki 只索引元数据标签(轻量),日志内容存到对象存储(便宜),查询时即时扫描(LogQL 过滤)。这使 Loki 的成本比 Elasticsearch 低 5-10 倍。

Loki vs ELK 架构对比

ELK(Elasticsearch + Logstash + Kibana):
  Logstash 采集 → Elasticsearch(全文索引,每行都建索引)→ Kibana
  优点:搜索任意字段超快(已建索引)
  缺点:存储成本高、资源消耗大(Elasticsearch 内存吞噬机)

Loki(PLG 栈:Promtail + Loki + Grafana):
  Promtail 采集 → Loki(只索引标签,内容存对象存储)→ Grafana
  优点:存储成本低、资源消耗小、与 Prometheus/Grafana 原生集成
  缺点:全文搜索慢(查询时扫描)

选型建议:
  大规模生产、需要全文搜索 → ELK(或 OpenSearch)
  云原生、成本敏感、Kubernetes 场景 → Loki(与 Prometheus 体系整合)

一、Loki 部署

单机部署(All-in-One 模式,适合开发/小规模)

# docker-compose.yml
version: '3'
services:
  loki:
    image: grafana/loki:3.2.0
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/config.yml
    volumes:
      - ./loki-config.yml:/etc/loki/config.yml
      - loki-data:/loki
      
  promtail:
    image: grafana/promtail:3.2.0
    volumes:
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - ./promtail-config.yml:/etc/promtail/config.yml
    command: -config.file=/etc/promtail/config.yml

  grafana:
    image: grafana/grafana:11.2.0
    ports:
      - "3000:3000"
    environment:
      - GF_DATASOURCE_PROMETHEUS_URL=http://prometheus:9090
    
volumes:
  loki-data:
# loki-config.yml(单机配置)
auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096

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

schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

limits_config:
  retention_period: 30d    # 日志保留 30 天
  max_streams_per_user: 0  # 0 = 不限制
  ingestion_rate_mb: 16    # 每秒最大摄入 16MB

生产部署(Simple Scalable 模式 + S3 存储)

# loki-config.yml(生产配置,使用阿里云 OSS)
auth_enabled: true

server:
  http_listen_port: 3100

common:
  path_prefix: /loki
  storage:
    s3:
      endpoint: oss-cn-shanghai-internal.aliyuncs.com
      bucketnames: my-loki-logs
      access_key_id: ${AWS_ACCESS_KEY_ID}
      secret_access_key: ${AWS_SECRET_ACCESS_KEY}
      s3forcepathstyle: true
      insecure: false
  replication_factor: 3   # 3 副本高可用

schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: s3
      schema: v13
      index:
        prefix: loki_index_
        period: 24h

limits_config:
  retention_period: 90d           # 保留 90 天
  per_stream_rate_limit: 10MB    # 每个日志流限速
  ingestion_burst_size_mb: 32
  max_query_length: 0             # 查询时间范围不限制
  max_query_parallelism: 32       # 最大并行查询数
  
compactor:
  working_directory: /loki/compactor
  compaction_interval: 1h
  retention_enabled: true
  delete_request_store: s3

二、Promtail 日志采集配置

采集 Kubernetes Pod 日志

# promtail-config.yml(Kubernetes 场景)
server:
  http_listen_port: 9080

positions:
  filename: /tmp/positions.yaml   # 记录读取位置,重启后续读

clients:
  - url: http://loki:3100/loki/api/v1/push
    batchwait: 1s          # 积累 1 秒再批量发送
    batchsize: 1048576     # 单批最大 1MB
    timeout: 10s

scrape_configs:
  # 采集 Kubernetes Pod 日志
  - job_name: kubernetes-pods
    kubernetes_sd_configs:
      - role: pod
    pipeline_stages:
      # 1. 解析 Docker JSON 格式日志
      - cri: {}
      # 2. 过滤特定日志
      - drop:
          expression: '.*health.*check.*'   # 丢弃健康检查日志
      # 3. 提取字段作为标签(从日志内容)
      - regex:
          expression: '.*level=(?P<level>[a-zA-Z]+).*'
      - labels:
          level:                             # 把 level 字段变成 Loki 标签
    relabel_configs:
      # 只采集有 logging: "true" 注解的 Pod
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: true
      # 把 Pod 元信息变成 Loki 标签
      - source_labels: [__meta_kubernetes_pod_namespace]
        target_label: namespace
      - source_labels: [__meta_kubernetes_pod_name]
        target_label: pod
      - source_labels: [__meta_kubernetes_pod_container_name]
        target_label: container
      - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_pod_name]
        target_label: job
        separator: /
        replacement: $1

  # 采集系统日志(/var/log/syslog)
  - job_name: system
    static_configs:
      - targets:
          - localhost
        labels:
          job: syslog
          host: ${HOSTNAME}
          __path__: /var/log/syslog

  # 采集应用日志(指定路径,带 glob 通配)
  - job_name: nginx-access
    static_configs:
      - targets:
          - localhost
        labels:
          job: nginx
          __path__: /var/log/nginx/*access*.log
    pipeline_stages:
      # 解析 Nginx 日志格式
      - regex:
          expression: '^(?P<remote_addr>\S+) - (?P<remote_user>\S+) \[(?P<time_local>.*)\] "(?P<method>\S+) (?P<request>\S+) (?P<protocol>\S+)" (?P<status>\d+) (?P<bytes>\d+)'
      - labels:
          method:
          status:           # 把 HTTP 状态码变成标签(方便过滤)

三、LogQL 查询语言

LogQL 是 Loki 的查询语言,类似 PromQL 的风格。

日志查询(Log Queries)

# 基础查询:所有来自 production 命名空间的日志
{namespace="production"}

# 多标签过滤
{namespace="production", container="nginx"}

# 正则匹配标签
{namespace=~"production|staging"}

# 日志内容过滤(在标签流中过滤包含特定字符串的行)
{namespace="production"} |= "ERROR"          # 包含 ERROR
{namespace="production"} != "DEBUG"          # 不包含 DEBUG
{namespace="production"} |~ "timeout|error"  # 正则匹配
{namespace="production"} !~ "health.*check"  # 排除健康检查

# 多步骤过滤(pipeline)
{namespace="production", job="nginx"}
  |= "status=5"           # 先过滤 5xx 错误
  |~ "GET|POST"           # 再过滤 GET/POST 请求
  | json                  # 解析 JSON 格式日志
  | status >= 500         # 提取字段后再过滤

解析器(Parse Logs)

# JSON 格式日志
{job="app"} | json
# 自动提取 JSON 字段为标签,如 .level .msg .timestamp

# 指定字段提取
{job="app"} | json level, msg, timestamp
| level="error"           # 提取后可以用字段过滤

# Logfmt 格式(key=value 格式,如 Go 应用)
{job="go-app"} | logfmt
| level="error"

# 正则提取
{job="nginx"} 
  | regexp `(?P<method>\w+) (?P<path>/[^ ]+) HTTP/(?P<version>[\d.]+)"`
  | method="POST"         # 只看 POST 请求

# 自定义 pattern
{job="app"} 
  | pattern `<timestamp> <level> <msg>`
  | level="ERROR"

指标查询(Metric Queries)

# 每分钟的日志行数
count_over_time({namespace="production"}[1m])

# 每分钟的错误数
count_over_time({namespace="production"} |= "ERROR" [1m])

# 错误率(每分钟错误数 / 总行数)
sum(rate({namespace="production"} |= "ERROR" [5m]))
/
sum(rate({namespace="production"}[5m]))

# 按服务统计错误率(聚合)
sum by (container) (
  rate({namespace="production"} |= "ERROR" [5m])
)

# 日志量排行(找出哪个服务日志最多)
topk(10, 
  sum by (container) (
    bytes_over_time({namespace="production"}[1h])
  )
)

# P99 响应时间(从日志中提取延迟)
quantile_over_time(0.99,
  {job="nginx"}
    | json
    | request_time != ""
    | unwrap request_time [5m]
) by (host)

四、Loki 日志告警

# Loki 告警规则(写入 Loki 配置中的 ruler 路径)
# /loki/rules/production/alerts.yml
groups:
  - name: application-alerts
    rules:
      # 错误率告警
      - alert: HighErrorRate
        expr: |
          sum(rate({namespace="production"} |= "ERROR" [5m])) by (namespace, container)
          /
          sum(rate({namespace="production"}[5m])) by (namespace, container)
          > 0.05
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.container }} 错误率过高"
          description: "{{ $labels.namespace }}/{{ $labels.container }} 错误率 {{ $value | humanizePercentage }} 超过 5%"

      # 日志停止(服务可能挂了)
      - alert: NoLogsReceived
        expr: |
          sum(rate({namespace="production", container=~"api-server|worker"}[5m])) by (container) == 0
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.container }} 停止产生日志"
          
      # 特定关键词告警
      - alert: OutOfMemoryError
        expr: |
          count_over_time({namespace="production"} |= "OutOfMemoryError" [5m]) > 0
        labels:
          severity: critical
        annotations:
          summary: "检测到 OutOfMemoryError"

五、Loki 查询优化

# 优化技巧1:标签过滤放前面(减少扫描的日志流数量)
# 差:
{namespace="production"} |= "ERROR" | json | container="api"

# 好:(标签越精确,扫描越少)
{namespace="production", container="api"} |= "ERROR" | json

# 优化技巧2:控制时间范围(Loki 按时间分块存储)
# 避免查询超长时间范围(例如查 30 天),性能差
# 建议:精确指定时间段,或使用短时间窗口 + Recording Rules

# 优化技巧3:使用 Recording Rules 预计算高频指标
# 在 Loki ruler 中定义,预计算结果存到 Prometheus
groups:
  - name: loki-recording
    interval: 1m
    rules:
      - record: job:log_errors:rate5m
        expr: sum by (namespace, container) (rate({namespace=~".+"}|= "ERROR"[5m]))

小结

Loki 的核心设计哲学是标签索引 + 内容即时扫描:只给日志打最重要的几个标签(namespace、container、job),日志正文不建索引(存对象存储很便宜),查询时用 LogQL pipeline 过滤(|=| json| regexp)。最佳实践:标签数量控制在 10 个以内(标签基数太高会导致 Loki 性能下降);把高基数字段(请求 ID、用户 ID)留在日志内容里,不要变成标签;用 Pipeline 阶段解析日志| json| logfmt),提取需要聚合的字段再做指标查询。