Spark进程模型

图片

图片

我们先来说区别。首先,Word Count计算流图是一种抽象的流程图,而土豆工坊的流水线是可操作、可运行而又具体的执行步骤。然后,计算流图中的每一个元素,如lineRDD、wordRDD,都是“虚”的数据集抽象,而流水线上各个环节不同形态的食材,比如一颗颗脏兮兮的土豆,都是“实实在在”的实物。

厘清了二者之间的区别之后,它们之间的联系自然也就显而易见了。如果把计算流图看作是“设计图纸”,那么流水线工艺其实就是“施工过程”。前者是设计层面、高屋建瓴的指导意见,而后者是执行层面、按部就班的实施过程。前者是后者的基石,而后者是前者的具化。

你可能会好奇:“我们为什么非要弄清这二者之间的区别和联系呢?”原因其实很简单, 分布式计算的精髓,在于如何把抽象的计算流图,转化为实实在在的分布式计算任务,然后以并行计算的方式交付执行。

今天这一讲,我们就来聊一聊,Spark是如何实现分布式计算的。 分布式计算的实现,离不开两个关键要素,一个是进程模型,另一个是分布式的环境部署。接下来,我们先去探讨Spark的进程模型,然后再来介绍Spark都有哪些分布式部署方式。

进程模型

在Spark的应用开发中,任何一个应用程序的入口,都是带有SparkSession的main函数。SparkSession包罗万象,它在提供Spark运行时上下文的同时(如调度系统、存储系统、内存管理、RPC通信),也可以为开发者提供创建、转换、计算分布式数据集(如RDD)的开发API。 不过,在Spark分布式计算环境中,有且仅有一个JVM进程运行这样的main函数,这个特殊的JVM进程,在Spark中有个专门的术语,叫作“ Driver ”。 Driver最核心的作用在于,解析用户代码、构建计算流图,然后将计算流图转化为分布式任务,并把任务分发给集群中的执行进程交付运行。换句话说,Driver的角色是拆解任务、派活儿,而真正干活儿的“苦力”,是执行进程。在Spark的分布式环境中,这样的执行进程可以有一个或是多个,它们也有专门的术语,叫作“ Executor ”。

我把DriverExecutor的关系画成了一张图,你可以看看:

图片

分布式计算的核心是任务调度,而分布式任务的调度与执行,仰仗的是Driver与Executors之间的通力合作。在后续的课程中,我们会深入讲解Driver如何与众多Executors协作完成任务调度,不过在此之前,咱们先要厘清Driver与Executors的关系,从而为后续的课程打下坚实的基础。

Driver与Executors:包工头与施工工人

简单来看,Driver与Executors的关系,就像是工地上包工头与施工工人们之间的关系。包工头负责“揽活儿”,拿到设计图纸之后负责拆解任务,把二维平面图,细化成夯土、打地基、砌墙、浇筑钢筋混凝土等任务,然后再把任务派发给手下的工人。工人们认领到任务之后,相对独立地去完成各自的任务,仅在必要的时候进行沟通与协调。 其实不同的建筑任务之间,往往是存在依赖关系的,比如,砌墙一定是在地基打成之后才能施工,同理,浇筑钢筋混凝土也一定要等到砖墙砌成之后才能进行。因此,Driver这个“包工头”的重要职责之一,就是合理有序地拆解并安排建筑任务。 再者,为了保证施工进度,Driver除了分发任务之外,还需要定期与每个Executor进行沟通,及时获取他们的工作进展,从而协调整体的执行进度。

一个篱笆三个桩,一个好汉三个帮。要履行上述一系列的职责,Driver自然需要一些给力的帮手才行。 在Spark的Driver进程中,DAGScheduler、TaskScheduler和SchedulerBackend这三个对象通力合作,依次完成分布式任务调度的3个核心步骤,也就是:

  1. 根据用户代码构建计算流图;
  2. 根据计算流图拆解出分布式任务;
  3. 将分布式任务分发到Executors中去。

 

接收到任务之后,Executors调用内部线程池,结合事先分配好的数据分片,并发地执行任务代码。对于一个完整的RDD,每个Executors负责处理这个RDD的一个数据分片子集。这就好比是,对于工地上所有的砖头,甲、乙、丙三个工人分别认领其中的三分之一,然后拿来分别构筑东、西、北三面高墙。

好啦,到目前为止,关于Driver和Executors的概念,他们各自的职责以及相互之间的关系,我们有了最基本的了解。尽管对于一些关键对象,如上述DAGScheduler、TaskScheduler,我们还有待深入,但这并不影响咱们居高临下地去理解Spark进程模型。

不过,你可能会说:“一说到模型就总觉得抽象,能不能结合示例来具体说明呢?”接下来,我们还是沿用前两讲展示的Word Count示例,一起去探究spark-shell在幕后是如何运行的。

spark-shell执行过程解析

在第1讲,我们在本机搭建了Spark本地运行环境,并通过在终端敲入spark-shell进入交互式REPL。与很多其他系统命令一样, spark-shell有很多命令行参数,其中最为重要的有两类:一类是用于指定部署模式的master,另一类则用于指定集群的计算资源容量。

不带任何参数的spark-shell命令,实际上等同于下方这个命令:

spark-shell --master local[*]
这行代码的含义有两层。第一层含义是部署模式,其中local关键字表示部署模式为Local,也就是本地部署;第二层含义是部署规模,也就是方括号里面的数字,它表示的是在本地部署中需要启动多少个Executors,星号则意味着这个数量与机器中可用CPU的个数相一致。

也就是说, 假设你的笔记本电脑有4个CPU,那么当你在命令行敲入spark-shell的时候,Spark会在后台启动1个Driver进程和3个Executors进程。

那么问题来了,当我们把Word Count的示例代码依次敲入到spark-shell中,Driver进程和3个Executors进程之间是如何通力合作来执行分布式任务的呢?

为了帮你理解这个过程,我特意画了一张图,你可以先看一下整体的执行过程:

图片

首先,Driver通过take这个Action算子,来触发执行先前构建好的计算流图。沿着计算流图的执行方向,也就是图中从上到下的方向,Driver以Shuffle为边界创建、分发分布式任务。

Shuffle 的本意是扑克牌中的“洗牌”,在大数据领域的引申义,表示的是集群范围内跨进程、跨节点的数据交换。我们在专栏后续的内容中会对Shuffle做专门的讲解,这里我们不妨先用Word Count的例子,来简单地对Shuffle进行理解。

 

在reduceByKey算子之前,同一个单词,比如“spark”,可能散落在不用的Executors进程,比如图中的Executor-0、Executor-1和Executor-2。换句话说,这些Executors处理的数据分片中,都包含单词“spark”。 那么,要完成对“spark”的计数,我们需要把所有“spark”分发到同一个Executor进程,才能完成计算。而这个把原本散落在不同Executors的单词,分发到同一个Executor的过程,就是Shuffle。

 

大概理解了Shuffle后,我们回过头接着说Driver是怎么创建分布式任务的。对于reduceByKey之前的所有操作,也就是textFile、flatMap、filter、map等,Driver会把它们“捏合”成一份任务,然后一次性地把这份任务打包、分发给每一个Executors。 三个Executors接收到任务之后,先是对任务进行解析,把任务拆解成textFile、flatMap、filter、map这4个步骤,然后分别对自己负责的数据分片进行处理。

 

为了方便说明,我们不妨假设并行度为3,也就是原始数据文件wikiOfSpark.txt被切割成了3份,这样每个Executors刚好处理其中的一份。数据处理完毕之后,分片内容就从原来的RDD[String]转换成了包含键值对的RDD[(String, Int)],其中每个单词的计数都置位1。此时Executors会及时地向Driver汇报自己的工作进展,从而方便Driver来统一协调大家下一步的工作。

 

这个时候,要继续进行后面的聚合计算,也就是计数操作,就必须进行刚刚说的Shuffle操作。在不同Executors完成单词的数据交换之后,Driver继续创建并分发下一个阶段的任务,也就是按照单词做分组计数。 数据交换之后,所有相同的单词都分发到了相同的Executors上去,这个时候,各个Executors拿到reduceByKey的任务,只需要各自独立地去完成统计计数即可。完成计数之后,Executors会把最终的计算结果统一返回给Driver。 这样一来,spark-shell便完成了Word Count用户代码的计算过程。经过了刚才的分析,对于Spark进程模型、Driver与Executors之间的关联与联系,想必你就有了更清晰的理解和把握。

 

不过,到目前为止,对于Word Count示例和spark-shell的讲解,我们一直是在本地部署的环境中做展示。我们知道,Spark真正的威力,其实在于分布式集群中的并行计算。只有充分利用集群中每个节点的计算资源,才能充分发挥出Spark的性能优势。因此,我们很有必要去学习并了解Spark的分布式部署。

 

本文转自网络,如有侵权,联系删除

THE END
分享
二维码
打赏
文章目录
关闭
目 录