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

123 阅读4分钟

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

MapReduce概述

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

  • Map阶段,是在单机上进行的针对一小块数据的计算过程,简单来说呢,就是按照给定的方法进行筛选分类;
  • Shuffle 阶段,在map阶段的基础上,进行数据移动,为后续的reduce阶段做准备,也就是说,map阶段将几个小块数据分类完成后,shuffle将同类型的数据进行合并;
  • Reduce阶段,对移动后的数据进行处理,依然是在单机上处理一小份数据,举个例子,对Shuffle得到的合并后的数据进行count,得到sum值。

为什么Shuffle对性能非常重要

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

  • M*R 次网络连接
  • 大量的数据移动
  • 数据移动和计算的过程中,存在丢失风险
  • 可能存在大量排序操作
  • 大量的数据序列化、反序列化操作
  • 数据压缩(如果数据量非常大,存储中可能涉及)

Shuffle算子分类

  • repartition:coalesce、repartition(重新改变分区)
  • ByKey:groupByKey、reduceByKey、aggregateByKey、combineByKey、sortByKey、sortBy(把(key,value)中的key聚合在一起计算)
  • join:cogroup、join、leftOuterJoin、intersection、subtract、subtractByKey(把本身没有在一起的数据按某种条件放在一起)
  • Distinct:distinct(可以理解为特殊的ByKey操作)

Shuffle实现

  • Hash Shuffle v1 - 写数据

    • 每个partition映射到一个独立文件,会生成大量文件,同时打开的文件也会非常多
  • Hash Shuffle v2 - 写数据优化

    • 每个partition映射到一个文件片段,减少了文件数量
  • Sort Shuffle - 写数据

    • 每个task生成一个包含所有partition数据的文件
    • 通过排序的方式将相同的partition放到一起,不过排序会消耗更多的CPU
  • Shuffle - 读数据

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

Shuffle Writer实现

BypassMergeShuffleWriter

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

UnsafeShuffleWriter

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

SortShuffleWriter

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

Shuffle Reader 实现

ShuffleBlockFetchIterator

  • 区分local和remote节省网络消耗
  • 为防止OOM设置了很多限制,e.g.maxBytesInFlight(限制获取数据块的大小);maxReqsInFlight(限制获取数量);maxBlocksInFlightPerAddress(限制每个地址上获取的数量)

External Shuffle Service

ESS作为一个存在于每个节点上的agent为所有Shuffle Reader提供服务,从而优化了Spark作业的资源利用率,MapTask在运行结束后可以正常退出。

Shuffle优化使用的技术

Zero Copy:读写数据时都会用到,避免了数据在用户空间和内存空间之间的拷贝,从而提高了系统的整体性能。

  • 可堆外内存,避免JVM堆内存到堆外内存的数据拷贝。
  • ( 举例:CompositeByteBuf、Unpooled.wrappedBuffer、ByteBuf.slice),可以合并、包装、切分数组,避免发生内存拷贝
  • Netty 使用FileRegion实现文件传输,FileRegion 底层封装了FileChannel#transferTo() 方法,可以将文件缓冲区的数据直接传输到目标Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝

Spark3.0典型问题

  • 数据存储在本地磁盘,没有备份
  • IO并发:大量RPC请求(M*R)
  • IO吞吐:随机读、写放大(3X)
  • GC频繁,影响NodeManager

Shuffle优化/参数优化/倾斜优化

  • 避免shuffle,使用broadcast替代join。(对于小文件(较小数据RDD)可以进行广播变量)
  • 使用可以map-side预聚合的算子。(比如word count,在Shuffle前聚合一次)
  • 参数优化
  • 数据倾斜是某些task分配的数据过大,导致作业运行时间变长和Task OOM导致作业失败。
    一般解决办法是提高并行度,但是只能缓解、不能根治。
    可以使用AQE根据shuffle文件统计数据自动检测倾斜数据,将那些倾斜的分区打散成小的子分区,然后各自进行join。(系统检测大的task数据,将他进行拆分成小的task执行)