Storm快速入门
1 Storm - 简介
什么是Apache Storm?
Apache Storm是一个分布式实时大数据处理系统。Storm设计用于以容错和水平可伸缩方法处理大量数据。这是一个流媒体数据框架,具有最高的摄取率。虽然Storm是无状态的,但它通过Apache ZooKeeper管理分布式环境和集群状态。这很简单,您可以并行执行各种对实时数据的操作。
Apache Storm继续成为实时数据分析的领导者。Storm很容易设置,操作,并保证每个消息至少通过拓扑结构处理一次。
Apache Storm vs Hadoop
基本上Hadoop和Storm框架用于分析大数据。它们两者相辅相成,在某些方面有所不同。Apache Storm执行除持久性以外的所有操作,而Hadoop擅长于一切,但缺乏实时计算。下表比较了Storm和Hadoop的属性。
Storm | Hadoop |
---|---|
实时流处理 | 批量处理 |
无状态 | 有状态 |
基于ZooKeeper协调的主/从体系结构。主节点被称为nimbus,从属节点是监督者。 | 主/从架构有/无ZooKeeper协调。主节点是作业跟踪器,从节点是任务跟踪器。 |
Storm流式处理可以在群集上每秒访问数以万计的消息。 | Hadoop分布式文件系统(HDFS)使用MapReduce框架处理需要几分钟或几小时的大量数据。 |
Storm拓扑运行直到用户关闭或意外的不可恢复的故障。 | MapReduce作业按顺序执行并最终完成。 |
两者都是分布式和容错的 | |
如果nimbus / supervisor死亡,重新启动会使其从停止的地方继续,因此不会受到影响。 | 如果JobTracker死亡,所有正在运行的作业都将丢失。 |
Apache Storm的使用案例
Apache Storm对于实时大数据流处理非常有名。出于这个原因,大多数公司都将Storm作为其系统的一个组成部分。一些值得注意的例子如下 -
Twitter - Twitter正在将Apache Storm用于其“发布商分析产品”系列。“发布商分析产品”在Twitter平台中处理每个推文和点击。Apache Storm与Twitter基础架构深度整合。
NaviSite - NaviSite将Storm用于事件日志监视/审计系统。系统中生成的每个日志都将通过Storm。Storm将根据配置的正则表达式集检查消息,如果匹配,则该特定消息将被保存到数据库中。
Wego - Wego是位于新加坡的旅行元搜索引擎。旅游相关数据来自世界各地不同时间的许多来源。Storm帮助Wego搜索实时数据,解决并发问题并找到最终用户的最佳匹配。
Apache Storm的优点
这里列出了Apache Storm提供的好处 -
Storm是开源的,强大的和用户友好的。它可以用于小公司以及大公司。
Storm容错,灵活,可靠,并支持任何编程语言。
允许实时流处理。
Storm的速度令人难以置信,因为它具有处理数据的巨大能力。
通过线性增加资源,Storm即使在负载增加的情况下也能保持性能。它具有高度的可扩展性。
Storm在数秒或数分钟内执行数据刷新和端到端交付响应取决于问题。它具有非常低的延迟。
Storm拥有运营智能。
即使群集中的任何连接节点死亡或消息丢失,Storm仍可提供有保证的数据处理。
-
2 核心概念
Apache Storm从一端读取实时数据的原始流,并通过一系列小处理单元传递它,并在另一端输出处理/有用的信息。
下图描述了Apache Storm的核心概念。
现在让我们仔细看看Apache Storm的组件 -
组件 | 描述 |
---|---|
元组 | 元组是Storm中的主要数据结构。它是有序元素的列表。默认情况下,Tuple支持所有数据类型。通常,它被建模为一组逗号分隔值并传递给Storm集群。 |
流 | 流是一个无序的元组序列。 |
嘴 | 流的来源。一般来说,Storm接受来自原始数据源的输入数据,例如Twitter Streaming API,Apache Kafka队列,Kestrel队列等。否则,您可以写出喷嘴以从数据源读取数据。“ISpout”是实现喷嘴的核心接口,其中一些特定的接口是IRichSpout,BaseRichSpout,KafkaSpout等。 |
螺栓 | 螺栓是逻辑处理单元。Spouts将数据传递到螺栓和螺栓过程并生成新的输出流。螺栓可以执行过滤,聚合,加入,与数据源和数据库进行交互的操作。螺栓接收数据并发射到一个或多个螺栓。“IBolt”是实现螺栓的核心接口。一些常见的接口是IRichBolt,IBasicBolt等。 |
我们来看一个“Twitter分析”的实时示例,看看它如何在Apache Storm中建模。下图描述了结构。
“Twitter分析”的输入来自Twitter Streaming API。Spout将使用Twitter Streaming API读取用户的推文,并将其输出为元组流。来自喷口的单个元组将具有twitter用户名和单个推文作为逗号分隔值。然后,这组元组将被转发给Bolt,Bolt将把tweet分成单独的单词,计算单词数量,并将信息保存到配置的数据源中。现在,我们可以通过查询数据源轻松获得结果。
Topology
喷嘴和螺栓连接在一起,形成拓扑结构。实时应用程序逻辑在Storm拓扑结构中指定。简而言之,拓扑是一个有向图,其顶点是计算,边是数据流。
一个简单的拓扑开始于喷口。Spout将数据发送到一个或多个螺栓。螺栓表示拓扑中具有最小处理逻辑的节点,并且螺栓的输出可以作为输入发射到另一螺栓中。
Storm将始终运行拓扑,直到您终止拓扑。Apache Storm的主要工作是运行拓扑,并在给定的时间运行任意数量的拓扑。
Tasks
现在你对喷嘴和螺栓有了一个基本的想法。它们是拓扑中最小的逻辑单元,拓扑结构是使用单个喷口和一组螺栓构建的。它们应该以特定的顺序正确执行,以便拓扑成功运行。Storm提供的每个喷嘴和螺栓的执行称为“任务”。简而言之,任务是执行喷口或螺栓。在给定时间,每个喷嘴和螺栓可以有多个实例在多个单独的线程中运行。
Workers
拓扑在多个工作节点上以分布式方式运行。Storm将这些任务均匀分散到所有工作节点上。工作节点的角色是监听作业,并在新作业到达时启动或停止进程。
Stream Grouping
数据流从喷口流向螺栓或从一个螺栓流向另一个螺栓。流分组控制如何在拓扑中路由元组,并帮助我们理解拓扑中的元组流。如下所述,有四个内置分组。
Shuffle Grouping
在随机分组中,相同数量的元组随机分布在所有执行螺栓的工作人员中。下图描述了结构。
Field Grouping
元组中相同值的字段被组合在一起,剩下的元组保留在外面。然后,将具有相同字段值的元组向前发送给执行螺栓的同一工作人员。例如,如果流按字段“字”分组,则具有相同字符串“Hello”的元组将移动到同一个工作者。下图显示了Field Grouping的工作原理。
Global Grouping
所有的流可以分组并转发给一个螺栓。此分组将源的所有实例生成的元组发送到单个目标实例(具体来说,选择ID最低的工作者)。
All Grouping
所有分组将每个元组的单个副本发送到接收螺栓的所有实例。这种分组被用于向螺栓发送信号。所有分组对于连接操作都很有用。
3 集群架构
Apache Storm的主要亮点之一是它容错,快速,没有“单点故障”(SPOF)分布式应用程序。我们可以根据需要在尽可能多的系统中安装Apache Storm,以增加应用程序的容量。
我们来看看Apache Storm群集的设计方式和内部架构。下图描述了群集设计。
Apache Storm有两种类型的节点:Nimbus(主节点)和Supervisor(工作节点)。Nimbus是Apache Storm的核心组件。Nimbus的主要工作是运行Storm拓扑。Nimbus分析拓扑并收集要执行的任务。然后,它会将任务分配给可用的主管。
主管将有一个或多个工作进程。主管会将任务委派给工作进程。工作进程将根据需要产生尽可能多的执行程序并运行任务。Apache Storm使用内部分布式消息系统来实现nimbus和supervisor之间的通信。
组件 | 描述 |
---|---|
Nimbus | Nimbus是Storm集群的主节点。集群中的所有其他节点都称为工作节点。主节点负责在所有工作节点之间分配数据,将任务分配给工作节点并监视故障。 |
Supervisor | 遵循由nimbus给出的指令的节点被称为监督者。一个主管有多个工作进程和它管理工作进程,完成由灵气交给的任务。 |
Worker process | 工作进程将执行与特定拓扑相关的任务。工作进程本身不会运行任务,而是创建执行程序并要求它们执行特定的任务。一个工作进程将有多个执行者。 |
Executor | 执行者只不过是工作进程产生的单个线程。执行者运行一个或多个任务,但仅限于特定的喷嘴或螺栓。 |
Task | 任务执行实际的数据处理。所以,它不是喷嘴就是螺栓。 |
ZooKeeper framework |
Apache ZooKeeper是由群集(节点组)使用的服务,用于协调它们之间的关系,并使用强大的同步技术来维护共享数据。Nimbus是无状态的,所以它依赖于ZooKeeper来监视工作节点的状态。 ZooKeeper帮助主管与灵气交互。它负责维护灵气和监督者的状态。 |
Storm本质上是无状态的。尽管无状态自然有它自己的缺点,但它实际上有助于Storm以最佳和最快的方式处理实时数据。
尽管风暴并非完全无国籍。它将其状态存储在Apache ZooKeeper中。由于状态在Apache ZooKeeper中可用,因此可以重新启动失败的nimbus并使其从离开的地方开始工作。通常,像monit这样的服务监视工具将监视Nimbus并在出现故障时重新启动它。
Apache Storm还具有称为Trident Topology的高级拓扑,并具有状态维护功能,并且还提供了像Pig这样的高级API。我们将在接下来的章节中讨论所有这些功能。
4 工作流程
一个正在运行的Storm集群应该有一个灵活的和一个或多个主管。另一个重要的节点是Apache ZooKeeper,它将用于nimbus和主管之间的协调。
现在让我们仔细看看Apache Storm的工作流程 -
最初,灵气将等待“风暴拓扑”提交给它。
提交拓扑后,它将处理拓扑并收集要执行的所有任务以及执行任务的顺序。
然后,灵气将任务均匀分配给所有可用的主管。
在特定的时间间隔内,所有主管人员都会将心跳发送给灵气通知他们还活着。
当主管死亡并且不向心灵发送心跳时,则灵气分配任务给另一个主管。
当灵气本身死亡时,主管将在没有任何问题的情况下处理已分配的任务。
一旦所有任务完成,主管将等待新任务进入。
同时,服务监控工具将自动重启死光弹。
重新启动的灵气将从停止的地方继续。同样,死亡监督员也可以自动重新启动。由于nimbus和管理员都可以自动重新启动,两者都会像以前一样继续运行,所以Storm保证至少处理一次所有任务。
一旦处理完所有拓扑,光纤就等待新的拓扑到达,同样,管理员等待新的任务。
默认情况下,Storm群集中有两种模式 -
本地模式 - 此模式用于开发,测试和调试,因为它是查看所有拓扑组件一起工作的最简单方法。在这种模式下,我们可以调整参数,使我们能够看到我们的拓扑如何在不同的Storm配置环境中运行。在本地模式下,风暴拓扑在单个JVM中的本地计算机上运行。
生产模式 - 在这种模式下,我们将拓扑提交给工作Storm群集,该群集由多个进程组成,通常运行在不同的机器上。正如Storm工作流程中所讨论的,工作集群将无限期地运行,直到它关闭。
-
5 分布式消息系统
Apache Storm处理实时数据,输入通常来自消息排队系统。外部分布式消息系统将提供实时计算所需的输入。Spout将从消息系统读取数据并将其转换为元组并输入到Apache Storm中。有趣的事实是,Apache Storm在其内部使用其自己的分布式消息系统来进行其光纤和主管之间的通信。
什么是分布式消息系统?
分布式消息传递基于可靠消息队列的概念。消息在客户端应用程序和消息传递系统之间异步排队。分布式消息传递系统提供了可靠性,可伸缩性和持久性的好处。
大多数消息传递模式遵循发布 - 订阅模型(简称Pub-Sub),其中消息的发送者称为发布者,而想要接收消息的人称为订阅者。
一旦发送者发布了该消息,用户就可以借助过滤选项来接收选择的消息。通常我们有两种类型的过滤,一种是基于主题的过滤,另一种是基于内容的过滤。
请注意,pub-sub模型只能通过消息进行通信。它是一个非常松散耦合的架构; 即使发件人也不知道他们的订户是谁。许多消息模式使消息代理能够交换发布消息,以便许多订户及时访问。一个真实的例子是Dish TV,它发布体育,电影,音乐等不同的频道,任何人都可以订阅他们自己的频道,并在他们订阅的频道可用时获得。
下表介绍了一些流行的高吞吐量消息传送系统 -
分布式消息系统 描述 Apache Kafka Kafka在LinkedIn公司开发,后来成为Apache的一个子项目。Apache Kafka基于brokerenabled,持久的分布式发布 - 订阅模式。卡夫卡快速,可扩展且高效。 RabbitMQ RabbitMQ是一款开源的分布式健壮消息应用程序。它很容易使用并在所有平台上运行。 JMS(Java消息服务) JMS是一个开源的API,支持创建,阅读和发送消息从一个应用程序到另一个应用程序。它提供有保证的消息传递并遵循发布 - 订阅模型。 ActiveMQ ActiveMQ消息系统是JMS的开源API。 ZeroMQ ZeroMQ是无代理的点对点消息处理。它提供了推拉,路由器经销商消息模式。 Kestrel Kestrel是一个快速,可靠,简单的分布式消息队列。 Thrift协议
Thrift建立在Facebook上,用于跨语言服务开发和远程过程调用(RPC)。后来,它成为了一个开源的Apache项目。Apache Thrift是一种接口定义语言,允许以简单的方式在定义的数据类型之上定义新的数据类型和服务实现。
Apache Thrift也是一个支持嵌入式系统,移动应用程序,Web应用程序和许多其他编程语言的通信框架。与Apache Thrift相关的一些关键特性是其模块化,灵活性和高性能。另外,它可以在分布式应用程序中执行流式传输,消息传递和RPC。
Storm广泛使用Thrift协议来进行内部通信和数据定义。风暴拓扑结构简直就是节俭结构。在Apache Storm中运行拓扑的Storm Nimbus是Thrift服务。
6 Storm - 安装
现在让我们看看如何在您的机器上安装Apache Storm框架。这里有三个majo步骤 -
- 如果您尚未安装Java,请在系统上安装Java。
- 安装ZooKeeper框架。
- 安装Apache Storm框架。
第1步 - 验证Java安装
第2步 - ZooKeeper框架安装
第3步 - Apache Storm框架安装
步骤3.1下载Storm
要在您的机器上安装Storm框架,请访问以下链接并下载最新版本的Storm http://storm.apache.org/downloads.html
截至目前,Storm的最新版本是“apache-storm-0.9.5.tar.gz”。
第3.2步 - 解压缩tar文件
使用以下命令提取tar文件 -
$ cd opt/ $ tar -zxf apache-storm-0.9.5.tar.gz $ cd apache-storm-0.9.5 $ mkdir data
步骤3.3 - 打开配置文件
Storm的当前版本包含一个配置Storm守护进程的文件“conf / storm.yaml”。将以下信息添加到该文件。
$ vi conf/storm.yaml storm.zookeeper.servers: - "localhost" storm.local.dir: “/path/to/storm/data(any path)” nimbus.host: "localhost" supervisor.slots.ports: - 6700 - 6701 - 6702 - 6703
应用完所有更改后,保存并返回到终端。
步骤3.4 - 启动Nimbus
$ bin/storm nimbus
第3.5步 - 启动主管
$ bin/storm supervisor
步骤3.6启动UI
$ bin/storm ui
启动Storm用户界面应用程序后,在您喜欢的浏览器中键入URL http:// localhost:8080,您可以看到Storm群集信息及其运行拓扑。该页面应与以下屏幕截图类似。
7 工作示例
我们已经了解了Apache Storm的核心技术细节,现在是编写一些简单场景的时候了。
Scenario - Mobile Call日志分析器
移动电话及其持续时间将作为Apache Storm的输入提供,Storm将处理并分组相同呼叫者和接收者之间的呼叫及其呼叫总数。
Spout Creation
Spout是用于数据生成的组件。基本上,喷嘴将实现一个IRichSpout接口。“IRichSpout”界面有以下重要方法 -
open - 为喷口提供执行环境。执行者将运行此方法来初始化喷口。
nextTuple - 通过收集器发出生成的数据。
close - 喷嘴将要关闭时调用此方法。
declareOutputFields - 声明元组的输出模式。
ack - 确认处理了特定的元组
fail - 指定一个特定的元组不被处理并且不被重新处理。
open
open方法的签名如下 -
open(Map conf, TopologyContext context, SpoutOutputCollector collector)
conf - 为此喷口提供风暴配置。
context - 提供关于拓扑中喷口位置,其任务ID,输入和输出信息的完整信息。
collector - 使我们能够发出将由螺栓处理的元组。
nextTuple
nextTuple方法的签名如下 -
nextTuple()
nextTuple()从与ack()和fail()方法相同的循环周期性地调用。当没有工作要做时,它必须释放对线程的控制,以便其他方法有机会被调用。所以nextTuple的第一行检查处理是否完成。如果是这样,它应该睡眠至少一毫秒,以在返回之前减少处理器上的负载。
close
close方法的签名如下 -
close()
declareOutputFields
declareOutputFields方法的签名如下所示 -
declareOutputFields(OutputFieldsDeclarer declarer)
declarer - 它用于声明输出流ID,输出字段等。
此方法用于指定元组的输出模式。
ACK
ack方法的签名如下 -
ack(Object msgId)
该方法确认已经处理了特定的元组。
fail
nextTuple方法的签名如下 -
ack(Object msgId)
此方法通知某个特定的元组尚未完全处理。Storm将重新处理特定的元组。
FakeCallLogReaderSpout
在我们的场景中,我们需要收集通话记录详细信息。通话记录的信息包含。
- 来电号码
- 接收器号码
- 持续时间
由于我们没有实时的通话记录信息,我们会生成虚假的通话记录。假信息将使用Random类创建。完整的程序代码如下。
Coding - FakeCallLogReaderSpout.java
import java.util.*; //import storm tuple packages import backtype.storm.tuple.Fields; import backtype.storm.tuple.Values; //import Spout interface packages import backtype.storm.topology.IRichSpout; import backtype.storm.topology.OutputFieldsDeclarer; import backtype.storm.spout.SpoutOutputCollector; import backtype.storm.task.TopologyContext; //Create a class FakeLogReaderSpout which implement IRichSpout interface to access functionalities public class FakeCallLogReaderSpout implements IRichSpout { //Create instance for SpoutOutputCollector which passes tuples to bolt. private SpoutOutputCollector collector; private boolean completed = false; //Create instance for TopologyContext which contains topology data. private TopologyContext context; //Create instance for Random class. private Random randomGenerator = new Random(); private Integer idx = 0; @Override public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) { this.context = context; this.collector = collector; } @Override public void nextTuple() { if(this.idx <= 1000) { List<String> mobileNumbers = new ArrayList<String>(); mobileNumbers.add("1234123401"); mobileNumbers.add("1234123402"); mobileNumbers.add("1234123403"); mobileNumbers.add("1234123404"); Integer localIdx = 0; while(localIdx++ < 100 && this.idx++ < 1000) { String fromMobileNumber = mobileNumbers.get(randomGenerator.nextInt(4)); String toMobileNumber = mobileNumbers.get(randomGenerator.nextInt(4)); while(fromMobileNumber == toMobileNumber) { toMobileNumber = mobileNumbers.get(randomGenerator.nextInt(4)); } Integer duration = randomGenerator.nextInt(60); this.collector.emit(new Values(fromMobileNumber, toMobileNumber, duration)); } } } @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("from", "to", "duration")); } //Override all the interface methods @Override public void close() {} public boolean isDistributed() { return false; } @Override public void activate() {} @Override public void deactivate() {} @Override public void ack(Object msgId) {} @Override public void fail(Object msgId) {} @Override public Map<String, Object> getComponentConfiguration() { return null; } }
Bolt Creation
Bolt是一个将元组作为输入,处理元组并生成新的元组作为输出的组件。Bolts将实施IRichBolt界面。在这个程序中,使用两个螺栓类CallLogCreatorBolt和CallLogCounterBolt来执行操作。
IRichBolt接口有以下方法 -
prepare - 为螺栓提供执行的环境。执行者将运行此方法来初始化喷口。
execute - 处理输入的单个元组。
cleanup - 当螺栓即将关闭时调用。
declareOutputFields - 声明元组的输出模式。
Prepare
prepare方法的签名如下 -
prepare(Map conf, TopologyContext context, OutputCollector collector)
conf - 为此螺栓提供风暴配置。
context - 提供有关拓扑中螺栓位置,其任务ID,输入和输出信息等的完整信息。
collector - 使我们能够发出处理过的元组。
execute
execute方法的签名如下 -
execute(Tuple tuple)
这里的tuple是要处理的输入元组。
所述execute方法一次处理单元组。元组数据可以通过Tuple类的getValue方法访问。没有必要立即处理输入元组。多元组可以作为单个输出元组进行处理和输出。处理过的元组可以通过使用OutputCollector类发出。
cleanup
cleanup方法的签名如下 -
cleanup()
declareOutputFields
declareOutputFields方法的签名如下所示 -
declareOutputFields(OutputFieldsDeclarer declarer)
这里参数声明器用于声明输出流ID,输出字段等。
此方法用于指定元组的输出模式
Call log Creator Bolt
通话记录创建器螺栓接收通话记录元组。通话记录元组具有主叫号码,接收者号码和通话时长。通过组合主叫方号码和接收方号码,此螺栓简单地创建一个新值。新值的格式为“来电号码 - 接收方号码”,并将其命名为新字段“call”。完整的代码如下。
Coding - CallLogCreatorBolt.java
//import util packages import java.util.HashMap; import java.util.Map; import backtype.storm.tuple.Fields; import backtype.storm.tuple.Values; import backtype.storm.task.OutputCollector; import backtype.storm.task.TopologyContext; //import Storm IRichBolt package import backtype.storm.topology.IRichBolt; import backtype.storm.topology.OutputFieldsDeclarer; import backtype.storm.tuple.Tuple; //Create a class CallLogCreatorBolt which implement IRichBolt interface public class CallLogCreatorBolt implements IRichBolt { //Create instance for OutputCollector which collects and emits tuples to produce output private OutputCollector collector; @Override public void prepare(Map conf, TopologyContext context, OutputCollector collector) { this.collector = collector; } @Override public void execute(Tuple tuple) { String from = tuple.getString(0); String to = tuple.getString(1); Integer duration = tuple.getInteger(2); collector.emit(new Values(from + " - " + to, duration)); } @Override public void cleanup() {} @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("call", "duration")); } @Override public Map<String, Object> getComponentConfiguration() { return null; } }
Call log Counter Bolt
呼叫记录计数器螺栓接收呼叫及其持续时间作为元组。这个螺栓在prepare方法中初始化一个字典(Map)对象。在execute方法中,它检查元组并在元组中为每个新的“调用”值在字典对象中创建一个新条目,并在字典对象中设置值1。对于字典中已有的条目,它只是递增其值。简单地说,这个螺栓将调用和它的计数保存在字典对象中。我们可以将它保存到数据源中,而不是将调用和它的计数保存在字典中。完整的程序代码如下所示 -
Coding - CallLogCounterBolt.java
import java.util.HashMap; import java.util.Map; import backtype.storm.tuple.Fields; import backtype.storm.tuple.Values; import backtype.storm.task.OutputCollector; import backtype.storm.task.TopologyContext; import backtype.storm.topology.IRichBolt; import backtype.storm.topology.OutputFieldsDeclarer; import backtype.storm.tuple.Tuple; public class CallLogCounterBolt implements IRichBolt { Map<String, Integer> counterMap; private OutputCollector collector; @Override public void prepare(Map conf, TopologyContext context, OutputCollector collector) { this.counterMap = new HashMap<String, Integer>(); this.collector = collector; } @Override public void execute(Tuple tuple) { String call = tuple.getString(0); Integer duration = tuple.getInteger(1); if(!counterMap.containsKey(call)){ counterMap.put(call, 1); }else{ Integer c = counterMap.get(call) + 1; counterMap.put(call, c); } collector.ack(tuple); } @Override public void cleanup() { for(Map.Entry<String, Integer> entry:counterMap.entrySet()){ System.out.println(entry.getKey()+" : " + entry.getValue()); } } @Override public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("call")); } @Override public Map<String, Object> getComponentConfiguration() { return null; } }
Creating Topology
Storm拓扑基本上是一个Thrift结构。TopologyBuilder类提供了简单而简单的方法来创建复杂的拓扑。TopologyBuilder类具有设置喷口(setSpout)和设置螺栓(setBolt)的方法。最后,TopologyBuilder创建拓扑来创建拓扑。使用下面的代码片段来创建一个拓扑 -
TopologyBuilder builder = new TopologyBuilder(); builder.setSpout("call-log-reader-spout", new FakeCallLogReaderSpout()); builder.setBolt("call-log-creator-bolt", new CallLogCreatorBolt()) .shuffleGrouping("call-log-reader-spout"); builder.setBolt("call-log-counter-bolt", new CallLogCounterBolt()) .fieldsGrouping("call-log-creator-bolt", new Fields("call"));
shuffleGrouping和fieldsGrouping方法有助于设置喷嘴和螺栓的流分组。
Local Cluster
出于开发目的,我们可以使用“LocalCluster”对象创建本地集群,然后使用“LocalCluster”类的“submitTopology”方法提交拓扑。“submitTopology”的一个参数是“Config”类的一个实例。在提交拓扑之前,“Config”类用于设置配置选项。此配置选项将在运行时与集群配置合并,并通过prepare方法发送到所有任务(spout和bolt)。将拓扑提交到群集后,我们将等待10秒钟,以便群集计算提交的拓扑,然后使用“LocalCluster”的“关闭”方法关闭群集。完整的程序代码如下所示 -
Coding - LogAnalyserStorm.java
import backtype.storm.tuple.Fields; import backtype.storm.tuple.Values; //import storm configuration packages import backtype.storm.Config; import backtype.storm.LocalCluster; import backtype.storm.topology.TopologyBuilder; //Create main class LogAnalyserStorm submit topology. public class LogAnalyserStorm { public static void main(String[] args) throws Exception{ //Create Config instance for cluster configuration Config config = new Config(); config.setDebug(true); // TopologyBuilder builder = new TopologyBuilder(); builder.setSpout("call-log-reader-spout", new FakeCallLogReaderSpout()); builder.setBolt("call-log-creator-bolt", new CallLogCreatorBolt()) .shuffleGrouping("call-log-reader-spout"); builder.setBolt("call-log-counter-bolt", new CallLogCounterBolt()) .fieldsGrouping("call-log-creator-bolt", new Fields("call")); LocalCluster cluster = new LocalCluster(); cluster.submitTopology("LogAnalyserStorm", config, builder.createTopology()); Thread.sleep(10000); //Stop the topology cluster.shutdown(); } }
构建和运行应用程序
完整的应用程序有四个Java代码。他们是 -
- FakeCallLogReaderSpout.java
- CallLogCreaterBolt.java
- CallLogCounterBolt.java
- LogAnalyerStorm.java
应用程序可以使用以下命令构建 -
javac -cp “/path/to/storm/apache-storm-0.9.5/lib/*” *.java
应用程序可以使用以下命令运行 -
java -cp “/path/to/storm/apache-storm-0.9.5/lib/*”:. LogAnalyserStorm
输出
一旦应用程序启动,它将输出有关集群启动过程,喷出和螺栓处理的完整详细信息,最后还会输出集群关闭过程。在“CallLogCounterBolt”中,我们打印了通话及其计数详细信息。这些信息将如下显示在控制台上 -
1234123402 - 1234123401 : 78 1234123402 - 1234123404 : 88 1234123402 - 1234123403 : 105 1234123401 - 1234123404 : 74 1234123401 - 1234123403 : 81 1234123401 - 1234123402 : 81 1234123403 - 1234123404 : 86 1234123404 - 1234123401 : 63 1234123404 - 1234123402 : 82 1234123403 - 1234123402 : 83 1234123404 - 1234123403 : 86 1234123403 - 1234123401 : 93
非JVM语言
Storm风格的拓扑结构通过Thrift接口实现,这使得用任何语言提交拓扑变得非常容易。Storm支持Ruby,Python和许多其他语言。我们来看看python绑定。
Python Binding
Python是一种通用的解释型,交互式,面向对象和高级编程语言。Storm支持Python来实现其拓扑。Python支持发射,锚定,确认和记录操作。
如你所知,螺栓可以用任何语言来定义。以另一种语言编写的螺栓作为子流程执行,Storm通过标准输入/标准输出使用JSON消息与这些子流程进行通信。首先拿一个支持python绑定的示例螺栓WordCount。
public static class WordCount implements IRichBolt { public WordSplit() { super("python", "splitword.py"); } public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("word")); } }
这里的WordCount类实现了IRichBolt接口,并使用python实现指定的超级方法参数“splitword.py”运行。现在创建一个名为“splitword.py”的python实现。
import storm class WordCountBolt(storm.BasicBolt): def process(self, tup): words = tup.values[0].split(" ") for word in words: storm.emit([word]) WordCountBolt().run()
这是Python的示例实现,用于计算给定句子中的单词。同样,您也可以使用其他支持语言进行绑定。
8 Trident
Trident是Storm的延伸。像Storm一样,Trident也是由Twitter开发的。开发Trident的主要原因是在Storm之上提供高级抽象以及有状态流处理和低延迟分布式查询。
Trident使用喷嘴和螺栓,但这些底层组件在执行前由Trident自动生成。Trident具有功能,过滤器,连接,分组和聚合。
Trident处理流作为一系列被称为交易的批次。通常,这些小批量的大小将取决于数千或数百万个元组,取决于输入流。这样,Trident不同于Storm,它执行元组处理。
批处理概念与数据库事务非常相似。每笔交易都分配一个交易ID。一旦所有处理完成,交易即被视为成功。但是,处理一个事务元组失败将导致整个事务被重新传输。对于每个批次,Trident将在交易开始时调用beginCommit,并在结束时进行提交。
Trident Topology
Trident API公开了使用“TridentTopology”类创建Trident拓扑的简单选项。基本上,Trident拓扑接收来自喷口的输入流,并在该流上执行有序的操作序列(过滤,聚合,分组等)。风暴元组被三叉戟元组取代,螺栓被操作取代。一个简单的三叉戟拓扑可以创建如下 -
TridentTopology topology = new TridentTopology();
Trident Tuples
三叉戟元组是一个已命名的值列表。TridentTuple接口是Trident拓扑的数据模型。TridentTuple接口是可以由Trident拓扑处理的基本数据单元。
Trident Spout
三叉戟喷口与Storm喷口相似,具有使用Trident功能的附加选项。实际上,我们仍然可以使用我们在Storm拓扑中使用的IRichSpout,但它本质上不具有事务性,我们将无法使用Trident提供的优势。
具有使用Trident功能的所有功能的基本喷嘴是“ITridentSpout”。它支持事务性和不透明事务语义。其他喷嘴是IBatchSpout,IPartitionedTridentSpout和IOpaquePartitionedTridentSpout。
除了这些通用喷嘴之外,Trident还有很多三叉戟喷嘴的实例。其中一个是FeederBatchSpout喷口,我们可以使用它轻松发送三叉戟元组的命名列表,而无需担心批处理,并行性等问题。
FeederBatchSpout的创建和数据馈送可以按照如下所示完成 -
TridentTopology topology = new TridentTopology(); FeederBatchSpout testSpout = new FeederBatchSpout( ImmutableList.of("fromMobileNumber", "toMobileNumber", “duration”)); topology.newStream("fixed-batch-spout", testSpout) testSpout.feed(ImmutableList.of(new Values("1234123401", "1234123402", 20)));
Trident Operations
Trident依靠“三叉戟操作”来处理三叉戟元组的输入流。Trident API具有许多内置操作来处理从简单到复杂的流处理。这些操作从简单验证到复杂的三叉戟元组分组和聚合。让我们来看看最重要和最常用的操作。
Filter
过滤器是用于执行输入验证任务的对象。Trident过滤器获取三叉戟元组字段的子集作为输入,并根据某些条件是否满足返回true或false。如果返回true,则元组保存在输出流中; 否则,该元组将从流中移除。过滤器将基本上继承自BaseFilter类并实现isKeep方法。以下是过滤器操作的示例实现 -
public class MyFilter extends BaseFilter { public boolean isKeep(TridentTuple tuple) { return tuple.getInteger(1) % 2 == 0; } } input [1, 2] [1, 3] [1, 4] output [1, 2] [1, 4]
可以使用“每个”方法在拓扑中调用过滤器函数。“Fields”类可用于指定输入(三叉戟元组的子集)。示例代码如下 -
TridentTopology topology = new TridentTopology(); topology.newStream("spout", spout) .each(new Fields("a", "b"), new MyFilter())
Function
Function是用于在单个三叉戟元组上执行简单操作的对象。它需要三叉戟元组字段的子集并发出零个或更多新的三叉戟元组字段。
Function基本上从BaseFunction类继承并实现了execute方法。下面给出了一个示例实现 -
public class MyFunction extends BaseFunction { public void execute(TridentTuple tuple, TridentCollector collector) { int a = tuple.getInteger(0); int b = tuple.getInteger(1); collector.emit(new Values(a + b)); } } input [1, 2] [1, 3] [1, 4] output [1, 2, 3] [1, 3, 4] [1, 4, 5]
就像过滤器操作一样,可以使用每种方法在拓扑中调用函数操作。示例代码如下 -
TridentTopology topology = new TridentTopology(); topology.newStream("spout", spout) .each(new Fields(“a, b"), new MyFunction(), new Fields(“d")));
Aggregation
聚集是用于对输入批处理或分区或流执行聚合操作的对象。Trident有三种类型的聚合。他们如下 -
aggregate - 孤立地聚集每批三叉戟元组。在聚合过程中,元组最初使用全局分组重新分区,以将同一批次的所有分区合并到一个分区中。
partitionAggregate - 聚合每个分区,而不是整个批次的三叉戟元组。分区聚合的输出完全替换了输入元组。分区聚合的输出包含单个字段元组。
persistentaggregate - 在所有批次的所有三叉戟元组上聚合并将结果存储在内存或数据库中。
TridentTopology topology = new TridentTopology(); // aggregate operation topology.newStream("spout", spout) .each(new Fields(“a, b"), new MyFunction(), new Fields(“d”)) .aggregate(new Count(), new Fields(“count”)) // partitionAggregate operation topology.newStream("spout", spout) .each(new Fields(“a, b"), new MyFunction(), new Fields(“d”)) .partitionAggregate(new Count(), new Fields(“count")) // persistentAggregate - saving the count to memory topology.newStream("spout", spout) .each(new Fields(“a, b"), new MyFunction(), new Fields(“d”)) .persistentAggregate(new MemoryMapState.Factory(), new Count(), new Fields("count"));
可以使用CombinerAggregator,ReducerAggregator或通用Aggregator接口创建聚合操作。上面例子中使用的“count”聚合器是内置聚合器之一,它使用“CombinerAggregator”实现,具体实现如下 -
public class Count implements CombinerAggregator<Long> { @Override public Long init(TridentTuple tuple) { return 1L; } @Override public Long combine(Long val1, Long val2) { return val1 + val2; } @Override public Long zero() { return 0L; } }
Grouping
分组操作是一种内置操作,可以通过groupBy方法调用。groupBy方法通过在指定的字段上执行partitionBy来重新分区流,然后在每个分区内将它的组字段相等的元组分组在一起。通常,我们使用“groupBy”和“persistentAggregate”来获得分组聚合。示例代码如下 -
TridentTopology topology = new TridentTopology(); // persistentAggregate - saving the count to memory topology.newStream("spout", spout) .each(new Fields(“a, b"), new MyFunction(), new Fields(“d”)) .groupBy(new Fields(“d”) .persistentAggregate(new MemoryMapState.Factory(), new Count(), new Fields("count"));
Merging和Joining
合并和连接可以分别使用“合并”和“连接”方法完成。合并合并一个或多个流。连接类似于合并,除了连接使用来自双方的三叉形元组字段来检查和连接两个流。而且,加入只能在批次级别下工作。示例代码如下 -
TridentTopology topology = new TridentTopology(); topology.merge(stream1, stream2, stream3); topology.join(stream1, new Fields("key"), stream2, new Fields("x"), new Fields("key", "a", "b", "c"));
State Maintenance
Trident提供了状态维护机制。状态信息可以存储在拓扑本身中,否则可以将其存储在单独的数据库中。原因是维护一个状态,即如果任何元组在处理期间失败,则重试失败的元组。这在更新状态时会产生问题,因为您不确定此元组的状态是否已更新过。如果元组在更新状态之前失败了,那么重试元组将使状态稳定。但是,如果元组在更新状态后失败,则重试同一元组将再次增加数据库中的计数并使状态不稳定。需要执行以下步骤来确保消息只处理一次 -
小批量处理元组。
为每个批次分配一个唯一的ID。如果批次重试,则会给出相同的唯一ID。
状态更新在批次中排序。例如,第二批次的状态更新将不可能,直到第一批次的状态更新完成。
分布式RPC
分布式RPC用于查询和检索Trident拓扑的结果。Storm有一个内置的分布式RPC服务器。分布式RPC服务器接收来自客户端的RPC请求并将其传递给拓扑。拓扑处理请求并将结果发送到分布式RPC服务器,该服务器由分布式RPC服务器重定向到客户端。Trident的分布式RPC查询像普通的RPC查询一样执行,除了这些查询是并行运行的。
何时使用Trident?
和许多用例一样,如果需求只处理查询一次,我们可以通过在Trident中编写拓扑来实现。另一方面,在风暴的情况下,很难实现一次处理。因此Trident对那些需要精确处理一次的用例非常有用。Trident并非针对所有用例,特别是高性能用例,因为它增加了Storm的复杂性并管理了状态。
Trident的工作例子
我们将把我们在前一节中制定的呼叫日志分析器应用程序转换为Trident框架。由于其高级API,Trident应用相对简单风暴相对容易。Storm基本上需要执行Trident中的Function,Filter,Aggregate,GroupBy,Join和Merge操作中的任何一个。最后,我们将使用LocalDRPC类启动DRPC服务器,并使用LocalDRPC类的执行方法搜索一些关键字。
格式化通话信息
FormatCall类的用途是格式化包含“呼叫者号码”和“接收者号码”的呼叫信息。完整的程序代码如下所示 -
Coding:FormatCall.java
import backtype.storm.tuple.Values; import storm.trident.operation.BaseFunction; import storm.trident.operation.TridentCollector; import storm.trident.tuple.TridentTuple; public class FormatCall extends BaseFunction { @Override public void execute(TridentTuple tuple, TridentCollector collector) { String fromMobileNumber = tuple.getString(0); String toMobileNumber = tuple.getString(1); collector.emit(new Values(fromMobileNumber + " - " + toMobileNumber)); } }
CSVSplit
CSVSplit类的用途是根据“逗号(,)”分割输入字符串并发送字符串中的每个单词。该函数用于解析分布式查询的输入参数。完整的代码如下 -
Coding:CSVSplit.java
import backtype.storm.tuple.Values; import storm.trident.operation.BaseFunction; import storm.trident.operation.TridentCollector; import storm.trident.tuple.TridentTuple; public class CSVSplit extends BaseFunction { @Override public void execute(TridentTuple tuple, TridentCollector collector) { for(String word: tuple.getString(0).split(",")) { if(word.length() > 0) { collector.emit(new Values(word)); } } } }
Log Analyzer日志分析器
这是主要的应用程序。最初,应用程序将使用FeederBatchSpout初始化TridentTopology并提供来电者信息。Trident拓扑流可以使用TridentTopology类的newStream方法创建。同样,可以使用TridentTopology类的newDRCPStream方法创建Trident拓扑DRPC流。一个简单的DRCP服务器可以使用LocalDRPC类创建。LocalDRPC具有执行搜索某个关键字的方法。完整的代码如下。
Coding:LogAnalyserTrident.java
import java.util.*; import backtype.storm.Config; import backtype.storm.LocalCluster; import backtype.storm.LocalDRPC; import backtype.storm.utils.DRPCClient; import backtype.storm.tuple.Fields; import backtype.storm.tuple.Values; import storm.trident.TridentState; import storm.trident.TridentTopology; import storm.trident.tuple.TridentTuple; import storm.trident.operation.builtin.FilterNull; import storm.trident.operation.builtin.Count; import storm.trident.operation.builtin.Sum; import storm.trident.operation.builtin.MapGet; import storm.trident.operation.builtin.Debug; import storm.trident.operation.BaseFilter; import storm.trident.testing.FixedBatchSpout; import storm.trident.testing.FeederBatchSpout; import storm.trident.testing.Split; import storm.trident.testing.MemoryMapState; import com.google.common.collect.ImmutableList; public class LogAnalyserTrident { public static void main(String[] args) throws Exception { System.out.println("Log Analyser Trident"); TridentTopology topology = new TridentTopology(); FeederBatchSpout testSpout = new FeederBatchSpout(ImmutableList.of("fromMobileNumber", "toMobileNumber", "duration")); TridentState callCounts = topology .newStream("fixed-batch-spout", testSpout) .each(new Fields("fromMobileNumber", "toMobileNumber"), new FormatCall(), new Fields("call")) .groupBy(new Fields("call")) .persistentAggregate(new MemoryMapState.Factory(), new Count(), new Fields("count")); LocalDRPC drpc = new LocalDRPC(); topology.newDRPCStream("call_count", drpc) .stateQuery(callCounts, new Fields("args"), new MapGet(), new Fields("count")); topology.newDRPCStream("multiple_call_count", drpc) .each(new Fields("args"), new CSVSplit(), new Fields("call")) .groupBy(new Fields("call")) .stateQuery(callCounts, new Fields("call"), new MapGet(), new Fields("count")) .each(new Fields("call", "count"), new Debug()) .each(new Fields("count"), new FilterNull()) .aggregate(new Fields("count"), new Sum(), new Fields("sum")); Config conf = new Config(); LocalCluster cluster = new LocalCluster(); cluster.submitTopology("trident", conf, topology.build()); Random randomGenerator = new Random(); int idx = 0; while(idx < 10) { testSpout.feed(ImmutableList.of(new Values("1234123401", "1234123402", randomGenerator.nextInt(60)))); testSpout.feed(ImmutableList.of(new Values("1234123401", "1234123403", randomGenerator.nextInt(60)))); testSpout.feed(ImmutableList.of(new Values("1234123401", "1234123404", randomGenerator.nextInt(60)))); testSpout.feed(ImmutableList.of(new Values("1234123402", "1234123403", randomGenerator.nextInt(60)))); idx = idx + 1; } System.out.println("DRPC : Query starts"); System.out.println(drpc.execute("call_count","1234123401 - 1234123402")); System.out.println(drpc.execute("multiple_call_count", "1234123401 - 1234123402,1234123401 - 1234123403")); System.out.println("DRPC : Query ends"); cluster.shutdown(); drpc.shutdown(); // DRPCClient client = new DRPCClient("drpc.server.location", 3772); } }
构建和运行应用程序
完整的应用程序有三个Java代码。他们如下 -
- FormatCall.java
- CSVSplit.java
- LogAnalyerTrident.java
应用程序可以通过使用以下命令来构建 -
javac -cp “/path/to/storm/apache-storm-0.9.5/lib/*” *.java
该应用程序可以通过使用以下命令运行 -
java -cp “/path/to/storm/apache-storm-0.9.5/lib/*”:. LogAnalyserTrident
输出
应用程序启动后,应用程序将输出关于集群启动过程,操作处理,DRPC服务器和客户端信息以及集群关闭过程的完整详细信息。该输出将显示在控制台上,如下所示。
DRPC : Query starts [["1234123401 - 1234123402",10]] DEBUG: [1234123401 - 1234123402, 10] DEBUG: [1234123401 - 1234123403, 10] [[20]] DRPC : Query ends
9 应用程序
Apache Storm框架支持当今最好的工业应用程序。我们将在本章中简要介绍一些Storm最显着的应用。
Klout
Klout是一款应用程序,它使用社交媒体分析通过Klout Score(基于在线社交影响力对用户进行排名),Klout Score是1到100之间的数值.Klout使用Apache Storm内置的Trident抽象来创建流式数据的复杂拓扑。
The Weather Channel
Weather Channel使用Storm拓扑结构来摄取天气数据。它已经与Twitter捆绑在一起,在Twitter和移动应用上实现天气预报广告。OpenSignal是一家专门从事无线覆盖测绘的公司。StormTag和WeatherSignal是由OpenSignal创建的基于天气的项目。StormTag是一个连接到钥匙链的蓝牙气象站。设备收集的天气数据将发送到WeatherSignal应用程序和OpenSignal服务器。
Telecom Industry电信业
电信运营商每秒处理数百万个电话。他们对掉话和糟糕的音质进行取证。呼叫细节记录以每秒数百万的速度流入,Apache Storm实时处理这些记录,并识别出任何令人不安的模式。风暴分析可用于持续改善通话质量。