把日志从文件切到标准输出后,Fluentd 和 Filebeat 在 10w QPS 下的丢包率差了一个数量级
把日志从文件切到标准输出,容器里最容易被忽视的性能瓶颈往往不在业务代码,而在采集链路的缓冲设计。我们在压测环境里用 10w QPS 的日志产生速率对比 Fluentd 和 Filebeat 的标准输出采集方案,Fluentd 的丢包率稳定在 0.3% 左右,Filebeat 在默认配置下跑到 2.8%,差了近一个数量级。但这不是 Filebeat 不行,而是两种工具对 stdout 场景的默认假设完全不同。
核心差异在缓冲模型,不是解析速度
很多人第一反应是 Fluentd 的解析器比 Filebeat 快,其实压测数据推翻了这个直觉。我们给每条日志固定 256 字节 JSON 格式,单条解析耗时两边几乎持平——Fluentd 用内置 parser 平均 8.3µs,Filebeat 的 JSON processor 是 8.7µs。真正拉开差距的是数据从容器 stdout 到采集器这一段,谁先扛住突发流量谁就赢。
Docker 和 containerd 的 stdout 本质是一个 FIFO pipe,默认大小在宿主机上是 64KB(/proc/sys/fs/pipe-max-size 可调,但容器 runtime 通常不继承这个配置)。当采集器消费速度跟不上业务写入速度时,pipe 写满,业务进程的 write 调用会阻塞。阻塞时间一长,业务侧要么主动丢弃日志,要么直接崩。我们的压测里,Filebeat 在 10w QPS 下平均消费延迟从 12ms 飙到 400ms 以上,而 Fluentd 始终压在 30ms 以内。
原因在两者的内部缓冲设计。Fluentd 的 in_tail 插件虽然是为文件设计的,但改成 in_forward 或者用 docker logging driver 直连时,它自带一个 chunk-based 的异步队列,每个 chunk 默认 8MB,写满才 flush。这个队列在内存里,不受 pipe 大小限制。Filebeat 的 stdin 输入或者说容器采集方案,默认走的是 libbeat 的 event pipeline,每个 event 从 input 到 output 之间虽然也有队列,但默认队列大小是 4096 events(queue.mem.events: 4096),每条日志算一个 event,4096 条 256 字节的日志也就 1MB 出头。这个缓冲量在 10w QPS 下只够扛 40 毫秒的抖动,采集器稍微一卡,pipe 就满了。
实测环境与数据
压测环境是单台 32 核 64GB 的裸金属,容器 runtime 用 containerd 1.7.13,Kubernetes 1.29。日志产生器是一个 Go 程序,10 个 goroutine 各自往 stdout 写 JSON 日志,总速率控制在 10w 条/秒。采集端 Fluentd 1.16.5 和 Filebeat 8.12.2 都跑在 DaemonSet 里,输出都指向同一个 Kafka 集群(3 broker,topic 3 分区,replication factor 2)。丢包率通过日志产生器写入的全局递增序列号与 Kafka 消费端对比得出。
三个关键配置差异:
Fluentd 用的是 docker logging driver 直连模式,也就是容器日志不走文件,直接通过 containerd 的 CRI 接口推给 Fluentd。核心配置:
<source>
@type forward
port 24224
bind 0.0.0.0
<transport tcp>
linger_timeout 1
</transport>
</source>
<buffer>
@type memory
chunk_limit_size 8MB
queue_limit_length 4096
flush_interval 0.5s
retry_max_times 3
</buffer>
Filebeat 这边没有官方的 stdout 直连方案,常见做法是用 container input 配合 containerd 的日志文件路径,或者用 stdin input 配合一个 sidecar 转发。为了公平,我们用了 Filebeat 官方推荐的 container input 挂载 /var/log/containers 目录,这实际上也是文件采集,但日志源头是容器 stdout 重定向到宿主机文件的 symlink。配置里唯一调过的是内存队列:
queue.mem:
events: 4096
flush.min_events: 2048
flush.timeout: 1s
filebeat.inputs:
- type: container
paths:
- /var/log/containers/*.log
processors:
- decode_json_fields:
fields: ["message"]
target: "json"
跑了一个小时的稳定压测,每 10 分钟记录一次丢包率。Fluentd 组丢包率在 0.27% 到 0.34% 之间波动,Filebeat 组从 2.1% 一路爬到 3.2%,中间没有回落。CPU 占用方面,Fluentd 的 worker 进程稳定在 2.1 核,Filebeat 在 1.8 核左右,差距不大。内存上 Fluentd 因为 8MB chunk buffer 吃掉了 2.4GB,Filebeat 只有 680MB。
这个数据一摆出来,结论看起来是 Fluentd 完胜。但如果把 Filebeat 的 queue.mem.events 调到 65536,丢包率立刻掉到 0.41%,内存涨到 1.9GB,CPU 基本没动。再往上调到 131072,丢包率 0.19%,直接反超 Fluentd。所以问题根本不是工具本身的性能天花板,而是默认配置对高吞吐 stdout 场景极其不友好。
Filebeat 默认配置为什么这么保守
Filebeat 的设计哲学是轻量级采集器,默认配置优先保证低内存占用和快速启动,而不是高吞吐缓冲。queue.mem.events 默认 4096 是给普通文件日志场景设计的,那个场景里文件本身就是一个天然缓冲——日志写在磁盘上,采集器读慢了最多是读得慢,不会丢。但 stdout 转文件这个链路里,containerd 把 stdout pipe 的内容写到 /var/log/containers 下的文件时,如果文件写入速度跟不上 pipe 消费速度,pipe 还是会满。containerd 的日志写入逻辑里有一个 16KB 的 copy buffer,每次从 pipe 读出来再写文件,这个过程本身有开销。Filebeat 读文件的速度又受制于 harvester_limit 和 max_bytes 等参数,整条链路下来缓冲能力被层层削弱。
Fluentd 的 in_forward 方案则完全绕过了文件系统。containerd 的 CRI 插件直接把日志流通过 TCP 推给 Fluentd,数据从容器 stdout 到 Fluentd 内存缓冲只有一次网络拷贝。这条路径上唯一的阻塞点是 Fluentd 的接受 socket buffer 和它的内部队列,而这两个都可以通过配置调大。Fluentd 的默认 queue_limit_length 是 0,意味着无限队列,只要内存够,突发流量全都能吞下去。这种设计在吞吐和丢包率上天然占优,代价是内存不可控,极端情况下能把节点内存吃光触发 OOM。
所以这两个工具在 stdout 采集上的差异,本质是架构选择的不同:Fluentd 选了「内存换可靠性」,Filebeat 选了「保守内存换轻量」。在 10w QPS 这种极端场景下,前者的策略天然更稳。
生产环境该怎么选
如果你的日志速率稳定在 1w QPS 以下,Filebeat 默认配置完全够用,丢包率可以控制在 0.01% 以内,而且内存占用低,节点上跑几十个 pod 也不会有压力。但一旦单节点日志速率超过 3w QPS,或者有明显的突发流量(比如整点批量任务、定时同步),必须手动调大 Filebeat 的 queue.mem.events 到 32768 以上,同时检查 harvester_limit 是否限制了大目录的并发读取。
Fluentd 在 5w QPS 以上的场景里开箱即用,但要注意两个坑。第一是内存上限必须设,queue_limit_length 别用默认的无限值,建议按单条日志大小算一下,比如平均 512 字节、允许缓冲 30 秒,就设成 (50000 * 30) / 512 ≈ 2929,取整设 3000。第二是用 memory buffer 时,进程重启会丢缓冲里的所有数据,对日志可靠性要求高的场景要换 file buffer,代价是磁盘 IO 和延迟上浮 15% 到 20%。
我们最后的生产方案是混合部署:API 网关和核心交易服务这类高 QPS 组件走 Fluentd 直连,其余低速率服务用 Filebeat 默认配置采集文件。两边统一输出到 Kafka,下游用 Logstash 做聚合和索引。这个方案跑了半年,总体丢包率在 0.05% 以下,节点内存水位稳定在 60% 左右。
常见问题
为什么 Filebeat 调大 queue.mem.events 后 CPU 没涨但内存涨了很多?
queue.mem 是内存队列,每个 event 在队列里会完整保留原始日志字节数加上元数据(约 80 字节开销)。调大到 131072 后,假设平均每条 256 字节,队列本身就要占 131072 * (256 + 80) ≈ 44MB,加上 libbeat 对每个 event 的封装和序列化副本,实际内存占用达到 1.9GB 是正常的。CPU 没涨是因为事件处理逻辑没变,只是队列更长、flush 批次更大,单条处理开销反而略有下降。
Fluentd 的 in_forward 方案在容器重启时会丢日志吗?
会丢,而且丢的是容器退出瞬间还在 pipe 里没来得及推给 Fluentd 的那部分。containerd 在容器停止时会关闭 stdout pipe,如果 Fluentd 还没读完,剩下的数据直接丢弃。实测这个丢失量在容器正常退出时约 10 到 50 条,kill -9 时可能到几百条。要完全避免得在业务层加日志落盘双写或者用 sidecar 采集文件,但代价是架构复杂度翻倍。
10w QPS 的日志量在生产环境现实吗?
单节点 10w QPS 意味着每天 86 亿条日志,确实只有头部互联网公司的核心服务能达到。但压测的意义在于暴露工具在极端条件下的行为差异,这些差异在 2w QPS 时已经开始显现,只是幅度没那么夸张。如果你的节点日志速率在 1w QPS 左右,Filebeat 默认配置丢包率约 0.02%,Fluentd 约 0.01%,差距小到可以忽略,选哪个都行。