1. 整体说明
整个代码分析是在storm-2.0的基础上面。
整个过程可以分为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获取任务信息.