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.

158 lines
16 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 04 | 进程模型与分布式部署:分布式计算是怎么回事?
你好,我是吴磊。
在[第2讲](https://time.geekbang.org/column/article/417164)的最后我们留了一道思考题。Word Count的计算流图与土豆工坊的流水线工艺二者之间有哪些区别和联系如果你有点记不清了可以看下后面的图回忆一下。
![图片](https://static001.geekbang.org/resource/image/af/6d/af93e6f10b85df80a7d56a6c1965a36d.jpg?wh=1920x512 "Word Count计算流图")
![图片](https://static001.geekbang.org/resource/image/4f/da/4fc5769e03f68eae79ea92fbb4756bda.jpg?wh=1920x586 "土豆工坊的流水线工艺")
我们先来说区别。首先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**”。
我把**Driver**和**Executor**的关系画成了一张图,你可以看看:
![图片](https://static001.geekbang.org/resource/image/de/36/de80376be9c39600ab7c4cc109c8f336.jpg?wh=1920x1503 "Driver与ExecutorsSpark进程模型")
分布式计算的核心是任务调度而分布式任务的调度与执行仰仗的是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命令实际上等同于下方这个命令
```plain
spark-shell --master local[*]
```
这行代码的含义有两层。第一层含义是部署模式其中local关键字表示部署模式为Local也就是本地部署第二层含义是部署规模也就是方括号里面的数字它表示的是在本地部署中需要启动多少个Executors星号则意味着这个数量与机器中可用CPU的个数相一致。
也就是说假设你的笔记本电脑有4个CPU那么当你在命令行敲入spark-shell的时候Spark会在后台启动1个Driver进程和3个Executors进程。
那么问题来了当我们把Word Count的示例代码依次敲入到spark-shell中Driver进程和3个Executors进程之间是如何通力合作来执行分布式任务的呢
为了帮你理解这个过程,我特意画了一张图,你可以先看一下整体的执行过程:
![图片](https://static001.geekbang.org/resource/image/b0/22/b05139c82a7882a5b3b3074f3be50d22.jpg?wh=1920x952 "Word Count在spark-shell中的执行过程")
首先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的分布式部署。
## 分布式环境部署
Spark支持多种分布式部署模式如Standalone、YARN、Mesos、Kubernetes。其中Standalone是Spark内置的资源调度器而YARN、Mesos、Kubernetes是独立的第三方资源调度与服务编排框架。
由于后三者提供独立而又完备的资源调度能力对于这些框架来说Spark仅仅是其支持的众多计算引擎中的一种。Spark在这些独立框架上的分布式部署步骤较少流程比较简单我们开发者只需下载并解压Spark安装包然后适当调整Spark配置文件、以及修改环境变量就行了。
因此对于YARN、Mesos、Kubernetes这三种部署模式我们不做详细展开我把它给你留作课后作业进行探索。今天这一讲我们仅专注于Spark在Standalone模式下的分布式部署。
为了示意Standalone模式的部署过程我这边在AWS环境中创建并启动了3台EC2计算节点操作系统为Linux/CentOS。
需要指出的是Spark分布式计算环境的部署对于节点类型与操作系统本身是没有要求和限制的但是**在实际的部署中,请你尽量保持每台计算节点的操作系统是一致的,从而避免不必要的麻烦**。
接下来我就带你手把手地去完成Standalone模式的分布式部署。
Standalone在资源调度层面采用了一主多从的主从架构把计算节点的角色分为Master和Worker。其中Master有且只有一个而Worker可以有一到多个。所有Worker节点周期性地向Master汇报本节点可用资源状态Master负责汇总、变更、管理集群中的可用资源并对Spark应用程序中Driver的资源请求作出响应。
为了方便描述我们把3台EC2的hostname分别设置为node0、node1、node2并把node0选做Master节点而把node1、node2选做Worker节点。
首先为了实现3台机器之间的无缝通信我们先来在3台节点之间配置无密码的SSH环境
![图片](https://static001.geekbang.org/resource/image/f5/c0/f57e5456ff8a10d26d162ac52f20dcc0.jpg?wh=1394x617 "配置3个节点之间免密的SSH环境")
接下来我们在所有节点上准备Java环境并安装Spark其中步骤2的“sudo wget”你可以参考[这里](https://www.apache.org/dyn/closer.lua/spark/spark-3.1.2/spark-3.1.2-bin-hadoop3.2.tgz)的链接),操作命令如下表所示:
![图片](https://static001.geekbang.org/resource/image/0e/c4/0e1324ce7c3539698e70dae60609b6c4.jpg?wh=1591x1211 "在所有节点上安装Java和Spark")
在所有节点之上完成Spark的安装之后我们就可以依次启动Master和Worker节点了如下表所示
![图片](https://static001.geekbang.org/resource/image/07/fc/078e9e891dee487a61b3f4f0931f80fc.jpg?wh=1489x607 "分别启动Master和Workers")
集群启动之后我们可以使用Spark自带的小例子来验证Standalone分布式部署是否成功。首先打开Master或是Worker的命令行终端然后敲入下面这个命令
```verilog
MASTER=spark://node0:7077 $SPARK_HOME/bin/run-example org.apache.spark.examples.SparkPi
```
如果程序能够成功计算Pi值也就是3.14如下图所示那么说明咱们的Spark分布式计算集群已然就绪。你可以对照文稿里的截图验证下你的环境是否也成功了。
![图片](https://static001.geekbang.org/resource/image/99/20/990d84c917f4c1156cca663yyf2eb720.png?wh=578x42 "Spark计算Pi的执行结果")
## 重点回顾
今天这一讲,我们提到,**分布式计算的精髓在于,如何把抽象的计算流图,转化为实实在在的分布式计算任务,然后以并行计算的方式交付执行。**而要想透彻理解分布式计算你就需要掌握Spark进程模型。
进程模型的核心是Driver和Executors我们需要重点理解它们之间的协作关系。任何一个Spark应用程序的入口都是带有SparkSession的main函数而在Spark的分布式计算环境中运行这样main函数的JVM进程有且仅有一个它被称为 “Driver”。
Driver最核心的作用在于解析用户代码、构建计算流图然后将计算流图转化为分布式任务并把任务分发给集群中的Executors交付执行。接收到任务之后Executors调用内部线程池结合事先分配好的数据分片并发地执行任务代码。
对于一个完整的RDD每个Executors负责处理这个RDD的一个数据分片子集。每当任务执行完毕Executors都会及时地与Driver进行通信、汇报任务状态。Driver在获取到Executors的执行进度之后结合计算流图的任务拆解依次有序地将下一阶段的任务再次分发给Executors付诸执行直至整个计算流图执行完毕。
之后我们介绍了Spark支持的分布式部署模式主要有Standalone、YARN、Mesos、Kubernetes。其中Standalone是Spark内置的资源调度器而YARN、Mesos、Kubernetes是独立的第三方资源调度与服务编排框架。在这些部署模式中你需要重点掌握Standalone环境部署的操作步骤。
## **每课一练**
好,在这一讲的最后,我给你留两道作业,帮助你巩固今日所学。
1.与take算子类似collect算子用于收集计算结果结合Spark进程模型你能说一说相比collect算子相比take算子来说都有哪些隐患吗
2.如果你的生产环境中使用了YARN、Mesos或是Kubernetes你不妨说一说要完成Spark在这些独立框架下的分布式部署都需要哪些必备的步骤
今天这一讲就到这里了,如果你在部署过程中遇到的什么问题,欢迎你在评论区提问。如果你觉得这一讲帮助到了你,也欢迎你分享给更多的朋友和同事,我们下一讲再见。