背景
Hello出行物联网平台继1.0使用Jsqlparse来作为规则引擎的方案。
随着场景越来越复杂,用原来的方案满足不了当下业务场景。
规则匹配各种If Else 条件判断。 响应参数各种拼装组合等。对灵活度提出了更高的要求。
所以2.0架构我们开始着手进行以JS脚本语言作为载体,用JS来编辑规则。
用JS作为规则脚本我们需要做到JS能调用后端API接口,API接口能调用JS本地方法,经过多次技术调研,我们选择了JDK1.8的Nashorn引擎来作为最终落地方案。
这里我简单介绍一下Nashorn相关知识,方便大家了解它是什么,能用来解决什么问题?
Nashorn简介
Nashorn是一个以Java编程语言开发的JavaScript 引擎,最初由Oracle开发,后来由 OpenJDK 社区开发。
它依赖于对 Java 平台 (JSR 292) 上的动态类型语言的支持(这个概念首先在实验性的达芬奇机器中实现,并且是 Java 7 及更高版本的标准部分。)Nashorn 已包含在Java 8到 JDK 14 中。
从 JDK 6 开始,Java 就已经捆绑了JavaScript 引擎,该引擎基于 Mozilla 的 Rhino 。该特性允许开发人员将 JavaScript 代码嵌入到 Java 中,甚至从嵌入的 JavaScript 中调用 Java。 此外,它还提供了使用 jrunscript 从命令行运行 JavaScript 的能力。如果不需要非常好的性能,并且可以接受 ECMAScript 3 有限的功能集的话,那它相当不错了。 从 JDK 8 开始, Nashorn 取代 Rhino 成为 Java 的嵌入式 JavaScript 引擎。Nashorn 完全支持 ECMAScript 5.1 规范以及一些扩展。它使用基于 JSR 292 的新语言特性,其中包含在 JDK 7 中引入的 invokedynamic,将 JavaScript 编译成 Java 字节码。 与先前的 Rhino 实现相比,这带来了 2 到 10 倍的性能提升,虽然它仍然比Chrome 和Node.js 中的V8 引擎要差一些
性能调优
在生产使用的过程中,我们通过上线前的压测,对核心链路部分做出了相应的代码优化,避免流量过大应用性能下降甚至雪崩发生。
优化一:
调整CompiledScript对象,避免频繁CmsGc最终触发FullGc。
由于规则引擎层是流量的入口,基本上网关的流量都会经过物模型解析后会透传到这里。
所以每次的设备消息,都需要经过Nashorn根据指定的规则(提前配置好的规则脚本)作为前置判断,我们线上接口QPS大概有1W 。
前期上线的时候没有将CompiledScript缓存起来,以至于每次来一个设备消息就需要新一个CompiledScript对象。
从Context.compileScript() 入口源码分析看:需要经历的过程如下:[ JavaScript源码 ] -> ( 语法分析器 Parser ) -> [ 抽象语法树(AST) ir ] -> ( 编译优化 Compiler ) -> [ 优化后的AST Java Class文件(包含Java字节码) ] -> JVM加载和执行生成的字节码 -> [ 运行结果 ]
此过程是十分耗时的,每次执行eval 去运行js ,都需要编译成字节码、然后加载执行。同时会将编译过的字节码缓存起来,以便后续使用,因此加载的类会长时间存活,占用很大的内存空间,所以容易导致老年代空间占比非常大:详见图1
dump了堆文件后发现scripts.JO这个对象占比非常大。
于是我们做了优化,因为现实场景下 商户配置完规则后,基本是不会二次修改,所以我们尝试将规则放在本地内存中,启动时全量载入本地内存,以后会通过RocketMQ增量载入内存。
代码语言:javascript复制//全局变量
private static Map scriptMap = new ConcurrentHashMap<>();
//一个resolverId对应一个脚本编译实例对象
public CompiledScript compileScript(Long resolverId, String script) {
if (scriptMap.get(resolverId) != null) {
return scriptMap.get(resolverId);
}
synchronized (ScriptResolverAble.class) {
if (scriptMap.get(resolverId) == null) {
script = SYSTEM_FUNCTION_CONTENT script;//SYSTEM_FUNCTION_CONTENT是系统函数String内容
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
Compilable compEngine = (Compilable) engine;
try {
CompiledScript compile = compEngine.compile(script);
scriptMap.put(resolverId, compile);
return compile;
} catch (Exception e) {
log.error("initCompileScript failure;" e.getMessage(), e);
return null;
}
}
return scriptMap.get(resolverId);
}
}
优化二:
不要每次都去URL.connection的方式去加载内置的系统函数。
我们开放平台自定义了不少函数,供商户配置JS脚本时使用,比如诸多目的地函数:writeMq写入对应Topic业务方。
payload()获取设备消息,还有一些元配置信息,比如Java.type的定义 才有能力JS调用服务端API方法。
代码语言:javascript复制var instance = Java.type('xx.script.TargetHandlerCallback');
var writeHelloMq=function(targetId,data){
var json=JSON.stringify(data);
instance.invoke(..,targetId,json,1);
};
大家看问题一的加注释的SYSTEM_FUNCTION_CONTENT部分 ,内容就是来自上述内置脚步的内容。
不过如果你用默认的处理方式,即每次都是用URLConnection去拉取内容,像线上环境流量比较高,很容易导致open too many files异常,这个我们在压测的时候也看到了这一点。
所以我们也做了相应优化,直接应用启动的时候,放入类静态变量中,下次直接取就OK。如下:
代码语言:javascript复制/**
* 系统自定义脚本内容:system_fun.js
*/
private static String SYSTEM_FUNCTION_CONTENT;
@PostConstruct
public void init() {
try {
URL resource = ScriptResolverAble.class.getClassLoader().getResource("system_fun.js");
File file = new File(resource.getFile());
SYSTEM_FUNCTION_CONTENT = FileUtils.readFileToString(file, StandardCharsets.UTF_8.toString());
log.info("system script :{}", SYSTEM_FUNCTION_CONTENT);
} catch (IOException e) {
log.error("load system_fun.js error,detail:" e.getMessage(), e);
}
}
结尾
上述两处调优基本讲解完毕,希望对大家在生产环境使用有所帮助。