糯米促销引擎的实现

  1. 背景
  2. 整体架构
  3. 数据结构
    1. 活动与团单的对应关系
    2. 活动的规则配置
    3. 活动倒排索引
    4. 活动正排索引
  4. 请求预处理
  5. 抽象过程
  6. 性能影响因素
    1. 请求来源
    2. 全国活动
    3. 活动类型
    4. pipeline
  7. 性能优化总结
    1. 多层拦截、过滤,减少下游请求
    2. 缓存策略,降低IO
    3. 为下游服务设置合理的超时时间
    4. 服务监控
  8. 总结
  9. 参考

背景

糯米作为一个O2O平台,团单维度的促销信息自然占很大比重,促销系统承担着对外输出促销信息,通过促销对预算、购买限制等问题的控制,进而提升整个应用的转化效率,这是促销引擎主要的存在意义。

糯米使用的促销引擎,为糯米的各个端与组件提供团单、城市、端类型维度的促销信息,还可以完成对用户设备类型、手机号、百度账号级别的购买次数限制,之所以叫做促销引擎下游不用关心具体的规则,给出请求,算价,渲染物料等活动规则,另外一部分原因也是其中与搜索引擎在排序方面有一部分相关。

整体架构


$$图1 促销引擎的整体架构$$

促销引擎的对外服务的数据来源主要依赖Redis,在Redis数据服务不可用的情况下,将无法输出团单优惠信息,营销服务降级为无优惠活动,用户需要用团单原价支付。Redis服务基于百度内部的分布式平台,机器数量150左右,数据量在900G左右,应该是糯米最大的Redis集群了。

促销引擎第一个版本从2014年7月上线至今,于2016年10月份重构了第二个版本。本文主要的内容是重构的版本。

数据结构

在介绍数据结构之前,先说说正排索引与倒排索引。

正排索引

正排索引(forward index):从文档角度看其中的单词,表示每个文档(用文档ID标识)都含有哪些单词,以及每个单词出现了多少次(词频)及其出现位置(相对于文档首部的偏移量)。

倒排索引

倒排索引(inverted index),也常被称为反向索引、置入档案或反向档案,是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。它是文档检索系统中最常用的数据结构。

活动与团单的对应关系

再说说促销活动与团单的关系,他们的关系是多对多,既一个活动可以配置多个团单,一个团单也可以参与到多个促销活动当中,端的类型可选三端,城市可多选。
也就是说,活动与团单的关系是一种映射关系,既可以在活动配置之初,完成一个活动挂载多个团单;也可以完成在为端上输出活动时,先根据团单(城市、端类型)维度先倒序索引出活动ID,然后再正排索引,将通过得到的活动ID,召回活动规则信息。

下面可以看看将这些数据从活动配置平台到促销引擎的同步拆解过程。

活动的规则配置

活动倒排索引

类型 键名
团单白名单 前缀+城市+终端+团单id actid=>团单id
分团单立减 前缀+城市+终端+团单id actid=>规则信息
品类 前缀+城市+终端+品类id actid=>对应品类
团单活动黑名单 前缀+团单id actid=>value

$$表1 活动规则的索引$$

活动正排索引

活动ID,对应值:召回活动规则信息。

请求预处理

将请求中的数据做预处理的目的,是将一些无关的、无效的数据过滤掉,不让其参与后面的逻辑,进而降低下游服务的获取,达到有效降低IO、压缩响应请求时间的目的。

将无效团单、黑团单的过滤、拦截,不让其进入下一步逻辑,进而完成会员信息、商品数据的获取、活动数据的获取。

抽象过程

根据请求信息匹配活动的过程中,是先根据倒排索引,完成对活动ID的获取,以此为基础,在第二次中以活动ID为正排索引的方式获取活动规则,以此来创建之后在过滤、算价、物料渲染中使用的活动对象。

举例来说:

这样在同步redis构造索引的过程中,就是比较繁杂的一步。
从各个城市、各个端上来的请求,在获取的过程中也有需要特别实现的地方:

活动抽象
在完成活动规则数据从Redis的获取后,需要将其转化为促销引擎内部处理的基本单位,抽象化的活动对象。

活动作为促销引擎模块处理的基本单位,是被实例化的抽象对象。

过滤模块

基于抽象的活动对象,过滤模块针对活动规则,团单规则等信息进行过滤。

过滤是针对匹配到的所有活动进行的,过滤的过程会将活动是否通过过滤、团单是否通过过滤属性做相应标记,之后再各自的业务层中只关心通过过滤的即可。

性能影响因素

调用方多,并且各个调用方内部还有并发调用多个模块等的原因,从而要求促销引擎需要在性能方面不止于在合理的范围内。以下梳理了对促销引擎性能有影响的因素,分而治之:

请求来源

每个请求的城市、端类型只有一个,团单可以有任意多个,这样需要完成这个维度的匹配,在团单列表这类的浏览类接口中,可以认为是单方面的只读接口。影响的原因是每增加一个团单,对商品中心的请求、对redis的请求时成4~6倍的形式增长。

全国活动

某个团单配置了全国范围的活动,就需要在请求倒序索引的过程中,在城市ID的基础上,补上全国维度(ALL)维度的Key。

活动类型

在请求之初,团单对应的活动并不可知,需要将所有可能的活动类型都统一添加请求Redis的一组key数组当中。

pipeline

这其中有一步与Redis的交互方式为使用的是pipeline的方式将请求的key按一定数量切割分批请求,并处理成请求维度的结果集。

基于以上几点,可以看到该引擎系统对于增加一个团单数量的情况下,对Redis以及下游的服务造成的压力是非常明显的。举例:增加一个团单,就要增加如此多的索引;稍不注意,增加一次额外的Redis读写,接口本身的耗时也会明显增加。

对于促销引擎来说,团单数量较大的情况(基本上都很多),每个团单对应的活动,每次都增加一次redis操作,对应增加的io次数就是翻倍。慎重考量每次的数据读写可以避免这种问题。

性能优化总结

对于性能,经验告诉我们,再如何强调都不为过。

多层拦截、过滤,减少下游请求

层层过滤,将对下游产生读取的团单、用户数据,前置过滤,尽量减少对Redis、商品中心、会员活动等服务的请求,尽可能地降低请求内的耗时。

缓存策略,降低IO

附图说明团单维度数量对接口耗时与下游的敏感性。

$$图2 增加一个redis读写带来的耗时增加$$

$$图3 redis读次数增加背后的稳定性相关变化$$

这个图表,充分说明降低下游IO的重要性。主要的方式有:

为下游服务设置合理的超时时间

在为下游服务设置超时时间,确保在对应的请求时间内无结果返回,请求端断开请求。这会造成请求丢失的情况,按照是否对营销业务有影响的情况,为这些下游服务区分强、弱依赖。

以此采取90-95分为的请求耗时,作为下游服务的合理超时时间。

$$图4 为下游服务区别强弱依赖、设置合理超时时间$$

服务监控

此处提及的服务监控主要是基于实例的业务日志异常监控。

针对具体的错误码跟关键字监控,保证逻辑业务在运行异常的情况下,即时通知报警,提高可用性。


$$图5 错误码,关键字的日志监控$$

监控的作用,不仅体现在服务运行期间,也可以在服务上线之初,先配置好基于日志的相关监控,方便在上线之初,针对上线的部分机器(预览机、单台、单边等)时发现问题,做出及时的修正方案。

总结

综上,从整体架构、数据结构、活动在促销引擎的处理过程、性能优化等方面介绍了促销引擎的实践过程。

随着促销工具的多样化,策略更精细,更精准,如何快速地在现有营销活动的基础上或新增活动类型、提高促销引擎的处理能力等问题是接下来的需要考虑的问题。

参考

script>