1. 整体说明

整个代码分析是在storm-2.0的基础上面。

storm查看supervisor状态_客户端


整个过程可以分为5步:

1. 用户执行storm jar的命令提交任务到Nimbus上面

2. Nimbus的定时线程查看是否有需要运行的任务

3. 当有任务时,发送消息到Supervisor

4. 启动一个logWriter进程

5. LogWriter启动实际的Worker进程

2. 任务提交

用户的任务都是提交到Nimbus上面。这其中可以分为如下几个步骤:客户端的处理, Nimbus接收topology, 定时任务处理topology

2.1 客户端的处理

客户端的程序中会将我们常用的几个名词带入:TopologyBuilder, spout, bolt

2.1.1 Topo的创建

下面的程序是wordcout为例:

TopologyBuilder builder = new TopologyBuilder();
        builder.setSpout("spout", new RandomSentenceSpout(), 5);
        builder.setBolt("split", new SplitSentence(), 8).shuffleGrouping("spout");
        builder.setBolt("count", new WordCount(), 12).fieldsGrouping("split", new Fields("word"));

这是构建Topology的过程,topology中有几个参数需要注意:

private final Map<String, IRichBolt> _bolts = new HashMap<>();
    private final Map<String, IRichSpout> _spouts = new HashMap<>();
    private final Map<String, ComponentCommon> commons = new HashMap<>();
    private final Map<String, Set<String>> _componentToSharedMemory = new HashMap<>();
    private final Map<String, SharedMemory> _sharedMemory = new HashMap<>();

我们的bolt, spout信息就是保存在相应的Map中, 这其中有一个commons的Map,这个Map需要特别注意。所有的spout和bolt都会在这个map中保存一次。 这一点从setSpout或setBolt中可以看到

public SpoutDeclarer setSpout(String id, IRichSpout spout, Number parallelism_hint) throws IllegalArgumentException {
        validateUnusedId(id);
        initCommon(id, spout, parallelism_hint); //这里就是设置到commons中
        _spouts.put(id, spout);
        return new SpoutGetter(id);
    }

2.2 Nimbus接收topology

Nimbus.java中有一个函数submitTopologyWithOpts(),就是实际处理任务提交的代码。其代码过程如下(只列出了主要部分):

Nimbus::submitTopologyWithOpts( ... )
{
    //1. 参数的检查,以及topologyId的组装等,

    //2. 上传jar包到nimbus上面
    LOG.info("uploadedJar {}", uploadedJarLocation);
    setupStormCode(conf, topoId, uploadedJarLocation, totalConfToSave, topology);

    //3. 在zookeeper上面设置一些topo需要的目录
    state.setupHeatbeats(topoId, topoConf);
    state.setupErrors(topoId, topoConf);

    //4. 调用startTopology()函数
    startTopology(topoName, topoId, status, topologyOwner, topologyPrincipal); 
}

我们接着查看一下startTopology的函数:

private void startTopology( ... )
{
    //1. 解析numExecutors的信息
    StormTopology topology = StormCommon.systemTopology(topoConf, readStormTopology(topoId, topoCache));
    Map<String, Integer> numExecutors = new HashMap<>();
    for (Entry<String, Object> entry : StormCommon.allComponents(topology).entrySet()) {
        numExecutors.put(entry.getKey(), StormCommon.numStartExecutors(entry.getValue()));
    }
    //在这里,需要注意的,除了有用户创建的bolt与spout对象外,还有两个特殊的bolt,它们的名称是:__acker与__system,特别是这个__acker后面在Ack消息的时候会使用到
    //2. 参数信息的设置到,如当前时间,状态,提交者等 

    //3. 调用激活函数
    state.activateStorm(topoId, base, topoConf);
}

但是当我们查看 activateStorm函数的时候,会就会现它也没有与supervisor联系,其代码如下:

public void activateStorm(String stormId, StormBase stormBase, Map<String, Object> topoConf) {
        String path = ClusterUtils.stormPath(stormId);
        stateStorage.mkdirs(ClusterUtils.STORMS_SUBTREE, defaultAcls); //在zookeeper创建相应目录(/storm/mk)
        stateStorage.set_data(path, Utils.serialize(stormBase), ClusterUtils.mkTopoReadOnlyAcls(topoConf));
        this.assignmentsBackend.keepStormId(stormBase.get_name(), stormId);
    }

查看zookeeper的信息,可以看到:

[zk: localhost:2181(CONNECTED) 2] ls /storm/assignments
[start-topology]

函数至此,就会给client返回成功的信息,但是此时topology根据没有在supervisor节点上面运行起来。总结一下,我们就会发现它在这个两个函数中一共做四件事:
1. 各种参数校验,参数的拼结
2. 将客户端上面的jar包上传到nimbus节点上面
3. 在zookeeper上面创建相应的目录,在/storm/assignments等
3. 将已经完成参数的topo保存到stormClusterState对象中。

2.3 处理 topology

topology对应的任务在正式执行之前,还有一个工作,就是需要选择对应的work(即在哪个supervisor节点上面启动相应的work进程),这些工作主要是由Nimbus::mkAssignment()这个函数完成的。而这个函数的调用是通过StormTimer调用的。其调用栈如下:

org.apache.storm.daemon.nimbus.Nimbus.mkAssignments() 2,078 <- 
    org.apache.storm.daemon.nimbus.Nimbus.mkAssignments() 2,003 <- 
    org.apache.storm.daemon.nimbus.Nimbus.lambda$launchServer$29() 2,701 <- 
    org.apache.storm.StormTimer$1.run() 111 <- 
    org.apache.storm.StormTimer$StormTimerTask.run() 227

我们一起来看一下mkAssignment()的使用

private void mkAssignments(String scratchTopoId) throws Exception {

    // 1. 注意这里的stormClusterState,与我们之前在startTopology()是同一个
    IStormClusterState state = stormClusterState;  
    ... ... 

    //2. 获取部署的节点信息
    newSchedulerAssignments = computeNewSchedulerAssignments(existingAssignments, topologies, bases, scratchTopoId);
    ... ... 

    //3. 开始部署
    notifySupervisorsAssignments(newAssignments, assignmentsDistributer, totalAssignmentsChangedNodes,
                                     basicSupervisorDetailsMap);

    ... ... 

}

这里特别提一下notifySupervisorsAssignments()函数,Supervisor接收到消息,就是通过这个函数中的RPC调用

2.4 Supervisor节点接收消息

Supervisor进程启动会启动一个进程:SynchronizeAssignments,这一点我们可以从Supervisor的日志中看到

2018-05-30 08:02:06.825 o.a.s.u.NimbusClient Thread-4 [INFO] Found leader nimbus : node129:6627
2018-05-30 08:02:06.826 o.a.s.d.s.t.SynchronizeAssignments Thread-4 [DEBUG] Sync an assignments from master, will start to sync with assignments: SupervisorAssignments(storm_assignment:{})

这行日志对应的代码是SynchronizeAssignments::getAssignmentsFromMaster(),其代码如下:

public void getAssignmentsFromMaster(Map conf, IStormClusterState clusterState, String node) {
            ... ... 
                SupervisorAssignments assignments = master.getClient().getSupervisorAssignments(node);
                LOG.debug("Sync an assignments from master, will start to sync with assignments: {}", assignments);
                assignedAssignmentsToLocal(clusterState, assignments);
            ... ... 

        }
    }

这里很明显,Supervisor会通过远程调用Nimbus::getSupervisorAssignments()函数,通过个函数,获取相应的任务分配情况,然后调用函数assignedAssignmentsToLocal(),来执行任务的创建,看一下assignedAssignmentsToLocal()函数的实现,它最终调到StormClusterStateImpl::syncRemoteAssignments()

public void syncRemoteAssignments(Map<String, byte[]> remote) {
        if (null != remote) {
            this.assignmentsBackend.syncRemoteAssignments(remote);
        } else {
            Map<String, byte[]> tmp = new HashMap<>();
            List<String> stormIds = this.stateStorage.get_children(ClusterUtils.ASSIGNMENTS_SUBTREE, false);
            for (String stormId : stormIds) {
                byte[] assignment = this.stateStorage.get_data(ClusterUtils.assignmentPath(stormId), false);
                tmp.put(stormId, assignment);
            }
            this.assignmentsBackend.syncRemoteAssignments(tmp);
        }
    }

总结

整个提交过程,我们可以分为三个部分:
1) 客户端, 客户端会将spout, bolt统一处理,将在同一个map中
2)客户端将任务提交到Nimbus后,Nimbus并不是马上创建任务;Nimbus会启动一种定时线程,这个定时线程负责选择任务执行的Supervisor节点
3)Supervisor也会启动一个线程,通过RPC远程调用,从Nimbus获取任务信息.