在前面的章节中,你发现了什么是Apache Spark,以及如何构建简单的应用程序,并且,希望你理解了关键的概念,比如数据框架和惰性加载。本章与前一章有联系:你在第5章中构建了一个应用程序,并将在本章中部署它。在本章之前阅读第5章并不是必须的,但强烈建议。
在这一章中,你将抛开代码本身,在走向部署和生产的过程中发现如何与Spark交互。你可能会问:"为什么我们在书中这么早就讨论部署?部署都是在最后,不是吗?"
20多年前,当我使用Visual Basic 3 (VB3)构建应用程序时,在项目即将结束时,我会运行Visual Basic设置向导,帮助构建3.5英寸软盘。在那些日子里,我的圣经是25章的《Microsoft Visual Basic 3.0程序员指南》,而部署在第25章中有所涉及。
快进到今天,你可能正在或即将运行DevOps,你(可能)已经听说过诸如持续集成和持续交付(CICD)这样的术语,而且部署现在比过去更早地发生在流程中。在我最后的一个项目中,团队使用Spark实现了一个数据管道的原型;CICD是实现原型的完整部分。部署很重要。了解部署的约束条件是关键,我鼓励你在项目中尽可能早地了解。
持续集成和持续部署
CICD,即CI/CD,指的是持续集成和持续交付的综合实践。
持续集成(CI)是指以频繁的时间间隔将所有开发人员的工作副本合并到主线代码的做法。可以是一天几次。Grady Booch(UML的创始人之一,IBMer,图灵讲师等)在1991年的软件工程方法中提出了CI。极限编程(XP)采用了CI的概念,并提倡每天集成一次以上。
CI的主要目标是防止集成问题的发生。CI旨在与由于测试驱动开发(TDD)而编写的自动单元测试相结合使用。最初,这被认为是在提交到主线代码之前,在开发人员的本地环境中运行并通过所有单元测试。这有助于防止一个开发者的在建工作破坏另一个开发者的副本。最近对这一概念的阐述引入了构建服务器,它定期甚至在每次提交后自动运行单元测试,并将结果报告给开发人员。
持续交付(CD)允许团队在较短的周期内生产软件,确保软件可以在任何时间可靠地发布。它的目的是以更快的速度和频率来构建、测试和发布软件。该方法通过允许对生产中的应用程序进行更多的增量更新,有助于降低交付变更的成本、时间和风险。直观且可重复的部署过程对于持续交付来说非常重要。
持续交付有时会与持续部署相混淆。在持续部署中,任何通过一系列测试的生产变更都会自动部署到生产中。相反,在持续交付下,软件需要在任何时候可靠地发布,但由人决定何时发布,通常基于业务原因。
这两个定义都是改编自维基百科。
因此,与1994年用VB3工作的我不同,你是在第5章开始探索部署的。不过,不要担心:传统是要被尊重的,因为高级部署(包括管理集群、资源、共享文件等)仍然在第18章。
你将首先看一个例子,在这个例子中,数据是在Spark内部和由Spark生成的,避免了摄取数据的需要。在集群中摄取数据比创建一个自生成的数据集要复杂一些。
然后,您将了解与Spark交互的三种方式。
本地模式,通过前几章的例子你已经很熟悉了。
集群模式(多台计算机或节点)
互动模式(通过shell)
你将为你的实验室设置环境。你将了解在游戏中,当你在几个节点上分割计算资源时,会有哪些限制条件。现在收集这些经验是很重要的,这样当你计划部署你的应用程序时,你就会有更好的意识。最后,你将在一个集群上运行你的应用程序。
LAB 本章的示例与第5章相连,因此它们共享同一个仓库。它们可以在GitHub上获得,网址是:https://github.com/jgperrin/net.jgp.books.spark.ch05。
组件的作用
在上一章中,你读到了如何通过使用Spark与类和lambda函数来计算π的近似值。你运行了应用程序,但你并没有真正看清你的基础设施中发生了什么,也没有考虑架构中每个元素的作用。
组件是一种逻辑表示,它封装了一组相关的功能。组件在物理上可以是一个包,一个Web服务,或者更多。识别组件的一大好处是,你可以更容易地识别它的接口,也就是与它通信的方式。
在本章的介绍中,你读到了与Spark交互的三种方式。然而,无论你是以本地、集群还是交互模式运行Spark,Spark都会使用一组组件。
每个组件都有独特的作用。了解每个组件的作用很重要,这样你就可以更容易地调试或优化你的进程。您将首先对这些组件及其相互作用进行快速概述,然后将深入了解更多细节。
组件及其相互作用的快速概述
本小节为您提供Spark架构中每个组件的高层次概述,包括它们之间的联系。你将根据第5章中近似π的示例应用来遵循流程,图6.1将各组件放在架构图中。
从应用程序的角度来看,您将建立的唯一连接是通过在Master/集群管理器中创建会话,图6.1中的链接1。表6.1描述了这些链接。"注释 "一栏解释了为什么你应该关心Spark架构中的这个特定项目:当涉及到安全、调试或部署Spark时,这将是有用的。
清单6.1是你在第5章中学习的应用程序,在这里你计算π。 在本章中,我不会解释这个应用程序做了什么,而是解释这个应用程序使用/触发了哪些组件。这段代码将运行在你的驱动节点上,但它将控制和生成其他节点上的活动。数字链接列表6.1到图6.1。
LAB 这是第5章的第200号实验室。它可以在GitHub上找到 https://github.com/jgperrin/net.jgp.books.spark.ch05。
Spark应用程序在集群上作为独立的进程运行。你的应用程序中的SparkSession对象(也称为驱动程序)协调这些进程。无论你是在本地模式还是有10000个节点,你的应用都有一个唯一的SparkSession。SparkSession是在你构建你的会话时创建的,如下图所示。
作为你的会话的一部分,你还会得到一个上下文。SparkContext. 在v2之前,上下文是你与Spark打交道的唯一方式。你大多数情况下不需要与SparkContext进行交互,但当你需要时(访问基础设施信息,创建累加器等,在第17章中描述),你就是这样访问它的。
基本上,集群管理器在各个应用程序之间分配资源。然而,为了在集群上运行,SparkSession可以连接到几种类型的集群管理器。这可能是由你的基础设施、企业架构师或懂行的人决定。你在这里可能没有选择。第18章讨论了更多的集群管理器选项,包括YARN、Mesos和Kubernetes。
一旦连接,Spark就会在集群中的节点上获取执行器,这些执行器是JVM进程,为你的应用运行计算和存储数据。图6.2展示了集群管理器获取资源的过程。
接下来,集群管理器将你的应用程序代码发送给执行者。你不需要在每个节点上部署你的应用。最后,SparkSession将任务发送给执行者来运行。
Spark架构的故障排除技巧
如6.1.1节所述,Spark的架构可能看起来很不寻常,但希望仍然很容易理解,至少在数据和软件工程师需要理解它的层面上(性能调优当然需要更深入的知识)。在本节中,你将会看到架构的细节和限制。
其中典型的可能出错的事情是Spark花费了太多的时间,或者你永远得不到结果。如果部署后出了问题,请考虑以下几点。
即使你的操作没有给你的应用带回结果,当你导出数据时就是如此,你必须始终确保执行器可以与驱动程序对话。与驱动对话意味着执行器和驱动不应该被防火墙隔离,在另一个网络上,暴露多个IP地址等等。当你的应用程序在本地运行(例如在开发中)并试图连接到远程集群时,通信中的那些问题就会发生。驱动程序在其整个生命周期中必须监听和接受来自其执行者的传入连接(参见附录K中 "应用程序配置 "部分的spark.driver.port)。
每一个应用程序都会得到自己的执行进程,在整个应用程序的持续时间内,这些执行进程都会一直保持运行,并在多个线程中运行任务。这样做的好处是,无论是在调度方面(每个驱动都会调度自己的任务)还是在执行方面(不同应用的任务在不同的 JVM 中运行),都可以将应用相互隔离。然而,这也意味着,如果不将数据写入外部存储系统,就无法在不同的Spark应用(SparkSession或Spark-Context的实例)之间共享数据。
Spark对底层集群管理器是不可知的,只要它能获得执行者进程,并且这些进程之间相互通信。可以在支持其他应用的集群管理器上运行,比如Mesos或YARN(见第18章)。
由于驱动程序在集群上调度任务,它应该在物理上靠近工作节点运行,最好是在同一个局域网上。进程是网状工作密集型的,当盒子离得更近时,你可以减少延迟。当你使用云服务时,将物理节点维护在一起并不容易,也不容易知道如何做或需求。如果你计划在云中运行Spark,请向你的云运营商咨询让机器物理位置相互靠近的问题。
附录R列出了常见的问题和解决方案,以及在哪里可以找到帮助。
更进一步
在6.1节中,我向你介绍了Spark架构,这是部署的最基本的基础。你可以想象,还有更多的内容。
本书的内页列出了你在执行应用程序时驱动日志中的术语。如果想进一步了解,你可以阅读Spark文档,可以在https://spark.apache.org/docs/latest/cluster-overview.html。
建立一个集群
在6.1节中,你经历了构建一个无摄取的应用程序,阅读了与Spark交互的三种方式,并探索了各种组件和它们之间的联系。
这应该已经为你的下一个任务提供了足够的胃口:在真正的集群上部署你的应用程序。在本节中,你将看到如何做以下工作。
构建一个集群
设置其环境
部署你的应用程序(通过构建一个uber JAR或使用Git和Maven)。
运行您的应用程序
分析执行日志
构建适合自己的集群
我意识到在家里或办公室建立一个分布式环境并不容易,但正如你所知道的,Spark被设计成在分布式环境中工作。在本节中,我将描述你的选择;有些比其他更现实,这取决于你的时间和预算。我将描述使用四个节点的部署,但如果你愿意,你也可以在单个节点上工作。我强烈建议你至少有两个节点;这将使你了解网络问题以及如何共享数据、配置和二进制文件(应用程序、JAR)。
那么,你在集群上工作的选择是什么?你有几种选择,可以在家里构建一个分布式环境。
最简单的可能是使用云:在云提供商中获得两到三个虚拟机,如Amazon EC2、IBM Cloud、OVH或Azure。需要注意的是,这个方案的成本可能难以估算。我现在不推荐Amazon EMR,因为它有一些限制(你将在第18章中发现更多的内容)。你将不需要庞大的服务器,目标是超过8GB的内存和32GB的磁盘。CPU对于这个实验室来说没有那么重要。
第二种选择是在家里有一台稍微大一点的类似服务器的机器,你将在上面安装虚拟机或容器。这个选项对每台机器的要求与第一个选项相同;这意味着如果你计划有四个节点,物理机器中至少要有32 GB的内存。这个选项可以用Docker或VirtualBox免费完成。提示:你的青少年时用的Fortnite的游戏机可能是Spark的一个很好的候选者,你可以将GPU用于Spark上的TensorFlow等有用的东西。
你的第三种选择是使用你家里可能有的所有旧东西,用它来构建一个集群,内存小于8GB的电脑除外。这不包括你的Atari 800XL,你的Commodore 64,你的ZX81,以及其他的一些电脑。
最后,你可以按照我的方法,从头开始购买并建立四个节点。我的集群被称为CLEGO,你可以在我的博客上找到一个方法(以及为什么我这样命名):http://jgp.net/clego。
在本章中,我将使用CLEGO的四个节点,因为硬件是为分布式处理设计的。图6.3显示了本实验室使用的架构。
如果你没有一个以上的节点,你仍然可以跟着做,只对un节点进行操作。
设置环境
现在你已经定义了你的环境,你将需要做以下工作。
安装Spark.
配置并运行Spark。
下载/上传你的应用程序。
运行它。
你需要在每个节点上安装Spark(详情请参考附录K)。安装是非常直接的。你不必在每个节点上安装你的应用程序。在本章的其余部分,我将假设你在/opt/apache-spark中安装了Spark。
在主节点上(本例中为un)进入/opt/apache-spark/sbin。运行主节点。
$ ./start-master.sh
请记住,Master不做什么,但它总是需要Worker。要运行你的第一个Worker,请输入以下内容:
$ ./start-slave.sh spark://un:7077
在这种情况下,worker与主节点运行在同一个物理节点上。
小心你的网络配置 在你构建集群的过程中,网络在这里起到了关键作用。每个节点都应该能够与其他节点来回交谈。检查你需要的端口是否可以访问,并且希望没有被其他东西使用;通常,它们是7077、8080和4040。你不会把这些端口开放给互联网,只是内部开放。你可以用ping、telnet(使用telnet <host> <port>命令)检查。不要在任何命令中使用localhost作为主机名。
你总是先启动你的Master,然后再启动你的Worker。要检查一切是否正常,打开浏览器并访问http://un:8080/。这个web界面是由Spark提供的;你不需要启动web服务器或将web服务器链接到Spark。确保你没有任何东西在8080端口上运行,或者修改相应的配置。图 6.4 显示了结果。
当你运行更多的应用程序时,这个界面会自动填充,你将能够发现更多关于应用程序的信息,它们的执行情况等等。在6.4.1节,你将看到如何访问应用程序的日志。
现在你可以进入下一个节点(在这个例子中,deux)。进入/opt/apache-
spark/sbin。不要再运行另一个Master,而是启动第二个Worker。
$ ./start-slave.sh spark://un:7077
您可以刷新您的浏览器。图6.5说明了结果。
因为我有四个节点,我将对第三个和第四个节点重复这个操作。我的Master与第一个节点运行在同一个节点上。最后,浏览器应该显示图6.6。
现在你已经有了一个工作集群,由一个Master和四个Worker组成,准备好执行你的工作。物理集群使用的是四个节点。
构建您的应用程序以在集群上运行
我们几乎到了压轴的时候了。你已经部署了Spark并构建了你的集群。我相信你已经迫不及待地想在集群上部署你的π近似代码,看看它的性能如何。
但先别急--你首先需要将你的代码部署到集群上。要部署你的应用程序,你有几个选择。
构建一个包含你的代码和所有依赖关系的uber JAR。
构建一个包含你的应用程序的JAR,并确保所有的依赖项都在每个工作节点上(不推荐)。
从你的源代码控制库中克隆/拉取。
SPARK将你的代码部署到Worker上,这是一个非常酷的功能。你只需要部署一次,复制到每个Worker节点的工作由Spark自己负责。
在uber JAR和从Git下载并本地重建之间的部署选择,可能由你的组织中负责部署/基础设施的部门做出。它可以由安全策略驱动,比如 "不在生产服务器上编译"。
那数据呢?
当涉及到将应用程序部署到每个节点时,Spark会从主节点为你做这件事。然而,Spark不会确保你的所有执行者都能访问数据。请记住,在本章描述的过程中,你并没有部署任何数据,因为Spark会自行生成包含计算π所需数据的数据框。 当你使用外部数据时,所有工作者也需要访问数据。Hadoop分布式文件系统(HDFS)是一个分布式文件系统,是以复制方式共享数据的流行选择。要部署数据,您可以使用以下方法。
Worker都可以访问的共享驱动器,如服务器消息块/通用互联网文件系统(SMB/CIFS)、网络文件系统(NFS)等。我强烈建议每个worker的挂载点都是一样的。
文件共享服务,如Nextcloud/ownCloud、Box、Dropbox或任何其他。数据会在每个worker上自动复制。这种解决方案限制了数据传输:数据只复制一次。与共享驱动器一样,我强烈建议每个worker上的挂载点是相同的。
分布式文件系统,如HDFS。
在第18章中会详细介绍这些技巧和技术。
构建你的应用程序的uber JAR
在你的部署之路上,你的一个选择是将你的应用程序打包在一个uber JAR中。正如你在第5章中所回忆的那样,uber JAR是一个包含你的应用程序所需要的所有类的档案,不管你的类路径上有多少个JAR。uber JAR包含了你的应用程序的大部分(如果不是全部)依赖关系。然后,被uber简化了,因为你将只处理一个JAR。让我们用Maven构建一个uber JAR。
要构建uber JAR,你将使用Maven Shade构建插件。你可以在 http://maven.apache.org/plugins/maven-shade-plugin/index.html 阅读它的完整文档。
打开项目的pom.xml文件。找到 build/plugins 部分,添加以下内容。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<minimizeJAR>true</minimizeJAR>
<artifactSet>
<excludes>
<exclude>org.apache.spark</exclude>
<exclude>org.apache.hadoop</exclude>
<exclude>junit:junit</exclude>
<exclude>jmock:*</exclude>
<exclude>*:xml-apis</exclude>
<exclude>log4j:log4j:jar:</exclude>
</excludes>
</artifactSet>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>uber</shadedClassifierName>
</configuration>
</execution>
</executions>
</plugin>
exclude是关键;你不希望携带所有的依赖类。如果你没有排除,你所有的依赖类都会被转移到你的uber JAR中。这包括所有的Spark类和工件。它们确实是需要的,但是因为它们是包含在Spark中的,所以它们将在你的目标系统中可用。如果你把它们捆绑在你的uber JAR中,那个uber JAR会变得非常大,你可能会遇到库之间的冲突。
因为Spark有超过220个库,所以你不需要在你的uber JAR中加入目标系统中已经存在的依赖关系。你可以通过包名来指定它们,比如这个Hadoop的排除:
<exclude>org.apache.hadoop</exclude>
或者按artifact,加上通配符,就像排除测试的jmock库一样:
<exclude>jmock:*</exclude>
测试,即使它们是至关重要的,它们的库在部署中也不需要。清单6.2有一个排除项的节选。第5章的GitHub仓库中的pom.xml有一个几乎详尽的排除列表,你可以在你的项目中使用它作为基础。
在你的项目目录中(你的pom.xml所在的地方),你可以通过调用以下内容来构建uber JAR。
$ mvn package
结果是在目标目录下:
$ ls -l target
...
-rw-r--r-- ... 748218 ... spark-chapter05-1.0.0-SNAPSHOT-uber.JAR -rw-r--r-- ... 25308 ... spark-chapter05-1.0.0-SNAPSHOT.JAR
虽然uber JAR的大小要大得多(约750KB而不是约25KB),但可以尝试暂时去掉exclusions和minimumJAR参数,看看这些参数对uber JAR大小的影响。
使用Git和Maven构建你的应用程序
当谈到部署你的应用程序时,另一个选择是转移源代码并在本地重新编译。我必须承认这是我最喜欢的方式,因为你可以在服务器上调整参数,并将你的代码推回你的源码控制。
安全专家可能不会让你在生产系统上这样做(那些日子可能已经一去不复返了)。但是,我强烈支持在开发环境中这样做。
你可以在测试环境上这么使用,这取决于你公司在DevOps方面的成熟度。如果你完全掌握了CICD的流程,几乎不需要在开发服务器上进行本地重新编译。如果你的部署仍然涉及大量的手动流程,或者你的CICD管道很繁琐,本地编译可以帮助你。
附录H提供了一些关键的提示,以简化你的Maven生活。
访问源码控制服务器 部署代码到目标系统需要你的目标系统能够访问你的源码仓库,这在某些情况下是很棘手的。我曾在一个项目中工作过,在这个项目中,用户是通过活动目录/LDAP来识别源码控制系统的,所以你不能把你的登录名和密码暴露在开发服务器上。幸运的是,像Bitbucket这样的产品支持公钥和私钥。
在这种情况下,代码在GitHub上是免费提供的,所以你可以很容易地拉它。在节点上,你要运行你的驱动应用程序。在这种情况下,你将不必在每个节点上运行应用程序。输入以下内容。
现在你可以通过调用mvn install来编译和安装你的程序。请注意,这个过程在第一次调用时可能需要一点时间,因为Maven会下载所有的依赖关系:
请注意,一个包含源代码的包已经被构建并安装。在这个方案中,我使用了我在un上的个人帐号,但你也可以使用一个通用账号或与所有帐号共享你的Maven仓库。你可以在你的本地Maven仓库中检查它:
你真的在部署源码吗?
我可以听到一些人说 "你疯了吗?你为什么要部署源代码,我们最宝贵的资产?" 我想说,你最宝贵的资产可能不是你的源代码,而是你的数据。然而,这不是重点。
在我的大部分职业生活中,我一直在使用源码控制软件。并发版本系统(CVS),Apache Subversion(SVN),Git,甚至微软的Visual SourceSafe。然而,就像所谓的链条和环节一样,一个过程只有最弱的元素才是强大的。最弱的元素通常是键盘和椅子之间的部分:人。无数次,尽管有流程、规则和自动化,我和团队还是无法恢复与部署版本相匹配的源代码:标签没有设置,分支没有创建,存档没有建立......。
就像墨菲定律所说的那样,你丢失了源代码的应用程序总是会出现生产问题。好吧,这并不完全是墨菲定律,但你懂的。
所以,我对这些人的问题的回答是,"谁在乎呢?" 因为在生产系统宕机的紧急情况下,首要任务是确保团队能够获得正确的资产,而部署匹配的源码可以确保一部分。Maven可以确保部署的应用有相应的源代码,如清单6.3所示。
你可以轻松地指示Maven自动打包源代码。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
你有了JAR文件,现在可以在集群上运行它了。
在集群上运行您的应用程序
所以我们到了最后。在经历了Spark如何工作的所有关键概念,学习如何与它交互,构建所有这些JAR,并深入研究Maven之后,你终于可以在集群上运行你的应用程序。不是开玩笑的!
在6.3节中,你构建了两个可以执行的构建。
你将提交给Spark的uber JAR。
编译源码的JAR
让我们部署并执行它们。你对执行的选择取决于你如何构建 您的申请。
提交uber JAR
你的第一个选择是运行你通过spark-submit建立的uber JAR。这是你在6.3.1节准备的uber JAR。除了你的JAR之外,你不需要其他任何东西。
上传你的uber JAR到服务器上。
$ cd /opt/apache-spark/bin
然后向Master提交申请。
$ ./spark-submit \
--class net.jgp.books.spark.ch05.lab210.piComputeClusterSubmitJob.PiComputeClusterSubmitJobApp \ --master "spark://un:7077" \
<path to>/spark-chapter05-1.0.0-SNAPSHOT.jar
Spark会很啰嗦,但通过日志,你会看到你的信息。
运行应用程序
运行应用程序的第二个选择是直接通过Maven运行。这是6.3.2节中本地编译的继续。
进入存放源代码的目录:
$ cd ~/net.jgp.books.spark.ch05
然后运行以下内容:
你已经通过两种方式成功地执行了一个应用程序。让我们看看幕后发生了什么。
分析Spark用户界面
在6.2节中,当你在构建你的集群时,你看到Spark有一个用户界面,你可以在你的主节点的8080端口(默认情况下)访问它。现在你已经运行了你的第一个应用程序,你可以回到这些视图,它显示了你的集群和应用程序的状态,包括运行和完成。
进入你的主节点的Web界面(在本例中,它是http://un:8080)。图6.7显示了运行几个测试后的界面。
当你刷新屏幕时,应用程序将被移动到 "已完成的应用程序 "部分。如果你点击链接,你将访问执行的细节,包括标准输出和标准错误输出。如果你查看日志文件,如图6.8所示,你会发现更多关于应用程序执行的信息。
在这里,你可以看到与Master的连接是成功的,执行器开始工作了。
概要
Spark支持三种执行模式:本地模式、集群模式和交互模式。
本地模式允许开发人员在几分钟内开始进行Spark开发。
集群模式用于生产。
你可以向Spark提交一个作业或连接到Master。
驱动程序应用程序是你的main()方法所在的地方。
Master知道所有的Worker。
执行发生在Worker节点上。
Spark在集群模式下处理你的应用程序JAR的分发到每个Worker节点。
CICD(持续集成和持续交付)是一种敏捷方法论,鼓励频繁的集成和交付。
Spark提供了一个Web界面来分析作业和应用程序的执行情况。
我的职业生涯是从VB3开发人员开始的。