一、Shuffle机制
shuffle的意义和设计挑战
什么是shuffle机制?
运行在不同stage、不同节点上的task间如何进行数据传递。
shuffle机制解决了什么问题?
shuffle解决的问题是如何将数据重新组织,使其能够在上游和下游之间进行传递和计算。如果是单纯的数据传输,则只需要对数据进行分区、通过网络传输即可,没有太大难度,但是shuffle机制还需要进行各种类型的计算(如聚合、排序),而且数据量一般会很大。
shuffle设计过程中的多个挑战
-
计算的多样性:shuffle机制分为shuffle write和shuffle read阶段,前者主要是解决上游stage输出数据的分区问题,后者主要是解决下游stage从上游stage获取数据、重新组织、并为后续操作提供数据的问题。
如何建立一个统一的操作来支持这些操作呢?
如何根据不同数据操作的特点,灵活地构建shuffle write/read的过程呢?
如何确定聚合函数、数据分区、数据排序的执行顺序呢? -
计算的耦合性
聚合和读取的过程耦合在一起,具体是在什么时候调用这些聚合函数呢?是先读取再聚合还是边读边聚合呢? -
中间数据存储问题
在shuffle机制中需要对数据进行重新组织(分区、聚合、排序等),也需要执行一些计算(执行聚合函数),那么在shuffle write和shuffle read过程中的中间数据应如何表示?如何组织?如何存放?
若shuffle的数据量过大,内存中无法存下应该怎么办?
shuffle的设计思想
解决数据分区和数据聚合的问题
数据分区问题
:针对shuffle write端,分区个数与下游stage的task个数一致。
数据聚合问题
:针对shuffle read 端将相同key的record放在一起
,方法是使用两步聚合:
先将不同tasks获取到的<k, v>record存放到hashMap中,HashMap中的Key是K,Value是list(V)。
对于HashhMap中的每一个<K,list(V)>record,使用func计算得到<K,func(list(V))> record。
优缺点:可以解决数据聚合问题、逻辑清晰、容易实现;但是所有shuffle的数据都要放到HashMap中去,占用的内存空间比较大
。
优化方案:在线聚合
解决map端的combine问题
什么情况会需要map端的combine? 只有包含聚合函数的数据操作需要进行map端的combine,目的是减少shuffle 的数据量。
combine解决方案:使用HashMap进行combine,然后对HashMap中的每一个record进行分区,输出到对应的分区文件中
。
解决sort问题
在哪里完成排序?
Shuffle read端进行排序,理论在如果在shuffle write端进行排序,那么在shuffle read端获取到的数据已经是部分有序的数据,可以减少shuffle read端排序的复杂度。
何时进行排序,排序和聚合的顺序是什么样的?
先排序再聚合:使用线性数据结构,如Array存储Shuffle Read的kv record,然后对key进行排序,排序后可以从前向后进行扫描聚合,不需要再使用HashMap进行hash-based 聚合。
聚合和排序同时进行:使用自带排序功能的Map,如TreeMap对中间数据进行聚合,与HashMap对比的缺点在于,TreeMap数据插入的复杂度较高,在O(nlogn),不适合数据量非常大的情况。
先聚合后排序
:spark设计了一种特殊的HashMap来高效完成先聚合后排序的任务。
解决内存不足的问题
shuffle数据量过大导致内存放不下怎么办?使用内存+磁盘混合存储
方案。
溢写到磁盘上,这个可以不断重复。spill到磁盘上的数据实际上是部分聚合的结果,并没有和后续的数据进行过聚合。
“全剧聚合”:在进行下一步数据操作之前对磁盘和内存中的数据进行再次聚合。为了加速全局聚合,我们需要将数据spill到磁盘上时进行排序,这样全局聚合才能够按顺序读取spill到磁盘上的数据,并减少磁盘IO。
Spark中Shuffle框架的设计
shuffle write 框架设计和实现
数据操作需要有聚合、排序和分区三个功能,框架的具体计算顺序为“map()输出 ➡️ 数据聚合 ➡️ 排序 ➡️ 分区” 输出。
shuffle read 框架设计和实现
支持高效聚合和排序的数据结构
内存为主,磁盘为辅。
Array[(K, V)]