本文是 Grafana Stack 的 vendor deep article,深化 overview「Loki 設計與限制」段。初次接觸 Grafana Stack 的讀者建議先讀 Grafana Stack 服務頁

問題情境

團隊從 ELK stack 或 CloudWatch Logs 遷到 Grafana Stack 時,Loki 是 log backend 的預設選擇。遷移後最常遇到的衝擊是查詢模式的根本差異:Elasticsearch 做 full-text index(寫入時索引每個欄位、查詢時任意搜尋),Loki 只 index labels(寫入時只索引 stream labels、查詢時先篩 stream 再 grep content)。

這個差異是刻意的設計選擇 — Loki 的目標是「Prometheus for logs」:用跟 Prometheus metrics 相同的 label 體系管理 logs,讓 log 查詢跟 metric 查詢使用同一組 label selector。代價是失去 full-text search 的即時性。理解這個設計哲學才能正確設計 label、寫出有效率的 LogQL、避免常見的效能陷阱。

核心概念

Like Prometheus, but for logs

Prometheus 用 label set 識別 time series — {job="checkout", instance="10.0.1.5"} 是一條 series。Loki 用相同概念識別 log stream — {job="checkout", namespace="production"} 是一條 stream。同一條 stream 的所有 log entries 存在同一組 chunks。

Elasticsearch 的索引模式是「寫入時建 inverted index、查詢時走索引」。Loki 的索引模式是「寫入時只記錄 stream label → chunk 的 mapping、查詢時先用 label 選 stream、再在 chunk 內做 grep」。

這代表:

  • 有 label filter 的查詢很快 — Loki 只掃對應 stream 的 chunks
  • 沒有 label filter 的查詢很慢 — Loki 要掃所有 stream 的 chunks(相當於 full scan)
  • Label cardinality 跟 Prometheus 一樣敏感 — 高 cardinality label 產生大量 stream、每個 stream 的 chunk 很小、index 膨脹

Stream 與 chunk

一條 stream = 一組唯一的 label set。每條 stream 的 log entries 依時間排序存在 chunks 裡。Chunk 是 Loki 的最小儲存單位。

1Stream: {job="checkout", namespace="production"}
2  └─ Chunk 1: [2026-06-22T00:00 ~ 2026-06-22T01:00] (compressed)
3  └─ Chunk 2: [2026-06-22T01:00 ~ 2026-06-22T02:00] (compressed)
4  └─ ...

Chunk 存在 object storage(S3 / GCS / MinIO),index 存在 key-value store(BoltDB / TSDB,3.0 起預設 TSDB)。Object storage 便宜(相比 Elasticsearch 的 SSD),這是 Loki 成本優勢的來源。

跟 Elasticsearch 的根本差異

面向LokiElasticsearch
索引對象只索引 labels(stream metadata)索引所有欄位(full-text + structured)
查詢模式Label selector → stream → grep contentQuery DSL / KQL → inverted index lookup
寫入成本低(不建 content index)高(建 inverted index + doc values)
查詢成本取決於 stream 篩選效率(label 越精準越快)取決於 index 覆蓋度(indexed field 查詢快)
儲存成本低(object storage)高(SSD / local disk)
Full-text search不支援(只有 line filter grep)原生支援
適用場景已有 Prometheus/Grafana 生態的 log aggregation需要 full-text search 的 log analytics / SIEM

判讀:如果團隊的 log 查詢模式是「先選 service/namespace/pod、再看時間範圍內的 log entries」,Loki 足夠。如果查詢模式是「在所有 log 裡搜某個 error message 或 request ID」,Elasticsearch 的 full-text index 更適合。

配置 step-by-step

Label 設計原則

Label 設計是 Loki 最重要的操作決策。原則跟 Prometheus 相同:低 cardinality、穩定、有查詢意義。

LabelCardinality適合當 label理由
job低(服務數量)適合篩選到特定服務
namespace適合篩選到特定環境
pod_name中(pod 數量)視情境K8s 環境常用但 pod 頻繁重建會產生大量短命 stream
level(info/warn/error)低(3-5 值)適合快速篩選 error log
request_id極高(per-request)不適合每個 request 一條 stream、chunk 極小、index 爆炸
user_id不適合同上
trace_id極高不適合用 Tempo 查 trace、不用 Loki label

request_id / user_id / trace_id 不應該是 label,它們應該在 log content 裡用 structured JSON 欄位表達,查詢時用 LogQL 的 line filter 或 parser 提取。

LogQL 常見查詢模式

Stream selector + line filter(最基本):

{job="checkout", namespace="production"} |= "error" |= "timeout"

先選 stream、再 grep 包含 “error” 和 “timeout” 的 log lines。|= 是包含、!= 是不包含、|~ 是 regex。

Structured metadata parser(JSON log):

{job="checkout"} | json | status_code >= 500 | line_format "{{.method}} {{.path}} {{.status_code}}"

| json 解析 JSON log entry 的欄位,後續可以用欄位做 filter 和格式化。

Metric 聚合(log → metric):

sum by (status_code) (rate({job="checkout"} | json | __error__="" [5m]))

計算每 5 分鐘每個 status_code 的 log entry 速率。這是 Loki 的「metric from logs」能力 — 不需要額外的 metrics pipeline,直接從 log 產生 time series。

Loki config 核心段

 1# loki-config.yaml
 2schema_config:
 3  configs:
 4    - from: 2024-01-01
 5      store: tsdb
 6      object_store: s3
 7      schema: v13
 8      index:
 9        prefix: loki_index_
10        period: 24h
11
12storage_config:
13  tsdb_shipper:
14    active_index_directory: /loki/index
15    cache_location: /loki/cache
16  aws:
17    s3: s3://loki-chunks-bucket
18    region: us-east-1
19
20limits_config:
21  ingestion_rate_mb: 10
22  ingestion_burst_size_mb: 20
23  max_streams_per_user: 10000
24  max_label_name_length: 1024
25  max_label_value_length: 2048

limits_config 是防護網。max_streams_per_user 限制每個 tenant 的 stream 數量,超過時新 stream 的 log 被拒(HTTP 429)。這是 label cardinality 爆炸的最後防線。

故障與邊界

Label cardinality 爆炸

觸發條件:label 包含高 cardinality 值(pod UID、request ID、container ID)。每個唯一 label set 產生一條 stream,stream 數量快速增長。

表現loki_ingester_memory_streams 持續上升、ingester memory 增長、最終觸發 max_streams_per_user 限制(429 error)。跟 Prometheus series explosion 是同一個問題的 log 版本。

修法:檢查產出大量 stream 的 label。Loki 的 /loki/api/v1/labels/loki/api/v1/label/{name}/values API 可以列出所有 label 值。找到高 cardinality label 後,從 promtail / alloy 的 pipeline 中移除該 label、改放進 log content 的 structured field。

Stream rate limit

觸發條件:單一 stream 的 ingestion rate 超過 per_stream_rate_limit(預設 3 MB/s)。通常是某個 service 大量噴 debug log。

表現:Loki 回傳 429 + rate limit exceeded error。部分 log entries 被丟棄。

修法:先解決 log 噴量問題(降低 debug log level 或加 sampling)。如果噴量合理(高 QPS 服務),調高 per_stream_rate_limit 或拆分 stream(加一層 label 分散流量)。

大時間範圍查詢 timeout

觸發條件:LogQL 查詢沒有精確的 label filter、時間範圍 > 24 小時。Loki 要掃描大量 chunks、query timeout(預設 3 分鐘)觸發。

表現:Grafana 顯示 query timeout error。

修法:查詢時先用 label selector 縮小 stream 範圍({job="checkout", namespace="production"} 而非 {namespace="production"}),再用 line filter 進一步篩。如果業務需要長時間範圍的 log analytics,考慮用 LogQL 的 metric aggregation(rate(...) / count_over_time(...))替代原始 log 掃描。

Chunk target size 與 ingestion rate 的關係

chunk_target_size(預設 1.5 MB)控制 chunk 的大小。ingestion rate 低的 stream 可能幾個小時才填滿一個 chunk — 這段期間 chunk 停在 ingester memory 裡。大量低 ingestion rate 的 stream(= 高 cardinality label)會讓 ingester 同時持有大量未 flush 的 chunks,佔用記憶體。

修法方向:降低 chunk_idle_period(預設 30 分鐘,時間到即使 chunk 未滿也 flush),或減少低 cardinality stream 的數量。

容量與成本

Loki 的成本結構跟 Elasticsearch 根本不同:

成本項LokiElasticsearch
儲存Object storage(S3/GCS)— 便宜SSD / local disk — 貴
Index小(只索引 labels)大(inverted index + doc values)
查詢 compute每次查詢 grep chunks — CPU 密集走 index — 相對輕
適合的 workload高 volume、低 query frequency高 query frequency、需要 full-text

Loki 在「每天寫 TB 級 log、偶爾查一下」的場景成本遠低於 Elasticsearch。但在「每天查數百次、需要快速 full-text search」的場景,Elasticsearch 的 pre-indexed 查詢效能更好,Loki 每次 grep 的 compute cost 反而更高。

成本治理的判讀:監控 loki_ingester_bytes_received_total(ingestion volume)和 loki_querier_query_duration_seconds(query cost)。如果 query duration 持續上升,先檢查是 label filter 不夠精確還是 query 時間範圍太大。

整合與下一步