这是我参与「第四届青训营 」笔记创作活动的的第5天
今天是大数据专场基础班的第五次课,主要内容是介绍Spark 的原理与实践,主要分为下面四个板块。
一、 大数据处理引擎Spark介绍
1. 大数据处理技术栈
2. 常见大数据处理链路
3. Spark特点
- 统一引擎,支持多种分布式场景
- 多语言支持
- 可读写丰富数据源
- 丰富灵活的API/算子
- 支持K8S/YARN/Mesos资源调度
4. Spark运行架构
二、 SparkCore原理解析
1. RDD
1.1 什么是 RDD ?
- RDD(Resilient Distributed Dataset): 弹性分布式数据集,是一个容错的、并行的数据结构
1.2 如何创建RDD?
- 内置RDD
- ShuffleRDD/HadoopRDD/JDBCRDD
- /KafkaRDD/ UnionRDD/MapPartitionsRDDI/...
- 自定义RDD
- class CustomRDD(...) extends RDD {}
- 实现五要素对应的函数
1.3 RDD算子
- 两类RDD算子
- Transform算子:生成一个新的RDD
- Action算子:触发Job 提交
1.4 RDD 依赖
- RDD依赖:描述父子RDD之间的依赖关系(lineage)
- 窄依赖: 父RDD的每个partition至多对应一个子RDD分区
- NarrowDependency
- OneToOneDependency
- RangeDependency
- PruneDependency
- 宽依赖: 父RDD的每个partition都可能对应多个子RDD分区
- ShuffleDependency
- 窄依赖: 父RDD的每个partition至多对应一个子RDD分区
1.5 RDD 执行流程
- Job: RDD action算子触发
- Stage: 依据宽依赖划分
- Task: Stage内执行单个partition任务
2. Scheduler
- 根据 ShuffleDependency切分Stage,并按照依赖顺序调度Stage,为每个Stage生成并提交TaskSet 到TaskScheduler
- 根据调度算法(FIFO/FAIR)对多个TaskSet进行调度,对于调度到的TaskSet,会将Task 调度(locality)到相关Executor上面执行,Executor SchedulerBackend提供
3. 内存管理
- Executor 内存主要有两类: Storage、Execution
- UnifiedMemoryManager统一管理Storage/Execution内存
- Storage和Execution内存使用是动态调整,可以相互借用
- 当Storage空闲,Execution可以借用 Storage的内存使用,可以减少spill等操作,Execution使用的内存不能被Storage 驱逐
- 当Execution空闲,Storage可以借用Execution的内存使用
- 当Execution需要内存时,可以驱逐被Storage借用的内存,直到spark.memory.storageFraction边界
- UnifiedMemoryManager 统一管理多个并发Task的内存分配
- 每个Task获取的内存区间为1/(2*N)~1/N,N为当前Executor中正在并发运行的task数量
4. Shuffle
- 每个MapTask生成一个Shuffle数据文件和一个index文件dataFile 中的数据按照partitionld进行排序,同一个partitionld的数据聚集在一起
- indexFile保存了所有paritionld在dataFlle中的位置信息,方便后续ReduceTask 能 Fetch 到对应 partitionld的数据
- External Shuffle Service + Dynamic Resource Allocation(DRA)
- shuffle write的文件被NodeManage r中的Shuffle Service 托管,供后续ReduceTask进行shuffle fetch,如果Executor空闲,DRA可以进行回收
三、 SparkSQL 原理解析
1. SparkSQL
2. Catalyst优化器 - RBO
- Rule Based Optimizer(RBO): 基于规则优化,对语法树进行一次遍历,模式匹配能够满足特定规则的节点,再进行相应的等价转换
-
Batch执行策略:
-
Once ->只执行一次
-
FixedPoint ->重复执行,直到plan不再改变,或者执行达到固定次数(默认100次)
-
3. Adaptive Query Execution(AQE)
- 每个Task结束会发送MapStatus信息给Driver
- Task的MapStatus中包含当前Task Shuffle产生的每个Partition的size统计信息
- Driver获取到执行完的Stages的MapStatus信息之后,按照MapStatus中partition大小信息识别匹配一些优化场景,然后对后续未执行的Plan进行优化
- 目前支持的优化场景:
- Partiiton合并,优化shuffle读取,减少reduce task个数
- SMJ -> BHJ
- Skew Join优化
AQE框架三种优化场景:
- 动态合并shuffle分区(Dynamically coalescing shuffle partitions) Partition合并
- 动态调整Join策略(Dynamically switching join strategies) problem: Catalyst Optimizer优化阶段,算子的statistics估算不准确,生成的执行计划并不是最优
solution: AQE运行过程中动态获取准确Join的leftChild/rightChild的实际大小,将SortMergeJoin (SMJ) 转化为BroadcastHashJoin (BHJ)
- 动态优化数据倾斜Join(Dynamically optimizing skew joins) AQE根据MapStatus信息自动检测是否有倾斜,将大的partition拆分成多个Task进行Join
4. RuntimeFilter
实现在Catalyst中。动态获取Filter内容做相关优化,当我们将一张大表和一张小表等值连接时,我们可以从小表侧收集一些统计信息,并在执行join前将其用于大表的扫描,进行分区修剪或数据过滤。可以大大提高性能
- Runtime优化分两类:
- 全局优化: 从提升全局资源利用率、消除数据倾斜、降低IO等角度做优化。包括AQE。
- 局部优化: 提高某个task的执行效率,主要从提高CPU与内存利用率的角度进行优化。依赖Codegen技术。
5. Codegen
从提高cpu的利用率的角度来进行runtime优化。
- Expression级别
- 表达式常规递归求值语法树。需要做很多类型匹配、虚函数调用、对象创建等额外逻辑,这些overhead远超对表达式求值本身,为了消除这些overhead,Spark Codegen直接拼成求值表达式的java代码并进行即时编译
- 将表达式中的大量虚函数调用压平到一个函数内部,类似手写代码
- WholeStage级别
-
传统的火山模型:SQL经过解析会生成一颗查询树,查询树的每个节点为Operator,火山模型把operator看成迭代器,每个迭代器提供一个next()接口。通过自顶向下的调用 next 接口,数据则自底向上的被拉取处理,火山模型的这种处理方式也称为拉取执行模型,每个Operator 只要关心自己的处理逻辑即可,耦合性低。
-
火山模型问题:数据以行为单位进行处理,不利于CPU cache 发挥作用;每处理一行需要调用多次next() 函数,而next()为虚函数调用。会有大量类型转换和虚函数调用。虚函数调用会导致CPU分支预测失败,从而导致严重的性能回退
- 算子之间大量的虚函数调用,开销大
- 将同一个Stage中的多个算子压平到一个函数内部进行执行 Spark WholestageCodegen:为了消除这些overhead,会为物理计划生成类型确定的java代码。并进行即时编译和执行。
Codegen打破了Stage内部算子间的界限,拼出来跟原来的逻辑保持一致的裸的代码(通常是一个大循环)然后把拼成的代码编译成可执行文件。
四、 业界挑战与实践
1. Shuffle稳定性问题
- 在大规模作业下,开源External Shuffle Service(ESS)的实现机制容易带来大量随机读导致的磁盘IOPS瓶颈、Fetch请求积压等问题,进而导致运算过程中经常会出现Stage重算甚至作业失败,继而引起资源使用的恶性循环,严重影响SLA
- Shuffle稳定性解决方案
2. SQL 执行性能问题
压榨CPU资源
- SQL执行性能解决方向
- Photon: C++实现的向量化执行引擎
- lntel: OAP/gazelle_plugin
3. 参数推荐/作业诊断
- 问题
- Spark参数很多,资源类/Shuffle/Join/Aggl ...调参难度大
- 参数不合理的作业,对资源利用率/Shuffle稳定性/性能有非常大影响
- 同时,线上作业失败/运行慢,用户排查难度大
- 解决方向
- 自动参数推荐/作业诊断
五、 课程总结
通过本节课的学习,让我对于大数据处理常见的场景链路有了大致的了解,以及Spark技术栈的相关知识,包括SparkCore中的RDD/调度/Shuffle/内存管理等概念机制,以及SQL在Spark引擎中执行的详细流程,了解目前业界主要遇到的挑战以及解决方案,学习思考问题是如何产生的、从而去寻找解决问题的思路。
引用参考
内容主要参考了刘畅老师在「Spark 原理与实践」课程里所教授的内容,同时也参考了学员手册里第二节的内容,图片来自于老师的PPT,链接如下: