Kafka为什么这么快?

本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~

标题上的"快"过于口语化,准确来讲是"高吞吐量",也就是为什么Kafka每秒钟可处理百万级别的消息。

面试官通常希望,候选人能够逻辑清晰地从Kafka底层原理的角度,盘点出若干个高吞吐量的原因。

在本文中,我们就来针对这个问题梳理一下。

批处理机制

在Kafka的内部实现中,无论是生产者发送消息给Broker,还是Broker将消息落盘持久化,以及消费者从Broker上拉取消息,都是以批处理的方式进行的。

这是Kafka实现高吞吐量的核心设计之一。

1、生产者端

Kafka生产者端有一个非常重要的参数,batch.size,默认值为16384字节,16KB。

该参数为消息发送的批次大小,在追求高吞吐量的情况下,生产者并不是一条条发送消息给同一个分区的,而是在内存缓冲区中攒成一个批次再进行发送。

如果我们把该参数值设置得大些,可以攒一个大的batch后再发送,这样吞吐量就可以进一步提升。

不过,这个参数需要另一个参数进行配合,两者相辅相成完成生产者发消息的控制,那就是linger.ms,默认值为 0。

该参数会跟batch.size配合使用,表示等待生产者消息攒成批次的时间。

生产者会在消息攒成batch.size大小或达到linger.ms时间的情况下,将消息发送出去。

如果将linger.ms的值设置为0的话,这就意味着生产者没有给消息留攒成批次的时间,还是按照一条条发送的。

如果我们想要优化生产者的吞吐量,这个值一定不能设置为默认值。

2、Broker端

Kafka的Broker端接收到生产者端发送过来的消息,会以批次追加写入(Append)的方式将其保存到分区的日志分段中,这样可以减少磁盘IO次数,提升写入的吞吐量。

3、消费者端

我们来看一个消费者端的代码片段:

java 复制代码
while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record : records) {
        // 处理消息
        System.out.printf("Offset = %d, Key = %s, Value = %s%n", 
            record.offset(), record.key(), record.value());
    }
}

从代码的处理逻辑中可以看到,消费者端一样是以批次为单位进行消息拉取并处理的。

消费者端的 max.poll.records 参数,用于控制单次调用poll()方法能够返回批次的消息数量,默认是500。

分区机制

Kafka的分区机制是实现高吞吐量的另一个核心设计,通过将一个主题的消息分散到多个Broker的分区上,以此实现消息的并行发送、接收、保存和处理。

如果Kafka消息的键值为null,并且使用了默认的分区器,分区器会使用轮询(Round Robin)算法将消息分配到不同Broker的分区上。

反之,如果Kafka消息的键值不为null,并且使用了默认的分区器,分区器会对键进行散列,然后根据散列值将消息分配到不同Broker的分区上。

这样一来,就可以实现Kafka分区机制的负载均衡性。

零拷贝机制

Kafka的零拷贝机制,是通过减少消息数据在内核态和用户态之间的拷贝次数,来达到提升数据传输效率的。

1、传统拷贝机制

在该机制中,需经历4次数据拷贝和4次上下文切换,才能完成当消费者从Broker拉取消息时,Broker从磁盘中读取消息数据并发送到网卡缓冲区上。

如下图所示:

(1)Kafka Broker磁盘 ---> Read Buffer(一次DMA Copy,用户态--->内核态)

(2)Read Buffer ---> APP Buffer(一次CPU Copy,内核态 ---> 用户态)

(3)APP Buffer ---> Socket Buffer(一次CPU Copy,用户态 ---> 内核态)

(4)Socket Buffer---> NIC Buffer(一次DMA Copy,内核态 ---> 用户态)

术语解释:

DMA Copy:Direct Memory Access,数据直接在内存磁盘、网卡之间,或内存不同区域之间传输,‌无需CPU参与介入,与之相对应的是CPU Copy。

Read Buffer:操作系统的Page Cache。

Socket Buffer:操作系统用来管理数据包的缓冲区。

NIC Buffer:网卡缓冲区。

2、零拷贝机制

如上图所示,通过零拷贝机制,Kafka Broker磁盘上的数据读取到Read Buffer后,不再需要拷贝到APP Buffer中,而是直接拷贝到NIC Buffer中。

图中的步骤二只是通过DMA的scatter/gather操作,将Read Buffer数据指针存储在Socket Buffer中,并让DMA直接从内存中进行地址读取。

通过零拷贝机制优化后,4次上下文切换变成了2次,4次数据拷贝只剩下2次DMA数据拷贝 + 一次CPU指针拷贝,而两次最消耗CPU资源的CPU数据拷贝操作则不再需要了。

双线程机制

生产者端发送消息的代码如下:

java 复制代码
ProducerRecord<String, String> record = new ProducerRecord<>("Topic1", "12345", "order_event");
producer.send(record);

代码实现非常简单,但其底层的处理机制则复杂很多,核心是通过双线程(主线程、Sender线程)并行机制各自处理不同逻辑,并提升整体生产者吞吐量的。

主线程负责消息创建,然后会依次经过拦截器、序列化器和分区器,并将消息缓存在消息累加器中。

随后,Sender线程再从消息累加器中获取批次消息,并完成后续消息发送逻辑。

压缩机制

一般来讲,绝大多数的业务系统都不属于CPU密集型,CPU占用率不会太高,此时我们可以对消息进行压缩,以达到减少数据量,提升吞吐量的目的。

生产者端的compression.type参数用于压缩设定,其默认值为none,不进行压缩,我们可以选择gzip、snappy、lz4、zstd等压缩算法。

其中,zstd的压缩率最高,适用于磁盘存储和网络传输占用少的场景,lz4的压缩、解压速度最快,且CPU占用率低,适用于高并发的业务场景。

Kafka的压缩机制在生产者端执行,生产者将多条消息合并到一个批次中,并选择合适的压缩算法对整个批次进行压缩,再发送到Broker上。

Broker接收到压缩后的数据后,直接存储和转发这些数据,不会进行数据解压缩操作,当消费者从Broker上拉取数据时,会在消费端进行数据解压。

相关推荐
yanlele6 小时前
我用爬虫抓取了 25 年 5 月掘金热门面试文章
前端·javascript·面试
ai小鬼头7 小时前
Ollama+OpenWeb最新版0.42+0.3.35一键安装教程,轻松搞定AI模型部署
后端·架构·github
萧曵 丶7 小时前
Rust 所有权系统:深入浅出指南
开发语言·后端·rust
老任与码8 小时前
Spring AI Alibaba(1)——基本使用
java·人工智能·后端·springaialibaba
小兵张健8 小时前
武汉拿下 23k offer 经历
java·面试·ai编程
华子w9089258598 小时前
基于 SpringBoot+VueJS 的农产品研究报告管理系统设计与实现
vue.js·spring boot·后端
爱莉希雅&&&8 小时前
技术面试题,HR面试题
开发语言·学习·面试
天天扭码9 小时前
《很全面的前端面试题》——HTML篇
前端·面试·html
星辰离彬9 小时前
Java 与 MySQL 性能优化:Java应用中MySQL慢SQL诊断与优化实战
java·后端·sql·mysql·性能优化
GetcharZp10 小时前
彻底告别数据焦虑!这款开源神器 RustDesk,让你自建一个比向日葵、ToDesk 更安全的远程桌面
后端·rust