hyperloglog实战 - 你的过滤器选对了吗?

一、业务背景

最近接到一个统计需求,为了监控视频效果,我们每次浏览视频都要记录uv。当量小的时候,没有问题,随着用户量的的增长,占用空间大的问题开始暴露。假如有1000万用户,采用set结构记录UV,我们只存储int类型的用户id,1000万*4byte/1024kb/1024mb=38.14G,这是一个视频一天的数据,很明显存储成本太高,必须找到一种既能满足需求,又很少占用空间的方案。

二、技术选型

经过调研,找到了以下几种技术方案,下面一一分析。

1、flink的state去重

MapState 是 Flink 中 KeyedState 的状态类型,这种方式实现去重是一个精确的去重结果,将设备 ID 保存在 MapState 中。而state 直接存放在 RocksDB 中,RocksDB是类似HBase的kv数据库,本地性能好,维护较大的状态集合问题不大。

这种方案的优点是数据准确定非常高,确定是随着state增长,对checkpoint效率和成功率都有挑战,而且需要定期清理state,无发做到长时间数据去重。

2、bitmap去重

bitmap的原理是使用一个bit来存放某种状态,适用于大规模数据,但是状态又不是很多的情况。假设使用redis的bitmap,首先将用户id通过算法转换为整数,这个整数就是偏移量,每个id占用一位,1是访问过。

这个方案的优点是占用内存小,查询方便,效率高,能够精确去重,缺点是需要计算偏移量,如果偏移量过大就占用很多空间。

3、布隆过滤器

布隆过滤器是由一个很长的二进制向量和一系列随机映射函数。它适合过滤不存的元素场景,如果判断不存在则一定不存在,如果判断存在,则有一定概率不存在。计算puv时,用一个变量计数,用这个过滤器判断计数器是否增加。

这个方案的优点是空间效率高,查询时间短。缺点是有误判率,多个维度时,初始分配的空间一样,会占用较大空间。

4、hyperloglog

hyperloglog是一种算法,目的是做基数统计,可以预估一个集合中不同数据的个数,它不会保存元数据,只记录数量,支持输入非常大体积的数据量。在 redis 中也存在 hyperloglog 类型的结构,能够使用 12k 的内存,允许误差在 0.81%的情况下统计 2^64 个数据。

这个方案的有点事占用空间小,空间不会随着数量的变大而无线变大。缺点是有0.81%的标准误差率,只能用来计数,不能用来查询元素。

综合上面的方案,对于1000万用户,统计上亿视频,允许一定误差率的场景,方案 4 更加合适,内存能够控制在12k以内,查询速度也能满足需求。

三、hyperloglog 原理分析

1、原理说明

HyperLogLog 算法来源于论文 HyperLogLog the analysis of a near-optimal cardinality estimation algorithm,想要了解 HLL 的原理,先要从伯努利试验说起,伯努利实验说的是抛硬币的事。一次伯努利实验相当于抛硬币,不管抛多少次只要出现一个正面,就称为一次伯努利实验。

我们用 k 来表示每次抛硬币的次数,n 表示第几次抛的硬币,用 k_max 来表示抛硬币的最高次数,最终根据估算发现 n 和 k_max 存在的关系是 n=2^(k_max),但同时我们也发现了另一个问题当试验次数很小的时候,这种估算方法的误差会很大,例如我们进行以下 3 次实验:

  • 第 1 次试验:抛 3 次出现正面,此时 k=3,n=1;
  • 第 2 次试验:抛 2 次出现正面,此时 k=2,n=2;
  • 第 3 次试验:抛 6 次出现正面,此时 k=6,n=3。

对于这三组实验来说,k_max=6,n=3,但放入估算公式明显 3≠2^6。为了解决这个问题 HLL 引入了分桶算法和调和平均数来使这个算法更接近真实情况。

分桶算法是指把原来的数据平均分为 m 份,在每段中求平均数在乘以 m,以此来消减因偶然性带来的误差,提高预估的准确性,简单来说就是把一份数据分为多份,把一轮计算,分为多轮计算。

而调和平均数指的是使用平均数的优化算法,而非直接使用平均数。

例如小明的月工资是 1000 元,而小王的月工资是 100000 元,如果直接取平均数,那小明的平均工资就变成了 (1000+100000)/2=50500 元,这显然是不准确的,而使用调和平均数算法计算的结果是 2/(1/1000+1/100000)≈1998 元,显然此算法更符合实际平均数。

2、具体的计算流程

这是hyperloglog的在线计算网站: http://content.research.neustar.biz/blog/hll.html

hy.png
  • 第一步对输入的值3,157,369进行hash获得值2,297,270,962,hash的值转二进制 11100000101001010101001011101110100111010
  • 第二步二进制数的最后6位设置为桶的index(长16,宽4的矩形),为什么是6位呢?因为2的6次方正好等于矩阵数组的个数64,从图中看出index是50,红色的方块是要放入的位置
  • 第三步获取从右向左的第一个1,如图看到是10,也就是2
  • 最后一步多个不同的值,就被分散到不同的桶中去了,且每个桶有其 k_max。然后当要统计出总共有多少的时候,就是一次估算。最终结合所有桶中的 k_max,代入估算公式,便能得出估算值。

四、hyperloglog redis实战

在 Redis 的 HyperLogLog 实现中用到的是 16384 个桶,也就是 2^14,每个桶的 maxbits 需要 6 个 bits 来存储,最大可以表示 maxbits=63,于是总共占用内存就是2^14 * 6 / 8 = 12k字节。\

HyperLogLog 提供了两个指令 pfadd 和 pfcount,根据字面意义很好理解,一个是增加计数,一个是获取计数。pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用户 ID 塞进去就是。pfcount 和 scard 用法是一样的,直接获取计数值。

127.0.0.1:6379> PFADD runoobkey "redis"

(integer) 1

127.0.0.1:6379> PFADD runoobkey "mongodb"

(integer) 1

127.0.0.1:6379> PFADD runoobkey "mysql"

(integer) 1

127.0.0.1:6379> PFCOUNT runoobkey

我们将数据增加到 10w 个,看看总量差距有多大。

public class Test {

    public static void main(String[] args) {

        //连接本地的redis

        Jedis jedis = new Jedis("localhost");

        System.out.println(jedis.getClient().getPort());

        System.out.println("连接本地的Redis服务器成功");


        for (int i = 0; i < 100000; i++) {

            jedis.pfadd("my-user-uv", "user" + i);

        }

        long total = jedis.pfcount("my-user-uv");

        System.out.printf("%d %d\n", 100000, total);

        jedis.close();

    }

}

差了 275 个,按百分比是 0.275%,对于上面的 UV 统计需求来说,误差率也不算高。然后我们把上面的脚本再跑一边,也就相当于将数据重复加入一边,查看输出,可以发现,pfcount 的结果没有任何改变,还是 99725,说明它确实具备去重功能。

参考资料

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

推荐阅读更多精彩内容