周二早上正在带薪拉S,群里突然弹出一个消息说一个统计报表数据无法加载了。
作为多年见过世面的老程序员,我当然不能轻而易举放弃等了一刻钟的坑位,于是@了一下鸡哥,麻烦鸡哥跟进一下。
过了半个小时,我回到工位,看见鸡哥愁眉苦脸,我这时候适时候的用湿手撸了撸头发说,94年的小年轻,不小了。重启一下服务吧。
果然好了。
享受着鸡哥崇拜的眼神。但是善于思考的我并没有停止思考,到底是什么原因呢?
项目背景:
该报表群会每隔两个小时从数仓中拉取原始数据,加载到redis/以及java对象中。原始数据比较大。可能会有2G左右。
看到项目日志,果然有OOM。
于是我直接dump了一份预发上的日志,用eclipse Anylisis分析一下。扫描了三个比较大的对象,发现前两个对象MRData和overviewData都是之前预存到java缓存中的原始数据。应该是预期之内的。但是第三个对象,HashMap,大概有20多万条,并且里面的数据和MRData和overviewData对象中保持的hashmap几乎是一致。
为什么会duplicate这么多hashmap,是如何引发的呢?
考虑到日志中的OOM,均发生在quatz同步数据的时候,我想,会不会是同步数据的时候,旧的那份数据没有清除呢?
于是我拉上鸡哥一起走读代码:
发现在计算逻辑内,有大量threadLocal变量:
而这些threadLocal都指向了之前缓存的那些java对象。
果然,这应该就是罪魁祸首。
虽然我们每次做原始数据刷新的时候,都把缓存对象指向了从数仓里读取的新的对象。但是之前那些threadLocal对象,是由tomcat线程池生成的,如果这些线程没有释放的话,那么依然保持着对旧的对象的强引用。因此,就会出现:即使刷新job中对缓存对象的reference改变了,但这些旧的缓存对象依然没有释放的情况。
所以,我们要做的,就是每次去取java缓存对象的时候,用完之后将threadLocal中的对象直接从线程中的map里remove掉。
修改之后发线上,基本内存保持稳定。
这次引发我几个思考
第一,90%的OOM问题都是由代码不规范引发,因此要从业务上排查OOM。
第二,对业务逻辑的理解在OOM排查很重要,比如鸡哥很了解业务,一眼就能看出问题。
第三,高并发情况下threadLocal变量及时释放,做到有借有还,再借不难。