《Spark 原理与实践》|青训营笔记

162 阅读8分钟

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

本节课程目录:

  1. 大数据处理引擎 Spark 介绍
  2. SparkCore 原理解析
  3. SparkSQL 原理解析
  4. 业界挑战与实践

1. 大数据处理引擎 Spark 介绍

1.1 大数据处理技术栈

截屏2022-08-01 08.44.42.png

1.2 常见大数据处理链路

截屏2022-08-01 08.45.52.png

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.

这是一个用于大规模数据处理的统一分析多语言引擎,可以用于单机节点或集群上执行数据工程,数据科学和机器学习。

image.png

1.4.2 Spark 版本演进

截屏2022-08-01 08.54.20.png

1.5 Spark 生态 & 特点

  • 统一引擎,支持多种分布式场景 截屏2022-08-01 08.58.43.png
  • 多语言支持
    • 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 运行架构 & 部署方式

image-2.png

  • 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 yarn
    • spark -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

截屏2022-08-01 09.48.09.png spark - submit \\--deploy-mode cluster \\ 截屏2022-08-01 09.48.22.png

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 image-3.png
  • 自定义 RDD
    • class CustomRDD(...) extends RDD {}

2.2.2 RDD 算子

  • Transform 算子:生成一个新的 RDD map/filter/flatMap/groupByKey/reduceByKey/... image-4.png
  • Action 算子:触发 Job 提交 collect/count/take/saveAsTextFile/... image-5.png image-6.png

2.2.3 RDD 依赖

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

  • 窄依赖:父 RDD 的每个 partition 至多对应一个子 RDD 分区。
    • NarrowDependency
    • OneToOneDependency
    • RangeDependency
    • PruneDependency
  • 宽依赖(会产生 Shuffle):父 RDD 的每个 partition 都kennel对应多个子 RDD 分区
    • ShuffleDependency

image-7.png

2.2.4 RDD 执行流程

  • Job:RDD action 算子触发
  • Stage:依据宽依赖划分。从后往前遇到宽依赖就断开,划分为一个 Stage;遇到窄依赖就将这个 RDD 加入该 Stage 中。
  • Task:Stage 内执行单个 partition 任务

image.jpeg

2.3 调度器

RDD.action -> runJob() 截屏2022-08-01 10.26.14.png

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

截屏2022-08-01 10.26.26.png

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

截屏2022-08-01 10.26.34.png

2.4 内存管理

  • Executor 内存主要有两类:Storage、Execution
    • UnifiedMemoryManager 统一管理 Storage 和 Execution 内存
    • Storage 和 Execution 内存使用是动态占用机制。双方的空间都不足时,则存储到硬盘;若己方空间不足而对方空余时,可借用对方的空间。
    • 当Storage空闲,Execution可以借用Storage的内存使用,可以减少 spill 等操作,Execution 使用的内存不能被 Storage 驱逐,但当 Execution 需要内存时,可以驱逐被 Storage 借用的内存。

截屏2022-08-01 10.31.34.png

2.4.1 多任务间内存分配

  • UnifiedMemoryManager 统一管理多个并发 Task 的内存分配
  • 每个 Task 获取的内存区间为 1/(2*N) ~ 1/N,N为当前Executor中正在并发运行的 task 数量

截屏2022-08-01 10.38.26.png

2.5 Shuffle

2.5.1 SortShuffleManager

每个 MapTask 生成一个 Shuffle 数据文件和一个 index 文件

截屏2022-08-01 10.43.26.png

2.5.2 External Shuffle Service

shuffle write 的文件被 NodeManage r中的 Shuffle Service 托管,供后续 Reduce Task 进行 shuffle fetch,如果 Executor 空闲,DRA 可以进行回收

截屏2022-08-01 10.45.22.png

3. SparkSQL 原理解析

3.1 Catalyst 优化器

image-8.png

3.1.1 RBO (Rule Based Optimizer)

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

截屏2022-08-01 16.59.43.png

3.1.2 CBO (Cost Based Optimizer)

基于代价优化,根据优化规则对关系表达式进行转换,生成多个执行计划,然后CBO会通过根据 Statistics 和代价模型 Cost Model 计算各种可能执行计划的代价,从中选用 COST 最低的执行方案,作为实际运行方案。

CBO 依赖数据库对象的统计信息,统计信息的准确与否会影响 CBO 做出最优的选择。

  • 采集表的 statistics

image-10.png

  • TableStat 从 ANALYZE TABLE 获取,后续的算子的 Stat 通过对应的 Estimation 进行估算

截屏2022-08-01 17.10.37.png

3.2 Adaptive Query Execution (AQE)

截屏2022-08-01 17.12.05.png

每个 Task 结束会发送 MapStatus 信息给 Driver,其中包含当前 Task Shuffle 产生的每个 Partition 的 size 统计信息,Driver 或许到执行完的 Stages 的 MapStatus 信息之后,按照 MapStatus 中 partition 大小信息识别匹配一些优化场景,然后对后续为执行的 Plan 进行优化。

3.2.1 Coalescing Shuffle Partitions 动态合并 shuffle 分区

image-12.png

作业运行过程中,根据前面运行完的 Stage 的 MapStatus 中实际的 partition 大小信息,可以将多个相邻的较小的 partition 进行动态合并,由一个 Task 读取进行处理。

image-11.png

3.2.2 Switching Join Strategies 动态调整 Join 策略

image-13.png

AQE 运行过程中动态获取准确 Join 的 leftChild/rightChild 的实际大小,将 SortMergeJoin 转换为 BroadcastHashJoin。

3.2.3 Optimizing Skew Joins 动态优化数据倾斜 Join

image-14.png

AQE 根据 MapStatus 信息自动检测是否有倾斜,将大的 partition 拆分成多个 Task 进行 Join。

image-15.png

3.3 Runtime Filter

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

  • 全局优化:从提升全局资源利用率、消除数据倾斜、降低IO等角度做优化。包括AQE。
  • 局部优化:提高某个task的执行效率,主要从提高CPU与内存利用率的角度进行优化。依赖Codegen技术。

3.5 Codegen

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

3.5.1 Expression 级别

截屏2022-08-01 17.30.28.png

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

3.5.2 WholeStage 级别

  • 传统的火山模型:SQL经过解析会生成一颗查询树,查询树的每个节点为Operator,火山模型把operator看成迭代器,每个迭代器提供一个next()接口。通过自顶向下的调用 next 接口,数据则自底向上的被拉取处理,火山模型的这种处理方式也称为拉取执行模型,每个Operator 只要关心自己的处理逻辑即可,耦合性低。
  • 火山模型问题:数据以行为单位进行处理,不利于 CPU cache 发挥作用;每处理一行需要调用多次 next() 函数,而 next() 为虚函数调用。会有大量类型转换和虚函数调用。虚函数调用会导致 CPU 分支预测失败,从而导致严重的性能回退

image-16.png

为了消除这些 overhead,会为物理计划生成类型确定的 java 代码,并进行即时编译和执行。Codegen打破了Stage内部算子间的界限,拼出来跟原来的逻辑保持一致的裸的代码(通常是一个大循环)然后把拼成的代码编译成可执行文件。

4. 业界挑战与实践

4.1 Shuffle 稳定性问题

截屏2022-08-01 17.35.20.png

在大规模作业下,开源 ExternalShuffleService 的实现机制容易带来大量随机读导致的磁盘 IOPS 瓶颈,Fetch 请求积压等问题,进而导致运算过程中经常会出现 Stage 重算甚至作业失败,继而引起资源使用的恶性循环,严重影响 SLA。

截屏2022-08-01 17.37.28.png

Shuffle 稳定性解决方案如上。

4.2 SQL 执行性能问题

image-17.png

压榨 CPU 资源

4.3 参数推荐/作业诊断

  • Spark 参数很多,资源类/Shuffle/Join/Agg... 调参难度大
  • 参数不合理的作业,对资源利用率/Shuffle稳定性/性能有非常大影响
  • 线上作业失败/运行慢,用户排查难度大
  • 自动参数推荐/作业诊断