Arishem
Arishem是字节跳动客服平台架构组自研的一款轻量、高性能的DSL规则引擎。它旨在将频繁变更的业务决策从应用程序中分离出来,通过可视化界面灵活编写业务决策,提高业务需求的响应速度。
Arishem采用完全兼容的JSON语法格式定义规则语法,通过组合、嵌套方式灵活表达业务规则。使用Arishem可以轻松地将规则可视化,使不具备编程基础的人员也能快速上手。
Arishem在AST解析生成和规则执行方面进行了一系列优化,使单个复杂规则的执行能在微秒级别完成。
Arishem支持自定义规则执行顺序和并发执行粒度,支持运行时下游数据的并发和预测获取。
Arishem内置了丰富的操作符和函数。
完全兼容JSON语法
Arishem由条件表达式和目的表达式组成,其最大特点是语法完全兼容JSON语法,同时也能完全兼容IDL(如thrift、protobuf等)。在examples部分提供了使用thrift和protobuf定义规则的示例。
Arishem在可视化场景(如规则配置页面)具有显著优势。在字节跳动的客服平台,几十个涉及规则判断的场景都接入了Arishem,并实现了运营可视化规则配置页面。这使得原本需要研发维护的规则配置场景,变成了运营同学也能进行配置维护,极大地解放了研发资源。
非常快速!
在benchmark测试中,执行单个无网络请求的复杂规则仅需微秒级别的时间!当然,没有最好的性能框架,只有最适合的应用场景。
测试环境:
goos: darwin
goarch: amd64
pkg: */arishem/arishem
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
PASS
测试条件样例,共495个AST节点,包含基本的逻辑操作,如字符串正则、数组遍历、数组交集等:
// 实时数据
{"username":"Andrew","usernames":["Jack","Mike","Andrew"],"news":"Jack hanged out with Mike last weekend.","number1":100,"numbers":["10",99.9,0]}
// 条件表达式
{"OpLogic":"||","ConditionGroups":[{"OpLogic":"&&","Conditions":[{"Operator":"STRING_START_WITH","Lhs":{"VarExpr":"username"},"Rhs":{"Const":{"StrConst":"Banana"}}},{"Operator":"STRING_END_WITH","Lhs":{"VarExpr":"usernames#1"},"Rhs":{"Const":{"StrConst":"A"}}},{"Operator":"STRING_END_WITH","Lhs":{"VarExpr":"usernames#0"},"Rhs":{"Const":{"StrConst":"hahaha"}}},{"Operator":"CONTAIN_REGULAR","Lhs":{"VarExpr":"usernames#0"},"Rhs":{"Const":{"StrConst":"^M.*"}}}]},{"OpLogic":"and","ConditionGroups":[{"OpLogic":"&&","ConditionGroups":[{"OpLogic":"||","Conditions":[{"Operator":"<=","Lhs":{"Const":{"NumConst":100}},"Rhs":{"Const":{"NumConst":10}}},{"Operator":"<=","Lhs":{"Const":{"NumConst":100}},"Rhs":{"Const":{"NumConst":10}}},{"Operator":"<=","Lhs":{"Const":{"NumConst":100}},"Rhs":{"Const":{"NumConst":10}}},{"Operator":"<=","Lhs":{"Const":{"NumConst":100}},"Rhs":{"Const":{"NumConst":10}}},{"Operator":"<","Lhs":{"Const":{"NumConst":100}},"Rhs":{"Const":{"NumConst":10}}}]}]},{"OpLogic":"&&","Conditions":[{"Operator":"LIST_IN","Lhs":{"VarExpr":"number1"},"Rhs":{"ConstList":[{"NumConst":1},{"NumConst":98},{"NumConst":101},{"NumConst":1.32e-3},{"NumConst":12.234},{"NumConst":-1}]}},{"Operator":"!LIST_IN","Lhs":{"VarExpr":"numbers#0"},"Rhs":{"ConstList":[{"NumConst":1},{"NumConst":99},{"NumConst":999},{"NumConst":1.32e-3},{"NumConst":10},{"NumConst":-1}]}},{"Operator":"LIST_CONTAINS","Lhs":{"VarExpr":"numbers"},"Rhs":{"MathExpr":{"OpMath":"+","ParamList":[{"Const":{"NumConst":6}},{"Const":{"StrConst":"5"}}]}}},{"Operator":"LIST_RETAIN","Lhs":{"VarExpr":"numbers"},"Rhs":{"MathExpr":{"OpMath":"+","ParamList":[{"Const":{"NumConst":6}},{"Const":{"StrConst":"5"}}]}}},{"Operator":"LIST_RETAIN","Lhs":{"VarExpr":"numbers"},"Rhs":{"ConstList":[{"StrConst":"-1"},{"BoolConst":true},{"NumConst":-3.1415926}]}},{"Operator":"!LIST_RETAIN","Lhs":{"VarExpr":"numbers"},"Rhs":{"ConstList":[{"StrConst":"-1"},{"BoolConst":true},{"NumConst":-3.1415926}]}}]}]},{"OpLogic":"&&","Conditions":[{"Operator":"==","Lhs":{"Const":{"NumConst":1}},"Rhs":{"Const":{"NumConst":1}}}]}]}
// 目的表达式
{"ActionName":"Greeting2","ParamMap":{"UserAge":{"VarExpr":"user.age"},"TempUsername":{"VarExpr":"user.name"}}}
运行结果:平均不到30微秒
goos: darwin
goarch: amd64
pkg: */arishem/arishem
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkSingleComplexRule-12 205094 28959 ns/op 2227 B/op 69 allocs/op
BenchmarkSingleComplexRule-12 208012 28637 ns/op 2226 B/op 69 allocs/op
BenchmarkSingleComplexRule-12 207696 28614 ns/op 2226 B/op 69 allocs/op
PASS
快速开始
请使用v1.0.9及以上版本
go get github.com/bytedance/arishem@{version}
使用前必须先调用Initialize方法,否则执行将导致Arishem执行异常。通常情况下使用默认配置即可,该操作应在init方法中进行。
func init() {
arishem.Initialize(arishem.DefaultConfiguration())
}
- 快速判断一个条件表达式是否符合匹配的预期 Arishem语法参考
func main() {
condition := `
{
"OpLogic": "&&",
"Conditions": [
{
"Operator": "==",
"Lhs": {
"Const": {
"NumConst": 1
}
},
"Rhs": {
"Const": {
"NumConst": 1
}
}
}
]
}
`
pass, err := arishem.JudgeCondition(condition)
if err != nil {
// 处理错误
// ...
println(err.Error())
}
if pass {
// 业务代码
// ...
println("条件通过!")
}
}
输出结果:
条件通过!
- 在规则中通过关键字VarExpr获取实时数据进行判断
func main() {
condition := `
{
"OpLogic": "&&",
"Conditions": [
{
"Operator": ">",
"Lhs": {
"VarExpr": "user.age"
},
"Rhs": {
"VarExpr": "user_ages#1"
}
}
]
}
`
pass, err := arishem.JudgeConditionWithFactMeta(condition, `
{
"user": {
"name": "KJ",
"age": 24
},
"user_ages": [
15,
20,
32
]
}
`)
if err != nil {
// 处理错误
return
}
if pass {
println("KJ的年龄大于20岁!")
}
}
- 创建一个规则并进行判断,输出规则目的
func main() {
condition := `
{
"OpLogic": "&&",
"Conditions": [
{
"Operator": ">=",
"Lhs": {
"VarExpr": "user.age"
},
"Rhs": {
"VarExpr": "user_ages#1"
}
}
]
}
`
// 创建一个表达式目的
aim := `
{
"Const": {
"StrConst": "规则通过!"
}
}
`
rule, err := arishem.NewNoPriorityRule("rule1", condition, aim)
if err != nil {
// 处理错误
return
}
dc, err := arishem.DataContext(`
{
"user": {
"name": "KJ",
"age": 24
},
"user_ages": [
20,
18,
32
]
}
`)
if err != nil {
// 处理错误
return
}
rr := arishem.ExecuteSingleRule(rule, dc)
if rr.Passed() {
fmt.Printf("%s 通过,输出=>%s", rr.Identifier(), rr.Aim().AsExpr())
}
}
输出结果
规则通过!
或者通过内置的builder函数来构建条件表达式和目的表达式
func main() {
condGroup := arishem.NewConditionsCondGroup(arishem.OpLogicAnd)
cond1 := arishem.NewCondition(operator.Equal)
cond1.Lhs = arishem.NewConstExpr(arishem.NewNumConst(1.0))
cond1.Rhs = arishem.NewConstExpr(arishem.NewNumConst(1.0))
condGroup.AddConditions(cond1)
expr, _ := condGroup.Build()
println(expr)
}
输出结果
{"OpLogic":"&&","Conditions":[{"Operator":"==","Lhs":{"Const":{"NumConst":1}},"Rhs":{"Const":{"NumConst":1}}}]}
更多使用方式请参考详细文档
可自定义的规则优先级定义和执行顺序
Arishem的另一个强大功能是支持灵活多变的规则执行顺序。Arishem支持优先级规则执行、非优先级规则执行以及优先级和非优先级混合执行。你甚至可以通过自实现Arishem规则接口,实现动态的优先级计算。相关使用方式请参考详细文档
丰富的操作符支持和内置函数
arishem支持多达20多种操作符,包括常用的值判断、数组判断,字符串判断等,并且arishem的另一个特性就是强大的类型自动转换功能。 在进行判断时,如果左值和右值的类型不一致,arishem将尝试进行类型统一,在进行类型转换时,以右值的类型为标准进行转换。 更多使用方式请参考详细文档
arishem内部集成了一些常见的功能函数,包括日期、数组、map和字符串函数,并且支持将自定义函数注册到arishem中使用!使用函数表达式来调用这些函数。
支持网络数据的并发访问和预取
arishem将实时获取的网络数据定义为一个feature(特征)。arishem在执行规则时通过分批的方式进行规则运算,因此在获取feature时也是按批次进行的。在使用涉及网络数据的场景中,使用FeatureExpr关键字来使用,并实现feature的获取方法。
{
...
"FeatureExpr": {
// user是特征名称,username是字段路径
"FeaturePath": "user.username"
}
}
type MyFeatureFetcher struct{}
...
func (m *MyFeatureFetcher) FetchFeature(feat typedef.FeatureParam, dc typedef.DataCtx) (typedef.MetaType, error) {
println("准备获取特征=>%s", feat.FeatureName())
// 在此处编写你的代码
return nil, nil
}
func init() {
arishem.Initialize(
arishem.DefaultConfiguration(),
arishem.WithFeatureFetcherFactory(func() typedef.FeatureFetcher {
return &MyFeatureFetcher{}
}),
)
}
每个feature的获取都在一个异步协程中执行,因此你可以无顾虑地进行网络IO访问。更多使用方式请参考详细文档。
灵活的自定义配置和监听回调
arishem支持多项自定义配置,包括规则运算时的缓存实现、批大小的计算方式以及无优先级的最大并发数量等。
在规则运算场景下,我们小组面临最多的问题是排查规则为什么通过/没通过?规则运算过程是否有错误?feature获取的具体过程是怎样的,究竟是哪个数据没有获取到? 因此,感知规则运算的具体过程非常必要,这将为后续排查规则命中详情提供基础能力支持。
arishem支持规则匹配过程中的条件回调和错误回调,并且FeatureFetcher也必须实现observable方法,以便让arishem内部将feature fetch的过程通知给每一个已注册的观察者。
// MyObserver实现了VisitObserver和FeatureFetchObserver接口
type MyObserver {}
func (m *MyObserver) OnFeatureFetchStart(feat typedef.FeatureParam) {}
func (m *MyObserver) OnFeatureFetchEnd(featureHash string, featureValue typedef.MetaType, err error) {}
func (m *MyObserver) OnJudgeNodeVisitEnd(info typedef.JudgeNode, vt typedef.VisitTarget) {}
func (m *MyObserver) OnVisitError(node, errMsg string, vt typedef.VisitTarget) {}
func main() {
arishem.ExecuteSingleRule(rule, dataCtx, WithVisitObserver(myObserver), WithFetchObserver(myObserver))
}
有关自定义配置和监听回调的更多使用方式,请参考详细文档。
开源许可
Arishem 基于Apache License 2.0 许可证。
联系&&问题反馈
目前,我们仅支持错误报告,请在此处提交您的问题。