规则引擎从ILOG切换Drools过程总结

概述

有个项目的规则引擎一直用的ibm公司的ILOG产品,最近采用开源规则引擎drools进行替换。 下文介绍一下选型、规则文件迁移以及性能方面遇到的问题。

技术选型

成熟的规则引擎实现是比较复杂,如果直接转换成普通的代码实现,无论是转换还是后续维护的工作量都比较大,所以还是考虑采用规则引擎。

目前的规则引擎系统中,其实没有太多选择,使用较多的开源规则引擎是Drools,另外还有商用的ILOG。但这两款规则引擎设计和实现都比较复杂,学习成本高,适用于大型应用系统。

所以主要是对比一下ILOG和Drools的特性,评估可行性。

  ILOG Drools
功能特性 支持各种规则编写方式,功能丰富 类似,DRL对应规则文件,也有决策表的方式
语法规则 规则文件编写比较容易编写 DRL语法也是一种DSL,上手难度还能接受。语法基本有一对一的映射,有迁移的可行性
性能 性能高 采用Phreak算法(基于成熟RETE算法演进过来的),性能也相当不错
活跃度 活跃,发布频率也是很高的

尝试在调用入口改造,做了一些简单的功能验证,包括规则流、规则文件和决策表,都可以跑起来。 下面就是要考虑原始的规则文件怎么处理了。

规则文件迁移

存量的规则文件怎么搬,这是一个关键问题。

我们首先用手工搬了几个常规的规则文件,从中可以发现或多或少是有一些对应关系的,至少功能上应该是问题不大的。

但是项目的规则文件比较多,不同的流程,规则文件数量从几百个到两千个不等,手工转换是不现实的,要考虑程序转换。

预定义

  指定'A属性'为一个 服务属性来自(in) '服务属性列表'
  	  符合条件:如下的所有条件都成立:
  	  -当前这个A属性 的属性值 不是 ""
  	  -当前这个A属性 的属性编码 是 # "xxx"
  	  -当前这个A属性 的编码是 # "yyy";
   指定 '服务费用'  为  'A属性'取为属性双精度值;

  如果
	 'A属性' 不为空
	 并且 'A费项' 不为空
  那么
     'A费项' 初始化服务费用( 规则名称: "计算服务费" , 金额: '服务费'*100);

例如上面这个规则。很明显有“预定义-指定-如果-那么”等关键字,我们可以猜测,“预定义”表示这是一个规则,“指定”是选择原始的数据,“如果”是过滤条件,“那么”是规则要触发的逻辑。

对应到Drools规则,“指定”和“如果”都是属于条件部分(when),“那么”对应“then”。

很明显,无论是ILOG还是Drools,都是采用了一种领域特定语言(DSL),是有语法的,只是这个语法没有严格的指导说明,但是我们还是可以把它当成一种语言,解析成一个通用模型,然后再转成另外一套语言。

我们是采用Antlr这个语法解析库来解析ILOG规则(主要的g4文件连注释有600多行)。有了程序,转换工作就非常顺利了。

性能优化

最开始在环境测试,响应就几十毫秒,效果还是不错的。

后来准备上生产环境的时候,发现就慢很多,发现用的规则数差不多2千条,主要表现为:

  • 并发少量用户就不行,例如5个用户,耗时一下就上去了,几百毫秒~一两秒不等
  • 增加POD的内存,会有一定的改善,但不是很明显
  • 增加POD的数量,TPS没有明显改善,性能没法成线性增长

好吧,Drools的资料的确不多,不过应该不至于这么弱鸡的,需要分析分析。

首先,我们发现GC影响非常大(通过jstat -gcutil pid 5s查看)

S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00  18.75  17.13  85.89  91.67  86.34   8713  311.998    10   27.300  339.298
  0.00  18.75  17.13  85.89  91.67  86.34   8743  312.997    12   28.400  341.397

通过火焰图也可以看到类似的情况

各种数据表明,Drools执行过程中产生大量的对象,需要大量的GC工作。

通过查阅相关资料,发现很多示例代码的写法(包括官方的代码片段)都是有问题的,在默认情况下,需要在执行规则后清理原来插入的fact对象,否则会影响存在内存的及时回收。具体资料如下:

  • https://www.cnblogs.com/daoqidelv/p/7246624.html
  • https://stackoverflow.com/questions/48533759/drools-with-high-gc-time

修复了这个bug之后,GC问题就不是很明显了,但性能依然不大行,压测的TPS还是上不去。

又翻了一波材料,发现有个sequential模式(具体不解释了),可以优化规则的执行方式,但是需要在无状态会话情况下(看不懂也请跳过)。于是改造成无会话规则执行方式,并开启了sequential模式。

KieBaseConfiguration conf = ks.newKieBaseConfiguration();
conf.setOption(SequentialOption.YES);
conf.setOption(SequentialAgendaOption.SEQUENTIAL);
KieBase kieBase = kieContainer.newKieBase(conf);

果然,调整之后,压力较少的情况下,性能有了比较明显的变化,不占用大量内存也可以跑得比较欢快,但还是并发加大的情况下,响应时间上涨比较快,加POD很难获得持续的性能提升。

为什么呢,由于规则引擎是CPU密集,如果同时处理的任务超过可用的CPU资源,那么只会增加CPU竞争,性能反而下降。于是在启动的时候,通过CPU资源调整并发线程数,多余的请求只能排队了。

// CPU密集型,需要限制线程数量,目前设置为CPU Core+1
int processors = Runtime.getRuntime().availableProcessors();
System.setProperty("server.tomcat.threads.max", String.valueOf(processors + 1));

通过这一步调整,通过增加POD可以获得比较持续的性能提升,基本满足要求。

总结

我们的同学是很优秀的,做事情也非常有耐心,没被各种没做过的事情、没见过的困难吓到。

但是适应不同的场景,需要各层面的知识,同时需要用到不同的开发工具或者分析工具。这需要在业余时间注意积累,持续学习。像各种开源软件,可定制的选项还是比较多的,需要充分阅读官方文档,遇到要多查找相关案例,通常可以找到蛛丝马迹。

在这个过程中学到很多,谢谢大家。