《深入理解Kafka:核心设计与实践原理》-第5章 日志存储

5.1 文件目录布局

  • 一个分区对应一个日志(Log)。为了防止 Log 过大,Kafka又引入了日志分段(LogSegment)的概念,将Log切分为多个LogSegment。每个LogSegment 对应于磁盘上的一个日志文件和两个索引文件,偏移量索引文件(以“.index”为文件后缀)和时间戳索引文件(以“.timeindex”为文件后缀)。
  • 向Log 中追加消息时是顺序写入的,只有最后一个 LogSegment 才能执行写入操作,在此之前所有的 LogSegment 都不能写入数据。
  • 每个 LogSegment 都有一个基准偏移量 baseOffset,用来表示当前 LogSegment中第一条消息的offset。日志文件和两个索引文件都是根据基准偏移量(baseOffset)命名的,名称固定为20位数字,没有达到的位数则用0填充。

5.2 日志格式的演变

5.2.1 v0版本

  • 每个RECORD(v0和v1版)必定对应一个offset和message size。每条消息都有一个offset 用来标志它在分区中的偏移量,这个offset是逻辑值,而非实际物理偏移值,message size表示消息的大小,这两者在一起被称为日志头部(LOG_OVERHEAD),固定为12B。
  • 与消息对应的还有消息集的概念,消息集中包含一条或多条消息,消息集不仅是存储于磁盘及在网络上传输(Produce&Fetch)的基本形式,也是压缩基本单元。
  • V0版本的消息格式V0版本的消息格式
  • crc32(4B):crc32校验值。校验范围为magic至value之间。
  • magic(1B):消息格式版本号,分为0,1,2。
  • attributes(1B):消息的属性。总共占1个字节,低3位表示压缩类型:0表示NONE、1表示GZIP、2表示SNAPPY、3表示LZ4(LZ4自Kafka 0.9.x引入),其余位保留。
  • key length(4B):表示消息的key的长度。如果为-1,则表示没有设置key,即key=null。
  • key:可选,如果没有key则无此字段。
  • value length(4B):实际消息体的长度。如果为-1,则表示消息为空。
  • value:消息体。可以为空,比如墓碑(tombstone)消息。

5.2.2 v1版本

比V0版本增加了时间戳。CreateTime,即采用生产者创建消息时的时间戳。

5.2.3 消息压缩

  • 生产者发送的压缩数据在broker中也是保持压缩状态进行存储的,消费者从服务端获取的也是压缩的消息,消费者在处理消息之前才会解压消息,这样保持了端到端的压缩。
  • 消息集进行压缩作为内层消息(inner message),内层消息整体作为外层(wrapper message)的 value
  • 消息压缩消息压缩
  • 外层消息保存了内层消息中最后一条消息的绝对位移(absolute offset),绝对位移是相对于整个分区而言的。
  • 一条内部位移a的绝对位移求取公式:
    Related_a(a的相对位移),Absolute_a(a的绝对位移),in_a(a的内部位移),in_last(last的内部位移)
    Related_a = in_a - in_last
    Absolute_a = Absolute_last - Related_a

5.2.4 变长字段 (非重点)

  • Kafka从0.11.0版本开始所使用的消息格式版本为v2,引入了变长整型(Varints)和ZigZag编码。
  • Varints是使用一个或多个字节来序列化整数的一种方法。数值越小,其占用的字节数就越少。
  • Varints中的每个字节都有一个位于最高位的msb位(most significant bit),除最后一个字节外,其余msb位都设置为1,最后一个字节的msb位为0。
  • 除msb位外,剩余的7位用于存储数据本身,这种表示类型又称为Base 128。通常而言,一个字节8位可以表示256个值,所以称为Base 256,而这里只能用7位表示,2的7次方即128。
  • Varints使用了ZigZag的编码方式。ZigZag编码以一种锯齿形(zig-zags)的方式来回穿梭正负整数,将带符号整数映射为无符号整数,这样可以使绝对值较小的负数仍然享有较小的Varints编码值,比如-1编码为1,1编码为2,-2编码为3
  • Varints中的一个字节中只有7位是有效数值位,即只能表示128个数值,转变成绝对值之后其实质上只能表示64个数值。比如对消息体长度而言,其值肯定是大于等于0的正整数,那么一个字节长度的Varints最大只能表示64。

5.2.5 v2版本

  • v2版本节省了很多空间,因为它将多个消息(Record)打包存放到单个RecordBatch中,又通过Varints编码极大地节省了空间。

5.3 日志索引

  • 每个日志分段文件对应了两个索引文件,来提高查找消息的效率。偏移量索引文件用来建立消息偏移量(offset)到物理地址之间的映射关系,方便快速定位消息所在的物理文件位置;时间戳索引文件则根据指定的时间戳(timestamp)来查找对应的偏移量信息。
  • Kafka 中的索引文件以稀疏索引(sparse index)的方式构造消息的索引,它并不保证每个消息在索引文件中都有对应的索引项。
  • 每当写入一定量(由 broker 端参数 log.index.interval.bytes指定,默认值为4096,即4KB)的消息时,偏移量索引文件和时间戳索引文件分别增加一个偏移量索引项和时间戳索引项

5.3.1 偏移量索引

  • 每个索引项占用8个字节,分为两个部分。(1)relativeOffset:相对偏移量,表示消息相对于baseOffset 的偏移量,占用4 个字节,当前索引文件的文件名即为baseOffset的值。(2)position:物理地址,也就是消息在日志分段文件中对应的物理位置,占用4个字节。

  • 如何使用偏移量索引:以查找偏移量为268的消息为例。
    首先肯定是定位到baseOffset为251的日志分段,然后计算相对偏移量relativeOffset=268-251=17,之后再在对应的索引文件中找到不大于17的索引项,最后根据索引项中的position定位到具体的日志分段文件位置开始查找目标消息。(下图为用相对偏移量查找消息)
    在这里插入图片描述

  • 如何查找baseOffset 为251的日志分段的呢?这里并不是顺序查找,而是用了跳跃表的结构。Kafka 的每个日志对象中使用了ConcurrentSkipListMap来保存各个日志分段,每个日志分段的baseOffset作为key,这样可以根据指定偏移量来快速定位到消息所在的日志分段。

  • Kafka 强制要求索引文件大小必须是索引项大小的整数倍,对偏移量索引文件而言,必须为8的整数倍。

5.3.2 时间戳索引

  • 每个索引项占用12个字节,分为两个部分。(1)timestamp:当前日志分段最大的时间戳。(2)relativeOffset:时间戳所对应的消息的相对偏移量。
  • 与偏移量索引文件相似,时间戳索引文件大小必须是索引项大小(12B)的整数倍,如果不满足条件也会进行裁剪。
  • 如何使用时间戳索引:以查找指定时间戳targetTimeStamp=1526384718288开始的消息为例
    步骤1:将targetTimeStamp和每个日志分段中的最大时间戳largestTimeStamp逐一对比,直到找到不小于 targetTimeStamp 的 largestTimeStamp 所对应的日志分段。
    步骤 2:找到相应的日志分段之后,在时间戳索引文件中使用二分查找算法查找到不大于targetTimeStamp的最大索引项,即[1526384718283,28],如此便找到了一个相对偏移量28。
    步骤3:在偏移量索引文件中使用二分算法查找到不大于28的最大索引项,即[26,838]。
    步骤4:从步骤1中找到日志分段文件中的838的物理位置开始查找不小于targetTimeStamp的消息。
    下图为使用指定时间戳查找消息的过程
    在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43260719/article/details/121310619