原文地址: https://0x0fff.com/spark-memory-management/
從Apache Spark 1.6開始,內存管理模型就發生了變化了。以前的內存管理模型是通過StaticMemoryManager 實現的,而現在,默認情況下,并不會使用這個內存管理模型。也就是說,即使你在Spark 1.5.x和Spark 1.6.0上運行相同的代碼,結果也會是不同的。這一點要特別小心。
當然,你還是可以手動啟用舊的內存管理模型的。只需要指定這么一個參數spark.memory.useLegacyMode
在前面的文章中,我已經描述過舊的Spark內存管理模型(譯者注:譯者同樣翻譯過這篇文章,鏈接為Spark架構)。而且,還寫了Spark Shuffle implementations這篇文章,來簡單描述不同的shuffle策略對內存的使用。
而在這篇文章中,我們會描述Spark 1.6.0中出現的新的內存管理模型。它是通過UnifiedMemoryManager實現的。
簡而言之,新的內存管理模型,如下所示:
在上圖中,我們會看到,總共有三個區域:
- Reserved Memory。如名字所示,這塊內存是留給系統使用的。它的大小是硬編碼的,我們無法改變它的大小,除非重新編譯源代碼,或者使用
spark.testing.reservedMemory
這個配置,而毫無疑問,后者是我們不推薦使用的,畢竟只是一個測使用的配置項。
在Spark 1.6.0中,它的大小是300MB,也就是說,我們分配的堆內存中的300MB,在我們計算程序可用內存時,不應該計算在內。你不可能用全部的堆內存來緩沖數據,因為這塊內存的存在。當然這塊內存也不是劃分出來,啥事都不做。它會保存很多Spark的內部使用到的對象。
在給Spark executor設置堆內存時,如果你分配的堆內存小于1.5 * Reserved Memory=450MB
,那么,會直接甩你一個please use larger heap size然后轉身離開。 - User Memory。這塊內存,如名字所示,完全取決于用戶。你可以用它存儲一些你在RDD transformation中用到的數據結構。舉個栗子,如果你想寫通過使用
mapPartitions
實現一個聚集函數,并且內部維護一個Hash表,那么,這個Hash表就會被存儲在User Memory中。
在Spark 1.6.0中,這塊內存的大小,可以通過(Java Heap – Reserved Memory) * (1.0 – spark.memory.fraction)
計算得出,也就是說,默認情況下,是(Java Heap – 300MB) * 0.25
。如果我們給executor分配了4GB的堆內存,那么,User Memory將是949MB。
再重申一遍,這塊內存,怎么使用,完全取決于用戶。Spark不會幫你監管它。所以,如果你不清楚你的User Memory的大小,而存儲了一個比它大的對象,那么,很可能會導致OOM。 - Spark Memory。這塊內存是Spark管理的內存,它的大小是
(Java Heap – Reserved Memory) * spark.memory.fraction
,默認情況下,是(Java Heap – 300MB) * 0.75
。如果我們給Executor分配4GB的堆內存,那么,這塊內存有2847MB。
整個Spark Memory被分成兩部分,Storage Memory和Execution Memory。這兩者,各占大小,通過spark.memory.storageFraction
這個參數來配置,默認情況,是0.5,也就是五五開。
在新的內存管理模型下,Storage Memory和Execution Memory的大小并不是固定的。
我們下面介紹Storage Memory和Execution Memory是如何被使用的:
- Storage Memory。這塊內存會被Spark用來緩存數據,以及臨時存放序列化過的數據。全部的broadcast變量,都會作為緩存,存儲在這塊區域中。如果你感興趣,可以讀一下這份代碼unroll。你可能會看到,對于unrolled block,如果內存不夠,沒關系,只要persistence level允許,它會直接把unrolled partition放到硬盤里。全部的broadcast變量,會以MEMORY_AND_DISK的persistence level,存放在緩存中。
- Execution Memory.這塊內存,Spark會用來存儲執行Task時產生的對象。例如,它會存儲Shuffle時,Map端產生的中間對象(譯者疑問:那Reducer端存放在哪兒呢?前幾天就是Shuffle時Reducer內存爆掉了),它也會存儲Hash Aggregation時,內部需要用到的HashTable。當內存不足時,會自動刷到磁盤上。但是,其它的線程(Task),不能強制收回內存。
好,接下來我們介紹Storage Memory and Execution Memory
之間的內存重新分配問題。
從上面對Execution Memory
的介紹中,我們可以看到,我們不能強制回收Execution Memory
中的內存。因為Execution Memory
中存儲的,都是運行Task時需要的對象,如果回收掉,那么Task就不能正常運行了。但是Storage Memory
就不一樣了,它只是緩存,即使我們回收了這些內存,我們只需要簡單的更新元數據,告訴它這塊內存被刷到磁盤上或者已經被移除掉了,然后當我們再次訪問這些緩存數據時,Spark會直接從磁盤上讀(如果persistence level不允許刷到磁盤上,那么會重新計算。)
所以,我們完全可以回收掉Storage Memory,將它們劃給Execution Meomry使用。那到底在什么情況下,Execution Memory可以使用Storage Memory呢?
只要發生下面兩種情況中的一個即可:
- Storage Memory中有空閑的內存。比如說,緩存的數據并沒有用了Storage Memory中全部的內存。那我們就可以將剩下的給Execution Memory使用。
- Storage Memory超過了剛開始給它分配的大小,并且這些內存全部被使用了。當發生這種情況時,
current storage memroy size - intial storage memory size
這些內存,都會被強制刷到磁盤上。
而與此相反,只有當Execution Memory有空閑的時,Storage Memory才能使用。
Initial Storage Memory size,可以通過下面的公式計算Spark Memory * spark.memory.fraction * spark.memory.storageFraction = (Java Heap – Reserved Memory) * spark.memory.fraction * spark.memory.storageFraction
(譯者注,原文這里少了spark.memory.fraction)。在默認情況下,是(Java Heap – 300MB) * 0.75 * 0.5 = (Java Heap – 300MB) * 0.375
。如果Executor的堆內存有4GB,那Initial Storage Memory size是1423.5MB
所以,如果你要放到Storage Memory中的緩存的大小,要跟initial Storage Memory size一樣大,甚至比它還大。而Execution Memory使用的內存,比Execution Memory的initial size還大,并且,此時Execution Memory已經壓榨了Storage Memory的內存,使它不能放下全部的緩存。那么,Execution Memory并不會說,"抱歉,我騰出來點給你吧"。而是直接就一把掌,"滾犢子"。Storage Memory就只能委屈的用此時僅有的那點內存,只有當Execution Memory主動釋放了一部分內存以后,它才能占用。
后記
原文中,有很多有價值的提問,所以建議還是讀原文。并附帶看一下提問。
其他鏈接
Spark內存管理詳解
[Spark性能調優] Spark Shuffle 中 JVM 內存使用及配置內幕詳情
Spark study notes: core concepts visualized