D19 Storm增强

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

storm的集群表面上看和hadoop的集群非常像。一个关键的区别是: 一个MapReduce Job最终会结束, 而一个Topology运永远运行(除非你显式的杀掉他)。
在Storm的集群里面有两种节点: 控制节点(master node)和工作节点(worker node)。
控制节点上面运行一个后台程序:Nimbus, 它的作用类似Hadoop里面的JobTracker。Nimbus负责在集群里面分布代码,分配工作给机器, 并且监控状态。
每一个工作节点上面运行一个叫做Supervisor的节点(类似 TaskTracker)。Supervisor会监听分配给它那台机器的工作,根据需要 启动/关闭工作进程。每一个工作进程执行一个Topology(类似 Job)的一个子集;一个运行的Topology由运行在很多机器上的很多工作进程 Worker(类似 Child)组成。

Stream
Stream是storm里面的关键抽象。一个stream是一个没有边界的tuple序列。storm提供一些元组来分布式地、可靠地把一个stream传输进一个新的stream。比如: 你可以把一个tweets流传输到热门话题的流。
storm提供的最基本的处理stream的原语是spout和bolt。你可以实现Spout和Bolt对应的接口以处理你的应用的逻辑。
通常Spout会从外部数据源(队列、数据库等)读取数据,然后封装成Tuple形式,之后发送到Stream中。
Spout是一个主动的角色,在接口内部有个nextTuple函数,Storm框架会不停的调用该函数。


bolt可以接收任意多个输入stream,有些bolt可能还会发射一些新的stream。一些复杂的流转换也就需要多个bolt。 
Bolt可以做任何事情: 运行函数, 过滤tuple, 做一些聚合, 做一些合并以及访问数据库等等。

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

spout和bolt所组成一个网络会被打包成topology, topology是storm里面最高一级的抽象(类似 Job)。

topology里面的每一个节点都是并行运行的。 在你的topology里面, 你可以指定每个节点的并行度, storm则会在集群里面分配那么多线程来同时计算。


storm使用tuple来作为它的数据模型。每个tuple是一堆值,每个值有一个名字,并且每个值可以是任何类型。总体来看,storm支持所有的基本类型、字符串以及字节数组作为tuple的值类型。
可以使用你自己定义的类型来作为值类型, 但要实现对应的序列化器(serializer)!

Tuple本来应该是一个Key-Value的Map,由于各个组件间传递的tuple的字段名称已经事先定义好了,所以Tuple只需要按序填入各个Value,所以就是一个Value List。
一个没有边界的、源源不断的、连续的Tuple序列就组成了Stream。


topology里面的每个节点必须定义它要发射的tuple的每个字段。 比如下面这个bolt定义它所发射的tuple包含两个字段,类型分别是: double和triple。

该bolt类不发射就不写。在最后一个bolt。要显示数据来测试时,相当于结尾。可以不写发射的字段
1
  @Override
2
    publicvoiddeclareOutputFields(OutputFieldsDeclarer declarer) {
3
        declarer.declare(newFields("double","triple"));
4
    }
5
}

一个简单的Topology
让我们来看一个简单的topology的例子, 我们看一下storm-starter里面的ExclamationTopology:
1
TopologyBuilder builder =newTopologyBuilder();
2
builder.setSpout(1,newTestWordSpout(),10);
3
builder.setBolt(2,newExclamationBolt(),3).shuffleGrouping(1);
4
builder.setBolt(3,newExclamationBolt(),2).shuffleGrouping(2);
这个Topology包含一个Spout和两个Bolt。Spout发射单词, 每个bolt在每个单词后面加个”!!!”。这三个节点被排成一条线: 
spout发射单词给第一个bolt, 第一个bolt然后把处理好的单词发射给第二个bolt;
如果spout发射的单词是["bob"]和["john"],;
那么第二个bolt会发射["bob!!!!!!"]和["john!!!!!!"]出来。

我们使用setSpout和setBolt来定义Topology里面的节点。这些方法接收我们指定的一个id, 一个包含处理逻辑的对象(spout或者bolt), 以及你所需要的并行度。
这个包含处理的对象如果是spout那么要实现IRichSpout的接口, 如果是bolt,那么就要实现IRichBolt接口.
最后一个指定并行度的参数是可选的。它表示集群里面需要多少个thread来一起执行这个节点。如果你忽略它那么storm会分配一个线程来执行这个节点。

setBolt方法返回一个InputDeclarer对象, 这个对象是用来定义Bolt的输入。 
这里第一个Bolt声明它要读取spout所发射的所有的tuple — 使用shuffle grouping。------>即第一次添加"!!!"
而第二个bolt声明它读取第一个bolt所发射的tuple。----->即第二次添加"!!!"
shufflegrouping表示所有的tuple会被随机的分发给bolt的所有task。


如果你想第二个bolt读取spout和第一个bolt所发射的所有的tuple, 那么你应该这样定义第二个bolt:
1
builder.setBolt(3,newExclamationBolt(),5).shuffleGrouping(1)
2
.shuffleGrouping(2);
让我们深入地看一下这个topology里面的spout和bolt是怎么实现的。Spout负责发射新的tuple到这个topology里面来。 
TestWordSpout从["nathan", "mike", "jackson", "golda", "bertels"]里面随机选择一个单词发射出来。TestWordSpout里面的nextTuple()方法是这样定义的:
1
publicvoidnextTuple() {
2
    Utils.sleep(100);
3
    final String[] words =newString[] {"nathan","mike",
4
    
5
    final Random rand =newRandom();
6
    final String word = words[rand.nextInt(words.length)];
7
    _collector.emit(newValues(word));
8
}
1
public static class ExclamationBolt implements IRichBolt {
2
    OutputCollector _collector;
3
 
4
    publicvoidprepare(Map conf, TopologyContext context,
5
                        OutputCollector collector) {
6
        _collector = collector;
7
    }
8
 
9
    publicvoidexecute(Tuple tuple) {
10
        _collector.emit(tuple,newValues(tuple.getString(0) +"!!!"));
11
        _collector.ack(tuple);
12
    }
13
 
14
    publicvoidcleanup() {
15
    }//该节点执行结束时执行的方法
16
 
17
    publicvoiddeclareOutputFields(OutputFieldsDeclarer declarer) {
18
        declarer.declare(newFields("word"));
19
    }
20
}
ExclamationBolt执行给单词添加"!!!"的步骤
prepare方法提供给bolt一个Outputcollector用来发射tuple。Bolt可以在任何时候发射tuple — 在prepare, execute或者cleanup方法里面, 或者甚至在另一个线程里面异步发射。这里prepare方法只是简单地把OutputCollector作为一个类字段保存下来给后面execute方法使用。
execute方法从bolt的一个输入接收tuple(一个bolt可能有多个输入源). ExclamationBolt获取tuple的第一个字段,加上”!!!”之后再发射出去。
如果一个bolt有多个输入源,你可以通过调用 Tuple.getSourceComponent方法来知道它是来自哪个输入源的。

clean up()方法在bolt被关闭的时候调用, 它应该清理所有被打开的资源。但是集群不保证这个方法一定会被执行。比如执行task的机器down掉了,那么根本就没有办法来调用那个方法。 cleanup设计的时候是被用来在local mode的时候才被调用(也就是说在一个进程里面模拟整个storm集群), 并且你想在关闭一些topology的时候避免资源泄漏。

storm的运行有两种模式: 本地模式和分布式模式. 在本地模式中, storm用一个进程里面的线程来模拟所有的spout和bolt。本地模式对开发和测试来说比较有用。 

在分布式模式下,当你提交topology给master的时候, 同时也把topology的代码提交了。master负责分发你的代码并且负责给你的topolgoy分配工作进程。如果一个工作进程挂掉了, master节点会把认为重新分配到其它节点。

下面是以本地模式运行ExclamationTopology的代码:
1
Config conf =newConfig();
2
conf.setDebug(true);
3
conf.setNumWorkers(2);
4
 
5
LocalCluster cluster =newLocalCluster();
6
cluster.submitTopology("test", conf, builder.createTopology());
7
Utils.sleep(10000);
8
cluster.killTopology("test");
9
cluster.shutdown();
首先, 这个代码定义通过定义一个LocalCluster对象来定义一个进程内的集群。通过调用submitTopology(topology.name,conf,topology)方法来提交topology。
topology的名字是用来唯一区别一个topology的,这样你然后可以用这个名字来杀死这个topology的。

Conf对象可以配置很多东西, 下面两个是最常见的:
  • TOPOLOGY_WORKERS(setNumWorkers) 定义你希望集群分配多少个工作进程给你来执行这个topology. topology里面的每个组件会需要线程来执行。每个组件到底用多少个线程是通过setBolt和setSpout来指定的。这些线程都运行在工作进程里面. 每一个工作进程包含一些节点的一些工作线程。比如, 如果你指定300个线程,60个进程, 那么每个工作进程里面要执行5个线程, 而这5个线程可能属于不同的组件(Spout, Bolt)。你可以通过调整每个组件的并行度以及这些线程所在的进程数量来调整topology的性能。
  • TOPOLOGY_DEBUG(setDebug), 当它被设置成true的话, storm会记录下每个组件所发射的每条消息。这在本地环境调试topology很有用, 但是在线上这么做的话会影响性能的。
运行中的Topology主要由以下三个组件组成的:

Worker processes(进程)
Executors (threads)(线程)
Tasks

Spout或者Bolt的Task个数一旦指定之后就不能改变了,而Executor的数量可以根据情况来进行动态的调整。默认情况下# executor = #tasks即一个Executor中运行着一个Task

流分组策略(Stream grouping)
流分组策略告诉topology如何在两个组件之间发送tuple。 要记住, spouts和bolts以很多task的形式在topology里面同步执行。
当Bolt A的一个task要发送一个tuple给Bolt B, 它应该发送给Bolt B的哪个task呢?
stream grouping专门回答这种问题的。在我们深入研究不同的stream grouping之前, 让我们看一下storm-starter里面的另外一个topology。

wordcount例子分析
WordCountTopology读取一些句子, 输出句子里面每个单词出现的次数.
1
TopologyBuilder builder =new TopologyBuilder();
2
 
3
builder.setSpout(1,newRandomSentenceSpout(),5);
4
builder.setBolt(2,new SplitSentence(),8)
5
        .shuffleGrouping(1);
6
builder.setBolt(3,new WordCount(),12)
7
        .fieldsGrouping(2,newFields("word"));
SplitSentence对于句子里面的每个单词发射一个新的tuple, WordCount在内存里面维护一个单词->次数的mapping, WordCount每收到一个单词, 它就更新内存里面的统计状态。

有好几种不同的stream grouping:
  • 最简单的grouping是shuffle grouping, 它随机发给任何一个task。上面例子里面RandomSentenceSpout和SplitSentence之间用的就是shuffle grouping, shuffle grouping对各个task的tuple分配的比较均匀。
  • 一种更有趣的grouping是fields grouping, SplitSentence和WordCount之间使用的就是fields grouping, 这种grouping机制保证相同field值的tuple会去同一个task, 这对于WordCount来说非常关键,如果同一个单词不去同一个task, 那么统计出来的单词次数就不对了。

 这两种要根据实际要求选择!!

fieldsgrouping是stream合并,stream聚合以及很多其它场景的基础。在背后呢, fieldsgrouping使用的一致性哈希来分配tuple的

下面是一些常用的 “路由选择” 机制:
Storm的Grouping即消息的Partition机制。当一个Tuple被发送时,如何确定将它发送给某个(些)Task来处理??

①ShuffleGrouping:随机选择一个Task来发送。
②FiledGrouping:根据Tuple中Fields来做一致性hash,相同hash值的Tuple被发送到相同的Task。
③AllGrouping:广播发送,将每一个Tuple发送到所有的Task。
④GlobalGrouping:所有的Tuple会被发送到某个Bolt中的id最小的那个Task。
⑤NoneGrouping:不关心Tuple发送给哪个Task来处理,等价于ShuffleGrouping。
⑥ DirectGrouping:直接将Tuple发送到指定的Task来处理。

可靠的消息处理

在这个教程的前面,我们跳过了有关tuple的一些特征。这些特征就是storm的可靠性API: storm如何保证spout发出的每一个tuple都被完整处理。
Storm允许用户在Spout中发射一个新的源Tuple时为其指定一个MessageId,这个MessageId可以是任意的Object对象。多个源Tuple可以共用同一个MessageId,表示这多个源Tuple对用户来说是同一个消息单元。Storm的可靠性是指Storm会告知用户每一个消息单元是否在一个指定的时间内被完全处理。
完全处理的意思是该MessageId绑定的源Tuple以及由该源Tuple衍生的所有Tuple都经过了Topology中每一个应该到达的Bolt,并且都被该bolt所指定的逻辑进行了处理。----->就是保证每个tuple都接受处理了!没有消息漏掉。

在Spout中由message 1绑定的tuple1和tuple2分别经过bolt1和bolt2的处理,然后生成了两个新的Tuple,并最终流向了bolt3。当bolt3处理完之后,称message 1被完全处理了。
Storm中的每一个Topology中都包含有一个Acker组件。Acker组件的任务就是跟踪从Spout中流出的每一个messageId所绑定 的Tuple树中的所有Tuple的处理情况。如果在用户设置的最大超时时间内这些Tuple没有被完全处理,那么Acker会告诉Spout该消息处理 失败,相反则会告知Spout该消息处理成功。
那么Acker是如何记录Tuple的处理结果呢??
A xor A = 0.
A xor B…xor B xor A = 0,其中每一个操作数出现且仅出现两次。
在Spout中,Storm系统会为用户指定的MessageId生成一个对应的64位的整数,作为整个Tuple Tree的RootId。RootId会被传递给Acker以及后续的Bolt来作为该消息单元的唯一标识。同时,无论Spout还是Bolt每次新生成 一个Tuple时,都会赋予该Tuple一个唯一的64位整数的Id。
当Spout发射完某个MessageId对应的源Tuple之后,它会告诉Acker自己发射的RootId以及生成的那些源Tuple的Id。而当 Bolt处理完一个输入Tuple并产生出新的Tuple时,也会告知Acker自己处理的输入Tuple的Id以及新生成的那些Tuple的Id。 Acker只需要对这些Id进行异或运算,就能判断出该RootId对应的消息单元是否成功处理完成了。

spout--A--bolt--B--bolt--C--bolt……
每个组件的两端异或运算来判断是否完成了此组件的逻辑运算


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值