决策表的 Drools 性能

prz*_*tel 6 java drools

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

我在我的项目中使用 Drools 将业务逻辑与 Java 代码分开,我决定也将它用于高级计算。

  • 我是否以错误的方式使用 Drools?
  • 如何以更高效的方式满足要求?

详情如下:


计算

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

合约配置为

  • productCode(字典中的代码)
  • contractCode(字典中的代码)
  • 客户的个人资料(例如年龄、地址)
  • 保险金额 (SI)
  • 等等。

目前,使用以下公式计算保费

premium := SI * px * (1 + py) / pz
Run Code Online (Sandbox Code Playgroud)

在哪里:

  • PX因素参数在Excel文件,并依赖于两个属性(客户的年龄和性别)
  • PY因素参数在Excel文件,并依赖于4合约的性质
  • pz - 同样

要求

  • R1 – java 代码不知道公式
  • R2 - java 代码对公式依赖一无所知,换句话说,溢价取决于:px、py、pz、
  • R3 - java 代码对参数的依赖一无所知,我的意思是 px 取决于客户的年龄和性别,等等。

实现 R1、R2 和 R3 后,我将 Java 代码与业务逻辑分离,任何业务分析师 (BA) 都可以修改公式并添加新的依赖项,而无需重新部署。


我的解决方案,到目前为止

我有合同域模型,它由类 Contract、Product、Client、Policy 等组成。合约类定义为:

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.
Run Code Online (Sandbox Code Playgroud)

此外,我引入了Var类,它是任何参数化变量的容器:

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
}
Run Code Online (Sandbox Code Playgroud)

最后 -请求类:

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
}
Run Code Online (Sandbox Code Playgroud)

现在我可以在这样的流程中使用这些类:

  1. request = new ContractPremiumRequest(contract)
    • 创建请求 state == INIT
  2. px = request.addVar( "px" )
    • 创造Var("px")ready == false
    • 将请求移至 state == IN_PROGRESS
  3. py = request.addVar( "py" )
  4. px.setValue( factor ), px.setReady( true )
    • 设置计算值 px
    • 使它 ready == true
  5. request.check()使得state == READY如果所有瓦尔准备
  6. 现在我们可以使用公式,因为请求已经计算了所有依赖项

我已经创建了2 个 DRL 规则并准备了3 个决策表(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
Run Code Online (Sandbox Code Playgroud)

规则 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
Run Code Online (Sandbox Code Playgroud)

决策表 px.xls:

来自 px.xls 决策表的片段

决策表py.xls:

来自 px.xls 决策表的片段

KieContainer 在启动时构造一次:

dtconf = KnowledgeBuilderFactory.newDecisionTableConfiguration();
dtconf.setInputType(DecisionTableInputType.XLS);
KieServices ks = KieServices.Factory.get();
KieContainer kc = ks.getKieClasspathContainer();
Run Code Online (Sandbox Code Playgroud)

现在计算给定合约的溢价,我们写:

ContractPremiumRequest request = new ContractPremiumRequest(contract);  // state == INIT
kc.newStatelessKieSession("session-rules").execute(request);
BigDecimal premium = request.getValue();
Run Code Online (Sandbox Code Playgroud)

这是发生的事情:

  • Rule1ContractPremiumRequest[INIT]
  • 此规则创建并添加 px、py 和 pz 依赖项(Var对象)
  • 为每个 px、py、pz 对象触发适当的 excel 行并使其准备就绪
  • Rule2触发ContractPremiumRequest[READY]并使用公式

  • PX 决策表有~100行,
  • PY 决策表有~8000行,
  • PZ 决策表有~50行。

我的结果

  • 第一次计算,加载和初始化决策表需要大约 45 秒- 这可能会成为问题。

  • 每次计算(在一些热身之后)大约需要0.8 毫秒——这对我们的团队来说是可以接受的。

  • 堆消耗约为 150 MB——这是有问题的,因为我们预计将使用更多的大表。


  • 我是否以错误的方式使用 Drools?
  • 如何以更高效的方式满足要求?
  • 如何优化内存使用?

       

========== 编辑(2 年后)==========

这是两年后的简短总结。

正如我们预期的那样,我们的系统已经发展得非常快。我们已经结束了 500 多个表(或矩阵),其中包含保险定价、精算因素、覆盖配置等。一些表的大小超过 100 万行。我们使用了drools,但我们无法处理性能问题。

最后我们使用了Hyperon引擎 ( http://hyperon.io )

这个系统是一个野兽——它允许我们在大约 10 毫秒的总时间内运行数百个规则匹配。

我们甚至能够在 UI 字段上的每个 KeyType 事件上触发完整的策略重新计算。

正如我们所了解的,Hyperon 为每个规则表使用快速内存​​索引,并且这些索引以某种方式被压缩,因此它们几乎不占用内存。

我们现在还有一个好处——所有定价、因素、配置表都可以在线修改(值和结构),这对 Java 代码是完全透明的。应用程序只是继续以新逻辑工作,不需要开发或重启。

然而,我们需要一些时间和精力来充分了解 Hyperon :)

我发现我们团队一年前做了一些比较——它从 jvisualVM 的角度显示了引擎初始化(drools/hyperon)和 100k 的简单计算:

流口水的策略计算 使用超子的策略计算

lau*_*une 1

问题在于您为相对少量的数据创建了大量代码(所有规则均来自表)。我见过类似的案例,它们都受益于将表作为数据插入。PxRow、PyRow 和 PzRow 应该这样定义:

class PxRow { 
    private String gender;
    private int age;
    private double px;
    // Constructor (3 params) and getters
} 
Run Code Online (Sandbox Code Playgroud)

数据仍然可以位于(更简单的)电子表格或任何您喜欢由 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
Run Code Online (Sandbox Code Playgroud)

忘记流程和所有其他装饰。但您可能需要另一个规则,以防万一您的合同与 Px 或 Py 或 Pz 不匹配:

rule "no match"
salience -100
when
    $c: Contract( premium == null ) # or 0.00
then
    // diagnostic
end
Run Code Online (Sandbox Code Playgroud)