Shuffle operations
Spark触发事件叫作shuffle,shuffle是spark的对于重新分布式数据机制,因此它在不同的分区分组不相同。其中包含复制数据在executors和机器上,执行shuffle是复杂和耗时的操作。
Background
为了进一步了解shuffle期间,发生的事情,思考这个reduceByKey操作的例子。
reduceByKey操作产生了一个新的RDD,rdd.reduceByKey(func:(v,v) => v),最大的挑战是,并不是所有的相同key的数据在相同的partitions中,甚至在同一台机器上,但是必须被组合去计算结果。
在spark中,为了在必要的地方指定的操作,数据并不是分布在分区之间。在计算期间,单个任务操作单个分区,为了管理所有数据,为单个reduceByKey减少任务去执行,Spark需要去执行一个 all-to-all的操作。它必须读取分区来查找所有的key的value,并且获取所有的value到一起在分区之间,去计算每个key的最终结果--这叫做shuffle。
尽管shuffle数据的每个分区element集都是确定性的,并且分区的排序也是如此,元素的排序不是确定的。如果希望在shuflle后获取预期的排序,可以通过以下方式:
mapPartitions
to sort each partition using, for example,.sorted
repartitionAndSortWithinPartitions
to efficiently sort partitions while simultaneously repartitioningsortBy
to make a globally ordered RDD
产生shuffle的原因:RDD分区之间的宽依赖
名称 | 描述 | 场景 |
宽依赖 | 父RDD每个分区的数据可能被多个子RDD分区使用 | 一个父RDD的分区对应所有的子RDD的分区 一个父RDD分区对应非全部的的多个RDD分区 |
窄依赖 | 父RDD每个分区的只被子RDD的一个分区使用 |
如下图:
引起shuffle的操作有:
类型 | 函数 |
map | repartition:repartition、coalesce |
ByKey:groupByKey、reduceByKey | |
Join:cogroup、join |
Performance Impact
shuffle是一个昂贵的操作,因为它包括disk I/O,data serialize, network I/O。为了shuffle管理数据,Spark 生成一系列任务 -- map 任务,去管理数据,还有一系列的reduce任务去聚合。
本质上,单个map任务的结果被保存在内存中,其他内存不能再保存。然后,结果基于目标分区被排序,并且写入到单个文件中。在reduce端,reduce task 读取相关排序的blocks。
shuffle操作能够消耗大量的heap内存,之前它雇用内存数据结构去管理记录或者之后转移记录。特别是,reduceByKey和aggregateByKey创建这些结果在map端,‘ByKey 操作产生数据结构在map端。当数据在内存超过一定的值,spark将划分这些表到disk,增加磁盘I/O和增加垃圾的回收。-- 优化点
Shuffle也能产生大量的中间文件在disk。如spark1.3,这些文件被保存,直到相应的RDD不再使用,且垃圾回收。这样做的目的是,shuffle文件不需要重新创建如果血脉是重新计算。垃圾回收可能仅仅发生在一个长期的时间,如果应用保留对这些RDD的引用,或者如果GC不经常出现。这意味长时间的运行Spark jobs可能会消耗大量的disk 存储。临时目录通过 spark.local.dir
进行配置。--这是一个优化点
RDD Persistence
Spark最重要的功能之一,是持久化(或者缓存)数据集在内存和操作之间。当你持久化RDD时,每个node存储它在内存中计算任何分区,并且在该数据的其他action中重用这个数据。这使得之后的操作更加快。Caching是一个关键工具对于简化计算和快速交互使用。
使用persist()或者cache()方法去缓存RDD。第一次计算在action,RDD将被保存在node的内存中。Spark的缓存是容错的 -- 如果RDD的任何分区丢失,这个分区将被重新计算,使用那些创建它的transformation。
另外,能够使用不能的存储级别每个持久化RDD,如
level | 描述 | |
MEMORY_ONLY | default level. 存储在JVM中的RDD没有序列化,如果内存不够,一些分区将不被缓存,并且在它们被需要时,进行动态的重新计算 The | |
MEMORY_ONLY_SER | 存储在JVM中的RDD有序列化,更高效的存储,但是需要更多的CPU消耗 | |
MEMORY_AND_DISK | 存储在JVM中的RDD没有序列化,如果内存不够,一些分区将保存在本地disk,当使用时,再进行读取 | |
MEMORY_AND_DISK_SER | 存储在JVM中的RDD序列化,如果内存不够,一些分区将保存在本地disk,当使用时,再进行读取 | |
MEMORY_ONLY_2, MEMORY_AND_DISK_2, | Same as the levels above, but replicate each partition on two cluster nodes. | |
DISK_ONLY | Store the RDD partitions only on disk. | |
OFF_HEAP (experimental) | Store RDD in serialized format in Tachyon. 相比MEMORY_ONLY_SER, OFF_HEAP减小了GC回收的消耗并且允许executors更加智能和共享内存池, 在环境有有大量的内存和更多并行化的应用中,共享OFF_HEAP更加吸引人的。 与此同时, as the RDDs reside in Tachyon, 执行程序崩溃不会导致内存缓存丢失。 In this mode, the memory in Tachyon is discardable. Thus, Tachyon does not attempt to reconstruct a block that it evicts from memory. If you plan to use Tachyon as the off heap store, Spark is compatible with Tachyon out-of-the-box. 思考:如何释放当程序崩溃以后的数据 | https://www.ibm.com/developerworks/cn/opensource/os-cn-spark-tachyon/ Tachyon 是 AMPLab 开发的一款内存分布式文件系统。它介于计算层和存储层之间,可以简单的理解为存储层在内存内的一个 Cache 系统。 |
Which Storage Level to Choose
存储的级别意味提供一个在memory使用与CPU效率之间的平衡。挑选原则:
操作 | 级别 |
allowing operations on the RDDs to run as fast as possible. | MEMORY_ONLY |
more space-efficient, but still reasonably fast to access. | MEMORY_ONLY_SER |
fast fault recovery | MEMORY_ONLY_2, MEMORY_AND_DISK_2, |
n environments with high amounts of memory or multiple applications | OFF_HEAP |
-
Don’t spill to disk unless the functions that computed your datasets are expensive, or they filter a large amount of the data. Otherwise, recomputing a partition may be as fast as reading it from disk.
-
Use the replicated storage levels if you want fast fault recovery (e.g. if using Spark to serve requests from a web application). All the storage levels provide full fault tolerance by recomputing lost data, but the replicated ones let you continue running tasks on the RDD without waiting to recompute a lost partition.
-
In environments with high amounts of memory or multiple applications, the experimental
OFF_HEAP
mode has several advantages:- It allows multiple executors to share the same pool of memory in Tachyon.
- It significantly reduces garbage collection costs.
- Cached data is not lost if individual executors crash.
-
Removing Data
自动:drops out old data partitions in a least-recently-used (LRU) fashion
手动RDD.unpersist()
Shared Variables
名称 | 分类 | 描述 |
Shared Variables | broadcast | 允编程人员保存只读变量缓存在每台机器,而不是随任务一起传送副本,以高效的方式向每个node提供大量输入数据集的副本。 Spark actions由一系列的stages,stages的划分依据由分化的shuffle。Spark自动广播需每个stage任务被需要的公共数据。通过广播方式的数据以序列化的形式缓存,在每个任务运行前反序列化数据。 actions --1:n--> stage --1:n--> task 创建broadcast variables 注意:广播的类型在广播之后不能进行更改 |
accumulator | Spark natively supports accumulators of numeric types, and programmers can add support for new types.
在action中执行,只执行一次,不重复执行 | |
SparkContext.accumulableCollection 对集合进行元素进行操作 |
创建broadcast variable
scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0)
scala> broadcastVar.value
res0: Array[Int] = Array(1, 2, 3)
自定义Accumulator
object VectorAccumulatorParam extends AccumulatorParam[Vector] {
def zero(initialValue: Vector): Vector = {
Vector.zeros(initialValue.size)
}
def addInPlace(v1: Vector, v2: Vector): Vector = {
v1 += v2
}
}
// Then, create an Accumulator of this type:
val vecAccum = sc.accumulator(new Vector(...))(VectorAccumulatorParam)
对集合元素进行操作
object MyAccumulator {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("my accumulator spark").setMaster("local[*]")
val context = new SparkContext(conf)
val multiAccumulators = context.accumulableCollection(mutable.MutableList(1,2,3))
context.parallelize(Array(4,5,6,7)).map(x => {
multiAccumulators += x
x
}).collect()
multiAccumulators.value.foreach(println)
context.stop()
}
}