目录
- Transformation算子
- map
- filter
- flatMap
- mapPartitions
- mapPartitionsWithIndex
- sample
- glom
- union
- intersection
- distinct
- groupBy
- groupByKey
- reduceByKey
- aggregateByKey
- combineByKey
- sortByKey
- sortBy
- join
- cogroup
- cartesian
- coalesce vs repartition
- repartitionAndSortWithinPartitions
- Action算子
Transformation算子
Transformation 操作是延迟计算的(lazy),也就是说从一个RDD 转换生成另一个 RDD 的转换操作不是马上执行,需要等到有 Action 操作的时候才会真正触发运算。
map
- 对RDD中的每个元素都执行一个指定函数来产生一个新的RDD,任何原RDD中的元素在新RDD中都有且只有一个元素与之对应。
val a = sc.parallelize(List("dog", "salmon", "salmon", "rat", "elephant"), 3)
val b = a.map(item=>(item,item.length).collect
res0: Array[(String, Int)] = Array((dog,3), (salmon,6), (salmon,6), (rat,3), (elephant,8))
filter
- 返回一个新的RDD,该RDD由经过func函数计算后,返回值为true的输入元素组成。
val a = sc.parallelize(1 to 10, 3)
val b = a.filter(_ % 2 == 0).collect
res3: Array[Int] = Array(2, 4, 6, 8, 10)
flatMap
- 与map类似,将原RDD中的每个元素通过函数f转换为新的元素,并将这些元素放入一个集合,构成新的RDD。
sc.parallelize(List(1, 2, 3), 2).flatMap(x => List(x, x, x)).collect
res85: Array[Int] = Array(1, 1, 1, 2, 2, 2, 3, 3, 3)
mapPartitions
- mapPartitions是map的一个变种。map的输入函数应用于RDD中的每个元素,而mapPartitions的输入函数应用于每个分区,也就是把每个分区中的内容作为整体来处理的。
map 一次处理一个 partition 中的一条数据,mapPartitions 一次处理一个 partition 的全部数据
mapPartitionsWithIndex
- 类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,func的函数类型必须是(Int, Iterator[T]) =>Iterator[U]
mapPartitionWithIndex 可以得到每个 partition 的 index,从 0 开始
sample
- sample 算子就是从数据集中抽取一部分数据,sample(withReplacement, fraction, seed): 第一个参数是是否有放回抽取数据,第二个参数是抽取的百分比,第三个参数是用来生成随机数的种子。
// 1到10000,随机抽取比例0.1,种子0
val a = sc.parallelize(1 to 10000, 3)
a.sample(false, 0.1, 0).count
res24: Long = 960
glom
- 将每个分区内的元素组成一个数组,分区不变。
union
- 合并同一数据类型元素,但不去重。合并后返回同类型的数据元素。
intersection
- 对源RDD和参数RDD求交集后返回一个新的RDD.
intersection 的原理:
1.首先 map 操作变成一个 tuple
2.然后 cogroup 聚合两个 RDD 的 key
3.filter, 过滤掉两个集合中任意一个集合为空的 key
4.map,还原出单 key
distinct
- 对源RDD进行去重后返回一个新的RDD
distinct 的原理:
1.首先 map 操作给自己每个值都打上一个 v2,变成一个 tuple
2.然后调用 reduceByKey (仅仅返回一个 value)
3.将去重后的数据,从 tuple 还原为单值
val c = sc.parallelize(List("Gnu", "Cat", "Rat", "Dog", "Gnu", "Rat"), 2)
c.distinct.collect
res6: Array[String] = Array(Dog, Gnu, Cat, Rat)
groupBy
- 对value类型的数据使用,生成相应的key,相同的放在一起返回一个(key, Iterator[V]),这个key是生成的,也可以指定。
val a = sc.parallelize(1 to 9, 3)
a.groupBy(x => { if (x % 2 == 0) "even" else "odd" }).collect
res42: Array[(String, Seq[Int])] = Array((even,ArrayBuffer(2, 4, 6, 8)), (odd,ArrayBuffer(1, 3, 5, 7, 9)))
groupByKey
- 在一个(K,V)的RDD上调用,返回一个(K, Iterator[V])的RDD
val list = List("hadoop","spark","hive","spark")
val rdd = sc.parallelize(list)
val pairRdd = rdd.map(x => (x,1))
pairRdd.groupByKey().collect.foreach(println)
得到:
(hive,CompactBuffer(1))
(spark,CompactBuffer(1, 1))
(hadoop,CompactBuffer(1))
reduceByKey
- reduceByKey(func, [numTasks]):在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,与groupByKey类似,reduce任务的个数可以通过第二个可选的参数来设置。
educeByKey 和 groupByKey 异同之处
不同之处:reduceByKey ,多了一个 RDD,MapPartitionRDD,存在于 stage0 的,主要是代表了
进行本地数据规约之后的 RDD, 所以,网络传输的数据量以及磁盘 I/O 等都会减少,性能更高。
相同之处: 后面进行 shuffle read 和聚合的过程基本和 groupByKey 类似。
都是 shuffleRDD,去做 shuffle read。然后聚合, 聚合后的数据就是最终的 RDD。
val list = List("hadoop","spark","hive","spark")
val rdd = sc.parallelize(list)
val pairRdd = rdd.map((_,1))
pairRdd.reduceByKey(_+_).collect.foreach(println)
得到:
(hive,1)
(spark,2)
(hadoop,1)
aggregateByKey
- aggregateByKey(zeroValue)(seqOp, combOp,[numTasks]):相同的Key值进行聚合操作,在聚合过程中同样使用了一个中立的初始值zeroValue:中立值,定义返回value的类型,并参与运算seqOp:用来在同一个partition中合并值combOp:用来在不同partiton中合并值。
aggregateByKey是reduceByKey的复杂版:
aggregateByKey 最重要的一点是,多提供了一个函数,Seq Function 就是说自己可以控制如何对每个 partition 中的数据进行先聚合,
类似于 mapreduce 中的,map-side combine 然后才是对所有 partition 中的数据进行全局聚合
第一个参数是,每个 key 的初始值
第二个是个函数,Seq Function,如何进行 shuffle map-side 的本地聚合
第三个是个函数,Combiner Function,如何进行 shuffle reduce-side 的全局聚合
// 创建一个rdd,分区为3
scala> val rdd = sc.parallelize(List(("a",1),("a",2),("a",3),("b",4),("b",5),("c",6)),3)
rdd: org.apache.spark.rdd.RDD[(String, Int)] = ParallelCollectionRDD[108] at parallelize at <console>:24
// 查看一下分区
scala> rdd.glom.collect
res175: Array[Array[(String, Int)]] = Array(Array((a,1), (a,2)), Array((a,3), (b,4)), Array((b,5), (c,6)))
// 进行聚合
// 第一个参数zeroValue:0,后面seqOp中的value先于zeroValue操作;
// 第二个参数seqOp:map-side也就是分区内的第一次聚合,math.max(_+_),迭代每一个分区中的所有元组的 value 值进行比较, 注意是按照分区,
//所以算出的结果就是每一个分区中每个 key 都筛选出一个最大值, 有多少个 key 就有多少个元组, 组成集合. 比如第一个分区中, 有 (a, 1) (a, 2), 那么筛选的结果就是一个 (a, 2)
// 第三个参数combOp:_+_,进行全局reduce
scala> val res = rdd.aggregateByKey(0)(math.max(_,_),_+_)
scala> res.collect
res171: Array[(String, Int)] = Array((c,6), (a,5), (b,9))
combineByKey
- combineByKey 是对 RDD 中的数据集按照 Key 进行聚合操作。聚合操作的逻辑是通过自定义函数提供给 combineByKey,把 (K,V) 类型的RDD转换为 (K,C) 类型的 RDD,C 和 V 可以不一样。
combineByKey(createCombiner: (V) => C,
mergeValue: (C, V) => C,
mergeCombiners: (C, C) => C,
numPartitions: Int): RDD[(K,C)]{}
1.createCombiner: 组合器函数,用于将 V 类型转换为 C 类型,输入参数为 RDD[K,V] 的 V, 输出为 C
2.mergeValue: 合并值函数,将一个 C 类型和一个 V 类型合并成一个 C 类型,输入参数为 (C,V),输出为 C
3.mergeCombiners: 合并组合起函数,用于将两个 C 类型值合并成一个 C 类型值,输入参数为 (C,C),输出为 C
4.numPartitions: 结果 RDD 分区数,默认保持原有的分区数
例子1:
val data = Array((1, 1.0), (1, 2.0), (1, 3.0), (2, 4.0), (2, 5.0), (2, 6.0))
val rdd = sc.parallelize(data, 2)
val combine1 = rdd.combineByKey(
// (k,v) => (k, (v,1)) 初始遍历
createCombiner = (v:Double) => (v:Double, 1),
// ((v,1),v) => ((v+v),(1+1)) 分区内
mergeValue = (c:(Double, Int), v:Double) => (c._1 + v, c._2 + 1),
// ((v,1),(v,1)) => ((v+v),(1+1)) 分区之间
mergeCombiners = (c1:(Double, Int), c2:(Double, Int)) => (c1._1 + c2._1, c1._2 + c2._2),
numPartitions = 2 )
combine1.foreach(println)
得到:
(2,(15.0,3))
(1,(6.0,3))
例子2:
val data = sc.parallelize(List((1, "www"), (1, "iteblog"), (1, "com"), (2, "bbs"), (2,
val result = data.combineByKey(
// 所有元素转为List类型,为了后面的::和:::操作
List(_),
// 分区内,相同k的v依次拼接到一个list上
(x: List [String], y: String) => y :: x,
// 分区之间,相同k的List拼接为一个
(x: List[String], y: List[String]) => x ::: y)
result.collect
得到:
Array[(Int, List[String])] = Array((1,List(www, iteblog, com)),(2,List(bbs, iteblog, com)), (3,List(good)))
例子3:
val data = sc.parallelize(List(("iteblog", 1), ("bbs", 1), ("iteblog", 3)))
val result = data.combineByKey(
// 遍历所有元素
x => x,
// 分区内相同k的v相加
(x: Int, y:Int) => x + y,
// 分区之间相同k的v相加
(x:Int, y: Int) => x + y)
result.collect
得到:
Array[(String, Int)] = Array((iteblog,4), (bbs,1))
sortByKey
- 在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD。
sortByKey 的原理:
1.shuffleRDD, 做 shuffle read, 将相同的 key 拉到一个 partition 中来
2.mapPartition, 对每个 partition 内的 key 进行全局的排序
例子:
val a = sc.parallelize(List("wyp", "iteblog", "com", "397090770", "test"), 2)
val b = sc. parallelize (1 to a.count.toInt , 2)
// ("wyp",1)("iteblog",2)("com",3)("397090770",4)("test",5)
val c = a.zip(b)
c.sortByKey().collect
得到:
Array((397090770,4), (com,3), (iteblog,2), (test,5), (wyp,1))
拓展:
sortBy可以自定义排序方式,而sortByKey根据Key进行排序,问题来了:sortByKey的排序方式可以不用默认的嘛?
答案是可以:OrderedRDDFunctions类中有个变量ordering它是隐形的:private val ordering = implicitly[Ordering[K]]。
他就是默认的排序规则,我们可以对它进行重写:
例子:
val a = sc.parallelize(List("wyp", "iteblog", "com", "397090770", "test"), 2)
val b = sc.parallelize(List(3,1,9,12,4))
// (3,"wyp")(1,"iteblog")(9,"com")(12,"397090770")(4,"test")
val c = b.zip(a)
c: org.apache.spark.rdd.RDD[(Int, String)] = ZippedPartitionsRDD2[39] at zip at c.sortByKey().collect
得到:
// 此时,使用默认排序规则,按照数字大小
Array((1,iteblog), (3,wyp), (4,test), (9,com), (12,397090770))
// 重写
implicit val sortIntegersByString = new Ordering[Int]{
override def compare(a: Int, b: Int) =
a.toString.compare(b.toString)
}
c.sortByKey().collect
得到:
// 数字按照string类型比较大小,变为了字典序排序方式
Array((1,iteblog), (12,397090770), (3,wyp), (4,test), (9,com))
sortBy
- 与sortByKey类似,但是更灵活,且依赖与sortByKey的实现。
def sortBy[K](
f: (T) => K,
ascending: Boolean = true,
numPartitions: Int = this.partitions.size)
(implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T] =
this.keyBy[K](f)
.sortByKey(ascending, numPartitions)
.values
该函数最多可以传三个参数:
第一个参数是一个函数,该函数的也有一个带T泛型的参数,返回类型和RDD中元素的类型是一致的;
第二个参数是ascending,从字面的意思大家应该可以猜到,是的,这参数决定排序后RDD中的元素是升序还是降序,默认是true,也就是升序;
第三个参数是numPartitions,该参数决定排序后的RDD的分区个数,默认排序后的分区个数和排序之前的个数相等,即为this.partitions.size。
从sortBy函数的实现可以看出,第一个参数是必须传入的,而后面的两个参数可以不传入。而且sortBy函数函数的实现依赖于sortByKey函数,关于sortByKey函数后面会进行说明。
keyBy函数也是RDD类中进行实现的,它的主要作用就是将将传进来的每个元素作用于f(x)中,并返回tuples类型的元素,也就变成了Key-Value类型的RDD了,它的实现如下:
/**
* Creates tuples of the elements in this RDD by applying `f`.
*/
def keyBy[K](f: T => K): RDD[(K, T)] = {
map(x => (f(x), x))
}
使用:
val = sc.parallelize(List(("iteblog", 1), ("bbs", 1), ("iteblog", 3)))
1.按key升序
sortBy(x => x._1)
2.按key降序
sortBy(x => x._1,false)
3.按value排序
sortBy(x => x._2)
4.调整分区
sortBy(x => x._1, 4)
5.二次排序
sortBy(x => (x._1,x._2))
join
- 在类型为 (K,V) 和 (K,W) 的RDD上调用,返回一个相同key对应的所有元素对在一起的 (K,(V,W)) 的RDD。
join 算子的原理:
1.cogroup, 聚合两个 RDD 的 key
2.flatMap, 聚合后,每条数据可能返回多条数据,将每个 key 对应两个集合做了一个笛卡儿积
本质是对两个含有KV对元素的RDD进行coGroup算子协同划分,再通过flatMapValues将合并的数据分散。
val a = sc.parallelize(List("dog", "salmon", "bird"), 2)
val b = a.keyBy(_.length) // b = List((3,"dog"),(6,"salmon"),(4,"bird"))
val c = sc.parallelize(List("dog","chep","gnu","salmon"), 2)
val d = c.keyBy(_.length) // d = List((3,"dog"),(4,"chep"),(3,"gnu"),(6,"salmon"))
b.join(d).collect
得到:
Array[(Int, (String, String))] = Array((6,(salmon,salmon)), (4,(bird,chep)), (3,(dog,dog)), (3,(dog,gnu)))
cogroup
- 将多个RDD中同一个Key对应的Value组合到一起。在类型为(K,V)和(K,W)的RDD上调用,返回一个 (K, (Iterable,Iterable)) 类型的RDD。
val a = sc.parallelize(List(1, 2, 1, 3), 1)
val b = a.map((_, "b"))
val c = a.map((_, "c"))
b.cogroup(c).collect
得到:
Array[(Int, (Iterable[String], Iterable[String]))] = Array(
(2,(ArrayBuffer(b),ArrayBuffer(c))),
(3,(ArrayBuffer(b),ArrayBuffer(c))),
(1,(ArrayBuffer(b, b),ArrayBuffer(c, c)))
)
cartesian
- 对输入RDD内的所有元素计算笛卡尔积
val x = sc.parallelize(List(1,2,3))
val y = sc.parallelize(List(6,7,8))
x.cartesian(y).collect
res0: Array[(Int, Int)] = Array((1,6), (1,7), (1,8), (2,6), (2,7), (2,8), (3,6), (3,7), (3,8),)
coalesce vs repartition
- 都是重新分区。
coalesce 原理:
coalesce 操作使用 HashPartition 进行重分区,第一个参数为重分区的数目,第二个为是否进行 shuffle,默认情况为 false:
def coalesce(numPartitions: Int, shuffle: Boolean = false)
(implicit ord: Ordering[T] = null): RDD[T]
var data = sc.textFile("...") // 假设分区数为2
var rdd1 = data.coalesce(1) // 分区数小于原分区数目,可以正常进行
var rdd2 = data.coalesce(4) // 如果分区数目大于原来分区数,必须指定shuffle为true,否则分区数不变
!!!也就是说coalesce只能用来减少分区
repartition 原理:
repartition 操作是 coalesce 函数第二个参数为 true 的实现:
1.map, 附加了前缀,根据要重分区成几个分区,计算出前缀
2.shuffleRDD -> coalesceRDD
3.去掉前缀
val rdd2 = data.repartition(4)
!!!repartition可以用来增加分区
repartitionAndSortWithinPartitions
- 重新分区和排序
repartitionAndSortWithinPartitions是Spark官网推荐的一个算子。
如果需要在repartition重分区之后,还要进行排序,建议直接使用repartitionAndSortWithinPartitions算子。
因为该算子可以一边进行重分区的shuffle操作,一边进行排序。shuffle与sort两个操作同时进行,比先shuffle再sort来说,性能可能是要高的。
Action算子
Action 行动算子:这类算子会触发 SparkContext 提交 Job 作业,并将数据输出 Spark系统。
算子 | 作用 |
reduce(func) | 通过func函数聚集RDD中的所有元素,这个功能必须是可交换且可并联的 |
collect() | 在驱动程序中,以数组的形式返回数据集的所有元素 |
count() | 返回RDD的元素个数 |
first() | 返回RDD的第一个元素(类似于take(1)) |
take(n) | 返回一个由数据集的前n个元素组成的数组 |
foreach(func) | 在数据集的每一个元素上,运行函数func进行更新。 |
countByKey() | 针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。 |
takeSample(withReplacement,num,[seed]) | 返回一个数组,该数组由从数据集中随机采样的num个元素组成,可以选择是否用随机数替换不足的部分,seed用于指定随机数生成器种子 |
takeOrdered(n, [ordering]) | takeOrdered和top类似,只不过以和top相反的顺序返回元素 |
saveAsTextFile(path) | 将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本 |
saveAsSequenceFile(path) | 将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录 下,可以使HDFS或者其他Hadoop支持的文件系统。 |
saveAsObjectFile(path) | 将RDD中的元素序列化成对象,存储到文件中 |