1. 介绍
Spark用于大规模数据处理的统一分析引擎,基于内存计算,提高了在大数据环境下数据处理的实时性,同时保证了高容错性和高可伸缩性,允许用户将Spark部署在大量硬件之上,形成集群。
主要分为:
- Spark core:Spark 最基础与最核心的功能
- Spark Sql:Spark 用来操作结构化数据的组件
- Spark streaming:Spark 平台上针对实时数据进行流式计算的组件
- Spark Graphx:Spark 面向图计算提供的框架与算法库
- Spark MLlib:Spark 提供的一个机器学习算法库
2. 运行架构
Spark 框架的核心是一个计算引擎,整体来说,它采用了标准master-slave的结构
1. 核心组件:
-
Driver: 驱动器节点,执行Spark任务中的main方法,负责实际代码的执行工作,;
- 用户程序转化为作业job
- 在executor之调度任务并跟踪执行情况
-
Executor:工作节点(Worker)中的JVM进程,负责Spark作业中运行具体任务(Task),任务间独立
- 负责运行Spark应用任务,并结果返回给Driver驱动器
- 为RDD提供缓存需求,直接缓存在Executor的进程内
-
Master & Worker
Spark独立部署的两个核心组件
- Master:一个进程,负责资源的调度和分配,并进行集群的监控等职责(类似Yarn中的RM)
- Worker:运行在集群中的服务器上,由Master分配资源对数据进行并行处理和计算,类似Yarn中的NM
-
ApplicationMaster
Yarn集群应用程序中的组件,用于向资源调度器申请执行任务的资源容器Container,运行用户自己的程序任务job,监控整个任务的状态、处理任务失败等异常情况。
2. 核心概念
资源参数
- --num-executors 配置Executor 的数量;
- --executor-memory 配置每个Executor 的内存大小;
- --executor-cores 配置每个Executor 的虚拟CPU core 数量;
并行度
整个集群并行执行任务的数量。
有向无环图(DAG)
是由点和线组成的拓扑图形,该图形具有方向,不会闭环。
3. 提交流程
以Yarn集群为例
- Driver启动后,会向ResourceManager 通讯申请启动ApplicationMaster
- 随后ResourceManager 分配container,在合适的NodeManager 上启动ApplicationMaster,负责向ResourceManager 申请Executor 内存;
- ResourceManager 接到ApplicationMaster 的资源申请后会分配container,然后 ApplicationMaster 在资源分配指定的NodeManager 上启动Executor 进程;
- Executor 进程启动后会向Driver 反向注册,Executor 全部注册完成后Driver 开始执行 main 函数;
- 之后执行到Action 算子时,触发一个Job,并根据宽依赖开始划分stage,每个stage 生成对应的TaskSet,之后将task 分发到各个Executor 上执行。
注意:
- Yarn Client :在客户端上执行,不在Yarn集群中,所以一般用于测试;
- Yarn Cluster :在Yarn 集群资源中执行,一般用以实际生产环境执行;
5.Spark Core
5.1 RDD
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark 中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。
- 弹性:(1)存储上:内存与磁盘的自动切换;(2)容错:数据丢失可以依赖血缘自动恢复;(3)计算:计算出错重试机制;(4)分片:可根据需要重新分片;
- 分布式: 数据存储在大数据集群不同节点上;
- 数据集: RDD 封装了计算逻辑,并不保存数据;
- 数据抽象:RDD 是一个抽象类,需要子类具体实现;
- 不可变:RDD 封装了计算逻辑,是不可以改变的,想要改变,只能产生新的RDD,在 新的RDD 里面封装计算逻辑;
- 可分区、并行计算
5.2 核心属性:
* Internally, each RDD is characterized by five main properties:
* - A list of partitions
// 分区的列表,由partition组成
* - A function for computing each split/partition
// 函数作用于RDD的每个分区
* - A list of dependencies on other RDDs
// 依赖关系
* - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
// 可选项:key-value的一个partition默认是使用hash分区的
* - Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
// 可选:每一分片的优先计算位置,比如HDFS的block的所在位置应该是优先计算的位置. 移动数据不如移动计算
-
分区列表: RDD中存在分区列表,用于执行任务时 并行计算
-
分区计算函数:当数据为KV 类型数据时,可以通过设定分区器自定义数据的分区
-
最佳位置:根据计算节点的状态选择不同的节点位置进行计算
这三个属性其实说的就是数据集在哪,在哪计算更合适,如何分区
-
计算函数:Spark执行计算时,使用分区函数对每一个分区进行计算
-
依赖关系:RDD 是计算模型的封装,当需求中需要将多个计算模型进行组合时,就需要将多个RDD 建立依赖关系
这两个属性其实说的是数据集怎么来的
5.3 RDD创建
-
从集合中创建,parallelize 和makeRDD
# parallelize val rdd1 = sc.parallelize(1 to 10,3) # makeRDD val rdd2 = sc.makeRDD(1 to 10,3) -
从外部存储(文件)创建RDD
sparkContext.textFile("input") -
运算转换
5.4 并行度与分区
并行度(paralleism):在分布式计算框架中,一般都是多个任务同时执行,由于任务分布在不同的计算节点进行计算,所以能够真正实现多个任务并行执行,记住,这里是并行,而不是并发,这里我们将整个集群并行执行任务的数量,成为并行度。
spark中的并行度和分区之间是有关系的,rdd的每一个分区都是一个task,然后传送到对应的executor中进行计算。如果资源充足(executor core数=task数)并行度就等于分区数,如果(executor core数<task数)就是并发执行。
官方推荐: task数量,设置成spark Application 总cpu core数量的2到3倍 ,比如150个cpu core ,基本设置 task数量为 300~ 500
总结: spark根据分区数来决定task的个数,而task的个数和executor所拥有的core数来决定着spark的并行度,当task数多余core数时,就会产生并发操作。
5.5 RDD依赖
-
血缘
RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区
// RDD血缘关系 scala> rdd2.toDebugString res3: String = (2) ShuffledRDD[4] at reduceByKey at <console>:25 [] +-(2) MapPartitionsRDD[3] at map at <console>:24 [] | MapPartitionsRDD[2] at flatMap at <console>:24 [] | README.md MapPartitionsRDD[1] at textFile at <console>:24 [] | README.md HadoopRDD[0] at textFile at <console>:24 [] // RDD依赖类型 // 窄依赖(narrow dependency) scala> rdd1.dependencies res4: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.OneToOneDependency@37efd738) // 宽依赖(wide dependency) scala> rdd2.dependencies res5: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.ShuffleDependency@4b37b02a) -
窄依赖:
1个父RDD分区对应1个子RDD的分区;- 1个子RDD的分区对应于1个父RDD的分区,比如:map,filter,union等算子,
子RDD每个分区的生成与父RDD的数据规模无关 - 1个子RDD的分区对应于N个父RDD的分区,比如co-partioned join,
子RDD每个分区的生成与父RDD的数据规模相关
- 1个子RDD的分区对应于1个父RDD的分区,比如:map,filter,union等算子,
-
宽依赖:
**1个父RDD分区对应多个子RDD分区**- 1个父RDD对应非全部多个子RDD分区,比如groupByKey,reduceByKey,sortByKey
- 1个父RDD对应所有子RDD分区,比如未经协同划分的join
5.6 任务划分
RDD任务切分中间分为:Application、Job、Stage和Task;
- Application:初始化一个SparkContext即生成一个Application
- Job:一个Action算子就会生成一个Job
- Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage
- Task:Stage是一个TaskSet,将Stage划分的结果发送到不同的Executor执行即为一个Task
Application -> Job-> Stage-> Task 每一层都是 1对n 的关系
5.7 持久化
持久化使用方法:
- persist(): 参数,尽量选择_SER结尾的级别,表示经过java序列化操作
- cache() ,调用的是persist(MEMORY_ONLY)
数据将会在第一次 action 操作时进行计算,并缓存在节点的内存中
注意:在python中,存储的对象都是通过Pickle库序列化了的,所以是否选择序列化等级并不重要
删除缓存:RDD.unpersist()
5.8 检查点
检查点是将RDD中间结果写入磁盘,调用setCheckpointDir设置检查点目录。
-
背景:由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点之后有节点出现问题,可以从检查点开始重做血缘,减少了开销;
-
存储:Checkpoint的数据通常是
存储在HDFS等容错、高可用的文件系统 -
存储格式:二进制的文件
-
触发:不会立即执行,等待action操作才会触发,但是检查点为了数据安全,会从血缘关系的最开始执行一遍
-
使用方法:
- SparkContext的setCheckPointDIR()方法,设置高可用文件系统、 HDFS
- 然后对RDD调用checkpoint()方法
5.9 缓存与检查点的区别
- 血缘依赖:Cache 缓存不切断,Checkpoint 检查点切断;
- 存储:Cache 缓存通常存储在内存、磁盘,可靠性低 (节点的故障会导致磁盘、内存的数据丢失); Checkpoint 的数据通常存储在 HDFS 等容错、高可用的文件系统,可靠性高。
- 使用建议:建议对checkpoint()的RDD使用Cache缓存,这样checkpoint的job只需从Cache缓存中读取数据即可,否则需要再从头计算一次RDD。
5.10 共享变量
一般情况下,当一个传递给Spark操作(例如map和reduce)的函数在远程节点上面运行时,Spark操作实际上操作的是这个函数所用变量的一个独立副本。这些变量被复制到每台机器上,并且这些变量在远程机器上 的所有更新都不会传递回驱动程序。通常跨任务的读写变量是低效的,但是,Spark还是为两种常见的使用模式提供了两种有限的共享变量:广播变量(broadcast variable)和累加器(accumulator)。
-
广播变量
-
释义:广播变量允许程序员缓存一个只读的变量在每台机器上面,而不是每个任务保存一份拷贝
-
使用方法:SparkContext.broadcast(v),v是需要广播的变量,且广播之后不能被修改
// 找出key相等的值 val rdd1 = sc.makeRDD(List( ("a",1), ("b", 2), ("c", 3), ("d", 4) ),4) val list = List( ("a",4), ("b", 5), ("c", 6), ("d", 7) ) // 声明广播变量 val broadcast = sc.broadcast(list) val resultRDD = rdd1.map { case (key, num) => { var num2 = 0 for ((k, v) <- broadcast.value) { if (k == key) num2 = v } (key, (num, num2)) } } resultRDD.collect res6: Array[(String, (Int, Int))] = Array((a,(1,4)), (b,(2,5)), (c,(3,6)), (d,(4,7)))
-
-
累加器
-
释义:支持在所有不同节点之间进行累加计算,可自定义
-
使用方法:SparkContext.accumulator(v),v:初始变量
scala> val rdd1 = sc.makeRDD(List(1,2,3,4,5,6)) scala> var sum = sc.longAccumulator("sum") scala> rdd1.foreach(num => sum.add(num)) scala> println("sum = " + sum.value) sum = 21
-
5.11 疑惑
1.MR的shuflle与spark的shulle的区别?
- MR的shuffle:
- Map端的shuffle,主要是Partition、Collector、Sort、Spill、Merge几个阶段 Reduce端的shuffle,主要是Copy、Merge、Reduce几个阶段
存在全局排序(reduce task 是根据key去处理数据的),合并排序。先进行小范围排序,最后再大范围排序。最后的复杂度为O(nlog(n)),比普通排序复杂度O(n的平方)快
-
Spark的shuffle
MapReduce Shuffle基础上进行的调优,主要针对 排序、合并逻辑做了一些优化 , Spark中Shuffle write相当于MapReduce 的map,Shuffle read相当于MapReduce 的reduce。负责shuffle过程的主要是ShuffleManager,主要有 HashShuffleManager和SortShuffleManager 。
- HashShuffle:分为普通机制和合并机制 :(1)普通机制:会产生MR(Map个数Reduce个数)个据量小文件,产生大量性能低下的IO操作,易发生OOM;(2)合并机制:复用buffer,同分区文件合并,文件数量下降到coreR(core个数Reduce个数据)依然会产生大量小文件
- SortShuffle:分普通机制和bypass机制: 普通机制在内存数据结构(默认为5M)完成排序,会产生2M个磁盘小文件 。 而当shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值。或者算子不是聚合类的shuffle算子(比如reduceByKey)的时候会触发SortShuffle的bypass机制,SortShuffle的bypass机制不会进行排序,极大的提高了其性能。
-
异同点
- 功能上 : 差别不大, 都是对Map端的数据进行分区,要么聚合排序,要么不聚合排序,然后Reduce端或者下一个调度阶段进行拉取数据,完成map端到reduce端的数据传输功能
- 方案上: MR的shuffle是基于合并排序的思想,在数据进入reduce端之前,都会进行sort,为了方便后续的reduce端的全局排序 , 而Spark的shuffle是可选择的聚合,特别是1.2之后,需要通过调用特定的算子才会触发排序聚合的功能。
- 流程 : MR的Map端和Reduce区分非常明显,两块涉及到操作也是各司其职, park的RDD是内存级的数据转换,不落盘,所以没有明确的划分,只是区分不同的调度阶段,不同的算子模型。
- 数据拉取 : MR的reduce是直接拉去Map端的分区数据,而Spark是根据索引读取,而且是在action触发的时候才会拉去数据 。
2 Spark与Hadoop差异
Spark是在借鉴了MapReduce之上发展而来的,继承了其分布式并行计算的优点并改进了MapReduce明显的缺陷 。
- Spark把中间数据放到内存中,DAG图的分布式并行计算的编程框架,减少了迭代过程中数据的落地 ,迭代运算效率高
- Spark容错性高: Spark引进了弹性分布式数据集RDD, 如果数据集一部分丢失,则可以根据血缘进行重建,也可以通过CheckPoint来实现容错。
- 通用: 包含了Spark Core、Spark SQL、Spark Streaming、MLLib和GraphX等组件 。此外算子丰富,分为转化和动作算子,表达能力强。