Storm常見模式2——TOP N介紹

五柳-先生發表於2015-11-18
導讀問題:
1.TOP N計算的應用場景有哪些?
2.TOP N的實現方法和原理是什麼?
Storm的另一種常見模式是對流式資料進行所謂“streaming top N”的計算,它的特點是持續的在記憶體中按照某個統計指標(如出現次數)計算TOP N,然後每隔一定時間間隔輸出實時計算後的TOP N結果。
流式資料的TOP N計算的應用場景很多,例如計算twitter上最近一段時間內的熱門話題、熱門點選圖片等等。
下面結合Storm-Starter中的例子,介紹一種可以很容易進行擴充套件的實現方法:首先,在多臺機器上並行的執行多個Bolt,每個Bolt負責一部分資料的TOP N計算,然後再有一個全域性的Bolt來合併這些機器上計算出來的TOP N結果,合併後得到最終全域性的TOP N結果。
該部分示例程式碼的入口是RollingTopWords類,用於計算文件中出現次數最多的N個單詞。首先看一下這個Topology結構:
 
Topology構建的程式碼如下:
  1. TopologyBuilder builder = new TopologyBuilder();
  2.         builder.setSpout("word", new TestWordSpout(), 5);
  3.         builder.setBolt("count", new RollingCountObjects(60, 10), 4)
  4.                  .fieldsGrouping("word", new Fields("word"));
  5.         builder.setBolt("rank", new RankObjects(TOP_N), 4)
  6.                  .fieldsGrouping("count", new Fields("obj"));
  7.         builder.setBolt("merge", new MergeObjects(TOP_N))
  8.                  .globalGrouping("rank");
複製程式碼

(1)首先,TestWordSpout()是Topology的資料來源Spout,持續隨機生成單詞發出去,產生資料流“word”,輸出Fields是“word”,核心程式碼如下:
  1. public void nextTuple() {
  2.         Utils.sleep(100);
  3.         final String[] words = new String[] {"nathan", "mike", "jackson", "golda", "bertels"};
  4.         final Random rand = new Random();
  5.         final String word = words[rand.nextInt(words.length)];
  6.         _collector.emit(new Values(word));
  7.   }
  8.     public void declareOutputFields(OutputFieldsDeclarer declarer) {
  9.         declarer.declare(new Fields("word"));
  10.   }
複製程式碼

(2)接下來,“word”流入RollingCountObjects這個Bolt中進行word count計算,為了保證同一個word的資料被髮送到同一個Bolt中進行處理,按照“word”欄位進行field grouping;在RollingCountObjects中會計算各個word的出現次數,然後產生“count”流,輸出“obj”和“count”兩個Field,核心程式碼如下:
  1. public void execute(Tuple tuple) {

  2.         Object obj = tuple.getValue(0);
  3.         int bucket = currentBucket(_numBuckets);
  4.         synchronized(_objectCounts) {
  5.             long[] curr = _objectCounts.get(obj);
  6.             if(curr==null) {
  7.                 curr = new long[_numBuckets];
  8.                 _objectCounts.put(obj, curr);
  9.             }
  10.             curr[bucket]++;
  11.             _collector.emit(new Values(obj, totalObjects(obj)));
  12.             _collector.ack(tuple);
  13.         }
  14.     }
  15.     public void declareOutputFields(OutputFieldsDeclarer declarer) {
  16.         declarer.declare(new Fields("obj", "count"));
  17.     }
複製程式碼

(3)然後,RankObjects這個Bolt按照“count”流的“obj”欄位進行field grouping;在Bolt內維護TOP N個有序的單詞,如果超過TOP N個單詞,則將排在最後的單詞踢掉,同時每個一定時間(2秒)產生“rank”流,輸出“list”欄位,輸出TOP N計算結果到下一級資料流“merge”流,核心程式碼如下:
  1. public void execute(Tuple tuple, BasicOutputCollector collector) {
  2.         Object tag = tuple.getValue(0);
  3.         Integer existingIndex = _find(tag);
  4.         if (null != existingIndex) {
  5.             _rankings.set(existingIndex, tuple.getValues());
  6.         } else {
  7.             _rankings.add(tuple.getValues());
  8.         }
  9.         Collections.sort(_rankings, new Comparator<List>() {
  10.             public int compare(List o1, List o2) {
  11.                 return _compare(o1, o2);
  12.             }
  13.         });
  14.         if (_rankings.size() > _count) {
  15.             _rankings.remove(_count);
  16.         }
  17.         long currentTime = System.currentTimeMillis();
  18.         if(_lastTime==null || currentTime >= _lastTime + 2000) {
  19.             collector.emit(new Values(new ArrayList(_rankings)));
  20.             _lastTime = currentTime;
  21.         }
  22.     }

  23.     public void declareOutputFields(OutputFieldsDeclarer declarer) {
  24.         declarer.declare(new Fields("list"));
  25.     }
複製程式碼

(4)最後,MergeObjects這個Bolt按照“rank”流的進行全域性的grouping,即所有上一級Bolt產生的“rank”流都流到這個“merge”流進行;MergeObjects的計算邏輯和RankObjects類似,只是將各個RankObjects的Bolt合併後計算得到最終全域性的TOP N結果,核心程式碼如下:
  1. public void execute(Tuple tuple, BasicOutputCollector collector) {
  2.         List<List> merging = (List) tuple.getValue(0);
  3.         for(List pair : merging) {
  4.             Integer existingIndex = _find(pair.get(0));
  5.             if (null != existingIndex) {
  6.                 _rankings.set(existingIndex, pair);
  7.             } else {
  8.                 _rankings.add(pair);
  9.             }

  10.             Collections.sort(_rankings, new Comparator<List>() {
  11.                 public int compare(List o1, List o2) {
  12.                     return _compare(o1, o2);
  13.                 }
  14.             });

  15.             if (_rankings.size() > _count) {
  16.                 _rankings.subList(_count, _rankings.size()).clear();
  17.             }
  18.         }

  19.         long currentTime = System.currentTimeMillis();
  20.         if(_lastTime==null || currentTime >= _lastTime + 2000) {
  21.             collector.emit(new Values(new ArrayList(_rankings)));
  22.             LOG.info("Rankings: " + _rankings);
  23.             _lastTime = currentTime;
  24.         }
  25.     }

  26.     public void declareOutputFields(OutputFieldsDeclarer declarer) {
  27.         declarer.declare(new Fields("list"));
  28.     }
複製程式碼

關於上述例子的幾點說明:

(1) 為什麼要有RankObjects和MergeObjects兩級的Bolt來計算呢?

其實,計算TOP N的一個最簡單的思路是直接使用一個Bolt(通過類似於RankObjects的類實現)來做全域性的求TOP N操作。

但是,這種方式的明顯缺點在於受限於單臺機器的處理能力。

(2) 如何保證計算結果的正確性?

首先通過field grouping將同一個word的計算放到同一個Bolt上處理;最後有一個全域性的global grouping彙總得到TOP N。

這樣可以做到最大可能並行性,同時也能保證計算結果的正確。

(3) 如果當前計算資源無法滿足計算TOP N,該怎麼辦?

這個問題本質上就是系統的可擴充套件性問題,基本的解決方法就是儘可能做到在多個機器上的平行計算過程,針對上面的Topology結構:

a) 可以通過增加每一級處理單元Bolt的數量,減少每個Bolt處理的資料規模;

b) 可以通過增加一級或多級Bolt處理單元,減少最終彙總處理的資料規模。

相關文章