下面以本地kafka日志文件夹为例,介绍kafka文件目录布局。
kafka文件的存储目录,可以通过配置服务log.dirs
确定,我本地环境使用默认的地址,log.dirs=/usr/local/var/lib/kafka-logs
,进入文件夹,可以看到目前的文件内容如下:
➜ kafka-logs ls
__consumer_offsets-0 __consumer_offsets-22 __consumer_offsets-36 __consumer_offsets-5
__consumer_offsets-1 __consumer_offsets-23 __consumer_offsets-37 __consumer_offsets-6
__consumer_offsets-10 __consumer_offsets-24 __consumer_offsets-38 __consumer_offsets-7
__consumer_offsets-11 __consumer_offsets-25 __consumer_offsets-39 __consumer_offsets-8
__consumer_offsets-12 __consumer_offsets-26 __consumer_offsets-4 __consumer_offsets-9
__consumer_offsets-13 __consumer_offsets-27 __consumer_offsets-40 cleaner-offset-checkpoint
__consumer_offsets-14 __consumer_offsets-28 __consumer_offsets-41 localTestTopic-0
__consumer_offsets-15 __consumer_offsets-29 __consumer_offsets-42 localTestTopic-1
__consumer_offsets-16 __consumer_offsets-3 __consumer_offsets-43 localTestTopic-2
__consumer_offsets-17 __consumer_offsets-30 __consumer_offsets-44 log-start-offset-checkpoint
__consumer_offsets-18 __consumer_offsets-31 __consumer_offsets-45 meta.properties
__consumer_offsets-19 __consumer_offsets-32 __consumer_offsets-46 recovery-point-offset-checkpoint
__consumer_offsets-2 __consumer_offsets-33 __consumer_offsets-47 replication-offset-checkpoint
__consumer_offsets-20 __consumer_offsets-34 __consumer_offsets-48
__consumer_offsets-21 __consumer_offsets-35 __consumer_offsets-49
其中:
__consumer_offsets-xx
文件夹是kafka存储消费者offset的默认主题,暂不深究xxx-checkpoint
文件统称为其他文件,暂不深究localTestTopic-x
文件夹则是我们测试使用主题localTestTopic
的日志文件夹了,可以看到
-
进入文件夹:
➜ kafka-logs ls -l localTestTopic-0
total 784
-rw-r--r-- 1 zhangsan admin 72 Oct 24 22:15 00000000000000000014.index
-rw-r--r-- 1 zhangsan admin 41805 Oct 24 22:15 00000000000000000014.log
-rw-r--r-- 1 zhangsan admin 0 Oct 24 22:15 00000000000000000014.timeindex
-rw-r--r-- 1 zhangsan admin 80 Oct 24 22:15 00000000000000000536.index
-rw-r--r-- 1 zhangsan admin 41760 Oct 24 22:15 00000000000000000536.log
-rw-r--r-- 1 zhangsan admin 0 Oct 24 22:15 00000000000000000536.timeindex
-rw-r--r-- 1 zhangsan admin 80 Oct 24 22:15 00000000000000001058.index
-rw-r--r-- 1 zhangsan admin 41760 Oct 24 22:15 00000000000000001058.log
-rw-r--r-- 1 zhangsan admin 0 Oct 24 22:15 00000000000000001058.timeindex
-rw-r--r-- 1 zhangsan admin 80 Oct 24 22:15 00000000000000001580.index
-rw-r--r-- 1 zhangsan admin 41760 Oct 24 22:15 00000000000000001580.log
-rw-r--r-- 1 zhangsan admin 0 Oct 24 22:15 00000000000000001580.timeindex
LogSegment
包含三个主要文件:
LogSegment
都有一个基准偏移量baseOffset
, 用来表示当前LogSegment中第一条消息的偏移量,日志文件和两个索引文件都是根据baseOffset
命名的,文件名称固定20位整数,不够用0填充。以第二个分段00000000000000000536.log
为例:
00000000000000000536.log
表示当前LogSegment
中第一条消息偏移量为53614 - 535
日志索引包含两个: 偏移量索引和时间戳索引。索引文件建立偏移量(offset)/时间戳(timestamp)到物理地址之间的映射关系,方便快速定位消息所在的物理文件的位置;
kafka中使用稀疏索引的方式构造消息索引,所以索引中不保证每个消息在索引中都有对应物理地址的映射;索引记录的方式由配置参数log.index.interval.bytes
指定,默认4KB。当kafka写入消息大小大于4KB时,偏移量索引文件和时间戳索引文件分别增加一个索引项,记录此刻偏移量/时间戳与消息物理地址的映射,修改此配置,可以改变索引文件中索引项的密度
之前提到日志分段,因为分段的原因可能和索引有关,因此这里总结下日志分段的几个条件:
log.segment.bytes
配置值,默认1G
41824
才能快速出现分段效果,不然1个G的消息够我生产半天的log.roll.ms
或log.roll.hours
参数配置(ms优先级更高,同时出现以ms配置值为准),默认配置后者为168,即7天log.index.size.max.bytes
,默认10MBaseOffset
之间差值大于Integer.MAX_VALUE
偏移量索引即记录偏移量(offset)到消息物理位置的映射。每个索引项大小为8个字节,分为两部分:
relative offset
: 相对偏移量,即消息的绝对偏移量和基础偏移量BaseOffset
的差值,占4个字节
offset
本身为8个字节,这里保存相对位移可以降低索引文件的大小position
: 物理地址,即消息在日志分段文件中对应的物理地址,占4个字节仍然以00000000000000000536.index
为例,使用kafka自带命令kafka-dump-log
可以解析日志文件:
➜ localTestTopic-0 /usr/local/Cellar/kafka/3.0.0/bin/kafka-dump-log --files 00000000000000000536.index
Dumping 00000000000000000536.index
offset: 588 position: 4160
offset: 640 position: 8320
offset: 692 position: 12480
offset: 744 position: 16640
offset: 796 position: 20800
offset: 848 position: 24960
offset: 900 position: 29120
offset: 952 position: 33280
offset: 1004 position: 37440
offset: 1056 position: 41600
好吧,这里解析出来的已经是绝对偏移量了。。。总之解析出来后索引文件分为两列,第一列是偏移量,第二列则是该偏移量在日志文件中的物理位置了
kafka在查找某偏移量时使用偏移量索,假设我们现在需要查找partition = 0, offset = 745的消息,其过程如下:
offset = 745
在00000000000000000536
分段中
BaseOffset
,确定分段会先经过跳表查询offset = 745
在744 - 796
之间,kafka根据offset = 744
对应的物理位置再去日志文件中查找
offset:744 position: 16640
开始顺序查找,知道找到offset = 745
的消息时间戳索引记录时间戳(timestamp)到相对偏移量(relativeOffset)之间的映射。每个索引项大小为12个字节,包含两个部分:
这里不再过多介绍时间戳索引的细节,但是有一点是明确的,即时间戳索引是以偏移量索引为前提的,在时间戳中确定偏移量后,必须再从偏移量索引中才能知道消息的具体位置
kafka日志存储在磁盘中,随着时间推移日志文件必然越来越大,kafka提供两种方式清理日志文件:
broker参数log.cleanup.policy
可设置日志清理策略,delete
表示日志删除,compact
为日志压缩,默认前者
日志删除有多种策略:
log.retention.ms/minutes/hours
三个参数可指定保留时间,ms优先级最高,默认hours = 168,即保留7天log.retention.bytes
设置总大小阈值,默认为-1,表示可以无穷大logStartOffset
时,日志分段被删除
logStartOffset
可以被管理员修改,从而指定删除某些分段日志压缩针对所有的历史消息,根据key对消息进行合并,保留相同key的最后一条消息
日志压缩适用于某些场景,必须适用kafka记录某些状态,这种情况不用记录过程,保留最终状态是可行的;但不是所有场景都适用,比如相同key的消息通过不同的字段划分不同的行为,这时合并消息就不是理想的方案了
kafka使用磁盘存储消息数据,一般情况下我们认为磁盘的读写速率较低。kafka在磁盘存储的前提下,可以保持高吞吐,主要原因在以下三个方面
日志存储是基于日志文件的追加,所以在单个日志分段上日志文件的写入是顺序写入;而磁盘顺序写入的效率比内存随机写入效率更高,所以在写入效率上kafka依然可以保持高效
页缓存是操作系统实现的磁盘缓存,使用页缓存可以减少对磁盘磁盘I/O的操作。当一个进程准备去读磁盘文件时,会首先查看页缓存是否存在,如果存在则直接读取数据并返回,否则再从磁盘获取数据,并更新到页缓存中;同样在数据写入磁盘时,也会查看对应的数据页是否在页缓存中,如果有则直接更新页缓存,没有则从磁盘中读取该页,然后写入页缓存;
被写入的数据页就变成了脏页,操作系统会定时将脏页同步到磁盘中
kafka没有在进程中管理缓存,而是将缓存行为完全交给操作系统,一方面可以简化进程的处理逻辑,提高处理效率,同时可以不用考虑进程重启后的缓存丢失;另一方面,可以利用操作系统页缓存的特性,提高磁盘读写效率。
所以,如果生产者和消费者的速率相差不多,那么写入和读取几乎可以通过页缓存完成,进行极少的磁盘操作,可以达到很高的吞吐量;
零拷贝是指将数据从磁盘文件中直接复制到网卡设备中,而无需经过应用程序。零拷贝技术可以减少文件被拷贝的次数,同时不用在用户态和内核态之间转换,提高文件传输效率。
假设我们将磁盘文件A通过网络传输给用户,在使用零拷贝前其过程如下:
Read Buffer
中Read Buffer
数据从内核态复制到用户态的进程内存中可以看到,在使用零拷贝之前,文件经过四次复制,并两次经过用户态和内核态的切换;而文件进入用户态后基本上没有任何处理,就被直接复制到内核buffer中,可见经过用户空间的这两次复制是多余的,因此,在使用零拷贝后:
零拷贝通过DMA(Direct Memory Access)将文件复制到内核空间的Read Buffer
中,随后不经过用户空间,直接复制到网卡待发送。经过零拷贝后,文件的准备过程减少了两次文件复制,并不经过用户态和内核态的转换,提高了文件发送效率