Storm 測試

本文將學習如何使用java創建Storm拓撲並將其部署到Storm集羣。

Storm集羣的組件

Storm集羣類似於Hadoop集羣,只不過 Hadoop 上運行"MapReduce jobs", Storm 上運行"topologies"。
兩者最大的差別是,MapReducejobs 最終是完成的,而 topologies 是一直處理消息(或直到你殺死它)。

集羣 任務名稱 任務時效性
Storm topologies(拓撲) 一直處理消息(或直到你殺死它)
Hadoop MapReduce jobs 最終是完成的

Storm集羣上有兩種節點:master 和 worker 節點

  • master:
    運行一個名爲 Nimbus 的守護進程,
    負責在集羣周圍分發代碼,
    爲機器分配任務以及監控故障。
    (類似 Hadoop 的 JobTracker)
  • worker:
    運行一個名爲 Supervisor 的守護進程,
    負責監聽、並根據需要啓動、停止 "Nimbus" 分配給其的任務。
    每個工作進程都執行拓撲的子集。 運行拓撲由分佈在許多計算機上的許多工作進程組成。

Nimbus 和 Supervisors 之間的協調是通過 Zookeeper 實現的。
此外,Nimbus 守護程序和 Supervisors 守護程序是 fail-fast 和 stateless;
所有狀態都保存在Zookeeper或本地磁盤上。這意味着你可以通過 kill -9 殺死 Nimbus 或者 Supervisors ,但是它們會像沒事一樣重新開始。
這種設計使Storm集羣非常穩定。
Storm集羣的組件.png

Topologies

要想在 Storm 上進行實時計算,你需要創建一個 topologies 。
topologies 是一個計算圖,topologies中的每個節點包含計算邏輯,並且通過節點之間的連接定義了數據在節點之間的流動方向。

運行拓撲很簡單。首先,將所有代碼和依賴項打包到一個jar中。然後,運行如下命令:

storm jar all-my-code.jar org.apache.storm.MyTopology arg1 arg2
額 這個命令沒啥好解釋的....

Streams

Stream 是一個無限的元組序列,是 Storm 抽象的核心。
Storm 提供了以分佈式和可靠的方式進行 Stream 傳換的原語。
比如將微博 Stream 轉換爲轉換熱門主題 Stream。

Storm 爲進行 Stream 轉換提供 spouts 和 bolt 兩個基本原語。

  • spouts
    spouts 是 Stream 的來源。
    例如,spout可以讀取Kestrel隊列中的元組並將其作爲 Stream 發出。或者 spout 可以連接到Twitter API併發出推文流。
  • bolt
    bolt會消耗任意數量的輸入流,進行一些處理,並可能發出新的流。
    像從推文流計算趨勢主題流之類的複雜流轉換,需要多個步驟,因此需要多個 bolt 。
    bolt 可以執行任何操作,包括運行函數,過濾元組,進行流聚合,進行流連接,與數據庫對話等等。

spout 和 bolt 網絡被打包成一個 topology ,這是提交給 Storm 集羣執行的頂級抽象。
拓撲是流轉換的圖形,其中每個節點都是一個 spout 或 bolt 。
圖中的表示哪些 bolt 訂閱了哪些流。
當一個 spout 或 bolt 向一個流發出一個元組時,它會將元組發送給訂閱該流的每個 bolt 。

Streams 抽象.png

拓撲中節點之間的鏈接指示應如何傳遞元組。
如上圖,Spout A 和Bolt B 之間有鏈接,Spout A 到 Bolt C 之間有鏈接,以及從 Bolt B 到 Bolt C 之間有鏈接。
那麼每次 Spout A 發出一個元組時,它都會將元組發送給 Bolt B 和 Bolt C .所有 Bolt B 的輸出元組也將送給 Bolt C.

Storm拓撲中的每個節點並行執行。
在拓撲中,你可以爲每個節點指定所需的並行度,Storm將在集羣中生成該數量的線程以執行。

拓撲會一直執行(或直到你殺死它)。
Storm會自動重新分配失敗的任務。
此外,Storm保證不會丟失數據,即使計算機出現故障並且消息丟失也是如此。


Data model

Storm使用元組作爲其數據模型。
元組是一個命名的值列表,元組中的字段可以是任何類型的對象。
Storm支持所有原始類型,字符串和字節數組作爲元組字段值。
要使用其他類型的對象,需要爲該類型實現一個序列化程序。

拓撲中的每個節點都必須聲明它發出的元組的輸出字段。
例如下面代碼中的bolt 聲明它發出2元組,字段爲 "double" 和 "triple"

package com.aaa.test;

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

import java.util.Map;

/**
 * @author lillcol
 * 2019/7/18-11:46
 */
public class DoubleAndTripleBolt extends BaseRichBolt {
    private OutputCollector _collector;

    @Override
    public void prepare(Map<String, Object> topoConf, TopologyContext context, OutputCollector collector) {
        _collector = collector;
    }

    @Override
    public void execute(Tuple input) {
        int val = input.getInteger(0);
        _collector.emit(input, new Values(val*2, val*3));
        _collector.ack(input);
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("double", "triple"));//聲明["double", "triple"]組件的輸出字段
    }
}

一個簡單的拓撲(A simple topology)

如何實現一個簡單的拓撲?
本地 idea測試
sbt構建

// libraryDependencies += "org.apache.storm" % "storm-core" % "2.0.0" % "provided"
libraryDependencies += "org.apache.storm" % "storm-core" % "2.0.0"

定義一個Spout,此處採用隨機數

package com.test.storm;

import org.apache.storm.spout.SpoutOutputCollector;
import org.apache.storm.task.OutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.base.BaseRichSpout;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Values;
import org.apache.storm.utils.Utils;

import java.util.Map;
import java.util.Random;

/**
 * @author lillcol
 * 2019/7/18-12:03
 */
public class TestWordSpout extends BaseRichSpout {
    private SpoutOutputCollector collector;

    @Override
    public void open(Map<String, Object> conf, TopologyContext context, SpoutOutputCollector collector) {
        this.collector = collector;
    }

    @Override
    public void nextTuple() {
        //Spouts負責向拓撲中發送新消息
        Utils.sleep(100);
        //每隔100ms就會從列表中隨機選一個單詞發出
        final String[] words = new String[]{"hellow", "lillcol", "study", "storm"};
        final Random rand = new Random();
        final String word = words[rand.nextInt(words.length)];
        collector.emit(new Values(word));
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("word"));
    }
}

定義Bolt,功能接收到的信息追加"levelUp!"

package com.test.storm;

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

import java.util.Map;

/**
 * @author lillcol
 * 2019/7/18-12:04
 */
public class ExclamationBolt extends BaseRichBolt {
    OutputCollector collector;

   //prepare方法爲 Bolt 提供了一個OutputCollector用於從 Bolt 中發出元組 。
   //元組可以隨時的從prepare,execute,cleanup,甚至在另一個線程中異步發出。
   //當前prepare實現只是將OutputCollector作爲實例變量保存,以便稍後在execute方法中使用。
    @Override
    public void prepare(Map<String, Object> topoConf, TopologyContext context, OutputCollector collector) {
        this.collector =collector;
    }
    //execute方法從一個Bolt的輸入接收一個元組。
    //此execute取數組的第一個字段併發出追加字符串“levelUp!” 得字符串。
    //如果您實現了一個訂閱多個輸入源的bolt,您可以通過使用Tuple#getSourceComponent方法找出Tuple來自哪個組件。
    @Override
    public void execute(Tuple input) {
        String sourceComponent = input.getSourceComponent();
        //輸入元組作爲第一個參數傳遞emit
        collector.emit(input, new Values(input.getString(0) + "levelUp!"));
        System.out.println(input.getString(0));
        // 輸入元組在最後一行被激活。這些是Storm的可靠性API的一部分,用於保證不會丟失數據
        collector.ack(input);
    }

    //declareOutputFields方法聲明ExclamationBolt發出1元組,其中一個字段稱爲“word”。
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("word"));
    }
}

//如果implements IRichBol
//還需要重寫下面兩個方法
//當Bolt被關閉時調用cleanup方法,並且應該清除所有打開的資源。
//無法保證在集羣上調用此方法:例如,如果任務正在運行的計算機爆炸,則無法調用該方法。
 @Override
    public void cleanup() {
    }
//getComponentConfiguration方法允許配置此組件運行方式的各個方面    
@Override
    public Map<String, Object> getComponentConfiguration() {
        return null;
    }
//但是一般情況下我們不需要這兩個方法,所以我們可以通過繼承BaseRichBolt來定義Bolt

定義調用類

package com.test.storm;

import org.apache.storm.Config;
import org.apache.storm.LocalCluster;
import org.apache.storm.shade.org.apache.jute.Utils;
import org.apache.storm.topology.TopologyBuilder;


/**
 * @author lillcol
 * 2019/7/18-12:03
 */
public class SimpleTopology {
    public static void main(String[] args) throws Exception {
        SimpleTopology topology = new SimpleTopology();
        topology.runLocal(60);

    }

    public void runLocal(int waitSeconds) throws Exception {
        TopologyBuilder topologyBuilder = new TopologyBuilder();
        //第一個參數是給Spout一個id "words", 
        //第二個參數是要調用的Spout類
        //第三個參數是節點所需的並行度,是可選的。它指示應在羣集中執行該組件的線程數。如果省略它,Storm將只爲該節點分配一個線程。
        topologyBuilder.setSpout("words", new TestWordSpout(), 1);
        //Bolt的參數與Spout
        //只是要通過shuffleGrouping 指定數據來源"words")
        //“shuffle grouping”意味着元組應該從輸入任務隨機分配到bolt的任務中。
        topologyBuilder.setBolt("DoubleAndTripleBolt1", new ExclamationBolt(), 1)
                .shuffleGrouping("words");
        //一個Bolt可以接收多個數據來源,是要多次調用shuffleGrouping即可       
        topologyBuilder.setBolt("DoubleAndTripleBolt2", new ExclamationBolt(), 1)
                .shuffleGrouping("DoubleAndTripleBolt1")
                .shuffleGrouping("words");

        //loacl 測試
        Config config = new Config();
        LocalCluster cluster = new LocalCluster();
        cluster.submitTopology("word_count", config, topologyBuilder.createTopology());

        org.apache.storm.utils.Utils.sleep(1000*10);
        cluster.killTopology("word_count");
        cluster.shutdown();
    }
}
運行結果:
study
study
studylevelUp!
study
study
studylevelUp!
hellow
hellow
hellowlevelUp!
lillcol
lillcol
lillcollevelUp!
hellow
hellow
hellowlevelUp!
hellow
hellow
hellowlevelUp!
lillcol
lillcol
lillcollevelUp!
. . .

異常

可能出現異常1:

java.lang.NoClassDefFoundError: org/apache/storm/topology/IRichSpout
    at java.lang.Class.getDeclaredMethods0(Native Method)
    at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
    at java.lang.Class.privateGetMethodRecursive(Class.java:3048)
    at java.lang.Class.getMethod0(Class.java:3018)
    at java.lang.Class.getMethod(Class.java:1784)
    at sun.launcher.LauncherHelper.validateMainClass(LauncherHelper.java:544)
    at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:526)
Caused by: java.lang.ClassNotFoundException: org.apache.storm.topology.IRichSpout
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 7 more
Error: A JNI error has occurred, please check your installation and try again

這個是因爲在sbt構建的時候  % "provided" 意思是已提供相關jar,但是我們idea測試的時候並沒有相關jar
libraryDependencies += "org.apache.storm" % "storm-core" % "2.0.0" % "provided"

所以不能用上面的語句,改成下面的即可
libraryDependencies += "org.apache.storm" % "storm-core" % "2.0.0"

maven 對應着改就可以了

可能出現異常2:

Exception in thread "main" java.lang.ExceptionInInitializerError
    at org.apache.log4j.LogManager.getLogger(LogManager.java:44)
    at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:64)
    at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:358)
    at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:383)
    at org.apache.storm.LocalCluster.<clinit>(LocalCluster.java:128)
    at com.test.storm.SimpleTopology.runLocal(SimpleTopology.java:28)
    at com.test.storm.SimpleTopology.main(SimpleTopology.java:16)
Caused by: java.lang.IllegalStateException: Detected both log4j-over-slf4j.jar AND slf4j-log4j12.jar on the class path, preempting StackOverflowError. See also http://www.slf4j.org/codes.html#log4jDelegationLoop for more details.
    at org.apache.log4j.Log4jLoggerFactory.<clinit>(Log4jLoggerFactory.java:49)
    ... 7 more
    
錯誤報的很明顯
log4j-over-slf4j.jar AND slf4j-log4j12.jar  衝突了
我的解決辦法是在測試的時候隨便刪掉一個,但是生產的時候在可能衝突的依賴中把它去掉

Storm 的 的hellow word(word count)
//定義Spout WordReader
package com.test.storm;

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

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Map;

/**
 * @author lillcol
 * 2019/7/19-9:17
 */
public class WordReader extends BaseRichSpout {
    private SpoutOutputCollector collector;
    private FileReader fileReader;
    private boolean completed = false;

    /**
     * open方法,接收三個參數:
     * 第一個是創建Topology的配置,
     * 第二個是所有的Topology數據
     * 第三個是用來把Spout的數據發射給bolt
     **/
    @Override
    public void open(Map<String, Object> conf, TopologyContext context, SpoutOutputCollector collector) {
        try {
            //獲取創建Topology時指定的要讀取的文件路徑
            this.fileReader = new FileReader(conf.get("wordsFile").toString());
        } catch (FileNotFoundException e) {
            throw new RuntimeException("Error reading file ["
                    + conf.get("wordFile") + "]");
        }
        //初始化發射器
        this.collector = collector;
    }

    /**
     * nextTuple是Spout最主要的方法:
     * 在這裏我們讀取文本文件,並把它的每一行發射出去(給bolt)
     * 這個方法會不斷被調用,爲了降低它對CPU的消耗,當任務完成時讓它sleep一下
     **/
    @Override
    public void nextTuple() {
        if (completed) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return;
        }
        String str;
        BufferedReader bufferedReader = new BufferedReader(fileReader);
        try {
            while ((str = bufferedReader.readLine()) != null) {
                //發送一行
                collector.emit(new Values(str), str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            completed = true;
        }

    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("line"));
    }
}
//定義Bolt WordSplit 實現切割
package com.test.storm;

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

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

/**
 * @author lillcol
 * 2019/7/19-9:38
 */
public class WordSplit implements IRichBolt {
    private OutputCollector collector;


    @Override
    public void prepare(Map<String, Object> topoConf, TopologyContext context, OutputCollector collector) {
        this.collector = collector;
    }

    /**
     * execute是bolt中最重要的方法:
     * 當接收到一個tuple時,此方法被調用
     * 這個方法的作用就是把接收到的每一行切分成單個單詞,並把單詞發送出去(給下一個bolt處理)
     **/
    @Override
    public void execute(Tuple input) {
        String line = input.getString(0);
        String[] words = line.split(",| |\\|");
        for (String word : words) {
            word = word.trim();
            if (!word.isEmpty()) {
                List a = new ArrayList();
                a.add(input);
                collector.emit(a, new Values(word));
            }
        }
        collector.ack(input);
    }

    @Override
    public void cleanup() {

    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("word"));
    }

    @Override
    public Map<String, Object> getComponentConfiguration() {
        return null;
    }
}
//定義Bolt WordCounter 實現統計
package com.test.storm;

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.Tuple;

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

/**
 * @author lillcol
 * 2019/7/19-10:01
 */
public class WordCounter implements IRichBolt {
    Integer id;
    String name;
    Map<String, Integer> counters;
    private OutputCollector collector;

    @Override
    public void prepare(Map<String, Object> topoConf, TopologyContext context, OutputCollector collector) {
        this.counters = new HashMap<String, Integer>();
        this.collector = collector;
        this.name = context.getThisComponentId();
        this.id = context.getThisTaskId();
    }

    @Override
    public void execute(Tuple input) {
        String str = input.getString(0);
        if (!counters.containsKey(str)) {
            counters.put(str, 1);
        } else {
            counters.put(str, counters.get(str) + 1);
        }
        collector.ack(input);
    }

    @Override
    public void cleanup() {
        System.out.println("--Word Counter [" + name + "-" + id + "] --");
        for (Map.Entry<String, Integer> entry : counters.entrySet()) {
            System.out.println(entry.getKey() + ":" + entry.getValue());
        }
        counters.clear();
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {

    }

    @Override
    public Map<String, Object> getComponentConfiguration() {
        return null;
    }
}
//定義主類
package com.test.storm;

import org.apache.storm.Config;
import org.apache.storm.LocalCluster;
import org.apache.storm.topology.TopologyBuilder;

/**
 * @author lillcol
 * 2019/7/19-10:33
 */
public class WordCountTopology {
    public static void main(String[] args) throws Exception {
        TopologyBuilder topologyBuilder = new TopologyBuilder();
        topologyBuilder.setSpout("wordReader",new WordReader(),1);
        topologyBuilder.setBolt("WordSplit",new WordSplit(),1)
                .shuffleGrouping("wordReader");
        topologyBuilder.setBolt("",new WordCounter(),2)
                .shuffleGrouping("WordSplit");

        //配置
        Config config = new Config();
        config.put("wordsFile","D:\\stromFile");
        config.put(Config.TOPOLOGY_MAX_SPOUT_PENDING, 1);
        //創建一個本地模式cluster
        LocalCluster localCluster = new LocalCluster();
        //提交Topology
        localCluster.submitTopology("WordCountTopology",config,topologyBuilder.createTopology());

        Thread.sleep(2000);//這個時間要控制好,太短看不到效果
        localCluster.shutdown();

    }
}
//輸出結果
11:35:16.845 [SLOT_1024] INFO  o.a.s.e.ExecutorShutdown - Shutting down executor :[2, 2]
11:35:16.845 [Thread-37--executor[2, 2]] INFO  o.a.s.u.Utils - Async loop interrupted!
--Word Counter [-2] --
Thread[SLOT_1027:73
40673ms:1
11:34:31.865:1
30724ms:1
11:34:23.065:1
11:34:27.365:1
. . .


11:35:16.846 [SLOT_1024] INFO  o.a.s.e.ExecutorShutdown - Shut down executor :[2, 2]
11:35:16.846 [SLOT_1024] INFO  o.a.s.e.ExecutorShutdown - Shutting down executor :[1, 1]
11:35:16.846 [Thread-38--executor[1, 1]] INFO  o.a.s.u.Utils - Async loop interrupted!
--Word Counter [-1] --
Thread[SLOT_1027:87
11:34:31.465:1
29524ms:1
26024ms:1
. . . 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章