【流式計算】Twitter Storm: Transactional Topolgoy簡介

作者: xumingming | 可以轉載, 但必須以超鏈接形式標明文章原始出處和作者信息及版權聲明
網址: http://xumingming.sinaapp.com/736/twitter-storm-transactional-topolgoy/

 
本文翻譯自: https://github.com/nathanmarz/storm/wiki/Transactional-topologies

概述

Storm通過保證每個tuple至少被處理一次來提供可靠的數據處理。關於這一點最常被問到的問題就是“既然tuple可能會被重寫發射(replay), 那麼我們怎麼在storm上面做統計個數之類的事情呢?storm有可能會重複計數吧?”

Storm 0.7.0引入了Transactional Topology, 它可以保證每個tuple”被且僅被處理一次”, 這樣你就可以實現一種非常準確,非常可擴展,並且高度容錯方式來實現計數類應用。

Distributed RPC類似, transactional topology其實不能算是storm的一個特性,它其實是用storm的底層原語spout, bolt, topology, stream等等抽象出來的一個特性。

這篇文章解釋了事務性topology是怎樣的一種抽象,怎樣使用它的api,同時也討論了有關它實現的一些細節。

概念

讓我們一步步地建立transactional topology的抽象。我們先提出一種最簡單的抽象方式, 然後一步步的完善改進,最後介紹storm代碼裏面所使用的抽象方式。

第一個設計: 最簡單的抽象方法

事務性topology背後的核心概念是要在處理數據的提供一個強順序性。這種強順序性最簡單的表現、同時也是我們第一個設計就是:我們每次只處理一個tuple, 除非這個tuple處理成功,否則我們不去處理下一個tuple。

每一個tuple都跟一個transaction id相關聯。如果這個tuple處理失敗了,然後需要重寫發射,那麼它會被重新發射 — 並且附着同樣的transaction id。這裏說的trasaction id其實就是一個數字, 來一個tuple,它就遞增一個。所以第一個tuple的transaction id是1, 第二個tuple的transaction id是2,等等等等。

tuple的強順序性使得我們即使在tuple重發的時候也能夠實現“一次而且只有一次”的語義。 讓我們看個例子:

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

  • 數據庫裏面的transaction id跟當前的transaction id不一樣: 由於我們transaction的強順序性,我們知道當前的tuple肯定沒有統計在數據庫裏面。所以我們可以安全地遞增這個數字,並且更新這個transaction id.
  • 數據庫裏面的transaction id一樣: 那麼我們知道當前tuple已經統計在數據庫裏面了,那麼可以忽略這個更新。這個tuple肯定之前在更新了數據庫之後,反饋給storm的時候失敗了(ack超時之類的)。

這個邏輯以及事務的強順序性保證數據庫裏面的個數(count)即使在tuple被重發的時候也是準確的。這個主意(保存count + transaction-id)是Kafka的開發者在這個設計文檔裏面提出來的。

更進一步來說,這個topology可以在一個事務裏面更新很多不同的狀態,並且可以到達”一次而且只有一次的邏輯”。如果有任何失敗,那麼已經成功的更新你再去更新它會忽略,失敗的更新你去再次更新它則會接受。比如,如果你在處理一個url流,你可以更新每個url的轉發次數, 同時更新每個domain下url的轉發次數。

這個簡單設計有一個很大的問題, 那就是你需要等待一個tuple完全處理成功之後才能去處理下一個tuple。這個性能是非常差的。這個需要大量的數據庫調用(只要每個tuple一個數據庫調用), 而且這個設計也沒有利用到storm的並行計算能力, 所以它的可擴展能力是非常差的。

第二個設計

與每次只處理一個tuple的簡單方案相比, 一個更好的方案是每個transaction裏面處理一批tuple。所以如果你在做一個計數應用, 那麼你每次更新到總數裏面的是這一整個batch的tuple數量。如果這個batch失敗了,那麼你重新replay這整個batch。相應地, 我們不是給每個tuple一個transaction id而是給整個batch一個transaction id,batch與batch之間的處理是強順序性的, 而batch內部是可以並行的。下面這個是設計圖:

transactional-batches

所以如果你每個batch處理1000個tuple的話, 那麼你的應用將會少1000倍的數據庫調用。同時它利用了storm的並行計算能力(每個batch內部可以並行)

雖然這個設計比第一個設計好多了, 它仍然不是一個完美的方案。topology裏面的worker會花費大量的時間等待計算的其它部分完成。 比如看下面的這個計算。

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

第三個設計(storm採用的設計)

一個我們需要意識到的比較重要的問題是,爲了實現transactional的特性,在處理一批tuples的時候,不是所有的工作都需要強順序性的。比如,當做一個全局計數應用的時候, 整個計算可以分爲兩個部分。

  • 計算這個batch的局部數量。
  • 把這個batch的局部數量更新到數據庫裏面去。

其中第二步在多個batch之前需要保證強的順序性, 但是第一步並不許要, 所以我們可以把第一步並行化。所以當第一個batch在更新它的個數進入數據庫的時候,第2到10個batch可以開始計算它們的局部數量了。

Storm通過把一個batch的計算分成兩個階段來實現上面所說的原理:

  • processing階段: 這個階段很多batch可以並行計算。
  • commit階段: 這個階段各個batch之間需要有強順序性的保證。所以第二個batch必須要在第一個batch成功提交之後才能提交。

這兩個階段合起來稱爲一個transaction。許多batch可以在processing階段的任何時刻並行計算,但是隻有一個batch可以處在commit階段。如果一個batch在processing或者commit階段有任何錯誤, 那麼整個transaction需要被replay。

設計細節

當使用Transactional Topologies的時候, storm爲你做下面這些事情:

1) 管理狀態: Storm把所有實現Transactional Topologies所必須的狀態保存在zookeeper裏面。 這包括當前transaction id以及定義每個batch的一些元數據。

2) 協調事務: Storm幫你管理所有事情, 以幫你決定在任何一個時間點是該proccessing還是該committing。

3) 錯誤檢測: Storm利用acking框架來高效地檢測什麼時候一個batch被成功處理了,被成功提交了,或者失敗了。Storm然後會相應地replay對應的batch。你不需要自己手動做任何acking或者anchoring — storm幫你搞定所有事情。

4) 內置的批處理API: Storm在普通bolt之上包裝了一層API來提供對tuple的批處理支持。Storm管理所有的協調工作,包括決定什麼時候一個bolt接收到一個特定transaction的所有tuple。Storm同時也會自動清理每個transaction所產生的中間數據。

5) 最後,需要注意的一點是Transactional Topologies需要一個可以完全重發(replay)一個特定batch的消息的隊列系統(Message Queue)。Kestrel之類的技術做不到這一點。而Apache的Kafka對於這個需求來說是正合適的。storm-contrib裏面的storm-kafka實現了這個。

一個基本的例子

你可以通過使用TransactionalTopologyBuilder來創建transactional topology. 下面就是一個transactional topology的定義, 它的作用是計算輸入流裏面的tuple的個數。這段代碼來自storm-starter裏面的TransactionalGlobalCount

1
2
3
4
5
6
7
8
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");

TransactionalTopologyBuilder接受如下的參數

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

topology的id是用來在zookeeper裏面保存這個topology的當前進度的,所以如果你重啓這個topology, 它可以接着前面的進度繼續執行。

一個transaction topology裏面有一個唯一的TransactionalSpout, 這個spout是通過TransactionalTopologyBuilder的構造函數來制定的。在這個例子裏面,MemoryTransactionalSpout被用來從一個內存變量裏面讀取數據(DATA)。第二個參數制定數據的fields, 第三個參數指定每個batch的最大tuple數量。關於如何自定義TransactionalSpout我們會在後面介紹。

現在說說 bolts。這個topology並行地計算tuple的總數量。第一個bolt:BatchBolt,隨機地把輸入tuple分給各個task,然後各個task各自統計局部數量。第二個bolt:UpdateBlobalCount, 用全局grouping來從彙總這個batch的總的數量。然後再把總的數量更新到數據庫裏面去。

下面是BatchCount的定義:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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) {
        _collector = collector;
        _id = id;
    }
 
    @Override
    public void execute(Tuple tuple) {
        _count++;
    }
 
    @Override
    public void finishBatch() {
        _collector.emit(new Values(_id, _count));
    }
 
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("id", "count"));
    }
}

storm會爲每個batch創建這個一個BatchCount對象。而這些BatchCount是運行在BatchBoltExecutor裏面的。而BatchBoltExecutor負責創建以及清理這個對象的實例。

這個對象的prepare方法接收如下參數:

  • 包含storm config信息的map。
  • TopologyContext
  • OutputCollector
  • 這個batch的id。而在Transactional Topologies裏面, 這個id則是一個TransactionAttempt對象。

這個batch bolt的抽象在DRPC裏面也可以用, 只是id的類型不一樣而已。BatchBolt其實真的接收一個id類型的參數 — 它是一個java模板類,所以如果你只是想在transactioinal topology裏面使用這個BatchBolt,你可以這樣定義:

1
2
3
public abstract class BaseTransactionalBolt
       extends BaseBatchBolt<TransactionAttempt> {
}

在transaction topology裏面發射的所有的tuple都必須以TransactionAttempt作爲第一個field, 然後storm可以根據這個field來判斷哪些tuple屬於一個batch。所以你在發射tuple的時候需要滿足這個條件。

TransactionAttempt包含兩個值: 一個transaction id,一個attempt id。transaction id的作用就是我們上面介紹的對於每個batch是唯一的,而且不管這個batchreplay多少次都是一樣的。attempt id是對於每個batch唯一的一個id, 但是對於統一個batch,它replay之後的attempt id跟replay之前就不一樣了, 我們可以把attempt id理解成replay-times, storm利用這個id來區別一個batch發射的tuple的不同版本。

transaction id對於每個batch加一, 所以第一個batch的transaction id是”1″, 第二個batch是”2″,以此類推。

execute方法會爲batch裏面的每個tuple執行一次,你應該把這個batch裏面的狀態保持在一個本地變量裏面。對於這個例子來說, 它在execute方法裏面遞增tuple的個數。

最後, 當這個bolt接收到某個batch的所有的tuple之後, finishBatch方法會被調用。這個例子裏面的BatchCount類會在這個時候發射它的局部數量到它的輸出流裏面去。

下面是UpdateGlobalCount類的定義。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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));
    }
 
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("id", "sum"));
    }
}

UpdateGlobalCount是Transactional Topologies相關的類, 所以它繼承自BaseTransactionalBolt。在execute方法裏面, UpdateGlobalCount累積這個batch的計數, 比較有趣的是finishBatch方法。

首先, 注意這個bolt實現了ICommitter接口。這告訴storm要在這個事務的commit階段調用finishBatch方法。所以對於finishBatch的調用會保證強順序性(順序就是transaction id的升序), 而相對來說execute方法在任何時候都可以執行,processing或者commit階段都可以。另外一種把bolt標識爲commiter的方法是調用TransactionalTopologyBuildersetCommiterBolt來添加Bolt(而不是setBolt)。

UpdateGlobalCount裏面finishBatch方法的邏輯是首先從數據庫中獲取當前的值,並且把數據庫裏面的transaction id與當前這個batch的transaction id進行比較。如果他們一樣, 那麼忽略這個batch。否則把這個batch的結果加到總結果裏面去,並且更新數據庫。

關於transactional topology的更深入的例子可以卡看storm-starter裏面的TransactionalWords類, 這個類裏面會在一個事務裏面更新多個數據庫。

Transactional Topology API

這一節介紹Transaction topology API

Bolts

在一個transactional topology裏面最多有三種類型的bolt:

  • BasicBolt: 這個bolt不跟batch的tuple打交道,它只基於單個tuple的輸入來發射新的tuple。
  • BatchBolt: 這個bolt處理batch在一起的tuples。對於每一個tuple調用execute方法。而在整個batch處理完成的時候調用finishBatch方法
  • 被標記成Committer的BatchBolt: 和普通的BatchBolt的唯一的區別是finishBatch這個方法被調用的時機。作爲committer的BatchBolt的finishBatch方法在commit階段調用。一個batch的commit階段由storm保證只在前一個batch成功提交之後纔會執行。並且它會重試直到topology裏面的所有bolt在commit完成提交。有兩個方法可以讓一個普通BatchBolt變成committer: 1) 實現ICommitter接口 2) 通過TransactionalTopologyBuilder的setCommitterBolt方法把BatchBolt添加到topology裏面去。

    Processing phase vs. commit phase in bolts

    爲了搞清除processing階段與commit階段的區別, 讓我們看個例子:

    在這個topology裏面只有用紅線標出來的是committers。

    在processing階段, bolt A會處理從spout發射出來的整個batch。並且發射tuple給bolt B和bolt C。Bolt B是一個committer, 所以它會處理所有的tuple, 但是不會調用finishBatch方法。Bolt C同樣也不會調用finishBatch方法, 它的原因是:它不知道它有沒有從Bolt B接收到所有的tuple。(因爲Bolt B還在等着事務提交)最後Bolt D會接收到Bolt C在調用execute方法的時候發射的所有的tuple。

    當batch提交的時候, Bolt B上的finishBatch被調用。Bolt C現在可以判斷它接收到了所有的tuple, 所以可以調用finishBatch了。最後Bolt D接收到了它的所有的tuple所以就調用finishBatch了。

    要注意的是,雖然Bolt D是一個committer, 它在接收到整個batch的tuple之後不需要等待第二個commit信號。因爲它是在commit階段接收到的整個batch,它會調用finishBatch來完成整個事務。

    Acking

    注意, 你不需要顯式地去做任何的acking或者anchoring。storm在背後都做掉了。(storm對transactional topolgies裏面的acking機制進行了高度的優化)

    Failing a transaction

    在使用普通bolt的時候, 你可以通過調用OutputCollector的fail方法來fail這個tuple所在的tuple樹。由於Transactional Topologies把acking框架從用戶的視野裏面隱藏掉了, 它提供一個不同的機制來fail一個batch(從而使得這個batch被replay)。只要拋出一個FailedException就可以了。跟普通的異常不一樣, 這個異常只會導致當前的batch被replay, 而不會使整個進程crash掉。

    Transactional spout

    TransactionalSpout接口跟普通的Spout接口完全不一樣。一個TransactionalSpout的實現一個batch一個batch的tuple, 而且必須保證同一個batch的transaction id始終一樣。

    在transactional topology中運行的時候, transactional spout看起來是這樣的一個結構:

    在圖的左邊的coordinator是一個普通的storm的spout — 它一直爲事務的batch發射tuple。Emitter則像一個普通的storm bolt,它負責爲每個batch實際發射tuple。emitter以all grouping的方式訂閱coordinator的”batch emit”流。

    由於TransactionalSpout發射的tuple可能需要會被replay, 因此需要具有冪等性(否則多次replay同一個tuple會使得最後的結果不對), 爲了實現冪等性,需要保存Transactional Spout的少量的狀態,這個狀態是保存在ZooKeeper裏面的。

    關於如何實現一個TransactionalSpout的細節可以參見Javadoc

    Partitioned Transactional Spout

    一種常見的TransactionalSpout是那種從多個queue broker奪取數據然後再發射的tuple。比如TransactionalKafkaSpout是這樣工作的。IPartitionedTransactionalSpout把這些管理每個分區的狀態以保證可以replay的冪等性的工作都自動化掉了。更多可以參考Javadoc

    配置

    Transactional Topologies有兩個重要的配置:

    • Zookeeper: 默認情況下,transactional topology會把狀態信息保存在主zookeeper裏面(協調集羣的那個)。你可以通過這兩個配置來指定其它的zookeeper:”transactional.zookeeper.servers” 和 “transactional.zookeeper.port“。
    • 同時活躍的batch數量:你必須設置同時處理的batch數量。你可以通過”topology.max.spout.pending” 來指定, 如果你不指定,默認是1。

    實現

    Transactional Topologies的實現是非常優雅的。管理提交協議,檢測失敗並且串行提交看起來很複雜,但是使用storm的原語來進行抽象是非常簡單的。

    • transactional topology裏面的spout是一個子topology, 它由一個spout和一個bolt組成。
      • spout是協調者,它只包含一個task。
      • bolt是發射者
      • bolt以all grouping的方式訂閱協調者的輸出。
      • 元數據的序列化用的是kryo。
    • 協調者使用acking框架來決定什麼時候一個batch被成功執行完成,然後去決定一個batch什麼時候被成功提交。
    • 狀態信息被以RotatingTransactionalState的形式保存在zookeeper裏面了。
    • commiting bolts以all grouping的方式訂閱協調者的commit流。
    • CoordinatedBolt被用來檢測一個bolt是否收到了一個特定batch的所有tuple。
      • 這一點上面跟DRPC裏面是一樣的。
      • 對於commiting bolt來說, 他會一直等待, 知道從coordinator的commit流裏面接收到一個tuple之後,它纔會調用finishBatch方法。
      • 所以在沒有從coordinator的commit流接收到一個tuple之前,committing bolt不可能調用finishBolt方法。

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