歡迎關(guān)注我的微信公眾號(hào):FunnyBigData
作為打著 “內(nèi)存計(jì)算” 旗號(hào)出道的 Spark,內(nèi)存管理是其非常重要的模塊。作為使用者,搞清楚 Spark 是如何管理內(nèi)存的,對(duì)我們編碼、調(diào)試及優(yōu)化過(guò)程會(huì)有很大幫助。本文之所以取名為 "Spark 內(nèi)存管理的前世今生" 是因?yàn)樵?Spark 1.6 中引入了新的內(nèi)存管理方案,而在之前一直使用舊方案。
剛剛提到自 1.6 版本引入了新的內(nèi)存管理方案,但并不是說(shuō)在 1.6 及之后的版本中不能使用舊的方案,而是默認(rèn)使用新方案。我們可以通過(guò)設(shè)置 spark.memory.userLegacyMode
值來(lái)選擇,該值為 false
表示使用新方案,true
表示使用舊方案,默認(rèn)為 false
。該值是如何發(fā)揮作用的呢?如下:
val useLegacyMemoryManager = conf.getBoolean("spark.memory.useLegacyMode", false)
val memoryManager: MemoryManager =
if (useLegacyMemoryManager) {
new StaticMemoryManager(conf, numUsableCores)
} else {
UnifiedMemoryManager(conf, numUsableCores)
}
根據(jù) spark.memory.useLegacyMode
值的不同,會(huì)創(chuàng)建 MemoryManager 不同子類的實(shí)例:
- 值為
false
:創(chuàng)建UnifiedMemoryManager
類實(shí)例,為新的內(nèi)存管理的實(shí)現(xiàn) - 值為
true
:創(chuàng)建StaticMemoryManager
類實(shí)例,為舊的內(nèi)存管理的實(shí)現(xiàn)
不管是在新方案中還是舊方案中,都根據(jù)內(nèi)存的不同用途,都包含三大塊。
- storage 內(nèi)存:用于緩存 RDD、展開(kāi) partition、存放 Direct Task Result、存放廣播變量。在 Spark Streaming receiver 模式中,也用來(lái)存放每個(gè) batch 的 blocks
- execution 內(nèi)存:用于 shuffle、join、sort、aggregation 中的緩存、buffer
storage 和 execution 內(nèi)存都通過(guò) MemoryManager 來(lái)申請(qǐng)和管理,而另一塊內(nèi)存則不受 MemoryManager 管理,主要有兩個(gè)作用:
- 在 spark 運(yùn)行過(guò)程中使用:比如序列化及反序列化使用的內(nèi)存,各個(gè)對(duì)象、元數(shù)據(jù)、臨時(shí)變量使用的內(nèi)存,函數(shù)調(diào)用使用的堆棧等
- 作為誤差緩沖:由于 storage 和 execution 中有很多內(nèi)存的使用是估算的,存在誤差。當(dāng) storage 或 execution 內(nèi)存使用超出其最大限制時(shí),有這樣一個(gè)安全的誤差緩沖在可以大大減小 OOM 的概率
這塊不受 MemoryManager 管理的內(nèi)存,由系統(tǒng)預(yù)留以及 storage 和 execution 安全系數(shù)之外的內(nèi)存組成,這個(gè)會(huì)在下文中詳述。
接下來(lái),讓我們先來(lái)看看 “前世”
前世
舊方案的內(nèi)存結(jié)構(gòu)如下圖所示:
讓我們結(jié)合上圖做進(jìn)一步說(shuō)明:
execution 內(nèi)存
execution 最大可用內(nèi)存為 jvm space * spark.storage.memoryFraction * spark.storage.safetyFraction
,默認(rèn)為 jvm space * 0.2 * 0.8
。
spark.shuffle.memoryFraction
很大程度上影響了 spill 的頻率,如果 spill 過(guò)于頻繁,可以適當(dāng)增大 spark.shuffle.memoryFraction
的值,增加用于 shuffle 的內(nèi)存,減少Spill的次數(shù)。這樣一來(lái)為了避免內(nèi)存溢出,可能需要減少 storage 的內(nèi)存,即減小spark.storage.memoryFraction
的值,這樣 RDD cache 的容量減少,在某些場(chǎng)景下可能會(huì)對(duì)性能造成影響。
由于 shuffle 數(shù)據(jù)的大小是估算出來(lái)的(這主要為了減少計(jì)算數(shù)據(jù)大小的時(shí)間消耗),會(huì)存在誤差,當(dāng)實(shí)際使用的內(nèi)存比估算大的時(shí)候,這里 spark.shuffle.safetyFraction
用來(lái)作為一個(gè)保險(xiǎn)系數(shù),增加一定的誤差緩沖,降低實(shí)際內(nèi)存占用超過(guò)用戶配置值的可能性。所以 execution 真是最大可用的內(nèi)存為 0.2*0.8=0.16
。shuffle 時(shí),一旦 execution 內(nèi)存使用超過(guò)該比例,就會(huì)進(jìn)行 spill。
storage 內(nèi)存
storage 最大可用內(nèi)存為 jvm space * spark.storage.memoryFraction * spark.storage.safetyFraction
,默認(rèn)為 jvm space * 0.6 * 0.9
。
由于在 cache block 時(shí)大小也是估算的,所以也需要一個(gè)保險(xiǎn)系數(shù)用來(lái)防止誤差引起 OOM,即 spark.storage.safetyFraction
,所以真實(shí)能用來(lái)進(jìn)行 memory cache block 的內(nèi)存大小的比例為 0.6*0.9=0.54
。一旦 storage 使用內(nèi)存超過(guò)該比例,將根據(jù) StorageLevel 決定不緩存 block 還是 OOM 或是存儲(chǔ)到磁盤。
storage 內(nèi)存中有 spark.shuffle.unrollFraction
的部分是用來(lái) unroll,即用于 “展開(kāi)” 一個(gè) partition 的數(shù)據(jù),這部分默認(rèn)為 0.2
不由 MemoryManager 管理的內(nèi)存
系統(tǒng)預(yù)留的大小為:1 - spark.storage.memoryFraction - spark.shuffle.memoryFraction
,默認(rèn)為 0.2。另一部分是 storage 和 execution 保險(xiǎn)系數(shù)之外的內(nèi)存大小,默認(rèn)為 0.1。
存在的問(wèn)題
舊方案最大的問(wèn)題是 storage 和 execution 的內(nèi)存大小都是固定的,不可改變,即使 execution 有大量的空閑內(nèi)存且 storage 內(nèi)存不足,storage 也無(wú)法使用 execution 的內(nèi)存,只能進(jìn)行 spill,反之亦然。所以,在很多情況下存在資源浪費(fèi)。
另外,舊方案中,只有 execution 內(nèi)存支持 off heap,storage 內(nèi)存不支持 off heap。
今生
上面我們提到舊方案的兩個(gè)不足之處,在新方案中都得到了解決,即:
- 新方案 storage 和 execution 內(nèi)存可以互相借用,當(dāng)一方內(nèi)存不足可以向另一方借用內(nèi)存,提高了整體的資源利用率
- 新方案中 execution 內(nèi)存和 storage 內(nèi)存均支持 off heap
這兩點(diǎn)將在后文中進(jìn)一步展開(kāi),我們先來(lái)看看新方案中,默認(rèn)的內(nèi)存結(jié)構(gòu)是怎樣的?依舊分為三塊(這里將 storage 和 execution 內(nèi)存放在一起講):
- 不受 MemoryManager 管理內(nèi)存,由以下兩部分組成:
- 系統(tǒng)預(yù)留:大小默認(rèn)為
RESERVED_SYSTEM_MEMORY_BYTES
,即 300M,可以通過(guò)設(shè)置spark.testing.reservedMemory
改變,一般只有測(cè)試的時(shí)候才會(huì)設(shè)置該配置,所以我們可以認(rèn)為系統(tǒng)預(yù)留大小為 300M。另外,executor 的最小內(nèi)存限制為系統(tǒng)預(yù)留內(nèi)存的 1.5 倍,即 450M,若 executor 的總內(nèi)存大小小于 450M,則會(huì)拋出異常 - storage、execution 安全系數(shù)外的內(nèi)存:大小為
(heap space - RESERVED_SYSTEM_MEMORY_BYTES)*(1 - spark.memory.fraction)
,默認(rèn)為(heap space - 300M)* 0.4
- 系統(tǒng)預(yù)留:大小默認(rèn)為
- storage + execution:storage、execution 內(nèi)存之和又叫 usableMemory,總大小為
(heap space - 300) * spark.memory.fraction
,spark.memory.fraction
默認(rèn)為 0.6。該值越小,發(fā)生 spill 和 block 踢除的頻率就越高。其中:- storage 內(nèi)存:默認(rèn)占其中 50%(包含 unroll 部分)
- execution 內(nèi)存:默認(rèn)同樣占其中 50%
由于新方案是 1.6 后默認(rèn)的內(nèi)存管理方案,也是目前絕大部分 spark 用戶使用的方案,所以我們有必要更深入且詳細(xì)的展開(kāi)分析。
初探統(tǒng)一內(nèi)存管理類
在最開(kāi)始我們提到,新方案是由 UnifiedMemoryManager
實(shí)現(xiàn)的,我們先來(lái)看看該類的成員及方法,類圖如下:
通過(guò)這個(gè)類圖,我想告訴你這幾點(diǎn):
- UnifiedMemoryManager 具有 4 個(gè) MemoryPool,分別是堆內(nèi)的 onHeapStorageMemoryPool 和 onHeapExecutionMemoryPool 以及堆外的 offHeapStorageMemoryPool 和 offHeapExecutionMemoryPool(其中,execution 和 storage 使用堆外內(nèi)存的方式不同,后面會(huì)講到)
- UnifiedMemoryManager 申請(qǐng)、釋放 storage、execution、unroll 內(nèi)存的方法(看起來(lái)像廢話)
- tungstenMemoryAllocator 會(huì)根據(jù)不同的 MemoryMode 來(lái)生成不同的 MemoryAllocator
- 若 MemoryMode 為 ON_HEAP 為 HeapMemoryAllocator
- 若 MemoryMode 為 OFF_HEAP 則為 UnsafeMemoryAllocator(使用 unsafe api 來(lái)申請(qǐng)堆外內(nèi)存)
如何申請(qǐng) storage 內(nèi)存
有了上面的這些基礎(chǔ)知識(shí),再來(lái)看看是怎么申請(qǐng) storage 內(nèi)存的。申請(qǐng) storage 內(nèi)存是通過(guò)調(diào)用
UnifiedMemoryManager#acquireStorageMemory(blockId: BlockId,
numBytes: Long,
memoryMode: MemoryMode): Boolean
更具體的說(shuō)法應(yīng)該是為某個(gè) block(blockId 指定)以那種內(nèi)存模式(on heap 或 off heap)申請(qǐng)多少字節(jié)(numBytes)的 storage 內(nèi)存,該函數(shù)的主要流程如下圖:
對(duì)于上圖,還需要做一些補(bǔ)充來(lái)更好理解:
MemoryMode
- 如果 MemoryMode 是 ON_HEAP,那么 executionMemoryPool 為 onHeapExecutionMemoryPool、storageMemoryPool 為 onHeapStorageMemoryPool。maxMemory 為
(jvm space - 300M)* spark.memory.fraction
,如果你還記得的話,這在文章最開(kāi)始的時(shí)候有介紹 - 如果 MemoryMode 是 OFF_HEAP,那么 executionMemoryPool 為 offHeapExecutionMemoryPool、storageMemoryPool 為 offHeapMemoryPool。maxMemory 為 maxOffHeapMemory,由
spark.memory.offHeap.size
指定,由 execution 和 storage 共享
要向 execution 借用多少?
計(jì)算要向 execution 借用多少內(nèi)存的代碼如下:
val memoryBorrowedFromExecution = Math.min(executionPool.memoryFree, numBytes)
為 execution 空閑內(nèi)存和申請(qǐng)內(nèi)存 size 的較小值,這說(shuō)明了兩點(diǎn):
- 能借用到的內(nèi)存大小可能是小于申請(qǐng)的內(nèi)存大小的(當(dāng)
executionPool.memoryFree < numBytes
),更進(jìn)一步說(shuō),成功借用到的內(nèi)存加上 storage 原本空閑的內(nèi)存之和有可能還是小于要申請(qǐng)的內(nèi)存大小 - execution 只可能把自己當(dāng)前空閑的內(nèi)存借給 storage,即使在這之前 execution 已經(jīng)從 storage 借來(lái)了大量?jī)?nèi)存,也不會(huì)釋放自己已經(jīng)使用的內(nèi)存來(lái) “還” 給 storage。execution 這么不講道理是因?yàn)橐獙?shí)現(xiàn)釋放 execution 內(nèi)存來(lái)歸還給 storage 復(fù)雜度太高,難以實(shí)現(xiàn)
還有一點(diǎn)需要注意的是,借用是發(fā)生在相同 MemoryMode 的 storageMemoryPool 和 executionMemoryPool 之間,不能在不同的 MemoryMode 間進(jìn)行借用
借到了就萬(wàn)事大吉?
當(dāng) storage 空閑內(nèi)存不足以分配申請(qǐng)的內(nèi)存時(shí),從上面的分析我們知道會(huì)向 execution 借用,借來(lái)后是不是就萬(wàn)事大吉了?當(dāng)然······不是,前面也提到了即使借到了內(nèi)存也可能還不夠,這也是上圖中紅色圓框中問(wèn)號(hào)的含義,在我們?cè)龠M(jìn)一步跟進(jìn)到 StorageMemoryPool#acquireMemory(blockId: BlockId, numBytes: Long): Boolean
中一探究竟,該函數(shù)主要流程如下:
同樣,對(duì)于上面這個(gè)流程圖需要做一些說(shuō)明:
計(jì)算要釋放的內(nèi)存量
val numBytesToFree = math.max(0, numAcquireBytes - memoryFree)
如上,要釋放的內(nèi)存大小為再?gòu)?execution 借用了內(nèi)存,使得 storage 空閑內(nèi)存增大 n(n>=0) 后,還比申請(qǐng)的內(nèi)存少的那部分內(nèi)存,若借用后 storage 空閑內(nèi)存足以滿足申請(qǐng)的大小,則 numBytesToFree 為 0,無(wú)需進(jìn)行釋放
如何釋放 storage 內(nèi)存?
釋放的方式是踢除已緩存的 blocks,實(shí)現(xiàn)為 evictBlocksToFreeSpace(blockId: Option[BlockId], space: Long, memoryMode: MemoryMode): Long
,有以下幾個(gè)原則:
- 只能踢除相同 MemoryMode 的 block
- 不能踢除屬于同一個(gè) RDD 的另一個(gè) block
首先會(huì)進(jìn)行預(yù)踢除(所謂預(yù)踢除就是計(jì)算假設(shè)踢除該 block 能釋放多少內(nèi)存),預(yù)踢除的具體邏輯是:遍歷一個(gè)已緩存到內(nèi)存的 blocks 列表(該列表按照緩存的時(shí)間進(jìn)行排列,約早緩存的在越前面),逐個(gè)計(jì)算預(yù)踢除符合原則的 block 是否滿足以下條件之一:
- 預(yù)踢除的累計(jì)總大小滿足要踢除的大小
- 所有的符合原則的 blocks 都被預(yù)踢除
若最終預(yù)踢除的結(jié)果是可以滿足要提取的大小,則對(duì)預(yù)踢除中記錄的要踢除的 blocks 進(jìn)行真正的踢除。具體的方式是:如果從內(nèi)存中踢除后,還具有其他 StorageLevel 或在其他節(jié)點(diǎn)有備份,依然保留該 block 信息;若無(wú),則刪除該 block 信息。最終,返回踢除的總大小(可能稍大于要踢除的大小)。
若最終預(yù)踢除的結(jié)果是無(wú)法滿足要提取的大小,則不進(jìn)行任何實(shí)質(zhì)性的踢除,直接返回踢除size 為 0。需要再次提醒的是,只能踢除相同 MemoryMode 的 block。
以上,結(jié)合兩幅流程圖及相應(yīng)的說(shuō)明,相信你已經(jīng)搞清楚如何申請(qǐng) storage 內(nèi)存了。我們?cè)賮?lái)看看 execution 內(nèi)存是如何申請(qǐng)的
如何申請(qǐng) execution 內(nèi)存
我們知道,申請(qǐng) storage 內(nèi)存是為了 cache 一個(gè) numBytes 的 block,結(jié)果要么是申請(qǐng)成功、要么是申請(qǐng)失敗,不存在申請(qǐng)到的內(nèi)存數(shù)比 numBytes 少的情況,這是因?yàn)椴荒軐?block 一部分放內(nèi)存,一部分 spill 到磁盤。但申請(qǐng) execution 內(nèi)存則不同,申請(qǐng) execution 內(nèi)存是通過(guò)調(diào)用
UnifiedMemoryManager#acquireExecutionMemory(numBytes: Long,
taskAttemptId: Long,
memoryMode: MemoryMode): Long
來(lái)實(shí)現(xiàn)的,這里的 numBytes 是指至多 numBytes,最終申請(qǐng)的內(nèi)存數(shù)比 numBytes 少也是成功的,比如在 shuffle write 的時(shí)候使用的時(shí)候,如果申請(qǐng)?的內(nèi)存不夠,則進(jìn)行 spill。
另一個(gè)特點(diǎn)是,申請(qǐng) execution 時(shí)可能會(huì)一直阻塞,這是為了能確保每個(gè) task 在進(jìn)行 spill 之前都能占用至少 1/2N 的 execution pool 內(nèi)存數(shù)(N 為 active tasks 數(shù))。當(dāng)然,這也不是能完全確保的,比如 tasks 數(shù)激增但老的 tasks 還沒(méi)釋放內(nèi)存就不能滿足。
接下來(lái),我們來(lái)看看如何申請(qǐng) execution 內(nèi)存,流程圖如下:
從上圖可以看到,整個(gè)流程還是挺復(fù)雜的。首先,我先對(duì)上圖中的一些環(huán)節(jié)進(jìn)行進(jìn)一步說(shuō)明以幫助理解,最后再以簡(jiǎn)潔的語(yǔ)言來(lái)概括一下整個(gè)過(guò)程。
MemoryMode
同樣,不同的 MemoryMode 的情況是不同的,如下:
- 如果 MemoryMode 為 ON_HEAP:
- executionMemoryPool 為 onHeapExecutionMemoryPool
- storageMemoryPool 為 onHeapStorageMemoryPool
- storageRegionSize 為 onHeapStorageRegionSize,即
(heap space - 300M) * spark.memory.storageFraction
- maxMemory 為 maxHeapMemory,即
(heap space - 300M)
- 如果 MemoryMode 為 OFF_HEAP:
- executionMemoryPool 為 offHeapExecutionMemoryPool
- storageMemoryPool 為 offHeapStorageMemoryPool
- maxMemory 為 maxOffHeapMemory,即
spark.memory.offHeap.size
- storageRegionSize 為 offHeapStorageRegionSize,即
maxOffHeapMemory * spark.memory.storageFraction
這一小節(jié)描述的內(nèi)容非常重要,因?yàn)橹笏械牧鞒潭际腔诖耍吹胶竺娴牧鞒虝r(shí),還記著會(huì)有 ON_HEAP 和 OFF_HEAP 兩種情況
maybeGrowExecutionPool(向 storage 借用內(nèi)存)
只有當(dāng) executionMemoryPool 的空閑內(nèi)存不足以滿足申請(qǐng)的 numBytes 時(shí),該函數(shù)才會(huì)生效。那這個(gè)函數(shù)是怎么向 storage 借用內(nèi)存的呢?流程如下:
- 計(jì)算可從 storage 回收的內(nèi)存 memoryReclaimableFromStorage,為 storage 當(dāng)前的空閑內(nèi)存和之前 storage 從 execution 借走的內(nèi)存中較大的那個(gè)
- 如果 memoryReclaimableFromStorage 為 0,說(shuō)明之前 storage 沒(méi)有從 execution 這邊借用過(guò)內(nèi)存并且 storage 自己已經(jīng)把內(nèi)存用完了,沒(méi)有任何內(nèi)存可以借給 execution,那么本次借用就失敗,直接返回;如果 memoryReclaimableFromStorage 大于 0,則進(jìn)入下一步
- 計(jì)算本次真正要借用的內(nèi)存 spaceToReclaim,即 execution 不足的內(nèi)存(申請(qǐng)的內(nèi)存減去 execution 的空閑內(nèi)存)與 memoryReclaimableFromStorage 中的較小值。原則是即使能借更多,也只借夠用的就行
- 執(zhí)行借用操作,如果需要 storage 的空閑內(nèi)存和之前 storage 從 execution 借用的的內(nèi)存加起來(lái)才能滿足,則會(huì)進(jìn)行踢除 cached blocks
以上就是整個(gè) execution 向 storage 借用內(nèi)存的過(guò)程,與 storage 向 execution 借用最大的不同是:execution 會(huì)踢除 storage 已經(jīng)使用的向 execution 的內(nèi)存,踢除的流程在文章的前面有描述。這是因?yàn)椋@本來(lái)就是屬于 execution 的內(nèi)存并且通過(guò)踢除來(lái)實(shí)現(xiàn)歸還實(shí)現(xiàn)上也不復(fù)雜
一個(gè) task 能使用多少 execution 內(nèi)存?
也就是流程圖中的 maxMemoryPerTask 和 minMemoryPerTask 是如何計(jì)算的,如下:
val maxPoolSize = computeMaxExecutionPoolSize()
val maxMemoryPerTask = maxPoolSize / numActiveTasks
val minMemoryPerTask = poolSize / (2 * numActiveTasks)
maxPoolSize 為從 storage 借用了內(nèi)存后,executionMemoryPool 的最大可用內(nèi)存,maxMemoryPerTask 和 minMemoryPerTask 的計(jì)算方式也如代碼所示。這樣做是為了使得每個(gè) task 使用的內(nèi)存都能維持在 1/2*numActiveTasks ~ 1/numActiveTasks
范圍內(nèi),使得在整體上能保持各個(gè) task 資源占用比較均衡并且一定程度上允許需要更多資源的 task 在一定范圍內(nèi)能分配到更多資源,也照顧到了個(gè)性化的需求
最后到底分配多少 execution 內(nèi)存?
首先要計(jì)算兩個(gè)值:
- 最大可以分配多少,即 maxToGrant:是申請(qǐng)的內(nèi)存量與
(maxMemoryPerTask-已為該 task 分配的內(nèi)存值)
中的較小值,如果maxMemoryPerTask < 已為該 task 分配的內(nèi)存值
,則直接為 0,也就是之前已經(jīng)給該 task 分配的夠多了 - 本次循環(huán)真正可以分配多少,即 toGrant:maxToGrant 與當(dāng)前 executionMemoryPool 空閑內(nèi)存(注意是借用后)的較小值
所以,本次最終能分配的量也就是 toGrant,如果 toGrant 加上已經(jīng)為該 task 分配的內(nèi)存量之和
還小于 minMemoryPerTask 并且 toGrant 小于申請(qǐng)的量,則就會(huì)觸發(fā)阻塞。否則,分配 toGrant 成功,函數(shù)返回。
阻塞釋放的條件有兩個(gè),如下:
- 有 task 釋放了內(nèi)存:更具體的說(shuō)是有 task 釋放了相同 MemoryMode 的 execution 內(nèi)存,這時(shí)空閑的 execution 內(nèi)存變多了
- 有新 task 申請(qǐng)了內(nèi)存:同樣,更具體的說(shuō)是有新 task 申請(qǐng)了相同 MemoryMode 的 execution 內(nèi)存,這時(shí) numActiveTasks 變大了,minMemoryPerTask 則變小了
用簡(jiǎn)短的話描述整個(gè)過(guò)程如下:
- 申請(qǐng) execution 內(nèi)存時(shí),會(huì)循環(huán)不停的嘗試,每次嘗試都會(huì)看是否需要從 storage 中借用或回收之前借給 storage 的內(nèi)存(這可能會(huì)觸發(fā)踢除 cached blocks),如果需要?jiǎng)t進(jìn)行借用或回收;
- 之后計(jì)算本次循環(huán)能分配的內(nèi)存,
- 如果能分配的不夠申請(qǐng)的且該 task 累計(jì)分配的(包括本次)小于每個(gè) task 應(yīng)該獲得的最小值(1/2*numActiveTasks),則會(huì)阻塞,直到有新的 task 申請(qǐng)內(nèi)存或有 task 釋放內(nèi)存為止,然后進(jìn)入下一次循環(huán);
- 否則,直接返回本次分配的值
使用建議
首先,建議使用新模式,所以接下來(lái)的配置建議都是基于新模式的。
-
spark.memory.fraction
:如果 application spill 或踢除 block 發(fā)生的頻率過(guò)高(可通過(guò)日志觀察),可以適當(dāng)調(diào)大該值,這樣 execution 和 storage 的總可用內(nèi)存變大,能有效減少發(fā)生 spill 和踢除 block 的頻率 -
spark.memory.storageFraction
:為 storage 占 storage、execution 內(nèi)存總和的比例。雖然新方案中 storage 和 execution 之間可以發(fā)生內(nèi)存借用,但總的來(lái)說(shuō),spark.memory.storageFraction
越大,運(yùn)行過(guò)程中,storage 能用的內(nèi)存就會(huì)越多。所以,如果你的 app 是更吃 storage 內(nèi)存的,把這個(gè)值調(diào)大一點(diǎn);如果是更吃 execution 內(nèi)存的,把這個(gè)值調(diào)小一點(diǎn) -
spark.memory.offHeap.enabled
:堆外內(nèi)存最大的好處就是可以避免 GC,如果你希望使用堆外內(nèi)存,將該值置為 true 并設(shè)置堆外內(nèi)存的大小,即設(shè)置spark.memory.offHeap.size
,這是必須的
另外,需要特別注意的是,堆外內(nèi)存的大小不會(huì)算在 executor memory 中,也就是說(shuō)加入你設(shè)置了 --executor memory 10G
和 spark.memory.offHeap.size=10G
,那總共可以使用 20G 內(nèi)存,堆內(nèi)和堆外分別 10G。
總結(jié)&引子
到這里,已經(jīng)比較籠統(tǒng)的介紹了 Spark 內(nèi)存管理的 “前世”,也比較細(xì)致的介紹了 “今生”。篇幅比較長(zhǎng),但沒(méi)有一大段一大段的代碼,應(yīng)該還算比較好懂。如果看到這里,希望你多少能有所收獲。
然后,請(qǐng)你在大致回顧下這篇文章,有沒(méi)有覺(jué)得缺了點(diǎn)什么?是的,是缺了點(diǎn)東西,所謂 “內(nèi)存管理” 怎么就沒(méi)看到具體是怎么分配內(nèi)存的呢?是怎么使用的堆外內(nèi)存?storage 和 execution 的堆外內(nèi)存使用方式會(huì)不會(huì)不同?execution 和 storage 又是怎么使用堆內(nèi)內(nèi)存的呢?以怎么樣的數(shù)據(jù)結(jié)構(gòu)呢?
如果你想搞清楚這些問(wèn)題,關(guān)注公眾號(hào)并回復(fù) “內(nèi)存管理下”。