Twitter Storm 可靠消息处理

可靠消息处理

Storm保证从spout发射出来的每个消息都被完全处理。该文章描述storm如何实现消息可靠处理,从storm的可靠性特性获得好处,用户需要做哪些工作。

消息被完全处理是什么意思?
从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出来的一个元组是“完全地处理”。当一个元组的消息树在指定时间内未完全地处理,Storm认为一个元组失败。可使用 Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS配置Timeout,默认值为30秒。

如果一个消息完全处理或处理失败会发生什么?
为了理解这个问题,让我们来看一看从spout出来的元组生命周期。作为参考,这是spout实现的接口(更多信息参见 Javadoc)。
    public interface ISpout extends Serializable { 
        void open(Map conf, TopologyContext context, SpoutOutputCollector collector); 
        void close(); 
        void nextTuple(); 
        void ack(Object msgId); 
        void fail(Object msgId); 
    } 

 
首先,storm调用spout对象的nextTuple方法从spout请求一个元组,Spout使用在open方法中提供的SpoutOutputCollector发射一个元组到输出流。当发射一个元组时,spout提供一个“message id”用于标识该元组。例如,KestrelSpout对象从Kestrel队列读取消息,然后发射指定消息ID并发射。发射消息的代码看上去是这样的:
_collector.emit( new  Values( "field1" "field2" 3 ) , msgId); 
 
接下来,元组发送到消费bolt,storm负责追踪已创建的消息树。如果storm检测到一个元组已完全处理,storm将调用 spout任务的ack方法,参数中包括spout提供给storm的消息ID。同样地,如果元组超时,storm将调用源spout任务的fail方法。注意,一个元组被创建它的同一任务ack或fail。因此,如果一个spout以跨集群多任务的方式运行,一个元组只会被创建它的同一任务ack或fail,而不会被其它任务ack或fail。
 
让我们再看下kestrelSpout,了解一个spout需要做哪些事情才能保证消息可靠处理。当spout从Kestrel队列中取出一个消息时,它“opens”这个消息,意思是说这个消息实际上也不是从队列中取出,而是把消息置成“pending”状态,等待消息完成时确认消息。当在一个消息处于pending状态,该消息不会再发送到队列的其它消费者。此外,如果一个客户端断开连接,该客户端相关的所有pending状态消息将放回队列。当一个消息被打开,Kestrel提供给客户端这个消息的数据和消息ID。当发送元组到SpoutOutputCollector时,KestrelSpout为元组使用从kestrel返回的确切的ID作为消息ID。一段时间之后,当KestrelSpout的ack或fail方法被调用时,KestrelSpout给Kestrel发送一个带消息ID的ack或fail消息,把消息从Kestrel删除或把消息放回队列。
 
Storm的可靠性API是什么?
为了从storm的可靠性特性中受益,你要做两件事。首先,每当你在元组树中创建新的连接时通知storm。其次,每当你处理完一个单独的元组时通知storm。通过做这两件事,storm能检测元组树何时被完全处理并恰当地ack或fail这个spout的元组。Storm的API提供一种简明的方式来做这两件事。
 
在元组中指定一个连接称之为anchoring。Anchoring与发射一个新元组同时完成。以如下bolt为例,这个bolt把一个包含句子的元组分割成每个单词一个元组:
 
  
  
  1. public class SplitSentence implements IRichBolt { 
            OutputCollector _collector; 
             
            public void prepare(Map conf, TopologyContext context, OutputCollector collector) { 
                _collector = collector; 
            } 
     
            public void execute(Tuple tuple) { 
                String sentence = tuple.getString(0); 
                for(String word: sentence.split(" ")) { 
                    _collector.emit(tuple, new Values(word)); 
                } 
                _collector.ack(tuple); 
            } 
     
            public void cleanup() { 
            } 
     
            public void declareOutputFields(OutputFieldsDeclarer declarer) { 
                declarer.declare(new Fields("word")); 
            }         
        }

     
每个单词元组被固定到指定的输入元组,该输入元组作为emit方法的第一个参数。自从单词元组被固定后,如果单词元组在下游处理时失败,这个消息树的根,即这个spout的元组将被重放。相比之下,让我们看看,如果单词元组是像这样发送,这将发生什么?
 
  
  
  1. _collector.emit(new Values(word)); 
这种方式发送单词元组导致它未被固定。如果这个元组的下游处理失败,这个根元组将不会重放。根据拓扑的不同容错性要求,有时发送一个未固定的元组也是合适的。
 
一个输出元组可以被固定到多个输入元组。当处理流连接和流聚合时,这是有用的。一个多锚点(固定点)的元组在处理失败时会引起这个spout的多个元组重放。通过指定一个元组列表,而不是单一的元组来完成多锚点。例如:
 
    List<Tuple> anchors = new ArrayList<Tuple>(); 
    anchors.add(tuple1); 
    anchors.add(tuple2); 
    _collector.emit(anchors, new Values(1, 2, 3)); 


多锚点添加输出元组到多元组树。
 
Anchoring是用来指定元组树—当你处理完元组树中的一个单独元组时,下一个或最后一块代码是规定为Storm的可靠性API。这将通过使用 OutputCollector的ack或fail方法来完成。如果你回过头来看SplitSentence例子,你能看到所有单词元组被发射后,这个输入元组被acked。
 
你可以使用OutputCollector的fail方法直接使处于元组树的根位置的spout元组失败。例如,你的应用程序可能选择捕获一个异常,然后明确地使这个输入元组失败。通过明确地使这个元组失败,这个spout元组能更快的重放比你等待这个元组超时。
 
你处理的每个元组必须被acked或failed。Storm使用内存追踪每个元组。因此如果你没有ack/fail每个元组,这个任务最终将引起内存溢出。
 
许多bolts都遵从一个常见模式:首先,读一个输入元组;然后,在输入元组的基础上再发射它;最后,在执行方法的末尾直接ack那个输入元组。这些bolt分成过滤器和简单功能两类。Storm有个称之为BasicBolt的接口为你封闭了这个模式,SplitSentence例子能以BasicBolt的形式编写,如下所示:
 
public class SplitSentence implements IBasicBolt { 
        public void prepare(Map conf, TopologyContext context) { 
        } 
 
        public void execute(Tuple tuple, BasicOutputCollector collector) { 
            String sentence = tuple.getString(0); 
            for(String word: sentence.split(" ")) { 
                collector.emit(new Values(word)); 
            } 
        } 
 
        public void cleanup() { 
        } 
 
        public void declareOutputFields(OutputFieldsDeclarer declarer) { 
            declarer.declare(new Fields("word")); 
        }         
    }



这个实现比之前的实现简单得多,但功能是一样的。发送到BasicOutputCollector的元组会自动固定到输入元组,并且当execute方法完成时,为你自动acked输入元组。
 
相比之下,处理流连接和流聚合的bolt可能延迟ack一个元组直到基于一捆元组计算出结果。流聚合和流连接一般用多锚点来固定输出元组,这些在IBasicBolt的能力范围之外。
 
Strom如何高效实现可靠性?
Storm拓扑有一组特殊的“acker”任务,它们为每个spout元组跟踪元组树。当一个acker看到一个树已完成,它发送一个消息给创建该spout元组的spout任务。你可以在拓扑配置中使用Config.TOPOLOGY_ACKERS来设置拓扑的acker任务数量,storm默认的acker任务数是1—处理大量消息时你需要增加拓扑的acker数量。
 
理解storm可靠性实现的最好方式是研究元组的生命周期和元组树。当在拓扑中创建一个元组,不管是在spout还是在bolt中创建,它被赋予一个随机的64位ID,这些ID被acker任务用于为每个spout元组跟踪元组树。
 
每个元组知道它所在元组树中所有spout元组的id。当你在bolt中发射一个新元组时,元组锚点中的spout元组id被复制到新的元组中。当一个元组被acked,它发送一个消息给合适的acker任务,消息包含元组如何被改变的信息。特别是它告诉acker任务:“在spout元组树内,我现在已完成,并且树中一个新的元组固定到我
 
例如,如果基于元组“C” 创建元组“D”和“E”,当“C”被acked时,元组树如何变化,如下图所示:

由于“D”和“E”被添加到树中,同时“C”被storm从树中移除,树从来不会提前完成。
 
这里还有一些Storm如何跟踪元组树的细节。如前所述,你可以有任意数量的acker任务。这就导致如下问题:当acked拓扑中的一个元组时,如果才能知道向哪个acker任务发送信息。
 
Storm使用一致性Hash算法映射一个spout元组id到一个acker任务。由于每个元组都携带了它所在树的spout元组id,所以它知道发送消息给哪个acker任务。
 
Storm的另一个细节是acker任务如何跟踪spout任务负责的每个元组。当一个spout任务发射一个元组时,它仅仅发送消息给合适的acker任务,告诉它:它的任务id是负责那个元组。然后,当一个acker看到一颗树已完成时,它根据任务ID就知道向哪个任务发送消息。
 
Acker任务不显示地跟踪元组树。对于包含成千上万个节点(或更多)的大元组树,跟踪所有元组树会远远超过acker任务使用的内存。作为代替,acker任务采用一种不同策略,每spout元组仅需要固定数量空间(大约20字节)。这个跟踪算法是storm如何工作的关键,也是主要的突破点之一。
 
一个acker任务用map存储一个spout元组ID与一对值。第一个值是创建spout元组的任务ID,用于以后元组完成时发送完成信息。第二个值是称之为“ack val”的64位数字,它表示整个元组树的状态,不管这颗树的大小。它简单的异或所有树中已创建或acked的元组ID。
 
当一个acker任务遇到一个ack val的值变成0时,它知道这棵元组树已经处理完成。因为元组id是随机的64位数字,ack val碰巧变成0的几率极小。如果你算一下,每秒发生10K个ack,5000万年才可能发生一个错误。就算碰巧发生一个错误,仅那个元组失败的时才会导致数据丢失。
 
现在你已经知道了可靠性算法,让我们过一遍所有可能的失败场景,并看看在每种情况下,storm怎么避免数据丢失:
由于任务挂掉,一个元组未被acked在这种情况下,处于树根位置的spout元组由于失败元组将超时并被重放。
Acker任务挂掉:在这种情况下,由该acker任务跟踪的所有spout元组都将超并被重放。
Spout任务挂掉:在这种情况下,spout的消息源负责重新发送这些消息。例如,当一个客户端断开连接,Kestrel和RabbitMQ将把所有pending状态的消息放回队列。
 
如你所见, storm的可靠性机制是完全分布式的,可伸缩的并且是高度容错的。
 
调整可靠性
Acker任务是轻量级的,因此一个拓扑不需要太多acker任务。你可以通过Storm UI(组件id“__acker”)来跟踪它们的性能。如果吞吐量不正常,那么你将需要增加更多的acker任务。
 
如果可靠性对你来说不是那么重要--你不关心失败情况下丢失元组,那么你可以通过不跟踪spout元组的元组树来提升性能。不跟踪元组树将减少一半消息传递,因为正常情况下元组树中的每个元组都要发送一个ack消息。另个,下游元组保存的id更少,减少带宽使用。
 
有三种方式移除可靠性。第一种方式是设置Config.TOPOLOGY_ACKERS 为0。在这种情况下,spout发射一个元组后,storm直接调用spout的ack方法。元组树不会被跟踪。
 
第二种方式是在一个消息上通过消息基础移除可靠性。你可以通过使用SpoutOutputCollector.emit方法时不指定消息ID来对一个单独的元组关闭跟踪。
 
最后一种方式,如果你不关心元组的一个特定子集在拓扑下游处理失败,那么你可以把它们作为unanchored元组发射。由于它们未固定到任何spout元组,如果它们未acked,它们将不会引起任何spout元组失败。
 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值