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

81 阅读8分钟

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

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

一、课程概要

  1. shuffle概述:shuffle的定义和基本过程
  2. Spark中shuffle算子的使用和特性
  3. Spark中shuffle的核心原理和实现细节
  4. push based shuffle的实现方案

二、课程详细内容

1. shuffle概述

1.1 MapReduce概述

  • MapReduce存在Map、Shuffle、Reduce三个阶段
    • Map阶段:在单机上对一小块数据进行计算的过程
    • shuffle阶段:在map处理好的数据上进行数据移动,为reduce作准备
    • reduce阶段:对移动后的数据进行处理,在单机上处理一小块数据

1.2 shuffle对性能重要

  • M*R次网络连接 (网络请求、server压力大)
  • 大量的数据移动
  • 数据丢失风险
  • 存在大量的排序操作
  • 大量的数据序列化、反序列化操作 (转换为2进制数据流存储、读取,消耗CPU)
  • 数据压缩和解压缩

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

2. shuffle算子

2.1 shuffle算子分类

  • repartition:重新改变分区 coalesce/repartition
  • ByKey:group/agg/combine/sort
  • Join:cogroup/join
  • Distinct

2.2 shuffle算子应用

val counts = text
    .flatMap(line->line.split(" "))
    .map(word->(word,1))
    .reduceByKey(_+_)
counts.collect

reduceByKey触发shuffle。

2.3 Spark中对shuffle的抽象

是否有数据的移动。

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

2.4 算子内部的依赖关系

  • shuffleDependency

    • CoGroupedRDD
      • CoGroup
        • fullOuterJoin, left/rightOuterjoin
        • join
    • ShuffledRDD
      • combineByKeyWithClassTag
        • combineByKey
        • reduceByKey
      • coalesce
      • sortByKey
        • sortBy
  • shuffleDependency构造

    • single key-value pair RDD
    • partitioner
      • 两个接口:numberPartitions/getPartition
      • 经典实现:HashPartitioner
    • serializer
    • optional key ordering
    • optional aggregator
      • createCombiner:单value初始化方法
      • mergeValue:合并一个value到aggregator中
      • mergeCombiners:合并两个aggregator
    • mapSideCombine flag disabled by default

3. shuffle过程

3.1 Hash Shuffle

  • 写数据:每个partition会映射到一个独立的文件,写满后flush到内存。
  • 问题:生成的文件、同时打开的文件太多 (M*R),消耗占用的资源过多。
  • 写数据优化:把每个partition映射成一个文件的片段 只需要申请可以申请到的文件(C*R)
  • 问题:仍然需要打开过多文件 (R个)

3.2 Sort Shuffle

  • 写数据:每个task生成一个包含所有partition数据的文件。

3.3 读数据

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

3.4 shuffle过程的触发流程

graph LR
CollectAction --> SubmitJob --> GetDependencies --> RegisterShuffle

3.5 Shuffle Handle的创建

  • 不支持mapSide Combine,partition小于一定值(200):BypassMergeSortShuffleHandle (hash shuffle)

  • 不支持mapSide Combine,partition小于2^24:SerializedShuffleHandle

  • 其他:原始sortShuffleHandle

3.6 writer实现

3.6.1 bypassMergeShuffleWriter

  • partition较少 200
  • 不需要排序,节省时间
  • 写操作的时候会打开大量文件
  • 类似于hash shuffle,但会将partition file merge到一起

3.6.2 UnsafeShuffleWriter

  • 不能超过2^24 (前24bit记录partition id,不能溢出)
  • 对外内存:unsafe
    • 没有垃圾回收开销
    • 只按照partition进行排序,不排序record
    • record放到内存页 写满后申请新的内存页
    • 写满后触发spill,merge spill file生成两个文件
  • 类似内存页储存序列化数据,写入后不再反序列化
  • 只根据partition排序long array (记录record对应partition id、page num和data offset 不移动数据

3.6.3 SortShuffleWriter

  • 支持combine
    • 需要combine时使用partitionedAppendOnlyMap,本质是个HashTable
    • 不需要combine时PartitionedPairBuffer本质是个array
  • 需要对内内存,因为需要比较key所以如果放在对外内存需要反序列化

3.7 Reader的实现

3.7.1 网络时序图

  • 使用基于netty的网络通信框架
    • 申请对外内存
  • 位置信息记录在mapOutputTracker中
    • 存放在本地和远程,若在远程需要发送请求
  • 主要会发送两种类型请求
    • 客户端发送openBlocks请求
    • 客户端接收到响应后:发送Chunk请求或Stream请求
  • 客户端收取数据后,存储并解析数据

3.7.2 ShuffleBlockFetchIterator

  • 区分local和remote节省网络消耗
  • 对远端构造fetch request对象,添加到队列中(顺序随机,防止热点)
  • fetchUpToMaxBytes:从队列中取出对象,发送request请求
  • fetchLocalBlocks:获取本地blocks
  • 防止OOM内存溢出
    • maxBytesInFlight:限制数据块大小
    • maxReqsInFlight:限制请求数量
    • maxBlocksInFlightPerAddress:限制每个地址上block获取数量
    • maxReqSizeShuffletoMem:最大进入内存的请求size
    • maxAttemptsOnNettyOOM:控制对外OOM次数

3.7.3 External Shuffle Service

  • 解耦数据计算和数据shuffle。
  • ESS作为一个存在于每个节点上的agent为所有shuffle reader提供服务,从而优化Spark作业的资源利用率。
  • MapTask在运行结束后可以正常退出。
  • Map汇报shuffle数据存储的文件位置到ESS,Reduce端读取ESS输出的Shuffle数据

3.8 Zero Copy - shuffle优化

  • DMA direct memory access:直接存储器存取 外部设备不通过CPU直接与系统内存交换数据的接口技术

    • 不使用:打开inputStream和outputStream,kernel->app读入然后app->kernel写出 (发生4次context switch,2次DMA+2次CPU拷贝)
    • 使用sendFile:inputStream直接写到outputStream (2次context switch,2次DMA+1次CPU拷贝)
    • 使用sendfile+DMA gather copy(0次context switch,2次DMA拷贝):readbuffer记录到descriptor,DMA根据descriptor直接从readbuffer拷贝到NIC buffer
  • 合并文件时可以应用,无需拷贝文件到用户态

  • 发送数据时可以应用,直接将文件数据提供给网卡

  • Netty Zero Copy

    • 可堆外内存,避免JVM堆内到堆外内存数据拷贝
    • compositeByteBuf、Unpooled.wrappedBuffer、bytebuf.slice可以合并、包装、切分数组,避免发生内存拷贝 (合并数组可以直接包装整体,对外提供整体的内容)
    • Netty使用FileRegion实现文件传输,FileRegion底层封装了FileChannel#transferTo方法,可以将文件缓冲区数据直接传送到目标channel,避免内核和用户侧缓冲区进行的拷贝

3.9 常见问题

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

3.10 常见优化

  • 避免shuffle:使用broadcast替代join
  • 使用可以map-side预聚合的算子
  • shuffle参数优化 (见ppt)
  • spark.default.parallelism && spark.sql.shuffle.partitions
  • spark.hadoopRDD.ignoreEmptySplits
  • spark.hadoop.mapreduce.input.fileinputformat.split.minsize
  • spark.sql.file.maxPartitionBytes
  • spark.sql.adaptive.enabled && spark.sql.adaptive.shuffle.targetPostShuffleInputSize
  • spark.reducer.maxSizeInFlight
  • spark.reducer.maxReqsInFlight spark.reducer.maxBlocksInFlightPerAddress
  • shuffle倾斜优化
    • 有的task非常热点
    • 容易导致作业运行时间变长、TaskOOM作业失败
    • 解决方法
      • 提高并行度:足够简单/只缓解、不根治
      • AQE skew join:根据shuffle文件统计数据自动检测倾斜,将倾斜分区打散成为小的子分区,然后各自进行join

4. push shuffle

4.1 为什么需要push shuffle

  • avg IO size太小,造成大量的随机IO,严重影响磁盘的吞吐
  • M*R次读请求,造成大量网络连接,影响稳定性

4.2 push shuffle的实现

在读取之前将数据合并到一起。

4.2.1 Magnet

  • 实现原理

    • spark driver组件:协调整体的shuffle操作
    • map任务的shuffle writer完成后,增加一个额外的操作push-merge,将数据复制一份推到远程shuffle服务上
    • magnet shuffle service是一个强化版ESS。将隶属于同一个shuffle partition的block会在远程传输到magnet后被merge到一个文件中
    • reduce任务从magnet shuffle service接受合并好的shuffle数据
    • bitmap:存储已merge的mapper id,防止重复merge
    • position offset:如果本次block没有正常merge,可以恢复到上一个block的位置
    • currentMapId:标识当前正在append的block,保证不同的mapper的block能依次append
  • 可靠性

    • 如果maptask输出block没有成功push到magnet上并且反复重试失败,则reduce task直接从ESS上拉取原始block数据

4.2.2 Cloud Shuffle Service

  • IO聚合:所有mapper的同一partition数据都远程写到同一个文件
  • 架构
    • zookeeper 服务发现
    • CSS worker:向zookeeper注册,partitions/Disk|HDFS
    • Spark Driver:集成启动CSS Master
    • CSS Master:shuffle规划/统计
    • CSS ShuffleClient:读写
    • SparkExecutor:Mapper/Reducer
  • 写入流程
    • 向worker0,worker1同时写入 若pushData失败,则reallocation,改向worker2 worker3写入 (最终获得3个有效文件和一个失败Epoch file
  • AQE
    • 大文件会被读很多遍,跳过很多行
    • 切分大文件变为一般大的文件 Epoch files 512MB

三、实践例

  • 参数调整
    • targetPostShuffleInputSize:64M->512M
    • maxPartitionBytes:1G->40G
    • 增大MapTask数据处理量后,由于算子存在map-side agg,减少整体shuffle数据量
    • 通过参数调整增大了chunk size,减缓了随机读取,减少了shuffle过程中IOPS,避免长时间blocked Time

四、个人总结

  • 难点
    • shuffle的原理与实现:三种不同的shuffle
    • shuffle优化,push shuffle
  • 个人总结:本次课程主要学习了Spark shuffle的原理与相关实现,介绍了针对不同partition数的三种shuffle策略。同时学习了为了减轻磁盘随机读取而采用的优化push shuffle策略。课后应当查询更多相关源代码进一步了解shuffle的实现。

五、引用参考

  1. 【大数据专场 学习资料二】第四届字节跳动青训营