日志体系-ELK-Loki
1. 方案对比
| 栈 | 特点 | 适合 |
|---|---|---|
| ELK(Elasticsearch + Logstash/Fluentd + Kibana) | 全文索引、查询强 | 大型企业、深度搜索 |
| EFK(Fluentd 代 Logstash) | K8s 主流 | 容器场景 |
| PLG(Promtail + Loki + Grafana) | 只索引 label、便宜 | 中小团队、Grafana 已有 |
| Vector + 后端 | Rust 实现,超高性能 | 任意场景 |
| 商业 SaaS(Datadog / 阿里 SLS) | 省心 | 不想自维护 |
2. PLG(Loki)
2.1 部署
helm repo add grafana https://grafana.github.io/helm-charts
helm install loki grafana/loki-stack -n logging --create-namespace \
--set promtail.enabled=true \
--set grafana.enabled=true
2.2 Promtail 自动收集
DaemonSet 跑在每节点,读 /var/log/pods/ stdout 日志,打 label:
{namespace="frontend", pod="my-frontend-abc", container="web"}
应用只需 stdout 输出 JSON 即可。
2.3 LogQL 查询
# 看 namespace 所有日志
{namespace="frontend"}
# 按关键词过滤
{namespace="frontend"} |= "ERROR"
# JSON 解析后过滤字段
{namespace="frontend"} | json | level="error"
# 计数:5 分钟内错误数
sum(count_over_time({namespace="frontend"} |= "ERROR" [5m]))
# 按 pod 分组
sum by (pod) (rate({namespace="frontend"} |~ "ERROR" [5m]))
# 提取字段并聚合
{namespace="frontend"}
| json
| __error__ = ""
| line_format "}}.userId}} }}.action}}"
2.4 标签 vs 字段
Loki 只对标签建索引,不对内容。基数高的字段(userId)不能当 label,要存到日志内容里查询时解析。
3. ELK 栈
3.1 架构
应用 stdout → Filebeat / Fluentd → Logstash → Elasticsearch → Kibana
3.2 Filebeat(轻量)
# filebeat-kubernetes.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: filebeat
spec:
template:
spec:
containers:
- name: filebeat
image: docker.elastic.co/beats/filebeat:8.11.0
volumeMounts:
- name: varlog
mountPath: /var/log
- name: config
mountPath: /usr/share/filebeat/filebeat.yml
subPath: filebeat.yml
volumes:
- name: varlog
hostPath: { path: /var/log }
- name: config
configMap: { name: filebeat-config }
3.3 Elasticsearch 索引
按时间分索引:logs-2026.06.18。配 ILM(Index Lifecycle Management)自动 rollover + 删旧:
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": { "max_size": "50GB", "max_age": "1d" }
}
},
"delete": {
"min_age": "7d",
"actions": { "delete": {} }
}
}
}
}
3.4 Kibana 查询
KQL(Kibana Query Language):
kubernetes.namespace: "frontend" AND level: "error"
http.response.status_code >= 500
@timestamp >= "2026-06-18T10:00:00"
4. 应用层规范
4.1 必须 JSON 结构化
// pino
const logger = require('pino')()
logger.info({
userId: 123,
requestId: 'abc',
duration: 234,
}, '用户登录')
输出:
{"level":30,"time":1718681400000,"userId":123,"requestId":"abc","duration":234,"msg":"用户登录"}
4.2 必备字段
timestamp/@timestamplevelserviceenvtraceIdrequestIduserIdmessage
4.3 不该写
- 完整 token / 密码
- 大对象 dump(数 MB body)
- 大量 debug 日志(生产应 info+)
5. 容器 / K8s 日志
容器日志写 stdout / stderr → 容器运行时(containerd)→ /var/log/pods/<ns>_<pod>/<container>/0.log → DaemonSet 收集。
应用不要写文件:容器销毁数据丢,volume 挂复杂。
K8s 日志命令:
kubectl logs <pod> -n frontend -f
kubectl logs <pod> --previous # 上次崩的日志
kubectl logs -l app=frontend --tail=100 # 按 label 看多 pod
kubectl logs <pod> -c sidecar # 指定容器
6. 日志保留策略
| 数据 | 推荐保留 |
|---|---|
| INFO 日志 | 7-14 天 |
| ERROR 日志 | 30-90 天 |
| 审计日志 | 1-7 年(按合规) |
存储成本:Loki < Elasticsearch(10x+ 差距)。
7. 关联:traceId
每条日志带 traceId:
import { trace } from '@opentelemetry/api'
logger.info({
traceId: trace.getActiveSpan()?.spanContext().traceId,
userId,
}, '操作完成')
Grafana 一键日志 → trace 跳转。
8. 告警
基于日志的告警(Loki / Elasticsearch Alerting):
# Loki 告警规则
- alert: HighErrorRate
expr: |
sum(rate({namespace="frontend"} |~ "ERROR" [5m])) > 10
for: 5m
annotations:
summary: "frontend 5 分钟内 ERROR > 50 条"
9. 故障排查
# 日志查不到
# 1. 应用是否 stdout 输出?
kubectl logs <pod> # 有就对
# 2. promtail / filebeat 是否运行
kubectl get ds -n logging
# 3. 后端是否接收
# Loki: 看 distributor / ingester pod 日志
# ES: curl localhost:9200/_cluster/health
10. 常见反模式
- 应用写文件不写 stdout:容器销毁丢
- info 级别写每个 SQL:日志量爆炸
- userId 当 Loki label:基数爆
- 打印完整 cookie / token:审计灾难
- 不分级别:所有日志同优先级
- 保留时间无限:磁盘满
- 不关联 traceId:链路断
- 每个 Pod 日志独立查:应该集中化
11. 延伸阅读
- Loki 文档
- Elastic Stack
- Fluent Bit
- Vector
- 模块 01 系统日志与 journalctl
- 模块 08 可观测性三大支柱