问题描述
客户反馈说应用运行了一段时间后,页面突然打不开了,运维说是cpu很高,而且日志有OOM内存不足,刚开始以为是内存不够,将这个客户的应用最大内存double之后,运行可一段时间,又出现了同样的问题,怀疑可能存在内存泄漏和其他问题,所以进行了分析定位。
问题定位过程
根据反馈的高CPU问题,按照传统方法进行了定位:
出现问题的原因:内存不够,JVM一直在fullgc,一秒可能有数十次fullgc
排查步骤
1.定位出高cpu的线程
进入容器,top -Hp 1(因为是在docker容器内,默认的应用pid就是1)
线程
隔一段时间再打印一下
cpu比较高的线程PID大概在10-26之间
2.printf ’%x\n' 10~26
打印出16进制的线程号
3.jstack 1 |grep 线程号,可以发现耗cpu的线程为gc线程
4.为了确认是gc引起的问题
jstat -gc 1
fullgc此处达到10万次+,说明大部分时间都消耗在了fullgc上面,而且年轻代和老年代内存都占满了,也没有释放。
解决思路:
临时解决
客户的最大内存设置为4G,目前来看是太小,这个客户的内存大小可以设置为8G。
但是为什么会出现内存不足的情况,通过测试环境测试,业务代码中没有出现内存泄漏的可能,基本上minorgc就可以将内存给释放掉,不可能出现内存不足的可能性,所以可能是框架代码出现了问题,导致了内存泄漏;从这个思路出发,将客户环境当前的内存情况进行了jmap,输出堆转储文件,然后将这个问题下载到本地进行分析。
使用JDK自带的visualVM进行堆转储文件进行分析
其中有几个类型占据了绝大部分内存,char[] 、String和LinkedHashMap,然后分别对这几部分进行分析,char[]大部分数据是这样的
抽丝剥茧后发现,这些字符串数据被DefaultNode这个类对象所引用,这个类有个成员变量rawTokens。这是Cassandra驱动里面的一个类,项目中的驱动依赖。
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-cassandra</artifactId>
<version>3.3.0</version>
</dependency>
查看下DefaultNode类源码:
然后我们搜一下DefaultNode
上面红圈是DefaultNode实例被引用的地方有三个地方
LoadBalancingPolicyWrapper的distances变量
此处的map是hashmap
ControlConnection#SingleThreaded成员变量
此处的map是weakhashmap
hashmap和weakhashmap的不同,参见强引用和弱引用的不同,weakhashmap如果key没有被强引用所引用,GC的时候就会释放掉,不管内存是否够用都会释放,但是因为该key又被强引用所把持,所以此处的weakhashmap是没有意义的,不会被释放
写一个例子说明下这两个引用的区别:
public static void main(String[] args) throws Exception{
AppConfig appConfig = new AppConfig();
Map<AppConfig,String> weakHp = new WeakHashMap<>();
weakHp.put(appConfig, "22333");
Map<AppConfig,String> strongHp = new HashMap<>();
strongHp.put(appConfig,"233443222");
appConfig = null;
System.gc();
Thread.sleep(10000L);
System.out.println(weakHp.size());
System.out.println(strongHp.size());
}
输出为
1
1
因为被key强引用的map所使用,所以此处虽然声明为弱引用也没有用
到此,问题的原因大概清楚了,cassandra驱动存在内存泄漏,创建了很多的DefaultNode对象,而且无法释放,但是为什么会创建如此多的DefaultNode对象呢,测试环境好像没出现此类问题,为了找到这个根源,继续分析
查看客户的线上日志发现,有段时间warn日志疯狂输出
有32个线程每隔10几秒就输出了这些日志,我怀疑这段时间可能cassandra出现了问题,然后疯狂创建了DefaultNode,而且应该回收的DefaultNode又没法回收,造成了问题,然后去网上搜索了下
https://www.coder.work/article/7883970
这个问题貌似与我们遇到的问题有点类似,但是目前解决方案好像也没有,官方提供的方案没有效果,还是会出现问题
解决方案
1.这本身是该cassandra驱动存在的内存泄漏的bug,已经向开源项目组提交问题,希望他们能及时修改此问题;
https://datastax-oss.atlassian.net/browse/JAVA-3051
2.定时任务定时清理掉多余的****DefaultNode
目前主要内存泄漏在于LoadBalancingPolicyWrapper#distances,写个定时任务,清理掉多余的数据,代码
/**
* 清理cassandra驱动导致的内存泄漏问题
* @throws Exception
*/
@Scheduled(cron = "0 */10 * * * ?")
public void clearDefaultNodes() throws Exception{
Map<String, LoadBalancingPolicy> loadBalancingPolicies
= cqlSession.getContext().getLoadBalancingPolicies();
Field field = DefaultLoadBalancingPolicy.class.getSuperclass().getDeclaredField("context");
field.setAccessible(true);
InternalDriverContext internalDriverContext
= (InternalDriverContext)field.get(loadBalancingPolicies.get("default"));
Field distancesField = LoadBalancingPolicyWrapper.class.getDeclaredField("distances");
distancesField.setAccessible(true);
LoadBalancingPolicyWrapper loadBalancingPolicyWrapper = internalDriverContext.getLoadBalancingPolicyWrapper();
Map<Node, Map<LoadBalancingPolicy, NodeDistance>> distances
= (Map<Node, Map<LoadBalancingPolicy, NodeDistance>>)distancesField.get(loadBalancingPolicyWrapper);
//当distances为空或者distances数量小于10000不处理,超过10000认为是有异常的
if (MapUtils.isEmpty(distances)) {
return;
}
int distancesSize = distances.size();
logger.info("distances size is {}", distancesSize);
if (distancesSize <=10000) {
return;
}
Field distancesLockField = LoadBalancingPolicyWrapper.class.getDeclaredField("distancesLock");
distancesLockField.setAccessible(true);
Lock lock = (Lock) distancesLockField.get(loadBalancingPolicyWrapper);
lock.lock();
try {
distances.clear();
}finally {
lock.unlock();
}
}
网友评论