决策表的 Drools 性能
Drools performance for decision tables
当我尝试使用 Drools 引擎计算保险费时,我遇到了潜在的 performance/memory 瓶颈。
我在我的项目中使用 Drools 将业务逻辑与 java 代码分开,我决定也将其用于保费计算。
- 我使用 Drools 的方式不对吗?
- 如何以更高效的方式满足要求?
详情如下:
计算
我必须计算给定合同的保险费。
合约配置为
- productCode(来自字典的代码)
- contractCode(来自字典的代码)
- 客户的个人资料(例如年龄、地址)
- 保险金额 (SI)
- 等等
目前,溢价是使用这个公式计算的:
premium := SI * px * (1 + py) / pz
其中:
- px 是 factor 在 Excel 文件中参数化并且 depends on 2属性(客户的年龄和性别)
- py 是 factor 在 excel 文件中参数化并且 depends on 4合约的属性
- pz - 同样
要求
- R1 – java 代码不知道 公式,
- R2 - java 代码对 公式依赖关系一无所知 ,换句话说,premium 依赖于:px, py , pz,
- R3 - java 代码对 参数的依赖关系一无所知,我的意思是 px 取决于客户的年龄和性别, 依此类推。
实施 R1、R2 和 R3 后,我有 java 代码与业务逻辑分离,任何业务分析师 (BA) 都可以修改公式并添加新的依赖项而无需重新部署。
到目前为止我的解决方案
我有合同领域模型,它由classes合同、产品、客户、政策等组成。合约 class 定义为:
public class Contract {
String code; // contractCode
double sumInsured; // SI
String clientSex; // M, F
int professionCode; // code from dictionary
int policyYear; // 1..5
int clientAge; //
... // etc.
另外我介绍了 Var class 它是 any 参数化变量的容器:
public class Var {
public final String name;
public final ContractPremiumRequest request;
private double value; // calculated value
private boolean ready; // true if value is calculated
public Var(String name, ContractPremiumRequest request) {
this.name = name;
this.request = request;
}
...
public void setReady(boolean ready) {
this.ready = ready;
request.check();
}
...
// getters, setters
}
最后 - 请求 class:
public class ContractPremiumRequest {
public static enum State {
INIT,
IN_PROGRESS,
READY
}
public final Contract contract;
private State state = State.INIT;
// all dependencies (parameterized factors, e.g. px, py, ...)
private Map<String, Var> varMap = new TreeMap<>();
// calculated response - premium value
private BigDecimal value;
public ContractPremiumRequest(Contract contract) {
this.contract = contract;
}
// true if *all* vars are ready
private boolean _isReady() {
for (Var var : varMap.values()) {
if (!var.isReady()) {
return false;
}
}
return true;
}
// check if should modify state
public void check() {
if (_isReady()) {
setState(State.READY);
}
}
// read number from var with given [name]
public double getVar(String name) {
return varMap.get(name).getValue();
}
// adding uncalculated factor to this request – makes request IN_PROGRESS
public Var addVar(String name) {
Var var = new Var(name, this);
varMap.put(name, var);
setState(State.IN_PROGRESS);
return var;
}
...
// getters, setters
}
现在我可以使用这些 classes 和这样的流程:
request = new ContractPremiumRequest(contract)
- 使用
state == INIT
创建请求
px = request.addVar( "px" )
- 用
ready == false
创建 Var("px")
- 将请求移至
state == IN_PROGRESS
py = request.addVar( "py" )
px.setValue( factor )
,px.setReady( true )
- 在
px
上设置计算值
- 使
ready == true
request.check()
如果 ALL 变量准备就绪,则 state == READY
- 现在我们可以使用公式,因为请求已计算出所有依赖关系
我已经创建了 2 DRL 规则 并准备了 3 决定 tables (px.xls, py.xls, ...) 由 BA 提供的因素。
规则 1 - contract_premium_prepare.drl:
rule "contract premium request - prepare dependencies"
when
$req : ContractPremiumRequest (state == ContractPremiumRequest.State.INIT)
then
insert( $req.addVar("px") );
insert( $req.addVar("py") );
insert( $req.addVar("pz") );
$req.setState(ContractPremiumRequest.State.IN_PROGRESS);
end
规则 2 - contract_premium_calculate.drl:
rule "contract premium request - calculate premium"
when
$req : ContractPremiumRequest (state == ContractPremiumRequest.State.READY)
then
double px = $req.getVar("px");
double py = $req.getVar("py");
double pz = $req.getVar("pz");
double si = $req.contract.getSumInsured();
// use formula to calculate premium
double premium = si * px * (1 + py) / pz;
// round to 2 digits
$req.setValue(premium);
end
决定tablepx.xls:
决定tablepy.xls:
KieContainer 在启动时构建一次:
dtconf = KnowledgeBuilderFactory.newDecisionTableConfiguration();
dtconf.setInputType(DecisionTableInputType.XLS);
KieServices ks = KieServices.Factory.get();
KieContainer kc = ks.getKieClasspathContainer();
现在计算给定合约的权利金,我们写:
ContractPremiumRequest request = new ContractPremiumRequest(contract); // state == INIT
kc.newStatelessKieSession("session-rules").execute(request);
BigDecimal premium = request.getValue();
事情是这样的:
- 规则 1 针对
ContractPremiumRequest[INIT]
触发
- 此规则创建并添加 px、py 和 pz 依赖项(
Var
个对象)
- 适当的excel行为每个px、py、pz对象触发并且使其准备就绪
- Rule2 触发
ContractPremiumRequest[READY]
并使用公式
数量
- PX 决定 table 有 ~100 行,
- PY 决定 table 有 ~8000 行,
- PZ 决定 table 有 ~50 行。
我的成绩
第一次计算,加载和初始化决策 tables 需要 ~45 秒 – 这可能会出现问题。
每次计算(经过一些预热后)需要 ~0.8 毫秒——这对我们的团队来说是可接受的table。
堆消耗为 ~150 MB – 这是有问题的,因为我们预计将使用更多大 tables。
问题
- 我使用 Drools 的方式不对吗?
- 如何以更高效的方式满足要求?
- 如何优化内存使用?
========== 编辑(2 年后)==========
这是 2 年后的简短总结。
正如我们预期的那样,我们的系统发展得非常快。我们以超过 500 tables(或矩阵)结束,其中包含保险定价、精算因素、保险配置等。
一些 table 的大小超过 100 万行。我们使用了 drools 但我们无法处理性能问题。
终于用上了Hyperon引擎(http://hyperon.io)
这个系统是一个野兽 - 它允许我们在大约 10 毫秒的总时间内 运行 数百个规则匹配。
我们甚至能够对 UI 字段上的每个 KeyType 事件触发完整的策略重新计算。
据我们了解,Hyperon 为每个规则使用快速内存索引 table 并且这些索引以某种方式被压缩,因此它们几乎不占用内存。
我们现在还有一个好处 - 所有定价、因素、配置 table 都可以在线修改(值和结构),这对 java 代码是完全透明的。
应用程序只是继续使用新逻辑,不需要开发或重新启动。
但是我们需要一些时间和精力来充分了解 Hyperon :)
我找到了我们团队一年前做的一些比较 - 它显示了引擎初始化 (drools/hyperon) 和从 jvisualVM 角度来看的 100k 简单计算:
仔细阅读问题后,我会提供一些建议:
与 Excel 电子表格相比,我更喜欢关系数据库。
这些都是非常简单的计算。我认为这个模型有点矫枉过正。对于这种规模的问题,规则引擎似乎太大了。
我会更简单地编写代码。
使计算基于接口,以便您可以通过注入新的 class 实现来修改它。
了解如何编写 Junit 测试。
我的第一选择是简单的决策table计算,没有规则引擎,维护关系数据库中的因素。
Rete 规则引擎是 if/else 或 switch 语句的重锤。除非您利用归纳功能,否则我认为这太过分了。
我不会在会话中添加任何内容。我正在设想一个幂等的 REST 服务,它接受一个请求和 returns 一个带有溢价的响应以及任何其他必须返回的响应。
在我看来,您过早地使解决方案过于复杂了。做可能有效的最简单的事情;衡量绩效;根据您获得的数据和要求根据需要进行重构。
您的开发经验如何?你是一个人还是团队的一员?这是你没做过的新系统吗?
问题是您为相对少量的数据创建了大量代码(所有规则都来自表格)。我见过类似的案例,他们都受益于将表作为数据插入。 PxRow、PyRow 和 PzRow 应该这样定义:
class PxRow {
private String gender;
private int age;
private double px;
// Constructor (3 params) and getters
}
数据仍然可以在(更简单的)电子表格中或任何您喜欢的 BA 研究人员输入的数据中。您将所有行作为事实 PxRow、PyRow、PzRow 插入。那么你需要一两个规则:
rule calculate
when
$c: Contract( $cs: clientSex, $ca: clientAge,
$pc: professionCode, $py: policyYear,...
...
$si: sumInsured )
PxRow( gender == $cs, age == $ca, $px: px )
PyRow( profCode == $pc, polYear == $py,... $py: py )
PzRow( ... $pz: pz )
then
double premium = $si * $px * (1 + $py) / $pz;
// round to 2 digits
modify( $c ){ setPremium( premium ) }
end
忘记流量和所有其他装饰。但是你可能需要另一个规则,以防你的合约与 Px 或 Py 或 Pz 不匹配:
rule "no match"
salience -100
when
$c: Contract( premium == null ) # or 0.00
then
// diagnostic
end
当我尝试使用 Drools 引擎计算保险费时,我遇到了潜在的 performance/memory 瓶颈。
我在我的项目中使用 Drools 将业务逻辑与 java 代码分开,我决定也将其用于保费计算。
- 我使用 Drools 的方式不对吗?
- 如何以更高效的方式满足要求?
详情如下:
计算
我必须计算给定合同的保险费。
合约配置为
- productCode(来自字典的代码)
- contractCode(来自字典的代码)
- 客户的个人资料(例如年龄、地址)
- 保险金额 (SI)
- 等等
目前,溢价是使用这个公式计算的:
premium := SI * px * (1 + py) / pz
其中:
- px 是 factor 在 Excel 文件中参数化并且 depends on 2属性(客户的年龄和性别)
- py 是 factor 在 excel 文件中参数化并且 depends on 4合约的属性
- pz - 同样
要求
- R1 – java 代码不知道 公式,
- R2 - java 代码对 公式依赖关系一无所知 ,换句话说,premium 依赖于:px, py , pz,
- R3 - java 代码对 参数的依赖关系一无所知,我的意思是 px 取决于客户的年龄和性别, 依此类推。
实施 R1、R2 和 R3 后,我有 java 代码与业务逻辑分离,任何业务分析师 (BA) 都可以修改公式并添加新的依赖项而无需重新部署。
到目前为止我的解决方案
我有合同领域模型,它由classes合同、产品、客户、政策等组成。合约 class 定义为:
public class Contract {
String code; // contractCode
double sumInsured; // SI
String clientSex; // M, F
int professionCode; // code from dictionary
int policyYear; // 1..5
int clientAge; //
... // etc.
另外我介绍了 Var class 它是 any 参数化变量的容器:
public class Var {
public final String name;
public final ContractPremiumRequest request;
private double value; // calculated value
private boolean ready; // true if value is calculated
public Var(String name, ContractPremiumRequest request) {
this.name = name;
this.request = request;
}
...
public void setReady(boolean ready) {
this.ready = ready;
request.check();
}
...
// getters, setters
}
最后 - 请求 class:
public class ContractPremiumRequest {
public static enum State {
INIT,
IN_PROGRESS,
READY
}
public final Contract contract;
private State state = State.INIT;
// all dependencies (parameterized factors, e.g. px, py, ...)
private Map<String, Var> varMap = new TreeMap<>();
// calculated response - premium value
private BigDecimal value;
public ContractPremiumRequest(Contract contract) {
this.contract = contract;
}
// true if *all* vars are ready
private boolean _isReady() {
for (Var var : varMap.values()) {
if (!var.isReady()) {
return false;
}
}
return true;
}
// check if should modify state
public void check() {
if (_isReady()) {
setState(State.READY);
}
}
// read number from var with given [name]
public double getVar(String name) {
return varMap.get(name).getValue();
}
// adding uncalculated factor to this request – makes request IN_PROGRESS
public Var addVar(String name) {
Var var = new Var(name, this);
varMap.put(name, var);
setState(State.IN_PROGRESS);
return var;
}
...
// getters, setters
}
现在我可以使用这些 classes 和这样的流程:
request = new ContractPremiumRequest(contract)
- 使用
state == INIT
创建请求
- 使用
px = request.addVar( "px" )
- 用
ready == false
创建 - 将请求移至
state == IN_PROGRESS
Var("px")
- 用
py = request.addVar( "py" )
px.setValue( factor )
,px.setReady( true )
- 在
px
上设置计算值
- 使
ready == true
- 在
request.check()
如果 ALL 变量准备就绪,则state == READY
- 现在我们可以使用公式,因为请求已计算出所有依赖关系
我已经创建了 2 DRL 规则 并准备了 3 决定 tables (px.xls, py.xls, ...) 由 BA 提供的因素。
规则 1 - contract_premium_prepare.drl:
rule "contract premium request - prepare dependencies"
when
$req : ContractPremiumRequest (state == ContractPremiumRequest.State.INIT)
then
insert( $req.addVar("px") );
insert( $req.addVar("py") );
insert( $req.addVar("pz") );
$req.setState(ContractPremiumRequest.State.IN_PROGRESS);
end
规则 2 - contract_premium_calculate.drl:
rule "contract premium request - calculate premium"
when
$req : ContractPremiumRequest (state == ContractPremiumRequest.State.READY)
then
double px = $req.getVar("px");
double py = $req.getVar("py");
double pz = $req.getVar("pz");
double si = $req.contract.getSumInsured();
// use formula to calculate premium
double premium = si * px * (1 + py) / pz;
// round to 2 digits
$req.setValue(premium);
end
决定tablepx.xls:
决定tablepy.xls:
KieContainer 在启动时构建一次:
dtconf = KnowledgeBuilderFactory.newDecisionTableConfiguration();
dtconf.setInputType(DecisionTableInputType.XLS);
KieServices ks = KieServices.Factory.get();
KieContainer kc = ks.getKieClasspathContainer();
现在计算给定合约的权利金,我们写:
ContractPremiumRequest request = new ContractPremiumRequest(contract); // state == INIT
kc.newStatelessKieSession("session-rules").execute(request);
BigDecimal premium = request.getValue();
事情是这样的:
- 规则 1 针对
ContractPremiumRequest[INIT]
触发
- 此规则创建并添加 px、py 和 pz 依赖项(
Var
个对象) - 适当的excel行为每个px、py、pz对象触发并且使其准备就绪
- Rule2 触发
ContractPremiumRequest[READY]
并使用公式
数量
- PX 决定 table 有 ~100 行,
- PY 决定 table 有 ~8000 行,
- PZ 决定 table 有 ~50 行。
我的成绩
第一次计算,加载和初始化决策 tables 需要 ~45 秒 – 这可能会出现问题。
每次计算(经过一些预热后)需要 ~0.8 毫秒——这对我们的团队来说是可接受的table。
堆消耗为 ~150 MB – 这是有问题的,因为我们预计将使用更多大 tables。
问题
- 我使用 Drools 的方式不对吗?
- 如何以更高效的方式满足要求?
- 如何优化内存使用?
========== 编辑(2 年后)==========
这是 2 年后的简短总结。
正如我们预期的那样,我们的系统发展得非常快。我们以超过 500 tables(或矩阵)结束,其中包含保险定价、精算因素、保险配置等。 一些 table 的大小超过 100 万行。我们使用了 drools 但我们无法处理性能问题。
终于用上了Hyperon引擎(http://hyperon.io)
这个系统是一个野兽 - 它允许我们在大约 10 毫秒的总时间内 运行 数百个规则匹配。
我们甚至能够对 UI 字段上的每个 KeyType 事件触发完整的策略重新计算。
据我们了解,Hyperon 为每个规则使用快速内存索引 table 并且这些索引以某种方式被压缩,因此它们几乎不占用内存。
我们现在还有一个好处 - 所有定价、因素、配置 table 都可以在线修改(值和结构),这对 java 代码是完全透明的。 应用程序只是继续使用新逻辑,不需要开发或重新启动。
但是我们需要一些时间和精力来充分了解 Hyperon :)
我找到了我们团队一年前做的一些比较 - 它显示了引擎初始化 (drools/hyperon) 和从 jvisualVM 角度来看的 100k 简单计算:
仔细阅读问题后,我会提供一些建议:
与 Excel 电子表格相比,我更喜欢关系数据库。
这些都是非常简单的计算。我认为这个模型有点矫枉过正。对于这种规模的问题,规则引擎似乎太大了。
我会更简单地编写代码。
使计算基于接口,以便您可以通过注入新的 class 实现来修改它。
了解如何编写 Junit 测试。
我的第一选择是简单的决策table计算,没有规则引擎,维护关系数据库中的因素。
Rete 规则引擎是 if/else 或 switch 语句的重锤。除非您利用归纳功能,否则我认为这太过分了。
我不会在会话中添加任何内容。我正在设想一个幂等的 REST 服务,它接受一个请求和 returns 一个带有溢价的响应以及任何其他必须返回的响应。
在我看来,您过早地使解决方案过于复杂了。做可能有效的最简单的事情;衡量绩效;根据您获得的数据和要求根据需要进行重构。
您的开发经验如何?你是一个人还是团队的一员?这是你没做过的新系统吗?
问题是您为相对少量的数据创建了大量代码(所有规则都来自表格)。我见过类似的案例,他们都受益于将表作为数据插入。 PxRow、PyRow 和 PzRow 应该这样定义:
class PxRow {
private String gender;
private int age;
private double px;
// Constructor (3 params) and getters
}
数据仍然可以在(更简单的)电子表格中或任何您喜欢的 BA 研究人员输入的数据中。您将所有行作为事实 PxRow、PyRow、PzRow 插入。那么你需要一两个规则:
rule calculate
when
$c: Contract( $cs: clientSex, $ca: clientAge,
$pc: professionCode, $py: policyYear,...
...
$si: sumInsured )
PxRow( gender == $cs, age == $ca, $px: px )
PyRow( profCode == $pc, polYear == $py,... $py: py )
PzRow( ... $pz: pz )
then
double premium = $si * $px * (1 + $py) / $pz;
// round to 2 digits
modify( $c ){ setPremium( premium ) }
end
忘记流量和所有其他装饰。但是你可能需要另一个规则,以防你的合约与 Px 或 Py 或 Pz 不匹配:
rule "no match"
salience -100
when
$c: Contract( premium == null ) # or 0.00
then
// diagnostic
end