容器化改造之后,日志的形态发生了根本变化。传统虚拟机时代,一个应用的日志固定写在某个路径下,运维直接配置rsyslog或者crontab打包归档就行。而在Kubernetes环境里,Pod随时可能被调度到任意节点,重启之后旧的容器文件系统直接被清空,日志的写入位置变成了每个节点上/var/log/pods和/var/log/containers目录。如果还沿用老的思路去追日志文件,基本是追不上的。所以搭建一条容器化的日志采集管道,把分散在集群各处的日志统一收集、清洗、转发到后端存储,几乎是每个团队上生产之前必须完成的功课。

一、容器日志的三种采集模式,各自适合什么场景
先说结论:大多数团队应该优先选择节点级采集,也就是在每个节点上跑一个DaemonSet形式的采集器。Kubernetes官方文档把日志采集分成三种思路,分别是节点级代理、Sidecar容器和应用直推。这三种方式没有绝对的优劣,关键看应用规模、日志形态和团队能力。
节点级代理是最常见的方案。kubelet会把容器的stdout和stderr重定向到宿主机的/var/log/pods目录,并在/var/log/containers下建立软链接指向这些文件。采集器只需要挂载这两个目录,就能拿到节点上所有容器的日志,配合采集器内置的Kubernetes元数据插件,还能自动把Pod名称、命名空间、标签等信息附加到日志上。这种方式的优点是侵入性为零,应用完全不用改代码,新增应用也无需额外配置。缺点是它只能采集写到标准输出的日志,如果应用把日志写到容器内部的文件里,节点级采集就抓不到了。
Sidecar模式解决的就是文件日志的问题。在同一个Pod里再起一个容器,共享日志目录,由这个Sidecar容器负责读取文件并转发。代价是资源开销成倍增加,一个Pod变两个容器,集群规模大的时候这笔账很可观。应用直推则是让程序自己通过HTTP或者TCP协议把日志发到后端,省掉了中间环节,延迟最低,但应用和日志系统耦合在一起,后端故障可能反过来影响业务,一般只在对实时性要求极高的场景使用。
二、Fluentd与Filebeat,到底选哪个
确定了节点级采集的架构,接下来最纠结的就是采集器选型。社区里用得最多的两个是Fluentd(以及它的强化版Fluent Bit)和Filebeat,两者的设计哲学差异很大。
Fluentd是CNCF毕业项目,插件生态极其丰富,输入、过滤、输出三类插件加起来有上千个,几乎所有能想到的后端存储都有现成的输出插件。它的核心概念是Tag路由,日志进来后打上标签,再根据匹配规则走不同的处理链路,比如error级别的日志走告警管道,普通日志走存储管道,配置起来非常灵活。过滤能力也是Fluentd的强项,正则解析、JSON展开、字段增删、多行日志合并都能在采集端完成,大大减轻后端的解析压力。代价是Ruby实现的Fluentd内存占用偏高,一个节点跑下来常驻内存两三百MB很正常,不过Fluent Bit用C重写后把这个问题解决了,内存可以压到几十MB。
Filebeat是Elastic官方出品的采集器,走的是轻量可靠路线。它内置的Registrar机制会记录每个文件已经读取到的offset,持久化到磁盘的registry文件里,采集器重启后能从断点继续读,不丢日志也不重复,这个可靠性设计非常成熟。资源占用方面Filebeat表现优秀,正常负载下几十MB内存就够了。它的短板是过滤转换能力相对薄弱,复杂的多行合并、字段重构往往要依赖背后的Logstash来完成,架构上多了一层。如果你的后端本来就是ELK体系,Filebeat加Logstash的组合上手最快;如果后端多样,或者希望在采集端做重度处理,Fluentd或者Fluent Bit更合适。
| 对比维度 | Fluentd/Fluent Bit | Filebeat |
|---|---|---|
| 内存占用 | Fluentd较高,Fluent Bit很低 | 低 |
| 插件生态 | 极其丰富 | 够用,偏Elastic系 |
| 过滤能力 | 强,采集端可完成复杂处理 | 较弱,常需配合Logstash |
| 断点续传 | 支持,需正确配置 | 内置Registrar,成熟可靠 |
三、完整管道搭建:DaemonSet采集加Kafka缓冲加ES存储
下面给出一套经过生产验证的架构:Fluent Bit以DaemonSet方式采集节点日志,先发送到Kafka做缓冲削峰,再由Logstash消费并写入Elasticsearch。中间加Kafka是因为日志量高峰期采集速度可能超过ES的写入能力,没有缓冲层的话,要么日志在采集端堆积导致OOM,要么直接丢失。Kafka把生产者和消费者的速度差隔离开,整条管道的稳定性提升非常明显。
Fluent Bit的核心配置如下,重点是输入源、Kubernetes元数据和输出三段:
[SERVICE]
Flush 5
Daemon Off
Log_Level info
[INPUT]
Name tail
Path /var/log/containers/*.log
Tag kube.*
Refresh_Interval 10
Mem_Buf_Limit 50MB
# 跳过空的日志文件,避免无意义的轮询
Skip_Long_Lines On
[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Merge_Log On
# 附加Pod标签到日志字段,方便后续按业务检索
Labels On
Annotations Off
[OUTPUT]
Name kafka
Match kube.*
Brokers kafka-0.kafka-headless.logging:9092
Topics container-logs
这里有几个容易踩坑的点需要提醒。第一,一定要给INPUT配置Mem_Buf_Limit,并且启用storage.type filesystem,否则网络抖动时未被确认的日志全部堆在内存里,节点上的Fluent Bit会被OOM Killer干掉,丢的是缓冲区内全部日志。第二,Java应用的异常堆栈是多行日志,默认按行采集会把一个堆栈拆成几十条记录,检索时非常难受,需要配置Parser做多行合并,识别以空格或at开头的行,把它们拼接到上一条记录里。第三,/var/log/containers下的文件软链接指向/var/log/pods,挂载卷时两个路径都要挂,否则会报权限错误。
Logstash消费Kafka并写入ES的配置同样不复杂:
input {
kafka {
bootstrap_servers => "kafka-0.kafka-headless.logging:9092"
topics => ["container-logs"]
consumer_threads => 4
# 按照ES的分片数设置合适的线程数,避免rebalance频繁
decorate_events => "basic"
}
}
output {
elasticsearch {
hosts => ["http://es.logging.svc:9200"]
# 按天建索引,便于生命周期管理
index => "logs-%{+YYYY.MM.dd}"
}
}
索引按天切分是刻意的,配合ES的Index Lifecycle Management可以自动实现热温冷分层和过期删除,避免索引无限膨胀。消费端线程数建议设置为ES索引分片数的整数倍,能明显提升吞吐。
四、上线后必须关注的运维细节
管道跑起来只是第一步,长期稳定运行还需要关注几件事。首先是采集器自身的日志,建议单独输出到一个固定的检索路径,否则采集器出问题时你去查它的日志,而它的日志恰好就是它自己采的,形成死循环排查起来很痛苦。其次是监控指标,Fluent Bit和Filebeat都暴露了Prometheus格式的metrics,重点盯两个指标:丢弃或重试的记录数,以及输入输出的速率差,速率差持续为正说明消费端已经跟不上了,要及时扩容Kafka消费者或者优化ES写入。最后做好资源限制,给DaemonSet设置合理的requests和limits,避免采集器在日志高峰期抢占了业务容器的资源,本末倒置。
日志保留周期也要提前规划好。开发环境保留七天足够,生产环境按合规要求来,一般三十天到半年。与其把所有日志都塞进ES,不如把高频查询的热日志放ES,冷日志归档到对象存储,成本能降一个数量级。这条管道搭好之后,后续接入告警、审计、链路追踪都有现成的数据基础,值得在架构设计阶段就认真投入。