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,说明它确实具备去重功能。

参考资料

(转自:https://juejin.cn/post/6992890603503632415)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值