这是我参与「第四届青训营」笔记创作活动的第6天
本节课程目录:
- 大数据处理引擎 Spark 介绍
- SparkCore 原理解析
- SparkSQL 原理解析
- 业界挑战与实践
1. 大数据处理引擎 Spark 介绍
1.1 大数据处理技术栈
1.2 常见大数据处理链路
1.3 开源大数据处理引擎
- 批式计算 Batch
- Hive
- Hadoop
- Spark
- 流式计算 Streaming
- Flink
- OLAP
- Presto
- ClickHouse
- Impala
- Doris
1.4 什么是 Spark
1.4.1 Spark 定义
Apache Spark is a multi - language engine for executing data engineering, data science, and machine learning on single - node machines or clusters.
这是一个用于大规模数据处理的统一分析多语言引擎,可以用于单机节点或集群上执行数据工程,数据科学和机器学习。
1.4.2 Spark 版本演进
1.5 Spark 生态 & 特点
- 统一引擎,支持多种分布式场景
- 多语言支持
- SQL
- Java/Scala
- R
- Python
- 可读写丰富数据源
- 内置 DataSource(Text、Parquet/ORC、JSON/CSV、JDBC)
- 自定义 DataSource(实现 DataSourceV1/V2 API、HBase/Mongo/ElasticSearch/...)
- 丰富灵活的 API/算子
- SparkCore -> RDD
- SparkSQL -> DataFrame
- 支持 K8S/YARN/Mesos 资源调度
1.6 Spark 运行架构 & 部署方式
- Spark 运行架构
进程之间通过 Driver Program 中的 SparkContext 进行协调,SparkContext 能够与 Cluster Manager 通信,一旦连接 Spark 就会为该应用在各个集群节点上申请 Executor,用于执行计算任务和存储数据。Spark 将应用程序代码发送给所申请到的执行器,SparkContext 将分割出的 Task 发送给各个执行器去执行。
- 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)
spark -sql --master yarnspark -sql --master k8s://https://<k8s-apiserver-host>:<k8s-apiserver-port>
1.7 Spark UI
- Job 运行中 ->
http://${driver_host}:$port - Job 运行完 -> SparkHistoryServer 可查看
2. SparkCore 原理解析
2.1 SparkCore
spark - submit \\--deploy-mode cluster \\
2.2 什么是RDD
RDD (Resilient Distributed Dataset):Represents an immutable, partitioned collection of elements that can be operated on in parallel.
RDD是一个容错的,可以并行执行的分布式数据集。它是 Spark 中最基本的数据处理模型。
- RDD 的五个特性
- Partitions:A list of partitions 有多个分区在集群的不同节点上运行,分区决定了并行计算的数量,可以在创建RDD时指定分区个数
- Compute:A function for computing each split 含有计算函数
- Dependencies:A list of dependencies on other RDDs 每个 RDD 都依赖于其他的 RDD,每次转换都会生成新的 RDD
- Partitioner:A partitioner for key-value RDDs(e.g. to say that the RDD is hash-paartitioned)
- PreferredLocations:A list of preferred locations to compute each split on 含有优先位置列表,存储每个优先位置
2.2.1 如何创建 RDD
- 内置 RDD
- ShuffleRDD
- HadoopRDD
- JDBCRDD
- KafkaRDD
- UnionRDD
- MapPartitionsRDD
- 自定义 RDD
- class CustomRDD(...) extends RDD {}
2.2.2 RDD 算子
- Transform 算子:生成一个新的 RDD
map/filter/flatMap/groupByKey/reduceByKey/... - Action 算子:触发 Job 提交
collect/count/take/saveAsTextFile/...
2.2.3 RDD 依赖
RDD 依赖:描述父子 RDD 之间的依赖关系 lineage
- 窄依赖:父 RDD 的每个 partition 至多对应一个子 RDD 分区。
- NarrowDependency
- OneToOneDependency
- RangeDependency
- PruneDependency
- 宽依赖(会产生 Shuffle):父 RDD 的每个 partition 都kennel对应多个子 RDD 分区
- ShuffleDependency
2.2.4 RDD 执行流程
- Job:RDD action 算子触发
- Stage:依据宽依赖划分。从后往前遇到宽依赖就断开,划分为一个 Stage;遇到窄依赖就将这个 RDD 加入该 Stage 中。
- Task:Stage 内执行单个 partition 任务
2.3 调度器
RDD.action -> runJob()
根据 ShuffleDependency 划分 Stage,并按照依赖顺序调度 Stage,为每个 Stage 生成并提交 TaskSet 到 TaskScheduler
根据调度算法(FIFO/FAIR)对多个 TaskSet 进行调度,对于调度到的 TaskSet,会将 Task 调度 (locality) 到相关 Executor 上面执行,Executor SchedulerBackend 提供
2.4 内存管理
- Executor 内存主要有两类:Storage、Execution
- UnifiedMemoryManager 统一管理 Storage 和 Execution 内存
- Storage 和 Execution 内存使用是动态占用机制。双方的空间都不足时,则存储到硬盘;若己方空间不足而对方空余时,可借用对方的空间。
- 当Storage空闲,Execution可以借用Storage的内存使用,可以减少 spill 等操作,Execution 使用的内存不能被 Storage 驱逐,但当 Execution 需要内存时,可以驱逐被 Storage 借用的内存。
2.4.1 多任务间内存分配
- UnifiedMemoryManager 统一管理多个并发 Task 的内存分配
- 每个 Task 获取的内存区间为 1/(2*N) ~ 1/N,N为当前Executor中正在并发运行的 task 数量
2.5 Shuffle
2.5.1 SortShuffleManager
每个 MapTask 生成一个 Shuffle 数据文件和一个 index 文件
2.5.2 External Shuffle Service
shuffle write 的文件被 NodeManage r中的 Shuffle Service 托管,供后续 Reduce Task 进行 shuffle fetch,如果 Executor 空闲,DRA 可以进行回收
3. SparkSQL 原理解析
3.1 Catalyst 优化器
3.1.1 RBO (Rule Based Optimizer)
基于规则优化,对语法树进行一次遍历,模式匹配能够满足特定规则的节点,再进行相应的等价转换。
3.1.2 CBO (Cost Based Optimizer)
基于代价优化,根据优化规则对关系表达式进行转换,生成多个执行计划,然后CBO会通过根据 Statistics 和代价模型 Cost Model 计算各种可能执行计划的代价,从中选用 COST 最低的执行方案,作为实际运行方案。
CBO 依赖数据库对象的统计信息,统计信息的准确与否会影响 CBO 做出最优的选择。
- 采集表的 statistics
- TableStat 从 ANALYZE TABLE 获取,后续的算子的 Stat 通过对应的 Estimation 进行估算
3.2 Adaptive Query Execution (AQE)
每个 Task 结束会发送 MapStatus 信息给 Driver,其中包含当前 Task Shuffle 产生的每个 Partition 的 size 统计信息,Driver 或许到执行完的 Stages 的 MapStatus 信息之后,按照 MapStatus 中 partition 大小信息识别匹配一些优化场景,然后对后续为执行的 Plan 进行优化。
3.2.1 Coalescing Shuffle Partitions 动态合并 shuffle 分区
作业运行过程中,根据前面运行完的 Stage 的 MapStatus 中实际的 partition 大小信息,可以将多个相邻的较小的 partition 进行动态合并,由一个 Task 读取进行处理。
3.2.2 Switching Join Strategies 动态调整 Join 策略
AQE 运行过程中动态获取准确 Join 的 leftChild/rightChild 的实际大小,将 SortMergeJoin 转换为 BroadcastHashJoin。
3.2.3 Optimizing Skew Joins 动态优化数据倾斜 Join
AQE 根据 MapStatus 信息自动检测是否有倾斜,将大的 partition 拆分成多个 Task 进行 Join。
3.3 Runtime Filter
动态获取Filter内容做相关优化,当我们将一张大表和一张小表等值连接时,我们可以从小表侧收集一些统计信息,并在执行join前将其用于大表的扫描,进行分区修剪或数据过滤。减少了大表的扫描,可以大大提高性能。
- 全局优化:从提升全局资源利用率、消除数据倾斜、降低IO等角度做优化。包括AQE。
- 局部优化:提高某个task的执行效率,主要从提高CPU与内存利用率的角度进行优化。依赖Codegen技术。
3.5 Codegen
从提高cpu的利用率的角度来进行runtime优化
3.5.1 Expression 级别
表达式常规递归求值语法树。需要做很多类型匹配、虚函数调用、对象创建等额外逻辑,这些 overhead 远超对表达式求值本身,为了消除这些 overhead,Spark Codegen 直接拼成求值表达式的 java 代码并进行即时编译
3.5.2 WholeStage 级别
- 传统的火山模型:SQL经过解析会生成一颗查询树,查询树的每个节点为Operator,火山模型把operator看成迭代器,每个迭代器提供一个next()接口。通过自顶向下的调用 next 接口,数据则自底向上的被拉取处理,火山模型的这种处理方式也称为拉取执行模型,每个Operator 只要关心自己的处理逻辑即可,耦合性低。
- 火山模型问题:数据以行为单位进行处理,不利于 CPU cache 发挥作用;每处理一行需要调用多次 next() 函数,而 next() 为虚函数调用。会有大量类型转换和虚函数调用。虚函数调用会导致 CPU 分支预测失败,从而导致严重的性能回退
为了消除这些 overhead,会为物理计划生成类型确定的 java 代码,并进行即时编译和执行。Codegen打破了Stage内部算子间的界限,拼出来跟原来的逻辑保持一致的裸的代码(通常是一个大循环)然后把拼成的代码编译成可执行文件。
4. 业界挑战与实践
4.1 Shuffle 稳定性问题
在大规模作业下,开源 ExternalShuffleService 的实现机制容易带来大量随机读导致的磁盘 IOPS 瓶颈,Fetch 请求积压等问题,进而导致运算过程中经常会出现 Stage 重算甚至作业失败,继而引起资源使用的恶性循环,严重影响 SLA。
Shuffle 稳定性解决方案如上。
4.2 SQL 执行性能问题
压榨 CPU 资源
4.3 参数推荐/作业诊断
- Spark 参数很多,资源类/Shuffle/Join/Agg... 调参难度大
- 参数不合理的作业,对资源利用率/Shuffle稳定性/性能有非常大影响
- 线上作业失败/运行慢,用户排查难度大
- 自动参数推荐/作业诊断