Shuffle operations
Spark中的某些操作会触发一个称为shuffle的事件。shuffle是Spark重新分布数据的机制, The shuffle is Spark’s mechanism for re-distributing data so that it’s grouped differently across partitions。这通常涉及copying data across executors and machines,使shuffle成为一个复杂且昂贵的操作。
Background
要理解在 shuffle 期间会发生什么,我们可以考虑reduceByKey操作的例子。reduceByKey操作生成一个新的RDD,其中单个键的所有值组合成一个元组——键和对与该键关联的所有值执行reduce函数的结果。问题在于,单个键的所有值不一定都位于同一个分区,甚至是同一台机器上,但它们必须位于同一位置才能计算结果。
在Spark中,数据通常不会跨分区分布到特定操作所需的位置。在计算期间,单个任务将在单个分区上操作——因此,要组织单个reduceByKey reduce任务要执行的所有数据,Spark需要执行all-to-all操作。它必须从所有分区读取,以找到所有键的所有值,然后将跨partitions 的值组合在一起,以计算每个键的最终结果——这称为shuffle。
尽管每个分区中的新shuffle数据的元素集是确定的,分区本身的次序也是确定的,但是这些元素的次序却不是确定的。如果一个人想要在shuffle 之后得到可预测的有序数据,那么可以使用:
- mapPartitions to sort each partition using, for example, .sorted
- repartitionAndSortWithinPartitions to efficiently sort partitions while simultaneously repartitioning
- sortBy to make a globally ordered RDD
Operations which can cause a shuffle include repartition operations like repartition and coalesce, ‘ByKey operations (except for counting) like groupByKey and reduceByKey, and join operations like cogroup and join.
Performance Impact
shuffle是一项昂贵的操作,因为它涉及磁盘I/O、数据序列化和网络I/O。要为shuffle组织数据,Spark生成任务集——map任务用于组织数据,而reduce任务集用于聚合数据。这个术语来自MapReduce,与Spark的map和reduce操作没有直接关系。
在内部,来自单个map任务的结果被保存在内存中,直到它们不能匹配为止。然后,根据目标分区对这些分区进行排序并写入到单个文件中。在reduce端,任务读取相关的已排序的块。
shuffle操作会消耗大量堆内存,因为它们使用内存中的数据结构来组织传输之前或之后的记录。具体来说,reduceByKey和aggregateByKey在map端创建这些结构,'ByKey操作在reduce端生成这些结构。当数据不适合内存时,Spark会将这些表溢出到磁盘,导致磁盘I/O的额外开销和增加的垃圾收集。
Shuffle还会在磁盘上生成大量的中间文件。从Spark 1.3开始,这些文件一直保存到不再使用相应的RDDs并进行垃圾收集。这样做是为了在重新计算沿袭时不需要重新创建shuffle文件。如果应用程序保留对这些RDDs的引用,或者GC不经常启动,那么垃圾收集可能只会在很长一段时间之后才会发生。这意味着长时间运行的Spark作业可能会消耗大量磁盘空间。临时存储目录由spark.local指定。在配置Spark上下文时使用dir配置参数。
可以通过调整各种配置参数来调整Shuffle行为。请参阅Spark配置指南中的 ‘Shuffle Behavior’ 章节。