Kafka - 详解Kafka为什么这么快(零拷贝、PageCache、顺序写、分区、分段与索引等)
作者:hangge | 2024-06-13 08:26
Kafka 本身使用了很多手段来保证高性能,包括零拷贝、page cache(页缓存)、顺序读写、分区分段与索引、批量处理、压缩。其中零拷贝和顺序读写最重要。下面我将分别进行介绍。
![](http://hangge.com/blog_uploads/202403/2024030413585043974.png)
![](http://hangge.com/blog_uploads/202403/202403041402254199.png)
![](http://hangge.com/blog_uploads/202403/2024030414253753077.png)
![](http://hangge.com/blog_uploads/202403/2024030414265398121.png)
![](http://hangge.com/blog_uploads/202403/2024030414305479305.png)
![](http://hangge.com/blog_uploads/202403/2024030414520614770.png)
![](http://hangge.com/blog_uploads/202403/2024030415041592967.png)
![](http://hangge.com/blog_uploads/202403/2024030415211198215.png)
![](http://hangge.com/blog_uploads/202403/2024030415302528641.png)
一、零拷贝技术
1,什么是零拷贝?
(1)零拷贝(zero copy)是中间件广泛使用的一个技术,它能极大地提高中间件的性能。所谓的零拷贝,就是指没有 CPU 参与的拷贝。
(2)一般来说,要从磁盘读取内容,然后发送到网络上,这个流程里面总共有四个步骤:
- 应用进入内核态,从磁盘里读取数据到内核缓存,也就是读缓存。这一步应用就是发了一个指令,然后是 DMA 来完成的。
- 应用把读缓存里的数据拷贝到应用缓存里,这个时候切换回用户态。
- 应用进入内核态,把应用缓存里的数据拷贝到内核缓存里,也就是写缓存。
- 应用把数据从写缓存拷贝到 NIC 缓存里,这一步应用也就是发了一个指令,DMA 负责执行。
提示:DMA(Direct Memory Access)是一个独立于 CPU 的硬件,所以我们不太在意 DMA 拷贝。NIC(Network Interface Card)就是指网卡。
![](http://hangge.com/blog_uploads/202403/2024030413565745246.png)
- 而这里面总共有四次内核态与用户态的切换:
![](http://hangge.com/blog_uploads/202403/2024030413585043974.png)
(3)零拷贝就是数据不经过应用缓存,直接让磁盘读到内核缓存,然后内核缓存直接写到 NIC 缓存。
- 那么和最原始的操作比起来,零拷贝少了两次内核态与用户态的切换(由原来的 4 次变为 2 次),还少了两次 CPU 拷贝。但是零拷贝本身还是要用到 DMA 拷贝。
提示:这里的内核缓存,在 linux 系统上其实就是 PageCache。
![](http://hangge.com/blog_uploads/202403/202403041402254199.png)
2,Kafka 的内部实现
(1)Kafka 中存在大量的网络数据持久化到磁盘(Producer 到 Broker)和磁盘文件通过网络发送(Broker 到 Consumer)的过程。这一过程的性能直接影响 Kafka 的整体吞吐量。
(2)Kafka 使用零拷贝技术,避免了数据在内核态和用户态之间的多次复制,从而提高了数据传输效率。
二、PageCache
1,性能提升原因
(1)Kafka 充分利用了 PageCache(页缓存技术)。在写入数据时,Kafka 先将数据写入 PageCache 中(而不是直接刷新到磁盘上,这几乎等价于一个内存写入操作),然后异步写入磁盘。
(2)而 PageCache 是可以存放很多数据的,也就是说 Kafka 本身调用了很多次写入操作之后,才会真的触发 IO 操作,提高了性能。
(3)而且,Kafka 是基于 JVM 的,那么利用 page cache 也能缓解垃圾回收的压力。大多数跟 IO 操作打交道的中间件都有类似的机制,比如说 MySQL、Redis。
2,存在的缺陷
不过使用 PageCache 的缺陷就是如果消息还没刷新到磁盘上,服务器就宕机了,那么整个消息就丢失了。
三、顺序写磁盘
1,性能提升原因
(1)在计算机里面,普遍认为写很慢,但是实际上是随机写很慢,但是顺序写并不慢。即便是机械硬盘的顺序写也并不一定会比固态硬盘的顺序写慢。
提示:顺序写比随机写快得多,是因为顺序写省去了磁头寻址和旋转盘片的步骤
(2)Kafka 在写入数据的时候就充分利用了顺序写的特性。它针对每一个分区,有一个日志文件 WAL(write-ahead log),这个日志文件是只追加的,也就是顺序写的,因此发消息的性能会很好。MySQL、Redis 和其他消息中间件也采用了类似的技术。
提示:早期的时候业界就有人做过实验,一台 Kafka 服务器,把磁盘从机械硬盘切换到固态硬盘,性能虽然有提升,但是并不明显。在固态硬盘很贵的情况下,并不划算。
2,分区多影响写入性能
(1)但是 Kafka 的顺序写要求的是分区内部顺序写,不同的分区之间就不是顺序写的。所以如果一个 topic 下的分区数量不合理,偏多的话,写入性能是比较差的。
![](http://hangge.com/blog_uploads/202403/2024030414253753077.png)
(2)举个例子,假如说要写入 100M 的数据,如果只有一个分区,那就是直接顺序写入 100M。但是如果有 100 个分区,每个分区写入 1M,它的性能是要差很多的。因为一个 topic 至少有一个分区,topic 多也会影响 Kafka 的性能。最好是在创建 topic 的时候就规划好分区,但是如果没规划好,还是得考虑解决。
![](http://hangge.com/blog_uploads/202403/2024030414265398121.png)
3,分区过多如何解决?
(1)如果某个 topic 分区太多了用不上,就可以考虑不用其中的一些分区。假设说我们现在有 32 个分区,但是事实上业务本身用不上那么多分区,那么就可以考虑要求发送者只将消息发送到特定的 16 个分区上。当然,能够直接创建新 topic 是最好的。
![](http://hangge.com/blog_uploads/202403/2024030414305479305.png)
(2)topic 过多的话,可以考虑合并一些 topic,但这也是看业务的。比如说最开始的设计是某个主业务下的子业务都有一个 topic,那么可以考虑这些子业务合并使用一个 topic,然后在里面用 type 等字段来标记是归属于哪个子业务的。
4,多少个分区才算多?
(1)多少分区才算多,以及多少分区才会引起性能下降,这和 topic 本身有关,也和业务有关。
(2)不过之前阿里云中间件团队测试过,在一个 topic 八个分区的情况下,超过 64个 topic 之后,Kafka 性能就开始下降了。
四、分区
1,性能提升原因
(1)Kafka 的分区机制也能提高性能。其核心原理就是减少并发竞争。
(2)假如说现在 Kafka 没有分区机制,只有 topic,那么可以预计的是不管是读还是写,并发竞争都是 topic 维度的。
(3)而在引入了分区机制之后,并发竞争的维度就变成分区了。如果是操作不同的分区,那么完全不需要搞并发控制。
![](http://hangge.com/blog_uploads/202403/2024030414520614770.png)
五、分段与索引
1,性能提升原因
(1)在 Kafka 中,即便在同一个分区内部,Kafka 也进一步利用了分段日志和索引来加速消息查找:
- 每一个分区都对应多个段文件,放在同一个目录下。Kafka 根据 topic 和分区就可以确定消息存储在哪个目录内。
- 每个段文件的文件名就是偏移量,假设为 N,那么这个文件第一条消息的偏移量就是 N+1。所以 Kafka 根据偏移量和文件名进行二分查找,就能确定消息在哪个文件里。
(2)然后每一个段文件都有一个对应的偏移量索引文件和时间索引文件。Kafka 根据这个索引文件进行二分查找,就很容易在文件里面找到对应的消息。
- 如果目标消息刚好有这个索引项,那么直接读取对应位置的数据。
- 如果没有,就找到比目标消息偏移量小的,最接近目标消息的位置,顺序找过去。整个过程非常像跳表。
总之:topic 加分区定目录,偏移量定文件,索引定位置。
2,分段与索引详细说明
(1)在 Kafka 内部,一个分区的日志是由很多个段(segment)组成的,每个段你可以理解成一个文件。同一个 topic 的文件就存放在以 topic 命名的目录下。
- 为了快速找到对应的段文件,段日志文件使用了偏移量来命名。假如说一个文件的名字是 N.log,那么就表示这个段文件里第一条消息的偏移量是 N + 1。
- Kafka 完全可以根据文件名来进行二分查找,从而快速定位到段文件。
![](http://hangge.com/blog_uploads/202403/2024030415041592967.png)
(2)为了加快段文件内的查找,每一个段文件都有两个索引文件。
- 一个是偏移量索引文件,存储着部分消息偏移量到存储位置的映射,类似于 <offset, position> 这种二元组。这个 offset 不是全局 offset,是相对于这个文件第一条消息的偏移量。也就是说假如第一条消息的全局偏移量是 1000,那么偏移量为 1002 的消息的索引项是 <2, pos1>。
- 一个是时间索引文件,存储着时间戳到存储位置的映射,类似于 <timestamp, position> 二元组。
(3)所以整个日志文件目录看上去如下图,假如说要查找 topic 为 test_topic,分区为 1,偏移量为 20000 的消息,那么整个过程是这样的:
- 在日志目录下找到名字为 test_topic_1 的子目录,里面就放着这个分区的消息日志文件。
- 在 test_topic_1 子目录下,根据文件名进行二分查找,可以确定 20000 这条消息应该放在 010031.log 这个文件里面。
- 利用 010031.index 的内容进行二分查找,查找索引项。如果 20000 恰好有一个索引项 <20000, pos0>,那么就读取 pos0 这个位置的数据。
- 如果 20000 没有对应的索引项,就找到比 20000 小的最接近 20000 的索引项,假如有 <19990,pos1>,那么就从 pos1 往后遍历,找到 20000 对应的数据。
(4)对应的根据时间查找也差不多。这里要注意的是,索引文件放的只是部分消息对应的位置,因为 Kafka 希望索引文件能够装入内存。
六、批量处理
1,性能提升原因
(1)Kafka 还采用了批量处理来提高性能。Kafka 的客户端在发送的时候,并不是说每来一条消息就发送到 broker 上,而是说聚合够一批再发送。
(2)而在 broker 这一端,Kafka 也是同样按照批次来处理的,显然即便同样是顺序写,一次性写入数据都要比分多次快很多。
![](http://hangge.com/blog_uploads/202403/2024030415211198215.png)
2,批量处理高性能原因
(1)一方面是减少系统调用和内核态与用户态切换的次数。
- 比方说 100 个请求发送出去,即便采用零拷贝技术,也要 100 次系统调用 200 次内核态与用户态切换。而如果是一次性发送的话,那么就只需要 1 次系统调用和 2 次内核态与用户态切换。
(2)另外一方面,批量处理也有利于网络传输。在网络传输中,一个难以避免的问题就是网络协议自身的开销。
- 比如说协议头开销。那么如果发送 100 次请求,就需要传输 100 次协议头。如果 100 个请求合并为一批,那就只需要一个协议头。
3,批量处理的兜底技术
(1)不过批次也要设计合理。正常来说批次总是越大越好,但是批次太大会导致一个后果,就是客户端难以凑够一个批次。
- 比如说 100 条消息一批和 1000 条消息一批,后者肯定很难凑够一个批次。
(2)一般来说批量处理都是要兜底的,就是在固定时间内如果都没有凑够某个批次,那么就直接发送。
- 比如说 Kafka 里面生产者就可以通过 linger.ms 参数来控制生产者最终等多长时间。时间到了,即便只有一条消息,生产者也会把消息发送到 broker 上。
七、压缩
1,性能提升原因
(1)Kafka 为了进一步降低网络传输和存储的压力,还对消息进行了压缩。这种压缩是端到端的压缩,也就是生产者压缩,broker 直接存储压缩后的数据,只有消费者才会解压缩。
(2)这种方式带来的好处就是,网络传输的时候传输的数据会更少,存储的时候需要的磁盘空间也更少。
![](http://hangge.com/blog_uploads/202403/2024030415302528641.png)
2,缺点
缺点就是压缩还是会消耗 CPU。如果生产者和消费者都是 CPU 密集型的应用,那么这种压缩机制反而加重了它们的负担。
提示:不过在业界,CPU 密集型的应用非常少,会使用到 Kafka 的 CPU 密集型应用就更少了。
全部评论(0)