一、背景

在默认情况下,Spark Streaming 通过 receivers (或者是 Direct 方式) 以生产者生产数据的速率接收数据。当 batch processing time > batch interval 的时候,也就是每个批次数据处理的时间要比 Spark Streaming 批间隔时间长;越来越多的数据被接收,但是数据的处理速度没有跟上,导致系统开始出现数据堆积,可能进一步导致 Executor 端出现 OOM 问题而出现失败的情况。

而在 Spark 1.5 版本之前,为了解决这个问题,

  • 对于 Receiver-based 数据接收器,我们可以通过配置 spark.streaming.receiver.maxRate 参数来限制每个 receiver 每秒最大可以接收的记录的数据;
  • 对于 Direct App roach 的数据接收,我们可以通过配置 spark.streaming.kafka.maxRatePerPartition 参数来限制每次作业中每个 Kafka 分区最多读取的记录条数。

这种方法虽然可以通过限制接收速率,来适配当前的处理能力,但这种方式存在以下几个问题:

  • 需要事先估计好集群的处理速度以及消息数据的产生速度;
  • 修改完相关参数之后,我们需要手动重启 Spark Streaming 应用程序;
  • 如果当前集群的处理能力高于我们配置的 maxRate,而且 producer 产生的数据高于 maxRate,这会导致集群资源利用率低下,而且也会导致数据不能够及时处理。

二、反压机制

为了更好的协调数据接收速率与资源处理能力,Spark Streaming 从v1.5开始引入反压机制(back-pressure),通过动态控制数据接收速率来适配集群数据处理能力。详细的记录请参见 SPARK-7398 里面的说明。

Spark Streaming 1.5 以前的体系结构

在 Spark 1.5 版本之前,Spark Streaming 的体系结构如下所示:

  • 数据是源源不断的通过 receiver 接收,当数据被接收后,其将这些数据存储在 Block Manager 中;为了不丢失数据,其还将数据备份到其他的 Block Manager 中;
  • Receiver Tracker 收到被存储的 Block IDs,然后其内部会维护一个时间到这些 block IDs 的关系;
  • Job Generator 会每隔 batchInterval 的时间收到一个事件,其会生成一个 JobSet;
  • Job Scheduler 运行上面生成的 JobSet。

Spark Streaming 1.5 之后的体系结构

  • 为了实现自动调节数据的传输速率,在原有的架构上新增了一个名为 RateController 的组件,这个组件继承自 StreamingListener,其监听所有作业的 onBatchCompleted 事件,并且基于 processingDelay 、schedulingDelay 、当前 Batch 处理的记录条数以及处理完成事件来估算出一个速率;这个速率主要用于更新流每秒能够处理的最大记录的条数。速率估算器(RateEstimator)可以又多种实现,不过目前的 Spark 2.2 只实现了基于 PID 的速率估算器。
  • InputDStreams 内部的 RateController 里面会存下计算好的最大速率,这个速率会在处理完 onBatchCompleted 事件之后将计算好的速率推送到 ReceiverSupervisorImpl,这样接收器就知道下一步应该接收多少数据了。
  • 如果用户配置了 spark.streaming.receiver.maxRate 或 spark.streaming.kafka.maxRatePerPartition,那么最后到底接收多少数据取决于三者的最小值。也就是说每个接收器或者每个 Kafka 分区每秒处理的数据不会超过 spark.streaming.receiver.maxRate 或 spark.streaming.kafka.maxRatePerPartition 的值。

过程如下图所示:

三、BackPressure 源码解析

RateController

RateEstimator

RateLimiter

3.1 RateController类体系

RateController是一个实现了StreamingListener接口的控制器,其主要作用是根据监听所有作用的onBatchCompleted事件,根据processingDelay和schedulingDelay来使用RateEstimator速率估算器来估算出一个合理的最大数据处理速度,然后发送给各个Executor进行更新。

private[streaming] abstract class RateController(val streamUID: Int, rateEstimator: RateEstimator)
extends StreamingListener with Serializable {
……
……
  /**
   * Compute the new rate limit and publish it asynchronously.
   */
  private def computeAndPublish(time: Long, elems: Long, workDelay: Long, waitDelay: Long): Unit =
    Future[Unit] {
      val newRate = rateEstimator.compute(time, elems, workDelay, waitDelay)
      newRate.foreach { s =>
        rateLimit.set(s.toLong)
        publish(getLatestRate())
      }
    }
  def getLatestRate(): Long = rateLimit.get()

  override def onBatchCompleted(batchCompleted: StreamingListenerBatchCompleted) {
    val elements = batchCompleted.batchInfo.streamIdToInputInfo
    for {
      processingEnd <- batchCompleted.batchInfo.processingEndTime
      workDelay <- batchCompleted.batchInfo.processingDelay
      waitDelay <- batchCompleted.batchInfo.schedulingDelay
      elems <- elements.get(streamUID).map(_.numRecords)
    } computeAndPublish(processingEnd, elems, workDelay, waitDelay)
  }
}  

3.2、BatchCompleted事件处理过程

StreamingListenerBus将事件转交给具体的StreamingListener,因此BatchCompleted将交由RateController进行处理。RateController接到BatchCompleted事件后将调用onBatchCompleted对事件进行处理。

override def onBatchCompleted(batchCompleted: StreamingListenerBatchCompleted) {
  val elements = batchCompleted.batchInfo.streamIdToInputInfo

  for {
    processingEnd <- batchCompleted.batchInfo.processingEndTime
    workDelay <- batchCompleted.batchInfo.processingDelay
    waitDelay <- batchCompleted.batchInfo.schedulingDelay
    elems <- elements.get(streamUID).map(_.numRecords)
  } computeAndPublish(processingEnd, elems, workDelay, waitDelay)
}

onBatchCompleted会从完成的任务中抽取任务的执行延迟和调度延迟,然后用这两个参数用RateEstimator(目前存在唯一实现PIDRateEstimator,proportional-integral-derivative (PID) controller, PID控制器)估算出新的rate并发布。代码如下:

/**
   * Compute the new rate limit and publish it asynchronously.
   */
  private def computeAndPublish(time: Long, elems: Long, workDelay: Long, waitDelay: Long): Unit =
    Future[Unit] {
      val newRate = rateEstimator.compute(time, elems, workDelay, waitDelay)
      newRate.foreach { s =>
        rateLimit.set(s.toLong)
        publish(getLatestRate())
      }
    }

其中publish()由RateController的子类ReceiverRateController来定义。具体逻辑如下(ReceiverInputDStream中定义):

/**
   * A RateController that sends the new rate to receivers, via the receiver tracker.
   */
  private[streaming] class ReceiverRateController(id: Int, estimator: RateEstimator)
      extends RateController(id, estimator) {
    override def publish(rate: Long): Unit =
      ssc.scheduler.receiverTracker.sendRateUpdate(id, rate)
  }

publish的功能为新生成的rate 借助ReceiverTracker进行转发。ReceiverTracker将rate包装成UpdateReceiverRateLimit事交ReceiverTrackerEndpoint

/** Update a receiver's maximum ingestion rate */
def sendRateUpdate(streamUID: Int, newRate: Long): Unit = synchronized {
  if (isTrackerStarted) {
    endpoint.send(UpdateReceiverRateLimit(streamUID, newRate))
  }
}

ReceiverTrackerEndpoint接到消息后,其将会从receiverTrackingInfos列表中获取Receiver注册时使用的endpoint(实为ReceiverSupervisorImpl),再将rate包装成UpdateLimit发送至endpoint.其接到信息后,使用updateRate更新BlockGenerators(RateLimiter子类),来计算出一个固定的令牌间隔。

/** RpcEndpointRef for receiving messages from the ReceiverTracker in the driver */
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.asScala.foreach { bg =>
          bg.updateRate(eps)
        }
    }
  })

其中RateLimiter的updateRate实现如下:

/**
  * Set the rate limit to `newRate`. The new rate will not exceed the maximum rate configured by
  * {{{spark.streaming.receiver.maxRate}}}, even if `newRate` is higher than that.
  *
  * @param newRate A new rate in events per second. It has no effect if it's 0 or negative.
  */
 private[receiver] def updateRate(newRate: Long): Unit =
   if (newRate > 0) {
     if (maxRateLimit > 0) {
       rateLimiter.setRate(newRate.min(maxRateLimit))
     } else {
       rateLimiter.setRate(newRate)
     }
   }

setRate的实现 如下:

public final void setRate(double permitsPerSecond) {
    Preconditions.checkArgument(permitsPerSecond > 0.0
        && !Double.isNaN(permitsPerSecond), "rate must be positive");
    synchronized (mutex) {
      resync(readSafeMicros());
      double stableIntervalMicros = TimeUnit.SECONDS.toMicros(1L) / permitsPerSecond;  //固定间隔
      this.stableIntervalMicros = stableIntervalMicros;
      doSetRate(permitsPerSecond, stableIntervalMicros);
    }
  }

到此,backpressure反压机制调整rate结束。

***流量控制点***

  当Receiver开始接收数据时,会通过supervisor.pushSingle()方法将接收的数据存入currentBuffer等待BlockGenerator定时将数据取走,包装成block. 在将数据存放入currentBuffer之时,要获取许可(令牌)。如果获取到许可就可以将数据存入buffer, 否则将被阻塞,进而阻塞Receiver从数据源拉取数据。

/**
 * Push a single data item into the buffer.
 */
def addData(data: Any): Unit = {
  if (state == Active) {
    waitToPush()  //获取令牌
    synchronized {
      if (state == Active) {
        currentBuffer += data
      } else {
        throw new SparkException(
          "Cannot add data as BlockGenerator has not been started or has been stopped")
      }
    }
  } else {
    throw new SparkException(
      "Cannot add data as BlockGenerator has not been started or has been stopped")
  }
}

令牌桶机制: 大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。当进行某操作时需要令牌时会从令牌桶中取出相应的令牌数,如果获取到则继续操作,否则阻塞。用完之后不用放回。

  Streaming 数据流被Receiver接收后,按行解析后存入iterator中。然后逐个存入Buffer,在存入buffer时会先获取token,如果没有token存在,则阻塞;如果获取到则将数据存入buffer.  然后等价后续生成block操作。

四、Spark streaming 反压机制的使用

在 Spark 启用反压机制很简单,只需要将 spark.streaming.backpressure.enabled 设置为 true 即可,这个参数的默认值为 false。反压机制还涉及以下几个参数,包括文档中没有列出来的:

  • spark.streaming.backpressure.initialRate:         启用反压机制时每个接收器接收第一批数据的初始最大速率。默认值没有设置。
  • spark.streaming.backpressure.rateEstimator:    速率估算器类,默认值为 pid ,目前 Spark 只支持这个,大家可以根据自己的需要实现。
  • spark.streaming.backpressure.pid.proportional:用于响应错误的权重(最后批次和当前批次之间的更改)。默认值为1,只能设置成非负值。
  • spark.streaming.backpressure.pid.integral:        错误积累的响应权重,具有抑制作用(有效阻尼)。默认值为 0.2 ,只能设置成非负值。effect.
  • spark.streaming.backpressure.pid.derived:         对错误趋势的响应权重。 这可能会引起 batch size 的波动,可以帮助快速增加/减少容量。默认值为0,只能设置成非负值。w
  • spark.streaming.backpressure.pid.minRate:       可以估算的最低费率是多少。默认值为 100,只能设置成非负值。

【引用】

[ 1 ]《Spark Streaming Backpressure分析https://www.cnblogs.com/barrenlake/p/5349949.html

[ 2 ]《Spark Streaming 反压(Back Pressure)机制介绍 》https://www.iteblog.com/archives/2323.html#respond

SparkStreaming反压机制的更多相关文章

  1. [转帖]实时流处理系统反压机制(BackPressure)综述

    实时流处理系统反压机制(BackPressure)综述 https://blog.csdn.net/qq_21125183/article/details/80708142 2018-06-15 19 ...

  2. 一文搞懂 Flink 网络流控与反压机制

    https://www.jianshu.com/p/2779e73abcb8 看完本文,你能get到以下知识 Flink 流处理为什么需要网络流控? Flink V1.5 版之前网络流控介绍 Flin ...

  3. Spark Streaming反压机制

    反压(Back Pressure)机制主要用来解决流处理系统中,处理速度比摄入速度慢的情况.是控制流处理中批次流量过载的有效手段. 1 反压机制原理 Spark Streaming中的反压机制是Spa ...

  4. Flink中接收端反压以及Credit机制 (源码分析)

    先上一张图整体了解Flink中的反压   可以看到每个task都会有自己对应的IG(inputgate)对接上游发送过来的数据和RS(resultPatation)对接往下游发送数据, 整个反压机制通 ...

  5. Flink中发送端反压以及Credit机制(源码分析)

    上一篇<Flink接收端反压机制>说到因为Flink每个Task的接收端和发送端是共享一个bufferPool的,形成了天然的反压机制,当Task接收数据的时候,接收端会根据积压的数据量以 ...

  6. 如何分析及处理 Flink 反压?

    反压(backpressure)是实时计算应用开发中,特别是流式计算中,十分常见的问题.反压意味着数据管道中某个节点成为瓶颈,处理速率跟不上上游发送数据的速率,而需要对上游进行限速.由于实时计算应用通 ...

  7. 咱们从头到尾讲一次 Flink 网络流控和反压剖析

    本文根据 Apache Flink 系列直播整理而成,由 Apache Flink Contributor.OPPO 大数据平台研发负责人张俊老师分享.主要内容如下: 网络流控的概念与背景 TCP的流 ...

  8. spark storm 反压

    因特殊业务场景,如大促.秒杀活动与突发热点事情等业务流量在短时间内剧增,形成巨大的流量毛刺,数据流入的速度远高于数据处理的速度,对流处理系统构成巨大的负载压力,如果不能正确处理,可能导致集群资源耗尽最 ...

  9. 1、flink介绍,反压原理

    一.flink介绍  Apache Flink是一个分布式大数据处理引擎,可对有界数据流和无界数据流进行有状态计算. 可部署在各种集群环境,对各种大小的数据规模进行快速计算. 1.1.有界数据流和无界 ...

随机推荐

  1. Codeforces 776E: The Holmes Children (数论 欧拉函数)

    题目链接 先看题目中给的函数f(n)和g(n) 对于f(n),若自然数对(x,y)满足 x+y=n,且gcd(x,y)=1,则这样的数对对数为f(n) 证明f(n)=phi(n) 设有命题 对任意自然 ...

  2. Bootstrap的本地引入

    今天用前端框架时选择了Bootstrap,然后东西都下好了本地就是引入不进去. 查了一下发现必须jquery要在BootStrap之前引入,然后我更改了引入顺序,发现还是不行 <script s ...

  3. 多线程模拟生产者消费者示例之BlockQueue

    public class Test { public static void main(String[] args){ //创建一个阻塞队列,边界为1 BlockingQueue<String& ...

  4. php mt_rand()函数 语法

    php mt_rand()函数 语法 mt_rand()函数怎么用? php mt_rand()函数表示从参数范围内得到一个随机数,语法是mt_rand(X,Y),从两个参数范围内得到一个随机数,随机 ...

  5. 使用vue-i18n实现项目的国际化 以及iview的国际化

    一:项目的国际化 vue-i18n官网 1. 在src中新建一个language文件夹(包含index.js.US.js.CN.js) (1)US.js 保存变量的英文,内容: export defa ...

  6. vue将页面导出成pdf

    npm i jspdf-html2canvas prinOut(){ // 导出pdf let page = document.querySelector('.app-main'); // page ...

  7. 禅道安装--结合openldap

    原文地址: https://blog.csdn.net/plei_yue/article/details/79075298 ldap结合禅道(需要神道不是开源版) https://www.cnblog ...

  8. shell脚本——注释(单行注释 多行注释)

    参考 : https://blog.csdn.net/weixin_42167759/article/details/80703570 单行注释 以"#"开头的行就是注释,会被解释 ...

  9. Php 单元测试 phpunit && codecept

    Php 单元测试 phpunit && codecept phpunit: Windows版本 整体上说,在 Windows 下安装 PHAR 和手工在 Windows 下安装 Com ...

  10. ssd存储的SLC、MLC、TLC闪存芯片颗粒有什么区别?

    SLC = Single-Level Cell ,即1bit/cell,速度快寿命长,价格贵(约MLC 3倍以上的价格),约10万次擦写寿命: MLC = Multi-Level Cell,即2bit ...