Spark权威指南(中文版)----第15章 Spark如何在集群环境运行

到目前为止,在本书中,我们主要关注Spark作为编程接口的特性。我们已经讨论了结构化api如何接受逻辑操作,将其分解为逻辑计划,并将其转换为物理计划,该物理计划实际上由跨机器集群执行的弹性分布式数据集(RDD)操作组成。本章主要讨论Spark执行代码时会发生什么。我们以一种与实现无关的方式讨论这个问题----这既不依赖于您正在使用的集群管理器,也不依赖于您正在运行的代码。最终,所有Spark代码以相同的方式运行。本章涵盖了几个关键的主题:

  • Spark应用程序的体系结构和组件
  • Spark应用程序在Spark内外的生命周期
  • 重要的底层执行属性,如pipelining
  • 运行一个Spark应用程序需要什么。

让我们从体系结构开始。
      15.1.   Spark应用程序的体系结构

 

在第2章中,我们讨论了Spark应用程序的一些高级组件。让我们再复习一遍:

Spark Driver:

Driver程序是您的Spark应用程序的“驾驶座”中的进程。它是Spark应用程序执行的控制器,维护Spark集群的所有状态(执行器的状态和其上运行的任务)。其必须与集群管理器接口配合,以获得可用的物理资源,并运行executors。最终,这只是一个物理机器上的进程,负责维护集群上运行的应用程序的状态。

Spark executors:

Spark  executor是执行Spark  driver程序分配的任务的进程。executor有一个核心职责:接受driver程序分配的任务,运行它们,并报告它们的状态(成功或失败)和结果给driver。每个Spark应用程序都有自己独立的executor进程。

Cluster Manager集群管理器:

Spark Driver程序和Executor不会脱离Spark应用程序而存在,这就是集群管理器的作用所在。集群管理器负责维护运行Spark应用程序的集群。有点令人困惑的是,集群管理器有自己的“Driver程序”(有时称为master)和“worker”抽象。其核心区别在于,它们与物理机器相关联,而不是与进程相关联(如Spark中的进程)。图15-1显示了一个基本的集群设置。图左边的机器是集群管理器driver程序节点。圆圈表示运行在每个工作节点上并管理每个工作节点的守护进程。到目前为止还没有运行Spark应用程序—这些只是来自集群管理器的进程。

图片

当实际运行Spark应用程序时,我们从集群管理器请求资源来运行它。根据应用程序的配置方式,这可以包括一个运行Spark driver程序的资源,也可以只是Spark应用程序执行器的资源。在Spark应用程序执行过程中,集群管理器将负责管理运行应用程序的底层机器。

Spark目前支持三个集群管理器:一个简单的内置standalone集群管理器、Apache Mesos和Hadoop Yarn。但是,这个列表将继续增长,所以一定要检查您最喜欢的集群管理器的文档(译者注:目前已经支持第四个集群管理器:Kubernetes)。

现在我们已经介绍了应用程序的基本组件,让我们来看看运行应用程序时需要做的第一项选择:选择执行模式。

15.1.1.   执行模式

执行模式使您能够在运行应用程序时确定上述资源的物理位置。您有三种模式可供选择:

  • Cluster mode

  • Client mode

  • Local mode

我们将使用图15-1作为模板,详细介绍其中的每一个。在下一节中,带实边框的矩形表示Spark Driver程序进程,而带点边框的矩形表示executor进程。

Cluster mode

集群模式可能是运行Spark应用程序的最常见方式。在集群模式下,用户向集群管理器提交预编译的JAR、Python脚本或R脚本。然后,集群管理器在集群内的worker节点上启动driver程序进程,以及executor进程。这意味着集群管理器负责维护所有与Spark应用程序相关的进程。图15-2显示集群管理器将我们的driver程序放在一个worker节点上,将executor放在其他worker节点上。

图片

Client mode

client模式几乎与集群模式相同,只是Spark driver程序仍然位于提交应用程序的客户机上。这意味着客户机负责维护Spark Driver程序进程,集群管理器负责维护executor进程。在图15-3中,我们在一台集群外的机器上运行Spark应用程序。这些机器通常称为网关机器或边缘节点。在图15-3中,您可以看到Driver程序运行在集群外部的机器上,但是worker位于集群中的机器上。

图片

Local modeLocal模式与前两种模式有很大的不同:它在一台机器上运行整个Spark应用程序。它通过单台机器上的线程实现并行。这是学习Spark、测试应用程序或使用本地开发进行迭代试验的常见方法。但是,我们不建议使用本地模式来运行生产应用程序。

 

15.2.   Spark应用程序的生命周期

到目前为止,本章已经涵盖了讨论Spark应用程序所需的词汇表。现在是时候从实际Spark代码的“外部”讨论Spark应用程序的整个生命周期了。我们将通过一个使用spark-submit(在第3章中介绍)运行应用程序的示例来实现这一点。我们假设一个集群已经运行了四个节点,一个Driver程序(不是Spark Driver程序,而是集群管理器Driver程序)和三个worker节点。此时,实际的集群管理器我们并不关注:本节使用前一节中的词汇表逐步介绍Spark应用程序的生命周期,从初始化到程序退出。

提示本节还使用了插图,并遵循我们前面介绍的相同符号。此外,我们现在介绍表示网络通信的标线。较暗的箭头表示由Spark或与Spark相关的进程相关的通信,而虚线表示更一般的通信(如集群管理通信)。
15.2.1.   Client Request

第一步是提交一个实际的应用程序。这将是一个预编译的JAR或库。此时,您正在本地机器上执行代码,并将向集群管理器Driver程序节点发出请求(图15-4)。这里,我们明确地要求仅为Spark Driver程序进程提供资源。我们假设集群管理器接受这个提议,并将Driver放在集群中的一个节点上。提交原始作业的客户机进程退出,应用程序开始在集群上运行。

图片

要做到这一点,需要在终端上运行如下命令:

图片

15.2.2.   Launch运行

现在驱动程序进程已经放在集群上,它开始运行用户代码(图15-5)。这段代码必须包含一个初始化Spark集群(例如,driver + executor)的SparkSession。SparkSession随后将与集群管理器通信(深色线),要求它在集群中启动Spark executor进程(浅色线)。executor的数量及其相关配置由用户通过原始spark-submit调用中的命令行参数设置。

图片

集群管理器的响应是启动执行器进程(假设一切正常),并将有关它们位置的相关信息发送给Driver进程。在所有东西都正确地连接起来之后,我们有了一个“Spark Cluster”,正如您今天可能认为的那样。

15.2.3.   Execution执行

现在我们有了一个“Spark集群”,Spark以其愉快的方式执行代码,如图15-6所示。Driver和worker彼此通信,执行代码并移动数据。Driver将任务调度到每个worker上,每个worker用这些任务的状态和成功或失败来响应。(我们将很快介绍这些细节。)

图片

15.2.4.   Completion完成

在Spark应用程序完成后,Driver进程退出,成功或失败(图15-7)。然后集群管理器为Driver关闭Spark集群中的Executor。此时,您可以通过向集群管理器询问此信息来查看Spark应用程序的成功或失败。

图片

15.3.   Spark应用程序的生命周期(insidespark)

我们刚刚研究了用户代码之外的Spark应用程序的生命周期(基本上是支持Spark的基础设施),但是更重要的是讨论在运行应用程序时在Spark中发生了什么。这是“用户代码”(定义Spark应用程序的实际代码)。每个应用程序由一个或多个Spark作业组成。应用程序中的Spark作业是串行执行的(除非使用线程并行启动多个操作)。

15.3.1.   SparkSession

任何Spark应用程序的第一步都是创建一个SparkSession。在许多交互模式中,默认会为你创建这样一个对象,但在应用程序中,您必须手动完成对象的创建。您的一些遗留代码可能使用new SparkContext模式。应该避免这种情况,采用SparkSession上的builder方法,它更健壮地实例化Spark和SQL上下文,并确保没有上下文冲突,因为可能有多个库试图在同一个Spark应用程序中创建会话:

图片

在您拥有一个SparkSession之后,您应该能够运行您的Spark代码。从SparkSession中,您还可以相应地访问所有底层和遗留上下文和配置。注意,SparkSession类只是在Spark 2.X中添加的。您可能发现的旧代码将直接为结构化api创建SparkContext和SQLContext。

SparkContext

SparkSession中的SparkContext对象表示到Spark集群的连接。这个类是您与Spark的一些低层api(如RDDs)通信的方式。在旧的示例和文档中,它通常存储为变量sc。通过SparkContext,您可以创建RDDs、累加器和广播变量,并且可以在集群上运行代码。

在大多数情况下,不需要显式地初始化SparkContext;您应该能够通过SparkSession访问它。如果你想,你应该用最通用的方法创建它,通过getOrCreate方法:

图片

SparkSession, SQLContext, 和 HiveContextSpark的早期版本中,SQLContext和HiveContext提供了处理DataFrames和Spark SQL的能力,并且通常在示例、文档和遗留代码中作为变量sqlContext存储。作为一个历史点,Spark 1.X实际上有两个上下文:SparkContext和SQLContext。这两个上下文的表现各不相同。前者侧重于对Spark的核心抽象进行更细粒度的控制,而后者侧重于像Spark SQL这样的高级工具。Spark2.0中,开源社区将这两个api合并到我们今天使用的集中式SparkSession中。但是,这两个api仍然存在,您可以通过SparkSession访问它们。需要注意的是,您不应该使用SQLContext,也很少需要使用SparkContext。

初始化SparkSession之后,就可以执行一些代码了。正如我们在前几章中所知道的,所有Spark代码都编译为RDDs。因此,在下一节中,我们将使用一些逻辑说明(DataFrame作业),并逐步了解随着时间的推移会发生什么。

15.3.2.   Logical Instructions逻辑指令

正如您在本书的开头所看到的,Spark代码本质上由Transformation和action组成。如何构建这些取决于您——无论是通过SQL、低层RDD操作,还是机器学习算法。理解我们如何使用像DataFrames这样的声明性指令,并将它们转换为物理执行计划,这是理解Spark如何在集群上运行的一个重要步骤。在本节中,请确保在一个新的环境(一个新的Spark shell)中运行此操作,以跟踪job、stage和task编号。

逻辑指令到物理执行

我们在第2部分中提到了这一点,但是值得重申一遍,以便更好地理解Spark如何获取代码并在集群上实际运行命令。我们将逐行介绍更多的代码,解释幕后发生的事情,以便您能够更好地理解Spark应用程序。在后面的章节中,当我们讨论监控,我们将通过Spark UI对Spark作业执行更详细的跟踪。在当前的示例中,我们将采用更简单的方法。我们将做一个包含三个步骤的job:使用一个简单的DataFrame,我们将对它进行重新分区,执行一个值对一个值的操作,然后聚合一些值并收集最终结果。

提示这段代码是用Python Spark 2.2编写并运行的(您将在Scala中得到相同的结果,因此我们省略了它)。作业的数量不太可能发生巨大的变化,但是Spark的底层优化可能会得到改进,从而改变物理执行策略。

图片

当运行这段代码时,我们可以看到您的操作触发了一个完整的Spark作业。让我们来看看explain计划,以巩固我们对物理执行计划的理解。我们可以在Spark UI中的SQL选项卡(在实际运行查询之后)访问这些信息,以及:

图片

当您调用collect(或任何action)时,您所拥有的是Spark作业的执行,该作业由各个stages和tasks组成。如果您正在本地机器上运行此命令,请转到localhost:4040查看Spark UI。我们将跟随“jobs”选项卡,随着我们进一步深入细节,最终跳转到各个stages和tasks。

15.3.3.   A Spark Job

一般来说,一个action操作出发一个spark job。Action操作总是返回一个结果值。每个job可以包含多个stages,stages的个数多少,有shuffle操作的transformation决定。

这项job分为以下几个stages和tasks:

图片

我希望您至少对我们如何得到这些数字有些困惑,以便我们能够花时间更好地理解发生了什么!

15.3.4.   Stages

Spark中的stages表示可以一起执行的任务组,以便在多台机器上计算相同的操作。一般来说,Spark会尝试打包尽可能多的工作(如:尽可能多的transformation操作)到同一个stage,但是Spark引擎在shuffle操作之后产生新的stage。shuffle表示数据的物理重新分区——例如,对DataFrame进行排序,或按key对从文件中加载的数据进行分组(这需要将具有相同key的记录发送到相同节点)。这种类型的重新分区需要跨Executor协调来移动数据。Spark在每次shuffle之后启动一个新stage,并跟踪各个stage必须以什么顺序运行才能计算最终结果。

在我们前面看到的job中,前两个stages对应于创建DataFrames所执行的rang()方法。默认情况下,当您使用rang()创建一个DataFrame时,它有八个分区。下一步是重新分区。这通过shuffle数据来改变分区的数量。这些DataFrames被划分为6个分区和5个分区,对应于stage3和stages4中的task数量。

stage3和stage4对每个DataFrame执行操作,stage的末尾表示join(shuffle)。突然,我们有200个任务。这是因为spark.sql.shuffle.partitions配置的默认值为200,这意味着在执行过程中执行shuffle时,默认情况下输出200个洗牌分区。您可以更改此值,并且输出分区的数量也将发生更改。

提示我们将在第19章更详细地讨论分区的数量,因为它是一个非常重要的参数。应该根据集群中的内核数量设置此值,以确保高效执行。下面是设置方法:spark.conf.set("spark.sql.shuffle.partitions", 50)

一个好的经验法则是分区的数量应该大于集群中executor的数量,这可能是由多个因素造成的,具体取决于工作负载。如果您在本地机器上运行代码,您应该将该值设置得更低,因为本地机器不太可能并行执行那么多任务。对于一个集群,这更像是一个默认值,其中可能有更多的executors cores可供使用。无论分区的数量如何,整个stage都是并行计算的。最终的结果将单独地聚合这些分区,在最终将最终结果发送到Driver之前,将它们都放到一个单独的分区中。在本书的这一部分中,我们将多次看到这种配置。

15.3.5.  Tasks

Spark中的stage由task组成。每个task对应于将在单个executor上运行的数据块和一组transformation的组合。如果数据集中有一个大分区,我们将有一个任务。如果有1000个小分区,那么就有1000个任务可以并行执行。task只是应用于数据单元(分区)的计算单元。将数据划分为更多的分区意味着可以并行执行更多的分区。这不是万灵药,但它是一个开始优化的简单地方。

15.4.   执行细节

在我们结束本章之前,Spark中的task和stage有一些重要的特性值得回顾。首先,Spark自动将可以一起完成的task和stage串联起来,比如一个map操作后面跟着另一个map操作。其次,对于所有的shuffle操作,Spark将数据写入稳定的存储(例如磁盘),并可以在多个作业之间重用数据。我们将依次讨论这些概念,因为它们将在您开始通过Spark UI检查应用程序时出现。

15.4.1.   Pipelining

Spark之所以成为“内存计算框架”的一个重要原因是,与之前的框架(例如MapReduce)不同,Spark在将数据写入内存或磁盘之前,会在某个时间点执行尽可能多的步骤。Spark执行的一个关键优化是pipelining,它发生在RDD级别或以下。使用pipelining,任何直接相互提供数据的操作序列(不需要在节点之间移动数据)都被分解为一个单独的任务阶段,这些任务一起完成所有的操作。例如,如果您编写一个RDD-based程序,包括一个map,一个filter,然后另一个map,这些任务将存在于一个stage中,读取每个输入数据记录,然后传输数据到第一个map,然后数据通过filter,如果需要通过最后一个map函数。这种流水线版本的计算比在每一步之后将中间结果写入内存或磁盘要快得多。对于执行select、filter和select的DataFrame或SQL计算,也会发生类似的pipelining操作。

从实用的角度来看,pipelining对用户是透明的,你写一个应用,运行时将自动运行pipelining,但是你会发现如果你检查您的应用程序通过SparkUI或通过其日志文件,你会看到多个RDD或DataFrame操作被安排成一个单一的Stage。

15.4.2.  ShufflePersistence

有时会看到的第二个属性是shuffle持久性。当Spark需要运行一个必须跨节点移动数据的操作时,例如reduce-by-key操作(每个key的输入数据首先需要从多个节点收集到一起),此时Spark引擎就不能再执行流水线,而是执行跨网络shuffle。Spark总是通过在执行阶段首先让“源”任务(发送数据的任务)将shuffle文件写入本地磁盘来执行shuffle。然后,进行分组和合并数据的stage启动并运行任务,这些任务从每个shuffle文件中获取相应的记录并执行计算(例如,获取和处理特定范围的key的数据)。将shuffle文件保存到磁盘中,可以让Spark在比源阶段更晚的时间运行这个阶段(例如,如果没有足够的执行者同时运行这两个阶段),还可以让引擎在失败时重新启动reduce任务,而无需重新运行所有输入任务。

对于shuffle持久性,您将看到的一个副作用是,在已经shuffle的数据上运行一个新作业不会重新运行shuffle的“源”端。因为shuffle文件已经在更早的时候被写到磁盘上,Spark知道它可以使用它们来运行任务的后期,并且它不需要重做早期的那些。在Spark UI和日志中,您将看到预shuffle阶段标记为“跳过”。这种自动优化可以在对相同数据运行多个作业的工作负载中节省时间,但是当然,为了获得更好的性能,您可以使用DataFrame或RDD缓存方法执行自己的缓存,该方法允许您精确地控制保存哪些数据以及保存在何处。在对聚合数据运行一些Spark操作并在UI中检查它们之后,您将很快习惯这种行为。

15.5.  结束语

在本章中,我们讨论了在集群上执行应用程序时,会发生什么情况。这意味着集群将如何实际运行代码,以及在此过程中在Spark应用程序中发生了什么。此时,您应该很容易理解Spark应用程序内外发生了什么。这将为调试应用程序提供一个起点。第16章将讨论编写Spark应用程序以及在编写时应该考虑的事项。

posted @ 2021-08-19 16:16  bluesky1  阅读(191)  评论(0编辑  收藏  举报