3.1 在命令行模式下开发Spark程序
本节在命令行模式下开发第一个Spark程序,目的有两个:掌握Spark程序从代码编写到提交运行的整个过程;了解Spark开发工具链,即最简Spark开发环境的组成。
3.1.1 构建Scala程序编译环境
本书采用Spark的原生语言Scala来编写Spark程序。因此,使用Scala编写的Spark程序,在本质上也是一个Scala程序。
要将Scala代码转换成可以运行的Scala程序,首先要构建Scala的编译环境。本节将介绍 Scala 编译环境的构建,以及如何在命令行下开发一个最简单的Scala程序,为后续开发Spark程序打下基础。
1.构建Scala编译环境
Scala程序从编写代码到最后运行包括以下几个步骤:编写代码、编译(打包)、运行。其中,编译是一个非常重要的环节,它实现了由源码到可执行代码(可以被 JVM 解释执行的字节码)的转换。
整个Scala程序开发过程中各阶段的任务、工具、输入、输出如图 3-1 所示,虚线框所指的就是编译阶段,其输入文件是Scala源码文件(文件后缀名为.scala),所使用的工具是Scala编译器scalac,输出文件是JVM字节码(文件后缀名为.class)。
图3-1 Scala程序开发过程图
scalac 编译器在 Linux 环境下运行。为了使用 scalac,要先安装 Linux。如果 Host 是Windows的话,可以采用虚拟机配置Linux环境。因此,Scala的编译环境如图3-2所示。
图3-2 Scala编译环境图
根据图 3-2 所示,整个编译环境可分为 4 个层次,每个层次的软件和版本要求说明如下。
● 第一层为 Host 系统,也就是物理机器上安装的操作系统,版本为 Windows 7 64位,如果是Windows 7 32位,建议换成64位系统;
●第二层为虚拟机软件,这里采用的是VMware WorkStation 9。不建议更低版本的VMware WorkStation,因为低版本的VMware WorkStation创建出来的虚拟机性能受限;
●第三层为Guest系统,即安装在虚拟机上的操作系统,所选的Linux发行版为CentOS 7.2(CentOS 7 系列都是 64 位的),为了避免不必要的麻烦,这里统一发行版和版本号(不管第一、二层的软件和版本是否一致,到第三层统一即可);
●第四层为Scala编译器scalac,它包含在Scala包中,scalac运行需要Java的运行时环境 JRE,编译程序时需要Java开发库,这些都包含在 JDK 中,具体版本在具体实践环节会给出。
为了避免不必要的麻烦,建议读者将自己的 Scala 编译环境中软件及版本号保持和本书中的版本完全一致。
本节“构建Scala程序编译环境”属于实践内容,因为后续章节会用到本节成果,所以本实践必须完成。请参考本书配套资料《Spark 大数据编程实践教程》中的“实践 5:构建Scala程序编译环境”完成本节任务。
本书还提供了虚拟机 VMware 和 Linux 的高清免费入门视频,地址获取途径参见 1.6.4节内容。
2.Scala程序的编写、构建与运行
本节将展示从零开始编写 Scala 代码,到运行Scala程序的全过程,整个过程将剔除一切非必需的环节和工具,如 Scala IDE 集成开发环境和 Scala 构建工具 sbt 等,只使用构建Scala程序最基本的工具和命令,目的是使得大家明白:什么才是Scala程序编写过程中,最基础的东西。
本节“编写、构建与运行Scala程序”属于实践内容,因为后续章节会用到本节成果,所以本实践必须完成。请参考本书配套资料《Spark 大数据编程实践教程》中的“实践 6:编写、构建与运行Scala程序”完成本节任务。
3.1.2 使用Vim编写Spark程序
Vim是一个字符界面下的文本编辑器,几乎所有的Linux发行版都会自带Vim。
1.示例概述
本节使用Vim编写第一个Spark程序——HelloSpark。
HelloSpark 实现了 5 个并行 Task,每个 Task 会打印编号,等待规定的时间后返回一个二元组,Driver端收集所有Task的返回值,并打印输出。
2.示例代码
代码文件名:HelloSpark.scala。
代码路径:/home/user/prog/examples/03/src/examples/vim/spark/HelloSpark.scala。
具体代码如下。
说明:
●Spark程序的参数可以在spark-submit中通过参数指定,也可以在SparkConf中设定;
●SparkContext对象表示一个Spark Application,必须要创建;
●Spark提供了一种特殊的数据结构RDD用于并行处理;
●Spark程序退出前,要调用sc.stop来结束此次Spark任务。
后续还会对每行代码详细解释,在此不需要细究。
3.1.3 使用命令编译、打包Spark程序
本节将HelloSpark.scala编译成class文件,并将其打成jar包。具体步骤如下。1.编译
编译命令如下。
其中:
●编译器命令是scalac,Spark代码也是Scala代码,编译器是一样的;
●-cp 后面跟编译时所需的 jar 包路径,共两个:第一个是 scala-library-2.11.8.jar,这个是编译任何Scala程序都需要的库,Spark程序也是Scala程序,同样需要这个包。这里选择的scala-library-2.11.8.jar是Spark自带的,编译和运行时选择同一个jar包。当然,在 Scala 安装路径下,也有一个 scala-library.jar,也可以选择它;第二个路径是Spark自身所带的 jar 包,它位于 Spark 安装目录的 jars 目录,可以用*通配符表示此目录下的所有文件;
●路径和路径之间使用冒号隔开;
● 第一个路径也可以写成:~/spark-2.3.0-bin-hadoop2.7/jars/scala-library-2.11.8.jar,即用波浪号表示当前用户的 home 目录,但是第二个路径不能写成~/spark-2.3.0-bin-hadoop2.7/jars/*,否则编译会报错。
编译完毕后,编译器会在当前路径下自动创建路径:examples/vim/spark,这个路径就是根据 HelloSpark.scala 中 Package 信息:examples.vim.spark 自动生成的,编译好的文件HelloSpark.class就在此路径下。
列出spark目录下内容的命令如下。
Spark目录的内容显示如下。
总结:编译Spark代码和编译Scala代码唯一区别在于:编译Spark代码要加上Spark所提供的jar包,因为Spark程序代码要用到这个jar包所提供的类。
2.打包
下面将编译好的 class 文件打成一个jar包,之后就可以通过spark-submit 提交 jar 包运行了,打包命令如下。
其中:
●打包命令是jar;
●cvf是jar的选项,c表示create,创建jar包,v表示显示打包过程信息,f指定要创建的jar包名字,即examples.jar;
●-C后面跟一个路径path,它表示以该path作为打包的work directory,也就是说,后面参数所指定的路径,如examples,是从该path开始的;
●-C后面有一个空格,然后有一个点“.”,这个点就是-C后面的path,点本身表示当前路径,因此,-C和“.”组合起来表示:以当前路径作为打包的work directory;
● 点后面有空格,接着是 examples/,它是一个相对路径,其绝对路径是 work directory/examples,即path/examples,它表示对examples整个目录打包。
需要注意的是:
●exmaples 是 Package 的起始目录,也就是说,examples.jar 解压后,当前目录下就有examples目录,即使指定的是 examples 的子目录,例如“jar cvf examples.jar-C.examples/vim/spark/”,examples.jar解压后,当前目录下还是examples目录;
●examples路径要和Package信息一一对应,本例中Package是examples.vim.spark,那么examples的路径信息就应该是examples/vim/spark;
●jar 支持将多个目录打到一个 jar 包,对每个要打包的目录,仿照上面的例子,先用-C path切换到work directory(后面要有空格),再加上要打包的目录的相对路径即可。
有一个问题需要说明:为什么要采用-C+path+相对路径?而不是直接-C+路径的方式?这是因为:
●如果使用-C+路径,jar 就会把-C 后面的路径整个打成一个jar包,如果把examples放到不同路径,打出的jar包的目录结构是不一样的,此时,jar包的目录结构和Package就不一一对应了,调用jar包时,根据Package信息就会找不到对应的Class;
●使用-C+path+相对路径,无论examples在哪个路径下,打出的jar包都是一致的。
命令执行后,会在当前目录下生成examples.jar。
3.验证
使用jar xf命令来解压打好的jar包,查看并验证打进jar包的内容。
创建tmp目录,复制jar包到tmp目录,并解压。
解压后,可以看到原来的jar包中有3个目录。
第一个目录是examples,列出examples下的内容,命令如下。
examples的内容如下,它保存了编译后的HelloSpark.class文件,由此可知打包是成功的。
第二个目录是 META-INF,它里面有个 MANIFEST.MF 文件,用来指定 jar 包的 Main Class,以及此jar所依赖的其他jar包。此处该文件是空的。因此,需要在运行jar包时指定Main Class。
3.1.4 运行Spark程序
本节将上一节打好的 jar 包 examples.jar,提交到 Standalone 和 Yarn 上,以 client 模式运行。
1.提交examples.jar到Standalone运行(client deploy mode)
确保 scaladev 和 vm01 上的 Standalone 已经启动,其中 scaladev 上运行 Master 和Worker,vm01上运行Worker。
运行命令如下。
对代码的说明如下。
●如果不指定--deploy-mode,则默认是client mode;
●--master spark://scaladev:7077,指定Spark程序提交到 Standalone 集群上执行,scaladev为Master的主机名,7077是Master的监听端口;
●--class examples.vim.spark.HelloSpark,指定该jar包的Main Class 为examples.vim.spark.HelloSpark;
●./examples.jar是一个相对路径,表示当前目录下的examples.jar文件。
程序运行完毕后,如果能够看到下面的输出结果则说明运行成功。具体的运行过程后续会专门分析。
2.提交到Yarn运行(client deploy mode)
关闭Standalone,启动Yarn,其中scaladev上运行ResourceManager和NodeManager,vm01 上运行 NodeManager;启动 HDFS,其中 scaladev 上运行 NameNode 和 DataNode,vm01上运行DataNode。
运行命令如下。
对代码的说明如下。
●Yarn运行时,需要使用HDFS作为公共存储,因此HDFS也要运行,否则会报错;●Yarn的运行命令和前面Standalone命令的区别在于:--master yarn,指定Yarn。
同样的,程序运行完毕后会有下面的输出结果。
3.1.5 使用java命令运行Spark程序
使用java命令也可以运行Spark程序,不仅能以Local方式运行,还能提交到集群上分布式运行。分布式运行的示例命令如下。
参数说明如下:
●-Dspark.master=spark://scaladev:7077,指定Spark连接的集群管理器信息;
●-Dspark.jars=/home/user/prog/examples/bin/examples.jar,指定Spark程序的 Jar 包路径,Spark程序启动后,会将此路径下的 Jar 包发送到 Executor 所在的节点,当Executor中的Task执行时,会在此Jar包中加载Task对应的Class;
●-Dspark.app.name=HelloSpark,指定Spark Application名字,如果该信息在Spark程序代码中已经指定,则此处不需要再指定;
●-classpath,/home/user/scala-2.11.12/lib/scala-library.jar:/home/user/spark-2.3.0-bin-hadoop2.7/jars/*:/home/user/prog/examples/bin/examples.jar,用于指定java程序运行所需的Jar包路径;
在-Dspark.jars 和-classpath 的后面,都出现了相同的参数值:“/home/user/prog/examples/bin/examples.jar”,虽然参数值一样,但它们的使用者和作用是不一样的,其中,-classpath 是给 java 命令用的,用来加载和运行Spark程序;-Dspark.jars是给Spark程序用的,是Spark程序运行以后用的。
●examples.vim.spark.HelloSpark用来指定java程序的Mainclass。
结论:由上面的java运行命令,可以清晰地看出,Spark程序本质上就是一个Java程序,只是在运行时传入了Spark程序相关的配置。至于Spark程序的分布式运行等特性,都是在这个Java程序运行后由 Spark框架部分自动初始化完成的,但它本质上就是一个Java程序。
3.1.6 Spark程序编译、运行、部署的关键点
Spark程序在各环节中的关键点总结如下。
1)Spark代码的编辑和Scala代码编辑一样,可以使用普通的文本编辑器。像Eclipse、IDEA等IDE可以给代码编辑带来方便,但并不是编辑Spark代码所必需的;
2)Spark 代码的编译和 Scala 代码编译差别不大,只需要在编译时增加 Spark 安装目录下jars目录下的jar包即可,在编译Spark代码的节点上需要部署Spark;
3)Spark程序在 Standalone 上运行时,需要每个节点部署 Spark,因为集群中,有一个节点要运行Master,其他的节点要运行Worker,都需要Spark运行环境;
4)Spark 在Yarn上运行时,只需要在提交程序的节点上部署Spark,因为要用到Spark的运行环境;其他节点不需要部署 Spark,Executor 可以在这些节点上直接运行,即使这个Yarn集群有1000个节点也是一样;
5)JDK 是每个节点都需要的,因为,这些程序从本质上讲都是Java程序,最终的运行需要JVM支持;
6)Hadoop 也是每个节点都需要的,因为 HDFS 要部署到每个节点,如果使用 Yarn 的话,也需要部署到每个节点;
7)Scala 安装包,只在编译Spark代码所在的节点需要,因为要用到scalac命令,其他节点不需要,Spark程序的运行不需要Scala安装包;
8)Spark 安装包,在编译Spark程序和提交Spark程序的节点上是需要的,在运行Standalone的节点上是需要的;
9)要搞清楚各个安装包的作用,但在实际部署时,为了便于管理,建议每个节点的软件部署一致,不用根据角色做严格区分。