PySpark集群完全分布式搭建

本文的目的是使读者对spark的安装流程有一个清晰的认识,并且能根据本文的内容搭建一个属于自己的完全分布式Spark集群,并在此基础上增加pyspark的分布式环境。

阅读本文前,有几个点需要注意:

  1. 本文假设读者有Hadoop的搭建基础,并且成功搭建了完全分布式的Hadoop集群,因此本文不会对该方面的知识进行铺垫。
  2. 本文假设读者有在Linux上安装anaconda或者minconda的基础,并且成功的在每一个节点上的相同路径下配置好了相应的环境。(该过程可以每个节点一一配置,也在可以配置好某个节点后,把配置好的文件打包发送到所有节点再解压,因为略占篇幅、且不为本文重点是故省略)

寻找合适的Spark安装包

spark的官网为:https://spark.apache.org/

进入官网后可进入下载页面:https://spark.apache.org/downloads.html

下载页面的核心部分如下所示:

spark 写入hdfs 如何覆盖_大数据

下载Spark安装包时需要特别注意发行版的兼容性问题,特别是Spark版本与Hadoop版本的兼容性、以及Spark版本与Scala版本的兼容性。

虽然当前最新的Spark版本已经更新到3.3.0了,最新版的hadoop也更新到3.3.4,不过因为笔者的Hadoop版本为3.2.2,所以使用的Spark安装包为spark-3.1.1-bin-hadoop3.2.tgz,对应的Scala版本为2.12.12。

笔者更鼓励读者使用最新的安装包进行尝试,但是如果已经安装好了某一版本的hadoop,那更建议去官网的历史发行版页找到对应版本的Spark安装包进行下载。


解压Spark安装包与Scala安装包

假设读者已经下载好了Spark和Scala的安装包,并且上传到了主节点的某一文件路径。

笔者使用的主节点为:westgisB052

存放Spark安装包的路径为:~/pkg/spark-3.1.1-bin-hadoop3.2.tgz

存放Scala安装包的路径为:~/pkg/scala-2.12.12.tgz

Spark解压后存放的目标路径为:~/bigdata/

Scala解压后存放的目标路径为:~/program/

所以在配置环境变量时,SPARK_HOME=/home/G22/bigdata/sparkSCALA_HOME=/home/G22/program/scala

注1:配置环境变量时,指定变量的取值必须为绝对路径,~/bigdata/spark指向的绝对路径就是/home/G22/bigdata/spark/home/G22为笔者的用户根目录,简写为~~/program/scala同理。

注2:解压后的压缩包会带版本号的后缀,不过笔者觉得不太美观,所以还会进行重命名操作。

接下来,根据上述准备好的路径,我们可以执行:

#1.解压安装包到目标路径
tar -zxvf ~/pkg/spark-3.1.1-bin-hadoop3.2.tgz -C ~/bigdata/
tar -zxvf ~/pkg/scala-2.12.12.tgz -C ~/program/
#2.重命名
mv ~/bigdata/spark* ~/bigdata/spark # *是通配符,表示后面有0或n个任意字符
mv ~/program/scala* ~/program/scala

配置环境变量

  1. 配置scala环境下的spark只需要在~/.bashrc文件中添加如下六句:
#SCALA_ENV
export SCALA_HOME=/home/G22/program/scala
export PATH=$PATH:$SCALA_HOME/bin
#SPARK_ENV
export SPARK_HOME=/home/G22/bigdata/spark
export PATH=$PATH:$SPARK_HOME/bin:$SPARK_HOME/sbin

需要注意的是,读者需要根据自己spark的存放路径,更改SPARK_HOME的取值。此外,即使读者只想配置python环境下的Spark集群,也要配置SPARK_HOME,并将其的bin目录添加进$PATH变量,即添加最后3句。

  1. 配置pyspark环境时,还需要在~/.bashrc中添加以下配置:
#PYSPARK_ENV
export PYSPARK_PYTHON=$MINIC_HOME/bin/python
export PYSPARK_DRIVER_PYTHON=$MINIC_HOME/bin/python
export LD_LIBRARY_PATH=$MINIC_HOME/lib/:$LD_LIBRARY_PATH
export PYTHONPATH=$(ZIPS=("$SPARK_HOME"/python/lib/*.zip); IFS=:; echo "${ZIPS[*]}"):$PYTHONPATH

第一行和第二行的意义是指定pyspark启动和执行任务时使用的python解释器;

第三行和第四行的意义是指定pyspark运行时,加载模块库的路径。

注:此处的$MINIC_HOME为笔者minconda的安装路径,具体为~/minconda3,对应的绝对路径为/home/G22/minconda3,读者需要根据自己minconda或者anoconda的安装路径进行修改。

  1. 检验环境变量是否配置成功:
source ~/.bashrc #更新环境变量
run-example SparkPi #执行spark例子程序:SparkPi

如果执行成功,会在屏幕输出Spark的运行日志信息,以及运行结果:Pi is roughly 3.146675733378667,该运行结果夹杂在运行日志信息中间。


修改配置文件

  1. 修改$SPARK_HOME/conf/spark-env.sh解压Spark后,其conf目录下本身并不存在spark-env.sh文件,只有spark-env.sh.template文件,因此我们首先需要基于后者生成前者,命令如下:
cd $SPARK_HOME/conf/ #进入配置文件目录
cp ./spark-env.sh.template ./spark-env.sh #生成配置文件

之后编辑新生成的文件spark-env.sh,添加如下内容:

#PART1
export JAVA_HOME=/home/G22/bigdata/java
export SCALA_HOME=/home/G22/bigdata/scala
export HADOOP_HOME=/home/G22/bigdata/hadoop
export HADOOP_CONF_DIR=/home/G22/bigdata/hadoop/etc/hadoop
export YARN_CONF_DIR==/home/G22/bigdata/hadoop/etc/hadoop

#PART2
export SPARK_MASTER_HOST=westgisB052
export SPARK_MASTER_PORT=7077
export SPARK_PID_DIR=/home/G22/bigdata/spark/data/pid
export SPARK_DIST_CLASSPATH=$(/home/G22/bigdata/hadoop/bin/hadoop classpath)
export SPARK_HISTORY_OPTS=" 
-Dspark.history.ui.port=18080 
-Dspark.history.fs.logDirectory=hdfs://westgisB052:9000/directory 
-Dspark.history.retainedApplications=30"

#PART3
export SPARK_WORKER_CORES=4
export SPARK_WORKER_MEMORY=8G
export SPARK_EXECUTOR_CORES=1
export SPARK_EXECUTOR_MEMORY=2G
export SPARK_DRIVER_MEMORY=1G

可以看到添加的内容分为3个部分:第一部分是纵向配置,用于指定Spark的底层依赖,因为Spark依赖编程语言Java和Scala,所以需要设置JAVA_HOMESCALA_HOME,因为我们选择的Spark发行版是基于兼容版本的Hadoop构造的,所以也要指定Hadoop相关的配置,如HADOOP_HOMEHADOOP_CONF_DIRYARN_CONF_DIR

第二部分是Spark的主要配置,用于指定Master节点的IP或者主机名、Master和其它节点进行交互的端口、Spark守护进程pid的存放路径、Spark的依赖包的路径、Spark的历史服务器设置等。(以上5个配置分别按顺序与配置文件中part2部分的内容对应)

第三部分是Spark的资源配置,Spark是主从式架构,组件的角色包括主节点Master,从节点Worker,资源管理器ClusterManager(Spark有多种运行模式,在YARN模式下运行时ClusterManager是YARN;在Standalone模式下运行时,ClusterManager是主节点Master担任),而一个Worker节点又可以包含一个或多个Executor,每个Executor是一个进程,专门用于执行具体的计算任务。因此在进行集群配置时,可以按不同的粒度对Worker和Executor进行资源配置,在笔者上述的配置中:给每个Worker节点分配了4个CPU的核心、8GB的内存、给每个Executor分配了1个核心和2GB内存。(至于Part3的最后一个配置:Spark会在ClusterManager节点上启动一个Drive进程作为Spark应用程序的入口,此外Driver还包含SparkContext实例,负责向集群申请资源、向master注册信息,作业调度,作业解析、生成Stage并调度Task到Executor上等功能的实现,而Driver进程完成这些功能是需要内存的,因此SPARK_DRIVER_MEMORY参数指定的就是Driver进程可使用的内存资源)

上述的配置文件建议读者认真仔细地阅读,理解每一个参数的含义,并根据自己的配置修改每一个环境变量的取值。

  1. 修改$SPARK_HOME/conf/workersspark-env.sh一样,其在conf目录下本身并不存在,但存在workers.template文件,因此我们首先需要基于后者生成前者,命令如下:
cd $SPARK_HOME/conf/ #进入配置文件目录
cp workers.template workers #生成配置文件

workers配置文件的配置很简单,只需要把workers里的内容全部替换成从节点的主机名或者IP即可,笔者的内容为:

westgisB053
westgisB054
westgisB055
westgisB056

修改完上述两个配置文件后,Spark的配置文件就已经全部配置完毕了,此时可以将配置好的Spark文件打包,分发到从节点后解压,更新环境变量,则Spark就配置成功了。


打包分发

该过程同配置hadoop时,将配置好的Hadoop打包分发的过程类似,可以使用for循环来进行批量分发和解压、修改环境变量,执行命令如下:

#1.去到Spark的上级目录
cd $SPARK_HOME
cd ..
#2.打包Spark文件目录
tar -zcf spark.tar.gz ./spark
#3.分发Spark压缩包以及环境变量配置文件
for i in westgisB0{53..57}
do
  scp ./spark.tar.gz $i:~; 
  scp ~/.bashrc $i:~; 
done
#4.解压分发的Spark压缩包,之后删除压缩包,并刷新环境变量
for i in westgisB0{53..57}
do
  ssh $i  "tar -zxvf ~/spark.tar.gz -C ~/bigdata/" #注意替换为自己的路径 
  ssh $i  "rm ~/spark.tar.gz" #删除从节点上分发的压缩包
  ssh $i  "source ~/.bashrc"  #刷新环境变量
done

验证Spark集群是否搭建成功

  1. 启动Spark集群
start-dfs.sh       #启动HDFS
start-master.sh    #启动Spark的主节点Master
start-workers.sh   #启动Spark的从节点Worker
  1. 查看集群中是否存在Spark的Java守护进程
for i in westgisB0{52..57}
do
  ssh $i  "hostname;jps;echo"
done

如果结果同笔者类似,每个节点都成功的启动了安排的守护进程,则配置成功:

westgisB052
15477 SecondaryNameNode
15210 NameNode
16154 Jps
15899 Master

westgisB053
11907 Worker
11593 DataNode
12079 Jps

westgisB054
1448 Worker
1145 DataNode
1625 Jps

westgisB055
24212 Jps
23720 DataNode
24011 Worker

westgisB056
30112 Jps
29879 Worker
29544 DataNode

westgisB057
10079 Jps

解释:笔者的主节点为westgisB052,应该存在Master进程;客户端为westgisB057,理论上不存在守护进程;从节点为westgisB053~westgisB056,应该存在Worker进程。

  1. 查看Spark的web界面
    在windows主机的浏览器中输入网址:主节点IP:8080,若跳转页面如下,说明Spark集群配置成功且Web界面可用。

提交一个简单的Spark与HDFS集成的应用程序

  1. 上传数据文件到HDFS:
    假设我们的应用程序从HDFS的路径/user/G22/data/test下读取数据
#1.在HDFS上创建相应的文件夹
hdfs dfs -mkdir -p /user/G22/data/test
#2.从本地文件系统上传文件到HDFS
cd $SPARK_HOME
hdfs dfs -put ./README.md  /user/G22/data/test
#3.检查数据文件是否成功拷贝到了HDFS
hdfs dfs -ls /user/G22/data/test #若上一步骤成功,应返回"README.md"
  1. 以Standolne模式启动spark-shell:
    spark支持交互式数据分析以及对大型代码项目进行编译和运行,此处使用的spark-shell是Spark为使用者提供的交互式解释器,每输入一条指令,spark-shell就会翻译和执行。在启动spark-shell时,可以指定使用的集群模式(如local、standolne、yarn等)、还可以指定为spark-shell分配的硬件资源等设置。
    以standolne启动spark-shell的命令如下
spark-shell --master spark://westgisB052:7077  #记得替换主机为自己的主节点

执行效果如下:

[G22@westgisB052 ~]$ spark-shell --master spark://westgisB052:7077
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/home/G22/bigdata/spark/jars/slf4j-log4j12-1.7.30.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/home/G22/bigdata/hadoop/share/hadoop/common/lib/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
2022-10-06 00:15:24,006 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Spark context Web UI available at http://westgisB052:4040
Spark context available as 'sc' (master = spark://westgisB052:7077, app id = app-20221006001533-0000).
Spark session available as 'spark'.
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /___/ .__/\_,_/_/ /_/\_\   version 3.1.1
      /_/
         
Using Scala version 2.12.10 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_251)
Type in expressions to have them evaluated.
Type :help for more information.

scala>

上述内容中的最后一行有scala>的提示符,代表我们当前输入的命令不再由linux上的bash解释器进行翻译和执行,而是由spark-shell的scala解释器进行翻译和执行,此时我们便可输入scala语句进行交换。

  1. 输入简单的scala指令进行交互式分析:
//1.指定数据文件的输入路径(即前文在HDFS上创建的目录)
val logFile = "/user/G22/data/test/" 
//2.读取数据文件为RDD对象
val logData = sc.textFile(logFile).cache()
//3.统计数据文件中含有字符a的行数
val numAs = logData.filter(line => line.contains("a")).count()
//4.统计数据文件中含有字符b的行数
val numBs = logData.filter(line => line.contains("b")).count()
//5.打印统计结果
println("Lines with a: %s, Lines with b: %s".format(numAs, numBs))

若执行上述语句后,得到的最终结果如下,这说明Spark能够成功地运行scala应用程序:

Lines with a: 64, Lines with b: 32

此时,在spark-shell界面输入:q,敲击回车,则可退出spark-shell。


PySpark分布式应用程序测试

上一个步骤中,我们已经成功的执行了scala版本的Spark应用程序,我们现在将上面的程序修改为python版的,再启动PySpark进行运行。

  1. 启动PySpark
pyspark --master spark://westgisB052:7077 #记得替换主机为自己的主节点

执行效果如下,同spark-shell类似,pyspark也启动了一个交互式终端,不过与spark-shell不同的地方是pyspark使用python进行交互。

[G22@westgisB052 ~]$ pyspark --master spark://westgisB052:7077
Python 3.8.13 (default, Mar 28 2022, 11:38:47) 
[GCC 7.5.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/home/G22/bigdata/spark/jars/slf4j-log4j12-1.7.30.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/home/G22/bigdata/hadoop/share/hadoop/common/lib/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
2022-10-06 00:38:58,300 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /__ / .__/\_,_/_/ /_/\_\   version 3.1.1
      /_/

Using Python version 3.8.13 (default, Mar 28 2022 11:38:47)
Spark context Web UI available at http://westgisB052:4040
Spark context available as 'sc' (master = spark://westgisB052:7077, app id = app-20221006003900-0001).
SparkSession available as 'spark'.
>>>
  1. 输入python指令进行交互
#1.指定数据文件的输入路径(即前文在HDFS上创建的目录)
logFile = "/user/G22/data/test/"
#2.读取数据文件为RDD对象
logData = sc.textFile(logFile).cache()
#3.统计数据文件中含有字符a的行数
numAs = logData.filter(lambda line: 'a' in line).count()
#4.统计数据文件中含有字符b的行数
numBs = logData.filter(lambda line: 'b' in line).count()
#5.打印统计结果
print('Lines with a: %s, Lines with b: %s' % (numAs, numBs))

若执行上述语句后,得到的最终结果如scala程序运行的结果一致,则说明PySpark完全分布式环境配置成功。


基于PySpark的WordCount实现

在上文中我们使用了如下命令,将Spark的README.md文件上传到了HDFS

#1.在HDFS上创建相应的文件夹
hdfs dfs -mkdir -p /user/G22/data/test
#2.从本地文件系统上传文件到HDFS
cd $SPARK_HOME
hdfs dfs -put ./README.md  /user/G22/data/test
#3.检查数据文件是否成功拷贝到了HDFS
hdfs dfs -ls /user/G22/data/test #若上一步骤成功,应返回"README.md"

但前面的简单程序中我们实现的功能很简单,只是统计了包含a的行数和包含b的行数,而且也仅仅是在pyspark的交互环境中实现的。

在本部分,我们将编写WordCount程序统计每个单词出现的次数,因为程序使用PySpark执行,所以源码文件为python文件,存放路径由读者自定义,当然为了快速找到对应的代码文件,建议使用清晰规范的路径,笔者的存放pyspark源码文件的目录为~/code/pyspark

在创建对应的源码目录之后,我们在该目录下新建文件wordcount.py,添加如下内容:

#0.导入需要的库类
from pyspark import SparkConf, SparkContext
import os

#1.初始化SparkConf对象conf,读者记得修改对应的master的URL
conf = (SparkConf()
        .setMaster("spark://10.103.105.52:7077") #设置master的URL
        .setAppName("WordCount)                #设置应用程序的名称
        )

#2.使用conf生成SparkContext对象sc
sc = SparkContext(conf = conf)
sc.setLogLevel("WARN")  #设置日志输出级别为"WARN",可省略

#3.指定输入文件路径,文件路径可为本地文件系统的路径,也可为HDFS文件系统的路径
#3-1.指定local文件系统时,路径型如:"file:///home/G22/data/1.txt"
#3-2.指定HDFS文件系统时,路径形如:"hdfs://westgisB052:9000/user/G22/data/1.txt"
#    上述路径也可简写为"/user/G22/data/1.txt"
inputFile = "/user/G22/data/test/README.md"  #随便指定一个HDFS上的文件即可

#4.读取输入路径对应的文件转换为RDD对象
dataRDD = sc.textFile(inputFile)

#5.在RDD上使用算子进行wordcount的实现
wc = (dataRDD.flatMap(lambda line:line.split(" "))   #对每行文本进行分割
    .map(lambda x:(x,1))  #将"单词"映射为("单词",1),目的是构造以"单词"为key的键值对
    .reduceByKey(lambda a,b:a+b)) #根据键值对的键,即"单词"进行聚合,聚合方式为累加

#6.在客户端节点的显示屏输出词频统计的结果,即wc内的数据
wc.foreach(print)

#7.将词频统计的结果保存到HDFS文件系统
#7-1.设置保存路径,该路径最好不存在,否则常规输出模式下回报错
savePath="/user/G22/res/test"                #输出结果保存的路径

#7-2.判断保存路径是否存在,若存在则删除
#7-2-1.指定hdfs命令的存放路径
cmdPath="/home/G22/bigdata/hadoop/bin/hdfs"  #hdfs命令在linux上的路径
#7-2-2.通过os库的popen方法获取`hdfs dfs -test -e savePath`的执行结果
flag=os.popen(cmdPath +" dfs -test -e "+savePath+";echo $?").readlines()
#7-2-3.如果上一条语句执行后返回的结果为['0\n'],则savePath存在,需要删除
if flag == ['0\n']:
    rm=os.popen(cmdPath + " dfs -rm -r " + savePath).readlines() #删除savePath
    print(rm)

#7-3.写入词频统计的结果
wc.saveAsTextFile(savePath)

去掉注释后的代码内容为:

from pyspark import SparkConf, SparkContext
import os

conf = SparkConf().setMaster("spark://10.103.105.52:7077") .setAppName("WordCount")                
sc = SparkContext(conf = conf)
sc.setLogLevel("WARN")  

inputFile = "/user/G22/data/test/README.md"  
dataRDD = sc.textFile(inputFile)
wc = (dataRDD.flatMap(lambda line:line.split(" ")).map(lambda x:(x,1)).reduceByKey(lambda a,b:a+b)) 

wc.foreach(print)

savePath="/user/G22/res/test"                
cmdPath="/home/G22/bigdata/hadoop/bin/hdfs"  
flag=os.popen(cmdPath +" dfs -test -e "+savePath+";echo $?").readlines()
if flag == ['0\n']:
    print(os.popen(cmdPath + " dfs -rm -r " + savePath).readlines())

wc.saveAsTextFile(savePath)

无论是有无注释版本的代码,读者只需要将其中一个版本需要更换配置和路径的修改为自己的东西以后,即可使用spark-submit命令提交源代码文件:

#1.进入存储源代码的文件目录
cd ~/code/pyspark
#2.提交应用程序
spark-submit ./wordcount.py
#3.查看是否成功将统计结果存入了目标路径
hdfs dfs -ls /user/ZSX/res/test/

若执行成功,在执行上述语句后会得到形如下面的结果,特别关注有无_SUCCESS文件:

Found 3 items
-rw-r--r--   3 ZSX supergroup          0 2022-10-13 18:15 /user/ZSX/res/test/_SUCCESS
-rw-r--r--   3 ZSX supergroup       3048 2022-10-13 18:15 /user/ZSX/res/test/part-00000
-rw-r--r--   3 ZSX supergroup       2374 2022-10-13 18:15 /user/ZSX/res/test/part-00001