反压(Back Pressure)机制主要用来解决流处理系统中,处理速度比摄入速度慢的情况。是控制流处理中批次流量过载的有效手段。
一、SparkStreaming体系结构
Spark Streaming 1.5 以前的体系结构
- 数据是源源不断的通过 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 的值。
二、SparkStreaming反压原理
Spark Streaming中的反压机制是Spark 1.5.0推出的新特性,可以根据处理效率动态调整摄入速率。
当批处理时间(Batch Processing Time)大于批次间隔(Batch Interval,即 BatchDuration)时,说明处理数据的速度小于数据摄入的速度,持续时间过长或源头数据暴增,容易造成数据在内存中堆积,最终导致Executor OOM或任务奔溃。
1、反压的数据源方式及限流处理
spark streaming的数据源方式有两种:
- 1、若是基于Receiver的数据源,可以通过设置spark.streaming.receiver.maxRate来控制最大输入速率;
- 2、若是基于Direct的数据源(如Kafka Direct Stream),则可以通过设置spark.streaming.kafka.maxRatePerPartition来控制最大输入速率。
当然,在事先经过压测,且流量高峰不会超过预期的情况下,设置这些参数一般没什么问题。但最大值,不代表是最优值,最好还能根据每个批次处理情况来动态预估下个批次最优速率。
在Spark 1.5.0以上,就可通过反压机制来实现。开启反压机制,即设置spark.streaming.backpressure.enabled为true,Spark Streaming会自动根据处理能力来调整输入速率,从而在流量高峰时仍能保证最大的吞吐和性能。
2、反压的实现原理
Spark Streaming的反压机制中,有以下几个重要的组件:
- RateController
- RateEstimator
- RateLimiter
主要是通过RateController组件来实现。RateController继承自接口StreamingListener,并实现了onBatchCompleted方法。每一个Batch处理完成后都会调用此方法,具体如下:
override def onBatchCompleted(batchCompleted: StreamingListenerBatchCompleted) {
val elements = batchCompleted.batchInfo.streamIdToInputInfo
for {
// 处理结束时间
processingEnd <- batchCompleted.batchInfo.processingEndTime
// 处理时间,即`processingEndTime` - `processingStartTime`
workDelay <- batchCompleted.batchInfo.processingDelay
// 在调度队列中的等待时间,即`processingStartTime` - `submissionTime`
waitDelay <- batchCompleted.batchInfo.schedulingDelay
// 当前批次处理的记录数
elems <- elements.get(streamUID).map(_.numRecords)
} computeAndPublish(processingEnd, elems, workDelay, waitDelay)
}
可以看到,接着又调用的是computeAndPublish方法,如下:
private def computeAndPublish(time: Long, elems: Long, workDelay: Long, waitDelay: Long): Unit =
Future[Unit] {
// 根据处理时间、调度时间、当前Batch记录数,预估新速率
val newRate = rateEstimator.compute(time, elems, workDelay, waitDelay)
newRate.foreach { s =>
// 设置新速率
rateLimit.set(s.toLong)
// 发布新速率
publish(getLatestRate())
}
}
更深一层,具体调用的是rateEstimator.compute方法来预估新速率。
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每秒计算的速度大于该值则阻塞,如果小于该值则通过,将流数据加入缓存中进行计算。这种机制也可以叫做令牌桶机制,图示如下:
如果设置了maxRateLimit,则与接收到的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)
}
}
2、反压的参数设置
参数名称 | 默认值 | 说明 |
---|---|---|
spark.streaming.backpressure.enabled | false | 是否启用反压机制 |
spark.streaming.backpressure.initialRate | 无 | 初始最大接收速率。只适用于Receiver Stream,不适用于Direct Stream。 |
spark.streaming.backpressure.rateEstimator | pid | 速率控制器,Spark 默认只支持此控制器,可自定义。 |
spark.streaming.backpressure.pid.proportional | 1.0 | 只能为非负值。当前速率与最后一批速率之间的差值对总控制信号贡献的权重。用默认值即可。 |
spark.streaming.backpressure.pid.integral | 0.2 | 只能为非负值。比例误差累积对总控制信号贡献的权重。用默认值即可 |
spark.streaming.backpressure.pid.derived | 0 | 只能为非负值。比例误差变化对总控制信号贡献的权重。用默认值即可 |
spark.streaming.backpressure.pid.minRate | 100 | 只能为正数,最小速率 |
3、反压机制的使用
//启用反压
conf.set("spark.streaming.backpressure.enabled","true")
//最小摄入条数控制
conf.set("spark.streaming.backpressure.pid.minRate","1")
//最大摄入条数控制
conf.set("spark.streaming.kafka.maxRatePerPartition","12")
使用说明:
- 反压机制真正起作用时需要至少处理一个批:由于反压机制需要根据当前批的速率,预估新批的速率,所以反压机制真正起作用前,应至少保证处理一个批。
- 如何保证反压机制真正起作用前应用不会崩溃:要保证反压机制真正起作用前应用不会崩溃,需要控制每个批次最大摄入速率。若为Direct Stream,如Kafka Direct Stream,则可以通过spark.streaming.kafka.maxRatePerPartition参数来控制。此参数代表了 每秒每个分区最大摄入的数据条数。假设BatchDuration为10秒,spark.streaming.kafka.maxRatePerPartition为12条,kafka topic 分区数为3个,则一个批(Batch)最大读取的数据条数为360条(31210=360)。同时,需要注意,该参数也代表了整个应用生命周期中的最大速率,即使是背压调整的最大值也不会超过该参数。
4、设置反压机制后web UI的表现
INFO PIDRateEstimator: Created PIDRateEstimator with proportional = 1.0, integral = 0.2, derivative = 0.0, min rate = 1.0
可以看到,开启反压后,摄入速率Input Rate可以根据处理时间Processing Time来调整,从而保证应用的稳定性。
三、storm反压原理
Storm 是通过监控 Bolt 中的接收队列负载情况,如果超过高水位值就会将反压信息写到 Zookeeper ,Zookeeper 上的 watch 会通知该拓扑的所有 Worker 都进入反压状态,最后 Spout 停止发送 tuple
存在的问题:数据停止发送,等待系统回复,再次高速生产,然后再次停止发送,造成往数据流颠簸。
四、flink反压原理
- 在flink1.5 以前flink依靠tcp feedback实现网络流控
存在问题:
1.流控实现链路太长
2.由于依赖tcp本身流控,所以task任务触发反压机制,会直接阻塞tcp网络通信,造成无法正常通信,比如
checkpoint barrier 无法通过tcp发出 - 在flink1.5 以后flink在inputchannel层实现了tcp的反压机制,避免在tcp层阻塞。
flink 反压原理
- product [1,2,3] send window size=3
- consumer [1,2,3,4,5] reveive window size = 5
1、product 发送3个,consumer 消费1个,reveive window size 剩余3个,发送ack,可接受数量3
2、product接受到ack,滑动window,再发送3个,comsumer此时已经慢了,可接受数量0个,发送ack和可接受数量0个
3、product接受到ack,不在发送0个给consumer,并且周期性探测
flink 反压原理
可以使用以下配置键配置JobManager的样本数:
web.backpressure.refresh-interval:刷新时间间隔(默认值:60000,1分钟)。
web.backpressure.num-samples:用于确定backpressure的堆栈跟踪样本数(默认值:100个)。
web.backpressure.delay-between-samples:跟踪样本延迟时间(默认值:50 ms)。