参考文献
https://www.cnblogs.com/intsmaze/p/5918087.html
基本概念
-
一个worker就是一个JVM进程。一个worker中可以运行多个bolt和spout,但是这些bolt和spolt都必须属于一个topology。一台物理机器可以运行多个worker。所有同一个worker里的bolt和spout可以共享该JVM中的资源。为了防止资源重复,建议每一个bolt和spout用单例模式获取资源。一个woker拥有一个端口用于通信。一台机器上可以同时启动多个worker,每个woker占用一个端口。executor是实际执行的线程,可以执行一个或者多个task,但是这些task必须是相同的bolt或者spout。executor数量可以动态调整,task则在代码中指定,所以,你可以把task设置高点(提高bolt和spout的并发度),不然,即使你以后增加并发跳高executor数量,多于task的executor也没有任何意义。
-
storm使用tuple来作为它的数据模型。每个tuple是一堆值,每个值有一个名字,并且每个值可以是任何类型。一个没有边界的、源源不断的、连续的Tuple序列就组成了Stream。
-
storm的每一个executor都会创建自己的执行的bolt和spout的实例。线程之间不共享实例,所以实例(bolt spout)中的字段没有并发问题。
-
主控节点运行Nimbus守护进程,类似于Hadoop中的jobtracker,负责在集群中分发代码,对节点分配任务,并监视主机故障。每个工作节点运行Supervisor守护进程,负责监听工作节点上已经分配的主机作业,启动和停止Nimbus已经分配的工作进程。supervisor会定时从zookeeper获取拓补信息topologies、任务分配信息assignments及各类心跳信息,以此为依据进行任务分配。在supervisor同步时,会根据新的任务分配情况来启动新的worker或者关闭旧的worker并进行负载均衡
-
storm底层采取netty通信框架,kryo进行序列化。
结构
每个Worker进程配置了一个NettyServer,有一个线程专门负责监听在分配给自己这个worker端口。这个线程把从网络接收到的消息放在对应的task的反序列化队列里。又一个反序列化线程专门负责反序列化,然后放到对应task的执行队列里。每个task执行队列中的事件,最终由该task的executor线程负责执行。
Grouping
- Shuffle Grouping 随机分组
- Fields Grouping 按照字段分组
- All Grouping 广播分组(所有都会收到)
- Global Grouping:全局分组。这种分组会将所有的tuple都发到一个taskid最小的task上。由于所有的tuple都发到唯一一个task上,势必在数据量大的时候会造成资源不够用的情况。
- Non Grouping
- Direct Grouping:直接分组,这是一种比较特别的分组方法,用这种分组意味着消息的发送者指定由消息接受者的哪个task处理这个消息。
- Local or shuffle grouping:如果目标bolt有一个或者多个task在同一个工作进程中,tuple将会被随机发送给这些tasks。否则,和普通的Shuffle Grouping行为一致。
Storm消息机制
为了保证数据能正确的被处理, 对于spout产生的每一个tuple, storm都会进行跟踪。
Storm中有个特殊的task名叫acker,他们负责跟踪spout发出的每一个Tuple的Tuple树(因为一个tuple通过spout发出了,经过每一个bolt处理后,会生成一个新的tuple发送出去)。当acker(框架自启动的task)发现一个Tuple树已经处理完成了,它会发送一个消息给产生这个Tuple的那个task。
Acker的跟踪算法是Storm的主要突破之一,对任意大的一个Tuple树,它只需要恒定的20字节就可以进行跟踪。
实例
定义bolt的输出。下面的输出tuple包含两个字段。
@Override
publicvoiddeclareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(newFields("double","triple"));
}
Storm调优
##日志
不同于一般的JAVA应用,storm的日志配置文件需要手动指定。可以使用logback也可以使用log4j。
ConfigExtension.setUserDefinedLog4jConf(conf, "log4j.properties");
JAVA API
BaseRichBolt和BaseBasicBolt
在BaseBasicBolt中,BasicOutputCollector在emit数据的时候,会自动和输入的tuple相关联,而在execute方法结束的时候那个输入tuple会被自动ack。
//Basicbolt所实现的接口
public interface IBasicBolt extends IComponent {
void prepare(Map var1, TopologyContext var2);
void execute(Tuple var1, BasicOutputCollector var2);
void cleanup();
}
//Richbolt所实现的接口
public interface IBolt extends Serializable {
void prepare(Map var1, TopologyContext var2, OutputCollector var3);
void execute(Tuple var1);
void cleanup();
}
BaseBasicBolt中带了BasicOutputCollector。可以直接使用该BasicOutputCollector向下游发送。BaseRichBolt显然没有这个功能。
//BasicBoltExecutor类中代码
public void execute(Tuple input) {
this._collector.setContext(input);
try {
this._bolt.execute(input, this._collector);
this._collector.getOutputter().ack(input);
} catch (FailedException var3) {
if (var3 instanceof ReportedFailedException) {
this._collector.reportError(var3);
}
this._collector.getOutputter().fail(input);
}
}
BaseBasicBolt如果抛出异常,那么自动调用fail。否则调用ack。而BaseRichBolt必须自己手动调用ack。
性能指标
在Storm UI上,可以看到如下性能指标
MemoryUsed:使用的物理内存
HeapMemory:VM使用到的堆内存
CpuUsedRatio: cpu利用率
NettyCliSendSpeed:当前发送流量,单位字节/每秒
NettySrvRecvSpeed:当前接收流量,单位字节/每秒
FullGc:当前1分钟 full gc 次数
RecvTps: 接收到的tuple的tps
SendTps: 发送tuple的tps
Emitted:当前1分钟发送的消息数,包括业务消息和acker消息。
Acked: 当前1分钟被ack的消息数,注意这个和Emitted的区别:如果打开了acker机制, emitted的消息里面含有acker消息, 经常emitted 消息数量是acker消息数量的2倍
Failed:当前1分钟 被ack失败的消息数(可能是没有完全处理,也可能是超时)
如果你在一个spout coponent里发现下面的的指标,不用担心,因为emited的事件包含acked的事件(相当于原事件和ack事件),所以会保持差不多1:2的比例关系。
详细请参考阿里的文档
http://www.jstorm.io/Maintenance_cn/JStormMetrics.html
##T opo启动过程
- 使用Jstorm命令启动,Jstorm会将命令行加上自己的库和配置,然后执行以上传jar包的main方法为入口执行
- 在代码中调用 StormSubmitter.submitTopology(args[0], conf, builder.createTopology()); 启动nimbus client,把当前的topo提交给Nimbus
- 现在可以在控制台上终止命令。
- nimbus首先调用各个bolt和spout的构造函数,初始化spout和bolt对象。然后序列化,传给各个supervisor。
- supervisor接收到序列化对象后,启动ProcessLauncher进程,该进程负责启动woker进程,并将对象传给worker。理论上ProcessLauncher进程会在启动worker成功或失败后自动退出。
//第一步,构造命令行
public String getLauncherParameter(
LocalAssignment assignment, Map totalConf, String stormHome, String topologyId, int port) throws IOException {
boolean isEnable = ConfigExtension.isProcessLauncherEnable(totalConf);
if (!isEnable) {
return "";
}
// STORM-LOCAL-DIR/supervisor/stormdist/topologyId
String stormRoot = StormConfig.supervisor_stormdist_root(conf, topologyId);
// STORM-LOCAL-DIR/supervisor/stormdist/topologyId/stormjar.jar
String stormJar = StormConfig.stormjar_path(stormRoot);
StringBuilder sb = new StringBuilder();
sb.append(" java ");
sb.append(ConfigExtension.getProcessLauncherChildOpts(totalConf));
sb.append(getLogParameter(totalConf, stormHome, assignment.getTopologyName(), port));
sb.append(" -cp ");
sb.append(getClassPath(stormHome, totalConf));
if (ConfigExtension.isEnableTopologyClassLoader(totalConf)) {
// don't append storm jar when classloader is enabled
} else {
// user defined log configuration is likely to be bundled in the storm jar
sb.append(":").append(stormJar);
}
//启动的JVM的主类是ProcessLauncher
sb.append(" ").append(ProcessLauncher.class.getName()).append(" ");
String launcherCmd = sb.toString();
if (ConfigExtension.getWorkerRedirectOutput(totalConf)) {
String outFile = getWorkerRedirectOutput(totalConf, assignment, port);
outFile = "-Dlogfile.name=" + outFile + " ";
launcherCmd = launcherCmd.replaceAll("-Dlogfile\\.name=.*?\\s", outFile);
}
return launcherCmd;
}
//第二步,准备envirment
Map<String, String> environment = new HashMap<>();
if (ConfigExtension.getWorkerRedirectOutput(totalConf)) {
environment.put("REDIRECT", "true");
} else {
environment.put("REDIRECT", "false");//默认是False
}
environment.put("LD_LIBRARY_PATH", (String) totalConf.get(Config.JAVA_LIBRARY_PATH));
environment.put("jstorm.home", stormHome);
environment.put("jstorm.workerId", workerId);
environment.put("jstorm.on.yarn", isJstormOnYarn ? "1" : "0");
String launcherCmd = getLauncherParameter(assignment, totalConf, stormHome, topologyId, port);
String workerCmd = getWorkerParameter(assignment,
totalConf,
stormHome,
topologyId,
supervisorId,
workerId,
port);
String cmd = launcherCmd + " " + workerCmd;
cmd = cmd.replace("%JSTORM_HOME%", stormHome);
//日志会被打印到supervisor.log中,可以在UI看到
LOG.info("Launching worker with command: " + cmd);
LOG.info("Environment:" + environment.toString());
/**
* if run on yarn, set backend false, otherwise set true
* 注意,2.1.1版本没有yarn支持
*/
boolean backend = !isJstormOnYarn;
LOG.info("backend mode is " + backend);
JStormUtils.launchProcess(cmd, environment, backend);
//第三步,使用ProcessBuilder新建进程
protected static java.lang.Process launchProcess(final List<String> cmdlist,
final Map<String, String> environment) throws IOException {
ProcessBuilder builder = new ProcessBuilder(cmdlist);
//设置将标准错误输出合并到标准输出
builder.redirectErrorStream(true);
Map<String, String> process_evn = builder.environment();
for (Entry<String, String> entry : environment.entrySet()) {
process_evn.put(entry.getKey(), entry.getValue());
}
return builder.start();
}
查阅JAVA API可知,Process所有标准 io(即 stdin,stdout,stderr)操作都将通过三个流 (getOutputStream(),getInputStream(),getErrorStream()) 重定向到父进程。注意这点行为和C语言中的fork并不一致。
在完成后,要检查启动的Launcher线程的返回状态。
Process process = launchProcess(cmdlist, environment);
StringBuilder sb = new StringBuilder();
String output = JStormUtils.getOutput(process.getInputStream());
String errorOutput = JStormUtils.getOutput(process.getErrorStream());
sb.append(output);
sb.append("\n");
sb.append(errorOutput);
int ret = process.waitFor();
if (ret != 0) {
LOG.warn(command + " is terminated abnormally. ret={}, str={}", ret, sb.toString());
}
return sb.toString();
- worker将对象进行反序列化,根据自己被分配的executor的数目,决定具体反序列化多少个bolt/spout对象
- 调用bolt/spout中的初始化方法,准备完毕。开始接收事件。