最近對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站開源的號稱“第三代規則引擎”,還有很多其它的一些特性功能等待去研究發現,並將其融入到業務應用中去。