Apache Storm 实时流处理系统ACK机制以及源码分析

1.ACK机制简介

        Storm的可靠性是指Storm会告知用户每一个消息单元是否在一个指定的时间(timeout)内被完全处理。完全处理的意思是该MessageId绑定的源Tuple以及由该源Tuple衍生的所有Tuple都经过了Topology中每一个应该到达的Bolt的处理。
注: timetout 可以通过Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS 来指定
         Storm中的每一个Topology中都包含有一个Acker组件。Acker组件的任务就是跟踪从某个task中的Spout流出的每一个messageId所绑定的Tuple树中的所有Tuple的处理情况。如果在用户设置的最大超时时间内这些Tuple没有被完全处理,那么Acker会告诉Spout该消息处理失败,相反则会告知Spout该消息处理成功,它会分别调用Spout中的fail和ack方法。

      Storm允许用户在Spout中发射一个新的源Tuple时为其指定一个MessageId,这个MessageId可以是任意的Object对象。多个源Tuple可以共用同一个MessageId,表示这多个源Tuple对用户来说是同一个消息单元,它们会被放到同一棵tuple树中,如下图所示:


        在Spout中由message 1绑定的tuple1和tuple2分别经过bolt1和bolt2的处理,然后生成了两个新的Tuple,并最终流向了bolt3。当bolt3处理完之后,称message 1被完全处理了。

2. Acker原理分析

        storm里面有一类特殊的task称为Acker(acker bolt), 负责跟踪spout发出的每一个tuple的tuple树。当Acker发现一个tuple树已经处理完成了。它会发送一个消息给产生这个tuple的那个task。你可以通过Config.TOPOLOGY_ACKERS来设置一个topology里面的acker的数量, 默认值是1。 如果你的topology里面的tuple比较多的话, 那么把acker的数量设置多一点,效率会高一点。

       理解storm的可靠性的最好的方法是来看看tuple和tuple树的生命周期, 当一个tuple被创建, 不管是spout还是bolt创建的, 它会被赋予一个64位的id,而acker就是利用这个id去跟踪所有的tuple的。每个tuple知道它的祖宗的id(从spout发出来的那个tuple的id), 每当你新发射一个tuple, 它的祖宗id都会传给这个新的tuple。所以当一个tuple被ack的时候,它会发一个消息给acker,告诉它这个tuple树发生了怎么样的变化。具体来说就是它告诉acker:  我已经完成了, 我有这些儿子tuple, 你跟踪一下他们吧。
                                  (spout-tuple-id, tmp-ack-val)
                 tmp-ark-val =  tuple-id ^ (child-tuple-id1 ^ child-tuple-id2 ... )

        tmp-ack-val是要ack的tuple的id与由它新创建的所有的tuple的id异或的结果

        当一个tuple需要ack的时候,它到底选择哪个acker来发送这个信息呢?
       storm使用 一致性哈希来把一个spout-tuple-id对应到acker, 因为每一个tuple知道它所有的祖宗的tuple-id, 所以它自然可以算出要通知哪个Acker来ack。

        注:一个tuple可能存在于多个tuple树,所有可能存在多个祖宗的tuple-id

        acker是怎么知道每一个spout tuple应该交给哪个task来处理?

       当一个spout发射一个新的tuple, 它会简单的发一个消息给一个合适的acker,并且告诉acker它自己的id(taskid), 这样storm就有了taskid-tupleid的对应关系。 当acker发现一个树完成处理了, 它知道给哪个task发送成功的消息。

3.Acker的高效性

         acker task并不显式的跟踪tuple树。对于那些有成千上万个节点的tuple树,把这么多的tuple信息都跟踪起来会耗费太多的内存。相反, acker用了一种不同的方式, 使得对于每个spout tuple所需要的内存量是恒定的(20 bytes) .  这个跟踪算法是storm如何工作的关键,并且也是它的主要突破。

        一个acker task存储了一个spout-tuple-id到一对值的一个mapping。这个对子的第一个值是创建这个tuple的taskid, 这个是用来在完成处理tuple的时候发送消息用的。 第二个值是一个64位的数字称作:ack val, ack val是整个tuple树的状态的一个表示,不管这棵树多大。它只是简单地把这棵树上的所有创建的tupleid/ack的tupleid一起异或(XOR)。
          
          当一个acker task 发现一个 ack val变成0了, 它知道这棵树已经处理完成了。

 例如下图是一个简单的Topology。


        ack_val的初值为0,varl_x表示新产生的tuple id ,它们经过Spout,Bolt1,Bolt2,Bolt3 处理,并与arv_val异或,最终arv_val变为0,表示tuple1被成功处理。

   下面看一个稍微复杂一点的例子:


        msg1绑定了两个源tuple,它们的id分别为1001和1010.在经过Bolt1处理后新生成了tuple id为1110,新生成的tuple与传入的tuple 1001进行异或得到的值为0111,然后Bolt1通过spout-tuple-id映射到指定的Acker组件,向它发送消息,Acker组件将Bolt1传过来的值与ack_val异或,更新ack_val的值变为了0100。与此相同经过Bolt2处理后,ack_val的值变为0001。最后经Bolt3处理后ack_val的值变为了0,说明此时由msg1标识的Tuple处理成功,此时Acker组件会通过事先绑定的task id映射找到对应的Spout,然后调用该Spout的ack方法。

            其流程如下图所示:


4.ACK机制配置

        Acker task是非常轻量级的, 所以一个topology里面不需要很多acker。你可以通过Strom UI(id: -1)来跟踪它的性能。 如果它的吞吐量看起来不正常,那么你就需要多加点acker了。

       如果可靠性对你来说不是那么重要 — 你不太在意在一些失败的情况下损失一些数据, 那么你可以通过不跟踪这些tuple树来获取更好的性能。不去跟踪消息的话会使得系统里面的消息数量减少一半, 因为对于每一个tuple都要发送一个ack消息。并且它需要更少的id来保存下游的tuple, 减少带宽占用。

 有三种方法可以去掉可靠性:
       1.第一是把Config.TOPOLOGY_ACKERS 设置成 0. 在这种情况下, storm会在spout发射一个tuple之后马上调用spout的ack方法。也就是说这个tuple树不会被跟踪。
      2.第二个方法是在tuple层面去掉可靠性。 你可以在发射tuple的时候不指定Messageid来达到不跟踪某个特定的spout tuple的目的。

     3.最后一个方法是如果你对于一个tuple树里面的某一部分到底成不成功不是很关心,那么可以在发射这些tuple的时候unanchor它们。 这样这些tuple就不在tuple树里面, 也就不会被跟踪了。

5.ACK机制源码分析

        下面我们来分析一下Apache Storm 2.0.0-SNAPSHOT的ACK机制源码分析。

5.1 Acker类业务逻辑

        Acker Bolt就是通过不断更新和检测跟踪值来判断该消息是否已经被完全成功处理的。RotatingMap主要用于消息的超时。AckerBolt不断的接受来自各个地方发送过来的Tuple,并且根据Tuple的getSourceStreamId 进行不同的逻辑操作。

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.storm.daemon;

public class Acker implements IBolt {
    private static final Logger LOG = LoggerFactory.getLogger(Acker.class);

    private static final long serialVersionUID = 4430906880683183091L;

    public static final String ACKER_COMPONENT_ID = "__acker";
    public static final String ACKER_INIT_STREAM_ID = "__ack_init";
    public static final String ACKER_ACK_STREAM_ID = "__ack_ack";
    public static final String ACKER_FAIL_STREAM_ID = "__ack_fail";
    public static final String ACKER_RESET_TIMEOUT_STREAM_ID = "__ack_reset_timeout";

    public static final int TIMEOUT_BUCKET_NUM = 3;

    private OutputCollector collector;
    private RotatingMap<Object, AckObject> pending;

    private static class AckObject {
        public long val = 0L;
        public long startTime = Time.currentTimeMillis();
        public int spoutTask = -1;
        public boolean failed = false;

        // val xor value
        public void updateAck(Long value) {
            val = Utils.bitXor(val, value);
        }
    }

    @Override
    public void prepare(Map<String, Object> topoConf, TopologyContext context, OutputCollector collector) {
        this.collector = collector;
        this.pending = new RotatingMap<>(TIMEOUT_BUCKET_NUM);
    }

    @Override
    public void execute(Tuple input) {
        /**
         *  Storm ACK 源码分析
         *  1.。Acker Bolt就是通过不断更新和检测跟踪值来判断该消息是否已经被完全成功处理的。RotatingMap主要用于消息的超时。AckerBolt不断的接受
         *  来自各个地方发送过来的Tuple,并且根据Tuple的getSourceStreamId 进行不同的逻辑操作
         */
        if (TupleUtils.isTick(input)) {
            /**
             * 系统预定义流,用于消息超时。Acker Bolt会对成员变量pending进行旋转操作,然后退出execute方法,该操作将pending中最早的一个桶中的数据删除掉,于是实现了。
             * 消息的超时。由于初始化RotatingMap时,未传入关于expire的回调方法,故该操作只是进行简单的删除。如果继续对已经删除掉的消息的Rootld进行Ack操作,就会创建新的
             * <RootId,跟踪值>对,但是由于数据已被删除过的原因,跟踪值基本上不会再回到零,所以Spout将永远也收不到它发送出去的这条消息的Ack。Spout会通过自有的超时机制,
             * 将这条消息标记为处理失败,然后调用Spout的失败函数来决定对失败消息进行重传还是忽略。这个操作的结果是去除处于僵死状态的消息跟踪。
              */
            Map<Object, AckObject> tmp = pending.rotate();
            LOG.debug("Number of timeout tuples:{}", tmp.size());
            return;
        }

        boolean resetTimeout = false;
        String streamId = input.getSourceStreamId();
        Object id = input.getValue(0);
        AckObject curr = pending.get(id);
        if (ACKER_INIT_STREAM_ID.equals(streamId)) {
            /**
             * Acker中AckObject初始化操作 Spout输入消息的模式为<RootId,RawAckValue,SpoutTaskId> AckerBolt会根据Rootld取出<RootId, AckValue> ,并进行更新操作以及设置
             * spoutTaskId
             */
            if (curr == null) {
                curr = new AckObject();
                pending.put(id, curr);
            }
            curr.updateAck(input.getLong(1));
            curr.spoutTask = input.getInteger(2);
        } else if (ACKER_ACK_STREAM_ID.equals(streamId)) {
            /**
             * ID:输人消息的模式为<RootId,AckValue〉,与原有AckValue进行异或操作并存储。
             */
            if (curr == null) {
                curr = new AckObject();
                pending.put(id, curr);
            }
            curr.updateAck(input.getLong(1));
        } else if (ACKER_FAIL_STREAM_ID.equals(streamId)) {
            // For the case that ack_fail message arrives before ack_init
            /**
             * Acker收到Bolt或者Spout发送过来的Fail消息。输入消息的模式为< RootId >。设置failed 为true, 表示消息的处理已经失败。
             */
            if (curr == null) {
                curr = new AckObject();
            }
            curr.failed = true;
            pending.put(id, curr);
        } else if (ACKER_RESET_TIMEOUT_STREAM_ID.equals(streamId)) {
            /**
             * AckerBolt 收到消息超时
             */
            resetTimeout = true;
            if (curr != null) {
                pending.put(id, curr);
            } //else if it has not been added yet, there is no reason time it out later on
        } else {
            LOG.warn("Unknown source stream {} from task-{}", streamId, input.getSourceTask());
            return;
        }

        int task = curr.spoutTask;
        if (curr != null && task >= 0
            && (curr.val == 0 || curr.failed || resetTimeout)) {
            Values tuple = new Values(id, getTimeDeltaMillis(curr.startTime));
            //若此时消息对应的跟踪值已经为零,那么Storm认为该消息以及所有衍生的消息都已被成功处理,这时会通过向ACK - STREAM流向Spout节点发送消息,模式为<RootId>
            if (curr.val == 0) {
                pending.remove(id);
                collector.emitDirect(task, ACKER_ACK_STREAM_ID, tuple);
            } else if (curr.failed) {
            //若此时消息被标记为失败,那么Storm会通过FAIL-STREAM流向Spout发送消息,模式为< RootId>
                pending.remove(id);
                collector.emitDirect(task, ACKER_FAIL_STREAM_ID, tuple);
            } else if(resetTimeout) {
            //若此时消息被标记为发送超时,那么Storm通过ACKER_RESET_TIMEOUT_STREAM流将tuple发送给Spout
                collector.emitDirect(task, ACKER_RESET_TIMEOUT_STREAM_ID, tuple);
            } else {
                throw new IllegalStateException("The checks are inconsistent we reach what should be unreachable code.");
            }
        }

        collector.ack(input);
    }

    @Override
    public void cleanup() {
        LOG.info("Acker: cleanup successfully");
    }

    private long getTimeDeltaMillis(long startTimeMillis) {
        return Time.currentTimeMillis() - startTimeMillis;
    }
}

        1.Acker Bolt被初始化,定义一个this.pending = new RotatingMap<>(TIMEOUT_BUCKET_NUM)。RotatingMap旋转Map类(系统预定义流,用于消息超时)。系统预定义流,用于消息超时。Acker Bolt会对成员变量pending进行旋转操作,然后退出execute方法,该操作将pending中最早的一个桶中的数据删除掉,于是实现了。消息的超时。由于初始化RotatingMap时,未传入关于expire的回调方法,故该操作只是进行简单的删除。如果继续对已经删除掉的消息的Rootld进行Ack操作,就会创建新的<RootId,跟踪值>对,但是由于数据已被删除过的原因,跟踪值基本上不会再回到零,所以Spout将永远也收不到它发送出去的这条消息的Ack。Spout会通过自有的超时机制,将这条消息标记为处理失败,然后调用Spout的失败函数来决定对失败消息进行重传还是忽略。这个操作的结果是去除处于僵死状态的消息跟踪。

        2. 对进入到Acker Bolt中不同流的Tuple进行不同的逻辑处理。当进入到Acker Bolt的streamID为__ack_init时,对Acker中AckObject初始化操作。 Spout输入消息的模式为<RootId,RawAckValue,SpoutTaskId>, AckerBolt会根据Rootld取出<RootId, AckValue> ,并进行更新操作以及设置。

 if (ACKER_INIT_STREAM_ID.equals(streamId)) {
            /**
             * Acker中AckObject初始化操作 Spout输入消息的模式为<RootId,RawAckValue,SpoutTaskId> AckerBolt会根据Rootld取出<RootId, AckValue> ,并进行更新操作以及设置
             * spoutTaskId
             */
            if (curr == null) {
                curr = new AckObject();
                pending.put(id, curr);
            }
            curr.updateAck(input.getLong(1));
            curr.spoutTask = input.getInteger(2);
        } 
        当进入到Acker Bolt的streamID为__ack_ack时,ID:输人消息的模式为<RootId,AckValue>,与原有AckValue进行异或操作并存储。
else if (ACKER_ACK_STREAM_ID.equals(streamId)) {
            /**
             * ID:输人消息的模式为<RootId,AckValue〉,与原有AckValue进行异或操作并存储。
             */
            if (curr == null) {
                curr = new AckObject();
                pending.put(id, curr);
            }
            curr.updateAck(input.getLong(1));
        }

        当进入到Acker Bolt的streamID为__ack_fail, Acker收到Bolt或者Spout发送过来的Fail消息。输入消息的模式为< RootId >。设置failed 为true, 表示消息的处理已经失败。

else if (ACKER_FAIL_STREAM_ID.equals(streamId)) {
            // For the case that ack_fail message arrives before ack_init
            /**
             * Acker收到Bolt或者Spout发送过来的Fail消息。输入消息的模式为< RootId >。设置failed 为true, 表示消息的处理已经失败。
             */
            if (curr == null) {
                curr = new AckObject();
            }
            curr.failed = true;
            pending.put(id, curr);
        } 

        当进入到Acker Bolt的streamId为__ack_reset_timeout,AckerBolt 收到消息超时。

else if (ACKER_RESET_TIMEOUT_STREAM_ID.equals(streamId)) {
            /**
             * AckerBolt 收到消息超时
             */
            resetTimeout = true;
            if (curr != null) {
                pending.put(id, curr);
            } //else if it has not been added yet, there is no reason time it out later on
        }

        最后如果此时消息对应的跟踪值已经为零,那么Storm认为该消息以及所有衍生的消息都已被成功处理,这时会通过向ACK - STREAM流向Spout节点发送消息,模式为<RootId>。

int task = curr.spoutTask;
        if (curr != null && task >= 0
            && (curr.val == 0 || curr.failed || resetTimeout)) {
            Values tuple = new Values(id, getTimeDeltaMillis(curr.startTime));
            //若此时消息对应的跟踪值已经为零,那么Storm认为该消息以及所有衍生的消息都已被成功处理,这时会通过向ACK - STREAM流向Spout节点发送消息,模式为<RootId>
            if (curr.val == 0) {
                pending.remove(id);
                collector.emitDirect(task, ACKER_ACK_STREAM_ID, tuple);
            } else if (curr.failed) {
            //若此时消息被标记为失败,那么Storm会通过FAIL-STREAM流向Spout发送消息,模式为< RootId>
                pending.remove(id);
                collector.emitDirect(task, ACKER_FAIL_STREAM_ID, tuple);
            } else if(resetTimeout) {
            //若此时消息被标记为发送超时,那么Storm通过ACKER_RESET_TIMEOUT_STREAM流将tuple发送给Spout
                collector.emitDirect(task, ACKER_RESET_TIMEOUT_STREAM_ID, tuple);
            } else {
                throw new IllegalStateException("The checks are inconsistent we reach what should be unreachable code.");
            }
        }

        collector.ack(input);

5.2 SpoutOutputCollectorImpl 数据源头生成相应的Tuple

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.storm.executor.spout;


public class SpoutOutputCollectorImpl implements ISpoutOutputCollector {

    private final SpoutExecutor executor;
    private final Task taskData;
    private final int taskId;
    private final MutableLong emittedCount;
    private final boolean hasAckers;
    private final Random random;
    private final Boolean isEventLoggers;
    private final Boolean isDebug;
    private final RotatingMap<Long, TupleInfo> pending;

    @SuppressWarnings("unused")
    public SpoutOutputCollectorImpl(ISpout spout, SpoutExecutor executor, Task taskData, int taskId,
                                    MutableLong emittedCount, boolean hasAckers, Random random,
                                    Boolean isEventLoggers, Boolean isDebug, RotatingMap<Long, TupleInfo> pending) {
        this.executor = executor;
        this.taskData = taskData;
        this.taskId = taskId;
        this.emittedCount = emittedCount;
        this.hasAckers = hasAckers;
        this.random = random;
        this.isEventLoggers = isEventLoggers;
        this.isDebug = isDebug;
        this.pending = pending;
    }

    @Override
    public List<Integer> emit(String streamId, List<Object> tuple, Object messageId) {
        return sendSpoutMsg(streamId, tuple, messageId, null);
    }

    @Override
    public void emitDirect(int taskId, String streamId, List<Object> tuple, Object messageId) {
        sendSpoutMsg(streamId, tuple, messageId, taskId);
    }

    @Override
    public long getPendingCount() {
        return pending.size();
    }

    @Override
    public void reportError(Throwable error) {
        executor.getErrorReportingMetrics().incrReportedErrorCount();
        executor.getReportError().report(error);
    }

    /**
     * 1. 首先Spout调用sendSpoutMsg() 发送一个tuple到下游bolt
     * @param stream
     * @param values
     * @param messageId
     * @param outTaskId
     * @return
     */
    private List<Integer> sendSpoutMsg(String stream, List<Object> values, Object messageId, Integer outTaskId) {
        emittedCount.increment();

        List<Integer> outTasks;
        if (outTaskId != null) {
            outTasks = taskData.getOutgoingTasks(outTaskId, stream, values);
        } else {
            outTasks = taskData.getOutgoingTasks(stream, values);
        }

        List<Long> ackSeq = new ArrayList<>();
        boolean needAck = (messageId != null) && hasAckers;

        long rootId = MessageId.generateId(random);
        /**
         * Storm ACK 源码分析
         * 2.Storm中每条发送出去的消息都会对应一个随机的消息ID,并且这个long类型的消息ID将保存到MessageId这个对象中去。
         *  MessageId随着TupleImpl发送到下游相应的Bolt中去。若系统中含有Acker Bolt, 并且Spout在发送消息时指定了Messageld, Storm将对这条消息进行跟踪,并为其生成一条Rootld,
         *  然后为发送到每一个Task上面的消息也生成一个消息ID。消息ID是通过调用Messageld的generateld方法来产生的,为一个长整型随机数。
         */

        //2.根据上游spout和下游bolt之间的分组信息,将tuple发送到下游相应的task中,并且封装成TupleImpl类
        for (Integer t : outTasks) {
            MessageId msgId;
            if (needAck) {
                long as = MessageId.generateId(random);
                msgId = MessageId.makeRootId(rootId, as);
                ackSeq.add(as);
            } else {
                msgId = MessageId.makeUnanchored();
            }

            TupleImpl tuple = new TupleImpl(executor.getWorkerTopologyContext(), values, this.taskId, stream, msgId);
            //3.outputCollector调用executor的ExecutorTransfer类的transfer方法()将tuple添加目标taskId信息,封装成AddressTuple
            executor.getExecutorTransfer().transfer(t, tuple);
        }
        if (isEventLoggers) {
            executor.sendToEventLogger(executor, taskData, values, executor.getComponentId(), messageId, random);
        }

        /**
         * Storm ACK 源码分析
         * 3.首先Spout 发送一个锚定(anchored) ACKER_INIT_STREAM_ID 的消息给Acker Bolt。Acker Bolt将这个tuple的rootId进行保存下来,并且保存
         *  相应的_outAckVal(ACK值)以及Spout的TaskId。这个ACK值还是当前spout发送这个rootId对应的Tuples的异或值。
         */
        boolean sample = false;
        try {
            sample = executor.getSampler().call();
        } catch (Exception ignored) {
        }
        if (needAck) {
            /**
             * 将<RootId,元数据〉存储在RotatingMap中。消息元数据中含有Spout的Taskld、Messageld,Streamld以及消息的内容。存储原始的消息内容可以方便以后的失败重传,
             * ACKER_INIT_STREAM_ID 发送__ack__init流将 ackInitTuple发送给AckerBolt
             */
            TupleInfo info = new TupleInfo();
            info.setTaskId(this.taskId);
            info.setStream(stream);
            info.setMessageId(messageId);
            if (isDebug) {
                info.setValues(values);
            }
            if (sample) {
                info.setTimestamp(System.currentTimeMillis());
            }

            pending.put(rootId, info);
            List<Object> ackInitTuple = new Values(rootId, Utils.bitXorVals(ackSeq), this.taskId);
            executor.sendUnanchored(taskData, Acker.ACKER_INIT_STREAM_ID, ackInitTuple, executor.getExecutorTransfer());
        } else if (messageId != null) {
            TupleInfo info = new TupleInfo();
            info.setStream(stream);
            info.setValues(values);
            info.setMessageId(messageId);
            info.setTimestamp(0);
            Long timeDelta = sample ? 0L : null;
            info.setId("0:");
            //针对Spout发送消息时带有Messageld但系统中并没有Acker Bolt情况的一种特殊处理。此时,Storm将直接调用Spout的Ack方法,系统不对消息进行跟踪。
            executor.ackSpoutMsg(executor, taskData, timeDelta, info);
        }

        return outTasks;
    }
}

        Storm中每条发送出去的消息都会对应一个随机的消息ID,并且这个long类型的消息ID将保存到MessageId这个对象中去。MessageId随着TupleImpl发送到下游相应的Bolt中去。若系统中含有Acker Bolt, 并且Spout在发送消息时指定了Messageld, Storm将对这条消息进行跟踪,并为其生成一条Rootld。然后为发送到每一个Task上面的消息也生成一个消息ID。消息ID是通过调用Messageld的generateld方法来产生的,为一个长整型随机数。

 /**
         * Storm ACK 源码分析
         * 3.首先Spout 发送一个锚定(anchored) ACKER_INIT_STREAM_ID 的消息给Acker Bolt。Acker Bolt将这个tuple的rootId进行保存下来,并且保存
         *  相应的_outAckVal(ACK值)以及Spout的TaskId。这个ACK值还是当前spout发送这个rootId对应的Tuples的异或值。
         */
        boolean sample = false;
        try {
            sample = executor.getSampler().call();
        } catch (Exception ignored) {
        }
        if (needAck) {
            /**
             * 将<RootId,元数据〉存储在RotatingMap中。消息元数据中含有Spout的Taskld、Messageld,Streamld以及消息的内容。存储原始的消息内容可以方便以后的失败重传,
             * ACKER_INIT_STREAM_ID 发送__ack__init流将 ackInitTuple发送给AckerBolt
             */
            TupleInfo info = new TupleInfo();
            info.setTaskId(this.taskId);
            info.setStream(stream);
            info.setMessageId(messageId);
            if (isDebug) {
                info.setValues(values);
            }
            if (sample) {
                info.setTimestamp(System.currentTimeMillis());
            }

            pending.put(rootId, info);
            List<Object> ackInitTuple = new Values(rootId, Utils.bitXorVals(ackSeq), this.taskId);
            executor.sendUnanchored(taskData, Acker.ACKER_INIT_STREAM_ID, ackInitTuple, executor.getExecutorTransferAllGrouping());
        } else if (messageId != null) {
            TupleInfo info = new TupleInfo();
            info.setStream(stream);
            info.setValues(values);
            info.setMessageId(messageId);
            info.setTimestamp(0);
            Long timeDelta = sample ? 0L : null;
            info.setId("0:");
            //针对Spout发送消息时带有Messageld但系统中并没有Acker Bolt情况的一种特殊处理。此时,Storm将直接调用Spout的Ack方法,系统不对消息进行跟踪。
            executor.ackSpoutMsg(executor, taskData, timeDelta, info);
        }

5.3 BoltOutputCollectorImpl 锚定相应的Tuple

        Bolt在发送消息时,系统需要继续对其进行跟踪,这些由Bolt新发送的消息对应于从Spout收到消息的衍生消息。Bolt使用bolt -emit函数来发送消息。BoltOutputCollector对于每个OutgoingTask 进行锚定。

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.storm.executor.bolt;


/**
 * Storm ACK 源码分析
 * 4.Bolt在发送消息时,系统需要继续对其进行跟踪,这些由Bolt新发送的消息对应于从Spout收到消息的衍生消息。Bolt使用bolt -emit函数来发送消息
 */
public class BoltOutputCollectorImpl implements IOutputCollector {

    private static final Logger LOG = LoggerFactory.getLogger(BoltOutputCollectorImpl.class);

    private final BoltExecutor executor;
    private final Task taskData;
    private final int taskId;
    private final Random random;
    private final boolean isEventLoggers;
    private final boolean isDebug;

    public BoltOutputCollectorImpl(BoltExecutor executor, Task taskData, int taskId, Random random,
                                   boolean isEventLoggers, boolean isDebug) {
        this.executor = executor;
        this.taskData = taskData;
        this.taskId = taskId;
        this.random = random;
        this.isEventLoggers = isEventLoggers;
        this.isDebug = isDebug;
    }

    public List<Integer> emit(String streamId, Collection<Tuple> anchors, List<Object> tuple) {
        return boltEmit(streamId, anchors, tuple, null);
    }

    @Override
    public void emitDirect(int taskId, String streamId, Collection<Tuple> anchors, List<Object> tuple) {
        boltEmit(streamId, anchors, tuple, taskId);
    }

    /**
     * 1.首先Bolt调用boltEmit() 发送一个tuple到下游bolt
     * @param streamId
     * @param anchors
     * @param values
     * @param targetTaskId
     * @return
     */
    private List<Integer> boltEmit(String streamId, Collection<Tuple> anchors, List<Object> values, Integer targetTaskId) {
        List<Integer> outTasks;
        if (targetTaskId != null) {
            outTasks = taskData.getOutgoingTasks(targetTaskId, streamId, values);
        } else {
            outTasks = taskData.getOutgoingTasks(streamId, values);
        }

        /**
         * Acker源码分析
         * bolt-emit函数的传入参数为消息标记( anchors ),它对应于该消息的父节点消息。为了保证Ack系统正常工作,用户需要明确其产生的消息是由哪些消息衍生的。
         */
        for (Integer t : outTasks) {
            Map<Long, Long> anchorsToIds = new HashMap<>();
            if (anchors != null) {
                for (Tuple a : anchors) {
                    Set<Long> rootIds = a.getMessageId().getAnchorsToIds().keySet();
                    if (rootIds.size() > 0) {
                        long edgeId = MessageId.generateId(random);
                        ((TupleImpl) a).updateAckVal(edgeId);
                        for (Long root_id : rootIds) {
                            putXor(anchorsToIds, root_id, edgeId);
                        }
                    }
                }
            }
            MessageId msgId = MessageId.makeId(anchorsToIds);
            TupleImpl tupleExt = new TupleImpl(executor.getWorkerTopologyContext(), values, taskId, streamId, msgId);
            executor.getExecutorTransfer().transfer(t, tupleExt);
        }
        if (isEventLoggers) {
            executor.sendToEventLogger(executor, taskData, values, executor.getComponentId(), null, random);
        }
        return outTasks;
    }

    @Override
    public void ack(Tuple input) {
        long ackValue = ((TupleImpl) input).getAckVal();
        Map<Long, Long> anchorsToIds = input.getMessageId().getAnchorsToIds();
        for (Map.Entry<Long, Long> entry : anchorsToIds.entrySet()) {
            executor.sendUnanchored(taskData, Acker.ACKER_ACK_STREAM_ID,
                    new Values(entry.getKey(), Utils.bitXor(entry.getValue(), ackValue)),
                    executor.getExecutorTransfer());
        }
        long delta = tupleTimeDelta((TupleImpl) input);
        if (isDebug) {
            LOG.info("BOLT ack TASK: {} TIME: {} TUPLE: {}", taskId, delta, input);
        }
        BoltAckInfo boltAckInfo = new BoltAckInfo(input, taskId, delta);
        boltAckInfo.applyOn(taskData.getUserContext());
        if (delta >= 0) {
            ((BoltExecutorStats) executor.getStats()).boltAckedTuple(
                    input.getSourceComponent(), input.getSourceStreamId(), delta);
        }
    }

    @Override
    public void fail(Tuple input) {
        Set<Long> roots = input.getMessageId().getAnchors();
        for (Long root : roots) {
            executor.sendUnanchored(taskData, Acker.ACKER_FAIL_STREAM_ID,
                    new Values(root), executor.getExecutorTransfer());
        }
        long delta = tupleTimeDelta((TupleImpl) input);
        if (isDebug) {
            LOG.info("BOLT fail TASK: {} TIME: {} TUPLE: {}", taskId, delta, input);
        }
        BoltFailInfo boltFailInfo = new BoltFailInfo(input, taskId, delta);
        boltFailInfo.applyOn(taskData.getUserContext());
        if (delta >= 0) {
            ((BoltExecutorStats) executor.getStats()).boltFailedTuple(
                    input.getSourceComponent(), input.getSourceStreamId(), delta);
        }
    }

    @Override
    public void resetTimeout(Tuple input) {
        Set<Long> roots = input.getMessageId().getAnchors();
        for (Long root : roots) {
            executor.sendUnanchored(taskData, Acker.ACKER_RESET_TIMEOUT_STREAM_ID,
                    new Values(root), executor.getExecutorTransfer());
        }
    }

    @Override
    public void reportError(Throwable error) {
        executor.getErrorReportingMetrics().incrReportedErrorCount();
        executor.getReportError().report(error);
    }

    private long tupleTimeDelta(TupleImpl tuple) {
        Long ms = tuple.getProcessSampleStartTime();
        if (ms != null) {
            return Time.deltaMs(ms);
        }
        return -1;
    }


    /**
     * 根据key id 持续更新pending
     *
     * @param pending
     * @param key
     * @param id
     */
    private void putXor(Map<Long, Long> pending, Long key, Long id) {
        Long curr = pending.get(key);
        if (curr == null) {
            curr = 0l;
        }
        pending.put(key, Utils.bitXor(curr, id));
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值