使用Bloom Filters

仙子注:這篇文章是半年前翻譯的,最先貼於公司內部的BBS上,並引發一些爭論。Bloom Filters是一種效率較高的內存索引算法,它自己具備矛盾性:一方面能快速測試目標成員是否存在,另外一方面又不可避免的具備假命中率。以下文檔僅供參考。 
因爲不知道如何在這裏粘貼圖片,所以本文中沒有包含圖片說明,請對照原文檔來閱讀,原文檔在:http://www.perl.com/pub/a/2004/04/08/bloom_filters.html?page=1   或可email給我索取中文PDF文檔。 


使用Bloom Filters 

原做者:Maciej Ceglowski 
April 08, 2004  


任何perl使用者都熟悉hash查詢,一個存在測試的語句能夠這樣寫: 

foreach my $e ( @things ) { $lookup{$e}++ } 

sub check { 
my ( $key ) = @_; 
print "Found $key!" if exists( $lookup{ $key } ); 


雖然hash查詢頗有用,但對很是大的列表,或keys自身很是大時,這種查詢可能變得不實用。當查詢hash增加得太大,一般的作法是將它移到數據庫或文件中,只在本地緩存裏保存最經常使用的關鍵字,這樣能改善性能。 

許多人不知道有一種優雅的算法,用以代替hash查詢。它是一種古老的算法,叫作Bloom filter。 Bloom filter容許你在有限的內存裏(你想在這塊內存裏存放關鍵字的完整列表),執行成員測試,這樣就能避開使用磁盤或數據庫進行查詢的性能瓶頸。也許你會認爲,空間的節省是有代價的:存在着可大可小的假命中率風險,而且一旦你增長key到filter後,就不能刪除它。然而在許多情形下,這些侷限是可接受的,bloom filter能編制有用工具。(仙子注:例如代理服務器軟件Squid就使用了bloom filter算法。) 

例如,假如你運行了一個高流量的在線音樂存儲站點,而且若是你已知歌曲存在,就能夠經過僅獲取歌曲信息的方法,來最大程度的減小數據庫壓力。你能夠在啓動時構建一個bloom filter,在試圖執行昂貴的數據庫查詢前,能夠用它執行快速的成員存在測試。 

use Bloom::Filter; 

my $filter = Bloom::Filter->new( error_rate => 0.01, capacity => $SONG_COUNT ); 
open my $fh, "enormous_list_of_titles.txt" or die "Failed to open: $!"; 

while (<$fh>) { 
chomp; 
$filter->add( $_ ); 


sub lookup_song { 
my ( $title ) = @_; 
return unless $filter->check( $title ); 
return expensive_db_query( $title ) or undef; 


在該示例裏,該測試給出假命中的概率是1%,在假命中率狀況下程序會執行昂貴的數據庫索取操做,並最終返回空結果。儘管如此,你已避開了99%的昂貴查詢時間,僅使用了用於hash查詢的一小片內存。更進一步,1%假命中率的filter,每一個key的存儲空間在2字節如下。這比你執行完整的hash查詢所需的內存少得多。 

bloom filters在Burton Bloom以後命名,Burton Bloom 1970年首先在文檔裏描述了它們,文檔名Space/time trade-offs in hash coding with allowable errors.在那些內存稀少的日子裏,bloom filters因其簡潔而倍受重視。事實上,最先的應用之一是拼寫檢查程序。然而,因爲有少數很是明顯的特性,該算法特別適合社會軟件應用。 

由於bloom filters使用單向hash來存儲數據,所以不可能在不作窮舉搜索的狀況下,重建filter裏的keys列表。甚至這點看起來並不是象頗有用,既然來自窮舉搜索的假命中會覆蓋掉真正的keys列表。因此bloom filters能在不向全世界廣播完整列表的狀況下,共享關於已有資料的信息。由於這個理由,它們在peer-to-peer應用中特別有用,在這個應用中大小和隱私是重要的約束。 



bloom filters如何工做 

bloom filter由2部分組成:1套k hash函數,1個給定長度的位向量。選擇位向量的長度,和hash函數的數量,依賴於咱們想增長多少keys到設置中,以及咱們能容忍的多高的假命中率。 

bloom filter中全部的hash函數被配置過,其範圍匹配位向量的長度。例如,假如向量是200位長,hash函數返回的值就在1到200之間。在filter裏使用高質量的hash函數至關重要,它保證輸出等分在全部可能值上--hash函數裏的「熱點」會增長假命中率。(仙子注:所謂「熱點」是指結果過度頻繁的分佈在某些值上。) 

要將某個key輸入bloom filer中,咱們在每一個k hash函數裏遍歷它,並將結果做爲在位向量裏的offsets,並打開咱們在該offsets上找到的任何位。假如該位已經設置,咱們繼續保留其打開。尚未在bloom filter裏關閉位的機制。 

在本示例裏,讓咱們看看某個bloom filter,它有3個hash函數,而且位向量的長度是14。咱們用空格和星號來表示位向量,以便於觀察。你也許想到,空的bloom filter以全部的位關閉爲開始,如圖1所示。 



圖1:空的bloom filter 

如今咱們將字符apples增長到filter中去。爲了作到這點,咱們以apples爲參數來運行每一個hash函數,並採集輸出: 

hash1("apples") = 3 
hash2("apples") = 12 
hash3("apples") = 11 

而後咱們打開在向量裏相應位置的位--在這裏就是位3,11,和12,如圖2所示。 



圖2:激活了3位的bloom filter 

爲了增長另1個key,例如plums,咱們重複hash運算過程: 

hash1("plums") = 11 
hash2("plums") = 1 
hash3("plums") = 8 

再次打開向量裏相應的位,如圖3裏的高亮度顯示。 html



圖3:增長了第2個key的bloom filter 

注意位置11的位已被打開--在前面的步驟裏,當咱們增長apples時已設置了它。位11如今有雙重義務,存儲apples和plums二者的信息。當增長更多的keys時,它也會存儲其餘keys的信息。這種交迭讓bloom filters如此緊湊--任何位同時編碼多個keys。這種交迭也意味着你永不能從filter裏取出key,由於你不能保證你所關閉的位沒有攜載其餘keys的信息。假如咱們試圖執行反運算過程來從filter裏刪除apples,就會不經意的關閉編碼plums的1個位。從bloom filter裏剝離key的惟一方法是重建filter,剔除無用key。 

檢查是否某個key已經存在於filter的過程,很是相似於增長新key。咱們在全部的hash函數裏遍歷key,而後檢查是否在那些offsets上的位都是打開的。假如任何一位關閉,咱們知道該key確定不存在於filter中。假如全部位都打開,咱們知道該key可能存在。 

我說「可能」是由於存在一種狀況,該key是個假命中。例如,假如咱們用字符mango來測試filter,看看會發生什麼狀況。咱們運行mango遍歷hash函數: 

hash1("mango") = 8 
hash2("mango") = 3 
hash3("mango") = 12 

而後檢查在那些offsets上的位,如圖4所示。 



圖4:bloom filter的假命中  全部在位置3,8,和12的位都是打開的,故filter會報告mango是有效key。  固然,mango並不是有效key--咱們構建的filter僅包含apples和plums。事實是mango的offsets很是巧合的指向了已激活的位。這就找到了1個假命中--某個key看起來位於filter中,但實際不是。  正如你想的同樣,假命中率依賴於位向量的長度和存儲在filter裏的keys的數量。位向量越寬闊,咱們檢查的全部k位被打開的可能性越小,除非該key確實存在於filter中。在hash函數的數量和假命中率之間的關係更敏感。假如使用的hash函數太少,在keys之間的差異就不多;但假如使用hash函數太多,filter會過於密集,增長了衝突的可能性。可使用以下公式來計算任何filter的假命中率:  c = ( 1 - e(-kn/m) )k  這裏c是假命中率,k是hash函數的數量,n是filter裏keys的數量,m是filter的位長。  當使用bloom filters時,咱們先要有個意識,期待假命中率多大;也應該有個粗糙的想法,關於多少keys要增長到filter裏。咱們須要一些方法來驗證須要多大的位向量,以保證假命中率不會超出咱們的限制。下列方程式會從錯誤率和keys數量求出向量長度:  m = -kn / ( ln( 1 - c ^ 1/k ) )  請注意另1個自由變量:k,hash函數的數量。能夠用微積分來得出k的最小值,但有個偷懶的方法來作它:  sub calculate_shortest_filter_length {  my ( $num_keys, $error_rate ) = @_;  my $lowest_m;  my $best_k = 1;  foreach my $k ( 1..100 ) {  my $m = (-1 * $k * $num_keys) /   ( log( 1 - ($error_rate ** (1/$k))));  if ( !defined $lowest_m or ($m < $lowest_m) ) {  $lowest_m = $m;  $best_k   = $k;  }  }  return ( $lowest_m, $best_k );  }  爲了給你直觀的感受,關於錯誤率和keys數量如何影響bloom filters的存儲size,表1列出了一些在不一樣的容量/錯誤率組合下的向量size。  ErrorRate   Keys   RequiredSize   Bytes/Key   1%           1K       1.87 K         1.9   0.1%         1K       2.80 K         2.9   0.01%        1K       3.74 K         3.7   0.01%       10K       37.4 K         3.7   0.01%      100K        374 K         3.7   0.01%        1M       3.74 M         3.7   0.001%       1M       4.68 M         4.7   0.0001%      1M       5.61 M         5.7   在Perl裏構建bloom filter  爲了構建1個工做bloom filter,咱們須要1套良好的hash函數。這些容易解決--在CPAN上有幾個優秀的hash算法可用。對咱們的目的來講,較好的選擇是Digest::SHA1,它是強度加密的hash,用C實現速度很快。經過對不一樣值的輸出列表進行排序,咱們能使用該模塊來建立任意數量的hash函數。以下是構建惟一hash函數列表的子函數:  use Digest::SHA1 qw/sha1/;  sub make_hashing_functions {  my ( $count ) = @_;  my @functions;  for my $salt (1..$count ) {  push @functions, sub { sha1( $salt, $_[0] ) };  }  return @functions;  }  爲了可以使用這些hash函數,咱們必須找到1個方法來控制其範圍。Digest::SHA1返回使人爲難的過長160位hash輸出,這僅在向量長度爲2的160次方時有用,而這種狀況實在罕見。咱們結合使用位chopping和division來將輸出削減到可用大小。  以下子函數取某個key,運行它遍歷hash函數列表,並返回1個長度($FILTER_LENGTH)的位掩碼:  sub make_bitmask {  my ( $key ) = @_;  my $mask    = pack( "b*", '0' x $FILTER_LENGTH);  foreach my $hash_function ( @functions ){   my $hash       = $hash_function->($key);  my $chopped    = unpack("N", $hash );  my $bit_offset = $result % $FILTER_LENGTH;  vec( $mask, $bit_offset, 1 ) = 1;         }  return $mask;  }  讓咱們逐行分析上述代碼:  my $mask = pack( "b*", '0' x $FILTER_LENGTH);  咱們以使用perl的pack操做來建立零位向量開始,它是$FILTER_LENGTH長。pack取2個參數,1個模型和1個值。b模型告訴pack將值解釋爲bits,*指「重複任意多須要的次數」,跟正則表達式相似。perl實際上會補充位向量的長度爲8的倍數,但咱們將忽視這些多餘位。  有1個空的位向量在手中,咱們準備開始運行key遍歷hash函數:  my $hash = $hash_function->($key);  my $chopped = unpack("N", $hash );  咱們保存首個32位輸出,並丟棄剩下的。這點可以讓咱們沒必要要求BigInt支持。第2行作實際的位chopping。模型裏的N告訴unpack以網絡字節順序來解包32位整數。由於未在模型裏提供任何量詞,unpack僅解包1個整數,而後終止。  假如你對位chopping過分狂熱,你能夠將hash分割成5個32位的片段,並對它們一塊兒執行OR運算,將全部信息保存在原始hash裏:  my $chopped = pack( "N", 0 );  my @pieces  =  map { pack( "N", $_ ) } unpack("N*", $hash );  $chopped    = $_ ^ $chopped foreach @pieces;  但這樣做可能殺傷力過分。  如今咱們有了來自hash函數的32位整數輸出的列表,下一步必須作的是,裁減它們的大小,以使其位於(1..$FILTER_LENGTH)範圍內。  my $bit_offset = $chopped % $FILTER_LENGTH;  如今咱們已轉換key爲位offsets列表,這正是咱們所求的。  剩下惟一要作的事情是,使用vec來設置位,vec取3個參數:向量自身,開始位置,要設置的位數量。咱們能象賦值給變量同樣來分配值給vec:  vec( $mask, $bit_offset, 1 ) = 1;  在設置了全部位後,咱們以1個位掩碼來結束,位掩碼和bloom filter長度同樣。咱們可使用這個掩碼來增長key到filter中:  sub add {  my ( $key, $filter ) = @_;  my $mask = make_bitmask( $key );  $filter  = $filter | $mask;  }  或者咱們使用它來檢查是否key已存在:  sub check {  my ( $key, $filter ) = @_;  my $mask  = make_bitmask( $key );  my $found = ( ( $filter & $mask ) eq $mask );  return $found;  }  注意這些是位邏輯運算符OR(|)和AND(&),而並不是通用的邏輯OR(||)和AND(&&)運算符。將這二者混在一塊兒,會致使數小時的有趣調試。第1個示例將掩碼和位向量進行OR運算,打開任何未設置的位。第2個示例將掩碼和filter裏相應的位置進行比較--假如掩碼裏全部的打開位也在filter裏打開,咱們知道已找到一個匹配。  一旦你克服了使用vec,pack和位邏輯運算符的難度,bloom filters實際很是簡單。http://www.perl.com/2004/04/08/examples/Filter.pm 這裏給出了Bloom::Filter模塊的完整信息。  分佈式社會網絡中的bloom filters  當前的社會網絡機制的弊端之一是,它們要求參與者泄露其聯繫列表給中央服務器,或公佈它到公共Internet,這2種狀況下都犧牲了大量的用戶隱私。經過交換bloom filters而不是暴露聯繫列表,用戶能參與社會網絡實踐,而不用通知全世界他們的朋友是誰。編碼了某人聯繫信息的bloom filter能用來檢查它是否包含了給定的用戶名或email地址,但不能強迫要求它展現用於構建它的完整keys列表。甚至有可能將假命中率(雖然它聽起來不像好特性),轉換爲有用工具。  假如我很是關注這些人,他們經過對bloom filter運行字典攻擊,來試圖對社會網絡進行反工程。我能夠構建filter,它具有較高的假命中率(例如50%),而後發送filter的多個拷貝給朋友,並變換用於構建每一個filter的hash函數。個人朋友收集到的filters越多,他們見到的假命中率越低。例如,在5個filters狀況下,假命中率是0.5的5次方,或3%--經過發送更多filters,還能進一步減小假命中率。  假如這些filters中的任何一個被中途截取,它會展現所有50%的假命中率。因此我能隔離隱私風險,而且必定程度上能控制其餘人能多清楚的  瞭解個人網絡。個人朋友能較高程度的確認是否某我的位於聯繫列表裏,但那些僅截取了1個或2個filters的人,幾乎不會獲取到什麼。以下是個perl函數,它對1組嘈雜的filters檢查某個key:  use Bloom::Filter;            sub check_noisy_filters {  my ( $key, @filters ) = @_;  foreach my $filter ( @filters ) {  return 0 unless $filter->check( $key );  }  return 1;  }  假如你和你的朋友贊成使用相同的filter長度和hash函數設置,你也能使用位掩碼對比來估計在大家的社會網絡之間的交迭程度。在2個bloom filters裏的共享位數量會給出1個可用的距離度量。  sub shared_on_bits {  my ( $filter_1, $filter_2 ) = @_;  return unpack( "%32b*",  $filter_1 & $filter_2 )  }  另外,你能使用OR運算,結合2個有相同長度和hash函數的bloom filters來建立1個複合filter。例如,假如你參與某個小型郵件列表,並但願基於組裏每一個人的地址原本建立白名單,你能夠爲每一個參與者獨立的建立1個bloom filter,而後將filters一塊兒進行OR運算,將結果輸入Voltron-like主列表。組裏成員不會了解到其餘成員的聯繫信息,而且filter仍能展現正確的行爲。  確定還有其餘針對社會網絡和分佈式應用的bloom filter妙用。以下參考列出一些有用資源。  參考  · Bloom Filters -- the math. A good place to start for an overview of the math behind Bloom filters.   · Some Motley Bloom Tricks. Handy filter tricks and theory page.   · Bloom Filter Survey. A handy survey article on Bloom filter network applications.   · LOAF. Our own system for incorporating social networks onto email using Bloom filters.   · Compressed Bloom Filters. If you are passing filters around a network, you will want to optimize them for minimum size; this paper gives a good overview of compressed Bloom filters.   · Bloom16. A CPAN module implementing a counting Bloom filter.   · Text::Bloom. CPAN module for using Bloom filters with text collections.   · Privacy-Enhanced Searches Using Encryted Bloom Filters. This paper discusses how to use encryption and Bloom filters to set up a query system that prevents the search engine from knowing the query you are running.   · Bloom Filters as Summaries. Some performance data on actually using Bloom filters as cache summaries.   · Using Bloom Filters for Authenticated Yes/No Answers in the DNS. Internet draft for using Bloom filters to implement Secure DNS
相關文章
相關標籤/搜索