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 三种部署模式
- Single binary(单二进制):所有组件一个进程,本地磁盘存数据。适合 PoC、小规模(< 100 万 stream)、"日志不是核心资产"。
- Microservice(微服务):distributor / ingester / querier / query-frontend / query-scheduler / compactor / index-gateway 独立部署。适合中大规模、多机房、要 HA。
- 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 看当前基数:
或:
生产建议:定期跑 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 里。要查某个用户报错:
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 拉日志
2. 过滤 ERROR 日志
|= 精确匹配(整行),|~ 正则匹配(部分)。
3. JSON 解析并提取字段
4. 结构化日志里过滤 user_id
5. 正则解析非 JSON 日志
6. 按 level 分组计数
7. 速率:每秒日志条数
8. 匹配模式忽略噪音
pattern 会匹配符合模式的所有日志并合并展示(Loki 特有优化)。
9. 排序 + 限制返回
{app="payment"} | json | line_format "{{.timestamp}} {{.msg}}" | sort_by ({{.timestamp}}) | limit (50)
10. 宽筛 + 精筛性能优化
先用正则快速筛((?i) 忽略大小写),再 JSON 精确匹配。这种"宽筛 → 精筛"模式比单一过滤快 3-5 倍。
5.3 结构化日志解析的三个陷阱
陷阱 1:日志不是标准 JSON
logfmt 格式如 level="error" msg="xxx" 用 parse:
陷阱 2:嵌套字段
{"user":{"id":"U123"}} 用点路径:
陷阱 3:日志行太长被截断
Loki 默认 max_line_size 32KB,超过截断、JSON 解析失败。调大或 multiline 合并:
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 会用磁盘爆和查询超时报复你。