在我們?nèi)粘5墓ぷ鳟?dāng)中,通常應(yīng)用都會采用 Kubernetes 進(jìn)行容器化部署,但是總是會出現(xiàn)一些問題,例如,JVM 堆小于 Docker 容器中設(shè)置的內(nèi)存大小和 Kubernetes 的內(nèi)存大小,但是還是會被 OOMKilled。在此我們介紹一下 K8s 的 OOMKilled 的 Exit Code 編碼。
Exit Code 137
表明容器收到了 SIGKILL 信號,進(jìn)程被殺掉,對應(yīng) kill -9,引發(fā) SIGKILL 的是 docker kill。這可以由用戶或由 docker 守護(hù)程序來發(fā)起,手動(dòng)執(zhí)行:docker kill
137 比較常見,如果 pod 中的 limit 資源設(shè)置較小,會運(yùn)行內(nèi)存不足導(dǎo)致 OOMKilled,此時(shí) state 中的 ”O(jiān)OMKilled” 值為 true,你可以在系統(tǒng)的 dmesg -T 中看到 OOM 日志。因?yàn)槲业?heap 大小肯定是小于 Docker 容器以及 Pod 的大小的,為啥還是會出現(xiàn) OOMKilled?
原因分析
這種問題常發(fā)生在 JDK8u131 或者 JDK9 版本之后所出現(xiàn)在容器中運(yùn)行 JVM 的問題:在大多數(shù)情況下,JVM 將一般默認(rèn)會采用宿主機(jī) Node 節(jié)點(diǎn)的內(nèi)存為 Native VM 空間(其中包含了堆空間、直接內(nèi)存空間以及??臻g),而并非是是容器的空間為標(biāo)準(zhǔn)。
例如我的機(jī)器:
$dockerrun-m100MBopenjdk:8u121java-XshowSettings:vm-version VMsettings: Max.HeapSize(Estimated):444.50M ErgonomicsMachineClass:server UsingVM:OpenJDK64-BitServerVM
以上的信息出現(xiàn)了矛盾,我們在運(yùn)行的時(shí)候?qū)⑷萜鲀?nèi)存設(shè)置為 100MB,而 -XshowSettings:vm 打印出的 JVM 將最大堆大小為 444M,如果按照這個(gè)內(nèi)存進(jìn)行分配內(nèi)存的話很可能會導(dǎo)致節(jié)點(diǎn)主機(jī)在某個(gè)時(shí)候殺死我的 JVM。
解決方案
JVM 感知 cgroup 限制
一種方法解決 JVM 內(nèi)存超限的問題,這種方法可以讓 JVM 自動(dòng)感知 docker 容器的 cgroup 限制,從而動(dòng)態(tài)的調(diào)整堆內(nèi)存大小。JDK8u131 在 JDK9 中有一個(gè)很好的特性,即 JVM 能夠檢測在 Docker 容器中運(yùn)行時(shí)有多少內(nèi)存可用。為了使 jvm 保留根據(jù)容器規(guī)范的內(nèi)存,必須設(shè)置標(biāo)志 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap。
注意:如果將這兩個(gè)標(biāo)志與 Xms 和 Xmx 標(biāo)志一起設(shè)置,那么 jvm 的行為將是什么?-Xmx 標(biāo)志將覆蓋-XX:+ UseCGroupMemoryLimitForHeap 標(biāo)志。
總結(jié)一下:
標(biāo)志 -XX:+UseCGroupMemoryLimitForHeap 使 JVM 可以檢測容器中的最大堆大小。
-Xmx 標(biāo)志將最大堆大小設(shè)置為固定大小。
除了 JVM 的堆空間,還會對于非堆和 jvm 的東西,還會有一些額外的內(nèi)存使用情況。
使用 JDK9 的容器感知機(jī)制嘗試
$dockerrun-m100MBopenjdk:8u131java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XshowSettings:vm-version VMsettings: Max.HeapSize(Estimated):44.50M ErgonomicsMachineClass:server UsingVM:OpenJDK64-BitServerVM
可以看出來通過內(nèi)存感知之后,JVM 能夠檢測到容器只有 100MB,并將最大堆設(shè)置為 44M。我們調(diào)整一下內(nèi)存大小看看是否可以實(shí)現(xiàn)動(dòng)態(tài)化調(diào)整和感知內(nèi)存分配,如下所示。
$dockerrun-m1GBopenjdk:8u131java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XshowSettings:vm-version VMsettings: Max.HeapSize(Estimated):228.00M ErgonomicsMachineClass:server UsingVM:OpenJDK64-BitServerVM
我們設(shè)置了容器有 1GB 內(nèi)存分配,而 JVM 使用 228M 作為最大堆。因?yàn)槿萜髦谐?JVM 之外沒有其他進(jìn)程在運(yùn)行,所以我們還可以進(jìn)一步擴(kuò)大一下對于 Heap 堆的分配?
$dockerrun-m1GBopenjdk:8u131java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1-XshowSettings:vm-version VMsettings: Max.HeapSize(Estimated):910.50M ErgonomicsMachineClass:server UsingVM:OpenJDK64-BitServerVM
在較低的版本的時(shí)候可以使用 -XX:MaxRAMFraction 參數(shù),它告訴 JVM 使用可用內(nèi)存 /MaxRAMFract 作為最大堆。使用 -XX:MaxRAMFraction=1,我們將幾乎所有可用內(nèi)存用作最大堆。從上面的結(jié)果可以看出來內(nèi)存分配已經(jīng)可以達(dá)到了 910.50M。
問題分析
最大堆占用總內(nèi)存是否仍然會導(dǎo)致你的進(jìn)程因?yàn)閮?nèi)存的其他部分(如“元空間”)而被殺死?答案:MaxRAMFraction=1 仍將為其他非堆內(nèi)存留出一些空間。
但如果容器使用堆外內(nèi)存,這可能會有風(fēng)險(xiǎn),因?yàn)閹缀跛械娜萜鲀?nèi)存都分配給了堆。您必須將-XX:MaxRAMFraction=2 設(shè)置為堆只使用 50% 的容器內(nèi)存,或者使用 Xmx。
容器內(nèi)部感知 CGroup 資源限制
Docker1.7 開始將容器 cgroup 信息掛載到容器中,所以應(yīng)用可以從 /sys/fs/cgroup/memory/memory.limit_in_bytes 等文件獲取內(nèi)存、 CPU 等設(shè)置,在容器的應(yīng)用啟動(dòng)命令中根據(jù) Cgroup 配置正確的資源設(shè)置 -Xmx, -XX:ParallelGCThreads 等參數(shù)
在 Java10 中,改進(jìn)了容器集成
Java10+ 廢除了 -XX:MaxRAM 參數(shù),因?yàn)?JVM 將正確檢測該值。在 Java10 中,改進(jìn)了容器集成。無需添加額外的標(biāo)志,JVM 將使用 1/4 的容器內(nèi)存用于堆。
java10+ 確實(shí)正確地識別了內(nèi)存的 Docker 限制,但您可以使用新的標(biāo)志 MaxRAMPercentage(例如:-XX:MaxRAMPercentage=75)而不是舊的 MaxRAMFraction,以便更精確地調(diào)整堆的大小,而不是其余的(堆棧、本機(jī)…)
java10+ 上的 UseContainerSupport 選項(xiàng),而且是默認(rèn)啟用的,不用設(shè)置。同時(shí) UseCGroupMemoryLimitForHeap 這個(gè)就棄用了,不建議繼續(xù)使用,同時(shí)還可以通過 -XX:InitialRAMPercentage、-XX:MaxRAMPercentage、-XX:MinRAMPercentage 這些參數(shù)更加細(xì)膩的控制 JVM 使用的內(nèi)存比率。
Java 程序在運(yùn)行時(shí)會調(diào)用外部進(jìn)程、申請 Native Memory 等,所以即使是在容器中運(yùn)行 Java 程序,也得預(yù)留一些內(nèi)存給系統(tǒng)的。所以 -XX:MaxRAMPercentage 不能配置得太大。當(dāng)然仍然可以使用 -XX:MaxRAMFraction=1 選項(xiàng)來壓縮容器中的所有內(nèi)存。
通過前面的講解我們知道了如何設(shè)置和控制 Java 應(yīng)用對應(yīng)的堆內(nèi)存和容器內(nèi)存的之間的關(guān)系,進(jìn)而防止 JVM 的堆內(nèi)存超過了容器內(nèi)存,避免容器出現(xiàn) OOMKilled 的情況。但是在整個(gè) JVM 進(jìn)程體系而言,不僅僅只包含了 Heap 堆內(nèi)存,其實(shí)還有其他相關(guān)的內(nèi)存存儲空間是需要我們考慮的,一邊防止這些內(nèi)存空間會造成我們的容器內(nèi)存溢出的場景,正如下圖所示。
接下來我們需要進(jìn)行分析出 heap 之外的一部分就是對外內(nèi)存就是 Off Heap Space,也就是 Direct buffer memory 堆外內(nèi)存。主要通過的方式就是采用 Unsafe 方式進(jìn)行申請內(nèi)存,大多數(shù)場景也會通過 Direct ByteBuffer 方式進(jìn)行獲取。好廢話不多說進(jìn)入正題。
JVM 參數(shù) MaxDirectMemorySize
我們先研究一下 jvm 的 -XX:MaxDirectMemorySize,該參數(shù)指定了 DirectByteBuffer 能分配的空間的限額,如果沒有顯示指定這個(gè)參數(shù)啟動(dòng) jvm,默認(rèn)值是 xmx 對應(yīng)的值(低版本是減去幸存區(qū)的大小)。
DirectByteBuffer 對象是一種典型的”冰山對象”,在堆中存在少量的泄露的對象,但其下面連接用堆外內(nèi)存,這種情況容易造成內(nèi)存的大量使用而得不到釋放
-XX:MaxDirectMemorySize
-XX:MaxDirectMemorySize=size 用于設(shè)置 New I/O (java.nio) direct-buffer allocations 的最大大小,size 的單位可以使用 k/K、m/M、g/G;如果沒有設(shè)置該參數(shù)則默認(rèn)值為 0,意味著 JVM 自己自動(dòng)給 NIO direct-buffer allocations 選擇最大大小。
-XX:MaxDirectMemorySize 的默認(rèn)值是什么?
在 sun.misc.VM 中,它是 Runtime.getRuntime.maxMemory(),這就是使用-Xmx 配置的內(nèi)容。而對應(yīng)的 JVM 參數(shù)如何傳遞給 JVM 底層的呢?主要通過的是 hotspot/share/prims/jvm.cpp。我們來看一下 jvm.cpp 的 JVM 源碼來分一下。
//Convertthe-XX:MaxDirectMemorySize=commandlineflag //tothesun.nio.MaxDirectMemorySizeproperty. //Dothisaftersettinguserpropertiestopreventpeople //fromsettingthevaluewitha-Doption,asrequested. //Leaveemptyifnotsupplied if(!FLAG_IS_DEFAULT(MaxDirectMemorySize)){ charas_chars[256]; jio_snprintf(as_chars,sizeof(as_chars),JULONG_FORMAT,MaxDirectMemorySize); Handlekey_str=java_lang_String::create_from_platform_dependent_str("sun.nio.MaxDirectMemorySize",CHECK_NULL); Handlevalue_str=java_lang_String::create_from_platform_dependent_str(as_chars,CHECK_NULL); result_h->obj_at_put(ndx*2,key_str()); result_h->obj_at_put(ndx*2+1,value_str()); ndx++; }
jvm.cpp 里頭有一段代碼用于把 -XX:MaxDirectMemorySize 命令參數(shù)轉(zhuǎn)換為 key 為 sun.nio.MaxDirectMemorySize 的屬性。我們可以看出來他轉(zhuǎn)換為了該屬性之后,進(jìn)行設(shè)置和初始化直接內(nèi)存的配置。針對于直接內(nèi)存的核心類就在www.docjar.com/html/api/su…[1]
publicclassVM{ //theinitlevelwhentheVMisfullyinitialized privatestaticfinalintJAVA_LANG_SYSTEM_INITED=1; privatestaticfinalintMODULE_SYSTEM_INITED=2; privatestaticfinalintSYSTEM_LOADER_INITIALIZING=3; privatestaticfinalintSYSTEM_BOOTED=4; privatestaticfinalintSYSTEM_SHUTDOWN=5; //0,1,2,... privatestaticvolatileintinitLevel; privatestaticfinalObjectlock=newObject(); //...... //Auser-settableupperlimitonthemaximumamountofallocatabledirect //buffermemory.ThisvaluemaybechangedduringVMinitializationif //"java"islaunchedwith"-XX:MaxDirectMemorySize=". // //Theinitialvalueofthisfieldisarbitrary;duringJREinitialization //itwillberesettothevaluespecifiedonthecommandline,ifany, //otherwisetoRuntime.getRuntime().maxMemory(). // privatestaticlongdirectMemory=64*1024*1024;
上面可以看出來 64MB 最初是任意設(shè)置的。在 -XX:MaxDirectMemorySize 是用來配置 NIO direct memory 上限用的 VM 參數(shù)??梢钥匆幌?JVM 的這行代碼。
product(intx,MaxDirectMemorySize,-1, "MaximumtotalsizeofNIOdirect-bufferallocations")
但如果不配置它的話,direct memory 默認(rèn)最多能申請多少內(nèi)存呢?這個(gè)參數(shù)默認(rèn)值是-1,顯然不是一個(gè)“有效值”。所以真正的默認(rèn)值肯定是從別的地方來的。
//Returnsthemaximumamountofallocatabledirectbuffermemory. //ThedirectMemoryvariableisinitializedduringsysteminitialization //inthesaveAndRemovePropertiesmethod. // publicstaticlongmaxDirectMemory(){ returndirectMemory; } //...... //Saveaprivatecopyofthesystempropertiesandremove //thesystempropertiesthatarenotintendedforpublicaccess. // //Thismethodcanonlybeinvokedduringsysteminitialization. publicstaticvoidsaveProperties(Mapprops){ if(initLevel()!=0) thrownewIllegalStateException("Wronginitlevel"); //onlymainthreadisrunningatthistime,sosavedPropsand //itscontentwillbecorrectlypublishedtothreadsstartedlater if(savedProps==null){ savedProps=props; } //Setthemaximumamountofdirectmemory.Thisvalueiscontrolled //bythevmoption-XX:MaxDirectMemorySize= . //Themaximumamountofallocatabledirectbuffermemory(inbytes) //fromthesystempropertysun.nio.MaxDirectMemorySizesetbytheVM. //Ifnotsetorsetto-1,themaxmemorywillbeused //Thesystempropertywillberemoved. Strings=props.get("sun.nio.MaxDirectMemorySize"); if(s==null||s.isEmpty()||s.equals("-1")){ //-XX:MaxDirectMemorySizenotgiven,takedefault directMemory=Runtime.getRuntime().maxMemory(); }else{ longl=Long.parseLong(s); if(l>-1) directMemory=l; } //Checkifdirectbuffersshouldbepagealigned s=props.get("sun.nio.PageAlignDirectMemory"); if("true".equals(s)) pageAlignDirectMemory=true; } //...... }
從上面的源碼可以讀取 sun.nio.MaxDirectMemorySize 屬性,如果為 null 或者是空或者是 - 1,那么則設(shè)置為 Runtime.getRuntime().maxMemory();如果有設(shè)置 MaxDirectMemorySize 且值大于 -1,那么使用該值作為 directMemory 的值;而 VM 的 maxDirectMemory 方法則返回的是 directMemory 的值。
因?yàn)楫?dāng) MaxDirectMemorySize 參數(shù)沒被顯式設(shè)置時(shí)它的值就是 -1,在 Java 類庫初始化時(shí) maxDirectMemory() 被 java.lang.System 的靜態(tài)構(gòu)造器調(diào)用,走的路徑就是這條:
if(s.equals("-1")){ //-XX:MaxDirectMemorySizenotgiven,takedefault directMemory=Runtime.getRuntime().maxMemory(); }
而 Runtime.maxMemory() 在 HotSpot VM 里的實(shí)現(xiàn)是:
JVM_ENTRY_NO_ENV(jlong,JVM_MaxMemory(void)) JVMWrapper("JVM_MaxMemory"); size_tn=Universe::heap()->max_capacity(); returnconvert_size_t_to_jlong(n); JVM_END
這個(gè) max_capacity() 實(shí)際返回的是 -Xmx 減去一個(gè) survivor space 的預(yù)留大小。
結(jié)論分析說明:
MaxDirectMemorySize 沒顯式配置的時(shí)候,NIO direct memory 可申請的空間的上限就是 -Xmx 減去一個(gè) survivor space 的預(yù)留大小。例如如果您不配置 -XX:MaxDirectMemorySize 并配置 -Xmx5g,則 "默認(rèn)" MaxDirectMemorySize 也將是 5GB-survivor space 區(qū),并且應(yīng)用程序的總堆+直接內(nèi)存使用量可能會增長到 5 + 5 = 10 Gb。
其他獲取 maxDirectMemory 的值的 API 方法
BufferPoolMXBean 及 JavaNioAccess.BufferPool (通過 SharedSecrets 獲取) 的 getMemoryUsed 可以獲取 direct memory 的大??;其中 java9 模塊化之后,SharedSecrets 從原來的 sun.misc.SharedSecrets 變更到 java.base 模塊下的 jdk.internal.access.SharedSecrets;要使用 --add-exports java.base/jdk.internal.access=ALL-UNNAMED 將其導(dǎo)出到 UNNAMED,這樣才可以運(yùn)行:
publicBufferPoolMXBeangetDirectBufferPoolMBean(){ returnManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class) .stream() .filter(e->e.getName().equals("direct")) .findFirst() .orElseThrow(); } publicJavaNioAccess.BufferPoolgetNioBufferPool(){ returnSharedSecrets.getJavaNioAccess().getDirectBufferPool(); }
內(nèi)存分析問題
-XX:+DisableExplicitGC 與 NIO 的 direct memory 用了 -XX:+DisableExplicitGC 參數(shù)后,System.gc() 的調(diào)用就會變成一個(gè)空調(diào)用,完全不會觸發(fā)任何 GC(但是“函數(shù)調(diào)用”本身的開銷還是存在的哦~)。做 ygc 的時(shí)候會將新生代里的不可達(dá)的 DirectByteBuffer 對象及其堆外內(nèi)存回收了,但是無法對 old 里的 DirectByteBuffer 對象及其堆外內(nèi)存進(jìn)行回收,這也是我們通常碰到的最大的問題,如果有大量的 DirectByteBuffer 對象移到了 old,但是又一直沒有做 cms gc 或者 full gc,而只進(jìn)行 ygc,那么我們的物理內(nèi)存可能被慢慢耗光,但是我們還不知道發(fā)生了什么,因?yàn)?heap 明明剩余的內(nèi)存還很多 (前提是我們禁用了 System.gc)。
審核編輯:湯梓紅
-
內(nèi)存
+關(guān)注
關(guān)注
8文章
3060瀏覽量
74353 -
容器
+關(guān)注
關(guān)注
0文章
499瀏覽量
22128 -
JVM
+關(guān)注
關(guān)注
0文章
158瀏覽量
12267
原文標(biāo)題:JVM 內(nèi)存與 K8s 容器內(nèi)存不一致引發(fā)的 OOMKilled 總結(jié)
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
STM32H750DMA+SPi內(nèi)存數(shù)據(jù)不一致的原因?
不一致IP設(shè)置
鋰離子電池組一致性的含義與不一致性的改進(jìn)措施
![鋰離子電池組<b class='flag-5'>一致</b>性的含義與<b class='flag-5'>不一致</b>性的改進(jìn)措施](https://file.elecfans.com/web2/M00/4A/07/pYYBAGKhvIuAGzDOAABAO1ZM9a4501.png)
基于偏好不一致熵的偏好決策方法
不一致數(shù)據(jù)上精確決策樹生成算法
![<b class='flag-5'>不一致</b>數(shù)據(jù)上精確決策樹生成算法](https://file.elecfans.com/web2/M00/49/84/poYBAGKhwMGAbtzNAAASApowGsM930.jpg)
評論