干货|火山引擎DataTester:5个优化思路,构建高性能A/B实验平台

技术

picture.image

DataTester是由火山引擎推出的A/B测试平台,覆盖推荐、广告、搜索、UI、产品功能等业务应用场景,提供从A/B实验设计、实验创建、指标计算、统计分析到最终评估上线等贯穿整个A/B实验生命周期的服务。DataTester经过了字节跳动业务的多年打磨,在字节内部已累计完成150万次A/B实验,在外部也应用到了多个行业领域。

指标查询的产品高性能是DataTester的一大优势。 作为产品最复杂的功能模块之一,DataTester的指标查询能够在有限资源的前提下,发挥出最极致的A/B实验数据查询体验,而在这背后是多次的技术方案的打磨与迭代。

本文将分享DataTester在查询性能提升过程中的5个优化思路。

picture.image 文 | 凤林

来自字节跳动数据平台DataTester团队

picture.image

现状及问题

实验指标报告页是DataTester系统最核心的功能之一,报告页的使用体验直接决定了DataTester作为数据增长和实验评估引擎在业界的竞争力。该功能具有以下特点:

  1. 牵连系统多、链路长: 报告页涉及到控制台(Console)、科学计算模块、查询引擎、OLAP存储引擎。整个链路包括了:DSL到sql转化、后端查询结果缓存处理、查询结果的加工计算、前端查询接口的组装和数据渲染。
  2. 实现复杂: 实验指标有多种算子,在查询引擎侧中都有一套定制SQL,通过DSL将算子转换成SQL。这是DataTester中最复杂的功能模块之一。

picture.image

picture.image

优化思路

从一条SQL说起——

举一个例子,在DataTester中一次AB测试的查询分三部分逻辑。

  1. 实时扫描事件表,做过滤
  2. 根据用户首次进组时间过滤出用户
  3. 做聚合运算

需要查询详细的SQL代码如下:


                
SELECT event_date,
                
       count(DISTINCT uc1) AS uv,
                
       sum(value) AS sum_value,
                
       sum(pow(value, 2)) AS sum_value_square
                
FROM
                
  (SELECT uc1,
                
          event_date,
                
          count(s) AS value
                
   FROM
                
     (SELECT hash_uid AS uc1,
                
             TIME,
                
             server_time,
                
             event,
                
             event_date,
                
             TIME AS s
                
      FROM rangers.tob_apps_all et
                
      WHERE tea_app_id = 249532
                
        AND ((event = 'purchase'))
                
        AND (event_date >= '2021-05-10'
                
             AND event_date <= '2021-05-19'
                
             AND multiIf(server_time < 1609948800, server_time, TIME > 2000000000, toUInt32(TIME / 1000), TIME) >= 1620576000
                
             AND multiIf(server_time < 1609948800, server_time, TIME > 2000000000, toUInt32(TIME / 1000), TIME) <= 1621439999)
                
        AND (event in ('rangers_push_send',
                
                       'rangers_push_workflow')
                
             OR ifNull(string_params{'$inactive'},'null')!='true') ) et GLOBAL ANY
                
   INNER JOIN
                
     (SELECT min(multiIf(server_time < 1609948800, server_time, TIME > 2000000000, toUInt32(TIME / 1000), TIME)) AS first_time,
                
             hash_uid AS uc2
                
      FROM rangers.tob_apps_all et
                
      WHERE tea_app_id = 249532
                
        AND arraySetCheck(ab_version, (29282))
                
        AND event_date >= '2021-05-10'
                
        AND event_date <= '2021-05-19'
                
        AND multiIf(server_time < 1609948800, server_time, TIME > 2000000000, toUInt32(TIME / 1000), TIME) >= 1620651351
                
        AND multiIf(server_time < 1609948800, server_time, TIME > 2000000000, toUInt32(TIME / 1000), TIME) <= 1621439999
                
        AND (event in ('rangers_push_send',
                
                       'rangers_push_workflow')
                
             OR ifNull(string_params{'$inactive'},'null')!='true')
                
      GROUP BY uc2) tab ON et.uc1=tab.uc2
                
   WHERE multiIf(server_time < 1609948800, server_time, TIME > 2000000000, toUInt32(TIME / 1000), TIME)>=first_time
                
     AND first_time>0
                
   GROUP BY uc1,
                
            event_date)
                
GROUP BY event_date
            

DataTester底层OLAP引擎采用的是clickhouse,根据clickhouse引擎的特点,主要有两个优化方向:

  • 减少clickhouse的join,因为clickhouse最擅长的是单表查询和多维度分析,如果做一些轻量级聚合把结果做到单表上,性能可以极大提升。也就是把join提前到数据构建阶段,构建好的数据就是join好的数据。
  • 需要join的场景,则通过减小右表大小来加速查询。因为join的时候会把右表拉到本地构建hash表,所以必然会占用大量内存,影响性能。

重点优化方案

picture.image

  1. 【预构建加速】预聚合方案,把数据按细粒度预计算加速整体查询

  2. 【预构建加速】ablog方案,把用户进组数据单独存储并每天压缩构建,加速进组人群的圈选

  3. 【聚合查询】GroupBy查询优化

  4. 【缓存加速】au类指标优化,指标内的au数据可以直接复用进组数据的缓存

  5. 【交互优化】异步查询优化,避免了长链接导致的很多网关超时问题,页面多次刷新时更快返回数据提高用户体验

picture.image

picture.image

方案一:预聚合,压缩查询事件量

指标计算的本质

指标的4要素:

指标 = 事件 + 过滤 + 窗口 + 聚合

指标描述了符合过滤条件的事件在一定时间范围内做某种聚合操作之后的结果。事件、过滤条件、聚合操作是通过指标定义的元信息确定,而窗口是通过报告页里的时间范围指定的。

DataTester指标的特点

  • 支持过滤条件
  • 支持实时添加条件
  • 支持天级/小时级/5分钟级等不同粒度的查询
  • 支持组合指标

picture.image picture.image

虽然指标很灵活,但是大多数场景用户进入报告页只会查看进组信息,实验结论,指标天级统计数据等,很少实时带条件去查询。因此,天级查询是我们主要使用场景。天级查询可以通过「预计算」加速。为了支持置信度的计算,「预计算」可以从人的粒度着手,即每天保存一条人的聚合后结果,记录下这个人在所有实验下进组之后各指标下的累积值。这样每天数据量与日活量相当,可以大大压缩总体查询量。

方案详情

picture.image

总体流程图

分为如下几个关键步骤:Dump、Parse、Build、Query

Dump

即把事件dump到离线存储中。私有化采用flume来实现,

  • 自定义timestamp interceptor防止数据漂移
  • 使用file channel文件缓冲保证数据不丢失

Parse

从指标DSL中解析出聚合字段、聚合类型,事件名、过滤条件指标四要素,再根据这些信息生成md5作为clickhouse存储的key。考虑到不同指标配置可能会配置相同的聚合字段、聚合类型,事件名、过滤条件,生成md5的目的是保证唯一防止多次聚合。聚合类型包括count,sum,max,min,latest,distinct(暂不支持),任何算子都可以用这几个基础聚合结果计算出来。如avg可以通过sum/count来计算。

Build

离线构建最核心的部分在于自定义聚合函数(UDAF),自带的聚合函数无法满足我们的要求。

picture.image

Query

即数据如何查询,通过对查询引擎增加参数控制是否走预聚合逻辑,同时针对预聚合定制了查询实现。

picture.image

资源使用限制

私有化场景用户机器资源是非常宝贵的,夜间也有很多定时任务在执行会争抢资源。为了保证不占用太多资源,提交任务时会对spark参数做控制。

以如下参数为基准,对spark.dynamicAllocation.maxExecutors进行控制

driver-memory:4g

executor-memory:2g

executor-cores:2

配置梯度表:

| 资源梯度 | 规模定义 | 资源使用限制 | | 最小配置 | 日活用户<100w,且单日事件量<5000w | 10 | | 中等规模 | 单日事件量between [5000w,2亿)或日活between [100w,1000w) | max(yarn剩余资源的35%, 30) | | 大型规模 | 单日事件量>=2亿或日活>=1000w | max(yarn剩余资源的70%, 50) |

性能表现

4亿事件量,100w用户量,查询提升超过4倍

picture.image

picture.image

picture.image

方案二: ab_log,减小join时右表大小

背景

  1. 服务端实验进组人数通过事件表join事件表圈选,查询非常慢
  2. 事件表存储了大量曝光事件,作用不大,徒增查询事件量
  3. 私有化场景服务端进组时间存于用户属性中,然后时间推移比较难清理,并且存在性能隐患

方案概述

  1. 从实时流中过滤出曝光事件,把用户和进组时间写进实时clickhouse表
  2. 从clickhouse实时表中构建出天粒度的离线用户进组信息表,每天每个用户仅有1条进组记录,记录了该用户该天最早的进组时间。
  3. 查询的时候,为了获得用户首次进组时间,取min(「实时表中该用户当天的进组时间」,「离线表实验开始到T-1天数据中该用户进组时间」)

picture.image

提升效果

  • 通过天级进组表大大加速服务端实验进组人群的圈选

  • 彻底解决私有化进组用户属性的隐患

  • 在私有化环境可以一定程度上减少曝光事件量。在某些客户下,可减少30%以上事件量。

picture.image

方案三: GroupBy查询优化

背景

DataTester的数据查询和其他数据应用产品不同,DataTester在数据查询时,所有的查询都会针对每一个实验版本都查一遍,而过程中中唯一的区别就在于实验版本ID,所以和SQL中GroupBy的应用场景特别契合,通过GroupBy查询不仅可以极大的减少查询的数量,也可以降低多次查询造成的重复扫表,提高查询效率。

优化方案

DataTester对每个实验版本的查询语句都是类似的,只是版本id不同。对DataTester用到的所有查询类型和算子做GroupBy的改造,实现细节这里不做过多展开。

提升效果

测试数据规模为日均一亿,7天,3个实验版本

查询引擎接口响应时长(取10次平均):

| 查询分类 | 累计进组 | 单天进组 | 累积置信计算 | 天级置信计算 | | 客户端 | 三组分开 | 26.80s | 27.01s | 10.82s | 11.13s | | GroupBy | 18.57s(30%↓) | 18.71s(31%↓) | 8.13s(24.8%↓) | 8.28s(25.6%↓) | | 服务端 | 三组分开 | 27.92s | 27.25s | 11.47s | 11.85s | | GroupBy | 11.36s(59.3%↓) | 10.86s(60.1%↓) | 8.07s(29.6%↓) | 5.63s(52.5%↓) |

picture.image

方案四:au类指标优化, 减少重复查询次数

背景

指标查询引擎对DataTester的au类型算子都做了定制,一个指标查询会产生两条sql,一条正常指标的查询sql,另一条是对any_event的au的查询,在最后结果处理的时候对两条sql的查询结果做了一个合并,一起返回到DataTester的科学计算模块。但是,每次打开报告页都必定会查进组人数,它和any_event的au是同一个值,au类型算子查询的时候无法复用进 组人数的结果,而au查询又可以算是最慢的查询之一,降低了报告页打开的速度。

对有进组指标的算子做了缓存优化,减少重复查询。

优化方案

picture.image

picture.image

方案 五: 异步查询优化, 解决页面超时问题

背景

DataTester报告页等一些查询数据的接口本身确实比较耗时,需要实时计算,而大部分网关都有超时限制,这个问题在私有化中尤为明显,所以对报告页的整体交互做了优化改造。

方案介绍

前后端交互

picture.image

服务端架构设计

picture.image

用户体验改进效果

  1. 大幅缩短请求延时,避免出现页面请求失败的情况

  2. 通过增加redis缓存,同页面的多次刷新响应时间可以控制在100ms左右

picture.image

其他优化方案

  1. 业务逻辑优化,报告概览核心指标显著性和进组共用查询结果,去除实验版本按照核心指标显著性的排序,14个SQL降至10个,降低28.5%⬇️

  2. 多维度并发控制,限制资源使用

  3. 默认使用备查询,充分利用备节点的算力

  4. 灵活开关多种报告的缓存,保证核心链路正常运行

picture.image

总结

作为一站式A/B测试平台,火山引擎DataTester最核心的功能之一就是指标查询部分,它关系到产品体验和资源占用情况。而作为TOB领域的数据产品,DataTester能在有限的资源下发挥最极致的产品数据体验,也是产品最为重要的竞争力之一。

本次分享了DataTester在报告页查询优化过程中的5个技术方案落地。预聚合和ablog是从数据构建角度减少查询数据量的角度的优化,groupby和au类指标的优化是从减少并发的角度,异步查询是从产品体验角度。

查询和数据构建密不可分,DataTester未来的产品优化也会按照“去肥”和“增瘦”两个方向进行,“去肥”是优化科学计算模块和查询引擎的整体架构,优化业务逻辑,使得报告页查询逻辑更加清晰和简洁;另一方面“增瘦”就是通过合理的数据构建和数据模型优化加速查询,同时定向对部分难点问题重点优化,比如留存、盒须快照、同期群等等。

产品介绍

火山引擎 A/B 测试,限时免费,立即申请!

A/B 测试,摆脱猜测,用科学的实验衡量决策收益,打造更好的产品,让业务的每一步都通往增长。火山引擎首度发布增长助推「火种计划」,火山引擎 A/B 测试作为「火种计划」产品之一,将为您免费提供 2 亿事件量和 5 万 MAU,以及高达 12 个月的使用权。后台回复数字“8”了解产品

****picture.image

点击 阅读原文,**** 立即跳转火山引擎A/B测试DataTester官网了解详情

picture.image

0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论