更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群
 
由于流量红利逐渐消退,越来越多的广告企业和从业者开始探索精细化营销的新路径,取代以往的全流量、粗放式的广告轰炸。精细化营销意味着要在数以亿计的人群中优选出那些最具潜力的目标受众,这无疑对提供基础引擎支持的数据仓库能力,提出了极大的技术挑战。
 
本篇内容将聚焦字节跳动OLAP引擎技术和落地经验,从广告营销场景出发,上篇讲解利用ByteHouse 加速实时人群包分析查询的技术原理;下篇以字节跳动内部场景为例,具体拆解广告业务的实现逻辑和业务效果。(文本为上篇)
 

背景

 
人群圈选分析是客户画像平台(CDP)中的核心功能。 分析师利用各种标签组合,挑选出最合适的人群,进而进行广告推送,达到精准投放的效果。同时由于人群查询在不同标签组合下的结果集大小不同,在一次广告投放中,分析师需要经过多次的逻辑调整,以获得"最好"的人群包。在这种高频的操作下,画像平台通常会遇到两方面的问题:
  • 第一,由于此类查询分析是临时性的,各种标签组合数巨大,离线预计算无法满足此类灵活性。
  • 第二,由于此类查询是实时场景,查询性能变得非常关键, 通常一次查询在分钟级,耗时较长,无法满足分析师需求。
 
这篇文章中,我们将会分享人群圈选查询在实时分析OLAP场景下的解决思路,同时介绍如何利用ByteHouse来加速此类查询。从数据表现上看,在10亿级用户测试数据下,ByteHouse的人群查询P99小于10s,展现了优异的性能。
 

场景模型

一个支持人群圈选的数据架构大致如下:

用户的注册信息通过用户流进入数据湖,同时用户的行为信息通过事件流进入数据湖。 之后通过标签生产任务,我们为每个用户打上标签。
由于即时查询的实时性和灵活性,转化好的数据通常会写入OLAP引擎,例如ByteHouse,以提供灵活且实时的SQL查询。用户在分析时,一般会从画像平台应用界面去可视化构建标签逻辑,再由平台应用将这些逻辑转化成SQL,发给ByteHouse进行处理。
从数据模型上看, 数据仓库或者数据湖里存储的格式多数以id-tag为主,例如:
user_id
sex
age
tags
10001
F
20
[]
10002
M
22
[tag_1,tag_2]
10003
F
23
[tag_1]
10004
M
24
[tag_2]
10005
F
25
[tag_1,tag_2]
在人群分析中,以下以tag为主的模式会更合适,例如:
tags
active_users
tag_1
[10002,10003,10005]
tag_2
[10002,10005]
数据是通常是基于用户作为主体存储,这种情况导致用户数量非常多,同时存在很多不必要字段。 那么当用户通过组合标签(tag) 过滤人群时,几乎所有的行都需要被扫描, 使得性能开销随着标签和用户的增长越来越大。
当数据以标签作为主体时,有两个比较大的改动:
  • 其一,只有跟人群相关的维度会被保留,其他信息例如sex,age等会被移除。
  • 其二,active_users以数组(array)的形式存放所有的用户id, 这种操作带来的一个重要的收益是减少了行数,同时减少了数据大小。
在这种模型下, 根据tag组合选取用户就会变成集合的交并补操作,性能对比第一种模型会有显著提升。

ByteHouse Bitmap类型

第二种存储模型可以用如下ByteHouse SQL建表:
CREATE TABLE id_tags (
tags String,
active_users Array<UInt64>
) Engine = CnchMergeTree() order by tags
人群圈选查询,例如找到同时满足tag_1和tag_2的人群的数量,可以用如下SQL完成:
WITH (SELECT active_users as tag_1
FROM id_tags
WHERE tags = 'tag_1') as tag_1_user,
WITH(SELECT active_users as tag_2
FROM id_tags
WHERE tags = 'tag_2') as tag_2_user,
SELECT length(arrayIntersect(tag_1_user, tag_2_user))
虽然该模型可以简化部分操作,但是每个tag的选取需要有一个子查询(with 部分)。这种方式对于表的扫描有大量浪费,而且跟标签的数量线性相关。
为了解决这个问题,ByteHouse内置BitMap类型,可以直接用位(bit)来表示一个tag是否能存在。
沿用以上例子, 在利用BitMap后,建表语句改为:
CREATE TABLE id_tags (
tags String,
active_users BitMap64
) Engine = CnchMergeTree() order by tags
此处注意,我们只是将active_users的类型由Array<UInt64> 改成 BitMap64,其余的部分没有变动。
对于同样的“找到同时满足tag_1和tag_2的人群的数量”的查询,用以下查询:

SELECT bitmapCount('tag_1&tag_2')
FROM tag_uids_map
我们用bit代替了原始的数组,使得该查询可以被优化到在一次表扫描中完成。
基于字节跳动内部线上场景,我们观测到上述的查询优化在多标签场景下,能有10~50倍的性能提升。

数据导入

写入数据进入bitmap表跟普通表没有显著差异。 例如,小批量insert的方式可以用如下方式:
INSERT INTO TABLE id_tags values ('tag_1', [2,4,6]),('tag_2', [1,3,5])
因为id_tags中active_users定义为BitMap64的类型, 数组值[1,3,5], [2,4,6]会被自动转化为BitMap64。之后的计算和存储都会是BitMap64类型。
大批量文件导入时,我们可以利用ByteHouse提供的导入服务,目前离线(TOS, LASFS)以及实时(Kafka)等导入模式均已支持BitMap数据导入。流式写入(如Flink直写)可以通过JDBC接口用insert的方式写入。

相关函数

ByteHouse除了支持BitMap类型的数据进行交并补操作,也内置了大量的列函数,例如bitmapColumnAnd用来接收一个bitmap列,对该列所有bitmap做and运算; 以及bitmapColumnCardinality用来返回一个列中所有bitmap的元素个数。 详情可以参考官方文档

BitEngine原理介绍

BitMap结构解析

假设一个用户ID用32位unsigned integer表示, 那么使用常规bit存储的方式需要2^32 bits ~ 512MB 的空间。如果需要为每个标签对应512MB空间,在标签量增长时,存储量会变得巨大。实际上,很少有业务会遇到2^32 大约40亿用户,因此实际场景中用户ID的分布是很稀疏的。
我们可以基于这个特性,利用Roaring bitmap来进一步压缩这个空间。如下图所示:
在32位的Roaring bitmap中,前16位用于分桶,该取值范围内没有数据则bucket不会被创建,后16位存在对应的container中。Container有两种类型:
  • Array container: 数据量较少的时候(一般少于8K容量),更省空间
  • Bitmap container 适合存储稠密数据、占用空间小
在计算的时候只要对某些bucket中的值进行计算即可。扩展到64位的roaringbitmap的时候,我们可以通过一个map<uint32_t, Roaring>来支持,前32位作为map的key,后32位用roaringbitmap存储。

字典优化

在大部分场景中,以上的roaring bitmap已经有很好的性能。 但是在字节的实际场景中,我们发现由于user_id 不是连续生成的,array container的数量占比会很高。 对两个稀疏人群的交并补操作就变成了对两个有序数组的计算,这种计算对比单纯的位计算,在性能上还是有明显的差异。
因此在ByteHouse中,我们通过字典方式,对数据进行编码,让数据更加集中。
开启字典优化的方式如下:
CREATE TABLE id_tags (
tags String,
active_users BitMap64 BitEngineEncode
) Engine = CnchMergeTree() order by tags

本质上字典服务是个onto映射, 可以通过key 查找value, 也可以通过value反查key, 其中key原始值,value时编码值。开启编码之后,ByteHouse会依赖一个字典文件。在默认情况下,ByteHouse会在内部维护一个字典文件。
当底表更新时,内部字典文件也会随之异步更新。ByteHouse同时也支持用户维护外部字典,这里不做展开。

总结

人群分析是画像平台的基础功能,本文介绍了如何利用ByteHouse内置的BitMap类型来支持实时的画像查询分析。目前ByteHouse云数仓以及企业版均已登陆火山引擎。未来,火山引擎将通过 ByteHouse 来为客户持续提供字节跳动和外部最佳实践,构建交互式大数据分析平台,以应对复杂多变的业务需求和高速增长的数据场景。
 
点击跳转【云原生数据仓库ByteHouse】了解更多

10亿数据、查询<10s,论基于OLAP搭建广告系统的正确姿势的更多相关文章

  1. 这么设计,Redis 10亿数据量只需要100MB内存

    本文主要和大家分享一下redis的高级特性:bit位操作. 本文redis试验代码基于如下环境: 操作系统:Mac OS 64位 版本:Redis 5.0.7 64 bit 运行模式:standalo ...

  2. 怎么对10亿数据量级的mongoDB作高效的全表扫描

    转自:http://quentinxxz.iteye.com/blog/2149440 一.正常情况下,不应该有这种需求 首先,大家应该有个概念,标题中的这个问题,在大多情况下是一个伪命题,不应该被提 ...

  3. CNN实战篇-手把手教你利用开源数据进行图像识别(基于keras搭建)

    我一直强调做深度学习,最好是结合实际的数据上手,参照理论,对知识的掌握才会更加全面.先了解原理,然后找一匹数据来验证,这样会不断加深对理论的理解. 欢迎留言与交流! 数据来源: cifar10  (其 ...

  4. 【Linux】基于VMware搭建Linux系统

    本篇文章侧重于操作,主要内容大致包括: 两大类操作系统简要介绍 VMware Workstation Pro 15简要介绍及安装 CentOS简要介绍及基于Wi'n'dows 操作系统的安装 一 关于 ...

  5. .NET基于Eleasticsearch搭建日志系统实战演练

    一.需求背景介绍 1.1.需求描述 大家都知道C/S架构模式的客户端应用程序(比如:WinForm桌面应用.WPF.移动App应用程序.控制台应用程序.Windows服务等等)的日志记录都存储在本地客 ...

  6. 海量数据处理 - 10亿个数中找出最大的10000个数(top K问题)

    前两天面试3面学长问我的这个问题(想说TEG的3个面试学长都是好和蔼,希望能完成最后一面,各方面原因造成我无比想去鹅场的心已经按捺不住了),这个问题还是建立最小堆比较好一些. 先拿10000个数建堆, ...

  7. 比hive快10倍的大数据查询利器presto部署

    目前最流行的大数据查询引擎非hive莫属,它是基于MR的类SQL查询工具,会把输入的查询SQL解释为MapReduce,能极大的降低使用大数据查询的门槛, 让一般的业务人员也可以直接对大数据进行查询. ...

  8. ORM执行原生SQL语句、双下划线数据查询、ORM外键字段的创建、外键字段的相关操作、ORM跨表查询、基于对象的跨表查询、基于双下划线的跨表查询、进阶查询操作

    今日内容 ORM执行SQL语句 有时候ROM的操作效率可能偏低 我们是可以自己编写sql的 方式1: models.User.objects.raw('select * from app01_user ...

  9. 基于SQL和PYTHON的数据库数据查询select语句

    #xiaodeng#python3#基于SQL和PYTHON的数据库数据查询语句import pymysql #1.基本用法cur.execute("select * from biao&q ...

  10. 2016/05/10 thinkphp 3.2.2 ①系统常量信息 ②跨控制器调用 ③连接数据库配置及Model数据模型层 ④数据查询

    [系统常量信息] 获取系统常量信息: 如果加参数true,会分组显示: 显示如下: [跨控制器调用] 一个控制器在执行的时候,可以实例化另外一个控制,并通过对象访问其指定方法. 跨控制器调用可以节省我 ...

随机推荐

  1. P4870 [BalticOI 2009 Day1]甲虫 题解

    题目链接 简要题意 在一个数轴上有 \(n\) 滴露水,每滴露水初始水量为 \(m\),每秒会蒸发一滴水,一个甲虫初始在原点,速度为 1,水能瞬间喝完,问它最多能喝到几滴水. 题目分析 对于这种移动区 ...

  2. 栈源代码(c++)

    stack.h #ifndef STACK_H_ #define STACK_H_ #include<iostream> template<class T> struct No ...

  3. vue3.0父级组件调用子组件方法

    vue3.0父级组件调用子组件方法 场景:在页面开发过程中,我经常涉及到不同组件之间的元素和方法的调用.就此记录在vue3.0项目,也是我开发的开源项目中的实现方式. 父级组件调用子级 1.应用场景 ...

  4. nginx参数调优能提升多少性能

    前言 nginx安装后一般都会进行参数优化,网上找找也有很多相关文章,但是这些参数优化对Nginx性能会有多大影响?为此我做个简单的实验测试下这些参数能提升多少性能. 声明一下,测试流程比较简单,后端 ...

  5. 小测试:HashSet可以插入重复的元素吗?

    Set的定义是一群不重复的元素的集合容器.也就是说,只要使用Set组件,应该是要保证相同的数据只能写入一份,要么报错,要么忽略.当然一般是直接忽略. 如题,HashSet是Set的一种实现,自然也符合 ...

  6. 黑客玩具入门——5、继续Metasploit

    1.利用FTP漏洞并植入后门 实验靶机:Metasploitable2. 实践: 使用nmap扫描目标靶机 nmap -sV xxx.xxx.xxx.xxx(目标ip) 生成linux系统后门 msf ...

  7. nginx的keepalive和keepalive_requests(性能测试TPS波动)

    当使用nginx作为反向代理时,为了支持长连接,需要做到两点: 从client到nginx的连接是长连接 从nginx到server的连接是长连接 保持和client的长连接: http { keep ...

  8. jmeter编写java脚本

    jmeter开发java脚本主要的依赖包有三个如下图 步骤1 :打开idea,创建一个project,导入上图依赖包 步骤2:创建一个类,继承AbstractJavaSamplerClient类,并实 ...

  9. 记一次 .NET 某新能源材料检测系统 崩溃分析

    一:背景 1. 讲故事 上周有位朋友找到我,说他的程序经常会偶发性崩溃,一直没找到原因,自己也抓了dump 也没分析出个所以然,让我帮忙看下怎么回事,那既然有 dump,那就开始分析呗. 二:Wind ...

  10. LR(1)分析法

    SLR(1)方法的出现,解决了大部分的移进和规约冲突.规约和规约的冲突.并且SLR(1)其优点是状态数目少,造表算法简单,大多数程序设计语言基本上都可用SLR(1)文法来描述. 但是仍然有一些文法,不 ...