GPT3-探索指南-全-
GPT3 探索指南(全)
原文:
zh.annas-archive.org/md5/e19ec4b9c1d08c12abd2983dace7ff20译者:飞龙
前言
Apache Storm 是一个强大的框架,用于创建复杂的流程,可以处理大量数据。它通过通用的喷嘴和螺栓概念,以及简单的部署和监控工具,允许开发者专注于他们工作流程的具体细节,无需重新发明轮子。
然而,Storm 是用 Java 编写的。虽然它支持除了 Java 之外的其他编程语言,但工具不完整,文档很少,示例也很少。
本书的一位作者创建了 Petrel,这是第一个完全支持使用 100% Python 创建 Storm 拓扑的框架。他亲身经历了在 Java 工具集上构建 Python Storm 拓扑的挑战。本书填补了这一空白,为所有经验水平的 Python 开发者提供了一个资源,帮助他们使用 Storm 构建自己的应用程序。
本书涵盖的内容
第一章,熟悉 Storm,提供了关于 Storm 的用例、不同的安装模式和 Storm 的配置的详细信息。
第二章,Storm 的解剖结构,介绍了 Storm 特定的术语、流程、Storm 的容错性、在 Storm 中调整并行性以及保证元组处理,并对这些内容的每个方面进行了详细解释。
第三章,介绍 Petrel,介绍了一个用于在 Python 中构建 Storm 拓扑的框架。本章介绍了 Petrel 的安装,并包含了一个简单的示例。
第四章,示例拓扑 – Twitter,提供了一个深入示例,展示了实时计算 Twitter 数据统计信息的拓扑。示例介绍了使用滴答元组,这对于需要按计划计算统计信息或其他操作的拓扑非常有用。在本章中,您还可以看到拓扑如何访问配置数据。
第五章,使用 Redis 和 MongoDB 进行持久化,更新了示例 Twitter 拓扑以使用 Redis,这是一个流行的键值存储。它展示了如何使用内置的 Redis 操作简化复杂的 Python 计算逻辑。本章以将 Twitter 数据存储在 MongoDB(一个流行的 NoSQL 数据库)中的示例结束,并使用其聚合功能生成报告。
第六章,Petrel 在实践中,教授了使开发者更有效地使用 Storm 的实用技能。您将学习如何使用 Petrel 为运行在 Storm 之外的喷嘴和螺栓组件创建自动化测试。您还可以看到如何使用图形调试器调试运行在 Storm 内部的拓扑。
附录, 使用 Supervisord 管理 Storm,是使用集群上的管理器对 Storm 进行监控和控制的实际演示。
您需要这本书什么
您需要一个装有 Python 2.7、Java 7 JDK 和 Apache Storm 0.9.3 的计算机。推荐使用 Ubuntu,但不是必需的。
这本书面向的对象
这本书适合初学者以及希望使用 Storm 实时处理大数据的 Python 高级开发者。虽然熟悉 Java 运行环境有助于安装和配置 Storm,但本书中的所有代码示例都是用 Python 编写的。
规范
在这本书中,您将找到许多不同风格的文本,以区分不同类型的信息。以下是一些这些风格的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:"可以使用storm.yaml进行 Storm 配置,该文件位于conf文件夹中"。
代码块如下设置:
import nltk.corpus
from petrel import storm
from petrel.emitter import BasicBolt
class SplitSentenceBolt(BasicBolt):
def __init__(self):
super(SplitSentenceBolt, self).__init__(script=__file__)
self.stop = set(nltk.corpus.stopwords.words('english'))
self.stop.update(['http', 'https', 'rt'])
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
import logging
from collections import defaultdict
from petrel import storm
from petrel.emitter import BasicBolt
任何命令行输入或输出都如下所示:
tail -f petrel24748_totalrankings.log
新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"最后,点击创建您的 Twitter 应用程序"。
注意
警告或重要提示将以如下框的形式出现。
小贴士
小贴士和技巧如下所示。
读者反馈
我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中受益的标题非常重要。
要向我们发送一般反馈,只需发送一封电子邮件到<feedback@packtpub.com>,并在邮件的主题中提及书名。
如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您已经成为 Packt 书籍的骄傲拥有者,我们有一些东西可以帮助您充分利用您的购买。
下载示例代码
您可以从www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
错误清单
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过从www.packtpub.com/support选择您的标题来查看任何现有勘误。
盗版
互联网上版权材料的盗版是一个持续存在的问题,跨越所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
如果您在本书的任何方面遇到问题,请通过<copyright@packtpub.com>联系我们,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您在本书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。
第一章. 熟悉 Storm
在本章中,你将熟悉以下主题:
-
Storm 概述
-
“Storm 之前”的时代和 Storm 的关键特性
-
Storm 集群模式
-
Storm 安装
-
启动各种守护进程
-
玩转 Storm 配置
在本章的整个过程中,你将了解为什么 Storm 在业界引起了轰动,以及为什么它在当今场景中是相关的。什么是实时计算?我们还将解释 Storm 的不同集群模式、安装和配置方法。
Storm 概述
Storm 是一个分布式、容错且高度可扩展的平台,用于实时处理流数据。它在 2014 年 9 月成为 Apache 顶级项目,之前自 2013 年 9 月起一直是 Apache 孵化器项目。
在大规模上实时处理已成为企业的需求。Apache Storm 提供了实时处理数据(即元组或流)的能力,这些数据(元组或流)在到达时即可处理,具有分布式计算选项。能够向 Storm 集群添加更多机器使 Storm 可扩展。然后,随着 Storm 而来的第三件最重要的事情是容错性。如果 Storm 程序(也称为拓扑)配备了可靠的 spout,它可以重新处理由于机器故障而丢失的失败元组,并提供容错性。它基于 XOR 魔法,将在第二章Storm 解剖中解释。
Storm 最初是由 Nathan Marz 及其团队在 BackType 创建的。该项目在被 Twitter 收购后开源。有趣的是,Storm 被标记为实时 Hadoop。
Storm 非常适合许多实时用例。以下是一些有趣的用例说明:
-
ETL 管道:ETL 代表提取、转换和加载。这是 Storm 非常常见的用例。数据可以从任何来源提取或读取。在这里,数据可以是复杂的 XML、JDBC 结果集行,或者仅仅是几个键值记录。数据(在 Storm 中也称为元组)可以即时地添加更多信息,转换为所需的存储格式,并存储在 NoSQL/RDBMS 数据存储中。所有这些都可以通过简单的 Storm 程序以非常高的吞吐量实时完成。使用 Storm ETL 管道,你可以高速地将数据导入大数据仓库。
-
趋势话题分析:Twitter 使用此类用例来了解在给定时间段或当前的趋势话题。有无数种用例,并且需要实时地找到最热门的趋势。Storm 非常适合此类用例。你还可以借助任何数据库执行值的运行聚合。
-
监管检查引擎:实时事件数据可以通过业务特定的监管算法进行传递,该算法可以实时执行合规性检查。银行使用这些工具进行实时交易数据检查。
Storm 理想地适用于任何需要快速且可靠地处理数据的用例,每秒处理超过 10,000 条消息,数据一到即可处理。实际上,10,000+ 是一个较小的数字。Twitter 能够在大型集群上每秒处理数百万条推文。这取决于 Storm 拓扑编写的好坏、调优程度以及集群大小。
Storm 程序(即拓扑)设计为全天候运行,除非有人明确停止它们,否则不会停止。
Storm 使用 Clojure 和 Java 编写。Clojure 是一种运行在 JVM 上的 Lisp 函数式编程语言,非常适合并发和并行编程。Storm 利用过去 10 年中构建的成熟 Java 库。所有这些都可以在 storm/lib 文件夹中找到。
在 Storm 时代之前
在 Storm 流行之前,实时或准实时处理问题是通过中间代理和消息队列的帮助解决的。监听器或工作进程使用 Python 或 Java 语言运行。对于并行处理,代码依赖于编程语言本身提供的线程模型。很多时候,旧的工作方式没有很好地利用 CPU 和内存。在某些情况下,也使用了大型机,但随着时间的推移,它们也变得过时了。分布式计算并不容易。在旧的工作方式中,要么有大量的中间输出,要么有多个跳跃。无法自动执行故障重放。Storm 非常好地解决了所有这些痛点。它是可用的最佳实时计算框架之一。
Storm 的关键特性
这里是 Storm 的关键特性;它们解决了上述问题:
-
易于编程:学习 Storm 框架很容易。你可以使用你选择的编程语言编写代码,也可以使用该编程语言的现有库。没有任何妥协。
-
Storm 已经支持大多数编程语言:然而,即使某些语言不受支持,也可以通过提供代码和配置,使用在 Storm 数据规范语言(DSL)中定义的 JSON 协议来实现。
-
水平扩展或分布式计算是可能的:通过向 Storm 集群添加更多机器来增加计算能力,而无需停止正在运行的程序,也称为拓扑。
-
容错性: Storm 管理工作节点和机器级别的故障。每个进程的心跳被跟踪以管理不同类型的故障,例如一台机器上的任务故障或整个机器的故障。
-
保证消息处理:在 Storm 进程中,对消息(元组)进行自动和显式 ACK 有相应的机制。如果没有收到 ACK,Storm 可以对消息进行回复。
-
免费、开源,并且拥有大量的开源社区支持:作为 Apache 项目,Storm 具有免费分发和修改的权利,无需担心法律方面的问题。Storm 受到了开源社区的广泛关注,并吸引了许多优秀的开发者为其代码贡献力量。
Storm 集群模式
根据需求,Storm 集群可以设置为四种模式。如果你想搭建一个大型集群,可以选择分布式安装。如果你想学习 Storm,那么可以选择单机安装。如果你想连接到现有的 Storm 集群,请使用客户端模式。最后,如果你想在一个 IDE 上进行开发,只需解压storm TAR 文件,并指向storm库的所有依赖项。在初始学习阶段,单机 Storm 安装实际上是你所需要的。
开发者模式
开发者可以从分发站点下载 Storm,将其解压到$HOME目录下的某个位置,并以本地模式提交 Storm 拓扑。一旦拓扑在本地成功测试,就可以提交到集群上运行。
单机 Storm 集群
这种模式最适合学生和中等规模的计算。在这里,包括Zookeeper、Nimbus和Supervisor在内的所有操作都在单台机器上运行。使用Storm/bin目录下的所有命令。此外,不需要额外的 Storm 客户端。你可以在同一台机器上完成所有操作。以下图示很好地展示了这种情况:

多机 Storm 集群
当你有大规模计算需求时,这个选项是必需的。这是一个水平扩展选项。以下图详细解释了这种情况。在这个图中,我们有五台物理机器,为了提高系统的容错性,我们在两台机器上运行 Zookeeper。如图所示,机器 1和机器 2是一组 Zookeeper 机器;在任何时候,其中一台是领导者,当它死亡时,另一台成为领导者。Nimbus是一个轻量级进程,因此它可以在机器 1 或机器 2 上运行。我们还有机器 3、机器 4和机器 5专门用于执行实际处理。这些机器(3、4 和 5)都需要一个 supervisor 守护进程来运行。机器 3、4 和 5 应该知道 Nimbus/Zookeeper 守护进程在哪里运行,并且这个条目应该出现在它们的storm.yaml文件中。

因此,每台物理机器(3、4 和 5)运行一个监督守护进程,每台机器的 storm.yaml 文件指向运行 Nimbus 的机器的 IP 地址(这可以是 1 或 2)。所有监督机器都必须将 Zookeeper IP 地址(1 和 2)添加到 storm.yaml 文件中。Storm UI 守护进程应该在 Nimbus 机器上运行(这可以是 1 或 2)。
Storm 客户端
当你有一个多机 Storm 集群时,才需要 Storm 客户端。要启动客户端,解压 Storm 发行版并将 Nimbus IP 地址添加到 storm.yaml 文件中。Storm 客户端可以用于从命令行选项提交 Storm 拓扑并检查运行拓扑的状态。Storm 版本低于 0.9 的应该将 yaml 文件放在 $STORM_HOME/.storm/storm.yaml 内(对于新版本不是必需的)。
注意
jps 命令是一个非常有用的 Unix 命令,用于查看 Zookeeper、Nimbus 和 Supervisor 的 Java 进程 ID。kill -9 <pid> 选项可以停止正在运行的过程。jps 命令只有在 JAVA_HOME 设置在 PATH 环境变量中时才会工作。
Storm 安装的先决条件
安装 Java 和 Python 很简单。假设我们的 Linux 机器已经安装了 Java 和 Python:
-
一台 Linux 机器(Storm 版本 0.9 及以后的版本也可以在 Windows 机器上运行)
-
Java 6 (
set export PATH=$PATH:$JAVA_HOME/bin) -
Python 2.6(运行 Storm 守护进程和管理命令所必需)
我们将对 storm 配置文件(即 storm.yaml)进行很多修改,该文件实际上位于 $STORM_HOME/config 下。首先,我们启动 Zookeeper 进程,该进程执行 Nimbus 和监督者之间的协调。然后,我们启动 Nimbus 主守护进程,该守护进程在 Storm 集群中分发代码。接下来,监督守护进程监听分配给其运行的节点的工作(由 Nimbus 分配),并根据需要启动和停止工作进程。
ZeroMQ/JZMQ 和 Netty 是 JVM 间通信库,允许两台机器或两个 JVM 之间相互发送和接收进程数据(元组)。JZMQ 是 ZeroMQ 的 Java 绑定。Storm(0.9+)的最新版本已经迁移到 Netty。如果你下载了 Storm 的旧版本,则需要安装 ZeroMQ 和 JZMQ。在这本书中,我们将只考虑 Storm 的最新版本,因此你实际上不需要 ZeroMQ/JZMQ。
Zookeeper 安装
Zookeeper 是 Storm 集群的协调器。Nimbus 和工作节点之间的交互是通过 Zookeeper 完成的。Zookeeper 的安装过程在官方网站上解释得非常详细,请参阅zookeeper.apache.org/doc/trunk/zookeeperStarted.html#sc_InstallingSingleMode。
设置可以从以下链接下载:
archive.apache.org/dist/zookeeper/zookeeper-3.3.5/zookeeper-3.3.5.tar.gz。下载后,编辑 zoo.cfg 文件。
以下是用到的 Zookeeper 命令:
-
启动
zookeeper进程:../zookeeper/bin/./zkServer.sh start -
检查
zookeeper服务的运行状态:../zookeeper/bin/./zkServer.sh status -
停止
zookeeper服务:../zookeeper/bin/./zkServer.sh stop
或者,使用 jps 查找 <pid>,然后使用 kill -9 <pid> 杀死进程。
Storm 安装
Storm 可以通过以下两种方式之一进行安装:
-
使用 Git 从此位置获取 Storm 发布版:
-
直接从以下链接下载:
storm.apache.org/downloads.html
Storm 配置可以通过 storm.yaml 完成,该文件位于 conf 文件夹中。
以下是一个单机 Storm 集群安装的配置。
端口 # 2181 是 Zookeeper 的默认端口。要添加多个 zookeeper,请保持条目之间用空格分隔:
storm.zookeeper.servers:
- "localhost"
# you must change 2181 to another value if zookeeper running on another port.
storm.zookeeper.port: 2181
# In single machine mode nimbus run locally so we are keeping it localhost.
# In distributed mode change localhost to machine name where nimbus daemon is running.
nimbus.host: "localhost"
# Here storm will generate logs of workers, nimbus and supervisor.
storm.local.dir: "/var/stormtmp"
java.library.path: "/usr/local/lib"
# Allocating 4 ports for workers. More numbers can also be added.
supervisor.slots.ports:
- 6700
- 6701
- 6702
- 6703
# Memory is allocated to each worker. In below case we are allocating 768 mb per worker.worker.childopts: "-Xmx768m"
# Memory to nimbus daemon- Here we are giving 512 mb to nimbus.
nimbus.childopts: "-Xmx512m"
# Memory to supervisor daemon- Here we are giving 256 mb to supervisor.
注意
注意 supervisor.childopts: "-Xmx256m"。在此设置中,我们预留了四个 supervisor 端口,这意味着最多有四个工作进程可以在此机器上运行。
storm.local.dir:如果启动 Nimbus 和 Supervisor 时出现问题,应清理此目录位置。在 Windows 机器上在本地 IDE 上运行拓扑时,应清理 C:\Users\<用户名>\AppData\Local\Temp。
启用本地(仅 Netty)依赖项
Netty 允许 JVM 之间的通信,并且使用起来非常简单。
Netty 配置
您实际上不需要为 Netty 安装任何额外的软件。这是因为它是一个基于纯 Java 的通信库。所有新的 Storm 版本都支持 Netty。
将以下行添加到您的 storm.yaml 文件中。根据您的使用情况配置和调整这些值:
storm.messaging.transport: "backtype.storm.messaging.netty.Context"
storm.messaging.netty.server_worker_threads: 1
storm.messaging.netty.client_worker_threads: 1
storm.messaging.netty.buffer_size: 5242880
storm.messaging.netty.max_retries: 100
storm.messaging.netty.max_wait_ms: 1000
storm.messaging.netty.min_wait_ms: 100
启动守护进程
Storm 守护进程是在您将程序提交到集群之前需要预先运行的进程。当您在本地 IDE 上运行拓扑程序时,这些守护进程会在预定义的端口上自动启动,但在集群中,它们必须始终运行:
-
启动主守护进程
nimbus。转到 Storm 安装目录下的bin目录并执行以下命令(假设zookeeper正在运行):./storm nimbus Alternatively, to run in the background, use the same command with nohup, like this: Run in background nohup ./storm nimbus & -
现在我们必须启动
supervisor守护进程。转到 Storm 安装目录下的bin目录并执行以下命令:./storm supervisor要在后台运行,请使用以下命令:
nohup ./storm supervisor &注意
如果 Nimbus 或 Supervisors 重新启动,正在运行的拓扑不受影响,因为它们都是无状态的。
-
让我们启动
stormUI。Storm UI 是一个可选进程。它帮助我们查看正在运行的拓扑的 Storm 统计信息。您可以看到分配给特定拓扑的执行器和工人数。运行 storm UI 所需的命令如下:./storm ui或者,要在后台运行,请使用带有
nohup的以下行:nohup ./storm ui &要访问 Storm UI,请访问
http://localhost:8080。 -
我们现在将启动
storm logviewer。Storm UI 是另一个可选进程,用于通过浏览器查看日志。您还可以使用$STORM_HOME/logs文件夹中的命令行选项查看storm日志。要启动日志查看器,请使用此命令:./storm logviewer要在后台运行,请使用以下带有
nohup的行:nohup ./storm logviewer &注意
要访问 Storm 的日志,请访问
http://localhost:8000log viewer守护进程应在每台机器上运行。另一种访问<machine name>的6700工作端口日志的方法如下:<Machine name>:8000/log?file=worker-6700.log -
DRPC 守护进程:DRPC 是另一个可选服务。DRPC 代表 分布式远程过程调用。如果您想通过 DRPC 客户端将参数外部提供给 Storm 拓扑,则需要 DRPC 守护进程。请注意,参数只能提供一次,并且 DRPC 客户端可以长时间等待 Storm 拓扑完成处理并返回。由于首先,它会对客户端造成阻塞,其次,您一次只能提供一个参数,因此 DRPC 并不是在项目中使用的一个流行选项。Python 和 Petrel 不支持 DRPC。
总结,启动进程的步骤如下:
-
首先,所有的 Zookeeper 守护进程。
-
Nimbus 守护进程。
-
在一台或多台机器上的管理守护进程。
-
运行在 Nimbus 上的 UI 守护进程(可选)。
-
日志查看器守护进程(可选)。
-
提交拓扑。
您可以随时重启 nimbus 守护进程,而不会对现有进程或拓扑造成任何影响。您也可以重启管理守护进程,并且可以随时向 Storm 集群添加更多管理节点。
要将 jar 提交给 Storm 集群,请转到 Storm 安装目录的 bin 目录并执行以下命令:
./storm jar <path-to-topology-jar> <class-with-the-main> <arg1> … <argN>
玩转可选配置
所有之前的设置都是启动集群所必需的,但还有许多其他设置是可选的,可以根据拓扑的需求进行调整。前缀可以帮助找到配置的性质。完整的默认 yaml 配置列表可在 github.com/apache/storm/blob/master/conf/defaults.yaml 找到。
配置可以通过其前缀来识别。例如,所有 UI 配置都以 ui* 开头。
| 配置的性质 | 查找前缀 |
|---|---|
| 一般 | storm.* |
| Nimbus | nimbus.* |
| UI | ui.* |
| 日志查看器 | logviewer.* |
| DRPC | drpc.* |
| 管理器 | supervisor.* |
| 拓扑 | topology.* |
所有这些可选配置都可以添加到 STORM_HOME/conf/storm.yaml 中,以更改除默认值之外的所有设置。所有以 topology.* 开头的设置既可以从拓扑中程序化设置,也可以从 storm.yaml 中设置。所有其他设置只能从 storm.yaml 文件中设置。例如,以下表格显示了三种不同的参数设置方式。然而,这三种方式都做了同样的事情:
| /conf/storm.yaml | 拓扑构建器 | 自定义 yaml |
|---|---|---|
更改 storm.yaml(影响集群中所有拓扑) |
在编写代码时更改拓扑构建器(仅影响当前拓扑) | 将 topology.yaml 作为命令行选项提供(仅影响当前拓扑) |
topology.workers: 1 |
conf.setNumberOfWorker(1); 这是通过 Python 代码提供的 |
创建包含类似 storm.yaml 的条目的 topology.yaml,并在运行拓扑时提供它 Python:petrel submit --config topology.yaml |
在 storm.yaml 中的任何配置更改都将影响所有正在运行拓扑,但当你使用代码中的 conf.setXXX 选项时,不同的拓扑可以覆盖该选项,最适合它们的是哪个。
摘要
这里是第一章的结论。本章概述了在 Storm 出现之前应用程序是如何开发的。随着我们阅读本章并接近结论,我们也获得了对实时计算是什么以及 Storm 作为一个编程框架为何如此受欢迎的简要了解。本章教会了你如何执行 Storm 配置。它还提供了关于 Storm 守护进程、Storm 集群及其升级的详细信息。在下一章中,我们将探讨 Storm 结构的细节。
第二章. Storm 的解剖结构
本章详细介绍了 Storm 技术的内部结构和过程。在本章中,我们将涵盖以下主题:
-
Storm 处理过程
-
Storm 拓扑特定的术语
-
进程间通信
-
Storm 中的容错性
-
保证元组处理
-
Storm 中的并行性——扩展分布式计算
随着我们进入本章,你将详细了解 Storm 的处理过程及其在细节中的作用。在本章中,将解释各种与 Storm 相关的专业术语。你将学习 Storm 如何实现不同类型故障的容错性。我们将探讨什么是保证消息处理,最重要的是,如何配置 Storm 的并行性以实现快速和可靠的处理。
Storm 处理过程
我们首先从 Nimbus 开始,它是 Storm 的实际入口点守护进程。为了与 Hadoop 进行比较,Nimbus 实际上是 Storm 的作业跟踪器。Nimbus 的任务是向集群中所有监督守护进程分发代码。因此,当拓扑代码提交时,它实际上会到达集群中的所有物理机器。Nimbus 还监控监督守护进程的故障。如果一个监督守护进程持续失败,那么 Nimbus 将将这些工作者的工作重新分配给不同物理机器上的其他工作者。当前版本的 Storm 只允许运行一个 Nimbus 守护进程实例。Nimbus 还负责将任务分配给监督节点。如果你丢失了 Nimbus,工作者仍然会继续计算。监督者将继续在工作者死亡时重启它们。没有 Nimbus,工作者的任务不会被重新分配到集群中的另一台机器上的工作者。
如果 Nimbus 死亡,没有其他 Storm 进程会接管,也没有任何进程会尝试重启它。然而,无需担心,因为它可以随时重启。在生产环境中,当 Nimbus 死亡时,也可以设置警报。在未来,我们可能会看到高可用性的 Nimbus。
监督者
一个监督者管理着相应机器上的所有工作者。由于你的集群中每台机器都有一个监督者,因此 Storm 中的分布式计算是可能的。监督者守护进程监听由 Nimbus 分配给其运行的机器的工作,并将其分配给工作者。由于任何运行时异常,工作者可以随时死亡,当没有来自死亡工作者的心跳时,监督者将重启它们。每个工作者进程执行拓扑的一部分。类似于 Hadoop 生态系统,监督者是 Storm 的任务跟踪器。它跟踪同一机器上工作者的任务。可能的最大工作者数量取决于在 storm.yaml 中定义的端口号数量。
Zookeeper
除了其自身组件外,Storm 还依赖于 Zookeeper 集群(一个或多个 Zookeeper 服务器)来在 Nimbus 和 supervisors 之间执行协调工作。除了用于协调目的外,Nimbus 和 supervisors 还将它们的所有状态存储在 Zookeeper 中,而 Zookeeper 将它们存储在运行它的本地磁盘上。拥有多个 Zookeeper 守护进程可以提高系统的可靠性,因为如果一个守护进程宕机,另一个将成为领导者。
Storm UI
Storm 还配备了基于 Web 的用户界面。它应该在运行 Nimbus 的同一台机器上启动。Storm UI 提供了整个集群的报告,例如所有活动 supervisor 机器的总和、可用的总工作器数量、分配给每个拓扑的数量以及剩余数量,以及拓扑级别的诊断,如元组统计(发出了多少元组,以及 spout 到 bolt 或 bolt 到 bolt 之间的 ACK)。Storm UI 还显示了总工作器数量,这实际上是所有 supervisor 机器上所有可用工作器的总和。
下面的截图显示了 Storm UI 的一个示例屏幕:

以下是对 Storm UI 的解释:
-
拓扑统计:在拓扑统计下,你可以点击查看过去 10 分钟、3 小时或所有时间的统计信息。
-
Spouts(所有时间):此部分显示分配给此 spout 的执行者和任务数量,以及发出的元组和其他延迟统计信息。
-
螺栓(所有时间):此部分显示所有螺栓的列表,以及分配的执行者/任务。在进行性能调整时,请将容量列保持在接近
1的位置。在aggregatorBolt的前一个示例中,其值为1.500,因此我们不需要200个执行者/任务,而可以使用300个。容量列帮助我们决定正确的并行度。这个想法非常简单;如果容量列的值超过1,尝试以相同的比例增加执行者和任务的数量。如果执行者/任务的数量很高,而容量列接近零,尝试减少执行者/任务的数量。你可以这样做,直到得到最佳配置。
Storm 特定的术语
拓扑是将编程工作逻辑上划分为许多小规模处理单元的过程,称为 spout 和 bolt,这与 Hadoop 中的 MapReduce 类似。拓扑可以用多种语言编写,包括 Java、Python 以及更多受支持的语言。在视觉表示中,拓扑显示为连接 spout 和 bolt 的图。Spouts 和 bolt 在集群中执行任务。Storm 有两种操作模式,称为本地模式和分布式模式:
-
在本地模式下,Storm 和工人的所有进程都在你的代码开发环境中运行。这对于测试和拓扑的开发来说很好。
-
在分布式模式下,Storm 作为一个机器集群运行。当你向 Nimbus 提交拓扑代码时,Nimbus 负责分发代码并根据你的配置分配工作者来运行你的拓扑。
在以下图中,我们看到了紫色的 bolt;这些 bolt 从其上方的 spout 接收元组或记录。元组支持在编写拓扑代码的编程语言中可用的大多数数据类型。它作为一个独立的单元从 spout 流向 bolt 或从 bolt 流向另一个 bolt。元组的无界流动称为流。在一个元组中,你可以有多个键值对一起传递。
下一个图更详细地说明了流。spout 连接到元组的来源,并为拓扑生成连续的元组作为流。从 spout 作为键值对发出的内容可以通过使用相同键的 bolt 接收。

工作者进程、执行者和任务
Storm 区分以下三个主要实体,这些实体用于在 Storm 集群中实际运行拓扑:
-
工作者
-
执行者
-
任务
假设我们已经决定保留两个工作者,一个Bolt1执行者,三个Bolt2执行者,以及两个Bolt2执行者。假设执行者数量和任务数量的比例是相同的。总共有六个执行者,分别属于 spout 和 bolt。在六个执行者中,一些将在工作者 1 的范围内运行,而另一些将由工作者 2 控制;这个决定由管理员做出。这将在以下图中解释:

下一个图解释了在工作器在机器上运行的监督器范围内的位置:

执行者和任务的数量在构建拓扑代码时设置。在先前的图中,我们有两个工作者(1 和 2),由该机器的管理员运行和管理。假设执行者 1正在运行一个任务,因为执行者与任务的比例是相同的(例如,10 个执行者意味着 10 个任务,使得比例为 1:1)。但是执行者 2正在顺序运行两个任务,因此任务与执行者的比例为 2:1(例如,10 个执行者意味着 20 个任务,使得比例为 2:1)。拥有更多的任务并不意味着更高的处理速度,但对于更多的执行者来说,这是真的,因为任务是顺序运行的。
工作者进程
单个工作者进程执行拓扑的一部分,并在自己的 JVM 上运行。工作者在拓扑提交期间分配。一个工作者进程与一个特定的拓扑相关联,并可以为该拓扑的一个或多个 spout 或 bolt 运行一个或多个执行者。一个正在运行的拓扑由许多这样的工作者组成,这些工作者在 Storm 集群中的许多机器上运行。
执行者
执行器是在工作者的 JVM 范围内运行的线程。执行器可以为 spout 或 bolt 顺序运行一个或多个任务。
执行器始终为所有任务在一个线程上运行,这意味着任务在执行器上是顺序运行的。可以在拓扑启动后不关闭的情况下更改执行器的数量,使用rebalance命令:
storm rebalance <topology name> -n <number of workers> -e <spout>=<number of executors> -e <bolt1 name>=<number of executors> -e <bolt2 name>=<number of executors>
任务
任务执行数据处理,并在其父执行器的执行线程中运行。任务数量的默认值与执行器的数量相同。在构建拓扑时,我们还可以保持更多的任务数量。这有助于在将来增加执行器的数量,保持扩展的开放范围。最初,我们可以有 10 个执行器和 20 个任务,所以比例是 2:1。这意味着每个执行器有两个任务。未来的重新平衡操作可以将 20 个执行器和 20 个任务调整为 1:1 的比例。
进程间通信
下一个图展示了 Storm 提交者(客户端)、Nimbus thrift 服务器、Zookeeper、supervisors、supervisor 的工作者、执行器和任务之间的通信。每个工作进程作为一个单独的 JVM 运行。

Storm 集群的物理视图
下一个图解释了每个进程的物理位置。只有一个 Nimbus。然而,为了支持故障转移,有多个 Zookeeper,并且每台机器有一个 supervisor。

流分组
流分组控制元组从 spout 到 bolt 或 bolt 到 bolt 之间的流动。在 Storm 中,我们有四种分组类型。洗牌和字段分组是最常用的:
-
洗牌分组:在此分组中两个随机任务之间的元组流
-
字段分组:具有特定字段键的元组始终被发送到下游 bolt 的同一任务
-
全部分组:将相同的元组发送到下游 bolt 的所有任务
-
全局分组:所有任务中的元组都达到一个任务
下一个图示给出了所有四种分组类型的图解:

Storm 的容错性
管理员运行一个同步线程从 Zookeeper 获取分配信息(我应该运行拓扑的哪一部分)并将其写入本地磁盘。这个本地文件系统信息有助于保持工作者的最新状态:
-
案例 1:这是大多数情况下理想的案例。当集群正常工作时,工作者的心跳通过 Zookeeper 返回到 supervisors 和 Nimbus。
![Storm 的容错性]()
-
案例 2:如果一个 supervisor 死亡,处理仍然继续,但分配永远不会同步。Nimbus 将工作重新分配给另一台机器的不同 supervisor。那些工作者将继续运行,但不会收到任何新的元组。请设置一个警报来重启 supervisor 或使用可以重启 supervisor 的 Unix 工具。
-
案例 3:如果 Nimbus 死亡,拓扑将继续正常工作。处理仍将继续,但无法执行拓扑生命周期操作或将任务重新分配到另一台机器。
-
案例 4:如果工人死亡(如心跳停止到达),主管将尝试重启工人进程,处理将继续。如果工人反复死亡,Nimbus 将将工作重新分配给集群中的其他节点。
Storm 中保证元组处理
由于 Storm 已经能够处理各种进程级故障,另一个重要特性是处理当工人死亡时发生的元组故障的能力。这只是为了说明位运算 XOR:两个相同位集合的 XOR 是 0。这被称为 XOR 魔法,它可以帮助我们了解元组发送到下一个 bolt 的交付是否成功。Storm 使用 64 位来跟踪元组。每个元组都得到一个 64 位的元组 ID。这个 64 位 ID,连同任务 ID 一起,保存在 ACKer 中。
在下一图中,解释了 ACK 和重放案例:

XOR 魔法在 ACK 中
只有当链接的元组树中的所有元组都完成时,spout 元组才被完全处理。如果元组树在配置的超时时间内未完成(默认值为 topology.message.timeout.secs: 30),则 spout 元组将被重放。
在前面的图中,第一个 acker 从 spout 获取元组 1 的 10101(为了解释的简便,我们保持 5 位),一旦 Bolt 1 收到相同的元组,它也会向 acker ACK。从两个来源,acker 获取 10101。这意味着 10101 XOR 10101 = 0。元组 1 已成功被 Bolt 1 接收。相同的过程在 bolts 1 和 2 之间重复。最后,Bolt 2 向 acker 发送 ACK,元组树完成。这会创建一个信号来调用 spout 的 success 函数。元组处理中的任何失败都可以触发 spout 的 fail 函数调用,这表明需要将元组发送回再次处理。
Storm 的 acker 通过在发送者的元组和接收者的元组之间执行 XOR 来跟踪元组树的完成。每次发送元组时,其值都会与 acker 维护的校验和 XOR,每次 ACK 元组时,其值再次在 acker 中 XOR。
如果所有元组都已成功 ACK,校验和将为零。Ackers 是系统级执行器。
在 spout 中,我们有选择两个 emit 函数。
-
emit([tuple]): 这是一个简单的 emit -
storm.emit([tuple], id=the_value): 这创建了一个可靠的 spout,但前提是你可以使用the_value重新发射元组
在 Spout 中,我们也有两个 ACK 函数:
-
fail(the_value): 当发生超时或元组失败时调用此函数 -
ack(the_value): 当拓扑中的最后一个 bolt ACK 元组树时调用此函数
此 ID 字段应该是一个随机且唯一的值,以便从 spout 的fail函数中重新播放。使用此 ID,我们可以从fail函数中重新发出它。如果成功,success函数将被调用,并且可以从中删除成功的元组或从源处重新创建。
如果您在拓扑中有一个可靠的 spout,您将能够重新创建相同的元组。要创建一个可靠的 spout,请从 spout 的下一个元组函数中发出一个唯一的消息 ID(the_value),并附带元组:
storm.emit([tuple], id=the_value)
如果一个元组在配置的期间内没有被 ACK,或者编程代码由于某些错误条件而使元组失败,这两种情况都是有效的重放案例。
当调用fail函数时,代码可以使用相同的消息 ID 从 spout 的源读取,当调用success函数时,可以采取诸如从队列中删除消息之类的操作。
消息 ID 是一个应用程序特定的键,可以帮助您重新创建一个元组并将其从 spout 重新发出。消息 ID 的一个例子可以是队列消息 ID,或者表的主键。如果一个元组在超时或由于任何其他原因失败,则认为该元组已失败。
Storm 有一个容错机制,它保证了只从可靠的 spout 发出的所有元组的至少一次处理。
一旦设置了可靠的 spout,您可以让 bolt 在输入和输出元组之间进行链接,从而创建一个元组树。一旦建立了元组树,acker 就知道链接树中的任何故障,并且使用原始消息 ID 重新创建整个元组树。
在 bolt 中,有两个函数:
-
emit([tuple]):没有元组树链接。我们无法跟踪使用了哪个原始消息 ID。 -
storm.emit([tuple], anchors=[message_key]):有了链接,原始元组现在可以重新播放。
以下图解释了元组 B 是如何从元组 A 生成的:

下一图展示了 bolt 执行ACK的过程:

以下图展示了故障条件,其中信号在故障时到达 spout:

成功的ACK演示如下:

以下图示展示了没有螺栓的大元组树的状态,并且没有出现故障:

下一图演示了元组树中的故障示例——在元组树的中间:

调整 Storm 的并行性——扩展分布式计算
为了解释 Storm 的并行性,我们将配置三个参数:
-
工作器的数量
-
执行器的数量
-
任务的数量
以下图给出了一个示例的图解,其中我们有一个只有一个 spout 和一个 bolt 的拓扑。在这种情况下,我们将设置 spout 和 bolt 级别的工作者、执行者和任务的不同值,并查看每种情况下的并行性是如何工作的:

// assume we have two workers in total for topology.
topology.workers: 2
// just one executor of spout.
builder.setSpout("spout-sentence", TwitterStreamSpout(),1)
// two executors of bolt.
builder.setBolt("bolt-split", SplitSentenceBolt(),2)
// four tasks for bolts.
.setNumTasks(4)
.shuffleGrouping("spout-sentence");
对于这种配置,我们将有两个工作者,它们将在不同的 JVM(worker 1 和 worker 2)中运行。
对于 spout,有一个执行者,默认任务数是一个,这使得比例是 1:1(每个执行者一个任务)。
对于螺栓,有两个执行者和四个任务,这使得每个执行者有四个任务除以两个执行者等于两个任务。这两个执行者在 worker 2 下运行,每个执行者有两个任务,而 worker 1 的执行者只得到一个任务。
这可以通过以下图示很好地说明:

让我们改变 bolt 的配置为两个执行者和两个任务:
builder.setBolt("bolt-split", SplitSentenceBolt(),2)
// 2 tasks for bolts.
.setNumTasks(2)
.shuffleGrouping("spout-sentence");
这在这里可以很好地说明:

工作者的数量再次是两个。由于 bolt 有两个执行者和两个任务,所以是 2/2,或者每个执行者一个任务。现在你可以看到两个执行者各自得到一个任务。在性能方面,这两种情况完全相同,因为任务在执行者线程中顺序运行。更多的执行者意味着更高的并行度,更多的工作者意味着更有效地使用资源,如 CPU 和 RAM。内存分配是在工作者级别使用worker.childopts设置完成的。我们还应该监控特定工作者进程所持有的最大内存量。这在决定工作者总数方面起着重要作用。可以使用ps -ef选项查看。始终保持任务和执行者的相同比例,并使用 Storm UI 的容量列推导出执行者数量的正确值。作为重要提示,我们应该在 bolt 中保持较短的持续时间事务,并尝试通过将代码拆分成更多的 bolt 或减少批处理大小 tuple 来调整它。批处理大小是 bolt 在单个 tuple 交付中接收的记录数。另外,不要因为较长时间的事务而阻塞 spout 的nextTuple方法。
摘要
随着本章接近尾声,你一定对 Nimbus、监督器、UI 和 Zookeeper 进程有了大致的了解。本章还教你如何通过调整工作者、执行者和任务的数量来在 Storm 中调整并行性。你熟悉了分布计算的重要问题,即故障和通过系统中的不同容错机制克服故障。最重要的是,你学习了如何编写一个“可靠的”spout,以实现保证的消息处理和 bolt 中的链接。
下一章将为您提供有关如何使用名为 Petrel 的 Python 库构建简单拓扑的信息。Petrel 解决了 Storm 内置 Python 支持的一些局限性,提供了更简单、更流畅的开发体验。
第三章. 介绍 Petrel
如第一章中所述,熟悉 Storm,Storm 是一个用于实时处理大量数据的平台。Storm 应用程序通常用 Java 编写,但 Storm 也支持其他语言,包括 Python。虽然跨语言的概念相似,但细节因语言而异。在本章中,我们将通过 Python 使用 Storm 获得我们的第一次动手经验。首先,你将了解一个名为 Petrel 的 Python 库,这是在 Python 中创建拓扑所必需的。接下来,我们将设置我们的 Python/Storm 开发环境。然后,我们将仔细研究一个用 Python 编写的实际运行的 Storm 拓扑。最后,我们将运行该拓扑,并学习一些简化拓扑开发和调试过程的关键技术。完成本章后,你将对开发基本的 Storm 拓扑有一个很好的高级理解。在本章中,我们将涵盖以下主题:
-
什么是 Petrel?
-
安装 Petrel
-
创建你的第一个拓扑
-
运行拓扑
-
使用 Petrel 的生产力技巧
什么是 Petrel?
本书中的所有 Python 拓扑都依赖于一个名为 Petrel 的开源 Python 库。如果你之前有使用 Storm 的经验,你可能还记得有一个名为storm-starter的 GitHub 项目,该项目包含了使用 Storm 和各种语言(你可以在github.com/apache/storm/tree/master/examples/storm-starter)的示例(你可以在这里找到storm-starter的最新版本)。storm-starter项目包含一个名为storm.py的模块,它允许你使用 Python 实现 Storm 拓扑。鉴于storm.py的可用性,是否真的有必要使用另一个库?虽然使用storm.py构建拓扑是可能的,但它缺少一些重要功能。为了弥补这些差距,开发者必须使用大多数 Python 开发者不熟悉的语言和工具。如果你已经熟悉这些工具,并且不介意在处理 Storm 时同时处理多个技术栈,你可能会对storm.py感到满意。但大多数新接触 Storm 的开发者发现storm.py的方法过于复杂,甚至令人难以承受。让我们更详细地讨论storm.py的弱点。
构建拓扑
为了运行一个拓扑,Storm 需要描述其内部的 spouts、bolts 和 streams。这个描述被编码在一个称为Thrift的格式中。storm.py模块不支持创建这种描述;开发者必须使用另一种编程语言(通常是 Java 或 Clojure)来创建它。
打包拓扑
拓扑以 Java .jar文件的形式提交给 Storm(类似于 Python .egg或.tar.gz文件)。除了拓扑描述外,Python 拓扑.jar文件还必须包含拓扑的 Python 代码。创建 JAR 文件通常涉及使用 Java 开发工具,如 Ant 或 Maven。
记录事件和错误
如果拓扑包含日志消息以允许跟踪通过它的数据,则调试和监控拓扑会更加容易。如果在 Python 拓扑中出现问题并且代码崩溃,看到错误是什么以及它发生在哪里是无价的。storm.py模块在这些方面没有提供帮助。如果组件崩溃,它将简单地退出而不捕获任何信息。根据我的经验,这是使用storm.py最令人沮丧的方面。
管理第三方依赖项
实际的 Python 应用程序通常使用第三方库。如果集群需要运行多个拓扑,每个拓扑可能具有不同的,甚至冲突的这些库版本。Python 虚拟环境是管理这些的绝佳工具。然而,storm.py无法帮助您在 Python 虚拟环境中创建、激活或安装第三方库。Petrel 解决了 Storm 内置 Python 支持的这些所有限制,提供了一个更简单、更流畅的开发体验。Petrel 的关键特性包括以下内容:
-
Python API 用于构建拓扑
-
打包拓扑以提交给 Storm
-
记录事件和错误
-
在工作节点上,使用
setup.sh设置拓扑特定的 Python 运行时环境
在本章中,我们将讨论前三点。我们将看到第四点的示例,在第四章 示例拓扑 – Twitter。
安装 Petrel
让我们设置我们的 Python 开发环境。我们假设您已经遵循了第一章 熟悉 Storm中的说明来安装 Storm 0.9.3:
-
首先,我们需要安装
virtualenv,这是一个用于管理 Python 库的工具。在 Ubuntu 上,只需运行此命令:sudo apt-get install python-virtualenv -
接下来,我们创建一个 Python 虚拟环境。这提供了一种在不要求对机器具有 root 访问权限且不干扰系统 Python 包的情况下安装 Python 库的方法:
virtualenv petrel您将看到以下类似输出:
New python executable in petrel/bin/python Installing distribute.............................................................................................................................................................................................done -
接下来,运行此命令以激活虚拟环境。您的 shell 提示符将更改以包含
virtualenv名称,表示虚拟环境已激活:source petrel/bin/activate (petrel)barry@Dell660s:~$注意
您需要再次运行此命令——每次打开新终端时。
-
最后,安装 Petrel:
easy_install petrel==0.9.3.0.3注意
Petrel 版本号的前三位必须与您使用的 Storm 版本相匹配。如果您使用的是没有相应 Petrel 发布版本的 Storm 版本,您可以从源代码安装 Petrel。有关说明,请参阅
github.com/AirSage/Petrel#installing-petrel-from-source。
小贴士
下载示例代码
您可以从您在 www.packtpub.com 的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册以将文件直接通过电子邮件发送给您
创建你的第一个拓扑
现在,我们将创建一个 Storm 拓扑,该拓扑将句子分解成单词,然后计算每个单词的出现次数。在 Storm 中实现此拓扑需要以下组件:
-
句子喷嘴 (
randomsentence.py): 拓扑总是从喷嘴开始;这是数据进入 Storm 的方式。句子喷嘴将发出无限流量的句子。 -
分词 bolt (
splitsentence.py): 这个 bolt 接收句子并将它们分解成单词。 -
单词计数 bolt (
wordcount.py): 这个 bolt 接收单词并计算它们的出现次数。对于每个处理的单词,输出单词及其出现次数。
下图显示了数据如何在拓扑中流动:

单词计数拓扑
现在我们已经看到了基本的数据流,让我们实现拓扑并看看它是如何工作的。
句子喷嘴
在本节中,我们实现了一个生成随机句子的喷嘴。将以下代码输入一个名为 randomsentence.py 的文件中:
import time
import random
from petrel import storm
from petrel.emitter import Spout
class RandomSentenceSpout(Spout):
def __init__(self):
super(RandomSentenceSpout, self).__init__(script=__file__)
@classmethod
def declareOutputFields(cls):
return ['sentence']
sentences = [
"the cow jumped over the moon",
"an apple a day keeps the doctor away",
]
def nextTuple(self):
time.sleep(0.25)
sentence = self.sentences[
random.randint(0, len(self.sentences) - 1)]
storm.emit([sentence])
def run():
RandomSentenceSpout().run()
喷嘴继承自 Petrel 的 Spout 类。
Petrel 要求每个喷嘴和 bolt 类实现 __init__() 并将其文件名传递给 (script=__file__) 基类。script 参数告诉 Petrel 在启动组件实例时运行哪个 Python 脚本。
declareOutputFields() 函数告诉 Storm 这个喷嘴发出的元组的结构。每个元组由一个名为 sentence 的单个字段组成。
Storm 每次准备好从喷嘴获取更多数据时都会调用 nextTuple()。在现实世界的喷嘴中,您可能正在从外部数据源读取,例如 Kafka 或 Twitter。这个喷嘴只是一个示例,因此它生成自己的数据。它只是从两个句子中选择一个进行随机选择。
你可能已经注意到,在每次调用 nextTuple() 时,喷嘴会暂停 0.25 秒。为什么是这样呢?这从技术上讲并不是必要的,但它会减慢速度,使得在本地模式下运行拓扑时输出更容易阅读。
run()函数的作用是什么?它是 Petrel 所需的一点点胶水代码。当 spout 或 bolt 脚本被加载到 Storm 中时,Petrel 调用run()函数来创建组件并开始处理消息。如果你的 spout 或 bolt 需要执行额外的初始化,这是一个很好的地方来做。
Splitter bolt
本节提供了 splitter bolt,它从 spout 消耗句子并将它们拆分为单词。将以下代码输入名为splitsentence.py的文件中:
from petrel import storm
from petrel.emitter import BasicBolt
class SplitSentenceBolt(BasicBolt):
def __init__(self):
super(SplitSentenceBolt, self).__init__(script=__file__)
def declareOutputFields(self):
return ['word']
def process(self, tup):
words = tup.values[0].split("")
for word in words:
storm.emit([word])
def run():
SplitSentenceBolt().run()
SplitSentenceBolt从BasicBolt Petrel 类继承。这个类用于大多数简单的 bolt。你可能还记得,Storm 有一个确保每条消息都被处理的功能,“重放”未处理完成的先前元组。BasicBolt简化了与这个功能的工作。它是通过在处理每个元组时自动向 Storm 确认来实现的。更灵活的Bolt类允许程序员直接确认元组,但这超出了本书的范围。
分割句子 bolt 有一个类似于 spout 的 run 函数。
process()函数从 spout 接收句子并将它们拆分为单词。每个单词作为一个单独的元组发出。
单词计数 bolt
本节实现了单词计数 bolt,它从 spout 消耗单词并计数。将以下代码输入wordcount.py文件中:
from collections import defaultdict
from petrel import storm
from petrel.emitter import BasicBolt
class WordCountBolt(BasicBolt):
def __init__(self):
super(WordCountBolt, self).__init__(script=__file__)
self._count = defaultdict(int)
@classmethod
def declareOutputFields(cls):
return ['word', 'count']
def process(self, tup):
word = tup.values[0]
self._count[word] += 1
storm.emit([word, self._count[word]])
def run():
WordCountBolt().run()
单词计数 bolt 有一个新的特点;与句子 bolt 不同,它需要从一条元组存储到另一条元组的信息——单词计数。__init__()函数设置一个_count字段来处理这一点。
单词计数 bolt 使用 Python 的方便的defaultdict类,它通过在访问不存在的键时自动提供0条目来简化计数。
定义拓扑
前几节提供了单词计数拓扑的 spout 和 bolts。现在,我们需要告诉 Storm 组件如何组合形成一个拓扑。在 Petrel 中,这是通过一个create.py脚本完成的。此脚本提供以下信息:
-
构成拓扑的 spouts 和 bolts
-
对于每个 bolt,它的输入数据来自哪里
-
元组如何在 bolt 的实例之间分区
这里是create.py脚本:
from randomsentence import RandomSentenceSpout
from splitsentence import SplitSentenceBolt
from wordcount import WordCountBolt
def create(builder):
builder.setSpout("spout", RandomSentenceSpout(), 1)
builder.setBolt(
"split", SplitSentenceBolt(), 1).shuffleGrouping("spout")
builder.setBolt(
"count", WordCountBolt(), 1).fieldsGrouping(
"split", ["word"])
重要的是单词计数 bolt 使用 Storm 的fieldsGrouping行为(如第二章中Stream grouping部分所述),Stream grouping。这个设置允许你在数据流的一个或多个字段上对元组进行分组。对于单词计数拓扑,fieldsGrouping确保所有单词实例都将由同一个 Storm 工作进程计数。
当拓扑在集群上部署时,可能会运行许多单独的单词计数 bolt 实例。如果我们没有在"word"字段上配置fieldsGrouping,那么在处理句子“the cow jumped over the moon”时,我们可能会得到以下结果:
Word count instance 1: { "the": 1, "cow": 1, "jumped": 1 }
Word count instance 2: { "over": 1, "the": 1, "moon": 1 }
有两个 "the" 的条目,因此计数是错误的!我们想要的是这样的东西:
Word count instance 1: { "the": 2, "cow": 1, "jumped": 1 }
Word count instance 2: { "over": 1, "moon": 1 }
运行拓扑
只需再提供一些细节,我们就可以运行拓扑了:
-
创建一个
topology.yaml文件。这是 Storm 的一个配置文件。这个文件的完整解释超出了本书的范围,但你可以在github.com/apache/storm/blob/master/conf/defaults.yaml查看所有可用的选项:nimbus.host: "localhost" topology.workers: 1 -
创建一个空的
manifest.txt文件。你可以使用编辑器来完成这个任务,或者简单地运行touch manifest.txt。这是一个 Petrel 特定的文件,它告诉 Petrel 应该包含在提交给 Storm 的.jar文件中的附加文件(如果有的话)。在第四章中,我们将看到一个真正使用此文件的例子。 -
在运行拓扑之前,让我们回顾一下我们创建的文件列表。确保你已经正确创建了这些文件:
-
randomsentence.py -
splitsentence.py -
wordcount.py -
create.py -
topology.yaml -
manifest.txt
-
-
使用以下命令运行拓扑:
petrel submit --config topology.yaml --logdir `pwd`恭喜!你已经创建并运行了你的第一个拓扑!
Petrel 运行
create.py脚本来发现拓扑的结构,然后使用这些信息加上manifest.txt文件来构建topology.jar文件并将其提交给 Storm。接下来,Storm 解包topology.jar文件并准备工作者。使用 Petrel,这需要创建一个 Python 虚拟环境并从互联网上安装 Petrel。大约 30 秒后,拓扑将在 Storm 中启动并运行。你将看到一个无尽的输出流,其中穿插着类似于以下的消息:
25057 [Thread-20] INFO backtype.storm.daemon.task - Emitting: split default ["the"] 25058 [Thread-20] INFO backtype.storm.daemon.task - Emitting: split default ["moon"] 25059 [Thread-22] INFO backtype.storm.daemon.task - Emitting: count default ["cow",3] 25059 [Thread-9-count] INFO backtype.storm.daemon.executor - Processing received message source: split:3, stream: default, id: {}, ["over"] 25059 [Thread-9-count] INFO backtype.storm.daemon.executor - Processing received message source: split:3, stream: default, id: {}, ["the"] 25059 [Thread-9-count] INFO backtype.storm.daemon.executor - Processing received message source: split:3, stream: default, id: {}, ["moon"] 25060 [Thread-22] INFO backtype.storm.daemon.task - Emitting: count default ["jumped",3] 25060 [Thread-22] INFO backtype.storm.daemon.task - Emitting: count default ["over",3] 25060 [Thread-22] INFO backtype.storm.daemon.task - Emitting: count default ["the",9] 25060 [Thread-22] INFO backtype.storm.daemon.task - Emitting: count default ["moon",3] -
当你看得足够多了,按 Ctrl + C 来终止 Storm。有时,它不会干净地退出。如果它没有,通常以下步骤可以清理:再按几次 Ctrl + C,然后按 Ctrl + Z 来暂停 Storm。
-
输入
ps来获取processesLook的列表,查找 Java 进程并获取其进程idType "kill -9 processid",将processid替换为 Java 进程的 ID。
故障排除
如果拓扑没有正确运行,请检查当前目录中创建的日志文件。错误通常是由于使用了没有在 PyPI 网站上对应 Petrel 版本的 Storm 版本(pypi.python.org/pypi/petrel)。在撰写本书时,支持两个 Storm 版本:
-
0.9.3
-
0.9.4
如果你使用的是不受支持的 Storm 版本,你可能会看到类似于以下错误之一:
File "/home/barry/.virtualenvs/petrel2/lib/python2.7/site-packages/petrel-0.9.3.0.3-py2.7.egg/petrel/cmdline.py", line 19, in get_storm_version
return m.group(2)
AttributeError: 'NoneType' object has no attribute 'group'
IOError: [Errno 2] No such file or directory: '/home/barry/.virtualenvs/petrel2/lib/python2.7/site-packages/petrel-0.9.3.0.3-py2.7.egg/petrel/generated/storm-petrel-0.10.0-SNAPSHOT.jar'
使用 Petrel 的生产力技巧
在本章中,我们覆盖了很多内容。虽然我们并不了解 Storm 的每一个细节,但我们已经看到了如何构建一个包含多个组件的拓扑,并在它们之间发送数据。
拓扑的 Python 代码相当简短——总共只有大约 75 行。这做了一个很好的示例,但实际上,它可能还是有点短。当你开始编写自己的拓扑时,事情可能不会第一次就完美工作。新代码通常会有错误,有时甚至可能崩溃。为了使拓扑正确工作,你需要了解拓扑中发生的事情,尤其是在有问题的时候。当你努力解决问题时,你会一遍又一遍地运行相同的拓扑,而拓扑的 30 秒启动时间可能会感觉像永恒。
提高启动性能
让我们先谈谈启动性能。默认情况下,当 Petrel 拓扑启动时,它会创建一个新的 Python virtualenv并在其中安装 Petrel 和其他依赖项。虽然这种行为对于在集群上部署拓扑非常有用,但在开发过程中,你可能需要多次启动拓扑,这会非常低效。为了跳过virtualenv创建步骤,只需将submit命令更改为让 Petrel 重用现有的 Python 虚拟环境:
petrel submit --config topology.yaml --venv self
这将启动时间从 30 秒缩短到大约 10 秒。
启用和使用日志记录
与许多语言一样,Python 有一个日志框架,它提供了一种捕获运行中的应用程序内部发生情况信息的方法。本节描述了如何使用 Storm 进行日志记录:
-
在单词计数拓扑所在的同一目录中创建一个新文件,命名为
logconfig.ini:[loggers] keys=root,storm [handlers] keys=hand01 [formatters] keys=form01 [logger_root] level=INFO handlers=hand01 [logger_storm] qualname=storm level=DEBUG handlers=hand01 propagate=0 [handler_hand01] class=FileHandler level=DEBUG formatter=form01 args=(os.getenv('PETREL_LOG_PATH') or 'petrel.log', 'a') [formatter_form01] format=[%(asctime)s][%(name)s][%(levelname)s]%(message)s datefmt= class=logging.Formatter注意
你刚才看到的是一个简单的日志配置,用于演示目的。有关 Python 日志记录的更多信息,请参阅
www.python.org/的日志模块文档。 -
将
wordcount.py更新为记录其输入和输出。新添加的行已突出显示:import logging from collections import defaultdict from petrel import storm from petrel.emitter import BasicBolt log = logging.getLogger('wordcount') class WordCountBolt(BasicBolt): def __init__(self): super(WordCountBolt, self).__init__(script=__file__) self._count = defaultdict(int) @classmethod def declareOutputFields(cls): return ['word', 'count'] def process(self, tup): log.debug('WordCountBolt.process() called with: %s', tup) word = tup.values[0] self._count[word] += 1 log.debug('WordCountBolt.process() emitting: %s', [word, self._count[word]]) storm.emit([word, self._count[word]]) def run(): WordCountBolt().run() -
现在启动更新后的拓扑:
petrel submit --config topology.yaml --venv self --logdir `pwd`
当拓扑运行时,单词计数组件的日志文件将被写入当前目录,记录正在发生的事情。文件名会随着每次运行而变化,但可能会像petrel22011_wordcount.log这样的格式:
WordCountBolt.process() called with: <Tuple component='split' id='5891744987683180633' stream='default' task=3 values=['moon']>
WordCountBolt.process() emitting: ['moon', 2]
WordCountBolt.process() called with: <Tuple component='split' id='-8615076722768870443' stream='default' task=3 values=['the']>
WordCountBolt.process() emitting: ['the', 7]
自动记录致命错误
如果一个 spout 或 bolt 由于运行时错误而崩溃,你需要知道发生了什么才能修复它。为了帮助解决这个问题,Petrel 会自动将致命的运行时错误写入日志:
-
在单词计数 bolt 的
process()函数的开始处添加一行,使其崩溃:def process(self, tup): raise ValueError('abc') log.debug('WordCountBolt.process() called with: %s', tup) word = tup.values[0] self._count[word] += 1 log.debug('WordCountBolt.process() emitting: %s', [word, self._count[word]]) storm.emit([word, self._count[word]]) -
再次运行拓扑并检查单词计数日志文件。它将包含失败的回溯信息:
[2015-02-08 22:28:42,383][storm][INFO]Caught exception [2015-02-08 22:28:42,383][storm][ERROR]Sent failure message ("E_BOLTFAILED__wordcount__Dell660s__pid__21794__port__-1__taskindex__-1__ValueError") to Storm [2015-02-08 22:28:47,385][storm][ERROR]Caught exception in BasicBolt.run Traceback (most recent call last): File "/home/barry/dev/Petrel/petrel/petrel/storm.py", line 381, in run self.process(tup) File "/tmp/b46e3137-1956-4abf-80c8-acaa7d3626d1/supervisor/stormdist/test+topology-1-1423452516/resources/wordcount.py", line 19, in process raise ValueError('abc') ValueError: abc [2015-02-08 22:28:47,386][storm][ERROR]The error occurred while processing this tuple: ['an'] Worker wordcount exiting normally.
摘要
在本章中,你学习了 Petrel 如何使纯 Python 开发 Storm 拓扑成为可能。我们创建并运行了一个简单的拓扑,并学习了它是如何工作的。你还学习了如何使用 Petrel 的--venv self选项和 Python 日志记录来简化你的开发和调试过程。
在下一章中,我们将看到一些更复杂的拓扑,包括从真实世界数据源(Twitter)读取而不是随机生成数据的 spout。
第四章。示例拓扑 – Twitter
本章基于第三章(Introducing Petrel)的内容。在本章中,我们将构建一个拓扑,演示许多新功能和技巧。特别是,我们将看到如何:
-
实现从 Twitter 读取的 spout
-
基于第三方 Python 库构建拓扑组件
-
在滚动时间段内计算统计数据和排名
-
从
topology.yaml读取自定义配置设置 -
使用“tick tuples”按计划执行逻辑
Twitter 分析
大多数人都听说过 Twitter,但如果您还没有,可以查看维基百科是如何描述 Twitter 的:
"一个在线社交网络服务,允许用户发送和阅读称为“推文”的 140 个字符的短消息。"
2013 年,Twitter 上的用户每天发布 4 亿条消息。Twitter 提供了一个 API,允许开发者实时访问推文流。在这些推文中,消息默认是公开的。消息量、API 的可用性和推文的公开性使得 Twitter 成为了解当前事件、感兴趣的话题、公众情绪等的有价值的信息来源。
Storm 最初是在 BackType 开发的,用于处理推文,Twitter 分析仍然是 Storm 的一个流行用例。您可以在 Storm 网站上看到几个示例,网址为storm.apache.org/documentation/Powered-By.html。
本章中的拓扑演示了如何从 Twitter 的实时流 API 中读取,计算最流行单词的排名。这是 Storm 网站上“滚动最热单词”示例的 Python 版本(github.com/apache/storm/blob/master/examples/storm-starter/src/jvm/storm/starter/RollingTopWords.java),并包括以下组件:
-
Twitter 流输出组件(
twitterstream.py):此组件从 Twitter 样本流中读取推文。 -
分词 bolt(
splitsentence.py):此组件接收推文并将它们拆分为单词。这是第三章中介绍的 Petrel 的分词 bolt 的改进版本。 -
滚动词计数 bolt(
rollingcount.py):此组件接收单词并计算其出现次数。它与第三章中介绍的 Petrel 的词计数 bolt 类似,但实现了滚动计数(这意味着 bolt 会定期丢弃旧数据,因此词计数只考虑最近的消息)。 -
中间排名 bolt(
intermediaterankings.py):此组件消费词计数,并定期输出出现频率最高的n个单词。 -
总排名 bolt(
totalrankings.py):此组件与中间排名 bolt 类似。它将中间排名合并,生成一组总体排名。
Twitter 的流式 API
Twitter 的公共 API 功能强大且灵活。它具有许多用于发布和消费推文的功能。我们的应用程序需要实时接收和处理推文。Twitter 的流式 API 旨在解决这个问题。在计算机科学中,流是一系列随时间提供的数据元素(在这种情况下,推文)的序列。
流式 API 的详细解释可以在dev.twitter.com/streaming/overview找到。要使用它,应用程序首先需要与 Twitter 建立一个连接。该连接将无限期保持开启状态以接收推文。
流式 API 提供了几种方式来选择你的应用程序接收哪些推文。我们的拓扑使用所谓的样本流,它由 Twitter 任意选择的一小部分所有推文组成。样本流旨在用于演示和测试。生产应用程序通常使用其他流类型之一。有关可用流的更多信息,请参阅dev.twitter.com/streaming/public。
创建 Twitter 应用程序以使用流式 API
在我们能够使用 Twitter 的流式 API 之前,Twitter 要求我们创建一个应用。这听起来很复杂,但实际上设置起来非常简单;基本上,我们只需在网站上填写一个表格:
-
如果你没有 Twitter 账户,请在
twitter.com/创建一个。 -
一旦你有了账户,登录并前往
apps.twitter.com/。点击创建新应用。填写创建应用程序的表格。将回调 URL字段留空。默认访问级别为只读,这意味着此应用程序只能读取推文;它不能发布或进行其他更改。对于这个例子,只读访问是合适的。最后,点击创建你的 Twitter 应用程序。你将被重定向到你的应用页面。 -
点击密钥和访问令牌选项卡,然后点击创建我的访问令牌。Twitter 将生成一个由两部分组成的访问令牌:访问令牌和访问令牌密钥。在连接到 Twitter 时,你的应用程序将使用此令牌以及消费者密钥和消费者密钥密钥。
以下截图显示了生成访问令牌后的密钥和访问令牌选项卡:
![创建 Twitter 应用程序以使用流式 API]()
拓扑配置文件
现在我们已经设置了一个具有 API 访问权限的 Twitter 账户,我们准备创建拓扑。首先,创建topology.yaml。我们在第三章中首次看到了基本的topology.yaml文件,介绍 Petrel。在这里,topology.yaml也将包含 Twitter 的连接参数。输入以下文本,用你在apps.twitter.com/获取的四个oauth值替换:
nimbus.host: "localhost"
topology.workers: 1
oauth.consumer_key: "blahblahblah"
oauth.consumer_secret: "blahblahblah"
oauth.access_token: "blahblahblah"
oauth.access_token_secret: "blahblahblah"
Twitter 流源
现在,让我们看看 Twitter 喷嘴。在twitterstream.py中输入以下代码:
import json
import Queue
import threading
from petrel import storm
from petrel.emitter import Spout
from tweepy.streaming import StreamListener
from tweepy import OAuthHandler, Stream
class QueueListener(StreamListener):
def __init__(self, queue):
self.queue = queue
def on_data(self, data):
tweet = json.loads(data)
if 'text' in tweet:
self.queue.put(tweet['text'])
return True
class TwitterStreamSpout(Spout):
def __init__(self):
super(TwitterStreamSpout, self).__init__(script=__file__)
self.queue = Queue.Queue(1000)
def initialize(self, conf, context):
self.conf = conf
thread = threading.Thread(target=self._get_tweets)
thread.daemon = True
thread.start()
@classmethod
def declareOutputFields(cls):
return ['sentence']
def _get_tweets(self):
auth = OAuthHandler(
self.conf['oauth.consumer_key'],
self.conf['oauth.consumer_secret'])
auth.set_access_token(
self.conf['oauth.access_token'],
self.conf['oauth.access_token_secret'])
stream = Stream(auth, QueueListener(self.queue))
stream.sample(languages=['en'])
def nextTuple(self):
tweet = self.queue.get()
storm.emit([tweet])
self.queue.task_done()
def run():
TwitterStreamSpout().run()
喷嘴是如何与 Twitter 通信的?Twitter API 对 API 客户端提出了一系列要求:
-
连接必须使用安全套接字层(SSL)进行加密
-
API 客户端必须使用 OAuth 进行身份验证,OAuth 是一种流行的身份验证协议,用于与安全的 Web 服务交互
-
由于它涉及长期连接,流式 API 涉及的内容不仅仅是简单的 HTTP 请求
幸运的是,有一个名为Tweepy的库([www.tweepy.org/](http://www.tweepy.org/))实现了这些要求,并提供了一个简单易用的 Python API。Tweepy 提供了一个Stream类来连接到流式 API。它在_get_tweets()中使用。
创建 Tweepy 流需要之前列出的四个 Twitter 连接参数。我们可以在我们的喷嘴中直接将这些参数硬编码,但这样如果连接参数发生变化,我们就必须更改代码。相反,我们将这些信息放在topology.yaml配置文件中。我们的喷嘴在initialize()函数中读取这些设置。Storm 在启动此组件的任务时调用此函数,并传递有关环境和配置的信息。在这里,initialize()函数捕获self.conf中的拓扑配置。这个字典包括oauth值。
以下序列图显示了喷嘴如何与 Twitter 通信,接收推文并将它们发出。你可能已经注意到喷嘴创建了一个后台线程。这个线程从 Tweepy 接收推文,并通过 Python 队列将它们传递给主喷嘴线程。

为什么喷嘴使用线程?通常,线程用于支持并发处理。这里并非如此。相反,Tweepy 的行为与 Petrel 喷嘴 API 之间存在不匹配。
当从 Twitter 流中读取时,Tweepy 会阻塞执行,为收到的每条推文调用应用程序提供的处理函数
在 Petrel 中,喷嘴上的nextTuple()函数必须在每次返回元组后从函数中返回。
在后台线程中运行 Tweepy 并将其写入队列,为这些冲突的要求提供了一个简单而优雅的解决方案。
分割器 bolt
这里的分割器 bolt 的结构与第三章中的类似,介绍 Petrel。这个版本有两个改进,使其更有用和更真实。
小贴士
忽略那些在“常用词”列表中过于常见,在“常用词”列表中不有趣或无用的单词。这包括像“the”这样的英语单词,以及像“http”、“https”和“rt”这样的在推文中频繁出现的类似词。
在分割推文为单词时,省略标点符号。
一个名为 Natural Language Toolkit(NLTK)的 Python 库使得实现这两者变得容易。NLTK 有许多其他令人着迷、强大的语言处理功能,但这些超出了本书的范围。
在 splitsentence.py 中输入以下代码:
import nltk.corpus
from petrel import storm
from petrel.emitter import BasicBolt
class SplitSentenceBolt(BasicBolt):
def __init__(self):
super(SplitSentenceBolt, self).__init__(script=__file__)
self.stop = set(nltk.corpus.stopwords.words('english'))
self.stop.update(['http', 'https', 'rt'])
def declareOutputFields(self):
return ['word']
def process(self, tup):
for word in self._get_words(tup.values[0]):
storm.emit([word])
def _get_words(self, sentence):
for w in nltk.word_tokenize(sentence):
w = w.lower()
if w.isalpha() and w not in self.stop:
yield w
def run():
SplitSentenceBolt().run()
滚动词频 bolt
滚动词频 bolt 与第三章中介绍的 Petrel 的词频 bolt 类似,介绍 Petrel。早期章节中的 bolt 简单地无限期地累积词频。这不利于分析 Twitter 上的热门话题,因为热门话题可能在一瞬间发生变化。相反,我们希望计数反映最新的信息。为此,滚动词频 bolt 将数据存储在基于时间的桶中。然后,它定期丢弃超过 5 分钟年龄的桶。因此,此 bolt 的词频只考虑最后 5 分钟的数据。
在 rollingcount.py 中输入以下代码:
from collections import defaultdict
from petrel import storm
from petrel.emitter import BasicBolt
class SlotBasedCounter(object):
def __init__(self, numSlots):
self.numSlots = numSlots
self.objToCounts = defaultdict(lambda: [0] * numSlots)
def incrementCount(self, obj, slot):
self.objToCounts[obj][slot] += 1
def getCount(self, obj, slot):
return self.objToCounts[obj][slot]
def getCounts(self):
return dict((k, sum(v)) for k, v in self.objToCounts.iteritems())
def wipeSlot(self, slot):
for obj in self.objToCounts.iterkeys():
self.objToCounts[obj][slot] = 0
def shouldBeRemovedFromCounter(self, obj):
return sum(self.objToCounts[obj]) == 0
def wipeZeros(self):
objToBeRemoved = set()
for obj in self.objToCounts.iterkeys():
if sum(self.objToCounts[obj]) == 0:
objToBeRemoved.add(obj)
for obj in objToBeRemoved:
del self.objToCounts[obj]
class SlidingWindowCounter(object):
def __init__(self, windowLengthInSlots):
self.windowLengthInSlots = windowLengthInSlots
self.objCounter = /
SlotBasedCounter(
self.windowLengthInSlots)
self.headSlot = 0
self.tailSlot = self.slotAfter(self.headSlot)
def incrementCount(self, obj):
self.objCounter.incrementCount(obj, self.headSlot)
def getCountsThenAdvanceWindow(self):
counts = self.objCounter.getCounts()
self.objCounter.wipeZeros()
self.objCounter.wipeSlot(self.tailSlot)
self.headSlot = self.tailSlot
self.tailSlot = self.slotAfter(self.tailSlot)
return counts
def slotAfter(self, slot):
return (slot + 1) % self.windowLengthInSlots
class RollingCountBolt(BasicBolt):
numWindowChunks = 5
emitFrequencyInSeconds = 60
windowLengthInSeconds = numWindowChunks * \
emitFrequencyInSeconds
def __init__(self):
super(RollingCountBolt, self).__init__(script=__file__)
self.counter = SlidingWindowCounter(
self.windowLengthInSeconds /
self.emitFrequencyInSeconds
@classmethod
def declareOutputFields(cls):
return ['word', 'count']
def process(self, tup):
if tup.is_tick_tuple():
self.emitCurrentWindowCounts()
else:
self.counter.incrementCount(tup.values[0])
def emitCurrentWindowCounts(self):
counts = self.counter.getCountsThenAdvanceWindow()
for k, v in counts.iteritems():
storm.emit([k, v])
def getComponentConfiguration(self):
return {"topology.tick.tuple.freq.secs":
self.emitFrequencyInSeconds}
def run():
RollingCountBolt().run()
SlotBasedCounter 存储每个单词的 numSlots(五个)计数值。每个槽存储 emitFrequencyInSeconds(60)秒的数据。超过 5 分钟的计数值将被丢弃。
bolt 如何知道 60 秒已经过去?Storm 通过提供一个名为 tick tuples 的功能来简化这一点。当您需要根据计划在 bolt 中执行某些逻辑时,此功能非常有用。要使用此功能,请执行以下步骤:
-
在
getComponentConfiguration()中,返回一个包含topology.tick.tuple.freq.secs键的字典。该值是期望的 tick 之间的秒数。 -
在
process()中,检查元组是否为普通元组或 tick 元组。当接收到 tick 元组时,bolt 应该运行其计划中的处理。
中间排名 bolt
中间排名 bolt 维护一个按出现次数排序的 maxSize(10)项的字典,并且每 emitFrequencyInSeconds(15)秒发出这些项。在生产中,拓扑将运行许多此 bolt 的实例,每个实例维护整体单词集合的 子集 中的顶级单词。拥有许多相同组件的实例允许拓扑处理大量推文,并且即使不同单词的数量相当大,也能轻松地保持所有计数在内存中。
在 intermediaterankings.py 中输入以下代码:
from petrel import storm
from petrel.emitter import BasicBolt
def tup_sort_key(tup):
return tup.values[1]
class IntermediateRankingsBolt(BasicBolt):
emitFrequencyInSeconds = 15
maxSize = 10
def __init__(self):
super(IntermediateRankingsBolt, self).__init__(script=__file__)
self.rankedItems = {}
def declareOutputFields(self):
return ['word', 'count']
def process(self, tup):
if tup.is_tick_tuple():
for t in self.rankedItems.itervalues():
storm.emit(t.values)
else:
self.rankedItems[tup.values[0]] = tup
if len(self.rankedItems) > self.maxSize:
for t in sorted(
self.rankedItems.itervalues(), key=tup_sort_key):
del self.rankedItems[t.values[0]]
break
def getComponentConfiguration(self):
return {"topology.tick.tuple.freq.secs":
self.emitFrequencyInSeconds}
def run():
IntermediateRankingsBolt().run()
总排名 bolt
总排名 bolt 与中间排名 bolt 非常相似。拓扑中只有一个此 bolt 的实例。它接收来自该 bolt 每个实例的顶级单词,选择整体上的 maxSize(10)项。
在 totalrankings.py 中输入以下代码:
import logging
from petrel import storm
from petrel.emitter import BasicBolt
log = logging.getLogger('totalrankings')
def tup_sort_key(tup):
return tup.values[1]
class TotalRankingsBolt(BasicBolt):
emitFrequencyInSeconds = 15
maxSize = 10
def __init__(self):
super(TotalRankingsBolt, self).__init__(script=__file__)
self.rankedItems = {}
def declareOutputFields(self):
return ['word', 'count']
def process(self, tup):
if tup.is_tick_tuple():
for t in sorted(
self.rankedItems.itervalues(),
key=tup_sort_key,
reverse=True):
log.info('Emitting: %s', repr(t.values))
storm.emit(t.values)
else:
self.rankedItems[tup.values[0]] = tup
if len(self.rankedItems) > self.maxSize:
for t in sorted(
self.rankedItems.itervalues(),
key=tup_sort_key):
del self.rankedItems[t.values[0]]
break
zero_keys = set(
k for k, v in self.rankedItems.iteritems()
if v.values[1] == 0)
for k in zero_keys:
del self.rankedItems[k]
def getComponentConfiguration(self):
return {"topology.tick.tuple.freq.secs": self.emitFrequencyInSeconds}
def run():
TotalRankingsBolt().run()
定义拓扑
这里是定义拓扑结构的 create.py 脚本:
from twitterstream import TwitterStreamSpout
from splitsentence import SplitSentenceBolt
from rollingcount import RollingCountBolt
from intermediaterankings import IntermediateRankingsBolt
from totalrankings import TotalRankingsBolt
def create(builder):
spoutId = "spout"
splitterId = "splitter"
counterId = "counter"
intermediateRankerId = "intermediateRanker"
totalRankerId = "finalRanker"
builder.setSpout(spoutId, TwitterStreamSpout(), 1)
builder.setBolt(
splitterId, SplitSentenceBolt(), 1).shuffleGrouping("spout")
builder.setBolt(
counterId, RollingCountBolt(), 4).fieldsGrouping(
splitterId, ["word"])
builder.setBolt(
intermediateRankerId,
IntermediateRankingsBolt(), 4).fieldsGrouping(
counterId, ["word"])
builder.setBolt(
totalRankerId, TotalRankingsBolt()).globalGrouping(
intermediateRankerId)
这个拓扑的结构与第三章中“介绍 Petrel”的单词计数拓扑相似。第三章。TotalRankingsBolt有一个新的细节。如前所述,这个螺栓只有一个实例,它使用globalGrouping(),因此所有来自IntermediateRankingsBolt的元组都发送给它。
你可能想知道为什么拓扑需要中间排名和总排名螺栓。为了让我们知道整体上的顶级单词,需要有一个单一的螺栓实例(总排名)能够看到整个推文流。但在高数据速率下,单个螺栓不可能跟上流量。中间排名的螺栓实例“屏蔽”了总排名螺栓的流量,计算其推文流切片中的顶级单词。这允许最终的排名螺栓计算整体中最常见的单词,同时只消耗整体单词计数的一小部分。优雅!
运行拓扑
在我们运行拓扑之前,还有一些小问题需要解决:
-
将第三章中的第二个示例中的
logconfig.ini文件复制到这个拓扑的目录中。 -
创建一个名为
setup.sh的文件。Petrel 将在启动时将此脚本与拓扑打包并运行。此脚本安装拓扑使用的第三方 Python 库。文件看起来像这样:pip install -U pip pip install nltk==3.0.1 oauthlib==0.7.2 tweepy==3.2.0 -
创建一个名为
manifest.txt的文件,包含以下两行:logconfig.ini setup.sh -
在运行拓扑之前,让我们回顾一下我们创建的文件列表。确保你已经正确创建了这些文件:
-
topology.yaml -
twitterstream.py -
splitsentence.py -
rollingcount.py -
intermediaterankings.py -
totalrankings.py -
manifest.txt -
setup.sh
-
-
使用以下命令运行拓扑:
petrel submit --config topology.yaml --logdir `pwd`
一旦拓扑开始运行,在topology目录中打开另一个终端。输入以下命令以查看总排名螺栓的log文件,按时间顺序从旧到新排序:
ls -ltr petrel*totalrankings.log
如果这是你第一次运行拓扑,将只列出一个日志文件。每次运行都会创建一个新的文件。如果有多个文件列出,请选择最新的一个。输入以下命令以监控日志文件的内容(在您的系统上,确切的文件名可能不同):
tail -f petrel24748_totalrankings.log
大约每 15 秒,你会看到按流行度降序排列的前 10 个单词的日志消息,如下所示:

摘要
在本章中,我们使用多种新技术和库开发了一个复杂的拓扑。阅读这个示例后,你应该准备好开始将 Petrel 和 Storm 应用于解决实际问题。
在下一章中,我们将更详细地研究一些在操作集群时非常有用的 Storm 内置功能,例如日志记录和监控。
第五章:使用 Redis 和 MongoDB 进行持久化
通常有必要将元组存储在持久数据存储中,如 NoSQL 数据库或快速键值缓存,以执行额外的分析。在本章中,我们将借助两种流行的持久化媒体:Redis 和 MongoDB,回顾第四章中的 Twitter 趋势分析拓扑,示例拓扑 - Twitter。
Redis (redis.io/) 是一个开源的 BSD 许可的高级键值缓存和存储。MongoDB 是一个跨平台的文档型数据库 (www.mongodb.org/)。
本章我们将解决以下两个问题:
-
使用 Redis 查找最热门的推文主题
-
使用 MongoDB 计算城市提及的小时聚合
使用 Redis 查找排名前 n 的热门主题
拓扑将计算过去 5 分钟内最受欢迎的单词的滚动排名。词频存储在长度为 60 秒的单独窗口中。它由以下组件组成:
-
Twitter 流源(
twitterstream.py):此源从 Twitter 样本流中读取推文。此源与第四章中的相同,示例拓扑 - Twitter。 -
分词 bolt(
splitsentence.py):此 bolt 接收推文并将它们分割成单词。这也与第四章中的相同,示例拓扑 - Twitter。 -
滚动词频 bolt(
rollingcount.py):此 bolt 接收单词并计算它们的出现次数。Redis 键看起来像twitter_word_count:<当前窗口开始时间(秒)>,值以以下简单格式存储:{ "word1": 5, "word2", 3, }此 bolt 使用 Redis 的
expireat命令在 5 分钟后丢弃旧数据。以下代码行执行关键工作:self.conn.zincrby(name, word) self.conn.expireat(name, expires) Total rankings bolt (totalrankings.py)
在此 bolt 中,以下代码执行最重要的工作:
self.conn.zunionstore(
'twitter_word_count',
['twitter_word_count:%s' % t for t in xrange(
first_window, now_floor)])
for t in self.conn.zrevrange('twitter_word_count', 0, self.maxSize, withscores=True):
log.info('Emitting: %s', repr(t))
storm.emit(t)
此 bolt 计算过去 num_windows 个周期内的前maxSize个单词。zunionstore()将周期内的词频合并。zrevrange()对合并的计数进行排序,返回前maxSize个单词。
在原始的 Twitter 示例中,大致相同的逻辑在rollingcount.py、intermediaterankings.py和totalrankings.py中实现。使用 Redis,我们只需几行代码就能实现相同的计算。设计将大部分工作委托给了 Redis。根据您的数据量,这可能不如上一章中的拓扑扩展性好。然而,它展示了 Redis 的能力远不止简单地存储数据。
拓扑配置文件 - Redis 案例
接下来是拓扑配置文件。根据您的 Redis 安装情况,您可能需要更改redis_url的值。
在topology.yaml中输入以下代码:
nimbus.host: "localhost"
topology.workers: 1
oauth.consumer_key: "your-key-for-oauth-blah"
oauth.consumer_secret: "your-secret-for-oauth-blah"
oauth.access_token: "your-access-token-blah"
oauth.access_token_secret: "your-access-secret-blah"
twitter_word_count.redis_url: "redis://localhost:6379"
twitter_word_count.num_windows: 5
twitter_word_count.window_duration: 60
滚动词频 bolt - Redis 案例
滚动词计数螺栓与第三章中的词计数螺栓类似。早期章节中的螺栓简单地无限期地累积词计数。这不利于分析 Twitter 上的热门话题,因为热门话题可能在一瞬间发生变化。相反,我们希望计数反映最新信息。如前所述,滚动词计数螺栓将数据存储在基于时间的桶中。然后,它定期丢弃超过 5 分钟年龄的桶。因此,此螺栓的词计数只考虑最后 5 分钟的数据。
在rollingcount.py中输入以下代码:
import math
import time
from collections import defaultdict
import redis
from petrel import storm
from petrel.emitter import BasicBolt
class RollingCountBolt(BasicBolt):
def __init__(self):
super(RollingCountBolt, self).__init__(script=__file__)
def initialize(self, conf, context):
self.conf = conf
self.num_windows = self.conf['twitter_word_count.num_windows']
self.window_duration = self.conf['twitter_word_count.window_duration']
self.conn = redis.from_url(conf['twitter_word_count.redis_url'])
@classmethod
def declareOutputFields(cls):
return ['word', 'count']
def process(self, tup):
word = tup.values[0]
now = time.time()
now_floor = int(math.floor(now / self.window_duration) * self.window_duration)
expires = int(now_floor + self.num_windows * self.window_duration)
name = 'twitter_word_count:%s' % now_floor
self.conn.zincrby(name, word)
self.conn.expireat(name, expires)
def run():
RollingCountBolt().run()
总排名螺栓 – Redis 案例
在totalrankings.py中输入以下代码:
import logging
import math
import time
import redis
from petrel import storm
from petrel.emitter import BasicBolt
log = logging.getLogger('totalrankings')
class TotalRankingsBolt(BasicBolt):
emitFrequencyInSeconds = 15
maxSize = 10
def __init__(self):
super(TotalRankingsBolt, self).__init__(script=__file__)
self.rankedItems = {}
def initialize(self, conf, context):
self.conf = conf
self.num_windows = \
self.conf['twitter_word_count.num_windows']
self.window_duration = \
self.conf['twitter_word_count.window_duration']
self.conn = redis.from_url(
conf['twitter_word_count.redis_url'])
def declareOutputFields(self):
return ['word', 'count']
def process(self, tup):
if tup.is_tick_tuple():
now = time.time()
now_floor = int(math.floor(now / self.window_duration) *
self.window_duration)
first_window = int(now_floor - self.num_windows *
self.window_duration)
self.conn.zunionstore(
'twitter_word_count',
['twitter_word_count:%s' % t for t in xrange(first_window, now_floor)])
for t in self.conn.zrevrange('
'twitter_word_count', 0,
self.maxSize, withScores=True):
log.info('Emitting: %s', repr(t))
storm.emit(t)
def getComponentConfiguration(self):
return {"topology.tick.tuple.freq.secs":
self.emitFrequencyInSeconds}
def run():
TotalRankingsBolt().run()
定义拓扑 – Redis 案例
这里是定义拓扑结构的create.py脚本:
from twitterstream import TwitterStreamSpout
from splitsentence import SplitSentenceBolt
from rollingcount import RollingCountBolt
from totalrankings import TotalRankingsBolt
def create(builder):
spoutId = "spout"
splitterId = "splitter"
counterId = "counter"
totalRankerId = "finalRanker"
builder.setSpout(spoutId, TwitterStreamSpout(), 1)
builder.setBolt(
splitterId, SplitSentenceBolt(), 1).shuffleGrouping("spout")
builder.setBolt(
counterId, RollingCountBolt(), 4).fieldsGrouping(
splitterId, ["word"])
builder.setBolt(
totalRankerId, TotalRankingsBolt()).globalGrouping(
counterId)
运行拓扑 – Redis 案例
在我们运行拓扑之前,还有一些小事情要处理:
-
将第三章中的第二个示例
logconfig.ini文件复制到这个拓扑目录中。 -
创建一个名为
setup.sh的文件。Petrel 将与此拓扑一起打包此脚本并在启动时运行它。此脚本安装拓扑使用的第三方 Python 库。文件看起来像这样:pip install -U pip pip install nltk==3.0.1 oauthlib==0.7.2 tweepy==3.2.0 -
创建一个包含以下两行的
manifest.txt文件:logconfig.ini setup.sh -
在一个知名节点上安装 Redis 服务器。所有工作节点都将在此处存储状态:
sudo apt-get install redis-server -
在所有 Storm 工作机器上安装 Python Redis 客户端:
sudo apt-get install python-redis -
在运行拓扑之前,让我们回顾一下我们创建的文件列表。确保你已经正确创建了这些文件:
-
topology.yaml -
twitterstream.py -
splitsentence.py -
rollingcount.py -
totalrankings.py -
manifest.txt -
setup.sh
-
-
使用以下命令运行拓扑:
petrel submit --config topology.yaml --logdir `pwd`
一旦拓扑开始运行,在拓扑目录中打开另一个终端。输入以下命令以查看总排名螺栓的日志文件,按时间顺序从旧到新排序:
ls -ltr petrel*totalrankings.log
如果你第一次运行拓扑,将只列出一个日志文件。每次运行都会创建一个新的文件。如果有几个列出,请选择最新的一个。输入以下命令以监控日志文件的内容(在您的系统上,确切的文件名可能不同):
tail -f petrel24748_totalrankings.log
定期,你会看到如下输出,列出按流行度降序排列的前 5 个单词:
totalrankings的示例输出:
[2015-08-10 21:30:01,691][totalrankings][INFO]Emitting: ('love', 74.0)
[2015-08-10 21:30:01,691][totalrankings][INFO]Emitting: ('amp', 68.0)
[2015-08-10 21:30:01,691][totalrankings][INFO]Emitting: ('like', 67.0)
[2015-08-10 21:30:01,692][totalrankings][INFO]Emitting: ('zaynmalik', 61.0)
[2015-08-10 21:30:01,692][totalrankings][INFO]Emitting: ('mtvhottest', 61.0)
[2015-08-10 21:30:01,692][totalrankings][INFO]Emitting: ('get', 58.0)
[2015-08-10 21:30:01,692][totalrankings][INFO]Emitting: ('one', 49.0)
[2015-08-10 21:30:01,692][totalrankings][INFO]Emitting: ('follow', 46.0)
[2015-08-10 21:30:01,692][totalrankings][INFO]Emitting: ('u', 44.0)
[2015-08-10 21:30:01,692][totalrankings][INFO]Emitting: ('new', 38.0)
[2015-08-10 21:30:01,692][totalrankings][INFO]Emitting: ('much', 37.0)
使用 MongoDB 按城市名称查找推文的每小时计数
MongoDB 是一个用于存储大量数据的流行数据库。它设计用于跨多个节点轻松扩展。
要运行此拓扑,你首先需要安装 MongoDB 并配置一些数据库特定的设置。此示例使用名为 cities 的 MongoDB 数据库,其集合名为 minute。为了按城市和分钟计算计数,我们必须在 cities.minute 集合上创建一个唯一索引。为此,启动 MongoDB 命令行客户端:
mongo
在 cities.minute 集合上创建一个唯一索引:
use cities
db.minute.createIndex( { minute: 1, city: 1 }, { unique: true } )
此索引在 MongoDB 中存储每分钟城市计数的时序数据。在运行示例拓扑以捕获一些数据后,我们将运行一个独立的命令行脚本 (city_report.py) 来按小时和城市汇总每分钟的计数。
这是之前 Twitter 拓扑的一个变体。此示例使用 Python geotext 库 (pypi.python.org/pypi/geotext) 在推文中查找城市名称。
下面是拓扑的概述:
-
读取推文。
-
将它们拆分成单词并找出城市名称。
-
在 MongoDB 中,计算每分钟提及一个城市的次数。
-
Twitter 流源 (
twitterstream.py):此代码从 Twitter 样本流中读取推文。 -
城市计数 Bolt (
citycount.py):此代码查找城市名称并将其写入 MongoDB。它与 Twitter 样本的SplitSentenceBolt类似,但在按单词拆分后,它会查找城市名称。
这里的 _get_words() 函数与之前的示例略有不同。这是因为 geotext 不识别小写字符串为城市名称。
它创建或更新 MongoDB 记录,利用分钟和城市上的唯一索引来累积每分钟的计数。
这是表示 MongoDB 中时间序列数据的一种常见模式。每条记录还包括一个 hour 字段。city_report.py 脚本使用此字段来计算每小时计数。
在 citycount.py 文件中输入以下代码:
Import datetime
import logging
import geotext
import nltk.corpus
import pymongo
from petrel import storm
from petrel.emitter import BasicBolt
log = logging.getLogger('citycount')
class CityCountBolt(BasicBolt):
def __init__(self):
super(CityCountBolt, self).__init__(script=__file__)
self.stop_words = set(nltk.corpus.stopwords.words('english'))
self.stop_words.update(['http', 'https', 'rt'])
self.stop_cities = set([
'bay', 'best', 'deal', 'man', 'metro', 'of', 'un'])
def initialize(self, conf, context):
self.db = pymongo.MongoClient()
def declareOutputFields(self):
return []
def process(self, tup):
clean_text = ' '.join(w for w in self._get_words(tup.values[0]))
places = geotext.GeoText(clean_text)
now_minute = self._get_minute()
now_hour = now_minute.replace(minute=0)
for city in places.cities:
city = city.lower()
if city in self.stop_cities:
continue
log.info('Updating count: %s, %s, %s', now_hour, now_minute, city)
self.db.cities.minute.update(
{
'hour': now_hour,
'minute': now_minute,
'city': city
},
{'$inc': { 'count' : 1 } },
upsert=True)
@staticmethod
def _get_minute():
return datetime.datetime.now().replace(second=0, microsecond=0)
def _get_words(self, sentence):
for w in nltk.word_tokenize(sentence):
wl = w.lower()
if wl.isalpha() and wl not in self.stop_words:
yield w
def run():
CityCountBolt().run()
定义拓扑 – MongoDB 的情况
在 create.py 文件中输入以下代码:
from twitterstream import TwitterStreamSpout
from citycount import CityCountBolt
def create(builder):
spoutId = "spout"
cityCountId = "citycount"
builder.setSpout(spoutId, TwitterStreamSpout(), 1)
builder.setBolt(cityCountId, CityCountBolt(), 1).shuffleGrouping("spout")
运行拓扑 – MongoDB 的情况
在我们运行拓扑之前,还有一些小事情需要处理:
-
将 第三章 中的第二个示例中的
logconfig.ini文件复制到该拓扑目录。 -
创建一个名为
setup.sh的文件:pip install -U pip pip install nltk==3.0.1 oauthlib==0.7.2 tweepy==3.2.0 geotext==0.1.0 pymongo==3.0.3 -
接下来,创建一个名为
manifest.txt的文件。这与 Redis 示例相同。安装 MongoDB 服务器。在 Ubuntu 上,你可以使用
docs.mongodb.org/manual/tutorial/install-mongodb-on-ubuntu/中给出的说明。 -
在所有 Storm 工作机上都安装 Python MongoDB 客户端:
pip install pymongo==3.0.3 -
为了验证
pymongo是否已安装且索引已正确创建,通过运行python启动一个交互式 Python 会话。然后使用此代码:import pymongo from pymongo import MongoClient db = MongoClient() for index in db.cities.minute.list_indexes(): print index你应该看到以下输出。第二行是我们添加的索引:
SON([(u'v', 1), (u'key', SON([(u'_id', 1)])), (u'name', u'_id_'), (u'ns', u'cities.minute')]) SON([(u'v', 1), (u'unique', True), (u'key', SON([(u'minute', 1.0), (u'city', 1.0)])), (u'name', u'minute_1_city_1'), (u'ns', u'cities.minute')]) -
接下来,安装
geotext:pip install geotext==0.1.0 -
在运行拓扑之前,让我们回顾一下我们创建的文件列表。确保你已经正确创建了这些文件:
-
topology.yaml -
twitterstream.py -
citycount.py -
manifest.txt -
setup.sh
-
-
使用以下命令运行拓扑:
petrel submit --config topology.yaml --logdir `pwd`
city_report.py 文件是一个独立的脚本,它从拓扑插入的数据生成简单的每小时报告。此脚本使用 MongoDB 聚合来计算每小时的总数。如前所述,报告依赖于hour字段的存。
在 city_report.py 中输入以下代码:
import pymongo
def main():
db = pymongo.MongoClient()
pipeline = [{
'$group': {
'_id': { 'hour': '$hour', 'city': '$city' },
'count': { '$sum': '$count' }
}
}]
for r in db.cities.command('aggregate', 'minute', pipeline=pipeline)['result']:
print '%s,%s,%s' % (r['_id']['city'], r['_id']['hour'], r['count'])
if __name__ == '__main__':
main()
摘要
在本章中,我们展示了如何使用两个流行的 NoSQL 存储引擎(Redis 和 MongoDB)与 Storm 结合使用。我们还向您展示了如何在拓扑中创建数据并从其他应用程序访问它,证明了 Storm 可以成为 ETL 管道的有效部分。
第六章。Petrel 实践
在前面的章节中,我们看到了 Storm 拓扑的工作示例,既有简单的也有复杂的。然而,在这样做的时候,我们跳过了一些你在开发自己的拓扑时可能需要的工具和技术:
-
Storm 是一个运行代码的绝佳环境,但将代码部署到 Storm(即使在本地机器上)也会增加复杂性和额外的时间。我们将看到如何在 Storm 之外测试你的 spouts 和 bolts。
-
当组件在 Storm 中运行时,它们无法从控制台读取,这阻止了使用 pdb,即标准的 Python 调试器。本章演示了 Winpdb,这是一个适合调试 Storm 内部组件的交互式调试工具。
-
Storm 让你能够轻松利用多台服务器的强大功能,但你的代码的性能仍然很重要。在本章中,我们将看到一些衡量我们拓扑组件性能的方法。
测试 bolt
Storm 使得部署和运行 Python 拓扑变得容易,但在 Storm 中开发和测试它们具有挑战性,无论是在独立运行的 Storm 还是完整的 Storm 部署中:
-
Storm 代表你启动程序——不仅包括你的 Python 代码,还包括辅助的 Java 进程
-
它控制 Python 组件的标准输入和输出通道
-
Python 程序必须定期响应心跳消息,否则将被 Storm 关闭
这使得使用典型的工具和技术来调试 Storm 拓扑变得困难,这些工具和技术通常用于其他 Python 代码,例如从命令行运行并使用 pdb 进行调试的常见技术。
Petrel 的模拟模块帮助我们完成这项工作。它提供了一个简单的、独立的 Python 容器,用于测试简单的拓扑并验证预期的结果是否返回。
在 Petrel 术语中,一个简单的拓扑是指只输出到默认流且没有分支或循环的拓扑。run_simple_topology()假设列表中的第一个组件是一个 spout,将每个组件的输出传递给序列中的下一个组件。
示例 - 测试 SplitSentenceBolt
让我们来看一个例子。这里是第三章中第一个例子,即介绍 Petrel的splitsentence.py文件,添加了单元测试:
from nose.tools import assert_equal
from petrel import mock, storm
from petrel.emitter import BasicBolt
from randomsentence import RandomSentenceSpout
class SplitSentenceBolt(BasicBolt):
def __init__(self):
super(SplitSentenceBolt, self).__init__(script=__file__)
def declareOutputFields(self):
return ['word']
def process(self, tup):
words = tup.values[0].split(" ")
for word in words:
storm.emit([word])
def test():
bolt = SplitSentenceBolt()
mock_spout = mock.MockSpout(
RandomSentenceSpout.declareOutputFields(),
[["Madam, I'm Adam."]])
result = mock.run_simple_topology(
None, [mock_spout, bolt], result_type=mock.LIST)
assert_equal([['Madam,'], ["I'm"], ['Adam.']], result[bolt])
def run():
SplitSentenceBolt().run()
要运行测试,请输入以下命令:
pip install nosetests
-
首先,通过运行以下命令安装 Python 的
nosetests库:pip install nosetests -
接下来,运行以下行:
nosetests -v splitsentence.py
如果一切顺利,你会看到以下输出:
splitsentence.test ... ok
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Nose 是一个非常强大的工具,具有许多功能。我们在这里不会详细讨论它,但你可以在nose.readthedocs.org/en/latest/找到文档。
示例 - 使用 WordCountBolt 测试 SplitSentenceBolt
以下示例展示了如何测试一系列相关组件。在以下代码中,我们看到wordcount.py的新版本,它测试了SplitSentenceBolt和WordCountBolt之间的交互:
from collections import defaultdict
from nose.tools import assert_equal
from petrel import mock, storm
from petrel.emitter import BasicBolt
from randomsentence import RandomSentenceSpout
from splitsentence import SplitSentenceBolt
class WordCountBolt(BasicBolt):
def __init__(self):
super(WordCountBolt, self).__init__(script=__file__)
self._count = defaultdict(int)
@classmethod
def declareOutputFields(cls):
return ['word', 'count']
def process(self, tup):
word = tup.values[0]
self._count[word] += 1
storm.emit([word, self._count[word]])
def test():
ss_bolt = SplitSentenceBolt()
wc_bolt = WordCountBolt()
mock_spout = mock.MockSpout(
RandomSentenceSpout.declareOutputFields(),
[["the bart the"]])
result = mock.run_simple_topology(
None,
[mock_spout, ss_bolt, wc_bolt],
result_type=mock.LIST)
assert_equal([['the', 1], ['bart', 1], ['the', 2]], result[wc_bolt])
def run():
WordCountBolt().run()
测试非常直接;我们只是实例化两个组件,并在调用mock.run_simple_topology()时按正确的顺序包含它们。
注意
两个示例测试在调用run_simple_topology()时都指定了result_type=mock.LIST。这个参数选项告诉 Petrel 在返回输出元组时使用哪种格式。选项包括:
STORM_TUPLE
LIST
TUPLE
NAMEDTUPLE
通常,对于输出字段数量较少的组件,LIST是一个不错的选择,而对于字段数量较多的组件(即允许测试通过字段名而不是数字索引来访问结果字段),NAMEDTUPLE则更易读。如果测试需要检查结果的其他属性,例如,较少使用的流属性,则STORM_TUPLE很有用。
调试
到目前为止,我们使用日志消息和自动化测试来调试拓扑。这些技术非常强大,但有时可能需要在 Storm 环境中直接进行调试。例如,问题可能:
-
依赖于以特定用户身份运行
-
仅在有真实数据时发生
-
仅当组件有多个并行运行的实例时发生
本节介绍了一个在 Storm 内部进行调试的工具。
Winpdb 是一个基于 GUI 的 Python 调试器,支持嵌入式调试。如果你不熟悉“嵌入式调试”这个术语,请注意:它仅仅意味着 Winpdb 可以附加到以某种方式启动的程序,而不一定是来自 WinDbg 或你的命令行。因此,它非常适合调试在 Storm 中运行的 Petrel 组件。
安装 Winpdb
激活你的 Petrel 虚拟环境,然后使用pip来安装它:
source <virtualenv directory>/bin/activate
pip install winpdb
添加 Winpdb 断点
在splitsentence.py文件中,在run()函数的开始处添加以下内容:
import rpdb2
rpdb2.start_embedded_debugger('password')
'password'的值可以是任何东西;这仅仅是你在下一步附加到splitsentence.py时将使用的密码。
当此行代码执行时,脚本将冻结默认的 5 分钟,等待调试器附加。
启动和附加调试器
现在运行拓扑:
petrel submit --config topology.yaml
一旦你看到来自 spout 的日志消息,你就会知道拓扑已经启动并运行,因此你可以连接到调试器。
通过运行winpdb简单地启动Winpdb。
有关如何使用 Winpdb 进行嵌入式调试的更多详细信息,请参阅winpdb.org/docs/embedded-debugging/文档。
当窗口出现时,从菜单中选择文件 | 附加。将出现一个密码对话框。在这里,输入传递给start_embedded_debugger()的相同密码,然后点击确定按钮,如图所示:

接下来,选择要附加到的进程,然后点击确定,如图所示:

现在,您将看到主 Winpdb 窗口,其中断点下的行被突出显示。如果您使用过其他调试器,Winpdb 应该很容易使用。如果您需要使用 Winpdb 的帮助,以下教程对您非常有用:
code.google.com/p/winpdb/wiki/DebuggingTutorial。

分析您的拓扑性能
性能可能是任何应用的担忧。这对于 Storm 拓扑同样适用,也许更为重要。
当您试图通过拓扑推送大量数据时,原始性能当然是一个担忧——更快的组件意味着可以处理更多的数据。但了解单个组件的元组处理性能也同样重要。这些信息可以以两种方式使用。
第一点是了解哪些组件较慢,因为这告诉您在尝试使代码更快时应该关注的地方。一旦您知道哪个组件(或多个组件)较慢,您可以使用 Python cProfile 模块([pymotw.com/2/profile/](http://pymotw.com/2/profile/))和行分析器(https://github.com/rkern/line_profiler)等工具来了解代码花费大部分时间的地方。
即使经过分析,一些组件仍然会比其他组件更快。在这种情况下,了解组件之间的相对性能可以帮助您配置拓扑以获得最佳性能。
第二点有些微妙,让我们看看一个例子。在以下代码中,我们看到了单词计数拓扑中两个 Storm 组件的日志摘录。这些日志消息是由 Petrel 自动生成的。第一个是分句螺栓,第二个是单词计数螺栓:
[2015-05-07 22:51:44,772][storm][DEBUG]BasicBolt profile: total_num_tuples=79, num_tuples=79, avg_read_time=0.002431 (19.1%), avg_process_time=0.010279 (80.7%), avg_ack_time=0.000019 (0.2%)
[2015-05-07 22:51:45,776][storm][DEBUG]BasicBolt profile: total_num_tuples=175, num_tuples=96, avg_read_time=0.000048 (0.5%), avg_process_time=0.010374 (99.3%), avg_ack_time=0.000025 (0.2%)
[2015-05-07 22:51:46,784][storm][DEBUG]BasicBolt profile: total_num_tuples=271, num_tuples=96, avg_read_time=0.000043 (0.4%), avg_process_time=0.010417 (99.3%), avg_ack_time=0.000026 (0.2%)
[2015-05-07 22:51:47,791][storm][DEBUG]BasicBolt profile: total_num_tuples=368, num_tuples=97, avg_read_time=0.000041 (0.4%), avg_process_time=0.010317 (99.4%), avg_ack_time=0.000021 (0.2%)
分句螺栓日志
以下为分句螺栓日志:
[2015-05-07 22:51:44,918][storm][DEBUG]BasicBolt profile: total_num_tuples=591, num_tuples=591, avg_read_time=0.001623 (95.8%), avg_process_time=0.000052 (3.1%), avg_ack_time=0.000019 (1.1%)
[2015-05-07 22:51:45,924][storm][DEBUG]BasicBolt profile: total_num_tuples=1215, num_tuples=624, avg_read_time=0.001523 (94.7%), avg_process_time=0.000060 (3.7%), avg_ack_time=0.000025 (1.5%)
[2015-05-07 22:51:46,930][storm][DEBUG]BasicBolt profile: total_num_tuples=1829, num_tuples=614, avg_read_time=0.001559 (95.4%), avg_process_time=0.000055 (3.3%), avg_ack_time=0.000021 (1.3%)
[2015-05-07 22:51:47,938][storm][DEBUG]BasicBolt profile: total_num_tuples=2451, num_tuples=622, avg_read_time=0.001547 (95.7%), avg_process_time=0.000049 (3.0%), avg_ack_time=0.000020 (1.3%)
单词计数螺栓日志
这些日志表明,分句螺栓在处理和确认每个元组时花费了 0.010338 秒(0.010317 + 0.000021),而单词计数螺栓每个元组花费了 0.000069 秒(0.000049 + 0.000020)。分句螺栓较慢,这表明您可能需要比单词计数螺栓更多的分句螺栓实例。
注意
为什么在先前的计算中没有考虑读取时间?读取时间包括从 Storm 读取元组所花费的 CPU 时间,但也包括等待(即睡眠)元组到达所花费的时间。如果上游组件提供数据较慢,我们不希望将这段时间计入我们的组件。因此,为了简单起见,我们省略了读取时间。
当然,每条元组的性能只是其中一部分。您还必须考虑要处理的元组数量。在前面的 4 秒日志中,分割句子螺栓接收了 97 个元组(句子),而单词计数螺栓接收了 622 个元组(单词)。现在我们将这些数字应用到每条元组的处理时间上:
0.010338 seconds/tuple * 97 tuples = 1.002786 seconds (Split sentence)
0.000069 seconds/tuple * 622 tuples = 0.042918 seconds (Word count)
分割句子螺栓使用的总时间要大得多(大约大 23 倍),在配置拓扑的并行性时,我们应该考虑到这一点。例如,我们可能将 topology.yaml 配置如下:
petrel.parallelism.splitsentence: 24
petrel.parallelism.wordcount: 1
通过以这种方式配置拓扑,我们帮助确保在高流量率时,有足够的分割句子螺栓以避免成为瓶颈,使单词计数螺栓始终保持忙碌状态。
注意
前一节中的日志使用了故意修改以运行得更慢的分割句子螺栓版本,以便使示例更清晰。
摘要
在本章中,您学到了一些技能,这些技能将帮助您在构建自己的拓扑时更加高效。当您开发 spouts 或 bolts 时,您可以在将它们组装成完整的拓扑并在 Storm 上部署之前单独测试它们。如果您遇到仅在 Storm 运行时发生的棘手问题,您可以使用 Winpdb 以及(或代替)日志消息。当您的代码运行时,您可以了解哪些组件花费了大部分时间,因此您可以专注于提高这些领域的性能。有了这些技能,您现在可以出去构建自己的拓扑了。祝您好运!
附录 A. 使用 supervisord 管理 Storm
本附录为您概述以下主题:
-
在集群上管理 Storm
-
介绍 supervisord
-
supervisord 的组件
-
supervisord 安装和配置
在集群上管理 Storm
有许多工具可用于创建多个虚拟机,安装预定义的软件,甚至管理该软件的状态。
介绍 supervisord
supervisord 是一个进程控制系统。它是一个客户端-服务器系统,允许其用户在类 Unix 操作系统上监控和控制多个进程。有关详细信息,请访问 supervisord.org/。
supervisord 组件
监督器的服务器部分被称为 supervisord。它负责在其自己的调用时启动子程序,响应用户的命令,重启崩溃或退出的子进程,记录其子进程的 stdout 和 stderr 输出,并生成和处理与子进程生命周期中的点相对应的“事件”。服务器进程使用配置文件。这通常位于 /etc/supervisord.conf。此配置文件是一个 Windows-INI 风格的 config 文件。通过适当的文件系统权限保持此文件的安全非常重要,因为它可能包含解密的用户名和密码:
-
supervisorctl:supervisor 的命令行客户端部分被称为 supervisorctl。它提供了一个类似于 shell 的界面,用于提供 supervisord 的功能。从 supervisorctl,用户可以连接到不同的 supervisord 进程。他们可以获取由 supervisord 控制的子进程的状态,停止和启动子进程,以及获取 supervisord 的运行进程列表。命令行客户端通过 Unix 域套接字或互联网(TCP)套接字与服务器通信。服务器可以要求客户端用户在允许他们使用命令之前提供身份验证凭据。客户端进程通常使用与服务器相同的配置文件,但任何包含
[supervisorctl]部分的配置文件都将有效。 -
Web 服务器:如果你针对互联网套接字启动 supervisord,可以通过浏览器访问一个(稀疏的)Web 用户界面,其功能与 supervisorctl 相当。访问服务器 URL(例如,
http://localhost:9001/)以通过 Web 界面查看和控制进程状态,在激活配置文件的[inet_http_server]部分后。 -
XML-RPC 接口:提供 Web UI 的相同 HTTP 服务器还提供了一个 XML-RPC 接口,可以用来查询和控制监督程序及其运行的程序。请参阅XML-RPC API 文档。
-
机器:假设我们有两个 IP 地址为
172-31-19-62和172.31.36.23的 EC2 机器。我们将在两台机器上安装 supervisord,然后配置以决定每个机器上运行的 Storm 服务。 -
Storm 和 Zookeeper 设置:让我们在机器
172.31.36.23上运行 Zookeeper、Nimbus、supervisor 和 UI,只在172-31-19-62上运行 supervisor。 -
Zookeeper 版本:
zookeeper-3.4.6.tar.gz。 -
Storm 版本:
apache-storm-0.9.5.tar.gz。
这里是 Zookeeper 服务器设置和配置的过程:
-
下载 Zookeeper 的最新版本并解压:
tar –xvf zookeeper-3.4.6.tar.gz -
在
conf目录中配置zoo.cfg以在集群模式下启动 Zookeeper。 -
Zookeeper 配置:
server.1=172.31.36.23:2888:3888 tickTime=2000 initLimit=10 syncLimit=5 # the directory where the snapshot is stored. dataDir=/home/ec2-user/zookeeper-3.4.6/tmp/zookeeper clientPort=2181 -
确保在
dataDir中指定的目录已创建,并且用户对该目录有读写权限。 -
然后,进入 Zookeeper 的
bin目录,使用以下命令启动zookeeper服务器:[ec2-user@ip-172-31-36-23 bin~]$ zkServer.sh start
Storm 服务器设置和配置:
-
从 Apache Storm 网站下载 Storm 的最新版本并解压:
tar –xvf apache-storm-0.9.5.tar.gz -
这里是 Storm Nimbus 机器以及从属机(仅添加/更改配置)的配置:
storm.zookeeper.servers: - "172.31.36.23" nimbus.host: "172.31.36.23" nimbus.childopts: "-Xmx1024m -Djava.net.preferIPv4Stack=true" ui.childopts: "-Xmx768m -Djava.net.preferIPv4Stack=true" supervisor.childopts: "-Djava.net.preferIPv4Stack=true" worker.childopts: "-Xmx768m -Djava.net.preferIPv4Stack=true" storm.local.dir: "/home/ec2-user/apache-storm-0.9.5/local" supervisor.slots.ports: - 6700 - 6701 - 6702 - 6703
Supervisord 安装
可以通过以下两种方式安装 supervisord:
-
在有互联网访问的系统上安装:
下载设置工具并使用
easy_install方法。 -
在没有互联网访问的系统上安装:
下载所有依赖项,复制到每台机器上,然后安装它。
我们将遵循第二种安装方法,即不需要互联网访问的方法。我们将下载所有依赖项和 supervisord,并将其复制到服务器上。
Supervisord [supervisor-3.1.3.tar.gz] 需要安装以下依赖项:
-
Python 2.7 或更高版本
-
从
pypi.python.org/pypi/setuptools下载setuptools(最新版) -
从
effbot.org/downloads#elementtree下载elementtree(最新版)。elementtree-1.2-20040618.tar.gz -
meld3-0.6.5.tar.gz
让我们在两台机器上安装 supervisord 和必要的依赖项,分别是 172.31.36.23 和 172-31-19-62。
以下为安装依赖项的步骤:
-
setuptools:-
使用以下命令解压
.zip文件:[ec2-user@ip-172-31-19-62 ~]$ tar -xvf setuptools-17.1.1.zip -
进入
setuptools-17.1.1目录并使用sudo运行安装命令:[ec2-user@ip-172-31-19-62 setuptools-17.1.1]$ sudo python setup.py install![Supervisord 安装]()
storm.zookeeper.servers: - "172.31.36.23" nimbus.host: "172.31.36.23" nimbus.childopts: "-Xmx1024m -Djava.net.preferIPv4Stack=true" ui.childopts: "-Xmx768m -Djava.net.preferIPv4Stack=true" supervisor.childopts: "-Djava.net.preferIPv4Stack=true" worker.childopts: "-Xmx768m -Djava.net.preferIPv4Stack=true" storm.local.dir: "/home/ec2-user/apache-storm-0.9.5/local" supervisor.slots.ports: - 6700 - 6701 - 6702 - 6703
-
-
meld3:-
使用以下命令提取
.ts.gz文件:[ec2-user@ip-172-31-19-62 ~]$ tar -xvf meld3-0.6.5.tar.gz -
进入
meld3.-0.6.5目录并运行此命令:[ec2-user@ip-172-31-19-62 meld3-0.6.5]$ sudo pyth setup.py install
![Supervisord 安装]()
-
-
elementtree:-
提取
.ts.gz文件:[ec2-user@ip-172-31-19-62 ~]$ tar -xvf elementtree-1.2-20040618.tar.gz -
进入
elementtree-1.2-20040618并运行以下命令:[ec2-user@ip-172-31-19-62 elementtree-1.2-20040618]$ sudo python setup.py install
![Supervisord 安装]()
-
以下为 supervisord 安装步骤:
-
使用此命令提取
supervisor-3.1.3:[ec2-user@ip-172-31-19-62 ~]$ tar -xvf supervisor-3.1.3.tar.gz -
进入
supervisor-3.1.3目录并运行以下命令:[ec2-user@ip-172-31-19-62 supervisor-3.1.3]$ sudo python setup.py install![Supervisord 安装]()
注意
在另一台机器上也需要进行类似的 supervisord 配置,即 172.31.36.23。
supervisord.conf 的配置
让我们在 172.31.36.23 机器上配置服务,并假设 supervisord 的安装已如前所述完成。一旦安装了 supervisor,您可以使用 supervisorctl 在 172-31-19-62 机器上构建 supervisord.conf 文件以启动 supervisord 和 supervisorctl 命令:
-
创建
supervisor.conf文件。将其放入/etc目录。 -
我们可以使用以下命令来获取示例
supervisord.conf:[ec2-user@ip-172-31-36-23 ~]$ echo_supervisord_conf
查看一下 supervisord.conf 文件:
[unix_http_server]
file = /home/ec2-user/supervisor.sock
chmod = 0777
[inet_http_server] ; inet (TCP) server disabled by default
port=172.31.36.23:9001 ; (ip_address:port specifier, *:port for all iface)
username=user ; (default is no username (open server))
password=123 ; (default is no password (open server))
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisord]
logfile_backups=10 ; (num of main logfile rotation backups;default 10)
logfile=/home/ec2-user/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
pidfile=/home/ec2-user/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false ; (start in foreground if true;default false)
minfds=1024 ; (min. avail startup file descriptors;default 1024)
[supervisorctl]
;serverurl = unix:///home/ec2-user/supervisor.sock
serverurl=http://172.31.36.23:9001 ; use an http:// url to specify an inet socket
;username=chris ; should be same as http_username if set
;password=123 ; should be same as http_password if set
[program:storm-nimbus]
command=/home/ec2-user/apache-storm-0.9.5/bin/storm nimbus
user=ec2-user
autostart=false
autorestart=false
startsecs=10
startretries=999
log_stdout=true
log_stderr=true
stdout_logfile=/home/ec2-user/storm/logs/nimbus.out
logfile_maxbytes=20MB
logfile_backups=10
[program:storm-ui]
command=/home/ec2-user/apache-storm-0.9.5/bin/storm ui
user=ec2-user
autostart=false
autorestart=false
startsecs=10
startretries=999
log_stdout=true
log_stderr=true
stdout_logfile=/home/ec2-user/storm/logs/ui.out
logfile_maxbytes=20MB
logfile_backups=10
[program:storm-supervisor]
command=/home/ec2-user/apache-storm-0.9.5/bin/storm supervisor
user=ec2-user
autostart=false
autorestart=false
startsecs=10
startretries=999
log_stdout=true
log_stderr=true
stdout_logfile=/home/ec2-user/storm/logs/supervisor.out
logfile_maxbytes=20MB
logfile_backups=10
首先启动监督服务器:
[ec2-user@ip-172-31-36-23 ~] sudo /usr/bin/supervisord -c /etc/supervisord.conf
然后,使用 supervisorctl 启动所有进程:
[ec2-user@ip-172-31-36-23 ~] sudo /usr/bin/supervisorctl -c /etc/supervisord.conf status
storm-nimbus STOPPED Not started
storm-supervisor STOPPED Not started
storm-ui STOPPED Not started
[ec2-user@ip-172-31-36-23 ~]$ sudo /usr/bin/supervisorctl -c /etc/supervisord.conf start all
storm-supervisor: started
storm-ui: started
storm-nimbus: started
[ec2-user@ip-172-31-36-23 ~]$ jps
14452 Jps
13315 QuorumPeerMain
14255 nimbus
14233 supervisor
14234 core
[ec2-user@ip-172-31-36-23 ~]$

我们可以在浏览器中查看 supervisord 网页界面并控制进程。52.11.193.108 是 172-31-36-23 机器的公网 IP 地址(http://52.11.193.108:9001):

172-31-19-62 上的 supervisord.conf 配置
在配置文件中仅保留以下服务:
[unix_http_server]
[rpcinterface:supervisor]
[supervisord]
[supervisorctl]
[program:storm-supervisor]
之后,您可以使用 supervisorctl 在 172-31-19-62 机器上启动监督服务器和所有进程。
摘要
在本章中,我们了解了如何在多台机器上运行的分布式 Storm 进程可以通过 supervisord 进程进行管理。supervisord 提供了许多选项,例如autostart=true。如果我们为任何 Storm 进程设置此选项,它也会提高整个系统的可靠性并管理 Nimbus 的故障。








浙公网安备 33010602011771号