表达式引擎技术及比拟

Drools 简介

Drools(JBoss Rules )是一个开源业务规定引擎,合乎业内规范,速度快、效率高。业务分析师或审核人员能够利用它轻松查看业务规定,从而测验是否已编码的规定执行了所需的业务规定。

除了利用了 Rete 外围算法,开源软件 License 和 100% 的Java实现之外,Drools还提供了很多有用的个性。其中包含实现了JSR94 API和翻新的规定语义零碎,这个语义零碎可用来编写形容规定的语言。目前,Drools提供了三种语义模块

  • Python模块
  • Java模块
  • Groovy模块

Drools的规定是写在drl文件中。 对于后面的表达式,在Drools的drl文件形容为:

rule "Testing Comments"when    // this is a single line comment    eval( true ) // this is a comment in the same line of a patternthen    // this is a comment inside a semantic code blockend

When示意条件,then是满足条件当前,能够执行的动作,在这里能够调用任何java办法等。在drools不反对字符串的contians办法,只能采纳正则表达式来代替。
<!--more-->

IKExpression 简介

IK Expression 是一个开源的、可扩大的, 基于java 语言开发的一个超轻量级的公式化语言解析执行工具包。IK Expression 不依赖于任何第三方的 java 库。它做为一个简略的jar,能够集成于任意的Java 利用中。

对于后面的表达式,IKExpression 的写法为:

public static void main(String[] args) throws Throwable{    E2Say obj = new E2Say();    FunctionLoader.addFunction("indexOf",                                obj,                                E2Say.class.getMethod("indexOf",                                String.class,                                String.class));    System.out.println(ExpressionEvaluator.evaluate("$indexOf(\"abcd\",\"ab\")==0?1:0"));}

能够看到 IK 是通过自定义函数 $indexOf 来实现性能的。

Groovy简介

Groovy常常被认为是脚本语言,然而把 Groovy 了解为脚本语言是一种误会,Groovy 代码被编译成 Java 字节码,而后能集成到 Java 应用程序中或者 web 应用程序,整个应用程序都能够是 Groovy 编写的——Groovy 是非常灵活的。

Groovy 与 Java 平台十分交融,包含大量的java类库也能够间接在groovy中应用。对于后面的表达式,Groovy的写法为:

Binding binding = new Binding();binding.setVariable("verifyStatus", 1);GroovyShell shell = new GroovyShell(binding);boolean result = (boolean) shell.evaluate("verifyStatus == 1");Assert.assertTrue(result);

Aviator简介

Aviator是一个高性能、轻量级的java语言实现的表达式求值引擎,次要用于各种表达式的动静求值。当初曾经有很多开源可用的java表达式求值引擎,为什么还须要Avaitor呢?

Aviator的设计指标是轻量级和高性能,相比于Groovy、JRuby的轻便,Aviator十分小,加上依赖包也才450K,不算依赖包的话只有70K;当然,

Aviator的语法是受限的,它不是一门残缺的语言,而只是语言的一小部分汇合。

其次,Aviator的实现思路与其余轻量级的求值器很不雷同,其余求值器个别都是通过解释的形式运行,而Aviator则是间接将表达式编译成Java字节码,交给JVM去执行。简略来说,Aviator的定位是介于Groovy这样的重量级脚本语言和IKExpression这样的轻量级表达式引擎之间。对于后面的表达式,Aviator的写法为:

Map<String, Object> env = Maps.newHashMap();env.put(STRATEGY_CONTEXT_KEY, context);// triggerExec(t1) && triggerExec(t2) && triggerExec(t3)log.info("### guid: {} logicExpr: [ {} ], strategyData: {}",        strategyData.getGuid(), strategyData.getLogicExpr(), JSON.toJSONString(strategyData));boolean hit = (Boolean) AviatorEvaluator.execute(strategyData.getLogicExpr(), env, true);if (Objects.isNull(strategyData.getGuid())) {    //若guid为空,为check告警策略,间接返回    log.info("### strategyData: {} check success", strategyData.getName());    return;}

性能比照


Drools是一个高性能的规定引擎,然而设计的应用场景和在本次测试中的场景并不太一样,Drools的指标是一个简单对象比方有上百上千的属性,怎么疾速匹配规定,而不是简略对象反复匹配规定,因而在这次测试中后果垫底。
IKExpression是依附解释执行来实现表达式的执行,因而性能上来说也差强人意,和Aviator,Groovy编译执行相比,还是性能差距还是显著。

Aviator会把表达式编译成字节码,而后代入变量再执行,整体上性能做得很好。

Groovy是动静语言,依附反射形式动静执行表达式的求值,并且依附JIT编译器,在执行次数够多当前,编译成本地字节码,因而性能十分的高。对应于eSOC这样须要重复执行的表达式,Groovy是一种十分好的抉择。

场景实战

监控告警规定

监控规定配置效果图:

最终转化成表达式语言能够示意为:

// 0.t实体逻辑如下{"indicatorCode": "test001","operator": ">=","threshold": 1.5,"aggFuc": "sum","interval": 5,"intervalUnit": "minute",...}// 1.规定命中表达式triggerExec(t1) && triggerExec(t2) && (triggerExec(t3) || triggerExec(t4))// 2.单个 triggerExec 执行外部indicatorExec(indicatorCode) >= threshold

此时咱们只需调用 Aviator 实现表达式执行逻辑如下:

boolean hit = (Boolean) AviatorEvaluator.execute(strategyData.getLogicExpr(), env, true);if (hit) {    // 告警}

自定义函数实战

基于上节监控核心内 triggerExec 函数如何实现

先看源码:

public class AlertStrategyFunction extends AbstractAlertFunction {    public static final String TRIGGER_FUNCTION_NAME = "triggerExec";    @Override    public String getName() {        return TRIGGER_FUNCTION_NAME;    }    @Override    public AviatorObject call(Map<String, Object> env, AviatorObject arg1) {        AlertStrategyContext strategyContext = getFromEnv(STRATEGY_CONTEXT_KEY, env, AlertStrategyContext.class);        AlertStrategyData strategyData = strategyContext.getStrategyData();        AlertTriggerService triggerService = ApplicationContextHolder.getBean(AlertTriggerService.class);        Map<String, AlertTriggerData> triggerDataMap = strategyData.getTriggerDataMap();        AviatorJavaType triggerId = (AviatorJavaType) arg1;        if (CollectionUtils.isEmpty(triggerDataMap) || !triggerDataMap.containsKey(triggerId.getName())) {            throw new RuntimeException("can't find trigger config");        }        Boolean res = triggerService.executor(strategyContext, triggerId.getName());        return AviatorBoolean.valueOf(res);    }}

依照官网文档,只需继承 AbstractAlertFunction ,即可实现自定义函数,重点如下:

  • getName() 返回 函数对应的调用名称,必须实现
  • call() 办法能够重载,尾部参数可选,对应函数入参多个参数别离调用应用

实现自定义函数后,应用前须要注册,源码如下:

AviatorEvaluator.addFunction(new AlertStrategyFunction());

如果在 Spring 我的项目中应用,只需在 bean 的初始化办法中调用即可。

踩坑指南 & 调优

应用编译缓存模式

默认的编译办法如 compile(script) 、 compileScript(path 以及 execute(script, env) 都不会缓存编译的后果,每次都将从新编译表达式,生成一些匿名类,而后返回编译后果 Expression 实例, execute 办法会持续调用 Expression#execute(env) 执行。

这种模式下有两个问题:

  1. 每次都从新编译,如果你的脚本没有变动,这个开销是节约的,十分影响性能。
  2. 编译每次都产生新的匿名类,这些类会占用 JVM 办法区(Perm 或者 metaspace),内存逐渐占满,并最终触发  full gc。

因而,通常更举荐启用编译缓存模式, compile 、 compileScript 以及 execute 办法都有相应的重载办法,容许传入一个 boolean cached 参数,示意是否启用缓存,倡议设置为 true:

public final class AviatorEvaluatorInstance {  public Expression compile(final String expression, final boolean cached)  public Expression compile(final String cacheKey, final String expression, final boolean cached)  public Expression compileScript(final String path, final boolean cached) throws IOException  public Object execute(final String expression, final Map<String, Object> env,      final boolean cached)      }

其中的 cacheKey 是用来指定缓存的 key,如果你的脚本特地长,默认应用脚本作为 key 会占用较多的内存并消耗 CPU 做字符串比拟检测,能够应用 MD5 之类惟一的键值来升高缓存开销。

缓存治理

AviatorEvaluatorInstance 有一系列用于治理缓存的办法:

  • 获取以后缓存大小,缓存的编译后果数量 getExpressionCacheSize() 
  • 获取脚本对应的编译缓存后果 getCachedExpression(script) 或者依据 cacheKey 获取 getCachedExpressionByKey(cacheKey) ,如果没有缓存过,返回 null。
  • 生效缓存 invalidateCache(script) 或者 invalidateCacheByKey(cacheKey) 。
  • 清空缓存 clearExpressionCache() 

性能倡议

  • 优先应用执行优先模式(默认模式)。
  • 应用编译后果缓存模式,复用编译后果,传入不同变量执行。
  • 内部变量传入,优先应用编译后果的 Expression#newEnv(..args) 办法创立内部 env,将会启用符号化,升高变量拜访开销。
  • 生产环境切勿关上执行跟踪模式。
  • 调用 Java 办法,优先应用自定义函数,其次是导入办法,最初是基于 FunctionMissing 的反射模式。

往期精彩

集体技术博客:https://jifuwei.github.io/
公众号:是咕咕鸡
  • 性能调优——小小的log大大的坑
  • 性能优化必备——火焰图
  • Flink 在风控场景实时特色落地实战

参考:
[1].Drools, IKExpression, Aviator和Groovy字符串表达式求值比拟
[2].AviatorScript 编程指南