最近对B站开源的gengine规则引擎进行了入门级的研究。现在整理的资料记录如下。(欢迎交流讨论)
原博客地址: https://www.cnblogs.com/feixiang-energy/p/15572292.html
一:简介:
Gengine是一款基于golang和AST(抽象语法树)开发的规则引擎, Gengine支持的语法是一种自定义的DSL, Gengine通过内置的解释器对规则文件进行解析,构建规则模型,进行相应的规则计算和数据处理。Gengine于2020年7月由哔哩哔哩(bilibili.com)授权开源。Gengine现已应用于B站风控系统、流量投放系统、AB测试、推荐平台系统等多个业务场景。
官网上给出的Gengine相比于Java领域的著名规则引擎drools优势如下:
对比 |
drools |
gengine |
执行模式 |
仅支持顺序模式 |
支持顺序模式、并发模式、混合模式,以及其他细分执行模式 |
规则编写难易程度 |
高,与java强相关 |
低,自定义简单语法,与golang弱相关 |
规则执行性能 |
低、无论是规则之间还是规则内部,都是顺序执行 |
高,无论是规则间、还是规则内,都支持并发执行.用户基于需要来选择合适的执行模式 |
Gengine开源地址:https://github.com/bilibili/gengine
二:环境准备:
Go语言环境准备:
- Go语言官网(https://golang.google.cn/dl/)下载安装go语言开发包:go1.15.2.windows-amd64.msi
- 设置环境变量:GOROOT、GOPATH、GOPROXY、GO111MODULE:
- 执行go env命令进行测试:
开发工具准备:
- 推荐JetBrains GoLand:goland-2020.2.3.exe
第三方库准备:
- 在goland新建项目gengine。
- 在goland的settings中设置go mod库管理方式。
- 在src目录下新建go.mod文件:使用github.com/bilibili/gengine:v1.5.7版本
- hello world测试程序:
package main //库引用 import ( "fmt" "github.com/bilibili/gengine/builder" "github.com/bilibili/gengine/context" "github.com/bilibili/gengine/engine" ) //定义规则 (通过函数注入的方式,打印"hello world") const rule = ` rule "1" "rule-des" salience 10 begin println("hello world, gengine!") end ` //主函数 func main(){ //初始化数据环境变量 dataContext := context.NewDataContext() //注入println函数 dataContext.Add("println",fmt.Println) //初始化规则 ruleBuilder := builder.NewRuleBuilder(dataContext) //读取规则 err1 := ruleBuilder.BuildRuleFromString(rule) fmt.Println(err1) //初始化规则引擎 eng := engine.NewGengine() //执行规则引擎 err2 := eng.Execute(ruleBuilder,true) fmt.Println(err2) }
三:功能简介:
支持的规则语法:
- 逻辑运算:&&、||、!、==、!=、>、>=、<、<=等。
- 四则运算:+、-、*、/、()等。
- If else条件选择。
- 预加载API。
规则文件:
- 支持规则名称、描述、优先级设置。
- 支持规则注释。
- 支持@name、@id、@desc获取规则信息。
- 支持自定义变量。
- 支持报错时行号提示。
- 支持规则内调用注入的函数。
- 支持规则内conc{}语句块并发执行。
- 目前不支持web可视化编写规则文件,还需要技术人员进行手动配置。
执行模式:
- 顺序模式:当指定规则优先级时,按照优先级顺序执行。
- 并发模式:不考虑优先级、各个规则并发执行。
- 混合模式:先执行优先级最高的一个,剩余的n-1个并发执行。
- 逆混合模式:先并发执行优先级最高的n-1个,都执行结束后执行最后一个。
对外API接口:
- dataContext:支持注入需要在规则中使用的结构体、函数。
- ruleBuilder:与dataContext关联,支持通过字符串方式导入规则。
- engine:创建规则引擎,执行ruleBuilder关联的规则。
- GenginePoll:引擎实例池,支持在高QPS下实现高并发和线程安全。
支持的规则注入:
- golang的struct结构体。(以指针方式注入)
- 基础类的map、array、slice。
- Golang编写的函数。
支持引擎池:
- 类似于线程池或数据库连接池。
四:实验验证:
单规则:
一个比较全的单规则例子:
验证了:结构体注入、函数注入、加法运算、自定义变量、结构体变量修改。
package main import ( "fmt" "github.com/bilibili/gengine/engine" "github.com/bilibili/gengine/engine" "github.com/bilibili/gengine/engine" "strconv" ) type User struct { Name string Age int64 Male bool } func (u *User) SayHi(s string){ fmt.Println("Hi " + s + ", I am " + u.Name) } func PrintAge(age int64) { fmt.Println("Age is " + strconv.FormatInt(age, 10)) } const ( rule1 = ` rule "rule1" "a test" salience 10 begin println(@name) user.SayHi("lily") if user.Age > 20{ newAge = user.Age + 100 user.Age = newAge } PrintAge(user.Age) user.Male = false end ` ) func main(){ dataContext := context.NewDataContext() user := &User{ Name: "Calo", Age: 25, Male: true, } dataContext.Add("user",user) dataContext.Add("println",fmt.Println) dataContext.Add("PrintAge", PrintAge) ruleBuilder := builder.NewRuleBuilder(dataContext) err1 := ruleBuilder.BuildRuleFromString(rule1) if err1 != nil { panic(err1) } eng := engine.NewGengine() err2 := eng.Execute(ruleBuilder,true) if err2 != nil { panic(err2) } fmt.Printf("Age=%d Name=%s,Male=%t", user.Age, user.Name, user.Male)
顺序执行:
一个多规则顺序执行的例子:
模拟探测站总貌状态共3个规则:正常、预警、异常。顺序执行。
package main import ( "fmt" "github.com/bilibili/gengine/engine" "github.com/bilibili/gengine/engine" "github.com/bilibili/gengine/engine" ) type Station struct { Temperature int64 //温度 Humidity int64 //湿度 Water int64 //水浸 Smoke int64 //烟雾 Door1 int64 //门禁1 Door2 int64 //门禁2 StationState int64 //探测站状态: 0正常;1预警;2异常;3未知 } const ( stateRule = ` rule "normalRule" "探测站状态正常计算规则" salience 8 begin println("/***************** 正常规则 ***************") if Station.Temperature>0 && Station.Temperature<80 && Station.Humidity<70 && Station.Water==0 && Station.Smoke==0 && Station.Door1==0 && Station.Door2==0{ Station.StationState=0 println("满足") }else{ println("不满足") } end rule "errorRule" "探测站状态预警计算规则" salience 9 begin println("/***************** 预警规则 ***************") if Station.Temperature>0 && Station.Temperature<80 && Station.Humidity<70 && Station.Water==0 && Station.Smoke==0 && (Station.Door1==1 || Station.Door2==1){ Station.StationState=1 println("满足") }else{ println("不满足") } end rule "warnRule" "探测站状态异常计算规则" salience 10 begin println("/***************** 异常规则 ***************") if Station.Temperature<0 || Station.Temperature>80 || Station.Humidity>70 || Station.Water==1 || Station.Smoke==1{ Station.StationState=2 println("满足") }else{ println("不满足") } end ` ) func main(){ station := &Station{ Temperature: 40, Humidity: 30, Water: 0, Smoke: 1, Door1: 0, Door2: 1, StationState: 0, } dataContext := context.NewDataContext() dataContext.Add("Station", station) dataContext.Add("println",fmt.Println) ruleBuilder := builder.NewRuleBuilder(dataContext) err1 := ruleBuilder.BuildRuleFromString(stateRule) if err1 != nil { panic(err1) } eng := engine.NewGengine() err2 := eng.Execute(ruleBuilder, true) if err2 != nil { panic(err2) } fmt.Printf("StationState=%d", station.StationState) }
并发执行:
一个多规则并发执行的例子:
模拟探测站报警事件共3个规则:温度报警、水浸报警、烟雾报警。并发执行。
package main import ( "fmt" "github.com/bilibili/gengine/engine" "github.com/bilibili/gengine/engine" "github.com/bilibili/gengine/engine" ) type Temperature struct { Tag string //标签点名称 Value float64 //数据值 State int64 //状态 Event string //报警事件 } type Water struct { Tag string //标签点名称 Value int64 //数据值 State int64 //状态 Event string //报警事件 } type Smoke struct { Tag string //标签点名称 Value int64 //数据值 State int64 //状态 Event string //报警事件 } const ( eventRule = ` rule "TemperatureRule" "温度事件计算规则" begin println("/***************** 温度事件计算规则 ***************/") tempState = 0 if Temperature.Value < 0{ tempState = 1 }else if Temperature.Value > 80{ tempState = 2 } if Temperature.State != tempState{ if tempState == 0{ Temperature.Event = "温度正常" }else if tempState == 1{ Temperature.Event = "低温报警" }else{ Temperature.Event = "高温报警" } }else{ Temperature.Event = "" } Temperature.State = tempState end rule "WaterRule" "水浸事件计算规则" begin println("/***************** 水浸事件计算规则 ***************/") tempState = 0 if Water.Value != 0{ tempState = 1 } if Water.State != tempState{ if tempState == 0{ Water.Event = "水浸正常" }else{ Water.Event = "水浸异常" } }else{ Water.Event = "" } Water.State = tempState end rule "SmokeRule" "烟雾事件计算规则" begin println("/***************** 烟雾事件计算规则 ***************/") tempState = 0 if Smoke.Value != 0{ tempState = 1 } if Smoke.State != tempState{ if tempState == 0{ Smoke.Event = "烟雾正常" }else{ Smoke.Event = "烟雾报警" } }else{ Smoke.Event = "" } Smoke.State = tempState end `) func main(){ temperature := &Temperature{ Tag: "temperature", Value: 90, State: 0, Event: "", } water := &Water{ Tag: "water", Value: 0, State: 0, Event: "", } smoke := &Smoke{ Tag: "smoke", Value: 1, State: 0, Event: "", } dataContext := context.NewDataContext() dataContext.Add("Temperature", temperature) dataContext.Add("Water", water) dataContext.Add("Smoke", smoke) dataContext.Add("println",fmt.Println) ruleBuilder := builder.NewRuleBuilder(dataContext) err1 := ruleBuilder.BuildRuleFromString(eventRule) if err1 != nil { panic(err1) } eng := engine.NewGengine() eng.ExecuteConcurrent(ruleBuilder) fmt.Printf("temperature Event=%s\n", temperature.Event) fmt.Printf("water Event=%s\n", water.Event) fmt.Printf("smoke Event=%s\n", smoke.Event) for i := 0; i < 10; i++ { smoke.Value = int64(i % 3) eng.ExecuteConcurrent(ruleBuilder) fmt.Printf("smoke Event=%s\n", smoke.Event) } }
引擎池:
一个引擎池的例子:
创建了一个最大3个实例的引擎池。并发执行5个计算引擎。
package main import ( "fmt" "github.com/bilibili/gengine/engine" "math/rand" "sync/atomic" "time" ) const rulePool = ` rule "rulePool" "rule-des" salience 10 begin sleep() //print("do ", FunParam.Name) end ` type FunParam struct { Name string } func Sleep() { rand.Seed(time.Now().UnixNano()) i := rand.Intn(1000) time.Sleep(time.Nanosecond * time.Duration(i)) } func main(){ Sleep() apis := make(map[string]interface{}) apis["print"] = fmt.Println apis["sleep"] = Sleep pool, e1 := engine.NewGenginePool(1, 3, 2, rulePool, apis) if e1 != nil { panic(e1) } g1 := int64(0) g2 := int64(0) g3 := int64(0) g4 := int64(0) g5 := int64(0) cnt := int64(0) go func() { for { param := &FunParam{Name: "func1"} e2 := pool.ExecuteRules("FunParam", param, "", nil) if e2 != nil { println(fmt.Sprintf("e2: %+v", e2)) } //time.Sleep(1 * time.Second) atomic.AddInt64(&cnt, 1) g1++ } }() go func() { for { param := &FunParam{Name: "func2"} e2 := pool.ExecuteRules("FunParam", param, "", nil) if e2 != nil { println(fmt.Sprintf("e2: %+v", e2)) } //time.Sleep(1 * time.Second) atomic.AddInt64(&cnt, 1) g2++ } }() go func() { for { param := &FunParam{Name: "func3"} e2 := pool.ExecuteRules("FunParam", param, "", nil) if e2 != nil { println(fmt.Sprintf("e2: %+v", e2)) } //time.Sleep(1 * time.Second) atomic.AddInt64(&cnt, 1) g3++ } }() go func() { for { param := &FunParam{Name: "func4"} e2 := pool.ExecuteRules("FunParam", param, "", nil) if e2 != nil { println(fmt.Sprintf("e2: %+v", e2)) } //time.Sleep(1 * time.Second) atomic.AddInt64(&cnt, 1) g4++ } }() go func() { for { param := &FunParam{Name: "func5"} e2 := pool.ExecuteRules("FunParam", param, "", nil) if e2 != nil { println(fmt.Sprintf("e2: %+v", e2)) } //time.Sleep(1 * time.Second) atomic.AddInt64(&cnt, 1) g5++ } }() // 主进程运行5秒 time.Sleep(5 * time.Second) // 统计各个子进程分别运行次数 println(g1, g2, g3, g4, g5) // 统计在引擎池下总的各个子进程总的运行测试 println(g1 + g2 + g3 + g4 + g5, cnt) }
规则文件热更新:
一个单例引擎增量更新规则文件的例子:
验证了在不中断引擎计算的情况下:1)更新指定名称的规则配置;2)添加规则配置。
规则文件还支持动态删除、引擎池热更新等操作。不再验证。
package main import ( "fmt" "github.com/bilibili/gengine/builder" "github.com/bilibili/gengine/context" "github.com/bilibili/gengine/engine" "math/rand" "strconv" "time" ) type Student struct { Name string //姓名 score int64 //分数 } const ( ruleInit = ` rule "ruleScore" "rule-des" salience 10 begin if Student.score > 60 { println(Student.Name, FormatInt(Student.score, 10), "及格") }else{ println(Student.Name, FormatInt(Student.score, 10), "不及格") } end ` ruleUpdate = ` rule "ruleScore" "rule-des" salience 10 begin if Student.score > 80 { println(Student.Name, FormatInt(Student.score, 10), "及格") }else{ println(Student.Name, FormatInt(Student.score, 10), "不及格") } end ` ruleAdd = ` rule "ruleTeach " "rule-des" salience 10 begin if Student.score < 70 { println(Student.Name, FormatInt(Student.score, 10), "需要补课") } end ` ) func main(){ student := &Student{ Name: "Calo", score: 100, } dataContext := context.NewDataContext() dataContext.Add("FormatInt", strconv.FormatInt) dataContext.Add("println",fmt.Println) dataContext.Add("Student",student) ruleBuilder := builder.NewRuleBuilder(dataContext) err1 := ruleBuilder.BuildRuleFromString(ruleInit) if err1 != nil { panic(err1) } eng := engine.NewGengine() go func() { for { student.score = rand.Int63n(50) + 50 err2 := eng.Execute(ruleBuilder,true) if err2 != nil { panic(err2) } time.Sleep(1 * time.Second) } }() go func() { time.Sleep(3 * time.Second) err2 := ruleBuilder.BuildRuleWithIncremental(ruleUpdate) if err2 != nil { panic(err2) } time.Sleep(3 * time.Second) err3 := ruleBuilder.BuildRuleWithIncremental(ruleAdd) if err3 != nil { panic(err3) } }() time.Sleep(20 * time.Second) }
五:总结:
Gengine将规则文件的配置与程序代码的编写进行了一定程度的分离。规则文件采用类编程语言的方式进行编写,支持简单的数学运算、逻辑运算、if/else操作、结构体/函数注入等功能,同时能支持规则优先级设置和多种执行模式选择。规则引擎可以较便捷的通过规则文件的配置来反映实际业务场景中所需要的规则指标,并且能较灵活的适应业务规则的变化。
Gengine是由golang语言开发的,为了实现跨语言协同开发,通常可以将规则引擎封装为一个独立运行的规则引擎模块,通过zmq、mqtt等方式进行数据的接入,根据配置的规则进行业务计算,然后将计算结果对外发布。
Gengine规则引擎也可以搭配rpc、restful等接口,将其封装为一个独立的规则服务或计算服务,通过被其它服务调用的方式对外提供计算能力。
在实际的业务场景中通常采用微服务架构,各微服务之间通过rpc、restful等接口进行交互。由于Gengine规则文件支持函数注入,因此甚至可以将已编写好的接口调用进行事先罗列,在规则引擎中根据规则计算结果进行不同的业务调用。
Gengine的规则文件热更新功能也为生产环境中不停机更新业务规则提供了可能。
Gengine作为B站开源的号称“第三代规则引擎”,还有很多其它的一些特性功能等待去研究发现,并将其融入到业务应用中去。