我们知道 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