Spark之RDD
一、RDD的概述
1.1 什麼是RDD?
RDD(Resilient Distributed Dataset)叫做彈性分佈式數據集,是Spark中最基本的數據抽象,它代表一個不可變、可分區、裏面的元素可並行計算的集合。RDD具有數據流模型的特點:自動容錯、位置感知性調度和可伸縮性。RDD允許用戶在執行多個查詢時顯式地將工作集緩存在內存中,後續的查詢能夠重用工作集,這極大地提升了查詢速度。
1.2 RDD的屬性
(1)一組分片(Partition),即數據集的基本組成單位。對於RDD來說,每個分片都會被一個計算任務處理,並決定並行計算的粒度。用戶可以在創建RDD時指定RDD的分片個數,如果沒有指定,那麼就會採用默認值。默認值就是程序所分配到的CPU Core的數目。
(2)一個計算每個分區的函數。Spark中RDD的計算是以分片爲單位的,每個RDD都會實現compute函數以達到這個目的。compute函數會對迭代器進行復合,不需要保存每次計算的結果。
(3)RDD之間的依賴關係。RDD的每次轉換都會生成一個新的RDD,所以RDD之間就會形成類似於流水線一樣的前後依賴關係。在部分分區數據丟失時,Spark可以通過這個依賴關係重新計算丟失的分區數據,而不是對RDD的所有分區進行重新計算。
(4)一個Partitioner,即RDD的分片函數。當前Spark中實現了兩種類型的分片函數,一個是基於哈希的HashPartitioner,另外一個是基於範圍的RangePartitioner。只有對於於key-value的RDD,纔會有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函數不但決定了RDD本身的分片數量,也決定了parent RDD Shuffle輸出時的分片數量。
(5)一個列表,存儲存取每個Partition的優先位置(preferred location)。對於一個HDFS文件來說,這個列表保存的就是每個Partition所在的塊的位置。按照“移動數據不如移動計算”的理念,Spark在進行任務調度的時候,會盡可能地將計算任務分配到其所要處理數據塊的存儲位置。
1.3 WordCount粗圖解RDD
其中hello.txt
二、RDD的創建方式
- 通過讀取外部文件創建RDD
JavaRDD<String> lines = sc.textFile("test.log");
由外部存儲系統的數據集創建,包括本地的文件系統,還有所有Hadoop支持的數據集,比如HDFS、Cassandra、HBase等。
- 通過現有的集合轉變的方式創建RDD
List<Integer> list= Arrays.asList(1,2,3,4,5,6,7,8,9,10);
JavaRDD<Integer> listRdd=sc.parallelize(list);
- 其它行爲創建RDD
1:讀取數據庫等等其他的操作。也可以生成RDD。
2:RDD可以通過其他的RDD轉換而來的。
三、RDD編程API
Spark支持兩個類型(算子)操作:Transformation和Action
3.1 Transformation
主要做的是就是將一個已有的RDD生成另外一個RDD。Transformation具有lazy特性(延遲加載)。Transformation算子的代碼不會真正被執行。只有當我們的程序裏面遇到一個action算子的時候,代碼纔會真正的被執行。這種設計讓Spark更加有效率地運行。
常用的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)的RDD上調用,返回一個(K,V)的RDD,使用指定的reduce函數,將相同key的值聚合到一起,與groupByKey類似,reduce任務的個數可以通過第二個可選的參數來設置 |
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]) |
先按分區聚合 再總的聚合 每次要跟初始值交流 例如:aggregateByKey(0)(_+_,_+_) 對k/y的RDD進行操作 |
sortByKey([ascending], [numTasks]) |
在一個(K,V)的RDD上調用,K必須實現Ordered接口,返回一個按照key進行排序的(K,V)的RDD |
sortBy(func,[ascending], [numTasks]) |
與sortByKey類似,但是更靈活 第一個參數是根據什麼排序 第二個是怎麼排序 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<V>,Iterable<W>))類型的RDD |
cartesian(otherDataset) |
兩個RDD的笛卡爾積 的成很多個K/V |
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 |
RDD緩存,可以避免重複計算從而減少時間,區別:cache內部調用了persist算子,cache默認就一個緩存級別MEMORY-ONLY ,而persist則可以選擇緩存級別 |
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中出現的元素 |
3.2 Action
觸發代碼的運行,我們一段spark代碼裏面至少需要有一個action操作。
常用的Action:
動作 |
含義 |
reduce(func) |
通過func函數聚集RDD中的所有元素,這個功能必須是課交換且可並聯的 |
collect() |
在驅動程序中,以數組的形式返回數據集的所有元素 |
count() |
返回RDD的元素個數 |
first() |
返回RDD的第一個元素(類似於take(1)) |
take(n) |
返回一個由數據集的前n個元素組成的數組 |
takeSample(withReplacement,num, [seed]) |
返回一個數組,該數組由從數據集中隨機採樣的num個元素組成,可以選擇是否用隨機數替換不足的部分,seed用於指定隨機數生成器種子 |
takeOrdered(n, [ordering]) |
|
saveAsTextFile(path) |
將數據集的元素以textfile的形式保存到HDFS文件系統或者其他支持的文件系統,對於每個元素,Spark將會調用toString方法,將它裝換爲文件中的文本 |
saveAsSequenceFile(path) |
將數據集中的元素以Hadoop sequencefile的格式保存到指定的目錄下,可以使HDFS或者其他Hadoop支持的文件系統。 |
saveAsObjectFile(path) |
|
countByKey() |
針對(K,V)類型的RDD,返回一個(K,Int)的map,表示每一個key對應的元素個數。 |
foreach(func) |
在數據集的每一個元素上,運行函數func進行更新。 |
aggregate |
先對分區進行操作,在總體操作 |
reduceByKeyLocally |
|
lookup |
|
top |
|
fold |
|
foreachPartition |
|
|
|
3.3 Spark in Java
1:spark開發環境配置
- maven配置文件
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.10</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.10</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-hive_2.10</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.10</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>${hadoop.version}</version>
</dependency>
- 客戶端連接配置
SparkConf conf = new SparkConf().setAppName("WorldCountLocal").setMaster("local");
JavaSparkContext sc = new JavaSparkContext(conf);
由於是本地環境開發,因此mater配置爲local本地連接,啓動本地spark客戶端,配置如上代碼,本地就可以進行spark開發了。
如果是真正的生產環境,應該搭建spark集羣,再配置連接集羣,集羣生產不在本文討論,故不作展開講解。
- 用java8編寫第一個spark統計單詞出現的程序
首先在本地E盤創建test.txt文件內容如下:
asada,b,ds,asdsa,ffff,ssss,a,d,g,df,as,g,h,a,d,gc,z,fd
內容你可以自行輸入。
package spark;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import scala.Tuple2;
public class FirstDay {
public static void main(String[] args) {
SparkConf conf = new SparkConf();
conf.setAppName("WortCount");
conf.setMaster("local");
JavaSparkContext sc = new JavaSparkContext(conf);
JavaRDD<String> fileRDD = sc.textFile("E:\\test.txt");
JavaRDD<String> wordRdd = fileRDD.flatMap(line -> Arrays.asList(line.split(",")).iterator());
JavaPairRDD<String, Integer> wordOneRDD = wordRdd.mapToPair(word -> new Tuple2<>(word, 1));
JavaPairRDD<String, Integer> wordCountRDD = wordOneRDD.reduceByKey((x, y) -> x + y);
JavaPairRDD<Integer, String> count2WordRDD = wordCountRDD.mapToPair(tuple -> new Tuple2<>(tuple._2, tuple._1));
JavaPairRDD<Integer, String> sortRDD = count2WordRDD.sortByKey(false);
JavaPairRDD<String, Integer> resultRDD = sortRDD.mapToPair(tuple -> new Tuple2<>(tuple._2, tuple._1));
resultRDD.saveAsTextFile("E:\\result8");
sc.close();
}
}
如上程序實現了一個簡單的單詞出現此時統計的功能,其執行的順序和原理如下:
1:首先程序配置本地啓動模式運行代碼。
2:JavaRDD<String> fileRDD = sc.textFile("E:\\test.txt")讀取本地E盤名爲test.txt的文件作爲輸入轉化爲RDD。
3: JavaRDD<String> wordRdd = fileRDD.flatMap(line -> Arrays.asList(line.split(",")).iterator())
將步驟2中的RDD通過flatMap方法以逗號分隔的形式轉化爲新的RDD。
4:JavaPairRDD<String, Integer> wordOneRDD = wordRdd.mapToPair(word -> new Tuple2<>(word, 1))
通過mapToPair的方法將步驟三一維的單詞轉化爲出現一次就轉換爲{a,1}形式的二維map.比如 a通過步驟四就轉爲了
{a,1}的形式。
5: JavaPairRDD<String, Integer> wordCountRDD = wordOneRDD.reduceByKey((x, y) -> x + y)
通過reduceByKey將步驟四得到的RDD按照key進行reduce,相同的key將他的value進行加法然後返回。
比如{a,1},{b,1},{b,1}通過步驟五就變成了{a,1},{b,2}.
6:JavaPairRDD<Integer, String> count2WordRDD = wordCountRDD.mapToPair(tuple -> new Tuple2<>(tuple._2, tuple._1))
將步驟5得到的結果key和value調轉返回,例如{a,1}得到{1,a}。
注意:Tuple2是個二維的數組,類似java的map集合,tuple._2, tuple._1,tuple._2代表獲得數組的value,tuple._1代表獲得數組的key。
7:JavaPairRDD<Integer, String> sortRDD = count2WordRDD.sortByKey(false)將步驟六的結果按照降序排序。
8:JavaPairRDD<String, Integer> resultRDD = sortRDD.mapToPair(tuple -> new Tuple2<>(tuple._2, tuple._1))反轉步驟七結果。
下面給出執行圖幫助讀者更形象的理解RDD的執行步驟:
執行如上程序打開E盤會發現如下文件:
打開如圖所示的最後一個文件得到如下結果:
(d,2)
(a,2)
(g,2)
(z,1)
(ssss,1)
(asada,1)
(ffff,1)
(b,1)
(asdsa,1)
(h,1)
(as,1)
(ds,1)
(fd,1)
(gc,1)
(df,1)
四、RDD的寬依賴和窄依賴
4.1 RDD依賴關係的本質內幕
由於RDD是粗粒度的操作數據集,每個Transformation操作都會生成一個新的RDD,所以RDD之間就會形成類似流水線的前後依賴關係;RDD和它依賴的父RDD(s)的關係有兩種不同的類型,即窄依賴(narrow dependency)和寬依賴(wide dependency)。如圖所示顯示了RDD之間的依賴關係。
從圖中可知:
窄依賴:是指每個父RDD的一個Partition最多被子RDD的一個Partition所使用,例如map、filter、union等操作都會產生窄依賴;(獨生子女)
寬依賴:是指一個父RDD的Partition會被多個子RDD的Partition所使用,例如groupByKey、reduceByKey、sortByKey等操作都會產生寬依賴;(超生)
需要特別說明的是對join操作有兩種情況:
(1)圖中左半部分join:如果兩個RDD在進行join操作時,一個RDD的partition僅僅和另一個RDD中已知個數的Partition進行join,那麼這種類型的join操作就是窄依賴,例如圖1中左半部分的join操作(join with inputs co-partitioned);
(2)圖中右半部分join:其它情況的join操作就是寬依賴,例如圖1中右半部分的join操作(join with inputs not co-partitioned),由於是需要父RDD的所有partition進行join的轉換,這就涉及到了shuffle,因此這種類型的join操作也是寬依賴。
總結:
在這裏我們是從父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.2 依賴關係下的數據流視圖
在spark中,會根據RDD之間的依賴關係將DAG圖(有向無環圖)劃分爲不同的階段,對於窄依賴,由於partition依賴關係的確定性,partition的轉換處理就可以在同一個線程裏完成,窄依賴就被spark劃分到同一個stage中,而對於寬依賴,只能等父RDD shuffle處理完成後,下一個stage才能開始接下來的計算。
因此spark劃分stage的整體思路是:從後往前推,遇到寬依賴就斷開,劃分爲一個stage;遇到窄依賴就將這個RDD加入該stage中。因此在圖2中RDD C,RDD D,RDD E,RDDF被構建在一個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的計算算子更爲豐富。