作者 | Emil Koutanov

译者 | 弯月

以下为译文:

最近几年,软件体系结构领域发生了巨大的变化。单体应用,乃至共享一个通用数据存储的多个服务的概念已从软件从业者的世界消失了。微服务、事件驱动的体系结构以及CQRS成了构建以业务为中心的现代化应用程序的主要工具。而在这之上,则是物联网、移动设备、可穿戴设备等设备连接的激增,系统必须实时处理的事件数量的压力也增大了。

首先,我们必须承认“快速”这个词涉及的方面很广泛,很复杂,而且比较模糊。延迟、吞吐量和抖动等都是人们衡量“快速”的决定性指标。快速本身也要考虑上下文关系,各个行业和应用程序领域都有特定的规范和期望值。快慢的判断很大程度上取决于相应的参照系。

Apache Kafka针对吞吐量进行了优化,但牺牲了延迟和抖动,同时保留了其他所需的品质,比如持久性、严格的记录顺序以及“至少一次”的交付语义。当有人说“Kafka很快”,并假设它们具备一定的能力时,你可以认为他们指的是Kafka在短时间内安全地积累和分发大量记录的能力。

究其历史,Kafka的诞生是因为LinkedIn的需求,因为他们需要有效地移动大量的消息,每小时的数据总量达数TB。一条消息的传播延迟倒是次要的。毕竟,LinkedIn不是从事高频交易的金融机构,也不是有明确截止期限的工业控制系统。我们可以利用Kafka实现近乎实时(又名软实时)的系统。

注意:有些人可能对实时这个术语不太熟悉,“实时”并不意味着“快速”,它的意思是“可预测”。具体而言,实时意味着完成动作所需花费的时间有硬性上线,即截止期限。如果系统整体每次都无法满足截止期限,则不能称其为实时系统。能够在一定概率公差范围内完成操作的系统称为“近乎实时”。单从吞吐量看来,一般实时系统都比近乎实时或非实时系统慢。

为了提高速度,Kafka做了两方面的努力,下面我们来分别讨论。第一个关系到客户端与代理之间的低效;第二个源于流处理的机会并行性。

代理的性能

日志结构的持久性

Kafka利用分段式追加日志,将大部分读写都限制为顺序I / O,这种方式在各种存储介质上的读写速度都非常快。人们普遍认为磁盘的读写速度很慢,但实际上存储介质(尤其是旋转介质)的性能很大程度上取决于访问模式。常见的7,200 RPM SATA磁盘上的随机I / O的性能要比顺序I / O慢3~4个数量级。此外,现代操作系统提供了预读和延迟写入技术,可以预先取出大块的数据,并将较小的逻辑写入组合成较大的物理写入。因此,即使在闪存和其他形式的固态非易失性介质中,随机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进程更改日志文件),它们就很“廉价”。这意味着大量的消费者可以同时读取同一主题,而不会占用过多的集群。虽然添加消费者还是需要付出一些代价,但是大多都是顺序读取,加上极少量的顺序写入。因此,多个消费者生态系统共享一个主题是很正常的。

未刷新的缓冲写入

Kafka高性能的另一个根本原因(也是值得进一步探讨的原因)在于,在确认写入之前,Kafka在写入磁盘时实际上并不会调用fsync。ACK唯一的要求就是记录已被写入I / O缓冲区。这一点鲜为人知,但至关重要,正是因为这一点,Kafka的操作就像是一个内存队列一样,因为Kafka的目标就是由磁盘支持的内存队列(规模由缓冲区/页面缓存的大小决定)。

另一方面,这种写入的形式是不安全的,因为即使看似记录已被确认,副本出问题也可能导致数据丢失。换句话说,与关系数据库不同,仅承认写入并不意味着持久性。保证Kafka持久的原因在于它运行了多个同步副本。即使其中一个出现问题,其他副本也将继续运行,当然前提是其他副本没有受影响(有时,某个常见的上游故障可能会导致多个副本同时出问题)。因此,无fsync的I / O非阻塞方法与冗余的同步副本的结合保证了Kafka的高吞吐量、持久性和可用性。

客户端优化

大多数数据库、队列以及其他形式的持久性中间件的设计理念中,都有一个全能的服务器(或服务器集群),加上多个瘦客户端,两者之间通过常见的通信协议通信。通常我们认为,客户端的实现难度远低于服务器端。因此,服务器承担了大部分负载,而客户端仅充当应用程序代码和服务器之间的接口。

Kafka的客户端设计采用了不同的方法。在记录到达服务器之前,客户端需要执行大量操作,包括将记录暂存到收集器中,对记录的键进行哈希处理以获得正确的分区索引,对记录进行校验以及对批次进行压缩。客户端掌握了集群的元数据,并会定期刷新这些元数据,以了解代理拓扑的变化。因此,客户端可以决定底层的转发,生产者客户端不会将记录盲目地发给集群,并依赖集群将记录转发到合适的代理节点,而是直接将写入转发给分区的主服务器。同样,消费者客户在选择记录源时也可以做一些智能处理,例如在发送读取查询时,选择地理位置更接近的副本。(此功能是Kafka的新功能,自2.4.0版开始提供。)

零复制

最常见的低效处理来自缓冲区之间的字节数据复制。Kafka的生产者、代理和消费者之间共享了同样的二进制消息格式,因此数据块即使经过压缩,在端与端之间流动时也无需进行任何修改。尽管消除通信双方的结构差异是很重要的一步,但它本身并不能避免数据复制。

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

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

流程图大致如下:

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

详细来说:

  • 首先,read()需要将上下文从用户模式切换到内核模式。读取文件,然后通过DMA引擎(直接内存访问)将文件内容复制到内核地址空间的缓冲区中。这与上述代码中使用的缓冲区不同。

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

  • 后续的send()需要切换回内核模式,将用户空间缓冲区复制到内核地址空间,这次复制到与目标套接字关联的另一个缓冲区。在背后,由DMA引擎接管,将数据从内核缓冲区异步复制到协议栈。send()无需等待此操作即可返回。

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

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

相比之下,单个操作可以采用零复制的方法。上述示例中的代码可以改写成一行:

fileDesc.transferTo(offset, len, socket);

如下是零复制的方法:

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

最终结果,我们的副本数目从4个减少到了3个,而其中只有一个副本需要CPU。我们还将上下文切换的次数从4个减少到了2个。

这是一个巨大的提升,但还不是真正的查询零复制。在运行Linux内核2.4及更高版本,并且网卡支持gather操作的情况下,就可以实现查询零复制,作为进一步的优化来实现。如下所示。

根据前面的示例,调用transferTo()时,设备会通过DMA引擎将数据读入内核读取缓冲区。但是,使用gather 操作时,读取缓冲区和套接字缓冲区之间不需要复制数据。NIC会得到一个指向读取缓冲区的指针以及偏移量和长度,然后可以通过DMA直接读取。在任何时候,CPU都不需要复制缓冲区。

传统方式与零复制方式(文件大小范围从几兆字节到千兆字节)的比较结果表明。零复制的性能提高了2~3倍。但更令人惊讶的是,Kafka仅使用了普通的JVM就实现了该功能,没有用到任何原生库或JNI代码。

避免垃圾收集

大量使用通道、原生缓冲区和页面缓存还有另一个好处:减少了垃圾收集器(garbage collector,GC)的负载。例如,在拥有32GB内存的计算机上运行Kafka,就有28~30GB的页面缓存可用,这完全超出了GC的范围。吞吐量的差异很小(在几个百分点左右),因为经过正确地微调后,GC的吞吐量可以达到很高,尤其是在处理短期对象时。真正的收益在于抖动的减少:避免使用GC,代理可以减少可能会影响到客户端的暂停,延长记录端到端传播的延迟。

平心而论,对于Kafka来说,与最初的设想时相比,如今避免使用GC已不再是一个问题。Shenandoah和ZGC等现代GC可以扩展到数TB,并且其最坏情况下的暂停时间是可调节的,最低可以调节到几毫秒。如今,对于基于JVM的应用程序来说,使用大型基于堆的缓存的效果远胜于不采用堆的设计。

流并行

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

分区机制还为Kafka代理带来了水平可伸缩性。每个分区都有专门的主节点,因此,任何重大主题(具有多个分区)都可以利用代理的整个集群执行写操作。这是Kafka和消息队列之间的又一个区别,后者利用集群来提高可用性,而Kafka可以平衡代理之间的负载,并提高可用性、持久性和吞吐量。

如果你打算发布拥有多个分区的主题,那么生产者需要在发布记录时指定分区。(只有一个分区的主题没有这种问题。)实现方法有两种:直接的方式(指定分区索引)和间接的方式(通过记录键的方式,该键可以通过哈希生成唯一的分区索引)。拥有同样哈希的记录会占据同一个分区。假设一个主题具有多个分区,则具有不同键的记录可能也会位于不同的分区中。但是,由于哈希冲突,具有不同哈希值的记录也可能会在同一个分区中。这就是哈希的本质。如果你了解哈希表的运作方式,就会发现这正是哈希表的原理。

实际的记录处理由消费者负责,在一个消费者组(可选)内进行操作。Kafka可以保证分区最多只能指定给一个消费者组内的一个消费者。(我们说“最多”是考虑到可能所有消费者都离线的情况。)当组中的第一个消费者订阅该主题时,它会收到该主题上的所有分区。当第二个消费者加入时,它会收到大约一半的分区,从而降低了第一个消费者的大约一半的负担。这样,只要你的事件流中有足够多的分区,就能并行处理事件流,根据需要添加消费者(最好使用自动伸缩机制)。

控制记录的吞吐量可以通过两个途径完成:

  1. 主题分区架构。主题应该按照独立事件子流的最大数量分区。换句话说,记录的顺序只有在绝对必要的时候才需要保证。如果任何两个记录都不相关,那么不应该被绑定到同一个分区。这就需要使用不同的键,因为Kafka使用记录的键作为哈希的来源,以保证分区映射的一致性。

  2. 组内的消费者数量。你可以增加消费者数量来匹配输入记录的负载,最大等于主题中的分区数量。(你甚至可以拥有多个消费者,但能够获得至少一个分区的活跃消费者数量的上限就是分区数量,其余的消费者只能处于闲置状态。)注意消费者可以是进程或线程。根据消费者的负载类型,你可以采用多个独立的消费者线程,或者在线程池中处理记录。

如果你想知道为什么Kafka这么快,它的性能特性是怎样实现的,或者怎样才能伸缩你的集群,那么看完这篇文章,你就应该得到答案了。

更明确地说,Kafka并不是最快的(即并不是吞吐量最大的)消息中间件,有些平台的吞吐量更大,其中有软件实现的也有硬件实现的。Kafka对于吞吐量与延迟之间的平衡处理也算不上最好的,Apache Pulsar的吞吐量平衡性更好,还能提供顺序一致性和可靠性保证。选择Kafka的原因作为整个生态系统的原因是,从整体上来说它还没有对手。它展示了优异的性能,同时还提供了庞大、成熟且一直在进步的社区。

Kafka的设计者和维护者设计了一个非常优秀的、以性能为主的方案。其设计几乎没有任何返工或补丁的迹象。不论是将工作量交个客户端,还是代理的日志式架构,甚至是批处理、压缩、零复制I/O和流式并行,Kafka几乎打败了所有面向消息的中间件,不论是商业的还是开源的。更精彩的是,这些实现没有在持久性、记录顺序、“至少一次”传输语义等质量方面做出任何妥协。

作为消息平台,Kafka并不简单,需要学习许多知识才能掌握。你必须理解全序和偏序、主题、分区、消费者、消费者组等概念,才能毫无障碍地设计并构建一个高性能的事件驱动系统。尽管学习曲线陡峭,但结果非常值得。

原文链接:https://medium.com/swlh/why-kafka-is-so-fast-bde0d987cd03

end

Flink 从入门到精通 系列文章
基于 Apache Flink 的实时监控告警系统关于数据中台的深度思考与总结(干干货)日志收集Agent,阴暗潮湿的地底世界

公众号(zhisheng)里回复 面经、ClickHouse、ES、Flink、 Spring、Java、Kafka、监控 等关键字可以查看更多关键字对应的文章。
点个赞+在看,少个 bug ????

面试官:为什么 Kafka 如此之快?相关推荐

  1. 就计算机应用领域而言航天器,【判断题】面对面试官的提问,反应越快越好。...

    [判断题]面对面试官的提问,反应越快越好. 更多相关问题 [单选] 在excel2003中,向一个单元格输入公式时,必须使用的前导字符为() [单选] 计算机的运算速度通常采用的计量单位是() [单选 ...

  2. kafka计算机专业读法_面试官:Kafka 为什么快?

    无论 kafka 作为 MQ 也好,作为存储层也罢,无非就是两个功能(好简单的样子),一是 Producer 生产的数据存到 broker,二是 Consumer 从 broker 读取数据.那 Ka ...

  3. 面试官:Kafka是什么,它有什么特性与使用场景?

    哈喽!大家好,我是小奇,一位热爱分享的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新,可以微信搜索[小奇JAVA面试]第一时间 ...

  4. Java面试官:Kafka集群管理

    集群是一种计算机系统, 它通过一组松散集成的计算机软件和/或硬件连接起来高度紧密地协作完成计算工作.在某种意义上,他们可以被看作是一台计算机.集群系统中的单个计算机通常称为节点,通常通过局域网连接,但 ...

  5. 完爆面试官!kafka重启后同步时间太长

    4步套路,解决动态规划问题 1.确定问题状态 提炼最后一步 的问题转化 2.转移方程,把问题方程化 3.按照实际逻辑设置初始条件和边界情况 4.确定计算顺序并求解 结合实例感受下: 你有三种硬币,分别 ...

  6. 面试官:说说Kafka处理请求的全流程

    今天来讲讲 Kafka Broker端处理请求的全流程,剖析下底层的网络通信是如何实现的.Reactor在kafka上的应用. 再说说社区为何在2.3版本将请求类型划分成两大类,又是如何实现两类请求处 ...

  7. 面试官:如果让你设计一个消息中间件,如何将其网络通信性能优化10倍以上?【石杉的架构笔记】...

    目录 1.客户端与服务端的交互 2.频繁网络通信带来的性能低下问题 3.batch机制:多条消息打包成一个batch 4.request机制:多个batch打包成一个request " 这篇 ...

  8. 面试官:看你简历写了熟悉Kafka,它为什么速度会这么快?

    前言 Kafka的消息是保存或缓存在磁盘上的,一般认为在磁盘上读写数据是会降低性能的,因为寻址会比较消耗时间,但是实际上,Kafka的特性之一就是高吞吐率. 即使是普通的服务器,Kafka也可以轻松支 ...

  9. kafka是什么_技术面试官问:Kafka为什么速度那么快?

    Kafka的消息是保存或缓存在磁盘上的,一般认为在磁盘上读写数据是会降低性能的,因为寻址会比较消耗时间,但是实际上,Kafka的特性之一就是高吞吐率. 即使是普通的服务器,Kafka也可以轻松支持每秒 ...

  10. 京东某程序员哀叹:在大厂快待废了,出去面试问自己kafka,竟然全忘了!

    在一家公司待久了就容易变得懒惰,原来的许多知识都会慢慢遗忘,这种情况并不少见. 一个京东员工发帖吐槽:感觉在大厂快待废了,出去面试问自己kafka,自己都忘记了.平时用的时候都是别人的中间件,用法不难 ...

最新文章

  1. 霸榜18年,作者连续20年获得微软MVP,这本SQL书凭什么成为畅销经典
  2. linux离线安装rjava,无法在ubuntu系统上安装rJava
  3. Linux常用的(个人用)
  4. 20应用统计考研复试要点(part3)--统计学
  5. 先装vs还是先装sql_锅炉给水泵的止回阀到底安装在出口阀前还是阀后?
  6. 5、ABPZero系列教程之拼多多卖家工具 修改User表结构
  7. python实现简单爬虫百度首页_python实现简单爬虫功能的示例
  8. BZOJ3224 Tyvj 1728 普通平衡树
  9. 基于深度学习的银行卡号识别
  10. 模型评估方法【附python代码】(信息准则:赤池信息量准则AIC、贝叶斯信息准则BIC)
  11. ERA5再分析资料下载攻略
  12. kernel 打印时间戳
  13. 9:参数校验-Java Spring
  14. 问题 1125: 【C语言训练】委派任务*【最优解】
  15. 实现App跳转到应用商店
  16. 人脸识别 —— insightface
  17. springmvc下解析模板生成Excel ,用freemarker
  18. 程序员创业的方向选择
  19. 南京邮电大学图书管理系统
  20. 阿里矢量图iconfont的使用方法

热门文章

  1. C语言中判断浮点数是否等于0
  2. 如何迁移outlook邮件到另一个硬盘_急!如何转移outlook本地邮件
  3. 教你做超惊艳的南丁格尔玫瑰图
  4. 小工具 MyTool
  5. 【网络教程】如何运用rar压缩软件打包程序,实现安装引导等功能!
  6. 电脑开机时自动开启小键盘
  7. 展望2007,十类经典装机软件(全心收集了39款!)
  8. Tomcat目录结构
  9. hexo博客添加标签、分类、归档、关于等页面
  10. c语言scanf返回值