高效的reduceByKeyAndWindow算子

本文介绍Spark Streaming中reduceByKeyAndWindow方法的优化机制,通过避免重复计算提高效率。该方法利用之前窗口的计算结果,结合新旧数据进行增量计算。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

总的来说SparkStreaming提供这个方法主要是出于效率考虑。 比如说我要每10秒计算一下前15秒的内容,(每个batch 5秒), 可以想象每十秒计算出来的结果和前一次计算的结果其实中间有5秒的时间值是重复的。 

那么就是通过如下步骤 
1. 存储上一个window的reduce值 
2.计算出上一个window的begin 时间到 重复段的开始时间的reduce 值 =》 oldRDD 
3.重复时间段的值结束时间到当前window的结束时间的值 =》 newRDD 
4.重复时间段的值等于上一个window的值减去oldRDD 

这样就不需要去计算每个batch的值, 只需加加减减就能得到新的reduce出来的值。 

从代码上面来看, 入口为: 
reduceByKeyAndWindow(_+_, _-_, Duration, Duration) 

一步一步跟踪进去, 可以看到实际的业务类是在ReducedWindowedDStream 这个类里面: 
代码理解就直接拿这个类来看了: 主要功能是在compute里面实现, 通过下面代码回调mergeValues 来计算最后的返回值 
Scala代码  
  1. val mergedValuesRDD = cogroupedRDD.asInstanceOf[RDD[(K, Array[Iterable[V]])]]  
  2.       .mapValues(mergeValues)  


先计算oldRDD 和newRDD 

//currentWindow  就是以当前时间回退一个window的时间再向前一个batch 到当前时间的窗口 代码里面有一个图很有用: 
我们要计算的new rdd就是15秒-25秒期间的值, oldRDD就是0秒到10秒的值, previous window的值是1秒 - 15秒的值 

然后最终结果是 重复区间(previous window的值 - oldRDD的值) =》 也就是中间重复部分, 再加上newRDD的值, 这样的话得到的结果就是10秒到25秒这个时间区间的值 

Scala代码  
  1. // 0秒                  10秒     15秒                25秒  
  2. //  _____________________________  
  3. // |  previous window   _________|___________________  
  4. // |___________________|       current window        |  --------------> Time  
  5. //                     |_____________________________|  
  6. //  
  7. // |________ _________|          |________ _________|  
  8. //          |                             |  
  9. //          V                             V  
  10. //       old RDDs                     new RDDs  
  11. // 
Scala代码  
  1. val currentTime = validTime  
  2.   
  3.   
  4.     val currentWindow = new Interval(currentTime - windowDuration + parent.slideDuration,  
  5.       currentTime)  
  6.     val previousWindow = currentWindow - slideDuration  
  7.   
  8.    val oldRDDs =  
  9.       reducedStream.slice(previousWindow.beginTime, currentWindow.beginTime - parent.slideDuration)  
  10.     logDebug("# old RDDs = " + oldRDDs.size)  
  11.   
  12.     // Get the RDDs of the reduced values in "new time steps"  
  13.     val newRDDs =  
  14.       reducedStream.slice(previousWindow.endTime + parent.slideDuration, currentWindow.endTime)  
  15.     logDebug("# new RDDs = " + newRDDs.size) 

得到newRDD和oldRDD后就要拿到previous windows的值: 如果第一次没有previous window那么建一个空RDD, 为最后计算结果时 arrayOfValues(0).isEmpty 铺垫 
Java代码  
  1. val previousWindowRDD =  
  2.       getOrCompute(previousWindow.endTime).getOrElse(ssc.sc.makeRDD(Seq[(K, V)]())) 
然后把所有的值放到一个数组里面 0是previouswindow, 1到oldRDD.size是oldrdd, oldRDD.size到newRDD.size是newrdd 

Scala代码  
  1. val allRDDs = new ArrayBuffer[RDD[(K, V)]]() += previousWindowRDD ++= oldRDDs ++= newRDDs 

将每个RDD的(K,V) 转变成(K, Iterator(V))的形式: 

比如说有两个值(K,a) 和(K,b) 那么coGroup后就会成为(K, Iterator(a,b))这种形式 

Java代码  
  1. val cogroupedRDD = new CoGroupedRDD[K](allRDDs.toSeq.asInstanceOf[Seq[RDD[(K, _)]]],  
  2.      partitioner) 

进行最后的计算:

Scala代码  
  1.  val mergeValues = (arrayOfValues: Array[Iterable[V]]) => {  
  2. ...  
  3.   

首先判断RDD的value数量是不是正确 previous window因为已经计算过所以只有一组值 
正确值为 1 (previous window value) + numOldValues (oldRDD 每个RDD的value) + numNewValues (newRDD 每个RDD的value) 

Java代码  
  1. if (arrayOfValues.size != 1 + numOldValues + numNewValues) {  
  2.   throw new Exception("Unexpected number of sequences of reduced values")  

接下来取出oldRDD的值和newRDD的值: 
Scala代码  
  1. val oldValues = (1 to numOldValues).map(i => arrayOfValues(i)).filter(!_.isEmpty).map(_.head)  
  2. val newValues =  
  3.        (1 to numNewValues).map(i => arrayOfValues(numOldValues + i)).filter(!_.isEmpty).map(_.head) 
如果previous window是空的, 那么就直接计算newRDD的值(这也是为什么每次计算时候第一次打出来的值都比较少, 因为他只有newRDD部分没有重合部分, 也就是只有10秒的内容而不是15秒) 

Scala代码  
  1. if (arrayOfValues(0).isEmpty) {  
  2.        // If previous window's reduce value does not exist, then at least new values should exist  
  3.        if (newValues.isEmpty) {  
  4.          throw new Exception("Neither previous window has value for key, nor new values found. " +  
  5.            "Are you sure your key class hashes consistently?")  
  6.        }  
  7.        // Reduce the new values  
  8.        newValues.reduce(reduceF) // return  
  9.      } 
如果有previous window的值, 那么先存到tempValue, 如果有oldRDD那么减去oldRDD, 如果有newRDD (一般都有) 那么加上newRDD的值 这样就组成上图里面10到25秒区间的值了 

Scala代码  
  1. else {  
  2.         // Get the previous window's reduced value  
  3.         var tempValue = arrayOfValues(0).head  
  4.         // If old values exists, then inverse reduce then from previous value  
  5.         if (!oldValues.isEmpty) {  
  6.           tempValue = invReduceF(tempValue, oldValues.reduce(reduceF))  
  7.         }  
  8.         // If new values exists, then reduce them with previous value  
  9.         if (!newValues.isEmpty) {  
  10.           tempValue = reduceF(tempValue, newValues.reduce(reduceF))  
  11.         }  
  12.         tempValue // return  
  13.       } 
最后如果有filter的function的话就filter一下: 
Scala代码  
  1. if (filterFunc.isDefined) {  
  2.       Some(mergedValuesRDD.filter(filterFunc.get))  
  3.     } else {  
  4.       Some(mergedValuesRDD)  
  5.     } 

这样就返回了新window内的值

窗口函数的优化:
 * reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]):
 * 窗口长度(windowLength):窗口的持续时间
 * 滑动间隔(slideInterval):执行窗口操作的间隔
 * 这是比上一个reduceByKeyAndWindow()更有效的版本,
 * 根据上一个窗口的reduce value来增量地计算每个窗口的当前的reduce value值,
 * 这是通过处理进入滑动窗口的新数据,以及“可逆的处理”离开窗口的旧数据来完成的。
 * 一个例子是当窗口滑动时,“添加”和“减少”key的数量。
 * 然而,它仅适用于“可逆的reduce 函数”,即具有相应“可逆的reduce”功能的reduce函数(作为参数invFunc)。
 * 像在reduceByKeyAndWindow中,reduce task的数量可以通过可选参数进行配置。
 * 请注意,使用此操作必须启用 checkpointing 。即:优化的窗口函数需要checkpoint。
 * 以上的意思就是 传一个参数的reduceByKeyAndWindow每次计算包含多个批次,每次都会从新计算。造成效率比较低,因为存在重复计算数据的情况
 * 传二个参数的reduceByKeyAndWindow 是基于上次计算过的结果,计算每次key的结果,可以画图示意。

 
public class Operate_reduceByKeyAndWindow_2 {
public static void main(String[] args) {
SparkConf conf = new SparkConf().setMaster("local").setAppName("Operate_countByWindow");
JavaStreamingContext jsc = new JavaStreamingContext(conf,Durations.seconds(5));
jsc.checkpoint("checkpoint");
JavaDStream<String> textFileStream = jsc.textFileStream("data");
/**
* 首先将textFileStream转换为tuple格式统计word字数
*/
JavaPairDStream<String, Integer> mapToPair = textFileStream.flatMap(new FlatMapFunction<String, String>() {
private static final long serialVersionUID = 1L;
public Iterable<String> call(String t) throws Exception {
return Arrays.asList(t.split(" "));
}
}).mapToPair(new PairFunction<String, String, Integer>() {
private static final long serialVersionUID = 1L;
public Tuple2<String, Integer> call(String t) throws Exception {
return new Tuple2<String, Integer>(t.trim(), 1);
}
});

JavaPairDStream<String, Integer> reduceByKeyAndWindow = mapToPair.reduceByKeyAndWindow(new Function2<Integer, Integer, Integer>() {
private static final long serialVersionUID = 1L;
/**
* 这里的v1是指上一个所有的状态的key的value值(如果有出去的某一批次值,v1就是下面第二个函数返回的值),v2为本次的读取进来的值
*/
public Integer call(Integer v1, Integer v2) throws Exception {
System.out.println("***********v1*************"+v1);
System.out.println("***********v2*************"+v2);
return v1+v2;
}
}, new Function2<Integer,Integer,Integer>(){

private static final long serialVersionUID = 1L;
/**
* 这里的这个第二个参数的Function2是在windowLength时间后才开始执行,v1是上面一个函数刚刚加上最近读取过来的key的value值的最新值,
* v2是窗口滑动后,滑动间隔中出去的那一批值
* 返回的值又是上面函数的v1 的输入值
*/
public Integer call(Integer v1, Integer v2) throws Exception {

System.out.println("^^^^^^^^^^^v1^^^^^^^^^^^^^"+v1);
System.out.println("^^^^^^^^^^^v2^^^^^^^^^^^^^"+v2);

// return v1-v2-1;//每次输出结果递减1 
return v1-v2;
}

}, Durations.seconds(20), Durations.seconds(10));
reduceByKeyAndWindow.print();

jsc.start();
jsc.awaitTermination();
jsc.close();
}
}


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值