这是我参与「第四届青训营」笔记创作活动的第四天
一、大数据处理引擎Spark介绍
1.1 常见的大数据处理链路
数据采集——>数据存储——>数据处理——>数据应用
1.2 Spark介绍
Spark是类Hadoop MapReduce的通用并行框架,Spark,拥有Hadoop MapReduce所具有的优点;但不同于MapReduce的是Job中间输出结果可以保存在内存中,从而不再需要读写HDFS,因此Spark能更好地适用于数据挖掘与机器学习等需要迭代的MapReduce的算法。
1.3 Spark生态和特点
生态:
特点:
- 统一引擎,支持多种分布式场景
- 多语言支持
- 可读写丰富数据源
- 丰富灵活的API/算子
- 支持K8S/YRAN/Mesos资源调度
1.4 Spark运行架构和工作原理
当执行一个Application时,Driver会向集群管理器申请资源,启动Executor,并向Executor发送应用程序代码和文件,然后在Executor上执行Task,运行结束后,执行结果会返回给Driver,或者写到HDFS或者其它数据库中。
二、SparkCore
包含Spark的基本功能,包含任务调度,内存管理,容错机制等,内部定义了RDD(弹性分布式数据集),提供了很多API来创建和操作这些RDD。为其他组件提供底层的服务。
2.1 RDD介绍
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象。代码中是一个抽象类,它代表一个不可变、可分区、里面的元素可并行计算的集合。
RDD的五要素
- 一组分区(Partition),即数据集的基本组成单位;
- 一个计算每个分区的函数;
- RDD之间的依赖关系;
- 一个Partitioner,即RDD的分片函数;
- 一个列表,存储存取每个Partition的优先位置(preferred location)。
RDD 特点
- 分区:RDD逻辑上是分区的,每个分区的数据是抽象存在,计算的时候会通过一个compute函数得到每个分区的数据;
- 只读:RDD是只读的,如果要改变RDD中的数据,是只能在原有的RDD的基础上创建新的RDD;
- 操作算子有两类:
- transformations:用来将RDD进行转化,生成一个新的RDD
- action:触发RDD的计算,得到RDD的相关计算结果或者将RDD保存的文件系统中
- 依赖:描述父子RDD之间的依赖关系
- 窄依赖:父RDD的每个partition至多对应一个子RDD分区
- 宽依赖(会产生Shuffle):父RDD的每个partition都可能对应多个子RDD分区
- 缓存:如果在应用程序中多次使用同一个RDD,可以将该RDD缓存起来,该RDD只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该RDD的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用
- CheckPoint:RDD支持checkpoint将数据保存到持久化的存储中,这样就可以切断之前的血缘关系,因为checkpoint后的RDD不需要知道它的父RDDs了,它可以从checkpoint处拿到数据
RDD执行流程
Job:RDD actioon算子触发
Stage:依据宽依赖划分
Task:Stage内执行单个partition任务
2.2 内存管理
Spark采用统一内存管理机制,重点是其的动态占用机制。
- Executor内存主要有两类:Storage、Execution
- UnifiedMemoryManager统一管理Storage/Execution内存
- Storage和Execution内存使用是动态调整,可以相互借用
- 当Storage空闲,Execution可以借用Storage的内存使用,可以减少spill等操作,Execution使用的内存不能被Storage驱逐
- 当Execution空闲,Storage可以借用Execution的内存使用,当Execution需要内存时,可以驱逐被Storage借用的内存,直到spark.memory.storageFraction边界
三、SparkSQL
SparkSQL允许开发人员直接处理RDD,同时可以查询在Hive上存储的外部数据。SparkSQL的一个重要特点就是能够统一处理关系表和RDD,使得开发人员可以轻松的使用SQL命令进行外部查询,同时进行更加复杂的数据分析。
2.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性能两大技术:
- Optimizer:执行计划的优化,目标是找出最优的执行计划
- Runtime:运行时优化,目标是在既定的执行计划下尽可能快的执行完毕。
2.2 Catalyst优化
- Rule Based Optimizer(RBO): 基于规则优化,对语法树进行一次遍历,模式匹配能够满足特定规则的节点,再进行相应的等价转换。
- Cost Based Optimizer(CBO): 基于代价优化,根据优化规则对关系表达式进行转换,生成多个执行计划,然后CBO会通过根据统计信息(Statistics)和代价模型(Cost Model)计算各种可能执行计划的代价,从中选用COST最低的执行方案,作为实际运行方案。CBO依赖数据库对象的统计信息,统计信息的准确与否会影响CBO做出最优的选择。
2.3 AQE
AQE对于整体的Spark SQL的执行过程做了相应的调整和优化,它最大的亮点是可以根据已经完成的计划结点真实且精确的执行统计结果来不停的反馈并重新优化剩下的执行计划。 AQE框架三种优化场景:
- 动态合并shuffle分区(Dynamically coalescing shuffle partitions)
- 动态调整Join策略(Dynamically switching join strategies)
- 动态优化数据倾斜Join(Dynamically optimizing skew joins)
2.4 RuntimeFilter
实现在Catalyst中。动态获取Filter内容做相关优化,当我们将一张大表和一张小表等值连接时,我们可以从小表侧收集一些统计信息,并在执行join前将其用于大表的扫描,进行分区修剪或数据过滤。可以大大提高性能
Runtime优化分两类:
- 全局优化:从提升全局资源利用率、消除数据倾斜、降低IO等角度做优化。包括AQE。
- 局部优化:提高某个task的执行效率,主要从提高CPU与内存利用率的角度进行优化。依赖Codegen技术。
2.5 Codegen
从提高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的稳定性问题——数据远端存储
- SQL执行性能问题——向量法/Codegen/两者结合、C++/Java
- 参数推荐/作业判断——自动化