kafka专题笔记 - 日志存储

文件目录布局

下面以本地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

其中:

  1. __consumer_offsets-xx文件夹是kafka存储消费者offset的默认主题,暂不深究
  2. xxx-checkpoint文件统称为其他文件,暂不深究
  3. 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
  1. 分区内(文件夹)内的日志文件分为多个Segment,且每个LogSegment包含三个主要文件
    • xxx.log: 消息内容主文件,记录消息内容信息
    • xxx.index: 偏移量索引文件,记录偏移量到消息位置的映射
    • xxx.timeindex: 时间戳索引文件,记录时间戳到偏移量的映射
  2. 每个LogSegment都有一个基准偏移量baseOffset, 用来表示当前LogSegment中第一条消息的偏移量,日志文件和两个索引文件都是根据baseOffset命名的,文件名称固定20位整数,不够用0填充。以第二个分段00000000000000000536.log为例:
    • 00000000000000000536.log表示当前LogSegment中第一条消息偏移量为536
    • 同时可以推断出第一个分段中的偏移量为14 - 535

日志格式

日志索引

日志索引包含两个: 偏移量索引时间戳索引。索引文件建立偏移量(offset)/时间戳(timestamp)到物理地址之间的映射关系,方便快速定位消息所在的物理文件的位置;

kafka中使用稀疏索引的方式构造消息索引,所以索引中不保证每个消息在索引中都有对应物理地址的映射;索引记录的方式由配置参数log.index.interval.bytes指定,默认4KB。当kafka写入消息大小大于4KB时,偏移量索引文件和时间戳索引文件分别增加一个索引项,记录此刻偏移量/时间戳与消息物理地址的映射,修改此配置,可以改变索引文件中索引项的密度

日志分段的条件

之前提到日志分段,因为分段的原因可能和索引有关,因此这里总结下日志分段的几个条件:

  1. 当前日志分段文件的大小超过broker端参数log.segment.bytes配置值,默认1G
    • 在文件目录布局章节为了展示分段效果,我把该配置改为41824才能快速出现分段效果,不然1个G的消息够我生产半天的
  2. 当前日志分段文件中消息的最大时间戳和当前系统的时间戳差值大于log.roll.mslog.roll.hours参数配置(ms优先级更高,同时出现以ms配置值为准),默认配置后者为168,即7天
  3. 索引文件大小超过broker端参数log.index.size.max.bytes,默认10M
  4. 追加消息的偏移量与当前日志分段的BaseOffset之间差值大于Integer.MAX_VALUE
    • 这是由偏移量索引的格式所决定的,后面会提到

偏移量索引

偏移量索引即记录偏移量(offset)到消息物理位置的映射。每个索引项大小为8个字节,分为两部分:

  1. relative offset: 相对偏移量,即消息的绝对偏移量和基础偏移量BaseOffset的差值,占4个字节
    • offset本身为8个字节,这里保存相对位移可以降低索引文件的大小
    • 上述日志分段条件4是受相对偏移量影响,最多只能保存4B差值的偏移量
  2. 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的消息,其过程如下:

  1. 第一步,确定日志分段:先从跳表中确定offset = 74500000000000000000536分段中
    • kafka使用跳跃表缓存每个日志分段的BaseOffset,确定分段会先经过跳表查询
  2. 第二步,确定偏移量索引项:通过二分查找,确认offset = 745744 - 796之间,kafka根据offset = 744对应的物理位置再去日志文件中查找
    • kafka会在索引文件中找到不大于指定偏移量的最大偏移量对应的物理位置,去日志文件中顺序查找
  3. 第三步,确定消息位置:到日志文件中后,从offset:744 position: 16640开始顺序查找,知道找到offset = 745的消息

时间戳索引

时间戳索引记录时间戳(timestamp)到相对偏移量(relativeOffset)之间的映射。每个索引项大小为12个字节,包含两个部分:

  1. 时间戳timestamp: 当前日志分段最大的时间戳,指新增时间戳索引项的那个时刻,日志分段中最大的时间戳(相当于那个时刻的快照数据),大小为8个字节
  2. 相对偏移量relativeOffset: 该时间戳对应的相对偏移量

这里不再过多介绍时间戳索引的细节,但是有一点是明确的,即时间戳索引是以偏移量索引为前提的,在时间戳中确定偏移量后,必须再从偏移量索引中才能知道消息的具体位置

日志清理

kafka日志存储在磁盘中,随着时间推移日志文件必然越来越大,kafka提供两种方式清理日志文件:

  1. 日志删除:按照某种策略,删除不符合条件的日志分段
  2. 日志压缩/瘦身:针对key对日志重新梳理,相同key的消息只保留最后一条
    • 这里不是对日志文件直接进行压缩保存,而是从逻辑上删除日志文件的内容,所以我感觉用“日志瘦身”更形象些

broker参数log.cleanup.policy可设置日志清理策略,delete表示日志删除,compact为日志压缩,默认前者

日志删除

日志删除有多种策略:

  1. 基于时间:如果日志分段中最大时间戳和当前时间的差值大于设定的保留时间,则分段需要清除。保留时间可以通过配置确定:
    • log.retention.ms/minutes/hours三个参数可指定保留时间,ms优先级最高,默认hours = 168,即保留7天
  2. 基于日志大小:如果分区内**日志文件总大小(非单个日志分段文件大小)**超过阈值,则清理最老的日志分段
    • log.retention.bytes设置总大小阈值,默认为-1,表示可以无穷大
  3. 基于日志起始偏移量:当日志分段的偏移量小于日志起始偏移量logStartOffset时,日志分段被删除
    • logStartOffset可以被管理员修改,从而指定删除某些分段

日志压缩/瘦身

日志压缩针对所有的历史消息,根据key对消息进行合并,保留相同key的最后一条消息

日志压缩适用于某些场景,必须适用kafka记录某些状态,这种情况不用记录过程,保留最终状态是可行的;但不是所有场景都适用,比如相同key的消息通过不同的字段划分不同的行为,这时合并消息就不是理想的方案了

磁盘存储

kafka使用磁盘存储消息数据,一般情况下我们认为磁盘的读写速率较低。kafka在磁盘存储的前提下,可以保持高吞吐,主要原因在以下三个方面

顺序写

日志存储是基于日志文件的追加,所以在单个日志分段上日志文件的写入是顺序写入;而磁盘顺序写入的效率比内存随机写入效率更高,所以在写入效率上kafka依然可以保持高效

页缓存

页缓存是操作系统实现的磁盘缓存,使用页缓存可以减少对磁盘磁盘I/O的操作。当一个进程准备去读磁盘文件时,会首先查看页缓存是否存在,如果存在则直接读取数据并返回,否则再从磁盘获取数据,并更新到页缓存中;同样在数据写入磁盘时,也会查看对应的数据页是否在页缓存中,如果有则直接更新页缓存,没有则从磁盘中读取该页,然后写入页缓存;

被写入的数据页就变成了脏页,操作系统会定时将脏页同步到磁盘中

kafka没有在进程中管理缓存,而是将缓存行为完全交给操作系统,一方面可以简化进程的处理逻辑,提高处理效率,同时可以不用考虑进程重启后的缓存丢失;另一方面,可以利用操作系统页缓存的特性,提高磁盘读写效率。

kafka专题笔记 - 日志存储_第1张图片

  1. 生产者通过pwrite()方法,把消息写入page_cache
  2. 消费者通过sendfile()方法,通过零拷贝的方式把消息从page cache传输到broker的Socker buffer

所以,如果生产者和消费者的速率相差不多,那么写入和读取几乎可以通过页缓存完成,进行极少的磁盘操作,可以达到很高的吞吐量

零拷贝

零拷贝是指将数据从磁盘文件中直接复制到网卡设备中,而无需经过应用程序。零拷贝技术可以减少文件被拷贝的次数,同时不用在用户态和内核态之间转换,提高文件传输效率。

假设我们将磁盘文件A通过网络传输给用户,在使用零拷贝前其过程如下:

kafka专题笔记 - 日志存储_第2张图片

  1. 用户发起read()系统调用,将磁盘文件A复制到内核空间的Read Buffer
  2. 同时CPU控制将Read Buffer数据从内核态复制到用户态的进程内存中
  3. 进程发起write()系统调用,将用户态进程内存数据复制到内核态socket buffer中
  4. 内核态数据从Socket Buffer中复制到网卡设备,准备发送

可以看到,在使用零拷贝之前,文件经过四次复制,并两次经过用户态和内核态的切换;而文件进入用户态后基本上没有任何处理,就被直接复制到内核buffer中,可见经过用户空间的这两次复制是多余的,因此,在使用零拷贝后

kafka专题笔记 - 日志存储_第3张图片

零拷贝通过DMA(Direct Memory Access)将文件复制到内核空间的Read Buffer中,随后不经过用户空间,直接复制到网卡待发送。经过零拷贝后,文件的准备过程减少了两次文件复制,并不经过用户态和内核态的转换,提高了文件发送效率

你可能感兴趣的:(kafka,kafka)