MapReduce的Shuffle 和 Spark的Shuffle

187 阅读7分钟

1. MapReduce的Shuffle

根据输入数据、集群规模,适当改变切片的大小,这样尽量利用集群的资源,尽可能的并行执行任务,通过配置3个参数来优化:
切片大小= Math.max(minSize, Math.min(maxSize, blockSize)
mapreduce.input.fileinputformat.split.minsize
mapreduce.input.fileinputformat.split.maxsize
dfs.blocksize
在这里需要自定义分区,缓解数据倾斜问题,这样可以让后面的reduce阶段,平均分摊任务 
在这里:
1、对于不需要排序的需求,可以自定义排序,改变排序规则,使其尽量简单
2、尽量让spill文件的数量少一些,这样可以减少io操作
可以通过环型缓冲区参数配置:
mapreduce.task.io.sort.mb (默认是1024M )
mapreduce.map.sort.spill.percent (默认是0.8 )
可以Combiner的,就Combiner,减少键值对的存在
一次性尽量合并多个spill ,配置参数mapreduce.task.io.sort.factor
压缩会对cpu造成压力,对io有利,根据实际情况,适当平衡,配置下面参数:
mapreduce.map.output.compress
mapreduce.map.output.compress.codec
控制reduce节点,并发下载map输出的线程数,控制下载超时时间.配置下面参数:
mapreduce.reduce.shuffle.parallelcopies
mapreduce.reduce.shuffle.read.timeout
设置缓冲区大小:
mapreduce.reduce.shuffle.input.buffer.percent 
(占用mapreduce.admin.reduce.child.java.opts指定的reduce阶段堆内存的百分比)
mapreduce.reduce.shuffle.merge.percent ( spill阈值百分比)
Merge Sort
(1)内存到内存的Merge
(2)内存中的Merge
(3.1)copy过程中磁盘文件
Merge目的:减少最终合并量。
(3.2)最终磁盘的Merge,一般是边拷贝数据边排序
通用优化,Hadoop默认使用4KB作为缓冲,可以通过io.file.buffer.size来调高缓冲池大小。在core-site.xml配置
reduce的输入缓冲区 mapreduce.reduce.input.buffer.percent
mapreduce.job.jvm.numtasks为每个task启动一个新的JVM,将耗时1秒左右,对于运行时间较长(比如1分钟以上)的job影响不大。但如果都是时间很短的task,那么频繁启停JVM会有开销。
如果我们想使用JVM重用技术来提高性能,那么可以将mapreduce.job.jvm.numtasks设置成大于1的数。
这表示属于同一job的顺序执行的task可以共享一个JVM。也就是说第二轮的map可以重用前一轮的JVM,而不是第一轮结束后关闭JVM ,第二轮再启动新的JVM。

image.png

1.1 Partitioner常见分区规则

  • 随机
    • 优点:不会产生数据倾斜
    • 缺点:相同标记的待计算数据不会被发送到相同的计算节点
  • 轮询
    • 优点:不会产生数据倾斜
    • 缺点:相同标记的待计算数据不会被发送到相同的计算节点
  • Hash散列
    • 优点:相同标记的待计算数据会被发送到相同的计算节点
    • 缺点:会产生数据倾斜
  • 范围分区
    • 优点:相邻的标记的数据会被发送到同一个节点,相同的标记的数据会被发送到同一个节点
    • 缺点:如果范围分区规则做的不合适,也会导致数据倾斜
  • 自定义分区
    • 优点:灵活
    • 缺点:一般来说,都得自己写代码class XXXParititoiner implements Paritioiner
  • 广播分区
    • 优点:每个节点都会接收到这个数据一份
    • 缺点:极大的增加了网络负担,Strom直接提供了一个broadCast的shuffle方式。

1.2 MapReduce Shuffle 特性总结

MapReduce的MapTask负责计算输入文件中的一段数据, MapTask和MapTask之间是没有关系的,是并行运行的

总结起来:

  • MapTask扫描数据源,提取待计算数据,打上标记,也就是形成Key-Value输出到下游,其实Key就是标记,Value就是待计算数据
  • MapReduce Shuffle就是根据标记将计算数据分发到不同的计算节点
  • ReduceTask不停获取标记相同的一组数据执行逻辑计算得到计算结果

问:如果最终需要计算得到的结果集不需要排序,看起来,好像这个排序并没有必要!为什么要排序?

  • 不是为了让结果集有序,有些MR Job的计算结果不要求有序,但是只要有shuffle就一定会排序
  • 而是为了提高MR Job的执行效率,是为了提高ReduceTask的效率。为了排序,反而把MapTask的效率降低了,但是整体提高了

1.3 MapReduce Shuffle为什么要文件合并呢?

  1. 由于MapTask输出数据的时候,是先写入100M的内存区间中,每次装满80%则执行一次溢写形成一个磁盘临时文件, 这样必定会导致MapTask的输出磁盘文件会特别多,给文件系统带来负担。
  2. 如果不合并,那么ReduceTask过来拉取MapTask的输出数据的时候,需要打开很多的文件句柄,进一步增加负担。
  3. 每个MapTask输出的单个文件是有序的,但是不代表该MapTask输出的所有结果都是有序的,所以还需要做文件的合并来保证MapTask的输出有序。

2. Spark ShuffleManager

大多数Spark作业的性能主要就是消耗在了shuffle 环节,因为该环节包含了大量的磁盘I0、序列化、网络数据传输等操作。理解Spark的shuffle的工作原理,有助于对Spark application进行调优,减少资源消耗,提升生产效率。

在Spark的源码中,负责Shuffle过程的执行、计算和处理的组件主要就是ShuffleManager,也即shuffle管理器。

Spark-1.2版本以前:默认实现是: HashShuffleManager

Spark-1.2 版本以后:默认实现是: SortShuffleManager

HashShuffleManager的缺点是shuffle过程中会产生大量的临时结果文件

SortShuffleManager 的改进是让每个Task只产生一个结果文件(多个临时文件会合并到一个文件中),下游的Task过来拉取对应分区数据的时候,只需要去根据索引按需获取即可。

image.png

image.png

image.png

image.png

SortShuffleManager的普通运行机制的大致工作原理:

  • 数据首先写入内存数据结构(聚合类shuffle算子用Map或者union,join等,普通shuffle类算子Buffer)
  • 内存数据结构每写入一条数据,都会执行一次判断, 如果达到临界阈值,则会执行flush刷盘动作,然后清空内存数据结构,为了不阻碍Task的继续执行,会生成一个新的Map或者Buffer
  • 在溢写数据到磁盘之前,会先对内存数据结构中的数据进行排序,数据排序过后,会分批次写入磁盘文件
  • 多次溢写形成的临时磁盘文件会合并成一个大文件, 并且会生成一个索引文件用来记录该文件中每 个分区数据的起始偏移量

这种工作机制,和MapReduce的shuffle机制简直一模一样。目的都是为了减少临时文件数量。Spark SortShuffleManager相比MapReduce的Shuffle有两大改进:

  • 内存数据结构的灵活选用: map | buffer
  • 进行溢写的时候,分批次溢写

SortShuffleManager的普通运行机制,其实还可以进行优化,就是bypass机制。相较于普通SortShuffleManager的区别是:

  • 不再需要对数据进行排序:这是相较于MapReduce的一大改进,用户可以选择是否在shuffle中排序
  • 写磁盘机制不一样:不用再进行内存缓存,而是直接写磁盘

是否可以优化成bypass机制,取决于两个条件:

  • partition数量 <= 200
  • 没有设置map端的combine

Spark Shuffle Read设计实现总计:

  • 使用ShuffleBlockFetcherlterator获取本地或远程节点上的block 并转化为流,最终返回一小部分数据的迭代器
  • 完整的Spark Shuffle Read分为5个步骤:获取block输入流、反序列化输入流、添加监控、数据聚合、数据排序,在这个流程中,输入流和迭代器都没有把大 数据量的数据一次性全部加载到内存中,这些迭代器保证了处理大量数据的高效性。
  • 在数据聚合和排序阶段,大数据量被不断溢出到磁盘中,数据最终还是以迭代器形式返回,确保了内存不会被大数据量占用,提高了数据的吞吐量和处理数据 的高效性。
  • 迭代器设计模式的使用,使得程序易扩展,处理环节可插拔,处理流程清晰易懂。