HyperLoglog算法在Uv實時統計中的應用

1 傳統的Uv實時統計方法以及其缺點

給定時間段條件下,實時統計Uv就是統計不重複的訪客數。
最簡單的方法就是把用戶唯一id存儲到集合中,每次有新訪客,就把向集合新增元素。
但是當數據量千萬級別的時候,無論是內存中,還是redis等外部系統中,集合新增元素的效率都很低。

2 HyperLoglog

在不追求絕對準確的情況下,使用概率算法算是一個不錯的解決方案。
概率算法不直接存儲數據集合本身,通過一定的概率統計方法預估基數值,這種方法可以大大節省內存。
怎麼理解HyperLoglog算法呢,

下面是通過mapWithState算子計算單詞數的官方示例

...
val stateDstream = wordDstream.mapWithState(
      StateSpec.function(mappingFunc).initialState(initialRDD))

val mappingFunc = (word: String, one: Option[Int], state: State[Int]) => {
    val sum = one.getOrElse(0) + state.getOption.getOrElse(0)
    val output = (word, sum)
    state.update(sum)
    output
}
...

如果這的word代表訪客唯一id,那如何統計每天的訪客數(DAU)呢?
這裏引入Java版本的HLL,來實現大數據背景下的uv實時統計。

maven依賴:

<dependency>
  <groupId>com.clearspring.analytics</groupId>
  <artifactId>stream</artifactId>
  <version>2.9.5</version>
</dependency>

核心邏輯:

import com.clearspring.analytics.stream.cardinality.HyperLogLog;

//rdd類型爲RDD[(String,String)], means (dateStr, id)
//設置超時爲一天
val stateDstream = visitorDstream.mapWithState(
      StateSpec.function(mappingFuncHLL).timeout(Seconds(86400))
      )


private val mappingFuncHLL = (key: String, one: Option[Set[String]], state: State[HyperLogLog]) => {
    val defaultRes = new HyperLogLog(14)

    if (state.isTimingOut()) {
      (key, defaultRes)
    } else {
      var uv = state.getOption().getOrElse(defaultRes)
      one.foreach(it => {
        it._1.foreach(uv.offer(_))
        state.update(uv)
      })

      (key, uv)
    }
  }

3 性能評估

在Redis裏,每個HyperLogLog Key只需佔用十幾k的內存,就可以估算接近2^64個不同元素的基數。
Redis HyperLogLog測試給出了不同規模數據集下的誤差。

千萬級數據集下,隨機數誤差基本低於 1.5%, 完全不重複情況下誤差也不超過 2%。能滿足大部分情況下的需求。

4 討論

4.1 bitmap

bitmap可以理解爲通過一個bit數組來存儲特定數據的一種數據結構,每一個bit位都能獨立包含信息,bit是數據的最小存儲單位,因此能大量節省空間,也可以將整個bit數據一次性load到內存計算。
如果定義一個很大的bit數組,基數統計中每一個元素對應到bit數組的其中一位,例如bit數組1001010代表實際數組[2, 4, 7]
新加入一個元素,只需要將已有的bit數組和新加入的數字做按位或 (or)(or)計算。bitmap中1的數量就是集合的基數值。

bitmap有一個很明顯的優勢是可以輕鬆合併多個統計結果,只需要對多個結果求異或就可以。也可以大大減少存儲內存,
可以做個簡單的計算,如果要統計1億個數據的基數值,大約需要內存: 100000000/8/1024/1024 \approx≈ 12M

bitmap對於內存的節約量是顯而易見的,但是要做到每個元素唯一對應bit數組中固定一位需要一個良好的哈希算法。
我們希望有一個理想的哈希函數hash,能夠保證對於元素e, 得到唯一的數字loc = hash(e), loc就是元素e在bit數組中的下標或位置。
怎麼得到接近理想的,足夠好的哈希函數,這是另一個問題。

4.2 B樹

B樹最大的優勢是插入和查找效率很高,如果用B樹存儲要統計的數據,可以快速判斷新來的數據是否已經存在,並快速將元素插入B樹。要計算基數值,只需要計算B樹的節點個數。 將B樹結構維護到內存中,可以快速統計和計算,
但是B樹的問題是隻是加快了查找和插入效率,並沒有節省存儲內存。例如要同時統計幾萬個鏈接的UV,每個鏈接的訪問量都很大,如果把這些數據都維護到內存中,實在是夠嗆。

4.3 HLL的原理

see also [1]

References

  1. 神奇的HyperLogLog算法
  2. HyperLogLog算法詳解
  3. Redis HyperLogLog測試
  4. http://blog.codinglabs.org/articles/algorithms-for-cardinality-estimation-part-iv.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章