「Spark 原理与实践」第四届字节跳动青训营 - 大数据专场
这是我参与「第四届青训营」笔记创作活动的的第3天
01.大数据处理引擎 Spark介绍
大数据处理技术栈
开源大数据处理引擎
什么是 Spark?
Spark版本演进
Spark生态&特点
- 统一引擎,支持多种分布式场景
- 多语言支持
- 可读写丰富数据源
- 丰富灵活的API/算子
- 支持K8S/YARN/Mesos资源调度
Spark特点-多语言支持
Spark特点-丰富数据源
内置DataSource
- Text
- Parquet/ORC
- JSON/CSV
- JDBC
自定义DataSource
- 实现DataSourceV1N2 API
- HBase/Mongo/ElasticSearch/...
- A community index of third. party
- packages for Apache Spark. spankepackages.org
Spark特点-丰富的API/算子
Spark运行架构&部署方式
Spark Local Mode 本地测试/单进程多线程模式
spark- sql --master local[*] ...
Spark Standalone Mode 需要启动Spark的Standalone集群的Master/Worker
spark-sql --master spark:/${master_
ip}:${port} On YARN/K8S 依赖外部资源调度器(YARN/K8S)
02.SparkCore原理解析
SparkCore
什么是RDD ?
- RDD(Resilient Distributed Dataset) 弹性分布式数据集
- Represents an immutable, partitioned collection of elements that can be operated on in parallel. 表示可以并行操作的不可变的分区元素集合。
如何创建RDD?
RDD算子
RDD依赖
RDD依赖:描述父子RDD之间的依赖关系(lineage)
窄依赖:父RDD的每个parttion至多对应一个子RDD分区。
-
NarrowDependency
-
def getParents(partitionld: Int): Seq[Int]
-
-
OneToOneDependency
-
override def getParents(partitionld: Int): Lst[Int] = List(partitionld)
-
-
RangeDependency
-
override def getParents(partitionld: Int): List[Int] = if (partitionld >= outStart && partitionld < outStart + length) List(partitionld - outStart + inStart)
-
-
PruneDependency
宽依赖:父RDD的每个partition都可能对应多个子RDD分区。
- ShuffleDependency
窄依赖:父RDD的每个partition至多对应一个子RDD分区
- NarrowDependency
- PruneDependency
- RangeDependency
- OneToOneDependency
宽依赖(会产生Shuffle):父RDD的每个partition都可能对应多个子RDD分区
- ShuffleDependency
RDD执行流程
- Job:RDD action算子触发
- Stage:依据宽依赖划分
- Task: Stage内执行单个partition 任务
内存管理
-
Executor内存主要有两类: Storage、Execution
-
UnifiedMemoryManager统一管理Storage/Execution内存
-
Storage和Execution内存使用是动态调整,可以相互借用
- 当Storage空闲,Execution可以借用Storage的内存使用,可以减少spill等操作,Execution使用的内存不能被Storage 驱逐
- 当Execution空闲,Storage可以借用Execution的内存使用
- 当Execution需要内存时,可以驱逐被Storage借用的内存,直到spark.memory.storageFraction边界
Shuffle
03.SparkSQL原理解析
Catalyst优化器-RBO
Rule Based Optimizer(RBO): 基于规则优化,对语法树进行一次遍历,模式匹配能够满足特定规则的节点,再进行相应的等价转换。
Batch执行策略:
- Once ->只执行一次
- FixedPoint ->重复执行,直到plan不再改变,或者执行达到固定次数(默认100次)
- transformDown先序遍历树进行规则匹配
- transformUp后序遍历树进行规则匹配
Adaptive Query Execution(AQE,自适应查询执行)
-
AQE对于整体的Spark SQL的执行过程做了相应的调整和优化,它最大的亮点是可以根据已经完成的计划结点真实且精确的执行统计结果来不停的反馈并重新优化剩下的执行计划。
-
AQE框架三种优化场景:
- 动态合并shuffle分区(Dynamically coalescing shuffle partitions)
- 动态调整Join策略(Dynamically switching join strategies)
- 动态优化数据倾斜Join(Dynamically optimizing skew joins)
-
-
每个Task结束会发送MapStatus信息给Driver
-
Task的MapStatus中包含当前Task Shuffle产生的每个Partition的size统计信息
-
Driver获取到执行完的Stages的MapStatus信息之后,按照MapStatus中partition大小信息识别匹配一些优化场景,然后对后续未执行的Plan进行优化
-
目前支持的优化场景:
- Partiton合并,优化shuffle读取,减少reduce task个数-SMJ -> BHJ
- Skew Join优化
-
AQE Coalescing Shuffle Partitions 合并shuffle分区
-
spark.sql.shuffle.partition作业粒度参数,一个作业中所有Stage都一样,但是每个Stage实际处理的数据不一样,可能某些stage的性能比较差 -
比如:
- partition参数对某个Stage过大,则可能单个partition的大小比较小,而且Task个数会比较多,shuffle fetch阶段产生大量的小块的随机读,影响性能
- parition参数对某个Stage过小,则可能单个partition的大小比较大,会产生更多的spill或者OOM
-
-
AQE - Switching Join Strategies 动态调整join策略
SortMergeJoin (SMJ) ->BroadcastHashJoin (BHJ)
1. Broadcast hash join (BHJ)
Broadcast Hash Join 的实现是将小表的数据广播到 Spark 所有的 Executor 端,这个广播过程和我们自己去广播数据没什么区别:
- 利用 collect 算子将小表的数据从 Executor 端拉到 Driver 端
- 在 Driver 端调用 sparkContext.broadcast 广播到所有 Executor 端
- 在 Executor 端使用广播的数据与大表进行 Join 操作(实际上是执行map操作)
总结:这种 Join 策略避免了 Shuffle 操作。一般而言,Broadcast Hash Join 会比其他 Join 策略执行的要快,只能用于等值 Join,不要求参与 Join 的 keys 可排序.
2. Shuffle hash join(SHJ)
当表中的数据比较大,又不适合使用广播,这个时候就可以考虑使用 Shuffle Hash Join。
Shuffle Hash Join 同样是在大表和小表进行 Join 的时候选择的一种策略。它的计算思想是:把大表和小表按照相同的分区算法和分区数进行分区(根据参与 Join 的 keys 进行分区),这样就保证了 hash 值一样的数据都分发到同一个分区中,然后在同一个 Executor 中两张表 hash 值一样的分区就可以在本地进行 hash Join 了
总结:仅支持等值 Join,不要求参与 Join 的 Keys 可排序,对表的大小有限制。
3. Shuffle sort merge join (SMJ)
前面两种 Join 策略对表的大小都有条件的,如果参与 Join 的表都很大,这时候就得考虑用 Shuffle Sort Merge Join了。
Shuffle Sort Merge Join 的实现思想:
- 将两张表按照 join key 进行shuffle,保证join key值相同的记录会被分在相应的分区对每个分区内的数据进行排序排序后再对相应的分区内的记录进行连接。
总结:无论分区有多大,Sort Merge Join都不用把一侧的数据全部加载到内存中,而是即用即丢.仅支持等值 Join,并且要求参与 Join 的 Keys 可排序
-
问题
- Catalyst Optimizer优化阶段,算子的statistics估算不准确,生成的执行计划并不是最优
-
解决方法
- AQE运行过程中动态获取准确Join的leftChild/rightChild的实际大小,将SMJ转换为BHJ
AQE - Optimizing Skew Joins 解决数据倾斜的性能优化
- AQE根据MapStatus信息自动检测是否有倾斜
- 将大的partition拆分成多个Task进行Join
| 属性名 | 默认值 | 开始适用的版本 | | ----------------------------------------------------------- | ----- | ------- | | spark.sql.adaptive.skewJoin.enabled | true | 3.0.0 | | spark.sql.adaptive.skewJoin.skewedPartitionFactor | 10 | 3.0.0 | | spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes | 256MB | 3.0.0 |
Runtime Filter
Runtime Filter减少了大表的扫描,shuffle的数据量以及参加Join的数据星,所以对整个集群IO/网络/CPU有比较大的节省(10TB TPC-DS获得了大约35%的改进)
Runtime优化分两类:
- 全局优化:从提升全局资源利用率、消除数据倾斜、降低IO等角度做优化。包括AQE。
- 局部优化:提高某个task的执行效率,主要从提高CPU与内存利用率的角度进行优化。依赖Codegen技术。
Bloom Runtime Filter
京东 Spark 基于 Bloom Filter 算法的 Runtime Filter Join 优化机制
Codegen
从提高cpu的利用率的角度来进行runtime优化。
Expression级别
将表达式中的大量虚函数调用压平到一个函数内部,类似手写代码
WholeStage级别 (算子级别)
- 算子之间大量的虚函数调用,开销大
- 将同一个Stage中的多个算子压平到一个函数内部进行执行
- 一个SQL包含多个Stage的WholeStageCodegen
04.业界挑战与实践
- Shuffle稳定性解决方案
- SQL执行性能问题
- 参数推荐/作业诊断