《架构师》2020年10月
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

推荐文章 | Article

最新基准测试:Apache Kafka、Apache Pulsar和RabbitMQ哪个最快?

作者 Alok Nikhil, Vinoth Chandar 译者 平川

本文最初发布于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(8vCore,64GB RAM,2x 2500GB NVMe SSD),我们看中了它高达25Gbps的网络传输限额,可以确保测试设置不受网络限制。这意味着这些测试可以测出相应服务器的最大性能指标,而不仅仅是网速多快。i3en.2xlarge实例在两块磁盘上支持高达约655MB/s的写吞吐量,这给服务器带来了很大的压力。有关详细信息,请参阅完整的实例类型定义。根据一般建议和最初的OMB设置,Pulsar把一个磁盘用于journal,另一个用于ledger存储。Kafka和RabbitMQ的磁盘设置没有变化。

图1:确定跨两块磁盘的i3en.2xlarge实例的最大磁盘带宽,使用Linux命令dd进行测试,作为吞吐量测试的参考。

        Disk 1
        dd if=/dev/zero of=/mnt/data-1/test bs=1M count=65536 oflag=direct
        65536+0 records in
        65536+0 records out
        68719476736 bytes (69 GB) copied, 210.278 s, 327 MB/s
        Disk 2
        dd if=/dev/zero of=/mnt/data-2/test bs=1M count=65536 oflag=direct
        65536+0 records in
        65536+0 records out
        68719476736 bytes (69 GB) copied, 209.594 s, 328 MB/s

OS调优

此外,对于所比较的三个系统,为了获得更好的延迟性能,我们使用tune -adm的延迟性能配置文件对操作系统进行了调优,它会禁用磁盘和网络调度器的任何动态调优机制,并使用性能调控器进行CPU频率调优。它将每个内核的p-state固定在可能的最高频率上,并将I/O调度器设置为deadline,从而提供一个可预测的磁盘请求延迟上限。最后,它还优化内核中的电源管理服务质量(QoS),这是为了提高性能,而不是省电。

内存

与OMB中的默认实例相比,i3en.2xlarge测试实例物理内存几乎是前者的一半(64GB vs. 122GB)。优化Kafka和RabbitMQ使其与测试实例兼容非常简单。两者都主要依赖于操作系统的页面缓存,随着新实例的出现,页面缓存会自动缩小。

然而,Pulsar代理以及BookKeeper bookie都依赖于堆外/ 直接内存缓存,为了使这两个独立进程可以在i3en.2xlarge实例上良好地运行,我们调整了JVM堆/ 最大直接内存大小。具体来说,我们将堆大小从每个24GB(原始的OMB配置)减半为每个12GB,在两个进程和操作系统之间按比例划分了可用物理内存。

在测试中,当目标吞吐量比较高时,我们遇到了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,从而提供更好的总体吞吐量。特别是,在生产者批次大小较小(1KB和10KB)时,使用默认同步设置的吞吐量比fsync每条消息的吞吐量高3到5倍。然而,批次较大(100KB和1MB)时,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、500K100万条生产者消息的顺序稳步提高目标吞吐量,四个生产者和四个消费者都使用1KB的消息。然后,我们观察了每个系统在不同配置下提供稳定端到端性能的最大速率。

吞吐量结果

我们发现,Kafka在我们所比较的系统中吞吐量最高。考虑到它的设计,产生的每个字节都只在一个编码路径上写入磁盘一次,而这个编码路径已经被世界各地的数千个组织优化了近十年。我们将在下面更详细地研究每个系统的这些结果。

图5:比较这三个系统的峰值稳定吞吐量:100个主题分区,1KB消息,使用4个生产者和4个消费者

我们将Kafka配置为batch.size=1MB和linger.ms=10,以便生产者可以有效地对发送给代理的写操作进行批处理。此外,我们在生产者中配置了acks=all和min.insync.replicas=2,确保在向生产者返回确认之前每条消息至少复制到两个代理。我们发现,Kafka能够有效地最大限度地使用每个代理上的磁盘——这是存储系统的理想结果。有关详细信息,请参阅的驱动程序配置

图6:使用默认推荐fsync设置的Kafka性能。该图显示了Kafka代理上的I/O利用率和相应的生产者/ 消费者吞吐量(来源:Prometheus节点指标)。查看原始结果了解详细信息。

我们还采用另一种配置对Kafka进行了基准测试,即在确认写操作之前使用flush.messages=1和flush.ms=0在所有副本上将每条消息fsync到磁盘。结果如下图所示,非常接近默认配置。

图7:Prometheus节点指标显示Kafka代理上的I/O利用率以及相应的生产者/ 消费者吞吐量。查看原始结果了解详细信息。

在生产请求排队方面,Pulsar的生产者与Kafka的工作方式不同。具体来说,它内部有每个分区的生产者队列,以及对这些队列的大小限制,对来自给定生产者的所有分区的消息数量设置了上限。为了避免Pulsar生产者在发送消息的数量上遇到瓶颈,我们将每个分区和全局限制均设置为无穷大,同时匹配基于1MB字节的批处理限制。

        .batchingMaxBytes(1048576) // 1MB
        .batchingMaxMessages(Integer.MAX_VALUE)
        .maxPendingMessagesAcrossPartitions(Integer.MAX_VALUE);

我们还为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最大限度地利用了磁盘的联合吞吐量(~650MB/s),但峰值稳定吞吐量仍然限制在~340MB/s。

图9:Prometheus节点指标显示,BookKeeper journal磁盘在RAID 0配置下仍然达到极限

图10:Prometheus节点指标显示,RAID 0磁盘已达到极限,以及最终在Pulsar代理上测得的吞吐量。查看原始结果了解详细信息。

Pulsar有一个分层架构,将BookKeeper bookie(存储)与Pulsar代理(存储的缓存/ 代理)分开。出于完整性考虑,我们也在分层部署中运行了上述吞吐量测试,将Pulsar代理移到了另外三个计算优化的c5n.2xlarge实例上(8vCores, 21GB RAM, Upto 25Gbps 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消息/ 秒(消息大小1KB), RabbitMQ的速率为30K消息/ 秒。详查看原始结果(KafkaPulsarRabbitMQ)了解详细信息。注:延迟(毫秒)越低越好。

在本节的开始,我们已经讨论了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进行比较很有趣,后两者都以较低的延迟为代价,提供了更强的持久性保证,并且可用性是Rab-bitMQ的三倍。对于某些用例而言(例如设备位置跟踪),这可能很有意义。在这种情况下,用可用性来换取更好的性能是可以接受的,特别是当用例要求实时消息传递并且对可用性问题不敏感时。我们的结果表明,当禁用复制时,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?