这是我参与[第四届青训营]笔记创作的第5天。
流式(Streaming)典型框架Flink我们已经学完了,现在开始批式(Batch)典型框架Spark(新的布置Shuffle)。
一、大数据处理引擎Spark介绍
1、大数据处理技术栈
- 存储
(1)第二行代表的是存储格式,其中Parquet、ORC是高效能列式存储,DeltaLake、Hudi、Iceberg是优化存储的键。
(2)存储第三行代表分布式存储,其中Kudu是基于列式的分布式存储,TOS是字节提供的、S3是亚马逊提供的对象存储,Kafka是分布式消息存储,Hbase是基于<key,value>存储的nosql引擎的分布式消息存储。
- 计算
(1)PreSto、impala、clickHouse处理在线分布式查询场景,可交互的OLAP引擎。
(2)YARN、K8S资源管理器。
2、常见的大数据处理链路
- 其中数据处理包括数据写入与读出,可以是Kalfa、HDFS等的数据系统,也可以是MySQL等的数据库。
3、开源大数据处理引擎
- Hadoop解决大规模数据计算问题。
- Spark基于内存的,是MapReduce的优化,可以离线处理数据。
目前流行
4、Spark
(1)什么是spark
- 官网:Apache Spark是一个多元语言的用于大规模数据分析的统一引擎,用于数据工程、数据科学、机器学习在单一机器或集群上。
(2)Spark 版本演进
目标:简便、快速、可扩展、统一
- 2.0新特性:更好引入了SQL构建于Spark搜索引擎、性能代码优化。
- 3.0新特性:自身查询、动态剪、CPU计算加速调度。
- 3.3新特性:过滤器提高性能、自身查询性能升高。
(3)Spark生态&特点
Spark生态组件:
(1)Spark Core:Spark核心组件,它实现了Spark的基本功能,包含任务调度、内存管理、错误恢复、与存储系统交互等模块。
(2)Spark SQL:用来操作结构化数据的核心组件,通过Spark SQL可以直接查询Hive、HBase等多种外部数据源中的数据。
(3)Spark Structured Streaming:Spark提供的流式计算框架,支持高吞吐量、可容错处理的实时流式数据处理。
(4) MLlib:Spark提供的关于机器学习功能的算法程序库,包括分类、回归、聚类、协同过滤算法等,还提供了模型评估、数据导入等额外的功能。
(5) GraphX:Spark提供的分布式图处理框架,拥有对图计算和图挖掘算法的API接口以及丰富的功能和运算符。
(6)独立调度器、Yarn、Mesos、Kubernetes:Spark框架可以高效地在一个到数千个节点之间伸缩计算,集群管理器则主要负责各个节点的资源管理工作,为了实现这样的要求,同时获得最大灵活性,Spark支持在各种集群管理器(Cluster Manager)上运行。下面有介绍
- 丰富数据源
- 丰富灵活的API/算子
- 运行架构&部署方式
Spark应用在集群上运行时,包括了多个独立的进程,这些进程之间通过驱动程序(Driver Program)中的SparkContext对象进行协调,SparkContext对象能够与多种集群资源管理器(Cluster Manager)通信,一旦与集群资源管理器连接,Spark会为该应用在各个集群节点上申请执行器(Executor),用于执行计算任务和存储数据。Spark将应用程序代码发送给所申请到的执行器,SparkContext对象将分割出的任务(Task)发送给各个执行器去运行。
(1)Application(应用):Spark上运行的应用。Application中包含了一个驱动器(Driver)进程和集群上的多个执行器(Executor)进程。
(2) Driver Program(驱动器):运行main()方法并创建SparkContext的进程。
(3) Cluster Manager(集群管理器):用于在集群上申请资源的外部服务(如:独立部署的集群管理器、Mesos或者Yarn)。Spark目前支持几个集群管理器:(1) Standalone :Spark 附带的简单集群管理器,可以轻松设置集群。(2) Apache Mesos:通用集群管理器,也可以运行 Hadoop MapReduce 和服务应用程序。(已弃用)(3) Hadoop YARN: Hadoop 2 和 3 中的资源管理器。(4)Kubernetes:用于自动部署、扩展和管理容器化应用程序的开源系统。
(4) Worker Node(工作节点):集群上运行应用程序代码的任意一个节点。
(5) Executor(执行器):在集群工作节点上为某个应用启动的工作进程,该进程负责运行计算任务,并为应用程序存储数据。
(6) Task(任务):执行器的工作单元。
(7) Job(作业):一个并行计算作业,由一组任务(Task)组成,并由Spark的行动(Action)算子(如:save、collect)触发启动。
(8) Stage(阶段):每个Job可以划分为更小的Task集合,每组任务被称为Stage。
需要注意的是:
-
每个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节点比较近的机器上。
(4)Spark提交命令
二、SparkCore原理解析
-
DAG(Directed Acyclic Graph): 有向无环图,Spark中的RDD通过一系列的转换算子操作和行动算子操作形成了一个DAG。
-
DAGScheduler:将作业的DAG划分成不同的Stage,每个Stage都是TaskSet任务集合,并以TaskSet为单位提交给TaskScheduler。
-
TaskScheduler:通过TaskSetManager管理Task,并通过集群中的资源管理器(Standalone模式下是Master,Yarn模式下是ResourceManager)把Task发给集群中Worker的Executor。
-
Shuffle:Spark中数据重分发的一种机制。
(1)RDD弹性分布式数据集,是一个容错的、可以并行的数据结构。同时是Spark输入输出所有数据结构,是Spark的基本单元,
- 五要素
- 如何创建RDD?
- RDD算子
对任何函数进行某一项操作都可以认为是一个算子,RDD算子RDD的成员函数。
(1)Transform(转换)算子:根据已有的RDD创建新的RDD。
(2)Action(动作)算子:将在数据集在运行计算后的数值返回到驱动程序,从而触发真正的计算。
- RDD依赖
数据丢失可以根据依赖关系找到
宽依赖--ShuffleDependency
- 执行流程
(1)划分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提供。
(2)Scheduler
(3)Memory Management(内存管理)
spark作为一个基于内存的分布式计算引擎,Spark采用统一内存管理机制。重点在于动态占用机制。
(1)设定基本的存储内存(Storage)和执行内存(Execution)区域,该设定确定了双方各自拥有的空间的范围。
-
双方的空间都不足时,则存储到硬盘;若己方空间不足而对方空余时,可借用对方的空间
-
当Storage空闲,Execution可以借用Storage的内存使用,可以减少spill等操作, Execution内存不能被Storage驱逐。Execution内存的空间被Storage内存占用后,可让对方将占用的部分转存到硬盘,然后"归还"借用的空间
-
当Execution空闲,Storage可以借用Execution内存使用,当Execution需要内存时,可以驱逐被Storage借用的内存,可让对方将占用的部分转存到硬盘,然后"归还"借用的空间
(2)UnifiedMemoryManager统一管理多个并发的TAsk的内存分配。每个TAsk获取的内存区间为1/(2*N)~1/N,N为Executor中正在并发运行的Task数量。
(3)user memory存储用户自定义的数据结构或者spark内部元数据.
(4)Reserverd memory:预留内存,防止OOM,
(5)堆内(On-Heap)内存/堆外(Off-Heap)内存:Executor 内运行的并发任务共享 JVM 堆内内存。为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 可以直接操作系统堆外内存,存储经过序列化的二进制数据。减少不必要的内存开销,以及频繁的 GC 扫描和回收,提升了处理性能。
(4)Shuffle
每个MapTask生成一个Shuffle数据文件和一个index文件。其中dataFile中的数据按照partition id进行排序,同一个id的数据聚集在一起。indexFile保存了所有partition id在datafile中的位置信息,方便后续ReduceTask能Fetch到对应的分区中的数据。
三、SparkSQL原理解析
-
DataFrame: 是一种以RDD为基础的分布式数据集, 被称为SchemaRDD。
-
Catalyst:SparkSQL核心模块,主要是对执行过程中的执行计划进行处理和优化。
-
DataSource:SparkSQL支持通过 DataFrame 接口对各种数据源进行操作。
-
Adaptive Query Execution:自适应查询执行。
-
Runtime Filter:运行时过滤。
-
Codegen:生成程序代码的技术或系统,可以在运行时环境中独立于生成器系统使用。
-
Unresolved Logical Plan:未解析的逻辑计划,仅仅是数据结构,不包含任何数据信息。
-
Logical Plan:解析后的逻辑计划,节点中绑定了各种优化信息。
-
Optimized Logical Plan:优化后的逻辑计划。
-
Selected Physical Plan: 从列表中按照一定的策略选取最优的物理计划。
SparkSQL执行过程:
-
SQL Parse
SQL语句解析: 将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性能两大技术:
- Optimizer:执行计划的优化,目标是找出最优的执行计划。
- Runtime:运行时优化,目标是在既定的执行计划下尽可能快的执行完毕。
(1)Catalyst 优化器
像Parsed Logical Plan、Analyzed Logical Plan、Optimized Logical Plan、Physical Plan
谓词下推,简化查询,提高性能
- Rule Based Optimizer(RBO): 基于规则优化,对语法树进行一次遍历,模式匹配能够满足特定规则的节点,再进行相应的等价转换。
- Cost Based Optimizer(CBO): 基于代价优化,根据优化规则对关系表达式进行转换,生成多个执行计划,然后CBO会通过根据统计信息(Statistics)和代价模型(Cost Model)计算各种可能执行计划的代价,从中选用COST最低的执行方案,作为实际运行方案。CBO依赖数据库对象的统计信息,统计信息的准确与否会影响CBO做出最优的选择。
(2)Adaptive Query Execution(AQE)自适应查询
AQE对于整体的Spark SQL的执行过程做了相应的调整和优化,它最大的亮点是可以根据已经完成的计划结点真实且精确的执行统计结果来不停的反馈并重新优化剩下的执行计划。
AQE框架三种优化场景:
- 动态合并shuffle分区(Dynamically coalescing shuffle partitions)
- 动态调整Join策略(Dynamically switching join strategies)
- 动态优化数据倾斜Join(Dynamically optimizing skew joins)
(1) AQE-Coalescing Shuffle Partitions分区合并
- 每个Stage实际处理数据不一样,导致性能不一样--分区参数对某个Stage过大,则可能单个分区大小比较小,而且Task个数会比较多,shuffle Fetch阶段产生大量的小块的随机读,影响性能。分区过小,则可能单个分区比较大,会产生更多的Spill或者OOM。
- 作业运行过程中,可以将多个相邻的较小的分区进行动态合并,由一个Task读取进行处理。
(2)AQE-Switching JOin Strategies
(3)AQE-OPtiminzing Skew Joins
(3)Runtime Filter
实现在Catalyst中。动态获取Filter内容做相关优化,当我们将一张大表和一张小表等值连接时,我们可以从小表侧收集一些统计信息,并在执行join前将其用于大表的扫描,进行分区修剪或数据过滤。可以大大提高性能
Runtime优化分两类:
- 全局优化:从提升全局资源利用率、消除数据倾斜、降低IO等角度做优化。包括AQE。
- 局部优化:提高某个task的执行效率,主要从提高CPU与内存利用率的角度进行优化。依赖Codegen技术。
- Bloom Runtime Filter
(4)Codegen-Expression
将表达式中大量的虚函数调用函数用压平到一个函数内部,类似手写代码。 从提高cpu的利用率的角度来进行runtime优化。
- Expression级别
表达式常规递归求值语法树。需要做很多类型匹配、虚函数调用、对象创建等额外逻辑,这些overhead远超对表达式求值本身,为了消除这些overhead,Spark Codegen直接拼成求值表达式的java代码并进行即时编译
- WholeStage级别
传统的火山模型:SQL经过解析会生成一颗查询树,查询树的每个节点为Operator,火山模型把operator看成迭代器,每个迭代器提供一个next()接口。通过自顶向下的调用 next 接口,数据则自底向上的被拉取处理,火山模型的这种处理方式也称为拉取执行模型,每个Operator 只要关心自己的处理逻辑即可,耦合性低。
- 火山模型问题:数据以行为单位进行处理,不利于CPU cache 发挥作用;每处理一行需要调用多次next() 函数,而next()为虚函数调用。会有大量类型转换和虚函数调用。虚函数调用会导致CPU分支预测失败,从而导致严重的性能回退
Spark WholestageCodegen:为了消除这些overhead,会为物理计划生成类型确定的java代码。并进行即时编译和执行。
Codegen打破了Stage内部算子间的界限,拼出来跟原来的逻辑保持一致的裸的代码(通常是一个大循环)然后把拼成的代码编译成可执行文件。
四、业界挑战与实践
Shuffle稳定性问题:大规模作业下,开源的ExternalShuffleService(ESS)的实现机制容易带来大量随机读导致的硬盘IOPS瓶颈、Fetch请求积压等问题,进而导致运算过程中经常出现Stage重算甚至是作业失败,继而引起资源使用的恶行循环,严重影响SLA。
解决方案:
五、课后自测
- 大数据的基础链路是?
- Spark RDD是如何执行的?如何划分stage?
- Spark内存是如何管理的?有什么特别机制?
- Spark Shuffle是怎么产生的?基本流程是?
- SparkSQL执行流程有哪些步骤?每个步骤的左右是?
- Runtime优化的方式都有哪些?
- Spark业界面临的问题是如何产生的?都有什么优化方向?