Storm-和-Cassandra-实时分析-全-

Storm 和 Cassandra 实时分析(全)

原文:zh.annas-archive.org/md5/7C24B06720C9BE51000AF16D45BAD7FF

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

风暴最初是 Twitter 公司的一个项目,已经毕业并加入 Apache 联盟,因此从 Twitter 风暴改名。这是 Nathan Marz 的心血结晶,现在被 Cloudera 的包括 Apache Hadoop(CDH)和 Hortonworks 数据平台(HDP)等联盟所采用。

Apache Storm 是一个高度可扩展、分布式、快速、可靠的实时计算系统,旨在处理非常高速的数据。Cassandra 通过提供闪电般快速的读写能力来补充计算能力,这是目前与风暴一起提供的最佳数据存储组合。

风暴计算和卡桑德拉存储的结合正在帮助技术传道者解决涉及复杂和大数据量情况的各种业务问题,例如实时客户服务、仪表板、安全性、传感器数据分析、数据货币化等等。

本书将使用户能够利用风暴的处理能力与 Cassandra 的速度和可靠性相结合,开发实时用例的生产级企业解决方案。

本书内容

第一章,“让我们了解风暴”,让您熟悉需要分布式计算解决方案的问题。它将带您了解风暴及其出现的过程。

第二章,“开始您的第一个拓扑”,教您如何设置开发者环境——沙盒,并执行一些代码示例。

第三章,“通过示例了解风暴内部”,教您如何准备风暴的喷嘴和自定义喷嘴。您将了解风暴提供的各种分组类型及其在实际问题中的应用。

第四章,“集群模式下的风暴”,教您如何设置多节点风暴集群,使用户熟悉分布式风暴设置及其组件。本章还将让您熟悉风暴 UI 和各种监控工具。

第五章,“风暴高可用性和故障转移”,将风暴拓扑与 RabbitMQ 代理服务相结合,并通过各种实际示例探讨风暴的高可用性和故障转移场景。

第六章,“向风暴添加 NoSQL 持久性”,向您介绍 Cassandra,并探讨可用于与 Cassandra 一起工作的各种包装 API。我们将使用 Hector API 连接风暴和 Cassandra。

第七章,“Cassandra 分区”、“高可用性和一致性”,带您了解 Cassandra 的内部。您将了解并应用高可用性、暗示的转交和最终一致性的概念,以及它们在 Cassandra 中的上下文中的应用。

第八章,“Cassandra 管理和维护”,让您熟悉 Cassandra 的管理方面,如扩展集群、节点替换等,从而为您提供处理 Cassandra 实际情况所需的全部经验。

第九章,“风暴管理和维护”,让您熟悉风暴的管理方面,如扩展集群、设置并行性和故障排除风暴。

第十章,Storm 中的高级概念,让您了解 Trident API。您将使用一些示例和说明来构建 Trident API。

第十一章,使用 Storm 进行分布式缓存和 CEP,让您了解分布式缓存,其需求以及在 Storm 中解决实际用例的适用性。它还将教育您关于 Esper 作为 CEP 与 Storm 结合使用。

附录,测验答案,包含对真假陈述和填空部分问题的所有答案。

奖励章节使用 Storm 和 Cassandra 解决实际用例,解释了一些实际用例和使用诸如 Storm 和 Cassandra 等技术解决这些用例的蓝图。这一章节可以在www.packtpub.com/sites/default/files/downloads/Bonus_Chapter.pdf上找到。

您需要本书的什么

对于本书,您将需要 Linux/Ubuntu 操作系统、Eclipse 和 8GB 的 RAM。有关设置其他组件(如 Storm、RabbitMQ、Cassandra、内存缓存、Esper 等)的步骤在相应主题的章节中有所涵盖。

这本书适合谁

本书适用于希望使用 Storm 开始进行近实时分析的 Java 开发人员。这将作为开发高可用性和可扩展解决方案以解决复杂实时问题的专家指南。除了开发,本书还涵盖了 Storm 和 Cassandra 的管理和维护方面,这是任何解决方案投入生产的强制要求。

约定

在本书中,您会发现一些文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们的含义解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:"在 Storm 中定义的NumWorker配置或TOPOLOGY_WORKERS配置"。

代码块设置如下:

// instantiates the new builder object
TopologyBuilder builder = new TopologyBuilder();
// Adds a new spout of type "RandomSentenceSpout" with a  parallelism hint of 5
builder.setSpout("spout", new RandomSentenceSpout(), 5);

当我们希望引起您对代码块的特定部分的注意时,相关行或项目会被突出显示:

  public void execute(Tuple tuple) {
      String sentence = tuple.getString(0);
      for(String word: sentence.split(" ")) {
          _collector.emit(tuple, new Values(word)); //1
      }
      _collector.ack(tuple); //2
  }
  public void declareOutputFields(OutputFieldsDeclarer  declarer) {
      declarer.declare(new Fields("word")); //3
  }
}

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

sudo apt-get -qy install rabbitmq-server

新术语重要单词以粗体显示。例如,屏幕上看到的菜单或对话框中的单词会以这种方式出现在文本中:"转到管理选项卡,选择策略,然后单击添加策略"。

注意

警告或重要说明会以这样的方式出现在一个框中。

提示

提示和技巧是这样显示的。

第一章:让我们了解风暴

在本章中,您将熟悉需要分布式计算解决方案的问题,并了解创建和管理此类解决方案可能变得多么复杂。我们将研究解决分布式计算的可用选项。

本章将涵盖以下主题:

  • 熟悉需要分布式计算解决方案的一些问题

  • 现有解决方案的复杂性

  • 提供实时分布式计算的技术

  • 对 Storm 各个组件的高层次视图

  • 飞机通信寻址和报告系统的内部快速查看

在本章结束时,您将能够了解 Apache Storm 的实时场景和应用。您应该了解市场上提供的解决方案以及 Storm 仍然是最佳开源选择的原因。

分布式计算问题

让我们深入了解一些需要分布式解决方案的问题。在我们今天生活的世界中,我们对现在的力量如此敏感,这就产生了分布式实时计算的需求。银行、医疗保健、汽车制造等领域是实时计算可以优化或增强解决方案的中心。

实时商业解决方案,用于信用卡或借记卡欺诈检测

让我们熟悉以下图中描述的问题;当我们使用塑料货币进行任何交易并刷卡进行付款时,银行必须在五秒内验证或拒绝交易。在不到五秒的时间内,数据或交易细节必须加密,通过安全网络从服务银行到发卡银行,然后在发卡银行计算交易的接受或拒绝的整个模糊逻辑,并且结果必须通过安全网络返回。

实时商业解决方案,用于信用卡或借记卡欺诈检测

实时信用卡欺诈检测

挑战,如网络延迟和延迟,可以在一定程度上进行优化,但要在 5 秒内实现前述特性交易,必须设计一个能够处理大量数据并在 1 到 2 秒内生成结果的应用程序。

飞机通信寻址和报告系统

飞机通信寻址和报告系统(ACAR)展示了另一个典型的用例,如果没有可靠的实时处理系统,就无法实现。这些飞机通信系统使用卫星通信(SATCOM),根据以下图,它们实时收集来自飞行各个阶段的语音和数据包数据,并能够实时生成分析和警报。

飞机通信寻址和报告系统

让我们以前述案例中的图为例。飞行遭遇一些真正危险的天气,比如航线上的电暴,然后通过卫星链路和语音或数据网关将该信息发送给空中管制员,后者实时检测并发出警报,以便所有通过该区域的其他航班改变航线。

医疗保健

在这里,让我们向您介绍医疗保健的另一个问题。

这是另一个非常重要的领域,实时分析高容量和速度数据,为医疗保健专业人员提供准确和精确的实时信息,以采取知情的挽救生命行动。

医疗保健

前面的图表描述了医生可以采取明智行动来处理患者的医疗情况的用例。数据来自历史患者数据库、药物数据库和患者记录。一旦数据被收集,它就被处理,患者的实时统计数据和关键参数被绘制到相同的汇总数据上。这些数据可以用来进一步生成报告和警报,以帮助医护人员。

其他应用

还有各种其他应用,实时计算的力量可以优化或帮助人们做出明智的决定。它已成为以下行业的重要工具和辅助:

  • 制造业:实时的缺陷检测机制可以帮助优化生产成本。通常,在制造业领域,质量控制是在生产后进行的,由于货物中存在类似的缺陷,整批货物就会被拒绝。

  • 交通运输行业:基于实时交通和天气数据,运输公司可以优化其贸易路线,节省时间和金钱。

  • 网络优化:基于实时网络使用警报,公司可以设计自动扩展和自动缩减系统,以适应高峰和低谷时段。

复杂分布式用例的解决方案

现在我们了解了实时解决方案可以在各个行业垂直领域发挥的作用,让我们探索并找出我们在处理大量数据时产生的各种选择。

Hadoop 解决方案

Hadoop 解决方案是解决需要处理海量数据问题的解决方案之一。它通过在集群设置中执行作业来工作。

MapReduce 是一种编程范例,我们通过使用一个处理键和值对的 mapper 函数来处理大数据集,从而生成中间输出,再次以键值对的形式。然后,减速函数对 mapper 输出进行操作,并合并与相同中间键相关联的值,并生成结果。

Hadoop 解决方案

在前面的图中,我们演示了简单的单词计数 MapReduce 作业,其中使用 MapReduce 演示了简单的单词计数作业,其中:

  • 有一个巨大的大数据存储,可以达到赫兹或皮字节。

  • 输入数据集或文件被分割成配置大小的块,并根据复制因子在 Hadoop 集群中的多个节点上复制每个块。

  • 每个 mapper 作业计算分配给它的数据块上的单词数。

  • 一旦 mapper 完成,单词(实际上是键)及其计数存储在 mapper 节点上的本地文件中。然后,减速器启动减速功能,从而生成结果。

  • Reducer 将 mapper 输出合并,生成最终结果。

大数据,正如我们所知,确实提供了一种处理和生成结果的解决方案,但这主要是一个批处理系统,在实时使用情况下几乎没有用处。

一个定制的解决方案

在这里,我们谈论的是在我们拥有可扩展框架(如 Storm)之前在社交媒体世界中使用的解决方案。问题的一个简化版本可能是,您需要实时统计每个用户的推文数量;Twitter 通过遵循图中显示的机制解决了这个问题:

一个定制的解决方案

以下是前述机制的详细信息:

  • 一个定制的解决方案创建了一个消防软管或队列,所有推文都被推送到这个队列上。

  • 一组工作节点从队列中读取数据,解析消息,并维护每个用户的推文计数。该解决方案是可扩展的,因为我们可以增加工作人员的数量来处理系统中的更多负载。但是,用于将数据随机分布在这些工作节点中的分片算法应该确保数据均匀分布给所有工作节点。

  • 这些工作人员将第一级计数合并到下一组队列中。

  • 从这些队列(在第 1 级提到的队列)中,第二级工作人员从这些队列中挑选。在这里,这些工作人员之间的数据分布既不均匀,也不随机。负载平衡或分片逻辑必须确保来自同一用户的推文始终应该发送到同一个工作人员,以获得正确的计数。例如,假设我们有不同的用户——"A、K、M、P、R 和 L",我们有两个工作人员"工作人员 A"和"工作人员 B"。来自用户"A、K 和 M"的推文总是发送到"工作人员 A",而来自"P、R 和 L 用户"的推文发送到"工作人员 B";因此"A、K 和 M"的推文计数始终由"工作人员 A"维护。最后,这些计数被转储到数据存储中。

在前面的点中描述的队列工作解决方案对我们的特定用例效果很好,但它有以下严重的限制:

  • 这是非常复杂的,具体到使用情况

  • 重新部署和重新配置是一项巨大的任务

  • 扩展非常繁琐

  • 系统不具备容错性

有许可的专有解决方案

在开源 Hadoop 和自定义队列工作解决方案之后,让我们讨论市场上的有许可选项的专有解决方案,以满足分布式实时处理的需求。

大公司的阿拉巴马州职业治疗协会ALOTA)已经投资于这类产品,因为他们清楚地看到计算的未来发展方向。他们可以预见到这类解决方案的需求,并在几乎每个垂直领域支持它们。他们已经开发了这样的解决方案和产品,让我们进行复杂的批处理和实时计算,但这需要付出沉重的许可成本。一些公司的解决方案包括:

  • IBM:IBM 开发了 InfoSphere Streams,用于实时摄入、分析和数据相关性。

  • Oracle:Oracle 有一个名为实时决策RTD)的产品,提供实时环境下的分析、机器学习和预测

  • GigaSpaces:GigaSpaces 推出了一个名为XAP的产品,提供内存计算以提供实时结果

其他实时处理工具

还有一些其他技术具有一些类似的特征和功能,如雅虎的 Apache Storm 和 S4,但它缺乏保证处理。Spark 本质上是一个批处理系统,具有一些微批处理的功能,可以用作实时处理。

Storm 各个组件的高级视图

在本节中,我们将让您了解 Storm 的各个组件,它们的作用以及它们在 Storm 集群中的分布。

Storm 集群有三组节点(可以共同定位,但通常分布在集群中),分别是:

  • Nimbus

  • Zookeeper

  • 监督者

以下图显示了这些节点的集成层次结构:

Storm 各个组件的高级视图

集成层次结构的详细解释如下:

  • Nimbus 节点(类似于 Hadoop-JobTracker 的主节点):这是 Storm 集群的核心。你可以说这是负责以下工作的主要守护进程:

  • 上传和分发各种任务到集群中

  • 上传和分发拓扑 JAR 作业到各个监督者

  • 根据分配给监督者节点的端口启动工作人员

  • 监视拓扑执行并在必要时重新分配工作人员

  • Storm UI 也在同一节点上执行

  • Zookeeper 节点:Zookeeper 可以被指定为 Storm 集群中的簿记员。一旦拓扑作业从 Nimbus 节点提交并分发,即使 Nimbus 死亡,拓扑也会继续执行,因为只要 Zookeeper 还活着,可工作状态就会被它们维护和记录。这个组件的主要责任是维护集群的运行状态,并在需要从某些故障中恢复时恢复运行状态。它是 Storm 集群的协调者。

  • 监督者节点:这些是 Storm 拓扑中的主要处理室;所有操作都在这里进行。这些是守护进程,通过 Zookeeper 与 Nimbus 通信,并根据 Nimbus 的信号启动和停止工作进程。

深入了解 Storm 的内部

现在我们知道了 Storm 集群中存在哪些物理组件,让我们了解拓扑提交时各种 Storm 组件内部发生了什么。当我们说拓扑提交时,意味着我们已经向 Storm Nimbus 提交了一个分布式作业,以在监督者集群上执行。在本节中,我们将解释 Storm 拓扑在各种 Storm 组件中执行时所执行的各种步骤:

  • 拓扑被提交到 Nimbus 节点。

  • Nimbus 在所有监督者上上传代码 jar,并指示监督者根据 Storm 中定义的NumWorker配置或TOPOLOGY_WORKERS配置启动工作进程。

  • 在同一时间段内,所有 Storm 节点(Nimbus 和监督者)不断与 Zookeeper 集群协调,以维护工作进程及其活动的日志。

根据以下图,我们已经描述了拓扑结构和拓扑组件的分布,这在所有集群中都是相同的:

深入了解 Storm 的内部

在我们的情况下,假设我们的集群由一个 Nimbus 节点、一个 Zookeeper 集群中的三个 Zookeeper 和一个监督者节点组成。

默认情况下,每个监督者分配了四个插槽,因此每个 Storm 监督者节点将启动四个工作进程,除非进行了配置调整。

假设所描述的拓扑分配了四个工作进程,并且每个工作进程都有两个并行度的螺栓和一个并行度为四的喷口。因此,总共有八个任务要分配到四个工作进程中。

因此,拓扑将被执行为:每个监督者上有两个工作进程,每个工作进程内有两个执行器,如下图所示:

深入了解 Storm 的内部

测验时间

Q.1 尝试围绕以下领域的实时分析提出一个问题陈述:

  • 网络优化

  • 流量管理

  • 远程感知

总结

在本章中,您已经通过探索不同垂直领域和领域中的各种用例,了解了分布式计算的需求。我们还向您介绍了处理这些问题的各种解决方案,以及为什么 Storm 是开源世界中的最佳选择。您还了解了 Storm 组件以及这些组件在工作时的内部操作。

在下一章中,我们将介绍设置方面,并通过简单的拓扑使您熟悉 Storm 中的编程结构。

第二章:开始您的第一个拓扑

本章致力于指导您完成为执行 Storm 拓扑设置环境的步骤。目的是准备用户沙盒,并引导您执行一些示例代码,并了解各个组件的工作原理。所有概念都将附有代码片段和“自己动手试一试”部分,以便您能够以实际方式理解组件,并准备好探索和利用这一美妙技术的力量。

本章将涵盖的主题如下:

  • Storm 拓扑和组件

  • 执行示例 Storm 拓扑

  • 在分布式模式下执行拓扑

在本章结束时,您将能够理解拓扑中的组件和数据流,理解简单的单词计数拓扑,并在本地和分布式模式下执行它。您还将能够调整启动器项目拓扑,以添加自己的风格。

设置 Storm 的先决条件

列出了执行设置和执行步骤的先决条件:

  • 对于本地模式设置,您需要 Maven、Git、Eclipse 和 Java

  • 对于分布式设置,您需要以下内容:

  • Linux 或 Ubuntu 设置或分布式设置可以在 Windows 系统上使用 PowerShell 或 Cygwin

  • 使用 VMware player 的多个系统或虚拟机会有所帮助

您可以参考以下链接,并按照书中所述的过程设置所需的各种开源组件,以设置 Storm 并部署本书段中解释的组件:

Storm 拓扑的组件

Storm 拓扑由两个基本组件组成:一个喷口和一个或多个螺栓。这些构件使用流连接在一起;正是通过这些流,无尽的元组流动。

让我们用一个简单的类比来讨论拓扑,如图所示,并在此后进行解释:

Storm 拓扑的组件

在我们的示例拓扑中,我们有一个用于烤薯片的大型处理单元,其中输入的生土豆由喷口消耗,还有各种螺栓,如去皮螺栓、切片螺栓和烘烤螺栓,执行其名称所示的任务。有各种装配线或工人将薯片从去皮单元移动到切片机等等;在我们的情况下,我们有流来连接和连接喷口和螺栓。现在,去皮机和切片机之间的交换基本单元是去皮的土豆,切片机和烘烤机之间的交换基本单元是切片的土豆。这类似于元组,是喷口和螺栓之间信息交换的数据。

让我们更仔细地看看 Storm 拓扑的构件。

注意

Storm 中数据交换的基本单元称为元组;有时也称为事件

喷口

喷口是拓扑的收集漏斗;它将事件或元组馈送到拓扑中。它可以被视为 Storm 处理单元——拓扑的输入源。

spout 从外部源(如队列、文件、端口等)读取消息。同时,spout 将它们发射到流中,然后将它们传递给螺栓。Storm spout 的任务是跟踪每个事件或元组在其处理过程中通过有向无环图DAG)的整个过程。然后,Storm 框架根据拓扑中元组的执行结果发送和生成确认或失败通知。这种机制为 Storm 提供了保证处理的特性。根据所需的功能,spouts 可以被编程或配置为可靠或不可靠。可靠的 spout 将失败的事件重新播放到拓扑中。

下面的图表以图形方式描述了相同的流程:

Spouts

所有的 Storm spouts 都被实现为能够在一个或多个流螺栓上发射元组。就像前面的图表中,一个 spout 可以发射元组到螺栓AC

每个 spout 都应该实现IRichSpout接口。以下是与 spout 相关的重要方法:

  • nextTuple(): 这是一个不断轮询外部源以获取新事件的方法;例如,前面示例中的队列。在每次轮询时,如果方法发现一个事件,它会通过流发射到拓扑结构中,如果没有新事件,方法会简单地返回。

  • ack(): 当 spout 发射的元组被拓扑成功处理时调用这个方法。

  • fail(): 当 spout 发射的元组在指定的超时内没有成功处理时,调用这个方法。在这种情况下,对于可靠的 spouts,spout 使用messageIds事件跟踪和追踪每个元组,然后重新发射到拓扑中进行重新处理。例如,在前面的图表中,失败的元组被再次发射。

对于不可靠的 spouts,元组不使用messageIds进行跟踪,而ack()fail()等方法对于 spout 没有任何价值,因为 spout 不跟踪成功处理的元组。这些拓扑被标识为不可靠的。

注意

IRichSpout 是 Storm 提供的一个接口,提供了拓扑 spout 需要实现的合同或方法的详细信息。

螺栓

螺栓是拓扑的处理单元。它们是拓扑的组件,执行以下一个或多个任务:

  • 解析

  • 转换

  • 聚合

  • 连接

  • 数据库交互

拓扑执行的整个过程通常被分解为更小的任务和子任务,最好由不同的螺栓执行,以利用 Storm 的并行分布式处理的能力。

让我们看一下下面的图表,捕捉一个实时用例,其中来自各种飞机的位置坐标被跟踪和处理,以确定它们是否在正确的轨迹上移动:

Bolts

在这里,飞行位置坐标由飞机上的传感器发送,这些传感器被整理到日志服务器并输入到 Storm 拓扑中。Storm 拓扑被分解成以下螺栓,可以对 spout 发射的元组进行操作:

  • 解析事件螺栓:这个螺栓过滤和转换 spout 发射的事件。它将信息转换为可解密的格式。

  • 位置螺栓:这是从解析螺栓接收的元组中提取位置坐标然后将它们发送到下一个螺栓的螺栓。

  • 验证螺栓:这是验证飞机预定义轨迹与位置螺栓发送的位置坐标是否一致的螺栓,如果检测到偏差,它会向警报螺栓发送一个元组。

  • 警报螺栓:这个螺栓是通知外部系统(例如我们的情况下的空中交通管制)有关飞行路径中检测到的异常或偏差的行为者。

由于实时使用案例的性质,比如前面图中所示的案例,计算的速度和准确性至关重要,这也是使 Storm 成为实现这类解决方案的强大技术选择的原因。

总体处理逻辑被分解为在 bolt 中执行的较小任务;在 bolt 中配置任务和并行性让工程师们获得解决方案的正确性能。

一个 bolt 可以监听多个流,也可以在不同的流上向多个其他 bolt 发射。如Sprouts部分的图所示:

  • Bolt-A 向 Bolt-B 和 Bolt-C 发射

  • Bolt-D 订阅来自 Bolt-C 和 Bolt-B 的流

Storm 提供的用户定义的 bolt 要实现的常见接口如下:

  • IRichBolt

  • IBasicBolt

这两个接口的区别取决于是否需要可靠的消息传递和事务支持。

bolt 使用的主要方法如下:

  • prepare(): 这是在 bolt 初始化时调用的方法。基本上,Storm 拓扑会一直运行,一旦初始化,bolt 就不会在拓扑被终止之前终止。这个方法通常用于初始化连接和读取其他在整个 bolt 生命周期中需要的静态信息。

  • execute(): 这是在 bolt 上执行定义的功能和处理逻辑的方法。它为每个元组执行一次。

流可以被定义为无界的元组或事件序列。这些流通常以并行和分布的方式在拓扑中创建。流可以被称为从喷口到 bolt 之间的布线或信息流通道。它们是未处理、半处理和已处理信息的载体,用于各种执行任务的组件,如 bolt 和喷口之间的信息传递。在对拓扑进行编码时,流是使用模式配置的,该模式为流的元组命名字段。

元组-Storm 中的数据模型

元组是 Storm 中的基本和组成数据结构。它是从喷口开始旅程的命名值列表。然后从流到 bolt 发射,然后从 bolt 到其他 bolt,执行各种处理阶段。在成功完成所有预期的处理后,根据拓扑定义,元组被确认发送回喷口。

执行一个样本 Storm 拓扑-本地模式

在我们开始本节之前,假设您已经完成了先决条件并安装了预期的组件。

来自 Storm-starter 项目的 WordCount 拓扑结构

为了理解前一节中描述的组件,让我们下载 Storm-starter 项目并执行一个样本拓扑:

  1. 可以使用以下 Git 命令下载 Storm-starter 项目:
Linux-command-Prompt $ sudo git clone git://github.com/apache/incubator-storm.git && cd incubator-storm/examples/storm-starter

  1. 接下来,您需要将项目导入到 Eclipse 工作区中:

  2. 启动 Eclipse。

  3. 单击文件菜单,然后选择导入向导。

  4. 导入向导中,选择现有 Maven 项目来自 Storm-starter 项目的 WordCount 拓扑结构

  5. 在 Storm-starter 项目中选择pom.xml,并将其指定为<download-folder>/starter/incubator-storm/examples/storm-starter

  6. 一旦项目成功导入,Eclipse 文件夹结构将如下屏幕截图所示:来自 Storm-starter 项目的 WordCount 拓扑结构

  7. 使用 run 命令执行拓扑,您应该能够看到如下屏幕截图中显示的输出:

来自 Storm-starter 项目的 WordCount 拓扑结构

为了理解拓扑的功能,让我们看一下代码,并了解拓扑中每个组件的流程和功能:

// instantiates the new builder object
TopologyBuilder builder = new TopologyBuilder();
// Adds a new spout of type "RandomSentenceSpout" with a  parallelism hint of 5
builder.setSpout("spout", new RandomSentenceSpout(), 5);
TopologyBuilder object and used the template to perform the following:
  • setSpout –RandomSentenceSpout:这会生成随机句子。请注意,我们使用了一个称为并行性提示的属性,在这里设置为5。这是标识在提交拓扑时将生成多少个此组件实例的属性。在我们的示例中,将有五个 spout 实例。

  • setBolt:我们使用这个方法向拓扑中添加两个 bolt:SplitSentenceBolt,将句子拆分为单词,和WordCountBolt,对单词进行计数。

  • 在前面的代码片段中,其他值得注意的项目是suffleGroupingfieldsGrouping;我们将在下一章详细讨论这些;现在,了解这些是控制元组路由到拓扑中各个 bolt 的组件。

在分布式模式下执行拓扑

要在分布式模式下设置 Storm,需要执行以下步骤。

为 Storm 设置 Zookeeper(V 3.3.5)

Storm 拓扑的协调由 Zookeeper 集群维护。Zookeeper 的利用率并不是很高,因为它只是维护 Storm 集群的可运行状态。在大多数情况下,单个 Zookeeper 节点应该足够了,但在生产场景中,建议至少使用一个由三个节点组成的 Zookeeper 集群,以防止单个节点成为单点故障。

为了可靠的 Zookeeper 服务,将 Zookeeper 部署在一个称为集合的集群中。只要集合中的大多数机器正常运行,服务就会可用。集合中的一个节点会自动被选为领导者,其他节点会成为跟随者。如果领导者宕机,其中一个跟随者节点会成为领导者。

在所有将成为 Zookeeper 集合一部分的机器上执行以下步骤,以设置 Zookeeper 集群:

  1. 从 Apache Zookeeper 网站下载最新的稳定版本(版本 3.3.5)。

  2. /usr/local下创建一个zookeeper目录:

sudo mkdir /usr/local/zookeeper

  1. 将下载的 TAR 文件提取到/usr/local位置。使用以下命令:
sudo tar -xvf zookeeper-3.3.5.tar.gz -C /usr/local/zookeeper

  1. Zookeeper 需要一个目录来存储其数据。创建/usr/local/zookeeper/tmp来存储这些数据:
sudo mkdir –p /usr/local/zookeeper/tmp

  1. /usr/local/zookeeper/zookeeper-3.3.5/conf下创建一个名为zoo.cfg的配置文件。以下属性将放入其中:
  • tickTime:这是每个滴答的毫秒数(例如,2000)。

  • initLimit:这是初始同步阶段可以花费的滴答数(例如,5)。

  • syncLimit:这是在发送请求和获得确认之间可以经过的滴答数(例如,2)。

  • dataDir:这是快照存储的目录(例如,/usr/local/zookeeper/tmp)。

  • clientPort:这是 Zookeeper 客户端将连接到的端口(例如,2182)。

  • server.id=host:port:port:Zookeeper 集合中的每台机器都应该知道集合中的其他每台机器。这是通过server.id=host:port:port形式的一系列行来实现的(例如,server.1:<ZOOKEEPER_NODE_1 的 IP 地址>:2888:3888)。

  1. 重复前面的步骤或将分发复制到将成为 Zookeeper 集群一部分的其他机器上。

  2. 在由datadir属性指定的目录中创建名为myid的文件。myid文件包含一行文本,只包含该机器的 ID 的文本(服务器上的 1 和zoo.cfg中的 1)。因此,服务器 1 的myid将包含文本1,没有其他内容。ID 必须在集合中是唯一的,并且应该在 1 到 255 之间。在这种情况下,myid文件的路径是vi /usr/local/zookeeper/tmp/myid

  3. 编辑~/.bashrc文件,并添加一个 Zookeeper 主目录的环境变量,并将其 bin 目录添加到PATH环境变量中:为 Storm 设置 Zookeeper(V 3.3.5)

  4. 在进行更改后,对~/.bashrc文件进行源操作。这一步是为了确保对bashrc所做的更改应用到当前的终端会话中:

source ~/.bashrc

  1. 通过从$ZOOKEEPER_HOME执行以下命令在每个节点上启动 Zookeeper 守护进程:
sudo –E bin/zkServer.sh start

  1. 通过从$ZOOKEEPER_HOME执行以下命令在每个节点上停止 Zookeeper 守护进程:
sudo –E bin/zkServer.sh stop

  1. 可以通过从$ZOOKEEPER_HOME运行以下命令来检查 Zookeeper 状态:
sudo –E bin/zkServer.sh status

不同模式的输出如下:

  • 如果在独立模式下运行(Zookeeper 集群中只有一台机器),将在控制台上看到以下输出:为 Storm 设置 Zookeeper(V 3.3.5)

  • 如果在集群模式下运行,将在领导节点上看到以下输出:为 Storm 设置 Zookeeper(V 3.3.5)

  • 如果在集群模式下运行,将在 follower 节点上看到以下输出:为 Storm 设置 Zookeeper(V 3.3.5)

默认情况下,Zookeeper 日志(zookeeper.out)将在启动其实例的相同位置创建。这完成了 Zookeeper 集群的设置。

在分布式模式下设置 Storm

执行以下步骤设置分布式模式下的 Storm:

  1. 从 GitHub Storm 网站下载Storm-0.9.2-incubating.zip包。

  2. /usr/local下创建stormstorm/tmp目录:

sudo mkdir –p /usr/local/storm/tmp

  1. 为日志创建以下目录:
sudo mkdir –p /mnt/abc_logs/storm/storm_logs

  1. 在 Nimbus 和工作机器上的/usr/local目录中解压 ZIP 文件:
sudo unzip -d /usr/local/storm/ storm-0.9.2 -incubating.zip

  1. /usr/local/storm/storm-0.9.2-incubating/conf/storm.yaml中进行以下更改:
  • storm.zookeeper.servers:这是 Storm 集群中 Zookeeper 集群中主机的列表:
storm.zookeeper.servers:
 "<IP_ADDRESS_OF_ZOOKEEPER_ENSEMBLE_NODE_1>"
 "<IP_ADDRESS_OF_ZOOKEEPER_ENSEMBLE_NODE_2>"
  • storm.zookeeper.port:这是 Zookeeper 集群运行的端口:
storm.zookeeper.port: 2182
  • storm.local.dir:Nimbus 和 Supervisor 需要本地磁盘上的位置来存储与拓扑的配置和执行细节相关的少量数据。请确保在所有 Storm 节点上创建该目录并分配读/写权限。对于我们的安装,我们将在/usr/local/storm/tmp位置创建此目录:
storm.local.dir: "/usr/local/storm/tmp"
  • nimbus.host:节点需要知道哪台机器是主节点,以便下载拓扑 jar 包和配置文件。此属性用于此目的:
nimbus.host: "<IP_ADDRESS_OF_NIMBUS_HOST>"
  • java.library.path:这是 Storm 使用的本地库(ZeroMQ 和 JZMQ)的加载路径。对于大多数安装来说,默认值/usr/local/lib:/opt/local/lib:/usr/lib应该是可以的,所以在继续之前验证前面提到的位置中的库。

  • storm.messaging.netty:Storm 的基于 Netty 的传输已经进行了大幅改进,通过更好地利用线程、CPU 和网络资源,特别是在消息大小较小的情况下,显着提高了性能。为了提供 Netty 支持,需要添加以下配置:

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 集群安装中的storm.yaml片段如下:
#To be filled in for a storm configuration
storm.zookeeper.servers:
     - "nim-zkp-flm-3.abc.net"
storm.zookeeper.port: 2182
storm.local.dir: "/usr/local/storm/tmp"
nimbus.host: "nim-zkp-flm-3.abc.net"
topology.message.timeout.secs: 60
topology.debug: false
topology.optimize: true
topology.ackers: 4

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
  1. ~/.bashrc文件中设置STORM_HOME环境,并将 Storm 的bin目录添加到PATH环境变量中。这样可以在任何位置执行 Storm 二进制文件。

  2. 使用以下命令将Storm.yaml文件复制到 Nimbus 机器上 Storm 安装的bin文件夹中:

sudo cp /usr/local/storm/storm-0.9.2- incubating/conf/storm.yaml /usr/local/storm/storm-0.8.2/bin/

启动 Storm 守护进程

现在 Storm 集群已经设置好,我们需要在各自的 Storm 节点上启动三个进程。它们如下:

  • Nimbus: 通过从$STORM_HOME运行以下命令在被识别为主节点的机器上作为后台进程启动 Nimbus:
sudo –bE bin/storm nimbus

  • Supervisor: 可以像启动 Nimbus 一样启动 Supervisors。从$STORM_HOME运行以下命令:
sudo –bE bin/storm supervisor

  • UI: Storm UI 是一个 Web 应用程序,用于检查 Storm 集群,其中包含 Nimbus/Supervisor 状态。它还列出了所有运行中的拓扑及其详细信息。可以通过以下命令从$STORM_HOME启用 UI:
sudo –bE bin/storm ui

可以通过http://<IP_ADDRESS_OF_NIMBUS>:8080访问 UI。

从命令提示符执行拓扑

一旦 UI 可见并且所有守护程序都已启动,就可以使用以下命令在 Nimbus 上提交拓扑:

storm jar storm-starter-0.0.1-SNAPSHOT-jar-with-dependencies.jar  storm.starter.WordCountTopology WordCount -c nimbus.host=localhost

在这里显示了以分布式模式运行的带有WordCount拓扑的 Storm UI。它显示了拓扑状态、正常运行时间和其他详细信息(我们将在后面的章节中详细讨论 UI 的特性)。我们可以从 UI 中终止拓扑。

从命令提示符执行拓扑

调整 WordCount 拓扑以自定义它

现在我们已经以分布式模式部署了WordCount拓扑,让我们稍微调整螺栓中的代码,以将WordCount写入文件。为了实现这一点,我们将按照以下步骤进行:

  1. 我们打算创建一个新的螺栓FileWriterBolt,以实现这一目标。打开WordCountTopology.java并将以下片段添加到WordCountTopology.java中:
public static class FileWriterBolt extends BaseBasicBolt {
    Map<String, Integer> counts = new HashMap<String,  Integer>();
    @Override
    public void execute(Tuple tuple, BasicOutputCollector  collector) {
        String word = tuple.getString(0);
        Integer count = counts.get(word);
        if(count==null) {count = 0;
        count = 0;
    }
        count++;
        counts.put(word, count);
        OutputStream ostream;
        try {
            ostream = new  FileOutputStream("~/wordCount.txt", true);
            ostream.write(word.getBytes());
            ostream.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        collector.emit(new Values(word, count));
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer  declarer) {
        declarer.declare(new Fields("word", "count"));
    }
  1. 接下来,我们必须更改main()方法,以使用这个新的螺栓,而不是WordCount Bolt();以下是片段:
// instantiates the new builder object 
TopologyBuilder builder = new TopologyBuilder();
// Adds a new spout of type "RandomSentenceSpout" with a  parallelism hint of 5 
builder.setSpout("spout", new RandomSentenceSpout(), 5);
//Adds a new bolt to the  topology of type "SplitSentence"  with parallelism of 8
builder.setBolt("split", new SplitSentence(),  8).shuffleGrouping("spout");
//Adds a new bolt to the  topology of type "SplitSentence"  with parallelism of 8
//builder.setBolt("count", new FileWriterBolt()(),  12).fieldsGrouping("split", new Fields("word"));
  1. 接下来,您可以使用 Eclipse 执行拓扑,将其作为 Java 运行,输出将保存到名为wordCount.txt的文件中,保存在您的主目录中。

  2. 要以分布式模式运行,请使用以下步骤:

  3. 编译拓扑更改以生成新的 Storm-starter 项目,使用以下命令行:

mvn clean install

  1. 从 starter 项目的目标文件夹中复制storm-starter-0.0.1-SNAPSHOT-jar-with-dependencies.jar到 Nimbus,比如在/home/admin/topology/

  2. 使用以下命令提交拓扑:

storm jar /home/admin/topology/storm-starter-0.0.1-SNAPSHOT- jar-with-dependencies.jar storm.starter.WordCountTopology  WordCount -c nimbus.host=localhost

  1. 输出将与前一节中图中执行的WordCount拓扑相同。

测验时间

Q.1. 判断以下陈述是真还是假:

  1. 所有 Storm 拓扑都是可靠的。

  2. 一个拓扑通常有多个喷口。

  3. 一个拓扑通常有多个螺栓。

  4. 一个螺栓只能在一个流上发射。

Q.2. 填空:

  1. _______________ 是创建拓扑的模板。

  2. _______________ 指定了特定螺栓或喷嘴的实例数量。

  3. Storm 的 _______________ 守护程序类似于 Hadoop 的作业跟踪器。

Q.3. 执行以下任务:

  1. 对 Storm-starter 项目的WordCount拓扑进行更改,以便它能够从指定位置的文件中读取句子。

摘要

在本章中,我们已经设置了 Storm 集群。您已经了解了 Storm 拓扑的各种构建模块,如螺栓、喷口和布线模板-拓扑构建器。我们执行并了解了WordCount拓扑,并对其进行了一些修正。

在下一章中,您将阅读并了解有关流分组、锚定和确认的内容。这也将引导我们了解 Storm 框架下拓扑中的可靠和非可靠机制。

第三章:通过示例了解 Storm 内部

本书的这一章节致力于让您了解 Storm 的内部工作原理,并通过实际示例来说明它的工作方式。目的是让您习惯于编写自己的喷口,了解可靠和不可靠的拓扑,并熟悉 Storm 提供的各种分组。

本章将涵盖以下主题:

  • Storm 喷口和自定义喷口

  • 锚定和确认

  • 不同的流分组

在本章结束时,您应该能够通过使用锚定来理解各种分组和可靠性的概念,并能够创建自己的喷口。

自定义 Storm 喷口

您已经在之前的章节中探索和了解了 Storm-starter 项目提供的WordCount拓扑。现在是时候我们继续下一步,使用 Storm 进行自己的实践;让我们迈出下一步,用我们自己的喷口从各种来源读取。

创建 FileSpout

在这里,我们将创建自己的喷口,从文件源读取事件或元组并将它们发射到拓扑中;我们将在上一章的WordCount拓扑中使用RandomSentenceSpout的位置替换为喷口。

首先,将我们在第二章中创建的项目复制到一个新项目中,并对RandomSentenceSpout进行以下更改,以在 Storm-starter 项目中创建一个名为FileSpout的新类。

现在我们将更改FileSpout,使其从文件中读取句子,如下面的代码所示:

public class FileSpout extends BaseRichSpout {
  //declaration section
  SpoutOutputCollector _collector;
  DataInputStream in ;
  BufferedReader br;
  Queue qe;

  //constructor
    public FileSpout() {
        qe = new LinkedList();
    }

  // the messageId builder method
  private String getMsgId(int i) {
    return (new StringBuilder("#@#MsgId")).append(i).toString();
    }

  //The function that is called at every line being read by  readFile
  //method and adds messageId at the end of each line and then add
  // the line to the linked list
    private void queueIt() {
      int msgId = 0;
      String strLine;
      try {
          while ((strLine = br.readLine()) != null) {
              qe.add((new  StringBuilder(String.valueOf(strLine))).append("#@#"  + getMsgId(msgId)).toString());
              msgId++;
          }
      } catch (IOException e) {
          e.printStackTrace();
      } catch (Exception e) {
          e.printStackTrace();
      }
    }

  //function to read line from file at specified location 
  private void readFile() {
        try {
          FileInputStream fstream = new  FileInputStream("/home/mylog"); in =  new DataInputStream(fstream);
          br = new BufferedReader(new InputStreamReader( in ));
          queueIt();
          System.out.println("FileSpout file reading done");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

  //open function that is called at the time of spout  initialization
  // it calls the readFile method that reads the file , adds  events 
  // to the linked list to be fed to the spout as tuples
  @
    Override
    public void open(Map conf, TopologyContext context,  SpoutOutputCollector  collector) {
      _collector = collector;
      readFile();
    }

  //this method is called every 100 ms and it polls the list
  //for message which is read off as next tuple and emit the spout  to
  //the topology. When queue doesn't have any events, it reads the
  //file again calling the readFile method
    @
    Override
    public void nextTuple() {
      Utils.sleep(100);
      String fullMsg = (String) qe.poll();
      String msg[] = (String[]) null;
      if (fullMsg != null) {
          msg = (new String(fullMsg)).split("#@#");
          _collector.emit(new Values(msg[0]));
          System.out.println((new StringBuilder("nextTuple done  ")).append(msg[1]).toString());
      } else {
          readFile();
      }
    }

  @
  Override
  public void ack(Object id) {}

  @
  Override
  public void fail(Object id) {}

  @
  Override
  public void declareOutputFields(OutputFieldsDeclarer declarer) {
      declarer.declare(new Fields("word"));
  }
}

提示

下载示例代码

您可以从www.packtpub.com的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便直接将文件发送到您的电子邮件。

调整 WordCount 拓扑以使用 FileSpout

现在我们需要将FileSpout适应到我们的WordCount拓扑中并执行它。为此,您需要在WordCount拓扑中更改一行代码,并在TopologyBuilder中实例化FileSpout而不是RandomSentenceSpout,如下所示:

public static void main(String[] args) throws Exception {
  TopologyBuilder builder = new TopologyBuilder();
//builder.setSpout("spout", new RandomSentenceSpout(), 5);
  builder.setSpout("spout", new FileSpout(), 1);

这一行更改将处理从指定文件/home/mylog中读取的新喷口的实例化(请在执行程序之前创建此文件)。以下是您参考的输出的屏幕截图:

调整 WordCount 拓扑以使用 FileSpout

SocketSpout 类

为了更好地理解喷口,让我们创建一个SocketSpout类。假设您擅长编写 Socket 服务器或生产者,我将带您了解创建自定义SocketSpout类以在 Storm 拓扑中消耗套接字输出的过程:

public class SocketSpout extends BaseRichSpout{
  static SpoutOutputCollector collector;
  //The socket
    static Socket myclientSocket;
    static ServerSocket myserverSocket;
    static int myport;

  public SocketSpout(int port){
    myport=port;
  }

  public void open(Map conf,TopologyContext context,  SpoutOutputCollector collector){
    _collector=collector;
    myserverSocket=new ServerSocket(myport);
  }

  public void nextTuple(){
    myclientSocket=myserverSocket.accept();
    InputStream incomingIS=myclientSocket.getInputStream();
    byte[] b=new byte[8196];
    int len=b.incomingIS.read(b);
    _collector.emit(new Values(b));
  }
}

锚定和确认

我们已经谈到了为执行 Storm 拓扑创建的 DAG。现在,当您设计拓扑以满足可靠性时,有两个需要添加到 Storm 的项目:

  • 每当 DAG 添加新的链接,即新的流时,它被称为锚定

  • 当元组完全处理时,称为确认

当 Storm 知道这些先前的事实时,它可以在元组处理过程中对它们进行评估,并根据它们是否完全处理而失败或确认元组。

让我们看一下以下WordCount拓扑螺栓,以更好地理解 Storm API 的锚定和确认:

  • SplitSentenceBolt:这个螺栓的目的是将句子分割成不同的单词并发射它。现在让我们详细检查这个螺栓的输出声明者和执行方法(特别是高亮显示的部分),如下面的代码所示:
  public void execute(Tuple tuple) {
      String sentence = tuple.getString(0);
      for(String word: sentence.split(" ")) {
          _collector.emit(tuple, new Values(word)); //1
      }
      _collector.ack(tuple); //2
  }
  public void declareOutputFields(OutputFieldsDeclarer  declarer) {
      declarer.declare(new Fields("word")); //3
  }
}

上述代码的输出声明功能如下所述:

  • _collector.emit: 这里,由 bolt 在名为word的流上发射的每个元组(第二个参数)都使用方法的第一个参数(元组)进行了定位。在这种安排下,如果发生故障,树的根部定位的元组将由 spout 重新播放。

  • collector.ack: 这里我们通知 Storm 该元组已被这个 bolt 成功处理。在发生故障时,程序员可以显式调用fail方法,或者 Storm 在超时事件的情况下会内部调用它,以便可以重放。

  • declarer.declare: 这是用来指定成功处理的元组将被发射的流的方法。请注意,我们在_collector.emit方法中使用了相同的word流。同样,如果你查看WordCount拓扑的Builder方法,你会发现另一个关于word流整体集成的部分,如下所示:

  builder.setBolt("count", new WordCount(), 12).fieldsGrouping("split", new Fields("word"));

不可靠的拓扑

现在让我们看看相同拓扑的不可靠版本。在这里,如果元组未能被 Storm 完全处理,框架不会重放。我们之前在这个拓扑中使用的代码会像这样:

java _collector.emit(new Values(word));

因此,未定位的元组由 bolt 发射。有时,由于编程需要处理各种问题,开发人员会故意创建不可靠的拓扑。

流分组

接下来,我们需要熟悉 Storm 提供的各种流分组(流分组基本上是定义 Storm 如何在 bolt 任务之间分区和分发元组流的机制),这为开发人员处理程序中的各种问题提供了很大的灵活性。

本地或 shuffle 分组

WordCount topology (which we reated earlier), which demonstrates the usage of shuffle grouping:
TopologyBuilder myBuilder = new TopologyBuilder();
builder.setSpout("spout", new RandomSentenceSpout(), 5);
builder.setBolt("split", new SplitSentence(),  8).shuffleGrouping("spout");
builder.setBolt("count", new WordCount(),  12).fieldsGrouping("split", new Fields("word"));

在下图中,显示了 shuffle 分组:

本地或 shuffle 分组

在这里,Bolt ABolt B都有两个并行度,因此 Storm 框架会生成每个这些 bolt 的两个实例。这些 bolt 通过shuffle grouping连接在一起。我们现在将讨论事件的分发。

来自Bolt AInstance 1的 50%事件将发送到Bolt BInstance 1,剩下的 50%将发送到Bolt BInstance 2。同样,Bolt BInstance 2发射的 50%事件将发送到Bolt BInstance 1,剩下的 50%将发送到Bolt BInstance 2

字段分组

在这种分组中,我们指定了两个参数——流的来源和字段。字段的值实际上用于控制元组路由到各种 bolt 的过程。这种分组保证了对于相同字段的值,元组将始终路由到同一个 bolt 的实例。

在下图中,Bolt ABolt B之间显示了字段分组,并且每个 bolt 都有两个实例。根据字段分组参数的值,注意事件的流动。

字段分组

来自Bolt AInstance 1Instance 2的所有事件,其中Field的值为P,都发送到Bolt BInstance 1

来自Bolt AInstance 1Instance 2的所有事件,其中Field的值为Q,都发送到Bolt BInstance 2

所有分组

所有分组是一种广播分组,可用于需要将相同消息发送到目标 bolt 的所有实例的情况。在这里,每个元组都发送到所有 bolt 的实例。

这种分组应该在非常特定的情况下使用,针对特定的流,我们希望相同的信息被复制到所有下游的 bolt 实例中。让我们来看一个使用情况,其中包含与国家及其货币价值相关的一些信息,而后续的 bolts 需要这些信息进行货币转换。现在每当currency bolt 有任何更改时,它使用all分组将其发布到所有后续 bolts 的实例中:

所有分组

这里我们有一个图解表示所有分组,其中来自Bolt A的所有元组都被发送到Bolt B的所有实例。

全局分组

全局分组确保来自源组件(spout 或 bolt)的整个流都发送到目标 bolt 的单个实例,更准确地说是发送到具有最低 ID 的目标 bolt 实例。让我们通过一个例子来理解这个概念,假设我的拓扑如下:

全局分组

我将为组件分配以下并行性:

全局分组

另外,我将使用以下流分组:

全局分组

然后,框架将所有来自myboltA流实例的数据,都发送到myboltB流的一个实例,这个实例是 Storm 在实例化时分配了较低 ID 的实例:

全局分组

如前图所示,在全局分组的情况下,来自Bolt A的两个实例的所有元组都会发送到Bolt BInstance 1,假设它的 ID 比Bolt BInstance 2的 ID 更低。

注意

Storm 基本上为拓扑中创建的每个 bolt 或 spout 实例分配 ID。在全局分组中,分配是指向从 Storm 分配的 ID 较低的实例。

自定义分组

Storm 作为一个可扩展的框架,为开发人员提供了创建自己的流分组的功能。这可以通过为backtype.storm.grouping.CustomStreamGroupinginterface类提供实现来实现。

直接分组

在这种分组中,Storm 框架提供了发送者的能力

组件(spout 或 bolt)来决定消费者 bolt 的哪个任务会接收元组,而发送组件正在向流中发射元组。

必须使用特殊的emitDirect方法将元组发送到流中,并且必须指定消费组件的任务(注意可以使用TopologyContext方法获取任务)。

测验时间

Q.1 判断以下陈述是真是假:

  1. 可靠拓扑的所有组件都使用锚定。

  2. 在发生故障时,所有元组都会被重新播放。

  3. Shuffle 分组进行负载均衡。

  4. 全局分组就像广播一样。

Q.2 填空:

  1. _______________ 是告诉框架元组已成功处理的方法。

  2. _______________ 方法指定流的名称。

  3. ___________ 方法用于将元组推送到 DAG 中的下游。

对 Storm-starter 项目的WordCount拓扑进行更改,以创建自定义分组,使得以特定字母开头的所有单词始终发送到WordCount bolt 的同一个实例。

总结

在本章中,我们已经了解了 Storm spout 的复杂性。我们还创建了一个自定义文件 spout,并将其与WordCount拓扑集成。我们还向您介绍了可靠性、确认和锚定的概念。当前版本的 Storm 提供的各种分组知识进一步增强了用户探索和实验的能力。

在下一章中,我们将让您熟悉 Storm 的集群设置,并为您提供有关集群模式的各种监控工具的见解。

第四章:集群模式下的风暴

我们现在已经到达了我们与风暴的旅程的下一步,也就是理解风暴及其相关组件的集群模式设置。我们将浏览风暴和 Zookeeper 中的各种配置,并理解它们背后的概念。

本章将涵盖的主题如下:

  • 设置风暴集群

  • 了解集群的配置及其对系统功能的影响

  • 风暴 UI 和理解 UI 参数

  • 为生产设置提供和监视应用程序

到本章结束时,您应该能够理解风暴和 Zookeeper 节点的配置。此外,您应该能够理解风暴 UI,并设置风暴集群,并使用各种工具监视它们。

风暴集群设置

在第二章中,我们设置了风暴和 Zookeeper 参考集群,如下图所示,开始使用您的第一个拓扑

我们为一个三节点风暴集群(其中有一个 Nimbus 和两个监督者)设置了三节点 Zookeeper 集群。

我们正在使用推荐的三节点 Zookeeper 集群,以避免风暴设置中的单点故障。

Zookeeper 集群应该有奇数个节点。这个要求的原因是 Zookeeper 选举逻辑要求领导者拥有奇数个选票,只有在奇数节点在法定人数中时才可能出现这种组合,如下图所示:

风暴集群设置

Zookeeper 配置

假设您已在所有三个 Zookeeper 节点上安装了 Zookeeper;现在我们将带您浏览配置,以便更好地理解它们。

在我们的情况下,zoo.cfg的摘录位于<zookeeper_installation_dir>/ zookeeper-3.4.5/conf/。Zookeeper 的配置如下:

  • dataDir=/usr/local/zookeeper/tmp:这是 Zookeeper 存储其快照的路径;这些快照实际上是状态日志,用于维护当前集群状态以进行协调。在发生故障时,这些快照用于将集群恢复到最后稳定状态。这个目录还包含一个包含单个条目myID的文件。这个值从1开始,对于每个 Zookeeper 节点都是不同的,所以我们将保持如下:
zkp-1.mydomain.net – value of myId =1
zkp-2.mydomain.net – value of myId =2
zkp-3.mydomain.net – value of myId =3

每当您想要从头开始,或者当您升级或降级风暴或 Zookeeper 集群时,建议您清理这个local.dir文件,以便清除陈旧的数据。

  • clientPort=2182:这个配置指定了客户端与 Zookeeper 建立连接的端口:
server.1=zkp-1.mydomain.net:2888:3888
server.2=zkp-2\. mydomain.net:2888:3888
server.3=zkp-3\. mydomain.net:2888:3888

在前面的代码中,这三行实际上指定了组成 Zookeeper 集群一部分的服务器的 IP 或名称。在这个配置中,我们创建了一个三节点 Zookeeper 集群。

  • maxClientCnxns=30l:这个数字指定了单个客户端可以与这个 Zookeeper 节点建立的最大连接数。在我们的情况下,计算将如下进行:

一个监督者可以建立的最大连接数是 30 个,与一个 Zookeeper 节点。因此,一个监督者可以与三个 Zookeeper 节点创建的最大连接数是 90(即 30*3)。

以下截图显示了从风暴 UI 中捕获的已使用、可用和空闲插槽:

Zookeeper 配置

注意

风暴集群中的工作人员数量与 Zookeeper 集群中可用的连接数量有关。如果 Zookeeper 集群连接不足,风暴监督者将无法启动。

清理 Zookeeper

我们已经看到 Zookeeper 如何以快照的形式将其所有协调数据存储在dataDir配置中指定的路径中。这需要定期清理或归档以删除旧的快照,以免消耗整个磁盘空间。这是一个需要在所有 Zookeeper 节点上配置的小型清理脚本:

numBackUps=3
dataDir=/usr/local/zookeeper/tmp
logDir=/mnt/my_logs/
echo `date`' Time to clean up StormZkTxn logs' >>  $logDir/cleanStormZk.out
java -cp /usr/local/zookeeper/zookeeper-3.4.5/zookeeper- 3.4.5.jar:/usr/local/zookeeper/zookeeper-3.4.5/lib/log4j- 1.2.15.jar:/usr/local/zookeeper/zookeeper-3.4.5/lib/slf4j-api- 1.6.1.jar org.apache.zookeeper.server.PurgeTxnLog $dataDir -n  $numBackUps >> $logDir/cleanStormZk.out

这里有以下清理脚本:

  • numBackUps:在这里,我们指定了清理后要保留多少个快照;最少为三个,最多可以根据需求变化。

  • dataDir:在这里,我们指定了需要清理快照的数据目录的路径。

  • logDir:这是清理脚本将存储其日志的路径。

  • org.apache.zookeeper.server.PurgeTxnLog:这是一个实用类,清除除了最后三个快照之外的所有快照,如numBackups中所述。

Storm 配置

我们将查看 Storm 守护进程和守护进程周围的配置。对于 Nimbus 节点,在storm.yaml中有以下配置设置。让我们根据以下代码中给出的配置来理解这些配置:

storm.zookeeper.servers:
- "zkp-1.mydomain.net "
- "zkp-2.mydomain.net "
- "zkp-3.mydomain.net "

storm.zookeeper.port: 2182
storm.local.dir: "/usr/local/storm/tmp"
nimbus.host: "nim-zkp-flm-3.mydomain.net"
topology.message.timeout.secs: 60
topology.debug: false

supervisor.slots.ports:
    - 6700
    - 6701
    - 6702
    - 6703

在前面的代码中使用的配置的功能如下:

  • storm.zookeeper.servers:在这里,我们指定了 Zookeeper 集群中 Zookeeper 服务器的名称或 IP 地址;请注意,我们在前一节的zoo.cfg配置中使用了与之前相同的主机名。

  • storm.zookeeper.port:在这里,我们指定了 Storm 节点连接的 Zookeeper 节点上的端口。同样,我们在前一节中在zoo.cfg中指定了相同的端口。

  • storm.local.dir:Storm 有自己的临时数据存储在本地目录中。这些数据会自动清理,但每当您想要从头开始,或者当您扩展或缩小 Storm 或 Zookeeper 集群时,建议您清理此local.dir配置,以清除陈旧数据。

  • nimbus.host:这指定了要设置为 Nimbus 的主机名或 IP 地址。

  • topology.message.timeout.secs:此值指定拓扑处理的元组在经过一定秒数后被声明为超时并丢弃的持续时间。此后,根据拓扑是可靠还是不可靠,它会被重放或不会。应谨慎设置此值;如果设置得太低,所有消息最终都会超时。如果设置得太高,可能永远不会知道拓扑中的性能瓶颈。

  • topology.debug:此参数表示是否要在调试模式或节点中运行拓扑。调试模式是指打印所有调试日志,建议在开发和暂存模式下使用,但在生产模式下不建议使用,因为此模式下 I/O 非常高,从而影响整体性能。

  • supervisor.slots.ports:此参数指定了主管工作进程的端口。这个数字直接关联到主管上可以生成的工作进程的数量。当拓扑被生成时,必须指定要分配的工作进程的数量,这又与分配给拓扑的实际资源相关联。工作进程的数量非常重要,因为它们实际上确定了集群上可以运行多少个拓扑,从而确定了可以实现多少并行性。例如,默认情况下,每个主管有四个插槽,所以在我们的集群中,我们将有总插槽数/工作进程= 42 = 8*。每个工作进程从系统中获取一定数量的 CPU 和 RAM 资源,因此在主管上生成多少个工作进程取决于系统配置。

Storm 日志配置

现在我们将查看 Storm 的日志配置。它们使用 Log4J 的logback实现,其配置可以在<storm-installation-dir>/apache-storm-0.9.2-incubating/logback中的cluster.xml中找到并进行调整,使用以下代码:

<appender name="A1"  class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${storm.log.dir}/${logfile.name}</file>
    <rollingPolicy  class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
      <fileNamePattern>${storm.log.dir}/${logfile.name}.%i</fileNamePattern >
      <minIndex>1</minIndex>
 <maxIndex>9</maxIndex>
    </rollingPolicy>

    <triggeringPolicy  class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <maxFileSize>100MB</maxFileSize>
    </triggeringPolicy>

    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss} %c{1} [%p] %m%n</pattern>
    </encoder>
 </appender>

  <root level="INFO">
    <appender-ref ref="A1"/>
  </root>

在上面的片段中,有几个部分被突出显示,我们将逐一进行更详细的讨论。它们如下:

  • <file>:这个标签保存了 Storm 框架生成的日志的日志目录路径和文件名。

  • <filenamepattern>:这是文件形成和滚动的模式;例如,使用前面的代码模式,我们有 worker 日志文件worker-6700.logworker-6700.1.log

  • <minIndex>和<maxIndex>:这些非常重要,用于指定我们想要保留多少个文件在这个滚动 appender 中;在这种情况下,我们将有九个备份文件,编号从一到九,还有一个运行日志文件。

  • maxFileSize:这个参数指定文件应该在什么大小时滚动,例如,在我们的情况下,它是 100MB;这意味着当工作日志文件达到这个大小时,它将滚动到下一个索引。

  • 根级别:这指定了日志级别;在我们的情况下,我们已将其指定为Info,这意味着Info和以上的日志将被打印到日志文件中,但是低于Info级别的日志将不会被写入日志。以下是供参考的日志级别层次结构:

  • 关闭

  • 致命

  • 错误

  • 警告

  • 信息

  • 调试

  • TRACE

  • 全部

Storm UI

word-count, is the name of that topology:

cluster.submitTopology("word-count", conf, builder.createTopology());


In our preceding sample screenshot, **AAA-topology-1407803669812** is the name of the topology.**ID**: This is the Storm-generated unique ID that is a combination of the topology name, timestamp, and ID, which is used by Storm to identify and differentiate the topology.**Status**: This denotes the state of the topology, which could be *active* for a live topology, *killed* when a topology is killed using the UI or CLI, *inactive* for a deactivated topology, and *rebalancing* for a topology where the rebalance command is executed wherein the number of workers allocated to the topology is increased or decreased.**Uptime**: As the name suggests, this mentions the duration for which the topology has been running. For example, our sample topology has been running for 8 days 15 hours 6 months 16 seconds.**Num workers**: This specifies how many workers are allocated to the topology. Again, if we refer to `WordCountTopology.java`, we will see this snippet where it is declared as `3`:

conf.setNumWorkers(3);


**Num executors**: This specifies the sum total of the number of executors in the topology. This is connected to the parallelism hint that is specified during the overall integration of the topology in the topology builder as follows:

builder.setSpout("spout", new RandomSentenceSpout(), 5);


Here, in our `WordCount` topology, we have specified the parallelism of the spout as `5`, so five instances of the spout will be spawned in the topology.

**Num tasks**: This gains the sum total of another parameter that is specified at the time of overall integration in the topology, as shown:

builder.setSpout("spout", new RandomSentenceSpout(), 5).setNumTasks(10);


Here, we are specifying that for `5` executors dedicated to the spout, the total value of `numtasks` is `10`, so two tasks each will be spawned on each of the executors.

What we see on the UI is a total of all `numtasks`  values across all topology components.

第二部分

这一部分包含了可以在拓扑上执行的各种操作:

  • 激活:UI 提供了一个功能,可以重新激活之前被暂停的拓扑。一旦激活,它可以再次开始从 spout 消费消息并处理它们。

  • 停用:当执行此操作时,拓扑立即关闭 spout,也就是说,不会从 spout 读取新消息并将其推送到 DAG 下游。已经在各种 bolt 中处理的现有消息将被完全处理。

  • 重新平衡:当对活动拓扑的 worker 分配发生变化时执行此操作。

  • 终止:顾名思义,用于向 Storm 框架发送拓扑的终止信号。建议提供合理的终止时间,以便拓扑完全排空并能够在终止之前清理流水线事件。

第三部分

这一部分显示了时间轴上处理的消息数量的截图。它有以下关键部分:

  • 窗口:这个字段指定了以下时间段的时间段:最近 10 分钟,最近 3 小时,过去一天,或者一直。拓扑的进展是根据这些时间段来捕获的。

  • 发射:这捕获了 spout 在各个时间段发射的元组数量。

  • 传输:这指定了发送到拓扑中其他组件的元组数量。请注意,发射的元组数量可能与传输的元组数量相等,也可能不相等,因为前者是 spout 的 emit 方法执行的确切次数,而后者是基于使用的分组而传输的数量;例如,如果我们将一个 spout 绑定到一个具有两个元组并行度的 bolt,使用 all 分组,那么对于 spout 发射的每个x个元组,将传输2x个元组。

  • 完整延迟ms):这是元组在整个拓扑中执行所花费的平均总时间。

  • 已确认:这个字段保存了成功处理的已确认事件的数量。

  • 失败:这是处理失败的事件数量。

第四部分

这一部分与第三部分相同,唯一的区别是这里的术语显示在组件级别,即 spouts 和 bolts,而在第三部分中是在拓扑级别。UI 上还有一些术语需要介绍给你。它们如下:

  • 容量:这是微调拓扑时要查看的最重要的指标之一。它指示螺栓在最后十分钟内执行元组所花费的时间的百分比。任何接近或超过一的值都表明需要增加此螺栓的并行性。它使用以下公式计算:
Capacity = (Number of tuples Executed*Average execute  latency)/Window_Size*1000)
  • 执行延迟:这是元组在处理过程中在螺栓的执行方法中花费的平均时间。

  • 处理延迟:处理延迟是元组从螺栓接收到到被确认(表示成功处理)的平均时间。

可视化部分

Storm 0.9.2 中的一个改进是拓扑的可视化描述。以下图是 Storm UI 中样本拓扑的描述:

可视化部分

在前面的截图中,您可以看到拓扑上由各种螺栓和喷口可视标记的所有流,以及延迟和其他关键属性。

Storm UI 提供了一个非常丰富的界面,用户可以从非常高的级别开始,并深入到特定领域,就像在Storm 集群设置部分的截图中所看到的那样,我们讨论了 Storm 集群级属性;在第二级中,我们移动到特定的拓扑。接下来,在拓扑内,您可以单击任何螺栓或工作程序,组件级别的详细信息将呈现给您。在集群设置中,以下截图中突出显示的一个项目对于调试和日志解密非常重要——工作程序 ID。如果某个组件的喷口或螺栓给我们带来问题,并且我们想要了解其工作原理,首先要查看的地方是日志。要能够查看日志,需要知道有问题的螺栓在哪个监督者上执行以及哪个工作程序;这可以通过钻取该组件并查看执行器部分来推断:

可视化部分

Storm UI 捕获监督者端口

在这里,主机告诉您此组件正在哪个监督者上运行,端口告诉您有关工作程序,因此,如果我想查找此组件的日志,我将在logdir中查找sup-flm-dev-1.mydomain.net下的worker-6711.log日志目录。

Storm 监控工具

像 Storm 这样的集群设置需要不断监控,因为它们通常是为支持实时系统而开发的,其中停机可能会对服务级别协议SLA)构成问题。市场上有很多工具可用于监控 Storm 集群并发出警报。一些 Storm 监控工具如下:

  • Nagios:这是一个非常强大的监控系统,可以扩展以生成电子邮件警报。它可以监控各种进程和系统 KPI,并可以通过编写自定义脚本和插件来在发生故障时重新启动某些组件。Storm 监控工具

Nagios 服务控制台

在前面的 Storm 集群与 Nagios 监控的截图中,您可以看到各种可以监控的进程和其他系统级 KPI,如 CPU、内存、延迟、硬盘使用率等。

  • Ganglia:这是另一个广泛使用的开源工具,可以为 Storm 集群设置监控框架。Storm 监控工具

如前面的截图所示,我们有很多钻取选项;我们可以看到负载和 CPU 级别的详细信息,以及其他系统和集群级 KPI 来捕获和绘制集群的健康状态。

  • SupervisorD:这是另一个广泛使用的开源监控系统,通常与 Storm 一起使用以捕获和保持集群的健康状态。SupervisorD 还有助于配置和启动 Storm 服务,并且可以在发生故障时配置以重新启动它们。Storm 监控工具

  • Ankush:这是另一个可以用于 Storm 和其他大数据集群设置和管理的供应和监控系统。它有付费和开源版本(github.com/impetus-opensource/ankush)。它具有以下显著特点:

供应 此应用程序支持的环境物理节点云上的虚拟节点(AWS 或本地)
单一技术集群
多技术集群
基于模板的集群创建
重新部署出错的集群
机架支持
在部署前增强节点验证
监控 热图
服务监控
基于技术的监控
丰富的图表
关键事件的警报和通知
集中式日志视图
审计追踪
仪表板和电子邮件上的警报

以下截图是 Ankush 的仪表板截图。所有系统级 KPI(如 CPU、负载、网络、内存等)都被很好地捕获。

Storm 监控工具

测验时间

Q.1. 判断以下陈述是真还是假:

  1. Storm 配置存储在cluster.xml中。

  2. 每个监督者只能分配四个工作节点。

  3. Zookeeper 集群始终有奇数个节点。

  4. Zookeeper 需要至少三个快照才能从故障中恢复其状态。

  5. 如果 Nimbus 和监督者死亡,拓扑可以继续执行。

Q.2. 填空:

  1. _______________ 是元组被处理和确认所花费的平均时间。

  2. _______________ 是元组在执行方法中花费的平均时间。

  3. 在故障发生时,__________ 组件负责恢复 Storm 集群。

Q.3. 在一个三节点的 Storm 集群(一个 Nimbus 和两个监督者)上执行WordCount拓扑,然后执行以下任务:

  • 在拓扑运行时终止 Nimbus 节点—观察拓扑不会失败,它将继续不受影响。

  • 在拓扑运行时终止监督者—观察拓扑不会失败,它将继续不受影响。工作节点将继续执行,并与 Zookeeper 协调。

  • 尝试从 Storm UI 进行重新平衡和停用等各种操作。

摘要

在本章中,您详细了解了 Storm 和 Zookeeper 的配置。我们探索并向您介绍了 Storm UI 及其属性。完成了集群设置后,我们简要介绍了 Storm 中可用于运营生产支持的各种监控工具。

在下一章中,我们将介绍 RabbitMQ 及其与 Storm 的集成。

第五章:Storm 高可用性和故障转移

本章将带您进入 Storm 的旅程的下一个级别,在这里我们将让您熟悉 Storm 与生态系统中其他必要组件的集成。我们将实际涵盖高可用性和可靠性的概念。

本章是理解 Storm 及其相关组件的集群模式设置的下一步。我们将了解 Storm 和 Zookeeper 中的各种配置以及它们背后的概念。

本章将涵盖以下主题:

  • 设置 RabbitMQ(单实例和集群模式)

  • 开发 AMQP 喷流以集成 Storm 和 RabbitMQ

  • 创建 RabbitMQ 饲料器组件

  • 为 RabbitMQ 和 Storm 集群构建高可用性

  • Storm 调度程序

通过本章结束时,您将能够设置和理解 RabbitMQ,并将 Storm 与 RabbitMQ 集成。此外,您将能够测试 Storm 集群的高可用性和可靠处理。

RabbitMQ 概述

RabbitMQ 的要点是消息传递只是起作用

RabbitMQ 是 AMQP 消息协议最广泛使用的实现之一,它提供了一个用于接收和传递消息的平台。这个内存队列还有能力保存和保留消息,直到它们被消费者消耗。这种灵活的代理系统非常易于使用,并且适用于大多数操作系统,如 Windows、UNIX 等。

RabbitMQ 是高级消息队列协议AMQP)的实现。如下图所示,RabbitMQ 的关键组件是交换队列

RabbitMQ 概述

发布者和消费者是两个重要的角色;前者生成消息并将其发布到交换,后者根据其类型将消息从发布者发布到队列,然后从队列发布到消费者,消费者接收消息。

需要注意的是,这里的发布者与交换进行交互,而不是队列。RabbitMQ 支持各种类型的交换,如直接、扇出、主题等。交换的任务是根据交换的类型和与消息关联的路由键,将消息路由到一个或多个队列。因此,如果是直接交换,消息将被传递到与交换绑定的一个队列,其路由键与消息中的路由键匹配。如果是扇出交换,那么消息将被传递到与交换绑定的所有队列,路由完全被忽略。

安装 RabbitMQ 集群

RabbitMQ 是一个消息代理-消息的中间人。它为您的应用程序提供了一个发送和接收消息的共同平台,并为您的消息提供了一个安全的存放处,直到它们被接收。

设置 RabbitMQ 的先决条件

确保您已经注意到短名称也包括在/etc/hosts文件中,如下面的代码所示:

<ip address1>     <hostname1> <shortname1> 
<ip address2>     <hostname2> <shortname2> 

注意

在 RabbitMQ 集群中,/etc/hosts中的短名称是强制性的,因为节点间的通信是使用这些短名称进行的。

例如,我们的集群中有两台机器,具有以下提到的 IP 和主机名;RabbitMQ 守护程序在启动集群时使用这些信息:

10.191.206.83     rmq-flc-1.mydomain.net rmq-flc-1 
10.73.10.63       rmq-flc-2.mydomain.net rmq-flc-2

如果未设置短名称,您将看到此错误:系统未运行以使用完全限定的主机名

设置 RabbitMQ 服务器

Ubuntu 附带了 RabbitMQ,但通常不是最新版本。最新版本可以从 RabbitMQ 的 Debian 存储库中检索。应在 Ubuntu 上运行以下 shell 脚本以安装 RabbitMQ:

#!/bin/sh
sudo cat <<EOF > /etc/apt/sources.list.d/rabbitmq.list
sudo deb http://www.rabbitmq.com/debian/ testing main
EOF

sudo curl http://www.rabbitmq.com/rabbitmq-signing-key-public.asc -o  /tmp/rabbitmq-signing-key-public.asc
sudo apt-key add /tmp/rabbitmq-signing-key-public.asc
sudo rm /tmp/rabbitmq-signing-key-public.asc

sudo apt-get -qy update
sudo apt-get -qy install rabbitmq-server

测试 RabbitMQ 服务器

以下步骤将为您提供在 Ubuntu 终端上执行的命令,以启动 RabbitMQ 服务器并对其进行测试。它们如下:

  1. 通过在 shell 上运行以下命令启动 RabbitMQ 服务器:
sudo service rabbitmq-server start

测试 RabbitMQ 服务器

  1. 通过运行以下命令检查服务器状态:
sudo service rabbitmq-server status

测试 RabbitMQ 服务器

  1. 在每个 RabbitMQ 实例上,要启用 RabbitMQ 管理控制台,请执行以下命令,并使用以下命令重新启动该实例上运行的 RabbitMQ 服务器:
sudo rabbitmq-plugins enable rabbitmq_management

  1. 要启用 RabbitMQ 插件,请转到/usr/lib/rabbitmq/bin并在两个节点上执行以下命令,然后重新启动它们:
sudo rabbitmq-plugins enable rabbitmq_management

  1. 启动、关闭和错误日志将在/var/log/rabbitmq目录下创建。

创建 RabbitMQ 集群

以下是设置两个(或更多)节点 RabbitMQ 集群所需执行的步骤:

  1. 考虑到rmq-flc-1rmq-flc-2是两个实例的短主机名,我们将使用以下命令在两个实例上启动独立的 RabbitMQ 服务器:
sudo service rabbitmq-server start

  1. rmq-flc-2上,我们将停止 RabbitMQ 应用程序,重置节点,加入集群,并使用以下命令重新启动 RabbitMQ 应用程序(所有这些都是在rmq-flc-1上的 RabbitMQ 服务器正在运行时完成的):
sudo rabbitmqctl stop_app
sudo rabbitmqctl join_cluster rabbit@rmq-flc-1
sudo rabbitmqctl start_app

  1. 通过在任何一台机器上运行以下命令来检查集群状态:
sudo service rabbitmq-server status

  1. 应该看到以下输出:创建 RabbitMQ 集群

  2. 集群已成功设置。

如果启用了 UI,可以在http:/ /<hostip>:15672(用户名:guest,密码:guest)访问集群。

启用 RabbitMQ UI

执行以下步骤以启用 RabbitMQ UI:

  1. 执行以下命令:
sudo /usr/lib/rabbitmq/bin/rabbitmq-plugins enable  rabbitmq_management

  1. 上述命令将产生以下输出:
The following plugins have been enabled:
mochiweb
webmachine
rabbitmq_mochiweb
amqp_client
rabbitmq_management_agent
rabbitmq_management
Plugin configuration has changed. Restart RabbitMQ for changes to take effect.

  1. 在集群的所有节点上重复前面的步骤。

  2. 使用以下命令重新启动每个节点:

sudo service rabbitmq-server restart 

  1. 使用http:``//<hostip>:15672链接访问 UI。默认用户名和密码是guest

为高可用性创建镜像队列

在本节中,我们将讨论一种特殊类型的队列,它保证了 RabbitMQ 默认队列的高可用性。默认情况下,我们创建的队列位于单个节点上,根据它们声明的顺序,这可能成为单点故障。让我们看一个例子。我有一个由两个 RabbitMQ 节点rabbit1rabbit2组成的集群,并在我的集群上声明了一个交换机,比如myrabbitxchange。假设按照执行顺序,在rabbit1上创建了队列。现在,如果rabbit1宕机,那么队列就消失了,客户端将无法发布到它。

因此,为了避免情况,我们需要高可用性队列;它们被称为镜像队列,在集群中的所有节点上都有副本。镜像队列有一个主节点和多个从节点,最老的节点是主节点,如果它不可用,则可用节点中最老的节点成为主节点。消息被发布到所有从节点。这增强了可用性,但不会分配负载。要创建镜像队列,请使用以下步骤:

  1. 可以通过使用 Web UI 添加策略来启用镜像。转到管理选项卡,选择策略,然后单击添加策略

  2. 指定策略名称模式定义,然后单击添加策略,如下面的截图所示:为高可用性创建镜像队列

将 Storm 与 RabbitMQ 集成

现在我们已经安装了 Storm,下一步将是将 RabbitMQ 与 Storm 集成,为此我们将不得不创建一个名为 RabbitMQ spout 的自定义 spout。这个 spout 将从指定队列中读取消息;因此,它将提供一个消费者的角色,然后将这些消息推送到下游拓扑。

以下是 spout 代码的样子:

public class AMQPRecvSpout implements IRichSpout{

//The constructor where we set initialize all properties
  public AMQPRecvSpout(String host, int port, String username,  String password, String vhost, boolean requeueOnFail, boolean  autoAck) {
    this.amqpHost = host;
    this.amqpPort = port;
    this.amqpUsername = username;
    this.amqpPasswd = password;
    this.amqpVhost = vhost;
    this.requeueOnFail = requeueOnFail;
    this.autoAck = autoAck;
  }
/*
Open method of the spout , here we initialize the prefetch count ,  this parameter specified how many messages would be prefetched  from the queue by the spout – to increase the efficiency of the  solution */
  public void open(@SuppressWarnings("rawtypes") Map conf,  TopologyContext context, SpoutOutputCollector collector) {
    Long prefetchCount = (Long) conf.get(CONFIG_PREFETCH_COUNT);
    if (prefetchCount == null) {
      log.info("Using default prefetch-count");
      prefetchCount = DEFAULT_PREFETCH_COUNT;
    } else if (prefetchCount < 1) {
      throw new IllegalArgumentException(CONFIG_PREFETCH_COUNT + "  must be at least 1");
    }
    this.prefetchCount = prefetchCount.intValue();

    try {
      this.collector = collector;
      setupAMQP();
    } catch (IOException e) {
      log.error("AMQP setup failed", e);
      log.warn("AMQP setup failed, will attempt to reconnect...");
      Utils.sleep(WAIT_AFTER_SHUTDOWN_SIGNAL);
      reconnect();
    }
  }

  /**
   * Reconnect to an AMQP broker.in case the connection breaks at  some point
   */
  private void reconnect() {
    log.info("Reconnecting to AMQP broker...");
    try {
      setupAMQP();
    } catch (IOException e) {
      log.warn("Failed to reconnect to AMQP broker", e);
    }
  }
  /**
   * Set up a connection with an AMQP broker.
   * @throws IOException
   *This is the method where we actually connect to the queue  using AMQP client APIs
   */
  private void setupAMQP() throws IOException{
    final int prefetchCount = this.prefetchCount;
    final ConnectionFactory connectionFactory = new  ConnectionFactory() {
      public void configureSocket(Socket socket)
          throws IOException {
        socket.setTcpNoDelay(false);
        socket.setReceiveBufferSize(20*1024);
        socket.setSendBufferSize(20*1024);
      }
    };

    connectionFactory.setHost(amqpHost);
    connectionFactory.setPort(amqpPort);
    connectionFactory.setUsername(amqpUsername);
    connectionFactory.setPassword(amqpPasswd);
    connectionFactory.setVirtualHost(amqpVhost);

    this.amqpConnection = connectionFactory.newConnection();
    this.amqpChannel = amqpConnection.createChannel();
    log.info("Setting basic.qos prefetch-count to " +  prefetchCount);
    amqpChannel.basicQos(prefetchCount);
    amqpChannel.exchangeDeclare(Constants.EXCHANGE_NAME,  "direct");
    amqpChannel.queueDeclare(Constants.QUEUE_NAME, true, false,  false, null);
    amqpChannel.queueBind(Constants.QUEUE_NAME,  Constants.EXCHANGE_NAME, "");
    this.amqpConsumer = new QueueingConsumer(amqpChannel);
    assert this.amqpConsumer != null;
    this.amqpConsumerTag =  amqpChannel.basicConsume(Constants.QUEUE_NAME, this.autoAck,  amqpConsumer);
  }

  /* 
   * Cancels the queue subscription, and disconnects from the AMQP  broker.
   */
  public void close() {
    try {
      if (amqpChannel != null) {
        if (amqpConsumerTag != null) {
          amqpChannel.basicCancel(amqpConsumerTag);
        }
        amqpChannel.close();
      }
    } catch (IOException e) {
      log.warn("Error closing AMQP channel", e);
    }

    try {
      if (amqpConnection != null) {
        amqpConnection.close();
      }
    } catch (IOException e) {
      log.warn("Error closing AMQP connection", e);
    }
  }
  /* 
   * Emit message received from queue into collector
   */
  public void nextTuple() {
    if (spoutActive && amqpConsumer != null) {
      try {
        final QueueingConsumer.Delivery delivery =  amqpConsumer.nextDelivery(WAIT_FOR_NEXT_MESSAGE);
        if (delivery == null) return;
        final long deliveryTag =  delivery.getEnvelope().getDeliveryTag();
        String message = new String(delivery.getBody());

        if (message != null && message.length() > 0) {
          collector.emit(new Values(message), deliveryTag);
        } else {
          log.debug("Malformed deserialized message, null or zero- length. " + deliveryTag);
          if (!this.autoAck) {
            ack(deliveryTag);
          }
        }
      } catch (ShutdownSignalException e) {
        log.warn("AMQP connection dropped, will attempt to  reconnect...");
        Utils.sleep(WAIT_AFTER_SHUTDOWN_SIGNAL);
        reconnect();
      } catch (ConsumerCancelledException e) {
        log.warn("AMQP consumer cancelled, will attempt to  reconnect...");
        Utils.sleep(WAIT_AFTER_SHUTDOWN_SIGNAL);
        reconnect();
      } catch (InterruptedException e) {
        log.error("Interrupted while reading a message, with  Exception : " +e);
      }
    }
  }
  /* 
   * ack method to acknowledge the message that is successfully  processed 
*/

  public void ack(Object msgId) {
    if (msgId instanceof Long) {
      final long deliveryTag = (Long) msgId;
      if (amqpChannel != null) {
        try {
          amqpChannel.basicAck(deliveryTag, false);
        } catch (IOException e) {
          log.warn("Failed to ack delivery-tag " + deliveryTag,  e);
        } catch (ShutdownSignalException e) {
          log.warn("AMQP connection failed. Failed to ack  delivery-tag " + deliveryTag, e);
        }
      }
    } else {
      log.warn(String.format("don't know how to ack(%s: %s)",  msgId.getClass().getName(), msgId));
    }
  }

  public void fail(Object msgId) {
    if (msgId instanceof Long) {
      final long deliveryTag = (Long) msgId;
      if (amqpChannel != null) {
        try {
          if (amqpChannel.isOpen()) {
            if (!this.autoAck) {
              amqpChannel.basicReject(deliveryTag, requeueOnFail);
            }
          } else {
            reconnect();
          }
        } catch (IOException e) {
          log.warn("Failed to reject delivery-tag " + deliveryTag,  e);
        }
      }
    } else {
      log.warn(String.format("don't know how to reject(%s: %s)",  msgId.getClass().getName(), msgId));
    }
  }

public void declareOutputFields(OutputFieldsDeclarer declarer) {
    declarer.declare(new Fields("messages"));
  }
}

需要在项目pom.xml中引入 AMQP Maven 依赖项,如下所示:

    <dependency>
      <groupId>com.rabbitmq</groupId>
      <artifactId>amqp-client</artifactId>
      <version>3.2.1</version>
    </dependency>

创建 RabbitMQ 饲料器组件

现在我们已经安装了 RabbitMQ 集群,我们所需要做的就是开发一个发布者组件,它将把消息发布到 RabbitMQ。这将是一个简单的 Java 组件,模拟向 RabbitMQ 发布实时数据。这个组件的基本代码片段如下:

public class FixedEmitter {
  private static final String EXCHANGE_NAME = "MYExchange";
  public static void main(String[] argv) throws Exception {
    /*we are creating a new connection factory for builing  connections with exchange*/
    ConnectionFactory factory = new ConnectionFactory();
    /* we are specifying the RabbitMQ host address and port here  in */

    Address[] addressArr = {
      new Address("localhost", 5672)
    }; //specify the IP if the queue is not on local node where  this program would execute 
    Connection connection = factory.newConnection(addressArr);
    //creating a channel for rabbitMQ
    Channel channel = connection.createChannel();
    //Declaring the queue and routing key
    String queueName = "MYQueue";
    String routingKey = "MYQueue";
    //Declaring the Exchange
    channel.exchangeDeclare(EXCHANGE_NAME, "direct", false);
    Map < String, Object > args = new HashMap < String, Object >  ();
    //defining the queue policy
    args.put("x-ha-policy", "all");
    //declaring and binding the queue to the exchange
    channel.queueDeclare(queueName, true, false, false, args);
    channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
    String stoppedRecord;
    int i = 0;
    //emitting sample records
    while (i < 1) {
      try {
        myRecord = "MY Sample record";
        channel.basicPublish(EXCHANGE_NAME, routingKey,
          MessageProperties.PERSISTENT_TEXT_PLAIN,
          myRecord.getBytes());
        System.out.println(" [x] Sent '" + myRecord + "' sent at "  + new Date());
        i++;
        Thread.sleep(2);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
    channel.close();
    connection.close();
  }
}

为 AMQP spout 连接拓扑

现在我们已经准备好了集群队列设置,放置了 AMQP spout 和 feeder 组件;让我们放置最后一个部分,即 Storm 拓扑的整体集成。

让我们再次使用我们的WordCount拓扑,而不是RandomSentenceSpout,我们将使用在上一节中设计的AMQPRecvSpout将 Storm 与 RabbitMQ 集成

需要修改以下代码块:

builder.setSpout("spout", new RandomSentenceSpout(), 5);
builder.setBolt("split", new SplitSentence(),  8).shuffleGrouping("spout");
We will use the new spout instead, as follows:

builder.setSpout("queue_reader", new  AMQPRecvSpout(Constants.RMQ_ADDRESS, 5672, "guest", "guest",  "/"));

构建组件的高可用性

现在我们正处于寻找集群中各个组件的高可用性的合适时机。我们将通过一系列练习来完成这一点,假设每个组件都以集群模式安装,并且在生态系统中存在多个实例。

只有在设置了镜像队列之后,才能检查 RabbitMQ 的高可用性。假设:

  • 我们在 RabbitMQ 集群中有两个节点:node1 和 node2

  • MyExchange是为此练习创建的交换的名称

  • MyQueue是为此练习创建的镜像队列

接下来,我们将运行我们在创建 RabbitMQ feeder 组件部分创建的fixedEmitter代码。现在进行 Litmus 测试:

  • 假设队列MyQueue有 100 条消息

  • 现在关闭 node2(这意味着集群中的一个节点宕机)

  • 所有 100 条消息将被保留,并且在控制台上可见;当 node2 缺席时,node1 填充。

这种行为确保即使集群中的一个节点宕机,服务也不会中断。

Storm 集群的高可用性

现在让我们看一下 Storm 中故障转移或高可用性的演示。Storm 框架的构建方式使其可以继续执行,只要:

  • 它具有所需数量的 Zookeeper 连接

  • 它具有所需数量的工作进程在一个或多个监督者上

那么前面的陈述实际上是什么意思呢?好吧,让我们通过一个例子来理解。假设我在 Storm 集群上执行WordCount拓扑。这个集群的配置如下:

  • 有两个 Storm 监督者,每个 Storm 监督者有四个工作进程,所以集群中总共有八个工作进程

  • 有三个 Zookeeper 节点(最大连接数 30),所以总共有 3023=180 个连接

  • 一个拓扑分配了三个工作进程

假设当我们将这个拓扑提交到集群时,任务和进程会像下面的截图所示一样生成:

Storm 集群的高可用性

上图以图表方式描述了集群,灰色的工作进程是分配给拓扑的。现在我们已经准备好尝试 Storm 和 Zookeeper 的高可用性测试。Storm 和 Zookeeper 的测试如下:

  • 测试 1(所有组件都正常运行):在提交拓扑后关闭 Nimbus 节点;您会注意到拓扑将继续正常执行。

  • 测试 2(所有组件都正常运行):关闭一个 Zookeeper 节点,您会注意到拓扑将继续正常执行,因为其他两个可用的 Zookeeper 有足够的资源来保持 Storm 集群正常运行。

  • 测试 3(所有组件都正常运行):关闭两个 Zookeeper 节点,您会注意到拓扑将继续正常执行,因为其他两个可用的 Zookeeper 有足够的资源来保持 Storm 集群正常运行。

  • 测试 4(所有组件都正常运行,拓扑正在运行):杀死监督者 2;现在这个节点上有一个灰色的工作节点。因此当这个节点宕机时,灰色的工作节点会死掉,然后因为第二个监督者不可用,它会再次生成,这次在监督者 1 上。因此,拓扑的所有工作节点现在将在一个单独的监督者上执行,但系统将继续以有限的资源执行,但不会失败。

Storm 集群的保证处理

本节讨论的下一个主题是看Storm 的保证消息处理如何运作。我们在之前的章节中讨论过这个概念,但为了实际理解它,我没有深入讨论,因为我想先向大家介绍 AMQP spout。现在让我们回到我们在第二章中讨论的例子,开始你的第一个拓扑

现在如下图所示,虚线箭头流显示未能处理的事件被重新排队到队列中:

Storm 集群的保证处理

现在让我们稍微调整一下我们的wordCount拓扑,我们在其中添加了AMQPRecvSpout来使事件失败,并看看它们实际上出现在哪里。假设我使用FixedEmitter向队列中发出 10 个事件。现在我调整我的wordCount bolt,并在执行方法中引入人为的休眠,使每个事件在那里停留五分钟(使用Thread.sleep(300))。这将导致它的超时,因为默认事件超时时间为 60 秒。

现在当你运行拓扑时,你将能够看到事件通过 UI 重新排队回 RabbitMQ。

Storm 隔离调度程序

Storm 隔离调度程序是在 Storm 版本 0.8.2 中发布的。自从发布以来,这是一个非常方便的功能,非常积极地被使用,特别是在共享 Storm 集群的情况下。让我们通过一个例子来了解它的工作和能力;假设我们有一个由四个监督者节点组成的 Storm 集群,每个节点有四个插槽,所以总共有 16 个插槽。现在我想在这里使用三个 Storm 拓扑,比如 Topo1、Topo2 和 Topo3;每个拓扑都分配了四个工作节点。

因此,按照可能的默认设置,Storm 分发的调度行为将如下所示:

监督者 1 监督者 2 监督者 3 监督者 4
Topo1 Worker 1 Worker 2 Worker 3 Worker 4
Topo2 Worker 2 Worker 1 Worker 1 Worker 1
Topo3 Worker 3 Worker 3 Worker 2 Worker 2

Storm 将尊重负载分配,并在每个节点上生成每个拓扑的一个工作节点。

现在让我们稍微调整一下情景,并引入一个要求,即 Topo1 是一个非常资源密集型的拓扑结构。(我想要将一个监督者完全专门用于这个,这样我就可以节省网络跳数。)这可以通过使用隔离调度程序来实现。

我们需要在集群中每个 Storm 节点(Nimbus 和监督者)的storm.yaml文件中进行以下条目的设置:

isolation.scheduler.machines: 
    "Topol": 2

需要重新启动集群才能使此设置生效。这个设置意味着我们已经将两个监督者节点专门用于 Topo1,并且它将不再与提交到集群的其他拓扑共享。这也将确保在生产中遇到的多租户问题有一个可行的解决方案。

其他两个监督者将被 Topo2 和 Topo3 共享。可能的分配将如下所示:

监督者 1 监督者 2 监督者 3 监督者 4
Topo1 Worker 1Worker 2 Worker 1Worker 2
Topo2 Worker 1Worker 2 Worker 1Worker 2
Topo3 Worker 3Worker 4 Worker 3Worker 4

因此,从上表可以明显看出,Topo1 将被隔离到监督者 1 和 2,而 Top2 和 Topo3 将共享监督者 3 和 4 上的其余八个插槽。

测验时间

Q.1 判断以下句子是真是假:

  1. AMQP 是 STOMP 协议。

  2. RabbitMQ 不是故障安全的。

  3. 需要 AMQP 客户端来发布到 RabbitMQ。

  4. 镜像队列可以从集群中节点的故障中恢复。

Q.2 填空:

  1. _______________ 是根据路由键传递消息的交换机。

  2. _______________ 是消息被广播的交换机。

  3. _______________ 是 AMQP 消费者协议上 Storm spout 的实现。

Q.3 在一个三节点的 Storm 集群(一个 nimbus 和两个 supervisor 节点)上执行WordCount拓扑,与一个两节点的 RabbitMQ 集群结合在一起:

  • 尝试各种在构建组件的高可用性部分提到的故障场景

  • 在消息处理中引入人工延迟,以校准 Storm 拓扑的保证处理

总结

在本章中,您已经了解了 AMQP 协议的 RabbitMQ 实现。我们完成了集群设置,并将 Storm 拓扑的输出与队列集成在一起。我们还探索并实际测试了 RabbitMQ 和 Storm 的高可用性和可靠性场景。我们通过涉及 Storm 调度器来结束了本章。在下一章中,我们将了解使用 Cassandra 的 Storm 持久化。

第六章:将 NoSQL 持久性添加到 Storm

在本章中,我们将毕业于理解 Storm 的下一步——我们将为我们的拓扑添加持久性。我们选择了 Cassandra,原因是非常明显的,这将在本章中详细阐述。我们的目的是让您了解 Cassandra 数据存储如何与 Storm 拓扑集成。

本章将涵盖以下主题:

  • Cassandra 的优势

  • 列式数据库和列族设计基础知识的介绍

  • 设置 Cassandra 集群

  • 介绍 CQLSH、CLI 和连接器 API

  • Storm 拓扑与 Cassandra 存储相连

  • 理解持久性的机制

  • Storm Cassandra 应用程序的最佳实践

Cassandra 的优势

这是任何人都会问的第一个和最明显的问题,“为什么我们要使用 NoSQL?”嗯,对于选择 NoSQL 而不是传统数据存储的非常快速的答案与为什么世界正在转向大数据是一样的——低成本、高可扩展性和可靠的解决方案,可以存储无限量的数据。

现在,下一个问题是为什么选择 Cassandra,而不是 NoSQL 堆栈中的其他任何东西。答案在于我们正在尝试实现的问题和解决方案方法的性质。嗯,我们正在处理实时分析,我们需要的一切都应该准确、安全可靠和极快速。因此,Cassandra 是最佳选择,因为:

  • 它在其同行中(如 HBase 等)拥有最快的写入速度

  • 它具有点对点设计的线性可扩展性

  • 没有单点故障

  • 读写请求可以在不影响彼此性能的情况下处理

  • 处理包含数百万交易和极快速度的搜索查询

  • 具有复制因子的故障安全和高可用性

  • 在 NoSQL 数据库的 CAP 定理上保证最终一致性

  • 列族设计以处理各种格式

  • 没有或很低的许可成本

  • 较少的开发运维或运营成本

  • 它可以扩展以集成各种其他大数据组件

列式数据库基础知识

开始使用 NoSQL 数据存储最重要的一点是了解列式数据库的基础知识;或者更确切地说,让我们使用实际术语——列族。

这是一个在不同的 NoSQL 数据库中有各种实现的概念,例如:

  • Cassandra:这是一个基于键值对的 NoSQL 数据库

  • Mongo DB:这是一个基于文档的 NoSQL 数据库

  • Neo4J:这是一个图形数据库

它们在以下方面与传统的面向行的关系数据库系统不同:

  • 性能

  • 存储可扩展性

  • 容错性

  • 低或没有许可成本

但是,尽管已经列举了所有 NoSQL 数据库的差异和优势,您必须清楚地理解,转向 NoSQL 是对数据存储、可用性和访问的整个范式的转变,它们并不是关系数据库的替代品。

在关系数据库管理系统的世界中,我们都习惯于创建表,但在 Cassandra 中,我们创建列族,其中定义了列的元数据,但列实际上存储为行。每行可以有不同的列集,因此整个列族相对不太结构化和可扩展。

列族的类型

有两种类型的列族:

  • 静态列族:顾名思义,它具有静态的列集,并且非常接近所有众所周知的关系数据库表,除了一些由于其 NoSQL 传统而产生的差异。以下是静态列族的一个示例:
行键
Raman 名字
Raman Subramanian
Edison 名字
Edison Weasley
Amey 名字
Amey Marriot
Sriman 名字
Sriman Mishra
  • 动态列族:这个真正体现了无结构和无模式的真正本质。在这里,我们不使用与列族关联的预定义列,而是可以由客户端应用程序在插入数据时动态生成和提供。在创建或定义动态列族时,我们可以通过定义比较器和验证器来定义有关列名和值的信息。以下是动态列族的一个示例:
行键
Raman 名字
Edison 地址
Amey 国家
Sriman 国籍

列的类型

Cassandra 支持各种列:

  • 标准列:这些列包含一个名称;这是由写入应用程序静态或动态设置的。这里显示了一个值(实际上是存储数据的属性)和时间戳:
列名
时间戳

Cassandra 利用与列相关联的时间戳来查找列的最后更新。当从 Cassandra 查询数据时,它按照这个时间戳排序,并始终返回最近的值。

  • 复合列:Cassandra 利用这种存储机制来处理聚类行。这是一种处理所有逻辑行的独特方式,这些逻辑行共享相同的分区键,形成一个单个的物理宽行。这使得 Cassandra 能够完成存储每行 20 亿列的传奇壮举。例如,假设我想创建一个表,其中捕获来自一些社交网络站点的实时状态更新:
CREATE TABLE statusUpdates(
  update_id uuid PRIMARY KEY,
  username varchar,
  mesage varchar
  );

CREATE TABLE timeseriesTable (
  user_id varchar,
  udate_id uuid,
  username varchar,
  mesage varchar,
  PRIMARY KEY user_id , update_id )
);

实时更新记录在StatusUpdates表下,该表具有usernamemessageupdate_id(实际上是 UUID)属性。

在设计 Cassandra 列族时,应充分利用 UUID 提供的功能,这可以用于对数据进行排序。

来自timeseriesTableuser_idupdate_id属性的组合可以唯一标识时间顺序中的一行。

Cassandra 使用主键中定义的第一列作为分区键;这也被称为行键。

  • 过期列:这些是 Cassandra 的特殊类型列,它们与时间到期(TTL)相关联;存储在这些列中的值在 TTL 过去后会自动删除或擦除。这些列用于我们不希望保留超过规定时间间隔的数据的用例;例如,如果我们不需要 24 小时前的数据。在我们的列族中,我会将每个插入的列关联一个 24 小时的 TTL,并且这些数据将在插入后的 24 小时内被 Cassandra 自动删除。

  • 计数列:这些又是专门的功能列,用于递增存储数字。它们有一个特殊的实现和专门的用途,用于我们使用计数器的情况;例如,如果我需要计算事件发生的次数。

设置 Cassandra 集群

Cassandra 是一个非常可扩展的键值存储。它承诺最终一致性,其分布式基于环形的架构消除了集群中的任何单点故障,因此使其高度可用。它被设计和开发用于支持对大量数据进行非常快速的读写。这种快速的写入和读取能力使其成为用于支持大型业务智能系统的在线事务处理(OLTP)应用的一个非常强大的竞争者。

Cassandra 提供了基于列族的数据模型,比典型的键值系统更灵活。

安装 Cassandra

Cassandra 需要部署的最稳定版本的 Java 1.6,最好是 Oracle 或 Sun JVM。执行以下步骤安装 Cassandra:

  1. 从 Apache Cassandra 网站下载最新的稳定版本(写作时的版本为 1.1.6)。

  2. /usr/local下创建一个 Cassandra 目录,如下所示:

sudo mkdir /usr/local/cassandra

  1. 将下载的 TAR 文件提取到/usr/local位置。使用以下命令:
sudo tar –xvf apache-cassandra-1.1.6-bin.tar.gz -C  /usr/local/cassandra

  1. Cassandra 需要一个目录来存储其数据、日志文件和缓存文件。创建/usr/local/cassandra/tmp来存储这些数据:
sudo mkdir –p /usr/local/cassandra/tmp

  1. 更新/usr/local/Cassandra/apache-cassandra-1.1.6/conf下的Cassandra.yaml配置文件。

以下属性将进入其中:

cluster_name: 'MyClusterName'
seeds: <IP of Node-1><IP of Node-2>(IP address of each node  go into it)
listen_address: <IP of Current Node>
  1. 使用以下脚本为每个节点计算一个 token,并通过在Cassandra.yaml中添加唯一 token 值来更新每个节点的initial_token属性:
#! /usr/bin/python
import sys
if (len(sys.argv) > 1):
  num=int(sys.argv[1])
else:
  num=int(raw_input("How many nodes are in your cluster? "))
for i in range(0, num):
  print 'node %d: %d' % (i, (i*(2**127)/num))
  1. 更新conf/log4j-server.properties文件中的以下属性。在cassandra下创建temp目录:
Log4j.appender.R.File=/usr/local/cassandra/temp/system.log

  1. 增加Cassandra.yaml中的rpc_timeout属性(如果此超时非常小且网络延迟很高,Cassandra 可能会假定节点已死亡,而没有等待足够长的时间来传播响应)。

  2. /usr/local/Cassandra/apache-cassandra-1.1.6上运行 Cassandra 服务器,使用bin/Cassandra -f

  3. /usr/local/Cassandra/apache-cassandra-1.1.6上使用bin/Cassandra-cli和主机和端口运行 Cassandra 客户端。

  4. 使用/usr/local/Cassandra/apache-cassandra-1.1.6下的bin/nodetool ring 实用程序验证正确连接的集群:

bin/nodetool –host <ip-adress> -p <port number> ring 
192.168.1.30 datacenter1 rack1 Up    Normal 755.25 MB  25.00% 0
192.168.1.31 datacenter1 rack1 Up    Normal 400.62 MB  25.00% 42535295865117307932921825928970
192.168.1.51 datacenter1 rack1 Up    Normal 400.62 MB  25.00% 42535295865117307932921825928971
192.168.1.32 datacenter1 rack1 Up    Normal 793.06 MB  25.00% 85070591730234615865843651857941

前面的输出显示了一个连接的集群。此配置显示它已正确配置和连接。

以下是输出的屏幕截图:

安装 Cassandra

多个数据中心

在实际场景中,我们希望将 Cassandra 集群分布在不同的数据中心,以便系统更可靠和更具抗灾性,以应对局部网络故障和物理灾难。

设置多个数据中心的先决条件

以下是设置多个数据中心时应使用的一组先决条件:

  • 在每个节点上安装 Cassandra

  • 在集群中每个节点的 IP 地址

  • 确定集群名称

  • 确定种子节点

  • 确定要使用的 snitch

安装 Cassandra 数据中心

以下是设置 Cassandra 数据中心的一组步骤:

  1. 让我们假设我们已经在以下节点上安装了 Cassandra:

10.188.66.41(seed1)

10.196.43.66

10.188.247.41

10.196.170.59(seed2)

10.189.61.170

10.189.30.138

  1. 使用前一节中定义的 token 生成 Python 脚本为每个前面的节点分配 token。

  2. 假设我们将节点及其 token 分布对齐到以下分布:

节点 IP 地址 Token 数据中心
node0 10.188.66.41 0 Dc1
node1 10.196.43.66 56713727820156410577229101238628035245 Dc1
node2 10.188.247.41 113427455640312821154458202477256070488 Dc1
node3 10.196.170.59 10 Dc2
node4 10.189.61.170 56713727820156410577229101238628035255 Dc2
node5 10.189.30.138 113427455640312821154458202477256070498 Dc2
  1. 停止节点上的 Cassandra 并清除 Cassandra 的data_dir中的数据:
$ ps auwx | grep cassandra 

此命令查找 Cassandra Java 进程 ID(PID):

$ sudo kill <pid> 

这是用指定的 PID 杀死进程的命令:

$ sudo rm -rf /var/lib/cassandra/*

上述命令清除了 Cassandra 的默认目录中的数据。

  1. 为每个节点修改cassandra.yaml文件中的以下属性设置:
endpoint_snitch <provide the name of snitch> 
  initial_token: <provide the value of token from previous  step>
  seeds: <provide internal IP_address of each seed node>
  listen_address: <provide localhost IP address>

更新后的配置如下:

node0:
end_point_snitch:  org.apache.cassandra.locator.PropertyFileSnitch
initial_token: 0
seed_provider:
  - class_name:  org.apache.cassandra.locator.SimpleSeedProvider
  parameters:
  - seeds: "10.188.66.41,10.196.170.59"
  listen_address: 10.196.43.66
  node1 to node5

所有这些节点的属性与前面的node0定义的属性相同,除了initial_tokenlisten_address属性。

  1. 接下来,我们将不得不为每个数据中心及其机架分配名称;例如,Dc1Dc2Rc1Rc2

  2. 转到cassandra-topology.properties文件,并针对每个节点的 IP 地址添加数据中心和机架名称的赋值。例如:

# Cassandra Node IP=Data Center:Rack
10.188.66.41=Dc1:Rc1
10.196.43.66=Dc2:Rc1
10.188.247.41=Dc1:Rc1
10.196.170.59=Dc2:Rc1
10.189.61.170=Dc1:Rc1
10.199.30.138=Dc2:Rc1
  1. 下一步是逐个启动种子节点,然后启动所有其他节点。

  2. 检查您的环是否正常运行。

CQLSH 介绍

既然我们已经完成了 Cassandra 的设置,让我们熟悉一下 shell 和一些基本命令:

  1. /usr/local/Cassandra/apache-cassandra-1.1.6上使用bin/cqlsh运行 CQL,带有主机和端口:
bin/cqlsh  –host <ip-adress> -p <port number>

  1. 在 Cassandra 客户端或 CQL 中创建一个 keyspace,如下所示:
create keyspace <keyspace_name>; 

  1. 在 Cassandra 客户端或 CQL 中创建一个列族,如下所示:
use <keyspace_name>;
create column family <columnfamily name>;

例如,创建以下表:

CREATE TABLE appUSers (
 user_name varchar,
 Dept varchar,
 email varchar,
 PRIMARY KEY (user_name));

  1. 从命令行插入一些记录到列族中:
INSERT INTO appUSers (user_name, Dept, email)
 VALUES ('shilpi', 'bigdata, 'shilpisaxena@yahoo.com');

  1. 从列族中检索数据:
SELECT * FROM appUSers LIMIT 10;

CLI 介绍

本节让您熟悉了另一个用于与 Cassandra 进程交互的工具——CLI shell。

以下步骤用于使用 CLI shell 与 Cassandra 进行交互:

  1. 以下是连接到 Cassandra CLI 的命令:
Cd Cassandra-installation-dir/bin
cassandra-cli -host localhost -port 9160

  1. 创建一个 keyspace:
[default@unknown] CREATE KEYSPACE myKeySpace
with placement_strategy = 'SimpleStrategy'
and strategy_options = {replication_factor:1};

  1. 使用以下命令验证 keyspace 的创建:
[default@unknown] SHOW KEYSPACES;
 Durable Writes: true
 Options: [replication_factor:3]
 Column Families:
 ColumnFamily: MyEntries
 Key Validation Class:  org.apache.cassandra.db.marshal.UTF8Type
 Default column value validator:  org.apache.cassandra.db.marshal.UTF8Type
 Columns sorted by:  org.apache.cassandra.db.marshal.ReversedType (org.apache.cassandra.db.marshal.TimeUUIDType)
 GC grace seconds: 0
 Compaction min/max thresholds: 4/32
 Read repair chance: 0.1
 DC Local Read repair chance: 0.0
 Replicate on write: true
 Caching: KEYS_ONLY
 Bloom Filter FP chance: default
 Built indexes: []
 Compaction Strategy:  org.apache.cassandra.db.compaction. SizeTieredCompactionStrategy
 Compression Options:
 sstable_compression:  org.apache.cassandra.io.compress.SnappyCompressor
 ColumnFamily: MYDevicesEntries
 Key Validation Class:  org.apache.cassandra.db.marshal.UUIDType
 Default column value validator:  org.apache.cassandra.db.marshal.UTF8Type
 Columns sorted by:  org.apache.cassandra.db.marshal.UTF8Type
 GC grace seconds: 0
 Compaction min/max thresholds: 4/32
 Read repair chance: 0.1
 DC Local Read repair chance: 0.0
 Replicate on write: true
 Caching: KEYS_ONLY
 Bloom Filter FP chance: default
 Built indexes:  [sidelinedDevicesEntries. sidelinedDevicesEntries_date_created_idx,  sidelinedDevicesEntries. sidelinedDevicesEntries_event_type_idx]
 Column Metadata:
 Column Name: event_type
 Validation Class:  org.apache.cassandra.db.marshal.UTF8Type
 Index Name: sidelinedDevicesEntries_event_type_idx
 Index Type: KEYS
 Index Options: {}
 Column Name: date_created
 Validation Class:  org.apache.cassandra.db.marshal.DateType
 Index Name: sidelinedDevicesEntries_date_created_idx
 Index Type: KEYS
 Index Options: {}
 Column Name: event
 Validation Class:  org.apache.cassandra.db.marshal.UTF8Type
 Compaction Strategy:  org.apache.cassandra.db.compaction. SizeTieredCompactionStrategy
 Compression Options:
 sstable_compression:  org.apache.cassandra.io.compress.SnappyCompressor

  1. 创建一个列族:
[default@unknown] USE myKeySpace;
 [default@demo] CREATE COLUMN FAMILY appUsers
 WITH comparator = UTF8Type
 AND key_validation_class=UTF8Type
 AND column_metadata = [
 {column_name:user_name, validation_class: UTF8Type}
 {column_name: Dept, validation_class: UTF8Type}
 {column_name: email, validation_class: UTF8Type}
];

  1. 将数据插入到列族中:
[default@demo] SET appUsers['SS'][user_name']='shilpi';
 [default@demo] SET appUsers['ss'][Dept]='BigData';
 [default@demo] SET  appUsers['ss']['email']=shilpisaxena@yahoo.com';

注意

在这个例子中,代码ss是我的行键。

  1. 从 Cassandra 列族中检索数据:
GET appUsers[utf8('ss')][utf8('user_name')];
List appUsers;

使用不同的客户端 API 访问 Cassandra

现在我们已经熟悉了 Cassandra,让我们继续下一步,我们将以编程方式访问(插入或更新)数据到集群中。一般来说,我们谈论的 API 是在核心 Thrift API 上编写的包装器,它提供了使用程序员友好的包进行 Cassandra 集群上的各种 CRUD 操作。

用于访问 Cassandra 的客户端 API 如下:

  • Thrift 协议:访问 Cassandra 的最基本的 API 是远程过程调用RPC)协议,它提供了一个语言中立的接口,因此可以使用 Python、Java 等进行通信。请注意,我们将讨论的几乎所有其他 API 都在内部使用Thrift。它使用简单,并且提供了基本的功能,如环形发现和本地访问。然而,它不支持重试、连接池等复杂功能。然而,有许多库扩展了 Thrift 并添加了这些必要的功能,我们将在本章中介绍一些广泛使用的库。

  • Hector:这是用于 Java 客户端应用程序访问 Cassandra 的最稳定和广泛使用的 API 之一。如前所述,它在内部使用 Thrift,因此基本上不能提供 Thrift 协议不支持的任何功能或功能。它被广泛使用的原因是它具有许多基本功能,可以直接使用并且可用:

  • 它具有连接池的实现

  • 它具有环形发现功能,并附带自动故障转移支持

  • 它在 Cassandra 环中具有对宕机主机的重试选项。

  • Datastax Java driver:这是最近添加到 Cassandra 客户端访问选项堆栈中的一个选项,因此与较新版本的 Cassandra 兼容。以下是它的显著特点:

  • 连接池

  • 重新连接策略

  • 负载均衡

  • 游标支持

  • Astyanax:这是 Cassandra 客户端 API 花束的最新添加,由 Netflix 开发,这使它比其他更加神秘。让我们看看它的凭证,看看它是否符合条件:

  • 它支持 Hector 的所有功能,并且使用起来更加容易

  • 它承诺比 Hector 更好地支持连接池

  • 它比 Hector 更擅长处理故障转移

  • 它提供了一些开箱即用的类似数据库的功能(这是个大新闻)。在 API 级别上,它提供了称为 Recipes 的功能,其中包括:

并行行查询执行

消息队列功能

对象存储

分页

  • 它具有许多经常需要的实用程序,如 JSON Writer 和 CSV Importer

Storm 拓扑连接到 Cassandra 存储

现在您已经了解并知道为什么应该使用 Cassandra。您已经学会了设置 Cassandra 和列族创建,并且甚至涵盖了可编程访问 Cassandra 数据存储的各种客户端/协议选项。正如前面提到的,Hector 目前是访问 Cassandra 最广泛使用的 API,尽管DatastaxAstyanax驱动程序正在迅速赶上。对于我们的练习,我们将使用 Hector API。

我们要实现的用例是使用 Cassandra 支持实时的电信数据的即时报告,这些数据正在使用 Storm 拓扑进行整理、解析和丰富。

Storm 拓扑连接到 Cassandra 存储

如前图所示,用例需要使用数据收集组件(为了练习,我们可以使用样本记录和模拟器 shell 脚本来模拟实时 CDR 数据)进行实时电信通话详单CDR)捕获。整理的实时数据被推送到 RabbitMQ 代理,然后被 Storm 拓扑消费。

对于拓扑,我们有一个 AMQP spout 作为消费者,它读取队列的数据并将其推送到拓扑的 bolt;在这里,我们已经连接了 bolt 来解析消息并将其转换为普通旧 Java 对象POJO)。然后,我们在我们的拓扑中有一个新的条目,即 Cassandra bolt,它实际上将数据存储在 Cassandra 集群中。

从 Cassandra 集群中,基于用户定义的搜索查询,UI 界面的消费者检索数据,从而提供即时的、实时的报告。

为了我们的实现,我们将像这里所示从 CLI/CQLSH 查询数据:

  1. 创建一个键空间:
create keyspace my_keyspace with placement_strategy = 'SimpleStrategy' and strategy_options = {replication_factor : 3} and durable_writes = true;
 use my_keyspace;

  1. 创建列族:
create column family my_columnfamily
  with column_type = 'Standard'
  and comparator = 'UTF8Type'
  and default_validation_class = 'BytesType'
  and key_validation_class = 'TimeUUIDType'
  and read_repair_chance = 0.1
  and dclocal_read_repair_chance = 0.0
  and gc_grace = 0
  and min_compaction_threshold = 4
  and max_compaction_threshold = 32
  and replicate_on_write = true
  and compaction_strategy =  'org.apache.cassandra.db.compaction. SizeTieredCompactionStrategy'
  and caching = 'KEYS_ONLY'
  and bloom_filter_fp_chance = 0.5
  and column_metadata = [
{column_name : 'cellnumber',
  validation_class : Int32Type },
  {column_name : 'tollchrg',
  validation_class : UTF8Type},
{column_name : 'msgres',
  validation_class : UTF8Type},

{column_name : 'servicetype',
  validation_class : UTF8Type}]
  and compression_options = {'sstable_compression' :  'org.apache.cassandra.io.compress.SnappyCompressor'
};
  1. 需要对项目中的pom.xml进行以下更改。应该将 Hector 依赖项添加到pom.xml文件中,以便在构建时获取并添加到m2存储库,如下所示:
  <dependency>
    <groupId>me.prettyprint</groupId>
    <artifactId>hector-core</artifactId>
    <version>0.8.0-2</version>
  </dependency>

如果您正在使用非 Maven 项目,请遵循通常的协议——下载 Hector 核心 JAR 文件并将其添加到项目构建路径,以满足所有所需的依赖关系。

  1. 接下来,我们需要在我们的 Storm 拓扑中放置组件。我们将首先创建一个CassandraController Java 组件,它将保存所有与 Cassandra 相关的功能,并且将从拓扑中的CassandraBolt类中调用以将数据持久化到 Cassandra 中:
public class CassandraController {

  private static final Logger logger =  LogUtils.getLogger(CassandraManager.class);
  //various serializers are declared in here
  UUIDSerializer timeUUIDSerializer = UUIDSerializer.get();
  StringSerializer stringSerializer =  StringSerializer.get();
  DateSerializer dateSerializer = DateSerializer.get();
  LongSerializer longSerializer = LongSerializer.get();

  public CassandraController() {
      //list of IPs of Cassandra node in ring
      String nodes =  "10.3.1.41,10.3.1.42,10.3.1.44,10.3.1.45";
      String clusterName = "mycluster";
      //creating a new configurator
      CassandraHostConfigurator hostConfigurator = new  CassandraHostConfigurator(nodes);
      hostConfigurator.setCassandraThriftSocketTimeout(0);
      cluster = HFactory.getOrCreateCluster(clusterName,  hostConfigurator);

      String[] nodeList = nodes.split(",");
      if (nodeList != null && nodeList.length ==  cluster.getConnectionManager(). getDownedHosts().size()) {
        logger.error("All cassandra nodes are down. " +  nodes);
      }

      //setting up read and write consistencies
      ConfigurableConsistencyLevel consistency = new  ConfigurableConsistencyLevel();
      consistency.setDefaultWriteConsistencyLevel (HConsistencyLevel.ONE);
      consistency.setDefaultReadConsistencyLevel (HConsistencyLevel.ONE);
      keySpaceObj = HFactory.createKeyspace ("my_keyspace", cluster, consistency);
      stringMutator = HFactory.createMutator(keySpaceObj, stringSerializer);
      uuidMutator = HFactory.createMutator (keySpaceObj, timeUUIDSerializer);

      logger.info("Cassandra data store initialized,  Nodes=" + nodes + ", " + "cluster name=" +  clusterName + ", " + "keyspace=" + keyspace + ", " +  "consistency=" + writeConsistency);
    }
    //defining the mutator 
  public Mutator < Composite > getCompositeMutator() {
    return compositeMutator;
  }

  public void setCompositeMutator(Mutator < Composite >  compositeMutator) {
      this.compositeMutator = compositeMutator;
    }
    //getter and setters for all mutators and serializers

  public StringSerializer getStringSerializer() {
    return stringSerializer;
  }

  public Keyspace getKeyspace() {
    return keySpaceObj;
  }
}
  1. 我们拓扑中最后一个组件实际上是将数据写入 Cassandra 的组件,这是一个 Storm bolt,它将利用之前创建的CassandraController来将实时数据写入 Cassandra:
public class CassandraBolt extends BaseBasicBolt {
  private static final Logger logger =  LogUtils.getLogger(CassandraBolt.class);

  public void prepare(Map stormConf, TopologyContext  context) {

    logger.debug("Cassandra bolt, prepare()");
    try {
      cassandraMngr = new CassandraController();
      myCf = "my_columnfamily";
      );

    } catch (Exception e) {
      logger.error("Error while instantiating  CassandraBolt", e);
      throw new RuntimeException(e);
    }
  }

  @Override
  public void execute(Tuple input, BasicOutputCollector  collector) {
    logger.debug("execute method :: Start ");
      Calendar tCalendar = null;
      long eventts = eventObj.getEventTimestampMillis();
      com.eaio.uuid.UUID uuid = new  com.eaio.uuid.UUID(getTimeForUUID(eventts),  clockSeqAndNode);

  java.util.UUID keyUUID =  java.util.UUID.fromString(uuid.toString());

  /*
  * Persisting to my CF
  */

  try {
    if (keyUUID != null) {
        cassandraMngrTDR.getUUIDMutator().addInsertion(
            keyUUID,
            myCf,
            HFactory.createColumn("eventts",
                new Timestamp(tCalendar.getTimeInMillis()),  -1, cassandraMngr.getStringSerializer(),
                cassandraMngr.getDateSerializer()));
     }

  cassandraMngrTDR.getUUIDMutator().addInsertion(
    keyUUID,
    myCf,
    HFactory.createColumn("cellnumber",  eventObj.getCellnumber(), -1,  cassandraMngr.getStringSerializer(),
      cassandraMngr.getLongSerializer()));
      cassandraMngr.getUUIDMutator().execute();
  logger.debug("CDR event with key = " + keyUUID + "  inserted into Cassandra cf " + myCf);

  } else {
  logger.error("Record not saved. Error while parsing date  to generate KEY for cassandra data store, column family -  " + myCf);
    }
  }

  catch (Exception excep) {
  logger.error("Record not saved. Error while saving data  to cassandra data store, column family - " + myCf,  excep);
  }

   logger.debug("execute method :: End ");
  }
}

所以我们完成了最后一块拼图;现在我们可以使用 Storm 实时将数据流入 Cassandra。一旦您执行了整个拓扑,您可以使用 CLI/CQLSH 上的 select 或 list 命令验证 Cassandra 中的数据。

Storm/Cassandra 应用程序的最佳实践

在处理具有 24/7 运行 SLA、非常高速和微小平均处理时间的分布式应用程序时,某些方面变得极为重要:

  • 网络延迟在实时应用程序中起着重要作用,可能会成败产品,因此在数据中心或跨数据中心中放置各种节点时,要做出非常明智和有意识的决定,通常建议将 ping 延迟保持在最低限度。

  • Cassandra 的复制因子应该在三左右。

  • 压缩应该是常规 Cassandra 维护的一部分。

测验时间

Q.1. 判断以下陈述是真是假:

  1. Cassandra 是基于文档的 NoSQL。

  2. Cassandra 有单点故障。

  3. Cassandra 在键分发时使用一致性哈希。

  4. Cassandra 工作在主从架构上。

Q.2. 填空:

  1. Cassandra 遵循 CAP 定理的 _______________ 属性。

  2. _______________ 是使 Cassandra 成为与 Storm 一起使用的有力竞争者的显著特点。

  3. Cassandra 是使用 Java 客户端访问 Cassandra 的 API,并且是希腊神话中的角色-卡桑德拉的兄弟。

Q.3. 完成本章提到的用例,并演示将数据填充到 Cassandra 中的端到端执行。

总结

在本章中,您已经涵盖了 NoSQL 的基础知识,特别是 Cassandra。您已经亲身体验了设置 Cassandra 集群,并了解了各种 API、驱动程序和协议,这些提供了对 Cassandra 的编程访问。我们还将 Cassandra 集成为我们的 Storm 拓扑的数据存储,用于数据插入。

在下一章中,我们将涉及 Cassandra 的一些重要方面,特别是一致性和可用性。

第七章:Cassandra 分区、高可用性和一致性

在本章中,你将了解 Cassandra 的内部,学习数据分区是如何实现的,你将了解 Cassandra 的键集分布上采用的哈希技术。我们还将深入了解复制以及它的工作原理,以及暗示的传递特性。我们将涵盖以下主题:

  • 数据分区和一致性哈希;我们将看一些实际例子

  • 复制、一致性和高可用性

一致性哈希

在你理解它在 Cassandra 中的含义和应用之前,让我们先了解一致性哈希作为一个概念。

一致性哈希按照其名称的概念工作——即哈希,正如我们所知,对于一个给定的哈希算法,相同的键将始终返回相同的哈希码——因此,这种方法在本质和实现上都是非常确定的。当我们将这种方法用于在集群中的节点之间进行分片或划分键时,一致性哈希是一种确定哪个节点存储在集群中的哪个节点的技术。

看一下下面的图表,理解一致性哈希的概念;想象一下下面图表中所描述的环代表 Cassandra 环,这里标记的节点是用字母标记的,实际上标记了要映射到环上的对象(倒三角形)。

一致性哈希

Cassandra 集群的一致性哈希

要计算对象所属的节点的所有权,只需要顺时针遍历,遇到下一个节点即可。跟随数据项(倒三角形)的节点就是拥有该对象的节点,例如:

  • 1属于节点A

  • 2属于节点B

  • 3属于节点C

  • 4属于节点C

  • 5属于节点D

  • 6属于节点E

  • 7属于节点F

  • 8属于节点H

  • 9属于节点H

所以你看,这使用简单的哈希来计算环中键的所有权,基于拥有的标记范围。

让我们看一个一致性哈希的实际例子;为了解释这一点,让我们以一个样本列族为例,其中分区键值是名称。

假设以下是列值数据:

名字 性别
Jammy M
Carry F
Jesse M
Sammy F

这是哈希映射的样子:

分区键 哈希值
Jim 2245462676723220000.00
Carol 7723358927203680000.00
Johnny 6723372854036780000.00
Suzy 1168604627387940000.00

假设我有四个节点,具有以下范围;数据将如何分布:

节点 起始范围 结束范围 分区键 哈希值
A 9223372036854770000.00 4611686018427380000.00 Jammy 6723372854036780000.00
B 4611686018427380000.00 1.00 Jesse 2245462676723220000.00
C 0.00 4611686018427380000.00 suzy 1168604627387940000.00
D 4611686018427380000.00 9223372036854770000.00 Carry 7723358927203680000.00

现在你已经理解了一致性哈希的概念,让我们来看看一个或多个节点宕机并重新启动的情况。

一个或多个节点宕机

我们目前正在看一个非常常见的情况,即我们设想一个节点宕机;例如,在这里我们捕捉到两个节点宕机:BE。现在会发生什么?嗯,没什么大不了的,我们会像以前一样按照相同的模式进行,顺时针移动以找到下一个活动节点,并将值分配给该节点。

所以在我们的情况下,分配将改变如下:

一个或多个节点宕机

在前面图中的分配如下:

  • 1属于A

  • 234属于C

  • 5属于D

  • 67属于F

  • 89属于H

一个或多个节点重新上线

现在让我们假设一个场景,节点 2 再次上线;那么接下来的情况与之前的解释相同,所有权将重新建立如下:

  • 1 属于 A

  • 2 属于 B

  • 34 属于 C

  • 5 属于 D

  • 67 属于 F

  • 89 属于 H

因此,我们已经证明了这种技术适用于所有情况,这就是为什么它被使用的原因。

Cassandra 中的复制和策略

复制意味着创建一个副本。这个副本使数据冗余,因此即使一个节点失败或宕机,数据也是可用的。在 Cassandra 中,您可以选择在创建 keyspace 的过程中指定复制因子,或者稍后修改它。在这种情况下需要指定的属性如下:

  • 复制因子:这是指定副本数量的数字值

  • 策略:这可以是简单策略或拓扑策略;这决定了在集群中的副本放置

在内部,Cassandra 使用行键在集群的各个节点上存储数据的副本或复制。复制因子 n 意味着数据在 n 个不同节点上有 n 个副本。复制有一些经验法则,它们如下:

  • 复制因子不应该大于集群中节点的数量,否则由于副本不足,Cassandra 将开始拒绝写入和读取,尽管复制因子将继续不间断地进行

  • 如果复制因子太小,那么如果一个奇数节点宕机,数据将永远丢失

Snitch 用于确定节点的物理位置,例如彼此的接近程度等,在大量数据需要复制和来回移动时具有价值。在所有这些情况下,网络延迟都起着非常重要的作用。Cassandra 目前支持的两种策略如下:

  • 简单:这是 Cassandra 为所有 keyspaces 提供的默认策略。它使用一个数据中心。它的操作非常简单直接;正如其名称所示,分区器检查键值对与节点范围的关系,以确定第一个副本的放置位置。然后,后续的副本按顺时针顺序放置在下一个节点上。因此,如果数据项 "A" 的复制因子为 "3",并且分区器根据键和所有权决定了第一个节点,那么在这个节点上,后续的副本将按顺时针顺序创建。

  • 网络:这是当我们的 Cassandra 集群分布在多个数据中心时使用的拓扑。在这里,我们可以规划我们的副本放置,并定义我们想要在每个数据中心放置多少副本。这种方法使数据地理冗余,因此在整个数据中心崩溃的情况下更加安全。在选择跨数据中心放置副本时,应考虑以下两个因素:

  • 每个数据中心都应该是自给自足的,以满足请求

  • 故障转移或崩溃情况

如果在一个数据中心中有 2 个数据副本,那么我们就有四份数据副本,每个数据中心对一节点故障有一份数据的容忍度,以保持一致性 ONE。如果在一个数据中心中有 3 个数据副本,那么我们就有六份数据副本,每个数据中心对多个节点故障有一份数据的容忍度,以保持一致性 ONE。这种策略也允许不对称复制。

Cassandra 一致性

正如我们在前面的章节中所说,Cassandra 最终变得一致,并遵循 CAP 定理的 AP 原则。一致性指的是 Cassandra 集群中所有数据副本的信息有多新。Cassandra 最终保证一致性。现在让我们仔细看一下;假设我有一个由五个节点组成的 Cassandra 集群,复制因子为 3。这意味着如果我有一个数据项 1,它将被复制到三个节点,比如节点 1、节点 2 和节点 3;假设这个数据的键是键 1。现在,如果要重写此键的值,并且在节点 1 上执行写操作,那么 Cassandra 会在内部将值复制到其他副本,即节点 2 和节点 3。但此更新是在后台进行的,不是立即的;这就是最终一致性的机制。

Cassandra 提供了向(读和写)客户端应用程序提供决定使用何种一致性级别来读取和写入数据存储的概念。

写一致性

让我们仔细检查一下 Cassandra 中的写操作。当在 Cassandra 中执行写操作时,客户端可以指定操作应执行的一致性级别。

这意味着,如果复制因子为x,并且使用一致性为y(其中 y 小于 x)执行写操作,那么 Cassandra 将在成功写入y个节点后,才向客户端返回成功的确认,并标记操作为完成。对于剩余的x-y个副本,数据将由 Cassandra 进程在内部传播和复制。

以下表格显示了各种一致性级别及其含义,其中ANY具有最高可用性和最低一致性的优势,而ALL提供最高一致性但最低可用性。因此,作为客户端,在决定选择哪种一致性之前,必须审查使用情况。以下是一张包含一些常见选项及其含义的表格:

一致性级别 含义
ANY 当数据写入至少一个节点时,写操作将返回成功,其中节点可以是副本节点或非副本节点
ONE 当数据写入至少一个副本节点时,写操作将返回成功
TWO 当数据写入至少两个副本节点时,写操作将返回成功
QUORUM 当数据写入副本节点的法定副本数(法定副本数为 n/2+1,n 为复制因子)时,写操作将返回成功
ALL 当数据写入所有副本节点时,写操作将返回成功

以下图表描述了在具有复制因子3和一致性2的四节点集群上的写操作:

写一致性

因此,正如您所看到的,写操作分为三个步骤:

  • 从客户端发出写操作

  • 写操作在副本 1上执行并完成

  • 写操作在副本 2上执行并完成

  • 当写操作成功完成时,向客户端发出确认

读一致性

读一致性类似于写一致性,它表示在将结果返回给查询 Cassandra 数据存储的客户端之前,应有多少副本响应或确认其与返回的数据的一致性。这意味着,如果在具有复制因子xN节点集群上,使用读一致性y(y 小于 x)发出读查询,则 Cassandra 将检查y个副本,然后返回结果。结果将根据使用最新数据来满足请求,并通过与每个列关联的时间戳进行验证。

以下Cassandra 查询语言CQL),使用四分一一致性从列族中获取数据如下:

SELECT * FROM mytable USING CONSISTENCY QUORUM WHERE name='shilpi';

CQL 的功能如下:

一致性级别 含义
ONE 读请求由最近的副本的响应服务
TWO 读请求由最近的两个副本中的一个最新响应服务
THREE 此级别从最近的三个副本返回最新的数据
QUORUM 读请求由大多数副本的最新响应服务
ALL 读请求由所有副本的最新响应服务

一致性维护功能

在前一节中,我们深入讨论了读取和写入一致性,清楚的一点是 Cassandra 在执行读取或写入操作时不提供或不努力实现总一致性;它根据客户端的一致性规范执行并完成请求。另一个特性是最终一致性,它强调了在幕后有一些魔法,保证最终所有数据将是一致的。现在这个魔法是由 Cassandra 内部的某些组件执行的,其中一些如下所述:

  • 读修复:此服务确保所有副本之间的数据是最新的。这样,行就是一致的,并且已经使用最新的值更新了所有副本。此操作由作业执行。Cassandra 正在运行以执行由协调员发出的读修复操作。

  • 反熵修复服务:此服务确保不经常读取的数据,或者当一个宕机的主机重新加入时,处于一致的状态。这是一个常规的集群维护操作。

  • 提示性交接:这是 Cassandra 上另一个独特而奇妙的操作。当执行写操作时,协调员向所有副本发出写操作,而不管指定的一致性,并等待确认。一旦确认计数达到操作的一致性上提到的值,线程就完成了,并且客户端被通知其成功。在剩余的副本上,使用提示性交接写入值。当一些节点宕机时,提示性交接方法是一个救世主。假设其中一个副本宕机,并且使用ANY的一致性执行写操作;在这种情况下,一个副本接受写操作并提示给当前宕机的相邻副本。当宕机的副本恢复时,然后从活动副本获取提示将值写回它们。

测验时间

Q.1. 判断以下陈述是真还是假:

  1. Cassandra 有一个默认的ALL一致性。

  2. QUORUM是提供最高可用性的一致性级别。

  3. Cassandra 使用一个 snitch 来识别节点的接近程度。

  4. Cassandra 的读写特性默认具有一致性级别 1。

Q.2. 填空:

  1. _______________ 用于确定节点的物理接近程度。

  2. _______________ 是提供最高可用性和最低可用性的一致性。

  3. _______________ 是确保宕机一段时间的节点正确更新为最新更改的服务。

Q.3. 执行以下用例以查看 Cassandra 的高可用性和复制:

  1. 创建一个四节点的 Cassandra 集群。

  2. 创建一个副本因子为 3 的键空间。

  3. 在这个键空间下的列族中添加一些数据。

  4. 尝试使用ALL在选择查询中使用读一致性来检索数据。

  5. 关闭一个节点上的 Cassandra 守护程序,并从其他三个活动节点重复第 4 步。

  6. 关闭一个节点上的 Cassandra 守护程序,并使用ANY的一致性从其他三个活动节点重复第 4 步。

  7. 关闭两个节点并使用ANY的写一致性更新现有值。

  8. 尝试使用ANY进行读取。

  9. 将宕机的节点恢复并从所有四个节点上使用一致性ALL执行read操作。

摘要

在本章中,您已经了解了 Cassandra 中的复制和数据分区的概念。我们还了解了复制策略和最终一致性的概念。本章末尾的练习是一个很好的实践练习,可以帮助您以实际方式理解本章涵盖的概念。

在下一章中,我们将讨论八卦协议、Cassandra 集群维护和管理特性。

第八章:Cassandra 管理和维护

在本章中,我们将学习 Cassandra 的八卦协议。然后,我们将深入了解 Cassandra 管理和管理,以了解扩展和可靠性的实际情况。这将使您能够处理您不希望遇到但在生产中确实发生的情况,例如处理可恢复节点、滚动重启等。

本章将涵盖以下主题:

  • Cassandra——八卦协议

  • Cassandra 扩展——向集群添加新节点

  • 替换节点

  • 复制因子更改

  • 节点工具命令

  • 滚动重启和容错

  • Cassandra 监控工具

因此,本章将帮助您了解 Cassandra 的基础知识,以及维护和管理 Cassandra 活动所需的各种选项。

Cassandra - 八卦协议

八卦是一种协议,其中节点定期与其他节点交换关于它们所知道的节点的信息;这样,所有节点都通过这种点对点通信机制获取关于彼此的信息。这与现实世界和社交媒体世界的八卦非常相似。

Cassandra 每秒执行一次这个机制,一个节点能够与集群中最多三个节点交换八卦信息。所有这些八卦消息都有与之关联的版本,以跟踪时间顺序,旧的八卦交互更新会被新的覆盖。

既然我们知道 Cassandra 的八卦在很高的层面上是什么样子,让我们更仔细地看看它,并了解这个多嘴的协议的目的。以下是通过实施这个协议所达到的两个广泛目的:

  • 引导

  • 故障场景处理——检测和恢复

让我们了解它们在实际行动中的意义以及它们对 Cassandra 集群的健康和稳定性的贡献。

引导

引导是在集群中触发的一个过程,当一个节点第一次加入环时。我们在Cassandra.yaml配置文件下定义的种子节点帮助新节点获取有关集群、环、密钥集和分区范围的信息。建议您在整个集群中保持类似的设置;否则,您可能会在集群内遇到分区。一个节点在重新启动后会记住它与哪些节点进行了八卦。关于种子节点还有一点要记住,那就是它们的目的是在引导时为节点提供服务;除此之外,它既不是单点故障,也不提供任何其他目的。

故障场景处理——检测和恢复

好吧,八卦协议是 Cassandra 自己有效地知道何时发生故障的方式;也就是说,整个环都通过八卦知道了一个宕机的主机。相反的情况是,当一个节点加入集群时,同样的机制被用来通知环中的所有节点。

一旦 Cassandra 检测到环中的节点故障,它就会停止将客户端请求路由到该节点——故障确实对集群的整体性能产生了一定影响。然而,除非我们有足够的副本以确保一致性提供给客户端,否则它永远不会成为阻碍。

关于八卦的另一个有趣事实是,它发生在各个层面——Cassandra 的八卦,就像现实世界的八卦一样,可能是二手或三手等等;这是间接八卦的表现。

节点的故障可能是实际的或虚拟的。这意味着节点可能由于系统硬件故障而实际失败,或者故障可能是虚拟的,即在一段时间内,网络延迟非常高,以至于似乎节点没有响应。后一种情况大多数情况下是自我恢复的;也就是说,一段时间后,网络恢复正常,节点再次在环中被检测到。活动节点会定期尝试对失败的节点进行 ping 和 gossip,以查看它们是否正常。如果要将节点声明为永久离开集群,我们需要一些管理员干预来明确地从环中删除节点。

当节点在相当长时间后重新加入集群时,可能会错过一些写入(插入/更新/删除),因此,节点上的数据远非根据最新数据状态准确。建议使用nodetool repair命令运行修复。

Cassandra 集群扩展-添加新节点

Cassandra 非常容易扩展,并且无需停机。这是它被选择而不是许多其他竞争者的原因之一。步骤非常简单明了:

  1. 您需要在要添加的节点上设置 Cassandra。但是先不要启动 Cassandra 进程;首先按照以下步骤操作:

  2. seed_provider下的Cassandra.yaml中更新种子节点。

  3. 确保tmp文件夹是干净的。

  4. Cassandra.yaml中添加auto_bootstrap并将其设置为true

  5. Cassandra.yaml中更新cluster_name

  6. 更新Cassandra.yaml中的listen_address/broadcast_address

  7. 逐个启动所有新节点,每两次启动之间至少暂停 5 分钟。

  8. 一旦节点启动,它将根据自己拥有的标记范围宣布其数据份额并开始流式传输。可以使用nodetoolnetstat命令进行验证,如下面的代码所示:

mydomain@my-cass1:/home/ubuntu$ /usr/local/cassandra/apache- cassandra-1.1.6/bin/nodetool -h 10.3.12.29 netstats | grep - v 0%
Mode: JOINING
Not sending any streams.
Streaming from: /10.3.12.179
my_keyspace:  /var/lib/cassandra/data/my_keyspace/mycf/my_keyspace-my-hf- 461279-Data.db sections=1  progress=2382265999194/3079619547748 - 77%
Pool Name                    Active   Pending      Completed
Commands                        n/a         0             33
Responses                       n/a         0       13575829
mydomain@my-cass1:/home/ubuntu$

  1. 在所有节点加入集群后,强烈建议在所有节点上运行nodetool cleanup命令。这是为了让它们放弃以前由它们拥有但现在属于已加入集群的新节点的键的控制。以下是命令和执行输出:
mydomain@my-cass3:/usr/local/cassandra/apache-cassandra- 1.1.6/bin$ sudo -bE ./nodetool -h 10.3.12.178 cleanup  my_keyspacemycf_index
mydomain@my-cass3:/usr/local/cassandra/apache-cassandra- 1.1.6/bin$ du -h   /var/lib/cassandra/data/my_keyspace/mycf_index/
53G  /var/lib/cassandra/data/my_keyspace/mycf_index/
mydomain@my-cass3:/usr/local/cassandra/apache-cassandra- 1.1.6/bin$ jps
27389 Jps
26893 NodeCmd
17925 CassandraDaemon

  1. 请注意,NodeCmd进程实际上是 Cassandra 守护程序的清理过程。在前一个节点上清理后回收的磁盘空间显示在这里:
Size before cleanup – 57G
Size after cleanup – 30G

Cassandra 集群-替换死节点

本节涵盖了可能发生并导致 Cassandra 集群故障的各种情况和场景。我们还将为您提供处理这些情况的知识并讨论相关步骤。这些情况特定于版本 1.1.6,但也适用于其他版本。

假设问题是这样的:您正在运行一个 n 节点,例如,假设有三个节点集群,其中一个节点宕机;这将导致不可恢复的硬件故障。解决方案是:用新节点替换死节点。

以下是实现解决方案的步骤:

  1. 使用nodetool ring命令确认节点故障:
bin/nodetool ring -h hostname

  1. 死节点将显示为DOWN;假设node3已宕机:
192.168.1.54 datacenter1rack1 Up  Normal 755.25 MB 50.00% 0
192.168.1.55 datacenter1rack1 Down Normal 400.62 MB 25.00%  42535295865117307932921825928971026432
192.168.1.56 datacenter1rack1 Up  Normal 793.06 MB 25.00%  85070591730234615865843651857942052864

  1. 在替换节点上安装和配置 Cassandra。确保使用以下命令从替换的 Cassandra 节点中删除旧安装(如果有):
sudorm -rf /var/lib/cassandra/*

在这里,/var/lib/cassandra是 Cassandra 的数据目录的路径。

  1. 配置Cassandra.yaml,使其具有与现有 Cassandra 集群相同的非默认设置。

  2. 在替换节点的cassandra.yaml文件中,将initial_token范围设置为死节点的标记 1 的值,即42535295865117307932921825928971026431

  3. 启动新节点将在环中死节点的前一个位置加入集群:

192.168.1.54 datacenter1rack1 Up    Normal 755.25 MB 50.00% 0
192.168.1.51 datacenter1rack1 Up    Normal 400.62 MB 0.00%  42535295865117307932921825928971026431
192.168.1.55 datacenter1rack1 Down     Normal 793.06 MB 25.00%  42535295865117307932921825928971026432
192.168.1.56 datacenter1rack1 Up    Normal 793.06 MB 25.00%  85070591730234615865843651857942052864

  1. 我们快要完成了。只需在每个 keyspace 的每个节点上运行nodetool repair
nodetool repair -h 192.168.1.54 keyspace_name -pr
nodetool repair -h 192.168.1.51 keyspace_name -pr
nodetool repair -h 192.168.1.56 keyspace_name–pr

  1. 使用以下命令从环中删除死节点的令牌:
nodetoolremovetoken 85070591730234615865843651857942052864

这个命令需要在所有剩余的节点上执行,以确保所有活动节点知道死节点不再可用。

  1. 这将从集群中删除死节点;现在我们完成了。

复制因子

偶尔,我们会遇到需要改变复制因子的情况。例如,我开始时使用较小的集群,所以将复制因子保持为 2。后来,我从 4 个节点扩展到 8 个节点,为了使整个设置更加安全,我将复制因子增加到 4。在这种情况下,需要按照以下步骤进行操作:

  1. 以下是用于更新复制因子和/或更改策略的命令。在 Cassandra CLI 上执行这些命令:
ALTER KEYSPACEmy_keyspace WITH REPLICATION = { 'class' :  'SimpleStrategy', 'replication_factor' : 4 };

  1. 一旦命令已更新,您必须依次在每个节点上执行nodetool修复,以确保所有键根据新的复制值正确复制:
sudo -bE ./nodetool -h 10.3.12.29 repair my_keyspacemycf -pr
6
mydomain@my-cass3:/home/ubuntu$ sudo -E  /usr/local/cassandra/apache-cassandra-1.1.6/bin/nodetool -h  10.3.21.29 compactionstats
pending tasks: 1
compaction type  keyspace         column family bytes  compacted      bytes total  progress
Validation       my_keyspacemycf  1826902206  761009279707   0.24%
Active compaction remaining time :        n/a
mydomain@my-cass3:/home/ubuntu$

以下compactionstats命令用于跟踪nodetool repair命令的进度。

nodetool 命令

Cassandra 中的nodetool命令是 Cassandra 管理员手中最方便的工具。它具有所有类型的节点各种情况处理所需的工具和命令。让我们仔细看看一些广泛使用的命令:

  • Ring:此命令描述节点的状态(正常、关闭、离开、加入等)。令牌范围的所有权和键的百分比所有权以及数据中心和机架详细信息如下:
bin/nodetool -host 192.168.1.54 ring

输出将类似于以下内容:

192.168.1.54 datacenter1rack1 Up    Normal 755.25 MB 50.00% 0
192.168.1.51 datacenter1rack1 Up    Normal 400.62 MB 0.00%  42535295865117307932921825928971026431
192.168.1.55 datacenter1rack1 Down    Normal 793.06 MB 25.00%  42535295865117307932921825928971026432
192.168.1.56 datacenter1rack1 Up    Normal 793.06 MB 25.00%  85070591730234615865843651857942052864

  • Join:这是您可以与nodetool一起使用的选项,需要执行以将新节点添加到集群中。当新节点加入集群时,它开始从其他节点流式传输数据,直到根据环中的令牌确定的所有键都到达其指定的所有权。可以使用netsat命令检查此状态:
mydomain@my-cass3:/home/ubuntu$ /usr/local/cassandra/apache- cassandra-1.1.6/bin/nodetool -h 10.3.12.29 netstats | grep - v 0%
Mode: JOINING
Not sending any streams.
Streaming from: /10.3.12.179
my_keyspace:  /var/lib/cassandra/data/my_keyspace/mycf/my_keyspace-mycf- hf-46129-Data.db sections=1  progress=238226599194/307961954748 - 77%
Pool Name                    Active   Pending      Completed
Commands                        n/a         0             33
Responses                       n/a         0       13575829

  • Info:此nodetool选项获取有关以下命令指定的节点的所有必需信息:
bin/nodetool -host 10.176.0.146 info
Token(137462771597874153173150284137310597304)
Load Info        : 0 bytes.
Generation No    : 1
Uptime (seconds) : 697595
Heap Memory (MB) : 28.18 / 759.81

  • Cleanup:这通常是在扩展集群时使用的选项。添加新节点,因此现有节点需要放弃现在属于集群中新成员的键的控制权:
mydomain@my-cass3:/usr/local/cassandra/apache-cassandra- 1.1.6/bin$ sudo -bE ./nodetool -h 10.3.12.178 cleanup  my_keyspacemycf_index
mydomain@my-cass3:/usr/local/cassandra/apache-cassandra- 1.1.6/bin$ du -h  /var/lib/cassandra/data/my_keyspace/mycf_index/
53G  /var/lib/cassandra/data/my_keyspace/mycf_index/
aeris@nrt-prod-cass3-C2:/usr/local/cassandra/apache-cassandra- 1.1.6/bin$ sudo `which jps
27389 Jps
26893 NodeCmd
17925 CassandraDaemon
mydomain@my-cass3:/usr/local/cassandra/apache-cassandra- 1.1.6/bin$ du -h  /var/lib/cassandra/data/my_keyspace/mycf_index/
53G  /var/lib/cassandra/data/my_keyspace/mycf_index/

  • Compaction:这是最有用的工具之一。它用于明确向 Cassandra 发出compact命令。这可以在整个节点、键空间或列族级别执行:
sudo -bE /usr/local/cassandra/apache-cassandra- 1.1.6/bin/nodetool -h 10.3.1.24 compact
mydomain@my-cass3:/home/ubuntu$ sudo -E  /usr/local/cassandra/apache-cassandra-1.1.6/bin/nodetool -h  10.3.1.24 compactionstats
pending tasks: 1
compaction type keyspace column family bytes compacted bytes  total progress
Compaction my_keyspacemycf 1236772 1810648499806 0.00%
Active compaction remaining time:29h58m42s
mydomain@my-cass3:/home/ubuntu$

Cassandra 有两种类型的压缩:小压缩和大压缩。小压缩周期在创建新的sstable数据时执行,以删除所有墓碑(即已删除的条目)。

主要压缩是手动触发的,使用前面的nodetool命令。这可以应用于节点、键空间和列族级别。

  • Decommission:这在某种程度上是引导的相反,当我们希望节点离开集群时触发。一旦活动节点接收到命令,它将停止接受新的权限,刷新memtables,并开始从自身流式传输数据到将成为当前拥有键范围的新所有者的节点:
bin/nodetool -h 192.168.1.54 decommission

  • Removenode:当节点死亡,即物理不可用时,执行此命令。这通知其他节点节点不可用。Cassandra 复制开始工作,通过根据新的环所有权创建数据的副本来恢复正确的复制:
bin/nodetoolremovenode<UUID>
bin/nodetoolremovenode force

  • 修复:执行此nodetool repair命令以修复任何节点上的数据。这是确保数据一致性以及在一段时间后重新加入集群的节点存在的非常重要的工具。假设有一个由四个节点组成的集群,这些节点通过风暴拓扑不断进行写入。在这里,其中一个节点下线并在一两个小时后重新加入环。现在,在此期间,该节点可能错过了一些写入;为了修复这些数据,我们应该在节点上执行repair命令:
bin/nodetool repair

Cassandra 容错

使用 Cassandra 作为数据存储的主要原因之一是其容错能力。它不是由典型的主从架构驱动的,其中主节点的故障成为系统崩溃的单一点。相反,它采用环模式的概念,因此没有单一故障点。在需要时,我们可以重新启动节点,而不必担心将整个集群带下线;在各种情况下,这种能力都非常方便。

有时需要重新启动 Cassandra,但 Cassandra 的环架构使管理员能够在不影响整个集群的情况下无缝进行此操作。这意味着在需要重新启动 Cassandra 集群的情况下,例如需要逐个重新启动节点而不是将整个集群带下线然后重新启动的情况下,Cassandra 管理员可以逐个重新启动节点:

  • 使用内存配置更改启动 Cassandra 守护程序

  • 在已运行的 Cassandra 集群上启用 JMX

  • 有时机器需要例行维护和重新启动

Cassandra 监控系统

现在我们已经讨论了 Cassandra 的各种管理方面,让我们探索 Cassandra 集群的各种仪表板和监控选项。现在有各种免费和许可的工具可用,我们将在下面讨论。

JMX 监控

您可以使用基于jconsole的一种监控 Cassandra 的类型。以下是使用jconsole连接到 Cassandra 的步骤:

  1. 在命令提示符中,执行jconsole命令:JMX 监控

  2. 在下一步中,您必须指定 Cassandra 节点的 IP 和端口以进行连接:JMX 监控

  3. 一旦连接,JMX 提供各种图形和监控实用程序:JMX 监控

开发人员可以使用 jconsole 的内存选项卡监视堆内存使用情况。这将帮助您了解节点资源的利用情况。

jconsole 的限制在于它执行特定于节点的监控,而不是基于 Cassandra 环的监控和仪表板。让我们在这个背景下探索其他工具。

Datastax OpsCenter

这是一个由 Datastax 提供的实用程序,具有图形界面,可以让用户从一个中央仪表板监视和执行管理活动。请注意,免费版本仅适用于非生产用途。

Datastax Ops Center 为各种重要的系统关键性能指标KPI)提供了许多图形表示,例如性能趋势、摘要等。其用户界面还提供了对单个数据点的历史数据分析和深入分析能力。OpsCenter 将其所有指标存储在 Cassandra 本身中。OpsCenter 实用程序的主要特点如下:

  • 基于 KPI 的整个集群监控

  • 警报和报警

  • 配置管理

  • 易于设置

您可以使用以下简单步骤安装和设置 OpsCenter:

  1. 运行以下命令开始:
$ sudo service opscenterd start

  1. 在 Web 浏览器中连接到 OpsCenter,网址为http://localhost:8888

  2. 您将获得一个欢迎屏幕,在那里您将有选项生成一个新集群或连接到现有集群。

  3. 接下来,配置代理;一旦完成,OpsCenter 即可使用。

这是应用程序的屏幕截图:

Datastax OpsCenter

在这里,我们选择要执行的度量标准以及操作是在特定节点上执行还是在所有节点上执行。以下截图捕捉了 OpsCenter 启动并识别集群中的各个节点的情况:

Datastax OpsCenter

以下截图捕捉了集群读写、整体集群延迟、磁盘 I/O 等方面的各种关键绩效指标:

Datastax OpsCenter

测验时间

Q.1. 判断以下陈述是真还是假。

  1. Cassandra 存在单点故障。

  2. Cassandra 环中立即检测到死节点。

  3. Gossip 是一种数据交换协议。

  4. decommissionremovenode命令是相同的。

Q.2. 填空。

  1. _______________ 是运行压缩的命令。

  2. _______________ 是获取有关活动节点信息的命令。

  3. ___________ 是显示整个集群信息的命令。

Q.3. 执行以下用例以查看 Cassandra 的高可用性和复制:

  1. 创建一个 4 节点的 Cassandra 集群。

  2. 创建一个副本因子为 3 的键空间。

  3. 关闭一个节点上的 Cassandra 守护程序。

  4. 在每个节点上执行nestat以查看数据流。

总结

在本章中,您了解了疏散协议的概念和用于各种场景的适应工具,例如扩展集群、替换死节点、压缩和修复 Cassandra 上的操作。

在下一章中,我们将讨论风暴集群的维护和运营方面。

第九章:风暴管理和维护

在本章中,您将了解 Storm 集群的扩展。您还将看到如何调整 Storm 拓扑的工作节点和并行性。

我们将涵盖以下主题:

  • 添加新的监督员节点

  • 设置工作节点和并行性以增强处理

  • 故障排除

扩展 Storm 集群-添加新的监督员节点

在生产中,最常见的情况之一是处理需求超过了集群的大小。此时需要进行扩展;有两种选择:我们可以进行垂直扩展,在其中可以添加更多的计算能力,或者我们可以使用水平扩展,在其中添加更多的节点。后者更具成本效益,也使集群更加健壮。

以下是要执行的步骤,以将新节点添加到 Storm 集群中:

  1. 下载并安装 Storm 的 0.9.2 版本,因为它是集群中其余部分使用的,通过解压下载的 ZIP 文件。

  2. 创建所需的目录:

sudo mkdir –p /usr/local/storm/tmp

  1. 所有 Storm 节点、Nimbus 节点和监督员都需要一个位置来存储与本地磁盘上的配置相关的少量数据。请确保在所有 Storm 节点上创建目录并分配读/写权限。

  2. 创建日志所需的目录,如下所示:

sudo mkdir –p /mnt/app_logs/storm/storm_logs

  1. 更新storm.yaml文件,对 Nimbus 和 Zookeeper 进行必要的更改:
#storm.zookeeper.servers: This is a list of the hosts in the  Zookeeper cluster for Storm cluster
storm.zookeeper.servers: 
  - "<IP_ADDRESS_OF_ZOOKEEPER_ENSEMBLE_NODE_1>"
  - "<IP_ADDRESS_OF_ZOOKEEPER_ENSEMBLE_NODE_2>"
#storm.zookeeper.port: Port on which zookeeper cluster is running.
  storm.zookeeper.port: 2182
#For our installation, we are going to create this directory in  /usr/local/storm/tmp location.
storm.local.dir: "/usr/local/storm/tmp"
#nimbus.host: The nodes need to know which machine is the #master  in order to download topology jars and confs. This #property is  used for the same purpose.
nimbus.host: "<IP_ADDRESS_OF_NIMBUS_HOST>"
#storm.messaging.netty configurations: Storm's Netty-based  #transport has been overhauled to significantly improve  #performance through better utilization of thread, CPU, and  #network resources, particularly in cases where message sizes  #are small. In order to provide netty support, following  #configurations need to be added :
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

监督员端口的插槽值如下:

supervisor.slots.ports
- 6700
- 6701
- 6702
- 6703
  1. ~/.bashrc文件中设置STORM_HOME环境,并将 Storm 的bin目录添加到PATH环境变量中。这样可以从任何位置执行 Storm 二进制文件。要添加的条目如下:
STORM_HOME=/usr/local/storm
PATH=$PATH:$STORM_HOME/bin

  1. 在以下每台机器和节点上更新/etc/hosts
  • nimbus 机器:这是为了为正在添加的新监督员添加条目

  • 所有现有的监督员机器:这是为了为正在添加的新监督员添加条目

  • 新的监督员节点:这是为了添加 nimbus 条目,为所有其他监督员添加条目,并为 Zookeeper 节点添加条目

sup-flm-1.mydomain.com host:
10.192.206.160    sup-flm-2\. mydomain.net
10.4.27.405       nim-zkp-flm-3\. mydomain.net

一旦监督员被添加,启动进程,它应该在 UI 上可见,如下面的截图所示:

扩展 Storm 集群-添加新的监督员节点

请注意,前面截图中的第一行指向新添加的监督员;它总共有 16 个插槽,目前使用0个插槽,因为它刚刚添加到集群中。

扩展 Storm 集群和重新平衡拓扑

一旦添加了新的监督员,下一个明显的步骤将是重新平衡在集群上执行的拓扑,以便负载可以在新添加的监督员之间共享。

使用 GUI 重新平衡

重新平衡选项在 Nimbus UI 上可用,您可以选择要重新平衡的拓扑,然后使用 GUI 中的选项。拓扑会根据指定的超时时间排空。在此期间,它停止接受来自 spout 的任何消息,并处理内部队列中的消息,一旦完全清除,工作节点和任务将重新分配。用户还可以使用重新平衡选项增加或减少各种螺栓和 spout 的并行性。以下截图描述了如何使用 Storm UI 选项重新平衡拓扑:

使用 GUI 重新平衡

使用 CLI 重新平衡

重新平衡的第二个选项是使用 Storm CLI。其命令如下:

storm rebalance mystormtopology -n 5 -e my-spout=3 -e my-bolt=10

在这里,-n指定了重新平衡后分配给拓扑的工作器数量,-e my-spout指的是分配给 spout 的并行性,同样-e my-bolt指的是要分配给螺栓的并行性。在前面的命令中,我们从 Storm 安装 JAR 的bin目录下执行了 Storm shell,并在重新平衡 Storm 拓扑时同时改变了 spout 和螺栓的并行性。

可以从 Storm UI 验证对前面命令的执行更改。

设置工作器和并行性以增强处理

Storm 是一个高度可扩展、分布式和容错的实时并行处理计算框架。请注意,重点是可扩展性、分布式和并行处理——好吧,我们已经知道 Storm 以集群模式运行,因此在基本性质上是分布式的。可扩展性在前一节中已经涵盖了;现在,让我们更仔细地看看并行性。我们在早些时候的章节中向您介绍了这个概念,但现在我们将让您了解如何调整它以实现所需的性能。以下几点是实现这一目标的关键标准:

  • 拓扑在启动时被分配了一定数量的工作器。

  • 拓扑中的每个组件(螺栓和 spout)都有指定数量的执行者与之关联。这些执行者指定了拓扑的每个运行组件的并行性数量或程度。

  • Storm 的整体效率和速度因素都受 Storm 的并行性特性驱动,但我们需要明白一件事:所有归因于并行性的执行者都在拓扑分配的有限工作器集合内运行。因此,需要理解增加并行性只能在一定程度上提高效率,但超过这一点后,执行者将争夺资源。超过这一点增加并行性将无法提高效率,但增加分配给拓扑的工作器将使计算更加高效。

在效率方面,另一个需要理解的点是网络延迟;我们将在接下来的部分中探讨这一点。

场景 1

以下图示了一个简单的拓扑,有三个移动组件:一个 spout 和两个螺栓。在这里,所有组件都在集群中的不同节点上执行,因此每个元组必须经过两次网络跳转才能完成执行。

场景 1

假设我们对吞吐量不满意,并决定增加并行性。一旦我们尝试采用这种技术,就会出现一个问题,即在哪里增加以及增加多少。这可以根据螺栓的容量来计算,这应该可以从 Storm UI 中看到。以下截图说明了这一点:

场景 1

在这里,圈出的值是第二个螺栓的容量,大约为 0.9,已经是红色的,这意味着这个螺栓超负荷工作,增加并行性应该有所帮助。任何拓扑实际上都会在螺栓容量超过1时中断并停止确认。为了解决这个问题,让我们看看下一个场景,为这个问题提供一个解决方案。

场景 2

在这里,我们已经意识到Bolt B超负荷,并增加了并行性,如下图所示:

场景 2

前面的图描述了一个场景,捕捉了集群中不同节点上各种螺栓和 spout 实例的分布。在这里,我们已经意识到一个螺栓超负荷,并观察了容量,通过强制手段,只增加了该螺栓的并行性。

现在,做到了这一点,我们已经实现了所需的并行性;现在让我们来看看网络延迟,即元组在节点之间移动的数量(节点间通信是分布式计算设置中的一个必要元素):

  • 50%的流量在Machine 1Machine 2之间跳转

  • 50%的流量在Machine 1Machine 3之间跳转

  • 100%的流量在Machine 2Machine 3之间跳转

现在让我们看另一个示例,稍微改变并行性。

场景 3

场景 3 是在示例设置中可能出现的最佳场景,我们可以非常有效地使用网络和并行性,如下图所示:

场景 3

现在,上图是一个示例,展示了我们如何最大程度地利用并行性。如果您看一下上图,您会发现我们已经实现了效率,没有网络跳数;两全其美。

我试图说明的是,并行性应该在考虑网络延迟、跳数和本地处理速度的影响下进行审慎更改。

Storm 故障排除

作为开发人员,我们需要接受现实,事情确实会出错,需要调试。本节将使您能够有效和高效地处理这种情况。首先要理解编程世界的两个根本口诀:

  • 假设一切可能出问题的地方都会出问题

  • 任何可能出现问题的地方都可以修复

接受现实,首先通过了解可能出现问题的地方,然后清楚地了解我们应该从哪里开始分析,以帮助我们处理 Storm 集群中的任何情况。让我们了解一下各种指针,显示出问题,并引导我们找到潜在的解决方案。

Storm UI

首先,让我们了解 UI 本身存在哪些统计数据和指标。最新的 UI 有大量指标,让我们洞悉集群中正在发生的事情以及可能出现问题的地方(以防出现故障)。

让我们看一下 Storm UI,其中Cluster Summary包括,例如,http:// nimbus 的 IP:8080在我的情况下是http://10.4.2.122:8080,我的 UI 进程在具有此 IP 的 nimbus 机器上执行:10.4.2.122。

Storm UI

在前面的屏幕截图中,我们可以看到以下参数:

  • 使用的 Storm 版本在第一列中。

  • Nimbus 的正常运行时间(第二列)告诉我们自上次重启以来 Nimbus 节点已经运行了多长时间。正如我们所知,Nimbus 只在拓扑被提交时或监督者或工作人员下线并且任务再次被委派时才需要。在拓扑重平衡期间,Nimbus 也是必需的。

  • 第三列给出了集群中监督者的数量。

  • 第四、五和六列显示了 Storm 监督者中已使用的工作槽的数量、空闲工作槽的数量和工作槽的总数。这是一个非常重要的统计数据。在任何生产级别的集群中,应该始终为一些工作人员下线或一两个监督者被杀死做好准备。因此,我建议您始终在集群上有足够的空闲槽,以容纳这种突发故障。

  • 第七列和第八列指定了拓扑中正在移动的任务,即系统中运行的任务和执行者的数量。

让我们看一下 Storm UI 开启页面上的第二部分;这部分捕获了拓扑摘要:

Storm UI

本节描述了 Storm 在拓扑级别捕获和显示的各种参数:

  • 第一列和第二列分别显示了拓扑的Name字段和拓扑的Id字段。

  • 第三列显示了拓扑的状态,对于正在执行和处理的拓扑来说,状态是ACTIVE

  • 第四列显示了自拓扑启动以来的正常运行时间。

  • 接下来的三列显示NumworkersNum tasksNum executors;这些是拓扑性能的非常重要的方面。在调整性能时,人们必须意识到仅仅增加Num tasksNum executors字段的值可能不会导致更高的效率。如果工作人员的数量很少,而我们只增加执行器和任务的数量,那么由于工作人员数量有限,资源的匮乏会导致拓扑性能下降。

同样,如果我们将太多的工作人员分配给一个拓扑结构,而没有足够的执行器和任务来利用所有这些工作人员,我们将浪费宝贵的资源,因为它们被阻塞和空闲。

另一方面,如果我们有大量的工作人员和大量的执行器和任务,那么由于网络延迟,性能可能会下降。

在陈述了这些事实之后,我想强调性能调优应该谨慎和审慎地进行,以确定适用于我们正在尝试实施的用例的数量。

以下截图捕获了有关监督者的详细信息,以及相应信息的统计数据:

The Storm UI

  • 第一列是Id字段,用于监督者,第二列是运行监督者进程的hosts字段的名称。

  • 第三列显示了监督者运行的时间。

  • 第五列和第六列分别捕获了监督者上可用插槽的数量和已使用的插槽的数量。这两个数字在判断和理解监督者的运行容量以及它们处理故障情况的带宽方面提供了非常重要的指标;例如,我的所有监督者都以 100%的容量运行,所以在这种情况下,我的集群无法处理任何故障。

以下截图是从 Storm UI 中捕获的,显示了监督者及其属性:

The Storm UI

前面的部分为我们提供了有关监督者插槽、超时等的详细信息。这些值在storm.yaml中指定,但可以从 UI 中验证。例如,在我的情况下,http:// nimbus 的 IP:8080http://10.4.2.122:8080,我的 UI 进程在具有此 IP 的 Nimbus 机器上执行:10.4.2.122,如下图所示:

The Storm UI

现在,在下面的截图所示的部分中,可以通过在 Storm UI 上单击任何拓扑名称来深入了解拓扑详细信息。这一部分包含了有关拓扑组件的详细信息,包括螺栓、喷口的级别以及有关它们的详细信息,如下图所示:

The Storm UI

前面的截图显示了有关每个组件分配的执行器或任务数量,以及螺栓或喷口发射的元组数量以及传输到有向无环图DAG)中下一个组件的元组数量。

拓扑详细页面上应该注意的其他重要细节如下:

  • 过去 10 分钟内螺栓的容量:这个值应该远低于 1。

  • 执行延迟以毫秒为单位:这决定了通过该组件执行元组所需的时间。如果这个值太高,那么我们可能希望将执行分成两个或更多的螺栓,以利用并行性并提高效率。

  • 已执行:这个值存储了该组件成功执行的元组数量。

  • 处理延迟:这个值显示了组件执行元组所需的平均总时间。这个值应该与执行延迟一起分析。以下是可能发生的实际情况:

  • 执行延迟处理延迟都很低(这是最理想的情况)

  • 执行延迟很低,但处理延迟非常高(这意味着实际执行时间较短,与总执行时间相比较高,并且增加并行性可能有助于提高效率)

  • 执行延迟处理延迟都很高(再次增加并行性可能有所帮助)

Storm 日志

如果事情不如预期,下一个调试的地方就是 Storm 日志。首先,需要知道 Storm 日志的位置,还需要在cluster.xmlstorm-0.9.2-incubating.zip\apache-storm-0.9.2-incubating\logback\cluster.xml中更新路径:

<appender class="ch.qos.logback.core.rolling.RollingFileAppender"  name="A1">
  <!—update this as below  <file>${storm.home}/logs/${logfile.name}</file> -->
 <file>/mnt/app_logs/storm/storm_logs/${logfile.name}</file>
  <rollingPolicy  class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
    <fileNamePattern>${storm.home}/logs/${logfile.name}.%i </fileNamePattern>
    <minIndex>1</minIndex>
    <maxIndex>9</maxIndex>
</rollingPolicy>
<triggeringPolicy  class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
    <maxFileSize>100MB</maxFileSize>
</triggeringPolicy>
  <encoder>
    <pattern>%d{yyyy-MM-dd HH:mm:ss} %c{1} [%p] %m%n</pattern>
  </encoder>
</appender>

现在粗体字的那一行会告诉你 Storm 日志将被创建的路径/位置。让我们仔细看看不同 Storm 守护程序创建了哪些类型的日志。

可以使用以下命令在 shell 上获取 Nimbus 节点日志:

Cd /mnt/my_logs/strom/storm_logs
ls-lart

Nimbus 日志目录的列表如下截图所示:

Storm logs

注意我们有nimbus.log,其中包含有关 Nimbus 启动、错误和信息日志的详细信息;ui.log是在启动 Storm UI 应用程序的节点上创建的。

可以使用以下命令在 shell 上获取监督者节点的日志:

Cd /mnt/my_logs/strom/storm_logs
ls-lart

监督者日志目录的列表如下截图所示:

Storm logs

可以查看监督者日志和工作日志。监督者日志记录了监督者启动的详细信息,任何错误等。工作日志是开发人员拓扑日志和各种螺栓和喷口的 Storm 日志所在的地方。

因此,如果我们想要调试 Storm 守护进程,我们会查看nimbus.logsupervisor.log。如果你遇到问题,那么你需要使用相应的工作日志进行调试。Nimbus 和工作节点故障的情况已在第四章中进行了介绍,集群模式下的 Storm

现在让我们想象一个场景。我是一个开发人员,我的拓扑结构表现不如预期,我怀疑其中一个螺栓的功能不如预期。因此,我们需要调试工作日志并找出根本原因。现在我们需要找出多个监督者和众多工作日志中要查看哪个工作日志;我们将从 Storm UI 中获取这些信息。执行以下步骤:

  1. 打开Storm UI并点击有问题的拓扑。

  2. 点击拓扑的疑似螺栓或喷口。屏幕上会出现与此截图相似的内容:Storm logs

这是调试这个螺栓发生的情况的线索;我将查看Supervisor5Supervisor6supervisor5supervisor6上的worker-6705.log

测验时间

Q.1. 判断以下陈述是真还是假:

  1. 在执行拓扑的情况下,无法将 Storm 节点添加到集群中。

  2. 拓扑无法在 Storm 节点故障时生存。

  3. Storm 日志在集群中的每个节点上创建。

  4. Storm 日志创建的位置是可配置的。

Q.2. 填空:

  1. _______________ 是集群的心跳跟踪器。

  2. _______________ 是拓扑提交和重平衡所必需的守护程序。

  3. ___________ 文件保存了拓扑的工作配置。

Q.3. 执行以下用例以查看 Storm 的内部情况:

  1. 启动 Nimbus 并检查nimbus.log,查看成功启动的情况。

  2. 启动监督者并检查Supervisor.log,查看成功启动的情况。

  3. 提交拓扑,比如一个简单的WordCount拓扑,并找出worker.log文件的创建情况。

  4. 更新log4j.properties以更改日志级别并验证其影响。

摘要

在本章中,我们已经涵盖了 Storm 的维护概念,包括添加新节点、重新平衡和终止拓扑。我们已经了解并调整了诸如numtasks和并行性与numworkers和网络延迟相结合的内部机制。您学会了定位和解读 Storm 组件的日志。您还了解了 Storm UI 的指标及其对拓扑性能的影响。

在下一章中,我们将讨论 Storm 的高级概念,包括微批处理和 Trident API。

第十章:风暴中的高级概念

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

  • 构建 Trident 拓扑

  • 理解 Trident API

  • 示例和插图

在本章中,我们将学习事务性拓扑和 Trident API。我们还将探讨微批处理的方面以及它在 Storm 拓扑中的实现。

构建 Trident 拓扑

Trident 为 Storm 计算提供了批处理边缘。它允许开发人员在 Storm 框架上使用抽象层进行计算,从而在分布式查询中获得有状态处理和高吞吐量的优势。

嗯,Trident 的架构与 Storm 相同;它是建立在 Storm 之上的,以在 Storm 之上添加微批处理功能和执行类似 SQL 的函数的抽象层。

为了类比,可以说 Trident 在概念上很像 Pig 用于批处理。它支持连接、聚合、分组、过滤、函数等。

Trident 具有基本的批处理功能,例如一致处理和对元组的执行逻辑进行一次性处理。

现在要理解 Trident 及其工作原理;让我们看一个简单的例子。

我们选择的例子将实现以下功能:

  • 对句子流进行单词计数(标准的 Storm 单词计数拓扑)

  • 用于获取一组列出的单词计数总和的查询实现

这是解剖的代码:

FixedBatchSpout myFixedspout = new FixedBatchSpout(new  Fields("sentence"), 3,
new Values("the basic storm topology do a great job"),
new Values("they get tremendous speed and guaranteed processing"),
new Values("that too in a reliable manner "),
new Values("the new trident api over storm gets user more features  "),
new Values("it gets micro batching over storm "));
myFixedspout.setCycle(true);
myFixedspout cycles over the set of sentences added as values. This snippet ensures that we have an endless flow of data streams into the topology and enough points to perform all micro-batching functions that we intend to.

现在我们已经确保了连续的输入流,让我们看下面的片段:

//creating a new trident topology
TridentTopology myTridentTopology = new TridentTopology();
//Adding a spout and configuring the fields and query 
TridentState myWordCounts = topology.newStream("myFixedspout",  spout)
  .each(new Fields("sentence"), new Split(), new Fields("word"))
  .groupBy(new Fields("word"))
  .persistentAggregate(new MemoryMapState.Factory(), new Count(),  new Fields("count"))
  .parallelismHint(6);
Now the micro-batching; who does it and how? Well the Trident framework stores the state for each source (it kind of remembers what input data it has consumed so far). This state saving is done in the Zookeeper cluster. The tagging *spout* in the preceding code is actually a znode, which is created in the Zookeeper cluster to save the state metadata information.

这些元数据信息存储在小批处理中,其中批处理大小是根据传入元组的速度变化的变量;它可以是几百到数百万个元组,具体取决于每秒的事件事务数tps)。

现在我的喷口读取并将流发射到标记为sentence的字段中。在下一行,我们将句子分割成单词;这正是我们在前面提到的wordCount拓扑中部署的相同功能。

以下是捕捉split功能工作的代码上下文:

public class Split extends BaseFunction {
  public void execute(TridentTuple tuple, TridentCollector  collector) {
      String sentence = tuple.getString(0);
      for(String word: sentence.split(" ")) {
          collector.emit(new Values(word));
      }
  }
}
Trident with Storm is so popular because it guarantees the processing of all tuples in a fail-safe manner in exactly one semantic. In situations where retry is necessary because of failures, it does that exactly once and once only, so as a developer I don't end up updating the table storage multiple times on occurrence of a failure.

在前面的代码片段中,我们使用myTridentTopology创建了一个 DRPC 流,此外,我们还有一个名为word的函数。

  • 我们将参数流分割成其组成的单词;例如,我的参数storm trident topology被分割成诸如stormtridenttopology等单词* 然后,传入的流被按word分组* 接下来,状态查询操作符用于查询由拓扑的第一部分生成的 Trident 状态对象:

  • 状态查询接收拓扑先前部分计算的单词计数。

  • 然后它执行作为 DRPC 请求的一部分指定的函数来查询数据。

  • 在这种情况下,我的拓扑正在执行查询的MapGet函数,以获取每个单词的计数;在我们的情况下,DRPC 流以与拓扑前一部分中的TridentState完全相同的方式分组。这种安排确保了每个单词的所有计数查询都被定向到TridentState对象的相同 Trident 状态分区,该对象将管理单词的更新。

  • FilterNull确保没有计数的单词被过滤掉* 然后求和聚合器对所有计数求和以获得结果,结果会自动返回给等待的客户端

在理解开发人员编写的代码执行之后,让我们看看 Trident 的样板文件以及当这个框架执行时自动发生的事情。

  • 在我们的 Trident 单词计数拓扑中有两个操作,它们从状态中读取或写入——persistentAggregatestateQuery。Trident 具有自动批处理这些操作的能力,以便将它们批处理到状态。例如,当前处理需要对数据库进行 10 次读取和写入;Trident 会自动将它们一起批处理为一次读取和一次写入。这为您提供了性能和计算的便利,优化由框架处理。

  • Trident 聚合器是框架的其他高效和优化组件。它们不遵循将所有元组传输到一台机器然后进行聚合的规则,而是通过在可能的地方执行部分聚合,然后将结果传输到网络来优化计算,从而节省网络延迟。这里采用的方法类似于 MapReduce 世界中的组合器。

理解 Trident API

Trident API 支持五大类操作:

  • 用于操作本地数据分区的操作,无需网络传输

  • 与流重新分区相关的操作(涉及通过网络传输流数据)

  • 流上的数据聚合(此操作作为操作的一部分进行网络传输)

  • 流中字段的分组

  • 合并和连接

本地分区操作

正如其名称所示,这些操作在每个节点上对批处理进行本地操作,不涉及网络流量。以下功能属于此类别。

函数

  • 此操作接受单个输入值,并将零个或多个元组作为输出发射

  • 这些函数操作的输出附加到原始元组的末尾,并发射到流中

  • 在函数不发射输出元组的情况下,框架也会过滤输入元组,而在其他情况下,输入元组会被复制为每个输出元组

让我们通过一个示例来说明这是如何工作的:

public class MyLocalFunction extends BaseFunction {
  public void execute(TridentTuple myTuple, TridentCollector  myCollector) {
      for(int i=0; i < myTuple.getInteger(0); i++) {
          myCollector.emit(new Values(i));
      }
  }
}

现在假设,变量myTridentStream中的输入流具有以下字段["a","b","c"],流中的元组如下所示:

[10, 2, 30]
[40, 1, 60]
[30, 0, 80]
mystream.each(new Fields("b"), new MyLocalFunction(), new  Fields("d")))

这里期望的输出是根据函数应该返回["a","b","c","d"],所以对于流中的前面的元组,我将得到以下输出:

//for input tuple [10, 2, 30] loop in the function executes twice  //value of b=2
[10, 2, 30, 0]
[10, 2, 30, 1]
//for input tuple [4, 1, 6] loop in the function executes once  value //of b =1
[4, 1, 6, 0]
//for input tuple [3, 0, 8]
//no output because the value of field b is zero and the for loop  //would exit in first iteration itself value of b=0

过滤器

过滤器并非名不副实;它们的执行与其名称所示完全相同:它们帮助我们决定是否保留元组,它们确切地做到了过滤器的作用,即根据给定的条件删除不需要的内容。

让我们看下面的片段,以查看过滤函数的工作示例:

public class MyLocalFilterFunction extends BaseFunction {
    public boolean isKeep(TridentTuple tuple) {
      return tuple.getInteger(0) == 1 && tuple.getInteger(1) == 2;
    }
}

让我们看看输入流上的示例元组,字段为["a","b","c"]

[1,2,3]
[2,1,1]
[2,3,4]

我们执行或调用函数如下:

mystream.each(new Fields("b", "a"), new MyLocalFilterFunction())

输出将如下所示:

//for tuple 1 [1,2,3]
// no output because valueof("field b") ==1 && valueof("field a")  ==2 //is not satisfied 
//for tuple 1 [2,1,1]
// no output because valueof("field b") ==1 && valueof("field a")  ==2 [2,1,1]
//for tuple 1 [2,3,4]
// no output because valueof("field b") ==1 && valueof("field a")  ==2 //is not satisfied

partitionAggregate

partitionAggregate函数对一批元组的每个分区进行操作。与迄今为止执行的本地函数相比,此函数之间存在行为差异,它对输入元组发射单个输出元组。

以下是可以用于在此框架上执行各种聚合的其他函数。

Sum 聚合

以下是对 sum 聚合器函数的调用方式:

mystream.partitionAggregate(new Fields("b"), new Sum(), new Fields("sum"))

假设输入流具有["a","b"]字段,并且以下是元组:

Partition 0:
["a", 1]
["b", 2]
Partition 1:
["a", 3]
["c", 8]
Partition 2:
["e", 1]
["d", 9]
["d", 10]

输出将如下所示:

Partition 0:
[3]
Partition 1:
[11]
Partition 2:
[20]

CombinerAggregator

Trident API 提供的此接口的实现返回一个带有单个字段的单个元组作为输出;在内部,它对每个输入元组执行 init 函数,然后将值组合,直到只剩下一个值,然后将其作为输出返回。如果组合器函数遇到没有任何值的分区,则发射"0"。

以下是接口定义及其合同:

public interface CombinerAggregator<T> extends Serializable {
    T init(TridentTuple tuple);
    T combine(T val1, T val2);
    T zero();
}

以下是计数功能的实现:

public class myCount implements CombinerAggregator<Long> {
    public Long init(TridentTuple mytuple) {
        return 1L;
    }
public Long combine(Long val1, Long val2) {
        return val1 + val2;
    }

    public Long zero() {
        return 0L;
    }
}

这些CombinerAggregators函数相对于partitionAggregate函数的最大优势在于,它是一种更高效和优化的方法,因为它在通过网络传输结果之前执行部分聚合。

ReducerAggregator

正如其名称所示,此函数生成一个init值,然后迭代处理输入流中的每个元组,以生成包含单个字段和单个元组的输出。

以下是ReducerAggregate接口的接口契约:

public interface ReducerAggregator<T> extends Serializable {
    T init();
    T reduce(T curr, TridentTuple tuple);
}

以下是计数功能的接口实现:

public class myReducerCount implements ReducerAggregator<Long> {
    public Long init() {
        return 0L;
    }

    public Long reduce(Long curr, TridentTuple tuple) {
        return curr + 1;
    }
}

Aggregator

Aggregator函数是最常用和多功能的聚合器函数。它有能力发出一个或多个元组,每个元组可以有任意数量的字段。它们具有以下接口签名:

public interface Aggregator<T> extends Operation {
    T init(Object batchId, TridentCollector collector);
    void aggregate(T state, TridentTuple tuple, TridentCollector  collector);
    void complete(T state, TridentCollector collector);
}

执行模式如下:

  • init方法是每个批次处理之前的前导。它在处理每个批次之前被调用。完成后,它返回一个持有批次状态表示的对象,并将其传递给后续的聚合和完成方法。

  • init方法不同,aggregate方法对批次分区中的每个元组调用一次。该方法可以存储状态,并根据功能要求发出结果。

  • complete 方法类似于后处理器;当批次分区被聚合完全处理时执行。

以下是计数作为聚合器函数的实现:

public class CountAggregate extends BaseAggregator<CountState> {
    static class CountState {
        long count = 0;
    }
    public CountState init(Object batchId, TridentCollector  collector) {
        return new CountState();
    }
    public void aggregate(CountState state, TridentTuple tuple,  TridentCollector collector) {
        state.count+=1;
    }
    public void complete(CountState state, TridentCollector  collector) {
        collector.emit(new Values(state.count));
    }
}

许多时候,我们遇到需要同时执行多个聚合器的实现。在这种情况下,链接的概念就派上了用场。由于 Trident API 中的这个功能,我们可以构建一个聚合器的执行链,以便在传入流元组的批次上执行。以下是这种链的一个例子:

myInputstream.chainedAgg()
        .partitionAggregate(new Count(), new Fields("count"))
        .partitionAggregate(new Fields("b"), new Sum(), new  Fields("sum"))
        .chainEnd()

此链的执行将在每个分区上运行指定的sumcount聚合器函数。输出将是一个单个元组,其中包含sumcount的值。

与流重新分区相关的操作

正如其名称所示,这些流重新分区操作与执行函数来改变任务之间的元组分区有关。这些操作涉及网络流量,结果重新分发流,并可能导致整体分区策略的变化,从而影响多个分区。

以下是 Trident API 提供的重新分区函数:

  • Shuffle: 这执行一种重新平衡的功能,并采用随机轮询算法,以实现元组在分区之间的均匀重新分配。

  • Broadcast: 这就像其名称所示的那样;它将每个元组广播和传输到每个目标分区。

  • partitionBy: 这个函数基于一组指定字段的哈希和模运算工作,以便相同的字段总是移动到相同的分区。类比地,可以假设这个功能的运行方式类似于最初在 Storm 分组中学到的字段分组。

  • global: 这与 Storm 中流的全局分组相同,在这种情况下,所有批次都选择相同的分区。

  • batchGlobal: 一个批次中的所有元组都被发送到同一个分区(所以它们在某种程度上是粘在一起的),但不同的批次可以被发送到不同的分区。

流上的数据聚合

Storm 的 Trident 框架提供了两种执行聚合的操作:

  • aggregate: 我们在之前的部分中已经涵盖了这个,它在隔离的分区中工作,而不涉及网络流量

  • persistentAggregate: 这在分区间执行聚合,但不同之处在于它将结果存储在状态源中

流中字段的分组

分组操作的工作方式类似于关系模型中的分组操作,唯一的区别在于 Storm 框架中的分组操作是在输入源的元组流上执行的。

让我们通过以下图更仔细地了解这一点:

在流中对字段进行分组

Storm Trident 中的这些操作在几个不同分区的元组流上运行。

合并和连接

合并和连接 API 提供了合并和连接各种流的接口。可以使用以下多种方式来实现这一点:

  • 合并: 正如其名称所示,merge将两个或多个流合并在一起,并将合并后的流作为第一个流的输出字段发出:
myTridentTopology.merge(stream1,stream2,stream3);

  • 连接: 此操作与传统的 SQL join函数相同,但不同之处在于它适用于小批量而不是从喷口输出的整个无限流

例如,考虑一个连接函数,其中 Stream 1 具有诸如["key", "val1", "val2"]的字段,Stream 2 具有["x", "val1"],并且从这些函数中我们执行以下代码:

myTridentTopology.join(stream1, new Fields("key"), stream2, new  Fields("x"), new Fields("key", "a", "b", "c"));

结果,Stream 1 和 Stream 2 将使用keyx进行连接,其中key将连接 Stream 1 的字段,x将连接 Stream 2 的字段。

从连接中发出的输出元组将如下所示:

  • 所有连接字段的列表;在我们的情况下,它将是 Stream 1 的key和 Stream 2 的x

  • 所有参与连接操作的流中不是连接字段的字段列表,顺序与它们传递给join操作的顺序相同。在我们的情况下,对于 Stream 1 的val1val2,分别是ab,对于 Stream 2 的val1c(请注意,此步骤还会消除流中存在的任何字段名称的歧义,我们的情况下,val1字段在两个流之间是模棱两可的)。

当在拓扑中从不同的喷口中提供的流上发生像连接这样的操作时,框架确保喷口在批量发射方面是同步的,以便每个连接计算可以包括来自每个喷口的批量元组。

示例和插图

Trident 的另一个开箱即用且流行的实现是 reach 拓扑,它是一个纯 DRPC 拓扑,可以根据需要找到 URL 的可达性。在我们深入研究之前,让我们先了解一些行话。

Reach 基本上是暴露给 URL 的 Twitter 用户数量的总和。

Reach 计算是一个多步骤的过程,可以通过以下示例实现:

  • 获取曾经发推特的 URL 的所有用户

  • 获取每个用户的追随者树

  • 组装之前获取的大量追随者集

  • 计算集合

好吧,看看之前的骨架算法,你会发现它超出了单台机器的能力,我们需要一个分布式计算引擎来实现它。这是 Storm Trident 框架的理想候选,因为您可以在整个集群中的每个步骤上执行高度并行的计算。

  • 我们的 Trident reach 拓扑将从两个大型数据银行中吸取数据

  • 银行 A 是 URL 到发起者银行,其中将存储所有 URL 以及曾经发推特的用户的名称。

  • 银行 B 是用户追随者银行;这个数据银行将为所有 Twitter 用户提供用户追随映射

拓扑将定义如下:

TridentState urlToTweeterState =  topology.newStaticState(getUrlToTweetersState());
TridentState tweetersToFollowerState =  topology.newStaticState(getTweeterToFollowersState());

topology.newDRPCStream("reach")
       .stateQuery(urlToTweeterState, new Fields("args"), new  MapGet(), new Fields("tweeters"))
       .each(new Fields("tweeters"), new ExpandList(), new  Fields("tweeter"))
       .shuffle()
       .stateQuery(tweetersToFollowerState, new Fields("tweeter"),  new MapGet(), new Fields("followers"))
       .parallelismHint(200)
       .each(new Fields("followers"), new ExpandList(), new  Fields("follower"))
       .groupBy(new Fields("follower"))
       .aggregate(new One(), new Fields("one"))
       .parallelismHint(20)
       .aggregate(new Count(), new Fields("reach"));

在前述拓扑中,我们执行以下步骤:

  1. 为两个数据银行(URL 到发起者银行 A 和用户到追随银行 B)创建一个TridentState对象。

  2. newStaticState方法用于实例化数据银行的状态对象;我们有能力在之前创建的源状态上运行 DRPC 查询。

  3. 在执行中,当要计算 URL 的可达性时,我们使用数据银行 A 的 Trident 状态执行查询,以获取曾经发推特的所有用户的列表。

  4. ExpandList函数为查询 URL 的每个推特者创建并发出一个元组。

  5. 接下来,我们获取先前获取的每个推特者的追随者。这一步需要最高程度的并行性,因此我们在这里使用洗牌分组,以便在所有螺栓实例之间均匀分配负载。在我们的 reach 拓扑中,这是最密集的计算步骤。

  6. 一旦我们有了 URL 推特者的追随者列表,我们执行类似于筛选唯一追随者的操作。

  7. 我们通过将追随者分组在一起,然后使用one聚合器来得到唯一的追随者。后者简单地为每个组发出1,然后在下一步将所有这些计数在一起以得出影响力。

  8. 然后我们计算追随者(唯一),从而得出 URL 的影响力。

测验时间

  1. 状态是否以下陈述是真是假:

  2. DRPC 是一个无状态的,Storm 处理机制。

  3. 如果 Trident 拓扑中的元组执行失败,整个批次将被重放。

  4. Trident 允许用户在流数据上实现窗口函数。

  5. 聚合器比分区聚合器更有效。

  6. 填空:

  7. _______________ 是 RPC 的分布式版本。

  8. _______________ 是 Storm 的基本微批处理框架。

  9. ___________________ 函数用于根据特定标准或条件从流批次中删除元组。

  10. 创建一个 Trident 拓扑,以查找在过去 5 分钟内发表最多推文的推特者。

总结

在本章中,我们几乎涵盖了关于 Storm 及其高级概念的一切,并让您有机会亲自体验 Trident 和 DRPC 拓扑。您了解了 Trident 及其需求和应用,DRPC 拓扑以及 Trident API 中提供的各种功能。

在下一章中,我们将探索与 Storm 紧密配合并且对于使用 Storm 构建端到端解决方案必不可少的其他技术组件。我们将涉及分布式缓存和与 Storm 一起使用 memcache 和 Esper 进行复杂事件处理(CEP)的领域。

第十一章:分布式缓存和 CEP 与 Storm

在本章中,我们将学习与 Storm 结合使用分布式缓存的需求,以及将广泛使用的选项与 Storm 集成。我们还将涉及与 Storm 合作的复杂事件处理(CEP)引擎。

本章将涵盖以下主题:

  • Storm 框架中分布式缓存的需求

  • memcache 简介

  • 构建具有缓存的拓扑

  • CEP 和 Esper 简介

在本章的结尾,您应该能够将 CEP 和缓存与 Storm 结合起来,以解决实时使用案例。

Storm 中分布式缓存的需求

现在我们已经足够了解 Storm 的所有优势,让我们谈谈它最大的弱点之一:缺乏共享缓存,即所有在 Storm 集群的各个节点上运行的任务都可以访问和写入的共同内存存储。

下图说明了一个三节点的 Storm 集群,其中每个监督节点上都有两个运行的 worker:

Storm 中分布式缓存的需求

如前图所示,每个 worker 都有自己的 JVM,数据可以存储和缓存。然而,我们缺少的是一个缓存层,它可以在监督者的 worker 之间共享组件,也可以跨监督者之间共享。下图描述了我们所指的需求:

Storm 中分布式缓存的需求

前面的图描述了需要一个共享缓存层的情况,可以在所有节点中引用共同的数据。这些都是非常有效的使用案例,因为在生产中,我们会遇到以下情况:

  • 我们有很多只读的参考维度数据,我们希望将其放在一个地方,而不是在每个监督者级别进行复制和更新

  • 有时,在某些使用案例中,我们有事务性数据,需要所有 worker 读取和更新;例如,当计算某些事件时,计数必须保存在一个共同的位置

这就是共享缓存层的作用,可以在所有监督节点上访问。

memcached 简介

Memcached 是一个非常简单的内存键值存储;我们可以将其视为哈希映射的内存存储。它可以与 Storm 监督者结合使用,作为一个共同的内存存储,可以被 Storm 集群中各个节点上的所有 Storm worker 进行读写操作。

Memcached 有以下组件:

  • memcached 服务器

  • memcache 客户端

  • 哈希算法(基于客户端的实现)

  • 数据保留的服务器算法

Memcached 使用最近最少使用(LRU)算法来丢弃缓存中的元素。这意味着自最长时间以来未被引用的项目首先从缓存中移除。这些项目被认为已从缓存中过期,如果它们在过期后被引用,它们将从稳定存储重新加载。

以下是从缓存中加载和检索条目的流程:

memcached 简介

前面的图描述了缓存命中和未命中的情况,其中某些项目根据 LRU 算法过期。前图中的情况如下:

  • 当缓存应用程序启动时,它会从稳定存储(在我们的案例中是数据库)中加载数据。

  • 在请求从缓存中获取数据的情况下,可能会发生两种情况:

  • 缓存命中:这是我们请求的数据存在于缓存服务器上的情况,在这种情况下,请求将从缓存中提供

  • 缓存未命中:这是请求的数据在缓存服务器中不存在的情况,在这种情况下,数据从数据库中获取到缓存中,然后从缓存中提供请求

现在我们了解了缓存的功能以及在 Storm 解决方案背景下的需求。

设置 memcache

以下是需要执行并将需要安装 memcache 的步骤:

wget http://memcached.org/latest
tar -zxvfmemcached-1.x.x.tar.gz
cdmemcached-1.x.x
./configure && make && make test &&sudo make install

以下是连接到 memcache 客户端和函数的代码片段。它从缓存中检索数据:

public class MemCacheClient {
  private static MemcachedClient client = null;
  private static final Logger logger =  LogUtils.getLogger(MemCacheClient.class);

  /**
  * Constructor that accepts the cache properties as parameter  and initialises the client object accordingly.
   * @param properties
   * @throws Exception
   */

  publicMemCacheClient(Properties properties) throws Exception {
    super();
    try {
      if (null == client) {
        client = new MemcachedClient(new InetSocketAddress(
          102.23.34.22,
          5454)));
    }
  } catch (IOException e) {
    if (null != client)
      shutdown();
    throw new Exception("Error while initiating MemCacheClient",  e);
  }
}

/**
 * Shutdown the client and nullify it
 */

public void shutdown() {
    logger.info("Shutting down memcache client ");
    client.shutdown();
    client = null;
  }

  /**
    * This method sets a value in cache with a specific key and  timeout 
    * @param key the unique key to identify the value 
    * @paramtimeOut the time interval in ms after which the value  would be refreshed
    * @paramval
    * @return
    */

  public Future < Boolean > addToMemCache(String key, inttimeOut,  Object val) {
    if (null != client) {
      Future < Boolean > future = client.set(key, timeOut, val);
      return future;
    } else {
      return null;
    }
  }

  /**
    * retrives and returns the value object against the key passed  in as parameter
    * @param key
    * @return
    */

public Object getMemcachedValue(String key) {
  if (null != client) {
    try {
      returnclient.get(key);
    } catch (OperationTimeoutException e) {
      logger.error(
        "Error while fetching value from memcache server for key "  + key, e);
      return null;
    }
  } else
    return null;
  }
}

一旦编码了前面的代码片段,您将建立创建缓存客户端、将数据加载到缓存中并从中检索值的机制。因此,任何需要访问缓存的 Storm bolt 都可以使用通过与客户端交互创建的公共层。

使用缓存构建拓扑

cache:
public class MyCacheReaderBolt extends BaseBasicBolt {
  MyCacheReadercacheReader;
  @Override
  public void prepare(Map stormConf, TopologyContext context) {
      super.prepare(stormConf, context);
      try {
        cacheReader = new MyCacheReader();
      } catch (Exception e) {
        logger.error("Error while initializing Cache", e);
      }
    }

  /**
     * Called whenever a new tuple is received by this bolt.  Responsible for 
     * emitting cache enriched event onto output stream 
  */

  public void execute(Tuple tuple, BasicOutputCollector collector)  {
    logger.info("execute method :: Start ");
    event = tuple.getString(0);
    populateEventFromCache(event);
    collector.emit(outputStream, new Values(event));
  } else {
    logger.warn("Event not parsed :: " + tuple.getString(0));
  }
} catch (Exception e) {
  logger.error("Error in execute() ", e);
  }
}
logger.info("execute method :: End ");
}

private void populateEventFromCache(Event event) {
  HashMapfetchMap = (HashMap)  cacheReader.get(searchObj.hashCode());
  if (null != fetchMap) {
    event.setAccountID(Integer.parseInt((String)  fetchMap.get("account_id")));
    logger.debug("Populating event" + event + " using cache " +  fetchMap);
  } else {
    logger.debug("No matching event found in cache.");
  }
  logger.info("Time to fetch from cache=" +  (System.currentTimeMillis() - t1) + "msec");
  }
}

/**
 * Declares output streams and tuple fields emitted from this bolt
 */
  @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer)  {
    String stormStreamName = logStream.getName() + "_" +  eventType;
    declarer.declareStream(stormStreamName, new  Fields(stormStreamName));
  logger.debug("Topology : " + topology.getTopologyName() + ",  Declared output stream : " + stormStreamName + ", Output field :  " + stormStreamName);
}
 dimensional data from memcache, and emits the enriched bolt to the streams to the following bolts in the DAG topology.

复杂事件处理引擎简介

通常与之一起使用的有两个术语,它们是复杂事件处理CEP)和事件流处理ESP)。

嗯,在理论上,这些是技术范式的一部分,使我们能够构建具有戏剧性的实时分析的应用程序。它们让我们以非常快的速度处理传入事件,并在事件流之上执行类似 SQL 的查询以生成实时直方图。我们可以假设 CEP 是传统数据库的倒置。在传统的 DBMS 和 RDBMS 的情况下,我们有存储的数据,然后我们对它们运行 SQL 查询以得出结果,而在 CEP 的情况下,我们有预定义或存储的查询,然后我们通过它们运行数据。我们可以通过一个例子来设想这一点;比方说我经营一个百货商店,我想知道过去一小时内销量最高的商品。所以如果你看这里,我们即将执行的查询在性质上是相当固定的,但输入数据并不是恒定的——它在每次销售交易时都会改变。同样,比方说我经营一个股票持有公司,想知道过去 2 分钟内每 5 秒钟的前 10 名表现者。

复杂事件处理引擎简介

前面的图示了股票行情使用案例,我们有一个 2 分钟的滑动窗口,股票行情每 5 秒钟滑动一次。现在我们有许多实际的用例,比如:

  • 针对销售点POS)交易的欺诈检测模式

  • 在任何段中的前 N

  • 将深度学习模式应用于来自任何来源的流数据

现在,了解了 CEP 及其高层次需求后,让我们简要介绍其高层次组件:

  • 在每个 CEP 中的操作数是事件数据;它本质上是一个事件驱动的系统

  • 事件处理语言:这是一个工具,用于便利地构建要在数据上执行的查询

  • 监听器:这些是实际执行查询并在事件到达系统时执行操作的组件

Esper

Esper 是领先的 CEP 引擎之一,可在开源(GPL 和企业许可证)下使用。该软件包可从www.espertech.com/download/下载,如果您尝试执行基于 Maven 的 Esper 项目,依赖项可以构建如下:

<dependency>
<groupId>com.espertech</groupId>
<artifactId>esper</artifactId>
<version> ... </version>
</dependency>
Ref :Espertech.com

下一个显而易见的问题可能是为什么我们想要将 Esper-CEP 与 Storm 一起使用。嗯,Esper 具有一些独特的能力,与 Storm 配合得很好,并让 EQL 功能利用在 Storm 上得出的结果。以下是导致这种选择的互补功能:

  • 吞吐量:作为 Storm 能力的补充,Esper 也具有非常高的吞吐量,可以处理每秒从 1K 到 100K 条消息。

  • 延迟:Esper 有能力以非常低的延迟率执行 EQL 和基于 Esper 结果的操作;在大多数情况下,这是毫秒级的顺序。

  • 计算:这指的是执行功能的能力,例如基于聚合的模式检测、复杂查询和随时间的相关性。这些切片窗口的流数据。

开始使用 Esper

CasinoWinEvent, a value object where we store the name of the game, the prize amount, and the timestamp:
public static class CasinoWinEvent {
  String game;
  Double prizeAmount;
  Date timeStamp;

  publicCasinoWinEvent(String s, double p, long t) {
    game = s;
    prizeAmount = p;
    timeStamp = new Date(t);
  }
  public double getPrizeAmount() {
    return prizeAmount;
  }
  public String getGame() {
    return game;
  }
  public Date getTimeStamp() {
    return timeStamp;
  }

  @
  Override
  public String toString() {
    return "Price: " + price.toString() + " time: " +  timeStamp.toString();
  }
}

一旦我们有了值对象,下一步就是实例化 Esper 引擎和监听器,并将所有部分连接在一起:

public class myEsperMain {
  private static Random generator = new Random();
  public static void GenerateRandomCasinoWinEvent(EPRuntimecepRT)  {
    doubleprizeAmount = (double) generator.nextInt(10);
    longtimeStamp = System.currentTimeMillis();
    String game = "Roulette";
    CasinoWinEventcasinoEvent = new CasinoWinEvent(game,  prizeAmount, timeStamp);
    System.out.println("Sending Event:" + casinoEvent);
    cepRT.sendEvent(casinoEvent);
  }
  public static class CEPListener implements UpdateListener {
    public void update(EventBean[] newData, EventBean[] oldData) {
      System.out.println("Event received: " +  newData[0].getUnderlying());
    }
  }
  public static void main(String[] args) {
    //The Configuration is meant only as an initialization-time  object.
    Configuration cepConfig = new Configuration();
    cepConfig.addEventType("CasinoEvent",  CasinoWinEvent.class.getName());
    EPServiceProvidercep =  EPServiceProviderManager.getProvider("myCEPEngine",  cepConfig);
    EPRuntimecepRT = cep.getEPRuntime();
    EPAdministratorcepAdm = cep.getEPAdministrator();
    EPStatementcepStatement = cepAdm.createEPL("select * from " +   "CasinoEvent(symbol='Roulette').win:length(2) " + "having  avg(prizeAmount) > 10000.0");

    cepStatement.addListener(new CEPListener());
    // We generate a few ticks...
    for (inti = 0; i < 5; i++) {
      GenerateRandomCasinoWinEvent(cepRT);
    }
  }
}

CEPListener 是updateListener的实现(用于监听事件的到达),newData具有一个或多个新到达事件的流,oldData具有流的先前状态,即监听器到达当前触发器之前的状态。

在主方法中,我们可以加载 Esper 配置,或者如我们前面的案例所示,创建一个默认配置。然后,我们创建一个 Esper 运行时引擎实例,并将 EQL 查询绑定到它。

如果你看前面代码中的cepStatement.addListener(new CEPListener())语句,你会发现我们还将监听器绑定到了语句,从而将所有部分连接在一起。

将 Esper 与 Storm 集成

下图显示了我们计划如何将 Esper 与我们在第六章中早期创建的拓扑之一向 Storm 添加 NoSQL 持久性结合使用。Storm 与 Esper 的集成使开发人员能够在 Storm 处理的事件流上执行类似 SQL 的查询。

将 Esper 与 Storm 集成

ZeroDuration filter bolt that filters the CALL_END events that have a duration of 0 seconds to be emitted onto the stream feeding the Esper bolt:
  /*
  * Bolt responsible for forwarding events which satisfy following  criteria:
  * <ul>
  * <li>event should belong to 'End'  type</li>
  * <li>duration should be zero</li>
  * </ul>
  */

public class ZeroSecondsCDRBolt extends BaseRichBolt {

  /**
  * Called when {@link ZeroSecondsCDRBolt} is initialized
  */
  @Override
  public void prepare(Map conf, TopologyContext context,
    OutputCollector collector) {
    logger.info("prepare method :: Start ");
    this.collector = collector;
    logger.info("prepare() conf {},Collector {}", conf.toString(),  collector.toString());
    logger.info("prepare method :: End ");
  }

  /**
  * Called whenever a new tuple is received by this bolt. This  method 
   * filters zero duration End records 
   */

  @
  Override
  public void execute(Tuple tuple) {
    logger.info("execute method :: Start ");

    if (tuple != null && tuple.getString(0) != null) {
      eventCounter++;
      String event = tuple.getString(0);
      logger.info("execute :event recd :: {}", event);
      if (event != null && event.contains("CALL_END")) {
        emitCallEndRecords(tuple);
      }
      collector.ack(tuple);
    }
    logger.info("execute method :: End ");
  }

  private void emitCallEndRecords(Tuple tuple) {
    String event = tuple.getString(0);

      try {
        //splitting the event based on semicolon
        String[] eventTokens = event.split(",");
        duration = Long.parseLong(eventTokens[4]);
        callId = Long.parseLong(eventTokens[0]);
        logger.debug(" Event (callId = {}) is a Zero duration  Qualifier ", callId);
        collector.emit(....);

      } catch (Exception e) {
        logger.error("Corrupt Stopped record. Error occurred while  parsing the event : {}", event);
      }
    }

  /**
  * Declares output fields in tuple emitted from this bolt
  */

  @Override
  public void declareOutputFields(OutputFieldsDeclarer declarer) {
    declarer.declareStream(CALL_END, new Fields());
  }

  @
  Override
  public Map < String, Object > getComponentConfiguration() {
    return null;
  }
}

下一步是将 Esper bolt 结合到拓扑中。这可以从github.com/tomdz/storm-esper轻松下载为捆绑包,并且可以使用以下代码快速捆绑到拓扑中:

EsperBoltesperBolt = newEsperBolt.Builder()
  .inputs()
  .aliasComponent("ZeroSecondCallBolt")
  .withFields("a", "b")
  .ofType(Integer.class)
  .toEventType("CALL_END")
  .outputs()
  .outputs().onDefaultStream().emit("count")
  .statements()
  .add("select callID as CALL_ID,callType as CALL_TYPE, count(*)  as OCCURRENCE_CNT from CDR.win:time_batch(5 minutes)  where  (eventType = 'CALL_END') and (duration = 0) group by  callID,eventType having count(*) > 0 order by  OCCURRENCE_CNTdesc")
  .build();

输出将如下所示:

将 Esper 与 Storm 集成

前面图中的 Esper 查询在传入数据流上执行;以下是其分解和解释:

selectcallID as CALL_ID,callType as CALL_TYPE, count(*) as  OCCURRENCE_CNT

我们从传入的元组中选择以下字段,如Call_IdCall_typecount

fromCDR.win:time_batch(5 minutes)  where (eventType = 'CALL_END')  and (duration = 0) group by callID,eventTypehaving count(*) > 0
order by OCCURRENCE_CNTdesc

我们正在操作的命名窗口是CDR.WIN。批处理大小为 5 分钟,这意味着随着每个事件或元组的到达,我们会回顾过去 5 分钟的时间,并对过去 5 分钟内到达的数据执行查询。结果按事件类型分组,并按相反顺序排序。

测验时间

问题 1.判断以下陈述是真还是假:

  1. 缓存是只读内存空间。

  2. 一旦数据添加到缓存中,就会永远保留在那里。

  3. CEP 允许在流数据上实现类似 SQL 的查询。

  4. Esper 基于事件驱动架构。

问题 2.填空:

  1. _______________ 是 memcache 的算法。

  2. 当缓存中没有数据时,称为 _______________。

  3. _______________ 是 Esper 的组件,触发Endeca 查询语言EQL)的执行。

  4. _______________ 通常用于时间序列窗口函数数据。

问题 3.使用 Esper 创建一个端到端拓扑,以显示在某条高速公路上前 10 名超速设备的 Storm 和 Esper 的结合使用。

总结

在本章中,我们讨论了与 Storm 结合使用缓存的概念,以及开发人员使用缓存的实用性和应用。我们了解了 memcache 作为缓存系统。

在本章的后部分,我们探讨了 Esper 作为复杂事件处理系统,并了解了它与 Storm 拓扑的集成。

附录 A.测验答案

第一章

第 1 题。监视 ping 延迟,并在超过一定阈值时发出警报,以提供对网络的实时感知。
监视来自交通传感器的事件,并在一天的高峰时段绘制瓶颈点的图表。
感知边界入侵。

第二章

第 1 题。FalseFalseTrueFalse
第 2 题。Topology BuilderParallelismNimbus

第三章

第 1 题。TrueFalseTrueTrue
第 2 题。ack()declare()emit()

第四章

第 1 题。TrueFlaseFalseTrueTrue
第 2 题。Process latencyExecute latencyZookeeper

第五章

第 1 题。FalseFalseTrueTrue
第 2 题。Direct exchangeFan-outAMQP spout

第六章

第 1 题。FalseFalseTrueFalse
第 2 题。APLow write latencyhector

第七章

第 1 题。FalseFalseTrueTrue
第 2 题。SnitchANYrepair

第八章

第 1 题。FalseFalseFalseFalse
第 2 题。Nodetool compactRingRing

第九章

第 1 题。FalseFalseTrueTrue
第 2 题。ZookeeperNimbusstorm-config.xml

第十章

第 1 题。FalseTrueTrueFalse
第 2 题。DRPCTridentfilter

第十一章

第 1 题。FalseFalseTrueTrue
第 2 题。LRUcache-missEPRuntimebatch window
posted @ 2024-05-21 12:53  绝不原创的飞龙  阅读(16)  评论(0编辑  收藏  举报