Storm 性能優化

目錄


  1. 場景假設
  2. 調優步驟和方法
  3. Storm 的部分特性
  4. Storm 并行度
  5. Storm 消息機制
  6. Storm UI 解析
  7. 性能優化

場景假設


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

主機名 硬件配置 角色描述
hd01 2CPUs, 4G RAM, 2TB 機械硬盤 nimbus, supervisor, ui, kafka, zk
hd02 2CPUs, 4G RAM, 2TB 機械硬盤 supervisor, kafka, zk
hd03 2CPUs, 4G RAM, 2TB 機械硬盤 supervisor, kafka, zk

現有一個任務,需要實時計算訂單的各項匯總統計信息。訂單數據通過 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個方面:

  1. 一個 topology 指定多少個 worker 進程并行運行;
  2. 一個 worker 進程指定多少個 executor 線程并行運行;
  3. 一個 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 版本以后被刪除了,該參數指示 receive 線程批量讀取并轉發消息的最大數量)來配置該緩沖區存儲消息的最大數量,默認為8(個數,并且得是2的倍數),然后被推送到 ArrayList 中。receive 線程接收數據,是通過監聽 TCP的端口,該端口有 storm 配置文件中 supervisor.slots.prots 來配置,比如 6700;
  • 一個 sent 線程,該線程維護了一個消息隊列,負責將隊里中的消息發送給其他 worker 的 receive 線程。同樣具有緩沖區,可通過 topology.transfer.buffer.size 來配置緩沖區存儲消息的最大數量,默認為1024(個數,并且得是2的倍數)。該參數指示 sent 線程批量讀取并轉發消息的最大數量。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


    Cluster Summary
參數名 說明
Supervisors 集群中配置的 supervisor 數量
Used slots 集群中已用掉的 workers 數量
Free slots 集群中空閑的 workers 數量
Total slots 集群中總的的 workers 數量
Executors 當前集群中總的 Executor 線程數量,該值等于集群中所有 topology 的所有 spout/bolt 配置的 executor 數量之和,其中默認情況下每個 worker 進程還會派生一個 acker executor 線程,也一并計算在內了
Tasks 當前集群中總的 task 數量,也是所有 executor 派生的 task 數量之和
  • Nimbus Summary
    比較簡單,就略過了

  • Topology 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


    Supervisor Summary

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

  • Nimbus Configuration


    Nimbus Configuration

可搜索和查看當前 topology 的各項配置參數

topology 頁面
  • Topology summary


    Topology summary

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

  • Topology actions


    Topology actions
按鈕 說明
Activate 激活此 topology
Deactivate 暫停此 topology 運行
Rebalance 調整并行度并重新平衡資源
Kill 關閉并刪除此 topology
Debug 調試此 topology 運行,需要設置 topology.eventlogger.executors 數量 > 0
Stop Debug 停止調試
Change Log Level 調整日志級別
  • Topology stats


    Topology stats
參數 說明
Window 時間窗口,比如"10m 0s"表示在topology啟動后10m 0s之內
Emitted 此時間窗口內發射的總tuple數
Transferred 此時間窗口內成功轉移到下一個bolt的tuple數
Complete latency (ms) 此時間窗口內每個tuple在tuple tree中完全處理所花費的平均時間
Acked 此時間窗口內成功處理的tuple數
Failed 此時間窗口內處理失敗或超時的tuple數
  • Spouts (All time)


    Spouts (All time)
參數 說明
Id topologoy 中 spout 的名稱,一般是在代碼里設置的
Executors 當前分配給此 spout 的 executor 線程總數
Tasks 當前分配給此 spout 的 task 線程總數
Emitted 截止當前發射的總tuple數
Transferred 截止當前成功轉移到下一個bolt的tuple數
Complete latency (ms) 截止當前每個tuple在tuple tree中完全處理所花費的平均時間
Acked 截止當前成功處理的tuple數
Failed 截止當前處理失敗或超時的tuple數
  • Bolts (All time)


    Bolts (All time)
參數 說明
Id topologoy 中 bolt 的名稱,一般是在代碼里設置的
Executors 當前分配給此 bolt 的 executor 線程總數
Tasks 當前分配給此 bolt 的 task 線程總數
Emitted 截止當前發射的總tuple數
Transferred 截止當前成功轉移到下一個bolt的tuple數
Complete latency (ms) 截止當前每個tuple在tuple tree中完全處理所花費的平均時間
Capacity (last 10m) 性能指標,取值越小越好,當接近1的時候,說明負載很嚴重,需要增加并行度,正常是在 0.0x 到 0.1 0.2 左右。該值計算方式為 (number executed * average execute latency) / measurement time
Execute latency (ms) 截止當前成功處理的tuple數
Executed 截止當前處理過的tuple數
Process latency (ms) 截止當前單個 tuple 的平均處理時間,越小越好,正常也是 0.0x 級別;如果很大,可以考慮增加并行度,但主要以 Capacity 為準
Acked 截止當前成功處理的tuple數
Failed 截止當前處理失敗或超時的tuple數
spout 頁面

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

  • Executors (All time)


    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 的日志等,當然也可以在命令行上操作。

# 幾個與 debug 相關的命令
bin/storm set_log_level [topology name]-l [logger name]=[LEVEL]:[TIMEOUT]
bin/storm logviewer &

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

參考
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(RedisCli.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());

那么問題是:

  1. setNumWorkers 應該取多少?取決于哪些因素?
  2. kafkaSpout 的并行度應該取多少?取決于哪些因素?
  3. FilterBolt 的并行度應該取多少?取決于哪些因素?
  4. AlertBolt 的并行度應該取多少?取決于哪些因素?
  5. FilterBolt 用 shuffleGrouping 是最好的嗎?
  6. 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數
Paste_Image.png

怎么看這幾個參數呢?有幾個技巧:

  • 正常情況下 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 為準
Paste_Image.png

一般情況下,按照該 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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容

  • 性能優化1:kryo序列化 定制序列化 自定義的bolt之間emit數據是實體類的時候,注冊kryo Storm ...
    尼小摩閱讀 1,191評論 0 3
  • Storm架構 Storm是一個分布式、可靠的實時計算系統。與Hadoop不同的是,它采用流式的消息處理方法,對于...
    零度沸騰_yjz閱讀 2,806評論 0 6
  • 本文借鑒官文,添加了一些解釋和看法,其中有些理解,寫的比較粗糙,有問題的地方希望大家指出。寫這篇文章,是想把一些官...
    達微閱讀 989評論 0 0
  • 一、為什么用Storm storm是一個分布式開源的實時計算系統。可以用來做實時分析、在線機器學習、etl等。 計...
    青芒v5閱讀 786評論 2 5
  • storm的集群提交方式 StormSubmitter.subnitTopology()方法 問題一、如何把sto...
    夙夜M閱讀 534評論 0 0