前言
Spark是一种大规模、快速计算的集群平台,本公众号试图通过学习Spark官网的实战演练笔记提升笔者实操能力以及展现Spark的精彩之处。有关框架介绍和环境配置可以参考以下内容:
1.大数据处理框架Hadoop、Spark介绍
2.linux下Hadoop安装与环境配置
3.linux下Spark安装与环境配置
本文的参考配置为:Deepin 15.11、Java 1.8.0_241、Hadoop 2.10.0、Spark 2.4.4、scala 2.11.12
本文的目录为:
一、弹性分布式数据集(RDDs)
1.并行集合
2.外部数据源
3.RDD转换操作
4.RDD行动操作
5.持久化
二、共享变量
1.广播变量
2.累加器
一、弹性分布式数据集(RDDs)
Spark 主要以弹性分布式数据集(RDD)的概念为中心,它是一个容错且可以执行并行操作的元素的集合。有两种方法可以创建 RDD:在你的 driver program(驱动程序)中 parallelizing 一个已存在的集合,或者在外部存储系统中引用一个数据集,例如,一个共享文件系统,HDFS,HBase,或者提供 Hadoop InputFormat 的任何数据源。
1.并行集合
例如在已存在的集合上通过调用 SparkContext 的 parallelize 方法来创建并行集合。
scala> val data = Array(1, 2, 3, 4, 5)
data: Array[Int] = Array(1, 2, 3, 4, 5)
scala> val distData = sc.parallelize(data)
distData: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at
:26
2.外部数据源
Spark 可以从 Hadoop 所支持的任何存储源中创建 distributed dataset(分布式数据集),包括本地文件系统,HDFS,Cassandra,HBase,Amazon S3 等等。Spark 支持文本文件,SequenceFiles,以及任何其它的 Hadoop InputFormat。
val distFile1 = sc.textFile("file:///home/phenix/Documents/spark/data/test") //本地文件
val distFile2 =sc.textFile("data/test") //已上传的HDFS文件
// 注意:textFile可以读取多个文件,或者1个文件夹,也支持压缩文件、包含通配符的路径。
textFile("/input/001.txt, /input/002.txt ") //读取多个文件
textFile("/input") //读取目录
textFile("/input /*.txt") //含通配符的路径
textFile("/input /*.gz") //读取压缩文件
3.RDD操作
RDDs support 两种类型的操作:transformations(转换),它会在一个已存在的 dataset 上创建一个新的 dataset,和 actions(动作),将在 dataset 上运行的计算后返回到 driver 程序。为了说明 RDD 基础,请思考下面这个的简单程序:
// 从外部文件中定义了一个基本的 RDD,但这个数据集并未加载到内存中或即将被行动
scala> val lines = sc.textFile("data/test")
lines: org.apache.spark.rdd.RDD[String] = data/test MapPartitionsRDD[21] at textFile at
:24 // 定义了 lineLengths 作为 map transformation 的结果但没有立即发生计算
scala> val lineLengths = lines.map(s => s.length)
lineLengths: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[22] at map at
:25 // reduce为一个action,此处发生了真正的计算,结果为此文件长度为438
scala> val totalLength = lineLengths.reduce((a, b) => a + b)
totalLength: Int = 438
//如果我们也希望以后再次使用 lineLengths,我们还可以添加:
scala> lineLengths.persist()
res8: lineLengths.type = MapPartitionsRDD[22] at map at
:25
我们还可以传递函数给Spark。当 driver 程序在集群上运行时,Spark 的 API 在很大程度上依赖于传递函数。可以使用Anonymous function syntax(匿名函数语法),它可以用于短的代码片断:
这里,如果我们创建一个 MyClass 的实例,并调用 doStuff,在 map 内有 MyClass 实例的 func1 方法的引用,所以整个对象需要被发送到集群的。
import org.apache.spark.rdd.RDD
class MyClass {
val field = "Hello"
def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(x => field + x) }
}
在集群中执行代码时,一个关于 Spark 更难的事情是理解变量和方法的范围和生命周期。修改其范围之外的变量 RDD 操作可以混淆的常见原因。在下面的例子中,我们将看一下使用的 foreach() 代码递增累加计数器,但类似的问题,也可能会出现其他操作上
var counter = 0
var rdd = sc.parallelize(data)
// 下面这行操作是错误的
rdd.foreach(x => counter += x)
println("Counter value: " + counter)
闭包是指 executor 要在RDD上进行计算时必须对执行节点可见的那些变量和方法(在这里是foreach())。闭包的变量副本发给每个 executor,当 counter 被 foreach 函数引用的时候,它已经不再是 driver node 的 counter 了。虽然在 driver node 仍然有一个 counter 在内存中,但是对 executors 已经不可见。executor 看到的只是序列化的闭包一个副本。所以 counter 最终的值还是 0,因为对 counter 所有的操作均引用序列化的 closure 内的值。
大多数 Spark 操作工作在包含任何类型对象的 RDDs 上,只有少数特殊的操作可用于 Key-Value 对的 RDDs。最常见的是分布式 “shuffle” 操作,如通过元素的 key 来进行 grouping 或 aggregating 操作.
val lines = sc.textFile("data/test")
val pairs = lines.map(s => (s, 1))
val counts = pairs.reduceByKey((a, b) => a + b)
3.RDD转换操作
Spark 常用的 transformations(转换)有:map、filter、flatMap、mapPartitions、mapPartitionsWithIndex、sample、union、intersection、distinct、groupByKey、reduceByKey、aggregateByKey、sortByKey、join、cogroup、cartesian、pipe、coalesce、repartition、repartitionAndSortWithinPartitions。
map是对RDD中的每个元素都执行一个指定的函数来产生一个新的RDD;RDD之间的元素是一对一关系:
scala> val rdd1 = sc.parallelize(1 to 9,3)
rdd1: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at
:24
scala> val rdd2 = rdd1.map(x=>x*2)
rdd2: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[1] at map at
:25
scala> rdd2.collect()
res0: Array[Int] = Array(2, 4, 6, 8, 10, 12, 14, 16, 18)
Filter是对RDD元素进行过滤;返回一个新的数据集,是经过func函数后返回值为true的原元素组成:
scala> val rdd3 = rdd2.filter(x=> x> 10)
rdd3: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[2] at filter at
:25
scala> rdd3.collect()
res1: Array[Int] = Array(12, 14, 16, 18)
flatMap类似于map,但是每一个输入元素,会被映射为0到多个输出元素(因此,func函数的返回值是一个Seq,而不是单一元素),RDD之间的元素是一对多关系;
scala> val rdd4 = rdd3.flatMap(x=>x to 20)
rdd4: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[3] at flatMap at
:25
scala> rdd4.collect()
res2: Array[Int] = Array(12, 13, 14, 15, 16, 17, 18, 19, 20, 14, 15, 16, 17, 18, 19, 20, 16, 17, 18, 19, 20, 18, 19, 20)
mapPartitions是map的一个变种。map的输入函数是应用于RDD中每个元素,而mapPartitions的输入函数是每个分区的数据,也就是把每个分区中的内容作为整体来处理的
mapPartitionsWithSplit与mapPartitions的功能类似, 只是多传入split index而已,所有func 函数必需是 (Int, Iterator
sample(withReplacement,fraction,seed)是根据给定的随机种子seed,随机抽样出数量为frac的数据。withReplacement:是否放回抽样;fraction:比例,0.1表示10% ;
scala> val a = sc.parallelize(1 to 10000,3)
a: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[4] at parallelize at
:24
scala> a.sample(false,0.1,0).count()
res3: Long = 1032
union(otherDataset)是数据合并,返回一个新的数据集,由原数据集和otherDataset联合而成
scala> val rdd5 = rdd1.union(rdd3)
rdd5: org.apache.spark.rdd.RDD[Int] = UnionRDD[6] at union at
:27
scala> rdd5.collect()
res4: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 14, 16, 18)
intersection(otherDataset)是数据交集,返回一个新的数据集,包含两个数据集的交集数据;
scala> val rdd6 = rdd5.intersection(rdd1)
rdd6: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[12] at intersection at
:27
scala> rdd6.collect()
res5: Array[Int] = Array(6, 1, 7, 8, 2, 3, 9, 4, 5)
distinct([numTasks]))是数据去重,返回一个数据集,是对两个数据集去除重复数据,numTasks参数是设置任务并行数量
scala> val rdd7 = rdd5.union(rdd6).distinct
rdd7: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[16] at distinct at
:27
scala> rdd7.collect()
res7: Array[Int] = Array(12, 1, 14, 2, 3, 4, 16, 5, 6, 18, 7, 8, 9)
groupByKey([numTasks])是数据分组操作,在一个由(K,V)对组成的数据集上调用,返回一个(K,Seq[V])对的数据集。
scala> val rdd0 = sc.parallelize(Array((1,1),(1,2),(1,3),(2,1),(2,2),(2,3)))
rdd0: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[17] at parallelize at
:24
scala> val rdd8 = rdd0.groupByKey()
rdd8: org.apache.spark.rdd.RDD[(Int, Iterable[Int])] = ShuffledRDD[18] at groupByKey at
:25
scala> rdd8.collect()
res8: Array[(Int, Iterable[Int])] = Array((1,CompactBuffer(1, 2, 3)), (2,CompactBuffer(1, 2, 3)))
reduceByKey(func, [numTasks])是数据分组聚合操作,在一个(K,V)对的数据集上使用,返回一个(K,V)对的数据集,key相同的值,都被使用指定的reduce函数聚合到一起。
scala> val rdd9 = rdd0.reduceByKey((x,y)=>x+y)
rdd9: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[19] at reduceByKey at
:25
scala> rdd9.collect()
res9: Array[(Int, Int)] = Array((1,6), (2,6))
aggreateByKey(zeroValue: U)(seqOp: (U, T)=> U, combOp: (U, U) =>U) 和reduceByKey的不同在于,reduceByKey输入输出都是(K, V),而aggreateByKey输出是(K,U),可以不同于输入(K, V) ,aggreateByKey的三个参数:
zeroValue: U,初始值,比如空列表{} ;
seqOp: (U,T)=> U,seq操作符,描述如何将T合并入U,比如如何将item合并到列表 ;
combOp: (U,U) =>U,comb操作符,描述如果合并两个U,比如合并两个列表 ;
所以aggreateByKey可以看成更高抽象的,更灵活的reduce或group
scala> val rdd10 = sc.parallelize(List(1,2,3,4,5,6),2)
rdd10: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[21] at parallelize at
:24
scala> rdd10.aggregate(0)(math.max(_,_),_+_)
res12: Int = 9
scala> val rdd11 = sc.parallelize(List((1,3),(1,2),(1,4),(2,3)))
rdd11: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[1] at parallelize at
:24
scala> rdd11.aggregateByKey(0)(math.max(_,_),_+_)
res1: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[2] at aggregateByKey at
:26
scala> rdd11.collect()
res2: Array[(Int, Int)] = Array((1,3), (1,2), (1,4), (2,3))
combineByKey是对RDD中的数据集按照Key进行聚合操作。聚合操作的逻辑是通过自定义函数提供给combineByKey。combineByKey[C](createCombiner: (V) ⇒ C, mergeValue: (C, V) ⇒ C, mergeCombiners: (C, C) ⇒ C, numPartitions: Int):RDD[(K, C)]把(K,V) 类型的RDD转换为(K,C)类型的RDD,C和V可以不一样。
scala> val rdd12 = sc.parallelize(data,2)
rdd12: org.apache.spark.rdd.RDD[(Int, Double)] = ParallelCollectionRDD[0] at parallelize at
:26
scala> val combine1 = rdd12.combineByKey(createCombiner = (v:Double) => (v:Double, 1),mergeValue = (c:(Double, Int), v:Double) => (c._1 + v, c._2 + 1),mergeCombiners = (c1:(Double, Int), c2:(Double, Int)) => (c1._1 + c2._1, c1._2 + c2._2),numPartitions = 2)
combine1: org.apache.spark.rdd.RDD[(Int, (Double, Int))] = ShuffledRDD[1] at combineByKey at
:25
scala> combine1.collect()
res0: Array[(Int, (Double, Int))] = Array((2,(15.0,3)), (1,(6.0,3)))
sortByKey([ascending],[numTasks])是排序操作,对(K,V)类型的数据按照K进行排序,其中K需要实现Ordered方法
scala> val rdd13 = rdd0.sortByKey()
rdd13: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[5] at sortByKey at
:25
scala> rdd13.collect()
res1: Array[(Int, Int)] = Array((1,1), (1,2), (1,3), (2,1), (2,2), (2,3))
join(otherDataset, [numTasks])是连接操作,将输入数据集(K,V)和另外一个数据集(K,W)进行Join, 得到(K, (V,W));该操作是对于相同K的V和W集合进行笛卡尔积 操作,也即V和W的所有组合(连接操作除join 外,还有左连接、右连接、全连接操作函数:leftOuterJoin、rightOuterJoin、fullOuterJoin)
scala> val rdd14 = rdd0.join(rdd0)
rdd14: org.apache.spark.rdd.RDD[(Int, (Int, Int))] = MapPartitionsRDD[8] at join at
:25
scala> rdd14.collect()
res3: Array[(Int, (Int, Int))] = Array((1,(1,1)), (1,(1,2)), (1,(1,3)), (1,(2,1)), (1,(2,2)), (1,(2,3)), (1,(3,1)), (1,(3,2)), (1,(3,3)), (2,(1,1)), (2,(1,2)), (2,(1,3)), (2,(2,1)), (2,(2,2)), (2,(2,3)), (2,(3,1)), (2,(3,2)), (2,(3,3)))
cogroup(otherDataset, [numTasks])是将输入数据集(K, V)和另外一个数据集(K, W)进行cogroup,得到一个格式为(K, Seq[V], Seq[W])的数据集
scala> val rdd15 = rdd0.cogroup(rdd0)
rdd15: org.apache.spark.rdd.RDD[(Int, (Iterable[Int], Iterable[Int]))] = MapPartitionsRDD[10] at cogroup at
:25
scala> rdd15.collect()
res5: Array[(Int, (Iterable[Int], Iterable[Int]))] = Array((1,(CompactBuffer(1, 2, 3),CompactBuffer(1, 2, 3))), (2,(CompactBuffer(1, 2, 3),CompactBuffer(1, 2, 3))))
cartesian(otherDataset)是做笛卡尔积:对于数据集T和U 进行笛卡尔积操作, 得到(T, U)格式的数据集
scala> val rdd16 = rdd1.cartesian(rdd3)
rdd16: org.apache.spark.rdd.RDD[(Int, Int)] = CartesianRDD[14] at cartesian at
:27
scala> rdd16.collect()
res6: Array[(Int, Int)] = Array((1,12), (2,12), (3,12), (1,14), (1,16), (1,18), (2,14), (2,16), (2,18), (3,14), (3,16), (3,18), (4,12), (5,12), (6,12), (4,14), (4,16), (4,18), (5,14), (5,16), (5,18), (6,14), (6,16), (6,18), (7,12), (8,12), (9,12), (7,14), (7,16), (7,18), (8,14), (8,16), (8,18), (9,14), (9,16), (9,18))
4.RDD行动操作
reduce(func)是对数据集的所有元素执行聚集(func)函数,该函数必须是可交换的。
collect是将数据集中的所有元素以一个array的形式返回
count返回数据集中元素的个数。
first返回数据集中的第一个元素, 类似于take(1)
Take(n)返回一个包含数据集中前n个元素的数组
takeSample(withReplacement,num, [seed])返回包含随机的num个元素的数组,和Sample不同,takeSample 是行动操作,所以返回的是数组而不是RDD , 其中第一个参数withReplacement是抽样时是否放回,第二个参数num会精确指定抽样数,而不是比例。
takeOrdered(n, [ordering])是返回包含随机的n个元素的数组,按照顺序输出
saveAsTextFile把数据集中的元素写到一个文本文件,Spark会对每个元素调用toString方法来把每个元素存成文本文件的一行。
countByKey对于(K, V)类型的RDD. 返回一个(K, Int)的map, Int为K的个数
foreach(func)是对数据集中的每个元素都执行func函数
5.持久化
Spark 中一个很重要的能力是将数据 persisting 持久化(或称为 caching 缓存),在多个操作间都可以访问这些持久化的数据。RDD 可以使用 persist() 方法或 cache() 方法进行持久化。Spark 的缓存具有容错机制,如果一个缓存的 RDD 的某个分区丢失了,Spark 将按照原来的计算过程,自动重新计算并进行缓存。另外,每个持久化的 RDD 可以使用不同的 storage level 存储级别进行缓存:
Storage Level(存储级别) | Meaning(含义) |
---|---|
MEMORY_ONLY | 将 RDD 以反序列化的 Java 对象的形式存储在 JVM 中。如果内存空间不够,部分数据分区将不再缓存,在每次需要用到这些数据时重新进行计算。这是默认的级别。 |
MEMORY_AND_DISK | 将 RDD 以反序列化的 Java 对象的形式存储在 JVM 中。如果内存空间不够,将未缓存的数据分区存储到磁盘,在需要使用这些分区时从磁盘读取。 |
MEMORY_ONLY_SER | |
(Java and Scala) | 将 RDD 以序列化的 Java 对象的形式进行存储(每个分区为一个 byte 数组)。这种方式会比反序列化对象的方式节省很多空间,尤其是在使用 fast serializer 时会节省更多的空间,但是在读取时会增加 CPU 的计算负担。 |
MEMORY_AND_DISK_SER | |
(Java and Scala) | 类似于 MEMORY_ONLY_SER,但是溢出的分区会存储到磁盘,而不是在用到它们时重新计算。 |
DISK_ONLY | 只在磁盘上缓存 RDD。 |
MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc。 | 与上面的级别功能相同,只不过每个分区在集群中两个节点上建立副本。 |
OFF_HEAP(experimental 实验性) | 类似于 MEMORY_ONLY_SER,但是将数据存储在 off-heap memory 中。这需要启用 off-heap 内存。 |
二、共享变量
通常情况下,一个传递给 Spark 操作(例如 map 或 reduce)的函数 func 是在远程的集群节点上执行的。该函数 func 在多个节点执行过程中使用的变量,是同一个变量的多个副本。这些变量的以副本的方式拷贝到每个机器上,并且各个远程机器上变量的更新并不会传播回 driver program(驱动程序)。通用且支持 read-write(读-写)的共享变量在任务间是不能胜任的。所以,Spark 提供了两种特定类型的共享变量:broadcast variables(广播变量)和 accumulators(累加器)。
1.广播变量
Broadcast variables(广播变量)允许程序员将一个 read-only(只读的)变量缓存到每台机器上,而不是给任务传递一个副本。广播变量通过在一个变量 v 上调用 SparkContext.broadcast(v) 方法来进行创建。广播变量是 v 的一个 wrapper(包装器),可以通过调用 value 方法来访问它的值。代码示例如下:
scala> val broadcastVar = sc.broadcast(Array(1,2,3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(13)
scala> broadcastVar.value
res7: Array[Int] = Array(1, 2, 3)
在创建广播变量之后,在集群上执行的所有的函数中,应该使用该广播变量代替原来的 v 值,所以节点上的 v 最多分发一次。另外,对象 v 在广播后不应该再被修改,以保证分发到所有的节点上的广播变量具有同样的值(例如,如果以后该变量会被运到一个新的节点)。
2.累加器
Accumulators(累加器)是一个仅可以执行 “added”(添加)的变量来通过一个关联和交换操作,因此可以高效地执行支持并行。累加器可以用于实现 counter(计数,类似在 MapReduce 中那样)或者 sums(求和)。原生 Spark 支持数值型的累加器,并且程序员可以添加新的支持类型。
可以通过调用 sc.longAccumulator() 或sc.doubleAccumulator() 方法创建数值类型的 accumulator(累加器)以分别累加 Long 或 Double 类型的值。集群上正在运行的任务就可以使用 add 方法来累计数值。然而,它们不能够读取它的值。只有 driver program(驱动程序)才可以使用 value 方法读取累加器的值。
scala> val accum = sc.longAccumulator("My Accumulator")
accum: org.apache.spark.util.LongAccumulator = LongAccumulator(id: 0, name: Some(My Accumulator), value: 0)
scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum.add(x))
scala> accum.value
res1: Long = 10
RDD和共享变量的内容至此结束,下文将进一步对Spark SQL 、DataFrame、DataSets的内容做详细介绍。
前文笔记请参考下面的链接:
Spark大数据分布式处理实战笔记(一):快速开始
你可能错过了这些~
“高频面经”之数据分析篇
“高频面经”之数据结构与算法篇
“高频面经”之大数据研发篇
“高频面经”之机器学习篇
“高频面经”之深度学习篇
我就知道你“在看”