1. 程式人生 > 實用技巧 >大資料流的線上Heavy Hitters演算法(上篇):基於計數器的方法

大資料流的線上Heavy Hitters演算法(上篇):基於計數器的方法

Question!

有海量(e.g. 日均千億級別)的訪問日誌流,如何在不要求結果100%精確的前提下,儘量快速地統計出被訪問次數最多的一些域名,以及它們的訪問頻率?

Heavy Hitters(頻繁項)以及它衍生出來的Top-K(前K最高頻項)是大資料和流式計算領域非常經典的問題,並且在海量資料+記憶體有限+線上計算的前提下,傳統的HashMap + Heap-Sort方式幾乎不可行,需要利用更加高效的資料結構和演算法來解決。好在大佬們對Heavy Hitters問題進行了深入的研究,並總結出了很多有效的方案,本文簡要介紹一種主流的類別,即基於計數器(Counter)的方法,包括:

  • Misra-Gries演算法
  • Lossy Counting演算法
  • Space Saving演算法

在下篇文章中(計劃明天寫),會繼續介紹另一類,即基於摘要(Sketch)的方法。

Majority問題

先看一個非常經典的問題。

陣列中有一個數字出現的次數超過陣列長度的一半,請找出這個數字。

思路很簡單:遍歷陣列,如果前後遇到的兩個數不相等,就將這兩個數消去,最終剩下的那個肯定是出現次數超過一半的那個數。具體到操作上,可以設定一個候選值與一個計數器,在遍歷過程中,如果遇到的數與候選值相同則增加計數,不同則減少計數。候選值的計數減為0時,表示它肯定不是所求的結果,選取下一個數作為候選值,直到遍歷完畢。

Misra-Gries演算法

將Majority問題推廣,就會變成:

資料流中一共有m個元素,請找出出現頻率超過m / k的k - 1個元素。

可見,Majority問題就是上述問題k = 2時的特例。套用上面的計數器思路,就是Misra-Gries演算法,該演算法早在1982年就提出了。

如圖所示,維護k - 1個候選值與計數器的集合:

  • 如果元素在集合中,將其對應的計數器自增;
  • 如果元素不在集合中且集合未滿,就將元素加入集合,計數器設為1;
  • 如果元素不在集合中且集合已滿,將集合內所有計數器自減,計數器減為0的元素被移除。

Misra-Gries演算法可以利用O(k)的空間對元素j的出現頻率 fj 做出如下的估計:

  • fj - (m - m')/k <= fj <= fj
    (其中fj是j的真實出現頻率,m'則是集合中的所有計數器之和)

為什麼會有這樣的結果呢?因為計數器自減只會發生在集合滿時,且觸發計數器自減的那個元素也不會被統計到,所以相當於少統計了(k - 1) + 1 = k個元素。也就是說,計數器自減的操作最多能發生(m - m')/k輪——即fjfj之間的最大差值。由此可以總結出:

  • Misra-Gries演算法對元素出現頻率的估計總是偏低的;
  • k越大(即計數器的集合越大),頻率的估計誤差越小;
  • 最終結果能夠保證沒有假陰性(false negative),即不會漏掉實際頻率高於m / k的元素。但可能會出現假陽性(false positive),即混入實際頻率低於m / k的元素。

Lossy Counting演算法

Lossy Counting演算法在2002年提出,與Misra-Gries演算法的思路不太相同,但也很簡單。其流程如下。

  1. 將資料流劃分為固定大小的視窗。
  1. 統計每一個視窗中元素的頻率,維護在計數器的集合中。然後將所有計數器的值自減1,將計數器減為0的元素從集合中移除。
  1. 重複上述步驟,每次都統計一個視窗中的元素,將頻率值累加到計數器中,並將所有計數器自減1,並將計數器減為0的元素從集合中移除。

在視窗大小為1/ε的情況下,套用Misra-Gries演算法的誤差分析思路,容易得出Lossy Counting演算法對元素出現頻率的估計同樣是偏低的,會出現假陽性,且誤差在εm的範圍內。換句話說,如果我們希望得出頻率超過Fm的所有元素(F是個比例,如20%),那麼我們最終得到的是頻率超過(F - ε)m的結果。原作論文內建議F大約設為ε的10倍。

論文也指出Lossy Counting演算法的空間佔用為O(1/ε · log εm),可見它是以比Misra-Gries演算法更多的空間作為trade-off換來了更低的誤差。

話說回來,Misra-Gries和Lossy Counting這樣的演算法為什麼具有實用價值呢?根據著名的Zipf's Law思想,元素在資料流中的分佈往往高度傾斜,少數頻繁出現的元素佔據了資料流中的大部分空間(考慮一下“二八定律”)。所以,即使它們是不精準的,但仍然能夠給出大致正確的、有意義的統計結果。

Space Saving演算法

Space Saving演算法在2005年提出,本質上是Misra-Gries和Lossy Counting演算法的折衷,也是目前應用最廣泛的Heavy Hitters演算法之一。它維護k = 1/ε個候選值與計數器的集合,操作流程如下圖所示。

  • 如果元素在集合中,將其對應的計數器自增;
  • 如果元素不在集合中且集合未滿,就將元素加入集合,計數器設為1;
  • 如果元素不在集合中且集合已滿,將集合內計數器值最小的元素移除,將新元素插入到它的位置,並且在原計數值的基礎上自增。(這裡維護計數值最小的元素可以用傳統的堆)

可見,Space Saving演算法構建在Misra-Gries演算法的基礎上,且只有第三種情況的處理方式是不一樣的——借鑑了Lossy Counting的合併思路。除了只需要O(k)的空間之外,這樣操作的好處是,所有計數器的和一定等於資料流的總元素數m(因為不需要做減法,只需要自增),且那些沒有被移除過的元素的計數值是準確的。容易分析得出:

  • 集合中最小的計數值min一定不會大於m / k = εm,同時能夠保證找出所有頻率大於εm的元素;
  • 元素出現頻率的估計誤差同樣在εm的範圍內,不過會偏高;
  • Space Saving演算法也有假陽性的問題,特別是在非頻繁項集中位於流的末尾時。

Space Saving演算法在貼近實際應用的Zipfian資料集上的benchmark如下圖所示,可見與其他演算法相比,無論在準確率方面還是效率方面都幾乎是最優的。

在大資料相關的元件中,筆者所熟知的Space Saving演算法應用有兩處:一是Apache Kylin中的Top-N近似預計算特性;二是ClickHouse函式庫中的anyHeavy()函式,它能夠返回資料集中任意一個頻繁項。特別地,它們使用的都是並行化的Space Saving演算法,能夠顯著提升多執行緒環境下的計算效率。

The End

明天早起搬磚,民那晚安晚安。