從0開始學習spark(4)Spark Rdd常用算子和RDD必備知識!!!

在這裏插入圖片描述

每日福利來一個,話不多說,我們直接開始今天的spark的學習,之前我們學習了Spark的基礎原理,和概念,然後學習的spark的第一個程序wordcount,並學習了spark的作業提交運行方法和模式,下面我們開始學習spark RDD ,並熟悉使用RDD裏面的常用的算子。
沒有看前面的同學可以回顧一下:

3.spark core 核心知識
2.spark 之 wordcount入門
1.spark 入門講解

1. RDD

1.1 什麼是RDD

RDD(Resilient Distributed Dataset)叫做分佈式數據集,是 Spark 中最基本的數據抽象,它 代表一個不可變、可分區、裏面的元素可並行計算的集合。RDD 具有數據流模型的特點: 自動容錯、位置感知性調度和可伸縮性。RDD 允許用戶在執行多個查詢時顯式地將工作集緩 存在內存中,後續的查詢能夠重用工作集,這極大地提升了查詢速度。

我們可以從三個方面來理解這個RDD:

**1.數據集 DataSet:**顧名思義,RDD是數據集合的抽象表現,是複雜物理介質質上存在數據的一 種邏輯視圖。從外部來看,RDD 的確可以被看待成經過封裝,帶擴展特性(如容錯性)的數據集合。

2、分佈式 Distributed:RDD 的數據可能在物理上存儲在多個節點的磁盤或內存中,也就是 所謂的多級存儲。

3、彈性 Resilient:雖然 RDD 內部存儲的數據是隻讀的,但是,我們可以去修改(例如通 過 repartition 轉換操作)並行計算計算單元的劃分結構,也就是分區的數量。

我們可以將 RDD 理解爲一個大的集合,將所有數據都加載到內存中,方便進行多次重用。第一, 它是分佈式的,可以分佈在多臺機器上,進行計算。第二,它是彈性的,我認爲它的彈性體 現在每個 RDD 都可以保存內存中,如果某個階段的 RDD 丟失,不需要從頭計算,只需要提 取上一個 RDD,再做相應的計算就可以了

1.2 RDD 的屬性

在這裏插入圖片描述
1、A list of partitions:一組分片(Partition),即數據集的基本組成單位
1、一個分區通常與一個計算任務關聯,分區的個數決定了並行的粒度;
2、分區的個數可以在創建 RDD 的時候進行設置。如果沒有設置,默認情況下由節點的 cores 個數決定;
3、每個 Partition 最終會被邏輯映射爲 BlockManager 中的一個 Block,而這個 Block 會被下一 個 Task(ShuffleMapTask/ResultTask)使用進行計算

2、A function for computing each split:
一個計算每個分區的函數,也就是算子 分區處理函數-compute
1、每個 RDD 都會實現 compute,用於對分區進行計算;
2、compute 函數會對迭代器進行復合,不需要保存每次計算結果;
3、該方法負責接收 parent RDDs 或者 data block 流入的 records 並進行計算,然後輸出加工 後的 records。

3、A list of dependencies on other RDDs:
RDD 之間的依賴關係:寬依賴和窄依賴 RDD 的每次轉換都會生成一個新的 RDD,所以 RDD 之間就會形成類似於流水線一樣的前後 依賴關係。在部分分區數據丟失時,Spark 可以通過這個依賴關係重新計算丟失的分區數據, 而不是對 RDD 的所有分區進行重新計算。
RDDx 依賴的 parent RDD 的個數由不同的轉換操作決定,例如二元轉換操作 x = a.join(b),RDD x 就會同時依賴於 RDD a 和 RDD b。而具體的依賴關係可以細分爲完全依賴和部分依賴,詳 細說明如下:
1、完全依賴:一個子 RDD 中的分區可以依賴於父 RDD 分區中一個或多個完整分區。 例如,map 操作產生的子 RDD 分區與父 RDD 分區之間是一對一的關係;對於 cartesian 操作
產生的子 RDD 分區與父 RDD 分區之間是多對多的關係。

2、部分依賴:父 RDD 的一個 partition 中的部分數據與 RDD x 的一個 partition 相關,而另一 部分數據與 RDD x 中的另一個 partition 有關。

4、Optionally, a Partitioner for key-value RDDs (to say that the RDD is hash-partitioned): 一個 Partitioner,即 RDD 的分片函數。 當前 Spark 中實現了兩種類型的分片函數,一個是基於哈希的 HashPartitioner,另外一個是 基於範圍的 RangePartitioner。只有對於於 key-value 的 RDD,纔會有 Partitioner,非 key-value的 RDD 的 Parititioner 的值是 None。Partitioner 函數不但決定了 RDD 本身的分片數量,也決 定了 parent RDD Shuffle 輸出時的分片數量。
1、只有鍵值對 RDD,纔會有 Partitioner。其他非鍵值對的 RDD 的 Partitioner 爲 None; 2、它定義了鍵值對 RDD 中的元素如何被鍵分區,能夠將每個鍵映射到對應的分區 ID,從 0 到”numPartitions- 1”上;
3、Partitioner 不但決定了 RDD 本身的分區個數,也決定了 parent RDD shuffle 輸出的分區個 數。
4、在分區器的選擇上,默認情況下,如果有一組 RDDs(父 RDD)已經有了 Partitioner,則 從中選擇一個分區數較大的 Partitioner;否則,使用默認的 HashPartitioner。
5、對於 HashPartitioner 分區數的設置,如果配置了 spark.default.parallelism 屬性,則將分區 數設置爲此值,否則,將分區數設置爲上游 RDDs 中最大分區數。

5、Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file):一個列表,存儲存取每個 Partition 的優先位置(preferred location)。
1、對於一個 HDFS 文件來說,這個列表保存的就是每個 Partition 所在的塊的位置。
2、按照”移動數據不如移動計算”的理念,Spark 在進行任務調度的時候,會儘可能地將計算 任務分配到其所要處理數據塊的存儲位置。
3、每個子 RDDgetPreferredLocations 的實現中,都會優先選擇父 RDD 中對應分區的 preferedLocation,其次才選擇自己設置的優先位置

2. 創建 RDD

上面我們學習了什麼是RDD,然後我們下面開始學習如何創建一個RDD數據集:
創建 RDD 主要有兩種方式:官網解釋 There are two ways to create RDDs: parallelizing an existing collection in your driver program, or referencing a dataset in an external storage system, such as a shared filesystem, HDFS, HBase, or any data source offering a Hadoop InputFormat.
1、由一個已經存在的 Scala 數據集合創建

val rdd = sc.parallelize(Array(1,2,3,4,5,6,7,8)) 
val rdd = sc.makeRDD(Array(1,2,3,4,5,6,7,8)) 

2、由外部存儲系統的數據集創建,包括本地的文件系統,還有所有 Hadoop 支持的數據集, 比如 HDFS、Cassandra、HBase 等

 val rdd = sc.textFile("hdfs://myha01/spark/wc/input/words.txt") 

3、擴展
從 HBase 當中讀取
從 ElasticSearch 中讀取

3. RDD常見的算子介紹

RDD 的編程 API
RDD編程官網:http://spark.apache.org/docs/latest/rdd-programming-guide.html#resilient-distributed-datasets-rdds
首先我們學習算子的時候分爲兩類:

3.1 Transformation 類算子:

RDD 中的所有轉換(Transformation)都是延遲加載的,也就是說,它們並不會直接計算結 果。相反的,它們只是記住這些應用到基礎數據集(例如一個文件)上的轉換動作。只有當 發生一個要求返回結果給 Driver 的動作時,這些轉換纔會真正運行。這種設計讓 Spark 更加 有效率地運行
官網:http://spark.apache.org/docs/latest/rdd-programming-guide.html#transformations
概念:字面意思就是進行轉換,將rdd由一個形態,轉化成另外一個形態,比如rdd1的類型爲RDD[String]—>RDD[(String, Int)]
注意: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]
sample(withReplacement, fraction, seed) 根據 fraction 指定的比例對數據進行採樣,可以選擇是 否使用隨機數進行替換,seed 用於指定隨機數生成器 種子 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)對的數據集上使用,返回一個(K,V)對的數據 集,key 相同的值,都被使用指定的 reduce 函數聚合 到一起。和 groupByKey 類似,任務的個數是可以通過 第二個可選參數來配置的。
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]) 先按分區聚合再總的聚合,每次要跟初始值交流 例如:aggregateByKey(0)(+,+) 對 K/V 的 RDD 進行 操作
sortByKey([ascending], [numTasks]) 在一個(K,V)的 RDD 上調用,K 必須實現 Ordered 接口,返回一個按照 key 進行排序的(K,V)的 RDD
sortBy(func,[ascending], [numTasks]) 與 sortByKey 類似,但是更靈活 第一個參數是根據什麼排序 第二個是怎麼排序,true 正序,false 倒序 第三個排序後分區數,默認與原 RDD 一樣
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
cartesian(otherDataset) 笛卡爾積
pipe(command, [envVars]) 調用外部排序
coalesce(numPartitions) 重新分區,第一個參數是分區數,第二個參數是否 shuffle 默認 false,少分區變多分區 true,多分區變少 分區 false repartition(numPartitions) 重新分區,必須 shuffle,參數是要分多少區,少變多 repartitionAndSortWithinPartitions( partitioner) 重新分區+排序,比先分區再排序效率高,對K/V 的RDD 進行操作
foldByKey(zeroValue)(seqOp) 該函數用於 K/V 做摺疊,合併處理,與 aggregate 類似 第一個括號的參數應用於每個 V 值,第二括號函數是 聚合例如:+
combineByKey 合併相同的 key 的值 rdd1.combineByKey(x => x, (a: Int, b: Int) => a + b, (m: Int, n: Int) => m + n)
partitionBy(partitioner) 對 RDD 進行分區,partitioner 是分區器 例如 new HashPartition(2)
cache 與 persist RDD 緩存,可以避免重複計算從而減少時間,區別: cache 內部調用了 persist 算子,cache 默認就一個緩存 級別 MEMORY-ONLY ,而 persist 則可以選擇緩存級別
subtract(rdd) 返回前 rdd 元素不在後 rdd 的 rdd
leftOuterJoin leftOuterJoin 類似於 SQL 中的左外關聯 left outer join, 返回結果以前面的 RDD 爲主,關聯不上的記錄爲空。 只能用於兩個 RDD之間的關聯,如果要多個 RDD關聯, 多關聯幾次即可。
rightOuterJoin rightOuterJoin 類似於 SQL 中的有外關聯 right outer join,返回結果以參數中的 RDD 爲主,關聯不上的記錄 爲空。只能用於兩個 RDD 之間的關聯,如果要多個 RDD 關聯,多關聯幾次即可
subtractByKey substractByKey 和基本轉換操作中的 subtract 類似只不 過這裏是針對 K 的,返回在主 RDD 中出現,並且不在 otherRDD 中出現的元素
總結: Transformation 返回值還是一個 RDD。它使用了鏈式調用的設計模式,對一個 RDD 進行計 算後,變換成另外一個 RDD,然後這個 RDD 又可以進行另外一次轉換。這個過程是分佈式 的

3.2 action 類算子:

官網:http://spark.apache.org/docs/latest/rdd-programming-guide.html#actions
概念:字面意思就是執行,action的操作目的就是用來執行一個job作業的
注意:action操作是spark作業運行的動因

Action 算子 算子含義
reduce(func) 通過 func 函數聚集 RDD 中的所有元素,這個功能必須是可交 換且可並聯的
reduceByKeyLocally def reduceByKeyLocally(func: (V, V) => V): Map[K, V] 該函數將RDD[K,V]中每個K對應的V值根據映射函數來運算, 運算結果映射到一個 Map[K,V]中,而不是 RDD[K,V]
collect() 在驅動程序中,以數組的形式返回數據集的所有元素
count() 返回 RDD 的元素個數
first() 返回 RDD 的第一個元素(類似於 take(1))
take(n) 返回一個由數據集的前 n 個元素組成的數組
takeSample(withReplacement ,num, [seed]) 返回一個數組,該數組由從數據集中隨機採樣的 num 個元素 組成,可以選擇是否用隨機數替換不足的部分,seed 用於指 定隨機數生成器種子
top top 函數用於從 RDD 中,按照默認(降序)或者指定的排序 規則,返回前 num 個元素
takeOrdered(n, [ordering]) takeOrdered 和 top 類似,只不過以和 top 相反的順序返回元 素
countByKey() 針對(K,V)類型的 RDD,返回一個(K,Int)的 map,表示每一個 key 對應的元素個數 foreach(func) 在數據集的每一個元素上,運行函數 func 進行更新。
foreachPartition def foreachPartition(f: Iterator[T] => Unit): Unit 遍歷每個 Partition
fold def fold(zeroValue: T)(op: (T, T) => T): T fold 是 aggregate 的簡化,將 aggregate 中的 seqOp 和 combOp 使用同一個函數 op
aggregate def aggregate[U](zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U)(implicit arg0: ClassTag[U]): U aggregate 用戶聚合 RDD 中的元素,先使用 seqOp 將 RDD 中 每個分區中的 T 類型元素聚合成 U 類型,再使用 combOp 將 之前每個分區聚合後的U類型聚合成U類型,特別注意seqOp 和 combOp 都會使用 zeroValue 的值,zeroValue 的類型爲 U
lookup 針對 key-value 類型的 RDD 進行查找
saveAsTextFile(path) 將數據集的元素以 textfile 的形式保存到 HDFS 文件系統或者 其他支持的文件系統,對於每個元素,Spark 將會調用 toString 方法,將它裝換爲文件中的文本
saveAsSequenceFile(path) 將數據集中的元素以 Hadoop sequencefile 的格式保存到指定 的目錄下,可以使 HDFS 或者其他 Hadoop 支持的文件系統
saveAsObjectFile(path) saveAsObjectFile 用於將 RDD 中的元素序列化成對象,存儲到 文件中。對於 HDFS,默認採用 SequenceFile 保存
總結: Action 返回值不是一個 RDD。它要麼是一個 Scala 的普通集合,要麼是一個值,要麼是空, 最終或返回到 Driver 程序,或把 RDD 寫入到文件系統中

3.3 WordCount 中的 RDD

在這裏插入圖片描述
在這裏插入圖片描述
那麼問題來了,請問在下面這一句標準的 wordcount 中到底產生了幾個 RDD 呢??? sc.textFile(“hdfs://myha01/wc/input/words.txt”).flatMap(.split("")).map((,1))
.reduceByKey(+).collect

4. RDD的依賴關係

RDD 和它依賴的父 RDD(s)的關係有兩種不同的類型,即窄依賴(narrow dependency)和 寬依賴(wide dependency
在這裏插入圖片描述

4.1 窄依賴和寬依賴對比

窄依賴指的是每一個父 RDD 的 Partition 最多被子 RDD 的一個 Partition 使用
總結:窄依賴我們形象的比喻爲獨生子女,窄依賴的函數有:map, filter, union, join(父 RDD 是 hash-partitioned ), mapPartitions, mapValues

寬依賴指的是多個子 RDD 的 Partition 會依賴同一個父 RDD 的 Partition
總結:窄依賴我們形象的比喻爲超生,寬依賴的函數有:groupByKey、partitionBy、reduceByKey、 sortByKey、join(父 RDD 不是 hash-partitioned )

4.2、窄依賴和寬依賴總結

在這裏我們是從父 RDD 的 partition 被使用的個數來定義窄依賴和寬依賴,因此可以用一句 話概括下:如果父 RDD 的一個 Partition 被子 RDD 的一個 Partition 所使用就是窄依賴,否則 的話就是寬依賴。因爲是確定的 partition 數量的依賴關係,所以 RDD 之間的依賴關係就是 窄依賴;由此我們可以得出一個推論:即窄依賴不僅包含一對一的窄依賴,還包含一對固定 個數的窄依賴。

一對固定個數的窄依賴的理解:即子 RDD 的 partition 對父 RDD 依賴的 Partition 的數量不會 隨着 RDD 數據規模的改變而改變;換句話說,無論是有 100T 的數據量還是 1P 的數據量, 在窄依賴中,子 RDD 所依賴的父 RDD 的 partition 的個數是確定的,而寬依賴是 shuffle 級別 的,數據量越大,那麼子 RDD 所依賴的父 RDD 的個數就越多,從而子 RDD 所依賴的父 RDD 的 partition 的個數也會變得越來越多。

4.3、Lineage

RDD 只支持粗粒度轉換,即在大量記錄上執行的單個操作。將創建 RDD 的一系列 Lineage(即 血統)記錄下來,以便恢復丟失的分區。RDD 的 Lineage 會記錄 RDD 的元數據信息和轉換行 爲,當該 RDD 的部分分區數據丟失時,它可以根據這些信息來重新運算和恢復丟失的數據 分區。

5. DAG 生成

DAG(Directed Acyclic Graph)叫做有向無環圖,原始的RDD通過一系列的轉換就就形成了DAG, 根據 RDD 之間的依賴關係的不同將 DAG 劃分成不同的 Stage,對於窄依賴,partition 的轉換 處理在 Stage 中完成計算。對於寬依賴,由於有 Shuffle 的存在,只能在 parent RDD 處理完 成後,才能開始接下來的計算,因此寬依賴是劃分 Stage 的依據。
在這裏插入圖片描述
在 spark 中,會根據 RDD 之間的依賴關係將 DAG 圖(有向無環圖)劃分爲不同的階段,對 於窄依賴,由於 partition 依賴關係的確定性,partition 的轉換處理就可以在同一個線程裏完 成,窄依賴就被 spark 劃分到同一個 stage 中,而對於寬依賴,只能等父 RDD shuffle 處理完 成後,下一個 stage 才能開始接下來的計算。

因此 spark 劃分 stage 的整體思路是:從後往前推,遇到寬依賴就斷開,劃分爲一個 stage; 遇到窄依賴就將這個 RDD 加入該 stage 中。因此在上圖中 RDD C,RDD D,RDD E,RDD F 被 構建在一個 stage 中,RDD A 被構建在一個單獨的 Stage 中,而 RDD B 和 RDD G 又被構建在 同一個 stage 中。

在 spark 中,Task 的類型分爲 2 種:ShuffleMapTask 和 ResultTask

簡單來說,DAG 的最後一個階段會爲每個結果的 partition 生成一個 ResultTask,即每個 Stage 裏面的 Task 的數量是由該 Stage 中最後一個 RDD 的 Partition 的數量所決定的!而其餘所有 階段都會生成 ShuffleMapTask;之所以稱之爲 ShuffleMapTask 是因爲它需要將自己的計算結 果通過 shuffle 到下一個 stage 中;也就是說上圖中的 stage1 和 stage2 相當於 MapReduce 中 的 Mapper,而 ResultTask 所代表的 stage3 就相當於 MapReduce 中的 reducer。

在之前動手操作了一個 WordCount 程序,因此可知,Hadoop 中 MapReduce 操作中的 Mapper 和 Reducer 在 spark 中的基本等量算子是 map 和 reduceByKey;不過區別在於:Hadoop 中的 MapReduce 天生就是排序的;而 reduceByKey 只是根據 Key 進行 reduce,但 spark 除了這兩 個算子還有其他的算子;因此從這個意義上來說,Spark 比 Hadoop 的計算算子更爲豐富。

6. RDD 緩存

Spark 速度非常快的原因之一,就是在不同操作中可以在內存中持久化或緩存個數據集。當 持久化某個 RDD 後,每一個節點都將把計算的分片結果保存在內存中,並在對此 RDD 或衍 生出的 RDD 進行的其他動作中重用。這使得後續的動作變得更加迅速。RDD 相關的持久化 和緩存,是 Spark 最重要的特徵之一。可以說,緩存是 Spark 構建迭代式算法和快速交互式 查詢的關鍵。

6.1 RDD 的緩存方式

RDD 通過 persist 方法或 cache 方法可以將前面的計算結果緩存,但是並不是這兩個方法被 調用時立即緩存,而是觸發後面的 action 時,該 RDD 將會被緩存在計算節點的內存中,並 供後面重用。
在這裏插入圖片描述

通過查看源碼發現 cache 最終也是調用了 persist 方法,默認的存儲級別都是僅在內存存儲一 份,Spark 的存儲級別還有好多種,存儲級別在 object StorageLevel 中定義的。
在這裏插入圖片描述
在這裏插入圖片描述
緩存有可能丟失,或者存儲存儲於內存的數據由於內存不足而被刪除,RDD 的緩存容錯機制 保證了即使緩存丟失也能保證計算的正確執行。通過基於 RDD 的一系列轉換,丟失的數據 會被重算,由於 RDD 的各個 Partition 是相對獨立的,因此只需要計算丟失的部分即可,並 不需要重算全部 Partition。
在這裏插入圖片描述
下次我們還要來學習喲!!!
記得點贊加關注!!! 不迷路 !!!
人活着真累:上車得排隊,愛你又受罪,吃飯沒香味,喝酒容易醉,掙錢得交稅!

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