我们都知道 Kafka 是基于磁盘进行存储的,但 Kafka 官方又称其具有高性能、高吞吐、低延时的特点,其吞吐量动辄几十上百万。
在座的靓仔和靓女们是不是有点困惑了,一般认为在磁盘上读写数据是会降低性能的,因为寻址会比较消耗时间。那 Kafka 又是怎么做到其吞吐量动辄几十上百万的呢?
Kafka Reactor I/O 网络模型是一种非阻塞 I/O 模型,利用事件驱动机制来处理网络请求。
该模型通过 Reactor 模式实现,即一个或多个 I/O 多路复用器(如 Java 的 Selector)监听多个通道的事件,当某个通道准备好进行 I/O 操作时,触发相应的事件处理器进行处理。
这种模型在高并发场景下具有很高的效率,能够同时处理大量的网络连接请求,而不需要为每个连接创建一个线程,从而节省系统资源。
Reactor 线程模型如图 2 所示。
图 2
Reacotr 模型主要分为三个角色。
Kafka 基于 Reactor 模型架构如图 3 所示。
图 3
Kafka 的网络通信模型基于 NIO(New Input/Output)库,通过 Reactor 模式实现,具体包括以下几个关键组件:
图 4
高并发处理能力 :通过 I/O 多路复用机制,Kafka 能够同时处理大量的网络连接请求,而不需要为每个连接创建一个线程,从而节省了系统资源。
低延迟 :非阻塞 I/O 操作避免了线程的阻塞等待,使得 I/O 操作能够更快地完成,从而降低了系统的响应延迟。
资源节省 :通过减少线程的数量和上下文切换,Kafka 在处理高并发请求时能够更有效地利用 CPU 和内存资源。
扩展性强 :Reactor 模式的分层设计使得 Kafka 的网络模块具有很好的扩展性,可以根据需要增加更多的 I/O 线程或调整事件处理器的逻辑。
零拷贝技术是一种计算机操作系统技术,用于在内存和存储设备之间进行数据传输时,避免 CPU 的参与,从而减少 CPU 的负担并提高数据传输效率。
Kafka 使用零拷贝技术来优化数据传输,特别是在生产者将数据写入 Kafka 和消费者从 Kafka 读取数据的过程中。在 Kafka 中,零拷贝主要通过以下几种方式实现:
比如 Broker 读取磁盘数据并把数据发送给 Consumer 的过程,传统 I/O 经历以下步骤。
这一过程经过的四次 copy 如图 5 所示。
图 5
零拷贝技术通过减少 CPU 负担和内存带宽消耗,提高了 Kakfa 性能。
三、Partition 并发和分区负载均衡
在说 Topic patition 分区并发之前,我们先了解下 kafka 架构设计。
一个典型的 Kafka 架构包含以下几个重要组件,如图 6 所示。
图 6
Topic 是 Kafka 中数据的逻辑分类单元,可以理解成一个队列。Broker 是所有队列部署的机器,Producer 将消息发送到特定的 Topic,而 Consumer 则从特定的 Topic 中消费消息。
为了提高并行处理能力和扩展性,Kafka 将一个 Topic 分为多个 Partition。每个 Partition 是一个有序的消息队列,消息在 Partition 内部是有序的,但在不同的 Partition 之间没有顺序保证。
Producer 可以并行地将消息发送到不同的 Partition,Consumer 也可以并行地消费不同的 Partition,从而提升整体处理能力。
因此,可以说,每增加一个 Paritition 就增加了一个消费并发。Partition的引入不仅提高了系统的可扩展性,还使得数据处理更加灵活。
主要有以下几种分区策略:
比如一个 Topic 下有 3个分区,那么第一条消息被发送到分区0,第二条被发送到分区1,第三条被发送到分区2,以此类推。
当生产第4条消息时又会重新开始,即将其分配到分区0,如图 5 所示。
轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略,也是我们最常用的分区策略之一。
所谓随机就是我们随意地将消息放置到任意一个分区上。如图所示,9 条消息随机分配到不同分区。
一旦消息被定义了 Key,那么你就可以保证同一个 Key 的所有消息都进入到相同的分区里面,比如订单 ID,那么绑定同一个 订单 ID 的消息都会发布到同一个分区,由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略,如图所示。
这种策略一般只针对那些大规模的 Kafka 集群,特别是跨城市、跨国家甚至是跨大洲的集群。
我们就可以根据 Broker 所在的 IP 地址实现定制化的分区策略。比如下面这段代码:
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);return partitions.stream().filter(p -> isSouth(p.leader().host())).map(PartitionInfo::partition).findAny().get();
我们可以从所有分区中找出那些Leader副本在南方的所有分区,然后随机挑选一个进行消息发送。
四、Segment 日志文件和稀疏索引
前面已经介绍过,Kafka 的 Topic 可以分为多个 Partition,每个 Partition 有多个副本,你可以理解为副本才是存储消息的物理存在。其实每个副本都是以日志(Log)的形式存储。
为了解决单一日志文件过大的问题,kafka采用了分段(Segment)的形式进行存储。
所谓 Segment,就是当一个日志文件大小到达一定条件之后,就新建一个新的 Segment,然后在新的Segment写入数据。Topic、Partition、和日志的关系如图 8 所示。
图 8
一个对应磁盘上多个文件。
文件存储实际的 message,kafka为每一个日志文件添加了2 个索引文件以及
.timeindex
。
segment 文件命名规则:partition 第一个 segment 从 0 开始,后续每个 segment 文件名为上一个 segment 文件最后一条消息的 offset 值。数值最大为 64 位 long 大小,19 位数字字符长度,没有数字用 0 填充。
为了提高查找消息的性能。kafka为消息数据建了两种稀疏索引,一种是方便offset查找的.index 稀疏索引,还有一种是方便时间查找的.timeindex 稀疏索引。
万万不可,Kafka 作为海量数据处理的中间件,每秒高达几百万的消息写入,这个哈希索引会把把内存撑爆炸。
稀疏索引不会为每个记录都保存索引,而是写入一定的记录之后才会增加一个索引值,具体这个间隔有多大则通过
log.index.interval.bytes
参数进行控制,默认大小为 4 KB,意味着 Kafka 至少写入 4KB 消息数据之后,才会在索引文件中增加一个索引项。
哈希稀疏索引把消息划分为多个 block ,只索引每个 block 第一条消息的 offset 即可 。
.timeindex 文件同理,只不过它的查找结果是,之后还要在走一遍索引查找流程。
由于设计为顺序读写磁盘,因此遍历区间的数据并对速度有太大的影响,而选择 稀疏索引 还能节约大量的磁盘空间。
有了稀疏索引,当给定一个 offset 时,Kafka 采用的是二分查找来扫描索引定位不大于 offset 的物理位移 position,再到日志文件找到目标消息。
利用稀疏索引,已经基本解决了高效查询的问题,但是这个过程中仍然有进一步的优化空间,那便是 通过 mmap(memory mapped files) 读写上面提到的稀疏索引文件,进一步提高查询消息的速度 。
就是基于 JDK nio 包下的 MappedByteBuffer 的 map 函数,将磁盘文件映射到内存中。
进程通过调用mmap系统函数,将文件或物理内存的一部分映射到其虚拟地址空间。这个过程中,操作系统会为映射的内存区域分配一个虚拟地址,并将这个地址与文件或物理内存的实际内容关联起来。
一旦内存映射完成,进程就可以通过指针直接访问映射的内存区域。这种访问方式就像访问普通内存一样简单和高效。
图引自《码农的荒岛求生》
磁盘就一定很慢么?人们普遍错误地认为硬盘很慢。然而,存储介质的性能,很大程度上依赖于数据被访问的模式。
同样在一块普通的7200 RPM SATA硬盘上,随机I/O(random I/O)与顺序I/O相比,随机I/O的性能要比顺序I/O慢3到4个数量级。
合理的方式可以让磁盘写操作更加高效,减少了寻道时间和旋转延迟。
码楼,你还留着课本吗?来,翻到讲磁盘的章节,让我们回顾一下磁盘的运行原理。
磁盘的运行原理如图所示。
硬盘在逻辑上被划分为磁道、柱面以及扇区。硬盘的每个盘片的每个面都有一个读写磁头。
完成一次磁盘 I/O ,需要经过寻道、旋转和数据传输三个步骤。
因此,如果在写磁盘的时候省去寻道、旋转可以极大地提高磁盘读写的性能。
Kafka 采用顺序写文件的方式来提高磁盘写入性能。顺序写文件,顺序 I/O 的时候,磁头几乎不用换道,或者换道的时间很短。减少了磁盘寻道和旋转的次数。磁头再也不用在磁道上乱舞了,而是一路向前飞速前行。
Kafka 中每个Partition 是一个有序的,不可变的消息序列,新的消息可以不断追加到 Partition 的末尾,在 Kafka 中 Partition 只是一个逻辑概念,每个Partition 划分为多个 Segment,每个 Segment 对应一个物理文件,Kafka 对 Segment 文件追加写,这就是顺序写文件。
每条消息在发送前会根据负载均衡策略计算出要发往的目标 Partition 中,broker 收到消息之后把该条消息按照追加的方式顺序写入 Partition 的日志文件中。
如下图所示,可以看到磁盘顺序写的性能远高于磁盘随机写,甚至比内存随机写还快。
小姑娘,你的想法很好,作为快到令人发指的 Kafka,确实想到了一个方式来提高读写写磁盘文件的性能。这就是接下来的主角 Page Cache 。
简而言之:利用操作系统的缓存技术,在读写磁盘日志文件时,操作的是内存,而不是文件,由操作系统决定什么在某个时间将 Page Cache 的数据刷写到磁盘中。
Kafka重度依赖底层操作系统提供的PageCache功能。当上层有写操作时,操作系统只是将数据写入PageCache,同时标记Page属性为Dirty。
当读操作发生时,先从PageCache中查找,如果发生缺页才进行磁盘调度,最终返回需要的数据。
于是我们得到一个重要结论:如果Kafka producer的生产速率与consumer的消费速率相差不大,那么就能几乎只靠对broker page cache的读写完成整个生产-消费过程,磁盘访问非常少。
实际上PageCache是把尽可能多的空闲内存都当做了磁盘缓存来使用。
七、数据压缩和批量处理
数据压缩在 Kafka 中有助于减少磁盘空间的使用和网络带宽的消耗,从而提升整体性能。
通过减少消息的大小,压缩可以显著降低生产者和消费者之间的数据传输时间。
在Kafka 2.1.0版本之前,Kafka支持3种压缩算法:GZIP、Snappy和LZ4。从2.1.0开始,Kafka正式支持Zstandard算法(简写为zstd)。
一个压缩算法的优劣,有两个重要的指标:压缩比,文件压缩前的大小与压缩后的大小之比,比如源文件占用 1000 M 内存,经过压缩后变成了 200 M,压缩比 = 1000 /200 = 5,压缩比越高越高;另一个指标是压缩/解压缩吞吐量,比如每秒能压缩或者解压缩多少 M 数据,吞吐量越高越好。
Kafka 的数据压缩主要在生产者端进行。具体步骤如下:
有压缩,那必有解压缩。通常情况下,Producer 发送压缩后的消息到 Broker ,原样保存起来。
Consumer 消费这些消息的时候,Broker 原样发给 Consumer,由 Consumer 执行解压缩还原出原本的信息。
Kafka会将启用了哪种压缩算法封装进消息集合中,这样当Consumer读取到消息集合时,它自然就知道了这些消息使用的是哪种压缩算法。
总之一句话: Producer端压缩、Broker端保持、Consumer端解压缩。
Kafka Producer 向 Broker 发送消息不是一条消息一条消息的发送,将多条消息打包成一个批次发送。
批量数据处理可以显著提高 Kafka 的吞吐量并减少网络开销。
Kafka Producer 的执行流程如下图所示:
发送消息依次经过以下处理器:
Offset 是 Kafka 中的一个重要概念,用于标识消息在分区中的位置。
每个分区中的消息都有一个唯一的 offset,消费者通过维护自己的 offset 来确保准确消费消息。offset 的高效管理对于 Kafka 的性能至关重要。
offset 是从 0 开始的,每当有新的消息写入分区时,offset 就会加 1。offset 是不可变的,即使消息被删除或过期,offset 也不会改变或重用。
Consumer需要向Kafka汇报自己的位移数据,这个汇报过程被称为提交位移 (Committing Offsets)。因为Consumer能够同时消费多个partition的数据,所以位移的提交实际上是在partition粒度上进行的,即 Consumer需要为分配给它的每个partition提交各自的位移数据 。
提交位移主要是为了表征Consumer的消费进度,这样当Consumer发生故障重启之后,就能够从Kafka中读取之前提交的位移值,然后从相应的位移处继续消费。
在传统的消息队列系统中,offset 通常需要通过锁机制来保证一致性,但这会带来性能瓶颈。Kafka 的设计哲学是尽量减少锁的使用,以提升并发处理能力和整体性能。
Kafka 在 offset 设计中采用了一系列无锁的技术,使其能够在高并发的环境中保持高效。
graph TD;A[启动消费者] --> B[从分区读取消息];B --> C[处理消息];C --> D{是否成功处理?};D --> |是| E[更新 Offset];D --> |否| F[记录失败, 重新处理];E --> G[提交 Offset];G --> H[继续处理下一个消息];F --> B;H --> B;
Kafka 通过无锁轻量级 offset 的设计,实现了高性能、高吞吐和低延时的目标。
Kafka 通过无锁轻量级 offset 的设计,实现了高性能、高吞吐和低延时的目标。
其 Reactor I/O 网络模型、磁盘顺序写入、内存映射文件、零拷贝、数据压缩和批量处理等技术,为 Kafka 提供了强大的数据处理能力和高效的消息队列服务。
本网站的文章部分内容可能来源于网络和网友发布,仅供大家学习与参考,如有侵权,请联系站长进行删除处理,不代表本网站立场,转载者并注明出处:https://jmbhsh.com/xinwenzixun/36033.html