加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

记一次线上Groovy导致的OOM的问题解决过程

发布时间:2020-12-14 16:40:44 所属栏目:大数据 来源:网络整理
导读:jobmgr模块是一个任务执行框架,里面有一个定时任务,定时从数据库读取最新的script,并通过groovy load成class,以此实现动态更新脚本。 代码如下: @Service ( "jobTemplateClassService" ) public class JobTemplateClassServiceImpl extends ScheduledTas

jobmgr模块是一个任务执行框架,里面有一个定时任务,定时从数据库读取最新的script,并通过groovy load成class,以此实现动态更新脚本。

代码如下:

@Service("jobTemplateClassService")
public class JobTemplateClassServiceImpl extends ScheduledTaskExecutor implements JobTemplateClassService {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    @Value("${galileo.api.endpoint}")
    private String endPoint;
    private String jobTemplateScriptListApi = "/job_template_script/list";
    private String jobTemplateScriptGetApi = "/job_template_script/get";
    private Map<String,Class<JobTemplate>> jobTemplateScriptMap = new HashMap<>();
    @Autowired
    private JobTemplateClassFactory jobTemplateClassFactory;

    @Override
    public Class<JobTemplate> getJobTemplateClass(String jobTemplateScriptUuid) {
        Class<JobTemplate> jobTemplateClass = null;
        if (null != jobTemplateScriptMap) {
            jobTemplateClass = jobTemplateScriptMap.get(jobTemplateScriptUuid);
        }
        if (null == jobTemplateClass) {
            JobTemplateScript jobTemplateScript = HttpClient.get(HttpUtils.buildRequestUrl(endPoint,jobTemplateScriptGetApi,new Parameter("jobTemplateScriptUuid",jobTemplateScriptUuid)),new TypeReference<JobTemplateScript>() {
            });

            if (null == jobTemplateScript) {
                return null;
            }
            jobTemplateScriptMap.put(jobTemplateScriptUuid,jobTemplateClassFactory.createJobTemplateClass(jobTemplateScript.getScript()));
        }
        return jobTemplateClass;
    }

    @Override
    protected void scheduledTask() {
        try {
            List<JobTemplateScript> jobTemplateScripts = HttpClient.get(HttpUtils.buildRequestUrl(endPoint,jobTemplateScriptListApi),new TypeReference<List<JobTemplateScript>>(){});
            Map<String,Class<JobTemplate>> newJobTemplateScriptMap = new HashMap<>();

            for (JobTemplateScript jobTemplateScript : jobTemplateScripts) {
                newJobTemplateScriptMap.put(jobTemplateScript.getUuid(),jobTemplateClassFactory.createJobTemplateClass(jobTemplateScript.getScript()));
            }

            logger.debug("JobTemplateClassService old map size : {}",this.jobTemplateScriptMap.size());
            //指向新的缓存对象 旧的对象无引用自动回收
            this.jobTemplateScriptMap = newJobTemplateScriptMap;
            logger.debug("JobTemplateClassService new map size : {}",this.jobTemplateScriptMap.size());
        } catch (RPCCallException e) {
            logger.error("Failed to init template script,galileo.api is not started?",e);
        } catch (Exception e) {
                logger.error("Failed to init template script",e);
        }
    }
}

问题描述:

在线上运行了将近半个月后,jobmgr模块OOM了

java.lang.OutOfMemoryError: PermGen space

猜测是类加载,没有卸载干净导致类无限制增长。
但是按理说groovy.load同一个类,新的应该覆盖旧的啊

问题解决

1.先确认问题确实出在此处

本地debug,使用jconsole观察类及PermGen内存使用情况.(本机jdk1.8因此是metaspace)

这里写图片描述


这里写图片描述

通过观察发现,经过20分钟过后,两个地方都依然稳定的增长,并且是和任务执行周期一致,一分钟增长一次。基本确定问题出在此处。

修改scheduled部分代码:

@Override
    protected void scheduledTask() {
        if(!scriptMap.isEmpty()) {
          return;
        }
        /* * */
    }

再次观察,约几分钟后,类趋于稳定,不再增长,至此确定问题所在。

2.追根溯源

启动VM参数加上-verbose查看类加载及卸载信息。

这里写图片描述


可以看到,每次更新脚本script,只会load class,但是没有unload.

永久代Class的卸载
方法区(永久代):
永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:
1.类的所有实例都已经被回收
2.加载类的ClassLoader已经被回收
3.类对象的Class对象没有被引用(即没有通过反射引用该类的地方)

猜测是groovy的类加载方式引起的旧map无法回收。查资料。
groovy class loader详解

这个博主讲的很详细,Very Good!但是看完发现问题不应该是groovy load()更新class的锅。试验一下.只加载类,无任何引用.

@Override
    protected void scheduledTask() {
        try {
            List<JobTemplateScript> jobTemplateScripts = HttpClient.get(HttpUtils.buildRequestUrl(endPoint,this.jobTemplateScriptMap.size());
            //指向新的缓存对象 旧的对象无引用自动回收
            //this.jobTemplateScriptMap = newJobTemplateScriptMap;
            newJobTemplateScriptMap = null;
            logger.debug("JobTemplateClassService new map size : {}",e);
        }
    }

发现问题依旧!!!
那么很显然,就是groovy无法卸载类的原因导致的!!!!

继续查资料
Groovy动态加载类踩中的那些坑
显然这个博主很靠谱,应该就是groovy自带的坑了!!!!

这里写图片描述

这个BUG已经被修复,修复版本为2.4.8,看了下我们groovy版本

<groupId>org.codehaus.groovy</groupId>
  <artifactId>groovy-all</artifactId>
  <version>2.4.7</version>

都是雷坑啊!!!!赶紧升级了看看.

升级完后,问题依旧。。。于是跟进开发人员2.4.8提交的看。说是确实修复了无法unload class的问题,这里写链接内容
仔细想了下,应该是没有gc导致的!
于是在代码里面加上System.gc();
再次观察,问题完美解决!
能够看到unload class BizPeriodPolicy了。

这里写图片描述

另外,参考大神的写法,每次load后clearCache()

//注意 不要一parse就clear,这时候新的还没put进map。
在完全load并put进缓存之后再clearCache。或者不加这一句
finally {
        groovyClassLoader.clearCache();
    }

总结:

此次BUG是因为groovy本身存在无法卸载class的bug导致。(持有一个强引用?)
解决方法为升级到2.4.8
groovy git提交2.4.8修复OOM

经过线上验证,发现确实升级后此BUG已经解决。
我们内存设置GC是95%,大概一星期左右,缓慢的线性上升到了95%触发了full Gc,Meta空间完美被释放。


后记:

最开始猜测是这样更新map不合理导致的。
就是说指向新的map,旧map自动回收没有成功。
搞的我纳闷死了!百思不得其解。
其实这个方法没什么问题!(map对象不多的情况下)

另外就是解决这种问题很有意思。

以后自己写程序一定要注意内存泄漏的问题!


参考:

JAVA内存和GC机制
JAVA动态编译

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读