Twitter Strom的源码分析(version 0.7)

Twitter Storm

Storm的主线主要包括4条:nimbus, supervisor, worker和task。

1. Nimbus

要了解nimbus的具体做的操作,可以从提交一个topology的流程开始。

1.1  Prepare

Nimbus启动时候,运行了一个ThriftServer。它会在topology提交之前做以下四个工作。

(1)       清理一些中断了的topology(nimbus目录下/storm.local.dir/stormdist下存在,zk中            storms/topologyid中不存在的topology):删除ZK上相关信息(清理tasks/topologyid; storms/topologyid; assignments/topologyid这个三个目录)。

(2)      将storms/下所有的topology设置为启动状态: 能转换成startup状态的两种状态分别是:killed和rebalancing。nimbus的状态转换是很有意思的事情,killed状态的topology在nimbus启动的时候会被干掉;rebalancing状态的topology在nimbus启动的时候会重新分发任务,状态会变成rebalancing的上一个状态。

举例: 当某个topology提交的时候,会被设置成active状态,假设storm集群增加机器了,你希望重新分发任务,可以将状态转换成rebalance状态,转换成这个状态要做这几件事:

首先,启动一个延迟TOPOLOGY-MESSAGE-TIMEOUT-SECS秒执行的事件,将当前状态转换成do-rebalance状态,在这之前会将当前topology的状态设置成rebalancing状态(注意设置和转换的区别,设置就是指将ZK上存储的topology的状态进行重新设置)。

然后,将rebalancing的状态转换成do-rebalance,也就是将任务重新分发。

最后将状态设置成rebalancing的上一个状态active。

(3)       每间隔NIMBUS-MONITOR-FREQ-SECS长时间将ZK上/storms 下所有的topology状态转换成monitor状态,并且将不活跃的storm清理掉。只有当状态为activeinactive的时候,才能转换成monitor状态,转换成该状态就是将任务重新分发,监控是否与上一次的分配情况不同,如果存在不同,则替换,这个过程ZK上存储的topology的状态是不会被设置的。

(4)       删除过期的jar包: 过期时间为NIMBUS-INBOX-JAR-EXPIRATION-SECS。每间隔NIMBUS-CLEANUP-INBOX-FREQ-SECS长时间进行一次清理。

1.2   Submit topology

1.2.1  Submit

以下是提交一个topology的执行流程概述

(1)      检查:检查所提交的topology是否已经是active状态,如果是,抛出异常。

(2)      计数器:计数器+1,用来计算共提交了多少topology。

(3)      为这个topology生一个id。

(4)      计算要启动的acker的相关信息,每个acker都是一个bolt。根据配置的信息获取并发数等。

(5)      获取topology的code和config。

(6)      为该topology建立心跳目录(ZK:storm-zk-root/taskbeats/topologyid)。

(7)      为该topology分配任务。

(8)      设置storm状态为active。

1.2.2  Task assigned

这个是nimbus的重点,分配任务的流程:

(1)       获取所有的node+port:分配任务的目的就是将tasks分配到具体的node+port。

(2)       获取该topology已经分配了的任务(如果是新提交的topology,则为空)。

(3)       为每个需要分配的task分配到具体node+port。合并(1)和(2)中所有的node和port。具体为什么要这样,还不是很清楚。因为通过(1)应该是可以获取所有的node+port。

(4)       将2)中需要重新分配的task的启动时间不变,其他新分配的任务的时间设置为当前时间。

(5)       将以上的信息Assignment写入到ZK中。

1.2.3  scratch?

分配任务主要分为两种情况,代码中用scratch?来标识两种不同的情况(正常的分配任务和rebalance状态转换)。1.2.2中所说的需要分配的任务暗指此处的两种任务分配情况。

(1)    rebalance状态转换

这种原因导致的重新分发任务,与正常的分配任务的区别在于两点:

第一:所有的任务都认为是活跃任务,即认为任务心跳都未超时。

第二:所有的任务都需要重新分配,即认为所有端口都是空闲的端口,允许被分配任务。

举例说明:

假设:提交的topology确定有10个task, taskid分别为1,2,3,…10。有两个supervisor(n

ode1和node2),每个supervisor可用的端口号为{6001, 6002, 6003}。

 

分配任务的目标是尽可能负载均衡,即每个node均衡,每个端口均衡, rebalance情况

下分配的结果为:

tasked

node+port

1、7

node1:6001

3、9

node1:6002

5

node1:6003

2、8

node2:6001

4、10

node2:6002

6

node2:6003


 

(2)    正常分配任务:

正常分配任务比rebalance多考虑两个问题,考虑task的心跳是否超时和哪些端口上分配的任务是符合负载均衡的,可以不需要重新分发。所以要重新分发的task是那些心跳超时和所在端口没有分配均衡的那些任务。

 

nimbus用了一个叫task-heartbeat-cache来存储task的心跳时间,而不是直接用写入到zk上的心跳时间,这么做的目的是防止各个node上面的时间不同步。

 

举例说明:

假设提交的topology,跟上个例子相同。确定有10个task, taskid分别为1,2,3,…10。有两个supervisor(node1和node2),每个supervisor可用的端口号为{6001, 6002, 6003}。

首先,重新分配任务时,会先判断心跳超时的task,假设task1和task6心跳超时,task1和task6需要重新分配。

然后,判断哪些端口的分配是符合负载均衡的要求。判断过程是这样的:共10个task,6个端口,按照负载均衡原则,应该是4个端口上分配两个task,2个端口上分配1个task。依次判断哪些端口不符合要求。如果按顺序来判断,node1:6001和node2:6002是不符合要求的,他们应该要被分别分配2个任务。所以两个端口的任务也需要重新分发。

最后,确定要重新分发的任务是1、4、7和10,可用的端口号是node1:6001和node2:6002。

按照每个node均衡,每个端口均衡的分配原则,分配结果如下:

tasked

node+port

1、7

node1:6001

4、10

node2:6002

 

 

 

 

 

2. Supervisor

Supervisor主要做了以下的事情。

2.1  HeartBeats

心跳之前会将supervisorid写入到本地。

Supervisor将自己的心跳信息(SupervisorInfo[心跳时间, 主机名, 端口号和运行时间])以SUPERVISOR-HEARTBEAT-FREQUENCY-SECS的频率写入到zk中。Nimbus会读zk上的心跳信息,以确定集群的supervisor的状态(目前还没实现)

Supervisor启动的时候会执行一次心跳写入操作,然后会启动一个线程每隔SUPERVISOR-HEARTBEAT-FREQUENCY-SECS秒就写入一次心跳信息。

2.2  Synchronizesupervisor

启动一个线程,每隔10s执行一次这个操作。参数不可通过配置文件配置。主要包括的操作如下:

(1)      下载需要下载却未下载的topology资源(代码, jar包和配置文件):某个topology的任务分配到了该supervisor,则下载topology的资源到指定的目录(先下载到临时目录,然后把jar包进行解压复制到最终目录,删除临时目录)。

(2)      删除无效的topology资源,这些资源是指supervisor本地的从nimbus下载下来的topology资源,且这些topology没有被nimbus分配任务。

(3)       将要分发到supervisor的任务存入到localstate(本地/storm-zk-root/supervisor/localstate)中(key为LS-LOCAL-ASSIGNMENTS)。

(4)       sync-processes

2.3  Sync-processes

启动一个线程,每隔SUPERVISOR_MONITOR_FREQUENCY_SECS秒执行以下操作

(1)       关闭本地无效的worker:通过获取worker心跳(WorkerHeartbeat)获取worker的状态,对状态不为:valid的worker的进程kill掉。

(2)      根据本地/storm-zk-root/supervisor/localstate下的信息判断哪些新的worker是需要启动的,哪些启动了的worker是可以保留的。将这些需要启动的worker建立相关的本地目录,将所有启动了的和将要启动的worker的ID和port存入localstate中。并启动要启动的worker。

2.4  Shutdownable、 SupervisorDaemon and DaemonCommon

(1)      关闭该supervisor:关闭supervisor后,它的worker和task相关的进程和线程不会被关闭。

(2)      可以获取配置文件,获取supervisorid和关闭所有的worker

(3)      判断supervisor是否处于等待状态

3. Worker

Worker主要完成了三件事情:

3.1  HeartBeats

Worker的心跳(WorkerHeartbeat)是写在本地的,路径是/STORM-LOCAL-DIR/workers/workerid/。

public class WorkerHeartbeat implements Serializable{

              private int timeSecs;

              private StringstormId;

              private Set<Integer>taskIds;

private Integer port;

}

timeSecs:写入心跳的时间

stormId: stormId就是topologyId,怎么叫都行。

taskIds:每个worker对应于一个node下的一个port,就是某台机子上的某个端口

port:就是该Worker(端口)下所有要启动的任务。

 

Worker启动的时候,会从ZK获得哪些task是自己需要启动的,然后写入一次心跳到/STORM-LOCAL-DIR/workers/workerid/中。Worker还会启动一个线程,这个线程每隔WORKER-HEARTBEAT-FREQUENCY-SECS时间往/STORM-LOCAL-DIR/workers/workerid/上写入一次心跳信息。

监控Worker的心跳是由supervisor来做的,具体参考supervisor。

3.2   Refresh

3.2.1  Refresh connection

这个跟nimbus有关系,比如说:某种原因导致任务要重新分配,重新分配后,Worker下的任务发生变化了,所以需要一个线程来监控这个事件。

 

Worker会找到他要启动的任务都会发送消息到其他哪些任务中,这些任务都由那些Worker启动,会利用zeromq逐一建立对每个worker建立socket连接。当nimbus重新分配任务了,Worker会去重新读分配给她的任务是什么,然后判断那些任务是新的,那些任务是不要的,根据此来断开不需要连接的socket,连接需要连接却未连接的socket。

 

启动Worker的时候会执行一次以上过程,然后会启动一个线程,每隔TASK-REFRESH-POLL-SECS时间往事件管理器中添加refresh connection事件,事件管理器会每隔TASK-REFRESH-POLL-SECS时间执行该事件。除了以上时刻会执行外,Worker把refresh connection操作作为从ZK上读取分配任务的时候的watcher事件。也就是说一旦重新分配了任务,就会执行该事件。个人认为这么做的目的是为了双保险。其实不这么做也是可以的。

3.2.2  Refresh storm active

一个worker首先只能属于一个stormidRefresh storm active目的是为了监控Worker所属的storm(topology)的状态,都有哪些状态呢,详细参考nimbus主线。Worker要做的事情是判断该storm(topology)的状态是否是active。判断这个有啥用,给task用的,task怎么用,参考task。

 

启动Worker的时候会执行一次以上的过程,然后启动一个线程,每隔TASK-REFRESH-POLL-SECS时间往事件管理器中添加refresh storm active的操作,事件管理器会每隔TASK-REFRESH-POLL-SECS时间执行该事件。

 

除了以上时候会执行外,Worker把refreshstorm active的操作作为从ZK上读取/storms/topologyid上的topology信息(StormBase)时候的watcher事件。即topology的信息一旦发生变化,就会执行该操作。个人认为这么做的目的也是为了双保险。

 

3.3  Sendmessage and Transfer message

3.3.1 Difference

Storm的消息收发是通过zeroMQ来完成的。流程如下:

(1)      task发送消息给所属的worker:是通过一个队列实现的,每个worker产生一个队列,task往队列里写消息,消息的内容包括要发送到的taskid和所要发送的消息。

(2)      worker从队列里取出消息通过zeroMQ发送到目的worker。

(3)      worker接收通过zeroMQ发送的消息后,根据消息的taskid转发到目标task。

 整个消息通信的过程表明:task和task之间的消息通信是通过worker来完成的。Sendmessage就是指过程(3), transfer message是指(1)和(2)。

 3.3.2 Implements

(1)      首先Worker启动一个线程,该线程循环的从Worker的队列中取出消息,将消息+目标taskid发送到目的worker。

(2)      Worker再启动一个线程(msg-loader/launch-virtual-port!),循环将收到的通过zeroMQ得到的消息分解:(taskid+msg),建立与虚拟端口的连接(zeroMQ中同一进程内的多线程之间的通信"inproc://" + virtualport)。Virtualport此处等于taskid。然后将消息msg发送到指定的task。

测试worker的时候,发现连接虚拟端口的时候,这个虚拟端口需要先被绑定。必须用同一进程的zero context。

 

4. Task

task由worker启动,由nimbus分配。

4.1  HeartBeats

task的心跳信息(TaskHeartbeat)由nimbus来监测,心跳信息写入到zk(/storm-zk-root/taskbeats/topologyid/taskid )中。

Task会启动一个线程,每间隔TASK-HEARTBEAT-FREQUENCY-SECS秒写入一次心跳信息。

BaseStatsData包括以下信息:

Type表示该任务是属于bolt还是spout

TaskStats 的主要内容为:

emitted和transferred存储的内容为:<the time window, <streamid, count/average>

union TaskSpecificStats {

       BoltStats bolt;

       SpoutStats spout;

}

struct BoltStats {

       map<string, map<GlobalStreamId,i64>> acked; 

       map<string, map<GlobalStreamId,i64>> failed; 

       map<string, map<GlobalStreamId,double>> process_ms_avg;

}

struct GlobalStreamId {

        string componentId;

        string streamId;

    }

struct SpoutStats {

       map<string, map<string, i64>>acked;

       map<string, map<string, i64>>failed;

       map<string, map<string,double>> complete_ms_avg;

}

4.2  Transfermessage

Task最主要的工作就是往所属worker的队列中发送消息,消息的内容是[taskid, tuple],taskid是消息要发送的目的task。要完成发送需要做到两点:第一,确定要发送的目标task;第二,保证消息的可靠性(acker)。

(1)    确定目标task

Task通过分组来确定目标task。分组的方式由以下几种:

a.        按字段分组:通过对字段(提交topology的时候设置)hash实现,具有相同字段的值会分发到相同的task中,但是不同字段的值不一定会分发到不同的task中。

b.        随机分组:这种随机分组其实不是完全随机的,从代码来看可以看作是随机均匀分组,它会尽可能的保证每个目标task上的负载均匀,也就说让目标bolt中的每个task处理tuple的数量尽可能的相同。

c.        none分组:和随机分组的差别不大,但是这种是纯粹的随机分组。

d.        直接分组:这种分组就是指定taskid,发送消息的时候用的方法也必须使用emitDirect来发射。

e.        All: 全局发送

f.         自定义分组:自己定义分组方式

(2)    消息的可靠性acker

Task是通过acker来保证消息的可靠发送的,流程如下:

1)      首先spout会发送一条消息[val, taskid], 一个新的tuple产生时val为tupleid^0;

2)        然后bolt接收消息转发后,进行ack,会将接收的tupleid和产生的所有tupleid都与val异或;

3)        最后,acker(一个acker就是一个bolt,可以设置acker的并发数)会一直检测val什么时候变成0。变成0表示这个tuple发送完毕。

 

举例说明:

假设topology的结构是:spout发送消息到bolt1,然后bolt1发送消息分别到bolt11和bolt12,然后bolt11和bolt12将消息做一些处理后写入到KV中。

1)        spout中的一个task(假设taskid为1),发送了tuple(假设tupleid为1)到bolt1中的一个task(假设taskid为2)

此时:acker会收到一条消息[0^1,1], 也就是[val, taskid];

2)        task2接收到tuple1后,生成两个tuple(假设id分别为11和12),分别发送给bolt11中的task(假设taskid为3)和bolt12中的task(假设taskid为4)。

此时:task2 ack的时候会将接收到的tuple和新产生的tuple进行异或,val这时候的值是0^1^1^11^12

3)        task3接收到消息后写入到KV中

此时: task3 ack的时候,val的值为0^1^1^11^12^11

4)        task4接收到消息后

此时: task4 ack的时候,val的值为0^1^1^11^12^11^12=0

4.3  Implements

task的整体执行流程

1)        启动一个线程定时写入task心跳。

2)        再启动一个线程,循环执行spout或者bolt, task会判断自己是bolt的task还是spout的task。

Spout

a.        定义一个TimeCacheMap,用来将消息超时(TOPOLOGY-MESSAGE-TIMEOUT-SECS)的tuple发送给acker一个消息,表示发送失败。前提是发送消息的时候要指定message-id并且ack的并发数大于0(TOPOLOGY-ACKERS>0),否则不进行acker操作。

b.        发送消息之前会确定要输出到哪个taskid,这个是由分组的方式决定的。

c.        循环的执行spout的nextTuple函数,这里要提到worker中的一个操作,refresh storm active,此处如果发现topology的状态不是active的时候, 会持续等待,不会执行nextTuple, 除非topology的状态变为active. nextTuple里一般调用ISpoutOutputCollector中的emit方法,该方法会将消息和要发送的目标taskid写入到所属worker的队列中。

d.        循环的将收到的消息发送给acker。Spout的task接收的消息是指acker发送的消息,acker会将tuple的acker结果发送给消息的发起者spout task,这个发起者会调用spout的ack方法或者fail方法。

Bolt

a.        发送消息之前会确认要发送到哪个taskid,同样由分组的方式决定的。

b.        task会循环的将收到的tuple信息传给bolt的execute方法,循环执行execute方法,execute一般会调用IOutputCollector中的emit方法,该方法会将产生的新的tupleid异或的结果存起来,将tuple和要发送的目标taskid写入到所属worker的队列中。如果调用ack方法,则会将新的tupleid异或的结果和收到的tupleid进行异或,结果发送给acker。最后acker会将tuple的结果(发送成功/发送失败)以tuple的形式报告给spout。

 

 

备注:

The complete latency: is the timefrom the spout emitting a tuple to that  tuple being acked on the spout. So it tracks the time for the whole tuple tree to be processed.

The process latency:It's thetime from a bolt receiving a tuple to the time the bolt acks that tuple.

每个处理的tuple,必须被ack或者fail。因为storm追踪每个tuple要占用内存。所以如果你不ack/fail每一个tuple,那么最终你会看到OutOfMemory错误。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值