Storm入門系列之一:storm核心概念及特性
本文的將介紹一些 storm 入門的基礎(chǔ)知識(shí),包括 storm 的核心概念,storm 的并發(fā)機(jī)制和消息可靠處理機(jī)制基于 storm 1.0.x版本。
什么是Storm?
Strom 是一款開(kāi)源的分布式實(shí)時(shí)計(jì)算框架,是一種基于數(shù)據(jù)流的實(shí)時(shí)處理系統(tǒng),數(shù)據(jù)吞吐量大,實(shí)時(shí)性高。
為什么使用Storm?
來(lái)自官方的回答:
It is scalable, fault-tolerant, guarantees your data will be processed, and is easy to set up and operate.
確實(shí)如官方所言,本人在使用 storm 的過(guò)程中深有感觸,其可以幫助開(kāi)發(fā)人員很容易的針對(duì)海量數(shù)據(jù)實(shí)現(xiàn)實(shí)時(shí)、可靠的數(shù)據(jù)處理。
Storm的核心概念
Storm 計(jì)算結(jié)構(gòu)中的幾個(gè)核心概念為 topology,stream,spout,bolt,下面我們將依次介紹。
Topology
Topology 是 storm 中最核心的概念,其是運(yùn)行在 storm 集群上的一個(gè)實(shí)時(shí)計(jì)算應(yīng)用,相當(dāng)于 hadoop 中的一個(gè) job,區(qū)別于 job 的時(shí),job 會(huì)有明確的開(kāi)始和結(jié)束,而 topology 由于實(shí)時(shí)的流式計(jì)算的特殊性,從啟動(dòng)的那一刻起會(huì)永遠(yuǎn)的運(yùn)行下去,直到手動(dòng)停止。
Topology 由 stream,spouts,bolts 組成,可以描述為一個(gè)有向無(wú)環(huán)圖,如下:
Stream
Stream 是 storm 中對(duì)數(shù)據(jù)流的抽象,是由無(wú)限制的 tuple 組成的序列。Tuple 可以理解為包含一個(gè)或多個(gè)鍵值對(duì)的 hash。Tuples 在 stream 中流經(jīng) bolts,被逐步處理,最終得到預(yù)設(shè)的結(jié)果。
Stream 可比作一條源源不絕的河流,tuple 就是組成這條河流的無(wú)數(shù)水滴。每一個(gè) stream 在 storm 中都有一個(gè)唯一標(biāo)示的 id。
Spout
從圖一可以看出,spout 是一個(gè) topology 的數(shù)據(jù)源,負(fù)責(zé)連接數(shù)據(jù)源,并將數(shù)據(jù)轉(zhuǎn)化為 tuple emit 到 topology中,經(jīng)由 bolts 處理。
Spout 提供了一對(duì)核心方法<ack, fail>來(lái)保障 storm 在數(shù)據(jù)沒(méi)有被正確處理的情況下,不會(huì)被丟棄,仍能被重新處理,當(dāng)然這是可選的,我們也可以不關(guān)心 tuple 是否被正確的處理,只負(fù)責(zé)向topology 中 emit 數(shù)據(jù)(在某些場(chǎng)景下可能不需要)。具體實(shí)現(xiàn)原理在后文會(huì)詳細(xì)介紹。
Storm + Kakfa 是很常見(jiàn)的組合,storm提供了storm-kafka擴(kuò)展,封裝了多個(gè)可用的 kafka spouts 供直接使用,相關(guān)文檔可以參考這里。
Bolt
Bolt 是 topology 中的數(shù)據(jù)處理單元,每個(gè) bolt 都會(huì)對(duì) stream 中的 tuple 進(jìn)行數(shù)據(jù)處理。復(fù)雜的數(shù)據(jù)處理邏輯一般拆分成多個(gè)簡(jiǎn)單的處理邏輯交由每個(gè) Bolt 負(fù)責(zé)。
Bolt 可以執(zhí)行豐富的數(shù)據(jù)處理邏輯,如過(guò)濾,聚合,鏈接,數(shù)據(jù)庫(kù)操作等等。
Bolt 可以接受任意個(gè)數(shù)據(jù)流中的 tuples,并在對(duì)數(shù)據(jù)進(jìn)行處理后選擇性的輸出到多個(gè)流中。也就是說(shuō),bolt 可以訂閱任意數(shù)量的spouts 或其他 bolts emit 的數(shù)據(jù)流,這樣最終形成了復(fù)雜的數(shù)據(jù)流處理網(wǎng)絡(luò),如圖一。
理解了 storm 的核心概念后,下文將介紹storm的并發(fā)機(jī)制。
Storm 的并發(fā)
上文提到 storm 是 scalable 的,是因?yàn)?storm 能將計(jì)算切分成多個(gè)獨(dú)立的 tasks 在集群上并發(fā)執(zhí)行,從而支持其在多臺(tái)設(shè)備水平擴(kuò)容。那 storm 的并發(fā)是如何實(shí)現(xiàn)的呢?回答這個(gè)問(wèn)題之前先來(lái)看一下 topology 是如何運(yùn)行在 storm 集群中的:
上圖中包含三個(gè)核心概念:
worker: 一個(gè) worker 對(duì)應(yīng)一個(gè)進(jìn)程,是一個(gè) topology 的子集,在 storm 集群中的一個(gè)node上可根據(jù)配置啟動(dòng)N個(gè) worker。
Executor:一個(gè) executor 是運(yùn)行在一個(gè) worker 進(jìn)程上的線程,executor 可以執(zhí)行同一個(gè) spout 或 bolt 的一個(gè)或多個(gè) task ,默認(rèn)的一個(gè) executor 會(huì)分配一個(gè) task。
Task:task負(fù)責(zé)真正的數(shù)據(jù)處理邏輯,一個(gè) task 實(shí)質(zhì)上是一個(gè)spout 或者 bolt 的實(shí)例。
所以,一個(gè)物理設(shè)備上可以運(yùn)行多個(gè) worker ,一個(gè) worker 內(nèi)部又可以啟動(dòng)多個(gè) executor ,每個(gè) executor 可以執(zhí)行一個(gè)或多個(gè)task。
Strom的并發(fā)度是用來(lái)描述所謂的 "parallelism hint",它是指一個(gè) component(spout or bolt)的初始啟動(dòng)時(shí)的 executor 數(shù)量。通過(guò)下圖我們來(lái)看一個(gè) topology 的并發(fā)示例:
上圖的 topology 有一個(gè) spout 和兩個(gè) bolt 組成。其中 blue spout 包含兩個(gè) executor,每個(gè) executor 各執(zhí)行一個(gè) blue spout 的 task;green bolt 包含了兩個(gè) executor,每個(gè) executor 各執(zhí)行兩個(gè)task;yellow bolt 包含6個(gè) executor,每個(gè) executor 各執(zhí)行一個(gè)task。
整個(gè) topology 啟動(dòng)了兩個(gè) worker,共包含 12 個(gè)task,每個(gè)worker 包含5個(gè) executor,也就是5個(gè) Thread。所以其 parallelism hint 是10。
從上例可以看出,增加分配給 topology 的 worker 數(shù)和 executor
數(shù)是直接增加其計(jì)算能的簡(jiǎn)單辦法。Storm 提供了相關(guān)的 API 或通過(guò)配置文件來(lái)修改一個(gè) topology 的 woker 數(shù),同樣的
storm 提供了相關(guān) API 控制 executor 的數(shù)量和每個(gè) executor執(zhí)行的 task 數(shù)量用以控制并發(fā)。
Stream grouping 數(shù)據(jù)分組
除了spout 和 bolt外,定義一個(gè) topology 還有一個(gè)重要的組成,那就是 stream grouping,它規(guī)定了 topology 中的每一個(gè) bolt 實(shí)例(也即是task)要接收什么樣的 stream 作為輸入。
具體來(lái)說(shuō),stream group 定義了一個(gè) stream 中的 tuple 最終被emit 到哪個(gè) bolt task 上被處理,是一個(gè)數(shù)據(jù)分組機(jī)制。storm 提供了八種內(nèi)置的 stream grouping 類型(storm 1.o.x版本的內(nèi)置類型,):
- Shuffle grouping : 隨機(jī)分組,隨機(jī)的分發(fā) tuple 到每個(gè) bolt 的各個(gè) task,每個(gè) task 接收的 tuples 數(shù)量相同。
- Fields grouping : 按字段分組,會(huì)根據(jù) tuple 的 某一個(gè)字段(可以理解為 tuple 這個(gè) hash 的 key)分組,同一個(gè)字段的 tuple 永遠(yuǎn)被分配給同一個(gè) task 處理。
- Partial Key grouping : 類似2,但實(shí)現(xiàn)了 stream 下游的兩個(gè)
bolts 間的負(fù)載均衡,在 tuple 的字段分布不均勻時(shí)提供了更好的資源利用效果。 - All grouping : 全復(fù)制分組,所有的 tuple 復(fù)制后,都會(huì)分發(fā)給所有的 bolt 的 task 進(jìn)行處理。
- Global grouping : 全局分組,所有的 tuples 都 emit 到唯一的一個(gè) task 上,如果為一個(gè) bolt 設(shè)置了多個(gè) task,會(huì)選擇 task id 最小的 task 來(lái)接收數(shù)據(jù),此時(shí)設(shè)置的并發(fā)是沒(méi)有意義的。
- None grouping : 不分組,功能上同1,是預(yù)留接口。
- Direct grouping : 指定分組,數(shù)據(jù)源會(huì)調(diào)用 emitDerect 方法來(lái)判斷一個(gè) tuple 將發(fā)送到哪個(gè) cosumer task 來(lái)接收這個(gè) tuple。這種分組只能在?被聲明為指向性的數(shù)據(jù)流上使用。
- Local or shuffle grouping : 本地隨機(jī)分組,和1類似,但是在隨機(jī)分組的過(guò)程中會(huì),如果在同一個(gè) woker 內(nèi)包含 consumer task,則在 woker 內(nèi)部的 consumer tasks 中進(jìn)行隨機(jī)分組,否則同1。
另外,可以通過(guò)擴(kuò)展CustomStreamGrouping實(shí)現(xiàn)自定義的分組方式。
Strom的消息可靠處理機(jī)制
Storm可靠性分類
在這之前,我們需要介紹一個(gè)概念 "fully processed"。一條message 自從它由 spout emit 到 topology,被這個(gè) tuple 途徑的整個(gè)?DAG 中的所有 bolt 都處理過(guò),storm 認(rèn)為這個(gè) message 是被 "fully processed"。Storm 的消息保障處理機(jī)制是針對(duì) "fully processed" 而言的。
在系統(tǒng)級(jí),storm 提供了 "best effort","at least once","exactly once" 三種類型。其中 "best effort" 是不保證每條消息都被處理,"at least once" 是保障消息最少能被處理一次,可能會(huì)被多次處理,"exactly once" 是保證消息被處理且只被處理一次。
"best effort" 這種類型沒(méi)什么可說(shuō)的,就是每條消息 storm 都會(huì)按程序邏輯走下去,但是不會(huì)關(guān)注其是否成功。"at least once",是storm-core 提供的可靠性級(jí)別,即保證每條 message 至少會(huì)被處理一次,可能會(huì)出現(xiàn)多次處理的情況,下文將詳細(xì)介紹其實(shí)現(xiàn)原理。
至于 "exactly once" 其實(shí)是由 storm 的高級(jí)抽象 Trident 實(shí)現(xiàn)的,我們會(huì)在后文對(duì)其介紹。
Storm實(shí)現(xiàn)可靠性的API
現(xiàn)在,我們介紹一下 storm 保證可靠性的實(shí)現(xiàn)接口。在 storm 中要保障消息被處理你需要做以下兩件事才能保證 spout 發(fā)出 tuple 被處理:
- 無(wú)論在什么節(jié)點(diǎn),每當(dāng)你新創(chuàng)建一個(gè) tuple 是都要告知 storm
- 無(wú)論在什么節(jié)點(diǎn),每當(dāng)你處理完成一個(gè) tuple 都需要告知 storm
對(duì)于spout,storm的提供了非常簡(jiǎn)單的API保證可靠性:
- nextTuple:這個(gè)接口負(fù)責(zé)emit tuple,為了保證可靠性需要為每個(gè) tuple 生成一個(gè)唯一 ID,在通過(guò) collector emit tuple 時(shí),是需要帶上這個(gè) ID。同時(shí)會(huì)將這個(gè) tuple 和 ID 保存在一個(gè) hash 中,以等待 tuple 被完全處理后相應(yīng)的操作.
- ack:這個(gè)接口負(fù)責(zé)處理成功的應(yīng)答,一般當(dāng)收到成功處理這個(gè)tuple 的消息后,刪除 hash 中這個(gè) tuple 的記錄。
- fail: 這個(gè)接口復(fù)雜處理失敗的應(yīng)答,當(dāng)某個(gè) tuple 處理失敗而超時(shí)后會(huì)調(diào)用這個(gè)接口,一般選擇重新 emit 這條消息。
而對(duì)于 bolt 要做的則是,當(dāng)接收到一個(gè) tuple 后,如果有新生成tuple 則需要將新生成的 tuple 與輸入 tuple 錨定,當(dāng)處理成功或失敗后分別確認(rèn)應(yīng)答或報(bào)錯(cuò)。錨定通過(guò) collector.emit 方法實(shí)現(xiàn):
this.collector.emit(input_tuple, output_tuple)
確認(rèn)和失敗則分別調(diào)用 collector 的 ack 和 fail 方法。其中調(diào)用 fail方法能讓這個(gè) tuple 對(duì)應(yīng)的 spout tuple 快讀失敗,不必讓 spout task 等待超時(shí)后才處理它。
this.collector.ack(input_tuple)this.collector.fail(input_tuple)
Storm高效實(shí)現(xiàn)可靠性的原理
在 storm 中有這樣一個(gè)special "acker" tasks,它負(fù)責(zé)跟蹤所有由spout 發(fā)出的 tuple?產(chǎn)生的 DAG。當(dāng)一個(gè) tuple 成功的在 DAG
中完成整個(gè)生命周期,這個(gè) task 會(huì)通知 emit 這個(gè) tuple 的 spout task 這個(gè) tuple 被處理了。所以如果期望消息至少被處理一次,最少要啟動(dòng)一個(gè) acker task,當(dāng)然你可以啟動(dòng)任意個(gè)。
Storm 會(huì)通過(guò) "mod hashing" 的方法將一個(gè) tuple 分配到合適的acker 去跟蹤,因?yàn)槊恳粋€(gè) tuple 都對(duì)應(yīng)一個(gè)64位的唯一ID,并且在錨定 tuple 時(shí)這個(gè)ID也會(huì)隨之傳給新生成的 tuple,所以 DAG 中的每個(gè)節(jié)點(diǎn)根據(jù)這個(gè) ID 可以判斷應(yīng)答消息發(fā)送給哪個(gè) acker。同樣 acker 也能從在應(yīng)答消息中確認(rèn)哪個(gè) tuple 的狀態(tài)被更新了,當(dāng)一個(gè) tuple 的整個(gè) DAG 完成,acker 會(huì)發(fā)送確認(rèn)消息給源 spout。
Acker 不會(huì)明確的追蹤整個(gè) DAG,否則當(dāng) DAG 越發(fā)復(fù)雜時(shí)其負(fù)擔(dān)越重。Acker 的追蹤算法非常之簡(jiǎn)潔高效,并且只對(duì)于每個(gè)追蹤的tuple 只會(huì)占用大約20B的固定空間。
Storm 會(huì)在系統(tǒng)中維護(hù)一個(gè)表,這個(gè)表的 key 是 acker 追蹤的每個(gè) tuple 的 ID,value 的初始值也是這個(gè) ID。當(dāng) DAG 中的下游節(jié)點(diǎn)處理了這個(gè) tuple 后,acker 接到確認(rèn)信息后會(huì)做一個(gè) XOR 運(yùn)算,用 XOR 的運(yùn)算結(jié)果來(lái)更新這個(gè) ID 在表中對(duì)應(yīng)的 val。
在這里需要說(shuō)明一下在 DAG 中每個(gè)新生成 tuple 都會(huì)有一個(gè)64位的隨機(jī)值ID(注意:不是其錨定的tuple傳來(lái)的spout emit的那個(gè)tuple 的ID。也就是說(shuō)每個(gè)新生成的 tuple 會(huì)有一個(gè)唯一 ID,新生成的 tuple 錨定某一個(gè) tuple 后也會(huì)知曉 spout tuple 的那個(gè) ID),在每個(gè)計(jì)算節(jié)點(diǎn),storm 會(huì)將這個(gè)計(jì)算節(jié)點(diǎn)生成的所有 tuple 的 ID 與所有輸入 tuple 的 ID 以及這個(gè) DAG 所追蹤的 tuple 在系統(tǒng)表中對(duì)應(yīng)的 value 做 XOR 操作,得到一個(gè)結(jié)果,并用這個(gè)結(jié)果更新系統(tǒng)表中對(duì)應(yīng)的 value。
因?yàn)閄OR操作的特殊性:
N XOR N = 0
N XOR 0 = N
所以當(dāng)一個(gè) tuple 在在整個(gè) DAG 中運(yùn)行完成后這個(gè) tuple 在系統(tǒng)表中對(duì)應(yīng) value 一定為 0,通過(guò)這點(diǎn)可以判定一個(gè) tuple 是否被成功處理。我們通過(guò)實(shí)例來(lái)計(jì)算一下:
Storm在各種失敗場(chǎng)景下的保障方法:
- 情景1:DAG 中某個(gè)節(jié)點(diǎn)掛掉沒(méi)有正常發(fā)送 fail msg。這時(shí)其對(duì)應(yīng)的根節(jié)點(diǎn)的 tuple 最后會(huì)因超時(shí)而被 spout 重發(fā)。
- 情景2:跟蹤 tuple 的 acker task 掛了。此時(shí),這個(gè)acker跟蹤的所有task都會(huì)因?yàn)槌瑫r(shí)而重發(fā)(因?yàn)?acker 不會(huì)更新其在系統(tǒng)中對(duì)應(yīng)的value)。
- 情景3:spout 掛了。因?yàn)閟pout的輸入往往來(lái)自隊(duì)列,當(dāng) spout 掛掉后,這個(gè) spout 沒(méi)有對(duì)隊(duì)列中的消息做確認(rèn)回應(yīng),所以隊(duì)列不會(huì)認(rèn)為這個(gè) spout 提走的數(shù)據(jù)被正常消費(fèi)了,而作"出隊(duì)"處理(其實(shí)是將執(zhí)行中并沒(méi)有確認(rèn)的數(shù)據(jù)重新歸隊(duì))。
小結(jié)
本文簡(jiǎn)要的介紹了 storm 的核心概念 topology,并介紹 topology
的組成,topology 中的數(shù)據(jù)流分組方式,topology 在 storm 集群中如何并發(fā)運(yùn)行,以及 storm 是如何保障消息可靠執(zhí)行的。在下一章我們將會(huì)介紹一個(gè)生產(chǎn)環(huán)境中的簡(jiǎn)單實(shí)例。
參考資料
(已更新為storm-1.0.0版本的文檔)
http://storm.apache.org/releases/1.0.0/Concepts.htmlhttp://storm.apache.org/releases/1.0.0/Understanding-the-parallelism-of-a-Storm-topology.htmlhttp://storm.apache.org/releases/1.0.0/Guaranteeing-message-processing.html