K8S集群问题——案例一:Java调用Glibc2.28-69内存分配器无法限制虚拟内存VIRT问题
一、问题描述
1、背景:
租户反馈,Apr 7 11:22容器出现夯死现象, 容器部署的单个java进程;
宿主机上,top显示的容器进程virt内存持续增长32G,目前messages日志没有看到oom的记录,基本是。租户其他bc7、8系统上有添加参数MALLOC_ARENA_MAX进行限制,基本维持在16G左右,目前bcoe21.10系统配置参数MALLOC_ARENA_MAX可能没有生效,导致宿主机内存碎片问题;
2、容器内部java进程的虚拟内存异常增长30g-60g
二、排查过程
1、租户三台测试K8S节点主机使用MALLOC_ARENA_MAX=4限制内存可分配块的创建、合并、回收,传参给容器,由于java线程占用64M内存无法及时释放,导致主内存区块被占满,后续java线程请求不断从其他内存块分配内存,导致内存持续增长,尝试解决办法:调整MALLOC_ARENA_MAX=1和glibc版本;
ii.三节点调整MALLOC_ARENA_MAX=1
export MALLOC_ARENA_MAX=1
参考:
https://blog.csdn.net/m0_38017860/article/details/122192243
查看虚拟内存堆栈调用详情:pmap -x 进程ID
发现有61880KB的匿名线程的内存调用
iii.结果
两台测试机156是2.28-84和157是2.28-93,内存分配参数malloc_arena_max=1可以做到局部内存块回收,容器虚拟内存至17.3G左右,但是三台测试机均出现javacore问题;
2、测试发现glibc2.28-69、glibc2.28-93、glibc2.28-97内存分配器无法做到有效限制VIRT虚拟内存,尝试调整新的tcmalloct内存分配器;
i.安装方法:
tcmalloct分配器安装包:
https://github.com/gperftools/gperftools/releases/download/gperftools-2.15/gperftools-2.15.tar.gz
部署方案:
tar -zxvf gperftools-2.15.tar.gz cd gperftools-2.15 ./configure --enable-frame-pointers make -j8 make install echo /usr/local/lib > /etc/ld.so.conf.d/libtcmalloc.conf ldconfig 编译test3.c gcc test3.c -o test3 -lpthread -ltcmalloc
测试代码
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #define NUM_THREADS 10 void *allocate_memory(void *arg) { size_t *thread_id = (size_t *) arg; for (int i = 0; i <64*1024*1024; i+=1024){ char* addr = (char*) malloc(1024); } // 为了让程序持续运行,这里让线程进入无限循环 while (1) { sleep(1); // 每秒休眠一次,避免无谓的 CPU 占用 } return NULL; // 实际上永远不会到达此处 } int main() { pthread_t threads[NUM_THREADS]; size_t thread_ids[NUM_THREADS]; for (size_t i = 0; i < NUM_THREADS; ++i) { thread_ids[i] = i; if (pthread_create(&threads[i], NULL, allocate_memory, &thread_ids[i]) != 0) { fprintf(stderr, "Failed to create thread %zu.\n", i); exit(EXIT_FAILURE); } } // 主线程同样进入无限循环,确保程序不退出 while (1) { sleep(1); // 每秒休眠一次,避免无谓的 CPU 占用 } return 0; // 实际上永远不会到达此处 }
ii.k8s节点,调整容器java配置
涉及应用容器java配置的调整,用于调用libtcmalloct库;
iii.验证libtcmalloct内存分配器库的调用情况
top #按两下e键(切换内存单位GB),按M切换内存为降序,按P切换CPU为降序,观察CPU或内存使用率较高的进程;
# VRIT:虚机内存数
# RES:物理内存数
# VRIT:RES为10:1
pmap -x java进程id | grep ‘libtcmalloc.so‘ #抓取java线程通过内存分配器分配ARENA内存块数量级,一、4*内核数(32bit处理器);二、8*内核数(64bit处理器);
iv.三台测试机的调整详情
156:K8S节点主机部署TCMALLOCT内存分配器; 157:建议更换jdk版本; 158:通过yum源升级升级glibc版本;
v.结果
经过一天的观察, 调整内存分配器后,目前java容器内存稳定17.3g,期间客户有调整jdk版本,经过业务压测没有出现javacore的问题;
三、根因
这种情况是由于业务量较大,导致glibc可分配主内存空间数占满,后续申领主内存块时,导致MALLOC_ARENA_MAX 限制的可分配最大内存空间数失效;
那么按照测试参数1是否可以解释放堆顶层线程占用内存块?有明显的效果,可以做到部分内存块回收;
通过tcmalloc内存分配器,来解决后续glibc申请主内存块不足的问题,可以做到有效回收主内存块空间,维持17.3G。
主要问题是由于客户的jdk版本问题,jdk版本代码存在内存泄漏导致匿名内存块的持续消耗。由于栈顶java线程申领的主内存块没有释放变为匿名内存,持续占用主内存空间,导致后续java线程没有主内存块可以申领,后续非主内存持续消耗,top中查看业务线程的虚拟内存VIRT的异常消耗(32G~60G),通过安装tcmalloct内存分配器,可以有效的限制java线程的VIRT申领维持在17.3G左右,后续客户调整jdk版本,观察容器加载业务后,目前java容器没有出现javacore的现象;