You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

15 KiB

06 | Shuffle管理为什么Shuffle是性能瓶颈

你好,我是吴磊。

在上一讲我们拜访了斯巴克国际建筑集团总公司结识了Spark调度系统的三巨头DAGScheduler、TaskScheduler和SchedulerBackend。相信你已经感受到调度系统组件众多且运作流程精密而又复杂。

任务调度的首要环节是DAGScheduler以Shuffle为边界把计算图DAG切割为多个执行阶段Stages。显然Shuffle是这个环节的关键。那么我们不禁要问“Shuffle是什么为什么任务执行需要Shuffle操作Shuffle是怎样一个过程

今天这一讲我们转而去“拜访”斯巴克国际建筑集团的分公司用“工地搬砖的任务”来理解Shuffle及其工作原理。由于Shuffle的计算几乎需要消耗所有类型的硬件资源比如CPU、内存、磁盘与网络在绝大多数的Spark作业中Shuffle往往是作业执行性能的瓶颈因此我们必须要掌握Shuffle的工作原理从而为Shuffle环节的优化打下坚实基础。

什么是Shuffle

我们先不急着给Shuffle下正式的定义为了帮你迅速地理解Shuffle的含义从而达到事半功倍的效果我们不妨先去拜访斯巴克集团的分公司去看看“工地搬砖”是怎么一回事。

斯巴克集团的各家分公司分别驻扎在不同的建筑工地,每家分公司的人员配置和基础设施都大同小异:在人员方面,各家分公司都有建筑工人若干、以及负责管理这些工人的工头。在基础设施方面,每家分公司都有临时搭建、方便存取建材的临时仓库,这些仓库配备各式各样的建筑原材料,比如混凝土砖头、普通砖头、草坪砖头等等。

咱们参观、考察斯巴克建筑集团的目的毕竟还是学习Spark因此我们得把分公司的人与物和Spark的相关概念对应上这样才能方便你快速理解Spark的诸多组件与核心原理。

分公司的人与物和Spark的相关概念是这样对应的

图片

基于图中不同概念的对应关系接下来我们来看“工地搬砖”的任务。斯巴克建筑集团的3家分公司分别接到3个不同的建筑任务。第一家分公司的建筑项目是摩天大厦第二家分公司被要求在工地上建造一座“萌宠乐园”而第三家分公司收到的任务是打造露天公园。为了叙述方便我们把三家分公司分别称作分公司1、分公司2和分公司3。

显然,不同建筑项目对于建材的选型要求是有区别的,摩天大厦的建造需要刚性强度更高的混凝土砖头,同理,露天公园的建设需要透水性好的草坪砖头,而萌宠乐园使用普通砖头即可。

可是不同类型的砖头分别散落在3家公司的临时仓库中。为了实现资源的高效利用每个分公司的施工工人们都需要从另外两家把项目特需的砖头搬运过来。对于这个过程我们把它叫作“搬砖任务”。

图片

有了“工地搬砖”的直观对比我们现在就可以直接给Shuffle下一个正式的定义了。

Shuffle的本意是扑克的“洗牌”在分布式计算场景中它被引申为集群范围内跨节点、跨进程的数据分发。在工地搬砖的任务中如果我们把不同类型的砖头看作是分布式数据集那么不同类型的砖头在各个分公司之间搬运的过程与分布式计算中的Shuffle可以说是异曲同工。

要完成工地搬砖的任务,每位工人都需要长途跋涉到另外两家分公司,然后从人家的临时仓库把所需的砖头搬运回来。分公司之间相隔甚远,仅靠工人们一块砖一块砖地搬运,显然不现实。因此,为了提升搬砖效率,每位工人还需要借助货运卡车来帮忙。不难发现,工地搬砖的任务需要消耗大量的人力物力,可以说是劳师动众。

Shuffle的过程也是类似分布式数据集在集群内的分发会引入大量的磁盘I/O网络I/O。在DAG的计算链条中Shuffle环节的执行性能是最差的。你可能会问“既然Shuffle的性能这么差为什么在计算的过程中非要引入Shuffle操作呢免去Shuffle环节不行吗

其实计算过程之所以需要Shuffle往往是由计算逻辑、或者说业务逻辑决定的。

比如对于搬砖任务来说不同的建筑项目就是需要不同的建材只有这样才能满足不同的施工要求。再比如在Word Count的例子中我们的“业务逻辑”是对单词做统计计数那么对单词“Spark”来说在做“加和”之前我们就是得把原本分散在不同Executors中的“Spark”拉取到某一个Executor才能完成统计计数的操作。

结合过往的工作经验我们发现在绝大多数的业务场景中Shuffle操作都是必需的、无法避免的。既然我们躲不掉Shuffle那么接下来我们就一起去探索看看Shuffle到底是怎样的一个计算过程。

Shuffle工作原理

为了方便你理解我们还是用Word Count的例子来做说明。在这个示例中引入Shuffle操作的是reduceByKey算子也就是下面这行代码完整代码请回顾第1讲)。

// 按照单词做分组计数
val wordCounts: RDD[(String, Int)] = kvRDD.reduceByKey((x, y) => x + y) 

我们先来直观地回顾一下这一步的计算过程然后再去分析其中涉及的Shuffle操作

图片

如上图所示以Shuffle为边界reduceByKey的计算被切割为两个执行阶段。约定俗成地我们把Shuffle之前的Stage叫作Map阶段而把Shuffle之后的Stage称作Reduce阶段。在Map阶段每个Executors先把自己负责的数据分区做初步聚合又叫Map端聚合、局部聚合Shuffle环节不同的单词被分发到不同节点的Executors中最后的Reduce阶段Executors以单词为Key做第二次聚合又叫全局聚合从而完成统计计数的任务。

不难发现Map阶段与Reduce阶段的计算过程相对清晰明了二者都是利用reduce运算完成局部聚合与全局聚合。在reduceByKey的计算过程中Shuffle才是关键。

仔细观察上图你就会发现与其说Shuffle是跨节点、跨进程的数据分发不如说Shuffle是Map阶段与Reduce阶段之间的数据交换。那么问题来了两个执行阶段之间是如何实现数据交换的呢

Shuffle中间文件

如果用一句来概括的话,那就是,Map阶段与Reduce阶段通过生产与消费Shuffle中间文件的方式来完成集群范围内的数据交换。换句话说Map阶段生产Shuffle中间文件Reduce阶段消费Shuffle中间文件二者以中间文件为媒介完成数据交换。

那么接下来的问题是,什么是Shuffle中间文件,它是怎么产生的,又是如何被消费的?

我把它的产生和消费过程总结在下图中了:

图片

在上一讲介绍调度系统的时候我们说过DAGScheduler会为每一个Stage创建任务集合TaskSet而每一个TaskSet都包含多个分布式任务Task。在Map执行阶段每个Task以下简称Map Task都会生成包含data文件与index文件的Shuffle中间文件如上图所示。也就是说Shuffle文件的生成是以Map Task为粒度的Map阶段有多少个Map Task就会生成多少份Shuffle中间文件。

再者Shuffle中间文件是统称、泛指它包含两类实体文件一个是记录KeyValue键值对的data文件另一个是记录键值对所属Reduce Task的index文件。换句话说index文件标记了data文件中的哪些记录应该由下游Reduce阶段中的哪些Task简称Reduce Task消费。在上图中为了方便示意我们把首字母是S、i、c的单词分别交给下游的3个Reduce Task去消费显然这里的数据交换规则是单词首字母。

在Spark中Shuffle环节实际的数据交换规则要比这复杂得多。数据交换规则又叫分区规则,因为它定义了分布式数据集在Reduce阶段如何划分数据分区。假设Reduce阶段有N个Task这N个Task对应着N个数据分区那么在Map阶段每条记录应该分发到哪个Reduce Task是由下面的公式来决定的。

P = Hash(Record Key) % N

对于任意一条数据记录Spark先按照既定的哈希算法计算记录主键的哈希值然后把哈希值对N取模计算得到的结果数字就是这条记录在Reduce阶段的数据分区编号P。换句话说这条记录在Shuffle的过程中应该被分发到Reduce阶段的P号分区。

熟悉了分区规则与中间文件之后,接下来,我们再来说一说中间文件是怎么产生的。

Shuffle Write

我们刚刚说过Shuffle中间文件是以Map Task为粒度生成的我们不妨使用下图中的Map Task以及与之对应的数据分区为例来讲解中间文件的生成过程。数据分区的数据内容如图中绿色方框所示

图片

在生成中间文件的过程中Spark会借助一种类似于Map的数据结构来计算、缓存并排序数据分区中的数据记录。这种Map结构的Key是Reduce Task Partition IDRecord Key而Value是原数据记录中的数据值如图中的“内存数据结构”所示。

对于数据分区中的数据记录Spark会根据我们前面提到的公式1逐条计算记录所属的目标分区ID然后把主键Reduce Task Partition IDRecord Key和记录的数据值插入到Map数据结构中。当Map结构被灌满之后Spark根据主键对Map中的数据记录做排序然后把所有内容溢出到磁盘中的临时文件如图中的步骤1所示。

随着Map结构被清空Spark可以继续读取分区内容并继续向Map结构中插入数据直到Map结构再次被灌满而再次溢出如图中的步骤2所示。就这样如此往复直到数据分区中所有的数据记录都被处理完毕。

到此为止磁盘上存有若干个溢出的临时文件而内存的Map结构中留有部分数据Spark使用归并排序算法对所有临时文件和Map结构剩余数据做合并分别生成data文件、和与之对应的index文件如图中步骤4所示。Shuffle阶段生成中间文件的过程又叫Shuffle Write。

总结下来Shuffle中间文件的生成过程分为如下几个步骤

1.对于数据分区中的数据记录,逐一计算其目标分区,然后填充内存数据结构;
2.当数据结构填满后,如果分区中还有未处理的数据记录,就对结构中的数据记录按(目标分区 IDKey排序将所有数据溢出到临时文件同时清空数据结构
3.重复前 2 个步骤,直到分区中所有的数据记录都被处理为止;
4.对所有临时文件和内存数据结构中剩余的数据记录做归并排序,生成数据文件和索引文件。

到目前为止我们熟悉了Spark在Map阶段生产Shuffle中间文件的过程那么在Reduce阶段不同的Tasks又是如何基于这些中间文件来定位属于自己的那部分数据从而完成数据拉取呢

Shuffle Read

首先我们需要注意的是对于每一个Map Task生成的中间文件其中的目标分区数量是由Reduce阶段的任务数量(又叫并行度决定的。在下面的示意图中Reduce阶段的并行度是3因此Map Task的中间文件会包含3个目标分区的数据index文件恰恰是用来标记目标分区所属数据记录的起始索引。

图片

对于所有Map Task生成的中间文件Reduce Task需要通过网络从不同节点的硬盘中下载并拉取属于自己的数据内容。不同的Reduce Task正是根据index文件中的起始索引来确定哪些数据内容是“属于自己的”。Reduce阶段不同于Reduce Task拉取数据的过程往往也被叫做Shuffle Read

好啦到此为止我们依次解答了本讲最初提到的几个问题“什么是Shuffle为什么需要Shuffle以及Shuffle是如何工作的”。Shuffle是衔接不同执行阶段的关键环节Shuffle的执行性能往往是Spark作业端到端执行效率的关键因此掌握Shuffle是我们入门Spark的必经之路。希望今天的讲解能帮你更好地认识Shuffle。

重点回顾

今天的内容比较多,我们一起来做个总结。

首先我们给Shuffle下了一个明确的定义在分布式计算场景中Shuffle指的是集群范围内跨节点、跨进程的数据分发

我们在最开始提到Shuffle的计算会消耗所有类型的硬件资源。具体来说Shuffle中的哈希与排序操作会大量消耗CPU而Shuffle Write生成中间文件的过程会消耗宝贵的内存资源与磁盘I/O最后Shuffle Read阶段的数据拉取会引入大量的网络I/O。不难发现Shuffle是资源密集型计算因此理解Shuffle对开发者来说至关重要。

紧接着我们介绍了Shuffle中间文件。Shuffle中间文件是统称它包含两类文件一个是记录KeyValue键值对的data文件另一个是记录键值对所属Reduce Task的index文件。计算图DAG中的Map阶段与Reduce阶段正是通过中间文件来完成数据的交换。

接下来我们详细讲解了Shuffle Write过程中生成中间文件的详细过程归纳起来这个过程分为4个步骤

1.对于数据分区中的数据记录,逐一计算其目标分区,然后填充内存数据结构;
2.当数据结构填满后,如果分区中还有未处理的数据记录,就对结构中的数据记录按(目标分区 IDKey排序将所有数据溢出到临时文件同时清空数据结构
3.重复前 2 个步骤,直到分区中所有的数据记录都被处理为止;
4.对所有临时文件和内存数据结构中剩余的数据记录做归并排序,生成数据文件和索引文件。

最后在Reduce阶段Reduce Task通过index文件来“定位”属于自己的数据内容并通过网络从不同节点的data文件中下载属于自己的数据记录。

每课一练

这一讲就到这里了,我在这给你留个思考题:

在Shuffle的计算过程中中间文件存储在参数spark.local.dir设置的文件目录中这个参数的默认值是/tmp你觉得这个参数该如何设置才更合理呢

欢迎你在评论区分享你的答案,我在评论区等你。如果这一讲对你有所帮助,你也可以分享给自己的朋友,我们下一讲见。