跳转至

Loki 日志聚合实战:单集群部署与日志管道优化

我这台机器上跑着 Prometheus 三年,指标体系很成熟。但日志一直是短板——早期是 ELK,5 节点 ES 集群、每台 256G 内存,一年 400 TB 存储,成本比整套 Prometheus 还高。后来切到 Loki,跑完一年后的现实:存储成本砍到 1/8,P99 查询延迟跟 ES 差距 200ms 以内(前提是 label 设计对),Grafana 一个数据源搞定指标+日志+trace。

这篇文章把我从 ELK 迁到 Loki 的过程、部署模板、log collection 选型、label 设计原则、LogQL 速查、5 个真实事故复盘摊开讲。目标读者是那种"没预算上 ELK、也不想自己造轮子"的一线运维/SRE。所有配置都可以直接抄,但抄之前先想清楚 label 怎么设计——这一条决定了 Loki 是帮你省钱还是给你挖坑。

一、为什么从 ELK 迁到 Loki——架构差异与成本账

1.1 架构差异

ELK 写入路径:Filebeat → Logstash → Elasticsearch。ES 每条日志写入做倒排索引(inverted index),每个 token 映射到"哪些文档包含它"。全文搜索靠这个索引——搜 error timeout 500ms,ES 在 index 里找三个 token 都命中的文档。

Loki 完全不同。日志按 label 组织成 stream,每个 stream 是一串按时间追加的 chunk,chunk 里存原始日志文本(LZ4 压缩)。查询流程:先用 label 选出相关 stream → 只在这几个 stream 的 chunk 里做 regex/logql 匹配 → 返回命中的原始行。日志本体不建全文索引,只在 label 上有 index。

维度 Elasticsearch Loki
索引对象 每个 token(倒排) 仅 label
Index 大小 ≈ 原文 80-100% ≈ 原文 1-5%
全文搜索速度 极快 慢(扫 chunk)
按 label 过滤速度 快 极快
存储成本 高 低 5-10 倍
查询语言 Lucene DSL LogQL
运维复杂度 高(分片、rebalance) 低(单二进制起)

ES 把力气花在"任你怎么搜都能秒回",Loki 把力气花在"只要 label 对得上,秒回"。 你的查询模式决定选谁。

1.2 我的真实成本账

我在 200 节点 K8s 集群上跑过两年 ELK、一年半 Loki,规模都差不多(每天约 3-5 TB 原始日志,保留 90 天):

ELK 侧:5 节点 × 256G 内存 × 2TB SSD × 4 盘 + Logstash/Kibana/Filebeat。一年存储成本 ≈ 60 万,P99 查询延迟 300-800ms,日常维护分片规划、rebalance、hot-warm-cold 分层。

Loki 侧:4 节点 × 64G 内存 × 2TB SSD(后扩 3TB),微服务模式。一年存储成本 ≈ 8 万,P99 延迟 200-600ms,主要维护就是 retention 检查和 compactor 监控。

注意:这套对比的前提是"查询都以 label 为主"。如果日常要跑 * :~ "user_id=12345 AND error" 这种没 label 的全文搜,Loki 会慢得多,甚至超时。我这边 Grafana 上 90% 的查询都有明确 app/env/severity 前缀,剩下 10% 是故障排查时的全文搜,能接受慢一点。

1.3 三种部署模式

  1. Single binary(单二进制):所有组件一个进程,本地磁盘存数据。适合 PoC、小规模(< 100 万 stream)、"日志不是核心资产"。
  2. Microservice(微服务):distributor / ingester / querier / query-frontend / query-scheduler / compactor / index-gateway 独立部署。适合中大规模、多机房、要 HA。
  3. Loki Enterprise / Grafana Cloud:Grafana 官方托管,带多租户、更好的查询优化。贵但省心。

我在生产用第二种,但入门建议从单二进制开始——10 分钟能跑起来,先让团队感受 Loki 的工作方式,再考虑要不要上微服务。

二、Loki 部署路径:Docker Compose 单集群(10 分钟跑起来)

我维护的一份可直接用的模板,git clone 下来改两个 IP 就能跑,Loki + Grafana + Alloy 一个 compose 文件搞定。

2.1 目录结构

mkdir -p ~/loki-stack/{etc/loki,etc/grafana/provisioning/datasources,etc/alloy,data/{loki,grafana,alloy}}
cd ~/loki-stack

2.2 loki-config.yaml

etc/loki/loki-config.yaml:

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: loki_index_
        period: 24h

limits_config:
  max_line_size: 32KB
  max_entry_size_safe: 512KB
  max_label_names_per_series: 30
  max_entries_limit_per_query: 5000
  max_query_series: 500
  max_query_bytes: 200MB
  retention_period: 90d

compactor:
  working_directory: /loki/compactor
  compaction_interval: 10m
  retention_enabled: true
  delete_request_store: filesystem
  shared_store: filesystem

analytics:
  reporting_enabled: false

关键几项: - replication_factor: 1 是单机。生产微服务要设 3,否则单点数据丢失。 - store: tsdb 是 2024 年后推荐,比 boltdb 更适合海量 stream。 - retention_period: 90d 一定要写,不写就是永不删除。 - compactor.retention_enabled: true 是让 retention 真正生效的开关——只写 retention_period 不开这个,数据不会删(详见 6.3)。 - max_query_series: 500 是防 label 基数爆炸的关键。

2.3 Alloy 配置(替代 Promtail)

etc/alloy/config.alloy:

// 采集本机 /var/log/**/*.log
local.file_match "syslog" {
  path_target    = "/var/log/**/*.log"
  ignore_missing = true
}
loki.source.file "syslog" {
  targets    = local.file_match.syslog.targets
  forward_to = loki.write.default.receiver
}

// 采集 Docker 容器日志
docker.container "docker" {
  match_all = true
}
loki.source.docker "docker" {
  targets    = docker.container.docker.targets
  forward_to = loki.write.default.receiver
}

// 采集 journald
loki.source.journald "system" {
  forward_to = loki.write.default.receiver
}

loki.write "default" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
    external_labels = {
      env    = "prod",
      region = "cn-north-1",
    }
  }
}

注意 loki.write 的 external_labels 是每个日志都会加的公共 label,加太多就是全局基数爆炸。建议只放 env、region 这种真低基数字段,其他用 loki.process 或 loki.relabel 动态注入。

2.4 docker-compose.yml

version: "3.9"

services:
  loki:
    image: grafana/loki:3.0.0
    container_name: loki
    command: -config.file=/etc/loki/loki-config.yaml
    volumes:
      - ./etc/loki/loki-config.yaml:/etc/loki/loki-config.yaml:ro
      - ./data/loki:/loki
    ports:
      - "3100:3100"
    networks: [loki-net]
    deploy:
      resources:
        limits:
          memory: 4G

  alloy:
    image: grafana/alloy:v1.5.0
    container_name: alloy
    volumes:
      - ./etc/alloy/config.alloy:/etc/alloy/config.alloy:ro
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/lib/docker/network/files:/var/lib/docker/network/files:ro
      - /run/systemd/journal:/run/systemd/journal:ro
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
      - /:/rootfs:ro
    command: -run=/etc/alloy/config.alloy
    depends_on: [loki]
    networks: [loki-net]

  grafana:
    image: grafana/grafana:11.1.0
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=change_me_please
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - ./etc/grafana/provisioning:/etc/grafana/provisioning:ro
      - ./data/grafana:/var/lib/grafana
    depends_on: [loki]
    networks: [loki-net]

networks:
  loki-net:
    driver: bridge

2.5 Grafana 数据源

etc/grafana/provisioning/datasources/datasource.yaml:

apiVersion: 1
datasources:
  - name: Loki
    type: loki
    access: proxy
    url: http://loki:3100
    jsonData:
      maxLines: 1000
    isDefault: true

2.6 启动与验证

cd ~/loki-stack
docker compose up -d
curl -s http://localhost:3100/ready  # 返回 "ready" 即正常

# 验证日志写入
curl -X POST http://localhost:3100/loki/api/v1/push \
  -H "Content-Type: application/json" \
  -d '{"streams":[{"stream":{"job":"manual-test","env":"prod"},
    "values":[[1700000000000000,"hello from manual push"]]}]}'
# 返回 204 即成功

Grafana 访问 http://localhost:3000,admin/change_me_please 登录,Explore → Loki 数据源,跑 {"job":"manual-test"} 应该看到那条日志。

到这里 Loki 单集群已经跑起来了。生产要用的话至少改三处:storage.filesystem 换成 S3/GCS/MinIO 并把 object_store 改成 s3;加独立的 compactor 服务别跟 ingester 抢 CPU;Grafana 密码改掉或加反代基本认证。

三、Log Collection 三件套:Alloy / Promtail / Vector 选型

日志采集是 Loki 生态里最容易被忽视、但踩坑最多的地方。三种 collector 对比:

特性 Promtail Alloy Vector
维护状态 维护模式 Grafana 主推 活跃(Timber)
配置格式 YAML River(DSL) TOML/YAML/JSON
语言 Go Go Rust
结构化解析 有限 丰富 丰富(VRL)
多后端转发 弱 强 强
学习曲线 低 中 中

Promtail:老系统里已经在用、跑得好好的,不用换。稳定、轻、社区例子多。缺点是没新特性了,复杂解析绕不过去。

Alloy:Grafana 生态新标准。如果团队已经在用 Grafana + Loki + Tempo,Alloy 最自然——一个 agent 推三种遥测到三个后端。缺点:River 语法要学习,配置文件比 Promtail 长。

Vector:如果后端不止 Loki(还有 Splunk、DataDog、GCP Logging),或者已经在用 Vector 做别的,选它。Rust 写的、性能最好、remap 语言能力强。

我在生产用的组合:K8s 集群里 Alloy 作为 DaemonSet,每个 node 一个 agent,采集 /var/log/containers/*.log(容器日志)、journald(节点系统日志)、/var/log/nginx/*.log。配置关键片段:

kubernetes.pod "pods" {}

kubernetes.pod_logs "syslogs" {
  forward_to = [loki.process.pipeline.receiver]
}

// 关键:只保留低基数 label,高基数字段挪进结构化日志
loki.process "pipeline" {
  stage.static {
    values = { env: "prod" }
  }
  stage.rename {
    rename = {
      kubernetes_namespace_name  = "namespace",
      kubernetes_pod_name        = "pod",
      kubernetes_container_name  = "container",
    }
  }
  forward_to = [loki.write.main.receiver]
}

stage.rename 和 stage.labels 是 label 设计关键——只保留你要过滤的字段作为 label,其他丢进结构化日志,用 LogQL 的 parse 或 unwrap 查。

四、Label 设计黄金三原则

Loki 的性能 80% 取决于 label 设计。搞错了直接给 index 制造灾难。

4.1 基数控制:不同 label 值组合 ≤ 10 万

基数 = 所有 label 组合的唯一值总数。Loki 的 index 存的是每个组合的元数据,基数越大 index 越大、内存占用越高、查询越慢。

举例:3 个 label app(10 种)、env(3 种)、pod(2000 种),总基数 = 10×3×2000 = 60000。换 pod 为 namespace(5 种):10×3×5 = 150。差 400 倍。

用 Grafana Explore 或 Loki API 看当前基数:

count by (__name__) (count_over_time({__name__!=""} [5m]))

或:

curl -s "http://loki:3100/loki/api/v1/labels/app/values" | jq .

生产建议:定期跑 sum by(app, env, namespace) (count_over_time({namespace=~".+"} [1m])),丢 Grafana dashboard,一旦超 10 万就告警。

4.2 只用低基数字段,别打 pod_name/container_name

低基数 label(推荐):app、env、region、namespace、severity(LogQL 从内容提取)、cluster、node(≤200 节点才用)。

高基数 label(禁用):pod_name、container_name、pod_ip、user_id、session_id、request_id、trace_id、span_id、file_path、url、uri_path、host。

替代方案:把高基数字段放进结构化日志(JSON 字段),用 LogQL 的 parse、unwrap 提取,只在查询时过滤,不建 index。

例如日志:

{"timestamp":"2026-10-27T10:00:00Z","level":"error","service":"payment","user_id":"U123456","trace_id":"abc-123","msg":"payment timeout"}

label 只留 {app="payment", env="prod"},user_id、trace_id 全在 JSON 里。要查某个用户报错:

{app="payment"} | json | user_id="U123456" | line_format "{{.msg}}"

4.3 别塞 30 个 label,label 是"过滤开关"

有新人曾经给我提 PR,想给每个日志加 15 个 label,理由是"这样查询灵活"。我让他先看 label 组合数量再决定——他的应用有 3000 个 pod、50 个 namespace、10 种日志级别,label 组合直接 150 万,Loki 直接 OOM。

label 的正确心态:它是给 Grafana 下拉框用的过滤器,不是给全文搜索用的。真正想"搜任意内容"应该走全文匹配(| ~"regex"),而不是给内容加 label。

K8s 环境实用 label 模板:

{
  app:       "payment-service",   # 从 helm chart name 取
  env:       "prod",              # 从 namespace label 取
  namespace: "payment",           # K8s namespace
  cluster:   "cn-north-1",        # 集群名
  severity:  "error",             # LogQL 从日志内容提取,不用采集时打
}

4 个 label,基数 = 应用数 × 环境数 × namespace 数 × 集群数。我这配置下 30×3×20×2 = 3600,非常健康。

4.4 常见反模式速查

反模式 后果 正确做法
每个 pod 一个 label index 爆炸 用 namespace + pod_name 放结构化日志
每个文件路径一个 label 同上 用文件名前缀做 label
每个 IP 一个 label 同上 用 subnet/region
把 timestamp 当 label 每个日志一个新组合 timestamp 是 Loki 内建字段
把 msg 内容做 label 同上 用 | parse 或 | regexp 提取
label 值包含时间戳 同上 label 必须是"稳定分类"

五、LogQL 速查表

LogQL 是 Loki 的查询语言,两大类:Stream Selector 和 Pipeline。

5.1 语法总览

{label1="value1", label2="value2"}     # Stream Selector:选 stream
  | log_format "<format>"               # 格式日志(非 JSON)
  | json                                # 解析 JSON 日志为结构化字段
  | unwrap <field>                      # 提取指定字段作为日志文本
  | parse "<pattern>"                   # 正则提取字段
  | regexp "^(?<code>\d{3})"            # 命名捕获组
  | line_format "<fmt>"                 # 重新格式化日志行
  | pattern <regex>                     # 匹配指定模式(Loki 特有优化)
  | rate()                              # 速率
  | count_over_time()                   # 计数
  | sort_by(<field>)                    # 排序
  | limit(<n>)                          # 限制返回

5.2 10 条速查命令

1. 按 label 拉日志

{app="payment", env="prod"}

2. 过滤 ERROR 日志

{app="payment"} |~ "ERROR"

|= 精确匹配(整行),|~ 正则匹配(部分)。

3. JSON 解析并提取字段

{app="payment"} | json | line_format "user={{.user_id}} trace={{.trace_id}} {{.msg}}"

4. 结构化日志里过滤 user_id

{app="payment"} | json | user_id="U123456"

5. 正则解析非 JSON 日志

{app="nginx"} | parse "GET /path - {{.status}} {{.bytes}}ms" | unwrap status

6. 按 level 分组计数

sum by (level) (count_over_time({app="payment"} | json [5m]))

7. 速率:每秒日志条数

sum(rate({app="payment"}[5m]))

8. 匹配模式忽略噪音

{app="payment"} | pattern `<string> payment succeeded <number>ms` | drop __error__

pattern 会匹配符合模式的所有日志并合并展示(Loki 特有优化)。

9. 排序 + 限制返回

{app="payment"} | json | line_format "{{.timestamp}} {{.msg}}" | sort_by ({{.timestamp}}) | limit (50)

10. 宽筛 + 精筛性能优化

{app="payment"} |~ "(?i)error" | json | level="error"

先用正则快速筛((?i) 忽略大小写),再 JSON 精确匹配。这种"宽筛 → 精筛"模式比单一过滤快 3-5 倍。

5.3 结构化日志解析的三个陷阱

陷阱 1:日志不是标准 JSON

logfmt 格式如 level="error" msg="xxx" 用 parse:

{app="myapp"} | parse `level={{$level}} msg="{{.msg}}"`

陷阱 2:嵌套字段

{"user":{"id":"U123"}} 用点路径:

{app="myapp"} | json user.id | unwrap user.id

陷阱 3:日志行太长被截断

Loki 默认 max_line_size 32KB,超过截断、JSON 解析失败。调大或 multiline 合并:

{app="java-app"} | multiline pattern `\tat ` max_wait=50ms

Java 堆栈的多行日志必须 multiline,不然一行 stack trace 会被拆成 N 条。

5.4 rate() 和 count_over_time() 的区别

  • count_over_time({app="x"}[5m]):过去 5 分钟有多少条日志,返回时间序列
  • sum(count_over_time(...)):所有 stream 加起来
  • sum by (label) (count_over_time(...)):按 label 分组
  • rate({app="x"}[5m]):每秒速率(count_over_time / 时间窗口)

告警规则写法:

groups:
- name: loki-alerts
  rules:
  - alert: ErrorRateHigh
    expr: |
      sum by (app) (count_over_time({env="prod"} | json | level="error" [5m])) > 100
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "{{ $labels.app }} 5 分钟内 error > 100 条"

5.5 unwrap 常见用法

# 提取 trace_id 用于关联 Tempo
{app="payment"} | json | unwrap trace_id

# 提取后重命名输出
{app="payment"} | json level, msg | line_format "L={{.level}} {{.msg}}"

# 提取嵌套字段
{app="payment"} | json user.id, user.email | line_format "{{.user.email}}:{{.user.id}}"

六、5 个真实事故复盘

每个都按症状 → 根因 → 处置 → 长期修复四段展开。

6.1 日志量暴增导致磁盘爆

症状:早 9 点收到"磁盘使用率 > 90%"告警,Loki 节点磁盘 2TB 已用 95%,compactor 卡死。

根因:某个新上线服务没做日志级别控制,把 INFO 日志里所有请求详情都打了出来,日志量从 5 MB/s 飙到 60 MB/s,一晚上写爆 2 TB。

处置:Alloy 加限流 rate_limit_mb_per_sec = 10;通知开发把日志级别从 INFO 改回 DEBUG(生产);临时把 retention 从 90d 改到 30d;扩容磁盘到 4 TB、告警阈值改到 70%。

长期修复:Loki 配 ingestion_rate_mb、ingestion_burst_size_mb 限制单 stream 写入速率;Alloy 侧对每个日志源配独立 rate limit;Grafana 加"当前写入速率"面板,超 20 MB/s 告警;开发规范:生产默认 INFO,DEBUG 必须通过动态开关开。

6.2 label 基数爆炸导致查询超时

症状:Grafana 上所有 Loki 查询超时(> 30s),loki_ingester_memory_series_count 从 5 万飙到 80 万,ingester OOM 反复重启。

根因:某业务方给 Alloy 配置加了 stage.static,把 user_agent、request_id 都塞进 label。这两个字段几乎每行都不同,1 小时内 label 组合从 5 万涨到 80 万。

处置:查 count by (__name__) (count_over_time({__name__!=""}[1m])) 定位突增 label;通知业务方回滚 Alloy 配置;重启 ingester 释放内存;用 loki-bucket-tool 清理历史 label。

长期修复:部署"基数监控" sum(count_over_time({__name__!=""}[5m])) 超 10 万告警;max_label_names_per_series 从 30 降到 15;Alloy 配置 label 走 SRE 审批;配置 stage.labels.drop_namespaces 白名单。

复盘:Loki 的 index 是乘积式的。A × B × C × D × E,任何一维爆炸都会拖累全局。基数监控是唯一能提前发现的手段。

6.3 存储保留不生效导致磁盘撑爆

症状:磁盘使用率一周涨 30%,Loki 配置里明明写了 retention_period: 90d。查了 30 天前的日志还能查到。

根因:只配了 retention_period,没开 compactor.retention_enabled: true。Loki 3.x 之前 retention 是"可选功能"默认关闭,不启用时 retention_period 只是"文档性注释"。

处置:补上 compactor.retention_enabled: true;重启 Loki 触发 compactor;观察 loki_boltdb_shipper_active_index_files 和 loki_compactor_deletion_request_created_total 指标;24 小时后老数据开始被删。

长期修复:配置文件加"生产检查清单",retention_enabled 必须为 true;compactor 健康检查;定期用 loki-bucket-tool verify 检查对象存储里老 block 是否真的删除。

教训:Loki 很多功能"配了但默认关",别以为写了就生效。每次改配置必须看日志和指标验证。

6.4 跨机房 federation 数据同步延迟

症状:北京机房 Grafana 查上海机房日志,延迟 10-30 分钟,有时甚至查到 1 小时前的日志。开发反馈"我们的日志看不到"。

根因:异地容灾用了 Loki federation(上海 Loki 作为北京的 upstream),但 federation 是拉取模式:北京 querier 定期拉上海数据,默认 30s 一次。加上跨机房网络延迟、上海 Loki 查询慢,累积延迟能到 10 分钟以上。而且 federation 有 bug:网络抖动时数据可能跳过一段。

处置:短期拉取间隔从 30s 改到 10s;中期改用 Loki multi-region(每个机房独立 Loki,Grafana 数据源聚合展示,查询时并行请求两个 Loki);长期评估 Thanos 模式的 Loki 联邦方案。

# loki-config.yaml
querier:
  max_outstanding_requests: 64

federation:
  upstreams:
    - name: shanghai
      address: http://loki-shanghai:3100
      poll_interval: 10s
      lookback_window: 30m

教训:federation 适合"备份查询",不适合"实时聚合查询"。真要多机房高可用,用微服务的 multi-region 部署,每个机房独立写入,查询时跨机房 fanout。

6.5 多租户误配导致日志泄漏

症状:某次安全审计发现,A 团队 Grafana 面板里能看到 B 团队日志,包括用户 PII 数据。

根因:Loki 开启了 auth_enabled: true,但没配多租户,所有租户共用一个 org_id。开发用同一个 Grafana 登录不同数据源时,Loki 无法区分归属,任何 org 都能看到所有数据。

处置:立刻关闭该数据源访问权限;排查泄漏范围(哪些日志、多少时间窗口);联系受影响团队删除 Grafana 缓存里的截图、导出数据。

长期修复:开启 auth_enabled: true + multiorg_config,每个租户独立 org_id;Alloy 推送强制带 X-Scope-OrgID header;Grafana 数据源 URL 里加 ?org_id=team-a;加 limits_config.per_stream_rate_limit 按租户限速;定期审计 loki_request_duration_seconds 按 org_id 分布。

auth_enabled: true
multiorg_config:
  allow_by_default: false
  tenants:
    team-a:
      retention: 30d
      ingestion_rate_mb: 5
    team-b:
      retention: 60d
      ingestion_rate_mb: 10

教训:多租户不是"配一下 auth"就完事。Grafana 数据源、Alloy header、Loki auth 三层都要配,任何一层错都会导致越权访问。

七、生产 checklist:8 个必须过的检查点

每次新集群上线都要过这张清单,抄一份贴到工单里:

1. 基础部署:写入超 20 MB/s 或 50 万 stream 切微服务;至少开 S3/MinIO 做主存储;compactor 独立进程;Grafana 密码改默认。

2. 数据与安全:retention_period 显式配 + retention_enabled: true;多租户每团队独立 org_id;Loki → 客户端 TLS;PII 字段在 Alloy 用 stage.regex 脱敏成 ***。

3. Label 设计:基数 ≤ 10 万;禁用 pod_name/container_name/user_id 作为 label;高基数字段放结构化日志;Alloy 配 label 白名单。

4. 查询优化:max_query_series: 500、max_query_bytes: 200MB;query-frontend 独立部署;开 query_range_results_cache 结果缓存;Grafana dashboard 强制时间窗口选择。

5. 监控:写入速率 rate(loki_distributor_lines_received_total[1m]);基数 sum(count_over_time({__name__!=""}[5m]));磁盘 node_filesystem_avail_bytes / node_filesystem_size_bytes;查询延迟 P99 histogram_quantile(0.99, rate(loki_request_duration_seconds_bucket[5m]));compactor loki_compactor_successful_compactions_total。

6. 灾备:微服务 replication_factor: 3;boltdb-shipper 定期导出到另一 region 对象存储;RTO/RPO 明确(我们目标 RTO 30 分钟、RPO 5 分钟);每季度做恢复演练。

7. 成本:每月成本报告跟 ELK 对比;超预算 20% 告警;90 天以上走 tiered storage(Loki 3.x 支持冷数据压缩)。

8. 变更:Alloy/Loki 配置走 GitOps(argocd/kustomize);上线必须走 checklist;变更前用 loki-benchmark 或 grafana/loki-bench 压测。

八、速查 Cheat Sheet

8.1 Loki HTTP API

# 健康检查
curl http://loki:3100/ready

# 列出所有 label 名
curl http://loki:3100/loki/api/v1/labels

# 列出某 label 的所有值
curl "http://loki:3100/loki/api/v1/labels/app/values"

# 手动推送日志
curl -X POST http://loki:3100/loki/api/v1/push \
  -H "Content-Type: application/json" \
  -d '{"streams":[{"stream":{"job":"test"},"values":[[1700000000000000,"hello"]]}]}'

# 查询日志
curl "http://loki:3100/loki/api/v1/query_range?query={app=\"payment\"}&start=1700000000000000&end=1700003600000000&limit=100"

8.2 LogQL 快速对照

需求 LogQL
拉日志 {app="x"}
过滤错误 {app="x"} \|~ "ERROR"
精确匹配整行 {app="x"} \|="ERROR"
JSON 解析 {app="x"} \| json
提取字段 {app="x"} \| json \| unwrap field
字段过滤 {app="x"} \| json \| level="error"
正则解析 {app="x"} \| parse "code={{$code}}"
5 分钟计数 count_over_time({app="x"} [5m])
5 分钟速率 sum(rate({app="x"}[5m]))
分组统计 sum by (level) (count_over_time({app="x"} \| json [5m]))
排序 ... \| sort_by ({{.timestamp}})
限制条数 ... \| limit (100)
忽略大小写 {app="x"} \|~ "(?i)error"
排除关键字 {app="x"} \| !~ "DEBUG"
多行合并 {app="java"} \| multiline pattern="\tat "

8.3 常用运维命令

# 查看 ingester 状态
curl -s http://loki:3100/loki/api/v1/ring | jq '.ingesters'

# 查看 compactor 状态
curl -s http://loki:3100/loki/api/v1/compactor | jq .

# 列出对象存储里的 blocks
loki-bucket-tool index list

# 删除某个 block(危险!)
loki-bucket-tool block delete <ulid>

# 手动触发 compaction
loki-block-builder build --since=1h --until=now --output-dir=/tmp

# 验证 index 完整性
loki-bucket-tool index verify --store=s3

8.4 常见问题排查

问题 排查方向
查询超时 看 loki_query_frontend_timeout、检查 limits_config、缩小时间窗口
写入 500 看 max_line_size、检查 JSON 是否合法、看 ingest rate limit
日志延迟高 看 Alloy 到 Loki 网络、看 ingester shard、查 rate limit
数据丢失 看 compactor 健康、检查 replication_factor、查 boltdb shipper
Grafana 无数据 查数据源 URL、X-Scope-OrgID、时间窗口、label 是否正确
磁盘满 检查 retention_enabled、看 compactor 是否运行、看 boltdb index 大小
ingester OOM 查 loki_ingester_memory_series_count、label 基数、重启并扩容
权限 403 检查 Loki auth_enabled、X-Scope-OrgID header、租户配置

8.5 Loki 支持告警和 Tempo 联动吗?

告警:通过 Loki rules(Loki 3.x 内置 rule evaluation)+ Alertmanager。单二进制配 rule_path: /etc/loki/rules.yaml,微服务模式用 ruler 组件。核心就一条 LogQL 表达式(见 5.4 示例)。

Tempo 联动:日志里必须有 trace_id 字段(结构化日志里),Grafana 配置 log_to_trace = Loki,Tempo 后,点日志里的 trace_id 直接跳到 Tempo 查完整链路。这是 Loki + Tempo 组合最大的生产力增益。

8.6 Loki 能替代 Kibana 吗?

覆盖 80% 场景。Grafana Explore 支持全文查询、时间筛选、日志关联指标/trace。Kibana 独占的是 Discover 多文档对比、Lens 可视化、Canvas 报告——这些 Grafana 做不了。重度依赖 Kibana 的团队可以双存储:Loki 存热数据,S3+ES 存冷分析。

8.7 单二进制适合生产吗?

小规模(< 20 MB/s、< 50 万 stream)适合。判断阈值:磁盘 await > 20ms、内存超机器 60%、compactor 时 CPU > 80%——任一持续超过就该切微服务。

收尾

Loki 是个"看着简单、坑不少"的系统。它简单在部署——10 分钟 Docker Compose 就能跑;坑在 label 设计、retention 配置、多租户权限这些"细节"上。我踩过的这些坑,希望帮你在 PoC 阶段就绕开。

核心心法一句话:Loki 的性能 = label 设计的质量。你的 label 有多干净,你的 Loki 就有多稳定。别在 index 里堆高基数字段,Loki 会用磁盘爆和查询超时报复你。