初次接触氛围系统架构,聊聊我这三个月的理解
对易2023-01-04


本文主要介绍了作者对于氛围中心的业务理解。从氛围的概念出发,阐述了氛围系统的必要性,然后展示了配置端的数据写入、调用端的配置读取等氛围系统的架构细节,最后作者提出了一些对于氛围中心未来的想法和思考。

概述

氛围的概念

氛围是能够刺激消费者购买欲望的一类视觉表达。氛围一般与优惠、价格相关,其目的在于烘托当前商品所处的营销氛围,促进消费者的购买欲望。

氛围的概念

  1. 从产品的角度,氛围中心提供了一个操作台,在配置规则之后,就可以在不同位置设定不同的氛围文案
  2. 从开发者的角度,调用氛围中心的接口,将商品的信息和要展示的位置等参数输入进去,就会输出需要展示的氛围结果
  3. 从用户的角度,氛围中心使得商品在页面上的不同位置展示了一些不同的营销氛围

氛围中心的主要工作就是决定在什么位置经过某些规则后展示什么内容。

举个例子,在淘宝App的购物车页面,如果满足访问用户是88VIP用户的规则,就展示与88VIP相关的文案,同时屏蔽掉其他氛围。

为什么需要氛围中心?

  1. 从用户体验上:导购、交易等全链路需要进行统一的氛围表达和规则管控,以保证营销活动整体氛围的完整性,促进成交转化
  2. 从开发成本上:由氛围中心统一收口,能够减少团队之间的沟通成本,屏蔽各方差异性,提高开发效率。如果没有氛围中心,单个团队推动耗费精力较大:每新增一类营销优惠工具(例如卡券)需要对接搜索、详情、下单等多条链路;每开辟一条新的渠道链路(例如新的搜索页)需要确认是否涵盖原有工具、协调新渠道的透出优先级等等。

氛围中心的整体架构

与一般业务系统不同,氛围系统涉及淘宝和其他App的各种位置,QPS非常高。任何的外调、DB/Tair查询都会导致RT急剧上升,所以采取本地调用的方式进行接入。在业务方的应用中,氛围二方包通过中间件Diamond读取并同步配置信息,并为业务方提供本地接口。

Diamond 是在阿里巴巴集团中广泛使用的配置中心,提供持久化管理和动态配置推送服务。

氛围中心的整体架构如下图所示,以氛围二方包为中心,红线左侧为前端界面,红线右侧为后端逻辑;以蓝线为划分,上方为配置端逻辑,下方为实际业务方调用链路。

从图中可以看出,配置端的操作会经由“前端—应用M—应用B”的链路最终影响到Diamond配置,业务方应用通过二方包进行配置同步并更新配置。

此外,氛围中心还通过应用S提供了HSF接口。除了用以集成测试和用例回归外,对于一些QPS不是很高的业务方,HSF调用也是一种远程接入的方式。

HSF(高速服务框架,High-speed Service Framework)是在阿里巴巴集团中广泛使用的分布式RPC服务框架。

为什么有两套配置端?

如果观察氛围中心的架构图,你会发现其实氛围中心有两套配置端,这其中有一部分是历史的原因,但很大程度上也是必然的结果。

我们回顾一下氛围中心的核心任务:决定在什么位置经过某些规则后展示什么内容

似乎看起来责任很清晰,只需要将位置、规则和内容抽象出来,就生成了一套对应的氛围逻辑。配置端A正是如此:在配置端A里,我们将规则抽象为利益点,位置抽象为渠道,每一个利益点在一个渠道配置有相应的氛围内容,由此实现了规则到位置的 NxN 映射关系。但是在实际中,因为大促活动的存在,我们还有能够随着活动时间快速配置、但透出位置和规则(活动标)却相对固定的需求,即为一种 MxNxN 的一种映射关系,这里的M特指活动时间,由此形成了配置端B。

我们可以看到两套配置端其实侧重点不一样,配置端A主要关注的是规则到位置之间业务映射,而配置端B更关注于如何在大促活动中进行统筹管控。

那么,真的需要同时存在两套配置端吗?

需要,但也不需要。

从现状来看,两套配置端各自有自己特殊的表达,无法单独支撑全部氛围管控任务,需要同时存在:配置端A无法支持大促信息;配置端B的日常氛围管理(即除大促活动外的普通日常氛围)虽然能在一定程度上的等价替代,但是对于位置而言,页面逻辑还是不足以完全表达。

从未来看,两套配置端合并只是时间和成本的问题:第一,两套配置端非常容易引起误解,很多初次接入的应用方会配错位置;第二,两套配置端底层有着相同的输出(二方包、Diamond),改动点只在于前端界面如何从产品角度完整展示位置、规则和内容这三部分的逻辑。

配置端链路

下面通过简述配置链路来说明氛围配置是如何生产并同步的。

在配置链路中,应用M仅起到了认证和权限控制的作用,不作过多阐述。

在应用B中,氛围中心维护了一个针对TDDL数据库的增删改查模块,而且由于配置端使用人数很少,只有有限个产品和开发使用,不需要考虑高并发场景。氛围中心与一般的后端系统相比特殊之处在于:保存数据库时,会同时保存两份数据。一份是一般的数据表单对象,包含了修改人、更新时间等较为详细的信息,用以向前端输出展示,另一份数据是需要同步到Diamond的配置信息,它以字符串的形式,按照分类保存在数据库中,并通过中间件ScheduleX的定时推送任务周期性同步至Diamond。

TDDL(Taobao Distributed Data Layer):是在阿里巴巴集团中广泛使用的一套分布式数据访问引擎,这里作为数据库使用

ScheduleX:是在阿里巴巴集团中广泛使用的分布式任务调度工具,这里作为定时任务中间件使用

氛围中心选择ScheduleX的推送模式为单机模式,即在应用B的集群中随机选择一台机器执行该任务。同时为了保障手动推送时的数据一致性,氛围中心还使用Redis维护了全局锁,保证了每次操作有且仅有一个线程在运行。

在数据库中,每条Diamond配置是分类型存放的,比如时间、图标、文案等都分别保存在不同的Diamond中,这种保存方式有利于配置读取操作的实现,但是却给保存操作带来了困难。对于普通表单的增删改查,只需要操作DAO层即可,但是如果要同时修改对应的Diamond信息,因为不同场景保存的Diamond各有不同,则需要针对不同业务做定制。氛围中心使用抽象类来实现对基本表单对象的修改;而在不同的继承子类中实现对不同业务的Diamond配置的更新。例如在业务处理器A中,通过定制方法只更新了图标、规则等A业务所需要的配置信息。

public abstract class BaseHandler{
  /**
   * 抽象类中add方法示例
   */
  public Result<T> add(Request request, boolean effect) {

    // 使用Maybatis对应的DAO修改普通表单数据
    // ……

    // 走定制逻辑修改Diamond配置
    if (effect) {
      effect(request);
    }
    // ……
  }

 /**
  * 实际继承类单独定制方法
  */
  protected abstract void effect(T t);
}
public class AHandler extends BaseHandler{
 /**
  * 针对业务A的定制
  */
  @Override
  protected void effect(ADTO dto) {
    // 保存与A相关的配置
    // 输入参数已序列化为待保存的对象
    Result<ADTO> result = AManager.saveA2Diamond(dto);

    // ……
  }
}

由上述逻辑可以推测出,在Controller层至少要存在有两个字段才能区分出前端的请求:一个字段指定要操作的处理器,另一个字段要指定进行操作的具体方法(包括增加、删除、修改、生效、推送上线等等)

// 从请求参数中取出识别码获取对应的处理器
Handler handler = HandlerFactory.getHandler(request.getBusinessCode());
handler.check(request);

// 针对不同的操作,调用对应处理器的处理方法
switch (request.getHandleType()) {
  case add:
    return handler.add(request, true);
  case detail:
    return handler.detail(request);
  case list:
    return handler.list(request);
  case copyOnline:
    return handler.copyOnline(request);
  // ... 
  default:
    return Result.buildErrorResult("handle not support");
}

实际上前端的请求也确实包含了这两个字段,一个典型的POST前端请求参数如下:

{
    "environment": "pre", // 用以区分线上还是预发
    "businessCode": "业务code", // 指定要使用的Handler
    "handleType": "modefy", // 具体操作方法
    "id": 0, // 主键id
    "content": {
         //JSON对象
    }
}

至此,我们沿着数据库/Diamond—Manager/Handler—Controller—前端的链路了解了氛围中心配置端的整体调用链路。

总结一下配置端的几个特点:

  1. 数据库同时保存两份数据,一份普通表单对象,另一份整理为Diamond配置
  2. 配置通过ScheduleX进行周期性同步,并使用Tair的全局锁保证数据一致性
  3. Handler层使用抽象类封装对普通表单数据的操作,继承子类实现对不同Diamond配置的保存
  4. 前端请求参数中有处理器、处理方法的标识字段

二方包逻辑

我们再来看一下氛围二方包是如何调用的。

氛围二方包除同步配置之外不进行任何远程调用,应用方如果查询氛围,请求参数除了必备的位置信息(应用名+位置)外,还需要提供商品、优惠等信息。

不同链路依赖的数据源不同,为屏蔽链路之间的差异性,首先需要将输入标准化,构建出统一输入对象。

统一输入对象构建完成后,就是计算有效氛围列表的环节了,首先氛围二方包会从Diamond中获取对应渠道和当前时间的利益点列表,利益点经过一些特制的预处理操作之后,就会循环计算单个氛围是否满足规则条件,最后,通过规则过滤的氛围还会进行互斥等后置处理。

验证氛围首先是根据配置中的全局规则、时间等规则进行过滤,再执行用户自定义规则的过滤。

用户自定义规则按照树型结构进行整合,主规则节点定义了条件执行的策略,包含与(And)、或(Or)的关系;叶子节点定义了实际规则,例如{商品标:1234,优惠:abc,取反:true}

符合条件的氛围会进行素材写入。在素材写入中,除了读取Diamond中配置好的固定内容之外,氛围中心还支持动态文案拼接。动态文案是指在配置端配置诸如满${a}件打${d}折的文案,只需要在请求参数的扩展字段Map中填入相应的内容,例如{a:2,d:75}即可为满足透出逻辑的氛围动态生成,这个功能是通过org.apache.commons.lang3.text.StrSubstitutor#replace(java.lang.Object, java.util.Map<java.lang.String,V>)实现的。

总结与展望

初次接触氛围中心,谈一谈我这三个月来的所见所想。

氛围中心是一个有其自身特性的系统,它经过了不断的迭代和优化,形成了现在的体系。它很好,但是仍有不足:

  1. 整合配置端:同时存在两个配置端确实带来了一些困扰,我们需要通过合理的产品抽象,将位置、规则、时间、活动等信息统一表达,将配置端整合为一个。
  2. Diamond优化:目前氛围中心有四十多条配置,约6MB。一方面超出了Diamond推荐的大小了;另一方面,手工很难去维护和管理如此数量的配置信息。近期的计划是增加过期删除逻辑,先将一部分过期的配置从Diamond中剔除掉,只保留在基本表单中。在开发过程中也在思考是否需要重新组织Diamond的结构,减少Diamond的条数,同时使用分区分片的方式,使得单个应用只读取与自己有关的配置,增强配置的可读性。
  3. 特殊逻辑的代码越来越多:随着开发的不断迭代、修改,层出不穷的预处理器和后置逻辑,还是被不可避免的引入到了代码中。每当个性积累到一定程度,我们就需要对代码进行重构和整理,提取其中的共性加以抽象。这是一个周期性的任务,一个应用只有不断的重构和优化,才能走得更远。