描述:
我们的火花版本是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();
}
答案 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
使用代理键来增加粒度。
想法非常简单。我们可以使用(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)}
分而治之 - 分割频繁和不频繁的密钥。
首先让我们使用不频繁的密钥加入:
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)
快速健全检查:
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?
您使用检查点和强制计算但如果任何分区丢失,它仍然会失败。