天天看點

Spark 記憶體管理模型的基本原理

原文連結:https://mp.weixin.qq.com/s?__biz=MzI4MzY5MDU5Mg==&mid=2247483849&idx=1&sn=3a53d18d44a0c272e570ddafe1cd904d&chksm=eb8792c6dcf01bd04110e20459718cac96295213802d0dda066c350c4486206865664978714b&scene=21#wechat_redirect

Spark 作為一個基于記憶體的分布式計算引擎,其記憶體管理子產品在整個系統中扮演着非常重要的角色。了解 Spark 記憶體管理的基本原理,有助于更好地開發 Spark 應用程式和進行性能調優。

在執行 Spark 的應用程式時,Spark 叢集會啟動 Driver 和 Executor 兩種 JVM 程序,前者為主要程序,負責建立 Spark 上下文,送出 Spark 作業(Job),并将作業轉化為計算任務(Task),在各個 Executor 程序間協調任務的排程,後者負責在工作節點上執行具體的計算任務,并将結果傳回給 Driver,同時為需要持久化的 RDD 提供存儲功能。由于 Driver 的記憶體管理相對來說較為簡單,本文主要對 Executor 的記憶體管理進行分析,下文中的 Spark 記憶體均特指 Executor 的記憶體。

堆内和堆外記憶體規劃

作為一個 JVM 程序,Executor 的記憶體管理建立在 JVM 的記憶體管理之上,Spark 對 JVM 的堆内(On-heap)空間進行了更為詳細的配置設定,以充分利用記憶體。同時,Spark 引入了堆外(Off-heap)記憶體,使之可以直接在工作節點的系統記憶體中開辟空間,進一步優化了記憶體的使用。

Spark 記憶體管理模型的基本原理

堆内記憶體

堆内記憶體的大小,由 Spark 應用程式啟動時的 –executor-memory 或 spark.executor.memory 參數配置。Executor 内運作的并發任務共享 JVM 堆内記憶體,這些任務在緩存 RDD 資料和廣播(Broadcast)資料時占用的記憶體被規劃為存儲(Storage)記憶體,而這些任務在執行 Shuffle 時占用的記憶體被規劃為執行(Execution)記憶體,剩餘的部分不做特殊規劃,那些 Spark 内部的對象執行個體,或者使用者定義的 Spark 應用程式中的對象執行個體,均占用剩餘的空間。不同的管理模式下,這三部分占用的空間大小各不相同。

Spark 對堆内記憶體的管理是一種邏輯上的"規劃式"的管理,因為對象執行個體占用記憶體的申請和釋放都由 JVM 完成,Spark 隻能在申請後和釋放前記錄這些記憶體,我們來看其具體流程:

申請記憶體:

  1. Spark 在代碼中 new 一個對象執行個體
  2. JVM 從堆内記憶體配置設定空間,建立對象并傳回對象引用
  3. Spark 儲存該對象的引用,記錄該對象占用的記憶體

釋放記憶體:

  1. Spark 記錄該對象釋放的記憶體,删除該對象的引用
  2. 等待 JVM 的垃圾回收機制釋放該對象占用的堆内記憶體

堆外記憶體

為了進一步優化記憶體的使用以及提高 Shuffle 時排序的效率,Spark 引入了堆外(Off-heap)記憶體,使之可以直接在工作節點的系統記憶體中開辟空間,存儲經過序列化的二進制資料。利用 JDK Unsafe API(從 Spark 2.0 開始),在管理堆外的存儲記憶體時不再基于 Tachyon,而是與堆外的執行記憶體一樣,基于 JDK Unsafe API 實作,Spark 可以直接作業系統堆外記憶體,減少了不必要的記憶體開銷,以及頻繁的 GC 掃描和回收,提升了處理性能。堆外記憶體可以被精确地申請和釋放,而且序列化的資料占用的空間可以被精确計算,是以相比堆内記憶體來說降低了管理的難度,也降低了誤差。

在預設情況下堆外記憶體并不啟用,可通過配置 spark.memory.offHeap.enabled 參數啟用,并由 spark.memory.offHeap.size 參數設定堆外空間的大小。除了沒有 other 空間,堆外記憶體與堆内記憶體的劃分方式相同,所有運作中的并發任務共享存儲記憶體和執行記憶體。

記憶體管理模型

Spark 1.6 之後預設為統一管理(UnifiedMemoryManager)方式,1.6 之前采用的靜态管理(StaticMemoryManager)方式仍被保留,可通過配置 spark.memory.useLegacyMode=true 參數啟用靜态記憶體管理方式。下面我們介紹下兩種記憶體管理模型的進化。

靜态記憶體管理

在 Spark 最初采用的靜态記憶體管理機制下,存儲記憶體、執行記憶體和其他記憶體的大小在 Spark 應用程式運作期間均為固定的,但使用者可以應用程式啟動前進行配置,堆内記憶體的配置設定如下圖所示:

Spark 記憶體管理模型的基本原理

記憶體空間被分成了三塊獨立的區域,每塊區域的記憶體容量是按照JVM堆大小的固定比例進行配置設定的:

  • Execution:在執行shuffle、join、sort和aggregation時,用于緩存中間資料。通過spark.shuffle.memoryFraction進行配置,預設為0.2。
  • Storage:主要用于緩存資料塊以提高性能,同時也用于連續不斷地廣播或發送大的任務結果。通過spark.storage.memoryFraction進行配置,預設為0.6。
  • Other:這部分記憶體用于存儲運作Spark系統本身需要加載的代碼與中繼資料,預設為0.2。

對于JVM的記憶體管理,我們還要考慮Out Of Memory(OOM)的情形,例如突然出現不可預知的超大的資料項,就可能導緻記憶體不夠。為避免這種情況,我們就不能将記憶體都配置設定給Spark的這三塊記憶體空間,就好似設計電梯,必須要保證明際的承重要遠大于規定的安全承重值。畢竟這種配置的方式很難保證準确估算,以滿足各種複雜的資料分析場景。

于是Spark提供了一個safe fraction,以便于為記憶體使用提供一個安全的緩存空間。Execution與Storage記憶體空間的safe fraction分别通過如下三個配置項配置:

spark.shuffle.safeFraction (預設值為0.8)
spark.storage.safeFraction (預設值為0.9)
spark.storage.unrollFraction(預設值為0.2)
           

以預設設定而論,用于執行的記憶體空間隻占整個JVM堆容量的spark.shuffle.memoryFraction * spark.shuffle.safeFraction = 0.2*0.8=16%,正常情況下,記憶體的使用率極低,為安全卻又必須預留出更多的記憶體避免記憶體溢出。

Spark 記憶體管理模型的基本原理

堆外的空間配置設定較為簡單,隻有存儲記憶體和執行記憶體,如下圖所示。可用的執行記憶體和存儲記憶體占用的空間大小直接由參數 spark.memory.storageFraction 決定,由于堆外記憶體占用的空間可以被精确計算,是以無需再設定保險區域。

靜态記憶體管理機制實作起來較為簡單,但如果使用者不熟悉 Spark 的存儲機制,或沒有根據具體的資料規模和計算任務或做相應的配置,很容易造成"一半海水,一半火焰"的局面,即存儲記憶體和執行記憶體中的一方剩餘大量的空間,而另一方卻早早被占滿,不得不淘汰或移出舊的内容以存儲新的内容。這樣一來,就會影響到整個系統的性能,導緻I/O增長,或者重複計算。

Spark從1.6.0版本開始,記憶體管理子產品就發生了改變,靜态記憶體管理機制實作了StaticMemoryManager 類,現在被稱為"legacy","Legacy"模式預設被置為不可用。考慮的相容性,可以通過設定spark.memory.useLegacyMode為可用,預設是false.

統一記憶體管理

Spark 1.6 之後引入的統一記憶體管理機制,與靜态記憶體管理的差別在于存儲記憶體和執行記憶體共享同一塊空間,可以動态占用對方的空閑區域。如下圖所示:

Spark 記憶體管理模型的基本原理

其中最重要的優化在于動态占用機制,其規則如下:

  • 設定基本的存儲記憶體和執行記憶體區域(spark.storage.storageFraction 參數),該設定确定了雙方各自擁有的空間的範圍
  • 雙方的空間都不足時,則存儲到硬碟;若己方空間不足而對方空餘時,可借用對方的空間;(存儲空間不足是指不足以放下一個完整的 Block)
  • Executio記憶體的空間被Storage記憶體占用後,可讓對方将占用的部分轉存到硬碟,然後"歸還"借用的空間
  • Storage記憶體的空間被Execution記憶體占用後,無法讓對方"歸還",因為需要考慮 Shuffle 過程中的很多因素,實作起來較為複雜

新的版本引入了新的配置項:

  • spark.memory.fraction(預設值為0.75):用于execution和storage的堆記憶體比例。該值越低,越容易發生spill和緩存資料回收。該配置實際上也限定了OTHER記憶體大小,以及偶發超大record的記憶體消耗。
  • spark.memory.storageFraction(預設值為0.5):顯然,這是存儲記憶體所占spark.memory.fraction設定比例記憶體的大小。當整體的存儲容量超過該比例對應的容量時,緩存的資料會被回收。
  • Reserved Memory:預設都是300MB,這個數字一般都是固定不變的,在系統運作的時候 Java Heap 的大小至少為 Heap Reserved Memory x 1.5. e.g. 300MB x 1.5 = 450MB 的 JVM配置。
  • spark.memory.useLegacyMode(預設值為false):若設定為true,則使用1.6版本前的記憶體管理機制。此時,如下五項配置均生效:
   spark.storage.memoryFraction
   spark.storage.safetyFraction
   spark.storage.unrollFraction
   spark.shuffle.memoryFraction
   spark.shuffle.safetyFraction
           

動态占用機制圖示

Spark 記憶體管理模型的基本原理

憑借統一記憶體管理機制,Spark 在一定程度上提高了堆内和堆外記憶體資源的使用率,降低了開發者維護 Spark 記憶體的難度,但并不意味着開發者可以高枕無憂。譬如,是以如果存儲記憶體的空間太大或者說緩存的資料過多,反而會導緻頻繁的全量垃圾回收,降低任務執行時的性能,因為緩存的 RDD 資料通常都是長期駐留記憶體的。是以要想充分發揮 Spark 的性能,需要開發者進一步了解存儲記憶體和執行記憶體各自的管理方式和實作原理。

Spark 記憶體管理模型的基本原理

繼續閱讀