风控系统是雪球社区风控的壁垒,高效稳定运行关系到社区内容安全合法合规的底线,所以系统在应变能力上要有更高的要求,随着业务发展和外部环境变化,原有系统不能很好的提供支持甚至造成“敏捷”障碍,如面对水军、黑产、羊毛党等风险时,系统的处置能力滞后,业务与研发需要紧急制定应对这些临时突发事件的处理措施。规则没有统一的分类整理,风险识别与处置逻辑都是以硬编码的方式混杂在一起,scala与java混杂,长期的累进式代码迭代,开发维护测试成本很高。没有统一的日志和快照数据输出,不支持离线分析。定位问题效率低下,线上排查问题使用远程debug等一系列痛点。原系统在数据、系统、工具层面相对薄弱的能力已经跟不上风控需要。
对业务和原系统了解加深后 ,开始梳理原系统内外部的关联关系和其内部规则实现逻辑,规划新系统要达到的目标,其应该有应对需求快速变化的响应能力,降低业务人员学习门槛、缩短产研同学介入周期的能力,安全调控规则与策略的能力,全面的数据回溯能力等,鉴于此开启了规则引擎的建设之路。
什么是规则引擎?风控系统中的核心是规则引擎,我们理解的规则引擎是现有业务场景下数据驱动的内容风险决策工具,使用可视化的DSL模块编写决策策略,把繁杂的决策逻辑从对业务人员不友好的程序代码中剥离,并在策略调整的时效性、准确性、安全性上具备保障能力,且能够支持具有学习能力的算法模型接入,为此我们总结规则引擎应具有的特性如下:
什么是规则?规则是由一系列特征和具有动作触发能力的策略构成的职能组件。什么是引擎?系统内组织了众多规则,引擎将他们纳入自身的管理体系中,承担起调度的职责,根据调度规则分配和协调资源之间依赖性,在什么情况下做出什么样的反应。下图就是简化后的引擎驱动模型,预设的规则和外部特征输入通过规则引擎的推理,按照编排信息触发规则,最终引擎将策略的结果输出。
下图是系统建设的框架图,在编码之前我们已经做了很多准备工作,对原系统规则进行分析,有拆分的安全性验证、有判断条件的准确性核对、有对外接口依赖情况的梳理,能否本地替换或做成标准的接口特征等方面做的调查,这些工作让我们更加清晰理解原系统,给我们对建设规则引擎可行性上增强信心,也预留规则引擎的设计时间,提供从原系统平滑过渡和迁移的依据,后续就是开始了自下而上好几个阶段的系统建设,对于每类系统组件的职能、适用范围,接下来开始分别介绍。
特征为系统运转提供数据来源,作为策略执行的输入(fact对象),比如用户数据对象,帖子数据对象,模型输出,也会有对输入进行加工形成的新特征。在底层建设一段时间后开始对原系统规则进行梳理和拆解,其被拆分成特征、条件和动作三部分,动作与特征拆解时首先遇到的问题就是Scala语法差异性,在“翻译”成Java后要保证结果的一致性、准确性,为此编写了大量的测试用例来覆盖各种场景,对于无数据写入能力的特征,从生产环境引流灌入到引擎,分析其性能和准确性。
特征集是一系列特征集合。对于一些复杂场景的策略,触发时需要不同的数据源,规则调度器会将其所关联的所有特征数据填充至流程上下文中。针对特征集内会有多个特征的情况做了串/并行执行模式的适配,对执行时序方面有要求的场景使用串行模式,高优先级的特征会优先被加载,为了缩短服务的响应时间和提高系统性能及吞吐,多个无依赖关系的特征我们通常会采用并行模式去触发。
策略在系统定义上包含两部分,即条件(LHS)和动作(RHS),这也是建设规则引擎的理论和构建基础,条件属于动态变化的部分,用于策略逻辑判定,当逻辑输出为true时才会触发动作。动作具有单一职能特性属于固化的部分,动态变化的策略条件会抽象出来交由业务人员管理。
策略集即一系列相同使用场景下策略的集合,策略集也具有串/并行执行模式,在串行模式上引入了中断概念,支持集合内某个策略条件输出为true后不会再触发其他策略,并行模式下有所不同,所有策略都会触发,待所有策略执行完成后引擎自动合并结果。
策略条件即逻辑表达式,在过往的项目中我们有使用mvel2的经验,它的特性完全符合我们的需求,涵盖了众多的算术、逻辑运算符,还有列表、map等复杂数据类型的支持,在编译模式下有着不错的性能,另外支持static method方式扩展函数可以满足95%以上的需求,比如可以增加求和,字符串截取,日期转换等函数。
动作,策略条件干预下触发的行为,也是输出执行结果的组件,在定制动作时通常将其应用范围收缩到最小,尽可能做到职能单一,通过多个动作组合的形式实现复杂的逻辑,如当用户发布的内容被模型检测出来涉黄,将触发删帖与用户降权动作。以下是一段策略的伪码实现。
/** 策略1.1 */
rule "1.1"
if
${date}>"2021-07-02" && funA(funB(${text})) && ${type}==1 //策略条件
then
action( "1.1 matched" ); //动作
action( "1.2 matched" ); //动作
end
/** 策略1.2 */
rule "1.2"
if
${date}>"2021-07-01" && ${type}==2 && ${list}.contain("rule")
then
action( "1.3 matched" ); //动作
end
/** 策略2.1 */
rule "2.1"
if
${date}>"2021-07-03" && ${type}==4
then
action( "2.1 matched" ); //动作
end
一系列特征的集合与具有相同输入数据源要求的策略集共同组成的最小检测单元,原系统中的规则在概念上被拆解扩展,赋予了更多的职能,它能组织和调度特征集进行数据提取,特征数据加载完成后转移调度控制权交由策略集调度器,在拿到策略结果后收回调度权转移给下一个规则。我们利用流程上下文的传递特性赋予规则空挂载的特性增加其不同场景下的适应能力,前置特征数据已经存在的情况下则可以只挂载策略,当只挂载特征集时相当于为后续规则制备数据。
流程是服务于特定场景下的规则集合通过编排形成的规则流,在发布平台内支持流程内规则增删、顺序调整,策略调整等操作,这些都可以让业务在研发不用参与的情况下由业务自行干预,干预的实时性从原系统以天为单位缩短到分钟级别。如下所示我们其中一个场景下串联编排成的流程。流程化的好处是规则引擎不再局限在内容管理场景,通过组合编排有需要风险决策的场景都可以接进来。发布平台内构建的结构化知识库,流程内各组件使用情况一目了然,依赖关系清晰明了。
上述内容对规则引擎内各个基础组件的作用与承担的角色做了描述,组件的运转和驱动是依赖调度引擎完成的,相对独立化的组件设计给未来扩展系统提供便利,解除强耦合性后,一些辅助控制器可以轻松集成到引擎内。下图是某个场景下引擎工作时序示意图。
流程在系统中有版本的概念,通过复制已发布的流程创建新版本,基于版本管理赋予了流程调度器使用分流和并行模式的实验能力,另外在处置各类风险事件的过程中,也可基于版本定制出应对不同风险场景的流程实现自由切换,做到面对黑产、水军分别一个版本。在流程执行过程中引入上下文,上下文内的特征数据和策略结果会在流程中由前到后单向传递,这种方式的好处在于前面已经存在特征后续规则可以复用,缩短流程的执行时间。
这种模式下流程调度器路由按预设的比例随机从多个版本中选择一个调度,同一时刻只有一个版本被触发。这在一些需要参数微调的场景下比较有用,通过缩放比例实验各版本的效果。
此模式下,通常是流程版本差异过大,需要大量的数据进行验证。流程调度器的路由会同时触发所有的版本,每个版本的流程都有独立的上下文,与分流模式的差异在于只有已发布状态的版本流程中的动作会被触发,其他版本内的动作永远不会被执行。
在实际操作中业务人员通常克隆之前的版本来创建流程缩短配置时间,在克隆好的流程上进行规则上下线、策略调整,当验证通过后才上线指定的版本并同时将旧版本下线,解决实时调整流程(策略)的周期过长的问题,同时提高了流程(策略)上线的安全性,自此流程管理可以不用研发参与。
从上面引擎调度时序图我们看到,它负责特征集内特征的调度,当然规则也可以不需要特征,前面我们说到引入了流程上下文,规则调度器会将特征集内所有特征数据挂载到流程上下文中,待特征数据加载完成后向策略集调度器转移控制权,也会将特征数据一并移交,与策略集类似在流程中也引入中断机制,流程调度器会依据规则合并的策略结果决定是否中断流程或继续后续规则,可以应用到某些场景下命中一个规则后续规则无需再被触发的需求。
策略集主要是管理其内部的策略以及版本,有着和流程一样分流与并行执行模式,是解决策略安全变更和上下线的第二个途径,其作用就是提供多版本实验能力并向策略调度器转移控制权并移交特征数据。在引擎内有个约定就是只能在上下文内追加特征数据和策略结果,不可有更新和删除行为,防止数据污染和失效,在编写特征和动作时会依据此约定。
策略调度器职能是“翻译”前文伪码表达的策略,即执行策略条件并根据条件结果决定是否触发动作,动作结果是流程调度器决定是否执行后续规则的结果来源。前面也说到表达式引擎是用mvel2作为动态/静态可嵌入的表达式解析器,mvel2引擎功能强大,覆盖了很多应用场景的表达式需求。为了保证策略执行的安全性,我们又增加了策略预上线控制,处于此状态的策略不会触发动作,至此可以看到在引擎处理过程中提供了多种策略安全上下线途径。
每一种调度器都预留扩展位置,这其中就包含了资源同步器,发布平台内的同步器会推送组件配置变更事件至规则引擎,同步器会在调度器初始化和接收到变更事件时,启动更新动作。
原风控系统使用单独的子系统对接模型,每次新增模型都得重新开发接口去接入,模型迭代意味着接入工作会重复。梳理后我们发现与算法模型对接的接口所需参数都在引擎内,于是提出模型特征一体化方案,标准化模型特征输入输出,统一后的模型输出是一个Map,其输出key可以在发布平台上设置,mvel2脚本引擎根据这些key从上下文内获取模型的输出,这一系列改动后,基于模型的规则与原系统规则整合在一个流程上,分散的模型识别数据聚合在一个平台内,接口接入被平台的能力接管,将研发从繁琐的接口对接工作中解放出来,业务人员熟悉后也可自主完成模型对接。在配置上模型特征生成后其配置更新过程和其他组是一致的,都是由资源同步器完成。
策略编辑器面向的是业务人员,他们平常会接触sql和一些简单的逻辑表达式,在设计编辑器时站在了使用者角度上,目的就是降低他们使用门槛,为此我们迭代了两个版,从最初只能编辑简单的一维表达式向拥有任意复杂表达式编辑能力转变。在编辑器内所有关联组件都会被罗列在操作面板上,并使用不同的颜色以示区分,常用的基本运算符是引擎默认支持的,使用者只需要通过单击、双击、拖拽即可完成任意表达式的创建,这种可视化DSL对业务人员十分友好,稍加培训就能上手。如下图创建一个简单的策略表达式。
策略编辑器Schema文件被引擎解释后交由前端完成表达式渲染,以及内置逻辑的初始化,也可以被解释成引擎能识别的逻辑表达式。Schema的定义成了编辑器的重点,Schema定义好后引擎开发完毕,就很难重新设计Schema,甚至少许结构上的调整都是不允许的,逻辑上要准确自然,它的设计将直接影响引擎的性能或者直接影响引擎的实现方式。在开始设计时参考了antlr,想用它解释翻译定义好的可视化DSL与标准Schema,在实践过程中发现改造麻烦,影响项目周期。
后来重新设计以json这种宽松的文档结构为载体,实现了一套可视化DSL与标准Schema文档转换引擎,解决前端展示以及后端解释、编译问题,同时json扩展性强,在引擎翻译和解释时严格既定的字段属性解释,新增或者变化的属性名不予处理。性能和安全我们也会考虑的方面,schema解释后形成的逻辑表达式会在保存时进行基本语法上的校验,最后提交到规则引擎的虚拟环境进行验证,由表达式引擎负责编译,异常会给出提醒。当新增的预上线策略挂载到流程中后,业务人员会根据运行数据跟踪其运行情况,系统层面也有策略监控或者异常报警,再者预上线的策略不会触发动作,对生产系统上的业务不会有影响。
用户层面的交互上,前端通过可视化DSL解决了渲染问题,业务人员调整策略后DSL随之发生变化,变更后的DSL会重新进入到DSL到Schema,Schema到引擎内策略字节码的变化流程中,对使用者无感知,将在分析实时数据时体现变化结果。下图即是编辑器到引擎表达式的转换过程。
函数解决复杂场景中的逻辑计算问题,比如日期格式转换等,如果要在表达式内实现这些复杂计算会使DSL设计难度增大,生成的逻辑表达式会包含大量冗长不便阅读的代码,这违背了我们的设计初衷,也给业务人员增加了编写难度。基于mvel2提供的static method扩展能力,我们搜集业务常用的小功能和函数,在系统层面就集成进来,另外我们增加了一项应急的脚本函数功能,解决了mvel2内置的脚本函数不支持创建同名函数的问题,实现了对脚本函数的在线修改、发布与测试。最重要的一点就是我们自定义的schema实现了编辑器内函数嵌套,更加复杂的逻辑计算不再是问题。
常量是另类的变量或者说是特征,对参数进行固化后形成的标准“特征”,常用的数据类型都会被覆盖到,如数值类型、字符串、列表(list)、Map等,引擎初始化时它们被自动加载,当策略被触发时脚本引擎会从常量池内取值。它解决了不同人员在编辑表达式时对一些固化的配置重复定义、变更时需要修改多处的问题。
每天社区都会生产出大量内容,快速甄别出违规内容是首要目标,首先是迁移原系统识别风险的35个有效规则,以下是原系统的一个风控串行流程示意图,串行模式下规则未命中时下一个规则开始执行,这样的过程会一直重复下去,这种模式最大的好处是结构简单,有规则命中时终止流程运行,不足就是无任何策略命中时执行过程非常的长。接下来就是依据引擎特性挖掘潜力尽可能去平衡,为此会从如下方向上进行适配优化:
引擎在设计时就考虑了特征重复使用的需求,像用户信息、用户发帖内容详情这类数据对象会优先挂载到流程中,后续策略改从上下文内获取这些数据,另外特征数据对象获取方式改并行模式,这两项优化下来不再有重复的特征,生成的数据对象减少至26个,接口调用从34次减少到14次,整体耗时从原来750ms缩短至500ms。
策略的接入稍微复杂点,原规则内只会判断是否命中,部分判断逻辑在动作里面也有嵌入需要剥离拆分,在数据源不变的情况下原来一个策略将会细分出更多策略,这些新增的策略将会在策略集内并行方式触发。在实验中发现并没有因策略数量和调度次数增多对系统产生负面影响,依然维持在500ms左右,结果符合预期。下图即优化后新的流程示意图,虚线框内特征集与策略集都是并行模式触发。
截止目前对该流程做了简单汇总,用到的数据对象覆盖了涉黄、涉毒、新手规则、广告、非法荐股等众多类别,内容风险类别越来越多,系统覆盖的场景会同步增多,引擎便捷的扩展能力为内容管理提供高效服务。
规则 | 特征数据对象 | 策略 | |
---|---|---|---|
数量 | 50(其中10个未挂载特征) | 48 | 62 |
我们就以引入算法模型甄别高危内容这个场景为例,介绍业务人员做策略调整的过程,算法模型有不断迭代修正、长期实验的特性,这就要求系统有快速应变能力。在原接入模式下从研发接到需求开始到上线结束需要多方介入,当遇到复杂需求时整个周期可能是以天为单位,如果有调整流程参数或策略判断条件之类的快速变更需求整个流程会重新轮回,周期变得越来越长。
引擎完成度的增加,其完善的接入、处置以及发布机制的介入,规避了参与主体过多产生的部分人为因素风险,简化了风控需求接入流程,通常情况下研发干预的环节是创建模版,业务人员在研发的协助下生成模板特征、虚拟环境内调试特征和策略,需求的参与主体将只有业务人员,在平台安全机制保障下辅助业务人员平稳调整策略,接入过程变成如下示意图所描述的那样。
参与主体只有业务人员后,其自身就可以实现需求闭环,可在数分钟内完成配置,极大的提高效率,安全性也有保证,最后通过离线数据分析策略命中情况,策略的上线与否是由业务人员通过分析结果做决策。如下图荐股模型先审策略条件的调整,业务人员编辑完成发布后机会立即生效。
规则引擎在设计之初就将安全上线做为重点,任何业务类需求或者改动都需要在虚拟环境内检验通过后,方能配置到流程中,虚拟环境具有支持流程、特征和策略的验证,形成配置化和模板化的测试接入能力。
规则引擎经过几个版本迭代后,发布平台接入实验接口,业务人员填充好待测组件所需的输入参数,提交到运行容器使用引擎返回的上下文ID即可在数仓获取相应的关联数据,因虚拟环境的安全特性所有提交的实验都不会触发动作。
在前面说到了模型特征一体化,模型特征和普通的特征差异化体现在创建上,模型特征基于模型特征模板构建,创建好的模型特征不具备配置策略的能力,必须在虚拟环境内实验通过,结果符合预期才能发布,使用时其和普通的特征没有任何差别。
策略虚拟环境,面向的对象主要是业务和研发人员,在本地或者测试环境无法支持的条件下,生产上进行无害化的实验变得十分必要,容器内集成了日志引擎,具有数据收集能力,运行时数据会被日志引擎推送到数仓和Hbase集群作为后续分析的数据源。
以上总结了我们在建设规则引擎过程中的探索和实践过程,推出对业务人员非常友好的可视化的策略编排功能,在使用上接近sql条件的编写,在研发方面,缩短了开发时间,只需要关注动作与特征开发。目前的规则引擎经过多个版本迭代,在策略编排上的难度做到了极大的简化,业务效率和应对风险的能力提升非常明显,后续我们会在系统层面完善精细化的权限控制,完善安全使用制度,增加上线审批,专人创建,专人维护等方面的权限校验,增强平台安全使用方面的控制能力,支持线下或者线上策略分析的全流程回放、规则运行时并行化和特征数据之间的依赖分析等。在业务层面延伸使用场景,依据系统已经内置的大量特征,涉及决策的业务场景都可以进行快速接入,目前规则引擎已经应用到内容管理,搜索业务内。