Kafka 2.2.0 服务端消息存储方式

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.mslog.roll.hours参数的值(前者参数优先级大于后者),默认为168小时,7天。
  • 索引文件大小达到log.index.size.max.bytes的值,该值默认为10MB
  • 追加的消息的偏移量与当前日志分段的差值大于Integer.MAX_VALUE
    在这里插入图片描述
    Kafka在Linux系统中默认将日志存储在目录/tmp/kafka-logs(可通过参数log.dirlog.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都不能够写入消息,只能进行读取操作。我们称最后一个LogSegmentActiveLogSegment,当ActiveLogSegment满足一定的条件时,就需要再创建一个新的LogSegment并作为新的ActiveLogSegment

LogSegment除了包含.log文件、.index文件和timeindex文件,还有可能包含.deleted.cleaned.swap等临时文件,也有.snapshot.txnindexleader-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版本而言这个值为2
  • CRC32:通过CRC32算法得出的校验值。
  • Attributes:消息属性,低3位表示压缩格式。第4位表示时间戳类型,第5位标识此RecordBatch是否在事务当中,第6位表示是否是控制消息,控制消息用于Kafka事务机制。
  • Last Offset DeltaRecordBatch最后一个Recordoffset delta。用于保证Record组装的正确性。
  • First Timestamp:起始消息时间戳,也就是第一个Record的时间戳。
  • Max TimestampRecordBatch中最大的时间戳,一般情况下是最后一个Record的时间戳。
  • Producer ID:生产者ID,用于实现事务和幂等特性。
  • producer epoch:同样用于实现事务和幂等特性。
  • first sequence:同样用于实现事务和幂等特性。
  • Records CountRecord总数。

Record中的字段采用了varint(15字节)或者`varlong`(110字节)的形式,这种类型的字段可以根据该字段具体的大小来确定占用空间。其字段解释如下:

  • Length:该Record(消息)的总长度
  • attributes:已弃用,占用1字节大小,未来版本可能会利用此字段。
  • timestamp delta:时间戳增量。可通过RecordBatch中的First Timestamp字段运算出该消息的时间戳。
  • offset delta:位移增量。保存与RecordBatchFirst 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对象时是可以指定该时间戳的)。

当给定一个时间戳 T T ,要求查找大于该时间戳的所有消息时,查找过程是这样的:

  1. T T 与每个日志分段中的最大时间戳逐一对比,直到找到不小于 T T 的最大时间戳 T m a x T_{max} 所对应的日志分段。 T m a x T_{max} 的值等于最后一条索引项的时间戳。
  2. 找到对应的日志分段后,根据二分法查找到不小于 T T 的最大索引项。
  3. 找到索引项后,根据索引项中relativeOffset字段找到偏移量索引文件中的索引项,然后根据偏移量索引项找到消息的物理位置。
    在这里插入图片描述
发布了117 篇原创文章 · 获赞 96 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/abc123lzf/article/details/100748461
今日推荐