K8s 中 Pod OOMKilled 原因
在我們?nèi)粘5墓ぷ鳟斨校ǔ枚紩捎?Kubernetes 進行容器化部署,但是總是會出現(xiàn)一些問題,例如,JVM 堆小于 Docker 容器中設置的內(nèi)存大小和 Kubernetes 的內(nèi)存大小,但是還是會被 OOMKilled。在此我們介紹一下 K8s 的 OOMKilled 的 Exit Code 編碼。
Exit Code 137
- 表明容器收到了 SIGKILL 信號,進程被殺掉,對應 kill -9,引發(fā) SIGKILL 的是 docker kill。這可以由用戶或由 docker 守護程序來發(fā)起,手動執(zhí)行:docker kill
- 137 比較常見,如果 pod 中的 limit 資源設置較小,會運行內(nèi)存不足導致 OOMKilled,此時 state 中的 ”O(jiān)OMKilled” 值為 true,你可以在系統(tǒng)的 dmesg -T 中看到 OOM 日志。
因為我的 heap 大小肯定是小于 Docker 容器以及 Pod 的大小的,為啥還是會出現(xiàn) OOMKilled?
原因分析
這種問題常發(fā)生在 JDK8u131 或者 JDK9 版本之后所出現(xiàn)在容器中運行 JVM 的問題:在大多數(shù)情況下,JVM 將一般默認會采用宿主機 Node 節(jié)點的內(nèi)存為 Native VM 空間(其中包含了堆空間、直接內(nèi)存空間以及棧空間),而并非是是容器的空間為標準。
例如我的機器:
$ docker run -m 100MB openjdk:8u121 java -XshowSettings:vm -version VM settings: Max. Heap Size (Estimated): 444.50M Ergonomics Machine Class: server Using VM: OpenJDK 64-Bit Server VM
以上的信息出現(xiàn)了矛盾,我們在運行的時候?qū)⑷萜鲀?nèi)存設置為 100MB,而 -XshowSettings:vm 打印出的 JVM 將最大堆大小為 444M,如果按照這個內(nèi)存進行分配內(nèi)存的話很可能會導致節(jié)點主機在某個時候殺死我的 JVM。
解決方案
JVM 感知 cgroup 限制
一種方法解決 JVM 內(nèi)存超限的問題,這種方法可以讓 JVM 自動感知 docker 容器的 cgroup 限制,從而動態(tài)的調(diào)整堆內(nèi)存大小。JDK8u131 在 JDK9 中有一個很好的特性,即 JVM 能夠檢測在 Docker 容器中運行時有多少內(nèi)存可用。為了使 jvm 保留根據(jù)容器規(guī)范的內(nèi)存,必須設置標志 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap。
?
注意:如果將這兩個標志與 Xms 和 Xmx 標志一起設置,那么 jvm 的行為將是什么?-Xmx 標志將覆蓋-XX:+ UseCGroupMemoryLimitForHeap 標志。
總結(jié)一下:
- 標志
-XX:+UseCGroupMemoryLimitForHeap使 JVM 可以檢測容器中的最大堆大小。 - -Xmx 標志將最大堆大小設置為固定大小。
- 除了 JVM 的堆空間,還會對于非堆和 jvm 的東西,還會有一些額外的內(nèi)存使用情況。
使用 JDK9 的容器感知機制嘗試
$ docker run -m 100MB openjdk:8u131 java \ -XX:+UnlockExperimentalVMOptions \ -XX:+UseCGroupMemoryLimitForHeap \ -XshowSettings:vm -version VM settings: Max. Heap Size (Estimated): 44.50M Ergonomics Machine Class: server Using VM: OpenJDK 64-Bit Server VM
可以看出來通過內(nèi)存感知之后,JVM 能夠檢測到容器只有 100MB,并將最大堆設置為 44M。我們調(diào)整一下內(nèi)存大小看看是否可以實現(xiàn)動態(tài)化調(diào)整和感知內(nèi)存分配,如下所示。
$ docker run -m 1GB openjdk:8u131 java \ -XX:+UnlockExperimentalVMOptions \ -XX:+UseCGroupMemoryLimitForHeap \ -XshowSettings:vm -version VM settings: Max. Heap Size (Estimated): 228.00M Ergonomics Machine Class: server Using VM: OpenJDK 64-Bit Server VM
我們設置了容器有 1GB 內(nèi)存分配,而 JVM 使用 228M 作為最大堆。因為容器中除了 JVM 之外沒有其他進程在運行,所以我們還可以進一步擴大一下對于 Heap 堆的分配?
$ docker run -m 1GB openjdk:8u131 java \ -XX:+UnlockExperimentalVMOptions \ -XX:+UseCGroupMemoryLimitForHeap \ -XX:MaxRAMFraction=1 -XshowSettings:vm -version VM settings: Max. Heap Size (Estimated): 910.50M Ergonomics Machine Class: server Using VM: OpenJDK 64-Bit Server VM
在較低的版本的時候可以使用 -XX:MaxRAMFraction 參數(shù),它告訴 JVM 使用可用內(nèi)存 /MaxRAMFract 作為最大堆。使用 -XX:MaxRAMFraction=1,我們將幾乎所有可用內(nèi)存用作最大堆。從上面的結(jié)果可以看出來內(nèi)存分配已經(jīng)可以達到了 910.50M。
問題分析
- 最大堆占用總內(nèi)存是否仍然會導致你的進程因為內(nèi)存的其他部分(如“元空間”)而被殺死?
答案:MaxRAMFraction=1 仍將為其他非堆內(nèi)存留出一些空間。
?
但如果容器使用堆外內(nèi)存,這可能會有風險,因為幾乎所有的容器內(nèi)存都分配給了堆。您必須將-XX:MaxRAMFraction=2 設置為堆只使用 50% 的容器內(nèi)存,或者使用 Xmx。
容器內(nèi)部感知 CGroup 資源限制
Docker1.7 開始將容器 cgroup 信息掛載到容器中,所以應用可以從 /sys/fs/cgroup/memory/memory.limit_in_bytes 等文件獲取內(nèi)存、 CPU 等設置,在容器的應用啟動命令中根據(jù) Cgroup 配置正確的資源設置 -Xmx, -XX:ParallelGCThreads 等參數(shù)
在 Java10 中,改進了容器集成
- Java10+ 廢除了 -XX:MaxRAM 參數(shù),因為 JVM 將正確檢測該值。在 Java10 中,改進了容器集成。無需添加額外的標志,JVM 將使用 1/4 的容器內(nèi)存用于堆。
- java10+ 確實正確地識別了內(nèi)存的 Docker 限制,但您可以使用新的標志
MaxRAMPercentage(例如:-XX:MaxRAMPercentage=75)而不是舊的MaxRAMFraction,以便更精確地調(diào)整堆的大小,而不是其余的(堆棧、本機…) - java10+ 上的
UseContainerSupport選項,而且是默認啟用的,不用設置。同時UseCGroupMemoryLimitForHeap這個就棄用了,不建議繼續(xù)使用,同時還可以通過-XX:InitialRAMPercentage、-XX:MaxRAMPercentage、-XX:MinRAMPercentage這些參數(shù)更加細膩的控制 JVM 使用的內(nèi)存比率。
?
Java 程序在運行時會調(diào)用外部進程、申請 Native Memory 等,所以即使是在容器中運行 Java 程序,也得預留一些內(nèi)存給系統(tǒng)的。所以-XX:MaxRAMPercentage不能配置得太大。當然仍然可以使用-XX:MaxRAMFraction=1選項來壓縮容器中的所有內(nèi)存。
通過前面的講解我們知道了如何設置和控制 Java 應用對應的堆內(nèi)存和容器內(nèi)存的之間的關系,進而防止 JVM 的堆內(nèi)存超過了容器內(nèi)存,避免容器出現(xiàn) OOMKilled 的情況。但是在整個 JVM 進程體系而言,不僅僅只包含了 Heap 堆內(nèi)存,其實還有其他相關的內(nèi)存存儲空間是需要我們考慮的,一邊防止這些內(nèi)存空間會造成我們的容器內(nèi)存溢出的場景,正如下圖所示。
接下來我們需要進行分析出 heap 之外的一部分就是對外內(nèi)存就是 Off Heap Space,也就是 Direct buffer memory 堆外內(nèi)存。主要通過的方式就是采用 Unsafe 方式進行申請內(nèi)存,大多數(shù)場景也會通過 Direct ByteBuffer 方式進行獲取。好廢話不多說進入正題。
JVM 參數(shù) MaxDirectMemorySize
我們先研究一下 jvm 的 -XX:MaxDirectMemorySize,該參數(shù)指定了 DirectByteBuffer 能分配的空間的限額,如果沒有顯示指定這個參數(shù)啟動 jvm,默認值是 xmx 對應的值(低版本是減去幸存區(qū)的大小)。
DirectByteBuffer 對象是一種典型的”冰山對象”,在堆中存在少量的泄露的對象,但其下面連接用堆外內(nèi)存,這種情況容易造成內(nèi)存的大量使用而得不到釋放
-XX:MaxDirectMemorySize
-XX:MaxDirectMemorySize=size 用于設置 New I/O (java.nio) direct-buffer allocations 的最大大小,size 的單位可以使用 k/K、m/M、g/G;如果沒有設置該參數(shù)則默認值為 0,意味著 JVM 自己自動給 NIO direct-buffer allocations 選擇最大大小。
-XX:MaxDirectMemorySize 的默認值是什么?
在 sun.misc.VM 中,它是 Runtime.getRuntime.maxMemory(),這就是使用-Xmx 配置的內(nèi)容。而對應的 JVM 參數(shù)如何傳遞給 JVM 底層的呢?主要通過的是 hotspot/share/prims/jvm.cpp。我們來看一下 jvm.cpp 的 JVM 源碼來分一下。
// Convert the -XX:MaxDirectMemorySize= command line flag
// to the sun.nio.MaxDirectMemorySize property.
// Do this after setting user properties to prevent people
// from setting the value with a -D option, as requested.
// Leave empty if not supplied
if (!FLAG_IS_DEFAULT(MaxDirectMemorySize)) {
char as_chars[256];
jio_snprintf(as_chars, sizeof(as_chars), JULONG_FORMAT, MaxDirectMemorySize);
Handle key_str = java_lang_String::create_from_platform_dependent_str("sun.nio.MaxDirectMemorySize", CHECK_NULL);
Handle value_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)換為了該屬性之后,進行設置和初始化直接內(nèi)存的配置。針對于直接內(nèi)存的核心類就在www.docjar.com/html/api/su…[1]
public class VM {
// the init level when the VM is fully initialized
private static final int JAVA_LANG_SYSTEM_INITED = 1;
private static final int MODULE_SYSTEM_INITED = 2;
private static final int SYSTEM_LOADER_INITIALIZING = 3;
private static final int SYSTEM_BOOTED = 4;
private static final int SYSTEM_SHUTDOWN = 5;
// 0, 1, 2, ...
private static volatile int initLevel;
private static final Object lock = new Object();
//......
// A user-settable upper limit on the maximum amount of allocatable direct
// buffer memory. This value may be changed during VM initialization if
// "java" is launched with "-XX:MaxDirectMemorySize=<size>".
//
// The initial value of this field is arbitrary; during JRE initialization
// it will be reset to the value specified on the command line, if any,
// otherwise to Runtime.getRuntime().maxMemory().
//
private static long directMemory = 64 * 1024 * 1024;
上面可以看出來 64MB 最初是任意設置的。在 -XX:MaxDirectMemorySize 是用來配置 NIO direct memory 上限用的 VM 參數(shù)。可以看一下 JVM 的這行代碼。
product(intx, MaxDirectMemorySize, -1, "Maximum total size of NIO direct-buffer allocations")
但如果不配置它的話,direct memory 默認最多能申請多少內(nèi)存呢?這個參數(shù)默認值是-1,顯然不是一個“有效值”。所以真正的默認值肯定是從別的地方來的。
// Returns the maximum amount of allocatable direct buffer memory.
// The directMemory variable is initialized during system initialization
// in the saveAndRemoveProperties method.
//
public static long maxDirectMemory() {
return directMemory;
}
//......
// Save a private copy of the system properties and remove
// the system properties that are not intended for public access.
//
// This method can only be invoked during system initialization.
public static void saveProperties(Map<String, String> props) {
if (initLevel() != 0)
throw new IllegalStateException("Wrong init level");
// only main thread is running at this time, so savedProps and
// its content will be correctly published to threads started later
if (savedProps == null) {
savedProps = props;
}
// Set the maximum amount of direct memory. This value is controlled
// by the vm option -XX:MaxDirectMemorySize=<size>.
// The maximum amount of allocatable direct buffer memory (in bytes)
// from the system property sun.nio.MaxDirectMemorySize set by the VM.
// If not set or set to -1, the max memory will be used
// The system property will be removed.
String s = props.get("sun.nio.MaxDirectMemorySize");
if (s == null || s.isEmpty() || s.equals("-1")) {
// -XX:MaxDirectMemorySize not given, take default
directMemory = Runtime.getRuntime().maxMemory();
} else {
long l = Long.parseLong(s);
if (l > -1)
directMemory = l;
}
// Check if direct buffers should be page aligned
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 參數(shù)沒被顯式設置時它的值就是 -1,在 Java 類庫初始化時 maxDirectMemory() 被 java.lang.System 的靜態(tài)構(gòu)造器調(diào)用,走的路徑就是這條:
if (s.equals("-1")) {
// -XX:MaxDirectMemorySize not given, take default
directMemory = Runtime.getRuntime().maxMemory();
}
而 Runtime.maxMemory() 在 HotSpot VM 里的實現(xiàn)是:
JVM_ENTRY_NO_ENV(jlong, JVM_MaxMemory(void))
JVMWrapper("JVM_MaxMemory");
size_t n = Universe::heap()->max_capacity();
return convert_size_t_to_jlong(n);
JVM_END
這個 max_capacity() 實際返回的是 -Xmx 減去一個 survivor space 的預留大小。
結(jié)論分析說明:
MaxDirectMemorySize 沒顯式配置的時候,NIO direct memory 可申請的空間的上限就是 -Xmx 減去一個 survivor space 的預留大小。例如如果您不配置 -XX:MaxDirectMemorySize 并配置 -Xmx5g,則 "默認" MaxDirectMemorySize 也將是 5GB-survivor space 區(qū),并且應用程序的總堆+直接內(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 將其導出到 UNNAMED,這樣才可以運行:
public BufferPoolMXBean getDirectBufferPoolMBean(){
return ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class)
.stream()
.filter(e -> e.getName().equals("direct"))
.findFirst()
.orElseThrow();
}
public JavaNioAccess.BufferPool getNioBufferPool(){
return SharedSecrets.getJavaNioAccess().getDirectBufferPool();
}
內(nèi)存分析問題
-XX:+DisableExplicitGC 與 NIO 的 direct memory
- 用了
-XX:+DisableExplicitGC參數(shù)后,System.gc() 的調(diào)用就會變成一個空調(diào)用,完全不會觸發(fā)任何 GC(但是“函數(shù)調(diào)用”本身的開銷還是存在的哦~)。 - 做 ygc 的時候會將新生代里的不可達的 DirectByteBuffer 對象及其堆外內(nèi)存回收了,但是無法對 old 里的 DirectByteBuffer 對象及其堆外內(nèi)存進行回收,這也是我們通常碰到的最大的問題,如果有大量的 DirectByteBuffer 對象移到了 old,但是又一直沒有做 cms gc 或者 full gc,而只進行 ygc,那么我們的物理內(nèi)存可能被慢慢耗光,但是我們還不知道發(fā)生了什么,因為 heap 明明剩余的內(nèi)存還很多 (前提是我們禁用了 System.gc)。

浙公網(wǎng)安備 33010602011771號