如何拆分巨大的rdd并轮流播放?

时间:2016-01-07 13:27:29

标签: apache-spark

描述

我们的火花版本是1.4.1

我们希望加入两个巨大的RDD,其中一个具有偏斜数据。所以spark rdd操作连接可能会导致内存问题。我们尝试将较小的一个分成几个,然后分批广播。在每次广播转弯时,我们尝试将较小的rdd的一部分收集到驱动程序,然后将其保存到HashMap,然后广播HashMap。每个执行器使用广播值与较大的rdd进行映射操作。我们通过这种方式实现我们的偏斜数据连接。

但是当它在每个回合中处理广播值时。我们发现处理后我们无法破坏广播值。如果我们使用broadcast.destroy(),接下来我们将处理数据 触发错误。像这样:

java.io.IOException: org.apache.spark.SparkException: Attempted to use Broadcast(6) after it was destroyed (destroy at xxx.java:369)

我们已经查看了spark的源代码,发现这个问题是由rdd依赖关系引导的。如果rdd3 - > rdd2 - > rdd1(箭头表示依赖关系)。和rdd1是使用名为b1的广播变量生成的,rdd2使用b2。在生成rdd3时,源代码显示需要序列化b1和b2。如果在rdd3生成过程之前销毁b1或b2。它会导致我在上面列出的失败。

问题

是否存在方式可以让rdd3忘记它的依赖性并使它不需要b1,b2,在生成过程中只需要rdd2?

或者是否存在处理倾斜连接问题的方法?

顺便说一下,我们为每个回合设置了检查点。并将spark.cleaner.ttl设置为600.问题仍然存在。如果我们不破坏广播变量,遗嘱执行人将在第5回合丢失。

我们的代码是这样的:

for (int i = 0; i < times; i++) {
    JavaPairRDD<Tuple2<String, String>, Double> prevItemPairRdd = curItemPairRdd;
    List<Tuple2<String, Double>> itemSplit = itemZippedRdd
            .filter(new FilterByHashFunction(times, i))
            .collect();

    Map<String, Double> itemSplitMap = new HashMap<String, Double>();
    for (Tuple2<String, Double> item : itemSplit) {
        itemSplitMap.put(item._1(), item._2());
    }
    Broadcast<Map<String, Double>> itemSplitBroadcast = jsc
            .broadcast(itemSplitMap);

    curItemPairRdd = prevItemPairRdd
            .mapToPair(new NormalizeScoreFunction(itemSplitBroadcast))
            .persist(StorageLevel.DISK_ONLY());
    curItemPairRdd.count();

    itemSplitBroadcast.destroy(true);
    itemSplit.clear();

}

1 个答案:

答案 0 :(得分:2)

我个人会尝试一些不同的方法。让我们从一个小的模拟数据集开始

import scala.util.Random
Random.setSeed(1)

val left = sc.parallelize(
  Seq.fill(200)(("a", Random.nextInt(100))) ++ 
  Seq.fill(150)(("b",  Random.nextInt(100))) ++ 
  Seq.fill(100)(Random.nextPrintableChar.toString, Random.nextInt(100))
)

按键计数:

val keysDistribution = left.countByKey

进一步假设第二个RDD是均匀分布的:

val right = sc.parallelize(
  keysDistribution.keys.flatMap(x => (1 to 5).map((x, _))).toSeq)

并将每个键可以处理的值的阈值设置为10:

val threshold = 10
  1. 使用代理键来增加粒度。

    想法非常简单。我们可以使用(k, v)而不是加入((k, i), v)对,而i是一个整数,取决于给定k的阈值和多个元素。

    val buckets = keysDistribution.map{
      case (k, v) => (k -> (v / threshold + 1).toInt)
    }
    
    // Assign random i to each element in left
    val leftWithSurrogates = left.map{case (k, v) => {
      val i = Random.nextInt(buckets(k))
      ((k, i), v)
    }}
    
    // Replicate each value from right to i buckets
    val rightWithSurrogates = right.flatMap{case (k, v) => {
      (0 until buckets(k)).map(i => ((k, i), v))
    }}
    
    val resultViaSurrogates = leftWithSurrogates
      .join(rightWithSurrogates)
      .map{case ((k, _), v) => (k, v)}
    
  2. 分而治之 - 分割频繁和不频繁的密钥。

    首先让我们使用不频繁的密钥加入:

    val infrequentLeft = left.filter{
      case (k, _) => keysDistribution(k) < threshold
    }
    
    val infrequentRight = right.filter{
      case (k, _) => keysDistribution(k) < threshold
    }
    
    val infrequent = infrequentLeft.join(infrequentRight)
    

    接下来让我们分别处理每个频繁的密钥:

    val frequentKeys = keysDistribution
      .filter{case (_, v) => v >= threshold}
      .keys
    
    val frequent = sc.union(frequentKeys.toSeq.map(k => {
      left.filter(_._1 == k)
        .cartesian(right.filter(_._1 == k))
        .map{case ((k, v1), (_, v2)) => (k, (v1, v2))}
    }))
    

    最后让我们联合两个子集:

    val resultViaUnion = infrequent.union(frequent)
    
  3. 快速健全检查:

    val resultViaJoin = left.join(right).sortBy(identity).collect.toList
    
    require(resultViaUnion.sortBy(identity).collect.toList == resultViaJoin)
    require(resultViaSurrogates.sortBy(identity).collect.toList == resultViaJoin)
    

    显然,这不是一个草图,而是一个完整的解决方案,但应该让你知道如何继续。与broadcast相比,它消除了驱动程序瓶颈的最大优势。

      

    是否存在可以让rdd3忘记其依赖性并使其不需要b1,b2,在生成过程中只需要rdd2?

    您使用检查点和强制计算但如果任何分区丢失,它仍然会失败。