在Apache Spark中使用数据集进行交叉联接非常慢

时间:2019-02-15 16:52:32

标签: apache-spark join apache-spark-dataset cross-join

我已在Spark用户论坛上发布了此问题,但未收到任何答复,因此请在此处再次提问。

我们有一个用例,需要进行笛卡尔连接,由于某种原因,我们无法使其与Dataset API一起使用。

我们有两个数据集:

  
      
  • 一个具有2个字符串列的数据集,例如c1,c2。这是一个小型数据集,具有约100万条记录。两列都是32个字符的字符串,因此应小于500 mb。      
        

    我们广播了这个数据集

      
  •   
  • 其他数据集稍大一点,大约有1000万条记录
  •   
val ds1 = spark.read.format("csv").option("header", "true").load(<s3-location>).select("c1", "c2")
ds1.count
val ds2 = spark.read.format("csv").load(<s3-location>).toDF("c11", "c12", "c13", "c14", "c15", "ts")
ds2.count
ds2.crossJoin(broadcast(ds1)).filter($"c1" <= $"c11" && $"c11" <= $"c2").count

如果我使用RDD api实现它,我在ds1中广播数据,然后在ds2中过滤数据,则效果很好。

我已经确认广播成功。

  

2019-02-14 23:11:55 INFO CodeGenerator:54-代码在10.469136 ms中生成   2019-02-14 23:11:55 INFO TorrentBroadcast:54-开始读取广播变量29   2019-02-14 23:11:55 INFO TorrentBroadcast:54-读取广播变量29需要6毫秒   2019-02-14 23:11:56 INFO CodeGenerator:54-代码在11.280087 ms中生成

查询计划:

  

==身体计划==
   BroadcastNestedLoopJoin BuildRight,Cross,(((c1#68 <= c11#13)&&(c11#13 <= c2#69))
  :-*项目[]
  :+-*过滤器isnotnull(_c0#0)
     :+-* FileScan csv [_c0#0,_c1#1,_c2#2,_c3#3,_c4#4,_c5#5]批处理:false,格式:CSV,位置:InMemoryFileIndex [],PartitionFilters:[], PushedFilters:[IsNotNull(_c0)],ReadSchema:struct <_c0:string,_c1:string,_c2:string,_c3:string,_c4:string,_c5:string>
  +-BroadcastExchange IdentityBroadcastMode
    +-*项目[c1#68,c2#69]
       +-*过滤器(isnotnull(c1#68)&& isnotnull(c2#69))
          +-* FileScan csv [c1#68,c2#69]批处理:false,格式:CSV,位置:InMemoryFileIndex [],PartitionFilters:[],PushedFilters:[IsNotNull(c1),IsNotNull(c2)],ReadSchema:结构

然后阶段就不会进行。

我更新了代码以使用广播ds1,然后在ds2的mapPartitions中加入了连接。

val ranges = spark.read.format("csv").option("header", "true").load(<s3-location>).select("c1", "c2").collect
val rangesBC = sc.broadcast(ranges)

然后在mapPartitions方法中使用此rangeBC来确定ds2中每一行所属的范围,此作业将在3小时内完成,而其他作业甚至在24小时后也不会完成。这种暗示意味着查询优化器没有按照我想要的去做。

我在做什么错?任何指针都会有所帮助。谢谢!

2 个答案:

答案 0 :(得分:1)

我不知道您是使用裸机还是使用具有现货,按需或专用的AWS或具有AZURE等的VM。我的看法:

  • 即使将.filter应用于最终的交叉联接,也要意识到10M x 1M的工作量很大。需要一些时间。您的期望是什么?
  • 火花通常是关于线性缩放的。
  • 带有VM的数据中心没有专用的,因此性能也不是最快的。

然后:

  • 我在模拟设置中使用Databricks 10M x 100K,其具有0.86内核和6GB社区版驱动程序。跑了17分钟。
  • 在您的示例中,我在4节点AWS EMR非专用群集上运行了10M x 1M(带有一些EMR奇特功能,例如在有价值的实例上保留了驱动程序!),它花了3个小时才能部分完成。请参见下面的图片。

enter image description here

因此,要回答您的问题: -你没做错什么。

  • 只需要更多资源即可实现更多并行化。
  • 您确实看到了一些明确的分区。

答案 1 :(得分:1)

我最近遇到了这个问题,发现当交叉连接大型数据帧时,Spark具有奇怪的分区行为。如果您的输入数据框包含几百万条记录,那么交叉连接的数据框的分区等于输入数据框分区的乘积,即

crossJoinDF的分区=(ds1的分区)*(ds2的分区)。

如果ds1或ds2包含大约几百个分区,则交叉连接数据帧的分区范围约为10,000。这些分区太多了,在管理许多小任务时会导致过多的开销,使得交叉连接数据帧上的任何计算(在您的情况下为-过滤器)都非常慢地运行。

那么您如何使计算更快?首先检查这是否确实是您遇到的问题:

scala> val crossJoinDF = ds2.crossJoin(ds1)
# This should return immediately because of spark lazy evaluation

scala> val crossJoinDFPartitions = crossJoinDF.rdd.partitions.size

检查交叉连接数据帧上的分区数。如果crossJoinDFPartitions> 10,000,则确实存在相同的问题,即交叉联接数据框的分区过多。

要使对交叉联接的数据框的操作更快,请减少输入数据框上的分区数。例如:

scala> val ds1 = ds1.repartition(40)
scala> ds1.rdd.partitions.size 
res80: Int = 40

scala> val ds2 = ds2.repartition(40)
scala> ds2.rdd.partitions.size 
res81: Int = 40

scala> val crossJoinDF = ds1.crossJoin(ds2)
scala> crossJoinDF.rdd.partitions.size 
res82: Int = 1600

scala> crossJoinDF.count()

count()操作应导致执行交叉连接。现在,计数应该在合理的时间内返回。您选择的确切分区数将取决于群集中可用的内核数。

这里的关键要点是确保交叉连接的数据框具有合理数量的分区(<< 10,000)。您可能还会发现this post有用,它可以更详细地说明此问题。