一、引言
这是一次特殊的pre环境的内存溢出导致服务被K8S杀掉,为什么说它是特殊的,因为可以说这不是jvm的锅!
二、环境及现象
1、pod内存分配
设置该服务所在pod的限制为2G内存,超过2G就会达到K8S的oom界限,被K8S重启。
2、jvm设置
3、现象
服务在pre环境重启,K8S层面看下来是由于该pod使用内存过大超出限制导致的oom重启(并未被驱逐,排除被物理机OOM),但是JVM堆内并没超过限制。
三、排查与解决
1、堆外内存
jvm堆内存既然没有超过限制,怀疑方向指到了堆外内存,但是排查了整个服务之后,没有使用nio进行堆外存储,第三方中间件例如rocketMq、Redisson等是基于Netty进行消息收发的,但是它们使用的nio非常少,可以忽略。
2、NativeMemoryTracking
让运维开启了NMT追踪jvm使用的所有内存,可以看出,jvm整体使用的内存离2G海域一段距离。
通过pod监控可以看出在jvm使用1.6g左右内存时,pod内存达到了1.95g左右,濒临K8S的kill界限。
通过查阅相关资料了解到在当前的容器化环境中,jvm释放的内存不会立刻被转化为虚拟机内存,jvm可以对这部分内存随时取用,但是它不归属于jvm,因此不会实时的触发gc。方向似乎清晰了,K8S很可能将这部分游离内存和jvm内存进行累加,达到2G限制就进行oom重启。
3、持续观察NMT和pod节点内存变化
有了方向就产生了新的疑问,容器这部分游离的内存随什么变化?还是毫无规律?是否是线程数量或者类加载?
带着这些疑问,作者开始了持续两个星期的跟踪观察,但是很遗憾,从观察结果来看,游离内存变化几乎毫无规律。
4、解决
其实在有了方向之后就已经可以知道解决方案了,那就是增加K8S容器可进行自由转换的游离内存空间:
1、修改jvm的启动参数,将jvm占据的内存减少,增加K8S容器可进行自由转换的游离内存空间,但是很遗憾运维坚决不同意,因为这部分启动参数是被所有服务发布时共用的。
2、增加pod内存,同样可以大大增加K8S容器可进行自由转换的游离内存空间,作者采用了这种方式,但是其实这有一点资源浪费。
3、研究K8S的内存机制并且进行修改,因为实际上将游离内存计算到限制里面并不是一种很好的算法,但是作者目前对于K8S、docker的容器的了解还处于初级水平,也没有额外时间去进行研究,这个方法只能搁置到后期。
四、总结
看到这里,大家应该明白作者为什么说这次特殊的oom问题不在于jvm,更大的责任在于容器的内存算法。
随着技术不断发展,问题的原因也越来越复杂,以前的oom只需要根据jvm堆栈确认服务的代码问题,现在有时候却不能把oom全部甩给jvm了。
如果有同学遇到类似作者这样的情况,确认与jvm无关,服务又没有使用堆外内存,不妨尝试增加K8S容器可进行自由转换的游离内存空间。