Spark内存管理(3)—— 统一内存管理设计理念
Spark內存管理系列文章:?
Spark內存管理(1)—— 靜態內存管理?
Spark內存管理(2)—— 統一內存管理
在本文中,將會對各個內存的分布以及設計原理進行詳細的闡述?
相對于靜態內存模型(即Storage和Execution相互隔離、彼此不可拆借),動態內存實現了存儲和計算內存的動態拆借:
-
也就是說,當計算內存超了,它會從空閑的存儲內存中借一部分內存使用
-
存儲內存不夠用的時候,也會向空閑的計算內存中拆借
值得注意的地方是:
-
被借走用來執行運算的內存,在執行完任務之前是不會釋放內存的
-
通俗的講,運行任務會借存儲的內存,但是它直到執行完以后才能歸還內存
和動態內存相關的參數
-
spark.memory.fraction?
Spark 1.6.1 默認0.75,Spark 2.2.0 默認0.6?
這個參數用來配置存儲和計算內存占整個可用內存的比例?
這個參數設置的越低,也就是存儲和計算內存占可用的比例越低,就越可能頻繁的發生內存的釋放(將內存中的數據寫磁盤或者直接丟棄掉)?
反之,如果這個參數越高,發生釋放內存的可能性就越小?
這個參數的目的是在jvm中留下一部分空間用來保存spark內部數據,用戶數據結構,并且防止對數據的錯誤預估可能造成OOM的風險,這就是Other部分 -
spark.memory.storageFraction?
默認 0.5;在統一內存中存儲內存所占的比例,默認是0.5,如果使用的存儲內存超過了這個范圍,緩存的數據會被驅趕 -
spark.memory.useLegacyMode?
默認false;設置是否使用saprk1.5及以前遺留的內存管理模型,即靜態內存模型,前面的文章介紹過這個,主要是設置以下幾個參數:- spark.storage.memoryFraction
- spark.storage.safetyFraction
- spark.storage.unrollFraction
- spark.shuffle.memoryFraction
- spark.shuffle.safetyFraction
動態內存設計中的取舍
因為內存可以被Execution和Storage拆借,我們必須明確在這種機制下,當內存壓力上升的時候,該如何進行取舍?從三個角度進行分析:
-
傾向于優先釋放計算內存
-
傾向于優先釋放存儲內存
-
不偏不倚,平等競爭
1.釋放內存的代價
釋放存儲內存的代價取決于Storage Level.:
-
如果數據的存儲level是MEMORY_ONLY的話代價最高,因為當你釋放在內存中的數據的時候,你下次再復用的話只能重新計算了
-
如果數據的存儲level是MEMORY_AND_DIS_SER的時候,釋放內存的代價最低?
因為這種方式,當內存不夠的時候,它會將數據序列化后放在磁盤上,避免復用的時候再計算,唯一的開銷在I/O
綜述:?
釋放計算內存的代價不是很顯而易見:
-
這里沒有復用數據重計算的代價,因為計算內存中的任務數據會被移到硬盤,最后再歸并起來(后面會有文章介紹到這點)
-
最近的spark版本將計算的中間數據進行壓縮使得序列化的代價降到了最低
值得注意的是:
-
移到硬盤的數據總會再重新讀回來
-
從存儲內存移除的數據也許不會被用到,所以當沒有重新計算的風險時,釋放計算的內存要比釋放存儲內存的代價更高(假使計算內存部分剛好用于計算任務的時候)
2.實現復雜度
-
實現釋放存儲內存的策略很簡單:我們只需要用目前的內存釋放策略釋放掉存儲內存中的數據就好了
-
實現釋放計算內存卻相對來說很復雜
這里有2個釋放計算內存的思路:
-
當運行任務要拆借存儲內存的時候,給所有這些任務注冊一個回調函數以便日后調這個函數來回收內存
-
協同投票來進行內存的釋放
值得我們注意的一個地方是,以上無論哪種方式,都需要考慮一種特殊情況:
-
即如果我要釋放正在運行的計算任務的內存,同時我們想要cache到存儲內存的一部分數據恰巧是由這個計算任務產生的
-
此時,如果我們現在釋放掉正在運行的任務的計算內存,就需要考慮在這種環境下會造成的饑餓情況:即生成cache的數據的計算任務沒有足夠的內存空間來跑出cache的數據,而一直處于饑餓狀態(因為計算內存已經不夠了,再釋放計算內存更加不可取)
-
此外,我們還需要考慮:一旦我們釋放掉計算內存,那么那些需要cache的數據應該怎么辦?有2種方案:
- 最簡單的方式就是等待,直到計算內存有足夠的空閑,但是這樣就可能會造成死鎖,尤其是當新的數據塊依賴于之前的計算內存中的數據塊的時候
- 另一個可選的操作就是丟掉那些最新的正準備寫入到磁盤中的塊并且一旦當計算內存夠了又馬上加載回來。為了避免總是丟掉那些等待中的塊,我們可以設置一個小的內存空間(比如堆內存的5%)去確保內存中至少有一定的比例的的數據塊
綜述:?
所給的兩種方法都會增加額外的復雜度,這兩種方式在第一次的實現中都被排除了?
綜上目前看來,釋放掉存儲內存中的計算任務在實現上比較繁瑣,目前暫不考慮?
即計算內存借了存儲內存用來計算任務,然后釋放,這種不考慮;計算內存借來內存之后,是可以不還的
結論:?
我們傾向于優先釋放掉存儲內存?
即如果存儲內存拆借了計算內存,當計算內存需要進行計算并且內存空間不足的時候,優先把計算內存中這部分被用來存儲的內存釋放掉
可選設計
1.設計方案
結合我們前面的描述,針對在內存壓力下釋放存儲內存有以下幾個可選設計:
-
釋放存儲內存數據塊,完全平滑?
計算和存儲內存共享一片統一的區域,沒有進行統一的劃分:- 內存壓力上升,優先釋放掉存儲內存部分中的數據
- 如果壓力沒有緩解,開始將計算內存中運行的任務數據進行溢寫磁盤
-
釋放存儲內存數據塊,靜態存儲空間預留,存儲空間的大小是定死的?
這種設計和1設計很像,不同的是會專門劃分一個預留存儲內存區域:在這個內存區域內,存儲內存不會被釋放,只有當存儲內存超出這個預留區域,才會被釋放(即超過50%了就被釋放,當然50%為默認值)。這個參數由spark.memory.storageFraction(默認值為0.5,即計算和存儲內存的分割線) 配置 -
釋放存儲內存數據塊,動態存儲空間預留?
這種設計于設計2很相似,但是存儲空間的那一部分區域不再是靜態設置的了,而是動態分配;這樣設置帶來的不同是計算內存可以盡可能借走存儲內存中可用的部分,因為存儲內存是動態分配的
結論:最終采用的的是設計3
2.各個方案的優劣
-
設計1被拒絕的原因?
設計1不適合那些對cache內存重度依賴的saprk任務,因為設計1中只要內存壓力上升就釋放存儲內存 -
設計2被拒絕的原因?
設計2在很多情況下需要用戶去設置存儲內存中那部分最小的區域?
另外無論我們設置一個具體值,只要它非0,那么計算內存最終也會達到一個上限,比如,如果我們將存儲內存設置為0.6,那么有效的執行內存就是:- Spark 1.6.1 可用內存*0.4*0.75
- Spark 2.2.0 可用內存*0.4*0.6?
那么如果用戶沒有cache數據,或是cache的數據達不到設置的0.6,那么這種情況就又回到了靜態內存模型那種情況,并沒有改善什么
-
最終選擇設計3的原因?
設計3就避免了2中的問題只要存儲內存有空余的情況,那么計算內存就可以借用?
需要關注的問題是:- 當計算內存已經使用了存儲內存中的所有可用內存但是又需要cache數據的時候應該怎么處理
- 最早的版本中直接釋放最新的block來避免引入執行驅趕策略(eviction策略,上述章節中有介紹)的復雜性
同時設計3是唯一一個同時滿足下列條件的:?
1. 存儲內存沒有上限?
2. 計算內存沒有上限?
3. 保障了存儲空間有一個小的保留區域
總結
以上是生活随笔為你收集整理的Spark内存管理(3)—— 统一内存管理设计理念的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spark内存管理(2)—— 统一内存管
- 下一篇: 史上最全Java面试266题:算法+缓存