基于bitmap的数据模型

1.从一个需求开始说起:
假设给定如下一组用户行为的原始数据:


image.png

数据含义: 表示某个用户的某次访问记录。(这里仅列举了地区和设备维度,当然还会存在浏览器、平台、版本等维度,这里不一一列举了。)

1.1 使用 SQL 分析统计

现在业务想计算「过去7天」在「地区」维度下,「设备: Mac」的人数是多少?So Easy,一个 SQL 搞定


image.png

使用 GrowingIO 平台的分析工具可以表示如下:


image.png

但是通过 SQL 这种现查的方式,随着数据量的越来越大,几十亿或上百亿的时候,对计算所需要的资源和响应时间也会线性地增长,此时客户在使用平台工具最直观的感受就是“菊花”转转转,图表一直加载不出来。

1.2 如何使查询更加高效

1.2.1 堆机器,加资源

最直接粗暴的方式,就是增加更多的计算资源,或者对查询的结果进行缓存、预热。但是对于 SaaS 产品来讲,在查询并发比较高的时候,再多的计算资源也会因为查询排队而遇到性能瓶颈。

1.2.2 数据分层

在数仓的分层架构中,对于经常使用的查询结果,我们可以通过离线计算的方式生成了一个结果表「过去7天-地区-设备-指标表」,示例如下:


image.png

这样在特性的查询场景中,只需要查询结果表就行,很大的减少了计算量,响应时间也短了不少。

但是业务那边的需求往往是千变万化的,然后一堆统计的需求砸到了你的脑袋上

「过去 30 天」的「总用户的量」和「总访问量」是多少?
「昨天」「设备: Windows」的「用户量」是多少?
「获取 30 天」按「平台」拆分的「用户量」是多少?
「11 月- 11 日」来了哪些用户?请导出这个用户列表
其他更多漏斗、留存、画像等数不尽的需求让人头晕目眩。
......

你看了看生成结果表,发现并不能完全解决这些问题,你觉得需要生成更多的结果表来满足更多的需求。然而到最后你发现有些表居然仅仅只使用了一次,数仓里面堆了一堆垃圾。

1.2.3 数据预聚合

和数仓分层的理念类似,对数据进行预计算、预聚合,使用空间换时间的思想加快计算。这也是目前一些主流开源框架的解决思路,比如 SparkSQL 的物化视图、Kylin的 Cube、Druid 的 Segment 、Carbondata 的 MV 等。

下面使用一张图展示主要区别:

image.png

基于我们所追求的方式,我们首先需要寻找一种高效并且灵活的存储模型。

1.3 优化存储模型

基于上节中数据预聚合的思路,从预聚合的结果中,我们不难发现,其中有几个没有摆脱的点:

多个维度依然是横向存储在一起
维度和指标绑定在一条记录中
因为存在这种局限性,刚好限制了我们灵活进行「维度+指标」的任意组装和扩展

如何才能更好的让维度和指标随心所欲地组合呢?我们在预聚合结果的基础之上做了一些改进:

将每个维度纵向存储
将维度和指标分开存储

  1. 基于 BitMap 的存储模型
    2.1 纵向存储维度(人数)

依然以开篇的那组数据为例,此时将维度进行纵向存储:


image.png

此时想取「地区: 北京」和「设备: Mac」的「用户量」


image.png

OK!这样可以很灵活的解决各种维度组合起来的问题了,而且连用户的群体也能直接获取。

但是从表格中发现,用户存储「用户集合」的数据结构还没有解决。那么既能以类似数组的方式存储整数值,还能使用交集(and)操作,还需要达到更好的数据压缩和计算。

此时你应该想到了 BitMap 这种数据结构

至于为何选用 BitMap 的数据结构,以及 BitMap 的功能和基本使用,这里不再探讨。可以参考 java 的实现 BitSet 以及优化的库 RoaringBitmap。

2.2 存储指标量(次数)

为了解决存储指标次数的存储问题,你需要用一个Map 的结构来存储「总的次数」: Map<Int, BitMap> (其中key为次数,value为符合访问次数的人)


image.png

访问量表示: 总共访问「1次」有哪些人,访问「2次」的有哪些人等等。

此时计算「地区: 北京」和「设备: Mac」的「访问量」


image.png

2.3 使用更优雅的方式存储次数

在 Map<Int, BitMap> 这个结构中,key 存储的是 10 进制的数字。这就会导致 Map 的 key 变得特别特别多,所以需要有一种方式来优化一下结构。

方式就是用将 10 进制转化为 2 进制的方式去存储次数,此时 Map 的 key 存储是 二进制为 1 的位置:

比如 2 的二进制是: 「10」,从右向左分别表示(下标i从0开始)「第0位是0」,「第1位是1」。所以将key为1的 bitmap 中存储这个人。

比如5的二进制是: 「101」,从右向左分别表示「第0位是1」,「第1位是0」,「第2位是1」。所以将 key 为 0 和 2 的 bitmap 中存储这个人。

然后将上节 2.2 中结果表示如下:


image.png

此时计算「地区: 北京」和「设备: Mac」的「访问量」

image.png
  1. 多维度交叉的问题 ⚔️

理想是美好的,但是现实很残酷。在 2.1 小节的例子中,每个用户的维度组合只有一种,但是现实中往往一个用户行为可能会存在多种维度组合的情况。

那么什么是维度组合: 一条数据中唯一的所有维度值,称为一个组合。

PS: 如果你的系统中某个 ID 的维度组合只有一种。比如某个订单,一旦生成了,他的价格,商品,物流等信息基本都是固定的。那么之前的模型基本都能满足大多场景了。

3.1 面临的问题

那么会导致什么问题呢?此时回到起点,又来了一批用户行为的数据如下:


image.png

此时多了一个「用户1」在「杭州」使用了「Windows」。如果按照之前的模型存储如下:


image.png

此时计算「地区: 北京」的用户:直接返回 [ 1 , 2 , 3 ],问题不大

此时计算「地区: 北京」和「设备: Windows」的用户


image.png

❌ 你会发现,得出的结果是错的,应该只有「用户 3 」满足才对。

3.2 使用维度组合编号的方式解决

其实问题出在将维度分开进行存储的时候,丢失了「维度组合关系」这个重要的衡量条件。「用户 1 」虽然在「北京」待过,也使用过「Windows」,但是他却没有同时满足这个条件,这就是问题所在。

所以需要一种方式来存储「维度组合关系」这一重要信息。

将每个人维度组合进行顺序编号,得到如下结果:


image.png

注意:编号是对应到每个人的,相同的维度组合,编号是一样的。

此时对应的存储结构也发生了变化:Map<Short, BitMap>( key 代表编号,value 代表人的集合


image.png

此时我们再来计算「地区: 北京」和 「设备: Windows」维度的用户


image.png

最后得到的结果就是「用户3」,算出来的数据就变准确了。

3.3 多维度情况下计算次数

其实稍微想一下就是两层的 Map 结构:Map<Int, Map<Short, BitMap>>,比如刚刚的那组数据表示如下:


image.png

比如「用户1」这个用户,在「编号0」发生的 2 次,在「编号1」发生了 1 次


image.png

此时我们来计算「地区: 北京」和 「设备: Mac」维度的访问量
image.png
  1. 简单的性能对比

环境准备:SparkSQL(local[16], 内存4G), BitMap 单线程计算(内存4G)

场景:简单的 2 ~ 3 个维度组合求人数、次数,按照值的降序取 Top 1000

image.png

x轴含义: 数据量/用户量。
y轴含义: 计算时间, 单位毫秒。

可以看到随着数据量的不断递增,SparkSQL 的计算时间也在不断递增,但是 BitMap 的计算时间却相对比较稳定。

  1. 总结

BitMap 是一个兼并计算和存储优势的数据结构,在存储上百万甚至上千万的 ID 时,也能得到很好的计算效果。

并且当你使用 BitMap 存储的时候,就已经天然支持很多的业务场景,比如分群计算、标签计算、漏斗分析、留存分析、用户触达等,因为无需再重新计算人群。

本篇主要揭晓我们是如何基于 BitMap 来作为底层的数据模型,当然在实际应用过程中还有很多的挑战,由于篇幅原因,这里就不展开讲述了。

以下列出一些我们后续的工作内容和攻克方向:

bitmap 是以 int 值进行存储的,但是在实际生产中,你的 ID 数据可能是类似 UUID 的这种字符串,那么需要解决 string 转唯一 int 的问题。

如何使 bitmap 在分布式环境下达到更好的计算效果
如何解决高基维度所带来的挑战
如何在实时、图表分析、分群画像以及更多的业务场景中进行使用
如何设计一个类 SQL 语言来兼容这种数据模型

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容