做者編輯:王瑋,胡玉林html
在前面的一篇文章中咱們介紹了spark靜態內存管理模式以及相關知識https://blog.csdn.net/anitinaj/article/details/80901328 在上一篇文章末尾,咱們陳述了傳統spark靜態內存管理模式的侷限性:
(1) 沒有適用於全部應用的默認配置,一般須要開發人員針對不一樣的應用進行不一樣的參數配置。好比根據任務的執行邏輯,調整shuffle和storage內存佔比來適應任務的需求。
(2) 這樣須要開發人員具有較高的spark原理知識。
(3) 那些不cache數據的應用在運行時只佔用一小部分可用內存,由於默認的內存配置中,storage用去了safety內存的60%。
所以,在1.6以後,spark引入了動態(統一)內存管理模式,本文將針對動態內存管理模式的設計理念以及原理進行相關陳述。web
spark從1.6版本之後,默認的內存管理方式就調整爲統一內存管理模式。由UnifiedMemoryManager實現。Unified Memory Management模型,重點是打破運行內存和存儲內存之間的界限,使spark在運行時,不一樣用途的內存之間能夠實現互相的拆借。
由下圖可知,spark每一個executor(JVM)內存由一下幾個部分組成:緩存
本節將對第二部分各個內存的分佈以及設計原理進行詳細的闡述
相對於靜態內存模型(即存儲和運行內存相互隔離、彼此不可拆借),動態內存實現了存儲和計算內存的動態拆借。也就是說,當計算內存超了,它會從空閒的存儲內存中借一部份內存使用,存儲內存不夠用的時候,也會向空閒的計算內存中拆借。值得注意的地方是,被借走用來執行運算的內存,在執行完任務以前是不會釋放內存的。通俗的講,運行任務會借存儲的內存,可是它直到執行完之後才能歸還內存。
和動態內存相關的參數以下:數據結構
下面對動態內存設計原理的一些取捨進行分析:app
由於內存能夠被計算和存儲內存拆借,咱們必須明確在這種機制下,當內存壓力上升的時候,咱們如何取捨?接下來會從不一樣維度對下面三個取捨進行分析:
a、傾向於優先釋放計算內存
b、傾向於優先釋放存儲內存
c、不偏不倚,平等競爭jvm
維度一、釋放內存的代價函數
釋放存儲內存的代價取決於storage level. 若是數據的存儲level是MEMORY_ONLY的話代價最高,由於當你釋放在內存中的數據的時候,你下次再複用的話只能從新計算了。若是數據的存儲level是MEMORY_AND_DIS_SER的時候,釋放內存的代價最低。由於這種方式,當內存不夠的時候,它會將數據序列化後放在磁盤上,避免複用的時候再計算,惟一的開銷只是I/O上。
釋放計算內存的代價不是很顯而易見。這裏沒有複用數據重計算的代價,由於計算內存中的任務數據會被移到硬盤,最後再歸併起來。最近的spark版本將計算的中間數據進行壓縮使得序列化的代價降到了最低。
值得注意的是,移到硬盤的數據總會再從新讀回來,從存儲內存移除的數據也許不會被用到,因此當沒有從新計算的風險時,釋放計算的內存要比釋放存儲內存的代價更高。ui
維度二、實現複雜度url
實現釋放存儲內存的策略很簡單:咱們只須要用目前的內存釋放策略釋放掉存儲內存中的數據就行了。
實現釋放計算內存卻相對來講很複雜。這裏有幾個實現該方案的思路:
a、當運行任務要拆借存儲內存的時候,給全部這些任務註冊一個回調函數以便往後調這個函數來回收內存
b、協同投票來進行內存的釋放spa
值得咱們注意的一個地方是,以上不管哪一種方式,都須要考慮一個地方:即若是我要釋放正在運行的任務的內存,同時咱們想要cache到存儲內存的一部分數據恰巧是由這個任務產生的,若是咱們如今釋放掉正在運行的任務的內存,就須要考慮在這種環境下會形成飢餓的狀況:即生成cache的數據的任務沒有足夠的內存空間來跑出cache的數據一直處於飢餓狀態。
此外,咱們還須要考慮,一旦咱們釋放掉計算內存,那麼那些須要cache的數據應該怎麼辦?最簡單的方式就是等待,直到計算內存有足夠的空閒,可是這樣就可能會形成死鎖,尤爲是當新的數據塊依賴於以前的計算內存中的數據塊。另外一個可選的操做就是丟掉那些最新寫入到磁盤中的塊而且一旦當計算內存夠了又立刻加載回來。爲了不老是丟掉那些等待中的塊,咱們能夠設置一個小的內存空間(好比堆內存的5%)去確保內存中至少有必定的比例的的數據塊。
所給的兩種方法都會增長額外的複雜度, 這兩種方式在第一次的實現中都被排除了。綜上目前看來,釋放掉存儲內存中的計算任務在實現上比較繁瑣,目前暫不考慮。
結論:咱們傾向於優先釋放掉存儲內存。即若是存儲內存拆借了計算內存,當計算內存須要進行計算而且內存空間不足的時候,優先把計算內存中這部分被用來存儲的內存釋放掉。
可選的幾種設計理念:結合咱們前面的描述,針對在內存壓力下釋放存儲內存有如下幾個可選設計。
A: 釋放存儲內存數據塊,徹底平滑: 計算和存儲內存共享一片統一的區域。內存壓力上升,優先釋放掉存儲內存部分中的數據。若是壓力沒有緩解,開始將計算內存中運行的任務數據進行溢寫磁盤。
B:釋放存儲內存數據塊,靜態存儲空間預留:這種設計和A設計很像,不一樣的是會專門劃分一個預留存儲內存區域。在這個內存區域內,存儲內存不會被釋放,只有當存儲內存超出這個預留區域,纔會被釋放。這個參數由spark.memory.storageFraction 配置。
C:釋放存儲內存數據塊,動態存儲空間預留:這種設計於設計B很類似,可是存儲空間的那一部分區域再也不是靜態設置的了,而是動態分配。這樣設置帶來的不一樣是計算內存能夠儘量借走存儲內存中可用的部分。
結論:最終採用的的是設計C。
設計A被拒絕的緣由是:設計A不適合那些對cache內存重度依賴的saprk任務。
設計B被拒絕的緣由是:設計B在不少狀況下須要用戶去設置存儲內存中那部分最小的區域。另外不管咱們設置一個什麼值,只要它非0,那麼計算內存最終也會達到一個上限,好比,若是咱們將其設置爲0.6,那麼有效的執行內存就是堆內存的0.4 * 0.75=0.3,那麼若是用戶沒有cache數據,或是cache的數據達不到設置的0.6,那麼這種狀況就又回到了靜態內存模型那種狀況,並無改善什麼。
設計C:設計C就避免了B中的問題,只要存儲內存有空餘的,那麼計算內存就能夠借用,須要關注的問題是當計算內存已經使用了存儲內存中的全部可用內存可是又須要cache數據的時候應該怎麼處理。最先的版本中直接釋放最新的block來避免引入執行驅趕策略的複雜性
同時設計C是惟一一個同時知足下列條件的:
闡述該類的幾個主要方法:
1. acquireExecutionMemory(numBytes: Long, taskAttemptId: Long, memoryMode: MemoryMode)方法:當前的任務嘗試從executor中獲取numBytes這麼大的內存。該方法直接向ExecutionMemoryPool索要所需內存,索要內存有如下幾個關注點:
val maxMemoryPerTask = maxPoolSize / numActiveTasks
和 `val minMemoryPerTask = poolSize / (2numActiveTasks)`其中maxPoolSize= maxMemory(storage+execution的最大內存) - math.min(storageMemoryUsed, storageRegionSize),poolSize= 當前這個pool的大小。而maxPoolSize也表明了execution pool的最大內存。val memoryReclaimableFromStorage =math.max(storageMemoryPool.memoryFree, storageMemoryPool.poolSize - storageRegionSize)
取決於StorageMemoryPool的剩餘內存和 storageMemoryPool 從ExecutionMemory借來的內存哪一個大,取最大的那個,做爲能夠從新歸還的最大內存。用公式表達出來就是這一個樣子:ExecutionMemory 能借到的最大內存= StorageMemory 借的內存 + StorageMemory 空閒內存固然,若是實際須要的小於可以借到的最大值,則以實際須要值爲準。val spaceToReclaim = storageMemoryPool.freeSpaceToShrinkPool( math.min(extraMemoryNeeded, memoryReclaimableFromStorage))
2. acquireStorageMemory(blockId: BlockId,numBytes: Long, evictedBlocks: mutable.Buffer[(BlockId, BlockStatus)])方法:
結合兩篇文章,咱們對spark的兩種內存管理模型都作了一個簡單的介紹,二者的不一樣之處也作出了說明,但願這兩篇文章對spark的使用者有必定的幫助,也歡迎你們交流。
參考內容: