一点推荐策略平台的探索与实践
一、背景
推荐系统一般可以抽象为下面几个核心的流程:
资源加载(用户画像、用户历史、ab实验集等)
召回
融合
过滤(负反馈、运营等)
排序
重排
而每个流程都可能会设置大量的策略实验,以选取最优策略全量发布,以提高用户点击率等指标。在一点内部,整个推荐系统分为了若干微服务,主要的服务如下图所示,其中召回层、策略层、融合层有很多公共的逻辑,这些层中每个服务都有资源加载、召回(从下游获取数据)、融合、过滤、排序等流程。
由于很早之前的老服务没有抽象出业务模型,很多服务实际上流程是一致的,如果模型合理,不需要分开,而且代码冗余问题比较严重。另一方面,老服务项目代码中的条件逻辑有90%都是硬编码,导致修改很多逻辑的修改都需要上线,周期较长,无法支撑策略的快速迭代。
因此,一点内部开发了策略执行平台(Hermes),进行快速策略迭代。Hermes不仅仅适用于推荐的场景,因为目前只有推荐场景接入,所以本文均使用推荐相关的概念来介绍该平台。Hermes推荐策略平台不仅包括策略执行引擎、策略配置服务等模块,还提供了大量基于Hermes的组件,用于一站式开发,从而降低开发难度,提高开发效率。
二、设计方案
2.1 设计思路
策略执行引擎是策略平台的核心组件,而执行引擎的核心则是一个责任链。在责任链实现方面,首先排除了开源工作流组件,工作流场景更看重的是在上下游存在较强依赖关系的基础上做流转控制,甚至支持事务,而且其中大量代码用来支持在配置文件比如xml中配置流程。而推荐服务场景,是一个数据流漏斗,而且只会往前走不存在回滚。为了支持快速迭代,策略配置的修改就需要实时生效,因此开发配套的策略配置平台。
本文的策略平台主要有以下设计点:
策略组成与结构
串行、并行控制
动态条件
策略执行上下文
执行责任链
策略配置读取、构建、实时同步
2.2 基本概念
Hermes的基本概念有:策略(strategy)、子策略(subStrategy)、层(layer)、执行单元(processor)和执行单元组(processorGroup)。子策略、层、执行单元(组)都可以通过配置动态地创建、修改、组合与编排。
执行单元
用户请求执行流程当中最小粒度的业务处理单元,叫做“执行单元”,比如一个召回或者一个过滤器。在策略引擎执行过程中,会有一个执行上下文对象(context对象)传给所有执行单元,来进行数据读写。
执行单元属性有:
名称:前端属性,不影响执行;
所属层:表达从属关系;
所属执行单元组:表达从属关系,可选;
Java全限定类名:实现指定接口,用于创建processor实例;
初始化参数:也叫构造函数参数,用于创建processor实例;
执行顺序(优先级):一个数值,表示同一个执行链中processor执行的先后顺序,数值越小顺序越靠前;若所属组设置了“互斥”,则当前字段值表示优先级,数值越大优先级越大;
执行方式:直接执行、同步阻塞(可设置超时时间);多个顺序相同且同步阻塞执行的processor会并发执行,超时时间以其中最长的时间为准;
执行条件:根据上下文context判断是否执行当前执行单元。
执行单元组
一个执行单元组由多个“执行单元”或者“组”组成。“执行单元组”是是树形结构,叶子节点都是“执行单元”,中间节点为“子执行单元组”。
执行单元组的属性有:
名称:前端属性,不影响执行;
所属层:表达从属关系;
所属组:父节点,表达从属关系,可选;
顺序:一个数值,用来生成执行链中执行单元的执行顺序,数值越小顺序越靠前;若所属组设置了“互斥”,则当前字段值表示优先级,数值越大优先级越大;
条件:根据上下文context判断是否命中当前组;
是否互斥:互斥时同一个组里命中条件的执行单元(组)只会执行一个,优先执行优先级更大的,若优先级相同则以执行单元(组)创建时间先后为准。
层
多个功能类似的执行单元可以组合成一个“层”,比如多个召回构成“召回层”。推荐中各个流程的执行顺序是相对固定的,比如召回层位于排序层之前,“层”是一个虚拟的概念,只会在配置后台的前端页面上看到,用于执行单元功能的划分,而对策略执行引擎是不可见的。“层”只有“名称”和“顺序”两个属性,策略执行引擎读取的策略配置中,执行单元的顺序由“层”顺序、“组”顺序和原“执行单元”顺序生成,从而对引擎隐藏“层”的概念。
子策略
多个层组成一条“子策略”。子策略是执行链的最小单位,执行单元接口有“intercept”和“process”两个方法,分别表示拦截和执行。
子策略执行链结构如下图所示,执行单元先后顺序为A → B → C → D,执行顺序为Di → Ci → Bi → Ai → Ap → Bp → Cp → Dp,如果“intercept”方法返回true,则跳过剩下的执行单元,直接执行当前执行单元的“process”方法,比如Ci返回true,则执行顺序为Di → Ci → Cp → Dp。
“intercept”可以应用在缓存的场景,比如执行单元C表示“返回结果封装”,Ci表示“从缓存中取数据集”,命中缓存则返回true,未命中缓存则返回false,继续执行A、B,获取候选数据集。
子策略属性有:
名称:前端属性,不影响执行;
所属策略:表达从属关系;
执行顺序:一个数值,表示同一个执行链中子策略执行的先后顺序(子策略执行链由多个执行单元链组成,子策略接口中只有“process”方法,纯单向链表结构),数值越小顺序越靠前;
执行方式:直接执行、同步阻塞(可设置超时时间);多个顺序相同且同步阻塞执行的子策略会并发执行,超时时间以其中最长的时间为准;
执行条件:根据上下文context判断是否执行当前子策略。
策略
“策略”由一条或多条子策略组成,结构为树形。策略有3种类型:资源加载(resourceLoader)、主策略和fallback策略,“资源加载”过程主要是解析请求参数、加载用户数据等操作,当主策略执行抛出异常或者满足一定条件触发fallback时,会执行fallback策略,以填补主策略执行结果。
策略属性有:
名称:前端属性,不影响执行;
fallback策略:每个策略可绑定多个fallback策略;
优先级:一个数值,表示优先级,数值越大优先级越大;
执行条件:根据上下文context判断是否执行当前策略,一次请求只会执行一条主策略,命中多个主策略则执行优先级最高的。
一条完整的策略如下图所示:
2.3 框架设计
Hermes策略执行引擎,架构特点是“模块化、低耦合、快速反应”,主要分为配置同步、策略中心、执行单元池、线程池和任务中心五个模块。Hermes策略引擎使用Drools规则引擎www.drools.org.cn/实现规则匹配,使用Spring做组件间依赖的管理。
配置同步
配置同步模块有两部分,分别在策略执行引擎(客户端)和配置后台(服务端),除了可以保证配置实时同步到客户端外,还提供了策略构建成功确认机制,策略构建失败时通知服务端处理(发送报警等)。
(1)客户端
Hermes应用从配套的配置后台(Hermes-Config-Center)读取策略配置,在配置后台发布的策略会实时同步到Hermes策略执行引擎。
配置同步参考了Apollo配置中心的配置同步机制,使用HTTP长轮询机制:
客户端不断通过HTTP获取配置更新的通知消息(超时时间60秒),若要更新配置,则请求服务端拉取最新配置;
一次轮询结束,紧接着开始下一次轮询;
每隔5分钟请求最新配置,进一步保证可以加载到最新配置。
使用长轮询实现消息同步是因为长轮询实现简单可靠,无第三方依赖,充分使用了HTTP自身特性,方便实现各种语言的客户端。
如果需要更新策略配置,则把相关配置数据传给策略中心、执行单元池和线程池,构建运行时数据。这里通过使用消息id对比,避免了策略配置重复构建的情况,而且每个客户端都可以加载到最新配置。
(2)服务端
提供两个接口
获取配置接口:返回最新配置和最新通知消息id
配置更新通知接口:通过Spring的“DeferredResult”来处理,有策略需要发布时,调用DeferredResult的回调函数,通知客户端。
服务端还提供了灰度发布功能,配置更新可以只通知指定客户端。
策略中心
策略中心负责将从配置后台读取的策略配置构建为一个个的drools规则文件,每次更配配置,都会重新构建drools规则。
执行单元池
执行单元池是现有的全部可运行的执行单元集合。从静态角度看,它们是业务逻辑的具体代码;运行时,它们是内存中的一个个对象,由统一的manager管理。策略构建成功时,才会构建新增的执行单元,如果构建期间发生异常则回滚策略配置。
线程池
为了避免某条策略执行过程中占用大量资源,影响其他策略执行,开发了线程池隔离功能,每条策略可单独配置线程池。
任务中心
任务中心主要负责处理用户请求,一次请求的执行流程如下:
执行resourceLoader
规则条件匹配,生成执行计划(策略子树)a. 首先匹配所有的主策略的条件,drools条件匹配则返回该主策略的ID。由于策略之间是互斥的,命中多条策略时则执行优先级最大的。
b. 接着对已匹配策略的子策略和执行单元(组)进行匹配,得到一组子策略和processor的ID集合。
c. 根据策略配置,生成执行计划。
结合线程池,将执行计划构建为可执行的执行链(方法调用链)
调用执行链,出现异常或者返回结果为空,触发fallback
合并主策略和fallback的执行结果
构建和返回响应数据
(1)规则匹配
策略、子策略和执行单元(组)都可以添加条件,在策略树中,父节点匹配(命中条件)才继续匹配子节点,最后匹配完得到一颗策略子树,由策略子树构建策略执行链,做到了执行与条件的分离。
条件支持灵活动态配置,并且支持常见的逻辑关系表达,策略树中一个节点对应一条drools规则,节点间递进关系使用drools的“setFocus”和规则“extends”实现,互斥关系使用drools的“activation-group”实现。
规则“条件”示例:
(2)执行链
对于执行链具体设计的选择,先看下下面两种常见方案。
a.Tomcat中Pipeline方式的责任链设计
\
b.Filter式的责任链
两者最主要的区别在于,单个处理模块能影响的范围不同,生命周期不同。另外,实现同样功能时,组件排列和执行的顺序不同。
Filter模式相当于在外部循环执行每个filter,无论当前filter做了什么,都会把当前输出作为下一个filter的输入按顺序调用,不以当前filter的意志为转移。
Pipeline则是嵌套执行:把一个单元的执行嵌套在前一个单元的执行里面;每个单元的执行都会有一句"next.invoke()"来调用下一个单元,然后在这句代码前后做一些拦截;当前单元可以决定后面单元继续执行,还是直接返回。一个Pipeline的执行流是一个环路:单元1 → 单元2 → 单元3 → 单元2 → 单元1,从拦截的角度讲,每个单元既是前置拦截(下一个单元执行前),又是后置拦截(下一个单元执行后)。
Pipeline方式有很多优点,比如更方便地中断执行并处理异常,具有更好的扩展性。
执行流程必须提供中断机制和异常处理机制,比如鉴权单元发现用户未登录时需立即返回 403 并中断流,当一个单元出现异常时这个异常需要被捕捉和处理。如果单元是嵌套执行的,这些机制会很方便实现:鉴权单元发现用户未登录,不调用“next.invoke()”即可中断 pipeline;
如果单元不是嵌套执行,而是由 pipeline 通过按序循环编排执行的,那么这些非正常流的“扳道工”职责必须由 pipeline 自己来承担,这可能会使 pipeline 不够精简;如果有新的逻辑(比如异步执行),就需要修改 pipeline,导致 pipeline 不稳定。
从加缓存功能的角度来看,也是同理,pipeline更适合。每个单元本身就有权力决定后续走向,比如一个执行流程召回A、过滤B、排序C、缓存模块D,如果D命中缓存则直接返回,不走ABC等流程。如果用filter,流程定义上会更复杂一些,需要先走缓存判定,再确定后面走向。
Hermes的策略执行链结合了Filter方式和Pipeline方式,策略执行链整体采用Filter方式,每个单元是一个子策略执行链,而子策略执行链采用Pipeline方式实现。
2.4 策略配置平台
配置平台的主要功能有:
权限控制:登录、分角色权限控制、操作记录、发布审核等;
策略管理:策略增删改查、灰度发布、全量发布等;
配置同步:保障最新配置能实时可靠地同步到所有策略执行引擎;
客户端监控:实例列表、实例状态;
版本控制:历史发布版本查看和回滚。
策略管理页面
策略树操作页面
三、未来规划
Hermes是一点推荐系统各服务重构升级过程中的一个基础沉淀,目前只有推荐场景部分服务接入了Hermes,其他服务也在陆续迁移中。
Hermes当前也还存在一些问题:
策略配置太过灵活,没有在平台层面加以约束,比如开发类似XML的DTD描述,来规定和约束策略配置;
非法的执行单元初始化参数在引擎构建策略失败时,才会反馈给开发者;
drools规则有一定的学习成本,需要技术基础,策略PM很难使用该平台;
...
Hermes也在持续完善中,未来工作主要有:
执行链边匹配边执行的实现
策略分集群、命名空间,减小作用范围
基于Hermes实现更多的通用组件
策略树页面优化,UI重构,提供拖拽、快速复制节点功能等
使用Json Schema做策略配置校验
...
文章来自一点资讯推荐系统团队
作者:一点资讯技术团队
链接:https://juejin.cn/post/7023573581728579592