在这一章,你将知道Storm的不同拓扑组件之间是如何传递tuple的,以及如何将一个拓扑部署到生产Storm集群中。
流分组
设计一个拓扑你需要做的一件非常重要的事是定义组件(流如何被bolts消费)之间数据的交换方式。流分组指定了每个bolt要消费的流(可能多个)和流如何被消费。
一个节点可以发生多于一个数据流。流分组允许我们选择要接收的流。
正如在第二章看到的那样,流分组在定义拓扑结构时被指定:
...
builder.setBolt("word-normalizer",new WordNormalizer())
.shuffleGrouping("word-reader");
...
在前面的代码块中,在topology builder对象中设置了一个bolt,同时指定使用shuffle streamgrouping来消费数据流。流分组通常将源组件的id作为参数,同时还有一些可选的其它参数,依赖于选择的流分组样式。
每个InputDeclarer可以有超过一种源,每个源使用不同的流分组样式。
Shuffle Grouping
Shuffle grouping是最常用的一种分组方式。它使用单个参数(源组件id),源随机地选择bolt将每个tuple发送出去,并保证每个消费者接收到相同数目的tuple。
Shuffle grouping分组方式对于执行原子操作是非常有用的,例如数学运算。然而,如果操作不能被随机分发,例如第二章中的单词计数,你就应该考虑其它分组方式的使用了。
Fields Grouping
Fields Grouping允许你基于tuple中一个或多个field字段的值来控制tuple往哪里发送。它保证给定fields字段值的组合一定会发送到相同的bolt。回到单词计数示例,如果使用word字段来分组流,word-nomalizerbolt将会将相同word值的tuple发送到同一个word-counter bolt实例处理。
...
builder.setBolt("word-counter",new WordCounter(),2)
.fieldsGrouping("word-normalizer",new Fields("word"));
...
在流分组中指定的所有字段必须在源的字段声明中。
All Grouping
All Grouping发送每个tuple的一份拷贝到接收bolt的所有实例。这种分组方式被用来发送信号给bolts。例如,如果你需要刷新缓存,你可以发送一个refresh cache信号给所有bolt。在单词计数示例中,你可以使用all grouping分组来增加一个清空计数缓存的能力(见拓扑示例)。
public void execute(Tupleinput) {
String str =null;
try{
if(input.getSourceStreamId().equals("signals")){
str =input.getStringByField("action");
if("refreshCache".equals(str))
counters.clear();
}
}catch(IllegalArgumentException e) {
//Do nothing
}
...
}
我们加入了一个if来检查数据源流。Storm有声明命名式流的能力(如果你不发送tuple到一个命名了的流,流默认是default);这是一种优秀的方式来识别tuple源,如本示例中我们想识别信号。
在拓扑定义中,我们将给word-counter bolt加入第二个流,它从signals-spout流中往所有bolt实例发送每个tuple。
builder.setBolt("word-counter",new WordCounter(),2)
.fieldsGrouping("word-normalizer",new Fields("word"))
.allGrouping("signals-spout","signals");
Signals-spout实现可以在 git仓库 找到。
Custom Grouping
你可以通过实现backtype.storm.grouping.CustomStreamGrouping接口来创建你自定义的流分组。这给了你来决定哪些bolt接收这些tuple的能力。
让我们修改单词计数示例,让它按照相同单词首字母会被同一个bolt接收来分组。
public class ModuleGrouping implements CustomStreamGrouping,Serializable{
int numTasks = 0;
@Override
public List<Integer> chooseTasks(List<Object> values) {
List<Integer> boltIds= new ArrayList();
if(values.size()>0){
String str =values.get(0).toString();
if(str.isEmpty())
boltIds.add(0);
else
boltIds.add(str.charAt(0)% numTasks);
}
Return boltIds;
}
@Override
public void prepare(TopologyContext context, Fields outFields,
List<Integer>targetTasks) {
numTasks =targetTasks.size();
}
}
你可以看到CustomStreamGrouping的简单实现,它使用单词首字母的整数值对任务数取模来决定哪些bolt来接收该分组。
要想在示例中使用该分组,按下面方式修改word-normalizer分组:
builder.setBolt("word-normalizer",new WordNormalizer())
.customGrouping("word-reader",new ModuleGrouping());
Direct Grouping
这是一种特殊的分组方式,tuple源来决定到底哪些组件会接收该tuple。和之前示例一样,tuple仍使用单词的首字母来决定哪些bolt来接收。要使用direct grouping分组方式,在WordNormal bolt中使用emitDirect来代替emit方法。
public void execute(Tuple input) {
...
for(String word : words){
if(!word.isEmpty()){
...
collector.emitDirect(getWordCountIndex(word),new Values(word));
}
}
// Acknowledge the tuple
collector.ack(input);
}
public Integer getWordCountIndex(String word) {
word = word.trim().toUpperCase();
if(word.isEmpty())
return 0;
else
return word.charAt(0) % numCounterTasks;
}
在prepare方法中计算出目标任务数:
public void prepare(Map stormConf, TopologyContext context,
OutputCollector collector) {
this.collector = collector;
this.numCounterTasks = context.getComponentTasks("word-counter");
}
在拓扑定义中,指定stream的分组方式为direct grouping:
builder.setBolt("word-counter", new WordCounter(),2)
.directGrouping("word-normalizer");
Global Grouping
Global Grouping将源的所有实例产生的tuple发送到一单个目标实例中(明确地,拥有最小id的任务)。
None Grouping
在本书编写的时候(Storm版本0.7.1),使用该分组和使用Shuffle grouping是一样的效果。换句话说,当使用该分组时,它不关心流是如何分组的。
LocalCluster与StormSubmitter
直到现在,你已经使用了一个叫LocalCluster的工具来在你本地计算机中运行拓扑。在你自己计算机中运行Storm架构使你运行和调试不同的拓扑变得更加简单。但当你想提交拓扑到生产环境怎么办?Storm一个非常有意义的特征就是很简单地将你的拓扑运行在一个实际集群中。只需要将LocalCluster修改为StormSubmitter,并实现submitTopology方法,它负责将拓扑发送到集群中。
你可以看到代码的修改如下:
//LocalCluster cluster = new LocalCluster();
//cluster.submitTopology("Count-Word-Topology-With-Refresh-Cache",conf,
builder.createTopology());
StormSubmitter.submitTopology("Count-Word-Topology-With-Refresh-Cache",conf,
builder.createTopology());
//Thread.sleep(1000);
//cluster.shutdown();
当你使用StormSubmitter时,你不能使用代码来扩展集群除非你使用LocalCluster。
下一步,将源代码打包成jar包,当你运行Storm客户端命令来提交拓扑时,jar包被发送到集群中。因为你已经使用Maven,你唯一需要做的事就是进去到源代码目录,并运行如下命令:
mvn package
一旦你已经生成jar包,使用“storm jar”命令来提交拓扑(你从附件A应该知道如何安装Storm客户端)。语法是storm jarallmycode.jar org.me.MyTopology arg1 arg2 arg3
在本例中,从拓扑源代码项目目录,运行:
storm jartarget/Topologies-0.0.1-SNAPSHOT.jar countword.TopologyMainsrc/main/resources/words.txt
使用上述命令,你就已经提交了拓扑到集群中。
要停止或杀死它,使用命令运行:
storm killCount-Word-Topology-With-Refresh-Cache
拓扑名字必须是唯一的
安装Storm客户端,见附件A。
DRPC拓扑
这是一种特殊类型的拓扑,称作分布式远程过程调用(DistributedRemote Procedure Call,DRPC),它使用Storm的分布式能力来执行远程过程调用(Remote ProcedureCalls,RPC)。Storm提供了你许多工具来使用DRPC。DRPC服务器一方面作为客户端和Storm拓扑之间的连接器,另一方面作为拓扑spout的输入。它接收一次函数调用和函数参数。接下来,对函数操作的每个分片数据,服务器分配一个请求id来标识RPC请求。当拓扑知道到最后一个bolt,它必须发送RPC的请求id和结果,使得DRPC服务器能将结果返回给对应的服务器。
单个DRPC服务器可以执行许多函数。每个函数以一个唯一的名字标识。
Storm提供的第二个工具(在例子中使用)是LinearDRPCTopologyBuilder,帮助构建DRPC拓扑的抽象。生成的拓扑创建DRPCSpouts——负责连接DRPC服务器,发送数据到拓扑——包装bolts以便为了结果能从最后一个bolt返回。所有的bolt被加入到LinearDRPCTopologyBuilder,并按一定的次序执行。
作为这种类型的拓扑的示例,你将创建一个进程来数字加法。这是一个简单例子,但是该概念可以被延伸到执行复杂的分布式数学操作。
Bolt的输出声明如下:
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("id","result"));
}
因为这是拓扑中唯一的bolt,它必须发送RPC ID和结果。execute方法负责执行add操作。
public void execute(Tuple input) {
String[] numbers = input.getString(1).split("\\+");
Integer added = 0;
if(numbers.length<2){
throw new InvalidParameterException("Should be at least 2numbers");
}
for(String num : numbers){
added += Integer.parseInt(num);
}
collector.emit(new Values(input.getValue(0),added));
}
将该增加的bolt按如下方式加入到拓扑:
public static void main(String[] args) {
LocalDRPC drpc = new LocalDRPC();
LinearDRPCTopologyBuilder builder = newLinearDRPCTopologyBuilder("add");
builder.addBolt(new AdderBolt(),2);
Config conf = new Config();
conf.setDebug(true);
LocalCluster cluster = new LocalCluster();
cluster.submitTopology("drpc-adder-topology", conf,
builder.createLocalTopology(drpc));
String result = drpc.execute("add", "1+-1");
checkResult(result,0);
result = drpc.execute("add", "1+1+5+10");
checkResult(result,17);
cluster.shutdown();
drpc.shutdown();
}
创建LocalDRPC对象来在本地运行DRPC服务器。下一步,创建一个topology builder,将bolt加入到topology中。为了测试topology,使用DRPC对象的execute方法。
为了连接远程DRPC服务器,使用DRPCClient类。DRPC服务器暴露了一个Thrift API,这样就可以使用多语言了。在本地或远程运行DRPC服务器,API都是一样的。
要提交拓扑到Storm集群,使用builder对象的createRemoteTopology方法代替createLocalTopology,它使用DRPC配置代替Storm配置。