一.多层次
Kafka本质是数组,数组的优势在于存储连续,方便索引,如果顺序写入性能会高。但如果光是一个数组,随着数据越积越多,单一磁盘存放越来越吃力,造成I/O压力过大。所以kafka不仅仅是一个数组,实际上,Kafka充分利用分治的思想,将这个抽象的大数组,划分为很多个小数组。
kafka多层划分
首先是Topic划分,不同的Topic可以看作不同的小数组,这些小数组可以分别存放在不同的Broker上
其次,每个Topic还做了切分,分为了多个Partition,也就是说把一个主题可以划分为多个主题分片。这些主题分片共同组成主题数组

最后,每个Partition还要进一步拆分,一个partition实际对应了多个不同的文件,这些文件是分离开的,分为:
- .log文件,即消息本身,记录了数据
- .timeindex文件,时间索引,即可以通过时间对.log文件做索引查询
- .index文件,即偏移量索引,即可以通过偏移量对.log文件做索引查询

显然,.log就是数据,其它两个文件是不同维度快速定位数据的索引。这样查找的时候,就不是在.log文件中直接找,而是先去找.index或者.timeindex这两个文件,这两个文件是要加载到内核内存的,所以不能太多。
聊到这里,其实基本已经清楚,Kafka通过topic进行数据分片,再通过Partition将Topic进行分片,每个Partition分为三个文件,其中利用(.index和.timeindex)索引文件,可以实现(.log)数据的快速查找,这个查找主要是给消费者提供支持。
有同学可能会问,最后如果某个Topic的某个Partition,因为消息非常多,Partition对应的3个文件不最终也会不堪重负吗?是的,所以Kafka实际还做了进一步分片:按大小滚动,单个文件达到阈值就分裂出一个新的文件继续写。
Partition数据拆分成的多个小文件叫segment,每个segment的大小可以通过log.segment.size 配置,默认是1GB,也就是说每1GB,滚动分片一次。注意,每个小文件也有自己的数据文件和索引文件,索引文件包含偏移索引和时间索引。
二.顺序写
前面有讲过Kafka写入数据其实最终是写到每个Partition的末端,也就是写入对应的磁盘文件中,本节我们就介绍一下这个机制:

顺序写磁盘
我们都知道内存写入通常远快于磁盘写入,但是也有例外,也就是磁盘顺序写入的话,性能和内存差距并不会太大,所以落盘场景是可以考虑磁盘顺序写的。这个顺序写其实就是追加写,每条消息都要紧跟在前一条后面。
基于兼顾复杂度和性能的考虑,Kafka的写入模式专门设计成了顺序写入,这里要注意一点,写磁盘也不一定是直接刷盘的,只是说提交给了操作系统,这里还是有丢失数据的可能性,只是相对于先写Kafka应用程序内存,已经是减少了一个可能遗失的环节了,当然后面小节我们也会提到相关机制,这里稍微了解下即可。
顺序写入为什么这么快
一般而言写磁盘的性能会远远低于操作内存,但是顺序写入则不一样,顺序写入的性能通常而言可以高出随机写入3个以上的数量级,甚至接近内存写入。
为什么顺序写入性能会这么高?为了解决这个问题,我们需要先理解。写入磁盘具体是做什么?我们可以简单一点把写入磁盘分为两步:1.寻址;2.数据传输,寻址需要磁头转动,是机械操作,是主要耗时的地方,而随机写入,就得每一次都去寻址,这就意味着每一次都需要机械活动,自然就非常曼,所以从磁盘的视角来看,它是很讨厌随机写入的。
有个简单理解其实就差不多了,如果想更深入一点理解,可以把磁盘写入拆得更细致:
- 磁头沿着半径机械移动,最终移动到数据所在的磁柱
- 盘片旋转,是磁盘对齐数据所在扇区
- 数据传输,也就是写入数据

1,2我们都可以看作寻址,3是数据传输,在随机读写情况下,写100,000次数据,就需要100,000次磁头移动时间、100,000次盘面旋转时间,而顺序写入情况下,只需要1次磁头移动和1次盘面旋转,即一次寻址,然后最后都是写100,000次数据,但是大的耗时被缩减了,所以顺序写入的性能自然就上来了。
三.页缓存
顺序写磁盘的速度已经很快了,顺序写内存的话就更快,Kafka利用操作系统自带的Page Cache,来实现一定程度循序读写内存。
Page Cache可以简单看作热点磁盘数据的内存缓存,当消息写入时,是先写入Page Cache,后面又操作系统将其刷入磁盘,这样性能就提升了很多。

同时,如果查询时候发现PageCache中有对应数据,那么也就不用去磁盘读取,这样读取性能也会有很大的提升。

值得一提的是,Kafka是生产消费者模式,即生产了消息,在无积压情况下,这个消息很快就会被消费,也就是说我们生产消费时写入了Page Cache,而很快就有消费者来触发Kafka应用程序读取对应数据,而这个时间间隔很短,PageCache命中的可能性会很高。
Page Cache数据和磁盘同步
数据写入Page Cache之后,是需要和磁盘同步的,这是因为如果电脑断电或者重启,这部分数据就会丢失。数据同步有几个时机:
- 当空间内存不够用了,也就是说低于某个阈值时,此时将PageCache刷入并释放Page Cache
- 当脏页在内存驻留时间超过一个阈值时
写入数据之后,Page Cache会标记为脏页,即内存数据页和磁盘页的内容不一致
- 用户主动调用刷盘系统调用sync()和fsync(),这两个调用大概知道就行
总之,Page Cache 是提升Kafka性能的重要机制,通过合理配置Kafka参。数和操作系统参数,可以充分利用 Page Cache 提高消息的读写性能。了解 Page Cache的工作原理并进行适当的优化,对于保障Kafka集群的高效运行至关重要。
四.零拷贝
详细文章可看:https://mp.weixin.qq.com/s/4ZTqvsLzg6-kJFJez4Zkqw
常规传输为何低效
如果应用程序要从磁盘读取数据发送到网络,就会经历下图的流程:

- 2次系统调用: 一次是read(),一次是一次是write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态,所以共发生了4次用户态与内核态的上下文切换。上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。
- 4次数据拷贝: 还发生了4次数据拷贝,其中两次是DMA的拷贝,另外两次则是通过CPU拷贝的
我们回过头看这个数据传输的过程,我们只是搬运一份数据,结果却搬运了4次,过多的数据拷贝无疑会消耗CPU资源,大大降低了系统性能。这种简单又传统的数据传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。
优化思路
减少系统调用次数
读取磁盘数据的时候,之所以要发生上下文切换,这是因为用户空间没有权限操作磁盘或者网卡,内核的权限最高,这些操作设备的过程都需要交给操作系统内核来完成,所以一般要通过内核去完成某些任务的时候,就需要使用操作系统提供的系统调用函数。
而一次系统调用必然会发生2次上下文切换:首先从用户态切换到内核态,当内核执行完任务后,再切换回用户态交由进程代码执行。所以,要想减少上下文切换的次数,就要减少系统调用的次数。
减少数据拷贝次数
再来看看,如何减少「数据拷贝」的次数?
在前面我们知道了,传统的文件传输方式会历经4次数据拷贝,而且这里面,「从内核的读缓冲区拷贝到用户的缓冲区里,再从用户的缓冲区里拷贝到socket的缓冲区里」,这个过程是没有必要的。
因为文件传输的应用场景中,在用户空间我们并不会对数据「再加工」,所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的。
那么有没有一种技术,可以是实现数据之间在核心态进行传输,而不需要将数据在核心态和用户态之间来回复制,最终发送给接收端呢?答案是肯定的,下面我们来简单介绍一下零拷贝技术。
零拷贝技术
所谓的零拷贝是指将数据在内核空间直接从磁盘文件复制到网卡中,而不需要经由用户态的应用程序之手。这样既可以提高数据读取的性能,也能减少核心态和用户态之间的上下文切换,提高数据传输效率。

具体流程如下:
- sendfile开启流程,可以看到sendfile是代替了read和write,相当于只有一次系统调用了
- 操作系统将数据从磁盘通过DMA加载到内核空间的缓存区
- 操作系统将数据的描述符拷贝到Socket缓冲区中。Socket缓冲区仅仅会拷贝一个描述符过去,不会拷贝数据。
- 操作系统直接将数据从内核空间的缓存区传输到网卡中,并通过网卡将数据发送给接收方,这也是通过DMA来做的,不过这里的DMA叫SG-DMA,基本上现阶段的网卡都是支持SG-DMA的,所以没有什么特别的,知道它是一种加强的DMA技术就行了,甚至面试时候就说是DMA也是一样的。
从上面流程我们可以看到,使用零拷贝之后,系统调用次数从2次变成了1次,拷贝次数从4次变成了2次,显著减少了数据传输的损耗。
零拷贝是实现Kafka高性能的其中一种手段,涉及到从磁盘到网卡的数据传输操作都可以使用零拷贝这项技术来进行优化以提升性能
五.批量操作
Kafka主要有两个批量操作的地方,一个是批量生产,也就是批量发送,其实就是通过发送缓冲,将数据缓冲起来,等聚集了一批数据,再一次性发送给Broker,另一个就是批量消费,本质就是一次多拉几条,一起消费。要注意,批量生产和批量消费不是成对关系,是相互独立的优化手段。
六.数据压缩
生产者通常发送基于文本的数据,例如JSON数据。在这种情况下,对生产者应用压缩非常重要。如果开启了压缩,生产者消息以压缩的方式发送,待消费时再解压。Kafka支持两种类型的压缩,producer端和broker端。
那么什么时候需要压缩呢?
如果追求高性能的服务,那么正常来说,是需要开启压缩的。软件领域没有银弹,数据压缩也是有代价的,它会付出额外的CPU,你需要有这么一个判断:压缩带来的磁盘、带宽节省的收益,是大于CPU一定程度的损失的。那怎么判断收益是否大于损失呢?这个得看业务团队的具体情况来分析:
- 如果团队CPU资源多到用不完,而瓶颈在于带宽或者磁盘存储,那么显然是要开启压缩的
- 如果消息比较大,可以考虑压缩
- 消息自身内容如果重复度高,可以考虑压缩
压缩的本质就是针对重复片段用简单代码取代,以实现让数据更小的目的,所以重复度越高,压缩效果肯定是越好的
在Producer端进行消息压缩
producer可以选择使用该compression.type设置来压缩消息,这个参数可以设置为none, gzip, lz4, snappy,和zstd(注意:zstd压缩是在Kafka 2.1之后引入的)等压缩算法,考虑测试 snappy 或lz4以获得最佳速度/压缩比
注意此时压缩操作是由producer来进行,和broker无关,也就不需要broker更改任何配置。

另外,为了进一步提高性能,我们还可以可以结合上一节消息批量发送的优化一起,因为一批消息一起压缩,则单个生产者批次中的所有消息将被压缩在一起,并作为一个整体行的压缩消息发送,这样压缩效果肯定会更好,进而吞吐量也就更高。

在Broker端进行消息压缩
broker也可以进行压缩,即在发送端不进行压缩,这样享受不了发送环节的性能增长,但还是可以享受更高磁盘利用率。Broker压缩是主题级的,也就是说不同主题可以有不同配置,包括用不同的压缩算法。
默认情况下,broker是不开启压缩的,体现在配置上,就是主题压缩定义为compression.type=producer ,即即直接继承producer 端所发来消息的压绎方式,即无论是否压缩,无论采用哪种压缩算法,broker都原样存储消息。
如果Broker开启了压缩,而Producer未开启压缩,那么没有歧义,就在Broker执行压缩即可。但是如果Broker和Producer端同时开启了压缩配置,那么有如下规则:
- 如果Broker端和Producer端的压缩设置是一样的(例如1z4),那么Broker不会进行重复压缩,消息批次将按原样写入日志文件
- 如果Broker端和Producer端的压缩设置是不一样的,那么Broker将解压缩消息并将其重新压缩为其配置的格式。