drools规则引擎因为内存泄露导致的内存溢出

Posted crazy_itman

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了drools规则引擎因为内存泄露导致的内存溢出相关的知识,希望对你有一定的参考价值。

进入这个问题之前,先了解一下drools:

在很多行业应用中比如银行、保险领域,业务规则往往非常复杂,并且规则处于不断更新变化中,而现有很多系统做法基本上都是将业务规则绑定在程序代码中。

主要存在的问题有以下几个方面:

1) 当业务规则变更时,对应的代码也得跟着更改,每次即使是小的变更都需要经历开发、测试验证上线等过程,变更成本比较大。
2) 长时间系统变得越来越难以维护。
3) 开发团队一般是由一个熟悉业务的BA(业务分析人员)和若干个熟悉技术的开发人员组成,开发人员对业务规则的把握能力远不及BA,但实际上却承担了将业务规则准确无误实现的重任。
4) 系统僵化,新需求插入困难。
5) 新需求上线周期较长。
能否让我们的业务系统更灵活一点呢?
思路:将业务规则从技术实现中提取出来,实现技术和业务分离,开发人员处理 技术、业务分析人员定义业务规则,各自做自己所擅长的事情。
方案:目前已经有比较成熟的开源产品支持,它就是Drools,我们将业务规则定义在DataBase或者BRMS(Business Rule Management System)中,通过管理DB或者BRMS实现业务逻辑的动态改变。
什么时候应该使用规则引擎?
虽然规则引擎能解决我们的许多问题,但我们还需要认真考虑一下规则引擎对我们的项目本身是否是合适的。需要关注的点有:
1)我的应用程序有多复杂?
对于那些只是把数据从数据库中传入传出,并不做更多事情的应用程序,最好不要使用规则引擎。但是,当在Java中有一定量的商业逻辑处理的话,可以考虑Drools的使用。这是因为很多应用随着时间的推移越来越复杂,而Drools可以让你更轻松应对这一切。
2) 我的应用的生命周期有多久?
如果我们应用的生命周期很短,也没有必要使用Drools,使用规则引擎将会在中长期得到好处。
3) 我的应用需要改变吗?
这个答案一般情况下是肯定的,“这世界唯一不变的只有变化”,我们需求也是这样的,无论是在开发过程中或是在开发完成以后,Drools能从频繁变化的需求中获得好处。
规则引擎是基于规则的专家系统的核心部分,主要由三部分组成:规则库(Knowledge base)+Working Memory(Fact base)+推理机(规则引擎),规则引擎根据既定事实和知识库按照一定的算法执行推理逻辑得到正确的结果。
Drools 是一个基于Charles Forgy‘s的RETE算法的,易于访问企业策略、易于调整以及易于管理的开源业务规则引擎,符合业内标准,速度快、效率高。
业务分析人员或审核人员可以利用它轻松查看业务规则,从而检验是否已编码的规则执行了所需的业务规则。

Drools是一个基于java的规则引擎,开源的,可以将复杂多变的规则从硬编码中解放出来,以规则脚本的形式存放在文件中,使得规则的变更不需要修正代码重启机器就可以立即在线上环境生效。

drools的基本工作过程:通常而言我们使用一个接口来做事情,首先要传进去参数,其次要获取到接口的实现执行完毕后的结果,而drools也是一样的,我们需要传递进去数据,用于规则的检查,调用外部接口,同时还可能需要获取到规则执行完毕后得到的结果。在drools中,这个传递数据进去的对象,术语叫 Fact对象。Fact对象是一个普通的java bean,规则中可以对当前的对象进行任何的读写操作,调用该对象提供的方法,当一个java bean插入到WorkingMemory中,规则使用的是原有对象的引用,规则通过对fact对象的读写,实现对应用数据的读写,对于其中的属性,需要提供getter setter访问器,规则中,可以动态的往当前WorkingMemory中插入删除新的fact对象。
规则文件可以使用 .drl文件,也可以是xml文件。
规则语法:
package:对一个规则文件而言,package是必须定义的,必须放在规则文件第一行。特别的是,package的名字是随意的,本人认为可以直接将其理解为namespace命名空间就行,不必必须对应物理路径,跟java的package的概念不同,这里只是逻辑上的一种区分。同样的package下定义的function和query等可以直接使用。
比如:package com.drools.zken.test
import:导入规则文件需要使用到的外部变量,这里的使用方法跟java相同,但是不同于java的是,这里的import导入的不仅仅可以是一个类,也可以是这个类中的某一个可访问的静态方法。
比如:
import com.drools.zken.test.User;
import com.drools.zken.test.User.getUserById;
rule:定义一个规则。rule "ruleName"。一个规则可以包含三个部分:
属性部分:定义当前规则执行的一些属性等,比如是否可被重复执行、过期时间、生效时间等。
条件部分,即LHS(Left Hand Side),定义当前规则的条件,如  when User(); 判断当前WorkingMemory中是否存在User对象。
结果部分,即RHS(Right Hand Side),这里可以写普通java代码,即当前规则条件满足后执行的操作,可以直接调用Fact对象的方法来操作应用。
规则实例:
rule "name"
       no-loop true
       when
               $user:User(id == 6666)
       then
               System.out.println("Ok");
               $user.setSalary(10000);
               update($user);
end
上述的属性中:
no-loop : 定义当前的规则是否不允许多次循环执行,默认是false,也就是当前的规则只要满足条件,可以无限次执行。什么情况下会出现一条规则执行过一次又被多次重复执行呢?drools提供了一些api,可以对当前传入WorkingMemory中的Fact对象进行修改或者个数的增减,比如上述的update方法,就是将当前的workingMemory中的User类型的Fact对象进行属性更新,这种操作会触发规则的重新匹配执行,可以理解为Fact对象更新了,所以规则需要重新匹配一遍,那么疑问是之前规则执行过并且修改过的那些Fact对象的属性的数据会不会被重置?结果是不会,已经修改过了就不会被重置,update之后,之前的修改都会生效。当然对Fact对象数据的修改并不是一定需要调用update才可以生效,简单的使用set方法设置就可以完成,这里类似于java的引用调用,所以何时使用update是一个需要仔细考虑的问题,一旦不慎,极有可能会造成规则的死循环。上述的no-loop true,即设置当前的规则,只执行一次,如果本身的RHS部分有update等触发规则重新执行的操作,也不要再次执行当前规则。
但是其他的规则会被重新执行,岂不是也会有可能造成多次重复执行,数据紊乱甚至死循环?答案是使用其他的标签限制,也是可以控制的:lock-on-active true
lock-on-active true:通过这个标签,可以控制当前的规则只会被执行一次,因为一个规则的重复执行不一定是本身触发的,也可能是其他规则触发的,所以这个是no-loop的加强版。当然该标签正规的用法会有其他的标签的配合,后续提及。
date-expires:设置规则的过期时间,默认的时间格式:“日-月-年”,中英文格式相同,但是写法要用各自对应的语言,比如中文:"29-七月-2010",但是还是推荐使用更为精确和习惯的格式,这需要手动在java代码中设置当前系统的时间格式,后续提及。属性用法举例:date-expires "2011-01-31 23:59:59" , 这里我们使用了更为习惯的时间格式
date-effective:设置规则的生效时间,时间格式同上。
duration:规则定时,duration 3000   3秒后执行规则
salience:优先级,数值越大越先执行,这个可以控制规则的执行顺序。
其他的属性可以参照相关的api文档查看具体用法。
规则的条件部分,即LHS部分:
when:规则条件开始。条件可以单个,也可以多个,多个条件依次排列,比如
 when
         eval(true)
         $customer:Customer()
         $user:User(id==6666)
上述罗列了三个条件,当前规则只有在这三个条件都匹配的时候才会执行RHS部分,三个条件中第一个
eval(true):是一个默认的api,true 无条件执行,类似于 while(true)
$user:User(id==6666) 这句话表示:当前的WorkingMemory中存在User类型并且id属性的值为6666的Fact对象,这个对象通常是通过外部java代码插入或者自己在前面已经执行的规则的RHS部分中insert进去的。
前面的$user代表着当前条件的引用变量,在后续的条件部分和RHS部分中,可以使用当前的变量去引用符合条件的FACT对象,修改属性或者调用方法等。可选,如果不需要使用,则可以不写。
条件可以有组合,比如:
User(name==‘张三‘ || (id > 1000 && age == 27))
RHS中对Fact对象private属性的操作必须使用getter和setter方法,而RHS中则必须要直接用.的方法去使用,比如
  $order:Order(name=="shopping_001")
  $user:User(id==6666 && orders contains $order && $order.name=="shopping_001")
特别的是,如果条件全部是 &&关系,可以使用“,”来替代,但是两者不能混用
如果现在Fact对象中有一个List,需要判断条件,如何判断呢?
看一个例子:
User {
        int id;
        List<String> names;
}
$user:User(id==6666 && names contains "张三" && names.size >= 1)
上述的条件中,id必须是6666,并且names列表中含有“张三”并且列表长度大于等于1
contains:对比是否包含操作,操作的被包含目标可以是一个复杂对象也可以是一个简单的值。 
Drools提供了十二种类型比较操作符:
>  >=  <  <=  ==  !=  contains / not contains / memberOf / not memberOf /matches/ not matches
not contains:与contains相反。
memberOf:判断某个Fact是否在某个集合中,与contains不同的是他被比较的对象是一个集合,而contains被比较的对象是单个值或者对象。
not memberOf:正好相反。
matches:正则表达式匹配,与java不同的是,不用考虑‘/‘的转义问题
not matches:正好相反。
规则的结果部分
当规则条件满足,则进入规则结果部分执行,结果部分可以是纯java代码,比如:
then
       System.out.println("Ok"); //会在控制台打印出ok
end
当然也可以调用Fact的方法,比如  $user.shopping();操作数据库等等一切操作。
结果部分也有drools提供的方法:
insert:往当前workingMemory中插入一个新的Fact对象,会触发规则的再次执行,除非使用no-loop限定;
update:更新
modify:修改,与update语法不同,结果都是更新操作
retract:删除
RHS部分除了调用Drools提供的api和Fact对象的方法,也可以调用规则文件中定义的方法,方法的定义使用 function 关键字
function void console {
   System.out.println();
   DBHelper.getConnection();// 调用外部静态方法,DBHelper必须使用import导入,getConnection()必须是静态方法
}
Drools还有一个可以定义类的关键字:
declare 可以在规则文件中定义一个class,使用起来跟普通java对象相似,你可以在RHS部分中new一个并且使用getter和setter方法去操作其属性。
declare Address
     @author(iamzken) // 元数据,仅用于描述信息
     @createTime(2015-10-13)
      city : String @maxLengh(100)
      id    : int
end
上述的‘@‘是什么呢?是元数据定义,用于描述数据的数据,没什么执行含义
你可以在RHS部分中使用Address address = new Address()的方法来定义一个对象。

drools固然好用,但是如果用不好,极有可能出现oom问题

上面大致介绍了一下drools,下面进入正题:

直接上代码:

这个是主要的代码片段:

WorkingMemory w = init("service.drl");//初始化WorkingMemory对象
User user = new User();
FactHandle fact =  w.insert(user);
w.fireAllRules();
w.dispose();
w.retract(fact);				
						

规则文件(service.drl):

package com.iamzken.test.service
import com.iamzken.test.model.User
rule "rule-test"
	activation-group "test001"
	salience 100
	lock-on-active true
	dialect "mvel"
	when
		 $user : User( salary > 2000 , gender=="1")
	then
		 $user.setSalary(3000);
		 update($user);
end


刚开始一直让我无法理解的是:测试环境上的数据量有370万左右,生产环境上的数据量有350万,从数据量上来看,生产环境并没有测试环境的数据量大,为什么测试环境就没有出现OOM而生产环境就OOM了呢?

带着这种疑问,我们首先加大了生产环境机器的内存,并调大了jvm内存相关参数,发现还是OOM

最后,经过种种排查,包括构造数据,使用jprofiler工具,分析dump文件,最终定位到是drools规则引擎导致的问题!

原因:

修改之前的代码:

w.insert(user);
w.fireAllRules();
w.dispose();


修改之后的代码:

FactHandle fact =  w.insert(user);
w.fireAllRules();
w.dispose();
w.retract(fact);

或者:

w.insert(user);
w.fireAllRules();
w.dispose();
w.retract(w.getFactHandle(user));


如上代码所示,w.insert(user)执行完这句代码后会返回一个FactHandle对象,需要WorkingMemory调用retract方法将其从内存中清除掉,不然,即使走完了规则变成了垃圾对象,也无法被垃圾回收器回收,因为WorkingMemory还在引用这个插进去的User对象 。

为什么数据量大的测试环境没有出现oom呢?原因很简单:因为测试环境的数据量虽然大,但能够匹配规则的数据量少,也就是说插入进WorkingMemory中的User对象少,而生产环境正好相反,这就是为什么数据量小反倒还出现oom的原因!

结论:这是因为drools内存泄露导致的内存溢出!

网上关于drools内存泄露方面的资料还是很少的,特此记录,希望能够帮助遇到同样问题的朋友!


以上是关于drools规则引擎因为内存泄露导致的内存溢出的主要内容,如果未能解决你的问题,请参考以下文章

Andorid 内存溢出与内存泄露,几种常见导致内存泄露的写法

内存溢出和泄露

常用概念比较

JAVA中内存泄露和内存溢出

内存溢出(Oom)和内存泄露(Memory leak)

Andfroid 内存溢出与内存泄漏的简单分析与解决