Spark 为啥比 MapReduce 快?

Spark 为啥比 MapReduce 快?

DAG优化 和 内存

(1) 算子灵活性:MR只支持Map和Reduce 两种操作,而Spark有丰富的算子。

(2) Map 中间结果写磁盘,Reduce 写HDFS,多个MR之间通过HDFS交换数据。

(3) DAG引擎,先划分为Stage,Stage之间才Shuffle落盘,Stage之内,都可以内存处理。

(4) spark 中的rdd数据可以缓存到内存中,充分使用内存,多次使用,减少IO。

进程和线程

(1) MR的任务调度和启动都是进程级别的,每个进程都是JVM,资源和时间开销都很大。

(2) spark开启的JVM是Driver和Executor,每个Executor内部可以在每个core上都生成一个task,spark的task是基于线程的,线程池模型有效减少task的启动开销,一个executor上可以占用多个core,每最终task并行度为executor * core的数量。

Shuffle

(2) MR的Map端和Reduce端均需要排序。Spark在Shuffle过程中,尽量避免不必要的Sort操作。

一、Spark算子 VS MapReduce算子

MR只有Map 和 Reduce 两种操作。而spark基于RDD构建了丰富的算子。

RDD:Resilient Distribute DataSets,分布式弹性数据集

(1)RDD是分布于集群中的,有多个Partition组成的只读对象集合。

分区数的确定

(2)支持内存、磁盘等多种存储级别。

(3)通过并行的 Transform 操作,逐步构建需要的结果。

(4)通过Lineage血统体系,支持自动重构。

Transform操作:生成新的RDD

PartitionBy ,map, filter,groupBy,reduceBy ,reduceByKey

Action 操作:获取一个或者一组值

count,reduce,saveAsTextFile

## 二、Spark Shuffle VS MR Shuffle

MapReduce Shuffle过程

MapReduce Shuffle过程

1、首先 map 在做输出时候会在内存里开启一个环形内存缓冲区,专门用来做输出,同时map还会启动一个守护线程;

2、如缓冲区的内存达到了阈值的80%,守护线程就会把内容写到磁盘上,这个过程叫spill,另外的20%内存可以继续写入要写进磁盘的数据;

3、写入磁盘和写入内存操作是互不干扰的,如果缓存区被撑满了,那么map就会阻塞写入内存的操作,让写入磁盘操作完成后再继续执行写入内存操作;

4、写入磁盘时会有个内存排序操作,如果定义了combiner函数,那么排序前还会执行combiner操作;

5、每次spill操作也就是写入磁盘操作时候就会写一个溢出文件,也就是说在做map输出有几次spill就会产生多少个溢出文件等map输出全部做完后,map会合并这些输出文件,这个过程里还会有一个Partitioner操作(打标签,看当前数据应该分到哪个Reducer),确定数据应该分配到哪个Reducer。

6、最后 reduce 就是合并map输出文件,Partitioner会找到对应的map输出文件,然后进行复制操作,复制操作时reduce会开启几个复制线程,这些线程默认个数是5个(可修改),这个复制过程和map写入磁盘过程类似,也有阈值和内存大小,阈值一样可以在配置文件里配置,而内存大小是直接使用reduce的tasktracker的内存大小,复制时候reduce还会进行排序操作和合并复制线程负责过来的小文件操作,这些操作完了就会进行reduce计算了。 reduce阶段:由我们自己编写,最终结果存储在hdfs上的。

Spark Shuffle过程

shuffle过程只有在stage与stage之间才会运行,前一个stage可以看作是MR的MapTask;后面的stage可以看作是ReduceTask,而且stage的切分规则是根据RDD的宽窄依赖关系切分的.

完全依赖是宽依赖。

部分依赖就是窄依赖。

宽依赖和窄依赖04.png

Spark 中涉及 Shuffle 的算子

去重

def distinct()
def distinct(numPartitions: Int)

分组聚合

def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]
def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)]
def groupBy[K](f: T => K, p: Partitioner):RDD[(K, Iterable[V])]
def groupByKey(partitioner: Partitioner):RDD[(K, Iterable[V])]
def combineByKey[C](createCombiner: V => C, mergeValue: (C, V) => C, mergeCombiners: (C, C) => C): RDD[(K, C)]

排序

def sortByKey(ascending: Boolean = true, numPartitions: Int = self.partitions.length): RDD[(K, V)]
def sortBy[K](f: (T) => K, ascending: Boolean = true, numPartitions: Int = this.partitions.length)(implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]

重分区

def coalesce(numPartitions: Int, shuffle: Boolean = false, partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null)

Join操作

def join[W](other: RDD[(K, W)], partitioner: Partitioner): RDD[(K, (V, W))]
def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]
def join[W](other: RDD[(K, W)], numPartitions: Int): RDD[(K, (V, W))]
def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]

Shuffle Write

为下游Stage准备上一个Satge输出的结果文件。

对于Hash Shuffle 来说下一个stage的task即Reduce Task有多少个,当前stage的每个task,或者每个Core就要创建多少份磁盘文件。

对应Sort Shuffle 来说,有多少Map Task 就创建多少磁盘文件 ,和索引文件。

Shuffle Read

接着我们来说说shuffle read。shuffle read,通常就是一个stage刚开始时要做的事情。此时该stage的每一个task就需要将上一个stage的计算结果中的所有相同key,从各个节点上通过网络都拉取到自己所在的节点上,然后进行key的聚合或连接等操作。由于shuffle write的过程中,task给下游stage的每个task都创建了一个磁盘文件,因此shuffle read的过程中,每个task只要从上游stage的所有task所在节点上,拉取属于自己的那一个磁盘文件即可。

shuffle read的拉取过程是一边拉取一边进行聚合的。每个shuffle read task都会有一个自己的buffer缓冲,每次都只能拉取与buffer缓冲相同大小的数据,然后通过内存中的一个Map进行聚合等操作。聚合完一批数据后,再拉取下一批数据,并放到buffer缓冲中进行聚合操作。以此类推,直到最后将所有数据到拉取完,并得到最终的结果。

HashShuffleMagnage 和SortShuffleManage

Spark程序中的Shuffle操作是通过shuffleManage对象进行管理。Spark目前支持的ShuffleMange模式主要有两种:HashShuffleMagnage 和SortShuffleManage 不同的地方在Shuffle Write的方式,Shuffle Read方式是一致的。
Shuffle操作包含当前阶段的Shuffle Write(存盘)和下一阶段的Shuffle Read(fetch),两种模式的主要差异是在Shuffle Write阶段,下面将着重介绍。

HashShuffleWrite

HashShuffle是根据Reduce Task 的个数进行Hash的 , 每个Hash结果文件都只属于下游stage的一个task,如果涉及到按Key分组(比如reduceByKey),那么就是key值的hashcode%ReduceTask来决定放入哪一个区分,这样保证相同的数据一定放入一个分区。

Hash Shuffle过程如下:

HashShuffleWrite02.png

根据为Reduce阶段的Shuffle Read的文件数目进行了优化

为什么进行优化?

  1. Write阶段创建大量的写文件的对象
  2. read阶段就要进行多次网络通信,来拉取磁盘小文件
  3. read阶段创建大量的读文件对象

造成的影响,创建的对象过多,会导致JVM内存不足,JVM内存不足又会导致GC垃圾回收OOM。所以之后提出了合并机制的HashShuffle.

优化方式是什么?

由 M*R个小文件 ==》 变为 Core * R 个小文件。

HashShuffleWriteBetter05.png

SortShuffleWrite

这种shuffle最终产出的文件个数是 M*2 个,即一个Map Task产出一个磁盘数据文件和一个索引文件(标识Reduce拉取的开始文件句柄)。按照是否排序分为

ByPass机制 和 普通机制。

优先选用ByPass机制

触发机制:

  1. shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数(200)的值。
  2. 不是聚合类的shuffle算子(比如reduceByKey),不涉及Key。

SortShuffleWrite07.png

此时task会为每个下游task都创建一个临时磁盘文件,并将数据按key进行hash然后根据key的hash值,将key写入对应的磁盘文件之中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢写到磁盘文件的。最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。

该过程的磁盘写机制其实跟未经优化的HashShuffleManager是一模一样的,因为都要创建数量惊人的磁盘文件,只是在最后会做一个磁盘文件的合并而已。因此少量的最终磁盘文件,也让该机制相对未经优化的HashShuffleManager来说,shuffle read的性能会更好。

而该机制与普通SortShuffleManager运行机制的不同在于:第一,磁盘写机制不同;第二,不会进行排序。也就是说,启用该机制的最大好处在于,shuffle write过程中,不需要进行数据的排序操作,也就节省掉了这部分的性能开销。

普通机制

下图说明了普通的SortShuffleManager的原理。在该模式下,数据会先写入一个内存数据结构中,此时根据不同的shuffle算子,可能选用不同的数据结构。如果是reduceByKey这种聚合类的shuffle算子,那么会选用Map数据结构,一边通过Map进行聚合,一边写入内存;如果是join这种普通的shuffle算子,那么会选用Array数据结构,直接写入内存。

CommonWrite02.png

其实这里和MR的运行过程十分相似,当数据写入内存之前也会进行“打标签”,当内存满了之后申请资源申请不下来时,会先进行排序,然后溢写。

在溢写到磁盘文件之前,会先根据key对内存数据结构中已有的数据进行排序。排序过后,会分批将数据写入磁盘文件。但是,这一个Task的整个过程只产生2个磁盘文件,一个是溢写的磁盘文件,一个是索引文件(其中标识了下游各个task的数据在文件中的start offset与end offset),因为当第一个磁盘写文件溢写完成后,剩下的每次产生的小文件都会与之前的文件进行合并,所以一直只会产生两个磁盘文件。这 , 就是merge过程

这个内存(Executor中内存)的数据结构(Map 或者 Array)的默认大小约为5M。那它为什么是约为5M呢?
因为Executor是一个JVM进程,而Spark只是一个框架,不能准确的控制JVM的资源情况,所以说这个初始值约等于5M,当这5M满了之后,此时监控此内存结构的监控对象会去申请资源(当前已用资源*2-5M),如果申请到了,则不会溢写,如果申请不到则会溢写

总结与MR中shuffle的不同之处

1、 spark中HashShuffle的shuffle write中没有分组和排序

首先,MR的计算思想就是,相同key值的数据为一组,每一组调用一次reduce()函数,但是spark中没有这个说法,所以HashShuffle没有分组和排序.

2、Spark中SortShuffle的普通运行机制和MR

Spark中SortShuffle的普通运行机制和MR的过程几乎是一模一样的,都有写入内存前的打标签、内存写满之后的排序、溢写到磁盘;但是有一点不同,就是SortShuffle的内存是约等于5M且动态变化的,而MR的内存是固定100M的(当然可以改配置文件来修改)

3、Spark中SortShuffle的bypass运行机制和MR

Spark中SortShuffle的bypass运行机制中没有排序,Spark shuffle默认是SortShuffle的bypass运行机制,因为它没有排序和分组,所以这也是比MR快的原因之一。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章