Twitter Storm源代碼分析之acker工作流程

Twitter Storm源代碼分析之acker工作流程

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

 

概述

我們知道storm一個很重要的特性是它能夠保證你發出的每條消息都會被完整處理, 完整處理的意思是指:

一個tuple被完全處理的意思是: 這個tuple以及由這個tuple所導致的所有的tuple都被成功處理。而一個tuple會被認爲處理失敗瞭如果這個消息在timeout所指定的時間內沒有成功處理。

也就是說對於任何一個spout-tuple以及它的所有子孫到底處理成功失敗與否我們都會得到通知。關於如果做到這一點的原理,可以看看Twitter Storm如何保證消息不丟失這篇文章。從那篇文章裏面我們可以知道,storm裏面有個專門的acker來跟蹤所有tuple的完成情況。這篇文章就來討論acker的詳細工作流程。

源代碼列表

這篇文章涉及到的源代碼主要包括:

  1. backtype.storm.daemon.acker
  2. backtype.storm.daemon.task
  3. backtype.storm.task.OutputCollectorImpl

算法簡介

acker對於tuple的跟蹤算法是storm的主要突破之一, 這個算法使得對於任意大的一個tuple樹, 它只需要恆定的20字節就可以進行跟蹤了。原理很簡單:acker對於每個spout-tuple保存一個ack-val的校驗值,它的初始值是0, 然後每發射一個tuple/ack一個tuple,那麼tuple的id都要跟這個校驗值異或一下,並且把得到的值更新爲ack-val的新值。那麼假設每個發射出去的tuple都被ack了, 那麼最後ack-val一定是0(因爲一個數字跟自己異或得到的值是0)。

進入正題

那麼下面我們從源代碼層面來看看哪些組件在哪些時候會給acker發送什麼樣的消息來共同完成這個算法的。acker對消息進行處理的主要是下面這塊代碼:

01
02
03
04
05
06
07
08
09
10
11
(let [id (.getValue tuple 0)
   ^TimeCacheMap pending @pending
   curr (.get pending id)
   curr (condp = (.getSourceStreamId tuple)
        ACKER-INIT-STREAM-ID (-> curr
               (update-ack id)
               (assoc :spout-task (.getValue tuple 1)))
        ACKER-ACK-STREAM-ID (update-ack
                         curr (.getValue tuple 1))
        ACKER-FAIL-STREAM-ID (assoc curr :failed true))]
            ...)

Spout創建一個新的tuple的時候給acker發送消息

消息格式(看上面代碼的第1行和第7行對於tuple.getValue()的調用)

1
(spout-tuple-id, task-id)

消息的streamId是__ack_init(ACKER-INIT-STREAM-ID)

這是告訴acker, 一個新的spout-tuple出來了, 你跟蹤一下,它是由id爲task-id的task創建的(這個task-id在後面會用來通知這個task:你的tuple處理成功了/失敗了)。處理完這個消息之後, acker會在它的pending這個map(類型爲TimeCacheMap)裏面添加這樣一條記錄:

1
{spout-tuple-id {:spout-task task-id :val ack-val)}

這就是acker對spout-tuple進行跟蹤的核心數據結構, 對於每個spout-tuple所產生的tuple樹的跟蹤都只需要保存上面這條記錄。acker後面會檢查:val什麼時候變成0,變成0, 說明這個spout-tuple產生的tuple都處理完成了。

Bolt發射一個新tuple的時候會給acker發送消息麼?

任何一個bolt在發射一個新的tuple的時候,是不會直接通知acker的,如果這樣做的話那麼每發射一個消息會有三條消息了:

  1. Bolt創建這個tuple的時候,把它發給下一個bolt的消息
  2. Bolt創建這個tuple的時候,發送給acker的消息
  3. ack tuple的時候發送的ack消息

事實上storm裏面只有第一條和第三條消息,它把第二條消息省掉了, 怎麼做到的呢?storm這點做得挺巧妙的,bolt在發射一個新的bolt的時候會把這個新tuple跟它的父tuple的關係保存起來。然後在ack每個tuple的時候,storm會把要ack的tuple的id, 以及這個tuple新創建的所有的tuple的id的異或值發送給acker。這樣就給每個tuple省掉了一個消息(具體看下一節)。

Tuple被ack的時候給acker發送消息

每個tuple在被ack的時候,會給acker發送一個消息,消息格式是:

1
(spout-tuple-id, tmp-ack-val)

消息的streamId是__ack_ack(ACKER-ACK-STREAM-ID)

注意,這裏的tmp-ack-val是要ack的tuple的id與由它新創建的所有的tuple的id異或的結果:

1
tuple-id ^ (child-tuple-id1 ^ child-tuple-id2 ... )

我們可以從task.clj裏面的send-ack方法看出這一點:

01
02
03
04
05
06
07
08
09
10
11
12
13
(defn- send-ack [^TopologyContext topology-context
                          ^Tuple input-tuple
                          ^List generated-ids send-fn]
  (let [ack-val (bit-xor-vals generated-ids)]
    (doseq [
      [anchor id] (.. input-tuple
                      getMessageId
                      getAnchorsToIds)]
      (send-fn (Tuple. topology-context
                 [anchor (bit-xor ack-val id)]
                 (.getThisTaskId topology-context)
                 ACKER-ACK-STREAM-ID))
      )))

這裏面的generated-ids參數就是這個input-tuple的所有子tuple的id, 從代碼可以看出storm會給這個tuple的每一個spout-tuple發送一個ack消息。

爲什麼說這裏的generated-ids是input-tuple的子tuple呢? 這個send-ack是被OutputCollectorImpl裏面的ack方法調用的:

1
2
3
4
5
6
7
public void ack(Tuple input) {
    List generated = getExistingOutput(input);
    // don't just do this directly in case
    // there was no output
    _pendingAcks.remove(input);
    _collector.ack(input, generated);
}

generated是由getExistingOutput(input)方法計算出來的, 我們再來看看這個方法的定義:

1
2
3
4
5
6
7
8
9
private List getExistingOutput(Tuple anchor) {
    if(_pendingAcks.containsKey(anchor)) {
        return _pendingAcks.get(anchor);
    } else {
        List ret = new ArrayList();
        _pendingAcks.put(anchor, ret);
        return ret;
    }
}

_pendingAcks裏面存的是什麼東西呢?

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
private Tuple anchorTuple(Collection< Tuple > anchors,
                                String streamId,
                                List< Object > tuple) {
    // The simple algorithm in this function is the key
    // to Storm. It is what enables Storm to guarantee
    // message processing.
    // 這個map存的東西是 spout-tuple-id到ack-val的映射
    Map< Long, Long > anchorsToIds
                       = new HashMap<Long, Long>();
    // anchors 其實就是它的所有父親:spout-tuple
    if(anchors!=null) {
        for(Tuple anchor: anchors) {
            long newId = MessageId.generateId();
            // 告訴每一個父親,你們又多了一個兒子了。
            getExistingOutput(anchor).add(newId);
            for(long root: anchor.getMessageId()
                          .getAnchorsToIds().keySet()) {
                Long curr = anchorsToIds.get(root);
                if(curr == null) curr = 0L;
 
                // 更新spout-tuple-id的ack-val
                anchorsToIds.put(root, curr ^ newId);
            }
        }
    }
    return new Tuple(_context, tuple,
                    _context.getThisTaskId(),
                    streamId,
                    MessageId.makeId(anchorsToIds));
}

從上面代碼裏面的紅色部分我們可以看出, _pendingAcks裏面維護的其實就是tuple到自己兒子的對應關係。

Tuple處理失敗的時候會給acker發送失敗消息

acker會忽略這種消息的消息內容(消息的streamId爲ACKER-FAIL-STREAM-ID), 直接將對應的spout-tuple標記爲失敗(最上面代碼第9行)

最後Acker發消息通知spout-tuple對應的Worker

最後, acker會根據上面這些消息的處理結果來通知這個spout-tuple對應的task:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(when (and curr
          (:spout-task curr))
 (cond (= 0 (:val curr))
       ;; ack-val == 0 說明這個tuple的所有子孫都
       ;; 處理成功了(都發送ack消息了)
       ;; 那麼發送成功消息通知創建這個spout-tuple的task.
       (do
         (.remove pending id)
         (acker-emit-direct @output-collector
                            (:spout-task curr)
                            ACKER-ACK-STREAM-ID
                            [id]
                            ))
       ;; 如果這個spout-tuple處理失敗了
       ;; 發送失敗消息給創建這個spout-tuple的task
       (:failed curr)
       (do
         (.remove pending id)
         (acker-emit-direct @output-collector
                            (:spout-task curr)
                            ACKER-FAIL-STREAM-ID
                            [id]
                            ))
       ))
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章