今天接到交付中心客户支持团队C同学的电话,从客户那里收集到了一个需求,客户想将APP埋点数据能够在服务端进行动态的管理控制,能够控制APP某个埋点事件是否收集并上报。后来经过和客户进行详细的沟通确认后了解到客户的需求具体如下:
- 客户APP已经有2万多个种类的埋点事件,需要通过服务端对每个种类的埋点事件进行控制管理APP是否进行采集;
- 兼容性:能够对不同APP版本的埋点事件兼容处理;
- 准确性:方案需要考虑埋点事件控制管理的准确性;
- 性能方面:服务端传输给APP的埋点事件控制表单大小和对APP性能的影响要求最小。
确认最终需求后,首先在我脑海里浮现的可行方案是:布隆过滤器。之前我对布隆过滤器只是了解大概,针对这个需求对布隆过滤器原理进行了学习,借此将布隆过滤器的原理、使用场景和注意事项整理后和大家分享,希望非技术研发人员也能够通过这篇文章知道什么是布隆过滤器及其基本原理。
举个例子,像 Yahoo,Hotmail 和 Gmail 这样的公众电子邮件提供商,总是需要过滤来自发送垃圾邮件的人(spammer)的垃圾邮件。为了避免使用者受到垃圾邮件的困扰,一个最简单的处理办法就是记录下所有那些发过垃圾邮件的 email 地址。但是由于那些发送者不停地在注册新的地址,全世界少说也有几十亿个发垃圾邮件的地址,将他们都存起来则需要大量的网络服务器。如果用哈希表,每存储一亿个email地址, 就需要1.6GB 的内存(用哈希表实现的具体办法是将每一个 email 地址对应成一个八字节的信息指纹,然后将这些信息指纹存入哈希表,由于哈希表的存储效率一般只有50%,因此一个email 地址需要占用十六个字节。一亿个地址大约要1.6GB, 即十六亿字节的内存)。因此存贮几十亿个邮件地址可能需要上百GB的内存。除非是超级计算机,一般服务器是无法存储的。
而使用布隆过滤器,它只需要哈希表1/8到1/4的大小就能解决同样的问题。正式介绍布隆过滤器之前,先摘抄一句话:
Data structures are nothing different. They are like the bookshelves of your application where you can organize your data. Different data structures will give you different facility and benefits. To properly use the power and accessibility of the data structures you need to know the trade-offs of using one.
不同的数据结构有不同的适用场景和优缺点,你需要针对自己的需求仔细权衡之后妥善使用它们。
布隆过滤器就是践行这句话的代表之一。
什么是布隆过滤器
布隆过滤器是由巴顿.布隆于1970年提出的。它是一个很长的二进制向量和一系列随机映射函数。本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,这个过滤器可以用来告诉你 “判断某样东西一定不存在或者可能存在”。注意这句话比较关键,后面会解释为什么说“可能存在”。
为了方便理解,我们继续上面邮件的例子:
假定我们存储一亿个电子邮件地址,我们先建立一个十六亿二进制(bit),即两亿字节的向量,然后将这十六亿个二进制全部设置为零。对于每一个电子邮件地址 X,我们用八个不同的随机数产生器(F1,F2, ...,F8) 产生八个信息指纹(f1, f2, ..., f8),再用一个随机数产生器 G 把这八个信息指纹映射到 1 到十六亿中的八个自然数 g1, g2, ...,g8。现在我们把这八个位置的二进制全部设置为1。当我们对这一亿个email地址都进行这样的处理后。一个针对这些email地址的布隆过滤器就建成了。
如何用布隆过滤器来检测一个可疑的电子邮件地址 Y 是否在黑名单中?原理还是用相同的八个随机数产生器(F1, F2, ..., F8)对这个地址产生八个信息指纹 f1,f2,...,f8,然后将这八个指纹对应到布隆过滤器的八个二进制位,分别是 g1,g2,...,g8。如果 Y 在黑名单中,显然,g1,g2,..,t8 对应的八个二进制一定是1。这样在遇到任何在黑名单中的电子邮件地址,我们都能准确地发现。
上面的布隆过滤器决不会漏掉任何一个在黑名单中的可疑邮件地址。但是,它有一条不足之处:也就是它有极小的可能将一个不在黑名单中的电子邮件地址判定为在黑名单中,因为有可能某个正常的邮件地址通过转换后g1,g2,...,g8,正巧对应个八个都被设置成1的二进制位。好在这种可能性很小。我们把它称为误报概率。在上面的例子中,误报概率在万分之一以下。
布隆过滤器的好处在于快速,省空间。但是有一定的误识别率。常见的补救办法是在建立一个小的白名单,存储那些可能别误判的邮件地址。
如果你读到这里还是没有完全理解布隆过滤器,没有关系,这里有一个Demo,访问此链接:Bloom Filters,可以自己动手去创建一个布隆过滤器。(不用担心,不需要写代码哈)。
比如我根据下面一段话“TalkingData is China's leading third-party data intelligence solution provider”每个单词作为一个Key添加,创建好布隆过滤器,然后再输入一个单词,让创建好的布隆过滤器去判断是否这个单词存在于之中,最后效果图如下。
布隆过滤器长度和如何选择哈希函数个数
上面邮件例子中,我们用八个不同的随机数产生器(F1,F2, ...,F8) 产生八个信息指纹(f1, f2, ..., f8),再用一个随机数产生器 G 把这八个信息指纹映射到 1 到十六亿中的八个自然数 g1, g2, ...,g8。这个随机数产生器我们称之为哈希函数。
很显然,过小的布隆过滤器很快所有的bit位均为1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置为 1 的速度越快,且布隆过滤器的效率越低,但是如果太少的话,那我们的误报率会变高。他们之间的关系如下:
k 为哈希函数个数,m 为布隆过滤器bit长度,n 为插入的元素个数,p 为误报率。至于这个公式的推导过程,感兴趣可以看看https://en.wikipedia.org/wiki/Bloom_filter,不感兴趣的话记住上面这个公式就行了。
这里有一张直观的图表对误报率p统计可以参考:
支持删除么
目前我们知道布隆过滤器可以支持 add 和 isExist 操作,那么 delete 操作可以么,答案是不可以,例如对于两个Key的输入bit 位如果被两个值共同覆盖的话,一旦你删除其中一个值的bit位而将其置位 0,那么下次判断另一个值例如是否存在的话,会直接返回 false,而实际上是存在的。
如何解决这个问题,答案是计数删除。但是计数删除需要存储一个数值,而不是原先的 bit 位,会增大占用的内存大小。这样的话,增加一个值就是将对应索引槽上存储的值加一,删除则是减一,判断是否存在则是看值是否大于0。
最佳实践
使用布隆过滤器来加速查找和判断是否存在,那么性能很低的哈希函数不是个好选择,推荐 MurmurHash:https://sites.google.com/site/murmurhash/ 。
布隆过滤器开源库:Google Guava, GitHub - google/guava: Google core libraries for Java。
参考资料
http://googlechinablog.com/2007/07/bloom-filter.html