有赞广告平台依托于有赞微商城,帮助商家投放广告。通过有赞广告平台,商家可以在腾讯广点通、云堆、小博无线等流量渠道投放广告。 对于有赞广告平台,除了提供基础的广告编辑、投放、素材管理等功能,最重要的就是广告的投放效果的展示、分析功能了。有赞广告平台的数据分析模块提供了不同的时间维度(天、小时),不同的实体维度(广告计划、广告、性别、年龄、地域)下的不同类型指标(曝光、点击、花费、转化下单、增粉数)的分析。所有这些数据都是秒级到10min级别的准实时数据,为了做到将实时数据和离线数据方便的结合,我们引入了大数据系统的lambda架构, 并在这样的lambda架构的基础下演进了几个版本。在这里想把广告系统的数据统计服务演进历程以及踩过的坑、得到的感悟和各位同僚分享一下😊
大数据系统的Lambda架构
大数据处理技术需要解决数据的可伸缩性与复杂性。首先要很好地处理分区与复制,不会导致错误分区引起查询失败。当需要扩展系统时,可以非常方便地增加节点,系统也能够针对新节点进行rebalance。其次是要让数据成为不可变的。原始数据永远都不能被修改,这样即使犯了错误,写了错误数据,原来好的数据并不会受到破坏。
Lambda架构的主要思想是将大数据系统架构为多个层次:批处理层(batchlayer)、实时处理层(speedlayer)、服务层(servinglayer)。批处理层生产离线数据,是每天重新计算的,实时处理层的数据增量更新,数据时效过去之后会被清理,由批处理层的数据替代。服务层则对外提供数据服务,综合批处理层以及实时处理层的数据。典型的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表的stday(统计日期)字段作为索引,stday=当天的保存实时数据,st_day<当天的保存离线数据。
在第三版中,我们只维护一张mysql数据统计表,每天的离线任务会生成两张hive表,分别包含转化数据和曝光数据。这两张hive表分别更新mysql表的st_day 在实时数据这块,常驻后台的python脚本更新stday=当天的数据的曝光类字段。spark streaming程序在处理kafka中的实时下单消息时,不再统计数据到redis,而是请求业务java服务暴露出来的更新数据接口。在更新数据的接口中,找到当前下单的追踪链接所属的广告,更新mysql中stday=当天的数据的转化类字段。这样就把查询阶段的关联操作分散在了每条订单下单的处理过程中,解决了实时数据查询的瓶颈。最终的java服务层也只需要读取一个mysql表,非常简洁。 有赞广告平台经历了三版的数据架构演进,历时大半年,最终做到了结合内部、外部两个数据源,可以在多维度分析离线+实时的数据。在数据架构的设计中,我们一开始完全遵照标准的lambda架构设计,发现了当数据来源比较多的时候,标准lambda架构会导致服务层的任务过重,成为性能的瓶颈。后续两版的改进都是不断的把本来服务层需要做的工作提前到数据收集、计算层处理。第二版将不同来源的指标合并到了同一个mysql表中。第三版则将redis数据与业务数据关联的工作从统计阶段提前到了数据收集阶段,最终暴露给服务层的只有一张mysql表。 综合这两版的经验,我们发现在lambda架构的基础上,尽可能的将一些复杂的合并、关联工作从服务层前提到数据采集层,能够让整个数据流结构更加简洁,最终向外提供的服务性能也会更高。总结