當前位置:
首頁 > 最新 > Spark 動態內存分析

Spark 動態內存分析

回顧

在前面的一篇文章中我們介紹了spark靜態內存管理模式以及相關知識點:

在上一篇文章末尾,我們陳述了傳統spark靜態內存管理模式的局限性:

(1)沒有適用於所有應用的默認配置,通常需要開發人員針對不同的應用進行不同的參數配置。比如根據任務的執行邏輯,調整shuffle和storage內存佔比來適應任務的需求。

(2)這樣需要開發人員具備較高的spark原理知識。

(3)那些不cache數據的應用在運行時只佔用一小部分可用內存,因為默認的內存配置中,storage用去了safety內存的60%。

因此,在1.6之後,spark引入了動態(統一)內存管理模式,本文將針對動態內存管理模式的設計理念以及原理進行相關陳述。

總體概覽

spark從1.6版本以後,默認的內存管理方式就調整為統一內存管理模式,由UnifiedMemoryManager實現。

Unified MemoryManagement模型,重點是打破運行內存和存儲內存之間的界限,使spark在運行時,不同用途的內存之間可以實現互相的拆借。由下圖可知,spark每個executor(JVM)內存由一下幾個部分組成:

2 User Memory : 分配Spark Memory剩餘的內存,用戶可以根據需要使用。默認占(Java Heap - Reserved Memory) * 0.25。

內存分布邏輯圖

設計理念

本節將對第二部分各個內存的分布以及設計原理進行詳細的闡述

相對於靜態內存模型(即存儲和運行內存相互隔離、彼此不可拆借),動態內存實現了存儲和計算內存的動態拆借。也就是說,當計算內存超了,它會從空閑的存儲內存中借一部分內存使用,存儲內存不夠用的時候,也會向空閑的計算內存中拆借。值得注意的地方是,被借走用來執行運算的內存,在執行完任務之前是不會釋放內存的。通俗的講,運行任務會借存儲的內存,但是它直到執行完以後才能歸還內存。

和動態內存相關的參數如下:

下面對動態內存設計原理的一些取捨進行分析:

1.當內存壓力上升時候的取捨:

因為內存可以被計算和存儲內存拆借,我們必須明確在這種機制下,當內存壓力上升的時候,我們如何取捨?接下來會從不同維度對下面三個取捨進行分析:

a、傾向於優先釋放計算內存

b、傾向於優先釋放存儲內存

c、不偏不倚,平等競爭

維度1、釋放內存的代價

釋放存儲內存的代價取決於storage level. 如果數據的存儲level是MEMORY_ONLY的話代價最高,因為當你釋放在內存中的數據的時候,你下次再復用的話只能重新計算了。如果數據的存儲level是MEMORY_AND_DIS_SER的時候,釋放內存的代價最低。因為這種方式,當內存不夠的時候,它會將數據序列化後放在磁碟上,避免復用的時候再計算,唯一的開銷只是I/O上。

釋放計算內存的代價不是很顯而易見。這裡沒有復用數據重計算的代價,因為計算內存中的任務數據會被移到硬碟,最後再歸併起來。最近的spark版本將計算的中間數據進行壓縮使得序列化的代價降到了最低。

值得注意的是,移到硬碟的數據總會再重新讀回來,從存儲內存移除的數據也許不會被用到,所以當沒有重新計算的風險時,釋放計算的內存要比釋放存儲內存的代價更高。

維度2、實現複雜度

實現釋放存儲內存的策略很簡單:我們只需要用目前的內存釋放策略釋放掉存儲內存中的數據就好了。

實現釋放計算內存卻相對來說很複雜。這裡有幾個實現該方案的思路:

a、當運行任務要拆借存儲內存的時候,給所有這些任務註冊一個回調函數以便日後調這個函數來回收內存

b、協同投票來進行內存的釋放

值得我們注意的一個地方是,以上無論哪種方式,都需要考慮一個地方:即如果我要釋放正在運行的任務的內存,同時我們想要cache到存儲內存的一部分數據恰巧是由這個任務產生的,如果我們現在釋放掉正在運行的任務的內存,就需要考慮在這種環境下會造成飢餓的情況:即生成cache的數據的任務沒有足夠的內存空間來跑出cache的數據一直處於飢餓狀態。

此外,我們還需要考慮,一旦我們釋放掉計算內存,那麼那些需要cache的數據應該怎麼辦?最簡單的方式就是等待,直到計算內存有足夠的空閑,但是這樣就可能會造成死鎖,尤其是當新的數據塊依賴於之前的計算內存中的數據塊。另一個可選的操作就是丟掉那些最新寫入到磁碟中的塊並且一旦當計算內存夠了又馬上載入回來。為了避免總是丟掉那些等待中的塊,我們可以設置一個小的內存空間(比如堆內存的5%)去確保內存中至少有一定的比例的的數據塊。

所給的兩種方法都會增加額外的複雜度, 這兩種方式在第一次的實現中都被排除了。綜上目前看來,釋放掉存儲內存中的計算任務在實現上比較繁瑣,目前暫不考慮。

結論:我們傾向於優先釋放掉存儲內存。即如果存儲內存拆借了計算內存,當計算內存需要進行計算並且內存空間不足的時候,優先把計算內存中這部分被用來存儲的內存釋放掉。

2.可選設計

可選的幾種設計理念:結合我們前面的描述,針對在內存壓力下釋放存儲內存有以下幾個可選設計。

A: 釋放存儲內存數據塊,完全平滑: 計算和存儲內存共享一片統一的區域。內存壓力上升,優先釋放掉存儲內存部分中的數據。如果壓力沒有緩解,開始將計算內存中運行的任務數據進行溢寫磁碟。

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. 存儲內存沒有上限。

2. 計算內存沒有上限。

3. 保障了存儲空間有一個小的保留區域。

實現類分析-UnifiedMemoryManager

1.闡述該類的幾個主要方法:

(1)acquireExecutionMemory(numBytes:Long,taskAttemptId:Long, memoryMode: MemoryMode)方法:

當前的任務嘗試從executor中獲取numBytes這麼大的內存。該方法直接向ExecutionMemoryPool索要所需內存,索要內存有以下幾個關注點:

程序一直處理該task的請求,直到系統判定無法滿足該請求或者已經為該請求分配到足夠的內存為止。如果當前execution內存池剩餘內存不足以滿足此次請求時,會向storage部分請求釋放出被借走的內存以滿足此次請求。

根據此刻execution內存池的總大小maxPoolSize,以及從memoryForTask中統計出的處於active狀態的task的個數計算出每個task能夠得到的最大內存數maxMemoryPerTask = maxPoolSize / numActiveTasks。每個task能夠得到的最少內存數minMemoryPerTask = poolSize /(2 * numActiveTasks)。

根據申請內存的task當前使用的execution內存大小決定分配給該task多少內存,總的內存不能超過maxMemoryPerTask。但是如果execution內存池能夠分配的最大內存小於numBytes並且如果把能夠分配的內存分配給當前task,但是該task最終得到的execution內存還是小於minMemoryPerTask時,該task進入等待狀態,等其他task申請內存時將其喚醒。如果滿足,就會返回能夠分配的內存數,並且更新memoryForTask,將該task使用的內存調整為分配後的值。一個Task最少需要minMemoryPerTask才能開始執行。

(2)acquireStorageMemory(blockId:BlockId,numBytes:Long, evictedBlocks: mutable.Buffer[(BlockId,BlockStatus)])方法:

流程和acquireExecutionMemory類似,當storage的內存不足時,同樣會向execution借內存,但區別是當且僅當ExecutionMemory有空閑內存時,StorageMemory 才能借走該內存。

能借到的內存數為:valmemoryBorrowedFromExecution = Math.min(onHeapExecutionMemoryPool.memoryFree,numBytes)。所以StorageMemory從ExecutionMemory借走的內存,完全取決於當時ExecutionMemory是不是有空閑內存。借到內存後,storageMemoryPool增加借到的這部分內存,之後同上一樣,會調用StorageMemoryPool的acquireMemory方法,主要如下:

在申請內存時,如果numBytes大於此刻storage內存池的剩餘內存,即if (numBytesToFree > 0),那麼需要storage內存池釋放一部分內存以滿足申請需求。釋放內存後如果memoryFree >= numBytes,就會把這部分內存分配給申請內存的task,並且更新storage內存池的使用情況。同時他與ExecutionMemoryPool不同的是,他不會像前者那樣分不到資源就進行等待,acquireStorageMemory只會返回一個true或是false,告知內存分配是否成功。

總結

結合兩篇文章,我們對spark的兩種內存管理模型都做了一個簡單的介紹,兩者的不同之處也做出了說明,希望這兩篇文章對spark的使用者有一定的幫助,也歡迎大家交流。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 TalkingData 的精彩文章:

2017年最受歡迎文章TOP10

TAG:TalkingData |