概述
首先,对于window
函数的作用可以参考官网介绍,不过官网只是对使用进行了简单的介绍,而对于内部如何实现我们今天想来进行一探究竟!因为只是个人也只是简单的用过,但是依据之前看Spark Streaming实现相关的源码,个人在看源码之前会思考其中应该会怎么实现,window
函数的作用就是(窗口范围:windowDuration
,滑动频率:slideDuration
)每隔slideDuration
长的时间,统计计算windowDuration
范围内的数据结果,套用官网的图片:
如上的设置是dstream.reduceByKeyAndWindow((x:Int,y:Int)=>x+y,Seconds(15),Seconds(10))
,对应一个批次是5s
,意思就是每隔10s
中计算前15s
的统计结果,结合之前了解的DStream
的存储结构,这里面的实现应该是通过generatedRDD
这个HashMap
成员中存储的RDD
数据,来实现追溯前若干个批次的结果和当前进行汇总,但是这里个人有个问题就是如图time1
是上个window
的执行时间,time3
是下一个窗口执行时间,那么中间的time2
没有执行操作,那在time2
这个批次中会做些什么呢?
抱着对实现的猜测和中间过程的疑问,我们来深入源码对这些问题进行解答。
deep into code
对应的window
类型的操作有多种,这里我们挑选的解析api是reduceByKeyAndWindow
,我们点点点来到了第一站实现如下:
def reduceByKeyAndWindow(
reduceFunc: (V, V) => V,
windowDuration: Duration,
slideDuration: Duration,
partitioner: Partitioner
): DStream[(K, V)] = ssc.withScope {
self.reduceByKey(reduceFunc, partitioner)
.window(windowDuration, slideDuration)
.reduceByKey(reduceFunc, partitioner)
}
这里的self
就是当前批次产生的dstream
,看这实现的意思是先对当前批次的数据单独执行了reduceByKey
,得到了本批次内部数据聚合的结果。然后,执行window
操作,我们来看window
内做了啥子:
def window(windowDuration: Duration, slideDuration: Duration): DStream[T] = ssc.withScope {
new WindowedDStream(this, windowDuration, slideDuration)
}
它的实现很简单就是生成了一个WindowedDStream
对象,依据构造参数可以猜测这里面应该是核心实现部分了,直接看它的代码:
private[streaming]
class WindowedDStream[T: ClassTag](
parent: DStream[T],
_windowDuration: Duration,
_slideDuration: Duration)
extends DStream[T](parent.ssc) {
parent.persist(StorageLevel.MEMORY_ONLY_SER)
def windowDuration: Duration = _windowDuration
override def dependencies: List[DStream[_]] = List(parent)
override def slideDuration: Duration = _slideDuration
// 在原有计算得到的rememerDuration基础上增加了窗口范围windowDuration
// 这样可以避免rememberDuration<windowDuration,确保generatedRDD中存储的RDD一定满足窗口函数
override def parentRememberDuration: Duration = rememberDuration + windowDuration
override def persist(level: StorageLevel): DStream[T] = {
parent.persist(level)
this
}
// 实际计算
override def compute(validTime: Time): Option[RDD[T]] = {
// 时间范围
val currentWindow = new Interval(validTime - windowDuration + parent.slideDuration, validTime)
// 计算得到所依赖的parent的rdd
val rddsInWindow = parent.slice(currentWindow)
Some(ssc.sc.union(rddsInWindow))
}
}
如上代码十分简单,主要就是对parentRememberDuration
进行了重写,加上了窗口范围,这个意思很显然就是说需要使得generatedRDD
中一定存储了最近一个窗口范围内的全部RDD
数据,然后计算方法compute
中实现也较为简单,可以看到最后是把若干个rdd
进行了union
操作返回,我们来看看是如何获取rddsInWindow
的:
def slice(interval: Interval): Seq[RDD[T]] = ssc.withScope {
slice(interval.beginTime, interval.endTime)
}
def slice(fromTime: Time, toTime: Time): Seq[RDD[T]] = ssc.withScope {
if (!isInitialized) {
throw new SparkException(this + " has not been initialized")
}
//结束时间,如果不满足条件则取最近一个窗口的结束时间
val alignedToTime = if ((toTime - zeroTime).isMultipleOf(slideDuration)) {
toTime
} else {
logWarning(s"toTime ($toTime) is not a multiple of slideDuration ($slideDuration)")
toTime.floor(slideDuration, zeroTime)
}
//窗口开始时间,会判断当前传入进来的开始时间到应用启动中间这个时间是否是我们所设置的滑动窗口时间的整数倍
//如果是的就返回传入开始时间,否则返回最近一个窗口的开始时间
val alignedFromTime = if ((fromTime - zeroTime).isMultipleOf(slideDuration)) {
fromTime
} else {
logWarning(s"fromTime ($fromTime) is not a multiple of slideDuration ($slideDuration)")
fromTime.floor(slideDuration, zeroTime)
}
logInfo(s"Slicing from $fromTime to $toTime" +
s" (aligned to $alignedFromTime and $alignedToTime)")
// 依据传入参数计算得到的时间跨度,去获取这个跨度内的所有rdd
alignedFromTime.to(alignedToTime, slideDuration).flatMap { time =>
if (time >= zeroTime) getOrCompute(time) else None
}
}
以上可以看到其实在每个批次的时候,都会依据当前批次时间计算和这个批次最近的一个窗口时间范围,如果当前批次是需要新建窗口执行的时间点,则自己拉取数据执行一次window
操作,如果当前批次时间属于之前说的time2
这种中间过渡阶段,那么就取最近一个批次的执行结果,然后返回即可,也就是说对于time2
来说,它做的事情只是把当前自己批次的数据执行了一次局部的reduceByKey
聚合,然后返回最近一个批次的window
窗口范围内的所有rdd
进行后续的union
操作,那么这里就已经解答了我们之前的疑问time2
阶段会做啥,并且依据rdd
的获取方式,我们也知道了内部是如何实现的,不过这里面有一个问题需要注意就是,如果窗口跨度太大,内部由于remeberDuration
加上了窗口跨度,所以可能会导致内存占用过多,所以这一点需要谨慎考虑集群资源和数据量来适当使用window
。
以上就是本文介绍的关于window
函数的内部实现,这种方式其实和updateStateByKey
以及mapWithState
都类似,理解了DStream
的内部构造后,后续对一些算子内部的实现其实是很有帮助的。