导引
但凡写过几个流处理应用的开发者,想必对Storm和JStorm中的源节点(Spout)和计算节点(Bolt)已经很熟悉了。我们只需要继承BaseRichSpout和BaseRichBolt或者与之类似的抽象类或接口即可,用具体的业务逻辑填充其中的prepare(), nextTuple(), execute()等方法,然后把这些节点组合成计算拓扑(Topology)交给Storm或JStorm,它们就会负责按照我们编写的业务逻辑来不断地接收外部数据和处理数据,执行计算任务。这两个简单的例子展现了这一点,SentenceSpout 能随机产生句子:
public class SentenceSpout extends BaseRichSpout {
private SpoutOutputCollector collector;
private String[] sentences = {
"my dog has fleas",
"i like cold beverages",
"the dog ate my homework",
"don't have a cow man",
"i don't think i like fleas"
};
private int index = 0;
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("sentence"));
}
public void open(Map config, TopologyContext
context, SpoutOutputCollector collector) {
this.collector = collector;
}
public void nextTuple() {
this.collector.emit(new Values(sentences[index]));
index++;
if (index >= sentences.length) {
index = 0;
}
Utils.sleep(1);
}
}
而SplitSentenceBolt根据句子间的空格,将句子划分成一个个单词:
public class SplitSentenceBolt extends BaseRichBolt {
private OutputCollector collector;
public void prepare(Map config, TopologyContext
context, OutputCollector collector) {
this.collector = collector;
}
public void execute(Tuple tuple) {
String sentence = tuple.getStringByField("sentence");
String[] words = sentence.split(" ");
for(String word : words){
this.collector.emit(new Values(word));
}
}
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("word"));
}
}
虽然我们编写的逻辑很简单,但是Storm和JStorm为了将我们的业务逻辑运转起来,在底层做了大量的工作,相关的代码也非常多。我们今天就来看看其中最核心的部分——Storm是如何根据我们编写的业务逻辑来执行流计算的。
类的层次
在Storm的底层,使用执行者(Executors)这个概念来对应根据具体业务逻辑执行流计算的部件。其中,SpoutExecutors对应着是Spout的底层实现,而BoltExecutors则对应着是Bolt的底层实现,它们都继承自BaseExecutors,而BaseExecutors则继承自RunnableCallback。这个类实现了三个接口,分别是Java的Runnable和JStorm自定义的Callback和Shutdownable接口,如下所示:
public class RunnableCallback implements Runnable, Callback, Shutdownable {
@Override
public <T> Object execute(T... args) {
return null;
}
public void preRun() {
}
@Override
public void run() {
}
public void postRun() {
}
public Exception error() {
return null;
}
public Object getResult() {
return null;
}
public void shutdown() {
}
public String getThreadName() {
return null;
}
}
可以看出,RunnableCallback其实就是一个功能比较丰富的Runnable,添加了一些运行前postRun()、运行后postRun()、汇报结果getResult()等方法。读者们大可以把它看作Runnable,理解后续代码是没有障碍的。
按照一般的顺序,可能先讨论BaseExecutors的实现,然后再谈谈 SpoutExecutors和BoltExecutors在它基础之上做的更改会是一个自然而然的顺序。但是我决定将顺序反过来,由于大部分读者对Spout和Bolt更为熟悉一些,我打算先讨论它们各自为上层API提供的具体实现,然后再讨论由BaseExecutors实现的一些公共支持。
源节点执行者——SpoutExecutors
SpoutExecutors中的一个至关重要的成员就是一个ISpout实例:
public class SpoutExecutors extends BaseExecutors implements EventHandler {
// ...
protected backtype.storm.spout.ISpout spout;
}
这个ISpout实例就是包含开发者编写的具体业务逻辑的源节点,其中开发者在nextTuple()方法中编写了以何种方式获取下一个元组的业务逻辑,这个nextTuple()方法会被 SpoutExecutors的nextTuple()方法所调用:
public void nextTuple() {
if (!taskStatus.isRun()) {
JStormUtils.sleepMs(1);
return;
}
// if don't need ack, pending map will be always empty
if (max_spout_pending == null || pending.size() < max_spout_pending) {
emptyCpuGauge.stop();
long start = nextTupleTimer.getTime();
try {
spout.nextTuple();
} catch (Throwable e) {
error = e;
LOG.error("spout execute error ", e);
report_error.report(e);
} finally {
nextTupleTimer.updateTime(start);
}
} else {
if (isSpoutFullSleep) {
JStormUtils.sleepMs(1);
}
emptyCpuGauge.start();
// just return, no sleep
}
}
在nextTuple()方法的开始,要检查任务是否仍是活跃状态。之后要做一个关于输出压力的判断,我们在后面探讨Storm如何应对数据流量压力时再作阐述,这里我们看最核心部分的代码:
long start = nextTupleTimer.getTime();
try {
spout.nextTuple();
} catch (Throwable e) {
error = e;
LOG.error("spout execute error ", e);
report_error.report(e);
} finally {
nextTupleTimer.updateTime(start);
}
在调用ISpout实例spout的nextTuple()方法之前,我们记一个开始时间。然后调用spout的nextTuple()方法,由于该方法可能会抛出FailedException,因此这里进行异常捕捉,将捕捉到的异常通过继承自父类的ITaskReportErr实例进行汇报,之后再记一个结束的时间戳就可以了。整个过程就在开发者的nextTuple()方法基础上作了三件事:第一,将时间戳记录下来,用来跟踪元组采集的效率;第二,判断是否要终止当前任务;第三,看看是否已经达到了采集能力上限,如果是,就休眠一会儿并启动相应的CPU空转管理。
那么nextTuple()是什么时候被调用的呢?SpoutExecutors没有告诉我们答案,我们只能去它的子类中去寻找。它有两个子类——SingleThreadSpoutExecutors和
MultipleThreadSpoutExecutors,在它们的run()方法里,调用了nextTuple()。我们以SingleThreadSpoutExecutors为例:
@Override
public void run() {
if (checkTopologyFinishInit == false ) {
initWrapper();
int delayRun = ConfigExtension.getSpoutDelayRunSeconds(storm_conf);
long now = System.currentTimeMillis();
while (!checkTopologyFinishInit){
// wait other bolt is ready, but the spout can handle the received message
executeEvent();
controlQueue.consumeBatch(this);
if (System.currentTimeMillis() - now > delayRun * 1000){
executorStatus.setStatus(TaskStatus.RUN);
this.checkTopologyFinishInit = true;
LOG.info("wait {} timeout, begin operate nextTuple", delayRun);
break;
}
}
while (true){
JStormUtils.sleepMs(10);
if (taskStatus.isRun()){
this.spout.activate();
break;
}else if (taskStatus.isPause()){
this.spout.deactivate();
break;
}
}
LOG.info(idStr + " is ready, due to the topology finish init. ");
}
executeEvent();
controlQueue.consumeBatch(this);
super.nextTuple();
}
首先检查集群拓扑是否初始化完毕,如果已经初始化完毕,那么逻辑很简单:
executeEvent();
controlQueue.consumeBatch(this);
super.nextTuple();
调用executeEvent()方法执行事件处理,然后从控制消息队列中消费信息,完成之后,调用nextTuple()方法即可。
如果集群没有初始化完毕,那么就一直循环等待集群初始化完毕,这过程中,SingleThreadSpoutExecutors依然可以处理接收到的消息,只是不能调用nextTuple()方法而已。
那么executeEvent()方法处理的是什么事件呢?答案是处理有可能存在的来自下游的元组接收成功或失败的ack/fail消息,所以这个方法需要在调用nextTuple()之前来完成。
发送元组的任务则交给了SpoutCollector,在sendMsg()方法中,生成元组,并由TaskTransfer实例调用transfer()方法发送该元组:
TupleImplExt tp = new TupleImplExt(topology_context, values, task_id, out_stream_id, msgid);
tp.setTargetTaskId(t);
transfer_fn.transfer(tp);
关于TaskTransfer的作用和实现,我们将在下一篇文章中详细讲述。现在,大家只要知道它可以将元组发给指定的目标(例如标记了taskId的目标)就可以了。
计算节点执行者——BoltExecutors
在BoltExecutors的processTupleEvent()方法中,我们可以发现对execute()方法的调用:
private void processTupleEvent(Tuple tuple) {
if (tuple.getMessageId() != null && tuple.getMessageId().isAnchored()) {
tuple_start_times.put(tuple, System.currentTimeMillis());
}
try {
if (!isSystemBolt && tuple.getSourceStreamId().equals(Common.TOPOLOGY_MASTER_CONTROL_STREAM_ID)) {
TopoMasterCtrlEvent event = (TopoMasterCtrlEvent) tuple.getValue(0);
if (event.isTransactionEvent()) {
bolt.execute(tuple);
} else {
LOG.warn("Received unexpected control event, {}", event);
}
} else if (tuple.getSourceStreamId().equals(Common.TOPOLOGY_MASTER_REGISTER_METRICS_RESP_STREAM_ID)) {
this.metricsReporter.updateMetricMeta((Map<String, Long>) tuple.getValue(0));
} else {
bolt.execute(tuple);
}
} catch (Throwable e) {
error = e;
LOG.error("bolt execute error ", e);
report_error.report(e);
}
}
那么这个Tuple是哪来的呢?在实现EventHandler的onEvent()方法中可以发现,元组被包含在了Object实例event中。那么这些包含元组的事件从哪里得到呢?由于EventHandler是Disruptor框架中的内容,所以我们终于找到了最终的关键——Storm本身是利用Disruptor框架来进行高效的消息接收和处理的。
关于Disruptor框架的内容、特性和优势,可以查看【小贴士】,目前来讲,大家把它看作一个异常高效的消息队列就可以了,不会影响到后续代码的阅读。