Kafka学习五

前面我们知道其重要的启动方法里面有关的方法:它的注释是非常具有启发性的, 启动API,以启动Kafka服务器的单个实例。实例化LogManager,SocketServer和请求处理程序-KafkaRequestHandlers。也即告诉我们它会启动kafka服务实例,同时实例是单个的,同时里面实例化日志管理器、socket服务器、kafka请求处理器。而启动kafka服务中,不免会启动kafka控制器,而kafka控制器中涉及到主从broker服务、主从副本、状态机、监听、重平衡以及与其他broker通信。下面我们来看日志相关的信息:

//日志管理器启动
logManager = LogManager(config, initialOfflineDirs, zkClient, brokerState, kafkaScheduler, time, brokerTopicStats, logDirFailureChannel)
logManager.startup()

//查看LogManager对象
object LogManager {

  //恢复点检查点文件
  val RecoveryPointCheckpointFile = "recovery-point-offset-checkpoint"
  //日志起始偏移量检查点文件
  val LogStartOffsetCheckpointFile = "log-start-offset-checkpoint"
  //生产者id过期检查间隔时间 10分钟
  val ProducerIdExpirationCheckIntervalMs = 10 * 60 * 1000

  //apply方法
  def apply(config: KafkaConfig,
            initialOfflineDirs: Seq[String],
            zkClient: KafkaZkClient,
            brokerState: BrokerState,
            kafkaScheduler: KafkaScheduler,
            time: Time,
            brokerTopicStats: BrokerTopicStats,
            logDirFailureChannel: LogDirFailureChannel): LogManager = {
    val defaultProps = KafkaServer.copyKafkaConfigToLog(config)
    val defaultLogConfig = LogConfig(defaultProps)

    // read the log configurations from zookeeper
    //从zookeeper中读取日志配置
    val (topicConfigs, failed) = zkClient.getLogConfigs(zkClient.getAllTopicsInCluster, defaultProps)
    if (!failed.isEmpty) throw failed.head._2

    val cleanerConfig = LogCleaner.cleanerConfig(config)

    //创建LogManager对象,里面有日志目录
    new LogManager(logDirs = config.logDirs.map(new File(_).getAbsoluteFile),
      initialOfflineDirs = initialOfflineDirs.map(new File(_).getAbsoluteFile),
      topicConfigs = topicConfigs,
      initialDefaultConfig = defaultLogConfig,
      cleanerConfig = cleanerConfig,
      recoveryThreadsPerDataDir = config.numRecoveryThreadsPerDataDir,
      flushCheckMs = config.logFlushSchedulerIntervalMs,
      flushRecoveryOffsetCheckpointMs = config.logFlushOffsetCheckpointIntervalMs,
      flushStartOffsetCheckpointMs = config.logFlushStartOffsetCheckpointIntervalMs,
      retentionCheckMs = config.logCleanupIntervalMs,
      maxPidExpirationMs = config.transactionIdExpirationMs,
      scheduler = kafkaScheduler,
      brokerState = brokerState,
      brokerTopicStats = brokerTopicStats,
      logDirFailureChannel = logDirFailureChannel,
      time = time)
  }
}

//日志管理
@threadsafe
class LogManager(logDirs: Seq[File],
                 initialOfflineDirs: Seq[File],
                 val topicConfigs: Map[String, LogConfig], // note that this doesn't get updated after creation
                 val initialDefaultConfig: LogConfig,
                 val cleanerConfig: CleanerConfig,
                 recoveryThreadsPerDataDir: Int,
                 val flushCheckMs: Long,
                 val flushRecoveryOffsetCheckpointMs: Long,
                 val flushStartOffsetCheckpointMs: Long,
                 val retentionCheckMs: Long,
                 val maxPidExpirationMs: Int,
                 scheduler: Scheduler,
                 val brokerState: BrokerState,
                 brokerTopicStats: BrokerTopicStats,
                 logDirFailureChannel: LogDirFailureChannel,
                 time: Time) extends Logging with KafkaMetricsGroup {
 //当前日志
  private val currentLogs = new Pool[TopicPartition, Log]() 
  //future日志    
  private val futureLogs = new Pool[TopicPartition, Log]()
}

//查看Log中的文件
//日志实际的段 =>日志段
@threadsafe
class Log(@volatile var dir: File,
          @volatile var config: LogConfig,
          @volatile var logStartOffset: Long,
          @volatile var recoveryPoint: Long,
          scheduler: Scheduler,
          brokerTopicStats: BrokerTopicStats,
          time: Time,
          val maxProducerIdExpirationMs: Int,
          val producerIdExpirationCheckIntervalMs: Int,
          val topicPartition: TopicPartition,
          val producerStateManager: ProducerStateManager,
          logDirFailureChannel: LogDirFailureChannel) extends Logging with KafkaMetricsGroup {
  private val segments: ConcurrentNavigableMap[java.lang.Long, LogSegment] = new ConcurrentSkipListMap[java.lang.Long, LogSegment]
}


//文件消息、偏移量索引、时间戳索引、事务索引、基准偏移量、索引间隔字节、扰动值时间、最大段时间、时间
@nonthreadsafe
class LogSegment private[log] (val log: FileRecords,
                               val offsetIndex: OffsetIndex,
                               val timeIndex: TimeIndex,
                               val txnIndex: TransactionIndex,
                               val baseOffset: Long,
                               val indexIntervalBytes: Int,
                               val rollJitterMs: Long,
                               val maxSegmentMs: Long,
                               val maxSegmentBytes: Int,
                               val time: Time) extends Logging {
/**
   * Append the given messages starting with the given offset. Add
   * an entry to the index if needed.
   *
   * It is assumed this method is being called from within a lock.
   * 从给定的偏移量开始附加给定的消息。 如果需要,将一个条目添加到索引。 假定正在从锁内调用此方法。 重点 线程不安全
   *
   * @param firstOffset The first offset in the message set.
   * @param largestOffset The last offset in the message set
   * @param largestTimestamp The largest timestamp in the message set.
   * @param shallowOffsetOfMaxTimestamp The offset of the message that has the largest timestamp in the messages to append.
   * @param records The log entries to append.
   * @return the physical position in the file of the appended records
   */
  @nonthreadsafe
  def append(firstOffset: Long,
             largestOffset: Long,
             largestTimestamp: Long,
             shallowOffsetOfMaxTimestamp: Long,
             records: MemoryRecords): Unit = {
    if (records.sizeInBytes > 0) {
      trace("Inserting %d bytes at offset %d at position %d with largest timestamp %d at shallow offset %d"
          .format(records.sizeInBytes, firstOffset, log.sizeInBytes(), largestTimestamp, shallowOffsetOfMaxTimestamp))
      val physicalPosition = log.sizeInBytes()
      if (physicalPosition == 0)
        rollingBasedTimestamp = Some(largestTimestamp)
      // append the messages
      //拼接消息  
      require(canConvertToRelativeOffset(largestOffset), "largest offset in message set can not be safely converted to relative offset.")
      //做日志拼接   重要
      val appendedBytes = log.append(records)
      trace(s"Appended $appendedBytes to ${log.file()} at offset $firstOffset")
      // Update the in memory max timestamp and corresponding offset.
      if (largestTimestamp > maxTimestampSoFar) {
        maxTimestampSoFar = largestTimestamp
        offsetOfMaxTimestamp = shallowOffsetOfMaxTimestamp
      }
      // append an entry to the index (if needed)
      //拼接entry到index索引中   重要
      if(bytesSinceLastIndexEntry > indexIntervalBytes) {
        offsetIndex.append(firstOffset, physicalPosition)
        timeIndex.maybeAppend(maxTimestampSoFar, offsetOfMaxTimestamp)
        bytesSinceLastIndexEntry = 0
      }
      bytesSinceLastIndexEntry += records.sizeInBytes
    }
  }
}

执行拼接操作log.append(records):

/**
 * Append log batches to the buffer 将日志批次追加到缓冲区
 * @param records The records to append
 * @return the number of bytes written to the underlying file
 */
public int append(MemoryRecords records) throws IOException {
    int written = records.writeFullyTo(channel);
    size.getAndAdd(written);
    return written;
}

 /**
     * Write all records to the given channel (including partial records).
     * 将所有记录写入给定通道(包括部分记录)。
     * @param channel The channel to write to
     * @return The number of bytes written
     * @throws IOException For any IO errors writing to the channel
     */
    public int writeFullyTo(GatheringByteChannel channel) throws IOException {
        buffer.mark();
        int written = 0;
        while (written < sizeInBytes())
            written += channel.write(buffer);
        buffer.reset();
        return written;
    }

拼接操作offsetIndex.append偏移量索引拼接操作:

 /**
  * Append an entry for the given offset/location pair to the index. This entry must have a larger offset than all subsequent entries.
  * 将给定偏移量/位置对的条目追加到索引。 该条目的偏移量必须大于所有后续条目的偏移量。
  */
def append(offset: Long, position: Int) {
  inLock(lock) {
    require(!isFull, "Attempt to append to a full index (size = " + _entries + ").")
    if (_entries == 0 || offset > _lastOffset) {
      trace(s"Adding index entry $offset => $position to ${file.getAbsolutePath}")
      mmap.putInt((offset - baseOffset).toInt)
      mmap.putInt(position)
      _entries += 1
      _lastOffset = offset
      require(_entries * entrySize == mmap.position(), entries + " entries but file position in index is " + mmap.position() + ".")
    } else {
      throw new InvalidOffsetException(s"Attempt to append an offset ($offset) to position $entries no larger than" +
        s" the last offset appended (${_lastOffset}) to ${file.getAbsolutePath}.")
    }
  }
}

//MappedByteBuffer
 @volatile
  protected var mmap: MappedByteBuffer = {
    val newlyCreated = file.createNewFile()
    val raf = if (writable) new RandomAccessFile(file, "rw") else new RandomAccessFile(file, "r")
    try {
      /* pre-allocate the file if necessary */
      if(newlyCreated) {
        if(maxIndexSize < entrySize)
          throw new IllegalArgumentException("Invalid max index size: " + maxIndexSize)
        raf.setLength(roundDownToExactMultiple(maxIndexSize, entrySize))
      }

      /* memory-map the file */
      _length = raf.length()
      val idx = {
        if (writable)
          raf.getChannel.map(FileChannel.MapMode.READ_WRITE, 0, _length)
        else
          raf.getChannel.map(FileChannel.MapMode.READ_ONLY, 0, _length)
      }
      /* set the position in the index for the next entry */
      if(newlyCreated)
        idx.position(0)
      else
        // if this is a pre-existing index, assume it is valid and set position to last entry
        idx.position(roundDownToExactMultiple(idx.limit(), entrySize))
      idx
    } finally {
      CoreUtils.swallow(raf.close(), this)
    }
  }

下面我们来看看日志:

一个主题对应多个分区,而一个分区又对应一个文件夹目录,而一个文件夹目录中有一个log文件和两个索引IndexFile和时间戳文件,甚至如果启用事务会有事务的相关文件。

日志段,日志的一部分。每个段都有两个组成部分:日志和索引。日志是包含实际消息的FileMessageSet。索引是一个OffsetIndex,它从逻辑偏移量映射到物理文件位置。kafka创建topic时可以指定topic的分区个数,每个broker按照自己分到的topic的分区创建对应的log,其中每个log由多个LogSement组成,每个LogSement以LogSement的第一条message索引供segments管理。

其中FileMessageSet通过设定segment之内的start和end来读取segment内的文件,OffsetIndex是Segment里面的Message索引,它并不是每条message建立索引,而是间隔log.index.interval.bytes条message添加一条索引。因此查找每一条记录的话,如果给定topic和offset,则分两步完成:快速定位segmentFile,segment file中查找msg trunk。

日志相关参数信息:

/**
 * Helper functions for logs
  * 日志的辅助功能
 */
object Log {

  /** a log file */
  //日志文件后缀 .log
  val LogFileSuffix = ".log"

  /** an index file */
  //索引文件后缀
  val IndexFileSuffix = ".index"

  /** a time index file */
  //时间索引文件后缀
  val TimeIndexFileSuffix = ".timeindex"

  //生产者快照文件后缀
  val ProducerSnapshotFileSuffix = ".snapshot"

  /** an (aborted) txn index */
  //(中止的)txn索引
  val TxnIndexFileSuffix = ".txnindex"

  /** a file that is scheduled to be deleted */
  // 计划删除的文件
  val DeletedFileSuffix = ".deleted"

  /** A temporary file that is being used for log cleaning */
  // 清理文件后缀 用于日志清理的临时文件
  val CleanedFileSuffix = ".cleaned"

  /** A temporary file used when swapping files into the log */
  //将文件交换到日志时使用的临时文件
  val SwapFileSuffix = ".swap"

  /** Clean shutdown file that indicates the broker was cleanly shutdown in 0.8 and higher.
   * This is used to avoid unnecessary recovery after a clean shutdown. In theory this could be
   * avoided by passing in the recovery point, however finding the correct position to do this
   * requires accessing the offset index which may not be safe in an unclean shutdown.
   * For more information see the discussion in PR#2104
   */
  val CleanShutdownFile = ".kafka_cleanshutdown"

  /** a directory that is scheduled to be deleted */
  // 计划删除的目录
  val DeleteDirSuffix = "-delete"

  /** a directory that is used for future partition */
  // 用于future分区的目录
  val FutureDirSuffix = "-future"
 //删除目录正则
  private val DeleteDirPattern = Pattern.compile(s"^(\\S+)-(\\S+)\\.(\\S+)$DeleteDirSuffix")
  //future目录正则
  private val FutureDirPattern = Pattern.compile(s"^(\\S+)-(\\S+)\\.(\\S+)$FutureDirSuffix")

文件消息

/**
 * A {@link Records} implementation backed by a file. An optional start and end position can be applied to this
 * instance to enable slicing a range of the log records.
 * 由文件支持的{@link Records}实现。 可以将可选的开始位置和结束位置应用于此实例,以允许对一系列日志记录进行切片
 */
public class FileRecords extends AbstractRecords implements Closeable {
    //是否分片
    private final boolean isSlice;
    //开始
    private final int start;
    //最终
    private final int end;

    //批次
    private final Iterable<FileLogInputStream.FileChannelRecordBatch> batches;

    // mutable state
    private final AtomicInteger size;
    //通道
    private final FileChannel channel;
    //文件
    private volatile File file;

  /**
     * The {@code FileRecords.open} methods should be used instead of this constructor whenever possible.
     * The constructor is visible for tests.
     * 尽可能使用{@code FileRecords.open}方法代替此构造方法。该构造方法对于测试是可见的。
     */
    public FileRecords(File file,
                       FileChannel channel,
                       int start,
                       int end,
                       boolean isSlice) throws IOException {
        this.file = file;
        this.channel = channel;
        this.start = start;
        this.end = end;
        this.isSlice = isSlice;
        this.size = new AtomicInteger();

        if (isSlice) {
            // don't check the file size if this is just a slice view
            size.set(end - start);
        } else {
            int limit = Math.min((int) channel.size(), end);
            size.set(limit - start);

            // if this is not a slice, update the file pointer to the end of the file
            // set the file position to the last byte in the file
            channel.position(limit);
        }

        batches = batchesFrom(start);
    }

OffsetIndex:

/**
 * Find the largest offset less than or equal to the given targetOffset
 * and return a pair holding this offset and its corresponding physical file position.
  * 查找小于或等于给定targetOffset的最大偏移量,并返回一对包含该偏移量及其对应物理文件位置的偏移量。
 *
 * @param targetOffset The offset to look up.
 * @return The offset found and the corresponding file position for this offset
 *         If the target offset is smaller than the least entry in the index (or the index is empty),
 *         the pair (baseOffset, 0) is returned.
 */
class OffsetIndex(_file: File, baseOffset: Long, maxIndexSize: Int = -1, writable: Boolean = true)
    extends AbstractIndex[Long, Int](_file, baseOffset, maxIndexSize, writable) {
def lookup(targetOffset: Long): OffsetPosition = {
  maybeLock(lock) {
    val idx = mmap.duplicate
    val slot = largestLowerBoundSlotFor(idx, targetOffset, IndexSearchType.KEY)
    if(slot == -1)
      OffsetPosition(baseOffset, 0)
    else
      parseEntry(idx, slot).asInstanceOf[OffsetPosition]
   }
  }
}

case class OffsetPosition(offset: Long, position: Int) extends IndexEntry {
  override def indexKey = offset
  override def indexValue = position.toLong
}

  override def parseEntry(buffer: ByteBuffer, n: Int): IndexEntry = {
      OffsetPosition(baseOffset + relativeOffset(buffer, n), physical(buffer, n))
  }

  /**
   * Get the nth offset mapping from the index 从索引获取第n个偏移量映射
   * @param n The entry number in the index
   * @return The offset/position pair at that entry
   */
  def entry(n: Int): OffsetPosition = {
    maybeLock(lock) {
      if(n >= _entries)
        throw new IllegalArgumentException(s"Attempt to fetch the ${n}th entry from index ${file.getAbsolutePath}, " +
          s"which has size ${_entries}.")
      val idx = mmap.duplicate
      OffsetPosition(relativeOffset(idx, n), physical(idx, n))
    }
  }

对日志段进行具体查询,查询哪一个文件

/**
 * Lookup lower and upper bounds for the given target. 查找给定目标的上下限。
 */
private def indexSlotRangeFor(idx: ByteBuffer, target: Long, searchEntity: IndexSearchEntity): (Int, Int) = {
  // check if the index is empty
  if(_entries == 0)
    return (-1, -1)

  // check if the target offset is smaller than the least offset
  if(compareIndexEntry(parseEntry(idx, 0), target, searchEntity) > 0)
    return (-1, 0)

  // binary search for the entry
  var lo = 0
  var hi = _entries - 1
  while(lo < hi) {
    val mid = ceil(hi/2.0 + lo/2.0).toInt
    val found = parseEntry(idx, mid)
    val compareResult = compareIndexEntry(found, target, searchEntity)
    if(compareResult > 0)
      hi = mid - 1
    else if(compareResult < 0)
      lo = mid
    else
      return (mid, mid)
  }

  (lo, if (lo == _entries - 1) -1 else lo + 1)
}

日志管理启动:

//恢复点检查点文件
  val RecoveryPointCheckpointFile = "recovery-point-offset-checkpoint"
  //日志起始偏移量检查点文件
  val LogStartOffsetCheckpointFile = "log-start-offset-checkpoint"
@volatile private var recoveryPointCheckpoints = liveLogDirs.map(dir =>
    (dir, new OffsetCheckpointFile(new File(dir, RecoveryPointCheckpointFile), logDirFailureChannel))).toMap
  @volatile private var logStartOffsetCheckpoints = liveLogDirs.map(dir =>
    (dir, new OffsetCheckpointFile(new File(dir, LogStartOffsetCheckpointFile), logDirFailureChannel))).toMap

  private val preferredLogDirs = new ConcurrentHashMap[TopicPartition, String]()

/**
 *  Start the background threads to flush logs and do log cleanup
 *  启动backgroud线程去刷新日志和操作log清理
 */
def startup() {
  /* Schedule the cleanup task to delete old logs */
  //定时清理任务:删除旧日志
  if (scheduler != null) {
    info("Starting log cleanup with a period of %d ms.".format(retentionCheckMs))
    scheduler.schedule("kafka-log-retention",
                       cleanupLogs _,
                       delay = InitialTaskDelayMs,
                       period = retentionCheckMs,
                       TimeUnit.MILLISECONDS)
    info("Starting log flusher with a default period of %d ms.".format(flushCheckMs))
    scheduler.schedule("kafka-log-flusher",
                       flushDirtyLogs _,
                       delay = InitialTaskDelayMs,
                       period = flushCheckMs,
                       TimeUnit.MILLISECONDS)
    scheduler.schedule("kafka-recovery-point-checkpoint",
                       checkpointLogRecoveryOffsets _,
                       delay = InitialTaskDelayMs,
                       period = flushRecoveryOffsetCheckpointMs,
                       TimeUnit.MILLISECONDS)
    scheduler.schedule("kafka-log-start-offset-checkpoint",
                       checkpointLogStartOffsets _,
                       delay = InitialTaskDelayMs,
                       period = flushStartOffsetCheckpointMs,
                       TimeUnit.MILLISECONDS)
    scheduler.schedule("kafka-delete-logs", // will be rescheduled after each delete logs with a dynamic period
                       deleteLogs _,
                       delay = InitialTaskDelayMs,
                       unit = TimeUnit.MILLISECONDS)
  }
  //日志合并,把小的多个logSegment合并为大的一个logsegment
  if (cleanerConfig.enableCleaner)
    cleaner.startup()
}

消息组成:

object Message {

  /**
   * The current offset and size for all the fixed-length fields
   */
  val CrcOffset = 0
  val CrcLength = 4
  val MagicOffset = CrcOffset + CrcLength
  val MagicLength = 1
  val AttributesOffset = MagicOffset + MagicLength
  val AttributesLength = 1
  // Only message format version 1 has the timestamp field.
  val TimestampOffset = AttributesOffset + AttributesLength
  val TimestampLength = 8
  val KeySizeOffset_V0 = AttributesOffset + AttributesLength
  val KeySizeOffset_V1 = TimestampOffset + TimestampLength
  val KeySizeLength = 4
  val KeyOffset_V0 = KeySizeOffset_V0 + KeySizeLength
  val KeyOffset_V1 = KeySizeOffset_V1 + KeySizeLength
  val ValueSizeLength = 4
}

总结:kafka中,一个主题topic中包含多个分区,而一个borker通常会分到多个分区,而特定的分区又对应多个日志目录,因此日志目录中存在日志段的概念,而一个日志段对应一个日志文件和一个日志索引文件和时间戳索引文件。通常如果存在事务的话,还有事务的文件和中止文件。由于kafka中,存在日志段的概念,因此,其采用跳跃表的方式定位到具体的区间,从而定位具体的哪一个日志文件。其所建的索引是采用间隔的方式建立的,也即通过建稀疏索引的方式定位到具体的日志文件。每一个LogSegment以第一个message为索引提供segment管理。在原来的版本中,LogSegment采用FileMessageSet和start、end进行确定,而现在则是采用FileMessageRecord.

本文分享自微信公众号 - 后端技术学习(gh_9f5627e6cc61),作者:路行的亚洲

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-10-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • kafka学习二 -发送消息

    从源码中我们发现在Sender的run方法中,并没有涉及到append追加操作。因此可以看到源码中,如果消息收集器中的消息收集结果为空或者新的消息批次已经创建好...

    路行的亚洲
  • RocketMQ学习六-消息存储

    消息存储主要做的事情:首先将消息放入,然后进行消息追加,进行统计,然后进行刷盘操作,最后进行HA主从同步。此时的消息放入是在CommitLog中会进行转发到Co...

    路行的亚洲
  • kafka学习六-生产延迟操作

    这里思考问题,什么时候会用到延迟组件,同时哪些时候会用到延迟组件,同时为什么要用延迟组件?

    路行的亚洲
  • 数据扩充在NLP中什么时候有助于泛化?(CS.CL)

    神经模型常常利用表面的(“弱”)特性来获得良好的性能,而不是派生出我们希望模型使用的更一般的(“强”)特性。克服这种倾向是表征学习和ML公平性等领域的核心挑战。...

    用户7236395
  • SAP S/4HANA最佳业务实践:Order-to-Cash订单到收款-4报价单处理

    •The tile Manage Sales Quotationsis part of the business catalog Sales –Quotatio...

    SAP最佳业务实践
  • Using a self-rewriting README powered by GitHub to track TILs

    Using a self-rewriting README powered by GitHub Actions to track TILs

    仇诺伊
  • 使用DenyHosts保护服务器安全

    在几个月前,笔者介绍了一种保护服务器安全的方法 自动禁止攻击IP登陆SSH,保护服务器安全。这种方法需要自己去动手写相应的脚本,今天要介绍的是开源的脚本实现。

    zhangheng
  • [java][JEECG] Maven settings.xml JEECG项目初始化

    懒得整理了,看懂了就看,看不懂自己琢磨JEECG的帮助文档去,不过嘛我喜欢用Intelij IDEA,他里面都是别的IDE,不喜欢那个。哈哈哈

    landv
  • composer 报错集合

    This is a list of common pitfalls on using Composer, and how to avoid them.

    双面人
  • Save Your Linux Machine From Certain Death

    Troubleshooting damaged systems is an essential skill of every SysAdmin, SRE, or...

    仇诺伊

扫码关注云+社区

领取腾讯云代金券