小米A/B实验场景基于Apache Doris的查询提速优化实践
业务背景
A/B 实验是互联网场景中对比策略优劣的重要手段。为了验证一个新策略的效果,需要准备原策略 A 和新策略 B 两种方案。随后在总体用户中取出一小部分,将这部分用户完全随机地分在两个组中,使两组用户在统计角度无差别。将原策略 A 和新策略 B 分别展示给不同的用户组,一段时间后,结合统计方法分析数据,得到两种策略生效后指标的变化结果,并以此判断新策略 B 是否符合预期。
数据平台架构
平台使用的数据主要包含平台自用的实验配置数据、元数据,以及业务方上报的日志数据。
由于业务方引入 SDK,并与分流服务进行交互,日志数据中包含其参与的实验组 ID 信息。
用户在实验平台上配置、分析、查询,以获得报告结论满足业务诉求。
CREATE TABLE `dwd_xxxxxx` (
`olap_date` int(11) NULL COMMENT "分区日期",
`user_id` varchar(256) NULL COMMENT "用户id",
`exp_id` varchar(512) NULL COMMENT "实验组ID",
`dimension1` varchar(256) NULL COMMENT "",
`dimension2` varchar(256) NULL COMMENT "",
......
`dimensionN` bigint(20) NULL COMMENT "",
`index1` decimal(20, 3) NULL COMMENT "",
......
`indexN` int(11) NULL COMMENT "",
) ENGINE=OLAP
DUPLICATE KEY(`olap_date`, `user_id`)
COMMENT "OLAP"
PARTITION BY RANGE(`olap_date`)
(
PARTITION p20221101 VALUES [("20221101"), ("20221102")),
PARTITION p20221102 VALUES [("20221102"), ("20221103")),
PARTITION p20221103 VALUES [("20221103"), ("20221104"))
)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 300
;
数据现状分析
报告查询基于明细
当前报告查询的数据来源为明细表,而明细表的数据量巨大:
BE节点CPU使用率
BE节点磁盘IO
当前报告所有查询基于明细数据,且平均查询时间跨度为 4 天,查询扫描数据量上百亿。由于扫描数据量级大,计算成本高,给集群造成较大压力,导致数据查询效率不高。
如果通过对数据进行预聚合处理,控制 Scan Rows 和 Scan Bytes,减小集群的压力,查询性能会大幅提升。
字段查询热度分层分布
由于之前流程管控机制相对宽松,用户添加的埋点字段都会进入到明细表中,导致字段冗余较多。统计历史查询报告发现,明细表中常用的维度和指标只集中在部分字段,且查询热度分层分布:
参与计算的指标也集中在部分字段,且大部分都是聚合计算(sum)或可以转化为聚合计算(avg):
个人思考:
明细表中参与使用的维度只占 54.3%,高频使用的维度只占 15.2%,维度查询频次分层分布。 数据聚合需要对明细表中维度字段做取舍,选择部分维度进行上卷从而达到合并的目的,但舍弃部分字段必然会影响聚合数据对查询请求的覆盖情况。而维度查询频次分层分布的场景非常适合根据维度字段的热度做不同层次的数据聚合,同时兼顾聚合表的聚合程度和覆盖率。
实验组 ID 匹配效率低
当前明细数据的格式为:
exp_id
过滤,查询数据时使用 LIKE 方式匹配,查询效率低下。将实验组 ID 建模成一个单独的维度,可使用完全匹配代替 LIKE 查询,且可利用到 Doris 索引,提高数据查询效率。 将逗号分隔的实验组 ID 直接打平会引起数据量的急剧膨胀,因此需要设计合理的方案,同时兼顾到数据量和查询效率。
进组人数计算有待改进
当进组人数作为独立指标计算时,使用近似计算函数 APPROX_COUNT_DISTINCT
处理,是通过牺牲准确性的方式提升查询效率。当进组人数作为复合指标的分母进行计算时,使用
COUNT DISTINCT
处理,此方式在大数据量计算场景效率较低。
AB实验报告的数据结论会影响到用户决策,牺牲准确性的方式提升查询效率是不可取的,特别是广告这类涉及金钱和业绩的业务场合,用户不可能接受近似结果。
进组人数使用的 COUNT DISTINCT
计算需要依赖明细信息,这也是之前查询基于明细数据的重要因素。必须为此类场景设计新的方案,使进组人数的计算在保证数据准确的前提下提高效率。
数据优化方案
基于以上的数据现状,我们优化的核心点是将明细数据预聚合处理,通过压缩数据来控制 Doris 查询的 Scan Rows 和 Scan Bytes。与此同时,使聚合数据尽可能多的覆盖报告查询。从而达到,减小集群的压力,提高查询效率的目的。
选取高频使用维度聚合
在生成数据聚合的过程中,聚合程度与请求覆盖率是负相关的。使用的维度越少,能覆盖的请求就越少,但数据聚合程度越高;使用的维度越多,覆盖的请求也越多,但数据粒度就越细,聚合程度也越低。因此需要在聚合表建模的过程中取得一个平衡。
因此不难得出结论:选择 14 个维度字段对聚合表建模比较理想,数据量能控制到单日 8 千万条左右,且请求覆盖率约为 83%。
使用物化视图
在分析报告历史查询日志时,我们发现不同的维度字段查询频次有明显的分层:
当向基表写入和更新数据时,集群会自动同步到物化视图,并通过事务方式保证数据一致性。 当对基表进行查询时,集群会自动判断是否路由到物化视图获取结果。当查询字段能被物化视图完全覆盖时,会优先使用物化视图。
精确匹配取代 LIKE 查询
既然物化视图这么好用,为什么我们不是基于 Doris 明细表配置物化视图,而是单独开发聚合表呢?是因为明细数据中的实验组ID字段存储和查询方式并不合理,聚合数据并不适合通过明细数据直接上卷来得到。
exp_id
(实验组ID)在明细表中以逗号分隔的字符串进行存储,查询数据时使用 LIKE 方式匹配。作为 AB 实验报告查询的必查条件,这种查询方式无疑是低效的。exp_id
字段拆开,把数据打平,使用精确匹配来取代LIKE查询,提高查询的效率。exp_id
打平后的数据量:聚合表选取维度字段建模的时候,除了上文提到的,以字段的使用频次热度作为依据之外,也要关注字段的取值基数,进行综合取舍。如果取值基数过高的维度字段进入聚合表,必然会对控制聚合表的数据量造成阻碍。因此,我们在保证聚合表请求覆盖量的前提下,酌情舍弃部分高基数(取值有十万种以上)的维度。
从业务的角度尽可能过滤无效数据(比如一个实验组的流量为 0% 或者 100%,业务上就没有对照的意义,用户也不会去查,这样的数据就不需要进入聚合表)。
exp_id
打平而膨胀。exp_id
字段拆分后,除了查询从LIKE匹配变为精确匹配,还额外带来了两项收益:字段从
String
类型变为Int
类型,作为查询条件时的比对效率变高。能利用Doris的前缀索引和布隆过滤器等能力,进一步提高查询效率。
使用 BITMAP 去重代替 COUNT DISTINCT
要提速实验报告查询,针对进组人数(去重用户数)的优化是非常重要的一个部分。作为一个对明细数据强依赖的指标,我们如何在不丢失明细信息的前提下,实现像 Sum,Min,Max 等指标一样高效的预聚合计算呢?BITMAP 去重计算可以很好的满足我们的需求。
什么是BITMAP去重?
bit_or
的方式进行合并,以bit_count
的方式得到结果。更重要的是,如此能实现去重用户数的预聚合。BITMAP 性能优势主要体现在两个方面:空间紧凑:通过一个 bit 位是否置位表示一个数字是否存在,能节省大量空间。以 Int32 为例,传统的存储空间为 4 个字节,而在 BITMAP 计算时只需为其分配 1/8 字节(1个 bit 位)的空间。 计算高效:BITMAP 去重计算包括对给定下标的 bit 置位,统计 BITMAP 的置位个数,分别为 O(1) 和 O(n) 的操作,并且后者可使用 CLZ,CTZ 等指令高效计算。此外,BITMAP 去重在 Doris 等 MPP 执行引擎中还可以并行加速处理,每个节点各自计算本地子 BITMAP,而后进行合并。
user_id
为String
类型,因此我们还需设计维护一个全局字典,将user_id
映射为数字,从而实现 BITMAP 去重计算。生成 Doris 聚合表时,将 user_id
作为查询指标以 BITMAP 类型来存储,其他常规查询指标则通过 COUNT/SUM/MAX/MIN 等方式聚合:
优化效果
常规聚合指标查询的性能提升自不必说(速度提升 50~60 倍) 进组人数查询性能的提升也非常可观(速度提升 10 倍左右)
集群视角
小技巧
targetConvNum
(目标转化个数)字段,此字段的取值为 0 和 1,查询场景如下:--作为维度
select targetConvNum,count(distinct user_id)
from analysis.doris_xxx_event
where olap_date = 20221105
and event_name='CONVERSION'
and exp_id like '%154556%'
group by targetConvNum;
--作为指标
select sum(targetConvNum)
from analysis.doris_xxx_event
where olap_date = 20221105
and event_name='CONVERSION'
and exp_id like '%154556%';
在聚合表中把这类字段建模成维度 聚合表中需要一个计数指标 cnt,表示聚合表中一条数据由明细表多少条数据聚合得
当这类字段被作为指标查询时,可将其与cnt指标配合计算得到正确结果
明细表查询:
select sum(targetConvNum)
from analysis.doris_xxx_event
where olap_date = 20221105
and event_name='CONVERSION'
and exp_id like '%154556%';
对应的聚合表查询:
select sum(targetConvNum * cnt)
from agg.doris_xxx_event_agg
where olap_date = 20221105
and event_name = 'CONVERSION'
and exp_id = 154556;
结束语
经过这一系列基于 Doris 的性能优化和测试,A/B 实验场景查询性能的提升超过了我们的预期。值得一提的是,Doris 较高的稳定性和完备的监控、分析工具也为我们的优化工作提效不少。希望本次分享可以给有需要的朋友提供一些参考。
扫描下方二维码
抽开源中国周边啦~
往期推荐
Forest + IDEA = 双倍快乐! ForestX 隆重登场
整活大师ChatGPT:实现编程语言、构建虚拟机……
AWS:.NET开源资金严重不足,但我会出手
点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦~
微信扫码关注该文公众号作者