Spark Streaming window函数源码解析

概述

  首先,对于window函数的作用可以参考官网介绍,不过官网只是对使用进行了简单的介绍,而对于内部如何实现我们今天想来进行一探究竟!因为只是个人也只是简单的用过,但是依据之前看Spark Streaming实现相关的源码,个人在看源码之前会思考其中应该会怎么实现,window函数的作用就是(窗口范围:windowDuration,滑动频率:slideDuration)每隔slideDuration长的时间,统计计算windowDuration范围内的数据结果,套用官网的图片:
image
  如上的设置是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的内部构造后,后续对一些算子内部的实现其实是很有帮助的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值