本文翻译自storm官方wiki: https://github.com/nathanmarz/storm/wiki/Rationale
过去的十年是数据处理变革的十年, MapReduce, Hadoop以及一些相关的技术使得我们能处理的数据量比以前要大得多得多。但是这些数据处理技术都不是实时的系统 — 它们设计的目的也不是为了实时计算。没有什么办法可以简单地把hadoop变成一个实时计算系统。实时数据处理系统和批量数据处理系统在需求上有着本质的差别。
然而大规模的实时数据处理已经越来越成为一种业务需求了, 而缺少一个“实时版本的hadoop”已经成为数据处理整个生态系统的一个巨大缺失。
Storm填补了这个缺失。
Storm出现之前,你可能需要自己手动维护一个由消息队列和消息处理者所组成的实时处理网络, 消息处理者从消息队列取出一个消息进行处理, 更新数据库,发送消息给其它队列, 等等等等。不幸的是,这种方式有以下几个缺陷:
1. 单调乏味: 你花费了绝大部分开发时间去配置把消息发送到哪里, 部署消息处理者,部署中间消息节点 — 你的大部分时间花在设计, 配置这个数据处理框架上, 而你真正关心的消息处理逻辑在你的代码里面占的比例很少。
2. 脆弱: 不够健壮, 你要自己写代码保证所有的消息处理者和消息队列正常运行。
3. 伸缩性差: 当一个消息处理者的消息量达到阀值,你需要对这些数据进行分流, 你需要配置这些新的处理者以让他们处理分流的消息。
虽然对于一个大量消息处理系统来说,分解到最后就是消息队列和消息处理者的组合,而消息处理无疑是实时计算的基础。那么现在问题就是:怎样去做才能不丢失数据,可以很好的扩展到更大的消息量并且非常容易操作呢?
Storm满足你的需求。
Storm定义了一批实时计算的原语。如同hadoop大大简化了并行批量数据处理,storm的这些原语大大简化了并行实时数据处理。storm的一些关键特性如下:
1. 适用场景广泛: storm可以用来处理消息和更新数据库(消息流处理), 对一个数据量进行持续的查询并返回客户端(持续计算), 对一个耗资源的查询作实时并行化的处理(分布式方法调用), storm的这些基础原语可以满足大量的场景。
2. 可伸缩性高: Storm的可伸缩性可以让storm每秒可以处理的消息量达到很高。为了扩展一个实时计算任务,你所需要做的就是加机器并且提高这个计算任务的并行度设置(parallelism setting)。作为Storm可伸缩性的一个例证, 一个Storm应用在一个10个节点的集群上每秒处理1000000个消息 — 包括每秒一百多次的数据库调用。Storm使用ZooKeeper来协调集群内的各种配置使得Storm的集群可以很容易的扩展很大。
3. 保证无数据丢失: 实时系统必须保证所有的数据被成功的处理。 那些会丢失数据的系统的适用场景非常窄, 而storm保证每一条消息都会被处理, 这一点和S4相比有巨大的反差。
4. 异常健壮: 不像Hadoop — 出了名的难管理, storm集群非常容易管理。容易管理是storm的设计目标之一。
5. 容错性好:如果在消息处理过程中出了一些异常, storm会重新安排这个出问题的处理逻辑。 storm保证一个处理逻辑永远运行 — 除非你显式杀掉这个处理逻辑。
6. 语言无关性: 健壮性和可伸缩性不应该局限于一个平台。Storm的topology和消息处理组件可以用任何语言来定义, 这一点使得任何人都可以使用storm.
在这个教程里面我们将学习如何创建Topologies, 并且把topologies部署到storm的集群里面去。Java将是我们主要的示范语言, 个别例子会使用python以演示storm的多语言特性。
这个教程使用storm-starter项目里面的例子。我推荐你们下载这个项目的代码并且跟着教程一起做。先读一下:配置storm开发环境和新建一个strom项目这两篇文章把你的机器设置好。
storm的集群表面上看和hadoop的集群非常像。但是在Hadoop上面你运行的是MapReduce的Job, 而在Storm上面你运行的是Topology。它们是非常不一样的 — 一个关键的区别是: 一个MapReduce Job最终会结束, 而一个Topology运永远运行(除非你显式的杀掉他)。
在Storm的集群里面有两种节点: 控制节点(master node)和工作节点(worker node)。控制节点上面运行一个后台程序: Nimbus, 它的作用类似Hadoop里面的JobTracker。Nimbus负责在集群里面分布代码,分配工作给机器, 并且监控状态。
每一个工作节点上面运行一个叫做Supervisor的节点。Supervisor会监听分配给它那台机器的工作,根据需要 启动/关闭工作进程。每一个工作进程执行一个Topology的一个子集;一个运行的Topology由运行在很多机器上的很多工作进程组成。
Nimbus和Supervisor之间的所有协调工作都是通过一个Zookeeper集群来完成。并且,nimbus进程和supervisor都是快速失败(fail-fast)和无状态的。所有的状态要么在Zookeeper里面, 要么在本地磁盘上。这也就意味着你可以用kill -9来杀死nimbus和supervisor进程, 然后再重启它们,它们可以继续工作, 就好像什么都没有发生过似的。这个设计使得storm不可思议的稳定。
为了在storm上面做实时计算, 你要去建立一些topologies。一个topology就是一个计算节点所组成的图。Topology里面的每个处理节点都包含处理逻辑, 而节点之间的连接则表示数据流动的方向。
运行一个Topology是很简单的。首先,把你所有的代码以及所依赖的jar打进一个jar包。然后运行类似下面的这个命令。
1
|
strom jar all-your-code.jar backtype.storm.MyTopology arg1 arg2
|
这个命令会运行主类: backtype.strom.MyTopology, 参数是arg1, arg2。这个类的main函数定义这个topology并且把它提交给Nimbus。storm jar负责连接到nimbus并且上传jar文件。
因为topology的定义其实就是一个Thrift结构并且nimbus就是一个Thrift服务, 有可以用任何语言创建并且提交topology。上面的方面是用JVM
-based语言提交的最简单的方法, 看一下文章: 在生产集群上运行topology去看看怎么启动以及停止topologies。
Stream是storm里面的关键抽象。一个stream是一个没有边界的tuple序列。storm提供一些原语来分布式地、可靠地把一个stream传输进一个新的stream。比如: 你可以把一个tweets流传输到热门话题的流。
storm提供的最基本的处理stream的原语是spout和bolt。你可以实现Spout和Bolt对应的接口以处理你的应用的逻辑。
spout的流的源头。比如一个spout可能从Kestrel队列里面读取消息并且把这些消息发射成一个流。又比如一个spout可以调用twitter的一个api并且把返回的tweets发射成一个流。
bolt可以接收任意多个输入stream, 作一些处理, 有些bolt可能还会发射一些新的stream。一些复杂的流转换, 比如从一些tweet里面计算出热门话题, 需要多个步骤, 从而也就需要多个bolt。 Bolt可以做任何事情: 运行函数, 过滤tuple, 做一些聚合, 做一些合并以及访问数据库等等。
spout和bolt所组成一个网络会被打包成topology, topology是storm里面最高一级的抽象, 你可以把topology提交给storm的集群来运行。topology的结构在Topology那一段已经说过了,这里就不再赘述了。
topology里面的每一个节点都是并行运行的。 在你的topology里面, 你可以指定每个节点的并行度, storm则会在集群里面分配那么多线程来同时计算。
一个topology会一直运行直到你显式停止它。storm自动重新分配一些运行失败的任务, 并且storm保证你不会有数据丢失, 即使在一些机器意外停机并且消息被丢掉的情况下。
storm使用tuple来作为它的数据模型。每个tuple是一堆值,每个值有一个名字,并且每个值可以是任何类型, 在我的理解里面一个tuple可以看作一个没有方法的java对象。总体来看,storm支持所有的基本类型、字符串以及字节数组作为tuple的值类型。你也可以使用你自己定义的类型来作为值类型, 只要你实现对应的序列化器(serializer)。
topology里面的每个节点必须定义它要发射的tuple的每个字段。 比如下面这个bolt定义它所发射的tuple包含两个字段,类型分别是: double和triple。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public
class
DoubleAndTripleBolt
implements
IRichBolt {
private
OutputCollectorBase _collector;
@Override
public
void
prepare(Map conf, TopologyContext context, OutputCollectorBase collector) {
_collector = collector;
}
@Override
public
void
execute(Tuple input) {
int
val = input.getInteger(
0
);
_collector.emit(input,
new
Values(val*
2
, val*
3
));
_collector.ack(input);
}
@Override
public
void
cleanup() {
}
@Override
public
void
declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(
new
Fields(
"double"
,
"triple"
));
}
}
|
declareOutputFields方法定义要输出的字段 : ["double", "triple"]。这个bolt的其它部分我们接下来会解释。
让我们来看一个简单的topology的例子, 我们看一下storm-starter里面的ExclamationTopology:
1
2
3
4
5
6
|
TopologyBuilder builder =
new
TopologyBuilder();
builder.setSpout(
1
,
new
TestWordSpout(),
10
);
builder.setBolt(
2
,
new
ExclamationBolt(),
3
)
.shuffleGrouping(
1
);
builder.setBolt(
3
,
new
ExclamationBolt(),
2
)
.shuffleGrouping(
2
);
|
这个Topology包含一个Spout和两个Bolt。Spout发射单词, 每个bolt在每个单词后面加个”!!!”。这三个节点被排成一条线: spout发射单词给第一个bolt, 第一个bolt然后把处理好的单词发射给第二个bolt。如果spout发射的单词是["bob"]和["john"], 那么第二个bolt会发射["bolt!!!!!!"]和["john!!!!!!"]出来。
我们使用setSpout和setBolt来定义Topology里面的节点。这些方法接收我们指定的一个id, 一个包含处理逻辑的对象(spout或者bolt), 以及你所需要的并行度。
这个包含处理的对象如果是spout那么要实现IRichSpout的接口, 如果是bolt,那么就要实现IRichBolt接口.
最后一个指定并行度的参数是可选的。它表示集群里面需要多少个thread来一起执行这个节点。如果你忽略它那么storm会分配一个线程来执行这个节点。
setBolt方法返回一个InputDeclarer对象, 这个对象是用来定义Bolt的输入。 这里第一个Bolt声明它要读取spout所发射的所有的tuple — 使用shuffle grouping。而第二个bolt声明它读取第一个bolt所发射的tuple。shuffle grouping表示所有的tuple会被随机的分发给bolt的所有task。给task分发tuple的策略有很多种,后面会介绍。
如果你想第二个bolt读取spout和第一个bolt所发射的所有的tuple, 那么你应该这样定义第二个bolt:
1
2
3
|
builder.setBolt(
3
,
new
ExclamationBolt(),
5
)
.shuffleGrouping(
1
)
.shuffleGrouping(
2
);
|
让我们深入地看一下这个topology里面的spout和bolt是怎么实现的。Spout负责发射新的tuple到这个topology里面来。TestWordSpout从["nathan", "mike", "jackson", "golda", "bertels"]里面随机选择一个单词发射出来。TestWordSpout里面的nextTuple()方法是这样定义的:
1
2
3
4
5
6
7
8
|
public
void
nextTuple() {
Utils.sleep(
100
);
final
String[] words =
new
String[] {
"nathan"
,
"mike"
,
"jackson"
,
"golda"
,
"bertels"
};
final
Random rand =
new
Random();
final
String word = words[rand.nextInt(words.length)];
_collector.emit(
new
Values(word));
}
|
可以看到,实现很简单。
ExclamationBolt把”!!!”拼接到输入tuple后面。我们来看下ExclamationBolt的完整实现。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
public
static
class
ExclamationBolt
implements
IRichBolt {
_OutputCollector _collector;
public
void
prepare(Map conf, TopologyContext context,
OutputCollector collector) {
_collector = collector;
}
public
void
execute(Tuple tuple) {
_collector.emit(tuple,
new
Values(tuple.getString(
0
) +
"!!!"
));
_collector.ack(tuple);
}
public
void
cleanup() {
}
public
void
declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(
new
Fields(
"word"
));
}
}
|
prepare方法提供给bolt一个Outputcollector用来发射tuple。Bolt可以在任何时候发射tuple — 在prepare, execute或者cleanup方法里面, 或者甚至在另一个线程里面异步发射。这里prepare方法只是简单地把OutputCollector作为一个类字段保存下来给后面execute方法使用。
execute方法从bolt的一个输入接收tuple(一个bolt可能有多个输入源). ExclamationBolt获取tuple的第一个字段,加上”!!!”之后再发射出去。如果一个bolt有多个输入源,你可以通过调用Tuple#getSourceComponent方法来知道它是来自哪个输入源的。
execute方法里面还有其它一些事情值得一提: 输入tuple被作为emit方法的第一个参数,并且输入tuple在最后一行被ack。这些呢都是Storm可靠性API的一部分,后面会解释。
cleanup方法在bolt被关闭的时候调用, 它应该清理所有被打开的资源。但是集群不保证这个方法一定会被执行。比如执行task的机器down掉了,那么根本就没有办法来调用那个方法。cleanup设计的时候是被用来在local mode的时候才被调用(也就是说在一个进程里面模拟整个storm集群), 并且你想在关闭一些topology的时候避免资源泄漏。
最后,declareOutputFields定义一个叫做”word”的字段的tuple。
以local mode运行ExclamationTopology
让我们看看怎么以local mode运行ExclamationToplogy。
storm的运行有两种模式: 本地模式和分布式模式. 在本地模式中, storm用一个进程里面的线程来模拟所有的spout和bolt. 本地模式对开发和测试来说比较有用。 你运行storm-starter里面的topology的时候它们就是以本地模式运行的, 你可以看到topology里面的每一个组件在发射什么消息。
在分布式模式下, storm由一堆机器组成。当你提交topology给master的时候, 你同时也把topology的代码提交了。master负责分发你的代码并且负责给你的topolgoy分配工作进程。如果一个工作进程挂掉了, master节点会把认为重新分配到其它节点。关于如何在一个集群上面运行topology, 你可以看看Running topologies on a production cluster文章。
下面是以本地模式运行ExclamationTopology的代码:
1
2
3
4
5
6
7
8
9
|
Config conf =
new
Config();
conf.setDebug(
true
);
conf.setNumWorkers(
2
);
LocalCluster cluster =
new
LocalCluster();
cluster.submitTopology(
"test"
, conf, builder.createTopology());
Utils.sleep(
10000
);
cluster.killTopology(
"test"
);
cluster.shutdown();
|
首先, 这个代码定义通过定义一个LocalCluster对象来定义一个进程内的集群。提交topology给这个虚拟的集群和提交topology给分布式集群是一样的。通过调用submitTopology方法来提交topology, 它接受三个参数:要运行的topology的名字,一个配置对象以及要运行的topology本身。
topology的名字是用来唯一区别一个topology的,这样你然后可以用这个名字来杀死这个topology的。前面已经说过了, 你必须显式的杀掉一个topology, 否则它会一直运行。
Conf对象可以配置很多东西, 下面两个是最常见的:
感兴趣的话可以去看看Conf对象的Javadoc去看看topology的所有配置。
可以看看创建一个新storm项目去看看怎么配置开发环境以使你能够以本地模式运行topology.
流分组策略告诉topology如何在两个组件之间发送tuple。 要记住, spouts和bolts以很多task的形式在topology里面同步执行。如果从task的粒度来看一个运行的topology, 它应该是这样的:
当Bolt A的一个task要发送一个tuple给Bolt B, 它应该发送给Bolt B的哪个task呢?
stream grouping专门回答这种问题的。在我们深入研究不同的stream grouping之前, 让我们看一下storm-starter里面的另外一个topology。WordCountTopology读取一些句子, 输出句子里面每个单词出现的次数.
1
2
3
4
5
6
7
|
TopologyBuilder builder =
new
TopologyBuilder();
builder.setSpout(
1
,
new
RandomSentenceSpout(),
5
);
builder.setBolt(
2
,
new
SplitSentence(),
8
)
.shuffleGrouping(
1
);
builder.setBolt(
3
,
new
WordCount(),
12
)
.fieldsGrouping(
2
,
new
Fields(
"word"
));
|
SplitSentence对于句子里面的每个单词发射一个新的tuple, WordCount在内存里面维护一个单词->次数的mapping, WordCount每收到一个单词, 它就更新内存里面的统计状态。
有好几种不同的stream grouping:
fields grouping是stream合并,stream聚合以及很多其它场景的基础。在背后呢, fields grouping使用的一致性哈希来分配tuple的。
还有一些其它类型的stream grouping. 你可以在Concepts一章里更详细的了解。
Bolt可以使用任何语言来定义。用其它语言定义的bolt会被当作子进程(subprocess)来执行, storm使用JSON消息通过stdin/stdout来和这些subprocess通信。这个通信协议是一个只有100行的库, storm团队给这些库开发了对应的Ruby, Python和Fancy版本。
下面是WordCountTopology里面的SplitSentence的定义:
1
2
3
4
5
6
7
8
9
|
public
static
class
SplitSentence
extends
ShellBolt
implements
IRichBolt {
public
SplitSentence() {
super
(
"python"
,
"splitsentence.py"
);
}
public
void
declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(
new
Fields(
"word"
));
}
}
|
SplitSentence继承自ShellBolt并且声明这个Bolt用python来运行,并且参数是: splitsentence.py。下面是splitsentence.py的定义:
1
2
3
4
5
6
7
8
9
|
import
storm
class
SplitSentenceBolt(storm.BasicBolt):
def
process(
self
, tup):
words
=
tup.values[
0
].split(
" "
)
for
word
in
words:
storm.emit([word])
SplitSentenceBolt().run()
|
更多有关用其它语言定义Spout和Bolt的信息, 以及用其它语言来创建topology的 信息可以参见: Using non-JVM languages with Storm.
在这个教程的前面,我们跳过了有关tuple的一些特征。这些特征就是storm的可靠性API: storm如何保证spout发出的每一个tuple都被完整处理。看看《storm如何保证消息不丢失》以更深入了解storm的可靠性API.
这个入门教程比较广泛的介绍了从开发,测试和部署一个topology. 文档的其它部分会深入介绍使用storm的各个方面。