用 Loki + Grafana 搭建轻量日志系统:采集、查询与告警

中小规模系统上日志系统,ELK 全家桶往往太重:光 Elasticsearch 的内存和磁盘规划就够折腾一阵。Loki 的思路不同——只索引日志的标签,不索引全文,存储和运维成本低得多,再配合 Grafana 与指标告警共用一个界面。本文在一台机器上把 Loki 日志系统搭起来:采集、查询、告警全流程走通,并重点讲清标签设计这条最容易踩的红线。

适用环境:一台 Linux 主机(示例 IP 192.0.2.20),Docker 与 Docker Compose 可用;日志来源先以本机文件与 systemd journal 为例,组件版本为 Loki 3.x、Grafana 11.x。

前置条件

  • 主机已安装 Docker 与 Docker Compose v2,磁盘预留空间按日志量与保留期估算(先留 50 GB 起步)。
  • 准备好目录规划:/opt/loki 下放 compose 文件与配置,数据卷独立挂载便于备份。
  • 明确要采集的日志路径清单,以及每类日志对应的服务名——标签在采集端决定,进了 Loki 再改成本很高。
  • Grafana 如已有独立部署可直接复用,只需给 Loki 开数据源;本文用 Compose 一并启动便于演示。

第一步:架构与组件分工

三件套分工明确:采集端(Promtail 或其继任者 Grafana Alloy)负责读日志文件、打标签、推送;Loki 负责接收、按标签索引、落盘存储和执行查询;Grafana 负责查询展示与告警。Promtail 已进入维护收尾阶段,新部署建议直接用 Alloy,它的配置语法是 River 格式,功能与 Promtail 对应;本文采集示例以 Promtail 的 YAML 写法为主,过渡到 Alloy 时对照组件名替换即可。单节点 Loki 用 monolithic 模式加本地文件系统存储即可起步,量大后再演进到对象存储与读写分离。

第二步:单机 Compose 部署

# docker-compose.yaml
services:
  loki:
    image: grafana/loki:3.1.0
    container_name: loki
    command: -config.file=/etc/loki/local-config.yaml
    ports:
      - "3100:3100"
    volumes:
      - ./loki-config.yaml:/etc/loki/local-config.yaml:ro
      - loki-data:/loki
    restart: unless-stopped
  grafana:
    image: grafana/grafana:11.2.0
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=change-me-please
    volumes:
      - grafana-data:/var/lib/grafana
    restart: unless-stopped
volumes:
  loki-data:
  grafana-data:

Loki 的关键配置是保留期与存储路径,其余先用官方单机默认值:

# loki-config.yaml(节选关键项)
auth_enabled: false
server:
  http_listen_port: 3100
common:
  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: 720h        # 全局保留 30 天
compactor:
  working_directory: /loki/compactor
  retention_enabled: true       # 开启后保留期才真正生效

执行 docker compose up -d 后,curl http://192.0.2.20:3100/ready 返回 ready 即 Loki 就绪。Grafana 里添加 Loki 数据源,URL 填 http://loki:3100(同 compose 网络内用服务名)。

第三步:采集配置与标签设计

Promtail 两类典型采集:文件采集用静态 job 加路径匹配,journal 采集直接读 systemd 日志。标签设计是整套系统的地基,原则只有三条:标签只放低基数、用于筛选流的字段(服务名、环境、主机、日志级别);绝不放用户 ID、请求 ID、订单号这类高基数值——每个不同的标签组合都会生成一条独立的日志流,海量组合会把索引和内存打爆;宁可把高基数字段留在日志正文里靠查询时解析。

# promtail-config.yaml(节选)
server:
  http_listen_port: 9080
positions:
  filename: /tmp/positions.yaml
clients:
  - url: http://192.0.2.20:3100/loki/api/v1/push
scrape_configs:
  - job_name: app-files
    static_configs:
      - targets: [localhost]
        labels:
          job: app
          host: node-01
          __path__: /var/log/app/*.log
    pipeline_stages:
      - json:
          expressions:
            level: level
      - labels:
          level:            # 从日志内容提取级别做标签(级别取值有限,属低基数)
  - job_name: journal
    journal:
      max_age: 12h
      labels:
        job: systemd-journal
    relabel_configs:
      - source_labels: [__journal__systemd_unit]
        target_label: unit

第四步:LogQL 常用查询

LogQL 查询分两段:前半是流选择器(按标签筛流),后半是管道(对正文过滤、解析、聚合)。常用写法:

# 1. 看某服务最近的 ERROR 日志
{job="app"} |= "ERROR"

# 2. JSON 解析后按字段过滤
{job="app"} | json | status >= 500

# 3. 正则提取字段再过滤
{job="app"} | regexp "(?P<method>GET|POST) (?P<path>/\\S+)" | path =~ "/api/.*"

# 4. 指标查询:最近 5 分钟每分钟错误数(可画图、可告警)
sum(rate({job="app"} |= "ERROR" [5m]))

# 5. 按服务分组统计错误量
sum by (job) (rate({job=~".+"} |~ "ERROR|FATAL" [5m]))

第五步:日志告警的最简配置

最省事的是在 Grafana 里用统一告警:新建 Alert rule,数据源选 Loki,查询填上面的 rate 表达式(如 5 分钟内错误速率),条件设为最近求值大于阈值(例如持续 3 分钟 > 0.1 条/秒),再绑定通知策略发到值班群 Webhook 或邮件。这样日志告警和指标告警在同一个界面管理,不必再单独维护 Loki 的 ruler 组件。规模上来后,再考虑把规则下沉到 ruler 用文件管理、纳入版本控制。

与 ELK 的取舍

Loki 赢在轻:不建全文索引,存储便宜、组件少、和 Prometheus/Grafana 生态天然一体;输在查:全文检索能力弱,按关键词大海捞针式的排查不如 Elasticsearch 顺手,复杂分析更吃亏。经验判断:日志主要用于「按服务、按时间段看错误和上下文」,且团队已在用 Grafana,选 Loki;日志要做审计检索、全文搜索和复杂聚合分析的,ELK(或 OpenSearch)更合适。两者也可以共存:核心审计日志进 ELK,应用运行日志进 Loki。

验证

往 /var/log/app/app.log 写一条 JSON 格式测试日志,在 Grafana 的 Explore 里选 Loki 数据源,执行 {job="app"} 应能看到该条;改用 |= "ERROR" 过滤确认管道生效;再执行 sum(rate({job="app"} |= "ERROR" [5m])) 能出数值。最后手动刷几条错误日志,观察告警规则进入 Pending 并触发通知,整条链路即通。

常见问题

推送被拒,日志报 out of order 或 label 冲突。写入被拒先看 Loki 返回信息:最常见是同一流里时间戳乱序(老版本严格,3.x 已放宽但跨度太大仍拒)和采集端给同一流打了互相冲突的标签。标签打错的典型场景是把动态值误设为标签,先在采集端用 promtail --dry-run 或 Alloy 的日志核对实际推出的标签组合。

查询很慢甚至超时。先缩小时间范围和标签范围:只给一个大 job 不加任何收敛标签的全量扫是最慢的查法。时间跨度大时把 max_query_length 等限制适当放开,并确认查询没有对超大结果做全文正则。根子上还是标签设计——流切得越合理,查询越快。

配了 retention_period 磁盘却不释放。保留期要靠 compactor 执行:retention_enabled 必须为 true,且 compactor 正常运行;另外 schema 的保留是按索引周期粒度删的,刚到期不会立刻见效,等一个压实周期再看磁盘趋势。

日志突然断流。查采集端 positions 文件是否损坏、目标日志是否被切割后改名导致路径匹配失效,以及 Loki 的 ingestion rate limit 是否因突发量把流限流(日志里有 rate limit 字样),按顺序排查基本都能定位。

小结

Loki 的搭建本身不难,难的是把标签纪律守住:低基数进标签,高基数留正文。先单机跑通采集、查询、告警三件套,再按量演进到对象存储和规则下沉,这套轻量日志系统足够支撑大多数中小团队的日常排障。

发表回复

后才能评论