spark KafkaRDD的理解

4 篇文章 0 订阅

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值