前言
引擎之动态脚本
Groovy
QLExpress
引擎之流程编排
引擎之业务基础
最后
我们提到风控规则引擎的时候,势必会联想起动态脚本,因为它是其灵魂所在之处,不仅被广泛运用到运营、CRM、电商风控和金融风控等业务场景中,还常出现在运维、ETL工程、实时计算和服务编排等技术场景中
在这里我们主要选择两个动态脚本进行对比详解
依赖版本2.5.10
得益于Java中的SPI机制,它是典型的文件配置、策略模式和面向接口编程的集合体,可以轻松自由接入使用轻量级脚本,常见有Groovy、JavaScript等脚本。对应的脚本实现接入接口类javax.script.ScriptEngineFactory
我们先来看下引擎实例化过和编译执行流程
Groovy脚本的编译和执行流程已经可以从图上了解个大概了,那接下来我们将会做一一映射的源码,以及相应的剖析其优缺点
按照日常开发思维,若想提高执行动态脚本的性能而言,除了优化脚本和提高外在配置以外,最直接能想到便是缓存编译后的脚本,Java的脚本包有可被预编译的接口和直接执行脚本的接口,那么预编译和仅首次执行时编译有何区别。
自定义实现使用本地缓存进行脚本预编译,将脚本作为key,预编译后的脚本CompileScript实例作为value,这样每次执行脚本都可以通过缓存获取的方式去执行compileScript.eval(bindings)获得执行结果
为什么我们不可以直接通过脚本引擎核心ScriptEngine去调用?当然GroovyEngine也实现了此入口,让我们来瞅瞅是怎么具体操作的
从图4
和图5
中我们可以看出预编译和直接执行脚本都使用了getScriptClass
去获取编译后脚本对象。从引擎的这个方法实现中我们可以看到classMap,GroovyScriptEngineImpl使用ConcurrentHashMap去做脚本缓存,如果不存在GroovyClassLoader则开始进行编译脚本,获取class脚本类
(我们可以看出预编译小节中的自定义缓存像是多做了一层无用缓存)此处就是原生支持首次编译(几百毫秒左右,视脚本复杂度而定)后续无需在编译相同脚本,且大大提高脚本执行性能(0~5ms)的优势体现,但是编译期间
如果引擎为单例模式,且做了微服务化后提供引擎服务,由于需要编译各个业务线的所有脚本,而GroovyClassLoader中有一个class对象的缓存,每次编译脚本时都会在Map中缓存脚本对象
,可能会造成内存泄露,此时我们应该及时清理缓存。
由上图可知,执行所需的必需要素:编译后的脚本对象Class<?>
和执行的上下文ScriptContext
,其中上下文(线程安全)包含了执行的参数变量值、外部类以及针对脚本输入输出的处理。我们主要看下参数变量值和外部类的具体使用。
解释: Bindings
继承了Map并再次重写Map中的操作;在执行期间,eval方法会将bingings中的KV对写入上下文的scope中,当然还支持外部方法的调用(如下图),支持单一参数和多参数的方法以及静态方法,关于invokeMethod的深入就留给各位去探索发现了
小结
上述讲解了Groovy脚本预编译和首次执行编译的区别和相同点,无论是单例引擎执行器SDK(去中心化)的方式亦或是单例引擎微服务(中心化)方式,遇到需要编译大量脚本时都要及时清理编译编译期缓存,避免可能发生的内存泄漏
玩物风控前期引入此作为规则引擎的脚本解析运行核心。
两者都使用了Map作为脚本本地缓存策略,QLExpressContext上下文中也传入Map作为变量集合,此处和Bindings如出一辙,但是QLExpressContext的构造函数内额外集成了org.springframework.context.ApplicationContext
从图上可以看出我们可以自由访问Spring容器中的业务实例,方便我们进行业务数据调用。
由于QLExpress诞生于阿里电商环境中,面临较多定制化的需求,所以它更注重功能扩展;底层语言,QLExpress使用纯Java编写,无需用到SPI机制,它去掉了语法声明和一些集合遍历方式,这里可以看出兼容java语法方面不及groovy;在编译脚本方面,支持首次执行时的编译缓存,其编译过程与groovy的首次执行编译相似,同样使用Map作为本地缓存,但不会累积编译缓存,较为安全稳定,无需再额外操作清理缓存
特点:
//IOperateDataCache中包含了函数数据类型定义、指令集合上下文等
private ThreadLocal<IOperateDataCache> m_OperateDataObjectCache = new ThreadLocal<IOperateDataCache>(){
protected IOperateDataCache initialValue() {
return new OperateDataCacheImpl(30);
}
};
// 线程本地存储
private static ThreadLocal<RunnerDataCache> m_OperateDataObjectCache = new ThreadLocal<RunnerDataCache>(){
protected RunnerDataCache initialValue() {
return new RunnerDataCache();
}
};
弱类型脚本语言,和groovy,javascript语法类似,虽然比强类型脚本语言要慢一些,但是使业务的灵活度大大增强。
安全控制,可以通过设置相关运行参数,预防死循环、高危系统api调用等情况。
以下在指令运行期间进行超时判断,可外部自由设置超时时长,如遇死循环时可及时抛出异常处理,释放机器资源
代码精简,依赖最小,250k的jar包适合所有java的运行环境,在android系统的低端pos机也得到广泛运用。
基于可维护,轻量级的目的,我们采用了目前前端市场上较为前沿的可视化图编辑库。
采用了G6作为前端流程编排的主要工具,期间开发成本较高,代码量较多,自定义的参数更偏底层,所需的学习成本也随之上升
特点:
以下是针对条件判断的节点图形的自定义,需要去调校svg路径,带了额外的工作开销
getPath (cfg) {
const size = cfg.size || [40, 40] // 如果没有 size 时的默认大小
const width = size[0]
const height = size[1]
// /\
// /条\
// \件/
// \/
const path = [
['M', 0, 0 - height / 2], // 上部顶点
['L', width / 2, 0], // 右侧点
['L', 0, height / 2], // 下部
['L', -width / 2, 0], // 左侧
['Z'] // 封闭
]
return path
},
为了解决第一版流程引擎遗留下来的痛点,我们调研了刚出世不久的x6,相对于g6而言,它在流程编排上更为专业,原生也更友好的支持开发配置规则流程,与风控业务的适配性更高,且开发使用成本是第一版的一半
特点:
// 对原生的Rect(矩形)进行扩展,样式优化等得到flow-chart-rect图形
shape: 'flow-chart-rect',
width: 60, // 宽
height: 60, // 高
angle: 45, // 旋转角度
目前玩物风控处于发展初期,还有待进一步深入挖掘,为适应未来更为复杂的风控业务和多变未知的业务风险,革命还得继续,相关的规则版本的管理、风控算法的升级、规则引擎的迭代升级、规则模型的自动优化以及针对规则链路的机器学习生成等,这是一个不断对抗业务风险,保障业务平稳健康增长的升级过程,需要开发人员和业务策略分析师共同努力。
牛年邀牛人
一起战斗、一起成长
技术、产品、UED、运营、职能等海量岗位
玩物得志期待你的加入