Storm模擬分析電話日誌

兵工廠三劍客發表於2018-06-07

環境:Storm-1.2.2,ubuntu-16.0.4,Idea2018(Linux版),maven-3.3.9

所有的測試,部署都是在Linux系統上進行。

一、知識點介紹

       Storm叢集和Hadoop叢集表面上看很類似。但是Hadoop上執行的是MapReduce jobs,而在Storm上執行的是拓撲(topology),這兩者之間是非常不一樣的。一個關鍵的區別是: 一個MapReduce job最終會結束, 而一個topology永遠會執行(除非你手動kill掉)。
       Storm叢集主要由一個主節點(Nimbus後臺程式)和一群工作節點(worker node)Supervisor的節點組成,通過 Zookeeper進行協調。Nimbus類似Hadoop裡面的JobTracker。Nimbus負責在叢集裡面分發程式碼,分配計算任務給機器, 並且監控狀態。
      每一個工作節點上面執行一個叫做Supervisor的節點。Supervisor會監聽分配給它那臺機器的工作,根據需要啟動/關閉工作程式。每一個工作程式執行一個topology的一個子集;一個執行的topology由執行在很多機器上的很多工作程式組成。

        

1、 Nimbus主節點:
     主節點通常執行一個後臺程式 —— Nimbus,用於響應分佈在叢集中的節點,分配任務和監測故障。這個很類似於Hadoop中的Job Tracker。
2、Supervisor工作節點:
      工作節點同樣會執行一個後臺程式 —— Supervisor,用於收聽工作指派並基於要求執行工作程式。每個工作節點都是topology中一個子集的實現。而Nimbus和Supervisor之間的協調則通過Zookeeper系統或者叢集。
3、Zookeeper
     Zookeeper是完成Supervisor和Nimbus之間協調的服務。而應用程式實現實時的邏輯則被封裝進Storm中的“topology”。topology則是一組由Spouts(資料來源)和Bolts(資料操作)通過Stream Groupings進行連線的圖。下面對出現的術語進行更深刻的解析。
4、Worker:
       執行具體處理元件邏輯的程式。
5、Task:
       worker中每一個spout/bolt的執行緒稱為一個task. 在storm0.8之後,task不再與物理執行緒對應,同一個spout/bolt的task可能會共享一個物理執行緒,該執行緒稱為executor。

      個人理解為worker中執行程式,程式中執行執行緒,執行緒中執行任務。一個執行緒可以執行多個任務。併發度就等於所有的任務數(Task)之和。

        現在用程式碼解釋一下:        

TopologyBuilder builder = new TopologyBuilder();
        //設定Spout
        builder.setSpout("wcspout", new WordCountSpout(),3).setNumTasks(4);
        //設定creator-Bolt
        builder.setBolt("split-bolt", new SplitBolt(),4).shuffleGrouping("wcspout").setNumTasks(5);
        //設定counter-Bolt
        builder.setBolt("counter-bolt", new CountBolt(),5).fieldsGrouping("split-bolt", new Fields("word")).setNumTasks(6);
        Config conf = new Config();
        conf.setNumWorkers(2);

        該程式碼開啟了2個worker程式(worker本身不執行Task(任務),它相當於領導,用於產生executor,讓executor去執行任務)。給wcspout分配了3個執行緒4個任務,給split-bolt分配了4個執行緒5個任務,給counter-bolt分配了5個執行緒6個任務。

        程式/執行緒/任務都是平均的。因此對於上面的程式碼來說,假如開了1個supervisor,那麼這2個worker就都由這一個supervisor監管,假如開了2個supervisor,那麼每個supervisor管理一個worker。用圖形來表示上述程式碼的任務分配如下:


    每一個task執行一個物件例項。。

6、Topology(拓撲):
       storm中執行的一個實時應用程式,因為各個元件間的訊息流動形成邏輯上的一個拓撲結構。一個topology是spouts和bolts組成的圖, 通過stream groupings將圖中的spouts和bolts連線起來,如下圖:


一個topology會一直執行直到你手動kill掉,Storm自動重新分配執行失敗的任務, 並且Storm可以保證你不會有資料丟失(如果開啟了高可靠性的話)。如果一些機器意外停機它上面的所有任務會被轉移到其他機器上。
執行一個topology很簡單。首先,把你所有的程式碼以及所依賴的jar打進一個jar包。然後執行類似下面的這個命令:
      storm jar all-my-code.jar backtype.storm.MyTopology arg1 arg2
這個命令會執行主類: backtype.strom.MyTopology, 引數是arg1, arg2。這個類的main函式定義這個topology並且把它提交給Nimbus。storm jar負責連線到Nimbus並且上傳jar包。
Topology的定義是一個Thrift結構,並且Nimbus就是一個Thrift服務, 你可以提交由任何語言建立的topology。上面的方面是用JVM-based語言提交的最簡單的方法。

7、Spout:
       訊息源spout是Storm裡面一個topology裡面的訊息生產者。簡而言之,Spout從來源處讀取資料並放入topology。Spout分成可靠和不可靠兩種;當Storm接收失敗時,可靠的Spout會對tuple(元組,資料項組成的列表)進行重發;而不可靠的Spout不會考慮接收成功與否只發射一次。
       訊息源可以發射多條訊息流stream。使用OutputFieldsDeclarer.declareStream來定義多個stream,然後使用SpoutOutputCollector來發射指定的stream。
      而Spout中最主要的方法就是nextTuple(),該方法會發射一個新的tuple到topology,如果沒有新tuple發射則會簡單的返回。
       要注意的是nextTuple方法不能阻塞,因為storm在同一個執行緒上面呼叫所有訊息源spout的方法。

另外兩個比較重要的spout方法是ack和fail。storm在檢測到一個tuple被整個topology成功處理的時候呼叫ack,否則呼叫fail。storm只對可靠的spout呼叫ack和fail。
8、Bolt:
     Topology中所有的處理都由Bolt完成。即所有的訊息處理邏輯被封裝在bolts裡面。Bolt可以完成任何事,比如:連線的過濾、聚合、訪問檔案/資料庫、等等。
        Bolt從Spout中接收資料並進行處理,如果遇到複雜流的處理也可能將tuple傳送給另一個Bolt進行處理。即需要經過很多blots。比如算出一堆圖片裡面被轉發最多的圖片就至少需要兩步:第一步算出每個圖片的轉發數量。第二步找出轉發最多的前10個圖片。(如果要把這個過程做得更具有擴充套件性那麼可能需要更多的步驟)。
        Bolts可以發射多條訊息流, 使用OutputFieldsDeclarer.declareStream定義stream,使用OutputCollector.emit來選擇要發射的stream。
      而Bolt中最重要的方法是execute(),以新的tuple作為引數接收。不管是Spout還是Bolt,如果將tuple發射成多個流,這些流都可以通過declareStream()來宣告。
     bolts使用OutputCollector來發射tuple,bolts必須要為它處理的每一個tuple呼叫OutputCollector的ack方法,以通知Storm這個tuple被處理完成了,從而通知這個tuple的發射者spouts。 一般的流程是: bolts處理一個輸入tuple,  發射0個或者多個tuple, 然後呼叫ack通知storm自己已經處理過這個tuple了。storm提供了一個IBasicBolt會自動呼叫ack。
9、Tuple:
       一次訊息傳遞的基本單元。本來應該是一個key-value的map,但是由於各個元件間傳遞的tuple的欄位名稱已經事先定義好,所以tuple中只要按序填入各個value就行了,所以就是一個value list.
10、Stream:
        源源不斷傳遞的tuple就組成了stream。訊息流stream是storm裡的關鍵抽象。一個訊息流是一個沒有邊界的tuple序列, 而這些tuple序列會以一種分散式的方式並行地建立和處理。通過對stream中tuple序列中每個欄位命名來定義stream。在預設的情況下,tuple的欄位型別可以是:integer,long,short, byte,string,double,float,boolean和byte array。你也可以自定義型別(只要實現相應的序列化器)。
     每個訊息流在定義的時候會被分配給一個id,因為單向訊息流使用的相當普遍, OutputFieldsDeclarer定義了一些方法讓你可以定義一個stream而不用指定這個id。在這種情況下這個stream會分配個值為‘default’預設的id 。
      Storm提供的最基本的處理stream的原語是spout和bolt。你可以實現spout和bolt提供的介面來處理你的業務邏輯。
       
11、Stream Groupings:
Stream Grouping定義了一個流在Bolt任務間該如何被切分。這裡有Storm提供的6個Stream Grouping型別:
1). 隨機分組(Shuffle grouping):隨機分發tuple到Bolt的任務,保證每個任務獲得相等數量的tuple。
2). 欄位分組(Fields grouping):根據指定欄位分割資料流,並分組。例如,根據“user-id”欄位,相同“user-id”的元組總是分發到同一個任務,不同“user-id”的元組可能分發到不同的任務。
3). 全部分組(All grouping):tuple被複制到bolt的所有任務。這種型別需要謹慎使用。
4). 全域性分組(Global grouping):全部流都分配到bolt的同一個任務。明確地說,是分配給ID最小的那個task。
5). 無分組(None grouping):你不需要關心流是如何分組。目前,無分組等效於隨機分組。但最終,Storm將把無分組的Bolts放到Bolts或Spouts訂閱它們的同一執行緒去執行(如果可能)。
6). 直接分組(Direct grouping):這是一個特別的分組型別。元組生產者決定tuple由哪個元組處理者任務接收。
當然還可以實現CustomStreamGroupimg介面來定製自己需要的分組。

storm 和hadoop的對比來了解storm中的基本概念。

二、程式碼


1.CallLogSpout類

該類用於模擬產生資料來源

package com.strorm.test;

import org.apache.storm.spout.SpoutOutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.IRichSpout;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Values;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;

/**
 * @Author zhang
 * @Date 18-6-7 下午2:40
 * Spout類,負責產生資料流
 */
public class CallLogSpout implements IRichSpout {

    //Spout輸出收集器
    private SpoutOutputCollector collector;
    //是否完成
    private boolean completed=false;
    //上下文
    private TopologyContext context;
    //隨機發生器
    private Random randomGenerator = new Random();

    private Integer idx=0;

    public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) {
        this.context=topologyContext;
        this.collector=spoutOutputCollector;
    }

    public void close() {

    }

    public void activate() {

    }

    public void deactivate() {

    }

    /**
     * 下一個元組
     */
    public void nextTuple() {
        if (idx<=1000){
            List<String> mobileNumbers=new ArrayList<String>();
            mobileNumbers.add("13901645322");
            mobileNumbers.add("13805376831");
            mobileNumbers.add("13500803713");
            mobileNumbers.add("15988321818");
            Integer localIndex=0;
            while (localIndex++<100 && idx<1000){
                //主叫
                String caller=mobileNumbers.get(randomGenerator.nextInt(4));
                //被叫
                String callee=mobileNumbers.get(randomGenerator.nextInt(4));
                while (caller==callee){
                    //主叫與被叫不能相同,重新賦值被叫
                    callee=mobileNumbers.get(randomGenerator.nextInt(4));
                }
                //模擬通話時間
                Integer callTime=randomGenerator.nextInt(60);
                //輸出元組
                this.collector.emit(new Values(caller,callee,callTime));

            }
        }

    }

    public void ack(Object o) {

    }

    /**
     *
     * @param o
     */
    public void fail(Object o) {

    }

    /**
     * 定義輸出欄位
     * @param outputFieldsDeclarer
     */
    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
        //輸出元組中到元素為三個,這也要定義三個欄位
        outputFieldsDeclarer.declare(new Fields("from","to","callTime"));
    }

    public Map<String, Object> getComponentConfiguration() {
        return null;
    }
}

2.CallLogBolt類

該類用於處理從Spout類傳遞過來的源資料,可以實現多個IRichBolt類進行連續處理。

package com.strorm.test;

import org.apache.storm.task.OutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.IRichBolt;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Tuple;
import org.apache.storm.tuple.Values;

import java.util.Map;

/**
 * @Author zhang
 * @Date 18-6-7 下午3:19
 * 建立Bolt
 */
public class CallLogBolt implements IRichBolt {

    private OutputCollector collector;
    public void prepare(Map map, TopologyContext topologyContext, OutputCollector outputCollector) {
        this.collector=outputCollector;
    }

    public void execute(Tuple tuple) {
        //處理通話記錄
        String from=tuple.getString(0);
        String to=tuple.getString(1);
        Integer callTime=tuple.getInteger(2);
        collector.emit(new Values(from+" 呼叫 "+to,callTime));
    }

    public void cleanup() {

    }

    /**
     * 定義輸出欄位
     * @param outputFieldsDeclarer
     */
    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
        outputFieldsDeclarer.declare(new Fields("call","callTime"));
    }

    public Map<String, Object> getComponentConfiguration() {
        return null;
    }
}

3.CallCounterBolt類

該類用於統計之前的Bolt類傳送過來的資料,其功能類似於Hadoop的Reducer。

package com.strorm.test;

import org.apache.storm.task.OutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.IRichBolt;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Tuple;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author zhang
 * @Date 18-6-7 下午3:29
 * 計數器,類似於Reducer
 */
public class CallCounterBolt implements IRichBolt {
    Map<String,Integer> counterMap;
    OutputCollector collector;
    public void prepare(Map map, TopologyContext topologyContext, OutputCollector outputCollector) {
        this.counterMap=new HashMap<String, Integer>();
        collector=outputCollector;
    }

    public void execute(Tuple tuple) {
        String call=tuple.getString(0);
        Integer callTime=tuple.getInteger(1);
        if (!counterMap.containsKey(call)){
            counterMap.put(call,callTime);
        }else {
            Integer integer=counterMap.get(call)+callTime;
            counterMap.put(call,integer);
        }
        collector.ack(tuple);
    }

    public void cleanup() {
        for (Map.Entry<String,Integer> map : counterMap.entrySet()){
            System.out.println(map.getKey()+"   :"+map.getValue()+" 分鐘");
        }
    }

    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
        outputFieldsDeclarer.declare(new Fields("call"));
    }

    public Map<String, Object> getComponentConfiguration() {
        return null;
    }
}

4.CallLogOutput類

該類用於提交Topology,類似於Hadoop的Job提交。由於Storm不會停止資料流作業,所以在測試環境下要人為停止。這裡的Sleep時間可以根據實際情況設定。如果時間過短,可能看不到輸出結果。

package com.strorm.test;

import org.apache.storm.Config;
import org.apache.storm.LocalCluster;
import org.apache.storm.StormSubmitter;
import org.apache.storm.generated.AlreadyAliveException;
import org.apache.storm.generated.AuthorizationException;
import org.apache.storm.generated.InvalidTopologyException;
import org.apache.storm.topology.TopologyBuilder;
import org.apache.storm.tuple.Fields;

/**
 * @Author zhang
 * @Date 18-6-7 下午3:46
 */
public class CallLogOutput {

    public static void main(String[] args) throws InterruptedException, InvalidTopologyException, AuthorizationException, AlreadyAliveException {
        TopologyBuilder topologyBuilder=new TopologyBuilder();
        //設定Spout
        topologyBuilder.setSpout("spout",new CallLogSpout());
        //設定Bolt
        topologyBuilder.setBolt("bolt",new CallLogBolt()).shuffleGrouping("spout");
        //設定counterBolt
        topologyBuilder.setBolt("counterBolt",new CallCounterBolt()).fieldsGrouping("bolt",new Fields("call"));
        Config config=new Config();
        config.setDebug(true);
        LocalCluster localCluster=new LocalCluster();
        localCluster.submitTopology("Log",config,topologyBuilder.createTopology());
        Thread.sleep(20000);
        localCluster.shutdown();
    }
}

5.輸出結果


根據程式碼中的隨機產生4個號碼。再根據排列組合的

可以知道輸出是正確的。

6.叢集部署執行

叢集部署執行需要將localCluster提交改成StormSubmitter.submitTopology提交。並將相關的Module打包成jar包。

以本文的程式碼為例,部署時,執行:

storm jar storm-call-core-1.0-SNAPSHOT.jar com.strorm.test.CallLogOutput



在上圖中,可以看到提交的Topology名字為Log,就是程式碼中設定的Topology名字。


拓撲圖:


可以看到上圖中三個紅色圈中拓撲節點的名字就是程式碼中設定的名字。



然後回到web ui的主頁可以看到Topology已經沒有資料資訊了,表示沒有Topology在執行。


標準的執行格式為:

storm jar jar包名  xxx.類名 [arg1] [arg2] [arg3]...

當在叢集上執行了jar包後,會在某個supervisor的目錄下,產生一個worker.log,這個日誌檔案裡面記錄了輸出結果,但是該檔案一般會非常的大。



原始碼下載:點選下載storm電話日誌分析原始碼

下載原始碼完成後,只需要以匯入maven工程的方式匯入root目錄下的 pom.xml檔案,然後會自動引入所有相關的module。

相關文章