堆內內存
Spark 1.6之後引入的統一內存管理機制,與靜態內存管理的區別在於Storage和Execution共享同一塊內存空間,可以動態佔用對方的空閒區域
堆內內存
Spark 1.6之後引入的統一內存管理機制,與靜態內存管理的區別在於Storage和Execution共享同一塊內存空間,可以動態佔用對方的空閒區域
其中最重要的優化在於動態佔用機制,其規則如下:
- 設定基本的Storage內存和Execution內存區域(spark.storage.storageFraction參數),該設定確定了雙方各自擁有的空間的範圍
- 雙方的空間都不足時,則存儲到硬盤,若己方空間不足而對方空餘時,可借用對方的空間(存儲空間不足是指不足以放下一個完整的 Block)
- Execution的空間被對方佔用後,可讓對方將佔用的部分轉存到硬盤,然後”歸還”借用的空間
- Storage的空間被對方佔用後,無法讓對方”歸還”,因為需要考慮 Shuffle過程中的很多因素,實現起來較為複雜
動態內存佔用機制
動態佔用機制如下圖所示:
堆內內存
Spark 1.6之後引入的統一內存管理機制,與靜態內存管理的區別在於Storage和Execution共享同一塊內存空間,可以動態佔用對方的空閒區域
其中最重要的優化在於動態佔用機制,其規則如下:
- 設定基本的Storage內存和Execution內存區域(spark.storage.storageFraction參數),該設定確定了雙方各自擁有的空間的範圍
- 雙方的空間都不足時,則存儲到硬盤,若己方空間不足而對方空餘時,可借用對方的空間(存儲空間不足是指不足以放下一個完整的 Block)
- Execution的空間被對方佔用後,可讓對方將佔用的部分轉存到硬盤,然後”歸還”借用的空間
- Storage的空間被對方佔用後,無法讓對方”歸還”,因為需要考慮 Shuffle過程中的很多因素,實現起來較為複雜
動態內存佔用機制
動態佔用機制如下圖所示:
憑藉統一內存管理機制,Spark 在一定程度上提高了堆內和堆外內存資源的利用率,降低了開發者維護 Spark 內存的難度,但並不意味著開發者可以高枕無憂
譬如:如果Storage的空間太大或者說緩存的數據過多,反而會導致頻繁的全量垃圾回收,降低任務執行時的性能,因為緩存的 RDD 數據通常都是長期駐留內存的。所以要想充分發揮 Spark 的性能,需要開發者進一步瞭解存儲內存和執行內存各自的管理方式和實現原理
堆外內存
如下圖所示,相較於靜態內存管理,引入了動態佔用機制
堆內內存
Spark 1.6之後引入的統一內存管理機制,與靜態內存管理的區別在於Storage和Execution共享同一塊內存空間,可以動態佔用對方的空閒區域
其中最重要的優化在於動態佔用機制,其規則如下:
- 設定基本的Storage內存和Execution內存區域(spark.storage.storageFraction參數),該設定確定了雙方各自擁有的空間的範圍
- 雙方的空間都不足時,則存儲到硬盤,若己方空間不足而對方空餘時,可借用對方的空間(存儲空間不足是指不足以放下一個完整的 Block)
- Execution的空間被對方佔用後,可讓對方將佔用的部分轉存到硬盤,然後”歸還”借用的空間
- Storage的空間被對方佔用後,無法讓對方”歸還”,因為需要考慮 Shuffle過程中的很多因素,實現起來較為複雜
動態內存佔用機制
動態佔用機制如下圖所示:
憑藉統一內存管理機制,Spark 在一定程度上提高了堆內和堆外內存資源的利用率,降低了開發者維護 Spark 內存的難度,但並不意味著開發者可以高枕無憂
譬如:如果Storage的空間太大或者說緩存的數據過多,反而會導致頻繁的全量垃圾回收,降低任務執行時的性能,因為緩存的 RDD 數據通常都是長期駐留內存的。所以要想充分發揮 Spark 的性能,需要開發者進一步瞭解存儲內存和執行內存各自的管理方式和實現原理
堆外內存
如下圖所示,相較於靜態內存管理,引入了動態佔用機制
計算公式
spark從1.6版本以後,默認的內存管理方式就調整為統一內存管理模式
由UnifiedMemoryManager實現
Unified MemoryManagement模型,重點是打破運行內存和存儲內存之間的界限,使spark在運行時,不同用途的內存之間可以實現互相的拆借
Reserved Memory
這部分內存是預留給系統使用,在1.6.1默認為300MB,這一部分內存不計算在Execution和Storage中;可通過spark.testing.reservedMemory進行設置;然後把實際可用內存減去這個reservedMemor得到usableMemory
ExecutionMemory 和 StorageMemory 會共享usableMemory * spark.memory.fraction(默認0.75)
注意:
- 在Spark 1.6.1 中spark.memory.fraction默認為0.75
- 在Spark 2.2.0 中spark.memory.fraction默認為0.6
User Memory
分配Spark Memory剩餘的內存,用戶可以根據需要使用
在Spark 1.6.1中,默認佔(Java Heap - Reserved Memory) * 0.25
在Spark 2.2.0中,默認佔(Java Heap - Reserved Memory) * 0.4
Spark Memory
計算方式為:(Java Heap – ReservedMemory) * spark.memory.fraction
在Spark 1.6.1中,默認為(Java Heap - 300M) * 0.75
在Spark 2.2.0中,默認為(Java Heap - 300M) * 0.6
Spark Memory又分為Storage Memory和Execution Memory兩部分
兩個邊界由spark.memory.storageFraction設定,默認為0.5
對比
相對於靜態內存模型(即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拆借,我們必須明確在這種機制下,當內存壓力上升的時候,該如何進行取捨?
從三個角度進行分析:
- 傾向於優先釋放計算內存
- 傾向於優先釋放存儲內存
- 不偏不倚,平等競爭
釋放內存的代價
釋放存儲內存的代價取決於Storage Level.:
- 如果數據的存儲level是MEMORY_ONLY的話代價最高,因為當你釋放在內存中的數據的時候,你下次再複用的話只能重新計算了
- 如果數據的存儲level是MEMORYANDDIS_SER的時候,釋放內存的代價最低,因為這種方式,當內存不夠的時候,它會將數據序列化後放在磁盤上,避免複用的時候再計算,唯一的開銷在I/O
綜述:
釋放計算內存的代價不是很顯而易見:
- 這裡沒有複用數據重計算的代價,因為計算內存中的任務數據會被移到硬盤,最後再歸併起來(後面會有文章介紹到這點)
- 最近的spark版本將計算的中間數據進行壓縮使得序列化的代價降到了最低
值得注意的是:
- 移到硬盤的數據總會再重新讀回來
- 從存儲內存移除的數據也許不會被用到,所以當沒有重新計算的風險時,釋放計算的內存要比釋放存儲內存的代價更高(假使計算內存部分剛好用於計算任務的時候)
實現複雜度
- 實現釋放存儲內存的策略很簡單:我們只需要用目前的內存釋放策略釋放掉存儲內存中的數據就好了
- 實現釋放計算內存卻相對來說很複雜
這裡有2個釋放計算內存的思路:
- 當運行任務要拆借存儲內存的時候,給所有這些任務註冊一個回調函數以便日後調這個函數來回收內存
- 協同投票來進行內存的釋放
值得我們注意的一個地方是,以上無論哪種方式,都需要考慮一種特殊情況:
- 即如果我要釋放正在運行的計算任務的內存,同時我們想要cache到存儲內存的一部分數據恰巧是由這個計算任務產生的
- 此時,如果我們現在釋放掉正在運行的任務的計算內存,就需要考慮在這種環境下會造成的飢餓情況:即生成cache的數據的計算任務沒有足夠的內存空間來跑出cache的數據,而一直處於飢餓狀態(因為計算內存已經不夠了,再釋放計算內存更加不可取)
- 此外,我們還需要考慮:一旦我們釋放掉計算內存,那麼那些需要cache的數據應該怎麼辦?有2種方案:
- 最簡單的方式就是等待,直到計算內存有足夠的空閒,但是這樣就可能會造成死鎖,尤其是當新的數據塊依賴於之前的計算內存中的數據塊的時候
- 另一個可選的操作就是丟掉那些最新的正準備寫入到磁盤中的塊並且一旦當計算內存夠了又馬上加載回來。為了避免總是丟掉那些等待中的塊,我們可以設置一個小的內存空間(比如堆內存的5%)去確保內存中至少有一定的比例的的數據塊
綜述:
所給的兩種方法都會增加額外的複雜度,這兩種方式在第一次的實現中都被排除了
綜上目前看來,釋放掉存儲內存中的計算任務在實現上比較繁瑣,目前暫不考慮
即計算內存借了存儲內存用來計算任務,然後釋放,這種不考慮;計算內存借來內存之後,是可以不還的
結論:
我們傾向於優先釋放掉存儲內存
即如果存儲內存拆借了計算內存,當計算內存需要進行計算並且內存空間不足的時候,優先把計算內存中這部分被用來存儲的內存釋放掉
可選設計
1.設計方案
結合我們前面的描述,針對在內存壓力下釋放存儲內存有以下幾個可選設計:
設計1:釋放存儲內存數據塊,完全平滑
計算和存儲內存共享一片統一的區域,沒有進行統一的劃分
- 內存壓力上升,優先釋放掉存儲內存部分中的數據
- 如果壓力沒有緩解,開始將計算內存中運行的任務數據進行溢寫磁盤
設計2:釋放存儲內存數據塊,靜態存儲空間預留,存儲空間的大小是定死的
這種設計和1設計很像,不同的是會專門劃分一個預留存儲內存區域:在這個內存區域內,存儲內存不會被釋放,只有當存儲內存超出這個預留區域,才會被釋放(即超過50%了就被釋放,當然50%為默認值)。這個參數由spark.memory.storageFraction(默認值為0.5,即計算和存儲內存的分割線)配置
設計3:釋放存儲內存數據塊,動態存儲空間預留
這種設計於設計2很相似,但是存儲空間的那一部分區域不再是靜態設置的了,而是動態分配;這樣設置帶來的不同是計算內存可以儘可能借走存儲內存中可用的部分,因為存儲內存是動態分配的
結論:最終採用的的是設計3
2.各個方案的優劣
設計1被拒絕的原因
設計1不適合那些對cache內存重度依賴的saprk任務,因為設計1中只要內存壓力上升就釋放存儲內存
設計2被拒絕的原因
設計2在很多情況下需要用戶去設置存儲內存中那部分最小的區域 另外無論我們設置一個具體值,只要它非0,那麼計算內存最終也會達到一個上限,比如,如果我們將存儲內存設置為0.6,那麼有效的執行內存就是:
- Spark 1.6.1 可用內存0.40.75
- Spark 2.2.0 可用內存0.40.6
那麼如果用戶沒有cache數據,或是cache的數據達不到設置的0.6,那麼這種情況就又回到了靜態內存模型那種情況,並沒有改善什麼
最終選擇設計3的原因
設計3就避免了2中的問題只要存儲內存有空餘的情況,那麼計算內存就可以借用
需要關注的問題是:
- 當計算內存已經使用了存儲內存中的所有可用內存但是又需要cache數據的時候應該怎麼處理
- 最早的版本中直接釋放最新的block來避免引入執行驅趕策略(eviction策略,上述章節中有介紹)的複雜性
設計3是唯一一個同時滿足下列條件的:
- 存儲內存沒有上限
- 計算內存沒有上限
- 保障了存儲空間有一個小的保留區域