Lambda 架构在有赞广告平台的应用与演进

有赞广告平台依托于有赞微商城,帮助商家投放广告。通过有赞广告平台,商家可以在腾讯广点通云堆小博无线等流量渠道投放广告。
对于有赞广告平台,除了提供基础的广告编辑、投放、素材管理等功能,最重要的就是广告的投放效果的展示、分析功能了。有赞广告平台的数据分析模块提供了不同的时间维度(天、小时),不同的实体维度(广告计划、广告、性别、年龄、地域)下的不同类型指标(曝光、点击、花费、转化下单、增粉数)的分析。所有这些数据都是秒级到 10min 级别的准实时数据,为了做到将实时数据和离线数据方便的结合,我们引入了大数据系统的 lambda 架构, 并在这样的 lambda 架构的基础下演进了几个版本。在这里想把广告系统的数据统计服务演进历程以及踩过的坑、得到的感悟和各位同僚分享一下😊

大数据系统的 Lambda 架构

大数据处理技术需要解决数据的可伸缩性与复杂性。首先要很好地处理分区与复制,不会导致错误分区引起查询失败。当需要扩展系统时,可以非常方便地增加节点,系统也能够针对新节点进行 rebalance。其次是要让数据成为不可变的。原始数据永远都不能被修改,这样即使犯了错误,写了错误数据,原来好的数据并不会受到破坏。

Lambda 架构的主要思想是将大数据系统架构为多个层次:批处理层(batchlayer)、实时处理层(speedlayer)、服务层(servinglayer)。批处理层生产离线数据,是每天重新计算的,实时处理层的数据增量更新,数据时效过去之后会被清理,由批处理层的数据替代。服务层则对外提供数据服务,综合批处理层以及实时处理层的数据。典型的 lambda 架构图如下:
lambda架构
- 批处理层每天离线的计算历史数据,全量刷新昨日之前的历史统计数据,产生 batch Views
- 实时处理层实时的获取增量数据,产生当日实时的增量统计数据,产生 real-time Views
- 服务层从 batch Views 以及 real-time View 读取数据,向外提供实时 + 离线的数据统计服务

有赞广告平台的数据来源

有赞广告平台展示的数据指标包含两类:曝光类(包括曝光数、点击数、点击单价、花费),转化类(包括转化下单数,转化下单金额,转化付款数,转化付款金额)。前一类的数据主要由流量方以接口的方式提供(比如对接的腾讯广点通平台),后一类则是有赞特有的数据,通过买家的浏览、下单、付款日志算出来。

第一版架构

第一版架构

第一版采用了典型的 lambda 架构形式。批处理层每天凌晨将 kafka 中的浏览、下单消息同步到 hdfs 中,再将 hdfs 中的日志数据解析成 hive 表,用 hive sql/spark sql 计算出分区的统计结果 hive 表,最终将 hive 表导出到 mysql 中供服务层读取。另一方面,曝光、点击、花费等外部数据指标则是通过定时任务,调用第三方的 api,每天定时写入另一张 mysql 表中。

实时处理层方面,用 spark streaming 程序监听 kafka 中的下单、付款消息,计算出每个追踪链接维度的转化数据,存储在 redis 中。

服务层则是一个 java 服务,向外提供 http 接口。java 服务读取两张 mysql 表 + 一个 redis 库的数据。

第一版的数据处理层比较简单,性能的瓶颈在 java 服务层这一块。
java 服务层收到一条数据查询请求之后,需要查询两张 mysql 表,按照聚合的维度把曝光类数据与转化类数据合并起来,得到全量离线数据。同时还需要查询业务 mysql,找到一条广告对应的所有 redis key,再将 redis 中这些 key 的统计数据聚合,得到当日实时的数据。最后把离线数据和实时数据相加,返回给调用方。
这个复杂的业务逻辑导致了 java 服务层的代码很复杂,数据量大了之后性能也跟不上系统要求。

另一方面,实时数据只对接了内部的 kafka 消息,没有实时的获取第三方的曝光/点击/浏览数据。因此,第一版虽然满足了历史广告效果分析的功能,却不能满足广告操盘手实时根据广告效果调整价格、定向的需求。

第二版架构

第二版架构

针对第一版的两个问题,我们在第二版对数据流的结构做了一些修改:
- 在实时处理层做了一个常驻后台的 python 脚本,不断的调用第三方 api 的小时报表,更新当日的曝光数据表。
这里有一个小技巧:由于第三方提供的 api 有每日调用次数上限的限制,将每天的时间段分为两档:1:00-8:00 为不活跃时间段,8:00- 第二天 1:00 为活跃时间段,不活跃时间段的同步频率为 30min 一次,活跃时间段为 10min 一次。每次同步完数据之后会根据当天消耗的 api 调用次数和当天过去的时间来计算出在不超过当天调用次数前提下,下一次调用需要间隔的时间。同步脚本会在满足不超过当天限额的前提下尽可能多的调用同步 api。从而避免了太快消耗掉当日的调用限额,出现在当天晚上由于达到调用限额而导致数据无法更新的情况。
- 在批处理层,把转化数据表和曝光数据表导入到 hive 中,用 hive sql 做好 join,将两张表聚合而成的结果表导出到 mysql,提供给服务层

完成第二版改动之后,java 服务的计算压力明显下降。性能的瓶颈变成了查询 redis 数据这一块。由于 redis 里面的实时数据是业务无关的,仅统计了追踪链接维度的聚合数据。每次查询当日的转化数据,需要现在 mysql 中查询出广告和跟踪链接的关系,找出所有的跟踪链接,再查询出这些跟踪链接的统计数据做聚合。

另一方面,离线计算的过程中涉及到多次 mysql 和 hive 之间的导表操作,离线任务依赖链比较长,一旦出错,恢复离线任务的时间会比较久。

第三版架构

第三版架构

考虑到 mysql 方便聚合、方便服务层读取的优点,在第三版中我们对 lambda 架构做了一些改动,在数据层面只维护一张包含所有指标的 mysql 表。mysql 表的 st_day(统计日期)字段作为索引,st_day= 当天的保存实时数据,st_day< 当天的保存离线数据。

在第三版中,我们只维护一张 mysql 数据统计表,每天的离线任务会生成两张 hive 表,分别包含转化数据和曝光数据。这两张 hive 表分别更新 mysql 表的 st_day<today 的行中的曝光类指标和转化类指标

在实时数据这块,常驻后台的 python 脚本更新 st_day= 当天的数据的曝光类字段。spark streaming 程序在处理 kafka 中的实时下单消息时,不再统计数据到 redis,而是请求业务 java 服务暴露出来的更新数据接口。在更新数据的接口中,找到当前下单的追踪链接所属的广告,更新 mysql 中 st_day= 当天的数据的转化类字段。这样就把查询阶段的关联操作分散在了每条订单下单的处理过程中,解决了实时数据查询的瓶颈。最终的 java 服务层也只需要读取一个 mysql 表,非常简洁。

总结

有赞广告平台经历了三版的数据架构演进,历时大半年,最终做到了结合内部、外部两个数据源,可以在多维度分析离线 + 实时的数据。在数据架构的设计中,我们一开始完全遵照标准的 lambda 架构设计,发现了当数据来源比较多的时候,标准 lambda 架构会导致服务层的任务过重,成为性能的瓶颈。后续两版的改进都是不断的把本来服务层需要做的工作提前到数据收集、计算层处理。第二版将不同来源的指标合并到了同一个 mysql 表中。第三版则将 redis 数据与业务数据关联的工作从统计阶段提前到了数据收集阶段,最终暴露给服务层的只有一张 mysql 表。

综合这两版的经验,我们发现在 lambda 架构的基础上,尽可能的将一些复杂的合并、关联工作从服务层前提到数据采集层,能够让整个数据流结构更加简洁,最终向外提供的服务性能也会更高。