Spark自建的邏輯內存管理器是怎么申請和釋放內存的?

漫談Spark內存管理(一)中,概述了Spark內存管理做的事情,并著重對unroll memory的概念做了解釋及分析。本文繼續討論Spark Memory Manager的功能實現.

Spark的MemoryManager提供了一套邏輯上的內存申請和釋放機制。spark1.6之后,UnifiedMemoryManager成為默認內存管理器,所以筆者以UnifiedMemoryManger為例分析spark內存管理器的具體實現。

1 存儲內存管理

1.1 申請存儲內存

Spark中的RDD Block,Broadcast Block都可能使用存儲內存(也可能用磁盤)進行存儲,存儲之前必須先向MemoryManager申請所需要的內存空間。

UnifiedMemoryManger.acquireStorageMemory方法用于為block申請指定memory mode(onHeap或offHeap),指定memory size的存儲內存空間:

UnifiedMemoryManger.acquireStorageMemory

1. 首先,根據申請的memorymode獲取對應的執行內存池,存儲內存池和最大存儲內存(maxMemory);

2. 最大存儲內存由UnifiedMemoryManager的兩個方法提供:


In UnifiedMemoryManager

UnifiedMemoryManager的執行和存儲內存是可以互借的,所以這里獲取最大存儲內存時,直接用最大內存減去已用執行內存。

3. 看看maxHeapMemory和maxOffHeapMemory是怎么得到的:

? ? a. maxHeapMemory由UnifiedMemoryManager.getMaxMemory方法計算得到:


UnifiedMemoryManager.getMaxMemory

? ? 首先,獲取系統內存大小,默認直接調用 java 的Runtime.getRuntime.maxMemory 方法獲取當前 jvm 最大內存,這個最大內存的值可通過 jvm 參數-Xmx 配置,但是這個值并不等于-Xmx 指定的值,會稍小一些,不同 jvm 和操作系統可能不同。-Xmx 的值可由--driver-memory和--executor-memory 控制;

? ? 然后, 預留一部分系統內存,默認的RESERVED_SYSTEM_MEMORY_BYTES 為300MB, 也就是說默認預留 450MB 系統內存。可用內存就是系統內存減去預留內存。

? ? 最后,UnifiedMemoryManager 的 maxHeapMemory 就是可用內存乘以spark.memory.fraction.

? ? b. maxOffHeapMemory直接從配置中讀取:

maxOffHeapMemory in MemoryManager

4. 當申請的內存比存儲內存池的空閑內存大,則向執行內存池借內存,并調整執行內存池和存儲內存池的_poolSize.

5. 最后調用存儲內存池的 acquireMemory 方法申請內存。

6. 再看看 storagePool.acquireMemory 的實現:

這里的參數numBytesToFree就是要申請的內存大小減去存儲內存池的空閑內存大小:

numBytesToFree
MemoryStore.evictBlocksToFreeSpace

如果存儲內存池的當前空閑內存不夠,則調用MemoryStore.evictBlocksToFreeSpace方法釋放內存。該方法會按照LRU的順序遍歷memoryStore中存儲的所有blocks,選擇出可驅逐的blocks,可驅逐block需要滿足條件:

? ? a. 使用的memorymode與要申請的memorymode相同

? ? b. 與申請內存的block不屬于同一個rdd

? ? c. 沒有正在被讀取(not locked for reading)

? ? 注意,因為MemoryStore.entries的類型為java.util.LinkedHashMap,并且accessOrder為true:

MemoryStore.entries

? ? 所以,MemoryStore.evictBlocksToFreeSpace在遍歷entries選擇可驅逐塊時,是按照LRU(Lease Recently Used)算法進行的。

? ? 只有當可釋放的內存總量大于需要釋放的內存量時才會調用blockEvictionHandler.dropFromMemory從內存中刪除選中的blocks. 需注意,因為evictBlocksToFreeSpace方法會調用memoryManager.synchronized,所以,同一時刻最多只有一個task在刪除memoryStore中的block.

釋放完內存后,如果空閑內存足夠,則更新存儲內存池的_memoryUsed變量。

從上面的分析可以看出,acquireStorageMemory會判斷當前空閑存儲內存是否足夠,如果不夠則會從執行內存池借空閑內容,如果還不夠,則會按照LRU的順序驅逐當前內存池中某些滿足條件的blocks以釋放內存。如果這兩種措施都無法獲取足夠的空閑存儲內存,則申請存儲內存失敗,acquireStorageMemory返回false. 如果申請成功,則更新存儲內存池的_memoryUsed變量,表示這部分內存已被使用。

1.2 釋放存儲內存

UnifiedMemoryManger.releaseStorageMemory方法用于釋放存儲內存:

UnifiedMemoryManger.releaseStorageMemory

同樣也是根據memory mode調用不同存儲內存池的releaseMemory方法。onHeapStoreageMemoryPool和offHeapStorageMemoryPool都是StorageMemoryPool類的對象,下面看看StorageMemoryPool.releaseMemory方法的實現:

StorageMemoryPool.releaseMemory

很簡單,就是更新存儲內存池的_memoryUsed變量。

2 執行內存管理

2.1 申請執行內存

Spark中的shuffle,aggregate,join等操作都會使用執行內存,每個task在執行時會通過taskMemoryManager調用MemoryManager的acquireExecutionMemory方法申請需要的執行內存。UnifiedMemoryManger.acquireExecutionMemory方法做了以下步驟:

? ? 1. 根據memory mode獲取對應的執行內存池,存儲內存池, 用于存儲的內存大小, 最大內存:

UnifiedMemoryManger. acquireExecutionMemory

? ? ? 其中maxHeapMemory,maxOffHeapMemory和上文介紹的是一樣的,這點也體現了UnifiedMemoryManager的unified,哈哈。storageRegionSize是用于數據存儲的內存大小,對于onHeap內存,storageRegionSize為:

onHeapStorageRegionSize

其中的maxMemory就是上文中介紹的UnifiedMemoryManager.getMaxMemory方法返回的值。對于offHeap內存, storageRegionSize為:


offHeapStorageRegionSize

這里的maxOffHeapMemory和上文提到的也是同一個。

? ? 2. 定義用于增長執行內存池的maybeGrowExecutionPool方法,以及用于計算最大執行內存池大小的computeMaxExecutionPoolSize方法:

UnifiedMemoryManager.maybeGrowExecutionPool

? ? maybeGrowExecutionPool方法會做:

? ? ? ? a. 計算存儲內存池的實際poolsize和用于數據存儲的內存大小storageRegionSize之間的差值。因為在存儲block(比如RDD block,broadcastblock)時,存儲內存池可能從執行內存池借內存,所以它的實際poolsize可能大于配置的storageRegionSize.我們暫且稱這個差值為delta.

? ? ? ? b. 比較存儲內存池的空閑內存和delta,取更大的作為memoryReclaimableFromStorage.也就是說這里不僅會收回存儲內存池從執行內存池借的內存,還會向存儲內存池借空閑內存,即“回收+借”。

? ? ? ? c. 調用StorageMemoryPool.freeSpaceToShrinkPool方法,該方法會先從存儲內存池的空閑內存中獲取需要reclaim的內存,如果不夠則會調用MemoryStore.evictBlocksToFreeSpace方法驅逐存儲在內存中的某些block以釋放內存。

? ? ? ? d. 最后,調整存儲內存池和執行內存池的大小。

? ? computeMaxExecutionPoolSize方法用于計算調用maybeGrowPool之后,執行內存池的最大size,實現比較簡單:

UnifiedMemoryManager.computeMaxExecutionPoolSize

? ? 注意,如果storagePool.memoryUsed大于storageRegionSize,則說明在maybeGrowPool中調用freeSpaceToShrinkPool方法時未能成功釋放delta大小的內存(或者是不需要釋放那么多)。此時,computeMaxExecutionPoolSize方法返回的值會大于執行內存池的實際poolsize。

? ? 3. 調用ExecutionMemoryPool.acquireMemory方法申請執行內存。

再來看看ExecutionMemoryPool.acquireMemory的實現:

ExecutionMemoryPool.acquireMemory

ExecutionMemoryPool.acquireMemory用了一個while循環不斷嘗試分配內存,只有分配成功的情況下才會退出循環。每次嘗試會做:

1. 嘗試從存儲內存池回收內存,從而增長執行內存池的大小;

2. 獲取執行內存池的最大容量maxPoolSize,累計分配給單個task的內存大小范圍為[maxPoolSize/2*numActiveTasks,maxPoolSize/(numActiveTasks)];

3. 根據當前task已占用內存,申請的內存大小,可分配的內存大小范圍,以及執行內存池的空閑內存大小,最終確定要分配的內存大小toGrant;

4. 如果分配toGrant內存之后task所占內存仍小于maxPoolSize/2*numActiveTasks,則調用lock.wait()等待其他task釋放內存;

5. 如果toGrant在正常范圍內,則更新指定task的已占用內存,并結束循環。

2.2 釋放執行內存

ExecutionMemoryPool.releaseMemory

可以看到,釋放執行內存最終是通過更新memoryForTask這個map來實現的。最后通過執行內存池的lock通知所有在請求執行內存時由于內存不足調用lock.wait()等待的任務線程。執行內存池的已用內存大小是從memoryForTask計算得到的:


ExecutionMemoryPool.memoryUsed

3 分析&總結

3.1 存儲內存和執行內存申請過程的不同

基于上文中的源碼分析,比較申請存儲內存和執行內存的過程會發現,在申請執行內存時,spark可能會驅逐存儲內存中的block以滿足執行內存的需要;而申請存儲內存時,只會從執行內存池借空閑內存(而且借的有可能包括執行內存向存儲內存借的,所以也應該是 “回收+借”),并不會釋放執行內存以滿足存儲內存的需要。也就是說,在Spark中,執行內存的優先級是更高的。筆者認為,這是因為執行內存用于shuffle,aggregation,join等操作中的各種map等數據結構,強行釋放這些內存可能會導致task運行錯誤或失敗,而存儲內存主要用于存放緩存的RDD Block,Broadcast Block等數據,spark可以將這些block從內存移到磁盤存儲或直接刪除,在需要訪問時可以根據lineage重新計算。

3.2 Spark內存管理器的功能

通過上文對spark memory manager各個方法的源碼分析,可以看到spark的內存管理器自建了一套控制內存使用的方案,但這是一套邏輯上的內存管理方案。從實現角度上講,就是維護了一系列的變量來記錄和控制spark各個模塊對內存(包括onHeap和offHeap內存)的使用。而真正向操作系統申請和釋放物理內存的工作由JVM或Tungsten完成,Tungsten內存管理的核心內容是在TaskMemoryManager類中實現的,后續文章我們會詳細討論。

3.3 總結

本文以UnifiedMemoryManager為例,從源碼角度分析了spark內存管理器如何將內存劃分為存儲和執行內存,詳述了存儲和執行內存的申請和釋放過程。分析了存儲和執行內存申請過程中的不同之處,總結了spark內存管理器的功能。

4 說明

? ? a. 本文spark源碼版本為spark 2.4.0

? ? b. 水平有限,如有錯誤,望讀者指出

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容