路线图

26-日志栈选型

星辉 2026-07-02 阅读 3 min 633 字 路线图
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。
text
节点 /var/log/containers/*.log
    ↓(tail)
Fluent Bit DaemonSet(K8s enrichment → Forward)
    ↓(Forward 协议)
Fluentd Deployment(JSON 解析 → 打标签 → Buffer)
    ↓(bulk API)
Elasticsearch → Kibana

Fluent Bit 配置#

yaml
[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   10

Elasticsearch ILM 策略#

按日期滚动 + ILM 控制数据生命周期:

json
{
  "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。

架构#

text
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 查询#

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 / LuceneLogQL(类 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。

结构化日志规范#

必须包含的字段#

json
{
  "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 写入日志:

go
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。