kafka日志段LogSegment-源码学习笔记

kafka日志结构概述

kafka 日志在磁盘上的组织结构如下:
kafka日志文件
kafka日志由多个日志段组成,每个日志段会在磁盘上创建一组文件,包括消息日志文件(.log),位移索引文件(.index),时间戳索引文件(.timeindex),以及终止事务文件(.txnindex),该文件只在使用索引的情况下创建。
一般情况下,一个kafka主题有很多分区,每个分区对应一个Log对象,在物理磁盘上对应一个子目录,比如你创建了两个分区的主题test-topic,在磁盘上就会创建两个子目录test-topic-0和test-topic-1.而在服务器端就是两个log对象,每个子目录下存在多组日志段,也就是多组.log、.index、.timeindex 文件组合,只不过文件名不同,因为每个日志段的起始位移不同。
下面详细说下日志段对象的三个重要方法的源码:append方法、read方法、recover方法;这几个方法位于core/src/main/scala/kafka/log/LogSegment.scala。

首先看下这个类的注释:

/**
 * A segment of the log. Each segment has two components: a log and an index. The log is a FileRecords containing
 * the actual messages. The index is an OffsetIndex that maps from logical offsets to physical file positions. Each
 * segment has a base offset which is an offset <= the least offset of any message in this segment and > any offset in
 * any previous segment.
 *
 * A segment with a base offset of [base_offset] would be stored in two files, a [base_offset].index and a [base_offset].log file.
 **/

翻译一下,大致是:
一个日志段有两部分组成,一个log和一个index 。 log 文件记录了实际的消息文件,index文件就是一个逻辑位移到物理文件位置的文件,每一个日志段有一个初始位移,这个初始位移小于该日志段任何一条消息的唯一,大于先前日志段的任何位移。
每一个日志段都有一个基础位移会被存到index和log 文件中,一旦创建就不能修改。

append 方法

def append(largestOffset: Long,
             largestTimestamp: Long,
             shallowOffsetOfMaxTimestamp: Long,
             records: MemoryRecords): Unit = {
    if (records.sizeInBytes > 0) {
      trace(s"Inserting ${records.sizeInBytes} bytes at end offset $largestOffset at position ${log.sizeInBytes} " +
            s"with largest timestamp $largestTimestamp at shallow offset $shallowOffsetOfMaxTimestamp")
      val physicalPosition = log.sizeInBytes()
      if (physicalPosition == 0)
        rollingBasedTimestamp = Some(largestTimestamp)

      ensureOffsetInRange(largestOffset)

      // append the messages
      val appendedBytes = log.append(records)
      trace(s"Appended $appendedBytes to ${log.file} at end offset $largestOffset")
      // Update the in memory max timestamp and corresponding offset.
      if (largestTimestamp > maxTimestampSoFar) {
        maxTimestampSoFar = largestTimestamp
        offsetOfMaxTimestampSoFar = shallowOffsetOfMaxTimestamp
      }
      // append an entry to the index (if needed)
      if (bytesSinceLastIndexEntry > indexIntervalBytes) {
        offsetIndex.append(largestOffset, physicalPosition)
        timeIndex.maybeAppend(maxTimestampSoFar, offsetOfMaxTimestampSoFar)
        bytesSinceLastIndexEntry = 0
      }
      bytesSinceLastIndexEntry += records.sizeInBytes
    }
  }

append方法有4个参数,分别表示待写入消息批次中消息的最大位移值、最大时间戳,最大时间戳对应消息位移以及真正要写入的消息集合,整个写入流程如下:
append方法写入流程
第一步:
首先调用records.sizeInBytes判断该日志段是否为空,为空就记录写入消息集合的最大时间戳,并将其作为后面新增日志段倒计时的依据。
第二步:
代码调用ensureOffsetInRange 方法确保输入参数最大位移值是合法的,标准是看它与日志段起始位移的差值是否在整数范围内,如果不在抛出异常,并终止后续消息的写入,如果遇到这个问题就升级kafka的版本解决。
第三步:
log.append(records)方法进行写入操作系统页缓存。
第四步:
更新日志段的最大时间戳以及最大时间戳所属的消息的位移值属性,每个日志段都要保存当前最大时间戳信息和所属消息的位移信息。
最后一步:
更新索引项和写入的字节数。日志段每写入4KB数据就要写入一个索引项,同时清空已写入字节数,以备下次重新累计计算。

read方法

def read(startOffset: Long,
           maxSize: Int,
           maxPosition: Long = size,
           minOneMessage: Boolean = false): FetchDataInfo = {
    if (maxSize < 0)
      throw new IllegalArgumentException(s"Invalid max size $maxSize for log read from segment $log")

    val startOffsetAndSize = translateOffset(startOffset)

    // if the start position is already off the end of the log, return null
    if (startOffsetAndSize == null)
      return null

    val startPosition = startOffsetAndSize.position
    val offsetMetadata = LogOffsetMetadata(startOffset, this.baseOffset, startPosition)

    val adjustedMaxSize =
      if (minOneMessage) math.max(maxSize, startOffsetAndSize.size)
      else maxSize

    // return a log segment but with zero size in the case below
    if (adjustedMaxSize == 0)
      return FetchDataInfo(offsetMetadata, MemoryRecords.EMPTY)

    // calculate the length of the message set to read based on whether or not they gave us a maxOffset
    val fetchSize: Int = min((maxPosition - startPosition).toInt, adjustedMaxSize)

    FetchDataInfo(offsetMetadata, log.slice(startPosition, fetchSize),
      firstEntryIncomplete = adjustedMaxSize < startOffsetAndSize.size)
  }

read 方法接收 4 个输入参数。
startOffset:要读取的第一条消息的位移;
maxSize:能读取的最大字节数;
maxPosition :能读到的最大文件位置;
minOneMessage:是否允许在消息体过大时至少返回第一条消息,这个参数主要是为了确保消息不出现饿死的情况。

read 方法的执行流程如下:

read方法流程
1、第一步是将startOffset 的值转换成物理位移值才能开始读取消息。
2、根据maxSize 和maxPosition 计算出最大能读取的数据量(两者取小)
3、通过log.slice(startPosition, fetchSize)读取消息。

recover 方法

  def recover(producerStateManager: ProducerStateManager, leaderEpochCache: Option[LeaderEpochFileCache] = None): Int = {
    offsetIndex.reset()
    timeIndex.reset()
    txnIndex.reset()
    var validBytes = 0
    var lastIndexEntry = 0
    maxTimestampSoFar = RecordBatch.NO_TIMESTAMP
    try {
      for (batch <- log.batches.asScala) {
        batch.ensureValid()
        ensureOffsetInRange(batch.lastOffset)

        // The max timestamp is exposed at the batch level, so no need to iterate the records
        if (batch.maxTimestamp > maxTimestampSoFar) {
          maxTimestampSoFar = batch.maxTimestamp
          offsetOfMaxTimestampSoFar = batch.lastOffset
        }

        // Build offset index
        if (validBytes - lastIndexEntry > indexIntervalBytes) {
          offsetIndex.append(batch.lastOffset, validBytes)
          timeIndex.maybeAppend(maxTimestampSoFar, offsetOfMaxTimestampSoFar)
          lastIndexEntry = validBytes
        }
        validBytes += batch.sizeInBytes()

        if (batch.magic >= RecordBatch.MAGIC_VALUE_V2) {
          leaderEpochCache.foreach { cache =>
            if (batch.partitionLeaderEpoch >= 0 && cache.latestEpoch.forall(batch.partitionLeaderEpoch > _))
              cache.assign(batch.partitionLeaderEpoch, batch.baseOffset)
          }
          updateProducerState(producerStateManager, batch)
        }
      }
    } catch {
      case e@ (_: CorruptRecordException | _: InvalidRecordException) =>
        warn("Found invalid messages in log segment %s at byte offset %d: %s. %s"
          .format(log.file.getAbsolutePath, validBytes, e.getMessage, e.getCause))
    }
    val truncated = log.sizeInBytes - validBytes
    if (truncated > 0)
      debug(s"Truncated $truncated invalid bytes at the end of segment ${log.file.getAbsoluteFile} during recovery")

    log.truncateTo(validBytes)
    offsetIndex.trimToValidSize()
    // A normally closed segment always appends the biggest timestamp ever seen into log segment, we do this as well.
    timeIndex.maybeAppend(maxTimestampSoFar, offsetOfMaxTimestampSoFar, skipFullCheck = true)
    timeIndex.trimToValidSize()
    truncated
  }

Broker 在启动时会从磁盘上加载所有日志段信息到内存中,并创建相应的 LogSegment 对象实例,这个过程就是调用recover 方法。

recover执行流程
recover 开始时,代码依次调用索引对象的reset方法清空所有的索引文件,之后会开始遍历日志段中的所有消息集合会消息批次(RecordBatch)。对于取到的每个消息集合,日志段必须要确保它们是合法的(1、该集合的消息必须要符合kafka定义的二进制格式;2、该集合中最后一条消息的位移值不能越界,即它与日志段起始唯位移值的差值必须是一个正整数)。

校验完成后,根据最大时间戳和位移值构建索引项,并且累加已经读取的消息字节数。最后是更新Producer事务的状态和Leader Epoch 缓存。
遍历完成后,kafka 会比较日志段和当前已累计的的字节数比较,如果前者大就进行日志截断,并调整日志段大小,索引文件大小。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值