漫談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的存儲內存空間:

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

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


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

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

    a. maxHeapMemory由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直接從配置中讀取:

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

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

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

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

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

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

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

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

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


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

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

1.2 釋放存儲內存

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

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

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

2 執行內存管理

2.1 申請執行內存

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

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

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

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


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

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

    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,實現比較簡單:

    注意,如果storagePool.memoryUsed小於storageRegionSize,則說明在maybeGrowPool中調用freeSpaceToShrinkPool方法時未能成功釋放delta大小的內存。此時,computeMaxExecutionPoolSize方法返回的值會大於執行內存池的實際poolsize。

    3. 調用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 釋放執行內存

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


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. 水平有限,如有錯誤,望讀者指出

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章