摘要
在 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();
}
}
//...
}