【流式計算】Twitter Storm源代碼分析之Topology的執行過程

作者: xumingming | 可以轉載, 但必須以超鏈接形式標明文章原始出處和作者信息及版權聲明
網址: http://xumingming.sinaapp.com/647/twitter-storm-code-analysis-topology-execution/

 
我們通過前面的文章(Twitter Storm源代碼分析之ZooKeeper中的目錄結構)知道了storm集羣裏面nimbus是通過zookeeper來給supervisor發送指令的,並且知道了通過zookeeper到底交換了哪些信息。 那麼一個topology從提交到執行到底是個什麼樣的過程?nimbus和supervisor到底做了什麼樣的事情呢?本文將帶你去探尋這些答案。

代碼列表

如何提交一個topology?

要提交一個topology給storm的話, 我們在命令行裏面是這麼做的:

1
storm jar allmycode.jar org.me.MyTopology arg1 arg2 arg3

那麼在這個命令的背後,storm集羣裏面發生了什麼呢?

storm裏的幕後英雄:nimbus,supervisor

看似簡單的topology提交, 其實背後充滿着血雨腥風(好吧,我誇張了), 我們來看看我們的幕後英雄nimbus, supervisor都做了什麼。

上傳topology的代碼

首先由Nimbus$IfacebeginFileUpload, uploadChunk以及finishFileUpload方法來把jar包上傳到nimbus服務器上的/inbox目錄

1
2
3
4
5
6
7
8
9
/{storm-local-dir}
  |
  |-/nimbus
     |
     |-/inbox                   -- 從nimbus客戶端上傳的jar包
        |                            會在這個目錄裏面
        |
        |-/stormjar-{uuid}.jar  -- 上傳的jar包其中{uuid}表示
                                     生成的一個uuid

運行topology之前的一些校驗

topology的代碼上傳之後Nimbus$IfacesubmitTopology方法會負責對這個topology進行處理, 它首先要對storm本身,以及topology進行一些校驗:

  • 它要檢查storm的狀態是否是active的
  • 它要檢查是否已經有同名的topology已經在storm裏面運行了
  • 因爲我們會在代碼裏面給spout, bolt指定id, storm會檢查是否有兩個spout和bolt使用了相同的id。
  • 任何一個id都不能以”__”開頭, 這種命名方式是系統保留的。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(check-storm-active! nimbus storm-name false)
 
(defn validate-topology! [topology]
  (let [bolt-ids (keys (.get_bolts topology))
        spout-ids (keys (.get_spouts topology))
        state-spout-ids (keys (.get_state_spouts topology))
        ; 三種id之間有沒有交集?
        common (any-intersection bolt-ids spout-ids state-spout-ids)]
    ; 這些id之間是不能有交集的: spout的id和bolt的id是不能一樣的
    (when-not (empty? common)
      (throw
       (InvalidTopologyException.
        (str "Cannot use same component id for both spout and bolt: "
                (vec common))
        )))
    ; 用戶定義的id不能以__開頭, 這些是系統保留的
    (when-not (every?
                    (complement system-component?)
                   (concat bolt-ids spout-ids state-spout-ids))
      (throw
       (InvalidTopologyException.
        "Component ids cannot start with '__'")))
    ;; TODO: validate that every declared stream is not a system stream
    ))

如果以上檢查都通過了,那麼就進入下一步了。

建立topology的本地目錄

然後爲這個topology建立它的本地目錄:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
/{storm-local-dir}
  |
  |-/nimbus
      |
      |-/inbox                  -- 從nimbus客戶端上傳的jar包
      |  |                            會在這個目錄裏面
      |  |
      |  |-/stormjar-{uuid}.jar -- 上傳的jar包其中{uuid}表示
      |                               生成的一個uuid
      |
      |-/stormdist
         |
         |-/{topology-id}
            |
            |-/stormjar.jar     -- 包含這個topology所有代碼
            |                       的jar包(從nimbus/inbox
            |                       裏面挪過來的)
            |
            |-/stormcode.ser    -- 這個topology對象的序列化
            |
            |-/stormconf.ser    -- 運行這個topology的配置

對應的代碼:

01
02
03
04
05
06
07
08
09
10
11
12
13
(defn- setup-storm-code
  [conf storm-id tmp-jar-location storm-conf topology]
  (let [stormroot (master-stormdist-root conf storm-id)]
   (FileUtils/forceMkdir (File. stormroot))
   (FileUtils/cleanDirectory (File. stormroot))
   (setup-jar conf tmp-jar-location stormroot)
   (FileUtils/writeByteArrayToFile
      (File. (master-stormcode-path stormroot))
      (Utils/serialize topology))
   (FileUtils/writeByteArrayToFile
      (File. (master-stormconf-path stormroot))
      (Utils/serialize storm-conf))
   ))

建立topology在zookeeper上的心跳目錄

nimbus老兄是個有責任心的人, 它雖然最終會把任務分成一個個task讓supervisor去做, 但是他時刻都在關注着大家的情況, 所以它要求每個task每隔一定時間就要給它打個招呼(心跳信息), 以讓它知道事情還在正常發展, 如果有task超時不打招呼, nimbus會認爲這個task不行了, 然後進行重新分配。zookeeper上面的心跳目錄:

1
2
3
4
5
6
7
8
|-/taskbeats              -- 所有task的心跳
    |
    |-/{topology-id}      -- 這個目錄保存這個topology的所
        |                    有的task的心跳信息
        |
        |-/{task-id}      -- task的心跳信息,包括心跳的時
                             間,task運行時間以及一些統計
                             信息

計算topology的工作量

nimbus是個精明人, 它對每個topology都會做出詳細的預算:需要多少工作量(多少個task)。它是根據topology定義中給的parallelism hint參數, 來給spout/bolt來設定task數目了,並且分配對應的task-id。並且把分配好task的信息寫入zookeeper上的/task目錄下:

1
2
3
4
5
6
7
8
9
|-/tasks                  -- 所有的task
    |
    |-/{topology-id}      -- 這個目錄下面id爲
        |                    {topology-id}的topology
        |                    所對應的所有的task-id
        |
        |-/{task-id}      -- 這個文件裏面保存的是這個
                             task對應的component-id:
                             可能是spout-id或者bolt-id

從上圖中註釋中看到{task-id}這個文件裏面存儲的是它所代表的spout/bolt的id, 這其實就是一個細化工作量的過程。
打比方說我們的topology裏面一共有一個spout, 一個bolt。 其中spout的parallelism是2, bolt的parallelism是4, 那麼我們可以把這個topology的總工作量看成是6, 那麼一共有6個task,那麼/tasks/{topology-id}下面一共會有6個以task-id命名的文件,其中兩個文件的內容是spout的id, 其它四個文件的內容是bolt的id。

看代碼:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
(.setup-heartbeats! storm-cluster-state storm-id)
(setup-storm-static conf storm-id storm-cluster-state)
(defn- setup-storm-static [conf storm-id storm-cluster-state]
  (doseq [[task-id component-id] (mk-task-component-assignments conf storm-id)]
    (.set-task! storm-cluster-state storm-id task-id (TaskInfo. component-id))
    ))
(defn mk-task-maker [max-parallelism parallelism-func id-counter]
  (fn [[component-id spec]]
    (let [parallelism (parallelism-func spec)
          parallelism (if max-parallelism (min parallelism max-parallelism) parallelism)
          num-tasks (max 1 parallelism)]
      (for-times num-tasks
                 [(id-counter) component-id])
      )))

把計算好的工作分配給supervisor去做

然後nimbus就要給supervisor分配工作了。工作分配的單位是task(上面已經計算好了的,並且已經給每個task編號了), 那麼分配工作意思就是把上面定義好的一堆task分配給supervisor來做, 在nimbus裏面,Assignment表示一個topology的任務分配信息:

1
2
(defrecord Assignment [master-code-dir
    node->host task->node+port task->start-time-secs])

其中核心數據就是task->node+port, 它其實就是從task-id到supervisor-id+port的映射, 也就是把這個task分配給某臺機器的某個端口來做。 工作分配信息會被寫入zookeeper的如下目錄:

01
02
03
04
05
06
07
08
09
10
11
/-{storm-zk-root}           -- storm在zookeeper上的根
  |                            目錄
  |
  |-/assignments            -- topology的任務分配信息
      |
      |-/{topology-id}      -- 這個下面保存的是每個
                               topology的assignments
                               信息包括: 對應的
                               nimbus上的代碼目錄,所有
                               task的啓動時間,
                               每個task與機器、端口的映射

TODO: 補充工作分配的細節

正式運行topology

到現在爲止,任務都分配好了,那麼我們可以正式啓動這個topology了,在源代碼裏面,啓動topology其實就是向zookeeper上面該topology所對應的目錄寫入這個topology的信息:

1
2
3
4
5
6
7
8
|-/storms                 -- 這個目錄保存所有正在運行
    |                        的topology的id
    |
    |-/{topology-id}      -- 這個文件保存這個topology
                             的一些信息,包括topology的
                             名字,topology開始運行的時
                             間以及這個topology的狀態
                             (具體看StormBase類)

看代碼:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
(defn- start-storm
  [storm-name storm-cluster-state storm-id]
  (log-message "Activating " storm-name ": " storm-id)
  (.activate-storm! storm-cluster-state
                    storm-id
                    (StormBase. storm-name
                                (current-time-secs)
                                {:type :active})))
 
(activate-storm! [this storm-id storm-base]
  ; 把這個topology的信息(StormBase)
  ; 寫入/storms/{topology-id}這個文件
  (set-data cluster-state (storm-path storm-id)
    (Utils/serialize storm-base))
  )

好!nimbus乾的不錯,到這裏爲止nimbus的工作算是差不多完成了,它對topology進行了一些檢查,發現沒什麼問題, 然後又評估了一下工作量, 然後再看看它的小弟們(supervisor)哪些有空,它進行了合理的分配,所有的事情都安排妥當了,nimbus終於可以鬆一口氣了。下面就看supervisor的了。

Supervisor領任務

我們的supervisor同志無時無刻不想着爲大哥nimbus分憂, 它每隔幾秒鐘就去看看大哥有沒有給它分配新的任務,這些邏輯主要在supervisor.clj裏面的synchronize-supervisorsync-processes兩個方法裏面它:

  • 首先它看看storm裏面有沒有新提交的它沒有下載的topology的代碼, 如果有的話, 它就把這個新topology的代碼下載下來。它可不管這個topology由不由它負責哦(這一點是可以優化的)
    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    (doseq [[storm-id master-code-dir] storm-code-map]
     (when-not (downloaded-storm-ids storm-id)
       (log-message
          "Downloading code for storm id "
          storm-id
          " from "
          master-code-dir)
       ; 從nimbus上下載這個topology的代碼
       (download-storm-code conf storm-id
            master-code-dir)
       (log-message
          "Finished downloading code for storm id "
          storm-id
          " from "
          master-code-dir)
       ))
  • 然後它會刪除那些已經不再運行的topology的代碼
    1
    2
    3
    4
    5
    6
    (doseq [storm-id downloaded-storm-ids]
     (when-not (assigned-storm-ids storm-id)
       (log-message "Removing code for storm id "
                    storm-id)
       (rmr (supervisor-stormdist-root conf storm-id))
       ))
  • 然後他根據老大哥nimbus給它指派的任務信息(task-id對應到的topology的spout或者bolt), 來讓它自己的小弟:worker來做這個事情
    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    (dofor [[port assignment] reassign-tasks]
      (let [id (new-worker-ids port)]
        (log-message "Launching worker with assignment "
                     (pr-str assignment)
                     " for this supervisor "
                     supervisor-id
                     " on port "
                     port
                     " with id "
                     id
                     )
        ; 啓動一個worker(supervisor+port)
        ; 來處理assignments
        (launch-worker conf
                       shared-context
                       (:storm-id assignment)
                       supervisor-id
                       port
                       id
                       worker-thread-pids-atom)
        id))

Worker執行

worker是個苦命的人, 上面的nimbus, supervisor只會指手畫腳, 它要來做所有的髒活累活。

  • 1. 它首先去zookeeper上去看看老大哥們都給他分配了哪些task(task-ids)
    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    (defn read-worker-task-ids
      [storm-cluster-state storm-id supervisor-id port]
      (let [assignment
        (:task->node+port
            (.assignment-info
                storm-cluster-state storm-id nil))]
        (doall
          (mapcat (fn [[task-id loc]]
                  ; 找出這個worker(supervisor+port)的tasks
                  (if (= loc [supervisor-id port])
                    [task-id]
                    ))
                assignment))
        ))
  • 2. 然後根據這些task-id來找出所對應的topology的spout/bolt
    1
    2
    task->component (storm-task-info
        storm-cluster-state storm-id)
  • 3. 計算出它所代表的這些spout/bolt會給哪些task發送消息
    1
    2
    3
    4
    5
    ; task-ids是這個worker所負責的那些task, 那麼
    ; worker-outbound-tasks函數的結果就是這些task
    ; 的消息要發送的task(supervisor+port)
    outbound-tasks (worker-outbound-tasks
        task->component mk-topology-context task-ids)
  • 4. 建立到3裏面所提到的那些task的連接(socket), 然後在需要發送消息的時候就通過這些socket來發送
    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    (swap! node+port->socket
     merge
     (into {}
       (dofor [[node port :as endpoint] new-connections]
         [endpoint
          ; msg/connect函數返回的就是從這個worker的端口
          ; 到目的地主機、端口的socket
          (msg/connect
           mq-context
           ((:node->host assignment) node)
           port)
          ]
         )))


到這裏爲止,topology裏面的組件(spout/bolt)都根據parallelism被分成多個task, 而這些task被分配給supervisor的多個worker來執行。大家各司其職,整個topology已經運行起來了。

Topology的終止

除非你顯式地終止一個topology, 否則它會一直運行的,可以用下面的命令去終止一個topology:

1
storm kill {stormname}

在這個命令的背後, storm-cluster-stateremove-storm!命令會被調用:

1
2
3
4
(remove-storm! [this storm-id]
  (delete-node cluster-state (storm-task-root storm-id))
  (delete-node cluster-state (assignment-path storm-id))
  (remove-storm-base! this storm-id))

上面的代碼會把zookeeper上面/tasks, /assignments, /storms下面有關這個topology的數據都刪除了。這些數據(或者目錄)之前都是nimbus創建的。還剩下/taskbeats以及/taskerrors下的數據沒有清除, 這塊數據會在supervisor下次從zookeeper上同步數據的時候刪除的(supervisor會刪除那些已經不存在的topology相關的數據)。這樣這個topology的數據就從storm集羣上徹底刪除了。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章