Spark的編程核心RDD的實現(xiàn)詳解

一.什么是RDD

RDD是彈性分布式數(shù)據(jù)集(Resilient Distributed Dataset),RDD是只讀的、 分區(qū)記錄的集合。 RDD只能基于在穩(wěn)定物理存儲中的數(shù)據(jù)集和其他已有的RDD上執(zhí)行確定性操作來創(chuàng)建。 這些確定性操作稱為轉(zhuǎn)換, 如map、 filter、 groupBy、 join。RDD含有如何從其他RDD衍生(即計算)出本RDD的相關(guān)信息(即Lineage) , 因此在RDD部分分區(qū)數(shù)據(jù)丟失的時候可以從物理存儲的數(shù)據(jù)計算出相應(yīng)的RDD分區(qū)。

每個RDD有5個主要的特點:
1.RDD由一系列Partition組成
對于RDD來說, 每個分區(qū)都會被一個計算任務(wù)處理, 并決定并行計算的粒度。 用戶可以在創(chuàng)建RDD時指定RDD的分區(qū)個數(shù), 如果沒有指定, 那么就會采用默認值。 默認值就是程序所分配到的CPU Core的數(shù)目。在分區(qū)存儲的計算模型中,每個分配的存儲是由BlockManager實現(xiàn)的。 每個分區(qū)都會被邏輯映射成BlockManager的一個Block, 而這個Block會被一個Task負責(zé)計算。

2.算子函數(shù)是作用在Partition上的
Spark中RDD的計算是以Partition為單位的, 每個RDD都會實現(xiàn)compute函數(shù)以達到這個目的。compute函數(shù)會對迭代器進行復(fù)合, 不需要保存每次計算的結(jié)果。(即獲取父RDD的迭代器,然后將自定義的函數(shù)作用在該迭代器迭代出的每一條數(shù)據(jù))。

3.RDD之間有依賴關(guān)系
RDD的每次轉(zhuǎn)換都會生成一個新的RDD, 所以RDD之間就會形成類似于流水線一樣的前后依賴關(guān)系。 在部分分區(qū)數(shù)據(jù)丟失時, Spark可以通過這個依賴關(guān)系重新計算丟失的分區(qū)數(shù)據(jù), 而不是對RDD的所有分區(qū)進行重新計算。

4.分區(qū)器作用在K-V 格式RDD上
當(dāng)前Spark中實現(xiàn)了兩種類型的分片函數(shù), 一個是基于哈希的HashPartitioner, 另外一個是基于范圍的RangePartitioner。 只有對于key-value的RDD, 才會有Partitioner, 非key-value的RDD的Parititioner的值是None。Partitioner函數(shù)不但決定了RDD本身的分片數(shù)量, 也決定了parent RDD Shuffle輸出時的分片數(shù)量。

5.每個Partition對外提供最佳計算位置(preferred location)
一個列表, 存儲存取每個Partition的優(yōu)先位置(preferred location) 。 對于一個HDFS文件來說, 這個列表保存的就是每個Partition所在的塊的位置。 按照“計算向數(shù)據(jù)移動”的理念, Spark在進行任務(wù)調(diào)度的時候, 會盡可能地將計算任務(wù)分配到
其所要處理數(shù)據(jù)塊的存儲位置,(數(shù)據(jù)本地化級別)。

二.如何創(chuàng)建RDD

可通過以下幾種方式創(chuàng)建RDD:

  • 通過讀取外部數(shù)據(jù)集 (本地文件系統(tǒng)/HDFS/Cassandra/HBase/...)
  • 通過一個已經(jīng)存在的Scala集合創(chuàng)建(List/Set/...)
  • 通過已有的RDD生成新的RDD

三.Spark對RDD操作方式

Spark對RDD的算子分為三種,即轉(zhuǎn)換算子(Transformation)和行動算子(Action)和控制算子。

1.轉(zhuǎn)換算子
不觸發(fā)實際計算,從現(xiàn)有的數(shù)據(jù)集創(chuàng)建一個新的數(shù)據(jù)集,返回一個新的RDD,例如對數(shù)據(jù)的匹配操作map和過濾操作filter,惰性求值。

2.動作算子
會觸發(fā)實際計算,即在數(shù)據(jù)集上進行計算后,會向Driver程序驅(qū)動器返回結(jié)果或?qū)⒔Y(jié)果寫到外部系統(tǒng)。

如何區(qū)別兩種算子?
看返回值類型,返回RDD類型的為轉(zhuǎn)換操作,返回其他數(shù)據(jù)類型的是行動操作。

3.控制算子
如persist,cache和checkpoint這三種算子,可以用來做緩存或者持久化,復(fù)用RDD時避免重復(fù)計算,或者在應(yīng)用崩潰時恢復(fù)。

惰性求值?
RDD中的所有轉(zhuǎn)換都是惰性的, 也就是說, 它們并不會直接計算結(jié)果。 相反的, 它們只是記住這些應(yīng)用到最原始數(shù)據(jù)集上的轉(zhuǎn)換操作。 只有當(dāng)調(diào)用行動算子(Action)返回結(jié)果給Driver的動作時, 這些轉(zhuǎn)換才會真正運行。 這個設(shè)計讓Spark更加有效率地運行。

為何會有惰性求值?
如果每經(jīng)過一次轉(zhuǎn)換操作都觸發(fā)計算,將會有系統(tǒng)負擔(dān),而惰性求值會將多個轉(zhuǎn)換操作合并到一起,抵消不必要的步驟后,在最后必要的時才進行運算,獲得性能的提升同時又減輕系統(tǒng)運算負擔(dān)。

Transformation操作

函數(shù)名 目的 示例 結(jié)果
map(f) 將函數(shù)應(yīng)用于每一個元素中,返回值構(gòu)成新的RDD rdd.map(x=>x+1) {2,3,4,4}
flatMap(f) 將函數(shù)應(yīng)用于每一個元素中,并把元素中迭代器內(nèi)所有內(nèi)容一并生成新的RDD,常用于切分單詞 rdd.flatMap(x=>x.to(3)) {1,2,3,,2,3,3,3}
filter(f) 過濾元素 rdd.filter(x=>x!=1) {2,3,3}
distinct() 元素去重 rdd.distinct() {1,2,3}
sample( withReplacement, fraction , [seed] ) 元素采樣,以及是否需要替換 rdd.sample(false,0.5) 不確定值,不確定數(shù)目
union(rdd) 合并兩個RDD所有元素(不去重) rdd1.union(rdd2) {1,2,3,3,4,5}
intersection(rdd) 求兩個RDD的交集 rdd1.intersection(rdd2) {3}
substract(rdd) 移除在RDD2中存在的RDD1元素 rdd1.substract(rdd2) {1,2}
cartesian(rdd) 求兩個RDD的笛卡爾積 rdd1.cartesian(rdd2) {(1,3),(1,4),(1,5)...(3,5)}

Action操作

函數(shù)名 目的 示例 結(jié)果
collect() 收集并返回RDD中所有元素 rdd.collect() {1,2,3,3}
count() RDD中元素的個數(shù) rdd.count() 4
countByValue() 各元素出現(xiàn)的個數(shù) rdd.countByValue() {(1,1),(2,1),(3,2)}
take(num) 從RDD中返回num個元素 rdd.take(2) {1,2}
top(num) 返回最前面的num個元素 rdd.take(2) {3,3}
takeOrdered(num,[ordering]) 按提供的順序返回前num個元素 rdd.takeOrdered(2,[myOrdering]) {3,3}
takeSample(withReplacement, num ,[seed]) 返回任意元素 takeSample(false,1) 不確定值
reduce(f) 并行整合RDD中所有元素,返回一個同一類型元素 rdd.reduce((x,y) => x+y ) 9
fold(zeroValue)(f) 與reduce一樣,不過需要提供初始值 rdd.fold(0)((x,y) => x+y ) 9
aggregate(zeroValue)(seqOp , combOp) 與reduce相似,不過返回不同類型的元素 rdd. aggregate(( 0, 0)) ((x, y) => (x._1 + y, x._2 + 1), (x, y) => (x._1 + y._1, x._2 + y._2)) {9,4}
foreach(f) 給每個元素使用給定的函數(shù),結(jié)果不需發(fā)回本地 rdd.foreach(f)

四.RDD的持久化(緩存)

Spark速度非常快的原因之一, 就是在不同操作中在內(nèi)存中持久化(或緩存) 一個數(shù)據(jù)集。 當(dāng)持久化一個RDD后, 每一個節(jié)點
都將把計算的分片結(jié)果保存在內(nèi)存中, 并在對此數(shù)據(jù)集(或者衍生出的數(shù)據(jù)集) 進行的其他動作(action) 中重用。 這使得后
續(xù)的動作變得更加迅速(通???0倍) 。 RDD相關(guān)的持久化和緩存, 是Spark最重要的特征之一。 可以說, 緩存是Spark構(gòu)建迭代式算法和快速交互式查詢的關(guān)鍵。

出于不同目的和場景需求,我們可選擇的持久化級別有:

級別 使用空間 CPU時間 是否在內(nèi)存中 是否在磁盤上
MEMORY_ONLY
MEMORY_ONLY_SER
MEMORY_AND_DISK 部分 部分
MEMORY_AND_DISK_SER 部分 部分
DISK_ONLY

我們可以通過persist() 或cache() 方法可以標(biāo)記一個要被持久化的RDD, 一旦首次被觸發(fā), 該RDD將會被保留在計算節(jié)點的內(nèi)存中并重用。

persist的源碼實現(xiàn)如下

 /**
   * Set this RDD's storage level to persist its values across operations after the first time
   * it is computed. This can only be used to assign a new storage level if the RDD does not
   * have a storage level set yet. Local checkpointing is an exception.
   */
  def persist(newLevel: StorageLevel): this.type = {
    if (isLocallyCheckpointed) {
      // This means the user previously called localCheckpoint(), which should have already
      // marked this RDD for persisting. Here we should override the old storage level with
      // one that is explicitly requested by the user (after adapting it to use disk).
      persist(LocalRDDCheckpointData.transformStorageLevel(newLevel), allowOverride = true)
    } else {
      persist(newLevel, allowOverride = false)
    }
  }

代碼示例

scala> val rdd = sc.parallelize(1 to 5)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[5] at parallelize at <console>:24

scala> val rdd1 = rdd.map(_+5)
rdd1: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[6] at map at <console>:26

scala> rdd1 = rdd1.persist(StorageLevel.MEMORY_ONLY)
res2: rdd1.type = MapPartitionsRDD[6] at map at <console>:26

scala> rdd1.reduce(_+_)
res3: Int = 40

scala> rdd1.count()
res4: Long = 5

scala> rdd1.first()
res5: Int = 6

scala> rdd1.unpersist()
res7: rdd1.type = MapPartitionsRDD[6] at map at <console>:26

如果要緩存的數(shù)據(jù)太多,內(nèi)存放不下,Spark會自動使用LRU(最近最小使用)的緩存策略把最老的分區(qū)從內(nèi)存中移除。同時緩存有可能丟失,RDD的緩存的容錯機制保證了即使緩存丟失也能保證計算的正確執(zhí)行。 通過基于RDD的一系列的轉(zhuǎn)換, 丟失的數(shù)據(jù)會被重算。 RDD的各個Partition是相對獨立的, 因此只需要計算丟失的部分即可, 并不需要重算全部Partition。

最后,可調(diào)用rdd.unpersist()方法手動移除RDD緩存。

五.RDD之間的依賴關(guān)系

Spark會根據(jù)用戶提交的計算邏輯中的RDD的轉(zhuǎn)換和動作來生成RDD之間的依賴關(guān)系, RDD之間的關(guān)系可以從兩個維度來理解:

  • RDD是從哪些RDD轉(zhuǎn)換而來, 也就是RDD的parent RDD是什么
  • RDD中的Partition,依賴于parent RDD中 的哪些Partition

根據(jù)依賴于parentRDD的Partitions的不同情況, Spark將這種依賴分為兩種, 一種是寬依賴, 一種是窄依賴。

  • 窄依賴指的是子RDD依賴于父RDD中固定的Partitions
  • 寬依賴指的是子RDD對父RDD中的所有partition都有依賴,或者說依賴于父RDD的數(shù)量不能明確
窄依賴和寬依賴

從上面的圖中我們可以理解下這兩種依賴關(guān)系之間的區(qū)別

對于map和filter形式的轉(zhuǎn)換來說, 它們只是將Partition的數(shù)據(jù)根據(jù)轉(zhuǎn)換的規(guī)則進行轉(zhuǎn)化, 并不涉及其他的處理, 可以簡單地認為
只是將數(shù)據(jù)從一個形式轉(zhuǎn)換到另一個形式。

對于union, 只是將多個RDD合并成一個,parent RDD的Partition不會有任何的變化, 可以認為只是把parent RDD的Partition 簡單進行復(fù)制與合并。

對于join, 如果每個Partition僅僅和已知的、 特定的Partition進行join, 那么這個依賴關(guān)系也是窄依賴。 對于這種有規(guī)則的數(shù)據(jù)的join, 并不會引入昂貴的Shuffle。 對于窄依賴, 由于RDD每個Partition依賴固定數(shù)量的parent RDD的Partition, 因此可以通過一個計算任務(wù)來處理這些Partition, 并且這些Partition相互獨立, 這些計算任務(wù)也就可以并行執(zhí)行了。

對于groupByKey, 子RDD的所有Partition會依賴于parent RDD的所有Partition, 子RDD的Partition是parent RDD的所有Partition Shuffle的結(jié)果, 因此這兩個RDD是不能通過一個計算任務(wù)來完成的。 同樣, 對于需要parent RDD的所有Partition進行join的轉(zhuǎn)換, 也是需要Shuffle, 這類join的依賴就是寬依賴而不是前面提到的窄依賴了。

五.RDD依賴關(guān)系的具體代碼實現(xiàn)

RDD的依賴關(guān)系繼承圖

Spark中對應(yīng)窄依賴的的抽象類為NarrowDependency,具體實現(xiàn)有兩種。

一種是一對一的依賴, 即OneToOneDependency:

class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
     override def getParents(partitionId: Int) = List(partitionId)
}

通過OneToOneDependency的源碼中的getParents的實現(xiàn)不難看出, RDD僅僅依賴于parent RDD相同ID的Partition。

還有一個是范圍的依賴, 即RangeDependency:
它僅僅被UnionRDD使用,UnionRDD是把多個RDD合成一個RDD,這些RDD是被拼接而成, 即每個parent RDD的Partition的相對順序不會變, 只不過每個parent RDD在UnionRDD中的Partition的起始位置不同。 因此它的getPartents如下:

class RangeDependency[T](rdd: RDD[T], inStart: Int, outStart: Int, length: Int)
  extends NarrowDependency[T](rdd) {

  override def getParents(partitionId: Int): List[Int] = {
    // inStart是parent RDD中Partition的起始位置
    // outStart是在UnionRDD中的起始位置
    // length就是parent RDD中Partition的數(shù)量。
    if (partitionId >= outStart && partitionId < outStart + length) {
      List(partitionId - outStart + inStart)
    } else {
      Nil
    }
  }
}

寬依賴的實現(xiàn)只有一種,ShuffleDependency的實現(xiàn)相對前面幾種較為復(fù)雜,會在后續(xù)的文章中詳細講解...

class ShuffleDependency[K: ClassTag, V: ClassTag, C: ClassTag](
    @transient private val _rdd: RDD[_ <: Product2[K, V]],
    val partitioner: Partitioner,
    val serializer: Serializer = SparkEnv.get.serializer,
    val keyOrdering: Option[Ordering[K]] = None,
    val aggregator: Option[Aggregator[K, V, C]] = None,
    val mapSideCombine: Boolean = false)
  extends Dependency[Product2[K, V]] {

  override def rdd: RDD[Product2[K, V]] = _rdd.asInstanceOf[RDD[Product2[K, V]]]

  private[spark] val keyClassName: String = reflect.classTag[K].runtimeClass.getName
  private[spark] val valueClassName: String = reflect.classTag[V].runtimeClass.getName
  // Note: It's possible that the combiner class tag is null, if the combineByKey
  // methods in PairRDDFunctions are used instead of combineByKeyWithClassTag.
  private[spark] val combinerClassName: Option[String] =
    Option(reflect.classTag[C]).map(_.runtimeClass.getName)

  val shuffleId: Int = _rdd.context.newShuffleId()

  val shuffleHandle: ShuffleHandle = _rdd.context.env.shuffleManager.registerShuffle(
    shuffleId, _rdd.partitions.length, this)

  _rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(this))
}

六.區(qū)分兩種依賴的作用

1.劃分 Stage
根據(jù)RDD之間的依賴關(guān)系將DAG圖劃分為不同的階段Stage( Stage之間的依賴關(guān)系可以認為就是Lineage)。

對于窄依賴,由于partition依賴關(guān)系的確定性,partition的轉(zhuǎn)換處理就可以在同一個線程里完成,窄依賴就被spark劃分到同一個stage中。

而對于寬依賴,只能等父RDD shuffle處理完成后,下一個stage才能開始接下來的計算,因此寬依賴要單獨劃分一個Stage。

Stage 之間做 shuffle,Stage 之內(nèi)做 pipeline(流水線)。方便stage內(nèi)優(yōu)化。

2.解決數(shù)據(jù)容錯的高效性

假如某個節(jié)點出故障了,窄依賴只需重新計算丟失RDD分區(qū)的父分區(qū),而且不同節(jié)點之間可以并行計算;而對于一個寬依賴關(guān)系的Lineage圖,單個節(jié)點失效可能導(dǎo)致這個RDD的所有父RDD都要進行重新計算。

七.RDD的檢查點(checkpoint)機制

RDD的緩存能夠在第一次計算完成后,將計算結(jié)果保存到內(nèi)存、本地文件系統(tǒng)或者Tachyon中。通過緩存,Spark避免了RDD上的重復(fù)計算,能夠極大地提升計算速度。但是,如果緩存丟失了,則需要重新計算。如果計算特別復(fù)雜或者計算耗時特別多,那么緩存丟失對于整個Job的影響是不容忽視的。

為了避免緩存丟失重新計算帶來的開銷,Spark又引入了檢查點(checkpoint)機制。

緩存是在計算結(jié)束后,直接將計算結(jié)果寫入不同的介質(zhì)。而檢查點不同,它是在計算完成后,為數(shù)據(jù)創(chuàng)建一個目錄,并且將計算結(jié)果寫入新創(chuàng)建的目錄,之后重新建立一個Job來計算。接著創(chuàng)建一個CheckpointRDD,RDD變成CheckPointRDD后,前邊的所有RDD依賴都會被移除。這就意味著RDD的轉(zhuǎn)換的計算鏈(compute chain) 等信息都被清除。

一般推薦先將RDD緩存,這樣就能保證檢查點的操作可以快速完成。

設(shè)置檢查點:

//設(shè)置檢查點目錄 存儲在HDFS上,并使用checkpoint設(shè)置檢查點,該操作屬于懶加載
sc.setCheckpointDir("hdfs://xxxx:9000/checkpoint/")
rdd.checkpoint()
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,462評論 2 378