Storm入門指南第三章 拓撲結構

五柳-先生發表於2015-11-18

在本章你將會看到如何在一個Storm拓撲的不同元件間傳遞元組,以及如何將一個topology部署到一個執行的Storm叢集上。

流分組

在設計一個topology時,一件最重要的事就是定義資料在元件之間怎樣交換(流怎樣被bolt消費)。流分組(Stream Grouping)指定了每個bolt消費哪些流,以及這些流如何被消費。

提示:一個節點可以傳送不止一條資料流,流分組允許我們選擇接收哪些流。

正如第二章中見到的,流分組在topology被定義的時候就已經被設定了:

1
2
builder.setBolt("word-normalizer",new WordNormalizer())
    .shuffleGrouping("word-reader");

在上面的程式碼塊中,在topology builder上設定了一個bolt,然後設定該bolt的源使用隨機分組(shuffleGrouping)。通常情況下,一個流分組攜帶源元件的Id作為引數,並且還有其他可選引數,這取決於流分組的種類。

提示:每個InputDeclarer可以有不止一個源,並且每個源可以用不同的流分組來分組。

隨機分組

隨機分組(Shuffle Grouping)是最常用的分組方式,該分組方式攜帶一個引數(源元件Id),源元件傳送元組到一個隨機選擇的bolt並確保每個消費者(即bolt)會收到相等數量的元組。

隨機分組對於做原子操作(例如數學運算)是很有用的。然而,如果操作不能被隨機分配,就應該考慮使用其他分組,例如在第二章為單詞計數的例子中。

欄位分組

欄位分組(Fields Grouping)允許你基於tuple中的一個或多個欄位控制元組如何被髮送到bolt,該分組方式確保了對於一個組合欄位所確定的的值集合總是會被髮送到相同的bolt。回到單詞計數的例子,如果你根據“word”欄位將流分組,則WordNormalizer bolt總是會將包含給定的單詞的元組一起傳送到相同的WordCounter bolt例項中。

1
2
builder.setBolt("word-counter",new WordCounter(),2)
    .fieldsGrouping("word-normalizer",new Fields("word"));

提示:欄位分組中設定的所有欄位在源元件的輸出欄位宣告中也必須存在(譯者注:在源元件的declareOutputFields()方法中宣告)。

全部分組

全部分組(All Grouping)會向接收bolt的所有例項傳送一份每個元組的副本,這種分組方式被用於向bolts傳送訊號。例如,如果你需要重新整理快取,你可以向所有bolt傳送一個重新整理快取訊號。在第二章單詞計數的例子中,可以使用所有分組方式增加清空計數器快取的功能(WordCounter中相應程式碼如下)

01
02
03
04
05
06
07
08
09
10
11
12
13
public void execute(Tuple input) {
    String str =null;
    try{
        if(input.getSourceStreamId().equals("signals")){
            str =input.getStringByField("action");
            if("refreshCache".equals(str))
                counters.clear();
        }
    }catch(IllegalArgumentException e) {
    //Do nothing
    }
    ...
}

上面的程式碼中增加了一個if檢查源流的名字 (sourceStreamId) 是否為”signal”。Storm中可以宣告命名的流 (named streams) ,如果你不傳送元組到一個命名的流,則流的預設名字為“default”。這是一個非常好的方式來確定元組的源,正如這個例子中我們需要確定訊號一樣。

在topology定義中,向word-counter bolt 增加第二個流分組方式,以便來自signals-spout 流中的每個tuple能傳送到bolt的所有例項中。

1
2
3
builder.setBolt("word-counter",new WordCounter(),2)
    .fieldsGrouping("word-normalizer",new Fields("word"))
    .allGrouping("signals-spout","signals")

關於signals-spout的實現可以在git庫中找到。

自定義分組

通過實現CustomStreamGrouping介面,你也可以建立自定義流分組,這讓你有權決定每個元組將被哪個(些)bolt接收。

下面我們修改單詞計數的例子,將元組分組以便相同字母開頭的單詞能被相同的bolt接收。

01
02
03
04
05
06
07
08
09
10
11
12
public class ModuleGrouping implements CustomStreamGrouping,Serializable{
 
    int numTasks=0;
 
    @Override
    public List chooseTasks(List</pre>
上面是一個CustomStreamGrouping的簡單實現,在這裡我們使用任務的數量 (numTasks) 來對單詞的第一個字元的整型值取模,由此選擇哪個bolt將接收這個元組。
<p align="left">要在單詞計數的例子中使用這種分組,按照下列方式修改word-counter分組(<strong>譯者注:</strong>此處有修改):</p>
 
1
builder.setBolt("word-counter",new WordCounter())
    .customGrouping("word-normalizer",new ModuleGrouping());

直接分組

這是一個特殊的分組方式,由源元件決定哪個元件將接收元組。同前面的例子類似,源元件將基於單詞中的第一個字母決定哪個bolt接收這個tuple,為了使用直接分組,需要在WordNormalizer bolt中使用emitDirect()方法代替emit方法。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public void execute(Tuple input) {
    ...
    for(String word:words){
        if(!word.isEmpty()){
            ...
            collector.emitDirect(getWordCountIndex(word),new Values(word));
        }
    }
    // Acknowledge the tuple
    collector.ack(input);
}
 
public Integer getWordCountIndex(String word) {
    word =word.trim().toUpperCase();
    if(word.isEmpty())
        return 0;
    else
        return word.charAt(0) % numCounterTasks;
}

在prepare()方法中算出目標任務的數目:

1
2
3
4
5
public void prepare(Map stormConf,TopologyContext context,
    OutputCollector collector) {
    this.collector=collector;
    this.numCounterTasks=context.getComponentTasks("word-counter");
}

在topology的定義中,指定流將被直接分組:

1
2
builder.setBolt("word-counter",new WordCounter(),2)
    .directGrouping("word-normalizer");

全域性分組

全域性分組(Global Grouping)將源元件的所有例項產生的元組傳送到一個目標元件的例項()中(具體地說,是bolt中Id最小的那個任務)。

無分組

在Storm0.7.1時,使用這種分組和使用隨機分組一樣,即不關注流怎樣被分組。


LocalCluster vs. StormSubmitter

到現在為止,你都在使用一個叫LocalCluster的工具在本地計算機上執行topology。在自己的計算機上執行Storm基礎結構可以使你方便地執行和除錯不同的topology。但是當你想將你的topology提交到一個執行的Storm叢集上時該怎麼做呢?Storm的一大特點就是可以很方便地將你的topology傳送到一個真實的叢集上執行,此時你需要將LocalCluster改成StormSubmitter並且實現其中的submitTopology()方法,該方法負責將topology傳送到叢集上。

程式碼改變如下:

1
2
3
4
5
6
7
//LocalCluster cluster = new LocalCluster();
//cluster.submitTopology("Count-Word-Topology-With-Refresh-Cache",conf,
    builder.createTopology());
StormSubmitter.submitTopology("Count-Word-Topology-With-Refresh-Cache",conf,
    builder.createTopology());
//Thread.sleep(1000);
//cluster.shutdown();

提示:當使用StormSubmitter時,不可以在程式碼中控制叢集,但是使用LocalCluster可以。

接著,將原始碼打包成一個jar檔案,它將在你執行Storm客戶端命令提交topology時被髮送。因為使用的是Maven,所以你只需進入原始檔夾下執行命令:mvn package

生成jar檔案後,使用 storm jar 命令提交topology(如何安裝Storm客戶端參見附錄A),這個命令的語法是:

storm jar allmycode.jar org.me.MyTopology arg1 arg2 arg3

本例中,在topologies的源專案資料夾執行:

storm jar target/Topologies-0.0.1-SNAPSHOT.jar countword.TopologyMain src/main/resources/words.txt

通過這些命令,你就已經將topology提交到叢集上了。要想停止或者殺死該topology,執行:

storm kill Count-word-Topology-With-Refresh-Cache

提示:Topology的名字必須唯一。

DRPC拓撲結構

有一種被稱為DRPC(分散式遠端過程呼叫,Distributed Remote Procedure Call)的特殊的topology型別,它使用Storm分散式的能力來執行遠端過程呼叫 (RPC)。如圖3-1所示,Storm提供了一些工具讓你使用DRPC,第一個工具是DRPC伺服器,它的作用是充當DRPC 客戶端和topology之間的聯結器 ,以及topology spouts的源。DRPC伺服器接收一個函式和它的引數來執行,然後對於函式操作的每個資料片,伺服器都分配一個在整個topology中使用的請求ID來識別RPC請求。當topology執行最後一個bolt時,它必須傳送標識RPC的請求ID和結果,使得DRPC伺服器可以返回結果至正確的客戶端。

DRPCTopology圖3-1.DRPC topology圖解

提示:一個DRPC伺服器可以執行很多函式,每個函式都被一個唯一的ID標識。

Storm提供的第二個工具是LinearDRPCTopologyBuilder——一個來幫助構建DRPC topologies的抽象。構建的topology建立DRPCSpouts(它連線DRPC伺服器,並且傳送資料到topology的剩餘部分)、包裝bolt(以便結果從最後一個bolt返回)。所有新增到LinearDRPCTopologyBuilder的bolt被順序執行。

作為這種型別topology的一個例子,我們將建立一個累加數的程式。這個例子很簡單,但是這種理念可以被擴充套件到執行復雜的分散式數學運算。

bolt有如下輸出宣告:

1
2
3
public void declareOutputFields(OutputFieldsDeclarer declarer) {
    declarer.declare(new Fields("id","result"));
}

由於這是topology中的唯一bolt,所以必須傳送標識RPC的請求ID和結果。

execute()方法負責執行累加操作:

01
02
03
04
05
06
07
08
09
10
11
public void execute(Tuple input) {
    String[] numbers= input.getString(1).split("\\+");
    Integer added =0;
    if(numbers.length<2){
        throw new InvalidParameterException("Shouldbe at least 2 numbers");
    }
    for(String num:numbers){
        added +=Integer.parseInt(num);
    }
    collector.emit(new Values(input.getValue(0),added));
}

包含做累加的bolt的topology的定義如下:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
    LocalDRPC drpc =new LocalDRPC();
 
    LinearDRPCTopologyBuilder builder = new LinearDRPCTopologyBuilder("add");
    builder.addBolt(new AdderBolt(),2);
 
    Config conf =new Config();
    conf.setDebug(true);
 
    LocalCluster cluster =new LocalCluster();
    cluster.submitTopology("drpc-adder-topology",conf,
        builder.createLocalTopology(drpc));
    String result =drpc.execute("add","1+-1");
    checkResult(result,0);
    result =drpc.execute("add","1+1+5+10");
    checkResult(result,17);
 
    cluster.shutdown();
    drpc.shutdown();
}

建立LocalDRPC物件來在本地執行DRPC。接著,建立topology builder來新增bolt到topology中。為了測試這個topology,在DRPC物件上使用execute方法。

提示:使用DRPCClient類連線到遠端DRPC伺服器。DRPC伺服器提供一個可被多種語言使用的Trift API,並且在本地或者遠端執行DRPC伺服器使用同樣的API。  為了將topology提交到Storm叢集,使用builder物件中的createRemoteTopology()方法代替createLocalTopology()方法,該方法使用storm配置中的DRPC配置。


相關文章