Storm是一個分佈式是實時計算系統,它設計了一種對流和計算的抽象,概念比較簡單,實際編程開發起來相對容易。下面,簡單介紹編程實踐過程中需要理解的Storm中的幾個概念:
Storm中Topology的概念類似於Hadoop中的MapReduce Job,是一個用來編排、容納一組計算邏輯組件(Spout、Bolt)的對象(Hadoop MapReduce中一個Job包含一組Map Task、Reduce Task),這一組計算組件可以按照DAG圖的方式編排起來(通過選擇Stream Groupings來控制數據流分發流向),從而組合成一個計算邏輯更加負責的對象,那就是Topology。一個Topology運行以後就不能停止,它會無限地運行下去,除非手動干預(顯式執行bin/storm
kill )或意外故障(如停機、整個Storm集羣掛掉)讓它終止。
Storm中Spout是一個Topology的消息生產的源頭,Spout應該是一個持續不斷生產消息的組件,例如,它可以是一個Socket Server在監聽外部Client連接併發送消息,可以是一個消息隊列(MQ)的消費者、可以是用來接收Flume Agent的Sink所發送消息的服務,等等。Spout生產的消息在Storm中被抽象爲Tuple,在整個Topology的多個計算組件之間都是根據需要抽象構建的Tuple消息來進行連接,從而形成流。
Storm中消息的處理邏輯被封裝到Bolt組件中,任何處理邏輯都可以在Bolt裏面執行,處理過程和普通計算應用程序沒什麼區別,只是需要根據Storm的計算語義來合理設置一下組件之間消息流的聲明、分發、連接即可。Bolt可以接收來自一個或多個Spout的Tuple消息,也可以來自多個其它Bolt的Tuple消息,也可能是Spout和其它Bolt組合發送的Tuple消息。
Storm中用來定義各個計算組件(Spout、Bolt)之間流的連接、分組、分發關係。Storm定義瞭如下7種分發策略:Shuffle Grouping(隨機分組)、Fields Grouping(按字段分組)、All Grouping(廣播分組)、Global Grouping(全局分組)、Non Grouping(不分組)、Direct Grouping(直接分組)、Local or Shuffle Grouping(本地/隨機分組),各種策略的具體含義可以參考Storm官方文檔、比較容易理解。
下面,作爲入門實踐,我們簡單介紹幾種開發中常用的流操作處理方式的實現:
Storm組件簡單串行
這種方式是最簡單最直觀的,只要我們將Storm的組件(Spout、Bolt)串行起來即可實現,只需要瞭解編寫這些組件的基本方法即可。在實際應用中,如果我們需要從某一個數據源連續地接收消息,然後順序地處理每一個請求,就可以使用這種串行方式來處理。如果說處理單元的邏輯非常複雜,那麼就需要處理邏輯進行分離,屬於同一類操作的邏輯封裝到一個處理組件中,做到各個組件之間弱耦合(除了定義Field的schema外,只通過發送消息來連接各個組件)。
下面,我實現一個簡單的WordCount的例子,各個組件之間的連接方式,如下圖所示:
ProduceRecordSpout類是一個Spout組件,用來產生消息,我們這裏模擬發送一些英文句子,實際應用中可以指定任何數據源,如數據庫、消息中間件、Socket連接、RPC調用等等。ProduceRecordSpout類代碼如下所示:
01 |
public static class ProduceRecordSpout extends BaseRichSpout
{ |
03 |
private static final long serialVersionUID
= 1L; |
04 |
private static final Log
LOG = LogFactory.getLog(ProduceRecordSpout. class ); |
05 |
private SpoutOutputCollector
collector; |
06 |
private Random
random; |
07 |
private String[]
records; |
09 |
public ProduceRecordSpout(String[]
records) { |
10 |
this .records
= records; |
14 |
public void open(Map
conf, TopologyContext context, SpoutOutputCollector collector) { |
15 |
this .collector
= collector; |
16 |
random
= new Random(); |
20 |
public void nextTuple()
{ |
22 |
String
record = records[random.nextInt(records.length)]; |
23 |
List<Object>
values = new Values(record); |
24 |
collector.emit(values,
values); |
25 |
LOG.info( "Record
emitted: record=" +
record); |
29 |
public void declareOutputFields(OutputFieldsDeclarer
declarer) { |
30 |
declarer.declare( new Fields( "record" )); |
構造一個ProduceRecordSpout對象時,傳入一個字符串數組,然後隨機地選擇其中一個句子,emit到下游(Downstream)的WordSplitterBolt組件,只聲明瞭一個Field,WordSplitterBolt組件可以根據聲明的Field,接收到emit的消息,WordSplitterBolt類代碼實現如下所示:
01 |
public static class WordSplitterBolt extends BaseRichBolt
{ |
03 |
private static final long serialVersionUID
= 1L; |
04 |
private static final Log
LOG = LogFactory.getLog(WordSplitterBolt. class ); |
05 |
private OutputCollector
collector; |
08 |
public void prepare(Map
stormConf, TopologyContext context, |
09 |
OutputCollector
collector) { |
10 |
this .collector
= collector; |
14 |
public void execute(Tuple
input) { |
15 |
String
record = input.getString( 0 ); |
16 |
if (record
!= null &&
!record.trim().isEmpty()) { |
17 |
for (String
word : record.split( "\\s+" ))
{ |
18 |
collector.emit(input, new Values(word, 1 )); |
19 |
LOG.info( "Emitted:
word=" +
word); |
26 |
public void declareOutputFields(OutputFieldsDeclarer
declarer) { |
27 |
declarer.declare( new Fields( "word" , "count" )); |
在execute方法中,傳入的參數是一個Tuple,該Tuple就包含了上游(Upstream)組件ProduceRecordSpout所emit的數據,直接取出數據進行處理。上面代碼中,我們將取出的數據,按照空格進行的split,得到一個一個的單詞,然後在emit到下一個組件,聲明的輸出schema爲2個Field:word和count,當然這裏面count的值都爲1。
進行統計詞頻的組件爲WordCounterBolt,實現代碼如下所示:
01 |
public static class WordCounterBolt extends BaseRichBolt
{ |
03 |
private static final long serialVersionUID
= 1L; |
04 |
private static final Log
LOG = LogFactory.getLog(WordCounterBolt. class ); |
05 |
private OutputCollector
collector; |
06 |
private final Map<String,
AtomicInteger> counterMap = Maps.newHashMap(); |
09 |
public void prepare(Map
stormConf, TopologyContext context, |
10 |
OutputCollector
collector) { |
11 |
this .collector
= collector; |
15 |
public void execute(Tuple
input) { |
16 |
String
word = input.getString( 0 ); |
17 |
int count
= input.getIntegerByField( "count" ); |
18 |
AtomicInteger
ai = counterMap.get(word); |
20 |
ai
= new AtomicInteger( 0 ); |
21 |
counterMap.put(word,
ai); |
24 |
LOG.info( "DEBUG:
word=" +
word + ",
count=" +
ai.get()); |
29 |
public void declareOutputFields(OutputFieldsDeclarer
declarer) { |
33 |
public void cleanup()
{ |
35 |
LOG.info( "Word
count results:" ); |
36 |
for (Entry<String,
AtomicInteger> entry : counterMap.entrySet()) { |
37 |
LOG.info( "\tword=" +
entry.getKey() + ",
count=" +
entry.getValue().get()); |
上面代碼通過一個Map來對每個單詞出現的頻率進行累加計數,比較簡單。因爲該組件是Topology的最後一個組件,所以不需要在declareOutputFields方法中聲明Field的Schema,而是在cleanup方法中輸出最終的結果,只有在該組件結束任務退出時纔會調用cleanup方法輸出。
最後,需要基於上面的3個組件來創建一個Topology實例,提交到Storm集羣去運行,配置代碼如下所示:
01 |
public static void main(String[]
args) throws AlreadyAliveException,
InvalidTopologyException, InterruptedException { |
03 |
TopologyBuilder
builder = new TopologyBuilder(); |
04 |
String[]
records = new String[]
{ |
05 |
"A
Storm cluster is superficially similar to a Hadoop cluster" , |
06 |
"All
coordination between Nimbus and the Supervisors is done through a Zookeeper cluster" , |
07 |
"The
core abstraction in Storm is the stream" |
10 |
.setSpout( "spout-producer" , new ProduceRecordSpout(records), 1 ) |
13 |
.setBolt( "bolt-splitter" , new WordSplitterBolt(), 2 ) |
14 |
.shuffleGrouping( "spout-producer" ) |
16 |
builder.setBolt( "bolt-counter" , new WordCounterBolt(), 1 ) |
17 |
.fieldsGrouping( "bolt-splitter" , new Fields( "word" )) |
21 |
Config
conf = new Config(); |
22 |
String
name = WordCountTopology. class .getSimpleName(); |
23 |
if (args
!= null &&
args.length > 0 )
{ |
24 |
String
nimbus = args[ 0 ]; |
25 |
conf.put(Config.NIMBUS_HOST,
nimbus); |
26 |
conf.setNumWorkers( 2 ); |
27 |
StormSubmitter.submitTopologyWithProgressBar(name,
conf, builder.createTopology()); |
29 |
LocalCluster
cluster = new LocalCluster(); |
30 |
cluster.submitTopology(name,
conf, builder.createTopology()); |
上面通過TopologyBuilder來配置組成一個Topology的多個組件(Spout或Bolt),然後通過調用createTopology()方法創建一個Topology實例。上面方法中,對應着2種運行模式:如果沒有傳遞任何參數,則是使用LocalCluster來運行,適合本地調試代碼;如果傳遞一個Topology名稱作爲參數,則是在真實的Storm集羣上運行,需要對實現的Topology代碼進行編譯打包,通過StormSubmitter提交到集羣上作爲服務運行。
Storm組合多種流操作
Storm支持流聚合操作,將多個組件emit的數據,匯聚到同一個處理組件來統一處理,可以實現對多個Spout組件通過流聚合到一個Bolt組件(Sout到Bolt的多對一、多對多操作),也可以實現對多個Bolt通過流聚合到另一個Bolt組件(Bolt到Bolt的多對一、多對多操作)。實際,這裏面有兩種主要的操作,一種是類似工作流中的fork,另一種是類似工作流中的join。下面,我們實現一個例子來演示如何使用,實時流處理邏輯如下圖所示:
上圖所描述的實時流處理流程,我們期望能夠按照如下流程進行處理:
-
存在3類數據:數字字符串(NUM)、字母字符串(STR)、特殊符號字符串(SIG)
-
每個ProduceRecordSpout負責處理上面提到的3類數據
-
所有數據都是字符串,字符串中含有空格,3種類型的ProduceRecordSpout所emit的數據都需要被相同的邏輯處理:根據空格來拆分字符串
-
一個用來分發單詞的組件DistributeWordByTypeBolt能夠接收到所有的單詞(包含類型信息),統一將每類單詞分別分發到指定的一個用來存儲數據的組件
-
SaveDataBolt用來存儲處理過的單詞,對於不同類型單詞具有不同的存儲邏輯,需要設置3類SaveDataBolt
將Spout分爲3類,每一個Spout發射不同類型的字符串,這裏定義了一個Type常量類來區分這三種類型:
2 |
String
NUMBER = "NUMBER" ; |
3 |
String
STRING = "STRING" ; |
首先看一下,我們實現的Topology是如何進行創建的,代碼如下所示:
01 |
public static void main(String[]
args) throws AlreadyAliveException,
InvalidTopologyException, InterruptedException { |
04 |
TopologyBuilder
builder = new TopologyBuilder(); |
07 |
builder.setSpout( "spout-number" , new ProduceRecordSpout(Type.NUMBER, new String[]
{ "111
222 333" , "80966
31" }), 1 ); |
08 |
builder.setSpout( "spout-string" , new ProduceRecordSpout(Type.STRING, new String[]
{ "abc
ddd fasko" , "hello
the word" }), 1 ); |
09 |
builder.setSpout( "spout-sign" , new ProduceRecordSpout(Type.SIGN, new String[]
{ "++
-*% *** @@" , "{+-}
^#######" }), 1 ); |
12 |
builder.setBolt( "bolt-splitter" , new SplitRecordBolt(), 2 ) |
13 |
.shuffleGrouping( "spout-number" ) |
14 |
.shuffleGrouping( "spout-string" ) |
15 |
.shuffleGrouping( "spout-sign" ); |
18 |
builder.setBolt( "bolt-distributor" , new DistributeWordByTypeBolt(), 6 ) |
19 |
.fieldsGrouping( "bolt-splitter" , new Fields( "type" )); |
22 |
builder.setBolt( "bolt-number-saver" , new SaveDataBolt(Type.NUMBER), 3 ) |
23 |
.shuffleGrouping( "bolt-distributor" , "stream-number-saver" ); |
24 |
builder.setBolt( "bolt-string-saver" , new SaveDataBolt(Type.STRING), 3 ) |
25 |
.shuffleGrouping( "bolt-distributor" , "stream-string-saver" ); |
26 |
builder.setBolt( "bolt-sign-saver" , new SaveDataBolt(Type.SIGN), 3 ) |
27 |
.shuffleGrouping( "bolt-distributor" , "stream-sign-saver" ); |
30 |
Config
conf = new Config(); |
31 |
String
name = MultiStreamsWordDistributionTopology. class .getSimpleName(); |
32 |
if (args
!= null &&
args.length > 0 )
{ |
33 |
String
nimbus = args[ 0 ]; |
34 |
conf.put(Config.NIMBUS_HOST,
nimbus); |
35 |
conf.setNumWorkers( 3 ); |
36 |
StormSubmitter.submitTopologyWithProgressBar(name,
conf, builder.createTopology()); |
38 |
LocalCluster
cluster = new LocalCluster(); |
39 |
cluster.submitTopology(name,
conf, builder.createTopology()); |
40 |
Thread.sleep( 60 * 60 * 1000 ); |
一個SplitRecordBolt組件從3個不同類型的ProduceRecordSpout接收數據,這是一個多Spout流聚合。SplitRecordBolt將處理後的數據發送給DistributeWordByTypeBolt組件,然後根據收到的數據的類型進行一個分發處理,這裏用了fieldsGrouping操作,也就是SplitRecordBolt發送的數據會按照類型發送到不同的DistributeWordByTypeBolt任務(Task),每個Task收到的一定是同一個類型的數據,如果直接使用shuffleGrouping操作也沒有問題,只不過每個Task可能收到任何類型的數據,在DistributeWordByTypeBolt內部進行流向控制。DistributeWordByTypeBolt組件中定義了多個stream,根據類型來分組發送給不同類型的SaveDataBolt組件。
下面看每個組件的實現:
通過我們定義的一個ProduceRecordSpout類,可以創建3個不同的ProduceRecordSpout實例,每個實例負責生產特定類型的數據,實現代碼如下所示:
01 |
public static class ProduceRecordSpout extends BaseRichSpout
{ |
03 |
private static final long serialVersionUID
= 1L; |
04 |
private static final Log
LOG = LogFactory.getLog(ProduceRecordSpout. class ); |
05 |
private SpoutOutputCollector
collector; |
07 |
private String[]
recordLines; |
10 |
public ProduceRecordSpout(String
type, String[] lines) { |
16 |
public void open(Map
conf, TopologyContext context, SpoutOutputCollector collector) { |
17 |
this .collector
= collector; |
23 |
public void nextTuple()
{ |
25 |
String
record = recordLines[rand.nextInt(recordLines.length)]; |
26 |
List<Object>
values = new Values(type,
record); |
27 |
collector.emit(values,
values); |
28 |
LOG.info( "Record
emitted: type=" +
type + ",
record=" +
record); |
32 |
public void declareOutputFields(OutputFieldsDeclarer
declarer) { |
33 |
declarer.declare( new Fields( "type" , "record" )); |
這比較簡單,根據傳遞的參數來創建上圖中的3個Spout實例。
由於前面3個ProduceRecordSpout產生的數據,在開始時的處理邏輯是相同的,所以可以將3個ProduceRecordSpout聚合到一個包含通用邏輯的SplitRecordBolt組件,實現如下所示:
01 |
public static class SplitRecordBolt extends BaseRichBolt
{ |
03 |
private static final long serialVersionUID
= 1L; |
04 |
private static final Log
LOG = LogFactory.getLog(SplitRecordBolt. class ); |
05 |
private OutputCollector
collector; |
08 |
public void prepare(Map
stormConf, TopologyContext context, |
09 |
OutputCollector
collector) { |
10 |
this .collector
= collector; |
14 |
public void execute(Tuple
input) { |
15 |
String
type = input.getString( 0 ); |
16 |
String
line = input.getString( 1 ); |
17 |
if (line
!= null &&
!line.trim().isEmpty()) { |
18 |
for (String
word : line.split( "\\s+" ))
{ |
19 |
collector.emit(input, new Values(type,
word)); |
20 |
LOG.info( "Word
emitted: type=" +
type + ",
word=" +
word); |
28 |
public void declareOutputFields(OutputFieldsDeclarer
declarer) { |
29 |
declarer.declare( new Fields( "type" , "word" )); |
無論接收到的Tuple是什麼類型(STRING、NUMBER、SIGN)的數據,都進行split,然後在emit的時候,仍然將類型信息傳遞給下一個Bolt組件。
-
DistributeWordByTypeBolt組件
DistributeWordByTypeBolt組件只是用來分發Tuple,通過定義Stream,將接收到的Tuple發送到指定的下游Bolt組件進行處理。通過SplitRecordBolt組件emit的Tuple包含了類型信息,所以在DistributeWordByTypeBolt中根據類型來進行分發,代碼實現如下:
01 |
public static class DistributeWordByTypeBolt extends BaseRichBolt
{ |
03 |
private static final long serialVersionUID
= 1L; |
04 |
private static final Log
LOG = LogFactory.getLog(DistributeWordByTypeBolt. class ); |
05 |
private OutputCollector
collector; |
08 |
public void prepare(Map
stormConf, TopologyContext context, |
09 |
OutputCollector
collector) { |
10 |
this .collector
= collector; |
11 |
Map<GlobalStreamId,
Grouping> sources = context.getThisSources(); |
12 |
LOG.info( "sources==>
" +
sources); |
16 |
public void execute(Tuple
input) { |
17 |
String
type = input.getString( 0 ); |
18 |
String
word = input.getString( 1 ); |
21 |
emit( "stream-number-saver" ,
type, input, word); |
24 |
emit( "stream-string-saver" ,
type, input, word); |
27 |
emit( "stream-sign-saver" ,
type, input, word); |
32 |
emit( "stream-discarder" ,
type, input, word); |
38 |
private void emit(String
streamId, String type, Tuple input, String word) { |
39 |
collector.emit(streamId,
input, new Values(type,
word)); |
40 |
LOG.info( "Distribution,
typed word emitted: type=" +
type + ",
word=" +
word); |
44 |
public void declareOutputFields(OutputFieldsDeclarer
declarer) { |
45 |
declarer.declareStream( "stream-number-saver" , new Fields( "type" , "word" )); |
46 |
declarer.declareStream( "stream-string-saver" , new Fields( "type" , "word" )); |
47 |
declarer.declareStream( "stream-sign-saver" , new Fields( "type" , "word" )); |
48 |
declarer.declareStream( "stream-discarder" , new Fields( "type" , "word" )); |
實際上,下游的3個Bolt組件(SaveDataBolt)在訂閱該流組件(DistributeWordByTypeBolt)的時候,方式相同,只是分發的邏輯交由DistributeWordByTypeBolt來統一控制。
我們在配置該Bolt組件時,使用了fieldsGrouping分組方式,實際每個DistributeWordByTypeBolt只會收到同一種類型的Tuple,這裏也可以使用shuffleGrouping分組方式,這種分組方式會有不同類型的Tuple被emit到同一個DistributeWordByTypeBolt組件上。
另外,該Bolt組件中我們還定義了一個名稱爲stream-discarder的stream,在Topology中並沒有滿足該stream的條件,可以根據實際情況選擇是否實現它。
最後這個Bolt用來模擬保存處理過的數據內容,代碼如下:
01 |
public static class SaveDataBolt extends BaseRichBolt
{ |
03 |
private static final long serialVersionUID
= 1L; |
04 |
private static final Log
LOG = LogFactory.getLog(SaveDataBolt. class ); |
05 |
private OutputCollector
collector; |
09 |
public SaveDataBolt(String
type) { |
14 |
public void prepare(Map
stormConf, TopologyContext context, |
15 |
OutputCollector
collector) { |
16 |
this .collector
= collector; |
20 |
public void execute(Tuple
input) { |
22 |
LOG.info( "[" +
type + "]
" + |
23 |
"SourceComponent=" +
input.getSourceComponent() + |
24 |
",
SourceStreamId=" +
input.getSourceStreamId() + |
25 |
",
type=" +
input.getString( 0 )
+ |
26 |
",
value=" +
input.getString( 1 )); |
30 |
public void declareOutputFields(OutputFieldsDeclarer
declarer) { |
在實際應用中,你可能需要將處理過的數據保存到數據庫中,就可以在該Bolt中實現存儲數據的邏輯。
總結
Storm中最核心的計算組件的抽象就是Spout、Bolt,以及Stream Grouping,其它高級的功能,像Trident、DRPC,他們或者基於這些基礎組件以及Streaming Grouping分發策略來實現的,屏蔽了底層的分發計算處理邏輯以更高層的編程抽象面向開發者,減輕了開發人員對底層複雜機制的處理;或者是爲了方便使用Storm計算服務而增加的計算機制衍生物,如批量事務處理、RPC等。
參考鏈接