Grafana Loki 工具手册
Loki 省掉的不是索引治理
Grafana Loki 不会给日志正文中的每个字段建立一套通用索引。它先用租户和 labels 确定 stream,索引保存 stream 到 chunk 的关联,日志正文压缩后进入 chunk。LogQL 查询通常先用 label selector 缩小 stream,再读取候选 chunk 做行过滤和解析。
这种模型适合按服务、环境、集群等稳定维度圈定范围的日志,也适合已经使用 Grafana 和 Prometheus 的团队。它不意味着“标签越多越好”。trace_id、订单号、用户 ID、完整 URL 或容器 UID 一旦成为 label,每个新值都可能产生新 stream,活跃 stream、内存、索引项和小 chunk 会一起增长。请求级身份应保留在正文或 structured metadata 中。
本文使用 Loki 3.7.2、Grafana Alloy 1.18.0 和 Grafana 12.4.0 作为固定实验基线。版本来自各自正式发行线,部署前仍应核对 Loki release notes、Alloy release notes 和 Grafana 升级说明,而不是把示例改成 latest。
跑通 Alloy 到 Loki 的文件采集
先建立 logs 与 grafana-provisioning/datasources 目录。下面的单进程 Loki 和本地文件系统只用于观察对象,不具备主机故障容忍;三个端口都绑定回环地址。
services:
loki:
image: grafana/loki:3.7.2
command: ["-config.file=/etc/loki/loki.yaml"]
volumes:
- ./loki.yaml:/etc/loki/loki.yaml:ro
- loki-data:/loki
ports:
- "127.0.0.1:3100:3100"
alloy:
image: grafana/alloy:v1.18.0
command: ["run", "--server.http.listen-addr=0.0.0.0:12345", "--storage.path=/var/lib/alloy", "/etc/alloy/config.alloy"]
volumes:
- ./config.alloy:/etc/alloy/config.alloy:ro
- ./logs:/logs:ro
- alloy-data:/var/lib/alloy
ports:
- "127.0.0.1:12345:12345"
depends_on:
- loki
grafana:
image: grafana/grafana:12.4.0
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: dev-only-change-me
volumes:
- ./grafana-provisioning:/etc/grafana/provisioning:ro
ports:
- "127.0.0.1:3000:3000"
depends_on:
- loki
volumes:
loki-data:
alloy-data:loki.yaml 使用 TSDB schema、文件系统 chunk store 和 Compactor 保留。from 必须替换为过去的 UTC 日期,让新安装立即匹配;现有集群增加 schema 时,新条目则要安排在未来 UTC 日界线。
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /loki
replication_factor: 1
ring:
kvstore:
store: inmemory
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
schema_config:
configs:
- from: <EVENT_DATE>
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
compactor:
working_directory: /loki/compactor
retention_enabled: true
delete_request_store: filesystem
limits_config:
retention_period: 168h
allow_structured_metadata: true已经按新 schema 写入的数据不能靠删除配置回滚。修正方式是保留旧配置,再追加未来生效的条目。跨 schema 边界的查询必须在影子租户验证,对象存储也要先有可恢复副本。
Alloy 负责文件发现、JSON 解析和字段归位。service、environment 与低基数 level 进入 labels,trace_id 进入 structured metadata:
local.file_match "checkout" {
path_targets = [{
"__path__" = "/logs/*.jsonl",
"service" = "checkout",
"environment" = "dev",
}]
}
loki.source.file "checkout" {
targets = local.file_match.checkout.targets
forward_to = [loki.process.checkout.receiver]
}
loki.process "checkout" {
stage.json {
expressions = {
level = "level",
trace_id = "trace_id",
timestamp = "@timestamp",
}
}
stage.labels {
values = { level = "" }
}
stage.structured_metadata {
values = { trace_id = "" }
}
stage.timestamp {
source = "timestamp"
format = "RFC3339"
}
forward_to = [loki.write.local.receiver]
}
loki.write "local" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}Grafana 数据源运行在容器内,URL 必须使用服务名 loki。写成 localhost:3100 会访问 Grafana 容器自己。
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: truemkdir -p logs grafana-provisioning/datasources
: > logs/app.jsonl
docker compose up -d
curl -fsS http://localhost:3100/ready
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
printf '%s\n' "{\"@timestamp\":\"$NOW\",\"level\":\"ERROR\",\"trace_id\":\"TE-LK-001\",\"message\":\"payment timeout\"}" >> logs/app.jsonl
sleep 3
curl -fsS --get 'http://localhost:3100/loki/api/v1/query_range' \
--data-urlencode 'query={service="checkout",environment="dev"} |= "TE-LK-001"' \
--data-urlencode 'limit=20'/ready 返回 ready 只证明 Loki 当前可接请求;查询响应为 success 且结果包含固定身份,才能证明这条事件穿过 Alloy、写入与查询链。Alloy 的调试 UI 还要检查组件健康、文件 target 和发送错误。
高基数要用反例证明
在空实验环境直接写入一百个不同 trace_id label,可以看到 stream 数怎样被请求身份放大:
for i in $(seq 1 100); do
ns=$(date +%s%N)
curl -fsS -X POST http://localhost:3100/loki/api/v1/push \
-H 'Content-Type: application/json' \
-d "{\"streams\":[{\"stream\":{\"service\":\"checkout-bad\",\"trace_id\":\"T-$i\"},\"values\":[[\"$ns\",\"bad cardinality experiment\"]]}]}" >/dev/null
done
curl -fsS --get 'http://localhost:3100/loki/api/v1/series' \
--data-urlencode 'match[]={service="checkout-bad"}'相同的一百行如果只使用 service=checkout-good,就能进入同一个 stream 并形成更饱满的 chunk。修正采集配置只会停止制造新高基数 stream,旧 stream 不会自动合并;它们要等保留清理,或经过明确的数据删除流程。
从 400、429 和慢查询反推内部对象
写入先由 Distributor 校验标签、租户、速率和行大小,再把 stream 路由给 Ingester。missing_labels 属于不可重试的 400;过旧或乱序事件要核对应用时钟、采集延迟和允许窗口。429 则可能来自租户写入速率、活跃 stream、单行大小或并发限制。把所有错误都无限重试,只会把后端限流转成 Alloy 队列和节点磁盘故障。
查询慢时先检查 selector。{service="checkout",environment="prod"} |= "timeout" 会先缩小 stream;{environment=~".+"} | json | trace_id="..." 可能扫描大量 chunk。Query Frontend 会拆分时间范围,Querier 根据 TSDB index 读取对象存储,所以过宽窗口、小 chunk、高基数、缓存失效和对象存储尾延迟会一起影响查询。
生产部署常从 simple scalable 起步,再按读、写、backend 路径扩容。规模足够大时才拆 Distributor、Ingester、Query Frontend、Querier、Index Gateway 和 Compactor。组件拆开并不会自动获得可用性;ring、复制因子、zone-aware placement、WAL、对象存储权限、网关限流、查询公平性和缓存都要同时设计。
多租户头不是认证
Loki 本身不负责用户认证。auth_enabled: true 启用的是多租户语义,X-Scope-OrgID 只是租户选择,不是身份证明。生产必须由可信网关认证用户或工作负载、完成授权后再注入租户头,Loki 与内部组件端口不能直接暴露公网。
对象存储凭证优先使用 workload identity。Compactor 若承担删除,需要删除对象权限;查询组件不该顺便获得删除权。保留也不是写了 retention_period 就结束,应观察 Compactor 日志、保留指标和对象数量趋势。文件系统模式不会根据剩余空间自动删到安全水位。
回滚 Alloy 时保留 alloy-data,否则文件读取位置可能丢失并造成重采。旧采集器和新采集器不能长时间同时读取同一文件。实验清理可以停止 Alloy 后删除容器卷:
docker compose stop alloy
docker compose down -v生产 schema 迁移不能用删卷回滚。正确动作是切回旧租户或旧写入路径,保留已经写入的对象和旧 schema 配置,让历史日志仍可查询。Loki 的容量账也不只是一项对象存储费用:活跃 stream、WAL、索引请求、小对象、缓存、查询扫描和跨区出口都需要由平台 owner 持续解释。
