Storm是基于数据流的实时处理系统,提供了大吞吐量的实时计算能力。通过数据入口获取每条到来的数据,在一条数据到达系统的时候,立即会在内存中进行相应的计算;Storm适合要求实时性较高的数据分析场景。


1.Storm框架



上面这幅图是Stom框架图,和很多分布式系统一样,基于zk作为集群配置运行的元数据基础平台。



nimbus和supervisor是服务器端守护进程。



以下是对启动一个应用所需要的集群上JVM进程线程的简单介绍,建议记忆后再继续阅读。



· Nodes (服务器):指配置在一个 Storm 集群中的服务器,会执行 topology 的一部分运算。一个 Storm 集群可以包括一个或者多个工作 node 。


· Workers (JVM 虚拟机):指一个 node 上相互独立运行的 JVM 进程 。每个 node 可以配置运行一个或者多个 worker 。一个topology 会分配到一个或者多个 worker 上运行。


· Executor (线程):指一个 worker 的jvm 进程中运行的 Java 线程 。多个 task 可以指派给同一个 executer 来执行。除非是明确指定, Storm 默认会给每个 executor 分配一个 task。


· Task (bolt/spout 实例): task 是 spout 和bolt 的实例, 它们的 nextTuple() 和execute() 方法会被executors 线程调用执行。


1、一个Storm集群的基本组件


storm的集群表面上看和hadoop的集群非常像。但是在Hadoop上面你运行的是MapReduce的Job, 而在Storm上面你运行的是Topology。它们是非常不一样的 — 一个关键的区别是: 一个MapReduce Job最终会结束, 而一个Topology运永远运行(除非你显式的杀掉他)。


在Storm的集群里面有两种节点: 控制节点(master node)和工作节点(worker node)。控制节点上面运行一个后台程序:Nimbus, 它的作用类似Hadoop里面的JobTracker。Nimbus负责在集群里面分布代码,分配工作给机器, 并且监控状态。


每一个工作节点上面运行一个叫做Supervisor的节点(类似 TaskTracker)。Supervisor会监听分配给它那台机器的工作,根据需要 启动/关闭工作进程。每一个工作进程执行一个Topology(类似 Job)的一个子集;一个运行的Topology由运行在很多机器上的很多工作进程 Worker(类似 Child)组成。


Nimbus和Supervisor之间的所有协调工作都是通过一个Zookeeper集群来完成。并且,nimbus进程和supervisor都是快速失败(fail-fast)和无状态的。所有的状态要么在Zookeeper里面, 要么在本地磁盘上。这也就意味着你可以用kill -9来杀死nimbus和supervisor进程, 然后再重启它们,它们可以继续工作, 就好像什么都没有发生过似的。这个设计使得storm不可思议的稳定。



2、Tuple(元组)


元组是信息传递的基本单元,是一个命名的值列表,元组中的字段可以是任何类型的对象。拓扑的每个节点必须声明emit(发射)的元组的输出字段。


首先,在declareOutputFields方法中declarer.declare(new Fields("double","triple"));声明了输出为["line","woed"]二元数组。


然后,在prepare方法中把OutputCollector对象赋给私有成员变量。this.collector = collector;


最后,由collector发射元组。collector.emit(input, new Values(val*2,val*3));



storm使用Tuple来作为它的数据模型。每个tuple是一堆值,每个值有一个名字,并且每个值可以是任何类型, 在我的理解里面一个tuple可以看作一个没有方法的java对象。总体来看,storm支持所有的基本类型、字符串以及字节数组作为tuple的值类 型。你也可以使用你自己定义的类型来作为值类型, 只要你实现对应的序列化器(serializer)。


一个Tuple代表数据流中的一个基本的处理单元,例如一条cookie日志,它可以包含多个Field,每个Field表示一个属性。



Tuple本来应该是一个Key-Value的Map,由于各个组件间传递的tuple的字段名称已经事先定义好了,所以Tuple只需要按序填入各个Value,所以就是一个Value List。


一个没有边界的、源源不断的、连续的Tuple序列就组成了Stream。


3、Streams (流)


Stream在Storm中是一个核心的抽象概念。一个流是由无数个元组序列构成,这些元组并行、分布式的被创建和执行。 在stream的许多元组中,Streams被定义为以Fields区域命名的一种模式。流由元组组成,使用OutputFieldsDeclarer声明流及其模式。


Stream是storm里面的关键抽象。一个stream是一个没有边界的tuple序列。storm提供一些原语来分布式地、可靠地把一个stream传输进一个新的stream。比如: 你可以把一个tweets流传输到热门话题的流。


storm提供的最基本的处理stream的原语是spout和bolt。你可以实现Spout和Bolt对应的接口以处理你的应用的逻辑。


spout是流的源头。比如一个spout可能从Kestrel队列里面读取消息并且把这些消息发射成一个流。又比如一个spout可以调用twitter的一个api并且把返回的tweets发射成一个流。


通常Spout会从外部数据源(队列、数据库等)读取数据,然后封装成Tuple形式,之后发送到Stream中。Spout是一个主动的角色,在接口内部有个nextTuple函数,Storm框架会不停的调用该函数。


bolt可以接收任意多个输入stream, 作一些处理, 有些bolt可能还会发射一些新的stream。一些复杂的流转换, 比如从一些tweet里面计算出热门话题, 需要多个步骤, 从而也就需要多个bolt。 Bolt可以做任何事情: 运行函数, 过滤tuple, 做一些聚合, 做一些合并以及访问数据库等等。


Bolt处理输入的Stream,并产生新的输出Stream。Bolt可以执行过滤、函数操作、Join、操作数据库等任何操作。Bolt是一个被动的 角色,其接口中有一个execute(Tuple input)方法,在接收到消息之后会调用此函数,用户可以在此方法中执行自己的处理逻辑。


spout和bolt所组成一个网络会被打包成topology, topology是storm里面最高一级的抽象(类似 Job), 你可以把topology提交给storm的集群来运行。


每一个Stream在声明的时候都会赋予一个id。单个Stream——spouts和bolts,可以使用 OutputFieldsDeclarer  的convenience方法声明一个stream,而不用指定一个id。但是这种方法会给予一个默认的id——default




4、Spouts


在Topology中,每个Spout都是一个Streams源,通常情况下,Spouts会从外部源读取Tuple,并输入这些Tuple到Topology中 。


Spouts既是可靠的又是不可靠的 ,因为,可靠的spout会在发送Tuple失败的情况下,重复发送;相反,不可靠的spout会忘记它发送过的Tuple,无论是否成功。 在集群中,应该是每个node的JVM中启动一个线程跑spout。



Spout代码过程:


Spouts能够发送多个流:使用 OutputFieldsDeclarer (interface)的declareStream方法声明多个流,并且当使用 SpoutOutputCollector (实现2,接口模式)的emit方法可以指定这个流去发送Tuple。


Spouts的主要方法:

nextTuple()发送tuple, nextTuple可以发送一个新的Tuple到Topology,或者当没有新的Tuple被发送的时候,就简单的返回。对于任何spout的实现, nextTuple都不能阻塞,因为Storm调用的所有spout都是基于同一个线程!



List<Integer> emit(String streamId,List<Object> tuple,Object messageId);


emit方法向外发射数据,它的返回值是改消息所发生目标的TaskID集合。其输入参数为:


  • streamId:消息将被输出到的流;
  • tuple:要输出的消息,为一个object列表;
  • messageId:输出消息的标记信息。如设置为null,则storm将不会追踪改消息。


ack fail 方法,它们都会被调用,当Storm发现一个tuple被从spout发射后,要么成功地完成的通过topology,要么错误的完成。ack 和 fail 方法只有在可靠的spouts下才能被调用。spout可靠性,请搜本页下面内容,或移至代码。


可靠的spout


collector.emit(List tuple, Object messageId); 不可靠的spout


collector.emit(List tuple); 在使用了可靠的消息保证机制时,需要下游的bolt的execute方法中调用:


collector.ack(Tupe input);


以通知上游,该Tupe已经被当前节点处理完成。当Spout在超时时间内未等到ack确认或者fail确认,Storm将认为该节点处理失败,则调用spout的fail方法处理;相应地,如果处理成功,则调用Spout的ack方法。



declareOutputFields: 这是Spout中一个很有用的方法,用来定义不同的数据流和元组。在一个topology中,一个Spout可能发送很多数据消息,同时下游也可能有很多Bolt组件接收Spout发出的消息,但往往某个Bolt并不想接收Spout发来的所有数据,可能只需要接收某一类型数据流中的某些数据。Storm为我们提供给了这样的“订阅”机制,即Spout可以发送多种多样的数据流,而下游的Bolt可以根据自己的需求进行订阅,其实现的关键方法就是declareOutputFields。



open 方法是在打开Spout数据源的时Storm为我们调用的,相当于一个初始化方法,因此里面可以执行一些必要的初始化代码。但需要注意的是,在并发的Spout中,有多少个线程(executor),该方法就会被调用多少次。有关executor的概念在下文中将提及。因此在实际项目中,该处牵涉到的一些资源问题,需要慎重使用。



5、Bolts


在Topologies中所有的处理都会在bolts中被执行,它能够 过滤tuple、函数操作、合并(连接join、聚合aggregation)、数据库读写 等。Bolt可以做复杂的流传输,需要多步骤、多bolt的连接。


Bolt也可以发射出一个或多个流,它需要使用 OutputFieldsDeclarer  类的  declareStream  方法声明多个流,并且需要指定这个流去使用 OutputCollector l类的 emit 方法去发射。


当你声明一个bolt的输入流时,你需要订阅一个指定的其他组件的流。 每一个流的订阅都是一个个添加。 InputDeclarer 类可以声明一个流在默认的流id上。   declarer.shuffleGrouping("1")  说明在组件“1”上订阅了这个默认流,等价于 declarer.shuffleGrouping("1", DEFAULT_STREAM_ID)。


Bolts的主要 方法是 execute  方法,它会吸收作为输入的一个新Tuple。Bolts使用  OutputCollector  对象发射新的Tuples。Bolts必须对每一个tuple调用 OutputCollector  的 ack  方法,以便于Storm知道什么时候元组们被处理完成(可以最终确定它的安全对于包装这个初始化spout tuples)。  共同处理一个输入元组的情况下,发射0或多个元组们基于元组,然后包装输入元组,Storm提供一个 IBasicBolt 接口的自动包装。


在Bolts异步处理的时候,完全可以启动新线程;同时 OutputCollector 是线程安全的,可以在任何时候被调用。


Bolt中的关键方法有execute、prepare、declareOutputFields和cleanup等。


declareOutputFields: 与 Spout 相同,Bolt 也可以输出多个数据流。为了实现这个功能,可以先通过 OutputFieldsDeclarer 的 declareStream 方法来声明定义不同的数据流,然后在发送数据时在 OutputCollector 的 emit 方法中将数据流 id 作为参数来实现数据发送的功能。


在定义 Bolt 的输入数据流时,你需要从其他的 Storm 组件中订阅指定的数据流。如果你需要从其他所有的组件中订阅数据流,你就必须要在定义 Bolt 时分别注册每一个组件。


execute :**Bolt 的关键方法是 execute 方法。execute 方法负责接收一个元组作为输入,并且使用 OutputCollector 对象发送新的元组。在接收时,可以通过Tupe.getValueByField()方法获取指定的元组,也可以根据元组List的下标接收或者全部接收。


如果有消息可靠性保障的需求,Bolt 必须为它所处理的每个元组调用 OutputCollector 的 ack 方法,以便 Storm 能够了解元组是否处理完成(并且最终决定是否可以响应最初的 Spout 输出元组树)。一般情况下,对于每个输入元组,在处理之后可以根据需要选择不发送还是发送多个新元组,然后再响应(ack)输入元组。IBasicBolt 接口能够实现元组的自动应答。


对于需要保证消息可靠性的topology,bolt也需要在emit数据的时候, 将传入的Tupe作为anchor进行锚定( Anchoring


collector.emit(tuple, new Values(word));



List<Integer> emit(String streamId,Collection<Tuple> anchors,List<Object> tuple);


emit方法向外发射数据,它的返回值是改消息所发生目标的TaskID集合。其输入参数为:


  • streamId:消息将被输出到的流;
  • 多锚定。
  • tuple:要输出的消息,为一个object列表;


prepare: 此方法类似Spout中的open方法,在初始化bolt时调用。同样地,如果一个bolt有多个executor线程,则该方法将被执行多次。


cleanup: 在bolt执行完毕后关闭时执行,可以释放一些资源等。需要注意的是,在本地模式(LocalCluster)中,该方法一定会执行,但是在集群模式下,Storm不保证该方法一定执行。




6、Topologies(拓扑)


Topology是Storm中实时应用的一种封装。 其功能 analogous to a MapReduce job ,但唯一不同的是它是循环执行的——无数据流等待,有数据流执行,直到被kill progress。



一个Topology是spouts和bolts组成并被Stream groupings连接的一副流程图,相关概念如下:


Resources:


  • TopologyBuilder: use this class to construct topologies in Java:在java中,该类构建了topologies。
  • Running topologies on a production cluster:在生产集群中,运行多个topologies。
  • Local mode: Read this to learn how to develop and test topologies in local mode. 在本地模型中开发和测试topologies。

Topology结构:

先看此 topology 的配置:


Config conf = new Config(); 


conf.setNumWorkers(2); // 为此 topology 配置两个 worker 进程


topologyBuilder.setSpout(“blue-spout”, new BlueSpout(), 2); // blue-spout 并行度=2


topologyBuilder.setBolt(“green-bolt”, new GreenBolt(), 2) // green-bolt 并行度=2 


.setNumTasks(4) // 为此 green-bolt 配置 4 个 task 


.shuffleGrouping(“blue-spout”);


topologyBuilder.setBolt(“yellow-bolt”, new YellowBolt(), 6) // yellow-bolt 并行度=6 


.shuffleGrouping(“green-bolt”);


StormSubmitter.submitTopology( 


“mytopology”, 


conf, 


topologyBuilder.createTopology() 


); 


从上面的代码可以知道:


  • 这个 topology 装备了2个 worker 进程,也就是同样的工作会有 2 个进程并行进行,可以肯定地说,2个 worker 肯定比1个 worker 执行效率要高很多,但是并没有2倍的差距;
  • 配置了一个 blue-spout,并且为其指定了 2 个 executor,即并行度为2;
  • 配置了一个 green-bolt,并且为其指定了 2 个 executor,即并行度为2;
  • 配置了一个 yellow-bolt,并且为其指定了 6 个 executor,即并行度为6;



可以看出,这个图片完整无缺地还原了代码里设定的 topology 结构:


  • 图左最大的灰色方框,表示这个 topology;
  • topology 里面刚好有两个白色方框,表示2个 worker 进程;
  • 每个 worker 里面的灰色方框表示 executor 线程,可以看到2个 worker 方框里各有5个 executor,为什么呢?因为代码里面指定的 spout 并行度=2,green-bolt并行度=2,yellow-bolt并行度=6,加起来刚好是10,而配置的 worker 数量为2,那么自然地,这10个 executor 会均匀地分配到2个 worker 里面;
  • 每个 executor 里面的黄蓝绿(写着Task)的方框,就是最小的处理单元 task 了。大家仔细看绿色的 Task 方框,与其他 Task 不同的是,两个绿色方框同时出现在一个 executor 方框内。为什么会这样呢?大家回到上文看 topology 的定义代码,topologyBuilder.setBolt(“green-bolt”, new GreenBolt(), 2).setNumTasks(4),这里面的 setNumTasks(4) 表示为该 green-bolt 指定了4个 task,且 executor 的并行度为2,那么自然地,这4个 task 会均匀地分配到2个 executor 里面;
  • 图右的三个圆圈,依次是蓝色的 blue-spout,绿色的 green-bolt 和黄色的 yellow-bolt,并且用箭头指示了三个组件之间的关系。spout 是数据的产生元件,而 green-bolt 则是数据的中间接收节点,yellow-bolt 则是数据的最后接收节点。这也是 DAG 的体现,有向的(箭头不能往回走)无环图。


7、Stream groupings 流分组


定义一个拓扑部分是指定了每个bolt门闩的流都应该作为输入被接收。一个流分组定义为: 在门闩的任务之中如何区分流 。


在Storm中有8种流分组方式,通过实现 CustomStreamGrouping j接口,你可以实现一种风格流分组方式:


Storm 定义了八种内置数据流分组的方式:


1、Shuffle grouping(随机分组):这种方式会随机分发 tuple 给bolt 的各个 task,每个bolt 实例接收到的相同数量的 tuple 。


shuffleGrouping("word-normalizer")



2、Fields grouping(按字段分组):根据指定字段的值进行分组。它保证拥有相同域组合的值集发送给同一个 bolt。如说,一个数据流根据“ word”字段进行分组, 所有具有相同“ word ”字段值的 tuple 会路由到同一个 bolt 的 task 中 。


fieldsGrouping( "word-normalizer", new Fields("word"))



3、All grouping(全复制分组):将所有的 tuple 复制后分发给所有 bolt task。每个订阅数据流的 task 都会接收到 tuple 的拷贝。


4、Globle grouping(全局分组):这种分组方式将所有的 tuples 路由到唯一一个 task 上。Storm 按照最小的 task ID 来选取接收数据的 task 。注意,当使用全局分组方式时, 设置 bolt 的 task 并发度是没有意义的(spout并发有意义) ,因为所有 tuple 都转发到同一个 task 上了。使用全局分组的时候需要注意,因为所有的 tuple 都转发到一个 JVM 实例上, 可能会引起 Storm 集群中某个 JVM 或者服务器出现性能瓶颈或崩溃。


5、None grouping(不分组):在功能上和随机分组相同,是为将来预留的。


6、Direct grouping(指向型分组):数据源会调用 emitDirect() 方法来判断一个 tuple 应该由哪个 Storm 组件来接收。只能在声明了是指向型的数据流上使用。


7、Local or shuffle grouping (本地或随机分组):和随机分组类似,但是,会将 tuple 分发给同一个 worker 内的bolt task (如果 worker 内有接收数据的 bolt task )。其他情况下,采用随机分组的方式。取决于topology 的并发度,本地或随机分组可以减少网络传输,从而提高 topology 性能。


8、Partial Key grouping: The stream is partitioned by the fields specified in the grouping, like the Fields grouping, but are load balanced between two downstream bolts, which provides better utilization of resources when the incoming data is skewed.  This paper  provides a good explanation of how it works and the advantages it provides.



8、Reliability(可靠性)


Storm保证每一个spout tuple都将会在拓扑中完整的被处理。处理过程:它会追踪这个tuple tree被每一个spout tuple所触发,并且确定tuple tree已经成功完成。每个拓扑都有一个“信息超时”与之相关联。假如Storm未能检测到一个spout tuple已经超时完成,它将舍弃并重新执行这个tuple。


为了改善Storm的可靠性能力,你可以告诉Storm什么时候需要在元组树种创建一个新的边界,告诉Storm无论在什么时候都可以完成处理一个独立的tuple。Bolt们都使用了 OutputCollector 对象去发射tuple。“锚定”(实际上就是mark)的完成于这个emit方法,你可以声明一个元组使用了ack方法而被完成。



9、Tasks


每个喷口spout或者门闩bolt都有许多任务在集群中执行。 每一个任务对应一个执行线程,流分组定义了如何从一个任务集到另外一个任务集发送元组。你可以使用 TopologyBuilder  类的setSpout和setBolt方法,为每一个spout或bolt是设置并行度和并发度。


Ps:Tasks可以理解为每个节点上的任务实例,运行在对应executor线程上。



10、Workers


拓扑执行要通过一个或多个worker进程。每一个worker进程都是一个物理的JVM和这个拓扑中执行了一个所有这个任务的子集。


例子:如果拓扑的联合并发数为300,分配了50个worker,因此每一个worker将会执行6个task(task将作为worker的线程)。Storm将会均匀的分配任务到所有worker上。


Resources:


  • Config.TOPOLOGY_WORKERS: this config sets the number of workers to allocate for executing the topology

Worker结构:





11.Topology的并发机制:


storm的Worker、Executor、Task默认配置都是1


1、增加worker(本地模式无效,只有一个JVM)


Config对象的setNumWorkers()方法: 


Config config = new Config();


config.setNumWorkers(2):


2、配置executor 和 task


默认都为1,setXXX指定一个Worker中有几个线程,而后面的setNumXXX指定总共需要执行的tasks数量,因此,一个Thread--Executor中需要跑tasks/threads个任务。


        topologyBuilder.setSpout(SENTENCE_SPOUT_ID, spout, 2);


        // StormBaseSpout -> StormBaseBolt


        topologyBuilder.setBolt(SPLIT_BOLT_ID, bolt).setNumTasks(2).shuffleGrouping(SENTENCE_SPOUT_ID);


        // StormBaseBolt -> StormBaseBoltSecond


        topologyBuilder.setBolt(COUNT_BOLT_ID, boltSecond, 4).fieldsGrouping(SPLIT_BOLT_ID, new Fields("word"));


        // StormBaseBoltSecond -> StormBaseBoltThird


        topologyBuilder.setBolt(REPORT_BOLT_ID, boltThird).globalGrouping(COUNT_BOLT_ID);





12.storm的处理保障机制:



1、spout的可靠性


spout会记录它所发射出去的tuple,当下游任意一个bolt处理失败时spout能够重新发射该tuple。在spout的nextTuple()发送一个tuple时,为实现可靠消息处理需要给每个spout发出的tuple带上唯一ID,并将该ID作为参数传递给SpoutOutputCollector的emit()方法: collector.emit(new Values("value1","value2"), tupleID);


实际上Values extends ArrayList<Object>


保障过程中,每个bolt每收到一个tuple,都要向上游应答或报错,在tuple树上的所有bolt都确认应答,spout才会隐式调用 ack()方法 表明这条消息( 一条完整的流 )已经处理完毕,将会对编号ID的消息应答确认;处理报错、超时则会调用 fail()方法 。


2、bolt的可靠性


bolt的可靠消息处理机制包含两个步骤:


a、当发射衍生的tuple,需要锚定读入的tuple


b、当处理消息时,需要应答或报错


可以通过OutputCollector中emit()的一个重载函数锚定或tuple: collector.emit(tuple, new Values(word)); 并且需要调用一次this.collector.ack(tuple)应答。



13.ACK机制


为了保证数据能正确的被处理, 对于spout产生的每一个tuple, storm都会进行跟踪。


  这里面涉及到ack/fail的处理,如果一个tuple处理成功是指这个Tuple以及这个Tuple产生的所有Tuple都被成功处理, 会调用spout的ack方法;


  如果失败是指这个Tuple或这个Tuple产生的所有Tuple中的某一个tuple处理失败, 则会调用spout的fail方法;


  在处理tuple的每一个bolt都会通过OutputCollector来告知storm, 当前bolt处理是否成功。


  另外需要注意的,当spout触发fail动作时,不会自动重发失败的tuple,需要我们在spout中重新获取发送失败数据,手动重新再发送一次。


Ack原理


  Storm中有个特殊的task名叫acker,他们负责跟踪spout发出的每一个Tuple的Tuple树(因为一个tuple通过spout发出了,经过每一个bolt处理后,会生成一个新的tuple发送出去)。当acker(框架自启动的task)发现一个Tuple树已经处理完成了,它会发送一个消息给产生这个Tuple的那个task。


Acker的跟踪算法是Storm的主要突破之一,对任意大的一个Tuple树,它只需要恒定的20字节就可以进行跟踪。


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


要实现ack机制:


1,spout发射tuple的时候指定messageId


2,spout要重写BaseRichSpout的fail和ack方法


3,spout对发射的tuple进行缓存(否则spout的fail方法收到acker发来的messsageId,spout也无法获取到发送失败的数据进行重发),看看系统提供的接口,只有msgId这个参数,这里的设计不合理,其实在系统里是有cache整个msg的,只给用户一个messageid,用户如何取得原来的msg貌似需要自己cache,然后用这个msgId去查询,太坑爹了


3,spout根据messageId对于ack的tuple则从缓存队列中删除,对于fail的tuple可以选择重发。


4,设置acker数至少大于0;Config.setNumAckers(conf, ackerParal);


Storm的Bolt有BsicBolt和RichBolt:


  在BasicBolt中,BasicOutputCollector在emit数据的时候,会自动和输入的tuple相关联,而在execute方法结束的时候那个输入tuple会被自动ack。


  使用RichBolt需要在emit数据的时候,显示指定该数据的源tuple要加上第二个参数anchor tuple,以保持tracker链路,即collector.emit(oldTuple, newTuple);并且需要在execute执行成功后调用OutputCollector.ack(tuple), 当失败处理时,执行OutputCollector.fail(tuple);


由一个tuple产生一个新的tuple称为:anchoring,你发射一个tuple的同时也就完成了一次anchoring。


  ack机制即,spout发送的每一条消息,在规定的时间内,spout收到Acker的ack响应,即认为该tuple 被后续bolt成功处理;在规定的时间内(默认是30秒),没有收到Acker的ack响应tuple,就触发fail动作,即认为该tuple处理失败,timeout时间可以通过Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS来设定。或者收到Acker发送的fail响应tuple,也认为失败,触发fail动作


  注意,我开始以为如果继承BaseBasicBolt那么程序抛出异常,也会让spout进行重发,但是我错了,程序直接异常停止了



2.守护进程Storm Cluster


特点介绍:


Storm 采用云计算框架中最实用的master/slave结构,静态设置。


主节点为nimbus,半容错;从节点supervisor;中间关系通过Zookeeper维护。


半容错:由于Nimbus不参与数据处理过程,且topology不会自动停止。所以只能对topology初始化、任务分发和监控,不会影响任务处理过程。但如果supervisor挂掉,nimbus不能再重新指派任务。



当然,可以使用HA nimbus,这个我们以后谈###############。



进程组件:



如果大家还记得上一章的Storm框架吗?本章就是围绕该架构来分析机理,首先,来看下每个进程组件的功能。



Nimbus:


Nimbus守护进程主要负责管理 (类似于namenode+ResourceManager,因为Storm不需要hdfs),协调+监控 = topology的发布、任务指派、失败重新指派等。


Nimbus会记录所有supervisor节点的状态和分配给它们的task;如果supervisor没有heartbeat或不可达,它会将故障supervisor中的task重新分配到其他节点。



Supervisor:


Supervisor守护进程等待nimbus分配任务后,生成并监控workers(JVM进程)的执行。


Supervisor和worker都是运行在不同的JVM进程上(一个节点上可以运行多个JVM,每个JVM为一个进程,即worker),worker失败是由supervisor重启。


保障传输机制:当打开了可靠传输选项,传输到故障节点的tuple将不会收到应答确认,spout会因为超时而重新发射原始tuple(问题5),直到topology从故障中回复开始正常处理数据。



Zookeeper:


Storm 主要使用ZooKeeper   来协调一个集群中的状态信息 ,比如任务的分配情况,worker的状态,supervisor之间的nimbus的拓扑度量。   nimbus 和supervisor 节点之间的通信主要是结合 ZooKeeper 的状态变更通知和监控通知来处理的 。 (Zookeeper的在集群中最重要的应用!)



Thrift:


对于重量级的数据传输操作,比如发布topology 时传输jar 包,Storm 依赖Thirft 进行通信。(在以后的源码分析中会有详解)



在理解每个守护进程后,我们来看看他们之间是如何工作的。



集群运行机制:


Storm的集群主要由三部分组成:nimbus、zookeeper、supervisor。


master是不会与每个slaver直接通信,他们之间的交流是通过zk,其中包括:代码下载、心跳机制、任务分配、同步集群等等。


这里简单提一下zk的主要功能:1、同步少量数据。2、有监听触发机制。


感兴趣的朋友可以自行学习zk,这里默认大家使用过zk。



nimbus的元数据存储在zk中,由于supervisor并不会直接与其通信,所以才会有半容错的存在。


1、实际上,nimbus在获取代码后运行代码,解析配置和topology结构,得到需要分配多少worker、executor、task等等,topology运行时需要的资源。


2、把相关信息提交给zk。这里其实就是修改zk中的tree文件内容,至于zk中的Storm tree是什么样的接下来会讲到。


3、zk作为中间服务系统,会触发消息到对应的supervisor。


4、每个supervisor得到信息后,会根据信息分配资源,首先是worker的建立、开启多少个executor、提交对应的tasks。


至于并发度、并行度,这些在提交代码、配置时已经定义好了,不用多说,如何做到这点请参考上一章 Storm概念、原理详解及其应用(一)BaseStorm



zookeeper:


    Storm在zookeeper中生成的树型目录结构如下图:




可以看出,根目录为/storm,其下的每一个叶子目录都是Storm存储元数据的地方。


下面分别介绍ZooKeeper中每项数据的具体含义:



/storm/ workerbeats /<topology-id>/node-port:它存储由 node和port指定的Worker的运行状态 和一些统计信息,主要包括storm-id(也即topology-id)、当前Worker上所有Executor的统计信息(如发送的消息数目、接收的消息数目等)、当前Worker的启动时间以及最后一次更新这些信息的时间。在一个topology-id下面,可能有多个node-port节点。它的内容在运行过程中会被更新。



/storm/ storms /<topology-id>:它存储 Topology本身的信息 ,包括它的名字、启动时间、运行状态、要使用的Worker数目以及每个组件的并行度设置。它的内容在运行过程中是不变的。



/storm/ assignments /<topology-id>:它存储了Nimbus为每个 Topology分配的任务信息 ,包括该Topology在Nimbus机器本地的存储目录、被分配到的Supervisor机器到主机名的映射关系、每个Executor运行在哪个Worker上以及每个Executor的启动时间。该节点的数据在运行过程中会被更新。



/storm/ supervisors /<supervisor-id>:它存储 Supervisor机器本身的运行统计信息 ,主要包括最近一次更新时间、主机名、supervisor-id、已经使用的端口列表、所有的端口列表以及运行时间。该节点的数据在运行过程中也会被更新。



/storm/ errors /<topology-id>/<component-id>/e<sequential-id>:它存 储运行过程中每个组件上发生的错误信息 。<sequential-id>是一个递增的序列号,每一个组件最多只会保留最近的10条错误信息。它的内容在运行过程中是不变的(但是有可能被删除)。