Storm原理

来源:互联网 发布:豆瓣fm推荐算法 编辑:程序博客网 时间:2024/05/29 19:46

1.1 Storm简介

Twitter Storm 是使用 Clojure(发音同 closure)语言实现的。 Clojure 是 Lisp 语言的一种现代方言。类似于 Lisp,Clojure 支持一种功能性编程风格,但 Clojure 还引入了一些特性来简化多线程编程(一种对创建 Storm 很有用的特性)。Twitter列举了Storm的三大类应用:

1.信息流处理{Stream processing},Storm可用来实时处理新数据和更新数据库,兼具容错性和可扩展性。

2.连续计算{Continuous computation}

3.Storm可进行连续查询并把结果即时反馈给客户端,比如把Twitter上的热门话题发送到浏览器中。

3.分布式远程程序调用{Distributed RPC},Storm可用来并行处理密集查询。Storm的拓扑结构是一个等待调用信息的分布函数,当它收到一条调用信息后,会对查询进行计算,并返回查询结果。举个例子Distributed RPC可以做并行搜索或者处理大集合的数据。

Storm集群由一个主节点和多个工作节点组成。主节点运行了一个名为“Nimbus”的守护进程,用于分配代码、布置任务及故障检测。每个工作节点都运行了一个名为“Supervisor”的守护进程,用于监听工作,开始并终止工作进程。Nimbus和Supervisor都能快速失败,而且是无状态的,这样一来它们就变得十分健壮,两者的协调工作是由Apache ZooKeeper来完成的。

Storm的术语包括Stream、Spout、Bolt、Task、Worker、Stream Grouping和Topology。Stream是被处理的数据Sprout是数据源Bolt处理数据Task是运行于Spout或Bolt中的线程。Worker是运行这些线程的进程。StreamGrouping规定了Bolt接收什么东西作为输入数据数据可以随机分配(术语为Shuffle),或者根据字段值分配(术语为Fields),或者 广播(术语为All),或者总是发给一个Task(术语为Global),也可以不关心该数据(术语为None),或者由自定义逻辑来决定(术语为 Direct)。Topology是由Stream Grouping连接起来的Spout和Bolt节点网络。

storm拓扑的并行度可以从以下4个维度进行设置: 1、node(服务器):指一个storm集群中的supervisor服务器数量。 2、worker(jvm进程):指整个拓扑中worker进程的总数量,这些数量会随机的平均分配到各个node。 3、executor(线程):指某个spout或者bolt的总线程数量,这些线程会被随机平均的分配到各个worker。 4、task(spout/bolt实例):task是spout和bolt的实例,它们的nextTuple()和execute()方法会被executors线程调用。除非明确指定,storm会给每个executor分配一个task。如果设置了多个task,即一个线程持有了多个spout/bolt实例. 注意:以上设置的都是总数量,这些数量会被平均分配到各自的宿主上,而不是设置每个宿主进行多少个进程/线程。

一个worker是一个jvm进程,它只属于一个 topology,一个worker可以起多个executors去跑某个topology。 一个executor会跑多个tasks(默认配置是一个executor一个task),但是一个executor只属于一个spout或者bolt。

阿里的内推,问到storm内部的消息传递的机制。这里search总结一下:

一个work是一个进程,这个进程会启动一个线程(NettyServer(0.9版本))会监听tcp端口,将收到的消息存到到ZeroMQ(0.8版本)消息队列里,然后在反序列化根据id将他交给不同的excutor处理。消息除了从其他work来,也可以从本个work来,这些消息被直接放入队列中。

并行度的动态调整 对storm拓扑的并行度进行调整有2种方法: 1、kill topo—>修改代码—>编译—>提交拓扑 2、动态调整 第1种方法太不方便了,有时候topo不能说kill就kill,另外,如果加几台机器,难道要把所有topo kill掉还要修改代码? 因此storm提供了动态调整的方法,动态调整有2种方法: 1、ui方式:进入某个topo的页面,点击rebalance即可,此时可以看到topo的状态是rebalancing。但此方法只是把进程、线程在各个机器上重新分配,即适用于增加机器,或者减少机器的情形,不能调整worker数量、executor数量等 2、cli方式:storm rebalance 举个例子

storm rebalance toponame -n 7 -e filter-bolt=6 -e hdfs-bolt=8

将topo的worker数量设置为7,并将filter-bolt与hdfs-bolt的executor数量分别设置为6、8. 此时,查看topo的状态是rebalancing,调整完成后,可以看到3台机器中的worker数量分别为3、2、2

1.2 Storm特点

Storm的主要特点:

1. 简单的编程模型。类似于MapReduce降低了并行批处理复杂性,Storm降低了进行实时处理的复杂性。

2. 可以使用各种编程语言。你可以在Storm之上使用各种编程语言。默认支持ClojureJavaRubyPython。要增加对其他语言的支持,只需实现一个简单的Storm通信协议即可。

3. 容错性。Storm会管理工作进程和节点的故障。

4. 水平扩展。计算是在多个线程、进程和服务器之间并行进行的。

5. 可靠的消息处理。Storm保证每个消息至少能得到一次完整处理。任务失败时,它会负责从消息源重试消息。

6. 快速。系统的设计保证了消息能得到快速的处理,使用ØMQ作为其底层消息队列。

7. 本地模式。Storm有一个本地模式,可以在处理过程中完全模拟Storm集群。这让你可以快速进行开发和单元测试。

StormHadoop的对比:

hadoop是实现了mapreduce的思想,将数据切片计算来处理大量的离线数据。hadoop处理的数据必须是已经存放在hdfs上或者类似hbase的数据库中,所以hadoop实现的时候是通过移动计算到这些存放数据的机器上来提高效率而storm不同,storm是一个流计算框架,处理的数据是实时消息队列中的,所以需要我们写好一个topology逻辑放在那,接收进来的数据来处理,所以是通过移动数据平均分配到机器资源来获得高效率。

hadoop的优点是处理数据量大(瓶颈是硬盘和namenode,网络等),分析灵活,可以通过实现dslmdx等拼接hadoop命令或者直接使用hivepig等来灵活分析数据。适应对大量维度进行组合分析缺点就是慢:每次执行前要分发jar包,hadoop每次map数据超出阙值后会将数据写入本地文件系统,然后在reduce的时候再读进来

storm的优点是全内存计算,因为内存寻址速度是硬盘的百万倍以上,所以storm的速度相比较hadoop非常快(瓶颈是内存,cpu)缺点就是不够灵活:必须要先写好topology结构来等数据进来分析


1.3 Storm架构

1.3.1 Storm集群组成

Storm集群类似于一个Hadoop集群。 然而你在Hadoop的运行“MapReduce job”,在storm上你运行 topologies (拓扑)”。 job”和“topologies ”本身有很大的不同 - 一个关键的区别是,MapReduce的工作最终完成,而topologies 处理消息永远保持(或直到你杀了它)Strom集群有主要有两类节点:主节点和工作节点。 主节点上运行一个叫做“Nimbus”的守护进程,也就是类似Hadoop的“JobTracker”。 Nimbus 负责在集群分发的代码,将任务分配给其他机器,和故障监测。

每个工作节点运行一个叫做”Supervisor”的守护进程  Supervisor监听分配给它的机器,根据Nimbus 的委派在必要时启动和关闭工作进程。 每个工作进程执行topology 的一个子集。一个运行中的topology 由很多运行在很多机器上的工作进程组成。


                                               

Nimbus Supervisors 之间所有的协调工作是通过 一个Zookeeper 集群。 此外,Nimbus的守护进程和 Supervisors 守护进程是无法连接和无状态的;所有的状态维持在Zookeeper 或保存在本地磁盘上。这意味着你可以 kill -9 Nimbus Supervisors 进程,所以他们不需要做备份。 这种设计导致storm集群具有令人难以置信的稳定性。

Zookeeper是一种开源的、高效的分布式应用协调服务。一个Zookeeper集群(以下简称ZK集群)通常是由多个Zookeeper节点组成的。ZK集群有一个Leader节点,其他的都是Follower节点。Leader节点负责写服务和数据同步;Follower节点负责读服务。ZK集群支持高可用。一般ZK集群中只要有超过半数的节点存活,ZK集群就仍然是高可用的(因此通常建议ZK集群的节点数设置为奇数个)。

ZK集群负责Nimbus与Supervisor节点之间的通信。监控各个节点之间的状态。我们提交任务的时候是在Nimbus上执行的。Nimbus通过ZK集群将任务分发给Supervisor节点并监控Supervisor节点的执行状态,如果有某个节点出现故障,Nimbus节点就会通过ZK集群将那个节点上的任务重新分发给其他Supervisor节点执行。这也意味着一个Storm集群也是高可用的。不过整个Storm集群只有一个Nimbus节点。当Nimbus节点出现故障的时候,任务不会停止执行,可我们也无法终止任务。即一旦Nimbus节点出现问题,任务的执行不会受到影响,但是任务的管理会出现故障。此时重新恢复Nimbus服务即可。不过我们可能会丢失从故障到恢复这段时间的数据。Nimbus节点不支持高可用,这也是Storm面临的问题之一。一般情况下,Nimbus承担的压力比较小,不会轻易出现故障。


1.3.1 Storm拓扑结构

Storm 实现了一种数据流模型,其中数据持续地流经一个转换实体网络(参见  1)。一个数据流的抽象称为一个流,这是一个无限的元组序列。元组就像一种使用一些附加的序列化代码来表示标准数据类型(比如整数、浮点和字节数组)或用户定义类型的结构。每个流由一个唯一 ID 定义,这个 ID 可用于构建数据源和接收器 (sink) 的拓扑结构。流起源于喷嘴(spout)Spout将数据从外部来源流入 Storm 拓扑结构中。

 1. 一个普通的 Storm 拓扑结构的概念性架构

接收器(或提供转换的实体)称为螺栓(bolt)。螺栓实现了一个流上的单一转换和一个Storm 拓扑结构中的所有处理。Bolt既可实现 MapReduce 之类的传统功能,也可实现更复杂的操作(单步功能),比如过滤、聚合或与数据库等外部实体通信。典型的 Storm 拓扑结构会实现多个转换,因此需要多个具有独立元组流的BoltBoltSpout都实现为 Linux系统中的一个或多个任务。 

但是,Storm 架构中一个最有趣的特性是有保障的消息处理Storm 可保证一个Spout发射出的每个元组都会处理;如果它在超时时间内没有处理,Storm 会从该Spout重放该元组。此功能需要一些聪明的技巧来在拓扑结构中跟踪元素,也是 Storm 的重要的附加价值之一。Bolts的主要方法是execute(死循环)连续处理传入的tuple,成功处理完每一个tuple调用OutputCollector的ack方法,以通知storm这个tuple被处理完成了。当处理失败时,可以调fail方法通知Spout端可以重新发送该tuple。

除了支持可靠的消息传送外,Storm 还使用 ZeroMQ 最大化消息传送性能(删除中间排队,实现消息在任务间的直接传送)。ZeroMQ 合并了拥塞检测并调整了它的通信,以优化可用的带宽。


1.4 Storm原理

1.4.1 Storm组成

计算拓扑: Topologies

一个实时计算应用程序的逻辑在storm里面被封装到topology对象里面, 我把它叫做计算拓补. Storm里面的topology相当于Hadoop里面的一个MapReduce Job, 它们的关键区别是:一个MapReduce Job最终总是会结束的, 然而一个stormtopoloy会一直运行 — 除非你显式的杀死它。 一个TopologySpoutsBolts组成的图状结构, 链接SpoutsBolts的则是Stream groupings。消息的发射omit与订阅subscribe.

消息流: Streams

消息流是storm里面的最关键的抽象对象。一个消息流是一个没有边界的tuple序列, 而这些tuples会被以一种分布式的方式并行地创建和处理。 对消息流的定义主要是对消息流里面的tuple的定义, 我们会给tuple里的每个字段一个名字。 并且不同tuple的对应字段的类型必须一样。 也就是说: 两个tuple的第一个字段的类型必须一样, 第二个字段的类型必须一样, 但是第一个字段和第二个字段可以有不同的类型。 在默认的情况下, tuple的字段类型可以是:integer, long, short, byte, string, double, float, booleanbyte array 你还可以自定义类型 — 只要你实现对应的序列化器。

每个消息流在定义的时候会被分配给一个id 因为单向消息流是那么的普遍,OutputFieldsDeclarer定义了一些方法让你可以定义一个stream而不用指定这个id。在这种情况下这个stream会有个默认的id: 1.

消息源: Spouts

消息源Spoutsstorm里面一个topology里面的消息生产者。一般来说消息源会从一个外部源读取数据并且向topology里面发出消息: tuple 消息源Spouts可以是可靠的也可以是不可靠的。一个可靠的消息源可以重新发射一个tuple如果这个tuple没有被storm成功的处理, 但是一个不可靠的消息源Spouts一旦发出一个tuple就把它彻底忘了 — 也就不可能再发了。

消息源可以发射多条消息流stream。要达到这样的效果, 使用OutFieldsDeclarer.declareStream来定义多个stream, 然后使用SpoutOutputCollector来发射指定的stream

Spout类里面最重要的方法是nextTuple要么发射一个新的tupletopology里面或者简单的返回如果已经没有新的tuple了。要注意的是nextTuple方法不能block Spout的实现, 因为storm在同一个线程上面调用所有消息源Spout的方法。

另外两个比较重要的Spout方法是ackfailstorm在检测到一个tuple被整个topology成功处理的时候调用ack, 否则调用failstorm只对可靠的spout调用ackfail

消息处理者: Bolts

所有的消息处理逻辑被封装在bolts里面。 Bolts可以做很多事情: 过滤, 聚合, 查询数据库等等等等。

Bolts可以简单的做消息流的传递。复杂的消息流处理往往需要很多步骤, 从而也就需要经过很多Bolts。比如算出一堆图片里面被转发最多的图片就至少需要两步: 第一步算出每个图片的转发数量。第二步找出转发最多的前10个图片。(如果要把这个过程做得更具有扩展性那么可能需要更多的步骤)。

Bolts可以发射多条消息流, 使用OutputFieldsDeclarer.declareStream定义stream 使用OutputCollector.emit来选择要发射的stream

Bolts的主要方法是execute, 它以一个tuple作为输入,Bolts使用OutputCollector来发射tuple, Bolts必须要为它处理的每一个tuple调用OutputCollectorack方法,以通知storm这个tuple被处理完成了。– 从而我们通知这个tuple的发射者Spouts 一般的流程是: Bolts处理一个输入tuple, 发射0个或者多个tuple, 然后调用ack通知storm自己已经处理过这个tuple了。storm提供了一个IBasicBolt会自动调用ack

Stream groupings 消息分发策略

定义一个Topology的其中一步是定义每个bolt接受什么样的流作为输入。stream grouping就是用来定义一个stream应该如果分配给Bolts上面的多个Tasks

storm里面有6种类型的stream grouping:

1. Shuffle Grouping: 随机分组, 随机派发stream里面的tuple 保证每个bolt接收到的tuple数目相同。

2. Fields Grouping:按字段分组, 比如按userid来分组, 具有同样useridtuple会被分到相同的Bolts 而不同的userid则会被分配到不同的Bolts

3. All Grouping 广播发送, 对于每一个tuple 所有的Bolts都会收到。

4. Global Grouping: 全局分组, 这个tuple被分配到storm中的一个bolt的其中一个task。再具体一点就是分配给id值最低的那个task

5. Non Grouping: 不分组, 这个分组的意思是说stream不关心到底谁会收到它的tuple。目前这种分组和Shuffle grouping是一样的效果, 有一点不同的是storm会把这个bolt放到这个bolt的订阅者同一个线程里面去执行。

6. Direct Grouping: 直接分组, 这是一种比较特别的分组方法,用这种分组意味着消息的发送者举鼎由消息接收者的哪个task处理这个消息。 只有被声明为Direct Stream的消息流可以声明这种分组方法。而且这种消息tuple必须使用emitDirect方法来发射。消息处理者可以通过TopologyContext来或者处理它的消息的taskid (OutputCollector.emit方法也会返回taskid)


1.4.2 Storm工作原理

Storm中有对于流stream的抽象,流是一个不间断的无界的连续tuple,注意Storm在建模事件流时,把流中的事件抽象为tuple即元组,后面会解释storm中如何使用tuple

Storm认为每个stream都有一个stream源,也就是原始元组的源头,所以它将这个源头抽象为spoutspout可能是连接twitter api并不断发出tweets,也可能是从某个队列中不断读取队列元素并装配为tuple发射。

有了源头即spout也就是有了stream,那么该如何处理stream内的tuple呢,同样的思想twitter将流的中间状态转换抽象为Boltbolt可以消费任意数量的输入流,只要将流方向导向该bolt,同时它也可以发送新的流给其他bolt使用,这样一来,只要打开特定的spout(管口)再将spout中流出的tuple导向特定的bolt,又bolt对导入的流做处理后再导向其他bolt或者目的地。

我们可以认为spout就是一个的水龙头,并且每个水龙头里流出的水是不同的,我们想拿到哪种水就拧开哪个水龙头,然后使用管道将水龙头的水导向到一个水处理器(bolt),水处理器处理后再使用管道导向另一个处理器或者存入容器中。

为了增大水处理效率,我们很自然就想到在同个水源处接上多个水龙头并使用多个水处理器,这样就可以提高效率。没错Storm就是这样设计的,看到下图我们就明白了。

对应上文的介绍,我们可以很容易的理解这幅图,这是一张有向无环图,Storm将这个图抽象为Topology即拓扑(的确,拓扑结构是有向无环的),拓扑是storm中最高层次的一个抽象概念,它可以被提交到storm集群执行,一个拓扑就是一个流转换图,图中每个节点是一个spout或者bolt,图中的边表示bolt订阅了哪些流,当spout或者bolt发送元组到流时,它就发送元组到每个订阅了该流的bolt(这就意味着不需要我们手工拉管道,只要预先订阅,spout就会将流发到适当bolt上)。

插个位置说下stormtopology实现,为了做实时计算,我们需要设计一个拓扑图,并实现其中的Bolt处理细节,Storm中拓扑定义仅仅是一些Thrift结构体,这样一来我们就可以使用其他语言来创建和提交拓扑。

Storm将流中元素抽象为tuple,一个tuple就是一个值列表value listlist中的每个value都有一个name,并且该value可以是基本类型,字符类型,字节数组等,当然也可以是其他可序列化的类型。拓扑的每个节点都要说明它所发射出的元组的字段的name,其他节点只需要订阅该name就可以接收处理。0

The tuple is the main data structure in Storm. A tuple is a named list of values, where each value can be any type. Tuples are dynamically typed -- the types of the fields do not need to be declared. Tuples have helper methods like getInteger and getString to get field values without having to cast the result. Storm needs to know how to serialize all the values in a tuple. By default, Storm knows how to serialize the primitive types, strings, and byte arrays. If you want to use another type, you'll need to implement and register a serializer for that type. Seehttp://github.com/apache/incubator-storm/wiki/Serialization for more info.


Storm(四)消息的可靠传输

  消息在各个节点之间的传递,要保证准确性,一般要做到两点:一是接收者必须能收到,二是必须只收到一个,或者收到第二个时知道重复了,能够把重复的舍弃。这在Storm里面是通过Guaranteeing Message Processing和Transactional Topologies这两种机制来保障的,这一篇主要介绍可靠传输。

  在网络的传输层(TCP)以及链路层,为了确保数据的可靠传输,发送方在发送数据的时候会指定一个序列号,接收方收到之后返回ACK,如果发送方等待ACK超时,就会认为数据丢失,进行重传。Storm可靠传输机制的原理是基本一致的,当Spout发射数据的时候,会附带一个id,当这个tuple被消费完之后,由ACKer线程调用Spout的ack方法告诉Spout这个消息已经被成功处理,因为Spout可能会有很多task并行执行,ACKer调用的ack一定是发射该tuple那个task的acker方法,如果超过一定时间这个tuple还没有被消费完,Spout的fail方法会被调用,Spout可以根据情况选择是否重传。这个超时时间可以在Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS中设置,默认为30s,有时候我们也可以在消息处理节点上直接调用fail方法,让Spout不等到超时就立即重置发送,比如这样的场景,最后往数据库里存数据的节点,由于某种原因存储失败,就可以fail让Spout重新发这个数据。

  每个节点都可能收到tuple,根据这个tuple可能要发射新的tuple,这些相互关联tuple就可以构成一棵树,如下图所示,一个句子发出来,被Bolt拆分,这些拆分后再次被emit的数据就和原来的tuple形成父子关系。

  Storm提供的可靠传输的API主要要求开发者做两件事,第一件事是在这些tuple之间创建link,构建这个tuple树,这个link在storm中叫做anchoring,创建的方式是在调用collector.emit发射数据的时候和输入tuple做关联,第二件事是tuple的发出者要知道这个tuple有没有被成功消费,这个是通过在消费节点调用collector.ack或collector.fail方法实现。

  像这个图,每个节点代表一个tuple,这就是4个tuple,Spout调用collector.emit(A)把这个tuple发送出来,消费节点通过调用collector.emit(A, B)和collector.emit(A, C)在A和B以及A和C之间创建anchor。可以通过

List<Tuple> list;

list.add(A);

list.add(B);

collector.emit(list,D);

  在D和多个上级tuple之间创建anchor。这种多源的绑定会破坏树形的结构,构成了一个DAG(有向无环图)。

  如果消费节点都不调用ack会怎么样?因为storm要在内存中维持各个tuple的状态和从属关系,如果所有的tuple都不被ack,那么超时之前storm就会存很多很多的数据和信息,就有可能发生内存耗尽。

  前面我写的collector都是OutputCollector这个类的实例,我们经常会在Bolt里面做类似的事情,那就是接收一个Tuple、发射新的Tuple并与前面的Tuple建立anchor,然后向前一个Tuple发送ack,为了简化这种重复性的操作,Storm封装了一个BasicBolt类,并把OutputCollector也做了封装,变成了BasicOutputCollector,这样一来,用户就只是简单调用collector.emit(B),所有的anchor、ack操作就都由Storm来完成了。

  当Spout发射出来的tuple被消费完之后,由ACKer线程调用发射该tuple的task的acker方法通知Spout,ACKer线程的数量应该怎么设置?这个属性由Config.TOPOLOGY_ACKERS来控制,默认为1,这个线程一般不用太多,一般情况下一个已经够用,可以参照StormUI上的吞吐量来改变它的大小。

  ACKer线程维护着一个个DAG,每当有一个tuple被ACK、以及有一个新的tuple被发出,只要这个DAG发生变化了,ACKer线程就会得到通知,因为ACKer线程可能有多个,tuple又不携带ACKer线程的信息,那它怎么知道该通知哪个ACKer呢?这里有一个默认的实现,就是messageid,本文一开始举了TCP的例子,发送方发送时会指定一个序列号,作为接收ACK的标识,messageid就是Spout发某个数据的时候指定的序列号,把messageid对ACKer线程数量取模,得到的序号就是处理这个DAG的ACKer线程。另外,Spout在发数据的时候也会通知对应ACKer自己是哪一个task,等到这个消息被处理完,这个task的ack方法就会被调用。

  ACKer线程会扫描整个DAG,当发现DAG上的每一个tuple都被ACK了,就认为这个消息被完全处理了。但是,ACKer要怎么扫描DAG,一个tuple树动不动就可能有上万个节点,去遍历它开销太大,Storm采用了一种很巧妙的方法,是Storm性能优化上一个关键的突破:所有的tuple在创建的时候都会被Storm分配一个64位长的随机id,下文把它标记为tupleid。前面提到,Spout在发射原始数据的时候会附带一个messageid用以可靠传输。Spout发出的tuple以及所有衍生tuple都会携带这个messageid,ACKer线程为每一个messageid维护了一个64位的字符串作为校验和(下文标记为checksum),每当有tuple被ack(消费掉)或者emit(新生成),对应的ACKer线程就会得到这个tuple对应的messageid和tupleid,ACKer把新得到的tupleid和checksum去做异或,当checksum的值变成0的时候,就认为这个DAG被完全处理了,因为只有当这些随机生成的tupleid都被异或两次(一次是生成一次是被消费)时,checksum才能是0。当然这个话说的太绝对,是有可能出现偶然情况让checksum在DAG还没有被完全处理的时候就变成0的,但是因为随机id 64位的长度,假设每秒有10k的ack,那么数学上算一下,需要5000万年才可能出现一次这种偶然。再者,即便这种偶然发生了,还有可能因为某个节点调用了fail造成整个数据的重传,从而使出现错误的几率更低。

  最后看几个能够导致fail的情况,并且看看Storm是怎么处理的:

  1. 处理tuple的task挂了,所以没ack:这种情况下发送节点等待timeout,数据会被重传。
  2. ACKer线程挂了:Spout发送的所有tuple都会因timeout而重传。
  3. Spout挂了:这个要依赖数据源比如Kestrel或者Kafka的恢复机制。
  4. Nimbus或Supervisor挂了:他们都是无状态的,但他们会把状态信息存储在zookeeper里面,重启之后可以读取之前的状态,继续处理。

 

  如果可靠传输不那么重要,出于性能考虑,可以关掉可靠传输,关闭的方法有这么几种:一、把Config.TOPOLOGY_ACKERS设成0,如果ACKer线程数是0,那么tuple在Spout中发出来之后立即就会被执行ack方法。二、前面说了,ACKer对整个过程的管理依赖于Spout发送tuple之后附带的一个messageid,如果Spout发送的时候不指定,这个功能就没有了。三、bolt中发射新的tuple的时候不要和前面的tuple建立anchor。

  这里面屡屡提到DAG被消费完全,Storm里面用的词是complete,其实不仅仅所有的tuple都被ACK了才叫complete,它包含两种情况:tuple全部被ack和某节点出现了fail都叫做complete,一旦complete了,这个tuple tree就要被删掉,如果是所有tuple都被ack,那么tuple tree被删除之后继续后面的处理;如果fail了,tuple tree被删掉之后还要根据Spout的策略重新发送失败的数据。

  ACKer本质上也是一个Bolt,并且使用RotatingMap来管理超时。


以上讲的是每个tuple至少被处理一次,但如果需要每个消息有且仅有一次,如果某个tuple失败,导致了spout重传root tuple,有些tuple就会被多次处理,那怎么办?

Trident将stream中的tuples分成batches进行处理,API封装了对这些batches的处理过程,保证tuple只被处理一次。处理batches中间结果存储在TridentState对象中。

记住一点,Trident 是通过小数据块(batch)的方式来处理 tuple 的,而且每个 batch 都会有一个唯一的 txid。spout 的特性是由他们所提供的容错性保证机制决定的,而且这种机制也会对每个 batch 发生作用。事务型 spout 包含以下特性:

  1. 每个 batch 的 txid 永远不会改变。对于某个特定的 txid,batch 在执行重新处理操作时所处理的 tuple 集和它的第一次处理操作完全相同。
  2. 不同 batch 中的 tuple 不会出现重复的情况(某个 tuple 只会出现在一个 batch 中,而不会同时出现在多个 batch 中)。
  3. 每个 tuple 都会放入一个 batch 中(处理操作不会遗漏任何的 tuple)。

这是一种很容易理解的 spout,其中的数据流会被分解到固定的 batches 中。Storm-contrib 项目中提供了一种基于 Kafka 的事务型 spout 实现。

看到这里,你可能会有这样的疑问:为什么不在拓扑中完全使用事务型 spout 呢?这个原因很好理解。一方面,有些时候事务型 spout 并不能提供足够可靠的容错性保障,所以不需要使用事务型 spout。比如,TransactionalTridentKafkaSpout 的工作方式就是使得带有某个 txid 的 batch 中包含有来自一个 Kafka topic 的所有 partition 的 tuple。一旦一个 batch 被发送出去,在将来无论重新发送这个 batch 多少次,batch 中都会包含有完全相同的 tuple 集,这是由事务型 spout 的语义决定的。现在假设TransactionalTridentKafkaSpout 发送出的某个 batch 处理失败了,而与此同时,Kafka 的某个节点因为故障下线了。这时你就无法重新处理之前的 batch 了(因为 Kafka 的节点故障,Kafka topic 必然有一部分 partition 无法获取到),这个处理过程也会因此终止。

这就是要有“模糊事务型” spout 的原因了 —— 模糊事务型 spout 支持在数据源节点丢失的情况下仍然可以实现恰好一次的处理语义。我们会在下一节讨论这类 spout。

顺便提一点,如果 Kafka 支持数据复制,那么就可以放心地使用事务型 spout 提供的容错性机制了,因为这种情况下某个节点的故障不会导致数据丢失,不过 Kafka 暂时还不支持该特性。(本文的写作时间应该较早,Kakfa 早就已经可以支持复制的机制了 —— 译者注)。

在讨论“模糊事务型” spout 之前,让我们先来看看如何为事务型 spout 设计一种支持恰好一次语义的 State。这个 State 就称为 “事务型 state”,它支持对于特定的 txid 永远只与同一组 tuple 相关联的特性。

假如你的拓扑需要计算单词数,而且你准备将计数结果存入一个 K-V 型数据库中。这里的 key 就是单词,value 对应于单词数。从上面的讨论中你应该已经明白了仅仅存储计数结果是无法确定某个 batch 中的tuple 是否已经被处理过的。所以,现在你应该将 txid 作为一种原子化的值与计数值一起存入数据库。随后,在更新计数值的时候,你就可以将数据库中的 txid 与当前处理的 batch 的 txid 进行比对。如果两者相同,你就可以跳过更新操作 —— 由于 Trident 的强有序性处理机制,可以确定数据库中的值是对应于当前的 batch 的。如果两者不同,你就可以放心地增加计数值。由于一个 batch 的 txid 永远不会改变,而且 Trident 能够保证 state 的更新操作完全是按照 batch 的顺序进行的,所以,这样的处理逻辑是完全可行的。

下面来看一个例子。假如你正在处理 txid 3,其中包含有以下几个 tuple:

["man"]["man"]["dog"]

假如数据库中有以下几个 key-value 对:

man => [count=3, txid=1]dog => [count=4, txid=3]apple => [count=10, txid=2]

其中与 “man” 相关联的 txid 为 1。由于当前处理的 txid 为 3,你就可以确定当前处理的 batch 与数据库中存储的值无关,这样你就可以放心地将 “man” 的计数值加上 2 并更新 txid 为 3。另一方面,由于 “dog” 的 txid 与当前的 txid 相同,所以,“dog” 的计数是之前已经处理过的,现在不能再对数据库中的计数值进行更新操作。这样,在结束 txid3 的更新操作之后,数据库中的结果就会变成这样:

man => [count=5, txid=3]dog => [count=4, txid=3]apple => [count=10, txid=2]

模糊事务型 spout(Opaque transactional spouts)

前面已经提到过,模糊事务型 spout 不能保证一个 txid 对应的 batch 中包含的 tuple 完全一致。模糊事务型 spout 有以下的特性:

  1. 每个 tuple 都会通过某个 batch 处理完成。不过,在 tuple 处理失败的时候,tuple 有可能继续在另一个 batch 中完成处理,而不一定是在原先的 batch 中完成处理。

OpaqueTridentKafkaSpout 就具有这样的特性,同时它对 Kafka 节点的丢失问题具有很好的容错性。OpaqueTridentKafkaSpout在发送一个 batch 的时候总会总上一个 batch 结束的地方开始发送新 tuple。这一点可以保证 tuple 不会被遗漏,而且也不会被多个 batch 处理。

不过,模糊事务型 spout 的缺点就在于不能通过 txid 来识别数据库中的 state 是否是已经处理过的。这是因为在 state 的更新的过程中,batch 有可能会发生变化。

在这种情况下,你应该在数据库中存储更多的 state 信息。除了一个结果值和 txid 之外,你还应该存入前一个结果值。我们再以上面的计数值的例子来分析以下这个问题。假如你的 batch 的部分计数值是 “2”,现在你需要应用一个更新操作。假定现在数据库中的值是这样的:

{ value = 4,  prevValue = 1,  txid = 2}
  • 情形1:假如当前处理的 txid 为 3,这与数据库中的 txid 不同。这时可以将 “prevValue” 的值设为 “value” 的值,再为 “value” 的值加上部分计数的结果并更新 txid。执行完这一系列操作之后的数据库中的值就会变成这样:
{ value = 6,  prevValue = 4,  txid = 3}
  • 情形2:如果当前处理的 txid 为 2,也就是和数据库中存储的 txid 一致,这种情况下的处理逻辑与上面的 txid 不一致的情况又有所不同。因为此时你会知道数据库中的更新操作是由上一个拥有相同 txid 的batch 做出的。不过那个 batch 有可能与当前的 batch 并不相同,所以你需要忽略它的操作。这个时候,你应该将 “prevValue” 加上 batch 中的部分计数值来计算新的 “value”。在这个操作之后数据库中的值就会变成这样:
{ value = 3,  prevValue = 1,  txid = 2}

这种方法之所以可行是因为 Trident 具有强顺序性处理的特性。一旦 Trident 开始处理一个新的 batch 的状态更新操作,它永远不会回到过去的 batch 的处理上。同时,由于模糊事务型 spout 会保证 batch 之间不会存在重复 —— 每个 tuple 只会被某一个 batch 完成处理 —— 所以你可以放心地使用 prevValue 来更新 value。

非事务型 spout(Non-transactional spouts)

非事务型 spout 不能为 batch 提供任何的安全性保证。非事务型 spout 有可能提供一种“至多一次”的处理模型,在这种情况下 batch 处理失败后 tuple 并不会重新处理;也有可能提供一种“至少一次”的处理模型,在这种情况下可能会有多个 batch 分别处理某个 tuple。总之,此类 spout 不能提供“恰好一次”的语义。

不同类型的 Spout 与 State 的总结

下图显示了不同的 spout/state 的组合是否支持恰好一次的消息处理语义:

spout-state

模糊事务型 state 具有最好的容错性特征,不过这是以在数据库中存储更多的内容为代价的(一个 txid 和两个 value)。事务型 state 要求的存储空间相对较小,但是它的缺点是只对事务型 spout 有效。相对的,非事务型要求的存储空间最少,但是它也不能提供任何的恰好一次的消息执行语义。

你选择 state 与 spout 的时候必须在容错性与存储空间占用之间权衡。可以根据你的应用的需求来确定哪种组合最适合你。



关于storm的wordCount的例子:

git clone https://github.com/qinzhaokun/storm-start.git

欢迎指出不足指出不足之处。

0 0
原创粉丝点击