Spark從零到一 (二)Spark之RDD

 

 

                                                         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種:ShuffleMapTaskResultTask

簡單來說,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的計算算子更爲豐富。

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