这是我参与「第四届青训营 」笔记创作活动的第5天
1.大数据处理引擎Spark介绍
Spark:用于大规模数据处理的统一的引擎,可用于单机节点或者集群上进行数据工程、数据科学和机器学习
①Spark生态&特点
-
统一引擎,支持多种分布式场景
- SparkCore:Spark的核心组件,实现了Spark的基本功能,包括任务调度、内存管理、故障恢复、存储系统交互等
- SparkSQL:操作结构化数据的核心组件,可以通过SparkSQL直接查询Hive,Hbase等数据源
- Spark Structured Streaming:是建立在SparkSQL上的,是Spark提供的流式计算框架,来支持高吞吐的可容错的实时流式数据
- MLlib:是Spark提供的关于机器学习的算法程序库
- GraghX:针对图计算提出的分布的图形处理框架
-
多语言支持
- SQL/Python/Java/Scala/R
-
可读写丰富数据源
- 内置DataSource:Text、Parquet/ORC、JSON/CSV、JDBC
- 自定义DataSource:实现DataSourceV1/V2 API,来对接HBase/Mongo/ElasticSearch等数据源
-
丰富灵活的API/算子
-
支持K8S/TARN/Mesos资源调度
②Spark运行架构&部署方式
(Master-Slave架构模式)Cluster Manager是master的集群管理器,控制整个集群,负责资源管理和调度,监控worker节点
worker节点是从节点,在Yarn中称为node manager ,负责控制计算节点,启动executor进程
对于一个任务来说,Driver Program相当于一个app master,是整个应用的管理者,一个spark application只有一个driver,负责作业的任务调度,是一个JVM进程,运行程序的main函数,创建一个Spark Context上下文,这是整个应用的上下文,控制应用的生命周期
Executor是实际执行任务的
-
用户程序从开始提交到最终计算经历的阶段
用户创建一个SparkContext,SparkContext连接到Cluster Manager,Cluster Manager会根据用户提交时设置的参数(CPU、内存)为本次提交分配计算资源来启动Excecutor
Driver会将用户程序划分成不同的stage,每个stage由完全相同的一组task构成,这些task会分别作用一些待处理的数据和不同分区,在stage划分完成并且task创建完成后,driver会向executor发送task,executor在接收到task后,会下载task运行时的依赖,准备好task的执行环境,然后开始执行task,并实时将task的运行状态汇报给driver,driver会根据收到task的运行状态然后做状态更新,不断调用task,并将task发送给executor执行,直到所有的task都执行正确,或直到超过执行次数限制
-
Spark部署方式
Saprk Local Mode:本地测试/单进程多线程模式
Spark Standalone Mode:需要启动Spark的Standalone集群的Master/Worker
on YARN/K8S:依赖外部资源调度器
2.SparkCore原理解析
cluster模式说明是集群提交,使用外部资源管理器(eg.YARN)
①RDD
是一个容错的、可以并行处理的分布式数据集,是Spark中最基本的处理数据的模型,是一个基本单元
特性:
- 分区列表:每个RDD都有多个分区,这些分区运行在集群的不同节点上,每个分区都会被一个计算任务处理,分区决定并行计算的数量,创建RDD时可以指定其分区个数,若不指定分区个数(比如从集群创建),则默认的分区数就是CPU核数;若从HDFS文件存储创建,默认的分区数是文件的block数
- 计算函数:Spark RDD是以partition为基本单位,每个RDD都会实现一个compute函数,并在partition进行计算
- 依赖:每个RDD都会依赖于其他RDD,RDD的每次转换都会生成一个新的RDD,因此会生成向pipeline一样的前后依赖关系,那么若部分分区数据丢失,spark就可以通过依赖关系重新计算丢失的所有数据,而不是对所有的RDD都重新进行分区计算
- 实现了两种类型的分区函数:spark有两种分区器,一个基于哈希的hash partitioner,还有一个基于范围的range partitioner;但是只有key-value RDD才会有partitioner,非key-value RDD的partitioner值为空;partitioner函数决定了RDD本身的分区数量,也决定了parent RDD shuffle时的分区数量
- 优先位置:每个分区都有一个优先位置列表,比如对于一个HDFS文件,spark会存储每个partition块的位置(Spark中移动数据而不移动计算,所以在任务调度时,会尽可能将计算分配到需要处理的数据块的存储位置)
RDD提供了很多算子(实际上是RDD的成员函数)
- 缓存函数:当一个RDD被多次使用或者该RDD的计算链路非常长,那么计算结果非常珍贵,所以可以通过中间进行缓存来保存计算结果,这也是Spark速度非常快的原因(可以在内存中持久化或者缓存数据集)
如何创建RDD
-
内置RDD:如果没设置分区可以通过以下两种函数创建
-
自定义RDD
class CustomRDD(...) extends RDD{}
实现五要素对应的函数
RDD算子
-
Transform算子:生成一个新的RDD
-
Action算子:触发Job提交
RDD依赖
-
RDD依赖:描述父子RDD之间的依赖关系
-
窄依赖:父RDD的每个partition至多对应一个子RDD分区
-
宽依赖(会产生Shuffle):父RDD的每个partition都可能对应多个子RDD分区
ShuffleDependency
问题:当一个分区的数据丢失,其所在的RDD可能有多个父RDD,这时数据的重新计算会变得很麻烦,因此可以设置Checkpoint
如何根据宽窄依赖划分stage
RDD执行流程
②调度器
在RDD创建之后,Spark Context就会根据RDD对象创建一个有向无环图,触发job之后,会将DAG提交给DAG Scheduler,然后DAG调度会根据ShuffleDependency划分stage,并按照依赖顺序调度Stage,为每个Stage生成并提交TaskSet到TaskScheduler
Task Scheduler会根据调度算法(FIFO/FAIR)对多个TaskSet进行调度,对于调度到的TaskSet,会将Task(locality)调度到相关Executor上面执行
③内存管理
Executor内存主要有两类:Storage、Execution(对 内内存:管理难度低,误差小;)
- UnlfiedMemoryManager统一管理Storage/Execution内存
- Storage和Execution内存使用是动态调整,可以相互借用
- 当Storage空闲,Execution可以借用Storage的内存使用,可以减少spill等操作,Execution使用的内存不能被Storage驱逐
- 当Execution空闲,Storage可以借用Execution的内存使用,当Execution需要内存时,可以驱逐被Storage借用的内存,直到spark.memory.storageFraction边界
多任务间内存分配
- 一个Executor中,task是共享其内存的,UnifildMemoryManager统一管理多个并发Task的内存分配
- 每个Task获取的内存区间为1/(2*N)~1/N,N为当前Executor中正在并发运行的Task数量,如果task要求获取的内存不能被满足,那么该任务会被阻塞,知道有足够的执行空间
④Shuffle
SortShuffleManager
-
join的两个数据量都比较庞大时可以用
-
每个Map Task生成一个Shuffle数据文件和一个索引文件
External Shuffle Service
-
运行在每一台主机上,管理该主机的所有Executor节点产生的Shuffle数据
-
shuffle write的文件被NodeManager中的Shuffle Service托管,供后续ReduceTask进行shuffle fetch,如果Executor空闲,DRA可以进行回收
3.SparkSQL原理解析
①Catalyst优化器
白色流程图为Catalyst的详细步骤
Catalyst优化器-RBO
- RBO:经验式的启发式的优化思路
-
对语法树进行遍历,同时匹配,找到满足规则的节点并进行相应转换
-
每个batch代表一个执行规则,batch执行策略
- Once->只执行一次
- FixedPoint->重复执行,直到plan不再改变,或者执行达到固定次数(默认100次)
-
常见规则:常量折叠、谓词下推、列裁剪、动态过滤裁剪、简化表达式
eg.谓词下推
-
transformDown:先序遍历树进行规则匹配
-
transformUp:后序遍历树进行规则匹配
Catalyst优化器-CBO
-
根据优化规则对关系进行转换,生成多个执行计划,CBO会根据之前的统计信息和代价模型去计算刚刚生成的执行计划的代价,从中选择最优方案来实际运行
-
流程:
-
CBO依赖与数据库对对象的统计,需要使用特定的SQL语句收集列信息
-
打开参数spark.sql.cbo.enabled -> true
-
根据列和表的信息进行统计估算:每个算子相对而言代价固定,是可以用一定的规则描述的,执行节点输出的数据的大小分布主要分为两个部分:(1)初始数据:原始表的数据集的大小分布可以直接统计到 (2)中间节点的数据集:根据输入的数据的信息以及自身操作的特点进行估算
-
Saprk会根据这些信息进行优化
②Adaptive Query Execution(AQE)自适应查询
是Spark在运行过程中会检测到满足条件的情况,进行自动的查询规划,自适应查询规划会基于运行时的统计数据,对正在运行的任务进行优化,边执行边优化提高了SQL的执行效率
最大的亮点在于,它根据已经完成的计划的真实节点进行统计并不停反馈,来优化剩余的执行计划
目前支持的优化场景:
- 动态合并Shuffle分区
- 动态调整Join策略,(eg.sort merge join 变成)
- 对于数据倾斜的Join,会有Skew Join优化
AQE - Coalescing Shuffle Partitions(分区合并)
-
数据量很大时,shuffle会很影响性能,Shuffle数据重分发非常费时,因为需要网络移动数据,因此shuffle中partition的个数很关键;partition的数量取决于数据,数据量的大小在不同的query或stage有很大的差异,很难确定最佳partition数目;如果partition数目过少,那么一个partition就会有大量的数据,导致分区会有spill到磁盘的操作;如果过少,shuffle阶段会产生大量小块的随机读,影响性能,会产生额外的网络开销
-
合理的操作是(partition动态合并)初始时设置较多的partition数目,在作业运行过程中,根据前面运行完的stage的MapStatus中实际的partition大小信息,可以将多个相邻的较小的partition进行动态合并,由一个Task读取进行处理,如图,最终的聚合只要执行三个task而不是五个
AQE - Switching Join Strategies(动态切换join)
- 解决的问题:Catalyst Optimizer优化阶段,算子的statistics估算不准确,生成的执行计划并不是最优,AQE运行过程中动态获取准确Join的leftChild/rightChild的实际大小,将SMJ转换为BHJ
- Spark支持三种Join,其中broadcast join性能最好,因此参加join的小表可以装入内存;但是该join有条件限制:只能是小表join
- 当Spark估计一些表的大小小于阈值时,会将其参与的SortMergeJoin(SMJ)转成BroadcastHashJoin(BHJ)