1 現(xiàn)象描述及初步分析
近期公司yarn集群中存在NodeManager因OOM 而掛掉的情況, 且發(fā)生OOM前存在大量的Spark Shuffle Services相關(guān)信息, 通過分析最近Crash的NodeManager進(jìn)程的dump信息發(fā)現(xiàn)存在大量的Finalizer,占用了大部分分的內(nèi)存資源,其中dump分析結(jié)果如下所示:
附:NodeManager日志信息
java.lang.OutOfMemoryError: GC overhead limit exceeded
通過dump分析可知滨砍,OOM原因?yàn)椋篎ileInputStream引發(fā)的Finalizer對(duì)象堆集所致。
該對(duì)象為Shuffle Server在處理Shuffle Client數(shù)據(jù)請(qǐng)求時(shí)所創(chuàng)建捌省。 其中包含如下代碼部分(Spark1.6):
/**
* Sort-based shuffle data uses an index called "shuffle_ShuffleId_MapId_0.index" into a data file
* called "shuffle_ShuffleId_MapId_0.data". This logic is from IndexShuffleBlockResolver,
* and the block id format is from ShuffleDataBlockId and ShuffleIndexBlockId.
*/
private ManagedBuffer getSortBasedShuffleBlockData(
ExecutorShuffleInfo executor, int shuffleId, int mapId, int reduceId) {
File indexFile = getFile(executor.localDirs, executor.subDirsPerLocalDir,
"shuffle_" + shuffleId + "_" + mapId + "_0.index");
DataInputStream in = null;
try {
in = new DataInputStream(new FileInputStream(indexFile));
in.skipBytes(reduceId * 8);
long offset = in.readLong();
long nextOffset = in.readLong();
return new FileSegmentManagedBuffer(
conf,
getFile(executor.localDirs, executor.subDirsPerLocalDir,
"shuffle_" + shuffleId + "_" + mapId + "_0.data"),
offset,
nextOffset - offset);
} catch (IOException e) {
throw new RuntimeException("Failed to open file: " + indexFile, e);
} finally {
if (in != null) {
JavaUtils.closeQuietly(in);
}
}
}
其中 創(chuàng)建FileInputStream的作用是為了拿到索引文件中的數(shù)據(jù)偏移量及文件長(zhǎng)度,用于讀取數(shù)據(jù)。(備注:讀取shuffle數(shù)據(jù)時(shí)也會(huì)創(chuàng)建FileInputStream對(duì)象,上述代碼供獻(xiàn)1/2的對(duì)象數(shù)量佑钾。)并且,該實(shí)現(xiàn)中每次數(shù)據(jù)請(qǐng)求都會(huì)創(chuàng)建一個(gè)FileInputStream對(duì)象用于讀取索引文件烦粒。
2 存在大量FileInputStream相關(guān)Fanalizer的原因
其FileInputStream自定義了finalize()方法,因此JVM會(huì)為每一個(gè)FileInputStream對(duì)象創(chuàng)建一個(gè)Finalizer引用對(duì)象,用于確保FileInputStream最終處理關(guān)閉狀態(tài)代赁。
/**
* Cleans up the connection to the file, and ensures that the
* <code>close</code> method of this file output stream is
* called when there are no more references to this stream.
*
* @exception IOException if an I/O error occurs.
* @see java.io.FileInputStream#close()
*/
protected void finalize() throws IOException {
if (fd != null) {
if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
flush();
} else {
/*
* Finalizer should not release the FileDescriptor if another
* stream is still using it. If the user directly invokes
* close() then the FileDescriptor is also released.
*/
runningFinalize.set(Boolean.TRUE);
try {
close();
} finally {
runningFinalize.set(Boolean.FALSE);
}
}
}
}
所有的Finalizer組成一個(gè)雙向鏈表扰她,其共同維護(hù)一個(gè)ReferenceQueue,進(jìn)入隊(duì)列中的對(duì)象可以被gc回收芭碍。
同時(shí)徒役,JVM中存在一個(gè)守護(hù)線程:FinalizerThread 其優(yōu)先級(jí)為 8,用于從雙向鏈表中清除進(jìn)入ReferenceQueue中的Finalizer窖壕,以便在下次GC時(shí)回收這部分Finalizer忧勿。
資源充足的情況下,F(xiàn)inalizerThread線程可以被調(diào)度執(zhí)行瞻讽,從而ReferenceQueue中的Finalizer會(huì)很快被清理掉鸳吸,從而在GC時(shí)釋放占用內(nèi)存。
而在External Shuffle Services 場(chǎng)景中 Shuffle Server作為NodeManager進(jìn)程中的daemon線程執(zhí)行速勇,并且其創(chuàng)建了大量提供數(shù)據(jù)服務(wù) 的shuffle-server服務(wù)線程(數(shù)量默認(rèn)為NodeManager管理的cores * 2, 因此配制最低的機(jī)型擁有48個(gè)線程)晌砾, 該線程優(yōu)先級(jí)為5.
經(jīng)過上述分析,我們可知NodeManager中有
- 一個(gè)消費(fèi)Finalizer的FinalizerThread線程烦磁,優(yōu)先級(jí)為8
- 48 個(gè)用于生產(chǎn)Finalizer的shuffle-server線程养匈,優(yōu)先級(jí)為5
- 其它大量線程(如Thread-7872425匿名線程等),此處不一一給出
"Thread-7872425" #8190511 prio=5 os_prio=0 tid=0x00007f1aa8d51000 nid=0xc671 runnable [0x00007f1a83435000] java.lang.Thread.State: RUNNABLE at java.io.FileInputStream.readBytes(Native Method) at java.io.FileInputStream.read(FileInputStream.java:255)
在Java中線程優(yōu)先級(jí)的范圍是:1-10都伪,且數(shù)字越大優(yōu)先級(jí)越高呕乎,線程優(yōu)先級(jí)高僅僅表示線程獲取的 CPU時(shí)間片的幾率高。然而陨晶,由于shuffle-server線程數(shù)量較多猬仁,當(dāng)Shuffle Client端請(qǐng)求頻繁(大量reduce任務(wù)Fetch數(shù)據(jù))時(shí),shuffle-server線程被調(diào)度的機(jī)率會(huì)比Finalizer線程大,這會(huì)導(dǎo)致shuffle-server線程生產(chǎn)Finalizer的速率遠(yuǎn)大于FinalizerTread線程清理的速率逐虚,從而導(dǎo)致Finalizer堆集聋溜。
3 實(shí)驗(yàn)復(fù)現(xiàn)方案
其于如前所述的原因分析:Client端請(qǐng)求頻繁時(shí),會(huì)導(dǎo)致shuffle-server線程生產(chǎn)Finalizer的速率遠(yuǎn)大于FinalizerTread線程清理的速率叭爱,會(huì)導(dǎo)致Finalizer堆集撮躁。因此,可增加部分節(jié)點(diǎn)的shuffle-server線程买雾,使用問題更易復(fù)現(xiàn)把曼。
- 調(diào)整實(shí)驗(yàn)集群中一個(gè)節(jié)點(diǎn)的NodeManager管理的Cores的數(shù)量
操作方法:yarn.nodemanager.resource.cpu-vcore = 80 <更大的值> 或 spark.shuffle.io.serverThreads=160
備注: 該問題會(huì)出現(xiàn)在Shuffle fetch密集的場(chǎng)景(即分布式任務(wù)并發(fā)度高的場(chǎng)景)。
3.1 模擬實(shí)驗(yàn)
復(fù)現(xiàn)方案中提出以調(diào)參的方式增加服務(wù)線程數(shù)量漓穿,從而增加shuffle-server線程被調(diào)度的機(jī)率嗤军。但復(fù)現(xiàn)的前提是要造出大量的密集的Fetch請(qǐng)求,然而晃危,目前測(cè)試集群規(guī)模無法與生產(chǎn)環(huán)境相提并論叙赚,不易造出上述場(chǎng)景;而且僚饭,用戶應(yīng)用負(fù)載及數(shù)據(jù)的獲取不易震叮。因此,進(jìn)行以下實(shí)驗(yàn)?zāi)M真實(shí)環(huán)境的執(zhí)行情況鳍鸵。
3.1.1 實(shí)驗(yàn)
實(shí)驗(yàn)一
50個(gè)線程創(chuàng)建FileInputStream-
實(shí)驗(yàn)二
100個(gè)線程創(chuàng)建FileInputStream上述實(shí)驗(yàn)苇瓣,采用Java 8,采用JVM默認(rèn)配制偿乖,并在相同的節(jié)點(diǎn)中進(jìn)行击罪。
3.1.2 結(jié)果
- 實(shí)驗(yàn)一
Full GC時(shí),ParOldGen(老年代)可以正程靶剑回收媳禁。 - 實(shí)驗(yàn)二
Full GC時(shí),ParOldGen(老年代)幾乎不能回收,從而引發(fā)如下異常:
Exception in thread "shuffle-server-5" java.lang.OutOfMemoryError: GC overhead limit exceeded
異常發(fā)生時(shí)画切,Java Heap信息:
Heap
PSYoungGen total 465920K, used 226578K [0x0000000795580000, 0x00000007c0000000, 0x00000007c0000000)
eden space 232960K, 97% used [0x0000000795580000,0x00000007a32c4bb0,0x00000007a3900000)
from space 232960K, 0% used [0x00000007b1c80000,0x00000007b1c80000,0x00000007c0000000)
to space 232960K, 0% used [0x00000007a3900000,0x00000007a3900000,0x00000007b1c80000)
ParOldGen total 1398272K, used 1397883K [0x0000000740000000, 0x0000000795580000, 0x0000000795580000)
object space 1398272K, 99% used [0x0000000740000000,0x000000079551ee20,0x0000000795580000)
Metaspace used 2718K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 291K, capacity 386K, committed 512K, reserved 1048576K
3.1.3 實(shí)驗(yàn)結(jié)果分析
上述實(shí)驗(yàn)唯一區(qū)別為線程數(shù)量不同损话,即FileInputStream的生產(chǎn)線程與Finalizer線程競(jìng)爭(zhēng)執(zhí)行資源的激烈程度不同。
實(shí)驗(yàn)一 由于線程數(shù)量較少槽唾,競(jìng)爭(zhēng)激烈程度低丧枪,F(xiàn)inalizerThread線程可以被調(diào)度執(zhí)行,從而可以從Finalizer鏈表中清除無引用的對(duì)象庞萍,進(jìn)而在GC時(shí)回收掉Finalizer.
實(shí)驗(yàn)二 線程數(shù)量較大拧烦,競(jìng)爭(zhēng)激烈程度高,F(xiàn)inalizerThread線程被調(diào)度的機(jī)會(huì)少钝计,從而Finalizer鏈表(雙向鏈表)中的對(duì)象無法被回收恋博,只能在Heap的 from 區(qū) 及 to 區(qū)進(jìn)行拷貝齐佳,多個(gè)回合后進(jìn)入old區(qū)(老年代)。當(dāng)FinalizerThread持續(xù)被阻塞時(shí)债沮,就會(huì)發(fā)生Finalizer堆滿old區(qū)的情況炼吴。由于Finalizer對(duì)象在一個(gè)雙向鏈表中相互引用,F(xiàn)ull GC 依然會(huì)無法回收疫衩,最終會(huì)引發(fā):“java.lang.OutOfMemoryError: GC overhead limit exceeded”硅蹦。
在真實(shí)的NodeManager中,除了存在shuffle-server線程外闷煤,還存在大量其它大量線程(有些線程也會(huì)產(chǎn)生FileInputStream)童芹。在負(fù)載較高時(shí),這些線程都會(huì)與FinalizerThread發(fā)生競(jìng)爭(zhēng)鲤拿,從而降低FinalizerThread執(zhí)行的機(jī)率假褪。
- 附1:GC overhead limit exceeded發(fā)生的原因
This message means that for some reason the garbage collector is taking an excessive amount of time (by default 98% of all CPU time of the process) and recovers very little memory in each run (by default 2% of the heap)
- 附2:實(shí)驗(yàn)過程中jstack快照
"shuffle-server-28" #37 prio=5 os_prio=31 tid=0x00007faea901f000 nid=0x8503 waiting for monitor entry [0x000070000e6e7000]
java.lang.Thread.State: BLOCKED (on object monitor)
at java.lang.ref.Finalizer.add(Finalizer.java:51)
"- waiting to lock <0x000000074127c070> (a java.lang.Object)"
at java.lang.ref.Finalizer.<init>(Finalizer.java:82)
at java.lang.ref.Finalizer.register(Finalizer.java:87)
at java.lang.Object.<init>(Object.java:37)
at java.io.InputStream.<init>(InputStream.java:45)
at java.io.FileInputStream.<init>(FileInputStream.java:123)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
at TestThread.run(Test.java:32)
"shuffle-server-26" #35 prio=5 os_prio=31 tid=0x00007faea88c9800 nid=0x8103 waiting for monitor entry [0x000070000e4e1000]
java.lang.Thread.State: BLOCKED (on object monitor)
at java.lang.ref.Finalizer.add(Finalizer.java:51)
"- waiting to lock <0x000000074127c070> (a java.lang.Object)"
at java.lang.ref.Finalizer.<init>(Finalizer.java:82)
at java.lang.ref.Finalizer.register(Finalizer.java:87)
at java.lang.Object.<init>(Object.java:37)
at java.io.InputStream.<init>(InputStream.java:45)
at java.io.FileInputStream.<init>(FileInputStream.java:123)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
at TestThread.run(Test.java:32)
......
"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007faeaa00d800 nid=0x3103 waiting for monitor entry [0x000070000c37e000]
java.lang.Thread.State: BLOCKED (on object monitor)
at java.lang.ref.Finalizer.remove(Finalizer.java:61)
"- waiting to lock <0x000000074127c070> (a java.lang.Object)"
at java.lang.ref.Finalizer.runFinalizer(Finalizer.java:93)
- locked <0x00000007bacfba70> (a java.lang.ref.Finalizer)
at java.lang.ref.Finalizer.access$100(Finalizer.java:34)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:210)
4 結(jié)論
通過上述分析及實(shí)驗(yàn)可知,NodeManager發(fā)生OOM的主要原因?yàn)椋浩鋬?nèi)部線程生產(chǎn)Finalizer的速率大于FinalizerTread線程清理的速率近顷,從而使Finalizer鏈表(雙向鏈表)中無法回收的對(duì)象生音,只能在Heap的 from 區(qū) 及 to 區(qū)進(jìn)行拷貝,多個(gè)回合后進(jìn)入old區(qū)(老年代)窒升。當(dāng)FinalizerThread持續(xù)被阻塞時(shí)久锥,就會(huì)發(fā)生Finalizer堆滿old區(qū)的情況,由于Finalizer對(duì)象在一個(gè)雙向鏈表中相互引用异剥,F(xiàn)ull GC依然會(huì)無法回收,最終會(huì)引發(fā):“java.lang.OutOfMemoryError: GC overhead limit exceeded”絮重。
5 擬解決方案
shuffle-server線程存在為獲取得Shuffle IndexFile中reduce任務(wù)對(duì)應(yīng)數(shù)據(jù)的偏移量及數(shù)據(jù)長(zhǎng)度而創(chuàng)建FileInputStream的情況冤寿,且原有方案中每次獲取都重新打開一次文件,即創(chuàng)建一個(gè)FileInputStream對(duì)象青伤。 因此督怜,可以引入緩存機(jī)制減少讀取該文件的次數(shù)。
-
引入緩存機(jī)制減少讀取該文件的次數(shù)狠角。
一個(gè)IndexFile中包含一個(gè)APP在該節(jié)點(diǎn)中的所有數(shù)據(jù)索引号杠,因此引入緩存具有
較大收益。Spark-15074中已引入緩存特性丰歌,且#SPARK-21501對(duì)緩存方案進(jìn)行了完善姨蟋,
因此可merge官方feature 達(dá)到緩解問題的目的。
缺點(diǎn):緩存只能涵蓋讀取IndexFile時(shí)產(chǎn)生FileInputStream的場(chǎng)景立帖,僅覆蓋
Shuffle Server中1/2的FileInputStream對(duì)象眼溶。
除此之外,還可以使用以下方式進(jìn)行調(diào)整:
使用Files NIO替換FileInputStream
因?yàn)樵搯栴}主要是FileInputStream中實(shí)現(xiàn)了finalize()方法所置晓勇。
缺點(diǎn):不能減少文件頻繁讀寫的開銷, 對(duì)Netty等的影響暫無法評(píng)估堂飞。減少shuffle-server線程數(shù)量灌旧,降低FileInputStream產(chǎn)生速率,通過參數(shù)io.serverThreads調(diào)整绰筛。
缺點(diǎn):機(jī)型較多枢泰,一種配制可能不能滿足三種機(jī)型, 且不合適的配制可能影響作業(yè)的執(zhí)行效率,目前缺少數(shù)據(jù)支撐铝噩。
綜上所述:為將風(fēng)險(xiǎn)降至最低衡蚂,可以先嘗試 1 或1、3結(jié)合的方案薄榛。最后嘗試1讳窟、2結(jié)合方案(事實(shí)證明1、2結(jié)合可以有效解決問題)敞恋。
文獻(xiàn):http://www.oracle.com/technetwork/java/javamail/finalization-137655.html 中提到Finalizer產(chǎn)生的原因及一些處理辦法丽啡。