Spark学习笔记——SparkCore核心编程之RDD算子
文章目录
一、概念
RDD (Resilient Distributed Dataset) 叫做弹性分布式数据集,是 Spark 中最基本的数据 处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行 计算的集合。
二、特点
➢ 弹性
存储的弹性:内存与磁盘的自动切换;
容错的弹性:数据丢失可以自动恢复;
计算的弹性:计算出错重试机制;
分片的弹性:可根据需要重新分片。
➢ 分布式:数据存储在大数据集群不同节点上
➢ 数据集:RDD 封装了计算逻辑, 并不保存数据
➢ 数据抽象: RDD 是一个抽象类, 需要子类具体实现
➢ 不可变:RDD 封装了计算逻辑, 是不可以改变的, 想要改变, 只能产生新的 RDD,在新的 RDD 里面封装计算逻辑
➢ 可分区、并行计算
三、核心属性
RDD 有五大核心属性,分别是
1)分区列表:RDD 数据结构中存在分区列表, 用于执行任务时并行计算, 是实现分布式计算的重要属性。
2)分区计算函数:Spark 在计算时, 是使用分区函数对每一个分区进行计算
3)RDD 之间的依赖关系:RDD 是计算模型的封装,当需求中需要将多个计算模型进行组合时,就需要将多个 RDD 建 立依赖关系
4)分区器:当数据为 KV 类型数据时,可以通过设定分区器自定义数据的分区
5)首选位置:计算数据时,可以根据计算节点的状态选择不同的节点位置进行计算
四、基础编程
1.RDD的创建
1.1从集合(内存)中创建
方法:parallelize 和 makeRDD
val rdd1 = sparkContext.parallelize (
List(1,2,3,4)
)
val rdd2 = sparkContext.makeRDD (
List(1,2,3,4)
)
从底层代码实现来讲,makeRDD 方法其实就是 parallelize 方法。
1.2从外部存储(文件) 创建
RDD还可以从本地的文件系统,所有 Hadoop 支持的数据集,比如 HDFS 、HBase 等等这些外部存储系统的数据集中创建。
读取文件数据时,数据是按照 Hadoop 文件读取的规则进行切片分区
val fileRDD: RDD[String] = sparkContext.textFile ("input/word.txt")
1.3从其他RDD创建
通过一个 RDD 运算完后, 再产生新的 RDD。
1.4直接创建
使用 new 的方式直接构造 RDD,一般由 Spark 框架自身使用。
2.RDD并行度与分区
Spark 可以将一个作业切分多个任务后,发送给 Executor 节点并行计算,而能够并行计算的任务数量我们称之为并行度。这个数量可以在构建 RDD 时指定。这个数量可以在构建 RDD 时指定。注意, 这里的并行执行的任务数量, 并不是指的切分任务的数量,不要混淆了。
读取内存数据时,数据可以按照并行度的设定进行数据的分区操作; 读取文件数据时,数据是按照 Hadoop 文件读取的规则进行切片分区,而切片规则和数据读取的规则有些差异
val dataRDD: RDD[Int] = sparkContext.makeRDD (List(1,2,3,4), 4)
上面的代码就指定了4个并行度,即分区数为4。
3.RDD转换算子
RDD 根据数据处理方式的不同将算子整体上分为 Value 类型、双 Value 类型和 Key-Value 类型
3.1 Value 类型
3.1.1 map
签名函数
def map[U: ClassTag](f: T => U): RDD[U]
其中f: T => U 就是我们对数据进行的操作。将处理的数据逐条进行映射转换,这里的转换可以是类型的转换, 也可以是值的转换。
val dataRDD1: RDD[Int] = dataRDD.map (
num => {
num * 2
}
)
上面的代码就是对每个数据的值乘以2
3.1.2 mapPartitions
签名函数
def mapPartitions[U: ClassTag](
f: Iterator[T] => Iterator[U],
preservesPartitioning: Boolean = false): RDD[U]
可以看到,mapPartitions 算子需要传递一个迭代器,返回一个迭代器,没有要求的元素的个数保持不变, 所以可以增加或减少数据。将待处理的数据以分区为单位发送到计算节点进行处理,这里的处理是指可以进行任意的处理,哪怕是过滤数据。
val dataRDD1: RDD[Int] = dataRDD.mapPartitions (
datas => {
datas.filter (_==2)
}
)
上面的代码就是对等于2的数据过滤
注意:
mapPartitions 算子 是以分区为单位进行批处理操作,所以性能较高。但是 mapPartitions 算子会长时间占用内存,那么这样会导致内存可能不够用,出现内存溢出的错误。在内存有限的情况下,不推荐使用。
3.1.3 mapPartitionsWithIndex
签名函数
def mapPartitionsWithIndex[U: ClassTag](
f: (Int, Iterator[T]) => Iterator[U],
preservesPartitioning: Boolean = false): RDD[U]
可以看到它与mapPartitions类似,但是传入参数多了一个索引,所以它在mapPartitions的功能的基础上,可以获取当前分区索引。
3.1.4 flatMap
签名函数
def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U]
将处理的数据进行扁平化后再进行映射处理, 所以flatMap算子也称之为扁平映射
val value: RDD[List[Int]] = sc.makeRDD(List(
List(1, 2), 3 ,List(3, 4)
), 1)
val dataRDD: RDD[Int] = value.flatMap(
data =>{
data match {
case list: List[_] => list
case dat => List(dat)
}
}
)
上面的代码就是将 List(1, 2), 3,List(4,5) 拆成1,2,3,4,5
3.1.5 glom
函数签名
def glom(): RDD[Array[T]]
将同一个分区的数据直接转换为相同类型的内存数组进行处理,分区不变。
// 计算所有分区最大值求和 (分区内取最大值,分区间最大值求和)
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
// Int => Array[Int]
val glomrdd: RDD[Array[Int]] = rdd.glom()
val maxrdd: RDD[Int] = glomrdd.map(
array => {
array.max
}
)
println(maxrdd.collect().sum)
输出结果:6
3.1.6 groupBy
函数签名
def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])]
将数据根据指定的规则进行分组, 分区默认不变, 但是数据会被打乱重新组合,这就是 shuffle。极限情况下, 数据可能被分在同一个分区中。
一个组的数据在一个分区中,但是并不是说一个分区中只有一个组
val dataRDD = sparkContext.makeRDD(List(1,2,3,4),1)
val dataRDD1 = dataRDD.groupBy(
_%2
)
上面的代码就是将偶数分到一组
3.1.7 filter
函数签名
def filter(f: T => Boolean): RDD[T]
将数据根据指定的规则进行筛选过滤,符合规则的数据保留, 不符合规则的数据丢弃。
当数据进行筛选过滤后, 分区不变, 但是分区内的数据可能不均衡,生产环境下, 可能会出
现数据倾斜。
val dataRDD = sparkContext.makeRDD(List(
1,2,3,4
),1)
val dataRDD1 = dataRDD.filter(_%2 == 0) // 将偶数保留,奇数过滤
3.1.8 sample
签名函数
def sample(
withReplacement: Boolean, // 抽取完之后是否放回去
fraction: Double, // 分子比率
seed: Long = Utils.random.nextLong // 随机数种子,如果不传递就用系统时间作为种子
): RDD[T]
根据指定的规则从数据集中抽取数据,在特殊场合使用,比如可以用来检验数据倾斜。
val dataRDD = sparkContext.makeRDD(List(
1,2,3,4
),1)
// 不放回的情况:第二个参数:抽取的几率,范围在[0,1]之间,0:全不取; 1:全取;
val dataRDD1 = dataRDD.sample(false, 0.5)
// 第二个参数:重复数据的几率,范围大于等于 0.表示每一个元素被期望抽取到的次数
val dataRDD2 = dataRDD.sample(true, 2)
3.1.9 distinct
函数签名
def distinct()(implicit ord: Ordering[T] = null): RDD[T]
def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
用来将数据集中重复的数据去重
Scala集合中的 distinct 方法的底层用到了 HashSet 的数据结构来实现去重
而RDD中的 distinct 方法的底层原理:
先用 map 将每个数据变成( data, null )这样的元组
在用 reduceByKey 将相同的数据聚合
最后在用 map 将( data, null )变成data
3.1.10 coalesce
函数签名
def coalesce(
numPartitions: Int, // 重新分区的数量
shuffle: Boolean = false, // 是否进行shuffle操作
partitionCoalescer: Option[PartitionCoalescer] = Option.empty
)(implicit ord: Ordering[T] = null): RDD[T]
可以根据数据量缩减分区,常用于大数据集过滤后, 提高小数据集的执行效率
当 spark 程序中, 存在过多的小任务的时候,可以通过 coalesce 方法,收缩合并分区,减少
分区的个数,减小任务调度成本
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4, 5, 6), 3)
// 默认情况下数据不会被打乱重组,可能会导致数据倾斜
val newRDD1: RDD[Int] = rdd.coalesce(2)
// 指定第二个参数,即 shuffle=ture,数据均衡但全部打乱
val newRDD2: RDD[Int] = rdd.coalesce(2, true)
注意,coalesce 是可以用来扩大分区的,此时必须设置 shuffle=ture 否则没有意义。
3.1.11 repartition
函数签名
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
这是 Spark 提供的一个化简操作,用来扩大分区,底层代码调用的就是 coalesce 且写死必须 shuffle,用法与 coalesce 一样。
3.1.12 sortBy
函数签名
def sortBy[K](
f: (T) => K,
ascending: Boolean = true,
numPartitions: Int = this.partitions.length
)(implicit ord: Ordering[K],
ctag: ClassTag[K]): RDD[T]
sortBy 方法可以根据指定的规则 f: (T) => K 来对数据进行排序,默认为升序,第二个参数为 false 时为降序。默认情况下,不会改变分区,但是中间存在 shuffle 操作。
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("1", 1), ("11", 2), ("2", 3)), 2)
rdd.sortBy(t => t._1.toInt, false)
上面的代码是按 (“1”, 1) 的字符串里面的数字进行降序排序。
3.2 双Value类型
intersection、union、subtract、zip
函数签名
def intersection(other: RDD[T]): RDD[T] // 交集
def union(other: RDD[T]): RDD[T] // 并集,重复的地方算两次
def subtract(other: RDD[T]): RDD[T] // 差集
def zip[U: ClassTag](other: RDD[U]): RDD[(T, U)] // 拉链,将相同位置配对
对源 RDD 和参数 RDD 进行计算后返回一个新的 RDD。
交集,并集,差集数据类型必须一致;
拉链不要求数据类型,但两个数据源的分区数量要保持一致,每个分区的数据数量也要保持一致。
val dataRDD1 = sparkContext.makeRDD(List(1,2,3,4))
val dataRDD2 = sparkContext.makeRDD(List(3,4,5,6))
val dataRDD3 = dataRDD1.intersection (dataRDD2) // 3,4
val dataRDD4 = dataRDD1.union (dataRDD2) // 1,2,3,4,3,4,5,6
val dataRDD5 = dataRDD1.subtract (dataRDD2) // 1,2
val dataRDD6 = dataRDD1.zip (dataRDD2) // (1,3),(2,4)(3,5),(4,6)
3.3Key-Value类型
3.3.1 partitionBy
函数签名
def partitionBy(partitioner: Partitioner): RDD[(K, V)]
将数据(必须是 K-V 类型)按照指定 Partitioner 对数据重新进行分区。 Spark 默认的分区器是 HashPartitioner 即按照(key % 分区数) 的值来重分区。
val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1,"aaa"),(2,"bbb"),(3,"ccc")),3)
val rdd2: RDD[(Int, String)] = rdd.partitionBy(new HashPartitioner(2))
3.3.2 reduceByKey
函数签名
def reduceByKey(func: (V, V) => V): RDD[(K, V)]
def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]
可以将数据按照相同的 Key 对 Value 进行聚合
val dataRDD1 = sparkContext.makeRDD(List(("a",1),("a",2),("b",3)))
val dataRDD2 = dataRDD1.reduceByKey( _+_ ) // (a,3) (b,3)
val dataRDD3 = dataRDD1.reduceByKey( _+_, 2) // 一样,但会重新设定分区数为2
3.3.3 groupByKey
函数签名
def groupByKey(): RDD[(K, Iterable[V])]
def groupByKey(numPartitions: Int): RDD[(K, Iterable[V])] // numPartitions,这个参数用于指定分区的数量
def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])] // 可以使用自定义的分区策略:partitioner对象
将数据源中的数据,相同Key的数据分在一个组中,形成一个对偶元组,元组中的第一个元素就是key,第二个元素就是相同key的value集合
val dataRDD1 = sparkContext.makeRDD(List(("a",1),("a",2),("b",3)))
val dataRDD2 = dataRDD1.groupByKey () // ("a",(1,2)), ("b",3)
val dataRDD3 = dataRDD1.groupByKey (2)
val dataRDD4 = dataRDD1.groupByKey (new HashPartitioner(2))
!!!groupByKey 和 reduceByKey 的区别:
1)从功能的角度:groupByKey 只能进行分组操作,无法对数据进行聚合,想要聚合数据需要借助 map;而 reduceByKey 能对数据进行分组,还能对相同key的value值进行聚合操作。所以在分组聚合的场合下,推荐使用 reduceByKey,如果仅仅是分组而不需要聚合,那 么还是只能使用 groupByKey。
2)从性能的角度:两者都要打乱数据并重组,都需要进行 shuffle,但是性能上有差异。在Spark中,shuffle 操作必须落盘处理(写入磁盘),不能在内存中进行数据等待,否则会导致内存溢出。既然要落盘,就涉及到磁盘IO,性能会受到影响。但是,reduceByKey 支持分区内预聚合功能(Combine),可以有效减少 Shuffle 时落盘的数据量。所以 reduceByKey 性能比较高。
3.3.4 aggregateByKey
函数签名
def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U,combOp: (U, U) => U): RDD[(K, U)]
可以看到,aggregateByKey 存在函数柯里化,有两个参数列表
1)第一个参数列表需要传递一个参数:表示初始值,主要用于当碰见第一个key的时候,和value进行分区内计算
2)第二个参数列表需要传递两个参数:第一个表示分区内计算规则,第二个表示分区间计算规则
reduceByKey 在分区内和分区间的计算规则是相同的, aggregateByKey 可以将数据根据不同的规则进行分区内计算和分区间计算
// TODO : 获取相同key的数据的平均值
val rdd = sc.makeRDD(List(
("a", 1), ("a", 2), ("b", 3),
("b", 5), ("b", 4), ("a", 6)
), 2)
// aggregateByKey的最终返回值类型:key保持不变,value和初始值的类型保持一致
// aggregateByKey的最终返回值由初始值决定
val newRDD: RDD[(String, (Int, Int))] = rdd.aggregateByKey((0, 0))(
(tuple, v) => { // 分区内计算规则
// tuple表示传进来的初始值(0,0),v表示每个相同key数据的value值
// 第一个0用来计算累加的和,第二个0用来计算出现次数
(tuple._1 + v, tuple._2 + 1)
},
(t1, t2) => { // 分区间计算规则
// t1、t2是两个分区经过上面计算分别得到的结果
// 如,对a来说t1是(3,2) t2是(6,1),计算后得到(9,3),最终返回值为("a",(9,3))
(t1._1 + t2._1, t1._2 + t2._2)
}
)
val resultRDD: RDD[(String, Int)] = newRDD.mapValues {
case (num, cnt) => {
num / cnt
}
}
resultRDD.collect().foreach(println) // 输出结果:(a,3) (b,4)
3.3.5 foldByKey
函数签名
def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]
当分区内计算规则和分区间计算规则相同时, aggregateByKey 就可以简化为 foldByKey
val dataRDD1 = sparkContext.makeRDD(List(("a",1),("b",2),("a",3)))
val dataRDD2 = dataRDD1.foldByKey(0)(_+_) // 输出:(a,4) (b,2)
3.3.6 combineByKey
函数签名
def combineByKey[C](
createCombiner: V => C, // 第一个参数:对相同key的第一个数据进行的结构的转换
mergeValue: (C, V) => C, // 第二个参数:分区内的计算规则
mergeCombiners: (C, C) => C): RDD[(K, C)] // 第三个参数:分区间的计算规则
最通用的对 key-value 型 rdd 进行聚集操作的聚集函数(aggregation function) 。类似于 aggregate() ,combineByKey() 但允许用户返回值的类型与输入不一致。
// TODO : 取出每个分区内相同 key 的最大值然后分区间相加
val rdd = sc.makeRDD(List(
("a", 1), ("a", 2), ("b", 3),
("b", 5), ("b", 4), ("a", 6)
), 2)
// combineByKey方法可以将计算数据的第一个值变化
rdd.combineByKey(
v => (v, 1), // 对相同key的第一个数据进行的结构的转换
(tuple:(Int,Int), v) => { // 分区内计算规则
(tuple._1 + v, tuple._2 + 1)
},
(t1:(Int,Int), t2:(Int,Int)) => { // 分区间计算规则
(t1._1 + t2._1, t1._2 + t2._2)
}
).mapValues {
case (num, cnt) => {
num / cnt
}
}
.collect().foreach(println) // 输出结果依然是 (a,3) (b,4)
!!!四个聚合算子的区别:
reduceByKey: 相同 key 的第一个数据不进行任何计算,分区内和分区间计算规则相同。
FoldByKey: 相同 key 的第一个数据和初始值进行分区内计算, 分区内和分区间计算规则相同。
AggregateByKey:相同 key 的第一个数据和初始值进行分区内计算,分区内和分区间计算规则可以不相同。
CombineByKey:当计算时,发现数据结构不满足要求时, 可以让第一个数据转换结构。分区内和分区间计算规则可以不相同。
3.3.7 join
函数签名
def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]
在类型为 (K,V) 和 (K,W) 的 RDD 上调用, 返回一个相同 key 对应的所有元素连接在一起的 (K,(V,W)) 的 RDD。如果两个数据源中有的数据的 key 没有匹配上那么它不会出现在结果中。在数据源中如果有多个相同的 key,则会依次匹配,可能会出现笛卡尔乘积,数据量几何性增长,导致性能降低。
val rdd: RDD[(Int, String)] = sc.makeRDD(Array((1, "a"), (2, "b"), (3, "c")))
val rdd1: RDD[(Int, Int)] = sc.makeRDD(Array((1, 4), (2, 5), (3, 6)))
rdd.join(rdd1).collect().foreach(println) // 输出:(1,(a,4)) (2,(b,5)) (3,(c,6))
3.3.8 leftOuterJoin
函数签名
def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]
类似于 SQL 语句的左外连接(右外连接 rightOuterJoin 同理)
val dataRDD1 = sparkContext.makeRDD(
List(("a", 1), ("b", 2), ("c", 3)
))
val dataRDD2 = sparkContext.makeRDD(
List(("a", 3), ("b", 5) //,("c",7)
))
val rdd: RDD[(String, (Int, Option[Int]))] = dataRDD1.leftOuterJoin(dataRDD2)
rdd.collect().foreach(println) // 输出结果:(a,(1,Some(3))) (b,(2,Some(5))) (c,(3,None))
3.3.9 cogroup
函数签名
def cogroup[W](other: RDD[(K, W)]): RDD[(K, (Iterable[V], Iterable[W]))]
在类型为 (K,V) 和 (K,W) 的 RDD 上调用, 返回一个 (K,(Iterable<V>,Iterable<W>)) 类型的 RDD。可以将 cogroup 理解成 connect + group (分组链接)
val dataRDD1 = sparkContext.makeRDD(
List(("a", 1), ("b", 2)//, ("c", 3)
))
val dataRDD2 = sparkContext.makeRDD(
List(("a", 3), ("b", 5), ("c", 7), ("c", 9)
))
val rdd: RDD[(String, (Iterable[Int], Iterable[Int]))] = dataRDD1.cogroup(dataRDD2)
rdd.collect().foreach(println)
// 输出结果:
// (a,(CompactBuffer(1),CompactBuffer(3)))
// (b,(CompactBuffer(2),CompactBuffer(5)))
// (c,(CompactBuffer(),CompactBuffer(7, 9)))
3.4 案例实操
3.4.1 数据准备
agent.log:时间戳, 省份,城市, 用户,广告, 中间字段使用空格分隔。如下所示:
1516609143867 6 7 64 16
1516609143869 9 4 75 18
1516609143869 1 7 87 12
1516609143869 2 8 92 9
......
3.4.2 案例需求
统计出每一个省份每个广告被点击数量排行的 Top3
3.4.3 需求分析
步骤如下:
1、获取原始数据
2、将原始数据进行结构的转化,方便统计 时间戳,省份,城市,用户,广告 =>((省份,广告),1)
3、将转换结构后的数据,进行分组聚合 ((省份,广告),1)=>((省份,广告),sum)
4、将聚合的结果进行结构的转化 ((省份,广告),sum)=>(省份,(广告,sum))
5、将转换结构后的数据根据省份进行分组 (省份A,(广告A,sumA)),(省份A,(广告B,sumB))......
6、将分组后的数据进行组内排序(降序),取前三名
7、采集数据并打印在控制台
3.4.5 代码实现
package operator.transform
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object Requirement {
def main(args: Array[String]): Unit = {
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Opertaor")
val sc = new SparkContext(sparkConf)
// TODO 案例实操:统计出每一个省份每个广告被点击数量排行的 Top3
// agent.log数据格式:时间戳,省份,城市,用户,广告 中间字段使用空格分隔。
// 1、获取原始数据
val dataRDD: RDD[String] = sc.textFile("input/agent.log")
// 2、将原始数据进行结构的转化,方便统计(缺什么,补什么,多什么,删什么)
// 时间戳,省份,城市,用户,广告
// =>((省份,广告),1)
dataRDD.map(
line => {
val strings: Array[String] = line.split(" ")
Tuple2(Tuple2(strings(1), strings(4)), 1)
}
)
// 3、将转换结构后的数据,进行分组聚合
// ((省份,广告),1)=>((省份,广告),sum)
.reduceByKey(_ + _)
// 4、将聚合的结果进行结构的转化
// ((省份,广告),sum)=>(省份,(广告,sum))
/*
.map(
t => {
Tuple2(t._1._1, Tuple2(t._1._2, t._2))
}
)
*/// 这里可以用模式匹配来优化
.map {
case ((prv, ad), sum) => {
(prv, (ad, sum))
}
}
// 5、将转换结构后的数据根据省份进行分组
// (省份A,(广告A,sumA)),(省份A,(广告B,sumB))......
.groupByKey()
// 6、将分组后的数据进行组内排序(降序),取前三名
/*
.map(
t =>{
t._2.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
*/// 这里可以用mapValues来优化
.mapValues(
iter => {
iter.toList.sortBy(_._2)(Ordering.Int.reverse).take(3)
}
)
// 7、采集数据并打印在控制台
.collect().foreach(println)
}
}
输出结果如下:
(4,List((12,25), (2,22), (16,22)))
(8,List((2,27), (20,23), (11,22)))
(6,List((16,23), (24,21), (22,20)))
(0,List((2,29), (24,25), (26,24)))
(2,List((6,24), (21,23), (29,20)))
(7,List((16,26), (26,25), (1,23)))
(5,List((14,26), (21,21), (12,21)))
(9,List((1,31), (28,21), (0,20)))
(3,List((14,28), (28,27), (22,25)))
(1,List((3,25), (6,23), (5,22)))
可以看到,每个省份的广告点击量的前三名都被打印出来了,满足需求。
4. RDD行动算子
所谓行动算子,其实就是触发作业(job)执行的方法,底层代码调用的就是 runJob方法。
4.1 reduce
函数签名
def reduce(f: (T, T) => T): T
聚集 RDD 中的所有元素,先聚合分区内数据, 再聚合分区间数据
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 聚合数据
val reduceResult: Int = rdd.reduce(_+_)
println(reduceResult) // 输出:10
4.2 collect
函数签名
def collect(): Array[T]
方法会将不同分区的数据按照分区顺序采集到 Driver 段内存中,形成数组(以数组 Array 的形式返回数据集的所有元素)。
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 收集数据到 Driver
rdd.collect().foreach(println) // 输出:1 2 3 4
4.3 count
函数签名
def count(): Long
顾名思义,它可以统计并返回 RDD 中元素的个数
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 返回 RDD 中元素的个数
val countResult: Long = rdd.count()
println(countResult) // 输出:4
4.4 first
函数签名
def first(): T
也是顾名思义,获取 RDD 中的第一个元素
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 返回 RDD 中元素的个数
val firstResult: Int = rdd.first()
println(firstResult) // 输出:1
4.5 take
函数签名
def take(num: Int): Array[T]
用来获取前 n 个数据,返回一个由 RDD 的前 n 个元素组成的数组
vval rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 返回 RDD 中元素的个数
val takeResult: Array[Int] = rdd.take(3)
println(takeResult.mkString(",")) // 输出:1,2,3
4.6 takeOrdered
函数签名
def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T]
将数据排序之后,取前 n 个(想要降序的话,将第二个参数,传 Ordering.Int.reverse )
val rdd: RDD[Int] = sc.makeRDD(List(4,2,3,1))
// 返回 RDD 中元素的个数
val result: Array[Int] = rdd.takeOrdered(3)(Ordering.Int.reverse)
println(result.mkString(",")) // 输出:4,3,2
4.7 aggregate
函数签名
def aggregate[U: ClassTag](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): U
aggregate 可以将分区的数据通过初始值和分区内的数据进行聚合, 然后再和初始值进行分区间的数据聚合
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4), 2)
// 将该 RDD 所有元素相加得到结果
val result: Int = rdd.aggregate(10)(_ + _, _ + _) // 输出:40
!!!aggregate 和 aggregateByKey 的区别
aggregate:行动算子,返回值类型就是计算结果,初始值会参与分区内计算,并且参与分区间计算。
aggregateByKey:转换算子,返回值类型是一个 RDD 类型数值,初始值会参与分区内计算,但不会参与分区间计算
4.8 fold
函数签名
def fold(zeroValue: T)(op: (T, T) => T): T
与 foldByKey 是 aggregateByKey 的简化版同理, fold 就是 aggregate 的简化版
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4),2)
val foldResult: Int = rdd.fold(0)(_+_) // 输出:40
4.9 countByKey
函数签名
def countByKey(): Map[K, Long]
统计每种 key 的个数,注意,跟 key 所对应的 value 值无关,返回值是一个 Map 类型
val rdd: RDD[(String, Int)] = sc.makeRDD(List(
("a", 1), ("a", 2), ("a", 3)
))
val stringToLong: collection.Map[String, Long] = rdd.countByKey()
println(stringToLong) // 输出:Map(a -> 3)
4.10 countByValue
函数签名
def countByValue() : Map[T, Long]
与 countByKey 功能类似,用来统计相同的数据值出现的次数,注意,这里的 Value 并不是指键值对中的 Value,而是直接指数据
val rdd: RDD[Int] = sc.makeRDD(List(1, 1, 1, 4), 2)
val intToLong: collection.Map[Int, Long] = rdd.countByValue()
println(intToLong) // 输出:Map(4 -> 1, 1 -> 3)
4.11 save相关算子
函数签名
def saveAsTextFile(path: String): Unit
def saveAsObjectFile(path: String): Unit
def saveAsSequenceFile(
path: String,
codec: Option[Class[_ <: CompressionCodec]] = None): Unit
save 相关算子的功能是,将数据保存到到不同格式的文件中,但要注意的是,saveAsSequenceFile 方法要求数据的格式必须为 K-V 类型
// 保存成 Text 文件
rdd.saveAsTextFile ("output")
// 序列化成对象保存到文件
rdd.saveAsObjectFile ("output1")
// 保存成 Sequencefile 文件
rdd.map ((_,1)).saveAsSequenceFile ("output2")
4.12 foreach
函数签名
def foreach(f: T => Unit): Unit = withScope {
val cleanF = sc.clean(f)
sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
}
分布式遍历 RDD 中的每一个元素,调用指定函数
val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
// 收集后打印
// 这里的 foreach 其实是Driver端内存集合的循环遍历方法(有序)
rdd.map (num=>num).collect().foreach(println) // 输出:1 2 3 4
println("****************")
// 分布式打印
// 这里的 foreach 是Executor端内存数据打印(乱序)
rdd.foreach (println) // 输出:2 1 3 4
!!!概念补充:为什么 RDD 的方法被称为算子(Operator)!!!
RDD 的方法和 Scala 中集合对象的方法不一样:集合对象的方法都是在同一个节点的内存中完成的;而 RDD 的方法可以以将计算逻辑发到 Executor 端(分布式节点)中执行。为了区分不同的执行效果,所以将 RDD 的方法称为算子。RDD 的方法外部操作都是在 Driver 端执行的,而方法内部的逻辑代码在 Executor 端执行。
5. RDD序列化
5.1 闭包检测
上面我们提到了计算的节点分 Driver 端和 Executor 端,算子外的代码在 Driver 端执行, 算子里面的代码都是在 Executor 端执行。经常会出现算子内调用算子外的数据的现象,这就意味着数据要进行网络传输,所以必须要将数据序列化
而在 scala 的函数式编程中,在算子内调用算子外的数据,这样就形成了闭包的效果,如果使用的算子外的数据无法序列化,就意味着无法传值给 Executor 端执行,就会发生错误,所以需要在执行任务计算前, 检测闭包内的对象是否可以进行序列化,这个操作我们称之为闭包检测
5.2 序列化方法和属性
来看案例:
package serializable
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object Serializable01 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("Serial")
val sc = new SparkContext(conf)
val rdd: RDD[String] = sc.makeRDD(List("hello world", "hello spark", "hadoop"))
// 想从数据源中查询符合要求的数据
val search: Search = new Search("spark")
search.getMatch1(rdd).collect().foreach(println)
sc.stop()
}
// 自定义查询对象
class Search(query: String) {
def isMatch(s: String): Boolean = {
s.contains(query)
}
// 函数序列化案例
// getMatch1 采用外部函数
def getMatch1(rdd: RDD[String]): RDD[String] = {
rdd.filter(isMatch)
}
}
}
运行后出现报错:
Task not serializable // 任务没有序列化
java.io.NotSerializableException: serializable.Serializable01$Search // Search 这个对象没有序列化
思考:
getMatch1 调用了 isMatch 函数,使用了 query 这个参数,但是 query 是字符串,本身已经序列化,为什么还会报错
尝试加上extends Serializable ,再次运行,成功不报错,输出结果:
hello spark
原因:
这是因为在 Scala 中类的构造器中的参数其实是类的属性,构造器参数需要进行闭包检测,其实就等同于类进行闭包检测,所以要对类进行序列化。
补充:class 前面加上关键字 case 也可以使编译通过,这是因为样例类会自动进行序列化操作
5.3 Kryo 序列化框架
Java 的序列化能够序列化任何的类。但是比较重 (字节多) ,序列化后,对象的提交也 比较大。Spark 出于性能的考虑, Spark2.0 开始支持另外一种 Kryo 序列化机制。Kryo 速度 是 Serializable 的 10 倍。当RDD 在 Shuffle 数据的时候,简单数据类型、数组和字符串类型 已经在 Spark 内部使用 Kryo 来序列化。
6. RDD依赖关系
6.1 RDD血缘关系
val rdd1 = rdd.map(_*2)
如上所示,想要构建 rdd1 就要用到 rdd,则称 rdd1 依赖于 rdd。相邻的两个 rdd 的关系称为依赖关系,新的 rdd 依赖于旧的 rdd。多个连续的依赖关系,称之为血缘关系。
RDD 是不会保存数据的,为了提供容错性,需要将 RDD 之间的关系保存下来,一旦出现错误,可以根据血缘关系找到数据源重新读取数据。
6.2 宽窄依赖
窄依赖表示每一个父(上游)RDD 的 Partition 最多被子 (下游) RDD 的一个 Partition 使用,窄依赖我们形象的比喻为独生子女。
宽依赖表示同一个父 (上游) RDD 的 Partition 被多个子 (下游) RDD 的 Partition 依赖,会引起 Shuffle,宽依赖形象的比喻为多生。
6.3 RDD阶段划分
DAG (Directed Acyclic Graph) 有向无环图是由点和线组成的拓扑图形, 该图形具有方向,不会闭环。例如,DAG 记录了 RDD 的转换过程和任务的阶段。
RDD 中存在宽依赖时阶段数会自动增加一个ResultStage,即 阶段的数量 = 宽依赖数量 + 1
6.4 RDD任务划分
RDD 任务切分中间分为:Application 、Job 、Stage 和 Task
⚫ Application:初始化一个 SparkContext 即生成一个 Application;
⚫ Job:一个 Action 算子就会生成一个Job;
⚫ Stage:Stage 等于宽依赖(ShuffleDependency)的个数加 1;
⚫ Task:一个 Stage 阶段中, 最后一个 RDD 的分区个数就是 Task 的个数。
注意: Application->Job->Stage->Task 每一层都是 1 对 n 的关系。任务的数量 = 当前阶段中最后一个RDD的分区数量
7. RDD持久化
7.1 是什么
前面说到,RDD是不会存储数据的,如果一个 RDD 要重复使用,那么就要从头到尾再来执行一次来获取数据。RDD的对象是可以重复使用的,但是数据无法重复使用。将计算得到的数据存到内存或者文件中就叫RDD的持久化。
7.2 RDD Cache缓存
RDD 通过 Cache 或者 Persist 方法将前面的计算结果缓存, 默认情况下会把数据以缓存 在 JVM 的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的行动算子时, 该 RDD 将会被缓存在计算节点的内存中,并供后面重用。
// 数据缓存。
rdd.cache ()
// 可以更改存储级别
rdd.persist (StorageLevel.MEMORY_AND_DISK)
Cache缓存默认持久化到内存,如果想要保存到磁盘文件,需要更改存储级别。这里的 MEMORY_AND_DISK 就是表示缓存到内存和磁盘。RDD对象的持久化操作不一定是为了重用,在数据执行比较长,或者数据比较重要的场合也可以采用持久化操作。
Spark 会自动对一些 Shuffle 操作的中间数据做持久化操作(比如: reduceByKey)。这样 做的目的是为了当一个节点 Shuffle 失败了避免重新计算整个输入。但是,在实际使用的时 候,如果想重用数据,仍然建议调用 persist 或 cache
7.3 RDD CheckPoint检查点
CheckPoint 检查点是要落盘的,且不会像缓存一样被删除,所以需要指定文件路径。一般保存路径都是在分布式存储系统中,例如HDFS
// 设置检查点路径
sc.setCheckpointDir ("./checkpoint1")
由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点 之后有节点出现问题,可以从检查点开始重做血缘,减少了开销。同样,对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action(行动算子) 操作才能触发。
7.4 缓存和检查点区别
cache:将数据临时存储在内存中进行数据重用。
会在血缘关系中添加新的依赖。一旦出现问题,可以重头读取数据
persist:将数据临时存储到磁盘文件中进行数据重用,
涉及到磁盘IO,性能较低,但相对安全。
也会在血缘关系中添加新的依赖。
checkpoint:将数据长久的保存到磁盘文件中进行数据重用,
涉及到磁盘IO,性能较低,但数据安全。
为了保证数据安全,所以一般情况下会独立执行作业。(行动算子执行的时候,checkpoint 会产生一个新的作业)
为了提高效率,一般会和 cache 一起使用。(先 cache 再 checkpoint)
执行过程中会切断血缘关系并建立新的血缘关系。
checkpoint 等同于改变数据源
8. RDD 分区器
Spark 目前支持 Hash 分区和 Range 分区,和用户自定义分区。 Hash 分区为当前的默认 分区。分区器直接决定了 RDD 中分区的个数、RDD 中每条数据经过 Shuffle 后进入哪个分 区,进而决定了 Reduce 的个数。
注意:只有 Key-Value 类型的 RDD 才有分区器,非 Key-Value 类型的 RDD 分区的值是 None
下面是一段自定义分区器的代码演示:
package partitioner
import org.apache.spark.rdd.RDD
import org.apache.spark.{Partitioner, SparkConf, SparkContext}
object PartitionerTset {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("partitioner")
val sc = new SparkContext(conf)
val rdd: RDD[(String, String)] = sc.makeRDD(List(
("nba", "xxxx"), ("cba", "xxxx"), ("nba", "xxxx"), ("cxk", "xxxx")
),3)
val partRDD: RDD[(String, String)] = rdd.partitionBy(new Mypartitioner)
partRDD.saveAsTextFile("output")
sc.stop()
}
/**
* 自定义分区器
* 1、继承 Partitioner
* 2、重写方法
*/
class Mypartitioner extends Partitioner{
// 分区数量
override def numPartitions: Int = 3
// 根据数据的 key 值来返回数据的分区索引(从零开始)
override def getPartition(key: Any): Int = {
key match {
case "nba"=>0
case "cba"=>1
case _=>2
}
}
}
}
9. RDD文件的读取和保存
Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。
文件格式分为: text 文件、csv 文件、 sequence 文件以及 Object 文件;
文件系统分为: 本地文件系统、 HDFS 、HBASE 以及数据库。
9.1 text文件
比较简单,直接代码演示
// 读取输入文件
val inputRDD: RDD[String] = sc.textFile ("input/1.txt")
// 保存数据
inputRDD.saveAsTextFile("output")
9.2 sequence 文件
SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文件。
// 保存数据为 SequenceFile
dataRDD.saveAsSequenceFile("output")
// 读取 SequenceFile 文件
sc.sequenceFile[Int,Int]("output").collect().foreach(println)
9.3 Object 文件
对象文件是将对象序列化后保存的文件, 采用 Java 的序列化机制。可以通过 objectFile 函数接收一个路径,读取对象文件, 返回对应的 RDD ,也可以通过调用saveAsObjectFile()实现对对象文件的输出。因为是序列化所以要指定数据类型。
// 保存数据
dataRDD.saveAsObjectFile("output")
// 读取数据
sc.objectFile[Int]("output").collect().foreach(println)
/**
- 自定义分区器
- 1、继承 Partitioner
- 2、重写方法
*/
class Mypartitioner extends Partitioner{
// 分区数量
override def numPartitions: Int = 3
// 根据数据的 key 值来返回数据的分区索引(从零开始)
override def getPartition(key: Any): Int = {
key match {
case "nba"=>0
case "cba"=>1
case _=>2
}
}
}
}
### 9. RDD文件的读取和保存
Spark 的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。
文件格式分为: text 文件、csv 文件、 sequence 文件以及 Object 文件;
文件系统分为: 本地文件系统、 HDFS 、HBASE 以及数据库。
#### 9.1 text文件
比较简单,直接代码演示
```scala
// 读取输入文件
val inputRDD: RDD[String] = sc.textFile ("input/1.txt")
// 保存数据
inputRDD.saveAsTextFile("output")
9.2 sequence 文件
SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文件。
// 保存数据为 SequenceFile
dataRDD.saveAsSequenceFile("output")
// 读取 SequenceFile 文件
sc.sequenceFile[Int,Int]("output").collect().foreach(println)
9.3 Object 文件
对象文件是将对象序列化后保存的文件, 采用 Java 的序列化机制。可以通过 objectFile 函数接收一个路径,读取对象文件, 返回对应的 RDD ,也可以通过调用saveAsObjectFile()实现对对象文件的输出。因为是序列化所以要指定数据类型。
// 保存数据
dataRDD.saveAsObjectFile("output")
// 读取数据
sc.objectFile[Int]("output").collect().foreach(println)