Spark 原理与实践笔记(二)| 青训营笔记
这是我参与「第四届青训营 -大数据场」笔记创作活动的第7天
三、SparkSQL原理解析
1. SparkSQL执行链路
2. 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字节码
2.1 影响SparkSQL性能两大技术:
- Optimizer:执行计划的优化,目标是找出最优的执行计划
- Runtime:运行时优化,目标是在既定的执行计划下尽可能快的执行完毕。
3. Catalyst优化
3.1 RBO(Rule Based Optimizer)
-
使用RuleExecutor,每个batch代表一个rule集合
-
两种执行策略
- once:只执行一次
- fixedpoint:重复执行直到plan不再改变或达到固定次数
-
两种遍历规则
- transformDown 先序遍历树进行规则匹配
- transformUp 后序遍历树进行规则匹配
3.2 CBO(Cost Based Optimizer)
-
根据优化规则生成多个执行计划,计算代价选择最优方案,依赖数据库对象的统计(ANALYZE TABLE收集表信息)进行估算
-
JoinSelection
- Broadcast Join:大表和小表
- Shuffle Hash Join
- SortMergeJoin:大表
4. Adaptive Query Execution(AQE)
- 根据已经完成计划的反馈进行统计,优化后续的task
- 每个task结束后发送mapstatus信息给driver
支持的优化场景:
- 动态合并shuffle分区(Dynamically coalescing shuffle partitions)
- 动态调整Join策略(Dynamically switching join strategies)
- 动态优化数据倾斜Join(Dynamically optimizing skew joins)
5. RuntimeFilter
实现在Catalyst中。动态获取Filter内容做相关优化,当我们将一张大表和一张小表等值连接时,我们可以从小表侧收集一些统计信息,并在执行join前将其用于大表的扫描,进行分区修剪或数据过滤。可以大大提高性能。
-
Runtime优化分两类:
- 全局优化:从提升全局资源利用率、消除数据倾斜、降低IO等角度做优化。包括AQE。
- 局部优化:提高某个task的执行效率,主要从提高CPU与内存利用率的角度进行优化。依赖Codegen技术。
-
Bloom RuntimeFilter:利用bloomfilter对数据源进行过滤
6. Codegen
从提高cpu的利用率的角度来进行runtime优化。
6.1 Expression级别
- 表达式常规递归求值语法树。需要做很多类型匹配、虚函数调用、对象创建等额外逻辑,这些overhead远超对表达式求值本身,为了消除这些overhead,Spark Codegen直接拼成求值表达式的java代码并进行即时编译
6.2 WholeStage级别
- 传统的火山模型:SQL经过解析会生成一颗查询树,查询树的每个节点为Operator,火山模型把operator看成迭代器,每个迭代器提供一个next()接口。通过自顶向下的调用 next 接口,数据则自底向上的被拉取处理,火山模型的这种处理方式也称为拉取执行模型,每个Operator 只要关心自己的处理逻辑即可,耦合性低。
- 火山模型问题:数据以行为单位进行处理,不利于CPU cache 发挥作用;每处理一行需要调用多次next() 函数,而next()为虚函数调用。会有大量类型转换和虚函数调用。虚函数调用会导致CPU分支预测失败,从而导致严重的性能回退
- Spark WholestageCodegen:为了消除这些overhead,会为物理计划生成类型确定的java代码。并进行即时编译和执行。
Codegen打破了Stage内部算子间的界限,拼出来跟原来的逻辑保持一致的裸的代码(通常是一个大循环)然后把拼成的代码编译成可执行文件。
四、业界挑战与实践
1. shuffle稳定性问题
在大规模作业下,开源ESS的实现机制容易带来大量随机读导致磁盘的IOPS瓶颈、fetch请求积压等问题,进而导致运算过程中经常出现stage重算及作业失败继而引起资源使用的恶性循环,严重影响SLA
- 解决方案:
2. SQL执行性能问题
压榨CPU资源(CPU瓶颈)
- 超标量流水线/乱序执行/分支预测 并行程序越多越好/CPU缓存友好(后续cache预存)/SIMD(单指令多数据流)
- Vectorized 向量化(拉取模式函数返回一批 CPU开销一组数据分摊 适用于列存储 缺点中间数据很大)/ Codegen (打破算子之间界限,复合算子)
- Codegen限制Java代码,相对native C++等性能有缺陷,无法进行SIMD优化
3. 参数推荐/作业诊断
-
问题:
- Spark参数很多,资源类/shuffle/join/agg等,调参难度大
- 参数不合理的作业对资源利用率/shuffle稳定性/性能有非常大影响
- 线上作业失败/运行慢,用户排查难度大
-
解决方案:
- 自动参数推荐/作业诊断