轉帖|使用教程|編輯:龔雪|2017-03-31 12:04:27.000|閱讀 241 次
概述:Spark 作為一個基于內存的分布式計算引擎,其內存管理模塊在整個系統中扮演著非常重要的角色。
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
Spark 作為一個基于內存的分布式計算引擎,其內存管理模塊在整個系統中扮演著非常重要的角色。理解 Spark 內存管理的基本原理,有助于更好地開發 Spark 應用程序和進行性能調優。本文旨在梳理出 Spark 內存管理的脈絡,拋磚引玉,引出讀者對這個話題的深入探討。本文中闡述的原理基于 Spark 2.1 版本,閱讀本文需要讀者有一定的 Spark 和 Java 基礎,了解 RDD、Shuffle、JVM 等相關概念。
在執行 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)內存,使之可以直接在工作節點的系統內存中開辟空間,進一步優化了內存的使用。
圖 1 . 堆內和堆外內存示意圖
堆內內存的大小,由 Spark 應用程序啟動時的 –executor-memory 或 spark.executor.memory 參數配置。Executor 內運行的并發任務共享 JVM 堆內內存,這些任務在緩存 RDD 數據和廣播(Broadcast)數據時占用的內存被規劃為存儲(Storage)內存,而這些任務在執行 Shuffle 時占用的內存被規劃為執行(Execution)內存,剩余的部分不做特殊規劃,那些 Spark 內部的對象實例,或者用戶定義的 Spark 應用程序中的對象實例,均占用剩余的空間。不同的管理模式下,這三部分占用的空間大小各不相同(下面第 2 小節會進行介紹)。
Spark 對堆內內存的管理是一種邏輯上的”規劃式”的管理,因為對象實例占用內存的申請和釋放都由 JVM 完成,Spark 只能在申請后和釋放前記錄這些內存,我們來看其具體流程:
我們知道,JVM 的對象可以以序列化的方式存儲,序列化的過程是將對象轉換為二進制字節流,本質上可以理解為將非連續空間的鏈式存儲轉化為連續空間或塊存儲,在訪問時則需要進行序列化的逆過程——反序列化,將字節流轉化為對象,序列化的方式可以節省存儲空間,但增加了存儲和讀取時候的計算開銷。
對于 Spark 中序列化的對象,由于是字節流的形式,其占用的內存大小可直接計算,而對于非序列化的對象,其占用的內存是通過周期性地采樣近似估算而得,即并不是每次新增的數據項都會計算一次占用的內存大小,這種方法降低了時間開銷但是有可能誤差較大,導致某一時刻的實際內存有可能遠遠超出預期。此外,在被 Spark 標記為釋放的對象實例,很有可能在實際上并沒有被 JVM 回收,導致實際可用的內存小于 Spark 記錄的可用內存。所以 Spark 并不能準確記錄實際可用的堆內內存,從而也就無法完全避免內存溢出(OOM, Out of Memory)的異常。
雖然不能精準控制堆內內存的申請和釋放,但 Spark 通過對存儲內存和執行內存各自獨立的規劃管理,可以決定是否要在存儲內存里緩存新的 RDD,以及是否為新的任務分配執行內存,在一定程度上可以提升內存的利用率,減少異常的出現。
為了進一步優化內存的使用以及提高 Shuffle 時排序的效率,Spark 引入了堆外(Off-heap)內存,使之可以直接在工作節點的系統內存中開辟空間,存儲經過序列化的二進制數據。利用 JDK Unsafe API(從 Spark 2.0 開始,在管理堆外的存儲內存時不再基于 Tachyon,而是與堆外的執行內存一樣,基于 JDK Unsafe API 實現[3]),Spark 可以直接操作系統堆外內存,減少了不必要的內存開銷,以及頻繁的 GC 掃描和回收,提升了處理性能。堆外內存可以被精確地申請和釋放,而且序列化的數據占用的空間可以被精確計算,所以相比堆內內存來說降低了管理的難度,也降低了誤差。
在默認情況下堆外內存并不啟用,可通過配置 spark.memory.offHeap.enabled 參數啟用,并由 spark.memory.offHeap.size 參數設定堆外空間的大小。除了沒有 other 空間,堆外內存與堆內內存的劃分方式相同,所有運行中的并發任務共享存儲內存和執行內存。
Spark 為存儲內存和執行內存的管理提供了統一的接口——MemoryManager,同一個 Executor 內的任務都調用這個接口的方法來申請或釋放內存:
清單 1 . 內存管理接口的主要方法
//申請存儲內存 def acquireStorageMemory(blockId: BlockId, numBytes: Long, memoryMode: MemoryMode): Boolean //申請展開內存 def acquireUnrollMemory(blockId: BlockId, numBytes: Long, memoryMode: MemoryMode): Boolean //申請執行內存 def acquireExecutionMemory(numBytes: Long, taskAttemptId: Long, memoryMode: MemoryMode): Long //釋放存儲內存 def releaseStorageMemory(numBytes: Long, memoryMode: MemoryMode): Unit //釋放執行內存 def releaseExecutionMemory(numBytes: Long, taskAttemptId: Long, memoryMode: MemoryMode): Unit //釋放展開內存 def releaseUnrollMemory(numBytes: Long, memoryMode: MemoryMode): Unit
我們看到,在調用這些方法時都需要指定其內存模式(MemoryMode),這個參數決定了是在堆內還是堆外完成這次操作。
MemoryManager 的具體實現上,Spark 1.6 之后默認為統一管理()方式,1.6 之前采用的靜態管理()方式仍被保留,可通過配置 spark.memory.useLegacyMode 參數啟用。兩種方式的區別在于對空間分配的方式,下面的第 2 小節會分別對這兩種方式進行介紹。
在 Spark 最初采用的靜態內存管理機制下,存儲內存、執行內存和其他內存的大小在 Spark 應用程序運行期間均為固定的,但用戶可以應用程序啟動前進行配置,堆內內存的分配如圖 2 所示:
圖 2 . 靜態內存管理圖示——堆內
可以看到,可用的堆內內存的大小需要按照下面的方式計算:
清單 2 . 可用堆內內存空間
可用的存儲內存 = systemMaxMemory * spark.storage.memoryFraction * spark.storage.safetyFraction 可用的執行內存 = systemMaxMemory * spark.shuffle.memoryFraction * spark.shuffle.safetyFraction
其中 systemMaxMemory 取決于當前 JVM 堆內內存的大小,最后可用的執行內存或者存儲內存要在此基礎上與各自的 memoryFraction 參數和 safetyFraction 參數相乘得出。上述計算公式中的兩個 safetyFraction 參數,其意義在于在邏輯上預留出 1-safetyFraction 這么一塊保險區域,降低因實際內存超出當前預設范圍而導致 OOM 的風險(上文提到,對于非序列化對象的內存采樣估算會產生誤差)。值得注意的是,這個預留的保險區域僅僅是一種邏輯上的規劃,在具體使用時 Spark 并沒有區別對待,和”其它內存”一樣交給了 JVM 去管理。
堆外的空間分配較為簡單,只有存儲內存和執行內存,如圖 3 所示。可用的執行內存和存儲內存占用的空間大小直接由參數 spark.memory.storageFraction 決定,由于堆外內存占用的空間可以被精確計算,所以無需再設定保險區域。
圖 3 . 靜態內存管理圖示——堆外
靜態內存管理機制實現起來較為簡單,但如果用戶不熟悉 Spark 的存儲機制,或沒有根據具體的數據規模和計算任務或做相應的配置,很容易造成”一半海水,一半火焰”的局面,即存儲內存和執行內存中的一方剩余大量的空間,而另一方卻早早被占滿,不得不淘汰或移出舊的內容以存儲新的內容。由于新的內存管理機制的出現,這種方式目前已經很少有開發者使用,出于兼容舊版本的應用程序的目的,Spark 仍然保留了它的實現。
Spark 1.6 之后引入的統一內存管理機制,與靜態內存管理的區別在于存儲內存和執行內存共享同一塊空間,可以動態占用對方的空閑區域,如圖 4 和圖 5 所示
圖 4 . 統一內存管理圖示——堆內
圖 5 . 統一內存管理圖示——堆外
其中最重要的優化在于動態占用機制,其規則如下:
圖 6 . 動態占用機制圖示
憑借統一內存管理機制,Spark 在一定程度上提高了堆內和堆外內存資源的利用率,降低了開發者維護 Spark 內存的難度,但并不意味著開發者可以高枕無憂。譬如,所以如果存儲內存的空間太大或者說緩存的數據過多,反而會導致頻繁的全量垃圾回收,降低任務執行時的性能,因為緩存的 RDD 數據通常都是長期駐留內存的 。所以要想充分發揮 Spark 的性能,需要開發者進一步了解存儲內存和執行內存各自的管理方式和實現原理。
未完待續......
更多行業資訊,更新鮮的技術動態,盡在。
本站文章除注明轉載外,均為本站原創或翻譯。歡迎任何形式的轉載,但請務必注明出處、不得修改原文相關鏈接,如果存在內容上的異議請郵件反饋至chenjj@fc6vip.cn