记一次特殊的K8S内存溢出

一、引言

        这是一次特殊的pre环境的内存溢出导致服务被K8S杀掉,为什么说它是特殊的,因为可以说这不是jvm的锅!

二、环境及现象

        1、pod内存分配

                设置该服务所在pod的限制为2G内存,超过2G就会达到K8S的oom界限,被K8S重启。  

        2、jvm设置

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAdGluZ21haWxhbmc=,size_20,color_FFFFFF,t_70,g_se,x_16

         3、现象

        服务在pre环境重启,K8S层面看下来是由于该pod使用内存过大超出限制导致的oom重启(并未被驱逐,排除被物理机OOM),但是JVM堆内并没超过限制。

记一次特殊的K8S内存溢出_第1张图片

记一次特殊的K8S内存溢出_第2张图片

三、排查与解决

1、堆外内存

        jvm堆内存既然没有超过限制,怀疑方向指到了堆外内存,但是排查了整个服务之后,没有使用nio进行堆外存储,第三方中间件例如rocketMq、Redisson等是基于Netty进行消息收发的,但是它们使用的nio非常少,可以忽略。

2、NativeMemoryTracking

        让运维开启了NMT追踪jvm使用的所有内存,可以看出,jvm整体使用的内存离2G还有一段距离。

记一次特殊的K8S内存溢出_第3张图片

         通过pod监控可以看出在jvm使用1.6g左右内存时,pod内存达到了1.95g左右,濒临K8S的kill界限。

        通过查阅相关资料了解到在当前的容器化环境中,jvm释放的内存不会立刻被转化为物理机内存,jvm可以对这部分内存随时取用,但是它不归属于jvm,因此不会实时的触发gc。方向似乎清晰了,K8S很可能将这部分游离内存和jvm内存进行累加,达到2G限制就进行oom重启。

记一次特殊的K8S内存溢出_第4张图片

3、持续观察NMT和pod节点内存变化

        有了方向就产生了新的疑问,容器这部分游离的内存随什么变化?还是毫无规律?是否是线程数量或者类加载?

        带着这些疑问,作者开始了持续两个星期的跟踪观察,但是很遗憾,从观察结果来看,游离内存变化几乎毫无规律。

记一次特殊的K8S内存溢出_第5张图片

 4、解决

        其实在有了方向之后就已经可以知道解决方案了,那就是增加K8S容器可进行自由转换的游离内存空间:

                1、修改jvm的启动参数,将jvm占据的内存减少,增加K8S容器可进行自由转换的游离内存空间,但是很遗憾运维坚决不同意,因为这部分启动参数是被所有服务发布时共用的。

                2、增加pod内存,同样可以大大增加K8S容器可进行自由转换的游离内存空间,作者采用了这种方式,但是其实这有一点资源浪费。

                3、研究K8S的内存机制并且进行修改,因为实际上将游离内存计算到限制里面并不是一种很好的算法,但是作者目前对于K8S、docker的容器的了解还处于初级水平,也没有额外时间去进行研究,这个方法只能搁置到后期。

 四、总结

        看到这里,大家应该明白作者为什么说这次特殊的oom问题不在于jvm,更大的责任在于容器的内存算法。

        随着技术不断发展,问题的原因也越来越复杂,以前的oom只需要根据jvm堆栈确认服务的代码问题,现在有时候却不能把oom全部甩给jvm了。

        如果有同学遇到类似作者这样的情况,确认与jvm无关,服务又没有使用堆外内存,不妨尝试增加K8S容器可进行自由转换的游离内存空间。

你可能感兴趣的:(jvm,K8S,docker,java)