竞价-价格表达配置化平台

背景

前场价格表达场景

img

价格表达调用老链路

img

随着竞价系统在商品提报/审核、参考价工具、价格纠错、价格巡检等场景中不断引入各类的价格因子,以及业务侧不断有从行业/类目维度切入的价格定制化运营策略,每次改动都涉及到上述众多场景,开发成本、改造风险高,产技侧迫切希望能够在竞价系统中沉淀价格域的统一管控、输出,以及配置化的能力,能够快速上线价格需求,并降低风险,最终实现精细化高效运营价格的能力。

目标

业务目标

  • 需求交付提效

价格相关需求能够快速上线,类似需求能够小时级别交付

  • 价格精细化运营

能够支持从不同垂直业务+使用场景+行业/类目等维度定制的精细化价格运营

技术目标

  • 技术提效

    • 竞价系统价格原子能力收拢,统一维护&监控,同时沉淀价格域通用可复用价格能力,支持快速编排并输出前场

    • 基于历史存量价格因子规则变更0开发成本,新接入价格因子开发效能提升至少50%以上

  • 稳定性保障

    • 预发和线上环境价格表达隔离(只支持预发规则同步线上),线上价格表达配置上线/变更需要走审批流(比如需要测试负责人和开发负责人同意)

    • 发布0问题、0故障,能够一建切换历史价格公式版本,快速回滚、可溯源

价格配置化平台设计&落地

画面感

img

为了实现上述画面感(实现目标&解决痛点),设计并落地了一套价格配置化平台。以下主要从方案选型系统架构、实体模型设计、规则引擎设计、可视化配置平台搭建&演示、稳定性建设等方面分享

平台整体方案设计

业务架构

img

技术架构

img

模型设计

模型概念定义

img

抽象并定义以下几个配置化的基础概念:

实体名描述
数据源img 按照数据源来源、类型分、能力可分为三个维度的数据源,比如比如hsf、lindorm这类数据源通用的数据源,我们可以做更加细致化的配置化能力,而类似离线数据源我们可以做离线数据加工的数据源封装
规则因子img 规则因子是规则编辑的最小单元,基于维护好的规则因子能够帮助用户快速创建规则
函数一些复杂逻辑无法通过简单配置完成(比如价格处理场景),即可封装为函数给前场使用
基础规则依赖规则因子,是规则编排的最小单元,通过规则因子、函数、宏、标准程序控制逻辑等条件组合快速生成规则
规则组规则编排的承载单元,通过解析并构建对应编排的有向无环图(DAG)完成基于规则的二次编排,复用基础通用规则,更加灵活化
模型关系图

img

规则引擎设计

方案选型
规则引擎对比
 业界权威规则引擎国内商业化规则引擎轻量级规则引擎规则脚本 
产品DroolsUruleEasy RulesQlexpressGroovy
运行效率中等中等
语法功能coditioncoditionconditionallall
稳定可靠中等中等中等
引入依赖
优化改造成本中等中等
优点使用广泛,支持多种规则定义模式、提供插件,提供部分扩展功能支持多种规则定义,有完善的图形页面提供管理功能轻量级框架和易于学习的API; 支持使用表达式语言(如MVEL和SpEL)定义规则的能力基于语法树,稳定,扩展性高,兼容其他引擎,支持高精度计算、中文宏定义同Qlexpress,支持语法更丰富
劣势不支持嵌套语义,运行内存占用较大,有学习成本,且一般业务系统规则并不复杂用不到大多数特征。分为开源版本和商业版本,开源版本不维护代码api风格较差比较难理解扩展性较差,并基于POJO的开发与注解的编程模型只是脚本语言,决策表,决策树需要进行扩展支持同Qlexpress,并且引入额外依赖多,改造成本相对较大
结果

对数据处理的业务规则来说,在处理流程上不会太冗长和复杂,因此在选型上更偏向于轻量级的规则引擎,能够支持表达式求值,同时支持用户自定义处理组件,并且集团内大多使用的是规则脚本,例如汇金、星环等规则决策底层都是Qlexpress的脚本引擎,相较轻量,可维护,学习成本也较低。 最后选择了QLExpress:稳定性好、性能、扩展性高、集团多业务深度应用、社区活跃、改造成本低

QLExpress

优势&特点

  • 线程安全,引擎运算过程中的产生的临时变量都是threadlocal类型

  • 高效执行,比较耗时的脚本编译过程可以缓存在本地机器,运行时的临时变量创建采用了缓冲池的技术,和groovy性能相当

  • 弱类型脚本语言,和groovy,javascript语法类似,虽然比强类型脚本语言要慢一些,但是使业务的灵活度大大增强

  • 安全控制,可以通过设置相关运行参数,预防死循环、高危系统api调用等情况

  • 代码精简,依赖最小,250k的jar包适合所有java的运行环境

  • 在集团内部深耕多年,在稳定性、业务功能、性能方面的得到了很多的优化和验证

  • Qlexpress提供了很多地方可以扩展脚本能力,比如自定义的函数、自定义的操作符、中文宏定义等。分别通过ExpressRunner的addFunction、addOperator、addMacro实现,尤其是中文宏定义对于可视化规则有很好的帮助。

  • Qlexpress支持高精度计算,初始化时aIsPrecise设置为true,以实现一些精度敏感的金额计算。且有一些增强特征,比如防止空指针、空值比较之类的,见QLExpressRunStrategy

语法支持

  • 支持 +,-,*,/,<,>,<=,>=,==,!=,<>【等同于!=】,%,mod【取模等同于%】,++,--,&&,||in【类似sql】,like【类似sql】,&&,||,!,等操作符,and、or 和java里面的&& || 等价(同时也能够自定义操作符)

  • 支持for,break、continue、if then else 等标准程序控制逻辑

  • 不支持java语法:

    • 不支持try{}catch{}

    • 不支持java8的lambda表达式

    • 不支持增强for循环集合操作

    • 弱类型语言,请不要定义类型声明,更不要用Templete(Map<String,List>之类的)

    • array的声明不一样

    • min,max,round,print,println,like,in 都是系统默认函数关键字,请不要作为变量名

  • 支持/** **/ 注释

  • 支持java对象操作:比如设置某个对象的属性、调用某个bean的方法

  • 支持自定义操作符/函数/宏

脚本性能测试

测试机型:MacBook Pro,Intel Core i7/2.6 GHz/6 Core

测试用例:对ql/groovy在无缓存下执行一个简单布尔组合表达式1万次

测试demo

/**
* @author: huzeng.shz
*/
public class TestDemo {

public static void main(String[] args) throws Exception {
String express = "(a > 3 || (b > 0 && c > 0 && d > 0)) && (a > 2 || (b > 1 && c > 1 && d > 1))";

// QLExpress
DefaultContext<String, Object> context = new DefaultContext<>();
context.put("a", 2);
context.put("b", 2);
context.put("c", 2);
context.put("d", 2);
ExpressRunner runner = new ExpressRunner();

long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
runner.execute(express, context, null, false, false);
}

long end = System.currentTimeMillis();
System.out.println("QLExpress run 10000 times, total cost: " + (end - start) + "ms");

// groovy
Binding binding = new Binding();
binding.setVariable("a", 2);
binding.setVariable("b", 2);
binding.setVariable("c", 2);
binding.setVariable("d", 2);
GroovyShell shell = new GroovyShell(binding);

start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
shell.evaluate(express);
}
end = System.currentTimeMillis();
System.out.println("groovy run 10000 times, total cost: " + (end - start) + "ms");
}
}

结果输出:

结果

QLExpress run 10000 times, total cost: 3180ms
groovy run 10000 times, total cost: 104567ms

从结果上看,执行1万次规则引擎时间耗费: QLExpress < groovy。QLExpress平均每次执行简单表达式求值约0.3ms。

推送方案调研

规则和数据源变更都涉及到本地缓存的更新逻辑,需要调研推送方案进行选型

    废弃废弃
方案ConfigServer发布订阅模式Diamond动态配置推送MetaQ广播消费+本地线程池更新Zookeeper watcher机制精卫广播机制
运行效率
稳定可靠
引入依赖
优化改造成本中等中等
优点基于发布与订阅模型,主要提供非持久数据(发布数据的连接断开,数据就删除,不落盘)的发布和订阅,支持数据聚合,类似Zookeeper可靠、易用、高效,可以通过控制台、客户端API,发布、变更配置值,借助Diamond提供配置变更推送功能,可以实现不重启应用而获取最新的配置值,集团内部应用广泛广播消费天然支持进行应用主机的批量推送(虽然不支持失败重试, 但是从本地缓存里覆盖缓存不太会失败),搭配本地线程池分钟级更新  
劣势大促时间会停推,并且不适合太频繁的推送diamond的推送只保证最终一致性, 无法确定是否覆盖中间状态的值,幂等性业务自己保证,适合适合低频变更,且推送有限流阈值不支持重试,且可能存在消费延迟zookpeeper的watcher机制,是客户端到zookpeeper中心拉数据,了保证性能,拉完一次销毁watcher,然后再次创建watcher,两次watch之间,同diamond一样,update的数据只保持最终一致性精卫的监听是单点的,无法广播。
方案选型基于规则配置平台规则变更的频率不高,MetaQ和diamond能够快速支持多机的批量推送,适配度很高 最终选用MetaQ广播消息 + 本地线程池定时更新方案    
实时引擎

实时规则引擎分享主要分为三块:规则的本地缓存&路由、规则运行、规则的生命周期维护

规则缓存更新&路由

img

需要保证在规则变更后各机器规则的实时性,选用MetaQ广播消息进行应用主机的批量推送+ 本地单线程池定时更新规则方案

  • 消息广播消费,通过广播消息保证各服务器本地缓存的实时更新,同时规则变更频率也很低

  • 消息还是有概率延时,增加机器本地单线程池做热更新规则任务,定时覆盖缓存

  • 路由规则是按照规则类型+业务域 + 查询场景来查询对应规则,如果发现有多个规则匹配,则按照优先级来返回。(前置链路卡住,保证不存在相同优先级情况:按照路由规则 + 优先级已发现存在生效规则,则不应该新建规则,而是应当在老规则上新建版本修改发布)

  • 代码设计

img BidRuleRoute为竞价规则路由组件,主要负责规则的路由查询&规则缓存初始化/热更新

  • 缓存初始化&定时更新

img

img

    • 路由规则

img

规则执行
  • 执行流程

img

  • 框架设计

    • 数据源

img

BidRuleDataFactory工厂负责底层规则数据源的注册、读取、任务dispatch分派,BidRuleDataProvider是真正封装invoke执行数据源内部取数逻辑的入口类,负责将计算的结果填充到ExecutionContext中、已经后续的对结算结果的解析取值逻辑,DefaultHsfDataProvider、DefaultLindormDataProvider支持对于hsf和lindrom的外部调用提供默认通用配置化实现

    • 规则引擎

img

BidPriceRuleEngine继承了QlRuleEngine规则引擎,实现了规则路由、规则编排执行、规则灰度、规则回滚等能力

img

规则创建/变更&发布

img

这里给大家简单演示一下一次规则变更发布的流程:

(📚 → 平台传送门))

离线加工引擎

竞价业务如火如荼做了一段时间,系统本身已经积累了一定的商品sku加工、同款价格的能力,这些能力能够通过离线加工引擎被快速配置外化输出

方案设计

通过配置化任务驱动方式定时产出加工数据,离线数据在线加工后回流到离线加工表

img

  • 任务导入

  • 任务驱动定时加工(离线数据转在线加工->产出加工表到离线调度平台)

外化案例
  • 官补招商竞价sku加工能力外化

  • 价格数据外化为同款比价能力输出

手动业务流程触发流程
涉及到的API
  • 触发手动业务流程执行 CreateManualDag

  • 查询手动业务流程Dag的详情 GetDagDetail

  • 查询手动业务流程中实例的详情 SearchTasks

前提:已经在数据开发中把手动业务流程提交到了调度系统,并记录了手动业务流程的名称

可视化页面搭建

前端页面托管在乐高低代码平台,通过web应用与规则门户接口交互能够快速搭建一个内部规则管理系统。

稳定性建设

img

  • 数据隔离、预发变更不影响线上:线上/预发规则隔离

  • 规则发布流程严格、规范,并有灰度能力

  • 支持秒级回滚规则,快速止血

  • 监控&预警:对线上规则执行&底层数据源查询&异常code都有统一监控&预警,某个数据源rt存在问题秒级定位

项目结果

  • 业务接入效果:0bug,请求水位很平稳

趋势品业务接入价格规则平台后,规则执行成功率一直在100%,同时价格规则执行结果平均都是10ms左右,和之前动则几百ms的价格读取有了显著的性能提升

  • 代码改造,框架升级,后续维护成本很低

价格相关逻辑完全收拢在单独module,低耦合,和外部依赖只通过一个接口,开发者完全不用关心内部逻辑

  • 价格需求变更流程规范化、配置化,提效降本(解放开发&测试双手)

通过可视化平台发布规则,常规价格需求变更无需代码发布,安全又放心,再也不用提心吊胆了

  • 稳定性保障

建立了精细化的规则运行&异常code&数据源调用监控预警(能够快速定位问题),通过规则预跑、审批、灰度机制以及规则的多版本切换/回滚止血方案来保障最终发布的可靠&稳定