大家好,我是 JavaClimber。
Kafka 是一款性能非常优秀的消息队列,每秒处理的消息体量可以达到千万级别。 今天来聊一聊 Kafka 高性能背后的技术原理,接下来从生产消息、存储消息、消费消息 3 个角度11个技术点进行剖析。
一、生产消息
数据生产阶段,高性能的原因主要是批量发送、消息压缩和异步发送。
1、批量发送
Kafka 收发消息都是批量进行处理的。我们看一下 Kafka 生产者发送消息的代码:
//Kafka 生产者在向集群发送消息时,并不会直接把消息发送出去,而是把消息缓存起来,缓存消息量达到配置的批量大小后,才会发送出去且收发消息都是批量进行处理的。
private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
TopicPartition tp = null;
try {
//省略前面代码
Callback interceptCallback = new InterceptorCallback<>(callback, this.interceptors, tp);
//把消息追加到之前缓存的这一批消息上
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
serializedValue, headers, interceptCallback, remainingWaitMs);
//积累到设置的缓存大小,则发送出去
if (result.batchIsFull || result.newBatchCreated) {
log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
this.sender.wakeup();
}
return result.future;
// handling exceptions and record the errors;
// for API exceptions return them in the future,
// for other exceptions throw directly
} catch /**省略 catch 代码*/
}
从代码中可以看到,生产者调用 doSend 方法后,并不会直接把消息发送出去,而是把消息缓存起来,缓存消息量达到配置的批量大小后,才会发送出去。
从上面 accumulator.append 代码可以看到,一批消息属于同一个 topic 下面的同一个 partition
2、消息压缩
如果消息体比较大,Kafka 消息吞吐量要达到千万级别,网卡支持的网络传输带宽会是一个瓶颈。Kafka 的解决方案是消息压缩。发送消息时,如果增加参数 compression.type,就可以开启消息压缩; 如果 compression.type 的值设置为 none,则不开启压缩。 有了消息批量收集和压缩,kafka 生产者发送消息的过程如下图:
3、异步发送
kafka发送消息支持同步发送和异步发送,对于可靠性要求不高的系统,可以使用异步发送,不必等待每个消息的确认,大大提高发送消息的速率。
二、存储消息
kafka集群层面的高性能主要在于数据的存储,kafka数据存储到磁盘,但为什么还这么快呢?原因包括:磁盘顺序写入、零拷贝、页缓存pageCache、索引机制和多副本
1、磁盘顺序写
Kafka 的 Broker 在写消息数据时,首先为每个 Partition 创建一个文件,然后把数据顺序地追加到该文件对应的磁盘空间中,不是在文件的随机位置来修改数据。
[ 参考:3个角度11个技术点剖析Kafka高性能背后的原理 ]如果这个文件写满了,再创建一个新文件继续追加写。这样大大减少了寻址时间,提高了读写性能。
在固态硬盘上,顺序读写的性能是随机读写的好几倍。而在机械硬盘上,寻址时需要移动磁头,这个机械运动会花费很多时间,因此机械硬盘的顺序读写性能是随机读写的几十倍。
2、PageCache页缓存
在 Linux 系统中,所有文件 IO 操作都要通过 PageCache,PageCache 是磁盘文件在内存中建立的缓存。当应用程序读写文件时,并不会直接读写磁盘上的文件,而是操作 PageCache。
应用程序写文件时,都先会把数据写入 PageCache,然后操作系统定期地将 PageCache 的数据写到磁盘上。如下图:
而应用程序在读取文件数据时,首先会判断数据是否在 PageCache 中,如果在则直接读取,如果不在,则读取磁盘,并且将数据缓存到 PageCache。
Kafka 充分利用了 PageCache 的优势,当生产者生产消息的速率和消费者消费消息的速率差不多时,Kafka 基本可以不用落盘就能完成消息的传输。
3、零拷贝
我们电脑上跑着的应用程序,其实是需要经过操作系统,才能做一些特殊操作,如磁盘文件读写、内存的读写
等等。因为这些都是比较危险的操作,不可以由应用程序乱来,只能交给底层操作系统
来。
因此,操作系统为每个进程都分配了内存空间,一部分是用户空间,一部分是内核空间。内核空间是操作系统内核访问的区域,是受保护的内存空间,而用户空间是用户应用程序访问的内存区域。
-
内核空间:主要提供进程调度、内存分配、连接硬件资源等功能
-
用户空间:提供给各个程序进程的空间,它不具有访问内核空间资源的权限,如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成——进程从用户空间切换到内核空间,完成相关操作后,再从内核空间切换回用户空间。
消费者拉取Kafka Broker分区的数据时,即使命中了 PageCache,也需要将 PageCache 中的数据先复制到应用程序(kafka服务)的内存空间,然后从应用程序的内存空间复制到 Socket 缓存区,将数据发送出去。如下图所示:
Kafka 采用了零拷贝技术把数据直接从 PageCache 复制到 Socket 缓冲区中,这样数据不用复制到用户态的内存空间,同时 DMA 控制器直接完成数据复制,不需要 CPU 参与。如下图:
3、分段存储和索引机制
Kafka将消息持久化到磁盘上,以日志文件的形式存储。每个分区(partition)在Kafka中都是一个独立的日志文件。这种设计方式使得Kafka具有非常好的写入性能,因为写入操作只是简单的追加到日志文件的末尾。
[ 参考:3个角度11个技术点剖析Kafka高性能背后的原理 ]
随着消息的不断写入,日志文件会变得越来越大。为了管理这些文件,Kafka采取分段存储和索引机制,如下图所示:
kafka将每个日志文件分为多个段
(segment),每个段都有一个基础偏移量(baseOffset)
,表示该段中第一条消息的偏移量,每个segment由三部分组成:
-
数据文件(.log):存储实际的消息内容
-
索引文件(.index):存储消息的
偏移量
和该消息在数据文件中的物理位置
之间的映射关系 -
timeindex时间索引文件。
Kafka使用偏移量(offset)来唯一标识每条消息。当生产者发送消息到Kafka时,Kafka会为每条消息分配一个递增的偏移量。消费者可以通过指定偏移量来读取特定的消息。索引文件使得Kafka可以快速定位到任意偏移量的消息位置,从而实现高效的读取操作。
4、多副本
副本机制实质是冗余设计。Kafka通过副本(replica)机制实现数据的容错和高可用性。每个分区可以有多个副本,这些副本分布在不同的Kafka服务器上。当某个服务器出现故障时,其他服务器上的副本可以继续提供服务,确保数据的可靠性和可用性。
如上图所示:当含有Leader副本的节点(broker-2)宕机时, 分区partition-1的数据仍然存在于broker1和broker3的follower副本
中,即2个 partition-1 follower。
Kafka会自动从Follower副本中选举一个新的Leader副本。这个选举过程由Kafka的控制器(Controller)负责,它会在剩余的Follower副本中选择一个作为新的Leader。
新的Leader副本选举完成后,Kafka会将所有的读写请求路由到新的Leader副本上,以确保服务的连续性。因此,即使某个节点宕机,Kafka集群仍然能够继续对外提供服务。
特别说明:上图2个follower副本必须都在 ISR 列表中才能被选举为 leader
4.1 ISR机制
由于Kafka是一个分布式系统,follower必然会存在与leader不能实时同步的风险,那么follower副本在什么条件下才算与Leader同步
?ISR同步副本机制解决这个问题。
[ 参考:3个角度11个技术点剖析Kafka高性能背后的原理 ]
In-sync replica(ISR)称之为同步副本,ISR中的副本都是与Leader进行同步的副本
。ISR中是什么副本呢?首先可以明确的是:Leader副本总是存在于ISR中,而follower副本是否在ISR中,取决于该follower副本是否与leader副本保持了“同步”。
4.2 ACK机制
Kafka确认机制是确保消息在生产者和消费者之间可靠传递的一种机制,即可靠性机制。Kafka的ACK确认机制有三个级别,默认ack=1。
(1)acks=0
这是最快速的确认级别,也是最不可靠的。生产者发送消息后不会等待任何确认,直接将消息添加到分区的副本中,并认为消息已成功发送。在这种模式下,如果broker发生故障或错误,生产者将不会知道,也不会重试发送消息。这种模式通常用于不太关心消息可靠性的场景。
这是默认的确认级别,也称为“leader确认”。在这个级别下,生产者发送消息后会等待分区的领导者(leader)确认消息已成功写入到其本地日志。一旦领导者确认,生产者会认为消息已成功发送。这种模式下,如果领导者成功写入消息但在复制给其他副本时发生故障,消息可能会丢失。但在大多数情况下,消息可靠性已经得到保证。
(3)acks=all或acks=-1
这是最严格的确认级别,也称为“全部确认”或“等待所有副本确认”。在这个级别下,生产者发送消息后会等待所有的ISR
(In-Sync Replicas,同步副本)确认。ISR是分区的所有副本中与领导者保持同步的副本集合。在这种模式下,消息只有在被领导者和所有同步副本都确认接收后才被视为已提交。这确保了消息的可靠性。