遇到的问题是 DF1.crossJoin(DF2)
执行的时间特别慢,两个 DF 的数据量大概是在千万级别,刚开始以为数据量太大导致的执行特别耗时,但后来发现在另一批同等数量级的数据上 crossJoin 是执行很快的。那这就有问题了,花时间研究了下。
都是分区惹的祸。 spark 将数据按照按照分区存放,在执行运算时每个分区作为一个 task 且多个 task 并行运算从而提高处理效率。很显然分区数对计算的快慢有很大的影响。并不是分区越多执行效率越快,因为过多的分区意味着越多的 task,对这些 task 的管理也需要耗费很大的开销,所以有时反而会降低执行效率。
在资源充分利用的情况下,对于 crossJoin 方法的执行,DF 的分区越小执行效率越高。
当 DF 数据量较小时,crossJoin执行后的分区数等于其中一个输入 DF 的分区数。
scala> val xDF = (1 to 1000).toList.toDF("x")
scala> xDF.rdd.partitions.size
res11: Int = 64
scala> val yDF = (1 to 1000).toList.toDF("y")
scala> yDF.rdd.partitions.size
res12: Int = 64
scala> val crossJoinDF = xDF.crossJoin(yDF)
scala> crossJoinDF.rdd.partitions.size
res13: Int = 64
scala> val zDF = yDF.repartition(128)
scala> zDF.rdd.partitions.size
res5: Int = 128
scala> val xzcrossJoinDF = xDF.crossJoin(zDF)
scala> xzcrossJoinDF.rdd.partitions.size
res6: Int = 64
当 DF 数据量较小时,crossJoin执行后的分区数等于两个输入分区数的乘积。
scala> val xDF = (1 to 1000000).toList.toDF("x")
scala> xDF.rdd.partitions.size
res15: Int = 2
scala> val yDF = (1 to 1000000).toList.toDF("y")
scala> yDF.rdd.partitions.size
res16: Int = 2
scala> val crossJoinDF = xDF.crossJoin(yDF)
scala> crossJoinDF.rdd.partitions.size
res17: Int = 4
crossJoin 后分区是否发生变化的标准是什么呢?
我的理解:保持分每个区中数据量尽可能少,让 task 执行的快些。DF 数据本身很大时,笛卡尔积后数据量变成 M*N,增大分区数,降低每个分区中的数据量。
DataFrame 的分区数对计算效率有下列影响:
crossJoin 后的 dataFrame 属于第二种,太多的分区让在 dataFrame 上的任何操作都很慢。有可能还会出现下列异常:
org.apache.spark.SparkException Job aborted due to stage failure:
Total size of serialized results of 147936 tasks (1024.0 MB) is bigger than
spark.driver.maxResultSize (1024.0 MB)
这个异常的原因很简单,driver 存储了所有 task 的 metadata 并且追踪 task 的执行情况,exector 执行完 task 后需要向每个 task 的状态数据传回driver,超大量的分区会产生等同量的状态信息传回 driver,结果超了 driver 的默认大小。
解决办法也很简单:使用 spark.driver.maxResultSize
提高大小就行了。但这种方式只是解决了上述异常,对计算效率的优化没有任何效果。
这个也很简单:在 cross join 之前先降低输入 DF 分区数。
继续实验:
变量 | 数据量 | 分区数 |
---|---|---|
df1 | 17,000 | 200 |
df2 | 15,000 | 200 |
scala> df1.count()
res73: Long = 17000
scala> df1.rdd.partitions.size
res74: Int = 200
scala> df2.count()
res75: Long = 15000
scala> df2.rdd.partitions.size
res76: Int = 200
scala> val finalDF = df1.crossJoin(df2)
scala> finalDF.rdd.partitions.size
res77: Int = 40000
scala> time {finalDF.count()}
Elapsed time: 285163427988ns
res78: Long = 255000000
将 df1 和 df2 的分区数调整到 40
scala> val df1 = df1.repartition(40)
scala> df1.rdd.partitions.size
res80: Int = 40
scala> val df2 = df2.repartition(40)
scala> df2.rdd.partitions.size
res81: Int = 40
scala> val finalDF = df1.crossJoin(df2)
scala> finalDF.rdd.partitions.size
res82: Int = 1600
scala> time {finalDF.count()}
Elapsed time: 47178149994ns
res86: Long = 255000000
可以看出:调整前花费的计算时间是调整后的 6 倍左右。
https://towardsdatascience.com/make-computations-on-large-cross-joined-spark-dataframes-faster-6cc36e61a222