当前位置: 首页 > 科技观察

为什么Kafka一口气这么快?

时间:2023-03-12 06:03:21 科技观察

在过去的几年里,软件架构领域发生了翻天覆地的变化。人们不再认为所有系统都应该共享一个数据库。图片来自Pexels微服务、事件驱动架构和CQRS(命令查询责任分离)是构建现代业务应用程序的主要工具。此外,物联网、移动设备和可穿戴设备的普及进一步挑战了系统的近实时能力。首先让我们就“快”这个词达成一致,这个词是多方面的、复杂的,而且非常模糊。一种解释是使用“延迟、吞吐量和抖动”作为“快”的衡量标准。又比如在工业应用领域,行业本身就对“快”设定了规范和期望。所以,“快”在很大程度上取决于你的参考框架是什么。ApacheKafka以延迟和抖动为代价优化吞吐量,但不会牺牲诸如持久性、严格的记录排序和至少一次交付语义等东西。当有人说“Kafka很快”时,假设他们至少有一定的容量,您可以假设他们指的是Kafka在短时间内分发大量记录的能力。Kafka诞生于LinkedIn,当时LinkedIn需要高效传输大量信息,相当于每小时传输TB级的数据。当时,消息传播的延迟被认为是可以接受的。领英毕竟不是从事高频交易的金融机构,也不是按明确期限运作的工控系统。Kafka可用于近实时系统。注意:“实时”并不意味着“快速”,它意味着“可预测”。具体来说,实时是指完成一个动作是有时间限制的,也就是deadline。如果一个系统不能满足这个要求,它就不能被归类为“实时系统”。可以容忍一定量延迟的系统称为“近实时”系统。从吞吐量的角度来看,实时系统通常比近实时或非实时系统慢。Kafka的速度有两个重要方面需要单独讨论:它与客户端和服务器之间的低效实现有关。来自流处理的并行性。服务器端优化的日志存储Kafka使用分段和追加日志的方式在很大程度上限制了对顺序I/O(顺序I/O)的读写,这在大多数存储介质上都是很快的。硬盘驱动器速度慢是一种常见的误解。然而,存储介质的性能在很大程度上取决于访问数据的模式。同样在普通的7200RPMSATA硬盘上,随机I/O(随机I/O)与顺序I/O相比,随机I/O的性能比顺序I/O慢3到4个数量级。此外,现代操作系统提供预读和延迟写入技术,可以预取块中的大量数据,并将较小的逻辑写入合并为较大的物理写入。因此,顺序I/O和随机I/O之间的性能差异在闪存和其他固态非易失性介质中仍然很明显,尽管它们在旋转存储(例如固态驱动器)中并不那么明显。记录的批量顺序I/O在大多数存储介质上都非常快,可以与网络I/O的最高性能相媲美。实际上,这意味着设计良好的日志持久层可以跟上从网络读取和写入的速度。事实上,Kafka的性能瓶颈通常不在硬盘上,而在网络上。因此,除了操作系统提供的批处理外,Kafka的客户端和服务端会批量累积多条记录——包括读写记录,然后通过网络发送。批处理记录可以减轻网络往返的开销,使用更大的数据包,并提高带宽效率。批处理压缩启用压缩后,对批处理的影响尤为明显,因为随着数据量的增加,压缩通常会变得更加有效。尤其是在处理JSON等基于文本的格式时,压缩效果可能非常显着,压缩率通常在5倍到7倍之间。此外,记录批处理主要是客户端操作,传输中的负载不仅对网络带宽有积极影响,而且对服务器端磁盘I/O利用率也有积极影响。CheapConsumers与传统的消息队列模型在消息被消费时删除消息(造成随机I/O)不同,Kafka不会在消息被消费后删除消息——相反,它会跟踪每个消费者组的偏移量。您可以参考Kafka的内部主题__consumer_offsets了解更多信息。同样,因为它只是一个追加操作,所以速度很快。消息的大小在后台进一步减小(使用Kafka的压缩功能),只保留任何给定消费者组的最后一个已知偏移量。将此模型与传统的消息传递模型进行对比,传统的消息传递模型通常提供多种不同的消息分发拓扑。一种是消息队列——一种用于点对点消息传递的持久传输,没有点对多点功能。另一个是允许点对多点消息传递的发布-订阅主题,但以持久性为代价。在传统的消息队列模型中实现持久化的点对多点消息通信模型,需要为每个有状态的消费者维护一个专用的消息队列。这样会放大读写的消耗。消息生产者被迫将消息写入多个消息队列。另一种选择是使用扇出中继,它可以从一个队列中消耗记录并将记录写入多个其他队列,但这只会稍微增加延迟。此外,一些消费者正在服务器端生成负载-读取和写入I/O的混合,顺序的和随机的。Kafka中的消费者是“廉价”的,只要他们不更改日志文件(只允许生产者或Kafka的内部进程这样做)。这意味着大量消费者可以同时读取同一个主题而不会导致集群崩溃。添加消费者仍然有一些成本,但主要是顺序读取和很少的顺序写入。因此,在多样化的消费者系统中,看到一个主题被共享是很正常的。UnflushedBufferedWritesKafka性能的另一个根本原因,也是一个值得进一步研究的原因:Kafka在确认写入之前不会调用fsync。ACK的唯一要求是记录已写入I/O缓冲区。这是一个鲜为人知的事实,但却是一个至关重要的事实。事实上,这就是Kafka的执行方式,就好像它是一个内存队列——Kafka实际上是一个磁盘支持的内存队列(受缓冲区/页面缓存大小限制)。然而,这种形式的写入并不安全,因为副本中的错误会导致数据丢失,即使记录似乎已被确认。换句话说,与关系数据库不同,仅写入缓冲区并不意味着持久性。保证Kafka持久性的是运行多个同步副本。即使其中一个失败,其他副本(假设不止一个)将继续运行——假设失败的原因不会导致其他副本也失败。因此,无fsync的非阻塞I/O方法和冗余同步副本的组合为Kafka提供了高吞吐量、持久性和可用性。客户端优化大多数数据库、队列和其他形式的持久性中间件都是围绕成熟的服务器(或服务器集群)和瘦客户端的概念设计的。客户端实现通常被认为比服务器端实现简单得多。服务器处理大部分负载,客户端仅充当服务器的外观。Kafka采用不同的客户端设计方法。在记录到达服务器之前,在客户端执行了大量工作。这包括在累加器中分段记录、散列记录键以获得正确的分区索引、验证记录和压缩记录批次。客户端了解集群元数据并定期刷新元数据以跟上服务器端拓扑的变化。这允许客户端做出更准确的转发决策。生产者客户端可以将写请求直接转发给分区主机,而不是盲目地将记录发送到集群并依赖后者将它们转发到适当的节点。同样,消费者客户端在获取记录时能够做出更明智的决策,例如在发出读取查询时使用在地理上更接近消费者客户端的副本。(此功能自Kafka版本2.4.0起可用。)零复制的典型低效是在缓冲区之间复制数据字节。Kafka采用生产者、消费者和服务器共享的二进制消息格式,这样即使数据块被压缩,也可以不加修改地传递数据。虽然消除通信双方之间的数据结构差异是重要的一步,但它本身并不能避免数据重复。Kafka在Linux和UNIX系统上使用Java的NIO框架解决了这个问题,特别是java.nio.channels.FileChannel的transferTo()方法。此方法允许将字节从源通道传输到接收器通道,而无需应用程序作为传输的中介。要了解NIO的不同之处,请考虑将源通道读入字节缓冲区并将写入接收器通道作为两个单独的操作的传统方法:File.read(fileDesc,buf,len);Socket.send(socket,buf,len);可以用下图来表示:虽然这张图看起来很简单,但在内部,复制操作需要在用户态和内核态之间进行四次上下文切换,并且在操作完成之前复制了四次数据。下图概述了每个步骤的上下文切换:详细说明:初始read()方法导致从用户态到内核态的上下文切换。DMA(直接内存访问)引擎读取文件并将其内容复制到内核地址空间中的缓冲区。这与代码片段中使用的缓冲区不同。在read()方法返回之前,将数据从内核缓冲区复制到用户空间缓冲区。此时,我们的应用程序就可以读取文件的内容了。随后的send()方法切换回内核模式并将数据从用户空间缓冲区复制到内核地址空间——这次是复制到与目标套接字关联的另一个缓冲区。在后台,DMA引擎接管工作,将数据从内核缓冲区异步复制到协议栈。send()方法在返回之前不会等待操作完成。send()方法调用返回并切换回用户模式。虽然用户态和内核态之间的上下文切换效率低下并且需要额外的复制,但在很多情况下它可以提高性能。它可以充当预读缓存,异步预读,从而提前运行来自应用程序的请求。但是,当请求的数据量远大于内核缓冲区的大小时,内核缓冲区就会成为性能瓶颈。它不是直接复制数据,而是强制系统在用户态和内核态之间频繁切换,直到所有数据都传输完毕。相反,零拷贝方法是在单个操作中处理的。上例中的代码可以改写为一行代码:fileDesc.transferTo(offset,len,socket);下面详细说明它是零拷贝:在这个模型中,上下文切换的次数减少到一次。具体来说,transferTo()方法指示块设备通过DMA引擎将数据读入读取缓冲区。然后,将数据从读取缓冲区复制到套接字缓冲区。最后通过DMA将数据从socketbuffer复制到NICbuffer。因此,我们将副本数从4个减少到3个,其中只有一个涉及到CPU。我们还将上下文切换的次数从4次减少到2次。这是一个巨大的改进,但还没有查询零拷贝。在运行Linux内核2.4或更高版本时,以及在支持收集操作的NIC上,可以进一步优化。如下图所示:按照前面的示例,调用transferTo()方法会使设备通过DMA引擎将数据读入内核缓冲区。但是,对于收集操作,读取缓冲区和套接字缓冲区之间没有复制。相反,NIC被赋予一个指向读取缓冲区的指针,以及一个偏移量和长度。在任何情况下,CPU都不参与复制缓冲区。对于从几MB到1GB的文件大小,结果显示零复制的性能是传统复制和零复制的两到三倍。但更令人印象深刻的是,Kafka使用纯JVM来做到这一点,没有本地库或JNI代码。避免垃圾收集大量使用通道、缓冲区和页面缓存具有减少垃圾收集器工作量的额外好处。例如,在具有32GBRAM的机器上运行Kafka将导致28-30GB的可用空间用于页面缓存,这远远超出了垃圾收集器的范围。吞吐量的差异非常小(大约百分之几),但是经过适当调优的垃圾收集器的吞吐量可能非常高,尤其是在处理寿命较短的对象时。真正的好处是减少了抖动。通过避免垃圾收集,服务器不太可能遇到垃圾收集导致的程序暂停,这会影响客户端并增加记录的通信延迟。与Kafka早期相比,现在避免垃圾回收不是什么大问题。像Shenandoah和ZGC这样的现代垃圾收集器在最坏的情况下可以扩展到巨大的、数TB的堆,并且可以自动将垃圾收集暂停时间调整到几毫秒。今天,你可以看到大量基于Java虚拟机的应用程序使用堆缓存而不是堆外缓存。流处理的并行日志的I/O效率是性能的一个重要方面,主要的性能影响在于写入。Kafka对topic结构和消费生态系统的并行处理是其读取性能的基础。这种组合产生了非常高的端到端消息吞吐量。将并发集成到分区方案和消费者组的操作中,实际上是Kafka中的一种负载均衡机制——在消费者之间平均分配分区。将其与传统的消息队列进行比较:在RabbitMQ设置中,多个并发消费者可以以循环方式从队列中读取,但这样做会丢失消息的顺序。分区机制有利于Kafka服务器的水平扩展。每个分区都有一个专门的领导者。因此,任何重要的多分区主题都可以利用整个服务器集群进行写入操作。这是Kafka和传统消息队列的另一个区别。后者利用集群来提高可用性,而Kafka通过负载平衡来提高可用性、持久性和吞吐量。发布包含多个分区的主题时,生产者在发布记录时指定分区。(可能有单分区topic,那没问题)这可以直接指定分区索引,也可以间接通过recordkey,通过计算hash值来确定分区索引。具有相同散列值的记录共享同一个分区。假设一个主题有多个分区,具有不同键的记录可能会出现在不同的分区中。但是,具有不同哈希值的记录也可能由于哈希冲突而最终位于同一分区中。这就是散列的本质。如果您了解哈希表的工作原理,那么一切都应该自然而然。记录的实际处理由消费者在可选的消费者组中完成。Kafka保证一个partition最多只能分配给一个consumergroup中的一个consumer。(为什么是“最”,当所有消费者都不在线时,就是0个消费者)当组中的第一个消费者订阅了一个topic时,它会收到该topic的所有partition。当第二个消费者订阅主题时,它会收到大约一半的分区,减轻第一个消费者的负担。根据需要添加消费者(理想情况下,使用自动缩放机制),这使您能够并行处理事件流,前提是您已对事件流进行分区。记录吞吐量的控制有两种方式:①主题划分方案。应该对主题进行分区以最大化事件流的数量。换句话说,只有在绝对必要时才提供记录的顺序。如果任何两条记录不相关,则不应将它们绑定到同一分区。这意味着使用不同的键,因为Kafka使用记录键的散列作为分区映射的基础。②群体中消费者的数量。您可以增加消费者的数量来平衡传入记录的负载,消费者数量最多与分区数一样多。(可以增加更多的消费者,但是每个partition最多只能有一个activeconsumer,剩下的consumer会空闲)注意可以提供一个线程池根据consumer来执行workloadconsumer可以是进程也可以是线程。如果你想知道为什么Kafka如此之快,它是如何做到的,以及它是否适合你,我想你现在已经有了答案。为了更清楚地说明问题,Kafka不是最快的消息中间件,也不是最大的吞吐量。还有其他平台可以提供更高的吞吐量——有些是基于软件的,有些是基于硬件的。很难同时实现高吞吐量和低延迟,ApachePulsar[1]是一种很有前途的技术,可扩展,更好的吞吐量-延迟配置文件,同时提供顺序性和持久性。支持Kafka的理由是,作为一个完整的生态系统,它在整体上仍然是无与伦比的。它展示了出色的性能,同时提供了一个丰富而成熟的环境,Kafka仍在以令人羡慕的速度增长。Kafka的设计者和维护者在设计以性能为中心的解决方案方面投入了大量工作。它的设计元素很少让人觉得像是事后的想法或补充。从将工作负载卸载到客户端,到服务器端日志持久化、批处理、压缩、零拷贝I/O和并行流处理——Kafka挑战任何其他消息中间件供应商,无论是商业的还是开源的。最令人印象深刻的是,它在不牺牲持久性、记录排序和至少一次调度语义的情况下做到了这一点。Kafka并不是最简单的消息中间件平台,还有很多需要改进的地方。在设计和构建高性能事件驱动系统之前,必须掌握整体和部分序列、主题、分区、消费者和消费者组的概念。虽然知识曲线很陡峭,但值得您花时间学习。如果你知道谚语“redpill”(红色药丸,为了达到对某事的深入探索或追求,选择思考,不放弃,继续前行,不管路有多艰难),请阅读“简介”“Kafka和Kafdrop事件流简介[2]”。相关链接:https://pulsar.apache.org/https://medium.com/swlh/introduction-to-event-streaming-with-kafka-and-kafdrop-22afdb4b380a