资料全部来源于尚硅谷,如果有侵权请联系删除
1. 基础
- Spark 和Hadoop 的根本差异是多个作业之间的数据通信问题 : Spark 多个作业之间数据通信是基于内存,而 Hadoop 是基于磁盘。
- 主要模式:
本地模型:不需要其他任何节点资源就可以在本地执行 Spark 代码的环境
Standalone 模式:只使用 Spark 自身节点运行的集群模式,独立部署
Yarn 模式:
K8S & Mesos 模式
Windows 模式 - 数据库的连接对象不能被序列化,RDD提供foreachPartition可以一个分区创建一个连接对象,而不是一条数据创建一个,大幅度减少连接对象
2. Spark 运行架构
运行架构
标准 master-slave 的结构。
运行流程
- 任务提交后,都会先启动 Driver 程序;
- 随后 Driver 向集群管理器注册应用程序;
- 之后集群管理器根据此任务的配置文件分配 Executor 并启动;
- Driver 开始执行 main 函数,Spark 查询为懒执行,当执行到 Action 算子时开始反向推
算,根据宽依赖进行 Stage 的划分,随后每一个 Stage 对应一个 Taskset,Taskset 中有多
个 Task,查找可用资源 Executor 进行调度; - 根据本地化原则,Task 会被分发到指定的 Executor 去执行,在任务执行的过程中, Executor 也会不断与 Driver 进行通信,报告任务运行情况。
核心组件
Driver
Driver 就是驱使整个应用运行起来的程序,也称之为Driver 类。
主要功能:
➢将用户程序转化为作业(job)
➢ 在 Executor 之间调度任务(task)
➢ 跟踪 Executor 的执行情况
➢ 通过 UI 展示查询运行情况
Executor
Spark Executor 是集群中工作节点(Worker)中的一个 JVM 进程,负责在 Spark 作业
中运行具体任务(Task),任务彼此之间相互独立。Spark 应用启动时,Executor 节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有 Executor 节点发生了
故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他 Executor 节点
上继续运行。
Executor 有两个核心功能:
➢负责运行组成 Spark 应用的任务,并将结果返回给驱动器进程
➢ 它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存
式存储。RDD 是直接缓存在 Executor 进程内的,因此任务可以在运行时充分利用缓存
数据加速运算。
Master & Worker
Spark 集群的独立部署环境,不需要依赖其他的资源调度框架,自身就实现了资源调
度的功能,所以环境中还有其他两个核心组件:Master 和 Worker,Master 负责资源的调度和分配,并进行集群的监控等职责,类似于 Yarn 环境中的 RM, Worker 也是进程,一个 Worker 运行在集群中的一台服务器上,由 Master 分配资源对数据进行并行的处理和计算,类似于 Yarn 环境中 NM。
提交流程
Spark 应用程序提交到 Yarn 环境中执行的时候,一般会有两种部署执行的方式:Client和 Cluster。两种模式主要区别在于:Driver 程序的运行节点位置。
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 上执行。
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 上执行。
3.Spark核心编程
三大数据结构分别是:
➢ RDD : 弹性分布式数据集
➢ 累加器:分布式共享只写变量
➢ 广播变量:分布式共享只读变量
RDD
基本概念
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据
处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行
计算的集合
➢ 弹性
⚫ 存储的弹性:内存与磁盘的自动切换;
⚫ 容错的弹性:数据丢失可以自动恢复;
⚫ 计算的弹性:计算出错重试机制;
⚫ 分片的弹性:可根据需要重新分片。
➢ 分布式:数据存储在大数据集群不同节点上
➢ 数据集:RDD 封装了计算逻辑,并不保存数据
➢ 数据抽象:RDD 是一个抽象类,需要子类具体实现
➢ 不可变:RDD 封装了计算逻辑,是不可以改变的,想要改变,只能产生新的 RDD,在
新的 RDD 里面封装计算逻辑
➢ 可分区、并行计算
RDD与IO关系
RDD的数据处理方式类似IO流,也有装饰者设计模式
RDD的数据只有在调用collect方法时才会真正执行业务逻辑操作,之前的都是封装
RDD是不保存数据的,但是IO可以临时保存一部分数据
首选位置:计算向数据靠近,计算优先发给有数据的节点
RDD的 计算一个分区内的数据是一个一个执行逻辑的,只有前面一个数据的全部逻辑执行完毕,才会执行下一个数据,分区内数据是有序的。
基础编程
- RDD 创建
-
从集合(内存)中创建 RDD
分区:按长度/分区数划分,计算出分区的位置然后切分。以内存的,默认 分区数以CPU数一致 -
从外部存储(文件)创建 RDD
默认分区数最小为2;spark读取文件 底层是 Hadoop,按行读取,而计算分区采用的是根据数据偏移量为单位,
例如文档是:
1
2
3
实际分区是【1,2】,【3】,【】
实际偏移量是7个字节,切分位置是7/2=3,和6,则字节数是[1-3],[3-6],[6-7],读过的位置不再读取
-
从其他 RDD 创建
-
直接创建 RDD(new)
RDD 转换算子
整体上分为 Value 类型、双 Value 类型和 Key-Value类型
⚫ Value 类型
- map
将处理的数据逐条进行映射转换 - mapPartitions
将待处理的数据以分区为单位发送到计算节点进行处理,但是会将整个分区数据加载到内存,处理完的数据是不会被释放,存在对象的引用,在内存较小,数据较大的时候容易内存溢出。
map 和 mapPartitions 的区别:
➢ 数据处理角度
Map 算子是分区内一个数据一个数据的执行,类似于串行操作。而 mapPartitions 算子
是以分区为单位进行批处理操作。
➢ 功能的角度
Map 算子主要目的将数据源中的数据进行转换和改变,不会减少或增多数据。
MapPartitions 算子需要传递一个迭代器,返回一个迭代器,没有要求的元素的个数保持不变,可以增加或减少数据
➢ 性能的角度
Map 算子因为类似于串行操作,所以性能比较低,而是 mapPartitions 算子类似于批处
理,所以性能较高。但是 mapPartitions 算子会长时间占用内存,那么这样会导致内存可能
不够用,出现内存溢出的错误。所以在内存有限的情况下,不推荐使用。使用 map 操作。 - mapPartitionsWithIndex
将待处理的数据以分区为单位发送到计算节点进行处理,在处理时同时可以获取当前分区索引。 - flatMap
将处理的数据进行扁平化后再进行映射处理 - glom
将同一个分区的数据直接转换为相同类型的内存数组进行处理,分区不变 - groupBy
将数据根据指定的规则进行分组, 分区默认不变,但是数据会被打乱重新组合,我们将这样
的操作称之为 shuffle。极限情况下,数据可能被分在同一个分区中
一个组的数据在一个分区中,但是并不是说一个分区中只有一个组 - filter
将数据根据指定的规则进行筛选过滤,符合规则的数据保留
当数据进行筛选过滤后,分区不变,但是分区内的数据可能会出现数据倾斜。 - sample
根据指定的规则从数据集中抽取数据,在数据倾斜的时候可以来抽样
参数:withReplacement: Boolean, 是否放回
fraction: Double, 抽取 比例
seed: Long = Utils.random.nextLong): RDD[T] 随机 种子 - distinct
将数据集中重复的数据去重 - coalesce
根据数据量缩减分区,用于大数据集过滤后,提高小数据集的执行效率,默认分区内数据不会打乱;也可以扩大分区,但是不shuffle是没有意义的。
当 spark 程序中,存在过多的小任务的时候,可以通过 coalesce 方法,收缩合并分区,减少
分区的个数,减小任务调度成本 - repartition
扩大分区,该操作内部其实执行的是 coalesce 操作,参数 shuffle 的默认值为 true。 - sortBy
用于排序数据。排序前,可以将数据通过 f 函数处理,之后按照 f 函数处理的结果进行排序,默认为升序排列。排序后新产生的 RDD 的分区数与原 RDD 的分区数一致。中间存在 shuffle 的过程
⚫ 双 Value 类型 - intersection
对源 RDD 和参数 RDD 求交集后返回一个新的 RDD - union
求并集 - subtract
以一个 RDD 元素为主,去除两个 RDD 中重复元素,将其他元素保留下来。求差集 - zip
将两个 RDD 中的元素,以键值对的形式进行合并。
交集、并集、差集要求两个RDD类型一样,zip可以不用一致;zip要求数据源分区数量一致,分区内元素个数一致
⚫ Key - Value 类型 - partitionBy
rdd.partitionBy(new HashPartitioner(2))
将数据按照指定 Partitioner 重新进行分区。Spark 默认的分区器是 HashPartitioner
如果重分区的分区器和当前 RDD 的分区器一样,是不会变化。还有RangePartitioner分区器。 - reduceByKey
可以将数据按照相同的 Key 对 Value 进行聚合 - groupByKey
将数据源的数据根据 key 对 value 进行分组
reduceByKey 和 groupByKey 的区别
从 shuffle 的角度:reduceByKey 和 groupByKey 都存在 shuffle 的操作,但是 reduceByKey
可以在 shuffle 前对分区内相同 key 的数据进行预聚合(combine)功能,这样会减少落盘的
数据量,而 groupByKey 只是进行分组,不存在数据量减少的问题,reduceByKey 性能比较
高。
从功能的角度:reduceByKey 其实包含分组和聚合的功能。GroupByKey 只能分组,不能聚
合,所以在分组聚合的场合下,推荐使用 reduceByKey,如果仅仅是分组而不需要聚合。那
么还是只能使用 groupByKey - aggregateByKey
将数据根据不同的规则进行分区内计算和分区间计算
def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U, - foldByKey
当分区内计算规则和分区间计算规则相同时,aggregateByKey 就可以简化为 foldByKey - combineByKey
最通用的对 key-value 型 rdd 进行聚集操作的聚集函数(aggregation function)。类似aggregate(),combineByKey()允许用户返回值的类型与输入不一致。
reduceByKey、foldByKey、aggregateByKey、combineByKey 的区别:
reduceByKey: 相同 key 的第一个数据不进行任何计算,分区内和分区间计算规则相同
FoldByKey: 相同 key 的第一个数据和初始值进行分区内计算,分区内和分区间计算规则相同
AggregateByKey:相同 key 的第一个数据和初始值进行分区内计算,分区内和分区间计算规则可以不相同
CombineByKey:当计算时,发现数据结构不满足要求时,可以让第一个数据转换结构。分区内和分区间计算规则不相同。
23) sortByKey
在一个(K,V)的 RDD 上调用,K 必须实现 Ordered 接口(特质),返回一个按照 key 进行排序的
24) join
在类型为(K,V)和(K,W)的 RDD 上调用,返回一个相同 key 对应的所有元素连接在一起的
(K,(V,W))的 RDD,
如果key不存在则不会出现,类似交集,内连接;如果key重复存在,可能会笛卡尔积存在
25) leftOuterJoin
类似于 SQL 语句的左外连接
26) cogroup
在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD
RDD 行动算子
触发作业执行的算子,底层调用的runJob方法,底层会创建activeJob
- reduce
聚集 RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据 - collect
在驱动程序中,以数组 Array 的形式返回数据集的所有元素 - count
返回 RDD 中元素的个数 - first
返回 RDD 中的第一个元素 - take
返回一个由 RDD 的前 n 个元素组成的数组 - takeOrdered
返回该 RDD 排序后的前 n 个元素组成的数组 - aggregate
分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合 - fold
折叠操作,aggregate 的简化版操作 - countByKey
统计每种 key 的个数 - save 相关算子
将数据保存到不同格式的文件中 - foreach
// 收集后打印,在driver端打印
rdd.map(num=>num).collect().foreach(println)
println(“****************”)
// 分布式打印,在节点打印
rdd.foreach(println)
RDD 序列化
- 闭包检查
算子以外的代码都是在 Driver 端执行, 算子里面的代码都是在 Executor
端执行。
scala 的函数式编程中,就会导致算子内经常会用到算子外的数据,这样就
形成了闭包的效果,如果使用的算子外的数据无法序列化,就意味着无法传值给 Executor
端执行,就会发生错误,所以需要在执行任务计算前,检测闭包内的对象是否可以进行序列
化,这个操作我们称之为闭包检测。 - 序列化方法和属性
类的构造参数是类的属性,构造参数需要进行闭包检测,其实等于类进行闭包检测
3)Kryo 序列化框架
Kryo 速度是 Serializable 的 10 倍。
RDD依赖关系
- RDD 血缘关系
多个连续的RDD依赖关系,每个RDD会保存血缘关系,不会保存数据,一旦出现错误,可以根据血缘关系将数据源重新读取计算 - RDD 窄依赖
窄依赖表示每一个父(上游)RDD 的 Partition 最多被子(下游)RDD 的一个 Partition 使用,
窄依赖我们形象的比喻为独生子女。 - RDD 宽依赖
宽依赖表示同一个父(上游)RDD 的 Partition 被多个子(下游)RDD 的 Partition 依赖,会
引起 Shuffle,总结:宽依赖我们形象的比喻为多生。
4)RDD 阶段划分
DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向,
不会闭环。
当RDD中存在shuffle依赖时,阶段会自动增加一个;
阶段数量= shuffle依赖数量 +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 的关系。
RDD对象可以重用,但是数据是无法重用的。
RDD 持久化 在行动算子执行启用
- RDD Cache 缓存
RDD 通过 Cache 或者 Persist 方法将前面的计算结果缓存,默认情况下会把数据以缓存
在 JVM 的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的 action 算
子时,该 RDD 将会被缓存在计算节点的内存中,并供后面重用
mapRdd.persist(StorageLevel.DISK_ONLY)
RDD 的缓存容错机制:丢失的数据会被重算,由于 RDD 的各个 Partition 是相对独立的,因此只需要计算丢失的部分即可。
Spark 会自动对一些 Shuffle 操作的中间数据做持久化操作 - RDD CheckPoint 检查点
sc.setCheckpointDir(“./checkpoint1”) // 设置检查点路径
检查点是通过将 RDD 中间结果写入磁盘
对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action 操作才能触发。
3) 缓存和检查点区别 **********
1)Cache 缓存只是将数据保存起来,不切断血缘依赖。
2)Checkpoint 检查点切断血缘依赖,重新建立新的血缘;将数据长久保存在磁盘 进行数据重用;涉及磁盘IO,性能低,数据安全;为了保证数据安全,会独立执行作业(执行 两次),为了提高效率,和cache联合使用。
mapRdd.cache() //临时保存数据
mapRdd.checkpoint()
3)persist:将数据临时存储在磁盘文件进行数据重用;涉及磁盘IO,性能低,数据安全;执行完毕作业,临时保存的数据文件会丢失
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 分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序
RDD 文件读取与保存
➢ text 文件
// 读取输入文件
val inputRDD: RDD[String] = sc.textFile(“input/1.txt”)
// 保存数据
inputRDD.saveAsTextFile(“output”)
➢ sequence 文件
SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文件
// 保存数据为 SequenceFile
dataRDD.saveAsSequenceFile(“output”)
// 读取 SequenceFile 文件
sc.sequenceFileInt,Int.collect().foreach(println)
➢ object 对象文件
对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制。
// 保存数据
dataRDD.saveAsObjectFile(“output”)
// 读取数据
sc.objectFileInt.collect().foreach(println)
累加器
累加器用来把 Executor 端变量信息聚合到 Driver 端。在 Driver 程序中定义的变量,在
Executor 端的每个 Task 都会得到这个变量的一份新的副本,每个 task 更新这些副本的值后,传回 Driver 端进行 merge。
少加:转换算子中调用累加器,如果没有执行行动算子,就不会执行累加器***
多加:多次调用行动算子,会导致多加
- 系统累加器
- .自定义累加器
广播变量
闭包数据都是以task为单位发送,每个任务包含闭包数据会导致一个executor 中包含大量重复数据。executor其实就是一个JVM,在启动时会自动分配内存,可以将任务中的闭包数据放在executor的内存中,实现共享。
4. Spark内核
环境准备
RPCEnv:通信环境
Backend:后台
Endpoint:终端
进程与线程的区别总结:
本质区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
包含关系:一个进程至少有一个线程,线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
资源开销:每个进程都有独立的地址空间,进程之间的切换会有较大的开销;线程可以看做轻量级的进程,同一个进程内的线程共享进程的地址空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
影响关系:一个进程崩溃后,在保护模式下其他进程不会被影响,但是一个线程崩溃可能导致整个进程被操作系统杀掉,所以多进程要比多线程健壮
并行和并发的区别
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行,所以无论从微观还是从宏观来看,二者都是一起执行的。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
YARN 模式运行机制
YARN Cluster 模式
- 执行脚本提交任务,实际是启动一个 SparkSubmit 的 JVM 进程;
- SparkSubmit 类中的 main 方法反射调用 YarnClusterApplication 的 main 方法;
- YarnClusterApplication 创建 Yarn 客户端,然后向 Yarn 服务器发送执行指令:bin/java
ApplicationMaster; - Yarn 框架收到指令后会在指定的 NM 中启动 ApplicationMaster;
- ApplicationMaster 启动 Driver 线程,执行用户的作业;
- AM 向 RM 注册,申请资源;
- 获取资源后 AM 向 NM 发送指令:bin/java YarnCoarseGrainedExecutorBackend;
- CoarseGrainedExecutorBackend 进程会接收消息,跟 Driver 通信,注册已经启动的
Executor;然后启动计算对象 Executor 等待接收任务 - Driver 线程继续执行完成作业的调度和任务的执行。
- Driver 分配任务并监控任务的执行。
YARN Client 模式
Spark 通讯架构
Spark2.x 版本使用 Netty 通讯框架作为内部通讯组件。Spark 基于 Netty 新的 RPC 框架
借鉴了 Akka 的中的设计,它是基于 Actor 模型
➢ RpcEndpoint:RPC 通信终端。Spark 针对每个节点(Client/Master/Worker)都称之为一
个 RPC 终端,且都实现 RpcEndpoint 接口
➢ RpcEnv:RPC 上下文环境,每个 RPC 终端运行时依赖的上下文环境称为 RpcEnv;
➢ Dispatcher:消息调度(分发)器,针对于 RPC 终端需要发送远程消息或者从远程 RPC
接收到的消息,分发至对应的指令收件箱(发件箱)。
➢ Inbox:指令消息收件箱。一个本地 RpcEndpoint 对应一个收件箱,Dispatcher 在每次向
Inbox 存入消息时,都将对应 EndpointData 加入内部 ReceiverQueue 中,另外 Dispatcher
创建时会启动一个单独线程进行轮询 ReceiverQueue,进行收件箱消息消费;
➢ RpcEndpointRef:RpcEndpointRef 是对远程 RpcEndpoint 的一个引用。
➢ OutBox:指令消息发件箱。**一个目标 RpcEndpoint 对应一个发件箱,**如果向多个目标RpcEndpoint发送信息,则有多个OutBox。
➢ RpcAddress:表示远程的 RpcEndpointRef 的地址,Host + Port。
➢ TransportClient:Netty 通信客户端,一个 OutBox 对应一个 TransportClient,TransportClient不断轮询 OutBox,根据 OutBox 消息的 receiver 信息,请求对应的远程 TransportServer;
➢ TransportServer:Netty 通信服务端,一个 RpcEndpoint 对应一个 TransportServer,接受远程消息后调用 Dispatcher 分发消息至对应收发件箱;
Spark任务调度机制
- Job 是以 Action 方法为界,遇到一个 Action 方法则触发一个 Job;行动算子
- Stage 是 Job 的子集,以 RDD 宽依赖(即 Shuffle)为界,遇到 Shuffle 做一次划分;
- Task 是 Stage 的子集,以并行度(分区数)来衡量,分区数是多少,则有多少个 task。
Job(一个行动算子前操作) > Stage(遇到shuffle前的一段操作) > Task(一个stage里面的操作)
Spark任务调度
Spark RDD 通过其 Transactions 操作,形成了 RDD 血缘(依赖)关系图,即 DAG,最后通过 Action 的调用,触发 Job 并调度执行,执行过程中会创建两个调度器:DAGScheduler
和 TaskScheduler。
➢ DAGScheduler 负责 Stage 级的调度,主要是将 job 切分成若干 Stages,并将每个 Stage
打包成 TaskSet 交给 TaskScheduler 调度。
➢ TaskScheduler 负责 Task 级的调度,将 DAGScheduler 给过来的 TaskSet 按照指定的调度
策略分发到 Executor 上执行,调度过程中 SchedulerBackend 负责提供可用资源,其中
SchedulerBackend 有多种实现,分别对接不同的资源管理系统
Spark Stage 级调度(DAGScheduler)
SparkContext 将 Job 交给 DAGScheduler 提交,它会根据 RDD 的血缘关系构成的 DAG
进行切分,将一个 Job 划分为若干 Stages,具体划分策略是,由最终的 RDD 不断通过
依赖回溯判断父依赖是否是宽依赖,即以 Shuffle 为界,划分 Stage,窄依赖的 RDD 之
间被划分到同一个 Stage 中,可以进行 pipeline 式的计算。
Spark Task 级调度(TaskScheduler)
- DAGScheduler 将 Stage 打包到交给 TaskScheTaskSetduler,TaskScheduler 会将 TaskSet 封装为 TaskSetManager 加入到调度队列中。
- TaskSetManager 负 责监控 管理 同一 个 Stage 中的 Tasks, TaskScheduler 就是以
TaskSetManager 为单元来调度任务。 - TaskScheduler 初始化后会启动 SchedulerBackend,负责跟外界打交道,接收 Executor 的注册信息,并维护 Executor 的状态,定期询问TaskScheduler 是否有任务需要执行。
- 将 TaskSetManager 加入 rootPool 调度池中之后,调用 SchedulerBackend 的riviveOffers 方法给 driverEndpoint 发送 ReviveOffer 消息;driverEndpoint 收到 ReviveOffer 消息后调用 makeOffers 方法,过滤出活跃状态的 Executor(这些 Executor 都是任务启动时反向注册到 Driver 的 Executor),然后将 Executor 封装成 WorkerOffer 对象;准备好计算资源
(WorkerOffer)后,taskScheduler 基于这些资源调用 resourceOffer 在 Executor 上分配task。 - TaskScheduler 支持两种调度策略,一种是 FIFO,也是默认的调度策略,另一种是 FAIR。
两种调度策略:
- FIFO 调度策略
TaskSetManager 按照先来先到的方式入队,出队时直接拿出最先进队的 TaskSetManager - FAIR 调度策略
FAIR 模式中有一个 rootPool 和多个子 Pool,各个子 Pool 中存储着所有待分配的TaskSetMagager。需要先对子 Pool 进行排序,再对子 Pool 里面的 TaskSetMagager 进行排序,因为 Pool 和 TaskSetMagager 都继承了 Schedulable 特质
排序过程的比较是基于 Fair-share 来比较的,每个要排序的对象包含三个属性:
runningTasks值(正在运行的Task数)、minShare值、weight值
通过minShare和weight这两个参数控制比较过程,可以做到让minShare使用率和权重使用率少(实际运行 task 比例较少)的先运行。
- Task优先位置(选哪个节点Executor执行)
1)PROCESS_LOCAL 进程本地化,task 和数据在同一个 Executor 中,性能最好
2)NODE_LOCAL 节点本地化,task 和数据在同一个节点中,但是 task 和数据不在同一个 Executor 中,数据需要在进程间进行传输。
3)RACK_LOCAL 机架本地化,task 和数据在同一个机架的两个节点上,数据需要
通过网络在节点之间进行传输
4)NO_PREF 对于 task 来说,从哪里获取都一样,没有好坏之分。
5)ANY task 和数据可以在集群的任何地方,而且不在一个机架中,性能最差。 - 失败重试与黑名单机制
重试:Task 被提交到 Executor 启动执行后,Executor 会将执行状态上报给 SchedulerBackend,SchedulerBackend 则告诉 TaskScheduler,TaskScheduler 找到该Task 对应的TaskSetManager,并通知到该 TaskSetManager,这样 TaskSetManager 就知道 Task的失败与成功状态,对于失败的 Task,会记录它失败的次数,如果失败次数还没有超过最大重试次数,那么就把它放回待调度的 Task 池子中,否则整个 Application 失败。
黑名单:在记录 Task 失败次数过程中,会记录它上一次失败所在的 Executor Id 和 Host,这样下次再调度这个 Task 时,会使用黑名单机制,避免它被调度到上一次失败的节点上
Spark Shuffle 解析
ShuffleMapStage 与 ResultStage
最后一个 stage 称为 finalStage,它本质上是一个 ResultStage 对象,前面的所有 stage 被称为 ShuffleMapStage
ShuffleMapStage 的结束伴随着 shuffle 文件的写磁盘。
ResultStage 基本上对应代码中的 action 算子,意味着一个 job 的运行结束。
HashShuffle 解析***
优化的 HashShuffle 过程就是启用合并机制,合并机制就是复用 buffer。实际本身在shuffle时会产生很多小文件,但是会增加大量IO开销,把多个shuffle结果合并到一个文件,用一个index和偏移量来标记下游文件应该读取的位置。
SortShuffle 解析
- 普通 SortShuffle
数据会先写入一个数据结构,reduceByKey 写入 Map,一边通过 Map 局部聚合,一遍写入内存。Join 算子写入 ArrayList 直接写入内存中。如果达到阈值就会将内存数据结构的数据写入到磁盘,清空内存数据结构。
在溢写磁盘前,先根据 key 进行排序,分批写入到磁盘文件中。默认批次为 10000 条,写入磁盘文件通过缓冲区溢写的方式,每次溢写都会产生一个磁盘文件,一个 Task 过程会产生多个临时文件。
最后在每个 Task 中,将所有的临时文件合并,这就是 merge 过程,此过程将所有临时文件读取出来,一次写入到最终文件。意味着一个 Task 的所有数据都在这一个文件中。同时单独写一份索引文件,标识下游各个Task的数据在文件中的索引,start offset和end offset。 - bypass SortShuffle(区别是不排序)
bypass 运行机制的触发条件:
- shuffle reduce task 数量小于等于 spark.shuffle.sort.bypassMergeThreshold 参数的值,默认为 200。
- 不是聚合类的 shuffle 算子(比如 reduceByKey)。
Spark 内存管理
堆内和堆外内存规划
1)堆内内存:Executor 内运行的并发任务共享 JVM 堆内内存,这些任务在缓存 RDD 数据和广播(Broadcast)数据时占用的内存被规划为存储(Storage)内存,而这些任务在执行 Shuffle 时占用的内存被规划为执行(Execution)内存,剩余的部分不做特殊规划
2) 堆外内存:为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 引入了堆外(Offheap)内存,使之可以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据。
内存空间分配
动态占用机制
5. SparkSQL
基本概念
DataFrame
- DataFrame:DataFrame 是一种以 RDD 为基础的分布式数据集,类似于传统数据库中的二维表格。
- DataFrame 与 RDD 的主要区别在于,前者带有 schema 元信息,即 DataFrame所表示的二维表数据集的每一列都带有名称和类型。
- DataFrame 是为数据提供了 Schema 的视图。
- DataFrame 是懒执行的,触发才执行。 但性能上比 RDD 要高,主要原因:优化的执行计划,即查询计划通过 Spark catalyst optimiser 进行优化。
DataSet
- DataSet 是分布式数据集合。
SparkSQL 核心编程
基本概念
- DataFrame 提供一个特定领域语言(domain-specific language, DSL)去管理结构化的数据。
- RDD 转换为 DataFrame:sc.makeRDD(List((“zhangsan”,30), (“lisi”,40))).map(t=>User(t._1, t._2)).toDF.show
- DataFrame 转换为 RDD: df.rdd 得到的 RDD 存储类型为 Row
- 涉及到运算的时候, 每列都必须使用$, 或者采用引号表达式:单引号+字段名
- 普通临时表是 Session 范围内的,如果想应用范围内有效,可以使用全局临时表。使用全局临时表时需要全路径访问,global_temp.sutdent
- //RDD=>DataFrame=>DataSet 转换需要引入隐式转换规则,否则无法转换
import spark.implicits._ - RDD、DataFrame、DataSet关系
共性
1)三者都有惰性机制
2)三者都会根据 Spark 的内存情况自动缓存运算
3)三者都有 partition 的概念
区别
-
RDD
➢ RDD 一般和 spark mllib 同时使用
➢ RDD 不支持 sparksql 操作 -
DataFrame
➢ 与 RDD 和 Dataset 不同,DataFrame 每一行的类型固定为 Row,每一列的值没法直接访问,只有通过解析才能获取各个字段的值
➢ DataFrame 与 DataSet 一般不与 spark mllib 同时使用
➢ DataFrame 与 DataSet 均支持 SparkSQL 的操作,比如 select,groupby 之类,还能注册临时表/视窗,进行 sql 语句操作
➢ DataFrame 与 DataSet 支持一些特别方便的保存方式,可以带上表头 -
DataSet
➢ Dataset 和 DataFrame 拥有完全相同的成员函数,区别只是每一行的数据类型不同。
DataFrame 其实就是 DataSet 的一个特例 type DataFrame = Dataset[Row]
➢ DataFrame 也可以叫 Dataset[Row],每一行的类型是 Row,不解析,每一行究竟有哪
些字段,各个字段又是什么类型都无从得知,只能用上面提到的 getAS 方法或者共
性中的第七条提到的模式匹配拿出特定字段。而 Dataset 中,每一行是什么类型是
不一定的,在自定义了 case class 之后可以很自由的获得每一行的信息
数据的加载和保存
- SparkSQL 默认读取和保存的文件格式为 parquet
- spark.read.load 是加载数据的通用方法
- df.write.save 是保存数据的通用方法
df.write.mode(“append”).json(“/opt/module/data/output”)
mode可以解决文件存在的问题:append如果文件已经存在则追加;overwrite文件已经存在则覆盖;ignore已经存在则忽略
6. SparkStreaming
基本概念
- Spark Streaming 用于流式数据的处理,是准实时,微批次数据处理框架
- Spark Streaming 使用离散化流(discretized stream)作为抽象表示,叫作 DStream
- Spark Streaming 架构
- 背压机制:根据JobScheduler 反馈作业的执行信息来动态调整 Receiver 数据接收率。
DStream 创建
RDD队列
ssc.queueStream(queueOfRDDs)来创建 DStream,每一个推送到这个队列中的 RDD,都会作为一个 DStream 处理
自定义数据源
需要继承 Receiver,并实现 onStart、onStop 方法来自定义数据源采集。
Kafka 数据源 *************
DirectAPI:是由计算的 Executor 来主动消费 Kafka 的数据,速度由自身控制。
Kafka 0-10 Direct 模式
DStream 转换
无状态转化操作
- 无状态转化操作就是把简单的 RDD 转化操作应用到每个批次上
- Transform:该函数每一批次调度一次,可以将底层的RDD获取到后进行操作。可以在driver端每个周期执行一次(当需要周期更新 的),也可以执行在executor端。
- join:两个流之间的 join 需要两个流的批次大小一致,这样才能做到同时触发计算。计算过程就是对当前批次的两个流中各自的 RDD 进行 join,与两个 RDD 的 join 效果相同。
有状态转化操作
- UpdateStateByKey 原语用于记录历史记录,可以在 DStream 中跨批次维护状态。
updateStateByKey 操作需要做下面两步:
1) 定义状态,状态可以是一个任意的数据类型。
2)定义状态更新函数,用此函数阐明如何使用之前的状态和来自输入流的新值对状态进行更
新。
使用 updateStateByKey 需要对检查点目录进行配置,会使用检查点来保存状态。 - WindowOperations
Window Operations 可以设置窗口的大小和滑动窗口的间隔来动态的获取当前 Steaming 的允许状态。所有基于窗口的操作都需要两个参数,分别为窗口时长以及滑动步长。
➢ 窗口时长:计算内容的时间范围
➢ 滑动步长:隔多久触发一次计算。
二者都必须为采集周期大小的整数倍。
DStream 输出
➢ print():在运行流程序的驱动结点上打印 DStream 中每一批次数据的
➢ saveAsTextFiles(prefix, [suffix]):以 text 文件形式存储这个 DStream 的内容。
➢ saveAsObjectFiles(prefix, [suffix]):以 Java 对象序列化的方式将 Stream 中的数据保存为
SequenceFiles .
➢ saveAsHadoopFiles(prefix, [suffix]):将 Stream 中的数据保存为 Hadoop files
➢ foreachRDD(func):这是最通用的输出操作
- 连接不能写在 driver 层面(序列化)
- 如果写在 foreach 则每个 RDD 中的每一条数据都创建,得不偿失;
- 增加 foreachPartition,在分区创建(获取)。
优雅关闭
如果想关闭采集器,需创建新线程,而且需要在第三方程序中(mysql、zk、redis)增加关闭的状态
计算节点不在接收新的数据,将现有数据处理完毕然后关闭。
数据恢复:从检查点中恢复