这是我参与 「第四届青训营 」 笔记创作活动的第5天
一、Spark 概述
1.1 什么是Spark
1、定义:
Spark是一种基于内存的快速、通用、可扩展的大数据分析引擎。
2、历史
2009年诞生于加州大学伯克利分校AMPLab,项目采用Scala编写。
2010年开源;
2013年6月成为Apache孵化项目
2014年2月成为Apache顶级项目。
1.2 spark内置模块:
Spark Core:实现了 Spark 的基本功能,包含任务调度、内存管理、错误恢复、与存储系统交互等模块。Spark Core 中还包含了对弹性分布式数据集(Resilient Distributed DataSet,简称 RDD)的 API 定义。
Spark SQL:是 Spark 用来操作结构化数据的程序包。通过 Spark SQL,我们可以使用SQL 或者 Apache Hive 版本的 SQL 方言(HQL)来查询数据。Spark SQL 支持多种数据源,比如 Hive 表、Parquet 以及 JSON 等。
Spark Streaming:是 Spark 提供的对实时数据进行流式计算的组件。提供了用来操作数据流的 API,并且与 Spark Core 中的 RDD API 高度对应。
Spark MLlib:提供常见的机器学习(ML)功能的程序库。包括分类、回归、聚类、协同过滤等,还提供了模型评估、数据 导入等额外的支持功能。
集群管理器:Spark 设计为可以高效地在一个计算节点到数千个计算节点之间伸缩计算。为了实现这样的要求,同时获得最大灵活性,Spark 支持在各种集群管理器(Cluster Manager)上运行,包括 Hadoop YARN、Apache Mesos,以及 Spark 自带的一个简易调度器,叫作独立调度器。
Spark 得到了众多大数据公司的支持,这些公司包括 Hortonworks、IBM、Intel、Cloudera、MapR、Pivotal、百度、阿里、腾讯、京东、携程、优酷土豆。当前百度的Spark 已应用于大搜索、直达号、百度大数据等业务;阿里利用 GraphX 构建了大规模的图计算和图挖掘系统,实现了很多生产系统的推荐算法;腾讯 Spark 集群达到 8000 台的规模,是当前已知的世界上最大的 Spark 集群。
1.3 spark 的特点
1)快:与Hadoop的MapReduce相比,Spark基于内存的运算要快100倍以上,基于硬盘的运算也要快10倍以
上。Spark实现了高效的DAG执行引擎,可以通过基于内存来高效处理数据流。计算的中间结果是存在于内存中
的。
2)易用:Spark支持Java、Python和Scala的API,还支持超过80种高级算法,使用户可以快速构建不同的应用。而且Spark支持交互式的Python和Scala的Shell,可以非常方便地在这些Shell中使用Spark集群来验证解决问题的方法。
3)通用:Spark提供了统一的解决方案。Spark可以用于批处理、交互式查询(Spark SQL)、实时流处理(Spark Streaming)、机器学习(Spark MLlib)和图计算(GraphX)。这些不同类型的处理都可以在同一个应
用中无缝使用。减少了开发和维护的人力成本和部署平台的物力成本。
4)兼容性:Spark可以非常方便地与其他的开源产品进行融合。比如,Spark可以使用Hadoop的YARN和Apache Mesos作为它的资源管理和调度器,并且可以处理所有Hadoop支持的数据,包括HDFS、HBase等。这对于已经部署Hadoop集群的用户特别重要,因为不需要做任何数据迁移就可以使用Spark的强大处理能力。
二、spark 运行模式
2.1 Local 模式
概述
Local模式就是运行在一台计算机上的模式,通常就是用于在本机上练手和测试。它可以通过以下集中方式设置Master。
local: 所有计算都运行在一个线程当中,没有任何并行计算,通常我们在本机执行一些测试代码,或者练手,就用这种模式;
local[K] : 指定使用几个线程来运行计算,比如local[4]就是运行4个Worker线程。通常我们的Cpu有几个Core,就指定几个线程,最大化利用Cpu的计算能力;
local[*] : 这种模式直接帮你按照Cpu最多Cores来设置线程数了
2.2 提交流程
- 提交任务解析:
重要角色:
Driver(驱动器)
Spark 的驱动器是执行开发程序中的 main 方法的进程。它负责开发人员编写的用来创建 SparkContext、创建 RDD,以及进行 RDD 的转化操作和行动操作代码的执行。如果你是用 spark shell,那么当你启动 Spark shell 的时候,系统后台自启了一个 Spark 驱动器程序,就是在 Spark shell 中预加载的一个叫作 sc 的 SparkContext 对象。如果驱动器程序终止,那么 Spark 应用也就结束了。主要负责:
1)把用户程序转为任务
2)跟踪 Executor 的运行状况
3)为执行器节点调度任务
4)UI 展示应用运行状况
Executor(执行器)
Spark Executor 是一个工作进程,负责在 Spark 作业中运行任务,任务间相互独立。Spark 应用启动时,Executor 节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有 Executor 节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出
错节点上的任务调度到其他 Executor 节点上继续运行。主要负责:
1)负责运行组成 Spark 应用的任务,并将结果返回给驱动器进程;
2)通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD 是直接缓存在 Executor 进程内的,因此任务可以在运行时充分利用缓存数据加速运算。
2.3 数据流程
textFile("input") :读取本地文件 input 文件夹数据;
flatMap(_.split(" ")) :压平操作,按照空格分割符将一行数据映射成一个个单词;
map((_,1)) :对每一个元素操作,将单词映射为元组;
reduceByKey( + ) :按照 key 将值进行聚合,相加;
collect:将数据收集到 Driver 端展示。
2.4 Standalone 模式
概述
构建一个由 Master+Slave 构成的 Spark 集群,Spark 运行在集群中。
三、RDD 概述
3.1 什么是 RDD
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据抽象。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。
3.2 RDD 的属性
- 一组分区(Partition),即数据集的基本组成单位;
- 一个计算每个分区的函数;
- RDD 之间的依赖关系;
- 一个 Partitioner,即 RDD 的分片函数;
- 一个列表,存储存取每个 Partition 的优先位置(preferred location)
3.3 RDD 特点
RDD 表示只读的分区的数据集,对 RDD 进行改动,只能通过 RDD 的转换操作,由一个 RDD 得到一个新的 RDD,新的 RDD 包含了从其他 RDD 衍生所必需的信息。RDDs 之间存在依赖,RDD 的执行是按照血缘关系延时计算的。如果血缘关系较长,可以通过持久化RDD 来切断血缘关系。
3.4.1弹性
存储的弹性:内存与磁盘的自动切换;
容错的弹性:数据丢失可以自动恢复;
计算的弹性:计算出错重试机制
分片的弹性:可根据需要重新分片。
3.4.2 分区
RDD 逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个 compute函数得到每个分区的数据。如果 RDD 是通过已有的文件系统构建,则 compute 函数是读取指定文件系统中的数据,如果 RDD 是通过其他 RDD 转换而来,则 compute 函数是执行转换逻辑将其他 RDD 的数据进行转换。
3.4.3 只读
RDD 是只读的,要想改变 RDD 中的数据,只能在现有的 RDD 基础上创建新的 RDD。
由一个 RDD 转换到另一个 RDD,可以通过丰富的操作算子实现,不再像 MapReduce那样只能写 map 和 reduce 了。
RDD 的操作算子包括两类,一类叫做 transformations,它是用来将 RDD 进行转化,构建 RDD 的血缘关系;另一类叫做 actions,它是用来触发 RDD 的计算,得到 RDD 的相关计算结果或者将 RDD 保存的文件系统中。下图是 RDD 所支持的操作算子列表。
3.4.4 依赖
RDDs 通过操作算子进行转换,转换得到的新 RDD 包含了从其他 RDDs 衍生所必需的信息,RDDs 之间维护着这种血缘关系,也称之为依赖。如下图所示,依赖包括两种,一种是窄依赖,RDDs 之间分区是一一对应的,另一种是宽依赖,下游 RDD 的每个分区与上游RDD(也称之为父 RDD)的每个分区都有关,是多对多的关系。
3.4.5 缓存
如果在应用程序中多次使用同一个 RDD,可以将该 RDD 缓存起来,该 RDD 只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该 RDD 的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用。如下图所示,RDD- 1 经过一系列的转换后得到 RDD-n 并保存到 hdfs,RDD-1 在这一过程中会有个中间结果,如果将其缓存到内存,那么在随后的 RDD-1 转换到 RDD-m 这一过程中,就不会计算其之前的 RDD-0 了。
3.4.6 CheckPoint
虽然 RDD 的血缘关系天然地可以实现容错,当 RDD 的某个分区数据失败或丢失,可以通过血缘关系重建。但是对于长时间迭代型应用来说,随着迭代的进行,RDDs 之间的血缘关系会越来越长,一旦在后续迭代过程中出错,则需要通过非常长的血缘关系去重建,势必影响性能。为此,RDD 支持 checkpoint 将数据保存到持久化的存储中,这样就可以切断之前的血缘关系,因为 checkpoint后的RDD 不需要知道它的父RDDs 了,它可以从 checkpoint处拿到数据。
四、
SparkCore
RDD(Resilient Distributed Dataset):弹性分布式数据集,是一个容错的、并行的数据结构
RDD算子:对任何函数进行某一项操作都可以认为是一个算子,RDD算子是RDD的成员函数
Transform(转换)算子: 根据已有RDD创建新的RDD
Action(动作)算子: 将在数据集上运行计算后的数值返回到驱动程序,从而触发真正的计算
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中数据重分发的一种机制。
目标:认识spark的核心概念RDD,RDD两种算子处理过程,理解RDD依赖,学习RDD在Spark中的执行流程。了解spark中调度、内存管理机制、shuffle机制。
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提供。
内存管理
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 扫描和回收,提升了处理性能。
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 从列表中按照一定的策略选取最优的物理计划
目标:了解SQL执行链路。重点学习核心模块calalyst优化器以及SparkSQL三大重点特性(Codegen/AQE/RuntimeFilter)
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:运行时优化,目标是在既定的执行计划下尽可能快的执行完毕。
Catalyst优化
- Rule Based Optimizer(RBO): 基于规则优化,对语法树进行一次遍历,模式匹配能够满足特定规则的节点,再进行相应的等价转换。
- 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优化分两类:
- 全局优化:从提升全局资源利用率、消除数据倾斜、降低IO等角度做优化。包括AQE。
- 局部优化:提高某个task的执行效率,主要从提高CPU与内存利用率的角度进行优化。依赖Codegen技术。
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内部算子间的界限,拼出来跟原来的逻辑保持一致的裸的代码(通常是一个大循环)然后把拼成的代码编译成可执行文件。
五、业界挑战与实践
向量化(vectorization):将循环转换为向量操作的编译器优化
代码生成(Codegen:Code generation):生成程序代码的技术或系统,可以在运行时环境中独立于生成器系统使用