在我們日常的工作當中,通常應用都會采用 Kubernetes 進行容器化部署,但是總是會出現一些問題,例如,JVM 堆小于 Docker 容器中設置的內存大小和 Kubernetes 的內存大小,但是還是會被 OOMKilled。在此我們介紹一下 K8s 的 OOMKilled 的 Exit Code 編碼。
Exit Code 137
表明容器收到了 SIGKILL 信號,進程被殺掉,對應 kill -9,引發 SIGKILL 的是 docker kill。這可以由用戶或由 docker 守護程序來發起,手動執行:docker kill
137 比較常見,如果 pod 中的 limit 資源設置較小,會運行內存不足導致 OOMKilled,此時 state 中的 ”OOMKilled” 值為 true,你可以在系統的 dmesg -T 中看到 OOM 日志。因為我的 heap 大小肯定是小于 Docker 容器以及 Pod 的大小的,為啥還是會出現 OOMKilled?
原因分析
這種問題常發生在 JDK8u131 或者 JDK9 版本之后所出現在容器中運行 JVM 的問題:在大多數情況下,JVM 將一般默認會采用宿主機 Node 節點的內存為 Native VM 空間(其中包含了堆空間、直接內存空間以及棧空間),而并非是是容器的空間為標準。
例如我的機器:
$dockerrun-m100MBopenjdk:8u121java-XshowSettings:vm-version VMsettings: Max.HeapSize(Estimated):444.50M ErgonomicsMachineClass:server UsingVM:OpenJDK64-BitServerVM
以上的信息出現了矛盾,我們在運行的時候將容器內存設置為 100MB,而 -XshowSettings:vm 打印出的 JVM 將最大堆大小為 444M,如果按照這個內存進行分配內存的話很可能會導致節點主機在某個時候殺死我的 JVM。
解決方案
JVM 感知 cgroup 限制
一種方法解決 JVM 內存超限的問題,這種方法可以讓 JVM 自動感知 docker 容器的 cgroup 限制,從而動態的調整堆內存大小。JDK8u131 在 JDK9 中有一個很好的特性,即 JVM 能夠檢測在 Docker 容器中運行時有多少內存可用。為了使 jvm 保留根據容器規范的內存,必須設置標志 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap。
注意:如果將這兩個標志與 Xms 和 Xmx 標志一起設置,那么 jvm 的行為將是什么?-Xmx 標志將覆蓋-XX:+ UseCGroupMemoryLimitForHeap 標志。
總結一下:
標志 -XX:+UseCGroupMemoryLimitForHeap 使 JVM 可以檢測容器中的最大堆大小。
-Xmx 標志將最大堆大小設置為固定大小。
除了 JVM 的堆空間,還會對于非堆和 jvm 的東西,還會有一些額外的內存使用情況。
使用 JDK9 的容器感知機制嘗試
$dockerrun-m100MBopenjdk:8u131java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XshowSettings:vm-version VMsettings: Max.HeapSize(Estimated):44.50M ErgonomicsMachineClass:server UsingVM:OpenJDK64-BitServerVM
可以看出來通過內存感知之后,JVM 能夠檢測到容器只有 100MB,并將最大堆設置為 44M。我們調整一下內存大小看看是否可以實現動態化調整和感知內存分配,如下所示。
$dockerrun-m1GBopenjdk:8u131java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XshowSettings:vm-version VMsettings: Max.HeapSize(Estimated):228.00M ErgonomicsMachineClass:server UsingVM:OpenJDK64-BitServerVM
我們設置了容器有 1GB 內存分配,而 JVM 使用 228M 作為最大堆。因為容器中除了 JVM 之外沒有其他進程在運行,所以我們還可以進一步擴大一下對于 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
在較低的版本的時候可以使用 -XX:MaxRAMFraction 參數,它告訴 JVM 使用可用內存 /MaxRAMFract 作為最大堆。使用 -XX:MaxRAMFraction=1,我們將幾乎所有可用內存用作最大堆。從上面的結果可以看出來內存分配已經可以達到了 910.50M。
問題分析
最大堆占用總內存是否仍然會導致你的進程因為內存的其他部分(如“元空間”)而被殺死?答案:MaxRAMFraction=1 仍將為其他非堆內存留出一些空間。
但如果容器使用堆外內存,這可能會有風險,因為幾乎所有的容器內存都分配給了堆。您必須將-XX:MaxRAMFraction=2 設置為堆只使用 50% 的容器內存,或者使用 Xmx。
容器內部感知 CGroup 資源限制
Docker1.7 開始將容器 cgroup 信息掛載到容器中,所以應用可以從 /sys/fs/cgroup/memory/memory.limit_in_bytes 等文件獲取內存、 CPU 等設置,在容器的應用啟動命令中根據 Cgroup 配置正確的資源設置 -Xmx, -XX:ParallelGCThreads 等參數
在 Java10 中,改進了容器集成
Java10+ 廢除了 -XX:MaxRAM 參數,因為 JVM 將正確檢測該值。在 Java10 中,改進了容器集成。無需添加額外的標志,JVM 將使用 1/4 的容器內存用于堆。
java10+ 確實正確地識別了內存的 Docker 限制,但您可以使用新的標志 MaxRAMPercentage(例如:-XX:MaxRAMPercentage=75)而不是舊的 MaxRAMFraction,以便更精確地調整堆的大小,而不是其余的(堆棧、本機…)
java10+ 上的 UseContainerSupport 選項,而且是默認啟用的,不用設置。同時 UseCGroupMemoryLimitForHeap 這個就棄用了,不建議繼續使用,同時還可以通過 -XX:InitialRAMPercentage、-XX:MaxRAMPercentage、-XX:MinRAMPercentage 這些參數更加細膩的控制 JVM 使用的內存比率。
Java 程序在運行時會調用外部進程、申請 Native Memory 等,所以即使是在容器中運行 Java 程序,也得預留一些內存給系統的。所以 -XX:MaxRAMPercentage 不能配置得太大。當然仍然可以使用 -XX:MaxRAMFraction=1 選項來壓縮容器中的所有內存。
通過前面的講解我們知道了如何設置和控制 Java 應用對應的堆內存和容器內存的之間的關系,進而防止 JVM 的堆內存超過了容器內存,避免容器出現 OOMKilled 的情況。但是在整個 JVM 進程體系而言,不僅僅只包含了 Heap 堆內存,其實還有其他相關的內存存儲空間是需要我們考慮的,一邊防止這些內存空間會造成我們的容器內存溢出的場景,正如下圖所示。
接下來我們需要進行分析出 heap 之外的一部分就是對外內存就是 Off Heap Space,也就是 Direct buffer memory 堆外內存。主要通過的方式就是采用 Unsafe 方式進行申請內存,大多數場景也會通過 Direct ByteBuffer 方式進行獲取。好廢話不多說進入正題。
JVM 參數 MaxDirectMemorySize
我們先研究一下 jvm 的 -XX:MaxDirectMemorySize,該參數指定了 DirectByteBuffer 能分配的空間的限額,如果沒有顯示指定這個參數啟動 jvm,默認值是 xmx 對應的值(低版本是減去幸存區的大小)。
DirectByteBuffer 對象是一種典型的”冰山對象”,在堆中存在少量的泄露的對象,但其下面連接用堆外內存,這種情況容易造成內存的大量使用而得不到釋放
-XX:MaxDirectMemorySize
-XX:MaxDirectMemorySize=size 用于設置 New I/O (java.nio) direct-buffer allocations 的最大大小,size 的單位可以使用 k/K、m/M、g/G;如果沒有設置該參數則默認值為 0,意味著 JVM 自己自動給 NIO direct-buffer allocations 選擇最大大小。
-XX:MaxDirectMemorySize 的默認值是什么?
在 sun.misc.VM 中,它是 Runtime.getRuntime.maxMemory(),這就是使用-Xmx 配置的內容。而對應的 JVM 參數如何傳遞給 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 命令參數轉換為 key 為 sun.nio.MaxDirectMemorySize 的屬性。我們可以看出來他轉換為了該屬性之后,進行設置和初始化直接內存的配置。針對于直接內存的核心類就在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 最初是任意設置的。在 -XX:MaxDirectMemorySize 是用來配置 NIO direct memory 上限用的 VM 參數。可以看一下 JVM 的這行代碼。
product(intx,MaxDirectMemorySize,-1, "MaximumtotalsizeofNIOdirect-bufferallocations")
但如果不配置它的話,direct memory 默認最多能申請多少內存呢?這個參數默認值是-1,顯然不是一個“有效值”。所以真正的默認值肯定是從別的地方來的。
//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,那么則設置為 Runtime.getRuntime().maxMemory();如果有設置 MaxDirectMemorySize 且值大于 -1,那么使用該值作為 directMemory 的值;而 VM 的 maxDirectMemory 方法則返回的是 directMemory 的值。
因為當 MaxDirectMemorySize 參數沒被顯式設置時它的值就是 -1,在 Java 類庫初始化時 maxDirectMemory() 被 java.lang.System 的靜態構造器調用,走的路徑就是這條:
if(s.equals("-1")){ //-XX:MaxDirectMemorySizenotgiven,takedefault directMemory=Runtime.getRuntime().maxMemory(); }
而 Runtime.maxMemory() 在 HotSpot VM 里的實現是:
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
這個 max_capacity() 實際返回的是 -Xmx 減去一個 survivor space 的預留大小。
結論分析說明:
MaxDirectMemorySize 沒顯式配置的時候,NIO direct memory 可申請的空間的上限就是 -Xmx 減去一個 survivor space 的預留大小。例如如果您不配置 -XX:MaxDirectMemorySize 并配置 -Xmx5g,則 "默認" MaxDirectMemorySize 也將是 5GB-survivor space 區,并且應用程序的總堆+直接內存使用量可能會增長到 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 將其導出到 UNNAMED,這樣才可以運行:
publicBufferPoolMXBeangetDirectBufferPoolMBean(){ returnManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class) .stream() .filter(e->e.getName().equals("direct")) .findFirst() .orElseThrow(); } publicJavaNioAccess.BufferPoolgetNioBufferPool(){ returnSharedSecrets.getJavaNioAccess().getDirectBufferPool(); }
內存分析問題
-XX:+DisableExplicitGC 與 NIO 的 direct memory 用了 -XX:+DisableExplicitGC 參數后,System.gc() 的調用就會變成一個空調用,完全不會觸發任何 GC(但是“函數調用”本身的開銷還是存在的哦~)。做 ygc 的時候會將新生代里的不可達的 DirectByteBuffer 對象及其堆外內存回收了,但是無法對 old 里的 DirectByteBuffer 對象及其堆外內存進行回收,這也是我們通常碰到的最大的問題,如果有大量的 DirectByteBuffer 對象移到了 old,但是又一直沒有做 cms gc 或者 full gc,而只進行 ygc,那么我們的物理內存可能被慢慢耗光,但是我們還不知道發生了什么,因為 heap 明明剩余的內存還很多 (前提是我們禁用了 System.gc)。
審核編輯:湯梓紅
-
內存
+關注
關注
8文章
3055瀏覽量
74336 -
容器
+關注
關注
0文章
499瀏覽量
22124 -
JVM
+關注
關注
0文章
158瀏覽量
12261
原文標題:JVM 內存與 K8s 容器內存不一致引發的 OOMKilled 總結
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論