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

138 阅读14分钟

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

05 Spark 原理与实践

今天的学习内容为Spark的原理与实践

本节课第一部分介绍了Spark基本概念,Spark是一个支持各种各样的一个分布式的场景。支持多语言,可以读写丰富的数据源,还有支持各种各样的资源调度的统一分析引擎,采用了分布式计算中的Master-Slave模型。Driver负责管理整个集群中的作业任务调度。Executor 则负责实际执行任务。既可以部署本地也可以部署于集群中。

第二部分介绍了Spark的核心组件SparkCore ,RDD是是一个容错的可以并行执行的一个分布式数据集;RDD的宽依赖(n:m)、窄依赖(1:n);RDD的执行过程;内存管理,存储内存与执行内存的动态调整,Shuffla机制(下节内容主要讲)

第三部分介绍了SparkSQL的执行流程,在每一个阶段讲了一些它的优化方案。

第四部分主要我了解了一下现在业界的一些挑战,然后还有一些解决方案,提供一些解决问题思考解决问题的思路


1 Spark介绍

1.1 什么是Spark?

Spark是用于大规模数据处理的统一分析引擎

Spark 是一个统一引擎,它支持各种各样的一个分布式的场景。支持多语言,可以读写丰富的数据源,还有支持各种各样的资源调度。

1.2 Spark生态圈

  • Spark Core:内核
  • Spark SQL:用于处理结构化数据的组件,类似Hive
  • Spark Streaming:用于处理流式数据的组件,类似Storm
  • Spark MLLib:机器学习
  • Spark Graphx: 图计算

1.3 Spark体系结构

Spark架构采用了分布式计算中的Master-Slave模型。Master是对应集群中的含有Master进程的节点,Slave是集群中含有Worker进程的节点。图形中的Driver表示master,负责管理整个集群中的作业任务调度。图形中的Executor 则是 slave,负责实际执行任务。

1

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

需要注意的是

  • 每个Spark application都有其对应的多个executor进程。Executor进程在整个应用程序生命周期内,都保持运行状态,并以多线程方式执行任务。这样做的好处是,Executor进程可以隔离每个Spark应用。从调度角度来看,每个driver可以独立调度本应用程序的内部任务。从executor角度来看,不同Spark应用对应的任务将会在不同的JVM中运行。然而这样的架构也有缺点,多个Spark应用程序之间无法共享数据,除非把数据写到外部存储结构中。

  • Spark对底层的集群管理器一无所知,只要Spark能够申请到executor进程,能与之通信即可。这种实现方式可以使Spark比较容易的在多种集群管理器上运行,例如Mesos、Yarn、Kubernetes。

  • Driver Program在整个生命周期内必须监听并接受其对应的各个executor的连接请求,因此driver program必须能够被所有worker节点访问到。

  • 因为集群上的任务是由driver来调度的,driver应该和worker节点距离近一些,最好在同一个本地局域网中,如果需要远程对集群发起请求,最好还是在driver节点上启动RPC服务响应这些远程请求,同时把driver本身放在离集群Worker节点比较近的机器上。


2 SparkCore:

2.1 RDD基本概念

RDD(RESILIENT Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象,代表一个不可变、可分区、里面元素可并行计算的集合。

  • Dataset:一个数据集合,用于存放数据的
  • Distributed:RDD中的数据是分布式存储的,可用于分布式计算
  • Resilient:RDD中的数据可以存储在内存中或者磁盘中
  • 所有的运算以及操作都建立在RDD数据结构的基础之上
  • 可以认为RDD是分布式的列表List或数组Array,抽象的数据结构,RDD是一个抽象类Abstract Class和泛型Generic Type

RDD提供了一个抽象的数据模型,不比担心底层数据的分布式特性,只需将具体的应用逻辑表达为一系列转换操作(函数),不同RDD之间的转换操作之间还可以形成依赖关系,进而实现管道化,从而避免了中间结果的存储,大大降低了数据复制、磁盘IO和序列化开销,并且还提供了更多的API

2.2 RDD依赖

窄依赖:父RDD的每个partition至多对应一个子RDD分区

  • NarrowDependency
  • PruneDependency
  • RangeDependency
  • OneToOneDependency

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

  • ShuffleDependency

2.3 RDD执行过程

img

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

当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提供。

2.4 内存管理

img

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

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

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

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

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

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

Reserverd memory:预留内存,防止OOM,

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

2.5 suffle 机制

Spark程序中的Shuffle操作是通过shuffleManage对象进行管理。Spark目前支持的ShuffleMange模式主要有两种:HashShuffleMagnage 和SortShuffleManage Shuffle操作包含当前阶段的Shuffle Write(存盘)和下一阶段的Shuffle Read(fetch),两种模式的主要差异是在Shuffle Write阶段,下面将着重介绍。

  • 1、HashShuffleMagnage

HashShuffleMagnage是Spark1.2之前版本的默认模式,在集群中的每个executor上,其具体流程如下图所示: img

从图中可知,在executor中处理每个task后的结果均会通过buffler缓存的方式写入到多个磁盘文件中,其中文件的个数由shuffle算子的numPartition参数指定(图中partition为3)。因此Shuffle Write 阶段会产生大量的磁盘文件,整个Shuffle Write 阶段的文件总数为: Write阶段的task数目* Read阶段的task数目。 由于HashShuffleManage方式会产生很多的磁盘文件,Spark对其进行了优化,具体优化点为: (1)executor处理多个task的时候只会根据Read阶段的task数目(设为m)生成对应的文件数,具体做法是:处理第一个task时生成m个文件,后续task的结果追加到对应的m个文件中。 (2)考虑到executor的并行计算能力(core数量),处理任务的每个core均会生成m个文件。 因此,优化后的HashShuffleManage最终的总文件数:Write阶段的core数量* Read阶段的task数目

  • 2、SortShuffleManage

SortShuffleManage是Spark1.2及以上版本默认的ShuffleManage模式,具体包含普通模式和bypass模式。 1、普通模式 在集群中的每个executor上,其普通模式的具体流程如下图所示: img

从图中可知,SortShuffleManage在数据写入磁盘文件前有两个重要操作: (1)数据聚合,针对可聚合的shuffle操作(比如reduceBykey()),会基于key值进行数据的聚合操作,以此减少数据量。 (2)数据聚合之后会对数据进行排序操作。 (问题:基于key排序?排序的目的是什么?), 最后对每个task生成的文件进行合并,通过索引文件标注key值在文件中的位置。 因此,SortShuffleManage产生的总文件数为:Writer 阶段的task数*2 2、bypass模式 bypass模式与HashShuffleMagnage基本一致,只是Shuffle Write 阶段在最后有一个文件合并的过程,最终输出的文件个数为:Writer阶段的task数目*2 spark.shuffle.sort.bypassMergeThreshold默认值为200,即Read阶段的task数目小于等于该阈值时以及Write端是非聚合操作(比如join),会启用bypass模式,其他情况下采用普通机制。

3 SparkSQL

目标:了解SQL执行链路。重点学习核心模块calalyst优化器以及SparkSQL三大重点特性(Codegen/AQE/RuntimeFilter)

img

3.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字节码

影响SparkSQL性能两大技术:

  1. Optimizer:执行计划的优化,目标是找出最优的执行计划
  2. Runtime:运行时优化,目标是在既定的执行计划下尽可能快的执行完毕。

3.2 Catalyst优化

本部分着重讲解了RBO和CBO的两种策略,在第一节课详细讲过

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

Cost Based Optimizer(CBO): 基于代价优化,根据优化规则对关系表达式进行转换,生成多个执行计划,然后CBO会通过根据统计信息(Statistics)和代价模型(Cost Model)计算各种可能执行计划的代价,从中选用COST最低的执行方案,作为实际运行方案。CBO依赖数据库对象的统计信息,统计信息的准确与否会影响CBO做出最优的选择。

3.3 AQE

本部分为自适应查询优化

AQE对于整体的Spark SQL的执行过程做了相应的调整和优化,它最大的亮点是可以根据已经完成的计划结点真实且精确的执行统计结果来不停的反馈并重新优化剩下的执行计划。

AQE框架三种优化场景:

  • 动态合并shuffle分区(Dynamically coalescing shuffle partitions)

  • 动态调整Join策略(Dynamically switching join strategies)

  • 动态优化数据倾斜Join(Dynamically optimizing skew joins)

3.4 RuntimeFilter

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

Runtime优化分两类:

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

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

3.5 Codegen

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

  1. Expression级别

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

  1. WholeStage级别

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

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

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

Codegen打破了Stage内部算子间的界限,拼出来跟原来的逻辑保持一致的裸的代码(通常是一个大循环)然后把拼成的代码编译成可执行文件。

4 业界挑战与实践

目标:了解三种业内Spark面临的问题及解决策略,学习思考问题的产生、寻找解决问题思路。

4.1 Shuffle稳定性问题

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

解决策略:

1、数据远端存储

在 map 或 reduce 端,在 Python 的数据尽可能在 shuffle 的时候就进行聚合

4.2 SQL执行性能问题

提升 Spark 搜索性能的有两个方向,第一个是找一个最好的执行计划。第二个针对一个选定的一个 plan 的执行计划的话,我们怎么把它跑得更快?目前解决问题主要是聚焦于解决当下的一个硬件瓶颈。

CPU流水线/分支预测/乱序执行/SIMD/CPU缓存友好/...

优化 CPU 的一个两条技术注技术路线一条是向量化(火山模型的拉取模式)另一个就是Codegen(压缩)

参考连接:juejin.cn/post/712390…