Spark:DataFrameWriter必须是阻塞步骤吗?

时间:2018-03-26 13:12:19

标签: apache-spark spark-dataframe distributed-computing partitioning

我有一个按列分区的数据(比如id),我把这个数据集保存了一些地方。我不时地得到一个具有相同结构的较小的增量数据集,我基本上必须根据我的iddate列来确定哪条记录是最新的。 (我不会把它写在同一个地方,我将整个新blob保存在其他地方。)

我有两种方法可以做到这一点 - 在窗口中分组并获取最高date的行。或者通过dropDuplicates,依靠事实,我的数据是订购的。 (我宁愿使用前者,但我一直在尝试各种各样的事情。)

一个大问题是每个id组都不可忽略不计(几千兆字节),所以我希望Spark(有n名工作人员)会理解,因为我正在阅读{ {1}} - 分区数据并编写id - 分区数据,它会立即处理id个ID并不断将它们写入我的存储空间,因为它已经完成了之前的新内容的。

不幸的是,似乎正在发生的事情是,在将任何内容写入磁盘之前,Spark会在一个大作业中处理所有n组(并自然地溢出到磁盘)。它真的很慢。

问题是:有没有办法强制Spark处理这些群组并在他们准备好后立即编写?同样,它们是分区的,因此其他任务都不会影响我的分区。

这里有一些代码可以重现问题:

id

# generate dummy data first import random from typing import List from datetime import datetime, timedelta from pyspark.sql.functions import desc, col, row_number from pyspark.sql.window import Window from pyspark.sql.dataframe import DataFrame def gen_data(n: int) -> List[tuple]: names = 'foo, bar, baz, bak'.split(', ') return [(random.randint(1, 25), random.choice(names), datetime.today() - timedelta(days=random.randint(1, 100))) \ for j in range(n)] def get_df(n: int) -> DataFrame: return spark.createDataFrame(gen_data(n), ['id', 'name', 'date']) n = 10_000 df = get_df(n) dd = get_df(n*10) df.write.mode('overwrite').partitionBy('id').parquet('outputs/first') dd.write.mode('overwrite').partitionBy('id').parquet('outputs/second') d1都由d2分区,结果数据集也是如此,但它没有反映在计划中:

id

我还试图明确说明分区键(否则代码是相同的):

w = Window().partitionBy('id').orderBy(desc('date'))

d1 = spark.read.parquet('outputs/first')
d2 = spark.read.parquet('outputs/second')

d1.union(d2).\
  withColumn('rn', row_number().over(w)).filter(col('rn') == 1).drop('rn').\
  write.mode('overwrite').partitionBy('id').parquet('outputs/window')

这里使用d1 = spark.read.parquet('outputs/first').repartition('id') d2 = spark.read.parquet('outputs/second').repartition('id') d1.union(d2).\ withColumn('rn', row_number().over(w)).filter(col('rn') == 1).drop('rn').\ write.mode('overwrite').partitionBy('id').parquet('outputs/window2')

是一样的
dropDuplicates

我也试着强调我的联盟仍然使用类似的东西进行分区,但是再次无济于事:

d1 = spark.read.parquet('outputs/first')
d2 = spark.read.parquet('outputs/second')

d1.union(d2).\
  dropDuplicates(subset=['id']).\
  write.mode('overwrite').partitionBy('id').parquet('outputs/window3')

我可以列出所有分区(df.union(d2).repartition('id').\ .withColumn... s),逐个加载它们,同时利用分区修剪,重复数据删除和写入。但这似乎是不必要的额外样板。或者是否可以通过id

执行此操作

更新(2018-03-27):

事实证明,有关分区的信息确实以某种方式存在于窗口功能中,因为当我在最后进行过滤时,会对输入进行分区修剪:

foreach

结果

d1 = spark.read.parquet('outputs/first')
d2 = spark.read.parquet('outputs/second')

w = Window().partitionBy('id', 'name').orderBy(desc('date'))

d1.union(d2).withColumn('rn', row_number().over(w)).filter(col('rn') == 1).filter(col('id') == 12).explain(True)

所以它确实只读取了两个分区,每个文件一个。所以我可以,而不是循环,只需一次运行一个过滤器的代码(过滤器在窗口函数和== Physical Plan == *(4) Filter (isnotnull(rn#387) && (rn#387 = 1)) +- Window [row_number() windowspecdefinition(id#187, name#185, date#186 DESC NULLS LAST, specifiedwindowframe(RowFrame, unboundedpreceding$(), currentrow$())) AS rn#387], [id#187, name#185], [date#186 DESC NULLS LAST] +- *(3) Sort [id#187 ASC NULLS FIRST, name#185 ASC NULLS FIRST, date#186 DESC NULLS LAST], false, 0 +- Exchange hashpartitioning(id#187, name#185, 200) +- Union :- *(1) FileScan parquet [name#185,date#186,id#187] Batched: true, Format: Parquet, Location: InMemoryFileIndex[file:/.../spark_perf_partitions/outputs..., PartitionCount: 1, PartitionFilters: [isnotnull(id#187), (id#187 = 12)], PushedFilters: [], ReadSchema: struct<name:string,date:timestamp> +- *(2) FileScan parquet [name#191,date#192,id#193] Batched: true, Format: Parquet, Location: InMemoryFileIndex[file:/.../spark_perf_partitions/outputs..., PartitionCount: 1, PartitionFilters: [isnotnull(id#193), (id#193 = 12)], PushedFilters: [], ReadSchema: struct<name:string,date:timestamp> 之间)。单调乏味且不太实用,但可能比将所有内容泄露到磁盘上更快。

1 个答案:

答案 0 :(得分:0)

是的,这正是火花分区的工作方式。因此,它计算整个谱系,然后在磁盘上以分区形式写入。这有几个优点。其中一个重要原因是并行写入。因此,当计算完成时,spark可以在磁盘上并行写入所有分区。这显着改善了性能。

如果你想在数据就绪时写入,你也可以通过不同的Ids过滤数据帧,并在循环中计算进程并写入。但是,根据我的经验,这种方法需要在同一数据帧上进行多次迭代,从而导致巨大的性能损失。