Spark 原理与实践笔记(一)| 青训营笔记
这是我参与「第四届青训营 -大数据场」笔记创作活动的第7天
一、大数据处理引擎Spark介绍
1. 大数据处理技术栈
- 应用:BI报表/ 实时大盘/ 广告/ 推荐
- 计算:Spark/ Flink/ Presto/ Impala/ ClickHouse...YARN/ K8s
- 存储:MetaStore/ Paequet/ ORC/ DeltaLake/ Hudi/ Iceberg/ HDFS/ Kafka/ HBase/ Kudu/ TOS/ S3...
- 数据:Volume/ Variety/ Velocity;On-Premise/ On-cloud;平台/ 管理/ 安全...
2. 开源大数据处理引擎
- Batch:Hive/ hadoop/ Spark
- streaming:Flink
- OLAP:Presto/ ClickHouse/ Impala/ DORIS
3. 生态和特点
- 统一引擎支持多种分布式场景
- 多语言支持 SQL/ Python/ Java/ Scala/ R
- 可读写丰富的数据源 内置 DataSource 及自定义 DataSource
- 丰富灵活的API/算子:SparkCore -> RDD/ SparkSQL -> DataFrame
- 支持K8S/ YARN/ Mesos资源调度
4. Spark运行架构
- Application(应用):Spark上运行的应用。Application中包含了一个驱动器(Driver)进程和集群上的多个执行器(Executor)进程
- Driver Program(驱动器):运行main()方法并创建SparkContext的进程
- Cluster Manager(集群管理器):用于在集群上申请资源的外部服务(如:独立部署的集群管理器、Mesos或者Yarn)
- Worker Node(工作节点):集群上运行应用程序代码的任意一个节点
- Executor(执行器):在集群工作节点上为某个应用启动的工作进程,该进程负责运行计算任务,并为应用程序存储数据
- Task(任务):执行器的工作单元
- Job(作业):一个并行计算作业,由一组任务(Task)组成,并由Spark的行动(Action)算子(如:save、collect)触发启动
- Stage(阶段):每个Job可以划分为更小的Task集合,每组任务被称为Stage
5. Spark工作原理
Spark应用在集群上运行时,包括了多个独立的进程,这些进程之间通过驱动程序(Driver Program)中的SparkContext对象进行协调,SparkContext对象能够与多种集群资源管理器(Cluster Manager)通信,一旦与集群资源管理器连接,Spark会为该应用在各个集群节点上申请执行器(Executor),用于执行计算任务和存储数据。Spark将应用程序代码发送给所申请到的执行器,SparkContext对象将分割出的任务(Task)发送给各个执行器去运行。
二、SparkCore原理解析
1. 什么是RDD
- RDD(Resilient Distributed Dataset) - (弹性分布式数据集)表示可以并行操作的不可变的、可分区、里面的元素可以并行计算的集合。
- 在Spark 中,对数据的所有操作不外乎创建RDD、转化已有RDD 以及调用RDD 操作进行求值。每个RDD 都被分为多个分区,这些分区运行在集群中的不同节点上。RDD 可以包含Python、Java、Scala 中任意类型的对象, 甚至可以包含用户自定义的对象。
- RDD 允许用户在执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。
- RDD 支持两种操作:transformation操作和action操作。RDD 的转化操作是返回一个新的RDD 的操作,比如map()和filter(),而action操作则是向驱动器程序返回结果或把结果写入外部系统的操作。比如count() 和first()。
- Spark 采用惰性计算模式,RDD 只有第一次在一个行动操作中用到时,才会真正计算。Spark 可以优化整个计算过程。默认情况下,Spark 的RDD 会在你每次对它们进行行动操作时重新计算。如果想在多个行动操作中重用同一个RDD , 可以使用RDD.persist() 让Spark 把这个RDD 缓存下来。
2. RDD的属性
-
一组分片(Partition),即数据集的基本组成单位。
- 对于RDD 来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD 时指定RDD 的分片个数,如果没有指定,那么就会采用默认值。默认值就是程序所分配到的CPU Core的数目。
-
一个计算每个分区的函数。
- Spark 中RDD 的计算是以分片为单位的,每个RDD都会实现compute 函数以达到这个目的。ompute 函数会对迭代器进行复合,不需要保存每次计算的结果。
-
RDD 之间的依赖关系。
- RDD 的每次转换都会生成一个新的RDD,所以RDD 之间就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark 可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD 的所有分区进行重新计算。
-
一个Partitioner, 即RDD 的分片函数(分区器)。
- 当前Spark 中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于于key-value 的RDD , 才会有Partitioner, 非key-value 的RDD 的Parititioner 的值是None。Partitioner 函数不但决定了RDD 本身的分片数量, 也决定了parent RDD Shuffle 输出时的分片数量。
-
一个列表, 存储存取每个Partition 的优先位置( preferred location)。
- 对于一个HDFS 文件来说,这个列表保存的就是每个Partition 所在的块的位置。按照“移动数据不如移动计算”的理念,Spark 在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。
3. RDD的执行流程
3.1 创建RDD
- 内置RDD:shuffleRDD/HadoopRDD...
- 自定义RDD
3.2 RDD 算子
- Transform算子:生成一个新的RDD
- Action算子:触发Job提交
3.3 RDD 依赖
描述父子RDD之间的依赖关系(lineage)
-
窄依赖:父RDD的每个partition至多对应一个子RDD的分区
- NarrowDependency
- OneToOneDependency
- RangeDependency:如果子RDD partition在父RDD range内
- Prune Dependency
-
宽依赖:可以对应多个子RDD分区
- shuffleDependency
3.4 执行流程
- Job: RDD action算子触发
- Stage:依据宽依赖划分
- Task:Stage内执行单个partition任务
4. 内存管理
-
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 扫描和回收,提升了处理性能。
5. Shuffle
创建shuffle manager
- SortShuffleManager:join两个比较大的表,对数据进行排序,进行spill产生多个spillFile临时文件,将其合并至一个shuffle dataFile,并创建一个indexFile
- external shuffle service:运行在主机上管理所有产生的shuffle数据,供后续reduceTask进行shuffle fetch