背景:正常运行的一个某个业务系统,并发量比较大,相当于10万用户每5秒访问一次的数据量。因为某次发版之后,运行一段时间之后,通过监控,发现新生代内存,老年代内存都是99%的使用情况,单台实例服务IO线程达到600多,cpu使用率达到90%。GC垃圾回收时长大概比平常高出10倍以上
1.出现这样的情况,首先想到的是代码问题,不是环境配置,和服务器本身问题。但还是先检查了服务器和环境,最开始想到的是代码有for循环,以及大对象读取创建。
例如
for (int i=0;i<100000000;i++){ // 创建对象 Object o = new Object(); // 复杂逻辑处理 System.out.println(o.toString()); }
或者文件的加载读取
Workbook wb = null; InputStream is = getClass().getClassLoader().getResourceAsStream("2.xlsx"); wb = new XSSFWorkbook(is);
2.经过第一轮排查,并没有发现服务器有问题,然后查看dump记录(正常情况,通过dump即可发现问题),但是由于出现问题之后,系统比较重要,那边紧急回滚了,导致dump堆栈日志重新加载,删掉了记录。导致无法从这个地方打开入口
3.只能先查看配置以及代码,然后检查配置文件,比如.properties文件,和。yml文件以及其它加载文件
3.同样没有检查到问题。然后排查代码,并没有明细的for循环和大对象创建。这个时候由于业务非常复杂,我们采取分段排查方法。我们的流程当时是:接受数据-》解析数据-》组装数据-》处理数据-》发送数据
我们把:接受数据-》解析数据-》组装数据-》 分为第一段。分段发送这一段代码。查看和第三方对接是否有异常。
(由于线下环境,测试和预生产都模拟不出来上述情况)发了其中一台生产实例,然后通过ip代理指向我们特定的实例一些流量。检查没问题。
4.然后把:处理数据 分为第二段,上线同样的操作。出现了问题。关闭。再次认真 check 这段逻辑。终于在一个方法上发现了一个问题点:
本地缓存代码
// 从redis拿到数据,缓存到本地ConcurrentHashMap public static ConcurrentHashMap<String,xxxDto> map = new ConcurrentHashMap(); public xxxDto getxxxData(String terminalId){ if (map.isEmpty()) { // 从redis拿数据的方法 List<xxxDto> list = dataCov(); if (list == null) { return null; } for (int i =0 ;i < list.size();i++){ xxxDto xxxDto = list.get(i); map.put(xxxDto.getTerminalId(),xxxDto) ; } } if(map.isEmpty()){ return null; } return map.get(terminalId); }
看下这段代码有没有问题,当几万个用户,平均每5秒的频率访问。会不会出问题?
首先没有加锁。其次当并发量在几万的频率的话,并没有提前预加载redis缓存数据到本地缓存。 而是每次使用到,在请求redis读取,放入本地缓存。导致大批量请求来的时候。同时出发条件加载数据。在没有加载完时,还有源源不断请求过来。导致其它请求处于等待状态。越来越多线程过来都等待。堆积的对象越来越多。最终呈现的是新生代,老年代,内存满负荷运行,IO线程,CPU全部满负载异常情况。