概述
线上炸了,你打开 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 的方式。
优点:零额外资源开销 缺点:实时性差、无法做复杂处理、大规模集群性能瓶颈
这个方案在实际生产中用得少,不做展开。
三种架构对比
| 维度 | DaemonSet | Sidecar | Agentless |
|---|---|---|---|
| 资源开销 | 低(每节点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.Sync | DB 同步策略 | 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 Bit | Filebeat |
|---|---|---|
| 语言 | C | Go |
| 内存占用 | 10-50MB | 50-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 Bit | ES 节点配置 | ES 节点数 | 日存储量 |
|---|---|---|---|---|
| 小型(20节点) | 0.5核/100MB | 4核/8GB | 3 | ~5GB/天 |
| 中型(100节点) | 1核/200MB | 8核/16GB | 5 | ~25GB/天 |
| 大型(300节点+) | 2核/500MB | 16核/32GB | 9+ | ~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 对比
| 维度 | Elasticsearch | Loki |
|---|---|---|
| 索引方式 | 全文索引 | 仅标签索引 |
| 存储成本 | 高 | 低(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 模式或多行解析器
选型清单:
- 集群节点数:______
- 日均日志量:______ GB
- 日志格式:stdout / 文件 / 混合
- 是否需要全文搜索:是 / 否
- 日志保留天数:______ 天
- 预算限制:______
- 是否已有 Grafana:是 / 否
- 是否已有 ES 集群:是 / 否
总结
K8s 日志采集的核心决策有三个:采集模式选什么、采集工具选什么、后端存储选什么。
几个实战经验:
DaemonSet 模式覆盖 90% 的场景:除非有特殊需求,不要用 Sidecar。Sidecar 的资源开销和运维复杂度远大于它的好处。我见过一个团队给每个 Pod 都加 Sidecar,结果集群里一半的容器是日志采集器,资源浪费惊人
采集位置 DB 是生命线:Fluent Bit 的 DB 参数记录了读取位置,不配或丢失会导致重复采集或漏采。必须挂到 hostPath 持久化
多行日志要提前配好:Java 堆栈跟踪是多行日志的经典场景。上线前用测试数据验证多行解析是否正确,别等故障排查时才发现堆栈日志是散的
日志洪峰要做背压:大促、压测等场景日志量会暴增。配置 Mem_Buf_Limit 做背压,超出限制暂停读取而不是无限缓冲导致 OOM
日志采集系统自身也要监控:Fluent Bit 挂了、ES 磁盘满了、Loki 写入超限了,这些都要有告警。否则日志系统静默故障,你以为日志在采集其实已经断了几个小时
容器日志轮转要提前配好:kubelet 的
containerLogMaxSize和containerLogMaxFiles不配的话,磁盘可能被日志撑满导致节点 NotReady
日志采集看起来是个小活,但在 K8s 环境里涉及的组件和决策点不少。选对方案、配好参数、做好监控,才能在故障排查时快速定位问题。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- Filebeat vs Logstash vs Fluent Bit: 三大日志采集器深度对比与选型指南 — 腾讯云开发者社区,Filebeat/Logstash/Fluent Bit 架构对比与性能分析
- 别再纠结DaemonSet还是Sidecar了!手把手教你根据业务场景选对K8S日志采集方案 — CSDN,DaemonSet 与 Sidecar 模式的业务场景选型指南
- 从零搭建一套生产可用的K8S日志监控栈:EFK/ELK保姆级配置与避坑指南 — CSDN,EFK/ELK 生产环境部署配置与性能调优
- Fluentd/FluentBit 接入 — 腾讯云文档,Fluent Bit 部署方式与 Prometheus 监控接入
- 如何收集k8s集群日志? — 腾讯云开发者社区,Fluentd/Filebeat/Vector 日志采集方案对比