概述

线上炸了,你打开 Kibana 搜日志,发现关键 Pod 3 分钟前被 OOM Kill 重启了,旧日志跟着容器一起灰飞烟灭。这种事在 K8s 环境里太常见了——Pod 是短命的,日志不能跟着 Pod 一起消失。

K8s 的日志采集和传统虚拟机完全不是一回事。传统环境日志躺在 /var/log/ 下,SSH 上去就能看。K8s 里 Pod 随时可能被调度到任何节点,随时可能被销毁重建,日志分散在整个集群。如果不做集中采集,故障排查时你根本找不到日志在哪。

这篇文章把 K8s 日志采集的几种方案拆开讲清楚:DaemonSet 模式、Sidecar 模式、以及基于节点 Agent 的方案。每种方案什么场景用、怎么配置、踩什么坑,全部覆盖。最后给出一套生产可用的选型决策框架。

K8s 日志的三种类型

在讲采集方案之前,先搞清楚 K8s 里有哪些日志。不同类型的日志采集策略完全不同。

1. 容器标准输出日志

这是最常见的一类。应用把日志写到 stdout/stderr,容器运行时(containerd 或 Docker)负责把这些日志落盘到节点的 /var/log/containers/ 目录。

# 查看某节点的容器日志文件
ls /var/log/containers/

# 输出示例
# nginx-xxx_default_app-abc123.log
# redis-yyy_default_app-def456.log

# 这些文件实际是指向 /var/log/pods/ 下的符号链接
ls -l /var/log/containers/nginx-xxx_default_app-abc123.log
# lrwxrwxrwx 1 root root 99 ... nginx-xxx_default_app-abc123.log -> /var/log/pods/default_nginx-xxx_abc123/app/0.log

这类日志的好处是采集简单——DaemonSet 模式直接读 /var/log/containers/*.log 就行。坏处是应用必须把日志输出到 stdout,不能写文件。

2. 容器内部文件日志

有些应用(特别是 Java 应用)习惯把日志写到文件里,比如 /app/logs/app.log。这些日志不会出现在 /var/log/containers/ 下,需要额外处理。

# 典型的 Java 应用日志配置
apiVersion: apps/v1
kind: Deployment
metadata:
  name: java-app
spec:
  template:
    spec:
      containers:
        - name: app
          image: openjdk:17
          # 应用日志写到 /app/logs/app.log
          volumeMounts:
            - name: logs
              mountPath: /app/logs
      volumes:
        - name: logs
          emptyDir: {}

这类日志的采集是个麻烦事。DaemonSet 模式默认读不到容器内部的文件,得用 Sidecar 模式或者让应用把日志也输出到 stdout。

3. 节点系统日志

节点本身的系统日志,比如 kubelet、容器运行时、内核日志。这些日志不在容器的管辖范围内,通常通过 systemd journal 或节点上的文件采集。

# 查看节点上的关键系统日志位置
# kubelet 日志
journalctl -u kubelet

# 容器运行时日志
journalctl -u containerd

# 内核日志
dmesg | tail -20

三种日志采集架构对比

架构一:DaemonSet 模式

DaemonSet 模式是 K8s 日志采集最主流的方案。每个节点上跑一个日志采集 Agent(Fluent Bit、Filebeat 等),挂载节点的日志目录,采集该节点上所有容器的日志。

打个比方:DaemonSet 就像一栋大楼的物业管理员,每层楼配一个,负责收集这层所有房间的垃圾。住户不需要自己操心垃圾怎么运出去。

┌─────────────────────────────────────┐
            Node 1                   
  ┌──────────┐  ┌──────────┐        
    Pod A       Pod B           
   stdout     stdout          
  └──────────┘  └──────────┘        
                                   
                                   
  /var/log/containers/*.log          
                                    
  ┌────▼──────────┐                  
    Fluent Bit     DaemonSet     
    (采集Agent)                     
  └───────────────┘                  
                                    
└───────┼─────────────────────────────┘
        
    后端存储 (ES / Loki / Kafka)

优点

  • 资源开销低,每个节点只跑一个 Agent
  • 对业务无侵入,不需要修改 Pod 配置
  • 部署维护简单,一次部署覆盖全集群

缺点

  • 无法针对单个 Pod 定制采集规则
  • 容器内部文件日志采集困难
  • 多租户场景下日志隔离不好做

适用场景:日志格式统一、输出到 stdout 的标准容器化应用。90% 的集群用这个就够了。

架构二:Sidecar 模式

Sidecar 模式在每个 Pod 里跑一个额外的日志采集容器,和业务容器共享存储卷。

┌─────────────────────────────────┐
│            Pod A                │
│  ┌──────────┐  ┌─────────────┐ │
│  │ App      │  │ Fluent Bit  │ │
│  │ 写日志→  │←│ 采集容器     │ │
│  │ /app/log │  │ (Sidecar)   │ │
│  └──────────┘  └─────────────┘ │
│       共享 emptyDir 卷          │
└─────────────────────────────────┘
    后端存储 (ES / Loki / Kafka)

Sidecar 有两种变体:

变体一:日志代理 Sidecar

采集容器直接读取业务容器的日志文件,处理后发往后端:

apiVersion: v1
kind: Pod
metadata:
  name: java-app-with-sidecar
spec:
  containers:
    - name: app
      image: openjdk:17
      command: ["java", "-jar", "/app/app.jar"]
      volumeMounts:
        - name: logs
          mountPath: /app/logs
    - name: log-collector
      image: fluent/fluent-bit:2.2.0
      volumeMounts:
        - name: logs
          mountPath: /app/logs
          readOnly: true
        - name: config
          mountPath: /fluent-bit/etc/
  volumes:
    - name: logs
      emptyDir: {}
    - name: config
      configMap:
        name: fluent-bit-sidecar-config

变体二:Streaming Sidecar

如果应用只支持写日志到文件,但你又想用 DaemonSet 模式采集 stdout,可以用 Streaming Sidecar——它把业务容器写的日志文件读到后输出到自己的 stdout,这样 DaemonSet Agent 就能采集到了:

apiVersion: v1
kind: Pod
metadata:
  name: app-with-streaming-sidecar
spec:
  containers:
    - name: app
      image: myapp:latest
      # 应用日志写到 /var/log/app.log
      volumeMounts:
        - name: logs
          mountPath: /var/log
    - name: streaming-sidecar
      image: busybox
      # 把日志文件内容持续输出到 stdout
      command: ["sh", "-c", "tail -n+1 -f /var/log/app.log"]
      volumeMounts:
        - name: logs
          mountPath: /var/log
  volumes:
    - name: logs
      emptyDir: {}

我的看法:Streaming Sidecar 是个 hack 方案。如果你的应用能改,直接让它输出 stdout 才是正道。Streaming Sidecar 多一个容器、多一层 IO 开销,还多了文件 IO 的单点风险。

优点

  • 可以针对每个 Pod 定制采集规则
  • 能采集容器内部文件日志
  • 支持多租户日志隔离

缺点

  • 资源开销大,每个 Pod 多一个容器
  • 运维复杂度高,需要管理大量 Sidecar 配置
  • Pod 启动顺序和日志丢失风险

适用场景:日志格式特殊需要定制解析、多租户隔离需求、Java 多行堆栈日志需要特殊处理。

架构三:Agentless 模式

Agentless 模式不部署额外 Agent,直接用 K8s API 和容器运行时接口采集日志。比如通过 kubelet 的 API 获取日志,或者用 kubectl logs 的方式。

优点:零额外资源开销 缺点:实时性差、无法做复杂处理、大规模集群性能瓶颈

这个方案在实际生产中用得少,不做展开。

三种架构对比

维度DaemonSetSidecarAgentless
资源开销低(每节点1个)高(每Pod1个)极低
业务侵入
定制能力
运维复杂度
容器文件日志不支持支持不支持
集群规模适应性
推荐度首选特殊场景不推荐

Fluent Bit 实战配置

Fluent Bit 是 CNCF 旗下的轻量级日志采集器,用 C 语言编写,内存占用通常只有 10-50MB,是 K8s 日志采集的首选方案。从 Fluentd 转向 Fluent Bit 是近几年的趋势。

DaemonSet 部署 Fluent Bit

完整的 Fluent Bit DaemonSet 部署配置:

# fluent-bit-daemonset.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluent-bit
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: fluent-bit
rules:
  - apiGroups: [""]
    resources:
      - namespaces
      - pods
      - nodes
      - nodes/proxy
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: fluent-bit
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: fluent-bit
subjects:
  - kind: ServiceAccount
    name: fluent-bit
    namespace: kube-system
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluent-bit-config
  namespace: kube-system
data:
  fluent-bit.conf: |
    [SERVICE]
        Flush         5
        Log_Level     info
        Daemon        off
        HTTP_Server   On
        HTTP_Listen   0.0.0.0
        HTTP_Port     2020
        Parsers_File  parsers.conf
    
    [INPUT]
        Name              tail
        Path              /var/log/containers/*.log
        Parser            cri        # containerd 用 cri,docker 用 docker
        Tag               kube.*
        Refresh_Interval  5
        Mem_Buf_Limit    50MB
        Skip_Long_Lines   On
        DB                /var/log/flb_kube.db
        DB.Sync           Normal
    
    [FILTER]
        Name           kubernetes
        Match          kube.*
        Kube_URL       https://kubernetes.default.svc:443
        Kube_CA_File   /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
        Kube_Token     /var/run/secrets/kubernetes.io/serviceaccount/token
        Merge_Log      On
        K8S-Logging.Parser  On
        K8S-Logging.Exclude On
    
    [OUTPUT]
        Name            es
        Match           *
        Host            elasticsearch.logging.svc
        Port            9200
        Index           k8s-logs
        Type            _doc
        Replace_Dots    On
        Trace_Error     On
        Retry_Limit     5
    
    [OUTPUT]
        Name            stdout
        Match           *
        # 调试用,上线后注释掉    
  parsers.conf: |
    [PARSER]
        Name        cri
        Format      regex
        Regex       ^(?<time>[^ ]+) (?<stream>stdout|stderr) (?<logtag>[^ ]*) (?<log>.*)$
        Time_Key    time
        Time_Format %Y-%m-%dT%H:%M:%S.%L%z
    
    [PARSER]
        Name        docker
        Format      json
        Time_Key    time
        Time_Format %Y-%m-%dT%H:%M:%S.%L
        Time_Keep   On
    
    # JSON 日志解析器
    [PARSER]
        Name        json
        Format      json
        Time_Key    time
        Time_Format %Y-%m-%dT%H:%M:%S.%L
    
    # 多行日志解析器(Java 堆栈跟踪)
    [PARSER]
        Name        multiline_java
        Format      regex
        Regex       ^(?<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) (?<level>[A-Z]+) (?<message>.*)$    
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluent-bit
  namespace: kube-system
  labels:
    k8s-app: fluent-bit
spec:
  selector:
    matchLabels:
      k8s-app: fluent-bit
  template:
    metadata:
      labels:
        k8s-app: fluent-bit
    spec:
      serviceAccountName: fluent-bit
      tolerations:
        - key: node-role.kubernetes.io/control-plane
          operator: Exists
          effect: NoSchedule
      containers:
        - name: fluent-bit
          image: fluent/fluent-bit:2.2.0
          imagePullPolicy: Always
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
          volumeMounts:
            - name: varlog
              mountPath: /var/log
            - name: varlibdockercontainers
              mountPath: /var/lib/docker/containers
              readOnly: true
            - name: config
              mountPath: /fluent-bit/etc/
            - name: positions-db
              mountPath: /var/log/
          ports:
            - containerPort: 2020
              name: metrics
              protocol: TCP
      volumes:
        - name: varlog
          hostPath:
            path: /var/log
        - name: varlibdockercontainers
          hostPath:
            path: /var/lib/docker/containers
        - name: config
          configMap:
            name: fluent-bit-config
        - name: positions-db
          hostPath:
            path: /var/log/fluent-bit/
            type: DirectoryOrCreate

关键配置详解

INPUT 段

参数说明生产建议
Path日志文件路径/var/log/containers/*.log
Parser日志格式解析器containerd 用 cri,Docker 用 docker
Refresh_Interval扫描新文件间隔5 秒
Mem_Buf_Limit内存缓冲区上限50MB,超出会暂停读取
Skip_Long_Lines跳过超长行On,防止 OOM
DB位置记录文件必须配,否则重启会重复采集
DB.SyncDB 同步策略Normal,兼顾性能和安全

最重要的参数是 DB。它记录了每个日志文件读取到了哪个位置(offset)。如果不配,Fluent Bit 重启后会从头开始读取所有日志,导致大量重复数据。我见过有人不配 DB,每次 Pod 重启后 ES 里日志翻倍。

FILTER 段:Kubernetes 过滤器会调用 K8s API 获取 Pod 的元数据(namespace、pod name、labels、annotations),附加到每条日志上。这就是为什么你在 Kibana 里能看到日志属于哪个 Pod、哪个 Deployment。

# 通过 annotation 控制日志采集行为
# 在 Pod 上加 annotation
apiVersion: v1
kind: Pod
metadata:
  name: my-app
  annotations:
    # Fluent Bit 解析器:指定日志格式
    fluentbit.io/parser: json
    # 排除该 Pod 的日志
    fluentbit.io/exclude: "true"

OUTPUT 段:输出目标配置。除了 Elasticsearch,还支持 Loki、Kafka、S3 等。

Fluent Bit 输出到 Loki

如果用 Grafana Loki 替代 Elasticsearch,OUTPUT 配置改为:

[OUTPUT]
    Name        loki
    Match       kube.*
    Host        loki.logging.svc
    Port        3100
    Labels      job=fluent-bit
    Label_keys  $kubernetes['namespace_name'], $kubernetes['pod_name']
    Line_Format json
    Tenant_ID   ""

Loki 的优势是轻量、存储成本低(只索引标签不索引全文),适合中小规模集群。ES 的优势是全文搜索能力强,适合大规模日志检索。两者选型后面详细讲。

Filebeat 实战配置

Filebeat 是 Elastic 官方的轻量级日志采集器,用 Go 编写,和 ELK 生态集成最好。

# filebeat-daemonset.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: filebeat-config
  namespace: kube-system
data:
  filebeat.yml: |
    filebeat.autodiscover:
      providers:
        - type: kubernetes
          node: ${NODE_NAME}
          hints:
            enabled: true
          # 自动发现 Pod 并附加元数据
          templates:
            - condition:
                contains:
                  kubernetes.labels.app: nginx
              config:
                - module: nginx
                  access:
                    input:
                      type: container
                      stream: stdout
    
    processors:
      - add_kubernetes_metadata:
          host: ${NODE_NAME}
          # 如果 Kubelet 不是标准端口,需要配置
          kube_client_config:
            kube_config: /etc/kubernetes/kubelet.conf
      - drop_event:
          when:
            # 丢弃 kube-system 命名空间的日志(可选)
            or:
              - contains:
                  kubernetes.namespace: kube-system
    
    output.elasticsearch:
      hosts: ["elasticsearch.logging.svc:9200"]
      index: "k8s-%{[kubernetes.namespace]}-%{+yyyy.MM.dd}"
    
    # 或者输出到 Kafka(高吞吐场景)
    # output.kafka:
    #   hosts: ["kafka-0.kafka.logging:9092"]
    #   topic: k8s-logs
    #   partition.round_robin:
    #     reachable_only: true
    
    logging.level: info
    monitoring.enabled: true
    monitoring.elasticsearch: true    
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: filebeat
  namespace: kube-system
spec:
  selector:
    matchLabels:
      k8s-app: filebeat
  template:
    metadata:
      labels:
        k8s-app: filebeat
    spec:
      serviceAccountName: filebeat
      containers:
        - name: filebeat
          image: docker.elastic.co/beats/filebeat:8.11.0
          env:
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 512Mi
          volumeMounts:
            - name: config
              mountPath: /usr/share/filebeat/filebeat.yml
              subPath: filebeat.yml
            - name: data
              mountPath: /usr/share/filebeat/data
            - name: varlog
              mountPath: /var/log
              readOnly: true
            - name: varlibdockercontainers
              mountPath: /var/lib/docker/containers
              readOnly: true
      volumes:
        - name: config
          configMap:
            name: filebeat-config
        - name: data
          hostPath:
            path: /var/lib/filebeat-data
            type: DirectoryOrCreate
        - name: varlog
          hostPath:
            path: /var/log
        - name: varlibdockercontainers
          hostPath:
            path: /var/lib/docker/containers

Fluent Bit vs Filebeat 选型

维度Fluent BitFilebeat
语言CGo
内存占用10-50MB50-150MB
CPU 开销中等
插件生态丰富(CNCF)丰富(Elastic 生态)
配置复杂度中等简单
多行日志处理支持但配置复杂原生支持
Kafka 输出支持原生支持更好
Loki 输出原生支持需额外配置
监控指标内置 Prometheus exporter内置
社区活跃度高(CNCF 毕业)高(Elastic 维护)

我的选型建议

  • 用 ELK 技术栈 → Filebeat,和 Elastic 生态集成最好,开箱即用
  • 用 Loki 技术栈 → Fluent Bit,原生支持最好
  • 资源极度受限(边缘节点) → Fluent Bit,内存占用更低
  • 需要复杂日志解析 → Fluent Bit + Fluentd 两层架构,Fluent Bit 采集,Fluentd 聚合处理

后端存储选型:Elasticsearch vs Loki

日志后端存储的选择直接影响运维成本和查询能力。

Elasticsearch

Elasticsearch(ES)是日志检索的事实标准,全文索引能力强,但资源消耗大。

适合场景

  • 日均日志量大(TB 级)
  • 需要全文检索、模糊查询
  • 需要复杂的聚合分析
  • 团队有 ES 运维经验

资源规划

集群规模Fluent BitES 节点配置ES 节点数日存储量
小型(20节点)0.5核/100MB4核/8GB3~5GB/天
中型(100节点)1核/200MB8核/16GB5~25GB/天
大型(300节点+)2核/500MB16核/32GB9+~75GB/天

ES 的 JVM 堆内存不应超过物理内存的 50%,且必须预留至少 2GB 给系统缓存(Linux page cache)。这个很多人不知道,堆设太大反而拖慢性能。

Grafana Loki

Loki 是 Grafana 团队做的日志系统,设计理念是"像 Prometheus 但存日志"。它只索引日志的标签(metadata),不索引日志内容本身,存储成本比 ES 低一个数量级。

适合场景

  • 日志量中等(GB 级到几十 GB/天)
  • 主要通过标签查询日志(namespace、pod、app)
  • 已有 Grafana 基础设施
  • 存储预算有限
# Loki 简化部署(单节点,测试用)
apiVersion: v1
kind: ConfigMap
metadata:
  name: loki-config
  namespace: logging
data:
  loki.yaml: |
    auth_enabled: false
    server:
      http_listen_port: 3100
    common:
      path_prefix: /loki
      storage:
        filesystem:
          chunks_directory: /loki/chunks
          rules_directory: /loki/rules
      replication_factor: 1
      ring:
        instance_addr: 0.0.0.0
        kvstore:
          store: inmemory
    schema_config:
      configs:
        - from: 2024-01-01
          store: tsdb
          object_store: filesystem
          schema: v13
          index:
            prefix: index_
            period: 24h
    limits_config:
      retention_period: 30d        # 日志保留 30 天
      max_query_series: 5000
      ingestion_rate_mb: 10        # 每秒写入限制
      ingestion_burst_size_mb: 20
    compactor:
      working_directory: /loki/compactor
      retention_enabled: true
      delete_request_store: filesystem    

ES vs Loki 对比

维度ElasticsearchLoki
索引方式全文索引仅标签索引
存储成本低(1/10 量级)
查询能力全文搜索、模糊匹配标签过滤 + LogQL
查询速度快(有索引)中等(需扫描)
资源消耗
运维复杂度
日志量上限TB/天GB/天
适合规模大规模中小规模

我的建议

  • 日志量 <50GB/天 → Loki,存储便宜,运维简单
  • 日志量 50-500GB/天 → 看查询需求,全文搜索用 ES,标签查询用 Loki
  • 日志量 >500GB/天 → ES + 冷热数据分离 + Kafka 缓冲

别被 Loki 的"低成本"忽悠了。Loki 适合通过标签查日志(比如查某个 namespace 的日志),但如果需要全文搜索(比如搜某个 error message 出现在哪些日志里),Loki 会扫描大量数据,速度比 ES 慢得多。两种场景需求不同,选型要按实际查询模式来。

生产环境踩坑指南

坑1:日志丢失——Pod 销毁后日志消失

容器被删除时,节点上的日志文件也会被清理。如果 Fluent Bit 还没来得及采集完,这部分日志就永久丢失了。

# 检查 Fluent Bit 的采集进度
# 查看位置记录 DB
sqlite3 /var/log/flb_kube.db "SELECT * FROM in_tail;"
# 对比文件大小和已读 offset

解决方案:

# 方案1:配置 PreStop hook,等待 Fluent Bit 采集完成
apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
    - name: app
      image: myapp:latest
      lifecycle:
        preStop:
          exec:
            command: ["sleep", "10"]  # 等 10 秒让 Fluent Bit 采集完
# 方案2:Fluent Bit 配置 Graceful Shutdown
# 在 DaemonSet 中配置 terminationGracePeriodSeconds
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: fluent-bit
          # 配置优雅退出,把缓冲区数据刷出
          env:
            - name: FLUENT_BIT_FLUSH_INTERVAL
              value: "1"

坑2:多行日志合并失败

Java 应用抛异常时,堆栈跟踪是多行的。如果不做多行合并,每行都会变成一条独立日志,在 Kibana 里看到的是支离破碎的堆栈。

Fluent Bit 多行配置:

# parsers.conf 中添加多行解析器
[PARSER]
    Name        multiline_java
    Format      regex
    Regex       ^(?<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (?<level>\w+) (?<message>.*)$

# fluent-bit.conf 中 INPUT 段使用多行
[INPUT]
    Name              tail
    Path              /var/log/containers/*.log
    Parser            cri
    Tag               kube.*
    multiline.parser  java_exception    # 内置 Java 多行解析器
    Refresh_Interval  5

Fluent Bit 2.0+ 内置了常用多行解析器:

# 内置多行解析器
multiline.parser: java_exception, python, go

Filebeat 多行配置更直观:

filebeat.inputs:
  - type: container
    paths:
      - /var/log/containers/*.log
    multiline:
      type: pattern
      pattern: '^\d{4}-\d{2}-\d{2}'  # 以日期开头的行视为新日志
      negate: true
      match: after
      flush_pattern: '^\d{4}-\d{2}-\d{2}'
      timeout: 5s

坑3:Fluent Bit 内存 OOM

日志洪峰时 Fluent Bit 内存飙升,被 OOM Kill。

# 限制内存并配置背压机制
[SERVICE]
    Flush         5
    Mem_Buf_Limit 50MB        # 内存缓冲区上限
    # 超过限制时暂停读取,等缓冲区数据刷出后继续

[INPUT]
    Name              tail
    Path              /var/log/containers/*.log
    Mem_Buf_Limit    10MB    # INPUT 级别缓冲限制
    Buffer_Chunk_Size 512KB  # 每个读取块大小
    Buffer_Max_Size   5MB    # 单文件最大缓冲
# K8s 资源限制要合理
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi    # 不要设太低,日志洪峰时容易 OOM

踩坑实录:有次大促期间日志量暴增 10 倍,Fluent Bit 内存限制设的 128Mi,直接 OOM。Pod 反复重启,导致大量日志丢失。后来把内存限制调到 512Mi,同时配了 Mem_Buf_Limit 做背压,才稳住。

坑4:日志格式不统一导致解析失败

不同应用日志格式不同,有的 JSON、有的纯文本、有的 key=value,统一解析很头疼。

# 通过 Pod annotation 指定解析器
apiVersion: v1
kind: Pod
metadata:
  name: json-app
  annotations:
    fluentbit.io/parser: json    # 使用 JSON 解析器
---
apiVersion: v1
kind: Pod
metadata:
  name: nginx-app
  annotations:
    fluentbit.io/parser: nginx   # 使用 Nginx 解析器
# 对应 parsers.conf
[PARSER]
    Name        json
    Format      json
    Time_Key    timestamp
    Time_Format %Y-%m-%dT%H:%M:%S.%LZ

[PARSER]
    Name        nginx
    Format      regex
    Regex       ^(?<remote>\S+) - (?<user>\S+) \[(?<time>[^\]]+)\] "(?<method>\S+) (?<path>\S+) (?<protocol>\S+)" (?<status>\d+) (?<size>\d+) "(?<referer>[^"]*)" "(?<agent>[^"]*)"$
    Time_Key    time
    Time_Format %d/%b/%Y:%H:%M:%S %z

坑5:K8s 日志轮转配置不当

节点上的容器日志如果不做轮转,磁盘会被撑满。

# 检查容器日志轮转配置
# containerd 配置文件 /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd]
  # 日志文件最大大小
  max_container_log_line_size = -1
  
# 或者通过 kubelet 配置
# /var/lib/kubelet/config.yaml
containerLogMaxSize: "100Mi"       # 单个日志文件最大 100MB
containerLogMaxFiles: 5             # 最多保留 5 个日志文件
# 检查节点日志磁盘占用
du -sh /var/log/containers/
du -sh /var/log/pods/

# 如果日志占满了磁盘,紧急清理
# 先找到最大的日志文件
find /var/log/containers/ -name "*.log" -size +500M -exec ls -lh {} \;
# 截断(不删除,避免文件句柄问题)
truncate -s 0 /var/log/containers/huge-app.log

坑6:Fluent Bit 采集位置 DB 丢失

DaemonSet Pod 被调度到其他节点时,本地的位置记录 DB 也会丢失。新 Pod 启动后会从头读取日志,导致重复。

# 方案1:把 DB 挂载到 hostPath
volumes:
  - name: positions-db
    hostPath:
      path: /var/lib/fluent-bit/
      type: DirectoryOrCreate
volumeMounts:
  - name: positions-db
    mountPath: /var/lib/fluent-bit/
# 方案2:配置中使用持久化路径
[INPUT]
    Name              tail
    Path              /var/log/containers/*.log
    DB                /var/lib/fluent-bit/flb_kube.db
    DB.Sync           Normal

日志监控与告警

日志采集系统本身也需要监控。Fluent Bit 内置了 Prometheus metrics exporter。

# Fluent Bit 内置 metrics 端口默认在 2020
curl http://localhost:2020/metrics

# 关键指标:
# fluentbit_input_records_total       → 采集的日志条数
# fluentbit_output_proc_records_total → 输出的日志条数
# fluentbit_output_errors_total       → 输出错误数
# fluentbit_input_bytes_total         → 采集的字节数

Prometheus 告警规则示例:

# fluent-bit-alerts.yaml
groups:
  - name: fluent-bit
    rules:
      - alert: FluentBitDown
        expr: up{job="fluent-bit"} == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Fluent Bit is down on {{ $labels.node }}"
          description: "Fluent Bit has been down for 2 minutes"
      
      - alert: FluentBitHighErrorRate
        expr: rate(fluentbit_output_errors_total[5m]) > 10
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Fluent Bit high error rate on {{ $labels.node }}"
          description: "Fluent Bit is experiencing high output error rate"
      
      - alert: FluentBitNoLogsCollected
        expr: rate(fluentbit_input_records_total[5m]) == 0
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "No logs collected by Fluent Bit on {{ $labels.node }}"
          description: "Fluent Bit hasn't collected any logs in the last 10 minutes"
      
      - alert: FluentBitInputOutputGap
        # 采集量和输出量差距过大说明有积压
        expr: |
          sum(rate(fluentbit_input_records_total[5m])) by (node) - 
          sum(rate(fluentbit_output_proc_records_total[5m])) by (node) > 100          
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Fluent Bit log backlog on {{ $labels.node }}"
          description: "Input rate significantly exceeds output rate, logs are backing up"

日志采集方案选型决策框架

根据集群规模和需求选择方案:

集群规模 < 20 节点?
├─ 是 → DaemonSet + Fluent Bit + Loki
│       低成本,运维简单,够用
├─ 否 → 日均日志量?
│   ├─ < 50GB/天 → DaemonSet + Fluent Bit + Loki
│   │              中等规模,Loki 存储成本低
│   │
│   ├─ 50-500GB/天 → DaemonSet + Fluent Bit + Kafka + ES
│   │                 Kafka 做缓冲,ES 做检索
│   │
│   └─ > 500GB/天 → DaemonSet + Fluent Bit + Kafka + ES(冷热分离)
│                    ES 用热温冷三层数据节点
└─ 有特殊日志格式需求?
    └─ Java 堆栈、多行日志 → Sidecar 模式或多行解析器

选型清单

  1. 集群节点数:______
  2. 日均日志量:______ GB
  3. 日志格式:stdout / 文件 / 混合
  4. 是否需要全文搜索:是 / 否
  5. 日志保留天数:______ 天
  6. 预算限制:______
  7. 是否已有 Grafana:是 / 否
  8. 是否已有 ES 集群:是 / 否

总结

K8s 日志采集的核心决策有三个:采集模式选什么、采集工具选什么、后端存储选什么。

几个实战经验:

  1. DaemonSet 模式覆盖 90% 的场景:除非有特殊需求,不要用 Sidecar。Sidecar 的资源开销和运维复杂度远大于它的好处。我见过一个团队给每个 Pod 都加 Sidecar,结果集群里一半的容器是日志采集器,资源浪费惊人

  2. 采集位置 DB 是生命线:Fluent Bit 的 DB 参数记录了读取位置,不配或丢失会导致重复采集或漏采。必须挂到 hostPath 持久化

  3. 多行日志要提前配好:Java 堆栈跟踪是多行日志的经典场景。上线前用测试数据验证多行解析是否正确,别等故障排查时才发现堆栈日志是散的

  4. 日志洪峰要做背压:大促、压测等场景日志量会暴增。配置 Mem_Buf_Limit 做背压,超出限制暂停读取而不是无限缓冲导致 OOM

  5. 日志采集系统自身也要监控:Fluent Bit 挂了、ES 磁盘满了、Loki 写入超限了,这些都要有告警。否则日志系统静默故障,你以为日志在采集其实已经断了几个小时

  6. 容器日志轮转要提前配好:kubelet 的 containerLogMaxSizecontainerLogMaxFiles 不配的话,磁盘可能被日志撑满导致节点 NotReady

日志采集看起来是个小活,但在 K8s 环境里涉及的组件和决策点不少。选对方案、配好参数、做好监控,才能在故障排查时快速定位问题。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. Filebeat vs Logstash vs Fluent Bit: 三大日志采集器深度对比与选型指南 — 腾讯云开发者社区,Filebeat/Logstash/Fluent Bit 架构对比与性能分析
  2. 别再纠结DaemonSet还是Sidecar了!手把手教你根据业务场景选对K8S日志采集方案 — CSDN,DaemonSet 与 Sidecar 模式的业务场景选型指南
  3. 从零搭建一套生产可用的K8S日志监控栈:EFK/ELK保姆级配置与避坑指南 — CSDN,EFK/ELK 生产环境部署配置与性能调优
  4. Fluentd/FluentBit 接入 — 腾讯云文档,Fluent Bit 部署方式与 Prometheus 监控接入
  5. 如何收集k8s集群日志? — 腾讯云开发者社区,Fluentd/Filebeat/Vector 日志采集方案对比