漫談Spark內存管理 (一)

談到Spark內存管理,估計大家都會想到:static memory manager,unified memory manager,execution memory,storage memory,tungsten, task memory manager等一系列模塊。網絡上介紹這些模塊的文章已經非常多,筆者不想一個個地系統介紹,只想"漫不經心"地談談平時思考過的關於spark內存管理的一些問題,比如:

1. Spark的內存管理與JVM的內存分配回收機制有什麼區別和聯繫?哪些事是spark內存管理做的,哪些事是JVM做的?

2. Spark中用到內存的地方有哪些?存儲內存主要消耗在哪些地方?執行內存主要消耗在哪些地方?

3. Spark程序出現OOM的可能原因有哪些?除了用戶代碼外,Spark自身框架有哪些環節可能出現OOM?

4. Tungsten在內存優化方面都做了些什麼?優化了spark的哪些環節?

在“漫談”的過程中,筆者會結合源碼,針對筆者認爲有必要說明的一些問題做細節分析。

1 Spark內存管理都做了些啥?

我們知道JVM有自己的內存模型和內存分配回收機制,它會負責與操作系統交互進行內存的申請和釋放等。那麼,Spark內存管理又做了什麼呢?筆者覺得它主要做了三件事:

1. 在JVM之上搭建了一套邏輯上的內存管理機制,在spark的存儲和執行框架使用JVM堆內存之前確保有足夠內存空間。當內存空間不足時,spark memory manager的各個調用模塊會採取相應的措施,比如ExternalSorter會在內存中不足時將數據spill到disk上。

2. Tungsten構建了一套類似操作系統內存頁管理的機,用MemoryBlock表示一個內存頁,用自己的page table進行管理,實現了類似操作系統中的虛擬內存邏輯地址,對(pageNumber, offsetInPage)進行編碼生成邏輯地址,統一了on heap和off heap內存的訪問方式。

3. Tungsten在off heap模式下會繞過JVM使用sun.misc.Unsafe的API直接與操作系統交互,進行內存的申請和釋放,從而免除了創建JVM對象帶來的額外內存開銷以及GC對性能的影響。

1.1 Memory Manager

上面#1中的事情主要由MemoryManager (StaticMemoryManager或UnifiedMemoryManager)負責,它會利用不同的MemoryPool將內存按功能和性質區分開來,包括堆內存儲內存池,堆外存儲內存池,堆內執行內存池,堆外執行內存池:

memoryPool記錄了內存使用狀態的各項metrics,比如最大內存,可用內存,已用內存等。

MemoryManager提供了幾個方法供調用者使用以申請和釋放指定類型的內存空間:

unroll memory是什麼?

這裏重點講一下unroll memory的概念,在《Spark SQL內核剖析》上看到對"unroll"的定義:“將partition由不連續的存儲空間轉換爲連續的存儲空間的過程”。

爲了說明這個問題,我們先來看看acquireUnrollMemory方法的一個調用全過程:

ShuffleMapTask/ResultTask.runTask -> RDD.iterator -> RDD.getOrCompute -> BlockManager.getOrElseUpdate -> BlockManager.doPutIterator -> MemoryStore.putIteratorAsBytes -> MemoryStore.putIterator -> MemoryStore.reserveUnrollMemoryForThisTask -> MemoryManager.acquireUnrollMemory

可以看到,task(shuffle map task和result task)執行時調用RDD.iterator獲取指定partition的數據迭代器,這個過程中的MemoryStore.putIterator會遍歷指定partition的所有records,獲取每個value並將其存放在連續內存中:

因爲是用迭代器一條一條record獲取的,事先並不知道是否有足夠內存存放下partition的所有數據,所以這裏的步驟是這樣的:

1. 先向memoryManager申請一份unroll內存(初始大小由參數spark.storage.unrollMemoryThreshold控制,默認爲1mb);

2. 然後每讀一條record都會評估一下當前所需內存是否超過已分配內存,如果超過,則向memoryManager申請額外需要的內存。如果申請成功,則繼續讀取下一個record,否則就停止unroll,即存儲partition到內存失敗。

3. 重複步驟#2,直到partition所有數據都成功unroll,或因內存不足而停止unroll.

4. 如果partition所有數據都成功unroll,則將unroll memory轉化成storage memory :

可以看到,最終會release unroll memory並申請storage memory. 我們看一下UnifiedMemoryManager中acquireUnrollMemory和MemoryManager中releaseUnrollMemory的實現:

可以看到,其實unroll memory和storage memory的申請及釋放調用的是同樣的方法。

筆者對unroll memory的理解是:unroll memory和storage memory本質上是同一份內存,只是在任務執行的不同階段的不同邏輯表述形式。在partition數據的讀取存儲過程中,這份內存叫做unroll memory,而當成功讀取存儲了所有reocrd到內存中後,這份內存就改了個名字叫storage memory了。

注意,unroll memory的概念只存在於spark的存儲模塊中,在執行模塊中是不存在unroll memory的。

不知不覺已經寫了不少字,今天先談到這,未完待續。

說明

1. 本文內容及源碼均基於spark 2.4.0之前版本

2. 水平有限,有誤之處望讀者指出

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章