获取指定时间的offsets类:GetOffsetShell 源码导读

总览:全程有两种rpc调用,server端就是kafka集群(通过给定的参数broker list进行连接),第一种只调用一次,获取topic的元数据信息:topicsMetadata,其中包含了该topic的partition数量及每个partition所在的brokerid(当然也能够找到该partition的leader)。第二种调用多次(次数=partition数,也就是每个partition需要调用一次)直接读取该partition的指定时间的offset。

// 第一次rpc,从集群中获取topic的元数据信息
val topicsMetadata = ClientUtils.fetchTopicMetadata(Set(topic), metadataTargetBrokers, clientId, maxWaitMs).topicsMetadata

// 获取该topic的所有partition id
val partitions =
  if(partitionList == "") {
    topicsMetadata.head.partitionsMetadata.map(_.partitionId)
  } else {
    partitionList.split(",").map(_.toInt).toSeq
  }
 
// 遍历所有partition
partitions.foreach { partitionId =>
  // 从第一次rpc获得的topic元数据信息中拿取该partition的元数据,包括这个partition所位于的broker。
  val partitionMetadataOpt = topicsMetadata.head.partitionsMetadata.find(_.partitionId == partitionId)
  partitionMetadataOpt match {
    case Some(metadata) =>
      // 获取该partition所处leader的broker信息
      metadata.leader match {
        case Some(leader) =>
          // 构建一个SimpleConsumer(该consumer不同于High consumer,不需要向zookeeper中注册group)
          val consumer = new SimpleConsumer(leader.host, leader.port, 10000, 100000, clientId)
          val topicAndPartition = TopicAndPartition(topic, partitionId)
          // OffsetRequest是一个拥有样例类的伴生对象,其样例类继承自RequestOrResponse,requestid设置为OffsetsKey,这个requestid在server端处理时会用到,用来获取对应的处理方法。
          val request = OffsetRequest(Map(topicAndPartition -> PartitionOffsetRequestInfo(time, nOffsets)))
          // 每个partition调用一次的第二种rpc,得到对应时间的offsets
          val offsets = consumer.getOffsetsBefore(request).partitionErrorAndOffsets(topicAndPartition).offsets

          println("%s:%d:%s".format(topic, partitionId, offsets.mkString(",")))
        case None => System.err.println("Error: partition %d does not have a leader. Skip getting offsets".format(partitionId))
      }
    case None => System.err.println("Error: partition %d does not exist".format(partitionId))
  }
} 

server端,kafka启动时会构建一个:KafkaRequestHandlerPool,从配置文件中读取配置属性:num.io.threads 默认值为8。每个线程都是一个KafkaRequestHandler线程,将KafkaApis这个方法作为参数传入,在线程处理中,每遇到request,就用KafkaApis的handle方法来处理。

该handle方法针对不同的requestid有不同的处理方法,而针对OffsetsKey调用的是handleOffsetRequest->fetchOffsets->fetchOffsetsBefore  到了这一步,发现实现是如此的简单,原来kafka定位时间的offset,仅仅是用的logsegment 的最后修改时间,如下图(以log结尾的就是一个logsegment,而index中则记录了该log的offset及message size,从而能够方便的定位到某个offset的具体message位置。有点跑题~~)后面贴上fetchOffsetsBefore的代码,很容易读懂,就是获取最后修改时间timestamp参数的那个logsegment,直接返回这个logsegment的起始offset,over。

获取指定时间的offsets类:GetOffsetShell 源码导读_第1张图片

def fetchOffsetsBefore(log: Log, timestamp: Long, maxNumOffsets: Int): Seq[Long] = {
  val segsArray = log.logSegments.toArray
  var offsetTimeArray: Array[(Long, Long)] = null
  if(segsArray.last.size > 0)
    offsetTimeArray = new Array[(Long, Long)](segsArray.length + 1)
  else
    offsetTimeArray = new Array[(Long, Long)](segsArray.length)

  for(i <- 0 until segsArray.length)
    offsetTimeArray(i) = (segsArray(i).baseOffset, segsArray(i).lastModified)
  if(segsArray.last.size > 0)
    offsetTimeArray(segsArray.length) = (log.logEndOffset, SystemTime.milliseconds)

  var startIndex = -1
  timestamp match {
    case OffsetRequest.LatestTime =>
      startIndex = offsetTimeArray.length - 1
    case OffsetRequest.EarliestTime =>
      startIndex = 0
    case _ =>
      var isFound = false
      debug("Offset time array = " + offsetTimeArray.foreach(o => "%d, %d".format(o._1, o._2)))
      startIndex = offsetTimeArray.length - 1
      while (startIndex >= 0 && !isFound) {
        if (offsetTimeArray(startIndex)._2 <= timestamp)
          isFound = true
        else
          startIndex -=1
      }
  }

  val retSize = maxNumOffsets.min(startIndex + 1)
  val ret = new Array[Long](retSize)
  for(j <- 0 until retSize) {
    ret(j) = offsetTimeArray(startIndex)._1
    startIndex -= 1
  }
  // ensure that the returned seq is in descending order of offsets
  ret.toSeq.sortBy(- _)
}

kafka获取指定时间的offsets竟然使用了如此投机取巧的办法,不过如果通过良好的配置,还是可以接受的。

# log的最长轮转时间,也就是如果大小一直没有达到下一个条件,经过log.roll.hours,log也需要改名。
log.roll.hours=72  
# log即便没有达到上面一个时间的条件,当大小达到下面的条件,也需要改名。
log.segment.bytes=536870912

这两个配置选项可以针对具体的topic进行配置(可覆盖server端写死的配置)有了上面两个选项,可以方便的进行调整,比如改小准确率要求高的topic的轮转时间或大小等方式,视具体情况来定。


 

你可能感兴趣的:(kafka)