网址: 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的详细工作流程。
源代码列表
这篇文章涉及到的源代码主要包括:
- backtype.storm.daemon.acker
- backtype.storm.daemon.task
- 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的,如果这样做的话那么每发射一个消息会有三条消息了:
- 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
]
))
))
|
Pingback 引用通告: Twitter Storm如何保证消息不丢失 | 徐明明
请教个问题哈,Spout发送消息的时候如果需要跟踪ACK的话会emit(new Values(…),msgID)。那么这个msgID是不是发送给了ackBolt,而下游的bolt是获取 不了的呢?
Tuple类里面有个getMessageId的方法。 另外, emit方法似乎不接受msgId参数?
非常感谢。
public List emit(List tuple, Object messageId) {
return emit(Utils.DEFAULT_STREAM_ID, tuple, messageId);
}
这个方法是可以接收Object类型作为messageId的,可是在我的尝试下,这里设定的messageId和在bolt端用tuple.getMessageId方法获取的ID并不相同,是经过了什么处理么?
哪个类里面定义的? 给我github上的源码链接我看下呢。(https://github.com/nathanmarz/storm/blob/master/src/)
https://github.com/nathanmarz/storm/blob/master/src/jvm/backtype/storm/spout/SpoutOutputCollector.java
我在讨论群里问了作者,他的回答是否定的,具体可以看http://groups.google.com/group/storm-user/browse_thread/thread/b4cb4ad3d6899722
恩,我误导你了。。James Xu就是我
Storm的这种ack机制会不会有一定的问题呢?比如:1 ^(异或) 2 ^(异或) 3 = 0
它的id是随机生成的long, 取值空间是2 ^ 64, 出现这种情况的可能性很小。
您好!问个问题,ack或者fail消息是acker异步通知给spout么?spout收到消息后异步处理自己得ack或者fail方法,是么?这两个方法和nextTuple有锁保证么?就是处理完nextTuple再处理ack或者fail,或者反过来?期待您的解答!
谢谢!!
是异步的。互相不干扰的。
请问OutputCollectorImpl在新版本中是否放到Clojure中实现了?因为看到目前所有IOutputCollector的实现的构造函数都需要一个IOutputCollector实例。
另外能否开贴说明下storm源码的目录结构?这对理解storm源码应该能有所帮助,谢谢
不知道是否发表了重复评论,用163的邮箱评论就会报错……
您好,买了您翻译的书 非常不错 ,正在学习中
想请教一下,storm只能够保证每条消息能够完整处理 ,但是不能保证被处理多次吧。因为一个tuple tree 如果只有最后一个节点处理失败 也会导致整棵树重新处理一次是吧。
期待能够解答 谢谢了
同问
应该是不能保证重复处理的。所以有一个叫transactional topology的东西。
上面改成“不能保证不被重复处理”。