Spark学习笔记-理论篇-02-Spark RDD及编程接口

概要:介绍Spark非常重要的概念RDD,以及基于RDD的相关操作——利用Spark接口编写出复杂的数据处理流水线。

2.1 快用Spark向世界问好

var sc = new SparkContext("spark://...", "Hello World", "YOUR_SPRAK_HOME", "YOUR_PP_JAR")
var file = sc.textFile("hdfs:///root/Log")
var filterRDD = file.filter(_.contains("Hello World"))
filterRDD.cache()
filterRDD.count()

程序说明:在一个存储于HDFS的Log文件中,计算出现过字符串“Hello World"的行数。假设Log文件的存储路径是hdfs://root/Log

  1. 对于Spark的任何程序,要进行操作,首先要创建Spark的上下文,在创建上下文的过程中,程序会向集群申请资源以及构建相应的运行环境。创建Spark对象要传入4个参数
    - Spark程序运行的集群地址,比如spark://localhost:7077
    - Spark程序的标识
    - Spark安装的路径
    - Spark程序的jar包路径

  2. 通过sc变量,利用textfile接口从HDFS文件系统中读入Log文件,返回一个变量file。

  3. 对file变量进行过滤操作,传入的参数是一个function对象,function对象的原型:

    p: (A)=>Boolean

    对于file中的每一行字符串判断是否有”Hello World"字符串,生成新的变量filterRDD。

  4. 对filterRDD进行cache操作,以便后续操作重用filterRDD这个变量。

  5. 对filterRDD进行count计数操作,最后返回“Hello World"字符串的文本行数。

麻雀虽小,五脏俱全。这个程序涉及了Spark中非常多的重要概念。下面逐一介绍。

概念解释
弹性分布式数据集RDDfilefilterRDD变量都是RDD
创建操作(creation operation)RDD的初始创建都是由SparkContext来负责的,将内存中的集合或者外部文件系统作为输入源。
转换操作(transformation operation)将一个RDD通过一定的操作变换成另一个RDD,比如file这个RDD通过一个filter操作变换成ilterRDD,所以filter就是一个转换操作。
控制操作(control operation)对RDD进行持久化,可以让RDD保存在磁盘或者内存中,以便后续重复使用。比如cache接口默认将filterRDD缓存在内存中。
行动操作(action operation)由于Spark是**惰性计算(lazy computing)**的,所以对于任何RDD进行行动操作都会触发Spark作业的运行,从而产生最终的结果。例如我们对filterRDD进行的count操作就是一个行动操作。1

对于一个Spark数据处理程序而言,经过输入操作(创建操作)、转换操作、控制操作、输出操作(行动操作)来完成一个作业。当然在一个Spark程序中,可以有多个行动操作,也就是有多个作业存在。

2.1 Spark RDD

RDD是弹性分布式数据集,即一个代表一个被分区的只读数据集。
一个RDD生成只有两种途径:

  • 来自于内存集合和外部存储系统
  • 通过转换操作来自其他RDD,比如map,join,filter

Spark没必要随时被实例化, 由于RDD的接口只支持粗粒度操作(即一个操作会被应用在RDD的所有数据上),所以只要通过记录下这些作用在RDD之上的转换操作,来构建RDD的继承关系(lineage),就可以有效地进行容错处理,而不需要将实际地RDD数据进行拷贝。
这对RDD来说是一项非常强大地功能,在一个Spark程序中,我们所用到的每一个RDD,在丢失或者操作失败之后都是可以重建的。

RDD还有另外两个方面的控制操作持久化和分区

  • 开发者可以指明需要重用哪些RDD,然后选择一种存储策略将它们保存起来。
  • 开发者还可以让RDD根据记录中的键值,在集群的机器之间重新分区。
    例如,让将要进行join操作的两个数据集以同样的方式进行哈希分区。

RDD的接口模式

如何表示这样一个分区的、高效容错的而且能够持久化的分布式数据集呢?
一般,抽象的RDD需要包含如下5个接口:

接口名解释
partition分区,一个RDD会有一个或多个分区
preferredLocations(d)对于分区d而言,返回数据本地化计算的节点
dependencies()RDD的依赖关系
compute(p,context)对于分区p而言,进行迭代运算
partitioner()RDD的分区函数

2.2.1 RDD分区(partition)

RDD是一个分区的数据集,具备分区的属性。对于一个RDD而言,分区的多少涉及对这个RDD进行并行计算的粒度,每个RDD分区操作都在一个单独的任务中被执行。
对于RDD分区而言,用户可以自行指定多少分区,如果没有制定,那么将会使用默认值。

如下程序将scala中的1~100的数组转换为RDD,第二个参数就可以制定分区数。

scala> val rdd=sc.parallelize(1 to 100, 2)
rdd: org.apache.spark.rdd.RDD[Int] = ParalleCollectionRDD[0] at parallelize at <console>:12

可以使用RDD的成员变量partitions所返回的partition数组的大小,来查询一个RDD被划分的分区数。

scala> rdd.partitions.size
res0: Int = 2

在创建RDD的时候不指定分区,创建出的RDD就采用系统默认的分区数,系统默认的数值是在这个程序所分配的资源的CPU核的个数。

scala> val rdd =sc.parallelize(1 to 100)
rdd: org.apache.spark.rdd.RDD[Int] = ParalleCollectionRDD[0] at parallelize at <console>:12
scala> rdd.partitions.size
res0: Int = 24

2.2.2 RDD优先位置(preferredLocations)

RDD优先位置属性与Spark中的调度相关,返回的是此RDD的每个partition所存储的位置。
按照“移动数据不如移动计算”的理念,在Spark进行任务调度的时候,尽可能地将任务分配到数据块所存储的位置。
以从Hadoop中读取数据生成RDD为例,preferredLocations返回每一个数据块所在的机器名或者IP地址,如果每一块数据是多份存储的,那么就返回多个机器地址。

程序示例:
首先通过SparkContext的TextFile函数读取一个文件bigfile,生成一个类型为MappedRDD的rdd。

scala> val rdd = sc.textFile("hdfs://10.0.2.19:9000/bigfile")
14/03/25 14:29:44 INFO MemeryStore: ensureFreeSpace(35504) called with curMem=35456, maxMem=308713881
14/03/25 14:29:44 INFO MemeryStore: Block broadcast_1 stored as values to memory (estimated size 34.7KB, free 294.3 MB)
rdd:org.apache.spark.rdd.RDD[String] = MappedRDD[3] at textFile at <console>:12

然后通过rdd的依赖关系找到原始的hadoopRDD,hadoopRDD的partition的个数是254个。

scala> val hadoopRDD = rdd.dependencies(0).rdd
hadoopRDD: org.apache.spark.rdd.RDD[ _ ] = HadoopRDD [2] at textFile at <console>: 12
scala> val hadoopRDD.partitions.size
14/03/25 14:30:15 WARN NativeCodeLoader:.......
14/03/25 14:30:15 WARN LoadSnappy:.......
14/03/25 14:30:15 INFO FileInputFormat: Total input paths to process : 1
14/03/25 14:30:15 INFO NetworkTopology: Add a new node : /default-rack/10.0.2.21:50010
14/03/25 14:30:15 INFO NetworkTopology: Add a new node : /default-rack/10.0.2.22:50010
14/03/25 14:30:15 INFO NetworkTopology: Add a new node : /default-rack/10.0.2.23:50010
14/03/25 14:30:15 INFO NetworkTopology: Add a new node : /default-rack/10.0.2.24:50010
res0: Int = 254

对第一个partition(index为0)而言,其preferredLocations返回了三个机器地址,以便后续调度的程序根据这个地址更加有效地分配任务。

scala> hadoopRDD.preferredLocations(hadoopRDD.partitions(0))
res1: Seq[String] = WrappedArray(10.0.2.21, 10.0.2.22, 10.0.2.23) 

2.2.3 RDD依赖关系(dependencies)

由于RDD是粗粒度地操作数据集,每一个转换操作都会生成一个新的RDD,所以RDD之间就会形成一个类似流水线一样地前后依赖关系。

在Spark中存在两种类型的依赖:

  • 窄依赖——每一个父RDD的分区最多只被子RDD的一个分区所使用

  • 宽依赖——多个子RDD的分区会依赖同一个父RDD的分区

    窄依赖

窄依赖示意图

在这里插入图片描述

转换操作Map和Filter就会形成一个窄依赖
经过co-patition操作的两个RDD数据集之间进行join操作也会形成窄依赖

在这里插入图片描述

宽依赖示意图

没有经过co-partition操作的两个RDD数据集之间进行join操作就会形成宽依赖

为什么要区分窄依赖和宽依赖?

有两个方面的原因:

方面窄依赖宽依赖
A窄依赖可以在集群的一个节点上如流水线一般执行,可以计算所有父RDD的分区宽依赖需要取得父RDD的所有分区上的数据进行计算,将会执行类似于MapReduce一样的Shuffle操作
B节点计算失败后的恢复会更加有效,只需要重新计算对应的父RDD的分区,而且可以在其他的节点上并行地计算一个节点地失败将会导致其父RDD地多个分区重新计算,这个代价是非常高的
scala> val rdd = sc.makeRDD(1 to 10)
14/04/04 10:23;25 INFO SparkContext: numSlices is : 24
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at makeRDD at <console>: 12
scala> val mapRDD = rdd.map(x =>(x,x))
mapRDD: org.apache.spark.rdd.RDD [(Int, Int)] =MappedRDD[1] at map at <console>:14
scala> mapRDD.dependencies
res0: Seq[org.apache.spark.Dependency[ _ ] ] = List(org.apache.spark.OneToOneDependency@50456ce9)

如上程序展现了窄依赖关系,org.apache.spark.OneToOneDependency

scala> val shuffleRDD = mapRDD.partitionBy(new org.apache.spark.HashPartitioner(3))
shuffleRDD: org.apache.spark.rdd.RDD[ (Int, Int)] = ShuffledRDD[6] at partitionBy at <console>: 16
scala> shuffleRDD.dependencies
res2: Seq[org.apache.spark.Dependency[ _ ] ] = List(org.apache.spark.ShuffleDependency@14fc91af)

如上程序展现了宽依赖关系,org.apache.spark.ShuffleDependency


利用RDD接口实现PageRank算法

关于PageRank算法
在这里插入图片描述

//------------构建RDD的依赖关系
val sc = new SparkContext(...)
val links = sc.parallelize(Array(('A', Array('D')), (B, Array('A')), ('C', Array('A', 'B')), 
			('D', Array('A''C'))),2).map(x => (x._1, x._2)).cache()
var ranks = sc.parallelize(Array(('A', 1.0), ('B',1.0), ('C',1.0),('D', 1.0)), 2)
//迭代循环
for (i<-1 to ITERATIONS) {

val contribs=links.join(ranks,2).flatMap{
case (url, (links, rank))=> links.map(dest=>(dest, rank/links.size))
}

ranks = contribs.reduceByKey(_+_,2).mapValues(0.5 +0.5* _)
} 
//----------
ranks.saveAsTextFile(...)

每一轮循环都是首先将links和ranks做join操作,在计算links中的每一个邻居获得的contribs,对获得的contribs按照URL分组后计算权重值。
如下图所示,上述程序中的RDD依赖关系在这里插入图片描述

2.2.4 RDD分区计算(compute)

对于Spark中每个RDD的计算都是以partition(分区)为单位的,而且RDD中的compute函数都是在对迭代器进行复合,不需要保存每次计算的结果。

程序示例:
变量rdd是一个被分成2个分区的1~10的集合

scala> val rdd= sc.parallelize(1 to 10,2
rdd: org.apache.spark.rdd.RDD...........

rdd连续进行转换操作mapfilter

scala> val map_rdd = rdd.map(a=>a+1)
map_rdd: org.apache.spark......
scala> val filter_rdd= map_rdd.filter(a=>(a>3))
filter_rdd: org.apache.spark.rdd.RDD...........

由于compute函数只返回相应分区数据的迭代器,所以只有最终实例化时才能,显示出两个分区的最终计算结果。

scala> val context = new org.apache.spark.TaskContext(0,0,0)
context: org.apache.spark.TaskContext = ................

scala> iter0 = filter_rdd.compute(filter_rdd.partitions(0),context)
iter0: Iterator[Int] = non-empty iterator

scala> iter0.toList
res0: List[Int] = List(4,5,6)

scala> val iter1 = filter_rdd.compute(filter_rdd.partitions(1),context)
iter1: Iterator[Int] = non-empty iterator

scala> iter1.toList
res1: List[Int] = List(7,8,9,10,11)

2.2.5 RDD分区函数(partition)

partitioner就是RDD分区函数,目前在Spark中实现了两种类型的分区函数,即

  • HashPartitioner(哈希分区)
  • RangePartitioner (区域分区)
    且partitioner这个属性只存在于(K, V)类型的RDD中,对于非(K, V)类型的partitioner的值就是None。

partitioner函数既决定了RDD本身的分区数量,也可以为其父RDD Shuffle输出(MapOutput)中每个分区进行数据切割的依据

程序示例:(以HashPartitioner为例)

scala> val rdd = sc.makeRDD(1 to 10,2).map(x =>(x,x))
rdd: org.apache.spark.rddRDD[(Int,Int)] =MappedRDD[1] at map at <console>:12
scala> rdd.partitioner
res0: Option[org.apache.spark.Partitioner] = None
//首先构造出一个MappedRDD,其partitioner值为None
scala> val group_rdd= rdd.groupByKey(new org.apache.spark.HashPartitioner(3))
//这里创建了新的HashPartitioner对象,参数3代表group_rdd最终会有3个分区
.....
scala> group_rdd.partitioner
......
scala> group_rdd.collectPartitions()
//最后执行collectPartitions行动操作(测试分区的值)
res2:Array[Array[(Int,Seq[Int])]] = Array( Array((6,ArrayBuffer(6)),(9,ArrayBuffer(9)),(3,ArrayBuffer(3))), ..........))
//6,9,3在一组

在这里插入图片描述

HashPartitioner原理图

2.3 创建操作

2.3.1 集合创建操作

RDD的形成可以由内部集合类型来生成。Spark提供了parallelizemakeRDD两类函数来实现从集合生成RDD,两个接口功能类似,不同的时makeRDD还提供了一个可以指定每一个分区preferredLocations参数的实现版本。

程序示例:(makeRDD接口)

scala> val rdd = sc.makeRDD(1 to 10, 3)
....
scala> rdd.collectPartitions()
.........
scala> val collect = Seq((1 to 10, Seq("host1", "host3")),(11 to 20, Seq("host2")))
.........
scala> val rdd =sc.makeRDD(collect)
.........
scala> rdd.preferredLocations(rdd.partitions(0))
......
scala> rdd.preferredLocations(rdd.partitions(1))
............

2.3.2 存储创建操作

spark的整个生态系统于Hadoop都是弯曲兼容的,所以对于Hadoop所支持的文件类型或者数据库类型,spark也同样支持。另外,由于Hadoop的api有新旧两个版本,所以spark为了能够兼容Hadoop的所有版本,也提供了两套创建操作接口。对于外部存储创建操作而言,hadoopRDD和newHadoopRDD是最为抽象的两个函数接口,主要包含以下四个参数。

  • 输入格式(InputFormation):指定数据输入的类型,如TextInputFormation等,新旧两个版本所引用的版本分别是org.apache.hadoop.mapred.InputFormat和org.apache.hadoop.mapreduce.InputFormat (NewInputFormat)
  • 键类型:指定[K,V]键值对中的K的类型
  • 值类型:指定[K,V]键值对中的V的类型
  • 分区值:指定由外部存储生成的RDD的partition数量的最小值,如果没有指定,系统会使用默认值defaultMinSplits

其他创建操作的api接口都是为了方便最终spark程序开发这而设置的,是这两个接口的高效实现版。例如对于textFile接口而言,只有path这个指定文件路径的参数,其他参数在系统内部指定了默认值。

下表描述了兼容旧版本Hadoop API的创建操作

·文件路径输入格式键类型值类型分区值
textFile(path:String,minSplits:Int=defaultMinSplits)pathTextInputFormatLongWritableTextminSplits
hadoopFile[…
sequenceFile[…
objectFile[…
hadoopRDD[…

下表描述了兼容新版本Hadoop API的创建操作

·文件路径输入格式键类型值类型分区值
newAPIHadoopFileK,V,F <: NewInputFormat[K,V](implicit km: ClassTag[K],vm:ClassTag[V],fm:ClassTag[F]):RDD[(K,V)]pathFKVn/a
newAPIHadoopRDD[…

2.4 转换操作

2.4.1 RDD基本转换操作

  1. map[U: ClassTag](f: T=>U): RDD[U]
  2. distinct():RDD[T]
  3. flatMap[U:ClassTag](f:T=>TraversableOnce[U]:RDD[U]

map函数将RDD中类型为T的元素,一对一地映射为类型为U的元素。
distinct函数返回RDD中所有不一样的元素,而flatMap函数则是将RDD中的每一个元素进行一对多转换。

scala> val rdd =sc.makeRDD(1 to 5, 1)
...

scala> val mapRDD= rdd.map(x=>x.toFloat)
..

scala> mapRDD.collect()
...

scala> val flatMapRDD= rdd.flatMap(x =>(1 to x))
..

scala> flatMapRDD.collect()
res7: Array[Int] =Array(1,1,2,1,2,3,1,2,3,4,1,2,3,4,5)

scala> val distinctRDD= flatMapRDD.distinct()
...

scala> distinctRDD.collect()
res8:Array[Int]=Array(4,2,3,1,5)
  1. repartition(numPatitions:Int):RDD[T]
  2. coalesce(numPartitions:Int,shuffle:Boolean=flase):RDD[T]

repartitioncoalesce对RDD的分区进行重新划分,repartition只是coalesce接口中shuffle为true的简易实现。所以这里主要讨论coalesce合并函数该如何设置shuffle参数,这里分三种情况(假设RDD有N个分区,需要重新划分成M个分区)。

  • 如果N<M,一般情况下N个分区有数据分布不均的状况,利用HashPartitioner函数将数据重新分区为M个,这时需要将shuffle参数设置为true
  • 如果N>M且N和M差不多(比如一个是1000一个是100),那么就可以将N个分区中的若干个分区合并成M个分区,这是可以将shuffle参数设置为false(在shuffle为false的情况下,设置M>N,coalesce是不起作用的),不进行shuffle过程,父RDD和子RDD之间是窄依赖关系
  • 如果N>M且N和M差距悬殊(比如一个是1000一个是1),这时如果把shuffle参数设置为false,由于父子RDD是窄依赖,它们共同处于一个stage中,就可能造成spark程序运行的并行度不够,从而影响性能。比如在M为1时,由于只有一个分区,所以只会有一个任务在运行,为了使coalesce之前的操作有更好的并行度,可以将shuffle参数设置为true。

这里衡量M和N的差距需要结合具体的应用场景来确定,在程序运行的并行度和shuffle数据写磁盘这两个因素之间找到平衡。

当shuffle参数为false时,如果传入参数大于现有分区数,那么分区数保持不变,也就是说在,不进行洗牌情况下,是无法将RDD的分区数目变多的。

下图形象的展示了以上三种情形:

val rdd = sc.makeRDD(1 to 10,100)
..
val repartitionRDD = rdd.repartition(4)
..
coalesceRDD.collectPartitions()   //列出partitions
...
val coalesceRDD =rdd.coalesce(3)
..
coalesceRDD.collectPartitions()
..
val coalesceRDD =rdd.coalesce(3,true)  //shuffle重新洗牌
..
coalesceRDD.collectPartitions()
..
val coalesceRDD= rdd.coalesce(200)  //shuffle为false时,如果传入参数大于原有分区数
..
coalesceRDD.partitions.size  //返回RDD的分区数
res:Int=100
..
val coalesceRDD= rdd.coalesce(200,true)
..
coalesceRDD.partitions.size 
res:Int=200
  1. randomSplit(weight:Array[Double], seed:Long=System.nanoTime):Array[RDD[T]]
  2. glom():RDD[Array[T]]

randomglom的返回值中的Array和RDD似乎颠倒了一下。randomSplit函数是根据weight权重将一个RDD切分成多个RDD,而glom函数是将RDD中每一个分区类型为T的元素转换成数组Array[T],这样每一个分区就只有一个数组元素

val rdd=sc.makeRDD(1 to 103...
rdd.collect()   //列出RDD及其所有元素
..
val glomRDD=rdd.glom()
..
glomRDD.collect()
res4:Array[Array[Int]]= Array(Array(1,2,3),Array(4,5,6),Array(7,8,9,10))

glomRDD.collectPartitions()  //列出partitions
res5:Array[Array[Array[Int]]]= Array(Array(Array(1,2,3)),Array(Array(4,5,6)),Array(Array(7,8,9,10)))


val rdd=sc.collectPartitions()
..
val splitRDD= rdd.randomSplit(Array(1.0,3.0,6.0))
..
splitRDD(0).collect()
res44: Array[Int]=Array(9)

splitRDD(1).collect()
res44: Array[Int]=Array(1,10)

splitRDD(2).collect()

res45: Array[Int]=Array(2,3,4,5,6,7,8)
  1. union(other:RDD[T]):RDD[T]
  2. intersection(other:RDD[T]):RDD[T]
  3. intersection(other:RDD[T],partitioner:Partitioner)
  4. subtract(other:RDD[T]):RDD[T]
  5. subtract(other:RDD[T],p:Partitioner):RDD[T]

这些是针对RDD的集合操作。
union操作将两个RDD集合中的数据进行合并、返回两个RDD的并集(包含两个RDD中相同的元素,不回去重)。
intersection操作返回两个RDD集合的交集,且交集中不会包含相同的元素。如果subtract所针对的两个集合是A和B,即操作是val result=A.subtract(B),那么result中将会包含在A中出现且不在B出现的元素。
intersectionsutract一般情况下都会有shuffle的过程。

val rdd1=sc.makeRDD(1 to 3,1)
..
val rdd2=sc.makeRDD(2 to 4,1)
..
val unionRDD=rdd1.union(rdd2)
..
val intersectionRDD=rdd1.intersection(rdd2)
..
unionRDD.collect()
res0:Array[Int]=Array(1,2,3,2,3,4)

intersectionRDD.collect()
res1:Array[Int]=Array(2,3)

val substractRDD =rdd1.substract(rdd2)
..
substractRDD.collect()
res3:Array[Int]=Array(1)

  1. mapPartitions[U:ClassTag](f:Iterator[T]=>Iterator[U], preservesPartitioning: Boolean=false): RDD[U]
  2. mapPartitionsWithIndex[U:ClassTag](f:(Int,Iterator[T])=>Iterator[U], preservesPartitioning:Boolean=false):RDD[U]

mapPartitions与map转换操作类似,只不过映射函数的输入参数有RDD中的每一个元素变成了RDD中每一个分区的迭代器,那么已经有了map为什么还需要mapPartitions函数呢?如果在映射的过程中需要频繁创建额外的对象,map就显得不高效了,RDD中的分区可以共享一个对象以便提高性能。

《Spark大数据处理技术》电子工业出版社 夏俊鸾


  1. Spark中的行动操作基本分为两类。一类是操作结果变成Scala集合或者标量,另一类就将RDD保存到外部文件或者数据库系统中。 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值