决策表的 Drools 性能

Drools performance for decision tables

当我尝试使用 Drools 引擎计算保险费时,我遇到了潜在的 performance/memory 瓶颈。

我在我的项目中使用 Drools 将业务逻辑与 java 代码分开,我决定也将其用于保费计算。

详情如下:


计算

我必须计算给定合同的保险费。

合约配置为

目前,溢价是使用这个公式计算的:

premium := SI * px * (1 + py) / pz

其中:


要求

实施 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 和这样的流程:

  1. request = new ContractPremiumRequest(contract)
    • 使用 state == INIT
    • 创建请求
  2. px = request.addVar( "px" )
    • ready == false
    • 创建 Var("px")
    • 将请求移至 state == IN_PROGRESS
  3. py = request.addVar( "py" )
  4. px.setValue( factor ),px.setReady( true )
    • px
    • 上设置计算值
    • 使ready == true
  5. request.check() 如果 ALL 变量准备就绪,则 state == READY
  6. 现在我们可以使用公式,因为请求已计算出所有依赖关系

我已经创建了 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();

事情是这样的:


数量


我的成绩


问题

========== 编辑(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