大数据Shuffle原理与实践 | 青训营笔记

153 阅读5分钟

这是我参与「第四届青训营 」笔记创作活动的第6天

1.Shuffle概述

Shuffle是什么,为什么需要Shuffle,Shuffle的基本过程是怎么样的

①MapReduce概述

  • 在开源实现的MapReduce中,存在Map、Shuffle、Reduce三个阶段

  • 在Map和Reduce中被分成一块一块的单独处理的数据叫做partition

  • Map阶段:是在单机上进行的针对一小块数据的计算过程

  • Shuffle阶段:在map阶段的基础上,进行数据移动,为后续的reduce阶段做准备

  • Reduce阶段:对移动后的数据进行处理,依然是在单机上处理一小份数据

②为什么shuffle对性能非常重要

  • M*R次网络连接
  • 大量的数据移动
  • 数据丢失风险
  • 可能存在大量的排序操作
  • 大量的数据序列化、反序列化操作:将内存中直观的数据转化成二进制数据流放到文件中,再从文件中读到内存中来转换成对象,会消耗大量CPU
  • 数据压缩、解压缩

③小结

批计算的发展流程:

image.png

在大数据场景下,数据Shuffle表示了不同分区数据交换的过程,不同的shuffle策略性能差异大,目前在各个引擎中shuffle都是优化的重点,在spark框架中,shuffle是支撑spark进行大规模复杂数据处理的基石

2.Shuffle算子

介绍Spark中常用的Shuffle算子

①shuffle算子分类

spark中会产生shuffle的算子大概可以分成4类

image.png

②shuffle算子应用

image.png

③Saprk中对shuffle的抽象-宽依赖、窄依赖

算子会产生shuffle是因为它需要做数据移动,在spark中,它被抽象为宽依赖和窄依赖

  • 窄依赖:父RDD的每个分片至多被子RDD中的一个分片所依赖

  • 宽依赖:父RDD中的分片可能被子RDD中的索格分片所依赖

  • 当出现宽依赖时,spark会把运算拆分成两个stage,一个是map stage,一个是reduce stage,两个stage之间很产生shuffle操作

算子内部的依赖关系

  • ShuffleDependency的创建场景

    • 当CoGroupedRDD被创建时(其创建场景:)

      • 当调用Cogroup操作时

        • fullOuterJoin、rightOuterJoin、leftOuterJoin
        • join
    • 当ShuffledRDD被创建时(其创建场景:)

      • 当调用combineByKeyWithClassTag以及其他transform操作时

        • combineByKey
        • reduceByKey
      • Colalesce

      • sortByKey

        • sortBy
  • 于是得到一个树形结构(一个更合理的算子分类)

Shuffle Dependency构造函数包含的变量

  • 一个key-value对形式的RDD
  • Partitioner:负责给定一个key时产生一个key对应的分区
  • Serializer:把一个对象映射成一个二进制的数据流,或把二进制数据映射成对象
  • key是否需要排序的flag
  • Optional Aggregator
  • 描述shuffle是否有mapSideCombine的flag

Shuffle Dependency构造-Partitioner

  • 负责把key映射成一个数字,这个数字代表某一个具体的分区

  • Partitioner是一个抽象类,有两个接口:

    • numberPartitions(分区个数)
    • getPartition(输入为key,输出为key对应的分区)
  • 经典实现

    • HashPartitioner(大多数情况下的默认实现),在构造时需要传入总的partition的数量

Shuffle Depenfdency构造-Aggregator

  • 是shuffle时重要的性能优化器:把部分reduce的工作让map来做

  • 三个方法

    • createCombiner:只有一个value时初始化的方法
    • mergeValue:合并一个value到Aggregator中
    • mergeCombiners:合并两个Aggregator

3.Shuffle过程

Spark中shuffle的核心原理

①Hash Shuffle-写数据

每个partition会映射到一个独立的文件,每个Map Task都会为Partition创建一个buffer,写满了就flush到磁盘里,最终会生成M*R个文件

随着数据的增大,会对文件系统造成压力,不仅生成的文件多,同时打开的文件也多

Hash Shuffle-写数据优化

  • 每个partition会映射到一个文件片段
  • 生成的文件个数为c*R(c是文件核数)

②Sort shuffle-写数据

每个task生成一个包含所有partition数据的文件,不再给每个partition一个shuffle,当内存满了,通过排序的方式,将相同partition的数据放在一起

③Shuffle-读数据

每个reduce task分别获取所有map task生成的属于自己的片段

④Shuffle过程的触发流程

image.png

前五行只是记录运算过程的对象,不会触发任何计算,只有当执行collect(action算子)时才会触发计算,触发流程如下图所示:

image.png

⑤Shuffle Handle的创建

Register Shuffle时做的最重要的事情是根据不同条件创建不同的Shuffle Handle

image.png

⑥Shuffle Handle与Shuffle Writer的对应关系

image.png

Writer实现-BypassMergeShuffleWriter

  • 不需要排序,节省时间(只适用于partition较少的情况)
  • 写操作时会打开大量的文件
  • 类似于Hash Shuffle

Writer实现-UnsafeShuffleWriter

  • 使用类似内存页储存序列化数据

  • 数据写入后不再反序列化

  • Unsafe:使用的是对外内存,而不是Java本身的HBase;使用对外内存的原因:没有Java对象模型内存开销,没有垃圾回收的开销,这在性能上是更优的

  • 它把对外内存分成若干内存页,一个内存页写满就新建一个内存页,直到内存写满,spill到磁盘上,最后merge会将spill文件和内存文件进行merge

  • 对于对外内存的管理

    • record序列化往对外内存写时,还会在堆内有一个Long Array记录原信息(record属于哪个partition,记录在第几页,内存页偏移量多少)
    • spill后会触发排序,因为要把相同partition的放在一起,这个排序只发生在array上,而不管记录在对外内存的record,数据不移动

Writer实现-Sort Shuffle Writer

  • 支持combine
  • 需要combine时,使用PartitionedAppendOnlyMap,本质是个hashTable
  • 不需要combine时PartitionPairBuffer本质是个array