竞价-价格表达配置化平台
背景
前场价格表达场景
价格表达调用老链路
随着竞价系统在商品提报/审核、参考价工具、价格纠错、价格巡检等场景中不断引入各类的价格因子,以及业务侧不断有从行业/类目维度切入的价格定制化运营策略,每次改动都涉及到上述众多场景,开发成本、改造风险高,产技侧迫切希望能够在竞价系统中沉淀价格域的统一管控、输出,以及配置化的能力,能够快速上线价格需求,并降低风险,最终实现精细化高效运营价格的能力。
目标
业务目标
需求交付提效
价格相关需求能够快速上线,类似需求能够小时级别交付
价格精细化运营
能够支持从不同垂直业务+使用场景+行业/类目等维度定制的精细化价格运营
技术目标
技术提效
竞价系统价格原子能力收拢,统一维护&监控,同时沉淀价格域通用可复用价格能力,支持快速编排并输出前场
基于历史存量价格因子规则变更0开发成本,新接入价格因子开发效能提升至少50%以上
稳定性保障
预发和线上环境价格表达隔离(只支持预发规则同步线上),线上价格表达配置上线/变更需要走审批流(比如需要测试负责人和开发负责人同意)
发布0问题、0故障,能够一建切换历史价格公式版本,快速回滚、可溯源
价格配置化平台设计&落地
画面感
为了实现上述画面感(实现目标&解决痛点),设计并落地了一套价格配置化平台。以下主要从方案选型、系统架构、实体模型设计、规则引擎设计、可视化配置平台搭建&演示、稳定性建设等方面分享
平台整体方案设计
业务架构
技术架构
模型设计
模型概念定义
抽象并定义以下几个配置化的基础概念:
实体名 | 描述 |
---|---|
数据源 | ![]() |
规则因子 | ![]() |
函数 | 一些复杂逻辑无法通过简单配置完成(比如价格处理场景),即可封装为函数给前场使用 |
基础规则 | 依赖规则因子,是规则编排的最小单元,通过规则因子、函数、宏、标准程序控制逻辑等条件组合快速生成规则 |
规则组 | 规则编排的承载单元,通过解析并构建对应编排的有向无环图(DAG)完成基于规则的二次编排,复用基础通用规则,更加灵活化 |
模型关系图
规则引擎设计
方案选型
规则引擎对比
业界权威规则引擎 | 国内商业化规则引擎 | 轻量级规则引擎 | 规则脚本 | ||
---|---|---|---|---|---|
产品 | Drools | Urule | Easy Rules | Qlexpress | Groovy |
运行效率 | 高 | 高 | 高 | 中等 | 中等 |
语法功能 | codition | codition | condition | all | all |
稳定可靠 | 中等 | 中等 | 中等 | 高 | 高 |
引入依赖 | 少 | 少 | 少 | 少 | 多 |
优化改造成本 | 高 | 中等 | 高 | 低 | 中等 |
优点 | 使用广泛,支持多种规则定义模式、提供插件,提供部分扩展功能 | 支持多种规则定义,有完善的图形页面提供管理功能 | 轻量级框架和易于学习的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广播消息 + 本地线程池定时更新方案 |
实时引擎
实时规则引擎分享主要分为三块:规则的本地缓存&路由、规则运行、规则的生命周期维护
规则缓存更新&路由
需要保证在规则变更后各机器规则的实时性,选用MetaQ广播消息进行应用主机的批量推送+ 本地单线程池定时更新规则方案
消息广播消费,通过广播消息保证各服务器本地缓存的实时更新,同时规则变更频率也很低
消息还是有概率延时,增加机器本地单线程池做热更新规则任务,定时覆盖缓存
路由规则是按照规则类型+业务域 + 查询场景来查询对应规则,如果发现有多个规则匹配,则按照优先级来返回。(前置链路卡住,保证不存在相同优先级情况:按照路由规则 + 优先级已发现存在生效规则,则不应该新建规则,而是应当在老规则上新建版本修改发布)
代码设计
BidRuleRoute为竞价规则路由组件,主要负责规则的路由查询&规则缓存初始化/热更新
缓存初始化&定时更新
路由规则
规则执行
执行流程
框架设计
数据源
BidRuleDataFactory工厂负责底层规则数据源的注册、读取、任务dispatch分派,BidRuleDataProvider是真正封装invoke执行数据源内部取数逻辑的入口类,负责将计算的结果填充到ExecutionContext中、已经后续的对结算结果的解析取值逻辑,DefaultHsfDataProvider、DefaultLindormDataProvider支持对于hsf和lindrom的外部调用提供默认通用配置化实现
规则引擎
BidPriceRuleEngine继承了QlRuleEngine规则引擎,实现了规则路由、规则编排执行、规则灰度、规则回滚等能力
规则创建/变更&发布
这里给大家简单演示一下一次规则变更发布的流程:
(📚 → 平台传送门))
离线加工引擎
竞价业务如火如荼做了一段时间,系统本身已经积累了一定的商品sku加工、同款价格的能力,这些能力能够通过离线加工引擎被快速配置外化输出
方案设计
通过配置化任务驱动方式定时产出加工数据,离线数据在线加工后回流到离线加工表
任务导入
任务驱动定时加工(离线数据转在线加工->产出加工表到离线调度平台)
外化案例
官补招商竞价sku加工能力外化
价格数据外化为同款比价能力输出
手动业务流程触发流程
涉及到的API
触发手动业务流程执行 CreateManualDag
查询手动业务流程Dag的详情 GetDagDetail
查询手动业务流程中实例的详情 SearchTasks
前提:已经在数据开发中把手动业务流程提交到了调度系统,并记录了手动业务流程的名称
可视化页面搭建
前端页面托管在乐高低代码平台,通过web应用与规则门户接口交互能够快速搭建一个内部规则管理系统。
稳定性建设
数据隔离、预发变更不影响线上:线上/预发规则隔离
规则发布流程严格、规范,并有灰度能力
支持秒级回滚规则,快速止血
监控&预警:对线上规则执行&底层数据源查询&异常code都有统一监控&预警,某个数据源rt存在问题秒级定位
项目结果
业务接入效果:0bug,请求水位很平稳
趋势品业务接入价格规则平台后,规则执行成功率一直在100%,同时价格规则执行结果平均都是10ms左右,和之前动则几百ms的价格读取有了显著的性能提升
代码改造,框架升级,后续维护成本很低
价格相关逻辑完全收拢在单独module,低耦合,和外部依赖只通过一个接口,开发者完全不用关心内部逻辑
价格需求变更流程规范化、配置化,提效降本(解放开发&测试双手)
通过可视化平台发布规则,常规价格需求变更无需代码发布,安全又放心,再也不用提心吊胆了
稳定性保障
建立了精细化的规则运行&异常code&数据源调用监控预警(能够快速定位问题),通过规则预跑、审批、灰度机制以及规则的多版本切换/回滚止血方案来保障最终发布的可靠&稳定