一次线上OOM事故,学会一个小众JVM参数

前言

作为一个刚入职一坤(2.5)月的校招生,本想着能好好的水一段时间,万万没想到带我的menter让我去复盘一个线上OOM事故,并给出一个解决方案。我当时心情

首先,这个OOM事故发生后menter就做了紧急的措施:重启机器、回滚代码,让这个事故没有产生更大的影响。

事故现象

简单描述一下这个事故现象,首先我们收到了多个服务的接口调用异常告警,当我们正在排查这些服务为什么突然告警的过程中,又收到了服务A(内部项目不便透露)发生了OOM。然后我们就意识到了为什么那些服务发生了告警,因为这些告警服务都依赖喻服务A,服务A发生了OOM使得依赖它的服务都发生了告警。在服务A发生OOM后,它的2台机器直接宕机了。我们立马采取了补救措施:重启机器,回滚代码。 事后查看各种各种监控平台发现,是因为发生了Full GC但未成功回收到预期的空间发生了OOM。

事故背景

在一个平常的发布中,项目代码发布完成后,需要通过Apollo打开一个开关。这个开关打开后,会将原本单引擎的规则计算变为双引擎计算,新增的计算引擎为Aviator。

此处的规则计算概念就是现今市场流行的规则引擎(如:drools)计算概念

排查过程

具体的排查过程,由表象OOM问题逐步深入到JVM元空间的垃圾回收、Aviator引擎底层源码。

为什么发生Full GC

首先,从表象看是发生了OOM。发生OOM前肯定会发生Full GC,机器才刚启动为什么会立马将堆内存打满呢?查看监控发现,确实发生了Full GC,但发生Full GC时堆空间内存使用占比不足5%。这个时候就更懵逼了。那是什么原因触发了Full GC呢?再更细致的查看监控后,发现发生Full GC时metaSpace空间使用占比同时也飙升。

此时,不由的怀疑触发Full GC是否是因为metaSpace空间使用占比飙升引起的呢?但通过监控可以看到发生Full GC时metaSpace使用占比仅仅在75%左右,并没有达到设置的MaxMetaspaceSize=256m。在翻阅了《深入理解Java虚拟机》以及众多技术博客想去了解metaSpace扩容原理后还是没有找到对应的答案。这个时候不得不说大数据的神奇了,当我在各大技术论坛、网站搜索这类信息后,给我推了这篇文章,让我了解到了MaxMetaspaceFreeRatio参数,这个参数意思是当metaSpace空间占比达到该阈值后就进行Full GC,默认为70%。这就可以闭环为什么会在75%左右发生Full GC了。

metaSpace空间使用占比为什么会飙升

在解决为什么会发生Full GC问题后,此时就引入了另外一个问题,metaSpace空间使用占比为什么会飙升。懵逼的我是想到懵逼,这该怎么查啊。回想当初学习JVM的时候对metaSpace的了解只停留在metaSpace中存储着类的元数据、字符串常量等信息。到这里心想只能去分析dump文件了。经过多次踩坑分析后,发现一个奇特的现象,metaSpace中存在大量以"Script_"开头的类的元数据和大量的AviatorClassLoader类加载器。

看到AviatorClassLoader类加载器这个名字,这不呼应上了嘛。我们当时的操作就是开启了Aviator引擎。 而后,为了验证这一猜测,我在测试环境进行了事故复现。发现确实是因为"Script_"开头的类的元数据和大量的AviatorClassLoader类加载器大致的metaSpace空间使用占比飙升。

为什么会产生大量"Script_"开头的类和AviatorClassLoader类加载器

后续,我对项目中调用的Aviator代码以及引入的Aviator源码进行了分析。发现 项目中调用Aviator的这个方法

css 复制代码
AviatorEvaluator.compile(a + "+" + b);

在Aviator引擎中实际存在着多个重载实现,其中包含了缓存方法。

typescript 复制代码
public static Expression compile(String expression, boolean cached) {
    return getInstance().compile(expression, cached);
}

public static Expression compile(String expression) {
    return compile(expression, false);
}

我们在项目中使用的是不使用缓存的方法。深入分析该方法后发现若不使用缓存方法,Aviator每次调用都将new一个AviatorClassLoader类加载器,并且为每个计算表达式生成一个以"Script_"开头的类。

kotlin 复制代码
private Expression innerCompile(final String expression, final String sourceFile,
    final boolean cached) {
  ExpressionLexer lexer = new ExpressionLexer(this, expression);
  CodeGenerator codeGenerator = newCodeGenerator(sourceFile, cached);
  ExpressionParser parser = new ExpressionParser(this, lexer, codeGenerator);
  Expression exp = parser.parse();
  if (getOptionValue(Options.TRACE_EVAL).bool) {
    ((BaseExpression) exp).setExpression(expression);
  }
  return exp;
}

public AviatorClassLoader getAviatorClassLoader(final boolean cached) {
  if (cached) {
    return this.aviatorClassLoader;
  } else {
    return new AviatorClassLoader(this.getClass().getClassLoader());
  }
}

public ASMCodeGenerator(final AviatorEvaluatorInstance instance, final String sourceFile,
    final AviatorClassLoader classLoader, final OutputStream traceOut) {
  super(instance, sourceFile, classLoader);
  this.className = "Script_" + System.currentTimeMillis() + "_" + CLASS_COUNTER.getAndIncrement();
  this.classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
  visitClass();
}

public Expression getResult(final boolean unboxObject) {
  end(unboxObject);

  byte[] bytes = this.classWriter.toByteArray();
  try {
    Class<?> defineClass =
        ClassDefiner.defineClass(this.className, Expression.class, bytes, this.classLoader);
    Constructor<?> constructor =
        defineClass.getConstructor(AviatorEvaluatorInstance.class, List.class, SymbolTable.class);
    BaseExpression exp = (BaseExpression) constructor.newInstance(this.instance,
        new ArrayList<VariableMeta>(this.variables.values()), this.symbolTable);
    exp.setLambdaBootstraps(this.lambdaBootstraps);
    exp.setFuncsArgs(this.funcsArgs);
    exp.setSourceFile(this.sourceFile);
    return exp;
  } catch (ExpressionRuntimeException e) {
    throw e;
  } catch (Throwable e) {
    if (e.getCause() instanceof ExpressionRuntimeException) {
      throw (ExpressionRuntimeException) e.getCause();
    }
    throw new CompileExpressionErrorException("define class error", e);
  }
}

这样看来,这次OOM事故的这个原因分析的就差不多了。是由于调用Aviator引擎时compile()方法并没有使用缓存,使得每次计算时都会创建一个AviatorClassLoader类加载器以及以"Script_"命名开头的类。当大量流量进入后,使得metaSpace使用占比飙升,进而产生了OOM。

解决思路

在排查完问题原因后,提出了如下解决方案:

  • 使用Aviator缓存方法: 将原本的compile("1+1")方法替换为compile("1+1",true)缓存方法,避免相同的计算表达式重复计算,导致大量用途相同的AviatorClassLoader和"Script_"类重复产生。
  • 修改JVM参数,提高metaSpace最大内存 将原本MaxMetaspaceSize=256m修改为MaxMetaspaceSize=512m,因为在未发生OOM时metaSpace空间占比已经达到了65%距离触发Full GC的70%的波动空间较小。

那么为什么不修改MaxMetaspaceFreeRatio这个参数呢? 公司中间件不支持修改这个参数

  • 上线前压测 这个事故的产生,同时也暴露出了我们组对部分高QPS的接口并没有进行充分了压测。在后续开发过程中,需要对高QPS接口进行充分的压测,让这类问题扼杀在摇篮之中。

收获

通过这次事故原因复盘经历,我收获了很多实际问题解决的经验。

  • 对此类问题有了基本的排查方向和思路
  • 认识到了一个很小众的JVM参数:MaxMetaspaceFreeRatio。这个参数被提及的很少,就连大名鼎鼎的《深入理解Java虚拟机》都没有提及。
  • 认识到压测的重要性
相关推荐
刘大辉在路上1 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
东阳马生架构2 小时前
JVM实战—1.Java代码的运行原理
jvm
追逐时光者3 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql
初晴~3 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱581364 小时前
InnoDB 的页分裂和页合并
数据库·后端
小_太_阳4 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾4 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
ThisIsClark4 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
王佑辉5 小时前
【jvm】内存泄漏与内存溢出的区别
jvm