本章我们将会创建一个Storm工程和我们的第一个Storm topology。
提示:下述假设你已经安装JRE1.6或者更高级版本。推荐使用Oracle提供的JRE:http://www.java.com/downloads/.
在开始创建项目之前,了解Storm的操作模式(operation modes)是很重要的。Storm有两种运行方式:
本地模式
在本地模式下,Storm topologies 运行在本地机器的一个JVM中。因为本地模式下查看所有topology组件共同工作最为简单,所以这种模式被用于开发、测试和调试。例如,我们可以调整参数,这使得我们可以看到我们的topology在不同的Storm配置环境中是如何运行的。为了以本地模式运行topologies,我们需要下载Storm的开发依赖包,这是我们开发、测试topologies所需的所有东西。当我们建立自己的第一个Storm工程的时候我们很快就可以看到是怎么回事了。
提示:本地模式下运行一个topology同Storm集群中运行类似。然而,确保所有组件线程安全非常重要,因为当它们被部署到远程模式时,它们可能运行在不同的JVM或者不同的物理机器上,此时,它们之间不能直接交流或者共享内存。
在本章的所有示例中,我们都以本地模式运行。
远程模式
在远程模式下,我们将topology提交到Storm集群中,Storm集群由许多进程组成,这些进程通常运行在不同的机器上。远程模式下不显示调试信息,这也是它被认为是产品模式的原因。然而,在一台机器上创建一个Storm集群也是可能的,并且在部署至产品前这样做还是一个好方法,它可以确保将来在一个成熟的产品环境中运行topology不会出现任何问题。
译者的话:所谓产品环境/模式,指的是代码比较成熟,可以当成产品发布了,与开发环境相对。
在第六章中可以了解到更多关于远程模式的内容,我会在附录B中展示如何安装一个集群。
在这个项目中,我们会建立一个简单的topology来统计单词个数,我们可以将它看成是Storm topologies中的“Hello World”。然而,它又是一个非常强大的topology,因为它几乎可以扩展到无限大小,并且经过小小的修改,我们甚至可以使用它创建一个统计系统。例如,我们可以修改本项目来找到Twitter上的热门话题。
为了建立这个topology,我们将使用一个spout来负责从文件中读取单词,第一个bolt来标准化单词,第二个bolt去统计单词个数,如图2-1所示:
你可以在https://github.com/storm-book/examples-ch02-getting_started/zipball/master下载本例源码的ZIP文件。
译者的话:本站有备份:http://www.flyne.org/example/storm/storm-book-examples-ch02-getting_started-8e42636.zip
提示:如果你使用git(一个分布式的版本控制和源码管理工具),则可以运行命令:git clone [email protected]:storm-book/examplesch02-getting_started.git进入你想要下载的源码所在的目录。
检查Java安装
搭建环境的第一步就是检查正在运行的Java版本。运行java -version命令,我们可以看到类似如下信息:
java -version
java version “1.6.0_26″
Java(TM) SE Runtime Environment(build 1.6.0_26-b03)
Java HotSpot(TM) Server VM (build 20.1-b02,mixed mode)
首先,创建一个文件夹,用于存放这个应用(就像对于任何Java应用一样),该文件夹包含了整个项目的源代码。
接着我们需要下载Storm的依赖包——添加到本应用classpath的jar包集合。可以通过下面两种方式完成:
下载依赖包,解压,并将它们加入classpath路径
使用Apache Maven
提示:Maven是一个软件项目管理工具,可以用于管理一个项目开发周期中的多个方面(从从依赖包到发布构建过程),在本书中我们会广泛使用Maven。可以使用mvn命令检查maven是否安装,如果未安装,可以从http://maven.apache.org/download.html下载。
下一步我们需要新建一个pom.xml文件(pom:project object model,项目的对象模型)去定义项目的结构,该文件描述了依赖包、封装、源码等等。这里我们将使用由nathanmarz构建的依赖包和Maven库,这些依赖包可以在https://github.com/nathanmarz/storm/wiki/Maven找到。
提示:Storm的Maven依赖包引用了在本地模式下运行Storm所需的所有函数库。
使用这些依赖包,我们可以写一个包含运行topology基本的必要组件的pom.xml文件:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>storm.book</groupId> <artifactId>Getting-Started</artifactId> <version>0.0.1-SNAPSHOT</version> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> <configuration> <source>1.6</source> <target>1.6</target> <compilerVersion>1.6</compilerVersion> </configuration> </plugin> </plugins> </build> <repositories> <!-- Repository where we can found the storm dependencies --> <repository> <id>clojars.org</id> <url>http://clojars.org/repo</url> </repository> </repositories> <dependencies> <!-- Storm Dependency --> <dependency> <groupId>storm</groupId> <artifactId>storm</artifactId> <version>0.6.0</version> </dependency> </dependencies> </project>
前几行指定了项目名称、版本;然后我们添加了一个编译器插件,该插件告诉Maven我们的代码应该用Java1.6编译;接着我们定义库(repository)(Maven支持同一个项目的多个库),clojars是Storm依赖包所在的库,Maven会自动下载本地模式运行Storm需要的所有子依赖包。
本项目的目录结构如下,它是一个典型的Maven Java项目。
java目录下的文件夹包含了我们的源代码,并且我们会将我们的单词文件放到resources文件夹中来处理。
为建立我们第一个topology,我们要创建运行本例(统计单词个数)的所有的类。本阶段例子中的有些部分不清楚很正常,我们将在接下来的几个章节中进一步解释它们。
Spout(WordReader类)
WordReader类实现了IRichSpout接口,该类负责读取文件并将每一行发送到一个bolt中去。
提示:spout发送一个定义字段(field)的列表,这种架构允许你有多种bolt读取相同的spout流,然后这些bolt可以定义字段(field)供其他bolt消费。
例2-1包含WordReader类的完整代码(后面会对代码中的每个部分进行分析)
例2-1.src/main/java/spouts/WordReader.java
package spouts; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.util.Map; import backtype.storm.spout.SpoutOutputCollector; import backtype.storm.task.TopologyContext; import backtype.storm.topology.IRichSpout; import backtype.storm.topology.OutputFieldsDeclarer; import backtype.storm.tuple.Fields; import backtype.storm.tuple.Values; public class WordReader implements IRichSpout{ private SpoutOutputCollector collector; private FileReader fileReader; private boolean completed=false; private TopologyContext context; public boolean isDistributed(){return false;} public void ack(Object msgId) { System.out.println("OK:"+msgId); } public void close(){} public void fail(Object msgId) { System.out.println("FAIL:"+msgId); } /** * 该方法用于读取文件并发送文件中的每一行 */ public void nextTuple() { /** * The nextuple it is called forever, so if we have beenreaded the file * we will wait and then return */ if(completed){ try { Thread.sleep(1000); } catch(InterruptedException e) { //Do nothing } return; } String str; //Open the reader BufferedReader reader =new BufferedReader(fileReader); try{ //Read all lines while((str=reader.readLine())!=null){ /** * By each line emmit a new value with the line as a their */ this.collector.emit(new Values(str),str); } }catch(Exception e){ throw new RuntimeException("Errorreading tuple",e); }finally{ completed = true; } } /** * We will create the file and get the collector object */ public void open(Map conf,TopologyContext context,SpoutOutputCollector collector) { try { this.context=context; this.fileReader=new FileReader(conf.get("wordsFile").toString()); } catch(FileNotFoundException e) { throw new RuntimeException("Errorreading file["+conf.get("wordFile")+"]"); } this.collector=collector; } /** * 声明输出字段“line” */ public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("line")); } }
在任何spout中调用的第一个方法都是open()方法,该方法接收3个参数:
TopologyContext:它包含了所有的topology数据
conf对象:在topology定义的时候被创建
SpoutOutputCollector:该类的实例可以让我们发送将被bolts处理的数据。
下面的代码块是open()方法的实现:
public void open(Map conf,TopologyContext context,SpoutOutputCollector collector) { try { this.context=context; this.fileReader=new FileReader(conf.get("wordsFile").toString()); } catch(FileNotFoundException e) { throw new RuntimeException("Errorreading file["+conf.get("wordFile")+"]"); } this.collector=collector; }
在open()方法中,我们也创建了reader,它负责读文件。接着,我们需要实现nextTuple()方法,在该方法中发送要被bolt处理的值(values)。在我们的例子中,这个方法读文件并且每行发送一个值。
public void nextTuple() { if(completed){ try { Thread.sleep(1000); } catch(InterruptedException e) { //Do nothing } return; } String str; //Open the reader BufferedReader reader =new BufferedReader(fileReader); try{ //Read all lines while((str=reader.readLine())!=null){ /** * By each line emmit a new value with the line as a their */ this.collector.emit(new Values(str),str); } }catch(Exception e){ throw new RuntimeException("Errorreading tuple",e); }finally{ completed = true; } }
提示:Values类是ArrayList的一个实现,将列表中的元素传递到构造方法中。
nextTuple()方法被周期性地调用(和ack()、fail()方法相同的循环),当没有工作要做时,nextTuple()方法必须释放对线程的控制,以便其他的方法有机会被调用。因此必须在nextTuple()第一行检查处理是否完成,如果已经完成,在返回前至少应该休眠1秒来降低处理器的负载,如果还有工作要做,则将文件中的每一行读取为一个值并发送出去。
提示:元组(tuple)是一个值的命名列表,它可以是任何类型的Java对象(只要这个对象是可以序列化的)。默认情况下,Storm可以序列化的常用类型有strings、byte arrays、ArrayList、HashMap和HashSet。
Bolt(WordNormalizer&WordCounter类)
上面我们设计了一个spout来读取文件,并且每读取一行发送一个元组(tuple)。现在,我们需要创建两个bolt处理这些元组(见图2-1)。这些bolt实现了IRichBolt接口。
在bolt中,最重要的方法是execute()方法,每当bolt收到一个元组,该方法就会被调用一次,对于每个收到的元组,该bolt处理完之后又会发送几个bolt。
提示:一个spout或bolt可以发送多个tuple,当nextTuple()或execute()方法被调用时,它们可以发送0、1或者多个元组。在第五章中你将会了解到更多。
第一个bolt,WordNormalizer,负责接收每一行,并且将行标准化——它将行分解为一个个的单词后转化成小写,并且消除单词前后的空格。
首先,我们需要声明bolt的输出参数:
public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("word")); }
这儿,我们声明bolt发送一个命名为“word”的字段。
接着,我们实现execute方法,输入的tuple将会在这个方法中被处理:
public void execute(Tuple input) { String sentence = input.getString(0); String[]words= sentence.split(" "); for(String word:words){ word =word.trim(); if(!word.isEmpty()){ word =word.toLowerCase(); //Emit the word List a =new ArrayList(); a.add(input); collector.emit(a,new Values(word)); } } // Acknowledge the tuple collector.ack(input); }
第一行读取元组中的值,可以按照位置或者字段命名读取。值被处理后使用collector对象发送出去。当每个元组被处理完之后,就会调用collector的ack()方法,表明该tuple成功地被处理。如果tuple不能被处理,则应该调用collector的fail()方法。
例2-2包含这个类的完整代码。
例2-2.src/main/java/bolts/WordNormalizer.java
下一页