SparkStreaming 中用 Direct 方式拉取越来超慢

本文深入探讨Spark Streaming如何与Kafka集成,解析Direct方式拉取数据的机制,包括untilOffsets的计算方法,以及自动推测程序执行情况的backpressure机制。详细分析了RateController的实现,介绍了动态调整消费速率的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们知道 SparkStreaming 用 Direct 的方式拉取 Kafka 数据时,是根据 kafka 中的 fromOffsets 和 untilOffsets 来进行获取数据的,而 fromOffsets 一般都是需要我们自己管理的,而每批次的 untilOffsets 是由 Driver 程序自动帮我们算出来的。 
于是产生了一个疑问:untilOffsets 是怎么算出来的? 
接下来就通过查看源码的方式来找出答案~

首先我们写一个最简单的 wordcount 程序,代码如下:

/**
  * Created by Lin_wj1995 on 2018/4/19.
  * 来源:https://blog.youkuaiyun.com/Lin_wj1995
  */
object DirectKafkaWordCount {
  def main(args: Array[String]) {
    val Array(brokers, topics) = args
    val sparkConf = new SparkConf().setAppName("DirectKafkaWordCount")
    val ssc = new StreamingContext(sparkConf, Seconds(2))

    val topicsSet = topics.split(",").toSet
    val kafkaParams = Map[String, String]("metadata.broker.list" -> brokers)
    val messages = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topicsSet)

    //拿到数据
    val lines = messages.map(_._2)
    val words = lines.flatMap(_.split(" "))
    val wordCounts = words.map(x => (x, 1L)).reduceByKey(_ + _)
    wordCounts.print()

    // 启动
    ssc.start()
    ssc.awaitTermination()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
我们可以看出, createDirectStream 是获得数据的关键方法的,我们点击进去

  def createDirectStream[
    K: ClassTag,
    V: ClassTag,
    KD <: Decoder[K]: ClassTag,
    VD <: Decoder[V]: ClassTag] (
      ssc: StreamingContext,
      kafkaParams: Map[String, String],
      topics: Set[String]
  ): InputDStream[(K, V)] = {
    val messageHandler = (mmd: MessageAndMetadata[K, V]) => (mmd.key, mmd.message)
    //kafka cluster 连接对象
    val kc = new KafkaCluster(kafkaParams)
    //读取数据的开始位置
    val fromOffsets = getFromOffsets(kc, kafkaParams, topics)
    //该方法返回了一个DirectKafkaInputDStream的对象
    new DirectKafkaInputDStream[K, V, KD, VD, (K, V)](
      ssc, kafkaParams, fromOffsets, messageHandler)
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ok,重点来了,点击 DirectKafkaInputDStream ,看一下该类内部是如何的,由于该类内部的方法都是重点,所有我把该类重点的属性和方法有选择性的贴出来: 
建议从下往上读!~

private[streaming]
class DirectKafkaInputDStream[
  K: ClassTag,
  V: ClassTag,
  U <: Decoder[K]: ClassTag,
  T <: Decoder[V]: ClassTag,
  R: ClassTag](
    ssc_ : StreamingContext,
    val kafkaParams: Map[String, String],
    val fromOffsets: Map[TopicAndPartition, Long],
    messageHandler: MessageAndMetadata[K, V] => R
  ) extends InputDStream[R](ssc_) with Logging {
  /**
    * 为了拿到每个分区leader上的最新偏移量(默认值为1),Driver发出请求的最大的连续重试次数
    * 默认值为1,也就是说最多请求 2 次
    */
  val maxRetries = context.sparkContext.getConf.getInt(
    "spark.streaming.kafka.maxRetries", 1)

  /**
    * 通过 receiver tracker 异步地维持和发送新的 rate limits 给 receiver
    * 注意:如果参数 spark.streaming.backpressure.enabled 没有设置,那么返回为None
   */
  override protected[streaming] val rateController: Option[RateController] = {
    /**
      * isBackPressureEnabled方法对应着“spark.streaming.backpressure.enabled”参数
      * 参数说明:简单来讲就是自动推测程序的执行情况并控制接收数据的条数,为了防止处理数据的时间大于批次时间而导致的数据堆积
      *           默认是没有开启的
      */
    if (RateController.isBackPressureEnabled(ssc.conf)) {
      Some(new DirectKafkaRateController(id,
        RateEstimator.create(ssc.conf, context.graph.batchDuration)))
    } else {
      None
    }
  }

  //拿到与Kafka集群的连接
  protected val kc = new KafkaCluster(kafkaParams)

  //每个partition每次最多获取多少条数据,默认是0
  private val maxRateLimitPerPartition: Int = context.sparkContext.getConf.getInt(
      "spark.streaming.kafka.maxRatePerPartition", 0)

  /**
    * 真实算出每个partition获取数据的最大条数
    */
  protected def maxMessagesPerPartition: Option[Long] = {
    val estimatedRateLimit = rateController.map(_.getLatestRate().toInt) //每批都根据rateContoller预估获取多少条数据
    val numPartitions = currentOffsets.keys.size

    val effectiveRateLimitPerPartition = estimatedRateLimit
      .filter(_ > 0)
      .map { limit =>
        if (maxRateLimitPerPartition > 0) {
          /*
          如果 spark.streaming.kafka.maxRatePerPartition 该参数有设置值且大于0
          那么就取 maxRateLimitPerPartition 和 rateController 算出来的值 之间的最小值(为什么取最小值,因为这样是最保险的)
           */
          Math.min(maxRateLimitPerPartition, (limit / numPartitions))
        } else {
          /*
          如果 spark.streaming.kafka.maxRatePerPartition 该参数没有设置
          那么就直接用 rateController 算出来的值
           */
          limit / numPartitions
        }
      }.getOrElse(maxRateLimitPerPartition) //如果没有设置自动推测的话,则返回参数设定的接收速率

    if (effectiveRateLimitPerPartition > 0) {
      val secsPerBatch = context.graph.batchDuration.milliseconds.toDouble / 1000
      Some((secsPerBatch * effectiveRateLimitPerPartition).toLong)
    } else {
      /*
      如果没有设置 spark.streaming.kafka.maxRatePerPartition 参数,则返回None
       */
      None
    }
  }

  //拿到每批的起始 offset
  protected var currentOffsets = fromOffsets

  /**
    * 获取此时此刻topic中每个partition 最大的(最新的)offset
    */
  @tailrec
  protected final def latestLeaderOffsets(retries: Int): Map[TopicAndPartition, LeaderOffset] = {
    val o = kc.getLatestLeaderOffsets(currentOffsets.keySet)
    // Either.fold would confuse @tailrec, do it manually
    if (o.isLeft) {
      val err = o.left.get.toString
      if (retries <= 0) {
        throw new SparkException(err)
      } else {
        log.error(err)
        Thread.sleep(kc.config.refreshLeaderBackoffMs)
        latestLeaderOffsets(retries - 1)//如果获取失败,则重试,且重试次数 -1
      }
    } else {
      o.right.get //如果没有问题,则拿到最新的 offset
    }
  }

  // limits the maximum number of messages per partition
  /**
    * ★★★★★重要方法,答案就在这里
    * @param leaderOffsets 该参数的offset是当前最新的offset
    * @return 包含untilOffsets的信息
    */
  protected def clamp(
    leaderOffsets: Map[TopicAndPartition, LeaderOffset]): Map[TopicAndPartition, LeaderOffset] = {
    maxMessagesPerPartition.map { mmp =>
      leaderOffsets.map { case (tp, lo) =>
        /**
          * 如果有设定自动推测,那么就将值设定为: min(自动推测出来的offset,此时此刻最新的offset)
          */
        tp -> lo.copy(offset = Math.min(currentOffsets(tp) + mmp, lo.offset))
      }
    }.getOrElse(leaderOffsets) //如果没有设定自动推测,那么untilOffsets的值就是最新的offset
  }

  override def compute(validTime: Time): Option[KafkaRDD[K, V, U, T, R]] = {
    //====》★★★★★从这里作为入口尽心查看
    val untilOffsets = clamp(latestLeaderOffsets(maxRetries))
    //根据offset去拉取数据,完!
    val rdd = KafkaRDD[K, V, U, T, R](
      context.sparkContext, kafkaParams, currentOffsets, untilOffsets, messageHandler)

  。。。
--------------

本期内容:
1. 动态Batch Size深入
2. RateController解析

1. 动态Batch Size深入

Dynamic Batch Size的方法实际在Spark Streaming中还没实现。论文中的解决方案:Fixed-point Iteration。
论文中有个比较重要的图:


基本思想:按100ms的批次发数据给Controller,Controller起初直接转给JobGenerator,再给Job Processor处理。Job Generator不是仅给出 处理结果,还要把job统计结果发给Controller,Controller接收到统计结果,会动态的改变batch size来给Job发数据。

至于窗口操作,也要做一些调整。如图:


试验表明,对Filter、Reduce、Join、Window还是有好的效果。
突然有其它作业加入时,也能动态调整。图例:


但算法是否会复杂,消耗时间。

2. RateController解析

Spark Streaming提供了RateController。ReceiverRateController、DirectKafkaRateController是其子类。
如果消费数据的速度的设置值有改变,会在batch中最后的Job完成时,会触发速率调整。
速率调整的主流程图:


流程较长,暂剖析最后的ReceriverSuperImpl.registerBlockGenerator和中间的ReceiverInputDStream.rateController的相关代码。

ReceiverSupervisorImpl:

  private val endpoint = env.rpcEnv.setupEndpoint(
    "Receiver-" + streamId + "-" + System.currentTimeMillis(), new ThreadSafeRpcEndpoint {
      override val rpcEnv: RpcEnv = env.rpcEnv

      override def receive: PartialFunction[Any, Unit] = {
        case StopReceiver =>
          logInfo("Received stop signal")
          ReceiverSupervisorImpl.this.stop("Stopped by driver", None)
        case CleanupOldBlocks(threshTime) =>
          logDebug("Received delete old batch signal")
          cleanupOldBlocks(threshTime)
        case UpdateRateLimit(eps) =>
          logInfo(s"Received a new rate limit: $eps.")
          registeredBlockGenerators.foreach { bg =>
            bg.updateRate(eps)
          }
      }
    })

bg是Spark Streaming中的RateLimiter子类。RateLimiter中有个成员rateLimiter,类型是Google Guava的限流工具类RateLimiter。
Google Guava的RateLimiter从概念上来讲,速率限制器会在可配置的速率下分配许可证。如果必要的话,每个acquire()会阻塞当前线程直到许可证可用后获取该许可证。一旦获取到许可证,不需要再释放许可证。
代码通过RateLimiter来更改速率。RateLimiter.updateRate:

  private[receiver] def updateRate(newRate: Long): Unit =
    if (newRate > 0) {
      if (maxRateLimit > 0) {
        rateLimiter.setRate(newRate.min(maxRateLimit))
      } else {
        rateLimiter.setRate(newRate)
      }
    }

如果maxRateLimit也有值(即设置了spark.streaming.receiver.maxRate值),则取newRate和maxRateLimit中间的最小值。
spark.streaming.receiver.maxRate控制了最大的接收速率。但有浪费资源的可能。配置最大速率不是太好的事情。

回到流程图中间的ReceiverInputDStream.rateController。
ReceiverInputDStream.rateController:

  override protected[streaming] val rateController: Option[RateController] = {
    if (RateController.isBackPressureEnabled(ssc.conf)) {
      Some(new ReceiverRateController(id, RateEstimator.create(ssc.conf, ssc.graph.batchDuration)))
    } else {
      None
    }
  }

其中的RateController.isBackPressureEnabled获得是否允许反压机制。
RateController.isBackPressureEnabled:

object RateController {
  def isBackPressureEnabled(conf: SparkConf): Boolean =
    conf.getBoolean("spark.streaming.backpressure.enabled", false)
}

如果允许反压机制,那么InputDStream子类中的成员rateController被赋予新生成的RateController子类ReceiverRateController对象。否则为None。

生成ReceiverRateController对象时会用调用RateEstimator.create。
RateEstimator.create:
  /**
   * Return a new RateEstimator based on the value of `spark.streaming.RateEstimator`.
   *
   * The only known estimator right now is `pid`.
   *
   * @return An instance of RateEstimator
   * @throws IllegalArgumentException if there is a configured RateEstimator that doesn't match any
   *         known estimators.
   */
  def create(conf: SparkConf, batchInterval: Duration): RateEstimator =
    conf.get("spark.streaming.backpressure.rateEstimator", "pid") match {
      case "pid" =>
        val proportional = conf.getDouble("spark.streaming.backpressure.pid.proportional", 1.0)
        val integral = conf.getDouble("spark.streaming.backpressure.pid.integral", 0.2)
        val derived = conf.getDouble("spark.streaming.backpressure.pid.derived", 0.0)
        val minRate = conf.getDouble("spark.streaming.backpressure.pid.minRate", 100)
        new PIDRateEstimator(batchInterval.milliseconds, proportional, integral, derived, minRate)

      case estimator =>
        throw new IllegalArgumentException(s"Unkown rate estimator: $estimator")
    }
目前spark.streaming.backpressure.rateEstimator配置只能是pid。另外还有4个反压的可配置项。
RateEstimator用于评估InputDStream消费数据的能力。根据消费数据的能力来调整接收数据的速率。RateEstimator.create给出了反压(back pressure)机制。这要比简单限制接收速率要好一些。

接着看其中生成的ReceiverRateController。ReceiverRateController是RateController子类。
继承关系:ReceiverRateController => RateController => StreamingListener => AsynchronousListenerBus => ListenerBus
如果允许反压机制,ReceiverInputDStream的rateController就不为None,才保证了上面流程图中RateController就能处理接收的消息,从而最终调整速率。

简单介绍一下BlockGenerator中的waitToPush方法。
BlockGenerator是RateLimiter子类。BlockGenerator利用waitToPush方法来限制receiver消费数据的速率。
BlockGenarator在生成Block时,BlockGenarator的加数据的方法addData、addDataWithCallback、addMultipleDataWithCallback中都调用了waitToPush。
有必要以后对waitToPush再做剖析。


注:Google Guava的限流工具类RateLimiter
  RateLimiter从概念上来讲,速率限制器会在可配置的速率下分配许可证。如果必要的话,每个acquire() 会阻塞当前线程直到许可证可用后获取该许可证。一旦获取到许可证,不需要再释放许可证。
  RateLimiter使用的是一种叫令牌桶的流控算法,RateLimiter会按照一定的频率往桶里扔令牌,线程拿到令牌才能执行,比如你希望自己的应用程序QPS不要超过1000,那么RateLimiter设置1000的速率后,就会每秒往桶里扔1000个令牌。
修饰符和类型    方法和描述
double    acquire()
从RateLimiter获取一个许可,该方法会被阻塞直到获取到请求
double    acquire(int permits)
从RateLimiter获取指定许可数,该方法会被阻塞直到获取到请求
static RateLimiter    create(double permitsPerSecond)
根据指定的稳定吞吐率创建RateLimiter,这里的吞吐率是指每秒多少许可数(通常是指QPS,每秒多少查询)
static RateLimiter    create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)
根据指定的稳定吞吐率和预热期来创建RateLimiter,这里的吞吐率是指每秒多少许可数(通常是指QPS,每秒多少个请求量),在这段预热时间内,RateLimiter每秒分配的许可数会平稳地增长直到预热期结束时达到其最大速率。(只要存在足够请求数来使其饱和)
double    getRate()
返回RateLimiter 配置中的稳定速率,该速率单位是每秒多少许可数
void    setRate(double permitsPerSecond)
更新RateLimite的稳定速率,参数permitsPerSecond 由构造RateLimiter的工厂方法提供。
String    toString()
返回对象的字符表现形式
boolean    tryAcquire()
从RateLimiter 获取许可,如果该许可可以在无延迟下的情况下立即获取得到的话
boolean    tryAcquire(int permits)
从RateLimiter 获取许可数,如果该许可数可以在无延迟下的情况下立即获取得到的话
boolean    tryAcquire(int permits, long timeout, TimeUnit unit)
从RateLimiter 获取指定许可数如果该许可数可以在不超过timeout的时间内获取得到的话,或者如果无法在timeout 过期之前获取得到许可数的话,那么立即返回false (无需等待)
boolean    tryAcquire(long timeout, TimeUnit unit)
从RateLimiter 获取许可如果该许可可以在不超过timeout的时间内获取得到的话,或者如果无法在timeout 过期之前获取得到许可的话,那么立即返回false(无需等待)

--------------------- 

使用SparkStreaming集成kafka时有几个比较重要的参数:

(1)spark.streaming.stopGracefullyOnShutdown (true / false)默认fasle

确保在kill任务时,能够处理完最后一批数据,再关闭程序,不会发生强制kill导致数据处理中断,没处理完的数据丢失

(2)spark.streaming.backpressure.enabled (true / false) 默认false

开启后spark自动根据系统负载选择最优消费速率

(3)spark.streaming.backpressure.initialRate (整数) 默认直接读取所有

在(2)开启的情况下,限制第一次批处理应该消费的数据,因为程序冷启动 队列里面有大量积压,防止第一次全部读取,造成系统阻塞

(4)spark.streaming.kafka.maxRatePerPartition (整数) 默认直接读取所有

限制每秒每个消费线程读取每个kafka分区最大的数据量

注意:

只有(4)激活的时候,每次消费的最大数据量,就是设置的数据量,如果不足这个数,就有多少读多少,如果超过这个数字,就读取这个数字的设置的值

只有(2)+(4)激活的时候,每次消费读取的数量最大会等于(4)设置的值,最小是spark根据系统负载自动推断的值,消费的数据量会在这两个范围之内变化根据系统情况,但第一次启动会有多少读多少数据。此后按(2)+(4)设置规则运行

(2)+(3)+(4)同时激活的时候,跟上一个消费情况基本一样,但第一次消费会得到限制,因为我们设置第一次消费的频率了。

除此之外,还应该考虑程序容错性,这个跟checkpoint有关系散仙在前面的文章已经描述过具体请参考:http://qindongliang.iteye.com/


--------------------- 

其它参考;https://blog.youkuaiyun.com/u012684933/article/details/48656629

https://blog.youkuaiyun.com/yjh314/article/details/52918072

转载于:https://my.oschina.net/hblt147/blog/2994899

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值