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

48 阅读50分钟

Storm 和 Cassandra 实时分析(二)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

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

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

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

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

一致性哈希

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

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

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

一致性哈希

Cassandra 集群的一致性哈希

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

  • 1属于节点A

  • 2属于节点B

  • 3属于节点C

  • 4属于节点C

  • 5属于节点D

  • 6属于节点E

  • 7属于节点F

  • 8属于节点H

  • 9属于节点H

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

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

假设以下是列值数据:

名字性别
JammyM
CarryF
JesseM
SammyF

这是哈希映射的样子:

分区键哈希值
Jim2245462676723220000.00
Carol7723358927203680000.00
Johnny6723372854036780000.00
Suzy1168604627387940000.00

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

节点起始范围结束范围分区键哈希值
A9223372036854770000.004611686018427380000.00Jammy6723372854036780000.00
B4611686018427380000.001.00Jesse2245462676723220000.00
C0.004611686018427380000.00suzy1168604627387940000.00
D4611686018427380000.009223372036854770000.00Carry7723358927203680000.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