对于spark shuffle的理解

78 阅读7分钟

读书笔记源于

《大数据处理框架Apache Spark设计与实现》 -- 许利杰

什么是suffle

物理计划会按照action分成多个stage,每个stage有多个task进行处理,一个task对应上游的一个partition(一一对应)。其中上游和下游之间的数据传递,即运行在不同stage、不同节点上的task之间的数据传递,这个数据传递过程叫做shuffle机制。

shuffle的难点

设计shuffle有有以下三个难点

  1. 计算的多样性:shuffle分为shuffle write和shuffle read两个阶段,前者解决上游stage输出数据分区的问题,后者解决下游stage从上游stage获取数据、重新组织、并为后续操作提供数据的问题。那么在进行shuffle的时候,有些operator是聚合(groupByKey);有些需要combine(reduceByKey需要在shuffle write端进行combine);有些是需要在shuffle read端对数据进行按key排序。如何构建shuffl以满足不同的操作?如何确定聚合函数、数据分区、数据排序的执行顺序?
  2. 计算的耦合性: 有些操作包含用户自定义聚合函数,如aggregateByKey(seqOp, combOp)中的seqOp, combOp,以及reduceByKey(func)中的func,这些函数计算过程和数据的shuffle write/read耦合在一起(一边进行write/read,一边执行func函数)。那么先读数据再聚合,还是一边读一边聚合?
  3. 中间数据存储问题:shuffle的中间数据如何表示,如何组织,如何存放?

spark如何解决的

数据分区问题

该问题针对shuffle write阶段,如何对map task输出结果进行分区,使得reduce task可以通过网络获取相应数据。

解决方案

  1. 第一个问题是如何确定分区个数?
    分区个数与下游stage的task个数一致。分区个数可以自定义,如果没有自定义,则默认分区个数是parent RDD的分区个数的最大值(上游分多少个partition,下游就多少个,一一对应),然后下游启动numPartitions个task来获取冰处理这些数据。每个key对应一个parititonId,计算为partitionId=Hash(key)%numPartitions
  2. 如何获取上游不同task的输出数据并按照key进行聚合?
    两步聚合法:先从上游不同task里获取到(K,V)record,存放到HashMap中每一个(K,list(V))recod。然后执行func(list(V)),实现数据。
    在线聚合:从上游不同task里获取到(K,V)record,存放到HashMap中每一个(K,func(list(V)))recod。这样就可以加速计算,且减少内存空间占用。(groupByKey这种没有聚合函数的操作使用哪种方法没有差别)

map端combine问题

实现了reduce端的聚合功能,map端如果进行预聚合,那么传递给下游stage的数据就会更少,因此需要考虑如何支持shuffle write端的combine功能。

解决方案:

本质上与shuffle read端的聚合过程没有区别,同样使用HashMap进行存储,将(K,V)record改成了(K,func(list(V)))recod。map端的combine属于单一task的数据聚合,而reduce聚合的是全局数据。具体来说,首先利用HashMap进行combine,然后对HashMap中的每一个record进行分区,输出到对应的分区文件中。

解决sort问题

一些操作如sortByKey()、sortBy()需要对数据按照Key进行排序,那么shuffle如何进行排序的?

  1. 在那里执行sort?
    首先,shuffle read端一定要sort。其次,shuffle write不需要排序,但是如果已经排好序了,那么shuffle read端排序的复杂度会减少。
  2. 何时进行排序,即先排序还是先聚合?
    第一种,先排序后聚合。利用线性结构Array对所有的key进行排序,然后从前到后进行扫描聚合,也不需要再使用HashMap进行hash-based聚合。MapReduce就采用这种方案。
    第二种,排序和聚合同时进行。比如使用treemap这种带排序功能的map,但是时间复杂度太高。
    第三种,先聚合再排序。先使用HashMap进行hash-based聚合,然后再将HashMap中的record或record的引用放入线性数据结构中进行排序。优点是聚合和排序过程独立,灵活性高,之前的在线聚合方案也不用改动。缺点是需要复制数据或引用,空间占用较大。spark选择的这一种方案。

解决内存不足问题

解决方案

使用内存+磁盘混合存储方案。先在内存中进行数据聚合,内存不足,就spill到磁盘上,然后继续处理新的数据。最后对之前spill到磁盘的数据和内存中的数据进行再次聚合,这个过程称为“全局聚合”。为了加速聚合,需要在spill磁盘前进行排序,这样全局聚合才能按顺序读取spill到磁盘上的数据,并减少磁盘I/O。

spark shuffle框架设计

shuffle write框架设计和实现(简略)

框架计算顺序为:

map()输出→数据聚合→排序→分区

map task每计算出一个record及其parititonId,就将record放入类似HashMap的数据结构中进行聚合;聚合完成后,再将HashMap中的数据放入类似Array的数据结构中进行排序,排序既可以按照partitionId,也可以按照partitionId+Key进行排序;最后根据partitionId将数据写入不同的数据分区中。

一个问题:为什么必须按照partitionId进行排序,且不能省略这一步?输出的数据按照partitionId进行排序后,可以放在一个分区文件中进行存储,即可标示不同的分区数据,否则如果每个分区都分出一个文件,那么文件数会过多。

shuffle read框架设计和实现(简略)

框架执行顺序为:

数据获取→聚合→排序

reduce task不断从各个map task的分区文件中获取数据,然后使用类似HashMap的结构来对数据进行聚合,该过程是在线聚合。聚合完成后,将HashMap中的数据放入类似Array的数据结构中按照key进行排序,最后将排序结果输出或传递给下一个操作。其中聚合和排序是可选的。

spark shuffle 与MapReduce的区别

spark shuffle是对MapReduce的优化,那么MapReduce必然是存在一定的缺陷。先总结下MapReduce的优缺点。

优点:

  1. MapReduce的shuffle流程固定,阶段分明,每个阶段读取什么数据、进行什么操作、输出什么数据都是确定性的。这种确定性使得实现起来比较容易。
  2. MapReduce框架的内存消耗也是确定的,map阶段框架只需要一个大的spill buffer,reduce阶段框架只需要一个大的数组(MergeQueue)来存放获取的分区文件中的record。这样,什么时候将数据spill到磁盘上是确定的,也易于实现和内存管理。
  3. MapReduce对Key进行了严格排序,使得可以使用最小堆或最大堆进行聚合,非常高效。而且可以原生支持sortByKey()。
  4. MapReduce按Key进行排序和spill到磁盘上的功能,可以在Shuffle大规模数据时仍然保证能够顺利进行。

缺点:

  1. 强制按key排序。大多数操作其实不需要按key进行排序,如groupByKey,排序增加了操作的计算量。
  2. 不能在线聚合。不管是map端还是reduce端,都是先将数据存到内存或磁盘上后,再执行聚合操作,存储这些数据需要消耗大量的内存和磁盘空间。
  3. 产生的临时文件过多。如果map task个数是M,reduce task个数是N,那么map阶段集群会产生MxN个分区文件

spark解决了这些缺陷

  1. 克服强制排序的方法是对操作类型进行分类,并提供按partitionId排序、按Key排序等多种方式来应对不同操作的排序需求。
  2. 采用hash-based进行聚合,利用HashMap的在线聚合特性。
  3. 将多个分区文件合并成一个文件,按照partitionId的顺序存储,这也是spark为什么一定要按partitionId进行排序的原因。