Storm 1.2 单词计算topology的数据流

单词计算topology由一个spout和下游的3个bolt组成,如下图:


一、语句生成spout
SentenceSpout类的功能很简单,向后端发射一个单值tuple组成的数据流,键名是“sentence”,键值是字符串格式存储的一句话,如下所示:
{"sentence":"my dog has fleas"}
为了简化起见,我们的数据源是一个静态语句的列表。Spout会一直循环将每句话作为tuple发射。


二、语句分割bolt
语句分割bolt(SplitSentenceBolt)类会订阅sentence spout发射的tuple流。每当收到一个tuple,bolt会获取“sentence”对应值域的语句,然后将语句分割为一个个的单词。每个单词向后发射一个tuple。
{"word":"my"}
{"word":"dog"}
{"word":"has"}
{"word":"fleas"}


三、单词计数bolt
单词计数bolt(WordCountBolt)订阅SplitSentenceBolt类的输出,保存每个特定单词出现的次数。每当bolt接收到一个tuple,会将对应单词的计数加一,并且向后发送该单词当前的计数。
{"word":"dog","count":5}


四、上报bolt
上报bolt订阅WordCountBolt类的输出,像WordCountBolt一样,维护一份所有单词对应的计数的表。当接收到一个tuple时,上报bolt会更新表中的计数数据,并且将值在终端打印。

五、实现单词计数topology
前面介绍了Storm的基础概念,我们已经准备好实现一个简单的应用。现在开始着手开发一个Storm topology,并且在本地模式执行。Storm本地模式会在一个JVM实例中模拟出一个Storm集群。大大简化了用户在开发环境或者IDE中进行开发和调试。
1、实现SentenceSpout
为简化起见,SentenceSpout的实现通过重复静态语句列表来模拟数据源。每句话作为一个单值的tuple向后循环发射。完整实现如下:
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 nextTuple() {
		this.collector.emit(new Values(sentences[index]));
		index++;
		if(index >= sentences.length){
			index=0;
		}
		Utils.sleep(1);
	}

	public void open(Map arg0, TopologyContext arg1, SpoutOutputCollector arg2) {
		this.collector = arg2;
	}

	public void declareOutputFields(OutputFieldsDeclarer declarer) {
		declarer.declare(new Fields("sentence"));
	}
}

BaseRichSpout类是Ispout接口和Icomponent接口的一个简便的实现。接口对本例中用不到的方法提供了默认实现。使用这个类,我们可以专注在所需要的方法上。方法declareOutputFields()实在Icomponent接口中定义的,所有Storm的组件(spout和bolt)都必须实现这个接口。Storm的组件通过这个方法告诉Storm该组件会发射哪些数据流,每个数据流的tuple中包含哪些字段。本例中,我们声明了spout会发射一个数据流,其中的tuple包含一个字段(sentence)。
Open()方法在ISpout接口中定义,所有Spout组件在初始化时调用这个方法。Open()方法接收三个参数,一个包含了Storm配置信息的map,TopologyContext对象提供了topology中组件的信息,SpoutOutputCollector对象提供了发射tuple的方法。本例中,初始化时不需要做额外的操作,因此open()方法实现仅仅是简单将SpoutOutputCollector对象的引用保存在变量中。
NextTuple()方法是所有spout实现的核心所在,Storm通过调用这个方法向输出的collector发射tuple。这个例子中,我们发射当前索引对应的语句,并且递增索引指向下一个语句。

2、实现语句分割bolt
SplitSentenceBolt类的实现如下:
public class SplitSentenceBolt extends BaseRichBolt{
	private OutputCollector 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 prepare(Map map, TopologyContext topologycontext,
			OutputCollector outputcollector) {
		this.collector=outputcollector;
	}

	public void declareOutputFields(OutputFieldsDeclarer outputfieldsdeclarer) {
		outputfieldsdeclarer.declare(new Fields("word"));
	}
}

BaseRichBolt类是Icomponent和IBolt接口的一个简便实现。继承这个类,就不用去实现本例不关心的方法,将注意力放在实现我们需要的功能上。
prepare()方法在IBolt中定义,类同与ISpout接口中定义的open()方法。这个方法在bolt初始化时调用,可以用来准备bolt用到的资源,如数据库连接。和SentenceSpout类一样,SplitSentenceBolt类在初始化时没有额外操作,因此prepare()方法仅仅保存OutputCollector对象的引用。
在declareOutputFields()方法中,SplitSentenceBolt声明了一个输出流,每个tuple包含一个字段“word”。
SplitSentenceBolt类的核心功能在execute()方法中实现,这个方法是IBolt接口定义的。每当从订阅的数据流中接收一个tuple,都会调用这个方法。本例中,execute()方法按照字符串读取“sentence”字段的值,然后将其拆分为单词,每个单词向后面的输出流发射一个tuple。

3、实现单词计数bolt
WordCountBolt类的实现如下:
public class WordCountBolt extends BaseRichBolt{
	private OutputCollector collector;
	private HashMap<String, Long> counts = null;

	public void prepare(Map map, TopologyContext topologycontext,
			OutputCollector outputcollector) {
		this.collector=outputcollector;
		this.counts = new HashMap<String, Long>();
	}

	public void execute(Tuple tuple) {
		String word = tuple.getStringByField("word");
		Long count = this.counts.get(word);
		if(count == null){
			count = 0L;
		}
		count++;
		this.counts.put(word, count);
		this.collector.emit(new Values(word,count));
	}

	public void declareOutputFields(OutputFieldsDeclarer outputfieldsdeclarer) {
		outputfieldsdeclarer.declare(new Fields("word","count"));
	}
}

WordCountBolt类是topology中实际进行单词计数的组件。该bolt的prepare()方法中,实例化了一个HashMap<String, Long>的实例,用来存储单词和对应的计数。大部分实例变量通常是在prepare()方法中进行实例化,这个设计模式是由topology的部署方式决定的。当topology发布时,所有的bolt和spout组件首先会进行序列化,然后通过网络发送到集群中。如果spout或者bolt在序列化之前(比如在构造函数中生成)实例化了任何无法序列化的实例变量,在进行序列化时会抛出NotSerializableException异常,topology就会部署失败。本例中,因为HashMap<String, Long>是可序列化的,所以在构造函数中进行实例化也是安全的。但是通常情况下最好是在构造函数中对基本数据类型和可序列化的对象进行赋值和实例化,在prepare()方法中对不可序列化的对应进行实例化。
在declareOutputFields()方法中,类WordCountBolt声明了一个输出流,其中的tuple包括了单词和对应的计数。
execute()方法中,当接收到一个单词时,首先会查找这个单词对应的计数(如果单词没有出现过则计数初始化为0),递增并存储计数,然后将单词和最新计数作为tuple向后发射。将单词计数作为数据流发射,topology中的其他bolt就可以订阅这个数据流进行进一步的处理。

4、实现上报bolt
public class ReportBolt extends BaseRichBolt{
	private HashMap<String, Long> counts = null;
	
	public void prepare(Map map, TopologyContext topologycontext,
			OutputCollector outputcollector) {
		this.counts = new HashMap<String, Long>();
	}

	public void execute(Tuple tuple) {
		String word = tuple.getStringByField("word");
		Long count = tuple.getLongByField("count");
		this.counts.put(word, count);
	}

	public void declareOutputFields(OutputFieldsDeclarer outputfieldsdeclarer) {
		// this bolt does not emit anything
	}
	
	public void cleanup(){
		List<String> keys = new ArrayList<String>();
		keys.addAll(this.counts.keySet());
		Collections.sort(keys);
		for(String key : keys){
			System.out.println(key+":"+this.counts.get(key));
		}
	}
}

ReportBolt类的作用是对所有单词的计数生成一份报告。和WordCountBolt类似,ReportBolt使用一个HashMap<String, Long>来保存单词和对应计数。本例中,它的功能是简单的存储接收到计数bolt发射出的计数tuple。
上报bolt和上述其他bolt的区别是,它是一个位于数据流末端的bolt,只接受tuple。因为它不发射任何数据流,所以declareOutputFields()方法是空的。
上报中初次引用了cleanup()方法,这个方法在IBolt接口中定义。Storm在终止一个bolt之前会调用这个方法。本例中我们利用cleanup()方法在topology关闭时输出最终的计算结果。通常情况下,cleanup()方法用来释放bolt占用的资源,比如打开的文件句柄或者数据库连接。
开发bolt时需要谨记的是,当topology在Storm集群上运行时,IBolt. cleanup()方法是不可靠的,不能保证会执行。

5、实现单词计数topology
public class WordCountTopolopy {
	private static final String SENTENCE_SPOUT_ID = "sentence-spout";
	private static final String SPLIT_BOLT_ID = "split-bolt";
	private static final String COUNT_BOLT_ID = "count-bolt";
	private static final String REPORT_BOLT_ID = "report-bolt";
	private static final String TOPOLOPY_NAME = "word-count-topolopy";
	
	public static void main(String[] args) throws Exception{
		SentenceSpout spout = new SentenceSpout();
		SplitSentenceBolt splitBolt = new SplitSentenceBolt();
		WordCountBolt countBolt = new WordCountBolt();
		ReportBolt reportBolt = new ReportBolt();
		
		TopologyBuilder builder = new TopologyBuilder();
		
		builder.setSpout(SENTENCE_SPOUT_ID, spout);
		builder.setBolt(SPLIT_BOLT_ID, splitBolt).shuffleGrouping(SENTENCE_SPOUT_ID);
		builder.setBolt(COUNT_BOLT_ID, countBolt).fieldsGrouping(SPLIT_BOLT_ID, new Fields("word"));
		builder.setBolt(REPORT_BOLT_ID, reportBolt).globalGrouping(COUNT_BOLT_ID);
		
		Config config = new Config();
		LocalCluster cluster = new LocalCluster();
		cluster.submitTopology(TOPOLOPY_NAME, config, builder.createTopology());
		
		watForSeconds(1000);
		
		cluster.killTopology(TOPOLOPY_NAME);
		cluster.shutdown();
	}
}

在本例中,我们首先定义了一系列字符串常量,作为Storm组件的唯一标识符。TopologyBuilder类提供了流式接口风格的API来定义topology组件之间的数据流。首先注册一个sentence spout并且赋值给其唯一的ID:
builder.setSpout(SENTENCE_SPOUT_ID, spout);
然后注册一个SplitSentenceBolt,这个bolt订阅SentenceSpout发射出来的数据流:
builder.setBolt(SPLIT_BOLT_ID, splitBolt).shuffleGrouping(SENTENCE_SPOUT_ID);
类TopologyBuilder的setBolt()方法会注册一个bolt,并且返回BoltDeclarer的实例,可以定义bolt的数据源。这个例子中,我们将SentenceSpout的唯一ID赋值给shuffleGrouping()方法确立了这种订阅关系。shuffleGrouping()方法告诉Storm,要将类SentenceSpout发射的tuple随机均匀的分发给SplitSentenceBolt的实例。代码下一行确立了类SplitSentenceBolt和类WordCountBolt之间的连接关系:
builder.setBolt(COUNT_BOLT_ID, countBolt).fieldsGrouping(SPLIT_BOLT_ID, new Fields("word"));
你将了解到,有时候需要将含有特定数据的tuple路由到特殊的bolt实例中。因此我们使用类BoltDeclarer的fieldsGrouping()方法来保证所有的"word"字段值相同的tuple会被路由到同一个WordCountBolt实例中。
定义数据流的最后一步是将WordCountBolt实例发射出的tuple流路由到类ReportBolt类上。本例中,我们希望WordCountBolt发射的所有tuple路由到唯一的ReportBolt任务重。globalGrouping()方法提供了这种方法:
builder.setBolt(REPORT_BOLT_ID, reportBolt).globalGrouping(COUNT_BOLT_ID);
所有的数据流都已经定义好,运算单词计数的最后一步是编译并提交到集群上:
Config config = new Config();
LocalCluster cluster = new LocalCluster();
cluster.submitTopology(TOPOLOPY_NAME, config, builder.createTopology());
watForSeconds(1000);
cluster.killTopology(TOPOLOPY_NAME);
cluster.shutdown();
这里我们采用用Storm的本地模式,使用Storm的LocalCluster类在本地开发环境来模拟一个完整的Storm集群。本地模式是开发和测试的简便方式。省去了分布式集群中反复部署的开销。本地模式还能够方便的在IDE中执行Storm topology,设置断点,暂停运行,观察变量,分析程序性能。当topology发布到分布式集群后,这些事情会很耗时甚至难以做到。
Storm的Config类是一个HashMap<String,Object>的子类,并定义了一些Storm特有的常量和简便的方法,用来配置topology运行时行为。当一个topology提交时,Storm会将默认配置和Config实例中的配置合并后作为参数传递给submitTopology()方法。合并后的配置被分发给各个spout的bolt的open()、prepare()方法。从这个层面上讲,Config对象代表了对topology所有组件全局生效的配置参数集合。现在可以运行WordCountTopology类了,mian()方法会提交topology,在执行10秒后,停止该topology,最后关闭本地模式集群。程序执行完毕后u,在控制台可以看到类似以下的输出:


以上来自:


猜你喜欢

转载自margaret0071.iteye.com/blog/2360175