机票自研实时规则引擎应用实践
机票业务由于其资源来源多样性,业务复杂,修改、增加业务逻辑繁琐耗时,成本奇高,在这个大背景下,急需一套解决方案来解耦复杂的业务逻辑。...
0000、复杂业务如何解耦
机票业务由于其资源来源多样性,业务复杂,修改、增加业务逻辑繁琐耗时,成本奇高,在这个大背景下,急需一套解决方案来解耦复杂的业务逻辑。0001、解决方案之《规则引擎》
经过思考和调研,决定采用规则引擎来解决以上问题。 规则引擎由推理引擎发展而来,是一种嵌入在应用程序中的组件,实现了将复杂常变的业务逻辑从应用程序代码中解耦出来,并使用预定义的语义模块编写业务决策规则。规则引擎具体执行基本上可以分为三步:
- 0x1:接受数据输入
- 0x2:解释业务规则
- 0x3:根据业务规则做出业务决策几个过程。
0010、规则引擎的实际用途和价值
规则引擎和流程引擎结合使用,是解决大规模复杂业务场景下两大利器,流程引擎本文不做说明,仅讲解规则引擎。0x1:举个例子
0x2:hard code的代价是很大的,随着业务的发展,越来越复杂会使开发人员和产品人员,厘清逻辑的难度和成本,成指数的倍增,而且会“心累”
相对于业务系统,规则引擎可以认为是一个独立于业务系统的模块,负责一些规则的计算等。一般来说,规则引擎主要应用在下面的场景中:
- 0x1:风控模型配置,风控是规则引擎
- 0x2:实时推荐系统配置,如根据用户流量属性,进行规则计算,输出计算结果
- 0x3:简单的离线计算,各类数据量比较小的统计等
- 0x4:一切需要解耦复杂业务逻辑的应用系统
0011、业界流行和先例
目前的规则引擎中,使用较多的开源规则引擎是Drools、和商用的ILOG JRules,经过调研,前者我们认为缺点是由于其引入了DRL语言比较复杂,学习曲线陡峭,而且很难二次开发符合当前的架构,后者是商用的,也不符合互联网行业的习性,因此要轻量、快速、接地气的规则引擎,只能自己开发了。0100、《规则引擎》的一般实现方式
一套规则引擎总体就是要做到以下的目标:
- 0x1:定义规则语言语义标准(我们采用的Groovy实现)
- 0x2:动态编辑规则条件,例如大于、小于、等于、如果、那么等等语义
- 0x3:动态编辑规则脚本
- 0x4:动态发布更新集群的能力
- 0x5:自动化执行
0101、规则引擎的一些难点要求
- 0x1:优先级问题,先执行什么判断、后执行什么判断,先执行什么逻辑、后执行什么逻辑
- 0x2:规则冲突如何避免
- 0x3:如何设计一套足够支撑全业务场景的规则管理模型。
0110、具体实现方案
业务规则我们认为可以抽象为以下几点:- 名称:一种唯一的规则名称
- code:每一条规则的唯一标识
- 描述:对规则的简要描述
- 内容:规则脚本具体内容
- 优先级:相对于其他规则的优先级
- 状态:规则脚本是测试中还是开发中还是验证中还是已上线
- 大于
- 小于
- 等于
- 如果
- 那么
- 否则
- 等等等
提供以下功能
- 定义规则脚本名称、编码、类型等
- 采用Codemirror实现在线编写规则脚本内容
- 提供在线配置规则判断流程
- 实时在线测试规则脚本
- 在线审核、上线
- 采用Zookeeper发布推送集群工作流
规则引擎如何执行&优化性能
通过规则条件、规则脚本预初始化,实现全部内存加载,我们的流程如下:
- 首先通过Zookeeper,发送通知,服务收到后,主动获取变更的规则
- 获取规则内容后,实例化规则对象
- 同时,使用规则code区分每一个规则
- 在集群中每一台服务器上的JVM内存中,缓存下来所有的规则对象,其中包含了Class文件(后文会说明,为何一定要缓存)
- 业务系统通过在管理平台,配置每一种业务在哪一种场景下,需要调用哪些规则,并且指定优先级
- 规则引擎,通过输入的code,在本地缓存中,找到在管理平台中,定义的code对应的规则执行流程
- 在流程中,依优先级,在本地缓存中获取每一条规则对象,取出其中的Class文件,获取实例对象Script
- 根据使用方输入参数,使用Binding对象,进行参数绑定,并使用setBinding(Binding binding)方法,进行绑定。
- 最后 call method run,进行规则执行。
1、规则提前初始化实现
[b]public[/b] GroovyRuleScript([b]@NonNull[/b] RuleScriptItemDTO ruleScriptItem) { [b]this[/b].createTime = System.currentTimeMillis();2、规则缓存实现
GroovyClassLoader classLoader = new GroovyClassLoader();
Class clazz = classLoader.parseClass(ruleScriptItem.getScript()); [b]this[/b].scriptClass = clazz; [b]this[/b].scriptId = ruleScriptItem.getScriptId(); [b]this[/b].name = ruleScriptItem.getName();
classLoader = null;
}
ConcurrentHashMap ruleScriptMaps = [b]new[/b] [b]ConcurrentHashMap[/b](16);3、规则执行接口
ConcurrentHashMap shuntScriptMaps = [b]new[/b] [b]ConcurrentHashMap[/b](16);
ConcurrentHashMap postScriptMaps = [b]new[/b] [b]ConcurrentHashMap[/b](16);[b]for[/b] (RuleScriptItemDTO scriptItem : [b]scriptResponse[/b].getData()) { [b]try[/b] { [b]if[/b] (Constants.RULE_TYPE.equals(scriptItem.getType())) {
ruleScriptMaps.put(scriptItem.getScriptId(), [b]new[/b] [b]GroovyRuleScript[/b](scriptItem));
} [b]else[/b] [b]if[/b] (Constants.SHUNT_TYPE.equals(scriptItem.getType())) {
shuntScriptMaps.put(scriptItem.getScriptId(), [b]new[/b] [b]GroovyRuleScript[/b](scriptItem));
} [b]else[/b] [b]if[/b] (Constants.POST_TYPE.equals(scriptItem.getType())) {
postScriptMaps.put(scriptItem.getScriptId(), [b]new[/b] [b]GroovyRuleScript[/b](scriptItem));
}
} [b]catch[/b] (Exception e) {
log.error("[RuleStrategyRepository] init script error:{}", e.getMessage(), e);
}
}
ruleScriptPool.reload(ruleScriptMaps);
shuntScriptPool.reload(shuntScriptMaps);
postScriptPool.reload(postScriptMaps);
/**4、规则执行实现
* 执行
* @param binding 上下文参数
* @return
* @throws ExecuteScriptException
*/Object [b]execute[/b](Binding binding) [b]throws[/b] ExecuteScriptException;
Script script = Script.class.cast(this.scriptClass.newInstance());
script.setBinding(binding);
Object rest = script.run();
0111、性能踩坑&优化
Groovy与Java结合有三种实现方式:- GroovyClassLoader
- GroovyShell
- GroovyScriptEngine
GroovyClassLoader loader = [b]new[/b] [b]GroovyClassLoader[/b]();GroovyShellGroovyShell允许在Java类中(甚至Groovy类)求任意Groovy表达式的值。您可使用Binding对象输入参数给表达式,并最终通过GroovyShell返回Groovy表达式的计算结果。
Class groovyClass = loader.parseClass(scirptString);
GroovyObject groovyObject = (GroovyObject) groovyClass.[b]new[/b][b]Instance[/b]();
groovyObject.invokeMethod("run", "helloworld");
GroovyShell shell = new GroovyShell();Script groovyScript = shell.parse(new File(groovyFileName));GroovyScriptEngineGroovyShell多用于推求对立的脚本或表达式,如果换成相互关联的多个脚本,使用GroovyScriptEngine会更好些。GroovyScriptEngine从指定的位置(文件系统,URL,数据库,等等)加载Groovy脚本,并且随着脚本变化而重新加载它们。如同GroovyShell一样,GroovyScriptEngine也能传入参数值,并能返回脚本的值。
Object[] args = {};
groovyScript.invokeMethod("run", args);
我们采用了GroovyClassLoader的方式实现其中的问题在于,使用不当, 会造成非常频繁的GC 通常使用如下代码在Java 中执行 Groovy 脚本:
GroovyClassLoader groovyLoader = new GroovyClassLoader();每次执行
Class groovyClass = (Class) groovyLoader.parseClass(groovyScript);Script groovyScript = groovyClass.newInstance();
[b]groovyLoader[/b].parseClass([b]groovyScript[/b])Groovy 为了保证每次执行的都是新的脚本内容,会每次生成一个新名字的Class文件。当对同一个规则脚本每次都执行这个方法时,会导致的现象就是装载的Class会越来越多,从而导致JVM的P区越来越大,而且无法被释放。
同时这里也存在性能瓶颈问题,去分析时会发现90%的耗时占用在Class的生成上
为了避免这一问题通常做法是缓存Script对象,从而避免以上2个问题。在这过程中通常又会引入新的问题:高并发情况下,binding对象混乱导致计算出错 在高并发的情况下,在执行赋值binding对象后,真正执行run操作时,拿到的binding对象可能是其它线程赋值的对象,所以出现数据计算混乱的情况。
长时间运行仍然出现oom,无法解决生成Class耗时耗力的问题由于groovyClassLoader会缓存每次编译groovy脚本的Class对象,下次编译该脚本时,会优先从缓存中读取,这样节省掉编译的时间。导致被加载的Class对象因为存在引用而无法被卸载,虽然通过缓存避免了短时间内大量生成新的class对象,但时间长了,无法被卸载的class会越来越多。
解决问题的办法是:
- 每个script都new一个GroovyClassLoader来装载;
- 对于parseClass后生成的Class对象进行本地cache。
1000、总结
干货总结,总要有点干货是不,在此之前呢,先总结下整个设计研发过程- 规则引擎研发设计历时 2周
- 上线方应用:机票实时推荐系统集成规则引擎
- 持续1小时峰值TPS 2.99万
- 99.9%规则执行响应时间10ms内
- 99.99%规则执行响应时间 50ms内
- 99.999%规则执行响应时间在500ms内
最后来点超级干货,附上我们的规则引擎的完成UML类图和调用时序图如下:
关注 同程研发中心
微信扫一扫关注公众号