【笔记四】spark原理与实践|青训营笔记

275 阅读9分钟

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

01.大数据处理引擎spark介绍

批式计算引擎 spark 基于内存

Spark运行架构和工作原理

  • 几个关键字

    • Application:应用程序,当前文件中的所有的代码就是一个Application
    • Driver:任务提交之后第一个启动的进程。执行主方法,初始化SparkContext
    • ClusterManager:集群管理者,负责对整个集群的资源调度,根据不同的部署模式确定集群管理者
    • WorkerNode:工作节点。具体执行任务的物理节点
    • Executor:执行任务的进程
  • 每个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节点比较近的机器上。

spark特点:

多语言支持,丰富的API/算子,丰富的数据源

多种资源调度 master和slave

02.sparkcore原理解析

1.png

RDD

  • 五大特性

    • A list of partitions
    • -分区的列表,每个节点有部分分区,所有节点的所有分区形成了RDD
    • A function for computing each split
    • -一个函数作用于每一个切片,对RDD整体定义计算规则,RDD中所有分区的每一条数据都必须按此规则执行
    • A list of dependencies on other RDDs
    • -对于其他RDD的依赖的列表,RDD之间存在依赖关系
    • Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
    • -可选的,对于KV键值对RDD可以通过设定分区器自定义数据的分区
    • Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS ?le)
    • -可选的,可以根据计算节点的状态选择不同的节点位置进行计算,优先选择本地(数据的本地性)

RDD执行过程

划分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提供。

job:RDD action 算子触发

stage:依据宽依赖划分

task:stage内执行单个partition任务

算子

map filter count cache//缓存 persist//缓存 。。。

创建RDD

内置RDD

shuffleRDD/HadoopRDD/JDBCRDD /KafkaRDD/UnionRDD/MapPatitionsRDD/。。。

自定义RDD

class CustomRDD(...)extends RDD{}

实现五要素对应的函数

RDD算子

两类算子:

Transform 算子:生成一个新的RDD

Action算子:触发 Job提交

RDD依赖

窄依赖(narrowDependency)

父级RDD里的每一个partition对应子级RDD里的唯一一个partition的关系

narrowDependency PruneDependency RangeDependency OneToOneDependency

宽依赖(shuffleDependency)

 父级RDD里的每一个partition对应子级RDD里的多个partition的关系
 只要执行了宽依赖,必然执行shuffle操作,一定避免不了磁盘IO
 如果可能的话,尽量不使用或少使用宽依赖

shuffleDependency

调度器

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

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

内存管理

Executor内存主要有两类: Storage、ExecutionUnifiedMemoryManager统一管理 Storage/Execution内存 Storage和 Execution内存使用是动态调整,可以相互借用

当Storage空闲,Execution可以借用 Storage的内存使用,可以减少spill等操作,Execution使用的内存不能被Storage驱逐

当Execution空闲,Storage可以借用Execution的内存使用,>当Execution 需要内存时,可以驱逐被Storage借用的内存,直到spark.memory.storageFraction边界

shuffle机制

每个MapTask生成一个Shuffle数据文件和一个index文件dataFile 中的数据按照partitionld进行排序, 同一个partitionld的数据聚集在起 indexFile保存了所有paritionld在dataFlle中的位置信息,方便后续ReduceTask 能Fetch 到对应 partitionld的数据

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

03.sparkSQL原理解析

DataFrame: 是一种以RDD为基础的分布式数据集, 被称为SchemaRDD

Catalyst:SparkSQL核心模块,主要是对执行过程中的执行计划进行处理和优化

DataSource:SparkSQL支持通过 DataFrame 接口对各种数据源进行操作。

Adaptive Query Execution:自适应查询执行

Runtime Filter:运行时过滤

Codegen:生成程序代码的技术或系统,可以在运行时环境中独立于生成器系统使用

SparkSql执行过程

  • Unresolved Logical Plan:未解析的逻辑计划,仅仅是数据结构,不包含任何数据信息。
  • Logical Plan:解析后的逻辑计划,节点中绑定了各种优化信息。
  • Optimized Logical Plan:优化后的逻辑计划
  • Physical Plans:物理计划列表
  • Selected Physical Plan 从列表中按照一定的策略选取最优的物理计划

Catalyst优化

  1. Rule Based Optimizer(RBO): 基于规则优化,对语法树进行一次遍历,模式匹配能够满足特定规则的节点,再进行相应的等价转换。
  1. Cost Based Optimizer(CBO): 基于代价优化,根据优化规则对关系表达式进行转换,生成多个执行计划,然后CBO会通过根据统计信息(Statistics)和代价模型(Cost Model)计算各种可能执行计划的代价,从中选用COST最低的执行方案,作为实际运行方案。CBO依赖数据库对象的统计信息,统计信息的准确与否会影响CBO做出最优的选择。

AQE

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

AQE框架三种优化场景:

  • 动态合并shuffle分区(Dynamically coalescing shuffle partitions)
  • 动态调整Join策略(Dynamically switching join strategies)
  • 动态优化数据倾斜Join(Dynamically optimizing skew joins)

RuntimeFilter

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

Runtime优化分两类:

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

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内部算子间的界限,拼出来跟原来的逻辑保持一致的裸的代码(通常是一个大循环)然后把拼成的代码编译成可执行文件。

04.业界挑战与实践

shuffle稳定性问题

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

SQL执行性能问题

压榨CPU资源

CPU流水线/分支预测/乱序执行/SIMD/CPU缓存友好/ ... Vectorized / Codegen ? C++ / Java ?

参数推荐/作业诊断

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

----->>

自动参数推荐/作业诊断

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