为什么用 Kafka 做日志采集,而不是 RocketMQ?

44 阅读7分钟

我们通常说,Kafka 更适合大数据量、高吞吐的批量传输场景(比如日志采集、流式计算),RocketMQ 更适合低延迟、高可用、灵活度高的在线业务场景。

以日志采集为例,就是典型的高吞吐批量场景,每秒有大批量日志写入消息队列,同时日志数据可能会通过 Flink/Spark(流处理引擎)流向 Elasticsearch、Hive、InfluxDB 等存储,用于日志分析和监控报警。

简化过的日志采集流程


从吞吐能力来说,同等配置下(使用 SSD),假设消息体为 1KB 左右,Kafka 能达到单集群百万 TPS(Transactions Per Second)的吞吐,RocketMQ 大概在几十万 TPS

要了解两者的吞吐能力差异,首先要看两者的底层实现有何不同。

下面是 Kafka 的架构图,熟悉 Kafka 的朋友应该知道,Kafka 的消息存储在 Broker(服务端)上,且每个 Topic 的每个分区都是独立文件存储的,顺序 IO 写入。因此 Kafka 的并行写入能力很强,在一定范围内,分区越多,吞吐越高

Kafka 架构图

但分区也不能太多,虽然每个分区都是顺序 IO,但如果Topic + 分区过多,分区文件在磁盘上过于分散,顺序 IO 可能会退化为随机 IO。所以当 Topic 数量过多时(比如单集群 1000 以上),Kafka 就不太合适了。

RocketMQ 则采取了另一种思路,它面向的主要是在线业务场景,需要在高吞吐、低延迟、高可用、灵活性之间取得最佳平衡。

RocketMQ 架构图

和 Kafka 不同,在服务端,RocketMQ 把所有 Topic 的所有 Queue(Kafka 中的分区)都写入一个叫 CommitLog 的文件集,每个 CommitLog 文件默认最大 1GB,写满后创建一个新的继续写。

同时,RocketMQ 会异步构建 ConsumeQueue 索引,本质是一个文件,每个 Topic 的每个 Queue 对应一个 ConsumeQueue 文件。它记录消息在 CommitLog 中的物理偏移量、消息长度、Tag Hash 等信息,相比消息体本身,每条索引的体积非常小,仅 20 个字节。

在查询(消费)消息时,会先查询 ConsumeQueue 文件,然后定位到消息在 CommitLog 中的位置,再读取原始消息。

这样设计的好处是,写入性能非常稳定,无论 Topic 和 Queue 如何扩展,在磁盘上始终都是顺序写入 CommitLog,虽然 ConsumeQueue 过多也会造成一定的随机 IO,但 ConsumeQueue 文件比 CommitLog 小太多,影响也相对较小。这就很适合在线业务场景,单集群上的Topic 数量多。比如当时我们一个部门共用一个集群,上面有几千个 Topic。

另外,基于 CommitLog + ConsumeQueue 的二级结构,很容易实现延迟队列、重试队列、死信队列(本质是不同的 ConsumeQueue)、按 Tag 消费等功能,这也是 RocketMQ 的强大之处,可以满足各种业务场景,并保证高可用和充足的容错性。

从 Kafka 和 RocketMQ 两者的实现差异上,你可以了解到为什么 Kafka 的吞吐能力更强。在 Topic 分区不多(比如日志采集)的情况下,Kafka 是并行顺序写入多个分区文件,而 RocketMQ 是顺序写入一个 CommitLog 文件,且 RocketMQ 还需要构建 ConsumeQueue 索引,产生额外开销。因此从写入性能上来说,Kafka 要更优

不过在单条消息的消费延迟上,RocketMQ 更优优势,从统计数据来看,Kafka 从生产端到消费端的消息延迟 PCT99 在 5-10ms,而 RocketMQ 的 PCT99 在 1-5ms(实际表现视业务而定)。因为RocketMQ 的 ConsumeQueue 索引会全量加载到内存中,消费端查询消息快于 Kafka 的磁盘稀疏索引。

然而日志采集的场景恰恰是批量生产和消费的场景, 通常不需要按单条消费(通常批量导入其他存储再进行处理),也用不上 RocketMQ 的延迟队列、重试队列、死信队列、按 Tag 消费这些能力,在服务端构建 ConsumeQueue 索引对日志采集场景来说没啥用,反而会带来额外开销。

因此,写入吞吐能力强,满足日志写入量大的场景,是用 Kafka 做日志采集的第一个原因。


另外日志消息和业务消息(比如点赞、收藏、视频发布)不太一样,业务消息一旦产生,通常就要及时发送给 Broker,避免消息丢失,以及保证能够快速消费。

而日志允许一定的延迟,可以按批次处理,比如先在本地磁盘中缓存一个批次的日志(通过 Log Agent),一旦缓冲区超过一定大小,再批量写入 Kafka。这样能更好利用 Kafka 的批量压缩机制

Kafka 通过两个参数控制批量压缩:
batch.size:单个分区的批次缓存达到指定字节数(默认 16KB)
linger.ms:批次在缓存中等待的最大时间(默认 0ms)

当消息密集写入时,比如在 0ms 内写入了 32 KB,就会分两批发送,每批 16KB。
当消息稀疏写入时,比如在 100ms 内只写入了一条,就会直接发送。

日志消息有一个特点是相似度比较高,比如 access log,通常都是请求时间、请求路径、状态码等等,因此压缩比也会很高

另外 Kafka 的 Broker 通常不解压消息,而是直接将 Producer 发过来的压缩包存储在磁盘,并原样发给 Consumer。这意味着 从生产到存储再到消费,全链路都是压缩的,极大地节省了 Broker 的 CPU 和网络带宽。

LogAgent 在配置同步批次时,会结合 Kafka 的压缩配置参数,尽量做到对齐,这样可以减少 Kafka 的本地攒批时间,这样 LogAgent 发送给 Kafka 一个批次的日志,Kafka Producer 压缩(lz4/gzip/snappy)完后直接发送给 Broker。

LogAgent 攒批大小 ≈ Kafka Producer 批次大小
LogAgent 攒批超时 ≈ Kafka Producer 等待超时
LogAgent 关闭自身压缩 → 交由 Kafka 做批次级压缩

虽然 RocketMQ 也支持批量发送消息,但早期需要代码层面手动处理,不支持自动攒批(要保证在线业务消息的实时性),不过 5.x 的 SDK 好像已经支持了。

更重要的是,目前 RocketMQ 只支持单条消息的压缩,不支持批量压缩。

RocketMQ Go Client

RocketMQ Java Client

这个也可以理解,如果要支持批量压缩,因为需要构建 ConsumeQueue 索引,就需要在 Broker 端解压(拆包后构建单条索引),增加实现的复杂度和额外开销。

当然也有曲线救国的方法,就是先在本地逐条压缩单条消息,然后批量发送给 Broker,最后在消费端逐条解压。但这样的数据压缩率以及压缩和解压效率就远远不如批量压缩和批量解压了。

综合来看,Kafka 在批量压缩的场景下有比较好的压缩效果,非常适配日志采集这种对延迟要求不高且需要批量处理的场景。这就是用 Kafka 做日志采集的第二个原因。


最后一个原因其实不必多说,就是生态了。

像我们常说的 ELK(Elasticsearch、Logstash、Kibana),也就是日志收集、存储、分析三件套,通常还要带上 Kafka,形成类似 日志源 → Filebeat/Logstash(Log Agent)→ Kafka → Elasticsearch → Kibana 这样一条链路。

这些上下游对 Kafka 的支持都比较好,比如 Filebeat 提供官方的 output.kafka 模块,Logstash 提供了官方的 Kafka 输入/输出插件;Kafka Connect 的 ES 连接器能方便 Kafka 到 ES 的准实时同步;另外 Kafka 和 Flink/Spark 等流处理引擎的适配也比较好,两者都有官方维护的高性能 Kafka 连接器。