大数据分析经常使用去重算法分析『Bitmap 篇』

去重分析在企业平常分析中的使用频率很是高,如何在大数据场景下快速地进行去重分析一直是一大难点。在近期的 Apache Kylin Meetup 北京站上,咱们邀请到 Kyligence 大数据研发工程师陶加涛为你们揭开了大数据分析经常使用去重算法的神秘面纱。html

      △ 陶加涛git

Apache Kylin 做为目前惟一一个同时支持精确与非精确去重查询的 OLAP 引擎,很是好地覆盖了大数据上的去重需求。本次分享讲解了 Kylin 这两种去重方式背后用到的算法,但愿能让你们从源头上理解为何 Kylin 的去重查询有着如此优异的性能。这次分享的回顾将分为两期,本篇首先为你们介绍精确去重算法 Bitmap 。github



 

     △ Meetup 现场视频算法



首先,请你们思考一个问题:在大数据处理领域中,什么环节是你最不但愿见到的?以个人观点来看,shuffle 是我最不肯意见到的环节,由于一旦出现了很是多的 shuffle,就会占用大量的磁盘和网络 IO,从而致使任务进行得很是缓慢。而今天咱们所讨论的去重分析,就是一个会产生很是多 shuffle 的场景,先来看如下场景:apache



咱们有一张商品访问表,表上有 item 和 user_id 两个列,咱们但愿求商品的 UV,这是去重很是典型的一个场景。咱们的数据是存储在分布式平台上的,分别在数据节点 1 和 2 上。数组

咱们从物理执行层面上想一下这句 SQL 背后会发生什么故事:首先分布式计算框架启动任务, 从两个节点上去拿数据, 由于 SQL group by 了 item 列, 因此须要以 item 为 key 对两个表中的原始数据进行一次 shuffle。咱们来看看须要 shuffle 哪些数据:由于 select/group by了 item,因此 item 须要 shuffle 。可是,user_id  咱们只须要它的一个统计值,能不能不 shuffle 整个 user_id 的原始值呢?网络

若是只是简单的求 count 的话, 每一个数据节点分别求出对应 item 的 user_id 的 count, 而后只要 shuffle 这个 count 就好了,由于count 只是一个数字, 因此 shuffle 的量很是小。可是因为分析的指标是 count distinct,咱们不能简单相加两个节点user_id 的 count distinct 值,咱们只有获得一个 key 对应的全部 user_id 才能统计出正确的 count distinct值,而这些值原先可能分布在不一样的节点上,因此咱们只能经过 shuffle 把这些值收集到同一个节点上再作去重。而当 user_id 这一列的数据量很是大的时候,须要 shuffle 的数据量也会很是大。咱们其实最后只须要一个 count 值,那么有办法能够不 shuffle 整个列的原始值吗?我下面要介绍的两种算法就提供了这样的一种思路,使用更少的信息位,一样可以求出该列不重复元素的个数(基数)。数据结构

精确算法: Bitmap框架

第一种要介绍的算法是一种精确的去重算法,主要利用了 Bitmap 的原理。Bitmap 也称之为 Bitset,它本质上是定义了一个很大的 bit 数组,每一个元素对应到 bit 数组的其中一位。例若有一个集合[2,3,5,8]对应的 Bitmap 数组是[001101001],集合中的 2 对应到数组 index 为 2 的位置,3 对应到 index 为 3 的位置,下同,获得的这样一个数组,咱们就称之为 Bitmap。很直观的,数组中 1 的数量就是集合的基数。追本溯源,咱们的目的是用更小的存储去表示更多的信息,而在计算机最小的信息单位是 bit,若是可以用一个 bit 来表示集合中的一个元素,比起原始元素,能够节省很是多的存储。dom

这就是最基础的 Bitmap,咱们能够把 Bitmap 想象成一个容器,咱们知道一个 Integer 是32位的,若是一个 Bitmap 能够存放最多 Integer.MAX_VALUE 个值,那么这个 Bitmap 最少须要 32 的长度。一个 32 位长度的 Bitmap 占用的空间是512 M (2^32/8/1024/1024),这种 Bitmap 存在着很是明显的问题:这种 Bitmap 中不论只有 1 个元素或者有 40 亿个元素,它都须要占据 512 M 的空间。回到刚才求 UV 的场景,不是每个商品都会有那么多的访问,一些爆款可能会有上亿的访问,可是一些比较冷门的商品可能只有几个用户浏览,若是都用这种 Bitmap,它们占用的空间都是同样大的,这显然是不可接受的。

升级版 Bitmap: Roaring Bitmap

对于上节说的问题,有一种设计的很是的精巧 Bitmap,叫作 Roaring Bitmap,可以很好地解决上面说的这个问题。咱们仍是以存放 Integer 值的 Bitmap 来举例,Roaring Bitmap 把一个 32 位的 Integer 划分为高 16 位和低 16 位,取高 16 位找到该条数据所对应的 key,每一个 key 都有本身的一个 Container。咱们把剩余的低 16 位放入该 Container 中。依据不一样的场景,有 3 种不一样的 Container,分别是 Array Container、Bitmap Container 和 Run Container,下文将一一介绍。



首先第一种,是 Roaring Bitmap 初始化时默认的 Container,叫作 Array Container。Array Container 适合存放稀疏的数据,Array Container 内部的数据结构是一个 short array,这个 array 是有序的,方便查找。数组初始容量为 4,数组最大容量为 4096。超过最大容量 4096 时,会转换为 Bitmap Container。这边举例来讲明数据放入一个 Array Container 的过程:有 0xFFFF0000 和 0xFFFF0001 两个数须要放到 Bitmap 中, 它们的前 16 位都是 FFFF,因此他们是同一个 key,它们的后 16 位存放在同一个 Container 中; 它们的后 16 位分别是 0 和 1, 在 Array Container 的数组中分别保存 0 和 1 就能够了,相较于原始的 Bitmap 须要占用 512M 内存来存储这两个数,这种存放实际只占用了 2+4=6 个字节(key 占 2 Bytes,两个 value 占 4 Bytes,不考虑数组的初始容量)。

第二种 Container 是 Bitmap Container,其原理就是上文说的 Bitmap。它的数据结构是一个 long 的数组,数组容量固定为 1024,和上文的 Array Container 不一样,Array Container 是一个动态扩容的数组。这边推导下 1024 这个值:因为每一个 Container 还需处理剩余的后 16 位数据,使用 Bitmap 来存储须要 8192 Bytes(2^16/8), 而一个 long 值占 8 个 Bytes,因此一共须要 1024(8192/8)个 long 值。因此一个 Bitmap container 固定占用内存 8 KB(1024 * 8 Byte)。当 Array Container 中元素到 4096 个时,也刚好占用 8 k(4096*2Bytes)的空间,正好等于 Bitmap 所占用的 8 KB。而当你存放的元素个数超过 4096 的时候,Array Container 的大小占用仍是会线性的增加,可是 Bitmap Container 的内存空间并不会增加,始终仍是占用 8 K,因此当 Array Container 超过最大容量(DEFAULT_MAX_SIZE)会转换为 Bitmap Container。

咱们本身在 Kylin 中实践使用 Roaring Bitmap 时,咱们发现 Array Container 随着数据量的增长会不停地 resize 本身的数组,而 Java 数组的 resize 其实很是消耗性能,由于它会不停地申请新的内存,同时老的内存在复制完成前也不会释放,致使内存占用变高,因此咱们建议把 DEFAULT_MAX_SIZE 调得低一点,调成 1024 或者 2048,减小 Array Container 后期 reszie 数组的次数和开销。



最后一种 Container 叫作Run Container,这种 Container 适用于存放连续的数据。好比说 1 到 100,一共 100 个数,这种类型的数据称为连续的数据。这边的Run指的是Run Length Encoding(RLE),它对连续数据有比较好的压缩效果。原理是对于连续出现的数字, 只记录初始数字和后续数量。例如: 对于 [11, 12, 13, 14, 15, 21, 22],会被记录为 11, 4, 21, 1。很显然,该 Container 的存储占用与数据的分布紧密相关。最好状况是若是数据是连续分布的,就算是存放 65536 个元素,也只会占用 2 个 short。而最坏的状况就是当数据所有不连续的时候,会占用 128 KB 内存。

总结:用一张图来总结3种 Container 所占的存储空间,能够看到元素个数达到 4096 以前,选用 Array Container 的收益是最好的,当元素个数超过了 4096 时,Array Container 所占用的空间仍是线性的增加,而 Bitmap Container 的存储占用则与数据量无关,这个时候 Bitmap Container 的收益就会更好。而 Run Container 占用的存储大小彻底看数据的连续性, 所以只能画出一个上下限范围 [4 Bytes, 128 KB]。



在 Kylin 中的应用

咱们再来看一下Bitmap 在 Kylin 中的应用,Kylin 中编辑 measure 的时候,能够选择 Count Distinct,且Return Type 选为 Precisely,点保存就能够了。可是事情没有那么简单,刚才上文在讲 Bitmap 时,一直都有一个前提,放入的值都是数值类型,可是若是不是数值类型的值,它们不可以直接放入 Bitmap,这时须要构建一个全区字典,作一个值到数值的映射,而后再放入 Bitmap 中。



在 Kylin 中构建全局字典,当列的基数很是高的时候,全局字典会成为一个性能的瓶颈。针对这种状况,社区也一直在努力作优化,这边简单介绍几种优化的策略,更详细的优化策略能够见文末的参考连接。



1)当一个列的值彻底被另一个列包含,而另外一个列有全局字典,能够复用另外一个列的全局字典。



2)当精确去重指标不须要跨 Segment 聚合的时候,可使用这个列的 Segment 字典代替(这个列须要字典编码)。在 Kylin 中,Segment 就至关于时间分片的概念。当不会发生跨 Segments 的分析时,这个列的 Segment 字典就能够代替这个全局字典。



3)若是你的 cube 包含不少的精确去重指标,能够考虑将这些指标放到不一样的列族上。不止是精确去重,像一些复杂 measure,咱们都建议使用多个列族去存储,能够提高查询的性能。

另外,做者的我的 Github 地址为: https://github.com/aaaaaaron,点击下面阅读原文便可进入。

参考

1) 康凯森,《Apache Kylin 精确去重和全局字典权威指南》,2018-01-07,https://blog.bcmeng.com/post/kylin-distinct-count-global-dict.html

2) Hexiaoqiao,《Apache Kylin精确计数与全局字典揭秘》,2016-11-27,https://hexiaoqiao.github.io/blog/2016/11/27/exact-count-and-global-dictionary-of-apache-kylin/

 

猜你喜欢

欢迎关注本公众号:iteblog_hadoop:

回复 spark_summit_201806 下载 Spark Summit North America 201806 所有PPT

回复 spark_summit_eu_2018 下载 Spark+AI Summit europe 2018 所有PPT

回复 HBase_book 下载 2018HBase技术总结 专刊

回复 all 获取本公众号全部资料

0、回复 电子书 获取 本站全部可下载的电子书

一、Apache Spark 2.4 回顾以及 3.0 展望

二、重磅 | Apache Spark 社区期待的 Delta Lake 开源了

三、Apache Spark 3.0 将内置支持 GPU 调度

四、分布式原理:一致性哈希算法简介

五、分布式快照算法: Chandy-Lamport 算法

六、Kafka分区分配策略

七、分布式原理:一文了解 Gossip 协议

八、列式存储和行式存储它们真正的区别是什么

九、HBase Rowkey 设计指南

十、HBase 入门之数据刷写详细说明

十一、更多大数据文章欢迎访问https://www.iteblog.com及本公众号( iteblog_hadoop)十二、Flink中文文档:http://flink.iteblog.com1三、Carbondata 中文文档:http://carbondata.iteblog.com
相关文章
相关标签/搜索