Expression Engine Technology and Comparison
Introduction to Drools
Drools (JBoss Rules) is an open source business rules engine that complies with industry standards and is fast and efficient. It can be used by business analysts or reviewers to easily view business rules to verify that the coded rules implement 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 for describing rules. Currently, Drools provides three semantic modules
- Python module
- Java modules
- Groovy Module
Drools rules are written in the drl file. For the preceding expression, the Drools drl file is described as:
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 represents a condition, then is an action that can be performed after the condition is met, and any java method can be called here. The contians method of strings is not supported in drools, and can only be replaced by regular expressions.
<!--more-->
Introduction to IKExpression
IK Expression is an open source, extensible, and an ultra-lightweight formulaic language parsing and execution toolkit developed based on the java language. IK Expression does not depend on any third party java library. As a simple jar, it can be integrated into any Java application.
For the previous expression, IKExpression 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 implements the function through the custom function $indexOf.
Introduction to Groovy
Groovy is often considered 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 Groovy Written - 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 preceding expression, Groovy writes:
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 the Java language, which is mainly used for dynamic evaluation of various expressions. There are already many open source java expression evaluation engines available, why do you need Avaitor?
The design goal of Aviator is light weight and high performance. Compared with Groovy and JRuby, Aviator is very small, and it is only 450K with dependent packages, and only 70K without dependent packages; of course,
The syntax of Aviator 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 hands them over to the JVM. to execute. Simply put, Aviator is positioned between a heavyweight scripting language like Groovy and a lightweight expression engine like IKExpression. 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
Drools is a high-performance rule engine, but the designed usage scenarios are not the same as the scenarios in this test. The target of Drools is a complex object with hundreds or thousands of properties, how to quickly match the rules, not Simple objects repeatedly match the rules, and therefore have the lowest results in this test.
IKExpression relies on interpretation and execution to complete the execution of expressions, so the performance is also unsatisfactory. 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 the variable and execute it, and the overall performance is very good.
Groovy is a dynamic language, it 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. For expressions that need to be executed repeatedly like eSOC, Groovy is a very good choice.
Scene combat
Monitoring alert rules
Monitoring rule configuration renderings:
The final translation into 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 in action
Based on the monitoring center in the previous section triggerExec
how to implement the function
Look at the source code first:
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, you only need to inherit AbstractAlertFunction
to implement custom functions. 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, the tail parameter is optional, and the corresponding function input parameters are called separately for multiple parameters.
After implementing the custom function, it needs to be registered before use. The source code is as follows:
AviatorEvaluator.addFunction(new AlertStrategyFunction());
If used in a Spring project, just call it in the bean's initialization method.
Stepping on the 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, and will recompile the expression each time to generate some anonymous classes. Then return the compilation result Expression
instance, execute
method will continue to call Expression#execute(env)
execute.
There are two problems with this mode:
- Recompile every time, if your script doesn't change, this overhead is wasteful and affects performance very much.
- Every time the compilation generates new anonymous classes, 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 more recommended to enable the compilation cache mode. compile
, compileScript
and execute
methods have corresponding overloaded methods, which allow to pass in a boolean cached
parameter, indicating whether to enable caching, 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 used to specify the cache key. 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 MD5 and other unique key-value to reduce cache overhead.
Cache management
AviatorEvaluatorInstance
There are a series of methods for managing the cache:
- Get the current cache size and the number of cached compilation results
getExpressionCacheSize()
- Get the compiled cache result corresponding to the script
getCachedExpression(script)
or get it according to the cacheKeygetCachedExpressionByKey(cacheKey)
, if it has not been cached, return null. - Invalidate the cache
invalidateCache(script)
orinvalidateCacheByKey(cacheKey)
. - Clear cache
clearExpressionCache()
Performance Recommendations
- Execute priority mode (default mode) is used first.
- Use the compilation result cache mode, reuse the compilation results, and pass in different variables for execution.
- When external variables are passed in, the
Expression#newEnv(..args)
method of the compilation result is preferred to create an external env, which will enable symbolization and reduce variable access overhead. - Do not turn on execution trace mode in a production environment.
- When calling a Java method, the custom function is used first, followed by the imported method, and finally the reflection mode based on FunctionMissing.
Wonderful past
- Performance tuning - a small log is a big pit
- Performance Optimization Essentials - Flame Graph
- Flink implements real-time features in risk control scenarios
Welcome to the official account: Gugu Chicken Technical Column Personal technical blog: https://jifuwei.github.io/
refer to:
[1]. Drools, IKExpression, Aviator and Groovy string expression evaluation comparison
[2]. AviatorScript Programming Guide
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。