Storm原理

1.1 Storm簡介

Twitter Storm 是使用 Clojure(發音同 closure)語言實現的。 Clojure 是 Lisp 語言的一種現代方言。類似於 Lisp,Clojure 支持一種功能性編程風格,但 Clojure 還引入了一些特性來簡化多線程編程(一種對創建 Storm 很有用的特性)。Twitter列舉了Storm的三大類應用:

1.信息流處理{Stream processing},Storm可用來實時處理新數據和更新數據庫,兼具容錯性和可擴展性。

2.連續計算{Continuous computation}

3.Storm可進行連續查詢並把結果即時反饋給客戶端,比如把Twitter上的熱門話題發送到瀏覽器中。

3.分佈式遠程程序調用{Distributed RPC},Storm可用來並行處理密集查詢。Storm的拓撲結構是一個等待調用信息的分佈函數,當它收到一條調用信息後,會對查詢進行計算,並返回查詢結果。舉個例子Distributed RPC可以做並行搜索或者處理大集合的數據。

Storm集羣由一個主節點和多個工作節點組成。主節點運行了一個名爲“Nimbus”的守護進程,用於分配代碼、佈置任務及故障檢測。每個工作節點都運行了一個名爲“Supervisor”的守護進程,用於監聽工作,開始並終止工作進程。Nimbus和Supervisor都能快速失敗,而且是無狀態的,這樣一來它們就變得十分健壯,兩者的協調工作是由Apache ZooKeeper來完成的。

Storm的術語包括Stream、Spout、Bolt、Task、Worker、Stream Grouping和Topology。Stream是被處理的數據Sprout是數據源Bolt處理數據Task是運行於Spout或Bolt中的線程。Worker是運行這些線程的進程。StreamGrouping規定了Bolt接收什麼東西作爲輸入數據數據可以隨機分配(術語爲Shuffle),或者根據字段值分配(術語爲Fields),或者 廣播(術語爲All),或者總是發給一個Task(術語爲Global),也可以不關心該數據(術語爲None),或者由自定義邏輯來決定(術語爲 Direct)。Topology是由Stream Grouping連接起來的Spout和Bolt節點網絡。

storm拓撲的並行度可以從以下4個維度進行設置: 1、node(服務器):指一個storm集羣中的supervisor服務器數量。 2、worker(jvm進程):指整個拓撲中worker進程的總數量,這些數量會隨機的平均分配到各個node。 3、executor(線程):指某個spout或者bolt的總線程數量,這些線程會被隨機平均的分配到各個worker。 4、task(spout/bolt實例):task是spout和bolt的實例,它們的nextTuple()和execute()方法會被executors線程調用。除非明確指定,storm會給每個executor分配一個task。如果設置了多個task,即一個線程持有了多個spout/bolt實例. 注意:以上設置的都是總數量,這些數量會被平均分配到各自的宿主上,而不是設置每個宿主進行多少個進程/線程。

一個worker是一個jvm進程,它只屬於一個 topology,一個worker可以起多個executors去跑某個topology。 一個executor會跑多個tasks(默認配置是一個executor一個task),但是一個executor只屬於一個spout或者bolt。

阿里的內推,問到storm內部的消息傳遞的機制。這裏search總結一下:

一個work是一個進程,這個進程會啓動一個線程(NettyServer(0.9版本))會監聽tcp端口,將收到的消息存到到ZeroMQ(0.8版本)消息隊列裏,然後在反序列化根據id將他交給不同的excutor處理。消息除了從其他work來,也可以從本個work來,這些消息被直接放入隊列中。

並行度的動態調整 對storm拓撲的並行度進行調整有2種方法: 1、kill topo—>修改代碼—>編譯—>提交拓撲 2、動態調整 第1種方法太不方便了,有時候topo不能說kill就kill,另外,如果加幾臺機器,難道要把所有topo kill掉還要修改代碼? 因此storm提供了動態調整的方法,動態調整有2種方法: 1、ui方式:進入某個topo的頁面,點擊rebalance即可,此時可以看到topo的狀態是rebalancing。但此方法只是把進程、線程在各個機器上重新分配,即適用於增加機器,或者減少機器的情形,不能調整worker數量、executor數量等 2、cli方式:storm rebalance 舉個例子

storm rebalance toponame -n 7 -e filter-bolt=6 -e hdfs-bolt=8

將topo的worker數量設置爲7,並將filter-bolt與hdfs-bolt的executor數量分別設置爲6、8. 此時,查看topo的狀態是rebalancing,調整完成後,可以看到3臺機器中的worker數量分別爲3、2、2

1.2 Storm特點

Storm的主要特點:

1. 簡單的編程模型。類似於MapReduce降低了並行批處理複雜性,Storm降低了進行實時處理的複雜性。

2. 可以使用各種編程語言。你可以在Storm之上使用各種編程語言。默認支持ClojureJavaRubyPython。要增加對其他語言的支持,只需實現一個簡單的Storm通信協議即可。

3. 容錯性。Storm會管理工作進程和節點的故障。

4. 水平擴展。計算是在多個線程、進程和服務器之間並行進行的。

5. 可靠的消息處理。Storm保證每個消息至少能得到一次完整處理。任務失敗時,它會負責從消息源重試消息。

6. 快速。系統的設計保證了消息能得到快速的處理,使用ØMQ作爲其底層消息隊列。

7. 本地模式。Storm有一個本地模式,可以在處理過程中完全模擬Storm集羣。這讓你可以快速進行開發和單元測試。

StormHadoop的對比:

hadoop是實現了mapreduce的思想,將數據切片計算來處理大量的離線數據。hadoop處理的數據必須是已經存放在hdfs上或者類似hbase的數據庫中,所以hadoop實現的時候是通過移動計算到這些存放數據的機器上來提高效率而storm不同,storm是一個流計算框架,處理的數據是實時消息隊列中的,所以需要我們寫好一個topology邏輯放在那,接收進來的數據來處理,所以是通過移動數據平均分配到機器資源來獲得高效率。

hadoop的優點是處理數據量大(瓶頸是硬盤和namenode,網絡等),分析靈活,可以通過實現dslmdx等拼接hadoop命令或者直接使用hivepig等來靈活分析數據。適應對大量維度進行組合分析缺點就是慢:每次執行前要分發jar包,hadoop每次map數據超出闕值後會將數據寫入本地文件系統,然後在reduce的時候再讀進來

storm的優點是全內存計算,因爲內存尋址速度是硬盤的百萬倍以上,所以storm的速度相比較hadoop非常快(瓶頸是內存,cpu)缺點就是不夠靈活:必須要先寫好topology結構來等數據進來分析


1.3 Storm架構

1.3.1 Storm集羣組成

Storm集羣類似於一個Hadoop集羣。 然而你在Hadoop的運行“MapReduce job”,在storm上你運行 topologies (拓撲)”。 job”和“topologies ”本身有很大的不同 - 一個關鍵的區別是,MapReduce的工作最終完成,而topologies 處理消息永遠保持(或直到你殺了它)Strom集羣有主要有兩類節點:主節點和工作節點。 主節點上運行一個叫做“Nimbus”的守護進程,也就是類似Hadoop的“JobTracker”。 Nimbus 負責在集羣分發的代碼,將任務分配給其他機器,和故障監測。

每個工作節點運行一個叫做”Supervisor”的守護進程  Supervisor監聽分配給它的機器,根據Nimbus 的委派在必要時啓動和關閉工作進程。 每個工作進程執行topology 的一個子集。一個運行中的topology 由很多運行在很多機器上的工作進程組成。


                                               

Nimbus Supervisors 之間所有的協調工作是通過 一個Zookeeper 集羣。 此外,Nimbus的守護進程和 Supervisors 守護進程是無法連接和無狀態的;所有的狀態維持在Zookeeper 或保存在本地磁盤上。這意味着你可以 kill -9 Nimbus Supervisors 進程,所以他們不需要做備份。 這種設計導致storm集羣具有令人難以置信的穩定性。

Zookeeper是一種開源的、高效的分佈式應用協調服務。一個Zookeeper集羣(以下簡稱ZK集羣)通常是由多個Zookeeper節點組成的。ZK集羣有一個Leader節點,其他的都是Follower節點。Leader節點負責寫服務和數據同步;Follower節點負責讀服務。ZK集羣支持高可用。一般ZK集羣中只要有超過半數的節點存活,ZK集羣就仍然是高可用的(因此通常建議ZK集羣的節點數設置爲奇數個)。

ZK集羣負責Nimbus與Supervisor節點之間的通信。監控各個節點之間的狀態。我們提交任務的時候是在Nimbus上執行的。Nimbus通過ZK集羣將任務分發給Supervisor節點並監控Supervisor節點的執行狀態,如果有某個節點出現故障,Nimbus節點就會通過ZK集羣將那個節點上的任務重新分發給其他Supervisor節點執行。這也意味着一個Storm集羣也是高可用的。不過整個Storm集羣只有一個Nimbus節點。當Nimbus節點出現故障的時候,任務不會停止執行,可我們也無法終止任務。即一旦Nimbus節點出現問題,任務的執行不會受到影響,但是任務的管理會出現故障。此時重新恢復Nimbus服務即可。不過我們可能會丟失從故障到恢復這段時間的數據。Nimbus節點不支持高可用,這也是Storm面臨的問題之一。一般情況下,Nimbus承擔的壓力比較小,不會輕易出現故障。


1.3.1 Storm拓撲結構

Storm 實現了一種數據流模型,其中數據持續地流經一個轉換實體網絡(參見  1)。一個數據流的抽象稱爲一個流,這是一個無限的元組序列。元組就像一種使用一些附加的序列化代碼來表示標準數據類型(比如整數、浮點和字節數組)或用戶定義類型的結構。每個流由一個唯一 ID 定義,這個 ID 可用於構建數據源和接收器 (sink) 的拓撲結構。流起源於噴嘴(spout)Spout將數據從外部來源流入 Storm 拓撲結構中。

 1. 一個普通的 Storm 拓撲結構的概念性架構

接收器(或提供轉換的實體)稱爲螺栓(bolt)。螺栓實現了一個流上的單一轉換和一個Storm 拓撲結構中的所有處理。Bolt既可實現 MapReduce 之類的傳統功能,也可實現更復雜的操作(單步功能),比如過濾、聚合或與數據庫等外部實體通信。典型的 Storm 拓撲結構會實現多個轉換,因此需要多個具有獨立元組流的BoltBoltSpout都實現爲 Linux系統中的一個或多個任務。 

但是,Storm 架構中一個最有趣的特性是有保障的消息處理Storm 可保證一個Spout發射出的每個元組都會處理;如果它在超時時間內沒有處理,Storm 會從該Spout重放該元組。此功能需要一些聰明的技巧來在拓撲結構中跟蹤元素,也是 Storm 的重要的附加價值之一。Bolts的主要方法是execute(死循環)連續處理傳入的tuple,成功處理完每一個tuple調用OutputCollector的ack方法,以通知storm這個tuple被處理完成了。當處理失敗時,可以調fail方法通知Spout端可以重新發送該tuple。

除了支持可靠的消息傳送外,Storm 還使用 ZeroMQ 最大化消息傳送性能(刪除中間排隊,實現消息在任務間的直接傳送)。ZeroMQ 合併了擁塞檢測並調整了它的通信,以優化可用的帶寬。


1.4 Storm原理

1.4.1 Storm組成

計算拓撲: Topologies

一個實時計算應用程序的邏輯在storm裏面被封裝到topology對象裏面, 我把它叫做計算拓補. Storm裏面的topology相當於Hadoop裏面的一個MapReduce Job, 它們的關鍵區別是:一個MapReduce Job最終總是會結束的, 然而一個stormtopoloy會一直運行 — 除非你顯式的殺死它。 一個TopologySpoutsBolts組成的圖狀結構, 鏈接SpoutsBolts的則是Stream groupings。消息的發射omit與訂閱subscribe.

消息流: Streams

消息流是storm裏面的最關鍵的抽象對象。一個消息流是一個沒有邊界的tuple序列, 而這些tuples會被以一種分佈式的方式並行地創建和處理。 對消息流的定義主要是對消息流裏面的tuple的定義, 我們會給tuple裏的每個字段一個名字。 並且不同tuple的對應字段的類型必須一樣。 也就是說: 兩個tuple的第一個字段的類型必須一樣, 第二個字段的類型必須一樣, 但是第一個字段和第二個字段可以有不同的類型。 在默認的情況下, tuple的字段類型可以是:integer, long, short, byte, string, double, float, booleanbyte array 你還可以自定義類型 — 只要你實現對應的序列化器。

每個消息流在定義的時候會被分配給一個id 因爲單向消息流是那麼的普遍,OutputFieldsDeclarer定義了一些方法讓你可以定義一個stream而不用指定這個id。在這種情況下這個stream會有個默認的id: 1.

消息源: Spouts

消息源Spoutsstorm裏面一個topology裏面的消息生產者。一般來說消息源會從一個外部源讀取數據並且向topology裏面發出消息: tuple 消息源Spouts可以是可靠的也可以是不可靠的。一個可靠的消息源可以重新發射一個tuple如果這個tuple沒有被storm成功的處理, 但是一個不可靠的消息源Spouts一旦發出一個tuple就把它徹底忘了 — 也就不可能再發了。

消息源可以發射多條消息流stream。要達到這樣的效果, 使用OutFieldsDeclarer.declareStream來定義多個stream, 然後使用SpoutOutputCollector來發射指定的stream

Spout類裏面最重要的方法是nextTuple要麼發射一個新的tupletopology裏面或者簡單的返回如果已經沒有新的tuple了。要注意的是nextTuple方法不能block Spout的實現, 因爲storm在同一個線程上面調用所有消息源Spout的方法。

另外兩個比較重要的Spout方法是ackfailstorm在檢測到一個tuple被整個topology成功處理的時候調用ack, 否則調用failstorm只對可靠的spout調用ackfail

消息處理者: Bolts

所有的消息處理邏輯被封裝在bolts裏面。 Bolts可以做很多事情: 過濾, 聚合, 查詢數據庫等等等等。

Bolts可以簡單的做消息流的傳遞。複雜的消息流處理往往需要很多步驟, 從而也就需要經過很多Bolts。比如算出一堆圖片裏面被轉發最多的圖片就至少需要兩步: 第一步算出每個圖片的轉發數量。第二步找出轉發最多的前10個圖片。(如果要把這個過程做得更具有擴展性那麼可能需要更多的步驟)。

Bolts可以發射多條消息流, 使用OutputFieldsDeclarer.declareStream定義stream 使用OutputCollector.emit來選擇要發射的stream

Bolts的主要方法是execute, 它以一個tuple作爲輸入,Bolts使用OutputCollector來發射tuple, Bolts必須要爲它處理的每一個tuple調用OutputCollectorack方法,以通知storm這個tuple被處理完成了。– 從而我們通知這個tuple的發射者Spouts 一般的流程是: Bolts處理一個輸入tuple, 發射0個或者多個tuple, 然後調用ack通知storm自己已經處理過這個tuple了。storm提供了一個IBasicBolt會自動調用ack

Stream groupings 消息分發策略

定義一個Topology的其中一步是定義每個bolt接受什麼樣的流作爲輸入。stream grouping就是用來定義一個stream應該如果分配給Bolts上面的多個Tasks

storm裏面有6種類型的stream grouping:

1. Shuffle Grouping: 隨機分組, 隨機派發stream裏面的tuple 保證每個bolt接收到的tuple數目相同。

2. Fields Grouping:按字段分組, 比如按userid來分組, 具有同樣useridtuple會被分到相同的Bolts 而不同的userid則會被分配到不同的Bolts

3. All Grouping 廣播發送, 對於每一個tuple 所有的Bolts都會收到。

4. Global Grouping: 全局分組, 這個tuple被分配到storm中的一個bolt的其中一個task。再具體一點就是分配給id值最低的那個task

5. Non Grouping: 不分組, 這個分組的意思是說stream不關心到底誰會收到它的tuple。目前這種分組和Shuffle grouping是一樣的效果, 有一點不同的是storm會把這個bolt放到這個bolt的訂閱者同一個線程裏面去執行。

6. Direct Grouping: 直接分組, 這是一種比較特別的分組方法,用這種分組意味着消息的發送者舉鼎由消息接收者的哪個task處理這個消息。 只有被聲明爲Direct Stream的消息流可以聲明這種分組方法。而且這種消息tuple必須使用emitDirect方法來發射。消息處理者可以通過TopologyContext來或者處理它的消息的taskid (OutputCollector.emit方法也會返回taskid)


1.4.2 Storm工作原理

Storm中有對於流stream的抽象,流是一個不間斷的無界的連續tuple,注意Storm在建模事件流時,把流中的事件抽象爲tuple即元組,後面會解釋storm中如何使用tuple

Storm認爲每個stream都有一個stream源,也就是原始元組的源頭,所以它將這個源頭抽象爲spoutspout可能是連接twitter api並不斷髮出tweets,也可能是從某個隊列中不斷讀取隊列元素並裝配爲tuple發射。

有了源頭即spout也就是有了stream,那麼該如何處理stream內的tuple呢,同樣的思想twitter將流的中間狀態轉換抽象爲Boltbolt可以消費任意數量的輸入流,只要將流方向導向該bolt,同時它也可以發送新的流給其他bolt使用,這樣一來,只要打開特定的spout(管口)再將spout中流出的tuple導向特定的bolt,又bolt對導入的流做處理後再導向其他bolt或者目的地。

我們可以認爲spout就是一個的水龍頭,並且每個水龍頭裏流出的水是不同的,我們想拿到哪種水就擰開哪個水龍頭,然後使用管道將水龍頭的水導向到一個水處理器(bolt),水處理器處理後再使用管道導向另一個處理器或者存入容器中。

爲了增大水處理效率,我們很自然就想到在同個水源處接上多個水龍頭並使用多個水處理器,這樣就可以提高效率。沒錯Storm就是這樣設計的,看到下圖我們就明白了。

對應上文的介紹,我們可以很容易的理解這幅圖,這是一張有向無環圖,Storm將這個圖抽象爲Topology即拓撲(的確,拓撲結構是有向無環的),拓撲是storm中最高層次的一個抽象概念,它可以被提交到storm集羣執行,一個拓撲就是一個流轉換圖,圖中每個節點是一個spout或者bolt,圖中的邊表示bolt訂閱了哪些流,當spout或者bolt發送元組到流時,它就發送元組到每個訂閱了該流的bolt(這就意味着不需要我們手工拉管道,只要預先訂閱,spout就會將流發到適當bolt上)。

插個位置說下stormtopology實現,爲了做實時計算,我們需要設計一個拓撲圖,並實現其中的Bolt處理細節,Storm中拓撲定義僅僅是一些Thrift結構體,這樣一來我們就可以使用其他語言來創建和提交拓撲。

Storm將流中元素抽象爲tuple,一個tuple就是一個值列表value listlist中的每個value都有一個name,並且該value可以是基本類型,字符類型,字節數組等,當然也可以是其他可序列化的類型。拓撲的每個節點都要說明它所發射出的元組的字段的name,其他節點只需要訂閱該name就可以接收處理。0

The tuple is the main data structure in Storm. A tuple is a named list of values, where each value can be any type. Tuples are dynamically typed -- the types of the fields do not need to be declared. Tuples have helper methods like getInteger and getString to get field values without having to cast the result. Storm needs to know how to serialize all the values in a tuple. By default, Storm knows how to serialize the primitive types, strings, and byte arrays. If you want to use another type, you'll need to implement and register a serializer for that type. Seehttp://github.com/apache/incubator-storm/wiki/Serialization for more info.


Storm(四)消息的可靠傳輸

  消息在各個節點之間的傳遞,要保證準確性,一般要做到兩點:一是接收者必須能收到,二是必須只收到一個,或者收到第二個時知道重複了,能夠把重複的捨棄。這在Storm裏面是通過Guaranteeing Message Processing和Transactional Topologies這兩種機制來保障的,這一篇主要介紹可靠傳輸。

  在網絡的傳輸層(TCP)以及鏈路層,爲了確保數據的可靠傳輸,發送方在發送數據的時候會指定一個序列號,接收方收到之後返回ACK,如果發送方等待ACK超時,就會認爲數據丟失,進行重傳。Storm可靠傳輸機制的原理是基本一致的,當Spout發射數據的時候,會附帶一個id,當這個tuple被消費完之後,由ACKer線程調用Spout的ack方法告訴Spout這個消息已經被成功處理,因爲Spout可能會有很多task並行執行,ACKer調用的ack一定是發射該tuple那個task的acker方法,如果超過一定時間這個tuple還沒有被消費完,Spout的fail方法會被調用,Spout可以根據情況選擇是否重傳。這個超時時間可以在Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS中設置,默認爲30s,有時候我們也可以在消息處理節點上直接調用fail方法,讓Spout不等到超時就立即重置發送,比如這樣的場景,最後往數據庫裏存數據的節點,由於某種原因存儲失敗,就可以fail讓Spout重新發這個數據。

  每個節點都可能收到tuple,根據這個tuple可能要發射新的tuple,這些相互關聯tuple就可以構成一棵樹,如下圖所示,一個句子發出來,被Bolt拆分,這些拆分後再次被emit的數據就和原來的tuple形成父子關係。

  Storm提供的可靠傳輸的API主要要求開發者做兩件事,第一件事是在這些tuple之間創建link,構建這個tuple樹,這個link在storm中叫做anchoring,創建的方式是在調用collector.emit發射數據的時候和輸入tuple做關聯,第二件事是tuple的發出者要知道這個tuple有沒有被成功消費,這個是通過在消費節點調用collector.ack或collector.fail方法實現。

  像這個圖,每個節點代表一個tuple,這就是4個tuple,Spout調用collector.emit(A)把這個tuple發送出來,消費節點通過調用collector.emit(A, B)和collector.emit(A, C)在A和B以及A和C之間創建anchor。可以通過

List<Tuple> list;

list.add(A);

list.add(B);

collector.emit(list,D);

  在D和多個上級tuple之間創建anchor。這種多源的綁定會破壞樹形的結構,構成了一個DAG(有向無環圖)。

  如果消費節點都不調用ack會怎麼樣?因爲storm要在內存中維持各個tuple的狀態和從屬關係,如果所有的tuple都不被ack,那麼超時之前storm就會存很多很多的數據和信息,就有可能發生內存耗盡。

  前面我寫的collector都是OutputCollector這個類的實例,我們經常會在Bolt裏面做類似的事情,那就是接收一個Tuple、發射新的Tuple並與前面的Tuple建立anchor,然後向前一個Tuple發送ack,爲了簡化這種重複性的操作,Storm封裝了一個BasicBolt類,並把OutputCollector也做了封裝,變成了BasicOutputCollector,這樣一來,用戶就只是簡單調用collector.emit(B),所有的anchor、ack操作就都由Storm來完成了。

  當Spout發射出來的tuple被消費完之後,由ACKer線程調用發射該tuple的task的acker方法通知Spout,ACKer線程的數量應該怎麼設置?這個屬性由Config.TOPOLOGY_ACKERS來控制,默認爲1,這個線程一般不用太多,一般情況下一個已經夠用,可以參照StormUI上的吞吐量來改變它的大小。

  ACKer線程維護着一個個DAG,每當有一個tuple被ACK、以及有一個新的tuple被髮出,只要這個DAG發生變化了,ACKer線程就會得到通知,因爲ACKer線程可能有多個,tuple又不攜帶ACKer線程的信息,那它怎麼知道該通知哪個ACKer呢?這裏有一個默認的實現,就是messageid,本文一開始舉了TCP的例子,發送方發送時會指定一個序列號,作爲接收ACK的標識,messageid就是Spout發某個數據的時候指定的序列號,把messageid對ACKer線程數量取模,得到的序號就是處理這個DAG的ACKer線程。另外,Spout在發數據的時候也會通知對應ACKer自己是哪一個task,等到這個消息被處理完,這個task的ack方法就會被調用。

  ACKer線程會掃描整個DAG,當發現DAG上的每一個tuple都被ACK了,就認爲這個消息被完全處理了。但是,ACKer要怎麼掃描DAG,一個tuple樹動不動就可能有上萬個節點,去遍歷它開銷太大,Storm採用了一種很巧妙的方法,是Storm性能優化上一個關鍵的突破:所有的tuple在創建的時候都會被Storm分配一個64位長的隨機id,下文把它標記爲tupleid。前面提到,Spout在發射原始數據的時候會附帶一個messageid用以可靠傳輸。Spout發出的tuple以及所有衍生tuple都會攜帶這個messageid,ACKer線程爲每一個messageid維護了一個64位的字符串作爲校驗和(下文標記爲checksum),每當有tuple被ack(消費掉)或者emit(新生成),對應的ACKer線程就會得到這個tuple對應的messageid和tupleid,ACKer把新得到的tupleid和checksum去做異或,當checksum的值變成0的時候,就認爲這個DAG被完全處理了,因爲只有當這些隨機生成的tupleid都被異或兩次(一次是生成一次是被消費)時,checksum才能是0。當然這個話說的太絕對,是有可能出現偶然情況讓checksum在DAG還沒有被完全處理的時候就變成0的,但是因爲隨機id 64位的長度,假設每秒有10k的ack,那麼數學上算一下,需要5000萬年纔可能出現一次這種偶然。再者,即便這種偶然發生了,還有可能因爲某個節點調用了fail造成整個數據的重傳,從而使出現錯誤的機率更低。

  最後看幾個能夠導致fail的情況,並且看看Storm是怎麼處理的:

  1. 處理tuple的task掛了,所以沒ack:這種情況下發送節點等待timeout,數據會被重傳。
  2. ACKer線程掛了:Spout發送的所有tuple都會因timeout而重傳。
  3. Spout掛了:這個要依賴數據源比如Kestrel或者Kafka的恢復機制。
  4. Nimbus或Supervisor掛了:他們都是無狀態的,但他們會把狀態信息存儲在zookeeper裏面,重啓之後可以讀取之前的狀態,繼續處理。

 

  如果可靠傳輸不那麼重要,出於性能考慮,可以關掉可靠傳輸,關閉的方法有這麼幾種:一、把Config.TOPOLOGY_ACKERS設成0,如果ACKer線程數是0,那麼tuple在Spout中發出來之後立即就會被執行ack方法。二、前面說了,ACKer對整個過程的管理依賴於Spout發送tuple之後附帶的一個messageid,如果Spout發送的時候不指定,這個功能就沒有了。三、bolt中發射新的tuple的時候不要和前面的tuple建立anchor。

  這裏面屢屢提到DAG被消費完全,Storm裏面用的詞是complete,其實不僅僅所有的tuple都被ACK了才叫complete,它包含兩種情況:tuple全部被ack和某節點出現了fail都叫做complete,一旦complete了,這個tuple tree就要被刪掉,如果是所有tuple都被ack,那麼tuple tree被刪除之後繼續後面的處理;如果fail了,tuple tree被刪掉之後還要根據Spout的策略重新發送失敗的數據。

  ACKer本質上也是一個Bolt,並且使用RotatingMap來管理超時。


以上講的是每個tuple至少被處理一次,但如果需要每個消息有且僅有一次,如果某個tuple失敗,導致了spout重傳root tuple,有些tuple就會被多次處理,那怎麼辦?

Trident將stream中的tuples分成batches進行處理,API封裝了對這些batches的處理過程,保證tuple只被處理一次。處理batches中間結果存儲在TridentState對象中。

記住一點,Trident 是通過小數據塊(batch)的方式來處理 tuple 的,而且每個 batch 都會有一個唯一的 txid。spout 的特性是由他們所提供的容錯性保證機制決定的,而且這種機制也會對每個 batch 發生作用。事務型 spout 包含以下特性:

  1. 每個 batch 的 txid 永遠不會改變。對於某個特定的 txid,batch 在執行重新處理操作時所處理的 tuple 集和它的第一次處理操作完全相同。
  2. 不同 batch 中的 tuple 不會出現重複的情況(某個 tuple 只會出現在一個 batch 中,而不會同時出現在多個 batch 中)。
  3. 每個 tuple 都會放入一個 batch 中(處理操作不會遺漏任何的 tuple)。

這是一種很容易理解的 spout,其中的數據流會被分解到固定的 batches 中。Storm-contrib 項目中提供了一種基於 Kafka 的事務型 spout 實現

看到這裏,你可能會有這樣的疑問:爲什麼不在拓撲中完全使用事務型 spout 呢?這個原因很好理解。一方面,有些時候事務型 spout 並不能提供足夠可靠的容錯性保障,所以不需要使用事務型 spout。比如,TransactionalTridentKafkaSpout 的工作方式就是使得帶有某個 txid 的 batch 中包含有來自一個 Kafka topic 的所有 partition 的 tuple。一旦一個 batch 被髮送出去,在將來無論重新發送這個 batch 多少次,batch 中都會包含有完全相同的 tuple 集,這是由事務型 spout 的語義決定的。現在假設TransactionalTridentKafkaSpout 發送出的某個 batch 處理失敗了,而與此同時,Kafka 的某個節點因爲故障下線了。這時你就無法重新處理之前的 batch 了(因爲 Kafka 的節點故障,Kafka topic 必然有一部分 partition 無法獲取到),這個處理過程也會因此終止。

這就是要有“模糊事務型” spout 的原因了 —— 模糊事務型 spout 支持在數據源節點丟失的情況下仍然可以實現恰好一次的處理語義。我們會在下一節討論這類 spout。

順便提一點,如果 Kafka 支持數據複製,那麼就可以放心地使用事務型 spout 提供的容錯性機制了,因爲這種情況下某個節點的故障不會導致數據丟失,不過 Kafka 暫時還不支持該特性。(本文的寫作時間應該較早,Kakfa 早就已經可以支持複製的機制了 —— 譯者注)。

在討論“模糊事務型” spout 之前,讓我們先來看看如何爲事務型 spout 設計一種支持恰好一次語義的 State。這個 State 就稱爲 “事務型 state”,它支持對於特定的 txid 永遠只與同一組 tuple 相關聯的特性。

假如你的拓撲需要計算單詞數,而且你準備將計數結果存入一個 K-V 型數據庫中。這裏的 key 就是單詞,value 對應於單詞數。從上面的討論中你應該已經明白了僅僅存儲計數結果是無法確定某個 batch 中的tuple 是否已經被處理過的。所以,現在你應該將 txid 作爲一種原子化的值與計數值一起存入數據庫。隨後,在更新計數值的時候,你就可以將數據庫中的 txid 與當前處理的 batch 的 txid 進行比對。如果兩者相同,你就可以跳過更新操作 —— 由於 Trident 的強有序性處理機制,可以確定數據庫中的值是對應於當前的 batch 的。如果兩者不同,你就可以放心地增加計數值。由於一個 batch 的 txid 永遠不會改變,而且 Trident 能夠保證 state 的更新操作完全是按照 batch 的順序進行的,所以,這樣的處理邏輯是完全可行的。

下面來看一個例子。假如你正在處理 txid 3,其中包含有以下幾個 tuple:

["man"]
["man"]
["dog"]

假如數據庫中有以下幾個 key-value 對:

man => [count=3, txid=1]
dog => [count=4, txid=3]
apple => [count=10, txid=2]

其中與 “man” 相關聯的 txid 爲 1。由於當前處理的 txid 爲 3,你就可以確定當前處理的 batch 與數據庫中存儲的值無關,這樣你就可以放心地將 “man” 的計數值加上 2 並更新 txid 爲 3。另一方面,由於 “dog” 的 txid 與當前的 txid 相同,所以,“dog” 的計數是之前已經處理過的,現在不能再對數據庫中的計數值進行更新操作。這樣,在結束 txid3 的更新操作之後,數據庫中的結果就會變成這樣:

man => [count=5, txid=3]
dog => [count=4, txid=3]
apple => [count=10, txid=2]

模糊事務型 spout(Opaque transactional spouts)

前面已經提到過,模糊事務型 spout 不能保證一個 txid 對應的 batch 中包含的 tuple 完全一致。模糊事務型 spout 有以下的特性:

  1. 每個 tuple 都會通過某個 batch 處理完成。不過,在 tuple 處理失敗的時候,tuple 有可能繼續在另一個 batch 中完成處理,而不一定是在原先的 batch 中完成處理。

OpaqueTridentKafkaSpout 就具有這樣的特性,同時它對 Kafka 節點的丟失問題具有很好的容錯性。OpaqueTridentKafkaSpout在發送一個 batch 的時候總會總上一個 batch 結束的地方開始發送新 tuple。這一點可以保證 tuple 不會被遺漏,而且也不會被多個 batch 處理。

不過,模糊事務型 spout 的缺點就在於不能通過 txid 來識別數據庫中的 state 是否是已經處理過的。這是因爲在 state 的更新的過程中,batch 有可能會發生變化。

在這種情況下,你應該在數據庫中存儲更多的 state 信息。除了一個結果值和 txid 之外,你還應該存入前一個結果值。我們再以上面的計數值的例子來分析以下這個問題。假如你的 batch 的部分計數值是 “2”,現在你需要應用一個更新操作。假定現在數據庫中的值是這樣的:

{ value = 4,
  prevValue = 1,
  txid = 2
}
  • 情形1:假如當前處理的 txid 爲 3,這與數據庫中的 txid 不同。這時可以將 “prevValue” 的值設爲 “value” 的值,再爲 “value” 的值加上部分計數的結果並更新 txid。執行完這一系列操作之後的數據庫中的值就會變成這樣:
{ value = 6,
  prevValue = 4,
  txid = 3
}
  • 情形2:如果當前處理的 txid 爲 2,也就是和數據庫中存儲的 txid 一致,這種情況下的處理邏輯與上面的 txid 不一致的情況又有所不同。因爲此時你會知道數據庫中的更新操作是由上一個擁有相同 txid 的batch 做出的。不過那個 batch 有可能與當前的 batch 並不相同,所以你需要忽略它的操作。這個時候,你應該將 “prevValue” 加上 batch 中的部分計數值來計算新的 “value”。在這個操作之後數據庫中的值就會變成這樣:
{ value = 3,
  prevValue = 1,
  txid = 2
}

這種方法之所以可行是因爲 Trident 具有強順序性處理的特性。一旦 Trident 開始處理一個新的 batch 的狀態更新操作,它永遠不會回到過去的 batch 的處理上。同時,由於模糊事務型 spout 會保證 batch 之間不會存在重複 —— 每個 tuple 只會被某一個 batch 完成處理 —— 所以你可以放心地使用 prevValue 來更新 value。

非事務型 spout(Non-transactional spouts)

非事務型 spout 不能爲 batch 提供任何的安全性保證。非事務型 spout 有可能提供一種“至多一次”的處理模型,在這種情況下 batch 處理失敗後 tuple 並不會重新處理;也有可能提供一種“至少一次”的處理模型,在這種情況下可能會有多個 batch 分別處理某個 tuple。總之,此類 spout 不能提供“恰好一次”的語義。

不同類型的 Spout 與 State 的總結

下圖顯示了不同的 spout/state 的組合是否支持恰好一次的消息處理語義:

spout-state

模糊事務型 state 具有最好的容錯性特徵,不過這是以在數據庫中存儲更多的內容爲代價的(一個 txid 和兩個 value)。事務型 state 要求的存儲空間相對較小,但是它的缺點是隻對事務型 spout 有效。相對的,非事務型要求的存儲空間最少,但是它也不能提供任何的恰好一次的消息執行語義。

你選擇 state 與 spout 的時候必須在容錯性與存儲空間佔用之間權衡。可以根據你的應用的需求來確定哪種組合最適合你。



關於storm的wordCount的例子:

git clone https://github.com/qinzhaokun/storm-start.git

歡迎指出不足指出不足之處。

發佈了27 篇原創文章 · 獲贊 67 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章