四、事務拓撲(Transactional Topolgoy)

fan_rockrock發表於2015-12-30

1、問題的提出

    怎樣做到每個出錯的tuple只被處理一次?這樣才能統計所有發射出的tuple的數量。


2、簡介

Storm 0.7.0引入了Transactional Topology, 它可以保證每個tuple”被且僅被處理一次”, 這樣你就可以實現一種非常準確,非常可擴充套件,並且高度容錯方式來實現計數類應用。跟DRPC類似, transactional topology其實不能算是storm的一個特性,它其實是用storm的底層原語spout, bolt, topology, stream等等抽象出來的一個特性。

3、三個設計

  <1>強順序流:順序id的tuple+資料庫

        比如你想統一個stream裡面tuple的總數。那麼為了保證統計數字的準確性,你在資料庫裡面不但要儲存tuple的個數, 還要儲存這個數字所對應的最新的transaction id。 當你的程式碼要到資料庫裡面去更新這個數字的時候,你要判斷只有當新的transaction id跟資料庫裡面儲存的transaction id不一樣的時候才去更新。考慮兩種情況:

  • 資料庫裡面的transaction id跟當前的transaction id不一樣: 由於我們transaction的強順序性,我們知道當前的tuple肯定沒有統計在資料庫裡面。所以我們可以安全地遞增這個數字,並且更新這個transaction id.
  • 資料庫裡面的transaction id一樣: 那麼我們知道當前tuple已經統計在資料庫裡面了,那麼可以忽略這個更新。這個tuple肯定之前在更新了資料庫之後,反饋給storm的時候失敗了(ack超時之類的)。
       缺點:需要等待一個tuple完全處理成功之後才能去處理下一個tuple。這個效能是非常差的。這個需要大量的資料庫呼叫(只要每個tuple一個資料庫呼叫), 而且這個設計也沒有利用到storm的平行計算能力, 所以它的可擴充套件能力是非常差的。

  <2>強順序batch流

    給整個batch一個transaction id,batch與batch之間的處理是強順序性的, 而batch內部是可以並行的

    

優點: 減少資料庫呼叫;利用了storm的平行計算能力(每個batch內部可以並行)

缺點:考慮下面這個topology


在bolt 1完成它的處理之後, 它需要等待剩下的bolt去處理當前batch, 直到發射下一個batch

  <3>storm的設計

  • processing階段: 這個階段很多batch可以平行計算。
  • commit階段: 這個階段各個batch之間需要有強順序性的保證。所以第二個batch必須要在第一個batch成功提交之後才能提交。
這兩個階段合起來稱為一個transaction。許多batch可以在processing階段的任何時刻平行計算,但是隻有一個batch可以處在commit階段。


4、例子

原始碼如下:

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import backtype.storm.Config;
import backtype.storm.LocalCluster;
import backtype.storm.coordination.BatchOutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.testing.MemoryTransactionalSpout;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseBatchBolt;
import backtype.storm.topology.base.BaseTransactionalBolt;
import backtype.storm.transactional.ICommitter;
import backtype.storm.transactional.TransactionAttempt;
import backtype.storm.transactional.TransactionalTopologyBuilder;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Tuple;
import backtype.storm.tuple.Values;


public class TransactionalGlobalCount {
	public static final int PARTITION_TAKE_PER_BATCH=3;
	public static final Map<Integer,List<List<Object>>> DATA=new HashMap<Integer, List<List<Object>>>(){
		{
			put(0,new ArrayList<List<Object>>(){
				{
					add(new Values("cat"));
					add(new Values("dog"));
					add(new Values("chicken"));
					add(new Values("cat"));
					add(new Values("dog"));
					add(new Values("apple"));
				}
			});
			put(1,new ArrayList<List<Object>>(){
				{
					add(new Values("cat"));
					add(new Values("dog"));
					add(new Values("apple"));
					add(new Values("banana"));
				}
			});
			put(2,new ArrayList<List<Object>>(){
				{
					add(new Values("cat"));
					add(new Values("cat"));
					add(new Values("cat"));
					add(new Values("cat"));
					add(new Values("cat"));
					add(new Values("dog"));
					add(new Values("dog"));
					add(new Values("dog"));
					add(new Values("dog"));
				}
			});
		}
	};
	public static class Value{
		int count=0;
		BigInteger txid;
	}
	public static Map<String, Value> DATABASE=new HashMap<String, Value>();
	public static final String GLOBAL_COUNT_KEY="GLOBAL-COUNT";
	
	public static class BatchCount extends BaseBatchBolt{
		Object _id;
		BatchOutputCollector _collector;
		
		int _count=0;
		@Override
		public void prepare(Map conf, TopologyContext context,
				BatchOutputCollector collector, Object id) {
			// TODO Auto-generated method stub
			_collector=collector;
			_id=id;
		}

		@Override
		public void execute(Tuple tuple) {
			// TODO Auto-generated method stub
			_count++;
		}

		@Override
		public void finishBatch() {
			// TODO Auto-generated method stub
			_collector.emit(new Values(_id,_count));
		}

		@Override
		public void declareOutputFields(OutputFieldsDeclarer declarer) {
			// TODO Auto-generated method stub
			declarer.declare(new Fields("id","count"));
		}
		
	}
	public static class UpdateGlobalCount extends BaseTransactionalBolt implements ICommitter {
		TransactionAttempt _attempt;
		BatchOutputCollector _collector;

		int _sum = 0;

		@Override
		public void prepare(Map conf,
                 TopologyContext context,
                 BatchOutputCollector collector,
                 TransactionAttempt attempt) {
			_collector = collector;
			_attempt = attempt;
		}

		@Override
		public void execute(Tuple tuple) {
			_sum+=tuple.getInteger(1);
		}

		@Override
		public void finishBatch() {
			Value val = DATABASE.get(GLOBAL_COUNT_KEY);
			Value newval;
			if(val == null ||
			   !val.txid.equals(_attempt.getTransactionId())) {
				newval = new Value();
				newval.txid = _attempt.getTransactionId();
				if(val==null) {
					newval.count = _sum;
				} else {
					newval.count = _sum + val.count;
				}
				DATABASE.put(GLOBAL_COUNT_KEY, newval);
			} else {
				newval = val;
			}
			_collector.emit(new Values(_attempt, newval.count));
			System.out.println(_attempt);
			System.out.println(newval.count);
		}

		@Override
		public void declareOutputFields(OutputFieldsDeclarer declarer) {
			declarer.declare(new Fields("id", "sum"));
		}
	}
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		MemoryTransactionalSpout spout = new MemoryTransactionalSpout(
		           DATA, new Fields("word"), PARTITION_TAKE_PER_BATCH);
		TransactionalTopologyBuilder builder = new TransactionalTopologyBuilder(
		           "global-count", "spout", spout, 3);
		builder.setBolt("partial-count", new BatchCount(), 5)
		        .shuffleGrouping("spout");
		builder.setBolt("sum", new UpdateGlobalCount())
		        .globalGrouping("partial-count");
		
		LocalCluster cluster=new LocalCluster();
		Config config=new Config();
		config.setDebug(true);
		config.setMaxSpoutPending(3);
		
		cluster.submitTopology("global-count-topology", config, builder.buildTopology());
		
		Thread.sleep(3000);
		cluster.shutdown();
		
	}
}

詳解如下:

<1>構建Topology

                MemoryTransactionalSpout spout = new MemoryTransactionalSpout(
		           DATA, new Fields("word"), PARTITION_TAKE_PER_BATCH);
		TransactionalTopologyBuilder builder = new TransactionalTopologyBuilder(
		           "global-count", "spout", spout, 3);
		builder.setBolt("partial-count", new BatchCount(), 5)
		        .shuffleGrouping("spout");
		builder.setBolt("sum", new UpdateGlobalCount())
		        .globalGrouping("partial-count");</span>

TransactionalTopologyBuilder接受如下的引數

  • 這個transaction topology的id
  • spout在整個topology裡面的id。
  • 一個transactional spout。
  • 一個可選的這個transactional spout的並行度。

一個transaction topology裡面有一個唯一的TransactionalSpout, 這個spout是通過TransactionalTopologyBuilder的建構函式來制定的。在這個例子裡面,MemoryTransactionalSpout被用來從一個記憶體變數裡面讀取資料(DATA)。第二個引數制定資料的fields, 第三個引數指定每個batch的最大tuple數量。

<2>第一個bolt:BatchBolt:隨機地把輸入tuple分給各個task,然後各個task各自統計區域性數量

	public static class BatchCount extends BaseBatchBolt{
		Object _id;
		BatchOutputCollector _collector;
		
		int _count=0;
		@Override
		public void prepare(Map conf, TopologyContext context,
				BatchOutputCollector collector, Object id) {
			// TODO Auto-generated method stub
			_id=id;
		}

		@Override
		public void execute(Tuple tuple) {
			// TODO Auto-generated method stub
			_count++;
		}

		@Override
		public void finishBatch() {
			// TODO Auto-generated method stub
			_collector.emit(new Values(_id,_count));
		}

		@Override
		public void declareOutputFields(OutputFieldsDeclarer declarer) {
			// TODO Auto-generated method stub
			declarer.declare(new Fields("id","count"));
		}
		
	}

storm會為每個batch建立這個一個BatchCount物件。而這些BatchCount是執行在BatchBoltExecutor裡面的。

這個物件的prepare方法接收如下引數:

  • 包含storm config資訊的map。
  • TopologyContext
  • OutputCollector
  • 這個batch的id。而在Transactional Topologies裡面, 這個id則是一個TransactionAttempt物件。

在transaction topology裡面發射的所有的tuple都必須以TransactionAttempt作為第一個field,然後storm可以根據這個field來判斷哪些tuple屬於一個batch。所以你在發射tuple的時候需要滿足這個條件。TransactionAttempt包含兩個值: 一個transaction id,一個attempt id。transaction id的作用就是我們上面介紹的對於每個batch是唯一的,而且不管這個batch replay多少次都是一樣的。 我們可以把attempt id理解成replay-times, storm利用這個id來區別一個batch發射的tuple的不同版本。transaction id對於每個batch加一, 所以第一個batch的transaction id是”1″, 第二個batch是”2″,以此類推。execute方法會為batch裡面的每個tuple執行一次最後, 當這個bolt接收到某個batch的所有的tuple之後, finishBatch方法會被呼叫。這個例子裡面的BatchCount類會在這個時候發射它的區域性數量到它的輸出流裡面去。

<3>第二個bolt:UpdateBlobalCount, 用全域性grouping來從彙總這個batch的總的數量。然後再把總的數量更新到資料庫裡面去。

public static class UpdateGlobalCount extends BaseTransactionalBolt implements ICommitter {
		TransactionAttempt _attempt;
		BatchOutputCollector _collector;

		int _sum = 0;

		@Override
		public void prepare(Map conf,
                 TopologyContext context,
                 BatchOutputCollector collector,
                 TransactionAttempt attempt) {
			_collector = collector;
			_attempt = attempt;
		}

		@Override
		public void execute(Tuple tuple) {
			_sum+=tuple.getInteger(1);
		}

		@Override
		public void finishBatch() {
			Value val = DATABASE.get(GLOBAL_COUNT_KEY);
			Value newval;
			if(val == null ||
			   !val.txid.equals(_attempt.getTransactionId())) {
				newval = new Value();
				newval.txid = _attempt.getTransactionId();
				if(val==null) {
					newval.count = _sum;
				} else {
					newval.count = _sum + val.count;
				}
				DATABASE.put(GLOBAL_COUNT_KEY, newval);
			} else {
				newval = val;
			}
			_collector.emit(new Values(_attempt, newval.count));
		}

UpdateGlobalCount是Transactional Topologies相關的類, 所以它繼承自BaseTransactionalBolt。在execute方法裡面, UpdateGlobalCount累積這個batch的計數, 比較有趣的是finishBatch方法。首先, 注意這個bolt實現了ICommitter介面。這告訴storm要在這個事務的commit階段呼叫finishBatch方法。所以對於finishBatch的呼叫會保證強順序性(順序就是transaction id的升序), 而相對來說execute方法在任何時候都可以執行,processing或者commit階段都可以。UpdateGlobalCount裡面finishBatch方法的邏輯是首先從資料庫中獲取當前的值,並且把資料庫裡面的transaction id與當前這個batch的transaction id進行比較。如果他們一樣, 那麼忽略這個batch。否則把這個batch的結果加到總結果裡面去,並且更新資料庫。



相關文章