golang 规则引擎 -gengine 最佳实践

用户头像
calo
关注
发布于: 15 小时前
golang规则引擎-gengine最佳实践

希望可以通过这篇帖子对gengine的使用介绍,能够帮助到有需要的同学快速上手使用,从而帮助其对自己所负责的业务快速理清条理,增强业务快速迭代的能力。

一、开源代码github地址

gengine的github地址:https://github.com/rencalo770/gengine

二、gengine设计与实现帖子链接

gengine的设计与实现帖子地址:《B 站新一代 golang 规则引擎的设计与实现》

三、工程引用

go mod方式:

module your_project_name

replace gengine => github.com/rencalo770/gengine v1.1.2

require (
gengine v1.1.2
)

当然也支持go vendor方式的引用

四、基准测试

克隆代码到本地之后,可以直接在gengine/test目录下运行相关的测试,或者自行添加想要验证的测试代码,以进行单元测试。

1.一个完整的规则构成

如下图所示

a. 有关键字 rule, salience, begin, end, conc, if, else, @name

b. 关键字rule是必须关键字,所有规则都以rule开头,其后紧随两个字符串,分别是“规则名”和“规则描述”,它们两也是必须的。

c. salience 是规则优先级字段,非必须的,其后紧随的是个整数,标示了规则的优先级,数值越大,规则优先级越高,当有多个规则时,越先被执行。

d. begin 和end是必须的关键字,它们包裹的是可执行的规则体。

e. 规则体可以包含四则运算、逻辑运算、if..else 结构、赋值语句、conc代码块、用户预先注入的函数或方法调用。

2.几个重要的测试

a. 测试目录gengine/test/complex, 测试gengine中的golang继承关系调用测试。

b. 测试目录gengine/test/concurrent, 测试从文件中读取多个规则,并发执行规则测试。

c. 测试目录gengine/test/map_slice_array, 测试gengine对map,slice,array的支持。

d. 测试目录gengine/test/math, 测试gengine中的数值类型。

e. 测试文件gengine/test/at_name_test.go, 测试@name语法调用。

f. 测试文件gengine/test/conc_statement_test.go, conc{}代码支持测试。

g. 测试文件gengine/test/else_if_test.go,完整的if ...else if ...else 语法支持测试。

h. 测试文件gengine/test/gengine_base_test.go, 一个复杂的测试用例。

i. 测试文件gengine/test/gengine_salience_test.go, 多个规则时,规则优先级支持测试。

j. 测试文件gengine/test/grammar_test.go, 一个小的测试用例。

k. 测试文件gengine/test/multi_rules_test.go, 多规则执行测试。

l. 测试文件gengine/test/pool_test.go, 规则池支持测试。



五、支持的语法

1.@name语法

a. 在规则中,@name指的是当前规则名(字符串)。

b. @name只能在规则体中出现,表示对当前规则的规则名称的引用。

c. 规则解析执行时,@name会被解析为当前规则名字符串,因此,@name具有所有的string类型的特征。

2.if..else if .. else语法

gengine支持完整的if..else if ...else语法,其表现与在具体的计算机语言中无异。可以看这个测试规则:

rule "elseif_test" "test"
begin

a = 8
if a < 1 {
println("a < 1")
} else if a >= 1 && a <6 {
println("a >= 1 && a <6")
} else if a >= 6 && a < 7 {
println("a >= 6 && a < 7")
} else if a >= 7 && a < 10 {
println("a >=7 && a < 10")
} else {
println("a > 10")
}

end

3.conc语法块(已支持)

之前有在设计实现的帖子中说过,为什么要支持这样一个代码块:因为在具体的业务中,很多数据指标并不在本地,而在可访问网络的某台机器上,当在规则中多次调用网络接口取数据指标时,这种网络操作会严重影响规则的响应性能,甚至直接导致规则服务不可用。因此,可以使用conc关键字代码块来优化网络调用,提升规则执行性能。使用规则配置如下:

rule "conc_test" "test"
begin
conc {
// this should be used in time-consuming operation,
// such as the operation contains network connection (get data from remote based on network)
a = 3.0
b = 4
c = 6.8
d = "hello world"
e = "you will be happy here!"
}
println(a, b, c, d, e)
end

a. conc关键字代码块中只允许赋值语句。

b. 当conc代码块中无赋值语句时,将不会有任何影响;当仅有一个赋值语句时,会退化成语句顺序执行;当有两个及以上的赋值语句时,会并发执行这些赋值语句来获得结果。如上规则,就会并发为a,b,c,d,e赋值。

c. 用户需要注意的是,当在conc内调用同一个请求时,用户应当自己来保证请求的线程安全。

六、规则池支持

1.规则池的使用

gengine规则池的实现完全类同golang的数据库连接池实现。具体测试和使用,可以看gengine/test/pool_test.go中的测试代码。

2.为什么要支持规则池

golang的服务通常是这个样子的:

type MyService struct{
//something
}
func NewMyService(p1, p2 string) *MyService{
//
return &MyService{
//something
}
}
func (ms *MyService)Service(px, py int) {
//todo
return //something
}

a. golang服务会调用NewMyService方法初始化一次,然后当请求过来之后,通过反射,重复调用Service,当请求高并发的时候,如果有公共的非线程安全的api,如非线程安全的map,并发写时,会发生panic.

b. 通常,一个gengine实例中会多达几十个规则,如果规则中包含耗时的操作,且在规则中有并发安全问题(非安全map的写操作),很容易在高并发情况下引发panic。因此为了gengine更实用,gengine框架提供一个具有多个gengine实例的gengine实例池。当请求到来时,会从池中获取一个gengine实例来对外提供服务,使用完毕会被放回gengine实例池内。

c.需要注意的时,如果用户向所有的gengine实例中注入的是同一个api,用户应该保证注入的api是线程安全的。



七、规则异步加载与执行

gengine中的规则执行和规则加载是异步的。因此,用户可以在规则执行时,更新gengine实例中的规则,当规则更新好之后,再执行指针替换,即可更新全部规则。这样可以保证规则服务的永远处于可用状态。具体的操作可以看所有的测试用例。

另外,gengine提供了多种规则执行模式,可以在执行时随时基于需要来切换,具体的可以看代码gengine/engine/gengine.go,代码上有详细的注释,用户可以根据需要取用。

八、其他注意事项

1.规则支持多返回值的函数、方法调用,但规则仅支持单返回值的函数、方法调用赋值(将函数结果赋值给一个变量)。

2.规则最多支持2两层调用,如支持“a.b”形式的调用,但不支持“a.b.c”甚至更多层级的调用。只为力求简单,并且层级多了之后,也难以记忆(迪米特法则)。

3.规则不支持nil,因为nil是太过特殊的语法,增加了DSL语法复杂度,规则期望返没有合适的值的时候,可以返回对应类型的默认值。

九、规则服务不重启

随着gengine逐步推广到越来越多的业务线,就有同事跑过来对我说:“我使用gengine,并不能使我的服务不重启,因为当我需要添加新功能的时候,还是要重启服务来更新gengine的注入内容。”

上面这句话当然是没有错的,那我们有没有办法能够让我们不断新增规则,但是永不重启我们服务呢?这当然是可以的,前提是需要业务开发同学对自己的业务有一个深刻的认知:如何将自己的业务抽象成“规则 + 指标”模式,若不如此,业务开发永远都是,来一个需求就写一段代码,然后发版,测试成本急剧升高,稳定性急剧下降。

规则是由指标组成的,当一个规则仅有一个指标组成时,规则就是指标。指标之间任意组合,就可以组合成任意的规则。

具体的抽象做法是,将具体的业务场景所需要的数据抽象成指标,以命名的方式访问数据指标,相同的接口,传入不同的称就实现了不同数据指标的取用,以此达到逻辑的变化。需求的新增与变化,本质在于数据的新增与变化,如果可以抽象变化的数据表示,自然可以使最终的代码不变(设计模式就是,抽象的是变化,而非不变)。因限于业务脱敏保密,故不在此详述,大致操作可见《B 站新一代 golang 规则引擎的设计与实现》3.6节的代码注释。

十、gengine应用场景

  1. 风控(实时、离线)

  2. 推荐

  3. 内容投放

  4. 流量分发

  5. 基于golang的数据清洗、识别、打标

  6. 以及任何需要规则和指标的场景

发布于: 15 小时前 阅读数: 29
用户头像

calo

关注

感谢大家关注~ 2020.06.25 加入

还未添加个人简介

评论

发布
暂无评论
golang规则引擎-gengine最佳实践