做者 張洋 | 發佈於 2012-12-30javascript
原地址:http://blog.codinglabs.org/articles/algorithms-for-cardinality-estimation-part-i.htmlhtml
基數計數(cardinality counting)是實際應用中一種常見的計算場景,在數據分析、網絡監控及數據庫優化等領域都有相關需求。精確的基數計數算法因爲種種緣由,在面對大數據場景時每每力不從心,所以如何在偏差可控的狀況下對基數進行估計就顯得十分重要。目前常見的基數估計算法有Linear Counting、LogLog Counting、HyperLogLog Counting及Adaptive Counting等。這幾種算法都是基於機率統計理論所設計的機率算法,它們克服了精確基數計數算法的諸多弊端(如內存需求過大或難以合併等),同時能夠經過必定手段將偏差控制在所要求的範圍內。java
做爲「解讀Cardinality Estimation算法」系列文章的第一部分,本文將首先介紹基數的概念,而後經過一個電商數據分析的例子說明基數如何在具體業務場景中發揮做用以及爲何在大數據面前基數的計算是困難的,在這一部分也同時會詳述傳統基數計數的解決方案及遇到的難題。web
後面在第二部分-第四部分會分別詳細介紹Linear Counting、LogLog Counting、HyperLogLog Counting及Adaptive Counting四個算法,會涉及算法的基本思路、機率分析及論文關鍵部分的解讀。算法
文章索引:數據庫
第二部分:Linear Countingcookie
第四部分:HyperLogLog Counting及Adaptive Counting數據結構
簡單來講,基數(cardinality,也譯做勢),是指一個集合(這裏的集合容許存在重複元素,與集合論對集合嚴格的定義略有不一樣,如不作特殊說明,本文中提到的集合均容許存在重複元素)中不一樣元素的個數。例如看下面的集合:
{1,2,3,4,5,2,3,9,7}{1,2,3,4,5,2,3,9,7}
這個集合有9個元素,可是2和3各出現了兩次,所以不重複的元素爲1,2,3,4,5,9,7,因此這個集合的基數是7。
若是兩個集合具備相同的基數,咱們說這兩個集合等勢。基數和等勢的概念在有限集範疇內比較直觀,可是若是擴展到無限集則會比較複雜,一個無限集可能會與其真子集等勢(例如整數集和偶數集是等勢的)。不過在這個系列文章中,咱們僅討論有限集的狀況,關於無限集合基數的討論,有興趣的同窗能夠參考實變分析相關內容。
容易證實,若是一個集合是有限集,則其基數是一個天然數。
下面經過一個實例說明基數在電商數據分析中的應用。
假設一個淘寶網店在其店鋪首頁放置了10個寶貝連接,分別從Item01到Item10爲這十個連接編號。店主但願能夠在一天中隨時查看從今天零點開始到目前這十個寶貝連接分別被多少個獨立訪客點擊過。所謂獨立訪客(Unique Visitor,簡稱UV)是指有多少個天然人,例如,即便我今天點了五次Item01,我對Item01的UV貢獻也是1,而不是5。
用術語說這實際是一個實時數據流統計分析問題。
要實現這個統計需求。須要作到以下三點:
一、對獨立訪客作標識
二、在訪客點擊連接時記錄下連接編號及訪客標記
三、對每個要統計的連接維護一個數據結構和一個當前UV值,當某個連接發生一次點擊時,能迅速定位此用戶在今天是否已經點過此連接,若是沒有則此連接的UV增長1
下面分別介紹三個步驟的實現方案
客觀來講,目前尚未能在互聯網上準確對一個天然人進行標識的方法,一般採用的是近似方案。例如經過登陸用戶+cookie跟蹤的方式:當某個用戶已經登陸,則採用會員ID標識;對於未登陸用戶,則採用跟蹤cookie的方式進行標識。爲了簡單起見,咱們假設徹底採用跟蹤cookie的方式對獨立訪客進行標識。
這一步能夠經過javascript埋點及記錄accesslog完成,具體原理和實現方案能夠參考我以前的一篇文章:網站統計中的數據收集原理及實現。
能夠看到,若是將每一個連接被點擊的日誌中訪客標識字段當作一個集合,那麼此連接當前的UV也就是這個集合的基數,所以UV計算本質上就是一個基數計數問題。
在實時計算流中,咱們能夠認爲任何一次連接點擊均觸發以下邏輯(僞代碼描述):
邏輯很是簡單,每當有一個點擊事件發生,就去相應的連接被訪集合中尋找此訪客是否已經在裏面,若是沒有則將此用戶標識加入集合,並將此連接的UV加1。
雖然邏輯很是簡單,可是在實際實現中尤爲面臨大數據場景時仍是會遇到諸多困難,下面一節我會介紹兩種目前被業界廣泛使用的精確算法實現方案,並經過分析說明當數據量增大時它們面臨的問題。
接着上面的例子,咱們看一下目前經常使用的基數計數的實現方法。
對上面的僞代碼作一個簡單分析,會發現關鍵操做有兩個:查找-迅速定位當前訪客是否已經在集合中,插入-將新的訪客標識插入到訪客集合中。所以,須要爲每個須要統計UV的點(此處就是十個寶貝連接)維護一個查找效率較高的數據結構,又由於實時數據流的關係,這個數據結構須要儘可能在內存中維護,所以這個數據結構在空間複雜度上也要比較適中。綜合考慮一種傳統的作法是在實時計算引擎採用了B樹來組織這個集合。下圖是一個示意圖:
之因此選用B樹是由於B樹的查找和插入相關高效,同時空間複雜度也能夠接受(關於B樹具體的性能分析請參考這裏)。
這種實現方案爲一個基數計數器維護一棵B樹,因爲B樹在查找效率、插入效率和內存使用之間很是平衡,因此算是一種能夠接受的解決方案。可是當數據量特別巨大時,例如要同時統計幾萬個連接的UV,若是要將幾萬個連接一天的訪問記錄所有維護在內存中,這個內存使用量也是至關可觀的(假設每一個B樹佔用1M內存,10萬個B樹就是100G!)。一種方案是在某個時間點將內存數據結構寫入磁盤(雙十一和雙十二大促時一淘數據部的效果平臺是每分鐘將數據寫入HBase)而後將內存中的計數器和數據結構清零,可是B樹並不能高效的進行合併,這就使得內存數據落地成了很是大的難題。
另外一個須要數據結構合併的場景是查看並集的基數,例如在上面的例子中,若是我想查看Item1和Item2的總UV,是沒有辦法經過這種B樹的結構快速獲得的。固然能夠爲每一種可能的組合維護一棵B樹。不過經過簡單的分析就能夠知道這個方案基本不可行。N個元素集合的非空冪集數量爲2N−12N−1,所以要爲10個連接維護1023棵B樹,而隨着連接的增長這個數量會以冪指級別增加。
爲了克服B樹不能高效合併的問題,一種替代方案是使用bitmap表示集合。也就是使用一個很長的bit數組表示集合,將bit位順序編號,bit爲1表示此編號在集合中,爲0表示不在集合中。例如「00100110」表示集合 {2,5,6}。bitmap中1的數量就是這個集合的基數。
顯然,與B樹不一樣bitmap能夠高效的進行合併,只需進行按位或(or)運算就能夠,而位運算在計算機中的運算效率是很高的。可是bitmap方式也有本身的問題,就是內存使用問題。
很容易發現,bitmap的長度與集合中元素個數無關,而是與基數的上限有關。例如在上面的例子中,假如要計算上限爲1億的基數,則須要12.5M字節的bitmap,十個連接就須要125M。關鍵在於,這個內存使用與集合元素數量無關,即便一個連接僅僅有一個1UV,也要爲其分配12.5M字節。
因而可知,雖然bitmap方式易於合併,卻因爲內存使用問題而沒法普遍用於大數據場景。
本文重點在於經過電商數據分析中UV計算的例子,說明基數的應用、傳統的基數計數算法及這些算法在大數據面前遇到的問題。實際上目前尚未發現更好的在大數據場景中準確計算基數的高效算法,所以在不追求絕對準確的狀況下,使用機率算法算是一個不錯的解決方案。在後續文章中,我將逐一解讀經常使用的基數估計機率算法。