/** * An append-only log for storing messages. * * The log is a sequence of LogSegments, each with a base offset denoting the first message in the segment. * * log 由多个 LogSegments 构成;每个 segment 都有一个 base offset 参数,代表 segment 中第一条消息 * * New log segments are created according to a configurable policy that controls the size in bytes or time interval * for a given segment. * * 系统会根据配置文件中预设的策略(比如当前 segment 大小、固定的时间间隔等),创建新的 segment * * @param _dir The directory in which log segments are created. * @param config The log configuration settings * @param logStartOffset The earliest offset allowed to be exposed to kafka client. * The logStartOffset can be updated by : * - user's DeleteRecordsRequest * - broker's log retention * - broker's log truncation * The logStartOffset is used to decide the following: * - Log deletion. LogSegment whose nextOffset <= log's logStartOffset can be deleted. * It may trigger log rolling if the active segment is deleted. * - Earliest offset of the log in response to ListOffsetRequest. To avoid OffsetOutOfRange exception after user seeks to earliest offset, * we make sure that logStartOffset <= log's highWatermark * Other activities such as log cleaning are not affected by logStartOffset. * * 日志中第一条日志的 offset,这是一个 volatile 类型的变量,存在被多个线程更新的可能,可能得来源有: * 1. 用户删除消息 * 2. broker 日志保留机制(kafka 支持根据时间、日志大小、offset 设置日志保留策略) * 3. broker 日志截断 * * logStartOffset 的作用是: * - 删除日志。nextOffset <= Log.logStartOffset 的 segment 都可以删掉 * - 作为 ListOffsetRequest 请求中的 Earliest offset 值返回。为了避免用户在向前追溯日志的时候触发 OffsetOutOfRange exception, * kafka 会确保 logStartOffset <= highWatermark * * * @param recoveryPoint The offset at which to begin recovery--i.e. the first offset which has not been flushed to disk * @param scheduler The thread pool scheduler used for background actions * @param brokerTopicStats Container for Broker Topic Yammer Metrics * @param time The time instance used for checking the clock * @param maxProducerIdExpirationMs The maximum amount of time to wait before a producer id is considered expired * @param producerIdExpirationCheckIntervalMs How often to check for producer ids which need to be expired */ @threadsafe classLog(@volatile private var _dir: File, @volatile var config: LogConfig, @volatile var logStartOffset: Long, @volatile var recoveryPoint: Long, scheduler: Scheduler, brokerTopicStats: BrokerTopicStats, val time: Time, val maxProducerIdExpirationMs: Int, val producerIdExpirationCheckIntervalMs: Int, val topicPartition: TopicPartition, // 对应的分区 val producerStateManager: ProducerStateManager, logDirFailureChannel: LogDirFailureChannel) extendsLoggingwithKafkaMetricsGroup{
/* A lock that guards all modifications to the log */ /* 所有的 modify 操作都用这个 lock.synchronized 加锁 */ privateval lock = newObject
/* Keep track of the current high watermark in order to ensure that segments containing offsets at or above it are * not eligible for deletion. This means that the active segment is only eligible for deletion if the high watermark * equals the log end offset (which may never happen for a partition under consistent load). This is needed to * prevent the log start offset (which is exposed in fetch responses) from getting ahead of the high watermark. */ // 持有 current HW 信息,kafka 会确保 offset ≥ HW 的 segment 的数据不被删除 //(即确保不会删除包含未提交消息的 segment) // 这意味着只有当一个 active segment 的 HW 等于 LEO 的时候,才允许删除数据 //(通常在一个 active segment 中,持续有数据写入,HW 总是在追 LEO,永远也达不成删除的条件) // 这个机制是必要的,可以防止 log start offset 超过 HW,从而使消费者跳过一部分数据 @volatileprivatevar highWatermarkMetadata: LogOffsetMetadata = LogOffsetMetadata(logStartOffset)
/* the actual segments of the log */ // Log 中所有的 segment 对象,都放在这个 concurrent map 中 privateval segments: ConcurrentNavigableMap[java.lang.Long, LogSegment] = newConcurrentSkipListMap[java.lang.Long, LogSegment]
/* Calculate the offset of the next message */ // 构建下一条带插入消息的 offset 元数据 nextOffsetMetadata = LogOffsetMetadata(nextOffset, activeSegment.baseOffset, activeSegment.size)
// The earliest leader epoch may not be flushed during a hard failure. Recover it here. // 保证数据合法性,清理掉无用的 leaderEpoch entry(即 offset < logStartOffset 的所有 entry), // 然后更正最早一条,将 entry 的 offset 设置为 logStartOffset leaderEpochCache.foreach(_.truncateFromStart(logStartOffset))
// Any segment loading or recovery code must not use producerStateManager, so that we can build the full state here // from scratch. // 加载 segment 或者 recovery 都不允许使用 producerStateManager。下面会重新构建 Producer State if (!producerStateManager.isEmpty) thrownewIllegalStateException("Producer state must be empty during log initialization")
// hasCleanShutdownFile: 如果有这个文件,说明 broker 是正常关闭的。 // ProducerStateManager:它维护了一个从生产者 id 到其最近追加条目(如 epoch、sequence number、last offset 等)元数据的 map // 只要生产者 id 仍包含在此映射中,相应的生产者就可以继续写入数据。 // 除非生产者很久不写日志,或者写入的所有消息都被删除(retention policy=delete),这个 id 就会过期。 // 这个东西暂时不涉及主线功能,这里暂时不深究了. loadProducerState(logEndOffset, reloadFromCleanShutdown = hasCleanShutdownFile) }
privatedefloadSegments(): Long = { // first do a pass through the files in the log directory and remove any temporary files // and find any interrupted swap operations // 清理 .delete 文件: 需要被删除的 log 文件和 index 文件 // 清理 .cleaned 文件: 在log compaction 过程中宕机,文件中的数据状态未知,需要删除掉 // 清理所有的无法使用的 .swap 文件: KAFKA-6264 // 返回可用的 .swap 文件: 即在 log compaction 结束后,替换原文件时宕机而遗留下的文件,只需要再做一次替换操作就可以了 val swapFiles = removeTempFilesAndCollectSwapFiles()
// Now do a second pass and load all the log and index files. // We might encounter legacy log segments with offset overflow (KAFKA-6264). We need to split such segments. When // this happens, restart loading segment files from scratch. // 加载日志文件和索引文件 // 可能会遇到带有偏移量溢出的旧版日志段(KAFKA-6264)。需要将这些日志段拆分出来。当这种情况发生时,从头开始重新加载日志段文件。 retryOnOffsetOverflow { // In case we encounter a segment with offset overflow, the retry logic will split it after which we need to retry // loading of segments. In that case, we also need to close all segments that could have been left open in previous // call to loadSegmentFiles(). // 如果遇到带有偏移量溢出的日志段,会抛出 LogSegmentOffsetOverflowException,重试逻辑将会将其拆分。然后我们需要重试加载这些日志段。 // 在这种情况下,还需要关闭在之前调用 loadSegmentFiles() 时可能已打开的所有日志段。 logSegments.foreach(_.close()) segments.clear() loadSegmentFiles() }
// Finally, complete any interrupted swap operations. To be crash-safe, // log files that are replaced by the swap segment should be renamed to .deleted // before the swap file is restored as the new segment file. // 最后,完成被中断的交换操作。 // 为了保证在崩溃情况下的数据安全, 被 swap segment 替换的日志文件应在 swap 文件恢复为新的 segment 文件之前重命名为 .deleted。 completeSwapOperations(swapFiles)
if (!dir.getAbsolutePath.endsWith(Log.DeleteDirSuffix)) {
// 如果日志目录存在,则执行 recoverLog val nextOffset = retryOnOffsetOverflow { // 根据 log 文件重建所有的 index 文件,并砍掉 log 和 index 末尾的无效 Bytes recoverLog() }
// reset the index size of the currently active log segment to allow more entries activeSegment.resizeIndexes(config.maxIndexSize) nextOffset } else { // 如果没有可用的目录,则创建一个空的日志文件 if (logSegments.isEmpty) { addSegment(LogSegment.open(dir = dir, baseOffset = 0, config, time = time, fileAlreadyExists = false, initFileSize = this.initFileSize, preallocate = false)) } 0 } }
privatedefloadSegmentFiles(): Unit = { // load segments in ascending order because transactional data from one segment may depend on the // segments that come before it // 因为事务型的数据可能和数据的时间关系有关,因此这里需要按时间顺序加载 segment 数据 for (file <- dir.listFiles.sortBy(_.getName) if file.isFile) { if (isIndexFile(file)) { // if it is an index file, make sure it has a corresponding .log file
// 确保索引文件有对应的(同名的,或者说同 offset 的) log 文件 val offset = offsetFromFile(file) // 读文件名,即就是当前日志的 base offset val logFile = Log.logFile(dir, offset) if (!logFile.exists) { warn(s"Found an orphaned index file ${file.getAbsolutePath}, with no corresponding log file.") Files.deleteIfExists(file.toPath) }
} elseif (isLogFile(file)) { // if it's a log file, load the corresponding log segment
// 读文件名,确定 baseOffset val baseOffset = offsetFromFile(file) val timeIndexFileNewlyCreated = !Log.timeIndexFile(dir, baseOffset).exists()
// 创建 LogSegment 对象 val segment = LogSegment.open(dir = dir, baseOffset = baseOffset, config, time = time, fileAlreadyExists = true)
try segment.sanityCheck(timeIndexFileNewlyCreated) catch { case _: NoSuchFileException => error(s"Could not find offset index file corresponding to log file ${segment.log.file.getAbsolutePath}, " + "recovering segment and rebuilding index files...") recoverSegment(segment) case e: CorruptIndexException => warn(s"Found a corrupted index file corresponding to log file ${segment.log.file.getAbsolutePath} due " + s"to ${e.getMessage}}, recovering segment and rebuilding index files...") recoverSegment(segment) }
privatedefcompleteSwapOperations(swapFiles: Set[File]): Unit = { for (swapFile <- swapFiles) { val logFile = newFile(CoreUtils.replaceSuffix(swapFile.getPath, SwapFileSuffix, "")) val baseOffset = offsetFromFile(logFile)
// 从磁盘中加载 swap segment val swapSegment = LogSegment.open(swapFile.getParentFile, baseOffset = baseOffset, config, time = time, fileSuffix = SwapFileSuffix) info(s"Found log file ${swapFile.getPath} from interrupted swap operation, repairing.")
// 以 FileChannelRecordBatch 维度遍历 segment 文件,重建所有的 index 文件,并砍掉 log 和 index 末尾的无效 Bytes recoverSegment(swapSegment)
// We create swap files for two cases: // (1) Log cleaning where multiple segments are merged into one, and // (2) Log splitting where one segment is split into multiple. // // 两种情况下会创建 swap 文件: // 1. log clean(log compaction)的时候,将多个 segment 合并成一个的时候 // 2. log splitting(segment 中的数据超过 Int.MAX 条)的时候,将一个 segment 拆分成多个 segment // // Both of these mean that the resultant swap segments be composed of the original set, i.e. the swap segment // must fall within the range of existing segment(s). If we cannot find such a segment, it means the deletion // of that segment was successful. In such an event, we should simply rename the .swap to .log without having to // do a replace with an existing segment. // // 如果找不到原始的 log 文件,也就不需要 replace 了,直接重命名就可以了 val oldSegments = logSegments(swapSegment.baseOffset, swapSegment.readNextOffset).filter { segment => segment.readNextOffset > swapSegment.baseOffset } replaceSegments(Seq(swapSegment), oldSegments.toSeq, isRecoveredSwapFile = true) } }
/** * If topic deletion is enabled, delete any log segments that have either expired due to time based retention * or because the log size is > retentionSize. * * 如果配置文件里的 clean policy 是 delete,则删除超过生存期限的日志,以及日志大小超限的日志 * * Whether or not deletion is enabled, delete any log segments that are before the log start offset * * 不论 clean policy 是什么,offset 小于 log start offset 的日志全部删除 */ defdeleteOldSegments(): Int = { if (config.delete) { // Breach: 违反\违背 deleteRetentionMsBreachedSegments() + deleteRetentionSizeBreachedSegments() + deleteLogStartOffsetBreachedSegments() } else { deleteLogStartOffsetBreachedSegments() } }
/** * Find segments starting from the oldest until the user-supplied predicate is false or the segment * containing the current high watermark is reached. We do not delete segments with offsets at or beyond * the high watermark to ensure that the log start offset can never exceed it. If the high watermark * has not yet been initialized, no segments are eligible for deletion. * * 从最早的 segment 开始遍历,直到传入的 predicate = false 或者达到了包含 high watermark 的 segment。 * 为了确保 log start offset ≤ high watermark,我们不会删除包含 high watermark 的 segment。 * 如果 hw 未初始化,则不会删除任何 segment。 * * A final segment that is empty will never be returned (since we would just end up re-creating it). * * @param predicate A function that takes in a candidate log segment and the next higher segment * (if there is one) and returns true iff it is deletable * @return the segments ready to be deleted */ privatedefdeletableSegments(predicate: (LogSegment, Option[LogSegment]) => Boolean): Iterable[LogSegment] = { if (segments.isEmpty) { Seq.empty } else { val deletable = ArrayBuffer.empty[LogSegment] var segmentEntry = segments.firstEntry 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)
privatedefdeleteSegments(deletable: Iterable[LogSegment], reason: SegmentDeletionReason): Int = { maybeHandleIOException(s"Error while deleting segments for $topicPartition in dir ${dir.getParent}") { val numToDelete = deletable.size if (numToDelete > 0) { // we must always have at least one segment, so if we are going to delete all the segments, create a new one first if (segments.size == numToDelete) roll() lock synchronized { checkIfMemoryMappedBufferClosed()
/** * Append this message set to the active segment of the log, rolling over to a fresh segment if necessary. * * 将数据 append 到 active segment 中,如果 segment 中的数据满了,就滚动创建一个新的 segment * * This method will generally be responsible for assigning offsets to the messages, * however if the assignOffsets=false flag is passed we will only check that the existing offsets are valid. * * @param records The log records to append * @param origin Declares the origin of the append which affects required validations * @param interBrokerProtocolVersion Inter-broker message protocol version * @param assignOffsets Should the log assign offsets to this message set or blindly apply what it is given * @param leaderEpoch The partition's leader epoch which will be applied to messages when offsets are assigned on the leader * @param ignoreRecordSize true to skip validation of record size. * @throws KafkaStorageException If the append fails due to an I/O error. * @throws OffsetsOutOfOrderException If out of order offsets found in 'records' * @throws UnexpectedAppendOffsetException If the first or last offset in append is less than next offset * @return Information about the appended messages including the first and last offset. */ privatedefappend(records: MemoryRecords, origin: AppendOrigin, interBrokerProtocolVersion: ApiVersion, assignOffsets: Boolean, leaderEpoch: Int, ignoreRecordSize: Boolean): LogAppendInfo = { maybeHandleIOException(s"Error while appending records to $topicPartition in dir ${dir.getParent}") {
// 校验 CRC、消息大小是否超限。构建 LogAppendInfo,里面保存着这批 records // 的 firstOffset, lastOffset, maxTimestamp, offsetOfMaxTimestamp, codec // 等信息 val appendInfo = analyzeAndValidateRecords(records, origin, ignoreRecordSize)
// return if we have no valid messages or if this is a duplicate of the last appended entry // records 中的数据为空,直接退出 if (appendInfo.shallowCount == 0) return appendInfo
// trim any invalid bytes or partial messages before appending it to the on-disk log // 第一步校验的时候,如果某一个 batch 格式异常,会直接报错退出, // 所以 invalid bytes 只有可能出现在最后一个 batch 的后面 // 这里比较 records.sizeInBytes 和 appendInfo.validBytes, // 尝试删除末尾的 invalid bytes(如果有的话) var validRecords = trimInvalidBytes(records, appendInfo)
// they are valid, insert them in the log lock synchronized { checkIfMemoryMappedBufferClosed()
// re-validate message sizes if there's a possibility that they have changed (due to re-compression or message // format conversion) // 上一步验证过程中,可能会做重新压缩或者格式转换,所以再次校验一下消息大小 if (!ignoreRecordSize && 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) thrownewRecordTooLargeException(s"Message batch size is ${batch.sizeInBytes} bytes in append to" + s"partition $topicPartition which exceeds the maximum configured size of ${config.maxMessageSize}.") } } } } else { // we are taking the offsets we are given // 采信消息体中的 offset(通常是 follower 副本拉取数据,然后调用 append 方法的场景)
// 拒绝非递增的消息序列 if (!appendInfo.offsetsMonotonic) thrownewOffsetsOutOfOrderException(s"Out of order offsets found in append to $topicPartition: " + records.records.asScala.map(_.offset))
if (appendInfo.firstOrLastOffsetOfFirstBatch < nextOffsetMetadata.messageOffset) { // we may still be able to recover if the log is empty // one example: fetching from log start offset on the leader which is not batch aligned, // which may happen as a result of AdminClient#deleteRecords() // // 异常情况: 待插入消息的 offset 小于 Log End Offset // // 一个已知的场景是:通过 AdminClient#deleteRecords() 删除了一部分消息, // 导致从 Leader 节点获取日志起始 offset 时,该 offset 可能未对齐到批次的边界 // // 通常来说是没得救的,但是在下面的情况下,还是能救的回来: // logEndOffset == log.logStartOffset && firstOffset < logEndOffset && appendInfo.lastOffset >= logEndOffset // 即:日志为空,并且,LogStartOffset 落在了待插入数据的中间的时候; // // 在 append 方法的最外层,会 catch 这个异常,然后对日志从 firstOffset 位置做一次 truncateFullyAndStartAt() // 然后重复做一次 append // // 之所以要在最外层处理,是因为需要先删除当前 segment,然后创建一个新的 segment,并以 firstOffset 作为其 baseOffset val firstOffset = appendInfo.firstOffset match { caseSome(offset) => offset caseNone => records.batches.asScala.head.baseOffset() }
val firstOrLast = if (appendInfo.firstOffset.isDefined) "First offset"else"Last offset of the first batch" thrownewUnexpectedAppendOffsetException( 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) } }
// update the epoch cache with the epoch stamped onto the message by the leader // 更新 leaderEpochCache(如果需要的话) validRecords.batches.forEach { batch => if (batch.magic >= RecordBatch.MAGIC_VALUE_V2) { // 比较入参和 epochCache 中的 item,如果有冲突,根据具体情况, // 可能会直接追加一个 item,也有可能截掉一部分 item 然后再追加, // 当然也有可能不做任何操作 maybeAssignEpochStartOffset(batch.partitionLeaderEpoch, batch.baseOffset) } else { // In partial upgrade scenarios, we may get a temporary regression to the message format. In // order to ensure the safety of leader election, we clear the epoch cache so that we revert // to truncation by high watermark after the next leader election. // 在分步升级的场景下,可能会出现消息格式降级。为了确保选举的正确性, // 会清理 epoch cache,以便在下一次选举之后,重新从高水位开始截断。
// 这是因为V2及以上版本中,使用 partitionLeaderEpoch 来明确的指示 epoch 信息, // 而旧版本的消息中不包含 partitionLeaderEpoch,它是以 hw 作为主要的截断依据。 // 如果不清除缓存,可能会导致领导者选举后无法正确回滚到合适的偏移量。 // 清除缓存后,系统会回退到基于高水位标记的日志截断(truncation by high watermark)机制, // 以确保系统的安全性 leaderEpochCache.filter(_.nonEmpty).foreach { cache => warn(s"Clearing leader epoch cache after unexpected append with message format v${batch.magic}") cache.clearAndFlush() } } }
// check messages set size may be exceed config.segmentSize if (validRecords.sizeInBytes > config.segmentSize) { thrownewRecordBatchTooLargeException(s"Message batch size is ${validRecords.sizeInBytes} bytes in append " + s"to partition $topicPartition, which exceeds the maximum configured segment size of ${config.segmentSize}.") }
// 如果当前 segment 已经满了,就轮转生成一个新的 segment val segment = maybeRoll(validRecords.sizeInBytes, appendInfo)
// now that we have valid records, offsets assigned, and timestamps updated, we need to // validate the idempotent/transactional state of the producers and collect some metadata // 验证事务状态 val (updatedProducers, completedTxns, maybeDuplicate) = analyzeAndValidateProducerState( logOffsetMetadata, validRecords, origin)
// Increment the log end offset. We do this immediately after the append because a // write to the transaction index below may fail and we want to ensure that the offsets // of future appends still grow monotonically. The resulting transaction index inconsistency // will be cleaned up after the log directory is recovered. Note that the end offset of the // ProducerStateManager will not be updated and the last stable offset will not advance // if the append to the transaction index fails. // 更新 logEndOffset。因为 transaction index 的写入可能失败,而我们又要保证未来写入消息的 offset 递增, // 所以在写入消息之后立即更新 LEO。因此产生的 transaction index 不一致将会在日志目录 recovered 的时候被解决。 // 注意,如果transaction index写入失败时,ProducerStateManager 的 end offset 不会被更新, // last stable offset 也不会前进。 updateLogEndOffset(appendInfo.lastOffset + 1)
// update the producer state // 更新事务状态 for (producerAppendInfo <- updatedProducers.values) { producerStateManager.update(producerAppendInfo) }
// update the transaction index with the true last stable offset. The last offset visible // to consumers using READ_COMMITTED will be limited by this value and the high watermark. for (completedTxn <- completedTxns) { val lastStableOffset = producerStateManager.lastStableOffset(completedTxn) segment.updateTxnIndex(completedTxn, lastStableOffset) producerStateManager.completeTxn(completedTxn) }
// always update the last producer id map offset so that the snapshot reflects the current offset // even if there isn't any idempotent data being written // 不太懂,以后再看 producerStateManager.updateMapEndOffset(appendInfo.lastOffset + 1)
// update the first unstable offset (which is used to compute LSO) 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")
if (unflushedMessages >= config.flushInterval) flush()
/** * Read messages from the log. * * @param startOffset The offset to begin reading at * @param maxLength The maximum number of bytes to read * @param isolation The fetch isolation, which controls the maximum offset we are allowed to read * @param minOneMessage If this is true, the first message will be returned even if it exceeds `maxLength` (if one exists) * @throws OffsetOutOfRangeException If startOffset is beyond the log end offset or before the log start offset * @return The fetch data information including fetch starting offset metadata and messages read. */ defread(startOffset: Long, maxLength: Int, isolation: FetchIsolation, minOneMessage: Boolean): FetchDataInfo = { maybeHandleIOException(s"Exception while reading from $topicPartition in dir ${dir.getParent}") { trace(s"Reading maximum $maxLength bytes at offset $startOffset from log with " + s"total length $size bytes")
val includeAbortedTxns = isolation == FetchTxnCommitted
// Because we don't use the lock for reading, the synchronization is a little bit tricky. // We create the local variables to avoid race conditions with updates to the log. // 读操作都没有加锁,所以这里也不打算取巧加锁。而是创建一个本地变量, // 用来保存最后一条消息的 offset,防止读到脏数据。 val endOffsetMetadata = nextOffsetMetadata val endOffset = endOffsetMetadata.messageOffset
// private val segments: ConcurrentNavigableMap[java.lang.Long, LogSegment] = new ConcurrentSkipListMap[java.lang.Long, LogSegment] // 在 segment map 中,找到 startOffset 所在的那一个 segment,从这个 segment 开始查找 var segmentEntry = segments.floorEntry(startOffset)
// return error on attempt to read beyond the log end offset or read below log start offset if (startOffset > endOffset || segmentEntry == null || startOffset < logStartOffset) thrownewOffsetOutOfRangeException(s"Received request for offset $startOffset for partition $topicPartition, " + s"but we only have log segments in the range $logStartOffset to $endOffset.")
// Do the read on the segment with a base offset less than the target offset // but if that segment doesn't contain any messages with an offset greater than that // continue to read from successive segments until we get some messages or we reach the end of the log // // 从 baseOffset 小于 startOffset 的 segment 开始遍历后续的所有 segment,查找消息 while (segmentEntry != null) { val segment = segmentEntry.getValue
val maxPosition = { // Use the max offset position if it is on this segment; otherwise, the segment size is the limit. // 1. 如果 maxOffsetMetadata 和我们要找的 offset 在同一个 segment, 就以它的 position 为查找的限制 // 2. 如果 maxOffsetMetadata 和我们要找的 offset 不在同一个 segment, // 则说明 maxOffsetMetadata 在后续的 segment 中,那么就以当前 segment 的 size 为查找的限制 if (maxOffsetMetadata.segmentBaseOffset == segment.baseOffset) { maxOffsetMetadata.relativePositionInSegment } else { segment.size } }
// okay we are beyond the end of the last segment with no data fetched although the start offset is in range, // this can happen when all messages with offset larger than start offsets have been deleted. // In this case, we will return the empty set with log end offset metadata // // 走到这里说明尽管 start offset 是在 max offset 的范围内,但是我们遍历完所有的 segment 也没有找到消息。 // 这种情况是因为所有 offset 大于 start offset 的消息都被删除了。 // 这时候,返回下一条待插入消息的 metaData(不包含 records) FetchDataInfo(nextOffsetMetadata, MemoryRecords.EMPTY) } }
/* * A log offset structure, including: * 1. the message offset * 2. the base message offset of the located segment * 3. the physical position on the located segment */
caseclassLogOffsetMetadata(messageOffset: Long, segmentBaseOffset: Long = Log.UnknownOffset, relativePositionInSegment: Int = LogOffsetMetadata.UnknownFilePosition) { ......
// 通过比较 segmentBaseOffset 可以判断两个 LogOffsetMetadata 是否处于同一个 segment defonSameSegment(that: LogOffsetMetadata): Boolean = { if (messageOffsetOnly) thrownewKafkaException(s"$this cannot compare its segment info with $that since it only has message offset info") this.segmentBaseOffset == that.segmentBaseOffset }
// compute the number of bytes between this offset to the given offset // if they are on the same segment and this offset precedes the given offset // 如果两个对象处于同一个 segment,计算他们对应的消息之间差了多少 bytes defpositionDiff(that: LogOffsetMetadata): Int = { if(!onSameSegment(that)) thrownewKafkaException(s"$this cannot compare its segment position with $that since they are not on the same segment") if(messageOffsetOnly) thrownewKafkaException(s"$this cannot compare its segment position with $that since it only has message offset info")
/** * Update high watermark with offset metadata. The new high watermark will be lower * bounded by the log start offset and upper bounded by the log end offset. * * 更新 high watermark。注意不能 < logStartOffset,也不能 ≥ logEndOffset * * @param highWatermarkMetadata the suggested high watermark with offset metadata * @return the updated high watermark offset */ defupdateHighWatermark(highWatermarkMetadata: LogOffsetMetadata): Long = { val endOffsetMetadata = logEndOffsetMetadata val newHighWatermarkMetadata = if (highWatermarkMetadata.messageOffset < logStartOffset) { LogOffsetMetadata(logStartOffset) } elseif (highWatermarkMetadata.messageOffset >= endOffsetMetadata.messageOffset) { endOffsetMetadata } else { highWatermarkMetadata }
/** * Update the high watermark to a new value if and only if it is larger than the old value. It is * an error to update to a value which is larger than the log end offset. * * This method is intended to be used by the leader to update the high watermark after follower * fetch offsets have been updated. * * 这个方法主要是用于 leader 副本更新了 follower 副本的 fetch offset 更新之后 * (也就是 follower 副本拉取了数据之后) * * @return the old high watermark, if updated by the new value */ defmaybeIncrementHighWatermark(newHighWatermark: LogOffsetMetadata): Option[LogOffsetMetadata] = { if (newHighWatermark.messageOffset > logEndOffset) thrownewIllegalArgumentException(s"High watermark $newHighWatermark update exceeds current " + s"log end offset $logEndOffsetMetadata")
// Ensure that the high watermark increases monotonically. We also update the high watermark when the new // offset metadata is on a newer segment, which occurs whenever the log is rolled to a new segment. // 确保 hw 是单调递增的 // 当 log 轮转的时候,offset metadata 移到新的 segment,也会更新 hw if (oldHighWatermark.messageOffset < newHighWatermark.messageOffset || (oldHighWatermark.messageOffset == newHighWatermark.messageOffset && oldHighWatermark.onOlderSegment(newHighWatermark))) { updateHighWatermarkMetadata(newHighWatermark) Some(oldHighWatermark) } else { None } } }