Storm - 事務管理

Storm0.7.0實現了一個新特性——事務性拓撲,這一特性使消息在語義上確保你可以安全的方式重發消息,並保證它們只會被處理一次。在不支持事務性拓撲的情況下,你無法在準確性,可擴展性,以空錯性上得到保證的前提下完成計算。

 

NOTE:事務性拓撲是一個構建於標準Storm spoutbolt之上的抽象概念。

設計

在事務性拓撲中,Storm以並行和順序處理混合的方式處理元組。spout並行分批創建供bolt處理的元組(譯者注:下文將這種分批創建、分批處理的元組稱做批次)。其中一些bolt作爲提交者以嚴格有序的方式提交處理過的批次。這意味着如果你有每批五個元組的兩個批次,將有兩個元組被bolt並行處理,但是直到提交者成功提交了第一個元組之後,纔會提交第二個元組。

NOTE: 使用事務性拓撲時,數據源要能夠重發批次,有時候甚至要重複多次。因此確認你的數據源——你連接到的那個spout——具備這個能力。 這個過程可以被描述爲兩個階段: 處理階段 純並行階段,許多批次同時處理。 提交階段 嚴格有序階段,直到批次一成功提交之後,纔會提交批次二。 這兩個階段合起來稱爲一個Storm事務。

NOTE: Storm使用zookeeper儲存事務元數據,默認情況下就是拓撲使用的那個zookeeper。你可以修改以下兩個配置參數鍵指定其它的zookeeper——transactional.zookeeper.servers和transactional.zookeeper.port。


接下來就看看如何在一個事務性拓撲中實現spout

Spout

一個事務性拓撲的spout與標準spout完全不同。

public class TestTransactionalSpout extends BaseTransactionalSpout<TransactionMetadata>{

正如你在這個類定義中看到的,TestTransactionalSpout繼承了帶範型的BaseTransactionalSpout。指定的範型類型的對象是事務元數據集合。


協調者Coordinator
下面是本例的協調者實現。

public static class TestTransactionalSpoutCoordinator implements ITransactionalSpout.Coordinator<TransactionMetadata> {
    TransactionMetadata lastTransactionMetadata;
    

    public TestTransactionalSpoutCoordinator () {
        
    }

    @Override
    public TransactionMetadata initializeTransaction(BigInteger txid, TransactionMetadata prevMetadata) {
        //處理代碼
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void close() {
        
    }
}

值得一提的是,在整個拓撲中只會有一個提交者實例

第一個方法是isReady。在initializeTransaction之前調用它確認數據源已就緒並可讀取。此方法應當相應的返回truefalse

最後,執行initializeTransaction。正如你看到的,它接收txidprevMetadata作爲參數。第一個參數是Storm生成的事務ID,作爲批次的惟一性標識。prevMetadata是協調器生成的前一個事務元數據對象。

在這個例子中,首先確認有多少tweets可讀。只要確認了這一點,就創建一個TransactionMetadata對象。元數據對象一經返回,Storm把它跟txid一起保存在zookeeper。這樣就確保了一旦發生故障,Storm可以利用分發器(譯者注:Emitter,見下文)重新發送批次。

Emitter

創建事務性spout的最後一步是實現分發器(Emitter)。實現如下:

public static class TestTransactionalSpoutEmitter implements ITransactionalSpout.Emitter<TransactionMetadata> {

   public TestTransactionalSpoutEmitter() {}
   @Override
    public void emitBatch(TransactionAttempt tx, TransactionMetadata coordinatorMeta, BatchOutputCollector collector) {
        //處理代碼
        /**
            分發器從數據源讀取數據並從數據流組發送數據。分發器應當問題能夠爲相同的事務id和事務元數據發送相同的批次。這樣,如果在處理批次的過程中發生了故障,Storm就能夠利用分發器重複相同的事務id和事務元數據,並確保批次已經重複過了.
          */
    }

    @Override
    public void cleanupBefore(BigInteger txid) {}

    @Override
    public void close() {
    }</pre>
<pre>
}


在這裏emitBatch是個重要方法。

Bolts

首先看一下這個拓撲中的標準bolt

public class TestSplitterBolt implements IBasicBolt{
    private static final long serialVersionUID = 1L;

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declareStream("users", new Fields("txid","id","test"));
    }

    @Override
    public Map<String, Object> getComponentConfiguration() {
        return null;
    }

    @Override
    public void prepare(Map stormConf, TopologyContext context) {}

    @Override
    public void execute(Tuple input, BasicOutputCollector collector) {
        //業務代碼
    }

    @Override
    public void cleanup(){}
}

TestSplitterBolt接收元組。HashtagSplitterBolt的實現。

public class HashtagSplitterBolt implements IBasicBolt{
    private static final long serialVersionUID = 1L;

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declareStream("hashtags", new Fields("txid","tweet_id","hashtag"));
    }

    @Override
    public Map<String, Object> getComponentConfiguration() {
        return null;
    }

    @Override
    public void prepare(Map stormConf, TopologyContext context) {}

    @Oerride
    public void execute(Tuple input, BasicOutputCollector collector) {
        //業務代碼
    }

    @Override
    public void cleanup(){}
}

現在看看TestHashTagJoinBolt的實現。首先要注意的是它是一個BaseBatchBolt。這意味着,execute方法會操作接收到的元組,但是不會分發新的元組。批次完成時,Storm會調用finishBatch方法。

public void execute(Tuple tuple) {
   //業務代碼
}

在批次處理完成時,調用finishBatch方法。

@Override
public void finishBatch() {
    //後續處理代碼
}


提交者bolts

我們已經學習了,批次通過協調器和分發器怎樣在拓撲中傳遞。在拓撲中,這些批次中的元組以並行的,沒有特定次序的方式處理。


在這裏向數據庫保存提交的最後一個事務ID。爲什麼要這樣做?記住,如果事務失敗了,Storm將會儘可能多的重複必要的次數。如果你不確定已經處理了這個事務,你就會多算,事務拓撲也就沒有用了。所以請記住:保存最後提交的事務ID,並在提交前檢查。

分區的事務Spouts
對一個spout來說,從一個分區集合中讀取批次是很普通的。通過實現IPartitionedTransactionalSpout,Storm提供了一些工具用來管理每個分區的狀態並保證重播的能力。
下面我們修改TestTransactionalSpout,使它可以處理數據分區。
首先,繼承BasePartitionedTransactionalSpout,它實現了IPartitionedTransactionalSpout

public class TestPartitionedTransactionalSpout extends
       BasePartitionedTransactionalSpout<TransactionMetadata> {
...
}

然後告訴Storm誰是你的協調器。

public static class TestPartitionedTransactionalCoordinator implements Coordinator {
    @Override
    public int numPartitions() {
        return 4;
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void close() {}
}

在這個例子裏,協調器很簡單。numPartitions方法,告訴Storm一共有多少分區。而且你要注意,不要返回任何元數據。對於IPartitionedTransactionalSpout,元數據由分發器直接管理。
下面是分發器的實現:

public static class TestPartitionedTransactionalEmitter
       implements Emitter<TransactionMetadata> {

    @Override
    public TransactionMetadata emitPartitionBatchNew(TransactionAttempt tx,
            BatchOutputCollector collector, int partition,
            TransactionMetadata lastPartitioonMeta) {
            //業務處理代碼
    }

    @Override
    public void emitPartitionBatch(TransactionAttempt tx, BatchOutputCollector collector,
            int partition, TransactionMetadata partitionMeta) {
       //業務處理代碼
    }

    @Override
    public void close() {}
}

這裏有兩個重要的方法,emitPartitionBatchNew,和emitPartitionBatch。對於emitPartitionBatchNew,從Storm接收分區參數,該參數決定應該從哪個分區讀取批次。在這個方法中,決定獲取哪些數據,生成相應的元數據對象,調用emitPartitionBatch,返回元數據對象,並且元數據對象會在方法返回時立即保存到zookeeper。
Storm會爲每一個分區發送相同的事務ID,表示一個事務貫穿了所有數據分區。通過emitPartitionBatch讀取分區中的數據,並向拓撲分發批次。如果批次處理失敗了,Storm將會調用emitPartitionBatch利用保存下來的元數據重複這個批次。


模糊的事務性拓撲

到目前爲止,你可能已經學會了如何讓擁有相同事務ID的批次在出錯時重播。但是在有些場景下這樣做可能就不太合適了。然後會發生什麼呢?

事實證明,你仍然可以實現在語義上精確的事務,不過這需要更多的開發工作,你要記錄由Storm重複的事務之前的狀態。既然能在不同時刻爲相同的事務ID得到不同的元組,你就需要把事務重置到之前的狀態,並從那裏繼續。另外,在之前的一個事務被取消時,每個並行處理的事務都要被取消。這是爲了確保你沒有丟失任何數據。

你的spout可以實現IOpaquePartitionedTransactionalSpout,而且正如你看到的,協調器和分發器也很簡單。

public static class TestOpaquePartitionedTransactionalSpoutCoordinator implements IOpaquePartitionedTransactionalSpout.Coordinator {
    @Override
    public boolean isReady() {
        return true;
    }
}

public static class TestOpaquePartitionedTransactionalSpoutEmitter
       implements IOpaquePartitionedTransactionalSpout.Emitter<TransactionMetadata> {
    
    @Override
    public TransactionMetadata emitPartitionBatch(TransactionAttempt tx,
           BatchOutputCollector collector, int partion,
           TransactionMetadata lastPartitonMeta) {
           //處理代碼
        return null;
    }

    private void emitMessage(TransactionAttempt tx, BatchOutputCollector collector,
                 int partition, TransactionMetadata partitionMeta) {
        //處理代碼
    }

    @Override
    public int numPartitions() {
        return 4;
    }

    @Override
    public void close() {}
}

最有趣的方法是emitPartitionBatch,它獲取之前提交的元數據。你要用它生成批次。這個批次不需要與之前的那個一致,你可能根本無法創建完全一樣的批次。剩餘的工作由提交器bolts藉助之前的狀態完成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章