標籤:
RDD的轉換和DAG的產生
Spark會根據使用者提交的計算邏輯中的RDD的轉換和動作來產生RDD之間的依賴關係,同時這個計算鏈也就產生了邏輯上的DAG。接下來以“Word Count”為例,詳細描述這個DAG產生的實現過程。
Spark Scala版本的Word Count程式如下:
1: val file = spark.textFile("hdfs://...")2: val counts = file.flatMap(line => line.split(" "))3: .map(word => (word, 1))4: .reduceByKey(_ + _)5: counts.saveAsTextFile("hdfs://...")
file和counts都是RDD,其中file是從HDFS上讀取檔案並建立了RDD,而counts是在file的基礎上通過flatMap、map和reduceByKey這三個RDD轉換產生的。最後,counts調用了動作saveAsTextFile,使用者的計算邏輯就從這裡開始提交的叢集進行計算。那麼上面這5行代碼的具體實現是什麼呢?
1)行1:spark是org.apache.spark.SparkContext的執行個體,它是使用者程式和Spark的互動介面。spark會負責串連到叢集管理者,並根據使用者佈建或者系統預設設定來申請計算資源,完成RDD的建立等。
spark.textFile("hdfs://...")就完成了一個org.apache.spark.rdd.HadoopRDD的建立,並且完成了一次RDD的轉換:通過map轉換到一個org.apache.spark.rdd.MapPartitions-RDD。
也就是說,file實際上是一個MapPartitionsRDD,它儲存了檔案的所有行的資料內容。
2)行2:將file中的所有行的內容,以空格分隔為單詞的列表,然後將這個按照行構成的單字清單合并為一個列表。最後,以每個單詞為元素的列表被儲存到MapPartitionsRDD。
3)行3:將第2步產生的MapPartitionsRDD再次經過map將每個單詞word轉為(word, 1)的元組。這些元組最終被放到一個MapPartitionsRDD中。
4)行4:首先會產生一個MapPartitionsRDD,起到map端combiner的作用;然後會產生一個ShuffledRDD,它從上一個RDD的輸出讀取資料,作為reducer的開始;最後,還會產生一個MapPartitionsRDD,起到reducer端reduce的作用。
5)行5:首先會產生一個MapPartitionsRDD,這個RDD會通過調用org.apache.spark.rdd.PairRDDFunctions#saveAsHadoopDataset向HDFS輸出RDD的資料內容。最後,調用org.apache.spark.SparkContext#runJob向叢集提交這個計算任務。
RDD之間的關係可以從兩個維度來理解:一個是RDD是從哪些RDD轉換而來,也就是RDD的parent RDD(s)是什麼;還有就是依賴於parent RDD(s)的哪些Partition(s)。這個關係,就是RDD之間的依賴,org.apache.spark.Dependency。根據依賴於parent RDD(s)的Partitions的不同情況,Spark將這種依賴分為兩種,一種是寬依賴,一種是窄依賴。
RDD的依賴關係
RDD和它依賴的parent RDD(s)的關係有兩種不同的類型,即窄依賴(narrow dependency)和寬依賴(wide dependency)。
1)窄依賴指的是每一個parent RDD的Partition最多被子RDD的一個Partition使用,1所示。
2)寬依賴指的是多個子RDD的Partition會依賴同一個parent RDD的Partition,2所示。
圖 1 RDD的窄依賴
圖 2 RDD的寬依賴
接下來可以從不同類型的轉換來進一步理解RDD的窄依賴和寬依賴的區別,3所示。
對於map和filter形式的轉換來說,它們只是將Partition的資料根據轉換的規則進行轉化,並不涉及其他的處理,可以簡單地認為只是將資料從一個形式轉換到另一個形式。對於union,只是將多個RDD合并成一個,parent RDD的Partition(s)不會有任何的變化,可以認為只是把parent RDD的Partition(s)簡單進行複製與合并。對於join,如果每個Partition僅僅和已知的、特定的Partition進行join,那麼這個依賴關係也是窄依賴。對於這種有規則的資料的join,並不會引入昂貴的Shuffle。對於窄依賴,由於RDD每個Partition依賴固定數量的parent RDD(s)的Partition(s),因此可以通過一個計算任務來處理這些Partition,並且這些Partition相互獨立,這些計算任務也就可以並存執行了。
對於groupByKey,子RDD的所有Partition(s)會依賴於parent RDD的所有Partition(s),子RDD的Partition是parent RDD的所有Partition Shuffle的結果,因此這兩個RDD是不能通過一個計算任務來完成的。同樣,對於需要parent RDD的所有Partition進行join的轉換,也是需要Shuffle,這類join的依賴就是寬依賴而不是前面提到的窄依賴了。
*******************************************************
所有的依賴都要實現trait Dependency[T]:
abstract
class
Dependency[T]
extends
Serializable {
def rdd: RDD[T]
}
其中rdd就是依賴的parent RDD。
對於窄依賴的實現是:
abstract
class
NarrowDependency[T](_rdd: RDD[T])
extends
Dependency[T] {
//返回子RDD的partitionId依賴的所有的parent RDD的Partition(s)
def getParents(partitionId: Int): Seq[Int]
override def rdd: RDD[T] = _rdd
}
現在有兩種窄依賴的具體實現,一種是一對一的依賴,即OneToOneDependency:
class
OneToOneDependency[T](rdd: RDD[T])
extends
NarrowDependency[T](rdd) {
override def getParents(partitionId: Int) = List(partitionId) ******************************************************* ******************************************************* 通過getParents的實現不難看出,RDD僅僅依賴於parent RDD相同ID的Partition。
還有一個是範圍的依賴,即RangeDependency,它僅僅被org.apache.spark.rdd.UnionRDD使用。UnionRDD是把多個RDD合成一個RDD,這些RDD是被拼接而成,即每個parent RDD的Partition的相對順序不會變,只不過每個parent RDD在UnionRDD中的Partition的起始位置不同。因此它的getPartents如下:
override def getParents(partitionId: Int) = {
if
(partitionId >= outStart && partitionId < outStart + length) {
List(partitionId - outStart + inStart)
}
else
{
Nil
}
}******************************************************* *******************************************************
其中,inStart是parent RDD中Partition的起始位置,outStart是在UnionRDD中的起始位置,length就是parent RDD中Partition的數量。
寬依賴的實現只有一種:ShuffleDependency。子RDD依賴於parent RDD的所有Partition,因此需要Shuffle過程:
class
ShuffleDependency[K, V, C](
@transient
_rdd: RDD[_ <: Product2[K, V]],
val partitioner: Partitioner,
val serializer: Option[Serializer] = None,
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.asInstanceOf[RDD[Product2[K, V]]]
//擷取新的shuffleId
val shuffleId: Int = _rdd.context.newShuffleId()
//向ShuffleManager註冊Shuffle的資訊
val shuffleHandle: ShuffleHandle =
_rdd.context.env.shuffleManager.registerShuffle(
shuffleId, _rdd.partitions.size,
this
)
_rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(
this
))
}寬依賴支援兩種Shuffle Manager,即org.apache.spark.shuffle.hash.HashShuffleManager(基於Hash的Shuffle機制)和org.apache.spark.shuffle.sort.SortShuffleManager(基於排序的Shuffle機制)。 *******************************************************
DAG的產生
原始的RDD(s)通過一系列轉換就形成了DAG。RDD之間的依賴關係,包含了RDD由哪些Parent RDD(s)轉換而來和它依賴parent RDD(s)的哪些Partitions,是DAG的重要屬性。藉助這些依賴關係,DAG可以認為這些RDD之間形成了Lineage(血統)。藉助Lineage,能保證一個RDD被計算前,它所依賴的parent RDD都已經完成了計算;同時也實現了RDD的容錯性,即如果一個RDD的部分或者全部的計算結果丟失了,那麼就需要重新計算這部分丟失的資料。
那麼Spark是如何根據DAG來產生計算任務呢?首先,根據依賴關係的不同將DAG劃分為不同的階段(Stage)。對於窄依賴,由於Partition依賴關係的確定性,Partition的轉換處理就可以在同一個線程裡完成,窄依賴被Spark劃分到同一個執行階段;對於寬依賴,由於Shuffle的存在,只能在parent RDD(s) Shuffle處理完成後,才能開始接下來的計算,因此寬依賴就是Spark劃分Stage的依據,即Spark根據寬依賴將DAG劃分為不同的Stage。在一個Stage內部,每個Partition都會被分配一個計算任務(Task),這些Task是可以並存執行的。Stage之間根據依賴關係變成了一個大粒度的DAG,這個DAG的執行順序也是從前向後的。也就是說,Stage只有在它沒有parent Stage或者parent Stage都已經執行完成後,才可以執行。
Apache Spark RDD初談3