Spark权威指南(中文版)----第16章 开发Spark应用程序
在第15章中,您了解了Spark如何在集群上运行代码。现在,我们将向您展示开发一个独立的Spark应用程序并将其部署到集群上是多么容易。我们将使用一个简单的模板来实现这一点,该模板分享了一些关于如何构建应用程序的简单技巧,包括设置构建工具和单元测试。这个模板可以在本书的代码存储库中找到。这个模板实际上并不是必需的,因为从头编写应用程序并不困难,但是它很有帮助。让我们从第一个应用程序开始。
16.1. 编写Spark应用程序
Spark应用程序是两种东西的组合:Spark集群和代码。在这种情况下,集群将是本地模式,应用程序将是预定义的模式。让我们浏览一下每种语言中的应用程序。
16.1.1. 一个简单的基于scala的应用程序
Scala是Spark的“原生”语言,自然是编写应用程序的好方法。这和编写Scala应用程序没什么不同。
提示
Scala看起来有些吓人,这取决于您的背景,但如果只是为了更好地理解Spark,那么还是值得学习的。此外,你不需要学习这门语言的所有细节;从基础开始,您会发现在Scala中很快就可以提高生产力。使用Scala还将打开许多大门。通过一点实践,通过Spark的代码库进行代码级跟踪并不困难。
您可以使用两个基于Java虚拟机(JVM)的构建工具sbt或Apache Maven构建应用程序。与任何构建工具一样,它们都有各自的怪癖,但是从sbt开始可能是最简单的。您可以在sbt网站上下载、安装和了解sbt。您也可以从Maven各自的网站上安装Maven。
要为Scala应用程序配置sbt build,我们需要指定一个build.sbt文件,用于管理包信息。在build.sbt文件中,有几个关键的地方:
-
项目元数据(包名称、包版本控制信息等)
-
在哪里解决依赖关系
-
库所需的依赖项
您可以指定更多的选项;但是,它们超出了本书的范围(您可以在web和sbt文档中找到相关信息)。也有一些关于这个主题的书籍,可以作为一个有用的参考。下面是Scala build.sbt文件的示例(以及我们在模板中包含的文件)。注意,我们必须指定Scala版本和Spark版本:
现在我们已经定义了构建文件,实际上可以开始向项目添加代码了。我们将使用标准的Scala项目结构,你可以在sbt参考手册中找到(这是与Maven项目相同的目录结构):
我们将源代码放在Scala和Java目录中。在本例中,我们将如下内容放入文件中;这将初始化SparkSession,运行应用程序,然后退出:
注意,我们定义了一个包括main方法的类,当使用spark-submit将其提交到集群执行时,可以从命令行运行这个类。
现在我们已经设置好了我们的项目并向其添加了一些代码,是时候构建它了。我们可以使用sbt assemble构建一个“超级JAR”或“胖JAR”,其中包含一个JAR中的所有依赖项。对于某些部署,这可能很简单,但对于其他部署,这可能会导致复杂性(尤其是依赖冲突)。一个轻量级的方法是运行sbt package,它将把所有依赖项收集到目标文件夹中,但不会将所有依赖项打包到一个大JAR中。
运行程序
目标文件夹包含我们可以用作spark-submit参数的JAR。在构建Scala包之后,您可以使用以下代码在本地机器上进行spark-submit(此代码片段利用别名创建$SPARK_HOME变量;你可以将$SPARK_HOME替换为包含你下载的Spark版本的目录):
16.1.2. 编写Python应用程序
编写PySpark应用程序实际上与编写普通的Python应用程序或包没有什么不同。它特别类似于编写命令行应用程序。Spark没有构建概念,只有Python脚本,因此要运行应用程序,只需对集群执行脚本。
为了促进代码重用,通常将多个Python文件打包到Spark代码的egg或ZIP文件中。要包含这些文件,您可以使用spark-submit的——py-files参数来添加.py、.zip或.egg文件,以便与应用程序一起分发。
当运行代码时,用Python创建一个等价于“Scala/Java main类”的类。将某个脚本指定为构建SparkSession的可执行脚本。这是我们传递给spark-submit的主要参数:
当您这样做时,您将获得一个SparkSession,您可以将它传递给您的应用程序。最佳实践是在运行时传递这个变量,而不是在每个Python类中实例化它。
在Python中开发时,一个有用的技巧是使用pip将PySpark指定为依赖项。您可以通过运行命令pip install pyspark来实现这一点。这允许您以可能使用其他Python包的方式使用它。这也使得许多编辑器中的代码完成非常有用。这是Spark 2.2中全新的一个版本,因此可能需要一两个版本才能完全投入生产,但是Python在Spark社区中非常流行,它肯定是Spark未来的基石。
运行程序
编写完代码之后,就可以提交代码执行了。(我们正在执行与项目模板中相同的代码。)您只需要调用spark-submit与该信息:
16.1.3. 编写Java应用程序
编写Java Spark应用程序与编写Scala应用程序是一样的。核心差异涉及到如何指定依赖项。
本例假设您正在使用Maven指定依赖项。在本例中,您将使用以下格式。在Maven中,您必须添加Spark Packages存储库,以便从这些位置获取依赖项:
当然,您遵循与Scala项目版本相同的目录结构(因为它们都符合Maven规范)。然后,我们只需遵循相关的Java示例来实际构建和执行代码。现在,我们可以创建一个简单的例子,指定一个main类,让我们执行(更多关于这个在本章末尾):
然后,我们使用mvn package对它进行打包(需要安装Maven)。
运行程序
这个操作将与运行Scala应用程序(或者Python应用程序)完全相同。简单地使用spark-submit:
16.2. 测试Spark应用程序
现在您已经知道编写和运行一个Spark应用程序需要什么,所以让我们转到一个不那么令人兴奋但仍然非常重要的主题:测试。测试Spark应用程序依赖于几个关键原则和策略,在编写应用程序时应该牢记这些原则和策略。
16.2.1. 策略原则
测试数据pipelines和Spark应用程序与实际编写它们一样重要。这是因为您希望确保它们对未来的数据、逻辑和输出方面的更改具有弹性。在本节中,我们将首先讨论您可能希望在典型的Spark应用程序中测试什么,然后讨论如何组织代码以便进行简单的测试。
输入数据的弹性
对不同类型的输入数据保持弹性对于如何编写数据管道非常重要。数据将会改变,因为业务需求将会改变。因此,您的Spark应用程序和管道应该至少对输入数据中的某种程度的更改具有弹性,或者确保以一种优雅而有弹性的方式处理这些故障。在大多数情况下,这意味着要聪明地编写测试来处理不同输入的边缘情况。
业务逻辑弹性和演化
管道中的业务逻辑和输入数据都可能发生更改。更重要的是,您希望确保从原始数据中推导出来的是您实际认为自己在推导的东西。这意味着您将需要对实际数据进行健壮的逻辑测试,以确保您实际上得到了您想要的结果。这里需要注意的一件事是,尝试编写一组“Spark单元测试”,只测试Spark的功能。你可能不想这样做;相反,您希望测试您的业务逻辑,并确保您设置的复杂业务管道实际上正在做您认为它应该做的事情。
输出的弹性和原子性
假设您已经为输入数据结构中的更改做好了准备,并且您的业务逻辑经过了良好的测试,现在您需要确保您的输出结构是您所期望的。这意味着您需要优雅地处理输出模式解析。通常情况下,数据不会被简单地转储到某个位置,再也不会被读取—您的大多数Spark管道可能正在为其他Spark管道提供数据。因为这个原因你要确保下游消费者理解的“状态”,可能意味着它更新的频率,以及数据是否“完整的”(例如,没有后期数据),或者不会有任何最后一分钟修正数据。
前面提到的所有问题都是构建数据管道时应该考虑的原则(实际上,无论是否使用Spark)。这种战略思维对于为您想要构建的系统打下基础非常重要。
16.2.2. 战术
虽然战略思维很重要,但是让我们更详细地讨论一些可以使应用程序易于测试的策略。最高价值的方法是通过使用适当的单元测试来验证您的业务逻辑是正确的,并确保您对不断变化的输入数据具有弹性,或者已经对其进行了结构化,以便将来模式演化不会失去作用。如何做到这一点,很大程度上取决于作为开发人员的您,因为这将根据您的业务领域和领域专长而有所不同。
管理SparkSessions
使用单元测试框架(如JUnit或ScalaTest)测试Spark代码相对容易,因为Spark具有本地模式——只需创建一个本地模式SparkSession作为测试工具的一部分来运行它。然而,要使此工作正常,您应该在管理代码中的Spark时尽可能多地执行依赖项注入。也就是说,只初始化SparkSession一次,并在运行时将其传递给相关的函数和类,以便在测试期间方便地进行替换。这使得在单元测试中使用一个虚拟的SparkSession测试每个单独的函数更加容易。
使用哪个Spark API ?
Spark提供了多种api的选择,从SQL到DataFrames和Datasets,每一种api都可能对应用程序的可维护性和可测试性产生不同的影响。坦白地说,正确的API取决于您的团队及其需求:一些团队和项目将需要不那么严格的SQL和DataFrame API来提高开发速度,而其他团队则希望使用类型安全的数据集或RDDs。
通常,我们建议对每个函数的输入和输出类型进行文档化和测试,而不管使用哪种API。类型安全API自动为您的函数强制执行一个最小的契约,这使得其他代码很容易在此基础上进行构建。如果您的团队更喜欢使用DataFrames或SQL,那么请花一些时间记录和测试每个函数返回什么,以及它接受什么类型的输入,以避免以后出现意外,就像在任何动态类型的编程语言中一样。虽然较低层的RDD API也是静态类型的,但我们建议只在需要数据集中不存在的底层特性(比如分区)时才使用它,这应该不是很常见;Dataset API允许更多的性能优化,并且将来可能提供更多的性能优化。
对于应用程序使用哪种编程语言也有类似的考虑:对于每个团队当然没有正确的答案,但是根据您的需要,每种语言将提供不同的好处。我们一般建议使用静态类型语言,像Scala和Java为更大的应用程序或者那些你希望能够进入低级代码完全控制性能,但Python和R可能是更好的在其他情况下,示例中,如果您需要使用一些其他的库。Spark代码应该很容易在每种语言的标准单元测试框架中进行测试。
连接到单元测试框架
要对代码进行单元测试,我们建议使用语言中的标准框架(例如JUnit或ScalaTest),并设置测试工具来为每个测试创建和清理SparkSession。不同的框架提供了不同的机制来实现这一点,例如“before”和“after”方法。我们在本章的应用程序模板中包含了一些单元测试代码示例。
连接到数据源
尽可能地,您应该确保您的测试代码不连接到生产数据源,这样,如果这些数据源发生更改,开发人员就可以轻松地单独运行它。实现这一点的一个简单方法是让所有业务逻辑函数都以DataFrames或数据集作为输入,而不是直接连接到各个源;毕竟,无论数据源是什么,后续代码都将以相同的方式工作。如果您在Spark中使用结构化api,实现这一点的另一种方法是命名表:您可以简单地注册一些虚拟数据集(例如,从小文本文件或内存对象加载)作为各种表名,然后从那里开始。
16.3. 开发过程
使用Spark应用程序的开发过程类似于您可能已经使用过的开发工作流。首先,您可能要维护一个划痕空间,比如交互式笔记本或其他类似的东西,然后在构建关键组件和算法时,将它们移动到更持久的位置,比如库或包。笔记本体验是我们经常推荐的体验之一(我们也经常用它来写这本书),因为它在实验中很简单。还有一些工具,比如Databricks,允许您将笔记本作为生产应用程序运行。
在本地机器上运行时,spark-shell及其各种特定于语言的实现可能是开发应用程序的最佳方法。在大多数情况下,shell用于交互式应用程序,而Spark -submit用于Spark集群上的生产应用程序。您可以使用shell以交互方式运行Spark,就像我们在本书开头介绍的那样。这是运行PySpark、Spark SQL和SparkR的模式。在bin文件夹中,当您下载Spark时,您将找到启动这些shell的各种方法。只需运行spark-shell(对于Scala)、spark-sql、pyspark和sparkR。
在您完成应用程序并创建要运行的包或脚本之后,spark-submit将成为您向集群提交此作业的最好朋友。
16.4. 运行程序
运行Spark应用程序的最常见方法是通过Spark -submit。在本章前面,我们向您展示了如何运行spark-submit;您只需指定选项、应用程序JAR或脚本以及相关参数:
在使用Spark -submit提交Spark作业时,始终可以指定是在客户机模式还是在集群模式下运行。但是,您应该几乎总是倾向于在集群模式下运行(或者在集群本身的客户机模式下),以减少执行者和驱动程序之间的延迟。
提交applciations时,在.jar中传递一个.py文件,并将Python .zip、.egg或.py添加到搜索路径中,其中包含—py文件。
为了便于参考,表16-1列出了所有可用的spark-submit选项,包括一些集群管理器特有的选项。要自己列举所有这些选项,请运行spark-submit with——help。
还有一些特定于部署的配置(参见表16-2)。
16.4.1. 应用程序运行的例子
在本章之前,我们已经介绍了一些本地模式的应用程序示例,但是值得一看的是我们如何使用前面提到的一些选项。Spark还在下载Spark时包含的examples目录中包含几个示例和演示应用程序。如果你纠结于如何使用某些参数,你可以先在本地机器上试试,然后使用SparkPi类作为主类:
下面的代码片段对Python也做了同样的操作。您可以从Spark目录运行它,这将允许您向独立集群管理器提交一个Python应用程序(都在一个脚本中)。您还可以设置与前一个示例相同的执行器限制:
16.5. 配置应用程序
Spark包含许多不同的配置,其中一些我们在第15章中已经介绍过。根据您希望实现的目标,有许多不同的配置。本节将详细介绍这些内容。大多数情况下,这些信息都是供参考的,可能只值得略读,除非您正在寻找特定的内容。大多数配置可分为以下几类:
Spark提供了三个位置来配置
-
Spark属性控制大多数应用程序参数,可以通过使用SparkConf对象来设置
-
Java系统属性
-
硬编码的配置文件
您可以使用几个模板,您可以在Spark home文件夹的根目录中找到/conf目录。您可以将这些属性设置为应用程序中的硬编码变量,或者在运行时指定它们。您可以使用环境变量在每个节点上通过conf/spark-env.sh脚本设置每台机器的设置,例如IP地址。最后,您可以通过log4j.properties配置日志记录。
16.5.1. SparkConf
SparkConf管理所有应用程序配置。您可以通过import语句创建一个,如下面的示例所示。创建之后,SparkConf对于特定的Spark应用程序是不可变的:
您可以使用SparkConf配置具有Spark属性的单个Spark应用程序。这些Spark属性控制Spark应用程序的运行方式和集群的配置方式。下面的示例将本地集群配置为有两个线程,并指定在Spark UI中显示的应用程序名称。
您可以在运行时配置它们,正如您在本章前面通过命令行参数看到的那样。这有助于启动一个Spark Shell,它将自动为您包含一个基本的Spark应用程序;例如:
值得注意的是,在设置基于时间期限的属性时,应该使用以下格式:
16.5.2. 应用程序属性
应用程序属性是您从Spark -submit或创建Spark应用程序时设置的属性。它们定义了基本的应用程序元数据以及一些执行特性。表16-3给出了当前应用程序属性的列表。
通过应用程序web UI(Driver程序端口4040)的”Environment”选项卡,查看所有的配置信息,可以确保正确设置了这些值。只有通过spark-defaults、SparkConf或命令行显式设置的值才会出现在选项卡中。对于所有其他配置属性,可以假设使用默认值。
16.5.3. 运行环境属性配置
虽然不太常见,但有时可能还需要配置应用程序的运行时环境。由于空间限制,我们不能在这里包含整个配置集。请参考Spark文档中有关运行时环境的相关配置表(http://spark.apache.org/docs/latest/configuration.html#runtime-environment)。这些属性允许您为Driver程序和Executor程序配置额外的类路径和python路径、python相关员配置以及其他日志记录配置属性。
16.5.4. 执行时属性配置
这些配置是您需要配置的最相关的配置之一,因为它们为您提供了对程序实际执行的更细粒度控制。由于空间限制,我们不能在这里包含整个配置集。请参考Spark文档中有关执行行为的相关配置表(http://spark.apache.org/docs/latest/configuration.html#execution-behavior)。最常见的更改配置是spark.executor.cores(控制可用cores的数量)和spark.files.maxPartitionBytes(读取文件时的最大分区大小)。
16.5.5. 配置内存管理
有时您可能需要手动管理内存选项来尝试优化应用程序。其中许多配置项与最终用户无关,因为它们涉及许多遗留概念或在Spark 2中因自动内存管理而被排除的细粒度控制配置项。由于空间限制,我们不能在这里包含整个配置集。请参阅Spark文档中有关内存管理的相关配置表(http://spark.apache.org/docs/latest/configuration.html#memory-management)。
16.5.6. 配置shuffle行为
我们已经强调过,由于Spark作业的通信开销很高,所以shuffle可能会成为瓶颈。因此,有许多低层配置用于控制shuffle行为。由于空间限制,我们不能在这里包含整个配置集。请参考Spark文档中有关Shuffle行为的相关配置表(http://spark.apache.org/docs/latest/configuration.html#shuffle-behavior)。
16.5.7. 环境变量
您可以通过环境变量配置某些Spark设置,这些环境变量来自安装Spark的目录中的conf/ Spark-env.sh脚本。在Standalone模式和Mesos模式中,该文件可以提供特定于机器的信息,比如主机名。当运行本地Spark应用程序或提交脚本时,它也会被引用。
注意,默认情况下,安装Spark时不存在conf/ Spark-env.sh文件,可以通过拷贝conf/spark-env.sh.template并重命名获得。
以下变量可以在spark-env.sh中设置:
JAVA-HOME安装Java的位置(如果它不在默认路径上)。PYSPARK_PYTHONPython二进制可执行文件,用于Driver程序和Worker程序中的PySpark(如果可用,默认为python2.7;否则,python)。如果spark.pyspark.python属性设置了,则spark.pyspark.python属性优先级高于此环境变量。PYSPARK_DRIVER_PYTHONPython二进制可执行文件,仅用于Driver程序中的PySpark(默认为PYSPARK_PYTHON)。属性spark.pyspark.driver.python如果设置了,则它优先。SPARKR_DRIVER_R用于SparkR shell的二进制可执行文件(默认为R)。属性spark.r.shell.command在设置时优先。SPARK_LOCAL_IP要绑定到的机器IP地址。SPARK_PUBLIC_DNSSpark应用程序将要通知的其他机器的主机名。除了列出的变量之外,还有设置Spark独立集群脚本的选项,比如在每台机器上使用的内核数量和最大内存。因为spark-env.sh是一个shell脚本,所以可以通过编程设置其中一些;例如,您可以通过查找特定网络接口的IP来计算SPARK_LOCAL_IP。
注意
在cluster模式下对Yarn运行Spark时,需要使用Spark .YARN.appmasterenv设置环境变量。在conf/spark-default .conf文件中设置[EnvironmentVariableName]属性。在spark-env.sh中设置的环境变量不会在cluster模式下反映在Yarn Application Master中。有关更多信息,请参见Yarn相关的Spark属性。
16.5.8. 应用程序中的Job调度
在给定的Spark应用程序中,如果从单独的线程提交多个并行作业,则可以同时运行它们。在本节中,我们所说的job指的是一个Spark action和任何需要运行以计算该action的任务。Spark的调度程序是完全线程安全的,并且支持此用例来启用服务于多个请求的应用程序(例如,针对多个用户的查询)。
默认情况下,Spark的调度程序以FIFO方式运行作业。如果队列头部的作业不需要使用整个集群,则稍后的作业可以立即开始运行,但是如果队列头部的作业很大,则稍后的作业可能会显著延迟。
还可以配置作业之间的公平共享。在公平共享下,Spark以循环方式在作业之间分配任务,以便所有作业获得大致相等的集群资源共享。这意味着在长作业运行时提交的短作业可以立即开始接收资源,并且仍然可以在不等待长作业完成的情况下获得良好的响应时间。这种模式最适合多用户设置。
要启用公平调度程序,设置spark.scheduler.mode为Fair,可以在SparkContext中设置。
Fair调度程序还支持将作业分组到池中,并为每个池设置不同的调度选项或权重。这可以为更重要的作业创建高优先级池,或者将每个用户的作业分组在一起,并给用户平等的共享,而不管他们有多少并发作业,而不是给作业平等的共享。该方法模仿Hadoop Fair调度程序。
在不进行任何干预的情况下,新提交的作业将进入默认池,可以通过设置spark.scheduler.pool属性来设置作业池。这是这样做的(假设sc是您的SparkContext:
sc.setLocalProperty("spark.scheduler.pool","pool1")
设置此LocalProperty后,此线程中提交的所有作业都将使用此池名称。设置为每个线程,以便让一个线程可以方便地代表同一个用户运行多个作业。如果希望清除线程关联的池,请将其设置为null。
16.6. 结束语
本章涵盖了很多关于Spark应用程序的内容;我们学习了如何用Spark的所有语言编写、测试、运行和配置它们。在第17章中,我们将讨论在运行Spark应用程序时的部署和集群管理选项。