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