前言
上一篇權當吹水了,從這篇開始進入正題。
二、Spark 的記憶體計算框架(重點?)
RDD(Resilient Distributed Dataset)叫做 彈性分散式資料集 ,是Spark中最基本的資料抽象,它代表一個不可變、可分割槽、裡面的元素可平行計算的集合.
Dataset:就是一個集合,儲存很多資料.
Distributed:它內部的元素進行了分散式儲存,方便於後期進行分散式計算.
Resilient:表示彈性,rdd的資料是可以儲存在記憶體或者是磁碟中.
複製程式碼
在程式碼中的表現是這樣的,每一個方法所對應的結果都是一個RDD,比如上面的scala程式碼,下一個RDD的結果會依賴於上一個RDD
我知道到目前大家都沒整懂,沒事,繼續往下看就會懂的?
2.1 RDD 的五大特性
接下來是在原始碼中對於RDD的解釋,分點中我使用了我覺得(接受反駁?)較為合理的語句來解釋
2.1.1 A list of partitions
一個分割槽(Partition)列表,組成了該RDD的資料。
這裡表示一個rdd有很多分割槽,每一個分割槽內部是包含了該rdd的部分資料,spark中任務是以task執行緒的方式執行, 一個分割槽就對應一個task執行緒。
使用者可以在建立RDD時指定RDD的分割槽個數,如果沒有指定,那麼就會採用預設值。(比如:讀取HDFS上資料檔案產生的RDD分割槽數跟block的個數相等)
RDD的分割槽其實可以簡單這樣理解,比如說我現在要來一個wordCount,這個文字的大小是300M,那按照我們 HDFS 的套路,每128M是一個block塊,那這個300M的檔案就是3個block,然後我們的RDD會按照你這個檔案的擁有的block塊數來決定RDD的分割槽數,此時RDD的分割槽數就是3,但是如果我這個檔案本身就小於128M呢,那RDD就會預設為2個分割槽數
2.1.2 A function for computing each split
每個分割槽的計算函式都算是一個RDD --- Spark中RDD的計算是以分割槽為單位的,每個RDD都會實現compute函式以達到這個目的.
2.1.3 A list of dependencies on other RDDs
一個rdd會依賴於其他多個rdd --- rdd與rdd之間的依賴關係,spark任務的容錯機制就是根據這個特性而來。
2.1.4 Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
針對key-value型別的RDD才有分割槽函式,分割槽函式其實就是把計算的結果丟到不同的分割槽中。
當前Spark中實現了兩種型別的分割槽函式,一個是基於雜湊的 HashPartitioner,另外一個是基於範圍的 RangePartitioner。
只有對於key-value的RDD,並且產生shuffle,才會有 Partitioner,非key-value的RDD的 Parititioner 的值是None。
HashPartitioner的套路在之前的 MapReduce 的那篇已經有提到過了,其實大資料的那些分割槽套路好多都是這個套路,RangePartitioner就是類似規定了多少到多少丟這個分割槽,多少到多少又丟那個分割槽這樣的玩法。
2.1.5 Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)
計算任務的位置優先為儲存每個Partition的位置 (可自定義)
這裡涉及到資料的本地性,資料塊位置最優。簡單點說也就是說哪裡有資料我們就在哪裡做計算的意思。
spark任務在排程的時候會優先(注意只是優先,而不是必然)考慮存有資料的節點開啟計算任務,減少資料的網路傳輸,提升計算效率。
2.2 基於wordCount分析RDD的5大屬性
需求就是HDFS上有一個大小為300M的檔案,通過 Spark 實現檔案單詞統計,最後把結果資料儲存到 HDFS 上,程式碼如下,注意我們使用的是 Scala 而不是 Java 程式碼
sc.textFile("/words.txt") // 讀取資料檔案
.flatMap(_.split(" ")) // 切分每一行,獲取所有單詞
.map((_,1)) // 每個單詞計為1
.reduceByKey(_+_) // 相同單詞(key)出現的1累加
.saveAsTextFile("/out") // 儲存輸出到/out
複製程式碼
因為我本地沒有部署好環境,所以我以下過程不會截圖,但是會把操作步驟和結果說明一下
2.2.1 RDD1 : sc.textFile("/words.txt")
把 Spark-shell跑起來,然後我們可以一步一步地把程式碼執行一下,此時我們可以看到
此時我們就可以執行scala的程式碼了,第一步先把sc.textFile("/words.txt")給打上去
因為RDD是一個抽象類,所以sc.textFile("/words.txt")的結果由它的子類 MapPartitionsRDD 進行了接收,這個程式碼會得出RDD的分割槽結果,我們也可以通過
sc.textFile("/words.txt").partitions
複製程式碼
檢視分割槽,此時會得出一個陣列,這個陣列的長度為3(300M的檔案會有3個block塊,而RDD的分割槽數是由block塊來決定的,注意RDD的分割槽數不一定都在不同的伺服器上。但是如果block塊只有1,RDD的分割槽數就會預設2)
sc.textFile("/words.txt").partitions.length
複製程式碼
執行這個檢視陣列的長度,必然為3
此時RDD2,RDD3的結果和RDD1的結果都是由 MapPartitionsRDD 進行接收,不同的是map得出來的RDD會是key-value型別而已
2.2.2 RDD4 : sc.textFile("/words.txt").flatMap(.split(" ")).map((,1)).reduceByKey(+)
RDD4的接收型別為 ShuffleRDD,因為此時的結果需要按照key來分組,就必定會產生 shuffle,那shuffle可以先簡單這麼理解,比如我們現在的wordCount中words.txt只有3個key分別是"zookeeper","kafka","spark",那我shuffle時我就做出這樣的規定
此時我們 saveAsTextFile 會得到3個檔案,不難發現其實 RDD 有幾個分割槽就會有幾個檔案了。每個RDD所對應體現的5大特性也已經寫在了圖右側。
2.3 RDD 的建立方式
2.3.1 通過已經存在的scala集合去構建
val rdd1=sc.parallelize(List(1,2,3,4,5))
val rdd2=sc.parallelize(Array("zookeeper","kafka","spark"))
val rdd3=sc.makeRDD(List(1,2,3,4))
複製程式碼
2.3.2 載入外部的資料來源去構建
val rdd1=sc.textFile("/words.txt")
複製程式碼
2.3.3 從已經存在的rdd進行轉換生成一個新的rdd
val rdd2=rdd1.flatMap(_.split(" "))
val rdd3=rdd2.map((_,1))
複製程式碼
2.4 RDD 的運算元分類
2.4.1 transformation(轉換)
根據已經存在的rdd轉換生成一個新的rdd, 它是延遲載入,它不會立即執行
就例如剛剛wordCount中用到的 map,flatMap,reduceByKey
2.4.2 action (動作)
它會真正觸發任務的執行。將rdd的計算的結果資料返回給Driver端,或者是儲存結果資料到外部儲存介質中
就例如剛剛wordCount中用到的 collect,saveAsTextFile 等
2.5 RDD 的常見運算元(後面結合程式碼說明)
2.5.1 transformation運算元
轉換 | 含義 |
---|---|
map(func) | 返回一個新的RDD,該RDD由每一個輸入元素經過func函式轉換後組成 |
filter(func) | 返回一個新的RDD,該RDD由經過func函式計算後返回值為true的輸入元素組成 |
flatMap(func) | 類似於map,但是每一個輸入元素可以被對映為0或多個輸出元素(所以func應該返回一個序列,而不是單一元素) |
mapPartitions(func) | 類似於map,但獨立地在RDD的每一個分片上執行,因此在型別為T的RDD上執行時,func的函式型別必須是Iterator[T] => Iterator[U] |
mapPartitionsWithIndex(func) | 類似於mapPartitions,但func帶有一個整數參數列示分片的索引值,因此在型別為T的RDD上執行時,func的函式型別必須是(Int, Interator[T]) => Iterator[U] |
union(otherDataset) | 對源RDD和引數RDD求並集後返回一個新的RDD |
intersection(otherDataset) | 對源RDD和引數RDD求交集後返回一個新的RDD |
distinct([numTasks])) | 對源RDD進行去重後返回一個新的RDD |
groupByKey([numTasks]) | 在一個(K,V)的RDD上呼叫,返回一個(K, Iterator[V])的RDD |
reduceByKey(func, [numTasks]) | 在一個(K,V)的RDD上呼叫,返回一個(K,V)的RDD,使用指定的reduce函式,將相同key的值聚合到一起,與groupByKey類似,reduce任務的個數可以通過第二個可選的引數來設定 |
sortByKey([ascending], [numTasks]) | 在一個(K,V)的RDD上呼叫,K必須實現Ordered介面,返回一個按照key進行排序的(K,V)的RDD |
sortBy(func,[ascending], [numTasks]) | 與sortByKey類似,但是更靈活 |
join(otherDataset, [numTasks]) | 在型別為(K,V)和(K,W)的RDD上呼叫,返回一個相同key對應的所有元素對在一起的(K,(V,W))的RDD |
cogroup(otherDataset, [numTasks]) | 在型別為(K,V)和(K,W)的RDD上呼叫,返回一個(K,(Iterable,Iterable))型別的RDD |
coalesce(numPartitions) | 減少 RDD 的分割槽數到指定值。 |
repartition(numPartitions) | 重新給 RDD 分割槽 |
repartitionAndSortWithinPartitions(partitioner) | 重新給 RDD 分割槽,並且每個分割槽內以記錄的 key 排序 |
2.5.2 action運算元
動作 | 含義 |
---|---|
reduce(func) | reduce將RDD中元素前兩個傳給輸入函式,產生一個新的return值,新產生的return值與RDD中下一個元素(第三個元素)組成兩個元素,再被傳給輸入函式,直到最後只有一個值為止。 |
collect() | 在驅動程式中,以陣列的形式返回資料集的所有元素 |
count() | 返回RDD的元素個數 |
first() | 返回RDD的第一個元素(類似於take(1)) |
take(n) | 返回一個由資料集的前n個元素組成的陣列 |
takeOrdered(n, [ordering]) | 返回自然順序或者自定義順序的前 n 個元素 |
saveAsTextFile(path) | 將資料集的元素以textfile的形式儲存到HDFS檔案系統或者其他支援的檔案系統,對於每個元素,Spark將會呼叫toString方法,將它裝換為檔案中的文字 |
saveAsSequenceFile(path) | 將資料集中的元素以Hadoop sequencefile的格式儲存到指定的目錄下,可以使HDFS或者其他Hadoop支援的檔案系統。 |
saveAsObjectFile(path) | 將資料集的元素,以 Java 序列化的方式儲存到指定的目錄下 |
countByKey() | 針對(K,V)型別的RDD,返回一個(K,Int)的map,表示每一個key對應的元素個數。 |
foreach(func) | 在資料集的每一個元素上,執行函式func |
foreachPartition(func) | 在資料集的每一個分割槽上,執行函式func |
2.6 常見運算元的程式碼說明
好吧我忘了我本地沒環境了···
這裡只提醒一下,執行了transformation運算元操作其實並沒有真正意義上的得出結果,要跑collect()才真正的開始計算
2.6.1 注意:repartition 和 coalesce
val rdd1 = sc.parallelize(1 to 10,3)
//列印rdd1的分割槽數
rdd1.partitions.size
//利用repartition改變rdd1分割槽數
//減少分割槽
rdd1.repartition(2).partitions.size
//增加分割槽
rdd1.repartition(4).partitions.size
//利用coalesce改變rdd1分割槽數
//減少分割槽
rdd1.coalesce(2).partitions.size
複製程式碼
repartition : 重新分割槽,有shuffle,可以用來處理小檔案的問題
coalesce : 合併分割槽 / 減少分割槽,預設不shuffle
預設 coalesce 不能擴大分割槽數量。除非新增true的引數,或者使用repartition。
適用場景:
- 如果要shuffle,都用 repartition
- 不需要shuffle,僅僅是做分割槽的合併,coalesce
- repartition常用於擴大分割槽。
2.6.2 注意:map、mapPartitions 和 mapPartitionsWithIndex
map:用於遍歷RDD,將函式f應用於每一個元素,返回新的RDD(transformation運算元)。
mapPartitions:用於遍歷操作RDD中的每一個分割槽,返回生成一個新的RDD(transformation運算元)。
總結:
如果在對映的過程中需要頻繁建立額外的物件,使用mapPartitions要比map高效
比如,將RDD中的所有資料通過JDBC連線寫入資料庫,如果使用map函式,可能要為每一個元素都建立一個connection,這樣開銷很大,如果使用mapPartitions,那麼只需要針對每一個分割槽建立一個connection。
2.6.3 注意:foreach、foreachPartition
foreach:用於遍歷RDD,將函式f應用於每一個元素,無返回值(action運算元)。
foreachPartition: 用於遍歷操作RDD中的每一個分割槽。無返回值(action運算元)。
總結:
一般使用mapPartitions或者foreachPartition運算元比map和foreach更加高效,推薦使用。
所以我們可以使用 foreachPartition 運算元實現
2.7 RDD的依賴關係
RDD和它依賴的父RDD的關係有兩種不同的型別:窄依賴(narrow dependency)和寬依賴(wide dependency)
2.7.1 窄依賴
窄依賴指的是每一個父RDD的Partition最多被子RDD的一個Partition使用,比如map/flatMap/filter/union等等,且所有的窄依賴不會產生shuffle
2.7.2 寬依賴
寬依賴指的是多個子RDD的Partition會依賴同一個父RDD的Partition,比如reduceByKey/sortByKey/groupBy/groupByKey/join等等
。所有的寬依賴會產生shuffle
上圖也可以看出join操作分為寬依賴和窄依賴,如果RDD有相同的partitioner,那麼將不會引起shuffle,這種join是窄依賴,反之就是寬依賴
2.8 lineage
lineage翻譯過來是血統的意思。還是之前的那張圖。
RDD的 Lineage 會記錄RDD的後設資料資訊和轉換行為,lineage儲存了RDD的依賴關係,當該RDD的部分分割槽資料丟失時,它可以根據這些資訊來重新運算和恢復丟失的資料分割槽。
不過需要注意RDD只支援 粗粒度轉換 (即只記錄單個塊上執行的單個操作),比如此時如果我是RDD4的0號莫得了,我就要重新拿到RDD3的所有資料,然後重新reducebyKey一次,這樣才能恢復結果,這種寬依賴需要經過shuffle的操作,恢復起來的成本就會高很多
值得再提的是:我們其實不需要人為干預分割槽資料的恢復,程式自身就可以幫我們根據 RDD 的血統關係自行恢復
2.9 RDD 的快取機制
可以把一個rdd的資料快取起來,後續有其他的job需要用到該rdd的結果資料,可以直接從快取中獲取得到,避免了重複計算。快取是加快後續對該資料的訪問操作。
比如上圖中我只要對RDD2的結果做一個快取,到時候假如用的上,恢復起來就非常方便
2.9.1 如何對 RDD 設定快取
RDD通過 persist 方法或 cache 方法可以將前面的計算結果快取。但是需要注意並不是這兩個方法被呼叫時立即快取,而是觸發後面的action時,該RDD將會被快取在計算節點的記憶體中,並供後面重用。
我們可以看看去RDD的原始碼彙總找出來look look,這倆方法是放在了一起的
通過檢視原始碼發現cache最終也是呼叫了persist方法,預設的快取級別就是 MEMORY_ONLY 都是僅在記憶體儲存一份,Spark的儲存級別還有好多種,儲存級別在object StorageLevel中定義的
StorageLevel 在Scala中是 object (注意這裡我用的是小寫,object 是單例物件的意思,而非Java中的Object),這裡面還有大量不同的儲存級別,這裡就不展開了,這些英文其實也不難懂
cache和persist區別是:cache: 預設是把資料快取在記憶體中,其本質就是呼叫persist方法,而persist:可以把資料快取在記憶體或者是磁碟,有豐富的快取級別,這些快取級別都被定義在StorageLevel這個object中。
2.9.2 快取的使用時機
當第一次使用RDD2做相應的運算元操作得到RDD3的時候,就會從RDD1開始計算,先讀取HDFS上的檔案,然後對RDD1 做對應的運算元操作得到RDD2,再由RDD2計算之後得到RDD3。同樣為了計算得到RDD4,前面的邏輯會被重新計算。
預設情況下多次對一個RDD執行運算元操作, RDD都會對這個RDD及之前的父RDD全部重新計算一次。 這種情況在實際開發程式碼的時候會經常遇到,但是我們一定要避免一個RDD重複計算多次,否則會導致效能急劇降低。
為了獲取得到一個RDD的結果資料,經過了大量的運算元操作或者是計算邏輯比較複雜,也就是某個RDD的資料來之不易的時候,就可以設定快取
總結:可以把多次使用到的RDD,也就是公共RDD進行持久化,避免後續需要,再次重新計算,提升效率。
2.9.3 清除快取資料
自動清除 : 一個application應用程式結束之後,對應的快取資料也就自動清除
手動清除 : 呼叫 RDD 的 unpersist 方法
雖然我們可以對 RDD 的資料進行快取,儲存在記憶體或者是磁碟中,之後就可以直接從記憶體或者磁碟中獲取得到,但是注意這個方式不是特別安全。
cache 它是直接把資料儲存在記憶體中,後續操作起來速度比較快,直接從記憶體中獲取得到。但這種方式很不安全,由於伺服器掛掉或者是程式終止,會導致資料的丟失。
persist 它可以把資料儲存在本地磁碟中,後續可以從磁碟中獲取得到該資料,但它也不是特別安全,由於系統管理員一些誤操作刪除了,或者是磁碟損壞,也有可能導致資料的丟失。
那麼我們有沒有一種更為安全的方式呢?
2.10 RDD 的 checkpoint 機制
checkpoint 提供了一種相對而言更加可靠的資料持久化方式。它是把資料儲存在分散式檔案系統,比如HDFS上。這裡就是利用了HDFS高可用性,高容錯性(多副本)來最大程度保證資料的安全性。
2.10.1 如何設定checkpoint
1.在 HDFS 上設定一個 checkpoint 目錄
sc.setCheckpointDir("hdfs://node1:9000/checkpoint")
複製程式碼
2.對需要做 checkpoint 操作的rdd呼叫 checkpoint 方法
val rdd1=sc.textFile("/words.txt")
rdd1.checkpoint
val rdd2=rdd1.flatMap(_.split(" "))
複製程式碼
3.最後需要有一個 action 操作去觸發任務的執行(checkpoint操作要執行需要有一個action操作,一個action操作對應後續的一個job。該job執行完成之後,它會再次單獨開啟另外一個job來執行 rdd1.checkpoint操作。)
對checkpoint在使用的時候進行優化,在呼叫checkpoint操作之前,可以先來做一個cache操作,快取對應rdd的結果資料,後續就可以直接從cache中獲取到rdd的資料寫入到指定checkpoint目錄中
rdd2.collect
複製程式碼
所以我們總結一下 cache、persist、checkpoint 三者區別
cache和persist
cache預設資料快取在記憶體中
persist可以把資料儲存在記憶體或者磁碟中
後續要觸發 cache 和 persist 持久化操作,需要有一個action操作
它不會開啟其他新的任務,一個action操作就對應一個job
它不會改變RDD的依賴關係,程式執行完成後對應的快取資料就自動消失
複製程式碼
checkpoint
可以把資料持久化寫入到 HDFS 上
後續要觸發checkpoint持久化操作,需要有一個action操作,後續會開啟新的job執行checkpoint操作
它會改變RDD的依賴關係,後續資料丟失了不能夠在通過血統進行資料的恢復。
(因為它判斷你已經持久化到 HDFS 中所以把依賴關係刪除了)
程式執行完成後對應的checkpoint資料就不會消失
複製程式碼
2.11 DAG 有向無環圖生成
DAG(Directed Acyclic Graph) 叫做有向無環圖(有方向,無閉環,代表著資料的流向),原始的RDD通過一系列的轉換就形成了DAG。
這個就是我們的 wordCount 例子裡生成的 DAG,這個圖我們可以在上一講中提到的web介面中的 Running Application 的 spark-shell ---> Completed Jobs 的 Description 中檢視
2.11.1 DAG劃分stage
stage是什麼: 一個Job會被拆分為多組Task,每組任務被稱為一個stage,stage表示不同的排程階段,一個spark job會對應產生很多個stage
stage型別一共有2種:
- ShuffleMapStage: 最後一個shuffle之前的所有變換叫ShuffleMapStage,它對應的task是shuffleMapTask
- ResultStage: 最後一個shuffle之後的操作叫ResultStage,它是最後一個Stage.它對應的task是ResultTask
2.11.2 為啥我們要劃分stage
根據RDD之間依賴關係的不同將DAG劃分成不同的Stage(排程階段)
對於窄依賴,partition的轉換處理在一個Stage中完成計算
對於寬依賴,由於有Shuffle的存在,只能在parent RDD處理完成後,才能開始接下來的計算,
由於劃分完stage之後,在同一個stage中只有窄依賴,沒有寬依賴,可以實現流水線計算,
stage中的每一個分割槽對應一個task,在同一個stage中就有很多可以並行執行的task。
2.11.3 如何劃分stage
我們劃分stage的依據就是寬依賴
首先根據rdd的運算元操作順序生成DAG有向無環圖,接下里從最後一個 RDD 往前推,建立一個新的stage,把該rdd加入到該stage中,它是最後一個stage。
在往前推的過程中執行遇到了窄依賴就把該 RDD 加入到本stage中,如果遇到了寬依賴,就從寬依賴的位置切開,那麼最後一個stage也就被劃分出來了。
重新建立一個新的stage,按照第二個步驟繼續往前推,一直到最開始的 RDD,整個劃分stage也就劃分結束了
2.11.4 stage與stage之間的關係
劃分完stage之後,每一個stage中有很多可以並行執行的task,後期把每一個stage中的task封裝在一個taskSet集合中,最後把一個一個的taskSet集合提交到worker節點上的executor程式中執行。
RDD 與 RDD 之間存在依賴關係,stage與stage之前也存在依賴關係,前面stage中的task先執行,執行完成了再執行後面stage中的task,也就是說後面stage中的task輸入資料是前面stage中task的輸出結果資料。
finally
以上我們就是把 RDD 的一些基礎知識點給簡單地過了一遍,還有一些更加深入,更加細緻的方面我們要放到 Spark Core,Spark Streaming中去闡述,感興趣的朋友可以繼續地關注一下