酝酿了很久才开始写这篇文章,实际就是记录一次线上问题处理的过程。在我们的日常工作中每天都会处理各种各样的问题,像线上环境内存飙高,服务出现频繁fullgc这一的问题也是面试中常见的问题,但是我们日常的开发中其实很少能遇到,故在此记录一下本次问题定位处理的过程希望能为大家遇到类似问题的时候提供一些参考。
1、背景介绍
问题出现在我们线上运行的一个soa服务,这个服务为商家端和审核端提供对应的业务,作为电商核心的商品系统每天也有不小的流量,同时也是一个运行多年的老系统存在着各种各样的小问题,整体架构为接入层对外提供rpc接口,service做逻辑处理,底层数据源使用mongodb。
2、问题发现与定位
问题出现在一个工作日的上午,系统监控不时的出现性能告警,并且线上也有用户反馈系统存在超时异常的现象。通过性能监控发现出现问题时总是有其中一台容器性能异常飙高(监控现象如下图),且每次都不是同一台容器,由此分析不是单台容器出现问题,是应用层逻辑有问题导致。
通过分析出现问题的机器发现每次性能飙高的同时容器也会出现cpu使用率高和网络流入高的告警而系统量较其他时段没有太大的变化,此应用并非cpu密集型应用,此时矛头就指向了gc,查看jvm监控发现在出问题的时段应用堆内存突然升高与此同时出现了大量的fullgc。
上面的监控我们发现了一个奇怪的想象,对内存突然飙高并且fullgc无法正常回收,所以出现持续二十多分钟的频繁fullgc。由此我们分析一定是存在大对象直接进入了老年代且存在持续的引用导致gc无法正常回收。到这时这个问题的处理脉络已逐渐清晰,我们只需要找到是哪些对象导致的,问题就引刃而解了。我们在出现问题的容器上将dump日志记录下来,通过mat工具分析dump日志发现有一个对象占了3个多G,那么罪魁祸首一定是它了。
通过引用关系继续向下排查,看到是有个ProductApply的List占用了3个多G。
结果如下:
看到执行线程发现时有一个rpc的线程,通过mat的Thread_Overview看看这个线程干了什么。
找到JSF-BZ-22000-609-T-217这个线程:
看到了一个 com.jd.vss.item.protocol.saf.impl.ProductAuditSafServiceImpl#findTaskByQuery这个方法,具体去看这个方法干了什么呢,发现这个方法主要执行的内容是重mongodb里面查询了一个审核列表然后在应用层做内存分页(我们在mat的分析中也能看到mongo的查询对象),随着数据量的增长,查到的List<ProductApply>这个List就越来越大,并且ProductApply这个对象是个商品信息的对象里面包含了商品图文详情这样的大字段本身对象就比较大。这也论证了我们排查过程中看到的一个现象每次出现问题时容器网络流入都会飙高,就是因为要从mongo服务器上查询这个大List导致的。
3、处理过程
问题分析清楚了我们处理起来也就容易了,让List<ProductApply>这个List变小就可以解决问题,出现问题的代码就是下面这段,只有在特殊情况下根据"提交时间"排序条件设置分页,其他情况下在内存中通过"上次审核时间"排序并且处理分页,分析之前这么写应该是由于"上次审核时间"这个字段没有索引,之前数据量比较小所以这么处理。
Query query = ProductAuditUtils.buildTaskQuery(queryDto);
if(queryDto.getState().intValue() != ProductState.PMPENDING && needSort){
query.with(new Sort(Sort.Direction.DESC, "applyTime"));
query.skip(offset);
query.limit(pageSize);
}
所以我们第一步就是对"上次审核时间"字段增加索引,然后代码中只处理排序条件,分页使用mongo来处理,修改如下
Query query = ProductAuditUtils.buildTaskQuery(queryDto);
if(queryDto.getState().intValue() != ProductState.PMPENDING && needSort){
query.with(new Sort(Sort.Direction.DESC, "applyTime"));
} else {
query.with(new Sort(Sort.Direction.DESC, "approveTime"));
}
query.skip(offset);
query.limit(pageSize);
在功能上我们也增加了分页大小的限制,防止出现用户设置分页数量过大导致类似问题。
4、思考
至此我们的问题就得到了解决,线上应用监控也恢复了正常。同时需要我们思考的点也有如下几方面:
(1)如何避免类目的代码逻辑出现,在编码规范上给出明确的规范避免无限大小的List出现。
(2)此类soa中心化的服务在出现类目问题时如何降低影响,本次的问题实际是审核端查询审核列表导致的,但是影响到了商家端的功能使用。后续可用通过应用分级,对不同等级的调用方进行分组隔离,避免出现类目相互影响的情况。