6
大数据和空间限制

认识布隆过滤器

【题目】

不安全网页的黑名单包含100亿个黑名单网页,每个网页的URL最多占用64B。现在想要实现一种网页过滤系统,可以根据网页的URL判断该网页是否在黑名单上,请设计该系统。

【要求】

1.该系统允许有万分之一以下的判断失误率。

2.使用的额外空间不要超过30GB。

【难度】

尉 ★★☆☆

【解答】

如果把黑名单中所有的URL通过数据库或哈希表保存下来,就可以对每条URL进行查询,但是每个URL有64B,数量是100亿个,所以至少需要640GB的空间,不满足要求2。

如果面试者遇到网页黑名单系统、垃圾邮件过滤系统、爬虫的网址判重系统等题目,又看到系统容忍一定程度的失误率,但是对空间要求比较严格,那么很可能是面试官希望面试者具备布隆过滤器的知识。一个布隆过滤器精确地代表一个集合,并可以精确判断一个元素是否在集合中。注意,只是精确代表和精确判断,到底有多精确呢?则完全在于你具体的设计,但想做到完全正确是不可能的。布隆过滤器的优势就在于使用很少的空间就可以将准确率做到很高的程度,该结构由Burton Howard Bloom于1970年提出。

首先介绍哈希函数(散列函数)的概念。哈希函数的输入域可以是非常大的范围,比如,任意一个字符串,但是输出域是固定的范围,假设为S,并具有如下性质:

1.典型的哈希函数都有无限的输入值域。

2.当给哈希函数传入相同的输入值时,返回值一样。

3.当给哈希函数传入不同的输入值时,返回值可能一样,也可能不一样,这是当然的,因为输出域统一是S,所以会有不同的输入值对应在S中的一个元素上。

4.最重要的性质是很多不同的输入值所得到的返回值会均匀地分布在S上。

第1~3点性质是哈希函数的基础,第4点性质是评价一个哈希函数优劣的关键,不同输入值所得到的所有返回值越均匀地分布在S上,哈希函数越优秀,并且这种均匀分布与输入值出现的规律无关。比如,"aaa1"、"aaa2"、"aaa3"三个输入值比较类似,但经过优秀的哈希函数计算后的结果应该相差非常大。读者只用记清哈希函数的性质即可,有兴趣的读者可以了解一些哈希函数经典的实现,比如MD5和SHA1算法,但了解这些算法的细节并不在准备代码面试的范围中。如果一个优秀的哈希函数能够做到很多不同的输入值所得到的返回值非常均匀地分布在S上,那么将所有的返回值对m 取余(%m ),可以认为所有的返回值也会均匀地分布在0~m -1的空间上。这是显而易见的,本书不再详述。

接下来介绍一下什么是布隆过滤器。假设有一个长度为m 的bit类型的数组,即数组中的每一个位置只占一个bit,如我们所知,每一个bit只有0和1两种状态,如图6-1所示。

image

图6-1

再假设一共有k 个哈希函数,这些函数的输出域S都大于或等于m ,并且这些哈希函数都足够优秀,彼此之间也完全独立。那么对同一个输入对象(假设是一个字符串记为URL),经过k 个哈希函数算出来的结果也是独立的,可能相同,也可能不同,但彼此独立。对算出来的每一个结果都对m 取余(%m ),然后在bit array上把相应的位置设置为1(涂黑),如图6-2所示。

image

图6-2

我们把bit类型的数组记为bitMap。至此,一个输入对象对bitMap的影响过程就结束了,也就是bitMap中的一些位置会被涂黑。接下来按照该方法处理所有的输入对象,每个对象都可能把bitMap中的一些白位置涂黑,也可能遇到已经涂黑的位置,遇到已经涂黑的位置让其继续为黑即可。处理完所有的输入对象后,可能bitMap中已经有相当多的位置被涂黑。至此,一个布隆过滤器生成完毕,这个布隆过滤器代表之前所有输入对象组成的集合。

那么在检查阶段时,如何检查某一个对象是否是之前的某一个输入对象呢?假设一个对象为a,想检查它是否是之前的输入对象,就把a通过k 个哈希函数算出k 个值,然后把k 个值取余(%m ),就得到在[0,m-1]范围上的k 个值。接下来在bitMap上看这些位置是不是都为黑。如果有一个不为黑,说明a一定不在这个集合里。如果都为黑,说明a在这个集合里,但可能有误判。再解释具体一点,如果a的确是输入对象,那么在生成布隆过滤器时,bitMap中相应的k 个位置一定已经涂黑了,所以在检查阶段,a一定不会被漏过,这个不会产生误判。会产生误判的是,a明明不是输入对象,但如果在生成布隆过滤器的阶段因为输入对象过多,而bitMap过小,则会导致bitMap绝大多数的位置都已经变黑。那么在检查a时,可能a对应的k 个位置都是黑的,从而错误地认为a是输入对象。通俗地说,布隆过滤器的失误类型是“宁可错杀三千,绝不放过一个”。

布隆过滤器到底该怎么实现?读者已经注意到,如果bitMap的大小m 相比于输入对象的个数n 过小,失误率会变大。接下来先介绍根据n 的大小和我们想达到的失误率p ,如何确定布隆过滤器的大小m 和哈希函数的个数k ,最后是布隆过滤器的失误率分析。下面以本题为例来说明。

黑名单中样本的个数为100亿个,记为n ;失误率不能超过0.01%,记为p ;每个样本的大小为64B,这个信息不会影响布隆过滤器的大小,只和选择哈希函数有关,一般的哈希函数都可以接收64B的输入对象,所以使用布隆过滤器还有一个好处是不用顾忌单个样本的大小,它丝毫不能影响布隆过滤器的大小。

所以n =100亿,p =0.01%,布隆过滤器的大小m 由以下公式确定:

image

根据公式计算出m =19.19n ,向上取整为20n ,即需要2000亿个bit,也就是25GB。

哈希函数的个数由以下公式决定:

image

计算出哈希函数的个数为k =14个。

然后用25GB的bitMap再单独实现14个哈希函数,根据如上描述生成布隆过滤器即可。

因为我们在确定布隆过滤器大小的过程中选择了向上取整,所以还要用如下公式确定布隆过滤器真实的失误率为:

image

根据这个公式算出真实的失误率为0.006%,这是比0.01%更低的失误率,哈希函数本身不占用什么空间,所以使用的空间就是bitMap的大小(即25GB),服务器的内存都可以达到这个级别,所有要求达标。之后的判断阶段如上文的描述。

布隆过滤器失误率分析。假设布隆过滤器中的k 个哈希函数足够好且各自独立,每个输入对象都等概率地散列到bitMap中m 个bit中的任意k 个位置,且与其他元素被散列到哪儿无关。那么对某一个bit位来说,一个输入对象在被k 个哈希函数散列后,这个位置依然没有被涂黑的概率为:

image

经过n 个输入对象后,这个位置依然没有被涂黑的概率为:

image

那么被涂黑的概率就为:

image

那么在检查阶段,检查k 个位置都为黑的概率为:

image

x ->0时,(1+x )^(1/x )->e。上面等式的右边可以认为m 为很大的数,所以-1/m ->0,所以化简为:

image

有关布隆过滤器失误率的公式如上,上文最先提到的确定布隆过滤器大小m 及其哈希函数的个数k 的两个公式都是从这个公式出发才推出的,接下来展示一下推出的过程。首先我们分析一下,如果给定mn 的值,根据如上的失误率公式,k 取何值可使误判率最低?设误判率为k 的函数为:

image

b =e n/m ,则公式化简为:

f (k )=(1-b -k )k

两边取对数得到:

ln f (k )=k × ln(1-b - k )

两边对k 求导:

image

对等号右边的部分求最值:

image
image

至此,我们得到了如何根据mn 的值得到最合适的哈希函数数量k 的公式,把这个公式带回失误率公式,就得到了如何根据失误率p 和样本数n 来确定布隆过滤器大小m 的公式。

布隆过滤器会有误报,对已经发现的误报样本可以通过建立白名单来防止误报。比如,已经发现“aaaaaa5”这个样本不在布隆过滤器中,但是每次计算后的结果都显示其在布隆过滤器中,那么就可以把这个样本加入到白名单中,以后就可以知道这个样本确实不在布隆过滤器中。

在此特别感谢本篇文章参考网文的作者Allen Sun(http://www.cnblogs.com/allensun/archive/2011/02/16/1956532.html)。

只用2GB内存在20亿个整数中找到出现次数最多的数

【题目】

有一个包含20亿个全是32位整数的大文件,在其中找到出现次数最多的数。

【要求】

内存限制为2GB。

【难度】

士 ★☆☆☆

【解答】

想要在很多整数中找到出现次数最多的数,通常的做法是使用哈希表对出现的每一个数做词频统计,哈希表的key是某一个整数,value是这个数出现的次数。就本题来说,一共有20亿个数,哪怕只是一个数出现了20亿次,用32位的整数也可以表示其出现的次数而不会产生溢出,所以哈希表的key需要占用4B,value也是4B。那么哈希表的一条记录(key,value)需要占用8B,当哈希表记录数为2亿个时,需要至少1.6GB的内存。

但如果20亿个数中不同的数超过2亿种,最极端的情况是20亿个数都不同,那么在哈希表中可能需要产生20亿条记录,这样内存会不够用,所以一次性用哈希表统计20亿个数的办法是有很大风险的。

解决办法是把包含20亿个数的大文件用哈希函数分成16个小文件,根据哈希函数的性质,同一种数不可能被哈希到不同的小文件上,同时每个小文件中不同的数一定不会大于2亿种,假设哈希函数足够好。然后对每一个小文件用哈希表来统计其中每种数出现的次数,这样我们就得到了16个小文件中各自出现次数最多的数,还有各自的次数统计。接下来只要选出这16个小文件各自的第一名中谁出现的次数最多即可。

把一个大的集合通过哈希函数分配到多台机器中,或者分配到多个文件里,这种技巧是处理大数据面试题时最常用的技巧之一。但是到底分配到多少台机器、分配到多少文件,在解题时一定要确定下来。可能是在与面试官沟通的过程中由面试官指定,也可能是根据具体的限制来确定,比如本题确定分成16个文件,就是根据内存限制2GB的条件来确定的。

40亿个非负整数中找到没出现的数

【题目】

32位无符号整数的范围是0~4294967295,现在有一个正好包含40亿个无符号整数的文件,所以在整个范围中必然有没出现过的数。可以使用最多1GB的内存,怎么找到所有没出现过的数?

进阶:内存限制为10MB,但是只用找到一个没出现过的数即可。

【难度】

尉 ★★☆☆

【解答】

原问题。如果用哈希表来保存出现过的数,那么如果40亿个数都不同,则哈希表的记录数为40亿条,存一个32位整数需要4B,所以最差情况下需要40亿×4B=160亿字节,大约需要16GB的空间,这是不符合要求的。

哈希表需要占用很多空间,我们可以使用bit map的方式来表示数出现的情况。具体地说,是申请一个长度为4294967295的bit类型的数组bitArr,bitArr上的每个位置只可以表示0或1状态。8个bit为1B,所以长度为4294967295的bit类型的数组占用500MB空间。

怎么使用这个bitArr数组呢?就是遍历这40亿个无符号数,例如,遇到7000,就把bitArr[7000]设置为1。遇到所有的数时,就把bitArr相应位置的值设置为1。

遍历完成后,再依次遍历bitArr,哪个位置上的值没被设置为1,哪个数就不在40亿个数中。例如,发现bitArr[8001]==0,那么8001就是没出现过的数,遍历完bitArr之后,所有没出现的数就都找出来了。

进阶问题。现在只有10MB的内存,但也只要求找到其中一个没出现过的数即可。首先,0~4294967295这个范围是可以平均分成64个区间的,每个区间是67108864个数,例如:第0区间(0~67108863)、第1区间(67108864~134217728)、第i 区间(67108864×i ~67108864×(i +1)-1),……,第63区间(4227858432~4294967295)。因为一共只有40亿个数,所以,如果统计落在每一个区间上的数有多少,肯定有至少一个区间上的计数少于67108864。利用这一点可以找出其中一个没出现过的数。具体过程为:

第一次遍历时,先申请长度为64的整型数组countArr[0..63],countArr[i]用来统计区间i 上的数有多少。遍历40亿个数,根据当前数是多少来决定哪一个区间上的计数增加。例如,如果当前数是3422552090,3422552090/67108864=51,所以第51区间上的计数增加countArr[51]++。遍历完40亿个数之后,遍历countArr,必然会有某一个位置上的值(countArr[i])小于67108864,表示第i 区间上至少有一个数没出现过。我们肯定会至少找到一个这样的区间。此时使用的内存就是countArr的大小(64×4B),是非常小的。

假设我们找到第37区间上的计数小于67108864,以下为第二次遍历的过程:

1.申请长度为67108864的bit map,这占用大约8MB的空间,记为bitArr[0..67108863];

2.再遍历一次40亿个数,此时的遍历只关注落在第37区间上的数,记为num(num/67108864==37),其他区间的数全部忽略。

3.如果步骤2的num在第37区间上,将bitArr[num - 67108864*37]的值设置为1,也就是只做第37区间上的数的bitArr映射。

4.遍历完40亿个数之后,在bitArr上必然存在没被设置成1的位置,假设第i 个位置上的值没设置成1,那么67108864×37+i 这个数就是一个没出现过的数。

总结一下进阶的解法:

1.根据10MB的内存限制,确定统计区间的大小,就是第二次遍历时的bitArr大小。

2.利用区间计数的方式,找到那个计数不足的区间,这个区间上肯定有没出现的数。

3.对这个区间上的数做bit map映射,再遍历bit map,找到一个没出现的数即可。

找到100亿个URL中重复的URL以及搜索词汇的top K 问题

【题目】

有一个包含100亿个URL的大文件,假设每个URL占用64B,请找出其中所有重复的URL。

【补充题目】

某搜索公司一天的用户搜索词汇是海量的(百亿数据量),请设计一种求出每天最热top 100词汇的可行办法。

【难度】

士 ★☆☆☆

【解答】

原问题的解法使用解决大数据问题的一种常规方法:把大文件通过哈希函数分配到机器,或者通过哈希函数把大文件拆成小文件。一直进行这种划分,直到划分的结果满足资源限制的要求。首先,你要向面试官询问在资源上的限制有哪些,包括内存、计算时间等要求。在明确了限制要求之后,可以将每条URL通过哈希函数分配到若干机器或者拆分成若干小文件,这里的“若干”由具体的资源限制来计算出精确的数量。

例如,将100亿字节的大文件通过哈希函数分配到100台机器上,然后每一台机器分别统计分给自己的URL中是否有重复的URL,同时哈希函数的性质决定了同一条URL不可能分给不同的机器;或者在单机上将大文件通过哈希函数拆成1000个小文件,对每一个小文件再利用哈希表遍历,找出重复的URL;或者在分给机器或拆完文件之后,进行排序,排序过后再看是否有重复的URL出现。总之,牢记一点,很多大数据问题都离不开分流,要么是哈希函数把大文件的内容分配给不同的机器,要么是哈希函数把大文件拆成小文件,然后处理每一个小数量的集合。

补充问题最开始还是用哈希分流的思路来处理,把包含百亿数据量的词汇文件分流到不同的机器上,具体多少台机器由面试官规定或者由更多的限制来决定。对每一台机器来说,如果分到的数据量依然很大,比如,内存不够或其他问题,可以再用哈希函数把每台机器的分流文件拆成更小的文件处理。处理每一个小文件的时候,哈希表统计每种词及其词频,哈希表记录建立完成后,再遍历哈希表,遍历哈希表的过程中使用大小为100的小根堆来选出每一个小文件的top 100(整体未排序的top 100)。每一个小文件都有自己词频的小根堆(整体未排序的top 100),将小根堆里的词按照词频排序,就得到了每个小文件的排序后top 100。然后把各个小文件排序后的top 100进行外排序或者继续利用小根堆,就可以选出每台机器上的top 100。不同机器之间的top100再进行外排序或者继续利用小根堆,最终求出整个百亿数据量中的top 100。对于top K 的问题,除哈希函数分流和用哈希表做词频统计之外,还经常用堆结构和外排序的手段进行处理。

40亿个非负整数中找到出现两次的数和所有数的中位数

【题目】

32位无符号整数的范围是0~4294967295,现在有40亿个无符号整数,可以使用最多1GB的内存,找出所有出现了两次的数。

【补充题目】

可以使用最多10MB的内存,怎么找到这40亿个整数的中位数?

【难度】

尉 ★★☆☆

【解答】

对于原问题,可以用bit map的方式来表示数出现的情况。具体地说,是申请一个长度为4294967295×2的bit类型的数组bitArr,用2个位置表示一个数出现的词频,1B占用8个bit,所以长度为4294967295×2的bit类型的数组占用1GB空间。怎么使用这个bitArr数组呢?遍历这40亿个无符号数,如果初次遇到num,就把bitArr[num*2 + 1]和bitArr[num*2]设置为01,如果第二次遇到num,就把bitArr[num*2+1]和bitArr[num*2]设置为10,如果第三次遇到num,就把bitArr[num*2+1]和bitArr[num*2]设置为11。以后再遇到num,发现此时bitArr[num*2+1]和bitArr[num*2]已经被设置为11,就不再做任何设置。遍历完成后,再依次遍历bitArr,如果发现bitArr[i*2+1]和bitArr[i*2]设置为10,那么i 就是出现了两次的数。

对于补充问题,用分区间的方式处理,长度为2MB的无符号整型数组占用的空间为8MB,所以将区间的数量定为4294967295/2M,向上取整为2148个区间。第0区间为0~2M -1,第1区间为2M ~4M -1,第i 区间为2M ×i ~2M ×(i +1)-1……

申请一个长度为2148的无符号整型数组arr[0..2147],arr[i]表示第i 区间有多少个数。arr必然小于10MB。然后遍历40亿个数,如果遍历到当前数为num,先看num落在哪个区间上(num/2M),然后将对应的进行arr[num/2M]++操作。这样遍历下来,就得到了每一个区间的数的出现状况,通过累加每个区间的出现次数,就可以找到40亿个数的中位数(也就是第20亿个数)到底落在哪个区间上。比如,0~K -1区间上数的个数为19.998亿,但是发现当加上第K 个区间上数的个数之后就超过了20亿,那么可以知道第20亿个数是第K 区间上的数,并且可以知道第20亿个数是第K 区间上的第0.002亿个数。

接下来申请一个长度为2MB的无符号整型数组countArr[0..2M-1],占用空间8MB。然后再遍历40亿个数,此时只关心处在第K 区间的数记为numi,其他的数省略,然后将countArr[numi-K*2M]++,也就是只对第K 区间的数做频率统计。这次遍历完40亿个数之后,就得到了第K 区间的词频统计结果countArr,最后只在第K 区间上找到第0.002亿个数即可。

一致性哈希算法的基本原理

【题目】

工程师常使用服务器集群来设计和实现数据缓存,以下是常见的策略:

1.无论是添加、查询还是删除数据,都先将数据的id通过哈希函数转换成一个哈希值,记为key。

2.如果目前机器有N 台,则计算key%N的值,这个值就是该数据所属的机器编号,无论是添加、删除还是查询操作,都只在这台机器上进行。

请分析这种缓存策略可能带来的问题,并提出改进的方案。

【难度】

尉 ★★☆☆

【解答】

题目中描述的缓存策略的潜在问题是如果增加或删除机器时(N 变化)代价会很高,所有的数据都不得不根据id重新计算一遍哈希值,并将哈希值对新的机器数进行取模操作,然后进行大规模的数据迁移。

为了解决这些问题,下面介绍一下一致性哈希算法,这是一种很好的数据缓存设计方案。我们假设数据的id通过哈希函数转换成的哈希值范围是232 ,也就是0~(232 )-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形,那么一个数据id在计算出哈希值之后认为对应到环中的一个位置上,如图6-3所示。

image

图6-3

接下来想象有三台机器也处在这样一个环中,这三台机器在环中的位置根据机器id计算出的哈希值来决定。那么一条数据如何确定归属哪台机器呢?首先把该数据的id用哈希函数算出哈希值,并映射到环中的相应位置,然后顺时针找寻离这个位置最近的机器,那台机器就是该数据的归属,如图6-4所示。

image

图6-4

在图6-4中,data1根据其id计算出的哈希值为key1,顺时针的第一台机器是machine2,所以data1归属machine2;同理,data2归属machine3,data3和data4都归属machine1。

增加机器时的处理。假设有两台机器(m1、m2)和三个数据(data1、data2、data3),数据和机器在环中的结构如图6-5所示。

image

图6-5

如果此时想加入新的机器m3,同时算出机器m3的id在m1与m2右半侧的环中,那么发生的变化如图6-6所示。

image

图6-6

在没有添加m3之前,从m1到现在m3位置上的这一段是m2掌管范围的一部分;添加m3之后则统一归属于m3,同时要把这一段旧数据从m2迁移到m3上。由此可见,添加机器时的调整代价是比较小的。在删除机器时也一样,只要把要删除机器的数据全部复制到顺时针找到的下一台机器上即可。比如,要在图6-6中删除机器m2,m2上有数据data2,那么只用把data2迁移到m1上即可。

机器负载不均时的处理。如果机器较少,很有可能造成机器在整个环上的分布不均匀,从而导致机器之间的负载不均衡,比如,图6-7所示的两台机器,m1可能比m2面临更大的负载。

image

图6-7

为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一台机器通过不同的哈希函数计算出多个哈希值,对多个位置都放置一个服务节点,称为虚拟节点。具体做法可以在机器ip或主机名的后面增加编号或端口号来实现。以图6-7的情况,可以为每台机器计算两个虚拟节点,分别计算m1-1、m1-2、m2-1和m2-2的哈希值,于是形成四个虚拟节点,节点数变多了,根据哈希函数的性质,平衡性自然会变好,如图6-8所示。

image

图6-8

此时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,比如下表:

虚拟节点 对应的实际节点
m1-1 m1
m1-2 m1
m2-1 m2
m2-2 m2

当某一条数据计算出归属于m1-2时,再根据上表的转跳,数据将最终归属于实际的m1节点。基于一致性哈希的原理有多种具体的实现,包括Chord算法、KAD算法等。有兴趣的读者可以进一步学习,本书由于篇幅所限,在此不再详述。