spark KafkaRDD的理解

Spark版本 2.4.0

 

先從0-8版本的kafka說起。

當jobGenerator根據時間準備生成相應的job的時候,會依次在graph中調用各個輸入流的getOrCompute()方法來獲取得到rdd,在這裏DirectKafkaInputDStream的compute()方法將會被調用,在這裏將會在driver端生成一個時間批次的rdd,也就是KafkaRDD。

KafkaRDD的生成一共分爲這麼幾步。

首先,DirectKafkaInputDStream在driver端維護了一個kafka客戶端,在kafkaRDD生成的第一步,將會通過這個kafka客戶端獲取監聽topic各個分區的當前offset,而這個topic偏移量集合,也將是之後用來計算kafkaRDD消費進度的一個重要依據。

之後,將會在maxMessagesPerPartition()方法中計算當前時間,每個分區的一個最大消費消息數。

protected[streaming] def maxMessagesPerPartition(
    offsets: Map[TopicAndPartition, Long]): Option[Map[TopicAndPartition, Long]] = {

  val estimatedRateLimit = rateController.map { x => {
    val lr = x.getLatestRate()
    if (lr > 0) lr else initialRate
  }}

  // calculate a per-partition rate limit based on current lag
  val effectiveRateLimitPerPartition = estimatedRateLimit.filter(_ > 0) match {
    case Some(rate) =>
      val lagPerPartition = offsets.map { case (tp, offset) =>
        tp -> Math.max(offset - currentOffsets(tp), 0)
      }
      val totalLag = lagPerPartition.values.sum

      lagPerPartition.map { case (tp, lag) =>
        val backpressureRate = lag / totalLag.toDouble * rate
        tp -> (if (maxRateLimitPerPartition > 0) {
          Math.min(backpressureRate, maxRateLimitPerPartition)} else backpressureRate)
      }
    case None => offsets.map { case (tp, offset) => tp -> maxRateLimitPerPartition.toDouble }
  }

  if (effectiveRateLimitPerPartition.values.sum > 0) {
    val secsPerBatch = context.graph.batchDuration.milliseconds.toDouble / 1000
    Some(effectiveRateLimitPerPartition.map {
      case (tp, limit) => tp -> Math.max((secsPerBatch * limit).toLong, 1L)
    })
  } else {
    None
  }
}

這裏的最大消息數量計算主要源於,當spark開啓了反壓之後,將會不斷通過rateController計算每一批次的最大數率來防止spark中存在大量任務積壓的情況,在這裏,如果當前kafka的偏移量的數據大於此次流量控制的最大處理速率,將會根據規定的最大數率拉取數據,並根據一批任務的數據間隔,獲得最後準備拉取的消息總量。

maxMessagesPerPartition(offsets).map { mmp =>
  mmp.map { case (tp, messages) =>
    val lo = leaderOffsets(tp)
    tp -> lo.copy(offset = Math.min(currentOffsets(tp) + messages, lo.offset))
  }
}.getOrElse(leaderOffsets)

而後,在clamp()方法中,將會根據消息最大數量,在每個分區拉取得到每個分區最後決定消費到的偏移量進度。

val untilOffsets = clamp(latestLeaderOffsets(maxRetries))
val rdd = KafkaRDD[K, V, U, T, R](
  context.sparkContext, kafkaParams, currentOffsets, untilOffsets, messageHandler)

而每個分區準備在這批任務當中消費到的具體偏移量則作爲變量untilOffsets,在KafkaRDD的構造方法中,作爲參數傳入。

 

KafKaRDD被task攜帶並下發到executor進行執行的時候,將會調用r的compue()方法將會得到一個迭代器,同時kafkaRDD也實現了這個方法。而依次遍歷迭代這個迭代器獲取消息,也就是對kafka消息進行消息消費的開始。

override def compute(thePart: Partition, context: TaskContext): Iterator[R] = {
  val part = thePart.asInstanceOf[KafkaRDDPartition]
  assert(part.fromOffset <= part.untilOffset, errBeginAfterEnd(part))
  if (part.fromOffset == part.untilOffset) {
    logInfo(s"Beginning offset ${part.fromOffset} is the same as ending offset " +
      s"skipping ${part.topic} ${part.partition}")
    Iterator.empty
  } else {
    new KafkaRDDIterator(part, context)
  }
}

在compute()方法,實則就是根據相應的分區號,生成了一個KafkaRDDIterator,kafka數據的拉取,也是又其產生。

val kc = new KafkaCluster(kafkaParams)
val keyDecoder = classTag[U].runtimeClass.getConstructor(classOf[VerifiableProperties])
  .newInstance(kc.config.props)
  .asInstanceOf[Decoder[K]]
val valueDecoder = classTag[T].runtimeClass.getConstructor(classOf[VerifiableProperties])
  .newInstance(kc.config.props)
  .asInstanceOf[Decoder[V]]
val consumer = connectLeader

每一個KafkaRDDIterator都將在生成後與kafka建立連接,並在通過getNext()方法嘗試獲取消息進行處理的時候,調用fetchBatch()方法,連接kafka並獲取消息進行消費。

private def fetchBatch: Iterator[MessageAndOffset] = {
  val req = new FetchRequestBuilder()
    .clientId(consumer.clientId)
    .addFetch(part.topic, part.partition, requestOffset, kc.config.fetchMessageMaxBytes)
    .build()
  val resp = consumer.fetch(req)
  handleFetchErr(resp)
  // kafka may return a batch that starts before the requested offset
  resp.messageSet(part.topic, part.partition)
    .iterator
    .dropWhile(_.offset < requestOffset)
}

到0-10版本。

加入了消費者連接的緩存。

val useConsumerCache = context.conf.getBoolean("spark.streaming.kafka.consumer.cache.enabled",
  true)
val rdd = new KafkaRDD[K, V](context.sparkContext, executorKafkaParams, offsetRanges.toArray,
  getPreferredHosts, useConsumerCache)

在driver端DirectKafkaInputDStream的compute()方法,在生成kafkaRDD之前,會獲取spark參數判斷是否需要使用緩存的,並將其帶到executor端,並在executor端準備拉取數據的時候判斷是否可以服用消費者減少kafka連接的連接次數。

 

並在stream中,給出了commitAsync()方法,在外部手動調用這個方法後,將會異步提交當前的消費進度,之所以是異步提交,是將當前消費進度提交到了一個隊列中,在stream的compute()方法最後,將會調用commitAll()方法將隊列當中的消費進度更新到隊列爲空,並並向kafka提交自己的消費進度。

 

並增加了compact的支持,在原有的KafkaRDDIterator基礎上增加了CompactedKafkaRDDIterator,支持kafka compact數據的支持。

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