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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章