storm的学习与使用(三)

来源:互联网 发布:网络语言爸爸什么意思 编辑:程序博客网 时间:2024/04/19 15:13

Storm如何保证数据不丢失

storm保证从spout出发的每个tuple都会被完全处理。这篇文章介绍storm是怎么做到这个保证的,以及我们使用者怎么做才能充分利用storm的可靠性特点。

一个tuple被完全处理是什么意思

就如同蝴蝶效应一样,从spout发射出的一个tuple可以引起其他成千上万个tuple因它而产生。想想那个计算一片文章中每个单词出现次数的topology.

TopologyBuilder builder = new TopologyBuilder();builder.setSpout(1, new KestrelSpout("kestrel.backtype.com", 22133, "sentence_queue", new StringScheme()));builder.setBolt(2, new SplitSentence(), 10).shuffleGrouping(1);builder.setBolt(3, new WordCount(), 20).fieldsGrouping(2, new Fields("word"));
这个topology从一个Kestrel队列读取句子,把每个句子分割成一个个单词,然后发射这一个个单词:一个源tuple(一个句子)引起后面很多tuple的产生,在storm里面一个tuple被完全处理的意思是:这个tuple以及由这个tuple所导致的所有tuple都被成功处理。而一个tuple会被认为处理失败了如果这个消息在timeout所指定的时间呗没有成功处理。而这个timeout可以通过Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS来制定


一个消息处理失败了或者成功了会发生什么

下面这个是spout要实现的接口:

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通过nextTuple方法来获取下一个tuple,spout通过open方法参数里面提供的SpoutOutputCollector来发射新的tuple到它的其中一个输出消息流,发射tuple的时候spout会提供一个message-id,后面我们通过这个message-id来追踪这个tuple。举例来说,KestrelSpout从Kestrel队列中读取一个消息,并且把Kestrel提供的消息id作为message-id,看例子:

_collector.emit(new Values("field1","field2", 3),msgId);
接下来,这个发射的tuple被传送到消息处理者bolt哪里,storm会跟踪由此所产生的这棵tuple树。如果storm检测到一个tuple被完全处理了,那么storm会以最开始的那个message-id作为参数去调用消息源的ack方法;反之storm会调用spout的fail方法。值得注意的一点是,storm调用ack或者fail的task始终是产生这个tuple的那个task。所以如果一个spout被分成很多歌task来执行,消息执行的成功失败与否始终会通知最开始发出tuplr的那个task.

我们在一KestrelSpout为例来看看spout需要做些社么才能保证"一个消息始终被完全处理",当KestrelSpout从队列当中读取一条消息时,首先它打开这个消息,这一位着这条消息还在队列里面,不过这条消息会被表示成"处理中"直到ack或者fail被调用。处于处理中的状态的消息不回被发给其他消息处理者了;并且如果这个spout断线了,那么所有处于处理中的消息会被重新标示成等待处理。(如果是这样,那么这些消息岂不是要存储起来,还有其状态,如果数据量巨大且失败的较多,会导致内存增大吧)

Storm的可靠性API

作为storm的使用者,有两件事情要做,以更好的利用storm的特性。首先,在你生成一个tuple的时候要通知storm;其次,完成处理一个tuple之后要通知storm。这样storm就可以检测整个tuple树有没有完全处理,并且通知源spout处理结果。storm提供了一些见解的api来做这些事情。


由一个tuple产生另一个新的tuple称为:anchoring.你发射一个新的tuple的同时也就完成了一次anchoring.看下面这个例子:这个bolt把一个包含一个句子的tuple分割成每个单词一一个tuple。

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"));        }    }
看一下这个execute方法,emit第一个参数是tuple,第二个参数则是输出这个tuple,这其实就是通过输出tuple anchoring了一个新的输出tuple.因为这个“单词tuple”被anchoring在“句子tuple”一起了,如果其中一个单词处理出错,那么这整个句子会被重新处理。作为对比,我们看看如果通过下面这个代码发射会有什么结果。

_collector.emit(new Values(word));
用这种方法发射会导致新发射的这个tuple脱离原来的tuple树,如果这个tuple处理失败了,整个句子不会被重新处理。到底要anchoring还是要unanchoring完全取取决于业务的需求。

一个输出tuple可以被anchoring到多个输出tuple。这种方式在stream合并或者stream聚合的时候很有用。一个多入口tuple梳理失败的话,那么它所对应的所有输入都要重新执行。看看下面怎么制定多个输入tuple:

List<Tuple> anchors = new ArrayList<Tuple>();anchors.add(tuple1);anchors.add(tuple2);_collector.emit(anchors, new Values(1, 2, 3));
多入口tuple把这个新tuple加到了多个tuple里面去了。

我们通过anchoring来构造这个tuple树,最后一件要做的事情是在你处理完当个tuple的时候告诉storm,通过OutputCollector类的ack或者fail方法来做,如果你回过头看看SplitSentence的例子,可以看到句子tuple在所有单词tuple被发出之后调用了ack

你可以调用outputCollector的fail方法去立即将消息源头发出的那个tuple标记为fail,比如你查询的数据库,发现了一个错误,你马上fail那个输入tuple,这样tuple就可以快速被重新处理了,因为你不需要等那个timeout时间让它自动fail.

每个你处理的tuple,必须被ack或者fail.因为storm追踪每个tuple要占用内存。所以如果你不ack/fail每个tuple,最终你会看到OutofMemory错误。

大多数bolt遵循这样的规律:读取一个tuple;发射一些新的tuple;在execute的结束的时候ack这个tuple.这些bolt往往是一些过滤器或者简单函数。Storm为这类规律封装了一个BasicBolt类。这个实现比之前的实现简单多了,但是功能上是一样的。发送到BasicOutputCollector的tuple会自动和输入tuple相关联,而在execute结束时那个输入tuple会被自动ack.作为对比,处理聚合和合并的bolt往往要处理一大堆的tuple之后才能被ack,而这类tuple通常都是多输入的tuple,那就不是这个类能罩得住的了。


Storm是怎么实现高效率的可靠性的?

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

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

关于storm怎么跟踪tuple还有一些细节,前面已经提到了,你可以自己设定你的topo中有多少个acker,那这又带来一个问题,当一个tuple需要acker时,它到底需要哪一个呢?storm使用一致性哈希来把一个spout-tuple-id对应到一个acker上,因为每一个tuple知道它的祖东tuple-id,所以它能算出使用哪个acker来ack


storm的另一个细节是acker怎么知道每一个spout tuple应该交给哪个task来处理。当一个spout发射一个新的tuple,它会简单的发一个消息给acker,并且告诉acker它自己的taskid,这样storm就有了taskid-tupleid的对应关系。当acker发现一个树完成处理了,他知道给哪个task发送成功的消息。

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树的状态的一个表示,不管这棵树多大,它只是简单的把这棵树上的所有创建以及被ack的tuple的tuple-id一起异或,排除巧合,只有所有的tuple经过创建和ack这两步操作之后,也就是经过XOR异或算法,64位的数字会变为0。当一个ack-val编程0了,它知道这棵树已经被处理完成了,因为tupleid是随机的64位数字,所以,ack val碰巧变成0的几率很小。


既然你已经理解了storm的可靠性算法,让我们一起过一遍所有失败的场景,并看看storm是如何避免数据丢失的。

1.由于对应的task挂掉了,一个tuple没有被ack:storm在的超时机制在超时之后会把这个tuple标记为失败,从而可以重新处理

2.Acker挂掉了:这种情况acker跟踪的所有tuple都会超时,重新处理

3.spout挂掉了:这种情况下给spout发送消息的消息源负责重新发送这些消息

(分布式的程序,只有一个acker是如果进行高可靠性的?比如我有4个进程,acker是怎么分布的?)


调整可靠性

acker-task是非常轻量级的,所以一个topo里面不需要太多的acker.你可以根据UI来跟踪它的性能,如果它的吞吐量看起来不正常,那你就需要多加点acker了。

如果可靠性对你而言不那么重要,即不太在意损失一些数据,那么不需要监控这个tupe以便获得更好的性能。不去跟踪消息的话会使得系统里面的消息数量减少一半,因为对每一个tuple都要发送ack消息,并且他需要更少的id来保存下游的tuple,减少带宽占用。

有3中方法去掉可靠性:

1.Config.TOPOLOGY_ACKERS设置成0,这样spout发射一个tuple后马上就会调用ack,tuple不会被跟踪

2.在tuple层面去掉可靠性。你可以发射tuple时不指定messageid来发到不跟踪某个tuple的目的(不明白)

3.如果你对一个tuple数里面的某一部分成不成功不关心,那么发射时unanchor他们,这样tuple就不在tuple树里面,就不会被跟踪了。


a>storm中检测到tuple被fail了,是不会自动重发的,必须自己编写逻辑去重发

b>max_spout_pending,系统内未ack/fail的tuple数量,超过了会怎么样?

问题解答

这个对于那种一个bolt, fieldsGrouping 接受tuple的情况,比如wordcount那个bolt,假如bolt用2个任务来执行(暂定叫a任务,和b任务吧),一个单词比如 cat,总是会流到a任务中执行,然后记录cat的数量,但是如果a任务死了,正在流的一个“cat” 就要重新发送,这时候会发送b任务吗? 如果发送到了b任务,哪cat的数量计数不就错误了吗?

这种场景就需要先将数据存储起来了,否则线程死掉数据肯定会丢失


请教一下,我们同一个spout源数据有多个业务处理场景,比如一个bolt直接进行存储数据到DB,另几个bolt进行不同的统计计算,当一个bolt宕掉后,其它bolt因为已经将数据存储到DB了,所以不想重复处理数据,又不想和数据库进行频繁交互有什么好的方式实现么?数据库我们使用mongoDB,当数据量大到一定程度后insert+update效率非常差


这种情况需要自己判断去重,如果tuple是重复的不要入库就好了

0 0