大數據去重(data deduplication)方案


數據去重(data deduplication)是大數據領域司空見慣的問題了。除了統計UV等傳統用法之外,去重的意義更在於消除不可靠數據源產生的臟數據——即重復上報數據或重復投遞數據的影響,使計算產生的結果更加准確。

介紹下經常使用的去重方案:

一、布隆過濾器(BloomFilter)

基本原理:

BloomFilter是由一個長度為m比特的位數組(bit array)k個哈希函數(hash function)組成的數據結構。位數組均初始化為0,所有哈希函數都可以分別把輸入數據盡量均勻地散列。當要插入一個元素時,將其數據分別輸入k個哈希函數,產生k個哈希值。以哈希值作為位數組中的下標,將所有k個對應的比特置為1。當要查詢(即判斷是否存在)一個元素時,同樣將其數據輸入哈希函數,然后檢查對應的k個比特。如果有任意一個比特為0,表明該元素一定不在集合中。如果所有比特均為1,表明該集合有(較大的)可能性在集合中。為什么不是一定在集合中呢?因為一個比特被置為1有可能會受到其他元素的影響,這就是所謂“假陽性”(false positive)。相對地,“假陰性”(false negative)在BloomFilter中是絕不會出現的。

實現參考:

1、Guava中的布隆過濾器:com.google.common.hash.BloomFilter類

2、開源java實現:https://github.com/Baqend/Orestes-Bloomfilter

3、Redis Bloom Filter:https://oss.redis.com/redisbloom/,基於redis做存儲后端的BloomFilter實現,可以將bit位存儲在redis中,防止計算任務在重啟后,當前狀態丟失的問題。

二、HyperLogLog(HLL)

HyperLogLog是去重計數的利器,能夠以很小的精確度誤差作為trade-off大幅減少內存空間占用,在不要求100%准確的計數場景下常用。

HLL基本原理:

  • HyperLogLog,以下簡稱 HLL,它的空間復雜度非常低(log(log(n)) ,故而得名 HLL),幾乎不隨存儲集合的大小而變化;根據精度的不同,一個 HLL 占用的空間從 1KB 到 64KB 不等。而 Bitmap 因為需要為每一個不同的 id 用一個 bit 位表示,所以它存儲的集合越大,所占用空間也越大;存儲 1 億內數字的原始 bitmap,空間占用約為 12MB。可以看到,Bitmap 的空間要比 HLL 大約一兩個數量級。
  • HLL 支持各種數據類型作為輸入,使用方便;Bitmap 只支持 int/long 類型的數字作為輸入,因此如果原始值是 string 等類型的話,用戶需要自己提前進行到 int/long 的映射。
  • HLL 之所以支持各種數據類型,是因為其采用了哈希函數,將輸入值映射成一個二進制字節,然后對這個二進制字節進行分桶以及再判斷其首個1出現的最后位置,來估計目前桶中有多少個不同的值。由於使用了哈希函數,以及使用概率估計的方式,因此 HLL 算法的結果注定是非精確的;盡管 HLL 采用了多種糾正方式來減小誤差,但無法改變結果非精確的事實,即便最高精度,理論誤差也超過了 1%。

在用Flink做實時計算的過程中,可以用HLL去重計數,比如統計UV。

實現參考:

https://github.com/aggregateknowledge/java-hll

結合Flink,下面的聚合函數即可實現從WindowedStream按天、分key統計PV和UV。

WindowedStream<AnalyticsAccessLogRecord, Tuple, TimeWindow> windowedStream = watermarkedStream
  .keyBy("siteId")
  .window(TumblingEventTimeWindows.of(Time.days(1)))
  .trigger(ContinuousEventTimeTrigger.of(Time.seconds(10)));

windowedStream.aggregate(new AggregateFunction<AnalyticsAccessLogRecord, Tuple2<Long, HLL>, Tuple2<Long, Long>>() {
  private static final long serialVersionUID = 1L;

  @Override
  public Tuple2<Long, HLL> createAccumulator() {
    return new Tuple2<>(0L, new HLL(14, 6));
  }

  @Override
  public Tuple2<Long, HLL> add(AnalyticsAccessLogRecord record, Tuple2<Long, HLL> acc) {
    acc.f0++;
    acc.f1.addRaw(record.getUserId());
    return acc;
  }

  @Override
  public Tuple2<Long, Long> getResult(Tuple2<Long, HLL> acc) {
    return new Tuple2<>(acc.f0, acc.f1.cardinality());
  }

  @Override
  public Tuple2<Long, HLL> merge(Tuple2<Long, HLL> acc1, Tuple2<Long, HLL> acc2) {
    acc1.f0 += acc2.f0;
    acc1.f1.union(acc2.f1);
    return acc1;
  }
});

三、Roaring Bitmap 

布隆過濾器和HyperLogLog,雖然它們節省空間並且效率高,但也付出了一定的代價,即:

  • 只能插入元素,不能刪除元素;
  • 不保證100%准確,總是存在誤差。

這兩個缺點可以說是所有概率性數據結構(probabilistic data structure)做出的trade-off,畢竟魚與熊掌不可兼得。

如果一定追求100%准確,普通的位圖法顯然不合適,應該采用壓縮位圖(Roaring Bitmap)。

基本原理:

將32位無符號整數按照高16位分桶,即最多可能有216=65536個桶,稱為container。存儲數據時,按照數據的高16位找到container(找不到就會新建一個),再將低16位放入container中。也就是說,一個RBM就是很多container的集合。依據不同的場景,有 3 種不同的 Container,分別是 Array Container、Bitmap Container 和 Run Container,它們分別通過不同的壓縮方法來壓縮。實踐證明,Roaring Bitmap 可以顯著減小 Bitmap 的存儲空間和內存占用。

實現參考:

https://github.com/RoaringBitmap/RoaringBitmap

使用限制:

  • 對去重的字段只能用整型:int或者long類型,如果要對字符串去重,需要構建一個字符串和整型的映射。
  • 對於無法有效壓榨的字段(如隨機生成的),占用內存較大。

四、外部存儲去重

利用外部K-V數據庫(Redis、HBase之類)存儲需要去重的鍵。由於外部存儲對內存和磁盤占用同樣敏感,所以也得設定相應的TTL,以及對大的鍵進行壓縮。另外,外部K-V存儲畢竟是獨立於應用之外的,一旦計算任務出現問題重啟,外部存儲的狀態和內部狀態的一致性(是否需要同步)也是要注意的。

外部存儲去重,比如Elasticsearch的 _id 就可以做“去重”功能,但是這種去重的只能針對少量低概率的數據,對全量數據去重是不合適的,因為對ES會產生非常大的壓力。

 

參考:

高效壓縮位圖RoaringBitmap的原理與應用

談談三種海量數據實時去重方案

Flink基於RoaringBitmap的精確去重方案

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM