前言
作为一个刚入职一坤(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虚拟机》都没有提及。
- 认识到压测的重要性