1. 概述
1.1 什么是spark
Spark 是一种基于内存的快速、通用、可扩展的大数据分析计算引擎。
1.2 Spark与Hadoop的对比
Spark | Hadoop | |
---|---|---|
Apache时间点 | 2013年 | 2008年 |
语言 | Scala | Java |
主要功能 | 数据计算 | 分布式计算+分布式存储 |
数据通信模式 | 内存 | 硬盘 |
1.3 Spark核心模块
- Spark Core 提供最基础最核心的功能,其他功能都是基于Spark Core进行拓展。
- Spark SQL是Spark用于操作结构化数据的组件。通过Spark SQL,用户可以使用SQL或者Apache Hive版本的HQL来查询数据
- Spark Streaming是Spark平台上针对实时数据进行流式计算的组件,提供丰富的处理流数据的API。
- Spark MLlib是一个机器学习算法库。MLlib不仅提供了模型评估、数据导入等额外的功能,还提供了一些更底层的机器学习原语。
- Spark GraphX是面向图计算的框架和算法库
2. Spark运行架构
2.1 运行架构
2.2核心组件
-
Driver
Spark Driver节点负责执行Spark程序中的main方法,负责实际代码的执行工作。
主要工作有:
- 将用户的程序转化为作业(Job)
- 在Executor之间调度任务(Task)
- 跟踪Executor的执行情况
- 通过UI展示查询运行情况
-
Executor
Spark Executor是集群中工作节点(Worker)中的一个JVM进程,负责在Spark作业中运行具体的任务(Task),任务之间彼此互相独立。Spark应用启动时,Executor节点被同时启动,并始终伴随着Spark的整个生命周期。如果Executor发生故障,Spark应用也可以继续执行,会将出错的Executor未完成的任务调度到其他Executor节点上继续运行。
主要功能有:
- 负责运行Spark应用的任务,并将结果返回给驱动器。
- 它们通过自身的块管理器(Block Manager)为用户程序中要求的RDD提供内存式存储。RDD是直接缓存在Executor进程内的,因此任务可以充分利用缓存数据加速运算。
Master & Worker
Spark集群的独立部署环境中,不需要依赖于其他的资源调度框架。其自身就实现了资源调度的功能。Master进程主要负责资源的调度和分配,并进行集群的监控,类似于YARN环境中的RM。Worker进程运行在集群中每台机器上,由Master分配资源对数据进行并行的处理和计算,类似于Yarn环境中的NM。
2.3 核心概念
-
Executor与Core
Spark Executor是集群中运行在工作节点(Worker)中的一个JVM进程,是整个集群中的专门用于计算的节点。在提交的应用中,可以提供参数指定计算节点的个数,以及对应的资源。这里的资源一般指分配的内存大小和虚拟CPU核(Core)数量。
-
并行度
分布式系统一般都是多个程序同时执行,这里的同时执行指的是并行而不是并发。整个集群并行执行的任务量可以称为并行度。
-
有向无环图
有向无环图是有点和线临时组成的拓扑图形,该图形具有方向,不会形成闭环。
2.4 任务提交流程
提交流程是指开发人员将Spark应用通过Spark客户端提交到集群执行的过程。
Spark应用提交到YARN环境中执行的时候,一般有两种部署模式——Client和Cluster,两种模式主要的区别在于Driver程序的运行节点的位置。
-
Client模式
Client模式中,用于监控和调度的Driver模块在客户端执行,而不是在YARN中。一般用于测试。
- Driver在任务提交的本地机器上运行
- 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上执行。
-
Cluster模式
- 任务提交后首先和ResourceManager申请启动ApplicationMaster
- ResourceManager分配container,在合适的NodeManager上启动ApplicationMaster,此时的ApplicationMaster就是Driver
- Driver启动后向ResourceManager申请Executor内存。
- ResourceManager接到Driver的资源申请后会分配container,然后Driver在资源分配指定的NodeManager上启动Executor进程。
- Executor进程启动以后会向Driver反向注册,Executor全部注册完成以后Driver开始执行main函数。
- 之后执行到action算子时,触发一个Job,并根据宽依赖开始划分stage,每个stage生成对应的TaskSet,之后将task分发到各个Executor上执行。
3.Spark核心编程
Spark三大数据结构
- RDD:弹性分布式数据集
- 累加器:分布式共享只写变量
- 广播变量:分布式共享只读变量
3.1 RDD
RDD(Resilient Distributed Dataset)弹性分布式数据集,是Spark中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变的、可分区、内部元素可并行计算的集合。
-
弹性:
- 存储的弹性,内存和磁盘的自动切换
- 容错的弹性:数据丢失可自动恢复
- 计算的弹性:计算出错重试机制
- 分片的弹性:可根据需要重新分片
-
分布式:数据存储在大数据集群不同节点上
-
数据集:RDD封装了计算逻辑,并不保存数据
-
数据抽象:RDD是一个抽象类,需要子类具体实现
-
不可变:RDD封装了计算逻辑,是不可以改变的。要想改变,只能生成新的RDD,在新的RDD里面封装计算逻辑。
-
可分区、并行计算
核心属性
- 分区列表:RDD数据结构中存在分区列表,用于并行计算
- 分区计算函数:Spark在计算的时候,是用分区函数对每一个分区进行计算
- RDD之间的依赖关系:RDD是计算模型的封装,当需求中需要将多个计算模型进行组合时,就需要将多个RDD建立依赖关系
- 分区器:当数据为KV类型时,可以通过设定的分区器进行自定义数据的分区
- 首选位置:计算数据时,可以根据计算节点的状态选择不同的节点位置进行计算
执行原理
从计算角度来看,数据处理过程中需要计算资源(内存和CPU)和计算模型(逻辑)。执行时,需要将计算资源和计算模型进行协调和整合。
Spark框架在执行时,先申请资源,然后将应用程序的处理处理逻辑分解成一个一个的计算任务,然后将计算任务发到已经分配资源的计算节点上,按照指定的计算模型进行数据计算,最后返回计算结果给驱动进程。
RDD是Spark框架中用于数据处理的核心模型,Yarn环境中,RDD的工作原理
基础编程
-
RDD的创建
- 从集合创建RDD:parallelize()和makeRDD()
- 从外部存储创建RDD:textFile()
- 从已有RDD生成新的RDD
- 直接使用new的方式构造RDD
-
RDD并行度与分区
-
默认情况下,Spark可以将一个作业切分为多个任务以后,发送给Executor节点并行计算,而能够并行计算的任务数量我们称之为并行度。这个数量可以在创建RDD的时候指定。
-
读取内存数据时,数据可以按照并行度进行分区操作。
def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = { (0 until numSlices).iterator.map { i => val start = ((i * length) / numSlices).toInt val end = (((i + 1) * length) / numSlices).toInt (start, end) }
-
读取文件数据时,数据是按照Hadoop文件读取的规则进行切片分区,而切片规则和数据读取的规则有些差异,源码如下
public InputSplit[] getSplits(JobConf job, int numSplits) throws IOException { long totalSize = 0; // compute total size for (FileStatus file: files) { // check we have valid files if (file.isDirectory()) { throw new IOException("Not a file: "+ file.getPath()); } totalSize += file.getLen(); } long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits); long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input. FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize); ... for (FileStatus file: files) { ... if (isSplitable(fs, path)) { long blockSize = file.getBlockSize(); long splitSize = computeSplitSize(goalSize, minSize, blockSize); ... } protected long computeSplitSize(long goalSize, long minSize, long blockSize) { return Math.max(minSize, Math.min(goalSize, blockSize)); }
-
-
RDD转换算子
RDD根据数据的处理方式不同,将算子整体上分成Value类型,双Value类型和Key-Value类型
-
Value类型
-
map:将数据逐条的进行转换,这里可以是值的转换或者是类型的转换。转换前后RDD的数据量保持不变。
-
mapPartitions: 将待处理的数据以分区为单位发送到计算节点进行处理。
map和mapPartition的区别在于,map中处理单位是RDD的每一条记录,而mapPartition中处理单位是每个数据分区的迭代器。
map操作不会减少和增加RDD的数据数量,而mapPartition操作返回的是一个迭代器,并没有对数据量进行限制。
mapPartition可能会导致内存溢出,而map不会。
mapPartition操作类似于批处理,性能比map更高,但是会长时间占用内存。
3)mapPartitionWithIndex:将数据以分区为单位发送到计算节点进行处理,处理的时候可以获取到当前分区的索引。
4)flatMap:将处理的数据进行扁平化后再进行映射处理,也成为扁平映射。
5)glom:将同一个分区的元素合并到一个数组里去,合并后每个分区是一个独立的数组。
6)groupBy:将数据按照指定的规则进行分组,默认分区不变。但是数据会被打乱重新组合(shuffle)
7)filter:将数据根据指定的规则进行过滤,符合规则的数据保留,不符合规则的数据丢弃。当数据进行筛选后,分区不变,但是不同分区中数据量可能不同,出现数据倾斜。
8)sample:从指定数据集抽取数据:伯努利算法(抽取不放回)和泊松算法(抽取后放回)
9)distinct:数据去重。
10)coalesce:根据数据量缩减分区,当spark程序中存在很多小任务时,可以使用该方法合并分区,减少分区数量和调度成本。
11)repartition:该操作内部执行的是coalesce操作,shuffle默认为true。该操作可以将分区由少变多或者由多变少。因为无论如何都会执行shuffle过程。
12)sortBy:通过指定处理函数后,对函数处理后的结果进行排序。排序前后分区数量保持不变,中间存在shuffle过程。
13)intersection:对源RDD和参数RDD取交集后返回新的RDD
14)union:对源RDD和参数RDD取并集后返回新的RDD
15)subtract:以一个RDD为主,去掉两个RDD重复的元素,即求两个RDD的差集。
16)zip:将两个RDD以键值对的形式合并,第一个RDD的值作为Key,第二个RDD的值作为Value。
17)partitionBy:将数据按照指定的Partitioner重新进行分区。Spark默认的分区器是HashPartitioner。
18)reduceByKey:可以按照相同的key,对value进行聚合
19)groupByKey: 对源数据按照key进行分组,存在Shuffle过程。与reduceByKey的差异在于,reduceByKey会在本地对相同key进行combine,相比groupByKey的落盘数据量会少很多。
20)aggregateByKey :将数据根据不同规则进行分区内计算和分区间计算
21)foldByKey :当aggregateByKey 中分区内计算规则和分区间计算规则相同时,可以简化为foldByKey。
22)combineByKey : 最通用的key-value 型 rdd 进行聚集操作的聚集函数 ,允许用户输入类型和返回值类型不一致。
23)sortByKey:按照Key进行排序,该Key必须实现sorted接口
24)join: 在类型为(K,V)和(K,W)的 RDD 上调用,返回一个相同 key 对应的所有元素连接在一起的(K,(V,W))的 RDD
- leftOuterJoin : 类似于SQL的左外连接,返回结果类似(K(V,[W]))
26)cogroup:在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD
-
-
-
RDD行动算子
- Reduce:聚集RDD中所有的元素,先聚合分区内的数据,再聚合分区间的数据
- collect:以Array的形式返回数据集中的所有元素
- count:返回RDD中元素的个数
- first:返回RDD中第一个元素
- take:返回一个由RDD的前N个元素组成的数组
- takeOrdered:返回该RDD排序后的前N个元素组成的数组
- aggregate:分区的数据通过初始值和分区内的数据进行聚合,然后再和初始值进行分区间的数据聚合。
- fold:aggregate的简化版操作
- countByKey :统计每种key的数量
- save 相关算子 :保存数据到文件
- foreach: 遍历RDD
RDD序列化
对于Spark应用来说,算子内部的代码在Executor上执行,算子以外的代码在Driver上面执行。算子以内的代码需要用到算子以内的数据时,就会形成闭包效果。如果算子以外的数据无法序列化,也就意味着无法传递给Executor端执行,从而导致错误。所以在执行任务之前,需要进行闭包检测。
RDD依赖关系
-
RDD血缘关系
RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD丢失部分分区数据时,可以根据这些信息重新计算和恢复丢失的数据分区。
-
RDD依赖关系:指的是相邻RDD之间的依赖关系
-
RDD窄依赖:窄依赖表示每一个父RDD的Partition最多被子RDD的一个Partition使用。
-
RDD宽依赖:一个父RDD的Partition会被多个子RDD的Partition依赖,会引起shuffle。
-
RDD阶段划分:DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向,不会闭环。例如, DAG 记录了 RDD 的转换过程和任务的阶段。
-
RDD任务划分,RDD任务切分中间分为:Application、Job、Stage、Task
- Application:初始化一个SparkContext即生成一个Application
- Job:一个Action算子就会生成一个Job
- Stage:Stage等于宽依赖(ShuffleDependency)的个数+1;
- Task: 一个stage阶段中,最后一个RDD的分区个数就是Task的个数。
RDD持久化
-
RDD Cache缓存
RDD 通过 Cache 或者 Persist 方法将前面的计算结果缓存,默认情况下会把数据以缓存在 JVM 的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的 action 算子时,该 RDD 将会被缓存在计算节点的内存中,并供后面重用。
缓存有可能丢失,或者存储于内存的数据由于内存不足而被删除, RDD 的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。
通过基于 RDD 的一系列转换,丢失的数据会被重算,由于 RDD 的各个 Partition 是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部 Partition。
Spark 会自动对一些 Shuffle 操作的中间数据做持久化操作(比如: reduceByKey)。这样做的目的是为了当一个节点 Shuffle 失败了避免重新计算整个输入。但是,在实际使用的时候,如果想重用数据,仍然建议调用 persist 或 cache。 -
RDD CheckPoint 检查点
所谓的检查点其实就是通过将 RDD 中间结果写入磁盘。由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点之后有节点出现问题, 可以从检查点开始重做血缘,减少了开销。
对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action 操作才能触发。 -
缓存和检查点的区别
- Cache 缓存只是将数据保存起来,不切断血缘依赖。 Checkpoint 检查点切断血缘依赖。
- Cache 缓存的数据通常存储在磁盘、内存等地方,可靠性低。 Checkpoint 的数据通常存
储在 HDFS 等容错、高可用的文件系统,可靠性高。 - 建议对 checkpoint()的 RDD 使用 Cache 缓存,这样 checkpoint 的 job 只需从 Cache 缓存
中读取数据即可,否则需要再从头计算一次 RDD。
RDD分区器
Spark 目前支持 Hash 分区和 Range 分区,和用户自定义分区。 Hash 分区为当前的默认分区。分区器直接决定了 RDD 中分区的个数、 RDD 中每条数据经过 Shuffle 后进入哪个分区,进而决定了 Reduce 的个数。
只有 Key-Value 类型的 RDD 才有分区器,非 Key-Value 类型的 RDD 分区的值是 None
每个 RDD 的分区 ID 范围: 0 ~ (numPartitions - 1),决定这个值是属于那个分区的。
**Hash分区:**对每个Key,计算其HashCode,对分区数进行取余得到分区号。
**Range分区:**将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而且分区间有序。
RDD文件的读取与保存
Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。
文件格式分为: text 文件、 csv 文件、 sequence 文件以及 Object 文件;
**文件系统分为:**本地文件系统、 HDFS、 HBASE 以及数据库。
-
Text文件
// 读取输入文件 val inputRDD: RDD[String] = sc.textFile("input/1.txt") // 保存数据 inputRDD.saveAsTextFile("output")
-
sequence文件
SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文件(FlatFile)。
在 SparkContext 中,可以调用 sequenceFilekeyClass, valueClass。
// 保存数据为 SequenceFile dataRDD.saveAsSequenceFile("output") // 读取 SequenceFile 文件 sc.sequenceFile[Int,Int]("output").collect().foreach(println)
-
object 对象文件
对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制。
可以通过 objectFileT:ClassTag函数接收一个路径, 读取对象文件, 返回对应的 RDD, 也可以通过调用saveAsObjectFile()实现对对象文件的输出。因为是序列化所以要指定类型。
// 保存数据 dataRDD.saveAsObjectFile("output") // 读取数据 sc.objectFile[Int]("output").collect().foreach(println)
3.2 累加器
**原理:**累加器用来把 Executor 端变量信息聚合到 Driver 端。在 Driver 程序中定义的变量,在Executor 端的每个 Task 都会得到这个变量的一份新的副本,每个 task 更新这些副本的值后,传回 Driver 端进行 merge。
系统累加器
val rdd = sc.makeRDD(List(1,2,3,4,5))
// 声明累加器
var sum = sc.longAccumulator("sum");
rdd.foreach(
num => {
// 使用累加器
sum.add(num)
}
)
// 获取累加器的值
println("sum = " + sum.value)
用户自定义累加器
// 自定义累加器
// 1. 继承 AccumulatorV2,并设定泛型
// 2. 重写累加器的抽象方法
class WordCountAccumulator extends AccumulatorV2[String, mutable.Map[String,
Long]]{
var map : mutable.Map[String, Long] = mutable.Map()
// 累加器是否为初始状态
override def isZero: Boolean = {
map.isEmpty
}
// 复制累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Long]] = {
new WordCountAccumulator
}
// 重置累加器
override def reset(): Unit = {
map.clear()
}
// 向累加器中增加数据 (In)
override def add(word: String): Unit = {
// 查询 map 中是否存在相同的单词
// 如果有相同的单词,那么单词的数量加 1
// 如果没有相同的单词,那么在 map 中增加这个单词
map(word) = map.getOrElse(word, 0L) + 1L
}
// 合并累加器
override def merge(other: AccumulatorV2[String, mutable.Map[String, Long]]):
Unit = {
val map1 = map
val map2 = other.value
// 两个 Map 的合并
map = map1.foldLeft(map2)(
( innerMap, kv ) => {
innerMap(kv._1) = innerMap.getOrElse(kv._1, 0L) + kv._2
innerMap
}
)
}
// 返回累加器的结果 (Out)
override def value: mutable.Map[String, Long] = map
}
3.3 广播变量
广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个 Spark 操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,广播变量用起来都很顺手。在多个并行操作中使用同一个变量,但是 Spark 会为每个任务分别发送。
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: Broadcast[List[(String, Int)]] = sc.broadcast(list)
val resultRDD: RDD[(String, (Int, Int))] = rdd1.map {
case (key, num) => {
var num2 = 0
// 使用广播变量
for ((k, v) <- broadcast.value) {
if (k == key) {
num2 = v
}
}
(key, (num, num2))
}
}