仙子注:這篇文章是半年前翻譯的,最先貼於公司內部的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