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數據的支持。