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函数的原理和源码的更多相关文章

  1. DStream-05 updateStateByKey函数的原理和源码

    Demo updateState 可以到达将每次 word count 计算的结果进行累加. object SocketDstream { def main(args: Array[String]): ...

  2. Java并发编程(七)ConcurrentLinkedQueue的实现原理和源码分析

    相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Java并发编程(三)volatile域 Java并发编程(四)Java内存模型 Java并发编程(五)Concurr ...

  3. Kubernetes Job Controller 原理和源码分析(二)

    概述程序入口Job controller 的创建Controller 对象NewController()podControlEventHandlerJob AddFunc DeleteFuncJob ...

  4. Kubernetes Job Controller 原理和源码分析(三)

    概述Job controller 的启动processNextWorkItem()核心调谐逻辑入口 - syncJob()Pod 数量管理 - manageJob()小结 概述 源码版本:kubern ...

  5. [Spark内核] 第32课:Spark Worker原理和源码剖析解密:Worker工作流程图、Worker启动Driver源码解密、Worker启动Executor源码解密等

    本課主題 Spark Worker 原理 Worker 启动 Driver 源码鉴赏 Worker 启动 Executor 源码鉴赏 Worker 与 Master 的交互关系 [引言部份:你希望读者 ...

  6. [Spark內核] 第41课:Checkpoint彻底解密:Checkpoint的运行原理和源码实现彻底详解

    本课主题 Checkpoint 运行原理图 Checkpoint 源码解析 引言 Checkpoint 到底是什么和需要用 Checkpoint 解决什么问题: Spark 在生产环境下经常会面临 T ...

  7. Dubbo原理和源码解析之服务引用

    一.框架设计 在官方<Dubbo 开发指南>框架设计部分,给出了引用服务时序图: 另外,在官方<Dubbo 用户指南>集群容错部分,给出了服务引用的各功能组件关系图: 本文将根 ...

  8. Dubbo原理和源码解析之标签解析

    一.Dubbo 配置方式 Dubbo 支持多种配置方式: XML 配置:基于 Spring 的 Schema 和 XML 扩展机制实现 属性配置:加载 classpath 根目录下的 dubbo.pr ...

  9. Dubbo原理和源码解析之“微内核+插件”机制

    github新增仓库 "dubbo-read"(点此查看),集合所有<Dubbo原理和源码解析>系列文章,后续将继续补充该系列,同时将针对Dubbo所做的功能扩展也进行 ...

随机推荐

  1. JavaWeb项目http请求报错:Error parsing HTTP request header

    详细报错信息如下图: 原因:一一排查后,发现是http的请求中,包含了  “ | ” 竖线特殊符号:而并不是网上说的请求url太长了的原因. 解决方法:把 | 替换为  @

  2. 使用myeclipse搭建简单的maven工程

    请点击或者复制以下链接 http://opiece.me/2016/03/17/maven-and-ssmframework/

  3. MYSQL 查询指定日期后的15天

    SELECT s.id, s.exhi_id, s.commission, s.order_main_id, s.create_date, s.fen_xiao_id, s.state, s.upda ...

  4. js中的arguments、Array.prototype.slice.call()

    类数组对象:arguments js把传入到这个函数的全部参数存储在arguments里面,其实arguments也是个对象,而且是一个特殊的对象,它的属性名是按照传入参数的序列来的,第1个参数的属性 ...

  5. node.js绑定监听事件EventEmitter类

    Node.js 有多个内置的事件,我们可以通过引入 events 模块,并通过实例化 EventEmitter 类来绑定和监听事件,如下: // 引入 events 模块 var events = r ...

  6. Python 基础之函数的嵌套与nonlocal修改局部变量及闭包函数

    一.函数的嵌套 嵌套在外层,称之为外函数 嵌套在里层,称之为内函数#例:def outer(): def inner():        print("I'm inner")    ...

  7. ubuntu14 安装Node.js

    @brief ubuntu  安装Node.js @date 2018-06-28 @see Ubuntu 上安装 Node.js(https://www.runoob.com/nodejs/node ...

  8. Windows下ARP协议的工作原理简介

       ARP协议(Address Resolve Protocol,地址解析协议)工作在TCP/IP协议的第二层-数据链路层,用于将IP地址转换为网络接口的硬件地址(媒体访问控制地址,即MAC地址). ...

  9. Lesson 13 The search for oil

    What do oilmen want to achieve as soon as they strike oil? The deepest holes of all are made for oil ...

  10. Cookie存储在哪里

    内存是临时存储,关机后信息就没了.硬盘是长久存储,不删除就一直在.内存容量决定你电脑能同时运行的程序多少,硬盘容量决定你电脑能存放多少东西. 好比内存是手,硬盘是口袋,东西可以临时放在手里,可是口袋才 ...