火山引擎 ByteHouse:一套方案,让 OLAP 引擎在精准投放场景更高效
更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群
由于流量红利逐渐消退,越来越多的广告企业和从业者开始探索精细化营销的新路径,取代以往的全流量、粗放式的广告轰炸。精细化营销意味着要在数以亿计的人群中优选出那些最具潜力的目标受众,这无疑对提供基础引擎支持的数据仓库能力,提出了极大的技术挑战。
本篇内容将聚焦字节跳动 OLAP 引擎技术和落地经验,从广告营销场景出发,上篇讲解利用ByteHouse 加速实时人群包分析查询的技术原理;下篇以字节跳动内部场景为例,具体拆解广告业务的实现逻辑和业务效果。(文本为下篇)
广告精准投放场景
广告投放过程一般包含数据收集->数据整合->人群圈选->广告投放->反馈分析等关键流程,人群圈选是广告精准投放的关键步骤,它帮助确定广告目标受众,辅助投放平台根据不同受众和广告目标优化投放策略,提升广告收益;
人群预估
人群预估主要是根据一定的圈选条件,确认命中的用户数目。在广告精准投放过程中,广告主需要知道当前选定的人群组合中大概会有多少人,用于辅助判断投放情况进而确定投放预算,通常要求计算时间不能超过 5 秒。
广告投放
广告精准投放过程中遇到的问题与痛点:
数据预估:广告主需要对选定的人群组合进行预估,以便判断投放情况并确定投放预算。但人群包数据量多,基数大。平台的用户数上亿,仅抖音的 DAU 就几亿,抖音、头条对应的人群包在亿级别,早期的预估版本采用 ElasticSearch,但由于数据过于庞大,只能采用 1/10 抽样存储,导致 10%的误差,业务难以接受。
查询性能:广告主可以设定一个非常复杂的圈选条件,导致计算复杂(单次计算可能包含几百上千个人群包),Hive 和 ES 等方案在处理大数据量时,查询速度会变得非常慢,如果需要查询某个广告主的所有用户,需要扫描整个用户库,而这个过程可能需要几分钟甚至几个小时,无法满足实时性要求。
存储空间大:Hive 和 ES 等方案需要额外的索引结构,导致存储空间变大,从而增加了存储成本。例如,如果需要对用户属性进行索引,就需要额外的存储空间来存储索引数据。
不支持高并发:Hive 和 ES 等方案在处理高并发请求时,容易出现性能问题,无法支持高效的广告投放。例如,如果同时有多个广告主需要查询用户信息,就可能会出现查询阻塞或响应延迟等问题。
数据查询效率:采用 ClickHouse 支持预估,但随着数据量的增长,ClickHouse 在当前存储引擎的支持下也难以保证查询时间。这导致了数据查询效率的问题,影响了用户体验。
ByteHouse BitEngine 方案
方案简介
新查询引擎
针对广告人群预估业务开发的新查询引擎,基于 ClickHouse 提供的 MergeTree Family 系列引擎,添加了新的 bitmap64 类型和一系列的相关聚合函数。BitEngine 提供的 bitmap64 类型适合存储和计算大量的用户 ID 之间的关系;在广告人群预估业务中,bitmap64 类型用于存储人群包数据,然后将人群包之间的交并补计算转化为 bitmap 之间的交并补,从而达到远超普通查询的性能指标。
实现步骤
创建一个 bitmap64 类型,可以将用户 ID 直接存储在 bitmap 中,提供一系列交并补的聚合计算,并且还希望可以充分利用多核 CPU 的并行计算能力,由此我们设计了 BitEngine。示例如下
tag_uids_map 存储格式如下
要查询 A&B 的结果 SQL 为
SELECT bitmapCount('A&B') FROM tag_uids_map
BitEngine 实现逻辑
核心思想
对数据做分区划分和编码,保证每个区间的数据之间不存在交集,然后使用 roaring bitmap 保存数据;
计算时每个分区的数据可以独立的做聚合计算,充分利用机器的并行能力,每个分区内部的聚合计算就是多个 bitmap 之间的交并补,利用 roaring bitmap 高效的交并补计算降低 CPU 和内存的使用;
通过字典将编码的结果反解回来,数据编码是为了让数据的分布尽可能稠密,roaring bitmap 在存储和计算的时候就可以获得更好的性能。
业务应用
业务关键要素
人群包:广告主自定义规则计算出来的人群数据,标签是 dmp 团队根据市场需求定义的人群数据。
标签 ID:每天定时根据产出规则更新一次,人群 ID 是自增的,每天根据广告主需求进行新建计算。
统一编码
为了对标签数据和人群数据的 uid 统一编码,编码服务先将标签数据中的 uid 和人群数据中的 uid 提取出来进行统一编码,将全量 uid 均匀 hash 到一万个桶中,桶编号为 i[0<=i<=9999],uid 在每个桶内由 1 开始顺序编码,每个桶的范围为 i*2^40 - (i+1)*2^40。
uid 数据每天都在增加,因此需要支持增量编码, 编码服务每天会先获取增量 uid,hash 后顺序放置到每个桶中。
数据存储
完成编码后,会先把字典数据统一写入 hive 表中,便于字典的各种使用场景。
在数据经过分区和编码之后,ClickHouse 可以以多种数据导入格式将数据以 bitmap64 类型存入磁盘。
数据计算
BitEngine 如何充分利用计算机的并行能力完成每个分区多个 bitmap 之间的交并补计算?
存在问题:
假设存在四个 bitmap,分别为 a,b,c,d;则(a | c) & (b | d)不一定等于(a & b) | (c & d)。
人群包
人群包 A = [10001, 20001,30001,40001,50001],人群包 B = [10001, 20001,20002,20003,20004]
期望结果
通过 BitEngine 计算 A&B = [10001, 20001]
设计方案
人群包按照一定的规则划分为多个区间,任意两个区间之间的人群包没有交集
一个计算线程只读取同一个区间的人群包进行计算,得到一个中间结果
最终的中间结果只需要简单的进行 bitmap or 计算即可
对于这个设计,BitEngine 需要保证数据的读取和计算是严格按照区间进行。BitEngine 在数据读取时会为每一个文件构建一个读任务,由一个线程调度模块完成整个任务的调度和读取,这个线程调度模块的调度原则是:
不同分区的文件不会交叉读取(ClickHouse 的文件读取粒度小于文件粒度,会存在多个线程先后读一个文件的情况,一个分区也可能由多个文件组成),即一个线程只会读 A_1,B_1,不会在这之间读取 A_2 或者 B_2。
一个分区读取完成后,可以立即触发聚合计算,执行 bitmap 之间的计算逻辑,获得中间结果。即 A_1,B_1 读取完成后,可以立即计算 A_1 & B_1。
线程计算完中间结果后,可以继续读其他文件
BitEngine 完成所有中间结果的计算后,会按照结果的输出要求做一次数据合并:
如果需要计算的结果是 bitmap 的基数的时候,BitEngine 直接将各个中间结果的基数相加
如果计算结果需要的是 bitmap,BitEngine 直接将所有的 bitmap 合并起来,这里合并指的是 bitmap or 计算
业务效果
广告业务效果
数据存储空间缩小了 3 倍+
导入时间缩小了 3 倍+
查询 avg/pct99/max 都下降明显,pct99 从 5 s 降低到 2 s
CPU 使用下降明显,PageCache 节省 100 G+
查询误差从 10% 下降到 0%
BitEngine 上线前后查询耗时监控
BitEngine 上线后 CPU 负载对比
PageCache 使用情况(lower is better)
案例总结
BitEngine 上线使用后,经过大量调优,在广告人群预估业务上取得了良好收益。未来,BitEngine 将继续增强功能以支撑广告业务场景,包括:引擎集成数据编码,使编码对用户透明;提供细粒度的缓存以缓存部分重复表达式的计算结果;优化表达式解析等。
点击跳转云原生数据仓库ByteHouse 了解更多
评论