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 ]
                             ))
        ))
此条目发表在  clojure,  java,  storm,  源代码分析 分类目录。将 固定链接加入收藏夹。

Twitter Storm源代码分析之acker工作流程》有 18 条评论

  1. Pingback 引用通告: Twitter Storm如何保证消息不丢失 | 徐明明

  2. fiw  说:

    请教个问题哈,Spout发送消息的时候如果需要跟踪ACK的话会emit(new Values(…),msgID)。那么这个msgID是不是发送给了ackBolt,而下游的bolt是获取 不了的呢?

    回复
    • xumingming  说:

      Tuple类里面有个getMessageId的方法。 另外, emit方法似乎不接受msgId参数?

      回复
      • fiw  说:

        非常感谢。
        public List emit(List tuple, Object messageId) {
        return emit(Utils.DEFAULT_STREAM_ID, tuple, messageId);
        }
        这个方法是可以接收Object类型作为messageId的,可是在我的尝试下,这里设定的messageId和在bolt端用tuple.getMessageId方法获取的ID并不相同,是经过了什么处理么?

        回复
        • xumingming  说:

          哪个类里面定义的? 给我github上的源码链接我看下呢。(https://github.com/nathanmarz/storm/blob/master/src/)

          回复
          • fiw  说:

            https://github.com/nathanmarz/storm/blob/master/src/jvm/backtype/storm/spout/SpoutOutputCollector.java

  3. fiw  说:

    我在讨论群里问了作者,他的回答是否定的,具体可以看http://groups.google.com/group/storm-user/browse_thread/thread/b4cb4ad3d6899722

    回复
    • xumingming  说:

      恩,我误导你了。。James Xu就是我

      回复
  4. debugcool  说:

    Storm的这种ack机制会不会有一定的问题呢?比如:1 ^(异或) 2 ^(异或) 3 = 0

    回复
    • xumingming  说:

      它的id是随机生成的long, 取值空间是2 ^ 64, 出现这种情况的可能性很小。

      回复
  5. tianzhu  说:

    您好!问个问题,ack或者fail消息是acker异步通知给spout么?spout收到消息后异步处理自己得ack或者fail方法,是么?这两个方法和nextTuple有锁保证么?就是处理完nextTuple再处理ack或者fail,或者反过来?期待您的解答!

    谢谢!!

    回复
    • xumingming  说:

      是异步的。互相不干扰的。

      回复
  6. mopishv0  说:

    请问OutputCollectorImpl在新版本中是否放到Clojure中实现了?因为看到目前所有IOutputCollector的实现的构造函数都需要一个IOutputCollector实例。
    另外能否开贴说明下storm源码的目录结构?这对理解storm源码应该能有所帮助,谢谢

    回复
  7. mopishv0  说:

    不知道是否发表了重复评论,用163的邮箱评论就会报错……

    回复
  8. 吴刚  说:

    您好,买了您翻译的书 非常不错 ,正在学习中
    想请教一下,storm只能够保证每条消息能够完整处理 ,但是不能保证被处理多次吧。因为一个tuple tree 如果只有最后一个节点处理失败 也会导致整棵树重新处理一次是吧。
    期待能够解答 谢谢了

    回复
    • habren  说:

      同问

      回复
    • fubupc  说:

      应该是不能保证重复处理的。所以有一个叫transactional topology的东西。

      回复
      • fubupc  说:

        上面改成“不能保证不被重复处理”。

        回复

你可能感兴趣的:(Twitter Storm源代码分析之acker工作流程)