当前位置:  首页 互联网 详情

为什么,和并行流——Kafka,这么快

发布来源:互联网    发布时间:2020-02-23 16:46

Kafka 的很多设计理念都比较前卫,从将工作负担分担到了客户端,从 broker 的日志持久化、批处理、压缩、零拷贝 I/O 和并行流—Kafka 对其他开源的和商业的中间件都带来了巨大的。

为什么,和并行流——Kafka,这么快(图1)

在过去的几年中,软件体系结构发生了巨大的变化。单体应用程序或者甚至几个粗粒度服务共享一个公共数据存储的情况已经一去不复返,微服务治理、事件驱动架构和 CQRS(命令查询职责分离模式) 已经成为构建当代以业务为中心的应用程序的主要工具。更糟糕的是随着物联网(IoT)移动设备、可穿戴设备等设备连接的普及,要能够几近实时处理事务。

我们首先要承认“快速”这个术语是多面的、复杂的、高度模糊的。延迟、吞吐量、抖动是影响这个术语解释的几个重要指标。它还具有固有的上下文关系:行业和应用程序领域自己设定了围绕性能的规范和标准—是否快在很大程度上取决于和什么参照物作对比。

Apache Kafka 以延迟和抖动为代价对吞吐量进行了优化,同时保留了其他重要特性,如持久化、顺序性和至少一次的交付的语义。当有人说“Kafka 很快” ,他们指的是 Kafka 有在短时间内安全地堆积和处理大量记录的能力。

从发展历史来看,Kafka 的诞生是因为 LinkedIn 需要高效地传输大量的信息(每小时的数据量达到了几百万兆字节)单个信息传播延迟相对次要,毕竟 LinkedIn 不是一个从事高频交易的金融机构,也不是一个在确定的期限内运行的工业控制。Kafka 可以用于实现接近实时(或称为软实时)的。

注意: 对于那些不熟悉“实时”这个词的人来说,“实时”并不意味着“快速” ,而是意味着“可预测” 具体来说,实时意味着对完成一项行动所花费的时间有一个确定的上限,也就是所谓的最后期限。如果作为一个整体不能每次都能在这个最后期限内完成,它就不能被归类为实时。能够在概率容差范围内运行的被标记为“接近实时”就吞吐量而言,实时通常比近实时或非实时慢。

Kafka 速度快有两个重要方面,需要分别进行讨论。第一个与客户端和 broker 实现的低消耗有关;第二个源于流处理的并行性。

broker 性能

日志结构的持久化

记录批处理

在大多数媒体类型上,顺序 I/O 的速度快得惊人,堪比网络 I/O 的最高性能。在实践中,这意味着设计良好的日志结构持久层能够跟上网络流量。事实上,很多时候,Kafka 的瓶颈并不是磁盘,而是网络。因此,除了操作的低级批处理之外,Kafka 的客户端和 broker 还会在通过网络发送数据之前,在一处理中累积多条记录 (包括读和写)记录的批处理分摊了网络往返的开销,使用了更大的数据包从而提高了带宽利用率。

批量压缩

当启用压缩时,批处理的影响特别明显,因为随着数据大小的增加,压缩通常会变得更有效。特别是当使用诸如 JSON 这样的基于文本的格式时,压缩的效果可能非常明显,压缩比通常在 5 倍到 7 倍。此外,记录批处理主要是由客户端操作完成的,它将负载转移到客户端。这不仅有利于提高网络带宽利用率,而且也有助于提高 broker 的磁盘 I/O 效率。

廉价的消费者

和消费后删除(将会产生随机 I/O)的传统 MQ 风格不同,Kafka 不会在消费后立即删除。Kafka 会在每个消费组级别跟踪消费的偏移。偏移量(offset)分区存储在 Kafka 自动创建的__consumer_offsetstopic 中。另外,这是一个只追加的操作,所以很快。这个主题(topic)的内容所占空间也会在后台压缩 (使用 Kafka 的压缩特性) ,只保留任意给定消费者组的最后已知偏移量。

未刷新的缓冲区写入

Kafka 性能的另一个基本原因,也是一个值得进一步探讨的原因: Kafka 在确认写入之前并没有在写入磁盘时实际调用fsync; 对 ACK 的唯一要求是记录已被写入 I/O 缓冲区。这是一个鲜为人知的事实,但却是一个至关重要的事实: 事实上,正是这个处理技巧让 Kafka 的表现看起来像是一个内存中的队列ー实际上 Kafka 是一个有磁盘支持的内存队列 (受缓冲区 / pagecache 大小的限制)

另一方面,这种写的方式并不安全,因为副本的失败可能导致数据丢失,即使记录似乎已经得到承认。换句话说,和关系数据库不同,写确认并不意味着持久化的完成。使 Kafka 持久运行的是几个同步的副本; 即使其中一个失败,其他副本 (假设有多个副本) 仍然可以运行ーー前提是这个失败是不相关的 (即多个副本由于一个常见的上游失败而同时失败)因此,结合使用无需fsync的非阻塞 I/O 方法和冗余的同步副本,Kafka 得到了高吞吐量、可靠性和可用性的结合。

客户端优化

大多数数据库、队列和其他形式的持久化中间件都是围绕万能 (或集群) 的概念设计的,而且还有通过一个众所周知的有线协议与进行通信的瘦客户端。客户端实现通常会比简单得多。因此,将吸收大部分负载ー客户端仅仅充当应用程序代码和之间的接口。

Kafka 在客户设计上采用了不同的方法。它会在记录到达之前,在客户端上执行了大量的工作。这包括在累加器中暂存记录、散列记录键以得到正确的分区索引、校验和记录批处理的压缩。客户端知道集群元数据,并定期刷新该元数据,以跟上 broker 拓扑的任何更改。这使得客户端可以做出底层的决策,而不是盲目地将记录发送到集群并依赖集群将其到适当的 broker 节点,生产者客户端将直接写到分区主节点。类似地,消费者客户端在寻找记录时能够做出明智的决策,在发出读查询时可能使用地理位置离客户端更近的副本。这个特性是 Kafka 最近添加的,可以在2.4.0版本中使用。

零拷贝

效率低下的一个典型原因是在缓冲区之间复制字节数据。Kafka 使用一种由生产者、broker 和消费者共享的二进制格式,这样数据块即使被压缩,也可以不经修改地端到端传输。虽然消除通信各方之间的结构性差异非常重要,但它本身并不能避免数据的复制。

Kafka 通过使用 Java 的 NIO 框架 (特别是java.nio.channels.FileChannel的transferTo方法 ) 在 Linux 和 UNIX 上解决了这个问题。此方法允许将字节从源信道传输到接收信道,而不涉及作为传输中介的应用程序。为了体会 NIO 的优势,我们来看看传统的方法,源信道被读入一个字节缓冲区,作为两个独立的操作写入一个接收信道:

File.read(fileDesc, buf, len)

Socket.send(socket, buf, len)

为什么,和并行流——Kafka,这么快(图2)

普通拷贝

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

为什么,和并行流——Kafka,这么快(图3)

上下文切换

接下来进行详细介绍:

在 read 函数返回之前,内容将从内核缓冲区拷贝到用户缓冲区中。此时,我们的应用程序就可以读取这个文件的内容了。

send 又从内核态切换到用户态。

尽管上下文切换效率低下,而且还需要额外的数据拷贝,但在许多情况下,它实际上中间内核缓存可以提高性能。它可以充当预读缓存,异步预取块,从而可以在应用程序中预先运行请求。但是,当请求的数据量大大超过内核缓冲区大小时,内核缓冲区就成为性能瓶颈。和直接数据不同,所有数据被传输完毕之前,需要在用户态和内核态之间不断切换。

相比之下,零拷贝方法是在一个操作中处理的。前面示例中的代码片段可以重写为一行程序:

fileDesc.transferTo(offset, len, socket)

零拷贝方法如下所示:

为什么,和并行流——Kafka,这么快(图4)

零拷贝

在该模型下,上下文切换的次数变为一次。具体地说,transferTo方法指示块设备通过 DMA 引擎将数据读入读缓冲区。这个缓冲区将另一个内核缓冲区拷贝到套接字中。最后,通过 DMA 将套接字缓冲区复制到 NIC (网络接口控制器) 缓冲区。

为什么,和并行流——Kafka,这么快(图5)

切换

因此,我们已经将拷贝的次数从四次减少到三次,而且其中只有一个拷贝涉及 CPU。我们还将上下文切换的次数从 4次减少到了 2 次。

这是一个巨大的改进,但还有改进空间。在运行 Linux 内核 2.4 及以后版本时,在支持gather操作的网络接口卡上,后者可以作为进一步的优化来实现。这一点如下图所示:

为什么,和并行流——Kafka,这么快(图6)

优化

如上图所示,调用transferTo方法会让设备通过 DMA 引擎将数据读入内核读缓冲区中。但是,使用 gather 操作时,读缓冲区和套接字缓冲区之间不存在拷贝。相反,NIC 将得到一个指向读缓冲区的指针,以及由 DMA 清空的偏移量和长度。这种模式下,CPU 不会参与到缓冲区拷贝中。

比较传统的和零拷贝的文件大小 (从几兆字节到一千兆字节) ,发现零拷贝的性能提高了两到三倍。但更令人折服的是 Kafka 没有使用本地 (native) 库函数或 JNI 代码,而仅使用纯 JVM 就实现了这一点。

避免 GC

频繁使用通道、native 缓冲区和页面缓存还有一个好处: 减少垃圾收集器(GC) 的负担。例如,在一台有32 GB RAM 的机器上运行 Kafka 将导致 28–30 GB 可用于页面缓存,完全超出了 GC 的范围。吞吐量方面的差异很小ー在几个百分点的区间内ー因为经过正确调优,特别在处理生命周期较短的对象时 GC 的吞吐量可能相当高。真正的好处是减少抖动; 通过避免 GC,broker 不太可能经历可能影响客户端的暂停,从而降低了记录的端到端传播延迟。

公平地说,与 Kafka 诞生时相比,现在降低 GC 次数已经不是什么大问题了。像 Shenandoah 和 ZGC 这样的现代垃圾回收器可以扩展到巨大的、数兆兆字节的堆,并且可以设置最坏情况下的暂停时间,甚至可以精确到毫秒级别。如今,使用大型基于堆的缓存的 JVM 的应用程序胜过堆外设计的情况也并不少见。

流并行性

日志结构的 I/O 效率是性能的一个关键,尤其对于写操作来说。Kafka 在主题结构和消费者生态中对并行性的处理是其读取性能的基础。这种组合产生非常高的总体端到端吞吐量。并发性植根于 Kafka 的分区方案和消费者组的操作中,这是 Kafka 内部的一种有效的负载平衡机制,即在消费者组内的实例之间大致均匀地分配分区任务。与更传统的 MQ 相比,在等效的 RabbitMQ 设置中,多个并发消费者可能以循环的方式从一个队列读取数据,但是这样做就无法保证消费的顺序性。

分区机制还允许 Kafka broker 的水平可伸缩。每个分区都有一个专门的 leader;因此,任何重要的主题(具有多个分区)都可以利用 broker 的整个集群进行写操作。这是 Kafka 和队列之间的另一个区别;当队列利用集群获得可用性时,Kafka 通过跨 broker 负载均衡来获得可用性、稳定性和吞吐量。

加入你需要发布一个带有多个分区 partition 的主题,生产者在发布记录时可以指定分区。可能有一个单分区主题,在这种情况下,并没啥问题 这可以直接通过指定一个分区索引来实现,也可以通过一个记录键来间接实现,记录键可以确定地散列到一个一致的 即每次都相同 分区索引。相同散列的记录就可以保证占用相同的分区。假设一个主题有多个分区,那么使用不同密钥的记录很可能会出现在不同的分区中。但是,由于散列冲突,具有不同散列的记录也可能最终出现在同一分区中。这就是散列的本质,这和散列表的原理是一样的。

吞吐量的控制通过两种方式实现:

主题分区模型。应该对主题进行分区,以实现最大化独立事件子流的数量。换句话说,记录顺序只能在绝对必要的情况下才保留。如果任何两个记录之间没有因果关系,那么它们就不应该绑定到同一个分区上。这意味着它们可以使用不同的键,因为 Kafka 将使用一个记录的键作为散列源会映射到一致的分区中。

如果你想知道 Kafka 是不是很快,它是如何实现高性能的,或者是否可以用到你的中,相信看到这里你应该已经有了答案。

很明显,Kafka 并不是最快的(换句话说,并不是吞吐量最大的)中间件。还有一些基于软件或硬件实现的具有更高吞吐量的其他中间件。Apache Pulsar 吞吐量和延迟率折中最好的方案,但是它非常有前途,具有可拓展性,能够在保证顺序性和稳定性的同时获得更大的吞吐量和更小的延迟。采用 Kafka 的主要原因是,它具有一个完整的无以伦生态。它的表现非常出色,了非常丰富和成熟的环境。尽管规模强大,但是 Kafka 仍然以令人羡慕的速度增长。

Kafka 的设计人员和维护人员在设计以性能为导向的方案上做的非常棒,它的很多设计理念都比较前卫。从将工作负担分担到了客户端。从 broker 的日志持久化、批处理、压缩、零拷贝 I/O 和并行流,Kafka 对其他开源的和商业的中间件都带来了巨大的。最重要的是 Kafka 在实现这些功能的同时并没有牺牲稳定性、记录顺序和至少一次交付的语义等特性。

Emil Koutanov,软件架构师、工程师,一个父亲。同时也是一个狂热的作家、博主和小说作家。译者:明明如月,知名互联网公司 Java 高级工程师,CSDN 博客专家。

本文相关词条概念解析:

缓冲区

缓冲区是地理空间目标的一种影响范围或服务范围,具体指在点、线、面实体的周围,自动建立的一定宽度的多边,数学表达为:Bi=(x:d(xi,Oi)≤R)。在军事中,缓冲区又称中立区、中立地带等,指的是两地的交界处因为战争或其他因素,而划定出的带状地区。缓冲区(buffer)这个中文译意源自当计算机的高速部件与低速部件通讯时,必须将高速部件的输出暂存到某处,以保证高速部件与低速部件相吻合.后来这个意思被扩展了,成为"临时存贮区"的意思。缓冲区又称中立区、中立地带等,指的是两地的交界处因为战争或其他因素,而划定出的带状地区,是在国与国之间设定的军事缓冲地带,,此带状地区并不完全属于两方之中的一方,通常由两方共管或是由第三方协助管理。

相关资讯

相关推荐

网友评论