kafka日誌對象(三)—— Log的操作

Log 的常見操作分爲 4 大部分:

  • 高水位管理操作:高水位的概念在 Kafka 中舉足輕重,對它的管理,是 Log 最重要的功能之一。
  • 日誌段管理:Log 是日誌段的容器。高效組織與管理其下轄的所有日誌段對象,是源碼的核心。
  • 關鍵位移值管理:日誌定義了很多重要的位移值,比如 Log Start Offset 和 LEO 等。確保這些位移值的正確性,是構建消息引擎一致性的基礎。
  • 讀寫操作:所謂的操作日誌,大體上就是指讀寫日誌。讀寫操作是kafka高吞吐量的基礎。

高水位管理操作

一. 高水位定義

代碼只有一行:

@volatile private var highWatermarkMetadata: LogOffsetMetadata = LogOffsetMetadata(logStartOffset)

這行語句傳達了兩個重要的事實:

  • 高水位值是 volatile(易變型)的。因爲多個線程可能同時讀取它,因此需要設置成 volatile,保證內存可見性。另外,由於高水位值可能被多個線程同時修改,因此源碼使用 Java Monitor 鎖來確保併發修改的線程安全。
  • 高水位值的初始值是 Log Start Offset 值。上一篇有提到,每個 Log 對象都會維護一個 Log Start Offset 值。當首次構建高水位時,它會被賦值成 Log Start Offset 值。

看下 LogOffsetMetadata 的代碼:

case class LogOffsetMetadata(messageOffset: Long, 
     segmentBaseOffset: Long = Log.UnknownOffset, 
     relativePositionInSegment: Int = LogOffsetMetadata.UnknownFilePosition)

裏面保存了三個重要的變量:

  • messageOffset:消息位移值,這是最重要的信息。我們總說高水位值(HW),其實指的就是這個變量的值。
  • segmentBaseOffset:保存該位移值所在日誌段的起始位移。日誌段起始位移值輔助計算兩條消息在物理磁盤文件中位置的差值,即兩條消息彼此隔了多少字節。這個計算有個前提條件,即兩條消息必須處在同一個日誌段對象上,不能跨日誌段對象。否則它們就位於不同的物理文件上,計算這個值就沒有意義了。這裏的 segmentBaseOffset,就是用來判斷兩條消息是否處於同一個日誌段的。
  • relativePositionSegment:保存該位移值所在日誌段的物理磁盤位置。這個字段在計算兩個位移值之間的物理磁盤位置差值時非常有用。Kafka 什麼時候需要計算位置之間的字節數呢?就是在讀取日誌的時候。假設每次讀取時只能讀 1MB 的數據,那麼,源碼肯定需要關心兩個位移之間所有消息的總字節數是否超過了 1MB。

二. 獲取和設置高水位值

// getter method:讀取高水位的位移值
def highWatermark: Long = highWatermarkMetadata.messageOffset

// setter method:設置高水位值
private def updateHighWatermarkMetadata(newHighWatermark: LogOffsetMetadata): Unit = {
    if (newHighWatermark.messageOffset < 0) // 高水位值不能是負數
      throw new IllegalArgumentException("High watermark offset should be non-negative")

    lock synchronized { // 保護Log對象修改的Monitor鎖
      highWatermarkMetadata = newHighWatermark // 賦值新的高水位值
      producerStateManager.onHighWatermarkUpdated(newHighWatermark.messageOffset) // 處理事務狀態管理器的高水位值更新邏輯,忽略它……
      maybeIncrementFirstUnstableOffset() // First Unstable Offset是Kafka事務機制的一部分,忽略它……
    }
    trace(s"Setting high watermark $newHighWatermark")
  }

三. 更新高水位值

源碼還定義了兩個更新高水位值的方法:updateHighWatermark 和 maybeIncrementHighWatermark。從名字上來看,前者是一定要更新高水位值的,而後者是可能會更新也可能不會。

// updateHighWatermark method
def updateHighWatermark(hw: Long): Long = {
    // 新高水位值一定介於[Log Start Offset,Log End Offset]之間
    val newHighWatermark = if (hw < logStartOffset)  
      logStartOffset
    else if (hw > logEndOffset)
      logEndOffset
    else
  hw
    // 調用Setter方法來更新高水位值
    updateHighWatermarkMetadata(LogOffsetMetadata(newHighWatermark))
    newHighWatermark  // 最後返回新高水位值
  }
// maybeIncrementHighWatermark method
def maybeIncrementHighWatermark(newHighWatermark: LogOffsetMetadata): Option[LogOffsetMetadata] = {
    // 新高水位值不能越過Log End Offset
    if (newHighWatermark.messageOffset > logEndOffset)
      throw new IllegalArgumentException(s"High watermark $newHighWatermark update exceeds current " +
        s"log end offset $logEndOffsetMetadata")

    lock.synchronized {
      val oldHighWatermark = fetchHighWatermarkMetadata  // 獲取老的高水位值

      // 新高水位值要比老高水位值大以維持單調增加特性,否則就不做更新!
      // 另外,如果新高水位值在新日誌段上,也可執行更新高水位操作
      if (oldHighWatermark.messageOffset < newHighWatermark.messageOffset ||
        (oldHighWatermark.messageOffset == newHighWatermark.messageOffset && oldHighWatermark.onOlderSegment(newHighWatermark))) {
        updateHighWatermarkMetadata(newHighWatermark)
        Some(oldHighWatermark) // 返回老的高水位值
      } else {
        None
      }
    }
  }
  • updateHighWatermark 方法,主要用在 Follower 副本從 Leader 副本獲取到消息後更新高水位值。一旦拿到新的消息,就必須要更新高水位值;
  • maybeIncrementHighWatermark 方法,主要是用來更新 Leader 副本的高水位值。

需要注意的是,Leader 副本高水位值的更新是有條件的——某些情況下會更新高水位值,某些情況下可能不會。就像我剛纔說的,Follower 副本成功拉取 Leader 副本的消息後必須更新高水位值,但 Producer 端向 Leader 副本寫入消息時,分區的高水位值就可能不需要更新——因爲它可能需要等待其他 Follower 副本同步的進度。因此,源碼中定義了兩個更新的方法,它們分別應用於不同的場景。

四. 讀取高水位值

關於高水位值管理的最後一個操作是 fetchHighWatermarkMetadata 方法。它不僅僅是獲取高水位值,還要獲取高水位的其他元數據信息,即日誌段起始位移和物理位置信息。下面是它的實現邏輯:

private def fetchHighWatermarkMetadata: LogOffsetMetadata = {
    checkIfMemoryMappedBufferClosed() // 讀取時確保日誌不能被關閉

    val offsetMetadata = highWatermarkMetadata // 保存當前高水位值到本地變量,避免多線程訪問干擾
    if (offsetMetadata.messageOffsetOnly) { //沒有獲得到完整的高水位元數據
      lock.synchronized {
        val fullOffset = convertToOffsetMetadataOrThrow(highWatermark) // 通過讀日誌文件的方式把完整的高水位元數據信息拉出來
        updateHighWatermarkMetadata(fullOffset) // 然後再更新一下高水位對象
        fullOffset
      }
    } else { // 否則,直接返回即可
      offsetMetadata
    }
  }

日誌段管理

所謂的日誌段管理,無非就是增刪改查。從上一篇知道日誌段的定義就是一個Map。

1. 增加

就是Map方法的append

def addSegment(segment: LogSegment): LogSegment = this.segments.put(segment.baseOffset, segment)

2. 刪除

刪除操作相對來說複雜一點。Kafka 有很多留存策略,包括基於時間維度的、基於空間維度的和基於 Log Start Offset 維度的。那啥是留存策略呢?其實,它本質上就是根據一定的規則決定哪些日誌段可以刪除。

從源碼角度來看,Log 中控制刪除操作的總入口是 deleteOldSegments 無參方法:

def deleteOldSegments(): Int = {
    if (config.delete) {
      deleteRetentionMsBreachedSegments() + 
      deleteRetentionSizeBreachedSegments() + 
      deleteLogStartOffsetBreachedSegments()
    } else {
      deleteLogStartOffsetBreachedSegments()
    }
  }

代碼中的 deleteRetentionMsBreachedSegments、deleteRetentionSizeBreachedSegments 和 deleteLogStartOffsetBreachedSegments 分別對應於上面時間、空間、Log Start Offset那 3 個策略。

這張圖畫出了刪除底層調用的方法。上面 3 個留存策略方法底層都會調用帶參數版本的 deleteOldSegments 方法,而這個方法又相繼調用了 deletableSegments 和 deleteSegments 方法。

首先是帶參數版的 deleteOldSegments 方法:

private def deleteOldSegments(predicate: (LogSegment, Option[LogSegment]) => Boolean, reason: String): Int = {
    lock synchronized {
      val deletable = deletableSegments(predicate)
      if (deletable.nonEmpty)
        info(s"Found deletable segments with base offsets [${deletable.map(_.baseOffset).mkString(",")}] due to $reason")
      deleteSegments(deletable)
    }
  }

該方法只有兩個步驟:

  • 使用傳入的函數計算哪些日誌段對象能夠被刪除;
  • 調用 deleteSegments 方法刪除這些日誌段。

接下來是 deletableSegments 方法:

private def deletableSegments(predicate: (LogSegment, Option[LogSegment]) => Boolean): Iterable[LogSegment] = {
    if (segments.isEmpty) { // 如果當前壓根就沒有任何日誌段對象,直接返回
      Seq.empty
    } else {
      val deletable = ArrayBuffer.empty[LogSegment]
    var segmentEntry = segments.firstEntry
  
    // 從具有最小起始位移值的日誌段對象開始遍歷,直到滿足以下條件之一便停止遍歷:
    // 1. 測定條件函數predicate = false
    // 2. 掃描到包含Log對象高水位值所在的日誌段對象
    // 3. 最新的日誌段對象不包含任何消息
    // 最新日誌段對象是segments中Key值最大對應的那個日誌段,也就是我們常說的Active Segment。完全爲空的Active Segment如果被允許刪除,後面還要重建它,故代碼這裏不允許刪除大小爲空的Active Segment。
    // 在遍歷過程中,同時不滿足以上3個條件的所有日誌段都是可以被刪除的!
  
      while (segmentEntry != null) {
        val segment = segmentEntry.getValue
        val nextSegmentEntry = segments.higherEntry(segmentEntry.getKey)
        val (nextSegment, upperBoundOffset, isLastSegmentAndEmpty) = 
          if (nextSegmentEntry != null)
            (nextSegmentEntry.getValue, nextSegmentEntry.getValue.baseOffset, false)
          else
            (null, logEndOffset, segment.size == 0)

        if (highWatermark >= upperBoundOffset && predicate(segment, Option(nextSegment)) && !isLastSegmentAndEmpty) {
          deletable += segment
          segmentEntry = nextSegmentEntry
        } else {
          segmentEntry = null
        }
      }
      deletable
    }
  }

最後是 deleteSegments 方法,這個方法執行真正的日誌段刪除操作:

private def deleteSegments(deletable: Iterable[LogSegment]): Int = {
    maybeHandleIOException(s"Error while deleting segments for $topicPartition in dir ${dir.getParent}") {
      val numToDelete = deletable.size
      if (numToDelete > 0) {
        // 不允許刪除所有日誌段對象。如果一定要做,先創建出一個新的來,然後再把前面N個刪掉
        if (segments.size == numToDelete)
          roll()
        lock synchronized {
          checkIfMemoryMappedBufferClosed() // 確保Log對象沒有被關閉
          // 刪除給定的日誌段對象以及底層的物理文件
          removeAndDeleteSegments(deletable, asyncDelete = true)
          // 嘗試更新日誌的Log Start Offset值 
          maybeIncrementLogStartOffset(segments.firstEntry.getValue.baseOffset)
        }
      }
      numToDelete
    }
  }

爲什麼要在刪除日誌段對象之後,嘗試更新 Log Start Offset 值?

Log Start Offset 值是整個 Log 對象對外可見消息的最小位移值。如果我們刪除了日誌段對象,很有可能對外可見消息的範圍發生了變化,自然要看一下是否需要更新 Log Start Offset 值。這就是 deleteSegments 方法最後要更新 Log Start Offset 值的原因。也就是上一篇中我手畫圖說明位移3之前截斷想表達的意思。

3. 修改

說完了日誌段刪除,接下來我們來看如何修改日誌段對象。

其實,源碼裏面不涉及修改日誌段對象,所謂的修改或更新也就是替換而已,用新的日誌段對象替換老的日誌段對象。舉個簡單的例子。segments.put(1L, newSegment) 語句在沒有 Key=1 時是添加日誌段,否則就是替換已有日誌段。

4. 查詢

最後再說下查詢日誌段對象。源碼中需要查詢日誌段對象的地方太多了,但主要都是利用了 ConcurrentSkipListMap 的現成方法。

  • segments.firstEntry:獲取第一個日誌段對象;
  • segments.lastEntry:獲取最後一個日誌段對象,即 Active Segment;
  • segments.higherEntry:獲取第一個起始位移值≥給定 Key 值的日誌段對象;
  • segments.floorEntry:獲取最後一個起始位移值≤給定 Key 值的日誌段對象。

關鍵位移值管理

Log 對象維護了一些關鍵位移值數據,比如 Log Start Offset、LEO 等。因爲這些數據經常用到。所以分析下。

代碼中定義更新 LEO 的 updateLogEndOffset 方法:

private def updateLogEndOffset(offset: Long): Unit = {
  nextOffsetMetadata = LogOffsetMetadata(offset, activeSegment.baseOffset, activeSegment.size)
  if (highWatermark >= offset) {
    updateHighWatermarkMetadata(nextOffsetMetadata)
  }
  if (this.recoveryPoint > offset) {
    this.recoveryPoint = offset
  }
}

需要注意的是,如果在更新過程中發現新 LEO 值小於高水位值,那麼 Kafka 還要更新高水位值,因爲對於同一個 Log 對象而言,高水位值是不能越過 LEO 值的。

那麼,Log 對象什麼時候需要更新 LEO 呢?源碼中以下幾個地方調用了updateLogEndOffset方法:

  • Log 對象初始化時:當 Log 對象初始化時,我們必須要創建一個 LEO 對象,並對其進行初始化。
  • 寫入新消息時:這個最容易理解。以上面的圖爲例,當不斷向 Log 對象插入新消息時,LEO 值就像一個指針一樣,需要不停地向右移動,也就是不斷地增加。
  • Log 對象發生日誌切分(Log Roll)時:日誌切分是啥呢?其實就是創建一個全新的日誌段對象,並且關閉當前寫入的日誌段對象。這通常發生在當前日誌段對象已滿的時候。一旦發生日誌切分,說明 Log 對象切換了 Active Segment,那麼,LEO 中的起始位移值和段大小數據都要被更新,因此,在進行這一步操作時,我們必須要更新 LEO 對象。
  • 日誌截斷(Log Truncation)時:這個也是顯而易見的。日誌中的部分消息被刪除了,自然可能導致 LEO 值發生變化,從而要更新 LEO 對象。

那同樣,Kafka 什麼時候需要更新 Log Start Offset 呢?

  • Log 對象初始化時:和 LEO 類似,Log 對象初始化時要給 Log Start Offset 賦值,一般是將第一個日誌段的起始位移值賦值給它。
  • 日誌截斷時:同理,一旦日誌中的部分消息被刪除,可能會導致 Log Start Offset 發生變化,因此有必要更新該值。
  • Follower 副本同步時:一旦 Leader 副本的 Log 對象的 Log Start Offset 值發生變化。爲了維持和 Leader 副本的一致性,Follower 副本也需要嘗試去更新該值。
  • 刪除日誌段時:這個和日誌截斷是類似的。凡是涉及消息刪除的操作都有可能導致 Log Start Offset 值的變化。
  • 刪除消息時:嚴格來說,這麼描述有點本末倒置了。在 Kafka 中,刪除消息就是通過擡高 Log Start Offset 值來實現的,因此,刪除消息時必須要更新該值。

讀寫操作

1. 寫操作

在 Log 中,涉及寫操作的方法有 3 個:appendAsLeader、appendAsFollower 和 append。

appendAsLeader 是用於寫 Leader 副本的,appendAsFollower 是用於 Follower 副本同步的。它們的底層都調用了 append 方法。

所以看下append方法:

private def append(records: MemoryRecords,
                     origin: AppendOrigin,
                     interBrokerProtocolVersion: ApiVersion,
                     assignOffsets: Boolean,
                     leaderEpoch: Int): LogAppendInfo = {
  maybeHandleIOException(s"Error while appending records to $topicPartition in dir ${dir.getParent}") {
    // 第1步:分析和驗證待寫入消息集合,並返回校驗結果
      val appendInfo = analyzeAndValidateRecords(records, origin)

      // 如果壓根就不需要寫入任何消息,直接返回即可
      if (appendInfo.shallowCount == 0)
        return appendInfo

      // 第2步:消息格式規整,即刪除無效格式消息或無效字節
      var validRecords = trimInvalidBytes(records, appendInfo)

      lock synchronized {
        checkIfMemoryMappedBufferClosed() // 確保Log對象未關閉
        if (assignOffsets) { // 需要分配位移
          // 第3步:使用當前LEO值作爲待寫入消息集合中第一條消息的位移值
          val offset = new LongRef(nextOffsetMetadata.messageOffset)
          appendInfo.firstOffset = Some(offset.value)
          val now = time.milliseconds
          val validateAndOffsetAssignResult = try {
            LogValidator.validateMessagesAndAssignOffsets(validRecords,
              topicPartition,
              offset,
              time,
              now,
              appendInfo.sourceCodec,
              appendInfo.targetCodec,
              config.compact,
              config.messageFormatVersion.recordVersion.value,
              config.messageTimestampType,
              config.messageTimestampDifferenceMaxMs,
              leaderEpoch,
              origin,
              interBrokerProtocolVersion,
              brokerTopicStats)
          } catch {
            case e: IOException =>
              throw new KafkaException(s"Error validating messages while appending to log $name", e)
          }
          // 更新校驗結果對象類LogAppendInfo
          validRecords = validateAndOffsetAssignResult.validatedRecords
          appendInfo.maxTimestamp = validateAndOffsetAssignResult.maxTimestamp
          appendInfo.offsetOfMaxTimestamp = validateAndOffsetAssignResult.shallowOffsetOfMaxTimestamp
          appendInfo.lastOffset = offset.value - 1
          appendInfo.recordConversionStats = validateAndOffsetAssignResult.recordConversionStats
          if (config.messageTimestampType == TimestampType.LOG_APPEND_TIME)
            appendInfo.logAppendTime = now

          // 第4步:驗證消息,確保消息大小不超限
          if (validateAndOffsetAssignResult.messageSizeMaybeChanged) {
            for (batch <- validRecords.batches.asScala) {
              if (batch.sizeInBytes > config.maxMessageSize) {
                // we record the original message set size instead of the trimmed size
                // to be consistent with pre-compression bytesRejectedRate recording
                brokerTopicStats.topicStats(topicPartition.topic).bytesRejectedRate.mark(records.sizeInBytes)
                brokerTopicStats.allTopicsStats.bytesRejectedRate.mark(records.sizeInBytes)
                throw new RecordTooLargeException(s"Message batch size is ${batch.sizeInBytes} bytes in append to" +
                  s"partition $topicPartition which exceeds the maximum configured size of ${config.maxMessageSize}.")
              }
            }
          }
        } else {  // 直接使用給定的位移值,無需自己分配位移值
          if (!appendInfo.offsetsMonotonic) // 確保消息位移值的單調遞增性
            throw new OffsetsOutOfOrderException(s"Out of order offsets found in append to $topicPartition: " +
                                                 records.records.asScala.map(_.offset))

          if (appendInfo.firstOrLastOffsetOfFirstBatch < nextOffsetMetadata.messageOffset) {
            val firstOffset = appendInfo.firstOffset match {
              case Some(offset) => offset
              case None => records.batches.asScala.head.baseOffset()
            }

            val firstOrLast = if (appendInfo.firstOffset.isDefined) "First offset" else "Last offset of the first batch"
            throw new UnexpectedAppendOffsetException(
              s"Unexpected offset in append to $topicPartition. $firstOrLast " +
              s"${appendInfo.firstOrLastOffsetOfFirstBatch} is less than the next offset ${nextOffsetMetadata.messageOffset}. " +
              s"First 10 offsets in append: ${records.records.asScala.take(10).map(_.offset)}, last offset in" +
              s" append: ${appendInfo.lastOffset}. Log start offset = $logStartOffset",
              firstOffset, appendInfo.lastOffset)
          }
        }

        // 第5步:更新Leader Epoch緩存
        validRecords.batches.asScala.foreach { batch =>
          if (batch.magic >= RecordBatch.MAGIC_VALUE_V2) {
            maybeAssignEpochStartOffset(batch.partitionLeaderEpoch, batch.baseOffset)
          } else {
            leaderEpochCache.filter(_.nonEmpty).foreach { cache =>
              warn(s"Clearing leader epoch cache after unexpected append with message format v${batch.magic}")
              cache.clearAndFlush()
            }
          }
        }

        // 第6步:確保消息大小不超限
        if (validRecords.sizeInBytes > config.segmentSize) {
          throw new RecordBatchTooLargeException(s"Message batch size is ${validRecords.sizeInBytes} bytes in append " +
            s"to partition $topicPartition, which exceeds the maximum configured segment size of ${config.segmentSize}.")
        }

        // 第7步:執行日誌切分。當前日誌段剩餘容量可能無法容納新消息集合,因此有必要創建一個新的日誌段來保存待寫入的所有消息
        val segment = maybeRoll(validRecords.sizeInBytes, appendInfo)

        val logOffsetMetadata = LogOffsetMetadata(
          messageOffset = appendInfo.firstOrLastOffsetOfFirstBatch,
          segmentBaseOffset = segment.baseOffset,
          relativePositionInSegment = segment.size)

        // 第8步:驗證事務狀態
        val (updatedProducers, completedTxns, maybeDuplicate) = analyzeAndValidateProducerState(
          logOffsetMetadata, validRecords, origin)

        maybeDuplicate.foreach { duplicate =>
          appendInfo.firstOffset = Some(duplicate.firstOffset)
          appendInfo.lastOffset = duplicate.lastOffset
          appendInfo.logAppendTime = duplicate.timestamp
          appendInfo.logStartOffset = logStartOffset
          return appendInfo
        }

        // 第9步:執行真正的消息寫入操作,主要調用日誌段對象的append方法實現
        segment.append(largestOffset = appendInfo.lastOffset,
          largestTimestamp = appendInfo.maxTimestamp,
          shallowOffsetOfMaxTimestamp = appendInfo.offsetOfMaxTimestamp,
          records = validRecords)

        // 第10步:更新LEO對象,其中,LEO值是消息集合中最後一條消息位移值+1
       // 前面說過,LEO值永遠指向下一條不存在的消息
        updateLogEndOffset(appendInfo.lastOffset + 1)

        // 第11步:更新事務狀態
        for (producerAppendInfo <- updatedProducers.values) {
          producerStateManager.update(producerAppendInfo)
        }

        for (completedTxn <- completedTxns) {
          val lastStableOffset = producerStateManager.lastStableOffset(completedTxn)
          segment.updateTxnIndex(completedTxn, lastStableOffset)
          producerStateManager.completeTxn(completedTxn)
        }

        producerStateManager.updateMapEndOffset(appendInfo.lastOffset + 1)
       maybeIncrementFirstUnstableOffset()

        trace(s"Appended message set with last offset: ${appendInfo.lastOffset}, " +
          s"first offset: ${appendInfo.firstOffset}, " +
          s"next offset: ${nextOffsetMetadata.messageOffset}, " +
          s"and messages: $validRecords")

        // 是否需要手動落盤。一般情況下我們不需要設置Broker端參數log.flush.interval.messages
       // 落盤操作交由操作系統來完成。但某些情況下,可以設置該參數來確保高可靠性
        if (unflushedMessages >= config.flushInterval)
          flush()

        // 第12步:返回寫入結果
        appendInfo
      }
    }
  }

額,過程有點多,用圖來描述下:

2. 讀取操作

read 方法的流程相對要簡單一些,首先來看它的方法簽名:

def read(startOffset: Long,
           maxLength: Int,
           isolation: FetchIsolation,
           minOneMessage: Boolean): FetchDataInfo = {
           ......
}

它接收 4 個參數,含義如下:

  • startOffset,即從 Log 對象的哪個位移值開始讀消息。
  • maxLength,即最多能讀取多少字節。
  • isolation,設置讀取隔離級別,主要控制能夠讀取的最大位移值,多用於 Kafka 事務。
  • minOneMessage,即是否允許至少讀一條消息。設想如果消息很大,超過了 maxLength,正常情況下 read 方法永遠不會返回任何消息。但如果設置了該參數爲 true,read 方法就保證至少能夠返回一條消息。

看下read方法的流程:

def read(startOffset: Long,
           maxLength: Int,
           isolation: FetchIsolation,
           minOneMessage: Boolean): FetchDataInfo = {
    maybeHandleIOException(s"Exception while reading from $topicPartition in dir ${dir.getParent}") {
      trace(s"Reading $maxLength bytes from offset $startOffset of length $size bytes")

      val includeAbortedTxns = isolation == FetchTxnCommitted

      // 讀取消息時沒有使用Monitor鎖同步機制,因此這裏取巧了,用本地變量的方式把LEO對象保存起來,避免爭用(race condition)
      val endOffsetMetadata = nextOffsetMetadata
      val endOffset = nextOffsetMetadata.messageOffset
      if (startOffset == endOffset) // 如果從LEO處開始讀取,那麼自然不會返回任何數據,直接返回空消息集合即可
        return emptyFetchDataInfo(endOffsetMetadata, includeAbortedTxns)

      // 找到startOffset值所在的日誌段對象。注意要使用floorEntry方法
      var segmentEntry = segments.floorEntry(startOffset)

      // return error on attempt to read beyond the log end offset or read below log start offset
      // 滿足以下條件之一將被視爲消息越界,即你要讀取的消息不在該Log對象中:
      // 1. 要讀取的消息位移超過了LEO值
      // 2. 沒找到對應的日誌段對象
      // 3. 要讀取的消息在Log Start Offset之下,同樣是對外不可見的消息
      if (startOffset > endOffset || segmentEntry == null || startOffset < logStartOffset)
        throw new OffsetOutOfRangeException(s"Received request for offset $startOffset for partition $topicPartition, " +
          s"but we only have log segments in the range $logStartOffset to $endOffset.")

      // 查看一下讀取隔離級別設置。
      // 普通消費者能夠看到[Log Start Offset, LEO)之間的消息
      // 事務型消費者只能看到[Log Start Offset, Log Stable Offset]之間的消息。Log Stable Offset(LSO)是比LEO值小的位移值,爲Kafka事務使用
      // Follower副本消費者能夠看到[Log Start Offset,高水位值]之間的消息
      val maxOffsetMetadata = isolation match {
        case FetchLogEnd => nextOffsetMetadata
        case FetchHighWatermark => fetchHighWatermarkMetadata
        case FetchTxnCommitted => fetchLastStableOffsetMetadata
      }

      // 如果要讀取的起始位置超過了能讀取的最大位置,返回空的消息集合,因爲沒法讀取任何消息
      if (startOffset > maxOffsetMetadata.messageOffset) {
        val startOffsetMetadata = convertToOffsetMetadataOrThrow(startOffset)
        return emptyFetchDataInfo(startOffsetMetadata, includeAbortedTxns)
      }

      // 開始遍歷日誌段對象,直到讀出東西來或者讀到日誌末尾
      while (segmentEntry != null) {
        val segment = segmentEntry.getValue

        val maxPosition = {
          if (maxOffsetMetadata.segmentBaseOffset == segment.baseOffset) {
            maxOffsetMetadata.relativePositionInSegment
          } else {
            segment.size
          }
        }

        // 調用日誌段對象的read方法執行真正的讀取消息操作
        val fetchInfo = segment.read(startOffset, maxLength, maxPosition, minOneMessage)
        if (fetchInfo == null) { // 如果沒有返回任何消息,去下一個日誌段對象試試
          segmentEntry = segments.higherEntry(segmentEntry.getKey)
        } else { // 否則返回
          return if (includeAbortedTxns)
            addAbortedTransactions(startOffset, segmentEntry, fetchInfo)
          else
            fetchInfo
        }
      }

      // 已經讀到日誌末尾還是沒有數據返回,只能返回空消息集合
      FetchDataInfo(nextOffsetMetadata, MemoryRecords.EMPTY)
    }
  }

總結一下日誌對日誌段的管理操作:

  1. 高水位管理:Log 對象定義了高水位對象以及管理它的各種操作,主要包括更新和讀取。
  2. 日誌段管理:作爲日誌段的容器,Log 對象保存了很多日誌段對象。日誌段對象被組織在一起的方式以及 Kafka Log 對象是如何對它們進行管理的。
  3. 關鍵位移值管理:主要涉及對 Log Start Offset 和 LEO 的管理。這兩個位移值是 Log 對象非常關鍵的字段。比如,副本管理、狀態機管理等高階功能都要依賴於它們。
  4. 讀寫操作:日誌讀寫是實現 Kafka 消息引擎基本功能的基石。

基本到此,kafka的Log對象總結告一段落。最大的感覺就是對Broker內部的操作心裏有個數,shell腳本里的一些參數什麼時候能用到也瞭解一些,但感覺還是少點東西,想起來再加吧。看到這裏了,莫忘點贊支持奧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章