Hadoop-现代大数据处理-二-

86 阅读1小时+

Hadoop 现代大数据处理(二)

原文:Modern big data processing with Hadoop

协议:CC BY-NC-SA 4.0

六、设计实时流数据管道

这本书的前三章都涉及批量数据。了解了 Hadoop 的安装、数据接收工具和技术以及数据存储之后,让我们来看看数据流。我们不仅要考虑如何处理实时数据流,还要考虑如何围绕数据流设计管道。

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

  • 实时流概念
  • 实时流组件
  • Apache 弗林克对 Spark
  • ApacheSpark 对 Storm

实时流概念

让我们在以下几节中了解一些与实时流应用相关的关键概念。

数据流

数据流是从一端到另一端、从发送者到接收者、从生产者到消费者的连续数据流。数据的速度和数量可能会有所不同;它可能是每秒 1 GB 的数据,也可能是每秒或每分钟 1 KB 的数据。

批处理与实时数据处理

在批处理中,数据是分批收集的,每批数据都被发送进行处理。批处理间隔可以是从一天到一分钟的任何时间。在当今的数据分析和商业智能世界中,数据不会在超过一天的时间内被批量处理。否则,业务团队将无法洞察日常业务发生了什么。例如,企业数据仓库团队可能会收集过去 24 小时内发出的所有订单,并将收集到的所有订单发送给分析引擎进行报告。

批次也可以是一分钟。在 Spark 框架中(我们将在第 7 章大规模数据处理框架中学习 Spark),数据是以微批处理的方式处理的。

在实时处理中,一旦在源端产生事件,数据(事件)就从生产者(发送者)传输(流传输)到消费者(接收者)。例如,在电子商务网站上,一旦客户在该网站上下了相同的订单,订单就会立即在分析引擎中得到处理。这样做的好处是,该公司的业务团队可以实时(在几毫秒或亚毫秒内)全面了解其业务。这将有助于他们调整促销以增加收入,所有这些都是实时的。

下图解释了流处理架构:

复杂事件处理

复杂事件处理 ( CEP )是将来自多个来源的数据进行组合以发现复杂关系或模式的事件处理。CEP 的目标是识别有意义的事件(如机会或威胁),并尽快做出反应。从根本上说,CEP 是将业务规则应用于流式事件数据。例如,CEP 用于用例,如股票交易、欺诈检测、医疗索赔处理等。

下图解释了流处理架构:

持续可用性

任何实时应用都应该随时可用,没有任何中断。事件收集、处理和存储组件应配置有下划线的高可用性假设。任何组件的任何故障都会对业务运行造成重大干扰。例如,在信用卡欺诈检测应用中,需要拒绝所有欺诈交易。如果应用中途停止,并且无法拒绝欺诈交易,那么将导致重大损失。

低延迟

在任何实时应用中,事件应该在几毫秒内从源流向目标。源收集事件,处理框架将事件移动到其目标数据存储中,在那里可以进一步分析事件以发现趋势和模式。所有这些都应该实时发生,否则可能会影响业务决策。例如,在信用卡欺诈检测应用中,预期应分析所有传入的交易,以发现可能的欺诈交易(如果有)。如果流处理花费的时间超过了所需的时间,这些事务可能会通过系统,给业务造成重大损失。

可扩展的处理框架

硬件故障可能会导致流处理应用中断。为了避免这种常见情况,我们总是需要一个处理框架,该框架提供内置的 API 来支持连续计算、容错事件状态管理、故障情况下的检查点功能、运行中聚合、窗口等。幸运的是,最近的所有 Apache 项目,如 Storm、Spark、Flink 和 Kafka,都支持所有这些开箱即用的特性。开发人员可以使用 Java、Python 和 Scala 来使用这些应用编程接口。

水平可伸缩性

流处理平台应该支持水平可伸缩性。这意味着在传入数据负载较高的情况下,向集群添加更多物理服务器,以保持吞吐量服务级别协议。这样,可以通过增加更多的节点来提高处理性能,而不是在现有的服务器上增加更多的 CPU 和内存;这就叫做垂直伸缩

储存;储备

流的优选格式是键值对。JSON 和 Avro 格式很好地代表了这种格式。保存键值类型数据的首选存储是 NoSQL 数据存储,如 HBase 和 Cassandra。目前市场上总共有 100 个 NoSQL 开源数据库。选择合适的数据库非常具有挑战性,因为所有这些数据库都为数据持久性提供了一些独特的功能。几个例子是模式不可知、高度可分发、商品硬件支持、数据复制等等。

下图解释了所有流处理组件:

在本章中,我们将详细讨论消息队列和流处理框架。在下一章中,我们将集中讨论数据索引技术。

实时流组件

在接下来的部分中,我们将介绍一些重要的实时流组件。

消息队列

消息队列允许您发布和订阅事件/记录流。在我们的实时流体系结构中,有多种可供选择的消息队列。比如有 RabbitMQ、ActiveMQ、Kafka。其中,卡夫卡因其各种独特的特征而获得了巨大的知名度。因此,我们将详细讨论卡夫卡的建筑。关于 RabbitMQ 和 ActiveMQ 的讨论超出了本书的范围。

那么什么是卡夫卡呢?

Kafka 是一个快速、可扩展、持久且容错的发布-订阅消息传递系统。Apache Kafka 是一个开源的流处理项目。它提供了统一的高吞吐量,是处理实时数据流的低延迟平台。它提供了一个分布式存储层,支持大规模可扩展的发布/订阅消息队列。Kafka Connect 通过连接外部系统支持数据导入和导出。Kafka Streams 为流处理提供了 Java APIs。Kafka 与 Apache Spark、Apache Cassandra、Apache HBase、Apache Spark 等结合使用,实现实时流处理。

Apache Kafka 最初由 LinkedIn 开发,随后在 2011 年初开源。2014 年 11 月,几名在领英从事卡夫卡工作的工程师创建了一家名为 Confluent 的新公司,专注于卡夫卡。请使用此网址www.confluent.io/了解更多关于融合平台的信息。

卡夫卡特色

以下是卡夫卡的特点:

  • Kafka 可扩展 : Kafka 集群由多个物理服务器组成,有助于分配数据负载。在需要额外吞吐量的情况下,它很容易扩展,因为可以添加额外的服务器来维护服务级别协议。
  • Kafka 持久:在流处理过程中,Kafka 将消息持久保存在持久存储上。该存储可以是服务器本地磁盘或 Hadoop 集群。在消息处理失败的情况下,可以从磁盘访问消息并重放消息以再次处理消息。默认情况下,消息存储七天;这可以进一步配置。
  • 卡夫卡可靠:卡夫卡借助名为数据复制的功能提供消息可靠性。每个消息至少复制三次(这是可配置的),以便在数据丢失的情况下,可以使用消息的副本进行处理。
  • Kafka 支持高性能吞吐量:由于其独特的架构、分区、消息存储和水平可扩展性,Kafka 有助于每秒处理万亿字节的数据。

卡夫卡式建筑

下图展示了卡夫卡的建筑:

卡夫卡建筑组件

让我们详细看看每个组件:

  • 制作人:制作人发布特定卡夫卡主题的消息。生产者可以在每条信息记录上附上一个密钥。默认情况下,生产者以循环方式向主题分区发布消息。有时,可以将生产者配置为根据消息关键字的哈希值将消息写入特定的主题分区。
  • 主题:所有消息都存储在一个主题中。主题是记录发布到的类别或订阅源名称。主题可以与关系数据库中的表进行比较。多个使用者可以订阅一个主题来使用消息记录。
  • 分区:一个话题分为多个分区。Kafka 通过将主题划分为分区,并将每个分区放在 Kafka 集群的单独代理(服务器)上,提供了主题并行性。每个分区在存储消息的磁盘上都有一个单独的分区日志。每个分区都包含一个有序的、不可变的消息序列。每条消息都分配有唯一的序列号,称为偏移量。使用者可以从分区中的任何点读取消息,无论是从开始还是从任何偏移量。
  • 消费者:消费者订阅一个话题,消费消息。为了提高可伸缩性,可以将同一应用的使用者分组到一个使用者组中,每个使用者都可以从一个唯一的分区中读取消息。
  • 经纪人:卡夫卡被分成多个服务器叫做经纪人。所有经纪人加起来叫做卡夫卡集群。卡夫卡经纪人处理来自生产者的信息写作和来自消费者的信息阅读。卡夫卡经纪人存储了来自制作人的所有信息。默认期限为七天。此期限(保留期)可以根据要求进行配置。保留期直接影响到卡夫卡经纪人的本地存储。如果配置了更长的保留期,则需要更多的存储。保留期结束后,邮件会自动丢弃。
  • Kafka Connect :根据 Kafka 文档,Kafka Connect 允许构建和运行可重用的生产者或消费者,将 Kafka 主题连接到现有的应用或数据系统。例如,关系数据库的连接器可能捕获表的每一个变化。
  • Kafka Streams:Stream API 允许应用充当流处理器,消耗来自一个或多个主题的输入流,并产生到一个或多个输出主题的输出流,有效地将输入流转换为输出流。

卡夫卡连接深潜

卡夫卡连接是融合平台的一部分。它与卡夫卡融为一体。使用 Kafka Connect,构建从多个源到多个目标的数据管道非常容易。源连接器从另一个系统导入数据(例如,从关系数据库导入卡夫卡),而接收器连接器导出数据(例如,卡夫卡主题的内容到 HDFS 文件)。

卡夫卡连接建筑

下图展示了卡夫卡连线的架构:

数据流可以解释如下:

  • 各种信号源连接到卡夫卡连接集群卡夫卡连接集群从数据源提取数据。
  • Kafka Connect Cluster 由一组工作进程组成,这些工作进程是执行连接器的容器,任务自动相互协调以分配工作并提供可伸缩性和容错性。
  • 卡夫卡连接集群将数据推送到卡夫卡集群
  • 卡夫卡集群将数据保存在代理本地磁盘或 Hadoop 上。
  • Storm、Spark Streaming 和 Flink 等流应用从卡夫卡集群中提取数据进行流转换、聚合、连接等。这些应用可以将数据发送回 Kafka,或者将其保存到外部数据存储中,例如 HBase、Cassandra、MongoDB、HDFS 等。
  • 卡夫卡连接集群卡夫卡集群提取数据,并将其推入接收器。
  • 用户可以扩展现有的卡夫卡连接器或开发全新的连接器。

卡夫卡连接工人独立模式与分布式模式

用户可以通过两种方式运行 Kafka Connect:独立模式或分布式模式。

在独立模式下,一个进程运行所有连接器。它不是容错的。因为它只使用一个进程,所以它是不可扩展的。一般来说,它对用户的开发和测试很有用。

在分布式模式下,多个工人运行 Kafka Connect。在这种模式下,Kafka Connect 具有可伸缩性和容错性,因此用于生产部署。

让我们了解更多关于卡夫卡和卡夫卡连接(独立模式)。在本例中,我们将执行以下操作:

  1. 安装卡夫卡
  2. 创建主题
  3. 生成一些消息来验证生产者和消费者
  4. 卡夫卡连接-文件-源和文件-接收器
  5. 卡夫卡连接-JDBC-来源

下图显示了一个使用卡夫卡连接的用例:

让我们通过运行几个例子来看看卡夫卡和卡夫卡连接是如何工作的。有关更多详细信息,请使用以下链接获取卡夫卡汇的文档:docs.confluent.io/current/

安装卡夫卡

让我们执行以下步骤来安装卡夫卡:

  1. www.confluent.io/download/下载汇合
  2. 点击融合开源
  3. tar.gz下载文件confluent-oss-4.0.0-2.11.tar.gz并执行以下操作:
tar xvf confluent-oss-4.0.0-2.11.tar.gz
cd /opt/confluent-4.0.0/etc/kafka
vi server.properties
  1. 取消注释listeners=PLAINTEXT://:9092
  2. 开始汇合:
$ ./bin/confluent start schema-registry
  1. 开始zookeeper:
zookeeper is [UP]
  1. 开始kafka:
kafka is [UP]
  1. 开始schema-registry:
schema-registry is [UP]
A4774045:confluent-4.0.0 m046277$

创建主题

执行以下步骤创建主题:

  1. 列出现有主题
  2. 打开另一个终端,输入以下命令:
/opt/confluent-4.0.0
bin/kafka-topics --list --zookeeper localhost:2181
_schemas
  1. 创建主题:
bin/kafka-topics --create --zookeeper localhost:2181 --replication-factor 1 --partitions 3 --topic my-first-topic

Created topic "my-first-topic"
  1. 仔细检查新创建的主题:
bin/kafka-topics --list --zookeeper localhost:2181
_schemas
my-first-topic

生成消息来验证生产者和消费者

执行以下步骤生成消息以验证生产者和消费者:

  1. 给卡夫卡发信息my-first-topic:
bin/kafka-console-producer --broker-list localhost:9092 --topic my-first-topic
test1
test2
test3
  1. 开始消费者消费消息
  2. 打开另一个终端,输入以下命令:
$ bin/kafka-console-consumer --bootstrap-server localhost:9092 --topic my-first-topic --from-beginning
test3
test2
test1
  1. 转到生产者终端并输入另一条消息:
test4
  1. 验证消费终端,查看是否能看到test4信息

卡夫卡连接使用文件源和接收器

让我们看一下如何使用文件“源”和“接收器”创建主题,这有以下帮助:

cd /opt/confluent-4.0.0/etc/kafka
vi connect-file-test-source.properties
name=local-file-source
connector.class=FileStreamSource
tasks.max=1
file=/opt/kafka_2.10-0.10.2.1/source-file.txt
topic=my-first-topic
vi connect-file-test-sink.properties
name=local-file-sink
connector.class=FileStreamSink
tasks.max=1
file=/opt/kafka_2.10-0.10.2.1/target-file.txt
topics=my-first-topic

请执行以下步骤:

  1. 启动源连接器和接收器连接器:
cd /opt/confluent-4.0.0
$ ./bin/connect-standalone config/connect-standalone.properties config/connect-file-test-source.properties config/connect-file-test-sink.properties

echo 'test-kafka-connect-1' >> source-file.txt
echo 'test-kafka-connect-2' >> source-file.txt
echo 'test-kafka-connect-3' >> source-file.txt
echo 'test-kafka-connect-4' >> source-file.txt
  1. 再次检查卡夫卡主题是否收到了消息:
$ ./bin/kafka-console-consumer.sh --zookeeper localhost:2181 --from-beginning --topic my-first-topic

test3
test1
test4

{"schema":{"type":"string","optional":false},"payload":"test-kafka-connect-1"}
{"schema":{"type":"string","optional":false},"payload":"test-kafka-connect-2"}
{"schema":{"type":"string","optional":false},"payload":"test-kafka-connect-3"}
{"schema":{"type":"string","optional":false},"payload":"test-kafka-connect-4"}

test2
  1. 验证target-file.txt:
$ cat target-file.txt

{"schema":{"type":"string","optional":false},"payload":"test-kafka-connect-1"}
{"schema":{"type":"string","optional":false},"payload":"test-kafka-connect-2"}
{"schema":{"type":"string","optional":false},"payload":"test-kafka-connect-3"}
{"schema":{"type":"string","optional":false},"payload":"test-kafka-connect-4"}

卡夫卡连接使用 JDBC 和文件接收器连接器

下图显示了如何将数据库表中的所有记录推送到文本文件:

让我们使用卡夫卡连接实现前面的例子:

  1. 安装 SQLite:
$ sqlite3 firstdb.db

SQLite version 3.16.0 2016-11-04 19:09:39
Enter ".help" for usage hints.

sqlite>
sqlite> CREATE TABLE customer(cust_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, cust_name VARCHAR(255));
sqlite> INSERT INTO customer(cust_id,cust_name) VALUES(1,'Jon');
sqlite> INSERT INTO customer(cust_id,cust_name) VALUES(2,'Harry');
sqlite> INSERT INTO customer(cust_id,cust_name) VALUES(3,'James');
sqlite> select * from customer;

1|Jon
2|Harry
3|James
  1. 配置 JDBC 源连接器:
cd /opt/confluent-4.0.0
vi ./etc/kafka-connect-jdbc/source-quickstart-sqlite.properties
name=test-sqlite-jdbc-autoincrement
connector.class=io.confluent.connect.jdbc.JdbcSourceConnector
tasks.max=1
connection.url=jdbc:sqlite:firstdb.db
mode=incrementing
incrementing.column.name=cust_id
topic.prefix=test-sqlite-jdbc-
  1. 配置文件接收器连接器:
cd /opt/confluent-4.0.0
vi etc/kafka/connect-file-sink.properties
name=local-file-sink
connector.class=FileStreamSink
tasks.max=1
file=/opt/confluent-4.0.0/test.sink.txt
topics=test-sqlite-jdbc-customer
  1. 启动卡夫卡连接(.jdbs源和文件接收器):
./bin/connect-standalone ./etc/schema-registry/connect-avro-standalone.properties ./etc/kafka-connect-jdbc/source-quickstart-sqlite.properties ./etc/kafka/connect-file-sink.properties
  1. 验证消费者:
$ ./bin/kafka-avro-console-consumer --new-consumer --bootstrap-server localhost:9092 --topic test-sqlite-jdbc-customer --from-beginning

--new-consumer选项已被弃用,将在未来的主要版本中删除。如果提供了--bootstrap-server选项,则默认使用新消费者:

{"cust_id":1,"cust_name":{"string":"Jon"}}
{"cust_id":2,"cust_name":{"string":"Harry"}}
{"cust_id":3,"cust_name":{"string":"James"}}
  1. 验证目标文件:
tail -f /opt/confluent-4.0.0/test.sink.txt

Struct{cust_id=1,cust_name=Jon}
Struct{cust_id=2,cust_name=Harry}
Struct{cust_id=3,cust_name=James}
  1. 在客户表中再插入几条记录:
sqlite> INSERT INTO customer(cust_id,cust_name) VALUES(4,'Susan');
sqlite> INSERT INTO customer(cust_id,cust_name) VALUES(5,'Lisa');
  1. 验证目标文件:
tail -f /opt/confluent-4.0.0/test.sink.txt

您将在目标文件中看到所有客户记录(cust_id)。使用前面的示例,您可以自定义和实验任何其他接收器。

下表显示了汇流平台上可用的卡夫卡连接器(由汇流开发并完全支持):

| 连接器名称 | 源/汇 | | JDBC | 源和汇 | | HDFS | Flume | | 弹性搜索 | Flume | | 亚马逊 S3 | Flume |

如需了解更多关于汇流公司认证的其他连接器的信息,请使用以下网址:www.confluent.io/product/con…

您一定已经观察到 Kafka Connect 是一个基于配置的流处理框架。这意味着我们只需配置源和接收器连接器文件。我们不需要使用 Java 或 Scala 等低级语言编写任何代码。但是,现在,让我们转向一个更流行的实时流处理框架,称为 Apache Storm 。让我们了解一下 Apache Storm 的一些很酷的特性。

ApacheStorm

Apache Storm 是一个免费的开源分布式实时流处理框架。在写这本书的时候,Apache Storm 的稳定发布版本是 1.0.5。Storm 框架主要是用 Clojure 编程语言编写的。最初,它是由内森·马尔斯和巴克特的团队创建和开发的。该项目后来被推特收购。

在他关于 Storm 框架的一次演讲中,Nathan Marz 谈到了使用任何框架的流处理应用,比如 Storm。这些应用涉及队列和工作线程。一些数据源线程将消息写入队列,而其他线程获取这些消息并写入目标数据存储。这里的主要缺点是源线程和目标线程彼此的数据负载不匹配,这导致了数据堆积。它还会导致数据丢失和额外的线程维护。

为了避免前面的挑战,Nathan Marz 想出了一个很棒的架构,将源线程和工作线程抽象成 tumbles 和 Bolts。这些喷口和螺栓被提交给拓扑框架,该框架负责整个流处理。

ApacheStorm 的特征

ApacheStorm 是分布式的。如果流的工作负载增加,可以将多个节点添加到 Storm 集群中,以添加更多的工作人员和更多的处理能力。

是真正的实时流处理系统,支持低延迟。事件可以在毫秒、秒或分钟内从源到达目标,具体取决于用例。

Storm 框架支持多种编程语言,但 Java 是首选。暴风是容错。即使集群中的任何节点出现故障,它也能继续运行。暴风是可靠的。它支持至少一次或恰好一次的处理。

使用 Storm 框架没有复杂性。有关更多详细信息,请参考 Storm 文档:storm.apache.org/releases/1.…

Storm 拓扑

下图显示了典型的Storm 拓扑:

Storm 拓扑组件

以下部分解释了 Storm 拓扑的所有组件:

  • 拓扑结构 : 拓扑结构是一个由喷口和螺栓组成的 DAG ( 有向无环图)与溪流分组相连。拓扑持续运行,直到被终止。
  • : 流是一个无界的元组序列。元组可以是任何数据类型。它支持所有的 Java 数据类型。
  • 流分组 : 流分组决定哪个螺栓从喷口接收元组。基本上,这些是关于流如何在不同螺栓之间流动的策略。以下是 Storm 中内置的流分组。
  • 洗牌分组 : 是默认的分组策略。元组是随机分布的,每个螺栓得到相同数量的流来处理。
  • 字段分组 : 在此策略中,一个流字段的相同值将被发送到一个螺栓。例如,如果所有元组按customer_id分组,那么相同customer_id的所有元组将被发送到一个螺栓任务,而另一个customer_id的所有元组将被发送到另一个螺栓任务。
  • 全部分组 : 在全部分组中,每个元组被发送到每个螺栓任务。当必须对同一组数据执行两种不同的功能时,可以使用它。在这种情况下,可以复制流,并且可以在数据的每个副本上计算每个函数。
  • 直接分组:这是一种特殊的分组。在这里,开发人员可以定义组件中的分组逻辑,元组本身就是在其中发出的。元组的生产者决定消费者的哪个任务将接收这个元组。
  • 自定义分组 : 开发者可以通过实现CustomGrouping方法来决定实现自己的分组策略。
  • 喷口 : 一个喷口连接到数据源,并将流吸入 Storm 拓扑。
  • 枪栓 : 喷口向枪栓发射一个元组。bolt 负责事件转换、将事件连接到其他事件、过滤、聚合和窗口。它将元组发送给另一个螺栓,或者将它保持在目标上。拓扑中的所有处理都是通过螺栓完成的。螺栓可以做任何事情,从过滤到函数、聚合、连接、与数据库对话等等。
  • Storm 星团:下图显示了一个Storm 星团的所有组成部分:

**

  • Storm 集群节点 : Storm 集群的三个主要节点是光轮、监督者和动物园管理员。下一节详细解释了所有组件。
  • 光轮节点 : 在 Storm 中,这是一个 Storm 集群的主节点。它分发代码并在集群中启动工作者任务。基本上,它为集群中的每个节点分配任务。它还监控提交的每个作业的状态。在任何作业失败的情况下,Nimbus 会将作业重新分配给集群中的不同主管。在光轮不可用的情况下,工人仍将继续工作。但是,没有 Nimbus,工人在必要时不会被重新分配到其他机器上。在节点不可用的情况下,分配给该节点的任务将超时,Nimbus 会将这些任务重新分配给其他机器。在 Nimbus 和 Supervisor 都不可用的情况下,它们需要像什么都没发生一样重新启动,并且不会影响任何工作进程。
  • 主管节点 : 在 Storm 中,这是一个从节点。它通过动物园管理员与光轮交流。它启动和停止主管内部的工作进程。例如,如果 Supervisor 发现某个特定的工作进程已经死亡,那么它会立即重新启动该工作进程。如果 Supervisor 在尝试几次后未能重新启动工作者,那么它会将此情况传达给 Nimbus,Nimbus 会在不同的 Supervisor 节点上重新启动该工作者。
  • 动物园管理员节点 : 它充当 Storm 集群中主(光轮)和从(监督者)之间的协调者。在生产环境中,通常设置一个 Zookeeper 集群,该集群有三个 Zookeeper 实例(节点)。

在单节点集群上安装 Storm

以下是在单台计算机上安装 Storm Cluster 的步骤:

  1. 安装jdk。确保你已经安装了 1.8:
$ java -version

您应该会看到以下输出:

openjdk version "1.8.0_141"
OpenJDK Runtime Environment (build 1.8.0_141-b16)
OpenJDK 64-Bit Server VM (build 25.141-b16, mixed mod
  1. 创建一个文件夹来下载 Storm 的.tar文件:
$ mkdir /opt/storm
$ cd storm
  1. 创建一个文件夹来保存动物园管理员和 Storm 数据:
$ mkdir /usr/local/zookeeper/data
$ mkdir /usr/local/storm/data
  1. 下载动物园管理员和 Storm:
$ wget http://apache.osuosl.org/zookeeper/stable/zookeeper-3.4.10.tar.gz
$ gunzip zookeeper-3.4.10.tar.gz
$ tar -xvf zookeeper-3.4.10.tar
$ wget http://mirrors.ibiblio.org/apache/storm/apache-storm-1.0.5/apache-storm-1.0.5.tar.gz
$ gunzip apache-storm-1.0.5.tar.gz
$ tar -xvf apache-storm-1.0.5.tar
  1. 配置 Zookeeper 并将以下内容设置为 Zookeeper ( zoo.cfg):
$ cd zookeeper-3.4.10
$ vi con/zoo.cfg
tickTime = 2000
dataDir = /usr/local/zookeeper/data
clientPort = 2181
  1. 按如下方式配置 Storm:
$ cd /opt/ apache-storm-1.0.5
$ vi conf/storm.yaml
  1. 添加以下内容:
storm.zookeeper.servers:
 - "127.0.0.1"
 nimbus.host: "127.0.0.1"
 storm.local.dir: "/usr/local/storm/data"
 supervisor.slots.ports:
 - 6700
 - 6701
 - 6702
 - 6703

(对于额外的工作人员,添加更多端口,如 6704 等)

  1. 启动动物园管理员:
$ cd /opt/zookeeper-3.4.10
$ bin/zkServer.sh start &
  1. 启动光轮:
$ cd /opt/ apache-storm-1.0.5
$ bin/storm nimbus &
  1. 启动主管:
$ bin/storm supervisor &
  1. 验证 Storm 用户界面中的安装:
http://127.0.0.1:8080

使用 Storm 开发实时流管道

在本节中,我们将创建以下三条管道:

  • 带卡夫卡-Storm- MySQL 的流媒体管道
  • 卡夫卡-Storm- HDFS -Hive 流媒体管道

在本节中,我们将看到数据流是如何从 Kafka 到 Storm 再到 MySQL 表的。

整个管道的工作原理如下:

  1. 我们将使用卡夫卡控制台-制作人应用编程接口摄取卡夫卡的客户记录(customer_firstnamecustomer_lastname)。
  2. 之后,Storm 会从卡夫卡那里拉消息。
  3. 将建立与 MySQL 的连接。
  4. Storm 将使用 MySQL-Bolt 将记录摄取到 MySQL 表中。MySQL 会自动生成customer_id
  5. MySQL 表数据(customer_idcustomer_firstnamecustomer_lastname)将使用 SQL 进行访问。

我们将开发以下 Java 类:

  • MysqlConnection.java:这个类将与本地 MySQL 数据库建立连接。
  • MysqlPrepare.java:这个类将准备要插入数据库的 SQL 语句。
  • MysqlBolt:这个类是一个 Storm 螺栓框架,把元组从 Kafka 发射到 MySQL。
  • MySQLKafkaTopology:这是一个 Storm 拓扑框架,它构建了一个工作流,将喷口(卡夫卡)绑定到螺栓(MySQL)。这里,我们使用的是一个本地 Storm 集群。

从卡夫卡到 Storm 再到 MySQL 的流媒体管道

下图显示了管道的组件。在这条管道中,我们将了解消息将如何从卡夫卡到 Storm 再到 MySQL 实时流动:

以下是MysqlConnection.java的完整 Java 代码:

package com.StormMysql;
import java.sql.Connection;
import java.sql.DriverManager;
public class MysqlConnection {
private String server_name;
 private String database_name;
 private String user_name;
 private String password;
 private Connection connection;

public MysqlConnection(String server_name, String database_name, String user_name, String password)
 {
 this.server_name=server_name;
 this.database_name=database_name;
 this.user_name=user_name;
 this.password=password;
 }

public Connection getConnection()
 {
 return connection;
 }

public boolean open()
 {
 boolean successful=true;
 try{
 Class.*forName*("com.mysql.jdbc.Driver");
 connection = DriverManager.*getConnection*("jdbc:mysql://"+server_name+"/"+database_name+"?"+"user="+user_name+"&password="+password);
 }catch(Exception ex)
 {
 successful=false;
 ex.printStackTrace();
 }
 return successful;
 }

public boolean close()
 {
 if(connection==null)
 {
 return false;
 }

boolean successful=true;
 try{
 connection.close();
 }catch(Exception ex)
 {
 successful=false;
 ex.printStackTrace();
 }

return successful;
 }
 }

以下是MySqlPrepare.java的完整代码:

package com.StormMysql;
import org.apache.storm.tuple.Tuple;
import java.sql.PreparedStatement;
public class MySqlPrepare {
 private MysqlConnection conn;

public MySqlPrepare(String server_name, String database_name, String user_name, String password)
 {
 conn = new MysqlConnection(server_name, database_name, user_name, password);
 conn.open();
 }

public void persist(Tuple tuple)
 {
 PreparedStatement statement=null;
 try{
 statement = conn.getConnection().prepareStatement("insert into customer (cust_id,cust_firstname, cust_lastname) values (default, ?,?)");
 statement.setString(1, tuple.getString(0));

statement.executeUpdate();
 }catch(Exception ex)
 {
 ex.printStackTrace();
 }finally {
 if(statement != null)
 {
 try{
 statement.close();
 }catch(Exception ex)
 {
 ex.printStackTrace();
 }
 }
 }
 }

public void close()
 {
 conn.close();
 }
 }

以下是MySqlBolt.java的完整代码:

package com.StormMysql;

import java.util.Map;

import org.apache.storm.topology.BasicOutputCollector;
 import org.apache.storm.topology.OutputFieldsDeclarer;
 import org.apache.storm.topology.base.BaseBasicBolt;
 import org.apache.storm.tuple.Fields;
 import org.apache.storm.tuple.Tuple;
 import org.apache.storm.tuple.Values;
 import org.apache.storm.task.TopologyContext;
 import java.util.Map;

public class MySqlBolt extends BaseBasicBolt {

private static final long *serialVersionUID* = 1L;
 private MySqlPrepare mySqlPrepare;

@Override
 public void prepare(Map stormConf, TopologyContext context)
 {
 mySqlPrepare=new MySqlPrepare("localhost", "sales","root","");
 }

public void execute(Tuple input, BasicOutputCollector collector) {
 *//* *TODO Auto-generated method stub* mySqlPrepare.persist(input);
 *//System.out.println(input);* }
@Override
 public void cleanup() {
 mySqlPrepare.close();
 }
}

以下是KafkaMySQLTopology.java的完整代码:

package com.StormMysql;
import org.apache.storm.Config;
 import org.apache.storm.spout.SchemeAsMultiScheme;
 import org.apache.storm.topology.TopologyBuilder;
 import org.apache.storm.kafka.*;
 import org.apache.storm.LocalCluster;
 import org.apache.storm.generated.AlreadyAliveException;
 import org.apache.storm.generated.InvalidTopologyException;
public class KafkaMySQLTopology
 {
 public static void main( String[] args ) throws AlreadyAliveException, InvalidTopologyException
 {
 ZkHosts zkHosts=new ZkHosts("localhost:2181");
String topic="mysql-topic";
 String consumer_group_id="id7";
SpoutConfig kafkaConfig=new SpoutConfig(zkHosts, topic, "", consumer_group_id);
kafkaConfig.scheme=new SchemeAsMultiScheme(new StringScheme());
KafkaSpout kafkaSpout=new KafkaSpout(kafkaConfig);
TopologyBuilder builder=new TopologyBuilder();
 builder.setSpout("KafkaSpout", kafkaSpout);
 builder.setBolt("MySqlBolt", new MySqlBolt()).globalGrouping("KafkaSpout");
LocalCluster cluster=new LocalCluster();
Config config=new Config();
cluster.submitTopology("KafkaMySQLTopology", config, builder.createTopology());
try{
 Thread.*sleep*(10000);
 }catch(InterruptedException ex)
 {
 ex.printStackTrace();
 }
// cluster.killTopology("KafkaMySQLTopology");
 // cluster.shutdown();
}
 }

使用pom.xml文件在 IDE 中构建您的项目。

从卡夫卡到 Storm 再到 HDFS

在本节中,我们将看到数据流将如何从卡夫卡到斯托姆再到 HDFS,并通过一个 Hive 外部表来访问它们。

下图显示了管道的组件。在这条管道中,我们将了解信息将如何实时从卡夫卡到 Storm 再到 HDFS:

整个管道的工作原理如下:

  1. 我们将使用卡夫卡控制台制作人应用编程接口摄取卡夫卡的客户记录(customer_idcustomer_firstnamecustomer_lastname)
  2. 之后,暴风将从卡夫卡那里获取信息
  3. 将建立与 HDFS 的联系
  4. “Storm”将利用 HDFS-博尔特将记录带入 HDFS
  5. 将创建 Hive 外部表来存储(customer_idcustomer_firstnamecustomer_lastname)
  6. 将使用 SQL 访问配置单元表数据(customer_idcustomer_firstnamecustomer_lastname

我们将开发以下 Java 类:

KafkaTopology.java:这是一个 Storm 拓扑框架,它构建了一个工作流,将喷点(卡夫卡)绑定到螺栓(HDFS)。这里我们使用的是本地 Storm 集群。

在前面的示例管道中,可以开发用于数据流解析和转换的多个单独的类来处理卡夫卡生产者和消费者。

以下是KafkaToplogy.java的完整 Java 代码:

package com.stormhdfs;
import org.apache.storm.Config;
 import org.apache.storm.LocalCluster;
 import org.apache.storm.generated.AlreadyAliveException;
 import org.apache.storm.generated.InvalidTopologyException;
 import org.apache.storm.hdfs.bolt.HdfsBolt;
 import org.apache.storm.hdfs.bolt.format.DefaultFileNameFormat;
 import org.apache.storm.hdfs.bolt.format.DelimitedRecordFormat;
 import org.apache.storm.hdfs.bolt.format.RecordFormat;
 import org.apache.storm.hdfs.bolt.rotation.FileRotationPolicy;
 import org.apache.storm.hdfs.bolt.rotation.FileSizeRotationPolicy;
 import org.apache.storm.hdfs.bolt.sync.CountSyncPolicy;
 import org.apache.storm.hdfs.bolt.sync.SyncPolicy;
 import org.apache.storm.kafka.KafkaSpout;
 import org.apache.storm.kafka.SpoutConfig;
 import org.apache.storm.kafka.StringScheme;
 import org.apache.storm.kafka.ZkHosts;
 import org.apache.storm.spout.SchemeAsMultiScheme;
 import org.apache.storm.topology.TopologyBuilder;
public class KafkaTopology {
 public static void main(String[] args) throws AlreadyAliveException, InvalidTopologyException {
// zookeeper hosts for the Kafka clusterZkHosts zkHosts = new ZkHosts("localhost:2181");
// Create the KafkaSpout configuartion
 // Second argument is the topic name
 // Third argument is the zookeeper root for Kafka
 // Fourth argument is consumer group id
SpoutConfig kafkaConfig = new SpoutConfig(zkHosts,
 "data-pipleline-topic", "", "id7");
// Specify that the kafka messages are String
kafkaConfig.scheme = new SchemeAsMultiScheme(new StringScheme());
// We want to consume all the first messages in the topic everytime
 // we run the topology to help in debugging. In production, this
 // property should be false
kafkaConfig.startOffsetTime = kafka.api.OffsetRequest.*EarliestTime*();
RecordFormat format = new DelimitedRecordFormat().withFieldDelimiter("|");
 SyncPolicy syncPolicy = new CountSyncPolicy(1000);
FileRotationPolicy rotationPolicy = new FileSizeRotationPolicy(1.0f,FileSizeRotationPolicy.Units.*MB*);
DefaultFileNameFormat fileNameFormat = new DefaultFileNameFormat();

fileNameFormat.withPath("/user/storm-data");

fileNameFormat.withPrefix("records-");

fileNameFormat.withExtension(".txt");

HdfsBolt bolt =
 new HdfsBolt().withFsUrl("hdfs://127.0.0.1:8020")
 .withFileNameFormat(fileNameFormat)
 .withRecordFormat(format)
 .withRotationPolicy(rotationPolicy)
 .withSyncPolicy(syncPolicy);

// Now we create the topology
TopologyBuilder builder = new TopologyBuilder();

// set the kafka spout class
builder.setSpout("KafkaSpout", new KafkaSpout(kafkaConfig), 1);

// configure the bolts
 // builder.setBolt("SentenceBolt", new SentenceBolt(), 1).globalGrouping("KafkaSpout");
 // builder.setBolt("PrinterBolt", new PrinterBolt(), 1).globalGrouping("SentenceBolt");
builder.setBolt("HDFS-Bolt", bolt ).globalGrouping("KafkaSpout");

// create an instance of LocalCluster class for executing topology in local mode.
LocalCluster cluster = new LocalCluster();
 Config conf = new Config();

// Submit topology for execution
cluster.submitTopology("KafkaTopology", conf, builder.createTopology());

try {
 // Wait for some time before exiting
System.out.println("Waiting to consume from kafka");
 Thread.sleep(10000);
 } catch (Exception exception) {
 System.out.println("Thread interrupted exception : " + exception);
 }

// kill the KafkaTopology
 //cluster.killTopology("KafkaTopology");

// shut down the storm test cluster
 // cluster.shutdown();
}
 }

的配置单元表如下:

CREATE EXTERNAL TABLE IF NOT EXISTS customer (
customer_id INT,
customer_firstname String,
customer_lastname String))
COMMENT 'customer table'
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '|'
STORED AS TEXTFILE
location '/user/storm-data';
$ hive > select * from customer;

其他流行的实时数据流框架

除了 Apache Storm,还有相当多的其他开源实时数据流框架。在这一节中,我将只简单地讨论开源的非商业框架。但是,在这一节的最后,我将提供一些商业供应商产品的网址,这些产品提供了一些非常有趣的功能。

卡夫卡溪流原料药

Kafka Streams 是一个用于构建流应用的库。Kafka Streams 是一个用于构建应用和微服务的客户端库,输入和输出数据存储在 Kafka Clusters 中。卡夫卡流应用编程接口转换和丰富数据。

以下是卡夫卡流应用编程接口的重要特性:

  • 它是开源 Apache Kafka 项目的一部分。
  • 它支持非常低延迟(毫秒)的每记录流处理。卡夫卡流应用编程接口中没有微批处理概念。进入流的每条记录都是单独处理的。
  • 它支持无状态处理(过滤和映射)、有状态处理(连接和聚合)和窗口操作(例如,计算最后一分钟、最后 5 分钟、最后 30 分钟或最后一天的数据量,等等)。
  • 要运行卡夫卡流应用编程接口,不需要构建一个有多台机器的独立集群。开发人员可以在他们的 Java 应用或微服务中使用卡夫卡流应用编程接口来处理实时数据。
  • 卡夫卡流应用编程接口是高度可扩展和容错的。
  • 卡夫卡流应用编程接口是完全不依赖于部署的。它可以部署在裸机、虚拟机、Kubernetes 容器和云上。完全没有限制。流应用编程接口从未在卡夫卡经纪人上部署。它是一个独立的应用,就像部署在 Kafka 代理之外的任何其他 Java 应用一样。
  • 它采用了卡夫卡式的安全模式。
  • 从 0.11.0 版本开始,它只支持一次语义。

让我们再次回顾前面的图像,找出卡夫卡流应用编程接口在整个卡夫卡建筑中的确切位置。

这里有几个有用的网址来详细了解卡夫卡流:

Spark 流

请注意,我们将在第 7 章、*大规模数据处理框架、*中讨论 Spark,这是专门针对 Spark 的。但是,在这一节中,我将讨论 Spark Streaming 的一些重要特性。为了更好的理解,建议读者先学习第 7 章大规模数据处理框架,回来进一步阅读本节,了解更多关于 Spark Streaming 的内容。

通常的做法是使用 Hadoop MapReduce 进行批处理,使用 Apache Storm 进行实时流处理。

使用这两种不同的编程模型会导致代码大小、需要修复的 bug 数量和开发工作量的增加;它还引入了一条学习曲线,并引发了其他问题。Spark Streaming 有助于解决这些问题,并提供了一个可扩展的、高效的、有弹性的和集成的(带有批处理)系统。

Spark Streaming 的优势在于其与批处理相结合的能力。可以使用普通的 Spark 编程创建一个 RDD,并将其与 Spark 流结合起来。此外,代码库是相似的,如果需要的话,允许容易的迁移——并且从 Spark 没有学习曲线。

Spark 流是核心 Spark 应用编程接口的扩展。它扩展了 Spark 来进行实时流处理。Spark 流具有以下特征:

  • 它是可扩展的,可以在数百个节点上扩展
  • 它提供高吞吐量并实现二级延迟
  • 它是容错的,可以有效地从故障中接收信息
  • 它集成了批处理和交互式数据处理

Spark Streaming 将数据流应用作为一系列非常小的确定性批处理作业进行处理。

Spark Streaming 用 Scala、Java 和 Python 提供了一个应用编程接口。Spark Streaming 根据时间将实时数据流分为多个批次。时间范围可以从一秒到几分钟/小时。一般来说,批次分为几秒钟。Spark 将每个批处理视为一个 RDD,并基于 RDD 操作(映射、筛选、连接平面图、distinct、reduceByKey 等)处理每个批处理。最后,RDDs 的处理结果被分批返回。

下图描述了 Spark 流数据流:

这里有几个有用的网址来详细了解 Spark 流:

Apache 很不错

Apache Flink 的文档是这样描述 Flink 的:Flink 是一个开源的分布式流处理框架。

Flink 提供准确的结果,并支持无序或延迟到达的数据集。它是有状态和容错的,可以从故障中无缝恢复,同时保持一次应用状态。它可以大规模运行,在数千个节点上运行,具有非常好的吞吐量和延迟特性。

以下是 Apache Flink 的功能:

  • Flink 保证有状态计算的语义只有一次
  • Flink 支持流处理和带有事件时间语义的窗口
  • 除了数据驱动窗口之外,Flink 还支持基于时间、计数或会话的灵活窗口
  • Flink 能够实现高吞吐量和低延迟
  • Flink 的保存点提供了一种状态版本控制机制,使更新应用或重新处理历史数据成为可能,而不会丢失状态,停机时间也最少
  • Flink 被设计为在具有数千个节点的大规模集群上运行,除了独立集群模式之外,Flink 还提供了对 Yarn 和 Mesos 的支持

Flink 的核心是分布式流式数据流引擎。它支持一次处理一个流,而不是一次处理整批流。

Flink 支持以下库:

  • 圆概率误差(circular error probable)
  • 机器学习
  • 图形处理
  • ApacheStorm 兼容性

Flink 支持以下 API:

  • 数据流应用编程接口:这个应用编程接口帮助所有的流、转换,也就是过滤、聚合、计数和窗口
  • 数据集 API :这个 API 帮助所有的批处理数据转换,也就是连接、分组、映射和过滤
  • 表 API :支持关系数据流上的 SQL
  • 流 SQL :支持批量和流表的 SQL

下图描述了 Flink 编程模型:

下图描述了 Flink 架构:

以下是 Flink 编程模型的组成部分:

  • 来源:收集数据并发送给 Flink 引擎的数据源
  • 转换:在这个组件中,整个转换发生
  • 接收器:发送已处理流的目标

这里有几个有用的网址来详细了解 Spark 流:

在接下来的部分中,我们将看一看各种流框架的比较。

Apache 弗林克对 Spark

Spark 流的主要焦点是流批处理操作,称为微批处理。这种编程模型适合许多用例,但并不是所有用例都需要亚秒延迟的实时流处理。例如,信用卡欺诈防范之类的用例需要毫秒级延迟。因此,微批处理编程模型不适合那里。(但是,最新版本的 Spark 2.4 支持毫秒级数据延迟)。

Apache Flink 支持毫秒级延迟,适合欺诈检测等用例。

ApacheSpark 对 Storm

Spark 使用微批处理来处理事件,而 Storm 则逐个处理事件。这意味着 Spark 有几秒钟的延迟,而 Storm 提供了一毫秒的延迟。Spark 流提供了一个高级抽象,称为离散流数据流,它代表一个连续的 rdd 序列。(但是,最新版本的 Spark,2.4 支持毫秒级的数据延迟。)最新的 Spark 版本支持数据帧。

几乎相同的代码(应用编程接口)可以用于 Spark 流和 Spark 批处理作业。这有助于重用两种编程模型的大部分代码库。此外,Spark 支持机器学习和图形应用编程接口。所以,同样的代码库也可以用于那些用例。

摘要

在本章中,我们首先详细了解了实时流处理概念,包括数据流、批处理与实时处理、CEP、低延迟、连续可用性、水平可扩展性、存储等。后来,我们了解了 Apache Kafka,它是现代实时流数据管道的一个非常重要的组成部分。Kafka 的主要特点是可扩展性、耐用性、可靠性和高吞吐量。

我们还了解了卡夫卡连接;它的体系结构、数据流、源和连接器。我们研究了使用文件源、文件接收器、JDBC 源和文件接收器连接器设计数据管道的案例研究。

在后面的章节中,我们学习了各种开源实时流处理框架,比如 Apache Storm 框架。我们也看到了一些实际的例子。Apache Storm 是分布式的,支持低延迟和多种编程语言。Storm 是容错和可靠的。它支持至少一次或恰好一次的处理。

Spark Streaming 有助于解决这些问题,并提供了一个可扩展的、高效的、有弹性的和集成的(带有批处理)系统。Spark Streaming 的优势在于其与批处理相结合的能力。Spark 流是可扩展的,并提供高吞吐量。它支持二级延迟的微批处理,具有容错能力,并集成了批处理和交互式数据处理。

Apache Flink 保证一次语义,支持事件时间语义,高吞吐量和低延迟。它被设计成在大规模集群上运行。**

七、大规模数据处理框架

随着数据源的数量和复杂性不断增加,从数据中获取价值也变得越来越困难。自从 Hadoop 诞生以来,它已经构建了一个可大规模扩展的文件系统,HDFS。它采用了来自函数式编程的 MapReduce 概念来应对大规模数据处理挑战。随着技术不断发展以克服数据挖掘带来的挑战,企业也在寻找方法来迎接这些变化以保持领先地位。

在本章中,我们将重点介绍这些数据处理解决方案:

  • MapReduce
  • ApacheSpark
  • Spark SQL
  • Spark 流

MapReduce

MapReduce 是从函数式编程中借用的一个概念。数据处理分为地图阶段和缩减阶段,在地图阶段进行数据准备,在缩减阶段计算实际结果。MapReduce 发挥重要作用的原因是,当数据被分割到多个分布式服务器时,我们可以实现巨大的并行性。如果没有这个优势,MapReduce 就无法真正发挥出色。

让我们举一个简单的例子来理解 MapReduce 在函数式编程中是如何工作的:

  • 使用我们选择的映射函数来处理输入数据
  • 映射器函数的输出应该处于 reduce 函数可消耗的状态
  • 映射器函数的输出被馈送到 reduce 函数,以生成必要的结果

让我们用一个简单的程序来理解这些步骤。该程序使用以下文本(随机创建)作为输入:

Bangalore,Onion,60
Bangalore,Chilli,10
Bangalore,Pizza,120
Bangalore,Burger,80
NewDelhi,Onion,80
NewDelhi,Chilli,30
NewDelhi,Pizza,150
NewDelhi,Burger,180
Kolkata,Onion,90
Kolkata,Chilli,20
Kolkata,Pizza,120
Kolkata,Burger,160

输入由以下字段组成的数据:城市名称产品名称当日商品价格

我们想写一个程序,显示给定城市所有产品的总成本。这可以通过多种方式实现。但是让我们尝试使用 MapReduce 来处理这个问题,看看它是如何工作的。

映射程序是这样的:

#!/usr/bin/env perl -wl

use strict;
use warnings;

while(<STDIN>) {
    chomp;
    my ($city, $product, $cost) = split(',');
    print "$city $cost";
}

减少程序是:

#!/usr/bin/perl

use strict;
use warnings;

my %reduce;

while(<STDIN>) {
    chomp;
    my ($city, $cost) = split(/\s+/);
    $reduce{$city} = 0 if not defined $reduce{$city};
    $reduce{$city} += $cost;
}

print "-" x 24;
printf("%-10s : %s\n", "City", "Total Cost");
print "-" x 24;

foreach my $city (sort keys %reduce) {
    printf("%-10s : %d\n", $city, $reduce{$city});
}

我们使用 UNIX 终端创建了一个数据管道,如下所示:

[user@node-1 ~]$ cat input.txt | perl map.pl | perl reduce.pl 
------------------------
City : Total Cost
------------------------
Bangalore : 270
Kolkata : 390
NewDelhi : 440

我们可以看到,结果是意料之中的。这是一个非常简单的 MapReduce 案例。让我们试着看看发生了什么:

  • 每个输入行都由map.pl程序处理,并打印城市和价格
  • map.pl程序的输出被馈送到reduce.pl,它对所有记录执行SUM()操作,并按城市对它们进行分类

让我们洗牌input.txt看看我们是否得到想要的结果。

下面是修改后的input.txt:

Bangalore,Onion,60
NewDelhi,Onion,80
Bangalore,Pizza,120
Bangalore,Burger,80
Kolkata,Onion,90
Kolkata,Pizza,120
Kolkata,Chilli,20
NewDelhi,Chilli,30
NewDelhi,Burger,180
Kolkata,Burger,160
NewDelhi,Pizza,150
Bangalore,Chilli,10

MapReduce 操作的输出是:

[user@node-1 ~]$ cat input-shuffled.txt | perl map.pl | perl reduce.pl 
------------------------
City : Total Cost
------------------------
Bangalore : 270
Kolkata : 390
NewDelhi : 440

没有区别,因为映射和缩减操作都是一次性独立执行的。这里没有数据并行。整个过程可以在该图中可视化:

我们可以看到,贴图阶段后有一个输入数据的副本,还原阶段后的最终输出才是我们感兴趣的。

运行单线程进程是有用的,当我们不需要处理大量数据时,它是必需的。当输入大小是无界的,无法放入单个服务器时,我们需要开始考虑分布式/并行算法来解决手头的问题。

Hadoop MapReduce

Apache MapReduce 是一个框架,它使我们更容易在非常大的分布式数据集上运行 MapReduce 操作。Hadoop 的优势之一是分布式文件系统,它支持机架感知和可扩展。Hadoop 作业调度器足够智能,可以确保计算发生在数据所在的节点上。这也是一个非常重要的方面,因为它减少了网络 IO 的数量。

让我们看看这个框架是如何在这个图表的帮助下使大规模并行计算变得更加容易的:

这个图看起来比上一个图复杂一点,但是大部分事情都是 Hadoop MapReduce 框架自己为我们完成的。我们仍然编写映射和减少输入数据的代码。

让我们从上图中详细看看当我们使用 Hadoop MapReduce 框架处理数据时会发生什么:

  • 我们的输入数据被分解成几部分
  • 每一条数据都被输入到一个映射程序
  • 来自所有映射程序的输出被收集、混洗和排序
  • 每一个被分类的零件都被输入到减速器程序
  • 来自所有减速器的输出被组合以生成输出数据

流式 MapReduce

流式 MapReduce 是 Hadoop MapReduce 框架中可用的功能之一,我们可以使用任何外部程序来充当映射器和缩减器。只要这些程序可以由目标操作系统执行,它们就可以运行映射和缩减任务。

在编写这些程序时,需要记住以下几点:

  • 这些程序应该从STDIN读取输入
  • 他们应该能够处理无限量的数据(流),否则就会崩溃
  • 在将这些程序用于流式 MapReduce 之前,应该提前了解它们的内存需求,否则我们可能会看到不可预测的行为

在前一节中,我们已经编写了简单的 Perl 脚本来进行映射和简化。同样在当前场景中,我们将使用相同的程序来理解它们如何执行我们的任务。

If you observe carefully, map.pl can process infinite amounts of data and will not have any memory overhead. But the reduce.pl program uses the Perl Hash data structure to perform the reduction operation. Here, we might face some memory pressure with real-world data.

在本练习中,我们使用随机输入数据,如下所示:

[user@node-3 ~]$ cat ./input.txt
 Bangalore,Onion,60
 NewDelhi,Onion,80
 Bangalore,Pizza,120
 Bangalore,Burger,80
 Kolkata,Onion,90
 Kolkata,Pizza,120
 Kolkata,Chilli,20
 NewDelhi,Chilli,30
 NewDelhi,Burger,180
 Kolkata,Burger,160
 NewDelhi,Pizza,150
 Bangalore,Chilli,10

稍后,我们需要将映射器和缩减器脚本复制到所有 Hadoop 节点:

We are using the same Hadoop cluster that's built as part of Chapter 10, Production Hadoop Cluster Deployment for this exercise. If you remember, the nodes are master, node-1, node-2, and node-3.

[user@master ~]$ scp *.pl node-1:~
[user@master ~]$ scp *.pl node-2:~
[user@master ~]$ scp *.pl node-3:~

在这一步中,我们将输入复制到hadoop /tmp/ directory

Please use a sensible directory in your production environments as per your enterprise standards. Here the /tmp directory is used for illustration purposes only.

[user@node-3 ~]$ hadoop fs -put ./input.txt /tmp/

在这一步中,我们使用 Hadoop 流式 MapReduce 框架来使用我们的脚本来执行计算:

The contents of the map.pl and reduce.pl are exactly the same as we have used in the previous examples.

[user@node-3 ~]$ hadoop jar \
    /usr/hdp/current/hadoop-mapreduce-client/hadoop-streaming.jar \
    -input hdfs:///tmp/input.txt \
    -output hdfs:///tmp/output-7 \
    -mapper $(pwd)/map.pl \
    -reducer $(pwd)/reduce.pl

输出存储在 HDFS,我们可以这样查看:

[user@node-3 ~]$ hadoop fs -cat /tmp/output-7/part*
 NewDelhi, 440
 Kolkata, 390
 Bangalore, 270
[user@node-3 ~]$

如果我们仔细观察,结果与我们的传统程序完全匹配。

Java MapReduce

在前一节中,我们已经看到了如何使用任意编程语言在 Hadoop 上运行 MapReduce 操作。但是在大多数实际场景中,如果我们利用 Hadoop MapReduce 基础架构提供的库是很好的,因为它们功能强大,能够满足我们的许多需求。

让我们尝试使用 MapReduce 库编写一个简单的 Java 程序,看看我们是否能生成与前面练习中相同的输出。在这个例子中,我们将使用来自官方文档的官方 MapReduce 实现。

文档位于:https://Hadoop . Apache . org/docs/r 2 . 8 . 0/Hadoop-MapReduce-client/Hadoop-MapReduce-client-core/MapReduce tutorial . html

由于我们的输入与示例有很大不同,并且我们还想找到给定城市中所有产品的总价,因此我们必须根据我们的 CSV input.txt文件更改映射程序。reduce 函数与官方文档中的函数相同,我们的 mapper 函数会生成一个<City, Price>对。这很容易被现有的实现所消耗。

我们已经调用了我们的程序TotalPrice.java。让我们看看源代码的样子:

[user@node-3 ~]$ cat TotalPrice.java 
import java.io.IOException;
import java.util.StringTokenizer;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

public class TotalPrice {
  public static class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable>{
    public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
      StringTokenizer itr = new StringTokenizer(value.toString(), ",");
      Text city = new Text(itr.nextToken());
      itr.nextToken();
      IntWritable price = new IntWritable(Integer.parseInt(itr.nextToken()));
      context.write(city, price);
    }
  }

  public static class IntSumReducer extends Reducer<Text,IntWritable,Text,IntWritable> {
  private IntWritable result = new IntWritable();

    public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
      int sum = 0;
      for (IntWritable val : values) {
        sum += val.get();
      }
      result.set(sum);
      context.write(key, result);
    }
  }

  public static void main(String[] args) throws Exception {
    Configuration conf = new Configuration();
    Job job = Job.getInstance(conf, "TotalPriceCalculator");
    job.setJarByClass(TotalPrice.class);
    job.setMapperClass(TokenizerMapper.class);
    job.setCombinerClass(IntSumReducer.class);
    job.setReducerClass(IntSumReducer.class);
    job.setOutputKeyClass(Text.class);
    job.setOutputValueClass(IntWritable.class);
    FileInputFormat.addInputPath(job, new Path(args[0]));
    FileOutputFormat.setOutputPath(job, new Path(args[1]));
    System.exit(job.waitForCompletion(true) ? 0 : 1);
  }
}

一旦我们有了源代码,我们需要编译它来创建一个 Java Archive ( JAR )文件。它以下列方式完成:

 [user@node-3 ~]$ javac -cp `hadoop classpath` TotalPrice.java 
 [user@node-3 ~]$ jar cf tp.jar TotalPrice*.class

一旦我们创建了 JAR 文件,我们就可以使用 Hadoop 命令提交作业来处理input.txt,并在/tmp/output-12目录中产生输出:

As in the case of streaming MapReduce, we need not copy the source to all the Hadoop servers.

 [user@node-3 ~]$ hadoop jar tp.jar TotalPrice /tmp/input.txt /tmp/output-12

这个运行应该通过 fine,并将产生/tmp/output-12目录中的输出文件。我们可以使用以下命令查看输出内容:

[user@node-3 ~]$ hadoop fs -cat /tmp/output-12/part*
Bangalore       270
Kolkata 390
NewDelhi        440

这也与之前的运行完全一致。

正如我们所看到的,Hadoop Mapreduce 框架已经采取了所有必要的步骤来确保整个管道进度保持在其控制范围内,从而为我们提供了期望的结果。

即使我们使用了一个非常简单的数据集进行计算,Hadoop Mapreduce 也能确保无论我们处理的数据大小如何,我们之前编写的相同程序都会产生我们想要的结果。这使得它成为一个非常强大的批处理作业架构。

摘要

到目前为止,我们已经看到 Hadoop Mapreduce 是一个强大的框架,它提供了流式和批处理操作模式,以非常简单的指令处理大量数据。尽管 Mapreduce 最初是 Hadoop 中计算框架的选择,但它未能满足市场不断变化的需求,因此开发了新的架构来解决这些问题。在下一节中,我们将学习一个名为 Apache Spark 的框架。

ApacheSpark 2

Apache Spark 是一个通用的集群计算系统。它非常适合大规模数据处理。当完全在内存中运行时,它的性能比 Hadoop 高 100 倍,当完全从磁盘运行时,它的性能比 Hadoop 高 10 倍。它有一个复杂的有向无环图执行引擎,支持无环数据流模型。

Apache Spark 拥有用 Java、Scala、Python 和 R 编程语言编写程序的一流支持,以迎合更广泛的受众。它提供了 80 多种不同的运营商来构建并行应用,而不用担心底层基础设施。

Apache Spark 拥有迎合结构化查询语言的库,被称为 SparkSQL;这支持在使用 ANSI SQL 的程序中编写查询。它还支持计算流数据,这在当今的实时数据处理需求中非常需要,例如为交互式用户体验系统的仪表板供电。Apache Spark 也有机器学习库,比如 Mlib ,它迎合了运行科学程序的需求。然后它支持为遵循图形数据结构的数据编写程序,称为 GraphX 。这使得它成为一个真正强大的框架,支持最先进的计算方式。

Apache Spark 不仅运行在 Hadoop 平台上,还运行在各种系统上,如 Apache Mesos、Kubernetes、Standalone 或 Cloud。这使得今天的企业选择它想要利用这个系统的力量的方式成为一个完美的选择。

在接下来的部分中,我们将了解更多关于 Spark 及其生态系统的信息。我们在本练习中使用的是 Spark 2.2.0。

使用安巴里安装 Spark

从上一章开始,我们有一个正在运行的现有 Ambari 安装。我们将利用相同的安装来增加 Spark 支持。让我们看看如何实现这一点。

安巴里管理中的服务选择

一旦我们登录到安巴里管理界面,我们会看到创建的主集群。在这个页面上,我们点击左侧菜单上的操作按钮。它显示如下屏幕。从该菜单中,我们单击添加服务选项:

添加服务向导

一旦我们点击添加服务菜单项,我们会看到一个向导,我们必须从安巴里所有支持的服务列表中选择 Spark 2。屏幕如下所示:

服务选择完成后,单击下一步按钮。

服务器放置

一旦选择了 Spark 2 服务,其他相关服务也会自动为我们选择,我们可以选择主服务器的位置。我保留默认选择不变:

当更改看起来不错时,单击“下一步”按钮。

客户和奴隶选择

在这一步中,我们可以选择作为我们在上一步中选择的主节点的客户端的节点列表。我们还可以选择可以安装客户端实用程序的服务器列表。根据您的选择进行选择:

更改完成后,单击“下一步”按钮。

服务定制

由于 Hive 也作为 Spark 2 选择的一部分被安装,我们可以选择定制 Hive 数据源的细节。我在主节点上创建了数据库,用户名为hive,密码为hive,数据库也为hive。在生产中进行更改时,请选择一个强密码。

定制屏幕如下所示:

正确完成更改后,单击下一步。

软件部署

在此屏幕中,我们看到了迄今为止所做选择的摘要。单击部署开始在选定的服务器上部署 Spark 2 软件。如果我们觉得我们错过了任何自定义,我们可以随时取消向导并在此步骤中重新开始:

Spark 安装进度

在这一步中,我们将看到 Spark 软件安装的进度及其其他依赖关系。一旦部署了所有内容,我们会看到任何警告和错误的摘要。从下面的屏幕中我们可以看到,在安装过程中遇到了一些警告,这表明一旦向导完成,我们需要重新启动一些服务。别担心,看到这些错误是很正常的。我们将在接下来的步骤中纠正这些错误,以使 Spark 系统成功运行:

单击完成完成向导。

服务重新启动和清理

由于在安装过程中出现警告,我们必须重新启动所有受影响的组件。该屏幕显示重启过程:

一旦我们给出一个确认,所有相关的服务将重新启动,我们将有一个成功运行的系统。

这就完成了 Spark 2 在由 Ambari 管理的现有 Hadoop 集群上的安装。在接下来的部分中,我们将了解更多关于 Spark 中各种数据结构和库的信息。

Apache Spark 数据结构

尽管 Mapreduce 提供了一种处理大量数据的强大方法,但由于以下几个缺点,它受到了限制:

  • 缺乏对各种运营商的支持
  • 实时数据处理
  • 缓存数据结果以加快迭代速度

仅举几个例子。自从 Apache Spark 从头开始构建以来,它一直以非常通用的方式处理大数据计算问题,并为开发人员提供了数据结构,使其更容易表示任何类型的数据,并以更好的方式使用这些数据进行计算。

关系数据库、数据框架和数据集

Apache Spark 的核心是被称为 RDD 的分布式数据集,也被称为弹性分布式数据集。这些是集群中存在的不可变数据集,具有高可用性和容错性。RDD 中的元素可以并行运行,为 Spark 簇提供大量能量。

由于数据已经存在于存储系统中,如 HDFS、关系数据库管理系统、S3 等,因此可以从这些外部数据源轻松创建关系数据库。该应用编程接口还为我们提供了从现有内存数据元素创建关系数据库的能力。

这些关系数据库没有任何预定义的结构。因此,它们可以采取任何形式,并且通过利用 Spark 库中的不同运算符,我们可以编写强大的程序,为我们提供必要的结果,而不必太担心数据的复杂性。

为了满足关系数据库管理系统的需求,数据帧开始发挥作用,其中数据帧可以与关系数据库系统中的表进行比较。我们知道,表有行和列,数据的结构是提前知道的。通过了解数据的结构,可以在数据处理过程中进行一些优化。

Spark 数据集与数据框有些相似。但是它们通过用本地语言对象(Java 和 Scala)支持半结构化数据对象来扩展数据框架的功能。数据帧是具有关系模式语义的不可变对象集合。因为我们处理的是半结构化数据和本地语言对象,所以有一个编码器/解码器系统负责在类型之间自动转换。

下面是一个快速对比图:

| 功能 | RDDs | 数据帧 | 数据集 | | 数据类型 | 非结构化数据 | 结构数据 | 半结构化数据 | | 模式要求 | 完全自由形式 | 严格的数据类型 | 松散耦合 | | Spark 提供的优化 | 不需要,因为数据是非结构化的 | 利用优化,因为数据类型是已知的 | 推断的数据类型提供了某种程度的优化 | | 高级表达式/过滤器 | 因为数据形式本质上是复杂的,所以很难 | 我们可以利用这些,因为我们知道我们正在处理的数据 | 也可以在这里借力 |

Apache Spark 编程

Apache Spark 有非常好的编程语言支持。它为 Java、Scala、Python 和 R 编程语言提供了一流的支持。尽管编程语言中可用的数据结构和运算符本质上是相似的,但我们必须使用特定于编程语言的结构来实现所需的逻辑。在本章中,我们将使用 Python 作为首选编程语言。然而,Spark 本身对这些编程语言是不可知的,无论使用哪种编程语言,都会产生相同的结果。

使用 Python 的 Apache Spark 可以有两种不同的使用方式。第一种方式是启动pyspark交互外壳,帮助我们运行 Python 指令。体验类似于 Python 外壳实用程序。另一种方法是编写可以使用 spark-submit 命令调用的独立程序。为了使用独立的 Spark 程序,我们必须了解 Spark 程序的基本结构:

spark 程序的典型结构由一个主要函数组成,该函数在 RDDs 上执行不同的运算符来生成所需的结果。Spark 库中支持 80 多种不同类型的操作员。在高层次上,我们可以将这些操作符分为两种类型:转换和动作。转换运算符将数据从一种形式转换为另一种形式。操作从数据中生成结果。出于性能原因,为了优化集群中的资源,Apache Spark 实际上在检查点执行程序。每个检查点只有在有操作符时才会到达。这是一件需要记住的重要事情,尤其是如果你是使用 Spark 编程的新手。即使是最高级的程序员有时也会困惑为什么他们没有看到期望的结果,因为他们没有对数据使用任何操作符。

回到前面的图表,我们有一个驱动程序,它有一个主例程,对存储在像 HDFS 这样的文件系统中的数据执行几个动作/转换,并给出我们想要的结果。我们知道 RDDs 是 Spark 编程语言中的基本并行数据存储。Spark 足够智能,可以像 HDFS 一样从种子存储中创建这些 rdd,一旦创建,它就可以将这些 rdd 缓存在内存中,并通过使这些 rdd 容错来使其高度可用。即使 RDD 的拷贝由于节点崩溃而离线,相同 RDDs 上的未来访问也将从最初生成它的计算中快速生成。

用于分析的样本数据

为了理解 spark 的编程 API,我们应该有一个样本数据集,在这个数据集上我们可以执行一些操作来获得信心。为了生成这个数据集,我们将从上一章的 employees 数据库中导入示例表。

以下是我们生成该数据集的说明:

登录服务器并切换到 Hive 用户:

ssh user@node-3
[user@node-3 ~]$ sudo su - hive

这将把我们放在一个远程 shell 中,我们可以从 MySQL 数据库转储该表:

[hive@node-3 ~]$ mysql -usuperset -A -psuperset -h master employees -e "select * from vw_employee_salaries" > vw_employee_salaries.tsv
[hive@node-3 ~]$ wc -l vw_employee_salaries.tsv 
2844048 vw_employee_salaries.tsv
[hive@node-3 ~]$ 

接下来,我们应该使用以下命令将文件复制到 Hadoop:

[hive@node-3 ~]$ hadoop fs -put ./vw_employee_salaries.tsv /user/hive/employees.csv

现在,数据准备工作已经完成,我们已成功将其复制到 HDFS。我们可以用 Spark 开始使用这些数据。

使用 pyspark 进行交互式数据分析

Apache Spark 发行版附带一个名为 pyspark 的交互式外壳。由于我们处理的是像 Python 这样的解释编程语言,所以我们可以在学习的同时编写交互式程序。

如果你还记得,我们已经安装了带有 Apache Ambari 的 Spark。因此,我们必须遵循 Apache Ambari 的标准目录位置来访问 Spark 相关的二进制文件:

[hive@node-3 ~]$ cd /usr/hdp/current/spark2-client/
[hive@node-3 spark2-client]$ ./bin/pyspark 
Python 2.7.5 (default, Aug  4 2017, 00:39:18) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /__ / .__/\_,_/_/ /_/\_\   version 2.2.0.2.6.4.0-91
      /_/

Using Python version 2.7.5 (default, Aug  4 2017 00:39:18)
SparkSession available as 'spark'.
>>> 

前面的步骤启动了交互式 Spark 外壳。

作为理解 Spark 数据结构的第一步,我们将从 HDFS 加载employees.csv文件,并使用以下指令计算文件中的总行数:

>>> ds = spark.read.text("employees.csv")
>>> ds.count()
2844048                                                                         
>>> 

如我们所见,该计数与 Unix shell 上之前的加载操作相匹配。

现在,让我们尝试从文件中加载前五条记录,并尝试查看数据结构对象的模式:

>>> ds.first()
Row(value=u'emp_no\tbirth_date\tfirst_name\tlast_name\tgender\thire_date\tsalary\tfrom_date\tto_date')
>>> ds.head(5)
[Row(value=u'emp_no\tbirth_date\tfirst_name\tlast_name\tgender\thire_date\tsalary\tfrom_date\tto_date'), Row(value=u'10001\t1953-09-02\tGeorgi\tFacello\tM\t1986-06-26\t60117\t1986-06-26\t1987-06-26'), Row(value=u'10001\t1953-09-02\tGeorgi\tFacello\tM\t1986-06-26\t62102\t1987-06-26\t1988-06-25'), Row(value=u'10001\t1953-09-02\tGeorgi\tFacello\tM\t1986-06-26\t66074\t1988-06-25\t1989-06-25'), Row(value=u'10001\t1953-09-02\tGeorgi\tFacello\tM\t1986-06-26\t66596\t1989-06-25\t1990-06-25')]
>>> ds.printSchema()
root
 |-- value: string (nullable = true)

>>> 

正如我们所看到的,即使我们有一个 CSV(制表符分隔的文件),Spark 已经将该文件读取为由换行符分隔的普通文本文件,并且该模式只包含一个值,该值是字符串数据类型。

在这种操作模式下,我们将每条记录视为一行,我们只能执行几种类型的操作,例如计算给定名称的所有出现次数:

>>> ds.filter(ds.value.contains("Georgi")).count()
2323                                                                            
>>> 

这种操作模式有点类似于日志处理。但是 Spark 真正的力量来自于把数据当作一个有行有列的表,也就是 DataFrames 的力量:

>>> ds = spark.read.format("csv").option("header", "true").option("delimiter", "\t").load("employees.csv")
>>> ds.count()
2844047   
>>> ds.show(5)
+------+----------+----------+---------+------+----------+------+----------+----------+
|emp_no|birth_date|first_name|last_name|gender| hire_date|salary| from_date|   to_date|
+------+----------+----------+---------+------+----------+------+----------+----------+
| 10001|1953-09-02|    Georgi|  Facello|     M|1986-06-26| 60117|1986-06-26|1987-06-26|
| 10001|1953-09-02|    Georgi|  Facello|     M|1986-06-26| 62102|1987-06-26|1988-06-25|
| 10001|1953-09-02|    Georgi|  Facello|     M|1986-06-26| 66074|1988-06-25|1989-06-25|
| 10001|1953-09-02|    Georgi|  Facello|     M|1986-06-26| 66596|1989-06-25|1990-06-25|
| 10001|1953-09-02|    Georgi|  Facello|     M|1986-06-26| 66961|1990-06-25|1991-06-25|
+------+----------+----------+---------+------+----------+------+----------+----------+
only showing top 5 rows

>>> 
>>> ds.printSchema()
root
 |-- emp_no: string (nullable = true)
 |-- birth_date: string (nullable = true)
 |-- first_name: string (nullable = true)
 |-- last_name: string (nullable = true)
 |-- gender: string (nullable = true)
 |-- hire_date: string (nullable = true)
 |-- salary: string (nullable = true)
 |-- from_date: string (nullable = true)
 |-- to_date: string (nullable = true)

>>> 

现在,我们可以看到 Spark 已经自动将输入的 CSV 文本转换为数据帧。但所有字段都被视为字符串。

让我们尝试使用 spark 的模式推断功能来自动查找字段的数据类型:

>>> ds = spark.read.format("csv").option("header", "true").option("delimiter", "\t").option("inferSchema", "true").load("employees.csv")
18/03/25 19:21:15 WARN FileStreamSink: Error while looking for metadata directory.
18/03/25 19:21:15 WARN FileStreamSink: Error while looking for metadata directory.
>>> ds.count()                                                                  
2844047                                                                         
>>> ds.show(2)
+------+-------------------+----------+---------+------+-------------------+------+-------------------+-------------------+
|emp_no|         birth_date|first_name|last_name|gender|          hire_date|salary|          from_date|            to_date|
+------+-------------------+----------+---------+------+-------------------+------+-------------------+-------------------+
| 10001|1953-09-02 00:00:00|    Georgi|  Facello|     M|1986-06-26 00:00:00| 60117|1986-06-26 00:00:00|1987-06-26 00:00:00|
| 10001|1953-09-02 00:00:00|    Georgi|  Facello|     M|1986-06-26 00:00:00| 62102|1987-06-26 00:00:00|1988-06-25 00:00:00|
+------+-------------------+----------+---------+------+-------------------+------+-------------------+-------------------+
only showing top 2 rows

>>> ds.printSchema()
root
 |-- emp_no: integer (nullable = true)
 |-- birth_date: timestamp (nullable = true)
 |-- first_name: string (nullable = true)
 |-- last_name: string (nullable = true)
 |-- gender: string (nullable = true)
 |-- hire_date: timestamp (nullable = true)
 |-- salary: integer (nullable = true)
 |-- from_date: timestamp (nullable = true)
 |-- to_date: timestamp (nullable = true)

>>> 

现在我们可以看到,所有字段都有一个最接近 MySQL 表定义的正确数据类型。

我们可以对数据应用简单的操作来查看结果。让我们试着找出男性记录总数:

>>> ds.filter(ds.gender == "M").count()
1706321 

此外,尝试找到工资超过 10 万美元的男性记录:

>>> ds.filter(ds.gender == "M").filter(ds.salary > 100000).count()
57317   

就这么简单,对吧?在官方的 Spark 文档中,有更多的操作符可供探索。

带 Spark 的独立应用

在前一节中,我们已经看到了如何使用交互外壳pyspark来学习 Spark Python API。在本节中,我们将编写一个简单的 Python 程序,在 Spark 集群上运行。在现实场景中,这是我们在 Spark 集群上运行应用的方式。

为了做到这一点,我们将编写一个名为MyFirstApp.py的程序,内容如下:

[hive@node-3 ~]$ cat MyFirstApp.py 
from pyspark.sql import SparkSession

# Path to the file in HDFS
csvFile = "employees.csv"

# Create a session for this application
spark = SparkSession.builder.appName("MyFirstApp").getOrCreate()

# Read the CSV File
csvTable = spark.read.format("csv").option("header", "true").option("delimiter", "\t").load(csvFile)

# Print the total number of records in this file
print "Total records in the input : {}".format(csvTable.count())

# Stop the application
spark.stop()
[hive@node-3 ~]$ 

为了在 Spark 集群上运行这个程序,我们必须使用 spark-submit 命令,它在调度和协调整个应用生命周期方面做了必要的工作:

[hive@node-3 ~]$ /usr/hdp/current/spark2-client/bin/spark-submit ./MyFirstApp.py 2>&1 | grep -v -e INFO -e WARN
Total records in the input : 2844047

正如预期的那样,这些是我们的输入文件中的记录总数(不包括标题行)。

Spark 流应用

spark 的强大功能之一是构建处理实时流数据并产生实时结果的应用。为了更好地理解这一点,我们将编写一个简单的应用,试图在输入流中找到重复的消息,并打印所有唯一的消息。

当我们处理不可靠的数据流,并且我们只想提交唯一的数据时,这种应用很有帮助。

这里给出了这个应用的源代码:

[hive@node-3 ~]$ cat StreamingDedup.py 
from pyspark import SparkContext
from pyspark.streaming import StreamingContext

context = SparkContext(appName="StreamingDedup")
stream = StreamingContext(context, 5)

records = stream.socketTextStream("localhost", 5000)
records
    .map(lambda record: (record, 1))
    .reduceByKey(lambda x,y: x + y)
    .pprint()

ssc.start()
ssc.awaitTermination()

在这个应用中,我们连接到端口5000上的远程服务,该服务在其自己的页面上发送消息。程序每 5 秒钟总结一次操作结果,如StreamingContext参数所定义。

现在,让我们使用 UNIX netcat 命令(nc)和一个简单的循环来启动一个简单的 TCP 服务器:

for i in $(seq 1 10)
do
  for j in $(seq 1 5)
  do
   sleep 1
   tail -n+$(($i * 3)) /usr/share/dict/words | head -3
  done
done | nc -l 5000

之后,将我们的计划提交给 spark cluster:

[hive@node-3 ~]$ /usr/hdp/current/spark2-client/bin/spark-submit ./StreamingDedup.py 2>&1 | grep -v -e INFO -e WARN

程序启动后,我们会看到以下输出:

-------------------------------------------
Time: 2018-03-26 04:33:45
-------------------------------------------
(u'16-point', 5)
(u'18-point', 5)
(u'1st', 5)

-------------------------------------------
Time: 2018-03-26 04:33:50
-------------------------------------------
(u'2', 5)
(u'20-point', 5)
(u'2,4,5-t', 5)

我们看到每个单词都有正好 5 作为计数,这是预期的,因为我们在 Unix 命令循环中打印了五次。

借助这个图表,我们可以理解这一点:

输入流产生连续的数据流,由Spark 程序实时消耗。之后,通过消除重复项来打印结果

如果我们按照时间顺序来看,从时间零点到时间五秒( T0 - T5 )的数据被处理,结果可以在 T5 时间获得。其他时间段也一样。

在这个简单的例子中,我们刚刚学习了如何使用 Spark Streaming 构建实时应用的基础知识。

Spark SQL 应用

使用 Spark 编写应用时,开发人员可以选择在结构化数据上使用 SQL 来获得所需的结果。一个例子让我们更容易理解如何做到这一点:

[hive@node-3 ~]$ cat SQLApp.py 
from pyspark.sql import SparkSession

# Path to the file in HDFS
csvFile = "employees.csv"

# Create a session for this application
spark = SparkSession.builder.appName("SQLApp").getOrCreate()

# Read the CSV File
csvTable = spark.read.format("csv").option("header", "true").option("delimiter", "\t").load(csvFile)
csvTable.show(3)

# Create a temporary view
csvView = csvTable.createOrReplaceTempView("employees")

# Find the total salary of employees and print the highest salary makers
highPay = spark.sql("SELECT first_name, last_name, emp_no, SUM(salary) AS total FROM employees GROUP BY emp_no, first_name, last_name ORDER BY SUM(salary)")

# Generate list of records
results = highPay.rdd.map(lambda rec: "Total: {}, Emp No: {}, Full Name: {} {}".format(rec.total, rec.emp_no, rec.first_name, rec.last_name)).collect()

# Show the top 5 of them
for r in results[:5]:
    print(r)

# Stop the application
spark.stop()
[hive@node-3 ~]$ 

在这个例子中,我们从employees.csv构建一个数据框架,然后在内存中创建一个名为员工的视图。稍后,我们可以使用 ANSI SQL 来编写和执行查询,以生成必要的结果。

由于我们有兴趣找到收入最高的员工,结果如预期所示:

[hive@node-3 ~]$ /usr/hdp/current/spark2-client/bin/spark-submit ./SQLApp.py 2>&1 | grep -v -e INFO -e WARN
[rdd_10_0]
+------+----------+----------+---------+------+----------+------+----------+----------+
|emp_no|birth_date|first_name|last_name|gender| hire_date|salary| from_date|   to_date|
+------+----------+----------+---------+------+----------+------+----------+----------+
| 10001|1953-09-02|    Georgi|  Facello|     M|1986-06-26| 60117|1986-06-26|1987-06-26|
| 10001|1953-09-02|    Georgi|  Facello|     M|1986-06-26| 62102|1987-06-26|1988-06-25|
| 10001|1953-09-02|    Georgi|  Facello|     M|1986-06-26| 66074|1988-06-25|1989-06-25|
+------+----------+----------+---------+------+----------+------+----------+----------+
only showing top 3 rows

Total: 40000.0, Emp No: 15084, Full Name: Aloke Birke
Total: 40000.0, Emp No: 24529, Full Name: Mario Antonakopoulos
Total: 40000.0, Emp No: 30311, Full Name: Tomofumi Coombs
Total: 40000.0, Emp No: 55527, Full Name: Kellyn Ouhyoung
Total: 40000.0, Emp No: 284677, Full Name: Richara Eastman

正如我们所看到的,Apache Spark 提供的简化的应用编程接口使得在 CSV 数据的基础上编写 SQL 查询变得更加容易(不需要关系数据库管理系统),以获得我们想要的东西。

摘要

在本章中,您了解了大规模数据处理框架的基本概念,并了解到 spark 的强大功能之一是构建处理实时流数据并产生实时结果的应用。

在接下来的几章中,我们将讨论如何使用 Elasticsearch 堆栈构建实时数据搜索管道。

八、构建企业搜索平台

在学习了数据摄取和数据持久化方法之后,让我们学习一下搜索数据。在本章中,我们将了解以下重要事项:

  • 数据搜索技术
  • 构建实时搜索引擎。
  • 搜索实时全文数据
  • 数据索引技术
  • 构建实时数据搜索管道

数据搜索概念

在我们的日常生活中,我们总是不断地寻找一些东西。早上,我们搜索牙刷、报纸,搜索股票价格、公交时刻表、办公包等等。这份名单越列越多。当我们一天结束睡觉时,搜索活动就停止了。我们使用很多工具和技术来搜索这些东西,以最大限度地减少实际搜索时间。我们用谷歌搜索大多数东西,比如新闻、股票价格、公共汽车时刻表,以及我们需要的任何东西。为了搜索一本书的特定页面,我们使用该书的索引。所以,重点是搜索是我们生活中非常重要的活动。有两个重要的概念可以浮出水面,那就是搜索工具和搜索时间。只要想一个你想知道某个公司的某个特定股票价格的情况,加载这个页面需要几分钟的时间。你肯定会很恼火。因为这种情况下的搜索时间是你不能接受的。那么接下来的问题就是,*如何减少这个搜索时间?*我们将在本章中了解这一点。

对企业搜索引擎的需求

就像我们都需要一个工具来搜索自己的东西一样,每个公司也需要一个搜索引擎来构建,这样内部和外部实体就可以找到他们想要的东西。

例如,员工必须搜索他/她的 PTO 余额、特定月份的工资单,等等。人力资源部可能会搜索财务组的员工。在电子商务公司中,产品目录是最容易搜索的对象。这是一个非常敏感的对象,因为它直接影响公司的收入。如果顾客想买一双鞋,他/她能做的第一件事就是搜索公司产品目录。如果搜索时间超过几秒钟,客户可能会对产品失去兴趣。也有可能是同一客户去另一个网站买一双鞋,导致收入损失。

看来,即使有世界上所有的技术和数据,没有两个关键的组成部分,我们也做不了什么:

  • 数据搜索
  • 数据索引

谷歌、亚马逊和苹果等公司改变了世界对搜索的期望。我们都希望他们可以随时随地使用任何工具搜索任何内容,例如网站、手机和语音激活工具,如谷歌回声、Alexa 和 HomePad。我们期待这些工具能回答我们所有的问题,从*今天天气怎么样?*给我一个我附近加油站的名单。

随着这些预期的增长,索引越来越多数据的需求也在增长。

构建企业搜索引擎的工具

以下是一些流行的工具/产品/技术:

  • Apache Lucene
  • 弹性搜索
  • Apache 人索尔
  • 定制(内部)搜索引擎

In this chapter, I will focus on Elasticsearch in detail. I will discuss Apache Solr on a conceptual level only.

弹性搜索

Elasticsearch 是一个开源搜索引擎。它基于 Apache Lucene。它是分布式的,支持多租户功能。它使用无模式的 JSON 文档,并有一个内置的基于 HTTP 的网络接口。它还支持分析性 RESTful 查询工作负载。它是一个基于 Java 的数据库服务器。它的主要协议是 HTTP/JSON。

为什么选择弹性搜索?

Elasticsearch 是目前最流行的数据索引工具。这是因为它具有以下特点:

  • 。数据以实时速度编制索引。
  • 它是可伸缩的。它水平缩放。
  • 灵活。它支持任何数据格式,结构化、半结构化或非结构化。
  • 分布式。如果一个节点出现故障,集群仍可用于业务。
  • 它支持任何语言的数据搜索查询:Java、Python Ruby、C#等等。
  • 它有一个 Hadoop 连接器,,这有助于弹性搜索和 Hadoop 之间的顺畅通信。
  • 它支持在庞大的数据集上进行稳健的数据聚合,以发现趋势和模式。
  • 弹性堆栈 (Beats、Logstash、Elasticsearch 和 Kibana)和 X-Pack 为数据摄取、数据索引、数据可视化、数据安全和监控提供了现成的支持。

弹性搜索组件

在我们深入探讨之前,让我们了解弹性搜索的几个重要组成部分。

索引

Elasticsearch 索引是 JSON 文档的集合。Elasticsearch 是一个可能包含多个索引的数据存储。每个索引可以分为一种或多种类型。类型是一组相似的文档。一个类型可能包含多个文档。就数据库类比而言,索引是一个数据库,它的每种类型都是一个表。每个 JSON 文档都是该表中的一行。

Indices created in Elasticsearch 6.0.0 or later may only contain a single mapping type.

在 Elasticsearch 7.0.0 中将完全删除映射类型。

文件

弹性搜索中的文档是指 JSON 文档。它是存储在索引中的基本数据单元。一个索引包含多个文档。在关系数据库管理系统世界中,文档只不过是表格中的一行。例如,客户文档可能如下所示:

{
"name": "Sam Taylor",
"birthdate": "1995-08-11",
"address":
{
"street": "155 rabbit Street",
"city": "San Francisco",
"state": "ca",
"postalCode": "94107"
},
"contactPhone":
[
{
"type": "home",
"number": "510-415-8929"
},
{
"type": "cell",
"number": "408-171-8187"
}
]
}

绘图

映射是索引的模式定义。就像数据库一样,我们必须定义一个表的数据结构。我们必须创建一个表、它的列和列数据类型。在弹性搜索中,我们已经定义了索引创建过程中的结构。我们可能需要定义哪些字段可以被索引、搜索和存储。

好消息是,Elasticsearch 支持动态映射。这意味着在索引创建时映射不是强制性的。无需映射即可创建索引。当文档发送到弹性搜索进行索引时,弹性搜索会自动定义每个字段的数据结构,并使每个字段成为可搜索字段。

Elasticsearch 是节点(服务器)的集合。每个节点可以将部分数据存储在索引中,并提供跨所有节点的联合索引和搜索功能。默认情况下,每个集群都有一个唯一的名称elasticsearch。集群分为多种类型的节点,即主节点和数据节点。但是,弹性搜索集群可以只使用一个节点来创建,该节点在同一个节点上同时安装了主节点和数据节点:

  • 主节点:控制整个集群。一个集群中可以有多个主节点(建议有三个)。它的主要功能是索引创建或删除以及将碎片(分区)分配给数据节点。
  • 数据节点:这将实际索引数据存储在碎片中。它们支持所有与数据相关的操作,如聚合、索引搜索等。

类型

文档分为各种逻辑类型,例如订单文档、产品文档、客户文档等。不是创建三个单独的订单、产品和客户索引,而是可以将单个索引逻辑上分为订单、产品和客户类型。在关系数据库管理系统的类比中,一个类型只不过是数据库中的一个表。因此,类型是索引的逻辑分区。

Type is deprecated in Elasticsearch version 6.0.

如何在 Elasticsearch 中索引文档?

让我们通过索引这三个示例文档来了解弹性搜索实际上是如何工作的。在学习这一点的同时,我们将触及弹性搜索的一些重要功能/概念。

以下是要索引的三个示例 JSON 文档:

{
"name": "Angela Martin",
"birthdate": "1997-11-02",
"street": "63542 Times Square",
"city": "New York",
"state": "NY",
"zip": "10036",
"homePhone": "212-415-8929",
"cellPhone": "212-171-8187"
} ,
{
"name": "Sam Taylor",
"birthdate": "1995-08-11",
"street": "155 rabbit Street",
"city": "San Francisco",
"state": "ca",
"zip": "94107",
"homePhone": "510-415-8929",
"cellPhone": "408-171-8187"
} ,
{
"name": "Dan Lee",
"birthdate": "1970-01-25",
"street": "76336 1st Street",
"city": "Los Angeles",
"state": "ca",
"zip": "90010",
"homePhone": "323-892-5363",
"cellPhone": "213-978-1320"
}

弹性搜索安装

首先是第一件事。让我们安装弹性搜索。

请执行以下步骤在您的服务器上安装弹性搜索。假设您正在服务器上使用 CentOS 7 安装弹性搜索。

最低硬件要求是什么?

  • RAM : 4 GB
  • CPU : 2

哪个 JDK 需要安装?我们需要 JDK 8 号。如果您的服务器上没有安装 JDK 8,请执行以下步骤安装 JDK 8:

  1. 更改到个人文件夹:
 $ cd ~
  1. 下载 JDK 转速:
$ wget --no-cookies --no-check-certificate --header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u73-b02/jdk-8u73-linux-x64.rpm
  1. 使用 YUM 安装 RMP(假设你有sudo权限):
$ sudo yum -y localinstall jdk-8u73-linux-x64.rpm

既然我们已经成功地在我们的服务器上安装了 JDK 8,让我们开始安装弹性搜索。

弹性搜索的安装

有关详细的安装步骤,请参考以下网址:

https://www . elastic . co/guide/en/elastic search/reference/current/rpm . html

  1. Elasticsearch v6.2.3 的 RPM 可从网站下载并按如下方式安装:
$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.1.2.rpm
$ sudo rpm --install elasticsearch-6.1.2.rpm
  1. 要将弹性搜索配置为在系统启动时自动启动,请运行以下命令。
sudo /bin/systemctl daemon-reload
sudo /bin/systemctl enable elasticsearch.service
  1. 弹性搜索可以按如下方式启动和停止:
sudo systemctl start elasticsearch.service
sudo systemctl stop elasticsearch.service

主配置文件位于名为elasticsearch.yml的配置文件夹中。

让我们在elasticsearch.yml中进行以下初始配置更改。查找并替换以下参数:

cluster.name: my-elaticsearch
path.data: /opt/data
path.logs: /opt/logs
network.host: 0.0.0.0
http.port: 9200

现在开始弹性搜索:

sudo systemctl start elasticsearch.service

使用以下网址检查弹性搜索是否正在运行:

http://localhost:9200

我们将得到以下回应:

// 20180320161034
// http://localhost:9200/
{
 "name": "o7NVnfX",
"cluster_name": "my-elasticsearch",
"cluster_uuid": "jmB-_FEuTb6N_OFokwxF1A",
"version": {
"number": "6.1.2",
"build_hash": "5b1fea5",
"build_date": "2017-01-10T02:35:59.208Z",
"build_snapshot": false,
"lucene_version": "7.1.0",
"minimum_wire_compatibility_version": "5.6.0",
"minimum_index_compatibility_version": "5.0.0"
},
"tagline": "You Know, for Search"
}

现在,我们的弹性研究进展顺利。让我们创建一个索引来存储我们的文档。

创建索引

我们将使用下面的curl命令来创建第一个名为my_index的索引:

curl -XPUT 'localhost:9200/my_index?pretty' -H 'Content-Type: application/json' -d'
{
"settings" : {
"index" : {
"number_of_shards" : 2,
"number_of_replicas" : 1
}
}
}
'

我们会得到这样的回应:

{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "my_index"
}

在索引创建 URL 中,我们使用了设置、碎片和副本。让我们理解什么是碎片和复制品。

主碎片

我们已经用三个碎片创建了索引。这意味着 Elasticsearch 会将索引分成三个分区。每个分区称为一个碎片。每个碎片都是一个完整的、独立的 Lucene 索引。基本思想是弹性搜索将每个碎片存储在一个单独的数据节点上,以提高可扩展性。我们必须提到在创建索引时我们想要多少碎片。然后,弹性搜索会自动处理它。在文档搜索过程中,Elasticsearch 将聚合所有可用碎片中的所有文档,以合并结果,从而满足用户的搜索请求。它对用户是完全透明的。因此,概念是索引可以分成多个碎片,每个碎片可以托管在每个数据节点上。碎片的放置将由 Elasticsearch 本身负责。如果我们没有在索引创建网址中指定碎片的数量,默认情况下,弹性搜索将为每个索引创建五个碎片。

副本碎片

我们已经用一个副本创建了索引。这意味着 Elasticsearch 将为每个分片创建一个副本(复本),并将每个复本放置在不同的数据节点上,而不是复制它的分片上。因此,现在有两个碎片,主碎片(原始碎片)和副本碎片(主碎片的副本)。在大量搜索活动中,弹性搜索可以从主碎片或放置在不同数据节点上的副本碎片中提供查询结果。这就是 Elasticsearch 提高查询吞吐量的方式,因为每个搜索查询可能会到达不同的数据节点。

总之,主碎片和副本碎片都提供了水平可扩展性和吞吐量。它可以扩展您的搜索量/吞吐量,因为搜索可以在所有副本上并行执行。

Elasticsearch 是一个分布式数据存储。这意味着数据可以分为多个数据节点。例如,假设我们只有一个数据节点,并且我们在同一数据节点上继续接收和索引文档,那么在达到该节点的硬件容量后,我们可能无法接收文档。因此,为了容纳更多的文档,我们必须向现有的弹性搜索集群添加另一个数据节点。如果我们添加另一个数据节点,弹性搜索会将碎片重新平衡到新创建的数据节点。所以现在,用户搜索查询可以适应两个数据节点。如果我们创建了一个副本碎片,那么每个碎片将创建两个副本,并放置在这两个数据节点上。现在,如果其中一个数据节点发生故障,那么用户搜索查询将只使用一个数据节点来执行。

此图显示了如何从两个数据节点执行用户搜索查询:

下图显示,即使数据节点 A 关闭,用户查询仍然从数据节点 B 执行:

让我们验证新创建的索引:

curl -XGET 'localhost:9200/_cat/indices?v&amp;amp;amp;pretty'

我们将得到以下回应:

health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
yellow open my_index 2MXqDHedSUqoV8Zyo0l-Lw 5 1 1 0 6.9kb 6.9kb

让我们了解一下回应:

  • **健康:**这表示集群整体健康状态为黄色。有三种状态:绿色、黄色和红色。状态Green表示集群功能齐全,一切正常。状态“黄色”表示群集完全可用,但某些副本尚未分配。在我们的示例中,由于我们只使用一个节点和 5 个碎片,每个碎片有一个副本,因此 Elasticsearch 不会将所有碎片的所有副本分配给一个数据节点。集群状态“红色”表示集群部分可用,某些数据集不可用。原因可能是数据节点关闭或其他原因。
  • 状态 : Open。这意味着集群对企业开放。
  • 指数:指数名称。在我们的例子中,索引名是my_index
  • Uuid :这是唯一的索引 id。
  • 优先级:主碎片数。
  • 代表:副本碎片的数量。
  • 文档数:一个索引中的文档总数。
  • docs.deleted :从一个索引中删除的文档总数。
  • 存储大小:主碎片和副本碎片占用的存储大小。
  • pri.store.size :仅由主碎片获取的存储大小。

将文档纳入索引

以下curl命令可用于摄取my_index索引中的单个文档:

curl -X PUT 'localhost:9200/my_index/customer/1' -H 'Content-Type: application/json' -d '
{
"name": "Angela Martin",
"birthdate": "1997-11-02",
"street": "63542 Times Square",
"city": "New York",
"state": "NY",
"zip": "10036",
"homePhone": "212-415-8929",
"cellPhone": "212-171-8187"
}'

在前面的命令中,我们使用了一个名为customer的类型,它是一个索引的逻辑分区。在关系数据库管理系统的类比中,一个类型就像弹性搜索中的一个表。

另外,我们在客户类型后使用了数字1。这是一个顾客的身份证。如果我们忽略它,那么弹性搜索将为文档生成一个任意的标识。

我们有多个文档要插入my_index索引。在命令行中逐个插入文档是非常繁琐和耗时的。因此,我们可以将所有文档包含在一个文件中,并批量插入my_index

创建一个sample.json文件,包括所有三个文档:

{"index":{"_id":"1"}}

{"name": "Sam Taylor","birthdate": "1995-08-11","address":{"street": "155 rabbit Street","city": "San Francisco","state": "CA","zip": "94107"},"contactPhone":[{"type": "home","number": "510-415-8929"},{"type": "cell","number": "408-171-8187"}]}

{"index":{"_id":"2"}}
{"name": "Dan Lee","birthdate": "1970-01-25","address":{"street": "76336 1st Street","city": "Los Angeles","state": "CA","zip": "90010"},"contactPhone":[{"type": "home","number": "323-892-5363"},{"type": "cell","number": "213-978-1320"}]}

{"index":{"_id":"3"}}

{"name": "Angela Martin","birthdate": "1997-11-02","address":{"street": "63542 Times Square","city": "New York","state": "NY","zip": "10036"},"contactPhone":[{"type": "home","number": "212-415-8929"},{"type": "cell","number": "212-171-8187"}]}

批量插入

让我们使用以下命令一次性摄取文件sample.json中的所有文档:

curl -H 'Content-Type: application/json' -XPUT 'localhost:9200/my_index/customer/_bulk?pretty&amp;amp;amp;refresh' --data-binary "@sample.json"

让我们使用我们最喜欢的浏览器来验证所有记录。它将显示所有三条记录:

http://localhost:9200/my_index/_search

文档搜索

由于我们的my_index索引中有文档,我们可以搜索这些文档:

找出city = " Los Angeles?所在的文档,查询如下:

curl -XGET 'http://localhost:9200/my_index2/_search?pretty' -H 'Content-Type: application/json' -d' {
"query": {
"match": {
"city": "Los Angeles" }
}
}'

回应:

{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 3,"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 1.3862944,
"hits" : [
{
"_index" : "my_index",
"_type" : "customer",
"_id" : "3",
"_score" : 1.3862944,
"_source" : {
"name" : "Dan Lee",
"birthdate" : "1970-01-25",
"street" : "76336 1st Street",
"city" : "Los Angeles", "state" : "ca",
"postalCode" : "90010",
"homePhone" : "323-892-5363",
"cellPhone" : "213-978-1320"
}
}
]
}
}

如果我们分析响应,我们可以看到源部分返回了我们正在寻找的文档。文件在索引my_index"_type" : "customer""_id" : "3"中。Elasticsearch 成功搜索所有three _shards

hits部分下,有一个名为_score的字段。Elasticsearch 计算文档中每个字段的相关频率,并将其存储在索引中。它被称为文档的重量。该权重是基于四个重要因素计算的:术语频率、反向频率、文档频率和字段长度频率。这就引出了另一个问题,【Elasticsearch 如何对文档进行索引?

例如,我们在 Elasticsearch 中有以下四个要索引的文档:

  • 我爱 Elasticsearch
  • 弹性搜索是一个文档存储
  • 糖化血红蛋白是关键值数据存储
  • 我爱巴舍

| 期限 | 频率 | 文件编号 | | a | Two | Two | | 指数 | one | Two | | 弹性搜索 | Two | 1,2 | | 巴什 | Two | one | | 我 | Two | 1,4 | | 是 | Two | 2,3 | | 钥匙 | one | three | | 爱 | Two | 1,4 | | 商店 | Two | 2,3 | | 价值 | one | three |

当我们在 Elasticsearch 中摄取三个文档时,会创建一个倒排索引,如下所示。

现在,如果我们想要查询术语 Elasticsearch,那么只需要搜索两个文档:1 和 2。如果我们运行另一个查询来查找 love Elasticsearch ,那么在只发送第一个文档的结果之前,需要搜索三个文档(文档 1、2 和 4)。

此外,还有一个更重要的概念我们需要理解。

元字段

当我们将一个文档摄取到索引中时,Elasticsearch 会为每个索引文档添加一些元字段。以下是参照我们的示例my_index的元字段列表:

  • _index:索引的名称。my_index
  • _type:映射类型。“客户”(在 6.0 版中已弃用)。
  • _uid:_type + _id(6.0 版本中已弃用)。
  • _id : document_id (1)。
  • _all:这将一个索引的所有字段连接成一个可搜索的字符串(在 6.0 版本中不推荐使用)。
  • _ttl:在文档可以被自动删除之前,将其保存。
  • _timestamp:为文档提供时间戳。
  • _source:这是一个实际的文档,默认自动索引。

绘图

在关系数据库管理系统的类比中,映射意味着定义一个表模式。我们总是定义一个表结构,也就是列数据类型。在 Elasticsearch 中,我们还需要为每个字段定义数据类型。但随之而来的是另一个问题。为什么我们之前把三篇文献摄入my_index索引的时候没有定义?答案很简单。弹性搜索不在乎。据称弹性搜索是一个无模式的数据模型

如果我们没有定义映射,Elasticsearch 会通过将所有字段定义为文本来为我们动态创建映射。Elasticsearch 足够智能,可以找出日期字段并将date数据类型分配给它们。

让我们找到现有的索引my_index的动态映射:

curl -XGET 'localhost:9200/my_index2/_mapping/?pretty'

回应:

{
"my_index" : {
"mappings" : {
customer" : {
"properties" : {
"birthdate" : {
"type" : "date"
},
"cellPhone" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"city" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"homePhone" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"name" : {
"type" : "text",
"fields" : {
"keyword" : {
type" : "keyword",
"ignore_above" : 256
}
}
},
"postalCode" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"state" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"street" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
}

Elasticsearch 支持以下两种映射类型:

  • 静态映射
  • 动态映射

静态映射

在静态映射中,我们总是知道我们的数据,并为每个字段定义适当的数据类型。静态映射必须在创建索引时定义。

动态映射

在我们的示例中,我们已经对文档使用了动态映射。基本上,我们没有为任何字段定义任何数据类型。但是当我们使用_Bulk加载摄取文档时,弹性搜索透明地为每个字段定义了合适的textdate数据类型。Elasticsearch 智能地将我们的Birthdate作为日期字段,并为其分配了date数据类型。

支持弹性搜索的数据类型

以下电子表格总结了弹性搜索中可用的数据类型:

| 常用 | 综合体 | Geo | 专业化 | | 线 | 排列 | 地理点 | ip | | 关键字 | 对象(单个 Json) | 地理形状 | completion | | 日期 | 嵌套(Json 数组) | | token_count | | 长的 | | | join | | 短的 | | | percolator | | 字节 | | | murmur3 | | 两倍 | | | | | 浮动 | | | | | 布尔代数学体系的 | | | | | 二进制的 | | | | | 整数范围 | | | | | 浮动范围 | | | | | 远程 | | | | | 双范围 | | | | | 日期范围 | | | |

大多数数据类型不需要解释。但是以下是对特定数据类型的一些解释:

  • 地理点 : 可以在这里定义经纬度点
  • 地理形状 : 这是用来定义形状的
  • 完成 : 该数据类型用于定义单词的自动完成。
  • 连接:定义父子关系
  • 渗滤器 : 这是查询-dsl
  • mur 3:在索引时间内,用于计算哈希值并将其存储到索引中

**# 映射示例

让我们重新创建另一个索引second_index,它类似于我们的静态映射的first_index,在这里我们将分别定义每个字段的数据类型:

curl -XPUT localhost:9200/second_index -d '{
"mappings": {
"customer": {
"_source": {
"enabled": false
},
"properties": {
"name": {"type": "string", "store": true},
"birthdate": {"type": "string"},
"street": {"type": "string"},
"city": {"type": "date"},
"state": {"type": "string", "index": "no", "store": true}
"zip": {"type": "string", "index": "no", "store": true}}
}
}
}

让我们理解前面的映射。我们禁用客户类型的_source字段。这意味着,我们摆脱了默认行为,默认情况下,弹性搜索存储和索引文档。现在,由于我们已经禁用了它,我们将分别处理每个字段,以决定该字段是应该索引存储还是两者都存储。

因此,在前面的示例中,我们只想存储三个字段:namestatezip。此外,我们不想索引statezip字段。这意味着statezip字段不可搜索。

分析者

我们已经了解了倒排索引。我们知道 Elasticsearch 将文档存储到一个倒排索引中。这种转变被称为分析。这是成功响应索引搜索查询所必需的。

同样,很多时候,我们需要在将文档发送到 Elasticsearch 索引之前使用某种转换。我们可能需要将文档更改为小写,从文档中剥离 HTML 标记(如果有),删除两个单词之间的空白,根据分隔符标记字段,等等。

Elasticsearch 提供以下内置分析器:

  • 标准分析仪 : 是默认分析仪。这使用标准标记器来划分文本。它规范了令牌,降低了令牌的级别,还删除了不需要的令牌。
  • 简单分析器 : 这个分析器由小写的 tokenizer 组成。
  • 空白分析器:这使用空白标记器在空格处划分文本。
  • 语言分析器 : Elasticsearch 提供了很多特定语言的分析器,比如英语等等。
  • 指纹分析仪 : 指纹分析仪是一款专业级分析仪。它创建了一个指纹,可用于重复检测。
  • 模式分析器 : 模式分析器使用正则表达式将文本拆分为术语。
  • 停止分析器 : 这使用字母标记器来划分文本。它从令牌流中移除停止字。例如,像 a、an、the、is 等所有停止字。
  • 关键词分析器 : 该分析器将整个流标记为单个标记。它可以用于邮政编码。
  • 字符过滤器:在字符串被标记之前准备一个字符串。示例:移除 html 标记。
  • 标记器:必须有一个标记器。它用于将字符串分解成单独的术语或标记。
  • 令牌过滤器:更改、添加或删除令牌。斯特梅尔是一个令牌过滤器,它用来获取单词的基数,例如:学会了,学习= >学会了

标准分析仪示例:

curl -XPOST 'localhost:9200/_analyze?pretty' -H 'Content-Type: application/json' -d'
{
"analyzer": "standard",
"text": " 1\. Today it's a Sunny-day, very Bright."
}'

回应:

[today, it's , a, sunny, day, very, bright ]

简单分析仪示例:

curl -XPOST 'localhost:9200/_analyze?pretty' -H 'Content-Type: application/json' -d'
{
"analyzer": "simple",
"text": " 1\. Today it's a Sunny-day, very Bright."
}'

回应:

[today, it's , a, sunny, day, very, bright ]

Elasticsearch 堆栈组件

弹性搜索堆栈包括以下内容

  • 搜索
  • logstash(日志记录)
  • 弹性搜索
  • 姆纳人

让我们简单地研究一下。

搜索

请参考以下网址了解更多节拍:www.elastic.co/products/be…

Beats 是轻量级数据托运人。Beats 作为代理安装到服务器上。他们的主要功能是收集数据并将其发送到 Logstash 或 Elasticsearch。我们还可以配置 beats 向卡夫卡主题发送数据。

有多个节拍。每个节拍都是为了收集特定的数据集和指标。以下是各种类型的节拍:

  • 文件节拍 : 用于收集日志文件。它们将常见日志格式的收集、解析和可视化简化为一个命令。Filebeat 自带内部模块(auditd、Apache、nginx、system 和 MySQL)。
  • Metricbeat : 用于收集指标。他们从任何系统和服务收集指标,例如内存、COU 和磁盘。Metricbeat 是一种发送系统和服务统计数据的轻量级方法。
  • Packetbeat :这是为了收集网络数据。Packetbeat 是一个轻量级的网络数据包分析器,它将数据发送到 Logstash 或 Elasticsearch。
  • Winlogbeat : 用于采集 Windows 事件数据。Winlogbeat 实时流窗口事件日志到弹性搜索和日志存储。
  • Auditbeat : 用于采集审计数据。Auditbeat 收集审核框架数据。
  • 心跳 : 用于采集正常运行时间监测数据。心跳将这些信息和响应时间发送给弹性搜索。

文件节拍的安装:

$wget https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-6.1.2-x86_64.rpm
$ sudo rpm --install filebeat-6.1.2-x86_64.rpm
sudo /bin/systemctl daemon-reload
sudo /bin/systemctl enable filebeat.service

logstash(日志记录)

Logstash 是一个轻量级的开源数据处理管道。它允许从各种各样的来源收集数据,动态转换数据,并将其发送到任何想要的目的地。

它最常被用作流行的分析和搜索引擎 Elasticsearch 的数据管道。Logstash 是将数据加载到 Elasticsearch 的流行选择,因为它紧密集成,强大的日志处理功能,以及 200 多个预构建的开源插件,可以帮助您按照自己想要的方式对数据进行索引。

以下是Logstash.conf的结构:

input {
...
}
filter {
...
}
output {
..
}

Logstash 的安装:

$ wget https://artifacts.elastic.co/downloads/logstash/logstash-6.1.2.rpm
$ sudo rpm --install logstash-6.1.2.rpm
$ sudo /bin/systemctl daemon-reload
$ sudo systemctl start logstash.service

姆纳人

Kibana 是一个开源的数据可视化和探索工具,用于日志和时间序列分析、应用监控和运营智能用例。Kibana 与流行的分析和搜索引擎 Elasticsearch 紧密集成,这使得 Kibana 成为可视化存储在 Elasticsearch 中的数据的默认选择。Kibana 也因其强大且易于使用的功能而广受欢迎,例如直方图、折线图、饼图、热图以及内置的地理空间支持**。**

基巴纳的安装:

$wget https://artifacts.elastic.co/downloads/kibana/kibana-6.1.2-x86_64.rpm
$ sudo rpm --install kibana-6.1.2-x86_64.rpm
sudo /bin/systemctl daemon-reload
sudo /bin/systemctl enable kibana.service

用例

让我们假设在应用服务器上部署了一个应用。该应用正在登录访问日志。那么我们如何使用仪表板来分析这个访问日志呢?我们希望创建以下信息的实时可视化:

  • 各种响应代码的数量
  • 响应总数
  • 综合方案清单

建议的技术堆栈:

  • Filebeat :读取访问日志,写入卡夫卡主题
  • **卡夫卡:**消息队列和 o 缓冲区消息
  • Logstash: 从 Kafka 提取消息并写入 Elasticsearch 索引
  • 弹性搜索:用于索引消息
  • 基巴纳:仪表盘可视化

为了解决这个问题,我们在 Appserver 上安装了 filebeat。Filebeat 将从访问日志中读取每一行,并实时写入 kafka 主题。信息将在卡夫卡缓冲。Logstash 将从卡夫卡主题中提取消息,并写入 Elasticsearch。

Kibana 将通过读取 Elasticsearch 索引中的消息来创建实时流式仪表板。下面是我们用例的架构:

下面是一步一步的代码示例,Acccss.log:

127.0.0.1 - - [21/Mar/2017:13:52:29 -0400] "GET /web-portal/performance/js/common-functions.js HTTP/1.1" 200 3558
127.0.0.1 - - [21/Mar/2017:13:52:30 -0400] "GET /web-portal/performance/js/sitespeed-functions.js HTTP/1.1" 200 13068
127.0.0.1 - - [21/Mar/2017:13:52:34 -0400] "GET /web-portal/img/app2-icon-dark.png HTTP/1.1" 200 4939
127.0.0.1 - - [21/Mar/2017:13:52:43 -0400] "GET /web-search-service/service/performanceTest/release/list HTTP/1.1" 200 186
127.0.0.1 - - [21/Mar/2017:13:52:44 -0400] "GET /web-portal/performance/fonts/opan-sans/OpenSans-Light-webfont.woff HTTP/1.1" 200 22248
127.0.0.1 - - [21/Mar/2017:13:52:44 -0400] "GET /web-portal/performance/img/icon/tile-actions.png HTTP/1.1" 200 100
127.0.0.1 - - [21/Mar/2017:13:52:44 -0400] "GET /web-portal/performance/fonts/fontawesome/fontawesome-webfont.woff?v=4.0.3 HTTP/1.1" 200 44432

以下是完整的Filebeat.ymal:

在卡夫卡输出部分,我们已经提到了卡夫卡经纪人的细节。output.kafka:

# initial brokers for reading cluster metadata
hosts: ["localhost:6667"]

以下是完整的Filebeat.ymal:

###################### Filebeat Configuration Example #########################
# This file is an example configuration file highlighting only the most common
# options. The filebeat.reference.yml file from the same directory contains all the
# supported options with more comments. You can use it as a reference.
#
# You can find the full configuration reference here:
# https://www.elastic.co/guide/en/beats/filebeat/index.html
# For more available modules and options, please see the filebeat.reference.yml sample
# configuration file.
#======================== Filebeat prospectors========================
filebeat.prospectors:
# Each - is a prospector. Most options can be set at the prospector level, so
# you can use different prospectors for various configurations.
# Below are the prospector specific configurations.
- type: log
# Change to true to enable this prospector configuration.
enabled: true
# Paths that should be crawled and fetched. Glob based paths.
paths:
- /var/log/myapp/*.log
#- c:programdataelasticsearchlogs*
#json.keys_under_root: true
#json.add_error_key: true
# Exclude lines. A list of regular expressions to match. It drops the lines that are
# matching any regular expression from the list.
#exclude_lines: ['^DBG']
# Include lines. A list of regular expressions to match. It exports the lines that are
# matching any regular expression from the list.
#include_lines: ['^ERR', '^WARN']
# Exclude files. A list of regular expressions to match. Filebeat drops the files that
# are matching any regular expression from the list. By default, no files are dropped.
#exclude_files: ['.gz$']
# Optional additional fields. These fields can be freely picked
# to add additional information to the crawled log files for filtering
#fields:
# level: debug
# review: 1
fields:
app: myapp
env: dev
dc: gce
### Multiline options
# Mutiline can be used for log messages spanning multiple lines. This is common
# for Java Stack Traces or C-Line Continuation
# The regexp Pattern that has to be matched. The example pattern matches all lines starting with [#multiline.pattern: ^[
# Defines if the pattern set under pattern should be negated or not. Default is false.
#multiline.negate: false
# Match can be set to "after" or "before". It is used to define if lines should be append to a pattern
# that was (not) matched before or after or as long as a pattern is not matched based on negate.
# Note: After is the equivalent to previous and before is the equivalent to to next in Logstash
#multiline.match: after
#============================= Filebeat modules ===============================
filebeat.config.modules:
# Glob pattern for configuration loading
path: ${path.config}/modules.d/*.yml
# Set to true to enable config reloading
reload.enabled: false
# Period on which files under path should be checked for changes
#reload.period: 10s
#==================== Elasticsearch template setting ==========================
setup.template.settings:
index.number_of_shards: 3
#index.codec: best_compression
#_source.enabled: false
#================================ General =====================================
# The name of the shipper that publishes the network data. It can be used to group
# all the transactions sent by a single shipper in the web interface.
#name:
# The tags of the shipper are included in their own field with each
# transaction published.
#tags: ["service-X", "web-tier"]
# Optional fields that you can specify to add additional information to the
# output.
#fields:
# env: staging
#============================== Dashboards =====================================
# These settings control loading the sample dashboards to the Kibana index. Loading
# the dashboards is disabled by default and can be enabled either by setting the
# options here, or by using the `-setup` CLI flag or the `setup` command.
#setup.dashboards.enabled: false
# The URL from where to download the dashboards archive. By default this URL
# has a value which is computed based on the Beat name and version. For released
# versions, this URL points to the dashboard archive on the artifacts.elastic.co
# website.
#setup.dashboards.url:
#============================== Kibana =====================================
# Starting with Beats version 6.0.0, the dashboards are loaded via the Kibana API.
# This requires a Kibana endpoint configuration.
setup.kibana:
# Kibana Host
# Scheme and port can be left out and will be set to the default (http and 5601)
# In case you specify and additional path, the scheme is required: http://localhost:5601/path
# IPv6 addresses should always be defined as: https://[2001:db8::1]:5601
#host: "localhost:5601"
#============================= Elastic Cloud ==================================
# These settings simplify using filebeat with the Elastic Cloud (https://cloud.elastic.co/).
# The cloud.id setting overwrites the `output.elasticsearch.hosts` and
# `setup.kibana.host` options.
# You can find the `cloud.id` in the Elastic Cloud web UI.
#cloud.id:
# The cloud.auth setting overwrites the `output.elasticsearch.username` and
# `output.elasticsearch.password` settings. The format is `<user>:<pass>`.
#cloud.auth:
#================================ Outputs =====================================
# Configure what output to use when sending the data collected by the beat.
#-----------------------------------Kafka Output-------------------------------
output.kafka: # initial brokers for reading cluster metadata hosts: ["localhost:6667"] # message topic selection + partitioning
topic: logs-topic
partition.round_robin:
reachable_only: false
required_acks: 1
compression: gzip
max_message_bytes: 1000000
#-------------------------- Elasticsearch output ------------------------------
#output.elasticsearch:
# Array of hosts to connect to.
#hosts: ["localhost:9200"]
# Optional protocol and basic auth credentials.
#protocol: "https"
#username: "elastic"
#password: "changeme"
#----------------------------- Logstash output --------------------------------#output.logstash:
# The Logstash hosts
#hosts: ["localhost:5044"]
# Optional SSL. By default is off.
# List of root certificates for HTTPS server verifications
#ssl.certificate_authorities: ["/etc/pki/root/ca.pem"]
# Certificate for SSL client authentication
#ssl.certificate: "/etc/pki/client/cert.pem"
# Client Certificate Key
#ssl.key: "/etc/pki/client/cert.key"
#================================ Logging =====================================
# Sets log level. The default log level is info.
# Available log levels are: error, warning, info, debug
logging.level: debug
# At debug level, you can selectively enable logging only for some components.
# To enable all selectors use ["*"]. Examples of other selectors are "beat",
# "publish", "service".
#logging.selectors: ["*"]
#============================== Xpack Monitoring ===============================
# filebeat can export internal metrics to a central Elasticsearch monitoring
# cluster. This requires xpack monitoring to be enabled in Elasticsearch. The
# reporting is disabled by default.
# Set to true to enable the monitoring reporter.
#xpack.monitoring.enabled: false
# Uncomment to send the metrics to Elasticsearch. Most settings from the
# Elasticsearch output are accepted here as well. Any setting that is not set is
# automatically inherited from the Elasticsearch output configuration, so if you
# have the Elasticsearch output configured, you can simply uncomment the
# the following line.
#xpack.monitoring.elasticsearch:

我们必须在卡夫卡中创造一个logs-topic主题,然后才能开始在其中吸收信息。假设我们已经在服务器上安装了卡夫卡。更多关于卡夫卡的内容,请参考第二章Hadoop 生命周期管理

创建日志-主题:

bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic logs-topic

以下是Logstash.conf(阅读卡夫卡的留言,并推至 Elasticseach):

input
{
kafka
{
bootstrap_servers => "127.0.0.1:6667"
group_id => "logstash_logs"
topics => ["logs-topic"]
consumer_threads => 1
type => "kafka_logs"
}
}
filter {
if [type] == "kafka_logs"
{
json {
source => "message"
}
grok {
match => { "message" => "%{IP:ip} - - [%{GREEDYDATA:log_timestamp}] %{GREEDYDATA:middle} %{NUMBER:status} %{NUMBER:bytes}" }
}
mutate {
add_field => {
"App" => "%{[fields][app]}"
}
}
}
}
output {
if [App] == "myapp"
{
elasticsearch
{
action => "index"
codec => "plain"
hosts => ["http://127.0.0.1:9200"]
index => "log_index-%{+YYYY-MM-dd}"
}
}
}

在卡夫卡部分,我们提到了以下事情:

Kafka bootstrap_servers => "127.0.0.1:6667"
Kafka topics => ["logs-topic"]

在过滤器部分**、**我们将每条消息转换为 JSON 格式。之后,我们解析每条消息,并将其分成多个字段,如iptimestampstatus。此外,我们将应用名称myapp字段添加到每个消息中。

在输出部分,我们将每条消息写入弹性搜索。索引名为log_index-YYYY-MM-dd

摘要

在本章中,您了解了弹性搜索集群的基本概念和组件。

之后,我们讨论了 Elasticsearch 如何使用倒排索引对文档进行索引。我们还讨论了映射和分析技术。我们学习了如何在加入弹性搜索之前对事件进行反规格化。我们讨论了弹性搜索如何使用水平可伸缩性和吞吐量。在了解了诸如 Beats、Logstash 和 Kibana 等 Elasticstack 组件之后,我们处理了一个真实的用例,在这个用例中,我们演示了如何使用 Filebeat 将访问日志事件摄入 Kafka。我们开发了一个代码来从卡夫卡那里提取消息,并使用 Logstash 将其摄入弹性搜索。最后,我们使用 Kibana 学习了数据可视化。

在下一章中,我们将了解如何构建分析来设计推动业务决策的数据可视化解决方案。**