Storm介紹&實際開發注意事項

fancybox發表於2019-04-03

一、使用元件的並行度代替執行緒池

        Storm 自身是一個分散式、多執行緒的框架,對每個Spout 和Bolt,我們都可以設定其併發度;它也支援通過rebalance 命令來動態調整併發度,把負載分攤到多個Worker 上。
       如果自己在元件內部採用執行緒池做一些計算密集型的任務,比如JSON 解析,有可能使得某些元件的資源消耗特別高,其他元件又很低,導致Worker 之間資源消耗不均衡,這種情況在元件並行度比較低的時候更明顯。

       比如某個Bolt 設定了1 個並行度,但在Bolt 中又啟動了執行緒池,這樣導致的一種後果就是,叢集中分配了這個Bolt 的Worker 程式可能會把機器的資源都給消耗光了,影響到其他Topology 在這臺機器上的任務的執行。如果真有計算密集型的任務,我們可以把元件的併發度設大,Worker 的數量也相應提高,讓計算分配到多個節點上。
       為了避免某個Topology 的某些元件把整個機器的資源都消耗光的情況,除了不在元件內部啟動執行緒池來做計算以外,也可以通過CGroup 控制每個Worker 的資源使用量。

二、不要用DRPC 批量處理大資料
RPC 提供了應用程式和Storm Topology 之間互動的介面,可供其他應用直接呼叫,使用Storm 的併發性來處理資料,然後將結果返回給呼叫的客戶端。這種方式在資料量不大的情況下,通常不會有問題,而當需要處理批量大資料的時候,問題就比較明顯了。
(1)處理資料的Topology 在超時之前可能無法返回計算的結果。
(2)批量處理資料,可能使得叢集的負載短暫偏高,處理完畢後,又降低迴來,負載均衡性差。
批量處理大資料不是Storm 設計的初衷,Storm 考慮的是時效性和批量之間的均衡,更多地看中前者。需要準實時地處理大資料量,可以考慮Spark Stream 等批量框架。

三、不要在Spout 中處理耗時的操作
Spout 中nextTuple 方法會發射資料流,在啟用Ack 的情況下,fail 方法和ack 方法會被觸發。
需要明確一點,在Storm 中Spout 是單執行緒(JStorm 的Spout 分了3 個執行緒,分別執行nextTuple 方法、fail 方法和ack 方法)。如果nextTuple 方法非常耗時,某個訊息被成功執行完畢後,Acker 會給Spout 傳送訊息,Spout 若無法及時消費,可能造成ACK 訊息超時後被丟棄,然後Spout 反而認為這個訊息執行失敗了,造成邏輯錯誤。反之若fail 方法或者ack方法的操作耗時較多,則會影響Spout 發射資料的量,造成Topology 吞吐量降低。

四、注意fieldsGrouping 的資料均衡性
fieldsGrouping 是根據一個或者多個Field 對資料進行分組,不同的目標Task 收到不同
的資料,而同一個Task 收到的資料會相同。
假設某個Bolt 根據使用者ID 對資料進行fieldsGrouping,如果某一些使用者的資料特別多,而另外一些使用者的資料又比較少,那麼就可能使得下一級處理Bolt 收到的資料不均衡,整個處理的效能就會受制於某些資料量大的節點。可以加入更多的分組條件或者更換分組策略,使得資料具有均衡性。

五、優先使用localOrShuffleGrouping
localOrShuffleGrouping 是指如果目標Bolt 中的一個或者多個Task 和當前產生資料的Task 在同一個Worker 程式裡面,那麼就走內部的執行緒間通訊,將Tuple 直接發給在當前Worker 程式的目的Task。否則,同shuffleGrouping。
localOrShuffleGrouping 的資料傳輸效能優於shuffleGrouping,因為在Worker 內部傳輸,只需要通過Disruptor 佇列就可以完成,沒有網路開銷和序列化開銷。因此在資料處理的複雜度不高, 而網路開銷和序列化開銷佔主要地位的情況下, 可以優先使用localOrShuffleGrouping 來代替shuffleGrouping。

六、設定合理的MaxSpoutPending 值
在啟用Ack 的情況下,Spout 中有個RotatingMap 用來儲存Spout 已經傳送出去,但還沒有等到Ack 結果的訊息。RotatingMap 的最大個數是有限制的,為p*num-tasks。其中p 是topology.max.spout.pending 值,也就是MaxSpoutPending(也可以由TopologyBuilder 在setSpout 通過setMaxSpoutPending 方法來設定),num-tasks 是Spout 的Task 數。如果不設定MaxSpoutPending 的大小或者設定得太大,可能消耗掉過多的記憶體導致記憶體溢位,設定太小則會影響Spout 發射Tuple 的速度。

七、設定合理的Worker 數
Worker 數越多,效能越好?先看一張Worker 數量和吞吐量對比的曲線(來源於JStorm文件:https://github.com/alibaba/jstorm/tree/master/docs/ 0.9.4.1jstorm https://github.com/alibaba/jstorm/tree/master/docs/ 0.9.4.1jstorm 效能測試.docx)。、

 

 

從圖可以看出,在12 個Worker 的情況下,吞吐量最大,整體效能最優。這是由於一方面,每新增加一個Worker 程式,都會將一些原本執行緒間的記憶體通訊變為程式間的網路通訊,這些程式間的網路通訊還需要進行序列化與反序列化操作,這些降低了吞吐率。
另一方面,每新增加一個Worker 程式,都會額外地增加多個執行緒(Netty 傳送和接收執行緒、心跳執行緒、SystemBolt 執行緒以及其他系統元件對應的執行緒等),這些執行緒切換消耗了不少CPU,sys 系統CPU 消耗佔比增加,在CPU 總使用率受限的情況下,降低了業務執行緒的使用效率。

 

八、平衡吞吐量和時效性
Storm 的資料傳輸預設使用Netty。在資料傳輸效能方面,有如下的引數可以調整:
(1)storm.messaging.netty.server_worker_threads:為接收訊息執行緒;
(2)storm.messaging.netty.client_worker_threads:傳送訊息執行緒的數量;
(3)netty.transfer.batch.size:是指每次Netty Client 向Netty Server 傳送的資料的大小,
如果需要傳送的Tuple 訊息大於netty.transfer.batch.size , 則Tuple 訊息會按照netty.transfer.batch.size 進行切分,然後多次傳送。
(4)storm.messaging.netty.buffer_size:為每次批量傳送的Tuple 序列化之後的Task
Message 訊息的大小。
(5)storm.messaging.netty.flush.check.interval.ms:表示當有TaskMessage 需要傳送的時候, Netty Client 檢查可以傳送資料的頻率。
降低storm.messaging.netty.flush.check.interval.ms 的值, 可以提高時效性。增加netty.transfer.batch.size 和storm.messaging.netty.buffer_size 的值,可以提升網路傳輸的吐吞量,使得網路的有效載荷提升(減少TCP 包的數量,並且TCP 包中的有效資料量增加),通常時效性就會降低一些。因此需要根據自身的業務情況,合理在吞吐量和時效性直接的平衡。

 

實際上woker的實際執行數量受限於setworker配置和supervisor.slots.ports兩個配置

 

在Storm 的UI中,對沒過topology都提供了相應的統計資訊,其中有三個引數對效能來說參考意義比較明顯,包括Execute latency,Process latency和Capacity。

分別看一下三個引數的含義哈!

·Execute latency:訊息的平均處理時間,單位是毫秒。

·Process latency:訊息從收到到被ack掉所花費的時間,單位為毫秒。如果沒有啟用Acker機制,那麼Process latency的值為0。

·Capacity:計算公式為Capacity = Bolt 或者 Executor 呼叫 execute 方法處理的訊息數量 × 訊息平均執行時間/時間區間。如果這個值越接近1,說明Bolt或者 Executor 基本一直在呼叫 execute 方法,因此並行度不夠,需要擴充套件這個元件的 Executor數量。

Execute latency,Process latency是處理訊息的時效性,而Capacity則表示處理能力是否已經飽和。從這3個引數可以知道Topology的瓶頸所在。

 

如果使用非同步kafka/meta客戶端(listener方式)時,當增加/重啟meta時,均需要重啟topology

如果使用trasaction時,增加kafka/meta時, brokerId要按順序,即新增機器brokerId要比之前的都要大,這樣reassign spout消費brokerId時就不會發生錯位。
非事務環境中,儘量使用IBasicBolt
計算併發度時,
spout 按單task每秒500的QPS計算併發
全記憶體操作的task,按單task 每秒2000個QPS計算併發
有向外部輸出結果的task,按外部系統承受能力進行計算併發。
對於MetaQ 和 Kafka推薦一個worker執行2個task
拉取的頻率不要太小,低於100ms時,容易造成MetaQ/Kafka 空轉次數偏多
一次獲取資料Block大小推薦是2M或1M,太大記憶體GC壓力比較大,太小效率比較低。
 

將storm的訊息打包後傳送將極大的提升storm的吞吐量,減少emit次數 單機環境下平均每多一次emit,速率降低6%
設定一個合理的spout的ack等待佇列的長度 通過topology.max.spout.pending進行配置大小 
當pending滿時,spout將不會再向bolt發射資料,防止雪崩的發生
設定合理的executor的傳輸配置 topology.transfer.buffer.size=32 
topology.executor.receive.buffer.size=16384 
topology.executor.send.buffer.size=16384 
http://www.michael-noll.com/blog/2013/06/21/understanding-storm-internal-message-buffers/ 
這篇文章有較好的說明
配置合理的jvm引數 storm的topology預設只給768M的最大容量,實際生產環境中可能不夠 應該設定合理的新生代和老年代大小,儘可能的減少全域性gc 當storm分配的記憶體過大時(比如8G),可以使用cms回收器進行回收 遠端除錯storm和jmx監測 
由於可能會有多個拓撲執行在storm上所以通過直接指定埠的方式會造成埠占用的問題 解決方法有兩個 
1) 直接修改storm.yaml 
加入 需要重啟nimbus worker.childopts: “-Xmx1024m 
-Dcom.sun.management.jmxremote 
-Dcom.sun.management.jmxremote.ssl=false 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.port=1%ID% 
storm api中的註釋:

/* 
The jvm opts provided to workers launched by this supervisor. All “%ID%”, “%WORKER-ID%”, “%TOPOLOGY-ID%” 
and “%WORKER-PORT%” substrings are replaced with: 
%ID% -> port (for backward compatibility), 
%WORKER-ID% -> worker-id, 
%TOPOLOGY-ID% -> topology-id, 
%WORKER-PORT% -> port. 
*/ 
2) 通過在topology的config中配置topology.worker.childopts為需要的jvm引數 對於一些System.properties也是通過這個方法設定,這個不需要重啟storm, 
注意topology中設定worker.childopts並不會生效,中topology的main方法中進行System.setProperty也是無效的,必須通過這種方式進行設定

----------------------------------------------------------------------------------------------------------------------------------------------------------------------

在介紹 Storm 的效能調優方法之前,假設一個場景: 
專案組部署了3臺機器,計劃執行且僅執行 Storm(1.0.1) + Kafka(0.9.0.1) + Redis(3.2.1) 的小規模實驗叢集,叢集的配置情況如下表: 
 

現有一個任務,需要實時計算訂單的各項彙總統計資訊。訂單資料通過 kafka 傳輸。在 Storm 中建立了一個 topology 來執行此項任務,並採用 Storm kafkaSpout 讀取該 topic 的資料。kafka 和 Storm topology 的基本資訊如下:

kafka topic partitions = 3 
topology 的配置情況: 
TopologyBuilder builder = new TopologyBuilder(); 
builder.setSpout(“kafkaSpout”, new kafkaSpout(), 3); 
builder.setBolt(“filter”, new FilterBolt(), 3).shuffleGrouping(“kafkaSpout”); 
builder.setBolt(“alert”, new AlertBolt(), 3).fieldsGrouping(“filter”, new Fields(“order”));

Config conf = new Config(); 
conf.setNumWorkers(2); 
StormSubmitter.submitTopologyWithProgressBar(“topology-name”, conf, builder.createTopology()); 
那麼,在此假設下,Storm topology 的資料怎麼分發?效能如何調優?這就是下文要討論的內容,其中效能調優是最終目的,資料分發即 Storm 的訊息機制,則是進行調優前的知識儲備。

調優步驟和方法 
Storm topology 的效能優化方法,整體來說,可依次劃分為以下幾個步驟:

1.硬體配置的優化 
2.程式碼層面的優化 
3.topology 並行度的優化 
4.Storm 叢集配置引數和 topology 執行引數的優化 
其中第一點不是討論的重點,無外乎增加機器的硬體資源,提高機器的硬體配置等,但是這一步卻也不能忽略,因為機器配置太低,很可能後面的步驟怎麼調優都無濟於事。

Storm 的一些特性和原理,是進行調優的必要知識儲備 
Storm 的部分特性 
目前 Storm 的最新版本為 2.0.0-SNAPSHOT。該版本太新,未經過大量驗證和測試,因此本文的討論都基於 2.0 以前的版本。Storm 有如下幾個重要的特性:

DAG
常駐記憶體,topology 啟動後除非 kill 掉否則一直執行
提供 DRPC 服務
Pacemaker(1.0以後的新特性)心跳守護程式,常駐記憶體,比ZooKeeper效能更好
採用了 ZeroMQ 和 Netty 作為底層框架
採用了 ACK/fail 方式的 tuple 追蹤機制
並且 ack/fail 只能由建立該tuple的task所承載的spout觸發
瞭解這些機制對優化 Storm 的執行效能有一定幫助 
Storm 並行度 
Storm 是一個分散式的實時計算軟體,各節點,各元件間的通訊依賴於 zookeeper。從元件的角度看,Storm 運作機制構建在 nimbus, supervisor, woker, executor, task, spout/bolt 之上,如果再加上 topology,有時也可以稱這些元件為 Concepts(概念)。詳見官網介紹文章 http://storm.apache.org/releases/2.0.0-SNAPSHOT/Concepts.html 和 
http://storm.apache.org/releases/current/Tutorial.html

在介紹 Storm 並行度之前,先概括地瞭解下 Storm 的幾個概念,此處假定讀者有一定的 Storm 背景知識,至少曾經跑過一個 topology 例項。 
nimbus 和 supervisor 
nimbus 是 Storm 叢集的管理和排程程式,一個叢集啟動一個 nimbus,主要用於管理 topology,執行rebalance,管理 supervisor,分發 task,監控叢集的健康狀況等。nimbus 依賴 zookeeper 來實現上述職責,nimbus 與 supervisor 等其他元件並沒有直接的溝通。執行 nimbus 的節點成為主節點,執行 supervisor 的節點成為工作節點,nimbus 向 supervisor 分派任務,因此 Storm 叢集也是一個 master/slave 叢集。簡單來說,nimbus 就是工頭,supervisor 就是工人,nimbus 通過 zookeeper 來管理 supervisor。

supervisor 是一個工作程式,負責監聽 nimbus 分派的任務。當它接到任務後,會啟動一個 worker 程式,由 worker 執行 topology 的一個子集。為什麼說是子集呢?因為當一個 topology 提交到叢集后,nimbus 便會根據該 topology 的配置(此處假定 numWorker=3),將 topology 分配給3個 worker 並行執行(正常情況下是這樣,也有不是均勻分配的,比如有一個 supervisor 節點記憶體不足了)。如果剛好叢集有3個 supervisor,則每個 supervisor 會啟動1個 worker,即一個節點啟動一個 worker(一個節點只能有一個 supervisor 有效執行)。因此,worker 程式執行的是 topology 的一個子集。supervisor 同樣通過 zookeeper 與 nimbus 進行交流,因此 nimbus 和 supervisor 都可以快速失敗/停止,因為所有的狀態資訊都儲存在本地檔案系統的 zookeeper 中, 當失敗停止執行後,只需要重新啟動 nimbus 或 supervisor 程式以快速恢復。當然,如果叢集中正在工作的 supervisor 停止了,其上執行著的 topology 子集也會跟著停止,不過一旦 supervisor 啟動起來,topology 子集又立刻恢復正常了。 
 

 


nimbus 和 supervisor 的協作關係 
worker 
worker 是一個JVM程式,由 supervisor 啟動和關閉。當 supervisor 接到任務後,會根據 topology 的配置啟動若干 worker,實際的任務執行便由 worker 進行。worker 程式會佔用固定的可由配置進行修改的記憶體空間(預設768M)。通常使用 conf.setNumWorkers() 函式來指定一個 topolgoy 的 worker 數量。

executor 
executor 是一個執行緒,由 worker 程式派生(spawned)。executor 執行緒負責根據配置派生 task 執行緒,預設一個 executor 建立一個 task,可通過 setNumTask() 函式指定每個 executor 的 task 數量。executor 將例項化後的 spout/bolt 傳遞給 task。

task 
task 可以說是 topology 最終的實際的任務執行者,每個 task 承載一個 spout 或 bolt 的例項,並呼叫其中的 spout.nexTuple(),bolt.execute() 等方法,而 spout.nexTuple() 是資料的發射器,bolt.execute() 則是資料的接收方,業務邏輯的程式碼基本上都在這兩個函式裡面處理了,因此可以說 task 是最終搬磚的苦逼。

topology 
topology 中文翻譯為拓撲,類似於 hdfs 上的一個 mapreduce 任務。一個 topology 定義了執行一個 Storm 任務的所有必要元件,主要包括 spout 和 bolt,以及 spout 和 bolt 之間的流向關係。

 

 


topology 結構 
並行度 
什麼是並行度?在 Storm 的設定裡,並行度大體分為3個方面:

一個 topology 指定多少個 worker 程式並行執行;
一個 worker 程式指定多少個 executor 執行緒並行執行;
一個 executor 執行緒指定多少個 task 並行執行。
一般來說,並行度設定越高,topology 執行的效率就越高,但是也不能一股腦地給出一個很高的值,還得考慮給每個 worker 分配的記憶體的大小,還得平衡系統的硬體資源,以避免浪費。 
Storm 叢集可以執行一個或多個 topology,而每個 topology 包含一個或多個 worker 程式,每個 worer 程式可派生一個或多個 executor 執行緒,而每個 executor 執行緒則派生一個或多個 task,task 是實際的資料處理單元,也是 Storm 概念裡最小的工作單元, spout 或 bolt 的例項便是由 task 承載。

 

 


worker executor task 的關係 
為了更好地解釋 worker、executor 和 task 之間的工作機制,我們用官網的一個簡單 topology 示例來介紹。先看此 topology 的配置:

Config conf = new Config(); 
conf.setNumWorkers(2); // 為此 topology 配置兩個 worker 程式

topologyBuilder.setSpout(“blue-spout”, new BlueSpout(), 2); // blue-spout 並行度=2

topologyBuilder.setBolt(“green-bolt”, new GreenBolt(), 2) // green-bolt 並行度=2 
.setNumTasks(4) // 為此 green-bolt 配置 4 個 task 
.shuffleGrouping(“blue-spout”);

topologyBuilder.setBolt(“yellow-bolt”, new YellowBolt(), 6) // yellow-bolt 並行度=6 
.shuffleGrouping(“green-bolt”);

StormSubmitter.submitTopology( 
“mytopology”, 
conf, 
topologyBuilder.createTopology() 
); 
從上面的程式碼可以知道:

這個 topology 裝備了2個 worker 程式,也就是同樣的工作會有 2 個程式並行進行,可以肯定地說,2個 worker 肯定比1個 worker 執行效率要高很多,但是並沒有2倍的差距;
配置了一個 blue-spout,並且為其指定了 2 個 executor,即並行度為2;
配置了一個 green-bolt,並且為其指定了 2 個 executor,即並行度為2;
配置了一個 yellow-bolt,並且為其指定了 6 個 executor,即並行度為6;
大家看官方給出的下圖: 

 


一個 topology 的結構圖示 
可以看出,這個圖片完整無缺地還原了程式碼裡設定的 topology 結構:

圖左最大的灰色方框,表示這個 topology;
topology 裡面剛好有兩個白色方框,表示2個 worker 程式;
每個 worker 裡面的灰色方框表示 executor 執行緒,可以看到2個 worker 方框裡各有5個 executor,為什麼呢?因為程式碼裡面指定的 spout 並行度=2,green-bolt並行度=2,yellow-bolt並行度=6,加起來剛好是10,而配置的 worker 數量為2,那麼自然地,這10個 executor 會均勻地分配到2個 worker 裡面;
每個 executor 裡面的黃藍綠(寫著Task)的方框,就是最小的處理單元 task 了。大家仔細看綠色的 Task 方框,與其他 Task 不同的是,兩個綠色方框同時出現在一個 executor 方框內。為什麼會這樣呢?大家回到上文看 topology 的定義程式碼,topologyBuilder.setBolt(“green-bolt”, new GreenBolt(), 2).setNumTasks(4),這裡面的 setNumTasks(4) 表示為該 green-bolt 指定了4個 task,且 executor 的並行度為2,那麼自然地,這4個 task 會均勻地分配到2個 executor 裡面;
圖右的三個圓圈,依次是藍色的 blue-spout,綠色的 green-bolt 和黃色的 yellow-bolt,並且用箭頭指示了三個元件之間的關係。spout 是資料的產生元件,而 green-bolt 則是資料的中間接收節點,yellow-bolt 則是資料的最後接收節點。這也是 DAG 的體現,有向的(箭頭不能往回走)無環圖。
參考 
http://storm.apache.org/releases/1.0.1/Understanding-the-parallelism-of-a-Storm-topology.html 
http://www.michael-noll.com/blog/2012/10/16/understanding-the-parallelism-of-a-storm-topology/ 
一個 topology 的程式碼較完整例子 
TopologyBuilder builder = new TopologyBuilder();

BrokerHosts hosts = new ZkHosts(zkConns); 
SpoutConfig spoutConfig = new SpoutConfig(hosts, topic, zkRoot, clintId); 
spoutConfig.scheme = new SchemeAsMultiScheme(new StringScheme());

/* 指示 kafkaSpout 從 kafka topic 最後記錄的 ofsset 開始讀取資料 / 
spoutConfig.startOffsetTime = kafka.api.OffsetRequest.LatestTime();

KafkaSpout kafkaSpout = new KafkaSpout(spoutConfig); 
builder.setSpout(“kafkaSpout”, kafkaSpout, 3); // spout 並行度=3 
builder.setBolt(“filter”, new FilterBolt(), 3).shuffleGrouping(“kafkaSpout”); // FilterBolt 並行度=3 
builder.setBolt(“alert”, new AlertBolt(), 3).fieldsGrouping(“filter”, new Fields(“order”)); // AlertBolt 並行度=3

Config conf = new Config(); 
conf.setDebug(false); 
conf.setNumWorkers(3); // 為此 topology 配置3個 worker 程式 
conf.setMaxSpoutPending(10000);

try { 
StormSubmitter.submitTopologyWithProgressBar(topology, conf, builder.createTopology()); 
} catch (Exception e) { 
e.printStackTrace(); 

Storm 訊息機制 
Storm 主要提供了兩種訊息保證機制(Message Processing Guarantee)

至少一次 At least once
僅且一次 exactly once
其中 exactly once 是通過 Trident 方式實現的(exactly once through Trident)。兩種模式的選擇要視業務情況而定,有些場景要求精確的僅且一次消費,比如訂單處理,決不能允許重複的處理訂單,因為很可能會導致訂單金額、交易手數等計算錯誤;有些場景允許一定的重複,比如頁面點選統計,訪客統計等。總之,不管何種模式,Storm 都能保證資料不會丟失,開發者需要關心的是,如何保證資料不會重複消費。

At least once 的訊息處理機制,在運用時需要格外小心,Storm 採用 ack/fail 機制來追蹤訊息的流向,當一個訊息(tuple)傳送到下游時,如果超時未通知 spout,或者傳送失敗,Storm 預設會根據配置策略進行重發,可通過調節重發策略來儘量減少訊息的重複傳送。一個常見情況是,Storm 叢集經常會超負載執行,導致下游的 bolt 未能及時 ack,從而導致 spout 不斷的重發一個 tuple,進而導致訊息大量的重複消費。 
在與 Kafka 整合時,常用 Storm 提供的 kafkaSpout 作為 spout 消費 kafka 中的訊息。Storm 提供的 kafkaSpout 預設有兩種實現方式:至少一次消費的 core Storm spouts 和僅且一次消費的 Trident spouts :(We support both Trident and core Storm spouts)。

在 Storm 裡面,訊息的處理,通過兩個元件進行:spout 和 bolt。其中 spout 負責產生資料,bolt 負責接收並處理資料,業務邏輯程式碼一般都寫入 bolt 中。可以定義多個 bolt ,bolt 與 bolt 之間可以指定單向連結關係。通常的作法是,在 spout 裡面讀取諸如 kafka,mysql,redis,elasticsearch 等資料來源的資料,併發射(emit)給下游的 bolt,定義多個 bolt,分別進行多個不同階段的資料處理,比如第一個 bolt 負責過濾清洗資料,第二個 bolt 負責邏輯計算,併產生最終運算結果,寫入 redis,mysql,hdfs 等目標源。

Storm 將訊息封裝在一個 Tuple 物件裡,Tuple 物件經由 spout 產生後通過 emit() 方法傳送給下游 bolt,下游的所有 bolt 也同樣通過 emit() 方法將 tuple 傳遞下去。一個 tuple 可能是一行 mysql 記錄,也可能是一行檔案內容,具體視 spout 如何讀入資料來源,並如何發射給下游。

如下圖,是一個 spout/bolt 的執行過程: 

 


spout/bolt 的執行過程 
spout -> open(pending狀態) -> nextTuple -> emit -> bolt -> execute -> ack(spout) / fail(spout) -> message-provider 將該訊息移除佇列(complete) / 將訊息重新壓回佇列

ACK/Fail 
上文說到,Storm 保證了資料不會丟失,ack/fail 機制便是實現此機制的法寶。Storm 在內部構建了一個 tuple tree 來表示每一個 tuple 的流向,當一個 tuple 被 spout 發射給下游 bolt 時,預設會帶上一個 messageId,可以由程式碼指定但預設是自動生成的,當下遊的 bolt 成功處理 tuple 後,會通過 acker 程式通知 spout 呼叫 ack 方法,當處理超時或處理失敗,則會呼叫 fail 方法。當 fail 方法被呼叫,訊息可能被重發,具體取決於重發策略的配置,和所使用的 spout。

對於一個訊息,Storm 提出了『完全處理』的概念。即一個訊息是否被完全處理,取決於這個訊息是否被 tuple tree 裡的每一個 bolt 完全處理,當 tuple tree 中的所有 bolt 都完全處理了這條訊息後,才會通知 acker 程式並呼叫該訊息的原始發射 spout 的 ack 方法,否則會呼叫 fail 方法。

ack/fail 只能由建立該 tuple 的 task 所承載的 spout 觸發 
預設情況下,Storm 會在每個 worker 程式裡面啟動1個 acker 執行緒,以為 spout/bolt 提供 ack/fail 服務,該執行緒通常不太耗費資源,因此也無須配置過多,大多數情況下1個就足夠了。 

 


ack/fail 示意 
Worker 間通訊 
上文所說是在一個 worker 內的情況,但是 Storm 是一個分散式的平行計算框架,而實現並行的一個關鍵方式,便是一個 topology 可以由多個 worker 程式分佈在多個 supervisor 節點並行地執行。那麼,多個 worker 之間必然是會有通訊機制的。nimbus 和 supervsor 之間僅靠 zookeeper 進行溝通,那麼為何 worker 之間不通過 zookeeper 之類的中介軟體進行溝通呢?其中的一個原因我想,應該是元件隔離的原則。worker 是 supervisor 管理下的一個程式,那麼 worker 如果也採用 zookeeper 進行溝通,那麼就有一種越級操作的嫌疑了。

 

 


Worker 間通訊 
大家看上圖,一個 worker 程式裝配了如下幾個元件:

一個 receive 執行緒,該執行緒維護了一個 ArrayList,負責接收其他 worker 的 sent 執行緒傳送過來的資料,並將資料儲存到 ArrayList 中。資料首先存入 receive 執行緒的一個緩衝區,可通過 topology.receiver.buffer.size (此項配置在 Storm 1.0 版本以後被刪除了)來配置該緩衝區儲存訊息的最大數量,預設為8(個數,並且得是2的倍數),然後才被推送到 ArrayList 中。receive 執行緒接收資料,是通過監聽 TCP的埠,該埠有 storm 配置檔案中 supervisor.slots.prots 來配置,比如 6700;
一個 sent 執行緒,該執行緒維護了一個訊息佇列,負責將隊裡中的訊息傳送給其他 worker 的 receive 執行緒。同樣具有緩衝區,可通過 topology.transfer.buffer.size 來配置緩衝區儲存訊息的最大數量,預設為1024(個數,並且得是2的倍數)。當訊息達到此閾值時,便會被髮送到 receive 執行緒中。sent 執行緒傳送資料,是通過一個隨機分配的TCP埠來進行的。
一個或多個 executor 執行緒。executor 內部同樣擁有一個 receive buffer 和一個 sent buffer,其中 receive buffer 接收來自 receive 執行緒的的資料,sent buffer 向 sent 執行緒傳送資料;而 task 執行緒則介於 receive buffer 和 sent buffer 之間。receive buffer 的大小可通過 Conf.TOPOLOGY_EXECUTOR_RECEIVE_BUFFER_SIZE 引數配置,sent buffer 的大小可通過 Config.TOPOLOGY_EXECUTOR_SEND_BUFFER_SIZE 配置,兩個引數預設都是 1024(個數,並且得是2的倍數)。

Config conf = new Config(); 
conf.put(Config.TOPOLOGY_RECEIVER_BUFFER_SIZE, 16); // 預設8 
conf.put(Config.TOPOLOGY_TRANSFER_BUFFER_SIZE, 32); 
conf.put(Config.TOPOLOGY_EXECUTOR_RECEIVE_BUFFER_SIZE, 16384); 
conf.put(Config.TOPOLOGY_EXECUTOR_SEND_BUFFER_SIZE, 16384);

參考 
http://storm.apache.org/releases/1.0.1/Guaranteeing-message-processing.html 
http://www.michael-noll.com/blog/2013/06/21/understanding-storm-internal-message-buffers/ 
Storm UI 解析

首頁

 

Cluster Summary 

 


Nimbus Summary 
比較簡單,就略過了
Topology Summary 

 

這部分也比較簡單,值得注意的是 Assigned Mem (MB),這裡值得是分配給該 topolgoy 下所有 worker 工作記憶體之和,單個 worker 的記憶體配置可由 Config.WORKER_HEAP_MEMORY_MB 和 Config.TOPOLOGY_WORKER_MAX_HEAP_SIZE_MB 指定,預設為 768M,另外再加上預設 64M 的 logwritter 程式記憶體空間,則有 832M。 
此處 fast-pay 的值為 2496M = 3*832

Supervisor Summary 

 

此處也比較簡單,值得注意的是 slot 和 used slot 分別表示當前節點總的可用 worker 數,及已用掉的 worker 數。

 

Nimbus Configuration 

可搜尋和檢視當前 topology 的各項配置引數

topology 頁面

 

Topology summary 

此處的大部分配置與上文中出現的意義一樣,值得注意的是: 
Num executors 和 Num tasks 的值。其中 Num executors 的數量等於當前 topology 下所有 spout/bolt 的並行度總和,再加上所有 worker 下的 acker executor 執行緒總數(預設情況下一個 worker 派生一個 acker executor)。

Topology actions 

 

按鈕 說明
Activate 啟用此 topology
Deactivate 暫停此 topology 執行
Rebalance 調整並行度並重新平衡資源
Kill 關閉並刪除此 topology
Debug 除錯此 topology 執行,需要設定 topology.eventlogger.executors 數量 > 0
Stop Debug 停止除錯
Change Log Level 調整日誌級別
- Topology stats 

 

 

Spouts (All time) 

 

 

Bolts (All time) 

 


spout 頁面 
這個頁面,大部分都比較簡單,就不一一說明了,值得注意的是下面這個 Tab:

Executors (All time) 

 

這個Tab的引數,應該不用解釋了,但是要注意看,Emitted,Transferred 和 Acked 這幾個引數,看看是否所有的 executor 都均勻地分擔了 tuple 的處理工作。

bolt 頁面 
這個頁面與 spout 頁面類似,也不贅述了。

參考:這個頁面通過 API 的方式,對 UI 介面的引數做了一些解釋 
http://storm.apache.org/releases/1.0.1/STORM-UI-REST-API.html 
Storm debug 
Storm 提供了良好的 debug 措施,許多操作可以再 UI 上完成,也可以在命令列完成。比如 Change log level 在不重啟 topology 的情況下動態修改日誌記錄的級別,在 UI 介面上檢視某個 bolt 的日誌等,當然也可以在命令列上操作。

下面的參考文章寫的很詳細,大家有興趣可以去閱讀一下,本文就不再討論了。

參考 
https://community.hortonworks.com/articles/36151/debugging-an-apache-storm-topology.html 
效能調優 
上文說了這麼多,這才進入主題。

1、合理地配置硬體資源 
此處暫不討論

2、優化程式碼的執行效能 
要優化程式碼的效能,如果嚴謹一點,首先要有一個衡量程式碼執行效率的方式。在數學上,通常使用大O函式來衡量一個演算法的時間複雜度。我們可以考慮使用大O函式來近似地估計一個程式碼片段的執行時間:假定一行程式碼花費1個單位時間,那麼程式碼片段的時間複雜度可以近似地用大O表示為 O(n),其中n表示程式碼的行數或執行次數。當然,如果程式碼裡引入了其他的類和函式,或者處於迴圈體內,那麼其他類、函式的程式碼行數,以及迴圈體內程式碼的重複執行次數也需要統計在內。這裡說到大O函式的概念,在實際中也很少用到,我們往往會用第三方工具來較為準確地計算程式碼的實際執行時間,但是理解這個概念有助於優化我們的程式碼。有興趣的同學可以閱讀《演算法概論》這本書。

這裡順便舉一個斐波那契數列的例子:

/* C程式碼:用遞迴實現的斐波那契數列 / 
int fibonacci(unsigned int n) 

if (n <= 0) return 0; 
if (n == 1) return 1; 
return fibonacci(n - 1) + fibonacci(n - 2); 

/* C程式碼:用迴圈實現的斐波那契數列 / 
int fibonacci(unsigned int n) 

int r, a, b; 
int i; 
int result[2] = {0, 1};

if (n < 2) return result[n];
a = 0;
b = 1;
r = 0;
for (i = 2; i <= n; i++)
{
r = a + b;
a = b;
b = r;
}
return r;

觀看兩個不同實現的例子,第一種遞迴的方式,當傳入的n很大時,程式碼執行的時間將會呈指數級增長,這時T(n)接近於 O(2^n);第二種迴圈的方式,即使傳入很大的n,程式碼也可以在較短的時間內執行完畢,這時T(n)接近於O(n),為什麼是O(n)呢,比如說n=1000,那麼整個演算法的執行時間基本集中在那個 for 迴圈裡了,相當於執行了 for 迴圈內3行程式碼1000多次,所以差不多是n。這其實就是一種用空間換時間的概念,利用迴圈代替遞迴的方式,從而大大地優化了程式碼的執行效率。

回到我們的 Storm。程式碼優化,歸結起來,應該有這幾種:

在演算法層面進行優化
在業務邏輯層面進行優化
在技術層面進行優化
特定於 Storm,合理地規劃 topology,即安排多少個 bolt,每個 bolt 做什麼,連結關係如何
在技術層面進行優化,手法就非常多了,比如連線資料庫時,運用連線池,常用的連線池有 alibaba 的 druid,還有 redis 的連線池;比如合理地使用多執行緒,合理地優化JVM引數等等。這裡舉一個工作中可能會遇到的例子來介紹一下:

在配置了多個並行度的 bolt 中,存取 redis 資料時,如果不使用 redis 執行緒池,那麼很可能會遇到 topology 執行緩慢,spout 不斷重發,甚至直接掛掉的情況。首先 redis 的單個例項並不是執行緒安全的,其次在不使用 redis-pool 的情況下,每次讀取 redis 都建立一個 redis 連線,同建立一個 mysql 連線一樣,在連線 redis 時所耗費的時間相較於 get/set 本身是非常巨大的。

/** 
* redis-cli 操作工具類 
*/ 
package net.mtide.dbtool;

import java.util.List;

import org.slf4j.Logger; 
import org.slf4j.LoggerFactory;

import redis.clients.jedis.Jedis; 
import redis.clients.jedis.JedisPool; 
import redis.clients.jedis.JedisPoolConfig;

public class RedisCli {

private static JedisPool pool = null;

private final static Logger logger = LoggerFactory.getLogger(FilterBolt.class);

/**
* 同步初始化 JedisPool
*/
private static synchronized void initPool() {
if (pool == null) {
String hosts = "HOST";
String port = "PORT";
String pass = "PASS";
pool = new JedisPool(new JedisPoolConfig(), hosts, Integer.parseInt(port), 2000, pass);
}
}

/**
* 將連線 put back to pool
*
* @param jedis
*/
private static void returnResource(final Jedis jedis) {
if (pool != null && jedis != null) {
pool.returnResource(jedis);
}
}

/**
* 同步獲取 Jedis 例項
*
* @return Jedis
*/
public synchronized static Jedis getJedis() {
if (pool == null) {
initPool();
}
return pool.getResource();
}

public static void set(final String key, final String value) {
Jedis jedis = getJedis();
try {
jedis.set(key, value);
}
catch (Exception e) {
logger.error(e.toString());
}
finally {
returnResource(jedis);
}
}

public static void set(final String key, final String value, final int seconds) {
Jedis jedis = getJedis();
try {
jedis.set(key, value);
jedis.expire(key, seconds);
}
catch (Exception e) {
logger.error(e.toString());
}
finally {
returnResource(jedis);
}
}

public static String get(final String key) {
String value = null;

Jedis jedis = getJedis();
try {
value = jedis.get(key);
}
catch (Exception e) {
logger.error(e.toString());
}
finally {
returnResource(jedis);
}

return value;
}

public static List<String> mget(final String... keys) {
List<String> value = null;

Jedis jedis = getJedis();
try {
value = jedis.mget(keys);
}
catch (Exception e) {
logger.error(e.toString());
}
finally {
returnResource(jedis);
}

return value;
}

public static Long del(final String key) {
Long value = null;

Jedis jedis = getJedis();
try {
value = jedis.del(key);
}
catch (Exception e) {
logger.error(e.toString());
}
finally {
returnResource(jedis);
}

return value;
}

public static Long expire(final String key, final int seconds) {
Long value = null;

Jedis jedis = getJedis();
try {
value = jedis.expire(key, seconds);
}
catch (Exception e) {
logger.error(e.toString());
}
finally {
returnResource(jedis);
}

return value;
}

public static Long incr(final String key) {
Long value = null;

Jedis jedis = getJedis();
try {
value = jedis.incr(key);
}
catch (Exception e) {
logger.error(e.toString());
}
finally {
returnResource(jedis);
}

return value;
}

當一個配置了多個並行度的 topology 執行在叢集上時,如果 redis 操作不當,很可能會造成執行該 redis 的 bolt 長時間阻塞,從而造成 tuple 傳遞超時,預設情況下 spout 在 fail 後會重發該 tuple,然而 redis 阻塞的問題沒有解決,重發不僅不能解決問題,反而會加重叢集的執行負擔,那麼 spout 重發越來越多,fail 的次數也越來越多, 最終導致資料重複消費越來越嚴重。上面貼出來的 RedisCli 工具類,可以在多執行緒的環境下安全的使用 redis,從而解決了阻塞的問題。

3、合理的配置並行度 
有幾個手段可以配置 topology 的並行度:

conf.setNumWorkers() 配置 worker 的數量
builder.setBolt(“NAME”, new Bolt(), 並行度) 設定 executor 數量
spout/bolt.setNumTask() 設定 spout/bolt 的 task 數量
現在回到我們的一開始的場景假設:

專案組部署了3臺機器,計劃執行一個 Storm(1.0.1) + Kafka(0.9.0.1) + Redis(3.2.1) 的小規模實驗叢集,每臺機器的配置為 2CPUs,4G RAM

/* 初始配置 / 
TopologyBuilder builder = new TopologyBuilder(); 
builder.setSpout(“kafkaSpout”, new kafkaSpout(), 3); 
builder.setBolt(“filter”, new FilterBolt(), 3).shuffleGrouping(“kafkaSpout”); 
builder.setBolt(“alert”, new AlertBolt(), 3).fieldsGrouping(“filter”, new Fields(“order”));

Config conf = new Config(); 
conf.setNumWorkers(2); 
StormSubmitter.submitTopologyWithProgressBar(“topology-name”, conf, builder.createTopology()); 
那麼問題是:

setNumWorkers 應該取多少?取決於哪些因素?
kafkaSpout 的並行度應該取多少?取決於哪些因素?
FilterBolt 的並行度應該取多少?取決於哪些因素?
AlertBolt 的並行度應該取多少?取決於哪些因素?
FilterBolt 用 shuffleGrouping 是最好的嗎?
AlertBolt 用 fieldsGrouping 是最好的嗎?
回答如下: 
第一個問題:關於 worker 的並行度:worker 可以分配到不同的 supervisor 節點,這也是 Storm 實現多節點平行計算的主要配置手段。據此, workers 的數量,可以說是越多越好,但也不能造成浪費,而且也要看硬體資源是否足夠。所以主要考慮叢集各節點的記憶體情況:預設情況下,一個 worker 分配 768M 的記憶體,外加 64M 給 logwriter 程式;因此一個 worker 會耗費 832M 記憶體;題設的叢集有3個節點,每個節點4G記憶體,除去 linux 系統、kafka、zookeeper 等的消耗,保守估計僅有2G記憶體可用來執行 topology,由此可知,當叢集只有一個 topology 在執行的情況下,最多可以配置6個 worker。 
另外,我們還可以調節 worker 的記憶體空間。這取決於流過 topology 的資料量的大小以及各 bolt 單元的業務程式碼的執行時間。如果資料量特別大,程式碼執行時間較長,那麼可以考慮增加單個 worker 的工作記憶體。有一點需要注意的是,一個 worker 下的所有 executor 和 task 都是共享這個 worker 的記憶體的,也就是假如一個 worker 分配了 768M 記憶體,3個 executor,6個 task,那麼這個 3 executor 和 6 task 其實是共用這 768M 記憶體的,但是好處是可以充分利用多核 CPU 的運算效能。

總結起來,worker 的數量,取值因素有:

節點數量,及其記憶體容量
資料量的大小和程式碼執行時間
機器的CPU、頻寬、磁碟效能等也會對 Storm 效能有影響,但是這些外在因素一般不影響 worker 數量的決策。

需要注意的是,Storm 在預設情況下,每個 supervisor 節點只允許最多4個 worker(slot)程式執行;如果所配置的 worker 數量超過這個限制,則需要在 storm 配置檔案中修改。 
第二個問題:關於 FilterBolt 的並行度:如果 spout 讀取的是 kafka 的資料,那麼正常情況下,設定為 topic 的分割槽數量即可。計算 kafkaSpout 的最佳取值,有一個最簡單的辦法,就是在 Storm UI裡面,點開 topology 的首頁,在 Spouts (All time) 下,檢視以下幾個引數的值:

Emitted 已發射出去的tuple數
Transferred 已轉移到下一個bolt的tuple數
Complete latency (ms) 每個tuple在tuple tree中完全處理所花費的平均時間
Acked 成功處理的tuple數
Failed 處理失敗或超時的tuple數 

 

怎麼看這幾個引數呢?有幾個技巧:
正常情況下 Failed 值為0,如果不為0,考慮增加該 spout 的並行度。這是最重要的一個判斷依據;
正常情況下,Emitted、Transferred和Acked這三個值應該是相等或大致相等的,如果相差太遠,要麼該 spout 
負載太重,要麼下游負載過重,需要調節該 spout 的並行度,或下游 bolt 的並行度;
Complete latency (ms) 時間,如果很長,十秒以上就已經算很長的了。當然具體時間取決於程式碼邏輯,bolt 
的結構,機器的效能等。
kafka 只能保證同一分割槽下訊息的順序性,當 spout 配置了多個 executor 的時候,不同分割槽的訊息會均勻的分發到不同的 executor 上消費,那麼訊息的整體順序性就難以保證了,除非將 spout 並行度設為 1 
第三個問題:關於 FilterBolt 的並行度:其取值也有一個簡單辦法,就是在 Storm UI裡面,點開 topology 的首頁,在 Bolts (All time) 下,檢視以下幾個引數的值:

Capacity (last 10m) 取值越小越好,當接近1的時候,說明負載很嚴重,需要增加並行度,正常是在 0.0x 到 0.1 
0.2 左右
Process latency (ms) 單個 tuple 的平均處理時間,越小越好,正常也是 0.0x 
級別;如果很大,可以考慮增加並行度,但主要以 Capacity 為準 

 

一般情況下,按照該 bolt 的程式碼時間複雜度,設定一個 spout 並行度的 1-3倍即可。
第四個問題:AlertBolt 的並行度同 FilterBolt。

第五個問題:shuffleGrouping 會將 tuple 均勻地隨機分發給下游 bolt,一般情況下用它就是最好的了。

總之,要找出並行度的最佳取值,主要結合 Storm UI 來做決策。

4、優化配置引數 
/* tuple傳送失敗重試策略,一般情況下不需要調整 / 
spoutConfig.retryInitialDelayMs = 0; 
spoutConfig.retryDelayMultiplier = 1.0; 
spoutConfig.retryDelayMaxMs = 60 * 1000;

/* 此引數比較重要,可適當調大一點 / 
/* 通常情況下 spout 的發射速度會快於下游的 bolt 的消費速度,當下遊的 bolt 還有 TOPOLOGY_MAX_SPOUT_PENDING 個 tuple 沒有消費完時,spout 會停下來等待,該配置作用於 spout 的每個 task。 / 
conf.put(Config.TOPOLOGY_MAX_SPOUT_PENDING, 10000)

/* 調整分配給每個 worker 的記憶體,關於記憶體的調節,上文已有描述 / 
conf.put(Config.WORKER_HEAP_MEMORY_MB, 768); 
conf.put(Config.TOPOLOGY_WORKER_MAX_HEAP_SIZE_MB, 768);

/* 調整 worker 間通訊相關的緩衝引數,以下是一種推薦的配置 / 
conf.put(Config.TOPOLOGY_RECEIVER_BUFFER_SIZE, 8); // 1.0 以上已移除 
conf.put(Config.TOPOLOGY_TRANSFER_BUFFER_SIZE, 32); 
conf.put(Config.TOPOLOGY_EXECUTOR_RECEIVE_BUFFER_SIZE, 16384); 
conf.put(Config.TOPOLOGY_EXECUTOR_SEND_BUFFER_SIZE, 16384); 
可以在 Storm UI 上檢視當前叢集的 Topology Configuration 
5、rebalance 
可以直接採用 rebalance 命令(也可以在 Storm UI上操作)重新配置 topology 的並行度:

storm rebalance TOPOLOGY-NAME -n 5 -e SPOUT/BOLT1-NAME=3 -e SPOUT/BOLT2-NAME=10

原文:https://blog.csdn.net/qq_36864672/article/details/86068356

相關文章