SparkCore入门

第1章 Spark 概述

1.1 Spark 是什么

Spark 是一种基于内存的快速、通用、可扩展的大数据分析计算引擎。

  • Spark 是一种由Scala 语言开发的快速、通用、可扩展的大数据分析引擎
  • Spark Core 中提供了 Spark 最基础与最核心的功能
  • Spark SQL 是Spark 用来操作结构化数据的组件。通过Spark SQL,用户可以使用SQL 或者Apache Hive 版本的 SQL 方言(HQL)来查询数据。
  • Spark Streaming 是 Spark 平台上针对实时数据进行流式计算的组件,提供了丰富的处理数据流的API。

1.2 Spark 核心模块

在这里插入图片描述

第2章 Spark 快速上手

2.1 创建 Maven 项目

2.1.1 Scala 插件

Settings->Plugins->Scala->Installed

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.3 WordCount

// 创建 Spark 运行配置对象 
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("WordCount") 
 
// 创建 Spark 上下文环境对象(连接对象) 
val sc : SparkContext = new SparkContext(sparkConf) 
 
// 读取文件数据 
val fileRDD: RDD[String] = sc.textFile("input/word.txt") 
 
// 将文件中的数据进行分词 
val wordRDD: RDD[String] = fileRDD.flatMap( _.split(" ") ) 
 
// 转换数据结构 word => (word, 1) 
val word2OneRDD: RDD[(String, Int)] = wordRDD.map((_,1)) 
 
// 将转换结构后的数据按照相同的单词进行分组聚合 
val word2CountRDD: RDD[(String, Int)] = word2OneRDD.reduceByKey(_+_) 
 
// 将数据聚合结果采集到内存中 
val word2Count: Array[(String, Int)] = word2CountRDD.collect() 
 
// 打印结果 
word2Count.foreach(println) 
 
//关闭 Spark 连接 
sc.stop() 

2.1.4 log4j.properties

项目的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 

2.1.5 Run Configuration

在这里插入图片描述

第3章 Spark 运行环境

在这里插入图片描述

3.1 Local 模式

3.1.0 解压缩文件

将 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.1 启动 Local 环境

进入解压缩后的路径,执行如下指令
bin/spark-shell

3.1.2 提交应用的语句

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 模式

Spark 的Standalone 模式体现了经典的master-slave 模式。

Linux1Linux2Linux3
SparkWorker MasterWorkerWorker

3.2.0 解压缩文件

将 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-standalone 

3.2.1 修改配置文件

  1. 进入解压缩后路径的 conf 目录,修改slaves.template 文件名为slaves
mv slaves.template slaves 
vim slaves
  1. 修改 slaves 文件,添加下列文本,也即添加work 节点
linux1 
linux2 
linux3 
  1. 修改 spark-env.sh.template 文件名为spark-env.sh
mv spark-env.sh.template spark-env.sh 
  1. 修改 spark-env.sh 文件,添加JAVA_HOME 环境变量和集群对应的 master 节点
export JAVA_HOME=/opt/module/jdk1.8.0_144 
SPARK_MASTER_HOST=linux1 
SPARK_MASTER_PORT=7077 
  1. 分发 spark-standalone 目录
xsync spark-standalone 

xsync 命令脚本见该博文:集群分发脚本

3.2.2 启动集群

  1. 执行脚本命令:
[root@hadoop102 spark-standalone]# sbin/start-all.sh 
  1. 查看 Master 资源监控Web UI 界面: http://hadoop102:8080

3.2.3 提交应用

bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://hadoop102:7077 \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10
  1. –class 表示要执行程序的主类
  2. –master spark://linux1:7077 独立部署模式,连接到Spark 集群
  3. spark-examples_2.12-3.0.0.jar 运行类所在的jar 包
  4. 数字 10 表示程序的入口参数,用于设定当前应用的任务数量

3.2.4 提交参数说明


参数解释
–classSpark 程序中包含主函数的类
–masterSpark 程序运行的模式(环境)
–executor-memory 1G指定每个 executor 可用内存为1G
–total-executor-cores 2指定所有executor 使用的cpu 核数
–executor-cores指定每个executor 使用的cpu 核数
application-jar打包好的应用 jar,包含依赖。这个 URL 在集群中全局可见。 比如 hdfs:// 共享存储系统,如果是file:// path,那么所有的节点的path 都包含同样的 jar
application-arguments传给 main()方法的参数

3.2.5 配置历史服务

  1. 修改 spark-defaults.conf.template 文件名为spark-defaults.conf
mv spark-defaults.conf.template spark-defaults.conf 
  1. 修改 spark-default.conf 文件,配置日志存储路径
spark.eventLog.enabled          true 
spark.eventLog.dir               hdfs://linux1:8020/directory 

注意:需要启动hadoop 集群,HDFS 上的directory 目录需要提前存在。

sbin/start-dfs.sh 
hadoop fs -mkdir /directory 
  1. 修改 spark-env.sh 文件, 添加日志配置
export SPARK_HISTORY_OPTS=" 
-Dspark.history.ui.port=18080  
-Dspark.history.fs.logDirectory=hdfs://linux1:8020/directory  
-Dspark.history.retainedApplications=30" 
  • 参数 1 含义:WEB UI 访问的端口号为18080
  • 参数 2 含义:指定历史服务器日志存储路径
  • 参数 3 含义:指定保存Application 历史记录的个数,如果超过这个值,旧的应用程序信息将被删除,这个是内存中的应用数,而不是页面上显示的应用数。
  1. 分发配置文件
xsync conf  
  1. 重新启动集群和历史服务
sbin/start-all.sh 
sbin/start-history-server.sh 
  1. 重新执行任务
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://hadoop102:7077 \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10 
  1. 查看历史服务:http://hadoop102:18080

3.2.6 配置高可用(HA)

所谓的高可用是因为当前集群中的 Master 节点只有一个,所以会存在单点故障问题。
所以为了解决单点故障问题,需要在集群中配置多个 Master 节点,一旦处于活动状态的 Master发生故障时,由备用 Master 提供服务,保证作业可以继续执行。
高可用一般采用 Zookeeper 设置。

hadoop102hadoop103hadoop104
SparkMaster Zookeeper WorkerMaster Zookeeper WorkerZookeeper Worker
  1. 停止集群
sbin/stop-all.sh  
  1. 启动Zookeeper
  2. 修改 spark-env.sh 文件添加如下配置

注释如下内容:

#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=linux1,linux2,linux3
-Dspark.deploy.zookeeper.dir=/spark" 
  1. 分发配置文件
xsync conf/  
  1. 启动集群
sbin/start-all.sh  
  1. 启动 hadoop103的单独 Master 节点,此时hadoop103节点 Master 状态处于备用状态
[root@hadoop103 spark-standalone]# sbin/start-master.sh  
  1. 提交应用到高可用集群
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://hadoop102:7077,hadoop103:7077 \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10 
  1. 查看 hadoop103 的 Master 资源监控Web UI,稍等一段时间后,hadoop103 节点的 Master 状态提升为活动状态

3.3 Yarn 模式

3.3.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-yarn 

3.3.2 修改配置文件

  1. 修改 hadoop 配置文件/opt/module/hadoop/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> 
  1. 修改 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 提交应用

bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode cluster \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10 

3.3.5 配置历史服务器

  1. 修改 spark-defaults.conf.template 文件名为spark-defaults.conf
mv spark-defaults.conf.template spark-defaults.conf 
  1. 修改 spark-default.conf 文件,配置日志存储路径
spark.eventLog.enabled          true 
spark.eventLog.dir               hdfs://linux1:8020/directory 

注意:需要启动hadoop 集群,HDFS 上的目录需要提前存在。

[root@hadoop102 hadoop]# sbin/start-dfs.sh 
[root@hadoop102 hadoop]# hadoop fs -mkdir /directory 
  1. 修改 spark-env.sh 文件, 添加日志配置
export SPARK_HISTORY_OPTS=" 
-Dspark.history.ui.port=18080  
-Dspark.history.fs.logDirectory=hdfs://linux1:8020/directory  
-Dspark.history.retainedApplications=30" 
  • 参数 1 含义:WEB UI 访问的端口号为18080
  • 参数 2 含义:指定历史服务器日志存储路径
  • 参数 3 含义:指定保存Application 历史记录的个数,如果超过这个值,旧的应用程序信息将被删除,这个是内存中的应用数,而不是页面上显示的应用数。
  1. 修改 spark-defaults.conf
spark.yarn.historyServer.address=linux1:18080 
spark.history.ui.port=18080 
  1. 启动历史服务
sbin/start-history-server.sh  
  1. 重新提交应用
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 
  1. Web 页面查看日志:http://hadoop103:8088

3.4 K8S & Mesos 模式

Mesos 是Apache 下的开源分布式资源管理框架,它被称为是分布式系统的内核。
容器化部署是目前业界很流行的一项技术,基于Docker 镜像运行能够让用户更加方便地对应用进行管理和运维。容器管理工具中最为流行的就是Kubernetes(k8s)。

3.5 部署模式对比


模式Spark 安装机器数需启动的进程所属者应用场景
Local1Spark测试
Standalone3Master 及 WorkerSpark单独部署
Yarn1Yarn 及 HDFSHadoop混合部署

3.7 端口号

➢ Spark 查看当前Spark-shell 运行任务情况端口号:4040(计算)
➢ Spark Master 内部通信服务端口号:7077
➢ Standalone 模式下,Spark Master Web 端口号:8080(资源)
➢ Spark 历史服务器端口号:18080
➢ Hadoop YARN 任务运行情况查看端口号:8088

第4章 Spark 运行架构

4.1 运行架构

在这里插入图片描述

4.2 核心组件

4.2.1 Driver

Spark 驱动器节点,用于执行 Spark 任务中的 main 方法,负责实际代码的执行工作。
Driver 在Spark 作业执行时主要负责:
➢ 将用户程序转化为作业(job)
➢ 在 Executor 之间调度任务(task)
➢ 跟踪Executor 的执行情况
➢ 通过UI 展示查询运行情况

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)数
量。

应用程序相关启动参数如下:

名称说明
–num-executors配置 Executor 的数量
–executor-memory配置每个 Executor 的内存大小
–executor-cores配置每个 Executor 的虚拟 CPU core 数量

4.3.2 并行度(Parallelism)

在分布式计算框架中一般都是多个任务同时执行,由于任务分布在不同的计算节点进行计算,所以能够真正地实现多任务并行执行,记住,这里是并行,而不是并发。这里我们将整个集群并行执行任务的数量称之为并行度。那么一个作业到底并行度是多少呢?这个取决于框架的默认配置。应用程序也可以在运行过程中动态修改。

4.4 提交流程

在这里插入图片描述

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.2 Yarn Cluster 模式

Cluster 模式将用于监控和调度的 Driver 模块启动在Yarn 集群资源中执行。一般应用于
实际生产环境。
➢ 在 YARN Cluster 模式下,任务提交后会和ResourceManager 通讯申请启动ApplicationMaster,
➢ 随后ResourceManager 分配container,在合适的NodeManager 上启动ApplicationMaster,此时的ApplicationMaster 就是Driver。
➢ Driver 启动后向ResourceManager 申请Executor 内存,ResourceManager 接到ApplicationMaster 的资源申请后会分配container,然后在合适的NodeManager 上启动Executor 进程
➢ Executor 进程启动后会向Driver 反向注册,Executor 全部注册完成后Driver 开始执行main 函数,
➢ 之后执行到Action 算子时,触发一个Job,并根据宽依赖开始划分 stage,每个stage 生成对应的TaskSet,之后将task 分发到各个Executor 上执行。

第5章 Spark 核心编程

Spark 计算框架为了能够进行高并发和高吞吐的数据处理,封装了三大数据结构,用于处理不同的应用场景。三大数据结构分别是:

数据结构说明
RDD弹性分布式数据集
累加器分布式共享只写变量
广播变量分布式共享只读变量

5.1 RDD

5.1.1 RDD 特性

RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark 中最基本的数据处理模型。
➢ 弹性

  • 存储的弹性:内存与磁盘的自动切换;
  • 容错的弹性:数据丢失可以自动恢复;
  • 计算的弹性:计算出错重试机制;
  • 分片的弹性:可根据需要重新分片。

➢ 分布式:数据存储在大数据集群不同节点上
➢ 数据集:RDD 封装了计算逻辑,并不保存数据
➢ 数据抽象:RDD 是一个抽象类,需要子类具体实现
➢ 不可变:RDD 封装了计算逻辑,是不可以改变的,想要改变,只能产生新的RDD,在新的RDD 里面封装计算逻辑
➢ 可分区、并行计算

5.1.2 核心属性

➢ 分区列表
RDD 数据结构中存在分区列表,用于执行任务时并行计算,是实现分布式计算的重要属性。

getPartitions: Array[Partition]

➢ 分区计算函数
Spark 在计算时,是使用分区函数对每一个分区进行计算

compute(spilit: Partition, context: TaskContext): Iterator[T]

➢ RDD 之间的依赖关系
RDD 是计算模型的封装,当需求中需要将多个计算模型进行组合时,就需要将多个 RDD 建立依赖关系

getDependencies: Seq[Dependency[_]]=deps

➢ 分区器(可选)
当数据为KV 类型数据时,可以通过设定分区器自定义数据的分区

val partitioner: Option[Partitioner]=None

➢ 首选位置(可选)
计算数据时,可以根据计算节点的状态选择不同的节点位置进行计算

getPreferredLocations(split: Partition):Seq[String]=Nil

5.1.3 执行原理

在Yarn 环境中,RDD的工作原理:

  1. 启动Yarn 集群环境
    在这里插入图片描述

  2. Spark 通过申请资源创建调度节点和计算节点
    在这里插入图片描述

  3. Spark 框架根据需求将计算逻辑根据分区划分成不同的任务
    在这里插入图片描述

  4. 调度节点将任务根据计算节点状态发送到对应的计算节点进行计算
    在这里插入图片描述

5.1.4 基础编程

5.1.4.1 RDD 创建
  1. 从集合(内存)中创建 RDD
    从集合中创建RDD,Spark 主要提供了两个方法:parallelize 和makeRDD
// spark配置:setMaster为local[*],setAppName为spark
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("spark") 
// 创建SparkContext
val sparkContext = new SparkContext(sparkConf) 

// parallelize 方法创建RDD
val rdd1 = sparkContext.parallelize( 
    List(1,2,3,4) 
) 

// makeRDD 方法创建RDD
val rdd2 = sparkContext.makeRDD( 
    List(1,2,3,4) 
) 
// 打印RDD
rdd1.collect().foreach(println) 
rdd2.collect().foreach(println) 
// sparkContext停止
sparkContext.stop() 

从底层代码实现来讲,makeRDD 方法其实就是parallelize 方法

def makeRDD[T: ClassTag](seq: Seq[T], 
					     numSlices: Int = defaultParallelism): RDD[T] = withScope { 
  // 可看到在makeRDD函数体中调用了parallelize
  parallelize(seq, numSlices) 
} 
  1. 从外部存储(文件)创建RDD
    由外部存储系统的数据集创建RDD包括:
    ①本地的文件系统
    ②所有Hadoop 支持的数据集:比如HDFS、HBase 等。
// 同上
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("spark") 
val sparkContext = new SparkContext(sparkConf) 
// sparkContext.textFile("input")  <= input.txt
val fileRDD: RDD[String] = sparkContext.textFile("input") 
// 同上
fileRDD.collect().foreach(println)
sparkContext.stop()
  1. 从其他RDD 创建
    主要是通过一个RDD 运算完后,再产生新的RDD。详情请参考后续章节

  2. 直接创建RDD(new)
    使用new 的方式直接构造RDD,一般由Spark 框架自身使用。

5.1.4.2 RDD 并行度与分区

Spark 可以将一个作业切分多个任务后,发送给Executor 节点并行计算,而能够并行计算的任务数量我们称之为并行度。
这里的并行执行的任务数量,并不是指的切分任务的数量。

val dataRDD: RDD[Int] = 
    sparkContext.makeRDD( 
        List(1,2,3,4), 
        4) // 并行执行的任务数量为4
val fileRDD: RDD[String] = 
    sparkContext.textFile( 
        "input", 
        2) // 并行执行的任务数量为2

数据分区规则的Spark 核心源码如下:

def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = { 
	// 0< i <numSlices
  (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; 
    // 检查是否有无效文件
    for (FileStatus file: files) { 
      if (file.isDirectory()) { 
        throw new IOException("Not a file: "+ file.getPath()); 
      } 
      totalSize += file.getLen(); 
    } 
 	// 若numSplits为0则goalSize设为totalSize,若不为0则拆分
    long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits); 
    // minSize 
    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 转换算子
map

➢ 函数签名

def map[U: ClassTag](f: T => U): RDD[U] 

➢ 函数说明
将处理的数据逐条进行映射转换

val dataRDD: RDD[Int] = sparkContext.makeRDD(List(1,2,3,4)) 
// 效果是所有数值*2,值的转换
val dataRDD1: RDD[Int] = dataRDD.map( 
    num => { 
        num * 2 
    } 
) 
// 效果是所有数值转为String类型,类型的转换
val dataRDD2: RDD[String] = dataRDD1.map( 
    num => { 
        "" + num 
    } 
) 
mapPartitions

➢ 函数签名

def mapPartitions[U: ClassTag]( 
 f: Iterator[T] => Iterator[U], 
 preservesPartitioning: Boolean = false): RDD[U] 
val dataRDD1: RDD[Int] = dataRDD.mapPartitions( 
	// 筛选出==2的数据
    datas => { 
        datas.filter(_==2) 
    } 
) 

角度MapmapPartitions
数据处理是分区内一个数据一个数据的执行,类似于串行操作。是以分区为单位进行批处理操作。
功能主要目的将数据源中的数据进行转换和改变,不会减少或增多数据。需要传递一个迭代器,返回一个迭代器,没有要求的元素的个数保持不变,所以可以增加或减少数据
性能性能比较低类似于批处理,所以性能较高。但是会长时间占用内存,那么这样会导致内存可能不够用,出现内存溢出的错误。所以在内存有限的情况下,不推荐使用
mapPartitionsWithIndex

➢ 函数签名

def mapPartitionsWithIndex[U: ClassTag]( 
 f: (Int, Iterator[T]) => Iterator[U], 
 preservesPartitioning: Boolean = false): RDD[U] 

➢ 函数说明
将待处理的数据以分区为单位发送到计算节点进行处理,
这里的处理是指可以进行任意的处理,哪怕是过滤数据,在处理时同时可以获取当前分区索引。

val dataRDD1 = dataRDD.mapPartitionsWithIndex( 
    (index, datas) => { 
         datas.map(index, _) 
    } 
) 
flatMap

➢ 函数签名

def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U] 

➢ 函数说明
将处理的数据进行扁平化后再进行映射处理,所以算子也称之为扁平映射

val dataRDD = sparkContext.makeRDD(List(List(1,2),List(3,4)),1) 
val dataRDD1 = dataRDD.flatMap( 
	// 将List(List(1,2),List(3,4))进行扁平化操作 
    list => list 
) 
glom

➢ 函数签名

def glom(): RDD[Array[T]] 

➢ 函数说明
将同一个分区的数据直接转换为相同类型的内存数组进行处理,分区不变

val dataRDD = sparkContext.makeRDD(List( 1,2,3,4 ),1) 
val dataRDD1:RDD[Array[Int]] = dataRDD.glom() 
groupBy

➢ 函数签名

def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])] 

➢ 函数说明
将数据根据指定的规则进行分组, 分区默认不变,但是数据会被打乱重新组合,我们将这样的操作称之为shuffle。极限情况下,数据可能被分在同一个分区中一个组的数据在一个分区中,但是并不是说一个分区中只有一个组 。

val dataRDD = sparkContext.makeRDD(List(1,2,3,4),1) 
// 1分到1,2分到0,3分到1,4分到0
val dataRDD1 = dataRDD.groupBy( _%2 ) 
filter

➢ 函数签名

def filter(f: T => Boolean): RDD[T] 

➢ 函数说明
将数据根据指定的规则进行筛选过滤,符合规则的数据保留,不符合规则的数据丢弃。
当数据进行筛选过滤后,分区不变,但是分区内的数据可能不均衡,生产环境下,可能会出现数据倾斜。

val dataRDD = sparkContext.makeRDD(List( 1,2,3,4 ),1) 
// 筛出:2、4,因为_%2 == 0
val dataRDD1 = dataRDD.filter(_%2 == 0) 
sample

➢ 函数签名

def sample( 
// 为true时是抽取数据放回(泊松算法) ,为false时是抽取数据不放回(伯努利算法) 
 withReplacement: Boolean, 
 fraction: Double, 
 seed: Long = Utils.random.nextLong): RDD[T] 

➢ 参数说明

算法是否放回第一个参数第二个参数第三个参数
伯努利算法Nfalse抽取的几率,范围在[0,1]之间,0:全不取;1:全取随机数种子
泊松算法Ytrue重复数据的几率,范围大于等于 0.表示每一个元素被期望抽取到的次数随机数种子

➢ 函数说明
根据指定的规则从数据集中抽取数据

val dataRDD = sparkContext.makeRDD(List( 1,2,3,4 ),1) 
// 抽取数据不放回(伯努利算法) 
// 伯努利算法:又叫 0、1 分布。例如扔硬币,要么正面,要么反面。 
// 具体实现:根据种子和随机算法算出一个数和第二个参数设置几率比较,小于第二个参数要,大于不要 
val dataRDD1 = dataRDD.sample(false, 0.5) 
// 抽取数据放回(泊松算法) 
val dataRDD2 = dataRDD.sample(true, 2) 
distinct

➢ 函数签名

// 没有传入参数的,无参distinct()中又调用了带参数的distinct(partitions.length)
def distinct()(implicit ord: Ordering[T] = null): RDD[T] 
// 有传入参数numPartitions的
def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] 

➢ 函数说明
将数据集中重复的数据去重

val dataRDD = sparkContext.makeRDD(List( 1,2,3,4,1,2 ),1) 
val dataRDD1 = dataRDD.distinct() 
val dataRDD2 = dataRDD.distinct(2) 

➢ 原理

  • 使用map算子把元素转为一个带有null的元组;
  • 使用reducebykey对具有相同key的元素进行统计;
  • 再使用map算子,取得元组中的单词元素,实现去重。
coalesce

➢ 函数签名

def coalesce(
	numPartitions: Int, 
	shuffle: Boolean = false, 
	partitionCoalescer: Option[PartitionCoalescer] = Option.empty) 
 (implicit ord: Ordering[T] = null) : RDD[T] 

➢ 函数说明
根据数据量缩减分区,用于大数据集过滤后,提高小数据集的执行效率
当 spark 程序中,存在过多的小任务的时候,可以通过 coalesce 方法,收缩合并分区,减少分区的个数,减小任务调度成本

val dataRDD = sparkContext.makeRDD(List( 1,2,3,4,1,2 ),6) 
// 收缩合并分区
val dataRDD1 = dataRDD.coalesce(2) 
repartition

➢ 函数签名

def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] 

➢ 函数说明
该操作内部其实执行的是 coalesce 操作,参数shuffle 的默认值为true。
无论是将分区数多的 RDD 转换为分区数少的 RDD,还是将分区数少的 RDD 转换为分区数多的RDD,repartition操作都可以完成,因为无论如何都会经 shuffle 过程。

val dataRDD = sparkContext.makeRDD(List( 
    1,2,3,4,1,2 
),2) 
// 分区数少的 RDD 转换为分区数多的RDD
val dataRDD1 = dataRDD.repartition(4) 
sortBy

➢ 函数签名

def sortBy[K]( 
 f: (T) => K, 
 ascending: Boolean = true, 
 numPartitions: Int = this.partitions.length) 
 (implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T] 

➢ 函数说明
该操作用于排序数据。

  • 在排序之前,可以将数据通过 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

➢ 函数签名

def intersection(other: RDD[T]): RDD[T] 

➢ 函数说明
对源RDD 和参数RDD 求交集后返回一个新的RDD

val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4)) 
val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6)) 
// A.intersection(B)
val dataRDD = dataRDD1.intersection(dataRDD2) 
union

➢ 函数签名

def union(other: RDD[T]): RDD[T] 

➢ 函数说明
对源RDD 和参数RDD 求并集后返回一个新的RDD

val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4)) 
val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6)) 
// A.union(B)
val dataRDD = dataRDD1.union(dataRDD2) 
subtract

➢ 函数签名

def subtract(other: RDD[T]): RDD[T] 

➢ 函数说明
以一个RDD 元素为主,去除两个RDD 中重复元素,将其他元素保留下来。求差集

val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4)) 
val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6)) 
// A.subtract(B)
val dataRDD = dataRDD1.subtract(dataRDD2) 
zip

➢ 函数签名

def zip[U: ClassTag](other: RDD[U]): RDD[(T, U)] 

➢ 函数说明
将两个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)) 
// key.zip(value)
val dataRDD = dataRDD1.zip(dataRDD2) 
reduceByKey

➢ 函数签名
无参版和有参版

def reduceByKey(func: (V, V) => V): RDD[(K, V)] 
def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)] 

➢ 函数说明
可以将数据按照相同的Key 对Value 进行聚合

val dataRDD1 = sparkContext.makeRDD(List(("a",1),("b",2),("c",3))) 
val dataRDD2 = dataRDD1.reduceByKey(_+_) 
val dataRDD3 = dataRDD1.reduceByKey(_+_, 2) 
groupByKey

➢ 函数签名

def groupByKey(): RDD[(K, Iterable[V])] 
def groupByKey(numPartitions: Int): RDD[(K, Iterable[V])] 
def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])] 

➢ 函数说明
将数据源的数据根据 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 区别
角度reduceByKeygroupByKey
shufflereduceByKey可以在shuffle 前对分区内相同 key 的数据进行预聚合功能,这样会减少落盘的数据量,reduceByKey 性能比较高。groupByKey 只是进行分组,不存在数据量减少的问题。
功能reduceByKey 其实包含分组和聚合的功能,所以在分组聚合的场合下,推荐使用reduceByKey。GroupByKey 只能分组,不能聚合,如果仅仅是分组而不需要聚合。那么还是只能使用groupByKey
aggregateByKey

➢ 函数签名

def aggregateByKey[U: ClassTag](zeroValue: U)
	(seqOp: (U, V) => U, 
 	 combOp: (U, U) => U): RDD[(K, U)] 

➢ 函数说明
将数据根据不同的规则进行分区内计算和分区间计算

val dataRDD1 = sparkContext.makeRDD(List(("a",1),("b",2),("c",3))) 
val dataRDD2 = dataRDD1.aggregateByKey(0)(_+_,_+_) 
foldByKey

➢ 函数签名

def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)] 

➢ 函数说明
当分区内计算规则和分区间计算规则相同时,aggregateByKey 就可以简化为foldByKey

val dataRDD1 = sparkContext.makeRDD(List(("a",1),("b",2),("c",3))) 
val dataRDD2 = dataRDD1.foldByKey(0)(_+_) 
combineByKey

➢ 函数签名

def combineByKey[C]( 
 createCombiner: V => C, 
 mergeValue: (C, V) => C, 
 mergeCombiners: (C, C) => C): RDD[(K, C)] 

➢ 函数说明
最通用的对 key-value 型 rdd 进行聚集操作的聚集函数(aggregation function)。
类似于aggregate(),combineByKey()允许用户返回值的类型与输入不一致。

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) 
) 
sortByKey

➢ 函数签名

def sortByKey(
	ascending: Boolean = true, 
	numPartitions: Int = self.partitions.length): RDD[(K, V)] 

➢ 函数说明
在一个 (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

➢ 函数签名

def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))] 

➢ 函数说明
在类型为 (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

➢ 函数签名

def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))] 

➢ 函数说明
类似于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

➢ 函数签名

def cogroup[W](other: RDD[(K, W)]): RDD[(K, (Iterable[V], Iterable[W]))] 

➢ 函数说明
在类型为(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.5 RDD 行动算子
reduce

➢ 函数签名

def reduce(f: (T, T) => T): T 

➢ 函数说明
聚集RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据

val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4)) 
 
// 聚合数据 
val reduceResult: Int = rdd.reduce(_+_) 
collect

➢ 函数签名

def collect(): Array[T] 

➢ 函数说明
在驱动程序中,以数组Array 的形式返回数据集的所有元素

val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4)) 
 
// 收集数据到 Driver 
rdd.collect().foreach(println) 
count

➢ 函数签名

def count(): Long 

➢ 函数说明
返回 RDD 中元素的个数

val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4)) 
 
// 返回 RDD 中元素的个数 
val countResult: Long = rdd.count() 
first

➢ 函数签名

def first(): T 

➢ 函数说明
返回RDD 中的第一个元素

val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4)) 
 
// 返回 RDD 中元素的个数 
val firstResult: Int = rdd.first() 
println(firstResult) 
take

➢ 函数签名

def take(num: Int): Array[T] 

➢ 函数说明
返回一个由 RDD 的前 n 个元素组成的数组

vval rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4)) 
 
// 返回 RDD 中元素的个数 
val takeResult: Array[Int] = rdd.take(2) 
println(takeResult.mkString(",")) 
takeOrdered

➢ 函数签名

def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T] 

➢ 函数说明
返回该 RDD 排序后的前 n 个元素组成的数组

val rdd: RDD[Int] = sc.makeRDD(List(1,3,2,4)) 
 
// 返回 RDD 排序后的前 2 个元素组成的数组 
val result: Array[Int] = rdd.takeOrdered(2) 
aggregate

➢ 函数签名

def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U 

➢ 函数说明
分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合

val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 8) 
 
// 将该 RDD 所有元素相加得到结果 
//val result: Int = rdd.aggregate(0)(_ + _, _ + _) 
val result: Int = rdd.aggregate(10)(_ + _, _ + _) 
fold

➢ 函数签名

def fold(zeroValue: T)(op: (T, T) => T): T 

➢ 函数说明
折叠操作,aggregate 的简化版操作

val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4)) 
 
val foldResult: Int = rdd.fold(0)(_+_) 
countByKey

➢ 函数签名

def countByKey(): Map[K, Long] 

➢ 函数说明
统计每种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() 
save 相关算子

➢ 函数签名

def saveAsTextFile(path: String): Unit 
def saveAsObjectFile(path: String): Unit 
def saveAsSequenceFile( 
 path: String, 
 codec: Option[Class[_ <: CompressionCodec]] = None): Unit 

➢ 函数说明
将数据保存到不同格式的文件中

// 保存成 Text 文件 
rdd.saveAsTextFile("output") 
 
// 序列化成对象保存到文件 
rdd.saveAsObjectFile("output1") 
 
// 保存成 Sequencefile 文件 
rdd.map((_,1)).saveAsSequenceFile("output2") 
foreach

➢ 函数签名

def foreach(f: T => Unit): Unit = withScope { 
 val cleanF = sc.clean(f) 
 sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF)) 
} 

➢ 函数说明
分布式遍历RDD 中的每一个元素,调用指定函数

val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4)) 
 
// 收集后打印 
rdd.map(num=>num).collect().foreach(println) 
 
println("****************") 
 
// 分布式打印 
rdd.foreach(println) 
5.1.4.6 RDD 序列化
  1. 闭包检查
    从计算的角度, 算子以外的代码都是在Driver 端执行, 算子里面的代码都是在 Executor端执行。
    导致算子内经常会用到算子外的数据,这样就形成了闭包的效果,如果使用的算子外的数据无法序列化,就意味着无法传值给Executor端执行,就会发生错误,所以需要在执行任务计算前,检测闭包内的对象是否可以进行序列化
  2. Kryo 序列化框架
    参考地址: https://github.com/EsotericSoftware/kryo
    Kryo 速度是Serializable 的10 倍。当 RDD 在Shuffle 数据的时候,简单数据类型、数组和字符串类型已经在Spark 内部使用 Kryo 来序列化。
    **注意:即使使用Kryo 序列化,也要继承Serializable 接口。 **
 val conf: SparkConf = new SparkConf() 
                .setAppName("SerDemo") 
                .setMaster("local[*]") 
                // 替换默认的序列化机制 
                .set("spark.serializer", 
"org.apache.spark.serializer.KryoSerializer") 
                // 注册需要使用 kryo 序列化的自定义类 
                .registerKryoClasses(Array(classOf[Searcher])) 
5.1.4.7 RDD 依赖关系
  1. RDD 血缘关系
    RDD 只支持粗粒度转换,即在大量记录上执行的单个操作。将创建 RDD 的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDD 的Lineage 会记录RDD 的元数据信息和转换行为,当该RDD 的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。
(RDD[String]).toDebugString
  1. RDD 依赖关系
    所谓的依赖关系,其实就是两个相邻RDD 之间的关系
(RDD[String]).dependencies
依赖RDD 窄依赖RDD 宽依赖
定义表示每一个父RDD的Partition最多被子RDD的一个Partition 使用宽依赖表示同一个父RDD的Partition被多个子RDD的Partition 依赖,会引起Shuffle
  1. RDD 阶段划分源码
try { 
  // New stage creation may throw an exception if, for example, jobs are run on a 
  // HadoopRDD whose underlying HDFS files have been deleted. 
  // 调用创建stage函数
  finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite) 
} catch { 
  case e: Exception => 
    logWarning("Creating new stage failed due to exception - job: " + jobId, e) 
    listener.jobFailed(e) 
    return 
} 
 
…… 
 
private def createResultStage( 
  rdd: RDD[_], 
  func: (TaskContext, Iterator[_]) => _, 
  partitions: Array[Int], 
  jobId: Int, 
  callSite: CallSite): ResultStage = { 
	// 获取或创建父Stage
	val parents = getOrCreateParentStages(rdd, jobId) 
	// 获取或自增Id
	val id = nextStageId.getAndIncrement() 
	// new ResultStage类
	val stage = new ResultStage(id, rdd, func, partitions, parents, jobId, callSite) 
	stageIdToStage(id) = stage 
	updateJobIdStageIdMaps(jobId, stage) 
	stage 
} 
…… 
// 获取或创建父Stage函数
private def getOrCreateParentStages(rdd: RDD[_], firstJobId: Int): List[Stage] 
= { 
	// 获取Shuffle依赖
	getShuffleDependencies(rdd).map { shuffleDep => 
	  getOrCreateShuffleMapStage(shuffleDep, firstJobId) 
	}.toList 
} 
 
…… 
 
private[scheduler] def getShuffleDependencies( 
  rdd: RDD[_]): HashSet[ShuffleDependency[_, _, _]] = { 
	val parents = new HashSet[ShuffleDependency[_, _, _]] 
	val visited = new HashSet[RDD[_]] 
	val waitingForVisit = new Stack[RDD[_]] 
	waitingForVisit.push(rdd) 
	while (waitingForVisit.nonEmpty) { 
	  val toVisit = waitingForVisit.pop() 
	  if (!visited(toVisit)) { 
	    visited += toVisit 
	    toVisit.dependencies.foreach { 
	      case shuffleDep: ShuffleDependency[_, _, _] => 
	        parents += shuffleDep 
	      case dependency => 
	        waitingForVisit.push(dependency.rdd) 
	    } 
	  } 
	} 
	parents 
} 
  1. RDD 任务划分

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.8 RDD 持久化
  1. RDD Cache 缓存
    RDD 通过Cache 或者Persist 方法将前面的计算结果缓存,默认情况下会把数据以缓存在JVM 的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的 action 算子时,该RDD 将会被缓存在计算节点的内存中,并供后面重用。
// 数据缓存。 
wordToOneRdd.cache() 
 
// 可以更改存储级别 
//mapRdd.persist(StorageLevel.MEMORY_AND_DISK_2) 
object StorageLevel { 
  val NONE = new StorageLevel(false, false, false, false) 
  val DISK_ONLY = new StorageLevel(true, false, false, false) 
  val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2) 
  val MEMORY_ONLY = new StorageLevel(false, true, false, true) 
  val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2) 
  val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false) 
  val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2) 
  val MEMORY_AND_DISK = new StorageLevel(true, true, false, true) 
  val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2) 
  val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false) 
  val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2) 
  val OFF_HEAP = new StorageLevel(true, true, true, false, 1) 
级别使用的空间CPU时间是否在内存中是否在磁盘上
DISK_ONLY
MEMORY_ONLY
MEMORY_ONLY_SER
MEMORY_AND_DISK中等部分部分
MEMORY_AND_DISK_SER部分部分

缓存有可能丢失,或者存储于内存的数据由于内存不足而被删除,RDD 的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD 的一系列转换,丢失的数据会被重算,由于RDD 的各个Partition 是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition。

  1. RDD CheckPoint 检查点
    所谓的检查点其实就是通过将RDD 中间结果写入磁盘 由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点之后有节点出现问题,可以从检查点开始重做血缘,减少了开销。
    对RDD 进行checkpoint 操作并不会马上被执行,必须执行Action 操作才能触发。
// 设置检查点路径 
sc.setCheckpointDir("./checkpoint1") 
....业务逻辑 
// 增加缓存,避免再重新跑一个 job 做 checkpoint 
wordToOneRdd.cache() 
// 数据检查点:针对 wordToOneRdd 做检查点计算 
wordToOneRdd.checkpoint() 
 
// 触发执行逻辑 
wordToOneRdd.collect().foreach(println) 
  1. 缓存和检查点区别
    1)Cache 缓存只是将数据保存起来,不切断血缘依赖。Checkpoint 检查点切断血缘依赖。
    2)Cache 缓存的数据通常存储在磁盘、内存等地方,可靠性低。Checkpoint 的数据通常存储在HDFS 等容错、高可用的文件系统,可靠性高。
    3)建议对checkpoint()的RDD 使用Cache 缓存,这样 checkpoint 的job 只需从 Cache 缓存中读取数据即可,否则需要再从头计算一次RDD。
5.1.4.9 RDD 分区器

Spark 目前支持Hash 分区和Range 分区,和用户自定义分区。Hash 分区为当前的默认分区。分区器直接决定了RDD 中分区的个数、RDD 中每条数据经过Shuffle 后进入哪个分区,进而决定了Reduce 的个数。
➢ 只有Key-Value 类型的RDD 才有分区器,非 Key-Value 类型的RDD 分区的值是 None
➢ 每个RDD 的分区 ID 范围:0 ~ (numPartitions - 1),决定这个值是属于那个分区的。

  1. Hash 分区:对于给定的 key,计算其hashCode,并除以分区个数取余

  2. Range 分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序

5.1.4.10 RDD 文件读取与保存

Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。
文件格式分为:text 文件、csv 文件、sequence 文件以及Object 文件;
文件系统分为:本地文件系统、HDFS、HBASE 以及数据库。
➢ text 文件

// 读取输入文件 
val inputRDD: RDD[String] = sc.textFile("input/1.txt") 
 
// 保存数据 
inputRDD.saveAsTextFile("output") 

➢ sequence 文件
SequenceFile 文件是Hadoop 用来存储二进制形式的key-value 对而设计的一种平面文件(Flat
File)。在 SparkContext 中,可以调用sequenceFile[keyClass, valueClass](path)。

// 保存数据为 SequenceFile 
dataRDD.saveAsSequenceFile("output") 
 
// 读取 SequenceFile 文件 
sc.sequenceFile[Int,Int]("output").collect().foreach(println) 

➢ object 对象文件
对象文件是将对象序列化后保存的文件,采用Java 的序列化机制。可以通过objectFileT:
ClassTag
函数接收一个路径,读取对象文件,返回对应的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。

// 1. 继承 AccumulatorV2,并设定泛型 
// 2. 重写累加器的抽象方法 
class WordCountAccumulator extends AccumulatorV2[String, mutable.Map[String,Long]]{ 
 
	var map : mutable.Map[String, Long] = mutable.Map() 
	 
	// 累加器是否为初始状态 
	override def isZero: Boolean = { 
	  map.isEmpty 
	} 
	 
	// 复制累加器
	override def copy(): AccumulatorV2[String, mutable.Map[String, Long]] = { 
	  new WordCountAccumulator 
	} 
	 
	// 重置累加器 
	override def reset(): Unit = { 
	  map.clear() 
	} 
	 
	// 向累加器中增加数据 (In) 
	override def add(word: String): Unit = { 
	 // 查询 map 中是否存在相同的单词 
	 // 如果有相同的单词,那么单词的数量加 1 
	 // 如果没有相同的单词,那么在 map 中增加这个单词 
	    map(word) = map.getOrElse(word, 0L) + 1L 
	} 
	 
	// 合并累加器 
	override def merge(other: AccumulatorV2[String, mutable.Map[String, Long]]): 
	Unit = { 
	 
	  val map1 = map 
	  val map2 = other.value 
	 
	 // 两个 Map 的合并 
	  map = map1.foldLeft(map2)( 
	    ( innerMap, kv ) => { 
	      innerMap(kv._1) = innerMap.getOrElse(kv._1, 0L) + kv._2 
	      innerMap 
	    } 
	  ) 
	} 
	 
	// 返回累加器的结果 (Out) 
	override def value: mutable.Map[String, Long] = map 
} 

5.3 广播变量

5.3.1 实现原理

广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个Spark 操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,广播变量用起来都很顺手。在多个并行操作中使用同一个变量,但是 Spark 会为每个任务分别发送。

val rdd1 = sc.makeRDD(List( ("a",1), ("b", 2), ("c", 3), ("d", 4) ),4) 
val list = List( ("a",4), ("b", 5), ("c", 6), ("d", 7) ) 
// 声明广播变量 
val broadcast: Broadcast[List[(String, Int)]] = sc.broadcast(list) 
 
val resultRDD: RDD[(String, (Int, Int))] = rdd1.map { 
  case (key, num) => { 
    var num2 = 0 
    // 使用广播变量 
    for ((k, v) <- broadcast.value) { 
      if (k == key) { 
        num2 = v 
      } 
    } 
    (key, (num, num2)) 
  } 
} 

参考尚硅谷Spark课程制作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值