DStream-04 Window函数的原理和源码
DStream 中 window 函数有两种,一种是普通 WindowedDStream,另外一种是针对 window聚合 优化的 ReducedWindowedDStream。
Demo
object SocketWordCountDstreamReduceByWindow {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf()
.setAppName("SocketWordCountDstream")
.setMaster("local[3]")
val sparkStreamContext = new StreamingContext(sparkConf,Seconds(5))
val sparkContext = sparkStreamContext.sparkContext
sparkContext.setLogLevel("WARN")
val dstream = sparkStreamContext.socketTextStream("localhost",9090)
val v = dstream.flatMap(_.split(" "))
.map((_,1))
.reduceByKeyAndWindow((p:Int,c:Int)=>{
p + c
},Durations.seconds(15),Durations.seconds(5))
v.foreachRDD(...)
sparkStreamContext.start()
sparkStreamContext.awaitTermination()
}
}
源码
DStream
前提知识
在每个DStream 中会把每个batch 产生的 Rdd 放入Map中,也就是放到内存中。
// 保存RDD的 Map
@transient
private[streaming] var generatedRDDs = new HashMap[Time, RDD[T]]()
private[streaming] final def getOrCompute(time: Time): Option[RDD[T]] = {
// If RDD was already generated, then retrieve it from HashMap,
// or else compute the RDD
// 如果map 中就直接拿着用,没有就创建
generatedRDDs.get(time).orElse {
if (isTimeValid(time)) {
val rddOption = createRDDWithLocalProperties(time, displayInnerRDDOps = false) {
SparkHadoopWriterUtils.disableOutputSpecValidation.withValue(true) {
compute(time)
}
}
rddOption.foreach { case newRDD =>
// Register the generated RDD for caching and checkpointing
if (storageLevel != StorageLevel.NONE) {
newRDD.persist(storageLevel)
logDebug(s"Persisting RDD ${newRDD.id} for time $time to $storageLevel")
}
if (checkpointDuration != null && (time - zeroTime).isMultipleOf(checkpointDuration)) {
newRDD.checkpoint()
logInfo(s"Marking RDD ${newRDD.id} for time $time for checkpointing")
}
// 放入Map中
generatedRDDs.put(time, newRDD)
}
rddOption
} else {
None
}
}
}
同时也会 batch 完成的时候去清理这个Map。
private[streaming] def clearMetadata(time: Time) {
//根据当前 batch time - rememberDuration
//time = 100030, rememberDuration = 10 , generatedRDDs = {100020->rdd}
val oldRDDs = generatedRDDs.filter(_._1 <= (time - rememberDuration))
generatedRDDs --= oldRDDs.keys
if (unpersistData) {
logDebug(s"Unpersisting old RDDs: ${oldRDDs.values.map(_.id).mkString(", ")}")
oldRDDs.values.foreach { rdd =>
rdd.unpersist(false)
// Explicitly remove blocks of BlockRDD
rdd match {
case b: BlockRDD[_] =>
logInfo(s"Removing blocks of RDD $b of time $time")
b.removeBlocks()
case _ =>
}
}
}
dependencies.foreach(_.clearMetadata(time))
}
这个清理的过程是从后往前的,先清理 子DStream 然后是父DStream
// 默认 rememberDuration = slideDuration 也就是 batchInterval 。
private[streaming] var rememberDuration: Duration = null
//所以这边 父继承子
private[streaming] def parentRememberDuration = rememberDuration
DStream 初始化时
private[streaming] def initialize(time: Time) {
var minRememberDuration = slideDuration
// checkpointDuration 一般的DStream都是空的
if (checkpointDuration != null && minRememberDuration <= checkpointDuration) {
minRememberDuration = checkpointDuration * 2
}
if (rememberDuration == null || rememberDuration < minRememberDuration) {
rememberDuration = minRememberDuration
}
// Initialize the dependencies
dependencies.foreach(_.initialize(zeroTime))
}
// 抽象方法
// slideDuration 官方的注释是:方法是 DStream生成RDD的时间间隔。
// 默认情况下 slideDuration 就是 batchInterval 。针对 WindowedDStream 就是滑动的时间 但也是批次的时间
def slideDuration: Duration
这边举个例子 interval = 1s window = 4s slide = 2s。JobGenerator 会每个隔 interval = 1s 发送 GenerateJobs 事件,然后会触发 最后一个DStream 的 getOrCompute,然后依次 compute 会优先调 parent.getOrCompute 依次递归到第一个DStream。但是 getOrCompute 会判断这个 batch 的 Time - zeroTime(zeroTime 是 Stream 开始的时间,第一个batch 就是 zeroTime + batchInterval) 是不是 slideDuration 的倍数。如果是 才会调 compute 否则 就会返回 None。
private[streaming] final def getOrCompute(time: Time): Option[RDD[T]] = {
// If RDD was already generated, then retrieve it from HashMap,
// or else compute the RDD
generatedRDDs.get(time).orElse {
// Compute the RDD if time is valid (e.g. correct time in a sliding window)
// of RDD generation, else generate nothing.
if (isTimeValid(time)) {
....
rdd
}
rddOption
} else {
None
}
}
}
校验时间。 zeroTime 就是第一个Batch的时间
private[streaming] def isTimeValid(time: Time): Boolean = {
if (!isInitialized) {
throw new SparkException (this + " has not been initialized")
} else if (time <= zeroTime || ! (time - zeroTime).isMultipleOf(slideDuration)) {
logInfo(s"Time $time is invalid as zeroTime is $zeroTime" +
s" , slideDuration is $slideDuration and difference is ${time - zeroTime}")
false
} else {
logDebug(s"Time $time is valid")
true
}
}
例如常见 MappedDStream 是父 DStream 的 slideDuration
override def slideDuration: Duration = parent.slideDuration
第一个DStream 的 slideDuration = batchDuration
override def slideDuration: Duration = {
if (ssc == null) throw new Exception("ssc is null")
if (ssc.graph.batchDuration == null) throw new Exception("batchDuration is null")
ssc.graph.batchDuration
}
InputDStream
InputDStream(输入流就是数据源) 是 DirectKafkaInputDStream 、FileInputDStream..... 的父类。 输入流都是继承这个方法。
override def slideDuration: Duration = {
if (ssc == null) throw new Exception("ssc is null")
if (ssc.graph.batchDuration == null) throw new Exception("batchDuration is null")
// 就是 new StreamContext 传入流的间隔时间
ssc.graph.batchDuration
}
默认情况下就是 slideDuration = batchInterval 批次间隔时间
也就是 rememberDuration = batchInterval , parentRememberDuration = batchInterval 。默认只会保留上次一个batch的RDD。
进入正题。
源码的入口就是 reduceByKeyAndWindow
PairDStreamFunctions
实际就是 dstream.reduceByKey().window().reduceByKey,点开window()
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)
}
DStream
其实只要是DStream 都有 window 函数
def window(windowDuration: Duration, slideDuration: Duration): DStream[T] = ssc.withScope {
new WindowedDStream(this, windowDuration, slideDuration)
}
WindowedDStream
跟到 WindowedDStream 类
class WindowedDStream[T: ClassTag](
parent: DStream[T],
_windowDuration: Duration,
_slideDuration: Duration)
extends DStream[T](parent.ssc) {
// Persist parent level by default, as those RDDs are going to be obviously reused.
// 默认吧 parent dsteam 进行持久化,因为 parent.dsteam中的rdd 将会被吃
parent.persist(StorageLevel.MEMORY_ONLY_SER)
// 窗口时间
def windowDuration: Duration = _windowDuration
override def dependencies: List[DStream[_]] = List(parent)
// 这边的 slideDuration 就不是parent.slideDuration 而是我们定传入 window方法的滑动间隔。
override def slideDuration: Duration = _slideDuration
// parentRememberDuration 是 slideDuration + windowDuration
override def parentRememberDuration: Duration = rememberDuration + windowDuration
override def compute(validTime: Time): Option[RDD[T]] = {
val currentWindow = new Interval(validTime - windowDuration + parent.slideDuration, validTime)
// 获取一个范围内的RDD
val rddsInWindow = parent.slice(currentWindow)
// 最后union 范围内的RDD
Some(ssc.sc.union(rddsInWindow))
}
}
DStream
def slice(fromTime: Time, toTime: Time): Seq[RDD[T]] = ssc.withScope {
if (!isInitialized) {
throw new SparkException(this + " has not been initialized")
}
// windowStream.parent 就是普通的DStream slideDuration = batchInterval
// zeroTime 是 Stream 开始的时间,第一个batch 就是 zeroTime + batchInterval
截至时间到第一个batch的时间 是不是 batchInterval 的倍数。主要是方便计算
val alignedToTime = if ((toTime - zeroTime).isMultipleOf(slideDuration)) {
toTime
} else {
logWarning(s"toTime ($toTime) is not a multiple of slideDuration ($slideDuration)")
toTime.floor(slideDuration, zeroTime)
}
// 同理 开始时间到第一个batch的时间 是不是 batchInterval 的倍数。主要是方便计算
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)")
alignedFromTime.to(alignedToTime, slideDuration).flatMap { time =>
//最后将这一个时间段的 根据 slideDuration 来切分,然后得到之前 batch的Time
// 然后 到DStrem getOrCompute 中,从内存中重新取回来。
if (time >= zeroTime) getOrCompute(time) else None
}
}
例子
这边还是举个例子 interval = 1s window = 4s slice = 2s。zeroTime = 1582800004.000
isTimeValid = (Time - zeroTime % slide 是否为整数)
1s后 ,Time = 1582800005 ,先到到 WindowedDStream isTimeValid = false None
2s后 ,Time = 1582800006 , 先到到 WindowedDStream isTimeValid = true :
1、val currentWindow = new Interval(validTime - windowDuration + parent.slideDuration, validTime)
第一个参数 1582800006 - 4 + 1 = 1582800003
第二个参数 1582800006
2、parent.slice(1582800003,1582800006 ) 也就是取 1582800003、1582800004、1582800005、1582800006 四个时间的RDD。1582800003、1582800004 是无效的时间 直接是None ,然后调用
getOrCompute(1582800005)和 getOrCompute(1582800006 ) ,这样就取到 1582800005 的 RDD,虽然1582800005 时刻的返回的是None 到了 1582800006 就会把1582800005 取到,依次类推就好了。
Demo 2
大家想,如果一个Window 的时间比较长,并且 reduceBykey().window().reduceBykey 涉及的计算比较慢。每次都需要重新计算 4个batch的RDD ,很浪费(前提是不是去重计算 ) 。
假设 batchInterval = 1s window = 4s sliace = 1s
这边的数字代表时间戳
第一次 计算 1、2、3、4 的RDD
第二次 计算 2、3、4、5 的RDD
第三次 计算 3、4、 5、6、 的RDD
规律就是 上一次计算的结果 可以被下一次所副复用,减少计算。怎么服用呢
第一个 1、2、3、4 减 1的RDD 加 5的RDD 就可以了。
object SocketWordCountDstreamReduceByWindowOptimization {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf()
.setAppName("SocketWordCountDstream")
.setMaster("local[3]")
val sparkStreamContext = new StreamingContext(sparkConf,Seconds(1))
val sparkContext = sparkStreamContext.sparkContext
sparkContext.setLogLevel("WARN")
val dstream = sparkStreamContext.socketTextStream("localhost",9090)
val v = dstream.flatMap(_.split(" "))
.map((_,1))
.reduceByKeyAndWindow((p:Int,c:Int)=>{
p + c
},(c:Int,p:Int)=>{
c - p
},Durations.seconds(4),Durations.seconds(2))
v.foreachRDD(rdd => {
....
})
sparkStreamContext.start()
sparkStreamContext.awaitTermination()
}
}
PairDStreamFunctions
需要传入两个关键的函数,第一个就是 减去 slide/batchinterval 个 RDD 的函数,第二个就是加上 slide/batchinterval 个 RDD 的函数。
def reduceByKeyAndWindow(
reduceFunc: (V, V) => V,
invReduceFunc: (V, V) => V,
windowDuration: Duration,
slideDuration: Duration,
partitioner: Partitioner,
filterFunc: ((K, V)) => Boolean
): DStream[(K, V)] = ssc.withScope {
val cleanedReduceFunc = ssc.sc.clean(reduceFunc)
val cleanedInvReduceFunc = ssc.sc.clean(invReduceFunc)
val cleanedFilterFunc = if (filterFunc != null) Some(ssc.sc.clean(filterFunc)) else None
new ReducedWindowedDStream[K, V](
self, cleanedReduceFunc, cleanedInvReduceFunc, cleanedFilterFunc,
windowDuration, slideDuration, partitioner
)
}
ReducedWindowedDStream
几个关键都是和window 一样
def windowDuration: Duration = _windowDuration
override def dependencies: List[DStream[_]] = List(reducedStream)
override def slideDuration: Duration = _slideDuration
// 这个就是 需要 checkpoint
override val mustCheckpoint = true
override def parentRememberDuration: Duration = rememberDuration + windowDuration
怎么获取上一个batch的RDD,可以之在map 中,根据 time - slideDuration 就是上一个批次的时间。
例如 batchInterval = 1s window = 4s slide = 2s
当前 时间 100006
上一个widow 的区间 [ 100001,100002,100003,100004 ]
第一个:需要减去的RDD 区间 [ 100001,100002 ] (区间的表示,下同)
第二个:需要加上的RDD 区间 [ 100005,100006 ]
期望 : [100003,100006 ]
// 这个就是 parent DStream,这边只是做了一个转换。
private val reducedStream = parent.reduceByKey(reduceFunc, partitioner)
override def compute(validTime: Time): Option[RDD[(K, V)]] = {
val reduceF = reduceFunc
val invReduceF = invReduceFunc
val currentTime = validTime
// 这边计算后就是 [100006 - 4 + 1 ,100006] => [100003 ,100006]
val currentWindow = new Interval(currentTime - windowDuration + parent.slideDuration,
currentTime)
// 这边计算后就是 [100003-2,100006-2] => [100001,100004] 上一个Window的区间
val previousWindow = currentWindow - slideDuration
// _____________________________
// | previous window _________|___________________
// |___________________| current window | --------------> Time
// |_____________________________|
//
// |________ _________| |________ _________|
// | |
// V V
// old RDDs new RDDs
//
// Get the RDDs of the reduced values in "old time steps"
// 这边计算后就是 [100001,100004-2] => [100001,100002] 和我们之前预算的一致
val oldRDDs =
reducedStream.slice(previousWindow.beginTime, currentWindow.beginTime - parent.slideDuration)
logDebug("# old RDDs = " + oldRDDs.size)
// Get the RDDs of the reduced values in "new time steps"
//这边计算后就是 [100004+1,100006] 注意 parent.slideDuration = batchInterval
val newRDDs =
reducedStream.slice(previousWindow.endTime + parent.slideDuration, currentWindow.endTime)
logDebug("# new RDDs = " + newRDDs.size)
// Get the RDD of the reduced value of the previous window
// 获取上一个window 的RDD
val previousWindowRDD =
getOrCompute(previousWindow.endTime).getOrElse(ssc.sc.makeRDD(Seq[(K, V)]()))
// Make the list of RDDs that needs to cogrouped together for reducing their reduced values
val allRDDs = new ArrayBuffer[RDD[(K, V)]]() += previousWindowRDD ++= oldRDDs ++= newRDDs
// Cogroup the reduced RDDs and merge the reduced valuesRDD
// 将 三个RDD cogroupe也就是 合并
val cogroupedRDD = new CoGroupedRDD[K](allRDDs.toSeq.asInstanceOf[Seq[RDD[(K, _)]]],
partitioner)
// val mergeValuesFunc = mergeValues(oldRDDs.size, newRDDs.size) _
val numOldValues = oldRDDs.size
val numNewValues = newRDDs.size
val mergeValues = (arrayOfValues: Array[Iterable[V]]) => {
if (arrayOfValues.length != 1 + numOldValues + numNewValues) {
throw new Exception("Unexpected number of sequences of reduced values")
}
// Getting reduced values "old time steps" that will be removed from current window
// 拿到 需要减去oldrdd 的 值
val oldValues = (1 to numOldValues).map(i => arrayOfValues(i)).filter(!_.isEmpty).map(_.head)
// Getting reduced values "new time steps"
// 拿到 需要加上 oldrdd 的 值
val newValues =
(1 to numNewValues).map(i => arrayOfValues(numOldValues + i)).filter(!_.isEmpty).map(_.head)
// 判断上一个Window的Rdd值是不是
if (arrayOfValues(0).isEmpty) {
// If previous window's reduce value does not exist, then at least new values should exist
if (newValues.isEmpty) {
throw new Exception("Neither previous window has value for key, nor new values found. " +
"Are you sure your key class hashes consistently?")
}
// Reduce the new values
// 如果上一个window 是空的,直接只计算新值
newValues.reduce(reduceF) // return
} else {
// Get the previous window's reduced value
var tempValue = arrayOfValues(0).head
// If old values exists, then inverse reduce then from previous value
if (!oldValues.isEmpty) {
// 减去 oldRDDs,其实应该说对 上一个window 的值和 oldValues 处理
tempValue = invReduceF(tempValue, oldValues.reduce(reduceF))
}
// If new values exists, then reduce them with previous value
if (!newValues.isEmpty) {
// 加上 newRdd的值,其实应该说对 上一个window 的值和 newValues 处理
tempValue = reduceF(tempValue, newValues.reduce(reduceF))
}
tempValue // return
}
}
// 调用上面的函数 拿到当前的值
val mergedValuesRDD = cogroupedRDD.asInstanceOf[RDD[(K, Array[Iterable[V]])]]
.mapValues(mergeValues)
if (filterFunc.isDefined) {
Some(mergedValuesRDD.filter(filterFunc.get))
} else {
Some(mergedValuesRDD)
}
}
DStream-04 Window函数的原理和源码的更多相关文章
- DStream-05 updateStateByKey函数的原理和源码
Demo updateState 可以到达将每次 word count 计算的结果进行累加. object SocketDstream { def main(args: Array[String]): ...
- Java并发编程(七)ConcurrentLinkedQueue的实现原理和源码分析
相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Java并发编程(三)volatile域 Java并发编程(四)Java内存模型 Java并发编程(五)Concurr ...
- Kubernetes Job Controller 原理和源码分析(二)
概述程序入口Job controller 的创建Controller 对象NewController()podControlEventHandlerJob AddFunc DeleteFuncJob ...
- Kubernetes Job Controller 原理和源码分析(三)
概述Job controller 的启动processNextWorkItem()核心调谐逻辑入口 - syncJob()Pod 数量管理 - manageJob()小结 概述 源码版本:kubern ...
- [Spark内核] 第32课:Spark Worker原理和源码剖析解密:Worker工作流程图、Worker启动Driver源码解密、Worker启动Executor源码解密等
本課主題 Spark Worker 原理 Worker 启动 Driver 源码鉴赏 Worker 启动 Executor 源码鉴赏 Worker 与 Master 的交互关系 [引言部份:你希望读者 ...
- [Spark內核] 第41课:Checkpoint彻底解密:Checkpoint的运行原理和源码实现彻底详解
本课主题 Checkpoint 运行原理图 Checkpoint 源码解析 引言 Checkpoint 到底是什么和需要用 Checkpoint 解决什么问题: Spark 在生产环境下经常会面临 T ...
- Dubbo原理和源码解析之服务引用
一.框架设计 在官方<Dubbo 开发指南>框架设计部分,给出了引用服务时序图: 另外,在官方<Dubbo 用户指南>集群容错部分,给出了服务引用的各功能组件关系图: 本文将根 ...
- Dubbo原理和源码解析之标签解析
一.Dubbo 配置方式 Dubbo 支持多种配置方式: XML 配置:基于 Spring 的 Schema 和 XML 扩展机制实现 属性配置:加载 classpath 根目录下的 dubbo.pr ...
- Dubbo原理和源码解析之“微内核+插件”机制
github新增仓库 "dubbo-read"(点此查看),集合所有<Dubbo原理和源码解析>系列文章,后续将继续补充该系列,同时将针对Dubbo所做的功能扩展也进行 ...
随机推荐
- python中numpy.concatenate()函数的使用
numpy库数组拼接np.concatenate 原文:https://blog.csdn.net/zyl1042635242/article/details/43162031 思路:numpy提供了 ...
- 「Luogu P5494 【模板】线段树分裂」
(因为没有认证,所以这道题就由Froggy上传) 线段树分裂用到的地方确实并不多,luogu上以前也没有这道模板题,所以就出了一道,实在是想不出怎么出模板了,所以这道题可能可以用一些其他的算法水过去. ...
- Linux centosVMware Linux监控平台介绍、zabbix监控介绍、安装zabbix、忘记Admin密码如何做
一.Linux监控平台介绍 cacti.nagios.zabbix.smokeping.open-falcon等等 cacti.smokeping偏向于基础监控,成图非常漂亮 cacti.nagios ...
- The way get information from mssql by using excel vba and special port
Yes, we can get information from mssql by using excel vba. But the default port of MSSQL is 1433. ...
- ABC154F - Many Many Paths
梦回高中,定义的f(i,j)为从(0,0)到(i,j)一共有多少条路可以选择,易知我们要做i+j次选择,其中有i次是选择x轴,剩下的是y轴,所以f(i,j)=C(i+j,i)=C(i+j,j),给你一 ...
- 设计模式课程 设计模式精讲 16-2,3 代理模式Coding-静态代理-1
1 代码演练 1.1 代码演练1(静态代理之分库操作) 1 代码演练 1.1 代码演练1(静态代理之分库操作) 需求: 订单管理,模拟前置后置方法,模拟分库管理 重点: 重点看订单静态代理,动态数据源 ...
- 怎样把exe程序注册成系统服务
怎样把exe程序注册成系统服务 最近一段时间我们公司开发一款新的产品,要在服务器上运行一个服务端程序,为了方便我就希望能将这个程序注册成系统服务开机自动启动而不用每次重启系统都要手动启动程序.要实现这 ...
- “嘭、嘭、嘭”---C/S架构下的心跳机制
本人想使用AU3开发多客户端.一服务端.需要使用到心跳机制,即 在线状态实时更新以及掉线自动重连. 搜索网络发现没有人用AU3写心跳机制. 下面是一篇转帖(原文地址:http://www.cnblog ...
- 配置web应用全局的错误页面
- Python输出三位数以内的水仙花数
num = 100 while num <= 999: a = num % 10 #取个位数 b = num // 10 % 10 #取十位数 c = num // 100 #取百位数 if n ...