阶段三 · Logging 日志

日志采集与存储:EFK 与 Loki

一句话总结

日志编写规范之后,下一步是将分散在成百上千个容器中的日志收集,存储并使之可查询.两大主流方案 EFK 和 Loki 的根本分歧归结为一点:是否对日志正文建立全文索引?
EFK 选择全索引--查询能力强,功能丰富,但成本极高;Loki 仅索引标签,不索引正文--成本降低一个数量级,代价是查询方式受到一定限制.

前置回顾

第 04 篇解决了单条日志的编写规范:结构化,分等级,携带 trace_id.但那仅覆盖了"数据源头".在 Kubernetes 环境中,数百个 Pod 每秒输出海量日志,且 Pod 随时可能被销毁或漂移(日志不能仅保留在容器内部,否则 Pod 销毁后日志即丢失).本篇解决的是"这些日志如何汇总到一处,如何以可控成本存储,如何高效查询"--这是日志这一支柱能够真正发挥作用的工程基础.

通用管线:日志从产生到可查询的四个环节

无论采用哪套方案,日志系统都遵循同一条处理管线.先建立整体骨架:

[应用<br/>输出日志到 stdout] → [采集 Agent<br/>每个节点一个<br/>(Fluent Bit / Promtail)] → [存储 + 索引<br/>(Elasticsearch / Loki)] → [查询 + 可视化<br/>(Kibana / Grafana)]
环节 职责 关键点
采集(Agent) 从每个节点收集容器日志,补充元数据(Pod 名称,namespace 等),转发至后端 通常以 DaemonSet 方式部署,每个 Kubernetes 节点运行一个实例
存储 + 索引 持久化日志数据,并建立索引以支持查询 索引策略是 EFK 与 Loki 的核心差异
查询 + 可视化 提供查询语言和可视化界面 Kibana(配合 Elasticsearch)/Grafana(配合 Loki)

应用应将日志输出到 stdout,而非写入文件

容器化环境中有一条重要约定(源自十二要素应用):应用将日志视为事件流,写入标准输出(stdout),不自行管理文件,不自行执行日志轮转.容器运行时会自动将 stdout 内容落盘至节点文件,采集 Agent 再从统一位置读取.

这样做的优势在于应用与日志收集系统完全解耦--应用不关心日志最终流向何处,以何种方式存储,更换日志后端无需修改任何应用代码.这是"解耦"思想(与第 03 篇 Pull 模型相呼应)的又一次体现.

核心分歧:是否对日志正文建立全文索引

管线中最关键,成本最高的环节是"存储 + 索引".要理解 EFK 与 Loki 的区别,需要先明确一个问题:"索引"解决什么问题,又需要付出什么代价?

索引类似于书末的"关键词索引页":有了它,查找"某个词出现在哪一页"可以即时定位,无需逐页翻阅.但建立索引需要额外的计算和存储资源--对于日志而言,需要索引的"词"可能是正文中的每一个单词,代价极为可观.

一条日志:"order 7731 failed: payment_gateway_timeout after 3 retries"

全文索引(Elasticsearch 的做法):
将每个词(order/7731/failed/payment.../retries...)拆解后建立倒排索引
→ 能够搜索任意词汇,查询 "timeout" 即时命中
→ 但索引本身可能比原始日志还大,CPU,内存,磁盘均需大量投入

不索引正文(Loki 的做法):
仅索引该日志的"标签"(如 service=order, level=error)
正文整体压缩存储,不拆词,不建索引
→ 存储和计算成本大幅下降
→ 但无法直接"搜索任意词",需要先用标签缩小范围,再在范围内扫描正文

这一取舍,即是 EFK 与 Loki 两种方案的全部差异根源.下面分别展开说明.

EFK:全文索引,功能强大但成本高昂

EFK = Elasticsearch + Fluentd(或 Fluent Bit)+ Kibana(其前身称为 ELK,L 指 Logstash).核心是 Elasticsearch--一个为全文搜索而设计的分布式搜索引擎.

  • Elasticsearch:对每条日志的(几乎)每个字段,每个词建立倒排索引(Inverted Index).由此支持复杂的全文检索,任意字段聚合和模糊匹配.
  • Fluentd/Fluent Bit:采集 Agent,负责收集并转发日志(Fluent Bit 更轻量,是当前主流选择).
  • Kibana:功能丰富的查询与可视化界面.
EFK 的优势 EFK 的代价
查询能力极强:支持任意词全文搜索,复杂聚合,模糊匹配 资源消耗巨大:索引体积常与原始数据相当甚至更大
生态成熟,功能丰富(告警,机器学习异常检测等) 运维复杂度高:ES 集群调优(分片策略,JVM 堆大小,冷热分层)需要专门经验
适用于"日志即数据"的深度分析场景 成本高:存储等量日志,花费可能为 Loki 的数倍至十倍

Loki:仅索引标签,以低成本为核心竞争力

Loki(Grafana 出品)的设计理念简洁明确:"以 Prometheus 的方式处理日志".其核心洞察在于--多数场景下,全文索引并非必要:

实际查询日志的流程通常是:先通过已知的维度缩小范围(已知是 order-service,ERROR 级别,过去 1 小时内),再在这一小批日志中查看内容.既然如此,无需对每个词建立索引--仅索引用于缩小范围的那几个标签即可.

因此 Loki 仅对标签(service,namespace,level 等,这正是第 03 篇 Prometheus 标签体系的延续)建立索引,日志正文以压缩形式直接存入低成本的对象存储(S3 等),完全不建立索引.其查询语言 LogQL 也刻意保持与 PromQL 相似的语法:

# LogQL:先用标签 {} 缩小范围(走索引,快速),再对正文做过滤(扫描)
{service="order-service", level="error"} |= "payment_gateway_timeout"
└──────────────┬──────────────────────┘ └──────────┬─────────────┘
标签选择(有索引,快速缩小范围) 在该范围内扫描正文查找关键词
Loki 的优势 Loki 的代价
存储成本极低:无全文索引,正文压缩存入对象存储,成本大幅下降 无法高效执行"全局搜索任意词":必须先通过标签限定范围
运维简单,水平扩展容易(无状态组件 + 对象存储) 若标签选择不当(无法有效缩小范围),正文扫描会变慢
与 Grafana/Prometheus 天然集成,标签体系统一 复杂全文分析能力不及 Elasticsearch

Elasticsearch 如同为整本书的每个词都编制了索引:查找任意词均可即时定位,但编制这套索引本身是一项庞大的工程(成本高).
Loki 如同仅为书籍编制了章节目录:查找内容时,先定位到大致章节(标签),再在这几页中快速浏览(扫描正文)--目录成本远低于全词索引,代价是无法直接"全书搜索某个词".

选型建议

一句话决策

日志主要用于"按已知维度排查"(已知服务,时间范围,日志级别),且关注成本 → 选择 Loki;日志需要作为数据进行深度全文分析,支持任意维度的自由探索,且预算充足 → 选择 EFK.

云原生新项目,尤其是已经采用 Prometheus + Grafana 的团队,Loki 通常是更自然,更经济的选择;而安全审计,需要复杂检索分析的场景,Elasticsearch 的能力仍不可替代.

这一选择本质上是第 04 篇结尾"成本意识"的延续:日志是成本最高的可观测性数据,而全文索引是日志成本的主要来源.Loki 的整体设计是一次"以查询灵活性换取存储成本"的精心权衡--其前提假设是"90% 的查询需求仅需标签选择加少量正文扫描即可满足".理解这一假设,即理解了 Loki 的设计逻辑.

承上启下:接下来关注"在哪个环节"

至此,可观测性的两大支柱已经齐备:指标(阶段二)告知系统出现异常,日志(阶段三)解释异常的原因.但还缺少关键的一环--当请求跨越十几个服务时,日志和指标都无法展现"请求在服务间如何流转,延迟具体发生在哪一跳".这正是第 04 篇反复提及的 trace_id 所指向的那个支柱:分布式追踪(Distributed Tracing).阶段四将对其进行深入讲解,并最终(第 08 篇)通过 trace_id 将三大支柱集成为一个完整的可观测性体系.

快速回顾

  • 通用管线:应用输出到 stdout → 采集 Agent(DaemonSet,每节点一个)→ 存储 + 索引 → 查询与可视化;应用输出到 stdout 是为了与日志后端解耦
  • 核心分歧:是否对日志正文建立全文索引--这是 EFK 与 Loki 所有差异的根源
  • EFK:Elasticsearch 提供全文索引,查询能力极强(任意词搜索,复杂聚合),但资源消耗大,运维复杂,成本高
  • Loki:仅索引标签,正文压缩存入对象存储,成本大幅降低,运维简单,与 Grafana 深度集成;代价是必须先通过标签限定范围,不擅长全局全文搜索
  • 选型:按已知维度排查 + 关注成本 → Loki;深度全文分析 + 预算充足 → EFK
  • 本质:Loki 是以"查询灵活性换取存储成本"的权衡方案,延续第 04 篇"日志成本最高"的成本控制主线

Loki 不建立全文索引,查询 payment_gateway_timeout 时是否需要扫描海量正文,性能会很差吗?

关键在于标签是否先将范围缩小到足够小的规模.{service="order-service", level="error"} 可能已将候选从"全集群数十亿条"缩小至"该服务的错误日志数万条",在此范围内扫描关键词性能很高.Loki 查询慢,通常是因为标签选择不当(例如仅写 {namespace="prod"},范围仍达上亿条).使用 Loki 的关键在于"设计合理的标签 + 查询时优先使用标签缩小范围".反之,这也要求像第 03 篇那样克制标签基数--Loki 的标签同样会出现基数爆炸问题.

采集 Agent 为什么使用 DaemonSet(每个节点一个实例)?

因为容器日志最终存储在各节点的本地磁盘上.每个节点运行一个 Agent(DaemonSet 正是"每节点一个 Pod"的工作负载类型,参见 Kubernetes 笔记),即可就近读取本节点上所有容器的日志,无需跨节点传输原始文件.这是最高效,最节省网络开销的采集拓扑.Agent 还会自动为日志补充 Pod 名称,namespace,节点等 Kubernetes 元数据,这些数据即成为 Loki 中可用的标签.

动手练习

  1. 搭建 Loki 环境:使用 docker-compose 搭建一套 Loki + Promtail + Grafana 环境,将第 04 篇中输出 JSON 日志的服务接入.
  2. 体验两段式查询:在 Grafana 的 Explore 界面中使用 LogQL 查询:先仅使用标签 {service="..."},再加入正文过滤 |= "error",体验"标签缩小范围 + 正文扫描"两段式查询模式.
  3. 验证 Trace ID 串联:利用第 04 篇携带的 trace_id,在 Loki 中使用 |= "a1b2c3" 拉取一次请求的全部日志--验证 Trace ID 串联在真实日志系统中可行.
  4. 对比存储成本:查阅资料对比:存储 1TB 日志,Elasticsearch 与 Loki 分别大致需要多少磁盘空间?差距来源是什么?用自己的语言解释这一差距.
  5. 评估选型:思考当前项目的日志使用方式:主要是"按服务/时间/级别排查"还是"需要全文自由搜索分析"?据此判断应选择 Loki 还是 EFK,并记录选择理由.