Twitter Storm源代码分析之Topology的执行过程
转自: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$Iface的beginFileUpload, uploadChunk以及finishFileUpload方法来把jar包上传到nimbus服务器上的/inbox目录
1 /{storm-local-dir} 2 | 3 |-/nimbus 4 | 5 |-/inbox -- 从nimbus客户端上传的jar包 6 | 会在这个目录里面 7 | 8 |-/stormjar-{uuid}.jar -- 上传的jar包其中{uuid}表示 9 生成的一个uuid
运行topology之前的一些校验
topology的代码上传之后Nimbus$Iface的submitTopology方法会负责对这个topology进行处理, 它首先要对storm本身,以及topology进行一些校验:
- 它要检查storm的状态是否是active的
- 它要检查是否已经有同名的topology已经在storm里面运行了
- 因为我们会在代码里面给spout, bolt指定id, storm会检查是否有两个spout和bolt使用了相同的id。
- 任何一个id都不能以”__”开头, 这种命名方式是系统保留的。
1 (check-storm-active! nimbus storm-name false) 2 3 (defn validate-topology! [topology] 4 (let [bolt-ids (keys (.get_bolts topology)) 5 spout-ids (keys (.get_spouts topology)) 6 state-spout-ids (keys (.get_state_spouts topology)) 7 ; 三种id之间有没有交集? 8 common (any-intersection bolt-ids spout-ids state-spout-ids)] 9 ; 这些id之间是不能有交集的: spout的id和bolt的id是不能一样的 10 (when-not (empty? common) 11 (throw 12 (InvalidTopologyException. 13 (str "Cannot use same component id for both spout and bolt: " 14 (vec common)) 15 ))) 16 ; 用户定义的id不能以__开头, 这些是系统保留的 17 (when-not (every? 18 (complement system-component?) 19 (concat bolt-ids spout-ids state-spout-ids)) 20 (throw 21 (InvalidTopologyException. 22 "Component ids cannot start with '__'"))) 23 ;; TODO: validate that every declared stream is not a system stream 24 ))
如果以上检查都通过了,那么就进入下一步了。
建立topology的本地目录
然后为这个topology建立它的本地目录:
1 /{storm-local-dir} 2 | 3 |-/nimbus 4 | 5 |-/inbox -- 从nimbus客户端上传的jar包 6 | | 会在这个目录里面 7 | | 8 | |-/stormjar-{uuid}.jar -- 上传的jar包其中{uuid}表示 9 | 生成的一个uuid 10 | 11 |-/stormdist 12 | 13 |-/{topology-id} 14 | 15 |-/stormjar.jar -- 包含这个topology所有代码 16 | 的jar包(从nimbus/inbox 17 | 里面挪过来的) 18 | 19 |-/stormcode.ser -- 这个topology对象的序列化 20 | 21 |-/stormconf.ser -- 运行这个topology的配置
对应的代码:
1 (defn- setup-storm-code 2 [conf storm-id tmp-jar-location storm-conf topology] 3 (let [stormroot (master-stormdist-root conf storm-id)] 4 (FileUtils/forceMkdir (File. stormroot)) 5 (FileUtils/cleanDirectory (File. stormroot)) 6 (setup-jar conf tmp-jar-location stormroot) 7 (FileUtils/writeByteArrayToFile 8 (File. (master-stormcode-path stormroot)) 9 (Utils/serialize topology)) 10 (FileUtils/writeByteArrayToFile 11 (File. (master-stormconf-path stormroot)) 12 (Utils/serialize storm-conf)) 13 ))
建立topology在zookeeper上的心跳目录
nimbus老兄是个有责任心的人, 它虽然最终会把任务分成一个个task让supervisor去做, 但是他时刻都在关注着大家的情况, 所以它要求每个task每隔一定时间就要给它打个招呼(心跳信息), 以让它知道事情还在正常发展, 如果有task超时不打招呼, nimbus会认为这个task不行了, 然后进行重新分配。zookeeper上面的心跳目录:
1 |-/taskbeats -- 所有task的心跳 2 | 3 |-/{topology-id} -- 这个目录保存这个topology的所 4 | 有的task的心跳信息 5 | 6 |-/{task-id} -- task的心跳信息,包括心跳的时 7 间,task运行时间以及一些统计 8 信息
计算topology的工作量
nimbus是个精明人, 它对每个topology都会做出详细的预算:需要多少工作量(多少个task)。它是根据topology定义中给的parallelism hint参数, 来给spout/bolt来设定task数目了,并且分配对应的task-id。并且把分配好task的信息写入zookeeper上的/task目录 下:
1 |-/tasks -- 所有的task 2 | 3 |-/{topology-id} -- 这个目录下面id为 4 | {topology-id}的topology 5 | 所对应的所有的task-id 6 | 7 |-/{task-id} -- 这个文件里面保存的是这个 8 task对应的component-id: 9 可能是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。
看代码:
1 (.setup-heartbeats! storm-cluster-state storm-id) 2 (setup-storm-static conf storm-id storm-cluster-state) 3 (defn- setup-storm-static [conf storm-id storm-cluster-state] 4 (doseq [[task-id component-id] (mk-task-component-assignments conf storm-id)] 5 (.set-task! storm-cluster-state storm-id task-id (TaskInfo. component-id)) 6 )) 7 (defn mk-task-maker [max-parallelism parallelism-func id-counter] 8 (fn [[component-id spec]] 9 (let [parallelism (parallelism-func spec) 10 parallelism (if max-parallelism (min parallelism max-parallelism) parallelism) 11 num-tasks (max 1 parallelism)] 12 (for-times num-tasks 13 [(id-counter) component-id]) 14 )))
把计算好的工作分配给supervisor去做
然后nimbus就要给supervisor分配工作了。工作分配的单位是task(上面已经计算好了的,并且已经给每个task编号了), 那么分配工作意思就是把上面定义好的一堆task分配给supervisor来做, 在nimbus里面,Assignment表示一个topology的任务分配信息:
1 (defrecord Assignment [master-code-dir 2 node->host task->node+port task->start-time-secs])
其中核心数据就是task->node+port, 它其实就是从task-id到supervisor-id+port的映射, 也就是把这个task分配给某台机器的某个端口来做。 工作分配信息会被写入zookeeper的如下目录:
1 /-{storm-zk-root} -- storm在zookeeper上的根 2 | 目录 3 | 4 |-/assignments -- topology的任务分配信息 5 | 6 |-/{topology-id} -- 这个下面保存的是每个 7 topology的assignments 8 信息包括: 对应的 9 nimbus上的代码目录,所有 10 task的启动时间, 11 每个task与机器、端口的映射
TODO: 补充工作分配的细节
正式运行topology
到现在为止,任务都分配好了,那么我们可以正式启动这个topology了,在源代码里面,启动topology其实就是向zookeeper上面该topology所对应的目录写入这个topology的信息:
1 |-/storms -- 这个目录保存所有正在运行 2 | 的topology的id 3 | 4 |-/{topology-id} -- 这个文件保存这个topology 5 的一些信息,包括topology的 6 名字,topology开始运行的时 7 间以及这个topology的状态 8 (具体看StormBase类)
看代码:
1 (defn- start-storm 2 [storm-name storm-cluster-state storm-id] 3 (log-message "Activating " storm-name ": " storm-id) 4 (.activate-storm! storm-cluster-state 5 storm-id 6 (StormBase. storm-name 7 (current-time-secs) 8 {:type :active}))) 9 10 (activate-storm! [this storm-id storm-base] 11 ; 把这个topology的信息(StormBase) 12 ; 写入/storms/{topology-id}这个文件 13 (set-data cluster-state (storm-path storm-id) 14 (Utils/serialize storm-base)) 15 )
好!nimbus干的不错,到这里为止nimbus的工作算是差不多完成了,它对topology进行了一些检查,发现没什么问 题, 然后又评估了一下工作量, 然后再看看它的小弟们(supervisor)哪些有空,它进行了合理的分配,所有的事情都安排妥当了,nimbus终于可以松一口气了。下面就看 supervisor的了。
Supervisor领任务
我们的supervisor同志无时无刻不想着为大哥nimbus分忧, 它每隔几秒钟就去看看大哥有没有给它分配新的任务,这些逻辑主要在supervisor.clj里面的synchronize-supervisor和sync-processes两个方法里面它:
- 首先它看看storm里面有没有新提交的它没有下载的topology的代码, 如果有的话, 它就把这个新topology的代码下载下来。它可不管这个topology由不由它负责哦(这一点是可以优化的)
1 (doseq [[storm-id master-code-dir] storm-code-map] 2 (when-not (downloaded-storm-ids storm-id) 3 (log-message 4 "Downloading code for storm id " 5 storm-id 6 " from " 7 master-code-dir) 8 ; 从nimbus上下载这个topology的代码 9 (download-storm-code conf storm-id 10 master-code-dir) 11 (log-message 12 "Finished downloading code for storm id " 13 storm-id 14 " from " 15 master-code-dir) 16 ))
- 然后它会删除那些已经不再运行的topology的代码
1 (doseq [storm-id downloaded-storm-ids] 2 (when-not (assigned-storm-ids storm-id) 3 (log-message "Removing code for storm id " 4 storm-id) 5 (rmr (supervisor-stormdist-root conf storm-id)) 6 ))
- 然后他根据老大哥nimbus给它指派的任务信息(task-id对应到的topology的spout或者bolt), 来让它自己的小弟:worker来做这个事情
1 (dofor [[port assignment] reassign-tasks] 2 (let [id (new-worker-ids port)] 3 (log-message "Launching worker with assignment " 4 (pr-str assignment) 5 " for this supervisor " 6 supervisor-id 7 " on port " 8 port 9 " with id " 10 id 11 ) 12 ; 启动一个worker(supervisor+port) 13 ; 来处理assignments 14 (launch-worker conf 15 shared-context 16 (:storm-id assignment) 17 supervisor-id 18 port 19 id 20 worker-thread-pids-atom) 21 id))
Worker执行
worker是个苦命的人, 上面的nimbus, supervisor只会指手画脚, 它要来做所有的脏活累活。
1. 它首先去zookeeper上去看看老大哥们都给他分配了哪些task(task-ids)
1 (defn read-worker-task-ids 2 [storm-cluster-state storm-id supervisor-id port] 3 (let [assignment 4 (:task->node+port 5 (.assignment-info 6 storm-cluster-state storm-id nil))] 7 (doall 8 (mapcat (fn [[task-id loc]] 9 ; 找出这个worker(supervisor+port)的tasks 10 (if (= loc [supervisor-id port]) 11 [task-id] 12 )) 13 assignment)) 14 ))
2. 然后根据这些task-id来找出所对应的topology的spout/bolt
1 task->component (storm-task-info 2 storm-cluster-state storm-id)
3 . 计算出它所代表的这些spout/bolt会给哪些task发送消息
1 ; task-ids是这个worker所负责的那些task, 那么 2 ; worker-outbound-tasks函数的结果就是这些task 3 ; 的消息要发送的task(supervisor+port) 4 outbound-tasks (worker-outbound-tasks 5 task->component mk-topology-context task-ids)
4. 建立到3里面所提到的那些task的连接(socket), 然后在需要发送消息的时候就通过这些socket来发送
1 (swap! node+port->socket 2 merge 3 (into {} 4 (dofor [[node port :as endpoint] new-connections] 5 [endpoint 6 ; msg/connect函数返回的就是从这个worker的端口 7 ; 到目的地主机、端口的socket 8 (msg/connect 9 mq-context 10 ((:node->host assignment) node) 11 port) 12 ] 13 )))
到这里为止,topology里面的组件(spout/bolt)都根据parallelism被分成多个task, 而这些task被分配给supervisor的多个worker来执行。大家各司其职,整个topology已经运行起来了。
Topology的终止
除非你显式地终止一个topology, 否则它会一直运行的,可以用下面的命令去终止一个topology:
1 storm kill {stormname}
在这个命令的背后, storm-cluster-state的remove-storm!命令会被调用:
1 (remove-storm! [this storm-id] 2 (delete-node cluster-state (storm-task-root storm-id)) 3 (delete-node cluster-state (assignment-path storm-id)) 4 (remove-storm-base! this storm-id))
上面的代码会把zookeeper上面/tasks, /assignments, /storms下面有关这个topology的数据都删除了。这些数据(或者目录)之前都是nimbus创建的。还剩下/taskbeats以及/taskerrors下的数据没有清除, 这块数据会在supervisor下次从zookeeper上同步数据的时候删除的(supervisor会删除那些已经不存在的topology相关的数据)。这样这个topology的数据就从storm集群上彻底删除了。

浙公网安备 33010602011771号