Spark为什么会流行?
原因1:
优秀的数据模型和丰富计算抽象
Spark 产生之前,已经有MapReduce这类非常成熟的计算系统存在了,并提供了高层次的API(map/reduce),把计算运行在集群中并提供容错能力,从而实现分布式计算。
虽然MapReduce提供了对数据访问和计算的抽象,但是对于数据的复用就是简单的将中间数据写到一个稳定的文件系统中(例如HDFS),所以会产生数据的复制备份,磁盘的I/O以及数据的序列化,所以在遇到需要在多个计算之间复用中间结果的操作时效率就会非常的低。而这类操作是非常常见的,例如迭代式计算,交互式数据挖掘,图计算等。
认识到这个问题后,学术界的 AMPLab 提出了一个新的模型,叫做 RDD。
RDD 是一个可以容错且并行的数据结构
(其实可以理解成分布式的集合,操作起来和操作本地集合一样简单),它可以让用户显式的将中间结果数据集保存在内存中,并且通过控制数据集的分区来达到数据存放处理最优化。同时 RDD也提供了丰富的 API (map、reduce、filter、foreach、redeceByKey...)来操作数据集。后来RDD被 AMPLab 在一个叫做 Spark 的框架中提供并开源。
简而言之,Spark 借鉴了 MapReduce 思想发展而来,保留了其分布式并行计算的优点并改进了其明显的缺陷。让中间数据存储在内存中提高了运行速度、并提供丰富的操作数据的API提高了开发速度。
原因2:
完善的生态圈-fullstack
目前,Spark已经发展成为一个包含多个子项目的集合,其中包含SparkSQL、Spark Streaming、GraphX、MLlib等子项目。
Spark Core
:实现了 Spark 的基本功能,包含RDD、任务调度、内存管理、错误恢复、与存储系统交互等模块。
Spark SQL
:Spark 用来操作结构化数据的程序包。通过 Spark SQL,我们可以使用 SQL操作数据。
Spark Streaming
:Spark 提供的对实时数据进行流式计算的组件。提供了用来操作数据流的 API。
Spark MLlib
:提供常见的机器学习(ML)功能的程序库。包括分类、回归、聚类、协同过滤等,还提供了模型评估、数据导入等额外的支持功能。
GraphX(图计算)
:Spark中用于图计算的API,性能良好,拥有丰富的功能和运算符,能在海量数据上自如地运行复杂的图算法。
集群管理器
:Spark 设计为可以高效地在一个计算节点到数千个计算节点之间伸缩计算。
StructuredStreaming
:处理结构化流,统一了离线和实时的API。
Spark VS Hadoop
Hadoop
Spark
类型
基础平台, 包含计算, 存储, 调度
分布式计算工具
场景
大规模数据集上的批处理
迭代计算, 交互式计算, 流计算
价格
对机器要求低, 便宜
对内存有要求, 相对较贵
编程范式
Map+Reduce, API 较为底层, 算法适应性差
RDD组成DAG有向无环图, API 较为顶层, 方便使用
数据存储结构
MapReduce中间计算结果存在HDFS磁盘上, 延迟大
RDD中间运算结果存在内存中 , 延迟小
运行方式
Task以进程方式维护, 任务启动慢
Task以线程方式维护, 任务启动快
❣️
注意:
尽管Spark相对于Hadoop而言具有较大优势,但Spark并不能完全替代Hadoop,Spark主要用于替代Hadoop中的MapReduce计算模型。存储依然可以使用HDFS,但是中间结果可以存放在内存中;调度可以使用Spark内置的,也可以使用更成熟的调度系统YARN等。
实际上,Spark已经很好地融入了Hadoop生态圈,并成为其中的重要一员,它可以借助于YARN实现资源调度管理,借助于HDFS实现分布式存储。
此外,Hadoop可以使用廉价的、异构的机器来做分布式存储与计算,但是,Spark对硬件的要求稍高一些,对内存与CPU有一定的要求。
Spark Core
一、RDD详解
1. 为什么要有RDD?
在许多迭代式算法(比如机器学习、图算法等)和交互式数据挖掘中,不同计算阶段之间会重用中间结果,即一个阶段的输出结果会作为下一个阶段的输入。但是,之前的MapReduce框架采用非循环式的数据流模型,把中间结果写入到HDFS中,带来了大量的数据复制、磁盘IO和序列化开销。且这些框架只能支持一些特定的计算模式(map/reduce),并没有提供一种通用的数据抽象。
AMP实验室发表的一篇关于RDD的论文:《Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing》就是为了解决这些问题的。
RDD提供了一个抽象的数据模型,让我们不必担心底层数据的分布式特性,只需将具体的应用逻辑表达为一系列转换操作(函数),不同RDD之间的转换操作之间还可以形成依赖关系,进而实现管道化,从而避免了中间结果的存储,大大降低了数据复制、磁盘IO和序列化开销,并且还提供了更多的API(map/reduec/filter/groupBy...)。
2. RDD是什么?
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象,代表一个不可变、可分区、里面的元素可并行计算的集合。单词拆解:
Resilient :它是弹性的,RDD里面的中的数据可以保存在内存中或者磁盘里面
Distributed :它里面的元素是分布式存储的,可以用于分布式计算
3. RDD主要属性
进入RDD的源码中看下:
RDD源码
在源码中可以看到有对RDD介绍的注释,我们来翻译下:
A list of partitions
:一组分片(Partition)/一个分区(Partition)列表,即数据集的基本组成单位。对于RDD来说,每个分片都会被一个计算任务处理,分片数决定并行度。用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值。
A function for computing each split
:一个函数会被作用在每一个分区。Spark中RDD的计算是以分片为单位的,compute函数会被作用到每个分区上。
A list of dependencies on other RDDs
:一个RDD会依赖于其他多个RDD。RDD的每次转换都会生成一个新的RDD,所以RDD之间就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算。(Spark的容错机制)
Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
:可选项,对于KV类型的RDD会有一个Partitioner,即RDD的分区函数,默认为HashPartitioner。
Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
:可选项,一个列表,存储存取每个Partition的优先位置(preferred location)。对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。按照"移动数据不如移动计算"的理念,Spark在进行任务调度的时候,会尽可能选择那些存有数据的worker节点来进行任务计算。
总结
RDD 是一个数据集的表示,不仅表示了数据集,还表示了这个数据集从哪来,如何计算,主要属性包括:
分区列表、分区函数、最佳位置,这三个属性其实说的就是数据集在哪,在哪计算更合适,如何分区;
计算函数、依赖关系,这两个属性其实说的是数据集怎么来的。
二、RDD-API
1. RDD的创建方式
由外部存储系统的数据集创建,包括本地的文件系统,还有所有Hadoop支持的数据集,比如HDFS、Cassandra、HBase等:
val rdd1 = sc.textFile("hdfs://node1:8020/wordcount/input/words.txt")
通过已有的RDD经过算子转换生成新的RDD:
val rdd2=rdd1.flatMap(_.split(" "))
由一个已经存在的Scala集合创建:
val rdd3 = sc.parallelize(Array(1,2,3,4,5,6,7,8))
或
val rdd4 = sc.makeRDD(List(1,2,3,4,5,6,7,8))
makeRDD方法底层调用了parallelize方法:
RDD源码
2. RDD的算子分类
RDD的算子分为两类:
Transformation
转换操作:
返回一个新的RDD
Action
动作操作:
返回值不是RDD(无返回值或返回其他的)
❣️注意:
1、RDD不实际存储真正要计算的数据,而是记录了数据的位置在哪里,数据的转换关系(调用了什么方法,传入什么函数)。
2、RDD中的所有转换都是惰性求值/延迟执行的,也就是说并不会直接计算。只有当发生一个要求返回结果给Driver的Action动作时,这些转换才会真正运行。
3、之所以使用惰性求值/延迟执行,是因为这样可以在Action时对RDD操作形成DAG有向无环图进行Stage的划分和并行优化,这种设计让Spark更加有效率地运行。
3. Transformation转换算子
转换算子
含义
map
(func)
返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成
filter
(func)
返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成
flatMap
(func)
类似于map,但是每一个输入元素可以被映射为0或多个输出元素(所以func应该返回一个序列,而不是单一元素)
mapPartitions
(func)
类似于map,但独立地在RDD的每一个分片上运行,因此在类型为T的RDD上运行时,func的函数类型必须是Iterator[T] => Iterator[U]
mapPartitionsWithIndex
(func)
类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,func的函数类型必须是(Int, Interator[T]) => Iterator[U]
sample(withReplacement, fraction, seed)
根据fraction指定的比例对数据进行采样,可以选择是否使用随机数进行替换,seed用于指定随机数生成器种子
union
(otherDataset)
对源RDD和参数RDD求并集后返回一个新的RDD
intersection(otherDataset)
对源RDD和参数RDD求交集后返回一个新的RDD
distinct
([numTasks]))
对源RDD进行去重后返回一个新的RDD
groupByKey
([numTasks])
在一个(K,V)的RDD上调用,返回一个(K, Iterator[V])的RDD
reduceByKey
(func, [numTasks])
在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,与groupByKey类似,reduce任务的个数可以通过第二个可选的参数来设置
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks])
对PairRDD中相同的Key值进行聚合操作,在聚合过程中同样使用了一个中立的初始值。和aggregate函数类似,aggregateByKey返回值的类型不需要和RDD中value的类型一致
sortByKey
([ascending], [numTasks])
在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD
sortBy(func,[ascending], [numTasks])
与sortByKey类似,但是更灵活
join
(otherDataset, [numTasks])
在类型为(K,V)和(K,W)的RDD上调用,返回一个相同key对应的所有元素对在一起的(K,(V,W))的RDD
cogroup(otherDataset, [numTasks])
在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable
,Iterable
))类型的RDD
cartesian(otherDataset)
笛卡尔积
pipe(command, [envVars])
对rdd进行管道操作
coalesce
(numPartitions)
减少 RDD 的分区数到指定值。在过滤大量数据之后,可以执行此操作
repartition
(numPartitions)
重新给 RDD 分区
4. Action动作算子
动作算子
含义
reduce(func)
通过func函数聚集RDD中的所有元素,这个功能必须是可交换且可并联的
collect()
在驱动程序中,以数组的形式返回数据集的所有元素
count()
返回RDD的元素个数
first()
返回RDD的第一个元素(类似于take(1))
take(n)
返回一个由数据集的前n个元素组成的数组
takeSample(withReplacement,num, [seed])
返回一个数组,该数组由从数据集中随机采样的num个元素组成,可以选择是否用随机数替换不足的部分,seed用于指定随机数生成器种子
takeOrdered(n, [ordering])
返回自然顺序或者自定义顺序的前 n 个元素
saveAsTextFile
(path)
将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本
saveAsSequenceFile
(path)
将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统
saveAsObjectFile(path)
将数据集的元素,以 Java 序列化的方式保存到指定的目录下
countByKey
()
针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数
foreach(func)
在数据集的每一个元素上,运行函数func进行更新
foreachPartition
(func)
在数据集的每一个分区上,运行函数func
统计操作:
算子
含义
count
个数
mean
均值
sum
求和
max
最大值
min
最小值
variance
方差
sampleVariance
从采样中计算方差
stdev
标准差:衡量数据的离散程度
sampleStdev
采样的标准差
stats
查看统计结果
三、RDD的持久化/缓存
在实际开发中某些RDD的计算或转换可能会比较耗费时间,如果这些RDD后续还会频繁的被使用到,那么可以将这些RDD进行持久化/缓存,这样下次再使用到的时候就不用再重新计算了,提高了程序运行的效率。
val rdd1 = sc.textFile("hdfs://node01:8020/words.txt" )val rdd2 = rdd1.flatMap(x=>x.split(" " )).map((_,1 )).reduceByKey(_+_) rdd2.cache //缓存/持久化 rdd2.sortBy(_._2,false ).collect//触发action,会去读取HDFS的文件,rdd2会真正执行持久化 rdd2.sortBy(_._2,false ).collect//触发action,会去读缓存中的数据,执行速度会比之前快,因为rdd2已经持久化到内存中了
持久化/缓存API详解
RDD通过persist或cache方法可以将前面的计算结果缓存,但是
并不是这两个方法被调用时立即缓存
,
而是
触发后面的action
时
,该RDD将会被缓存在计算节点的内存中,并供后面重用。
通过查看RDD的源码发现cache最终也是调用了persist无参方法(默认存储只存在内存中):
RDD源码
默认的存储级别都是仅在内存存储一份,Spark的存储级别还有好多种,存储级别在object StorageLevel中定义的。
持久化级别
说明
MORY_ONLY(默认)
将RDD以非序列化的Java对象存储在JVM中。如果没有足够的内存存储RDD,则某些分区将不会被缓存,每次需要时都会重新计算。这是默认级别
MORY_AND_DISK(开发中可以使用这个)
将RDD以非序列化的Java对象存储在JVM中。如果数据在内存中放不下,则溢写到磁盘上.需要时则会从磁盘上读取
MEMORY_ONLY_SER (Java and Scala)
将RDD以序列化的Java对象(每个分区一个字节数组)的方式存储.这通常比非序列化对象(deserialized objects)更具空间效率,特别是在使用快速序列化的情况下,但是这种方式读取数据会消耗更多的CPU
MEMORY_AND_DISK_SER (Java and Scala)
与MEMORY_ONLY_SER类似,但如果数据在内存中放不下,则溢写到磁盘上,而不是每次需要重新计算它们
DISK_ONLY
将RDD分区存储在磁盘上
MEMORY_ONLY_2, MEMORY_AND_DISK_2等
与上面的储存级别相同,只不过将持久化数据存为两份,备份每个分区存储在两个集群节点上
OFF_HEAP(实验中)
与MEMORY_ONLY_SER类似,但将数据存储在堆外内存中。(即不是直接存储在JVM内存中)
总结:
缓存的级别有很多,默认只存在内存中,开发中使用memory_and_disk
只有执行action操作的时候才会真正将RDD数据进行持久化/缓存
实际开发中如果某一个RDD后续会被频繁的使用,可以将该RDD进行持久化/缓存