談到Spark內(nèi)存管理拆祈,估計(jì)大家都會(huì)想到:static memory manager,unified memory manager倘感,execution memory放坏,storage memory,tungsten, task memory manager等一系列模塊老玛。網(wǎng)絡(luò)上介紹這些模塊的文章已經(jīng)非常多淤年,筆者不想一個(gè)個(gè)地系統(tǒng)介紹,只想"漫不經(jīng)心"地談?wù)勂綍r(shí)思考過(guò)的關(guān)于spark內(nèi)存管理的一些問(wèn)題蜡豹,比如:
1. Spark的內(nèi)存管理與JVM的內(nèi)存分配回收機(jī)制有什么區(qū)別和聯(lián)系麸粮?哪些事是spark內(nèi)存管理做的,哪些事是JVM做的镜廉?
2. Spark中用到內(nèi)存的地方有哪些弄诲?存儲(chǔ)內(nèi)存主要消耗在哪些地方?執(zhí)行內(nèi)存主要消耗在哪些地方娇唯?
3. Spark程序出現(xiàn)OOM的可能原因有哪些齐遵?除了用戶(hù)代碼外寂玲,Spark自身框架有哪些環(huán)節(jié)可能出現(xiàn)OOM?
4. Tungsten在內(nèi)存優(yōu)化方面都做了些什么梗摇??jī)?yōu)化了spark的哪些環(huán)節(jié)拓哟?
在“漫談”的過(guò)程中,筆者會(huì)結(jié)合源碼留美,針對(duì)筆者認(rèn)為有必要說(shuō)明的一些問(wèn)題做細(xì)節(jié)分析彰檬。
1 Spark內(nèi)存管理都做了些啥伸刃?
我們知道JVM有自己的內(nèi)存模型和內(nèi)存分配回收機(jī)制谎砾,它會(huì)負(fù)責(zé)與操作系統(tǒng)交互進(jìn)行內(nèi)存的申請(qǐng)和釋放等。那么捧颅,Spark內(nèi)存管理又做了什么呢景图?筆者覺(jué)得它主要做了三件事:
1. 在JVM之上搭建了一套邏輯上的內(nèi)存管理機(jī)制,在spark的存儲(chǔ)和執(zhí)行框架使用JVM堆內(nèi)存之前確保有足夠內(nèi)存空間碉哑。當(dāng)內(nèi)存空間不足時(shí)挚币,spark memory manager的各個(gè)調(diào)用模塊會(huì)采取相應(yīng)的措施,比如ExternalSorter會(huì)在內(nèi)存中不足時(shí)將數(shù)據(jù)spill到disk上扣典。
2. Tungsten構(gòu)建了一套類(lèi)似操作系統(tǒng)內(nèi)存頁(yè)管理的機(jī)制妆毕,用MemoryBlock表示一個(gè)內(nèi)存頁(yè),用自己的page table進(jìn)行管理贮尖,實(shí)現(xiàn)了類(lèi)似操作系統(tǒng)中的虛擬內(nèi)存邏輯地址笛粘,對(duì)(pageNumber, offsetInPage)進(jìn)行編碼生成邏輯地址,統(tǒng)一了on heap和off heap內(nèi)存的訪(fǎng)問(wèn)方式湿硝。
3. Tungsten在off heap模式下會(huì)繞過(guò)JVM使用sun.misc.Unsafe的API直接與操作系統(tǒng)交互薪前,進(jìn)行內(nèi)存的申請(qǐng)和釋放,從而免除了創(chuàng)建JVM對(duì)象帶來(lái)的額外內(nèi)存開(kāi)銷(xiāo)以及GC對(duì)性能的影響关斜。
1.1 Memory Manager
上面#1中的事情主要由MemoryManager (StaticMemoryManager或UnifiedMemoryManager)負(fù)責(zé)示括,它會(huì)利用不同的MemoryPool將內(nèi)存按功能和性質(zhì)區(qū)分開(kāi)來(lái),包括堆內(nèi)存儲(chǔ)內(nèi)存池痢畜,堆外存儲(chǔ)內(nèi)存池垛膝,堆內(nèi)執(zhí)行內(nèi)存池,堆外執(zhí)行內(nèi)存池:
memoryPool記錄了內(nèi)存使用狀態(tài)的各項(xiàng)metrics丁稀,比如最大內(nèi)存吼拥,可用內(nèi)存,已用內(nèi)存等二驰。
MemoryManager提供了幾個(gè)方法供調(diào)用者使用以申請(qǐng)和釋放指定類(lèi)型的內(nèi)存空間:
unroll memory是什么扔罪?
這里重點(diǎn)講一下unroll memory的概念,在《Spark SQL內(nèi)核剖析》上看到對(duì)"unroll"的定義:“將partition由不連續(xù)的存儲(chǔ)空間轉(zhuǎn)換為連續(xù)的存儲(chǔ)空間的過(guò)程”桶雀。
為了說(shuō)明這個(gè)問(wèn)題矿酵,我們先來(lái)看看acquireUnrollMemory方法的一個(gè)調(diào)用全過(guò)程:
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)執(zhí)行時(shí)調(diào)用RDD.iterator獲取指定partition的數(shù)據(jù)迭代器,這個(gè)過(guò)程中的MemoryStore.putIterator會(huì)遍歷指定partition的所有records全肮,獲取每個(gè)value并將其存放在連續(xù)內(nèi)存中:
因?yàn)槭怯玫饕粭l一條record獲取的敞咧,事先并不知道是否有足夠內(nèi)存存放下partition的所有數(shù)據(jù),所以這里的步驟是這樣的:
1. 先向memoryManager申請(qǐng)一份unroll內(nèi)存(初始大小由參數(shù)spark.storage.unrollMemoryThreshold控制辜腺,默認(rèn)為1mb)休建;
2. 然后每讀一條record都會(huì)評(píng)估一下當(dāng)前所需內(nèi)存是否超過(guò)已分配內(nèi)存,如果超過(guò)评疗,則向memoryManager申請(qǐng)額外需要的內(nèi)存测砂。如果申請(qǐng)成功,則繼續(xù)讀取下一個(gè)record百匆,否則就停止unroll砌些,即存儲(chǔ)partition到內(nèi)存失敗。
3. 重復(fù)步驟#2加匈,直到partition所有數(shù)據(jù)都成功unroll存璃,或因內(nèi)存不足而停止unroll.
4. 如果partition所有數(shù)據(jù)都成功unroll,則將unroll memory轉(zhuǎn)化成storage memory :
可以看到雕拼,最終會(huì)release unroll memory并申請(qǐng)storage memory. 我們看一下UnifiedMemoryManager中acquireUnrollMemory和MemoryManager中releaseUnrollMemory的實(shí)現(xiàn):
可以看到纵东,其實(shí)unroll memory和storage memory的申請(qǐng)及釋放調(diào)用的是同樣的方法。
筆者對(duì)unroll memory的理解是:unroll memory和storage memory本質(zhì)上是同一份內(nèi)存啥寇,只是在任務(wù)執(zhí)行的不同階段的不同邏輯表述形式偎球。在partition數(shù)據(jù)的讀取存儲(chǔ)過(guò)程中,這份內(nèi)存叫做unroll memory示姿,而當(dāng)成功讀取存儲(chǔ)了所有reocrd到內(nèi)存中后甜橱,這份內(nèi)存就改了個(gè)名字叫storage memory了。
注意栈戳,unroll memory的概念只存在于spark的存儲(chǔ)模塊中岂傲,在執(zhí)行模塊中是不存在unroll memory的。
不知不覺(jué)已經(jīng)寫(xiě)了不少字子檀,今天先談到這镊掖,未完待續(xù)。
說(shuō)明
1. 本文內(nèi)容及源碼均基于spark 2.4.0之前版本
2. 水平有限褂痰,有誤之處望讀者指出