机票自研实时规则引擎应用实践

 

机票业务由于其资源来源多样性,业务复杂,修改、增加业务逻辑繁琐耗时,成本奇高,在这个大背景下,急需一套解决方案来解耦复杂的业务逻辑。...

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();

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;
}
2、规则缓存实现

ConcurrentHashMap ruleScriptMaps = [b]new[/b] [b]ConcurrentHashMap[/b](16);
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);
3、规则执行接口

/**

* 执行

* @param binding 上下文参数

* @return

* @throws ExecuteScriptException

*/
Object [b]execute[/b](Binding binding) [b]throws[/b] ExecuteScriptException;
4、规则执行实现

Script script = Script.class.cast(this.scriptClass.newInstance());
script.setBinding(binding);
Object rest = script.run();
0111、性能踩坑&优化
Groovy与Java结合有三种实现方式:

  •  GroovyClassLoader
  •  GroovyShell
  •  GroovyScriptEngine
GroovyClassLoader用 Groovy 的 GroovyClassLoader ,动态地加载一个脚本并执行它的行为。GroovyClassLoader是一个定制的类装载器,负责解释加载Java类中用到的Groovy类。

GroovyClassLoader loader = [b]new[/b] [b]GroovyClassLoader[/b]();
Class groovyClass = loader.parseClass(scirptString);
GroovyObject groovyObject = (GroovyObject) groovyClass.[b]new[/b][b]Instance[/b]();
groovyObject.invokeMethod("run", "helloworld");
GroovyShellGroovyShell允许在Java类中(甚至Groovy类)求任意Groovy表达式的值。您可使用Binding对象输入参数给表达式,并最终通过GroovyShell返回Groovy表达式的计算结果。

GroovyShell shell = new GroovyShell();Script groovyScript = shell.parse(new File(groovyFileName));
Object[] args = {};
groovyScript.invokeMethod("run", args);
GroovyScriptEngineGroovyShell多用于推求对立的脚本或表达式,如果换成相互关联的多个脚本,使用GroovyScriptEngine会更好些。GroovyScriptEngine从指定的位置(文件系统,URL,数据库,等等)加载Groovy脚本,并且随着脚本变化而重新加载它们。如同GroovyShell一样,GroovyScriptEngine也能传入参数值,并能返回脚本的值。

我们采用了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类图和调用时序图如下:




    关注 同程研发中心


微信扫一扫关注公众号

0 个评论

要回复文章请先登录注册