文章目录
- Spark学习笔记
-
- 第一章、基本认识与快速上手
- 第二章、环境搭建
- 第三章、Spark运行架构
- 第四章、Spark核心编程
-
- 4.1、RDD
- 4.2、RDD基础编程
-
- 4.2.1、RDD创建
- 4.2.2、分区与并行度
- 4.2.3、RDD算子
- 4.2.4、转换算子
-
- 4.2.4.1、mapPartition
- 4.2.4.2、mapPartitionsWithIndex
- 4.2.4.3、glom
- 4.2.4.4、groupBy
- 4.2.4.5、filter
- 4.2.4.6、sample
- 4.2.4.7、distinct
- 4.2.4.8、coalesce
- 4.2.4.9、sortBy
- 4.2.4.10、交集、并集、差集、Zip
- 4.2.4.11、partitionBy
- 4.2.4.12、reduceByKey
- 4.2.4.13、groupByKey
- 4.2.4.14、aggregateByKey
- 4.2.4.15、foldByKey
- 4.2.4.16、CombineByKey
- 4.2.4.17、Join
- 4.2.4.18、Left(Right)OuterJoin
- 4.2.4.19、cogroup
- 4.2.5、行动算子
- 4.2.6、算子小结
- 4.2.7、闭包检查和序列化
- 4.2.8、闭包检查(二)
- 4.2.8、闭包检查(二)
- 4.2.9、Kryo序列化框架
Spark学习笔记
ohhhhh~从今天开始就要开始学习大数据中的杀手级计算引擎——Spark。了解大数据的不可能没有听说个Spark吧!听这个名字就感觉很炫酷哦!!火花!下面正式进入学习吧!
第一章、基本认识与快速上手
Spark官网:http://spark.apache.org/
1.1、认识Spark
我们在学习Hadoop的时候,在网上就能看到很多Spark和Hadoop的比较视频,并且明显可以看出Spark深受互联网大厂的青睐,不少互联网大数据公司都开始使用Spark作为其主流计算引擎,那么Spark究竟为什么俘获企业的喜爱呢?还是从它官网的那句slogan说起!
Lightning-fast unified analytics engine(闪电般的统一分析引擎!)
回想一下我们学习Hadoop时,MR任务的执行慢到宁人抓狂!那么Spark则是抓住了Hadoop这个弱点狠狠下了一顿功夫进行了优化!所以Spark常用的定义就是:
基于内存的快速、通用、可扩展的大数据分析计算引擎
一看到基于内存,我们就知道Spark快在哪里了!
1.2、对比Hadoop
Hadoop:
Hadoop起源于Apache Nutch项目,始于2002年,是Apache Lucene的子项目之一 。2004年,Google在“操作系统设计与实现”(Operating System Design and Implementation,OSDI)会议上公开发表了题为MapReduce:Simplified Data Processing on Large Clusters(Mapreduce:简化大规模集群上的数据处理)的论文之后,受到启发的Doug Cutting等人开始尝试实现MapReduce计算框架,并将它与NDFS(Nutch Distributed File System)结合,用以支持Nutch引擎的主要算法 。由于NDFS和MapReduce在Nutch引擎中有着良好的应用,所以它们于2006年2月被分离出来,成为一套完整而独立的软件,并被命名为Hadoop。到了2008年年初,hadoop已成为Apache的顶级项目,包含众多子项目,被应用到包括Yahoo在内的很多互联网公司
内容来源:百度百科
Spark:
Spark是UC Berkeley AMP lab (加州大学伯克利分校的AMP实验室)所开源的类Hadoop MapReduce的通用并行框架,Spark,拥有Hadoop MapReduce所具有的优点;但不同于MapReduce的是——Job中间输出结果可以保存在内存中,从而不再需要读写HDFS,因此Spark能更好地适用于数据挖掘与机器学习等需要迭代的MapReduce的算法。
内容来源:百度百科
两者如今都是Apache软件基金会的顶级项目!但是也不免有人会将他们放在一起比较,甚至他们的之间也会使用数据进行相互比较。相爱相杀~
通过百度百科对Spark的描述,我们就已经能够清晰了解到Spark究竟快在哪里。我们现在再来复习一下Hadoop的Job执行流程,用画图的方式来学习Spark优化之处!
这是一个Job的执行的大致流程:
中间有两次磁盘的IO操作,前者是Map的Shuffle结果序列化到磁盘中),后者是将一个Job的计算结果存入了磁盘!这两次磁盘IO操作就会大大降低整个Job的效率!并且复杂的MR也会带来一定的时间开销!并且将Job的结果存入磁盘,就注定了在多计算任务复杂需要串联多个Job时,就会无故增加IO操作的次数!这也就是为什么MapReduce不适合做迭代式计算的原因!
所以我们称MapReduce是基于磁盘的计算引擎!
而Spark针对上述一系列进行了重新设计和优化,优化了MapReduce的操作使用RDD计算模型,并且可以将Shuffle的结果保存在内存中,Job的计算结果也保存在内存中,这样的话读写速度大大提升,也更方便迭代式计算!所以Spark的场景定位就不同于Hadoop!
我们称Spark是基于内存的计算引擎!
Spark比Hadoop快的主要原因有:
消除了冗余的HDFS读写
Hadoop每次shuffle操作后,必须写到磁盘,而Spark在shuffle后不一定落盘,可以cache到内存中,以便迭代时使用。如果操作复杂,很多的shufle操作,那么Hadoop的读写IO时间会大大增加。消除了冗余的MapReduce阶段
Hadoop的shuffle操作一定连着完整的MapReduce操作,冗余繁琐。而Spark基于RDD提供了丰富的算子操作,且reduce操作产生shuffle数据,可以缓存在内存中。JVM的优化
Spark Task的启动时间快。Spark采用fork线程的方式,Spark每次MapReduce操作是基于线程的,只在启动。而Hadoop采用创建新的进程的方式,启动一个Task便会启动一次JVM。
Spark的Executor是启动一次JVM,内存的Task操作是在线程池内线程复用的。
每次启动JVM的时间可能就需要几秒甚至十几秒,那么当Task多了,这个时间Hadoop不知道比Spark慢了多少。
但是要说明一下的是:**Spark并不是MapReduce的高阶替代品,他和MapReduce的关系更像是互补!**因为Spark也有使用的局限:由于Spark是基于内存的,所以对内存资源要求严格,当资源不足时可能会导致计算任务无法完成,而此时对机器性能要求不那么高的MapReduce就是不二选择!
1.3、Spark组成基本介绍
spark的官网有这样一幅图:

很明显标注出了Spark的五个模块,以及他们之间的关系!
- Apache Spark(Spark Core):这是我们将要学习的Spark,他是Spark的核心部分!其余四个模块都是基于SparkCore进行扩展!
- SparkSQL:用于操作结构化数据!
- SparkStreaming:主要用于实时计算!
- SparkMLlib:主要用于machine learning(机器学习)!
- SparkGraphx:主要用于图像的计算处理
1.4、快速上手之WorldCount实现
前置准备
-
IDEA(集成开发环境,IDE)
-
创建maven工程,导入Spark3.0依赖
<dependencies> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-core_2.12</artifactId> <version>3.0.0</version> </dependency> </dependencies>
这里注意,使用的Scala版本应该是Scala-2.12!!版本要对应哦!
-
本地需要配置Hadoop,以及winutil.exe相关文件(否则程序启动会报ERROR,但并不影响计算结果!)
使用Spark的基本步骤!
- 创建配置类
SparkConf
,可以使用各种set方法,对配置进行修改。 - 创建与Spark连接
SparkContext
- 业务操作。。(略)
- 使用SparkContext的
stop()
方法关闭与Spark的连接!
object WordCountDemo {
def main(args: Array[String]): Unit = {
// 1. 建立连接
// 配置设置
val sparkConf = new SparkConf().setMaster("local").setAppName("WordCount")
// 连接
val context = new SparkContext(sparkConf)
// 2. 业务操作
// 3. 关闭连接
context.stop()
}
}
下面wordcount的计算处理过程就放在步骤3中来完成!
简要回顾WordCount的问题处理过程(复习MapReduce手动实现WordCount)
- 读取文件(逐行读取)
- 将行数据按照设定的分隔符拆分成一个个单词
- 将单词设为Key,进行分组排序
- 每组统计单词出现次数即Value
- 结果输出
数据:
words1.txt=>
Hello Spark
Hello Scala
Hello World
words2.txt=>
Hello Scala
Hello Spark
Spark Hello
1.4.1、方式一(Scala类似集合操作实现)
完整代码:
object WordCountDemo {
def main(args: Array[String]): Unit = {
// 1. 建立连接
// 配置设置
val sparkConf = new SparkConf().setMaster("local").setAppName("WordCount")
// 连接
val context = new SparkContext(sparkConf)
// 2. 业务操作
// 1)读取文件,获取到一行行的数据
val lines = context.textFile("datas/*")
// 2) 行数据扁平化,获取到一个个单词
val words = lines.flatMap(_.split(" "))
// 3) 将单词分类放置,得到类似(hello, hello, hello)(scala, scala)
val wordGroups = words.groupBy(word => word)
// 4) 改变形式,得到(hello, 4)的样子
val result = wordGroups.collect {
case (word, list) => (word, list.size)
}
// 5) 结果输出到控制台
result.foreach(println)
// 3. 关闭连接
context.stop()
}
}
结果:
(Hello,6)
(World,1)
(Scala,2)
(Spark,3)
里面有一个groupBy
是我在学习Scala过程中没有涉及到的,所以我将其相关的使用点补充到Scala的学习笔记中:11.2.9、groupBy!
上面这种写法虽然实现了目标功能,但是会感觉我们并没有做MR中Reduce端类似的聚合操作,而是取巧使用了Scala集合中size这个属性获取次数!
而正确的执行过程应该是若干个<word, 1>
进行聚合得到<word, n>
1.4.2、方式二(MR思维实现)
利用MR思维,使用聚合方式的话,我们就不能用size方法了,并且扁平化后得到的单词也要封装成Tuple2(word, 1)
。分组后,使用reduce()
聚合,得到最终的结果(word, n)
object WordCountDemo2 {
def main(args: Array[String]): Unit = {
// 1. 建立连接
// 配置设置
val sparkConf = new SparkConf().setMaster("local").setAppName("WordCount")
// 连接
val context = new SparkContext(sparkConf)
// 2. 业务操作
// 1)读取文件,获取到一行行的数据
val lines = context.textFile("datas/*")
// 2) 行数据扁平化,获取到一个个单词
val words = lines.flatMap(_.split(" "))
// 将单词封装成(word, 1)
val wordTuples = words.map((_,1))
// 3) 将单词分类放置,得到类似 hello => ((hello, 1), (hello, 1), (hello, 1))
val wordGroups = wordTuples.groupBy(_._1)
// 4) 聚合,得到(hello, 4)的样子
val result = wordGroups.collect {
case (word, wordList) => wordList.reduce{
(word1, word2) => (word1._1, word1._2 + word2._2)
}
}
// 5) 结果输出到控制台
result.foreach(println)
// 3. 关闭连接
context.stop()
}
}
我们增加了一个包装word的步骤,并修改了后面求结果的方式!我们用一张图来对比一下!
可以好好看看代码在最后聚合时是如何计算出最后的结果的!
但是这样写依旧不满意!这里面貌似除了这些集合是RDD相关的,并且其使用的方法和Scala中集合的方法没什么不同,我们还没有看到Spark的影子!我们希望利用Spark的方式实现!那么就要用到特殊方法啦!
1.4.3、方式三(Spark实现)
object WordCountDemo3 {
def main(args: Array[String]): Unit = {
// 1. 建立连接
// 配置设置
val sparkConf = new SparkConf().setMaster("local").setAppName("WordCount")
// 连接
val context = new SparkContext(sparkConf)
// 2. 业务操作
// 1)读取文件,获取到一行行的数据
val lines = context.textFile("datas/*")
// 2) 行数据扁平化,获取到一个个单词
val words = lines.flatMap(_.split(" "))
// 将单词封装成(word, 1)
val wordTuples = words.map((_,1))
// 使用Spark reduceByKey()方法
val result = wordTuples.reduceByKey(_+_)
// 5) 结果输出到控制台
result.foreach(println)
// 3. 关闭连接
context.stop()
}
}
一个reduceByKey()
直接解决了分组和聚合!官方的源码注释是说**利用这个函数,对相同Key的数值进行一次Merge 在mapper发送数据到reducer之前!**说明这里已经用到了MapReduce来执行任务,而这个函数的作用只是类似于我们学习Hadoop-MapReduce时所说的Mapper阶段的combine!在Mapper阶段结果进行一次合并!
reduceByKey()
所处理的是具有相同key的键值对的value部分!所以我们使用(_+_)
就表示value累加。
可以看出来,使用了Spark的特有方法无论是对代码的简洁性,还是执行的效率都有一定的提升!
题外话:Spark日志问题
使用默认的Log4j的配置,会输出很多无关的INFO日志,我们只需要看ERROR日志就行了,所以我使用一下配置就行了(在resource目录下创建log4j.properties粘贴一下内容即可)
log4j.rootCategory=ERROR, console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n
# Set the default spark-shell log level to WARN. When running the spark-shell, the
# log level for this class is used to overwrite the root logger's log level, so that
# the user can have different defaults for the shell and regular Spark apps.
log4j.logger.org.apache.spark.repl.Main=WARN
# Settings to quiet third party logs that are too verbose
log4j.logger.org.spark_project.jetty=WARN
log4j.logger.org.spark_project.jetty.util.component.AbstractLifeCycle=ERROR
log4j.logger.org.apache.spark.repl.SparkIMain$exprTyper=INFO
log4j.logger.org.apache.spark.repl.SparkILoop$SparkILoopInterpreter=INFO
log4j.logger.org.apache.parquet=ERROR
log4j.logger.parquet=ERROR
# SPARK-9183: Settings to avoid annoying messages when looking up nonexistent UDFs in SparkSQL with Hive support
log4j.logger.org.apache.hadoop.hive.metastore.RetryingHMSHandler=FATAL
log4j.logger.org.apache.hadoop.hive.ql.exec.FunctionRegistry=ERROR
第二章、环境搭建
上面我们都是使用的IDE,在集成开发环境下我们可以开发并运行环境,但是工作中我们的Spark程序一定是放服务器集群中运行的!在国内工作中主流的开发环境是Yarn,但是现在容器式环境逐渐流行!
除此以外还有Local(本地模式)、Standalone(独立模式)
前置准备
- 虚拟机
- Spark安装包
2.1、Local模式
本地模式我们也可以理解为单机模式,没有集群的概念,不需要其他任何节点资源就可以在本地运行Spark的代码!(区别于IDEA中的local,idea中只是为项目创建了开发环境,并没有为机器创建。。)
搭建步骤
-
解压tgz包,到指定文件夹下,并进入spark目录
-
bin/spark-shell
启动spark的命令行模式一些关键点,图中已经用红框标了出来!
-
我们开发中所用的
SparkContext
在命令行中已经为我们预先准备好了:即sc
-
给了一个SparkContext浏览的web页面,可以通过
4040
端口访问!就是一个Job的监控页面
-
scala>
命令行,表明里面可以直接写scala的代码,也就是说我们可以直接在命令行里面完成Spark程序并运行!
使用jps命令可以查看到
spark-shell
是一个SparkSubmit进程,也就是我们可以使用这种方式向本地Spark提交并运行我们的Spark程序(即Job) -
2.1.1、SparkShell命令行执行
使用命令行运行一次Spark程序(以WC为例):
-
在spark的data目录下创建我们的数据文件:words.txt
-
在命令行中编写程序代码:
2.1.2、spark-sublime提交任务
命令行方式在实际开发中并不会使用,正确的姿势应该是**使用集成开发环境完成Spark程序的开发和调试,然后将程序打包,然后扔到服务器上提交执行!**下面我们利用Spark的示例Jar包来测试一下程序的提交。
使用spark-submit
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master local \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10
运行结果:
这是Spark官方提供的案例程序,暂且不关心其正确性,但是确实能够正确运行!
2.1.3、提交任务的参数说明
bin/spark-sublimt \
--class xxx.xxx.xx \
--master xx \
[ApplicationJar] \
[ApplicationParam] \
[Others]
参数 | 说明 |
---|---|
--class |
Spark程序主函数所在类 |
--master |
Spark程序的运行模式(环境)【local】、【spark://xxxx:7077】、【Yarn】 |
--executor-memory 1G |
每个executor可用的内存大小1G |
--total-executor-cores n |
所有的executor共可用的CPU核心数量n |
--executor-cores n |
每个executor可以用的CPU核心数n |
[ApplicationJar] |
Spark程序所在的Jar包 |
[ApplicationParam] |
Spark程序运行时主函数的参数 |
以上是本地模式的搭建和使用,较适合学习和教学。
2.2、Standalone模式
此模式又称为独立部署模式,即不需要其他额外环境(自己提供计算资源),但是有鲜明的主从关系,这里需要用到三台虚拟机,他们分别担任的角色是:
Host | 角色 |
---|---|
hadoop102 | Master、Worker |
hadoop103 | Worker |
hadoop104 | Worker |
从Local模式转换到Standalone模式需要修改一些配置文件!
2.2.1、配置改动与启动
conf/slaves
- 和以往一样,将template文件后缀去掉
- 文件原始内容是localhost,我们需要改为我们三台机器的host名(注意/etc/hosts文件中的映射)
conf/spark-evn.sh
-
第一步相同操作
-
添加
export JAVA_HOME=xxx
配置本机的Jdk环境 -
设置集群master节点
SPARK_MASTER_HOST=hadoop102 # 7077是Spark中默认的Master的通信接口 SPARK_MASTER_PORT=7077
这里参考自己集群的配置。。
修改结束后,将整个Spark安装文件分发给其他两台机器!(不知道分发的,去看Hadoop的笔记)
启动与检查
在master节点下执行:sbin/start-all.sh
,群起。
使用jps查看进程:
然后访问master_host1:8080,(hadoop102:8080)页面可以看到当前集群的状态!
测试提交任务
此时提交任务所用的环境不再是local,而是spark://master_host1:7077
所以要改写成:--master spark://hadoop102:7077
没有问题!正常运行!
使用sbin/stop-all.sh
关闭Spark集群!
2.2.2、历史服务开启
之前我们使用spark-shell时,我们访问的job监控页面,当shell退出后就无法访问了再次启动就是全新的,但是我们希望能随时对历史Job的执行进行查看,那么就需要配置历史服务器(Hadoop中也做过类似的配置!)
启动历史服务的话,就涉及到历史记录日志的存放,所以这里还需要用到Hadoop集群,利用HDFS存放日志文件!
Spark配置修改
conf/spark-default.conf
-
还是去掉template后缀
-
增加以下配置:
spark.eventLog.enabled true spark.eventLog.dir hdfs://hadoop102:9000/directory
注意spark.eventLog.dir对应的是HDFS中的路径,记不清HDFS端口号的,查看hadoop安装目录下etc/hadoop/core-site.xml文件中NameNode配置
下面的我hdfs的配置:
<!--指定HDFS中NameNode的地址--> <property> <name>fs.defaultFS</name> <value>hdfs://hadoop102:9000</value> </property>
并且要保证配置中这个hdfs目录是已创建的!
所以要启动一下Hadoop集群(启动HDFS应该就够了)!检查一下!
conf/spark-env.sh
-
加入以下内容:
export SPARK_HISTORY_OPTS=" -Dspark.history.ui.port=18080 -Dspark.history.fs.logDirectory=hdfs://hadoop102:9000/directory -Dspark.history.retainedApplications=30"
解释以下这三条配置:
- 历史服务器的web页面访问端口:8080
- 历史服务的日志文件放置的位置:hdfs://hadoop102:9000/directory
- 历史服务中保留历史Spark程序的数量:30
启动检查
修改结束后,分发以下conf目录
然后确保HDFS服务器已经启动!然后启动Spark集群,最后启动历史服务:
sbin/start-history-server.sh
任意执行一个Spark程序,然后通过hadoop102:18080,访问web页面检查历史服务是否生效
sbin/stop-history-server.sh
关闭历史服务!
2.2.3、配置高可用
目前我们的Spark集群,只有一个master节点,就会出现单点故障(即一个节点出错,会导致整个系统不可用)的问题!所以最简单的办法就是再增设一个master节点作为后备!
Host | 角色 |
---|---|
hadoop102 | Master、Worker |
hadoop103 | Worker、Master[备用] |
hadoop104 | Worker |
而这个配置,需要zookeeper参与(通常高可用的配置都离不开zookeeper!)!
配置修改
spark.env.sh
-
将我们之间单节点master的配置注释掉,然后加入以下内容
# 单节点Master配置注释掉 #SPARK_MASTER_HOST=hadoop102 #SPARK_MASTER_PORT=7077 # 加入以下内容,这里集群的web页面端口不使用默认8080,担心会被zookeeper占用!改使用8989 # zookeeper.url配置集群中所有节点的host SPARK_MASTER_WEBUI_PORT=8989 export SPARK_DAEMON_JAVA_OPTS=" -Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=hadoop102,hadoop103,hadoop104 -Dspark.deploy.zookeeper.dir=/spark"
启动检查
分发conf/目录!!
在启动前,先启动zookeeper集群!!
然后启动Spark集群,这样我们只是按常规启动了Spark集群,我们的备用的master(hadoop103中)依旧没有启动,我们需要到对应的机器下,手动单独启动master进程!sbin/start-master.sh
为确保正确性,可以使用zkCli.sh连接zookeeper集群,
看看/spark这个node有没有被创建!/spark/leader_election下应该有两个node才对!分别对应两个master节点
此时我们访问hadoop102:8989、hadoop103:8989都是没有问题的。
测试提交任务
此时--master
的参数又要变化了:应改写为
--master spark://hadoop102:7077,hadoop103:7077
没有问题!
这里我犯傻遇到一个问题,我们之前设置了将日志写到hdfs中,所以如果不启动hdfs集群的话会导致Job执行失败!!(启动hdfs集群后,也不要急着启动Job,因为safemode下是不能文件创建的,很可能也会失败!等待safemode关闭后再提交Job即可!)
模拟master宕机,测试高可用
使用kill -9 xx
关闭正在正常运行的master进程,看看是否备用的master是否会被选举上位。
这个选举上位的过程需要耐心等待~ 最终实测可用!
2.3、Yarn模式
Yarn模式在工作中是最常用的部署模式!在开始介绍Spark的时候就说明了它是一个计算引擎!虽然他可以不依赖第三方框架通过独立部署就能完成整个计算资源调度。但是终归是一个计算框架,在资源调度方面并不如专业框架!所以还是那句话:**专业的事情让专业的人去做!**Hadoop的Yarn就是资源调度方面的高手!
那么此时我们将利用Spark只是将其作为计算引擎,替代MapReduce。这样一来Hadoop、Spark混合部署,各司其职、各尽其用!
搭建步骤
Hadoop配置文件:yarn-site.xml
加入以下配置:
<!-- 关闭任务物理内存使用检查,默认(true)开启 -->
<property>
<name>yarn.nodemanager.pmem-check-enabled</name>
<value>false</value>
</property>
<!-- 关闭任务虚拟内存使用检查,默认(true)开启 -->
<property>
<name>yarn.nodemanager.vmem-check-enabled</name>
<value>false</value>
</property>
说过Spark在计算时占用的内存资源很多,所以为了避免Yarn误杀了计算任务,我们关闭了物理内存和虚拟内存的检查!
spark-env.sh
现在我们只要Spark的计算功能,之前那些什么Worker、Master都是用于资源调度的,现在我们都可以不要了!并且只需要安装在一台机器上就行了,我们需要的是Spark的一些基本配置,以及它计算相关的jar包!
将Standalone的配置内容注释掉,保留历史服务和Java环境的配置,然后加入以下配置:
# 配置Yarn的配置目录,以自己机器为准
YARN_CONF_DIR=/opt/modules/hadoop-2.7.7/etc/hadoop
启动测试
这次我们不分发,也不用启动Spark,如果需要我们仅需启动Spark的历史服务!
在此之前要保证Hadoop集群完全启动(HDFS、Yarn都启动!)
因为现在我们是混合部署,我们的重心是Hadoop,Spark此时扮演的是MapReduce的替代者!!
Spark提交任务
你懂的,--master
又变了,这次变成了--master yarn
同时我们需要附加参数:设置部署模式–-deploy-mode [cluster/client]
(cluster和client二选一)
当你选择cluster部署模式(集群部署模式):结果不会在控制台输出!但是选择client(客户端模式),就会看到结果再控制台输出。
既然使用了Yarn,那么在yarn的监控页面:(由于我集群上ResourceManager是在hadoop103上的) hadoop103:8088就能看到所有任务的执行情况!
这就回到了我们熟悉的界面~
如果开启了Spark的历史服务,在Spark的历史服务的页面hadoop102:18080也能看到历史程序的运行。
2.4、三种模式对比
模式 | Spark安装机器数量 | 需启动进程 | 所属者 | 应用场景 |
---|---|---|---|---|
Local | 1 | NONE | Spark | 测试、学习 |
Standalone | 3 | Master、Worker | Spark | 单独部署 |
Yarn | 1 | Yarn、HDFS | Hadoop | 混合部署 |
这三种模式中,Yarn部署模式算是物尽其用!充分发挥了Spark的计算性能以及Yarn的资源调度功能!
第三章、Spark运行架构
3.1、核心架构初认识
3.1.1、计算核心组件
作为一个以计算为核心的框架,那么它一定有完善的用于计算的核心架构!
先看下面这张图:
很清楚就能看到两个部分:
- Driver
- Executor
这就是Spark计算的两个核心组件!Spark是标准的主从架构。那么这两者的关系很显然Driver是主、Executor是从!
下面我们来简单认识一下他们分别在计算任务中发挥什么作用吧!
Driver
驱动?!见名知意,他是整个计算任务的动力来源!他会驱动控制整个任务顺利执行,他的主要工作有:
- 将Spark程序转化为计算任务,分发给Executor执行
- 在Executor之间调度任务(Task)
- 跟踪监控Executor的任务执行情况
- 用过UI展示任务的执行情况
Driver其实并不参与任何计算步骤!他只是整个任务的“启动者”以及“监工”。
主要是将程序转化为任务,然后发给Executor(打工人)去完成,协调集群完整计算任务
至于他是如何将程序转化为任务的,我们后面再说~
Executor
执行器,对,我就是一个老老实实的打工人!你给我们任务我完成就行了。所以它的工作非常简单:
- 执行Driver分配的任务,并返回结果
- 缓存部分需要缓存的数据,以提高计算的效率!
Executor才是计算的实施者,他们会完成Driver发给他们的Task,并将结果反馈给Driver。虽然是可怜的打工人,但是他们的地位不可小看!
3.1.2、资源调度核心组件
我们在使用standalone模式部署时,我们使用Spark自带的资源调度组件Worker&Master。他们之间的关系也是普通的主从关系,我们甚至可以把他们俩拿来和Yarn的两个组件类比!
Worker相当于NodeManager,Master则相当于ResourceManager。
Worker管理着单个计算节点的资源,而Master则是掌管着整个集群的资源!
我们的计算任务总会涉及到资源的请求,而这个请求首先是由Driver发起(要准备要一个计算任务,就得算好资源的利用),但是如果让申请到Master那里,计算框架的组件和资源调度框架的组件这样直接交互,就会造成耦合!所以为了降低耦合,只好在他们之间加上一层:集群管理者图中的(Cluster Manager),然后由集群管理者向集群的资源管理者也就是Master“转述”这个资源请求,说了这么久的资源,其实说白了就是executor。请求成功后,会按照要求在Worker节点上启动若干个executor,准备执行Driver发送的任务!
以上是利用Worker和Master完成的,如果换上Yarn,那么资源的请求与分配就要按照Yarn那一套来了。(ResourceManager -> NodeManager -> ApplicationMaster -> Job)
3.2、架构相关知识了解
简单认识了Spark中重要的两部分架构后,还有一部分细节和相关内容我们需要了解。这里我们暂时只做了解,并不深入其中
3.2.1、Executor和Core
上面提到了资源就是executor,这句话其实并不严谨!在每个Worker节点上确实可以有若干个executor,(提交Spark程序时使用--num-executors n
指定executor的数量)
Executor是运行在Worker节点上的一个JVM进程,专门用于执行计算任务,需要内存、CPU资源。
所以我们可以近似认为executor就是资源!
至于每个executor的占用资源如何设置,我们在2.1.3中就说明了,可以返回去看一看哦!
但是得说明一下:里面所说的CPU核心数量,其实是虚拟CPU核心数!(因为这个设置是不受限于机器的配置的!)当资源充足时,他可以实现并行计算,若资源不够则是使用并发来模拟并行!
下面我们就来说一说这个并行与并发!
3.2.2、并行度(Parallelism)
并行度:同时执行的任务(Task)数量!
在Worker中我们的executor被分配到的CPU核心,并非代表我们机器的CPU核心。而是相当于给他开启了若干个线程,这些线程要同时去抢占机器的CPU的核心,这也就是我们常说的并发!
而并行,则是资源充足情况下(CPU多核),每个虚拟CPU(线程)都能拿到一个物理CPU核心,核心之间并行执行!
所以说合理设置资源数量是可以提高计算的并行度的!!
并行和并发
参考博客:并行与并发的区别https://blog.csdn.net/java_zero2one/article/details/51477791
并行是物理上多个核心处理多个线程各做各的事情!
而并发则是一个核心,通过快速切换执行不同线程上的任务,以达到宏观上的并行效果,其实微观上还是串行执行!
用参考博客中的例子生动说明:
并行就是两把铁锹,两个人,各挖各坑!
并发是一把铁锹,两个人,你挖一下然后他挖一下!(仿佛两个人一起在挖坑)
可想而知前者效率更高!
所以在工作中Spark的性能调优,资源的分配是很重要的!保证资源最大化利用不浪费,并提高计算的并行度!
3.2.3、有向无环图(DAG)
这个东西听着好像和计算没有半毛钱关系,其实这个是Spark计算任务执行流程的重要指标!
由于Spark是分布式的并行计算,一个计算任务会被分解成多个小任务放到不同的计算节点上完成,那么如果这些任务之间有相互依赖的关系,就需要用有向无环图来确定他们的执行顺序!
他主要是解决循环依赖 的问题,在Spring,Maven中应该都听过这个词,循环依赖会导致程序崩溃!我们需要避免循环依赖的出现!计算任务也是一样,利用DAG先确定下计算任务的执行顺序,然后按照顺序执行任务即可!
3.2.4、提交流程
提交Job的流程,主要分为两个方面:程序转为任务的准备,资源申请;
看这张图,SparkContext即Driver一边在进行Task的准备(黄色框),一边还在申请资源!(蓝色框)但是两者之间是由先后顺序的!
- 先申请资源,申请到资源后,Executor启动并反馈情况,做好准备接受Task!
- 然后Driver会将程序转为Job,并划分为Task发放给申请到的Executor。然后在Worker节点上Executor执行完Task,将结果返回,并同时回资源!
这是大致的任务提交流程,我们使用Yarn作为资源调度框架时,说了两种部署模式:
- Cluster
- Client
那么这两种部署模式的区别是什么呢?!**Driver程序运行节点的位置!**用两张图来看一下吧:
Cluster:在集群中!(SparkContext在Yarn集群中)
Client:在集群外!
至于他们之间更深层次区别,我们后面再说吧!
第四章、Spark核心编程
为了满足Spark分布式计算需要,封装了三大数据结构:
- RDD:弹性分布式数据集
- 累加器:分布式共享只写变量
- 广播变量:分布式共享只读变量
在这之前我们先简单了解一下什么是分布式计算。
首先很明确的一点:计算任务由Driver发出,由Worker节点上的Executor接收执行。
分布式计算,也就是我们有2个及以上Worker节点来共同完成Driver发出的任务,那么他们之间就必须得有任务的划分,以及结果的合并。为了支持这种分布式的计算,上面三种数据结构随之诞生!
4.1、RDD
4.1.1、什么是RDD?
弹性分布式数据集(Resilient Distributed Datasets)。初次接触可能会感觉难懂,我们稍后将其拆分开来说。他在整个计算中担任的是数据处理模型的角色。是最小的计算单元!
计算单元:即数据+复杂计算逻辑。而复杂的计算逻辑可以拆分成多个简单的计算逻辑;所以最小计算单元就是数据+最简单的计算逻辑。
在Spark中可以将一个数据处理程序看做为一个的计算单元。这个计算单元我们需要拆分成若干个最小计算单元发送给Executor执行。
总结起来就是一句话**“一个庞大的计算任务,我们可以使用若干RDD进行关联形成复杂的计算逻辑,以完成计算。”**
4.1.2、RDD运行原理
是不是已经崩溃了?!说的都是些什么啊?!没关系现在我们从我们较为熟悉的IO开始讲起:
简单回顾IO流
先看这张图:
不知道你还记不记得java.io中的节点流
和处理流
!
节点流:是我们使用IO进行数据传输的核心所在,利用节点流我们可以来两点之间使用字节/字节流的方式传输数据。(例如FileReader、FileInputStream、等图中粗体)
处理流:它是建立在节点流的,相当于在节点流上套了一层。(每个处理流的创建,其构造函数中都需要一个节点流对象作为参数。)(例如BufferedReader、BufferedInputStream)
既然节点流已经可以完成IO流传输数据的工作,那么处理流的存在意义何在呢?!
往往只是使用低级的节点流,会有性能和效率的问题。例如没有缓冲区,就只能读完一个字节后,必须写出后才能读下一个,严重影响性能。
同时低级的节点流不具备处理复杂情况和特殊数据的能力。
那么处理流的存在的意义就体现出来了:
- 提高节点流的性能,提高读写效率。(例如BufferedInputStream,增加了缓冲区)
- 丰富、完善节点流的功能,通过套上各种处理流,使得封装后的流可以处理各种情况、数据,并且利用处理流的方法,简化读写操作!(例如readLine())
我们来看一个最简单的IO流代码:
package JavaIO_02;
import java.io.*;
/**
* 文件流
* DataOutputStream DataInputStream
* 1.写出后读取
* 2.读取顺序应与写出保持一致
*
* @author gyc
* @Data 2019/8/23
*/
public class DataStream {
public static void main(String[] args) throws IOException {
String Path = "Data.txt";
//DataOutputStream写出
DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(Path)));
dos.writeUTF("官宇辰");
dos.writeInt(817);
dos.writeBoolean(true);
dos.writeChar('g');
dos.flush();
//DataInputStream读取
DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream(Path)));
//按写出的顺序读取
String msg = dis.readUTF();
int num = dis.readInt(