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內存泄露方面的資料還是很少的,特此記錄,希望能夠幫助遇到同樣問題的朋友!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章