货拉拉大数据基于规则引擎构建运力资源供需调节系统

4,418 阅读11分钟

业务背景

运力资源是货拉拉业务的核心要素之一,然而随着业务量的增长,运力资源出现了供需失衡的现象,有些专业市场的运力供应紧缺,而有些专业市场的运力供应饱和。面对供需失衡的现象,需要基于数据分析和诊断去制定专业市场的促销策略,以达到平衡供需关系的目的。但仅依赖BI人员通过线下零散报表和临时跑数等工具来辅助人工进行诊断、复盘、优化的手段,无法覆盖全国性运力资源的调节。并且BI人员需将数据在多个系统之间导入导出,导致数据链路冗长、效率低下。

因此,货拉拉大数据团队基于供需失衡的场景,构建一套线上实时动态运力资源调节的系统,通过指标因子结合规则引擎进行调节,以达到运力资源的供需平衡,从而促进司机、用户、平台三方共赢的效果。

项目架构

image.png

运力调节系统,在项目架构维度上,从上到下可分为:应用层、服务层、引擎层、数据存储层。本文主要介绍从应用层的策略配置到引擎层中的任务计算层这部分内容在规则引擎及K8s方面上的实践。

策略组

策略模式

运力的供需平衡调节,总体上可分为三个策略:

  • 诊断策略:BI/算法人员通过多个指标因子,制定诊断策略,由诊断策略中的规则筛选出需要进行供需调节的OD(Origin Grid,起点网格。Destination Grid,终点网格。OD:指起点网格与终点网格)。针对筛选出的OD进行供需调节,并上线。如增加/减少优惠券发放数量,增加/减少司机或用户的补贴额度等等。
  • 复盘策略:通过每日的复盘策略,对OD打上标签,观察经过诊断策略进行供需调节后OD的供需状况。当满足复盘策略的规则时,则执行结束或继续执行优化策略。
  • 优化策略:优化策略是基于复盘策略的结果进行的持续不断的优化,使得最终供需状况得到收敛。

image.png

规则

  • 规则:是筛选OD的最小“原子”单位,规则的构成如下:
左值关系运算符右值
变量(主要是指标变量)>=, <=, >, <, ==, !=, is, contains......具体数值/文本

比如:预付响应单量 >= 100

左值关系运算符右值
预付响应单量>=100

规则组:

  • 单一规则无法筛选出符合要求的OD,此时需要结合逻辑运算符AND、OR,构成相对复杂的逻辑表达式。格式如:规则A && ( 规则B || 规则C )...

规则互斥性校验:

  • 规则互斥性校验的目的:保证规则组筛选出来的OD集合间不存在交集,也就是:规则A ∩ 规则B = ∅。如下图,在全集U中,规则A与规则B扫描出来的OD是不存在交集的,从而避免多个供需策略调节包作用于一个OD,导致OD的供需调节出现不可预知的差错。

image.png

如何通过算法保证规则间互斥性呢,一种比较直观的方法:使用每个规则,从数据库筛选出OD集合,然后对集合求交集,但这种直观的方法却存在以下几个缺点:

  • 不可信:集合求交集必须依赖源数据,而源数据是持续变化的,也就意味着规则的互斥性在不同时刻下,可能是不一样的。
  • 性能低下:每一次的互斥性校验都必须基于全集U下去进行扫描,意味着对数据库的全表扫描,导致性能低下
  • 后置性感知:感知是基于数据库有数据才能执行下一步校验。

因此,这里通过数学方法来校验,做到不依赖源数据进行互斥性校验。其来自数学上的一个定理:任何一个逻辑函数表达式都能展开成一个“与或”表达式,也即称为:析取范式。(Disjunctive Normal Form,简称:DNF)

  • 对于每一个规则组,可用如下图的数据结构表示

    • 第一层节点存储:逻辑运算符or
    • 第二层节点存储:逻辑运算符and
    • 叶子节点:存储关系表达式

image.png

  • 对于没有逻辑运算符的单一规则,为保证归一性,也可用如下图的数据结构表示:

image.png

互斥性校验算法:

对于每条析取范式的规则,其就演变成求:规则A与规则B的Object(参考上图)是否存在交集。而对于Object的比较,遵循如下算法原理:

  • 当且仅当每个Object内的所有变量都存在交集时,则证明整个Object所表示的规则存在交集,也就是不互斥。如果两两对比的Object中不存在相同变量,则使用补足变量的方式,并且该变量占用全集。如:a > 1 与 b > 1对比,采用补足变量的方式,则变成:( a > 1 and -∞ < b < +∞ )与 ( -∞ < a < +∞ and b > 1)。很明显,a > 1 跟 -∞ < a < +∞ 存在交集,-∞ < b < +∞ 跟 b > 1存在交集。因此,a > 1 与 b > 1是存在交集的,也就是不互斥。
  • 当任意一个两两比较的Object存在交集时,则整个规则组存在交集,也就是不互斥。

规则引擎实践

BI/算法人员每天都会产出很多策略,每个策略都包含规则。

一般的项目中没有引用规则引擎之前,通常的做法都是使用一个接口进行业务工作。首先要传进去参数,通过if…else或其他方式进行业务逻辑判断,其次要获取到接口执行完毕后的结果。每次规则的变更,都需要修改写死在代码中的if...else代码,然后进行发版。这种每一次规则变更都需要发版的操作,毫无疑问将导致效率低下,同时也容易引发线上事故。但使用规则引擎后就截然不同了,原有的if…else不复存在,代替它们的是规则引擎脚本,通过规则引擎实现可动态变化的“if …else”。

  • 下面对几种常见的规则引擎进行对比
DroolsIKExpressionAviator
简介Drools(JBoss Rules )是一个开源业务规则管理系统解决方案 (BRMS),提供核心业务规则引擎 (BRE)、Web 创作和规则管理应用程序 (Drools Workbench)。符合业内标准,速度快、效率高IK Expression是一个开源的(OpenSource),可扩展的(Extensible),基于java语言开发的一个超轻量级(Super lightweight)的公式化语言解析执行工具包。Aviator是一个高性能、轻量级的java语言实现的规则引擎,主要用于各种表达式或逻辑表达式的动态求值。
学习成本
性能使用Rete算法快速匹配,性能高。尤其是对处理一个有成百上千属性的复杂对象时的快速匹配规则时速度快。依靠解释执行来完成表达式的执行,和 Aviator,Groovy 这种编译执行的引擎相比,性能较差。通过将逻辑表达式编译成Java字节码,交给JVM去执行。相比其它解释性的规则引擎而言,具有轻量级,高性能的特点。
功能功能丰富功能有限的解析引擎功能相比IKExpression丰富,但逊色于Drools

供需调节的规则并不像风控系统那般复杂,供需调节的核心是从存储海量OD的数据库中找出符合配置规则的特定OD,并对该OD进行如优惠券,补贴等促销手段的调节。可抽象为以下两个方面:

  • 判定OD的具体指标的值是否满足规则表达式,其返回的是一个布尔变量。
  • 由于规则的多变性,因此无法使用写死if...else代码,需通过规则引擎实现可动态变化的“if …else”。

因此,在综合考虑上述各个引擎的学习成本、性能、功能与业务场景的匹配性之后,技术团队选型使用了Aviator作为本项目的规则引擎框架。

image.png

  1. XXL-Job每5分钟定时扫描配置上线的OD策略,根据该策略配置的规则,生成字符串格式的逻辑表达式,并存储于Mysql中。
  2. 当需要扫描OD进行规则匹配时,则从Mysql中加载对应的逻辑表达式,使用 aviator进行compile后缓存。
  3. 将OD的指标的值作为compiledExp的newEnv的值,进行规则配置。
  4. 如果满足规则,则执行业务逻辑;否则,执行另一种业务逻辑。
// 从数据库获得规则,此处以字面常量展示

String expression = "(a > 50 && b > 200) || c > 10";

Expression compiledExp = 

    AviatorEvaluator

    .compile(md5Hash(md5Hash(expression)), expression, true);

// 查询OD,将OD的指标作为newEnv的args。

Boolean matched =

        (Boolean) compiledExp

        .execute(compiledExp.newEnv("a", 300, "b", 450, "c", 700));

if (matched) {

    // 供需策略包......

} else {

   //  其它供需策略调整......

}

性能优化实践

在供需调节系统上,对OD的调节,可以简化为如下的数据链路模型:从数据库查询OD,将查询到的OD经过规则引擎匹配,最终命中对应的供需策略调节包。所以,在性能优化上,主要从以下两方面切入:如何从数据库中快速查询大量的OD?规则的匹配如何优化?下面将从OD查询与规则匹配两方面做的性能提升进行介绍。

image.png

OD查询性能优化方法

虽然在OD的规则匹配过程中,使用了aviator进行规则匹配。但由于OD的数据量大,特别是遇到单个批次数据量大的时候,在查询OD时,即使命中了索引,也时常出现了超时现象。因此,这里引入了一个“批次合并切分”的概念,结合K8s实现分布式多进程与多线程共同协作,从此对读写性能进行了提升。

  • 借鉴贪心算法的思想,对批次中OD数量少的合并下一个批次,对批次中OD数据大的切分成多个子批次。
  • 通过任务切分模块,计算得到需要K8s同时启动的的Job数量以及每个Job内并发的Task数。

image.png

规则匹配性能优化方法

  • 缓存热点OD:由于OD的运力调整是持续不断的迭代直至收敛,也就意味着在多次迭代的过程中,会复用到之前的规则,从而导致同一个OD被同一个规则多次扫描到,因此通过使用LRU算法,对热点OD进行缓存。对于已经经过规则匹配的OD,不用使用Aviator进行规则匹配,直接复用上次规则匹配的结果值。
// 从数据库获得规则,此处以常量展示

String expression = "(a > 50 && b > 200) || c > 10";

String expressionKey = md5Hash(md5Hash(expression));

OdMatchStatus odMatchStatus = lruCacheMap.get(expressionKey);

if (odMatchStatus != null) {

    // 命中热点OD缓存,则不再使用AviatorEvaluator进行规则的匹配判定。

} else {

    // 使用AviatorEvaluator匹配。并记录匹配次数,

    // 当其达到热点OD数据的最低门槛时, 将其加入热点OD的缓存中

}
  • 使用Aviator默认的运行期优化优先的运行模式。
  • 使用编译结果缓存模式,复用编译结果,传入不同变量执行。同时缓存Key使用两次md5哈希算法对表达式进行哈希,避免缓存Key存在哈希碰撞。
AviatorEvaluator.getInstance()

     .setOption(Options.OPTIMIZE_LEVEL, AviatorEvaluator.EVAL);



Expression compiledExp = 

    AviatorEvaluator

    .compile(md5Hash(md5Hash(expression)), expression, true);
  • 外部变量传入,优先使用编译结果的 Expression#newEnv(..args) 方法创建外部 env,将会启用符号化,降低变量访问开销。

结束语:

本文主要介绍规则的互斥性校验原理,规则引擎在运力调节场景下的应用,同时简要介绍在性能优化上的实践。运力调节是一个庞大的工程,本文无法面面俱到,后续将通过其它文章进行介绍。希望读者能从本文中有所收获。

作者介绍:陈佳超|高级大数据工程师。曾任职于华为,现任职于货拉拉大数据部门,主要负责定价系统、营销一体化-AB、A/B实验平台与A/B SDK项目。在大数据应用架构设计、落地、开发、存储与性能优化提升方面有较丰富的实战经验。