动手撸一个规则引擎(三):规则引擎后记

2019-09-17 15:11:26 浏览数 (1)

写在前面

之前两篇文章是去年调研和自研规则引擎的存货,今天是最后一篇,后记。

有人会问,标题不是写的动手撸吗?哪里体现撸了?

其实撸起来一个引擎并不复杂,为了体现架构思想,调研心得和设计思想反而更重要,相信优秀如你写代码没有任何压力的。

那我就和大家聊聊业务背景和引擎要求。

设计思想

场景

比如[券表],对于字段属性有一定的规则要求,比如券的互斥属性需要做一定的校验,比如change-config是个json,需要进行解析之后和detail信息做规则校验,等其他的一些规则。梳理出来的需要主要设计到字段属性的处理,而没有涉及到复杂的流程,数据问题的处理。

核心

  • 定义规则;
  • 确定规则边界;

规则

  • 字段规则,涉及到字段长度,某些信息(地理围栏信息)需要逆向校验是否准确。
  • 流程规则,需要根据不同参数规则进行不同分支流程。
  • 变更频繁,某些业务场景存在每个月规则变化的需求。

举例

  • 字段规则,比如实体字段长度,地理围栏信息。
  • 流程规则,不同来源数据进行不同的规则校验。

校验

  • 规则:业务实体信息校验,采用字段校验规则。
  • 校验:需要配置对应字段的规则,比如名称字段长度,地址位置和经纬度是否一致。

方案调研

硬编码:适用于规则不易变场景。

优点

  • 逻辑简单,易于理解,开发效率高,编码可以由编译器保证。

缺点

  • 迭代成本高可维护性差,规则变更需要发版,上线周期较长,如果代码繁杂需要原RD介入。

Drools:开源规则引擎

流程:业务分析师编写业务需求文档,开发工程师根据规则进行DSL规则编写,DSL规则入库,Drools引擎根据规则库规则进行解析,动态执行规则。

优点

  • 可以解耦规则建立和规则执行,便于维护。

缺点

  • 需要业务分析师和开发工程师协同工作,缺一不可,存在人效浪费问题。

规则复杂之后,依然存在不好维护问题,某种程度上甚至比硬编码糟糕。

过多的if,else,when,then不利于维护。

基于Spark数据处理规则引擎

如果场景涉及大部分规则是数据处理,则可以认为此场景规则处理等于数据处理。为商业分析师提供友好可视化规则界面。规则引擎将配置信息解析为Spark作业进行计算。

优点

  • 规则配置简单,易上手,支持热部署。

缺点

  • 使用范围局限于数据场景的规则,不能覆盖更大业务场景。

自研规则引擎

基本假设

n个规则输入,一个规则结果输出;规则支持基本的逻辑运算,算数运算,关系运算,属性判断等;多个原子语义规则之间可聚合,可复用,可拆分;

性能要求

  • 高吞吐;
  • 低延迟;
  • 采用本地缓存加速;
  • 减少远程进程调用开销;
  • 提高系统并行度;
  • 代码采用编译后或解析后执行;

可用性要求

规则引擎可降级,不影响主流程;

系统设计要求

  • 代码侵入低,引jar包,采用注解,AOP,threadlocal等方式;
  • 支持热部署;
  • 支持规则版本回滚,支持规则灰度发布;

功能要求

  • 降低业务分析师使用门槛;
  • 提供配置中心;
  • 提供规则预警;
  • 结果分析报表展示;
  • 规则执行过程中有监控,有规则报表分析结果;

设计方案

  • 验证流程
  • 通过aop拦截需要规则验证的注解方法;
  • 进行方法入参获取,获取参数类名;
  • 如果参数类名和内存中标记的注解Domain名称相同,则进行规则验证;
  • 根据类属性进行对应属性规则获取;
  • 进行属性规则验证;
  • 验证不通过则提示对应配置提示信息,并返回;

技术点

  • 注解,反射,AOP,ThreadLocal,Kafka;
  • 规则表达式
  • aviator

难点

  • 规则下发方案,基于RPC推送/基于MQ订阅/基于Zookeeper的Watcher;
  • 规则编写的覆盖能力,不同规则对应的不同友好提示;

规则解析

示例

赋值

代码语言:javascript复制
coupon.setId(100L);
coupon.setAcctId(5L);
coupon.setRemark("备注备注备注备注备注1");
coupon.setType((byte) 5);
coupon.setDetail("{"an":"","charge_detail":[{"charge_amount":100000,"charge_side":"1","order_no":1,"setAn":false,"setCharge_amount":true,"setCharge_side":true,"setOrder_no":true},{"an":"17D03004416N001S0009T187R00493A0080$C1CB4","charge_amount":20000,"charge_side":"14010","order_no":2,"setAn":true,"setCharge_amount":true,"setCharge_side":true,"setOrder_no":true}],"charge_detailIterator":[{"$ref":"$.charge_detail[0]"},{"$ref":"$.charge_detail[1]"}],"charge_detailSize":2,"charge_method":0,"charge_side":"1","charge_type":1,"default_charge_side":"1","default_side_an":"","setAn":true,"setCharge_detail":true,"setCharge_method":true,"setCharge_side":true,"setCharge_type":true,"setDefault_charge_side":true,"setDefault_side_an":true}");
coupon.setCount(120000L);

规则

代码语言:javascript复制
rule.setKey("id");
rule.setRule("id > 10");
rule.setErrMessage("id 必须大于 10");
rule.setKey("type");
rule.setRule("acctId == 5 && type == 5");
rule.setErrMessage("acctId = 5 type 必须为5");
rule.setKey("remark");
rule.setRule("string.length(' remark ') > 10"); // 注意表达式都有空格
rule.setErrMessage("remark长度不能小于10");
rule.setKey("detail");
rule.setRule("(<json>detail.charge_detail.(0).charge_amount</json>   <json>detail.charge_detail.(1).charge_amount</json>) == <json>count</json>");
rule.setRuleTypeEnum(CompassRuleTypeEnum.JSON);
rule.setErrMessage("charge_amount值不相等");

Json

代码语言:javascript复制
{
    "an":"",
    "charge_detail":[
        {
            "charge_amount":100000,
            "charge_side":"1",
            "order_no":1,
            "setAn":false,
            "setCharge_amount":true,
            "setCharge_side":true,
            "setOrder_no":true
        },
        {
            "an":"17D03004416N001S0009T187R00493A0080$C1CB4",
            "charge_amount":20000,
            "charge_side":"14010",
            "order_no":2,
            "setAn":true,
            "setCharge_amount":true,
            "setCharge_side":true,
            "setOrder_no":true
        }
    ],
    "charge_detailIterator":[
        {
            "$ref":"$.charge_detail[0]"
        },
        {
            "$ref":"$.charge_detail[1]"
        }
    ],
    "charge_detailSize":2,
    "charge_method":0,
    "charge_side":"1",
    "charge_type":1,
    "default_charge_side":"1",
    "default_side_an":"",
    "setAn":true,
    "setCharge_detail":true,
    "setCharge_method":true,
    "setCharge_side":true,
    "setCharge_type":true,
    "setDefault_charge_side":true,
    "setDefault_side_an":true
}

收获

方案和核心代码实现起来比较简单,但是一个非业务相关项目可能面对着两个问题,做大和做小。

做小

指的是只满足当前业务场景和需求,这样可最快的实现需求,但后续如果有类似的需求不能满足或不易定制,这样最开始引入这个项目的目的全无。

做大

指的是如果做成一个业务和技术上都可用的项目,达到一个平台的效果,则需要在研发投入更多的时间。

包括代码细节,技术方案,UI界面等,后续在系统稳定性方案也需要投入一些时间,这样做一个非业务相关性的东西投入这么大是否值得,比如需要大概投入了3,4个人力,完整周期持续了小2个月。

规则引擎有多个场景:风控场景,业务场景。

风控场景

属于风控产品线产品,整体上功能比较完备,但是对于一般场景显得重一些,引入了场景,规则,规则因子,appkey,单元测试等很多新的概念,整体上比较重。

业务场景

业务场景比较轻量级一些,对于我们的场景支持的还可以,引入上对于代码入侵可以接受。

决定采用这种低侵入方案进行对接。

写在最后

调研收获

进行调研之前,自己对于业务场景对于规则引擎的需求进行了一定的设计和代码开发,在考虑上存在一些问题。

通用的方案

调研了一圈,大家大家在实现细节上都很类似,比如基于Aviator的表达式,通过Zk,MQ,DataBus的规则下发,规则放到内存中不存在跨进程调用。

离线方案基于Hive进行分析。

完全避免代码侵入 我自己设计的方案上想对代码做到尽量少的代码入侵,甚至零侵入,比如通过AOP的方式实现零侵入。

但经过深度思考后,发现完全的零侵入会限制规则在程序中的能力,调研了几个方案之后,发现所有的方案都存在代码侵入。

得到的收获是,不要为了某种洁癖达到零侵入,适当的代码侵入更有助于规则引擎的表达,只要做好侵入更友好就可以。

规则沉淀

通过规则管理平台配置和修改规则,基于MySql存储。规则配置采用多租户管理,便于不同团队进行操作。

日志存储

  • es hbase,实时日志
  • hive,离线日志
  • druid,聚合日志

监控告警

对于使用流程进行埋点监控,可视化报表查看。

接入方式

  • sdk
  • mq
  • rpc

核心功能

  • 异步规则引擎对接入规则进行并行异步处理。
  • 日志操作落盘采集,或直接nio上报。
  • 日志回收采集/分析,日志规则回放,便于redu操作。
  • 建立规则分析平台,发现热点优化规则。
  • 扩展因子。
  • 服务注册等。

0 人点赞