Kafka通过主题(topic
)将消息归类,各个主题相互独立,每个主题包含一个或多个分区(partition
),分区数量可以动态修改,Kafka保证消息在一个分区中是有序的,分区中的每个消息都有一个唯一的偏移量(offset
)。一个分区同时可以包含多个分区副本:一个leader
副本和一或多个follower
副本,只有leader
副本负责消息的接收和发送,其余副本负责与leader
副本保持同步,从而达到高可用。
Kafka通过分区来实现水平扩展,消息可以均匀地分摊到每个分区中,消费者可以消费其中一到多个分区中的消息,从而实现更高的消息吞吐量。
位于文件系统中的结构
Kafka在接收消息的过程中,会将消息持久化到磁盘上。如果不考虑多副本的情况,一个分区就对应一个日志(Log
),分区中每添加一个消息,消息就会追加到日志上。为了防止Log
过大,Kafka将一个Log
切分为多个日志分段(LogSegment
),每个LogSegment
对应一个日志文件和两个索引文件(如果使用事务的话还有事务索引文件),Log
在磁盘中是以文件夹的方式存在的,文件夹中的文件就包含了LogSegment
。
日志的切分需要满足以下几个条件:
- 当前日志分段文件的大小超过了
log.segment.bytes
配置的值。该值默认为1GB - 当前日志分段中消息的最大时间戳与当前系统时间戳相差大于
log.roll.ms
或log.roll.hours
参数的值(前者参数优先级大于后者),默认为168小时,7天。 - 索引文件大小达到
log.index.size.max.bytes
的值,该值默认为10MB - 追加的消息的偏移量与当前日志分段的差值大于
Integer.MAX_VALUE
Kafka在Linux系统中默认将日志存储在目录/tmp/kafka-logs
(可通过参数log.dir
或log.dirs
指定,后者可指定多个存储目录)下。其目录布局如下:
drwxr-xr-x. 53 root root 4096 9月 11 22:20 .
drwxrwxrwt. 11 root root 232 9月 11 20:55 ..
-rw-r--r--. 1 root root 0 9月 11 20:56 cleaner-offset-checkpoint
drwxr-xr-x. 2 root root 141 9月 11 20:56 __consumer_offsets-0
drwxr-xr-x. 2 root root 141 9月 11 20:56 __consumer_offsets-1
# 省略若干个__consumer_offsets文件夹...
drwxr-xr-x. 2 root root 141 9月 11 20:56 __consumer_offsets-49
-rw-r--r--. 1 root root 0 9月 11 20:56 .lock
-rw-r--r--. 1 root root 4 9月 11 22:19 log-start-offset-checkpoint
-rw-r--r--. 1 root root 54 9月 11 20:56 meta.properties
-rw-r--r--. 1 root root 1210 9月 11 22:19 recovery-point-offset-checkpoint
-rw-r--r--. 1 root root 1210 9月 11 22:20 replication-offset-checkpoint
# 自定义主题
drwxr-xr-x. 2 root root 141 9月 11 20:56 topic-test-0
每个Log
对应一个文件夹,如果一个主题名称为topic-test
,分区编号为0,那么该文件夹名称为topic-test-0
。该文件夹下有多个.log
文件、.index
文件和timeindex
文件,这些文件的名称为64位的纯数字,一共20位,表示该日志文件的基准偏移量(BaseOffset
)。
[root@kafka0 __consumer_offsets-8]# ls -al
总用量 8
drwxr-xr-x. 2 root root 141 9月 11 20:56 .
drwxr-xr-x. 53 root root 4096 9月 11 21:37 ..
-rw-r--r--. 1 root root 10485760 9月 11 20:56 00000000000000000000.index
-rw-r--r--. 1 root root 0 9月 11 20:56 00000000000000000000.log
-rw-r--r--. 1 root root 10485756 9月 11 20:56 00000000000000000000.timeindex
-rw-r--r--. 1 root root 8 9月 11 20:56 leader-epoch-checkpoint
由于消息是以追加的方式顺序写入日志的,所以只有最后一个LogSegment
才能执行写入操作,前面所有的LogSegment
都不能够写入消息,只能进行读取操作。我们称最后一个LogSegment
为ActiveLogSegment
,当ActiveLogSegment
满足一定的条件时,就需要再创建一个新的LogSegment
并作为新的ActiveLogSegment
。
LogSegment
除了包含.log
文件、.index
文件和timeindex
文件,还有可能包含.deleted
、.cleaned
、.swap
等临时文件,也有.snapshot
、.txnindex
、leader-epoch-checkpoint
文件。
消息存储在日志中的格式
Kafka从0.8版本发展到现在,消息格式也经历了3个版本:v0、v1和v2,这里我们只介绍最新的v2版本。
Kafka消息以消息集(RecordBatch
)的形式存储在日志中,每个消息集包含一条或者多条消息,下面是RecordBatch
存储在日志中的结构
RecordBatch
包含以下字段:
first offset
:当前RecordBatch
起始位移。Length
:从partition leader epoch
开始的字段长度。partition leader epoch
:分区leader
纪元。magic
:消息格式版本号,对于v2版本而言这个值为2CRC32
:通过CRC32算法得出的校验值。Attributes
:消息属性,低3位表示压缩格式。第4位表示时间戳类型,第5位标识此RecordBatch
是否在事务当中,第6位表示是否是控制消息,控制消息用于Kafka事务机制。Last Offset Delta
:RecordBatch
最后一个Record
的offset delta
。用于保证Record
组装的正确性。First Timestamp
:起始消息时间戳,也就是第一个Record
的时间戳。Max Timestamp
:RecordBatch
中最大的时间戳,一般情况下是最后一个Record
的时间戳。Producer ID
:生产者ID,用于实现事务和幂等特性。producer epoch
:同样用于实现事务和幂等特性。first sequence
:同样用于实现事务和幂等特性。Records Count
:Record
总数。
Record
中的字段采用了varint
(15字节)或者`varlong`(110字节)的形式,这种类型的字段可以根据该字段具体的大小来确定占用空间。其字段解释如下:
Length
:该Record
(消息)的总长度attributes
:已弃用,占用1字节大小,未来版本可能会利用此字段。timestamp delta
:时间戳增量。可通过RecordBatch
中的First Timestamp
字段运算出该消息的时间戳。offset delta
:位移增量。保存与RecordBatch
中First Offset
的差值。headers
:消息头。采用Map的方式存储
索引文件
每个日志分段有两个索引文件,用于提高查找消息的效率。偏移量索引文件(.index
)记录了偏移量和对应消息记录在日志分段文件中的物理位置的映射关系。时间戳索引文件(.timeindex
)则记录了时间戳和对应消息记录在日志分段文件中的物理位置的映射关系。
Kafka是以稀疏索引的方式来构造消息的索引,它并不会保证每个消息在索引文件中都有对应的唯一索引项。每当写入一定量(默认4KB,可通过参数log.index.interval.bytes
自定义)的消息时,就会在索引文件中构建一个索引项。
Kafka通过java.nio.MappedByteBuffer
(通过操作系统的mmap
机制实现)将索引文件映射到内存中来加快检索速度。偏移量索引文件的索引项是递增的,在检索时通过二分查找的方式来定位索引项,时间戳索引文件也同样是递增的方式存储的,检索时同样采取二分法。
偏移量索引文件
偏移量索引文件由索引项组成,每个索引项占用8字节,分为两个部分:
relativeOffset
:相对偏移量,将该值和baseOffset
(等于文件名)相加即可得到消息的实际偏移量。占用4字节position
:消息的物理地址,占用4字节。
通过使用相对偏移量和baseOffset
来计算实际偏移量,可以减少索引文件的空间占用,提高读取、写入性能。
Kafka提供了kafka-dump-log.sh
脚本来解析日志文件和索引文件:
[root@kafka0 bin]# ./kafka-dump-log.sh --files /tmp/kafka-logs/topic-test-0/00000000000000000000.index
Dumping /tmp/kafka-logs/topic-test-0/00000000000000000000.index
offset:6 position:102
offset:14 position:380
offset:22 position:604
时间戳索引文件
时间戳索引文件同样由索引项组成,每个索引项占用12个字节,分为两个部分:
timestamp
:当前日志分段的最大时间戳,占用8个字节relativeOffset
:对应消息的相对偏移量,不等同于偏移量索引文件的relativeOffset
在Kafka向时间戳索引文件追加索引项时,必须保证时间戳大于之前添加的时间戳。如果Broker参数log.message.timestamp.type
设置为LogAppendTime
,那么时间戳索引项都能够成功添加并保证其递增,如果是CreateTime
则无法保证(生产者在构造ProducerRecord
对象时是可以指定该时间戳的)。
当给定一个时间戳 ,要求查找大于该时间戳的所有消息时,查找过程是这样的:
- 将 与每个日志分段中的最大时间戳逐一对比,直到找到不小于 的最大时间戳 所对应的日志分段。 的值等于最后一条索引项的时间戳。
- 找到对应的日志分段后,根据二分法查找到不小于 的最大索引项。
- 找到索引项后,根据索引项中
relativeOffset
字段找到偏移量索引文件中的索引项,然后根据偏移量索引项找到消息的物理位置。