精通-Storm-全-

精通 Storm(全)

原文:zh.annas-archive.org/md5/5A2D98C1AAE9E2E2F9D015883F441239

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

实时数据处理不再是少数大公司的奢侈品,而已经成为希望竞争的企业的必需品,而 Apache Storm 是开发实时处理管道的事实标准之一。Storm 的关键特性是它具有水平可扩展性,容错性,并提供了保证的消息处理。Storm 可以解决各种类型的分析问题:机器学习、日志处理、图分析等。

精通 Storm 将作为一本入门指南,面向经验不足的开发人员,也是有经验的开发人员实施高级用例的参考。在前两章中,您将学习 Storm 拓扑的基础知识和 Storm 集群的各种组件。在后面的章节中,您将学习如何构建一个可以与各种其他大数据技术进行交互的 Storm 应用程序,以及如何创建事务性拓扑。最后,最后两章涵盖了日志处理和机器学习的案例研究。我们还将介绍如何使用 Storm 调度程序将精细的工作分配给精细的机器。

本书涵盖内容

第一章,实时处理和 Storm 介绍,介绍了 Storm 及其组件。

第二章,Storm 部署、拓扑开发和拓扑选项,涵盖了将 Storm 部署到集群中,在 Storm 集群上部署示例拓扑,以及如何使用 Storm UI 监视 storm 管道以及如何动态更改日志级别设置。

第三章,Storm 并行性和数据分区,涵盖了拓扑的并行性,如何在代码级别配置并行性,保证消息处理以及 Storm 内部生成的元组。

第四章,Trident 介绍,介绍了 Trident 的概述,对 Trident 数据模型的理解,以及如何编写 Trident 过滤器和函数。本章还涵盖了 Trident 元组上的重新分区和聚合操作。

第五章,Trident 拓扑和用途,介绍了 Trident 元组分组、非事务性拓扑和一个示例 Trident 拓扑。该章还介绍了 Trident 状态和分布式 RPC。

第六章,Storm 调度程序,介绍了 Storm 中可用的不同类型的调度程序:默认调度程序、隔离调度程序、资源感知调度程序和自定义调度程序。

第七章,Storm 集群的监控,涵盖了通过编写使用 Nimbus 发布的统计信息的自定义监控 UI 来监控 Storm。我们解释了如何使用 JMXTrans 将 Ganglia 与 Storm 集成。本章还介绍了如何配置 Storm 以发布 JMX 指标。

第八章,Storm 和 Kafka 的集成,展示了 Storm 与 Kafka 的集成。本章从 Kafka 的介绍开始,涵盖了 Storm 的安装,并以 Storm 与 Kafka 的集成来解决任何实际问题。

第九章,Storm 和 Hadoop 集成,概述了 Hadoop,编写 Storm 拓扑以将数据发布到 HDFS,Storm-YARN 的概述,以及在 YARN 上部署 Storm 拓扑。

第十章,Storm 与 Redis、Elasticsearch 和 HBase 集成,教您如何将 Storm 与各种其他大数据技术集成。

第十一章,使用 Storm 进行 Apache 日志处理,介绍了一个示例日志处理应用程序,其中我们解析 Apache Web 服务器日志并从日志文件中生成一些业务信息。

第十二章,Twitter 推文收集和机器学习,将带您完成一个案例研究,实现了 Storm 中的机器学习拓扑。

您需要为这本书做好准备

本书中的所有代码都在 CentOS 6.5 上进行了测试。它也可以在其他 Linux 和 Windows 变体上运行,只需在命令中进行适当的更改。

我们已经尝试使各章节都是独立的,并且每章中都包括了该章节中使用的所有软件的设置和安装。这些是本书中使用的软件包:

  • CentOS 6.5

  • Oracle JDK 8

  • Apache ZooKeeper 3.4.6

  • Apache Storm 1.0.2

  • Eclipse 或 Spring Tool Suite

  • Elasticsearch 2.4.4

  • Hadoop 2.2.2

  • Logstash 5.4.1

  • Kafka 0.9.0.1

  • Esper 5.3.0

这本书是为谁写的

如果您是一名 Java 开发人员,并且渴望进入使用 Apache Storm 进行实时流处理应用的世界,那么这本书适合您。本书从基础知识开始,不需要之前在 Storm 方面的经验。完成本书后,您将能够开发不太复杂的 Storm 应用程序。

约定

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“在 Nimbus 机器的 storm.yaml 文件中添加以下行以在 Nimbus 节点上启用 JMX。”

代码块设置如下:

<dependency>
  <groupId>org.apache.storm</groupId>
  <artifactId>storm-core</artifactId>
  <version>1.0.2</version>
  <scope>provided<scope>
</dependency>

任何命令行输入或输出都将按如下方式编写:

cd $ZK_HOME/conf touch zoo.cfg

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会在文本中显示为:“现在,单击“连接”按钮以查看监督节点的指标。”

警告或重要说明看起来像这样。

技巧和窍门看起来像这样。

第一章:实时处理和 Storm 介绍

随着生成的数据量呈指数级增长和先进的数据捕获能力,企业面临着从这些海量原始数据中获取信息的挑战。在批处理方面,Hadoop 已成为处理大数据的首选框架。直到最近,当人们寻找构建实时流处理应用程序的框架时,一直存在空白。这些应用程序已成为许多企业的重要组成部分,因为它们使企业能够迅速响应事件并适应不断变化的情况。其例子包括监视社交媒体以分析公众对您推出的任何新产品的反应,并根据与选举相关的帖子的情绪来预测选举结果。

组织正在从外部来源收集大量数据,并希望实时评估/处理数据以获取市场趋势、检测欺诈、识别用户行为等。实时处理的需求日益增加,我们需要一个支持以下功能的实时系统/平台:

  • 可扩展:平台应具有水平可扩展性,无需任何停机时间。

  • 容错性:即使集群中的一些节点出现故障,平台也应能够处理数据。

  • 无数据丢失:平台应提供消息的可靠处理。

  • 高吞吐量:系统应能够支持每秒数百万条记录,并支持任何大小的消息。

  • 易于操作:系统应具有易于安装和操作的特点。此外,集群的扩展应是一个简单的过程。

  • 多语言:平台应支持多种语言。最终用户应能够用不同的语言编写代码。例如,用户可以用 Python、Scala、Java 等编写代码。此外,我们可以在一个集群中执行不同语言的代码。

  • 集群隔离:系统应支持隔离,以便为处理分配专用进程到专用机器。

Apache Storm

Apache Storm 已成为行业领袖开发分布式实时数据处理平台的首选平台。它提供了一组原语,可用于开发可以高度可扩展地实时处理大量数据的应用程序。

风暴对实时处理就像 Hadoop 对批处理一样重要。它是开源软件,由 Apache 软件基金会管理。它已经被 Twitter、Yahoo!和 Flipboard 等公司部署,以满足实时处理的需求。Storm 最初是由 BackType 的 Nathan Marz 开发的,BackType 是一家提供社交搜索应用的公司。后来,BackType 被 Twitter 收购,成为其基础设施的关键部分。Storm 可以用于以下用例:

  • 流处理:Storm 用于处理数据流并实时更新各种数据库。这种处理是实时的,处理速度需要与输入数据速度匹配。

  • 持续计算:Storm 可以对数据流进行持续计算,并实时将结果传输给客户端。这可能需要在每条消息到达时进行处理,或者在短时间内创建小批处理。持续计算的一个例子是将 Twitter 上的热门话题流式传输到浏览器中。

  • 分布式 RPC:Storm 可以并行处理复杂查询,以便您可以实时计算它。

  • 实时分析:Storm 可以分析并响应来自不同数据源的实时数据。

在本章中,我们将涵盖以下主题:

  • 什么是 Storm?

  • Storm 的特点

  • Storm 集群的架构和组件

  • Storm 的术语

  • 编程语言

  • 操作模式

Storm 的特点

以下是一些使 Storm 成为实时处理数据流的完美解决方案的特点:

  • 快速:据报道,Storm 每个节点每秒可以处理高达 100 万个元组/记录。

  • 横向可扩展:快速是构建高容量/高速数据处理平台的必要特性,但单个节点对其每秒处理事件数量有上限。节点代表设置中的单台机器,执行 Storm 应用程序。作为分布式平台,Storm 允许您向 Storm 集群添加更多节点,并增加应用程序的处理能力。此外,它是线性可扩展的,这意味着通过增加节点可以使处理能力加倍。

  • 容错:Storm 集群中的工作单元由工作进程执行。当工作进程死掉时,Storm 将重新启动该工作进程,如果运行该工作进程的节点死掉,Storm 将在集群中的其他节点上重新启动该工作进程。这个特性将在第三章中详细介绍,Storm 并行性和数据分区

  • 数据处理保证:Storm 提供强有力的保证,即进入 Storm 进程的每条消息至少会被处理一次。在发生故障时,Storm 将重放丢失的元组/记录。此外,它可以配置为每条消息只被处理一次。

  • 易于操作:Storm 部署和管理都很简单。一旦部署了集群,就需要很少的维护。

  • 编程语言无关:尽管 Storm 平台在Java 虚拟机JVM)上运行,但在其上运行的应用程序可以用任何能够读写标准输入和输出流的编程语言编写。

Storm 组件

Storm 集群遵循主从模型,其中主和从进程通过 ZooKeeper 协调。以下是 Storm 集群的组件。

Nimbus

Nimbus 节点是 Storm 集群中的主节点。它负责在各个工作节点之间分发应用程序代码,将任务分配给不同的机器,监视任务是否出现故障,并在需要时重新启动它们。

Nimbus 是无状态的,它将所有数据存储在 ZooKeeper 中。在 Storm 集群中只有一个 Nimbus 节点。如果活动节点宕机,那么备用节点将成为活动节点。它被设计为快速失败,因此当活动 Nimbus 宕机时,备用节点将成为活动节点,或者宕机的节点可以重新启动而不会对工作节点上已经运行的任务产生任何影响。这与 Hadoop 不同,如果 JobTracker 宕机,所有正在运行的作业都会处于不一致状态,需要重新执行。即使所有 Nimbus 节点都宕机,Storm 工作节点也可以正常工作,但用户无法向集群提交任何新作业,或者集群将无法重新分配失败的工作节点到另一个节点。

主管节点

主管节点是 Storm 集群中的工作节点。每个主管节点运行一个主管守护进程,负责创建、启动和停止工作进程以执行分配给该节点的任务。与 Nimbus 一样,主管守护进程也是快速失败的,并将其所有状态存储在 ZooKeeper 中,以便可以在不丢失状态的情况下重新启动。通常,单个主管守护进程会处理在该机器上运行的多个工作进程。

ZooKeeper 集群

在任何分布式应用程序中,各种进程需要相互协调并共享一些配置信息。ZooKeeper 是一个应用程序,以可靠的方式提供所有这些服务。作为一个分布式应用程序,Storm 也使用 ZooKeeper 集群来协调各种进程。与 ZooKeeper 中的所有状态和提交给 Storm 的各种任务相关的所有数据都存储在 ZooKeeper 中。Nimbus 和监督节点不直接相互通信,而是通过 ZooKeeper。由于所有数据都存储在 ZooKeeper 中,因此 Nimbus 和监督守护程序都可以突然被杀死而不会对集群产生不利影响。

以下是一个 Storm 集群的架构图:

Storm 数据模型

Storm 应用程序可以处理的基本数据单元称为元组。每个元组由预定义的字段列表组成。每个字段的值可以是字节、字符、整数、长整数、浮点数、双精度浮点数、布尔值或字节数组。Storm 还提供了一个 API 来定义自己的数据类型,这些数据类型可以作为元组中的字段进行序列化。

元组是动态类型的,也就是说,您只需要定义元组中字段的名称而不需要它们的数据类型。动态类型的选择有助于简化 API 并使其易于使用。此外,由于 Storm 中的处理单元可以处理多种类型的元组,因此声明字段类型并不实际。

元组中的每个字段都可以通过其名称getValueByField(String)或其位置索引getValue(int)来访问。元组还提供了方便的方法,例如getIntegerByField(String),可以使您免于对对象进行类型转换。例如,如果您有一个表示分数的Fraction (numerator, denominator)元组,那么您可以通过使用getIntegerByField("numerator")getInteger(0)来获取分子的值。

您可以在位于storm.apache.org/releases/1.0.2/javadocs/org/apache/storm/tuple/Tuple.html的 Java 文档中查看org.apache.storm.tuple.Tuple支持的完整操作集。

Storm 拓扑的定义

在 Storm 术语中,拓扑是定义计算图的抽象。您可以创建一个 Storm 拓扑并将其部署到 Storm 集群中以处理数据。拓扑可以用有向无环图表示,其中每个节点都进行某种处理并将其转发到流程中的下一个节点。以下图是一个示例 Storm 拓扑:

以下是 Storm 拓扑的组件:

  • Tuple:在拓扑的不同实例之间流动的单个消息/记录称为元组。

  • Stream:Storm 中的关键抽象是流。流是一系列可以由 Storm 并行处理的元组。每个流可以由单个或多个类型的 bolt(Storm 中的处理单元,在本节后面定义)并行处理。因此,Storm 也可以被视为转换流的平台。在前面的图中,流用箭头表示。Storm 应用程序中的每个流都被赋予一个 ID,bolt 可以根据其 ID 从这些流中产生和消费元组。每个流还有一个与其流经的元组相关的模式。

  • Spout:Spout 是 Storm 拓扑中元组的来源。它负责从外部来源读取或监听数据,例如从日志文件中读取或监听队列中的新消息并发布它们--在 Storm 术语中发射到流中。Spout 可以发射多个流,每个流具有不同的模式。例如,它可以从日志文件中读取包含 10 个字段的记录,并将它们作为包含七个字段元组和四个字段元组的不同流发射出去。

org.apache.storm.spout.ISpout接口是用于定义喷口的接口。如果您在 Java 中编写拓扑,则应使用org.apache.storm.topology.IRichSpout,因为它声明了与TopologyBuilderAPI 一起使用的方法。每当喷口发射一个元组时,Storm 会跟踪处理此元组时生成的所有元组,当源元组的图中所有元组的执行完成时,它将向喷口发送确认。只有在发射元组时提供了消息 ID 时才会发生此跟踪。如果使用 null 作为消息 ID,则不会发生此跟踪。

还可以为拓扑定义元组处理超时,如果元组在指定的超时时间内未被处理,将向喷口发送失败消息。再次强调,只有在定义消息 ID 时才会发生这种情况。通过跳过发射元组时的消息 ID 来禁用消息确认,可以从 Storm 中获得一些小的性能提升,但也会有一些数据丢失的风险。

喷口的重要方法有:

    • nextTuple(): Storm 调用此方法从输入源获取下一个元组。在此方法内部,您将具有从外部源读取数据并将其发射到org.apache.storm.spout.ISpoutOutputCollector实例的逻辑。可以使用org.apache.storm.topology.OutputFieldsDeclarerdeclareStream方法声明流的模式。

如果喷口希望向多个流发射数据,可以使用declareStream方法声明多个流,并在发射元组时指定流 ID。如果此时没有更多的元组要发射,此方法将不会被阻塞。此外,如果此方法不发射元组,则 Storm 将在再次调用它之前等待 1 毫秒。可以使用topology.sleep.spout.wait.strategy.time.ms设置来配置此等待时间。

    • ack(Object msgId): 当具有给定消息 ID 的元组被拓扑完全处理时,Storm 将调用此方法。在这一点上,用户应标记消息已处理,并进行必要的清理,例如从消息队列中删除消息,以便不再处理它。
  • fail(Object msgId): 当 Storm 识别出具有给定消息 ID 的元组未能成功处理或超时配置的时间间隔时,将调用此方法。在这种情况下,用户应进行必要的处理,以便通过nextTuple方法再次发射消息。一个常见的做法是将消息放回传入消息队列。

  • open(): 当喷口初始化时,只调用一次此方法。如果需要连接到外部源以获取输入数据,应在 open 方法中定义连接到外部源的逻辑,然后在nextTuple方法中不断从外部源获取数据以进一步发射它。

在编写喷口时需要注意的另一点是,不能阻塞任何方法,因为 Storm 在同一线程中调用所有方法。每个喷口都有一个内部缓冲区,用于跟踪到目前为止发射的元组的状态。喷口将保留这些元组在缓冲区中,直到它们被确认或失败,分别调用ackfail方法。只有当此缓冲区不满时,Storm 才会调用nextTuple方法。

  • Bolt: 一个 bolt 是 Storm 拓扑的处理引擎,负责转换流。理想情况下,拓扑中的每个 bolt 都应该对元组进行简单的转换,许多这样的 bolt 可以相互协调,展示复杂的转换。

org.apache.storm.task.IBolt接口通常用于定义 bolt,如果拓扑是用 Java 编写的,则应该使用org.apache.storm.topology.IRichBolt接口。Bolt 可以订阅拓扑中其他组件(spouts 或其他 bolts)的多个流,同样也可以向多个流发出输出。可以使用org.apache.storm.topology.OutputFieldsDeclarerdeclareStream方法声明输出流。

一个 bolt 的重要方法有:

    • execute(Tuple input): 对于通过订阅的输入流传入的每个元组,将执行此方法。在此方法中,您可以对元组进行所需的任何处理,然后以发出更多元组到声明的输出流的形式,或者其他操作,比如将结果持久化到数据库。

在调用此方法时,您不需要立即处理元组,可以将元组保留直到需要。例如,在连接两个流时,当一个元组到达时,您可以将其保留,直到其对应的元组也到达,然后您可以发出连接的元组。

与元组相关的元数据可以通过Tuple接口中定义的各种方法来检索。如果元组关联了消息 ID,则 execute 方法必须使用OutputCollector为 bolt 发布ackfail事件,否则 Storm 将不知道元组是否被成功处理。org.apache.storm.topology.IBasicBolt接口是一个方便的接口,在 execute 方法完成后会自动发送确认。如果要发送失败事件,此方法应该抛出org.apache.storm.topology.FailedException

    • prepare(Map stormConf, TopologyContext context, OutputCollector collector): 在 Storm 拓扑中,一个 bolt 可以由多个 worker 执行。Bolt 的实例在客户端机器上创建,然后序列化并提交给 Nimbus。当 Nimbus 为拓扑创建 worker 实例时,它会将这个序列化的 bolt 发送给 worker。worker 将解序列化 bolt 并调用prepare方法。在这个方法中,您应该确保 bolt 被正确配置以执行元组。您希望保持的任何状态可以存储为 bolt 的实例变量,稍后可以进行序列化/反序列化。

Storm 中的操作模式

操作模式指示了拓扑在 Storm 中的部署方式。Storm 支持两种类型的操作模式来执行 Storm 拓扑:

  • 本地模式:在本地模式下,Storm 拓扑在单个 JVM 中在本地机器上运行。这种模式模拟了单个 JVM 中的 Storm 集群,并用于拓扑的测试和调试。

  • 远程模式:在远程模式下,我们将使用 Storm 客户端将拓扑提交给主节点,以及执行拓扑所需的所有必要代码。Nimbus 将负责分发您的代码。

在下一章中,我们将更详细地介绍本地模式和远程模式,以及一个示例。

编程语言

Storm 从一开始就被设计为可用于任何编程语言。Storm 的核心是用于定义和提交拓扑的 thrift 定义。由于 thrift 可以在任何语言中使用,因此可以用任何语言定义和提交拓扑。

同样,spouts 和 bolts 可以用任何语言定义。非 JVM spouts 和 bolts 通过stdin/stdout上的基于 JSON 的协议与 Storm 通信。实现这种协议的适配器存在于 Ruby、Python、JavaScript 和 Perl 中。您可以参考github.com/apache/storm/tree/master/storm-multilang了解这些适配器的实现。

Storm-starter 有一个示例拓扑,github.com/apache/storm/tree/master/examples/storm-starter/multilang/resources,其中使用 Python 实现了其中一个 bolt。

总结

在本章中,我们向您介绍了 Storm 的基础知识以及构成 Storm 集群的各种组件。我们看到了 Storm 集群可以操作的不同部署/运行模式的定义。

在下一章中,我们将建立一个单节点和三节点的 Storm 集群,并看看如何在 Storm 集群上部署拓扑。我们还将看到 Storm 支持的不同类型的流分组以及 Storm 提供的消息语义保证。

第二章:Storm 部署、拓扑开发和拓扑选项

本章中,我们将从在多个节点(三个 Storm 和三个 ZooKeeper)集群上部署 Storm 开始。这一章非常重要,因为它关注了我们如何设置生产 Storm 集群以及为什么我们需要 Storm Supervisor、Nimbus 和 ZooKeeper 的高可用性(因为 Storm 使用 ZooKeeper 来存储集群、拓扑等元数据)。

以下是本章将要涵盖的关键点:

  • Storm 集群的部署

  • 程序和部署词频统计示例

  • Storm UI 的不同选项——kill、active、inactive 和 rebalance

  • Storm UI 的演练

  • 动态日志级别设置

  • 验证 Nimbus 的高可用性

Storm 的先决条件

在开始部署 Storm 集群之前,您应该安装 Java JDK 和 ZooKeeper 集群。

安装 Java SDK 7

执行以下步骤在您的机器上安装 Java SDK 7。您也可以选择 JDK 1.8:

  1. 从 Oracle 网站(www.oracle.com/technetwork/java/javase/downloads/index.html)下载 Java SDK 7 RPM。

  2. 使用以下命令在您的 CentOS 机器上安装 Java jdk-7u<version>-linux-x64.rpm文件:

sudo rpm -ivh jdk-7u<version>-linux-x64.rpm 
  1. ~/.bashrc文件中添加以下环境变量:
export JAVA_HOME=/usr/java/jdk<version>
  1. 将 JDK 的bin目录的路径添加到PATH系统环境变量中,添加到~/.bashrc文件中:
export PATH=$JAVA_HOME/bin:$PATH 
  1. 运行以下命令在当前登录终端重新加载bashrc文件:
source ~/.bashrc
  1. 检查 Java 安装如下:
java -version  

上述命令的输出如下:

java version "1.7.0_71"
Java(TM) SE Runtime Environment (build 1.7.0_71-b14)
Java HotSpot(TM) 64-Bit Server VM (build 24.71-b01, mixed mode) 

ZooKeeper 集群的部署

在任何分布式应用程序中,各种进程需要相互协调并共享配置信息。ZooKeeper 是一个应用程序,以可靠的方式提供所有这些服务。作为一个分布式应用程序,Storm 也使用 ZooKeeper 集群来协调各种进程。与集群相关的所有状态和提交给 Storm 的各种任务都存储在 ZooKeeper 中。本节描述了如何设置 ZooKeeper 集群。我们将部署一个由三个节点组成的 ZooKeeper 集群,可以处理一个节点故障。以下是三个节点 ZooKeeper 集群的部署图:

在 ZooKeeper 集群中,集群中的一个节点充当领导者,而其余的节点充当跟随者。如果 ZooKeeper 集群的领导者节点死亡,那么在剩余的活动节点中进行新的领导者选举,并选举出一个新的领导者。来自客户端的所有写请求都会被转发到领导者节点,而跟随者节点只处理读请求。此外,我们无法通过增加节点数量来增加 ZooKeeper 集合的写性能,因为所有写操作都经过领导者节点。

建议运行奇数个 ZooKeeper 节点,因为只要大多数节点(活动节点数大于n/2,其中n为部署节点数)在运行,ZooKeeper 集群就会继续工作。因此,如果我们有一个由四个 ZooKeeper 节点组成的集群(3 > 4/2;只能有一个节点死亡),那么我们只能处理一个节点故障,而如果我们在集群中有五个节点(3 > 5/2;可以处理两个节点故障),那么我们可以处理两个节点故障。

步骤 1 到 4 需要在每个节点上执行以部署 ZooKeeper 集群:

  1. 从 ZooKeeper 网站(zookeeper.apache.org/releases.html)下载最新的稳定 ZooKeeper 版本。在撰写本文时,最新版本是 ZooKeeper 3.4.6。

  2. 一旦你下载了最新版本,解压它。现在,我们设置ZK_HOME环境变量以使设置更容易。

  3. ZK_HOME环境变量指向解压后的目录。使用以下命令在$ZK_HOME/conf目录中创建配置文件zoo.cfg

cd $ZK_HOME/conf 
touch zoo.cfg 
  1. 将以下属性添加到zoo.cfg文件中:
tickTime=2000 
dataDir=/var/zookeeper 
clientPort=2181 
initLimit=5 
syncLimit=2 
server.1=zoo1:2888:3888 
server.2=zoo2:2888:3888 
server.3=zoo3.2888.3888  

这里,zoo1zoo2zoo3是 ZooKeeper 节点的 IP 地址。以下是每个属性的定义:

    • tickTime:这是 ZooKeeper 中以毫秒为单位使用的基本时间单位。它用于发送心跳,最小会话超时将是tickTime值的两倍。
  • dataDir:这是用于存储内存数据库快照和事务日志的目录。

  • clientPort:这是用于监听客户端连接的端口。

  • initLimit:这是允许跟随者连接和同步到领导者节点所需的tickTime值的数量。

  • syncLimit:这是一个跟随者可以用来与领导者节点同步的tickTime值的数量。如果同步在此时间内未发生,跟随者将从集合中删除。

server.id=host:port:port格式的最后三行指定了集群中有三个节点。在集合中,每个 ZooKeeper 节点必须具有 1 到 255 之间的唯一 ID 号。通过在每个节点的dataDir目录中创建名为myid的文件来定义此 ID。例如,ID 为 1 的节点(server.1=zoo1:2888:3888)将在目录/var/zookeeper中具有一个myid文件,其中包含1

对于此集群,在三个位置创建myid文件,如下所示:

At zoo1 /var/zookeeper/myid contains 1 
At zoo2 /var/zookeeper/myid contains 2 
At zoo3 /var/zookeeper/myid contains 3  
  1. 在每台机器上运行以下命令以启动 ZooKeeper 集群:
bin/zkServer.sh start  

通过执行以下步骤检查 ZooKeeper 节点的状态:

  1. zoo1节点上运行以下命令以检查第一个节点的状态:
bin/zkServer.sh status 

以下信息显示:

JMX enabled by default 
Using config: /home/root/zookeeper-3.4.6/bin/../conf/zoo.cfg 
Mode: follower   

第一个节点以follower模式运行。

  1. 通过执行以下命令检查第二个节点的状态:
bin/zkServer.sh status  

以下信息显示:

JMX enabled by default 
Using config: /home/root/zookeeper-3.4.6/bin/../conf/zoo.cfg 
Mode: leader  

第二个节点以leader模式运行。

  1. 通过执行以下命令检查第三个节点的状态:
bin/zkServer.sh status

以下信息显示:

JMX enabled by default 
Using config: /home/root/zookeeper-3.4.6/bin/../conf/zoo.cfg 
Mode: follower  

第三个节点以follower模式运行。

  1. 在领导者机器上运行以下命令以停止领导者节点:
bin/zkServer.sh stop  

现在,通过执行以下步骤检查剩余两个节点的状态:

  1. 使用以下命令检查第一个节点的状态:
bin/zkServer.sh status  

以下信息显示:

JMX enabled by default 
Using config: /home/root/zookeeper-3.4.6/bin/../conf/zoo.cfg 
Mode: follower   

第一个节点再次以follower模式运行。

  1. 使用以下命令检查第二个节点的状态:
bin/zkServer.sh status   

以下信息显示:

JMX enabled by default 
Using config: /home/root/zookeeper-3.4.6/bin/../conf/zoo.cfg 
Mode: leader  

第三个节点被选举为新的领导者。

  1. 现在,使用以下命令重新启动第三个节点:
bin/zkServer.sh status  

这是一个快速介绍,介绍了如何设置 ZooKeeper,可用于开发;但是,不适合生产。有关 ZooKeeper 管理和维护的完整参考,请参阅 ZooKeeper 网站上的在线文档zookeeper.apache.org/doc/trunk/zookeeperAdmin.html

设置 Storm 集群

在本章中,我们将学习如何设置一个三节点 Storm 集群,其中一个节点将是活动的主节点(Nimbus),另外两个将是工作节点(supervisors)。

以下是我们三个节点 Storm 集群的部署图:

以下是设置三节点 Storm 集群所需执行的步骤:

  1. 安装并运行 ZooKeeper 集群。有关安装 ZooKeeper 的步骤在前一节中提到。

  2. storm.apache.org/downloads.html下载最新稳定的 Storm 版本;在撰写本文时,最新版本是 Storm 1.0.2。

  3. 一旦你下载了最新版本,在所有三台机器上复制并解压。现在,我们将在每台机器上设置$STORM_HOME环境变量,以便更轻松地进行设置。$STORM_HOME环境变量包含 Storm home文件夹的路径(例如,导出STORM_HOME=/home/user/storm-1.0.2)。

  4. 在主节点的$STORM_HOME/conf目录中,向storm.yaml文件添加以下行:

storm.zookeeper.servers: 
- "zoo1" 
- "zoo2" 
- "zoo3" 
storm.zookeeper.port: 2181 
nimbus.seeds: "nimbus1,nimbus2" 
storm.local.dir: "/tmp/storm-data"  

我们正在安装两个主节点。

  1. 在每个工作节点的$STORM_HOME/conf目录中,向storm.yaml文件添加以下行:
storm.zookeeper.servers: 
- "zoo1" 
- "zoo2" 
- "zoo3" 
storm.zookeeper.port: 2181 
nimbus.seeds: "nimbus1,nimbus2" 
storm.local.dir: "/tmp/storm-data" 
supervisor.slots.ports: 
- 6700 
- 6701 
- 6702 
- 6703  

如果你计划在同一台机器上执行 Nimbus 和 supervisor,则也在 Nimbus 机器上添加supervisor.slots.ports属性。

  1. 在主节点的$STORM_HOME目录中执行以下命令来启动主守护进程:
$> bin/storm nimbus &  
  1. 在每个工作节点(或 supervisor 节点)的$STORM_HOME目录中执行以下命令来启动工作守护进程:
$> bin/storm supervisor &  

开发 hello world 示例

在开始开发之前,你应该在你的项目中安装 Eclipse 和 Maven。这里解释的样本拓扑将涵盖如何创建一个基本的 Storm 项目,包括一个 spout 和 bolt,以及如何构建和执行它们。

通过使用com.stormadvance作为groupIdstorm-example作为artifactId创建一个 Maven 项目。

pom.xml文件中添加以下 Maven 依赖项:

<dependency> 
  <groupId>org.apache.storm</groupId> 
  <artifactId>storm-core</artifactId> 
  <version>1.0.2</version> 
  <scope>provided<scope> 
</dependency> 

确保 Storm 依赖的范围是提供的,否则你将无法在 Storm 集群上部署拓扑。

pom.xml文件中添加以下 Maven build插件:

<build> 
  <plugins> 
    <plugin> 
      <artifactId>maven-assembly-plugin</artifactId> 
      <version>2.2.1</version> 
      <configuration> 
        <descriptorRefs> 
          <descriptorRef>jar-with-dependencies 
          </descriptorRef> 
        </descriptorRefs> 
        <archive> 
          <manifest> 
            <mainClass /> 
          </manifest> 
        </archive> 
      </configuration> 
      <executions> 
        <execution> 
          <id>make-assembly</id> 
          <phase>package</phase> 
          <goals> 
            <goal>single</goal> 
          </goals> 
        </execution> 
      </executions> 
    </plugin> 
  </plugins> 
</build> 

通过在com.stormadvance.storm_example包中创建SampleSpout类来编写你的第一个样本 spout。SampleSpout类扩展了序列化的BaseRichSpout类。这个 spout 不连接到外部源来获取数据,而是随机生成数据并发出连续的记录流。以下是SampleSpout类的源代码及其解释:

public class SampleSpout extends BaseRichSpout { 
  private static final long serialVersionUID = 1L; 

  private static final Map<Integer, String> map = new HashMap<Integer, String>(); 
  static { 
    map.put(0, "google"); 
    map.put(1, "facebook"); 
    map.put(2, "twitter"); 
    map.put(3, "youtube"); 
    map.put(4, "linkedin"); 
  } 
  private SpoutOutputCollector spoutOutputCollector; 

  public void open(Map conf, TopologyContext context, SpoutOutputCollector spoutOutputCollector) { 
    // Open the spout 
    this.spoutOutputCollector = spoutOutputCollector; 
  } 

  public void nextTuple() { 
    // Storm cluster repeatedly calls this method to emita continuous 
    // stream of tuples. 
    final Random rand = new Random(); 
    // generate the random number from 0 to 4\. 
    int randomNumber = rand.nextInt(5); 
    spoutOutputCollector.emit(new Values(map.get(randomNumber))); 
    try{ 
      Thread.sleep(5000); 
    }catch(Exception e) { 
      System.out.println("Failed to sleep the thread"); 
    } 
  } 

  public void declareOutputFields(OutputFieldsDeclarer declarer) { 

  // emit the tuple with field "site" 
  declarer.declare(new Fields("site")); 
  } 
} 

通过在同一包中创建SampleBolt类来编写你的第一个样本 bolt。SampleBolt类扩展了序列化的BaseRichBolt类。这个 bolt 将消耗SampleSpout spout 发出的元组,并在控制台上打印site字段的值。以下是SampleStormBolt类的源代码及其解释:

public class SampleBolt extends BaseBasicBolt { 
  private static final long serialVersionUID = 1L; 

  public void execute(Tuple input, BasicOutputCollector collector) { 
    // fetched the field "site" from input tuple. 
    String test = input.getStringByField("site"); 
    // print the value of field "site" on console. 
    System.out.println("######### Name of input site is : " + test); 
  } 

  public void declareOutputFields(OutputFieldsDeclarer declarer) { 
  } 
} 

在同一包中创建一个主SampleStormTopology类。这个类创建了一个 spout 和 bolt 的实例以及类,并使用TopologyBuilder类将它们链接在一起。这个类使用org.apache.storm.LocalCluster来模拟 Storm 集群。LocalCluster模式用于在部署到 Storm 集群之前在开发者机器上进行调试/测试拓扑。以下是主类的实现:

public class SampleStormTopology { 
  public static void main(String[] args) throws AlreadyAliveException, InvalidTopologyException { 
    // create an instance of TopologyBuilder class 
    TopologyBuilder builder = new TopologyBuilder(); 
    // set the spout class 
    builder.setSpout("SampleSpout", new SampleSpout(), 2); 
    // set the bolt class 
    builder.setBolt("SampleBolt", new SampleBolt(), 4).shuffleGrouping("SampleSpout"); 
    Config conf = new Config(); 
    conf.setDebug(true); 
    // create an instance of LocalCluster class for 
    // executing topology in local mode. 
    LocalCluster cluster = new LocalCluster(); 
    // SampleStormTopology is the name of submitted topology 
    cluster.submitTopology("SampleStormTopology", conf, builder.createTopology()); 
    try { 
      Thread.sleep(100000); 
    } catch (Exception exception) { 
      System.out.println("Thread interrupted exception : " + exception); 
    } 
    // kill the SampleStormTopology 
    cluster.killTopology("SampleStormTopology"); 
    // shutdown the storm test cluster 
    cluster.shutdown(); 
  } 
} 

转到你的项目主目录,并运行以下命令以在本地模式下执行拓扑:

$> cd $STORM_EXAMPLE_HOME 
$> mvn compile exec:java -Dexec.classpathScope=compile -Dexec.mainClass=com.stormadvance.storm_example.SampleStormTopology 

现在为在实际 Storm 集群上部署拓扑创建一个新的拓扑类。在同一包中创建一个主SampleStormClusterTopology类。这个类还创建了一个 spout 和 bolt 的实例以及类,并使用TopologyBuilder类将它们链接在一起。

public class SampleStormClusterTopology { 
  public static void main(String[] args) throws AlreadyAliveException, InvalidTopologyException { 
    // create an instance of TopologyBuilder class 
    TopologyBuilder builder = new TopologyBuilder(); 
    // set the spout class 
    builder.setSpout("SampleSpout", new SampleSpout(), 2); 
    // set the bolt class 
    builder.setBolt("SampleBolt", new SampleBolt(), 4).shuffleGrouping("SampleSpout"); 
    Config conf = new Config(); 
    conf.setNumWorkers(3); 
    // This statement submit the topology on remote 
    // args[0] = name of topology 
    try { 
      StormSubmitter.submitTopology(args[0], conf, builder.createTopology()); 
    } catch (AlreadyAliveException alreadyAliveException) { 
      System.out.println(alreadyAliveException); 
    } catch (InvalidTopologyException invalidTopologyException) { 
      System.out.println(invalidTopologyException); 
    } catch (AuthorizationException e) { 
      // TODO Auto-generated catch block 
      e.printStackTrace(); 
    } 
  } 
} 

通过在项目的主目录上运行以下命令来构建你的 Maven 项目:

mvn clean install  

上述命令的输出如下:

    ------------------------------------------------------------------ ----- 
    [INFO] ----------------------------------------------------------- ----- 
    [INFO] BUILD SUCCESS 
    [INFO] ----------------------------------------------------------- ----- 
    [INFO] Total time: 58.326s 
    [INFO] Finished at: 
    [INFO] Final Memory: 14M/116M 
    [INFO] ----------------------------------------------------------- ----

我们可以使用以下 Storm 客户端命令将拓扑部署到集群:

bin/storm jar jarName.jar [TopologyMainClass] [Args] 

上述命令使用参数arg1arg2运行TopologyMainClassTopologyMainClass的主要功能是定义拓扑并将其提交到 Nimbus 机器。storm jar部分负责连接到 Nimbus 机器并上传 JAR 部分。

登录到 Storm Nimbus 机器并执行以下命令:

$> cd $STORM_HOME
$> bin/storm jar ~/storm_example-0.0.1-SNAPSHOT-jar-with-dependencies.jar com.stormadvance.storm_example.SampleStormClusterTopology storm_example  

在上述代码中,~/storm_example-0.0.1-SNAPSHOT-jar-with-dependencies.jar是我们在 Storm 集群上部署的SampleStormClusterTopology JAR 的路径。

显示以下信息:

702  [main] INFO  o.a.s.StormSubmitter - Generated ZooKeeper secret payload for MD5-digest: -8367952358273199959:-5050558042400210383
793  [main] INFO  o.a.s.s.a.AuthUtils - Got AutoCreds []
856  [main] INFO  o.a.s.StormSubmitter - Uploading topology jar /home/USER/storm_example-0.0.1-SNAPSHOT-jar-with-dependencies.jar to assigned location: /tmp/storm-data/nimbus/inbox/stormjar-d3007821-f87d-48af-8364-cff7abf8652d.jar
867  [main] INFO  o.a.s.StormSubmitter - Successfully uploaded topology jar to assigned location: /tmp/storm-data/nimbus/inbox/stormjar-d3007821-f87d-48af-8364-cff7abf8652d.jar
868  [main] INFO  o.a.s.StormSubmitter - Submitting topology storm_example in distributed mode with conf {"storm.zookeeper.topology.auth.scheme":"digest","storm.zookeeper.topology.auth.payload":"-8367952358273199959:-5050558042400210383","topology.workers":3}
 1007 [main] INFO  o.a.s.StormSubmitter - Finished submitting topology: storm_example  

运行jps命令,查看运行的 JVM 进程数量如下:

jps   

前面命令的输出是:

26827 worker 
26530 supervisor 
26824 worker 
26468 nimbus 
26822 worker  

在上述代码中,worker是为SampleStormClusterTopology拓扑启动的 JVM。

Storm 拓扑的不同选项

此部分涵盖了用户可以在 Storm 集群上执行的以下操作:

  • 停用

  • 激活

  • 重新平衡

  • 杀死

  • 动态日志级别设置

停用

Storm 支持停用拓扑。在停用状态下,spout 不会向管道中发射任何新的元组,但已经发射的元组的处理将继续。以下是停用运行中拓扑的命令:

$> bin/storm deactivate topologyName 

使用以下命令停用SampleStormClusterTopology

bin/storm deactivate SampleStormClusterTopology 

显示以下信息:

0 [main] INFO backtype.storm.thrift - Connecting to Nimbus at localhost:6627 
76 [main] INFO backtype.storm.command.deactivate - Deactivated topology: SampleStormClusterTopology  

激活

Storm 还支持激活拓扑。当拓扑被激活时,spout 将重新开始发射元组。以下是激活拓扑的命令:

$> bin/storm activate topologyName  

使用以下命令激活SampleStormClusterTopology

bin/storm activate SampleStormClusterTopology

显示以下信息:

0 [main] INFO backtype.storm.thrift - Connecting to Nimbus at localhost:6627 
65 [main] INFO backtype.storm.command.activate - Activated topology: SampleStormClusterTopology  

重新平衡

在运行时更新拓扑并行度的过程称为重新平衡。有关此操作的更详细信息可以在第三章中找到,Storm 并行性和数据分区

杀死

Storm 拓扑是永无止境的进程。要停止一个拓扑,我们需要杀死它。被杀死后,拓扑首先进入停用状态,处理已经发射到其中的所有元组,然后停止。运行以下命令杀死SampleStormClusterTopology

$> bin/storm kill SampleStormClusterTopology  

显示以下信息:

0 [main] INFO backtype.storm.thrift - Connecting to Nimbus at localhost:6627 
80 [main] INFO backtype.storm.command.kill-topology - Killed topology: SampleStormClusterTopology

现在,再次运行jps命令,查看剩余的 JVM 进程如下:

jps  

前面命令的输出是:

26530 supervisor 
27193 Jps 
26468 nimbus  

动态日志级别设置

这允许用户在不停止拓扑的情况下更改拓扑的日志级别。此操作的详细信息可以在本章末尾找到。

Storm UI 的演练

此部分将向您展示如何启动 Storm UI 守护程序。但是,在启动 Storm UI 守护程序之前,我们假设您已经有一个运行中的 Storm 集群。Storm 集群部署步骤在本章的前几节中有提到。现在,转到 Storm 主目录(cd $STORM_HOME)在领导 Nimbus 机器上,并运行以下命令启动 Storm UI 守护程序:

$> cd $STORM_HOME
$> bin/storm ui &  

默认情况下,Storm UI 在启动的机器的8080端口上启动。现在,我们将浏览到http://nimbus-node:8080页面,查看 Storm UI,其中 Nimbus 节点是 Nimbus 机器的 IP 地址或主机名。

以下是 Storm 主页的屏幕截图:

集群摘要部分

Storm UI 的这一部分显示了在集群中部署的 Storm 版本、Nimbus 节点的正常运行时间、空闲工作插槽数量、已使用的工作插槽数量等。在向集群提交拓扑时,用户首先需要确保空闲插槽列的值不为零;否则,拓扑将不会获得任何用于处理的工作进程,并将在队列中等待,直到有工作进程空闲为止。

Nimbus 摘要部分

Storm UI 的这一部分显示了在 Storm 集群中运行的 Nimbus 进程数量。该部分还显示了 Nimbus 节点的状态。状态为Leader的节点是活动主节点,而状态为Not a Leader的节点是被动主节点。

监督摘要部分

Storm UI 的这一部分显示了运行在集群中的监督节点的列表,以及它们的 Id、主机、正常运行时间、插槽和已使用插槽列。

Nimbus 配置部分

Storm UI 的此部分显示了 Nimbus 节点的配置。一些重要的属性是:

  • supervisor.slots.ports

  • storm.zookeeper.port

  • storm.zookeeper.servers

  • storm.zookeeper.retry.interval

  • worker.childopts

  • supervisor.childopts

以下是 Nimbus 配置的屏幕截图:

拓扑摘要部分

Storm UI 的此部分显示了在 Storm 集群中运行的拓扑列表,以及它们的 ID,分配给拓扑的工作进程数量,执行器数量,任务数量,正常运行时间等。

让我们通过运行以下命令在远程 Storm 集群中部署示例拓扑(如果尚未运行):

$> cd $STORM_HOME
$> bin/storm jar ~/storm_example-0.0.1-SNAPSHOT-jar-with-dependencies.jar com.stormadvance.storm_example.SampleStormClusterTopology storm_example  

我们通过定义三个工作进程、两个执行器用于SampleSpout和四个执行器用于SampleBolt创建了SampleStormClusterTopology拓扑。

在 Storm 集群上提交SampleStormClusterTopology后,用户必须刷新 Storm 主页。

以下屏幕截图显示在拓扑摘要部分为SampleStormClusterTopology添加了一行。拓扑部分包含拓扑的名称,拓扑的唯一 ID,拓扑的状态,正常运行时间,分配给拓扑的工作进程数量等。状态字段的可能值为ACTIVEKILLEDINACTIVE

让我们单击SampleStormClusterTopology以查看其详细统计信息。有两个屏幕截图。第一个包含有关分配给SampleStormClusterTopology拓扑的工作进程、执行器和任务数量的信息。

下一个屏幕截图包含有关喷口和螺栓的信息,包括分配给每个喷口和螺栓的执行器和任务数量:

前面屏幕截图中显示的信息如下:

  • 拓扑统计:此部分将提供有关在 10 分钟、3 小时、1 天和自拓扑启动以来的窗口内发出的元组数量、传输数量、确认数量、容量延迟等信息。

  • 喷口(所有时间):此部分显示拓扑内所有运行的喷口的统计信息

  • Bolts(所有时间):此部分显示拓扑内所有运行的螺栓的统计信息

  • 拓扑操作:此部分允许我们通过 Storm UI 直接对拓扑执行激活、停用、重平衡、杀死等操作:

  • 停用:单击停用以停用拓扑。一旦拓扑停用,喷口停止发出元组,并且在 Storm UI 上拓扑的状态变为 INACTIVE。

停用拓扑不会释放 Storm 资源。

    • 激活:单击激活按钮以激活拓扑。一旦拓扑被激活,喷口将再次开始发出元组。
  • Kill:单击 Kill 按钮销毁/杀死拓扑。一旦拓扑被杀死,它将释放分配给该拓扑的所有 Storm 资源。在杀死拓扑时,Storm 将首先停用喷口,并等待警报框中提到的杀死时间,以便螺栓有机会完成喷口发出的元组的处理,然后再执行杀命令。以下屏幕截图显示了如何通过 Storm UI 杀死拓扑:

让我们转到 Storm UI 的主页,以查看SampleStormClusterToplogy的状态,如下图所示:

动态日志级别设置

动态日志级别允许我们从 Storm CLI 和 Storm UI 在运行时更改拓扑的日志级别设置。

从 Storm UI 更新日志级别

按照以下步骤从 Storm UI 更新日志级别:

  1. 如果SampleStormClusterTopology没有运行,请在 Storm 集群上再次部署。

  2. 浏览 Storm UI,网址为http://nimbus-node:8080/

  3. 单击storm_example拓扑。

  4. 现在点击“更改日志级别”按钮来更改拓扑的ROOT记录器,如下面的屏幕截图所示:

  1. 配置以下屏幕截图中提到的条目,将ROOT记录器更改为 ERROR:

  1. 如果您计划将日志级别更改为 DEBUG,则必须指定该日志级别的超时(过期时间),如下面的屏幕截图所示:

  1. 一旦到达超时时间中提到的时间,日志级别将恢复为默认值:

  1. 操作列中提到的清除按钮将清除日志设置,并且应用将再次设置默认的日志设置。

从 Storm CLI 更新日志级别

我们可以从 Storm CLI 修改日志级别。以下是用户必须从 Storm 目录执行的命令,以更新运行时的日志设置:

bin/storm set_log_level [topology name] -l [logger name]=[LEVEL]:[TIMEOUT] 

在上述代码中,topology name是拓扑的名称,logger name是我们想要更改的记录器。如果要更改ROOT记录器,则将ROOT用作logger name的值。LEVEL是您要应用的日志级别。可能的值包括DEBUGINFOERRORTRACEALLWARNFATALOFF

TIMEOUT是以秒为单位的时间。超时时间后,日志级别将恢复为正常。如果要将日志级别设置为DEBUG/ALL,则TIMEOUT的值是必需的。

以下是更改storm_example拓扑的日志级别设置的命令:

$> bin/storm set_log_level storm_example -l ROOT=DEBUG:30  

以下是清除日志级别设置的命令:

$> ./bin/storm set_log_level storm_example -r ROOT 

总结

在本章中,我们已经涵盖了 Storm 和 ZooKeeper 集群的安装,Storm 集群上拓扑的部署,Nimbus 节点的高可用性,以及通过 Storm UI 进行拓扑监控。我们还介绍了用户可以在运行中的拓扑上执行的不同操作。最后,我们重点关注了如何改变运行中拓扑的日志级别。

在下一章中,我们将重点关注在多个 Storm 机器/节点上分发拓扑。

第三章:Storm 并行性和数据分区

在前两章中,我们已经介绍了 Storm 的概述、Storm 的安装以及开发一个示例拓扑。在本章中,我们将专注于将拓扑分布在多个 Storm 机器/节点上。本章涵盖以下内容:

  • 拓扑的并行性

  • 如何在代码级别配置并行性

  • Storm 集群中不同类型的流分组

  • 消息处理保证

  • Tick tuple

拓扑的并行性

并行性意味着将作业分布在多个节点/实例上,每个实例可以独立工作并有助于数据的处理。让我们首先看一下负责 Storm 集群并行性的进程/组件。

工作进程

Storm 拓扑在 Storm 集群中的多个监督节点上执行。集群中的每个节点可以运行一个或多个称为工作进程的 JVM,负责处理拓扑的一部分。

工作进程特定于特定的拓扑,并且可以执行该拓扑的多个组件。如果同时运行多个拓扑,它们中的任何一个都不会共享任何工作进程,因此在拓扑之间提供了一定程度的隔离。

执行器

在每个工作进程中,可以有多个线程执行拓扑的部分。这些线程中的每一个都被称为执行器。执行器只能执行拓扑中的一个组件,即拓扑中的任何 spout 或 bolt。

每个执行器作为一个单独的线程,只能按顺序执行分配给它的任务。在拓扑运行时,可以动态更改为 spout 或 bolt 定义的执行器数量,这意味着您可以轻松控制拓扑中各个组件的并行度。

任务

这是 Storm 中任务执行的最细粒度单位。每个任务都是 spout 或 bolt 的一个实例。在定义 Storm 拓扑时,可以为每个 spout 和 bolt 指定任务的数量。一旦定义,组件的任务数量就不能在运行时更改。每个任务可以单独执行,也可以与相同类型的另一个任务或相同 spout 或 bolt 的另一个实例一起执行。

以下图表描述了工作进程、执行器和任务之间的关系。在下图中,每个组件有两个执行器,每个执行器承载不同数量的任务。

此外,您可以看到为一个组件定义了两个执行器和八个任务(每个执行器承载四个任务)。如果您对这个配置没有获得足够的性能,您可以轻松地将组件的执行器数量更改为四个或八个,以增加性能,并且任务将在该组件的所有执行器之间均匀分布。以下图表显示了执行器、任务和工作进程之间的关系:

在代码级别配置并行性

Storm 提供了一个 API 来在代码级别设置工作进程的数量、执行器的数量和任务的数量。以下部分展示了我们如何在代码级别配置并行性。

我们可以通过使用org.apache.storm.Config类的setNumWorkers方法在代码级别设置工作进程的数量。以下是代码片段,展示了这些设置的实际应用:

Config conf = new Config(); 
conf.setNumWorkers(3); 

在上一章中,我们将工作进程的数量配置为三。Storm 将为SampleStormTopologySampleStormClusterTopology拓扑分配三个工作进程。

我们可以通过在org.apache.storm.topology.TopologyBuilder类的setSpout(args,args,parallelism_hint)setBolt(args,args,parallelism_hint)方法中传递parallelism_hint参数来在代码级别设置执行器的数量。以下是代码片段,展示了这些设置的实际应用:

builder.setSpout("SampleSpout", new SampleSpout(), 2); 
// set the bolt class 
builder.setBolt("SampleBolt", new SampleBolt(), 4).shuffleGrouping("SampleSpout"); 

在上一章中,我们为SampleSpout设置了parallelism_hint=2,为SampleBolt设置了parallelism_hint=4。在执行时,Storm 将为SampleSpout分配两个执行器,为SampleBolt分配四个执行器。

我们可以配置在执行器内部可以执行的任务数量。以下是展示这些设置的代码片段:

builder.setSpout("SampleSpout", new SampleSpout(), 2).setNumTasks(4); 

在上述代码中,我们已经配置了SampleSpout的两个执行器和四个任务。对于SampleSpout,Storm 将为每个执行器分配两个任务。默认情况下,如果用户在代码级别不设置任务数量,Storm 将为每个执行器运行一个任务。

Worker 进程、执行器和任务分布

假设为拓扑设置的 worker 进程数量为三,SampleSpout的执行器数量为三,SampleBolt的执行器数量为三。此外,SampleBolt的任务数量为六,这意味着每个SampleBolt执行器将有两个任务。以下图表显示了拓扑在运行时的样子:

重新平衡拓扑的并行性

在上一章中已经解释过,Storm 的一个关键特性是它允许我们在运行时修改拓扑的并行性。在运行时更新拓扑并行性的过程称为rebalance

有两种重新平衡拓扑的方式:

  • 使用 Storm Web UI

  • 使用 Storm CLI

在上一章中介绍了 Storm Web UI。本节介绍了如何使用 Storm CLI 工具重新平衡拓扑。以下是我们需要在 Storm CLI 上执行的命令:

> bin/storm rebalance [TopologyName] -n [NumberOfWorkers] -e [Spout]=[NumberOfExecutos] -e [Bolt1]=[NumberOfExecutos] [Bolt2]=[NumberOfExecutos]

rebalance命令将首先在消息超时期间停用拓扑,然后在 Storm 集群中均匀重新分配 worker。几秒钟或几分钟后,拓扑将恢复到之前的激活状态,并重新开始处理输入流。

重新平衡 SampleStormClusterTopology 拓扑的并行性

首先通过在 supervisor 机器上运行jps命令来检查 Storm 集群中运行的 worker 进程的数量:

在 supervisor-1 上运行jps命令:

> jps
24347 worker
23940 supervisor
24593 Jps
24349 worker  

两个 worker 进程分配给 supervisor-1 机器。

现在,在 supervisor-2 上运行jps命令:

> jps
24344 worker
23941 supervisor
24543 Jps

一个 worker 进程分配给 supervisor-2 机器。

Storm 集群上运行着三个 worker 进程。

让我们尝试重新配置SampleStormClusterTopology,使用两个 worker 进程,SampleSpout使用四个执行器,SampleBolt使用四个执行器:

> bin/storm rebalance SampleStormClusterTopology -n 2 -e SampleSpout=4 -e SampleBolt=4

0     [main] INFO  backtype.storm.thrift  - Connecting to Nimbus at nimbus.host.ip:6627
58   [main] INFO  backtype.storm.command.rebalance  - Topology SampleStormClusterTopology is rebalancing

重新运行 supervisor 机器上的jps命令,查看 worker 进程的数量。

在 supervisor-1 上运行jps命令:

> jps
24377 worker
23940 supervisor
24593 Jps 

在 supervisor-2 上运行jps命令:

> jps
24353 worker
23941 supervisor
24543 Jps  

在这种情况下,之前显示了两个 worker 进程。第一个 worker 进程分配给 supervisor-1,另一个分配给 supervisor-2。worker 的分布可能会根据系统上运行的拓扑数量和每个 supervisor 上可用的插槽数量而有所不同。理想情况下,Storm 会尝试在所有节点之间均匀分配负载。

Storm 集群中不同类型的流分组

在定义拓扑时,我们创建了一个计算图,其中包含了多个 bolt 处理流。在更细粒度的层面上,每个 bolt 在拓扑中执行多个任务。因此,特定 bolt 的每个任务只会从订阅的流中获取一部分元组。

Storm 中的流分组提供了对如何在订阅流的许多任务之间对元组进行分区的完全控制。可以在使用org.apache e.storm.topology.TopologyBuilder.setBolt方法定义 bolt 时,通过org.apache.storm.topology.InputDeclarer的实例来定义 bolt 的分组。

Storm 支持以下类型的流分组。

Shuffle 分组

Shuffle 分组以均匀随机的方式在任务之间分发元组。每个任务将处理相等数量的元组。当您希望在任务之间均匀分配处理负载,并且不需要任何数据驱动的分区时,这种分组是理想的。这是 Storm 中最常用的分组之一。

字段分组

此分组使您能够根据元组中的某些字段对流进行分区。例如,如果您希望特定用户的所有推文都发送到一个任务,则可以使用字段分组按用户名对推文流进行分区:

builder.setSpout("1", new TweetSpout()); 
builder.setBolt("2", new TweetCounter()).fieldsGrouping("1", new Fields("username")) 

由于字段分组是hash(字段)%(任务数),它不能保证每个任务都会获得要处理的元组。例如,如果您对字段应用了字段分组,比如X,只有两个可能的值,AB,并为 bolt 创建了两个任务,那么hash(A)%2hash(B)%2可能返回相等的值,这将导致所有元组都被路由到一个任务,另一个任务完全空闲。

字段分组的另一个常见用途是连接流。由于分区仅基于字段值而不是流类型,因此我们可以使用任何公共连接字段连接两个流。字段的名称不需要相同。例如,在订单处理领域,我们可以连接Order流和ItemScanned流以查看何时完成订单:

builder.setSpout("1", new OrderSpout()); 
builder.setSpount("2", new ItemScannedSpout()); 
builder.setBolt("joiner", new OrderJoiner()) 
.fieldsGrouping("1", new Fields("orderId")) 
.fieldsGrouping("2", new Fields("orderRefId")); 

由于流上的连接因应用程序而异,您将自己定义连接的定义,比如在时间窗口上进行连接,可以通过组合字段分组来实现。

所有分组

所有分组是一种特殊的分组,不会对元组进行分区,而是将它们复制到所有任务中,也就是说,每个元组将被发送到 bolt 的每个任务进行处理。

所有分组的一个常见用例是向 bolt 发送信号。例如,如果您对流进行某种过滤,可以通过向所有 bolt 的任务发送这些参数的流来传递或更改过滤参数,并使用所有分组进行订阅。另一个例子是向聚合 bolt 中的所有任务发送重置消息。

全局分组

全局分组不会对流进行分区,而是将完整的流发送到具有最小 ID 的 bolt 任务。这种情况的一般用例是在拓扑中需要减少阶段的情况,其中您希望将拓扑中以前步骤的结果合并到单个 bolt 中。

全局分组乍看起来可能是多余的,因为如果只有一个输入流,您可以通过将 bolt 的并行度定义为 1 来实现相同的结果。但是,当您有多个数据流通过不同路径传入时,您可能希望只有一个流被减少,而其他流被并行处理。

例如,考虑以下拓扑。在这种情况下,您可能希望将来自Bolt C的所有元组组合在一个Bolt D任务中,而您可能仍希望将来自Bolt EBolt D的元组并行处理:

直接分组

在直接分组中,发射器决定每个元组将在哪里进行处理。例如,假设我们有一个日志流,我们希望根据资源类型将每个日志条目处理为特定的 bolt 任务。在这种情况下,我们可以使用直接分组。

直接分组只能与直接流一起使用。要声明一个流为直接流,请使用backtype.storm.topology.OutputFieldsDeclarer.declareStream方法,该方法带有一个boolean参数。一旦有了要发射的直接流,请使用backtype.storm.task.OutputCollector.emitDirect而不是 emit 方法来发射它。emitDirect方法带有一个taskId参数来指定任务。您可以使用backtype.storm.task.TopologyContext.getComponentTasks方法获取组件的任务数。

本地或 shuffle 分组

如果 tuple 源和目标 bolt 任务在同一个 worker 中运行,使用此分组将仅在同一 worker 上运行的目标任务之间起到洗牌分组的作用,从而最大程度地减少任何网络跳数,提高性能。

如果源 worker 进程上没有运行目标 bolt 任务,这种分组将类似于前面提到的 shuffle 分组。

None 分组

当您不关心 tuple 在各个任务之间如何分区时,可以使用 None 分组。从 Storm 0.8 开始,这相当于使用 shuffle 分组。

自定义分组

如果前面的分组都不适合您的用例,您可以通过实现backtype.storm.grouping.CustomStreamGrouping接口来定义自己的自定义分组。

以下是一个基于 tuple 中的类别对流进行分区的示例自定义分组:

public class CategoryGrouping implements CustomStreamGrouping, Serializable { 
  private static final Map<String, Integer> categories = ImmutableMap.of 
  ( 
    "Financial", 0,  
    "Medical", 1,  
    "FMCG", 2,  
    "Electronics", 3 
  ); 

  private int tasks = 0; 

  public void prepare(WorkerTopologyContext context, GlobalStreamId stream, List<Integer> targetTasks)  
  { 
    tasks = targetTasks.size(); 
  } 

  public List<Integer> chooseTasks(int taskId, List<Object> values) { 
    String category = (String) values.get(0); 
    return ImmutableList.of(categories.get(category) % tasks); 
  } 
} 

以下图表以图形方式表示了 Storm 分组:

保证消息处理

在 Storm 拓扑中,spout 发出的单个 tuple 可能会导致拓扑后期生成多个 tuple。例如,考虑以下拓扑:

在这里,Spout A发出一个 tuple T(A),由bolt Bbolt C处理,它们分别发出 tuple T(AB)T(AC)。因此,当作为 tuple T(A)结果产生的所有 tuple--即 tuple 树T(A)T(AB)T(AC)--都被处理时,我们说该 tuple 已完全处理。

当 tuple 树中的一些 tuple 由于某些运行时错误或每个拓扑可配置的超时而未能处理时,Storm 将视其为失败的 tuple。

Storm 需要以下六个步骤来保证消息处理:

  1. 用唯一的消息 ID 标记 spout 发出的每个 tuple。这可以通过使用org.apache.storm.spout.SpoutOutputColletor.emit方法来实现,该方法带有一个messageId参数。Storm 使用此消息 ID 来跟踪由此 tuple 生成的 tuple 树的状态。如果您使用不带messageId参数的 emit 方法之一,Storm 将不会跟踪它以进行完全处理。当消息完全处理时,Storm 将使用发出 tuple 时使用的相同messageId发送确认。

  2. spout 实现的通用模式是,它们从消息队列(例如 RabbitMQ)中读取消息,将 tuple 生成到拓扑中进行进一步处理,然后一旦收到 tuple 已完全处理的确认,就将消息出队。

  3. 当拓扑中的一个 bolt 在处理消息过程中需要生成一个新的 tuple 时,例如前面拓扑中的bolt B,那么它应该发出新的 tuple,并用它从 spout 获取的原始 tuple 进行关联。这可以通过使用org.apache.storm.task.OutputCollector类中带有 anchor tuple 参数的重载 emit 方法来实现。如果您从同一个输入 tuple 发出多个 tuple,则要为每个输出的 tuple 进行关联。

  4. 每当您在 bolt 的 execute 方法中处理完一个 tuple 时,使用org.apache.storm.task.OutputCollector.ack方法发送确认。当确认到达发射的 spout 时,您可以安全地将消息标记为已处理,并从消息队列中出队(如果有的话)。

  5. 同样,如果在处理元组时出现问题,应该使用org.apache.storm.task.OutputCollector.fail方法发送失败信号,以便 Storm 可以重放失败的消息。

  6. 在 Storm bolt 中处理的一般模式之一是在 execute 方法的末尾处理一个元组,发出新的元组,并在 execute 方法的末尾发送确认。Storm 提供了org.apache.storm.topology.base.BasicBasicBolt类,它会在 execute 方法的末尾自动发送确认。如果要发出失败信号,请在 execute 方法中抛出org.apache.storm.topology.FailedException

这种模型导致至少一次消息处理语义,并且你的应用程序应该准备好处理一些消息会被多次处理的情况。Storm 还提供了一次消息处理语义,我们将在第五章 Trident Topology and Uses中讨论。

尽管可以通过这里提到的方法在 Storm 中实现一些消息处理的保证,但你是否真正需要它,这总是一个需要考虑的问题,因为你可以通过冒一些消息不被 Storm 完全处理来获得很大的性能提升。这是在设计应用程序时可以考虑的一个权衡。

Tick 元组

在某些用例中,一个 bolt 需要在执行某些操作之前缓存数据几秒钟,比如在每 5 秒清理缓存或者在单个请求中插入一批记录到数据库中。

tick 元组是系统生成(由 Storm 生成)的元组,我们可以在每个 bolt 级别进行配置。开发人员可以在编写 bolt 时在代码级别配置 tick 元组。

我们需要在 bolt 中重写以下方法以启用 tick 元组:

@Override 
public Map<String, Object> getComponentConfiguration() { 
  Config conf = new Config(); 
  int tickFrequencyInSeconds = 10; 
  conf.put(Config.TOPOLOGY_TICK_TUPLE_FREQ_SECS, 
  tickFrequencyInSeconds); 
  return conf; 
} 

在前面的代码中,我们已经将 tick 元组的时间配置为 10 秒。现在,Storm 将在每 10 秒开始生成一个 tick 元组。

此外,我们需要在 bolt 的 execute 方法中添加以下代码以识别元组的类型:

@Override 
public void execute(Tuple tuple) { 
  if (isTickTuple(tuple)) { 
    // now you can trigger e.g. a periodic activity 
  } 
  else { 
    // do something with the normal tuple 
  } 
} 

private static boolean isTickTuple(Tuple tuple) { 
  return
  tuple.getSourceComponent().equals(Constants.SYSTEM_COMPONENT_ID) && tuple.getSourceStreamId().equals(Constants.SYSTEM_TICK_STREAM_ID); 
} 

如果isTickTuple()方法的输出为 true,则输入元组是一个 tick 元组。否则,它是由前一个 bolt 发出的普通元组。

请注意,tick 元组会像普通元组一样发送到 bolt/spout,这意味着它们将排在 bolt/spout 即将通过其execute()nextTuple()方法处理的其他元组之后。因此,你为 tick 元组配置的时间间隔在实践中是尽力而为的。例如,如果一个 bolt 受到高执行延迟的影响--例如,由于被常规非 tick 元组的传入速率压倒--那么你会观察到在 bolt 中实现的周期性活动会比预期触发得晚。

总结

在本章中,我们已经介绍了如何定义 Storm 的并行性,如何在多个节点之间分发作业,以及如何在多个 bolt 实例之间分发数据。本章还涵盖了两个重要特性:消息处理的保证和 tick 元组。

在下一章中,我们将介绍 Storm 上的 Trident 高级抽象。Trident 主要用于解决实时事务问题,这是无法通过普通的 Storm 解决的。

第四章:Trident 介绍

在前几章中,我们介绍了 Storm 的架构、拓扑、bolt、spout、元组等。在本章中,我们介绍了 Trident,它是 Storm 的高级抽象。

本章涵盖了以下内容:

  • Trident 介绍

  • 理解 Trident 的数据模型

  • 编写 Trident 函数、过滤器和投影

  • Trident 重新分区操作

  • Trident 聚合器

  • 何时使用 Trident

Trident 介绍

Trident 是建立在 Storm 之上的高级抽象。Trident 支持有状态的流处理,而纯 Storm 是一个无状态的处理框架。使用 Trident 的主要优势在于它保证每个进入拓扑的消息只被处理一次,这在纯 Storm 中很难实现。Trident 的概念类似于高级批处理工具,如 Cascading 和 Pig,它们是在 Hadoop 上开发的。为了实现精确一次处理,Trident 会将输入流分批处理。我们将在第五章的Trident 拓扑和用途Trident 状态部分详细介绍。

在前三章中,我们了解到,在 Storm 的拓扑中,spout 是元组的来源。元组是 Storm 应用程序可以处理的数据单元,而 bolt 是我们编写转换逻辑的处理引擎。但在 Trident 拓扑中,bolt 被更高级的函数、聚合、过滤器和状态的语义所取代。

理解 Trident 的数据模型

Trident 元组是 Trident 拓扑的数据模型。Trident 元组是可以被 Trident 拓扑处理的数据的基本单元。每个元组由预定义的字段列表组成。每个字段的值可以是字节、字符、整数、长整型、浮点数、双精度浮点数、布尔值或字节数组。在构建拓扑时,对元组执行操作,这些操作要么向元组添加新字段,要么用一组新字段替换元组。

元组中的每个字段都可以通过名称(getValueByField(String))或其位置索引(getValue(int))来访问。Trident 元组还提供了方便的方法,如getIntegerByField(String),可以避免您对对象进行类型转换。

编写 Trident 函数、过滤器和投影

本节介绍了 Trident 函数、过滤器和投影的定义。Trident 函数、过滤器和投影用于根据特定条件修改/过滤输入元组。本节还介绍了如何编写 Trident 函数、过滤器和投影。

Trident 函数

Trident 函数包含修改原始元组的逻辑。Trident 函数接收元组的一组字段作为输入,并输出一个或多个元组。输出元组的字段与输入元组的字段合并,形成完整的元组,然后传递给拓扑中的下一个操作。如果 Trident 函数没有输出与输入元组对应的元组,则该元组将从流中移除。

我们可以通过扩展storm.trident.operation.BaseFunction类并实现execute(TridentTuple tuple, TridentCollector collector)方法来编写自定义的 Trident 函数。

让我们编写一个示例的 Trident 函数,它将返回一个名为sum的新字段:

public class SumFunction extends BaseFunction { 

  private static final long serialVersionUID = 5L; 

  public void execute(TridentTuple tuple, TridentCollector collector) { 
    int number1 = tuple.getInteger(0); 
    int number2 = tuple.getInteger(1); 
    int sum = number1+number2; 
    // emit the sum of first two fields 
    collector.emit(new Values(sum)); 

  } 

} 

假设我们将dummyStream作为输入,其中包含四个字段abcd,并且只有字段ab作为输入字段传递给SumFunction函数。SumFunction类会发出一个新字段sumSumFunction类的execute方法发出的sum字段与输入元组合并,形成完整的元组。因此,输出元组中的字段总数为5 (a, b, c, d, sum)。以下是一个示例代码片段,展示了如何将输入字段和新字段的名称传递给 Trident 函数:

dummyStream.each(new Fields("a","b"), new SumFunction (), new Fields("sum")) 

以下图显示了输入元组,SumFunction和输出元组。输出元组包含五个字段,abcdsum

Trident 过滤器

Trident 过滤器以一组字段作为输入,并根据某种条件是否满足返回 true 或 false。如果返回 true,则元组保留在输出流中;否则,元组从流中移除。

我们可以通过扩展storm.trident.operation.BaseFilter类并实现isKeep(TridentTuple tuple)方法来编写自定义的 Trident 过滤器。

让我们编写一个示例 Trident 过滤器,检查输入字段的和是偶数还是奇数。如果和是偶数,则 Trident 过滤器发出 true;否则发出 false:

public static class CheckEvenSumFilter extends BaseFilter{ 

  private static final long serialVersionUID = 7L; 

  public boolean isKeep(TridentTuple tuple) { 
    int number1 = tuple.getInteger(0); 
    int number2 = tuple.getInteger(1); 
    int sum = number1+number2; 
    if(sum % 2 == 0) { 
      return true; 
    } 
    return false; 
  } 

} 

假设我们得到了名为dummyStream的输入,其中包含四个字段,abcd,并且只有字段ab作为输入字段传递给CheckEvenSumFilter过滤器。CheckEvenSumFilter类的execute方法将仅发出那些ab的和为偶数的元组。以下是一段示例代码,展示了如何为 Trident 过滤器定义输入字段:

dummyStream.each(new Fields("a","b"), new CheckEvenSumFilter ()) 

以下图显示了输入元组,CheckEvenSumFilter和输出元组。outputStream仅包含那些字段ab的和为偶数的元组:

Trident 投影

Trident 投影仅保留流中在投影操作中指定的字段。假设输入流包含三个字段,xyz,并且我们将字段x传递给投影操作,那么输出元组将包含一个字段x。以下是一段代码,展示了如何使用投影操作:

mystream.project(new Fields("x")) 

以下图显示了 Trident 投影:

Trident 重新分区操作

通过执行重新分区操作,用户可以将元组分布在多个任务中。重新分区操作不会对元组的内容进行任何更改。此外,元组只会通过网络进行重新分区操作。以下是不同类型的重新分区操作。

利用 shuffle 操作

这种重新分区操作以一种均匀随机的方式将元组分布在多个任务中。当我们希望在任务之间均匀分配处理负载时,通常会使用这种重新分区操作。以下图显示了如何使用shuffle操作重新分区输入元组:

以下是一段代码,展示了如何使用shuffle操作:

mystream.shuffle().each(new Fields("a","b"), new myFilter()).parallelismHint(2) 

利用 partitionBy 操作

这种重新分区操作使您能够根据元组中的字段对流进行分区。例如,如果您希望来自特定用户的所有推文都发送到同一个目标分区,则可以通过以下方式对推文流进行分区,即应用partitionByusername字段:

mystream.partitionBy(new Fields("username")).each(new Fields("username","text"), new myFilter()).parallelismHint(2) 

partitionBy操作应用以下公式来决定目标分区:

目标分区 = 哈希(字段) % (目标分区数)

如前面的公式所示,partitionBy操作计算输入字段的哈希以决定目标分区。因此,它不能保证所有任务都会得到元组进行处理。例如,如果您对一个字段应用了partitionBy,比如X,只有两个可能的值,AB,并为MyFilter过滤器创建了两个任务,那么可能会出现哈希(A) % 2 和哈希(B) % 2 相等的情况,这将导致所有元组都被路由到一个任务,而其他元组完全空闲。

以下图显示了如何使用partitionBy操作重新分区输入元组:

如前图所示,Partition 0Partition 2包含一组元组,但Partition 1为空。

利用全局操作

这种重新分配操作将所有元组路由到同一分区。因此,流中所有批次都选择相同的目标分区。以下是一个显示如何使用global操作重新分配元组的图表:

以下是一段代码,显示了如何使用global操作:

mystream.global().each(new Fields("a","b"), new myFilter()).parallelismHint(2) 

利用 broadcast 操作

broadcast操作是一种特殊的重新分配操作,不会对元组进行分区,而是将它们复制到所有分区。以下是一个显示元组如何通过网络发送的图表:

以下是一段代码,显示了如何使用broadcast操作:

mystream.broadcast().each(new Fields("a","b"), new myFilter()).parallelismHint(2) 

利用 batchGlobal 操作

这种重新分配操作将属于同一批次的所有元组发送到同一分区。同一流的其他批次可能会进入不同的分区。正如其名称所示,此重新分配在批次级别是全局的。以下是一个显示如何使用batchGlobal操作重新分配元组的图表:

以下是一段代码,显示了如何使用batchGlobal操作:

mystream.batchGlobal().each(new Fields("a","b"), new myFilter()).parallelismHint(2) 

利用分区操作

如果前面的重新分配都不适合您的用例,您可以通过实现org.apche.storm.grouping.CustomStreamGrouping接口来定义自己的自定义重新分配函数。

以下是一个示例自定义重新分配,根据country字段的值对流进行分区:

public class CountryRepartition implements CustomStreamGrouping, Serializable { 

  private static final long serialVersionUID = 1L; 

  private static final Map<String, Integer> countries = ImmutableMap.of ( 
    "India", 0,  
    "Japan", 1,  
    "United State", 2,  
    "China", 3, 
    "Brazil", 4 
  ); 

  private int tasks = 0; 

  public void prepare(WorkerTopologyContext context, GlobalStreamId stream, List<Integer> targetTasks)  
    { 
      tasks = targetTasks.size(); 
    } 

  public List<Integer> chooseTasks(int taskId, List<Object> values) { 
    String country = (String) values.get(0);    
    return ImmutableList.of(countries.get(country) % tasks); 
  } 
} 

CountryRepartition类实现了org.apache.storm.grouping.CustomStreamGrouping接口。chooseTasks()方法包含重新分配逻辑,用于确定拓扑中输入元组的下一个任务。prepare()方法在开始时被调用,并执行初始化活动。

Trident 聚合器

Trident 聚合器用于对输入批次、分区或输入流执行聚合操作。例如,如果用户想要计算每个批次中元组的数量,则可以使用计数聚合器来计算每个批次中元组的数量。聚合器的输出完全替换输入元组的值。Trident 中有三种可用的聚合器:

  • partitionAggregate

  • aggregate

  • persistenceAggregate

让我们详细了解每种类型的聚合器。

partitionAggregate

正如其名称所示,partitionAggregate在每个分区上工作,而不是整个批次。partitionAggregate的输出完全替换输入元组。此外,partitionAggregate的输出包含一个单字段元组。以下是一段代码,显示了如何使用partitionAggregate

mystream.partitionAggregate(new Fields("x"), new Count() ,new new Fields("count")) 

例如,我们得到一个包含字段xy的输入流,并对每个分区应用partitionAggregate函数;输出元组包含一个名为count的字段。count字段表示输入分区中元组的数量:

aggregate

aggregate在每个批次上工作。在聚合过程中,首先使用全局操作对元组进行重新分配,将同一批次的所有分区合并为单个分区,然后对每个批次运行聚合函数。以下是一段代码,显示了如何使用aggregate

mystream.aggregate(new Fields("x"), new Count() ,new new Fields("count")) 

Trident 中有三种可用的聚合器接口:

  • ReducerAggregator

  • Aggregator

  • CombinerAggregator

这三种聚合器接口也可以与partitionAggregate一起使用。

ReducerAggregator

ReducerAggregator首先对输入流运行全局重新分配操作,将同一批次的所有分区合并为单个分区,然后对每个批次运行聚合函数。ReducerAggregator<T>接口包含以下方法:

  • init(): 此方法返回初始值

  • Reduce(T curr, TridentTuple tuple): 此方法遍历输入元组,并发出一个具有单个值的单个元组

此示例显示了如何使用ReducerAggregator实现Sum

public static class Sum implements ReducerAggregator<Long> { 

  private static final long serialVersionUID = 1L; 
  /** return the initial value zero     
  */ 
  public Long init() { 
    return 0L; 
  } 
  /** Iterates on the input tuples, calculate the sum and   
  * produce the single tuple with single field as output. 
  */ 
  public Long reduce(Long curr, TridentTuple tuple) {                       
    return curr+tuple.getLong(0);              
  } 

} 

聚合器

Aggregator首先在输入流上运行全局重分区操作,将同一批次的所有分区组合成单个分区,然后在每个批次上运行聚合函数。根据定义,AggregatorReduceAggregator非常相似。BaseAggregator<State>包含以下方法:

  • init(Object batchId, TridentCollector collector): 在开始处理批次之前调用init()方法。此方法返回将用于保存批次状态的State对象。此对象由aggregate()complete()方法使用。

  • aggregate (State s, TridentTuple tuple, TridentCollector collector): 此方法迭代给定批次的每个元组。此方法在处理每个元组后更新State对象中的状态。

  • complete(State state, TridentCollector tridentCollector): 如果给定批次的所有元组都已处理完毕,则调用此方法。此方法返回与每个批次对应的单个元组。

以下是一个示例,展示了如何使用BaseAggregator实现求和:

public static class SumAsAggregator extends BaseAggregator<SumAsAggregator.State> { 

  private static final long serialVersionUID = 1L; 
  // state class 
  static class State { 
    long count = 0; 
  } 
  // Initialize the state 
  public State init(Object batchId, TridentCollector collector) { 
    return new State(); 
  } 
  // Maintain the state of sum into count variable.   
  public void aggregate(State state, TridentTuple tridentTuple, TridentCollector tridentCollector) { 
    state.count = tridentTuple.getLong(0) + state.count; 
  } 
  // return a tuple with single value as output  
  // after processing all the tuples of given batch.       
  public void complete(State state, TridentCollector tridentCollector) { 
    tridentCollector.emit(new Values(state.count)); 
  } 

} 

CombinerAggregator

CombinerAggregator首先在每个分区上运行partitionAggregate,然后运行全局重分区操作,将同一批次的所有分区组合成单个分区,然后在最终分区上重新运行aggregator以发出所需的输出。与其他两个聚合器相比,这里的网络传输较少。因此,CombinerAggregator的整体性能优于AggregatorReduceAggregator

CombinerAggregator<T>接口包含以下方法:

  • init(): 此方法在每个输入元组上运行,以从元组中检索字段的值。

  • combine(T val1, T val2): 此方法组合元组的值。此方法发出具有单个字段的单个元组作为输出。

  • zero(): 如果输入分区不包含元组,则此方法返回零。

此示例显示了如何使用CombinerAggregator实现Sum

public class Sum implements CombinerAggregator<Number> { 

  private static final long serialVersionUID = 1L; 

  public Number init(TridentTuple tridentTuple) { 
    return (Number) tridentTuple.getValue(0); 
  } 

  public Number combine(Number number1, Number number2) { 
    return Numbers.add(number1, number2); 
  } 

  public Number zero() { 
    return 0; 
  } 

} 

persistentAggregate

persistentAggregate适用于流中所有批次的所有元组,并将聚合结果持久化到状态源(内存、Memcached、Cassandra 或其他数据库)中。以下是一些代码,展示了如何使用persistentAggregate

mystream.persistentAggregate(new MemoryMapState.Factory(),new Fields("select"),new Count(),new Fields("count")); 

我们将在第五章 Trident Topology and UsesTrident state部分进行更详细的讨论。

聚合器链接

Trident 提供了一种功能,可以将多个聚合器应用于同一输入流,这个过程称为聚合器链接。以下是一段代码,展示了如何使用聚合器链接:

mystream.chainedAgg() 
        .partitionAggregate(new Fields("b"), new Average(), new Fields("average")) 
        .partitionAggregate(new Fields("b"), new Sum(), new Fields("sum")) 
        .chainEnd(); 

我们已将Average()Sum()聚合器应用于每个分区。chainedAgg()的输出包含与每个输入分区对应的单个元组。输出元组包含两个字段,sumaverage

以下图表显示了聚合器链接的工作原理:

利用 groupBy 操作

groupBy操作不涉及任何重分区。groupBy操作将输入流转换为分组流。groupBy操作的主要功能是修改后续聚合函数的行为。以下图表显示了groupBy操作如何对单个分区的元组进行分组:

groupBy的行为取决于其使用的位置。可能有以下行为:

  • 如果在partitionAggregate之前使用groupBy操作,则partitionAggregate将在分区内创建的每个组上运行aggregate

  • 如果在聚合之前使用groupBy操作,同一批次的元组首先被重新分区到一个单一分区,然后groupBy被应用于每个单一分区,最后对每个组执行aggregate操作。

何时使用 Trident

使用 Trident 拓扑非常容易实现一次性处理,并且 Trident 就是为此目的而设计的。使用原始的 Storm 很难实现一次性处理,因此当我们需要一次性处理时,Trident 会很有用。

Trident 并不适用于所有用例,特别是对于高性能的用例,因为 Trident 会给 Storm 增加复杂性并管理状态。

总结

在本章中,我们主要集中讨论了 Trident 作为 Storm 的高级抽象,并学习了 Trident 的过滤器、函数、聚合器和重新分区操作。

在下一章中,我们将涵盖非事务拓扑、Trident 拓扑和使用分布式 RPC 的 Trident 拓扑。

第五章:Trident 拓扑和用途

在上一章中,我们介绍了 Trident 的概述。在本章中,我们将介绍 Trident 拓扑的开发。以下是本章将要涵盖的重点:

  • Trident groupBy操作

  • 非事务拓扑

  • Trident hello world 拓扑

  • Trident 状态

  • 分布式 RPC

  • 何时使用 Trident

Trident groupBy 操作

groupBy操作不涉及任何重分区。groupBy操作将输入流转换为分组流。groupBy操作的主要功能是修改后续聚合函数的行为。

在分区聚合之前进行分组

如果在partitionAggregate之前使用groupBy操作,则partitionAggregate将在分区内创建的每个组上运行aggregate

在聚合之前进行分组

如果在aggregate之前使用groupBy操作,则首先对输入元组进行重分区,然后对每个组执行aggregate操作。

非事务拓扑

在非事务拓扑中,spout 发出一批元组,并不保证每个批次中有什么。通过处理机制,我们可以将管道分为两类:

  • 至多一次处理:在这种类型的拓扑中,失败的元组不会被重试。因此,spout 不会等待确认。

  • 至少一次处理:处理管道中的失败元组将被重试。因此,这种类型的拓扑保证进入处理管道的每个元组至少被处理一次。

我们可以通过实现org.apache.storm.trident.spout.IBatchSpout接口来编写一个非事务 spout。

这个例子展示了如何编写一个 Trident spout:

public class FakeTweetSpout implements IBatchSpout{ 

   private static final long serialVersionUID = 10L; 
   private intbatchSize; 
   private HashMap<Long, List<List<Object>>>batchesMap = new HashMap<Long, List<List<Object>>>(); 
   public FakeTweetSpout(intbatchSize) { 
         this.batchSize = batchSize; 
   } 

   private static final Map<Integer, String> TWEET_MAP = new HashMap<Integer, String>(); 
   static { 
         TWEET_MAP.put(0, "#FIFA worldcup"); 
         TWEET_MAP.put(1, "#FIFA worldcup"); 
         TWEET_MAP.put(2, "#FIFA worldcup"); 
         TWEET_MAP.put(3, "#FIFA worldcup"); 
         TWEET_MAP.put(4, "#Movie top 10"); 
   } 

   private static final Map<Integer, String> COUNTRY_MAP = new HashMap<Integer, String>(); 
   static { 
         COUNTRY_MAP.put(0, "United State"); 
         COUNTRY_MAP.put(1, "Japan"); 
         COUNTRY_MAP.put(2, "India"); 
         COUNTRY_MAP.put(3, "China"); 
         COUNTRY_MAP.put(4, "Brazil"); 
   } 

   private List<Object>recordGenerator() { 
         final Random rand = new Random(); 
         intrandomNumber = rand.nextInt(5); 
         int randomNumber2 = rand.nextInt(5); 
         return new Values(TWEET_MAP.get(randomNumber),COUNTRY_MAP.get(randomNumber2)); 
   } 

   public void ack(long batchId) { 
         this.batchesMap.remove(batchId); 

   } 

   public void close() { 
         // Here we should close all the external connections 

   } 

   public void emitBatch(long batchId, TridentCollector collector) { 
         List<List<Object>> batches = this.batchesMap.get(batchId); 
         if(batches == null) { 
               batches = new ArrayList<List<Object>>();; 
               for (inti=0;i<this.batchSize;i++) { 
                     batches.add(this.recordGenerator()); 
               } 
               this.batchesMap.put(batchId, batches); 
         } 
         for(List<Object>list : batches){ 
collector.emit(list); 
        } 

   } 

   public Map getComponentConfiguration() { 
         // TODO Auto-generated method stub 
         return null; 
   } 

   public Fields getOutputFields() { 
         return new Fields("text","Country"); 
   } 

   public void open(Map arg0, TopologyContext arg1) { 
         // TODO Auto-generated method stub 

   } 

} 

FakeTweetSpout类实现了org.apache.storm.trident.spout.IBatchSpout接口。FakeTweetSpout(intbatchSize)的构造以batchSize作为参数。如果batchSize3,则FakeTweetSpout类发出的每个批次包含三个元组。recordGenerator方法包含生成虚假推文的逻辑。以下是示例虚假推文:

["Adidas #FIFA World Cup Chant Challenge", "Brazil"] 
["The Great Gatsby is such a good movie","India"] 

getOutputFields方法返回两个字段,textCountryemitBatch(long batchId, TridentCollector collector)方法使用batchSize变量来决定每个批次中的元组数量,并将一批发出到处理管道中。

batchesMap集合包含batchId作为键和元组批次作为值。emitBatch(long batchId, TridentCollector collector)发出的所有批次将被添加到batchesMap中。

ack(long batchId)方法接收batchId作为确认,并将从batchesMap中删除相应的批次。

Trident hello world 拓扑

本节解释了如何编写 Trident hello world 拓扑。执行以下步骤创建 Trident hello world 拓扑:

  1. 使用com.stormadvance作为groupIdstorm_trident作为artifactId创建一个 Maven 项目。

  2. 将以下依赖项和存储库添加到pom.xml文件中:

         <dependencies> 
         <dependency> 
               <groupId>junit</groupId> 
               <artifactId>junit</artifactId> 
               <version>3.8.1</version> 
               <scope>test</scope> 
         </dependency> 
         <dependency> 
               <groupId>org.apache.storm</groupId> 
               <artifactId>storm-core</artifactId> 
               <version>1.0.2</version> 
               <scope>provided</scope> 
         </dependency> 
   </dependencies> 
  1. com.stormadvance.storm_trident包中创建一个TridentUtility类。这个类包含我们将在 Trident hello world 示例中使用的 Trident 过滤器和函数:
public class TridentUtility { 
   /** 
    * Get the comma separated value as input, split the field by comma, and 
    * then emits multiple tuple as output. 
    *  
    */ 
   public static class Split extends BaseFunction { 

         private static final long serialVersionUID = 2L; 

         public void execute(TridentTuple tuple, TridentCollector collector) { 
               String countries = tuple.getString(0); 
               for (String word :countries.split(",")) { 
                     // System.out.println("word -"+word); 
                     collector.emit(new Values(word)); 
               } 
         } 
   } 

   /** 
    * This class extends BaseFilter and contain isKeep method which emits only 
    * those tuple which has #FIFA in text field. 
    */ 
   public static class TweetFilter extends BaseFilter { 

         private static final long serialVersionUID = 1L; 

         public booleanisKeep(TridentTuple tuple) { 
               if (tuple.getString(0).contains("#FIFA")) { 
                     return true; 
               } else { 
                     return false; 
               } 
         } 

   } 

   /** 
    * This class extends BaseFilter and contain isKeep method which will print 
    * the input tuple. 
    *  
    */ 
   public static class Print extends BaseFilter { 

         private static final long serialVersionUID = 1L; 

         public booleanisKeep(TridentTuple tuple) { 
               System.out.println(tuple); 
               return true; 
         } 

   } 
} 

TridentUtility类包含三个内部类:SplitTweetFilterPrint

Split类扩展了org.apache.storm.trident.operation.BaseFunction类,并包含execute(TridentTuple tuple, TridentCollector collector)方法。execute()方法以逗号分隔的值作为输入,拆分输入值,并将多个元组作为输出发出。

TweetFilter类扩展了org.apache.storm.trident.operation.BaseFilter类,并包含isKeep(TridentTuple tuple)方法。isKeep()方法以元组作为输入,并检查输入元组的text字段是否包含值#FIFA。如果元组的text字段包含#FIFA,则该方法返回 true。否则,返回 false。

Print类扩展了org.apache.storm.trident.operation.BaseFilter类,并包含isKeep(TridentTuple tuple)方法。isKeep()方法打印输入元组并返回 true。

  1. com.stormadvance.storm_trident包中创建一个TridentHelloWorldTopology类。该类定义了 hello world Trident 拓扑:
public class TridentHelloWorldTopology {   
   public static void main(String[] args) throws Exception { 
         Config conf = new Config(); 
         conf.setMaxSpoutPending(20); 
         if (args.length == 0) { 
               LocalCluster cluster = new LocalCluster(); 
               cluster.submitTopology("Count", conf, buildTopology()); 
         } else { 
               conf.setNumWorkers(3); 
               StormSubmitter.submitTopology(args[0], conf, buildTopology()); 
         } 
   } 

   public static StormTopologybuildTopology() { 

         FakeTweetSpout spout = new FakeTweetSpout(10); 
         TridentTopology topology = new TridentTopology(); 

         topology.newStream("spout1", spout) 
                     .shuffle() 
                     .each(new Fields("text", "Country"), 
                                 new TridentUtility.TweetFilter()) 
                     .groupBy(new Fields("Country")) 
                     .aggregate(new Fields("Country"), new Count(), 
                                 new Fields("count")) 
                     .each(new Fields("count"), new TridentUtility.Print()) 
                     .parallelismHint(2); 

         return topology.build(); 
   } 
} 

让我们逐行理解代码。首先,我们创建了一个TridentTopology类的对象来定义 Trident 计算。

TridentTopology包含一个名为newStream()的方法,该方法将以输入源作为参数。在本例中,我们使用在非事务性拓扑部分创建的FakeTweetSpout作为输入源。与 Storm 一样,Trident 也在 ZooKeeper 中维护每个输入源的状态。在这里,FakeTweetSpout字符串指定了 Trident 在 ZooKeeper 中维护元数据的节点。

喷口发出一个具有两个字段textCountry的流。

我们正在使用shuffle操作重新分区输入源发出的元组批量。拓扑定义的下一行对每个元组应用TweetFilterTweetFilter过滤掉所有不包含#FIFA关键字的元组。

TweetFilter的输出按Country字段分组。然后,我们应用Count聚合器来计算每个国家的推文数量。最后,我们应用Print过滤器来打印aggregate方法的输出。

这是TridentHelloWorldTopology类的控制台输出:

这是显示 hello world Trident 拓扑执行的图表:

Trident 状态

Trident 提供了一个从有状态源读取和写入结果的抽象。我们可以将状态维护在拓扑内部(内存)或者存储在外部源(Memcached 或 Cassandra)中。

让我们考虑一下,我们正在将之前的 hello world Trident 拓扑的输出保存在数据库中。每次处理元组时,元组中的国家计数都会在数据库中增加。我们无法通过仅在数据库中维护计数来实现精确一次的处理。原因是,如果在处理过程中任何元组失败,那么失败的元组将会重试。这给我们带来了一个问题,因为我们不确定这个元组的状态是否已经更新过。如果元组在更新状态之前失败,那么重试元组将会增加数据库中的计数并使状态一致。但如果元组在更新状态之后失败,那么重试相同的元组将再次增加数据库中的计数并使状态不一致。因此,仅通过在数据库中维护计数,我们无法确定这个元组是否已经被处理过。我们需要更多的细节来做出正确的决定。我们需要按照以下步骤来实现精确一次的处理语义:

  1. 以小批量处理元组。

  2. 为每个批次分配一个唯一 ID(事务 ID)。如果批次重试,它将获得相同的唯一 ID。

  3. 批量之间的状态更新是有序的。例如,批量 2 的状态更新在批量 1 的状态更新完成之前是不可能的。

如果我们使用上述三种语义创建一个拓扑,那么我们可以轻松地判断元组是否已经被处理过。

分布式 RPC

分布式 RPC 用于即时查询和检索 Trident 拓扑的结果。Storm 有一个内置的分布式 RPC 服务器。分布式 RPC 服务器接收来自客户端的 RPC 请求,并将其传递给 Storm 拓扑。拓扑处理请求并将结果发送到分布式 RPC 服务器,然后由分布式 RPC 服务器重定向到客户端。

我们可以通过在storm.yaml文件中使用以下属性来配置分布式 RPC 服务器:

drpc.servers: 
     - "nimbus-node" 

在这里,nimbus-node是分布式 RPC 服务器的 IP。

现在,在nimbus-node机器上运行以下命令以启动分布式 RPC 服务器:

> bin/storm drpc 

假设我们正在将 hello world Trident 拓扑的计数聚合存储在数据库中,并且想要即时检索给定国家的计数。我们需要使用分布式 RPC 功能来实现这一点。这个例子展示了如何在前一节创建的 hello world Trident 拓扑中整合分布式 RPC:

我们正在创建一个包含buildTopology()方法的DistributedRPC类:

public class DistributedRPC { 

  public static void main(String[] args) throws Exception { 
    Config conf = new Config(); 
    conf.setMaxSpoutPending(20); 
    LocalDRPCdrpc = new LocalDRPC(); 
    if (args.length == 0) { 

      LocalCluster cluster = new LocalCluster(); 
      cluster.submitTopology("CountryCount", conf, buildTopology(drpc)); 
      Thread.sleep(2000); 
      for(inti=0; i<100 ; i++) { 
        System.out.println("Result - "+drpc.execute("Count", "Japan India Europe")); 
        Thread.sleep(1000); 
      } 
    } else { 
      conf.setNumWorkers(3); 
      StormSubmitter.submitTopology(args[0], conf, buildTopology(null)); 
      Thread.sleep(2000); 
      DRPCClient client = new DRPCClient(conf, "RRPC-Server", 1234); 
      System.out.println(client.execute("Count", "Japan India Europe")); 
    } 
  } 

  public static StormTopologybuildTopology(LocalDRPCdrpc) { 

    FakeTweetSpout spout = new FakeTweetSpout(10); 
    TridentTopology topology = new TridentTopology(); 
    TridentStatecountryCount = topology.newStream("spout1", spout) 
                     .shuffle() 
                     .each(new Fields("text","Country"), new TridentUtility.TweetFilter()).groupBy(new Fields("Country")) 
                     .persistentAggregate(new MemoryMapState.Factory(),new Fields("Country"), new Count(), new Fields("count")) 
                     .parallelismHint(2); 

    try { 
      Thread.sleep(2000); 
    } catch (InterruptedException e) { 
    } 

    topology.newDRPCStream("Count", drpc) 
         .each(new Fields("args"), new TridentUtility.Split(), new Fields("Country"))                        
         .stateQuery(countryCount, new Fields("Country"), new MapGet(), 
                     new Fields("count")).each(new Fields("count"), 
                             new FilterNull()); 

    return topology.build(); 
  } 
} 

让我们逐行理解这段代码。

我们使用FakeTweetSpout作为输入源,并使用TridentTopology类来定义 Trident 计算。

在下一行中,我们使用persistentAggregate函数来表示所有批次的计数聚合。MemoryMapState.Factory()用于维护计数状态。persistentAggregate函数知道如何在源状态中存储和更新聚合:

persistentAggregate(new MemoryMapState.Factory(),new Fields("Country"), new Count(), new Fields("count")) 

内存数据库将国家名称存储为键,聚合计数存储为值,如下所示:

India 124 
United State 145 
Japan 130 
Brazil 155 
China 100 

persistentAggregate将流转换为 Trident State对象。在这种情况下,Trident State对象表示迄今为止每个国家的计数。

拓扑的下一部分定义了一个分布式查询,以即时获取每个国家的计数。分布式 RPC 查询以逗号分隔的国家列表作为输入,并返回每个国家的计数。以下是定义分布式查询部分的代码片段:

topology.newDRPCStream("Count", drpc) 
         .each(new Fields("args"), new TridentUtility.Split(), new Fields("Country"))                        
         .stateQuery(countryCount, new Fields("Country"), new MapGet(), 
                     new Fields("count")).each(new Fields("count"), 
                             new FilterNull()); 

Split函数用于拆分逗号分隔的国家列表。我们使用了stateQuery()方法来查询拓扑的第一部分中定义的 Trident State对象。stateQuery()接受状态源(在本例中是拓扑的第一部分计算出的国家计数)和用于查询此函数的函数。我们使用了MapGet()函数,用于获取每个国家的计数。最后,每个国家的计数作为查询输出返回。

以下是一段代码,展示了我们如何将输入传递给本地分布式 RPC:

System.out.println(drpc.execute("Count", "Japan,India,Europe")); 

我们已经创建了一个backtype.storm.LocalDRPC的实例来模拟分布式 RPC。

如果正在运行分布式 RPC 服务器,则需要创建分布式 RPC 客户端的实例来执行查询。以下是展示如何将输入传递给分布式 RPC 服务器的代码片段:

DRPCClient client = new DRPCClient(conf,"RRPC-Server", 1234); 
System.out.println(client.execute("Count", "Japan,India,Europe")); 

Trident 分布式 RPC 查询的执行方式类似于普通的 RPC 查询,只是这些查询是并行运行的。

以下是DistributedRPC类的控制台输出:

何时使用 Trident

使用 Trident 拓扑非常容易实现精确一次处理,Trident 也是为此而设计的。另一方面,在普通的 Storm 中实现精确一次处理会比较困难。因此,Trident 将对需要精确一次处理的用例非常有用。

Trident 并不适用于所有用例,特别是高性能用例,因为 Trident 会增加 Storm 的复杂性并管理状态。

摘要

在本章中,我们主要集中在 Trident 示例拓扑、Trident groupBy操作和非事务性拓扑上。我们还介绍了如何使用分布式 RPC 即时查询 Trident 拓扑。

在下一章中,我们将介绍不同类型的 Storm 调度程序。

第六章:Storm 调度程序

在前几章中,我们介绍了 Storm 的基础知识,Storm 的安装,Storm 的开发和部署,以及 Storm 集群中的 Trident 拓扑。在本章中,我们将专注于 Storm 调度程序。

在本章中,我们将涵盖以下要点:

  • Storm 调度程序介绍

  • 默认调度程序

  • 隔离调度程序

  • 资源感知调度程序

  • 客户感知调度程序

Storm 调度程序介绍

如前两章所述,Nimbus 负责部署拓扑,监督者负责执行 Storm 拓扑的 spouts 和 bolts 组件中定义的计算任务。正如我们所展示的,我们可以根据调度程序策略为每个监督者节点配置分配给拓扑的工作插槽数量,以及为拓扑分配的工作节点数量。简而言之,Storm 调度程序帮助 Nimbus 决定任何给定拓扑的工作分配。

默认调度程序

Storm 默认调度程序在给定拓扑分配的所有工作节点(监督者插槽)之间尽可能均匀地分配组件执行器。

让我们考虑一个包含一个 spout 和一个 bolt 的示例拓扑,两个组件都有两个执行器。如果我们通过分配两个工作节点(监督者插槽)提交了拓扑,下图显示了执行器的分配:

如前图所示,每个工作节点包含一个 spout 的执行器和一个 bolt 的执行器。只有当每个组件中的执行器数量可以被分配给拓扑的工作节点数量整除时,才能在工作节点之间均匀分配执行器。

隔离调度程序

隔离调度程序提供了一种在许多拓扑之间轻松安全地共享 Storm 集群资源的机制。隔离调度程序有助于在 Storm 集群中为拓扑分配/保留专用的 Storm 节点集。

我们需要在 Nimbus 配置文件中定义以下属性以切换到隔离调度程序:

storm.scheduler: org.apache.storm.scheduler.IsolationScheduler 

我们可以通过在isolation.scheduler.machines属性中指定拓扑名称和节点数量来为任何拓扑分配/保留资源,如下一节所述。我们需要在 Nimbus 配置中定义isolation.scheduler.machines属性,因为 Nimbus 负责在 Storm 节点之间分配拓扑工作节点:

isolation.scheduler.machines:  
  "Topology-Test1": 2 
  "Topology-Test2": 1 
  "Topology-Test3": 4 

在上述配置中,Topology-Test1分配了两个节点,Topology-Test2分配了一个节点,Topology-Test3分配了四个节点。

以下是隔离调度程序的关键要点:

  • 隔离列表中提到的拓扑优先于非隔离拓扑,这意味着如果与非隔离拓扑竞争,资源将首先分配给隔离拓扑

  • 在运行时没有办法更改拓扑的隔离设置。

  • 隔离调度程序通过为拓扑分配专用机器来解决多租户问题

资源感知调度程序

资源感知调度程序帮助用户指定单个组件实例(spout 或 bolt)所需的资源量。我们可以通过在storm.yaml文件中指定以下属性来启用资源感知调度程序:

storm.scheduler: "org.apache.storm.scheduler.resource.ResourceAwareScheduler" 

组件级配置

您可以为任何组件分配内存需求。以下是可用于为任何组件的单个实例分配内存的方法:

public T setMemoryLoad(Number onHeap, Number offHeap) 

或者,您可以使用以下方法:

public T setMemoryLoad(Number onHeap) 

以下是每个参数的定义:

  • onHeap:此组件实例将消耗的堆内存空间量(以兆字节为单位)

  • offHeap:此组件实例将消耗的堆外内存空间量(以兆字节为单位)

onHeapoffHeap的数据类型均为Number,默认值为0.0

内存使用示例

让我们考虑一个具有两个组件(一个 spout 和一个 bolt)的拓扑:

SpoutDeclarer spout1 = builder.setSpout("spout1", new spoutComponent(), 4); 
spout1.setMemoryLoad(1024.0, 512.0); 
builder.setBolt("bolt1", new boltComponent(), 5).setMemoryLoad(512.0); 

spout1组件的单个实例的内存请求为 1.5 GB(堆上 1 GB,堆外 0.5 GB),这意味着spout1组件的总内存请求为 4 x 1.5 GB = 6 GB。

bolt1组件的单个实例的内存请求为 0.5 GB(堆上 0.5 GB,堆外 0.0 GB),这意味着bolt1组件的总内存请求为 5 x 0.5 GB = 2.5 GB。计算两个组件所需的总内存的方法可以总结如下:

拓扑分配的总内存= spout1 + bolt1 = 6 + 2.5 = 8.5 GB

您还可以将 CPU 需求分配给任何组件。

以下是为任何给定组件的单个实例分配 CPU 资源量所需的方法:

public T setCPULoad(Double amount) 

amount是任何给定组件实例将消耗的 CPU 资源量。 CPU 使用是一个难以定义的概念。不同的 CPU 架构根据手头的任务而表现不同。按照惯例,CPU 核心通常有 100 个点。如果您觉得您的处理器更强大或更弱,可以相应地进行调整。CPU 密集型的重型任务将获得 100 分,因为它们可以占用整个核心。中等任务应该获得 50 分,轻型任务 25 分,微小任务 10 分。

CPU 使用示例

让我们考虑一个具有两个组件(一个 spout 和一个 bolt)的拓扑:

SpoutDeclarer spout1 = builder.setSpout("spout1", new spoutComponent(), 4); 
spout1.setCPULoad(15.0); 
builder.setBolt("bolt1", new boltComponent(), 5).setCPULoad(450.0); 

工作节点级配置

您可以为每个工作节点/插槽分配堆大小。以下是定义每个工作节点的堆大小所需的方法:

public void setTopologyWorkerMaxHeapSize(Number size) 

在这里,size是以兆字节为单位的单个工作节点可用的堆空间量。

这是一个例子:

Config conf = new Config(); 
conf.setTopologyWorkerMaxHeapSize(1024.0); 

节点级配置

我们可以通过在storm.yaml文件中设置以下属性来配置 Storm 节点可以使用的内存和 CPU 量。我们需要在每个 Storm 节点上设置以下属性:

supervisor.memory.capacity.mb: [amount<Double>] 
supervisor.cpu.capacity: [amount<Double>] 

这是一个例子:

supervisor.memory.capacity.mb: 10480.0 
supervisor.cpu.capacity: 100.0 

在这里,100表示整个核心,如前面讨论的。

全局组件配置

如前一节所述,我们可以通过定义拓扑来为每个组件定义内存和 CPU 需求。用户还可以在storm.yaml文件中设置组件的默认资源使用情况。如果我们在代码中定义组件配置,那么代码值将覆盖默认值:

//default value if on heap memory requirement is not specified for a component  
topology.component.resources.onheap.memory.mb: 128.0 

//default value if off heap memory requirement is not specified for a component  
topology.component.resources.offheap.memory.mb: 0.0 

//default value if CPU requirement is not specified for a component  
topology.component.cpu.pcore.percent: 10.0 

//default value for the max heap size for a worker   
topology.worker.max.heap.size.mb: 768.0 

自定义调度程序

在 Storm 中,Nimbus 使用调度程序将任务分配给监督者。默认调度程序旨在将计算资源均匀分配给拓扑。在拓扑之间公平性方面表现良好,但用户无法预测 Storm 集群中拓扑组件的放置,即拓扑的哪个组件需要分配给哪个监督者节点。

让我们考虑一个例子。假设我们有一个具有一个 spout 和两个 bolts 的拓扑,每个组件都有一个执行器和一个任务。如果我们将拓扑提交到 Storm 集群,则以下图表显示了拓扑的分布。假设分配给拓扑的工作节点数量为三,Storm 集群中的监督者数量为三:

假设我们的拓扑中的最后一个 bolt Bolt2 需要使用 GPU 而不是 CPU 来处理一些数据,并且只有一个监督者具有 GPU。我们需要编写自己的自定义调度程序来实现将任何组件分配给特定监督者节点的任务。以下是我们需要执行的步骤:

  1. 配置监督者节点中的更改。

  2. 在组件级别配置设置。

  3. 编写自定义调度程序类。

  4. 注册自定义调度程序类。

配置监督者节点中的更改

Storm 在监督节点的配置中为用户提供了一个字段,用于指定自定义调度元数据。在这种情况下,我们在监督节点中输入/tag和它们运行的类型,这是通过在它们的$STORM_HOME/conf/storm.yaml文件中的一行配置完成的。例如,每个监督节点的配置应该包含以下内容:

supervisor.scheduler.meta: 
  type: GPU 

在对每个监督节点添加配置更改后,我们需要重新启动监督节点。对于所有非 GPU 机器,您需要使用 CPU 类型。

组件级别的配置设置

这一步是在拓扑结构中使用TopologyBuilder的主方法中完成的。ComponentConfigurationDeclarer有一个叫做addConfiguration(String config, String value)的方法,允许添加自定义配置,也就是元数据。在我们的情况下,我们使用这个方法添加类型信息:

TopologyBuilder builder = new TopologyBuilder(); 
builder.setSpout("spout", new SampleSpout(), 1); builder.setBolt("bolt1", new ExampleBolt1(), 1).shuffleGrouping("spout"); 
builder.setBolt("bolt3", new SampleBolt2(), 1).shuffleGrouping("bolt2").addConfiguration("type", "GPU"); 

前面的代码显示我们已经用typeGPUbolt2组件进行了类型化。

编写自定义监督类

我们可以通过实现org.apache.storm.scheduler.IScheduler接口来编写我们的CustomScheduler类。这个接口包含两个重要的方法:

  • prepare(Map conf):这个方法只是初始化调度程序。

  • schedule(Topologies topologies, Cluster cluster):这个方法包含负责在集群监督节点插槽中进行拓扑工作的逻辑。

CustomScheduler包含以下私有方法,负责将工作程序分配给集群监督节点的插槽。

getSupervisorsByType()方法返回映射。映射的键表示节点类型(例如,CPU 或 GPU),值包含该类型监督节点的列表:

    private Map<String, ArrayList<SupervisorDetails>> getSupervisorsByType( 
            Collection<SupervisorDetails> supervisorDetails 
    ) { 
        // A map of type -> supervisors, to help with scheduling of components with specific types 
        Map<String, ArrayList<SupervisorDetails>> supervisorsByType = new HashMap<String, ArrayList<SupervisorDetails>>(); 

        for (SupervisorDetails supervisor : supervisorDetails) { 
            @SuppressWarnings("unchecked") 
            Map<String, String> metadata = (Map<String, String>) supervisor.getSchedulerMeta(); 

            String types; 

            if (metadata == null) { 
                types = unType; 
            } else { 
                types = metadata.get("types"); 

                if (types == null) { 
                    types = unType; 
                } 
            }
            // If the supervisor has types attached to it, handle it by populating the supervisorsByType map. 
            // Loop through each of the types to handle individually 
            for (String type : types.split(",")) { 
                type = type.trim(); 

                if (supervisorsByType.containsKey(type)) { 
                    // If we've already seen this type, then just add the supervisor to the existing ArrayList. 
                    supervisorsByType.get(type).add(supervisor); 
                } else { 
                    // If this type is new, then create a new ArrayList<SupervisorDetails>, 
                    // add the current supervisor, and populate the map's type entry with it. 
                    ArrayList<SupervisorDetails> newSupervisorList = new ArrayList<SupervisorDetails>(); 
                    newSupervisorList.add(supervisor); 
                    supervisorsByType.put(type, newSupervisorList); 
                } 
            } 
        } 

        return supervisorsByType; 
    } 

populateComponentsByType()方法也返回映射。映射的键表示类型(CPU 或 GPU),值包含需要分配给该类型监督节点的拓扑组件的列表。我们在这里使用一个无类型的类型来将没有类型的组件分组。这样做的目的是有效地处理这些无类型的组件,就像默认调度程序执行分配一样。这意味着没有类型组件的拓扑将以相同的方式成功调度,跨无类型的监督节点没有问题:

    private <T> void populateComponentsByType( 
            Map<String, ArrayList<String>> componentsByType, 
            Map<String, T> components 
    ) { 
        // Type T can be either Bolt or SpoutSpec, so that this logic can be reused for both component types 
        JSONParser parser = new JSONParser(); 

        for (Entry<String, T> componentEntry : components.entrySet()) { 
            JSONObject conf = null; 

            String componentID = componentEntry.getKey(); 
            T component = componentEntry.getValue(); 

            try { 
                // Get the component's conf irrespective of its type (via java reflection) 
                Method getCommonComponentMethod = component.getClass().getMethod("get_common"); 
                ComponentCommon commonComponent = (ComponentCommon) getCommonComponentMethod.invoke(component); 
                conf = (JSONObject) parser.parse(commonComponent.get_json_conf()); 
            } catch (Exception ex) { 
                ex.printStackTrace(); 
            } 

            String types; 

            // If there's no config, use a fake type to group all untypeged components 
            if (conf == null) { 
                types = unType; 
            } else { 
                types = (String) conf.get("types"); 

                // If there are no types, use a fake type to group all untypeged components 
                if (types == null) { 
                    types = unType; 
                } 
            } 

            // If the component has types attached to it, handle it by populating the componentsByType map. 
            // Loop through each of the types to handle individually 
            for (String type : types.split(",")) { 
                type = type.trim(); 

                if (componentsByType.containsKey(type)) { 
                    // If we've already seen this type, then just add the component to the existing ArrayList. 
                    componentsByType.get(type).add(componentID); 
                } else { 
                    // If this type is new, then create a new ArrayList, 
                    // add the current component, and populate the map's type entry with it. 
                    ArrayList<String> newComponentList = new ArrayList<String>(); 
                    newComponentList.add(componentID); 
                    componentsByType.put(type, newComponentList); 
                } 
            } 
        } 
    } 

populateComponentsByTypeWithStormInternals()方法返回 Storm 启动的内部组件的详细信息。

    private void populateComponentsByTypeWithStormInternals( 
            Map<String, ArrayList<String>> componentsByType, 
            Set<String> components 
    ) { 
        // Storm uses some internal components, like __acker. 
        // These components are topology-agnostic and are therefore not accessible through a StormTopology object. 
        // While a bit hacky, this is a way to make sure that we schedule those components along with our topology ones: 
        // we treat these internal components as regular untypeged components and add them to the componentsByType map. 

        for (String componentID : components) { 
            if (componentID.startsWith("__")) { 
                if (componentsByType.containsKey(unType)) { 
                    // If we've already seen untypeged components, then just add the component to the existing ArrayList. 
                    componentsByType.get(unType).add(componentID); 
                } else { 
                    // If this is the first untypeged component we see, then create a new ArrayList, 
                    // add the current component, and populate the map's untypeged entry with it. 
                    ArrayList<String> newComponentList = new ArrayList<String>(); 
                    newComponentList.add(componentID); 
                    componentsByType.put(unType, newComponentList); 
                } 
            } 
        } 
    } 

前三种方法管理监督和组件的映射。现在,我们将编写typeAwareScheduler()方法,它将使用这两个映射:

    private void typeAwareSchedule(Topologies topologies, Cluster cluster) { 
        Collection<SupervisorDetails> supervisorDetails = cluster.getSupervisors().values(); 

        // Get the lists of typed and unreserved supervisors. 
        Map<String, ArrayList<SupervisorDetails>> supervisorsByType = getSupervisorsByType(supervisorDetails); 

        for (TopologyDetails topologyDetails : cluster.needsSchedulingTopologies(topologies)) { 
            StormTopology stormTopology = topologyDetails.getTopology(); 
            String topologyID = topologyDetails.getId(); 

            // Get components from topology 
            Map<String, Bolt> bolts = stormTopology.get_bolts(); 
            Map<String, SpoutSpec> spouts = stormTopology.get_spouts(); 

            // Get a map of component to executors 
            Map<String, List<ExecutorDetails>> executorsByComponent = cluster.getNeedsSchedulingComponentToExecutors( 
                    topologyDetails 
            ); 

            // Get a map of type to components 
            Map<String, ArrayList<String>> componentsByType = new HashMap<String, ArrayList<String>>(); 
            populateComponentsByType(componentsByType, bolts); 
            populateComponentsByType(componentsByType, spouts); 
            populateComponentsByTypeWithStormInternals(componentsByType, executorsByComponent.keySet()); 

            // Get a map of type to executors 
            Map<String, ArrayList<ExecutorDetails>> executorsToBeScheduledByType = getExecutorsToBeScheduledByType( 
                    cluster, topologyDetails, componentsByType 
            ); 

            // Initialise a map of slot -> executors 
            Map<WorkerSlot, ArrayList<ExecutorDetails>> componentExecutorsToSlotsMap = ( 
                    new HashMap<WorkerSlot, ArrayList<ExecutorDetails>>() 
            ); 

            // Time to match everything up! 
            for (Entry<String, ArrayList<ExecutorDetails>> entry : executorsToBeScheduledByType.entrySet()) { 
                String type = entry.getKey(); 

                ArrayList<ExecutorDetails> executorsForType = entry.getValue(); 
                ArrayList<SupervisorDetails> supervisorsForType = supervisorsByType.get(type); 
                ArrayList<String> componentsForType = componentsByType.get(type); 

                try { 
                    populateComponentExecutorsToSlotsMap( 
                            componentExecutorsToSlotsMap, 
                            cluster, topologyDetails, supervisorsForType, executorsForType, componentsForType, type 
                    ); 
                } catch (Exception e) { 
                    e.printStackTrace(); 

                    // Cut this scheduling short to avoid partial scheduling. 
                    return; 
                } 
            } 

            // Do the actual assigning 
            // We do this as a separate step to only perform any assigning if there have been no issues so far. 
            // That's aimed at avoiding partial scheduling from occurring, with some components already scheduled 
            // and alive, while others cannot be scheduled. 
            for (Entry<WorkerSlot, ArrayList<ExecutorDetails>> entry : componentExecutorsToSlotsMap.entrySet()) { 
                WorkerSlot slotToAssign = entry.getKey(); 
                ArrayList<ExecutorDetails> executorsToAssign = entry.getValue(); 

                cluster.assign(slotToAssign, topologyID, executorsToAssign); 
            } 

            // If we've reached this far, then scheduling must have been successful 
            cluster.setStatus(topologyID, "SCHEDULING SUCCESSFUL"); 
        } 
    } 

除了前面提到的四种方法,我们还使用了更多的方法来执行以下操作。

将组件 ID 转换为执行程序

现在让我们从组件 ID 跳转到实际的执行程序,因为这是 Storm 集群处理分配的级别。

这个过程非常简单:

  • 从集群获取按组件的执行程序的映射

  • 根据集群检查哪些组件的执行程序需要调度

  • 创建类型到执行程序的映射,只填充等待调度的执行程序:

private Set<ExecutorDetails> getAllAliveExecutors(Cluster cluster, TopologyDetails topologyDetails) { 
        // Get the existing assignment of the current topology as it's live in the cluster 
        SchedulerAssignment existingAssignment = cluster.getAssignmentById(topologyDetails.getId()); 

        // Return alive executors, if any, otherwise an empty set 
        if (existingAssignment != null) { 
            return existingAssignment.getExecutors(); 
        } else { 
            return new HashSet<ExecutorDetails>(); 
        } 
    } 

    private Map<String, ArrayList<ExecutorDetails>> getExecutorsToBeScheduledByType( 
            Cluster cluster, 
            TopologyDetails topologyDetails, 
            Map<String, ArrayList<String>> componentsPerType 
    ) { 
        // Initialise the return value 
        Map<String, ArrayList<ExecutorDetails>> executorsByType = new HashMap<String, ArrayList<ExecutorDetails>>(); 

        // Find which topology executors are already assigned 
        Set<ExecutorDetails> aliveExecutors = getAllAliveExecutors(cluster, topologyDetails); 

        // Get a map of component to executors for the topology that need scheduling 
        Map<String, List<ExecutorDetails>> executorsByComponent = cluster.getNeedsSchedulingComponentToExecutors( 
                topologyDetails 
        ); 

        // Loop through componentsPerType to populate the map 
        for (Entry<String, ArrayList<String>> entry : componentsPerType.entrySet()) { 
            String type = entry.getKey(); 
            ArrayList<String> componentIDs = entry.getValue(); 

            // Initialise the map entry for the current type 
            ArrayList<ExecutorDetails> executorsForType = new ArrayList<ExecutorDetails>(); 

            // Loop through this type's component IDs 
            for (String componentID : componentIDs) { 
                // Fetch the executors for the current component ID 
                List<ExecutorDetails> executorsForComponent = executorsByComponent.get(componentID); 

                if (executorsForComponent == null) { 
                    continue; 
                } 

                // Convert the list of executors to a set 
                Set<ExecutorDetails> executorsToAssignForComponent = new HashSet<ExecutorDetails>( 
                        executorsForComponent 
                ); 

                // Remove already assigned executors from the set of executors to assign, if any 
                executorsToAssignForComponent.removeAll(aliveExecutors); 

                // Add the component's waiting to be assigned executors to the current type executors 
                executorsForType.addAll(executorsToAssignForComponent); 
            } 

            // Populate the map of executors by type after looping through all of the type's components, 
            // if there are any executors to be scheduled 
            if (!executorsForType.isEmpty()) { 
                executorsByType.put(type, executorsForType); 
            } 
        } 

        return executorsByType; 
} 

将监督转换为插槽

现在是我们必须执行的最终转换:从监督到插槽的跳转。与组件及其执行程序一样,我们需要这个,因为集群在插槽级别分配执行程序,而不是监督级别。

在这一点上有一些事情要做;我们已经将这个过程分解成更小的方法来保持可读性。我们需要执行的主要步骤如下:

找出我们可以分配的插槽,给定一个类型的监督节点列表。这只是使用一个 for 循环收集所有监督节点的插槽,然后返回拓扑所请求的插槽数量。

将等待调度的类型的执行程序分成均匀的组。

用条目填充插槽到执行程序的映射。

这里的想法是每种类型调用populateComponentExecutorsToSlotsMap方法一次,这将导致一个包含我们需要执行的所有分配的单个映射。

如代码注释中所解释的,我们先前发现有时我们会急切地将类型的执行者分配给一个插槽,只是为了让后续的类型无法分配其执行者,导致部分调度。我们已经确保调度流程确保不会执行部分调度(要么全部被调度,要么全部不被调度),尽管这会增加一个额外的循环,但我们认为这是拓扑结构的更清洁状态:

    private void handleFailedScheduling( 
            Cluster cluster, 
            TopologyDetails topologyDetails, 
            String message 
    ) throws Exception { 
        // This is the prefix of the message displayed on Storm's UI for any unsuccessful scheduling 
        String unsuccessfulSchedulingMessage = "SCHEDULING FAILED: "; 

        cluster.setStatus(topologyDetails.getId(), unsuccessfulSchedulingMessage + message); 
        throw new Exception(message); 
    } 

    private Set<WorkerSlot> getAllAliveSlots(Cluster cluster, TopologyDetails topologyDetails) { 
        // Get the existing assignment of the current topology as it's live in the cluster 
        SchedulerAssignment existingAssignment = cluster.getAssignmentById(topologyDetails.getId()); 

        // Return alive slots, if any, otherwise an empty set 
        if (existingAssignment != null) { 
            return existingAssignment.getSlots(); 
        } else { 
            return new HashSet<WorkerSlot>(); 
        } 
    } 

    private List<WorkerSlot> getAllSlotsToAssign( 
            Cluster cluster, 
            TopologyDetails topologyDetails, 
            List<SupervisorDetails> supervisors, 
            List<String> componentsForType, 
            String type 
    ) throws Exception { 
        String topologyID = topologyDetails.getId(); 

        // Collect the available slots of each of the supervisors we were given in a list 
        List<WorkerSlot> availableSlots = new ArrayList<WorkerSlot>(); 
        for (SupervisorDetails supervisor : supervisors) { 
            availableSlots.addAll(cluster.getAvailableSlots(supervisor)); 
        } 

        if (availableSlots.isEmpty()) { 
            // This is bad, we have supervisors and executors to assign, but no available slots! 
            String message = String.format( 
                    "No slots are available for assigning executors for type %s (components: %s)", 
                    type, componentsForType 
            ); 
            handleFailedScheduling(cluster, topologyDetails, message); 
        } 

        Set<WorkerSlot> aliveSlots = getAllAliveSlots(cluster, topologyDetails); 

        int numAvailableSlots = availableSlots.size(); 
        int numSlotsNeeded = topologyDetails.getNumWorkers() - aliveSlots.size(); 

        // We want to check that we have enough available slots 
        // based on the topology's number of workers and already assigned slots. 
        if (numAvailableSlots < numSlotsNeeded) { 
            // This is bad, we don't have enough slots to assign to! 
            String message = String.format( 
                    "Not enough slots available for assigning executors for type %s (components: %s). " 
                            + "Need %s slots to schedule but found only %s", 
                    type, componentsForType, numSlotsNeeded, numAvailableSlots 
            ); 
            handleFailedScheduling(cluster, topologyDetails, message); 
        } 

        // Now we can use only as many slots as are required. 
        return availableSlots.subList(0, numSlotsNeeded); 
    } 

    private Map<WorkerSlot, ArrayList<ExecutorDetails>> getAllExecutorsBySlot( 
            List<WorkerSlot> slots, 
            List<ExecutorDetails> executors 
    ) { 
        Map<WorkerSlot, ArrayList<ExecutorDetails>> assignments = new HashMap<WorkerSlot, ArrayList<ExecutorDetails>>(); 

        int numberOfSlots = slots.size(); 

        // We want to split the executors as evenly as possible, across each slot available, 
        // so we assign each executor to a slot via round robin 
        for (int i = 0; i < executors.size(); i++) { 
            WorkerSlot slotToAssign = slots.get(i % numberOfSlots); 
            ExecutorDetails executorToAssign = executors.get(i); 

            if (assignments.containsKey(slotToAssign)) { 
                // If we've already seen this slot, then just add the executor to the existing ArrayList. 
                assignments.get(slotToAssign).add(executorToAssign); 
            } else { 
                // If this slot is new, then create a new ArrayList, 
                // add the current executor, and populate the map's slot entry with it. 
                ArrayList<ExecutorDetails> newExecutorList = new ArrayList<ExecutorDetails>(); 
                newExecutorList.add(executorToAssign); 
                assignments.put(slotToAssign, newExecutorList); 
            } 
        } 

        return assignments; 
    } 

    private void populateComponentExecutorsToSlotsMap( 
            Map<WorkerSlot, ArrayList<ExecutorDetails>> componentExecutorsToSlotsMap, 
            Cluster cluster, 
            TopologyDetails topologyDetails, 
            List<SupervisorDetails> supervisors, 
            List<ExecutorDetails> executors, 
            List<String> componentsForType, 
            String type 
    ) throws Exception { 
        String topologyID = topologyDetails.getId(); 

        if (supervisors == null) { 
            // This is bad, we don't have any supervisors but have executors to assign! 
            String message = String.format( 
                    "No supervisors given for executors %s of topology %s and type %s (components: %s)", 
                    executors, topologyID, type, componentsForType 
            ); 
            handleFailedScheduling(cluster, topologyDetails, message); 
        } 

        List<WorkerSlot> slotsToAssign = getAllSlotsToAssign( 
                cluster, topologyDetails, supervisors, componentsForType, type 
        ); 

        // Divide the executors evenly across the slots and get a map of slot to executors 
        Map<WorkerSlot, ArrayList<ExecutorDetails>> executorsBySlot = getAllExecutorsBySlot( 
                slotsToAssign, executors 
        ); 

        for (Entry<WorkerSlot, ArrayList<ExecutorDetails>> entry : executorsBySlot.entrySet()) { 
            WorkerSlot slotToAssign = entry.getKey(); 
            ArrayList<ExecutorDetails> executorsToAssign = entry.getValue(); 

            // Assign the topology's executors to slots in the cluster's supervisors 
            componentExecutorsToSlotsMap.put(slotToAssign, executorsToAssign); 
        } 
    } 

注册一个 CustomScheduler 类

我们需要为CustomScheduler类创建一个 JAR,并将其放在$STORM_HOME/lib/中,并通过将以下行附加到$STORM_HOME/conf/storm.yaml配置文件中告诉 Nimbus 使用新的调度程序:

storm.scheduler: "com.stormadvance.storm_kafka_topology.CustomScheduler" 

重新启动 Nimbus 守护程序以反映对配置的更改。

现在,如果我们部署与上一个图中显示的相同的拓扑结构,那么执行者的分布将如下所示(Bolt2分配给了一个 GPU 类型的监督者):

摘要

在本章中,我们了解了内置的 Storm 调度程序,还介绍了如何编写和配置自定义调度程序。

在下一章中,我们将介绍使用 Graphite 和 Ganglia 监视 Storm 集群。

第七章:监控 Storm 集群

在之前的章节中,我们学习了如何在远程 Storm 集群上部署拓扑,如何配置拓扑的并行性,不同类型的流分组等。在本章中,我们将专注于如何监视和收集运行在 Storm 集群上的拓扑的统计信息。

在本章中,我们将涵盖以下主题:

  • 通过 Nimbus thrift 端口收集 Storm 指标

  • 将 Storm 与 Ganglia 集成

  • 安装 Graphite

使用 Nimbus thrift 客户端收集集群统计信息

本节涵盖了如何使用 Nimbus thrift 客户端收集集群详细信息(类似于 Storm UI 页面上显示的详细信息)。通过 Nimbus thrift 客户端提取/收集信息可以让我们可视化数据。

Nimbus thrift API 非常丰富,可以公开监视 Storm 集群所需的所有必要信息。

使用 Nimbus thrift 获取信息

在本节中,我们将使用 Nimbus thrift 客户端创建一个 Java 项目,该项目将包含执行以下操作的类:

  • 收集 Nimbus 配置

  • 收集监督者统计信息

  • 收集拓扑统计信息

  • 收集给定拓扑的喷口统计信息

  • 收集给定拓扑的螺栓统计信息

  • 终止给定的拓扑

以下是使用 Nimbus thrift 客户端获取集群详细信息的步骤:

  1. 使用com.stormadvance作为groupIdstormmonitoring作为artifactId创建一个 Maven 项目。

  2. 将以下依赖项添加到pom.xml文件中:

<dependency> 
  <groupId>org.apache.storm</groupId> 
  <artifactId>storm-core</artifactId> 
  <version>1.0.2</version> 
  <scope>provided</scope> 
</dependency> 

  1. com.stormadvance包中创建一个名为ThriftClient的实用类。ThriftClient类包含逻辑,用于与 Nimbus thrift 服务器建立连接并返回 Nimbus 客户端:
public class ThriftClient { 
  // IP of the Storm UI node 
  private static final String STORM_UI_NODE = "127.0.0.1"; 
  public Client getClient() { 
    // Set the IP and port of thrift server. 
    // By default, the thrift server start on port 6627 
    TSocket socket = new TSocket(STORM_UI_NODE, 6627); 
    TFramedTransport tFramedTransport = new TFramedTransport(socket); 
    TBinaryProtocol tBinaryProtocol = new TBinaryProtocol(tFramedTransport); 
    Client client = new Client(tBinaryProtocol); 
    try { 
      // Open the connection with thrift client. 
      tFramedTransport.open(); 
    }catch(Exception exception) { 
      throw new RuntimeException("Error occurs while making connection with Nimbus thrift server"); 
    } 
    // return the Nimbus Thrift client. 
    return client;           
  } 
} 
  1. 让我们在com.stormadvance包中创建一个名为NimbusConfiguration的类。该类包含使用 Nimbus 客户端收集 Nimbus 配置的逻辑:
public class NimbusConfiguration { 

  public void printNimbusStats() { 
    try { 
      ThriftClient thriftClient = new ThriftClient(); 
      Client client = thriftClient.getClient(); 
      String nimbusConiguration = client.getNimbusConf(); 
      System.out.println("*************************************"); 
      System.out.println("Nimbus Configuration : "+nimbusConiguration); 
      System.out.println("*************************************"); 
    }catch(Exception exception) { 
      throw new RuntimeException("Error occure while fetching the Nimbus statistics : "); 
    } 
  }

  public static void main(String[] args) { 
    new NimbusConfiguration().printNimbusStats(); 
  }      
}

上述代码使用org.apache.storm.generated.Nimbus.Client类的getNimbusConf()方法来获取 Nimbus 配置。

  1. com.stormadvance包中创建一个名为SupervisorStatistics的类,以收集 Storm 集群中所有监督者节点的信息:
public class SupervisorStatistics { 

  public void printSupervisorStatistics()  { 
    try { 
      ThriftClient thriftClient = new ThriftClient(); 
      Client client = thriftClient.getClient(); 
      // Get the cluster information. 
      ClusterSummary clusterSummary = client.getClusterInfo(); 
      // Get the SupervisorSummary iterator 
      Iterator<SupervisorSummary> supervisorsIterator = clusterSummary.get_supervisors_iterator(); 

      while (supervisorsIterator.hasNext()) { 
        // Print the information of supervisor node 
        SupervisorSummary supervisorSummary = (SupervisorSummary) supervisorsIterator.next();

        System.out.println("*************************************"); 
        System.out.println("Supervisor Host IP : "+supervisorSummary.get_host()); 
        System.out.println("Number of used workers : "+supervisorSummary.get_num_used_workers()); 
        System.out.println("Number of workers : "+supervisorSummary.get_num_workers()); 
        System.out.println("Supervisor ID : "+supervisorSummary.get_supervisor_id()); 
        System.out.println("Supervisor uptime in seconds : "+supervisorSummary.get_uptime_secs());

        System.out.println("*************************************"); 
      } 

    }catch (Exception e) { 
      throw new RuntimeException("Error occure while getting cluster info : "); 
    } 
  } 

} 

SupervisorStatistics类使用org.apache.storm.generated.Nimbus.Client类的getClusterInfo()方法来收集集群摘要,然后调用org.apache.storm.generated.ClusterSummary类的get_supervisors_iterator()方法来获取org.apache.storm.generated.SupervisorSummary类的迭代器。

请参阅SupervisorStatistics类的输出。

  1. com.stormadvance包中创建一个名为TopologyStatistics的类,以收集 Storm 集群中所有运行拓扑的信息:
public class TopologyStatistics { 

  public void printTopologyStatistics() { 
    try { 
      ThriftClient thriftClient = new ThriftClient(); 
      // Get the thrift client 
      Client client = thriftClient.getClient(); 
      // Get the cluster info 
      ClusterSummary clusterSummary = client.getClusterInfo(); 
      // Get the iterator over TopologySummary class 
      Iterator<TopologySummary> topologiesIterator = clusterSummary.get_topologies_iterator(); 
      while (topologiesIterator.hasNext()) { 
        TopologySummary topologySummary = topologiesIterator.next();

        System.out.println("*************************************"); 
        System.out.println("ID of topology: " + topologySummary.get_id()); 
        System.out.println("Name of topology: " + topologySummary.get_name()); 
        System.out.println("Number of Executors: " + topologySummary.get_num_executors()); 
        System.out.println("Number of Tasks: " + topologySummary.get_num_tasks()); 
        System.out.println("Number of Workers: " + topologySummary.get_num_workers()); 
        System.out.println("Status of toplogy: " + topologySummary.get_status()); 
        System.out.println("Topology uptime in seconds: " + topologySummary.get_uptime_secs());

        System.out.println("*************************************"); 
      } 
    }catch (Exception exception) { 
      throw new RuntimeException("Error occure while fetching the topolgies  information"); 
    } 
  }      
} 

TopologyStatistics类使用org.apache.storm.generated.ClusterSummary类的get_topologies_iterator()方法来获取org.apache.storm.generated.TopologySummary类的迭代器。TopologyStatistics类将打印每个拓扑分配的执行器数量、任务数量和工作进程数量的值。

  1. com.stormadvance包中创建一个名为SpoutStatistics的类,以获取喷口的统计信息。SpoutStatistics类包含一个名为printSpoutStatistics(String topologyId)的方法,用于打印给定拓扑提供的所有喷口的详细信息:
public class SpoutStatistics { 

  private static final String DEFAULT = "default"; 
  private static final String ALL_TIME = ":all-time"; 

  public void printSpoutStatistics(String topologyId) { 
    try { 
      ThriftClient thriftClient = new ThriftClient(); 
      // Get the nimbus thrift client 
      Client client = thriftClient.getClient(); 
      // Get the information of given topology  
      TopologyInfo topologyInfo = client.getTopologyInfo(topologyId);          
      Iterator<ExecutorSummary> executorSummaryIterator = topologyInfo.get_executors_iterator(); 
      while (executorSummaryIterator.hasNext()) { 
        ExecutorSummary executorSummary = executorSummaryIterator.next(); 
        ExecutorStats executorStats = executorSummary.get_stats(); 
        if(executorStats !=null) { 
          ExecutorSpecificStats executorSpecificStats = executorStats.get_specific(); 
          String componentId = executorSummary.get_component_id(); 
          //  
          if (executorSpecificStats.is_set_spout()) { 
            SpoutStats spoutStats = executorSpecificStats.get_spout();

             System.out.println("*************************************"); 
            System.out.println("Component ID of Spout:- " + componentId); 
            System.out.println("Transferred:- " + getAllTimeStat(executorStats.get_transferred(),ALL_TIME)); 
            System.out.println("Total tuples emitted:- " + getAllTimeStat(executorStats.get_emitted(), ALL_TIME)); 
            System.out.println("Acked: " + getAllTimeStat(spoutStats.get_acked(), ALL_TIME)); 
            System.out.println("Failed: " + getAllTimeStat(spoutStats.get_failed(), ALL_TIME));
             System.out.println("*************************************"); 
          } 
        } 
      } 
    }catch (Exception exception) { 
      throw new RuntimeException("Error occure while fetching the spout information : "+exception); 
    } 
  } 

  private static Long getAllTimeStat(Map<String, Map<String, Long>> map, String statName) { 
    if (map != null) { 
      Long statValue = null; 
      Map<String, Long> tempMap = map.get(statName); 
      statValue = tempMap.get(DEFAULT); 
      return statValue; 
    } 
    return 0L; 
  } 

  public static void main(String[] args) { 
    new SpoutStatistics().printSpoutStatistics("StormClusterTopology-1-1393847956"); 
  } 
}      

上述类使用org.apache.storm.generated.Nimbus.Client类的getTopologyInfo(topologyId)方法来获取给定拓扑的信息。SpoutStatistics类打印喷口的以下统计信息:

    • 喷口 ID
  • 发射的元组数量

  • 失败的元组数量

  • 确认的元组数量

  1. com.stormadvance包中创建一个BoltStatistics类,以获取螺栓的统计信息。BoltStatistics类包含一个printBoltStatistics(String topologyId)方法,用于打印给定拓扑提供的所有螺栓的信息:
public class BoltStatistics { 

  private static final String DEFAULT = "default"; 
  private static final String ALL_TIME = ":all-time"; 

  public void printBoltStatistics(String topologyId) { 

    try { 
      ThriftClient thriftClient = new ThriftClient(); 
      // Get the Nimbus thrift server client 
      Client client = thriftClient.getClient(); 

      // Get the information of given topology 
      TopologyInfo topologyInfo = client.getTopologyInfo(topologyId); 
      Iterator<ExecutorSummary> executorSummaryIterator = topologyInfo.get_executors_iterator(); 
      while (executorSummaryIterator.hasNext()) { 
        // get the executor 
        ExecutorSummary executorSummary = executorSummaryIterator.next(); 
        ExecutorStats executorStats = executorSummary.get_stats(); 
        if (executorStats != null) { 
          ExecutorSpecificStats executorSpecificStats = executorStats.get_specific(); 
          String componentId = executorSummary.get_component_id(); 
          if (executorSpecificStats.is_set_bolt()) { 
            BoltStats boltStats = executorSpecificStats.get_bolt();

            System.out.println("*************************************"); 
            System.out.println("Component ID of Bolt " + componentId); 
            System.out.println("Transferred: " + getAllTimeStat(executorStats.get_transferred(), ALL_TIME)); 
            System.out.println("Emitted: " + getAllTimeStat(executorStats.get_emitted(), ALL_TIME)); 
            System.out.println("Acked: " + getBoltStats(boltStats.get_acked(), ALL_TIME)); 
            System.out.println("Failed: " + getBoltStats(boltStats.get_failed(), ALL_TIME)); 
            System.out.println("Executed : " + getBoltStats(boltStats.get_executed(), ALL_TIME));
            System.out.println("*************************************"); 
          } 
        } 
      } 
    } catch (Exception exception) { 
      throw new RuntimeException("Error occure while fetching the bolt information :"+exception); 
    } 
  } 

  private static Long getAllTimeStat(Map<String, Map<String, Long>> map, String statName) { 
    if (map != null) { 
      Long statValue = null; 
      Map<String, Long> tempMap = map.get(statName); 
      statValue = tempMap.get(DEFAULT); 
      return statValue; 
    } 
    return 0L; 
  } 

  public static Long getBoltStats(Map<String, Map<GlobalStreamId, Long>> map, String statName) { 
    if (map != null) { 
      Long statValue = null; 
      Map<GlobalStreamId, Long> tempMap = map.get(statName); 
      Set<GlobalStreamId> key = tempMap.keySet(); 
      if (key.size() > 0) { 
        Iterator<GlobalStreamId> iterator = key.iterator(); 
        statValue = tempMap.get(iterator.next()); 
      } 
      return statValue; 
    } 
    return 0L; 
  }

  public static void main(String[] args) { new BoltStatistics().printBoltStatistics("StormClusterTopology-1-1393847956"); 
}  

前面的类使用backtype.storm.generated.Nimbus.Client类的getTopologyInfo(topologyId)方法来获取给定拓扑的信息。BoltStatistics类打印了以下螺栓的统计信息:

    • 螺栓 ID
  • 发射的元组数量

  • 元组失败的数量

  • 确认的元组数量

  1. com.stormadvance包中创建一个killTopology类,并按照以下所述定义一个kill方法:
public void kill(String topologyId) { 
  try { 
    ThriftClient thriftClient = new ThriftClient(); 
    // Get the Nimbus thrift client 
    Client client = thriftClient.getClient(); 
    // kill the given topology 
    client.killTopology(topologyId); 

  }catch (Exception exception) { 
    throw new RuntimeException("Error occure while fetching the spout information : "+exception); 
  } 
} 

public static void main(String[] args) { 
  new killTopology().kill("topologyId"); 
} 

前面的类使用org.apache.storm.generated.Nimbus.Client类的killTopology(topologyId)方法来终止拓扑。

在本节中,我们介绍了使用 Nimbus thrift 客户端收集 Storm 集群指标/详情的几种方法。

使用 JMX 监控 Storm 集群

本节将解释如何使用Java 管理扩展JMX)监控 Storm 集群。 JMX 是一组用于管理和监控在 JVM 中运行的应用程序的规范。我们可以在 JMX 控制台上收集或显示 Storm 指标,例如堆大小、非堆大小、线程数、加载的类数、堆和非堆内存、虚拟机参数和托管对象。以下是我们使用 JMX 监控 Storm 集群需要执行的步骤:

  1. 我们需要在每个监督者节点的storm.yaml文件中添加以下行以在每个监督者节点上启用 JMX:
supervisor.childopts: -verbose:gc -XX:+PrintGCTimeStamps - XX:+PrintGCDetails -Dcom.sun.management.jmxremote - Dcom.sun.management.jmxremote.ssl=false - Dcom.sun.management.jmxremote.authenticate=false - Dcom.sun.management.jmxremote.port=12346   

这里,12346是通过 JMX 收集监督者 JVM 指标的端口号。

  1. 在 Nimbus 机器的storm.yaml文件中添加以下行以在 Nimbus 节点上启用 JMX:
nimbus.childopts: -verbose:gc -XX:+PrintGCTimeStamps - XX:+PrintGCDetails -Dcom.sun.management.jmxremote - Dcom.sun.management.jmxremote.ssl=false - Dcom.sun.management.jmxremote.authenticate=false - Dcom.sun.management.jmxremote.port=12345

这里,12345是通过 JMX 收集 Nimbus JVM 指标的端口号。

  1. 此外,您可以通过在每个监督者节点的storm.yaml文件中添加以下行来收集工作进程的 JVM 指标:
worker.childopts: -verbose:gc -XX:+PrintGCTimeStamps - XX:+PrintGCDetails -Dcom.sun.management.jmxremote - Dcom.sun.management.jmxremote.ssl=false - Dcom.sun.management.jmxremote.authenticate=false - Dcom.sun.management.jmxremote.port=2%ID%   

这里,%ID%表示工作进程的端口号。如果工作进程的端口是6700,则其 JVM 指标将发布在端口号267002%ID%)上。

  1. 现在,在安装了 Java 的任何机器上运行以下命令以启动 JConsole:
cd $JAVA_HOME ./bin/jconsole

以下截图显示了我们如何使用 JConsole 连接到监督者 JMX 端口:

如果您在监督者机器之外的机器上打开 JMX 控制台,则需要在上述截图中使用监督者机器的 IP 地址,而不是127.0.0.1

现在,单击“连接”按钮以查看监督者节点的指标。以下截图显示了 JMX 控制台上 Storm 监督者节点的指标:

同样,您可以通过在 JMX 控制台上指定 Nimbus 机器的 IP 地址和 JMX 端口来收集 Nimbus 节点的 JVM 指标。

以下部分将解释如何在 Ganglia 上显示 Storm 集群指标。

使用 Ganglia 监控 Storm 集群

Ganglia 是一个监控工具,用于收集集群上运行的不同类型进程的指标。在大多数应用程序中,Ganglia 被用作集中监控工具,用于显示集群上运行的所有进程的指标。因此,通过 Ganglia 启用 Storm 集群的监控至关重要。

Ganglia 有三个重要组件:

  • Gmond:这是 Ganglia 的监控守护程序,用于收集节点的指标并将此信息发送到 Gmetad 服务器。要收集每个 Storm 节点的指标,您需要在每个节点上安装 Gmond 守护程序。

  • Gmetad:这从所有 Gmond 节点收集指标并将它们存储在循环数据库中。

  • Ganglia Web 界面:以图形形式显示指标信息。

Storm 没有内置支持使用 Ganglia 监视 Storm 集群。但是,使用 JMXTrans,您可以启用使用 Ganglia 监视 Storm。JMXTrans 工具允许您连接到任何 JVM,并在不编写一行代码的情况下获取其 JVM 指标。通过 JMX 公开的 JVM 指标可以使用 JMXTrans 在 Ganglia 上显示。因此,JMXTrans 充当了 Storm 和 Ganglia 之间的桥梁。

以下图表显示了 JMXTrans 在 Storm 节点和 Ganglia 之间的使用方式:

执行以下步骤设置 JMXTrans 和 Ganglia:

  1. 运行以下命令在每个 Storm 节点上下载并安装 JMXTrans 工具:
wget https://jmxtrans.googlecode.com/files/jmxtrans-239-0.noarch. rpm sudo rpm -i jmxtrans-239-0.noarch.rpm
  1. 运行以下命令在网络中的任何机器上安装 Ganglia Gmond 和 Gmetad 包。您可以在不属于 Storm 集群的机器上部署 Gmetad 和 Gmond 进程:
sudo yum -q -y install rrdtool sudo yum -q -y install ganglia-gmond sudo yum -q -y install ganglia-gmetad sudo yum -q -y install ganglia-web
  1. 编辑gmetad.conf配置文件中的以下行,该文件位于 Gmetad 进程的/etc/ganglia中。我们正在编辑此文件以指定数据源的名称和 Ganglia Gmetad 机器的 IP 地址:
data_source "stormcluster" 127.0.0.1

您可以将127.0.0.1替换为 Ganglia Gmetad 机器的 IP 地址。

  1. 编辑gmond.conf配置文件中的以下行,该文件位于 Gmond 进程的/etc/ganglia中:
cluster { 
  name = "stormcluster" 
  owner = "clusterOwner" 
  latlong = "unspecified" 
  url = "unspecified" 
  }
  host { 
    location = "unspecified" 
  }
  udp_send_channel { 
    host = 127.0.0.1 
    port = 8649 
    ttl = 1 
  }
  udp_recv_channel { 
    port = 8649 
  }

这里,127.0.0.1是 Storm 节点的 IP 地址。您需要将127.0.0.1替换为实际机器的 IP 地址。我们主要编辑了 Gmond 配置文件中的以下条目:

    • 集群名称
  • udp_send通道中的主 Gmond 节点的主机地址

  • udp_recv通道中的端口

  1. 编辑ganglia.conf文件中的以下行,该文件位于/etc/httpd/conf.d。我们正在编辑ganglia.conf文件以启用从所有机器访问 Ganglia UI:
Alias /ganglia /usr/share/ganglia <Location /ganglia>Allow from all</Location>

ganglia.conf文件可以在安装 Ganglia web 前端应用程序的节点上找到。在我们的情况下,Ganglia web 界面和 Gmetad 服务器安装在同一台机器上。

  1. 运行以下命令启动 Ganglia Gmond、Gmetad 和 web UI 进程:
sudo service gmond start setsebool -P httpd_can_network_connect 1 sudo service gmetad start sudo service httpd stop sudo service httpd start
  1. 现在,转到http://127.0.0.1/ganglia验证 Ganglia 的安装,并将127.0.0.1替换为 Ganglia web 界面机器的 IP 地址。

  2. 现在,您需要在每个监督者节点上编写一个supervisor.json文件,以使用 JMXTrans 收集 Storm 监督者节点的 JVM 指标,然后使用com.googlecode.jmxtrans.model.output.GangliaWriter OutputWriters类将其发布在 Ganglia 上。com.googlecode.jmxtrans.model.output.GangliaWriter OutputWriters类用于处理输入的 JVM 指标并将其转换为 Ganglia 使用的格式。以下是supervisor.json JSON 文件的内容:

{ 
  "servers" : [ { 
    "port" : "12346", 
    "host" : "IP_OF_SUPERVISOR_MACHINE", 
    "queries" : [ { 
      "outputWriters": [{ 
        "@class": 
        "com.googlecode.jmxtrans.model.output.GangliaWriter", "settings": { 
          "groupName": "supervisor", 
          "host": "IP_OF_GANGLIA_GMOND_SERVER", 
          "port": "8649" } 
      }], 
      "obj": "java.lang:type=Memory", 
      "resultAlias": "supervisor", 
      "attr": ["ObjectPendingFinalizationCount"] 
    }, 
    { 
      "outputWriters": [{ 
        "@class": 
        "com.googlecode.jmxtrans.model.output.GangliaWriter", "settings" { 
          "groupName": " supervisor ", 
          "host": "IP_OF_GANGLIA_GMOND_SERVER", 
          "port": "8649" 
        } 
      }], 
      "obj": "java.lang:name=Copy,type=GarbageCollector", 
      "resultAlias": " supervisor ", 
      "attr": [ 
        "CollectionCount", 
        "CollectionTime"  
      ] 
    }, 
    { 
      "outputWriters": [{ 
        "@class": 
        "com.googlecode.jmxtrans.model.output.GangliaWriter", "settings": { 
          "groupName": "supervisor ", 
          "host": "IP_OF_GANGLIA_GMOND_SERVER", 
          "port": "8649" 
        } 
      }], 
      "obj": "java.lang:name=Code Cache,type=MemoryPool", 
      "resultAlias": "supervisor ", 
      "attr": [ 
        "CollectionUsageThreshold", 
        "CollectionUsageThresholdCount", 
        "UsageThreshold", 
        "UsageThresholdCount" 
      ] 
    }, 
    { 
      "outputWriters": [{ 
        "@class": 
        "com.googlecode.jmxtrans.model.output.GangliaWriter", "settings": { 
          "groupName": "supervisor ", 
          "host": "IP_OF_GANGLIA_GMOND_SERVER", 
          "port": "8649" 
        } 
      }], 
      "obj": "java.lang:type=Runtime", 
      "resultAlias": "supervisor", 
      "attr": [ 
        "StartTime", 
        "Uptime" 
      ] 
    }
    ], 
    "numQueryThreads" : 2 
  }] 
} 

这里,12346storm.yaml文件中指定的监督者的 JMX 端口。

您需要将IP_OF_SUPERVISOR_MACHINE的值替换为监督机器的 IP 地址。如果集群中有两个监督者,那么节点 1 的supervisor.json文件包含节点 1 的 IP 地址,节点 2 的supervisor.json文件包含节点 2 的 IP 地址。

您需要将IP_OF_GANGLIA_GMOND_SERVER的值替换为 Ganglia Gmond 服务器的 IP 地址。

  1. 在 Nimbus 节点上创建nimbus.json文件。使用 JMXTrans,收集 Storm Nimbus 进程的 JVM 指标,并使用com.googlecode.jmxtrans.model.output.GangliaWriter OutputWriters类将其发布在 Ganglia 上。以下是nimbus.json文件的内容:
{ 
  "servers" : [{ 
    "port" : "12345", 
    "host" : "IP_OF_NIMBUS_MACHINE", 
    "queries" : [ 
      { "outputWriters": [{ 
        "@class": 
        "com.googlecode.jmxtrans.model.output.GangliaWriter", 
        "settings": { 
          "groupName": "nimbus", 
          "host": "IP_OF_GANGLIA_GMOND_SERVER", 
          "port": "8649" 
        } 
      }], 
      "obj": "java.lang:type=Memory", 
      "resultAlias": "nimbus", 
      "attr": ["ObjectPendingFinalizationCount"] 
      }, 
      { 
        "outputWriters": [{ 
          "@class": 
          "com.googlecode.jmxtrans.model.output.GangliaWriter", "settings": { 
            "groupName": "nimbus", 
            "host": "IP_OF_GANGLIA_GMOND_SERVER", 
            "port": "8649" 
          } 
        }], 
        "obj": "java.lang:name=Copy,type=GarbageCollector", 
        "resultAlias": "nimbus", 
        "attr": [ 
          "CollectionCount", 
          "CollectionTime" 
        ] 
      }, 
      { 
        "outputWriters": [{ 
          "@class": 
          "com.googlecode.jmxtrans.model.output.GangliaWriter", 
          "settings": { 
            "groupName": "nimbus", 
            "host": "IP_OF_GANGLIA_GMOND_SERVER", 
            "port": "8649" 
          } 
        }], 
        "obj": "java.lang:name=Code Cache,type=MemoryPool", 
        "resultAlias": "nimbus", 
        "attr": [ 
          "CollectionUsageThreshold", 
          "CollectionUsageThresholdCount", 
          "UsageThreshold", 
          "UsageThresholdCount" 
        ] 
      }, 
      { 
        "outputWriters": [{ 
          "@class": 
          "com.googlecode.jmxtrans.model.output.GangliaWriter", "settings": {    
           "groupName": "nimbus", 
            "host": "IP_OF_GANGLIA_GMOND_SERVER", 
            "port": "8649" 
          } 
        }], 
        "obj": "java.lang:type=Runtime",
        "resultAlias": "nimbus", 
        "attr": [ 
          "StartTime", 
          "Uptime" 
        ] 
      }
    ] 
    "numQueryThreads" : 2 
  } ] 
} 

这里,12345storm.yaml文件中指定的 Nimbus 机器的 JMX 端口。

您需要将IP_OF_NIMBUS_MACHINE的值替换为 Nimbus 机器的 IP 地址。

您需要将IP_OF_GANGLIA_GMOND_SERVER的值替换为 Ganglia Gmond 服务器的 IP 地址。

  1. 在每个 Storm 节点上运行以下命令以启动 JMXTrans 进程:
cd /usr/share/jmxtrans/ sudo ./jmxtrans.sh start PATH_OF_JSON_FILES

这里,PATH_OF_JSON_FILEsupervisor.jsonnimbus.json文件的位置。

  1. 现在,转到http://127.0.0.1/ganglia上的 Ganglia 页面,查看 Storm 指标。以下截图显示了 Storm 指标的样子:

执行以下步骤来查看 Ganglia UI 上的 Storm Nimbus 和 supervisor 进程的指标:

  1. 打开 Ganglia 页面。

  2. 现在点击stormcluster链接,查看 Storm 集群的指标。

以下截图显示了 Storm supervisor 节点的指标:

以下截图显示了 Storm Nimbus 节点的指标:

总结

在本章中,我们通过 Nimbus thrift 客户端监控了 Storm 集群,类似于我们通过 Storm UI 所做的。我们还介绍了如何配置 Storm 来发布 JMX 指标以及 Storm 与 Ganglia 的集成。

在下一章中,我们将介绍 Storm 与 Kafka 的集成,并查看一些示例来说明这个过程。

第八章:Storm 和 Kafka 的集成

Apache Kafka 是一个高吞吐量、分布式、容错和复制的消息系统,最初在 LinkedIn 开发。Kafka 的用例从日志聚合到流处理再到替代其他消息系统都有。

Kafka 已经成为实时处理流水线中与 Storm 组合使用的重要组件之一。Kafka 可以作为需要由 Storm 处理的消息的缓冲区或者提供者。Kafka 也可以作为 Storm 拓扑发出的结果的输出接收端。

在本章中,我们将涵盖以下主题:

  • Kafka 架构——broker、producer 和 consumer

  • Kafka 集群的安装

  • 在 Kafka 之间共享 producer 和 consumer

  • 使用 Kafka consumer 作为 Storm spout 开发 Storm 拓扑

  • Kafka 和 Storm 集成拓扑的部署

Kafka 简介

本节中,我们将介绍 Kafka 的架构——broker、consumer 和 producer。

Kafka 架构

Kafka 具有与其他消息系统显著不同的架构。Kafka 是一个点对点系统(集群中的每个节点具有相同的角色),每个节点称为broker。broker 通过 ZooKeeper 集合协调它们的操作。ZooKeeper 集合管理的 Kafka 元数据在在 Storm 和 Kafka 之间共享 ZooKeeper部分中提到。

图 8.1:Kafka 集群

以下是 Kafka 的重要组件:

Producer

生产者是使用 Kafka 客户端 API 将消息发布到 Kafka 集群的实体。在 Kafka broker 中,消息由生产者实体发布到名为topics的实体。主题是一个持久队列(存储在主题中的数据被持久化到磁盘)。

为了并行处理,Kafka 主题可以有多个分区。每个分区的数据都以不同的文件表示。同一个主题的两个分区可以分配到不同的 broker 上,从而增加吞吐量,因为所有分区都是相互独立的。每个分区中的消息都有一个与之关联的唯一序列号,称为offset

图 8.2:Kafka 主题分布

复制

Kafka 支持主题分区的复制以支持容错。Kafka 自动处理分区的复制,并确保分区的副本将分配给不同的 broker。Kafka 选举一个 broker 作为分区的 leader,并且所有写入和读取都必须到分区 leader。复制功能是在 Kafka 8.0.0 版本中引入的。

Kafka 集群通过 ZooKeeper 管理in sync replica(ISR)的列表——与分区 leader 同步的副本。如果分区 leader 宕机,那么在 ISR 列表中存在的跟随者/副本才有资格成为失败分区的下一个 leader。

Consumer

消费者从 broker 中读取一系列消息。每个消费者都有一个分配的 group ID。具有相同 group ID 的所有消费者作为单个逻辑消费者。主题的每条消息都会传递给具有相同 group ID 的消费者组中的一个消费者。特定主题的不同消费者组可以以自己的速度处理消息,因为消息在被消费后并不会立即从主题中移除。事实上,消费者有责任跟踪他们已经消费了多少消息。

如前所述,每个分区中的每条消息都有一个与之关联的唯一序列号,称为 offset。通过这个 offset,消费者知道他们已经处理了多少流。如果消费者决定重新播放已经处理过的消息,他只需要将 offset 的值设置为之前的值,然后再从 Kafka 中消费消息。

Broker

经纪人从生产者(推送机制)接收消息,并将消息传递给消费者(拉取机制)。经纪人还管理文件中消息的持久性。Kafka 经纪人非常轻量级:它们只在队列(主题分区)上打开文件指针,并管理 TCP 连接。

数据保留

Kafka 中的每个主题都有一个关联的保留时间。当此时间到期时,Kafka 会删除该特定主题的过期数据文件。这是一个非常高效的操作,因为它是一个文件删除操作。

安装 Kafka 经纪人

在撰写本文时,Kafka 的稳定版本是 0.9.x。

运行 Kafka 的先决条件是 ZooKeeper 集合和 Java 版本 1.7 或更高版本。Kafka 附带了一个方便的脚本,可以启动单节点 ZooKeeper,但不建议在生产环境中使用。我们将使用我们在第二章中部署的 ZooKeeper 集群。

我们将首先看如何设置单节点 Kafka 集群,然后再看如何添加另外两个节点以运行一个完整的、启用了复制的三节点 Kafka 集群。

设置单节点 Kafka 集群

以下是设置单节点 Kafka 集群的步骤:

  1. apache.claz.org/kafka/0.9.0.1/kafka_2.10-0.9.0.1.tgz下载 Kafka 0.9.x 二进制分发版,文件名为kafka_2.10-0.9.0.1.tar.gz

  2. 使用以下命令将存档文件提取到您想要安装 Kafka 的位置:

tar -xvzf kafka_2.10-0.9.0.1.tgz
cd kafka_2.10-0.9.0.1  

从现在开始,我们将把 Kafka 安装目录称为$KAFKA_HOME

  1. 更改$KAFKA_HOME/config/server.properties文件中的以下属性:
log.dirs=/var/kafka-logszookeeper.connect=zoo1:2181,zoo2:2181,zoo3:2181

在这里,zoo1zoo2zoo3代表了 ZooKeeper 节点的主机名。

以下是server.properties文件中重要属性的定义:

    • broker.id:这是 Kafka 集群中每个经纪人的唯一整数 ID。
  • port:这是 Kafka 经纪人的端口号。默认值为9092。如果您想在单台机器上运行多个经纪人,请为每个经纪人指定一个唯一的端口。

  • host.name:代表经纪人应该绑定和宣传自己的主机名。

  • log.dirs:这个属性的名称有点不幸,因为它代表的不是 Kafka 的日志目录,而是 Kafka 存储实际发送到它的数据的目录。它可以接受单个目录或逗号分隔的目录列表来存储数据。通过将多个物理磁盘连接到经纪人节点并指定多个数据目录,每个目录位于不同的磁盘上,可以增加 Kafka 的吞吐量。在同一物理磁盘上指定多个目录并没有太大用处,因为所有 I/O 仍然会在同一磁盘上进行。

  • num.partitions:这代表了新创建主题的默认分区数。在创建新主题时,可以覆盖此属性。分区数越多,可以实现更大的并行性,但会增加文件数量。

  • log.retention.hours:Kafka 在消费者消费消息后不会立即删除消息。它会保留消息一定小时数,由此属性定义,以便在出现任何问题时,消费者可以从 Kafka 重放消息。默认值为168小时,即 1 周。

  • zookeeper.connect:这是以hostname:port形式的 ZooKeeper 节点的逗号分隔列表。

  1. 通过运行以下命令启动 Kafka 服务器:

> ./bin/kafka-server-start.sh config/server.properties 

[2017-04-23 17:44:36,667] INFO New leader is 0 (kafka.server.ZookeeperLeaderElector$LeaderChangeListener)
[2017-04-23 17:44:36,668] INFO Kafka version : 0.9.0.1 (org.apache.kafka.common.utils.AppInfoParser)
[2017-04-23 17:44:36,668] INFO Kafka commitId : a7a17cdec9eaa6c5 (org.apache.kafka.common.utils.AppInfoParser)
[2017-04-23 17:44:36,670] INFO [Kafka Server 0], started (kafka.server.KafkaServer)  

如果您在控制台上得到类似于前三行的内容,那么您的 Kafka 经纪人已经启动,我们可以继续测试。

  1. 现在我们将通过发送和接收一些测试消息来验证 Kafka 经纪人是否设置正确。首先,让我们通过执行以下命令为测试创建一个验证主题:

> bin/kafka-topics.sh --zookeeper zoo1:2181 --replication-factor 1 --partition 1 --topic verification-topic --create

Created topic "verification-topic".  
  1. 现在让我们通过列出所有主题来验证主题创建是否成功:

> bin/kafka-topics.sh --zookeeper zoo1:2181 --list

verification-topic  
  1. 主题已创建;让我们为 Kafka 集群生成一些示例消息。Kafka 附带了一个命令行生产者,我们可以用来生成消息:

> bin/kafka-console-producer.sh --broker-list localhost:9092 --topic verification-topic    

  1. 在控制台上写入以下消息:
Message 1
Test Message 2
Message 3  
  1. 让我们通过在新的控制台窗口上启动新的控制台消费者来消费这些消息:
> bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topic verification-topic --from-beginning

Message 1
Test Message 2
Message 3  

现在,如果我们在生产者控制台上输入任何消息,它将自动被此消费者消费并显示在命令行上。

使用 Kafka 的单节点 ZooKeeper 如果您不想使用外部 ZooKeeper 集合,可以使用 Kafka 附带的单节点 ZooKeeper 实例进行快速开发。要开始使用它,首先修改$KAFKA_HOME/config/zookeeper.properties文件以指定数据目录,提供以下属性:

dataDir=/var/zookeeper

现在,您可以使用以下命令启动 Zookeeper 实例:

> ./bin/zookeeper-server-start.sh config/zookeeper.properties

设置三节点 Kafka 集群

到目前为止,我们有一个单节点 Kafka 集群。按照以下步骤部署 Kafka 集群:

  1. 创建一个三节点 VM 或三台物理机。

  2. 执行设置单节点 Kafka 集群部分中提到的步骤 1 和 2。

  3. 更改文件$KAFKA_HOME/config/server.properties中的以下属性:

broker.id=0
port=9092
host.name=kafka1
log.dirs=/var/kafka-logs
zookeeper.connect=zoo1:2181,zoo2:2181,zoo3:2181

确保broker.id属性的值对于每个 Kafka 代理都是唯一的,zookeeper.connect的值在所有节点上必须相同。

  1. 通过在所有三个框上执行以下命令来启动 Kafka 代理:
> ./bin/kafka-server-start.sh config/server.properties
  1. 现在让我们验证设置。首先使用以下命令创建一个主题:
> bin/kafka-topics.sh --zookeeper zoo1:2181 --replication-factor 4 --partition 1 --topic verification --create

    Created topic "verification-topic".  
  1. 现在,我们将列出主题以查看主题是否成功创建:
> bin/kafka-topics.sh --zookeeper zoo1:2181 --list

                topic: verification     partition: 0      leader: 0   replicas: 0             isr: 0
                topic: verification     partition: 1      leader: 1   replicas: 1             isr: 1
                topic: verification     partition: 2      leader: 2   replicas: 2             isr: 2  
  1. 现在,我们将通过使用 Kafka 控制台生产者和消费者来验证设置,就像在设置单节点 Kafka 集群部分中所做的那样:
> bin/kafka-console-producer.sh --broker-list kafka1:9092,kafka2:9092,kafka3:9092 --topic verification  
  1. 在控制台上写入以下消息:
First
Second
Third  
  1. 让我们通过在新的控制台窗口上启动新的控制台消费者来消费这些消息:
> bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topic verification --from-beginning

First
Second
Third 

到目前为止,我们有三个在工作的 Kafka 集群代理。在下一节中,我们将看到如何编写一个可以向 Kafka 发送消息的生产者:

单个节点上的多个 Kafka 代理

如果您想在单个节点上运行多个 Kafka 代理,则请按照以下步骤进行操作:

  1. 复制config/server.properties以创建config/server1.propertiesconfig/server2.properties

  2. config/server.properties中填写以下属性:

broker.id=0 
port=9092 
log.dirs=/var/kafka-logs 
zookeeper.connect=zoo1:2181,zoo2:2181,zoo3:2181 
  1. config/server1.properties中填写以下属性:
broker.id=1 
port=9093 
log.dirs=/var/kafka-1-logs 
zookeeper.connect=zoo1:2181,zoo2:2181,zoo3:2181 
  1. config/server2.properties中填写以下属性:
broker.id=2 
port=9094 
log.dirs=/var/kafka-2-logs 
zookeeper.connect=zoo1:2181,zoo2:2181,zoo3:2181 
  1. 在三个不同的终端上运行以下命令以启动 Kafka 代理:
> ./bin/kafka-server-start.sh config/server.properties
> ./bin/kafka-server-start.sh config/server1.properties
> ./bin/kafka-server-start.sh config/server2.properties

在 Storm 和 Kafka 之间共享 ZooKeeper

我们可以在 Kafka 和 Storm 之间共享相同的 ZooKeeper 集合,因为两者都将元数据存储在不同的 znodes 中(ZooKeeper 使用共享的分层命名空间协调分布式进程,其组织方式类似于标准文件系统。在 ZooKeeper 中,由数据寄存器组成的命名空间称为 znodes)。

我们需要打开 ZooKeeper 客户端控制台来查看为 Kafka 和 Storm 创建的 znodes(共享命名空间)。

转到ZK_HOME并执行以下命令以打开 ZooKeeper 控制台:

> bin/zkCli.sh  

执行以下命令以查看 znodes 列表:

> [zk: localhost:2181(CONNECTED) 0] ls /

**[storm, consumers, isr_change_notification, zookeeper, admin, brokers]**

在这里,消费者、isr_change_notification和代理是 znodes,Kafka 正在将其元数据信息管理到 ZooKeeper 的此位置。

Storm 在 ZooKeeper 中的 Storm znodes 中管理其元数据。

Kafka 生产者并将数据发布到 Kafka

在本节中,我们正在编写一个 Kafka 生产者,它将发布事件到 Kafka 主题中。

执行以下步骤创建生产者:

  1. 使用com.stormadvance作为groupIdkafka-producer作为artifactId创建一个 Maven 项目。

  2. pom.xml文件中为 Kafka 添加以下依赖项:

<dependency> 
  <groupId>org.apache.kafka</groupId> 
  <artifactId>kafka_2.10</artifactId> 
  <version>0.9.0.1</version> 
  <exclusions> 
    <exclusion> 
      <groupId>com.sun.jdmk</groupId> 
      <artifactId>jmxtools</artifactId> 
    </exclusion> 
    <exclusion> 
      <groupId>com.sun.jmx</groupId> 
      <artifactId>jmxri</artifactId> 
    </exclusion> 
  </exclusions> 
</dependency> 
<dependency> 
  <groupId>org.apache.logging.log4j</groupId> 
  <artifactId>log4j-slf4j-impl</artifactId> 
  <version>2.0-beta9</version> 
</dependency> 
<dependency> 
  <groupId>org.apache.logging.log4j</groupId> 
  <artifactId>log4j-1.2-api</artifactId> 
  <version>2.0-beta9</version> 
</dependency>  
  1. pom.xml文件中添加以下build插件。这将允许我们使用 Maven 执行生产者:
<build> 
  <plugins> 
    <plugin> 
      <groupId>org.codehaus.mojo</groupId> 
      <artifactId>exec-maven-plugin</artifactId> 
      <version>1.2.1</version> 
      <executions> 
        <execution> 
          <goals> 
            <goal>exec</goal> 
          </goals> 
        </execution> 
      </executions> 
      <configuration> 
        <executable>java</executable
        <includeProjectDependencies>true</includeProjectDependencies
        <includePluginDependencies>false</includePluginDependencies> 
        <classpathScope>compile</classpathScope> 
        <mainClass>com.stormadvance.kafka_producer. KafkaSampleProducer 
        </mainClass> 
      </configuration> 
    </plugin> 
  </plugins> 
</build> 
  1. 现在我们将在com.stormadvance.kafka_producer包中创建KafkaSampleProducer类。该类将从弗朗茨·卡夫卡的《变形记》第一段中的每个单词产生单词,并将其作为单个消息发布到 Kafka 的new_topic主题中。以下是KafkaSampleProducer类的代码及解释:
public class KafkaSampleProducer { 
  public static void main(String[] args) { 
    // Build the configuration required for connecting to Kafka 
    Properties props = new Properties(); 

    // List of kafka borkers. Complete list of brokers is not required as 
    // the producer will auto discover the rest of the brokers. 
    props.put("bootstrap.servers", "Broker1-IP:9092"); 
    props.put("batch.size", 1); 
    // Serializer used for sending data to kafka. Since we are sending string, 
    // we are using StringSerializer. 
    props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); 
    props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); 

    props.put("producer.type", "sync"); 

    // Create the producer instance 
    Producer<String, String> producer = new KafkaProducer<String, String>(props); 

    // Now we break each word from the paragraph 
    for (String word : METAMORPHOSIS_OPENING_PARA.split("\\s")) { 
      System.out.println("word : " + word); 
      // Create message to be sent to "new_topic" topic with the word 
      ProducerRecord<String, String> data = new ProducerRecord<String, String>("new_topic",word, word); 
      // Send the message 
      producer.send(data); 
    } 

    // close the producer 
    producer.close(); 
    System.out.println("end : "); 
  } 

  // First paragraph from Franz Kafka's Metamorphosis 
  private static String METAMORPHOSIS_OPENING_PARA = "One morning, when Gregor Samsa woke from troubled dreams, he found " 
               + "himself transformed in his bed into a horrible vermin.  He lay on " 
               + "his armour-like back, and if he lifted his head a little he could " 
               + "see his brown belly, slightly domed and divided by arches into stiff " 
               + "sections.  The bedding was hardly able to cover it and seemed ready " 
               + "to slide off any moment.  His many legs, pitifully thin compared " 
               + "with the size of the rest of him, waved about helplessly as he " 
               + "looked."; 

}  
  1. 现在,在运行生产者之前,我们需要在 Kafka 中创建new_topic。为此,请执行以下命令:

> bin/kafka-topics.sh --zookeeper ZK1:2181 --replication-factor 1 --partition 1 --topic new_topic --create 

Created topic "new_topic1".    

  1. 现在我们可以通过执行以下命令运行生产者:
> mvn compile exec:java
......
103  [com.learningstorm.kafka.WordsProducer.main()] INFO                kafka.client.ClientUti
ls$  - Fetching metadata from broker                                    id:0,host:kafka1,port:9092 with correlation id 0 for 1                  topic(s) Set(words_topic)
110  [com.learningstorm.kafka.WordsProducer.main()] INFO                kafka.producer.SyncProducer  - Connected to kafka1:9092 for             producing
140  [com.learningstorm.kafka.WordsProducer.main()] INFO                kafka.producer.SyncProducer  - Disconnecting from                       kafka1:9092
177  [com.learningstorm.kafka.WordsProducer.main()] INFO                kafka.producer.SyncProducer  - Connected to kafka1:9092 for             producing
378  [com.learningstorm.kafka.WordsProducer.main()] INFO                kafka.producer.Producer  - Shutting down producer
378  [com.learningstorm.kafka.WordsProducer.main()] INFO                kafka.producer.ProducerPool  - Closing all sync producers
381  [com.learningstorm.kafka.WordsProducer.main()] INFO                kafka.producer.SyncProducer  - Disconnecting from                       kafka1:9092
  1. 现在让我们通过使用 Kafka 的控制台消费者来验证消息是否已被生产,并执行以下命令:
> bin/kafka-console-consumer.sh --zookeeper ZK:2181 --topic verification --from-beginning

                One
                morning,
                when
                Gregor
                Samsa
                woke
                from
                troubled
                dreams,
                he
                found
                himself
                transformed
                in
                his
                bed
                into
                a
                horrible
                vermin.
                ......

因此,我们能够向 Kafka 生产消息。在下一节中,我们将看到如何使用KafkaSpout从 Kafka 中读取消息并在 Storm 拓扑中处理它们。

Kafka Storm 集成

现在我们将创建一个 Storm 拓扑,该拓扑将从 Kafka 主题new_topic中消费消息并将单词聚合成句子。

完整的消息流如下所示:

我们已经看到了KafkaSampleProducer,它将单词生产到 Kafka 代理中。现在我们将创建一个 Storm 拓扑,该拓扑将从 Kafka 中读取这些单词并将它们聚合成句子。为此,我们的应用程序中将有一个KafkaSpout,它将从 Kafka 中读取消息,并且有两个 bolt,WordBoltKafkaSpout接收单词,然后将它们聚合成句子,然后传递给SentenceBolt,它只是在输出流上打印它们。我们将在本地模式下运行此拓扑。

按照以下步骤创建 Storm 拓扑:

  1. 创建一个新的 Maven 项目,groupIdcom.stormadvanceartifactIdkafka-storm-topology

  2. pom.xml文件中添加以下 Kafka-Storm 和 Storm 的依赖项:

<dependency> 
  <groupId>org.apache.storm</groupId> 
  <artifactId>storm-kafka</artifactId> 
  <version>1.0.2</version> 
  <exclusions> 
    <exclusion> 
      <groupId>org.apache.kafka</groupId> 
      <artifactId>kafka-clients</artifactId> 
    </exclusion> 
  </exclusions> 
</dependency> 

<dependency> 
  <groupId>org.apache.kafka</groupId> 
  <artifactId>kafka_2.10</artifactId> 
  <version>0.9.0.1</version> 
  <exclusions> 
    <exclusion> 
      <groupId>com.sun.jdmk</groupId> 
      <artifactId>jmxtools</artifactId> 
    </exclusion> 
    <exclusion> 
      <groupId>com.sun.jmx</groupId> 
      <artifactId>jmxri</artifactId> 
    </exclusion> 
  </exclusions> 
</dependency> 

<dependency> 
  <groupId>org.apache.storm</groupId> 
  <artifactId>storm-core</artifactId> 
  <version>1.0.2</version> 
  <scope>provided</scope> 
</dependency> 
<dependency> 
  <groupId>commons-collections</groupId> 
  <artifactId>commons-collections</artifactId> 
  <version>3.2.1</version> 
</dependency> 

<dependency> 
  <groupId>com.google.guava</groupId> 
  <artifactId>guava</artifactId> 
  <version>15.0</version> 
</dependency>  
  1. pom.xml文件中添加以下 Maven 插件,以便我们能够从命令行运行它,并且还能够打包拓扑以在 Storm 中执行:
<build> 
  <plugins> 
    <plugin> 
      <artifactId>maven-assembly-plugin</artifactId> 
      <configuration> 
        <descriptorRefs> 
          descriptorRef>jar-with-dependencies</descriptorRef> 
        </descriptorRefs> 
        <archive> 
          <manifest> 
            <mainClass></mainClass> 
          </manifest> 
        </archive> 
      </configuration> 
      <executions> 
        <execution> 
          <id>make-assembly</id> 
          <phase>package</phase> 
          <goals> 
            <goal>single</goal> 
          </goals> 
        </execution> 
      </executions> 
    </plugin> 

    <plugin> 
      <groupId>org.codehaus.mojo</groupId> 
      <artifactId>exec-maven-plugin</artifactId> 
      <version>1.2.1</version> 
      <executions> 
        <execution> 
          <goals> 
            <goal>exec</goal> 
          </goals> 
        </execution> 
      </executions> 
      <configuration> 
        <executable>java</executable
        <includeProjectDependencies>true</includeProjectDependencies
        <includePluginDependencies>false</includePluginDependencies> 
        <classpathScope>compile</classpathScope> 
        <mainClass>${main.class}</mainClass> 
      </configuration> 
    </plugin> 

    <plugin> 
      <groupId>org.apache.maven.plugins</groupId> 
      <artifactId>maven-compiler-plugin</artifactId> 
    </plugin> 

  </plugins> 
</build> 
  1. 现在我们将首先创建WordBolt,它将单词聚合成句子。为此,在com.stormadvance.kafka包中创建一个名为WordBolt的类。WordBolt的代码如下,附有解释:
public class WordBolt extends BaseBasicBolt { 

  private static final long serialVersionUID = -5353547217135922477L; 

  // list used for aggregating the words 
  private List<String> words = new ArrayList<String>(); 

  public void execute(Tuple input, BasicOutputCollector collector) { 
    System.out.println("called"); 
    // Get the word from the tuple 
    String word = input.getString(0); 

    if (StringUtils.isBlank(word)) { 
      // ignore blank lines 
      return; 
    } 

    System.out.println("Received Word:" + word); 

    // add word to current list of words 
    words.add(word); 

    if (word.endsWith(".")) { 
      // word ends with '.' which means this is // the end of the sentence 
      // publish a sentence tuple 
      collector.emit(ImmutableList.of((Object) StringUtils.join(words, ' '))); 

      // reset the words list. 
      words.clear(); 
    } 
  } 

  public void declareOutputFields(OutputFieldsDeclarer declarer) { 
    // here we declare we will be emitting tuples with 
    // a single field called "sentence" 
    declarer.declare(new Fields("sentence")); 
  } 
} 
  1. 接下来是SentenceBolt,它只是打印接收到的句子。在com.stormadvance.kafka包中创建SentenceBolt。代码如下,附有解释:
public class SentenceBolt extends BaseBasicBolt { 

  private static final long serialVersionUID = 7104400131657100876L; 

  public void execute(Tuple input, BasicOutputCollector collector) { 
    // get the sentence from the tuple and print it 
    System.out.println("Recieved Sentence:"); 
    String sentence = input.getString(0); 
    System.out.println("Recieved Sentence:" + sentence); 
  } 

  public void declareOutputFields(OutputFieldsDeclarer declarer) { 
         // we don't emit anything 
  } 
} 
  1. 现在我们将创建KafkaTopology,它将定义KafkaSpout并将其与WordBoltSentenceBolt连接起来。在com.stormadvance.kafka包中创建一个名为KafkaTopology的新类。代码如下,附有解释:
public class KafkaTopology { 
  public static void main(String[] args) { 
    try { 
      // ZooKeeper hosts for the Kafka cluster 
      BrokerHosts zkHosts = new ZkHosts("ZKIP:PORT"); 

      // Create the KafkaSpout configuartion 
      // Second argument is the topic name 
      // Third argument is the zookeepr root for Kafka 
      // Fourth argument is consumer group id 
      SpoutConfig kafkaConfig = new SpoutConfig(zkHosts, "new_topic", "", "id1"); 

      // Specify that the kafka messages are String 
      // We want to consume all the first messages in the topic everytime 
      // we run the topology to help in debugging. In production, this 
      // property should be false 
      kafkaConfig.scheme = new SchemeAsMultiScheme(new StringScheme()); 
      kafkaConfig.startOffsetTime = kafka.api.OffsetRequest.EarliestTime(); 

      // Now we create the topology 
      TopologyBuilder builder = new TopologyBuilder(); 

      // set the kafka spout class 
      builder.setSpout("KafkaSpout", new KafkaSpout(kafkaConfig), 2); 

      // set the word and sentence bolt class 
      builder.setBolt("WordBolt", new WordBolt(), 1).globalGrouping("KafkaSpout"); 
      builder.setBolt("SentenceBolt", new SentenceBolt(), 1).globalGrouping("WordBolt"); 

      // create an instance of LocalCluster class for executing topology 
      // in local mode. 
      LocalCluster cluster = new LocalCluster(); 
      Config conf = new Config(); 
      conf.setDebug(true); 
      if (args.length > 0) { 
        conf.setNumWorkers(2); 
        conf.setMaxSpoutPending(5000); 
        StormSubmitter.submitTopology("KafkaToplogy1", conf, builder.createTopology()); 

      } else { 
        // Submit topology for execution 
        cluster.submitTopology("KafkaToplogy1", conf, builder.createTopology()); 
        System.out.println("called1"); 
        Thread.sleep(1000000); 
        // Wait for sometime before exiting 
        System.out.println("Waiting to consume from kafka"); 

        System.out.println("called2"); 
        // kill the KafkaTopology 
        cluster.killTopology("KafkaToplogy1"); 
        System.out.println("called3"); 
        // shutdown the storm test cluster 
        cluster.shutdown(); 
      } 

    } catch (Exception exception) { 
      System.out.println("Thread interrupted exception : " + exception); 
    } 
  } 
} 
  1. 现在我们将运行拓扑。确保 Kafka 集群正在运行,并且您已经在上一节中执行了生产者,以便 Kafka 中有消息可以消费。

  2. 通过执行以下命令运行拓扑:

> mvn clean compile exec:java  -Dmain.class=com.stormadvance.kafka.KafkaTopology 

这将执行拓扑。您应该在输出中看到类似以下的消息:

Recieved Word:One
Recieved Word:morning,
Recieved Word:when
Recieved Word:Gregor
Recieved Word:Samsa
Recieved Word:woke
Recieved Word:from
Recieved Word:troubled
Recieved Word:dreams,
Recieved Word:he
Recieved Word:found
Recieved Word:himself
Recieved Word:transformed
Recieved Word:in
Recieved Word:his
Recieved Word:bed
Recieved Word:into
Recieved Word:a
Recieved Word:horrible
Recieved Word:vermin.
Recieved Sentence:One morning, when Gregor Samsa woke from              troubled dreams, he found himself transformed in his bed                   into a horrible vermin.  

因此,我们能够从 Kafka 中消费消息并在 Storm 拓扑中处理它们。

在 Storm 集成拓扑中部署 Kafka

在 Storm 集群上部署 Kafka 和 Storm 集成拓扑与部署其他拓扑类似。我们需要设置工作程序的数量和最大的 spout pending Storm 配置,并且我们需要使用StormSubmittersubmitTopology方法将拓扑提交到 Storm 集群上。

现在,我们需要按照以下步骤构建拓扑代码,以创建 Kafka Storm 集成拓扑的 JAR 包:

  1. 转到项目主页。

  2. 执行命令:

mvn clean install

上述命令的输出如下:

------------------------------------------------------------------ ----- [INFO] ----------------------------------------------------------- ----- [INFO] BUILD SUCCESS [INFO] ----------------------------------------------------------- ----- [INFO] Total time: 58.326s [INFO] Finished at: [INFO] Final Memory: 14M/116M [INFO] ----------------------------------------------------------- -----
  1. 现在,将 Kafka Storm 拓扑复制到 Nimbus 机器上,并执行以下命令将拓扑提交到 Storm 集群上:
bin/storm jar jarName.jar [TopologyMainClass] [Args]

前面的命令运行TopologyMainClass并带有参数。TopologyMainClass的主要功能是定义拓扑并将其提交到 Nimbus。Storm JAR 部分负责连接到 Nimbus 并上传 JAR 部分。

  1. 登录 Storm Nimbus 机器并执行以下命令:
$> cd $STORM_HOME
$> bin/storm jar ~/storm-kafka-topology-0.0.1-SNAPSHOT-jar-with-dependencies.jar com.stormadvance.kafka.KafkaTopology KafkaTopology1

在这里,~/ storm-kafka-topology-0.0.1-SNAPSHOT-jar-with-dependencies.jar是我们在 Storm 集群上部署的KafkaTopology JAR 的路径。

总结

在本章中,我们学习了 Apache Kafka 的基础知识以及如何将其作为与 Storm 一起构建实时流处理管道的一部分。我们了解了 Apache Kafka 的架构以及如何通过使用KafkaSpout将其集成到 Storm 处理中。

在下一章中,我们将介绍 Storm 与 Hadoop 和 YARN 的集成。我们还将介绍此操作的示例示例。

第九章:Storm 和 Hadoop 集成

到目前为止,我们已经看到了 Storm 如何用于开发实时流处理应用程序。一般来说,这些实时应用程序很少单独使用;它们更常用于与其他批处理操作结合使用。

开发批处理作业的最常见平台是 Apache Hadoop。在本章中,我们将看到如何使用 Apache Storm 构建的应用程序可以借助 Storm-YARN 框架在现有的 Hadoop 集群上进行部署,以优化资源的使用和管理。我们还将介绍如何通过在 Storm 中创建一个 HDFS bolt 来将处理数据写入 HDFS。

在本章中,我们将涵盖以下主题:

  • Apache Hadoop 及其各个组件概述

  • 设置 Hadoop 集群

  • 将 Storm 拓扑写入 HDFS 以持久化数据

  • Storm-YARN 概述

  • 在 Hadoop 上部署 Storm-YARN

  • 在 Storm-YARN 上运行 storm 应用程序。

Hadoop 简介

Apache Hadoop 是一个用于开发和部署大数据应用程序的开源平台。最初是在 Yahoo!上开发的,基于 Google 发布的 MapReduce 和 Google 文件系统论文。在过去几年里,Hadoop 已成为旗舰大数据平台。

在本节中,我们将讨论 Hadoop 集群的关键组件。

Hadoop 通用

这是其他 Hadoop 模块基于的基本库。它提供了一个操作系统和文件系统操作的抽象,使得 Hadoop 可以部署在各种平台上。

Hadoop 分布式文件系统

通常被称为HDFSHadoop 分布式文件系统是一种可扩展的、分布式的、容错的文件系统。HDFS 充当了 Hadoop 生态系统的存储层。它允许在 Hadoop 集群中的各个节点之间共享和存储数据和应用程序代码。

在设计 HDFS 时,做出了以下关键假设:

  • 它应该可以部署在一组廉价硬件的集群上。

  • 硬件故障是预期的,它应该能够容忍这些故障。

  • 它应该可扩展到数千个节点。

  • 它应该针对高吞吐量进行优化,即使牺牲延迟。

  • 大多数文件都会很大,因此应该针对大文件进行优化。

  • 存储是廉价的,因此使用复制来保证可靠性。

  • 它应该具有位置感知能力,以便对数据请求的计算可以在实际数据所在的物理节点上执行。这将导致较少的数据移动,从而降低网络拥塞。

一个 HDFS 集群有以下组件。

Namenode

Namenode 是 HDFS 集群中的主节点。它负责管理文件系统的元数据和操作。它不存储任何用户数据,只存储集群中所有文件的文件系统树。它还跟踪文件的块的物理位置。

由于 namenode 将所有数据保存在 RAM 中,因此应该部署在具有大量 RAM 的机器上。此外,不应该在托管 namenode 的机器上托管其他进程,以便所有资源都专门用于它。

Namenode 是 HDFS 集群中的单点故障。如果 namenode 死机,HDFS 集群上将无法进行任何操作。

图 1:HDFS 集群

Datanode

Datanode 负责在 HDFS 集群中存储用户数据。在 HDFS 集群中可以有多个 datanode。Datanode 将数据存储在托管 datanode 的系统上的物理磁盘上。不建议将 datanode 数据存储在 RAID 配置的磁盘上,因为 HDFS 通过在 datanode 之间复制数据来实现数据保护。

HDFS 客户端

HDFS 客户端是一个客户端库,可用于与 HDFS 集群交互。它通常与 namenode 通信,执行元操作,如创建新文件等,而 datanodes 提供实际的数据读写请求。

次要名称节点

辅助 namenode 是 HDFS 中命名不当的组件之一。尽管它的名字是这样,但它并不是 namenode 的备用。要理解它的功能,我们需要深入了解 namenode 的工作原理。

Namenode 将文件系统元数据保存在主内存中。为了持久性,它还将这些元数据以镜像文件的形式写入本地磁盘。当 namenode 启动时,它读取这个 fs 镜像快照文件,以重新创建内存数据结构来保存文件系统数据。文件系统的任何更新都会应用到内存数据结构,但不会应用到镜像中。这些更改会被写入称为编辑日志的单独文件中。当 namenode 启动时,它将这些编辑日志合并到镜像中,以便下次重新启动将会很快。在生产环境中,由于 namenode 不经常重新启动,编辑日志可能会变得非常大。这可能导致 namenode 在重新启动时启动时间非常长。

辅助 namenode 负责将 namenode 的编辑日志与镜像合并,以便下次 namenode 启动更快。它从 namenode 获取镜像快照和编辑日志,然后将它们合并,然后将更新后的镜像快照放在 namenode 机器上。这减少了 namenode 在重新启动时需要进行的合并量,从而减少了 namenode 的启动时间。

以下截图展示了辅助 namenode 的工作原理:

图 2:辅助 Namenode 的功能

到目前为止,我们已经看到了 Hadoop 的存储部分。接下来我们将看一下处理组件。

YARN

YARN 是一个集群资源管理框架,它使用户能够向 Hadoop 集群提交各种作业,并管理可伸缩性、容错性、作业调度等。由于 HDFS 提供了大量数据的存储层,YARN 框架为编写大数据处理应用程序提供了所需的基础设施。

以下是 YARN 集群的主要组件。

ResourceManager(RM)

ResourceManager 是 YARN 集群中应用程序的入口点。它是集群中负责管理所有资源的主进程。它还负责调度提交到集群的各种作业。这种调度策略是可插拔的,用户可以根据需要支持新类型的应用程序进行自定义。

NodeManager(NM)

在集群中的每个处理节点上部署了一个 NodeManager 代理。它是与节点级别的 ResourceManager 对应的。它与 ResourceManager 通信,更新节点状态并接收来自 ResourceManager 的任何作业请求。它还负责生命周期管理和向 ResourceManager 报告各种节点指标。

ApplicationMaster(AM)

一旦 ResourceManager 调度了作业,它就不再跟踪其状态和进度。这使得 ResourceManager 能够支持集群中完全不同类型的应用程序,而不必担心应用程序的内部通信和逻辑。

每当提交一个应用程序时,ResourceManager 都会为该应用程序创建一个新的 ApplicationMaster,然后负责与 ResourceManager 协商资源,并与 NodeMangers 通信以获取资源。NodeManager 以资源容器的形式提供资源,这是资源分配的抽象,您可以告诉需要多少 CPU、内存等。

一旦应用程序在集群中的各个节点上开始运行,ApplicationMaster 就会跟踪各种作业的状态,并在失败时重新运行这些作业。作业完成后,它将释放资源给 ResourceManager。

以下截图展示了 YARN 集群中的各种组件:

图 3:YARN 组件

Hadoop 安装

现在我们已经看到了 Hadoop 集群的存储和处理部分,让我们开始安装 Hadoop。在本章中,我们将使用 Hadoop 2.2.0。请注意,此版本与 Hadoop 1.X 版本不兼容。

我们将在单节点上设置一个集群。在开始之前,请确保您的系统上已安装以下内容:

  • JDK 1.7

  • ssh-keygen

如果您没有wgetssh-keygen,请使用以下命令进行安装:

# yum install openssh-clients  

接下来,我们需要在此计算机上设置无密码 SSH,因为这对于 Hadoop 是必需的。

设置无密码 SSH

以下是设置无密码 SSH 的步骤:

  1. 通过执行以下命令生成您的 SSH 密钥对:
    $ ssh-keygen -t rsa -P ''
    Generating public/private rsa key pair.
    Enter file in which to save the key (/home/anand/.ssh/id_rsa): 
    Your identification has been saved in /home/anand/.ssh/id_rsa.
    Your public key has been saved in /home/anand/.ssh/id_rsa.pub.
    The key fingerprint is:
    b7:06:2d:76:ed:df:f9:1d:7e:5f:ed:88:93:54:0f:24 anand@localhost.localdomain
    The key's randomart image is:
    +--[ RSA 2048]----+
    |                 |
    |            E .  |
    |             o   |
    |         . .  o  |
    |        S + .. o |
    |       . = o.   o|
    |          o... .o|
    |         .  oo.+*|
    |            ..ooX|
    +-----------------+

  1. 接下来,我们需要将生成的公钥复制到当前用户的授权密钥列表中。要做到这一点,执行以下命令:
$ cp ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys  
  1. 现在,我们可以通过以下命令连接到 localhost 检查无密码 SSH 是否正常工作:
$ ssh localhost
Last login: Wed Apr  2 09:12:17 2014 from localhost  

由于我们能够在本地主机上使用 SSH 而无需密码,我们的设置现在正在工作,我们现在将继续进行 Hadoop 设置。

获取 Hadoop 捆绑包并设置环境变量

以下是设置 Hadoop 的步骤:

  1. 从 Apache 网站下载 Hadoop 2.2.0 hadoop.apache.org/releases.html#Download

  2. 在我们想要安装 Hadoop 的位置解压存档。我们将称此位置为$HADOOP_HOME

$ tar xzf hadoop-2.2.0.tar.gz
$ cd hadoop-2.2.0  
  1. 接下来,我们需要设置环境变量和 Hadoop 的路径,将以下条目添加到您的~/.bashrc文件中。确保根据您的系统提供 Java 和 Hadoop 的路径:
    export JAVA_HOME=/usr/java/jdk1.7.0_45
    export HADOOP_HOME=/home/anand/opt/hadoop-2.2.0
    export HADOOP_COMMON_HOME=/home/anand/opt/hadoop-2.2.0
    export HADOOP_HDFS_HOME=$HADOOP_COMMON_HOME
    export HADOOP_MAPRED_HOME=$HADOOP_COMMON_HOME
    export HADOOP_YARN_HOME=$HADOOP_COMMON_HOME
    export HADOOP_CONF_DIR=$HADOOP_COMMON_HOME/etc/hadoop
    export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_COMMON_HOME/lib/native
    export HADOOP_OPTS="-Djava.library.path=$HADOOP_COMMON_HOME/lib"

    export PATH=$PATH:$JAVA_HOME/bin:$HADOOP_COMMON_HOME/bin:$HADOOP_COMMON_HOME/sbin

  1. 刷新您的~/.bashrc文件:
$ source ~/.bashrc  
  1. 现在让我们用以下命令检查路径是否正确配置:
$ hadoop version
Hadoop 2.2.0
Subversion https://svn.apache.org/repos/asf/hadoop/common -r 1529768
Compiled by hortonmu on 2013-10-07T06:28Z
Compiled with protoc 2.5.0
From source with checksum 79e53ce7994d1628b240f09af91e1af4
This command was run using /home/anand/opt/hadoop-
2.2.0/share/hadoop/common/hadoop-common-2.2.0.jar  

在前面的片段中,我们可以看到路径已正确设置。现在我们将在系统上设置 HDFS。

设置 HDFS

按照以下步骤设置 HDFS:

  1. 创建用于保存 namenode 和 datanode 数据的目录:
$ mkdir -p ~/mydata/hdfs/namenode
$ mkdir -p ~/mydata/hdfs/datanode  
  1. 通过在$HADOOP_CONF_DIR/core-site.xml文件的<configuration>标记中添加以下属性来指定 namenode 端口:
<property> 
        <name>fs.default.name</name> 
        <value>hdfs://localhost:19000</value> 
   <!-- The default port for HDFS is 9000, but we are using 19000 Storm-Yarn uses port 9000 for its application master --> 
</property> 
  1. 通过在$HADOOP_CONF_DIR/hdfs-site.xml文件的<configuration>标记中添加以下属性来指定 namenode 和 datanode 目录:
<property> 
        <name>dfs.replication</name> 
        <value>1</value> 
   <!-- Since we have only one node, we have replication factor=1 --> 
</property> 
<property> 
        <name>dfs.namenode.name.dir</name> 
        <value>file:/home/anand/hadoop-data/hdfs/namenode</value> 
   <!-- specify absolute path of the namenode directory --> 
</property> 
<property> 
        <name>dfs.datanode.data.dir</name> 
        <value>file:/home/anand/hadoop-data/hdfs/datanode</value> 
   <!-- specify absolute path of the datanode directory --> 
</property> 
  1. 现在我们将格式化 namenode。这是一个一次性的过程,只需要在设置 HDFS 时执行:
    $ hdfs namenode -format
    14/04/02 09:03:06 INFO namenode.NameNode: STARTUP_MSG: 
    /*********************************************************
    STARTUP_MSG: Starting NameNode
    STARTUP_MSG:   host = localhost.localdomain/127.0.0.1
    STARTUP_MSG:   args = [-format]
    STARTUP_MSG:   version = 2.2.0
    ... ...
    14/04/02 09:03:08 INFO namenode.NameNode: SHUTDOWN_MSG: 
    /*********************************************************
    SHUTDOWN_MSG: Shutting down NameNode at localhost.localdomain/127.0.0.1
    ********************************************************/

  1. 现在,我们已经完成了配置,我们将启动 HDFS:
    $ start-dfs.sh 
    14/04/02 09:27:13 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
    Starting namenodes on [localhost]
    localhost: starting namenode, logging to /home/anand/opt/hadoop-2.2.0/logs/hadoop-anand-namenode-localhost.localdomain.out
    localhost: starting datanode, logging to /home/anand/opt/hadoop-2.2.0/logs/hadoop-anand-datanode-localhost.localdomain.out
    Starting secondary namenodes [0.0.0.0]
    0.0.0.0: starting secondarynamenode, logging to /home/anand/opt/hadoop-2.2.0/logs/hadoop-anand-secondarynamenode-localhost.localdomain.out
    14/04/02 09:27:32 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable

  1. 现在,执行jps命令查看所有进程是否正常运行:
$ jps
50275 NameNode
50547 SecondaryNameNode
50394 DataNode
51091 Jps  

在这里,我们可以看到所有预期的进程都在运行。

  1. 现在,您可以通过在浏览器中打开http://localhost:50070来检查 HDFS 的状态。您应该看到类似以下的内容:

图 4:Namenode web UI

  1. 您可以使用hdfs dfs命令与 HDFS 进行交互。在控制台上运行hdfs dfs以获取所有选项,或者参考hadoop.apache.org/docs/r2.2.0/hadoop-project-dist/hadoop-common/FileSystemShell.html上的文档。

现在 HDFS 已部署,我们将接下来设置 YARN。

设置 YARN

以下是设置 YARN 的步骤:

  1. 从模板mapred-site.xml.template创建mapred-site.xml文件:
$ cp $HADOOP_CONF_DIR/mapred-site.xml.template $HADOOP_CONF_DIR/mapred-
site.xml  
  1. 通过在$HADOOP_CONF_DIR/mapred-site.xml文件的<configuration>标记中添加以下属性来指定我们正在使用 YARN 框架:
<property> 
        <name>mapreduce.framework.name</name> 
        <value>yarn</value> 
</property> 
  1. $HADOOP_CONF_DIR/yarn-site.xml文件中配置以下属性:
<property> 
        <name>yarn.nodemanager.aux-services</name> 
        <value>mapreduce_shuffle</value> 
</property> 

<property> 
        <name>yarn.scheduler.minimum-allocation-mb</name> 
        <value>1024</value> 
</property> 

<property> 
        <name>yarn.nodemanager.resource.memory-mb</name> 
        <value>4096</value> 
</property> 

<property> 
        <name>yarn.nodemanager.aux-services.mapreduce.shuffle.class</name> 
   <value>org.apache.hadoop.mapred.ShuffleHandler</value> 
</property> 
<property> 
        <name>yarn.nodemanager.vmem-pmem-ratio</name> 
        <value>8</value> 
</property> 
  1. 使用以下命令启动 YARN 进程:
$ start-yarn.sh 
starting yarn daemons
starting resourcemanager, logging to /home/anand/opt/hadoop-2.2.0/logs/yarn-anand-resourcemanager-localhost.localdomain.out
localhost: starting nodemanager, logging to /home/anand/opt/hadoop-2.2.0/logs/yarn-anand-nodemanager-localhost.localdomain.out  
  1. 现在,执行jps命令查看所有进程是否正常运行:
$ jps
50275 NameNode
50547 SecondaryNameNode
50394 DataNode
51091 Jps
50813 NodeManager
50716 ResourceManager  

在这里,我们可以看到所有预期的进程都在运行。

  1. 现在,您可以通过在浏览器中打开http://localhost:8088/cluster来检查 YARN 的状态,使用 ResourceManager web UI。您应该会看到类似以下内容的内容:

图 5:ResourceManager web UI

  1. 您可以使用yarn命令与 YARN 进行交互。在控制台上运行yarn或参考hadoop.apache.org/docs/r2.2.0/hadoop-yarn/hadoop-yarn-site/YarnCommands.html获取所有选项。要获取当前在 YARN 上运行的所有应用程序,请运行以下命令:
    $ yarn application -list
    14/04/02 11:41:42 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
    14/04/02 11:41:42 INFO client.RMProxy: Connecting to ResourceManager at /0.0.0.0:8032
    Total number of applications (application-types: [] and states: [SUBMITTED, ACCEPTED, RUNNING]):0
                    Application-Id          Application-Name        Application-Type          User       Queue               State             Final-State             Progress                          Tracking-URL

通过这样,我们已经完成了在单节点上部署 Hadoop 集群。接下来我们将看到如何在此集群上运行 Storm 拓扑。

将 Storm 拓扑写入 HDFS 以持久化数据

在本节中,我们将介绍如何编写 HDFS bolt 以将数据持久化到 HDFS 中。在本节中,我们将重点介绍以下几点:

  • 从 Kafka 消费数据

  • 将数据存储到 HDFS 的逻辑

  • 在预定义的时间或大小后将文件旋转到 HDFS

执行以下步骤来创建将数据存储到 HDFS 的拓扑:

  1. 创建一个新的 maven 项目,groupId 为com.stormadvance,artifactId 为storm-hadoop

  2. pom.xml文件中添加以下依赖项。我们在pom.xml中添加 Kafka Maven 依赖项以支持 Kafka 消费者。请参考前一章节,在那里我们将从 Kafka 消费数据并存储在 HDFS 中:

         <dependency> 
               <groupId>org.codehaus.jackson</groupId> 
               <artifactId>jackson-mapper-asl</artifactId> 
               <version>1.9.13</version> 
         </dependency> 

         <dependency> 
               <groupId>org.apache.hadoop</groupId> 
               <artifactId>hadoop-client</artifactId> 
               <version>2.2.0</version> 
               <exclusions> 
                     <exclusion> 
                           <groupId>org.slf4j</groupId> 
                           <artifactId>slf4j-log4j12</artifactId> 
                     </exclusion> 
               </exclusions> 
         </dependency> 
         <dependency> 
               <groupId>org.apache.hadoop</groupId> 
               <artifactId>hadoop-hdfs</artifactId> 
               <version>2.2.0</version> 
               <exclusions> 
                     <exclusion> 
                           <groupId>org.slf4j</groupId> 
                           <artifactId>slf4j-log4j12</artifactId> 
                     </exclusion> 
               </exclusions> 
         </dependency> 
         <!-- Dependency for Storm-Kafka spout --> 
         <dependency> 
               <groupId>org.apache.storm</groupId> 
               <artifactId>storm-kafka</artifactId> 
               <version>1.0.2</version> 
               <exclusions> 
                     <exclusion> 
                           <groupId>org.apache.kafka</groupId> 
                           <artifactId>kafka-clients</artifactId> 
                     </exclusion> 
               </exclusions> 
         </dependency> 

         <dependency> 
               <groupId>org.apache.kafka</groupId> 
               <artifactId>kafka_2.10</artifactId> 
               <version>0.9.0.1</version> 
               <exclusions> 
                     <exclusion> 
                           <groupId>com.sun.jdmk</groupId> 
                           <artifactId>jmxtools</artifactId> 
                     </exclusion> 
                     <exclusion> 
                           <groupId>com.sun.jmx</groupId> 
                           <artifactId>jmxri</artifactId> 
                     </exclusion> 
               </exclusions> 
         </dependency> 

         <dependency> 
               <groupId>org.apache.storm</groupId> 
               <artifactId>storm-core</artifactId> 
               <version>1.0.2</version> 
               <scope>provided</scope> 
         </dependency> 
   </dependencies> 
   <repositories> 
         <repository> 
               <id>clojars.org</id> 
               <url>http://clojars.org/repo</url> 
         </repository> 
   </repositories> 
  1. 编写一个 Storm Hadoop 拓扑来消费 HDFS 中的数据并将其存储在 HDFS 中。以下是com.stormadvance.storm_hadoop.topology.StormHDFSTopology类的逐行描述:

  2. 使用以下代码行从 Kafka 消费数据:

         // zookeeper hosts for the Kafka cluster 
         BrokerHosts zkHosts = new ZkHosts("localhost:2181"); 

         // Create the KafkaReadSpout configuartion 
         // Second argument is the topic name 
         // Third argument is the zookeeper root for Kafka 
         // Fourth argument is consumer group id 
         SpoutConfig kafkaConfig = new SpoutConfig(zkHosts, "dataTopic", "", 
                     "id7"); 

         // Specify that the kafka messages are String 
         kafkaConfig.scheme = new SchemeAsMultiScheme(new StringScheme()); 

         // We want to consume all the first messages in the topic everytime 
         // we run the topology to help in debugging. In production, this 
         // property should be false 
         kafkaConfig.startOffsetTime = kafka.api.OffsetRequest.EarliestTime(); 

         // Now we create the topology 
         TopologyBuilder builder = new TopologyBuilder(); 

         // set the kafka spout class 
         builder.setSpout("KafkaReadSpout", new KafkaSpout(kafkaConfig), 1); 
  1. 使用以下代码行定义 HDFS Namenode 的详细信息和 HDFS 数据目录的名称,以将数据存储到 HDFS 中,在每存储 5MB 数据块后创建一个新文件,并在每存储 1,000 条记录后将最新数据同步到文件中:
         // use "|" instead of "," for field delimiter 
         RecordFormat format = new DelimitedRecordFormat() 
                     .withFieldDelimiter(","); 

         // sync the filesystem after every 1k tuples 
         SyncPolicy syncPolicy = new CountSyncPolicy(1000); 

         // rotate files when they reach 5MB 
         FileRotationPolicy rotationPolicy = new FileSizeRotationPolicy(5.0f, 
                     Units.MB); 

         FileNameFormat fileNameFormatHDFS = new DefaultFileNameFormat() 
                     .withPath("/hdfs-bolt-output/"); 

         HdfsBolt hdfsBolt2 = new HdfsBolt().withFsUrl("hdfs://127.0.0.1:8020") 
                     .withFileNameFormat(fileNameFormatHDFS) 
                     .withRecordFormat(format).withRotationPolicy(rotationPolicy) 
                     .withSyncPolicy(syncPolicy); 
  1. 使用以下代码将 Spout 连接到 HDFS bolt:
HdfsBolt hdfsBolt2 = new HdfsBolt().withFsUrl("hdfs://127.0.0.1:8020") 
                     .withFileNameFormat(fileNameFormatHDFS) 
                     .withRecordFormat(format).withRotationPolicy(rotationPolicy) 
                     .withSyncPolicy(syncPolicy); 

将 Storm 与 Hadoop 集成

开发和运行大数据应用程序的组织已经部署了 Hadoop 集群的可能性非常高。此外,他们也很可能已经部署了实时流处理应用程序,以配合在 Hadoop 上运行的批处理应用程序。

如果可以利用已部署的 YARN 集群来运行 Storm 拓扑,那将是很好的。这将通过只管理一个集群而不是两个来减少维护的操作成本。

Storm-YARN 是 Yahoo!开发的一个项目,它可以在 YARN 集群上部署 Storm 拓扑。它可以在 YARN 管理的节点上部署 Storm 进程。

以下图表说明了 Storm 进程如何部署在 YARN 上:

图 6:YARN 上的 Storm 进程

在接下来的部分,我们将看到如何设置 Storm-YARN。

设置 Storm-YARN

由于 Storm-YARN 仍处于 alpha 阶段,我们将继续使用git存储库的基础主分支。确保您的系统上已安装了git。如果没有,请运行以下命令:

# yum install git-core  

还要确保您的系统上已安装了 Apache Zookeeper 和 Apache Maven。有关其设置说明,请参考前面的章节。

部署 Storm-YARN 的步骤如下:

  1. 使用以下命令克隆storm-yarn存储库:
$ cd ~/opt
$ git clone https://github.com/yahoo/storm-yarn.git
$ cd storm-yarn  
  1. 通过运行以下mvn命令构建storm-yarn
    $ mvn package
    [INFO] Scanning for projects...
    [INFO] 
    [INFO] ----------------------------------------------------
    [INFO] Building storm-yarn 1.0-alpha
    [INFO] ----------------------------------------------------
    ...
    [INFO] ----------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ----------------------------------------------------
    [INFO] Total time: 32.049s
    [INFO] Finished at: Fri Apr 04 09:45:06 IST 2014
    [INFO] Final Memory: 14M/152M
    [INFO] ----------------------------------------------------

  1. 使用以下命令将storm.zip文件从storm-yarn/lib复制到 HDFS:
$ hdfs dfs -mkdir -p  /lib/storm/1.0.2-wip21
$ hdfs dfs -put lib/storm.zip /lib/storm/1.0.2-wip21/storm.zip  

确切的版本在您的情况下可能与1.0.2-wip21不同。

  1. 创建一个目录来保存我们的 Storm 配置:
$ mkdir -p ~/storm-data
$ cp lib/storm.zip ~/storm-data/
$ cd ~/storm-data/
$ unzip storm.zip  
  1. ~/storm-data/storm-1.0.2-wip21/conf/storm.yaml文件中添加以下配置:
storm.zookeeper.servers: 
     - "localhost" 

nimbus.host: "localhost" 

master.initial-num-supervisors: 2 
master.container.size-mb: 128 

如有需要,根据您的设置更改值。

  1. 通过将以下内容添加到~/.bashrc文件中,将storm-yarn/bin文件夹添加到您的路径中:
export PATH=$PATH:/home/anand/storm-data/storm-1.0.2-wip21/bin:/home/anand/opt/storm-yarn/bin 
  1. 刷新~/.bashrc
$ source ~/.bashrc  
  1. 确保 Zookeeper 在您的系统上运行。如果没有,请运行以下命令启动 ZooKeeper:
$ ~/opt/zookeeper-3.4.5/bin/zkServer.sh start  
  1. 使用以下命令启动storm-yarn
    $ storm-yarn launch ~/storm-data/storm-1.0.2-wip21/conf/storm.yaml 
    14/04/15 10:14:49 INFO client.RMProxy: Connecting to ResourceManager at /0.0.0.0:8032
    14/04/15 10:14:49 INFO yarn.StormOnYarn: Copy App Master jar from local filesystem and add to local environment
    ... ... 
    14/04/15 10:14:51 INFO impl.YarnClientImpl: Submitted application application_1397537047058_0001 to ResourceManager at /0.0.0.0:8032
    application_1397537047058_0001

Storm-YARN 应用程序已经提交,应用程序 ID 为application_1397537047058_0001

  1. 我们可以使用以下yarn命令检索应用程序的状态:
    $ yarn application -list
    14/04/15 10:23:13 INFO client.RMProxy: Connecting to ResourceManager at /0.0.0.0:8032
    Total number of applications (application-types: [] and states: [SUBMITTED, ACCEPTED, RUNNING]):1
                    Application-Id          Application-Name        Application-Type          User       Queue               State             Final-State             Progress                          Tracking-URL
    application_1397537047058_0001             Storm-on-Yarn                    YARN         anand    default             RUNNING               UNDEFINED                  50%                                   N/A

  1. 我们还可以在 ResourceManager web UI 上看到storm-yarn运行在http://localhost:8088/cluster/。您应该能够看到类似以下内容:

图 7:ResourceManager web UI 上的 Storm-YARN

您可以通过单击 UI 上的各种链接来探索各种公开的指标。

  1. Nimbus 现在也应该在运行中,您应该能够通过 Nimbus web UI 看到它,网址为http://localhost:7070/

图 8:Nimbus web UI 在 YARN 上运行

  1. 现在我们需要获取将在 YARN 上的 Storm 集群上部署拓扑时使用的 Storm 配置。为此,请执行以下命令:
    $ mkdir ~/.storm
    $ storm-yarn getStormConfig --appId application_1397537047058_0001 --output ~/.storm/storm.yaml
    14/04/15 10:32:01 INFO client.RMProxy: Connecting to ResourceManager at /0.0.0.0:8032
    14/04/15 10:32:02 INFO yarn.StormOnYarn: application report for application_1397537047058_0001 :localhost.localdomain:9000
    14/04/15 10:32:02 INFO yarn.StormOnYarn: Attaching to localhost.localdomain:9000 to talk to app master application_1397537047058_0001
    14/04/15 10:32:02 INFO yarn.StormMasterCommand: storm.yaml downloaded into /home/anand/.storm/storm.yaml  

确保将正确的应用程序 ID(在第 9 步中检索)传递给-appId参数。

现在我们已经成功部署了 Storm-YARN,我们将看到如何在这个 storm 集群上运行我们的拓扑。

在 Storm-YARN 上运行 Storm-Starter 拓扑

在本节中,我们将看到如何在storm-yarn上部署 Storm-Starter 拓扑。Storm-Starter 是一组随 Storm 一起提供的示例拓扑。

按照以下步骤在 Storm-YARN 上运行拓扑:

  1. 克隆storm-starter项目:
$ git clone https://github.com/nathanmarz/storm-starter
$ cd storm-starter  
  1. 使用以下mvn命令打包拓扑:
$ mvn package -DskipTests  
  1. 使用以下命令在storm-yarn上部署拓扑:
    $ storm jar target/storm-starter-0.0.1-SNAPSHOT.jar storm.starter.WordCountTopology word-cout-topology
    545  [main] INFO  backtype.storm.StormSubmitter - Jar not uploaded to master yet. Submitting jar...
    558  [main] INFO  backtype.storm.StormSubmitter - Uploading topology jar target/storm-starter-0.0.1-SNAPSHOT.jar to assigned location: storm-local/nimbus/inbox/stormjar-9ab704ff-29f3-4b9d-b0ac-e9e41d4399dd.jar
    609  [main] INFO  backtype.storm.StormSubmitter - Successfully uploaded topology jar to assigned location: storm-local/nimbus/inbox/stormjar-9ab704ff-29f3-4b9d-b0ac-e9e41d4399dd.jar
    609  [main] INFO  backtype.storm.StormSubmitter - Submitting topology word-cout-topology in distributed mode with conf {"topology.workers":3,"topology.debug":true}
    937  [main] INFO  backtype.storm.StormSubmitter - Finished submitting topology: word-cout-topology

  1. 现在我们可以在 Nimbus web UI 上看到部署的拓扑,网址为http://localhost:7070/

图 9:Nimbus web UI 显示了 YARN 上的单词计数拓扑

  1. 要查看如何与在storm-yarn上运行的拓扑进行交互,请运行以下命令:
$ storm-yarn  
  1. 它将列出与各种 Storm 进程交互和启动新监督者的所有选项。

因此,在本节中,我们构建了一个 Storm-started 拓扑,并在storm-yarn上运行它。

摘要

在本章中,我们介绍了 Apache Hadoop 以及 HDFS、YARN 等各种组件,这些组件是 Hadoop 集群的一部分。我们还看到了 HDFS 和 YARN 集群的子组件以及它们之间的交互。然后我们演示了如何设置单节点 Hadoop 集群。

我们还介绍了 Storm-YARN,这是本章的重点。Storm-YARN 使您能够在 Hadoop 集群上运行 Storm 拓扑。从可管理性和运维角度来看,这对我们很有帮助。最后,我们看到了如何在 YARN 上运行的 Storm 上部署拓扑。

在下一章中,我们将看到 Storm 如何与其他大数据技术(如 HBase、Redis 等)集成。

第十章:Storm 与 Redis、Elasticsearch 和 HBase 的集成

在上一章中,我们介绍了 Apache Hadoop 及其各个组件的概述。我们还介绍了 Storm-YARN 的概述,并介绍了如何在 Apache Hadoop 上部署 Storm-YARN。

在本章中,我们将解释如何将 Storm 与其他数据库集成以存储数据,以及如何在 Storm bolt 中使用 Esper 来支持窗口操作。

以下是本章将要涵盖的关键点:

  • 将 Storm 与 HBase 集成

  • 将 Storm 与 Redis 集成

  • 将 Storm 与 Elasticsearch 集成

  • 将 Storm 与 Esper 集成以执行窗口操作

将 Storm 与 HBase 集成

如前几章所述,Storm 用于实时数据处理。然而,在大多数情况下,您需要将处理后的数据存储在数据存储中,以便将存储的数据用于进一步的批量分析,并在存储的数据上执行批量分析查询。本节解释了如何将 Storm 处理的数据存储在 HBase 中。

在实施之前,我想简要介绍一下 HBase 是什么。HBase 是一个 NoSQL、多维、稀疏、水平可扩展的数据库,模型类似于Google BigTable。HBase 建立在 Hadoop 之上,这意味着它依赖于 Hadoop,并与 MapReduce 框架很好地集成。Hadoop 为 HBase 提供以下好处:

  • 在通用硬件上运行的分布式数据存储

  • 容错

我们假设您已经在系统上安装并运行了 HBase。您可以参考HBase 安装文章

我们将创建一个示例 Storm 拓扑,演示如何使用以下步骤将 Storm 处理的数据存储到 HBase:

  1. 使用com.stormadvance作为组 ID 和stormhbase作为 artifact ID 创建一个 Maven 项目。

  2. 将以下依赖项和存储库添加到pom.xml文件中:

    <repositories> 
        <repository> 
            <id>clojars.org</id> 
            <url>http://clojars.org/repo</url> 
        </repository> 
    </repositories> 
    <dependencies> 
        <dependency> 
            <groupId>org.apache.storm</groupId> 
            <artifactId>storm-core</artifactId> 
            <version>1.0.2</version> 
            <scope>provided</scope> 
        </dependency> 
        <dependency> 
            <groupId>org.apache.hadoop</groupId> 
            <artifactId>hadoop-core</artifactId> 
            <version>1.1.1</version> 
        </dependency> 
        <dependency> 
            <groupId>org.slf4j</groupId> 
            <artifactId>slf4j-api</artifactId> 
            <version>1.7.7</version> 
        </dependency> 

        <dependency> 
            <groupId>org.apache.hbase</groupId> 
            <artifactId>hbase</artifactId> 
            <version>0.94.5</version> 
            <exclusions> 
                <exclusion> 
                    <artifactId>zookeeper</artifactId> 
                    <groupId>org.apache.zookeeper</groupId> 
                </exclusion> 

            </exclusions> 
        </dependency> 

        <dependency> 
            <groupId>junit</groupId> 
            <artifactId>junit</artifactId> 
            <version>4.10</version> 
        </dependency> 
    </dependencies> 
    <build> 
        <plugins> 
            <plugin> 
                <groupId>org.apache.maven.plugins</groupId> 
                <artifactId>maven-compiler-plugin</artifactId> 
                <version>2.5.1</version> 
                <configuration> 
                    <source>1.6</source> 
                    <target>1.6</target> 
                </configuration> 
            </plugin> 
            <plugin> 
                <artifactId>maven-assembly-plugin</artifactId> 
                <version>2.2.1</version> 
                <configuration> 
                    <descriptorRefs> 
                        <descriptorRef>jar-
                        with-dependencies</descriptorRef> 
                    </descriptorRefs> 
                    <archive> 
                        <manifest> 
                            <mainClass /> 
                        </manifest> 
                    </archive> 
                </configuration> 
                <executions> 
                    <execution> 
                        <id>make-assembly</id> 
                        <phase>package</phase> 
                        <goals> 
                            <goal>single</goal> 
                        </goals> 
                    </execution> 
                </executions> 
            </plugin> 
        </plugins> 
    </build> 
  1. com.stormadvance.stormhbase包中创建一个HBaseOperations类。HBaseOperations类包含两个方法:
  • createTable(String tableName, List<String> ColumnFamilies): 此方法将表名和 HBase 列族列表作为输入,以在 HBase 中创建表。

  • insert(Map<String, Map<String, Object>> record, String rowId): 此方法将记录及其rowID参数作为输入,并将输入记录插入 HBase。以下是输入记录的结构:

{  

  "columnfamily1":  
  {  
    "column1":"abc",  
    "column2":"pqr"  
  },  
  "columnfamily2":  
  {  
    "column3":"bc",  
    "column4":"jkl"  
  }  
}  

这里,columnfamily1columnfamily2是 HBase 列族的名称,column1column2column3column4是列的名称。

rowId参数是 HBase 表行键,用于唯一标识 HBase 中的每条记录。

HBaseOperations类的源代码如下:

public class HBaseOperations implements Serializable{ 

    private static final long serialVersionUID = 1L; 

    // Instance of Hadoop Cofiguration class 
    Configuration conf = new Configuration(); 

    HTable hTable = null; 

    public HBaseOperations(String tableName, List<String> ColumnFamilies, 
            List<String> zookeeperIPs, int zkPort) { 
        conf = HBaseConfiguration.create(); 
        StringBuffer zookeeperIP = new StringBuffer(); 
        // Set the zookeeper nodes 
        for (String zookeeper : zookeeperIPs) { 
            zookeeperIP.append(zookeeper).append(","); 
        } 
        zookeeperIP.deleteCharAt(zookeeperIP.length() - 1); 

        conf.set("hbase.zookeeper.quorum", zookeeperIP.toString()); 

        // Set the zookeeper client port 
        conf.setInt("hbase.zookeeper.property.clientPort", zkPort); 
        // call the createTable method to create a table into HBase. 
        createTable(tableName, ColumnFamilies); 
        try { 
            // initilaize the HTable.  
            hTable = new HTable(conf, tableName); 
        } catch (IOException e) { 
            throw new RuntimeException("Error occure while creating instance of HTable class : " + e); 
        } 
    } 

    /** 
     * This method create a table into HBase 
     *  
     * @param tableName 
     *            Name of the HBase table 
     * @param ColumnFamilies 
     *            List of column famallies 
     *  
     */ 
    public void createTable(String tableName, List<String> ColumnFamilies) { 
        HBaseAdmin admin = null; 
        try { 
            admin = new HBaseAdmin(conf); 
            // Set the input table in HTableDescriptor 
            HTableDescriptor tableDescriptor = new HTableDescriptor( 
                    Bytes.toBytes(tableName)); 
            for (String columnFamaliy : ColumnFamilies) { 
                HColumnDescriptor columnDescriptor = new HColumnDescriptor( 
                        columnFamaliy); 
                // add all the HColumnDescriptor into HTableDescriptor 
                tableDescriptor.addFamily(columnDescriptor); 
            } 
            /* execute the creaetTable(HTableDescriptor tableDescriptor) of HBaseAdmin 
             * class to createTable into HBase. 
            */  
            admin.createTable(tableDescriptor); 
            admin.close(); 

        }catch (TableExistsException tableExistsException) { 
            System.out.println("Table already exist : " + tableName); 
            if(admin != null) { 
                try { 
                admin.close();  
                } catch (IOException ioException) { 
                    System.out.println("Error occure while closing the HBaseAdmin connection : " + ioException); 
                } 
            } 

        }catch (MasterNotRunningException e) { 
            throw new RuntimeException("HBase master not running, table creation failed : "); 
        } catch (ZooKeeperConnectionException e) { 
            throw new RuntimeException("Zookeeper not running, table creation failed : "); 
        } catch (IOException e) { 
            throw new RuntimeException("IO error, table creation failed : "); 
        } 
    } 

    /** 
     * This method insert the input record into HBase. 
     *  
     * @param record 
     *            input record 
     * @param rowId 
     *            unique id to identify each record uniquely. 
     */ 
    public void insert(Map<String, Map<String, Object>> record, String rowId) { 
        try { 
        Put put = new Put(Bytes.toBytes(rowId));         
        for (String cf : record.keySet()) { 
            for (String column: record.get(cf).keySet()) { 
                put.add(Bytes.toBytes(cf), Bytes.toBytes(column), Bytes.toBytes(record.get(cf).get(column).toString())); 
            }  
        } 
        hTable.put(put); 
        }catch (Exception e) { 
            throw new RuntimeException("Error occure while storing record into HBase"); 
        } 

    } 

    public static void main(String[] args) { 
        List<String> cFs = new ArrayList<String>(); 
        cFs.add("cf1"); 
        cFs.add("cf2"); 

        List<String> zks = new ArrayList<String>(); 
        zks.add("192.168.41.122"); 
        Map<String, Map<String, Object>> record = new HashMap<String, Map<String,Object>>(); 

        Map<String, Object> cf1 = new HashMap<String, Object>(); 
        cf1.put("aa", "1"); 

        Map<String, Object> cf2 = new HashMap<String, Object>(); 
        cf2.put("bb", "1"); 

        record.put("cf1", cf1); 
        record.put("cf2", cf2); 

        HBaseOperations hbaseOperations = new HBaseOperations("tableName", cFs, zks, 2181); 
        hbaseOperations.insert(record, UUID.randomUUID().toString()); 

    } 
} 
  1. com.stormadvance.stormhbase包中创建一个SampleSpout类。此类生成随机记录并将其传递给拓扑中的下一个操作(bolt)。以下是SampleSpout类生成的记录的格式:
["john","watson","abc"]  

SampleSpout类的源代码如下:

public class SampleSpout extends BaseRichSpout { 
    private static final long serialVersionUID = 1L; 
    private SpoutOutputCollector spoutOutputCollector; 

    private static final Map<Integer, String> FIRSTNAMEMAP = new HashMap<Integer, String>(); 
    static { 
        FIRSTNAMEMAP.put(0, "john"); 
        FIRSTNAMEMAP.put(1, "nick"); 
        FIRSTNAMEMAP.put(2, "mick"); 
        FIRSTNAMEMAP.put(3, "tom"); 
        FIRSTNAMEMAP.put(4, "jerry"); 
    } 

    private static final Map<Integer, String> LASTNAME = new HashMap<Integer, String>(); 
    static { 
        LASTNAME.put(0, "anderson"); 
        LASTNAME.put(1, "watson"); 
        LASTNAME.put(2, "ponting"); 
        LASTNAME.put(3, "dravid"); 
        LASTNAME.put(4, "lara"); 
    } 

    private static final Map<Integer, String> COMPANYNAME = new HashMap<Integer, String>(); 
    static { 
        COMPANYNAME.put(0, "abc"); 
        COMPANYNAME.put(1, "dfg"); 
        COMPANYNAME.put(2, "pqr"); 
        COMPANYNAME.put(3, "ecd"); 
        COMPANYNAME.put(4, "awe"); 
    } 

    public void open(Map conf, TopologyContext context, 
            SpoutOutputCollector spoutOutputCollector) { 
        // Open the spout 
        this.spoutOutputCollector = spoutOutputCollector; 
    } 

    public void nextTuple() { 
        // Storm cluster repeatedly call this method to emit the continuous // 
        // stream of tuples. 
        final Random rand = new Random(); 
        // generate the random number from 0 to 4\. 
        int randomNumber = rand.nextInt(5); 
        spoutOutputCollector.emit (new Values(FIRSTNAMEMAP.get(randomNumber),LASTNAME.get(randomNumber),COMPANYNAME.get(randomNumber))); 
    } 

    public void declareOutputFields(OutputFieldsDeclarer declarer) { 
        // emits the field  firstName , lastName and companyName. 
        declarer.declare(new Fields("firstName","lastName","companyName")); 
    } 
} 

  1. com.stormadvance.stormhbase包中创建一个StormHBaseBolt类。此 bolt 接收SampleSpout发出的元组,然后调用HBaseOperations类的insert()方法将记录插入 HBase。StormHBaseBolt类的源代码如下:
public class StormHBaseBolt implements IBasicBolt { 

    private static final long serialVersionUID = 2L; 
    private HBaseOperations hbaseOperations; 
    private String tableName; 
    private List<String> columnFamilies; 
    private List<String> zookeeperIPs; 
    private int zkPort; 
    /** 
     * Constructor of StormHBaseBolt class 
     *  
     * @param tableName 
     *            HBaseTableNam 
     * @param columnFamilies 
     *            List of column families 
     * @param zookeeperIPs 
     *            List of zookeeper nodes 
     * @param zkPort 
     *            Zookeeper client port 
     */ 
    public StormHBaseBolt(String tableName, List<String> columnFamilies, 
            List<String> zookeeperIPs, int zkPort) { 
        this.tableName =tableName; 
        this.columnFamilies = columnFamilies; 
        this.zookeeperIPs = zookeeperIPs; 
        this.zkPort = zkPort; 

    } 

    public void execute(Tuple input, BasicOutputCollector collector) { 
        Map<String, Map<String, Object>> record = new HashMap<String, Map<String, Object>>(); 
        Map<String, Object> personalMap = new HashMap<String, Object>(); 
        // "firstName","lastName","companyName") 
        personalMap.put("firstName", input.getValueByField("firstName")); 
        personalMap.put("lastName", input.getValueByField("lastName")); 

        Map<String, Object> companyMap = new HashMap<String, Object>(); 
        companyMap.put("companyName", input.getValueByField("companyName")); 

        record.put("personal", personalMap); 
        record.put("company", companyMap); 
        // call the inset method of HBaseOperations class to insert record into 
        // HBase 
        hbaseOperations.insert(record, UUID.randomUUID().toString()); 
    } 

    public void declareOutputFields(OutputFieldsDeclarer declarer) { 

    } 

    public Map<String, Object> getComponentConfiguration() { 
        // TODO Auto-generated method stub 
        return null; 
    } 

    public void prepare(Map stormConf, TopologyContext context) { 
        // create the instance of HBaseOperations class 
        hbaseOperations = new HBaseOperations(tableName, columnFamilies, 
                zookeeperIPs, zkPort); 
    } 

    public void cleanup() { 
        // TODO Auto-generated method stub 

    } 

} 

StormHBaseBolt类的构造函数以 HBase 表名、列族列表、ZooKeeper IP 地址和 ZooKeeper 端口作为参数,并设置类级变量。StormHBaseBolt类的prepare()方法将创建HBaseOperatons类的实例。

StormHBaseBolt类的execute()方法以输入元组作为参数,并将其转换为 HBase 结构格式。它还使用java.util.UUID类生成 HBase 行 ID。

  1. com.stormadvance.stormhbase包中创建一个Topology类。这个类创建spoutbolt类的实例,并使用TopologyBuilder类将它们链接在一起。以下是主类的实现:
public class Topology {
    public static void main(String[] args) throws AlreadyAliveException, 
            InvalidTopologyException { 
        TopologyBuilder builder = new TopologyBuilder(); 

        List<String> zks = new ArrayList<String>(); 
        zks.add("127.0.0.1"); 

        List<String> cFs = new ArrayList<String>(); 
        cFs.add("personal"); 
        cFs.add("company"); 

        // set the spout class 
        builder.setSpout("spout", new SampleSpout(), 2); 
        // set the bolt class 
        builder.setBolt("bolt", new StormHBaseBolt("user", cFs, zks, 2181), 2) 
                .shuffleGrouping("spout"); 
        Config conf = new Config(); 
        conf.setDebug(true); 
        // create an instance of LocalCluster class for 
        // executing topology in local mode. 
        LocalCluster cluster = new LocalCluster(); 

        // LearningStormTopolgy is the name of submitted topology. 
        cluster.submitTopology("StormHBaseTopology", conf, 
                builder.createTopology()); 
        try { 
            Thread.sleep(60000); 
        } catch (Exception exception) { 
            System.out.println("Thread interrupted exception : " + exception); 
        } 
        System.out.println("Stopped Called : "); 
        // kill the LearningStormTopology 
        cluster.killTopology("StormHBaseTopology"); 
        // shutdown the storm test cluster 
        cluster.shutdown(); 

    } 
} 

在本节中,我们介绍了如何将 Storm 与 NoSQL 数据库 HBase 集成。在下一节中,我们将介绍如何将 Storm 与 Redis 集成。

将 Storm 与 Redis 集成

Redis 是一个键值数据存储。键值可以是字符串、列表、集合、哈希等。它非常快,因为整个数据集存储在内存中。以下是安装 Redis 的步骤:

  1. 首先,您需要安装makegcccc来编译 Redis 代码,使用以下命令:
    sudo yum -y install make gcc cc
  1. 下载、解压并制作 Redis,并使用以下命令将其复制到/usr/local/bin
    cd /home/$USER 
    Here, $USER is the name of the Linux user. 
    http://download.redis.io/releases/redis-2.6.16.tar.gz 
    tar -xvf redis-2.6.16.tar.gz 
    cd redis-2.6.16 
    make 
    sudo cp src/redis-server /usr/local/bin 
    sudo cp src/redis-cli /usr/local/bin
  1. 执行以下命令将 Redis 设置为服务:
    sudo mkdir -p /etc/redis 
    sudo mkdir -p /var/redis 
    cd /home/$USER/redis-2.6.16/ 
    sudo cp utils/redis_init_script /etc/init.d/redis 
    wget https://bitbucket.org/ptylr/public-stuff/raw/41d5c8e87ce6adb3 
    4aa16cd571c3f04fb4d5e7ac/etc/init.d/redis 
    sudo cp redis /etc/init.d/redis 
    cd /home/$USER/redis-2.6.16/ 
    sudo cp redis.conf /etc/redis/redis.conf
  1. 现在,运行以下命令将服务添加到chkconfig,设置为自动启动,并实际启动服务:
    chkconfig --add redis 
    chkconfig redis on 
    service redis start
  1. 使用以下命令检查 Redis 的安装情况:
    redis-cli ping

如果测试命令的结果是PONG,则安装已成功。

我们假设您已经启动并运行了 Redis 服务。

接下来,我们将创建一个示例 Storm 拓扑,以解释如何将 Storm 处理的数据存储在 Redis 中。

  1. 使用com.stormadvance作为groupIDstormredis作为artifactID创建一个 Maven 项目。

  2. pom.xml文件中添加以下依赖和存储库:

<repositories> 
        <repository> 
            <id>central</id> 
            <name>Maven Central</name> 
            <url>http://repo1.maven.org/maven2/</url> 
        </repository> 
        <repository> 
            <id>cloudera-repo</id> 
            <name>Cloudera CDH</name> 
            <url>https://repository.cloudera.com/artifactory/cloudera-
            repos/</url> 
        </repository> 
        <repository> 
            <id>clojars.org</id> 
            <url>http://clojars.org/repo</url> 
        </repository> 
    </repositories> 
    <dependencies> 
        <dependency> 
            <groupId>storm</groupId> 
            <artifactId>storm</artifactId> 
            <version>0.9.0.1</version> 
        </dependency> 
                <dependency> 
            <groupId>com.fasterxml.jackson.core</groupId> 
            <artifactId>jackson-core</artifactId> 
            <version>2.1.1</version> 
        </dependency> 

        <dependency> 
            <groupId>com.fasterxml.jackson.core</groupId> 
            <artifactId>jackson-databind</artifactId> 
            <version>2.1.1</version> 
        </dependency> 
        <dependency> 
            <groupId>junit</groupId> 
            <artifactId>junit</artifactId> 
            <version>3.8.1</version> 
            <scope>test</scope> 
        </dependency> 
        <dependency> 
            <groupId>redis.clients</groupId> 
            <artifactId>jedis</artifactId> 
            <version>2.4.2</version> 
        </dependency> 
    </dependencies> 
  1. com.stormadvance.stormredis包中创建一个RedisOperations类。RedisOperations类包含以下方法:
  • insert(Map<String, Object> record, String id): 此方法接受记录和 ID 作为输入,并将输入记录插入 Redis。在insert()方法中,我们将首先使用 Jackson 库将记录序列化为字符串,然后将序列化记录存储到 Redis 中。每个记录必须具有唯一的 ID,因为它用于从 Redis 中检索记录。

以下是RedisOperations类的源代码:

public class RedisOperations implements Serializable { 

    private static final long serialVersionUID = 1L; 
    Jedis jedis = null; 

    public RedisOperations(String redisIP, int port) { 
        // Connecting to Redis on localhost 
        jedis = new Jedis(redisIP, port); 
    } 

    public void insert(Map<String, Object> record, String id) { 
        try { 
            jedis.set(id, new ObjectMapper().writeValueAsString(record)); 
        } catch (Exception e) { 
            System.out.println("Record not persist into datastore : "); 
        } 
    } 
} 

我们将使用在将 Storm 与 HBase 集成部分中创建的相同的SampleSpout类。

  1. com.stormadvance.stormredis包中创建一个StormRedisBolt类。这个 bolt 接收SampleSpout类发出的元组,将它们转换为 Redis 结构,然后调用RedisOperations类的insert()方法将记录插入 Redis。以下是StormRedisBolt类的源代码:
    public class StormRedisBolt implements IBasicBolt{ 

    private static final long serialVersionUID = 2L; 
    private RedisOperations redisOperations = null; 
    private String redisIP = null; 
    private int port; 
    public StormRedisBolt(String redisIP, int port) { 
        this.redisIP = redisIP; 
        this.port = port; 
    } 

    public void execute(Tuple input, BasicOutputCollector collector) { 
        Map<String, Object> record = new HashMap<String, Object>(); 
        //"firstName","lastName","companyName") 
        record.put("firstName", input.getValueByField("firstName")); 
        record.put("lastName", input.getValueByField("lastName")); 
        record.put("companyName", input.getValueByField("companyName")); 
        redisOperations.insert(record, UUID.randomUUID().toString()); 
    } 

    public void declareOutputFields(OutputFieldsDeclarer declarer) { 

    } 

    public Map<String, Object> getComponentConfiguration() { 
        return null; 
    } 

    public void prepare(Map stormConf, TopologyContext context) { 
        redisOperations = new RedisOperations(this.redisIP, this.port); 
    } 

    public void cleanup() { 

    } 

} 

StormRedisBolt类中,我们使用java.util.UUID类生成 Redis 键。

  1. com.stormadvance.stormredis包中创建一个Topology类。这个类创建spoutbolt类的实例,并使用TopologyBuilder类将它们链接在一起。以下是主类的实现:
public class Topology { 
    public static void main(String[] args) throws AlreadyAliveException, 
            InvalidTopologyException { 
        TopologyBuilder builder = new TopologyBuilder(); 

        List<String> zks = new ArrayList<String>(); 
        zks.add("192.168.41.122"); 

        List<String> cFs = new ArrayList<String>(); 
        cFs.add("personal"); 
        cFs.add("company"); 

        // set the spout class 
        builder.setSpout("spout", new SampleSpout(), 2); 
        // set the bolt class 
        builder.setBolt("bolt", new StormRedisBolt("192.168.41.122",2181), 2).shuffleGrouping("spout"); 

        Config conf = new Config(); 
        conf.setDebug(true); 
        // create an instance of LocalCluster class for 
        // executing topology in local mode. 
        LocalCluster cluster = new LocalCluster(); 

        // LearningStormTopolgy is the name of submitted topology. 
        cluster.submitTopology("StormRedisTopology", conf, 
                builder.createTopology()); 
        try { 
            Thread.sleep(10000); 
        } catch (Exception exception) { 
            System.out.println("Thread interrupted exception : " + exception); 
        } 
        // kill the LearningStormTopology 
        cluster.killTopology("StormRedisTopology"); 
        // shutdown the storm test cluster 
        cluster.shutdown(); 
} 
} 

在本节中,我们介绍了 Redis 的安装以及如何将 Storm 与 Redis 集成。

将 Storm 与 Elasticsearch 集成

在本节中,我们将介绍如何将 Storm 与 Elasticsearch 集成。Elasticsearch 是一个基于 Lucene 开发的开源分布式搜索引擎平台。它提供了多租户能力、全文搜索引擎功能。

我们假设 Elasticsearch 正在您的环境中运行。如果您没有任何正在运行的 Elasticsearch 集群,请参考www.elastic.co/guide/en/elasticsearch/reference/2.3/_installation.html在任何一个框中安装 Elasticsearch。按照以下步骤将 Storm 与 Elasticsearch 集成:

  1. 使用com.stormadvance作为groupIDstorm_elasticsearch作为artifactID创建一个 Maven 项目。

  2. pom.xml文件中添加以下依赖和存储库:

<dependencies> 
        <dependency> 
            <groupId>org.elasticsearch</groupId> 
            <artifactId>elasticsearch</artifactId> 
            <version>2.4.4</version> 
        </dependency> 
        <dependency> 
            <groupId>junit</groupId> 
            <artifactId>junit</artifactId> 
            <version>3.8.1</version> 
            <scope>test</scope> 
        </dependency> 
        <dependency> 
            <groupId>org.apache.storm</groupId> 
            <artifactId>storm-core</artifactId> 
            <version>1.0.2</version> 
            <scope>provided</scope> 
        </dependency> 
    </dependencies> 
  1. com.stormadvance.storm_elasticsearch包中创建一个ElasticSearchOperation类。ElasticSearchOperation类包含以下方法:
  • insert(Map<String, Object> data, String indexName, String indexMapping, String indexId): 这个方法以记录数据、indexNameindexMappingindexId作为输入,并将输入记录插入 Elasticsearch。

以下是ElasticSearchOperation类的源代码:

public class ElasticSearchOperation { 

    private TransportClient client; 

    public ElasticSearchOperation(List<String> esNodes) throws Exception { 
        try { 
            Settings settings = Settings.settingsBuilder() 
                    .put("cluster.name", "elasticsearch").build(); 
            client = TransportClient.builder().settings(settings).build(); 
            for (String esNode : esNodes) { 
                client.addTransportAddress(new InetSocketTransportAddress( 
                        InetAddress.getByName(esNode), 9300)); 
            } 

        } catch (Exception e) { 
            throw e; 
        } 

    } 

    public void insert(Map<String, Object> data, String indexName, String indexMapping, String indexId) { 
        client.prepareIndex(indexName, indexMapping, indexId) 
                .setSource(data).get(); 
    } 

    public static void main(String[] s){ 
        try{ 
            List<String> esNodes = new ArrayList<String>(); 
            esNodes.add("127.0.0.1"); 
            ElasticSearchOperation elasticSearchOperation  = new ElasticSearchOperation(esNodes); 
            Map<String, Object> data = new HashMap<String, Object>(); 
            data.put("name", "name"); 
            data.put("add", "add"); 
            elasticSearchOperation.insert(data,"indexName","indexMapping",UUID.randomUUID().toString()); 
        }catch(Exception e) { 
            e.printStackTrace(); 
            //System.out.println(e); 
        } 
    } 

} 

我们将使用在将 Storm 与 HBase 集成部分中创建的相同的SampleSpout类。

  1. com.stormadvance.storm_elasticsearch包中创建一个ESBolt类。这个 bolt 接收SampleSpout类发出的元组,将其转换为Map结构,然后调用ElasticSearchOperation类的insert()方法将记录插入 Elasticsearch。以下是ESBolt类的源代码:
public class ESBolt implements IBasicBolt { 

    private static final long serialVersionUID = 2L; 
    private ElasticSearchOperation elasticSearchOperation; 
    private List<String> esNodes; 

    /** 
     *  
     * @param esNodes 
     */ 
    public ESBolt(List<String> esNodes) { 
        this.esNodes = esNodes; 

    } 

    public void execute(Tuple input, BasicOutputCollector collector) { 
        Map<String, Object> personalMap = new HashMap<String, Object>(); 
        // "firstName","lastName","companyName") 
        personalMap.put("firstName", input.getValueByField("firstName")); 
        personalMap.put("lastName", input.getValueByField("lastName")); 

        personalMap.put("companyName", input.getValueByField("companyName")); 
        elasticSearchOperation.insert(personalMap,"person","personmapping",UUID.randomUUID().toString()); 
    } 

    public void declareOutputFields(OutputFieldsDeclarer declarer) { 

    } 

    public Map<String, Object> getComponentConfiguration() { 
        // TODO Auto-generated method stub 
        return null; 
    } 

    public void prepare(Map stormConf, TopologyContext context) { 
        try { 
            // create the instance of ESOperations class 
            elasticSearchOperation = new ElasticSearchOperation(esNodes); 
        } catch (Exception e) { 
            throw new RuntimeException(); 
        } 
    } 

    public void cleanup() { 

    } 

} 
  1. com.stormadvance.storm_elasticsearch包中创建一个ESTopology类。这个类创建了spoutbolt类的实例,并使用TopologyBuilder类将它们链接在一起。以下是主类的实现:
public class ESTopology {
    public static void main(String[] args) throws AlreadyAliveException, 
            InvalidTopologyException { 
        TopologyBuilder builder = new TopologyBuilder(); 

        //ES Node list 
        List<String> esNodes = new ArrayList<String>(); 
        esNodes.add("10.191.209.14"); 

        // set the spout class 
        builder.setSpout("spout", new SampleSpout(), 2); 
        // set the ES bolt class 
        builder.setBolt("bolt", new ESBolt(esNodes), 2) 
                .shuffleGrouping("spout"); 
        Config conf = new Config(); 
        conf.setDebug(true); 
        // create an instance of LocalCluster class for 
        // executing topology in local mode. 
        LocalCluster cluster = new LocalCluster(); 

        // ESTopology is the name of submitted topology. 
        cluster.submitTopology("ESTopology", conf, 
                builder.createTopology()); 
        try { 
            Thread.sleep(60000); 
        } catch (Exception exception) { 
            System.out.println("Thread interrupted exception : " + exception); 
        } 
        System.out.println("Stopped Called : "); 
        // kill the LearningStormTopology 
        cluster.killTopology("StormHBaseTopology"); 
        // shutdown the storm test cluster 
        cluster.shutdown(); 

    } 
} 

在本节中,我们介绍了如何通过在 Storm bolts 内部与 Elasticsearch 节点建立连接来将数据存储到 Elasticsearch 中。

将 Storm 与 Esper 集成

在本节中,我们将介绍如何在 Storm 中使用 Esper 进行窗口操作。Esper 是一个用于复杂事件处理CEP)的开源事件序列分析和事件关联引擎。

请参阅www.espertech.com/products/esper.php了解更多关于 Esper 的详细信息。按照以下步骤将 Storm 与 Esper 集成:

  1. 使用com.stormadvance作为groupIDstorm_esper作为artifactID创建一个 Maven 项目。

  2. pom.xml文件中添加以下依赖项和存储库:

    <dependencies>
        <dependency>
            <groupId>com.espertech</groupId>
            <artifactId>esper</artifactId>
            <version>5.3.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.storm</groupId>
            <artifactId>storm-core</artifactId>
            <version>1.0.2</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
  1. com.stormadvance.storm_elasticsearch包中创建一个EsperOperation类。EsperOperation类包含以下方法:
  • esperPut(Stock stock): 这个方法以股票 bean 作为输入,将事件发送给 Esper 监听器。

EsperOperation类的构造函数初始化了 Esper 监听器并设置了 Esper 查询。Esper 查询在 5 分钟内缓冲事件并返回每个产品在 5 分钟窗口期内的总销售额。在这里,我们使用了固定批处理窗口。

以下是EsperOperation类的源代码:

public class EsperOperation { 

    private EPRuntime cepRT = null; 

    public EsperOperation() { 
        Configuration cepConfig = new Configuration(); 
        cepConfig.addEventType("StockTick", Stock.class.getName()); 
        EPServiceProvider cep = EPServiceProviderManager.getProvider( 
                "myCEPEngine", cepConfig); 
        cepRT = cep.getEPRuntime(); 

        EPAdministrator cepAdm = cep.getEPAdministrator(); 
        EPStatement cepStatement = cepAdm 
                .createEPL("select sum(price),product from " 
                        + "StockTick.win:time_batch(5 sec) " 
                        + "group by product"); 

        cepStatement.addListener(new CEPListener()); 
    } 

    public static class CEPListener implements UpdateListener { 

        public void update(EventBean[] newData, EventBean[] oldData) { 
            try { 
                System.out.println("#################### Event received: 
                "+newData); 
                for (EventBean eventBean : newData) { 
                    System.out.println("************************ Event 
                     received 1: " + eventBean.getUnderlying()); 
                } 

            } catch (Exception e) { 
                e.printStackTrace(); 
                System.out.println(e); 
            } 
        } 
    } 

    public void esperPut(Stock stock) { 
        cepRT.sendEvent(stock); 
    } 

    private static Random generator = new Random(); 

    public static void main(String[] s) throws InterruptedException { 
        EsperOperation esperOperation = new EsperOperation(); 
        // We generate a few ticks... 
        for (int i = 0; i < 5; i++) { 
            double price = (double) generator.nextInt(10); 
            long timeStamp = System.currentTimeMillis(); 
            String product = "AAPL"; 
            Stock stock = new Stock(product, price, timeStamp); 
            System.out.println("Sending tick:" + stock); 
            esperOperation.esperPut(stock); 
        } 
        Thread.sleep(200000); 
    } 

} 
  1. com.stormadvance.storm_esper包中创建一个SampleSpout类。这个类生成随机记录并将它们传递给拓扑中的下一个操作(bolt)。以下是SampleSpout类生成的记录的格式:
    ["product type","price","sale date"] 

以下是SampleSpout类的源代码:

public class SampleSpout extends BaseRichSpout { 
    private static final long serialVersionUID = 1L; 
    private SpoutOutputCollector spoutOutputCollector; 

    private static final Map<Integer, String> PRODUCT = new 
    HashMap<Integer, String>(); 
    static { 
        PRODUCT.put(0, "A"); 
        PRODUCT.put(1, "B"); 
        PRODUCT.put(2, "C"); 
        PRODUCT.put(3, "D"); 
        PRODUCT.put(4, "E"); 
    } 

    private static final Map<Integer, Double> price = new 
    HashMap<Integer, Double>(); 
    static { 
        price.put(0, 500.0); 
        price.put(1, 100.0); 
        price.put(2, 300.0); 
        price.put(3, 900.0); 
        price.put(4, 1000.0); 
    } 

    public void open(Map conf, TopologyContext context, 
            SpoutOutputCollector spoutOutputCollector) { 
        // Open the spout 
        this.spoutOutputCollector = spoutOutputCollector; 
    } 

    public void nextTuple() { 
        // Storm cluster repeatedly call this method to emit the 
        continuous // 
        // stream of tuples. 
        final Random rand = new Random(); 
        // generate the random number from 0 to 4\. 
        int randomNumber = rand.nextInt(5); 

        spoutOutputCollector.emit (new 
        Values(PRODUCT.get(randomNumber),price.get(randomNumber), 
        System.currentTimeMillis())); 
        try { 
            Thread.sleep(1000); 
        } catch (InterruptedException e) { 
            // TODO Auto-generated catch block 
            e.printStackTrace(); 
        } 
    } 

    public void declareOutputFields(OutputFieldsDeclarer declarer) { 
        // emits the field  firstName , lastName and companyName. 
        declarer.declare(new Fields("product","price","timestamp")); 
    } 
} 
  1. com.stormadvance.storm_esper包中创建一个EsperBolt类。这个 bolt 接收SampleSpout类发出的元组,将其转换为股票 bean,然后调用EsperBolt类的esperPut()方法将数据传递给 Esper 引擎。以下是EsperBolt类的源代码:
public class EsperBolt implements IBasicBolt { 

    private static final long serialVersionUID = 2L; 
    private EsperOperation esperOperation; 

    public EsperBolt() { 

    } 

    public void execute(Tuple input, BasicOutputCollector collector) { 

        double price = input.getDoubleByField("price"); 
        long timeStamp = input.getLongByField("timestamp"); 
        //long timeStamp = System.currentTimeMillis(); 
        String product = input.getStringByField("product"); 
        Stock stock = new Stock(product, price, timeStamp); 
        esperOperation.esperPut(stock); 
    } 

    public void declareOutputFields(OutputFieldsDeclarer declarer) { 

    } 

    public Map<String, Object> getComponentConfiguration() { 
        // TODO Auto-generated method stub 
        return null; 
    } 

    public void prepare(Map stormConf, TopologyContext context) { 
        try { 
            // create the instance of ESOperations class 
            esperOperation = new EsperOperation(); 
        } catch (Exception e) { 
            throw new RuntimeException(); 
        } 
    } 

    public void cleanup() { 

    } 
} 
  1. com.stormadvance.storm_esper包中创建一个EsperTopology类。这个类创建了spoutbolt类的实例,并使用TopologyBuilder类将它们链接在一起。以下是主类的实现:
public class EsperTopology { 
    public static void main(String[] args) throws AlreadyAliveException, 
            InvalidTopologyException { 
        TopologyBuilder builder = new TopologyBuilder(); 

        // set the spout class 
        builder.setSpout("spout", new SampleSpout(), 2); 
        // set the ES bolt class 
        builder.setBolt("bolt", new EsperBolt(), 2) 
                .shuffleGrouping("spout"); 
        Config conf = new Config(); 
        conf.setDebug(true); 
        // create an instance of LocalCluster class for 
        // executing topology in local mode. 
        LocalCluster cluster = new LocalCluster(); 

        // EsperTopology is the name of submitted topology. 
        cluster.submitTopology("EsperTopology", conf, 
                builder.createTopology()); 
        try { 
            Thread.sleep(60000); 
        } catch (Exception exception) { 
            System.out.println("Thread interrupted exception : " + exception); 
        } 
        System.out.println("Stopped Called : "); 
        // kill the LearningStormTopology 
        cluster.killTopology("EsperTopology"); 
        // shutdown the storm test cluster 
        cluster.shutdown(); 

    } 
} 

总结

在本章中,我们主要关注了 Storm 与其他数据库的集成。此外,我们还介绍了如何在 Storm 中使用 Esper 执行窗口操作。

在下一章中,我们将介绍 Apache 日志处理案例研究。我们将解释如何通过 Storm 处理日志文件来生成业务信息。

第十一章:使用 Storm 进行 Apache 日志处理

在上一章中,我们介绍了如何将 Storm 与 Redis、HBase、Esper 和 Elasticsearch 集成。

在本章中,我们将介绍 Storm 最流行的用例,即日志处理。

本章涵盖以下主要部分:

  • Apache 日志处理元素

  • 安装 Logstash

  • 配置 Logstash 以将 Apache 日志生成到 Kafka

  • 拆分 Apache 日志文件

  • 计算国家名称、操作系统类型和浏览器类型

  • 识别网站的搜索关键词

  • 持久化处理数据

  • Kafka spout 和定义拓扑

  • 部署拓扑

  • 将数据存储到 Elasticsearch 并生成报告

Apache 日志处理元素

日志处理正在成为每个组织的必需品,因为他们需要从日志数据中收集业务信息。在本章中,我们基本上是在讨论如何使用 Logstash、Kafka、Storm 和 Elasticsearch 来处理 Apache 日志数据,以收集业务信息。

以下图示了我们在本章中开发的所有元素:

图 11.1:日志处理拓扑

使用 Logstash 在 Kafka 中生成 Apache 日志

如第八章中所解释的,Storm 和 Kafka 的集成,Kafka 是一个分布式消息队列,可以与 Storm 很好地集成。在本节中,我们将向您展示如何使用 Logstash 来读取 Apache 日志文件并将其发布到 Kafka 集群中。我们假设您已经运行了 Kafka 集群。Kafka 集群的安装步骤在第八章中概述。

安装 Logstash

在继续安装 Logstash 之前,我们将回答以下问题:什么是 Logstash?为什么我们要使用 Logstash?

什么是 Logstash?

Logstash 是一个用于收集、过滤/解析和发送数据以供将来使用的工具。收集、解析和发送分为三个部分,称为输入、过滤器和输出:

  • input部分用于从外部来源读取数据。常见的输入来源是文件、TCP 端口、Kafka 等。

  • filter部分用于解析数据。

  • output部分用于将数据发送到某些外部来源。常见的外部来源是 Kafka、Elasticsearch、TCP 等。

为什么我们要使用 Logstash?

在 Storm 开始实际处理之前,我们需要实时读取日志数据并将其存储到 Kafka 中。我们使用 Logstash 是因为它非常成熟地读取日志文件并将日志数据推送到 Kafka 中。

安装 Logstash

在安装 Logstash 之前,我们应该在 Linux 服务器上安装 JDK 1.8,因为我们将使用 Logstash 5.4.1,而 JDK 1.8 是此版本的最低要求。以下是安装 Logstash 的步骤:

  1. artifacts.elastic.co/downloads/logstash/logstash-5.4.1.zip下载 Logstash 5.4.1。

  2. 将设置复制到所有你想要发布到 Kafka 的 Apache 日志的机器上。

  3. 通过运行以下命令提取设置:

> unzip logstash-5.4.1.zip

Logstash 的配置

现在,我们将定义 Logstash 配置来消耗 Apache 日志并将其存储到 Kafka 中。

创建一个logstash.conf文件并添加以下行:

input {
  file {
    path => "PATH_TO_APACHE_LOG"
    start_position => "beginning"
  }
}
output {
  kafka {
    topic_id => "TOPIC_NAME"
    bootstrap_servers => "KAFKA_IP:KAFKA_PORT"
  }
}

我们应该更改前述配置中的以下参数:

  • TOPIC_NAME:替换为您要用于存储 Apache 日志的 Kafka 主题

  • KAFKA_IPKAFKA_PORT:指定所有 Kafka 节点的逗号分隔列表

  • PATH_TO_APACHE_LOG:Logstash 机器上 Apache 日志文件的位置

转到 Logstash 主目录并执行以下命令以开始读取日志并发布到 Kafka:

$ bin/logstash agent -f logstash.conf

现在,实时日志数据正在进入 Kafka 主题。在下一节中,我们将编写 Storm 拓扑来消费日志数据,处理并将处理数据存储到数据库中。

为什么在 Logstash 和 Storm 之间使用 Kafka?

众所周知,Storm 提供了可靠的消息处理,这意味着每条消息进入 Storm 拓扑都将至少被处理一次。在 Storm 中,数据丢失只可能发生在 spout 端,如果 Storm spout 的处理能力小于 Logstash 的生产能力。因此,为了避免数据在 Storm spout 端丢失,我们通常会将数据发布到消息队列(Kafka),Storm spout 将使用消息队列作为数据源。

分割 Apache 日志行

现在,我们正在创建一个新的拓扑,它将使用KafkaSpout spout 从 Kafka 中读取数据。在本节中,我们将编写一个ApacheLogSplitter bolt,它具有从 Apache 日志行中提取 IP、状态码、引用来源、发送的字节数等信息的逻辑。由于这是一个新的拓扑,我们必须首先创建新项目。

  1. 创建一个新的 Maven 项目,groupIdcom.stormadvanceartifactIdlogprocessing

  2. pom.xml文件中添加以下依赖项:

       <dependency> 
             <groupId>org.apache.storm</groupId> 
             <artifactId>storm-core</artifactId> 
             <version>1.0.2</version> 
             <scope>provided</scope> 
       </dependency> 

       <!-- Utilities --> 
       <dependency> 
             <groupId>commons-collections</groupId> 
             <artifactId>commons-collections</artifactId> 
             <version>3.2.1</version> 
       </dependency> 
       <dependency> 
             <groupId>com.google.guava</groupId> 
             <artifactId>guava</artifactId> 
             <version>15.0</version> 
       </dependency> 
  1. 我们将在com.stormadvance.logprocessing包中创建ApacheLogSplitter类。这个类包含了从 Apache 日志行中提取不同元素(如 IP、引用来源、用户代理等)的逻辑。
/** 
 * This class contains logic to Parse an Apache log file with Regular 
 * Expressions 
 */ 
public class ApacheLogSplitter { 

 public Map<String,Object> logSplitter(String apacheLog) { 

       String logEntryLine = apacheLog; 
       // Regex pattern to split fetch the different properties from log lines. 
       String logEntryPattern = "^([\\d.]+) (\\S+) (\\S+) \\[([\\w-:/]+\\s[+\\-]\\d{4})\\] \"(.+?)\" (\\d{3}) (\\d+) \"([^\"]+)\" \"([^\"]+)\""; 

       Pattern p = Pattern.compile(logEntryPattern); 
       Matcher matcher = p.matcher(logEntryLine); 
       Map<String,Object> logMap = new HashMap<String, Object>(); 
       if (!matcher.matches() || 9 != matcher.groupCount()) { 
             System.err.println("Bad log entry (or problem with RE?):"); 
             System.err.println(logEntryLine); 
             return logMap; 
       } 
       // set the ip, dateTime, request, etc into map. 
       logMap.put("ip", matcher.group(1)); 
       logMap.put("dateTime", matcher.group(4)); 
       logMap.put("request", matcher.group(5)); 
       logMap.put("response", matcher.group(6)); 
       logMap.put("bytesSent", matcher.group(7)); 
       logMap.put("referrer", matcher.group(8)); 
       logMap.put("useragent", matcher.group(9)); 
       return logMap; 
 } 
  1. logSplitter(String apacheLog)方法的输入是:
98.83.179.51 - - [18/May/2011:19:35:08 -0700] \"GET /css/main.css HTTP/1.1\" 200 1837 \"http://www.safesand.com/information.htm\" \"Mozilla/5.0 (Windows NT 6.0; WOW64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1\" 
  1. logSplitter(String apacheLog)方法的输出是:
{response=200, referrer=http://www.safesand.com/information.htm, bytesSent=1837, useragent=Mozilla/5.0 (Windows NT 6.0; WOW64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1, dateTime=18/May/2011:19:35:08 -0700, request=GET /css/main.css HTTP/1.1, ip=98.83.179.51}  
  1. 现在我们将在com.stormadvance.logprocessing包中创建ApacheLogSplitterBolt类。ApacheLogSplitterBolt扩展了org.apache.storm.topology.base.BaseBasicBolt类,并将ApacheLogSplitter类生成的字段集传递给拓扑中的下一个 bolt。以下是ApacheLogSplitterBolt类的源代码:
/** 
 *  
 * This class call the ApacheLogSplitter class and pass the set of fields (ip, 
 * referrer, user-agent, etc) to next bolt in Topology. 
 */ 

public class ApacheLogSplitterBolt extends BaseBasicBolt { 

 private static final long serialVersionUID = 1L; 
 // Create the instance of ApacheLogSplitter class. 
 private static final ApacheLogSplitter apacheLogSplitter = new ApacheLogSplitter(); 
 private static final List<String> LOG_ELEMENTS = new ArrayList<String>(); 
 static { 
       LOG_ELEMENTS.add("ip"); 
       LOG_ELEMENTS.add("dateTime"); 
       LOG_ELEMENTS.add("request"); 
       LOG_ELEMENTS.add("response"); 
       LOG_ELEMENTS.add("bytesSent"); 
       LOG_ELEMENTS.add("referrer"); 
       LOG_ELEMENTS.add("useragent"); 
 } 

 public void execute(Tuple input, BasicOutputCollector collector) { 
       // Get the Apache log from the tuple 
       String log = input.getString(0); 

       if (StringUtils.isBlank(log)) { 
             // ignore blank lines 
             return; 
       } 
       // call the logSplitter(String apachelog) method of ApacheLogSplitter 
       // class. 
       Map<String, Object> logMap = apacheLogSplitter.logSplitter(log); 
       List<Object> logdata = new ArrayList<Object>(); 
       for (String element : LOG_ELEMENTS) { 
             logdata.add(logMap.get(element)); 
       } 
       // emits set of fields (ip, referrer, user-agent, bytesSent, etc) 
       collector.emit(logdata); 

 } 

 public void declareOutputFields(OutputFieldsDeclarer declarer) { 
       // specify the name of output fields. 
       declarer.declare(new Fields("ip", "dateTime", "request", "response", 
                   "bytesSent", "referrer", "useragent")); 
 } 
} 

ApacheLogSplitterBolt类的输出包含七个字段。这些字段是ipdateTimerequestresponsebytesSentreferreruseragent

从日志文件中识别国家、操作系统类型和浏览器类型

本节解释了如何通过分析 Apache 日志行来计算用户国家名称、操作系统类型和浏览器类型。通过识别国家名称,我们可以轻松地确定我们网站受到更多关注的地点以及我们受到较少关注的地点。让我们执行以下步骤来计算 Apache 日志文件中的国家名称、操作系统和浏览器:

  1. 我们使用开源的geoip库来从 IP 地址计算国家名称。在pom.xml文件中添加以下依赖项:
       <dependency> 
             <groupId>org.geomind</groupId> 
             <artifactId>geoip</artifactId> 
             <version>1.2.8</version> 
       </dependency> 
  1. pom.xml文件中添加以下存储库:
        <repository> 
             <id>geoip</id> 
             <url>http://snambi.github.com/maven/</url> 
       </repository> 
  1. 我们将在com.stormadvance.logprocessing包中创建IpToCountryConverter类。这个类包含了带有GeoLiteCity.dat文件位置作为输入的参数化构造函数。你可以在logprocessing项目的资源文件夹中找到GeoLiteCity.dat文件。GeoLiteCity.dat文件的位置在所有 Storm 节点中必须相同。GeoLiteCity.dat文件是我们用来从 IP 地址计算国家名称的数据库。以下是IpToCountryConverter类的源代码:
/** 
 * This class contains logic to calculate the country name from IP address 
 *  
 */ 
public class IpToCountryConverter { 

 private static LookupService cl = null; 

 /** 
  * An parameterised constructor which would take the location of 
  * GeoLiteCity.dat file as input. 
  *  
  * @param pathTOGeoLiteCityFile 
  */ 
 public IpToCountryConverter(String pathTOGeoLiteCityFile) { 
       try { 
             cl = new LookupService("pathTOGeoLiteCityFile", 
                         LookupService.GEOIP_MEMORY_CACHE); 
       } catch (Exception exception) { 
             throw new RuntimeException( 
                         "Error occurs while initializing IpToCountryConverter class : "); 
       } 
 } 

 /** 
  * This method takes ip address an input and convert it into country name. 
  *  
  * @param ip 
  * @return 
  */ 
 public String ipToCountry (String ip) { 
       Location location = cl.getLocation(ip); 
       if (location == null) { 
             return "NA"; 
       } 
       if (location.countryName == null) { 
             return "NA"; 
       } 
       return location.countryName; 
 } 
} 
  1. 现在从code.google.com/p/ndt/source/browse/branches/applet_91/Applet/src/main/java/edu/internet2/ndt/UserAgentTools.java?r=856下载UserAgentTools类。这个类包含了从用户代理中计算操作系统和浏览器类型的逻辑。你也可以在logprocessing项目中找到UserAgentTools类。

  2. 让我们在com.stormadvance.logprocessing包中编写UserInformationGetterBolt类。这个 bolt 使用UserAgentToolsIpToCountryConverter类来计算国家名称、操作系统和浏览器。

 /** 
 * This class use the IpToCountryConverter and UserAgentTools class to calculate 
 * the country, os and browser from log line. 
 *  
 */ 
public class UserInformationGetterBolt extends BaseRichBolt { 

 private static final long serialVersionUID = 1L; 
 private IpToCountryConverter ipToCountryConverter = null; 
 private UserAgentTools userAgentTools = null; 
 public OutputCollector collector; 
 private String pathTOGeoLiteCityFile; 

 public UserInformationGetterBolt(String pathTOGeoLiteCityFile) { 
       // set the path of GeoLiteCity.dat file. 
       this.pathTOGeoLiteCityFile = pathTOGeoLiteCityFile; 
 } 

 public void declareOutputFields(OutputFieldsDeclarer declarer) { 
       declarer.declare(new Fields("ip", "dateTime", "request", "response", 
                   "bytesSent", "referrer", "useragent", "country", "browser", 
                   "os")); 
 } 

 public void prepare(Map stormConf, TopologyContext context, 
             OutputCollector collector) { 
       this.collector = collector; 
       this.ipToCountryConverter = new IpToCountryConverter( 
                   this.pathTOGeoLiteCityFile); 
       this.userAgentTools = new UserAgentTools(); 

 } 

 public void execute(Tuple input) { 

       String ip = input.getStringByField("ip").toString(); 

       // calculate the country from ip 
       Object country = ipToCountryConverter.ipToCountry(ip); 
       // calculate the browser from useragent. 
       Object browser = userAgentTools.getBrowser(input.getStringByField( 
                   "useragent").toString())[1]; 
       // calculate the os from useragent. 
       Object os = userAgentTools.getOS(input.getStringByField("useragent") 
                   .toString())[1]; 
       collector.emit(new Values(input.getString(0), input.getString(1), input 
                   .getString(2), input.getString(3), input.getString(4), input 
                   .getString(5), input.getString(6), country, browser, os)); 

 } 
} 
  1. UserInformationGetterBolt类的输出包含 10 个字段。这些字段是ipdateTimerequestresponsebytesSentreferreruseragentcountrybrowseros

计算搜索关键词

本节解释了如何从引荐 URL 计算搜索关键词。假设引荐 URL 是www.google.co.in/#q=learning+storm。我们将把这个引荐 URL 传递给一个类,这个类的输出将是learning storm。通过识别搜索关键词,我们可以轻松地确定用户搜索关键词以到达我们的网站。让我们执行以下步骤来计算引荐 URL 中的关键词:

  1. 我们在com.stormadvance.logprocessing包中创建一个KeywordGenerator类。这个类包含从引荐 URL 生成搜索关键词的逻辑。以下是KeywordGenerator类的源代码:
/** 
 * This class takes referrer URL as input, analyze the URL and return search 
 * keyword as output. 
 *  
 */ 
public class KeywordGenerator { 
 public String getKeyword(String referer) { 

       String[] temp; 
       Pattern pat = Pattern.compile("[?&#]q=([^&]+)"); 
       Matcher m = pat.matcher(referer); 
       if (m.find()) { 
             String searchTerm = null; 
             searchTerm = m.group(1); 
             temp = searchTerm.split("\\+"); 
             searchTerm = temp[0]; 
             for (int i = 1; i < temp.length; i++) { 
                   searchTerm = searchTerm + " " + temp[i]; 
             } 
             return searchTerm; 
       } else { 
             pat = Pattern.compile("[?&#]p=([^&]+)"); 
             m = pat.matcher(referer); 
             if (m.find()) { 
                   String searchTerm = null; 
                   searchTerm = m.group(1); 
                   temp = searchTerm.split("\\+"); 
                   searchTerm = temp[0]; 
                   for (int i = 1; i < temp.length; i++) { 
                         searchTerm = searchTerm + " " + temp[i]; 
                   } 
                   return searchTerm; 
             } else { 
                   // 
                   pat = Pattern.compile("[?&#]query=([^&]+)"); 
                   m = pat.matcher(referer); 
                   if (m.find()) { 
                         String searchTerm = null; 
                         searchTerm = m.group(1); 
                         temp = searchTerm.split("\\+"); 
                         searchTerm = temp[0]; 
                         for (int i = 1; i < temp.length; i++) { 
                               searchTerm = searchTerm + " " + temp[i]; 
                         } 
                         return searchTerm; 
                   }  else { 
                               return "NA"; 
                         } 
                   } 
       } 
 } 

} 
  1. 如果KeywordGenerator类的输入是:in.search.yahoo.com/search;_ylt=AqH0NZe1hgPCzVap0PdKk7GuitIF?p=india+live+score&toggle=1&cop=mss&ei=UTF-8&fr=yfp-t-704

  2. 然后,KeywordGenerator类的输出是:

india live score
  1. 我们在com.stormadvance.logprocessing包中创建一个KeyWordIdentifierBolt类。这个类调用KeywordGenerator来从引荐 URL 生成关键词。以下是KeyWordIdentifierBolt类的源代码:
/** 
 * This class use the KeywordGenerator class to generate the search keyword from 
 * referrer URL. 
 *  
 */ 
public class KeyWordIdentifierBolt extends BaseRichBolt { 

 private static final long serialVersionUID = 1L; 
 private KeywordGenerator keywordGenerator = null; 
 public OutputCollector collector; 

 public KeyWordIdentifierBolt() { 

 } 

 public void declareOutputFields(OutputFieldsDeclarer declarer) { 
       declarer.declare(new Fields("ip", "dateTime", "request", "response", 
                   "bytesSent", "referrer", "useragent", "country", "browser", 
                   "os", "keyword")); 
 } 

 public void prepare(Map stormConf, TopologyContext context, 
             OutputCollector collector) { 
       this.collector = collector; 
       this.keywordGenerator = new KeywordGenerator(); 

 } 

 public void execute(Tuple input) { 

       String referrer = input.getStringByField("referrer").toString(); 
       // call the getKeyword(String referrer) method KeywordGenerator class to 
       // generate the search keyword. 
       Object keyword = keywordGenerator.getKeyword(referrer); 
       // emits all the field emitted by previous bolt + keyword 
       collector.emit(new Values(input.getString(0), input.getString(1), input 
                   .getString(2), input.getString(3), input.getString(4), input 
                   .getString(5), input.getString(6), input.getString(7), input 
                   .getString(8), input.getString(9), keyword)); 

 } 
} 
  1. KeyWordIdentifierBolt类的输出包含 11 个字段。这些字段是ipdateTimerequestresponsebytesSentreferreruseragentcountrybrowseroskeyword

持久化处理数据

本节将解释如何将处理数据持久化到数据存储中。我们在日志处理用例中使用 MySQL 作为数据存储。我假设您已经在您的 centOS 机器上安装了 MySQL,或者您可以按照www.rackspace.com/knowledge_center/article/installing-mysql-server-on-centos上的博客来安装 MySQL。让我们执行以下步骤将记录持久化到 MySQL 中:

  1. 将以下依赖项添加到pom.xml

       <dependency> 
             <groupId>mysql</groupId> 
             <artifactId>mysql-connector-java</artifactId> 
             <version>5.1.6</version> 
       </dependency> 
  1. 我们在com.stormadvance.logprocessing包中创建一个MySQLConnection类。这个类包含getMySQLConnection(String ip, String database, String user, String password)方法,该方法返回 MySQL 连接。以下是MySQLConnection类的源代码:
/** 
 *  
 * This class return the MySQL connection. 
 */ 
public class MySQLConnection { 

 private static Connection connect = null; 

 /** 
  * This method return the MySQL connection. 
  *  
  * @param ip 
  *            ip of MySQL server 
  * @param database 
  *            name of database 
  * @param user 
  *            name of user 
  * @param password 
  *            password of given user 
  * @return MySQL connection 
  */ 
 public static Connection getMySQLConnection(String ip, String database, String user, String password) { 
       try { 
             // this will load the MySQL driver, each DB has its own driver 
             Class.forName("com.mysql.jdbc.Driver"); 
             // setup the connection with the DB. 
             connect = DriverManager 
                         .getConnection("jdbc:mysql://"+ip+"/"+database+"?" 
                                     + "user="+user+"&password="+password+""); 
             return connect; 
       } catch (Exception e) { 
             throw new RuntimeException("Error occurs while get mysql connection : "); 
       } 
 } 
} 
  1. 现在,我们在com.stormadvance.logprocessing包中创建一个MySQLDump类。这个类有一个带参数的构造函数,它以 MySQL 的服务器 ip、数据库名称、用户和密码作为参数。这个类调用MySQLConnection类的getMySQLConnection(ip,database,user,password)方法来获取 MySQL 连接。MySQLDump类包含persistRecord(Tuple tuple)记录方法,这个方法将输入元组持久化到 MySQL 中。以下是MySQLDump类的源代码:
/** 
 * This class contains logic to persist record into MySQL database. 
 *  
 */ 
public class MySQLDump { 
 /** 
  * Name of database you want to connect 
  */ 
 private String database; 
 /** 
  * Name of MySQL user 
  */ 
 private String user; 
 /** 
  * IP of MySQL server 
  */ 
 private String ip; 
 /** 
  * Password of MySQL server 
  */ 
 private String password; 

 public MySQLDump(String ip, String database, String user, String password) { 
       this.ip = ip; 
       this.database = database; 
       this.user = user; 
       this.password = password; 
 } 

 /** 
  * Get the MySQL connection 
  */ 
 private Connection connect = MySQLConnection.getMySQLConnection(ip,database,user,password); 

 private PreparedStatement preparedStatement = null; 

 /** 
  * Persist input tuple. 
  * @param tuple 
  */ 
 public void persistRecord(Tuple tuple) { 
       try { 

             // preparedStatements can use variables and are more efficient 
             preparedStatement = connect 
                         .prepareStatement("insert into  apachelog values (default, ?, ?, ?,?, ?, ?, ?, ? , ?, ?, ?)"); 

             preparedStatement.setString(1, tuple.getStringByField("ip")); 
             preparedStatement.setString(2, tuple.getStringByField("dateTime")); 
             preparedStatement.setString(3, tuple.getStringByField("request")); 
             preparedStatement.setString(4, tuple.getStringByField("response")); 
             preparedStatement.setString(5, tuple.getStringByField("bytesSent")); 
             preparedStatement.setString(6, tuple.getStringByField("referrer")); 
             preparedStatement.setString(7, tuple.getStringByField("useragent")); 
             preparedStatement.setString(8, tuple.getStringByField("country")); 
             preparedStatement.setString(9, tuple.getStringByField("browser")); 
             preparedStatement.setString(10, tuple.getStringByField("os")); 
             preparedStatement.setString(11, tuple.getStringByField("keyword")); 

             // Insert record 
             preparedStatement.executeUpdate(); 

       } catch (Exception e) { 
             throw new RuntimeException( 
                         "Error occurs while persisting records in mysql : "); 
       } finally { 
             // close prepared statement 
             if (preparedStatement != null) { 
                   try { 
                         preparedStatement.close(); 
                   } catch (Exception exception) { 
                         System.out 
                                     .println("Error occurs while closing PreparedStatement : "); 
                   } 
             } 
       } 

 } 
 public void close() { 
       try { 
       connect.close(); 
       }catch(Exception exception) { 
             System.out.println("Error occurs while clossing the connection"); 
       } 
 } 
} 
  1. 让我们在com.stormadvance.logprocessing包中创建一个PersistenceBolt类。这个类实现了org.apache.storm.topology.IBasicBolt。这个类调用MySQLDump类的persistRecord(Tuple tuple)方法来将记录/事件持久化到 MySQL。以下是PersistenceBolt类的源代码:
/** 
 * This Bolt call the getConnectionn(....) method of MySQLDump class to persist 
 * the record into MySQL database. 
 *  
 * @author Admin 
 *  
 */ 
public class PersistenceBolt implements IBasicBolt { 

 private MySQLDump mySQLDump = null; 
 private static final long serialVersionUID = 1L; 
 /** 
  * Name of database you want to connect 
  */ 
 private String database; 
 /** 
  * Name of MySQL user 
  */ 
 private String user; 
 /** 
  * IP of MySQL server 
  */ 
 private String ip; 
 /** 
  * Password of MySQL server 
  */ 
 private String password; 

 public PersistenceBolt(String ip, String database, String user, 
             String password) { 
       this.ip = ip; 
       this.database = database; 
       this.user = user; 
       this.password = password; 
 } 

 public void declareOutputFields(OutputFieldsDeclarer declarer) { 
 } 

 public Map<String, Object> getComponentConfiguration() { 
       return null; 
 } 

 public void prepare(Map stormConf, TopologyContext context) { 

       // create the instance of MySQLDump(....) class. 
       mySQLDump = new MySQLDump(ip, database, user, password); 
 } 

 /** 
  * This method call the persistRecord(input) method of MySQLDump class to 
  * persist record into MySQL. 
  */ 
 public void execute(Tuple input, BasicOutputCollector collector) { 
       System.out.println("Input tuple : " + input); 
       mySQLDump.persistRecord(input); 
 } 

 public void cleanup() { 
       // Close the connection 
       mySQLDump.close(); 
 } 

} 

在本节中,我们已经介绍了如何将输入元组插入数据存储中。

Kafka spout 和定义拓扑

本节将解释如何从 Kafka 主题中读取 Apache 日志。本节还定义了将在前面各节中创建的所有 bolt 链接在一起的LogProcessingTopology。让我们执行以下步骤来消费来自 Kafka 的数据并定义拓扑:

  1. pom.xml文件中添加以下 Kafka 的依赖和仓库:
       <dependency> 
             <groupId>org.apache.storm</groupId> 
             <artifactId>storm-kafka</artifactId> 
             <version>1.0.2</version> 
             <exclusions> 
                   <exclusion> 
                         <groupId>org.apache.kafka</groupId> 
                         <artifactId>kafka-clients</artifactId> 
                   </exclusion> 
             </exclusions> 
       </dependency> 

       <dependency> 
             <groupId>org.apache.kafka</groupId> 
             <artifactId>kafka_2.10</artifactId> 
             <version>0.9.0.1</version> 
             <exclusions> 
                   <exclusion> 
                         <groupId>com.sun.jdmk</groupId> 
                         <artifactId>jmxtools</artifactId> 
                   </exclusion> 
                   <exclusion> 
                         <groupId>com.sun.jmx</groupId> 
                         <artifactId>jmxri</artifactId> 
                   </exclusion> 
             </exclusions> 
       </dependency> 
  1. pom.xml文件中添加以下build插件。这将让我们使用 Maven 执行LogProcessingTopology
       <build> 
       <plugins> 
             <plugin> 
                   <artifactId>maven-assembly-plugin</artifactId> 
                   <configuration> 
                         <descriptorRefs> 
                               <descriptorRef>jar-with-
                               dependencies</descriptorRef> 
                         </descriptorRefs> 
                         <archive> 
                               <manifest> 
                                     <mainClass></mainClass> 
                               </manifest> 
                         </archive> 
                   </configuration> 
                   <executions> 
                         <execution> 
                               <id>make-assembly</id> 
                               <phase>package</phase> 
                               <goals> 
                                     <goal>single</goal> 
                               </goals> 
                         </execution> 
                   </executions> 
             </plugin> 

             <plugin> 
                   <groupId>org.codehaus.mojo</groupId> 
                   <artifactId>exec-maven-plugin</artifactId> 
                   <version>1.2.1</version> 
                   <executions> 
                         <execution> 
                               <goals> 
                                     <goal>exec</goal> 
                               </goals> 
                         </execution> 
                   </executions> 
                   <configuration> 
                         <executable>java</executable> 
                    <includeProjectDependencies>true</includeProjectDependencies> 
                    <includePluginDependencies>false</includePluginDependencies> 
                         <classpathScope>compile</classpathScope> 
                         <mainClass>${main.class}</mainClass> 
                   </configuration> 
             </plugin> 

             <plugin> 
                   <groupId>org.apache.maven.plugins</groupId> 
                   <artifactId>maven-compiler-plugin</artifactId> 
             </plugin> 

       </plugins> 
 </build> 
  1. com.stormadvance.logprocessing包中创建一个LogProcessingTopology类。该类使用org.apache.storm.topology.TopologyBuilder类来定义拓扑。以下是LogProcessingTopology类的源代码及解释:
public class LogProcessingTopology { 
 public static void main(String[] args) throws Exception { 

       // zookeeper hosts for the Kafka cluster 
       BrokerHosts zkHosts = new ZkHosts("ZK:2183"); 

       // Create the KafkaSpout configuartion 
       // Second argument is the topic name 
       // Third argument is the zookeepr root for Kafka 
       // Fourth argument is consumer group id 
       SpoutConfig kafkaConfig = new SpoutConfig(zkHosts, "apache_log", "", 
                   "id2"); 

       // Specify that the Kafka messages are String 
       kafkaConfig.scheme = new SchemeAsMultiScheme(new StringScheme()); 

       // We want to consume all the first messages in the topic everytime 
       // we run the topology to help in debugging. In production, this 
       // property should be false 

       kafkaConfig.startOffsetTime = kafka.api.OffsetRequest 
                   .EarliestTime(); 

       // Now we create the topology 
       TopologyBuilder builder = new TopologyBuilder(); 

       // set the Kafka spout class 
       builder.setSpout("KafkaSpout", new KafkaSpout(kafkaConfig), 2); 

       // set the LogSplitter, IpToCountry, Keyword and PersistenceBolt bolts 
       // class. 
       builder.setBolt("LogSplitter", new ApacheLogSplitterBolt(), 1) 
                   .globalGrouping("KafkaSpout"); 

       builder.setBolt( 
                   "IpToCountry", 
                   new UserInformationGetterBolt( 
                               args[0]), 1) 
                   .globalGrouping("LogSplitter"); 
       builder.setBolt("Keyword", new KeyWordIdentifierBolt(), 1) 
                   .globalGrouping("IpToCountry"); 
       builder.setBolt("PersistenceBolt", 
                   new PersistenceBolt(args[1], args[2], args[3], args[4]), 
                   1).globalGrouping("Keyword"); 

       if (args.length == 6) { 
             // Run the topology on remote cluster. 
             Config conf = new Config(); 
             conf.setNumWorkers(4); 
             try { 
                   StormSubmitter.submitTopology(args[4], conf, 
                               builder.createTopology()); 
             } catch (AlreadyAliveException alreadyAliveException) { 
                   System.out.println(alreadyAliveException); 
             } catch (InvalidTopologyException invalidTopologyException) { 
                   System.out.println(invalidTopologyException); 
             } 
       } else { 
             // create an instance of LocalCluster class for executing topology 
             // in local mode. 
             LocalCluster cluster = new LocalCluster(); 
             Config conf = new Config(); 
             conf.setDebug(true); 
             // Submit topology for execution 
             cluster.submitTopology("KafkaToplogy1", conf, 
                         builder.createTopology()); 

             try { 
                   // Wait for sometime before exiting 
                   System.out 
                               .println("**********************Waiting to consume from kafka"); 
                   Thread.sleep(100000); 
                   System.out.println("Stopping the sleep thread"); 

             } catch (Exception exception) { 
                   System.out 
                               .println("******************Thread interrupted exception : " 
                                           + exception); 
             } 

             // kill the KafkaTopology 
             cluster.killTopology("KafkaToplogy1"); 

             // shutdown the storm test cluster 
             cluster.shutdown(); 

       } 

 } 
} 

本节介绍了如何将不同类型的 bolt 链接成拓扑。我们还介绍了如何从 Kafka 消费数据。在下一节中,我们将解释如何部署拓扑。

部署拓扑

本节将解释如何部署LogProcessingTopology。执行以下步骤:

  1. 在 MySQL 控制台上执行以下命令定义数据库架构:
mysql> create database apachelog; 
mysql> use apachelog; 
mysql> create table apachelog( 
       id INT NOT NULL AUTO_INCREMENT, 
       ip VARCHAR(100) NOT NULL, 
       dateTime VARCHAR(200) NOT NULL, 
       request VARCHAR(100) NOT NULL, 
       response VARCHAR(200) NOT NULL, 
       bytesSent VARCHAR(200) NOT NULL, 
        referrer VARCHAR(500) NOT NULL, 
       useragent VARCHAR(500) NOT NULL, 
       country VARCHAR(200) NOT NULL, 
       browser VARCHAR(200) NOT NULL, 
       os VARCHAR(200) NOT NULL, 
       keyword VARCHAR(200) NOT NULL, 
       PRIMARY KEY (id) 
 ); 
  1. 我假设您已经通过 Logstash 在apache_log主题上产生了一些数据。

  2. 进入项目主目录并运行以下命令构建项目:

> mvn clean install -DskipTests 
  1. 执行以下命令以在本地模式下启动日志处理拓扑:
> java -cp target/logprocessing-0.0.1-SNAPSHOT-jar-with-dependencies.jar:$STORM_HOME/storm-core-0.9.0.1.jar:$STORM_HOME/lib/* com.stormadvance.logprocessing.LogProcessingTopology path/to/GeoLiteCity.dat localhost apachelog root root 
  1. 现在,进入 MySQL 控制台,检查apachelog表中的行:
mysql> select * from apachelog limit 2 
    -> ; 
+----+----------------+--------------------------+----------------+----------+-----------+-----------------------------------------+-----------------------------------------------------------------------------------------+---------------+----------------+-------+---------+ 
| id | ip             | dateTime                 | request        | response | bytesSent | referrer                                | useragent                                                                               | country       | browser        | os    | keyword | 
+----+----------------+--------------------------+----------------+----------+-----------+-----------------------------------------+-----------------------------------------------------------------------------------------+---------------+----------------+-------+---------+ 
|  1 | 24.25.135.19   | 1-01-2011:06:20:31 -0500 | GET / HTTP/1.1 | 200      | 864       | http://www.adeveloper.com/resource.html | Mozilla/5.0 (Windows; U; Windows NT 5.1; hu-HU; rv:1.7.12) Gecko/20050919 Firefox/1.0.7 | United States | Gecko(Firefox) | WinXP | NA      | 
|  2 | 180.183.50.208 | 1-01-2011:06:20:31 -0500 | GET / HTTP/1.1 | 200      | 864       | http://www.adeveloper.com/resource.html | Mozilla/5.0 (Windows; U; Windows NT 5.1; hu-HU; rv:1.7.12) Gecko/20050919 Firefox/1.0.7 | Thailand      | Gecko(Firefox) | WinXP | NA      | 
+----+----------------+--------------------------+----------------+----------+-----------+-----------------------------------------+-----------------------------------------------------------------------------------------+---------------+----------------+-------+---------+ 

在本节中,我们介绍了如何部署日志处理拓扑。下一节将解释如何从 MySQL 中存储的数据生成统计信息。

MySQL 查询

本节将解释如何分析或查询存储数据以生成一些统计信息。我们将涵盖以下内容:

  • 计算每个国家的页面点击量

  • 计算每个浏览器的数量

  • 计算每个操作系统的数量

计算每个国家的页面点击量

在 MySQL 控制台上运行以下命令,计算每个国家的页面点击量:

mysql> select country, count(*) from apachelog group by country; 
+---------------------------+----------+ 
| country                   | count(*) | 
+---------------------------+----------+ 
| Asia/Pacific Region       |        9 | 
| Belarus                   |       12 | 
| Belgium                   |       12 | 
| Bosnia and Herzegovina    |       12 | 
| Brazil                    |       36 | 
| Bulgaria                  |       12 | 
| Canada                    |      218 | 
| Europe                    |       24 | 
| France                    |       44 | 
| Germany                   |       48 | 
| Greece                    |       12 | 
| Hungary                   |       12 | 
| India                     |      144 | 
| Indonesia                 |       60 | 
| Iran, Islamic Republic of |       12 | 
| Italy                     |       24 | 
| Japan                     |       12 | 
| Malaysia                  |       12 | 
| Mexico                    |       36 | 
| NA                        |       10 | 
| Nepal                     |       24 | 
| Netherlands               |      164 | 
| Nigeria                   |       24 | 
| Puerto Rico               |       72 | 
| Russian Federation        |       60 | 
| Singapore                 |      165 | 
| Spain                     |       48 | 
| Sri Lanka                 |       12 | 
| Switzerland               |        7 | 
| Taiwan                    |       12 | 
| Thailand                  |       12 | 
| Ukraine                   |       12 | 
| United Kingdom            |       48 | 
| United States             |     5367 | 
| Vietnam                   |       12 | 
| Virgin Islands, U.S.      |      129 | 
+---------------------------+----------+ 
36 rows in set (0.08 sec) 

计算每个浏览器的数量

在 MySQL 控制台上运行以下命令,计算每个浏览器的数量:

mysql> select browser, count(*) from apachelog group by browser; 
+----------------+----------+ 
| browser        | count(*) | 
+----------------+----------+ 
| Gecko(Firefox) |     6929 | 
+----------------+----------+ 
1 row in set (0.00 sec)  

计算每个操作系统的数量

在 MySQL 控制台上运行以下命令,计算每个操作系统的数量:

mysql> select os,count(*) from apachelog group by os; 
+-------+----------+ 
| os    | count(*) | 
+-------+----------+ 
| WinXP |     6929 | 
+-------+----------+ 
1 row in set (0.00 sec) 

总结

在本章中,我们向您介绍了如何处理 Apache 日志文件,如何通过分析日志文件识别 IP 的国家名称,如何通过分析日志文件识别用户操作系统和浏览器,以及如何通过分析引荐字段识别搜索关键字。

在下一章中,我们将学习如何通过 Storm 解决机器学习问题。

第十二章:Twitter 推文收集和机器学习

在上一章中,我们介绍了如何使用 Storm 和 Kafka 创建日志处理应用程序。

在本章中,我们将涵盖 Storm 机器学习的另一个重要用例。

本章涵盖的主要主题如下:

  • 探索机器学习

  • 使用 Kafka 生产者将推文存储在 Kafka 集群中

  • 使用 Kafka Spout 从 Kafka 读取数据

  • 使用 Storm Bolt 来过滤推文

  • 使用 Storm Bolt 来计算推文的情感

  • 拓扑的部署

探索机器学习

机器学习是应用计算机科学的一个分支,在这个分支中,我们基于现有的可供分析的数据构建真实世界现象的模型,然后使用该模型,预测模型以前从未见过的数据的某些特征。机器学习已经成为实时应用程序非常重要的组成部分,因为需要实时做出决策。

从图形上看,机器学习的过程可以用以下图示表示:

从数据构建模型的过程在机器学习术语中称为训练。训练可以实时在数据流上进行,也可以在历史数据上进行。当训练实时进行时,模型随着数据的变化而随时间演变。这种学习被称为在线学习,当模型定期更新,通过在新数据集上运行训练算法时,被称为离线学习。

当我们谈论 Storm 上的机器学习时,往往我们谈论的是在线学习算法。

以下是机器学习的一些真实应用:

  • 在线广告优化

  • 新文章聚类

  • 垃圾邮件检测

  • 计算机视觉

  • 情感分析

Twitter 情感分析

我们将情感用例分为两部分:

  • 从 Twitter 收集推文并将其存储在 Kafka 中

  • 从 Kafka 读取数据,计算情感,并将其存储在 HDFS 中

使用 Kafka 生产者将推文存储在 Kafka 集群中

在本节中,我们将介绍如何使用 Twitter 流 API 从 Twitter 中获取推文。我们还将介绍如何将获取的推文存储在 Kafka 中,以便通过 Storm 进行后续处理。

我们假设您已经拥有 Twitter 账户,并且为您的应用程序生成了消费者密钥和访问令牌。您可以参考:bdthemes.com/support/knowledge-base/generate-api-key-consumer-token-access-key-twitter-oauth/ 生成消费者密钥和访问令牌。请按照以下步骤进行:

  1. 使用groupIdcom.stormadvanceartifactIdkafka_producer_twitter创建一个新的 maven 项目。

  2. 将以下依赖项添加到pom.xml文件中。我们正在向pom.xml添加 Kafka 和 Twitter 流 Maven 依赖项,以支持 Kafka 生产者和从 Twitter 流式传输推文。

   <dependencies> 
         <dependency> 
               <groupId>org.apache.kafka</groupId> 
               <artifactId>kafka_2.10</artifactId> 
               <version>0.9.0.1</version> 
               <exclusions> 
                     <exclusion> 
                           <groupId>com.sun.jdmk</groupId> 
                           <artifactId>jmxtools</artifactId> 
                     </exclusion> 
                     <exclusion> 
                           <groupId>com.sun.jmx</groupId> 
                           <artifactId>jmxri</artifactId> 
                     </exclusion> 
               </exclusions> 
         </dependency> 
         <dependency> 
               <groupId>org.apache.logging.log4j</groupId> 
               <artifactId>log4j-slf4j-impl</artifactId> 
               <version>2.0-beta9</version> 
         </dependency> 
         <dependency> 
               <groupId>org.apache.logging.log4j</groupId> 
               <artifactId>log4j-1.2-api</artifactId> 
               <version>2.0-beta9</version> 
         </dependency> 

         <!-- https://mvnrepository.com/artifact/org.twitter4j/twitter4j-stream --> 
         <dependency> 
               <groupId>org.twitter4j</groupId> 
               <artifactId>twitter4j-stream</artifactId> 
               <version>4.0.6</version> 
         </dependency> 

   </dependencies> 
  1. 现在,我们需要创建一个名为TwitterData的类,其中包含从 Twitter 获取/流式传输数据并将其发布到 Kafka 集群的代码。我们假设您已经有一个运行中的 Kafka 集群和在 Kafka 集群中创建的twitterData主题。有关 Kafka 集群的安装和创建 Kafka 主题的信息,请参阅第八章,Storm 和 Kafka 的集成

该类包含twitter4j.conf.ConfigurationBuilder类的一个实例;我们需要在配置中设置访问令牌和消费者密钥,如源代码中所述。

  1. twitter4j.StatusListener类在onStatus()方法中返回推文的连续流。我们在onStatus()方法中使用 Kafka Producer 代码来发布推文到 Kafka。以下是TwitterData类的源代码:
public class TwitterData { 

   /** The actual Twitter stream. It's set up to collect raw JSON data */ 
   private TwitterStream twitterStream; 
   static String consumerKeyStr = "r1wFskT3q"; 
   static String consumerSecretStr = "fBbmp71HKbqalpizIwwwkBpKC"; 
   static String accessTokenStr = "298FPfE16frABXMcRIn7aUSSnNneMEPrUuZ"; 
   static String accessTokenSecretStr = "1LMNZZIfrAimpD004QilV1pH3PYTvM"; 

   public void start() { 
         ConfigurationBuilder cb = new ConfigurationBuilder(); 
         cb.setOAuthConsumerKey(consumerKeyStr); 
         cb.setOAuthConsumerSecret(consumerSecretStr); 
         cb.setOAuthAccessToken(accessTokenStr); 
         cb.setOAuthAccessTokenSecret(accessTokenSecretStr); 
         cb.setJSONStoreEnabled(true); 
         cb.setIncludeEntitiesEnabled(true); 
         // instance of TwitterStreamFactory 
         twitterStream = new TwitterStreamFactory(cb.build()).getInstance(); 

         final Producer<String, String> producer = new KafkaProducer<String, String>( 
                     getProducerConfig()); 
         // topicDetails 
         // new CreateTopic("127.0.0.1:2181").createTopic("twitterData", 2, 1); 

         /** Twitter listener **/ 
         StatusListener listener = new StatusListener() { 
               public void onStatus(Status status) { 
                     ProducerRecord<String, String> data = new ProducerRecord<String, String>( 
                                 "twitterData", DataObjectFactory.getRawJSON(status)); 
                     // send the data to kafka 
                     producer.send(data); 
               } 

               public void onException(Exception arg0) { 
                     System.out.println(arg0); 
               } 

               public void onDeletionNotice(StatusDeletionNotice arg0) { 
               } 

               public void onScrubGeo(long arg0, long arg1) { 
               } 

               public void onStallWarning(StallWarning arg0) { 
               } 

               public void onTrackLimitationNotice(int arg0) { 
               } 
         }; 

         /** Bind the listener **/ 
         twitterStream.addListener(listener); 

         /** GOGOGO **/ 
         twitterStream.sample(); 
   } 

   private Properties getProducerConfig() { 

         Properties props = new Properties(); 

         // List of kafka borkers. Complete list of brokers is not required as 
         // the producer will auto discover the rest of the brokers. 
         props.put("bootstrap.servers", "localhost:9092"); 
         props.put("batch.size", 1); 
         // Serializer used for sending data to kafka. Since we are sending 
         // string, 
         // we are using StringSerializer. 
         props.put("key.serializer", 
                     "org.apache.kafka.common.serialization.StringSerializer"); 
         props.put("value.serializer", 
                     "org.apache.kafka.common.serialization.StringSerializer"); 

         props.put("producer.type", "sync"); 

         return props; 

   } 

   public static void main(String[] args) throws InterruptedException { 
         new TwitterData().start(); 
   } 

在执行TwitterData类之前,请使用有效的 Kafka 属性。

在执行上述类之后,用户将在 Kafka 中获得 Twitter 推文的实时流。在下一节中,我们将介绍如何使用 Storm 来计算收集到的推文的情感。

Kafka spout,情感 bolt 和 HDFS bolt

在本节中,我们将编写/配置一个 Kafka spout 来消费来自 Kafka 集群的推文。我们将使用开源的 Storm spout 连接器来从 Kafka 消费数据:

  1. 使用groupIDcom.stormadvanceartifactIdKafka_twitter_topology创建一个新的 maven 项目。

  2. 将以下 maven 依赖项添加到pom.xml文件中:

   <dependencies> 
         <dependency> 
               <groupId>org.codehaus.jackson</groupId> 
               <artifactId>jackson-mapper-asl</artifactId> 
               <version>1.9.13</version> 
         </dependency> 

         <dependency> 
               <groupId>org.apache.hadoop</groupId> 
               <artifactId>hadoop-client</artifactId> 
               <version>2.2.0</version> 
               <exclusions> 
                     <exclusion> 
                           <groupId>org.slf4j</groupId> 
                           <artifactId>slf4j-log4j12</artifactId> 
                     </exclusion> 
               </exclusions> 
         </dependency> 
         <dependency> 
               <groupId>org.apache.hadoop</groupId> 
               <artifactId>hadoop-hdfs</artifactId> 
               <version>2.2.0</version> 
               <exclusions> 
                     <exclusion> 
                           <groupId>org.slf4j</groupId> 
                           <artifactId>slf4j-log4j12</artifactId> 
                     </exclusion> 
               </exclusions> 
         </dependency> 
         <!-- Dependency for Storm-Kafka spout --> 
         <dependency> 
               <groupId>org.apache.storm</groupId> 
               <artifactId>storm-kafka</artifactId> 
               <version>1.0.2</version> 
               <exclusions> 
                     <exclusion> 
                           <groupId>org.apache.kafka</groupId> 
                           <artifactId>kafka-clients</artifactId> 
                     </exclusion> 
               </exclusions> 
         </dependency> 

         <dependency> 
               <groupId>org.apache.kafka</groupId> 
               <artifactId>kafka_2.10</artifactId> 
               <version>0.9.0.1</version> 
               <exclusions> 
                     <exclusion> 
                           <groupId>com.sun.jdmk</groupId> 
                           <artifactId>jmxtools</artifactId> 
                     </exclusion> 
                     <exclusion> 
                           <groupId>com.sun.jmx</groupId> 
                           <artifactId>jmxri</artifactId> 
                     </exclusion> 
               </exclusions> 
         </dependency> 

         <dependency> 
               <groupId>org.apache.storm</groupId> 
               <artifactId>storm-core</artifactId> 
               <version>1.0.2</version> 
               <scope>provided</scope> 
         </dependency> 
   </dependencies> 
   <repositories> 
         <repository> 
               <id>clojars.org</id> 
               <url>http://clojars.org/repo</url> 
         </repository> 
   </repositories> 
  1. com.stormadvance.Kafka_twitter_topology.topology包内创建一个StormHDFSTopology类,并添加以下依赖项以指定 Kafka spout 从twitterData主题中消费数据:
BrokerHosts zkHosts = new ZkHosts("localhost:2181"); 

       // Create the KafkaSpout configuartion 
       // Second argument is the topic name 
       // Third argument is the zookeeper root for Kafka 
       // Fourth argument is consumer group id 
       SpoutConfig kafkaConfig = new SpoutConfig(zkHosts, "twitterData", "", 
                   "id7"); 

       // Specify that the kafka messages are String 
       kafkaConfig.scheme = new SchemeAsMultiScheme(new StringScheme()); 

       // We want to consume all the first messages in the topic everytime 
       // we run the topology to help in debugging. In production, this 
       // property should be false 
       kafkaConfig.startOffsetTime = kafka.api.OffsetRequest 
                   .EarliestTime(); 

       // Now we create the topology 
       TopologyBuilder builder = new TopologyBuilder(); 

       // set the kafka spout class 
       builder.setSpout("KafkaSpout", new KafkaSpout(kafkaConfig), 1); 
  1. com.stormadvance.Kafka_twitter_topology.bolt包内创建一个JSONParsingBolt类,以从 Twitter 接收的 JSON 推文中提取推文文本:
public class JSONParsingBolt extends BaseRichBolt implements Serializable{ 

   private OutputCollector collector; 

   public void prepare(Map stormConf, TopologyContext context, 
               OutputCollector collector) { 
         this.collector = collector; 

   } 

   public void execute(Tuple input) { 
         try { 
               String tweet = input.getString(0); 
               Map<String, Object> map = new ObjectMapper().readValue(tweet, Map.class); 
               collector.emit("stream1",new Values(tweet)); 
               collector.emit("stream2",new Values(map.get("text"))); 
               this.collector.ack(input); 
         } catch (Exception exception) { 
               exception.printStackTrace(); 
               this.collector.fail(input); 
         } 
   } 

   public void declareOutputFields(OutputFieldsDeclarer declarer) { 
         declarer.declareStream("stream1",new Fields("tweet")); 
         declarer.declareStream("stream2",new Fields("text")); 
   } 

} 
  1. com.stormadvance.Kafka_twitter_topology.sentiments包内创建一个SentimentBolt类,以创建每条推文的情感。我们使用字典文件来查找推文中使用的词语是积极的还是消极的,并计算整条推文的情感。以下是该类的源代码:
public final class SentimentBolt extends BaseRichBolt { 
   private static final Logger LOGGER = LoggerFactory 
               .getLogger(SentimentBolt.class); 
   private static final long serialVersionUID = -5094673458112825122L; 
   private OutputCollector collector; 
   private String path; 
   public SentimentBolt(String path) { 
         this.path = path; 
   } 
   private Map<String, Integer> afinnSentimentMap = new HashMap<String, Integer>(); 

   public final void prepare(final Map map, 
               final TopologyContext topologyContext, 
               final OutputCollector collector) { 
         this.collector = collector; 
         // Bolt will read the AFINN Sentiment file [which is in the classpath] 
         // and stores the key, value pairs to a Map. 
         try { 
               BufferedReader br = new BufferedReader(new FileReader(path)); 
               String line; 
               while ((line = br.readLine()) != null) { 
                     String[] tabSplit = line.split("\t"); 
                     afinnSentimentMap.put(tabSplit[0], 
                                 Integer.parseInt(tabSplit[1])); 
               } 
               br.close(); 

         } catch (final IOException ioException) { 
               LOGGER.error(ioException.getMessage(), ioException); 
               ioException.printStackTrace(); 
               System.exit(1); 
         } 

   }
   public final void declareOutputFields( 
               final OutputFieldsDeclarer outputFieldsDeclarer) { 
         outputFieldsDeclarer.declare(new Fields("tweet","sentiment")); 
   } 

   public final void execute(final Tuple input) { 
         try { 
         final String tweet = (String) input.getValueByField("text"); 
         final int sentimentCurrentTweet = getSentimentOfTweet(tweet); 
         collector.emit(new Values(tweet,sentimentCurrentTweet)); 
         this.collector.ack(input); 
         }catch(Exception exception) { 
               exception.printStackTrace(); 
               this.collector.fail(input); 
         } 
   } 

   /** 
    * Gets the sentiment of the current tweet. 
    * 
    * @param status 
    *            -- Status Object. 
    * @return sentiment of the current tweet. 
    */ 
   private final int getSentimentOfTweet(final String text) { 
         // Remove all punctuation and new line chars in the tweet. 
         final String tweet = text.replaceAll("\\p{Punct}|\\n", " ") 
                     .toLowerCase(); 
         // Splitting the tweet on empty space. 
         final Iterable<String> words = Splitter.on(' ').trimResults() 
                     .omitEmptyStrings().split(tweet); 
         int sentimentOfCurrentTweet = 0; 
         // Loop thru all the wordsd and find the sentiment of this tweet. 
         for (final String word : words) { 
               if (afinnSentimentMap.containsKey(word)) { 
                     sentimentOfCurrentTweet += afinnSentimentMap.get(word); 
               } 
         } 
         LOGGER.debug("Tweet : Sentiment {} ==> {}", tweet, 
                     sentimentOfCurrentTweet); 
         return sentimentOfCurrentTweet; 
   } 

} 
  1. 我们需要将情感存储在 HDFS 中以生成图表或特征分析。接下来,在StormHDFSTopology类中添加以下代码以链接 spout 和 bolts:
// use "|" instead of "," for field delimiter 
         RecordFormat format = new DelimitedRecordFormat() 
                     .withFieldDelimiter(","); 

         // sync the filesystem after every 1k tuples 
         SyncPolicy syncPolicy = new CountSyncPolicy(1000); 

         // rotate files when they reach 5MB 
         FileRotationPolicy rotationPolicy = new FileSizeRotationPolicy(5.0f, 
                     Units.MB); 

         FileNameFormat fileNameFormatSentiment = new DefaultFileNameFormat() 
         .withPath("/sentiment-tweet/"); 

         HdfsBolt hdfsBolt2 = new HdfsBolt().withFsUrl("hdfs://127.0.0.1:8020") 
                     .withFileNameFormat(fileNameFormatSentiment).withRecordFormat(format) 
                     .withRotationPolicy(rotationPolicy).withSyncPolicy(syncPolicy); 

         //builder.setBolt("HDFSBolt", hdfsBolt).shuffleGrouping("KafkaSpout"); 
         builder.setBolt("json", new JSONParsingBolt()).shuffleGrouping("KafkaSpout"); 

         // 
         builder.setBolt("sentiment", new SentimentBolt("/home/centos/Desktop/workspace/storm_twitter/src/main/resources/AFINN-111.txt")).shuffleGrouping("json","stream2"); 

         // 
         builder.setBolt("HDFS2", hdfsBolt2).shuffleGrouping("sentiment"); 
  1. 以下是StormHDFSTopology类的完整代码:
public class StormHDFSTopology { 

   public static void main(String[] args) { 
         // zookeeper hosts for the Kafka cluster 
         BrokerHosts zkHosts = new ZkHosts("localhost:2181"); 

         // Create the KafkaSpout configuartion 
         // Second argument is the topic name 
         // Third argument is the zookeeper root for Kafka 
         // Fourth argument is consumer group id 
         SpoutConfig kafkaConfig = new SpoutConfig(zkHosts, "twitterData", "", 
                     "id7"); 

         // Specify that the kafka messages are String 
         kafkaConfig.scheme = new SchemeAsMultiScheme(new StringScheme()); 

         // We want to consume all the first messages in the topic everytime 
         // we run the topology to help in debugging. In production, this 
         // property should be false 
         kafkaConfig.startOffsetTime = kafka.api.OffsetRequest 
                     .EarliestTime(); 

         // Now we create the topology 
         TopologyBuilder builder = new TopologyBuilder(); 

         // set the kafka spout class 
         builder.setSpout("KafkaSpout", new KafkaSpout(kafkaConfig), 1); 

         // use "|" instead of "," for field delimiter 
         RecordFormat format = new DelimitedRecordFormat() 
                     .withFieldDelimiter(","); 

         // sync the filesystem after every 1k tuples 
         SyncPolicy syncPolicy = new CountSyncPolicy(1000); 

         // rotate files when they reach 5MB 
         FileRotationPolicy rotationPolicy = new FileSizeRotationPolicy(5.0f, 
                     Units.MB); 

         FileNameFormat fileNameFormatSentiment = new DefaultFileNameFormat() 
         .withPath("/sentiment-tweet/"); 

         HdfsBolt hdfsBolt2 = new HdfsBolt().withFsUrl("hdfs://127.0.0.1:8020") 
                     .withFileNameFormat(fileNameFormatSentiment).withRecordFormat(format) 
                     .withRotationPolicy(rotationPolicy).withSyncPolicy(syncPolicy); 

         //builder.setBolt("HDFSBolt", hdfsBolt).shuffleGrouping("KafkaSpout"); 
         builder.setBolt("json", new JSONParsingBolt()).shuffleGrouping("KafkaSpout"); 

         // 
         builder.setBolt("sentiment", new SentimentBolt("/home/centos/Desktop/workspace/storm_twitter/src/main/resources/AFINN-111.txt")).shuffleGrouping("json","stream2"); 

         // 
         builder.setBolt("HDFS2", hdfsBolt2).shuffleGrouping("sentiment"); 

         // create an instance of LocalCluster class for executing topology in 
         // local mode. 
         LocalCluster cluster = new LocalCluster(); 
         Config conf = new Config(); 

         // Submit topology for execution 
         cluster.submitTopology("KafkaToplogy", conf, builder.createTopology()); 

         try { 
               // Wait for some time before exiting 
               System.out.println("Waiting to consume from kafka"); 
               Thread.sleep(6000000); 
         } catch (Exception exception) { 
               System.out.println("Thread interrupted exception : " + exception); 
         } 

         // kill the KafkaTopology 
         cluster.killTopology("KafkaToplogy"); 

         // shut down the storm test cluster 
         cluster.shutdown(); 

   } 
} 
  1. 现在,我们可以为整个项目创建 JAR 并根据本书中的第二章,Storm 部署、拓扑开发和拓扑选项中定义的方式部署到 Storm 集群。

总结

在本节中,我们介绍了如何使用 Twitter 流 API 读取 Twitter 推文,如何处理推文以计算输入 JSON 记录中的推文文本,计算推文的情感,并将最终输出存储在 HDFS 中。

通过这一点,我们来到了本书的结尾。在本书的过程中,我们已经从开始使用 Apache Storm 迈出了一大步,发展成为了真实世界应用程序的开发者。在这里,我们想总结一下我们所学到的一切。

我们向您介绍了 Storm 的基本概念和组件,并介绍了如何在本地和集群模式下编写和部署/运行拓扑。我们还介绍了 Storm 的基本命令,并介绍了如何在运行时修改 Storm 拓扑的并行性。我们还专门介绍了监控 Storm 的整个章节,这在开发过程中经常被忽视,但是对于任何生产环境来说都是至关重要的部分。您还了解了 Trident,这是低级 Storm API 的抽象,可用于开发更复杂的拓扑并维护应用程序状态。

没有任何企业应用程序可以仅使用一种技术开发,因此我们的下一步是看看如何将 Storm 与其他大数据工具和技术集成。我们看到了 Storm 与 Kafka、Hadoop、HBase 和 Redis 的特定实现。大多数大数据应用程序使用 Ganglia 作为集中监控工具,因此我们还介绍了如何通过 JMX 和 Ganglia 监控 Storm 集群。

你还学习了有关使用各种模式将不同数据源与 Storm 集成的知识。最后,在第十一章《使用 Storm 进行 Apache 日志处理》和本章中,我们实施了两个 Apache Storm 中的案例研究,这可以作为开发更复杂应用程序的起点。

我们希望阅读本书对你来说是一次富有成效的旅程,并且你对 Storm 以及一般实时流处理应用程序开发的各个方面有了基本的了解。Apache Storm 正在成为流处理的事实标准,我们希望本书能够成为你启动构建实时流处理应用程序的激动人心旅程的催化剂。

posted @ 2024-05-21 12:54  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报