将Spark数据帧写入单个实木复合地板文件

时间:2018-09-06 14:37:51

标签: apache-spark pyspark pyspark-sql

我正在尝试做一些非常简单的事情,并且遇到了一些非常愚蠢的斗争。我认为这一定与对火花正在做什么的根本误解有关。我将不胜感激任何帮助或解释。

我有一个非常大的表(〜3 TB,约300MM行,25k分区),另存为s3中的镶木地板,我想将它的一个很小的样本作为一个镶木地板文件提供给某人。不幸的是,这需要永远完成,我不明白为什么。我尝试了以下方法:

tiny = spark.sql("SELECT * FROM db.big_table LIMIT 500")
tiny.coalesce(1).write.saveAsTable("db.tiny_table")

然后当它不起作用时,我尝试了一下,我认为应该是一样的,但是我不确定。 (我添加了print是为了进行调试。)

tiny = spark.table("db.big_table").limit(500).coalesce(1)
print(tiny.count())
print(tiny.show(10))
tiny.write.saveAsTable("db.tiny_table")

当我观看Yarn UI时, 和<{> {1}}的打印语句都使用25k映射器。 write花费了3分钟,count花费了25分钟,而show花费了约40分钟,尽管它最终 did 写入了我当时所用的单个文件表寻找。

在我看来,第一行应该占据前500行并将它们合并到一个分区,然后其他行应该非常快地发生(在单个映射器/缩减器上)。有人可以在这里看到我在做什么错吗?有人告诉我也许我应该使用write而不是sample,但是据我所知limit应该更快。是吗?

预先感谢您的任何想法!

2 个答案:

答案 0 :(得分:3)

我将首先解决print函数的问题,因为这是理解spark的基础。然后limitsample。然后repartitioncoalesce

print函数以这种方式花费很长时间的原因是因为coalesce是一个惰性转换。 spark中的大多数转换都是惰性的,只有在调用 action 之前,才会进行评估。

动作是可以做的事情,并且(大多)不要返回结果。像countshow一样。它们返回一个数字和一些数据,而coalesce返回一个具有1个分区的数据帧(有点,见下文)。

发生的事情是,您每次在coalesce数据帧上调用操作时,都会重新运行sql查询和tiny调用。这就是为什么他们每次通话都使用25k映射器的原因。

为节省时间,请将.cache()方法添加到第一行(始终使用print代码)。

然后,数据帧转换实际上是在第一行上执行的,结果一直保存在spark节点的内存中。

这不会对第一行的初始查询时间产生任何影响,但是至少您不会再运行该查询两次,因为结果已被缓存,然后操作便可以使用该缓存的结果。

要从内存中删除它,请使用.unpersist()方法。

现在要尝试执行的实际查询...

这实际上取决于数据的分区方式。就像是,它是否被划分在特定的字段上等等?

您在问题中提到了它,但是sample可能是正确的方法。

这是为什么?

limit必须在第一行中搜索500条。除非按行号(或某种递增ID)对数据进行分区,否则前500行可以存储在25k分区中的任何一个中。

因此spark必须对所有参数进行搜索,直到找到所有正确的值。不仅如此,它还必须执行一个额外的步骤来对数据进行排序以具有正确的顺序。

sample仅获取500个随机值。由于所涉及的数据没有顺序/排序,而且不必在特定分区中搜索特定行,因此操作起来容易得多。

虽然limit可以更快,但也有其局限性。我通常只将其用于非常小的子集(如10/20行)。

现在可以进行分区了。...

我认为coalesce的问题是实际上更改了分区。现在我不确定,所以要加些盐。

根据pyspark文档:

  

此操作导致狭窄的依存关系,例如如果您从1000个分区增加到100个分区,则不会进行混洗,而是100个新分区中的每一个将占用当前分区中的10个。

因此,您的500行实际上仍然位于您的25,000个物理分区上,这些分区被spark视为1个虚拟分区。

在这里引起混洗(通常很糟糕)并使用.repartition(1).cache()保留在火花存储器中可能是一个好主意。因为write时不让25k映射器查看物理分区,而是应该只让1个映射器查看火花存储器中的内容。然后write变得容易。您还需要处理一小部分,因此(希望)任何改组都应该是可管理的。

显然,这通常是不好的做法,并且不会改变spark在执行原始sql查询时可能要运行25k映射器的事实。希望sample能够解决这个问题。

修改以明确改组repartitioncoalesce

您在4节点群集上的16个分区中有2个数据集。您想加入它们并作为新数据集写入16个分区中。

数据1的行1可能在节点1上,数据2的行1可能在节点4上。

为了将这些行连接在一起,spark必须物理移动一个或两个,然后写入新分区。

这是一种随机操作,可以在群集中物理移动数据。

所有数据都被16分区并不重要,重要的是数据在群集中的位置。

data.repartition(4)将物理地将数据从每个节点的每4个分区集移动到每个节点1个分区。

Spark可能会将所有4个分区从节点1移到其他3个节点,并移到这些节点上的新单个分区中,反之亦然。

我不希望这样做,但这是一个极端的例子,可以证明这一点。

尽管coalesce(4)的调用不会移动数据,但更智能。相反,它认识到“每个节点已经有4个分区,总共4个节点...我将每个节点的所有4个分区称为一个分区,然后我将拥有4个分区!”

因此它不需要移动任何数据,因为它只是将现有分区合并到一个合并的分区中。

答案 1 :(得分:0)

尝试一下,以我的经验,重新分配对于这种问题更有效:

tiny = spark.sql("SELECT * FROM db.big_table LIMIT 500")
tiny.repartition(1).write.saveAsTable("db.tiny_table")

如果您对镶木地板感兴趣,甚至不需要将其另存为桌子,则更好:

tiny = spark.sql("SELECT * FROM db.big_table LIMIT 500")
tiny.repartition(1).write.parquet(your_hdfs_path+"db.tiny_table")