《分佈式實時計算系統之Storm》一、基本原理

Storm架構

Storm是一個分佈式、可靠的實時計算系統。與Hadoop不同的是,它採用流式的消息處理方法,對於每條消息輸入到系統中後就能被立即處理。適用於一些對實時性要求高的場景,比如廣告點擊在線統計、交易額實時統計等。

一些名詞解釋

  • Stream:Storm中被處理的數據流,一條消息稱爲一個元組。

  • Spout:Storm連接外部數據源的組件,可以認爲Storm的數據源。

  • Bolt:數據處理組件,Bolt裏面封裝了處理數據的邏輯。Spout和Bolt是Storm中的兩類組件,類似MapReduce中的Map和Reduce。比如可以在Bolt上定義過濾、聚合、join、寫數據庫等。

  • Stream Group:消息分組策略,定義了Bolt組件以何種方式接收數據。Storm內置了八種消息分組策略,我們也可以通過實現CustomStreamGrouping定義自己的消息分組策略。

    1. Shuffle grouping:隨機分配消息給Bolt task,能夠保證每個Bolt task都能分配到相同數據量的元組。
    2. Fileds grouping:根據字段進行劃分,比如按“user-id”字段進行劃分,那麼相同“user-id”的值會被分配到一個Bolt task中。
    3. Partial grouping:類似Filed grouping,但是能夠保證下游Bolt任務負載均衡。
    4. All grouping:將每條消息都廣播給所有Bolt任務,也就是說每個Bolt處理的數據完全相同。需要小心使用。
    5. Global grouping:所有消息流數據全部發送到一個Bolt任務中。
    6. None grouping:不關心分組策略,相當於Shuffle grouping。
    7. Direct grouping:直接分組,上游指定哪個Bolt任務接受數據。
    8. Local or shuffle grouping:資源本地化的一種實現方式,如果任務都在同一個進程中,則會發送到該Bolt任務中。入果沒有,相當於shuffle grouping。
  • Topology:由消息分組將Spout和Bolt連接起來的任務拓撲。相當於MapReduce中Map和Reduce組成的任務。

  • Worker:工作進程,運行在Supervisor節點上,一個Woker進程可以包含一個或多個Executor線程。每個Woker進程上會執行一組Task,比如Storm集羣總共有50個Woker,如果Task總數爲300,那麼每個Worker上面需要執行6個Task。Worker執行topology的一個子集(不會出現一個Worker進程爲多個Topology提供服務),一個Topology任務可能由多個Woker進程負責執行。

  • Executor:執行Spout或Bolt任務的線程,由Worker進程創建。每個Executor線程只會執行一個Topology中的一個Component的task實例(但不一定只執行一個task,可能執行多個task)。

  • Task:Storm中最小的處理單元,一個Executor中可以運行一個或多個Task。Topology中的Spout和Bolt可以設置並行度,一個並行度對應一個Task。

一個Topology任務啓動後,組件(Spout或Bolt)的task數量就已經確定了(就是組件的並行度)。但是我們可以爲該組件添加執行線程,也就是Executor(因爲有可能一個Executor執行了多個task,爲了提高執行效率,可以增加Executor線程。但是需要注意,一個Executor只會爲同一個Component的task服務)。默認情況下,task數是等於Executor數的,即一個Executor執行一個task。

Storm架構

Storm集羣有兩類節點:運行Nimbus守護進程的主節點和運行Supervisor守護進程的工作節點。Nimbus節點用於分配代碼、分配計算任務(分配給哪些Supervisor上的哪些Woker)和監控狀態(用於故障檢測、恢復)。Supervisor節點負責監聽工作(監聽Nimbus分配的任務)、啓動並停止Woker進程。

Worker是運行在Supervisor上的進程,Supervisor收到Nimbus分配的任務後,負責啓動Nimbus指定的Woker進程。Woker進程執行Topology的子集,一個Topology任務可能由運行在多臺機器上的Worker進程組成。

Woker進程啓動一個或多個Executor線程,Executor線程中可以有一個或多個Task。每個Executor都會啓動一個消息循環線程,用於接受、處理和發送消息。

Nimbus和Supervisor之間協調工作也是由Zookeeper來完成的。

image.png

Nimbus和Supervisor都能快速失敗恢復,而且它們都是無狀態的,狀態信息存儲在Zookeeper(元數據)和本地中。當Nimbus或Supervisor掛掉後,可以重新啓動並讀取狀態信息到集羣中來正常運行,所以Storm系統具有很高的容錯性。

在邏輯上將Storm中消息來源節點稱爲Spout,消息處理節點稱爲Bolt,它們通過流分組組成Topology。

 

image.png

Storm中元數據

爲了更好的理解Storm的設計,我們可以通過Zookeeper中存儲的元數據來理解Storm架構中各個節點之間的關係。

image.png

Storm在Zookeeper存儲的消息都經都是以/storm開始,所有數據都存儲在葉子節點上。下面說一個每個節點數據存儲的具體含義:

  • /storm/workerbeats/{topology-id}/node-port:拓撲任務所在的Woker進程信息,node和port是Woker所在的主機和端口。裏面主要存儲了Woker的運行狀態和統計信息。Woker進程會定時上報心跳到該節點,Nimbus通過心跳信息來確認Woker進程的存活,對於死掉的Woker,Nimbus會重新調度。統計信息包括該Woker上所有Executor的統計信息,比如發送消息數,接受消息數等,這些信息會顯示在UI中。一個Topology任務可能劃分到多個Woker(node-port)上。
  • /storm/storms/{topolog-id}:存儲了拓撲任務的本身信息,比如名字、啓動時間、運行狀態、使用Woker數、每個組件的並行度等。這個信息在任務註冊後,就不會在發生改變。該節點信息,可以幫助Nimbus進行資源分配,因爲能夠知道哪些Supervisor上面有哪些任務。
  • /storm/assignments/{topology-id}:Nimbus爲每個Topology任務的分配信息,比如Topology在該Nimbus中的存儲目錄、被分配給哪些Supervisor、Woker、Executor上。
  • /storm/supervisors/{supervisor-id>}:Supervisor節點的註冊信息,存儲了該節點自身一些統計信息,比如使用了哪些端口等。該Znode是臨時節點,當Supervisor下線後,這些信息會自動刪除,Nimbus感知到該節點下線後會重新爲該Supervisor下的任務分配節點。
  • /storm/errors/{topology-id}/{component-id}/{equential-id}:存儲運行中每個組件的錯誤信息,每個組件只會保留最近十條錯誤信息。

下面一張圖講述了節點直接如何創建、使用這些元數據:

image.png

Nimbus、Supervisor、Woker兩兩之間都需要維持心跳。Nimbus通過/storm/superviors/節點能夠知道哪些Supervisor存活。Nimbus通過/storm/workerbeats/節點能夠知道哪些Woker存活。Supervisor和Woker通過本地文件維持心跳,他們雖然是兩個進程無法直接通信,爲什麼通過文件維持心跳呢,應該有很多網絡通信框架可以吧。

Storm源碼

Storm基於Clojure和Java編寫的。其中Clojure實現了Nimbus、Supervisor、Woker、Executor以及Task這些基礎服務。Java實現了Storm的流處理(組件實現)以及事務Topology。
Storm中還有一些Trident代碼,Trident是Storm實時消息處理的更高層抽象。

Storm集羣搭建

環境準備

1、Java安裝
Storm1.x在Java7和Java8中完成了測試,所以Java版本最好是1.7+。
2、Python安裝
Storm1.x在Python2.6.6中完成了測試,理論上3.x也能夠運行,但是官方並沒有測試。
3、Zookeeper安裝
Storm使用Zookeeper進行協調服務,所以需要準備Zookeeper環境,版本官方上面沒有指定版本。

Storm安裝

下載Storm jar包

 

http://storm.apache.org/downloads.html

Storm文件配置
Storm配置文件在${STORM_HOME}/conf/storm.yaml中,下面是一些必須的配置項:

 

 #zookeeper集羣
 storm.zookeeper.servers:
     - "192.168.0.1"
     - "192.168.0.2"
     - "192.168.0.3"
 #zookeeper端口
 storm.zookeeper.port: 2181
 #可作爲nimbus的候選主機
 nimbus.seeds: ["192.168.0.1"]
 #storm數據存儲目錄,用於存儲少量狀態信息,比如jar、conf等
 storm.local.dir: "/opt/yjz/storm/data"
 #suppervisor可以作爲woker進程啓動的端口,表明該Supervisor最多可以啓動四個Worker進程
 supervisor.slots.ports:
     - 6700
     - 6701
     - 6702
     - 6703

更多配置可以查看https://github.com/apache/storm/blob/v1.2.2/conf/defaults.yaml。隨着深入瞭解,可以優化集羣配置。

節點啓動

 

nohub bin/storm nimbus &
nohub bin/storm ui &
nonub bin/storm supervisor &

查看nimbus節點,發現nimbus進程已經啓動,core爲ui進程。

 

jps
8134 nimbus
8713 core
10123 Jps
19710 QuorumPeerMain

查看supervisor節點,發現supervisor進程已經啓動。

 

jps
19237 QuorumPeerMain
7610 Supervisor
8975 Jps

查看UI,192.168.0.1:8080。

image.png

Storm編程

編程模型

我們上面說過Storm提供了兩類組件:Spout和Bolt。所以我們使用Storm編程的對象主要也就是針對Spout和Bolt。
Spout用於定義接受外部數據源數據,並將其轉換成Storm內部數據,然後將這些數據發送給Bolt。
Bolt組件定義了數據處理邏輯,也就是我們的業務處理邏輯,比如過濾、聚合、Join等。Bolt組件也可以用於寫入外部介質,比如寫入Mysql、Redis等。
組件之間傳輸的數據在Storm中稱爲元組(Tuple,可以理解爲一行數據),Tuple是Storm數據傳輸的基本單元。Tuple中定義了字段(Field,可以理解一行數據中一個字段),這些Field是有Schema的,Filed可以是byte、short、integer、long、float、double、boolean、string和byte array,除了這些基礎類型,我們還可以自定義數據類型(需要自己實現針對自定義類型的序列化)。

image.png

Storm 組件

在編寫Storm作業時我們會針對Spout和Bolt進行編程實現,下面我們分別看一下Storm爲我們提供的組件接口。

Spout組件

Spout是Storm中Topology的生產者,負責讀取外部數據,並轉換成Tuple。Spout可以設置處理的消息類型爲可靠或不可靠。對可靠的消息,Spout會緩存發出去的消息,當該消息在topology中處理失敗時,Spout可以重新發送該消息。對於不可靠的消息,Spout一旦發送出該消息後就會將該消息扔掉,所以如果該消息處理失敗,那麼該消息就會丟失。

Spout可以發射多條消息流Stream,通過使用OutputFieldsDeclarer.declareStream()方法來定義多個Stream,然後使用SpoutOutputCollector.emit()方法在發射消息時指定Stream。

Spout中最重要的方法nextTuple(),將外部數據源數據以tuple形式發送到Topology中的Bolt組件進行處理。需要注意nextTuple()不能阻塞,因爲Storm是在一個線程內調用Spout的所有方法。

Spout對於可靠類型消息,還有兩個比較重要的方法就是ack和fail。Storm在檢測一個tuple被整個Topology執行成功的時候會回調ack,否則調用fail。

下面是Spout組件在Storm中的實現,我們分別來看下這些組件都是什麼。

image.png

IComponent接口

IComponent接口是所有組件的頂級接口,IComponent中定義了所有組件可能需要用到的方法(其實就是定義了Spout和Bolt組件都用得到的方法)。

  • declareOutputFields方法用於聲明該拓撲所有流(Stream)的輸出模式(Schema),比如聲明輸出流ID、輸出字段(Field)以及輸出流是否爲直接流。
  • getComponentConfiguration方法用於聲明該組件的配置,但是它只能覆蓋以"topology.*"配置爲開頭的子集。我們也可以通過TopologyBuilder構建拓撲時,對組件進行進一步配置覆蓋。

ISpout接口

ISpout是Spout組件的定義的頂級接口,它定義了Spout組件支持的方法。Storm會在相應的階段調用ISpout接口中特定的方法,比如啓動拓撲時會首先調用open()方法,正常停止拓撲時會調用close()方法(kill -9不會被調用)。

  • open方法會在任務在集羣工作進程初始化時調用,用於提供Spout執行所需要的環境。比如讀取外部數據源的一些初始化配置可以寫在這裏。Map類型的conf參數是這個Spout的配置,它包含了拓撲與集羣配置的合併集。TopologyContext類型的context是該拓撲的上下文,包括拓撲id、組件id、輸入輸出信息等。SpoutOutputCollector類型的collector參數是Spout的收集器,用於發射tuple。
  • close方法當一個ISpout關閉時會被調用,但是並不能保證一定被調用,比如Supervisor被kill -9強制殺死的時候。
  • active方法是Spout從失效模式到激活狀態時被調用。Spout可以處於失效狀態或激活狀態,處於失效狀態的Spout不會調用nextTuple方法。從失效狀態到激活狀態調用nextTuple方法前,會調用active方法。
  • deactive方法是當Spout失效時被調用,失效狀態的Spout不會調用nextTuple方法。
  • nextTuple方法是Spout組件最重要的方法,nextTuple用於讀取外部數據源數據轉換爲tuple,並且通過SpoutOutputCollector收集器發射tuple。由於nextTuple、ack、fail方法都是在一個線程內被調用,所以nextTuple方法不應該有阻塞代碼。
  • ack方法是當Storm確定從該Spout發射出去標識符爲msgId的消息被topology完整處理完成時,會調用ack方法,我們可以在這裏實現一些邏輯,比如從我們的數據源隊列中移除該消息。
  • fail方法和ack方法相反,當msgId消息在topology中被處理失敗時會被調用。

Spout的可靠性消息類型不需要我們通過fail方法實現,Storm會自動實現。ack和fail方法只是給我們獲取消息被處理成功與否的接口。

IRichSpout接口

IRichSpout接口繼承了ISpout和IComponent接口,它是我們實現Spout組件的主要接口。它本身沒有定義任何方法,所以我們實現Spout組件時候只需要實現ISpout和IComponent接口的方法。

BaseComponent抽象類

BaseComponent抽象類實現實現了IComponent組件的getComponentConfiguration方法,並且返回爲空。
它主要的作用就是對於一些Spout組件並不需要進行覆蓋配置,這時候通過繼承BaseComponent抽象類就不需要實現該方法了。

BaseXxx在Storm組件中,一般都是指一些基礎實現。目的就是爲了避免我們寫代碼時候去實現一些我們用不到的方法(我們一般都是置爲空)。

BaseRichSpout抽象類

BaseRichSpout繼承了IRichSpout接口和BaseComponent接口,它實現了一些我們在實際編寫Spout組件時可能用不到的方法。比如close、active、deactive、ack、fail方法(方法體都爲空)。這樣當我們通過繼承BaseRichSpout抽象類來實現Spout組件時候,這些方法我們都可以不需要實現了。

 

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) {
    }
}

總結

當我們編寫Spout組件時,如果想要實現Spout給我們提供的所有方法,可以直接實現接口IRichSpout接口。如果我們只需要實現Spout組件所必須的方法,可以直接繼承BaseRichSpout抽象類。

Bolt組件接口

所有的消息處理邏輯被封裝到Bolt中,比如可以用來做過濾、聚合、查詢數據庫、寫入數據等。
Bolt也可以發送多條消息流Stream,使用OutputFieldsDeclarer.declareStream()方法定義Stream,然後使用OutputCollector.emit()方法在發射消息時,指定Stream。

Bolt最重要的方法時execute(),它以接受一個tuple,經過邏輯處理後,使用OutputCollector.emit()發射出0個或多個tuple。Bolt還需要爲每個tuple調用ack方法,來通知Storm這個消息被該Bolt task執行完成,從而通知這個tuple的發射者Spout(調用Spout的ack或fail)。

Bolt組件定義的邏輯和Spout組件的類似,下面是Bolt組件的實現方式。

image.png

IBolt接口

IBolt接口是Bolt組件的頂級接口,它定義了Bolt組件所需要的方法。IBolt的設計原則是以一個元組作爲輸入,通過邏輯處理生成零個或多個元組輸出。

  • prepare方法在拓撲作業在工作進程初始化時調用。和ISpout中的open方法一樣,做一些組件初始化的工作。prepare方法同樣提供了三個stormConf、context和collector,用途和ISpout中的open方法一樣。需要注意Bolt中的collector的類型是OutputCollector。
  • execute方法是Bolt組件最重要的方法,接收上游發送的元組,執行業務邏輯,然後通過OutputCollector來向下遊發射元組。在執行完execute方法後,我們應該調用ack或fail方法來通知Storm該消息已經被處理(否則Storm一直會等到消息處理超時,才認爲該消息處理失敗)。
  • cleanup方法當IBolt被即將關閉時調用,和ISpout的close方法一樣,不保證一定被執行到。

execute中的tuple可以不立即執行,可以等待其它元組到來一起執行。比如做聚合、join等操作。

IRichBolt接口

IRichBolt接口實現了IBolt和IComponent接口,它的作用和ISpout接口一樣,這裏就不在複述了。

BaseRichBolt抽象方法

BaseRichBolt抽象類和BaseRichSpout抽象方法一樣,對一些我們可能用不到的方法,提供默認實現。

 

public abstract class BaseRichBolt extends BaseComponent implements IRichBolt {
    @Override
    public void cleanup() {
    }    
}

IBasicBolt、BaseBasicBolt

使用IBasicBolt和BaseBasicBolt類來實現Bolt組件,我們可以不需要手動ack,IBasicBolt的execute方法會自動執行Acking機制(仔細看它使用的是BasicOutputCollector在emit後,執行ack方法)。如果我們希望該元組失敗,可以顯示拋出一個FailedException異常。

image.png

IBasicBolt接口方法和IRichBolt具有相同的方法,只是prepare中沒有傳遞OutputCollector收集器了,而是在execute方法中直接傳遞了BasicOutputCollector。所以如果我們不需要對該Bolt組件添加配置和獲取拓撲上下文對象,可以直接實現BaseBasicBolt抽象類,因爲該類提供了prepare和cleanup的默認實現,我們只需要實現execute方法即可。

總結

如果我們不想要手動調用ack,可以繼承IBasicBolt或BaseBasickBolt來實現Bolt組件。當然,如果想要顯示靈活調用,可以通過繼承IRichBolt或BaseRichBolt來實現Bolt組件。

Stream Groupings

我們上面說過Topology是通過Stream Grouping將Spout和Bolt組合而成的。無論Bolt還是Spout我們都可以爲其設置並行度,並行度對應着task,而Stream Grouping定義了該Bolt中的所有task以什麼形式來接受數據流。Stream Grouping支持的八種流分組方式在上面已經說過了,這裏就不說了。

Topology配置

當我編寫完Spout和Bolt組件後,需要提供一個主類來設置拓撲。這個主類也是Storm執行Topology任務的入口類。

並行度

在說Topology配置前,我們先理解一下Storm中的並行度。
在Storm中運行Topology任務主要依賴下面三個實體:

  • 工作進程(Worker processes)
  • 執行線程(Executor)
  • 任務(Task)

image.png

一個工作進程運行一個Topology的子集,並且每個工作進程只屬於一個Topology任務(不會存在一個Woker服務多個Topology),一個Topology任務可能由多臺機器的多個Woker組成。
Woker進程中可以啓動多個Executor線程,每個Executor線程運行一個或多個Task,但是這些task必須是同一Component中的。也就是每個Executor只能服務於一個Component。
Task是具體執行數據處理的,我們實現的Bolt或Spout組件中每個組件可以啓動一個或多個task來執行,以達到提高處理效率的目的。Task數在Topology啓動後就不能在改變了,但是我們可以修改執行Task的Executor線程數,來動態調整爲該拓撲分配的資源。默認情況下,每個Executor會對應一個task。

配置實例

我們首先需要使用TopologyBuilder來配置拓撲關係,在設置過程中可以添加配置、設置組件執行Executor數量、設置組件task數以及設置Stream Grouping。

 

public static void main(String[] args) throws Exception{
        //設置Component(bolt和spout)之間的拓撲結構
        TopologyBuilder builder = new TopologyBuilder();
        //添加spout組件
        builder.setSpout("word-reader",new WordReaderSpout()).addConfiguration("topology.debug",true);
        //添加bolt組件,並設置組件所需task數,這裏沒有設置Executor數量,默認會爲每個task啓動一個Executor線程。這裏還設置了以shuffleGrouping分組方式接收上游word-reader組件發送的消息
        builder.setBolt("word-normalizer",new WordNormalizerBolt()).setNumTasks(2).shuffleGrouping("word-reader");
        //設置bolt組件,並且設置了啓動Executor數,這裏沒有設置task數量,默認會爲麼給Executor分配一個task。這裏還設置了以shuffleGrouping分組方式接收上游word-normailizer組件發送的消息
        builder.setBolt("word-counter",new WordCounterBolt()).shuffleGrouping("word-normalizer",3);

        //運行時與集羣配置合併,並通過prepare或open方法發送給所有組件節點
        Config conf = new Config();
        //設置運行該拓撲需要幾個Worker進程,如果沒有設置默認爲1個。
        conf.setNumWorkers(2);
        conf.put(Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS,10);

        conf.put("word-file",args[0]);
        conf.setDebug(false);
        conf.put(Config.TOPOLOGY_MAX_SPOUT_PENDING,1);
        //使用集羣方式提交拓撲執行
        StormSubmitter.submitTopology("storm-hello-word",conf,builder.createTopology());

    }

如果集羣中Woker數量被用完(storm.yaml中設置的supervisor.slots.ports),在提交新Topology時會失敗。需要等待運行中的Topology釋放資源後纔可以執行。

更新運行中的Topology的Executor

Storm提供了對運行中Topology任務的Executor熱更新,有兩種方式可以進行更新。

  • 使用Storm web UI中rebalance更新。
  • 是用Storm命令行工具CLI進行更新。

比如將myTopology任務改爲5個工作進程,組件blue-spout使用3個Executor,組件yello-bolt使用5個bolt。

 

storm rebalance myTopology -n 5 -e blue-spout=3 -e yello-bolt=5

Storm配置文件

Storm有許多各種各樣的配置,有些是系統配置不能通過Topology任務進行修改,而有些配置是支持在Topology任務中進行修改。
默認Storm中的所有配置都在Storm代碼庫中的default.yaml中,我們可以通過在Nimbus節點或Supervisor節點中的storm.yaml進行覆蓋。除了通過storm.yaml進行覆蓋修改配置外,我們還可以通過StormSubmitter構建拓撲時來修改配置(傳入的Config對象),但是這裏只能修改以TOPOLOGY爲前綴的配置項。

我們也可以通過Java API修改配置,有兩種方式:

  • 內部修改:覆蓋Spout或Bolt的getComponentConfiguration方法來修改配置。
  • 外部修改:在TopologyBuilder中的setSpout或setBolt方法返回的對象調用addConfiguration來覆蓋配置。

這些配置的優先級爲:
default.yaml < storm.yaml < 配置拓撲添加配置項 < 組件內部添加配置項 < 組件外部添加配置項

Maven開發配置

使用Maven開發Storm作業,首先就需要配置pom.xml文件,包括Jar包引入、打包配置等。
Storm目前版本是1.2.2,Jar包引入:

 

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

這裏之所有使用provided模式是因爲,Storm會自動從工作節點下載Storm Jar包。
當我們編寫完Storm作業後,需要將相關依賴打到一個Jar包內,可以使用assembly插件進行打包。

 

  <plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <configuration>
      <descriptorRefs>  
        <descriptorRef>jar-with-dependencies</descriptorRef>
      </descriptorRefs>
      <archive>
        <manifest>
          <mainClass>com.path.to.main.Class</mainClass>
        </manifest>
      </archive>
    </configuration>
  </plugin>

其中mainClass就是我們作業的啓動主類。

本地模式

Storm爲了方便開發測試,提供了本地模式運行Storm Topology,本地模式模擬了集羣模式下作業的執行,所以我們在開發調試過程中可以使用本地模式測試開發的Storm作業。
本地模式將Spout和Bolt都運行在一個進程上的多個線程執行,來模擬真實的集羣運行情況。對於一些耗時操作,會採用Thread.sleep()方法模擬,所以有時會導致運行速度緩慢。

 

//只需要創建LocalCluster對象類就可以使用本地模式
LocalCluster localCluster = new LocalCluster();
//提交作業
localCluster.submitTopology();
//停止作業
localCluster.killTopology();
//關閉本地集羣模式
localCluster.shutdown();

Local模式提供了一下配置型:

 

Config.TOPOLOGY_MAX_TASK_PARALLELISM:設置Topology的最大並行度。
Config.TOPOLOGY_DEBUG:開啓DEBUG模式,方便作業調試。

Storm核心機制原理

Storm消息可靠性處理

Storm提供了幾種不同級別的消息可靠性處理:

  • 盡力處理(best effort)
  • 至少一次被處理(at least once)
  • 恰好處理一次(exactly once,需要藉助Trident)

盡力處理屬於最低級別保證機制,我們可以不添加任何額外操作,Storm就能幫我們達到。

至少一次被處理(at least once)

Spout發出的消息可能會產生成千上萬條消息(經過各種Bolt task處理後就會分散出一條消息),這些消息會組成一顆消息樹,其中Spout發出的消息爲消息根,Storm會跟蹤整棵樹的處理情況,如果這顆樹中的任一消息處理失敗,或者整棵樹在規定時間沒有被處理完(通過Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS配置,默認爲30s),那麼Storm就認爲Spout發出的這些消息處理失敗了,Spout會重新發送該條消息。

判斷一個tuple tree是否被處理完成,有以下兩個條件:

  1. tuple tree不在生長。
  2. tuple tree中的任何消息都被處理。

使用Storm API來實現

如果想要使用Storm提供的可靠性處理,我們需要做兩件事:

  1. 無論何時,只要在tuple tree中創建了一個新節點,就需要告知Storm。
  2. 當處理完一個消息後,需要告知Storm中對應tuple tree。

通過上面兩個步驟,Storm就可以檢測一個tuple tree是否被處理完成,並且會調用消息產生對應Spout的ack和fail方法。

當爲tuple tree中指定的節點增加一個新節點時,稱爲錨定(anchoring)。錨定是在發送消息的同時進行的,具體錨定方式爲:把輸入消息作爲emit方法的第一個參數。這樣就告知tuple tree,該節點產生了新節點,只有當新節點也完成時tuple tree纔算執行完成。

 

public class SplitSentence extends BaseRichBolt {
        ...
        public void execute(Tuple tuple) {
            String sentence = tuple.getString(0);
            for(String word: sentence.split(" ")) {
                _collector.emit(tuple, new Values(word));
            }
            _collector.ack(tuple);
        }
        ...

Storm支持一個輸出消息被錨定在一個或多個輸入消息上,比如join、聚合等場景。一個被多重錨定的消息處理失敗,會導致與之關聯的多個Spout重新發送消息。

 

List<Tuple> anchors = new ArrayList<Tuple>();
anchors.add(tuple1);
anchors.add(tuple2);
_collector.emit(anchors, new Values(1, 2, 3));

多重錨定會將錨定的消息添加到多棵tuple tree上,並且有可能打破樹結構,從而形成一個DAG圖。

如果沒有錨定,也就是沒有在emit方法的第一個參數指定輸入tuple。那麼這個節點所產生的子樹失敗,spout不會重新發送消息。該節點ack完成後,tuple tree就認爲被處理完成了。有些場景非常適合這種不需要錨定的消息。

完成錨定後,我們還需要在消息被處理完成後告知tuple tree。我們必須在每個execute方法的後面顯示調用OutputCollector的ack或fail方法,來表明該消息在該bolt是否被處理完成(否則會一直等到超時)。
顯示調用ack或fail,是除了快速告知tuple tree消息是否被處理完成外,還有一個原因就是防止內存被打滿。因爲Storm使用內存來跟蹤每個元組是否被處理完成,所以如果不調用ack或fail,很容易將內存打滿。

Storm提供了IBasicBolt和BaseBasicBolt接口來隱式調用ACK機制,也就是說我們如果使用它們實現Bolt組件,就不需要手動錨定和調用ack/fail方法了。

 

public class SplitSentence extends BaseBasicBolt {
        public void execute(Tuple tuple, BasicOutputCollector collector) {
            String sentence = tuple.getString(0);
            for(String word: sentence.split(" ")) {
                collector.emit(new Values(word));
            }
        }

針對於多重錨定,IBasicBolt和BaseBasicBolt是無法處理的。需要我們顯示完成,也就是需要實現IRichBolt或BaseRichBolt定義Bolt組件。

acker框架

Storm使用Acker框架來跟蹤消息是否被成功處理。Acker是Storm中一組特殊的任務,用於跟蹤每個Spout發送tuple的DAG。當acker發現DAG中節點都完全被處理完成後,它會向創建該tuple的Spout發送一條消息(成功或失敗)。
我們可以使用Config.TOPOLOGY_ACKERS在拓撲配置中設置Acker數量,默認情況下每個Woker進程會啓動一個Acker任務。

當在Topology中創建一個新的元組時,會爲每個元組分配一個64bit的隨機id(無論spout還是bolt組件)。Acker使用這些id來跟蹤tuple tree。每個tuple被創建後tuple tree中的根id都會被複制到這個消息中,當這個消息處理完成後,它會根據根id來找到跟蹤這棵樹的Acker,並向該Acker發送狀態變更信息,比如:該元組已經處理完成,又產生了新元組,需要你跟蹤下。
這裏有個點需要考慮下,就是每個元組如何知道自己的tuple tree對應着哪個Acker。Storm使用一種哈希算法根據Spout tupel id來確定那個Acker負責該tuple tree,而每個消息都知道根id,因此就知道與哪個Acker通信了。

我們知道了Acker與Spout tuple對應關係,知道了每個tuple tree 元組如何找到對應的Acker與其通信。接下來還需要考慮一點,Acker如何跟蹤tuple tree。
我們知道每個tuple tree都有可能有成千上萬個節點,如果跟蹤每個節點,那麼內存很容易就被打滿了。Storm採用了一個不同的跟蹤策略,每個Spout元組只需要固定數據量的空間(大約20字節),就可以跟蹤tuple tree。這種跟蹤算法是Storm能夠正常工作的關鍵,也是其重大突破之一。
我們看下Storm是如何做的。Acker爲每個Spout元組存儲一個消息ID(隨機分配的那個ID)到一對值的映射,這對值的第一個元素就是Spout任務ID,第二個元素是64bit數字,稱爲“ack val”,它是tuple tree中所有消息id的異或結果。ack val代表了整棵樹的狀態,當這個ack val爲0時就代表整棵樹已經被處理完成了。

它的異或原理就是,當我們無論創建一個節點還是完成一個節點都使用消息ID來與之異或,這樣同一個消息ID一來一回異或結果就爲0了。

image.png

如上圖,ack val最終值爲T1T2T3T4T5T1T2T3T4^T5=0。

選擇合適的可靠性

Acker任務是輕量級的,所以拓撲中不需要很多Acker任務,我們可以通過Storm UI來查看Acker吞吐量,如果吞吐量很差,可以適當添加Acker任務。
如果我們認爲消息可靠性不是必要的(處理失敗情況下丟失消息沒有關係),我們可以關閉消息可靠性。這樣拓撲性能也會提升,因爲不跟蹤元組樹,傳輸的消息會減半(因爲元組樹中每個元組都需要發送一條確認信息)。並且每個下游元組中保留更少的數據(不需要存儲根ID),從而減少帶寬使用。
關閉消息可靠性的方式:

  1. 將Config.TOPOLOGY_ACKERS設置爲0,這樣Spout發送元組後,它的ack方法會被立即調用。
  2. 在Spout中使用SpoutOutputCollector.emit發送消息時不指定消息ID。這樣可以對一些特定消息關閉消息可靠性。
  3. 如果不在意某個消息派生出子消息的可靠性,那麼在對應的botl組件中可以不進行錨定(不指定輸入tuple)。

學習資料

  • Storm官網(http://storm.apache.org/releases/1.2.2/index.html),看了許多Storm書籍,大部分都是直接翻譯的Storm官方文檔。
  • 《從零開始學Storm》對Storm講解的非常全面,由淺入深,覆蓋面比較廣。
  • 《Storm源碼分析》對Storm從源碼級別進行了講解,有些地方原理講的還是不錯的,就是該書講解的Storm版本比較低。



作者:零度沸騰_yjz
鏈接:https://www.jianshu.com/p/ba4a555bd968
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章