分散式流處理框架 Apache Storm —— 程式設計模型詳解

單人影發表於2019-06-28

一、簡介

下圖為Strom的執行流程圖,在開發Storm流處理程式時,我們需要採用內建或自定義實現spout(資料來源)和bolt(處理單元),並通過TopologyBuilder將它們之間進行關聯,形成Topology

分散式流處理框架 Apache Storm —— 程式設計模型詳解

二、IComponent介面

IComponent介面定義了Topology中所有元件(spout/bolt)的公共方法,自定義的spout或bolt必須直接或間接實現這個介面。

public interface IComponent extends Serializable {

    /**
     * 宣告此拓撲的所有流的輸出模式。
     * @param declarer這用於宣告輸出流id,輸出欄位以及每個輸出流是否是直接流(direct stream)
     */
    void declareOutputFields(OutputFieldsDeclarer declarer);

    /**
     * 宣告此元件的配置。
     *
     */
    Map<String, Object> getComponentConfiguration();

}

三、Spout

3.1 ISpout介面

自定義的spout需要實現ISpout介面,它定義了spout的所有可用方法:

public interface ISpout extends Serializable {
    /**
     * 元件初始化時候被呼叫
     *
     * @param conf ISpout的配置
     * @param context 應用上下文,可以通過其獲取任務ID和元件ID,輸入和輸出資訊等。
     * @param collector  用來傳送spout中的tuples,它是執行緒安全的,建議儲存為此spout物件的例項變數
     */
    void open(Map conf, TopologyContext context, SpoutOutputCollector collector);

    /**
     * ISpout將要被關閉的時候呼叫。但是其不一定會被執行,如果在叢集環境中通過kill -9 殺死程式時其就無法被執行。
     */
    void close();
    
    /**
     * 當ISpout從停用狀態啟用時被呼叫
     */
    void activate();
    
    /**
     * 當ISpout停用時候被呼叫
     */
    void deactivate();

    /**
     * 這是一個核心方法,主要通過在此方法中呼叫collector將tuples傳送給下一個接收器,這個方法必須是非阻塞的。     
     * nextTuple/ack/fail/是在同一個執行緒中執行的,所以不用考慮執行緒安全方面。當沒有tuples發出時應該讓
     * nextTuple休眠(sleep)一下,以免浪費CPU。
     */
    void nextTuple();

    /**
     * 通過msgId進行tuples處理成功的確認,被確認後的tuples不會再次被髮送
     */
    void ack(Object msgId);

    /**
     * 通過msgId進行tuples處理失敗的確認,被確認後的tuples會再次被髮送進行處理
     */
    void fail(Object msgId);
}

3.2 BaseRichSpout抽象類

通常情況下,我們實現自定義的Spout時不會直接去實現ISpout介面,而是繼承BaseRichSpoutBaseRichSpout繼承自BaseCompont,同時實現了IRichSpout介面。

分散式流處理框架 Apache Storm —— 程式設計模型詳解

IRichSpout介面繼承自ISpoutIComponent,自身並沒有定義任何方法:

public interface IRichSpout extends ISpout, IComponent {

}

BaseComponent抽象類空實現了IComponentgetComponentConfiguration方法:

public abstract class BaseComponent implements IComponent {
    @Override
    public Map<String, Object> getComponentConfiguration() {
        return null;
    }    
}

BaseRichSpout繼承自BaseCompont類並實現了IRichSpout介面,並且空實現了其中部分方法:

public abstract class BaseRichSpout extends BaseComponent implements IRichSpout {
    @Override
    public void close() {}

    @Override
    public void activate() {}

    @Override
    public void deactivate() {}

    @Override
    public void ack(Object msgId) {}

    @Override
    public void fail(Object msgId) {}
}

通過這樣的設計,我們在繼承BaseRichSpout實現自定義spout時,就只有三個方法必須實現:

  • open : 來源於ISpout,可以通過此方法獲取用來傳送tuples的SpoutOutputCollector
  • nextTuple :來源於ISpout,必須在此方法內部傳送tuples;
  • declareOutputFields :來源於IComponent,宣告傳送的tuples的名稱,這樣下一個元件才能知道如何接受。

四、Bolt

bolt介面的設計與spout的類似:

4.1 IBolt 介面

 /**
  * 在客戶端計算機上建立的IBolt物件。會被被序列化到topology中(使用Java序列化),並提交給叢集的主機(Nimbus)。  
  * Nimbus啟動workers反序列化物件,呼叫prepare,然後開始處理tuples。
 */

public interface IBolt extends Serializable {
    /**
     * 元件初始化時候被呼叫
     *
     * @param conf storm中定義的此bolt的配置
     * @param context 應用上下文,可以通過其獲取任務ID和元件ID,輸入和輸出資訊等。
     * @param collector  用來傳送spout中的tuples,它是執行緒安全的,建議儲存為此spout物件的例項變數
     */
    void prepare(Map stormConf, TopologyContext context, OutputCollector collector);

    /**
     * 處理單個tuple輸入。
     * 
     * @param Tuple物件包含關於它的後設資料(如來自哪個元件/流/任務)
     */
    void execute(Tuple input);

    /**
     * IBolt將要被關閉的時候呼叫。但是其不一定會被執行,如果在叢集環境中通過kill -9 殺死程式時其就無法被執行。
     */
    void cleanup();

4.2 BaseRichBolt抽象類

同樣的,在實現自定義bolt時,通常是繼承BaseRichBolt抽象類來實現。BaseRichBolt繼承自BaseComponent抽象類並實現了IRichBolt介面。

分散式流處理框架 Apache Storm —— 程式設計模型詳解

IRichBolt介面繼承自IBoltIComponent,自身並沒有定義任何方法:

public interface IRichBolt extends IBolt, IComponent {

}

通過這樣的設計,在繼承BaseRichBolt實現自定義bolt時,就只需要實現三個必須的方法:

  • prepare: 來源於IBolt,可以通過此方法獲取用來傳送tuples的OutputCollector
  • execute:來源於IBolt,處理tuples和傳送處理完成的tuples;
  • declareOutputFields :來源於IComponent,宣告傳送的tuples的名稱,這樣下一個元件才能知道如何接收。

五、詞頻統計案例

5.1 案例簡介

這裡我們使用自定義的DataSourceSpout產生詞頻資料,然後使用自定義的SplitBoltCountBolt來進行詞頻統計。

分散式流處理框架 Apache Storm —— 程式設計模型詳解

案例原始碼下載地址:storm-word-count

5.2 程式碼實現

1. 專案依賴

<dependency>
    <groupId>org.apache.storm</groupId>
    <artifactId>storm-core</artifactId>
    <version>1.2.2</version>
</dependency>

2. DataSourceSpout

public class DataSourceSpout extends BaseRichSpout {

    private List<String> list = Arrays.asList("Spark", "Hadoop", "HBase", "Storm", "Flink", "Hive");

    private SpoutOutputCollector spoutOutputCollector;

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

    @Override
    public void nextTuple() {
        // 模擬產生資料
        String lineData = productData();
        spoutOutputCollector.emit(new Values(lineData));
        Utils.sleep(1000);
    }

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


    /**
     * 模擬資料
     */
    private String productData() {
        Collections.shuffle(list);
        Random random = new Random();
        int endIndex = random.nextInt(list.size()) % (list.size()) + 1;
        return StringUtils.join(list.toArray(), "\t", 0, endIndex);
    }

}

上面類使用productData方法來產生模擬資料,產生資料的格式如下:

Spark   HBase
Hive    Flink   Storm   Hadoop  HBase   Spark
Flink
HBase   Storm
HBase   Hadoop  Hive    Flink
HBase   Flink   Hive    Storm
Hive    Flink   Hadoop
HBase   Hive
Hadoop  Spark   HBase   Storm

3. SplitBolt

public class SplitBolt extends BaseRichBolt {

    private OutputCollector collector;

    @Override
    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {
        this.collector=collector;
    }

    @Override
    public void execute(Tuple input) {
        String line = input.getStringByField("line");
        String[] words = line.split("\t");
        for (String word : words) {
            collector.emit(new Values(word));
        }
    }

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

4. CountBolt

public class CountBolt extends BaseRichBolt {

    private Map<String, Integer> counts = new HashMap<>();

    @Override
    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {

    }

    @Override
    public void execute(Tuple input) {
        String word = input.getStringByField("word");
        Integer count = counts.get(word);
        if (count == null) {
            count = 0;
        }
        count++;
        counts.put(word, count);
        // 輸出
        System.out.print("當前實時統計結果:");
        counts.forEach((key, value) -> System.out.print(key + ":" + value + "; "));
        System.out.println();
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {

    }
}

5. LocalWordCountApp

通過TopologyBuilder將上面定義好的元件進行串聯形成 Topology,並提交到本地叢集(LocalCluster)執行。通常在開發中,可先用本地模式進行測試,測試完成後再提交到伺服器叢集執行。

public class LocalWordCountApp {

    public static void main(String[] args) {
        TopologyBuilder builder = new TopologyBuilder();
        
        builder.setSpout("DataSourceSpout", new DataSourceSpout());
        
        // 指明將 DataSourceSpout 的資料傳送到 SplitBolt 中處理
        builder.setBolt("SplitBolt", new SplitBolt()).shuffleGrouping("DataSourceSpout");
        
        //  指明將 SplitBolt 的資料傳送到 CountBolt 中 處理
        builder.setBolt("CountBolt", new CountBolt()).shuffleGrouping("SplitBolt");

        // 建立本地叢集用於測試 這種模式不需要本機安裝storm,直接執行該Main方法即可
        LocalCluster cluster = new LocalCluster();
        cluster.submitTopology("LocalWordCountApp",
                new Config(), builder.createTopology());
    }

}

6. 執行結果

啟動WordCountApp的main方法即可執行,採用本地模式Storm會自動在本地搭建一個叢集,所以啟動的過程會稍慢一點,啟動成功後即可看到輸出日誌。

分散式流處理框架 Apache Storm —— 程式設計模型詳解

六、提交到伺服器叢集執行

6.1 程式碼更改

提交到伺服器的程式碼和原生程式碼略有不同,提交到伺服器叢集時需要使用StormSubmitter進行提交。主要程式碼如下:

為了結構清晰,這裡新建ClusterWordCountApp類來演示叢集模式的提交。實際開發中可以將兩種模式的程式碼寫在同一個類中,通過外部傳參來決定啟動何種模式。

public class ClusterWordCountApp {

    public static void main(String[] args) {
        TopologyBuilder builder = new TopologyBuilder();
        
        builder.setSpout("DataSourceSpout", new DataSourceSpout());
        
        // 指明將 DataSourceSpout 的資料傳送到 SplitBolt 中處理
        builder.setBolt("SplitBolt", new SplitBolt()).shuffleGrouping("DataSourceSpout");
        
        //  指明將 SplitBolt 的資料傳送到 CountBolt 中 處理
        builder.setBolt("CountBolt", new CountBolt()).shuffleGrouping("SplitBolt");

        // 使用StormSubmitter提交Topology到伺服器叢集
        try {
            StormSubmitter.submitTopology("ClusterWordCountApp",  new Config(), builder.createTopology());
        } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) {
            e.printStackTrace();
        }
    }

}

6.2 打包上傳

打包後上傳到伺服器任意位置,這裡我打包後的名稱為storm-word-count-1.0.jar

# mvn clean package -Dmaven.test.skip=true

6.3 提交Topology

使用以下命令提交Topology到叢集:

# 命令格式: storm jar jar包位置 主類的全路徑 ...可選傳參
storm jar /usr/appjar/storm-word-count-1.0.jar  com.heibaiying.wordcount.ClusterWordCountApp

出現successfully則代表提交成功:

分散式流處理框架 Apache Storm —— 程式設計模型詳解

6.4 檢視Topology與停止Topology(命令列方式)

# 檢視所有Topology
storm list

# 停止  storm kill topology-name [-w wait-time-secs]
storm kill ClusterWordCountApp -w 3
分散式流處理框架 Apache Storm —— 程式設計模型詳解

6.5 檢視Topology與停止Topology(介面方式)

使用UI介面同樣也可進行停止操作,進入WEB UI介面(8080埠),在Topology Summary中點選對應Topology 即可進入詳情頁面進行操作。

分散式流處理框架 Apache Storm —— 程式設計模型詳解

七、關於專案打包的擴充套件說明

mvn package的侷限性

在上面的步驟中,我們沒有在POM中配置任何外掛,就直接使用mvn package進行專案打包,這對於沒有使用外部依賴包的專案是可行的。但如果專案中使用了第三方JAR包,就會出現問題,因為package打包後的JAR中是不含有依賴包的,如果此時你提交到伺服器上執行,就會出現找不到第三方依賴的異常。

這時候可能大家會有疑惑,在我們的專案中不是使用了storm-core這個依賴嗎?其實上面之所以我們能執行成功,是因為在Storm的叢集環境中提供了這個JAR包,在安裝目錄的lib目錄下:

分散式流處理框架 Apache Storm —— 程式設計模型詳解

為了說明這個問題我在Maven中引入了一個第三方的JAR包,並修改產生資料的方法:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8.1</version>
</dependency>

StringUtils.join()這個方法在commons.lang3storm-core中都有,原來的程式碼無需任何更改,只需要在import時指明使用commons.lang3

import org.apache.commons.lang3.StringUtils;

private String productData() {
    Collections.shuffle(list);
    Random random = new Random();
    int endIndex = random.nextInt(list.size()) % (list.size()) + 1;
    return StringUtils.join(list.toArray(), "\t", 0, endIndex);
}

此時直接使用mvn clean package打包執行,就會丟擲下圖的異常。因此這種直接打包的方式並不適用於實際的開發,因為實際開發中通常都是需要第三方的JAR包。

分散式流處理框架 Apache Storm —— 程式設計模型詳解

想把依賴包一併打入最後的JAR中,maven提供了兩個外掛來實現,分別是maven-assembly-pluginmaven-shade-plugin。鑑於本篇文章篇幅已經比較長,且關於Storm打包還有很多需要說明的地方,所以關於Storm的打包方式單獨整理至下一篇文章:

Storm三種打包方式對比分析

參考資料

  1. Running Topologies on a Production Cluster
  2. Pre-defined Descriptor Files

更多大資料系列文章可以參見個人 GitHub 開源專案: 大資料入門指南

相關文章