SparkStreaming反压机制
一、背景
在默认情况下,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反压机制的更多相关文章
- [转帖]实时流处理系统反压机制(BackPressure)综述
实时流处理系统反压机制(BackPressure)综述 https://blog.csdn.net/qq_21125183/article/details/80708142 2018-06-15 19 ...
- 一文搞懂 Flink 网络流控与反压机制
https://www.jianshu.com/p/2779e73abcb8 看完本文,你能get到以下知识 Flink 流处理为什么需要网络流控? Flink V1.5 版之前网络流控介绍 Flin ...
- Spark Streaming反压机制
反压(Back Pressure)机制主要用来解决流处理系统中,处理速度比摄入速度慢的情况.是控制流处理中批次流量过载的有效手段. 1 反压机制原理 Spark Streaming中的反压机制是Spa ...
- Flink中接收端反压以及Credit机制 (源码分析)
先上一张图整体了解Flink中的反压 可以看到每个task都会有自己对应的IG(inputgate)对接上游发送过来的数据和RS(resultPatation)对接往下游发送数据, 整个反压机制通 ...
- Flink中发送端反压以及Credit机制(源码分析)
上一篇<Flink接收端反压机制>说到因为Flink每个Task的接收端和发送端是共享一个bufferPool的,形成了天然的反压机制,当Task接收数据的时候,接收端会根据积压的数据量以 ...
- 如何分析及处理 Flink 反压?
反压(backpressure)是实时计算应用开发中,特别是流式计算中,十分常见的问题.反压意味着数据管道中某个节点成为瓶颈,处理速率跟不上上游发送数据的速率,而需要对上游进行限速.由于实时计算应用通 ...
- 咱们从头到尾讲一次 Flink 网络流控和反压剖析
本文根据 Apache Flink 系列直播整理而成,由 Apache Flink Contributor.OPPO 大数据平台研发负责人张俊老师分享.主要内容如下: 网络流控的概念与背景 TCP的流 ...
- spark storm 反压
因特殊业务场景,如大促.秒杀活动与突发热点事情等业务流量在短时间内剧增,形成巨大的流量毛刺,数据流入的速度远高于数据处理的速度,对流处理系统构成巨大的负载压力,如果不能正确处理,可能导致集群资源耗尽最 ...
- 1、flink介绍,反压原理
一.flink介绍 Apache Flink是一个分布式大数据处理引擎,可对有界数据流和无界数据流进行有状态计算. 可部署在各种集群环境,对各种大小的数据规模进行快速计算. 1.1.有界数据流和无界 ...
随机推荐
- 前端开发工具-VsCode插件【个人开发常用】
前端开发工具-VsCode插件[个人开发常用] Atom One Dark Theme-主题 Chinese (Simplified) Language Pack for Visual Studio ...
- 日志数据如何同步到MaxCompute
摘要:日常工作中,企业需要将通过ECS.容器.移动端.开源软件.网站服务.JS等接入的实时日志数据进行应用开发.包括对日志实时查询与分析.采集与消费.数据清洗与流计算.数据仓库对接等场景.本次分享主要 ...
- C# 简单软件有效期注册的实现
◆需求:公司一直以来对开发的产品都没有进行使用时间的控制,要么就是将日期限制写死在程序里面,每次都要编译新程序再发给客户,很不方便.于是公司让我写个模块,要求如下:1.无论哪个新开发的程序只要调用这个 ...
- BeautifulSoup的用法
BeautifulSoup是一个模块,该模块用于接收一个HTML或XML字符串,然后将其进行格式化,之后遍可以使用他提供的方法进行快速查找指定元素,从而使得在HTML或XML中查找指定元素变得简单. ...
- mybatis如何通过接口查找对应的mapper.xml及方法执行详解
转:http://www.jb51.net/article/116402.htm 本文主要介绍的是关于mybatis通过接口查找对应mapper.xml及方法执行的相关内容,下面话不多说,来看看详细的 ...
- ubuntu 配置jre后出现问题Error occurred during initialization of VM
百度了好久,找到了一个可以解决的办法. https://blog.51cto.com/chris2013/1313117 就是在usr/java/jre/lib/rt.pack需要解压成rt.jar ...
- JS 替换
JS 字符串有replace() 方法.但这个方法只会对匹配到的第一个字串替换. 如下例: var str = "wordwordwordword"; var strNew = s ...
- python接口自动化测试三十四:github上某接口测试平台及配置
TeserHome地址:https://testerhome.com/opensource_projects/60前端:https://github.com/pencil1/ApiTestWeb 实现 ...
- 控件识别工具Inspect.exe下载
一.Inspect.exe 控件识别工具.官网上说通过下载安装Windows SDK后,可以在目录C:\Program Files (x86)\Microsoft SDKs\Windows Kits\ ...
- 【Unity3D NGUI】----UI尺寸和位置的调整
1 尺寸与位置 通过UIWidget类获取,该类是所有UI元素的基类 在unity中创建一个sprite,如下图所示 这里面这个sprite的大小受几个属性的影响,首先是属性面板里的Size,对应的U ...