spark學習之-----spark程式設計模型

後開啟撒打發了發表於2018-01-30

1、Spark程式設計模型

1.1 術語定義

l應用程式(Application): 基於Spark的使用者程式,包含了一個Driver Program 和叢集中多個的Executor;

l驅動程式(Driver Program):執行Application的main()函式並且建立SparkContext,通常用SparkContext代表Driver Program;

l執行單元(Executor): 是為某Application執行在Worker Node上的一個程式,該程式負責執行Task,並且負責將資料存在記憶體或者磁碟上,每個Application都有各自獨立的Executors;

l叢集管理程式(Cluster Manager): 在叢集上獲取資源的外部服務(例如:Standalone、Mesos或Yarn);

l操作(Operation):作用於RDD的各種操作分為Transformation和Action;

1.2 模型組成

Spark應用程式可分兩部分:Driver部分和Executor部分

clip_image002

1.2.1 Driver部分

Driver部分主要是對SparkContext進行配置、初始化以及關閉。初始化SparkContext是為了構建Spark應用程式的執行環境,在初始化SparkContext,要先匯入一些Spark的類和隱式轉換;在Executor部分執行完畢後,需要將SparkContext關閉。

1.2.2 Executor部分

Spark應用程式的Executor部分是對資料的處理,資料分三種:

1.2.2.1 原生資料

包含原生的輸入資料和輸出資料

l對於輸入原生資料,Spark目前提供了兩種:

Ø  Scala集合資料集:如Array(1,2,3,4,5),Spark使用parallelize方法轉換成RDD

Ø  Hadoop資料集:Spark支援儲存在hadoop上的檔案和hadoop支援的其他檔案系統,如本地檔案、HBase、SequenceFile和Hadoop的輸入格式。例如Spark使用txtFile方法可以將本地檔案或HDFS檔案轉換成RDD

l對於輸出資料,Spark除了支援以上兩種資料,還支援scala標量

Ø  生成Scala標量資料,如count(返回RDD中元素的個數)、reduce、fold/aggregate;返回幾個標量,如take(返回前幾個元素)。

Ø  生成Scala集合資料集,如collect(把RDD中的所有元素倒入 Scala集合型別)、lookup(查詢對應key的所有值)。

Ø  生成hadoop資料集,如saveAsTextFile、saveAsSequenceFile

1.2.2.2 RDD

RDD具體在下一節中詳細描述,RDD提供了四種運算元:

l輸入運算元:將原生資料轉換成RDD,如parallelize、txtFile等

l轉換運算元:最主要的運算元,是Spark生成DAG圖的物件,轉換運算元並不立即執行,在觸發行動運算元後再提交給driver處理,生成DAG圖 -->  Stage --> Task --> Worker執行。

l快取運算元:對於要多次使用的RDD,可以緩衝加快執行速度,對重要資料可以採用多備份快取。

l行動運算元:將運算結果RDD轉換成原生資料,如count、reduce、collect、saveAsTextFile等。

1.2.2.3 共享變數

在Spark執行時,一個函式傳遞給RDD內的patition操作時,該函式所用到的變數在每個運算節點上都複製並維護了一份,並且各個節點之間不會相互影響。但是在Spark Application中,可能需要共享一些變數,提供Task或驅動程式使用。Spark提供了兩種共享變數:

l廣播變數(Broadcast Variables):可以快取到各個節點的共享變數,通常為只讀

– 廣播變數快取到各個節點的記憶體中,而不是每個 Task

– 廣播變數被建立後,能在叢集中執行的任何函式呼叫

– 廣播變數是隻讀的,不能在被廣播後修改

– 對於大資料集的廣播, Spark 嘗試使用高效的廣播演算法來降低通訊成本

使用方法:

val broadcastVar = sc.broadcast(Array(1, 2, 3))

l累計器:只支援加法操作的變數,可以實現計數器和變數求和。使用者可以呼叫SparkContext.accumulator(v)建立一個初始值為v的累加器,而執行在叢集上的Task可以使用“+=”操作,但這些任務卻不能讀取;只有驅動程式才能獲取累加器的值。

使用方法:

val accum = sc.accumulator(0)

sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum  + = x)

accum.value

val num=sc.parallelize(1 to 100)

2、RDD

2.1 術語定義

l彈性分散式資料集(RDD): Resillient Distributed Dataset,Spark的基本計算單元,可以通過一系列運算元進行操作(主要有Transformation和Action操作);

l有向無環圖(DAG):Directed Acycle graph,反應RDD之間的依賴關係;

l有向無環圖排程器(DAG Scheduler):根據Job構建基於Stage的DAG,並提交Stage給TaskScheduler;

l任務排程器(Task Scheduler):將Taskset提交給worker(叢集)執行並回報結果;

l窄依賴(Narrow dependency):子RDD依賴於父RDD中固定的data partition;

l寬依賴(Wide Dependency):子RDD對父RDD中的所有data partition都有依賴。

2.2 RDD概念

RDD是Spark的最基本抽象,是對分散式記憶體的抽象使用,實現了以操作本地集合的方式來操作分散式資料集的抽象實現。RDD是Spark最核心的東西,它表示已被分割槽,不可變的並能夠被並行操作的資料集合,不同的資料集格式對應不同的RDD實現。RDD必須是可序列化的。RDD可以cache到記憶體中,每次對RDD資料集的操作之後的結果,都可以存放到記憶體中,下一個操作可以直接從記憶體中輸入,省去了MapReduce大量的磁碟IO操作。這對於迭代運算比較常見的機器學習演算法, 互動式資料探勘來說,效率提升非常大。

RDD 最適合那種在資料集上的所有元素都執行相同操作的批處理式應用。在這種情況下, RDD 只需記錄血統中每個轉換就能還原丟失的資料分割槽,而無需記錄大量的資料操作日誌。所以 RDD 不適合那些需要非同步、細粒度更新狀態的應用 ,比如 Web 應用的儲存系統,或增量式的 Web 爬蟲等。對於這些應用,使用具有事務更新日誌和資料檢查點的資料庫系統更為高效。

2.2.1 RDD的特點

1.來源:一種是從持久儲存獲取資料,另一種是從其他RDD生成

2.只讀:狀態不可變,不能修改

3.分割槽:支援元素根據 Key 來分割槽 ( Partitioning ) ,儲存到多個結點上,還原時只會重新計算丟失分割槽的資料,而不會影響整個系統

4.路徑:在 RDD 中叫世族或血統 ( lineage ) ,即 RDD 有充足的資訊關於它是如何從其他 RDD 產生而來的

5.持久化:可以控制儲存級別(記憶體、磁碟等)來進行持久化

6.操作:豐富的動作 ( Action ) ,如Count、Reduce、Collect和Save 等

2.2.2 RDD基礎資料型別

目前有兩種型別的基礎RDD:並行集合(Parallelized Collections):接收一個已經存在的Scala集合,然後進行各種平行計算。 Hadoop資料集(Hadoop Datasets):在一個檔案的每條記錄上執行函式。只要檔案系統是HDFS,或者hadoop支援的任意儲存系統即可。這兩種型別的RDD都可以通過相同的方式進行操作,從而獲得子RDD等一系列擴充,形成lineage血統關係圖。

1. 並行化集合

並行化集合是通過呼叫SparkContext的parallelize方法,在一個已經存在的Scala集合上建立的(一個Seq物件)。集合的物件將會被拷貝,建立出一個可以被並行操作的分散式資料集。例如,下面的直譯器輸出,演示瞭如何從一個陣列建立一個並行集合。

例如:val rdd = sc.parallelize(Array(1 to 10)) 根據能啟動的executor的數量來進行切分多個slice,每一個slice啟動一個Task來進行處理。

val rdd = sc.parallelize(Array(1 to 10), 5) 指定了partition的數量

2. Hadoop資料集

Spark可以將任何Hadoop所支援的儲存資源轉化成RDD,如本地檔案(需要網路檔案系統,所有的節點都必須能訪問到)、HDFS、Cassandra、HBase、Amazon S3等,Spark支援文字檔案、SequenceFiles和任何Hadoop InputFormat格式。

(1)使用textFile()方法可以將本地檔案或HDFS檔案轉換成RDD

支援整個檔案目錄讀取,檔案可以是文字或者壓縮檔案(如gzip等,自動執行解壓縮並載入資料)。如textFile(”file:///dfs/data”)

支援萬用字元讀取,例如:

val rdd1 = sc.textFile("file:///root/access_log/access_log*.filter");

val rdd2=rdd1.map(_.split("t")).filter(_.length==6)

rdd2.count()

......

14/08/20 14:44:48 INFO HadoopRDD: Input split: file:/root/access_log/access_log.20080611.decode.filter:134217728+20705903

......

textFile()可選第二個引數slice,預設情況下為每一個block分配一個slice。使用者也可以通過slice指定更多的分片,但不能使用少於HDFS block的分片數。

(2)使用wholeTextFiles()讀取目錄裡面的小檔案,返回(使用者名稱、內容)對

(3)使用sequenceFile[K,V]()方法可以將SequenceFile轉換成RDD。SequenceFile檔案是Hadoop用來儲存二進位制形式的key-value對而設計的一種平面檔案(Flat File)。

(4)使用SparkContext.hadoopRDD方法可以將其他任何Hadoop輸入型別轉化成RDD使用方法。一般來說,HadoopRDD中每一個HDFS block都成為一個RDD分割槽。

此外,通過Transformation可以將HadoopRDD等轉換成FilterRDD(依賴一個父RDD產生)和JoinedRDD(依賴所有父RDD)等。

2.2.3 例子:控制檯日誌挖掘

假設網站中的一個 WebService 出現錯誤,我們想要從數以 TB 的 HDFS 日誌檔案中找到問題的原因,此時我們就可以用 Spark 載入日誌檔案到一組結點組成叢集的 RAM 中,並互動式地進行查詢。以下是程式碼示例:

clip_image003

首先行 1 從 HDFS 檔案中建立出一個 RDD ,而行 2 則衍生出一個經過某些條件過濾後的 RDD 。行 3 將這個 RDD errors 快取到記憶體中,然而第一個 RDD lines 不會駐留在記憶體中。這樣做很有必要,因為 errors 可能非常小,足以全部裝進記憶體,而原始資料則會非常龐大。經過快取後,現在就可以反覆重用 errors資料了。我們這裡做了兩個操作,第一個是統計 errors 中包含 MySQL 字樣的總行數,第二個則是取出包含 HDFS 字樣的行的第三列時間,並儲存成一個集合。

clip_image005

這裡要注意的是前面曾經提到過的 Spark 的延遲處理。 Spark 排程器會將 filter 和 map 這兩個轉換儲存到管道,然後一起傳送給結點去計算。

2.3 轉換與操作

對於RDD可以有兩種計算方式:轉換(返回值還是一個RDD)與操作(返回值不是一個RDD)

l轉換(Transformations) (如:map, filter, groupBy, join等),Transformations操作是Lazy的,也就是說從一個RDD轉換生成另一個RDD的操作不是馬上執行,Spark在遇到Transformations操作時只會記錄需要這樣的操作,並不會去執行,需要等到有Actions操作的時候才會真正啟動計算過程進行計算。

l操作(Actions) (如:count, collect, save等),Actions操作會返回結果或把RDD資料寫到儲存系統中。Actions是觸發Spark啟動計算的動因。

clip_image007

2.3.1 轉換

reduce(func)

通過函式func聚集資料集中的所有元素。Func函式接受2個引數,返回一個值。這個函式必須是關聯性的,確保可以被正確的併發執行

collect()

在Driver的程式中,以陣列的形式,返回資料集的所有元素。這通常會在使用filter或者其它操作後,返回一個足夠小的資料子集再使用,直接將整個RDD集Collect返回,很可能會讓Driver程式OOM

count()

返回資料集的元素個數

take(n)

返回一個陣列,由資料集的前n個元素組成。注意,這個操作目前並非在多個節點上,並行執行,而是Driver程式所在機器,單機計算所有的元素(Gateway的記憶體壓力會增大,需要謹慎使用)

first()

返回資料集的第一個元素(類似於take(1)

saveAsTextFile(path)

將資料集的元素,以textfile的形式,儲存到本地檔案系統,hdfs或者任何其它hadoop支援的檔案系統。Spark將會呼叫每個元素的toString方法,並將它轉換為檔案中的一行文字

saveAsSequenceFile(path)

將資料集的元素,以sequencefile的格式,儲存到指定的目錄下,本地系統,hdfs或者任何其它hadoop支援的檔案系統。RDD的元素必須由key-value對組成,並都實現了Hadoop的Writable介面,或隱式可以轉換為Writable(Spark包括了基本型別的轉換,例如Int,Double,String等等)

foreach(func)

在資料集的每一個元素上,執行函式func。這通常用於更新一個累加器變數,或者和外部儲存系統做互動

2.3.2 操作

map(func)

返回一個新的分散式資料集,由每個原元素經過func函式轉換後組成

filter(func)

返回一個新的資料集,由經過func函式後返回值為true的原元素組成

flatMap(func)

類似於map,但是每一個輸入元素,會被對映為0到多個輸出元素(因此,func函式的返回值是一個Seq,而不是單一元素)

flatMap(func)

類似於map,但是每一個輸入元素,會被對映為0到多個輸出元素(因此,func函式的返回值是一個Seq,而不是單一元素)

sample(withReplacement,  frac, seed)

根據給定的隨機種子seed,隨機抽樣出數量為frac的資料

union(otherDataset)

返回一個新的資料集,由原資料集和引數聯合而成

groupByKey([numTasks])

在一個由(K,V)對組成的資料集上呼叫,返回一個(K,Seq[V])對的資料集。注意:預設情況下,使用8個並行任務進行分組,你可以傳入numTask可選引數,根據資料量設定不同數目的Task

reduceByKey(func,  [numTasks])

在一個(K,V)對的資料集上使用,返回一個(K,V)對的資料集,key相同的值,都被使用指定的reduce函式聚合到一起。和groupbykey類似,任務的個數是可以通過第二個可選引數來配置的。

join(otherDataset,  [numTasks])

在型別為(K,V)和(K,W)型別的資料集上呼叫,返回一個(K,(V,W))對,每個key中的所有元素都在一起的資料集

groupWith(otherDataset,  [numTasks])

在型別為(K,V)和(K,W)型別的資料集上呼叫,返回一個資料集,組成元素為(K, Seq[V], Seq[W]) Tuples。這個操作在其它框架,稱為CoGroup

cartesian(otherDataset)

笛卡爾積。但在資料集T和U上呼叫時,返回一個(T,U)對的資料集,所有元素互動進行笛卡爾積。

flatMap(func)

類似於map,但是每一個輸入元素,會被對映為0到多個輸出元素(因此,func函式的返回值是一個Seq,而不是單一元素)

2.4 依賴型別

在 RDD 中將依賴劃分成了兩種型別:窄依賴 (Narrow Dependencies) 和寬依賴 (Wide Dependencies) 。窄依賴是指 父 RDD 的每個分割槽都只被子 RDD的一個分割槽所使用 。相應的,那麼寬依賴就是指父 RDD 的分割槽被多個子 RDD 的分割槽所依賴。例如, Map 就是一種窄依賴,而 Join 則會導致寬依賴 ( 除非父RDD 是 hash-partitioned ,見下圖 ) 。
clip_image009

l窄依賴(Narrow Dependencies )

Ø  子RDD 的每個分割槽依賴於常數個父分割槽(即與資料規模無關)

Ø  輸入輸出一對一的運算元,且結果RDD 的分割槽結構不變,主要是map 、flatMap

Ø  輸入輸出一對一,但結果RDD 的分割槽結構發生了變化,如union 、coalesce

Ø  從輸入中選擇部分元素的運算元,如filter 、distinct 、subtract 、sample

l寬依賴(Wide Dependencies )

Ø  子RDD 的每個分割槽依賴於所有父RDD 分割槽

Ø  對單個RDD 基於Key 進行重組和reduce,如groupByKey 、reduceByKey ;

Ø  對兩個RDD 基於Key 進行join 和重組,如join

2.5 RDD快取

Spark可以使用 persist 和 cache 方法將任意 RDD 快取到記憶體、磁碟檔案系統中。快取是容錯的,如果一個 RDD 分片丟失,可以通過構建它的transformation自動重構。被快取的 RDD 被使用的時,存取速度會被大大加速。一般的executor記憶體60%做 cache, 剩下的40%做task。

Spark中,RDD類可以使用cache() 和 persist() 方法來快取。cache()是persist()的特例,將該RDD快取到記憶體中。而persist可以指定一個StorageLevel。StorageLevel的列表可以在StorageLevel 伴生單例物件中找到:

object StorageLevel {

  val NONE = new StorageLevel(false, false, false, false)

  val DISK_ONLY = new StorageLevel(true, false, false, false)

  val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)

  val MEMORY_ONLY = new StorageLevel(false, true, false, true)

  val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)

  val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)

  val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)

  val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)

  val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)

  val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)

  val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)

  val OFF_HEAP = new StorageLevel(false, false, true, false) // Tachyon

}

 

// 其中,StorageLevel 類的構造器引數如下:

class StorageLevel private(  private var useDisk_ : Boolean,  private var useMemory_ : Boolean,  private var useOf

Spark的不同StorageLevel ,目的滿足記憶體使用和CPU效率權衡上的不同需求。我們建議通過以下的步驟來進行選擇:

l如果你的RDDs可以很好的與預設的儲存級別(MEMORY_ONLY)契合,就不需要做任何修改了。這已經是CPU使用效率最高的選項,它使得RDDs的操作儘可能的快;

l如果不行,試著使用MEMORY_ONLY_SER並且選擇一個快速序列化的庫使得物件在有比較高的空間使用率的情況下,依然可以較快被訪問;

l儘可能不要儲存到硬碟上,除非計算資料集的函式,計算量特別大,或者它們過濾了大量的資料。否則,重新計算一個分割槽的速度,和與從硬碟中讀取基本差不多快;

l如果你想有快速故障恢復能力,使用複製儲存級別(例如:用Spark來響應web應用的請求)。所有的儲存級別都有通過重新計算丟失資料恢復錯誤的容錯機制,但是複製儲存級別可以讓你在RDD上持續的執行任務,而不需要等待丟失的分割槽被重新計算;

l如果你想要定義你自己的儲存級別(比如複製因子為3而不是2),可以使用StorageLevel 單例物件的apply()方法;

l在不使用cached RDD的時候,及時使用unpersist方法來釋放它。

3、RDD動手實戰

在這裡我們將對RDD的轉換與操作進行動手實戰,首先通過實驗我們能夠觀測到轉換的懶執行,並通過toDebugString()去檢視RDD的LineAge,檢視RDD在執行過程中的變換過程,接著演示了從檔案讀取資料並進行大資料經典的單詞計數實驗,最後對搜狗提供的搜尋資料進行查詢,在此過程中演示快取等操作。

3.1 啟動Spark Shell

3.1.1 啟動Hadoop

在隨後的實驗中將使用到HDFS檔案系統,需要進行啟動

$cd /app/hadoop/hadoop-2.2.0/sbin

$./start-dfs.sh

clip_image011

3.1.2 啟動Spark

$cd /app/hadoop/spark-1.1.0/sbin

$./start-all.sh

clip_image013

3.1.3 啟動Spark Shell

在spark客戶端(這裡在hadoop1節點),使用spark-shell連線叢集,各個Excetor分配的核數和記憶體可以根據需要進行指定

$cd /app/hadoop/spark-1.1.0/bin

$./spark-shell --master spark://hadoop1:7077 --executor-memory 1024m --driver-memory 1024m

clip_image015

啟動後檢視啟動情況,如下圖所示:

clip_image017

3.2 上傳測試資料

搜狗日誌資料可以從http://download.labs.sogou.com/dl/q.html 下載,其中完整版大概2GB左右,檔案中欄位分別為:訪問時間\t使用者ID\t[查詢詞]\t該URL在返回結果中的排名\t使用者點選的順序號\t使用者點選的URL。其中SogouQ1.txt、SogouQ2.txt、SogouQ3.txt分別是用head -n 或者tail -n 從SogouQ資料日誌檔案中擷取,分別包含100萬,200萬和1000萬筆資料,這些測試資料也放在該系列配套資源的data\sogou目錄下。

搜狗日誌資料放在data\sogou下,把該目錄下的SogouQ1.txt、SogouQ2.txt和SogouQ3.txt解壓,然後通過下面的命令上傳到HDFS中/sogou目錄中

cd /home/hadoop/upload/

ll sogou

tar -zxf *.gz

hadoop fs -mkdir /sogou

hadoop fs -put sogou/SogouQ1.txt /sogou

hadoop fs -put sogou/SogouQ2.txt /sogou

hadoop fs -put sogou/SogouQ3.txt /sogou

hadoop fs -ls /sogou

clip_image019

3.3 轉換與操作

3.3.1 並行化集合例子演示

在該例子中通過parallelize方法定義了一個從1~10的資料集,然後通過map(_*2)對資料集每個數乘以2,接著通過filter(_%3==0)過濾被3整除的數字,最後使用toDebugString顯示RDD的LineAge,並通過collect計算出最終的結果。

val num=sc.parallelize(1 to 10)

val doublenum = num.map(_*2)

val threenum = doublenum.filter(_ % 3 == 0)

threenum.toDebugString

threenum.collect

在下圖執行結果圖中,我們可以看到RDD的LineAge演變,通過paralelize方法建立了一個ParalleCollectionRDD,使用map()方法後該RDD為MappedRDD,接著使用filter()方法後轉變為FilteredRDD。

 

clip_image021

在下圖中使用collect方法時觸發執行作業,通過任務計算出結果

clip_image023

以下語句和collect一樣,都會觸發作業執行

num.reduce (_ + _)

num.take(5)

num.first

num.count

num.take(5).foreach(println)

執行的情況可以通過頁面進行監控,在Spark Stages頁面中我們可以看到執行的詳細情況,包括執行的Stage id號、Job描述、提交時間、執行時間、Stage情況等,可以點選作業描述檢視更加詳細的情況:

clip_image025

在這個頁面上我們將看到三部分資訊:作業的基本資訊、Executor資訊和Tasks的資訊。特別是Tasks資訊可以瞭解到作業的分片情況,執行狀態、資料獲取位置、耗費時間及所處的Executor等資訊

clip_image027

3.3.2 Shuffle操作例子演示

在該例子中通過parallelize方法定義了K-V鍵值對資料集合,通過sortByKey()進行按照Key值進行排序,然後通過collect方法觸發作業執行得到結果。groupByKey()為按照Key進行歸組,reduceByKey(_+_)為按照Key進行累和,這三個方法的計算和前面的例子不同,因為這些RDD型別為寬依賴,在計算過程中發生了Shuffle動作。

val kv1=sc.parallelize(List(("A",1),("B",2),("C",3),("A",4),("B",5)))

kv1.sortByKey().collect

kv1.groupByKey().collect

kv1.reduceByKey(_+_).collect

clip_image029

clip_image031

呼叫groupByKey()執行結果

clip_image033

呼叫reduceByKey ()執行結果

clip_image035

我們在作業執行監控介面上能夠看到:每個作業分為兩個Stage,在第一個Stage中進行了Shuffle Write,在第二個Stage中進行了Shuffle Read。

clip_image037

在Stage詳細執行頁面中可以觀察第一個Stage執行情況,內容包括:Stage執行的基本資訊、每個Executor執行資訊和任務的執行資訊,特別在任務執行中我們可以看到任務的狀態、資料讀取的位置、機器節點、耗費時間和Shuffle Write時間等。

clip_image039

在下面進行了distinct、union、join和cogroup等操作中涉及到Shuffle過程

val kv2=sc.parallelize(List(("A",4),("A",4),("C",3),("A",4),("B",5)))

kv2.distinct.collect

kv1.union(kv2).collect

 

val kv3=sc.parallelize(List(("A",10),("B",20),("D",30)))

kv1.join(kv3).collect

kv1.cogroup(kv3).collect

3.3.3檔案例子讀取

這個是大資料經典的例子,在這個例子中通過不同方式讀取HDFS中的檔案,然後進行單詞計數,最終通過執行作業計算出結果。本例子中通過toDebugString可以看到RDD的變化,

第一步   按照資料夾讀取計算每個單詞出現個數

在該步驟中RDD的變換過程為:HadoopRDD->MappedRDD-> FlatMappedRDD->MappedRDD->PairRDDFunctions->ShuffleRDD->MapPartitionsRDD

val text = sc.textFile("hdfs://hadoop1:9000/class3/directory/")

text.toDebugString

val words=text.flatMap(_.split(" "))

val wordscount=words.map(x=>(x,1)).reduceByKey(_+_)

wordscount.toDebugString

wordscount.collect

RDD型別的變化過程如下:

l  首先使用textFile()讀取HDFS資料形成MappedRDD,這裡有可能有疑問,從HDFS讀取的資料不是HadoopRDD,怎麼變成了MappedRDD。回答這個問題需要從Spark原始碼進行分析,在sparkContext類中的textFile()方法讀取HDFS檔案後,使用了map()生成了MappedRDD。

clip_image041

clip_image043

l  然後使用flatMap()方法對檔案內容按照空格拆分單詞,拆分形成FlatMappedRDD

l  其次使用map(x=>(x(1),1))對上步驟拆分的單詞形成(單詞,1)資料對,此時生成的MappedRDD,最後使用reduceByKey()方法對單詞的頻度統計,由此生成ShuffledRDD,並由collect執行作業得出結果。

 

clip_image045

clip_image047

clip_image049

第二步   按照匹配模式讀取計算單詞個數

val rdd2 = sc.textFile("hdfs://hadoop1:9000/class3/directory/*.txt")

rdd2.flatMap(_.split(" ")).map(x=>(x,1)).reduceByKey(_+_).collect

第三步   讀取gz壓縮檔案計算單詞個數

val rdd3 = sc.textFile("hdfs://hadoop1:8000/class2/test.txt.gz")

rdd3.flatMap(_.split(" ")).map(x=>(x,1)).reduceByKey(_+_).collect

3.3.4 搜狗日誌查詢例子演示

搜狗日誌資料可以從http://download.labs.sogou.com/dl/q.html 下載,其中完整版大概2GB左右,檔案中欄位分別為:訪問時間\t使用者ID\t[查詢詞]\t該URL在返回結果中的排名\t使用者點選的順序號\t使用者點選的URL。其中SogouQ1.txt、SogouQ2.txt、SogouQ3.txt分別是用head -n 或者tail -n 從SogouQ資料日誌檔案中擷取,分別包含100萬,200萬和1000萬筆資料,這些測試資料也放在該系列配套資源的data\sogou目錄下。

第一步   上傳測試資料

搜狗日誌資料放在data\sogou下,把該目錄下的SogouQ1.txt、SogouQ2.txt和SogouQ3.txt解壓,然後通過下面的命令上傳到HDFS中/sogou目錄中

cd /home/hadoop/upload/

ll sogou

tar -zxf *.gz

hadoop fs -mkdir /sogou

hadoop fs -put sogou/SogouQ1.txt /sogou

hadoop fs -put sogou/SogouQ2.txt /sogou

hadoop fs -put sogou/SogouQ3.txt /sogou

hadoop fs -ls /sogou

clip_image019[1]

第二步   查詢搜尋結果排名第1點選次序排在第2的資料

val rdd1 = sc.textFile("hdfs://hadoop1:9000/sogou/SogouQ1.txt")

val rdd2=rdd1.map(_.split("\t")).filter(_.length==6)

rdd2.count()

val rdd3=rdd2.filter(_(3).toInt==1).filter(_(4).toInt==2)

rdd3.count()

rdd3.toDebugString

該命令執行的過程如下:

l  首先使用textFile()讀入SogouQ1.txt檔案,讀入後由HadoopRDD轉變為MadppedRDD;

l  然後通過rdd1.map(_.split("\t"))對讀入資料使用\t分隔符進行拆分,拆分後RDD型別不變即為MadppedRDD,對這些拆分後的資料使用filter(_.length==6)過濾每行為6個欄位的資料,這時資料變為FilteredRDD;

clip_image051

l  執行rdd2.count()啟動對rdd2計數的作業,通過執行結果可以看到該資料集為100條;

clip_image053

l  rdd2.filter(_(3).toInt==1).filter(_(4).toInt==2)表示對rdd2的資料的第4個欄位搜尋結果排名第一,第5個欄位點選次序排在第二的資料進行過濾,通過count()方法執行作業得出最終的結果;

clip_image055

使用toDebugString可以檢視rdd3的RDD詳細變換過程,如下圖所示:

clip_image057

第三步   Session查詢次數排行榜並把結果儲存在HDFS中

val rdd4 = rdd2.map(x=>(x(1),1)).reduceByKey(_+_).map(x=>(x._2,x._1)). sortByKey(false).map(x=>(x._2,x._1))

rdd4.toDebugString

rdd4.saveAsTextFile("hdfs://hadoop1:9000/class3/output1")

該命令執行的過程如下:

l  rdd4的生成比較複雜,我們分步驟進行解析,軸線map(x=>(x(1),1))是獲取每行的第二個欄位(使用者Session)計數為1,然後reduceByKey(_+_)是安排Key進行累和,即按照使用者Session號進行計數求查詢次數,其次map(x=>(x._2,x._1))是把Key和Value位置互換,為後面排序提供條件,使用sortByKey(false)對資料進行按Key值進行倒排,此時需要注意的是Key為查詢次數,最後通過map(x=>(x._2,x._1)再次交換Key和Value位置,得到了(使用者Session號,查詢次數)結果。該過程RDD的變化如下圖所示:

clip_image059

l  計算的結果通過如下命令可以檢視到,可以看到由於輸入資料存放在2個節點上,所以結果也分為兩個檔案

hadoop fs -ls /class3/output1

clip_image061

這是使用HDFS的getmerge合併這兩個檔案並進行檢視

$cd /app/hadoop/hadoop-2.2.0/bin

$hdfs dfs -getmerge hdfs://hadoop1:9000/class3/output1 /home/hadoop/upload/result

$cd /home/hadoop/upload/

$head result

clip_image063

相關文章