一、简介:

  Gengine 是一款基于 golang 和 AST(抽象语法树)开发的规则引擎, Gengine 支持的语法是一种自定义的 DSL, Gengine 通过内置的解释器对规则文件进行解析,构建规则模型,进行相应的规则计算和数据处理。Gengine 于 2020 年 7 月由哔哩哔哩 (bilibili.com) 授权开源。Gengine 现已应用于 B 站风控系统、流量投放系统、AB 测试、推荐平台系统等多个业务场景。

  Gengine 开源地址:https://github.com/bilibili/gengine

二、初识:

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 , 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 站开源的号称 “第三代规则引擎”,还有很多其它的一些特性功能等待去研究发现,并将其融入到业务应用中去。