Spark原理 | 青训营笔记

105 阅读10分钟

这是我参与「第四届青训营 」笔记创作活动的的第5天。

1. 概述

Spark,是一种"One Stack to rule them all"的大数据计算框架,期望使用一个技术堆栈就完美地解决大数据领域的各种计算任务。Apache官方,对Spark的定义就是:通用的大数据快速处理引擎。Spark使用Spark RDD、Spark SQL、 Spark Streaming,MLlib,GraphX成功解决了大数据领城中,离线批处理、交互式查询、实时流计算、机器学习与图计算等最重要的任务和问题。Spark除了一站式的特点之外,另外一个最重要的特点,就是基于内存进行计算,从而让它的速度可以达到MapReduce、Hive的数倍甚至数十倍!

1.1 Spark生态组件

image.png

  •  park Core:Spark核心组件,实现了Spark的基本功能。包含:任务调度、内存管理、错误恢复、与存储系统交互等模块。

  •  Spark SQL:用来操作结构化数据的核心组件,通过Spark SQL可以直接查询Hive、HBase等多种外部数据源中的数据。

  •  Spark Structured Streaming:Spark提供的流式计算框架。支持高吞吐量、可容错处理的实时流式数据处理。

  •  MLlib:Spark提供的关于机器学习功能的算法程序库。包括分类、回归、聚类、协同过滤算法等,还提供了模型评估、数据导入等额外的功能。

  •  GraphX:Spark提供的分布式图处理框。,拥有对图计算和图挖掘算法的API接口以及丰富的功能和运算符。

  •  独立调度器、Yarn、Mesos、Kubernetes:Spark框架可以高效地在一个到数千个节点之间伸缩计算,集群管理器则主要负责各个节点的资源管理工作,为了实现这样的要求,同时获得最大灵活性,Spark支持在各种集群管理器(Cluster Manager)上运行。

1.2 Spark特点

由Spark组件可知Spark特点:

  •  多语言支持:SQL/Python/Java/Scala/R

  •  丰富的数据源:

    •  内置DataSource:Text/Parquet/Json/csv/JDBC...

    •  外置Datasource:Hbase/Hive...

  •  丰富的API/算子:

    •  SparkCore-->RDD(弹性分布式数据集):map/filter/count/...

    •  SparkSQL-->DataFrame(表格型数据结构):select/filter/group by...

  •  支持多种资源调度

1.3 Spark运行架构

image.png

  •  Application(应用):Spark上运行的应用。Application中包含了一个驱动器(Driver)进程和集群上的多个执行器(Executor)进程。

  •  Driver Program(驱动器):运行main()方法并创建SparkContext的进程。

  •  Cluster Manager(集群管理器):控制整个集群,监控Work Node,负责资源管理与调度。用于在集群上申请资源的外部服务(如:独立部署的集群管理器、Mesos或者Yarn)。

  •  Worker Node(工作节点):集群上运行应用程序代码的任意一个节点。

  •  Executor(执行器):在集群工作节点上为某个应用启动的工作进程,该进程负责运行计算任务,并为应用程序存储数据。

  •  Task(任务):执行器的工作单元。

  •  Job(作业):一个并行计算作业,由一组任务(Task)组成,并由Spark的行动(Action)算子(如:save、collect)触发启动。

  •  Stage(阶段):每个Job可以划分为更小的Task集合,每组任务被称为Stage。

2. SparkCore

image.png

2.1  RDD(弹性分布式数据集)

(RDD是Spark提供的核心抽象,全称为Resillient Distributed Dataset,即弹性分布式数据集。)

  RDD五个特性:

-  分区列表:每个RDD有多个分区,这些分区运行在不同集群上,每个分区被一个计算任务Task处理,分区数决定并行计算的数量。创建RDD时可指定分区,分区个数。

-  都有一个计算函数:RDD以Partition为基本单位,每个RDD会实现一个计算函数,对具体的partition进行计算。

-  依赖:每个RDD都会依赖其他RDD,RDD的每次转换都会生产新的依赖,具有前后依赖关系。若部分分区数据丢失,Spark可以通过依赖关系重新计算丢失的分区数据。

-  实现了两种分区函数:基于Hash的Hash partitional和基于范围的Range partitional

-  每个分区都有优先位置列表:存储每个partitional的优先位置。

 

  •  创建RDD:

    • 内置RDD

    •  自定义RDD

  •  RDD算子:

    • Transfrom算子:生成新的RDD

    •  Action算子:触发Job提交

  •  RDD依赖:描述父子RDD间的依赖关系

    •  窄依赖:父RDD的每个Partition至多对应一个子RDD分区,一个子RDD可对应多个父RDD分区

    •  宽依赖:父RDD的每个Partition都可能对应多个子RDD分区

    image.png

  •  RDD执行流程:

    image.png

    划分Stage的整体思路:从后往前推,遇到宽依赖就断开,划分为一个Stage。遇到窄依赖,就将这个RDD加入该Stage中,DAG最后一个阶段会为每个结果的Partition生成一个ResultTask。每个Stage里面的Task数量由最后一个RDD的Partition数量决定,其余的阶段会生成ShuffleMapTask。

    当RDD对象创建后,SparkContext会根据RDD对象构建DAG有向无环图,然后将Task提交给DAGScheduler。DAGScheduler根据ShuffleDependency将DAG划分为不同的Stage,为每个Stage生成TaskSet任务集合,并以TaskSet为单位提交给TaskScheduler。TaskScheduler根据调度算法(FIFO/FAIR)对多个TaskSet进行调度,并通过集群中的资源管理器(Standalone模式下是Master,Yarn模式下是ResourceManager)把Task调度(locality)到集群中Worker的Executor,Executor由SchedulerBackend提供。

2.2 Scheduler(调度器)

  •  根据ShuffleDependency 切分Stage,并按照依赖顺序调度Stage,为每个Stage生成并提交TaskSet到 TaskScheduler

  •  根据调度算法(FIFO/FAIR)对多个TaskSet进行调度,对于调度到的TaskSet ,会将Task 调度(locality)到相关Executor上面执行,Executor SchedulerBackend提供

2.3 Memory Manager(内存管理)

image.png

Spark 作为一个基于内存的分布式计算引擎,Spark采用统一内存管理机制。重点在于动态占用机制。

  •  设定基本的存储内存(Storage)和执行内存(Execution)区域,该设定确定了双方各自拥有的空间的范围,UnifiedMemoryManager统一管理Storage/Execution内存

  •  双方的空间都不足时,则存储到硬盘;若己方空间不足而对方空余时,可借用对方的空间

  •  当Storage空闲,Execution可以借用Storage的内存使用,可以减少spill等操作, Execution内存不能被Storage驱逐。Execution内存的空间被Storage内存占用后,可让对方将占用的部分转存到硬盘,然后"归还"借用的空间

  •  当Execution空闲,Storage可以借用Execution内存使用,当Execution需要内存时,可以驱逐被Storage借用的内存,可让对方将占用的部分转存到硬盘,然后"归还"借用的空间

    user memory存储用户自定义的数据结构或者spark内部元数据

    Reserverd memory:预留内存,防止OOM,

    堆内(On-Heap)内存/堆外(Off-Heap)内存:Executor 内运行的并发任务共享 JVM 堆内内存。为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 可以直接操作系统堆外内存,存储经过序列化的二进制数据。减少不必要的内存开销,以及频繁的 GC 扫描和回收,提升了处理性能。

3. SparkSQL

image.png

3.1 SparkSQL执行过程

SQL Parse: 将SparkSQL字符串或DataFrame解析为一个抽象语法树/AST,即Unresolved Logical Plan

Analysis:遍历整个AST,并对AST上的每个节点进行数据类型的绑定以及函数绑定,然后根据元数据信息Catalog对数据表中的字段进行解析。 利用Catalog信息将Unresolved Logical Plan解析成Analyzed Logical plan

Logical Optimization:该模块是Catalyst的核心,主要分为RBO和CBO两种优化策略,其中RBO是基于规则优化,CBO是基于代价优化。 利用一些规则将Analyzed Logical plan解析成Optimized Logic plan

Physical Planning: Logical plan是不能被spark执行的,这个过程是把Logic plan转换为多个Physical plans

CostModel: 主要根据过去的性能统计数据,选择最佳的物理执行计划(Selected Physical Plan)。

Code Generation: sql逻辑生成Java字节码

影响SparkSQL性能两大技术:

Optimizer:执行计划的优化,目标是找出最优的执行计划

Runtime:运行时优化,目标是在既定的执行计划下尽可能快的执行完毕。

3.2 Catalyst优化

  •  Rule Based Optimizer(RBO): 基于规则优化,对语法树进行一次遍历,模式匹配能够满足特定规则的节点,再进行相应的等价转换。

  •  Cost Based Optimizer(CBO): 基于代价优化,根据优化规则对关系表达式进行转换,生成多个执行计划,然后CBO会通过根据统计信息(Statistics)和代价模型(Cost Model)计算各种可能执行计划的代价,从中选用COST最低的执行方案,作为实际运行方案。CBO依赖数据库对象的统计信息,统计信息的准确与否会影响CBO做出最优的选择。

3.3 AQE

AQE对于整体的Spark SQL的执行过程做了相应的调整和优化,它最大的亮点是可以根据已经完成的计划结点真实且精确的执行统计结果来不停的反馈并重新优化剩下的执行计划。

AQE框架三种优化场景:

动态合并shuffle分区(Dynamically coalescing shuffle partitions)

动态调整Join策略(Dynamically switching join strategies)

动态优化数据倾斜Join(Dynamically optimizing skew joins)

3.4 RuntimeFilter

实现在Catalyst中。动态获取Filter内容做相关优化,当我们将一张大表和一张小表等值连接时,我们可以从小表侧收集一些统计信息,并在执行join前将其用于大表的扫描,进行分区修剪或数据过滤。可以大大提高性能

Runtime优化分两类:

  1. 全局优化:从提升全局资源利用率、消除数据倾斜、降低IO等角度做优化。包括AQE。

  2. 局部优化:提高某个task的执行效率,主要从提高CPU与内存利用率的角度进行优化。依赖Codegen技术。

3.5 Codegen

从提高cpu的利用率的角度来进行runtime优化。

  1. Expression级别

    表达式常规递归求值语法树。需要做很多类型匹配、虚函数调用、对象创建等额外逻辑,这些overhead远超对表达式求值本身,为了消除这些overhead,Spark Codegen直接拼成求值表达式的java代码并进行即时编译

  2. WholeStage级别

    传统的火山模型:SQL经过解析会生成一颗查询树,查询树的每个节点为Operator,火山模型把operator看成迭代器,每个迭代器提供一个next()接口。通过自顶向下的调用 next 接口,数据则自底向上的被拉取处理,火山模型的这种处理方式也称为拉取执行模型,每个Operator 只要关心自己的处理逻辑即可,耦合性低。

    火山模型问题:数据以行为单位进行处理,不利于CPU cache 发挥作用;每处理一行需要调用多次next() 函数,而next()为虚函数调用。会有大量类型转换和虚函数调用。虚函数调用会导致CPU分支预测失败,从而导致严重的性能回退

    Spark WholestageCodegen:为了消除这些overhead,会为物理计划生成类型确定的java代码。并进行即时编译和执行。

    Codegen打破了Stage内部算子间的界限,拼出来跟原来的逻辑保持一致的裸的代码(通常是一个大循环)然后把拼成的代码编译成可执行文件。

(资料:blog.csdn.net/weixin_4536…