Storm的基础知识-保证消息处理(4)

Storm提供了几种不同级别的保证消息处理,包括尽力而为,至少一次,以及通过Trident完成一次。

1,What does it mean for a message to be “fully processed”?(消息“完全处理”是什么意思?)

从Spout发射的元组可以触发基于它的数千个元组。例如,考虑流式字数统计拓扑:

TopologyBuilder builder = new TopologyBuilder();
builder.setSpout("sentences", 
     new KestrelSpout("kestrel.backtype.com",22133,"sentence_queue",new StringScheme()));
builder.setBolt("split", new SplitSentence(), 10)
        .shuffleGrouping("sentences");
builder.setBolt("count", new WordCount(), 20)
        .fieldsGrouping("split", new Fields("word"));

这种拓扑结构从Kestrel队列中读出句子,将句子分成其组成单词,然后为每个单词发出该单词出现的的次数。从Spout发射的元组触发了许多基于它的元组:句子中每个单词的元组和每个单词更新计数的元组。消息树看起来像这样:
在这里插入图片描述
当元组树已经用完并且树中的每条消息都已处理完毕时,Storm会认为从Spout发射的元组是“完全处理”的。如果在指定的超时内无法完全处理其消息树,则认为该元组失败。
可以在特定拓扑的基础上配置此超时,使用Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS配置,默认timeout为30秒。

2,如果邮件已完全处理或未能完全处理,会发生什么?

  • 如果一个tuple处理成功是指这个Tuple以及这个Tuple产生的所有Tuple都被成功处理, 会调用spout的ack方法
  • 如果失败是指这个Tuple或这个Tuple产生的所有Tuple中的某一个tuple处理失败, 则会调用spout的fail方法;
  • 在处理tuple的每一个bolt都会通过OutputCollector来告知storm, 当前bolt处理是否成功。
  • 另外需要注意的,当spout触发fail动作时,不会自动重发失败的tuple,需要我们在spout中重新获取发送失败数据,手动重新再发送一次。

实现细节:

  • 首先,Storm通过调用Spout的nextTuple方法来请求一个元组(Spout发射的)。Spout用使用open方法中所提供的SpoutOutputCollector来发射一个元组到其输出流中的一个。发出元组时,Spout会提供一个“message id”,用于稍后识别元组。例如,KestrelSpout从队列中读取消息,并将Kestrel提供的id作为"message id"发出。将消息发送到SpoutOutputCollector如下所示:

    _collector.emit(new Values("field1", "field2", 3) , msgId);      
    //在spout中发射tuple的时候需要同时发送messageid,这样才相当于开启了消息确认机制
    
  • 接下来,元组被发送到负责处理的Bolt,Storm负责跟踪创建的消息树。如果Storm检测到元组已完全处理,Storm会使用Spout为Storm提供的"message id"在原始Spout任务上调用ack方法。 同样,如果元组超时,Storm将调用这个Spout的fail方法。请注意,元组将被创建它的同一个South任务激活或失败。因此,如果一个Spout在群集中执行尽可能多的任务,一个元祖不会通过 与创建它的任务 不同的任务 来确定或失败。

  • 让我们KestrelSpout再次使用,看看Spout需要做些什么来保证消息处理。

  • 当KestrelSpout从Kestrel队列中取出消息时,它会“open”该消息。这意味着消息实际上并未从队列中取出,而是处于“待处理”状态,等待确认消息已完成。

  • 消息处于“待处理”状态时,不会被发送给队列的其他消费者。

  • 此外,如果客户端断开该客户端的所有挂起(待处理)消息,则将其重新放回队列。

  • 当消息被“打开”时,,Kestrel会为客户端提供消息的数据以及消息的唯一ID。KestrelSpout发送元组到SpoutOutputCollector时,将唯一id作为元组的“消息id”。以后的某个时间,KestrelSpout的ack方法或者fail方法被调用时,KestrelSpout向Kestrel发送一条带有 message id 的确认消息或失败消息:若是确认消息,将message从队列中取出;若是失败消息,将消息重新打开。

3,Storm消息处理至少一次

实现Storm消息处理至少一次需要ACK机制完成,至少一次是因为若一个Spout发消息多个Bolt处理时,其中一个Bolt处理失败调用fail,则Spout会重发消息给所有Bolt,消息处理成功的Bolt将会处理消息两次。

要实现ack机制步骤:

  • 1,spout发射tuple的时候指定messageId
  • 2,spout要重写BaseRichSpout的fail和ack方法
  • 3,spout对发射的tuple进行缓存(可以在Spout设置一个Map存储数据,key可以为自己设置的meaasge id, value存储tuple)
public class MySpout extends BaseRichSpout {
    private static final long serialVersionUID = 5028304756439810609L;
    private int dex = 0;
    // key:messageId,value:tuple
    private HashMap<String, String> waitAck = new HashMap<String, String>();

    private SpoutOutputCollector collector;

    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
        this.collector = collector;
    }

    public void nextTuple() {
        String sentence[] ={"Hello.",  "Today is warm and happy!" , "I like moon"};
        messageId = dex;
        waitAck.put(messageId, sentence[dex]);
        //指定messageId,开启ackfail机制
        collector.emit(new Values(sentence), messageId);
        dex++;
    }
    
   public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("sentence"));
    }
    @Override
    public void ack(Object msgId) {
        System.out.println("消息处理成功:" + msgId);
        System.out.println("删除缓存中的数据...");
        waitAck.remove(msgId);
    }

    @Override
    public void fail(Object msgId) {
        System.out.println("消息处理失败:" + msgId);
        System.out.println("重新发送失败的信息...");
        //重发如果不开启ackfail机制,那么spout的map对象中的该数据不会被删除的。
        collector.emit(new Values(waitAck.get(msgId)),msgId);
    }
}
  • 4,bolt需要把发射tuple锚定到输入tuple,并调用spout的ack和fail方法

    在元组树中指定链接称为锚定anchoring锚定是在您发出新元组的同时完成的Storm的Bolt有BsicBolt和RichBolt,实现ACK机制有下面两种情况:

    • 在BasicBolt中,BasicOutputCollector在emit数据的时候,会自动和输入的tuple相关联,而在execute方法结束的时候那个输入tuple会被自动ack。
      该SplitSentence示例可以写成如下所示:
   public class SplitSentence extends BaseBasicBolt {
                public void execute(Tuple tuple, BasicOutputCollector collector) {
                    String sentence = tuple.getString(0);
                    for(String word: sentence.split(" ")) {
                        collector.emit(new Values(word));       
                  //BasicOutputCollector会自动把发出的元组锚定到输入元组,并在execute方法完成时自动为您确认输入元组
                  //不需要在此写确认代码,
                    }
                }
            public void declareOutputFields(OutputFieldsDeclarer declarer) {
                declarer.declare(new Fields("word"));
            }        
        }

  • 使用RichBolt需要在emit数据的时候,将输入元组指定为emit的第一个参数,即collector.emit(inputTuple, newTuple),来锚定每个单词元组,以保持tracker链路。由于单词元组被锚定,并且需要在execute方法结尾调用OutputCollector.ack(tuple),当失败处理时,调用方法OutputCollector.fail(tuple);如果单词元组未能在下游处理,则稍后将重新发送 元组树根处的 spout元组
public class SplitSentence extends BaseRichBolt {
                OutputCollector _collector;
            
        public void prepare(Map conf, TopologyContext context, OutputCollector collector) {
            _collector = collector;
        }

        @Override
        public void execute(Tuple input) {
            try {
                String value = input.getString(0);
               
                for(String word : value.split(" ")){
                this.collector.emit(input,new Values(word));
                      }
                this.collector.ack(input);//确认数据处理成功,spout中的ack方法会被调用
            } catch (Exception e) {
                this.collector.fail(input);//确认数据处理失败,spout中的fail方法会被调用
                e.printStackTrace();
            }
        }

        public void declareOutputFields(OutputFieldsDeclarer declarer) {
            declarer.declare(new Fields("word"));
        }        
    }

  • 5,spout根据messageId,把ack的tuple从缓存中删除,对于fail的tuple可以选择重发。

  • 6, 通过Config.setNumAckers(conf, ackerParal)设置acker数至少大于0

  • 不锚定,让我们来看看如果像这样发出单词元组会发生什么:

     public static class MyBolt1 extends BaseRichBolt{
     private OutputCollector collector;
     ......
     @Override
     public void execute(Tuple input) {
         try {
             Integer value = input.getIntegerByField("num");
             
             this.collector.emit(new Values(value));          //未锚定
             
             this.collector.ack(input);//确认数据处理成功,spout中的ack方法会被调用
         } catch (Exception e) {
             this.collector.fail(input);//确认数据处理失败,spout中的fail方法会被调用
             e.printStackTrace();
         }
     }
    }
     
     public static class MyBolt2 extends BaseRichBolt{
         private OutputCollector collector;
          .......
         @Override
         public void execute(Tuple input) {
         try {
             String value = input.getStringByField("num_1");
             System.out.println(value);
             this.collector.ack(input);//确认数据处理成功,spout中的ack方法会被调用
         } catch (Exception e) {
             this.collector.fail(input);//确认数据处理失败,spout中的fail方法会被调用
             e.printStackTrace();
         }
         }
     }
    

以这种方式发出的单词元组不会被锚定。如果在MyBolt2处理元组失败,则不会重发Spout元组。Spout元组会在上一个MyBolt1处理确认成功时便返回ACK给Spout,若MyBolt1失败则调用fail,与当前MyBolt2处理成功与否无关。

  • 多锚定
    输出元组可以锚定到多个输入元组。这在进行流连接或聚合时很有用。处理失败的多锚定元组将导致spouts重发多个元组。通过指定元组列表而不仅仅是单个元组来完成多锚定。例如:
    List<Tuple> anchors = new ArrayList<Tuple>();
    anchors.add(tuple1);
    anchors.add(tuple2);
    _collector.emit(anchors, new Values(1, 2, 3));

多锚定将输出元组添加到多个元组树中。请注意,多锚定也可以打破树结构并创建元组DAGs(有向无环图),如下所示:
在这里插入图片描述

4,Storm保证消息处理-通过Trident完成一次

trident

5,Acker跟踪算法的原理:

acker对于每个spout-tuple保存一个ack-val的校验值,它的初始值是0,然后每发射一个Tuple或Ack一个Tuple时,这个Tuple的id就要跟这个校验值异或一下,并且把得到的值更新为ack-val的新值。那么假设每个发射出去的Tuple都被ack了,那么最后ack-val的值就一定是0。Acker就根据ack-val是否为0来判断是否完全处理,如果为0则认为已完全处理。

如果可以重发元组,我如何使我的应用程序正常工作?

与软件设计一样,答案是“看情况”。如果你真的想要一次语义,使用Trident API。在某些情况下,与大量分析一样,丢弃数据是可以的,因此通过将acker bolt的数量Config.TOPOLOGY_ACKERS设置为0 来禁用容错。但在某些情况下,您希望确保所有内容都至少处理过一次,并且没有任何内容被删除。如果所有操作都是幂等的,或者重复删除可能会发生,那么这尤其有用。

Storm如何以有效的方式实现可靠性?

Storm拓扑具有一组特殊的“acker”任务,可跟踪每个spout元组的元组DAG。当acker看到DAG完成时,它会向创建了该spout元组的spout任务发送一条确认消息。您可以使用Config.TOPOLOGY_ACKERS在拓扑配置中设置拓扑的acker任务数。Storm默认TOPOLOGY_ACKERS为每个worker一个acker。

  • 如前所述,您可以在拓扑中拥有任意数量的acker任务。这导致了以下问题:当拓扑中的元组被确认时,它如何知道发送该信息给哪个acker任务?

    Storm使用mod散列将spout元组id映射到acker任务。由于每个元组都带有它们存在的树的所有spout元组ID,因此它们知道要与之通信的acker任务是哪个。

  • acker任务如何跟踪哪些Spout任务对他们正在跟踪的每个Spout元组负责?

    当spout任务发出新元组时,它只是向适当的acker发送一条消息,告诉它其任务id并负责该spout元组。然后,当acker看到树已经完成时,它知道要发送完成消息去的任务ID。

  • Acker任务不会显式跟踪元组树。

  • Storm在每种情况下如何避免数据丢失:

    • 由于任务已经死亡,因此不会确定元组:在这种情况下,失败元组的树根处的spout元组id会超时并重放。

    • Acker任务死亡:在这种情况下,acker跟踪的所有spout元组将超时并重放。

    • Spout任务死亡:在这种情况下,spout与之对话的来源负责重放消息。例如,当客户端断开连接时,像Kestrel和RabbitMQ这样的队列会将所有挂起的消息放回队列中。

如您所见,Storm的可靠性机制是完全分布式,可伸缩的和容错的。

调整可靠性

Acker任务是轻量级的,因此在拓扑中不需要很多任务。您可以通过Storm UI(component id“__acker”)跟踪它们的性能。如果吞吐量看起来不正确,则需要添加更多的acker任务。

  • 如果可靠性对您来说并不重要 - 也就是说,您不关心在失败情况下丢失元组 -那么您可以通过不跟踪元组树的spout元组来提高性能。不跟踪元组树将传输的消息数减半,因为通常在元组树中的每个元组都有一条确认消息。此外,它需要在每个下游元组中保留更少的ID,从而减少带宽使用。
  • 有三种方法可以消除可靠性。
    • 将Config.TOPOLOGY_ACKERS设置为0.

      在这种情况下,Storm会在spout ack发出元组后立即调用spout上ack的方法。将不会跟踪元组树。

    • 省略SpoutOutputCollector.emit方法中的消息ID来关闭单个spout元组的跟踪。

    • 最后,如果您不关心拓扑中下游的元组的特定子集是否无法处理,则可以将它们作为未锚定的元组发出。因为它们没有锚定到任何Spout元组,所以如果它们没有被确认,它们将不会导致任何spout元组失败。

详细概念:Guaranteeing Message Processing

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值