深色模式
日志采集实战:EFK 与 Loki 在 K8s 中的落地
摘要:本文面向生产 SRE / 平台工程师,拆解 Kubernetes 日志的两种主流采集链路:EFK(Fluent Bit → Elasticsearch → Kibana)与 Loki(Promtail/Alloy → Loki → Grafana)。重点讲清"节点文件 → 采集 agent → 中心存储"的链路、label 设计对成本的决定性影响、LogQL 与指标关联,以及高基数导致的成本爆炸排障。适用 Kubernetes v1.28+。
适用版本与前提
- Kubernetes:v1.28+
- 组件(二选一或并存):
Fluent Bit/Promtail/Grafana Alloy;Elasticsearch或Loki;Kibana或Grafana - 前提:节点有 root/特权访问以读
/var/log/pods;应用输出结构化 JSON 到 stdout/stderr
背景与问题
容器运行时按 CRI 日志格式把每个容器的 stdout/stderr 写入节点文件,默认路径 /var/log/pods/<ns>_<pod>_<uid>/<container>/<n>.log(符号链接到 /var/log/containers/*.log)。kubectl logs 只是 kubelet 读这些文件的封装,日志不随 Pod 删除而自动留存,且跨节点排查极慢。生产必须有一条"节点级 agent tail 文件 → 中心存储 → 统一查询"的链路。
核心两难:索引什么。
- ES 对日志全文索引 → 查询强,但存储与写入成本高,大规模昂贵。
- Loki 只索引label(不索引内容),与 Prometheus 同思路 → 便宜,但内容过滤要靠 LogQL 在查询时扫描,需 label 命中后范围才小。
一句话选型
Loki ≠ Elasticsearch 替代品,也不是 SIEM/合规归档。Loki 适合"事故时要立刻看的运维日志";合规、审计、安全检索请用 Elasticsearch/SIEM 另存。把两者都当 Loki 用,会得到成本与性能双输。
架构与原理
两种链路的本质差异在标签/索引设计:
- Fluent Bit → ES:Fluent Bit 解析日志、加
kubernetes过滤(提取 ns/pod/container),ES 建索引。查询用 KQL/Lucene,全文检索强。 - Promtail/Alloy → Loki:只给少量静态 label(如
namespace、pod、container、service),内容进日志行。查询用 LogQL:{namespace="demo"} |= "error" | json | status_code >= 500。
生产实践一:Loki 链路(推荐起点)
部署 Promtail(DaemonSet 形态,核心片段)
yaml
# promtail DaemonSet(节选自官方 helm values 的等价 manifest)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: promtail
namespace: logging
spec:
selector:
matchLabels:
app: promtail
template:
metadata:
labels:
app: promtail
spec:
serviceAccountName: promtail
containers:
- name: promtail
image: grafana/promtail:3.1.0
args:
- -config.file=/etc/promtail/promtail.yaml
volumeMounts:
- name: pods
mountPath: /var/log/pods
- name: config
mountPath: /etc/promtail
volumes:
- name: pods
hostPath:
path: /var/log/pods
- name: config
configMap:
name: promtail-config1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
promtail.yaml 关键配置(scrape 容器日志 + 加 K8s label):
yaml
server:
http_listen_port: 3101
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
pipeline_stages:
- docker: {} # 解析 Docker/CRI 日志格式
- kubernetes: # 注入 namespace/pod/container 等 label
drop_labels: false
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: app # 把业务 label 提升为 Loki label(谨慎,勿用高基数值)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
label 基数是 Loki 的生命线
Loki 按 label 组织"流"(stream)。每个不同的 label 组合 = 一条独立流 = 一个 chunk 文件。若把 user_id、request_id、trace_id 这类高基数动态值设为 label,会出现"流爆炸",写入与查询都会崩、存储费用飙升。规则:label 只用静态维度(namespace、pod、container、service、cluster、level),动态值一律留在日志行,用 LogQL 内容过滤。
logql
# 错误的 label 设计会导致流爆炸,正确做法:动态值在行内
{service="checkout"} |= "timeout" | json | latency > 10001
2
2
生产实践二:EFK 链路(全文检索场景)
当业务强依赖全文检索、聚合分析(如安全/SIEM 衍生)时用 EFK。Fluent Bit 作为轻量 DaemonSet 采集,输出到 Elasticsearch。
yaml
# fluent-bit 输出到 ES(核心片段)
output:
- name: es
match: '*'
host: ${ES_HOST}
port: 9200
index: k8s-${NAMESPACE}-${YEAR}.${MONTH}.${DAY}
http_user: ${ES_USER}
http_passwd: ${ES_PASS}
tls: on
tls.verify: off1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
EFK 取舍
ES 全文索引带来强查询,但存储成本随日志量线性上升且显著高于 Loki。生产应设 ILM(索引生命周期管理)做热/温/冷分层与滚动删除,否则磁盘会涨满。[版本相关] ES 8.x 默认安全开启,Fluent Bit 需配 http_user/tls,与旧版明文配置不兼容。
统一采集:用 Grafana Alloy 替代 Promtail(OTel 原生)
Alloy 是 Grafana 对 OpenTelemetry Collector 的发行版,能用同一份配置处理 metrics/logs/traces,并通过 loki.source.file + discovery.kubernetes 采集容器日志,且天然支持把 service、namespace 与指标 label 对齐,实现指标↔日志一键跳转。
yaml
# alloy 配置核心片段(logs 管道)
loki.source.file "pods" {
targets = discovery.file "..."
forward_to = [loki.write.loki.receiver]
}
discovery.kubernetes "pods" {
role = "pod"
}
loki.write "loki" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
验证
bash
# 1. 节点日志文件存在
kubectl exec -n logging ds/promtail -- ls /var/log/pods | head
# 2. Loki 收到流(LogCLI)
kubectl exec -n logging deploy/loki -- \
logcli query '{namespace="demo"}' --limit=5
# 3. 在 Grafana Explore 用 LogQL 验证关联
# {service="checkout"} | json | status_code >= 5001
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
回滚与清理
生产危险
Loki/ES 删除索引不可逆。清理演示资源:
bash
kubectl delete ns logging # 仅当为独立演示命名空间
# ES 删除索引(不可逆):
# curl -X DELETE "https://es:9200/k8s-demo-*"1
2
3
2
3
故障排查
| 现象 | 根因 | 处理 |
|---|---|---|
| 某些 Pod 日志缺失 | 运行时日志路径不同 / hostPath 未挂载 | 核对容器运行时(containerd/docker)日志目录;kubectl get pods -n logging -o wide 确认 DaemonSet 已上每个节点 |
| Loki 写入 429/慢 | 流爆炸(高基数 label) | 检查 stream 数:sum(count by (stream) (...));把动态 label 移回日志行 |
| 查询结果不全 | 查询未先按 label 过滤 | LogQL 必须先有 label 选择器缩小范围再内容匹配,否则全扫描 |
| ES 磁盘写满 | 无 ILM/留存 | 配 ILM 滚动删除; curator 定期清理 |
| 日志时间错乱 | 容器无时区/无 timestamp 解析 | pipeline 用 time stage 解析日志内时间字段,统一 UTC |
性能、容量与成本
- Loki:成本 ≈ chunk 数 × 留存 × 对象存储单价。控制 chunk 数 = 控制 label 基数。Debug 级日志在繁忙服务可能比其余信号加起来还贵,应在采集层丢弃(
dropstage)。 - ES:成本随写入量与副本数上升,热节点用 SSD。ILM 把冷数据挪到对象存储/HDD。
- 采样/降噪:结构化 JSON + 固定字段(level、status_code)可在查询时直接过滤,避免 regex 全文扫描。
常见坑
- 把
request_id当 Loki label → 流爆炸。 - 应用打非结构化文本日志 → Loki 只能正则扫描,慢且脆;先改结构化 JSON。
kubectl logs误当持久存储 → Pod 删了日志就没,必须接中心存储。- 用 Loki 做合规归档 → 成本性能双输,合规另存。
替代方案与权衡
- Vector:Rust 实现、吞吐高、可同时输出多后端;学习曲线略陡。
- OpenTelemetry Collector(filelog receiver):统一 pipeline,与 traces/metrics 一套配置;需
presets.logsCollection.enabled。 - 托管日志(Loki/Grafana Cloud、ES Serverless):免运维,代价是出口费用。
FAQ
Q:Promtail 和 Alloy 怎么选? A:新集群优先 Alloy——OTel 原生、三信号统一、label 对齐收益大;存量 Promtail 足够且无迁移必要可不换。
Q:Loki 能替代 ELK 做 APM 吗? A:不能。Loki 是日志,链路追踪用 Tempo/Jaeger,指标用 Prometheus;三者经 Grafana 统一查询,而非互相替代。
参考资料
- Grafana 文档 - Loki(架构与 LogQL),访问日期:2026-10-08。
- Grafana 博客 - Five tricks for logging at scale in K8s with Loki,访问日期:2026-10-08。
- Grafana 文档 - Grafana Alloy(logs 采集),访问日期:2026-10-08。
- Fluent Bit 官方文档 - Kubernetes 部署,访问日期:2026-10-08。
- Kubernetes 官方文档 - Logging architecture,访问日期:2026-10-08。