• 作者:老汪软件技巧
  • 发表时间:2024-10-03 17:01
  • 浏览量:

前言

作为一个刚入职一坤(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的这个方法

AviatorEvaluator.compile(a + "+" + b);

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

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_”开头的类。

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(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。

解决思路

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

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

收获

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