Fork me on GitHub

Spark |03 SparkCore |序列化| 依赖关系| 持久化| 分区器| 数据读取保存| 广播变量和累加器

Spark中三大数据结构:

  • RDD; 
  • 广播变量: 分布式只读共享变量; 
  • 累加器:分布式只写共享变量; 线程和进程之间

 

1. RDD 序列化

1) 闭包检查

从计算的角度, 算子以外的代码都是在 Driver 端执行, 算子里面的代码都是在 Executor 端执行。那么在 scala 的函数式编程中,就会导致算子内经常会用到算子外的数据,

这样就 形成了闭包的效果,如果使用的算子外的数据无法序列化,就意味着无法传值给 Executor 端执行,就会发生错误,所以需要在执行任务计算前,检测闭包内的对象是否可以进

行序列化,这个操作我们称之为闭包检测。Scala2.12 版本后闭包编译方式发生了改变

  def main(args: Array[String]): Unit = {
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark-Transform")
    val sc: SparkContext = new SparkContext(sparkConf)
    val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))

    val user = new User()
    //org.apache.spark.SparkException: Task not serializable
   //java.io.NotSerializableException: com.hopson.wc.WordCount$User
//Rdd算子中传递的函数是会包含闭包操作的,那么就会进行 闭包检测功能 rdd.foreach( num => { println("age = " + (user.age + num)) } ) sc.stop() } class User(){ var age: Int = 30 }

原因分析: 算子的内部在executor中执行(foreach方法),算子的外部在driver端执行(User类、user对象);driver和executor要共享数据,就需要传递数据,需要序列化。

解决办法:

  class User() extends Serializable { //进行序列化
    var age: Int = 30
  }
或者使用样例类,因为样例类在编译时自动实现了序列化特质(实现可序列化接口)
  case class User() {
    var age: Int = 30
  }

如果没有序列化还是 class User() { var age: Int = 30 },并且集合为空 val rdd: RDD[Int] = sc.makeRDD(List[Int]()) ,还是报没有序列化的错误。

因为函数式编程,foreach它的匿名函数需要用到闭包的操作。foreach用到外部变量,外部变量就需要传递到executor中,执行之前就得判断了(传来的闭包的变量是否序列化,闭包就是函数,把外部变量引入内部形成闭合的效果,它改变了这个变量的声明周期)。

2) 序列化方法和属性

从计算的角度, 算子以外的代码都是在 Driver 端执行, 算子里面的代码都是在 Executor 端执行,

 1.RDD中的函数传递

自己定义一些RDD的操作,那么此时需要主要的是,初始化工作是在Driver端进行的,而实际运行程序是在Executor端进行的,这就涉及到了跨进程通信,是需要序列化的。

传递一个方法

//类的构造参数就是类的属性,构造参数需要进行闭包检测,等同于这个类需要进行闭包检测
class
Search(query: String){ // extends Serializable //过滤出包含字符串的数据 def isMatch(s: String): Boolean = { s.contains(query) //省略了 this.query } //函数序列化案例 过滤出包含字符串的RDD def getMatch1(rdd: RDD[String]): RDD[String] = { rdd.filter(isMatch) } //属性序列化案例 过滤出包含字符串的RDD def getMatch2(rdd: RDD[String]): RDD[String] = { val str: String = this.query //将类变量赋值给局部变量str,即可序列化; 它是driver端执行 rdd.filter(x => x.contains(str)) //在executor端执行。它引用了字符串str } }
object TestSearch {
  def main(args: Array[String]): Unit = {
    //初始化配置信息以及 sc
    val conf = new SparkConf().setAppName("WordCount").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)

    val rdd = sc.makeRDD(List("kris", "Baidu", "Google")) //创建一个RDD
    val search = new Search("ris")  //创建一个search对象
    println("===============")
    //运用第一个过滤函数并打印结果;
    val res: RDD[String] = search.getMatch1(rdd) //java.io.NotSerializableException: com.xxx.spark.Search
                                                //class Search(query: String) extends Serializable
    res.foreach(println(_))
  }
}

在这个方法中所调用的方法isMatch()是定义在Search这个类中的,实际上调用的是this. isMatch(),this表示Search这个类的对象,程序在运行过程中需要将Search对象序列化以后传递到Executor端。

解决方案 使类继承Serializable即可。 class Search() extends Serializable{...},因为类的构造参数就是类的属性。

2. 传递一个属性

//初始化sc
    val conf = new SparkConf().setAppName("WordCount").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)

    val rdd = sc.makeRDD(List("kris", "Baidu", "Google"))
    val search = new Search("ris")
    println("===============")

     val res2: RDD[String] = search.getMatch2(rdd)
     res2.foreach(println(_))

  rdd.filter(x => x.contains(query))

在这个方法中所调用的方法query是定义在Search这个类中的字段,实际上调用的是this. query,this表示Search这个类的对象,程序在运行过程中需要将Search对象序列化以后传递到Executor端。

解决方案:将类变量query赋值给局部变量如上所示;

3) Kryo 序列化框架

参考地址: https://github.com/EsotericSoftware/kryo

Java 的序列化能够序列化任何的类。但是比较重(字节多),序列化后,对象的提交也比较大。

Spark 出于性能的考虑,Spark2.0开始支持另外一种 Kryo 序列化机制。Kryo 速度是 Serializable 的 10 倍。当

RDD 在 Shuffle 数据的时候,简单数据类型、数组和字符串类型 已经在 Spark 内部使用 Kryo 来序列化。

Java中关键字transient 表示这个不能被序列化,但是在Kryo它可以绕过Java中的这个关键字,照样可以进行数据传输序列化。

注意:即使使用 Kryo 序列化,也要继承 Serializable 接口。

2. RDD依赖关系

1)血缘关系

  RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转

换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区

RDD不会保存数据,RDD为了提供容错性,需要将RDD间的关系保存下来,一旦出现错误,可以根据血缘关系将数据源重新读取进行计算。每个RDD都有这种保存血缘关系的能力

   val fileRDD: RDD[String] = sc.textFile("input/1.txt")
    println(fileRDD.toDebugString)    
    println("----------------------")
    val wordRDD: RDD[String] = fileRDD.flatMap(_.split(" "))
    println(wordRDD.toDebugString)    
    println("----------------------")
    val mapRDD: RDD[(String, Int)] = wordRDD.map((_,1))
    println(mapRDD.toDebugString)    
    println("----------------------")
    val resultRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
    println(resultRDD.toDebugString) 

----->
(2) input/1.txt MapPartitionsRDD[1] at textFile at Test.scala:8 []
 |  input/1.txt HadoopRDD[0] at textFile at Test.scala:8 []
----------------------
(2) MapPartitionsRDD[2] at flatMap at Test.scala:11 []
 |  input/1.txt MapPartitionsRDD[1] at textFile at Test.scala:8 []
 |  input/1.txt HadoopRDD[0] at textFile at Test.scala:8 []
----------------------
(2) MapPartitionsRDD[3] at map at Test.scala:14 []
 |  MapPartitionsRDD[2] at flatMap at Test.scala:11 []
 |  input/1.txt MapPartitionsRDD[1] at textFile at Test.scala:8 []
 |  input/1.txt HadoopRDD[0] at textFile at Test.scala:8 []
----------------------
(2) ShuffledRDD[4] at reduceByKey at Test.scala:17 []
 +-(2) MapPartitionsRDD[3] at map at Test.scala:14 [] //+ - 断开了,出现shuffle
    |  MapPartitionsRDD[2] at flatMap at Test.scala:11 []
    |  input/1.txt MapPartitionsRDD[1] at textFile at Test.scala:8 []
    |  input/1.txt HadoopRDD[0] at textFile at Test.scala:8 []

2)RDD依赖关系

这里所谓的依赖关系,其实就是两个相邻 RDD 之间的关系, OneToOneDependency 、 ShuffleDependency

   val fileRDD: RDD[String] = sc.textFile("input/1.txt")
    println(fileRDD.dependencies)    //List(org.apache.spark.OneToOneDependency@151db587) 可以看到它的依赖关系,窄依赖
    println("----------------------")
    val wordRDD: RDD[String] = fileRDD.flatMap(_.split(" "))
    println(wordRDD.dependencies)    //List(org.apache.spark.OneToOneDependency@151db587)
    println("----------------------")
    val mapRDD: RDD[(String, Int)] = wordRDD.map((_,1))
    println(mapRDD.dependencies)    //List(org.apache.spark.OneToOneDependency@151db587)
    println("----------------------")
    val resultRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
    println(resultRDD.dependencies)  //List(org.apache.spark.ShuffleDependency@199bc830) 可以看到它的依赖关系,宽依赖,产生shuffle;跨节点传输数据就会产生shuffle

 

3)Rdd宽窄依赖

RDD在Lineage依赖方面分为两种Narrow Dependencies与Wide Dependencies用来解决数据容错时的高效性。

Narrow Dependencies是指父(上游)RDD的每一个分区最多被一个子(下游)RDD的一个分区所用,窄依赖比喻为独生子女。

 表现为一个父RDD的分区对应于一个子RDD的分区或多个父RDD的分区对应于一个子RDD的分区,也就是说一个父RDD的一个分区不可能对应一个子RDD的多个分区。

Wide Dependencies是指 同一个父(上游)RDD 的 Partition 被多个子(下游)RDD 的 Partition 依赖,会引起 Shuffle,宽依赖我们形象的比喻为多生

  也就是说存在一个父RDD的一个分区对应一个子RDD的多个分区

窄依赖
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T] (rdd)

宽依赖
class ShuffleDependency[K: ClassTag, V: ClassTag, C: ClassTag]
(
     @transient private val _rdd: RDD[_ <: Product2[K, V]],
     val partitioner: Partitioner,
     val serializer: Serializer = SparkEnv.get.serializer,
     val keyOrdering: Option[Ordering[K]] = None,
     val aggregator: Option[Aggregator[K, V, C]] = None,
     val mapSideCombine: Boolean = false)
extends Dependency[Product2[K, V]] 

对与Wide Dependencies,这种计算的输入和输出在不同的节点上,lineage方法对与输入节点完好,而输出节点宕机时,通过重新计算,这种情况下,这种方法容错是有效的,否则无效,因为无法重试,需要向

上其祖先追溯看是否可以重试(这就是lineage,血统的意思),Narrow Dependencies对于数据的重算开销要远小于Wide Dependencies的数据重算开销。

4)RDD阶段划分

DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向, 不会闭环。例如,DAG 记录了 RDD 的转换过程和任务的阶段。

DAG(Directed Acyclic Graph)叫做有向无环图,原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同DAG划分成不同的Stage对于窄依赖,partition

的转换处理在Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据。

                                                

5)任务划分(重点)

RDD任务切分中间分为:Application、Job、Stage和Task

  • 1)Application:初始化一个SparkContext即生成一个Application; 提交一个jar包就是Application(一个Application可以 有多个job)
  • 2)Job:一个Action算子就会生成一个Job ;
  • 3)Stage:Stage等于款依赖(shuffleDependency)的个数加1;  根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage。
  • 4)Task:一个Stage阶段中,最后一个RDD的分区个数就是Task的个数; Stage是一个TaskSet将Stage划分的结果发送到不同的Executor执行即为一个Task。

  注意:Application->Job->Stage->Task每一层都是1对n的关系。

  有多少个task,由你当前stage的最后一个RDD的分区数决定

窄依赖分两种:OneToOneDependency和 RangeDependency(如两个分区union两个分区 => 四个分区)   NarrowDependency窄依赖

Union会产生窄依赖(查看源码);map也是窄依赖;  ReduceByKey是宽依赖,shuffledRDD---shufleDependency

scala> sc.makeRDD(1 to 8).map((_,1)).reduceByKey(_+_).collect
res3: Array[(Int, Int)] = Array((4,1), (6,1), (8,1), (2,1), (1,1), (7,1), (3,1), (5,1))

在宽依赖算子reduceByKey那切一刀;

scala> sc.makeRDD(1 to 8).map((_,1)).reduceByKey(_+_).map((_,1)).reduceByKey(_+_).collect
res4: Array[((Int, Int), Int)] = Array(((6,1),1), ((3,1),1), ((8,1),1), ((2,1),1), ((5,1),1), ((1,1),1), ((7,1),1), ((4,1),1))

  两个reduceByKey宽依赖,分成了3个stage;

RDD分区数对应Task数;

scala> sc.makeRDD(1 to 8,4).map((_,1)).coalesce(3,false).reduceByKey(_+_).coalesce(2,false).collect
res5: Array[(Int, Int)] = Array((6,1), (3,1), (4,1), (1,1), (7,1), (8,1), (5,1), (2,1))
  4个分区==> map算子 4个分区 ==> 经过coalesce转换为3个分区 reduceByKey宽依赖算子切分了两个stage coalesce产生 2个分区

  可以推断得到产生了1个Application,2个stage,第一个stage产生了3个task;第二个stage产生了2个task;

3个task

2个task

阶段和任务划分源码

3.RDD 持久化

 

    val list = List("Hello Scala", "Hello Spark")
    val rdd: RDD[String] = sc.makeRDD(list)
    val flatMap: RDD[String] = rdd.flatMap(_.split(" "))
    val flatMapRdd: RDD[(String, Int)] = flatMap.map((_,1))
    val reduceRdd: RDD[(String, Int)] = flatMapRdd.reduceByKey(_+_)
    reduceRdd.collect.foreach(println)
    println("**********************************")

    val groupRdd: RDD[(String, Iterable[Int])] = flatMapRdd.groupByKey()
    groupRdd.collect.foreach(println)
View Code

RDD中不存储数据,如果一个RDD需要重复使用,那么需要从头再次执行来获取数据。 RDD对象可以重用,但是数据无法重用。

    val list = List("Hello Scala", "Hello Spark")
    val rdd: RDD[String] = sc.makeRDD(list)
    val flatMap: RDD[String] = rdd.flatMap(_.split(" "))
    val flatMapRdd: RDD[(String, Int)] = flatMap.map(word => {
      println("@@@@@@@@@@")
      (word,1)
    })
    //cache 默认持久化的操作, 只能将数据保存到内存中, 如果想要保存到磁盘文件,需要更改存储级别
    //flatMapRdd.cache() //  def cache(): this.type = persist()   -->  def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
    // 持久化操作必须在行动算子执行时完成
flatMapRdd.persist(StorageLevel.DISK_ONLY) val reduceRdd: RDD[(String, Int)]
= flatMapRdd.reduceByKey(_+_) reduceRdd.collect.foreach(println) println("**********************************") val groupRdd: RDD[(String, Iterable[Int])] = flatMapRdd.groupByKey() groupRdd.collect.foreach(println)

RDD对象的持久化操作不一定是为了重用; 在数据执行较长,或数据比较重要的场合也可以采用持久化操作。

1) RDD Cache 缓存

RDD 通过 Cache 或者 Persist 方法将前面的计算结果缓存,默认情况下会把数据以缓存在JVM 的堆内存中。但是并不是这两个方法被调用时立即缓存,而是触发后面的 action 算子

时,该RDD 将会被缓存在计算节点的内存中,并供后面重用。

// cache 操作会增加血缘关系,不改变原有的血缘关系
println(wordToOneRdd.toDebugString)
// 数据缓存。
wordToOneRdd.cache()
// 可以更改存储级别
//mapRdd.persist(StorageLevel.MEMORY_AND_DISK_2)

StorageLevel.scala 存储级别源码,在存储级别的末尾加上“_2”来 表示把持久化数据存为两份
object StorageLevel {
 val NONE = new StorageLevel(false, false, false, false)
 val DISK_ONLY = new StorageLevel(true, false, false, false)
 val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
 val MEMORY_ONLY = new StorageLevel(false, true, false, true)
 val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
 val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
 val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
 val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
 val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
 val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
 val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
 val OFF_HEAP = new StorageLevel(true, true, true, false, 1)

缓存有可能丢失,或者存储于内存的数据由于内存不足而被删除,RDD 的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于 RDD 的一系列转换,丢失的数据会被重算,由于 RDD 的各个

Partition 是相对独立的,因此只需要计算丢失的部分即可, 并不需要重算全部 Partition。 Spark 会自动对一些 Shuffle 操作的中间数据做持久化操作(比如:reduceByKey)。这样做的目的是为了当一个节点

Shuffle 失败了避免重新计算整个输入。但是,在实际使用的时 候,如果想重用数据,仍然建议调用 persist 或 cache。

2) RDD CheckPoint 检查点

所谓的检查点其实就是通过将 RDD 中间结果写入磁盘 由于血缘依赖过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果检查点 之后有节点出现问题,可以从检查

点开始重做血缘,减少了开销。 对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action 操作才能触发。

    sc.setCheckpointDir("cp")
    //checkpoint需要落盘, 需要指定检查点保存路径
    //检查点路径保存的文件, 当作业执行完毕后,不会被删除
    //一般保存路径都是在分布式存储系统: HDFS
   flatMapRdd.cache()  
flatMapRdd.checkpoint()

//checkpoint: 将数据长久地保存在磁盘文件中进行数据重用
// 涉及到磁盘IO,性能较低,但是数据安全
// 为了数据安全,所以一般情况下,会独立执行作业
// 为了能够提高效率,一般情况下,需要和cache联合使用。

3) 缓存和检查点区别

    //cache: 将数据临时存储在内存中进行数据重用
        会在血缘关系中添加新的依赖,一旦出现问题,可以重头读取数据。
//persist: 将数据临时存储在磁盘文件中进行数据重用 // 涉及到磁盘IO, 性能较低,但是数据安全 // 如果作业执行完毕,临时保存的数据文件就会丢失 //checkpoint: 将数据长久地保存在磁盘文件中进行数据重用 // 涉及到磁盘IO, 性能较低,但是数据安全 // 为了保证数据安全,所以一般情况下,会独立执行作业 // 为了能够提高效率,一般情况下,需要和cache联合使用。
执行过程中,会切断血缘关系,重新建立新的血缘关系
checkpoint等同于改变数据源。
println(mapRdd.toDebugString) //可以在行动算子的前后打印查看血缘关系。
  • 1)Cache 缓存只是将数据保存起来,不切断血缘依赖。Checkpoint 检查点切断血缘依赖。
  • 2)Cache 缓存的数据通常存储在磁盘、内存等地方,可靠性低。Checkpoint 的数据通常存 储在 HDFS 等容错、高可用的文件系统,可靠性高。
  • 3)建议对 checkpoint()的 RDD 使用 Cache 缓存,这样 checkpoint 的 job 只需从 Cache 缓存 中读取数据即可,否则需要再从头计算一次 RDD。

4. RDD数据分区器

Spark目前支持Hash分区和Range分区,用户也可以自定义分区Hash分区为当前的默认分区,Spark中分区器直接决定了RDD中分区的个数、RDD中每条数据经过Shuffle后进入哪个分区,进而决定了

Reduce的个数。

  • 1) 只有Key-Value类型的RDD才有分区器的,非Key-Value类型的RDD分区的值是None
  • 2 )每个RDD的分区ID范围:0~numPartitions-1,决定这个值是属于那个分区的。

1) Hash 分区:对于给定的 key,计算其 hashCode,并除以分区个数取余

2) Range 分区:将一定范围内的数据映射到一个分区中,尽量保证每个分区数据均匀,而 且分区间有序

获取RDD分区:可以通过使用RDD的partitioner 属性来获取 RDD 的分区方式。它会返回一个 scala.Option 对象, 通过get方法获取其中的值。

scala> val pairs = sc.parallelize(List((1,1),(2,2),(3,3)))
pairs: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[3] at parallelize at <console>:24
scala> pairs.partitioner  查看RDD的分区器
res5: Option[org.apache.spark.Partitioner] = None
scala>  import org.apache.spark.HashPartitioner  导入HashPartitioner类
import org.apache.spark.HashPartitioner

scala> val partitioned = pairs.partitionBy(new HashPartitioner(2)) //使用HashPartitioner对RDD进行重新分区
partitioned: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[4] at partitionBy at <console>:27

scala> partitioned.partitioner
res6: Option[org.apache.spark.Partitioner] = Some(org.apache.spark.HashPartitioner@2)

Hash分区

HashPartitioner分区的原理:对于给定的key,计算其hashCode,并除以分区的个数取余,如果余数小于0,则用余数+分区的个数(否则加0),最后返回的值就是这个key所属的分区ID。

源码:  
def getPartition(key: Any): Int = key match {
    case null => 0  //key为null直接进0号分区;
    case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
  }
def nonNegativeMod(x: Int, mod: Int): Int = {
val rawMod = x % mod
rawMod + (if (rawMod < 0) mod else 0)
}

rdd.partitionBy(new org.apache.spark.HashPartitioner(7)     .partitioner查看分区器

Ranger分区

HashPartitioner分区弊端:可能导致每个分区中数据量的不均匀,极端情况下会导致某些分区拥有RDD的全部数据。

RangePartitioner作用:将一定范围内的数映射到某一个分区内,尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分区内的元素小或者大,但是分区内的元素是不能保证顺序的简单的说就是将一定范围内的数映射到某一个分区内。实现过程为:

第一步:先从整个RDD中抽取出样本数据,将样本数据排序,计算出每个分区的最大key值,形成一个Array[KEY]类型的数组变量rangeBounds;

第二步:判断key在rangeBounds中所处的范围,给出该key值在下一个RDD中的分区id下标;该分区器要求RDD中的KEY类型必须是可以排序的

自定义分区

要实现自定义的分区器,你需要继承 org.apache.spark.Partitioner 类并实现下面三个方法。

(1)numPartitions: Int:返回创建出来的分区数。

(2)getPartition(key: Any): Int:返回给定键的分区编号(0到numPartitions-1)。

(3)equals():Java 判断相等性的标准方法。这个方法的实现非常重要,Spark 需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样 Spark 才可以判断两个 RDD 的分区方式是否相同。

class MyPartitioner(partitions: Int) extends Partitioner{ //传入参数,可指定分区
  override def numPartitions: Int = partitions
  override def getPartition(key: Any): Int = { //只能根据key进行分区;而hadoop的分区既可根据key也可根据value (Partitioner.java)
    key.toString.toInt % partitions
    //0 //也可以写0,不管传进来什么key,数据只进入0号分区;
  }
}

测试

object TextPartition {
  def main(args: Array[String]): Unit = {
    //初始化sc
    val conf: SparkConf = new SparkConf().setAppName("WordCount").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)

    val rdd: RDD[(Int, Int)] = sc.makeRDD(List((1,1), (2,1), (3,1), (4,1)))
    val rdd2: RDD[(Int, Int)] = rdd.partitionBy(new MyPartitioner(2))
    //val rdd: RDD[String] = sc.textFile("E:\\wc.txt")
    rdd2.saveAsTextFile("E:\\output")
      sc.stop()
  }
}

结果是2个文件(2个分区,数据进入了2个分区)

5. 数据读取与保存

Spark的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统

  • 文件格式分为:Text文件、Json文件、Csv文件、Sequence文件以及Object文件;
  • 文件系统分为:本地文件系统、HDFS、HBASE以及数据库。

➢ text 文件 // 读取输入文件 val inputRDD: RDD[String] = sc.textFile("input/1.txt") // 保存数据 inputRDD.saveAsTextFile("output")

➢ sequence 文件 SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对而设计的一种平面文件(Flat File)。

在 SparkContext 中,可以调用 sequenceFile[keyClass, valueClass](path)。 // 保存数据为 SequenceFile dataRDD.saveAsSequenceFile("output") // 读取 SequenceFile 文件 sc.sequenceFile[Int,Int]("output").collect().foreach(println)

➢ object 对象文件 对象文件是将对象序列化后保存的文件,采用 Java 的序列化机制。可以通过 objectFile[T: ClassTag](path)函数接收一个路径,读取对象文件,返回对应的 RDD,也可以通过调用 saveAsObjectFile()实现对对象文件的输出。因为是序列化所以要指定类型。

文件类数据读取与保存

Text文件

1)数据读取:textFile(String)

scala> val hdfsFile = sc.textFile("hdfs://hadoop101:9000/fruit.txt")

hdfsFile: org.apache.spark.rdd.RDD[String] = hdfs://hadoop102:9000/fruit.txt MapPartitionsRDD[21] at textFile at <console>:24

2)数据保存: saveAsTextFile(String)

scala> hdfsFile.saveAsTextFile("/fruitOut")

Json文件

如果JSON文件中每一行就是一个JSON记录,那么可以通过将JSON文件当做文本文件来读取,然后利用相关的JSON库对每一条数据进行JSON解析。

注意:使用RDD读取JSON文件处理很复杂,同时SparkSQL集成了很好的处理JSON文件的方式,所以应用中多是采用SparkSQL处理JSON文件。

scala> import scala.util.parsing.json.JSON
import scala.util.parsing.json.JSON

scala> sc.textFile("/opt/module/spark/spark-local/examples/src/main/resources/people.json").collect
res13: Array[String] = Array({"name":"Michael"}, {"name":"Andy", "age":30}, {"name":"Justin", "age":19})
scala> val y = sc.textFile("/opt/module/spark/spark-local/examples/src/main/resources/people.json")
y: org.apache.spark.rdd.RDD[String] = /opt/module/spark/spark-local/examples/src/main/resources/people.json MapPartitionsRDD[25] at textFile at <console>:25
scala> y.map(JSON.parseFull).collect
res19: Array[Option[Any]] = Array(Some(Map(name -> Michael)), Some(Map(name -> Andy, age -> 30.0)), Some(Map(name -> Justin, age -> 19.0)))

Sequence文件

 SequenceFile文件是Hadoop用来存储二进制形式的key-value对而设计的一种平面文件(Flat File)。Spark 有专门用来读取 SequenceFile 的接口。在 SparkContext 中,可以调用 sequenceFile[keyClass, valueClass](path)。

注意:SequenceFile文件只针对PairRDD

scala> val rdd = sc.parallelize(Array((1,2),(3,4),(5,6)))
rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[28] at parallelize at <console>:25
scala> rdd.saveAsSequenceFile("./output/seque") //
scala> sc.sequenceFile[Int,Int]("/opt/module/spark/spark-local/output/seque").collect  //必须加泛型,不然会报错 ambiguous implicit values:
res22: Array[(Int, Int)] = Array((5,6), (1,2), (3,4))

对象文件

对象文件是将对象序列化后保存的文件,采用Java的序列化机制。可以通过objectFile[k,v](path) 函数接收一个路径,读取对象文件,返回对应的 RDD,也可以通过调用saveAsObjectFile() 实现对对象文件的输出。因为是序列化所以要指定类型。

scala> val rdd = sc.parallelize(Array(1,2,3,4))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[32] at parallelize at <console>:25
scala> rdd.saveAsObjectFile("./output/object")
scala> sc.objectFile[(Int)]("/opt/module/spark/spark-local/output/object").collect //也要加泛型
res25: Array[Int] = Array(4, 2, 3, 1)

文件系统类数据读取与保存

①HDFS

Spark的整个生态系统与Hadoop是完全兼容的,所以对于Hadoop所支持的文件类型或者数据库类型,Spark也同样支持.另外,由于Hadoop的API有新旧两个版本,所以Spark为了能够兼容Hadoop所有的版本,也提供了两套创建操作接口.对于外部存储创建操作而言,hadoopRDD和newHadoopRDD是最为抽象的两个函数接口,主要包含以下四个参数.

  1)输入格式(InputFormat): 制定数据输入的类型,如TextInputFormat等,新旧两个版本所引用的版本分别是org.apache.hadoop.mapred.InputFormat和org.apache.hadoop.mapreduce.InputFormat(NewInputFormat)

  2)键类型: 指定[K,V]键值对中K的类型

  3)值类型: 指定[K,V]键值对中V的类型

  4)分区值: 指定由外部存储生成的RDD的partition数量的最小值,如果没有指定,系统会使用默认值defaultMinSplits。

② MySQL

支持通过Java JDBC访问关系型数据库。需要通过JdbcRDD进行,

从Mysql读取数据

    //初始化
    val conf = new SparkConf().setAppName("WorldCount").setMaster("local[*]")
    val sc = new SparkContext(conf)

    //定义连接mysql的参数
    val driver = "com.mysql.jdbc.Driver"
    val url = "jdbc:mysql://hadoop101:3306/rdd"
    val userName = "root"
    val password = "123456"
    //读取 //创建JdbcRDD
    val rdd: JdbcRDD[(Int, String)] = new JdbcRDD(sc, () => {
      Class.forName(driver)
      DriverManager.getConnection(url, userName, password)},
      "select * from test where id >= ? and id <= ?;",
      1, 4, 2,
      r => (r.getInt(1), r.getString(2))
    )
    println(rdd.count())
    rdd.foreach(println(_))

    sc.stop()

从rdd写入mysql

   //rdd数据输出到mysql
    //写入数据,foreachPartition是每个分区创建一个连接
    val rdd: RDD[(Int, String)] = sc.makeRDD(List((5, "Amazon")))
    rdd.foreachPartition(x => {
      Class.forName(driver)
      val conn: Connection = DriverManager.getConnection(url, userName, password)
      x.foreach(x => {
        val id: Int = x._1
        val name: String = x._2
        val statement: PreparedStatement = conn.prepareStatement("insert into test (id, name) values(?, ?)")
        statement.setInt(1, id)
        statement.setString(2, name)
        statement.execute()
      })
    } )
    sc.stop()

③ Hbase

从HBase读取数据

由于 org.apache.hadoop.hbase.mapreduce.TableInputFormat 类的实现,Spark 可以通过Hadoop输入格式访问HBase。这个输入格式会返回键值对数据,其中键的类型为org. apache.hadoop.hbase.io.ImmutableBytesWritable,而值的类型为org.apache.hadoop.hbase.client.

Result。

    //初始化sc
    val conf = new SparkConf().setAppName("WordCount").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)

    //从hbase表读取数据
    val configuration: Configuration = HBaseConfiguration.create()
    configuration.set("hbase.zookeeper.quorum", "hadoop101,hadoop102,hadoop103")
    configuration.set(TableInputFormat.INPUT_TABLE, "fruit")

    val rdd = sc.newAPIHadoopRDD(configuration, classOf[TableInputFormat], classOf[ImmutableBytesWritable], classOf[Result])
    rdd.foreach(x => {
      val cells: Array[Cell] = x._2.rawCells()
      cells.foreach(cell => {
        val rowkey: String = Bytes.toString(CellUtil.cloneRow(cell))
        val family: String = Bytes.toString(CellUtil.cloneFamily(cell))
        val column: String = Bytes.toString(CellUtil.cloneQualifier(cell))
        val value: String = Bytes.toString(CellUtil.cloneValue(cell))
        println(s"$rowkey  $family  $column  $value")
      })
    })
    sc.stop()

往HBase写入

  //初始化sc
    val conf = new SparkConf().setAppName("WordCount").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)
    //rdd数据写入到hbase表
    val rdd: RDD[(String, String, String, String)] = sc.makeRDD(List(("1004", "info", "name", "pineapple")))
    val rdd2 = rdd.map(x => {
      val put: Put = new Put(Bytes.toBytes(x._1))
      put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes(x._4))
      (new ImmutableBytesWritable(), put)
    })

    //创建配置
    val configuration: Configuration = HBaseConfiguration.create()
    configuration.set("hbase.zookeeper.quorum", "hadoop101,hadoop102,hadoop103")
    configuration.set(TableOutputFormat.OUTPUT_TABLE, "fruit")
    //设置OutputFormat类型
    val job: Job = Job.getInstance(configuration)
    job.setOutputFormatClass(classOf[TableOutputFormat[ImmutableBytesWritable]])
    job.setOutputKeyClass(classOf[ImmutableBytesWritable])
    job.setOutputValueClass(classOf[Result])

    rdd2.saveAsNewAPIHadoopDataset(job.getConfiguration)

    sc.stop()

6. 累加器

实现原理

累加器用来把Executor端变量信息聚合到Driver端。在Driver程序中定义的变量,在Executor端的每个Task都会得到这个变量的一份新的副本,每个task更新这些副本的值后,传回

Driver端进行merge。   代码程序都在Driver,序列化传到Executor节点上去执行;

    val rdd = sc.makeRDD(1 to 4) //创建RDD
    var a = 0
    rdd.foreach(x => {
      a += 1
    })
    println(a)  //打印的a是driver端的,而不是executor端的;
执行,输出的却是0; 代码在Driver端,具体执行是在Executor,executor中会有副本a = 0,每个节点的executor都各自有各自的副本,在自己节点上修改

累加器用来对信息进行聚合,通常在向 Spark传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新

这些副本的值也不会影响驱动器中的对应变量。如果我们想实现所有分片处理时更新共享变量的功能,那么累加器可以实现我们想要的效果。

   //初始化sc
    val conf: SparkConf = new SparkConf().setAppName("WordCount").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)

    val rdd = sc.makeRDD(1 to 4) //创建RDD
    val acc: LongAccumulator = sc.longAccumulator //它有一个初始值
      /* 源码:class LongAccumulator extends AccumulatorV2[jl.Long, jl.Long] {
            private var _sum = 0L
            private var _count = 0L*/
    println("初始值: "+ acc.value) //0
    rdd.foreach(x => {
      acc.add(1)
    })
    println(acc.value) //4

 

  def main(args: Array[String]): Unit = {
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark-Transform")
    val sc: SparkContext = new SparkContext(sparkConf)
    val rdd = sc.makeRDD(1 to 4) //创建RDD
    //获取系统累加器
    //Spark默认就提供了简单数据聚合的累加器
    val sumAcc: LongAccumulator = sc.longAccumulator("sum")

/*    rdd.foreach(
      num => {
        //使用累加器
        sumAcc.add(num)
      }) //10*/

    val mapRdd: RDD[Unit] = rdd.map(
      num => {
        //使用累加器
        sumAcc.add(num)
      })

    //获取累加器的值
    //少加: 转换算子中调用累加器, 如果没有行动算子的话, 那么不会执行, 比如仅有map算子,不加mapRdd.collect() 结果就是 0
    //多加: 转换算子中多次调用累加器, 就会执行多次。如两次: mapRdd.collect()  mapRdd.collect() 20
    //一般情况下, 累加器会放置在行动算子进行操作
    mapRdd.collect()
    println(sumAcc.value)
    sc.stop()
  }

自定义累加器

自定义累加器类型的功能在1.X版本中就已经提供了,但是使用起来比较麻烦,在2.0版本后,累加器的易用性有了较大的改进,而且官方还提供了一个新的抽象类:AccumulatorV2来提供更加友好的自定义类型

累加器的实现方式。实现自定义类型累加器需要继承AccumulatorV2并覆写要求的方法。

copy每个节点都要copy Driver端的;每个节点再对它进行重置reset;add在自己各自节点操作;merge是其他Executor节点中的和Driver端的进行合并;

自定义一个wordcount累加器

def main(args: Array[String]): Unit = {
    val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("Spark-Transform")
    val sc: SparkContext = new SparkContext(sparkConf)
    val rdd = sc.makeRDD(List("Hello", "Spark", "Hello")) //创建RDD
    //累加器: WordCount
    //创建累加器对象
    val wcAcc = new MyAccumulator
    //向Spark进行注册
    sc.register(wcAcc, "wordcountAcc")
    rdd.foreach(
      word => {
        //数据的累加(使用累加器)
        wcAcc.add(word)
      }
    )
    //获取累加器累加的结果
    println(wcAcc.value) // Map(Hello -> 2, Spark -> 1)
    sc.stop()
  }

  //继承AccumulatorV2, 定义泛型
  //  IN: 累加器输入的数据类型String
  //  OUT: 累加器返回的数据类型 mutable.Map[String, Long]
  class MyAccumulator extends AccumulatorV2[String, mutable.Map[String, Long]] {
    private var wcMap = mutable.Map[String, Long]()
    //判断是否为初始状态
    override def isZero: Boolean = {
      wcMap.isEmpty
    }
    override def copy(): AccumulatorV2[String, mutable.Map[String, Long]] = {
      new MyAccumulator()
    }
    override def reset(): Unit = {
      wcMap.clear()
    }
    //获取累加器需要计算的值
    override def add(word: String): Unit = {
      val newCnt = wcMap.getOrElse(word, 0L) + 1
      wcMap.update(word, newCnt)
    }
    //Driver 合并多个累加器
    override def merge(other: AccumulatorV2[String, mutable.Map[String, Long]]): Unit = {
      //两个map的合并
      val map1 = this.wcMap
      val map2 = other.value
      map2.foreach {
        case (word, count) => {
          val newCount = map1.getOrElse(word, 0L) + count
          map1.update(word, newCount)
        }
      }
    }
    //累加器结果
    override def value: mutable.Map[String, Long] = {
      wcMap
    }
  }

 

//自定义一个类:
class MyAcc1 extends AccumulatorV2[Int, Int]{
  private var init = 0

  //判断是否为空
  override def isZero: Boolean = init == 0
  //复制
  override def copy(): AccumulatorV2[Int, Int] = {
    val acc: MyAcc1 = new MyAcc1
    acc.init = this.init
    acc
  }

  override def reset(): Unit = { //重置
    init = 0
  }

  override def add(v: Int): Unit = { //累加
    init += v
  }

  override def merge(other: AccumulatorV2[Int, Int]): Unit = { //合并
    init += other.value
  }

  override def value: Int = init //返回值
}
 调用自定义累加器

object TestAcc {
  def main(args: Array[String]): Unit = {
    //初始化sc
    val conf: SparkConf = new SparkConf().setAppName("WordCount").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)

    val rdd: RDD[Range.Inclusive] = sc.makeRDD(1 to 4) //创建RDD
    val acc: MyAcc1 = new MyAcc1 //创建自定义累加器对象
    
    //注册累加器, 在Driver中sc
    sc.register(acc, "MyAcc1")

    rdd.foreach(x => { //在行动算子中对累加器的值进行修改
      acc.add(1)
      println(x) //2 1 4 3
    })
    println("累加器:" + acc.value) //打印累加器的值 累加器:4
    sc.stop() //关闭SparkContext

  }
}
View Code

7. 广播变量

闭包数据,都是以Task为单位发送的,每个任务中包含闭包数据 这样可能会导致,一个Executor中包含大量重复的数据,并且占用大量的内存。

Executor其实就是一个JVM,所以在启动时,会自动分配内存。完全可以将任务中的闭包数据放置在Executor的内存中,达到共享的目的。

Spark中的广播变量就可以将闭包的数据保存到Executor的内存中;

Spark中的广播变量不能够更改;

广播变量是分布式共享只读变量

(调优策略-不用它也可以实现功能,作为调优使用)

* Broadcast a read-only variable to the cluster, returning a
* [[org.apache.spark.broadcast.Broadcast]] object for reading it in distributed functions.
* The variable will be sent to each cluster only once.

广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起来都很顺手。 在多个并行操作中使用同一个变量,但是 Spark会为每个任务分别发送。

scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))

broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(35)

scala> broadcastVar.value

res33: Array[Int] = Array(1, 2, 3)

使用广播变量的过程如下:

(1) 通过对一个类型T的对象调用SparkContext.broadcast创建出一个Broadcast[T]对象。任何可序列化的类型都可以这么实现。

(2) 通过value属性访问该对象的值(在Java中为value()方法)。

(3) 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)。

    val rdd: RDD[(String, Int)] = sc.makeRDD(List(
      ("a", 1), ("b", 2), ("c", 3)
    ))

    val rdd2: RDD[(String, Int)] = sc.makeRDD(List(
      ("a", 4), ("b", 5), ("c", 6)
    ))
    //join会导致数据量几何增长, 并且会影响shuffle的性能, 不推荐使用
    val joinRdd: RDD[(String, (Int, Int))] = rdd.join(rdd2)
 ---->
(a,(
1,4)) (b,(2,5)) (c,(3,6)) ---------------利用广播变量实现join的功能--------------------- val rdd: RDD[(String, Int)] = sc.makeRDD(List( ("a", 1), ("b", 2), ("c", 3) )) //(a, 1), (b, 2), (c, 3) //(a,(1,4)) (b,(2,5)) (c,(3,6)) val map = mutable.Map(("a", 4), ("b", 5), ("c", 6)) //封装广播变量 val bc: Broadcast[mutable.Map[String, Int]] = sc.broadcast(map) rdd.map{ case (w, c) => { //方法 广播变量 val l: Int = bc.value.getOrElse(w, 0) (w, (c, l)) } }.collect().foreach(println)

 

 

object TestBroadcast {
  def main(args: Array[String]): Unit = {
    //初始化sc
    val conf: SparkConf = new SparkConf().setAppName("WordCount").setMaster("local[*]")
    val sc = new SparkContext(conf)
    sc.setLogLevel("ERROR")

    val rdd: RDD[String] = sc.makeRDD(List("kris", "alex", "smile"))
    val temp = "ris"   //
    sc.broadcast(temp) //广播变量是Driver给每个Executor发一份,而不是每个Task(如果不是广播变量就会给每个task发送),每个task共享;减小数据传输量 ;

  /**
      * 累加器和广播变量的区别:
      * 都是共享变量
      * 累加器只能写
      * 广播变量只能读
      */
    val result = rdd.filter(x => {
      x.contains(temp)
    })
    result.foreach(println(_)) //kris


  }
}

 

posted @ 2019-04-10 08:45  kris12  阅读(759)  评论(0编辑  收藏  举报
levels of contents