第一章 Spark概述
1.1 Spark 是什么
Spark是一种基于内存的快速,通用,可扩展的大数据分析计算引擎
1.2 Spark and Hadoop
Hadoop 的 MapReduce 是大家广为熟知的计算框架,那为什么还要学习新的计算框架Spark呢,这里就不得不提到Spark和Hadoop的关系
从时间上看,Spark的发布时间远远晚于Hadoop
从功能上看:
Hadoop:
- Hadoop是由java语言编写的,在分布式服务器集群上存储海量数据并运行分布式分析应用的开源框架
- 作为Hadoop分布式文件系统,HDFS处于Hadoop生态圈的最下层,存储着所有的数据,支持着Hadoop的所有服务。它的理论基础源于Google的TheGoogleFileSystem这篇论文,它是GFS的开源实现。
- MapReduce是一种编程模型,Hadoop根据Google的MapReduce论文将其实现,作为Hadoop的分布式计算模型,是Hadoop的核心。基于这个框架,分布式并行程序的编写变得异常简单。综合了HDFS的分布式存储和MapReduce的分布式计算,Hadoop在处理海量数据时,性能横向扩展变得非常容易。
- HBase是对Google的Bigtable的开源实现,但又和Bigtable存在许多不同之处。HBase是一个基于HDFS的分布式数据库,擅长实时地随机读/写超大规模数据集。它也是Hadoop非常重要的组件。
Spark:
-
Spark是一种由Scala 语言开发的快速的,通用的,可扩展的大数据分析引擎
-
Spark Core 中提供了Spark最基础与核心的功能
-
Spark SQL 是 Spark 用来操作结构化数据的组件。通过Spark SQL,用户可以使用SQL 或者 Apache Hive 的SQL 来查询数据
-
Spark Streaming 是Spark平台上针对实时数据进行流式计算的组件,提供了丰富的处理数据流的API
Spark出现的时间相对较晚,并且主要功能主要是用于数据计算,所以其实Spark一直被认为是Hadoop 框架的升级版。
1.3 Spark or Hadoop
Hadoop的MR 框架和 Spark 框架都是数据处理框架,那么应该如何选择呢?
- Hadoop MapReduce由于其设计初衷并不是为了满足循环迭代式数据流处理,因此在多并行运行的数据可复用场景(如:机器学习、图挖掘算法、交互式数据挖掘算法)中存在诸多计算效率等问题。所以Spark应运而生,Spark就是在传统的MapReduce 计算框架的基础上,利用其计算过程的优化,从而大大加快了数据分析、挖掘的运行和读写速度,并将计算单元缩小到更适合并行计算和重复使用的RDD计算模型。
- 机器学习中ALS、凸优化梯度下降等。这些都需要基于数据集或者数据集的衍生数据反复查询反复操作。MR这种模式不太合适,即使多MR串行处理,性能和时间也是一个问题。数据的共享依赖于磁盘。另外一种是交互式数据挖掘,MR显然不擅长。而Spark所基于的scala语言恰恰擅长函数的处理。
- Spark是一个分布式数据快速分析项目,他的核心技术是弹性分布式数据集,提供了比MapReduce丰富的模型,可以快速在内存中对数据集进行多次迭代,支持复杂的数据挖掘算法和图形计算算法
- Spark和Hadoop 根本差异是多个作业之间的数据通信问题,Spark是通过内存通信,而Hadoop是基于磁盘
- Spark Task 的启动时间快,Spark采用fork 线程的方法,而Hadoop采用创还能新的进程的方式
- Spark 只有在 shuffle的时候将数据写入磁盘,而Hadoop中多个MR作业之间的数据交互都依赖磁盘
- Spark 的缓存机制比HDFS更高效
通过上面的比较,我们可以看出在绝大多数的数据计算场景中,Spark确实会比MapReduce更有优势。但是Spark是基于内存的,所以在实际的生产环境中,由于内存的限制,可能会由于内存资源不够导致Job执行失败,此时,MapReduce其实是一个更好的选择,所以Spark并不能完全替代MR。
1.4 Spark的核心模块
- Spark Core
Spark Core 中提供了最基础与核心的功能,Spark 的其他功能如:Spark SQL, Spark Streaming,GraphX MLlib 都是在Spark Core的基础上进行扩展的 - Spark SQL
Spark SQL是Spark用来操作结构化数据的组件,通过Spark SQL 用户可以使用SQL 或者Apache Hive版本的SQL 方言(HQL)来查数据数据 - Spark Streaming
Spark Streaming是Spark平台上针对实时数据进行流式计算的组件,提供了丰富的处理数据流的API。 - Spark MLlib
MLlib是Spark提供的一个机器学习算法库。MLlib不仅提供了模型评估、数据导入等额外的功能,还提供了一些更底层的机器学习原语。 - Spark GraphX
GraphX是Spark面向图计算提供的框架与算法库。
#第二章 Spark 快速上手
2.1创建Maven项目
2.1.1首先要添加Scala插件
Spark是Scala语言开发的,所以开发前应该保证IDEA开发工具含有Scala开发插件
2.1.2增加依赖关系
<dependencies> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-core_2.12</artifactId> <version>3.0.0</version> </dependency> </dependencies> <build> <plugins> <!-- 该插件用于将Scala代码编译成class文件 --> <plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <version>3.2.2</version> <executions> <execution> <!-- 声明绑定到maven的compile阶段 --> <goals> <goal>testCompile</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.1.0</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
2.1.3WordCount
为了能直观地感受Spark框架的效果,接下来我们实现一个大数据学科中最常见的教学案例WordCount
package com.bigdata.spark.wordcount
import org.apache.spark.{SparkConf, SparkContext}
object Spark01_WordCount_Env {
def main(args: Array[String]): Unit = {
//todo 使用Spark
//Spark 是一个计算框架
//1.添加依赖
//2.获取Spark 连接
val conf = new SparkConf().setMaster("local").setAppName("WordCount")
val sc = new SparkContext(conf)
//读取文件
val lines = sc.textFile("F:/worksapce_java/bigdata/spark/data/word.txt")
//将文件中的数据进行分词
val words = lines.flatMap(_.split(" "))
//将分词后的数据进行分组
val wordGroup = words.groupBy(word => word)
//将分组后的数据进行统计分析
val wordCount = wordGroup.mapValues(_.size)
//将统计结果打印
wordCount.collect().foreach(println(_))
sc.stop()
}
}
执行过程中,会产生大量的执行日志,如果为了能够更好的查看程序的执行结果,可以在项目的resources目录中创建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 ERROR. 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=ERROR
# Settings to quiet third party logs that are too verbose
log4j.logger.org.spark_project.jetty=ERROR
log4j.logger.org.spark_project.jetty.util.component.AbstractLifeCycle=ERROR
log4j.logger.org.apache.spark.repl.SparkIMain$exprTyper=ERROR
log4j.logger.org.apache.spark.repl.SparkILoop$SparkILoopInterpreter=ERROR
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
第三章 Spark 运行环境
Spark作为一个数据处理框架和计算引擎,被设计在所有常见的集群环境中运行,在国内工作主流的环境为Yarn,接下里就看看不同环境下Spark的运行
3.1 Local 模式
所谓的Local模式,就是不需要其他任何节点资源就可以在本地执行Spark代码的环境,一般用于教学,调试,演示等,之前在IDEA中运行代码的环境我们称之为开发环境,不太一样
3.1.1 解压缩文件
将spark-3.0.0-bin-hadoop3.2.tgz文件上传到Linux并解压缩,放置在指定位置,路径中不要包含中文或空格
tar -zxvf spark-3.0.0-bin-hadoop3.2.tgz -C /opt/module
cd /opt/module
mv spark-3.0.0-bin-hadoop3.2 spark-local
3.1.2 启动local环境
- 进入解压缩路径后执行如下命令
bin/spark-shell
- 启动成功后,可以输入网址进行Web UI 监控页面访问: http://ip:4040
3.1.3 命令行工具
在解压缩文件夹下的data目录中,添加word.txt文件。在命令行工具中执行如下代码指令(和IDEA中代码简化版一致)
sc.textFile("data/word.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).collect
3.1.4 退出本地模式
Ctrl+c或者:quit
3.1.5提交应用
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master local[2] \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10
1)–class表示要执行程序的主类,此处可以更换为咱们自己写的应用程序
2)–master local[2] 部署模式,默认为本地模式,数字表示分配的虚拟CPU核数量
3)spark-examples_2.12-3.0.0.jar 运行的应用类所在的jar包,实际使用时,可以设定为咱们自己打的jar包
4)数字10表示程序的入口参数,用于设定当前应用的任务数量
3.2 Standalone 模式
local本地模式只是用来进行练习演示的,真实工作中还是要将应用提交到对应的集群中去执行,这里我们来看看只使用Spark自身节点运行的集群模式,也就是我们所谓的独立部署(Standalone)模式。Spark的Standalone模式体现了经典的master-slave模式。
集群规划:
node01 | node02 | node03 |
---|---|---|
Worker | Worker | Worker |
3.2.1 解压缩文件
和上述 local的压缩包和解压缩方法完全一样
唯一不同的就是名字要更改一下 mv spark-3.0.0-bin-hadoop3.2 spark-standalone
3.2.2 修改配置文件
- 进入解压缩后的conf 目录 修改slaves.template文件名为slaves
- 修改 slaves 文件,添加worker节点
node01
node02
node03
- 3)修改spark-env.sh.template文件名为spark-env.sh,修改spark-env.sh文件,添加JAVA_HOME环境变量和集群对应的master节点
export JAVA_HOME=/export/servers/jdk1.8.0_141
SPARK_MASTER_HOST=node01
SPARK_MASTER_PORT=9000
注意:7077端口,相当于hadoop3内部通信的8020端口,此处的端口需要确认自己的Hadoop配置
- 分发spark-standalone目录
scp -r spark-standalone node02 :$PWD
3.2.3 启动集群
- sbin/start-all.sh
- 查看三台服务器运行进程
- 查看Master资源监控Web UI界面: http://node01:8080
3.2.4 提交应用
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://node01:9000 \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10
需要修改的和local一样
3.2.5 提交参数说明
在提交应用中,一般会同时一些提交参数
bin/spark-submit \
--class <main-class>
--master <master-url> \
... # other options
<application-jar> \
[application-arguments]
3.2.6 配置历史服务
由于spark-shell停止掉后,集群监控lnode011:4040页面就看不到历史任务的运行情况,所以开发时都配置历史服务器记录任务运行情况。
- 修改spark-defaults.conf.template文件名为spark-defaults.conf
mv spark-defaults.conf.template spark-defaults.conf - 修改spark-default.conf文件,配置日志存储路径
spark.eventLog.enabled true
spark.eventLog.dir hdfs://node01:9000/directory
注意:需要启动hadoop集群,HDFS上的directory 目录需要提前存在。
自己创建:hdfs dfs -mkdir /directory - 修改spark-env.sh文件, 添加日志配置
export SPARK_HISTORY_OPTS="
-Dspark.history.ui.port=18080
-Dspark.history.fs.logDirectory=hdfs://node01:9000/directory
-Dspark.history.retainedApplications=30"
参数1含义:WEB UI访问的端口号为18080
参数2含义:指定历史服务器日志存储路径
参数3含义:指定保存Application历史记录的个数,如果超过这个值,旧的应用程序信息将被删除,这个是内存中的应用数,而不是页 面上显示的应用数。
- 分发配置文件
- 重新启动集群和历史服务
sbin/start-all.sh
sbin/start-history-server.sh
- 重新执行命令
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://node01:9000\
./examples/jars/spark-examples_2.12-3.0.0.jar \
10
- 查看历史服务
http://node01:18080
3.2.7 配置高可用(HA)
所谓的高可用是因为当前集群中的Master节点只有一个,所以会存在单点故障问题。所以为了解决单点故障问题,需要在集群中配置多个Master节点,一旦处于活动状态的Master发生故障时,由备用Master提供服务,保证作业可以继续执行。这里的高可用一般采用Zookeeper设置
集群规划:
node01 | node02 | node03 |
---|---|---|
Master | Master | |
Zookeeper | Zookeeper | Zookeeper |
Worker | Worker | Worker |
- 启动Zookeeper
- 修改spark-env.sh文件添加如下配置
```xml
SPARK_MASTER_HOST=linux1
SPARK_MASTER_PORT=7077
添加如下内容:
#Master监控页面默认访问端口为8080,但是可能会和Zookeeper冲突,所以改成8989,也可以自定义,访问UI
控页面时请注意
SPARK_MASTER_WEBUI_PORT=8989
export SPARK_DAEMON_JAVA_OPTS="
-Dspark.deploy.recoveryMode=ZOOKEEPER
-Dspark.deploy.zookeeper.url=node01,node02,node03
-Dspark.deploy.zookeeper.dir=/spark"
```
- 分发配置
- 启动集群
- 启动node02的单独Master节点,此时node02节点Master状态处于备用状态
sbin/start-master.sh
3.3Yarn模式
独立部署(Standalone)模式由Spark自身提供计算资源,无需其他框架提供资源。这种方式降低了和其他的第三方资源框架的耦合性,独立性非常强,但是也要记住,Spark主要是计算框架,而不是资源调度框架,所以本身提供的资源调度也并不是他的强项,所以还是和其他的专业资源调度框架集成会更靠谱一些。所以接下来我们学习在强大的Yarn环境下Spark是如何工作的
3.3.1 解压缩文件
将spark-3.0.0-bin-hadoop3.2.tgz文件上传到linux并解压缩,把名字改为spark-yarn 放置在指定位置。
3.3.2 修改配置文件
- 修改hadoop配置文件/export/servers/hadoop-2.7.5/etc/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>
- 修改conf/spark-env.sh,添加JAVA_HOME和YARN_CONF_DIR配置
mv spark-env.sh.template spark-env.sh
export JAVA_HOME=/opt/module/jdk1.8.0_144
YARN_CONF_DIR=/opt/module/hadoop/etc/hadoop
3.3.3 启动HDFS以及YARN集群
3.3.4 配置然后启动历史服务
和前面一样的,不在多说
3.3.5重新提交应用
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode client \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10
3.4 K8S & Mesos模式
Mesos是Apache下的开源分布式资源管理框架,它被称为是分布式系统的内核,在Twitter得到广泛使用,管理着Twitter超过30,0000台服务器上的应用部署,但是在国内,依然使用着传统的Hadoop大数据框架,所以国内使用Mesos框架的并不多,但是原理其实都差不多,这里我们就不多说了
容器化部署是目前业界很流行的一项技术,基于Docker镜像运行能够让用户更加方便地对应用进行管理和运维。容器管理工具中最为流行的就是Kubernetes(k8s),而Spark也在最近的版本中支持了k8s部署模式。
3.5Window模式
在同学们自己学习时,每次都启动虚拟机启动集群显得格外麻烦并且会占大量的系统资源,导致系统执行变慢,不仅仅影响学习效果,也影响学习进度,Spark非常暖心地提供了可以在windows系统下启动本地集群的方式,这样,在不使用虚拟机的情况下,也能学习Spark的基本使用!
3.5.1解压缩文件
将文件spark-3.0.0-bin-hadoop3.2.tgz解压缩到无中文无空格的路径中
3.5.2 启动本地环境
- 执行解压缩文件路径下bin目录中的spark-shell.cmd文件,启动Spark本地环境
- 在bin目录中创建input目录,并添加word.txt文件, 在命令行中输入脚本代码
sc.textFile("input/word.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).collect
3.5.3 命令行提交应用
spark-submit
--class org.apache.spark.examples.SparkPi
--master local[2] ../examples/jars/spark-examples_2.12-3.0.0.jar 10
3.6部署模式对比
3.7 端口号
- Spark查看当前Spark-shell运行任务情况端口号:4040(计算)
- Spark Master内部通信服务端口号:7077
- Standalone模式下,Spark Master Web端口号:8080(资源)
- Spark历史服务器端口号:18080
- Hadoop YARN任务运行情况查看端口号:8088
第四章 Spark运行架构
4.1 运行架构
spark 框架的核心是一个计算引擎,整体来说,它采用了标准的 master-slave 的结构,如下图所示,他展示了一个Spark执行时的基本结构,图形中的Driver表示master,负责管理整个集群中的作业任务调度,图形中的Executor则是slave,负责实际执行任务
4.2核心组件
Spark框架一共两个核心组件:
4.2.1 Driver
spark驱动器节点,用于执行Spark任务中的main方法,负责实际代码的执行工作,Driver在Spark作业执行时主要负责:
- 将用户程序转化为作业(job)
- 在Executor之间调度任务(task)
- 跟踪Executor的执行情况
- 通过UI展示查询运行情况
简单理解,所谓的Driver就是驱使整个应用运行起来的程序,也称之为Driver类。
4.2.2 Executor
Spark Executor 是集群中工作节点(Worker)中的一个JVM进程,负责在Spark 作业中运行具体任务(Task),任务彼此之间相互独立。Spark 应用启动时,Executor节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有Executor节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他Executor节点上继续运行
Executor有两个核心功能:
- 负责运行组成Spark应用的任务,并将结果返回给驱动器进程
- 它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD 是直接缓存在Executor进程内的,因此任务可以在运行时充分利用缓存数据加速运算。
4.2.3 Master & Worker
Spark集群的独立部署环境中,不需要依赖其他的资源调度框架,自身就实现了资源调度的功能,所以环境中还有其他两个核心组件:Master和Worker,这里的Master是一个进程,主要负责资源的调度和分配,并进行集群的监控等职责,类似于Yarn环境中的RM, 而Worker呢,也是进程,一个Worker运行在集群中的一台服务器上,由Master分配资源对数据进行并行的处理和计算,类似于Yarn环境中NM。
4.2.4 ApplicationMaster
Hadoop用户向YARN集群提交应用程序时,提交程序中应该包含ApplicationMaster,用于向资源调度器申请执行任务的资源容器Container,运行用户自己的程序任务job,监控整个任务的执行,跟踪整个任务的状态,处理任务失败等异常情况。
说的简单点就是,ResourceManager(资源)和Driver(计算)之间的解耦合靠的就是ApplicationMaster。
4.3 核心概念
4.3.1 Executor与Core(核)
Spark Executor是集群中运行在工作节点(Worker)中的一个JVM 是整个集群中的专门用于计算的节点。在提交应用中,可以提供参数指定计算节点的个数,以及对应的资源。这里的资源一般指的是工作节点Executor的内存大小和使用的虚拟CPU核(Core)数量。
4.3.2 并行度(Parallelism)
在分布式计算框架中一般都是多个任务同时执行,由于任务分布在不同的计算节点进行计算,所以能够真正地实现多任务并行执行,记住,这里是并行,而不是并发。这里我们将整个集群并行执行任务的数量称之为并行度。
4.4提交流程
所谓的提交流程,其实就是我们开发人员根据需求写的应用程序通过Spark客户端提交给Spark运行环境执行计算的流程。在不同的部署环境中,这个提交过程基本相同,但是又有细微的区别,我们这里不进行详细的比较,但是因为国内工作中,将Spark引用部署到Yarn环境中会更多一些,所以本课程中的提交流程是基于Yarn环境的。
Spark 应用程序提交到Yarn环境中执行 的时候,一般会有两种部署执行模式:Client和Cluster。两种模式主要区别在于:Driver程序的运行节点位置
4.4.1 Yarn Client模式
Client模式将用于监控和调度的Driver模块在客户端执行,而不是在Yarn,一般用于测试
- Driver在任务提交的本地机器上执行
- Driver启动后会和ResourceManager通讯申请启动ApplicationMaster
- ResourceManager分配container,在合适的NodeManager上启动ApplicationMaster,负责向ResourceManager申请Executor内存
- ResourceManager接到ApplicationMaster的资源申请后会分配container,然后ApplicationMaster在资源分配指定的NodeManager上启动Executor进程
- Executor进程启动后会向Driver反向注册,Executor全部注册完成后Driver开始执行main函数
- 之后执行到Action算子时,触发一个Job,并根据宽依赖开始划分stage,每个stage生成对应的TaskSet,之后将task分发到各个Executor上执行。
4.4.2Yarn Cluster 模式
Cluster模式将用于监控和调度的Driver模块启动在Yarn集群资源中执行,一般用于实际生产环境
- 在Yarn Cluster模式下,任务提交后会和ResourceManager通讯申请后启动
- 随后ResourceManager分配container,在合适的NodeManager上启动ApplicationMaster,此时的ApplicationMaster就是Driver。
- Driver启动后向ResourceManager申请Executor内存,ResourceManager,ResourceManager接到ApplicationMaster的资源申请后会分配container,然后在合适的NodeManager上启动Executor进程
- Executor 进程启动后会向Driver反向注册,Executor全部注册完成后 Driver开始执行main函数
- 之后执行到Action算子时,触发一个Job,并根据宽依赖开始划分stage,每个stage生成对应的TaskSet,之后将task分发到各个Executor上执行。
第五章 Spark核心编程
Spark 计算框架为了能够进行高并发和高吞吐的数据处理,封装了三大数据结构,用于处理不同的应用场景三大数据结构分别是:
- RDD : 弹性分布式数据集
- 累加器:分布式共享只写变量
- 广播变量:分布式共享只读变量
5.1 RDD
5.1.1什么是RDD
RDD 叫做弹性分布式数据,是Spark中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。
弹性:
- 存储的弹性:内存与磁盘的自动切换;
- 容错的弹性:数据丢失可以自动恢复;
- 计算的弹性:计算出错重试机制;
- 分片的弹性:可根据需要重新分片。
5.1.2 执行原理
Spark框架在执行时 先申请资源,然后将应用程序的数据处理逻辑分解成一个一个计算任务。然后将任务分发到已经分配资源的计算节点上,按照指定的计算模型进行数据计算,最后得到计算结果
RDD是Spark框架中用于数据处理的核心模型,下面是他的工作原理:
- 启动Yarn集群环境
- Spark 通过申请资源创建调度节点和计算节点
- Spark 框架根据需求将就散逻辑区分划分成不同的任务
- 调度节点将任务根据计算节点状态发送到对应的计算节点进行计算
看
从以上流程可以看出RDD在整个流程中主要用于将逻辑进行封装,并生成Task发送给Executor节点执行计算,接下来我们就一起看看Spark框架中RDD是具体是如何进行数据处理的。
5.1.3 基础编程
5.1.3.1 RDD创建
RDD创建可以分为四种、
- 从集合(内存)中创建RDD
从集合中创建RDD,Spark主要提供了两个方法:parallelize和makeRDD
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object RDD_instance_Memory {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("spark")
val sparkContext = new SparkContext(sparkConf)
//通过parallelize 从内存中创建RDD,可以将集合当做数据模型处理源
//parallelize 代表并行的意思
val rdd1 = sparkContext.parallelize(List(1,2,3,4))
val rdd2: RDD[Int] = sparkContext.makeRDD(Seq(1,2,3,4))
rdd1.collect().foreach(println(_))
rdd2.collect().foreach(println(_))
sparkContext.stop()
}
}
- 从文件中读取RDD
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object RDD_instance_File {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("spark")
val sparkContext = new SparkContext(sparkConf)
//从文件中创建RDD,可以将文件当做数据模型处理源
val rdd1: RDD[String] = sparkContext.textFile("F:/worksapce_java/bigdata/spark/data/word.txt")
rdd1.collect().foreach(println(_))
//可以读取一个文件,也可以读取一个目录下所有文件
val rdd2: RDD[String] = sparkContext.textFile("F:/worksapce_java/bigdata/spark/data")
rdd2.collect().foreach(println(_))
//也可以按照通配符的方式批量读取
val rdd3: RDD[String] = sparkContext.textFile("F:/worksapce_java/bigdata/spark/data/word*.txt")
rdd3.collect().foreach(println(_))
//也可以把文件信息全部读取出来:文件名加内容
val rdd4: RDD[(String, String)] = sparkContext.wholeTextFiles("F:/worksapce_java/bigdata/spark/data")
rdd4.collect().foreach(println(_))
sparkContext.stop()
}
}
- 从其他RDD创建
主要是通过一个RDD运算完后,再产生新的RDD。详情请参考后续章节 - 直接创建RDD(new)
使用new的方式直接构造RDD,一般由Spark框架自身使用。
5.1.4.2 RDD并行度与分区
默认情况下 Spark可以将一个作业切分为多个任务后,发送给Executor节点并行计算,而能够并行计算的任务数量我们称之为并行度。这个数量可以在构建RDD时指定。记住这里并行执行的任务数量,并不是指切分任务的数量。
val sparkConf =
new SparkConf().setMaster("local[*]").setAppName("spark")
val sparkContext = new SparkContext(sparkConf)
val dataRDD: RDD[Int] =
sparkContext.makeRDD(
List(1,2,3,4),
4)
val fileRDD: RDD[String] =
sparkContext.textFile(
"input",
2)
fileRDD.collect().foreach(println)
sparkContext.stop()
下面是读取内存或者文件分区的底层原理,了解即可
读取内存数据时,数据可以按照并行度的设定进行数据的分区操作,数据分区规则的Spark核心源码如下:
def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = {
(0 until numSlices).iterator.map { i =>
val start = ((i * length) / numSlices).toInt
val end = (((i + 1) * length) / numSlices).toInt
(start, end)
}
}
读取文件数据时,数据是按照Hadoop文件读取的规则进行切片分区,而切片规则和数据读取的规则有些差异,具体Spark核心源码如下:
public InputSplit[] getSplits(JobConf job, int numSplits)
throws IOException {
long totalSize = 0; // compute total size
for (FileStatus file: files) { // check we have valid files
if (file.isDirectory()) {
throw new IOException("Not a file: "+ file.getPath());
}
totalSize += file.getLen();
}
long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize);
...
for (FileStatus file: files) {
...
if (isSplitable(fs, path)) {
long blockSize = file.getBlockSize();
long splitSize = computeSplitSize(goalSize, minSize, blockSize);
...
}
protected long computeSplitSize(long goalSize, long minSize,
long blockSize) {
return Math.max(minSize, Math.min(goalSize, blockSize));
}
5.1.4.3 RDD转换算子
RDD根据数据处理方式的不同将算子整体上分为Value类型、双Value类型和Key-Value类型
- Value类型
- map
将处理的数据逐条进行映射转换,这里的转换可以是类型的转换,也可以是值的转换。
val dataRDD: RDD[Int] = sparkContext.makeRDD(List(1,2,3,4))
val dataRDD1: RDD[Int] = dataRDD.map(
num => {
num * 2
}
)
val dataRDD2: RDD[String] = dataRDD1.map(
num => {
"" + num
}
)
- mapPartitions
将待处理的数据以分区为单位发送到计算节点进行处理,这里的处理是指可以进行任意的处理,哪怕是过滤数据。
val dataRDD1: RDD[Int] = dataRDD.mapPartitions(
datas => {
datas.filter(_==2)
}
)
思考一个问题:map和mapPartitions的区别?
-
- 数据处理角度
Map算子是分区内一个数据一个数据的执行,类似于串行操作。而mapPartitions算子是以分区为单位进行批处理操作。
- 数据处理角度
-
- 功能的角度
Map算子主要目的将数据源中的数据进行转换和改变。但是不会减少或增多数据。MapPartitions算子需要传递一个迭代器,返回一个迭代器,没有要求的元素的个数保持不变,所以可以增加或减少数据
- 功能的角度
-
- 性能的角度
Map算子因为类似于串行操作,所以性能比较低,而是mapPartitions算子类似于批处理,所以性能较高。但是mapPartitions算子会长时间占用内存,那么这样会导致内存可能不够用,出现内存溢出的错误。所以在内存有限的情况下,不推荐使用。使用map操作。
完成比完美更重要
- 性能的角度
- mapPartitionsWithIndex
将待处理的数据以分区为单位发送到计算节点进行处理,这里的处理是指可以进行任意的处理,哪怕是过滤数据,在处理时同时可以获取当前分区索引。
val dataRDD1 = dataRDD.mapPartitionsWithIndex(
(index, datas) => {
datas.map(index, _)
}
)
- flatMap
将处理的数据进行扁平化后再进行映射处理,所以算子也称之为扁平映射
val dataRDD = sparkContext.makeRDD(List(
List(1,2),List(3,4)
),1)
val dataRDD1 = dataRDD.flatMap(
list => list
)
- glom
将同一个分区的数据直接转换为相同类型的内存数组进行处理,分区不变
val dataRDD = sparkContext.makeRDD(List(
1,2,3,4
),1)
val dataRDD1:RDD[Array[Int]] = dataRDD.glom()
- groupBy
将数据根据指定的规则进行分组, 分区默认不变,但是数据会被打乱重新组合,我们将这样的操作称之为shuffle。极限情况下,数据可能被分在同一个分区中
一个组的数据在一个分区中,但是并不是说一个分区中只有一个组
val dataRDD = sparkContext.makeRDD(List(1,2,3,4),1)
val dataRDD1 = dataRDD.groupBy(
_%2
)
- filter
将数据根据指定的规则进行筛选过滤,符合规则的数据保留,不符合规则的数据丢弃。
当数据进行筛选过滤后,分区不变,但是分区内的数据可能不均衡,生产环境下,可能会出现数据倾斜。
val dataRDD = sparkContext.makeRDD(List(
1,2,3,4
),1)
val dataRDD1 = dataRDD.filter(_%2 == 0)
- sample
根据指定的规则从数据集中抽取数据
val dataRDD = sparkContext.makeRDD(List(
1,2,3,4
),1)
// 抽取数据不放回(伯努利算法)
// 伯努利算法:又叫0、1分布。例如扔硬币,要么正面,要么反面。
// 具体实现:根据种子和随机算法算出一个数和第二个参数设置几率比较,小于第二个参数要,大于不要
// 第一个参数:抽取的数据是否放回,false:不放回
// 第二个参数:抽取的几率,范围在[0,1]之间,0:全不取;1:全取;
// 第三个参数:随机数种子
val dataRDD1 = dataRDD.sample(false, 0.5)
// 抽取数据放回(泊松算法)
// 第一个参数:抽取的数据是否放回,true:放回;false:不放回
// 第二个参数:重复数据的几率,范围大于等于0.表示每一个元素被期望抽取到的次数
// 第三个参数:随机数种子
val dataRDD2 = dataRDD.sample(true, 2)
- distinct
去重
val dataRDD = sparkContext.makeRDD(List(
1,2,3,4,1,2
),1)
val dataRDD1 = dataRDD.distinct()
val dataRDD2 = dataRDD.distinct(2)
- coalesce
根据数据缩减分区,用于大数据集过滤后,提高小数据集的执行效率,当spark程序中,存在过多的小任务的时候,可以通过coalesce方法,收缩合并分区,减少分区的个数,减小任务调度成本
val dataRDD = sparkContext.makeRDD(List(
1,2,3,4,1,2
),6)
val dataRDD1 = dataRDD.coalesce(2)
- repartition
该操作内部其实执行的是coalesce操作,参数shuffle的默认值为true。无论是将分区数多的RDD转换为分区数少的RDD,还是将分区数少的RDD转换为分区数多的RDD,repartition操作都可以完成,因为无论如何都会经shuffle过程。
val dataRDD = sparkContext.makeRDD(List(
1,2,3,4,1,2
),2)
val dataRDD1 = dataRDD.repartition(4)
- sortBy
该操作用于排序数据。在排序之前,可以将数据通过f函数进行处理,之后按照f函数处理的结果进行排序,默认为升序排列。排序后新产生的RDD的分区数与原RDD的分区数一致。中间存在shuffle的过程
val dataRDD = sparkContext.makeRDD(List(
1,2,3,4,1,2
),2)
val dataRDD1 = dataRDD.sortBy(num=>num, false, 4)
- intersection
对原rdd和参数rdd求交集后返回一个新的rdd
val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4))
val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6))
val dataRDD = dataRDD1.intersection(dataRDD2)
- union
对源RDD和参数RDD求并集后返回一个新的RDD
val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4))
val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6))
val dataRDD = dataRDD1.union(dataRDD2)
- subtract
以一个RDD元素为主,去除两个RDD中重复 元素,将其他元素保留下来。求差集
val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4))
val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6))
val dataRDD = dataRDD1.subtract(dataRDD2)
**无论是intersection union 还是subtract 数据类型都要保持一致,否则编译就不会通过
16. zip
将两个rdd中的元素,以键值对的形式进行合并。其中,键值对中的Key为第1个RDD中的元素,Value为第2个RDD中的相同位置的元素。
val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4))
val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6))
val dataRDD = dataRDD1.zip(dataRDD2)
zip 进行拉链时,要保证数分区数量一致,分区内数据量一致,但是数据类型不用保持一致
- partitionBy
将数据按照指定Partitioner重新进行分区。Spark默认的分区器是HashPartitioner
val rdd: RDD[(Int, String)] =
sc.makeRDD(Array((1,"aaa"),(2,"bbb"),(3,"ccc")),3)
import org.apache.spark.HashPartitioner
val rdd2: RDD[(Int, String)] =
rdd.partitionBy(new HashPartitioner(2))
- reduceByKey
将数据按照相同Key对Value进行聚合
val dataRDD1 = sparkContext.makeRDD(List(("a",1),("b",2),("c",3)))
val dataRDD2 = dataRDD1.reduceByKey(_+_)
val dataRDD3 = dataRDD1.reduceByKey(_+_, 2)
- groupByKey
将数据源的数据根据key对value进行分组
val dataRDD1 =
sparkContext.makeRDD(List(("a",1),("b",2),("c",3)))
val dataRDD2 = dataRDD1.groupByKey()
val dataRDD3 = dataRDD1.groupByKey(2)
val dataRDD4 = dataRDD1.groupByKey(new HashPartitioner(2))
reduceByKey和groupByKey的区别:
从shuffle的角度:reduceByKey和groupByKey都存在shuffle的操作,但是reduceByKey可以在shuffle前对分区内相同key的数据进行预聚合(combine)功能,这样会减少落盘的数据量,而groupByKey只是进行分组,不存在数据量减少的问题,reduceByKey性能比较高。
从功能的角度:reduceByKey其实包含分组和聚合的功能。groupByKey只能分组,不能聚合,所以在分组聚合的场合下,推荐使用reduceByKey,如果仅仅是分组而不需要聚合。那么还是只能使用groupByKey
- aggregateByKey
将数据根据不同的规则进行分区内计算和分区间计算
val dataRDD1 =sparkContext.makeRDD(List(("a",1),("b",2),("c",3)))
val dataRDD2 =dataRDD1.aggregateByKey(0)(_+_,_+_)
实现:取出每个分区内相同key的最大值然后分区间相加:
// TODO : 取出每个分区内相同key的最大值然后分区间相加
// aggregateByKey算子是函数柯里化,存在两个参数列表
// 1. 第一个参数列表中的参数表示初始值
// 2. 第二个参数列表中含有两个参数
// 2.1 第一个参数表示分区内的计算规则
// 2.2 第二个参数表示分区间的计算规则
val rdd =
sc.makeRDD(List(
("a",1),("a",2),("c",3),
("b",4),("c",5),("c",6)
),2)
// 0:("a",1),("a",2),("c",3) => (a,10)(c,10)
// => (a,10)(b,10)(c,20)
// 1:("b",4),("c",5),("c",6) => (b,10)(c,10)
val resultRDD =
rdd.aggregateByKey(10)(
(x, y) => math.max(x,y),
(x, y) => x + y
)
resultRDD.collect().foreach(println)
- foldByKey
当分区间计算规则和分区内计算规则相同时,aggregateByKey 就可以简化为foldByKey
val dataRDD1 = sparkContext.makeRDD(List(("a",1),("b",2),("c",3)))
val dataRDD2 = dataRDD1.foldByKey(0)(_+_)
- combineByKey
最通用的对key-value型rdd进行聚集操作的聚集函数(aggregation function)。类似于aggregate(),combineByKey()允许用户返回值的类型与输入不一致。
小练习:将数据List((“a”, 88), (“b”, 95), (“a”, 91), (“b”, 93), (“a”, 95), (“b”, 98))求每个key的平均值
val list: List[(String, Int)] = List(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98))
val input: RDD[(String, Int)] = sc.makeRDD(list, 2)
val combineRdd: RDD[(String, (Int, Int))] = input.combineByKey(
(_, 1),
(acc: (Int, Int), v) => (acc._1 + v, acc._2 + 1),
(acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2)
)
reduceByKey、foldByKey、aggregateByKey、combineByKey的区别?
reduceByKey: 相同key的第一个数据不进行任何计算,分区内和分区间计算规则相同
foldByKey: 相同key的第一个数据和初始值进行分区内计算,分区内和分区间计算规则相同
aggregateByKey:相同key的第一个数据和初始值进行分区内计算,分区内和分区间计算规则可以不相同
combineByKey:当计算时,发现数据结构不满足要求时,可以让第一个数据转换结构。分区内和分区间计算规则不相同。
- sortByKey
在一个(K,V)的RDD上调用 ,K必须实现Ordered接口,返回一个按照Key排序的
val dataRDD1 = sparkContext.makeRDD(List(("a",1),("b",2),("c",3)))
//正排
val sortRDD1: RDD[(String, Int)] = dataRDD1.sortByKey(true)
//倒排
val sortRDD1: RDD[(String, Int)] = dataRDD1.sortByKey(false)
- join
在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素连接在一起的(K,(V,W))的RDD
val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (2, "b"), (3, "c")))
val rdd1: RDD[(Int, Int)] = sc.makeRDD(Array((1, 4), (2, 5), (3, 6)))
rdd.join(rdd1).collect().foreach(println)
- leftOuterJoin
类似于SQL语句的左外连接
val dataRDD1 = sparkContext.makeRDD(List(("a",1),("b",2),("c",3)))
val dataRDD2 = sparkContext.makeRDD(List(("a",1),("b",2),("c",3)))
val rdd: RDD[(String, (Int, Option[Int]))] = dataRDD1.leftOuterJoin(dataRDD2)
- cogroup
在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD
val dataRDD1 = sparkContext.makeRDD(List(("a",1),("a",2),("c",3)))
val dataRDD2 = sparkContext.makeRDD(List(("a",1),("c",2),("c",3)))
val value: RDD[(String, (Iterable[Int], Iterable[Int]))] =
dataRDD1.cogroup(dataRDD2)
5.1.4.4 案例实操
- 数据准备
agent.log:时间戳,省份,城市,用户,广告,中间字段使用空格分隔。 - 需求描述
统计出每一个省份每个广告被点击数量排行的Top3 - 需求分析和代码实现
package com.bigdata.spark.rdd.operator.transform
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object Spark07_RDD_Demo {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
// TODO 统计每个省份每个广告被点击量的top3
//1.读取数据,获取源文件
val lines = sc.textFile("F:/worksapce_java/bigdata/spark/data/agent.log")
//1516609143867 6 7 64 16
//2.将数据格式进行转换 => ((省份,广告),1)
val wordToOne: RDD[((String, String), Int)] = lines.map(
line => {
val datas: Array[String] = line.split(" ")
((datas(1), datas(4)), 1)
}
)
//3.将转换好的数据进行聚合 ((省份,广告),1) => ((省份,广告),num)
val wordToSum= wordToOne.reduceByKey(_+_)
//4.将数据格式进行转换,将省份独立出来 ((省份,广告),num) => (省份,(广告,num))
val wordToNum = wordToSum.map {
case ((prv, adv), sum) => {
(prv, (adv, sum))
}
}
//5.将转换好的数据按照省份进行分组 (省份,(广告,num)) => (省份,List((广告1,num),(广告2,num),(广告3,num)))
val wordList: RDD[(String, Iterable[(String, Int)])] = wordToNum.groupByKey()
//6.将数据降序排序
//7.取出排序后的top3
val top3: RDD[(String, List[(String, Int)])] = wordList.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
//8.打印在控制台
top3.collect().foreach(println)
sc.stop()
}
}
5.1.4.5 RDD行动算子
def main(args: Array[String]): Unit = {
//一个应用程序,Driver程序
//一个程序中会有多个作业,也就是行动算子
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
// todo 01 行动算子
//1.reduce
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
val result: Int = rdd.reduce(_ + _)
println(result)
//2.collect将数据从Excutor端采集回Driver端,如果数据很大,则Driver端会导致数据溢出,这时就不适合使用collect
val results: Array[Int] = rdd.collect()
results.foreach(println)
//3.count 统计元素个数
val countResult: Long = rdd.count()
println(countResult)
//4.first 返回第一个元素
val firstResult: Int = rdd.first()
println(firstResult)
//5.take(n) 返回前n个数组成的数组
val takeResults: Array[Int] = rdd.take(2)
takeResults.foreach(println)
//6.takeOrdered(n) 返回排序后的前n个元素组成的数组
val orderResults: Array[Int] = rdd.takeOrdered(3)
orderResults.foreach(println)
sc.stop()
}
def main(args: Array[String]): Unit = {
//一个应用程序,Driver程序
//一个程序中会有多个作业,也就是行动算子
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(conf)
// todo 01 行动算子
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4),2)
//7.aggregate()() 提供一个初始值,然后让初始值和分区值按照规则进行计算
//aggregate 和 aggregateByKey的区别?
//1.aggregateByKey是转换算子,得到的结果是一个新的RDD
//aggregate是行动算子,得到的结果是具体的数据
//2.aggregateByKey只会在分区内进行计算
//aggregate不仅会在分区内进行计算,最后也会在分区间进行计算
val i: Int = rdd.aggregate(5)(_ + _ , _ + _)
val j: Int = rdd.aggregate(0)(_ + _ , _ + _)
println(i+" "+j)
//8.fold 和foldByKey一样,当aggregate分区间计算规则一样时,使用fold简化
val i1: Int = rdd.fold(0)(_ + _)
println(i1)
//9.countByKey统计每种key的个数
val rdd: RDD[(Int, String)] = sc.makeRDD(List((1, "a"), (1, "a"), (1, "a"), (2, "b"), (3, "c"), (3, "c")))
// 统计每种key的个数
val result: collection.Map[Int, Long] = rdd.countByKey()
//10.save相关算子将数据保存到不同格式的文件中
// 保存成Text文件
rdd.saveAsTextFile("output")
// 序列化成对象保存到文件
rdd.saveAsObjectFile("output1")
// 保存成Sequencefile文件
rdd.map((_,1)).saveAsSequenceFile("output2")
//11.foreach分布式遍历RDD中的每一个元素,调用指定函数
// 收集后打印
rdd.map(num=>num).collect().foreach(println)
println("****************")
// 分布式打印
rdd.foreach(println)
sc.stop()
}
5.1.4.6 RDD序列化
闭包检查
从计算的角度, 算子以外的代码都是在Driver端执行, 算子里面的代码都是在Executor端执行。那么在scala的函数式编程中,就会导致算子内经常会用到算子外的数据,这样就形成了闭包的效果,如果使用的算子外的数据无法序列化,就意味着无法传值给Executor端执行,就会发生错误,所以需要在执行任务计算前,检测闭包内的对象是否可以进行序列化,这个操作我们称之为闭包检测
5.1.4.7 RDD依赖关系
- RDD 血缘关系
RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。
val fileRDD: RDD[String] = sc.textFile("input/1.txt")
println(fileRDD.toDebugString)
println("----------------------")
val wordRDD: RDD[String] = fileRDD.flatMap(_.split(" "))
println(wordRDD.toDebugString)
println("----------------------")
val mapRDD: RDD[(String, Int)] = wordRDD.map((_,1))
println(mapRDD.toDebugString)
println("----------------------")
val resultRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
println(resultRDD.toDebugString)
resultRDD.collect()
- RDD 依赖关系
这里所谓的依赖关系,其实就是两个相邻RDD之间的关系 - RDD 窄依赖
窄依赖表示每一个父(上游)RDD的Partition最多被子(下游)RDD的一个Partition使用,窄依赖我们形象的比喻为独生子女。 - RDD 宽依赖
宽依赖表示同一个父(上游)RDD的Partition被多个子(下游)RDD的Partition依赖,会引起Shuffle,总结:宽依赖我们形象的比喻为多生。
5.1.4.8阶段划分
5.1.4.9任务划分
RDD任务中间切分为:Application,Job,Stage,Task
- Application :初始化一个SparkContext并生成一个Application
- Job :一个Action行动算子就相当于一个Job
- Stage : Stage等于宽依赖(ShuffleDependency)的个数加1
- Task : 一个Stage阶段,最后一个RDD分区个数就是Task个数
注意:Application->Job->Stage->Task每一层都是1对n的关系。
5.1.4.10持久化
- RDD Cache 缓存 RDD通过Cache或者Persist方法将前面计算的结果缓存,默认情况下可以缓存在JVM的堆内存中,但并不是这两个方法被调用时立即缓存,而是触发后面的action行动算子时,该RDD将会被缓存在计算节点的内存中,并供后面重用。
//cache操作会增加血缘关系,不改变原有的血缘关系
println(wordToOneRdd.toDebugString)
//数据缓存
wordToOneRdd.cache()
//可以更改存储级别
mapRdd.persist(StorageLevel.MEMORY_AND_DISK_2)
存储级别:
缓存有可能丢失,或者存储于内存的数据由于内存不足而被删除,RDD的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列转换,丢失的数据会被重算,由于RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition。
Spark会自动对一些Shuffle操作的中间数据做持久化操作(比如:reduceByKey)。这样做的目的是为了当一个节点Shuffle失败了避免重新计算整个输入。但是,在实际使用的时候,如果想重用数据,仍然建议调用persist或cache。
- RDD CheckPoint 检查点
所谓的检查点其实就是通过将RDD中间结果写入磁盘 由于血缘依赖过长会导致容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点之后又节点出现问题,可以从检查点开始重做血缘,减少了开销。
对RDD进行checkpoint操作并不会马上被开始执行,必须执行Action操作才能触发
//设置检查点路径
sc.setCheckpointDir("./checkpoint1")
//创建一个RDD,读取指定位置文件:hello atguigu atguigu
val lineRdd: RDD[String] = sc.textFile("input/1.txt")
//业务逻辑
val wordRdd: RDD[String] = lineRdd.flatMap(line => line.split(" "))
val wordToOneRdd: RDD[(String, Long)] = wordRdd.map {
word => {
(word, System.currentTimeMillis())
}
}
// 增加缓存,避免再重新跑一个job做checkpoint
wordToOneRdd.cache()
// 数据检查点:针对wordToOneRdd做检查点计算
wordToOneRdd.checkpoint()
// 触发执行逻辑
wordToOneRdd.collect().foreach(println)
- 缓存和检查点的区别
- Cache缓存只是将数据保存起来,并不会切血缘依赖,Checkpoint检查点切断血缘依赖
- Cache缓存的数据通常存储在磁盘,内存等地方,可靠性低,Checkpoint的数据通常存储在HDFS等容错,高可用的文件系统,可靠性高
- 建议对checkpoint()的RDD使用Cache缓存,这样checkpoint的job只需从Cache缓存中读取数据即可,否则需要再从头计算一次RDD。
5.1.4.11 RDD分区器
Spark目前支持Hash分区和Range分区,和用户自定义分区。Hash分区为当前的默认分区。分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle后进入哪个分区,进而决定了Reduce的个数。
只有Key-Value类型的RDD才有分区器,非Key-Value类型的RDD分区的值是None
每个RDD的分区ID范围:0 ~ (numPartitions - 1),决定这个值是属于那个分区的。
- Hash分区:对于给定的key,计算其hashCode,并除以分区个数取余
- Range分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
5.1.4.12 文件读取与保存
Spark的数据及数据保存可以从两个维度来作区分,文件格式以及文件系统,
文件格式分为:text文件,csv文件,sequence文件及object文件
文件系统分为:本地文件系统,HDFS HBASE以及数据库
- text文件
//读取输入文件
val inputRDD : RDD[String] = sc.textFile("input/1.txt")
//保存数据
inputRDD.saveAsTextFile("output")
- sequence文件
Sequence文件是Hadoop 用来存储二进制形式的Key-Value对而设计的一种平面文件file。在SparkContext中,可以调用sequenceFilekeyClass, valueClass。
// 保存数据为SequenceFile
dataRDD.saveAsSequenceFile("output")
// 读取SequenceFile文件
sc.sequenceFile[Int,Int]("output").collect().foreach(println)
- object 对象文件
对象文件是将对象序列化后保存的文件,采用Java的序列化机制。可以通过objectFile函数接收一个路径,读取对象文件,返回对应的RDD,也可以通过调用saveAsObjectFile()来实现对对象文件的输出。因为是序列化所以要指定类型。
// 保存数据
dataRDD.saveAsObjectFile("output")
// 读取数据
sc.objectFile[Int]("output").collect().foreach(println)
5.2 累加器
5.2.1 实现原理
累加器用来把Executor端变量信息聚合到Driver端,在Driver程序定义的变量,在Executor端的每个Task都回得到这个变量的一份新的副本,每个task更新这些副本的值后,传回Driver端进行merge
5.2.2基础编程
5.2.2.1系统累加器
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("acc")
val sc = new SparkContext(conf)
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4),2)
//创建累加器
val sum: LongAccumulator = sc.longAccumulator("sum")
rdd.foreach(
num=>{
//使用累加器
sum.add(num)
}
)
//获取累加器结果
println(sum.value)
sc.stop()
}
5.2.2.2自定义累加器
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("acc")
val sc = new SparkContext(conf)
val rdd: RDD[String] = sc.makeRDD(List(
"Scala",
"Scala",
"Scala",
"Scala",
"Scala",
"Spark",
"Spark",
"Spark",
"Spark"
))
//todo 01 创建累加器
val wca = new WordCountAccumulator
//todo 02 向Spark注册累加器
sc.register(wca,"wordCount")
//todo 03 使用累加器
rdd.foreach(
word=>{
wca.add(word)
}
)
println(wca.value)
//获取累加器结果
sc.stop()
}
//自定义累加器
//1.继承AccumulatorV2类
//2.定义泛型 In :输入类型 Out :输出类型
//3.重写方法
class WordCountAccumulator extends AccumulatorV2[String,mutable.Map[String,Int]]{
private val wcMap = mutable.Map[String,Int]()
//判断累加器是否是初始状态
override def isZero: Boolean = {
wcMap.isEmpty
}
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
new WordCountAccumulator()
}
//重置累加器
override def reset(): Unit = {
wcMap.clear()
}
//从外部向累加器添加数据
override def add(word: String): Unit = {
val oldCount: Int = wcMap.getOrElse(word,0)
wcMap.update(word,oldCount + 1 )
}
// 合并两个累加器的结果
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
other.value.foreach{
case(word,count)=>{
val oldCount: Int = wcMap.getOrElse(word,0)
wcMap.update(word,oldCount + count)
}
}
}
//将数据返回给外部
override def value: mutable.Map[String, Int] = wcMap
}
5.3 广播变量
5.3.1 实现原理
广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,广播变量用起来都很顺手。在多个并行操作中使用同一个变量,但是 Spark会为每个任务分别发送。
5.3.2 基础编程
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("acc")
val sc = new SparkContext(conf)
val rdd1: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2)))
val map = mutable.Map[String,Int](("a",4) ,("b",5))
// 声明广播变量
val bcMap: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map)
val rdd3: RDD[(String, (Int, Int))] = rdd1.map {
case (word, count) => {
// 使用广播变量
val count2: Int = bcMap.value.getOrElse(word,0)
(word,(count,count2))
}
}
rdd3.collect().foreach(println)
}