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

131 阅读5分钟

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

Shuffle概述

MapReduce概述

image-20220825153240505

  • 2004年谷歌发布了《MapReduce:Simplified Data Processing on LargeClusters》论文
  • 在开源实现的MapReduce中,存在Map、Shuffle、Reduce三个阶段。

上图展示了一个典型的 mapreduce 过程。有一大堆数据,如果我们用单机去分析这些数据的话,可能一周或者一个月的时间都没有办法分析完。然后怎么办呢?我们把这些数据就拆分成若干的小份,分散在很多机器上去做并发处理。假设我把这些数据拆成单份只有 500 兆的数据。那在 1 在一个单机上去处理 500 兆的数据,是可以很快的处理完的。然后第一步的这个分布式的处理过程我们叫做map,然后因为每份数据都可能包含任意的颜色,所以我们需要对不同颜色的数据分别处理。在所有这些任务完成处理完成之后,为了把所有把颜色归并到一起,我们就需要移动数据。然后再把相同颜色的数据移动到一起之后,又可以把这些数据分散在很多机器上去做并发处理了。

第二步,这个分成单份数据做的这个处理我们叫做 reduce 在中间移动数据的这一步我们就是 shuffle,值得一提的是在这个 map 和 reduce 的过程中被分成一块单独去处理的这个数据,我们一般把它叫做partition,它是在 map 阶段的基础上进行数据移动。然后我们把每一个 map 处理好的数据再去做移动,然后把相同颜色的数据放到一起,为后续的 reduce 阶段做准备。第三个阶段就是 reduce 阶段。这个时候我们已经把相同颜色的数据放到一起了,就是在 reduce 阶段对移动后的数据继续进行处理。然后这一步这一步依然是在单件上处理一小份数据,最终我们可以拿到就是不同颜色的一个。 sum 值。

为什么Shuffle对性能非常重要

  • MR次网络连接

  • 大量的数据移动

  • 数据丢失风险

    数据在移动的过程中以及在计算的过程中都是有可能会丢失的,就是它是有丢失风险的。然后一旦这些数据丢失,我们就面临数据的重算。

  • 可能存在大量的排序操作

    我们需要在 map 阶段把不同颜色的数据区分开放到不同的位置,我们就需要对这些数据以颜色来进行排序。

  • 大量的数据序列化、反序列化操作

    要把 Java 或者是某一个内存中的一个直观的数据转化成一个二进制的数据流,然后放到文件中,然后我们再从文件中把它读到内存里面来,然后变成一重新变成一个对象。这个是序列化和反序列化操作,也会消耗大量的 CPU

  • 数据压缩

    如果我们的数据量非常大,我们在存储的过程中可能还涉及到数据的压缩与解压缩,这个也会消耗大量的 CPU

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

Shuffle算子

Spark中会产生Shuffle的算子大概可以分为4类

image-20220825155930404

Spark中队shuffle的抽象:宽依赖、窄依赖

image-20220825160718204

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

算子内部的依赖关系

  • ShuffleDependency
    • CoGroupedRDD
      • Cogroup
        • fullOuterJoin、rightOuterJoin、 leftOuterJoin
        • join
      • ShuffledRDD
        • combineByKeyWithClassTag
          • combineByKey
          • reduceByKey
        • Coalesce
        • SortByKey
          • sortBy

Shuffle Dependency构造

  • A single key-value pair RDD, i.e. RDD[Product2[K, V],
  • Partitioner (available as partitioner property),
  • Serializer,
  • Optional key ordering (of Scala's scala.math.Ordering type),
  • Optional Aggregator,
  • mapSideCombine flag which is disabled (i.e. false) by default.

Partition

image-20220825162045093

image-20220825162053913

两个接口

  • numberPartitions
  • getPartition

经典实现

  • HashPartitioner

Aggregator

它是在进行 Shark 的时候一个非常重要的性能优化器。

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

Shuffle过程

发展历史

  • Spark 0.8及以前Hash Based Shuffle
  • Spark 0.8. 1为Hash Based Shuffle引|入File Consolidation机制
  • Spark 0.9引入ExternalAppendOnlyMap
  • Spark 1.1引入Sort Based Shuffle,但默认仍为Hash Based Shuffle
  • Spark 1.2默认的Shuffle方式改为Sort Based Shuffle
  • Spark 1.4引入Tungsten-Sort Based Shuffle
  • Spark 1.6 Tungsten-Sort Based Shuffle并入Sort Based Shuffle
  • Spark 2.0 Hash Based Shuffle退出历史舞台

Hash Shuffle——写数据

image-20220825162406929

他是把不同的 partition 直接就写到不同的文件中,每一个 map task 都会给每一个 partition 创建一个 buffer 写满了之后就 Flash 到磁盘里面。最终会生成 M 乘以 R 个文件。

存在问题:

这样的话生成的文件太多了,对文件系统造成的压力太大了。而且不光它是生成的文件多,它实际上在运行过程中同时打开的文件也会非常多。然后我们知道打开一个文件本身也是需要消耗资源的。

Hash Shuffle——写数据优化

image-20220825162711388

每个partition会映射到一个文件片段

Sort Shuffle——写数据

image-20220825162820343

每个task生成-个包含所有pariton数据的文件

Shuffle过程的触发流程

image-20220825162901571

Shuffle Handle的创建

image-20220825162942799

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

Handle与Writer的对应关系

image-20220825163036067

BypassMergeShuffleWriter

image-20220825163122240

  • 不需要排序,节省时间
  • 写操作的时候会打开大量文件
  • 类似于Hash Shuffle

UnsafeShuffleWriter

image-20220825163241522

  • 使用类似内存页储存序列化数据
  • 数据写入后不再反序列化

image-20220825163317531

  • 只根据partition 排序Long Array
  • 数据不移动

SortShuffleWriter

image-20220825163407027

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