Spark源碼分析(二)RDD

前言

前段時間寫了第一篇博客,回頭想了想,補充一些東西:
1、我的Spark版本是1.0.2的
2、以後一個星期至少一篇博客,還請大家多多支持
3、因爲都是自己的一些拙見,有些問題還請大家指出,我會及時回覆
謝謝!!!

中文版本的連接:http://shiyanjun.cn/archives/744.html

什麼是RDD

RDD是彈性分佈式數據集,其實RDD只是一個標記,也可以稱做meta。一個RDD的生成只有兩種途徑,一是來自於內存集合和外部存儲系統,另一種是通過轉換操作來自於其他RDD,比如map、filter、reducebykey等等。
RDD一共有5個特徵,分別對應5個函數:
1、partions():一個RDD會有一個或者多個分區
2、preferredLocation(p):對於分區p而言,返回數據本地化計算的節點
3、dependencies():RDD之間的依賴關係
4、compute():這個函數是用戶編寫的,計算每一個分區
5、partitioner():RDD的分區函數

下面介紹每一個函數

RDD分區 partitons()

  final def partitions: Array[Partition] = {
    checkpointRDD.map(_.partitions).getOrElse {
      if (partitions_ == null) {
        partitions_ = getPartitions
      }
      partitions_
    }
  }
注意會調用checkpointRDD,如果該RDD checkpoint過,則調用CheckpointRDD.partitions(後面還會詳細講解),否則調用該RDD的getPartitions方法,不同的RDD會實現不同的getPartitions方法
對於一個RDD而言,分區的多少涉及對這個RDD進行並行計算的粒度,每一個RDD分區的計算操作都在一個單獨的任務中被執行

RDD優先位置 preferredLocation

  final def preferredLocations(split: Partition): Seq[String] = {
    checkpointRDD.map(_.getPreferredLocations(split)).getOrElse {
      getPreferredLocations(split)
    }
  }
也會優先在checkpointRDD查找,如果沒有就調用該RDD的getPreferredLocations方法
這個方法是在DAGScheduler.subMissingTasks會調用,會一直找到第一個RDD,返回數據本地化的節點

RDD依賴關係 dependencies

  final def dependencies: Seq[Dependency[_]] = {
    checkpointRDD.map(r => List(new OneToOneDependency(r))).getOrElse {
      if (dependencies_ == null) {
        dependencies_ = getDependencies
      }
      dependencies_
    }
  }
和上面一樣,也會先在checkpointRDD中查找是否有依賴,deps是在創建RDD實例傳入的,其實deps存放的就是依賴的RDD
由於RDD是粗粒度的操作數據集,每一個轉換操作都會生成一個新的RDD,所以RDD之間就會形成類似於流水線(一個Stage內,pipeline)一樣的前後依賴關係,在Spark中存在兩種類型的依賴,即窄依賴和寬依賴,如下圖所示
  • 窄依賴:每一個父RDD的分區最多隻被子RDD的一個分區所用
  • 寬依賴:多個子RDD的分區會依賴於同一個父RDD的分區
吃屎
在圖中,一個大矩形表示一個RDD,在大矩形中的小矩形表示這個RDD的一個分區
在Spark中要明確地區分這兩種依賴關係有兩個方面的原因:
第一,對於窄依賴中所有的RDD而言(一個Stage中),可以在集羣的一個節點上如流水線(pipeline)一般地執行,從而加快執行速度,寬依賴類似於MapReduce一樣的shuffle操作;
第二,解決數據容錯,如果一個節點死機了,而且運算窄依賴,則只要把丟失的父RDD分區重算即可,不依賴於其他節點,而寬依賴需要父RDD的所有分區都存在,重算就很昂貴了;
第三,在調度中作爲不同Stage的劃分點

RDD分區計算compute()

def compute(split: Partition, context: TaskContext): Iterator[T]
對於Spark中每個RDD的計算都是以partition爲單位,compute函數會使用用戶編寫好的程序,最終返回相應分區數據的迭代器
有兩種生成迭代器的方式:
1、firstParent[T].iterator(split, context).map(f),即父RDD的Iterator的next方法會調用f進行相應的處理。
2、f(firstParent.iterator()): 對父RDD的Iterator的hasNext和next方法進行自定義處理, 即f會調用Iterator的hasNext和next方法即對整個分區進行操作
很重要的一點,這種compute是複合式的,它會一直找到這個stage中的第一個RDD.iterator執行,也就是所謂的pipeline

RDD分區類partitioner

partitioner這個屬性只存在於(K,V)類型的RDD中,對於非(K,V)類型的partitioner的值就是None。
partitioner屬性既決定了RDD本身的分區數量,也可以作爲其父RDD shuffle輸出中每個分區進行數據切割的依據

RDD之間的轉換

典型的Job邏輯執行圖如下圖所示
吃屎
下面用一個實例講解,基本涵蓋RDD中的所有操作
    val hdfsFile = sc.textFile(args(1))
    val flatMapRdd = hdfsFile.flatMap(s => s.split(" "))
    val filterRdd = flatMapRdd.filter(_.length == 2)
    val mapRdd = filterRdd.map(word => (word, 1))
    val reduce = mapRdd.reduceByKey(_ + _)
    reduce.cache()
    reduce.saveAsTextFile(hdfs://...)
第一行屬於創建操作:從存儲系統HDFS、HBase等讀入數據,轉換成HadoopRDD
第二、三、四行屬於轉換操作:最後轉換成MappedRDD
第五行比較特別也屬於轉換操作,前面的轉換操作比較簡單,這個比較複雜,會產生shuffle,同時因爲在RDD類中沒有該函數,它採用了隱式轉換,源碼在SparkContext中實例化了PairRDDFunctions,然後我們寫代碼時引用SparkContext._,就能使用PairRDDFunctions裏面的方法
第六行屬於控制操作:該方法最終調用了persisit方法,該方法主要是控制變量storageLevel,默認是None,會影響RDD.iterator,是從BlockManager讀取緩存還是重新計算;控制操作還有checkpoint()方法,後面的章節還會詳細講解
第七行屬於行動操作:將計算結果存儲到存儲系統或者返回給Driver,還會觸發作業的真正提交(延遲執行lazy)

由於篇幅的原因的,在這裏主要講解reduceBykey(_+_),其餘RDD的用法都能網上搜到
reduceBykey(_+_)最終會調用以下函數:
  def combineByKey[C](createCombiner: V => C,
      mergeValue: (C, V) => C,
      mergeCombiners: (C, C) => C,
      partitioner: Partitioner,
      mapSideCombine: Boolean = true,
      serializer: Serializer = null): RDD[(K, C)] = {
    require(mergeCombiners != null, "mergeCombiners must be defined") // required as of Spark 0.9.0
    if (keyClass.isArray) {
      if (mapSideCombine) {
        throw new SparkException("Cannot use map-side combining with array keys.")
      }
      if (partitioner.isInstanceOf[HashPartitioner]) {
        throw new SparkException("Default partitioner cannot partition array keys.")
      }
    }
    val aggregator = new Aggregator[K, V, C](createCombiner, mergeValue, mergeCombiners)
    if (self.partitioner == Some(partitioner)) {
      self.mapPartitionsWithContext((context, iter) => {
        new InterruptibleIterator(context, aggregator.combineValuesByKey(iter, context))
      }, preservesPartitioning = true)
    } else if (mapSideCombine) {
      val combined = self.mapPartitionsWithContext((context, iter) => {
        aggregator.combineValuesByKey(iter, context)
      }, preservesPartitioning = true)//先在partition內部做mapSideCombine,返回一個MapPartitionsRDD
      val partitioned = new ShuffledRDD[K, C, (K, C)](combined, partitioner)
        .setSerializer(serializer)//ShuffledRDD,進行shuffle
      partitioned.mapPartitionsWithContext((context, iter) => {
        new InterruptibleIterator(context, aggregator.combineCombinersByKey(iter, context))
      }, preservesPartitioning = true)//shuffle完成後,在reduce端在做一次combine,返回一個MapPartitionsRDD
    } else {
      // Don't apply map-side combiner.
      val values = new ShuffledRDD[K, V, (K, V)](self, partitioner).setSerializer(serializer)
      values.mapPartitionsWithContext((context, iter) => {
        new InterruptibleIterator(context, aggregator.combineValuesByKey(iter, context))
      }, preservesPartitioning = true)
    }
  }
combine是這樣的操作, Turns an RDD[(K, V)] into a result of type RDD[(K, C)] 
其中C有可能只是簡單類型, 但經常是seq, 比如(Int, Int) to (Int, Seq[Int])
combineByKey函數一般需要傳入5個典型參數,說明如下
  • createCombiner: V => C, C不存在的情況下, 比如通過V創建seq C 
  • mergeValue: (C, V) => C, 當C已經存在的情況下, 需要merge, 比如把item V加到seq C中, 或者疊加
  • mergeCombiners: (C, C) => C,  合併兩個C 
  • partitioner: 分區函數, Shuffle時需要的Partitioner
  • mapSideCombine: Boolean = true, 爲了減小傳輸量, 很多combine可以在map端先做, 比如疊加, 可以先在一個partition中把所有相同的key的value疊加, 再shuffle 
ShuffledRDD
class ShuffledRDD[K, V, P <: Product2[K, V] : ClassTag](
    @transient var prev: RDD[P],
    part: Partitioner)
  extends RDD[P](prev.context, Nil) {

  private var serializer: Serializer = null

  def setSerializer(serializer: Serializer): ShuffledRDD[K, V, P] = {
    this.serializer = serializer
    this
  }

  override def getDependencies: Seq[Dependency[_]] = {
    List(new ShuffleDependency(prev, part, serializer))
  }

  override val partitioner = Some(part)

  override def getPartitions: Array[Partition] = {
    Array.tabulate[Partition](part.numPartitions)(i => new ShuffledRDDPartition(i))
  }

  override def compute(split: Partition, context: TaskContext): Iterator[P] = {
    val shuffledId = dependencies.head.asInstanceOf[ShuffleDependency[K, V]].shuffleId
    val ser = Serializer.getSerializer(serializer)
    SparkEnv.get.shuffleFetcher.fetch[P](shuffledId, split.index, context, ser)
  }

  override def clearDependencies() {
    super.clearDependencies()
    prev = null
  }
}
compute函數中主要是Shuffle Read,它由shuffleFetcher完成
因爲每個shuffle是有一個全局的shuffleid的,所以在compute裏面, 你只是看到用BlockStoreShuffleFetcher根據shuffleid和partitionid直接fetch到shuffle過後的數據


補充一點:Spark內部生成的RDD對象數量一般多於用戶書寫的Spark應用程序中包含的RDD,其根本原因是Spark的一些操作與RDD不是一一對應的,上面的combineByKey就是一個例子



發佈了20 篇原創文章 · 獲贊 9 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章