前面我们已经知道了在spark中,外部操作是由Driver进行的,而算子内部是在executor中执行的,因此,就需要将Driver中的数据通过网络IO传输到executor中,对于对象的传输就需要将对象进行序列化操作(serializable),接入序列化接口serializable,也可以用样例类case class ### {},这是因为样例类在编译时会自动混入序列化接口
RDD算子中传递的函数是会包含闭包操作的,就会进行检测功能,也就是会自动检测传入的变量是否已经序列化
闭包的意思就是函数将外部的变量引入其内部形成一个闭合的效果,来延长变量的生命周期
所以RDD算子传递的函数就会对这个闭包的变量进行闭包检测,检测其是否已经序列化,才能进行传输
因为大数据处理过程中,数据的IO是很重要的,如果序列化后的数据太重太大,就会导致IO速度慢,影响效率,Java的序列化能够序列化任何的类。但是比较重(字节多),序列化后,对象的提交也比较大。Spark出于性能的考虑,Spark2.0开始支持另外一种Kryo序列化机制。Kryo速度是Serializable的10倍。当RDD在Shuffle数据的时候,简单数据类型、数组和字符串类型已经在Spark 内部使用Kryo来序列化。并且序列化后的数据大小比Java的序列化要小得多,因此速度就提上来了
注意:即使使用Kryo序列化,也要继承Serializable接口。
相邻的两个RDD的关系称之为依赖关系
val rdd1 = rdd.map(_* 2)
rdd1依赖于rdd的变化
新的RDD依赖于旧的RDD
多个连续的RDD的依赖关系,称之为血缘关系
如果没有血缘关系,因为RDD是不会保存数据的,当发生错误的时候,是不能直接从上一个RDD重新读数据就能读到错误的RDD的,因此血缘关系就是这样的作用:
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object rdd_Consanguinity {
def main(args: Array[String]): Unit = {
//TODO 建立和Spark连接
val sparkConf = new SparkConf().set("spark.testing.memory","2147480000").setMaster("local").setAppName("WordCount")
val sc = new SparkContext(sparkConf)
//TODO 业务逻辑
val lines: RDD[String] = sc.textFile("data/2.txt")
println(lines.toDebugString)
println("***************")
val words: RDD[String] = lines.flatMap(_.split(" "))
println(words.toDebugString)
println("***************")
val wordtoone = words.map(
word=>(word,1)
)
println(wordtoone.toDebugString)
println("***************")
val wordtocount: RDD[(String, Int)] = wordtoone.reduceByKey(_ + _)
//5.采集数据 聚合
println(wordtocount.toDebugString)
println("***************")
val result: Array[(String, Int)] = wordtocount.collect()
result.foreach(println)
//TODO 关闭连接
sc.stop()
}
}
第一层是TextFile,第二层是在第一层基础上加flatMap,第三层是第二层基础上加map,第四层因为存在shuffle,所以断开。
窄依赖(OneToOne)
上游中一个分区的数据只能由下游中的一个分区独享
宽依赖(Shuffle)
上游中一个分区的数据能由下游中的多个分区独享
如果是窄依赖,则每一个分区的操作只需要一个Task就可以了,但是如果是宽依赖的话,需要分阶段,下一阶段需要等上一阶段的操作结束
从底层源码可以看出,在处理大数据的过程中,如果出现了shuffle依赖,那么阶段就会自动增加一个,这是因为不管是否出现shuffle,在前面的数据处理中,总会出现result阶段,但是如果每多一个shuffle依赖,那么就会增加多一个shufflemap阶段,所以总的阶段的数量= shuffle依赖的数量+1
而result阶段是只有一个的,是最后执行的阶段
RDD算子划分为Application、Job、Stage、Task
Application–>Job–>Stage–>Task 都是1:n的关系
一个Stage中,最后的RDD的分区数量就是Task的数量
任务的名称是和阶段的名称是相同的,例如:
ShuffleMapStage=>ShuffleMapTask
ResultStage=>ResultTask
RDD是不存储数据的,当使用的前部分算子逻辑相同时,可以不写重复代码,但是此时并不是把前面算子处理完的数据直接拿过来做下一步算子操作,而是从头再读一遍
RDD对象可以重用,但是数据无法重用
为了提高效率,不用重复从头开始再读数据,可以:
将前面算子处理后的数据落盘,让接下来的不同算子去取
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object rdd_cache {
def main(args: Array[String]): Unit = {
//TODO 建立和Spark连接
val sparkConf = new SparkConf().set("spark.testing.memory","2147480000").setMaster("local").setAppName("WordCount")
val sc = new SparkContext(sparkConf)
//TODO 业务逻辑
val lines: RDD[String] = sc.textFile("data/2.txt")
val words: RDD[String] = lines.flatMap(_.split(" "))
val wordtoone = words.map(
word=>{
println("**********")
(word,1)
}
)
//wordtoone.cache()
wordtoone.persist(StorageLevel.DISK_ONLY)
val wordtocount: RDD[(String, Int)] = wordtoone.reduceByKey(_ + _)
wordtocount.collect().foreach(println)
println("//")
val groupcount: RDD[(String, Iterable[Int])] = wordtoone.groupByKey()
groupcount.collect().foreach(println)
//TODO 关闭连接
sc.stop()
}
}
其中的cache默认持久化的操作,只能将数据存到内存中,其底层代码其实用的还是persist方法,只是参数不同
wordtoone.cache()
wordtoone.persist(StorageLevel.DISK_ONLY)
其实持久化操作并不是一定为了重用,当前面数据逻辑太长,而后面我们需要用到相同的逻辑时,或者数据的比较重要的场合的时候也可以用持久化的方法
**和前面的区别是:前面我们保存数据的时候是保存为临时文件,在作业完成之后是会删除的,而checkpoint是需要落盘,即使作业完成,也不会删除,是保存在分布式存储系统中
object rdd_checkpoint {
def main(args: Array[String]): Unit = {
//TODO 建立和Spark连接
val sparkConf = new SparkConf().set("spark.testing.memory","2147480000").setMaster("local").setAppName("WordCount")
val sc = new SparkContext(sparkConf)
sc.setCheckpointDir("checkpoint")
val lines: RDD[String] = sc.textFile("data/2.txt")
val words: RDD[String] = lines.flatMap(_.split(" "))
val wordtoone = words.map(
word=>{
println("**********")
(word,1)
}
)
wordtoone.checkpoint()
val wordtocount: RDD[(String, Int)] = wordtoone.reduceByKey(_ + _)
wordtocount.collect().foreach(println)
println("//")
val groupcount: RDD[(String, Iterable[Int])] = wordtoone.groupByKey()
groupcount.collect().foreach(println)
//TODO 关闭连接
sc.stop()
}
}
一般是会将cache和checkpoint一起用
将数据临时存储在内存中进行数据重用,会在血缘关系中添加新的依赖,如果内存出现问题,可以回溯重新读取数据
将数据临时存储在磁盘文件中进行数据重用,会在血缘关系中添加新的依赖,如果数据出现问题,可以回溯重新读取数
涉及到磁盘I0,性能较低,但是数据安全
如果作业执行完毕,临时保存的数据文件就会丢失
将数据长久地保存在磁盘文件中进行数据重用涉及到磁盘I0,性能较低,但是数据安全
为了保证数据安全,所以一般情况下会独立执行作业为了能够提高效率,一般情况下,是需要和cache联合使用
checkpoint执行过程中,是会切断血缘关系的,重新建立新的血缘关系,checkpoint等同于改变了数据源
由于spark有默认的分区器,但是如果需要根据我们数据来定制分区规则,就需要用到自定义的分区器
import org.apache.spark.rdd.RDD
import org.apache.spark.{Partition, Partitioner, SparkConf, SparkContext}
object Spark01_MyPartition {
def main(args: Array[String]): Unit = {
//TODO 建立和Spark连接
val sparkConf = new SparkConf().set("spark.testing.memory","2147480000").setMaster("local").setAppName("WordCount")
val sc = new SparkContext(sparkConf)
val rdd = sc.makeRDD(List(
("nba", "**********"),
("cba", "**********"),
("wnba", "**********")
), 3)
val parRDD = rdd.partitionBy(new MyPartition)
parRDD.saveAsTextFile("partRDD")
sc.stop()
}
class MyPartition extends Partitioner {
override def numPartitions: Int = 3
override def getPartition(key: Any): Int = {
key match {
case "nba" => 0
case "cba" => 1
case _ => 2
}
}
}
}