日采 TB 级日志,Kafka 零拷贝和 ES 直写到底差多少资源?我们跑了一周实测

去年底我们给一套老旧的日志采集链路动了一次“大手术”——把原来从 Filebeat 直写 Elasticsearch 的链路,改成了中间接一层 Kafka。原因不复杂:高峰期 ES 写入被间歇性打爆,协调节点 CPU 飙到 90%+,数据丢是没丢,但查询延迟从几十毫秒飘到秒级,团队已经忍了很久。

为了论证这个改动到底值不值,我们拿线上真实流量跑了一整周的对比压测。结论先放前面:日均 1.2TB 日志写入场景下,引入 Kafka 作为缓冲层后,ES 集群的 CPU 使用率下降约 40 个百分点,写入延迟 P99 从 2.3 秒收敛到 180ms,而 Kafka 本身的资源增量成本不到 ES 集群的 15%。 零拷贝不是噱头,在吞吐和 CPU 开销上的优势非常硬。

下面我把测试环境、数据和几个关键发现拆开讲。

测试环境与流量模型

三台物理机组成 ES 集群,版本 7.17.3,每台 32 核 Xeon Gold 5218、128GB 内存、4 块 NVMe SSD 组 RAID0。data node 兼 coordinating node,堆内存统一给 31GB。这个配置不算奢侈,是我们生产环境的真实规格。

日志来源是 200+ 个微服务实例的 stdout/stderr 输出,由 Filebeat 采集。日志以 JSON 结构化数据为主,单条平均 1.2KB,字段数量平均 38 个,其中 12 个做了 keyword 索引,其余 text 字段走标准分词。日均写入量稳定在 1.2TB 左右,峰值时段(晚 8 点到 11 点)写入速率约 28GB/小时。

Kafka 集群用 3 台物理机独立部署,版本 3.4.0,每台 16 核、64GB 内存、6 块 HDD 做 JBOD。Topic 分 12 个 partition,副本因子 3,min.insync.replicas=2。Producer 端 Filebeat 的 compression 开 gzip,batch_size 设 2048,linger.ms 给 50ms。Consumer 端 Logstash 7.17.3 消费后批量写 ES,pipeline.batch.size 设 500,pipeline.batch.delay 设 50ms。

直写方案则简单粗暴:Filebeat 直接连 ES 的 /bulk`` 端点,bulk_max_size 设 2000,worker 开 4 个。两条链路用的 ES 集群是同一套,分两轮测试,中间给足够的时间让 segment 合并、缓存冷却。

零拷贝的收益,不在 Kafka 自身,而在“替 ES 挡刀”

很多人以为零拷贝是让 Kafka 自己跑得更快。这话对一半。Kafka 用 sendfile 系统调用把磁盘上的日志段数据直接推到 Socket Buffer,不走用户态,零拷贝确实让 Kafka 的 CPU 开销压得很低——我们实测 3 节点的 Kafka 集群,在持续承接 28GB/小时写入并同时被消费的情况下,单节点 CPU 使用率稳定在 18%-22%,网络吞吐约 3.2Gbps,磁盘写入 IO 大约 420MB/s。对于 16 核的机器来说,这个负载相当轻。

但真正的大头在 ES 那一侧。

直写方案下,Filebeat 的 bulk 请求直接怼到 ES 的 /_bulk 接口。ES 做 indexing 时要做很多事情:JSON 解析、字段映射、分词、倒排索引构建、doc values 生成,然后定期 refresh 和 flush。这还没完,segment 合并时的 CPU 和 IO 竞争才是最要命的。我们抓了一周的数据,直写方案下 ES 三节点的 CPU 使用率均值在 72%,峰值时段(segment merge 密集时)会摸到 94%,并且伴有大量 EsRejectedExecutionException。写入延迟的 P50 看着还行,280ms,但 P99 直接飙到 2.3 秒,毛刺非常严重。

切到 Kafka 方案后,ES 的 CPU 使用率均值降到 31%,峰值不超过 55%。写入延迟 P50 下降到 95ms,P99 稳定在 180ms 上下。差别在哪?Kafka 充当了一个巨大的蓄水池,把前端 burst 的写入压力削峰填谷了。Logstash 从 Kafka 消费时可以按自己最舒服的节奏拉取数据,然后以恒定的 batch size 往 ES 里写,ES 的 indexing 压力变得非常均匀,merge 线程很少再被 bulk 请求抢 CPU,rejected 异常从监控里消失了。

这里有一个很容易被忽略的细节:ES 的写入性能不是线性的。 当 bulk 请求大小和并发超过某个阈值后,内部队列开始堆积,refresh/flush/merge 的触发频率失控,性能会断崖式下跌。直写方案下,Filebeat 的 4 个 worker 每个都拼命发 bulk,看似并发不大,但日志流量本身是脉冲式的,一旦某个窗口内 bulk 请求叠加了 merge 操作,CPU 就瞬间打满。Kafka 的解耦让 ES 避开了这个“共振点”。

资源账:Kafka 多吃的那点磁盘和内存,换回了什么

有人会问:加一层 Kafka,不也要吃机器吗?算总账到底划不划算?

我们把两种方案下的资源消耗拉了一张表,取一周的日均值:

资源项 直写 ES Kafka + ES 增量
ES 集群 CPU 均值 72% (3×32核) 31% -41pp
ES 集群内存使用 98GB (堆+OS cache) 84GB -14GB
ES 磁盘 IO 写吞吐 610MB/s 370MB/s -240MB/s
Kafka CPU 均值 20% (3×16核) +20pp
Kafka 内存使用 38GB +38GB
Kafka 磁盘写入 420MB/s +420MB/s
链路总写入延迟 P99 2.3s 180ms -2.12s

把 CPU 换算成核心数:直写方案下 ES 吃掉了约 69 个物理核,Kafka 方案下 ES 吃 30 个核,Kafka 吃 10 个核,合计 40 个核,净省 29 个核。这 29 个核折算成机器,差不多是少用一台 ES 节点。内存方面,Kafka 多占的 38GB 主要来自 page cache(Kafka 重度依赖 OS cache 做读加速),而 ES 那边因为写入压力降低,堆内存和 OS cache 的占用都降了,总内存占用反而少了几个 GB。

磁盘是唯一“变差”的指标——Kafka 的 3 副本意味着同样一份数据要被写 3 次。但 Kafka 用 HDD 就够了,ES 为了 indexing 和查询性能必须上 SSD。HDD 每 TB 的成本大约是 NVMe SSD 的 1/5 到 1/6。以日均 1.2TB、保留 7 天的数据量来算,Kafka 需要的有效存储约 8.4TB,3 副本后 25TB,全用 HDD 成本不到 1 万块。ES 那边,直写方案下为了扛住写入压力,必须保持足够多的 SSD 空间来避免 merge 时磁盘被打满,实际存储冗余度比 Kafka 更高。所以磁盘成本的增加,在这个体量下几乎可以忽略。

一个容易被误读的点:零拷贝省的不是 Kafka 的 CPU,是 ES 的命

回到标题里的“零拷贝”。Kafka 的零拷贝确实让它在做高吞吐数据转发时 CPU 效率极高,但在这个场景里,它真正的价值是让 ES 不需要再做“缓冲”这件事

很多人尝试过在 ES 前面加 Redis 或者 RabbitMQ 做缓冲,也能起到削峰作用,但 Redis 的内存成本和 RabbitMQ 的吞吐上限在 TB 级日志场景下都不太能打。Kafka 的零拷贝 + 顺序读写 + page cache 这一套组合,让它能用很廉价的硬件扛住极高的写入吞吐,这才是它在日志管道里不可替代的地方。

实测过程中还有一个有意思的发现:Kafka producer 端开 gzip 压缩后,网络吞吐从 4.8Gbps 降到 3.2Gbps,但 Kafka broker 的 CPU 使用率只涨了不到 3 个百分点。解压缩的 CPU 开销被分摊到了 consumer 端的 Logstash 上,而 Logstash 本来就设置了多个 pipeline worker,多出来的解压开销几乎没影响吞吐。这相当于用极少的 CPU 换 30%+ 的网络带宽节省,在跨机房或带宽受限的场景下非常划算。

生产落地的一些经验

测试跑完后我们就切了生产,到现在稳定运行了快 4 个月。踩过的几个坑值得提一下:

Filebeat 的 kafka output 默认 compression 是 none。 一定要开 gzip,否则网络带宽和 Kafka 磁盘空间都会多出不少。batch_size 别设太小,2048 是个比较均衡的值,配合 50ms 的 linger.ms,既不会引入明显延迟,又能把请求合并得足够大。

Kafka topic 的 partition 数量要跟 consumer 并发度匹配。 我们用 Logstash 消费,pipeline.workers 设 12,topic 就分 12 个 partition,保证每个 worker 独占一个 partition 消费,避免线程切换和 offset 管理的额外开销。

ES 侧的 refresh_interval 可以适当拉长。 有 Kafka 做缓冲后,数据从产生到可查询的延迟本来就会多个几百毫秒,不如把 refresh_interval 从默认的 1s 改成 5s 甚至 10s,减少 segment 频繁生成带来的写放大。查询时效性要求高的场景另说。

监控 Kafka 的 consumer lag。 这是整个链路里最重要的一个指标。我们设了 50 万的 lag 告警阈值,正常运行时 lag 在 2 万到 8 万之间波动,峰值没超过 20 万。一旦 lag 持续上涨,要么是 ES 写入慢了,要么是 Logstash 处理能力跟不上,可以动态调大 pipeline.workers 或 ES 的 bulk 线程池。


常见问题

Kafka 零拷贝到底省了什么?跟 ES 直写比,省的是谁的资源?

零拷贝省的是 Kafka 自身的 CPU——数据从磁盘到网卡不经过用户态内存拷贝,broker 的 CPU 主要花在网络协议处理和少量校验上。但在这个场景里更大的收益是:Kafka 替 ES 扛住了写入流量的脉冲,让 ES 的 CPU 不用频繁处理 segment merge 与 bulk 写入的竞争,ES 的 CPU 使用率从 72% 降到 31%,这才是真正的资源大头。

日均 TB 级日志,用 Kafka 会不会把磁盘打爆?要上 SSD 吗?

不用。Kafka 是顺序读写,HDD 完全够用。我们日均 1.2TB、保留 7 天、3 副本,实际磁盘占用约 25TB,6 块 6TB HDD 做 JBOD 就能搞定,磁盘成本很低。如果日志保留时间更长,加盘就行,每 TB 成本比 ES 用的 SSD 便宜得多。

加了 Kafka 之后,日志从产生到 ES 可查,延迟多了多少?

我们实测 Filebeat → Kafka → Logstash → ES 这条链路,在 linger.ms=50ms 和 Logstash batch.delay=50ms 的配置下,端到端延迟的 P99 大约 3 秒。直写方案 P99 虽然看起来是 2.3 秒,但毛刺严重,实际体验反而更差。如果你对实时性要求极高(秒级以内),需要把两端的 batch delay 调小,代价是吞吐会降一些。