文章目录
2 Spark运行架构
2.1 运行框架
Spark框架的核心是一个计算引擎,采用了标准的主从结构,也就是master-worker结构。如下图所示,Driver 表示master,负责集群的作业任务调度,而executor就是worker,负责实际任务的执行
2.2 核心组件
2.2.1 Driver
Spark的驱动节点,接收任务的接口,Spark作业执行时主要负责:
- 在Executor之间安排并调度任务
- 跟踪Executor情况
2.2.2 Executor
Spark Executor 是集群中工作节点(Worker)中的一个 JVM 进程,负责在 Spark 作业中运行具体任务(Task),任务彼此之间相互独立。Spark 应用启动时,Executor 节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有 Executor 节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他 Executor 节点上继续运行。
2.3 核心概念
2.3.1 executor 与 core
Executor本质上是一个进程,是Spark专门用于执行任务的节点,在启动时可以给其分配资源,所谓的资源就是工作节点Executor的内存大小以及虚拟CPU核数。
参数 | 说明 |
---|---|
–num-executor | 配置工作节点的个数 |
–executor-memory | 配置executor的内存大小 |
–executor-cores | 每个进程的虚拟cpu核数 |
2.3.2 并行度
在分布式计算框架中一般都是多个任务同时执行,由于任务分布在不同的计算节点进行计算,所以能够真正地实现多任务并行执行,这里我们将 整个集群并行执行任务的数量称之为并行度。并行度 = executor * cores(当然不能操作计算机最大情况)
- 并行:多个任务同时在多个进程上进行
- 并发:多个任务同时申请在同一个进程上执行
2.4 提交模式(yarn)
一般的提交流程如下图所示,但具体还有所不同,Spark 应用程序提交到 Yarn 环境中执行的时候,一般会有两种部署执行的方式:Client 和 Cluster。两种模式主要区别在于:Driver 程序的运行节点位置。
2.4.1 Yarn架构
-
Yarn是一个资源调度的工具,在多个任务执行时,集群的资源分配,其组成角色可以分为:
- resource manager:整个集群的资源管理者
- NodeManager:当个服务器的资源管理者
- ApplicationMaster:单个任务的管理者
- container:容器,存放资源
-
既然是集群,那就是有个总的管理者:resource manager,他管理着整个集群的资源,所有资源都需要通过他的调控,各个服务器也有自己的管理者:NodeManager,管理该服务器的资源。所谓资源调度,就是在执行多个任务时的资源分配,既然有任务,那就得有一个角色来负责处理这个任务,那就是 ApplicationMaster,他管理单个任务,既然资源能分配,那就得有东西来储存你分配的资源,因此就有 container(容器) 来储存资源。
-
合作流程:假设现在有job请求,那么resource Manager会首先收到情况,然后在合适的nodemanager上创建一个ApplicationMaster来接待它,ApplicationMaste首先对job的情况进行了解,明白了job需要的资源数量后,告诉resource manager情况,resource manager,会根据job的情况在合适的nodemanager上分配一个container,然后application会在该container上完成job任务。
(可以想象成一个顾客买东西的时候)
图片来源与于尚硅谷教堂
2.4.2 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 上执行。
2.4.3 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 上执行。
2.5 分布式计算
当Driver接收到job任务时,其将根据任务的逻辑对数据进行分块,并将逻辑和数据块进行封装成新的Task,然后传递给 Executor进行执行,执行完毕后的结果重新传递给Driver进行聚合,这就是所谓的分布式计算。注意:
- Driver接收到的Jobs中,既包含逻辑,也包含数据。
- 分块后的Task 也包含逻辑以及数据,只不过数据不是全部数据,而是job数据中的一部分
3 核心概念
Spark 计算框架为了能够进行高并发和高吞吐的数据处理,主要有三大数据结构,用于 处理不同的应用场景。三大数据结构分别是:
- RDD,弹性分布式数据集
- 累加器:分布式共享只写变量
- 广播变量:分布式共享只读变量
3.1 RDD
3.1.1 什么是RDD
IO指input 和 output 过程,RDD IO 和正常IO及其相似。主要的IO方式有字节流IO,字符流IO
a) 字节流IO
输入和输出都是以字节为单位进行操作的,因此称之为字节流IO(stream IO),过程以下方粒子所示。
在此代码中,每读一个字节就会输出一个字节,其过程就是 input-output -input 之间不断循环,每次input后将执行一次output后才会再一次进行input,读取效率较低。
InputStream in = new FIleInputStream("path")
int i = -1
while ((i = in.read()) ){
println(i);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
为改善其读一个输出一个导致的效率低下,通常情况下会引入一个缓冲区,来暂时存放数据,见下方例子。
见下图,所谓缓存区(Buff)作用就是将读取到的字节先放在Buff中,等Buff 中数据量达到阈值就将其输出,因此每次input不需要等待 output。(这也是所谓的批处理思想)
b) 字符流IO
将字节转化为字符,再进行输出,就是所谓的字符流IO,其本质其实就是对字节流进行修饰。如下图为字节流IO的模型,其本质就是将字节流IO进行包装成字符流:当读入字节后、储存在InputStreamReader中,当组成一个字符后,将传递到Buff中进行缓存,达到缓存阈值后再输出于工作台。
如图所示,IO模式体现了装饰者设计模式:将最基础的类进行不断的封装,达到一个功能强大的类。
c) RDD IO
RDD的IO模式体现装饰者,强大的功能都是通过不断封装原RDD(textFilie)得到的,如下图所示。注意:
- RDD数据只有在调用collect()时才会真正执行代码逻辑,包装只是对逻辑进行包装。
- RDD在传递过程中是保存数据的,而IO模式会保留数据
d) 什么是RDD
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。spark中所有执行类都是RDD类。
-
弹性 存储的弹性:内存与磁盘的自动切换;
⚫ 容错的弹性:数据丢失可以自动恢复;
⚫ 计算的弹性:计算出错重试机制;
⚫ 分片的弹性:可根据需要重新分片。
-
分布式:数据存储在大数据集群不同节点上
-
数据集:RDD 封装了计算逻辑,并不保存数据
-
数据抽象:RDD 是一个抽象类,需要子类具体实现
-
不可变:RDD 封装了计算逻辑,是不可以改变的,想要改变,只能产生新的 RDD,在 新的 RDD 里面封装计算逻辑
-
可分区、并行计算
3.2 RDD编程基础
在idea中需要提前连接环境,见下图
package RDD_build
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object RDD_memory {
def main(args: Array[String]): Unit = {
// TODO 准备环境
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD") // 申请
val sc = new SparkContext(sparkConf) // 连接
// TODO 任务
command。。。。
// TODO 关闭环境
sc.stop()
}
}
3.2.1 创建RDD
a) 从内存中创建
-
sc.makeRDD(seq):是sc.arallelize的包装,其实际调用的就是paralielize 方法。
-
sc.parallelize(seq)
其中seq表示传入的数据类型(常见的数据类型都为Seq型)
package RDD_build
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object RDD_memory {
def main(args: Array[String]): Unit = {
// TODO 准备环境
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD") // 申请
val sc = new SparkContext(sparkConf) // 连接
// TODO 创建RDD
// 集合中创建
val seq = Seq[Int](1,2,3,4)
val rdd1: RDD[Int] = sc.parallelize(seq)
val rdd2: RDD[Int] = sc.makeRDD(seq)
rdd1.collect().foreach(println)
rdd2.collect().foreach(println)
// TODO 关闭环境
sc.stop()
}
}
b) 从外部文件中创建
-
方法:
sc.textFile(“path”):以行为单位进行读取,所有文件中的每一行进行输出
sc.wholeTextFile(“paht”):以文件为单位进行读取,返回元组(文件路径, 内容)
-
注意:
- spark以当前项目所在路径为根目录,而不是以当前文件所在位置为根目录(与python相反)
- path可以是绝对路径,也可以是相对路径。
- path也可以指定文件夹,此时将读取目录中的全部文件
- path可以使用通配符,可以读取指定名称的文件
- path 也可以指定分布式储存系统中的文件,hdfs,hbase
- 当想知道内容来自于哪个文件时,可以使用第二种方法。
val rdd: RDD[String] = sc.textFile("data/1.txt")
val rdd1: RDD[String] = sc.textFile("data/") // 指定文件夹
val rdd2: RDD[String] = sc.textFile("data/1*") // 通配符
val rdd2: RDD[String] = sc.textFile("hdfs://nod1:9000/1.tx1") // hdfs文件
val rdd1: RDD[String] = sc.textFile("data")
val rdd2: RDD[(String, String)] = sc.wholeTextFiles("data")
rdd1.collect().foreach(println)
rdd2.collect().foreach(println)
// 结果如下图
3.2.1 分区与储存策略
- 所谓分区策略是指:指定分区的个数时,实际产生的分区个数
- 储存策略:已知分区个数了,怎么将数据分配到各个分区中去
指定分区个数
-
方法
sc.makeRDD(seq, numSlices):numSlices指定内存数据的分区个数
sc.textFile(seq, minPartitions):minPartitions指定文件读取时的分区个数
**rdd.saveAsTextFile(“path”)**方法查看核数。
sparkConf.set(“spark.default.parallelism”, “5”) 配置核数
-
实例如下
sparkConf.set("spark.default.parallelism", "5") // 指定默认核数
// RDD的并行度 与 分区
val seq = Seq[Int](1, 2,3)
val rdd: RDD[Int] = sc.makeRDD(seq, 2) // 分为两区
rdd.saveAsTextFile("output") // 查看分区个数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eVYQLtEz-1683266209116)(…/img/spark/image-20230322163916268.png)]
3.2.2 内存数据的分区储存策略
a) 分区策略
- 对于内存数据,指定多少个分区就产生多少个分区(文件数据会根据大小产生)
b) 储存策略
-
若 A = List(1,2, 3, 4, 5),即可见A的长度为5,将其分为 3 块,则其得到的各个分区数据情况为
-
第一块数据:1
第二块数据:23
第三块数据:45
-
分区策略,通过数据长度和分块数量计算元组( start, end),元组表示各个分区的索引,计算过程:
0 * 5 / 3 = 0; 1 * 5/ 3 = 1 ==>(0, 1)
1 * 5 / 3 = 1; 2 * 5 / 3= 3 ==> (1, 3)
2 * 5 / 3 = 3; 3 * 5 / 3 = 5 ==> (3, 5)
-
索引取前不取后,因此结果如上所示,源码如下:
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)
}
/*
因此若传入数据为 List(1,2,3,4,5)长度为5, 分区为 3个的话,将得到三个(start, end)元组
(0, 1) (1, 3) (3, 5)
也就是三个分区数据的对应索引,取前不取后,
(0, 1) 表示第1个元素,
(1, 3) 表示第2、3个元素,
(3, 5) 表示第4、5个元素
*/
3.2.3 文件数据的分区储存策略
a) 分区策略
-
文件数据的读取方式为:sc.textFile(path, minPartitions),minPartitions:表示最小分区数量,也就是人为指定的分区,但实际分区可能会大于minPartitions,这就是文件数据的分区策略造成的。(当没有指定时,默认情况minPartitons = min(defaultParallelism, 2) )
-
假设读取文件 A 的字节长度为len(A) = 7,人为指定分区个数为 minPartitions = 2;则实际分区个数将会是3。
Spark分区策略其实是采取Hadoop中*“1.1”分区模式,在Hadoop分块模式中,当字节数据不能整除时,若剩余字节数不超过每块数据量的0.1,那么其将于最后一块数据合并(最大能达到1.1倍),若超过了0.1,则重新归为一块**。*
因此 7 / 2 = 3…1, 1 / 3 > 0.1,重新生成一块,因此文件A将会被分为三个区。
b) 数据储存策略
-
spark数据读取方式和Hadoop 一样,当读取一个文件时,以行为单位进行读取,储存数据时,行不会被拆分,也就是说同一个行只会储存在同一个块,不会储存到另外的一个块中去。(当一次性读取多个文件时,以文件作为一个单位,同一个文件不会被储存到多个块中去)。
-
spark以偏移量的方式判断每一行(或文件)所应该分到的块位置。同时保持同一行数据不会被拆分。
偏移量 = 字节长度 / 指定分块数(并不是实际分块数)
每个分区给予相同的偏移量(但是数据长度不一定相同,见下文)
-
沿用上方例子情况,对长度为 7 的文件A进行分区,指定分区个数为2
假设文件A中的数据情况为:1/n2/n3(也就是三行数据,每一行一个数字)
因此 len(A) = 7 (换行符也算)
所以 偏移量 = 7 / 2 = 3
所以根据偏移量指定分区情况为
第一区偏移量:[0, 3] // 偏移量为03的字节,也就是第14个字节应该属于分区1
第二区偏移量:[3, 6]
第三区偏移量:[6, 9]
因此由上可知,第14个字节应该属于分区一,因此文件A中的**数字1及后面换行符**归属于分区一;**第4个字节为数字2,所以数字2也归属于分区一,不仅如此,第46个字节为一行,不可拆分,需归为同一分区**,因此数字2及后面换行符归属于第二个分区。同理,数字3为第7个字节,也就是字节数6,因此归属于第二个分区,而第三个分区却什么都没有!
综上所诉,文件A的数据储存情况为:
第一分区:1/n2/n
第二分区:3
第三分区:空
-
如果数据源为多个文件,那么计算分区时文件为单位进行分区
3.3 并行计算演示
Spark通过将数据及其任务逻辑封装到不同的RDD块中去,进而发送的各个线程去同时进行,各个线程之间并不干扰,这就是并行计算,让我们来直观感受一下并行结算。
-
如下方代码所示,numSlices=1,也就是指定分块个数为1时,表示没有进行并行计算。当numSlices>1时,表示进行多个分块,也就是并行计算。
-
当没有进行并行计算时,输出结果如左图所示,当进行并行计算时,结果如右图所示。分析结果:
-
由计算结果可以看出,RDD转化算子在进行计算时,是逐个元素进行转化,只有第一个元素全部执行完毕才会执行下一个元素。
-
见左图,没有进行并行计算,也就是只有一个块时,计算顺序为元素中的排列顺序。
见右图,当进行并行计算,多个块同时进行计算时,块内元素按顺序进行计算;而各个块同时进行计算,因此谁快谁慢无法确定,最后会出现乱序的情况。
-
package value_operator
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object RDD_map1 {
def main(args: Array[String]): Unit = {
val sparkconf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("map1")
val sc = new SparkContext(sparkconf)
var list = List(1, 2, 3, 4)
val Rdd: RDD[Int] = sc.makeRDD(list, numSlices=1) // 分为一块时,没有进行并行计算
val Rdd1: RDD[Unit] = Rdd.map(x => {
println("*******" + x)
})
val Rdd2: RDD[Unit] = Rdd1.map(x => {
println("########" + x)
})
Rdd2.collect()
sc.stop()
}
}
部分图片来自于视频
尚硅谷大数据Spark教程从入门到精通