摘要
在 Storm 的拓扑中,存在若干种流分发策略;而在拓扑的创建中,也容许一个拓扑中接收消息的为不同类型的 bolt。那么在复杂拓扑结构中,流分发机制是否可靠?本文以实验的方式模拟稍微复杂的网络拓扑,并发送数据流进行了验证,得出 jStorm 可以很好地识别 bolt 类型,不同组 bolt 订阅相同流互相不影响的结论。随后将对 jStorm 如何做到这种类似消息队列的消费者 offset 维护将进行浅析。
拓扑结构
如下图所示,TestSpout 通过 static Atomic Integer 原子操作类型进行线程安全的多线程计数,并将该值发送至两个不同处理逻辑的 Bolt。之后对两个 Bolt 的输入日志进行分析。
测试结论
- 不同的 Bolt 组订阅消息满足不同 groupping 的预期,即流量分发均可拿到全量数据而互不干扰;
- 只要有流出异常,如果两边都有 ack 机制,那么任务会被停止;
- 否则,如果未出异常,那么两边均正常 ack 后,继续发送下一个 tuple(从这里猜测,应该是 spout 内部维护了一个同步发送队列,即所有需要 ack 的流都被应答后,才同时广播给所有 Bolt 下一个数据)。
- 变换 worker 数目后发现,不同的 task(bolt/spout均被按顺序编号task-id)几乎会均匀的分配到不同的 worker(同一个worker间的task之间数据收发通过队列,不同的worker的task之间数据收发通过netty),未产生流丢失的现象,且日志分别在不同的两个worker所在机器上。
测试代码
/**
* @Description 测试spout的流订阅机制
* @param spoutSize
* @param boltSize
* @param workers
* @return
* @throws Exception
*/
private boolean testSample(Integer spoutSize, Integer boltSize, Integer workers) throws Exception {
TopologyBuilder builder = new TopologyBuilder();
builder.setSpout("testSpout", new TestSpout(), spoutSize);
builder.setBolt("TestBoltOrig", new TestBoltOrig(), 2).fieldsGrouping("testSpout",new Fields("modVal"));
builder.setBolt("TestBoltDouble", new TestBoltDouble(), 3).fieldsGrouping("testSpout",new Fields("modVal"));
conf.put(backtype.storm.Config.TOPOLOGY_WORKERS, 2); // 设置Worker的数量
try {
//提交测试拓扑
StormSubmitManager.getInstance().submitTopology(builder, "testSample", conf);
LOGGER.info("submit success!");
return true;
} catch (Exception e) {
LOGGER.error("submit failed : " + e.getMessage(), e);
throw e;
}
}
/**
* @Description 测试 wordCount
* @author cathar
* @Date 2017年1月4日 下午8:25:07
*/
public class TestSpout extends BaseRichSpout {
private SpoutOutputCollector collector;
private boolean completed = false;
private static final Logger LOGGER = Logger.getLogger(TestSpout.class);
//全局共享的,在spout初始化的时候,每个线程获取一次作为自己的编号
static AtomicInteger sAtomicInteger = new AtomicInteger(0);
//多线程共享的变量
static AtomicInteger pendNum = new AtomicInteger(0);
private int sqnum;
@Override
public void open(Map conf, TopologyContext context,
SpoutOutputCollector collector) {
//spout 线程编号
sqnum = sAtomicInteger.incrementAndGet();
//初始化发射器
this.collector = collector;
}
@Override
public void nextTuple() {
//模拟一直发送递增的全局数字
while (true) {
int a = pendNum.incrementAndGet();
//如果有多个线程,这里输出的数字是连续的,共同改变
LOGGER.info(String.format("spout %d, send pendNum %d", sqnum, a));
this.collector.emit(new Values(a%10, a));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
// TODO Auto-generated method stub
//declarer.declare(new Fields("word"));
declarer.declare(new Fields("modVal","val"));
}
/**
* 启用 ack 机制,详情参考:https://github.com/alibaba/jstorm/wiki/Ack-%E6%9C%BA%E5%88%B6
* @param msgId
*/
@Override
public void ack(Object msgId) {
super.ack(msgId);
}
/**
* 消息处理失败后需要自己处理
* @param msgId
*/
@Override
public void fail(Object msgId) {
super.fail(msgId);
LOGGER.info("ack fail,msgId"+msgId);
}
}
public class TestBoltOrig implements IRichBolt {
//...
@Override
public void execute(Tuple input) {
Integer partition = input.getIntegerByField("modVal");
Integer val = input.getIntegerByField("val");
LOGGER.error(String.format("TestBoltOrig received partition %d: val %d", partition,val));
this.collector.ack(input);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//...
}
public class TestBoltDouble implements IRichBolt {
//...
@Override
public void execute(Tuple input) {
Integer partition = input.getIntegerByField("modVal");
Integer val = input.getIntegerByField("val");
LOGGER.error(String.format(" TestBoltDouble received partition %d: val %d", partition,val*10));
this.collector.ack(input);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//...
}