Spark正在对已经排序的分区进行排序,从而导致性能损失

时间:2019-07-11 09:37:18

标签: apache-spark caching optimization pyspark apache-spark-sql

对于在分区内进行分区和排序的缓存数据帧,使用1 Drink1 Beef1 2 Drink2 Beef2 3 Beef3 子句查询键时,我获得了良好的性能,但是使用同一键上的一个小表执行联接时,性能却下降了。

请参阅下面的示例数据集where,其中有10Kx44K = 438M行。

dftest

以下查询现在大约需要一秒钟(在具有2个工作程序的小型集群上):

sqlContext.sql(f'set spark.sql.shuffle.partitions={32}')
sqlContext.clearCache()
sc.setCheckpointDir('/checkpoint/temp')
import datetime
from pyspark.sql.functions import *
from pyspark.sql import Row

start_date = datetime.date(1900, 1, 1)
end_date   = datetime.date(2020, 1, 1)

dates = [ start_date + datetime.timedelta(n) for n in range(int ((end_date - start_date).days))]

dfdates=spark.createDataFrame(list(map(lambda x: Row(date=x), dates))) # some dates
dfrange=spark.createDataFrame(list(map(lambda x: Row(number=x), range(10000)))) # some number range

dfjoin = dfrange.crossJoin(dfdates)
dftest = dfjoin.withColumn("random1", round(rand()*(10-5)+5,0)).withColumn("random2", round(rand()*(10-5)+5,0)).withColumn("random3", round(rand()*(10-5)+5,0)).withColumn("random4", round(rand()*(10-5)+5,0)).withColumn("random5", round(rand()*(10-5)+5,0)).checkpoint()
dftest = dftest.repartition("number").sortWithinPartitions("number", "date").cache()
dftest.count() # 438,290,000 rows

但是,当我编写与联接类似的条件时,它需要2分钟:

dftest.where("number = 1000 and date = \"2001-04-04\"").count()

我原本希望它能快得差不多。特别是因为我希望更大的数据帧已经被群集和缓存。查看计划:

dfsub = spark.createDataFrame([(10,"1900-01-02",1),
  (1000,"2001-04-04",2),
  (4000,"2002-05-05",3),
  (5000,"1950-06-06",4),
  (9875,"1980-07-07",5)],
["number","date", "dummy"]).repartition("number").sortWithinPartitions("number", "date").cache()
df_result = dftest.join(dfsub, ( dftest.number == dfsub.number ) & ( dftest.date == dfsub.date ), 'inner').cache()
df_result.count() # takes 2 minutes (result = 5)

似乎花费大量时间按数字和日期对较大的数据框进行排序(此行:== Physical Plan == InMemoryTableScan [number#771L, date#769, random1#775, random2#779, random3#784, random4#790, random5#797, number#945L, date#946, dummy#947L] +- InMemoryRelation [number#771L, date#769, random1#775, random2#779, random3#784, random4#790, random5#797, number#945L, date#946, dummy#947L], StorageLevel(disk, memory, deserialized, 1 replicas) +- *(3) SortMergeJoin [number#771L, cast(date#769 as string)], [number#945L, date#946], Inner :- *(1) Sort [number#771L ASC NULLS FIRST, cast(date#769 as string) ASC NULLS FIRST], false, 0 : +- *(1) Filter (isnotnull(number#771L) && isnotnull(date#769)) : +- InMemoryTableScan [number#771L, date#769, random1#775, random2#779, random3#784, random4#790, random5#797], [isnotnull(number#771L), isnotnull(date#769)] : +- InMemoryRelation [number#771L, date#769, random1#775, random2#779, random3#784, random4#790, random5#797], StorageLevel(disk, memory, deserialized, 1 replicas) : +- Sort [number#771L ASC NULLS FIRST, date#769 ASC NULLS FIRST], false, 0 : +- Exchange hashpartitioning(number#771L, 32) : +- *(1) Scan ExistingRDD[number#771L,date#769,random1#775,random2#779,random3#784,random4#790,random5#797] +- *(2) Filter (isnotnull(number#945L) && isnotnull(date#946)) +- InMemoryTableScan [number#945L, date#946, dummy#947L], [isnotnull(number#945L), isnotnull(date#946)] +- InMemoryRelation [number#945L, date#946, dummy#947L], StorageLevel(disk, memory, deserialized, 1 replicas) +- Sort [number#945L ASC NULLS FIRST, date#946 ASC NULLS FIRST], false, 0 +- Exchange hashpartitioning(number#945L, 32) +- *(1) Scan ExistingRDD[number#945L,date#946,dummy#947L] )。它给我留下了以下问题:

  • 在分区内,左侧和右侧的排序顺序完全相同,并且对于JOIN子句来说是最佳的,为什么Spark仍会再次对分区进行排序?
  • 由于5个连接记录匹配(最多)5个分区,为什么要评估所有分区?
  • Catalyst似乎没有使用缓存数据帧的Sort [number#771L ASC NULLS FIRST, date#769 ASC NULLS FIRST], false, 0repartition的信息。在此类情况下使用sortWithinPartitions是否有意义?

1 个答案:

答案 0 :(得分:1)

让我尝试回答您的三个问题:

  

在分区内,左侧和右侧的排序顺序完全相同,并且对于JOIN子句来说是最佳的,为什么Spark仍会再次对分区进行排序?

由于排序列date中的数据类型不同,两个DataFrame中的排序顺序不同,在dfsub中是StringType,在dftest中是DateType,因此在连接期间,Spark会发现两个分支中的顺序是不同的,因此强制使用Sort

  

由于5个连接记录匹配(最多)5个分区,为什么要评估所有分区?

在查询计划处理期间,Spark不知道小型DataFrame中有多少个非空分区,因此它必须处理所有分区。

  

Catalyst似乎没有使用缓存数据帧的repartition和sortWithinPartitions信息。在这种情况下使用sortWithinPartitions是否有意义?

Spark优化器正在使用repartitionsortWithinPartitions中的信息,但是有一些有关其工作原理的警告。要修正您的查询,对联接中使用的同一列(包括两列)进行重新分区(而不仅仅是一列)也很重要。原则上,这不是必须的,并且正在解决相关的jira

这是我对查询的建议更改:

  1. date列的类型更改为dftest中的StringType(或类似地更改为dfsub中的DateType):

    dftest.withColumn("date", col("date").cast('string'))
    
  2. 两个数据框都发生变化

    .repartition("number")
    

    .repartition("number", "date")
    

完成这些更改后,您应该获得如下计划:

*(3) SortMergeJoin [number#1410L, date#1653], [number#1661L, date#1662], Inner
:- Sort [number#1410L ASC NULLS FIRST, date#1653 ASC NULLS FIRST], false, 0
:  +- Exchange hashpartitioning(number#1410L, date#1653, 200)
:     +- *(1) Project [number#1410L, cast(date#1408 as string) AS date#1653, random1#1540, random2#1544, random3#1549, random4#1555, random5#1562]
:        +- *(1) Filter (isnotnull(number#1410L) && isnotnull(cast(date#1408 as string)))
:           +- *(1) Scan ExistingRDD[number#1410L,date#1408,random1#1540,random2#1544,random3#1549,random4#1555,random5#1562]
+- Sort [number#1661L ASC NULLS FIRST, date#1662 ASC NULLS FIRST], false, 0
   +- Exchange hashpartitioning(number#1661L, date#1662, 200)
      +- *(2) Filter (isnotnull(number#1661L) && isnotnull(date#1662))
         +- *(2) Scan ExistingRDD[number#1661L,date#1662,dummy#1663L]

因此,该计划的每个分支中只有一个Exchange和一个Sort,这两个都来自您在自己的调用中调用的repartitionsortWithinPartition转换和连接不会引起更多排序或改组。另外请注意,由于我没有使用缓存,因此在我的计划中没有InMemoryTableScan