Spark Executor 內存管理

主要對 Executor 的內存管理進行分析,下文中的 Spark 內存均特指 Executor 的內存

堆內內存和堆外內存

作為一個 JVM 進程,Executor 的內存管理建立在 JVM 的內存管理之上,此外spark還引入了堆外內存(不在JVM中的內存),在spark中是指不屬于該executor的內存。

  • 堆內內存:
    由 JVM 控制,由GC(垃圾回收)進行內存回收

  • 堆外內存:
    不受 JVM 控制,可以自由分配

堆外內存的優點: 減少了垃圾回收的工作。

堆外內存的缺點:

  • 堆外內存難以控制,如果內存泄漏,那么很難排查
  • 堆外內存相對來說,不適合存儲很復雜的對象。一般簡單的對象或者扁平化的比較適合。

堆內內存

堆內內存的大小,由 Spark 應用程序啟動時的 executor-memory 或 spark.executor.memory 參數配置,這些配置在 spark-env.sh 配置文件中。

Executor 內運行的并發任務共享 JVM 堆內內存,這些內存被規劃為 存儲(Storage)內存執行(Execution)內存

  • Storage 內存:
    用于存儲 RDD 的緩存數據 和 廣播(Broadcast)數據,主要用于存儲 spark 的 cache 數據,例如RDD的緩存

  • Execution 內存:
    執行 Shuffle 時占用的內存,主要用于存放 Shuffle、Join、Sort 等計算過程中的臨時數據

  • 用戶內存(User Memory):
    主要用于存儲 RDD 轉換操作所需要的數據,例如 RDD 依賴等信息

  • 預留內存(Reserved Memory):
    系統預留內存,會用來存儲Spark內部對象。

剩余的部分不做特殊規劃,那些 Spark 內部的對象實例,或者用戶定義的 Spark 應用程序中的對象實例,均占用剩余的空間。

Spark 對堆內內存的管理是一種邏輯上的”規劃式”的管理,因為對象實例占用內存的申請和釋放都由 JVM 完成,Spark 只能在申請后和釋放前記錄這些內存。

對于 Spark 中序列化的對象,由于是字節流的形式,其占用的內存大小可直接計算,而對于非序列化的對象,其占用的內存是通過周期性地采樣近似估算而得,這種方法降低了時間開銷但是有可能誤差較大,導致某一時刻的實際內存有可能遠遠超出預期。此外,在被 Spark 標記為釋放的對象實例,很有可能在實際上并沒有被 JVM 回收,導致實際可用的內存小于 Spark 記錄的可用內存。所以 Spark 并不能準確記錄實際可用的堆內內存,從而也就無法完全避免內存溢出(OOM, Out of Memory)的異常。

Spark 通過對存儲內存和執行內存各自獨立的規劃管理,可以決定是否要在存儲內存里緩存新的 RDD,以及是否為新的任務分配執行內存。
如果當前 Exector 內存不夠用,可以分配到其他內存占用小的 Exector 上。
在一定程度上可以提升其他 Exector 的內存利用率,減少當前 Exector 異常的出現。

堆外內存

為了進一步優化內存的使用以及提高 Shuffle 時排序的效率,Spark 1.6 引入了堆外(Off-heap)內存,使之可以直接在工作節點的系統內存中開辟空間,存儲經過序列化的二進制數據。

這種模式不在 JVM 內申請內存,而是調用 Java 的 unsafe 相關 API 進行諸如 C 語言里面的 malloc() 直接向操作系統申請內存,由于這種方式不進過 JVM 內存管理,所以可以避免頻繁的 GC,這種內存申請的缺點是必須自己編寫內存申請和釋放的邏輯。

Spark 可以直接操作系統堆外內存,減少了不必要的內存開銷,以及頻繁的 GC 掃描和回收,提升了處理性能。堆外內存可以被精確地申請和釋放,而且序列化的數據占用的空間可以被精確計算,所以相比堆內內存來說降低了管理的難度,也降低了誤差。

在默認情況下堆外內存并不啟用,可通過配置 spark.memory.offHeap.enabled 參數啟用,并由 spark.memory.offHeap.size 參數設定堆外空間的大小,單位為字節。堆外內存與堆內內存的劃分方式相同,所有運行中的并發任務共享存儲內存和執行內存。

如果堆外內存被啟用,那么 Executor 內將同時存在堆內和堆外內存,兩者的使用互補影響,這個時候 Executor 中的 Execution 內存是堆內的 Execution 內存和堆外的 Execution 內存之和,同理,Storage 內存也一樣。相比堆內內存,堆外內存只區分 Execution 內存和 Storage 內存。

spark內存分配

靜態內存管理

在 Spark 最初采用的靜態內存管理機制下,存儲內存、執行內存和其他內存的大小在 Spark 應用程序運行期間均為固定的,但用戶可以應用程序啟動前進行配置,堆內內存的分配如圖 所示:

可用堆內內存空間計算:

  • 可用的存儲內存 = systemMaxMemory * spark.storage.memoryFraction * spark.storage.safetyFraction

  • 可用的執行內存 = systemMaxMemory * spark.shuffle.memoryFraction * spark.shuffle.safetyFraction

靜態內存管理圖示——堆外

統一內存管理

Spark 1.6 之后引入的統一內存管理機制,與靜態內存管理的區別在于存儲內存和執行內存共享同一塊空間,可以動態占用對方的空閑區域,如圖 所示

統一內存管理圖示——堆內

reservedMemory 在 Spark 2.2.1 中是寫死的

統一內存管理圖示——堆外

其中最重要的優化在于動態占用機制,其規則如下:

  • 程序提交的時候我們都會設定基本的 Execution 內存和 Storage 內存區域(通過 spark.memory.storageFraction 參數設置);

  • 在程序運行時,如果雙方的空間都不足時,則存儲到硬盤;將內存中的塊存儲到磁盤的策略是按照 LRU 規則進行的。若己方空間不足而對方空余時,可借用對方的空間;(存儲空間不足是指不足以放下一個完整的 Block)

  • Execution 內存的空間被對方占用后,可讓對方將占用的部分轉存到硬盤,然后"歸還"借用的空間,Storage 占用 Execution 內存的數據被回收后,重新計算即可恢復。

  • Storage 內存的空間被對方占用后,目前的實現是無法讓對方"歸還",因為需要考慮 Shuffle 過程中的很多因素,實現起來較為復雜;而且 Shuffle 過程產生的文件在后面一定會被使用到

動態占用機制圖示

Task 之間內存分布

為了更好地使用使用內存,Executor 內運行的 Task 之間共享著 Execution 內存。具體的,Spark 內部維護了一個 HashMap 用于記錄每個 Task 占用的內存。當 Task 需要在 Execution 內存區域申請 numBytes 內存,其先判斷 HashMap 里面是否維護著這個 Task 的內存使用情況,如果沒有,則將這個 Task 內存使用置為0,并且以 TaskId 為 key,內存使用為 value 加入到 HashMap 里面。之后為這個 Task 申請 numBytes 內存,如果 Execution 內存區域正好有大于 numBytes 的空閑內存,則在 HashMap 里面將當前 Task 使用的內存加上 numBytes,然后返回;如果當前 Execution 內存區域無法申請到每個 Task 最小可申請的內存,則當前 Task 被阻塞,直到有其他任務釋放了足夠的執行內存,該任務才可以被喚醒。每個 Task 可以使用 Execution 內存大小范圍為 1/2N ~ 1/N,其中 N 為當前 Executor 內正在運行的 Task 個數。一個 Task 能夠運行必須申請到最小內存為 (1/2N * Execution 內存);當 N = 1 的時候,Task 可以使用全部的 Execution 內存。

比如如果 Execution 內存大小為 10GB,當前 Executor 內正在運行的 Task 個數為5,則該 Task 可以申請的內存范圍為 10 / (2 * 5) ~ 10 / 5,也就是 1GB ~ 2GB的范圍。

示例

1. 只用了堆內內存

現在我們提交的 Spark 作業關于內存的配置如下:
--executor-memory 18g
由于沒有設置 spark.memory.fraction(Storage 和 Execution 共用內存 占可用內存的比例,默認為0.6) 和 spark.memory.storageFraction(Storage 內存占 Storage 和 Execution 共用內存 比例,默認0.5) 參數,我們可以看到 Spark UI 關于 Storage Memory 的顯示如下:

上圖很清楚地看到 Storage Memory 的可用內存是 10.1GB,這個數是咋來的呢?根據前面的規則,我們可以得出以下的計算:

systemMemory = spark.executor.memory
reservedMemory = 300MB
usableMemory = systemMemory - reservedMemory
StorageMemory= usableMemory * spark.memory.fraction * spark.memory.storageFraction

把數據代進去,得出結果為:5.312109375 GB。

和上面的 10.1GB 對不上。為什么呢?這是因為 Spark UI 上面顯示的 Storage Memory 可用內存其實等于 Execution 內存和 Storage 內存之和,也就是 usableMemory * spark.memory.fraction

我們設置了 --executor-memory 18g,但是 Spark 的 Executor 端通過 Runtime.getRuntime.maxMemory 拿到的內存其實沒這么大,只有 17179869184 字節,這個數據是怎么計算的?
Runtime.getRuntime.maxMemory 是程序能夠使用的最大內存,其值會比實際配置的執行器內存的值小。這是因為內存分配池的堆部分分為 Eden,Survivor 和 Tenured 三部分空間,而這里面一共包含了兩個 Survivor 區域,而這兩個 Survivor 區域在任何時候我們只能用到其中一個,所以我們可以使用下面的公式進行描述:

ExecutorMemory = Eden + 2 * Survivor + Tenured
Runtime.getRuntime.maxMemory =  Eden + Survivor + Tenured
2. 用了堆內和堆外內存

現在如果我們啟用了堆外內存,情況咋樣呢?我們的內存相關配置如下:

spark.executor.memory           18g
spark.memory.offHeap.enabled    true
spark.memory.offHeap.size       10737418240

從上面可以看出,堆外內存為 10GB,現在 Spark UI 上面顯示的 Storage Memory 可用內存為 20.9GB,如下:

總結

憑借統一內存管理機制,Spark 在一定程度上提高了堆內和堆外內存資源的利用率,降低了開發者維護 Spark 內存的難度,但并不意味著開發者可以高枕無憂。譬如,所以如果存儲內存的空間太大或者說緩存的數據過多,反而會導致頻繁的 GC 垃圾回收,降低任務執行時的性能。

使用建議

首先,建議使用新模式,所以接下來的配置建議都是基于新模式的。

  • spark.memory.fraction:如果 application spill 或踢除 block 發生的頻率過高(可通過日志觀察),可以適當調大該值,這樣 execution 和 storage 的總可用內存變大,能有效減少發生 spill 和踢除 block 的頻率

  • spark.memory.storageFraction:為 storage 占 storage、execution 內存總和的比例。雖然新方案中 storage 和 execution 之間可以發生內存借用,但總的來說,spark.memory.storageFraction 越大,運行過程中,storage 能用的內存就會越多。所以,如果你的 app 是更吃 storage 內存的,把這個值調大一點;如果是更吃 execution 內存的,把這個值調小一點

  • spark.memory.offHeap.enabled:堆外內存最大的好處就是可以避免 GC,如果你希望使用堆外內存,將該值置為 true 并設置堆外內存的大小,即設置
    spark.memory.offHeap.size,這是必須的

另外,需要特別注意的是,堆外內存的大小不會算在 executor memory 中,也就是說加入你設置了 --executor memory 10G 和 -spark.memory.offHeap.size=10G,那總共可以使用 20G 內存,堆內和堆外分別 10G。

參考

http://www.lxweimin.com/p/d626a4f53d00

https://www.iteblog.com/archives/2342.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,238評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,430評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,134評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,893評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,653評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,136評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,372評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,888評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,738評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,179評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,588評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,829評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,610評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,916評論 2 372

推薦閱讀更多精彩內容