在流式数据的处理中,我们都非常关注数据流的速度,适当的速度可以的达到很好的一个处理效果,但是如果过高或者过低都会导致一些不期望的问题,这篇文章让我们一起来看看Spark Streaming是如何处理这一系列问题的。
一、为什么要限流
我们都知道,Spark Streaming是一个流式处理框架,但是他并不是完全的实时处理,而是按照batch机制来处理数据的。(画外音:在Spark Streaming2.3之后,引入了Continuous Processing的机制,可以达到近乎实时的数据处理,详情参考官网地址:http://spark.apache.org/docs/latest/structured-streaming-programming-guide.html#continuous-processing)
当数据处理的时间大于batch的间隔时间,即batch processing time > batch interval时,就会导致Executor端的Receiver的数据产生堆积,极端的情况下会导致OOM异常,因此,在Spark Streaming1.5之前可以通过参数来调整每秒处理的数据量(通过SparkConf可以直接设置):
spark.streaming.receiver.maxRate
在Driect方式下,可以通过下面参数来设置:
spark.streaming.kafka.maxRatePerPartition
这个参数可以限制作业每次从kafka的分区中最多读取的记录数。
从上面分析来看,似乎可以通过参数来解决数据流速度的问题,那么问题来了,如果我们升级集群或者扩展机器后,集群的吞吐量提高了很多,我们就需要手动去调整参数以避免浪费资源,有没有一种自动可以调节的方式呢?
在Spark Streaming1.5之后,引入了背压的机制,可以动态的自动调整数据处理的速率。
二、背压机制(Back Pressure)
背压优势:
- 动态调整消费速率,无需人工干预,充分利用集群资源。
开启背压:
spark.streaming.backpressure.enabled
将该参数设置为true即可开启背压机制。
背压原理:
在背压机制中,有以下几个重要的组件:
- RateController
- RateEstimator
- RateLimiter
其中,RateController是一个实现了StreamingListener接口的控制器,其主要作用是根据监听所有作用的onBatchCompleted事件,根据processingDelay和schedulingDelay来使用RateEstimator速率估算器来估算出一个合理的最大数据处理速度,然后发送给各个Executor进行更新。
其源码如下,位于RateController类中:
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)
}
RateEstimator是速率估算器,主要用来估算最大处理速率,默认的在2.2版本中只支持PIDRateEstimator,在以后的版本可能会支持使用其他插件,其源码如下:
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)
//默认的只支持pid,其他的配置抛出异常
case estimator =>
throw new IllegalArgumentException(s"Unknown rate estimator: $estimator")
}
以上这两个组件都是在Driver端用于更新最大速度的,而RateLimiter是用于接收到Driver的更新通知之后更新Executor的最大处理速率的组件。RateLimiter是一个抽象类,它并不是Spark本身实现的,而是借助了第三方Google的GuavaRateLimiter来产生的。
它实质上是一个限流器,也可以叫做令牌,如果Executor中task每秒计算的速度大于该值则阻塞,如果小于该值则通过,将流数据加入缓存中进行计算。这种机制也可以叫做令牌桶机制,图示如下:
接收到的newRate进行比较,取两者中的最小值来作为最大处理速率,如果没有设置,直接设置为newRate。源码如下:
private[receiver] def updateRate(newRate: Long): Unit =
if (newRate > 0) {
if (maxRateLimit > 0) {
//如果设置了maxRateLimit则取两者中的最小值
rateLimiter.setRate(newRate.min(maxRateLimit))
} else {
rateLimiter.setRate(newRate)
}
}
Spark Streaming1.5之后引入背压机制之后其整个架构图如下:
(图片来自微信号:itblog_hadoop)。
Spark Streaming的限流背压原理就简单介绍的这里,后续文章中会根据上面的架构图深入源码剖析其背压机制的整个过程,欢迎关注。(关键是饿了,写不动了。。。O(∩_∩)O哈哈~)