1 RDD的數據結構模型

1 RDD的數據結構模型

前言:自Google發表三大論文GFS、MapReduce、BigTable以來,衍生出的開源框架越來越多,其中Hadoop更是以高可用、高擴展、高容錯等特性形成了開源工業界事實標準。Hadoop是一個可以搭建在廉價PC上的分佈式集羣生態體系,用戶可以在不清楚底層運行細節的情況下,開發出自己的分佈式應用。但是Hadoop MapReduce由於其設計初衷並不是爲了滿足循環式數據流處理,因此在多並行運行的數據可複用場景(如:迭代式機器學習、交互式數據處理)中存在諸多的問題,所以Spark應運而生。Spark在傳統的Mapreduce計算框架的基礎上,將計算單元縮小到更適合並行計算和重複使用的RDD。

一、 什麼是RDD?
二、 RDD是怎樣形成的?
三、 RDD結構拆分及源碼解讀

一、 什麼是RDD?
RDD(Resilient Distributed Datasets),是Spark最爲核心的概念。
從字面上,直譯爲彈性分佈式數據集。所謂“彈性”,一種簡單解釋是指RDD是橫向多分區的,縱向當計算過程中內存不足時可刷寫到磁盤等外存上,可與外存做靈活的數據交換;而另一種個人更偏向的解釋是RDD是由虛擬數據結構組成,並不包含真實數據本體,RDD使用了一種“血統”的容錯機制,在結構更新和丟失後可隨時根據血統進行數據模型的重建。所謂“分佈式”,就是可以分佈在多臺機器上進行並行計算。
從空間結構上,可以理解爲是一組只讀的、可分區的分佈式數據集合,該集合內包含了多個分區。分區就是依照特定規則,將具有相同屬性的數據記錄放在一起。每個分區相當於一個數據集片段。下圖簡單表示了一個RDD的結構:

在這裏插入圖片描述
RDD是一個只讀的有屬性的數據集。屬性用來描述當前數據集的狀態,數據集是由數據的分區(partition)組成,並(由block)映射成真實數據。RDD屬性包括名稱、分區類型、父RDD指針、數據本地化、數據依賴關係等,主要屬性可以分爲3類:

與其他RDD 的關係(parents)
數據(partitioner,checkpoint,storagelevel,iterator)
RDD自身屬性(rddname,sparkcontext,sparkconf)
之後RDD源碼解析裏詳細介紹每個屬性。
二、 RDD是怎樣形成的?
有需要在Spark內計算的數據即形成RDD,所以開始輸入到Spark的數據和經過Spark算子(下文介紹算子)計算過的數據都會形成RDD,包括即將輸出的數據也會生成RDD後統一輸出的。如圖
在這裏插入圖片描述
關於RDD的形成, 主要是通過連接物理存儲輸入的數據集和在已有RDD基礎上進行相關計算操作衍生的。下面我們就通過一個大數據開源生態經典的例子(Wordcount)來描述下RDD的產生過程。強大的Scala代碼如下。

在這裏插入圖片描述
初識的小夥伴們會感覺很神奇,四行代碼就全部搞定了嗎,之前的MR代碼可是碼了一大堆呢……的確如此想學好Spark的小夥伴們,還是要掌握Scala這門語言,廢話不多說,簡單解釋下這幾行代碼:
第一行,從HDFS上讀取in.txt文件,創建了第一個RDD
第二行,按空格分詞,扁平化處理,生成第二個RDD,每個詞計數爲1,生成了第三個RDD。這裏可能有人會問,爲什麼生成了兩個RDD呢,因爲此行代碼RDD經過了兩次算子轉換(transformation)操作。關於算子這裏不多詳述,請關注下期文章。
第三行,按每個詞分組,累加求和,生成第四個RDD
第四行,將Wordcount統計結果輸出到HDFS
整個產生過程如下圖所示:

在這裏插入圖片描述
RDD的依賴關係
通過上文的例子可以瞭解到,一個作業從開始到結束的計算過程中產生了多個RDD,RDD之間是彼此相互依賴的,我們把這種父子依賴的關係,稱之爲“血統”。如果父RDD的每個分區最多隻能被子RDD的一個分區使用,我們稱之爲(narrow dependency)窄依賴;若一個父RDD的每個分區可以被子RDD的多個分區使用,我們稱之爲(wide dependency)寬依賴。簡單來講窄依賴就是父子RDD分區間”一對一“的關係,寬依賴就是”一對多“關係,具體理解可參考下圖:

在這裏插入圖片描述
那麼爲什麼Spark要將依賴分成這兩種呢,下面我們就瞭解下原因:
首先,從計算過程來看,窄依賴是數據以管道方式經一系列計算操作可以運行在了一個集羣節點上,如(map、filter等),寬依賴則可能需要將數據通過跨節點傳遞後運行(如groupByKey),有點類似於MR的shuffle過程。
其次,從失敗恢復來看,窄依賴的失敗恢復起來更高效,因爲它只需找到父RDD的一個對應分區即可,而且可以在不同節點上並行計算做恢復;寬依賴則牽涉到父RDD的多個分區,恢復起來相對複雜些。
綜上, 這裏引入了一個新的概念Stage。Stage可以簡單理解爲是由一組RDD組成的可進行優化的執行計劃。如果RDD的衍生關係都是窄依賴,則可放在同一個Stage中運行,若RDD的依賴關係爲寬依賴,則要劃分到不同的Stage。這樣Spark在執行作業時,會按照Stage的劃分, 生成一個完整的最優的執行計劃。下面引用一張比較流行的圖片輔助大家理解Stage,如圖RDD¬-A到RDD-B和RDD-F到RDD-G均屬於寬依賴,所以與前面的父RDD劃分到了不同的Stage中。

在這裏插入圖片描述
三、 RDD結構拆分及源碼解讀
到這裏,相信大家已經對RDD有了大體的瞭解,但要詳細瞭解RDD的內部結構,請繼續耐心往下看。先貼一張RDD的內部結構圖
在這裏插入圖片描述

RDD 的屬性主要包括(rddname、sparkcontext、sparkconf、parent、dependency、partitioner、checkpoint、storageLevel),下面我們先簡單逐一瞭解下:

1.rddname
即rdd的名稱
2.sparkcontext
SparkContext爲Spark job的入口,由Spark driver創建在client端,包括集羣連接,RddID,創建抽樣,累加器,廣播變量等信息。
3.sparkconf配置信息,即sc.conf
Spark參數配置信息
提供三個位置用來配置系統:
Spark api:控制大部分的應用程序參數,可以用SparkConf對象或者Java系統屬性設置
環境變量:可以通過每個節點的conf/spark-env.sh腳本設置。例如IP地址、端口等信息
日誌配置:可以通過log4j.properties配置

4.parent
指向依賴父RDD的partition id,利用dependencies方法可以查找該RDD所依賴的partiton id的List集合,即上圖中的parents。
5.iterator
迭代器,用來查找當前RDD Partition與父RDD中Partition的血緣關係。並通過StorageLevel確定迭代位置,直到確定真實數據的位置。迭代方式分爲checkpoint迭代和RDD迭代, 如果StorageLevel爲NONE則執行computeOrReadCheckpoint計算並獲取數據,此方法也是一個迭代器,迭代checkpoint數據存放位置,迭代出口爲找到真實數據或內存。如果Storagelevel不爲空,根據存儲級別進入RDD迭代器,繼續迭代父RDD的結構,迭代出口爲真實數據或內存。迭代器內部有數據本地化判斷,先從本地獲取數據,如果沒有則遠程查找。
6.prisist
rdd存儲的level,即通過storagelevel和是否可覆蓋判斷,
storagelevel分爲 5中狀態 ,useDisk, useMemory, useOffHeap, deserialized, replication 可組合使用。

if (useOffHeap) { 
require(!useDisk, "Off-heap storage level does not support using disk") 
require(!useMemory, "Off-heap storage level does not support using heap memory") require(!deserialized, "Off-heap storage level does not support deserialized storage") require(replication == 1, "Off-heap storage level does not support multiple replication") }
persist :
private def persist(newLevel: StorageLevel, allowOverride: Boolean): this.type = {
    // TODO: Handle changes of StorageLevel
    if (storageLevel != StorageLevel.NONE && newLevel != storageLevel && !allowOverride) {
      throw new UnsupportedOperationException(
        "Cannot change storage level of an RDD after it was already assigned a level")
    }
    // If this is the first time this RDD is marked for persisting, register it
    // with the SparkContext for cleanups and accounting. Do this only once.
    if (storageLevel == StorageLevel.NONE) {
      sc.cleaner.foreach(_.registerRDDForCleanup(this))
      sc.persistRDD(this)
    }
    storageLevel = newLevel
    this
  }```

7.partitioner 分區方式
RDD的分區方式。RDD的分區方式主要包含兩種(Hash和Range),這兩種分區類型都是針對K-V類型的數據。如是非K-V類型,則分區爲None。 Hash是以key作爲分區條件的散列分佈,分區數據不連續,極端情況也可能散列到少數幾個分區上,導致數據不均等;Range按Key的排序平衡分佈,分區內數據連續,大小也相對均等。
8.checkpoint
Spark提供的一種緩存機制,當需要計算的RDD過多時,爲了避免重新計算之前的RDD,可以對RDD做checkpoint處理,檢查RDD是否被物化或計算,並將結果持久化到磁盤或HDFS。與spark提供的另一種緩存機制cache相比, cache緩存數據由executor管理,當executor消失了,被cache的數據將被清除,RDD重新計算,而checkpoint將數據保存到磁盤或HDFS,job可以從checkpoint點繼續計算。

def checkpoint(): Unit = RDDCheckpointData.synchronized {
 // NOTE: we use a global lock here due to complexities downstream with ensuring 
// children RDD partitions point to the correct parent partitions. In the future 
// we should revisit this consideration.
 if (context.checkpointDir.isEmpty) { 
throw new SparkException("Checkpoint directory has not been set in the SparkContext") 
} else if (checkpointData.isEmpty) {
    checkpointData = Some(new ReliableRDDCheckpointData(this))
 } 
}

9.storageLevel
一個枚舉類型,用來記錄RDD的存儲級別。存儲介質主要包括內存、磁盤和堆外內存,另外還包含是否序列化操作以及副本數量。如:MEMORY_AND_DISK_SER代表數據可以存儲在內存和磁盤,並且以序列化的方式存儲。是判斷數據是否保存磁盤或者內存的條件。
storagelevel結構:

 class StorageLevel private(
    private var _useDisk: Boolean,
    private var _useMemory: Boolean,
    private var _useOffHeap: Boolean,
    private var _deserialized: Boolean,
    private var _replication: Int = 1)

綜上所述Spark RDD和Spark RDD算子組成了計算的基本單位,並由數據流向的依賴關係形成非循環數據流模型(DAG),形成Spark基礎計算框架。

鏈接:https://www.jianshu.com/p/dd7c7243e7f9
來源:簡書

2 RDD算子詳解

前方高能減速慢行!

在上一篇RDD結構已經介紹完了。雖然RDD結構是spark設計思想最重要的組成,但是沒有輔助的功能只有結構又不能獨立使用。真正使RDD完成計算優化的,就是今天我們要講到的spark RDD的另一個重要組成部分RDD算子。
一、RDD算子的定義
我給RDD算子的定義是:用來生成或處理RDD的方法叫做RDD算子。RDD算子就是一些方法,在Spark框架中起到運算符的作用。算子用來構建RDD及數據之間的關係。數據可以由算子轉換成RDD,也可以由RDD產生新RDD,或者將RDD持久化到磁盤或內存。
從技術角度講RDD算子可能比較枯燥,我們舉個裏生活學習中的例子來類比RDD算子的作用。
完成計算需要什麼呢?
需要數據載體和運算方式。數據載體可以是數字,數組,集合,分區,矩陣等。一個普通的計算器,它的運算單位是數字,而運算符號是加減乘除,這樣就可以得到結果並輸出了。一個矩陣通過加減乘除也可以得到結果,但是結果跟計算器的加減乘除一樣嗎?非也!

在這裏插入圖片描述矩陣相乘

在這裏插入圖片描述AB矩陣運算規則

所以說加減乘除在不同的計算框架作用是不同的,而加減乘除這樣的符號就是運算方式。在spark計算框架有自己的運算單位(RDD)和自己的運算符(RDD算子)。
是不是很抽象?下面來點具體的。
二、RDD算子的使用
Spark算子非常豐富,有幾十個,開發者把算子組合使用,從一個基礎的RDD計算出想要的結果。並且算子是優化Spark計算框架的主要依據。
我們以top算子舉例,rdd.top(n)獲取RDD的前n個排序後的結果。
例如計算:文件a的2倍與文件b的TOP 3結果。

算子的計算算子的計算

窄依賴優化:如圖中的RDD1,2,3在Stage3中被優化爲RDD1到RDD3直接計算。是否可以直接計算是由算子的寬窄依賴決定,推薦使用數據流向區分寬窄依賴: partiton流向子RDD的多個partiton屬於寬依賴,父RDD的partiton流向子RDD一個partiton或多個partiton流向一個子RDD的partiton屬於窄依賴。上圖中的RDD3和RDD4做top(3)操作,top是先排序後取出前3個值,排序過程屬於寬依賴,spark計算過程是逆向的DAG(DAG和拓撲排序下一篇介紹),RDD5不能直接計算,必須等待依賴的RDD完成計算,我把這種算子叫做不可優化算子(計算流程不可優化,必須等待父RDD的完成),Action算子(後文講解)都是不可優化算子,Transformation算子也有很多不可優化的算子(寬依賴算子),如:groupbykey,reducebykey,cogroup,join等。
數據量優化:上圖中的a文件數據乘2,爲什麼前面有一個filter,假設filter過濾後的數據減少到三分之一,那麼對後續RDD和shuffle的操作優化可想而知。而這只是提供一個思路,並不是說有的過濾都是高效的。
利用存儲算子優化Lineage:RDD算子中除了save(輸出結果)算子之外,還有幾個比較特別的算子,用來保存中間結果的,如:persist,cache 和 checkpoint ,當RDD的數據保持不變並被複用多次的時候可以用它們臨時保存計算結果。
1). cache和persist
修改當前RDD的存儲方案StorageLevel,默認狀態下與persist級別是一樣的MEMORY_ONLY級別,保存到內存,內存不足選擇磁盤。
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
def cache(): this.type = persist()
這2個方法都不會觸發任務,只是修改了RDD的存儲方案,當RDD被執行的時候按照方案存儲到相應位置。而checkpoint會單獨執行一個job,並把數據寫入磁盤。
注:不要把RDD cache和Dataframe cache混淆。Dataframe cache將在spark sql中介紹。
2).checkpoint
檢查RDD是否被物化或計算,一般在程序運行比較長或者計算量大的情況下,需要進行Checkpoint。這樣可以避免在運行中出現異常導致RDD回溯代價過大的問題。Checkpoint會把數據寫在本地磁盤上。Checkpoint的數據可以被同一session的多個job共用。

三、RDD算子之間的關係
算子從否觸發job的角度劃分,可以分爲Transformation算子和Action算子,Transformation算子不會產生job,是惰性算子,只記錄該算子產生的RDD及父RDD的partiton之間的關係,而Action算子將觸發job,完成依賴關係的所有計算操作。
那麼如果一個程序裏有多個action算子怎麼辦?
順序完成action操作,每個action算子產生一個job,上一job的結果轉換成RDD,繼續給後續的action使用。多數action返回結果都不是RDD,而transformation算子的返回結果都是RDD,但可能是多個RDD(如:randomSplit,將一個RDD切分成多個RDD)。

一張圖瞭解所有RDD算子之間的關係
在這裏插入圖片描述
算子的關係圖

上圖劃分爲4個大塊,從上到下我們順序講起:

圖中的RDD dependency正是RDD結構中的private var deps: Seq[Dependency[_]],dependency類被兩個類繼承,NarrowDependency(窄依賴)和ShuffleDependency(寬依賴)。窄依賴又分onetoonedependency和rangedependency,這是窄依賴提供的2種抽樣方式1對1數據抽樣和平衡數據抽樣,返回值都是一個partitonid的list集合。
第二層,是提供RDD底層計算的基本算法,繼承了RDD,並實現了dependency的一種或多種依賴關係的計算邏輯,並互相調用實現更復雜的功能。
最下層是Spark API,利用RDD基本的計算實現RDD所有的算子,並調用多個底層RDD算子實現複雜的功能。
右邊的泛型,是scala的一種類型,可以理解爲類的泛型,泛指編譯時被抽象的類型。Spark利用scala的這一特性把依賴關係抽象成一種泛型結構,並不需要真實的數據類型參與編譯過程。編譯的結構類由序列化和反序列化到集羣的計算節點取數並計算。
Transformation:轉換算子,這類轉換並不觸發提交作業,完成作業中間過程處理。Transformation按照數據類型又分爲兩種,value數據類型算子和key-value數據類型算子。
1) Value數據類型的Transformation算子
Map,flatMap,mapPartitions,glom,union,cartesian,groupBy,filter,distinct,subtract,sample,takeSample
2)Key-Value數據類型的Transfromation算子
mapValues,combineByKey,reduceByKey,partitionBy,cogroup,join,leftOuterJoin和rightOuterJoin
Action: 行動算子,這類算子會觸發SparkContext提交Job作業。Action算子是用來整合和輸出數據的,主要包括以下幾種:
Foreach,HDFS,saveAsTextFile,saveAsObjectFile, collect,collectAsMap,reduceByKeyLocally,lookup,count,top,reduce,fold,aggregate
注:上述舉例算子可能不全,隨着spark的更新也會不斷有新的算子加入其中。

鏈接:https://www.jianshu.com/p/97d65ce90cbb

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