Google Aviator - a lightweight Java expression engine in action

Expression Engine Technology and Comparison

Introduction to Drools

Drools (JBoss Rules) is an open source business rule engine that complies with industry standards and is fast and efficient. It allows a business analyst or reviewer to easily view business rules to verify that the coded rules enforce the desired business rules.

In addition to applying the Rete core algorithm, open source software License and 100% Java implementation, Drools also provides many useful features. These include the implementation of the JSR94 API and an innovative rule semantics system that can be used to write a language describing rules. Currently, Drools provides three semantic modules

  • Python module
  • java module
  • Groovy module

Drools rules are written in drl files. For the previous expression, the drl file description in Drools is:

rule "Testing Comments"
when
    // this is a single line comment
    eval( true ) // this is a comment in the same line of a pattern
then
    // this is a comment inside a semantic code block
end

When indicates a condition, then is an action that can be performed after the condition is met, and any java method can be called here. Drools does not support the contians method of strings, and can only be replaced by regular expressions.

Introduction to IKEExpression

IK Expression is an open source, extensible, ultra-lightweight formula language parsing and execution toolkit developed based on java language. IK Expression does not depend on any third-party java library. It comes as a simple jar that can be integrated in any Java application.

For the previous expression, IKEExpression is written as:

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"));
}

It can be seen that IK realizes the function through the custom function $indexOf.

Introduction to Groovy

Groovy is often thought of as a scripting language, but it is a misunderstanding to understand Groovy as a scripting language. Groovy code is compiled into Java bytecode, which can then be integrated into Java applications or web applications. The entire application can be written in Groovy - Groovy is very flexible.

Groovy is very integrated with the Java platform, including a large number of java class libraries that can also be used directly in groovy. For the previous expression, the Groovy way of writing is:

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

Introduction to Aviator

Aviator is a high-performance, lightweight expression evaluation engine implemented in java language, mainly used for dynamic evaluation of various expressions. There are already many open source java expression evaluation engines available, why do we need Avaitor?

The design goal of Aviator is lightweight and high performance. Compared with the bulkiness of Groovy and JRuby, Aviator is very small, and the dependent package is only 450K, and it is only 70K if the dependent package is not included; of course,

Aviator's grammar is limited, it is not a complete language, but only a small set of languages.

Secondly, the implementation idea of ​​Aviator is very different from other lightweight evaluators. Other evaluators generally run through interpretation, while Aviator directly compiles expressions into Java bytecodes and hand them over to the JVM for execution. Simply put, Aviator's positioning is between a heavyweight scripting language like Groovy and a lightweight expression engine like IKEExpression. For the previous expression, Aviator is written as:

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;
}

performance comparison

![image.png](https://img-blog.csdnimg.cn/img_convert/1dcde8235941a68d710a8e3b76d9bd80.png#clientId=u88b7feeb-0a39-4&crop=0&crop=0&crop=1&crop=1&errorMessage=unknown error&from=paste&height=182&id=uc2614e9c&margin=[object Object]&name=image.png&originHeight=364&originWidth=639&originalType=binary&ratio=1&rotation=0&showTitle=false&size=88291&status=error&style=none&taskId=ua8ae3403-82f7-4b77-8f38-7ce38f375fb&title=&width=319.5)![image.png](https://img-blog.csdnimg.cn/img_convert/6243aab52ed1c0cac78f45d9549d290b.png#clientId=u88b7feeb-0a39-4&crop=0&crop=0&crop=1&crop=1&errorMessage=unknown error&from=paste&height=169&id=u03154084&margin=[object Object]&name=image.png&originHeight=267&originWidth=631&originalType=binary&ratio=1&rotation=0&showTitle=false&size=54927&status=error&style=none&taskId=u1c361e01-22f7-4c00-b9b9-603d81a8cff&title=&width=400.5)

Drools is a high-performance rule engine, but the designed usage scenario is not the same as the one in this test. Drools aims at how to quickly match rules for a complex object such as hundreds or thousands of attributes, rather than repeated matching rules for simple objects, so the result in this test is at the bottom.
IKEExpression relies on interpretation and execution to complete the execution of expressions, so the performance is not satisfactory. Compared with Aviator and Groovy compilation and execution, the performance gap is still obvious.

Aviator will compile the expression into bytecode, and then substitute it into a variable before executing it. The overall performance is very good.

Groovy is a dynamic language, which relies on reflection to dynamically execute the evaluation of expressions, and relies on the JIT compiler to compile it into local bytecode after enough execution times, so the performance is very high. Groovy is a very good choice for expressions such as eSOC that need to be executed repeatedly.

Scenario actual combat

Monitoring and alerting rules

监控规则配置效果图:
![image.png](https://img-blog.csdnimg.cn/img_convert/8b3c4d47f14901c633da5e78cb3599e1.png#clientId=u88b7feeb-0a39-4&crop=0&crop=0&crop=1&crop=1&errorMessage=unknown error&from=paste&height=166&id=ubd7f027b&margin=[object Object]&name=image.png&originHeight=332&originWidth=663&originalType=binary&ratio=1&rotation=0&showTitle=false&size=49332&status=error&style=none&taskId=u049068a3-78ca-46fa-a8c1-54002917065&title=&width=331.5)

The final transformation into an expression language can be expressed as:

// 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

At this point, we only need to call Aviator to implement the expression execution logic as follows:

boolean hit = (Boolean) AviatorEvaluator.execute(strategyData.getLogicExpr(), env, true);

if (hit) {
    
    
    // 告警
}

Custom function practice

triggerExecHow to realize the inner function of the monitoring center based on the previous section

First look at the source code:

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);
    }
}

According to the official documentation, AbstractAlertFunctionyou can implement custom functions just by inheriting. The key points are as follows:

  • getName() Returns the calling name corresponding to the function, which must be implemented
  • The call() method can be overloaded, and the tail parameters are optional. The corresponding function input parameters can be called separately with multiple parameters.

After implementing the custom function, you need to register before using it. The source code is as follows:

AviatorEvaluator.addFunction(new AlertStrategyFunction());

If it is used in a Spring project, it only needs to be called in the initialization method of the bean.

Stepping Pit Guide & Tuning

Use compile cache mode

The default compilation methods such as compile(script) , , compileScript(path and , execute(script, env) will not cache the compiled results. The expression will be recompiled each time, some anonymous classes will be generated, and then the compiled result Expression instance will be returned, and execute the method will continue to be called Expression#execute(env) and executed.

There are two problems with this model:

  1. Recompile every time, if your script has not changed, this overhead is wasteful and affects performance very much.
  2. Compilation generates new anonymous classes every time. These classes will occupy the JVM method area (Perm or metaspace), the memory will gradually fill up, and eventually full gc will be triggered.

Therefore, it is usually recommended to enable the compilation cache mode. compile , compileScript and execute methods have corresponding overloaded methods that allow a boolean cached parameter to be passed in to indicate whether to enable the cache. It is recommended to set it to 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)      
}

Among them cacheKey is the key used to specify the cache. If your script is very long, using the script as the key by default will take up more memory and consume CPU for string comparison detection. You can use unique key values ​​such as MD5 to reduce cache overhead.

cache management

AviatorEvaluatorInstance There are a series of methods for managing the cache:

  • Get the current cache size, the number of cached compilation resultsgetExpressionCacheSize()
  • Get the compilation cache result corresponding to the script getCachedExpression(script) or get it according to the cacheKey getCachedExpressionByKey(cacheKey) . If it has not been cached, return null.
  • Invalidate cache invalidateCache(script) or invalidateCacheByKey(cacheKey) .
  • Empty the cacheclearExpressionCache()

performance advice

  • Execution priority mode is preferred (the default mode).
  • Use the compilation result cache mode to reuse the compilation results and pass in different variables for execution.
  • External variables are passed in, and the method of using the compilation result Expression#newEnv(..args) to create an external env will enable symbolization and reduce variable access overhead.
  • Do not enable execution trace mode in a production environment .
  • To call a Java method, use the custom function first, followed by the imported method, and finally the reflection mode based on FunctionMissing.

Wonderful past

Personal technical blog: https://jifuwei.github.io/
Public account: It’s Cuckoo Chicken

References:
[1]. Drools, IKEExpression, Aviator and Groovy String Expression Evaluation Comparison
[2]. AviatorScript Programming Guide

Guess you like

Origin blog.csdn.net/weixin_43975482/article/details/126986731