Java IO回憶
字節跟字符區別(基礎圖),InputStream、OutputStream、Writer、Reader 。
字節流,分2種:
a.輸入
b.輸出
2.字符流,也分2種:
a.輸入(讀,即讀取)
b.輸出(寫,即寫入)
PS
: 字節流是萬能的,方便人類讀寫纔出來的字符流。字符流就包裝後的字節流。
文件字節流讀入
InputStream in = new FIleInputStream("xxx")
緩衝流, 體現了Java裝飾者模式
InputStream bufferIn = new BufferedInputStream(new FileInputStream("xxxx"))
InputStream in = new FIleInputStream("xxx")
一行行的讀取字符, 字節到字符的轉換需要注意下
Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream("xxx"),"UTF-8"))
當我們真正當readLine的時候纔會不斷的向前請求需求讀取數據,真正數據數據的只有一個類,其餘的無非就是裝飾再裝飾而已。
InputStreamReader 類是從字節流到字符流的橋接器:它使用指定的字符集讀取字節並將它們解碼爲字符
End:Java中的IO不斷包裝,包裝的是結構的變化
,只有在readLine的時候纔會不斷向上反帶調。
1. RDD
模仿上面的Java IO 不斷的對基礎類進行裝飾
val sc = new SparkContext(conf)
//3.使用sc創建RDD並執行相應的transformation和action
val line: RDD[String] = sc.textFile(args(0))
val words: RDD[String] = line.flatMap(_.split(" "))
val wordToOne: RDD[(String, Int)] = words.map((_, 1))
val wordSum: RDD[(String, Int)] = wordToOne.reduceByKey(_ + _)
val result: Array[(String, Int)] = wordSum.collect()
這裏面基類就是RDD
。上面的流程圖就是在封裝邏輯思維
,用到的就是Scala中的 控制抽象 功能。也就是把一個邏輯思維傳遞給一個函數,就是把函數當參數傳遞的意思。上面的各種邏輯封裝只有調用了Collect 纔會觸發收集數據對功能
。
控制抽象:
def myShop(block: => Unit) {
println("Welcome in!")
block
println("Thanks for coming!")
}
def main(args: Array[String]){
myShop( println("I wanna buy a condom") )
}
什麼是RDD
RDD(Resilient Distributed Dataset
)叫做彈性分佈式數據集
,是Spark中最基本的數據處理邏輯抽象。,工作中可以認爲是一個Seq,代碼中是一個抽象類,它代表一個彈性的、不可變、可分區、裏面的元素可並行計算的集合。
不可變
:可以類比Java中的String,只是不提供可以改變的接口。操作了數據就返回一個新的String或者RDD。
可分區
:Kafka、HBase都是可分區的,注意理解併發跟並行的概念。可以將一個大的數據集放到不同的分區中,而不同的分區正好可以對應上spark中的多個executor
。
RDD屬性
* Internally, each RDD is characterized by five main properties:
*
* - A list of partitions
* - A function for computing each split
* - A list of dependencies on other RDDs
* - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
* - Optionally, a list of preferred locations to compute each split on (e.g. block locations for
* an HDFS file)
- 一組分區(Partition),即數據集的基本組成單位;
- 一個計算每個分區的函數;
- RDD之間的依賴關係,有直接依賴或者間接依賴,也可以稱血緣。
- 一個Partitioner,即RDD的分片函數;
- 一個列表,存儲存取每個Partition的優先位置(preferred location)。
PS :大數據計算時候移動數據不如移動計算划算,把數據copy到代碼所在位置不如把代碼copy到數據所在位置。對應第五個特性。
RDD特性
RDD表只讀
的分區的數據集,對RDD進行改動,只能通過RDD的轉換操作,由一個RDD得到一個新的RDD,新的RDD包含了從其他RDD衍生所必需的信息。RDDs之間存依賴
,RDD的執行是按照血緣關係延時計算的。如果血緣關係較長,可以通過持久化
RDD來切斷血緣關係。
1. 彈性
存儲的彈性
:內存與磁盤的自動切換;
容錯的彈性
:數據丟失可以自動恢復;
計算的彈性
:計算出錯重試機制;
分片的彈性
:可根據需要重新分片。
2. 分區
RDD邏輯上是分區
的,每個分區的數據是抽象存在的,計算的時候會通過一個compute
函數得到每個分區的數據。如果RDD是通過已有的文件系統構建,則compute函數是讀取指定文件系統中的數據,如果RDD是通過其他RDD轉換而來,則compute函數是執行轉換邏輯將其他RDD的數據進行轉換。
3. 只讀
如下圖所示,RDD是隻讀的,要想改變RDD中的數據,只能在現有的RDD基礎上創建新的RDD。
由一個RDD轉換到另一個RDD,可以通過豐富的操作算子
實現,不再像MapReduce那樣只能寫map和reduce了。
RDD的操作算子包括兩類
- 一類叫做Transformations(轉換算子),它是用來將RDD進行轉化,構建RDD的血緣關係;
- 另一類叫做Actions(行動算子),它是用來觸發RDD的計算,得到RDD的相關計算結果或者將RDD保存的文件系統中。
算子:
從認知心理學角度看解決問題其實是
將問題的初始狀態通過一系列的操作(算子(Operate))來對問題的狀態進行轉換
,然後最終達到完成解決的狀態。Spark 中的函數就是算子
4. 依賴
RDDs通過操作算子進行轉換,轉換得到的新RDD包含了從其他RDDs衍生所必需的信息,RDDs之間維護着這種血緣關係,也稱之爲依賴
。如下圖所示,依賴包括兩種,一種是窄依賴
,RDDs之間分區是一一對應的,另一種是寬依賴
,下游RDD的每個分區與上游RDD(也稱之爲父RDD)的每個分區都有關,是多對多的關係。
5. 緩存
如果在應用程序中多次
使用同一個RDD,可以將該RDD緩存
起來,該RDD只有在第一次計算的時候會根據血緣關係得到分區的數據,在後續其他地方用到該RDD的時候,會直接從緩存處取而不用再根據血緣關係計算,這樣就加速後期的重用。如下圖所示,RDD-1經過一系列的轉換後得到RDD-n並保存到hdfs,RDD-1在這一過程中會有個中間結果,如果將其緩存到內存,那麼在隨後的RDD-1轉換到RDD-m這一過程中,就不會計算其之前的RDD-0了。
6. CheckPoint
雖然RDD的血緣關係天然地可以實現容錯,當RDD的某個分區數據失敗或丟失,可以通過血緣關係重建
。但是對於長時間迭代型應用來說,隨着迭代的進行,RDDs之間的血緣關係會越來越長,一旦在後續迭代過程中出錯,則需要通過非常長的血緣關係去重建,勢必影響性能。爲此,RDD支持checkpoint
將數據保存到持久化的存儲
中,這樣就可以切斷之前的血緣關係,因爲checkpoint後的RDD不需要知道它的父RDDs了,它可以從checkpoint處拿到數據。
2. RDD 編程
編程模型
在Spark中,RDD被表示爲對象
,通過對象上的方法調用來對RDD進行轉換。經過一系列的transformations
定義RDD之後,就可以調用actions觸發RDD的計算,action可以是嚮應用程序返回結果(count, collect等),或者是向存儲系統保存數據(saveAsTextFile
等)。在Spark中,只有遇到action,纔會執行
RDD的計算(即延遲計算 lazy module),這樣在運行時可以通過管道的方式傳輸多個轉換。
要使用Spark,開發者需要編寫一個Driver程序(這裏說的都是在Spark自帶的集羣中哦,如果依託於YARN 則只要隨便找一個spark服務器連接Hadoop集羣吧任務提交上去即可),它被提交到集羣以調度運行Worker,如下圖所示。Driver中定義了一個或多個RDD,並調用RDD上的action,Worker則執行RDD分區計算任務。
RDD創建
在Spark中創建RDD的創建方式可以分爲三種:
- 從集合中創建RDD
- 從外部存儲創建RDD
- 從其他RDD創建
從集合中創建RDD
從集合中創建RDD,Spark主要提供了兩種函數:parallelize
和makeRDD
object SparkRDD01 {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.OFF)
Logger.getLogger("akka").setLevel(Level.OFF)
//1.創建SparkConf並設置App名稱
val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")
//2.創建SparkContext,該對象是提交Spark App的入口
val sc = new SparkContext(conf)
// 1. 從內存中創建RDD 底層實現就是 parallelize
val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
// 2. 從內存中創建 看調用底層會發現有關聯
val arrayRDD:RDD[Int] = sc.parallelize(Array(1, 2, 3, 4, 5, 6))
listRDD.collect().foreach(println)
}
}
從外部存儲創建RDD
包括本地的文件系統,還有所有Hadoop支持的數據集,比如HDFS、Cassandra、HBase等。默認從文件中讀取的都是String類型。
val lines:RDD[String] = sc.textFile("in")
scala> val rdd2= sc.textFile("hdfs://hadoop102:9000/RELEASE")
rdd2: org.apache.spark.rdd.RDD[String] = hdfs://hadoop102:9000/RELEASE MapPartitionsRDD[4] at textFile at <console>:24
從其他RDD創建
參考WordCount即可。
分區
val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
listRDD.saveAsTextFile("outputfile")
輸出結果:
def makeRDD[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T] = withScope {
parallelize(seq, numSlices)
}
---
def defaultParallelism: Int = {
assertNotStopped()
taskScheduler.defaultParallelism
}
---
def defaultParallelism(): Int ctrl +H 看當前類子類的實現
---
override def defaultParallelism(): Int = backend.defaultParallelism()
----
SchedulerBackend的 特質的父類,
---
CoarseGrainedSchedulerBackend
override def defaultParallelism(): Int = {
conf.getInt("spark.default.parallelism", math.max(totalCoreCount.get(), 2))
// 此處的conf 就是sc 中定義的conf
}
---
def getInt(key: String, defaultValue: Int): Int = {
getOption(key).map(_.toInt).getOrElse(defaultValue)
}
---
totalCoreCount.addAndGet(cores)
// 本地模式 所用核數
val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")
結論: 分區跟 local[*]有關。內存中的默認跟配置的核數有關。
指定分區個數:
看下 讀入文件寫入後的分區數:
val listRDD: RDD[String] = sc.textFile("in")
listRDD.saveAsTextFile("outputfile")
結果是:
def textFile(
path: String,
minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
assertNotStopped()
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString).setName(path)
}
---
def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
讀入文件時候指定分區個數寫出的的時候就按照分區個數輸出:
val listRDD: RDD[String] = sc.textFile("in",3)
listRDD.saveAsTextFile("outputfile")
sc.textFile 注意點:
比如in文件夾下的word.text
12345
邏輯思維應該是將數據分成了2份,可是我們看下里面代碼:
用到了Hadoop切分(Spark分區規則跟Hadoop規則完全一樣
), 5/2 = 2,2,1 這樣的三份。
並且:你可以看到part-00000有數據其餘兩個是沒有的!這說明計算分區跟往分區中寫入數據是兩個不同的操作
!,Hadoop默認是按照行來搞的!更細節要參考 20P
End
:
- 讀取文件時,傳遞的分區參數爲最小分區數,但是不一定是這個分區數, 取決於Hadoop讀取文件時候分片規則。
- 內存中分區相對來說很簡單,看參數即可。
RDD轉換(重點)
RDD整體上分爲Value類型和Key-Value類型
Value型
1. map(fun)
作用:返回一個新的RDD,該RDD由每一個輸入元素經過func函數轉換後組成。每個元素操作,分區不變。同時這個_ * 2
應該會把執行任務分發到若干個Executor
中。
val listRDD :RDD[Int] = sc.makeRDD(1 to 10)
val mapRDD:RDD[Int] = listRDD.map(_ * 2)
mapRDD.collect().foreach(println)
2. mapPartitions(func)
作用:類似於map,但獨立地在RDD的每一個分片(分區)上運行,因此在類型爲T的RDD上運行時,func的函數類型必須是Iterator[T]
=> Iterator[U]
。假設有N個元素,有M個分區,那麼map的函數的將被調用N
次,而mapPartitions被調用M
次,一個函數一次處理所有分區。
PS:這個跟Map的算子差別就是通過網絡傳輸分發任務的次數,效率上這個更好點。
val listRDD :RDD[Int] = sc.makeRDD(1 to 10)
// mapPartitions 可以對一個RDD中所有分區進行遍歷
// mapPartitions 效率優於Map 算子,減少來餓發送到執行器交互次數
// mapPartitions 單獨算子更加喫內存,可能出現內存溢出(OOM)
val mapPartitionsRDD:RDD[Int] = listRDD.mapPartitions(datas=>{
datas.map(data=>data*2)
// 這裏對 datas 是Scala中的 iterator, map 也是Scala中的map 。{}裏面的整體邏輯算一個Spark中的一個計算。
})
mapPartitionsRDD.collect().foreach(println)
3. mapPartitionsWithIndex
類似於python中的enumerate
,在2的基礎上想要知道數據都在那個分區中。
作用
:類似於mapPartitions,但func帶有一個整數參數表示分片的索引值,因此在類型爲T的RDD上運行時,func的函數類型必須是(Int, Interator[T]) => Iterator[U];
val listRDD: RDD[Int] = sc.makeRDD(1 to 10)
val tupleRDD: RDD[(Int, String)] = listRDD.mapPartitionsWithIndex {
case (num, datas) => {
datas.map((_, " 分區號:" + num))
}
// 模式匹配
}
tupleRDD.collect().foreach(println)
4. map、mapPartition、mapPartitionWithIndex區別。
- map():每次處理一條數據。
- mapPartition():每次處理一個分區的數據,這個分區的數據處理完後,原RDD中分區的數據才能釋放,可能導致OOM。
- 開發指導:當內存空間較大的時候建議使用mapPartition(),以提高處理效率。
- mapPartitionWithIndex則關注的是數據來自那個分區。
5. flatMap
可以認爲是跟Scala中的類似,就是個扁平化的操作。
val listRDD: RDD[List[Int]] = sc.makeRDD(Array(List(1, 2), List(3, 4)))
val flatMapRDD: RDD[Int] = listRDD.flatMap(datas => datas)
flatMapRDD.collect().foreach(println)
6. glom
作用:將每一個分區形成一個數組,形成新的RDD類型時RDD[Array[T]],相當於系統自動提供了一個分區函數,然後接下來你可以按照分區來求的每個分區中的min,max,sum,avg等等。
val listRDD: RDD[Int] = sc.makeRDD(1 to 16, 4)
// 如果是 1 to 18 ,4 則劃分爲 4 ,4,4,6
// 將一個分區的數據自動放到一個數組中
val glomRDD: RDD[Array[Int]] = listRDD.glom()
glomRDD.collect().foreach(array => println(array.mkString(",")))
結果:
5,6,7,8
9,10,11,12
13,14,15,16
7. groupBy
作用:分組,按照傳入函數的返回值進行分組。將相同的key對應的值放入一個迭代器。
// 按照指定規則進行分組
// 分組後的數據形成2元祖 K-V,K表示分組的key,V表示分組數據集合。
val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
val groupByRDD: RDD[(Int, Iterable[Int])] = listRDD.groupBy(_ % 2)
groupByRDD.collect().foreach(println)
8. filter
作用:過濾。返回一個新的RDD,該RDD由經過func函數計算後返回值爲true的輸入元素組成。
// 按照規則過濾數據
val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
val filterRDD: RDD[Int] = listRDD.filter(_ % 2 == 0)
filterRDD.collect().foreach(print)
9. sample(withReplacement, fraction, seed)
功能:隨機性抽取一些數據。
withReplacement:表示抽出樣本後是否在放回去,true表示會放回去,這也就意味着抽出的樣本可能有重複
fraction :這是一個double類型的參數(0-1之間),設定一個出現概率閾值,通過seed給每個數據評價出一個概率,大於閾值留下,否則不留。
seed:表示一個種子,根據這個seed隨機抽取,一般情況下只用前兩個參數就可以,那麼這個參數是幹嘛的呢,這個參數一般用於調試,有時候不知道是程序出問題還是數據出了問題,就可以將這個參數設置爲定值。一般用System.currentTimeMilles確保種子的不同性。可以參考下python的 numpy.seed,挺好玩的一個隨機數選擇器。
---
def sample(
withReplacement: Boolean,
fraction: Double,
seed: Long = Utils.random.nextLong): RDD[T] = {
require(fraction >= 0,
s"Fraction must be nonnegative, but got ${fraction}")
withScope {
require(fraction >= 0.0, "Negative fraction value: " + fraction)
if (withReplacement) {
new PartitionwiseSampledRDD[T, T](this, new PoissonSampler[T](fraction), true, seed)
// 柏松分佈 放回
} else {
new PartitionwiseSampledRDD[T, T](this, new BernoulliSampler[T](fraction), true, seed)
// 伯努利分佈 不放回
}
}
}
10. distinct([numPartitions]))
作用:對源RDD進行去重後返回一個新的RDD。
Logger.getLogger("org").setLevel(Level.OFF)
Logger.getLogger("akka").setLevel(Level.OFF)
//1.創建SparkConf並設置App名稱
val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")
//2.創建SparkContext,該對象是提交Spark App的入口
val sc = new SparkContext(conf)
val listRDD: RDD[Int] = sc.makeRDD(List(1, 2, 1, 5, 2, 9, 6, 1))
// 默認是 系統全部進程來分區數據 我這裏是8個
val distinctRDD: RDD[Int] = listRDD.distinct()
// 對數據進行 shuffle 刷新重組 然後數據還是8個分區, 因爲 去重後可能少於8個,可以指定分區數
val distinctRDD1: RDD[Int] = listRDD.distinct(2)
distinctRDD.collect().foreach(println)
distinctRDD.saveAsTextFile("output")
shuffle: 表示對一個分區的數據進行打亂拆分到不同的分區 ,不過如果是下面這樣不算是shuffle打亂哦。
11. coalesce(numPartitions)
作用:縮減分區數,用於大數據集過濾後,提高小數據集的執行效率。
// 縮減分區可以認爲簡單對分區合併,沒有涉及到shuffle
val listRDD: RDD[Int] = sc.makeRDD(1 to 16, 4)
println("縮減分區前:" + listRDD.partitions.size)
val coalesceRDD: RDD[Int] = listRDD.coalesce(3)
println("縮減分區後:" + coalesceRDD.partitions.size)
coalesceRDD.saveAsTextFile("output")
12. repartition(numPartitions)
作用:根據分區數,重新通過網絡隨機洗牌所有數據,repartitionAndSortWithinPartitions
val listRDD: RDD[Int] = sc.makeRDD(1 to 16, 4)
println("縮減分區前:" + listRDD.partitions.size)
val reRDD: RDD[Int] = listRDD.repartition(2)
println("縮減分區後:" + reRDD.partitions.size)
reRDD.saveAsTextFile("output")
13. coalesce和repartition的區別
- coalesce重新分區,suffle=false,可以選擇是否進行shuffle過程。由參數shuffle: Boolean = false/true決定。
- repartition實際上是調用的coalesce,進行shuffle。源碼如下:
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
coalesce(numPartitions, shuffle = true)
}
14. sortBy(func,[ascending], [numTasks])
作用:使用func先對數據進行處理,按照處理後的數據比較結果排序,默認爲正序,排序後的數據分區數多少也可以指定。
val listRDD: RDD[Int] = sc.makeRDD(1 to 16, 4)
listRDD.sortBy(x=>x).collect().foreach(println) // 從小到大
listRDD.sortBy(x=>x,false).collect().foreach(println) // 從大到小
15. pipe(command, [envVars])
作用:管道,針對每個分區,都執行一個shell腳本,返回輸出的RDD。
注意
:腳本需要放在Worker節點可以訪問到的位置
#!/bin/sh
echo "Running shell script"
while read LINE; do
echo ${LINE}
done
scala:
package test
import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
object PipeTest {
def main(args: Array[String]) {
val sparkConf = new SparkConf().setAppName("pipe Test")
val sc = new SparkContext(sparkConf)
val data = List("hi", "hello", "how", "are", "you")
val dataRDD = sc.makeRDD(data)
val scriptPath = "/home/gt/spark/bin/echo.sh"
val pipeRDD = dataRDD.pipe(scriptPath)
print("! "+pipeRDD.count()+" !!!")
//輸出6,如果是sc.makeRDD(data,2)輸出7
sc.stop()
}
}
雙Value類型交互
1. union(otherDataset)
作用:對源RDD和參數RDD求並集後返回一個新的RDD,有重複的也不會消除哦
val listRDD1: RDD[Int] = sc.makeRDD(1 to 6)
val listRDD2: RDD[Int] = sc.makeRDD(5 to 8)
val totalRDD: RDD[Int] = listRDD1.union(listRDD2)
// 1234565678
2. subtract (otherDataset)
計算差的一種函數,去除兩個RDD中相同的元素,不同的RDD將保留下來
val listRDD1: RDD[Int] = sc.makeRDD(1 to 5)
val listRDD2: RDD[Int] = sc.makeRDD(2 to 4)
val totalRDD: RDD[Int] = listRDD1.subtract(listRDD2) // 在1 不在2
3. intersection(otherDataset)
作用:對源RDD和參數RDD求交集
後返回一個新的RDD
val listRDD1: RDD[Int] = sc.makeRDD(1 to 5)
val listRDD2: RDD[Int] = sc.makeRDD(2 to 4)
val totalRDD: RDD[Int] = listRDD1.intersection(listRDD2) // 在1且在2
4. cartesian(OtherDataset)
作用:笛卡爾積(儘量避免使用)
5. zip(OtherDataset)
作用:將兩個RDD組合成Key/Value形式的RDD,這裏默認兩個RDD的partition數量
以及元素數量
都相同,否則會拋出異常。
val listRDD1: RDD[Int] = sc.makeRDD(1 to 3,3)
val listRDD2: RDD[Int] = sc.makeRDD(Array("a","b","c"),3)
val zipRDD: RDD[(Int, Int)] = listRDD1.zip(listRDD2)
Key-Value型
RDD集合中的每一個item 都是KV形式的。裏面用到了隱式轉換,在makeRDD後根據參數的不同有不同的RDD子類實現。比如KV的RDD
PairRDDFunctions
1. partitionBy
作用:對pairRDD
進行分區操作,如果原有的partionRDD
和現有的partionRDD
是一致的話就不進行分區, 否則會生成ShuffleRDD
,即會產生shuffle
過程。
package com.sowhat
import org.apache.log4j.{Level, Logger}
import org.apache.spark.rdd.RDD
import org.apache.spark.{Partitioner, SparkConf, SparkContext}
/**
* @author sowhat
* @create 2020-06-12 15:55
*/
object WordCountLocal {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.OFF)
Logger.getLogger("akka").setLevel(Level.OFF)
//1.創建SparkConf並設置App名稱
val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")
//2.創建SparkContext,該對象是提交Spark App的入口
val sc = new SparkContext(conf)
// 縮減分區可以認爲簡單對分區合併,沒有涉及到shuffle
val rdd = sc.parallelize(Array((1,"aaa"),(2,"bbb"),(3,"ccc"),(4,"ddd")),4)
println(rdd.partitions.size)
var rdd2 = rdd.partitionBy(new org.apache.spark.HashPartitioner(2))
rdd2 = rdd.partitionBy(new MyPartition(2))
println(rdd2.partitions.size)
sc.stop()
}
}
class MyPartition(num:Int) extends Partitioner {
override def numPartitions: Int = {
num
}
override def getPartition(key: Any): Int = {
1
}
}
2. groupByKey
作用:groupByKey也是對每個key進行操作,但只生成一個
seq。
val words = Array("one","two","two","three","three","three")
val wordPairsRDD: RDD[(String, Int)] = sc.makeRDD(words).map((_,1))
val group: RDD[(String, Iterable[Int])] = wordPairsRDD.groupByKey()
val res: RDD[(String, Int)] = group.map(t=>(t._1,t._2.sum))
res.collect().foreach(println)
res.saveAsTextFile("output")
---
(two,2)
(one,1)
(three,3)
3. reduceByKey(func, [numTasks])
在一個(K,V)的RDD上調用,返回一個(K,V)的RDD,使用指定的reduce函數,將相同key的值聚合到一起,reduce任務的個數可以通過第二個可選的參數來設置。
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("so",1),("so",2),("what",3),("what",5)))
val reduce: RDD[(String, Int)] = rdd.reduceByKey(_ + _)
reduce.collect().foreach(println)
---
(what,8)
(so,3)
4. reduceByKey和groupByKey的區別
- reduceByKey:按照key進行聚合,在shuffle之前有
combine
(預聚合)操作,返回結果是RDD[k,v]. - groupByKey:按照key進行分組,直接進行shuffle。
- 開發指導:reduceByKey比groupByKey,建議使用。但是需要注意是否會影響業務邏輯。在對大數據進行復雜計算時,reduceByKey優於groupByKey。
另外,如果僅僅是group處理,那麼以下函數應該優先於 groupByKey :
(1)、combineByKey 組合數據,但是組合之後的數據類型與輸入時值的類型不一樣。
(2)、foldByKey合併每一個 key 的所有值,在級聯函數和“零值”中使用。
細節分析如下 :
val words = Array("one", "two", "two", "three", "three", "three")
val wordPairsRDD = sc.parallelize(words).map(word => (word, 1))
val wordCountsWithReduce = wordPairsRDD.reduceByKey(_ + _)
val wordCountsWithGroup = wordPairsRDD.groupByKey().map(t => (t._1, t._2.sum))
wordCountsWithReduce
和wordCountsWithGroup
是完全一樣的,但是,它們的內部運算過程是不同的。
- 當採用
reduceByKey
時,Spark可以在每個分區移動數據之前將待輸出數據與一個共用的key結合。藉助下圖可以理解在reduceByKey裏究竟發生了什麼。 注意在數據對被搬移前同一機器上同樣的key是怎樣被組合的(reduceByKey中的lamdba函數)。然後lamdba函數在每個區上被再次調用來將所有值reduce成一個最終結果。整個過程如下:
- 當採用
groupByKey
時,由於它不接收函數,spark只能先將所有的鍵值對(key-value pair)都移動,這樣的後果是集羣節點之間的開銷很大,導致傳輸延時。整個過程如下:
5. aggregateByKey
參數:
def aggregateByKey[U: ClassTag](zeroValue: U)(seqOp: (U, V) => U,
combOp: (U, U) => U): RDD[(K, U)] = self.withScope {
aggregateByKey(zeroValue, defaultPartitioner(self))(seqOp, combOp)
}
// 注意這裏U 類型有個ClassTag 有個運行時候的反射類型推斷。所以不用寫類型在後面參數中。
- 作用:在KV對的RDD中,按key將value進行分組合並,合併時,將每個value和初始值作爲seq函數的參數,進行計算,返回的結果作爲一個新的KV對,然後再將結果按照key進行合併,最後將每個分組的value傳遞給combine函數進行計算(先將前兩個value進行計算,將返回結果和下一個value傳給combine函數,以此類推),將key與計算結果作爲一個新的kv對輸出。
- 參數解釋
(1)zeroValue:給每一個分區中的每一個key一個初始值;
(2)seqOp:函數用於在每一個分區中用初始值逐步迭代value;
(3)combOp:函數用於合併每個分區中的結果。
需求
:創建一個pairRDD,取出每個分區相同key對應值的最大值,然後相加。python版解釋
val rdd = sc.parallelize(List(("a", 3), ("a", 2), ("c", 4), ("b", 3), ("c", 6), ("c", 8)), 2)
val aggRDD: RDD[(String, Int)] = rdd.aggregateByKey(0)(math.max(_, _), _ + _)
aggRDD.collect().foreach(println)
---
(b,3)
(a,3)
(c,12)
6. foldByKey
- 作用:aggregateByKey的
簡化
操作,seqop和combop相同 需求
:創建一個pairRDD,計算相同key對應值的相加結果
def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)] = self.withScope {
foldByKey(zeroValue, defaultPartitioner(self))(func)
}
代碼:
val rdd = sc.parallelize(List(("a", 3), ("a", 2), ("c", 4), ("b", 3), ("c", 6), ("c", 8)), 2)
val flodRDD: RDD[(String, Int)] = rdd.foldByKey(0)(_ + _)
flodRDD.collect().foreach(println)
---
(b,3)
(a,5)
(c,18)
---
val intRDD = sc.parallelize(List((1, 3), (1, 2), (1, 4), (2, 3), (3, 6), (3, 8)), 3)
val intFoldRDD = rdd.foldByKey(0)(_ + _)
// Array((3,14), (1,9), (2,3))
7. combineByKey[C]
原型:
def combineByKey[C](
// 注意這裏的C 就是個範型,我們是不知道這個C是什麼類型的,所以需要在調用的時候指明C的類型。
createCombiner: V => C,
// 一個分區中 每一個key下面的 value賦予個初始值
mergeValue: (C, V) => C,
// 相當於在一個分區中把 同一個K下面 不同value 值進行相同操作,然後還要對出現次數操作
mergeCombiners: (C, C) => C,
// 不同分區相同key的 數據進行彙總
numPartitions: Int): RDD[(K, C)] = self.withScope {
combineByKeyWithClassTag(createCombiner, mergeValue, mergeCombiners, numPartitions)(null)
}
- 作用:對相同K,把V合併成一個集合。
- 參數描述:
- createCombiner: combineByKey() 會遍歷分區中的所有元素,因此每個元素的鍵要麼還沒有遇到過,要麼就和之前的某個元素的鍵相同。如果這是一個新的元素,combineByKey()會使用一個叫作createCombiner()的函數來創建那個鍵對應的累加器的初始值
- mergeValue: 如果這是一個在處理當前分區之前已經遇到的鍵,它會使用mergeValue()方法將該鍵的累加器對應的當前值與這個新的值進行合併
- mergeCombiners: 由於每個分區都是獨立處理的, 因此對於同一個鍵可以有多個累加器。如果有兩個或者更多的分區都有對應同一個鍵的累加器, 就需要使用用戶提供的 mergeCombiners() 方法將各個分區的結果進行合併。
- 需求:創建一個pairRDD,根據key計算每種key的均值。(
先計算每個key出現的次數以及可以對應值的總和
,再相除得到結果
) - 需求分析:
val rdd = sc.parallelize(Array(("a", 88), ("b", 95), ("a", 91), ("b", 93), ("a", 95), ("b", 98)), 2)
val combine: RDD[(String, (Int, Int))] = rdd.combineByKey(
(_, 1),
(acc: (Int, Int), v) => (acc._1 + v, acc._2 + 1), // 數值相加, 次數+1,
(acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2) // 不同分區數據的彙總
)
combine.collect().foreach(println) // (b,(286,3)) (a,(274,3))
val result = combine.map { case (key, value) => (key, value._1 / value._2.toDouble) }
result.collect.foreach(println)
// (b,95.33333333333333)
// (a,91.33333333333333)
---
rdd.combineByKey{
x=>x,
(x:Int,y)=> x + y,
(x:Int,y:Int) => x + y
}
理解類型有時候指明 有時候不指明的原因
8. sortByKey([ascending], [numTasks])
- 作用:在一個(K,V)的RDD上調用,K
必須實現Ordered
接口,返回一個按照key進行排序的(K,V)的RDD - 需求:創建一個pairRDD,按照key的正序和倒序進行排序
val rdd = sc.parallelize(Array((3, "aa"), (6, "cc"), (2, "bb"), (1, "dd")))
val sortRDD1: RDD[(Int, String)] = rdd.sortByKey(true)
sortRDD1.collect().foreach(print) // (1,dd)(2,bb)(3,aa)(6,cc)
println('-' * 20)
val sortRDD2: RDD[(Int, String)] = rdd.sortByKey(false)
sortRDD2.collect().foreach(print) // (6,cc)(3,aa)(2,bb)(1,dd)
9. mapValues
- 針對於(K,V)形式的類型只對V進行操作
- 需求:創建一個pairRDD,並將value添加字符串"1412"
val listRDD: RDD[(Int, String)] = sc.makeRDD(Array((1,"a"),(2,"b"),(3,"c"),(4,"d")))
val resultRDD: RDD[(Int, String)] = listRDD.mapValues(_ + "1412")
resultRDD.collect().foreach(println)
/**
* (1,a1412)
* (2,b1412)
* (3,c1412)
* (4,d1412)
* */
10. join(otherDataset, [numTasks])
- 作用:在類型爲(K,V)和(K,W)的RDD上調用,返回一個相同key對應的所有元素對在一起的(K,(V,W))的RDD。可以認爲是mysql的join操作,兩個RDD個數不同時候也允許。不過只顯示同時有的!
- 需求:創建兩個pairRDD,並將key相同的數據聚合到一個元組。
val rdd1: RDD[(Int, String)] = sc.makeRDD(Array((1,"a"),(2,"b"),(3,"c"),(4,"d")))
val rdd2: RDD[(Int, Int)] = sc.makeRDD(Array((1,1),(2,2),(3,3),(4,4)))
val result1: RDD[(Int, (String, Int))] = rdd1.join(rdd2)
result1.collect().foreach(println)
// (4,(d,4)) (1,(a,1)) (2,(b,2)) (3,(c,3))
val result2: RDD[(Int, (Int,String))] = rdd2.join(rdd1)
result2.collect().foreach(println)
// (4,(4,d)) (1,(1,a)) (2,(2,b)) (3,(3,c))
11. cogroup(otherDataset, [numTasks])
- 作用:在類型爲(K,V)和(K,W)的RDD上調用,返回一個(K,(Iterable,Iterable))類型的RDD,兩個RDD不同的時候,會返回
(獨有key,(CompactBuffer(),CompactBuffer(獨有V)))
- 需求:創建兩個pairRDD,並將key相同的數據聚合到一個迭代器。
val rdd1: RDD[(Int, String)] = sc.makeRDD(Array((1,"a"),(2,"b"),(3,"c"),(4,"d")))
val rdd2: RDD[(Int, Int)] = sc.makeRDD(Array((1,1),(2,2),(3,3),(4,4)))
val coRDD: RDD[(Int, (Iterable[String], Iterable[Int]))] = rdd1.cogroup(rdd2)
coRDD.collect().foreach(println)
/**
* (4,(CompactBuffer(d),CompactBuffer(4)))
* (1,(CompactBuffer(a),CompactBuffer(1)))
* (2,(CompactBuffer(b),CompactBuffer(2)))
* (3,(CompactBuffer(c),CompactBuffer(3)))
* */
實戰
- 數據結構:時間戳,省份,城市,用戶,廣告,中間字段使用空格分割。樣本如下:
1516609143867 6 7 64 16
1516609143869 9 4 75 18
1516609143869 1 7 87 12
-
需求: 統計出每一個省份廣告被點擊次數的TOP3
-
解題思路:
- 先把省 + 廣告 當整體看 點擊數總量。
- 再把省當K,廣告 + 點擊數 當V,
- 對2 進行groupByKey
- 對3 進行排序 取值前三。
package com.sowhat
import org.apache.log4j.{Level, Logger}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* @author sowhat
* @create 2020-06-12 15:55
*/
object WordCountLocal {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.OFF)
Logger.getLogger("akka").setLevel(Level.OFF)
//1.創建SparkConf並設置App名稱
val conf: SparkConf = new SparkConf().setAppName("WC").setMaster("local[*]")
//2.創建SparkContext,該對象是提交Spark App的入口
val sc = new SparkContext(conf)
// 時間戳,省份,城市,用戶,廣告,中間字段使用空格分割。
val line: RDD[String] = sc.textFile("in/agent.log")
//3.按照最小粒度聚合:((Province,AD),1)
val provinceAdToOne = line.map { x =>
val fields: Array[String] = x.split(" ")
((fields(1), fields(4)), 1)
}
//4.計算每個省中每個廣告被點擊的總數:((Province,AD),sum)
val provinceAdToSum = provinceAdToOne.reduceByKey(_ + _)
//5.將省份作爲key,廣告加點擊數爲value:(Province,(AD,sum))
val provinceToAdSum = provinceAdToSum.map(x => (x._1._1, (x._1._2, x._2)))
//6.將同一個省份的所有廣告進行聚合(Province,List((AD1,sum1),(AD2,sum2)...))
val provinceGroup = provinceToAdSum.groupByKey()
//7.對同一個省份所有廣告的集合進行排序並取前3條,排序規則爲廣告點擊總數
val provinceAdTop3 = provinceGroup.mapValues { x =>
x.toList.sortWith((x, y) => x._2 > y._2).take(3)
}
//8.將數據拉取到Driver端並打印
provinceAdTop3.collect().foreach(println)
/**
* (4,List((12,25), (2,22), (16,22)))
* (8,List((2,27), (20,23), (11,22)))
* (6,List((16,23), (24,21), (22,20)))
* (0,List((2,29), (24,25), (26,24)))
* (2,List((6,24), (21,23), (29,20)))
* (7,List((16,26), (26,25), (1,23)))
* (5,List((14,26), (21,21), (12,21)))
* (9,List((1,31), (28,21), (0,20)))
* (3,List((14,28), (28,27), (22,25)))
* (1,List((3,25), (6,23), (5,22)))
*/
sc.stop()
}
}
Action
前面說到的都是邏輯轉換算子
,接下來說下行動算子
,它是用來觸發
RDD的計算,得到RDD的相關計算結果或者將RDD保存的文件系統中。行動算子一定調用了sc.runJob
,而邏輯算子一般返回一個新的MapPartitionsRDD
1. reduce(func)
- 作用:通過func函數聚集RDD中的所有元素,
先聚合分區內
數據,再聚合分區間
數據。 - 需求:創建一個RDD,將所有元素聚合得到結果。
val rdd1: RDD[Int] = sc.makeRDD(1 to 10 ,2)
val result: Int = rdd1.reduce(_ + _)
println(result) // 55
2. collect()
- 作用:在驅動程序中,以
數組
的形式返回數據集的所有元素。 - 需求:創建一個RDD,並將RDD內容收集到Driver端打印
val rdd1: RDD[Int] = sc.makeRDD(1 to 10 ,2)
println(rdd1) // ParallelCollectionRDD[7] at makeRDD at WordCountLocal.scala:63
println("-"*10)
println(rdd1.collect()) // 返回一個Array [I@322ec9
val ints: Array[Int] = rdd1.collect() //此時數據已經返回到Driver了。
ints.foreach(println) // 此時是在driver中執行的!
val str: String = rdd1.collect().mkString(",")
3. count()
- 作用:返回RDD中元素的個數
- 需求:創建一個RDD,統計該RDD的條數
val rdd1: RDD[Int] = sc.makeRDD(1 to 10 ,2)
val num: Long = rdd1.count()
4. first()
- 作用:返回RDD中的第一個元素
- 需求:創建一個RDD,返回該RDD中的第一個元素
val rdd1: RDD[Int] = sc.makeRDD(1 to 10, 2)
val fir: Int = rdd1.first() // 第一個元素
5. take(num:Int)
- 作用:返回一個由RDD的前n個元素組成的數組
- 需求:創建一個RDD,統計該RDD的條數
val rdd: RDD[Int] = sc.parallelize(Array(2, 5, 4, 6, 8, 3))
val ints: Array[Int] = rdd.take(4) // 獲取前N 個數據,以Array 形式返回。
val str: String = ints.mkString(",")
6. takeOrdered(n:Int)
- 作用:返回該RDD
排序後
的前n個元素組成的數組 - 需求:創建一個RDD,統計該RDD的條數
val rdd: RDD[Int] = sc.parallelize(Array(2, 5, 4, 6, 8, 3))
val ints: Array[Int] = rdd.takeOrdered(4) // 獲取前N 個數據,以Array 形式返回。
val str: String = ints.mkString(",") // 2,3,4,5
println(str)
7. aggregate
- 參數:(zeroValue: U)(seqOp: (U, T) ⇒ U, combOp: (U, U) ⇒ U)
- 作用:aggregate函數將每個分區裏面的元素通過seqOp和初始值進行聚合,然後用combine函數將每個分區的結果和初始值(zeroValue)進行combine操作。這個函數最終返回的類型不需要和RDD中元素類型一致。
分區間合併的時候也要用到zeroValue
- 需求:創建一個RDD,將所有元素相加得到結果
val rdd: RDD[Int] = sc.parallelize(Array(2, 5, 4, 6, 8, 3),3)
val i: Int = rdd.aggregate(0)(math.max(_,_),_ + _) // 5 + 6 + 8
println(i) // 19
---
val rdd1 = sc.makeRDD(1 to 10,2)
rdd1.aggregate(0)(_ + _,_ + _)// 55
rdd1.aggregate(10)(_ + _,_ + _) // 55 + 10 + 10 + 10 ,兩個分區內一個分區見
8. fold(num)(func)
- 作用:摺疊操作,aggregate的
簡化操作
,seqop和combop一樣。原理通上。 - 需求:創建一個RDD,將所有元素相加得到結果
val rdd: RDD[Int] = sc.parallelize(Array(2, 5, 4, 6, 8, 3),3)
val i: Int = rdd.fold(0)(_ + _)
println(i) // 28
9. saveAsTextFile(path)
作用:將數據集的元素以textfile的形式保存到HDFS文件系統或者其他支持的文件系統,對於每個元素,Spark將會調用toString方法,將它裝換爲文件中的文本
10. saveAsSequenceFile(path)
作用:將數據集中的元素以Hadoop sequencefile的格式保存到指定的目錄下,可以使HDFS或者其他Hadoop支持的文件系統。
11. saveAsObjectFile(path)
作用:用於將RDD中的元素序列化成對象,存儲到文件中。
12. countByKey()
- 作用:針對(K,V)類型的RDD,返回一個(K,Int)的map,表示每一個key對應的元素個數。
- 需求:創建一個PairRDD,統計每種key的個數
val rdd = sc.parallelize(List((1,3),(1,2),(1,4),(2,3),(3,6),(3,8)),3)
val intToLong: collection.Map[Int, Long] = rdd.countByKey()
intToLong.foreach(println)
println("-"*10)
// https://blog.csdn.net/weixin_42181200/article/details/80696369
// countByValue把一個KV看成一個整體了
val tupleToLong: collection.Map[(Int, Int), Long] = rdd.countByValue()
tupleToLong.foreach(println)
----------
(3,2)
(1,3)
(2,1)
----------
((3,6),1)
((1,4),1)
((1,3),1)
((2,3),1)
((1,2),1)
((3,8),1)
13. foreach(func)
- 作用:在數據集的每一個元素上,運行函數func進行更新。
- 需求:創建一個RDD,對每個元素進行打印
rdd = sc.makeRDD(1 to 5,2)
rdd.foreach(println) // 這個代碼是在 executor中執行的 跟 collect後再foreach位置不同。
rdd.foreach{
case i =>{println(i)} //跟上面一樣 執行在executor中!
}
RDD中的函數傳遞
在實際開發中我們往往需要自己定義一些對於RDD的操作,那麼此時需要主要的是,初始化工作是在Driver端進行的,而實際運行程序是在Executor端進行的,這就涉及到了跨進程通信,是需要序列化
的。下面我們看幾個例子:
1. 傳遞方法
/**
* @author sowhat
* @create 2020-06-15 17:40
*/
class Search() {
def isMatch(s: String) = s.contains("H")
//過濾出包含字符串的RDD 需考慮序列化
def getMatch1(rdd: RDD[String]): RDD[String] = rdd.filter(isMatch)
//過濾出包含字符串的RDD 這個不需要考慮序列化
def getMatche2(rdd: RDD[String]): RDD[String] = rdd.filter(x => x.contains("H"))
}
object SeriTest {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.OFF)
Logger.getLogger("akka").setLevel(Level.OFF)
//1.創建SparkConf並設置App名稱
val conf: SparkConf = new SparkConf().setAppName("SeriTest").setMaster("local[*]")
//2.創建SparkContext,該對象是提交Spark App的入口
val sc = new SparkContext(conf)
val search = new Search()
val rdd: RDD[String] = sc.parallelize(Array("Hadoop","Spark","Hive","Sowhat"))
val unit: RDD[String] = search.getMatch1(rdd)
unit.collect().foreach(println)
/**
* Exception in thread "main" org.apache.spark.SparkException: Task not serializable
* Caused by: java.io.NotSerializableException: com.sowhat.Search
* */
}
}
原因:
在getMatch1 這個方法中所調用的方法
isMatch()
是定義在Search
這個類中的,實際上調用的是this. isMatch()
,this表示Search這個類的對象
,程序在運行過程中需要將Search對象序列化
以後傳遞到Executor端。
解決辦法:
使類繼承scala.Serializable即可。
class Search()extends Serializable
{…}
2. 傳遞屬性
class Search() {
val query = "H"
def isMatch(s: String) = s.contains(query)
//過濾出包含字符串的RDD 需考慮序列化
def getMatch1(rdd: RDD[String]): RDD[String] = rdd.filter(isMatch)
//過濾出包含字符串的RDD 這個不需要考慮序列化
def getMatche2(rdd: RDD[String]): RDD[String] = rdd.filter(x => x.contains(query))
}
object SeriTest {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.OFF)
Logger.getLogger("akka").setLevel(Level.OFF)
//1.創建SparkConf並設置App名稱
val conf: SparkConf = new SparkConf().setAppName("SeriTest").setMaster("local[*]")
//2.創建SparkContext,該對象是提交Spark App的入口
val sc = new SparkContext(conf)
val search = new Search()
val rdd: RDD[String] = sc.parallelize(Array("Hadoop","Spark","Hive","Sowhat"))
val unit: RDD[String] = search.getMatche2(rdd)
unit.collect().foreach(println)
}
}
此時還是會報錯,說我們任務中沒有序列化,因爲我們傳遞的屬性中的query是 Search的對象,所以我們必須解決這個調用類對象屬性序列化問題,
解決方法一:
class Search() extends Serializable 繼承序列化類實現
解決方法二:
將類變量query賦值給局部變量,修改getMatche2爲
def getMatche2(rdd: RDD[String]): RDD[String] = {
val query_ : String = this.query//將類變量賦值給局部變量
rdd.filter(x => x.contains(query_))
}
RDD依賴關係
RDD只支持粗粒度
轉換,即在大量記錄上執行的單個操作。將創建RDD的一系列Lineage(血統)記錄下來,以便恢復丟失的分區。RDD的Lineage會記錄RDD的元數據信息和轉換行爲,當該RDD的部分分區數據丟失時,它可以根據這些信息來重新運算和恢復丟失的數據分區。比如上圖的ABCD四個任務存在依賴關係然後在不同的executor中,需要記錄其依賴關係來保證系統穩定性。
scala> val listRDD = sc.makeRDD(List(1,2,3,4,4,3,2,5,4))
listRDD: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[1] at makeRDD at <console>:24
scala> val mapRDD = listRDD.map((_,1))
mapRDD: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[2] at map at <console>:26
scala> val resultRDD = mapRDD.reduceByKey( _ + _)
resultRDD: org.apache.spark.rdd.RDD[(Int, Int)] = ShuffledRDD[3] at reduceByKey at <console>:28
scala> resultRDD.toDebugString
res0: String =
(16) ShuffledRDD[3] at reduceByKey at <console>:28 []
+-(16) MapPartitionsRDD[2] at map at <console>:26 []
| ParallelCollectionRDD[1] at makeRDD at <console>:24 []
這裏是有一個任務調度依賴的記錄的
scala> listRDD.toDebugString
res1: String = (16) ParallelCollectionRDD[1] at makeRDD at <console>:24 []
scala> mapRDD.toDebugString
res2: String =
(16) MapPartitionsRDD[2] at map at <console>:26 []
| ParallelCollectionRDD[1] at makeRDD at <console>:24 []
scala> resultRDD.dependencies
res3: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.ShuffleDependency@28488f2e)
scala> listRDD.dependencies
res5: Seq[org.apache.spark.Dependency[_]] = List()
scala> mapRDD.dependencies
res6: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.OneToOneDependency@ce51a54)
注意
:RDD和它依賴的父RDD(s)的關係有兩種不同的類型,即窄依賴
(narrow dependency)和寬依賴
(wide dependency)。
1. 窄依賴
窄依賴指的是每一個父RDD的Partition最多被子RDD的一個Partition使用,窄依賴我們形象的比喻爲獨生子女
2. 寬依賴
寬依賴指的是多個子RDD的Partition會依賴同一個父RDD的Partition,會引起shuffle,你可以稱爲shuffle依賴,總結:寬依賴我們形象的比喻爲超生。
3. DAG
DAG(Directed Acyclic Graph
)叫做有向無環圖
,原始的RDD通過一系列的轉換就就形成了DAG,根據RDD之間的依賴關係的不同將DAG劃分成不同的Stage
,對於窄依賴,partition的轉換處理在Stage中完成計算。對於寬依賴,由於有Shuffle的存在,只能在parent RDD處理完成後,才能開始接下來的計算,因此寬依賴是劃分Stage的依據
。
ps
: 一般是從後往前推斷stage,並且 Stage個數 = 1 + shuffle個數,重點理解下上圖。
4. 任務劃分(面試重點)
RDD任務切分中間分爲:Application
、Job
、Stage
和Task
- Application:初始化一個SparkContext即生成一個Application,也就是一個AppMaster。
- Job:一個
Action
算子就會生成一個Job - Stage:根據RDD之間的依賴關係的不同將Job劃分成不同的Stage,遇到一個
寬依賴
則劃分一個Stage。
- Task:Stage是一個TaskSet,將Stage劃分的結果發送到不同的Executor執行即爲一個Task。TaskSet的個數要看接受方有幾個RDD。
注意
:Application == > Job ==> Stage ==> Task每一層都是1對n的關係。
Term(術語) | Meaning(解釋) |
---|---|
Application(Spark應用程序) | 運行於Spark上的用戶程序,由集羣上的一個driver program(包含SparkContext對象)和多個executor線程組成 |
Application jar(Spark應用程序JAR包) | Jar包中包含了用戶Spark應用程序,如果Jar包要提交到集羣中運行,不需要將其它的Spark依賴包打包進行,在運行時 |
Driver program | 包含main方法的程序,負責創建SparkContext對象 |
Cluster manager | 集羣資源管理器,例如Mesos,Hadoop Yarn |
Deploy mode | 部署模式,用於區別driver program的運行方式:集羣模式(cluter mode),driver在集羣內部啓動;客戶端模式(client mode),driver進程從集羣外部啓動 |
Worker node | 工作節點, 集羣中可以運行Spark應用程序的節點 |
Executor | Worker node上的進程,該進程用於執行具體的Spark應用程序任務,負責任務間的數據維護(數據在內存中或磁盤上)。不同的Spark應用程序有不同的Executor |
Task | 運行於Executor中的任務單元,Spark應用程序最終被劃分爲經過優化後的多個任務的集合 |
Job | 由多個任務構建的並行計算任務,具體爲Spark中的action操作,如collect,save等) |
Stage | 每個job將被拆分爲更小的task集合,這些任務集合被稱爲stage,各stage相互獨立(類似於MapReduce中的map stage和reduce stage),由於它由多個task集合構成,因此也稱爲TaskSet |
5. RDD緩存
RDD通過persist
方法或cache
方法可以將前面的計算結果緩存,默認情況下 persist()
會把數據以序列化
的形式緩存在 JVM 的堆空間中。 但是並不是這兩個方法被調用時立即緩存
,而是觸發後面的action時,該RDD將會被緩存在計算節點的內存中,並供後面重用。
但是並不是這兩個方法被調用時立即緩存,而是觸發後面的action
時,該RDD將會被緩存在計算節點的內存中,並供後面重用。
通過查看源碼發現cache
最終也是調用了persist
方法,不過persist
可以設置存儲的等級跟格式,默認的存儲級別都是僅在內存存儲一份,Spark的存儲級別還有好多種,存儲級別在object StorageLevel中定義的。
無非就是在內存中存儲,在內存中存儲兩份, 在磁盤中存儲,在磁盤中存儲兩份。 特別注意下OFF_HEAP
是在JVM的堆外
申請一個空間來存儲數據。在存儲級別的末尾加上_2
來把持久化數據存爲兩份。
緩存有可能丟失,或者存儲存儲於內存的數據由於內存不足而被刪除,RDD的緩存容錯機制保證了即使緩存丟失也能保證計算的正確執行。通過基於RDD的一系列轉換,丟失的數據會被重算
,由於RDD的各個Partition是相對獨立的,因此只需要計算丟失的部分即可,並不需要重算全部Partition。
6. RDD CheckPoint
Spark中對於數據的保存除了持久化操作之外,還提供了一種檢查點的機制,檢查點(本質是通過將RDD寫入Disk做檢查點)是爲了通過lineage做容錯的輔助,lineage過長會造成容錯成本過高,這樣就不如在中間階段做檢查點容錯,如果之後有節點出現問題而丟失分區,從做檢查點的RDD開始重做Lineage,就會減少開銷。檢查點通過將數據寫入到HDFS文件系統實現了RDD的檢查點功能。
爲當前RDD設置檢查點。該函數將會創建一個二進制的文件,並存儲到checkpoint目錄中,該目錄是用SparkContext.setCheckpointDir()
設置的。在checkpoint的過程中,因爲已經磁盤化所以該RDD的所有依賴於父RDD中的信息將全部被移除。對RDD進行checkpoint操作並不會馬上被執行,必須執行Action操作才能觸發。
sc.setCheckpointDir("output") // 指定生成checkpoint目錄
val rdd = sc.parallelize(Array("sowhat"))
val ch: RDD[String] = rdd.map(_.toString + System.currentTimeMillis())
ch.checkpoint() // 要顯示調用下
ch.collect().foreach(println)
Thread.sleep(1000)
ch.collect().foreach(println) // 從顯示結果來看 好像 在這一次會出發checkpoint
Thread.sleep(1000)
ch.collect().foreach(println)
println("-" * 10)
println(ch.toDebugString)
---
sowhat1592357507062
sowhat1592357508581
sowhat1592357508581
----------
(4) MapPartitionsRDD[8] at map at WordCountLocal.scala:62 []
| ReliableCheckpointRDD[9] at collect at WordCountLocal.scala:64 [] // 依賴到checkpoint 即可,
3. 鍵值對RDD數據分區器
Spark目前支持Hash
分區和Range
分區,用戶也可以自定義分區,Hash分區爲當前的默認分區,Spark中分區器直接決定了RDD中分區的個數、RDD中每條數據經過Shuffle過程屬於哪個分區和Reduce的個數
注意:
- 只有
Key-Value
類型的RDD纔有分區器的,非Key-Value類型的RDD分區器的值是None - 每個RDD的分區ID範圍:0~numPartitions-1,決定這個值是屬於那個分區的。
1. 獲取RDD分區
可以通過使用RDD的partitioner
屬性來獲取 RDD 的分區方式。它會返回一個 scala.Option 對象, 通過get方法獲取其中的值。相關源碼如下:
val pairs: RDD[(Int, Int)] = sc.makeRDD(Array((1, 1), (2, 2), (3, 3)))
val partitioner: Option[Partitioner] = pairs.partitioner
println(partitioner) // None
val pared: RDD[(Int, Int)] = pairs.partitionBy(new org.apache.spark.HashPartitioner(2))
println(pared.partitioner) // Some(org.apache.spark.HashPartitioner@2)
println(pared.partitioner.get) // org.apache.spark.HashPartitioner@2 分區方式獲取
val partitions: Int = pared.getNumPartitions
println(pared.glom().collect().foreach(println)) //
--- 源碼部分如下:
class HashPartitioner(partitions: Int) extends Partitioner {
require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.")
def numPartitions: Int = partitions
def getPartition(key: Any): Int = key match { // 對於每個KV數據要判斷該數據需要存儲在哪個partition中
case null => 0
case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
}
override def equals(other: Any): Boolean = other match {
case h: HashPartitioner =>
h.numPartitions == numPartitions
case _ =>
false
}
override def hashCode: Int = numPartitions
}
def nonNegativeMod(x: Int, mod: Int): Int = {
val rawMod = x % mod
rawMod + (if (rawMod < 0) mod else 0)
}
2. Hash分區
HashPartitioner分區的原理:對於給定的key,計算其hashCode,併除以分區的個數取餘,如果餘數小於0,則用餘數+分區的個數(否則加0),最後返回的值就是這個key所屬的分區ID。
使用Hash分區的實操。
val nopair: RDD[(Int, Int)] = sc.parallelize(List((1, 3), (1, 2), (2, 4), (2, 3), (3, 6), (3, 8)), 8)
nopair.mapPartitionsWithIndex((index, iter) => {
Iterator(index.toString + " : " + iter.mkString("|"))
}).collect.foreach(println)
// 對數據進行分區, 一共8個區域,然後
val hashpair: RDD[(Int, Int)] = nopair.partitionBy(new org.apache.spark.HashPartitioner(7))
println(hashpair.partitions.size) // 數據分區 一共7個,
// 以分區未單位 處理數據,
hashpair.mapPartitions(iter => Iterator(iter.length)).collect().foreach(print) // 0222000,注意看partitions的輸入跟輸出
hashpair.saveAsTextFile("output")
-----
0 :
1 : (1,3)
2 : (1,2)
3 : (2,4)
4 :
5 : (2,3)
6 : (3,6)
7 : (3,8)
7
0222000
3. Ranger分區
HashPartitioner分區弊端:可能導致每個分區中數據量的不均勻
,極端情況下會導致某些分區擁有RDD的全部數據。
RangePartitioner作用:將一定範圍內的數映射到某一個分區內,儘量保證每個分區中數據量的均勻,而且分區與分區之間是有序的,一個分區中的元素肯定都是比另一個分區內的元素小或者大,但是分區內的元素是不能保證順序的。簡單的說就是將一定範圍內的數映射到某一個分區內。實現過程爲:
- 先從整個RDD中抽取出樣本數據,將樣本數據排序,計算出每個分區的最大key值,形成一個Array[KEY]類型的數組變量rangeBounds;
- 判斷key在rangeBounds中所處的範圍,給出該key值在下一個RDD中的分區id下標;該分區器要求RDD中的
KEY
類型必須是可以排序的。
4. 自定義分區
要實現自定義的分區器,你需要繼承 org.apache.spark.Partitioner 類並實現下面三個方法。
- numPartitions: Int:返回創建出來的分區數。
- getPartition(key: Any): Int:返回給定鍵的分區編號(0到numPartitions-1)。
- equals():Java 判斷相等性的標準方法。這個方法的實現非常重要,Spark 需要用這個方法來檢查你的分區器對象是否和其他分區器實例相同,這樣 Spark 纔可以判斷兩個 RDD 的分區方式是否相同。
需求:將相同後綴的數據寫入相同的文件,通過將相同後綴的數據分區到相同的分區並保存輸出來實現。
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
}
}
---
val data: RDD[(Int, Int)] = sc.parallelize(Array((1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6)))
val parRDD: RDD[(Int, Int)] = data.partitionBy(new CustomerPartitioner(2))
val result: RDD[(Int, (Int, Int))] = parRDD.mapPartitionsWithIndex((index, items) => items.map((index, _)))
result.collect.foreach(println)
---
(0,(2,2))
(0,(4,4))
(0,(6,6))
(1,(1,1))
(1,(3,3))
(1,(5,5))
4. 數據讀取與保存
Spark的數據讀取及數據保存可以從兩個維度來作區分:文件格式以及文件系統。文件格式分爲:Text
文件、Json
文件、Csv
文件、Sequence
文件以及Object
文件;
文件系統分爲:本地文件系統
、HDFS
、HBASE
以及數據庫
。
文件類型讀取保存
1. Text文件
- 數據讀取:textFile(String)
scala> val hdfsFile = sc.textFile("hdfs://hadoop102:9000/fruit.txt")
hdfsFile: org.apache.spark.rdd.RDD[String] = hdfs://hadoop102:9000/fruit.txt MapPartitionsRDD[21] at textFile at <console>:24
- 數據保存: saveAsTextFile(String)
scala> hdfsFile.saveAsTextFile("/fruitOut")
2. Json文件
如果JSON文件中每一行就是一個JSON記錄,那麼可以通過將JSON文件當做文本文件來讀取,然後利用相關的JSON庫對每一條數據進行JSON解析。
注意:使用RDD讀取JSON文件處理很複雜,同時SparkSQL集成了很好的處理JSON文件的方式,所以應用中多是採用SparkSQL處理JSON文件。
- 導入解析json所需的包
scala> import scala.util.parsing.json.JSON
- 上傳json文件到HDFS
[atguigu@hadoop102 spark]$ hadoop fs -put ./examples/src/main/resources/people.json /
- 讀取文件
scala> val json = sc.textFile("/people.json")
json: org.apache.spark.rdd.RDD[String] = /people.json MapPartitionsRDD[8] at textFile at <console>:24
- 解析json數據
scala> val result = json.map(JSON.parseFull)
result: org.apache.spark.rdd.RDD[Option[Any]] = MapPartitionsRDD[10] at map at <console>:27
- 打印
scala> result.collect
res11: Array[Option[Any]] = Array(Some(Map(name -> Michael)), Some(Map(name -> Andy, age -> 30.0)), Some(Map(name -> Justin, age -> 19.0)))
3. Sequence文件
SequenceFile文件是Hadoop用來存儲二進制形式的key-value
對而設計的一種平面文件(Flat File)。Spark 有專門用來讀取 SequenceFile 的接口。在 SparkContext 中,可以調用 sequenceFile
[ keyClass, valueClass]。
注意:SequenceFile文件只針對PairRDD
- 創建一個RDD
scala> val rdd = sc.parallelize(Array((1,2),(3,4),(5,6)))
rdd: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[13] at parallelize at <console>:24
- 將RDD保存爲Sequence文件
scala> rdd.saveAsSequenceFile("file:///opt/module/spark/seqFile")
- 查看該文件
[atguigu@hadoop102 seqFile]$ pwd
/opt/module/spark/seqFile
[atguigu@hadoop102 seqFile]$ ll
總用量 8
-rw-r--r-- 1 atguigu atguigu 108 10月 9 10:29 part-00000
-rw-r--r-- 1 atguigu atguigu 124 10月 9 10:29 part-00001
-rw-r--r-- 1 atguigu atguigu 0 10月 9 10:29 _SUCCESS
[atguigu@hadoop102 seqFile]$ cat part-00000
SEQ org.apache.hadoop.io.IntWritable org.apache.hadoop.io.IntWritableط
- 讀取Sequence文件
scala> val seq = sc.sequenceFile[Int,Int]("file:///opt/module/spark/seqFile")
seq: org.apache.spark.rdd.RDD[(Int, Int)] = MapPartitionsRDD[18] at sequenceFile at <console>:24
- 打印讀取後的Sequence文件
scala> seq.collect
res14: Array[(Int, Int)] = Array((1,2), (3,4), (5,6))
4. 對象文件
對象文件是將對象序列化
後保存的文件,採用Java的序列化機制。可以通過objectFile
k,v 函數接收一個路徑,讀取對象文件,返回對應的 RDD,也可以通過調用saveAsObjectFile
() 實現對對象文件的輸出。因爲是序列化所以要指定類型。
- 創建一個RDD
scala> val rdd = sc.parallelize(Array(1,2,3,4))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[19] at parallelize at <console>:24
- 將RDD保存爲Object文件
scala> rdd.saveAsObjectFile("file:///opt/module/spark/objectFile")
- 查看該文件
[atguigu@hadoop102 objectFile]$ pwd
/opt/module/spark/objectFile
[atguigu@hadoop102 objectFile]$ ll
總用量 8
-rw-r--r-- 1 atguigu atguigu 142 10月 9 10:37 part-00000
-rw-r--r-- 1 atguigu atguigu 142 10月 9 10:37 part-00001
-rw-r--r-- 1 atguigu atguigu 0 10月 9 10:37 _SUCCESS
[atguigu@hadoop102 objectFile]$ cat part-00000
SEQ!org.apache.hadoop.io.NullWritable"org.apache.hadoop.io.BytesWritableW@`l
- 讀取Object文件
scala> val objFile = sc.objectFile[Int]("file:///opt/module/spark/objectFile")
objFile: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[31] at objectFile at <console>:24
- 打印讀取後的Sequence文件
scala> objFile.collect
res19: Array[Int] = Array(1, 2, 3, 4)
文件系統類數據讀取與保存
1. HDFS
Spark的整個生態系統與Hadoop是完全兼容
的,所以對於Hadoop所支持的文件類型或者數據庫類型,Spark也同樣支持,另外由於Hadoop的API有新舊兩個版本,所以Spark爲了能夠兼容Hadoop所有的版本,也提供了兩套創建操作接口,對於外部存儲創建操作而言,hadoopRDD和newHadoopRDD是最爲抽象的兩個函數接口,主要包含以下四個參數.
- 輸入格式(InputFormat): 制定數據輸入的類型,如TextInputFormat等,新舊兩個版本所引用的版本分別是
org.apache.hadoop.mapred.InputFormat
和org.apache.hadoop.mapreduce.InputFormat(NewInputFormat)
- 鍵類型: 指定[K,V]鍵值對中K的類型
- 值類型: 指定[K,V]鍵值對中V的類型
- 分區值: 指定由外部存儲生成的RDD的partition數量的最小值,如果沒有指定,系統會使用默認值defaultMinSplits
注意:其他創建操作的API接口都是爲了方便最終的Spark程序開發者而設置的,是這兩個接口的高效實現版本.例如,對於textFile而言,只有path這個指定文件路徑的參數,其他參數在系統內部指定了默認值。
5. 在Hadoop中以壓縮形式存儲的數據,不需要指定解壓方式就能夠進行讀取,因爲Hadoop本身有一個解壓器會根據壓縮文件的後綴推斷解壓算法進行解壓.
6. 如果用Spark從Hadoop中讀取某種類型的數據不知道怎麼讀取的時候,上網查找一個使用map-reduce的時候是怎麼讀取這種這種數據的,然後再將對應的讀取方式改寫成上面的hadoopRDD和newAPIHadoopRDD兩個類就行了。
2. MySQL
pom文件 添加依賴:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.27</version>
</dependency>
package com.sowhat
import java.sql.{Connection, DriverManager, PreparedStatement}
import org.apache.spark.rdd.{JdbcRDD, RDD}
import org.apache.spark.{SparkConf, SparkContext}
/**
* @author sowhat
* @create 2020-06-17 13:50
* spark 跟MySQL之間的 讀寫數據
*
*/
object MysqlRDD {
def main(args: Array[String]): Unit = {
//1.創建spark配置信息
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("JdbcRDD")
//2.創建SparkContext
val sc = new SparkContext(sparkConf)
//3.定義連接mysql的參數
val driver = "com.mysql.jdbc.Driver"
val url = "jdbc:mysql://hadoop102:3306/rdd"
val userName = "root"
val passWd = "000000"
//創建JdbcRDD
val rdd = new JdbcRDD(sc, () => {
Class.forName(driver)
DriverManager.getConnection(url, userName, passWd)
},
"select * from rddtable where id > ? and id < ?", // mysql 語句
1, // 從mysql中讀取數據的時候一般劃區域的讀取數據,然後需要獲得數據的起始點位置跟終止點位置
10,
1, // 讀取數據後分區數指定,
r => (r.getInt(1), r.getString(2))
)
//打印最後結果
println(rdd.count())
rdd.foreach(println)
// 數據庫連接比較重要,給每一個分區分配一個連接 而不是給每個數據分配一個連接。
val dataRDD: RDD[(String, Int)] = sc.makeRDD(List(("zhangsan", 20), ("lisi", 30), ("wangsu", 40)))
dataRDD.foreachPartition(data => {
Class.forName(driver)
val connection: Connection = java.sql.DriverManager.getConnection(url, userName, passWd)
data.foreach { case (username, age) => {
val sql = "insert into user (name,age) values (?,?)"
val statement: PreparedStatement = connection.prepareStatement(sql)
statement.setString(1, username)
statement.setInt(2, age)
statement.executeUpdate()
statement.close()
}
}
}
)
sc.stop()
}
}
3.HBase
由於org.apache.hadoop.hbase.mapreduce.TableInputFormat 類的實現,Spark 可以通過Hadoop輸入格式訪問HBase。這個輸入格式會返回鍵值對數據,
其中鍵的類型爲org.apache.hadoop.hbase.io.ImmutableBytesWritable,而值的類型爲org.apache.hadoop.hbase.client.Result。
Result。
- 添加依賴
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-server</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>1.3.1</version>
</dependency>
- 從HBase讀取數據
package com.sowhat
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.{HBaseConfiguration, HColumnDescriptor, HTableDescriptor, TableName}
import org.apache.hadoop.hbase.client.{HBaseAdmin, Put, Result}
import org.apache.hadoop.hbase.io.ImmutableBytesWritable
import org.apache.hadoop.hbase.mapreduce.{TableInputFormat, TableOutputFormat}
import org.apache.hadoop.hbase.util.Bytes
import org.apache.hadoop.mapred.JobConf
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* @author sowhat
* @create 2020-06-17 14:14
*/
object HBaseSpark {
def main(args: Array[String]): Unit = {
//創建spark配置信息
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("JdbcRDD")
//創建SparkContext
val sc = new SparkContext(sparkConf)
//構建HBase配置信息
val conf: Configuration = HBaseConfiguration.create()
conf.set("hbase.zookeeper.quorum", "hadoop102,hadoop103,hadoop104")
conf.set(TableInputFormat.INPUT_TABLE, "rddtable")
//從HBase讀取數據形成RDD
val hbaseRDD: RDD[(ImmutableBytesWritable, Result)] = sc.newAPIHadoopRDD(
conf,
classOf[TableInputFormat],
classOf[ImmutableBytesWritable],
classOf[Result])
val count: Long = hbaseRDD.count()
println(count)
//對hbaseRDD進行處理
hbaseRDD.foreach {
case (_, result) =>
val key: String = Bytes.toString(result.getRow)
val name: String = Bytes.toString(result.getValue(Bytes.toBytes("info"), Bytes.toBytes("name")))
val color: String = Bytes.toString(result.getValue(Bytes.toBytes("info"), Bytes.toBytes("color")))
println("RowKey:" + key + ",Name:" + name + ",Color:" + color)
}
//關閉連接
sc.stop()
}
def main1(args: Array[String]) {
//獲取Spark配置信息並創建與spark的連接
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("HBaseApp")
val sc = new SparkContext(sparkConf)
//創建HBaseConf
val conf = HBaseConfiguration.create()
val jobConf = new JobConf(conf)
jobConf.setOutputFormat(classOf[TableOutputFormat])
jobConf.set(TableOutputFormat.OUTPUT_TABLE, "fruit_spark")
//構建Hbase表描述器
val fruitTable = TableName.valueOf("fruit_spark")
val tableDescr = new HTableDescriptor(fruitTable)
tableDescr.addFamily(new HColumnDescriptor("info".getBytes))
//創建Hbase表
val admin = new HBaseAdmin(conf)
if (admin.tableExists(fruitTable)) {
admin.disableTable(fruitTable)
admin.deleteTable(fruitTable)
}
admin.createTable(tableDescr)
//定義往Hbase插入數據的方法
def convert(triple: (Int, String, Int)) = {
val put = new Put(Bytes.toBytes(triple._1))
put.addImmutable(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes(triple._2))
put.addImmutable(Bytes.toBytes("info"), Bytes.toBytes("price"), Bytes.toBytes(triple._3))
(new ImmutableBytesWritable, put)
}
//創建一個RDD
val initialRDD = sc.parallelize(List((1, "apple", 11), (2, "banana", 12), (3, "pear", 13)))
//將RDD內容寫到HBase
val localData = initialRDD.map(convert)
localData.saveAsHadoopDataset(jobConf)
}
}
Spark 累加器
Spark 三大數據結構
RDD
:彈性分佈式數據集
廣播變量
: 分佈式只讀共享變量
累加器
: 分佈式只寫共享變量
RDD的情況下 若干executor從Driver獲得的都是數據副本,各自操作不受干擾。
累加器用來對信息進行聚合
,通常在向 Spark傳遞函數時,比如使用 map() 函數或者用 filter() 傳條件時,可以使用驅動器程序中定義的變量,但是集羣中運行的每個任務都會得到這些變量的一份新的副本,更新這些副本的值也不會影響驅動器中的對應變量。如果我們想實現所有分片處理時更新共享變量的功能,那麼累加器可以實現我們想要的效果。
val dataRDD:RDD[Int] = sc.makeRDD(List(1,2,3,4),2)
val result:Int = dataRDD.reduce(_ + _)
var sum = 0
dataRDD.foreach(i=> sum = sum + i)
println(sum) // 0
需要Spark把數據結構返回到driver中。
val dataRDD:RDD[Int] = sc.makeRDD(List(1,2,3,4),2)
val accumulator:LongAccumulator = sc.LongAccumulator
dataRDD.foreach{
case i => {
accumulator.add(i)
}
}
println("sum = " + accumulator.value) // 10
- 工作節點上的任務不能訪問累加器的值。從這些任務的角度來看,累加器是一個
只寫變量
。 - 對於要在行動操作中使用的累加器,Spark只會把每個任務對各累加器的修改應用一次。因此,如果想要一個無論在失敗還是重複計算時都絕對可靠的累加器,我們必須把它放在 foreach() 這樣的行動操作中。轉化操作中累加器可能會發生不止一次更新。
自定義累加器
自定義累加器類型的功能在1.X版本中就已經提供了,但是使用起來比較麻煩,在2.0版本後,累加器的易用性有了較大的改進,而且官方還提供了一個新的抽象類:AccumulatorV2來提供更加友好的自定義類型累加器的實現方式。實現自定義類型累加器需要繼承AccumulatorV2並覆寫要求的方法。
需求:實現一個自定義的數值累加器
package com.sowhat
import org.apache.spark.util.AccumulatorV2
class MyAccu extends AccumulatorV2[Int, Int] {
var sum = 0
//判斷是否爲空
override def isZero: Boolean = sum == 0
//複製
override def copy(): AccumulatorV2[Int, Int] = {
val accu = new MyAccu
accu.sum = this.sum
accu
}
//重置
override def reset(): Unit = sum = 0
//累加
override def add(v: Int): Unit = sum += v
// 合併
override def merge(other: AccumulatorV2[Int, Int]): Unit = sum += other.value
//返回值
override def value: Int = sum
}
main
package com.sowhat
import org.apache.spark.rdd.RDD
import org.apache.spark.{Accumulator, SparkConf, SparkContext}
object AccuTest {
def main(args: Array[String]): Unit = {
//創建SparkConf
val sparkConf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("AccuTest")
//創建SC
val sc = new SparkContext(sparkConf)
//創建自定義累加器對象
val accu = new MyAccu
//註冊累加器
sc.register(accu)
//創建RDD
val value: RDD[Int] = sc.parallelize(Array(1, 2, 3, 4))
//在行動算子中對累加器的值進行修改
value.foreach { x =>
accu.add(1)
println(x)
}
//打印累加器的值
println(accu.value)
//關閉SparkContext
sc.stop()
}
}
Spark 廣播變量(調優策略)
如果一個任務有兩個Executor,每個Executor有3個Task,則Driver需要發送6
次重要數據到Task,用了廣播變量
可以直接將數據傳送到Executor中,並且是隻讀的。
廣播變量用來高效分發較大
的對象。向所有工作節點發送一個較大的只讀值,以供一個或多個Spark操作使用。比如,如果你的應用需要向所有節點發送一個較大的只讀查詢表,甚至是機器學習算法中的一個很大的特徵向量,廣播變量用起來都很順手。 在多個並行操作中使用同一個變量,但是 Spark會爲每個任務分別發送。
使用廣播變量的過程如下:
- 通過對一個類型T的對象調用SparkContext.broadcast創建出一個Broadcast[T]對象,任何
可序列化
的類型都可以這麼實現。 - 通過
value
屬性訪問該對象的值(在Java中爲value()方法)。 - 變量只會被髮到各個節點一次,應作爲只讀值處理(修改這個值不會影響到別的節點)。
val rdd1 = sc.makeRDD(List((1,"a"),(2,"b"),(3,"c")))
val list = List((1,1),(2,2),(3,3))
val joinRDD:RDD[(Int,(String,Int))] = rdd1.join(list)
// 會涉及到笛卡爾乘積的過程,耗時大,可以參考下面的 廣播變量的操作
val broadcast:Broadcast[List[(Int,Int)]] = sc.broadcast(list)
val resultRDD:RDD[(Int,(String,Any))] = rdd1.map {
case(key,value) =>{
var v2:Any = null
for(t <- broadcast.value){
if(key == t._1){
v2 = t._2
}
}
(key,(value,v2))
}
}
resultRDD.foreach(println)