0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

JVM内存与K8s容器内存不一致引发的OOMKilled总结

马哥Linux运维 来源:稀土掘金技术社区 作者:洛神灬殇 2022-12-20 09:38 次阅读

在我们日常的工作当中,通常应用都会采用 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 堆内存,其实还有其他相关的内存存储空间是需要我们考虑的,一边防止这些内存空间会造成我们的容器内存溢出的场景,正如下图所示。

f2c4ec2c-7f9d-11ed-8abf-dac502259ad0.png

接下来我们需要进行分析出 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

    文章

    2964

    浏览量

    73803
  • 容器
    +关注

    关注

    0

    文章

    492

    浏览量

    22027
  • JVM
    JVM
    +关注

    关注

    0

    文章

    157

    浏览量

    12195

原文标题:JVM 内存与 K8s 容器内存不一致引发的 OOMKilled 总结

文章出处:【微信号:magedu-Linux,微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    STM32H750DMA+SPi内存数据不一致的原因?

    发现用stm32H750 dma+spi读写数据时发现当spi速度大于8M时出现内存不一致的问题,我再读之前已经用SCB_CleanInvalidateDCache_by_Addr这个函数无效化
    发表于 05-24 07:53

    不一致IP设置

    有没有人使用cRIO之后,遇到过这个问题?我使用Ni MAX连接的时候,系统状态直是:不一致IP设置请教大家,这个问题该怎么处理?
    发表于 06-25 08:49

    K8S容器编排的互通测试

    K8S容器编排之NetWorkPolicy官方实例
    发表于 06-06 11:28

    锂离子电池组一致性的含义与不一致性的改进措施

    深入分析,并总结了生产、配组、使用、维护等过程提出弥补不一致性的措施。 不一致性 锂离子电池一致性是指用于成组的单体电池的初期性能指标的一致
    发表于 10-15 13:15 12次下载
    锂离子电池组<b class='flag-5'>一致</b>性的含义与<b class='flag-5'>不一致</b>性的改进措施

    基于偏好不一致熵的偏好决策方法

    针对多规则有序决策系统中的偏好决策问题,根据有序决策的偏好不一致特性,提出了种基于偏好不一致熵的偏好决策方法。首先,定义了样本的偏好不一致熵( PIEO),用来度量特定样本相对于样本
    发表于 12-05 11:50 0次下载

    不一致数据上精确决策树生成算法

    近年来,随着现实生活中数据量的不断增大,不一致数据的出现也越发频繁,这使得人工修正不一致数据变得更加耗时.而且。人工修正数据方法本身也存在着不可避免的人为操作错误,因此。这种修正方法不再可行.如何不
    发表于 12-26 16:13 0次下载
    <b class='flag-5'>不一致</b>数据上精确决策树生成算法

    感兴趣区域不一致性决策算法

    医学影像感兴趣区域( ROI)的噪声和疾病误判是个典型的不一致性决策问题,同时也是困扰临床诊断的个难题。针对这个问题,基于宏观与微观结合、全局与局部相结合的思想,提出了基于一致度、
    发表于 01-02 18:43 0次下载

    分布式大数据不一致性检测

    关系数据库中可能存在数据不一致性现象,关系数据库数据质量的个主要问题是存在违反函数依赖情况,为找出不一致数据需要进行函数依赖冲突检测.集中式数据库中可以通过SQL技术检测不一致情况,
    发表于 01-12 16:29 0次下载

    锂电池组不一致性的原因及危害是怎样的

    锂电池组不一致性的原因及损害,看了就明白!锂电池组电压不一致会发生什么损害?怎么应对锂电池组不一致性的损害?锂电池参数的不一致首要是指容量、内阻、开路电压的
    发表于 03-17 17:39 1.2w次阅读

    锂电池组不一致性的原因是什么,它的危害有哪些

    锂电池组不一致性的原因及损害,看了就明白!锂电池组电压不一致会发生什么损害?怎么应对锂电池组不一致性的损害?锂电池参数的不一致首要是指容量、内阻、开路电压的
    发表于 03-17 17:41 4376次阅读

    k8s是什么意思?kubeadm部署k8s集群(k8s部署)|PetaExpres

    k8s是什么意思? kubernetes简称K8s,是个开源的,用于管理云平台中多个主机上的容器化的应用,Kubernetes的目标是让部署容器
    发表于 07-19 13:14 1084次阅读

    跑大模型AI的K8s与普通K8s的区别分析

    Kubernetes是个在大量节点上管理容器的系统,其主要功能总结起来,就是在想要启动容器的时候,负责“找个「空闲」节点,启动
    发表于 09-03 12:07 849次阅读

    什么是电芯的不一致性?电芯不一致会造成什么后果?

    什么是电芯的不一致性?电芯不一致会造成什么后果? 电芯是电池组成部分之,由正负极、电解质和隔膜组成。在电动车和移动设备中广泛使用的锂离子电池,通常由数十个甚至数百个电芯组成。电芯的不一致
    的头像 发表于 11-06 10:56 3171次阅读

    什么是锂离子电池不一致性?如何提高锂离子电池的一致性?

    什么是锂离子电池不一致性?锂离子电池不稳定的原因?如何提高锂离子电池的一致性? 锂离子电池不一致性是指同批次或不同批次的锂离子电池在性能上出现不一
    的头像 发表于 11-10 14:49 1642次阅读

    充放电不一致影响超级电容器性能的原因及解决方案

    充放电不一致影响超级电容器性能的原因及解决方案  充放电不一致是指超级电容器在充电和放电过程中无法保持一致的电压和电流特征。这种
    的头像 发表于 02-03 15:02 1556次阅读