spark 源碼分析之二十二-- Task的內存管理


問題的提出

本篇文章將回答如下問題:

1.  spark任務在執行的時候,其內存是如何管理的?

2. 堆內內存的尋址是如何設計的?是如何避免由於JVM的GC的存在引起的內存地址變化的?其內部的內存緩存池回收機制是如何設計的?

3. 堆外和堆內內存分別是通過什么來分配的?其數據的偏移量是如何計算的?

4. 消費者MemoryConsumer是什么?

5. 數據在內存頁中是如何尋址的?

 

單個任務的內存管理是由 org.apache.spark.memory.TaskMemoryManager 來管理的。

TaskMemoryManager

它主要是負責管理單個任務的內存。

首先內存分為堆外內存和堆內內存。

對於堆外內存,可以內存地址直接使用64位長整型地址尋址。

對於堆內內存,內存地址由一個base對象和一個offset對象組合起來表示。

類在設計的過程中遇到的問題:

對於其他結構內部的結構的地址的保存是存在問題的,比如在hashmap或者是 sorting buffer 中的記錄的指針,盡管我們決定使用128位來尋址,我們不能只存base對象的地址,因為由於gc的存在,這個地址不能保證是穩定不變的。(由於分代回收機制的存在,內存中的對象會不斷移動,每次移動,對象內存地址都會改變,但這對於不關注對象地址的開發者來說,是透明的)

最終的方案:

對於堆外內存,只保存其原始地址,因為堆外內存不受gc影響;對於堆內內存,我們使用64位的高13位來保存內存頁數,低51位來保存這個頁中的offset,使用page表來保存base對象,其在page表中的索引就是該內存的內存頁數。頁數最多有8192頁,理論上允許索引 8192 * (2^31 -1)* 8 bytes,相當於140TB的數據。其中 2^31 -1 是整數的最大值,因為page表中記錄索引的是一個long型數組,這個數組的最大長度是2^31 -1。實際上沒有那么大。因為64位中除了用來設計頁數和頁內偏移量外還用於存放數據的分區信息。

MemoryLocation

其中這個base對象和offset對象被封裝進了 MemoryLocation對象中,也就是說,這個類就是用來內存尋址的,如下:

其唯一實現類為 org.apache.spark.unsafe.memory.MemoryBlock。

MemoryBlock

它表示一段連續的內存塊,包括一個起始位置和一個固定大小。起始位置有MemoryLocation來表示。

也就是說它有四個屬性:

這段連續內存塊的起始地址:從父類繼承而來的base對象和offset。

固定大小 length以及對這個內存塊的唯一標識 - 內存頁碼(page number)

 

主要方法如下,其中Platform是跟操作系統有關的一個類,不做過多說明。

MemoryAllocator

其主要負責內存的申請工作。這個接口的實現類是真正分配內存的。后面介紹的TaskMemoryManager只是負責管理內存,但是不負責具體的內存分配事宜。

其繼承關系如下,有兩個子類:

其定義的主要的常量和方法如下:

主要方法主要用來分配和釋放內存塊。下面主要來看一下它兩個子類的實現。

HeapMemoryAllocator

全稱:org.apache.spark.unsafe.memory.HeapMemoryAllocator

主要負責分配堆內內存,其主要分配long型數組,最大分配內存為16GB。

成員變量

bufferPoolBySize是一個HashMap,其內部的value里面存放的數據都是弱引用類型的數據,在JVM 發生GC時,數據可能會被回收。它里面存放的數據都是已經不用的廢棄掉的內存塊。

是否使用內存緩存池

申請的內存塊的大小大於閥值才使用內存緩存池。

分配內存

思路:首先根據bytes大小計算處words的大小,然后字節對齊計算出對齊需要的字節,斷言對齊后的字節大小大於等於之前未對齊的字節大小。為什么要對齊呢?因為長整型數組的內存大小是對齊的。

如果對齊后的字節大小滿足使用緩存池的條件,則先從緩存池中彈出對應的pool,並且如果彈出的pool不為空,則逐一取出之前釋放的數組,並將其封裝進MmeoryBlock對象,並且使用標志位清空之前的歷史數據返回之。

否則,則初始化指定的words長度的長整型數組,並將其封裝進MmeoryBlock對象,並且使用標志位清空之前的歷史數據返回之。總之緩存的是長整型數組,存放數據的也是長整型數組。

釋放內存

 

首先把要釋放的內存數據使用free標志位覆蓋,pageNumber置為占位的page number。

然后取出其內部的長整型數組賦值給臨時變量,並且把base對象置為null,offset置為0。

取出的長整型數組計算其對齊大小,內存頁的大小不一定等於數組的長度 * 8,此時的size是內存頁的大小,需要進行對齊操作。

對齊之后的內存頁大小如果滿足緩存池條件,則將其暫存緩存池,等待下次回收再用或者JVM的GC回收。

這個方法結束之后,這個長整型數組被LinkedList對象(即pool)引用,但這是一個若引用,所以說,現在這個數組是一個游離對象,當JVM回收時,會回收它。

對堆內內存的總結

對於堆內內存上的數據真實受JVM的GC影響,其真實數據的內存地址會發生改變,巧妙使用數組這種容器以及偏移量巧妙地將這個問題規避了,數據回收也可以使用緩存池機制來減少數組頻繁初始化帶來的開銷。其內部使用虛引用來引用釋放的數組,也不會導致無法回收導致內存泄漏。

UnsafeMemoryAllocator

全稱:org.apache.spark.unsafe.memory.UnsafeMemoryAllocator

負責分配堆外內存。

分配內存

思路:底層使用unsafe這個類來分配堆外內存。這里的offset就是操作系統的內存地址,base對象為null。

釋放內存

堆外內存的釋放不能使用緩存池,因為堆外內存不受JVM的管理,將會導致遺留的不用的內存無法回收從而引發更嚴重的內存泄漏,更甚者堆外內存使用的是系統內存,嚴重的話還會導致出現系統級問題。

堆堆外內存的總結

簡言之,對於堆外內存的分配和回收,都是通過java內置的Unsafe類來實現的,其統一規范中的base對象為null,其offset就是該內存頁在操作系統中的真實地址。

 

下面剖析一下TaskMemoryManager的成員變量和核心方法。

進一步剖析TaskMemoryManager

成員變量

下面,先來看一下其成員變量,截圖如下:

對主要的成員變量做如下解釋:

OFFSET_BITS:是指的page number 占用的bit個數

MAXIMUM_PAGE_SIZE_BYTES:約17GB,每頁最大可存內存大小

pageTable:主要用來存放內存頁的

allocatedPages:主要用來追蹤內存頁是否為空的

memoryManager:主要負責Spark內存管理,具體細節可以參照 spark 源碼分析之十五 -- Spark內存管理剖析 做進一步了解。

taskAttemptId:任務id

tungstenMemoryMode:tungsten內存模式,是堆外內存還是堆內內存

consumers:記錄了任務內存的所有消費者

核心方法

所有方法如下:

下面,我們來逐一對其進行源碼剖析。

1. 獲取執行內存

思路:首先先去MemoryManager中去申請執行內存,如果內存不夠,則獲取所有的MemoryConsumer,調用其spill方法將內存數據溢出到磁盤,直到釋放內存空間滿足申請的內存空間則停止spill操作。

2. 釋放執行內存

這其實不是真正意義上的內存釋放,只是管賬的把這筆內存占用划掉了,真正的內存釋放還是需要調用MemoryConsumer的spill方法將內存數據溢出到磁盤來釋放內存。

3. 獲取內存頁大小

 

4. 分配內存頁

思路:首先獲取執行內存。執行內存獲取成功后,找到一個空的內存頁。

如果內存頁碼大於指定的最大頁碼,則釋放剛申請的內存,返回;否則使用MemoryAllocator分配內存頁、初始化內存頁碼並將其放入page表的管理,最后返回page。關於MemoryAllocator分配內存的細節,請參照上文關於其堆內內存或堆外內存的內存分配的詳細剖析。

 

5. 釋放內存頁

思路:首先調用EMmoryAllocator的free 方法來釋放內存,並且調用 方法2 來划掉內存的占用情況。

 

6. 內存地址加密

思路:高13位保存的是page number,低51位保存的是地址的offset

 

7.內存地址解密

思路: 跟 方法6 的編碼思路相反

 

8.根據內存地址獲取內存的base對象,前提是必須是堆內內存頁,否則沒有base對象。

 

9.獲取內存地址在內存頁的偏移量offset

如果是堆內內存,則直接返回其解碼之后的offset即可。

如果是堆外內存,分配內存時的offset + 頁內的偏移量就是真正的偏移量,是針對操作系統的,也是絕對的偏移量。

 

10.清空所有內存頁

思路:使用MemoryAllocator釋放內存,並且請求管賬的MemoryManager釋放執行內存和task的所有內存。

 

11.獲取單個任務的執行內存使用情況

思路:從MemoryManager處獲取指定任務的執行內存使用情況。

 

下面看一下跟TaskMemoryManager交互的消費者對象 -- MemoryConsumer。

MemoryConsumer

類說明

全稱:org.apache.spark.memory.MemoryConsumer

它是任務內存的消費者。

其類結構如下:

成員變量

taskMemoryManager:是負責任務內存管理。

used:表示使用的內存。

mode:表示內存的模式是堆內內存還是堆外內存。

pageSize:表示頁大小。

主要方法

1. 內存數據溢出到磁盤,抽象方法,等待子類實現。

 

2. 申請釋放內存部分,不再做詳細的分析,都是依賴於 TaskMemoryManager 做的操作。

關於更多MemoryConsumer的以及其子類的相關內容,將在下一篇文章Shuffle的寫操作中詳細剖析。

 

總結

本篇文章主要剖析了Task在任務執行時內存的管理相關的內容,現在可能還看不出其重要性,后面在含有sort的shuffle過程中,會頻繁的使用基於內存的sorter,此時的sorter包含大量的數據,是需要內存管理的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM