一、引言
这是一次特殊的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容器可进行自由转换的游离内存空间。
五、后来
后来作者遇到一次mmap内存分配方式问题
也是非java内存导致的,tomcat给kill掉了,mmap属于操作系统内核层面的内存分配方式,他会预先分配内存给进程使用。4核的机器, 所以可以有32个64M的内存区域。在这个过程中加上进程使用了一部分非预先分配的内存,导致oom。
其实和k8s那个挺像的的,很可能是同一个问题,只不过这次运维没有捕捉到K8S oom killed内核日志,所以不能确定完全一样的原因。
这里采取的方案是改成jemlloc
不少文章是说glibc默认的内存分配器ptmalloc存在内存碎片化造成堆外不断上升的现象,推荐替换为jemalloc或tcmalloc,具体步骤是:
1、下载jemalloc
2、sudo yum install -y bzip2 gcc make graphviz 安装依赖工具
tar -xvf jemalloc-5.2.1.tar.bz2 解压
3、cd jemalloc-5.2.1 && ./configure --enable-prof && make && sudo make install 安装
4、sudo -u deploy /usr/local/bin/** --version 查看版本,是否安装成功
5、到tomcat目录,/opt/tomcat/bin/**.sh 编译启动文件,增加如下配置(挂载jemalloc)
6、export PRELOAD(一个环境变量,用于指定要在程序加载时预先加载的共享库)=/usr/local/lib/libjemalloc.so
7、export MALLOC_CONF(用于配置内存分配器的行为)=prof:true,lg_prof_interval:30(设置性能分析的采样间隔为 30),lg_prof_sample:17(设置性能分析的采样率为 17),prof_prefix:/**(设置性能分析结果输出的路径前缀)
8、sudo supervisorctl restart tomcat 执行重启tomcat
9、重启完成后,java进程的内存分配器就换成jemalloc