本文最初发布于 Confluent 官方博客,经授权由 InfoQ 中文站翻译并分享。
Apache Kafka 是最流行的事件流处理系统之一。在这个领域中有许多比较系统的方法,但是每个人都关心的一件事是性能。Kafka 的快众所周知,但现如今它有多快,与其他系统相比又如何?我们决定在最新的云硬件上测试下 Kafka 的性能。
为了进行比较,我们选择了一个传统的消息代理RabbitMQ和一个基于Apache BookKeeper的消息代理Apache Pulsar。我们主要关注(1)系统吞吐量和(2)系统延迟,因为它们是生产中事件流处理系统的主要性能指标。具体来说,吞吐量测试测量每个系统在硬件(特别是磁盘和 CPU)使用方面的效率。延迟测试测量每个系统传递实时消息的差别,包括 99.9 百分位尾延迟,这是实时任务关键型应用程序以及微服务架构的核心要求。
我们发现,Kafka 提供了最好的吞吐量,同时提供了最低的端到端延迟(99.9 百分位)。在吞吐量比较低时,RabbitMQ 传递消息的延迟非常低。
*当吞吐量高于 30MB/s 时,RabbitMQ 的延迟会显著降低。此外,当吞吐量较高时,镜像影响显著,而更低的延迟则可以通过只使用经典队列而不使用镜像来实现。
本文首先介绍了我们使用的基准测试框架,然后介绍了测试平台和工作负载。最后将使用不同的系统和应用程序指标对结果进行解释。所有这些都是开源的,所以感兴趣的读者可以自己重新生成结果,或者更深入地挖掘收集到的 Prometheus 指标。与大多数基准测试一样,我们比较特定工作负载下的性能。我们总是鼓励读者使用自己的工作负载/设置进行比较,以理解这些工作负载/设置如何转换为生产部署。要想更深入地了解特性、架构、生态系统等,请阅读这个完整的 Kafka、Pulsar 和 RabbitMQ对比指南。
概述
背景
分布式系统的持久性
基准测试框架
OMB 框架修复
OMB Kafka 驱动程序修复
OMB RabbitMQ 驱动程序修复
OMB Pulsar 驱动程序修复
测试平台
磁盘
OS 调优
内存
吞吐量测试
fsync 的效果
测试设置
吞吐量结果
延迟测试
延迟结果
延迟权衡
小结
背景
首先,让我们简要地讨论下每个系统,以了解它们的高级设计和架构,看下每个系统所做的权衡。
Kafka 是一个开源的分布式事件流处理平台,也是 Apache 软件基金会下五个最活跃的项目之一。在其核心,Kafka 被设计成一个多副本的分布式持久化提交日志,用于支撑事件驱动的微服务或大规模流处理应用程序。客户端向代理集群提供事件或使用代理集群的事件,而代理会向底层文件系统写入或从底层文件系统读取事件,并自动在集群中同步或异步地复制事件,以实现容错性和高可用性。
Pulsar 是一个开源的分布式发布/订阅消息系统,最初是服务于队列用例的。最近,它又增加了事件流处理功能。Pulsar 被设计为一个(几乎)无状态代理实例层,它连接到单独的 BookKeeper 实例层,由它实际地读取/写入消息,也可以选择持久地存储/复制消息。Pulsar 并不是唯一的同类系统,还有其他类似的消息传递系统,如 Apache DistributedLog 和 Pravega,它们都是在 BookKeeper 之上构建的,也是旨在提供一些类似 Kafka 的事件流处理功能。
BookKeeper 是一个开源的分布式存储服务,最初是为 Apache Hadoop 的 NameNode 而设计的预写日志。它跨服务器实例 bookies,在 ledgers 中提供消息的持久存储。为了提供可恢复性,每个 bookie 都会同步地将每条消息写入本地日志,然后异步地写入其本地索引 ledger 存储。与 Kafka 代理不同,bookie 之间不进行通信,BookKeeper 客户端使用 quorum 风格的协议在 bookie 之间复制消息。
RabbitMQ 是一个开源的传统消息中间件,它实现了 AMQP 消息标准,满足了低延迟队列用例的需求。RabbitMQ 包含一组代理进程,它们托管着发布消息的“交换器”,以及从中消费消息的队列。可用性和持久性是其提供的各种队列类型的属性。经典队列提供的可用性保证最少。经典镜像队列将消息复制到其他代理并提高可用性。最近引入的仲裁队列提供了更强的持久性,但是以性能为代价。由于这是一篇面向性能的博文,所以我们将评估限制在经典队列和镜像队列。
分布式系统的持久性
单节点存储系统(例如 RDBMS)依靠 fsync 写磁盘来确保最大的持久性。但在分布式系统中,持久性通常来自复制,即数据的多个副本独立失效。数据 fsync 只是在发生故障时减少故障影响的一种方法(例如,更频繁地同步可能缩短恢复时间)。相反,如果有足够多的副本失败,那么无论是否使用 fsync,分布式系统都可能无法使用。因此,我们是否使用 fsync 只是这样一个问题,即每个系统选择基于什么方式来实现其复制设计。有些系统非常依赖于从不丢失写入到磁盘的数据,每次写入时都需要 fsync,但其他一些则是在其设计中处理这种情况。
Kafka 的复制协议经过精心设计,可以确保一致性和持久性,而无需通过跟踪什么已 fsync 到磁盘什么未 fsync 到磁盘来实现同步 fsync。Kafka 假设更少,可以处理更大范围的故障,比如文件系统级的损坏或意外的磁盘移除,并且不会想当然地认为尚不知道是否已 fsync 的数据是正确的。Kafka 还能够利用操作系统批量写入磁盘,以获得更好的性能。
我们还不能十分确定,BookKeeper 是否在不 fsync 每个写操作的情况下提供了相同的一致性保证——特别是在没有同步磁盘持久化的情况下,它是否可以依赖复制来实现容错。关于底层复制算法的文档或文章中没有提及这一点。基于我们的观察,以及 BookKeeper 实现了一个分组 fsync 算法的事实,我们相信,它确实依赖于 fsync 每个写操作来确保其正确性,但是,社区中可能有人比我们更清楚我们的结论是否正确,我们希望可以从他们那里获得反馈。
无论如何,由于这可能是一个有争议的话题,所以我们分别给出了这两种情况下的结果,以确保我们的测试尽可能的公平和完整,尽管运行带有同步 fsync 功能的 Kafka 极其罕见,也是不必要的。
基准测试框架
对于任何基准测试,人们都想知道使用的是什么框架以及它是否公平。为此,我们希望使用OpenMessaging Benchmark Framework(OMB),该框架很大一部分最初是由 Pulsar 贡献者编写的。OMB 是一个很好的起点,它有基本的工作负载规范、测试结果指标收集/报告,它支持我们选择的三种消息系统,它还有针对每个系统定制的模块化云部署工作流。但是需要注意,Kafka 和 RabbitMQ 实现确实存在一些显著的缺陷,这些缺陷影响了这些测试的公平性和可再现性。最终的基准测试代码,包括下面将要详细介绍的修复程序,都是开源的。
OMB 框架修复
我们升级到 Java 11 和 Kafka 2.6、RabbitMQ 3.8.5 和 Pulsar 2.6(撰写本文时的最新版本)。借助 Grafana/Prometheus 监控栈,我们显著增强了跨这三个系统的监控能力,让我们可以捕获跨消息系统、JVM、Linux、磁盘、CPU 和网络的指标。这很关键,让我们既能报告结果,又能解释结果。我们增加了只针对生产者的测试和只针对消费者的测试,并支持生成/消耗积压,同时修复了当主题数量小于生产者数量时生产者速率计算的一个重要 Bug。
OMB Kafka 驱动程序修复
我们修复了 Kafka 驱动程序中一个严重的 Bug,这个 Bug 让 Kafka 生产者无法获得 TCP 连接,存在每个工作者实例一个连接的瓶颈。与其他系统相比,这个补丁使得 Kafka 的数值更公平——也就是说,现在所有的系统都使用相同数量的 TCP 连接来与各自的代理通信。我们还修复了 Kafka 基准消费者驱动程序中的一个关键 Bug,即偏移量提交的过于频繁及同步导致性能下降,而其他系统是异步执行的。我们还优化了 Kafka 消费者的 fetch-size 和复制线程,以消除在高吞吐量下获取消息的瓶颈,并配置了与其他系统相当的代理。
OMB RabbitMQ 驱动程序修复
我们增强了 RabbitMQ 以使用路由键和可配置的交换类型(DIRECT
交换和TOPIC
交换),还修复了 RabbitMQ 集群设置部署工作流中的一个 Bug。路由键被引入用来模仿主题分区的概念,实现与 Kafka 和 Pulsar 相当的设置。我们为 RabbitMQ 部署添加了一个 TimeSync 工作流,以同步客户端实例之间的时间,从而精确地测量端到端延迟。此外,我们还修复了 RabbitMQ 驱动程序中的另一个 Bug,以确保可以准确地测量端到端延迟。
OMB Pulsar 驱动程序修复
对于 OMB Pulsar 驱动程序,我们添加了为 Pulsar 生产者指定最大批次大小的功能,并关闭了那些在较高目标速率下、可能人为地限制跨分区生产者队列吞吐量的全局限制。我们不需要对 Pulsar 基准驱动程序做任何其他重大的更改。
测试平台
OMB 包含基准测试的测试平台定义(实例类型和 JVM 配置)和工作负载驱动程序配置(生产者/消费者配置和服务器端配置),我们将其用作测试的基础。所有测试都部署了四个驱动工作负载的工作者实例,三个代理/服务器实例,一个监视实例,以及一个可选的、供 Kafka 和 Pulsar 使用的三实例 Apache ZooKeeper 集群。在实验了几种实例类型之后,我们选定了网络/存储经过优化的 Amazon EC2 实例,它具有足够的 CPU 内核和网络带宽来支持磁盘 I/O 密集型工作负载。在本文接下来的部分,我们会列出我们在不同的测试中对这些基线配置所做的更改。
磁盘
具体来说,我们选择了i3en.2xlarge
(8 vCore,64GB RAM,2x 2500 GB NVMe SSD
),我们看中了它高达 25 Gbps 的网络传输限额,可以确保测试设置不受网络限制。这意味着这些测试可以测出相应服务器的最大性能指标,而不仅仅是网速多快。i3en.2xlarge
实例在两块磁盘上支持高达 约 655 MB/s 的写吞吐量,这给服务器带来了很大的压力。有关详细信息,请参阅完整的实例类型定义。根据一般建议和最初的 OMB 设置,Pulsar 把一个磁盘用于 journal,另一个用于 ledger 存储。Kafka 和 RabbitMQ 的磁盘设置没有变化。
图 1:确定跨两块磁盘的i3en.2xlarge
实例的最大磁盘带宽,使用 Linux 命令 dd 进行测试,作为吞吐量测试的参考。
OS 调优
此外,对于所比较的三个系统,为了获得更好的延迟性能,我们使用tune -adm的延迟性能配置文件对操作系统进行了调优,它会禁用磁盘和网络调度器的任何动态调优机制,并使用性能调控器进行 CPU 频率调优。它将每个内核的 p-state 固定在可能的最高频率上,并将 I/O 调度器设置为 deadline,从而提供一个可预测的磁盘请求延迟上限。最后,它还优化内核中的电源管理服务质量(QoS),这是为了提高性能,而不是省电。
内存
与 OMB 中的默认实例相比,i3en.2xlarge
测试实例物理内存几乎是前者的一半(64 GB vs. 122 GB)。优化 Kafka 和 RabbitMQ 使其与测试实例兼容非常简单。两者都主要依赖于操作系统的页面缓存,随着新实例的出现,页面缓存会自动缩小。
然而,Pulsar 代理以及 BookKeeper bookie 都依赖于堆外/直接内存缓存,为了使这两个独立进程可以在i3en.2xlarge
实例上良好地运行,我们调整了 JVM 堆/最大直接内存大小。具体来说,我们将堆大小从每个 24 GB(原始的 OMB 配置)减半为每个 12 GB,在两个进程和操作系统之间按比例划分了可用物理内存。
在测试中,当目标吞吐量比较高时,我们遇到了java.lang.OutOfMemoryError: Direct buffer memory
错误,如果堆大小再低一点,就会导致 bookie 完全崩溃。这是使用堆外内存的系统所面临的典型的内存调优问题。虽然直接字节缓冲区是避免 Java GC 的一个有吸引力的选项,但是大规模使用是一个颇具挑战性的做法。
吞吐量测试
我们开始测量的第一件事是,在网络、磁盘、CPU 和内存资源数量相同的情况下,每个系统可以实现的峰值稳定吞吐量。我们将稳定峰值吞吐量定义为消费者在不增加积压的情况下可以跟得上的最高平均生产者吞吐量。
fsync 的效果
如前所述,Apache Kafka 的默认建议配置是使用底层操作系统指定的页面缓存刷新策略(而不是同步地 fsync 每个消息)flush/fsync 到磁盘,并依赖复制来实现持久性。从根本上说,这提供了一种简单而有效的方法来分摊 Kafka 生产者所使用的不同批次大小的成本,在各种情况下都可以实现最大可能的吞吐量。如果 Kafka 被配置为每次写时 fsync,那么我们就会因强制进行 fsync 系统调用而人为地妨碍了性能,并且没有获得任何额外的好处。
也就是说,考虑到我们将要讨论这两种情况的结果,我们仍然有必要了解在 Kafka 中每次写时 fsync 的影响。各种生产者批次大小对 Kafka 吞吐量的影响如下所示。吞吐量随着批次大小的增加而增加,直到到达“最佳点”,即批次大小足以让底层磁盘完全饱和。在批次大小较大时,将 Kafka 上的每条消息 fsync 到磁盘(图 2 中的橙色条)可以产生类似的结果。注意,这些结果仅在所述实验平台的 SSD 上得到了验证。Kafka 确实在所有批次大小上都充分利用了底层磁盘,在批次大小较小时最大化 IOPS,在批次大小较大时最大化磁盘吞吐量,甚至在强制 fsync 每条消息时也是如此。
图 2:批次大小对 Kafka 吞吐量(每秒消息数)的影响,绿条表示fsync=off(默认),橙条表示fsync每条消息
从上图可以明显看出,使用默认的 fsync 设置(绿条)可以让 Kafka 代理更好地管理 page flush,从而提供更好的总体吞吐量。特别是,在生产者批次大小较小(1 KB 和 10 KB)时,使用默认同步设置的吞吐量比 fsync 每条消息的吞吐量高 3 到 5 倍。然而,批次较大(100 KB 和 1 MB)时,fsync 的成本被均摊了,吞吐量与默认 fsync 设置相当。
Pulsar 在生产者上实现了类似的批次,并在 bookie 间对产生的消息进行 quoro 风格的复制。BookKeeper bookie 在应用程序级实现分组提交/同步到磁盘,以最大化磁盘吞吐量。在默认情况下(由 bookie 配置journalSyncData=true
控制),BookKeeper 会将写入 fsync 到磁盘。
为了覆盖所有的情况,我们测试 Pulsar 时在 BookKeeper 上设置了journalSyncData=false
,并与 Kafka 的默认(建议)设置(不对每条消息进行 fsync)进行了比较。但是,我们在 BookKeeper bookie 上遇到了大量延迟和不稳定性,表明存在与 flush 相关的队列等待。我们还用 Pulsar 提供的工具pulsar-perf
验证到了同样的行为。据我们所知,在咨询了 Pulsar 社区后,这似乎是一个 Bug,所以我们选择从我们的测试中排除它。尽管如此,考虑到我们可以看到磁盘在journalSyncData=true
时吞吐量达到最大,我们相信它无论如何都不会影响最终结果。
图 3:Pulsar 在 BookKeeper 设置了journalSyncData=true
时,吞吐量明显下降,并且出现了延迟峰值
图 4:BookKeeper journal 回调队列在journalSyncData=false
设置下的增长情况
当且仅当消息尚未被消费时,RabbitMQ 会使用一个持久队列将消息持久化到磁盘。然而,与 Kafka 和 Pulsar 不同,RabbitMQ 不支持“回放”队列来再次读取较旧的消息。从持久性的角度来看,在我们的基准测试中,消费者与生产者保持同步,因此,我们没有注意到任何写入磁盘的操作。我们还在一个三代理集群中使用了镜像队列,使 RabbitMQ 提供与 Kafka 和 Pulsar 相同的可用性保证。
测试设置
本实验按照以下原则和预期保证进行设计:
为了实现容错,消息复制 3 份(具体配置见下文);
为了优化吞吐量,我们启用了所有三个系统的批处理。我们的批处理是 1MB 数据最多 10 毫秒;
为 Pulsar 和 Kafka 的一个主题配置了 100 个分区;
RabbitMQ 不支持主题分区。为了匹配 Kafka 和 Pulsar 的设置,我们声明了一个 direct exchange(相当于主题)和链接队列(相当于分区)。关于这个设置的更多细节见下文。
OMB 使用一个自动速率发现算法。该算法通过以多个速率探测积压来动态地获取目标生产者的吞吐量。在许多情况下,我们看到速率在每秒 2 条消息到每秒 50 万条消息之间剧烈波动。这严重影响了实验的可重复性和准确性。在我们的实验中,我们没有使用该特性,而是显式地配置了目标吞吐量,并按每秒 10K、50K、100K、200K、500K 和 100 万条生产者消息的顺序稳步提高目标吞吐量,四个生产者和四个消费者都使用 1 KB 的消息。然后,我们观察了每个系统在不同配置下提供稳定端到端性能的最大速率。
吞吐量结果
我们发现,Kafka 在我们所比较的系统中吞吐量最高。考虑到它的设计,产生的每个字节都只在一个编码路径上写入磁盘一次,而这个编码路径已经被世界各地的数千个组织优化了近十年。我们将在下面更详细地研究每个系统的这些结果。
图 5:比较这三个系统的峰值稳定吞吐量:100 个主题分区,1 KB 消息,使用 4 个生产者和 4 个消费者
我们将 Kafka 配置为batch.size=1MB
和linger.ms=10
,以便生产者可以有效地对发送给代理的写操作进行批处理。此外,我们在生产者中配置了acks=all
和min.insync.replicas=2
,确保在向生产者返回确认之前每条消息至少复制到两个代理。我们发现,Kafka 能够有效地最大限度地使用每个代理上的磁盘——这是存储系统的理想结果。有关详细信息,请参阅Kafka的驱动程序配置。
图 6:使用默认推荐 fsync 设置的 Kafka 性能。该图显示了 Kafka 代理上的 I/O 利用率和相应的生产者/消费者吞吐量(来源:Prometheus 节点指标)。查看原始结果了解详细信息。
我们还采用另一种配置对 Kafka 进行了基准测试,即在确认写操作之前使用flush.messages=1
和flush.ms=0
在所有副本上将每条消息 fsync 到磁盘。结果如下图所示,非常接近默认配置。
图 7:Prometheus 节点指标显示 Kafka 代理上的 I/O 利用率以及相应的生产者/消费者吞吐量。查看原始结果了解详细信息。
在生产请求排队方面,Pulsar 的生产者与 Kafka 的工作方式不同。具体来说,它内部有每个分区的生产者队列,以及对这些队列的大小限制,对来自给定生产者的所有分区的消息数量设置了上限。为了避免 Pulsar 生产者在发送消息的数量上遇到瓶颈,我们将每个分区和全局限制均设置为无穷大,同时匹配基于 1 MB 字节的批处理限制。
我们还为 Pulsar 提供了更高的基于时间的批处理限制,即batchingMaxPublishDelayMs=50
,以确保批处理主要是由字节限制引起的。我们通过不断增加这个值,直到它对 Pulsar 最终达到的峰值稳定吞吐量没有可测量的影响。对于复制配置,我们使用了ensemble blesize =3、writeQuorum=3、ackQuorum=2
,这与 Kafka 的配置方式相当。有关细节,请参阅Pulsar基准驱动配置。
在 BookKeeper 的设计中,bookie 将数据写入 journal 和 ledger 中,我们注意到,峰值稳定吞吐量实际上是 Kafka 所能达到的吞吐量的一半。我们发现,这种基本的设计选择对吞吐量有深远的负面影响,直接影响了开销。一旦 BookKeeper bookie 的 journal 磁盘完全饱和,Pulsar 的生产者速率就会被限制在那个点上。
图 8:Prometheus 节点指标显示了 BookKeeper journal 磁盘达到极限和最终在 BookKeeper bookie 上测得的吞吐量。查看原始结果了解详细信息。
为了进一步验证这一点,我们还配置 BookKeeper 在RAID 0配置中使用两个磁盘,这为 BookKeeper 提供了将 journal 和 ledger 写操作分到两个磁盘上的机会。我们观察到,Pulsar 最大限度地利用了磁盘的联合吞吐量(~650 MB/s),但峰值稳定吞吐量仍然限制在~340 MB/s。
图 9:Prometheus 节点指标显示,BookKeeper journal 磁盘在 RAID 0 配置下仍然达到极限
图 10:Prometheus 节点指标显示,RAID 0 磁盘已达到极限,以及最终在 Pulsar 代理上测得的吞吐量。查看原始结果了解详细信息。
Pulsar 有一个分层架构,将 BookKeeper bookie(存储)与 Pulsar 代理(存储的缓存/代理)分开。出于完整性考虑,我们也在分层部署中运行了上述吞吐量测试,将 Pulsar 代理移到了另外三个计算优化的c5n.2xlarge
实例上(8 vCores, 21 GB RAM, Upto 25 Gbps network transfer, EBS-backed storage
)。BookKeeper 节点仍在存储优化的i3en.2xlarge
实例上。这使得在这个特殊的设置中,Pulsar 和 BookKeeper 总共有 6 个实例/资源,比 Kafka 和 RabbitMQ 多了 2 倍的 CPU 资源和 33%的内存。
即使在高吞吐量下,系统也主要受到 I/O 限制,而且我们没有发现这种设置带来任何提升。该特定运行的完整结果见下表。事实上,Pulsar 的两层架构似乎只是增加了开销——两个 JVM 占用了更多的内存、两倍的网络传输以及系统架构中更多的移动部件。我们预计,当网络受到限制时(不像我们的测试提供了过剩的网络带宽),Pulsar 的两层架构将以两倍的速度耗尽网络资源,进而降低性能。
与 Kafka 和 Pulsar 不同的是,RabbitMQ 在主题中没有分区的概念。相反,RabbitMQ 使用exchange将消息路由到链接队列,使用头属性(header exchange)、路由键(direct 和 topic exchange)或绑定(fanout exchange),消费者可以从中处理消息。为了匹配工作负载的设置,我们声明了一个 direct exchange(相当于主题)和链接队列(相当于分区),每个队列专用于为特定的路由键提供服务。端到端,我们让所有生产者用所有路由键(轮询)生成消息,让消费者专门负责每个队列。我们还按照社区建议的最佳实践优化了 RabbitMQ:
启用复制(将队列复制到集群中的所有节点)
禁用消息持久化(队列仅在内存中)
启用消费者自动应答
跨代理的负载均衡队列
24 个队列,因为在 RabbitMQ 中,每个队列使用一个专用的内核(8 个 vCPUx 3 个代理)
RabbitMQ 在复制开销方面表现不佳,这严重降低了系统的吞吐量。我们注意到,在此工作负载期间,所有节点都是 CPU 密集型的(见下图右侧 y 轴绿线),几乎没有留出任何余地来代理任何其他消息。详细信息请参见RabbitMQ驱动程序配置。
图 11:RabbitMQ 吞吐量+ CPU 使用率。查看原始结果了解详细信息。
延迟测试
考虑到流处理和事件驱动架构的日益流行,消息系统的另一个关键方面是消息从生产者穿过管道通过系统到达消费者的端到端延迟。我们设计了一个实验,在每个系统都维持在最高稳定吞吐量而又没有显示出任何资源过度使用迹象的情况下,对所有三个系统进行比较。
为了优化延迟,我们更改了所有系统的生产者配置,将消息批处理时间设为最多仅为 1 毫秒(在吞吐量测试中是 10 毫秒),并让每个系统保持默认推荐配置,同时确保高可用性。Kafka 被配置为使用其默认的 fsync 设置(即 fsync off), RabbitMQ 被配置为不持久化消但镜像队列。在反复运行的基础上,我们选择在速率 200K 消息/秒或 200MB/s 下对比 Kafka 和 Pulsar,低于这个测试平台上单磁盘 300MB/s 的吞吐量限制。我们观察到,当吞吐量超过 30K 消息/秒时,RabbitMQ 将面临 CPU 瓶颈。
延迟结果
图 12:在 200K 消息/秒(Kafka 和 Pulsar,消息大小 1KB)和 30K 消息/秒(RabbitMQ,它不能承受更高的负载)速率下测得的配置为高可用标准模式的端到端延迟。注:延迟(毫秒)越低越好。
Kafka 的延迟始终比 Pulsar 更低。在这三个系统中,RabbitMQ 实现了最低的延迟,但考虑到其有限的垂直可扩展性,只是在吞吐量低很多的情况下才能提供。由于实验的设置是有意的,所以对于每个系统,消费者总是能够跟上生产者的速度,因此,几乎所有的读取都是从所有三个系统的缓存/内存中。
Kafka 的大部分性能可以归因于做了大量优化的消费者读取实现,它建立在高效的数据组织之上,没有任何额外的开销,比如数据跳过。Kafka 充分利用了 Linux 页面缓存和零复制机制来避免将数据复制到用户空间中。通常,许多系统(如数据库)都构建了应用程序级缓存,为支持随机读/写工作负载提供了更大的灵活性。无论如何,对于消息系统,依赖页面缓存是一个很好的选择,因为典型的工作负载执行顺序读/写操作。Linux 内核经过多年的优化,能够更智能地检测这些模式,并使用预读等技术来极大地提高读取性能。类似地,构建在页面缓存之上使得 Kafka 可以采用基于sendfile的网络传输,避免额外的数据复制。为了与吞吐量测试保持一致,我们还将 Kafka 配置为 fsync 每条消息然后运行了相同的测试。
Pulsar 采用了一种与 Kafka 非常不同的缓存方法,其中一些源于 BookKeeper 的核心设计选择,即将 journal 和 ledger 存储分开。除了 Linux 页面缓存之外,Pulsar 还引入了多个缓存层,举例来说,BookKeeper bookie 上的预读缓存(我们的测试保留了 OMB 默认的dbStorage_readAheadCacheMaxSizeMb = 1024
),托管 ledger(managedLedgerCacheSizeMB
,在我们的测试中是 20%的可用直接内存,即 12GB*0.2 = 2.4GB)。在我们的测试中,我们没有观察到这种多层缓存的任何好处。事实上,多次缓存可能会增加部署的总体成本,我们怀疑,为了避免前面提到的使用直接字节缓冲区带来的 Java GC 问题,这 12GB 的堆外使用中存在大量的填充。
RabbitMQ 的性能取决于生产者端交换和消费者端绑定到这些交换的队列。对于延迟实验,我们使用了与吞吐量实验相同的镜像设置,特别是直接交换和镜像队列。由于 CPU 瓶颈,我们无法驱动高于 38K 消息/秒的吞吐量,而且,基于这个速率度量延迟的任何尝试都显示出了性能的显著下降,p99 延迟几乎达到了 2 秒。
逐渐将吞吐量从 38K 消息/秒降低到 30K 消息/秒,我们获得了一个稳定的吞吐量,此时,系统资源似乎不存在过度利用。更好的 p99 延迟(1 毫秒)证实了这一点。我们认为,在吞吐量较高的情况下,在 3 个节点上复制 24 个队列的开销似乎对端到端延迟有严重的负面影响,而吞吐量小于 30K 消息/秒或 30MB/s(洋红色实线)时,RabbitMQ 可以提供比其他两个系统更低的端到端延迟。
通常,遵循最佳实践,RabbitMQ 可以提供界限内延迟。鉴于实验故意设置的延迟,消费者总是能跟上生产者,RabbitMQ 的消息管道效率归根结底是 Erlang BEAM VM 处理队列所需做的上下文切换的次数。因此,通过为每个 CPU 内核分配一个队列来限制这一点可以提供最低的延迟。此外,使用 Direct 或 Topic 交换允许对特定队列进行复杂的路由(类似于 Kafka 和 Pulsar 上专用于分区的消费者)。但是,直接交换提供了更好的性能,因为没有通配符匹配,这会增加开销,对这个测试来说,这是合适的选择。
图 13:Kafka、Pulsar 和 RabbitMQ 的端到端延迟,测量时 Kafka 和 Pulsar 的速率为 200K 消息/秒(消息大小 1 KB),RabbitMQ 的速率为 30K 消息/秒。详查看原始结果(Kafka、Pulsar、RabbitMQ)了解详细信息。注:延迟(毫秒)越低越好。
在本节的开始,我们已经讨论了 Kafka 在默认推荐 fsync 配置下的延迟结果(绿色实线)。在 Kafka 将每条消息 fsync 到磁盘(绿色虚线)的可选配置下,我们发现,Kafka 仍然比 Pulsar 延迟低,几乎一直到 p99.9 百分位,而 Pulsar(蓝色线)在更高的尾部百分位上表现更好。在 p99.9 及更高百分位上准确推断尾部延迟非常困难,我们相信,在 Kafka fsync 可选配置(绿色虚线)下,考虑到生产者延迟似乎遵循相同的趋势,p99.9 延迟的非线性暴涨可以归因于涉及 Kafka 生产者的边缘情况。
延迟权衡
图 14:RabbitMQ 的端到端延迟:速率为 10K、20K、30K 和 40K 消息/秒时镜像队列(测试中使用的配置)与经典队列对比(不复制)。注:在这个图中,y 轴上的刻度是对数。
我们承认,每个系统在设计时都有一定的权衡。尽管对 Kafka 和 Pulsar 不公平,但我们发现,在不提供高可用性的配置下把 RabbitMQ 与 Kafka&Pulsar 进行比较很有趣,后两者都以较低的延迟为代价,提供了更强的持久性保证,并且可用性是 RabbitMQ 的三倍。对于某些用例而言(例如设备位置跟踪),这可能很有意义。在这种情况下,用可用性来换取更好的性能是可以接受的,特别是当用例要求实时消息传递并且对可用性问题不敏感时。我们的结果表明,当禁用复制时,RabbitMQ 可以在更高的吞吐量下更好地保持较低的延迟,不过提高后的吞吐量(100K 消息/秒)仍然远低于 Kafka 和 Pulsar 所能达到的水平。
尽管 Kafka 和 Pulsar 速度较慢(p99 百分位分别为大约 5 毫秒和 25 毫秒),但它们提供的持久性、更高的吞吐量和更高的可用性,对于处理金融事务或零售库存管理等大规模事件流用例来说至关重要。对于需要较低延迟的用例,在负载不重的情况下,RabbitMQ 可以实现大约 1 毫秒的 p99 百分位延迟,这是因为消息只是在内存中排队,没有复制开销。
在实践中,操作人员需要谨慎地配置 RabbitMQ,使速率足够低,从而维持这样的低延迟,否则延迟会迅速而显著地退化。但是这个任务非常困难,实际上甚至不可能在所有用例中都以通用的方式实现。总的来说,一个比较好的、运营开销和成本都比较低的架构可能会为所有用例都选择一个像 Kafka 这样的持久性系统,该系统可以在所有负载级别上以低延迟提供最好的吞吐量。
小结
在这篇博文中,我们对 Kafka、RabbitMQ 和 Pulsar 这三种消息系统进行了全面、均衡的分析,得出了以下结论:
吞吐量:Kafka 在三个系统中的吞吐量最高,是 RabbitMQ 的 15 倍,Pulsar 的 2 倍。
延迟:Kafka 在较高的吞吐量下提供了最低的延迟,同时还提供了强大的持久性和高可用性。在默认配置下,Kafka 在所有延迟基准测试中都要比 Pulsar 快,而且,当设置为 fsync 每条消息时,一直到 p99.9 百分位,它都更快。RabbitMQ 可以实现比 Kafka 更低的端到端延迟,但只能在吞吐量低很多的情况下。
成本/复杂性:成本往往是性能的逆函数。作为具有最高稳定吞吐量的系统,由于其高效的设计,Kafka 提供了所有系统中最好的价值(即每字节写入成本)。事实上,Twitter 的Kafka之旅远离了像 Pulsar 这样的基于 BookKeeper 的架构,这证实了我们的观察:Kafka 相对较少的移动部件显著降低了它的成本(在 Twitter 的情况下高达 75%)。此外,将ZooKeeper从Apache Kafka中移除的工作(参见KIP-500)正在进行中,这进一步简化了 Kafka 的架构。
这篇博文的讨论完全集中在性能上,在比较分布式系统时还有更多的东西要讨论。如果您有兴趣了解更多关于 Kafka、Rabbit、Pulsar 和类似系统的分布式系统设计中那些有细微差别的权衡,请关注即将发布的新博文。
查看英文原文:Benchmarking Apache Kafka, Apache Pulsar, and RabbitMQ: Which is the Fastest?
评论 7 条评论