Storm0.7.0實現了一個新特性——事務性拓撲,這一特性使消息在語義上確保你可以安全的方式重發消息,並保證它們只會被處理一次。在不支持事務性拓撲的情況下,你無法在準確性,可擴展性,以空錯性上得到保證的前提下完成計算。
NOTE:事務性拓撲是一個構建於標準Storm spout和bolt之上的抽象概念。
設計
在事務性拓撲中,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之前調用它確認數據源已就緒並可讀取。此方法應當相應的返回true或false。
最後,執行initializeTransaction。正如你看到的,它接收txid和prevMetadata作爲參數。第一個參數是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藉助之前的狀態完成。