Twitter Storm源代码分析之Topology的执行过程

如何提交一个topology?

要提交一个topology给storm的话, 我们在命令行里面是这么做的:

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

那么在这个命令的背后,storm集群里面发生了什么呢?

storm里的幕后英雄:nimbus,supervisor

看似简单的topology提交, 其实背后充满着血雨腥风(好吧,我夸张了), 我们来看看我们的幕后英雄nimbus, supervisor都做了什么。

上传topology的代码

首先由Nimbus$IfacebeginFileUploaduploadChunk以及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集群上彻底删除了。

作者: xumingming | 可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明
原文网址: http://xumingming.sinaapp.com/647/twitter-storm-code-analysis-topology-execution/


你可能感兴趣的:(storm,storm,源代码,集群)