Storm 是一個分布式的,可靠的,容錯的數據流處理系統。下面我將分別從storm的整體架構以及部分原理進行講解。
一、基本的概念
storm中服務器節點分為主節點和從節點,Nimbus為主節點和Supervisor為從節點。以及若干組件構成。下面為對一些術語進行簡單的介紹:
Nimbus:主節點,是一個調度中心,負責分發任務
Supervisor:從節點,任務執行的地方
Worker:任務工作進程,一個Supervisor中可以有多個Worker。
Executor:Worker進程在執行任務時,會啟動多個Executor線程
Topology:任務的抽象概念。由于storm是流式計算的框架,它的數據流和拓撲圖很像,所以它的任務就叫topology。
Spout:從數據源獲取數據并進行分發。
Bolt:得到Spout或者上一個Bolt的數據,然后進行處理后交給下一個Bolt處理。
Tuple:在storm中,一條數據可以理解為是一個Tuple。
二、storm的架構
任務提交處理流程
Nimbus是調度中心,Supervisor是任務執行的地方。Supervisor上面有若干個Worker,每個Worker都有自己的端口,Worker可以理解為一個進程。另外,每個Worker中還可以運行若干個線程。
當客戶端向storm集群提交一個Topology時,這里的提交就是在集群上通過命令storm jar xxx
啟動topology。如果我們是在Supervisor節點上執行storm jar xxx
,那么Supervisor會將jar包拷貝到Nimbus,之后Nimbus對Topology進行調度。
Nimbus會根據Topology所需要的Worker進行分配,將其分配到各個Supervisor的節點上執行。
現在假設我們我們有4個Supervisor節點,每個Supervisor都配置4個Worker。這是我們提交了一個Topology,需要4個Worker,那可能的分配情況可能如下圖所示:
storm中的數據流
啟動完Topology后,相關組件就開始運行起來了。在Storm中,Spout組件主要用來從數據源拉取數據,形成一個Tuple后轉交給Bolt處理。Bolt接受到Tuple處理完后,可以選擇繼續交給下一個Bolt處理,也可以選擇不往下傳。這樣數據以Tuple的形式一個接一個的往下執行,就形成了一個拓撲數據流。
storm數據在組件間的流向如下圖所示:
三、Storm的并發度
在Storm中,Worker不是組件執行的最小單位。Executor才是,Executor可以理解為是一個線程。我們在創建topology的時候,可以設置執行spout的線程數和bolt的線程數。
假設spout和bolt的線程數加起來設置了8個,然后設置了2個worker,那么這8個線程可能就會隨機分配到2個worker中,可能一個worker3個,一個worker5個。也有可能各自分配4個。如下圖所示:
四、數據的Grouping策略
在實際應用中,Bolt組件的實例可能有多個,Tuple在流向Bolt時,選擇哪個Bolt實例的策略就是grouping策略。
下面是Storm中的6種Grouping策略:
- Shuffle Grouping: 隨機分組, 隨機派發stream里面的tuple, 保證每個bolt接收到的tuple數目相同。輪詢,平均分配。
- Fields Grouping:按字段分組, 比如按userid來分組, 具有同樣userid的tuple會被分到相同的Bolts, 而不同的userid則會被分配到不同的Bolts。
- All Grouping: 廣播發送, 對于每一個tuple, 所有的Bolts都會收到。
- Global Grouping: 全局分組, 這個tuple被分配到storm中的一個bolt的其中一個task。再具體一點就是分配給id值最低的那個task。
- Non Grouping: 不分組, 這個分組的意思是說stream不關心到底誰會收到它的tuple。目前這種分組和Shuffle grouping是一樣的效果,不平均分配。
- Direct Grouping: 直接分組, 這是一種比較特別的分組方法,用這種分組意味著消息的發送者舉鼎由消息接收者的哪個task處理這個消息。 只有被聲明為Direct Stream的消息流可以聲明這種分組方法。而且這種消息tuple必須使用emitDirect方法來發射。消息處理者可以通過TopologyContext來或者處理它的消息的taskid(OutputCollector.emit方法也會返回taskid)
五、消息的可靠性保證 —— ack機制
一條數據在Spout中形成一個Tuple,然后交給一個個Bolt執行,那我們怎么保證這個Tuple被完整的執行了呢?這里的完整執行說的是這個Tuple必須在后面的每一個Bolt都成功處理,假設在一個Bolt中發生異常導致失敗,這就不能算完整處理。
為了保證消息處理過程中的可靠性,storm使用了ack機制。storm會專門啟動若干acker線程,來追蹤tuple的處理過程。acker線程數量可以設置。
每一個Tuple在Spout中生成的時候,都會分配到一個64位的messageId。通過對messageId進行哈希我們可以執行要對哪個acker線程發送消息來通知它監聽這個Tuple。
acker線程收到消息后,會將發出消息的Spout和那個messageId綁定起來。然后開始跟蹤該tuple的處理流程。如果這個tuple全部都處理完,那么acker線程就會調用發起這個tuple的那個spout實例的ack()方法。如果超過一定時間這個tuple還沒處理完,那么acker線程就會調用對應spout的fail()方法,通知spout消息處理失敗。spout組件就可以重新發送這個tuple。
從上面的介紹我們知道了,tuple數據的流向會形成一個拓撲圖,也可以理解成是一個tuple樹。這個拓撲圖的節點可能會有很多個,如果要把這些節點全部保存起來,處理大量的數據時勢必會造成內存溢出。
對于這個難題,storm使用了一種非常巧妙的方法,使用20個字節就可以追蹤一個tuple是否被完整的執行。這也是storm的一個突破性的技術。
ack機制的具體原理
我們都知道,自己異或自己,結果肯定為零( a ^ a = 0)。ack中就利用這個特性
- acker對于每個spout-tuple保存一個ack-val的校驗值,它的初始值是0, 然后每發射一個tuple/ack一個tuple,那么tuple的id都要跟這個校驗值異或一下。注意,這里的tuple的id不是spout-tuple的id,和我們上面理解的messageId不是一個概念,要區分一下,是每個新生產的tuple的id,這個tupleId是隨機生成的64位比特值
- 之后把得到的值更新為ack-val的新值。那么假設每個發射出去的tuple都被ack了, 那么最后ack-val一定是0(因為一個數字跟自己異或得到的值是0)。
舉個例子,比如發射了某個tuple,就 ack-val ^ tupleId,然后ack了某個tuple,就再ack-val ^ tupleId,這樣,ack-val 最終又變成了0,說明tuple已經全部處理成功了。
六、Storm的HA保證——高可用性保證
1. 數據方面的高可用
使用ack機制保證數據處理的高可用
2. Worker進程掛了怎么辦?
Supervisor會自動重啟worker線程。
3. Supervisor節點失效了怎么辦?
可以在其他節點重啟該supervisor任務。
4. Nimbus掛了怎么辦?
在storm1.0之前,Nimbus是不支持HA的。Nimbus如果掛了,重啟Nimbus進程就可以了,不會影響到現有topology的運行。
因為Nimbus只是一個調度中心,Nimbus和Supervisor的狀態都保存在本地文件和ZooKeeper,因此他們進程可以隨便殺死,然后重啟,不會影響到Worker進程的運行。
另外,Nimbus的作用在就是在拓撲任務開始階段,負責將任務提交到集群,后期負責拓撲任務的管理,比如任務查看,終止等操作。在通常情況下,nimbus的任務壓力并不會很大,在自然情況下不會出現宕機的情況。
storm1.0后Nimbus的HA策略還沒有具體研究過,有興趣的小伙伴可自行前往官網查看文檔。http://storm.apache.org/releases/1.2.1/nimbus-ha-design.html
七、總結
Storm的架構及原理整體理解起來不算很難,但很多細節還是需要在實踐中才能發現。有興趣的小伙伴可以去讀讀storm的源碼,storm源碼大多數都是用Clojure實現,對Clojure語言不熟悉的朋友可以去看一下JStorm的源碼實現。這是阿里基于Storm用java實現的框架,據說更加穩定高效。
最后,哪里有說的不對的地方。敬請支出,感激不盡。