Spark在處理資料的時候,會將資料都載入到記憶體再做處理嗎?

大資料學習與分享發表於2021-02-22

對於Spark的初學者,往往會有一個疑問:Spark(如SparkRDDSparkSQL)在處理資料的時候,會將資料都載入到記憶體再做處理嗎?

很顯然,答案是否定的!

對該問題產生疑問的根源還是對Spark計算模型理解不透徹。

對於Spark RDD,它是一個分散式的彈性資料集,不真正儲存資料。如果你沒有在程式碼中呼叫persist或者cache運算元,Spark是不會真正將資料都放到記憶體裡的。

此外,還要考慮persist/cache的快取級別,以及對什麼進行快取(比如是對整張表生成的DataSet快取還是列裁剪之後生成的DataSet快取)(關於Spark RDD的特性解析參考《Spark RDD詳解》

既然Spark RDD不儲存資料,那麼它內部是如何讀取資料的呢?其實Spark內部也實現了一套儲存系統:BlockManager。為了更深刻的理解Spark RDD資料的處理流程,先拋開BlockManager本身原理,從原始碼角度闡述RDD內部函式的迭代體系。

我們都知道RDD運算元最終會被轉化為shuffle map task和result task,這些task通過呼叫RDD的iterator方法獲取對應partition資料,而這個iterator方法又會逐層呼叫父RDD的iterator方法獲取資料(通過重寫scala.collection.iterator的hasNext和next方法實現)。主要過程如下:

首先看ShuffleMapTask和ResultTask中runTask方法的原始碼:

關鍵看這部分處理邏輯:

rdd.iterator(partition, context)

 

 

getOrCompute方法會先通過當前executor上的BlockManager獲取指定blockId的block,如果block不存在則呼叫computeOrReadCheckpoint,如果要處理的RDD沒有被checkpoint或者materialized,則接著呼叫compute方法進行計算。

compute方法是RDD的抽象方法,由繼承RDD的子類具體實現。

以WordCount為例:

sc.textFile(input)
  .flatMap(line => line.split(" "))
  .map(word => (word, 1))
  .reduceByKey(_ + _)
  .saveAsTextFile(output)

 

  1. textFile會構建一個HadoopRDD

  2. flatMap/map會構建一個MapPartitionsRDD

  3. reduceByKey觸發shuffle時會構建一個ShuffledRDD

  4. saveAsTextFile作為action運算元會觸發整個任務的執行

以flatMap/map產生的MapPartitionsRDD實現的compute方法為例:

override def compute(split: Partition, context: TaskContext): Iterator[U] =
    f(context, split.index, firstParent[T].iterator(split, context))

 

底層呼叫了parent RDD的iterator方法,然後作為引數傳入到了當前的MapPartitionsRDD。而f函式就是對parent RDD的iterator呼叫了相同的map類函式以執行使用者給定的函式。

所以,這是一個逐層巢狀的rdd.iterator方法呼叫,子RDD呼叫父RDD的iterator方法並在其結果之上呼叫Iterator的map函式以執行使用者給定的函式,逐層呼叫直到呼叫到最初的iterator(比如上述WordCount示例中HadoopRDD partition的iterator)。

而scala.collection.Iterator的map/flatMap方法返回的Iterator就是基於當前Iterator重寫了next和hasNext方法的Iterator例項。比如,對於map函式,結果Iterator的hasNext就是直接呼叫了self iterator的hasNext,next方法就是在self iterator的next方法的結果上呼叫了指定的map函式。

flatMap和filter函式稍微複雜些,但本質上一樣,都是通過呼叫self iterator的hasNext和next方法對資料進行遍歷和處理。

所以,當我們呼叫最終結果iterator的hasNext和next方法進行遍歷時,每遍歷一個資料元素都會逐層呼叫父層iterator的hasNext和next方法。各層的map函式組成一個pipeline,每個資料元素都經過這個pipeline的處理得到最終結果。

這也是Spark的優勢之一,map類運算元整個形成類似流式處理的pipeline管道,一條資料被該鏈條上的各個RDD所包裹的函式處理。

再回到WordCount例子。HadoopRDD直接跟資料來源關聯,記憶體中儲存多少資料跟讀取檔案的buffer和該RDD的分割槽數相關(比如buffer*partitionNum,當然這是一個理論值),saveAsTextFile與此類似。MapPartitionsRDD裡實際在記憶體裡的資料也跟partition數有關係。ShuffledRDD稍微複雜些,因為牽扯到shuffle,但是RDD本身的特性仍然滿足(記錄檔案的儲存位置)。

說完了Spark RDD,再來看另一個問題:Spark SQL對於多表之間join操作,會先把所有表中資料載入到記憶體再做處理嗎?

當然,肯定也不需要!

具體可以檢視Spark SQL針對相應的Join SQL的查詢計劃,以及在之前的文章《Spark SQL如何選擇join策略》中,針對目前Spark SQL支援的join方式,任何一種都不要將join語句中涉及的表全部載入到記憶體。即使是Broadcast Hash Join也只需將滿足條件的小表完整載入到記憶體。

推薦文章:

對Spark硬體配置的建議

Spark和MapReduce任務計算模型

Spark叢集和任務執行

通過spark.default.parallelism談Spark談並行度

解析SparkStreaming和Kafka整合的兩種方式

Spark為什麼只有在呼叫action時才會觸發任務執行呢(附運算元優化和使用示例)?


關注微信公眾號:大資料學習與分享,獲取更對技術乾貨

相關文章