RDD 总结

RDD 中的传递

在实际开发中,我们常常定义一些对应rdd的操作,那么需要注意的是。初始化工作是所在Driver 端进的,而实际运行程序是在Executor 端进行的,这就涉及到了跨进程通信,需要序列化。

传递一个方法 、传递一个类的属性、一个类的字符串 序列化机制

package com.ypl.bigdata.spark

import org.apache.spark.rdd.RDD
//自定义类
class Search (query:String) extends Serializable {

    // 过滤出包含字符串的数据

    def isMatch(s:String):Boolean={
      s.contains(query)
    }

    // 过滤出包含字符串的RDD
    def getMatch1(rdd:RDD[String]):RDD[String]={
		// 传递方法
        rdd.filter(isMatch)
    }

    // 过滤出包含字符串的rdd
    def getMatch2(rdd:RDD[String]):RDD[String]={
      // 属性转换为字符串,不需要序列化	
      val q = query
      //传递属性
      rdd.filter(x=>x.contains(query))
    }

}

// 伴生对象入口
package com.ypl.bigdata.spark

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object Spark15_Serializable {
    def main(args: Array[String]): Unit = {

      val config: SparkConf = new SparkConf().setMaster("local[*]").setAppName("rdd_serialble")

      // 创建 SPARK 上下文对象
      val sc = new SparkContext(config)

      val rdd:RDD[String] = sc.parallelize(Array("hadoop","spark","hive","ypl"))

      val search = new Search("h")


      val match1 = search.getMatch1(rdd)

      match1.collect().foreach(println)



      sc.stop()
    }
}

RDD 依赖关系

血缘关系(Lineage)
RDD 只支持粗粒度的转换,即在大量记录上执行的单个操作。将创建 RDD 的一系列 Lineage记录下来,以便恢复丢失的分区。RDD 的 Lineage 会记录RDD 的元数据信息和转换行为,当该RDD 部分分区数据丢失时,他可以根据这些信息来重新元算和恢复丢失的数据分区。

// 读取一个HDFS 文件并将其内容映射成一个个元组
scala> val wordAndOne = sc.textFile("/fruit.tsv").flatMap(_.split("\t")).map((_,1))
// 统计每一种key对应的个数
scala> val wordAndCount = wordAndOne.reduceByKey(_+_)
// 查看 wordAndOne 的 Lineage
scala> wordAndOne.toDebugString
// 查看 wordAndCount 的 Lineage
scala> wordAndCount.toDebugString

宽依赖与窄依赖
窄依赖是每个父RDD 的 Partition 最多被一个子RDD 的一个 Partition使用,类似独生子女。
在这里插入图片描述
宽依赖的是多个子RDD 的Partition会以来同一个父RDD 的Partition,会引起 shuffle,类似超生。在这里插入图片描述
DAG
有向无环图,原始的rdd 通过一系列的转换就形成了DAG,根据RDD 之间的依赖关系的不同将DAG 划分成不同的Stage。
窄依赖 partition的转换处理在stage中完成计算。
宽依赖,shuffle的存在只能在parent RDD 处理完后,才能开始接下来的计算,因此宽依赖是划分Stage的依据。
在这里插入图片描述

任务划分

RDD 任务切分中间分为: Application、Job、 Stage 和 Task。
1)Application: 初始化一个SparkContext 即生成一个Application。
2)Job: 一个Action 算子就会生成一个Job。
3)Stage: 根据RDD 之间的依赖关系的不同将Job 划分成不同的Stage,遇到一个宽依赖则划分为一个Stage。
4)Task:Stage 是一个TaskSet, 将Stage 划分的结果发送到不同的Executor 执行即为一个Task。
注意: Application -> Job -> Stage -> Task 每一层都是一对 n 的关系。

一个应用(app) 多次调用了行动算子(在main方法调用)产生多个Job
一个Job 里面会有多个阶段(对一个rdd 进行 flatmap、map、reducebykey)
一个stage 里面有多个分区, 每个分区又一个任务。发往Executor执行。

stage = 1 + shuffle 的个数

RDD 缓存 & 检查点
RDD 通过persist 方法或 cache 方法可以将前面的计算结果缓存,默认情况下, persist() 会把数据以序列化的形式缓存在 JVM 的堆空间中。
但是并不是这两个方法被调用时立即缓存,而是出发后面的action时,该 RDD 将会被缓存在计算节点的内存中,并供后面重用。

def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)

def cache(): this.type = persist() 
通过查看源码发现cache 最终也是调用了 persist 方法,默认的存储级别都是仅在内存存储一份,Spark的存储级别还有好多种,存储级别在 object StorageLevel 中定义。
object StorageLevel extends scala.AnyRef with scala.Serializable {
  val NONE : org.apache.spark.storage.StorageLevel = { /* compiled code */ }
  val DISK_ONLY : org.apache.spark.storage.StorageLevel = { /* compiled code */ }
  val DISK_ONLY_2 : org.apache.spark.storage.StorageLevel = { /* compiled code */ }
  val MEMORY_ONLY : org.apache.spark.storage.StorageLevel = { /* compiled code */ }
  val MEMORY_ONLY_2 : org.apache.spark.storage.StorageLevel = { /* compiled code */ }
  val MEMORY_ONLY_SER : org.apache.spark.storage.StorageLevel = { /* compiled code */ }
  val MEMORY_ONLY_SER_2 : org.apache.spark.storage.StorageLevel = { /* compiled code */ }
  val MEMORY_AND_DISK : org.apache.spark.storage.StorageLevel = { /* compiled code */ }
  val MEMORY_AND_DISK_2 : org.apache.spark.storage.StorageLevel = { /* compiled code */ }
  val MEMORY_AND_DISK_SER : org.apache.spark.storage.StorageLevel = { /* compiled code */ }
  val MEMORY_AND_DISK_SER_2 : org.apache.spark.storage.StorageLevel = { /* compiled code */ }
  val OFF_HEAP : org.apache.spark.storage.StorageLevel = { /* compiled code */ }
}

缓存有可能丢失,或者存储存储于内存的数据由于内存不足而被删除,RDD的缓存容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列转换,丢失的数据会被重算,由于RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition。

//创建一个RDD
scala> val rdd = sc.makeRDD(Array("atguigu"))
rdd: org.apache.spark.rdd.RDD[String] = ParallelCollectionRDD[19] at makeRDD at :25

// 将RDD转换为携带当前时间戳不做缓存
scala> val nocache = rdd.map(_.toString+System.currentTimeMillis)
nocache: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[20] at map at :27

// 多次打印结果
scala> nocache.collect
res0: Array[String] = Array(atguigu1538978275359)

scala> nocache.collect
res1: Array[String] = Array(atguigu1538978282416)

scala> nocache.collect
res2: Array[String] = Array(atguigu1538978283199)

// 将RDD转换为携带当前时间戳并做缓存
scala> val cache =  rdd.map(_.toString+System.currentTimeMillis).cache
cache: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[21] at map at :27

//多次打印做了缓存的结果
scala> cache.collect
res3: Array[String] = Array(atguigu1538978435705)                                  

scala> cache.collect
res4: Array[String] = Array(atguigu1538978435705)

scala> cache.collect
res5: Array[String] = Array(atguigu1538978435705)

用checkpoint 要指定目录

sc.setCheckpointDir("cp")
rdd.checkpoint()

RDD 分区器

spark 目前支持Hash 分区 和 Range 分区,用户可以自定义分区, Hash 分区为当前的默认分区,Spark 中分区器直接决定了 RDD 中分区的个数、 RDD 中每条数据经过 shuffle 过程属于哪个分区和 Reduce 的个数。
注意:
(1) 只有 Key - Value 类型的RDD 才有分区器,非 Key - Value 类型的RDD 分区器的值是 None。
(2) 每个 RDD 的分区 ID 范围: 0 ~ numPartitions-1,决定这个值是属于哪个分区的,

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

def getPartition(key:Any):Int = key match{
	case null => 0
	case => Utils.nonNegativeMod(key.hashCode,numPartitions)
}

def nonNegativeMod(x: Int,mod:Int):Int = {
	val rawMod = x % mod
	rawMod + (if(rawMod< 0 ) mode else 0)
}

// 创建一个pairRDD
scala > val pairs = sc.parallelize(List((1,1),(2,2),(3,3)))
// 查看RDD 的分区器
scala > paris.partitioner
// 导入 HashPartitioner 类
import org.apache.spark.HashPartitioner
// 使用 HashPartitioner 对 RDD 进行重新分区
val partitioned = pairs.partitionBy(new HashPartitioner(2))

partitioned.partitioner

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

Ranger 分区
HashPartitioner 分区弊端: 可能导致每个分区中数据量不均匀,极端情况下会导致某些分区拥有rdd 的全部数据。
RangerPartitioner 作用: 将一定范围内的数映射到某一个分区内,尽量保证每个分区数据量的均匀,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分区内的元素小或者大,但是分区的元素是不能保证顺序的。简单的说就是将一定范围内的数映射到某一个分区内。
实现过程如下:
1、先重整个RDD 中抽取出样本数据,将样本数据排序,计算出每个分区的最大key值,形成一个Array[KEY] 类型的数组变量 rangeBouds;
2、判断key 在 rangeBounds 所处的范围,给出该key值在下一个rdd 中的分区id 下标;该分区器要求RDD 中的Key类型必须是可以排序的。

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

(1)numPartitions: Int:返回创建出来的分区数。
(2)getPartition(key: Any): Int:返回给定键的分区编号(0到numPartitions-1)。
(3)equals():Java 判断相等性的标准方法。这个方法的实现非常重要,Spark 需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样 Spark 才可以判断两个 RDD 的分区方式是否相同。
需求:将相同后缀的数据写入相同的文件,通过将相同后缀的数据分区到相同的分区并保存输出来实现。

使用自定义的 Partitioner 是很容易的:只要把它传给partitionBy() 方法即可。Spark 中有许多依赖于数据混洗的方法,比如 join() 和 groupByKey(),它们也可以接收一个可选的 Partitioner 对象来控制输出数据的分区方式。

1)创建一个pairRDD
scala> val data = sc.parallelize(Array((1,1),(2,2),(3,3),(4,4),(5,5),(6,6)))
data: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[3] at parallelize at :242)定义一个自定义分区类
scala> :paste
// Entering paste mode (ctrl-D to finish)
class CustomerPartitioner(numParts:Int) extends org.apache.spark.Partitioner{

  //覆盖分区数
  override def numPartitions: Int = numParts

  //覆盖分区号获取函数
  override def getPartition(key: Any): Int = {
    val ckey: String = key.toString
    ckey.substring(ckey.length-1).toInt%numParts
  }
}

// Exiting paste mode, now interpreting.
defined class CustomerPartitioner
3)将RDD使用自定义的分区类进行重新分区

scala> val par = data.partitionBy(new CustomerPartitioner(2))
par: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[2] at partitionBy at :274)查看重新分区后的数据分布
scala> par.mapPartitionsWithIndex((index,items)=>items.map((index,_))).collect

res3: Array[(Int, (Int, Int))] = Array((0,(2,2)), (0,(4,4)), (0,(6,6)), (1,(1,1)), (1,(3,3)), (1,(5,5)))

数据读取和保存

Spark的数据读取及数据保存可以从两个维度来作区分:文件格式以及文件系统。
文件格式分为:Text文件、Json文件、Csv文件、Sequence文件以及Object文件;
文件系统分为:本地文件系统、HDFS、HBASE以及数据库。

文件类数据读取与保存

text 文件读取:

	val hdfsFile = sc.textFile("hdfs://hadoop102:9000/fruit.txt")

text 文件保存:

hdfsFile.saveAsTextFile("/fruitOut")

Json 文件
如果 JSON 文件中每一行就是一条 JSON 记录,那么可以通过将JSON 文件当作文本文件来读取,然后利用相关的JSON 库对每一条数据进行JSON 解析。
使用 RDD 读取JSON 文件处理很复杂,同时SparkSQL 集成了很好的处理JSON 文件的方式,所以应用中多采用Sparksql 处理 JSON 文件。

import scala.util.parsing.json.JSON
// 上传文件到HDFS
hadoop fs -put ~/people.json /
val json = sc.textFile("/people.json")
val result = json.map(JSON.parseFull)
result.collect()

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

val rdd = sc.parallelize(Array((1,2),(3,4),(5,6)))
rdd.saveAsSequenceFile("file:///opt/module/spark/seqFile")
//读取 sequence 文件
val seq = sc.sequenceFile[Int,Int]("file:///opt/module/spark/seqFile")
seq.collect

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

val rdd = sc.parallelize(Array(1,2,3,4))
rdd.saveAsObjectFile("file:///opt/module/spark/seqFile")
//读取Object文件
scala> val objFile = sc.objectFile[(Int)]("file:///opt/module/spark/objectFile")
objFile: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[31] at objectFile at :24
//打印读取后的Sequence文件
scala> objFile.collect
res19: Array[Int] = Array(1, 2, 3, 4)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章