两个服务器之间怎么传输大量数据速度快 java socket_为什么卡夫卡这么快

探索使Kafka成为当今性能强国的精心设计决策

a6df7ef71b10271d37b2e87e72648bb6.png

最近几年在软件体系结构领域带来了巨大的变化。 单个整体应用程序或共享公用数据存储的几个粗粒度服务的概念已被全世界软件从业者的心灵所忽略。 自治微服务,事件驱动的体系结构和CQRS是构建以当代业务为中心的应用程序的主要工具。 最重要的是,物联网,移动设备,可穿戴设备等设备连接的激增,对系统必须实时处理的事件数量构成了压力。


首先,我们要承认"快速"一词是多方面的,复杂的且高度含糊的。 延迟,吞吐量,抖动是决定并影响人们对该术语解释的指标。 它本身也是上下文相关的:行业和应用程序领域本身就围绕性能设定了规范和期望。 一件事情是否快速,很大程度上取决于一个人的参照系。

Apache Kafka针对吞吐量进行了优化,但以延迟和抖动为代价,同时保留了其他所需的质量,例如耐用性,严格的记录顺序和最少一次的传递语义。 当有人说" Kafka很快",并假设他们至少具备一定的能力时,您可以假设他们指的是Kafka在短时间内安全地积累和分发大量记录的能力。

从历史上看,Kafka诞生于LinkedIn的需求,即有效地移动大量消息的需求,每小时的数据总量达到数TB。 单个消息传播延迟被认为是次要的,这与时间的可变性一样。 毕竟,LinkedIn不是从事高频交易的金融机构,也不是在确定的期限内运行的工业控制系统。 Kafka可用于实现近实时(也称为软实时)系统。

注意:对于不熟悉该术语的人,"实时"并不意味着"快速",而是"可预测"。 具体而言,实时意味着完成动作所花费的时间的硬性上限,也称为最后期限。 如果整个系统每次都无法满足此期限,则不能将其分类为实时。 能够在概率公差范围内运行的系统被标记为"近实时"。 就绝对吞吐量而言,实时系统通常比其近实时或非实时系统慢。

卡夫卡在速度上有两个重要方面,需要分别进行讨论。 第一个与客户端和代理实现的低效率有关。 第二个源于流处理的机会并行性。

代理的表现

日志结构的持久性

Kafka利用分段的仅支持附加的日志文件,将自身很大程度上限制为用于读取和写入的顺序I / O,这在各种存储介质上都是快速的。 人们普遍认为磁盘速度很慢。 但是,存储介质(尤其是旋转介质)的性能在很大程度上取决于访问模式。 与顺序I / O相比,典型的7,200 RPM SATA磁盘上的随机I / O的性能要慢三到四个数量级。 此外,现代操作系统提供了预读和后写技术,这些技术可以以大块倍数预取数据,并将较小的逻辑写入分组为大型物理写入。 因此,尽管在闪存和其他形式的固态非易失性介质中,顺序I / O与随机I / O之间的差异仍然很明显,尽管与旋转介质相比,它的差异要小得多。

记录批处理

在大多数媒体类型上,顺序I / O的速度非常快,可与网络I / O的最高性能相比。 实际上,这意味着精心设计的日志结构持久层将跟上网络流量。 实际上,Kafka的瓶颈通常不是磁盘,而是网络。 因此,除了操作系统提供的低级批处理外,Kafka客户和代理还将在通过网络发送记录之前,成批累积多个记录(用于读取和写入)。 记录的批处理使用较大的数据包并提高带宽效率,从而摊销了网络往返的开销。

批量压缩

启用压缩后,批处理的影响尤为明显,因为随着数据大小的增加,压缩通常会变得更加有效。 尤其是在使用基于文本的格式(例如JSON)时,压缩效果非常明显,压缩率通常在5到7倍之间。 此外,记录批处理很大程度上是作为客户端操作完成的,它将负载转移到客户端上,不仅对网络带宽有积极影响,而且对代理的磁盘I / O利用率也有积极影响。

轻量的消费者

与传统的MQ样式的代理程序在使用时删除消息(会产生随机I / O的损失)不同,Kafka不会在消息使用后删除消息,而是在每个消费者组级别独立跟踪偏移量。 偏移量的进度本身在内部Kafka主题__consumer_offsets上发布。 同样,作为仅追加操作,这是快速的。 在后台(使用Kafka的压缩功能)可以进一步减少该主题的内容,以仅保留任何给定消费者组的最新已知偏移量。

将该模型与更传统的消息代理进行比较,这些代理通常提供几种不同的消息分发拓扑。 一方面是消息队列—一种持久的传输,用于点对点消息传递,没有点对多点功能。 另一方面,pub-sub主题允许点对多点消息传递,但这样做却牺牲了持久性。 在传统的MQ中实现持久的点对多点消息传递模型需要为每个有状态使用者维护一个专用的消息队列。 这会产生读写放大。 一方面,发布者被迫写入多个队列。 或者,扇出继电器可以消耗一个队列中的记录并写入其他队列,但这只会延缓放大点。 另一方面,几个使用者在代理上产生负载-包括顺序I / O和随机I / O的混合读写。

只要Kafka中的用户不更改日志文件(仅允许生产者或内部Kafka进程允许这样做),它们就很"便宜"。 这意味着大量使用者可以同时阅读同一主题,而不会占用过多的群集。 添加使用者需要付出一定的代价,但主要是顺序读取,顺序写入率低。 因此,在不同的消费者生态系统中共享一个主题是很正常的。

未刷新的缓冲写入

卡夫卡性能的另一个根本原因,还有一个值得进一步探讨的原因:在确认写入之前,卡夫卡在写入磁盘时实际上并未调用fsync; ACK的唯一要求是已将记录写入I / O缓冲区。 这是一个鲜为人知的事实,但却是一个关键的事实:实际上,这实际上使Kafka像是一个内存队列一样执行-因为从所有意图和目的来说,Kafka都是磁盘支持的内存队列(有限 由缓冲区/页面缓存的大小决定)。

在另一方面,这种写形式是不安全的,因为即使看似已确认记录,副本的失败也可能导致数据丢失。 换句话说,与关系数据库不同,仅承认写入并不意味着持久性。 使Kafka持久的原因是运行多个同步副本。 即使其中一个失败,其他(假设有多个)也将继续运行-前提是该失败是不相关的(即,多个副本由于常见的上游故障而同时失败)。 因此,无fsync的I / O非阻塞方法与冗余的同步副本的组合使Kafka具有高吞吐量,持久性和可用性的组合。

客户端优化

大多数数据库,队列和其他形式的持久性中间件都是围绕全能服务器(或服务器集群)以及通过众所周知的有线协议与服务器进行通信的瘦客户端的概念设计的。 通常认为客户端实现比服务器要简单得多。 结果,服务器将承担大量的负载-客户端仅充当应用程序代码和服务器之间的接口。

Kafka在客户端设计上采用了不同的方法。 在记录到达服务器之前,需要在客户端上执行大量工作。 这包括在累加器中对记录进行分段,对记录键进行哈希处理以得出正确的分区索引,对记录进行校验和以及对记录批进行压缩。 客户端知道群集元数据,并定期刷新此元数据以了解对代理拓扑的任何更改。 这使客户端可以做出低级转发决定; 生产者客户端不会直接向集群发送记录并依靠后者将其转发给适当的代理节点,而是直接将写入转发给分区主服务器。 同样,消费者客户在采购记录时可以做出明智的决定,有可能使用在发出读取查询时在地理位置上更接近客户的副本。 (此功能是Kafka的较新功能,自版本2.4.0起可用。)

零拷贝

低效率的典型来源之一是在缓冲区之间复制字节数据。 Kafka使用生产者,经纪人和消费者方共享的二进制消息格式,以便数据块即使被压缩也可以不修改地端对端流动。 尽管消除沟通双方之间的结构差异是重要的一步,但它本身并不能避免数据的复制。

Kafka通过使用Java的NIO框架(特别是java.nio.channels.FileChannel的transferTo()方法)在Linux和UNIX系统上解决了此问题。 此方法允许将字节从源通道传输到接收器通道,而无需将应用程序用作传输中介。 要了解NIO的不同,请考虑传统方法,其中将源通道读取到字节缓冲区,然后作为两个单独的操作写入到接收器通道:

File.read(fileDesc, buf, len);Socket.send(socket, buf, len);

可以使用以下方式来表示。

10ed42d6f3b9fba0218a91da78f07ca5.gif

尽管这看起来很简单,但是在内部,复制操作需要在用户模式和内核模式之间进行四个上下文切换,并且在操作完成之前将数据复制四次。 下图概述了每个步骤的上下文切换。

d1cd6b3414483b47cd5eb879f29ebf1b.gif

更详细地看这个-

· 初始的read()会导致上下文从用户模式切换到内核模式。 读取文件,然后将其内容通过DMA(直接内存访问)引擎复制到内核地址空间中的缓冲区中。 这与代码段中使用的缓冲区不同。

· 从read()返回之前,将内核缓冲区复制到用户空间缓冲区中。 此时,我们的应用程序可以读取文件的内容。

· 随后的send()将切换回内核模式,将用户空间缓冲区复制到内核地址空间-这次复制到与目标套接字关联的另一个缓冲区。 在幕后,DMA引擎接管了工作,将数据从内核缓冲区异步复制到协议栈。 send()方法在返回之前不会等待此操作。

· send()调用返回,切换回用户空间上下文。

尽管存在模式切换效率低下和附加复制的问题,但在许多情况下,中间内核缓冲区实际上可以提高性能。 它可以充当预读缓存,异步预取块,从而预先运行来自应用程序的请求。 但是,当请求的数据量明显大于内核缓冲区大小时,内核缓冲区将成为性能瓶颈。 它不是直接复制数据,而是迫使系统在用户和内核模式之间振荡,直到所有数据都被传输为止。

相反,零复制方法是在单个操作中处理的。 可以将先前示例中的代码段重写为单行代码:

fileDesc.transferTo(offset, len, socket);

零复制方法如下所示。

f35ce4d9087e024cad3bb208411b5b30.gif

在这种模型下,上下文切换的数量减少到一个。 具体来说,transferTo()方法指示块设备通过DMA引擎将数据读取到读取缓冲区中。 然后,将该缓冲区复制到另一个内核缓冲区以暂存到套接字。 最后,套接字缓冲区通过DMA复制到NIC缓冲区。

39967fddc177de44289aa3f4f5d8b809.gif

结果,我们将副本数从四减少到三,并且这些副本中只有一个涉及CPU。 我们还将上下文切换的数量从四个减少到了两个。

这是一个很大的改进,但是还没有查询零副本。 后者可以在运行Linux内核2.4及更高版本以及支持收集操作的网络接口卡上作为进一步的优化来实现。 如下所示。

351ed3954e26c0a87f35826ec0d0ce0d.gif

根据前面的示例,调用transferTo()方法会使设备通过DMA引擎将数据读入内核读取缓冲区。 但是,使用聚集操作时,读取缓冲区和套接字缓冲区之间没有复制。 取而代之的是,给NIC一个指向读取缓冲区的指针以及偏移量和长度,该指针由DMA清除。 复制缓冲区决不涉及CPU。

传统文件复制和零复制文件大小从几兆字节到千兆字节的比较显示,使用零复制可以将性能提高2到3倍。 但是,更令人印象深刻的是,Kafka使用了一个没有本地库或JNI代码的普通JVM来实现了这一目标。

避免GC

大量使用通道,本机缓冲区和页面缓存还有另一个好处-减少了垃圾收集器(GC)的负载。 例如,在具有32 GB RAM的计算机上运行Kafka将导致28-30 GB GB的页面缓存可用,完全超出了GC的范围。 吞吐量的差异很小(在几个百分点左右),因为正确调整的GC的吞吐量可能很高,尤其是在处理短期对象时。 真正的收益在于减少了抖动; 通过避免使用GC,代理减少了可能影响客户端的暂停,从而延长了记录的端到端传播延迟。

公平地讲,与最初设想Kafka时相比,现在避免使用GC不再是一个问题。 像Shenandoah和ZGC这样的现代GC可以扩展到巨大的数TB的堆,并且具有可调节的最坏情况下的暂停时间,可低至个位数毫秒。 如今,使用大型基于堆的缓存的基于JVM的应用程序胜过堆外设计的情况并不少见。

流并行

日志结构I / O的效率是性能的关键方面,主要影响写入。 Kafka对主题结构和消费者生态系统中的并行性的处理是其读取性能的基础。 这种组合产生了很高的端到端消息传递吞吐量。 并发根植于其分区方案和用户组的操作中,这实际上是Kafka中的负载平衡机制-在组内各个用户实例之间平均分配分区分配。 将此与更传统的MQ进行比较:在等效的RabbitMQ设置中,多个并发使用者可以以循环方式从队列中读取,但是这样做却丧失了消息排序的概念。

分区机制还允许Kafka代理的水平可伸缩性。 每个分区都有专门的负责人; 因此,任何不重要的主题(具有多个分区)都可以利用代理的整个群集进行写入。 这是Kafka和消息队列之间的另一个区别。 如果后者利用群集来提高可用性,Kafka将真正平衡代理之间的负载,以提高可用性,持久性和吞吐量。

假设您要发布到具有多个分区的主题,则生产者在发布记录时指定分区。 (可能只有一个分区主题,在这种情况下这不是问题。)可以直接(通过指定分区索引)来实现,也可以间接地(通过确定性地哈希为一致的记录键)来实现。 (即每次相同)分区索引。 共享相同散列的记录可以保证占据相同的分区。 假设一个主题具有多个分区,则具有不同键的记录可能最终会位于不同的分区中。 但是,由于哈希冲突,具有不同哈希值的记录也可能最终会在同一分区中。 这就是哈希的本质。 如果您了解哈希表是如何工作的,这没什么不同。

记录的实际处理是由使用者(在一个可选的使用者组中)进行的。 Kafka保证一个分区最多只能分配给其消费者组中的一个消费者。 (我们说"最多"是指所有消费者都下线的情况。)当组中的第一个消费者订阅该主题时,它将收到该主题的所有分区。 当第二个使用者随后加入时,它将获得大约一半的分区,从而使第一个使用者减轻了先前负载的一半。 这使您能够并行处理事件流,并根据需要添加使用者(理想情况下,使用自动缩放机制),前提是您已对事件流进行了充分的分区。

控制记录吞吐量的方法有两种:

· 主题分区方案。 应该对主题进行分区,以使独立事件子流的数量最大化。 换句话说,仅在绝对必要时才保留记录顺序。 如果任何两个记录在因果关系上均不合法相关,则不应将它们绑定到同一分区。 这意味着要使用不同的键,因为Kafka将使用记录的键作为哈希源来导出其一致的分区映射。

· 该组中的消费者数量。 您可以增加使用者数量以匹配入站记录的负载,最多可以达到主题中的分区数量。 (如果需要,您可以有更多的使用者,但是分区数将为至少获得一个分区分配的活动使用者的数目设置上限;其余使用者将保持空闲状态。)请注意,使用者可以是进程或 一个线程。 根据使用者执行的工作负载类型,您可能能够使用多个单独的使用者线程或处理线程池中的记录。


如果您想知道Kafka是快速的,如何实现其著名的性能特征,还是可以根据您的用例进行扩展,那么您现在应该希望得到所有需要的答案。

为了使事情更加清楚,Kafka并不是最快的(即,具有最大吞吐量能力的)消息传递中间件-其他具有更高吞吐量能力的平台-有些是基于软件的,有些是在硬件中实现的。 它也不是最佳的吞吐量延迟折衷方案— Apache Pulsar是一种很有前途的技术,它具有可扩展性,可以实现更好的吞吐量延迟配置文件,同时提供相同的顺序和持久性保证。 采用Kafka的理由是,作为一个完整的生态系统,它在总体上仍然是无与伦比的。 它不仅具有出色的性能,而且还提供了丰富而成熟的环境,但是尽管涉及规模巨大,但卡夫卡仍在以令人羡慕的速度增长。

Kafka的设计人员和维护人员在设计以性能为核心的解决方案方面做得非常出色。 它的设计元素很少让人觉得是事后的想法或固定的想法。 从将工作分担给客户,再到代理上的日志结构持久性,批处理,压缩,零拷贝I / O和流级并行性,Kafka都将这种挑战降为几乎任何其他面向消息的中间件,商业或开源。 最令人印象深刻的是,它在不影响诸如耐用性,记录顺序和至少一次交付语义等质量的情况下做到了这一点。

Kafka不是最简单的消息传递平台,还有很多东西需要学习。 在舒适地设计和构建高性能的事件驱动系统之前,必须先掌握全部和部分订单,主题,分区,消费者和消费者群体的概念。 尽管知识曲线相当丰富,但结果肯定值得您花一会儿。 如果您热衷于服用众所周知的"红色药丸",请阅读Kafka和Kafdrop的事件流介绍。

这篇文章对您有用吗? 我很想听听您的意见,因此请不要退缩。 如果您对Kafka,Kubernetes,微服务或事件流感兴趣,或者有任何疑问,请在Twitter上关注我。

(本文翻译自Emil Koutanov的文章《Why Kafka Is so Fast》,参考:https://medium.com/swlh/why-kafka-is-so-fast-bde0d987cd03)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值