RDD(Resilient Distributed Dataset)
Spark 的核心是 RDD,即弹性分布数据集,通过 RDD 我们可以像操作本地集合一样以函数式编程的方式操作 RDD 这个分布式数据集,进行各种并行计算,RDD 中很多处理数据函数与列表 List 中相同与类似
RDD 的操作(函数、算子)分类
RDD Programming Guide - Spark 3.1.2 Documentation (apache.org)
① Transformation(转换操作)
返回一个新的 RDD,所有 Transformation 函数都是 Lazy,不会立即执行,需要 Action 函数触发
② Action(动作操作)
返回值不是 RDD(无返回值或返回其他的),所有 Action函数立即执行(Eager),比如count、first、collect、take等
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]) | |
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 分区 |
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 |
基本函数
map 函数
// 格式:表示将 RDD 经由某一函数 f 后,转变为另一个RDD
map(f:T=>U) : RDD[T]=>RDD[U]
// 示例:创建一个集合 [1,2,3,4],给每个集合的元素 +1,最终结果为 [2,3,4,5]
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* @author Kyle White
* @date Aug 18,2021
*/
object DemoTest {
def main(args: Array[String]): Unit = {
// 创建 SparkContext
val conf: SparkConf = new SparkConf().setAppName(this.getClass.getSimpleName.stripPrefix("$")).setMaster("local[2]")
val sc: SparkContext = new SparkContext(conf)
sc.setLogLevel("WARN")
// 创建序列并将其转换为 RDD
val seq: Seq[Int] = Seq(1, 2, 3, 4)
val inputRDD: RDD[Int] = sc.parallelize(seq, numSlices = 2)
val result: RDD[Int] = inputRDD.map(_ + 1)
// 查看结果
println(result.collect().toBuffer)
// 关闭 SparkContext
sc.stop()
}
}
flatMap 函数
先将集合中的数据 map
操作,然后将数据扁平化
// 格式:表示将 RDD 经由某一函数 f 后,转变为一个新的 RDD,但是与 map 不同,RDD 中的每一个元素会被映射成新的 0 到多个元素(f 函数返回的是一个序列 Seq)
flatMap(f:T=>Seq[U]) : RDD[T]=>RDD[U])
// 将集合 ["Hello World","Paris London","Kyle Jack"] 转换为 ["Hello","World","Paris","London","Kyle","Jack"]
// 使用 Scala 中集合 Seq 序列
val seq: Seq[String] = Seq("Hello World", "Hello Hadoop", "Hello Spark")
// 将 Scala 集合转换为 RDD(并行化)
val inputRDD:RDD[String] = sc.parallelize(seq, numSlices = 2)
val result:RDD[String] = inputRDD.flatMap(_.split(" "))
filter 函数
// 格式:表示将 RDD 经由某一函数 f 后,只保留 f 返回为 true 的数据,组成新的 RDD
filter(f:T=>Bool) : RDD[T]=>RDD[T]
// 过滤出偶数
val seq: Seq[Int] = Seq(1, 2, 3, 4)
val inputRDD: RDD[Int] = sc.parallelize(seq, numSlices = 2)
val result: RDD[Int] = inputRDD.filter(_.%(2)==0)
foreach 函数
// 格式:将函数 func 应用在数据集的每一个元素上,通常用于更新一个累加器,或者和外部存储系统进行交互
foreach(func)
saveAsTextFile 函数
// 格式:数据集内部的元素会调用其 toString 方法,转换为字符串形式,然后根据传入的路径保存成文本文件,既可以是本地文件系统,也可以是HDFS 等
saveAsTextFile(path:String)
分区操作函数
每个 RDD 由多分区组成的,实际开发建议对每个分区数据的进行操作
例如:map
函数使用 mapPartitions
代替、foreach
函数使用 foreachPartition
代替
// 示例一:对每个分区进行 map 操作
val result: RDD[(String, Int)] = inputRDD
.flatMap(_.trim.split(" "))
.mapPartitions { iter =>
iter.map((_, 1))
}
// 示例二:对每个分区数据遍历
result.foreachPartition{ iter =>
// 获取各个分区ID
val partitionId: Int = TaskContext.getPartitionId()
iter.foreach{ case (key, value) =>
println(s"p-${partitionId}: key = $key, value = $value")
}
}
重分区函数
对 RDD 中分区数目进行调整(增加分区或减少分区),有以下几个函数
① 增加分区函数
注意:此函数使用时会产生 shuffle ,使用时需谨慎
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
coalesce(numPartitions, shuffle = true)
}
② 减少分区函数
def coalesce(numPartitions: Int, shuffle: Boolean = false): RDD[T] = withScope {}
③ 调整分区函数
注意:此函数需要通过传递 分区器(Partitioner)
来改变 RDD 的分区数据
def partitionBy(partitioner: Partitioner): RDD[(K, V)] = self.withScope {
}
聚合函数
reduce 函数
def reduce(f: (T,T) => T): T
// 示例:求列表中 List 中元素之和,RDD 分区数目为 2
val inputRDD: RDD[Int] = sc.parallelize(1 to 10, numSlices = 2)
val result: Int = inputRDD.reduce(_ + _)
fold 聚合函数(可以设置初始值)
def fold(zeroValue: T)(op: (T,T)): T
// 示例:求列表中 List 中元素之和,RDD 分区数目为 2
val inputRDD: RDD[Int] = sc.parallelize(1 to 10, numSlices = 2)
val result: Int = inputRDD.fold(0)(_ + _)
aggregate 高级聚合函数
def aggregate[U: ClassTag](zeroValue: U) // 聚合中间临时变量初始值
(
// 分区内聚合函数
seqOp: (U, T) => U,
// 分区间聚合函数
combOp: (U, U) => U
): U = withScope {
// Clone the zero value since we will also be serializing it as part of tasks
// ...
}
① seqOp 函数的第一个参数是累加器,第一次执行时,会把 zeroValue
赋给累加器。第一次之后会把返回值赋给累加器,作为下一次运算的第一个参数
② seqOP 函数每个分区下的每个 key
有个累加器,combOp
函数全部分区有几个 key
就有几个累加器。如果某个 key
只存在于一个分区下,不会对他执行 combOp
函数
// 示例:求 1-10 最大的两个值
val dataRDD: RDD[Int] = sc.parallelize(1 to 10, numSlices = 2)
val top2: mutable.Seq[Int] = dataRDD.aggregate(new ListBuffer[Int]())(
// 分区内聚合函数,每个分区内数据如何聚合 seqOp: (U, T) => U,
(u, t) => {
println(s"p-${TaskContext.getPartitionId()}: u = $u, t = $t")
u += t
val top = u.sorted.takeRight(2)
top
},
// 分区间聚合函数,每个分区聚合的结果如何聚合 combOp: (U, U) => U
(u1, u2) => {
println(s"p-${TaskContext.getPartitionId()}: u1 = $u1, u2 = $u2")
u1 ++= u2
u1.sorted.takeRight(2)
}
)
PairRDDFunctions 聚合函数
在 Spark
中有一个 object
对象 PairRDDFunctions
,主要针对 RDD
的数据类型是 Key/Value
对的数据提供函数,方便数据分析处理
① 分组函数 groupByKey
将相同的 key
的 value
组合在一起,所有的 value
存储在 Iterable
中
# 此函数可能出现的问题
1.数据倾斜
当某个 key 对应的 value 的值非常多的时候,迭代器中的数据会非常多
2.OOM(内存溢出)
综合:在 SparkCore 开发中,能不用 groupByKey 就尽量别用
val linesSeq: Seq[String] = Seq("hello world test", "java python scala", "jack kyle", "hello")
val inputRDD: RDD[String] = sc.parallelize(linesSeq, numSlices = 2)
val wordsRDD: RDD[(String, Int)] = inputRDD.flatMap(_.split(" ")).map((_, 1))
val wordsGroupRDD: RDD[(String, Iterable[Int])] = wordsRDD.groupByKey()
val resultRDD: RDD[(String, Int)] = wordsGroupRDD.mapValues(_.sum)
② 分组聚合函数 reduceByKey 和 foldByKey
val linesSeq: Seq[String] = Seq("hello world test", "java python scala", "jack kyle", "hello")
val inputRDD: RDD[String] = sc.parallelize(linesSeq, numSlices = 2)
val wordsRDD: RDD[(String, Int)] = inputRDD.flatMap(_.split(" ")).map((_, 1))
val resultRDD: RDD[(String, Int)] = wordsRDD.reduceByKey(_ + _)
③ 分组聚合函数 aggregateByKey
val linesSeq: Seq[String] = Seq("hello world test", "java python scala", "jack kyle", "hello")
val inputRDD: RDD[String] = sc.parallelize(linesSeq, numSlices = 2)
val wordsRDD: RDD[(String, Int)] = inputRDD.flatMap(_.split(" ")).map((_, 1))
val resultRDD = wordsRDD.aggregateByKey(0)(
(tmp: Int, item: Int) => {
tmp + item
},
(tmp: Int, result: Int) => {
tmp + result
}
)
groupByKey 和 reduceByKey 的区别
① 返回值类型不同
reduceByKey 返回的是 RDD[(K, V)],而 groupByKey 返回的是 RDD[(K, Iterable[V])]
② 过程不同
reduceByKey 先分区内聚合然后最后再做一次聚合,groupByKey 先分区内分组,最后再做聚合,IO 比较高
关联函数
当两个 RDD
的数据类型为二元组 Key/Value
对时,可以依据 Key
进行关联 Join
def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]
def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]
def rightOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (Option[V], W))]
// 示例
// 模拟数据集
val empRDD: RDD[(Int, String)] = sc.parallelize(
Seq((1001, "Kyle"), (1002, "Jack"), (1003, "Lucy"), (1004, "Sam"))
)
val deptRDD: RDD[(Int, String)] = sc.parallelize(
Seq((1001, "tec"), (1002, "fac"))
)
val joinRDD: RDD[(Int, (String, String))] = empRDD.join(deptRDD)
println(joinRDD.collectAsMap())
val leftJoinRDD: RDD[(Int, (String, Option[String]))] = empRDD.leftOuterJoin(deptRDD)
println(leftJoinRDD.collectAsMap())
关联函数
当两个 RDD
的数据类型为二元组 Key/Value
对时,可以依据 Key
进行关联 Join
def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]
def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]
def rightOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (Option[V], W))]
// 示例
// 模拟数据集
val empRDD: RDD[(Int, String)] = sc.parallelize(
Seq((1001, "Kyle"), (1002, "Jack"), (1003, "Lucy"), (1004, "Sam"))
)
val deptRDD: RDD[(Int, String)] = sc.parallelize(
Seq((1001, "tec"), (1002, "fac"))
)
val joinRDD: RDD[(Int, (String, String))] = empRDD.join(deptRDD)
println(joinRDD.collectAsMap())
val leftJoinRDD: RDD[(Int, (String, Option[String]))] = empRDD.leftOuterJoin(deptRDD)
println(leftJoinRDD.collectAsMap())
排序函数
① sortByKey
针对 RDD
中数据类型 key/value 对
时,按照 Key
进行排序
val inputRdd: RDD[String] = sc.parallelize(Seq("hello world test", "java python scala", "jack scala", "hello"))
// sortByKey
val sortedRDD: RDD[(Int, String)] = inputRdd.flatMap(_.split("\\s+"))
.map((_, 1))
.reduceByKey(_ + _)
.map(_.swap) // 交换
.sortByKey(ascending = false) // 降序
sortedRDD.take(3).foreach(println) // 只取前 3
② sortBy
针对 RDD
中数据指定排序规则
val inputRdd: RDD[String] = sc.parallelize(Seq("hello world test", "java python scala", "jack scala", "hello"))
val sortedByRDD:RDD[(String, Int)] = inputRdd.flatMap(_.split("\\s+"))
.map((_, 1))
.reduceByKey(_ + _)
.sortBy(_._2, ascending = false) // 降序
sortedByRDD.take(3).foreach(println)
③ top
按照 RDD
中数据采用降序方式排序,如果是 Key/Value对
,按照 Key
降序排序
val inputRdd: RDD[String] = sc.parallelize(Seq("hello world test", "java python scala", "jack scala", "hello"))
val topRDD:Array[(String,Int)] = inputRdd.flatMap(_.split("\\s+"))
.map((_, 1))
.reduceByKey(_ + _)
.top(3)(Ordering.by(_._2))
topRDD.foreach(println)
交集、并集、差集
并集
// 初始化数据
val seq1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4))
val seq2: RDD[Int] = sc.parallelize(List(5, 6, 3, 4))
// 并集
val unionResult: RDD[Int] = seq1.union(seq2)
println(unionResult.collect().toBuffer)
交集
// 初始化数据
val seq1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4))
val seq2: RDD[Int] = sc.parallelize(List(5, 6, 3, 4))
val intersectionResult:RDD[Int] = seq1.intersection(seq2)
println(intersectionResult.collect().toBuffer)
差集
// 初始化数据
val seq1: RDD[Int] = sc.parallelize(List(1, 2, 3, 4))
val seq2: RDD[Int] = sc.parallelize(List(5, 6, 3, 4))
val subtractResult1: RDD[Int] = seq1.subtract(seq2)
println(subtractResult1.collect().toBuffer)
val subtractResult2: RDD[Int] = seq2.subtract(seq1)
println(subtractResult2.collect().toBuffer)
笛卡尔积
// 笛卡尔积
val seq3: RDD[String] = sc.parallelize(List("dept1", "dept2"))
val seq4: RDD[String] = sc.parallelize(List("Jack", "Sam", "Lisa"))
val cartesianResult: RDD[(String, String)] = seq3.cartesian(seq4)
println(cartesianResult.collect().toBuffer)