前言
背景
以下面的红包投放为例,它就是Luxury的一条投放策略投放到客户端上之后的展现形式。
一条投放策略包含了:
规则(策略触发时机)。对应红包例子,触发时机就是用户打开客户端首页时识别有未使用红包
触点(投放的素材样式)。对应红包例子,素材样式就是整个Pop大卡片样式
钩子(填充触点的动态数据)。对应红包例子,钩子就负责填充pop卡片中的红包金额,和标题文案
其中,钩子的定位是负责对接二三方的接口与服务,为素材投放提供动态数据。
现状
因为二三方服务接口格式都不同,做统一和抽象很困难。早期Luxury为了支撑业务快速上线,二三方服务接入都是用hard code的形式,case by case的解决。这种方式带来的影响有:不仅不能支撑快速迭代的业务需求,并且已有服务也不能沉淀提供给其他策略复用。
钩子到触点的映射需要运营同学手动填写解析表达式,如下图所示。需要运营同学理解后端的返回数据格式,配置成本高且容易配错。
目标
具备易用性:对运营来说不感知钩子的底层差异,能够快速完成钩子到触点的数据映射
可扩展:能够覆盖大部分的业务接入场景
可复用:能够对钩子进行一定程度抽象,避免每次业务接入都要开发新勾子
可沉淀:钩子沉淀为可复用资产,逐渐拓宽平台能力边界
技术方案
钩子模块的总体思路是采用适配器模式,抽象出钩子接口层,二三方服务可以通过实现接口来接入Luxury平台。
适配器模式是可扩展性的经典解决方案。例如:
云原生监控系统Prometheus的exporter用来解决各种异构数据源metrics上报的问题
服务网格ServiceMesh的Mixer用来解决每次http调用时注入额外的业务逻辑
适配器模式做统一抽象的解决方案一般是对出入参做数据映射,适配到统一的接口模型,对上层屏蔽底层实现差异。
钩子模块架构图:
钩子模块链路图:
入参构造
问题:钩子是策略链路上独立可复用的模块,因此钩子不与某个具体策略耦合,也不与某个具体的客户端调用耦合。这意味着客户端的每次调用不知道要传哪些参数给钩子。
解决方案:该类问题的常规解决思路是把入参的结构信息传递到调用方,让调用方感知每次调用的入参格式。但这意味着需要拆分成两次调用,一次获取入参结构,一次发起调用。但在我们在横向分析了自己的业务场景,发现策略的入参往往是精简,有限,可枚举的,比如用户id、商品id、页面id等等。因此我们转变方案,通过每次调用时客户端传递尽可能全的上下文,而钩子从调用上下文里取需要的字段。通过冗余的参数换取更简单的设计,更少的调用次数。
静态配置可视化
问题:运营需要在后台配置钩子的静态配置,但问题是每个钩子的静态配置结构都不一样,服务端的静态配置结构需要透传到后台生成表单让运营感知并且理解。传统解决前后端参数传递的方式是前后端约定接口协议并且开发页面表单,但这种方式对钩子不太适用。每次变更都涉及到前后端联调,发布,不能满足钩子敏捷扩展的需求。
解决方案:要解决表单多变且可持续拓展的问题,用schema动态生成表单是一个好的解决方案。该方案需要前后端约定schema协议,前端提供schema生成表单的引擎,后端提供类生成schema的工具。这样后端的类能直接映射为前端的表单样式,前端表单的一份配置就对应一个后端类实例。整个开发流程能大幅度缩短,支撑钩子敏捷迭代。
一份生成的表单与对应的schema例子如下:
{
"type": "object",
"properties": {
"appId": {
"type": "integer",
"title": "app id",
"required": true
},
"callSource": {
"type": "string",
"default": "fleamarket",
"title": "call source",
"required": true
},
"tppParam": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string",
"title": "tpp入参",
"required": true
},
"value": {
"type": "string",
"default": "${input.}",
"description": "支持从input或config从动态获取参数",
"title": "解析表达式",
"required": true
}
}
},
"description": "从input或config中根据mvel表达式获取参数,最后组装为kv结构的tppParam",
"title": "tpp请求param"
}
}
}
出参标准化
问题:钩子作为业务扩展模块,出参结构是不可穷举的,难以约束和进一步抽象。但矛盾在于如果不做统一的抽象,就会把出参的复杂度暴露给外部,外部就需要感知钩子各种case下的数据结构,如何对出参做统一与抽象,屏蔽出参的复杂性?
分析:我们配置触点与数据的映射时。触点用一个一个占位符挖出来动态数据的“坑”,“坑”是有限且是单一层级的,不会出现一个“坑”里填了个大对象或者大列表的场景。
解决方案:制定规范,要求将钩子的出参映射为平铺的结构,每份平铺的出参都是一份字典(KVKV结构),每个KV语义清晰,运营同学配置策略时,从字典里挑选需要的动态数据字段填入触点,“将有限的数据填入有限的坑”。平铺经验证是满足扩展性需求的,就算是嵌套的List结构,例如K[0:[0,1], 1:[0,1]]也能平铺为K_0_0,K_0_1,K_1_0,K_1_1这种结构。
一个通过字典配置触点的例子如下:
数据映射可配置化
问题:在做红包权益钩子的时候,我们发现红包权益钩子的可复用性不强,因为它仅是TPP算法中台上的一种业务场景,我们一旦需要接TPP算法中台的另一个算法业务场景,就需要重新扩展实现一遍钩子。
分析:可复用性不强的原因在于对接中台类钩子时,中台本身有很强的拓展能力,而我们的出入参的数据映射针对中台上的一种业务场景写死的,导致不能复用到中台上的其他业务场景。因此,我们需要做到数据映射的可配置化。
解决方案:当发现数据映射成为扩展性的瓶颈时,就要着手解决数据映射灵活性的问题。因此需要实现数据映射的可配置化,钩子实现可配置的数据映射引擎,将入参出参的数据映射配置关系抽出放到静态参数里。这样中台钩子就不与任意一个场景耦合,变更场景时只需要修改数据映射配置,不需要重新开发。当钩子实现不与具体场景耦合时,也就实现了最大化灵活性。
效果
运营同学可以独立配置钩子到触点的映射关系,钩子成为了可以灵活替换的数据字典,可以按需挑选自己需要的动态数据。
沉淀了常见中台钩子,能够覆盖大部分场景需求,服务接入仅需0.5day配置成本。
覆盖促买入、促发布、促浏览、活动互动等多个业务场景。
总结
扩展模块的设计都很“难”,难的原因在于为了保证可扩展性,在方案设计阶段就不能预设接入服务的结构。接入服务可以是任意一个接口,任意一个服务。对于这样没有边界的服务要做统一和抽象是很困难的。
关键的地方在于统一接口层的定义,也是“规范”和“协议“定义。需要我们从业务特点出发,在保证可扩展性的前提下,保证接口层的语义清晰,在可扩展性和易用性之间找到最佳的平衡点。