26-日志栈选型
日志是可观测性三支柱中"细但贵"的信号——保留完整事件上下文,能回答"这一次到底发生了什么",但数据量大、存储成本高。本章覆盖 EFK vs Loki 两套主流日志栈的架构对比、采集架构选型、结构化日志规范,以及国内云托管方案。
日志的关键性质#
贵:每个事件都要存,高吞吐服务一天几十上百 GB。所以日志必配三件套——采样 sampling、分级(debug/info/warn/error,线上默认 info 起)、保留策略 retention(热数据 7 天、冷数据归档对象存储)。
结构化日志是分水岭:
- 初级:纯文本
"user login failed for tenant A"→ 只能 grep,没法聚合。 - 成熟:结构化 JSON
{"event":"login_failed","tenant":"A","reason":"expired_token"}→ 每个字段可索引、过滤、聚合。
从文本到结构化,是日志从"翻日志"升级到"查询+分析"的关键一跃。
两种存储哲学#
日志存储有两条完全不同的技术路线:
| 全文索引派(Elasticsearch) | Label 索引派(Loki) | |
|---|---|---|
| 索引对象 | 日志每个词 | 只索引 label(service/level…) |
| 全文搜索 | 快 | 慢(暴力扫) |
| 存储成本 | 高(索引比原始数据还大) | 低(正文压缩存对象存储) |
| 查询模式 | 任意全文搜索 | 先 label 缩小范围再扫 |
| 适用 | 审计日志、安全日志、复杂聚合 | 实时监控、日志+指标关联 |
Loki 的设计哲学就是"像 Prometheus 一样只用 label 找日志"——这也是为什么它和 Prometheus/Grafana 天然一套。
EFK 日志栈#
EFK = Elasticsearch + Fluentd/Fluent Bit + Kibana。
两层采集架构#
为什么不直接用 Fluent Bit 写 ES?职责分离:
- Fluent Bit(DaemonSet):轻量(~5MB 内存),跑在每个节点上,tail 日志文件,做 K8s 元数据 enrichment,通过 Forward 协议转发。
- Fluentd(Deployment):内存大但插件丰富,集中做 JSON 解析、字段映射、批量写入 ES。
节点 /var/log/containers/*.log
↓(tail)
Fluent Bit DaemonSet(K8s enrichment → Forward)
↓(Forward 协议)
Fluentd Deployment(JSON 解析 → 打标签 → Buffer)
↓(bulk API)
Elasticsearch → KibanaFluent Bit 配置#
[SERVICE]
Flush 5
Log_Level info
storage.path /var/log/flb-storage/
[INPUT]
Name tail
Tag kube.*
Path /var/log/containers/*.log
Exclude_Path /var/log/containers/fluent-bit*
Parser docker
DB /var/log/flb_kube.db
Mem_Buf_Limit 50MB
[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Merge_Log On # 把 log 字段里的 JSON 解析合并到顶层
K8S-Logging.Parser On
K8S-Logging.Exclude On
[OUTPUT]
Name forward
Match kube.*
Host fluentd.logging.svc.cluster.local
Port 24224
Retry_Limit 10Elasticsearch ILM 策略#
按日期滚动 + ILM 控制数据生命周期:
{
"policy": {
"phases": {
"hot": {
"actions": { "rollover": { "max_primary_shard_size": "50gb", "max_age": "1d" } }
},
"warm": {
"min_age": "3d",
"actions": { "shrink": { "number_of_shards": 1 }, "forcemerge": { "max_num_segments": 1 } }
},
"cold": {
"min_age": "14d",
"actions": { "freeze": {} }
},
"delete": {
"min_age": "30d",
"actions": { "delete": {} }
}
}
}
}Kibana 适合的场景#
- 全文搜索、模糊匹配(ES 的 text 分析器强)
- 审计日志、安全日志,长期保存 + 精确查询
- 复杂聚合分析(按字段 group by、histogram、top N)
Loki 日志栈#
Loki = Grafana 出品的日志后端,只索引 label,不索引日志正文,存储成本远低于 ES。
架构#
Promtail/Fluent Bit
│ POST /loki/api/v1/push
▼
Distributor → Ingester (RF=3) → 对象存储 (S3/OSS)
▲
Query Frontend → Querier → Store Gateway- Distributor:校验 + 一致性哈希分发到 Ingester
- Ingester:内存 chunk,满了 flush 到对象存储
- Querier:最近数据问 ingester,历史数据问对象存储
LogQL 查询#
# 查某服务的错误日志
{namespace="production", app="my-service"} |= "ERROR"
# 解析 JSON 日志并过滤
{namespace="production"} | json | level="error" | duration > 1000
# 统计每分钟错误数
sum(rate({namespace="production"} |= "ERROR" [1m]))Loki 适合的场景#
- 实时监控和告警,配合 Prometheus 一起看
- 日志和指标的关联分析(同一 Grafana 面板)
- 存储成本敏感(Loki 不做全文索引,成本低很多)
- 开发阶段快速排查,LogQL 的流处理管道方便
EFK vs Loki 对比#
| 维度 | EFK(Elasticsearch) | Loki |
|---|---|---|
| 存储成本 | 高(索引比原始数据大) | 低(只索引 label,正文压缩存对象存储) |
| 全文搜索 | 强(ES 倒排索引) | 弱(label 缩范围后暴力扫) |
| 查询语言 | KQL / Lucene | LogQL(类 PromQL) |
| 聚合分析 | 强(group by / histogram / top N) | 中(基础聚合) |
| 运维复杂度 | 高(JVM 调优、分片管理) | 中(组件多但无 JVM) |
| 与 Grafana 集成 | 有数据源插件 | 原生集成 |
| 与 Prometheus 联动 | 弱 | 强(同一 UI、同一 label 体系) |
| 适合日志量 | 大(但有预算) | 大(成本敏感) |
选型经验:
- 需要全文搜索、审计日志长期保存 → EFK
- 成本敏感、日志和指标联动分析 → Loki
- 预算充足两者都装 → Loki 做实时排障 + ES 做审计合规
采集架构:DaemonSet vs Sidecar#
DaemonSet 模式#
每个节点一个采集 agent,tail 节点上所有容器的日志文件:
- 优点:资源开销低(每节点一个),部署简单
- 缺点:无法按 Pod 定制解析逻辑
- 适合:大多数场景,K8s 标准日志采集
Sidecar 模式#
每个 Pod 旁挂一个采集容器:
- 优点:可按 Pod 定制解析、多输出目标
- 缺点:资源开销高(每 Pod 一个),运维复杂
- 适合:特殊解析需求、非标准日志位置
经验:默认用 DaemonSet,除非有强烈的按 Pod 定制需求才用 Sidecar。
结构化日志规范#
必须包含的字段#
{
"timestamp": "2026-04-12T14:32:15.123Z",
"level": "error",
"service": "checkout-api",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"user_id": "u_12345",
"message": "Payment failed",
"error": "bank API timeout after 3000ms",
"http_path": "/api/checkout",
"http_method": "POST",
"http_status": 500,
"duration_ms": 3102
}关键:trace_id 是日志和链路联动的桥梁——没有 trace_id,三支柱变两支柱。
分级原则#
| 级别 | 含义 | 线上默认 |
|---|---|---|
| DEBUG | 调试细节 | 关 |
| INFO | 正常业务事件 | 开 |
| WARN | 异常但可恢复 | 开 |
| ERROR | 错误需关注 | 开 |
线上默认 INFO 起,DEBUG 按需临时开启。
国内云托管方案#
阿里云 SLS(Simple Log Service)#
- 采集:Logtail / LoongCollector
- 存储:Logstore,支持索引 + 投递 OSS
- 查询:SQL 92 + SPL(日志处理语言)
- 优势:与阿里云生态深度集成,免运维
腾讯云 CLS(Cloud Log Service)#
- 采集:Loglistener
- 存储:日志主题,支持索引 + 投递 COS
- 查询:Lucene 语法 + SQL
- 优势:腾讯云生态集成
选型:纯单云环境用托管方案省运维,多云或成本敏感用自建 Loki/EFK。
常见坑#
Fluentd Buffer 满了#
ES 写入不可用时 Fluentd buffer 会在几分钟内写满,触发背压,最终 Fluent Bit 也开始丢日志。要监控 buffer 使用率,total_limit_size 根磁盘容量合理设置。
ES 分片过多#
每天生成的 index 数量 = namespace × 日期 × shard 数,一个月可能几千个分片。每个分片在 JVM 堆里占 ~500KB,分片多了 master 节点 CPU 居高不下。解决:用 ILM Rollover 按 shard 大小滚动而非按天,warm 阶段 shrink 到 1 个分片。
Loki label 基数爆炸#
把 user_id、request_id 当 label → TSDB 膨胀 → ingester OOM。铁律:label 只放低基数维度,高基数字段放 structured metadata(v13 schema 支持)。
日志没有 trace_id#
日志里没有 trace_id,Metrics → Logs → Traces 的下钻链路就断了。在日志 Middleware 里从 span context 提取 trace_id 写入日志:
spanCtx := trace.SpanFromContext(ctx).SpanContext()
logger := log.With().
Str("trace_id", spanCtx.TraceID().String()).
Str("span_id", spanCtx.SpanID().String()).
Logger()小结#
日志栈选型的核心是存储哲学——ES 全文索引强但贵,Loki 只索引 label 便宜但全文搜索弱。预算充足两者都装:Loki 做实时排障 + ES 做审计合规。采集架构默认 DaemonSet,按 Pod 定制才用 Sidecar。结构化日志是基础——必须包含 timestamp/level/service/trace_id,trace_id 是三支柱联动的桥梁。核心坑:ES 分片别太多(ILM Rollover)、Loki label 别放高基数(用 structured metadata)、日志必带 trace_id。