golang 规则引擎调研

背景

渠道路由规则执行使用了Drools,Drools 目前只支持java语言。基于golang 比较流行的规则引擎grule-rule-engine、gengine、govaluate、gval 做对比

路由使用规则场景

  • 基础规则执行能力
  • 支持操作符:>、<、>=、<=、!=、=、contain、in、not in、
  • 支持规则string初始化规则上下文
  • 规则支持规则中增加日志
  • 规则string支持修改对象变量

golang规则引擎竞品对比

grule-rule-enginegenginegovaluate
项目地址https://github.com/hyperjumptech/grule-rule-enginehttps://github.com/bilibili/genginehttps://github.com/Knetic/govaluate
star&fork1.7K Start 284forkhyperjump 开源基于drools 思想开发,语法与drools相似1.6K230forkbilbili开源3.2K431fork个人开源
最近维护时间2020年开源,最近几月有维护2020年开源 2021年10月后没有维护2017年后没有更新过代码
算法抽象规则树AST(Abstract Syntax Tree) 底层基于antlr抽象规则树AST(Abstract Syntax Tree)底层基于antlr表达式引擎-golang语法
是否支持路由场景原生支持string in,不支持slice&array contain(自定义函数嵌入规则实现)支持自定义执行顺序支持规则支持自定义函数(不支持string in & slice&array contain)支持自定义执行顺序不支持contain,支持in 表达式引擎,小而美的组件
语法 DSL 自定义规则语法 类似Drools DSL 自定义规则语法golang 原生语法
文档比较丰富,有demo及文档,中文文档较少
中文文档较丰富。目前不维护状态文档较少

基础测试benchmark测试(Benchmark 基准测试

Benchmark指标对比

goos: darwin

goarch: amd64

pkg: templement-web/biz

cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz

每次初始化上下文

BenchmarkFunction1-12 1249 1047944 ns/op 529804 B/op 9449 allocs/op

BenchmarkFunction-12 240 4800814 ns/op 3309257 B/op 40973 allocs/op

复用上下文:

BenchmarkFunction-12 97605 11035 ns/op 2358 B/op 82 allocs/op

指标解释:

ns/op = 每次执行纳秒

B/op = 每次执行申请字节

allocs/op= 每次执行申请内存次数

package main
// require  	github.com/hyperjumptech/grule-rule-engine v1.13.0
import (
	"fmt"
	"github.com/hyperjumptech/grule-rule-engine/ast"
	"github.com/hyperjumptech/grule-rule-engine/builder"
	"github.com/hyperjumptech/grule-rule-engine/engine"
	"github.com/hyperjumptech/grule-rule-engine/pkg"
	"testing"
)

type TestCar struct {
	SpeedUp        bool
	Speed          int64
	MaxSpeed       int64
	SpeedIncrement int64
}

type DistanceRecord struct {
	TotalDistance int64
}

const s = "rule SpeedUp \"When testcar is speeding up we keep increase the speed.\" salience 10 " +
	" {\n    when\n    TestCar.SpeedUp==true && TestCar.Speed < TestCar.MaxSpeed\n   " +
	" then\n    TestCar.Speed = TestCar.Speed + TestCar.SpeedIncrement;\n    " +
	"DistanceRecord.TotalDistance = DistanceRecord.TotalDistance + TestCar.Speed; Retract(\"SpeedUp\");\n}"

// 缓存上下文
func BenchmarkFunction(b *testing.B) {
	b.ResetTimer()

	drls := s
	knowledgeLibrary := ast.NewKnowledgeLibrary()
	ruleBuilder := builder.NewRuleBuilder(knowledgeLibrary)
	err1 := ruleBuilder.BuildRuleFromResource("TutorialRules", "0.0.1", pkg.NewBytesResource([]byte(drls)))
	if err1 != nil {
		panic(err1)
	}
	knowledgeBase := knowledgeLibrary.NewKnowledgeBaseInstance("TutorialRules", "0.0.1")
	for i := 0; i < b.N; i++ {
		myFact := &TestCar{
			SpeedUp:        true,
			Speed:          200,
			MaxSpeed:       500,
			SpeedIncrement: 100,
		}

		myFactDistanceRecord := &DistanceRecord{
			TotalDistance: 1000,
		}
		i2 := myFact.MaxSpeed + myFactDistanceRecord.TotalDistance
		dataCtx := ast.NewDataContext()
		err := dataCtx.Add("TestCar", myFact)
		err = dataCtx.Add("DistanceRecord", myFactDistanceRecord)

		if err != nil {
			panic(err)
		}
		engineresult := engine.NewGruleEngine()
		err = engineresult.Execute(dataCtx, knowledgeBase)
		if err != nil {
			panic(err)
		}
		if i2 != myFactDistanceRecord.TotalDistance {
			fmt.Println("not equal")
		} else {
			fmt.Println("equal")
		}
	}
	b.StopTimer()

}

// 每次初始化上下文
func BenchmarkFunction1(b *testing.B) {
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		drls := s
		knowledgeLibrary := ast.NewKnowledgeLibrary()
		ruleBuilder := builder.NewRuleBuilder(knowledgeLibrary)
		err1 := ruleBuilder.BuildRuleFromResource("TutorialRules", "0.0.1", pkg.NewBytesResource([]byte(drls)))
		if err1 != nil {
			panic(err1)
		}
		knowledgeBase := knowledgeLibrary.NewKnowledgeBaseInstance("TutorialRules", "0.0.1")
		myFact := &TestCar{
			SpeedUp:        true,
			Speed:          200,
			MaxSpeed:       500,
			SpeedIncrement: 100,
		}

		myFactDistanceRecord := &DistanceRecord{
			TotalDistance: 1000,
		}
		i2 := myFact.MaxSpeed + myFactDistanceRecord.TotalDistance
		dataCtx := ast.NewDataContext()
		err := dataCtx.Add("TestCar", myFact)
		err = dataCtx.Add("DistanceRecord", myFactDistanceRecord)

		if err != nil {
			panic(err)
		}
		engineresult := engine.NewGruleEngine()
		err = engineresult.Execute(dataCtx, knowledgeBase)
		if err != nil {
			panic(err)
		}
		if i2 != myFactDistanceRecord.TotalDistance {
			fmt.Println("not equal")
		} else {
			fmt.Println("equal")
		}
	}
	b.StopTimer()

}
package main
// 	require github.com/bilibili/gengine v1.5.7
import (
	"fmt"
	"github.com/bilibili/gengine/builder"
	"github.com/bilibili/gengine/context"
	"github.com/bilibili/gengine/engine"
	"testing"
	"time"
)
type TestCar struct {
	SpeedUp        bool
	Speed          int64
	MaxSpeed       int64
	SpeedIncrement int64
}

type DistanceRecord struct {
	TotalDistance int64
}

const pass_status_test = `
rule "1" "1"
begin
if (TestCar.SpeedUp==true && TestCar.Speed < TestCar.MaxSpeed) {
	TestCar.Speed = TestCar.Speed + TestCar.SpeedIncrement
	DistanceRecord.TotalDistance = DistanceRecord.TotalDistance + TestCar.Speed
}
end
`

func BenchmarkFunction(b *testing.B) {
	b.StartTimer()
	for i := 0; i < b.N; i++ {

		myFact := &TestCar{
			SpeedUp:        true,
			Speed:          200,
			MaxSpeed:       500,
			SpeedIncrement: 100,
		}

		myFactDistanceRecord := &DistanceRecord{
			TotalDistance: 1000,
		}
		dataContext := context.NewDataContext()
		dataContext.Add("TestCar", myFact)
		dataContext.Add("DistanceRecord", myFactDistanceRecord)
		//init rule engine
		ruleBuilder := builder.NewRuleBuilder(dataContext)

		//resolve rules from string
		err := ruleBuilder.BuildRuleFromString(pass_status_test)

		if err != nil {
			panic(err)
		}
		eng := engine.NewGengine()

		// true: means when there are many rules, if one rule execute error,continue to execute rules after the occur error rule
		err = eng.Execute(ruleBuilder, true)
		if err != nil {
			panic(err)
		}
	}
	b.StopTimer()

}

star 历史对比

语法对比

Drools

/*
rule rulename
	when 
	then
end
**/

规则定义
rule "SpeedUp"
    salience 10
    when
        $TestCar : TestCarClass( speedUp == true && speed < maxSpeed )
        $DistanceRecord : DistanceRecordClass()
    then
        $TestCar.setSpeed($TestCar.Speed + $TestCar.SpeedIncrement);
        update($TestCar);
        $DistanceRecord.setTotalDistance($DistanceRecord.getTotalDistance() + $TestCar.Speed);
        update($DistanceRecord);
end


/*代码使用

    KieHelper kieHelper = new KieHelper();
    kieHelper.addContent("规则体", ResourceType.DRL);
    Results verify = kieHelper.verify();
	KieSession kieSession = kieHelper.getKieContainer().newKieSession();;
    KieSession kieSession = context.getKieSession();
    kieSession.setGlobal("log", log);
    // 塞入变量
    kieSession.insert(new TestCarClass());
    kieSession.insert(new DistanceRecordClass());
    int count = kieSession.fireAllRules();

*/

grule-rule-engine

/*
rule  规则名称 规则描述 salience 执行优先级
when
then
**/

rule SpeedUp "When testcar is speeding up we keep increase the speed." salience 10  {
    when
    TestCar.SpeedUp == true && TestCar.Speed < TestCar.MaxSpeed
    then
    TestCar.Speed = TestCar.Speed + TestCar.SpeedIncrement;
    DistanceRecord.TotalDistance = DistanceRecord.TotalDistance + TestCar.Speed;
}

/*
	//构建上线文
	knowledgeLibrary := ast.NewKnowledgeLibrary()
	ruleBuilder := builder.NewRuleBuilder(knowledgeLibrary)
	err1 := ruleBuilder.BuildRuleFromResource("TutorialRules", "0.0.1", pkg.NewBytesResource([]byte(drls)))
	if err1 != nil {
		panic(err1)
	}
	knowledgeBase := knowledgeLibrary.NewKnowledgeBaseInstance("TutorialRules", "0.0.1")

	dataCtx := ast.NewDataContext()
	err := dataCtx.Add("TestCar", testCar)
	err := dataCtx.Add("DistanceRecord", distanceRecord)
    // 执行规则
   	engineresult := engine.NewGruleEngine()
	err = engineresult.Execute(dataCtx, knowledgeBase)
*/

gengine

//rule  规则名称 规则描述 salience 执行优先级
//begin
//end

// 不能以; 结尾,否则规则解析异常

rule "SpeedUp" "When testcar is speeding up we keep increase the speed."
begin

if(TestCar.SpeedUp == true && TestCar.Speed < TestCar.MaxSpeed){
    TestCar.Speed = TestCar.Speed + TestCar.SpeedIncrement
    DistanceRecord.TotalDistance = DistanceRecord.TotalDistance + TestCar.Speed
}
end


/*
dataContext := context.NewDataContext()
	//inject struct
	rs := &TestCar{SpeedUp: false}
 	rd := &DistanceRecord{TotalDistance: 100}

	dataContext.Add("TestCar",rs)
	dataContext.Add("DistanceRecord",rd)

	//init rule engine
	ruleBuilder := builder.NewRuleBuilder(dataContext)

	//读取规则
	e1 := ruleBuilder.BuildRuleFromString(pass_status_rule)
	if e1 != nil {
		panic(e1)
	}

	eng := engine.NewGengine()
	e2 := eng.Execute(ruleBuilder, true)
*/

govaluate

  u := &TestCar{SpeedUp: false}
  parameters := make(map[string]interface{})
  parameters["u"] = u

  expr, _ := govaluate.NewEvaluableExpression("u.SpeedUp()==true")
  result, _ := expr.Evaluate(parameters)
  fmt.Println("user", result)

满足情况对比

grule-rule-enginegenginegovaluate
操作符可以支持原生+自定义函数(contain)可以支持原生+自定义函数(in、contain)不支持 in、contain小而美组件
规则中修改变量支持支持不支持
规则中断Complete()支持,使用return 不支持
支持模板不支持可以使用go 内置templatefor循环实现不支持可以使用go 内置templatefor循环实现不支持可以使用go 内置templatefor循环实现
日志支持传入日志支持传入日志不支持,显示打印
缓存规则上下文支持(https://github.com/hyperjumptech/grule-rule-engine/discussions/342不支持(每次都需要初始化)不支持

调研结论

  • grule-rule-engine、gengine 都可以满足java drools 迁移到 go
  • grule-rule-engine 语法与Drools 比较相似
  • grule-rule-engine 可以缓存上下文,执行速度较快
  • gengine 中文文档较多,目前不维护状态

偏向于grule-rule-engine

发表评论

电子邮件地址不会被公开。 必填项已用*标注