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数据的支持。