软件版本:Storm:0.9.3 ,Redis:2.8.19;jedis:2.6.2;
参考:http://storm.apache.org/documentation/Understanding-the-parallelism-of-a-Storm-topology.html
一、Storm原理
Storm简述:Storm中有两个组件:nimbus和supervisor,nimbus主要负责分配资源和schedule和协调任务,supervisor主要启动worker,每个worker可以启动一个到多个executor,一个executor可以启动一个到多个task,(默认一个executor对应一个task)实际执行任务的是task。
二、Storm编程
1. Topology (拓扑)
1.1 定义spout、bolt以及其关系
//Topology definition
TopologyBuilder builder = new TopologyBuilder();
builder.setSpout("wc-spout",new WCSpout(),Integer.parseInt(args[2]));
builder.setBolt("split-bolt", new SplitBolt(),Integer.parseInt(args[3]))
.shuffleGrouping("wc-spout");
builder.setBolt("count-bolt", new CountBolt(),Integer.parseInt(args[4]))
.fieldsGrouping("split-bolt", new Fields("word"));
在设置spout和bolt的时候还可以设置并行的个数,即executor的个数,当然也可以设置task的个数,如下代码,两个executor,四个task,则每个executor配置两个task。
topologyBuilder.setBolt("green-bolt", new GreenBolt(), 2)
.setNumTasks(4)
.shuffleGrouping("blue-spout);
<代码来自:
http://storm.apache.org/documentation/Understanding-the-parallelism-of-a-Storm-topology.html>
// 定义Configuration
Config conf = new Config();
conf.put("storeFrequent", Long.parseLong(args[0]));
conf.put("slow_fast", args[5]);
conf.put("printWC", args[6]);
conf.setNumWorkers(Integer.parseInt(args[1]));
上面的代码除了设置三个变量值之外,还设置了worker的数量;
1.3 提交Topology
集群提交方式:
// 提交任务
StormSubmitter.submitTopology("wc-redis", conf,builder. createTopology());
单机提交方式:
LocalCluster cluster = new LocalCluster();
cluster. submitTopology("wc-redis", conf,
builder. createTopology());
2.1 继承BasiRichSpout
继承这个类后,定义一个域变量SpoutOutputCollector collector,这个用于输出;
2.2 覆写open方法
public void open(@SuppressWarnings("rawtypes") Map conf, TopologyContext context,
SpoutOutputCollector collector) {
在这里首先使用collector来初始化之前定义的域变量,如果有需要获取的参数值可以从conf中获取,context里面是当前的spout的相关信息上下文;
2.3 覆写nextTuple
在这个方法里面使用collector.emit方法即可进行输出,一般使用下面的方式即可:
public List emit(List
如果要求容错基本较高,可以使用一个streamid的方式进行输出,如下:
public List emit(String streamId, List
2.4 覆写declareOutputFields方法
一般定义一个field名字即可,如下:
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("line"));
}
3. Bolt
3.1 继承BaseBasicBolt类
3.2 覆写prepare方法
public void prepare(@SuppressWarnings("rawtypes") Map conf,TopologyContext context)
这个方法类似Spout的open方法,进行一些初始化或获取参数值的操作;
3.3 覆写exec方法
public void execute(Tuple input, BasicOutputCollector collector)
这个方法的input即是从Spout中输出的数据,通过对这个数据进行处理,然后使用collector.emit方法进行输出,可以是输出到下一个Bolt的处理,作为下一个Bolt的输入。
3.4 覆写declareOutputFields方法
这个方法和Spout的declareOutputFields方法类似,声明field的名字。
4. 提交运行
storm jar wc.jar test.TopologyMain
比如分配了一个worker,然后一个Spout S分配两个executor,一个Bolt A分配一个executor,另一个Bolt B分配一个executor,那么这些executor一共有4个task(使用默认一个executor对应一个task),就会全部分配在一个worker上。
如果分配了两个worker,还按上的组件分配,那么可能worker 1上面分配了一个Spout S的executor和Bolt A的executor,worker 2 上面分配了一个Spout S的另一个executor,和Bolt B的executor。
2. Spout executor并行
Spout如果单单设置executor的并行个数,那么其输出可能是有重复的,这样的并行策略是有问题的。
比如下面的Spout:
package wc.redis.spout;
import java.util.Map;
import redis.clients.jedis.Jedis;
import wc.redis.util.RedisUtils;
import backtype.storm.spout.SpoutOutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseRichSpout;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Values;
public class WCSpout extends BaseRichSpout {
/**
*
*/
private static final long serialVersionUID = 1L;
private SpoutOutputCollector collector;
private Jedis jedis;
Integer taskId;
String conponentId;
String slow_fast;
@Override
public void open(@SuppressWarnings("rawtypes") Map conf, TopologyContext context,
SpoutOutputCollector collector) {
this.collector = collector;
slow_fast = (String)conf.get("slow_fast");
jedis = RedisUtils.connect(RedisUtils.HOSTNAME, RedisUtils.PORT, RedisUtils.INSERT_DB);
taskId = context.getThisTaskId();
conponentId = context.getThisComponentId();
context.getThisTaskIndex();
System.out.println(RedisUtils.getCurrDateWithInfo(conponentId, taskId, " WCSpout初始化完成!"));
}
@Override
public void nextTuple() {
long interval =0;
while(true){// 获取数据
interval++;
String zero = getItem("0");
String one = getItem("1");
String two = getItem("2");
try {
Thread.sleep(200);// 每200毫秒发送一次数据
} catch (InterruptedException e) {
e.printStackTrace();
}
if(zero==null||one==null||two==null){
// do nothing
// 没有数据
if(interval%15==0){
// System.out.println(new java.util.Date()+":ConponentId:"+conponentId+",taskID:"+taskId+
// ",spout:No Data...");
// System.out.println(RedisUtils.getCurrDateWithInfo(conponentId, taskId, "spout:No data..."));
}
}else{
this.collector.emit(new Values(zero+","+one+","+two));
if(interval%15==0&&"fast".equals(slow_fast)){
// System.out.println(new java.util.Date()+":ConponentId:"+conponentId+",taskID:"+taskId+
// ",spout:["+zero+","+one+","+two+"]");
System.out.println(RedisUtils.getCurrDateWithInfo(conponentId, taskId, "Spout:["+zero+","+one+","+two+"]"));
}else if("slow".equals(slow_fast)){
System.out.println(RedisUtils.getCurrDateWithInfo(conponentId, taskId, "Spout:["+zero+","+one+","+two+"]"));
}else{
new RuntimeException("Wrong argument!");
}
}
}
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("line"));
}
/**
* Redis中获取键值并删除对应的键
* @param index
*/
private String getItem(String index){
if(!jedis.exists(index)){
return null;
}
String val = jedis.get(index);
// if(val==null||"null".equals("null")){
// return ;
// }
jedis.del(index);
return val;
}
}
图1
从图1红色区域可以看到Spout的输出,从时间可以看出两个输出只相差了1毫秒;从蓝色的框也可以看出Spout的下一个Bolt获取了两条相同的数据,这就说明Spout输出了重复的数据;
所以Spout的并行策略应该是获取taskid,根据数据的特征来选择(可以随机)需要处理的executor,代码如下:
package wc.redis.spout;
import java.util.Map;
import redis.clients.jedis.Jedis;
import wc.redis.util.RedisUtils;
import backtype.storm.spout.SpoutOutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseRichSpout;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Values;
public class WCSpout extends BaseRichSpout {
/**
*
*/
private static final long serialVersionUID = 1L;
private SpoutOutputCollector collector;
private Jedis jedis;
Integer taskId;
String componentId;
String slow_fast;
int numTasks ;
int thisTaskId;
@Override
public void open(@SuppressWarnings("rawtypes") Map conf, TopologyContext context,
SpoutOutputCollector collector) {
this.collector = collector;
slow_fast = (String)conf.get("slow_fast");
jedis = RedisUtils.connect(RedisUtils.HOSTNAME, RedisUtils.PORT, RedisUtils.INSERT_DB);
taskId = context.getThisTaskId();
componentId = context.getThisComponentId();
numTasks = context.getComponentTasks(componentId).size();
thisTaskId = context.getThisTaskIndex();
System.out.println(RedisUtils.getCurrDateWithInfo(componentId, taskId, " WCSpout初始化完成!"));
}
@Override
public void nextTuple() {
long interval =0;
while(true){// 获取数据
interval++;
String zero = getItem("0");
String one = getItem("1");
String two = getItem("2");
try {
Thread.sleep(200);// 每200毫秒发送一次数据
} catch (InterruptedException e) {
e.printStackTrace();
}
if(zero==null||one==null||two==null){
// do nothing
// 没有数据
// if(interval%15==0){
// }
}else{
String tmpStr =zero+","+one+","+two;
if(thisTaskId==tmpStr.hashCode()%numTasks){ // spout负载均衡
this.collector.emit(new Values(tmpStr));
if(interval%15==0&&"fast".equals(slow_fast)){
System.out.println(RedisUtils.getCurrDateWithInfo(String.valueOf(thisTaskId),
taskId, "Spout:["+zero+","+one+","+two+"]"));
}else if("slow".equals(slow_fast)){
System.out.println(RedisUtils.getCurrDateWithInfo(String.valueOf(thisTaskId),
taskId, "Spout:["+zero+","+one+","+two+"]"));
}else{
new RuntimeException("Wrong argument!");
}
}
}
}
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("line"));
}
/**
* Redis中获取键值并删除对应的键
* @param index
*/
private String getItem(String index){
if(!jedis.exists(index)){
return null;
}
String val = jedis.get(index);
// if(val==null||"null".equals("null")){
// return ;
// }
jedis.del(index);
return val;
}
}
使用上面的代码后,Spout的输出就不会重复了,同时也达到了distribution的目的,如图2
从图2中红色框中可以看到从时间21:58 883 taskId5 Spout输出后,接着到了taskID6 21:59 494 Spout输出,然后又到taskID5 21:59 905 的Spout输出,并没有重复记录;
3. Bolt的并行
Bolt的并行只要设置了多个executor即可。
3.1 使用shuffle进行grouping
使用shuffle进行grouping,多个task的输入中同样的记录可能被分到了任何一个taskid中,如图3所示。
图3
从图3中的蓝色框中可以看到Spout输出了两条相同的记录,的那是一条记录被送到了taskID为5的Bolt中(红色框),一条被送到了taskID为6的Bolt中(红色框),当然从下面的描述中也可以知道,事实就是这样。
上图引自《Getting Started with Storm》
3.2 使用field进行grouping
使用field进行grouping其实是和shuffle一样的,但是有一点不一样,就是相同的记录只会被送往同一个taskid中,比如上面图3中,如果使用field进行grouping,那么Spout输出的两条相同的记录就只会被送往taskid为5的task中(或者为6)。
分享,成长,快乐
脚踏实地,专注
转载请注明blog地址:http://blog.csdn.net/fansy1990