我正在尝试做一些非常简单的事情,并且遇到了一些非常愚蠢的斗争。我认为这一定与对火花正在做什么的根本误解有关。我将不胜感激任何帮助或解释。
我有一个非常大的表(〜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
应该更快。是吗?
预先感谢您的任何想法!
答案 0 :(得分:3)
我将首先解决print
函数的问题,因为这是理解spark的基础。然后limit
与sample
。然后repartition
与coalesce
。
print
函数以这种方式花费很长时间的原因是因为coalesce
是一个惰性转换。 spark中的大多数转换都是惰性的,只有在调用 action 之前,才会进行评估。
动作是可以做的事情,并且(大多)不要返回结果。像count
,show
一样。它们返回一个数字和一些数据,而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
能够解决这个问题。
修改以明确改组repartition
和coalesce
您在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")