Nashorn在Hello出行物联网平台下的实践与性能调优

背景


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增量载入内存。

//全局变量
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方法。


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。如下:


/*** 系统自定义脚本内容: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);
  }
}


本文完!


如果‬本文‬对你‬有‬帮助‬的‬话‬,欢迎 点赞&在看&分享,这对我继续分享&创作优质文章非常重要。

非常感谢!另外‬推荐‬大家‬关注‬我‬的‬公众‬号:【陶‬朱公‬Boy】



里面不仅汇集了硬核的干货技术、还汇集了像左耳朵耗子、张朝阳总结的高效学习方法论、职场升迁窍门、软技能等‬。希望能辅助你达到另一个‬高度‬!

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章