FileMessageSet分析

在Kafka中使用FileMessageSet管理日志文件,它对应着磁盘上一个真正的日志文件。FileMessageSet继承了MessaeSet抽象类,MessageSet。保存的数据格式分为三部分:8字节的ofset和4字节的size以及size子集的message 数据,前两个部分被称为LogOverhead。


Kafka使用Message表示消息,Message使用ByteBuffer保存数据,其格式以及各个部分的含义:


CRC32: 消息的校验码,4个字节

magic: 魔数标识,与消息格式有关,取值0或者1.magic为0的时候,消息的offset使用绝对offset且消息格式没有timestamp部分;当magic为1的时候,消息的offset使用相对的offset且消息格式存在timestamp部分,所以magic不同消息长度也不一样。

attributes: 消息的属性,1个字节。其中0-2表示消息使用的压缩类型:0表示为无压缩,1表示gzip压缩,2表示snappy压缩

3表示时间戳类型:0表示创建时间,1表示追加时间

timestamp: 时间戳,其含义由attribute的第三位确定

key length: 消息 key的长度

key: 消息的key

value length: 消息value的长度

value: 消息的value

 

MessageSet定义了两个比较关键的方法:

writeTo: 将当前消息写入channel

iterator: 顺序读取MessageSet的消息

这两个方法说明了MessageSet有顺序读写的消息特性。

 

 

一 FileMessageSet 核心字段

file: 指向磁盘上日志文件

channel:FileChannel类型,用于读写对应的日志文件

start:FileMessageSet对象除了表示一个完整的日志文件,还可以表示日志文件的分片,start表示分片的开始位置

end:表示分片的结束位置

isSlice:表示当前FileMessageSet是否为日志文件的分片

_size:FileMessageSet大小,单位是字节,如果是分片则表示分片大小

二 FileMessageSet核心的方法

2.1 openChannel

打开给定文件的通道,对于Windows NTFS和一些老版本的Linux文件系统,设置preallocate(预分配)为true,我们也可以通过在构造FileMessageSet的时候,指定该文件是否只读,如果mutable为ture,表示可读可写

def openChannel(file: File, mutable: Boolean, fileAlreadyExists: Boolean = false, initFileSize: Int = 0, preallocate: Boolean = false): FileChannel = {
  if (mutable) {// 判断文件是否可写
   
if (fileAlreadyExists) // 如果文件存在,打开可写通道
     
new RandomAccessFile(file, "rw").getChannel()
    else {
      if (preallocate) {// 不能存在且预分配了,初始化文件大小为默认大小为512 * 1025 *1024
       
val randomAccessFile = new RandomAccessFile(file, "rw")
        randomAccessFile.setLength(initFileSize)
        randomAccessFile.getChannel()//打开可写通道
     
}
      else // 如果没有开启预分配,则直接打开可写通道
       
new RandomAccessFile(file, "rw").getChannel()
    }
  }
  else // 如果文件不可写,创建只读的FileChannel
   
new FileInputStream(file).getChannel()
}

 

在FileMessageSet初始化的过程中,会移动FileChannel的position指针,这是为了实现每次写入的消息都在日志文件的尾部,从而避免重启服务后的写入操作覆盖之前的操作。对于新创建的且进行了预分配的日志文件,他的end会出事为0,所以也是从起始写入数据的

 

2.2 append 添加message,进行文件的写入

首先使用ByteBufferMessageSet#writeFullyTo进行消息的添加,添加完后改变FileMessageSet的大小

def append(messages: ByteBufferMessageSet) {
  val written = messages.writeFullyTo(channel)
  _size.getAndAdd(written)// 改变FileMessageSet的大小
}
 
def writeFullyTo(channel: GatheringByteChannel): Int = {
  buffer.mark()
  var written = 0
  while (written < sizeInBytes)
    written += channel.write(buffer)
  buffer.reset()
  written
}

 

2.3查找指定的消息

根据offset查找:

根据给定的offset查找文件中大于或者等于这个给定offset的最后一个offset的实际物理位置和消息大小

比如给定offset为10,文件在offset为10的后面还有3条消息,offset是16,45,100,那么我们要找的就是100这个最后的offset对应的消息

实际物理位置和这个消息的大小

def searchForOffsetWithSize(targetOffset: Long, startingPosition: Int): (OffsetPosition, Int) = {
  var position = startingPosition // 获得起始位置
  // 创建一个12字节的缓冲区,用于读取LogOverhead
  val buffer = ByteBuffer.allocate(MessageSet.LogOverhead)// 获取offsetmessage数据的size的字节数12
  val size = sizeInBytes() // 获取FileMessageSet的大小
  // 从开始位置逐条遍历消息
  while(position + MessageSet.LogOverhead < size) {
    buffer.rewind()// 重置ByteBufferposition指针,准备读入数据
    // 读取LogOverhead。这里会确保startingPosition位于一个消息的开头,否则读取到的并不是LogOverhead
    channel.read(buffer, position)
    // 没有读取到12字节的LogOverhead抛出异常
    if(buffer.hasRemaining)
      throw new IllegalStateException("Failed to read complete buffer for targetOffset %d startPosition %d in %s"
        .format(targetOffset, startingPosition, file.getAbsolutePath))
    buffer.rewind()// 重置ByteBufferposition指针,准备从ByteBuffer读取数据
    val offset = buffer.getLong()// 获取消息的offset 8个字节
    val messageSize = buffer.getInt()// 获取消息的大小 4个字节
    if (messageSize < Message.MinMessageOverhead)
      throw new IllegalStateException("Invalid message size: " + messageSize)
    if (offset >= targetOffset)// 如果offset 大于如果给定的offset
      // offset和它对应position封装成OffsetPosition对象返回
      return (OffsetPosition(offset, position), messageSize + MessageSet.LogOverhead)
    // 移动position准备读取下一个消息
    position += MessageSet.LogOverhead + messageSize
  }
  null // 找不到offset大于等于给定的offset的消息
}

 

根据时间查找:查找消息的时间大于或者等于目标时间的消息,即根据时间查找消息

def searchForTimestamp(targetTimestamp: Long, startingPosition: Int): Option[TimestampOffset] = {
  // 返回一个从startingPosition读取,读取sizeInBytes大小的数据的FileMessageSet
  val messagesToSearch = read(startingPosition, sizeInBytes)
  // 开始遍历消息(因为事先了iterator),所以可以迭代
  for (messageAndOffset <- messagesToSearch) {
    // 获取message
    val message = messageAndOffset.message
    // 如果消息时间戳大于或者等于,目标时间戳,表示找打一个消息
    if (message.timestamp >= targetTimestamp) {
      message.compressionCodec match {
        case NoCompressionCodec =>
          return Some(TimestampOffset(messageAndOffset.message.timestamp, messageAndOffset.offset))
        case _ =>
          // Iterate over the inner messages to get the exact offset.
          for (innerMessageAndOffset <- ByteBufferMessageSet.deepIterator(messageAndOffset)) {
            val timestamp = innerMessageAndOffset.message.timestamp
            if (timestamp >= targetTimestamp)
              return Some(TimestampOffset(innerMessageAndOffset.message.timestamp, innerMessageAndOffset.offset))
          }
          throw new IllegalStateException(s"The message set (max timestamp = ${message.timestamp}, max offset = ${messageAndOffset.offset}" +
              s" should contain target timestamp $targetTimestamp but it does not.")
      }
    }
  }
  None
}

 

2.4 read 读取数据,返回一个FileMessageSet

// 返回一个给定开始位置和大小的FileMessageSet的视图,即我要从哪里开始读,读多少数据
def read(position: Int, size: Int): FileMessageSet = {
  if(position < 0)
    throw new IllegalArgumentException("Invalid position: " + position)
  if(size < 0)
    throw new IllegalArgumentException("Invalid size: " + size)
  new FileMessageSet(file,
                     channel,
                     start = this.start + position,
                     end = {
                       // Handle the integer overflow
                       if (this.start + position + size < 0)
                         sizeInBytes()
                       else
                         math.min(this.start + position + size, sizeInBytes())
                     })
}

 

2.5 剪裁文件truncateTo

主要负责将日志文件剪裁截断到目标大小,副本切换的时候用得到

def truncateTo(targetSize: Int): Int = {
  // 当前日志文件大小
  val originalSize = sizeInBytes
  // 给定的大小 > 当前日志文件大小或者给定的大小 < 0就会抛出异常
  if(targetSize > originalSize || targetSize < 0)
    throw new KafkaException("Attempt to truncate log segment to " + targetSize + " bytes failed, " +
                             " size of this log segment is " + originalSize + " bytes.")
  // 开始截取,并且移动position,就相当于1000字节的文件,我把它截取到800字节的位置,其他的我不要
  if (targetSize < channel.size.toInt) {
    channel.truncate(targetSize)
    channel.position(targetSize)
    _size.set(targetSize)// 更新_size
  }
  originalSize - targetSize // 返回剪裁掉的字节数
}

2.6 还有个中的方法就是iterator,返回一个迭代器

在FileMessageSet读取消息的流程:

# 读取LogOverhead,然后按照size分配合适的ByteBuffer

# 再读取message data部分,最后将message data和 offset封装成MessageOffset返回



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莫言静好、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值