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

192 阅读8分钟

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


一、笔记内容

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

2. SparkCore原理解析

3. SparkSQL原理解析

4. 业界挑战与实践

二、大数据处理引擎Spark介绍

1.Spark生态组件和特点

image.png

  • 统一引擎、支持多种分布式场景;

    • Spark Core:Spark核心组件,它实现了Spark的基本功能,包含任务调度、内存管理、错误恢复、与存储系统交互等模块。

    • Spark Structured Streaming:Spark提供的流式计算框架,支持高吞吐量、可容错处理的实时流式数据处理。

  • 多语言支持;

    • SQL、Python、Java、Scala、R等。
  • 可读写丰富数据源;

    • Parquet、ORC、Text、Json等。
  • Spark SQL:用来操作结构化数据的核心组件,通过Spark SQL可以直接查询Hive、HBase等多种外部数据源中的数据。

  • 丰富灵活的API/算子

    • SparkCore --> RDD、Spark SQL --> DataFrame;

    • GraphX:Spark提供的分布式图处理框架,拥有对图计算和图挖掘算法的API接口以及丰富的功能和运算符。

    • MLlib:Spark提供的关于机器学习功能的算法程序库,包括分类、回归、聚类、协同过滤算法等,还提供了模型评估、数据导入等额外的功能。

  • 支持K8S/YARA/Mesos资源调度。

    • 独立调度器、Yarn、Mesos、Kubernetes等;

Spark框架可以高效地在一个到数千个节点之间伸缩计算,集群管理器则主要负责各个节点的资源管理工,为了实现这样的要求,同时获得最大灵活性,Spark支持在各种集群管理器(Cluster Manager)上运行。

2.Spark运行架构和工作原理

image.png

Spark应用在集群上运行时,包括了多个独立的进程,这些进程之间通过驱动程序(Driver Program)中的SparkContext对象进行协调,SparkContext对象能够与多种集群资源管理器(Cluster Manager)通信,一旦与集群资源管理器连接,Spark会为该应用在各个集群节点上申请执行器(Executor),用于执行计算任务和存储数据。Spark将应用程序代码发送给所申请到的执行器,SparkContext对象将分割出的任务(Task)发送给各个执行器去运行。

  • Application(应用):Spark上运行的应用。Application中包含了一个驱动器(Driver)进程和集群上的多个执行器(Executor)进程。

  • Driver Program(驱动器):运行main()方法并创建SparkContext的进程。

  • Cluster Manager(集群管理器):用于在集群上申请资源的外部服务(如:独立部署的集群管理器、Mesos或者Yarn)。

  • Worker Node(工作节点):集群上运行应用程序代码的任意一个节点。

  • Executor(执行器):在集群工作节点上为某个应用启动的工作进程,该进程负责运行计算任务,并为应用程序存储数据。

  • Task(任务):执行器的工作单元。

  • Job(作业):一个并行计算作业,由一组任务(Task)组成,并由Spark的行动(Action)算子(如:save、collect)触发启动。

  • Stage(阶段):每个Job可以划分为更小的Task集合,每组任务被称为Stage。

三、SparkCore原理解析

1.RDD

1.认识RDD

RDD(Resilient Distributed Dataset):弹性分布式数据集,是一个容错的、并行的数据结构。

RDD算子:对任何函数进行某一项操作都可以认为是一个算子,RDD算子是RDD的成员函数。  

graph TD
RDD要素 --> Partitions

RDD要素 --> Compute

RDD要素 --> Dependencies

RDD要素 --> Partitioner

RDD要素 --> PreferredLocations

2.RDD算子

  • Transform(转换)算子: 根据已有RDD创建新的RDD。

  • Action(动作)算子: 将在数据集上运行计算后的数值返回到驱动程序,从而触发真正的计算。

3.RDD依赖

窄依赖(Narrow Dependency):父RDD的每个partition至多对应一个子RDD分区。

graph TD
NarrowDependency --> PruneDependency

NarrowDependency --> RangeeDependency

NarrowDependency --> OneToOneDependency

宽依赖(会产生Shuffle):父RDD的每个partition可能对应多个子RDD分区,有ShuffleDependency。

4.RDD执行过程

划分Stage的整体思路:从后往前推,遇到宽依赖就断开,划分为一个Stage。遇到窄依赖,就将这个RDD加入该Stage中,DAG最后一个阶段会为每个结果的Partition生成一个ResultTask。每个Stage里面的Task数量由最后一个RDD的Partition数量决定,其余的阶段会生成ShuffleMapTask。

2.调度器

  • 当RDD对象创建后,SparkContext会根据RDD对象构建DAG有向无环图,然后将Task提交给DAGScheduler。

  • DAGScheduler根据ShuffleDependency将DAG划分为不同的Stage,为每个Stage生成TaskSet任务集合,并以TaskSet为单位提交给TaskScheduler。

  • TaskScheduler根据调度算法(FIFO/FAIR)对多个TaskSet进行调度,并通过集群中的资源管理器(Standalone模式下是Master,Yarn模式下是ResourceManager)把Task调度(locality)到集群中Worker的Executor,Executor由SchedulerBackend提供。

3.内存管理

Spark 作为基于内存的分布式计算引擎,Spark采用统一内存管理机制。重点在于动态占用机制

graph TD
ExecutorContainer --> ExecutionMemory

ExecutorContainer -->StorageMemory

ExecutorContainer --> userMemory

ExecutorContainer --> ReserverdMemory
  • Executor 的内存主要有两类:Storage、Execution:

    • 设定基本的存储内存(Storage)和执行内存(Execution)区域,该设定确定了双方各自拥有的空间的范围,UnifiedMemoryManager统一管理Storage/Execution内存。

    • 双方的空间都不足时,则存储到硬盘;若己方空间不足而对方空余时,可借用对方的空间。

    • 当Storage空闲,Execution可以借用Storage的内存使用,可以减少spill等操作, Execution内存不能被Storage驱逐。Execution内存的空间被Storage内存占用后,可让对方将占用的部分转存到硬盘,然后"归还"借用的空间。

    • 当Execution空闲,Storage可以借用Execution内存使用,当Execution需要内存时,可以驱逐被Storage借用的内存,可让对方将占用的部分转存到硬盘,然后"归还"借用的空间。

  • user memory:存储用户自定义的数据结构或者spark内部元数据。

  • Reserverd memory:预留内存,防止OOM(Out Of Memory Killer)。

堆内(On-Heap)内存/堆外(Off-Heap)内存:Executor 内运行的并发任务共享 JVM 堆内内存。为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 可 以直接操作系统堆外内存,存储经过序列化的二进制数据。减少不必要的内存开销,以及频繁的 GC 扫描和回收,提升了处理性能。

UnifiedMemoryManager 统一管理多个并发Task的内存分配。

4.Shuffle

  • Shuffle的实现:SortShuffleManager。

  • External Shuffle Service:处理读写请求,运行在主机上,管理主机上的数据。

四、SparkSQL原理解析

1.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.calalyst优化器

RBO、CBO

JoinSelection:

  • Broadcast Hash Join(BHJ) --> 小表(小于10MB)

  • Shuffle Hash Join(SHJ) --> 小表

  • Sort Merge Join(SMJ) --> 大表

3.AQE(Adaptive Query Execution)

AQE框架三种优化场景:

  • 动态合并shuffle分区(Dynamically coalescing shuffle partitions)--> Partition个数。

  • 动态调整Join策略(Dynamically switching join strategies)--> SMJ转换为BHJ。

  • 动态优化数据倾斜Join(Dynamically optimizing skew joins)--> 拆分大的Partition。

4.RuntimeFilter

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

Runtime优化分两类

  • 全局优化:从提升全局资源利用率、消除数据倾斜、降低IO等角度做优化。包括AQE。

  • 局部优化:提高某个task的执行效率,主要从提高CPU与内存利用率的角度进行优化。依赖Codegen技术。

5.Codegen

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

1.Expression级别

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

2.WholeStage级别

传统的火山模型:SQL经过解析会生成一颗查询树,查询树的每个节点为Operator,火山模型把operator看成迭代器,每个迭代器提供一个next()接口。通过自顶向下的调用 next 接口,数据则自底向上的被拉取处理,火山模型的这种处理方式也称为拉取执行模型,每个Operator 只要关心自己的处理逻辑即可,耦合性低。

火山模型问题:数据以行为单位进行处理,不利于CPU cache 发挥作用;每处理一行需要调用多次next() 函数,而next()为虚函数调用。会有大量类型转换和虚函数调用。虚函数调用会导致CPU分支预测失败,从而导致严重的性能回退

Spark WholestageCodegen:为了消除这些overhead,会为物理计划生成类型确定的java代码。并进行即时编译和执行。

五、业界挑战与实践

1.Shuffle稳定性问题

  • Uber、LinkedIn、Alibaba等。

2.SQL执行性能问题

  • 向量化(vectorization)

  • 代码生成(Codegen:Code generation)

  • vectorization + Codegen

3.参数推荐/作业诊断

 - 自动化。


参考文章:juejin.cn/post/712390…