目录
程序组成: 分布式集群中,任何一个Spark程序都包含两种进程Driver和Executor
Executor进程: 计算进程,每一个Spark程序都至少有一个
4.4 RDD的cache,persist和checkpoint有什么区别?
1. Spark基础认识
1.1 Spark的应用及使用
-
应用场景
-
离线场景:实现离线数据仓库中的数据清洗、数据分析、即席查询等应用
-
实时场景:实现实时数据流数据处理,相对而言功能和性能不是特别的完善,工作中建议使用Flink替代
-
-
开发语言:Python、Scala、SQL、Java、R
-
Python
-
SQL
-
-
运行模式:5种模式
-
本地模式Local:一般用于做测试,不是分布式程序运行,只是启动1个进程运行所有Task运行
-
集群模式Cluster:一般用于测试集群或者生产集群
-
Standalone:Spark自带的集群资源管理平台
-
YARN:可以将Spark程序提交到YARN上运行
-
Mesos:类似于YARN,国外用的多
-
K8s:基于分布式容器的资源管理平台
-
-
1.2 Spark的Standalone集群架构
- 类比
概念 | MR+YARN | Spark Standalone |
---|---|---|
主节点 | ResourceManager | Master |
从节点 | NodeManager | Worker |
计算进程 | MapTask、ReduceTask | Executor |
-
架构: 分布式主从架构
-
功能: 提供分布式资源管理和任务调度
-
主:Master:管理节点,类比于YARN中的RM:管理从节点,接客,资源管理和任务调度
-
从:Worker:计算节点,类比于YARN中NM:利用自己节点的资源运行计算进程
-
2. Sparkcore核心设计: RDD
2.1 Spark程序核心概念
-
架构组成 [以Standalone集群为例]
-
主: Master: 管理节点: 管理从节点,资源管理和任务调度,接受客户端消息
-
从: Worker: 计算节点: 利用自己节点资源运行计算任务
-
-
程序组成: 分布式集群中,任何一个Spark程序都包含两种进程Driver和Executor
-
程序提交以后,会先启动Driver进程
-
Driver进程: 驱动进程,每一个Spark程序都有一个
-
向主节点申请资源去启动Executor进程
-
-
Driver会解析代码变成Task任务
-
Driver会将Task任务调度分配给Executor去运行,并且监控所有Task运行
-
-
-
Executor进程: 计算进程,每一个Spark程序都至少有一个
-
Executor进程会利用Worker节点的资源运行[1个RDD有100个分区 = 100个Task线程处理]
-
所有Executor一旦启动成功,向Driver反向注册
-
-
架构与程序关系
-
step1: 客户端提交程序给主节点
-
step2: 主节点会根据提交的参数在对应的位置启动Driver进程
-
step3: Driver向主节点申请启动Executor计算进程
-
step4: Master根据配置在Worker节点上启动对应的Executor
-
step5: 所有Executor启动成功以后会向Driver反向注册
-
step6: Driver解析代码,根据代码构建Task,分配给Executor运行,并监控所有Task
2.2 RDD的设计及定义
-
定义: 弹性 分布式 数据集
-
理解: 分布式的列表
-
本质: 一个分布式的逻辑的概念,物理上代表多台节点上的多个分区的数据
-
一个抽象的逻辑上的数据集合的概念,类似于Python中的list, 但RDD是分布式的
-
Python中的list:数据只存在于list构建的节点
-
Spark中的RDD:数据是分布式存储在多台节点上的
-
-
功能: 用于SparkCore中构建分布式数据对象,实现分布式数据的存储,实现分布式的数据存储,是一个对应多个物理分区的数据集合,每个分区的数据可以存储在不同的节点上
-
RDD本质上是一个逻辑的概念,代表多台机器上的多个分区的数据
-
RDD就类似于HDFS中的文件,RDD的分区就类似于HDFS中的Block块
-
-
-
实现
-
step1: SparkCore中所有的数据读取到程序中以后会存储在一个RDD中,将数据划分到多个分区中
-
step2: 所有的数据转换处理,都直接在代码中对RDD进行处理,底层会对RDD的每个分区进行并行处理
-
2.3 RDD的五大特性
特性一:每一个RDD都由一系列的分区构成
特性二:RDD的计算操作本质上是对RDD每个分区的计算
特性三:每个RDD都会保存与其他RDD之间的依赖关系: 血链机制或者血脉机制
特性四:可选的, 如果是二元组[KV]类型的RDD, 在Shuffle过程中可以自定义分区器
特性五:可选的, Spark程序运行时, Task的分配可以指定实现最优路径解:最优计算位置
2.4 RDD的两种创建方式
-
读取数据: 将所有数据源的数据放入RDD中 -- SparkContext
-
方式1: 并行化已存在的集合(列表,字典,元组...): sc.parallize(集合, numSlice=分区个数)
-
功能:将一个集合转换为RDD
-
-
方式2: 读取外部存储系统: sc.textFile, sc.wholeTextFile
-
功能:读取外部存储系统的数据转换为RDD
-
2.5 RDD的分区规则
- RDD 的分区数由什么决定
-
读取数据:
-
方式1: 并行化集合: parallelize
-
没有指定:spark.default.parallelism参数值决定
-
指定分区:指定几个,就是几个分区
-
-
方式2: 读取外部系统: textFile
-
没有指定:spark.default.parallelism和2取最小值得到最小分区数,最终也是根据文件大小来
-
参数:spark.default.parallelism:用于指定没有父RDD的RDD的分区数
-
-
指定分区:最小分区数,最少有这么多分区,具体的分区数可以根据HDFS分片规则来
-
-
-
处理数据
-
默认:子RDD的分区数 = 父RDD的分区数
-
特殊:允许通过调用算子进行修改:repartition、coalesce、reduceByKey等
-
3. RDD常用算子
3.1 RDD算子分类
-
Tranformation算子: 转换算子
-
功能: 用于实现对RDD的数据进行转换
-
特点: 都是lazy模式的,一般不会触发job运行,算子返回值一定是RDD
-
常见: map/filter/flatMap/reduceByKey/groupByKey/sortByKey
-
-
Action算子:触发算子
-
功能:触发job的运行,用户对RDD的数据进行输出或者保存
-
特点:一定会触发job的运行,返回值一定不是RDD
-
常见:foreach、first、count、reduce、saveAsTextFile、collect、take
-
3.2 常用转换算子
-
map算子
-
功能: 对RDD中每个元素调用一次参数中的函数,并将每次调入的返回值直接放入一个新的RDD中
-
分类: 转换算子
-
场景: 一对一的转换,需要返回值
-
语法:
def map(self , f: T -> U ) -> RDD[U]
-
-
flatMap算子
-
功能:将两层嵌套集合中的每个元素取出,扁平化处理,放入一层集合中返回,类似于SQL中explode函数
-
分类:转换算子
-
场景:多层集合元素展开,一个集合对应多个元素【一对多】
-
语法:
# 结构:def 函数名(参数) -> 返回值 # f:这个参数是一个函数function:lambda 参数T: 返回值U # T:f这个函数中传递的参数 # U:f这个函数中返回的结果 # RDD[U]:最终这个函数返回的是一个RDD,RDD中的元素就是U # Iterable:可以理解为列表 def flatMap(self , f : T -> Iterable[U]) -> RDD[U]
-
-
filter算子
-
功能:对RDD集合中的每个元素调用一次参数中的表达式对数据进行过滤,符合条件就保留,不符合就过滤
-
分类:转换算子
-
场景:行的过滤,类似于SQL中where或者having
-
语法:
def filter(self, f: T -> bool ) -> RDD[T]
-
3.3 常用触发算子
- count算子
-
功能:统计RDD集合中元素的个数,返回一个int值
-
分类:触发算子
-
场景:统计RDD的数据量,计算行数
- 语法:
def count(self) -> int
-
- foreach算子
-
功能:对RDD中每个元素调用一次参数中的函数,没有返回值【与map场景上区别】
-
分类:触发算子
-
场景:对RDD中的每个元素进行输出或者保存,一般用于测试打印或者保存数据到第三方系统【数据库等】
-
语法:
def foreach(self , f : T -> None) -> None
-
- saveAsTextFile算子
-
功能:用于将RDD的数据保存到外部文件系统中
-
分类:触发算子
-
场景:保存RDD的计算的结果,一般用于将结果保存到HDFS
-
文件个数 = Task个数 = 分区个数
-
-
语法:
def saveAsTextFile(self , path ) -> None
-
3.4 其他触发算子
- first算子
-
功能:返回RDD集合中的第一个元素 [RDD中有多个分区,返回的是第一个分区的第一个元素]
-
分类:触发算子
-
语法:
def first(self) -> T
-
- take算子
-
功能:返回RDD集合中的前N个元素 [先从第一个分区取,如果不够再从第二个分区取]
-
分类:触发算子
-
注意:take返回的结果放入Driver内存中的,take数据量不能过大
-
语法:
def take(self , num:int ) -> List[T]
-
-
collect算子
-
功能:将RDD转化成一个列表返回
-
分类:触发算子
-
注意:这个RDD的数据一定不能过大,如果RDD数据量很大,导致Driver内存溢出
-
语法:
def collect(self) -> List[T]
-
- reduce算子
-
功能:将RDD中的每个元素按照给定的聚合函数进行聚合,返回聚合的结果
-
分类:触发算子
-
语法:
# tmp用于存储每次计算临时结果,item就是RDD中的每个元素 def reduce(self,f : (T,T) -> U) -> U reduceByKey(lambda tmp,item: tmp+item)
-
3.5 其他转换算子
- union算子
-
功能:实现两个RDD中数据的合并
-
分类:转换算子
-
语法:
def union(self,other:RDD[U]) -> RDD[T/U]
-
- distinct算子
-
功能:实现对RDD元素的去重
-
分类:转换算子
-
语法:
def distinct(self) -> RDD[T]
-
3.6 分组聚合算子
-
分类:xxxByKey算子,只有KV类型的RDD才能调用
-
groupByKey算子
-
功能:对KV类型的RDD按照Key进行分组,相同K的Value放入一个集合列表中,返回一个新的RDD
RDD[K, V].groupByKey => RDD[K, List[V]]
-
要求:只有KV类型的RDD才能调用
-
分类:转换算子
-
场景:需要对数据进行分组的场景,或者说分组以后的聚合逻辑比较复杂,不适合用reduce
-
特点:必须经过Shuffle,可以指定新的RDD分区个数,可以指定分区规则
-
语法:
def groupByKey(self, numpartitions, partitionFunction) -> RDD[Tuple[K,Iterable[V]]]
-
-
reduceByKey算子
-
功能:对KV类型的RDD按照Key进行分组,并对相同Key的所有Value使用参数中的函数进行聚合
-
要求:只有KV类型的RDD才能调用
-
分类:转换算子
-
场景:需要对数据进行分组并且聚合的场景
-
特点:必须经过shuffle,可以执行新的RDD分区个数,可以指定分区规则
-
语法:
def reduceByKey(self,f: (T,T) -> T,numPartitions,partitionFunction) -> RDD[Tuple[K,V]]
-
3.7 排序算子
-
sortBy算子
-
功能:对RDD中的所有元素进行整体排序,可以指定排序规则【按照谁排序,升序或者降序】
-
分类:转换算子
-
场景:适用于所有对数据排序的场景,一般用于对大数据量非KV类型的RDD的数据排序
-
特点:经过Shuffle,可以指定排序后新RDD的分区个数,底层只能使用RangePartitioner来实现
-
语法:
def sortBy(self, keyFunc:(T) -> 0, asc: Bool, numPartitions) -> RDD
-
keyFunc:(T) -> 0:用于指定按照数据中的哪个值进行排序
-
asc: Boolean:用于指定升序还是降序,默认是升序
-
-
-
sortByKey算子
-
功能:对RDD中的所有元素按照Key进行整体排序,可以指定排序规则
-
要求:只有KV类型的RDD才能调用
-
分类:转换算子【sortByKey会触发job的运行】
-
场景:适用于大数据量的KV类型的RDD按照Key排序的场景
-
特点:经过Shuffle,可以指定排序后新RDD的分区个数
-
语法:
def sortByKey(self, asc, numPartitions) -> RR[Tuple[K,V]]
-
3.8 TopN算子
-
top算子
-
功能:对RDD中的所有元素降序排序,并返回前N个元素,即返回RDD中最大的前N个元数据
-
分类:触发算子
-
场景:取RDD数据中的最大的TopN个元素
-
特点:不经过Shuffle,将所有元素放入Driver内存中排序,性能更好,只能适合处理小数据量
-
语法:
def top(self,num) -> List[0]
-
-
takeOrdered算子
-
功能:对RDD中的所有元素升序排序,并返回前N个元素,即返回RDD中最小的前N个元数据
-
分类:触发算子
-
场景:取RDD数据中的最小的TopN个元素
-
特点:不经过Shuffle,将所有元素放入Driver内存中排序,只能适合处理小数据量
-
语法:
def takeOrdered(self,num) -> List[0]
-
3.9 重分区算子
-
repartition算子
-
功能:调整RDD的分区个数
-
分类:转换算子
-
场景:一般用于调大分区个数,必须经过shuffle才能实现
-
特点:必须经过Shuffle过程,repartition底层就是coalesce(shuffle=True)
-
语法:
def repartition(self,numPartitions) -> RDD[T]
-
-
coalesce算子
-
功能:调整RDD的分区个数
-
分类:转换算子
-
场景:一般用于降低分区个数,不需要经过shuffle就可以实现
-
特点:可以选择是否经过Shuffle,默认情况下不经过shuffle
-
语法:
def coalesce(self, numPartitions, shuffle:boolean) -> RDD[T]
-
4. Spark容错机制
4.1 Spark中RDD的数据如何保证数据的安全
-
每个RDD在构建数据时,会根据自己来源一步步倒推到数据来源,然后再一步步开始构建RDD数据
-
当RDD的数据被触发调用时,就会根据RDD的血缘关系层层构建RDD的数据
-
如果在计算过程中,RDD的数据丢失,就会通过依赖关系重新构建,彻底保证了RDD的数据安全
4.2 persist缓存机制
-
cache
-
功能:将RDD缓存在内存中
-
语法:
cache()
-
本质:底层调用的还是persist(StorageLevel.MEMORY_ONLY),但是只缓存在内存,如果内存不够,缓存会失败
-
场景:资源充足,需要将RDD仅缓存在内存中
-
-
persist
-
功能:将RDD【包含这个RDD的依赖关系】进行缓存,可以自己指定缓存的级别【和cache区别】
-
语法:
persist(StorageLevel)
-
级别:StorageLevel决定了缓存位置和缓存几份
-
推荐:实际工作中一般推荐使用以下两种
-
场景:根据资源情况,将RDD缓存在不同的地方或者缓存多份
-
-
unpersist
-
功能:将缓存的RDD进行释放
-
语法:
unpersist
-
unpersist(blocking=True):等释放完再继续下一步
-
-
‘场景:明确RDD已经不再使用,将RDD的数据从缓存中释放,避免占用资源
-
注意:如果不释放,这个Spark程序结束,也会释放这个程序中的所有内存
-
4.3 checkpoint检查点机制
-
问题: 如果一个RDD是经过非常复杂的转换得到的,可能只使用1次,如果RDD构建过程中数据丢失,只能依赖血脉重新构建,性能会受到影响?
-
检查点机制: checkpoint
-
自动触发一个job,构建RDD的数据存储在HDFS
-
-
设计目标: 更侧重于安全性
-
4.4 RDD的cache,persist和checkpoint有什么区别?
-
cache和persist本质上都是persist缓存,cache调用的就是persist,但是不能指定缓存级别,persist允许指定缓存级别
-
persist和checkpoint的区别
存储位置: persist可以选择将数据缓存在内存或者磁盘,checkpoint将数据存储在磁盘
生命周期: persist缓存的数据当遇到unpersist或者程序结束就被释放了,chk的数据必须手动删除
存储内容: persist存储的是RDD,保留RDD的血脉关系,chk只会存储RDD的数据,不包含血脉关系
5. Spark高级特性
5.1 Broadcast Variables广播变量
-
功能: 将一个变量元素进行广播到每台Worker节点的Executor中,让每个Task直接从本地读取数据,减少网络传输IO
-
语法
-
step1:将一个变量定义成为一个广播变量
-
step2:当需要用到这个变量时,就从广播变量中获取它的值
-
step3:释放广播变量
broadcast_city_dict.unpersist()
-
-
场景
-
a. 广播一个较大的数据,减少每次从Driver复制的数据量,降低网络IO损耗,提高性能
-
b. 两张表进行Join时,将小表进行广播,与大表的每个部分进行Join,实现Broadcast Join, 避免Shuffle Join【类似于map join】
-
-
特点:广播变量是一个只读变量,不能修改
5.2 Spark应用中的概念
-
问题:ClusterManager、Worker、Application、Driver、Executor、Job、Stage、Task分别是什么?
-
架构层面:ClusterManager、Worker
-
程序层面:Application、Driver、Executor
-
运行层面:job、DAG图【Stage】、Task
-
-
Cluster Manager:分布式资源管理的主节点
-
分布式资源管理平台主节点设计
-
Standalone: Master
-
YARN: ResourceManager
-
-
功能: 管理节点
-
管理从节点
-
实现资源管理和任务调度
-
接收客户端请求
-
-
-
Worker:分布式资源管理的从节点
-
分布式资源管理平台从节点
-
Standalong: Worker
-
YARN: NodeManager
-
-
功能: 利用自己节点的资源进行主节点分配的任务
-
-
Application:Spark的应用程序
-
开发者基于Spark API开发好的Spark的程序
-
任何一个Spark程序都包含一个Driver进程和至少一个Executor进程
-
不同语言: 产出的程序文件
-
Python: Python文件
-
Java/Scala: jar包
-
-
-
Driver:Spark程序的驱动进程
-
每个Spark程序都会包含一个Driver进程,负责初始化工作
-
功能:
-
负责申请启动Executor进程
-
负责解析代码构建Task,调度分配给Executor运行,监控Task运行
-
-
-
Executor:Spark程序的执行进程
-
每个Spark程序在集群模式都拥有自己的Executor
-
运行: 利用Worker的资源运行在Worker节点上
-
功能: 负责运行Driver分配的Task任务
-
-
Job
-
Spark程序触发Task的最小单元: 要想构建Task运行, 必须先触发job
-
job触发由触发算子决定,Driver会解析代码,一旦遇到了触发算子,就会构建job
-
Driver会通过DAGScheduler组件使用回溯算法【倒推】为每个job构建DAG图【执行计划图】
-
-
Stage
-
执行阶段: 每个job触发以后会构建DAG图,而构建DAG图的过程中会划分Stage
-
划分规则: 根据是否经过Shuffle或者宽依赖来划分
-
一个Stage内部的所有转换过程都是直接在内存中完成
-
Stage划分以后按照整个程序全局编号,执行的时候,按照编号从小到大开始执行
-
-
Task
-
每个Stage【逻辑计划】最终都会转换一个TaskSet集合
-
一个TaskSet集合中可以包含多个Task,每个分区会对应一个Task进行处理
-
Driver会通过TaskScheduler将TaskSet中的Task分配Executor来运行
-
每个Task【物理计划】在Executor中会使用1CoreCPU来运行这个Task
-
5.3 一个Spark程序是如何运行的
-
step0: 先启动分布式资源管理的集群:Spark Standalone / YARN
-
step1: 客户端提交用户开发好的Spark Application程序给ClusterManager
-
step2: ClusterManager根据配置参数运行程序,启动Driver进程
-
step3: Driver进程向主节点提交申请启动Executor进程
-
step4: 主节点根据资源配置和请求,在从节点上启动Executor进程
-
step5: 所有Executor启动成功以后,会向Driver反向注册,等待分配Task
-
step6: Driver解析代码,知道遇到触发算子,开始触发job运行
-
step7: Driver会调用DAGScheduler组件为当前这个job通过回溯算法构建DAG图,并划分Stage
-
step8:Driver会将这个job中每个Stage转换为TaskSet,根据Stage中最后一个RDD分区数来构建Task个数
-
step9:Driver调用TaskManager将Task调度分配到Executor中运行
5.4 Spark的宽窄依赖
-
什么是依赖关系?
-
RDD会不断进行转换处理,得到新的RDD,每个RDD之间就产生了依赖关系
-
-
窄依赖
-
定义: 父RDD的一个分区的数据只给了子RDD的一个分区[不用调用分区器]
-
特点: 一对一或一对多,不经过Shuffle,性能向对较快,但无法实现全局分区,排序,分组等
-
一个Stage内部的计算都是窄依赖的过程,全部在内存中完成
-
-
宽依赖
-
定义: 父RDD的一个分区的数据给了子RDD的多个分区【需要调用Shuffle的分区器来实现】
-
特点: 一对多,必须经过Shuffle,性能相对较慢,可以实现全局分区、排序、分组等
-
Spark的job中按照宽依赖来划分Stage
-
-
本质: 只是一种标记,标记两个RDD之间的依赖关系