問(wèn)題背景&現(xiàn)象
ExitCode:137 OOMkill
線(xiàn)上環(huán)境的后臺(tái)服務(wù)頻繁重啟绣张,重啟的原因日志是ExitCode:137 OOMkill 。OOMkill是服務(wù)器的一種保護(hù)行為花椭,服務(wù)器會(huì)定時(shí)的掃描服務(wù)器中的各個(gè)進(jìn)程,每個(gè)進(jìn)程根據(jù)一種機(jī)制得到一個(gè)分?jǐn)?shù),如果這個(gè)分?jǐn)?shù)最高則會(huì)被服務(wù)器主動(dòng)kill掉择同。OOMkill的具體機(jī)制在此不做過(guò)多贅述赁濒。而進(jìn)程得到高分的原因一般情況是內(nèi)存資源占用過(guò)高導(dǎo)致的轨奄。被OOMkill后總是難于保留當(dāng)時(shí)具體情況,每次都是服務(wù)被重啟了才發(fā)現(xiàn)問(wèn)題拒炎。還好運(yùn)維平臺(tái)可以支持OOMkill保護(hù)挪拟,發(fā)現(xiàn)資源超限情況不讓直接OOMkill而是先保護(hù)現(xiàn)場(chǎng)并發(fā)出告警。
Memory hit original limit
果然設(shè)置了保護(hù)之后击你,告警就接踵而至了玉组。而且確實(shí)是內(nèi)存超限導(dǎo)致的,如果沒(méi)有保護(hù)又事一次煩人的OOMkill 丁侄。知道是內(nèi)存的問(wèn)題接下來(lái)問(wèn)題定位的方向就更明了了惯雳。
問(wèn)題定位&分析
先dump堆內(nèi)存再說(shuō)
第一個(gè)首先懷疑的是應(yīng)用服務(wù)的堆內(nèi)存。但是dump堆內(nèi)存下來(lái)分析后鸿摇,發(fā)現(xiàn)占用的并不高石景。總內(nèi)存只有200M左右拙吉。
看來(lái)堆內(nèi)存并不是罪魁禍?zhǔn)住?br> 總結(jié):dump命令潮孽,jmap -dump:format=b,file= /文件路徑/dump文件名稱(chēng).hprof 進(jìn)程ID ,如:jmap -dump:format=b,file=/home/heap.hprof 1
是堆外內(nèi)存的原因嗎庐镐?
不是堆內(nèi)存恩商,那應(yīng)該就是堆外內(nèi)存的原因了。項(xiàng)目中有些開(kāi)源框架使用到Netty必逆,Netty是有使用堆外內(nèi)存的怠堪。而且JDK8的元空間也是屬于堆外。
JDK8元空間觀測(cè)
通過(guò)查看JVM的配置名眉,發(fā)現(xiàn)元空間的大小并沒(méi)有限制粟矿。
原因是項(xiàng)目以前使用的是JDK8以前的版本,升級(jí)到了JDK8后對(duì)應(yīng)的JVM配置沒(méi)有對(duì)應(yīng)更新损拢。運(yùn)維平臺(tái)觀測(cè)到的非堆內(nèi)存占用確實(shí)也是不小陌粹。
給JVM參數(shù)加元空間的限制,-XX:MetaspaceSize=xxxm -XX:MaxMetaspaceSize=xxxm 福压。
本以為萬(wàn)事大吉掏秩,但過(guò)了幾天煩人的內(nèi)存超限告警還是來(lái)了或舞。
netty堆外內(nèi)存占用觀測(cè)
不是元空間占用的大量?jī)?nèi)存,那會(huì)不會(huì)是項(xiàng)目其他地方使用到了堆內(nèi)存呢蒙幻? 查看了整個(gè)項(xiàng)目的業(yè)務(wù)代碼和相關(guān)的依賴(lài)映凳,只有Netty使用到堆外內(nèi)存。那么就想辦法觀測(cè)一下Netty使用堆外內(nèi)存的情況邮破。關(guān)于Netty的堆外內(nèi)存如何使用诈豌,因篇幅有限此處不做擴(kuò)展;只是通過(guò)以下工具可以監(jiān)控到堆外內(nèi)存的大惺愫汀:
public static List<String> startReport() {
Field field = ReflectionUtils.findField(PlatformDependent.class, "DIRECT_MEMORY_COUNTER");
field.setAccessible(true);
try {
AtomicLong directMemory = (AtomicLong) field.get(PlatformDependent.class);
int memoryInKb = (int) (directMemory.get() / 1024);
log.info("堆外內(nèi)存大薪糜妗:{}",memoryInKb);
if (list.size() <=100) {
list.add("時(shí)間:" + LocalDateTime.now()
.toString() + " 堆外內(nèi)存:" + memoryInKb + "k ");
}else {
list.clear();
}
return list;
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return list;
}
并通過(guò)一個(gè)上帝接口持續(xù)觀測(cè):
但是經(jīng)過(guò)一段時(shí)間的觀測(cè),占用的內(nèi)存并沒(méi)有異常增長(zhǎng)摧莽。
page cache占用總內(nèi)存情況
無(wú)奈之下庙洼,只能進(jìn)一步的通過(guò)運(yùn)維監(jiān)控平臺(tái)繼續(xù)找一些觀測(cè)數(shù)據(jù)。經(jīng)過(guò)長(zhǎng)時(shí)間的觀測(cè)對(duì)比范嘱。發(fā)現(xiàn)page cache的變化對(duì)總內(nèi)存變化影響很明顯送膳,而且發(fā)出內(nèi)存超限提醒的服務(wù)節(jié)點(diǎn)page cache都沒(méi)法正常回收丑蛤。
page cache是linux內(nèi)核為了提高磁盤(pán)文件操作而設(shè)計(jì)的一種機(jī)制叠聋。它會(huì)將一些磁盤(pán)的文件數(shù)據(jù)寫(xiě)入到內(nèi)存中,并設(shè)計(jì)一套淘汰機(jī)制回收內(nèi)存受裹。根據(jù)上圖觀測(cè)到的情況碌补,page cache占用的內(nèi)存并沒(méi)法正常回收棉饶。
vmtouch觀測(cè)文件緩存情況
梳理了整個(gè)業(yè)務(wù)項(xiàng)目的代碼厦章,并沒(méi)有發(fā)現(xiàn)有大量讀寫(xiě)文件的地方。懷疑是日志文件的讀寫(xiě)照藻,導(dǎo)致一直在產(chǎn)生page cache袜啃。為了驗(yàn)證個(gè)問(wèn)題需要使用到vmtouch命令,關(guān)于此命令的介紹可以參見(jiàn)官方文檔:https://hoytech.com/vmtouch
此命令可以查看磁盤(pán)文件被加載到內(nèi)存中的情況幸缕。
查看日志文件被加載到內(nèi)存的情況:
整個(gè)日志文件900M左右群发,全部被加載到內(nèi)存中。
將被加載到內(nèi)存中的緩沖驅(qū)逐掉:
觀測(cè)到的page cache和總內(nèi)存對(duì)應(yīng)馬上降低下來(lái):
終于問(wèn)題原因變得明朗了发乔,接下來(lái)就要看看page cache為什么不能正呈旒耍回收了。
page cache數(shù)據(jù)觀測(cè)
為什么page cache這么多栏尚?
page cache為什么這么多起愈,是因?yàn)槿罩疚募罅藛幔繛榱蓑?yàn)證這個(gè)問(wèn)題,通過(guò)修改log4j的配置抬虽,將每個(gè)日志文件分割成500M以?xún)?nèi)官觅,但是問(wèn)題依舊。而且日志文件被切割后阐污,不再寫(xiě)入的日志文件在依然被加載到內(nèi)存中缰猴,說(shuō)明是讀日志文件的原因。
日志文件的讀場(chǎng)景疤剑,是運(yùn)維平臺(tái)做日志采集的情況才會(huì)觸發(fā)的。于是找了運(yùn)維平臺(tái)的同事反饋闷堡。
為什么page cache沒(méi)正嘲欤回收?
反饋問(wèn)題后自己也持續(xù)觀察杠览,發(fā)現(xiàn)有問(wèn)題的節(jié)點(diǎn)弯菊。發(fā)現(xiàn)page cache的掃描速度很慢,而且臟頁(yè)閾值配置的也不一樣踱阿。和運(yùn)維同事溝通后管钳,得知服務(wù)器節(jié)點(diǎn)是被隨機(jī)分配的,可能節(jié)點(diǎn)屬于不同的物理機(jī)软舌,而臟頁(yè)閾值的配置使用的應(yīng)該是物理機(jī)的配置才漆。
問(wèn)題解決&防范
曲線(xiàn)救國(guó)
既然問(wèn)題原因是讀日志文件導(dǎo)致的,那就不讓監(jiān)控程序主動(dòng)來(lái)讀取日志佛点,而是主動(dòng)將數(shù)據(jù)通過(guò)消息隊(duì)列推送給監(jiān)控系統(tǒng)醇滥。公司基于log4j自研封裝了日志工具,可以根據(jù)配置將日志直接通過(guò)kafka推送給監(jiān)控系統(tǒng)超营,這樣就能避免讀日志文件鸳玩,page cache的問(wèn)題也可以避免了。
問(wèn)題根治
主動(dòng)推送日志的方法只能是避免問(wèn)題的發(fā)生演闭,要根治問(wèn)題還是得從page cache的相關(guān)配置入手不跟。page cache難回收的原因一般有三個(gè)(具體可以參見(jiàn):Page Cache難以回收問(wèn)題):
1.直接內(nèi)存回收導(dǎo)致
2.系統(tǒng)臟頁(yè)內(nèi)存積壓過(guò)多導(dǎo)致
3.系統(tǒng)NUMA策略配置不當(dāng)導(dǎo)致
其中情況1 通過(guò)sar -B命令觀測(cè)可以排插,因?yàn)閜gscand并沒(méi)有發(fā)生
情況3通過(guò)命令numactl --hardware確認(rèn)也可以排除了米碰。
情況2需要不斷配置系統(tǒng)參數(shù)并調(diào)試窝革,因是線(xiàn)上環(huán)境,而且初始環(huán)境沒(méi)法模擬驗(yàn)證见间,只能反饋給運(yùn)維團(tuán)隊(duì)聊闯,讓他們持續(xù)優(yōu)化。