目录
第一部分 Spark Core
第1节 Spark概述
1.1 什么是Spark
Spark 是一个快速、通用的计算引擎。
Spark的特点:
速度快。
使用简单。
通用。批处理、交互式查询(Spark SQL)、实时流处理(Spark Streaming)、机器学习(Spark MLlib)和图计算(GraphX)。
兼容好。Spark可以使用YARN、Mesos作为它的资源管理和调度器;可以处理所有Hadoop支持的数据,包括HDFS、HBase和Cassandra等。这对于已经部署Hadoop集群的用户特别重要,因为不需要做任何数据迁移就可以使用Spark的强大处理能力。Spark也可以不依赖于第三方的资源管理和调度器,它实现了Standalone作为其内置的资源管理和调度框架,这样进一步降低了Spark的使用门槛,使得所有人都可以非常容易地部署和使用Spark。此外,Spark还提供了在EC2上部署Standalone的Spark集群的工具。
1.2 Spark 与 Hadoop
从狭义的角度上看:Hadoop是一个分布式框架,由存储、资源调度、计算三部分组成;
Spark是一个分布式计算引擎,由 Scala 语言编写的计算框架,基于内存的快速、通用、可扩展的大数据分析引擎;
从广义的角度上看,Spark是Hadoop生态中不可或缺的一部分;
MapReduce的不足:
表达能力有限, 只有map和reduce
磁盘IO开销大
延迟高
- 任务之间的衔接有IO开销
- 在前一个任务执行完成之前,后一个任务无法开始。难以胜任复杂的、多阶段计算任务
Spark在借鉴MapReduce优点的同时,很好地解决了MapReduce所面临的问题。
备注:Spark的计算模式也属于MapReduce;Spark框架是对MR框架的优化;
在实际应用中,大数据应用主要包括以下三种类型:
- 批量处理(离线处理):通常时间跨度在数十分钟到数小时之间
- 交互式查询:通常时间跨度在数十秒到数分钟之间
- 流处理(实时处理):通常时间跨度在数百毫秒到数秒之间
当同时存在以上三种场景时,传统的Hadoop框架需要同时部署三种不同的软件。如:
- MapReduce / Hive 或 Impala / Storm
这样做难免会带来一些问题:
- 不同场景之间输入输出数据无法做到无缝共享,通常需要进行数据格式的转换
- 不同的软件需要不同的开发和维护团队,带来了较高的使用成本
- 比较难以对同一个集群中的各个系统进行统一的资源协调和分配
Spark所提供的生态系统足以应对上述三种场景,即同时支持批处理、交互式查询和流数据处理:
- Spark的设计遵循“一个软件栈满足不同应用场景”的理念(all in one),逐渐形成了一套完整的生态系统
- 既能够提供内存计算框架,也可以支持SQL即席查询、实时流式计算、机器学习和图计算等
- Spark可以部署在资源管理器YARN之上,提供一站式的大数据解决方案
Spark 为什么比 MapReduce 快:
1、Spark积极使用内存。MR框架中一个Job 只能拥有一个 map task 和一个 reduce task。如果业务处理逻辑复杂,一个map和一个reduce是表达不出来的,这时就需要将多个 job 组合起来;然而前一个job的计算结果必须写到HDFS,才能交给后一个job。这样一个复杂的运算,在MR框架中会发生很多次写入、读取操作操作;Spark框架则可以把多个map reduce task组合在一起连续执行,中间的计算结果不需要落地;
复杂的MR任务:mr + mr + mr + mr +mr ...
复杂的Spark任务:mr -> mr -> mr ...
2、多进程模型(MR) vs 多线程模型(Spark)。MR框架中的的Map Task和Reduce Task是进程级别的,而Spark Task是基于线程模型的。MR框架中的 map task、reduce task都是 jvm 进程,每次启动都需要重新申请资源,消耗了不必要的时间。Spark则是通过复用线程池中的线程来减少启动、关闭task所需要的系统开销。
1.3 系统架构
Spark运行架构包括:
- Cluster Manager
- Worker Node
- Driver
- Executor
Cluster Manager 是集群资源的管理者。Spark支持3种集群部署模式:Standalone、Yarn、Mesos;
Worker Node 工作节点,管理本地资源;
Driver Program。运行应用的 main() 方法并且创建了 SparkContext。由Cluster Manager分配资源,SparkContext发送 Task 到 Executor 上执行;
Executor:在工作节点上运行,执行 Driver 发送的 Task,并向 Dirver 汇报计算结果;
1.4 Spark集群部署模式
Spark支持3种集群部署模式:Standalone、Yarn、Mesos;
1、Standalone模式
- 独立模式,自带完整的服务,可单独部署到一个集群中,无需依赖任何其他资源管理系统。从一定程度上说,该模式是其他两种的基础
- Cluster Manager:Master
- Worker Node:Worker
- 仅支持粗粒度的资源分配方式
2、Spark On Yarn模式
- Yarn拥有强大的社区支持,且逐步已经成为大数据集群资源管理系统的标准
- 在国内生产环境中运用最广泛的部署模式
- Spark on yarn 的支持两种模式:yarn-cluster:适用于生产环境 yarn-client:适用于交互、调试,希望立即看到app的输出
- Cluster Manager:ResourceManager
- Worker Node:NodeManager
- 仅支持粗粒度的资源分配方式
3、Spark On Mesos模式
- 官方推荐的模式。Spark开发之初就考虑到支持Mesos
- Spark运行在Mesos上会比运行在YARN上更加灵活,更加自然
- Cluster Manager:Mesos Master
- Worker Node:Mesos Slave
- 支持粗粒度、细粒度的资源分配方式
粗粒度模式(Coarse-grained Mode):每个应用程序的运行环境由一个Dirver和若干个Executor组成,其中,每个Executor占用若干资源,内部可运行多个Task。应用程序的各个任务正式运行之前,需要将运行环境中的资源全部申请好,且运行过程中要一直占用这些资源,即使不用,最后程序运行结束后,回收这些资源。
细粒度模式(Fine-grained Mode):鉴于粗粒度模式会造成大量资源浪费,Spark On Mesos还提供了另外一种调度模式:细粒度模式,这种模式类似于现在的云计算,核心思想是按需分配。
三种集群部署模式如何选择:
- 生产环境中选择Yarn,国内使用最广的模式
- Spark的初学者:Standalone,简单
- 开发测试环境,可选择Standalone
- 数据量不太大、应用不是太复杂,建议可以从Standalone模式开始
- mesos不会涉及到
1.5 相关术语
http://spark.apache.org/docs/latest/cluster-overview.html
- Application 用户提交的spark应用程序,由集群中的一个driver 和 许多executor 组成
- Application jar 一个包含spark应用程序的jar,jar不应该包含 Spark 或 Hadoop的 jar,这些jar应该在运行时添加
- Driver program 运行应用程序的main(),并创建SparkContext(Spark应用程序)
- Cluster manager 管理集群资源的服务,如standalone,Mesos,Yarn
- Deploy mode 区分 driver 进程在何处运行。在 Cluster 模式下,在集群内部运行 Driver。 在 Client 模式下,Driver 在集群外部运行
- Worker node 运行应用程序的工作节点
- Executor 运行应用程序 Task 和保存数据,每个应用程序都有自己的executors,并且各个executor相互独立
- Task executors应用程序的最小运行单元
- Job 在用户程序中,每次调用Action函数都会产生一个新的job,也就是说每个 Action 生成一个job
- Stage 一个 job 被分解为多个 stage,每个 stage 是一系列 Task 的集合
第2节 Spark安装配置
2.1 Spark安装
官网地址:http://spark.apache.org/
文档地址:http://spark.apache.org/docs/latest/
下载地址:http://spark.apache.org/downloads.html
下载Spark安装包
下载地址:https://archive.apache.org/dist/spark/
备注:不用安装scala
安装步骤:
1、下载软件解压缩,移动到指定位置
cd /opt/lagou/software/
tar zxvf spark-2.4.5-bin-without-hadoop-scala-2.12.tgz
mv spark-2.4.5-bin-without-hadoop-scala-2.12/ ../servers/spark-2.4.5/
2、设置环境变量,并使之生效
vi /etc/profile
export SPARK_HOME=/opt/lagou/servers/spark-2.4.5
export PATH=$PATH:$SPARK_HOME/bin:$SPARK_HOME/sbinsource /etc/profile
3、修改配置
文件位置:$SPARK_HOME/conf
修改文件:slaves、spark-defaults.conf、spark-env.sh、log4j.properties
more slaves
linux121
linux122
linux123
more spark-defaults.conf
spark.master spark://linux121:7077
spark.eventLog.enabled true
spark.eventLog.dir hdfs://linux121:9000/spark-eventlog
spark.serializer org.apache.spark.serializer.KryoSerializer
spark.driver.memory 512m # 缺省是 1G
创建 HDFS 目录:hdfs dfs -mkdir /spark-eventlog
备注:
- spark.master。定义master节点,缺省端口号 7077
- spark.eventLog.enabled。开启eventLog
- spark.eventLog.dir。eventLog的存放位置
- spark.serializer。一个高效的序列化器
- spark.driver.memory。定义driver内存的大小(缺省1G)
修改spark-env.sh
export JAVA_HOME=/opt/lagou/servers/jdk1.8.0_231
export HADOOP_HOME=/opt/lagou/servers/hadoop-2.9.2
export HADOOP_CONF_DIR=/opt/lagou/servers/hadoop-2.9.2/etc/hadoop
export SPARK_DIST_CLASSPATH=$(/opt/lagou/servers/hadoop-2.9.2/bin/hadoop classpath)
export SPARK_MASTER_HOST=linux121
export SPARK_MASTER_PORT=7077
备注:这里使用的是 spark-2.4.5-bin-without-hadoop,所以要将 Hadoop 相关 jars 的位置告诉Spark
4、将Spark软件分发到集群;修改其他节点上的环境变量
cd /opt/lagou/servers/
scp -r spark-2.4.5/ linux122:$PWD
scp -r spark-2.4.5/ linux123:$PWD
5、启动集群
cd $SPARK_HOME/sbin
./start-all.sh
分别在linux121、linux122、linux123上执行 jps,可以发现:
linux121:Master、Worker
linux122:Worker
linux123:Worker
此时 Spark 运行在 Standalone 模式下。
在浏览器中输入:http://linux121:8080/
可以看见如下 Spark 的 Web 界面:
备注:在$HADOOP_HOME/sbin 及 $SPARK_HOME/sbin 下都有 start-all.sh 和 stop-all.sh 文件
在输入 start-all.sh / stop-all.sh 命令时,谁的搜索路径在前面就先执行谁,此时会产生冲突。
解决方案:
- 删除一组 start-all.sh / stop-all.sh 命令,让另外一组命令生效
- 将其中一组命令重命名。如:将 $HADOOP_HOME/sbin 路径下的命令重命名为:start-all-hadoop.sh / stop-all-hadoop.sh
- 将其中一个框架的 sbin 路径不放在 PATH 中
6、集群测试
run-example SparkPi 10
spark-shell// HDFS 文件
val lines = sc.textFile("/wcinput/wc.txt")
lines.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_+_).collect().foreach(println)
Spark集群是否一定依赖hdfs?不是的,除非用到了hdfs。
Apache Spark支持多种部署模式。最简单的就是单机本地模式(Spark所有进程都运行在一台机器的JVM中)、伪分布式模式(在一台机器中模拟集群运行,相关的进程在同一台机器上)。分布式模式包括:Standalone、Yarn、Mesos。
Apache Spark支持多种部署模式:
本地模式。最简单的运行模式,Spark所有进程都运行在一台机器的 JVM 中
伪分布式模式。在一台机器中模拟集群运行,相关的进程在同一台机器上(用的非常少)
分布式模式。包括:Standalone、Yarn、Mesos
- Standalone。使用Spark自带的资源调度框架
- Yarn。使用 Yarn 资源调度框架
- Mesos。使用 Mesos 资源调度框架
2.2 本地模式
本地模式部署在单机,主要用于测试或实验;最简单的运行模式,所有进程都运行在一台机器的 JVM 中;
本地模式用单机的多个线程来模拟Spark分布式计算,通常用来验证开发出来的应用程序逻辑上有没有问题;
这种模式非常简单,只需要把Spark的安装包解压后,改一些常用的配置即可使用。不用启动Spark的Master、Worker守护进程,也不用启动Hadoop的服务(除非用到HDFS)。
- local:在本地启动一个线程来运行作业;
- local[N]:启动了N个线程;
- local[*]:使用了系统中所有的核;
- local[N,M]:第一个参数表示用到核的个数;第二个参数表示容许作业失败的次数
前面几种模式没有指定M参数,其默认值都是1;
测试:
1、关闭相关服务
stop-dfs.sh
stop-all.sh
2、启动 Spark 本地运行模式
spark-shell --master local
备注:此时可能有错误。主要原因是配置了日志聚合(即是用来了hdfs,但hdfs服务关闭了),关闭该选项即可
# spark-defaults.conf文件中,注释以下两行:
spark.eventLog.enabled true
spark.eventLog.dir hdfs://linux121:9000/spark-eventlog
3、使用 jps 检查,发现一个 SparkSubmit 进程
这个SparkSubmit进程又当爹、又当妈。既是客户提交任务的Client进程、又是Spark的driver程序、还充当着Spark执行Task的Executor角色。
4、执行简单的测试程序
val lines = sc.textFile("file:///root/a.txt")
lines.count
2.3 伪分布式
伪分布式模式:在一台机器中模拟集群运行,相关的进程在同一台机器上;
备注:不用启动集群资源管理服务;
local-cluster[N,cores,memory]
- N模拟集群的 Slave(或worker)节点个数
- cores模拟集群中各个Slave节点上的内核数
- memory模拟集群的各个Slave节点上的内存大小
备注:参数之间没有空格,memory不能加单位
1、启动 Spark 伪分布式模式
spark-shell --master local-cluster[4,2,1024]
2、使用 jps 检查,发现1个 SparkSubmit 进程和4个 CoarseGrainedExecutorBackend 进程
SparkSubmit依然充当全能角色,又是Client进程,又是Driver程序,还有资源管理的作用。
4个CoarseGrainedExecutorBackend,用来并发执行程序的进程。
3、执行简单的测试程序
spark-submit --master local-cluster[4,2,1024] --class org.apache.spark.examples.SparkPi
$SPARK_HOME/examples/jars/spark-examples_2.11-2.4.5.jar 10
备注:
- local-cluster[4,2,1024],参数不要给太大,资源不够
- 这种模式少用,有Bug。SPARK-32236
2.4 集群模式--Standalone模式
参考:http://spark.apache.org/docs/latest/spark-standalone.html
- 分布式部署才能真正体现分布式计算的价值
- 与单机运行的模式不同,这里必须先启动Spark的Master和Worker守护进程;关闭 yarn 对应的服务
- 不用启动Hadoop服务,除非要使用HDFS的服务
使用jps检查,可以发现:
- linux121:Master、Worker
- linux122:Worker
- linux123:Worker
使用浏览器查看(linux121:8080)
2.4.1 Standalone 配置
- sbin/start-master.sh / sbin/stop-master.sh
- sbin/start-slaves.sh / sbin/stop-slave.sh
- sbin/start-slave.sh / sbin/stop-slaves.sh
- sbin/start-all.sh / sbin/stop-all.sh
备注:./sbin/start-slave.sh [options];启动节点上的worker进程,调试中较为常用
在 spark-env.sh 中定义:
SPARK_WORKER_CORES:Total number of cores to allow Spark applications to use on the machine (default: all available cores).
SPARK_WORKER_MEMORY:Total amount of memory to allow Spark applications to use on the machine, e.g.1000m , 2g (default: total memory minus 1 GiB); note that each application's individual memory is configured using its spark.executor.memory property.
测试在 spark-env.sh 中增加参数,分发到集群,重启服务:
export SPARK_WORKER_CORES=10
export SPARK_WORKER_MEMORY=20g
在浏览器中观察集群状态,测试完成后将以上两个参数分别改为2、2g,重启服务。
2.4.2 运行模式(cluster / client)
最大的区别:Driver运行在哪里;client是缺省的模式,能看见返回结果,适合调试;cluster与此相反;
- Client模式(缺省)。Driver运行在提交任务的Client,此时可以在Client模式下,看见应用的返回结果,适合交互、调试
- Cluster模式。Driver运行在Spark集群中,看不见程序的返回结果,合适生产环境
测试1(Client 模式):
spark-submit --class org.apache.spark.examples.SparkPi \
$SPARK_HOME/examples/jars/spark-examples_2.11-2.4.5.jar 1000
再次使用 jps 检查集群中的进程:
- Master进程做为cluster manager,管理集群资源
- Worker 管理节点资源
- SparkSubmit 做为Client端,运行 Driver 程序。Spark Application执行完成,进程终止
- CoarseGrainedExecutorBackend,运行在Worker上,用来并发执行应用程序
测试2(Cluster 模式):
spark-submit --class org.apache.spark.examples.SparkPi \
--deploy-mode cluster \
$SPARK_HOME/examples/jars/spark-examples_2.11-2.4.5.jar 1000
- SparkSubmit 进程会在应用程序提交给集群之后就退出
- Master会在集群中选择一个 Worker 进程生成一个子进程 DriverWrapper 来启动 Driver 程序
- Worker节点上会启动 CoarseGrainedExecutorBackend
- DriverWrapper 进程会占用 Worker 进程的一个core (缺省分配1个core,1G内存)
- 应用程序的结果,会在执行 Driver 程序的节点的 stdout 中输出,而不是打印在屏幕上
在启动 DriverWrapper 的节点上,进入 $SPARK_HOME/work/,可以看见类似 driver-20200810233021-0000 的目录,这个就是 driver 运行时的日志文件,进入该目录,会发现:
- jar 文件,这就是移动的计算
- stderr 运行日志
- stdout 输出结果
2.4.3 History Server
# spark-defaults.conf
# history server
spark.eventLog.enabled true
spark.eventLog.dir hdfs://linux121:9000/spark-eventlog
spark.eventLog.compress true
# spark-env.sh
export SPARK_HISTORY_OPTS="-Dspark.history.ui.port=18080 -Dspark.history.retainedApplications=50 -Dspark.history.fs.logDirectory=hdfs://linux121:9000/spark-eventlog"
spark.history.retainedApplications。设置缓存Cache中保存的应用程序历史记录的个数(默认50),如果超过这个值,旧的将被删除;
缓存文件数不表示实际显示的文件总数。只是表示不在缓存中的文件可能需要从硬盘读取,速度稍有差别
前提条件:启动hdfs服务(日志写到HDFS)
启动historyserver,使用 jps 检查,可以看见 HistoryServer 进程。如果看见该进程,请检查对应的日志。
$SPARK_HOME/sbin/start-history-server.sh
web端地址:http://linux121:18080/
2.4.4 高可用配置
Spark Standalone集群是 Master-Slaves架构的集群模式,和大部分的Master-Slaves结构集群一样,存着Master单点故障的问题。如何解决这个问题,Spark提供了两种方案:
(1)基于zookeeper的Standby Master,适用于生产模式。将 Spark 集群连接到Zookeeper,利用 Zookeeper提供的选举和状态保存的功能,一个 Master 处于 Active 状态,其他 Master 处于Standby状态;保证在ZK中的元数据主要是集群的信息,包括:Worker,Driver和Application以及Executors的信息如果Active的Master挂掉了,通过选举产生新的 Active 的 Master,然后执行状态恢复,整个恢复过程可能需要1~2分钟;
(2)基于文件系统的单点恢复(Single-Node Rcovery with Local File System),主要用于开发或者测试环境。将Spark Application 和 Worker 的注册信息保存在文件中,一旦Master发生故障,就可以重新启动Master进程,将系统恢复到之前的状态
配置步骤:
1、安装ZooKeeper,并启动
2、修改 spark-env.sh 文件,并分发到集群中
# 注释以下两行!!!
# export SPARK_MASTER_HOST=linux121
# export SPARK_MASTER_PORT=7077
# 添加以下内容
export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=linux121,linux122,linux123 -Dspark.deploy.zookeeper.dir=/spark"
备注:
- spark.deploy.recoveryMode:可选值 Zookeeper、FileSystem、None
- deploy.zookeeper.url:Zookeeper的URL,主机名:端口号(缺省2181)
- deploy.zookeeper.dir:保存集群元数据信息的地址,在ZooKeeper中保存该信息
3、启动 Spark 集群(linux121)
$SPARK_HOME/sbin/start-all.sh
浏览器输入:http://linux121:8080/,刚开始 Master 的状态是STANDBY,稍等一会变为:RECOVERING,最终是:ALIVE
4、在 linux122 上启动master
$SPARK_HOME/sbin/start-master.sh
进入浏览器输入:http://linux122:8080/,此时 Master 的状态为:STANDBY
5、杀掉linux121上 Master 进程,再观察 linux122 上 Master 状态,由 STANDBY => RECOVERING => ALIVE
小结:
- 配置每个worker的core、memory
- 运行模式:cluster、client;client缺省模式,有返回结果,适合调试;cluster与此相反
- History server
- 高可用(ZK、Local Flile;在ZK中记录集群的状态)
2.5 集群模式--Yarn模式
参考:http://spark.apache.org/docs/latest/running-on-yarn.html
需要启动的服务:hdfs服务、yarn服务
需要关闭 Standalone 对应的服务(即集群中的Master、Worker进程),一山不容二虎!
在Yarn模式中,Spark应用程序有两种运行模式:
- yarn-client。Driver程序运行在客户端,适用于交互、调试,希望立即看到app的输出
- yarn-cluster。Driver程序运行在由RM启动的 AppMaster中,适用于生产环境
二者的主要区别:Driver在哪里
1、关闭 Standalon 模式下对应的服务;开启 hdfs、yarn、historyserver 服务
2、修改 yarn-site.xml 配置
在 $HADOOP_HOME/etc/hadoop/yarn-site.xml 中增加,分发到集群,重启 yarn 服务
<property>
<name>yarn.nodemanager.pmem-check-enabled</name>
<value>false</value>
</property>
<property>
<name>yarn.nodemanager.vmem-check-enabled</name>
<value>false</value>
</property>
备注:
- yarn.nodemanager.pmem-check-enabled。是否启动一个线程检查每个任务正使用的物理内存量,如果任务超出分配值,则直接将其杀掉,默认是true
- yarn.nodemanager.vmem-check-enabled。是否启动一个线程检查每个任务正使用的虚拟内存量,如果任务超出分配值,则直接将其杀掉,默认是true
3、修改配置,分发到集群
# spark-env.sh 中这一项必须要有
export HADOOP_CONF_DIR=/opt/lagou/servers/hadoop-2.9.2/etc/hadoop# spark-default.conf(以下是优化)
# 与 hadoop historyserver集成
spark.yarn.historyServer.address linux121:18080# 添加(以下是优化)
spark.yarn.jars hdfs:///spark-yarn/jars/*.jar# 将 $SPARK_HOME/jars 下的jar包上传到hdfs
hdfs dfs -mkdir -p /spark-yarn/jars/
cd $SPARK_HOME/jars
hdfs dfs -put * /spark-yarn/jars/
4、测试
# client
spark-submit --master yarn \
--deploy-mode client \
--class org.apache.spark.examples.SparkPi \
$SPARK_HOME/examples/jars/spark-examples_2.12-2.4.5.jar 2000
在提取App节点上可以看见:SparkSubmit、CoarseGrainedExecutorBackend
在集群的其他节点上可以看见:CoarseGrainedExecutorBackend
在提取App节点上可以看见:程序计算的结果(即可以看见计算返回的结果)
# cluster
spark-submit --master yarn \
--deploy-mode cluster \
--class org.apache.spark.examples.SparkPi \
$SPARK_HOME/examples/jars/spark-examples_2.12-2.4.5.jar 2000
在提取App节点上可以看见:SparkSubmit
在集群的其他节点上可以看见:CoarseGrainedExecutorBackend、ApplicationMaster(Driver运行在此)
在提取App节点上看不见最终的结果
整合HistoryServer服务
前提:Hadoop的 HDFS、Yarn、HistoryServer 正常;Spark historyserver服务正常;
Hadoop:JobHistoryServer
Spark:HistoryServer
spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode client \
$SPARK_HOME/examples/jars/spark-examples_2.12-2.4.5.jar \
20
1、修改 spark-defaults.conf,并分发到集群
# 修改 spark-defaults.conf
spark.master spark://linux121:7077
spark.eventLog.enabled true
spark.eventLog.dir hdfs://linux121:9000/spark-eventlog
spark.serializer org.apache.spark.serializer.KryoSerializer
spark.driver.memory 512m# 新增
spark.yarn.historyServer.address linux121:18080
spark.history.ui.port 18080
2、重启/启动 spark 历史服务
stop-history-server.sh
start-history-server.sh
3、提交任务
spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode client \
$SPARK_HOME/examples/jars/spark-examples_2.12-2.4.5.jar \
100
4、Web页面查看日志(图见上)
备注:
1、在课程学习的过程中,大多数情况使用Standalone模式 或者 local模式
2、建议不使用HA;更多关注的Spark开发
2.6 开发环境搭建IDEA
前提:安装scala插件;能读写HDFS文件
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ch</groupId>
<artifactId>spark_code</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<scala.version>2.12.10</scala.version>
<spark.version>2.4.5</spark.version>
<hadoop.version>2.9.2</hadoop.version>
<encoding>UTF-8</encoding>
</properties>
<dependencies>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<version>${spark.version}</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<!-- 编译scala的插件 -->
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<!-- 编译java的插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<executions>
<execution>
<id>scala-compile-first</id>
<phase>process-resources</phase>
<goals>
<goal>add-source</goal>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>scala-test-compile</id>
<phase>process-test-resources</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 打jar插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
package com.ch.sparkcore
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* @author CH
*/
object WordCount {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local").setAppName("WordCount")
val sc = new SparkContext(conf)
// 使用hdfs文件
//val lines: RDD[String] = sc.textFile("hdfs://linux121:9000/wcinput/wc.txt")
// 使用hdfs文件, 有配置文件 src/main/resources/core-site.xml
val lines: RDD[String] = sc.textFile("/wcinput/wc.txt")
// 使用本地文件
// val lines: RDD[String] = sc.textFile("data/wc.dat")
lines.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_+_).collect().foreach(println)
sc.stop()
}
}
备注:core-site.xml;链接源码;
第3节 RDD编程
3.1 什么是RDD
RDD是 Spark 的基石,是实现 Spark 数据处理的核心抽象。
RDD 是一个抽象类,它代表一个不可变、可分区、里面的元素可并行计算的集合。
RDD(Resilient Distributed Dataset)是 Spark 中的核心概念,它是一个容错、可以并行执行的分布式数据集。
RDD包含5个特征(记住):
1. 一个分区的列表
2. 一个计算函数compute,对每个分区进行计算
3. 对其他RDDs的依赖(宽依赖、窄依赖)列表
4. 对key-value RDDs来说,存在一个分区器(Partitioner)【可选的】
5. 对每个分区有一个优先位置的列表【可选的】
- 一组分片(Partition),即数据集的基本组成单位。对于RDD来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值;
- 一个对分区数据进行计算的函数。Spark中RDD的计算是以分片为单位的,每个RDD都会实现 compute 函数以达到该目的。compute函数会对迭代器进行组合,不需要保存每次计算的结果;
- RDD之间的存在依赖关系。RDD的每次转换都会生成一个新的RDD,RDD之间形成类似于流水线一样的前后依赖关系(lineage)。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算;
- 对于 key-value 的RDD而言,可能存在分区器(Partitioner)。Spark 实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有 key-value 的RDD,才可能有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函数决定了RDD本身的分片数量,也决定了parent RDD Shuffle输出时的分片数量;
- 一个列表,存储每个Partition的优先位置(preferred location)。对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。按照“移动计算不移动数据”的理念,Spark在任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。
3.2 RDD的特点
1、分区
RDD逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个 compute 函数得到每个分区的数据。
如果RDD是通过已有的文件系统构建,则compute函数是读取指定文件系统中的数据,如果RDD是通过其他RDD转换而来,则compute函数是执行转换逻辑将其他RDD的数据进行转换。
2、只读
RDD是只读的,要想改变RDD中的数据,只能在现有的RDD基础上创建新的RDD;
一个RDD转换为另一个RDD,通过丰富的操作算子(map、filter、union、join、reduceByKey… …)实现,不再像MR那样只能写map和reduce了。
RDD的操作算子包括两类:
- transformation。用来对RDD进行转化,延迟执行(Lazy);
- action。用来触发RDD的计算;得到相关计算结果或者将RDD保存的文件系统中;
3、依赖
RDDs通过操作算子进行转换,转换得到的新RDD包含了从其他RDDs衍生所必需的信息,RDDs之间维护着这种血缘关系(lineage),也称之为依赖。
依赖包括两种:
- 窄依赖。RDDs之间分区是一一对应的(1:1 或 n:1)
- 宽依赖。子RDD每个分区与父RDD的每个分区都有关,是多对多的关系(即 n:m)。有shuffle发生
4、缓存
可以控制存储级别(内存、磁盘等)来进行缓存。
如果在应用程序中多次使用同一个RDD,可以将该RDD缓存起来,该RDD只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该RDD的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用。
5、checkpoint
虽然RDD的血缘关系天然地可以实现容错,当RDD的某个分区数据失败或丢失,可以通过血缘关系重建。
但是于长时间迭代型应用来说,随着迭代的进行,RDDs之间的血缘关系会越来越长,一旦在后续迭代过程中出错,则需要通过非常长的血缘关系去重建,势必影响性能。
RDD支持 checkpoint 将数据保存到持久化的存储中,这样就可以切断之前的血缘关系,因为checkpoint后的RDD不需要知道它的父RDDs了,它可以从 checkpoint 处拿到数据。
3.3 Spark编程模型
- RDD表示数据对象
- 通过对象上的方法调用来对RDD进行转换
- 最终显示结果 或 将结果输出到外部数据源
- RDD转换算子称为Transformation是Lazy的(延迟执行)
- 只有遇到Action算子,才会执行RDD的转换操作
要使用Spark,需要编写 Driver 程序,它被提交到集群运行
- Driver中定义了一个或多个 RDD ,并调用 RDD 上的各种算子
- Worker则执行RDD分区计算任务
3.4 RDD的创建
1、SparkContext
SparkContext是编写Spark程序用到的第一个类,是Spark的主要入口点,它负责和整个集群的交互;
如把Spark集群当作服务端,那么Driver就是客户端,SparkContext 是客户端的核心;
SparkContext是Spark的对外接口,负责向调用者提供 Spark 的各种功能;
SparkContext用于连接Spark集群、创建RDD、累加器、广播变量;
在 spark-shell 中 SparkContext 已经创建好了,可直接使用;
编写Spark Driver程序第一件事就是:创建SparkContext;
建议:Standalone模式或本地模式学习RDD的各种算子;
不需要HA;不需要IDEA
2、从集合创建RDD
从集合中创建RDD,主要用于测试。Spark 提供了以下函数:parallelize、makeRDD、range
val rdd1 = sc.parallelize(Array(1,2,3,4,5))
rdd1.collect
// res16: Array[Int] = Array(1, 2, 3, 4, 5)
val rdd2 = sc.parallelize(1 to 100)
// 检查 RDD 分区数
rdd2.getNumPartitions
rdd2.partitions.length
// 创建 RDD,并指定分区数
val rdd2 = sc.parallelize(1 to 100)
rdd2.getNumPartitions
val rdd3 = sc.makeRDD(List(1,2,3,4,5))
val rdd4 = sc.makeRDD(1 to 100)
rdd4.getNumPartitions // res17: Int = 6
val rdd5 = sc.range(1, 100, 3)
rdd5.getNumPartitions // res18: Int = 6
val rdd6 = sc.range(1, 100, 2 ,10)
rdd6.getNumPartitions // res19: Int = 10
val rdd5 = sc.range(1, 100, numSlices = 3)
rdd5.getNumPartitions // res18: Int = 3
备注:rdd.collect 方法在生产环境中不要使用,会造成Driver OOM
3、从文件系统创建RDD
用 textFile() 方法来从文件系统中加载数据创建RDD。方法将文件的 URI 作为参数,这个URI可以是:
- 本地文件系统 (注意:该文件是不是在所有的节点存在(在Standalone模式下))
- 分布式文件系统HDFS的地址
- Amazon S3的地址
// 从本地文件系统加载数据 需要保证 路径文件 真实存在
val lines = sc.textFile("file:///root/data/wc.txt")
// 从分布式文件系统加载数据
val lines = sc.textFile("hdfs://linux121:9000/user/root/data/uaction.dat")
val lines = sc.textFile("/user/root/data/uaction.dat")
val lines = sc.textFile("data/uaction.dat") // 不写前面的路径, 因为配置文件中已经写好
4、从RDD创建RDD
本质是将一个RDD转换为另一个RDD。详细信息参见 3.5 Transformation
3.5 Transformation【重要】
RDD的操作算子分为两类:
- Transformation。用来对RDD进行转化,这个操作时延迟执行的(或者说是 Lazy 的);
- Action。用来触发RDD的计算;得到相关计算结果 或者 将结果保存的外部系统中;
说明:
- Transformation:返回一个新的RDD
- Action:返回结果 可能是 int、double、集合(不会返回新的RDD)
- 要很准确区分Transformation、Action
每一次 Transformation 操作都会产生新的RDD,供给下一个“转换”使用;
转换得到的RDD是惰性求值的。也就是说,整个转换过程只是记录了转换的轨迹,并不会发生真正的计算,只有遇到Action 操作时,才会发生真正的计算,开始从血缘关系(lineage)源头开始,进行物理的转换操作;
常见的 Transformation 算子:
官方文档:http://spark.apache.org/docs/latest/rdd-programming-guide.html#transformations
常见转换算子1
下面全部都是窄依赖, 即处理后分区不变
map(func):对数据集中的每个元素都使用func,然后返回一个新的RDD
filter(func):对数据集中的每个元素都使用func,然后返回一个包含使func为true的元素构成的RDD
flatMap(func):与 map 类似,每个输入元素被映射为0或多个输出元素
mapPartitions(func):和map很像,但是map是将func作用在每个元素上,而mapPartitions是func作用在整个分区上。假设一个RDD有N个元素,M个分区(N >> M),那么map的函数将被调用N次,而mapPartitions中的函数仅被调用M次,一次处理一个分区中的所有元素
mapPartitionsWithIndex(func):与 mapPartitions 类似,多了分区的索引值的信息
val rdd1 = sc.parallelize(1 to 10)
val rdd2 = rdd1.map(_*2) // map是窄依赖, 分区数不会变
val rdd3 = rdd2.filter(_>10)
// 以上都是 Transformation 操作,没有被执行。如何证明这些操作按预期执行,此时需要引入Action算子
rdd2.collect
rdd3.collect
// collect 是Action算子,触发Job的执行,将RDD的全部元素从 Executor 搜集到 Driver 端。生产环境中禁用
// flatMap 使用案例
val rdd4 = sc.textFile("data/wc.txt")
rdd4.collect
// res1: Array[String] = Array(hadoop hdfs hadoop, hdfs lagou hadoop, mapreduce mr yarn, yarn mr mapreduce)
rdd4.flatMap(_.split("\\s+")).collect
// res2: Array[String] = Array(hadoop, hdfs, hadoop, hdfs, lagou, hadoop, mapreduce, mr, yarn, yarn, mr, mapreduce)
// RDD 是分区,rdd1有几个区,每个分区有哪些元素
rdd1.getNumPartitions // res7: Int = 6
rdd1.partitions.length
rdd1.mapPartitions{iter => Iterator(s"${iter.toList}")}.collect
// res9: Array[String] = Array(List(1), List(2, 3), List(4, 5), List(6), List(7, 8), List(9, 10))
rdd1.mapPartitions{iter =>
Iterator(s"${iter.toArray.mkString("-")}")}.collect
// res10: Array[String] = Array(1, 2-3, 4-5, 6, 7-8, 9-10)
// idx代表分区的索引
rdd1.mapPartitionsWithIndex{(idx, iter) =>
Iterator(s"$idx:${iter.toArray.mkString("-")}")}.collect
// res12: Array[String] = Array(0:1, 1:2-3, 2:4-5, 3:6, 4:7-8, 5:9-10)
// 每个元素 * 2
val rdd5 = rdd1.mapPartitions(iter => iter.map(_*2)) // 分区没变
rdd5.collect
// res13: Array[Int] = Array(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
map 与 mapPartitions 的区别
- map:每次处理一条数据
- mapPartitions:每次处理一个分区的数据,分区的数据处理完成后,数据才能释放,资源不足时容易导致OOM
- 最佳实践:当内存资源充足时,建议使用mapPartitions,以提高处理效率
常见转换算子2
groupBy(func):按照传入函数的返回值进行分组。将key相同的值放入一个迭代器
glom():将每一个分区形成一个数组,形成新的RDD类型 RDD[Array[T]]
sample(withReplacement, fraction, seed):采样算子。以指定的随机种子(seed)随机抽样出数量为fraction的数据,withReplacement表示是抽出的数据是否放回,true为有放回的抽样,false为无放回的抽样
distinct([numTasks])):对RDD元素去重后,返回一个新的RDD。可传入numTasks参数改变RDD分区数
coalesce(numPartitions):缩减分区数,无shuffle
repartition(numPartitions):增加或减少分区数,有shuffle
sortBy(func, [ascending], [numTasks]):使用 func 对数据进行处理,对处理后的结果进行排序
宽依赖的算子(有shuffle):groupBy、distinct、repartition、sortBy
// 将 RDD 中的元素按照3的余数分组
val rdd = sc.parallelize(1 to 10)
val group = rdd.groupBy(_%3)
// group: org.apache.spark.rdd.RDD[(Int, Iterable[Int])] = ShuffledRDD[15] at groupBy at <console>:25
group.collect
/ Array[(Int, Iterable[Int])] = Array((0,CompactBuffer(6, 9, 3)), (1,CompactBuffer(1, 4, 10, 7)), (2,CompactBuffer(5, 2, 8)))
// 将 RDD 中的元素每10个元素分组
val rdd = sc.parallelize(1 to 101)
// res17: Array[Int] = Array(1, 2, ... 101)
rdd.glom.map(_.sliding(10, 10).toArray)
// res18: org.apache.spark.rdd.RDD[Array[Array[Int]]] = MapPartitionsRDD[20] at map at <console>:26
// sliding是Scala中的方法
// 对数据采样。fraction采样的百分比,近似数
// 有放回的采样,使用固定的种子
rdd.sample(true, 0.2, 2).collect
// res26: Array[Int] = Array(2, 4, 5, 7, 9, 15, 33, 37, 37, 38, 39, 41, 44, 44, 51, 52, 57, 61, 69, 69, 70, 81, 86)
// 无放回的采样,使用固定的种子
rdd.sample(false, 0.2, 2).collect
// res27: Array[Int] = Array(1, 4, 11, 12, 15, 31, 37, 41, 42, 44, 45, 50, 51, 56, 58, 59, 69, 70, 71, 74, 75, 78, 84, 86, 89, 92)
// 有放回的采样,不设置种子
rdd.sample(false, 0.2).collect
// res28: Array[Int] = Array(1, 2, 5, 11, 16, 18, 25, 26, 28, 29, 33, 39, 41, 52, 62, 67, 73, 74, 80, 89, 96, 98, 100)
// 数据去重 随机产生30个15以内整数
val random = scala.util.Random
val arr = (1 to 20).map(x => random.nextInt(10))
// arr: scala.collection.immutable.IndexedSeq[Int] = Vector(3, 0, 6, 5, 0, 8, 7, 2, 9, 1, 6, 3, 1, 8, 2, 9, 1, 1, 1, 4)
val rdd = sc.makeRDD(arr)
rdd.distinct.collect
// res29: Array[Int] = Array(0, 6, 1, 7, 8, 2, 3, 9, 4, 5)
// RDD重分区
val rdd1 = sc.range(1, 10000, numSlices=10)
val rdd2 = rdd1.filter(_%2==0)
rdd2.getNumPartitions
// res30: Int = 10
// 减少分区数;都生效了
val rdd3 = rdd2.repartition(5)
rdd3.getNumPartitions
// res32: Int = 5
val rdd4 = rdd2.coalesce(5)
rdd4.getNumPartitions
// res33: Int = 5
// 增加分区数
val rdd5 = rdd2.repartition(20)
rdd5.getNumPartitions
// res34: Int = 20
// 增加分区数,这样使用没有效果
val rdd6 = rdd2.coalesce(20)
rdd6.getNumPartitions
// res35: Int = 10
// 增加分区数的正确用法
val rdd6 = rdd2.coalesce(20, true)
rdd6.getNumPartitions
// res36: Int = 20
// RDD元素排序
val random = scala.util.Random
val arr = (1 to 20).map(x => random.nextInt(10))
val rdd = sc.makeRDD(arr)
rdd.collect
// res37: Array[Int] = Array(3, 0, 2, 9, 8, 3, 6, 9, 2, 7, 8, 1, 8, 3, 0, 4, 0, 6, 3, 0)
// 数据全局有序,默认升序
rdd.sortBy(x=>x).collect
// res38: Array[Int] = Array(0, 0, 0, 0, 1, 2, 2, 3, 3, 3, 3, 4, 6, 6, 7, 8, 8, 8, 9, 9)
// 降序
rdd.sortBy(x=>x,false).collect
// res39: Array[Int] = Array(9, 9, 8, 8, 8, 7, 6, 6, 4, 3, 3, 3, 3, 2, 2, 1, 0, 0, 0, 0)
coalesce 与 repartition 的区别 (表面上: 有无shuffle)
小结:
- repartition:增大或减少分区数;有shuffle
- coalesce:一般用于减少分区数(此时无shuffle)
常见转换算子3
RDD之间的交、并、差算子,分别如下:
- intersection(otherRDD)
- union(otherRDD)
- subtract (otherRDD)
cartesian(otherRDD):笛卡尔积
zip(otherRDD):将两个RDD组合成 key-value 形式的RDD,默认两个RDD的partition数量以及元素数量都相同,否则会抛出异常。
宽依赖的算子(有shuffle):intersection、subtract
val rdd1 = sc.range(1, 21)
val rdd2 = sc.range(10, 31)
rdd1.intersection(rdd2).sortBy(x=>x).collect
// res40: Array[Long] = Array(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
// 元素求并集,不去重
rdd1.union(rdd2).sortBy(x=>x).collect
// res41: Array[Long] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30)
rdd1.subtract(rdd2).sortBy(x=>x).collect
// res42: Array[Long] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9)
// 检查分区数
rdd1.intersection(rdd2).getNumPartitions
// res6: Int = 6
rdd1.union(rdd2).getNumPartitions
// res7: Int = 12
rdd1.subtract(rdd2).getNumPartitions
// res8: Int = 6
// 笛卡尔积
val rdd1 = sc.range(1, 5) // 不包含5
val rdd2 = sc.range(6, 10) // 不包含10
rdd1.cartesian(rdd2).collect
// res9: Array[(Long, Long)] = Array((1,6), (1,7), (1,8), (1,9), (2,6), (2,7), (2,8), (2,9), (3,6), (3,7), (3,8), (3,9), (4,6), (4,7), (4,8), (4,9))
// 检查分区数
rdd1.cartesian(rdd2).getNumPartitions
// res10: Int = 36 // ?为什么变成36个分区??
// 拉链操作
rdd1.zip(rdd2).collect
// res11: Array[(Long, Long)] = Array((1,6), (2,7), (3,8), (4,9))
rdd1.zip(rdd2).getNumPartitions
// res12: Int = 6
// zip操作要求:两个RDD的partition数量以及元素数量都相同,否则会抛出异常
val rdd2 = sc.range(6, 20)
rdd1.zip(rdd2).collect
// org.apache.spark.SparkException: Can only zip RDDs with same number of elements in each partition
备注:
- union是窄依赖。得到的RDD分区数为:两个RDD分区数之和
- cartesian也是窄依赖。得到的RDD分区数为:两个RDD分区数之积;得到RDD的数据量是两个RDD数据量的积。会有很严重的数据膨胀,慎用