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 :24
(2)定義一個自定義分區類
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 :27
(4)查看重新分區後的數據分佈
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)