我正在使用的数据湖(df
)具有2 TB的数据和20,000个文件。我想将数据集压缩为2,000个1 GB文件。
如果运行df.coalesce(2000)
并写出到磁盘,则数据湖将包含1.9 TB的数据。
如果运行df.repartition(2000)
并将其写出到磁盘,则数据湖将包含2.6 TB的数据。
repartition()
数据湖中的每个文件正好比预期大0.3 GB(它们都是1.3 GB,而不是1 GB)。
为什么repartition()
方法会增加整个数据湖的大小?
有a related question讨论了为什么运行聚合后数据湖的大小会增加。答案是:
通常,像Parquet这样的列式存储格式在数据分配(数据组织)和单个列的基数方面非常敏感。数据越有条理,基数越低,存储效率越高。
coalesce()
算法是否提供了更有条理的数据...我认为不是...
我不认为其他问题能回答我的问题。
答案 0 :(得分:11)
免责声明:
此答案主要包含推测。对于这种现象的详细解释可能需要对输入和输出(或至少它们各自的元数据)进行深入分析。
观察:
persistent columnar formats和the internal Spark SQL representation都透明地应用了不同的压缩技术(例如Run-length encoding或dictionary encoding)来减少存储数据的内存占用。 / p>
另外,可以使用通用压缩算法对磁盘格式(包括纯文本数据)进行显式压缩-目前尚不清楚这种情况。
压缩(显式或透明)应用于数据块(通常是分区,但可以使用较小的单位)。
基于1),2)和3),我们可以假设平均压缩率将取决于群集中数据的分布。我们还应注意,如果上游谱系包含广泛的转化,则最终结果可能是不确定的。
coalesce
与repartition
的可能影响:
通常 coalesce
可以采用两条路径:
在第一种情况下,我们可以预期压缩率将与输入的压缩率相当。但是,在某些情况下可以实现更小的最终输出。让我们想象一个退化的数据集:
val df = sc.parallelize(
Seq("foo", "foo", "foo", "bar", "bar", "bar"),
6
).toDF
如果将这样的数据集写入磁盘,则没有压缩的可能-每个值都必须原样写入:
df.withColumn("pid", spark_partition_id).show
+-----+---+
|value|pid|
+-----+---+
| foo| 0|
| foo| 1|
| foo| 2|
| bar| 3|
| bar| 4|
| bar| 5|
+-----+---+
换句话说,我们需要大约6 * 3个字节,总共18个字节。
但是如果我们合并
df.coalesce(2).withColumn("pid", spark_partition_id).show
+-----+---+
|value|pid|
+-----+---+
| foo| 0|
| foo| 0|
| foo| 0|
| bar| 1|
| bar| 1|
| bar| 1|
+-----+---+
例如,我们可以将int较小的RLE应用于计数,并将每个分区存储3 +1字节,总共8字节。
这当然是一个极大的简化,但是显示了保持低熵输入结构以及合并块如何可以减少内存占用。
第二种coalesce
场景不太明显,但是在某些场景中,可以通过上游处理来减少熵(例如,考虑有关窗口函数的信息),并且保留这种结构将是有益的。
repartition
怎么样?
在没有分区表达式的情况下,repartition
适用于RoundRobinPartitioning
(使用基于分区ID的伪随机密钥实现为HashPartitioning
)。只要散列函数的行为合理,这种重新分配就应该使数据的熵最大化,从而降低可能的压缩率。
结论:
coalesce
不应仅提供任何特定的好处,而应保留数据分发的现有属性-该属性在某些情况下可能是有利的。
repartition
由于其性质,平均而言会使情况变得更糟,除非数据的熵已经最大化(这种情况可能会有所改善,但在非平凡的数据集上极不可能发生)。
repartition
>
最后repartitionByRange
带有分区表达式或{{1}}应该减少熵并提高压缩率。
注意:
我们还应该记住,列格式通常基于运行时统计信息来决定特定的压缩/编码方法(或缺乏压缩方法)。因此,即使特定块中的行集合是固定的,但是行的顺序发生了变化,我们也可以观察到不同的结果。