kafka之深入理解日志存储

3,781 阅读9分钟

kafka消息是存储在磁盘上的,具体来说,就是以日志的形式进行存储的。本文将会详细讲解kafka日志存储相关知识点,带你深入理解日志存储。

文章内容参考《深入理解Kafka:核心设计与实践原理》,欢迎大家购买阅读。

文件目录布局

前面介绍过,kafka的主题可以对应多个分区,而每个分区可以有多个副本。可以认为,副本才是真实存储消息的物理存在。如果再进一步,副本上的消息究竟是怎么存储的呢?实际上,每个副本都是以日志(Log)的形式存储消息的。同时,为了解决单一日志文件过大的问题,kafka采用了日志分段(LogSegment)的形式进行存储。所谓日志分段,就是当一个日志文件大小到达一定条件之后,就新建一个新的日志分段,然后在新的日志分段写入数据。

为了加快消息的检索,每个日志分段除了真实的数据日志文件(.log后缀)之外,还有对应的2个索引文件:偏移量索引文件(.index)和时间戳索引文件(.timeindex)。整个主题、分区、副本、日志关系如下:

image.png

每个日志分段都有一个基准偏移量(baseOffset,20位数字),表示当前日志分段的第一条消息的offset。日志分段相关文件名就用基准偏移量进行命名。比如:

00000000000000000000.log
00000000000000000000.index
00000000000000000000.timeindex

00000000000000000133.log
00000000000000000133.index
00000000000000000133.timeindex

日志格式的演变

v0版本

image.png Kafka 0.10.0之前采用了v0版本消息格式。如上图所示,v0版本主要由日志头(LOG_OVERHEAD)和消息记录(RECORD) 组成。消息头包括消息偏移量(offset)和消息大小(message size)组成。消息记录则由以下部分组成:

  1. crc32(4B):crc32校验值。
  2. magic(1B):消息格式版本号,v0版本值为0。
  3. attritubes(1B):消息属性。低三位表示压缩类型。
  4. key length(4B):表示消息key的长度。如果是-1,表示有设置key。
  5. key:消息key,可选。
  6. value length(4B):实际消息体长度。
  7. value:消息体。

v1版本

Kafka在0.10.0到0.11.0版本之间采用了v1版本消息格式。v1版本相比于v0版本,只多了一个timestamp字段,如下图所示。 image.png

消息压缩

实际上,Kafka发送消息的时候并不是一条一条消息进行发送,而是通过消息集(message set)的方式批量发送。为了实现更好的性能,Kafka支持了消息压缩功能,具体来讲就是压缩消息集。一般来说,压缩解压过程是:生产者发送压缩消息集,broker端保存压缩消息集,消费者解压消息集进行消费。这样就能减少网络IO消耗,提升整体性能。

生产者可以通过配置compression.type参数来开启压缩功能,可配置的值为gzipsnappylz4,分别对应了三种压缩算法。压缩消息时,将整个消息集进行压缩作为一个内层消息,作为外城消息的value。并且将原来消息集最大的offset作为外层消息的offset,而内层消息的offset永远从0开始。

image.png

v2版本

Kafka从0.11.0版本开始使用v2版本消息格式,v2版本的改动非常大。首先是引入了变长整形(varinits),做到了数值越小,占用的字节数就越少,从而大大节省了空间。

v2版本中的消息集换成了Record Batch,其内部也包含一条或者多条消息。v2完整消息格式如下: image.png

首先看看RecordBatch的关键字段:

  • first offset:表示当前RecordBatch的起始偏移量。
  • length:计算从partition leader epoch到末尾的长度。
  • partition leader epoch:分区leader纪元,可以看做是分区leader的版本号或者更新次数。
  • magic:消息格式版本号,v2版本是2。
  • crc32:crc32校验值。
  • attributes:消息属性,这里占用2个字节。低三位表示压缩格式,第4位表示时间戳类型,第5位表示此RecordBatch是否在事务中,第6位表示是否为控制消息。
  • last offset delta:RecordBatch中最后一个Record的offset与first offset的差值。主要用于broker确保RecordBatch中Recoord组装的正确性。
  • first timestamp:RecordBatch中第一条Record的时间戳。
  • max timestamp:RecordBatch中最大的时间戳。一般情况是最后一条Record的时间戳。
  • producer id:PID,用来支持事务和幂等。暂不解释。
  • producer epoch:用来支持事务和幂等。暂不解释。
  • first sequeue:用来支持事务和幂等。暂不解释。
  • records count:RecordBatch中record的个数。
  • records:消息记录集合

接下来看看Record的关键字段:

  • length:消息总长度
  • attributes:弃用。这里仍然占用了1B大小,供未来扩展。
  • timestamp delta:时间戳增量。
  • offset delta:偏移量增量。保存与RecordBatch起始偏移量的差值。
  • key length:消息key长度。
  • key value:消息key的值。
  • value length:消息体的长度。
  • value:消息体的值。
  • headers:消息头。用来支持应用级别的扩展。

最后看看Header的关键字段:

  • header key length:消息头key的长度。
  • header key:消息头key的值。
  • header value length:消息头值的长度。
  • header value:消息头的值。

日志索引

日志分段文件包括了2个索引文件:偏移量索引文件和时间戳索引文件。其中,偏移量索引文件用来建立消息偏移量与物理地址之间的映射关系,时间戳索引文件则是用来建立时间戳与偏移量的映射关系。索引文件是以稀疏索引的方式构建的。

偏移量索引文件中的偏移量是单调递增的,查询时通过二分法快速定位到小于指定偏移量的最大偏移量,然后根据对应的物理地址继续查找对应偏移量的消息。时间戳索引中的时间戳也是单调递增的,查询时也是先定位到小于指定时间的最大偏移量,然后再使用偏移量索引继续进行查询。

偏移量索引

image.png 偏移量索引项格式如上所示。每个索引项占用8个字节,分为两部分:

  1. relativeOffset:相对偏移量,4字节,表示消息相对于baseOffset的偏移量。当前文件的文件名即为baseOffset的值。
  2. position:物理地址,4字节,表示消息在分段日志文件的物理位置。

根据偏移量索引查找消息过程如下:

  1. 使用二分法快速定位偏移量索引文件。因为文件名就是baseOffset,所以可以快速定位到该文件。
  2. 使用二分法快速定位索引项。具体来说就是在索引文件中找到小于当前偏移量的最大偏移量的索引项。
  3. 从上一步中索引项的物理地址开始,顺序查找出对应偏移量的消息。

时间戳索引

image.png 时间戳索引项格式如上所示。每个索引项占用12个字节,分为两部分:

  1. timestamp:当前日志分段的最大时间戳。
  2. relativeOffset:时间戳对应消息的相对偏移量。

根据时间戳查找对应消息的过程如下:

  1. 根据时间戳到每个日志分段文件中最大的时间戳逐一比较,定位到时间戳索引文件。因为时间戳索引文件也是使用了baseOffset命名,所以没办法直接通过二分法快速定位到。
  2. 使用二分法快速定位时间戳索引项。
  3. 根据上一步中的偏移量,通过偏移量索引查出对应的消息。

日志清理

Kafka将消息存在磁盘中,为了控制占用磁盘空间不断增加,Kafka支持了日志清理功能。kafka提供了两种日志清理策略:

  1. 日志删除(Log Retention):按照一定的保留策略直接删除不符合条件的日志分段。
  2. 日志压缩(Log Compaction):针对每个消息的key进行整合,对于相同key的不同value值,只保留最后一个版本。

日志删除

Kafka会周期性的检测并删除不符合保留条件的日志分段文件。这个周期可以通过broker端参数log.tetention.check.interval.ms配置,默认5分钟。当前日志分段的保留策略有三种:基于时间的保留策略、基于日志大小的保留策略和基于日志起始偏移量的保留策略。

基于时间

日志删除任务会检查当前日志文件保留时间超过设置阈值的日志分段集合,然后对应的所有文件加上.deleted后缀,最后由一个延迟任务来删除这些文件。阈值可以通过broker端参数log.retention.mslog.retention.minuteslog.retention.hours设置,默认是7天。

基于日志大小

日志删除任务会检查当前日志大小超过设置阈值的日志分段集合,然后对应的所有文件加上.deleted后缀,最后由一个延迟任务来删除这些文件。阈值可以通过broker端参数log.retention.bytes设置,默认值为-1,表示无穷大。

基于日志起始偏移量

日志删除任务会检查当前日志分段的下一个日志分段的baseOffset小于等于logStartOffset的日志分段集合,然后对应的所有文件加上.deleted后缀,最后由一个延迟任务来删除这些文件。

日志压缩

日志压缩是在默认日志删除规则之外提供的一种清理数据的方式。日志压缩对于相同key的不同value值,只保留最后一个版本。

磁盘存储

kafka使用磁盘来存储消息。大家都知道,磁盘的读写性能是相对比较差的,那么使用磁盘的kafka做到高性能的呢?

顺序写盘

实际上,顺序写盘的性能并不差,是随机写盘的6000倍以上。kafka在设计时采用了文件追加的方式来写入消息,就是使用了顺序写盘来保证高性能。

页缓存

页缓存是操作系统支持的磁盘缓存,以此来减少磁盘I/O操作。简单来说,就是操作系统基于页为单位把磁盘中的热点数据缓存在内存中,进而将大量的I/O操作转换为内存操作,大大提高了整体性能。kafka中大量使用了页缓存,消息都是先写入页缓存中,然后才由操作系统写入到磁盘中。

零拷贝

零拷贝是指将数据直接从磁盘文件复制到网卡设备中,不需要经过应用程序。零拷贝大大提高了应用程序的性能,减少了内核和用户模式的上下文切换。kafka采用了零拷贝技术来进一步提升性能。