Twitter Storm源代碼分析之acker工作流程
網址: 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的詳細工作流程。
源代碼列表
這篇文章涉及到的源代碼主要包括:
算法簡介
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的,如果這樣做的話那麼每發射一個消息會有三條消息了:
- Bolt創建這個tuple的時候,把它發給下一個bolt的消息
-
Bolt創建這個tuple的時候,發送給acker的消息 - 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 ] )) )) |