文章目录
0. 前言
这里是我在学习Spark中的一些个人整理的笔记, 所以全文都只是我的个人理解,可能会出现错误,也有可能并不是那么的规范。这里并没有完全写完,因为这是我在学习的过程中写的,后续会不断的补充和修正。
1. Spark环境安装
这里我使用的是三台linux虚拟机集成的环境, 使用的端口号为:
192.168.88.161 node1
192.168.88.162 node2
192.168.88.163 node3
并且在三台虚拟机上都安装了anaconda并创建了虚拟环境, 但是并没有构建Spark集群, 即只在node1上安装了Spark, 这里不做过多的演示.
2. Pyspark入门
2.1 pyspark入门案例
2.1.1 从HDFS上读取文件并实现排序
- 启动hdfs集群
# 启动所有集群, 既然也启动了hdfs集群, 这里只需要在node1上运行即可
start-all.sh
# 确定是否启动成功
jps
- 将需要处理的文件上传到hdfs上
这里是我的需要处理的文件在node1虚拟机上的位置
cd /tmp/pycharm_project_715/day01/data # 这里是我的需要处理的文件所在文件夹的路径
# 前面一个是linux中文件所在路径, 由于在执行该语句之前我们已经cd到了所在文件夹, 所以这里的相对路径就只是word.txt
# 后面一个是要存储到hdfs上的路径
hdfs dfs -put word.txt /data/word.txt
该文件的内容为:
hello you Spark Flink
hello me hello she Spark
spark hadoop hadoop
hive hbase
- 编写pyspark代码处理文件
需求: 需要将这四行数据从hdfs中读取(或在linux本地读取), 并统计单词出现的次数
-
思路
-
- 对文件进行分割, 将每一列以空格为分隔符分割, 并将四行合并到一起
-
- 将单词转化为 (hello, 1) 的形势, 方便后续统计
-
- 将该元组tup[0]相同的元组合并
from pyspark import SparkContext, SparkConf
import os
os.environ['SPARK_HOME'] = '/export/server/spark'
os.environ['PYSPARK_PYTHON'] = '/root/anaconda3/bin/python3'
os.environ['PYSPARK_DRIVER_PYTHON'] = '/root/anaconda3/bin/python3'
os.environ['JAVA_HOME'] = '/export/server/jdk1.8.0_241'
if __name__ == '__main__':
# 1. 创建运行环境
# setMaster设置集群格式, 这里选择的是linux本地执行, 且分配能分配的最多核
# setAppName设置业务名称, 这里根据需求设置即可, 没有固定的值
conf = SparkConf().setMaster('local[*]').setAppName('统计单词')
sc = SparkContext(conf=conf)
# 2. 读取文件
# rdd_file = sc.textFile('file:///tmp/pycharm_project_715/day01/data/word.txt') # 从linux本地中读取文件, 格式为 file://linux虚拟机本地存储的位置
rdd_file = sc.textFile('hdfs://node1:8020/data/word.txt') # 从hdfs中读取文件, 格式为 hdfs://虚拟机地址(这里由于我的电脑配置了地址, 否则需要192.168.88.161):8020/需要读取的文件地址
# 3. 对文件进行处理
# 3.1 对文件进行分割, 将每一列以空格为分隔符分割, 并将四行合并到一起
rdd_flat = rdd_file.flatMap(lambda line: line.split())
# 3.2 将单词转化为(hello, 1)的形势, 方便后续统计
rdd_tup = rdd_flat.map(lambda word: (word, 1))
# 3.3 将该元组tup[0]相同的元组合并
result = rdd_tup.reduceByKey(lambda x, y: x + y)
print(result.collect())
2.1.2 基于Spark-Submit方式运行
上述2.1.1 的入门案例
中的代码是在pycharm中运行的, spark还有一种运行方式, 就是Spark-Submit方式, 这个文件所在位置为/export/server/spark/bin/spark-submit
, 即spark安装位置/bin/spark-submit
# cd切换到spark安装路径的bin目录下
cd /export/server/spark/bin
# 使用spark-submit运行刚刚写的python脚本文件, 格式为:
# spark安装地址/bin/spark-submit --master local[*](这里是设置在linux本地运行, 也可以选择yarn集群运行) python脚本位置
./spark-submit \
--master local[*] \
/tmp/pycharm_project_715/day01/src/01-pyspark-wordcount-hdfs.py
2.2 Spark On Yarn 环境搭建
2.2.1 Spark On Yarn的本质
SparkOnYearn的本质就是把spark应用提交到Yarn,由Yarn进行统一的调度和管理
这种方式是在公司使用比较多的方式
2.2.2 配置 Spark On Yarn
- 修改
spark-env.sh
cd /export/server/spark/conf
cp spark-env.sh.template spark-env.sh
vim /export/server/spark/conf/spark-env.sh
添加以下内容:
HADOOP_CONF_DIR=/export/server/hadoop/etc/hadoop
YARN_CONF_DIR=/export/server/hadoop/etc/hadoop
SPARK_HISTORY_OPTS="-Dspark.history.fs.logDirectory=hdfs://node1:8020/sparklog/ -Dspark.history.fs.cleaner.enabled=true"
同步到其他两台(单节点时候, 不需要处理)
cd /export/server/spark/conf
scp -r spark-env.sh node2:$PWD
scp -r spark-env.sh node3:$PWD
- 修改hadoop的
yarn-site.xml
cd /export/server/hadoop-3.3.0/etc/hadoop/
vim /export/server/hadoop-3.3.0/etc/hadoop/yarn-site.xml
添加以下内容, 将以下内容添加到<configuration> </configuration>中:
<!-- 设置yarn集群的内存分配方案 -->
<property>
<name>yarn.nodemanager.resource.memory-mb</name>
<value>20480</value>
</property>
<property>
<name>yarn.scheduler.minimum-allocation-mb</name>
<value>2048</value>
</property>
<property>
<name>yarn.nodemanager.vmem-pmem-ratio</name>
<value>2.1</value>
</property>
将以上操作同步到其他两台虚拟机中
cd /export/server/hadoop/etc/hadoop
scp -r yarn-site.xml node2:$PWD
scp -r yarn-site.xml node3:$PWD
- Spark设置
历史服务地址
cd /export/server/spark/conf
cp spark-defaults.conf.template spark-defaults.conf
vim spark-defaults.conf
添加以下内容:
spark.eventLog.enabled true
spark.eventLog.dir hdfs://node1:8020/sparklog/
spark.eventLog.compress true
spark.yarn.historyServer.address node1:18080
配置后, 需要在HDFS上创建 sparklog目录
hdfs dfs -mkdir -p /sparklog
设置日志级别:
cd /export/server/spark/conf
cp log4j.properties.template log4j.properties
vim log4j.properties
修改以下内容:(在公司开发中, 日志显示等级为WARN即可, 但是由于这里是学习阶段, 多看看日志, 所以这里我暂时没有改)
# 同步到其他节点(不需要同步, spark只有单节点)
cd /export/server/spark/conf
scp -r spark-defaults.conf log4j.properties node2:$PWD
scp -r spark-defaults.conf log4j.properties node3:$PWD
- 配置依赖
spark jar包
当Spark Application应用提交运行在YARN上时,默认情况下,每次提交应用都需要将依赖Spark相关jar包上传到YARN集群中,为了节省提交时间和存储空间,将Spark相关jar包上传到HDFS目录中,设置属性告知Spark Application应用。
# 在hdfs中创建/spark/jars文件夹, 并将linux本地中spark中的jars文件夹中的所有文件复制到hdfs上
hadoop fs -mkdir -p /spark/jars/
hadoop fs -put /export/server/spark/jars/* /spark/jars/
# 修改spark-defaults.conf
cd /export/server/spark/conf
vim spark-defaults.conf
添加以下内容:
spark.yarn.jars hdfs://node1:8020/spark/jars/*
同步到其他节点, 由于我只在node1上配置了spark, 所以这里不需要执行, 但是如果你搭建了spark集群, 这里也可以配置一下
cd /export/server/spark/conf
scp -r spark-defaults.conf root@node2:$PWD
scp -r spark-defaults.conf root@node3:$PWD
- 启动服务
Spark Application运行在YARN上时,上述配置完成
启动服务:HDFS、YARN、MRHistoryServer和Spark HistoryServer,命令如下:
# 启动HDFS和YARN服务,在node1执行命令
start-dfs.sh
start-yarn.sh
# 或
start-all.sh
# 注意:在onyarn模式下不需要启动start-all.sh(jps查看一下看到worker和master)
# 启动MRHistoryServer服务,在node1执行命令
mr-jobhistory-daemon.sh start historyserver
# 启动Spark HistoryServer服务,,在node1执行命令
/export/server/spark/sbin/start-history-server.sh
Spark HistoryServer服务WEB UI页面地址:
http://node1:18080/
- 测试修改结果
先将圆周率PI程序提交运行在YARN上,命令如下:
SPARK_HOME=/export/server/spark
${SPARK_HOME}/bin/spark-submit \
--master yarn \
--conf "spark.pyspark.driver.python=/root/anaconda3/bin/python3" \
--conf "spark.pyspark.python=/root/anaconda3/bin/python3" \
${SPARK_HOME}/examples/src/main/python/pi.py \
10
2.3 Spark 程序与PySpark交互流程
2.3.1 spark-submit提交任务到spark集群,部署模式是client模式
1 启动Driver
2 向Master申请资源
3 Master向Driver分配资源 返回资源列表
Executor1 : node1 1core 2G
Executor2 : node3 1core 2G
4 连接对应的worker节点,通知它们启动Executor,当worker启动完成后,会反向注册到dirver(通知driver)
5 启动main程序
5.1 启动sparkcontext,构造出sc对象,基于py4j把python程序转成java程序进行执行
5.2 当sc启动完成后,会把后续的rdd相关程序合并在一起,根据RDD的依赖关系,形成一个DAG的执行图。根据DAG可以划分出有几个阶段,每个阶段有多少个线程,每个线程运行在那个executor上(任务分配)
5.3 每个阶段有多少个线程,每个线程运行在那个executor上确定后,把任务分发到对应的executor执行
5.4 executor获取到分发的任务后,开始执行,Task执行完成后把结果返回给driver。前提是有需要返回结果的操作,比如collect,也有操作不需要返回结果比如saveAsTextFile
5.5 当driver获取所有的executor返回的结果之后,开始执行非RDD程序,通知master关闭sc,执行资源回收的工作
2.3.2 spark-submit提交任务到spark集群,部署模式是cluster模式
1 首先把任务提交到主节点
2 Master根据Driver资源信息,在worker节点中随机找一台符合要求的节点,启动Driver,把任务提交Driver
3 对应worker节点接收到任务后,启动Driver,并且和Master保持心跳机制,告知master启动成功,申请资源
4 master根据申请的资源在集群中进行查找,返回资源列表
Executor1 : node1 1core 2G
Executor2 : node3 1core 2G
5 连接对应的worker节点,通知它们启动Executor,当worker启动完成后,会反向注册到dirver(通知driver)
6 启动main程序
6.1 启动sparkcontext,构造出sc对象,基于py4j把python程序转成java程序进行执行
6.2 当sc启动完成后,会把后续的rdd相关程序合并在一起,根据RDD的依赖关系,形成一个DAG的执行图。根据DAG可以划分出有几个阶段,每个阶段有多少个线程,每个线程运行在那个executor上(任务分配)
6.3 每个阶段有多少个线程,每个线程运行在那个executor上确定后,把任务分发到对应的executor执行
6.4 executor获取到分发的任务后,开始执行,Task执行完成后把结果返回给driver。前提是有需要返回结果的操作,比如collect,也有操作不需要返回结果比如saveAsTextFile
6.5 当driver获取所有的executor返回的结果之后,开始执行非RDD程序,通知master关闭sc,执行资源回收的工作
2.3.3 spark-submit提交任务到yarn集群,部署模式是client模式
1 启动Driver
2 连接到yarn主节点(ResurceManager)向主节点提交资源任务(最终启动executor)
3 yarn主节点接收到任务后,会选择一个nodeManager,启动AppMaster,启动后,和ResourceManager建立心跳机制,通知主节点appmaster启动成功
4 appmaster根据提交的资源信息向ResourceManager申请资源,通过心跳方式发送请求,不断向ResoureManager询问
5 一旦appmaster检测到资源准备完成,也会获取资源信息列表,appmaster根据资源信息列表,找到对应的nodeManager启动Executor
6 当Executor启动完成后,会通知appMaster并且通知Driver
7 启动main程序
7.1 启动sparkcontext,构造出sc对象,基于py4j把python程序转成java程序进行执行
7.2 当sc启动完成后,会把后续的rdd相关程序合并在一起,根据RDD的依赖关系,形成一个DAG的执行图。根据DAG可以划分出有几个阶段,每个阶段有多少个线程,每个线程运行在那个executor上(任务分配)
7.3 每个阶段有多少个线程,每个线程运行在那个executor上确定后,把任务分发到对应的executor执行
7.4 executor获取到分发的任务后,开始执行,Task执行完成后把结果返回给driver。前提是有需要返回结果的操作,比如collect,也有操作不需要返回结果比如saveAsTextFile
7.5 当driver获取所有的executor返回的结果之后,开始执行非RDD程序,通知appmaster关闭sc,回收资源
2.3.4 spark-submit提交任务到yarn集群,部署模式是cluster模式
yarn的client模式和cluster模式区别
client下:Driver和app Master是两个不同程序
Driver负责任务分配 监控 管理工作
appMaster负责申请资源启动executor
cluster模式下:Driver和AppMaster合二为一,作用合在一起
3. RDD操作
3.1 Spark Code
3.1.1 RDD的基本介绍
RDD:弹性分布式数据集
早期计算模式是python + mysql, 这只适用于小规模数据, 不适合扩展
随着计算的发展, 数据的规模也在不断变大, 单机模式已经不适用于需求了, 所以出现了分布式技术, 也就是hadoop中的MR(mapreduce)
MR流程:
首先读取数据到内存,对数据进行转换处理,把处理的结果写入环形缓冲区(内存),当缓冲区满了则写入到磁盘。如果有多个缓冲区的内容,则进行合并,最终把结果存储在磁盘中。
这种处理流程,在有限的资源下可以处理大规模数据, 但是使用mr进行数据处理效率非常低, 有大量的磁盘交互工作, 因此出现了高效计算和迭代工具 spark(RDD)
3.1.2 RDD的特性
1 可分区: 每个RDD都必须是可以分区的,每个分区代表一个Task线程,可分区也是分布式的基本要求
2 计算函数会作用在每个分区: 每个RDD都是由一个计算函数得到的,计算函数作用在每个分区上
3 RDD之间存在依赖关系
4 对于kv类型rdd存在分区函数
5 移动存储不如移动计算(让计算和数据更近)
3.1.3 RDD的特点
1 RDD
可分区
2 RDD是只读(不能更改)
3 RDD之间有依赖:宽依赖
,窄依赖
4 RDD每次计算都会重新计算,需要把结果缓存下来,RDD不保存数据
5检查点(checkpoint),相当于对结果进行备份
。当RDD的依赖关系越来越长,为了减少因为计算失败导致回溯效率低问题,不要再检查之前的依赖关系,直接从检查点的位置获取结果即可
3.1.4 如何构建RDD
3.1.4.1 设置pycharm运行模板
在讲述如何创建RDD之前, 我想先讲一下如何在pycharm中设置模板
这里是模板内容
from pyspark import SparkContext, SparkConf
import os
os.environ['SPARK_HOME'] = '/export/server/spark'
os.environ['PYSPARK_PYTHON'] = '/root/anaconda3/bin/python3'
os.environ['PYSPARK_DRIVER_PYTHON'] = '/root/anaconda3/bin/python3'
os.environ['JAVA_HOME'] = '/export/server/jdk1.8.0_241'
if __name__ == '__main__':
print('pyspark模板')
# 初始化spark
conf = SparkConf().setMaster('local[*]').setAppName('APP') # 这里的APP需要根据业务名称修改
sc = SparkContext(conf=conf)
3.1.4.2 构建RDD的两种方法
- 通过并行化的方式把代码中数据构建RDD,测试中使用
# 使用parallelize将代码中数据构建成RDD格式
rdd_init = sc.parallelize([1, 2, 3, 4, 5])
- 通过读取外部数据的方式构建RDD,生产中使用
在之前的样例中读取hdfs或linux本地的word.txt文件时, 就是使用的这种方式
# hdfs
rdd_file = sc.textFile('hdfs://node1:8020/data/word.txt') # 读取hdfs上 /data/word.txt文件
rdd_file = sc.textFile('hdfs://node1:8020/data') # 读取hdfs上 /data 目录下的所有文件
# linux本地
rdd_file = sc.textFile('file:///tmp/data') # 读取linux本地上 /tmp/data目录下的所有文件, 注意, 这里的 /// 最后一个是由于linux的根目录为 / , 所以其他文件都是以/开头的
3.1.5 RDD算子相关操作
3.1.5.1 RDD算子分类
RDD中算子可以分为两类: transformation(转换算子)、action(动作算子)
转换算子:
- 返回的结果都是新的RDD(RDD是只读)
- 所有的转换算子都是惰性的,只有遇到action算子才会真正的执行
- RDD不存储数据,只存储转换的规则,遇到action算子后对数据进行处理
动作算子:
- 返回的结果不是RDD或者没有返回值
- 所有的动作算子都是立即执行的,每个action操作都会触发任务
3.1.5.2 transformation转换算子介绍
- glom, getNumPatitions, collect
glom: 显示如何分区
getNumPatitions: 获取分区数量
collect: 获取整个RDD的完整内容(不显示分区情况)
# 创建rdd, 第二个参数为分区数, 默认为2
rdd_init = sc.parallelize([1, 2, 3, 4, 5, 6], 3)
# 打印rdd_init的分区情况
print('rdd_init的分区情况:', rdd_init.getNumPartitions())
# 打印rdd_init的内容(不显示分区)
print('rdd_init的内容(不显示分区)', rdd_init.collect())
# 打印rdd_init的内容(显示分区)
print('rdd_init的内容(显示分区)', rdd_init.glom().collect())
运行结果:
- map