精通-Spark-全-

精通 Spark(全)

原文:zh.annas-archive.org/md5/5211DAC7494A736A2B4617944224CFC3

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

已经写了一本关于 Hadoop 生态系统的介绍性书籍,我很高兴 Packt 邀请我写一本关于 Apache Spark 的书。作为一个有支持和维护背景的实用主义者,我对系统构建和集成很感兴趣。因此,我总是问自己“系统如何被使用?”,“它们如何相互配合?”,“它们与什么集成?”在本书中,我将描述 Spark 的每个模块,并通过实际例子解释它们如何被使用。我还将展示如何通过额外的库(如来自h2o.ai/的 H2O)扩展 Spark 的功能。

我将展示 Apache Spark 的图处理模块如何与 Aurelius(现在是 DataStax)的 Titan 图数据库一起使用。这将通过将 Spark GraphX 和 Titan 组合在一起,提供基于图的处理和存储的耦合。流处理章节将展示如何使用 Apache Flume 和 Kafka 等工具将数据传递给 Spark 流。

考虑到过去几年已经有大规模迁移到基于云的服务,我将检查databricks.com/提供的 Spark 云服务。我将从实际的角度来做,本书不试图回答“服务器还是云”的问题,因为我认为这是另一本书的主题;它只是检查了可用的服务。

本书涵盖的内容

第一章 Apache Spark,将全面介绍 Spark,其模块的功能以及用于处理和存储的工具。本章将简要介绍 SQL、流处理、GraphX、MLlib、Databricks 和 Hive on Spark 的细节。

第二章 Apache Spark MLlib,涵盖了 MLlib 模块,其中 MLlib 代表机器学习库。它描述了本书中将使用的 Apache Hadoop 和 Spark 集群,以及涉及的操作系统——CentOS。它还描述了正在使用的开发环境:Scala 和 SBT。它提供了安装和构建 Apache Spark 的示例。解释了使用朴素贝叶斯算法进行分类的示例,以及使用 KMeans 进行聚类的示例。最后,使用 Bert Greevenbosch(www.bertgreevenbosch.nl)的工作扩展 Spark 以包括一些人工神经网络(ANN)工作的示例。我一直对神经网络很感兴趣,能够在本章中使用 Bert 的工作(在得到他的许可后)是一件令人愉快的事情。因此,本章的最后一个主题是使用简单的 ANN 对一些小图像进行分类,包括扭曲的图像。结果和得分都相当不错!

第三章 Apache Spark Streaming,涵盖了 Apache Spark 与 Storm 的比较,特别是 Spark Streaming,但我认为 Spark 提供了更多的功能。例如,一个 Spark 模块中使用的数据可以传递到另一个模块中并被使用。此外,正如本章所示,Spark 流处理可以轻松集成大数据移动技术,如 Flume 和 Kafka。

因此,流处理章节首先概述了检查点,并解释了何时可能需要使用它。它给出了 Scala 代码示例,说明了如何使用它,并展示了数据如何存储在 HDFS 上。然后,它继续给出了 Scala 的实际示例,以及 TCP、文件、Flume 和 Kafka 流处理的执行示例。最后两个选项通过处理 RSS 数据流并最终将其存储在 HDFS 上来展示。

第四章 Apache Spark SQL,用 Scala 代码术语解释了 Spark SQL 上下文。它解释了文本、Parquet 和 JSON 格式的文件 I/O。使用 Apache Spark 1.3,它通过示例解释了数据框架的使用,并展示了它们提供的数据分析方法。它还通过基于 Scala 的示例介绍了 Spark SQL,展示了如何创建临时表,以及如何对其进行 SQL 操作。

接下来,介绍了 Hive 上下文。首先创建了一个本地上下文,然后执行了 Hive QL 操作。然后,介绍了一种方法,将现有的分布式 CDH 5.3 Hive 安装集成到 Spark Hive 上下文中。然后展示了针对此上下文的操作,以更新集群上的 Hive 数据库。通过这种方式,可以创建和调度 Spark 应用程序,以便 Hive 操作由实时 Spark 引擎驱动。

最后,介绍了创建用户定义函数(UDFs),然后使用创建的 UDFs 对临时表进行 SQL 调用。

第五章 Apache Spark GraphX,介绍了 Apache Spark GraphX 模块和图形处理模块。它通过一系列基于示例的图形函数工作,从基于计数到三角形处理。然后介绍了 Kenny Bastani 的 Mazerunner 工作,该工作将 Neo4j NoSQL 数据库与 Apache Spark 集成。这项工作已经得到 Kenny 的许可;请访问www.kennybastani.com

本章通过 Docker 的介绍,然后是 Neo4j,然后介绍了 Neo4j 接口。最后,通过提供的 REST 接口介绍了一些 Mazerunner 提供的功能。

第六章 基于图形的存储,检查了基于图形的存储,因为本书介绍了 Apache Spark 图形处理。我寻找一个能够与 Hadoop 集成、开源、能够高度扩展,并且能够与 Apache Spark 集成的产品。

尽管在社区支持和开发方面仍然相对年轻,但我认为 Aurelius(现在是 DataStax)的 Titan 符合要求。截至我写作时,可用的 0.9.x 版本使用 Apache TinkerPop 进行图形处理。

本章提供了使用 Gremlin shell 和 Titan 创建和存储图形的示例。它展示了如何将 HBase 和 Cassandra 用于后端 Titan 存储。

第七章 使用 H2O 扩展 Spark,讨论了在h2o.ai/开发的 H2O 库集,这是一个可以用来扩展 Apache Spark 功能的机器学习库系统。在本章中,我研究了 H2O 的获取和安装,以及用于数据分析的 Flow 接口。还研究了 Sparkling Water 的架构、数据质量和性能调优。

最后,创建并执行了一个深度学习的示例。第二章 Spark MLlib,使用简单的人工神经网络进行神经分类。本章使用了一个高度可配置和可调整的 H2O 深度学习神经网络进行分类。结果是一个快速而准确的训练好的神经模型,你会看到的。

第八章 Spark Databricks,介绍了databricks.com/ AWS 基于云的 Apache Spark 集群系统。它提供了逐步设置 AWS 账户和 Databricks 账户的过程。然后,它逐步介绍了databricks.com/账户功能,包括笔记本、文件夹、作业、库、开发环境等。

它检查了 Databricks 中基于表的存储和处理,并介绍了 Databricks 实用程序功能的 DBUtils 包。这一切都是通过示例完成的,以便让您对这个基于云的系统的使用有一个很好的理解。

第九章,Databricks 可视化,通过专注于数据可视化和仪表板来扩展 Databricks 的覆盖范围。然后,它检查了 Databricks 的 REST 接口,展示了如何使用各种示例 REST API 调用远程管理集群。最后,它从表的文件夹和库的角度看数据移动。

本章的集群管理部分显示,可以使用 Spark 发布的脚本在 AWS EC2 上启动 Apache Spark。databricks.com/服务通过提供一种轻松创建和调整多个基于 EC2 的 Spark 集群的方法,进一步提供了这种功能。它为集群管理和使用提供了额外的功能,以及用户访问和安全性,正如这两章所示。考虑到为我们带来 Apache Spark 的人们创建了这项服务,它一定值得考虑和审查。

本书所需内容

本书中的实际示例使用 Scala 和 SBT 进行基于 Apache Spark 的代码开发和编译。还使用了基于 CentOS 6.5 Linux 服务器的 Cloudera CDH 5.3 Hadoop 集群。Linux Bash shell 和 Perl 脚本都用于帮助 Spark 应用程序并提供数据源。在 Spark 应用程序测试期间,使用 Hadoop 管理命令来移动和检查数据。

考虑到之前的技能概述,读者对 Linux、Apache Hadoop 和 Spark 有基本的了解会很有帮助。话虽如此,鉴于今天互联网上有大量信息可供查阅,我不想阻止一个勇敢的读者去尝试。我相信从错误中学到的东西可能比成功更有价值。

这本书是为谁准备的

这本书适用于任何对 Apache Hadoop 和 Spark 感兴趣的人,他们想了解更多关于 Spark 的知识。它适用于希望了解如何使用 Spark 扩展 H2O 等系统的用户。对于对图处理感兴趣但想了解更多关于图存储的用户。如果读者想了解云中的 Apache Spark,那么他/她可以了解由为他们带来 Spark 的人开发的databricks.com/。如果您是具有一定 Spark 经验的开发人员,并希望加强对 Spark 世界的了解,那么这本书非常适合您。要理解本书,需要具备 Linux、Hadoop 和 Spark 的基本知识;同时也需要合理的 Scala 知识。

约定

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

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“第一步是确保/etc/yum.repos.d目录下存在 Cloudera 存储库文件,在服务器 hc2nn 和所有其他 Hadoop 集群服务器上。”

代码块设置如下:

export AWS_ACCESS_KEY_ID="QQpl8Exxx"
export AWS_SECRET_ACCESS_KEY="0HFzqt4xxx"

./spark-ec2  \
    --key-pair=pairname \
    --identity-file=awskey.pem \
    --region=us-west-1 \
    --zone=us-west-1a  \
    launch cluster1

任何命令行输入或输出都是这样写的:

[hadoop@hc2nn ec2]$ pwd

/usr/local/spark/ec2

[hadoop@hc2nn ec2]$ ls
deploy.generic  README  spark-ec2  spark_ec2.py

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式出现在文本中:“选择用户操作选项,然后选择管理访问密钥。”

注意

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

提示

提示和技巧会以这样的方式出现。

第一章:Apache Spark

Apache Spark 是一个分布式和高度可扩展的内存数据分析系统,提供了在 Java、Scala、Python 以及 R 等语言中开发应用程序的能力。它在目前的 Apache 顶级项目中具有最高的贡献/参与率。现在,像 Mahout 这样的 Apache 系统使用它作为处理引擎,而不是 MapReduce。此外,正如在第四章中所示,Apache Spark SQL,可以使用 Hive 上下文,使 Spark 应用程序直接处理 Apache Hive 中的数据。

Apache Spark 提供了四个主要的子模块,分别是 SQL、MLlib、GraphX 和 Streaming。它们将在各自的章节中进行解释,但在这里简单的概述会很有用。这些模块是可互操作的,因此数据可以在它们之间传递。例如,流式数据可以传递到 SQL,然后创建一个临时表。

以下图解释了本书将如何处理 Apache Spark 及其模块。前两行显示了 Apache Spark 及其前面描述的四个子模块。然而,尽可能地,我总是试图通过示例来展示如何使用额外的工具来扩展功能:

Apache Spark

例如,第三章中解释的数据流模块,Apache Spark Streaming,将有工作示例,展示如何使用 Apache KafkaFlume执行数据移动。机器学习模块MLlib将通过可用的数据处理功能进行功能检查,但也将使用 H2O 系统和深度学习进行扩展。

前面的图当然是简化的。它代表了本书中呈现的系统关系。例如,Apache Spark 模块与 HDFS 之间的路线比前面的图中显示的要多得多。

Spark SQL 章节还将展示 Spark 如何使用 Hive 上下文。因此,可以开发一个 Spark 应用程序来创建基于 Hive 的对象,并对存储在 HDFS 中的 Hive 表运行 Hive QL。

第五章 Apache Spark GraphX 和 第六章 基于图的存储 将展示 Spark GraphX 模块如何用于处理大数据规模的图,以及如何使用 Titan 图数据库进行存储。将展示 Titan 允许存储和查询大数据规模的图。通过一个例子,将展示 Titan 可以同时使用HBaseCassandra作为存储机制。当使用 HBase 时,将会显示 Titan 隐式地使用 HDFS 作为一种廉价可靠的分布式存储机制。

因此,我认为本节已经解释了 Spark 是一个内存处理系统。在大规模使用时,它不能独立存在——数据必须存放在某个地方。它可能会与 Hadoop 工具集以及相关的生态系统一起使用。幸运的是,Hadoop 堆栈提供商,如 Cloudera,提供了与 Apache Spark、Hadoop 和大多数当前稳定工具集集成的 CDH Hadoop 堆栈和集群管理器。在本书中,我将使用安装在 CentOS 6.5 64 位服务器上的小型 CDH 5.3 集群。您可以使用其他配置,但我发现 CDH 提供了我需要的大多数工具,并自动化了配置,为我留下更多的时间进行开发。

提到了 Spark 模块和本书中将介绍的软件后,下一节将描述大数据集群的可能设计。

概述

在本节中,我希望提供一个关于本书中将介绍的 Apache Spark 功能以及将用于扩展它的系统的概述。我还将尝试审视 Apache Spark 与云存储集成的未来。

当您查看 Apache Spark 网站(spark.apache.org/)上的文档时,您会发现有涵盖 SparkR 和 Bagel 的主题。虽然我会在本书中涵盖四个主要的 Spark 模块,但我不会涵盖这两个主题。我在本书中时间和范围有限,所以我会把这些主题留给读者自行探究或将来研究。

Spark 机器学习

Spark MLlib 模块提供了在多个领域进行机器学习功能。Spark 网站上提供的文档介绍了使用的数据类型(例如,向量和 LabeledPoint 结构)。该模块提供的功能包括:

  • 统计

  • 分类

  • 回归

  • 协同过滤

  • 聚类

  • 维度约简

  • 特征提取

  • 频繁模式挖掘

  • 优化

基于 Scala 的 KMeans、朴素贝叶斯和人工神经网络的实际示例已在本书的第二章 Apache Spark MLlib中介绍和讨论。

Spark Streaming

流处理是 Apache Spark 的另一个重要和受欢迎的主题。它涉及在 Spark 中作为流处理数据,并涵盖输入和输出操作、转换、持久性和检查点等主题。

第三章 Apache Spark Streaming,涵盖了这一领域的处理,并提供了不同类型的流处理的实际示例。它讨论了批处理和窗口流配置,并提供了一个实际的检查点示例。它还涵盖了不同类型的流处理示例,包括 Kafka 和 Flume。

流数据还有许多其他用途。其他 Spark 模块功能(例如 SQL、MLlib 和 GraphX)可以用于处理流。您可以将 Spark 流处理与 Kinesis 或 ZeroMQ 等系统一起使用。您甚至可以为自己定义的数据源创建自定义接收器。

Spark SQL

从 Spark 版本 1.3 开始,数据框架已经引入到 Apache Spark 中,以便以表格形式处理 Spark 数据,并且可以使用表格函数(如 select、filter、groupBy)来处理数据。Spark SQL 模块与 Parquet 和 JSON 格式集成,允许数据以更好地表示数据的格式存储。这也提供了更多与外部系统集成的选项。

将 Apache Spark 集成到 Hadoop Hive 大数据数据库中的想法也可以介绍。基于 Hive 上下文的 Spark 应用程序可用于操作基于 Hive 的表数据。这使得 Spark 的快速内存分布式处理能力可以应用到 Hive 的大数据存储能力上。它有效地让 Hive 使用 Spark 作为处理引擎。

Spark 图处理

Apache Spark GraphX 模块使 Spark 能够提供快速的大数据内存图处理。图由顶点和边的列表(连接顶点的线)表示。GraphX 能够使用属性、结构、连接、聚合、缓存和取消缓存操作来创建和操作图。

它引入了两种新的数据类型来支持 Spark 中的图处理:VertexRDD 和 EdgeRDD 来表示图的顶点和边。它还介绍了图处理的示例函数,例如 PageRank 和三角形处理。这些函数中的许多将在第五章 Apache Spark GraphX中进行研究。

扩展生态系统

在审查大数据处理系统时,我认为重要的是不仅要看系统本身,还要看它如何扩展,以及它如何与外部系统集成,以便提供更高级别的功能。在这样大小的书中,我无法涵盖每个选项,但希望通过介绍一个主题,我可以激发读者的兴趣,以便他们可以进一步调查。

我已经使用了 H2O 机器学习库系统来扩展 Apache Spark 的机器学习模块。通过使用基于 Scala 的 H2O 深度学习示例,我展示了如何将神经处理引入 Apache Spark。然而,我知道我只是触及了 H2O 功能的表面。我只使用了一个小型神经集群和一种分类功能。此外,H2O 还有很多其他功能。

随着图形处理在未来几年变得更加被接受和使用,基于图形的存储也将如此。我已经调查了使用 NoSQL 数据库 Neo4J 的 Spark,使用了 Mazerunner 原型应用程序。我还调查了 Aurelius(Datastax)Titan 数据库用于基于图形的存储。同样,Titan 是一个新生的数据库,需要社区支持和进一步发展。但我想研究 Apache Spark 集成的未来选项。

Spark 的未来

下一节将展示 Apache Spark 发布包含的脚本,允许在 AWS EC2 存储上创建一个 Spark 集群。有一系列选项可供选择,允许集群创建者定义属性,如集群大小和存储类型。但这种类型的集群很难调整大小,这使得管理变化的需求变得困难。如果数据量随时间变化或增长,可能需要更大的集群和更多的内存。

幸运的是,开发 Apache Spark 的人创建了一个名为 Databricks 的新创企业databricks.com/,它提供基于 Web 控制台的 Spark 集群管理,以及许多其他功能。它提供了笔记本组织的工作思路,用户访问控制、安全性和大量其他功能。这些内容在本书的最后进行了描述。

它目前只在亚马逊 AWS 上提供基于云的存储服务,但将来可能会扩展到谷歌和微软 Azure。其他基于云的提供商,即谷歌和微软 Azure,也在扩展他们的服务,以便他们可以在云中提供 Apache Spark 处理。

集群设计

正如我之前提到的,Apache Spark 是一个分布式、内存中、并行处理系统,需要一个关联的存储机制。因此,当你构建一个大数据集群时,你可能会使用分布式存储系统,比如 Hadoop,以及用于数据移动的工具,如 Sqoop、Flume 和 Kafka。

我想介绍大数据集群中边缘节点的概念。集群中的这些节点将面向客户端,上面有像 Hadoop NameNode 或者 Spark 主节点这样的客户端组件。大多数大数据集群可能在防火墙后面。边缘节点将减少防火墙带来的复杂性,因为它们是唯一可访问的节点。下图显示了一个简化的大数据集群:

集群设计

它显示了四个简化的集群机架,带有交换机和边缘节点计算机,面向防火墙的客户端。当然,这是风格化和简化的,但你明白了。一般处理节点隐藏在防火墙后面(虚线),可用于一般处理,比如 Hadoop、Apache Spark、Zookeeper、Flume 和/或 Kafka。下图代表了一些大数据集群边缘节点,并试图展示可能驻留在它们上面的应用程序。

边缘节点应用程序将是类似于 Hadoop NameNode 或 Apache Spark 主服务器的主应用程序。它将是将数据带入和带出集群的组件,比如 Flume、Sqoop 和 Kafka。它可以是任何使用户界面对客户用户可用的组件,类似于 Hive:

集群设计

通常,防火墙在增加集群安全性的同时也增加了复杂性。系统组件之间的端口需要打开,以便它们可以相互通信。例如,Zookeeper 被许多组件用于配置。Apache Kafka,发布订阅消息系统,使用 Zookeeper 来配置其主题、组、消费者和生产者。因此,潜在地需要打开防火墙的客户端端口到 Zookeeper。

最后,需要考虑将系统分配给集群节点。例如,如果 Apache Spark 使用 Flume 或 Kafka,则将使用内存通道。需要考虑这些通道的大小和由数据流引起的内存使用。Apache Spark 不应该与其他 Apache 组件竞争内存使用。根据您的数据流和内存使用情况,可能需要在不同的集群节点上拥有 Spark、Hadoop、Zookeeper、Flume 和其他工具。

通常,作为集群 NameNode 服务器或 Spark 主服务器的边缘节点将需要比防火墙内的集群处理节点更多的资源。例如,CDH 集群节点管理器服务器将需要额外的内存,同样 Spark 主服务器也是如此。您应该监视边缘节点的资源使用情况,并根据需要调整资源和/或应用程序位置。

本节简要介绍了 Apache Spark、Hadoop 和其他工具在大数据集群中的情景。然而,在大数据集群中,Apache Spark 集群本身如何配置呢?例如,可以有多种类型的 Spark 集群管理器。下一节将对此进行探讨,并描述每种类型的 Apache Spark 集群管理器。

集群管理

下图从 spark.apache.org 网站借来,展示了 Apache Spark 集群管理器在主节点、从节点(工作节点)、执行器和 Spark 客户端应用程序方面的作用:

集群管理

正如您将从本书的许多示例中看到的那样,Spark 上下文可以通过 Spark 配置对象和 Spark URL 来定义。Spark 上下文连接到 Spark 集群管理器,然后为应用程序在工作节点之间分配资源。集群管理器在集群工作节点之间分配执行器。它将应用程序 jar 文件复制到工作节点,最后分配任务。

以下小节描述了目前可用的可能的 Apache Spark 集群管理器选项。

本地

通过指定一个 Spark 配置本地 URL,可以让应用程序在本地运行。通过指定 local[n],可以让 Spark 使用<n>个线程在本地运行应用程序。这是一个有用的开发和测试选项。

独立模式

独立模式使用了 Apache Spark 提供的基本集群管理器。Spark 主 URL 将如下所示:

Spark://<hostname>:7077

在这里,<hostname>是运行 Spark 主节点的主机名。我已经指定了端口7077,这是默认值,但它是可配置的。目前,这个简单的集群管理器只支持 FIFO(先进先出)调度。您可以通过为每个应用程序设置资源配置选项来构建允许并发应用程序调度。例如,使用spark.core.max来在应用程序之间共享核心。

Apache YARN

在与 Hadoop YARN 集成的较大规模下,Apache Spark 集群管理器可以是 YARN,并且应用程序可以在两种模式下运行。如果将 Spark 主值设置为 yarn-cluster,那么应用程序可以提交到集群,然后终止。集群将负责分配资源和运行任务。然而,如果应用程序主作为 yarn-client 提交,那么应用程序在处理的生命周期内保持活动,并从 YARN 请求资源。

Apache Mesos

Apache Mesos 是一个用于跨集群共享资源的开源系统。它允许多个框架通过管理和调度资源来共享集群。它是一个集群管理器,使用 Linux 容器提供隔离,允许多个系统(如 Hadoop、Spark、Kafka、Storm 等)安全地共享集群。它可以高度扩展到数千个节点。它是一个基于主从的系统,并且具有容错性,使用 Zookeeper 进行配置管理。

对于单个主节点 Mesos 集群,Spark 主 URL 将采用以下形式:

Mesos://<hostname>:5050

其中<hostname>是 Mesos 主服务器的主机名,端口被定义为5050,这是 Mesos 主端口的默认值(可配置)。如果在大规模高可用性 Mesos 集群中有多个 Mesos 主服务器,则 Spark 主 URL 将如下所示:

Mesos://zk://<hostname>:2181

因此,Mesos 主服务器的选举将由 Zookeeper 控制。<hostname>将是 Zookeeper quorum 中的主机名。此外,端口号2181是 Zookeeper 的默认主端口。

Amazon EC2

Apache Spark 发行版包含用于在亚马逊 AWS EC2 基础服务器上运行 Spark 的脚本。以下示例显示了在 Linux CentOS 服务器上安装的 Spark 1.3.1,位于名为/usr/local/spark/的目录下。Spark 发行版 EC2 子目录中提供了 EC2 资源:

[hadoop@hc2nn ec2]$ pwd

/usr/local/spark/ec2

[hadoop@hc2nn ec2]$ ls
deploy.generic  README  spark-ec2  spark_ec2.py

要在 EC2 上使用 Apache Spark,您需要设置一个 Amazon AWS 帐户。您可以在此处设置一个初始免费帐户来尝试:aws.amazon.com/free/

如果您查看第八章Spark Databricks,您会看到已经设置了这样一个帐户,并且用于访问databricks.com/。接下来,您需要访问 AWS IAM 控制台,并选择用户选项。您可以创建或选择一个用户。选择用户操作选项,然后选择管理访问密钥。然后,选择创建访问密钥,然后下载凭据。确保您下载的密钥文件是安全的,假设您在 Linux 上,使用chmod命令将文件权限设置为600,以便仅用户访问。

现在您已经拥有了访问密钥 ID秘密访问密钥、密钥文件和密钥对名称。您现在可以使用spark-ec2脚本创建一个 Spark EC2 集群,如下所示:

export AWS_ACCESS_KEY_ID="QQpl8Exxx"
export AWS_SECRET_ACCESS_KEY="0HFzqt4xxx"

./spark-ec2  \
 --key-pair=pairname \
 --identity-file=awskey.pem \
 --region=us-west-1 \
 --zone=us-west-1a  \
 launch cluster1

在这里,<pairname>是在创建访问详细信息时给出的密钥对名称;<awskey.pem>是您下载的文件。您要创建的集群的名称称为<cluster1>。此处选择的区域位于美国西部,us-west-1。如果您像我一样住在太平洋地区,可能更明智的选择一个更近的区域,如ap-southeast-2。但是,如果遇到访问问题,则需要尝试另一个区域。还要记住,像这样使用基于云的 Spark 集群将具有更高的延迟和较差的 I/O 性能。您与多个用户共享集群主机,您的集群可能位于远程地区。

您可以使用一系列选项来配置您创建的基于云的 Spark 集群。-s选项可以使用:

-s <slaves>

这允许您定义在您的 Spark EC2 集群中创建多少个工作节点,即-s 5表示六个节点集群,一个主节点和五个从节点。您可以定义您的集群运行的 Spark 版本,而不是默认的最新版本。以下选项启动了一个带有 Spark 版本 1.3.1 的集群:

--spark-version=1.3.1

用于创建集群的实例类型将定义使用多少内存和可用多少核心。例如,以下选项将将实例类型设置为m3.large

--instance-type=m3.large

Amazon AWS 的当前实例类型可以在aws.amazon.com/ec2/instance-types/找到。

下图显示了当前(截至 2015 年 7 月)AWS M3 实例类型、型号细节、核心、内存和存储。目前有许多实例类型可用;例如 T2、M4、M3、C4、C3、R3 等。检查当前可用性并选择适当的:

Amazon EC2

定价也非常重要。当前 AWS 存储类型的价格可以在此找到:aws.amazon.com/ec2/pricing/

价格按地区显示,并有一个下拉菜单和按小时计价。请记住,每种存储类型都由核心、内存和物理存储定义。价格也由操作系统类型定义,即 Linux、RHEL 和 Windows。只需通过顶级菜单选择操作系统。

下图显示了写作时(2015 年 7 月)的定价示例;它只是提供一个想法。价格会随时间而变化,而且会因服务提供商而异。它们会根据你需要的存储大小和你愿意承诺的时间长度而有所不同。

还要注意将数据从任何存储平台移出的成本。尽量考虑长期。检查你是否需要在未来五年将所有或部分基于云的数据移动到下一个系统。检查移动数据的过程,并将该成本纳入你的规划中。

Amazon EC2

如前所述,上图显示了 AWS 存储类型的成本,按操作系统、地区、存储类型和小时计价。成本是按单位小时计算的,因此像databricks.com/这样的系统在完整的小时过去之前不会终止 EC2 实例。这些成本会随时间变化,需要通过(对于 AWS)AWS 计费控制台进行监控。

当你想要调整你的 Spark EC2 集群大小时,你需要确保在开始之前确定主从配置。确定你需要多少工作节点和需要多少内存。如果你觉得你的需求会随着时间改变,那么你可能会考虑使用databricks.com/,如果你确实希望在云中使用 Spark。前往第八章 Spark Databricks,看看你如何设置和使用databricks.com/

在接下来的部分,我将研究 Apache Spark 集群性能以及可能影响它的问题。

性能

在继续涵盖 Apache Spark 的其他章节之前,我想要研究性能领域。需要考虑哪些问题和领域?什么可能会影响从集群级别开始到实际 Scala 代码结束的 Spark 应用程序性能?我不想只是重复 Spark 网站上的内容,所以请查看以下网址:http://spark.apache.org/docs/<version>/tuning.html

在这里,<version>指的是你正在使用的 Spark 版本,即最新版本或特定版本的 1.3.1。因此,在查看了该页面之后,我将简要提及一些主题领域。在本节中,我将列出一些一般要点,而不意味着重要性的顺序。

集群结构

你的大数据集群的大小和结构将影响性能。如果你有一个基于云的集群,你的 IO 和延迟会与未共享硬件的集群相比受到影响。你将与多个客户共享基础硬件,并且集群硬件可能是远程的。

此外,集群组件在服务器上的定位可能会导致资源争用。例如,如果可能的话,仔细考虑在大集群中定位 Hadoop NameNodes、Spark 服务器、Zookeeper、Flume 和 Kafka 服务器。在高工作负载下,你可能需要考虑将服务器分隔到单独的系统中。你可能还需要考虑使用 Apache 系统,如 Mesos,以共享资源。

另外,考虑潜在的并行性。对于大数据集,Spark 集群中的工作节点数量越多,就越有并行处理的机会。

Hadoop 文件系统

根据您的集群需求,您可能考虑使用 HDFS 的替代方案。例如,MapR 具有基于 MapR-FS NFS 的读写文件系统,可提高性能。该文件系统具有完整的读写功能,而 HDFS 设计为一次写入,多次读取的文件系统。它比 HDFS 性能更好。它还与 Hadoop 和 Spark 集群工具集成。MapR 的架构师 Bruce Penn 撰写了一篇有趣的文章,描述了其特性:www.mapr.com/blog/author/bruce-penn

只需查找名为“比较 MapR-FS 和 HDFS NFS 和快照”的博客文章。文章中的链接描述了 MapR 架构和可能的性能提升。

数据本地性

数据本地性或正在处理的数据的位置将影响延迟和 Spark 处理。数据是来自 AWS S3、HDFS、本地文件系统/网络还是远程来源?

如前面的调整链接所述,如果数据是远程的,那么功能和数据必须被整合在一起进行处理。Spark 将尝试使用最佳的数据本地性级别来进行任务处理。

内存

为了避免在 Apache Spark 集群上出现OOM内存不足)消息,您可以考虑以下几个方面:

  • 考虑 Spark 工作节点上可用的物理内存级别。能增加吗?

  • 考虑数据分区。您能增加 Spark 应用程序代码中使用的数据分区数量吗?

  • 您能增加存储分数,即 JVM 用于存储和缓存 RDD 的内存使用吗?

  • 考虑调整用于减少内存的数据结构。

  • 考虑将 RDD 存储序列化以减少内存使用。

编码

尝试调整代码以提高 Spark 应用程序的性能。例如,在 ETL 周期的早期筛选应用程序数据。调整并行度,尝试找到代码中资源密集型的部分,并寻找替代方案。

尽管本书大部分内容将集中在安装在基于物理服务器的集群上的 Apache Spark 的示例上(除了databricks.com/),我想指出有多种基于云的选项。有一些基于云的系统将 Apache Spark 作为集成组件,还有一些基于云的系统提供 Spark 作为服务。尽管本书无法对所有这些进行深入介绍,但我认为提到其中一些可能会有用:

  • 本书的两章涵盖了 Databricks。它提供了一个基于 Spark 的云服务,目前使用 AWS EC2。计划将该服务扩展到其他云供应商(databricks.com/)。

  • 在撰写本书时(2015 年 7 月),微软 Azure 已扩展到提供 Spark 支持。

  • Apache Spark 和 Hadoop 可以安装在 Google Cloud 上。

  • Oryx 系统是基于 Spark 和 Kafka 构建的实时大规模机器学习系统(oryx.io/)。

  • 用于提供机器学习预测的 velox 系统基于 Spark 和 KeystoneML(github.com/amplab/velox-modelserver)。

  • PredictionIO 是建立在 Spark、HBase 和 Spray 上的开源机器学习服务(prediction.io/)。

  • SeldonIO 是一个基于 Spark、Kafka 和 Hadoop 的开源预测分析平台(www.seldon.io/)。

总结

在结束本章时,我想邀请你逐个阅读以下章节中基于 Scala 代码的示例。我对 Apache Spark 的发展速度印象深刻,也对其发布频率印象深刻。因此,即使在撰写本文时,Spark 已经达到 1.4 版本,我相信你将使用更新的版本。如果遇到问题,请以逻辑方式解决。尝试向 Spark 用户组寻求帮助(<user@spark.apache.org>),或者查看 Spark 网站:spark.apache.org/

我一直对与人交流感兴趣,也愿意在 LinkedIn 等网站上与人联系。我渴望了解人们参与的项目和新机遇。我对 Apache Spark、你使用它的方式以及你构建的系统在规模上的应用很感兴趣。你可以通过 LinkedIn 联系我:linkedin.com/profile/view?id=73219349

或者,你可以通过我的网站联系我:semtech-solutions.co.nz/,最后,也可以通过电子邮件联系我:<info@semtech-solutions.co.nz>

第二章:Apache Spark MLlib

MLlib 是 Apache Spark 提供的机器学习库,它是基于内存的开源数据处理系统。在本章中,我将研究 MLlib 库提供的回归、分类和神经处理等领域的功能。我将在提供解决实际问题的工作示例之前,先研究每个算法背后的理论。网络上的示例代码和文档可能稀少且令人困惑。我将采用逐步的方法来描述以下算法的用法和能力。

  • 朴素贝叶斯分类

  • K-Means 聚类

  • ANN 神经处理

在决定学习 Apache Spark 之前,我假设你对 Hadoop 很熟悉。在继续之前,我将简要介绍一下我的环境。我的 Hadoop 集群安装在一组 Centos 6.5 Linux 64 位服务器上。接下来的部分将详细描述架构。

环境配置

在深入研究 Apache Spark 模块之前,我想解释一下我在本书中将使用的 Hadoop 和 Spark 集群的结构和版本。我将在本章中使用 Cloudera CDH 5.1.3 版本的 Hadoop 进行存储,并且我将使用两个版本的 Spark:1.0 和 1.3。

早期版本与 Cloudera 软件兼容,并经过了他们的测试和打包。它是作为一组 Linux 服务从 Cloudera 仓库使用 yum 命令安装的。因为我想要研究尚未发布的神经网络技术,我还将从 GitHub 下载并运行 Spark 1.3 的开发版本。这将在本章后面进行解释。

架构

以下图表解释了我将在本章中使用的小型 Hadoop 集群的结构:

架构

前面的图表显示了一个包含 NameNode(称为 hc2nn)和 DataNodes(hc2r1m1 到 hc2r1m4)的五节点 Hadoop 集群。它还显示了一个包含一个主节点和四个从节点的 Apache Spark 集群。Hadoop 集群提供了物理 Centos 6 Linux 机器,而 Spark 集群运行在同一台主机上。例如,Spark 主服务器运行在 Hadoop NameNode 机器 hc2nn 上,而 Spark 从节点 1 运行在主机 hc2r1m1 上。

Linux 服务器命名标准应该解释得更清楚。例如,Hadoop NameNode 服务器被称为 hc2nn。这个服务器名字中的 h 代表 Hadoop,c 代表集群,nn 代表 NameNode。因此,hc2nn 代表 Hadoop 集群 2 的 NameNode。同样,对于服务器 hc2r1m1,h 代表 Hadoop,c 代表集群,r 代表机架,m 代表机器。因此,这个名字代表 Hadoop 集群 2 的机架 1 的机器 1。在一个大型的 Hadoop 集群中,机器会被组织成机架,因此这种命名标准意味着服务器很容易被定位。

你可以根据自己的需要安排 Spark 和 Hadoop 集群,它们不需要在同一台主机上。为了撰写本书,我只有有限的机器可用,因此将 Hadoop 和 Spark 集群放在同一台主机上是有意义的。你可以为每个集群使用完全独立的机器,只要 Spark 能够访问 Hadoop(如果你想用它来进行分布式存储)。

请记住,尽管 Spark 用于其内存分布式处理的速度,但它并不提供存储。你可以使用主机文件系统来读写数据,但如果你的数据量足够大,可以被描述为大数据,那么使用像 Hadoop 这样的分布式存储系统是有意义的。

还要记住,Apache Spark 可能只是ETL提取转换加载)链中的处理步骤。它并不提供 Hadoop 生态系统所包含的丰富工具集。您可能仍然需要 Nutch/Gora/Solr 进行数据采集;Sqoop 和 Flume 用于数据传输;Oozie 用于调度;HBase 或 Hive 用于存储。我要说明的是,尽管 Apache Spark 是一个非常强大的处理系统,但它应被视为更广泛的 Hadoop 生态系统的一部分。

在描述了本章将使用的环境之后,我将继续描述 Apache Spark MLlib机器学习库)的功能。

开发环境

本书中的编码示例将使用 Scala 语言。这是因为作为一种脚本语言,它产生的代码比 Java 少。它也可以用于 Spark shell,并与 Apache Spark 应用程序一起编译。我将使用 sbt 工具来编译 Scala 代码,安装方法如下:

[hadoop@hc2nn ~]# su -
[root@hc2nn ~]# cd /tmp
[root@hc2nn ~]#wget http://repo.scala-sbt.org/scalasbt/sbt-native-packages/org/scala-sbt/sbt/0.13.1/sbt.rpm
[root@hc2nn ~]# rpm -ivh sbt.rpm

为了方便撰写本书,我在 Hadoop NameNode 服务器hc2nn上使用了名为hadoop的通用 Linux 帐户。由于前面的命令表明我需要以 root 帐户安装sbt,因此我通过su(切换用户)访问了它。然后,我使用wget从名为repo.scala-sbt.org的基于 Web 的服务器下载了sbt.rpm文件到/tmp目录。最后,我使用rpm命令安装了rpm文件,选项为i表示安装,v表示验证,h表示在安装包时打印哈希标记。

提示

下载示例代码

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

我在 Linux 服务器hc2nn上使用 Linux hadoop 帐户为 Apache Spark 开发了所有 Scala 代码。我将每组代码放在/home/hadoop/spark目录下的子目录中。例如,以下 sbt 结构图显示了 MLlib 朴素贝叶斯代码存储在spark目录下名为nbayes的子目录中。图表还显示了 Scala 代码是在名为src/main/scala的子目录结构下开发的。文件bayes1.scalaconvert.scala包含了下一节将使用的朴素贝叶斯代码:

开发环境

bayes.sbt文件是 sbt 工具使用的配置文件,描述了如何编译Scala目录中的 Scala 文件(还要注意,如果您在 Java 中开发,您将使用形式为nbayes/src/main/java的路径)。下面显示了bayes.sbt文件的内容。pwdcat Linux 命令提醒您文件位置,并提醒您转储文件内容。

名称、版本和scalaVersion选项设置了项目的详细信息,以及要使用的 Scala 版本。libraryDependencies选项定义了 Hadoop 和 Spark 库的位置。在这种情况下,使用 Cloudera parcels 安装了 CDH5,并且包库可以在标准位置找到,即 Hadoop 的/usr/lib/hadoop和 Spark 的/usr/lib/spark。解析器选项指定了 Cloudera 存储库的位置以获取其他依赖项:

[hadoop@hc2nn nbayes]$ pwd
/home/hadoop/spark/nbayes
[hadoop@hc2nn nbayes]$ cat bayes.sbt

name := "Naive Bayes"

version := "1.0"

scalaVersion := "2.10.4"

libraryDependencies += "org.apache.hadoop" % "hadoop-client" % "2.3.0"

libraryDependencies += "org.apache.spark" %% "spark-core"  % "1.0.0"

libraryDependencies += "org.apache.spark" %% "spark-mllib" % "1.0.0"

// If using CDH, also add Cloudera repo
resolvers += "Cloudera Repository" at https://repository.cloudera.com/artifactory/cloudera-repos/

Scala nbayes 项目代码可以使用以下命令从nbayes子目录编译:

[hadoop@hc2nn nbayes]$ sbt compile

使用 sbt compile命令将代码编译成类。然后将类放置在nbayes/target/scala-2.10/classes目录中。可以使用以下命令将编译后的类打包成 JAR 文件:

[hadoop@hc2nn nbayes]$ sbt package

Sbt package命令将在目录nbayes/target/scala-2.10下创建一个 JAR 文件。正如sbt 结构图中的示例所示,成功编译和打包后,创建了名为naive-bayes_2.10-1.0.jar的 JAR 文件。然后,可以在spark-submit命令中使用此 JAR 文件及其包含的类。随后将在探索 Apache Spark MLlib 模块中描述此功能。

安装 Spark

最后,当描述用于本书的环境时,我想谈谈安装和运行 Apache Spark 的方法。我不会详细说明 Hadoop CDH5 的安装,只是说我使用 Cloudera parcels 进行了安装。但是,我手动从 Cloudera 存储库安装了 Apache Spark 的 1.0 版本,使用了 Linux 的yum命令。我安装了基于服务的软件包,因为我希望能够灵活安装 Cloudera 的多个版本的 Spark 作为服务,根据需要进行安装。

在准备 CDH Hadoop 版本时,Cloudera 使用 Apache Spark 团队开发的代码和 Apache Bigtop 项目发布的代码。他们进行集成测试,以确保作为代码堆栈工作。他们还将代码和二进制文件重新组织为服务和包。这意味着库、日志和二进制文件可以位于 Linux 下的定义位置,即/var/log/spark/usr/lib/spark。这也意味着,在服务的情况下,可以使用 Linux 的yum命令安装组件,并通过 Linux 的service命令进行管理。

尽管在本章后面描述的神经网络代码的情况下,使用了不同的方法。这是如何安装 Apache Spark 1.0 以与 Hadoop CDH5 一起使用的:

[root@hc2nn ~]# cd /etc/yum.repos.d
[root@hc2nn yum.repos.d]# cat  cloudera-cdh5.repo

[cloudera-cdh5]
# Packages for Cloudera's Distribution for Hadoop, Version 5, on RedHat or CentOS 6 x86_64
name=Cloudera's Distribution for Hadoop, Version 5
baseurl=http://archive.cloudera.com/cdh5/redhat/6/x86_64/cdh/5/
gpgkey = http://archive.cloudera.com/cdh5/redhat/6/x86_64/cdh/RPM-GPG-KEY-cloudera
gpgcheck = 1

第一步是确保在服务器hc2nn和所有其他 Hadoop 集群服务器的/etc/yum.repos.d目录下存在 Cloudera 存储库文件。该文件名为cloudera-cdh5.repo,并指定 yum 命令可以定位 Hadoop CDH5 集群软件的位置。在所有 Hadoop 集群节点上,我使用 Linux 的 yum 命令,以 root 身份,安装 Apache Spark 组件核心、主、工作、历史服务器和 Python:

[root@hc2nn ~]# yum install spark-core spark-master spark-worker spark-history-server spark-python

这使我能够在将来以任何我想要的方式配置 Spark。请注意,我已经在所有节点上安装了主组件,尽管我目前只打算从 Name Node 上使用它。现在,需要在所有节点上配置 Spark 安装。配置文件存储在/etc/spark/conf下。首先要做的事情是设置一个slaves文件,指定 Spark 将在哪些主机上运行其工作组件:

[root@hc2nn ~]# cd /etc/spark/conf

[root@hc2nn conf]# cat slaves
# A Spark Worker will be started on each of the machines listed below.
hc2r1m1
hc2r1m2
hc2r1m3
hc2r1m4

从上面的slaves文件的内容可以看出,Spark 将在 Hadoop CDH5 集群的四个工作节点 Data Nodes 上运行,从hc2r1m1hc2r1m4。接下来,将更改spark-env.sh文件的内容以指定 Spark 环境选项。SPARK_MASTER_IP的值被定义为完整的服务器名称:

export STANDALONE_SPARK_MASTER_HOST=hc2nn.semtech-solutions.co.nz
export SPARK_MASTER_IP=$STANDALONE_SPARK_MASTER_HOST

export SPARK_MASTER_WEBUI_PORT=18080
export SPARK_MASTER_PORT=7077
export SPARK_WORKER_PORT=7078
export SPARK_WORKER_WEBUI_PORT=18081

主和工作进程的 Web 用户界面端口号已经指定,以及操作端口号。然后,Spark 服务可以从 Name Node 服务器以 root 身份启动。我使用以下脚本:

echo "hc2r1m1 - start worker"
ssh   hc2r1m1 'service spark-worker start'

echo "hc2r1m2 - start worker"
ssh   hc2r1m2 'service spark-worker start'

echo "hc2r1m3 - start worker"
ssh   hc2r1m3 'service spark-worker start'

echo "hc2r1m4 - start worker"
ssh   hc2r1m4 'service spark-worker start'

echo "hc2nn - start master server"
service spark-master         start
service spark-history-server start

这将在所有从节点上启动 Spark 工作服务,并在 Name Node hc2nn上启动主和历史服务器。因此,现在可以使用http://hc2nn:18080 URL 访问 Spark 用户界面。

以下图显示了 Spark 1.0 主节点 Web 用户界面的示例。它显示了有关 Spark 安装、工作节点和正在运行或已完成的应用程序的详细信息。给出了主节点和工作节点的状态。在这种情况下,所有节点都是活动的。显示了总内存使用情况和可用情况,以及按工作节点的情况。尽管目前没有应用程序在运行,但可以选择每个工作节点链接以查看在每个工作节点上运行的执行器进程,因为每个应用程序运行的工作量都分布在 Spark 集群中。

还要注意 Spark URL,spark://hc2nn.semtech-solutions.co.nz:7077,在运行 Spark 应用程序(如spark-shellspark-submit)时将被使用。使用此 URL,可以确保对该 Spark 集群运行 shell 或应用程序。

安装 Spark

这快速概述了使用服务的 Apache Spark 安装、其配置、如何启动以及如何监视。现在,是时候着手处理 MLlib 功能领域中的第一个部分,即使用朴素贝叶斯算法进行分类。随着 Scala 脚本的开发和生成的应用程序的监视,Spark 的使用将变得更加清晰。

朴素贝叶斯分类

本节将提供 Apache Spark MLlib 朴素贝叶斯算法的工作示例。它将描述算法背后的理论,并提供一个 Scala 的逐步示例,以展示如何使用该算法。

理论

为了使用朴素贝叶斯算法对数据集进行分类,数据必须是线性可分的,也就是说,数据中的类必须能够通过类边界进行线性划分。以下图形通过三个数据集和两个虚线所示的类边界来直观解释这一点:

理论

朴素贝叶斯假设数据集中的特征(或维度)彼此独立,即它们互不影响。Hernan Amiune 在hernan.amiune.com/提供了朴素贝叶斯的一个例子。以下例子考虑将电子邮件分类为垃圾邮件。如果你有 100 封电子邮件,那么执行以下操作:

60% of emails are spam
 80% of spam emails contain the word buy
 20% of spam emails don't contain the word buy
40% of emails are not spam
 10% of non spam emails contain the word buy
 90% of non spam emails don't contain the word buy

因此,将这个例子转换为概率,以便创建一个朴素贝叶斯方程。

P(Spam) = the probability that an email is spam = 0.6
P(Not Spam) = the probability that an email is not spam = 0.4
P(Buy|Spam) = the probability that an email that is spam has the word buy = 0.8
P(Buy|Not Spam) = the probability that an email that is not spam has the word buy = 0.1

那么,包含单词“buy”的电子邮件是垃圾邮件的概率是多少?这将被写成P(垃圾邮件|Buy)。朴素贝叶斯表示它由以下图中的方程描述:

理论

因此,使用先前的百分比数据,我们得到以下结果:

P(Spam|Buy) = ( 0.8 * 0.6 ) / (( 0.8 * 0.6 )  + ( 0.1 * 0.4 )  )  = ( .48 ) / ( .48 + .04 )
= .48 / .52 = .923

这意味着包含单词“buy”的电子邮件更有可能是垃圾邮件,概率高达 92%。这是对理论的一瞥;现在,是时候尝试使用 Apache Spark MLlib 朴素贝叶斯算法进行一个真实世界的例子了。

实践中的朴素贝叶斯

第一步是选择一些用于分类的数据。我选择了来自英国政府数据网站的一些数据,可在data.gov.uk/dataset/road-accidents-safety-data上找到。

数据集名为“道路安全-2013 年数字酒精测试数据”,下载一个名为DigitalBreathTestData2013.txt的压缩文本文件。该文件包含大约 50 万行数据。数据如下所示:

Reason,Month,Year,WeekType,TimeBand,BreathAlcohol,AgeBand,Gender
Suspicion of Alcohol,Jan,2013,Weekday,12am-4am,75,30-39,Male
Moving Traffic Violation,Jan,2013,Weekday,12am-4am,0,20-24,Male
Road Traffic Collision,Jan,2013,Weekend,12pm-4pm,0,20-24,Female

为了对数据进行分类,我修改了列布局和列数。我只是使用 Excel 来给出数据量。但是,如果我的数据量达到了大数据范围,我可能需要使用 Scala,或者像 Apache Pig 这样的工具。如下命令所示,数据现在存储在 HDFS 上,目录名为/data/spark/nbayes。文件名为DigitalBreathTestData2013- MALE2.csv。此外,来自 Linux wc命令的行数显示有 467,000 行。最后,以下数据样本显示我已经选择了列:Gender, Reason, WeekType, TimeBand, BreathAlcohol 和 AgeBand 进行分类。我将尝试使用其他列作为特征对 Gender 列进行分类:

[hadoop@hc2nn ~]$ hdfs dfs -cat /data/spark/nbayes/DigitalBreathTestData2013-MALE2.csv | wc -l
467054

[hadoop@hc2nn ~]$ hdfs dfs -cat /data/spark/nbayes/DigitalBreathTestData2013-MALE2.csv | head -5
Male,Suspicion of Alcohol,Weekday,12am-4am,75,30-39
Male,Moving Traffic Violation,Weekday,12am-4am,0,20-24
Male,Suspicion of Alcohol,Weekend,4am-8am,12,40-49
Male,Suspicion of Alcohol,Weekday,12am-4am,0,50-59
Female,Road Traffic Collision,Weekend,12pm-4pm,0,20-24

Apache Spark MLlib 分类函数使用一个名为LabeledPoint的数据结构,这是一个通用的数据表示,定义在:spark.apache.org/docs/1.0.0/api/scala/index.html#org.apache.spark.mllib.regression.LabeledPoint

这个结构只接受 Double 值,这意味着前面数据中的文本值需要被分类为数字。幸运的是,数据中的所有列都将转换为数字类别,我已经在本书的软件包中提供了两个程序,在chapter2\naive bayes目录下。第一个叫做convTestData.pl,是一个 Perl 脚本,用于将以前的文本文件转换为 Linux。第二个文件,将在这里进行检查,名为convert.scala。它将DigitalBreathTestData2013- MALE2.csv文件的内容转换为 Double 向量。

关于基于 sbt Scala 的开发环境的目录结构和文件已经在前面进行了描述。我正在使用 Linux 账户 hadoop 在 Linux 服务器hc2nn上开发我的 Scala 代码。接下来,Linux 的pwdls命令显示了我的顶级nbayes开发目录,其中包含bayes.sbt配置文件,其内容已经被检查过:

[hadoop@hc2nn nbayes]$ pwd
/home/hadoop/spark/nbayes
[hadoop@hc2nn nbayes]$ ls
bayes.sbt     target   project   src

接下来显示了运行朴素贝叶斯示例的 Scala 代码,在src/main/scala子目录下的nbayes目录中:

[hadoop@hc2nn scala]$ pwd
/home/hadoop/spark/nbayes/src/main/scala
[hadoop@hc2nn scala]$ ls
bayes1.scala  convert.scala

我们稍后将检查bayes1.scala文件,但首先,HDFS 上的基于文本的数据必须转换为数值 Double 值。这就是convert.scala文件的用途。代码如下:

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf

这些行导入了 Spark 上下文的类,连接到 Apache Spark 集群的类,以及 Spark 配置。正在创建的对象名为convert1。它是一个应用程序,因为它扩展了类App

object convert1 extends App
{

下一行创建了一个名为enumerateCsvRecord的函数。它有一个名为colData的参数,它是一个字符串数组,并返回一个字符串:

def enumerateCsvRecord( colData:Array[String]): String =
{

然后,该函数枚举每一列中的文本值,例如,Male 变成 0。这些数值存储在像 colVal1 这样的值中:

 val colVal1 =
 colData(0) match
 {
 case "Male"                          => 0
 case "Female"                        => 1
 case "Unknown"                       => 2
 case _                               => 99
 }

 val colVal2 =
 colData(1) match
 {
 case "Moving Traffic Violation"      => 0
 case "Other"                         => 1
 case "Road Traffic Collision"        => 2
 case "Suspicion of Alcohol"          => 3
 case _                               => 99
 }

 val colVal3 =
 colData(2) match
 {
 case "Weekday"                       => 0
 case "Weekend"                       => 0
 case _                               => 99
 }

 val colVal4 =
 colData(3) match
 {
 case "12am-4am"                      => 0
 case "4am-8am"                       => 1
 case "8am-12pm"                      => 2
 case "12pm-4pm"                      => 3
 case "4pm-8pm"                       => 4
 case "8pm-12pm"                      => 5
 case _                               => 99
 }

 val colVal5 = colData(4)

 val colVal6 =
 colData(5) match
 {
 case "16-19"                         => 0
 case "20-24"                         => 1
 case "25-29"                         => 2
 case "30-39"                         => 3
 case "40-49"                         => 4
 case "50-59"                         => 5
 case "60-69"                         => 6
 case "70-98"                         => 7
 case "Other"                         => 8
 case _                               => 99
 }

从数值列值创建一个逗号分隔的字符串lineString,然后返回它。函数以最终的大括号字符}结束。请注意,下一个创建的数据行从第一列的标签值开始,然后是一个代表数据的向量。向量是以空格分隔的,而标签与向量之间用逗号分隔。使用这两种分隔符类型可以让我稍后以两个简单的步骤处理标签和向量:

 val lineString = colVal1+","+colVal2+" "+colVal3+" "+colVal4+" "+colVal5+" "+colVal6

 return lineString
}

主脚本定义了 HDFS 服务器名称和路径。它定义了输入文件和输出路径,使用这些值。它使用 Spark URL 和应用程序名称创建一个新的配置。然后使用这些详细信息创建一个新的 Spark 上下文或连接:

val hdfsServer = "hdfs://hc2nn.semtech-solutions.co.nz:8020"
val hdfsPath   = "/data/spark/nbayes/"
val inDataFile  = hdfsServer + hdfsPath + "DigitalBreathTestData2013-MALE2.csv"
val outDataFile = hdfsServer + hdfsPath + "result"

val sparkMaster = "spark://hc2nn.semtech-solutions.co.nz:7077"
val appName = "Convert 1"
val sparkConf = new SparkConf()

sparkConf.setMaster(sparkMaster)
sparkConf.setAppName(appName)

val sparkCxt = new SparkContext(sparkConf)

使用 Spark 上下文的textFile方法从 HDFS 加载基于 CSV 的原始数据文件。然后打印数据行数:

val csvData = sparkCxt.textFile(inDataFile)
println("Records in  : "+ csvData.count() )

CSV 原始数据逐行传递给enumerateCsvRecord函数。返回的基于字符串的数字数据存储在enumRddData变量中:

 val enumRddData = csvData.map
 {
 csvLine =>
 val colData = csvLine.split(',')

 enumerateCsvRecord(colData)

 }

最后,打印enumRddData变量中的记录数,并将枚举数据保存到 HDFS 中:

 println("Records out : "+ enumRddData.count() )

 enumRddData.saveAsTextFile(outDataFile)

} // end object

为了将此脚本作为 Spark 应用程序运行,必须对其进行编译。这是通过package命令来完成的,该命令还会编译代码。以下命令是从nbayes目录运行的:

[hadoop@hc2nn nbayes]$ sbt package
Loading /usr/share/sbt/bin/sbt-launch-lib.bash
....
[info] Done packaging.
[success] Total time: 37 s, completed Feb 19, 2015 1:23:55 PM

这将导致创建的编译类被打包成一个 JAR 库,如下所示:

[hadoop@hc2nn nbayes]$ pwd
/home/hadoop/spark/nbayes
[hadoop@hc2nn nbayes]$ ls -l target/scala-2.10
total 24
drwxrwxr-x 2 hadoop hadoop  4096 Feb 19 13:23 classes
-rw-rw-r-- 1 hadoop hadoop 17609 Feb 19 13:23 naive-bayes_2.10-1.0.jar

现在可以使用应用程序名称、Spark URL 和创建的 JAR 文件的完整路径来运行应用程序convert1。一些额外的参数指定了应该使用的内存和最大核心:

spark-submit \
 --class convert1 \
 --master spark://hc2nn.semtech-solutions.co.nz:7077  \
 --executor-memory 700M \
 --total-executor-cores 100 \
 /home/hadoop/spark/nbayes/target/scala-2.10/naive-bayes_2.10-1.0.jar

这在 HDFS 上创建了一个名为/data/spark/nbayes/的数据目录,随后是包含处理过的数据的部分文件:

[hadoop@hc2nn nbayes]$  hdfs dfs -ls /data/spark/nbayes
Found 2 items
-rw-r--r--   3 hadoop supergroup   24645166 2015-01-29 21:27 /data/spark/nbayes/DigitalBreathTestData2013-MALE2.csv
drwxr-xr-x   - hadoop supergroup          0 2015-02-19 13:36 /data/spark/nbayes/result

[hadoop@hc2nn nbayes]$ hdfs dfs -ls /data/spark/nbayes/result
Found 3 items
-rw-r--r--   3 hadoop supergroup          0 2015-02-19 13:36 /data/spark/nbayes/result/_SUCCESS
-rw-r--r--   3 hadoop supergroup    2828727 2015-02-19 13:36 /data/spark/nbayes/result/part-00000
-rw-r--r--   3 hadoop supergroup    2865499 2015-02-19 13:36 /data/spark/nbayes/result/part-00001

在以下 HDFS cat命令中,我已经将部分文件数据连接成一个名为DigitalBreathTestData2013-MALE2a.csv的文件。然后,我使用head命令检查了文件的前五行,以显示它是数字的。最后,我使用put命令将其加载到 HDFS 中:

[hadoop@hc2nn nbayes]$ hdfs dfs -cat /data/spark/nbayes/result/part* > ./DigitalBreathTestData2013-MALE2a.csv

[hadoop@hc2nn nbayes]$ head -5 DigitalBreathTestData2013-MALE2a.csv
0,3 0 0 75 3
0,0 0 0 0 1
0,3 0 1 12 4
0,3 0 0 0 5
1,2 0 3 0 1

[hadoop@hc2nn nbayes]$ hdfs dfs -put ./DigitalBreathTestData2013-MALE2a.csv /data/spark/nbayes

以下 HDFS ls命令现在显示了存储在 HDFS 上的数字数据文件,位于nbayes目录中:

[hadoop@hc2nn nbayes]$ hdfs dfs -ls /data/spark/nbayes
Found 3 items
-rw-r--r--   3 hadoop supergroup   24645166 2015-01-29 21:27 /data/spark/nbayes/DigitalBreathTestData2013-MALE2.csv
-rw-r--r--   3 hadoop supergroup    5694226 2015-02-19 13:39 /data/spark/nbayes/DigitalBreathTestData2013-MALE2a.csv
drwxr-xr-x   - hadoop supergroup          0 2015-02-19 13:36 /data/spark/nbayes/result

现在数据已转换为数字形式,可以使用 MLlib 朴素贝叶斯算法进行处理;这就是 Scala 文件bayes1.scala的作用。该文件导入了与之前相同的配置和上下文类。它还导入了朴素贝叶斯、向量和 LabeledPoint 结构的 MLlib 类。这次创建的应用程序类名为bayes1

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf
import org.apache.spark.mllib.classification.NaiveBayes
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.regression.LabeledPoint

object bayes1 extends App
{

再次定义 HDFS 数据文件,并像以前一样创建一个 Spark 上下文:

 val hdfsServer = "hdfs://hc2nn.semtech-solutions.co.nz:8020"
 val hdfsPath   = "/data/spark/nbayes/"

 val dataFile = hdfsServer+hdfsPath+"DigitalBreathTestData2013-MALE2a.csv"

 val sparkMaster = "spark://hc2nn.semtech-solutions.co.nz:7077"
 val appName = "Naive Bayes 1"
 val conf = new SparkConf()
 conf.setMaster(sparkMaster)
 conf.setAppName(appName)

 val sparkCxt = new SparkContext(conf)

原始 CSV 数据被加载并按分隔符字符拆分。第一列成为数据将被分类的标签(男/女)。最后由空格分隔的列成为分类特征:

 val csvData = sparkCxt.textFile(dataFile)

 val ArrayData = csvData.map
 {
 csvLine =>
 val colData = csvLine.split(',')
 LabeledPoint(colData(0).toDouble, Vectors.dense(colData(1).split(' ').map(_.toDouble)))
 }

然后,数据被随机分成训练(70%)和测试(30%)数据集:

 val divData = ArrayData.randomSplit(Array(0.7, 0.3), seed = 13L)

 val trainDataSet = divData(0)
 val testDataSet  = divData(1)

现在可以使用先前的训练集来训练朴素贝叶斯 MLlib 函数。训练后的朴素贝叶斯模型存储在变量nbTrained中,然后可以用于预测测试数据的男/女结果标签:

 val nbTrained = NaiveBayes.train(trainDataSet)
 val nbPredict = nbTrained.predict(testDataSet.map(_.features))

鉴于所有数据已经包含标签,可以比较测试数据的原始和预测标签。然后可以计算准确度,以确定预测与原始标签的匹配程度:

 val predictionAndLabel = nbPredict.zip(testDataSet.map(_.label))
 val accuracy = 100.0 * predictionAndLabel.filter(x => x._1 == x._2).count() / testDataSet.count()
 println( "Accuracy : " + accuracy );
}

这解释了 Scala 朴素贝叶斯代码示例。现在是时候使用spark-submit运行编译后的bayes1应用程序,并确定分类准确度。参数是相同的。只是类名已经改变:

spark-submit \
 --class bayes1 \
 --master spark://hc2nn.semtech-solutions.co.nz:7077  \
 --executor-memory 700M \
 --total-executor-cores 100 \
 /home/hadoop/spark/nbayes/target/scala-2.10/naive-bayes_2.10-1.0.jar

Spark 集群给出的准确度只有43%,这似乎意味着这些数据不适合朴素贝叶斯:

Accuracy: 43.30

在下一个示例中,我将使用 K-Means 来尝试确定数据中存在的聚类。请记住,朴素贝叶斯需要数据类沿着类边界线性可分。使用 K-Means,将能够确定数据中的成员资格和聚类的中心位置。

使用 K-Means 进行聚类

这个示例将使用前一个示例中的相同测试数据,但将尝试使用 MLlib K-Means 算法在数据中找到聚类。

理论

K-Means 算法通过迭代尝试确定测试数据中的聚类,方法是最小化聚类中心向量的平均值与新候选聚类成员向量之间的距离。以下方程假设数据集成员的范围从X1Xn;它还假设了从S1SkK个聚类集,其中K <= n

Theory

实际中的 K-Means

再次,K-Means MLlib 功能使用 LabeledPoint 结构来处理其数据,因此需要数值输入数据。由于本节重复使用了上一节的相同数据,我不会重新解释数据转换。在本节中在数据方面唯一的变化是,HDFS 下的处理现在将在/data/spark/kmeans/目录下进行。此外,K-Means 示例的 Scala 脚本转换产生的记录是逗号分隔的。

K-Means 示例的开发和处理已经在/home/hadoop/spark/kmeans目录下进行,以便将工作与其他开发分开。sbt 配置文件现在称为kmeans.sbt,与上一个示例相同,只是项目名称不同:

name := "K-Means"

这一部分的代码可以在软件包的chapter2\K-Means目录下找到。因此,查看存储在kmeans/src/main/scala下的kmeans1.scala的代码,会发生一些类似的操作。导入语句涉及到 Spark 上下文和配置。然而,这次 K-Means 功能也被从 MLlib 中导入。此外,本示例的应用程序类名已更改为kmeans1

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf

import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.clustering.{KMeans,KMeansModel}

object kmeans1 extends App
{

与上一个示例一样,正在采取相同的操作来定义数据文件——定义 Spark 配置并创建 Spark 上下文:

 val hdfsServer = "hdfs://hc2nn.semtech-solutions.co.nz:8020"
 val hdfsPath   = "/data/spark/kmeans/"

 val dataFile   = hdfsServer + hdfsPath + "DigitalBreathTestData2013-MALE2a.csv"

 val sparkMaster = "spark://hc2nn.semtech-solutions.co.nz:7077"
 val appName = "K-Means 1"
 val conf = new SparkConf()

 conf.setMaster(sparkMaster)
 conf.setAppName(appName)

 val sparkCxt = new SparkContext(conf)

接下来,从数据文件加载了 CSV 数据,并按逗号字符分割为变量VectorData

 val csvData = sparkCxt.textFile(dataFile)
 val VectorData = csvData.map
 {
 csvLine =>
 Vectors.dense( csvLine.split(',').map(_.toDouble))
 }

初始化了一个 K-Means 对象,并设置了参数来定义簇的数量和确定它们的最大迭代次数:

 val kMeans = new KMeans
 val numClusters         = 3
 val maxIterations       = 50

为初始化模式、运行次数和 Epsilon 定义了一些默认值,这些值我需要用于 K-Means 调用,但在处理中没有变化。最后,这些参数被设置到 K-Means 对象中:

 val initializationMode  = KMeans.K_MEANS_PARALLEL
 val numRuns             = 1
 val numEpsilon          = 1e-4

 kMeans.setK( numClusters )
 kMeans.setMaxIterations( maxIterations )
 kMeans.setInitializationMode( initializationMode )
 kMeans.setRuns( numRuns )
 kMeans.setEpsilon( numEpsilon )

我缓存了训练向量数据以提高性能,并使用向量数据训练了 K-Means 对象以创建训练过的 K-Means 模型:

 VectorData.cache
 val kMeansModel = kMeans.run( VectorData )

我计算了 K-Means 成本、输入数据行数,并通过打印行语句输出了结果。成本值表示簇有多紧密地打包在一起,以及簇之间有多分离:

 val kMeansCost = kMeansModel.computeCost( VectorData )

 println( "Input data rows : " + VectorData.count() )
 println( "K-Means Cost    : " + kMeansCost )

接下来,我使用了 K-Means 模型来打印计算出的三个簇的簇中心作为向量:

 kMeansModel.clusterCenters.foreach{ println }

最后,我使用了 K-Means 模型的predict函数来创建簇成员预测列表。然后,我通过值来计算这些预测,以给出每个簇中数据点的计数。这显示了哪些簇更大,以及是否真的有三个簇:

 val clusterRddInt = kMeansModel.predict( VectorData )

 val clusterCount = clusterRddInt.countByValue

 clusterCount.toList.foreach{ println }

} // end object kmeans1

因此,为了运行这个应用程序,必须从kmeans子目录编译和打包,如 Linux 的pwd命令所示:

[hadoop@hc2nn kmeans]$ pwd
/home/hadoop/spark/kmeans
[hadoop@hc2nn kmeans]$ sbt package

Loading /usr/share/sbt/bin/sbt-launch-lib.bash
[info] Set current project to K-Means (in build file:/home/hadoop/spark/kmeans/)
[info] Compiling 2 Scala sources to /home/hadoop/spark/kmeans/target/scala-2.10/classes...
[info] Packaging /home/hadoop/spark/kmeans/target/scala-2.10/k-means_2.10-1.0.jar ...
[info] Done packaging.
[success] Total time: 20 s, completed Feb 19, 2015 5:02:07 PM

一旦这个打包成功,我检查 HDFS 以确保测试数据已准备就绪。与上一个示例一样,我使用软件包中提供的convert.scala文件将我的数据转换为数值形式。我将在 HDFS 目录/data/spark/kmeans中处理数据文件DigitalBreathTestData2013-MALE2a.csv

[hadoop@hc2nn nbayes]$ hdfs dfs -ls /data/spark/kmeans
Found 3 items
-rw-r--r--   3 hadoop supergroup   24645166 2015-02-05 21:11 /data/spark/kmeans/DigitalBreathTestData2013-MALE2.csv
-rw-r--r--   3 hadoop supergroup    5694226 2015-02-05 21:48 /data/spark/kmeans/DigitalBreathTestData2013-MALE2a.csv
drwxr-xr-x   - hadoop supergroup          0 2015-02-05 21:46 /data/spark/kmeans/result

spark-submit工具用于运行 K-Means 应用程序。在这个命令中唯一的变化是,类现在是kmeans1

spark-submit \
 --class kmeans1 \
 --master spark://hc2nn.semtech-solutions.co.nz:7077  \
 --executor-memory 700M \
 --total-executor-cores 100 \
 /home/hadoop/spark/kmeans/target/scala-2.10/k-means_2.10-1.0.jar

Spark 集群运行的输出如下所示:

Input data rows : 467054
K-Means Cost    : 5.40312223450789E7

先前的输出显示了输入数据量,看起来是正确的,还显示了 K-Means 成本值。接下来是三个向量,描述了具有正确维数的数据簇中心。请记住,这些簇中心向量将具有与原始向量数据相同的列数:

[0.24698249738061878,1.3015883142472253,0.005830116872250263,2.9173747788555207,1.156645130895448,3.4400290524342454]

[0.3321793984152627,1.784137241326256,0.007615970459266097,2.5831987075928917,119.58366028156011,3.8379106085083468]

[0.25247226760684494,1.702510963969387,0.006384899819416975,2.231404248000688,52.202897927594805,3.551509158139135]

最后,对 1 到 3 号簇的簇成员资格进行了给出,其中 1 号簇(索引 0)的成员数量最多,为407,539个成员向量。

(0,407539)
(1,12999)
(2,46516)

因此,这两个例子展示了如何使用朴素贝叶斯和 K 均值对数据进行分类和聚类。但是,如果我想对图像或更复杂的模式进行分类,并使用黑盒方法进行分类呢?下一节将介绍使用 ANN(人工神经网络)进行基于 Spark 的分类。为了做到这一点,我需要下载最新的 Spark 代码,并为 Spark 1.3 构建服务器,因为它在撰写本文时尚未正式发布。

ANN - 人工神经网络

为了研究 Apache Spark 中的ANN(人工神经网络)功能,我需要从 GitHub 网站获取最新的源代码。ANN功能由 Bert Greevenbosch (www.bertgreevenbosch.nl/) 开发,并计划在 Apache Spark 1.3 中发布。撰写本文时,当前的 Spark 版本是 1.2.1,CDH 5.x 附带的 Spark 版本是 1.0。因此,为了研究这个未发布的ANN功能,需要获取源代码并构建成 Spark 服务器。这是我在解释一些ANN背后的理论之后将要做的事情。

理论

下图显示了左侧的一个简单的生物神经元。神经元有树突接收其他神经元的信号。细胞体控制激活,轴突将电脉冲传递给其他神经元的树突。右侧的人工神经元具有一系列加权输入:将输入分组的求和函数,以及一个触发机制(F(Net)),它决定输入是否达到阈值,如果是,神经元将发射:

理论

神经网络对嘈杂的图像和失真具有容忍性,因此在需要对潜在受损图像进行黑盒分类时非常有用。接下来要考虑的是神经元输入的总和函数。下图显示了神经元 i 的总和函数Net。具有加权值的神经元之间的连接包含网络的存储知识。通常,网络会有一个输入层,一个输出层和若干隐藏层。如果神经元的输入总和超过阈值,神经元将发射。

理论

在前述方程中,图表和关键显示了来自模式P的输入值被传递到网络的输入层神经元。这些值成为输入层神经元的激活值;它们是一个特例。神经元i的输入是神经元连接i-j的加权值的总和,乘以神经元j的激活。神经元j的激活(如果它不是输入层神经元)由F(Net),即压缩函数给出,接下来将对其进行描述。

一个模拟神经元需要一个触发机制,决定神经元的输入是否达到了阈值。然后,它会发射以创建该神经元的激活值。这种发射或压缩功能可以用下图所示的广义 S 形函数来描述:

理论

该函数有两个常数:ABB影响激活曲线的形状,如前图所示。数值越大,函数越类似于开/关步骤。A的值设置了返回激活的最小值。在前图中为零。

因此,这提供了模拟神经元、创建权重矩阵作为神经元连接以及管理神经元激活的机制。但是网络是如何组织的呢?下图显示了一个建议的神经元架构 - 神经网络具有一个输入层的神经元,一个输出层和一个或多个隐藏层。每层中的所有神经元都与相邻层中的每个神经元相连。

理论

在训练期间,激活从输入层通过网络传递到输出层。然后,期望的或实际输出之间的错误或差异导致错误增量通过网络传递回来,改变权重矩阵的值。一旦达到期望的输出层向量,知识就存储在权重矩阵中,网络可以进一步训练或用于分类。

因此,神经网络背后的理论已经以反向传播的方式描述。现在是时候获取 Apache Spark 代码的开发版本,并构建 Spark 服务器,以便运行 ANN Scala 代码。

构建 Spark 服务器

我通常不建议在 Spark 发布之前下载和使用 Apache Spark 代码,或者在 Cloudera(用于 CDH)打包,但是对 ANN 功能进行检查的愿望,以及本书允许的时间范围,意味着我需要这样做。我从这个路径提取了完整的 Spark 代码树:

https://github.com/apache/spark/pull/1290.

我将这段代码存储在 Linux 服务器hc2nn的目录/home/hadoop/spark/spark下。然后我从 Bert Greevenbosch 的 GitHub 开发区域获取了 ANN 代码:

https://github.com/bgreeven/spark/blob/master/mllib/src/main/scala/org/apache/spark/mllib/ann/ArtificialNeuralNetwork.scala
https://github.com/bgreeven/spark/blob/master/mllib/src/main/scala/org/apache/spark/mllib/classification/ANNClassifier.scala

ANNClassifier.scala文件包含将被调用的公共函数。ArtificialNeuralNetwork.scala文件包含ANNClassifier.scala调用的私有 MLlib ANN 函数。我已经在服务器上安装了 Java open JDK,所以下一步是在/home/hadoop/spark/spark/conf路径下设置spark-env.sh环境配置文件。我的文件如下:

export STANDALONE_SPARK_MASTER_HOST=hc2nn.semtech-solutions.co.nz
export SPARK_MASTER_IP=$STANDALONE_SPARK_MASTER_HOST
export SPARK_HOME=/home/hadoop/spark/spark
export SPARK_LAUNCH_WITH_SCALA=0
export SPARK_MASTER_WEBUI_PORT=19080
export SPARK_MASTER_PORT=8077
export SPARK_WORKER_PORT=8078
export SPARK_WORKER_WEBUI_PORT=19081
export SPARK_WORKER_DIR=/var/run/spark/work
export SPARK_LOG_DIR=/var/log/spark
export SPARK_HISTORY_SERVER_LOG_DIR=/var/log/spark
export SPARK_PID_DIR=/var/run/spark/
export HADOOP_CONF_DIR=/etc/hadoop/conf
export SPARK_JAR_PATH=${SPARK_HOME}/assembly/target/scala-2.10/
export SPARK_JAR=${SPARK_JAR_PATH}/spark-assembly-1.3.0-SNAPSHOT-hadoop2.3.0-cdh5.1.2.jar
export JAVA_HOME=/usr/lib/jvm/java-1.7.0
export SPARK_LOCAL_IP=192.168.1.103

SPARK_MASTER_IP变量告诉集群哪个服务器是主服务器。端口变量定义了主服务器、工作服务器 web 和操作端口值。还定义了一些日志和 JAR 文件路径,以及JAVA_HOME和本地服务器 IP 地址。有关使用 Apache Maven 构建 Spark 的详细信息,请参阅:

http://spark.apache.org/docs/latest/building-spark.html

在相同目录中的 slaves 文件将像以前一样设置为四个工作服务器的名称,从hc2r1m1hc2r1m4

为了使用 Apache Maven 构建,我必须在我的 Linux 服务器hc2nn上安装mvn,我将在那里运行 Spark 构建。我以 root 用户的身份进行了这个操作,首先使用wget获取了一个 Maven 存储库文件:

wget http://repos.fedorapeople.org/repos/dchen/apache-maven/epel-apache-maven.repo -O /etc/yum.repos.d/epel-apache-maven.repo

然后使用ls长列表检查新的存储库文件是否就位。

[root@hc2nn ~]# ls -l /etc/yum.repos.d/epel-apache-maven.repo
-rw-r--r-- 1 root root 445 Mar  4  2014 /etc/yum.repos.d/epel-apache-maven.repo

然后可以使用 Linux 的yum命令安装 Maven,下面的示例展示了安装命令以及通过ls检查mvn命令是否存在。

[root@hc2nn ~]# yum install apache-maven
[root@hc2nn ~]# ls -l /usr/share/apache-maven/bin/mvn
-rwxr-xr-x 1 root root 6185 Dec 15 06:30 /usr/share/apache-maven/bin/mvn

我用来构建 Spark 源代码树的命令以及成功的输出如下所示。首先设置环境,然后使用mvn命令启动构建。添加选项以构建 Hadoop 2.3/yarn,并跳过测试。构建使用cleanpackage选项每次删除旧的构建文件,然后创建 JAR 文件。最后,构建输出通过tee命令复制到一个名为build.log的文件中:

cd /home/hadoop/spark/spark/conf ; . ./spark-env.sh ; cd ..

mvn  -Pyarn -Phadoop-2.3  -Dhadoop.version=2.3.0-cdh5.1.2 -DskipTests clean package | tee build.log 2>&1

[INFO] ----------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ----------------------------------------------------------
[INFO] Total time: 44:20 min
[INFO] Finished at: 2015-02-16T12:20:28+13:00
[INFO] Final Memory: 76M/925M
[INFO] ----------------------------------------------------------

您使用的实际构建命令将取决于您是否安装了 Hadoop 以及其版本。有关详细信息,请查看之前的构建 Spark,在我的服务器上构建大约需要 40 分钟。

考虑到这个构建将被打包并复制到 Spark 集群中的其他服务器,很重要的一点是所有服务器使用相同版本的 Java,否则会出现诸如以下错误:

15/02/15 12:41:41 ERROR executor.Executor: Exception in task 0.1 in stage 0.0 (TID 2)
java.lang.VerifyError: class org.apache.hadoop.hdfs.protocol.proto.ClientNamenodeProtocolProtos$GetBlockLocationsRequestProto overrides final method getUnknownFields.()Lcom/google/protobuf/UnknownFieldSet;
 at java.lang.ClassLoader.defineClass1(Native Method)

鉴于源代码树已经构建完成,现在需要将其捆绑并发布到 Spark 集群中的每台服务器上。考虑到这些服务器也是 CDH 集群的成员,并且已经设置了无密码 SSH 访问,我可以使用scp命令来发布构建好的软件。以下命令展示了将/home/hadoop/spark路径下的 spark 目录打包成名为spark_bld.tar的 tar 文件。然后使用 Linux 的scp命令将 tar 文件复制到每个从服务器;以下示例展示了hc2r1m1

[hadoop@hc2nn spark]$ cd /home/hadoop/spark
[hadoop@hc2nn spark]$ tar cvf spark_bld.tar spark
[hadoop@hc2nn spark]$ scp ./spark_bld.tar hadoop@hc2r1m1:/home/hadoop/spark/spark_bld.tar

现在,打包的 Spark 构建已经在从节点上,需要进行解压。以下命令显示了服务器hc2r1m1的过程。tar 文件解压到与构建服务器hc2nn相同的目录,即/home/hadoop/spark

[hadoop@hc2r1m1 ~]$ mkdir spark ; mv spark_bld.tar spark
[hadoop@hc2r1m1 ~]$ cd spark ; ls
spark_bld.tar
[hadoop@hc2r1m1 spark]$ tar xvf spark_bld.tar

一旦构建成功运行,并且构建的代码已经发布到从服务器,Spark 的构建版本可以从主服务器hc2nn启动。请注意,我已经选择了与这些服务器上安装的 Spark 版本 1.0 不同的端口号。还要注意,我将以 root 身份启动 Spark,因为 Spark 1.0 安装是在 root 帐户下管理的 Linux 服务。由于两个安装将共享日志记录和.pid文件位置等设施,root 用户将确保访问。这是我用来启动 Apache Spark 1.3 的脚本:

cd /home/hadoop/spark/spark/conf ;  . ./spark-env.sh ; cd ../sbin
echo "hc2nn - start master server"
./start-master.sh
echo "sleep 5000 ms"
sleep 5
echo "hc2nn - start history server"
./start-history-server.sh
echo "Start Spark slaves workers"
./start-slaves.sh

它执行spark-env.sh文件来设置环境,然后使用 Spark sbin目录中的脚本来启动服务。首先在hc2nn上启动主服务器和历史服务器,然后启动从服务器。在启动从服务器之前,我添加了延迟,因为我发现它们在主服务器准备好之前就尝试连接到主服务器。现在可以通过此 URL 访问 Spark 1.3 Web 用户界面:

http://hc2nn.semtech-solutions.co.nz:19080/

Spark URL 允许应用程序连接到 Spark 是这样的:

Spark Master at spark://hc2nn.semtech-solutions.co.nz:8077

根据 spark 环境配置文件中的端口号,Spark 现在可以与 ANN 功能一起使用。下一节将展示 ANN Scala 脚本和数据,以展示如何使用基于 Spark 的功能。

ANN 实践

为了开始 ANN 训练,需要测试数据。鉴于这种分类方法应该擅长分类扭曲或嘈杂的图像,我决定尝试在这里对图像进行分类:

ANN in practice

它们是手工制作的文本文件,包含由字符 1 和 0 创建的形状块。当它们存储在 HDFS 上时,回车字符被移除,因此图像呈现为单行向量。因此,ANN 将对一系列形状图像进行分类,然后将针对添加噪声的相同图像进行测试,以确定分类是否仍然有效。有六个训练图像,它们将分别被赋予从 0.1 到 0.6 的任意训练标签。因此,如果 ANN 被呈现为封闭的正方形,它应该返回一个标签 0.1。以下图像显示了添加噪声的测试图像的示例。通过在图像中添加额外的零(0)字符创建的噪声已经被突出显示:

ANN in practice

由于 Apache Spark 服务器已经从之前的示例中更改,并且 Spark 库的位置也已更改,用于编译示例 ANN Scala 代码的sbt配置文件也必须更改。与以前一样,ANN 代码是在 Linux hadoop 帐户中的一个名为spark/ann的子目录中开发的。ann.sbt文件存在于ann目录中:

 [hadoop@hc2nn ann]$ pwd
/home/hadoop/spark/ann

 [hadoop@hc2nn ann]$ ls
ann.sbt    project  src  target

ann.sbt文件的内容已更改为使用 Spark 依赖项的 JAR 库文件的完整路径。这是因为新的 Apache Spark 构建 1.3 现在位于/home/hadoop/spark/spark下。此外,项目名称已更改为A N N

name := "A N N"
version := "1.0"
scalaVersion := "2.10.4"
libraryDependencies += "org.apache.hadoop" % "hadoop-client" % "2.3.0"
libraryDependencies += "org.apache.spark" % "spark-core"  % "1.3.0" from "file:///home/hadoop/spark/spark/core/target/spark-core_2.10-1.3.0-SNAPSHOT.jar"
libraryDependencies += "org.apache.spark" % "spark-mllib" % "1.3.0" from "file:///home/hadoop/spark/spark/mllib/target/spark-mllib_2.10-1.3.0-SNAPSHOT.jar"
libraryDependencies += "org.apache.spark" % "akka" % "1.3.0" from "file:///home/hadoop/spark/spark/assembly/target/scala-2.10/spark-assembly-1.3.0-SNAPSHOT-hadoop2.3.0-cdh5.1.2.jar"

与以前的示例一样,要编译的实际 Scala 代码存在于名为src/main/scala的子目录中,如下所示。我创建了两个 Scala 程序。第一个使用输入数据进行训练,然后用相同的输入数据测试 ANN 模型。第二个使用嘈杂的数据测试训练模型,以测试扭曲数据的分类:

[hadoop@hc2nn scala]$ pwd
/home/hadoop/spark/ann/src/main/scala

[hadoop@hc2nn scala]$ ls
test_ann1.scala  test_ann2.scala

我将完全检查第一个 Scala 文件,然后只展示第二个文件的额外特性,因为这两个示例在训练 ANN 的时候非常相似。这里展示的代码示例可以在本书提供的软件包中找到,路径为chapter2\ANN。因此,要检查第一个 Scala 示例,导入语句与之前的示例类似。导入了 Spark 上下文、配置、向量和LabeledPoint。这次还导入了 RDD 类用于 RDD 处理,以及新的 ANN 类ANNClassifier。请注意,MLlib/classification例程广泛使用LabeledPoint结构作为输入数据,其中包含了应该被训练的特征和标签:

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf

import org.apache.spark.mllib.classification.ANNClassifier
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.linalg._
import org.apache.spark.rdd.RDD

object testann1 extends App
{

在这个例子中,应用程序类被称为testann1。要处理的 HDFS 文件已经根据 HDFS 服务器、路径和文件名进行了定义:

 val server = "hdfs://hc2nn.semtech-solutions.co.nz:8020"
 val path   = "/data/spark/ann/"

 val data1 = server + path + "close_square.img"
 val data2 = server + path + "close_triangle.img"
 val data3 = server + path + "lines.img"
 val data4 = server + path + "open_square.img"
 val data5 = server + path + "open_triangle.img"
 val data6 = server + path + "plus.img"

Spark 上下文已经创建,使用了 Spark 实例的 URL,现在端口号不同了——8077。应用程序名称是ANN 1。当应用程序运行时,这将出现在 Spark Web UI 上:

 val sparkMaster = "spark://hc2nn.semtech-solutions.co.nz:8077"
 val appName = "ANN 1"
 val conf = new SparkConf()

 conf.setMaster(sparkMaster)
 conf.setAppName(appName)

 val sparkCxt = new SparkContext(conf)

加载基于 HDFS 的输入训练和测试数据文件。每行的值都被空格字符分割,并且数值已经转换为双精度。包含这些数据的变量然后存储在一个名为 inputs 的数组中。同时,创建了一个名为 outputs 的数组,其中包含了从 0.1 到 0.6 的标签。这些值将用于对输入模式进行分类:

 val rData1 = sparkCxt.textFile(data1).map(_.split(" ").map(_.toDouble)).collect
 val rData2 = sparkCxt.textFile(data2).map(_.split(" ").map(_.toDouble)).collect
 val rData3 = sparkCxt.textFile(data3).map(_.split(" ").map(_.toDouble)).collect
 val rData4 = sparkCxt.textFile(data4).map(_.split(" ").map(_.toDouble)).collect
 val rData5 = sparkCxt.textFile(data5).map(_.split(" ").map(_.toDouble)).collect
 val rData6 = sparkCxt.textFile(data6).map(_.split(" ").map(_.toDouble)).collect

 val inputs = Array[Array[Double]] (
 rData1(0), rData2(0), rData3(0), rData4(0), rData5(0), rData6(0) )

 val outputs = ArrayDouble

输入和输出数据,表示输入数据特征和标签,然后被合并并转换成LabeledPoint结构。最后,数据被并行化以便对其进行最佳并行处理:

 val ioData = inputs.zip( outputs )
 val lpData = ioData.map{ case(features,label) =>

 LabeledPoint( label, Vectors.dense(features) )
 }
 val rddData = sparkCxt.parallelize( lpData )

创建变量来定义 ANN 的隐藏层拓扑。在这种情况下,我选择了有两个隐藏层,每个隐藏层有 100 个神经元。定义了最大迭代次数,以及批处理大小(六个模式)和收敛容限。容限是指在我们可以考虑训练已经完成之前,训练误差可以达到多大。然后,使用这些配置参数和输入数据创建了一个 ANN 模型:

 val hiddenTopology : Array[Int] = Array( 100, 100 )
 val maxNumIterations = 1000
 val convTolerance    = 1e-4
 val batchSize        = 6

 val annModel = ANNClassifier.train(rddData,
 batchSize,
 hiddenTopology,
 maxNumIterations,
 convTolerance)

为了测试训练好的 ANN 模型,相同的输入训练数据被用作测试数据来获取预测标签。首先创建一个名为rPredictData的输入数据变量。然后,对数据进行分区,最后使用训练好的 ANN 模型获取预测。对于这个模型工作,必须输出标签 0.1 到 0.6:

 val rPredictData = inputs.map{ case(features) =>

 ( Vectors.dense(features) )
 }
 val rddPredictData = sparkCxt.parallelize( rPredictData )
 val predictions = annModel.predict( rddPredictData )

打印标签预测,并以一个闭合括号结束脚本:

 predictions.toArray().foreach( value => println( "prediction > " + value ) )
} // end ann1

因此,为了运行这个代码示例,必须首先编译和打包。到目前为止,您一定熟悉ann子目录中执行的sbt命令:

[hadoop@hc2nn ann]$ pwd
/home/hadoop/spark/ann
[hadoop@hc2nn ann]$ sbt package

然后,使用spark-submit命令从新的spark/spark路径使用新的基于 Spark 的 URL 在端口 8077 上运行应用程序testann1

/home/hadoop/spark/spark/bin/spark-submit \
 --class testann1 \
 --master spark://hc2nn.semtech-solutions.co.nz:8077  \
 --executor-memory 700M \
 --total-executor-cores 100 \
 /home/hadoop/spark/ann/target/scala-2.10/a-n-n_2.10-1.0.jar

通过检查http://hc2nn.semtech-solutions.co.nz:19080/上的 Apache Spark Web URL,现在可以看到应用程序正在运行。下图显示了应用程序ANN 1正在运行,以及之前完成的执行:

实践中的 ANN

通过选择集群主机工作实例中的一个,可以看到实际执行该工作的执行程序列表:

实践中的 ANN

最后,通过选择一个执行程序,可以查看其历史和配置,以及日志文件和错误信息的链接。在这个级别上,通过提供的日志信息,可以进行调试。可以检查这些日志文件以获取处理错误消息。

实践中的 ANN

ANN 1应用程序提供以下输出,以显示它已经正确地重新分类了相同的输入数据。重新分类是成功的,因为每个输入模式都被赋予了与训练时相同的标签。

prediction > 0.1
prediction > 0.2
prediction > 0.3
prediction > 0.4
prediction > 0.5
prediction > 0.6

因此,这表明 ANN 训练和测试预测将使用相同的数据。现在,我将使用相同的数据进行训练,但使用扭曲或嘈杂的数据进行测试,这是我已经演示过的一个例子。您可以在软件包中的名为test_ann2.scala的文件中找到这个例子。它与第一个例子非常相似,所以我只会演示修改后的代码。该应用程序现在称为testann2

object testann2 extends App

在使用训练数据创建 ANN 模型后,会创建额外的一组测试数据。这些测试数据包含噪音。

 val tData1 = server + path + "close_square_test.img"
 val tData2 = server + path + "close_triangle_test.img"
 val tData3 = server + path + "lines_test.img"
 val tData4 = server + path + "open_square_test.img"
 val tData5 = server + path + "open_triangle_test.img"
 val tData6 = server + path + "plus_test.img"

这些数据被处理成输入数组,并被分区进行集群处理。

 val rtData1 = sparkCxt.textFile(tData1).map(_.split(" ").map(_.toDouble)).collect
 val rtData2 = sparkCxt.textFile(tData2).map(_.split(" ").map(_.toDouble)).collect
 val rtData3 = sparkCxt.textFile(tData3).map(_.split(" ").map(_.toDouble)).collect
 val rtData4 = sparkCxt.textFile(tData4).map(_.split(" ").map(_.toDouble)).collect
 val rtData5 = sparkCxt.textFile(tData5).map(_.split(" ").map(_.toDouble)).collect
 val rtData6 = sparkCxt.textFile(tData6).map(_.split(" ").map(_.toDouble)).collect

 val tInputs = Array[Array[Double]] (
 rtData1(0), rtData2(0), rtData3(0), rtData4(0), rtData5(0), rtData6(0) )

 val rTestPredictData = tInputs.map{ case(features) => ( Vectors.dense(features) ) }
 val rddTestPredictData = sparkCxt.parallelize( rTestPredictData )

然后,它被用来以与第一个示例相同的方式生成标签预测。如果模型正确分类数据,则应该从 0.1 到 0.6 打印相同的标签值。

 val testPredictions = annModel.predict( rddTestPredictData )
 testPredictions.toArray().foreach( value => println( "test prediction > " + value ) )

代码已经被编译,因此可以使用spark-submit命令运行。

/home/hadoop/spark/spark/bin/spark-submit \
 --class testann2 \
 --master spark://hc2nn.semtech-solutions.co.nz:8077  \
 --executor-memory 700M \
 --total-executor-cores 100 \
 /home/hadoop/spark/ann/target/scala-2.10/a-n-n_2.10-1.0.jar

这是脚本的集群输出,显示了使用训练过的 ANN 模型进行成功分类以及一些嘈杂的测试数据。嘈杂的数据已经被正确分类。例如,如果训练模型混淆了,它可能会在位置一的嘈杂的close_square_test.img测试图像中给出0.15的值,而不是返回0.1

test prediction > 0.1
test prediction > 0.2
test prediction > 0.3
test prediction > 0.4
test prediction > 0.5
test prediction > 0.6

摘要

本章试图为您提供 Apache Spark MLlib 模块中一些功能的概述。它还展示了即将在 Spark 1.3 版本中推出的 ANN(人工神经网络)的功能。由于本章的时间和空间限制,无法涵盖 MLlib 的所有领域。

您已经学会了如何为朴素贝叶斯分类、K 均值聚类和 ANN 或人工神经网络开发基于 Scala 的示例。您已经学会了如何为这些 Spark MLlib 例程准备测试数据。您还了解到它们都接受包含特征和标签的 LabeledPoint 结构。此外,每种方法都采用了训练和预测的方法,使用不同的数据集来训练和测试模型。使用本章展示的方法,您现在可以研究 MLlib 库中剩余的功能。您应该参考spark.apache.org/网站,并确保在查看文档时参考正确的版本,即spark.apache.org/docs/1.0.0/,用于 1.0.0 版本。

在本章中,我们已经研究了 Apache Spark MLlib 机器学习库,现在是时候考虑 Apache Spark 的流处理能力了。下一章将使用基于 Spark 和 Scala 的示例代码来研究流处理。

第三章:Apache Spark Streaming

Apache Streaming 模块是 Apache Spark 中基于流处理的模块。它利用 Spark 集群提供高度扩展的能力。基于 Spark,它也具有高度的容错性,能够通过检查点数据流重新运行失败的任务。在本章的初始部分之后,将涵盖以下领域,这部分将提供 Apache Spark 处理基于流的数据的实际概述:

  • 错误恢复和检查点

  • 基于 TCP 的流处理

  • 文件流

  • Flume 流源

  • Kafka 流源

对于每个主题,我将在 Scala 中提供一个示例,并展示如何设置和测试基于流的架构。

概览

在介绍 Apache Spark 流模块时,我建议您查看spark.apache.org/网站以获取最新信息,以及 Spark 用户组,如<user@spark.apache.org>。我之所以这样说是因为这些是 Spark 信息可获得的主要地方。而且,极快(并且不断增加)的变化速度意味着到您阅读此内容时,新的 Spark 功能和版本将会可用。因此,在这种情况下,当进行概述时,我会尽量概括。

概览

前面的图显示了 Apache Streaming 的潜在数据来源,例如KafkaFlumeHDFS。这些数据源输入到 Spark Streaming 模块中,并作为离散流进行处理。该图还显示了其他 Spark 模块功能,例如机器学习,可以用来处理基于流的数据。经过完全处理的数据可以作为HDFS数据库仪表板的输出。这个图是基于 Spark streaming 网站上的图,但我想扩展它,既表达了 Spark 模块的功能,也表达了仪表板的选项。前面的图显示了从 Spark 到 Graphite 的 MetricSystems 数据源。此外,还可以将基于 Solr 的数据源提供给 Lucidworks banana(kabana 的一个端口)。值得在这里提到的是 Databricks(见第八章,Spark Databricks和第九章,Databricks Visualization)也可以将 Spark 流数据呈现为仪表板。

概览

在讨论 Spark 离散流时,前面的图,再次取自 Spark 网站spark.apache.org/,是我喜欢使用的图。前面的图中的绿色框显示了连续的数据流发送到 Spark,被分解为离散流DStream)。然后,流中每个元素的大小基于批处理时间,可能是两秒。还可以创建一个窗口,表示为前面的红色框,覆盖 DStream。例如,在实时进行趋势分析时,可能需要确定在十分钟窗口内的前十个基于 Twitter 的 Hashtags。

因此,鉴于 Spark 可以用于流处理,如何创建流呢?以下基于 Scala 的代码显示了如何创建 Twitter 流。这个例子是简化的,因为没有包括 Twitter 授权,但您可以理解(完整的示例代码在检查点部分)。使用 Spark 上下文sc创建了名为ssc的 Spark 流上下文。在创建时指定了批处理时间;在这种情况下是五秒。然后从Streamingcontext创建了基于 Twitter 的 DStream,称为stream,并使用了 60 秒的窗口:

 val ssc    = new StreamingContext(sc, Seconds(5) )
 val stream = TwitterUtils.createStream(ssc,None).window( Seconds(60) )

流处理可以使用流上下文开始方法(下面显示),awaitTermination方法表示应该一直处理直到停止。因此,如果此代码嵌入在基于库的应用程序中,它将一直运行直到会话终止,也许使用Crtl + C

 ssc.start()
 ssc.awaitTermination()

这解释了 Spark 流是什么以及它的作用,但没有解释错误处理,或者如果基于流的应用程序失败该怎么办。下一节将讨论 Spark 流错误管理和恢复。

错误和恢复

通常,对于您的应用程序需要问的问题是:是否关键接收和处理所有数据?如果不是,那么在失败时,您可能只需重新启动应用程序并丢弃丢失的数据。如果不是这种情况,那么您将需要使用检查点,这将在下一节中描述。

值得注意的是,您的应用程序的错误管理应该是健壮和自给自足的。我的意思是,如果异常是非关键的,那么管理异常,也许记录它,并继续处理。例如,当任务达到最大失败次数(由spark.task.maxFailures指定)时,它将终止处理。

检查点

可以设置一个基于 HDFS 的检查点目录来存储 Apache Spark 基于流的信息。在这个 Scala 示例中,数据将存储在 HDFS 的/data/spark/checkpoint目录下。下面的 HDFS 文件系统ls命令显示,在开始之前,该目录不存在:

[hadoop@hc2nn stream]$ hdfs dfs -ls /data/spark/checkpoint
ls: `/data/spark/checkpoint': No such file or directory

接下来给出的基于 Twitter 的 Scala 代码示例,首先定义了应用程序的包名称,并导入了 Spark、流、上下文和基于 Twitter 的功能。然后定义了一个名为stream1的应用程序对象:

package nz.co.semtechsolutions

import org.apache.spark._
import org.apache.spark.SparkContext._
import org.apache.spark.streaming._
import org.apache.spark.streaming.twitter._
import org.apache.spark.streaming.StreamingContext._

object stream1 {

接下来定义了一个名为createContext的方法,该方法将用于创建 spark 和流上下文,并将流检查点到基于 HDFS 的目录,使用流上下文检查点方法,该方法以目录路径作为参数。目录路径是传递给createContext方法的值(cpDir):

 def createContext( cpDir : String ) : StreamingContext = {

 val appName = "Stream example 1"
 val conf    = new SparkConf()

 conf.setAppName(appName)

 val sc = new SparkContext(conf)

 val ssc    = new StreamingContext(sc, Seconds(5) )

 ssc.checkpoint( cpDir )

 ssc
 }

现在,主要方法已经定义,HDFS目录也已经定义,还有 Twitter 访问权限和参数。Spark 流上下文ssc要么通过StreamingContext方法-getOrCreate从 HDFS checkpoint目录中检索或创建。如果目录不存在,则调用之前的createContext方法,该方法将创建上下文和检查点。显然,出于安全原因,我在此示例中截断了自己的 Twitter 授权密钥。

 def main(args: Array[String]) {

 val hdfsDir = "/data/spark/checkpoint"

 val consumerKey       = "QQpxx"
 val consumerSecret    = "0HFzxx"
 val accessToken       = "323xx"
 val accessTokenSecret = "IlQxx"

 System.setProperty("twitter4j.oauth.consumerKey", consumerKey)
 System.setProperty("twitter4j.oauth.consumerSecret", consumerSecret)
 System.setProperty("twitter4j.oauth.accessToken", accessToken)
 System.setProperty("twitter4j.oauth.accessTokenSecret", accessTokenSecret)

 val ssc = StreamingContext.getOrCreate(hdfsDir,
 () => { createContext( hdfsDir ) })

 val stream = TwitterUtils.createStream(ssc,None).window( Seconds(60) )

 // do some processing

 ssc.start()
 ssc.awaitTermination()

 } // end main

运行了这段代码,没有实际处理,可以再次检查 HDFS checkpoint目录。这次明显可以看到checkpoint目录已经创建,并且数据已经存储:

[hadoop@hc2nn stream]$ hdfs dfs -ls /data/spark/checkpoint
Found 1 items
drwxr-xr-x   - hadoop supergroup          0 2015-07-02 13:41 /data/spark/checkpoint/0fc3d94e-6f53-40fb-910d-1eef044b12e9

这个例子来自 Apache Spark 网站,展示了如何设置和使用检查点存储。但是检查点操作有多频繁?元数据在每个流批次期间存储。实际数据存储的周期是批处理间隔或十秒的最大值。这可能不是您理想的设置,因此您可以使用该方法重置该值:

DStream.checkpoint( newRequiredInterval )

其中newRequiredInterval是您需要的新检查点间隔值,通常应该瞄准是批处理间隔的五到十倍。

检查点保存了流批次和元数据(关于数据的数据)。如果应用程序失败,那么在重新启动时,将使用检查点数据进行处理。在失败时正在处理的批处理数据将被重新处理,以及失败后的批处理数据。

记得监控用于检查点的 HDFS 磁盘空间。在下一节中,我将开始检查流源,并提供每种类型的一些示例。

流源

在本节中,我将无法涵盖所有流类型的实际示例,但在本章太小以至于无法包含代码的情况下,我至少会提供一个描述。在本章中,我将涵盖 TCP 和文件流,以及 Flume、Kafka 和 Twitter 流。我将从一个实际的基于 TCP 的示例开始。

本章探讨了流处理架构。例如,在流数据传递速率超过潜在数据处理速率的情况下会发生什么?像 Kafka 这样的系统提供了通过使用多个数据主题和消费者来解决这个问题的可能性。

TCP 流

有可能使用 Spark 流上下文方法socketTextStream通过指定主机名和端口号来通过 TCP/IP 流式传输数据。本节中的基于 Scala 的代码示例将在端口10777上接收使用netcat Linux 命令提供的数据。代码示例从定义包名开始,并导入 Spark、上下文和流类。定义了一个名为stream2的对象类,因为它是带有参数的主方法:

package nz.co.semtechsolutions

import org.apache.spark._
import org.apache.spark.SparkContext._
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._

object stream2 {

 def main(args: Array[String]) {

检查传递给类的参数数量,以确保它是主机名和端口号。创建了一个具有应用程序名称的 Spark 配置对象。然后创建了 Spark 和流上下文。然后,设置了 10 秒的流批处理时间:

 if ( args.length < 2 )
 {
 System.err.println("Usage: stream2 <host> <port>")
 System.exit(1)
 }

 val hostname = args(0).trim
 val portnum  = args(1).toInt

 val appName = "Stream example 2"
 val conf    = new SparkConf()

 conf.setAppName(appName)

 val sc  = new SparkContext(conf)
 val ssc = new StreamingContext(sc, Seconds(10) )

通过使用主机和端口名称参数调用流上下文的socketTextStream方法创建了一个名为rawDstream的 DStream。

 val rawDstream = ssc.socketTextStream( hostname, portnum )

通过按空格拆分单词,从原始流数据创建了一个前十个单词计数。然后创建了一个(键,值)对,即(word,1),通过键值进行了减少,这就是单词。现在,有一个单词及其相关计数的列表。现在,键和值被交换,所以列表变成了(countword)。然后,对现在是计数的键进行排序。最后,从 DStream 中的rdd中取出前 10 个项目并打印出来:

 val wordCount = rawDstream
 .flatMap(line => line.split(" "))
 .map(word => (word,1))
 .reduceByKey(_+_)
 .map(item => item.swap)
 .transform(rdd => rdd.sortByKey(false))
 .foreachRDD( rdd =>
 { rdd.take(10).foreach(x=>println("List : " + x)) })

代码以 Spark Streaming 的启动和调用awaitTermination方法结束,以启动流处理并等待处理终止:

 ssc.start()
 ssc.awaitTermination()

 } // end main

} // end stream2

该应用程序的数据是由 Linux 的netcat (nc)命令提供的,正如我之前所说的。Linux 的cat命令会将日志文件的内容转储到nclk选项强制netcat监听连接,并在连接丢失时继续监听。该示例显示使用的端口是10777

[root@hc2nn log]# pwd
/var/log
[root@hc2nn log]# cat ./anaconda.storage.log | nc -lk 10777

基于 TCP 的流处理的输出如下所示。实际输出并不像所示方法那样重要。然而,数据显示了预期的结果,即按降序列出了 10 个日志文件单词。请注意,顶部的单词为空,因为流没有过滤空单词:

List : (17104,)
List : (2333,=)
List : (1656,:)
List : (1603,;)
List : (1557,DEBUG)
List : (564,True)
List : (495,False)
List : (411,None)
List : (356,at)
List : (335,object)

如果您想要使用 Apache Spark 流处理基于 TCP/IP 的主机和端口的流数据,这是很有趣的。但是更奇特的方法呢?如果您希望从消息系统或通过基于内存的通道流式传输数据呢?如果您想要使用今天可用的一些大数据工具,比如 Flume 和 Kafka 呢?接下来的章节将探讨这些选项,但首先我将演示如何基于文件创建流。

文件流

我已经修改了上一节中基于 Scala 的代码示例,通过调用 Spark 流上下文方法textFileStream来监视基于 HDFS 的目录。鉴于这个小改变,我不会显示所有的代码。应用程序类现在称为stream3,它接受一个参数——HDFS目录。目录路径可以是 NFS 或 AWS S3(所有代码示例都将随本书提供):

 val rawDstream = ssc.textFileStream( directory )

流处理与以前相同。流被分割成单词,并打印出前十个单词列表。这次唯一的区别是,在应用程序运行时,数据必须放入HDFS目录中。这是通过 HDFS 文件系统的put命令实现的:

[root@hc2nn log]# hdfs dfs -put ./anaconda.storage.log /data/spark/stream

如您所见,使用的HDFS目录是/data/spark/stream/,文本源日志文件是anaconda.storage.log(位于/var/log/下)。如预期的那样,打印出相同的单词列表和计数:

List : (17104,)
List : (2333,=)
……..
List : (564,True)
List : (495,False)
List : (411,None)
List : (356,at)
List : (335,object)

这些都是基于 TCP 和文件系统数据的简单流式处理方法。但是,如果我想要在 Spark 流处理中使用一些内置的流处理功能呢?接下来将对此进行检查。将使用 Spark 流处理 Flume 库作为示例。

Flume

Flume 是一个 Apache 开源项目和产品,旨在以大数据规模移动大量数据。它具有高度可扩展性、分布式和可靠性,基于数据源、数据汇和数据通道工作,如此图所示,取自flume.apache.org/网站:

Flume

Flume 使用代理来处理数据流。如前图所示,代理具有数据源、数据处理通道和数据汇。更清晰地描述这一点的方法是通过以下图。通道充当源数据的队列,而汇将数据传递给链中的下一个链接。

Flume

Flume 代理可以形成 Flume 架构;一个代理的 sink 的输出可以是第二个代理的输入。Apache Spark 允许使用两种方法来使用 Apache Flume。第一种是基于 Avro 的推送式内存方法,而第二种仍然基于 Avro,是一个基于拉取的系统,使用自定义的 Spark sink 库。

我通过 Cloudera CDH 5.3 集群管理器安装了 Flume,它安装了一个单一代理。检查 Linux 命令行,我可以看到 Flume 版本 1.5 现在可用:

[root@hc2nn ~]# flume-ng version
Flume 1.5.0-cdh5.3.3
Source code repository: https://git-wip-us.apache.org/repos/asf/flume.git
Revision: b88ce1fd016bc873d817343779dfff6aeea07706
Compiled by jenkins on Wed Apr  8 14:57:43 PDT 2015
From source with checksum 389d91c718e03341a2367bf4ef12428e

我将在这里最初实现的基于 Flume 的 Spark 示例是基于 Flume 的推送方法,其中 Spark 充当接收器,Flume 将数据推送到 Spark。以下图表示我将在单个节点上实现的结构:

Flume

消息数据将使用 Linux netcat (nc)命令发送到名为hc2r1m1的主机的端口10777。这将作为 Flume 代理(agent1)的源(source1),它将有一个名为channel1的内存通道。agent1使用的 sink 将再次基于 Apache Avro,但这次是在名为hc2r1m1的主机上,端口号将为11777。Apache Spark Flume 应用程序stream4(我将很快描述)将在此端口上监听 Flume 流数据。

我通过执行netcat (nc)命令来启动流处理过程,针对10777端口。现在,当我在此窗口中输入文本时,它将被用作 Flume 源,并且数据将被发送到 Spark 应用程序:

[hadoop@hc2nn ~]$ nc  hc2r1m1.semtech-solutions.co.nz  10777

为了运行我的 Flume 代理agent1,我创建了一个名为agent1.flume.cfg的 Flume 配置文件,描述了代理的源、通道和 sink。文件的内容如下。第一部分定义了agent1的源、通道和 sink 名称。

agent1.sources  = source1
agent1.channels = channel1
agent1.sinks    = sink1

下一节定义source1为基于 netcat 的,运行在名为hc2r1m1的主机上,端口为10777

agent1.sources.source1.channels=channel1
agent1.sources.source1.type=netcat
agent1.sources.source1.bind=hc2r1m1.semtech-solutions.co.nz
agent1.sources.source1.port=10777

agent1的通道channel1被定义为一个基于内存的通道,最大事件容量为 1000 个事件:

agent1.channels.channel1.type=memory
agent1.channels.channel1.capacity=1000

最后,agent1的 sink sink1 被定义为在名为hc2r1m1的主机上的 Apache Avro sink,并且端口为11777

agent1.sinks.sink1.type=avro
agent1.sinks.sink1.hostname=hc2r1m1.semtech-solutions.co.nz
agent1.sinks.sink1.port=11777
agent1.sinks.sink1.channel=channel1

我创建了一个名为flume.bash的 Bash 脚本来运行 Flume 代理agent1。它看起来像这样:

[hadoop@hc2r1m1 stream]$ more flume.bash

#!/bin/bash

# run the bash agent

flume-ng agent \
 --conf /etc/flume-ng/conf \
 --conf-file ./agent1.flume.cfg \
 -Dflume.root.logger=DEBUG,INFO,console  \
 -name agent1

脚本调用 Flume 可执行文件flume-ng,传递agent1配置文件。调用指定了名为agent1的代理。它还指定了 Flume 配置目录为/etc/flume-ng/conf/,默认值。最初,我将使用基于 Scala 的netcat Flume 源示例来展示数据如何被发送到 Apache Spark 应用程序。然后,我将展示如何以类似的方式处理基于 RSS 的数据源。因此,最初接收netcat数据的 Scala 代码如下。定义了类包名称和应用程序类名称。导入了 Spark 和 Flume 所需的类。最后,定义了主方法:

package nz.co.semtechsolutions

import org.apache.spark._
import org.apache.spark.SparkContext._
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._
import org.apache.spark.streaming.flume._

object stream4 {

 def main(args: Array[String]) {

检查并提取了数据流的主机和端口名称参数:

 if ( args.length < 2 )
 {
 System.err.println("Usage: stream4 <host> <port>")
 System.exit(1)
 }
 val hostname = args(0).trim
 val portnum  = args(1).toInt

 println("hostname : " + hostname)
 println("portnum  : " + portnum)

创建了 Spark 和流上下文。然后,使用流上下文主机和端口号创建了基于 Flume 的数据流。通过调用 Flume 基类FlumeUtilscreateStream方法来实现这一点:

 val appName = "Stream example 4"
 val conf    = new SparkConf()

 conf.setAppName(appName)

 val sc  = new SparkContext(conf)
 val ssc = new StreamingContext(sc, Seconds(10) )

 val rawDstream = FlumeUtils.createStream(ssc,hostname,portnum)

最后,打印了一个流事件计数,并(在我们测试流时用于调试目的)转储了流内容。之后,流上下文被启动并配置为在应用程序终止之前运行:

 rawDstream.count()
 .map(cnt => ">>>> Received events : " + cnt )
 .print()

 rawDstream.map(e => new String(e.event.getBody.array() ))
 .print

 ssc.start()
 ssc.awaitTermination()

 } // end main
} // end stream4

编译完成后,我将使用spark-submit运行此应用程序。在本书的其他章节中,我将使用一个名为run_stream.bash的基于 Bash 的脚本来执行该作业。脚本如下所示:

[hadoop@hc2r1m1 stream]$ more run_stream.bash

#!/bin/bash

SPARK_HOME=/usr/local/spark
SPARK_BIN=$SPARK_HOME/bin
SPARK_SBIN=$SPARK_HOME/sbin

JAR_PATH=/home/hadoop/spark/stream/target/scala-2.10/streaming_2.10-1.0.jar
CLASS_VAL=$1
CLASS_PARAMS="${*:2}"

STREAM_JAR=/usr/local/spark/lib/spark-examples-1.3.1-hadoop2.3.0.jar

cd $SPARK_BIN

./spark-submit \
 --class $CLASS_VAL \
 --master spark://hc2nn.semtech-solutions.co.nz:7077  \
 --executor-memory 100M \
 --total-executor-cores 50 \
 --jars $STREAM_JAR \
 $JAR_PATH \
 $CLASS_PARAMS

因此,此脚本设置了一些基于 Spark 的变量,并为此作业设置了 JAR 库路径。它将要运行的 Spark 类作为其第一个参数。它将所有其他变量作为参数传递给 Spark 应用程序类作业。因此,应用程序的执行如下所示:

[hadoop@hc2r1m1 stream]$ ./run_stream.bash  \
 nz.co.semtechsolutions.stream4 \
 hc2r1m1.semtech-solutions.co.nz  \
 11777

这意味着 Spark 应用程序已准备就绪,并作为 Flume 接收器在端口11777上运行。Flume 输入已准备就绪,作为端口10777上的 netcat 任务运行。现在,可以使用名为flume.bash的 Flume 脚本启动 Flume 代理agent1,以将 netcat 源数据发送到基于 Apache Spark Flume 的接收器:

[hadoop@hc2r1m1 stream]$ ./flume.bash

现在,当文本传递到 netcat 会话时,它应该通过 Flume 流动,并由 Spark 作为流进行处理。让我们试一试:

[hadoop@hc2nn ~]$ nc  hc2r1m1.semtech-solutions.co.nz 10777
I hope that Apache Spark will print this
OK
I hope that Apache Spark will print this
OK
I hope that Apache Spark will print this
OK

已经向 netcat 会话添加了三个简单的文本片段,并收到了OK的确认,以便它们可以传递给 Flume。Flume 会话中的调试输出显示已接收和处理了事件(每行一个):

2015-07-06 18:13:18,699 (netcat-handler-0) [DEBUG - org.apache.flume.source.NetcatSource$NetcatSocketHandler.run(NetcatSource.java:318)] Chars read = 41
2015-07-06 18:13:18,700 (netcat-handler-0) [DEBUG - org.apache.flume.source.NetcatSource$NetcatSocketHandler.run(NetcatSource.java:322)] Events processed = 1
2015-07-06 18:13:18,990 (netcat-handler-0) [DEBUG - org.apache.flume.source.NetcatSource$NetcatSocketHandler.run(NetcatSource.java:318)] Chars read = 41
2015-07-06 18:13:18,991 (netcat-handler-0) [DEBUG - org.apache.flume.source.NetcatSource$NetcatSocketHandler.run(NetcatSource.java:322)] Events processed = 1
2015-07-06 18:13:19,270 (netcat-handler-0) [DEBUG - org.apache.flume.source.NetcatSource$NetcatSocketHandler.run(NetcatSource.java:318)] Chars read = 41
2015-07-06 18:13:19,271 (netcat-handler-0) [DEBUG - org.apache.flume.source.NetcatSource$NetcatSocketHandler.run(NetcatSource.java:322)] Events processed = 1

最后,在 Sparkstream4应用程序会话中,已接收和处理了三个事件。在这种情况下,将其转储到会话以证明数据已到达。当然,这不是您通常会做的事情,但我想证明数据通过此配置传输:

-------------------------------------------
Time: 1436163210000 ms
-------------------------------------------
>>> Received events : 3
-------------------------------------------
Time: 1436163210000 ms
-------------------------------------------
I hope that Apache Spark will print this
I hope that Apache Spark will print this
I hope that Apache Spark will print this

这很有趣,但实际上并不是一个生产值得的 Spark Flume 数据处理示例。因此,为了演示潜在的真实数据处理方法,我将更改 Flume 配置文件的源细节,以便使用一个 Perl 脚本,如下所示:

agent1.sources.source1.type=exec
agent1.sources.source.command=./rss.perl

之前提到的 Perl 脚本rss.perl只是作为路透社科学新闻的数据源。它将新闻作为 XML 接收,并将其转换为 JSON 格式。它还清理了不需要的噪音数据。首先,导入了像 LWP 和XML::XPath这样的包以启用 XML 处理。然后,它指定了基于科学的路透社新闻数据源,并创建了一个新的 LWP 代理来处理数据,类似于这样:

#!/usr/bin/perl

use strict;
use LWP::UserAgent;
use XML::XPath;

my $urlsource="http://feeds.reuters.com/reuters/scienceNews" ;

my  $agent = LWP::UserAgent->new;

然后打开一个无限循环,对 URL 执行 HTTP 的GET请求。请求被配置,代理通过调用请求方法发出请求:

while()
{
 my  $req = HTTP::Request->new(GET => ($urlsource));

 $req->header('content-type' => 'application/json');
 $req->header('Accept'       => 'application/json');

 my $resp = $agent->request($req);

如果请求成功,那么返回的 XML 数据被定义为请求的解码内容。通过使用路径/rss/channel/item/title调用 XPath 来从 XML 中提取标题信息:

 if ( $resp->is_success )
 {
 my $xmlpage = $resp -> decoded_content;

 my $xp = XML::XPath->new( xml => $xmlpage );
 my $nodeset = $xp->find( '/rss/channel/item/title' );

 my @titles = () ;
 my $index = 0 ;

对于从提取的标题数据标题 XML 字符串中的每个节点,都会提取数据。清除不需要的 XML 标签,并添加到名为titles的基于 Perl 的数组中:

 foreach my $node ($nodeset->get_nodelist)
 {
 my $xmlstring = XML::XPath::XMLParser::as_string($node) ;

 $xmlstring =~ s/<title>//g;
 $xmlstring =~ s/<\/title>//g;
 $xmlstring =~ s/"//g;
 $xmlstring =~ s/,//g;

 $titles[$index] = $xmlstring ;
 $index = $index + 1 ;

 } # foreach find node

对于请求响应 XML 中的基于描述的数据,进行相同的处理过程。这次使用的 XPath 值是/rss/channel/item/description/。需要清理的描述数据标签更多,因此有更多的 Perl 搜索和行替换操作(s///g):

 my $nodeset = $xp->find( '/rss/channel/item/description' );

 my @desc = () ;
 $index = 0 ;

 foreach my $node ($nodeset->get_nodelist)
 {
 my $xmlstring = XML::XPath::XMLParser::as_string($node) ;

 $xmlstring =~ s/<img.+\/img>//g;
 $xmlstring =~ s/href=".+"//g;
 $xmlstring =~ s/src="img/.+"//g;
 $xmlstring =~ s/src='.+'//g;
 $xmlstring =~ s/<br.+\/>//g;
 $xmlstring =~ s/<\/div>//g;
 $xmlstring =~ s/<\/a>//g;
 $xmlstring =~ s/<a >\n//g;
 $xmlstring =~ s/<img >//g;
 $xmlstring =~ s/<img \/>//g;
 $xmlstring =~ s/<div.+>//g;
 $xmlstring =~ s/<title>//g;
 $xmlstring =~ s/<\/title>//g;
 $xmlstring =~ s/<description>//g;
 $xmlstring =~ s/<\/description>//g;
 $xmlstring =~ s/&lt;.+>//g;
 $xmlstring =~ s/"//g;
 $xmlstring =~ s/,//g;
 $xmlstring =~ s/\r|\n//g;

 $desc[$index] = $xmlstring ;
 $index = $index + 1 ;

 } # foreach find node

最后,基于 XML 的标题和描述数据以 RSS JSON 格式输出,使用print命令。然后脚本休眠 30 秒,并请求更多的 RSS 新闻信息进行处理:

 my $newsitems = $index ;
 $index = 0 ;

 for ($index=0; $index < $newsitems; $index++) {

 print "{\"category\": \"science\","
 . " \"title\": \"" .  $titles[$index] . "\","
 . " \"summary\": \"" .  $desc[$index] . "\""
 . "}\n";

 } # for rss items

 } # success ?

 sleep(30) ;

} # while

我已经创建了第二个基于 Scala 的流处理代码示例,名为stream5。它类似于stream4示例,但现在它处理来自流的rss项数据。接下来定义了一个案例类来处理 XML rss信息中的类别、标题和摘要。定义了一个 HTML 位置来存储来自 Flume 通道的结果数据:

 case class RSSItem(category : String, title : String, summary : String)

 val now: Long = System.currentTimeMillis

 val hdfsdir = "hdfs://hc2nn:8020/data/spark/flume/rss/"

从基于 Flume 的事件的rss流数据转换为字符串。然后使用名为RSSItem的案例类进行格式化。如果有事件数据,那么将使用先前的hdfsdir路径将其写入 HDFS 目录:

 rawDstream.map(record => {
 implicit val formats = DefaultFormats
 readRSSItem.array()))
 })
 .foreachRDD(rdd => {
 if (rdd.count() > 0) {
 rdd.map(item => {
 implicit val formats = DefaultFormats
 write(item)
 }).saveAsTextFile(hdfsdir+"file_"+now.toString())
 }
 })

运行此代码示例,可以看到 Perl rss脚本正在生成数据,因为 Flume 脚本输出表明已接受和接收了 80 个事件:

2015-07-07 14:14:24,017 (agent-shutdown-hook) [DEBUG - org.apache.flume.source.ExecSource.stop(ExecSource.java:219)] Exec source with command:./news_rss_collector.py stopped. Metrics:SOURCE:source1{src.events.accepted=80, src.events.received=80, src.append.accepted=0, src.append-batch.accepted=0, src.open-connection.count=0, src.append-batch.received=0, src.append.received=0}

Scala Spark 应用程序stream5已经处理了 80 个事件,分为两批:

>>>> Received events : 73
>>>> Received events : 7

事件已存储在 HDFS 中,位于预期目录下,如 Hadoop 文件系统ls命令所示:

[hadoop@hc2r1m1 stream]$ hdfs dfs -ls /data/spark/flume/rss/
Found 2 items
drwxr-xr-x   - hadoop supergroup          0 2015-07-07 14:09 /data/spark/flume/rss/file_1436234439794
drwxr-xr-x   - hadoop supergroup          0 2015-07-07 14:14 /data/spark/flume/rss/file_1436235208370

此外,使用 Hadoop 文件系统cat命令,可以证明 HDFS 上的文件包含 rss feed 新闻数据,如下所示:

[hadoop@hc2r1m1 stream]$  hdfs dfs -cat /data/spark/flume/rss/file_1436235208370/part-00000 | head -1

{"category":"healthcare","title":"BRIEF-Aetna CEO says has not had specific conversations with DOJ on Humana - CNBC","summary":"* Aetna CEO Says Has Not Had Specific Conversations With Doj About Humana Acquisition - CNBC"}

这个基于 Spark 流的示例使用 Apache Flume 从 rss 源传输数据,通过 Flume,通过 Spark 消费者传输到 HDFS。这是一个很好的例子,但如果您想要向一组消费者发布数据怎么办?在下一节中,我将研究 Apache Kafka——一个发布订阅消息系统,并确定它如何与 Spark 一起使用。

Kafka

Apache Kafka (kafka.apache.org/) 是 Apache 中的一个顶级开源项目。它是一个快速且高度可扩展的大数据发布/订阅消息系统。它使用消息代理进行数据管理,并使用 ZooKeeper 进行配置,以便数据可以组织成消费者组和主题。Kafka 中的数据被分成分区。在这个示例中,我将演示一个无接收器的基于 Spark 的 Kafka 消费者,因此与我的 Kafka 数据相比,我不需要担心配置 Spark 数据分区。

为了演示基于 Kafka 的消息生产和消费,我将使用上一节中的 Perl RSS 脚本作为数据源。传递到 Kafka 并传递到 Spark 的数据将是 JSON 格式的 Reuters RSS 新闻数据。

当消息生产者创建主题消息时,它们会按消息顺序顺序放置在分区中。分区中的消息将保留一段可配置的时间。Kafka 然后为每个消费者存储偏移值,该值是该消费者在该分区中的位置(以消息消费为准)。

我目前正在使用 Cloudera 的 CDH 5.3 Hadoop 集群。为了安装 Kafka,我需要从archive.cloudera.com/csds/kafka/下载 Kafka JAR 库文件。

下载文件后,鉴于我正在使用 CDH 集群管理器,我需要将文件复制到我的 NameNode CentOS 服务器上的/opt/cloudera/csd/目录,以便安装时可见:

[root@hc2nn csd]# pwd
/opt/cloudera/csd

[root@hc2nn csd]# ls -l KAFKA-1.2.0.jar
-rw-r--r-- 1 hadoop hadoop 5670 Jul 11 14:56 KAFKA-1.2.0.jar

然后,我需要重新启动我的 NameNode 或主服务器上的 Cloudera 集群管理器服务器,以便识别更改。这是以 root 用户使用 service 命令完成的,命令如下:

[root@hc2nn hadoop]# service cloudera-scm-server restart
Stopping cloudera-scm-server:                              [  OK  ]
Starting cloudera-scm-server:                              [  OK  ]

现在,Kafka 包应该在 CDH 管理器的主机 | 包裹下可见,如下图所示。您可以按照 CDH 包安装的常规下载、分发和激活周期进行操作:

Kafka

我在集群中的每个数据节点或 Spark 从节点机器上安装了 Kafka 消息代理。然后为每个 Kafka 代理服务器设置了 Kafka 代理 ID 值,分别为 1 到 4。由于 Kafka 使用 ZooKeeper 进行集群数据配置,我希望将所有 Kafka 数据保留在 ZooKeeper 中名为kafka的顶级节点中。为了做到这一点,我将 Kafka ZooKeeper 根值设置为zookeeper.chroot,称为/kafka。在进行这些更改后,我重新启动了 CDH Kafka 服务器,以使更改生效。

安装了 Kafka 后,我可以检查可用于测试的脚本。以下清单显示了基于 Kafka 的消息生产者和消费者脚本,以及用于管理主题和检查消费者偏移的脚本。这些脚本将在本节中使用,以演示 Kafka 的功能:

[hadoop@hc2nn ~]$ ls /usr/bin/kafka*

/usr/bin/kafka-console-consumer         /usr/bin/kafka-run-class
/usr/bin/kafka-console-producer         /usr/bin/kafka-topics
/usr/bin/kafka-consumer-offset-checker

为了运行已安装的 Kafka 服务器,我需要设置经纪人服务器 ID(broker.id)值,否则将出现错误。安装并运行 Kafka 后,我需要准备一个消息生产者脚本。下面给出的简单 Bash 脚本名为kafka.bash,它定义了一个以逗号分隔的主机和端口的经纪人列表。它还定义了一个名为rss的主题。然后,它调用 Perl 脚本rss.perl生成基于 RSS 的数据。然后将这些数据传送到名为kafka-console-producer的 Kafka 生产者脚本以发送到 Kafka。

[hadoop@hc2r1m1 stream]$ more kafka.bash

#!/bin/bash

BROKER_LIST="hc2r1m1:9092,hc2r1m2:9092,hc2r1m3:9092,hc2r1m4:9092"
TOPIC="rss"

./rss.perl | /usr/bin/kafka-console-producer --broker-list $BROKER_LIST --topic $TOPIC

注意,我还没有在这一点上提到 Kafka 主题。在 Kafka 中创建主题时,可以指定分区的数量。在下面的示例中,使用create选项调用了kafka-topics脚本。分区的数量设置为5,数据复制因子设置为3。ZooKeeper 服务器字符串已定义为hc2r1m2-4,端口号为2181。还要注意,顶级 ZooKeeper Kafka 节点在 ZooKeeper 字符串中被定义为/kafka

/usr/bin/kafka-topics \
 --create  \
 --zookeeper hc2r1m2:2181,hc2r1m3:2181,hc2r1m4:2181/kafka \
 --replication-factor 3  \
 --partitions 5  \
 --topic rss

我还创建了一个名为kafka_list.bash的 Bash 脚本,用于测试时检查已创建的所有 Kafka 主题以及 Kafka 消费者偏移。它使用kafka-topics命令调用list选项和ZooKeeper字符串来获取已创建主题的列表。然后,它使用 Kafka 脚本kafka-consumer-offset-checker调用ZooKeeper字符串、主题名称和组名称来获取消费者偏移值的列表。使用此脚本,我可以检查我的主题是否已创建,并且主题数据是否被正确消耗:

[hadoop@hc2r1m1 stream]$ cat kafka_list.bash

#!/bin/bash

ZOOKEEPER="hc2r1m2:2181,hc2r1m3:2181,hc2r1m4:2181/kafka"
TOPIC="rss"
GROUP="group1"

echo ""
echo "================================"
echo " Kafka Topics "
echo "================================"

/usr/bin/kafka-topics --list --zookeeper $ZOOKEEPER

echo ""
echo "================================"
echo " Kafka Offsets "
echo "================================"

/usr/bin/kafka-consumer-offset-checker \
 --group $GROUP \
 --topic $TOPIC \
 --zookeeper $ZOOKEEPER

接下来,我需要创建基于 Apache Spark Scala 的 Kafka 消费者代码。正如我所说的,我将创建一个无接收器的示例,以便 Kafka 数据分区在 Kafka 和 Spark 中匹配。示例被称为stream6。首先,定义了包,并导入了 Kafka、spark、context 和 streaming 的类。然后,定义了名为stream6的对象类和主方法。代码如下:

package nz.co.semtechsolutions

import kafka.serializer.StringDecoder

import org.apache.spark._
import org.apache.spark.SparkContext._
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._
import org.apache.spark.streaming.kafka._

object stream6 {

 def main(args: Array[String]) {

接下来,检查和处理了类参数(经纪人字符串、组 ID 和主题)。如果类参数不正确,则打印错误并停止执行,否则定义参数变量:

 if ( args.length < 3 )
 {
 System.err.println("Usage: stream6 <brokers> <groupid> <topics>\n")
 System.err.println("<brokers> = host1:port1,host2:port2\n")
 System.err.println("<groupid> = group1\n")
 System.err.println("<topics>  = topic1,topic2\n")
 System.exit(1)
 }

 val brokers = args(0).trim
 val groupid = args(1).trim
 val topics  = args(2).trim

 println("brokers : " + brokers)
 println("groupid : " + groupid)
 println("topics  : " + topics)

Spark 上下文根据应用程序名称进行了定义。同样,Spark URL 保持默认值。使用 Spark 上下文创建了流上下文。我将流批处理间隔保持为 10 秒,与上一个示例相同。但是,您可以使用自己选择的参数进行设置:

 val appName = "Stream example 6"
 val conf    = new SparkConf()

 conf.setAppName(appName)

 val sc  = new SparkContext(conf)
 val ssc = new StreamingContext(sc, Seconds(10) )

接下来,设置了经纪人列表和组 ID 作为参数。然后使用这些值创建了一个名为rawDStream的基于 Kafka 的 Spark 流:

 val topicsSet = topics.split(",").toSet
 val kafkaParams : Map[String, String] =
 Map("metadata.broker.list" -> brokers,
 "group.id" -> groupid )

 val rawDstream = KafkaUtils.createDirectStreamString, String, StringDecoder, StringDecoder

出于调试目的,我再次打印了流事件计数,以便我知道应用程序何时接收和处理数据。

 rawDstream.count().map(cnt => ">>>>>>>>>>>>>>> Received events : " + cnt ).print()

Kafka 数据的 HDSF 位置已定义为/data/spark/kafka/rss/。它已从 DStream 映射到变量lines。使用foreachRDD方法,在lines变量上进行数据计数检查,然后使用saveAsTextFile方法将数据保存到 HDFS 中。

 val now: Long = System.currentTimeMillis

 val hdfsdir = "hdfs://hc2nn:8020/data/spark/kafka/rss/"

 val lines = rawDstream.map(record => record._2)

 lines.foreachRDD(rdd => {
 if (rdd.count() > 0) {
 rdd.saveAsTextFile(hdfsdir+"file_"+now.toString())
 }
 })

最后,Scala 脚本通过启动流处理并将应用程序类设置为使用awaitTermination直到终止来关闭:

 ssc.start()
 ssc.awaitTermination()

 } // end main

} // end stream6

在解释了所有脚本并运行了 Kafka CDH 代理之后,现在是时候检查 Kafka 配置了,您可能还记得这是由 Apache ZooKeeper 维护的(迄今为止描述的所有代码示例都将随本书一起发布)。我将使用zookeeper-client工具,并连接到名为hc2r1m2的主机上的2181端口上的zookeeper服务器。如您在此处所见,我已从client会话收到了连接消息。

[hadoop@hc2r1m1 stream]$ /usr/bin/zookeeper-client -server hc2r1m2:2181

[zk: hc2r1m2:2181(CONNECTED) 0]

如果您记得,我指定了 Kafka 的顶级 ZooKeeper 目录为/kafka。如果我现在通过客户端会话检查这一点,我可以看到 Kafka ZooKeeper 结构。我将对brokers(CDH Kafka 代理服务器)和consumers(先前的 Spark Scala 代码)感兴趣。ZooKeeper ls命令显示,四个 Kafka 服务器已在 ZooKeeper 中注册,并按其broker.id配置值从一到四列出。

[zk: hc2r1m2:2181(CONNECTED) 2] ls /kafka
[consumers, config, controller, admin, brokers, controller_epoch]

[zk: hc2r1m2:2181(CONNECTED) 3] ls /kafka/brokers
[topics, ids]

[zk: hc2r1m2:2181(CONNECTED) 4] ls /kafka/brokers/ids
[3, 2, 1, 4]

我将使用 Kafka 脚本kafka-topicscreate标志创建我想要用于此测试的主题。我这样做是因为我可以在手动操作时演示数据分区的定义。请注意,我已经在 Kafka topic rss中设置了五个分区,如下面的代码所示。还要注意,命令的 ZooKeeper 连接字符串是由逗号分隔的 ZooKeeper 服务器列表组成的,以/kafka结尾,这意味着命令将新主题放在适当的位置。

[hadoop@hc2nn ~]$ /usr/bin/kafka-topics \
>   --create  \
>   --zookeeper hc2r1m2:2181,hc2r1m3:2181,hc2r1m4:2181/kafka \
>   --replication-factor 3  \
>   --partitions 5  \
>   --topic rss

Created topic "rss".

现在,当我使用 ZooKeeper 客户端检查 Kafka 主题配置时,我可以看到正确的主题名称和预期的分区数。

[zk: hc2r1m2:2181(CONNECTED) 5] ls /kafka/brokers/topics
[rss]

[zk: hc2r1m2:2181(CONNECTED) 6] ls /kafka/brokers/topics/rss
[partitions]

[zk: hc2r1m2:2181(CONNECTED) 7] ls /kafka/brokers/topics/rss/partitions
[3, 2, 1, 0, 4]

这描述了 ZooKeeper 中 Kafka 代理服务器的配置,但数据消费者的情况如何呢?好吧,以下清单显示了数据将被保存的位置。但请记住,此时没有运行消费者,因此在 ZooKeeper 中没有表示。

[zk: hc2r1m2:2181(CONNECTED) 9]  ls /kafka/consumers
[]
[zk: hc2r1m2:2181(CONNECTED) 10] quit

为了开始这个测试,我将运行我的 Kafka 数据生产者和消费者脚本。我还需要检查 Spark 应用程序类的输出,并需要检查 Kafka 分区偏移和 HDFS,以确保数据已到达。这非常复杂,所以我将在下图中添加一个图表来解释测试架构。

名为rss.perl的 Perl 脚本将用于为 Kafka 数据生产者提供数据源,该数据生产者将数据提供给 CDH Kafka 代理服务器。数据将存储在 ZooKeeper 中,结构刚刚在顶级节点/kafka下进行了检查。然后,基于 Apache Spark Scala 的应用程序将充当 Kafka 消费者,并读取将存储在 HDFS 中的数据。

Kafka

为了尝试解释这里的复杂性,我还将检查运行 Apache Spark 类的方法。它将通过spark-submit命令启动。请再次记住,所有这些脚本都将随本书一起发布,这样您就可以在自己的时间内对它们进行检查。我总是使用脚本进行服务器测试管理,以便封装复杂性,并且命令执行可以快速重复。脚本run_stream.bash类似于本章和本书中已经使用过的许多示例脚本。它接受一个类名和类参数,并通过 spark-submit 运行该类。

[hadoop@hc2r1m1 stream]$ more run_stream.bash

#!/bin/bash

SPARK_HOME=/usr/local/spark
SPARK_BIN=$SPARK_HOME/bin
SPARK_SBIN=$SPARK_HOME/sbin

JAR_PATH=/home/hadoop/spark/stream/target/scala-2.10/streaming_2.10-1.0.jar
CLASS_VAL=$1
CLASS_PARAMS="${*:2}"

STREAM_JAR=/usr/local/spark/lib/spark-examples-1.3.1-hadoop2.3.0.jar
cd $SPARK_BIN

./spark-submit \
 --class $CLASS_VAL \
 --master spark://hc2nn.semtech-solutions.co.nz:7077  \
 --executor-memory 100M \
 --total-executor-cores 50 \
 --jars $STREAM_JAR \
 $JAR_PATH \
 $CLASS_PARAMS

然后我使用了第二个脚本,调用run_kafka_example.bash脚本来执行先前stream6应用程序类中的 Kafka 消费者代码。请注意,此脚本设置了完整的应用程序类名-代理服务器列表。它还设置了一个名为rss的主题名称,用于数据消耗。最后,它定义了一个名为group1的消费者组。请记住,Kafka 是一个发布/订阅消息代理系统。可以通过主题、组和分区组织许多生产者和消费者:

[hadoop@hc2r1m1 stream]$ more run_kafka_example.bash

#!/bin/bash

RUN_CLASS=nz.co.semtechsolutions.stream6
BROKERS="hc2r1m1:9092,hc2r1m2:9092,hc2r1m3:9092,hc2r1m4:9092"
GROUPID=group1
TOPICS=rss

# run the Apache Spark Kafka example

./run_stream.bash $RUN_CLASS \
 $BROKERS \
 $GROUPID \
 $TOPICS

因此,我将通过运行run_kafka_example.bash脚本来启动 Kafka 消费者,然后将运行先前的stream6 Scala 代码使用 spark-submit。在使用名为kafka_list.bash的脚本监视 Kafka 数据消耗时,我能够让kafka-consumer-offset-checker脚本列出基于 Kafka 的主题,但由于某种原因,它在检查偏移时不会检查正确的路径(在 ZooKeeper 中的/kafka下)如下所示:

[hadoop@hc2r1m1 stream]$ ./kafka_list.bash

================================
 Kafka Topics
================================
__consumer_offsets
rss

================================
 Kafka Offsets
================================
Exiting due to: org.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode = NoNode for /consumers/group1/offsets/rss/4.

通过使用kafka.bash脚本启动 Kafka 生产者 rss feed,我现在可以开始通过 Kafka 将基于 rss 的数据馈送到 Spark,然后进入 HDFS。定期检查spark-submit会话输出,可以看到事件通过基于 Spark 的 Kafka DStream 传递。下面的输出来自 Scala 代码中的流计数,并显示在那一点上,处理了 28 个事件:

-------------------------------------------
Time: 1436834440000 ms
-------------------------------------------
>>>>>>>>>>>>>>> Received events : 28

通过在/data/spark/kafka/rss/目录下检查 HDFS,通过 Hadoop 文件系统ls命令,可以看到现在在 HDFS 上存储了数据:

[hadoop@hc2r1m1 stream]$ hdfs dfs -ls /data/spark/kafka/rss
Found 1 items
drwxr-xr-x   - hadoop supergroup          0 2015-07-14 12:40 /data/spark/kafka/rss/file_1436833769907

通过检查这个目录的内容,可以看到存在一个 HDFS 部分数据文件,应该包含来自路透社的基于 RSS 的数据:

[hadoop@hc2r1m1 stream]$ hdfs dfs -ls /data/spark/kafka/rss/file_1436833769907
Found 2 items
-rw-r--r--   3 hadoop supergroup          0 2015-07-14 12:40 /data/spark/kafka/rss/file_1436833769907/_SUCCESS
-rw-r--r--   3 hadoop supergroup       8205 2015-07-14 12:40 /data/spark/kafka/rss/file_1436833769907/part-00001

使用下面的 Hadoop 文件系统cat命令,我可以转储这个基于 HDFS 的文件的内容以检查其内容。我已经使用了 Linux 的head命令来限制数据以节省空间。显然,这是 Perl 脚本rss.perl从 XML 转换为 RSS JSON 格式的 RSS 路透社科学信息。

[hadoop@hc2r1m1 stream]$ hdfs dfs -cat /data/spark/kafka/rss/file_1436833769907/part-00001 | head -2

{"category": "science", "title": "Bear necessities: low metabolism lets pandas survive on bamboo", "summary": "WASHINGTON (Reuters) - Giant pandas eat vegetables even though their bodies are better equipped to eat meat. So how do these black-and-white bears from the remote misty mountains of central China survive on a diet almost exclusively of a low-nutrient food like bamboo?"}

{"category": "science", "title": "PlanetiQ tests sensor for commercial weather satellites", "summary": "CAPE CANAVERAL (Reuters) - PlanetiQ a privately owned company is beginning a key test intended to pave the way for the first commercial weather satellites."}

这结束了这个 Kafka 示例。可以看到 Kafka 代理已经安装和配置。它显示了一个基于 RSS 数据的 Kafka 生产者已经将数据馈送到代理中。使用 ZooKeeper 客户端已经证明了 Kafka 架构,匹配代理、主题和分区已经在 ZooKeeper 中设置。最后,使用基于 Apache Spark 的 Scala 代码,在stream6应用程序中已经显示了 Kafka 数据已被消耗并保存到 HDFS 中。

总结

我本可以提供像 Kinesis 这样的系统的流式示例,以及排队系统,但在本章中没有足够的空间。Twitter 流已经在检查点部分的示例中进行了检查。

本章提供了通过 Spark 流检查点进行数据恢复的实际示例。它还触及了检查点的性能限制,并表明检查点间隔应设置为 Spark 流批处理间隔的五到十倍。检查点提供了一种基于流的恢复机制,以防 Spark 应用程序失败。

本章提供了一些基于流的 TCP、文件、Flume 和 Kafka 的 Spark 流编码示例。这里的所有示例都是基于 Scala 的,并且使用sbt进行编译。所有的代码都将随本书一起发布。当示例架构变得过于复杂时,我提供了一个架构图(我在这里考虑的是 Kafka 示例)。

对我来说,Apache Spark 流模块包含了丰富的功能,应该能满足大部分需求,并且随着未来版本的 Spark 发布而不断增长。记得查看 Apache Spark 网站(spark.apache.org/),并通过<user@spark.apache.org>加入 Spark 用户列表。不要害怕提问,或犯错误,因为在我看来,错误教会的比成功多。

下一章将审查 Spark SQL 模块,并提供 SQL、数据框架和访问 Hive 等主题的实例。

第四章:Apache Spark SQL

在本章中,我想检查 Apache Spark SQL,使用 Apache Hive 与 Spark 以及数据框。数据框在 Spark 1.3 中引入,是列式数据存储结构,大致相当于关系数据库表。本书的章节并非按顺序开发,因此早期章节可能使用比后期章节更旧的 Spark 版本。我还想检查 Spark SQL 的用户定义函数。关于 Spark 类 API 的信息,可以在以下位置找到:spark.apache.org/docs/<version>/api/scala/index.html

我更喜欢使用 Scala,但 API 信息也可用于 Java 和 Python 格式。<version>值是指您将使用的 Spark 版本的发布版本-1.3.1。本章将涵盖以下主题:

  • SQL 上下文

  • 导入和保存数据

  • 数据框

  • 使用 SQL

  • 用户定义的函数

  • 使用 Hive

在直接进入 SQL 和数据框之前,我将概述 SQL 上下文。

SQL 上下文

SQL 上下文是在 Apache Spark 中处理列数据的起点。它是从 Spark 上下文创建的,并提供了加载和保存不同类型数据文件的方法,使用数据框,以及使用 SQL 操作列数据等功能。它可用于以下操作:

  • 通过 SQL 方法执行 SQL

  • 通过 UDF 方法注册用户定义的函数

  • 缓存

  • 配置

  • 数据框

  • 数据源访问

  • DDL 操作

我相信还有其他领域,但你明白我的意思。本章的示例是用 Scala 编写的,只是因为我更喜欢这种语言,但你也可以用 Python 和 Java 进行开发。如前所示,SQL 上下文是从 Spark 上下文创建的。隐式导入 SQL 上下文允许您将 RDD 隐式转换为数据框:

val sqlContext = new org.apache.spark.sql.SQLContext(sc)
import sqlContext.implicits._

例如,使用之前的implicits调用,允许您导入 CSV 文件并按分隔符字符拆分它。然后可以使用toDF方法将包含数据的 RDD 转换为数据框。

还可以为访问和操作 Apache Hive 数据库表数据定义 Hive 上下文(Hive 是 Hadoop 生态系统的一部分的 Apache 数据仓库,它使用 HDFS 进行存储)。与 Spark 上下文相比,Hive 上下文允许使用 SQL 功能的超集。在本章的后面部分将介绍如何在 Spark 中使用 Hive。

接下来,我将检查一些支持的文件格式,用于导入和保存数据。

导入和保存数据

我想在这里添加有关导入和保存数据的部分,即使它并不纯粹关于 Spark SQL,这样我就可以介绍诸如ParquetJSON文件格式等概念。这一部分还让我能够涵盖如何在一个地方方便地访问和保存松散文本数据,以及 CSV、Parquet 和 JSON 格式。

处理文本文件

使用 Spark 上下文,可以使用textFile方法将文本文件加载到 RDD 中。此外,wholeTextFile方法可以将目录的内容读取到 RDD 中。以下示例显示了如何将基于本地文件系统(file://)或 HDFS(hdfs://)的文件读取到 Spark RDD 中。这些示例显示数据将被分成六个部分以提高性能。前两个示例相同,因为它们都操作 Linux 文件系统上的文件:

sc.textFile("/data/spark/tweets.txt",6)
sc.textFile("file:///data/spark/tweets.txt",6)
sc.textFile("hdfs://server1:4014/data/spark/tweets.txt",6)

处理 JSON 文件

JSON 是一种数据交换格式,由 Javascript 开发。JSON实际上代表JavaScript Object Notation。它是一种基于文本的格式,可以表示为 XML。以下示例使用名为jsonFile的 SQL 上下文方法加载基于 HDFS 的 JSON 数据文件,名称为device.json。生成的数据被创建为数据框:

val dframe = sqlContext.jsonFile("hdfs:///data/spark/device.json")

数据可以使用数据框toJSON方法以 JSON 格式保存,如下例所示。首先导入 Apache Spark 和 Spark SQL 类:

import org.apache.spark._
import org.apache.spark.SparkContext._
import org.apache.spark.sql.Row;
import org.apache.spark.sql.types.{StructType,StructField,StringType};

接下来,定义了一个名为sql1的对象类,以及一个带参数的主方法。定义了一个配置对象,用于创建一个 Spark 上下文。主 Spark URL 保留为默认值,因此 Spark 期望本地模式,本地主机和7077端口:

object sql1 {

 def main(args: Array[String]) {

 val appName = "sql example 1"
 val conf    = new SparkConf()

 conf.setAppName(appName)

 val sc = new SparkContext(conf)

从 Spark 上下文创建一个 SQL 上下文,并使用textFile方法加载 CSV 格式的原始文本文件adult.test.data_1x。然后创建一个包含数据列名称的模式字符串,并通过将字符串按其间距拆分,并使用StructTypeStructField方法将每个模式列定义为字符串值:

 val sqlContext = new org.apache.spark.sql.SQLContext(sc)

 val rawRdd = sc.textFile("hdfs:///data/spark/sql/adult.test.data_1x")

 val schemaString = "age workclass fnlwgt education " +   "educational-num  marital-status occupation relationship " +
"race gender capital-gain capital-loss hours-per-week " +
"native-country income"

 val schema =
 StructType(
 schemaString.split(" ").map(fieldName => StructField(fieldName, StringType, true)))

然后,通过使用逗号作为行分隔符从原始 CSV 数据中创建每个数据行,然后将元素添加到Row()结构中。从模式创建数据框,然后将行数据转换为 JSON 格式,使用toJSON方法。最后,使用saveAsTextFile方法将数据保存到 HDFS:

 val rowRDD = rawRdd.map(_.split(","))
 .map(p => Row( p(0),p(1),p(2),p(3),p(4),p(5),p(6),p(7),p(8),
 p(9),p(10),p(11),p(12),p(13),p(14) ))

 val adultDataFrame = sqlContext.createDataFrame(rowRDD, schema)

 val jsonData = adultDataFrame.toJSON

 jsonData.saveAsTextFile("hdfs:///data/spark/sql/adult.json")

 } // end main

} // end sql1

因此,可以在 HDFS 上看到生成的数据,Hadoop 文件系统ls命令如下所示,数据驻留在target目录中作为成功文件和两个部分文件。

[hadoop@hc2nn sql]$ hdfs dfs -ls /data/spark/sql/adult.json

Found 3 items
-rw-r--r--   3 hadoop supergroup          0 2015-06-20 17:17 /data/spark/sql/adult.json/_SUCCESS
-rw-r--r--   3 hadoop supergroup       1731 2015-06-20 17:17 /data/spark/sql/adult.json/part-00000
-rw-r--r--   3 hadoop supergroup       1724 2015-06-20 17:17 /data/spark/sql/adult.json/part-00001

使用 Hadoop 文件系统的cat命令,可以显示 JSON 数据的内容。我将展示一个示例以节省空间:

[hadoop@hc2nn sql]$ hdfs dfs -cat /data/spark/sql/adult.json/part-00000 | more

{"age":"25","workclass":" Private","fnlwgt":" 226802","education":" 11th","educational-num":"
 7","marital-status":" Never-married","occupation":" Machine-op-inspct","relationship":" Own-
child","race":" Black","gender":" Male","capital-gain":" 0","capital-loss":" 0","hours-per-we
ek":" 40","native-country":" United-States","income":" <=50K"}

处理 Parquet 数据非常类似,接下来我将展示。

处理 Parquet 文件

Apache Parquet 是 Hadoop 工具集中许多工具使用的另一种基于列的数据格式,例如 Hive、Pig 和 Impala。它通过使用高效的压缩和编码例程来提高性能。

Parquet 处理示例与 JSON Scala 代码非常相似。创建数据框,然后使用 Parquet 类型的 save 方法以 Parquet 格式保存:

 val adultDataFrame = sqlContext.createDataFrame(rowRDD, schema)
 adultDataFrame.save("hdfs:///data/spark/sql/adult.parquet","parquet")

 } // end main

} // end sql2

这会生成一个基于 HDFS 的目录,其中包含三个基于 Parquet 的文件:一个常见的元数据文件,一个元数据文件和一个临时文件:

[hadoop@hc2nn sql]$ hdfs dfs -ls /data/spark/sql/adult.parquet
Found 3 items
-rw-r--r--   3 hadoop supergroup       1412 2015-06-21 13:17 /data/spark/sql/adult.parquet/_common_metadata
-rw-r--r--   3 hadoop supergroup       1412 2015-06-21 13:17 /data/spark/sql/adult.parquet/_metadata
drwxr-xr-x   - hadoop supergroup          0 2015-06-21 13:17 /data/spark/sql/adult.parquet/_temporary

使用 Hadoop 文件系统的cat命令列出元数据文件的内容,可以了解数据格式。但是 Parquet 头是二进制的,因此不能使用morecat显示:

[hadoop@hc2nn sql]$ hdfs dfs -cat /data/spark/sql/adult.parquet/_metadata | more
s%
ct","fields":[{"name":"age","type":"string","nullable":true,"metadata":{}},{"name":"workclass
","type":"string","nullable":true,"metadata":{}},{"name":"fnlwgt","type":"string","nullable":
true,"metadata":{}},

有关可能的 Spark 和 SQL 上下文方法的更多信息,请检查名为org.apache.spark.SparkContextorg.apache.spark.sql.SQLContext的类的内容,使用 Apache Spark API 路径,以获取您感兴趣的 Spark 的特定<version>

spark.apache.org/docs/<version>/api/scala/index.html

在下一节中,我将研究在 Spark 1.3 中引入的 Apache Spark DataFrames。

数据框

我已经提到 DataFrame 是基于列的格式。可以从中创建临时表,但我将在下一节中展开。数据框可用许多方法允许数据操作和处理。我基于上一节中使用的 Scala 代码,所以我只会展示工作行和输出。可以像这样显示数据框模式:

adultDataFrame.printSchema()

root
 |-- age: string (nullable = true)
 |-- workclass: string (nullable = true)
 |-- fnlwgt: string (nullable = true)
 |-- education: string (nullable = true)
 |-- educational-num: string (nullable = true)
 |-- marital-status: string (nullable = true)
 |-- occupation: string (nullable = true)
 |-- relationship: string (nullable = true)
 |-- race: string (nullable = true)
 |-- gender: string (nullable = true)
 |-- capital-gain: string (nullable = true)
 |-- capital-loss: string (nullable = true)
 |-- hours-per-week: string (nullable = true)
 |-- native-country: string (nullable = true)
 |-- income: string (nullable = true)

可以使用select方法从数据中过滤列。在这里,我在行数方面进行了限制,但你可以理解:

adultDataFrame.select("workclass","age","education","income").show()

workclass         age education     income
 Private          25   11th          <=50K
 Private          38   HS-grad       <=50K
 Local-gov        28   Assoc-acdm    >50K
 Private          44   Some-college  >50K
 none             18   Some-college  <=50K
 Private          34   10th          <=50K
 none             29   HS-grad       <=50K
 Self-emp-not-inc 63   Prof-school   >50K
 Private          24   Some-college  <=50K
 Private          55   7th-8th       <=50K

可以使用filter方法过滤从 DataFrame 返回的数据。在这里,我已经将职业列添加到输出中,并根据工人年龄进行了过滤:

 adultDataFrame
 .select("workclass","age","education","occupation","income")
 .filter( adultDataFrame("age") > 30 )
 .show()

workclass         age education     occupation         income
 Private          38   HS-grad       Farming-fishing    <=50K
 Private          44   Some-college  Machine-op-inspct  >50K
 Private          34   10th          Other-service      <=50K
 Self-emp-not-inc 63   Prof-school   Prof-specialty     >50K
 Private          55   7th-8th       Craft-repair       <=50K

还有一个group by方法用于确定数据集中的数量。由于这是一个基于收入的数据集,我认为工资范围内的数量会很有趣。我还使用了一个更大的数据集以获得更有意义的结果:

 adultDataFrame
 .groupBy("income")
 .count()
 .show()

income count
 <=50K 24720
 >50K  7841

这很有趣,但如果我想比较income档次和occupation,并对结果进行排序以更好地理解呢?以下示例显示了如何做到这一点,并给出了示例数据量。它显示与其他职业相比,管理角色的数量很大。此示例还通过职业列对输出进行了排序:

 adultDataFrame
 .groupBy("income","occupation")
 .count()
 .sort("occupation")
 .show()

income occupation         count
 >50K   Adm-clerical      507
 <=50K  Adm-clerical      3263
 <=50K  Armed-Forces      8
 >50K   Armed-Forces      1
 <=50K  Craft-repair      3170
 >50K   Craft-repair      929
 <=50K  Exec-managerial   2098
 >50K   Exec-managerial   1968
 <=50K  Farming-fishing   879
 >50K   Farming-fishing   115
 <=50K  Handlers-cleaners 1284
 >50K   Handlers-cleaners 86
 >50K   Machine-op-inspct 250
 <=50K  Machine-op-inspct 1752
 >50K   Other-service     137
 <=50K  Other-service     3158
 >50K   Priv-house-serv   1
 <=50K  Priv-house-serv   148
 >50K   Prof-specialty    1859
 <=50K  Prof-specialty    2281

因此,可以对数据框执行类似 SQL 的操作,包括selectfilter、排序group byprint。下一节将展示如何从数据框创建表,以及如何对其执行基于 SQL 的操作。

使用 SQL

在使用先前的 Scala 示例从 HDFS 上的基于 CSV 的数据输入文件创建数据框后,我现在可以定义一个临时表,基于数据框,并对其运行 SQL。以下示例显示了临时表adult的定义,并使用COUNT(*)创建了行数:

 adultDataFrame.registerTempTable("adult")

 val resRDD = sqlContext.sql("SELECT COUNT(*) FROM adult")

 resRDD.map(t => "Count - " + t(0)).collect().foreach(println)

这给出了超过 32,000 行的行数:

Count – 32561

还可以使用LIMIT SQL 选项限制从表中选择的数据量,如下例所示。已从数据中选择了前 10 行,如果我只想检查数据类型和质量,这是有用的:

 val resRDD = sqlContext.sql("SELECT * FROM adult LIMIT 10")

 resRDD.map(t => t(0)  + " " + t(1)  + " " + t(2)  + " " + t(3)  + " " +
 t(4)  + " " + t(5)  + " " + t(6)  + " " + t(7)  + " " +
 t(8)  + " " + t(9)  + " " + t(10) + " " + t(11) + " " +
 t(12) + " " + t(13) + " " + t(14)
 )
 .collect().foreach(println)

数据的一个样本如下:

50  Private  283676  Some-college  10  Married-civ-spouse  Craft-repair  Husband  White  Male  0  0  40  United-States  >50K

当在上一节的基于 Scala 的数据框示例中创建此数据的模式时,所有列都被创建为字符串。但是,如果我想在 SQL 中使用WHERE子句过滤数据,那么拥有正确的数据类型将是有用的。例如,如果年龄列存储整数值,那么它应该存储为整数,以便我可以对其执行数值比较。我已经更改了我的 Scala 代码,以包括所有可能的类型:

import org.apache.spark.sql.types._

我现在也已经使用不同的类型定义了我的模式,以更好地匹配数据,并且已经根据实际数据类型定义了行数据,将原始数据字符串值转换为整数值:

 val schema =
 StructType(
 StructField("age",                IntegerType, false) ::
 StructField("workclass",          StringType,  false) ::
 StructField("fnlwgt",             IntegerType, false) ::
 StructField("education",          StringType,  false) ::
 StructField("educational-num",    IntegerType, false) ::
 StructField("marital-status",     StringType,  false) ::
 StructField("occupation",         StringType,  false) ::
 StructField("relationship",       StringType,  false) ::
 StructField("race",               StringType,  false) ::
 StructField("gender",             StringType,  false) ::
 StructField("capital-gain",       IntegerType, false) ::
 StructField("capital-loss",       IntegerType, false) ::
 StructField("hours-per-week",     IntegerType, false) ::
 StructField("native-country",     StringType,  false) ::
 StructField("income",             StringType,  false) ::
 Nil)

 val rowRDD = rawRdd.map(_.split(","))
 .map(p => Row( p(0).trim.toInt,p(1),p(2).trim.toInt,p(3),
 p(4).trim.toInt,p(5),p(6),p(7),p(8),
 p(9),p(10).trim.toInt,p(11).trim.toInt,
 p(12).trim.toInt,p(13),p(14) ))

SQL 现在可以正确地在WHERE子句中使用数值过滤器。如果age列是字符串,这将无法工作。现在您可以看到数据已被过滤以给出 60 岁以下的年龄值:

 val resRDD = sqlContext.sql("SELECT COUNT(*) FROM adult WHERE age < 60")
 resRDD.map(t => "Count - " + t(0)).collect().foreach(println)

这给出了大约 30,000 行的行数:

Count – 29917

可以在基于WHERE的过滤子句中使用布尔逻辑。以下示例指定了数据的年龄范围。请注意,我已经使用变量来描述 SQL 语句的selectfilter组件。这使我能够将语句分解为不同的部分,因为它们变得更大:

 val selectClause = "SELECT COUNT(*) FROM adult "
 val filterClause = "WHERE age > 25 AND age < 60"
 val resRDD = sqlContext.sql( selectClause + filterClause )
 resRDD.map(t => "Count - " + t(0)).collect().foreach(println)

给出了约 23,000 行的数据计数:

Count – 23506

我可以使用布尔术语(如ANDOR)以及括号创建复合过滤子句:

 val selectClause = "SELECT COUNT(*) FROM adult "
 val filterClause =
 "WHERE ( age > 15 AND age < 25 ) OR ( age > 30 AND age < 45 ) "

 val resRDD = sqlContext.sql( selectClause + filterClause )
 resRDD.map(t => "Count - " + t(0)).collect().foreach(println)

这给我一个约 17,000 行的行数,并表示数据中两个年龄范围的计数:

Count – 17198

在 Apache Spark SQL 中也可以使用子查询。您可以在以下示例中看到,我通过从表adult中选择三列ageeducationoccupation来创建了一个名为t1的子查询。然后我使用名为t1的表创建了一个行数。我还在表t1的年龄列上添加了一个过滤子句。还要注意,我已经添加了group byorder by子句,尽管它们目前是空的,到我的 SQL 中:

 val selectClause = "SELECT COUNT(*) FROM "
 val tableClause = " ( SELECT age,education,occupation from adult) t1 "
 val filterClause = "WHERE ( t1.age > 25 ) "
 val groupClause = ""
 val orderClause = ""

 val resRDD = sqlContext.sql( selectClause + tableClause +
 filterClause +
 groupClause + orderClause
 )

 resRDD.map(t => "Count - " + t(0)).collect().foreach(println)

为了检查表连接,我创建了一个名为adult.train.data2的成人 CSV 数据文件的版本,它与原始文件的唯一区别是添加了一个名为idx的第一列,这是一个唯一索引。Hadoop 文件系统的cat命令在这里显示了数据的一个样本。使用 Linux 的head命令限制了文件的输出:

[hadoop@hc2nn sql]$ hdfs dfs -cat /data/spark/sql/adult.train.data2 | head -2

1,39, State-gov, 77516, Bachelors, 13, Never-married, Adm-clerical, Not-in-family, White, Male, 2174, 0, 40, United-States, <=50K
2,50, Self-emp-not-inc, 83311, Bachelors, 13, Married-civ-spouse, Exec-managerial, Husband, White, Male, 0, 0, 13, United-States, <=50K

模式现在已重新定义,具有整数类型的第一列idx作为索引,如下所示:

 val schema =
 StructType(
 StructField("idx",                IntegerType, false) ::
 StructField("age",                IntegerType, false) ::
 StructField("workclass",          StringType,  false) ::
 StructField("fnlwgt",             IntegerType, false) ::
 StructField("education",          StringType,  false) ::
 StructField("educational-num",    IntegerType, false) ::
 StructField("marital-status",     StringType,  false) ::
 StructField("occupation",         StringType,  false) ::
 StructField("relationship",       StringType,  false) ::
 StructField("race",               StringType,  false) ::
 StructField("gender",             StringType,  false) ::
 StructField("capital-gain",       IntegerType, false) ::
 StructField("capital-loss",       IntegerType, false) ::
 StructField("hours-per-week",     IntegerType, false) ::
 StructField("native-country",     StringType,  false) ::
 StructField("income",             StringType,  false) ::
 Nil)

在 Scala 示例中的原始行 RDD 现在处理了新的初始列,并将字符串值转换为整数:

 val rowRDD = rawRdd.map(_.split(","))
 .map(p => Row( p(0).trim.toInt,
 p(1).trim.toInt,
 p(2),
 p(3).trim.toInt,
 p(4),
 p(5).trim.toInt,
 p(6),
 p(7),
 p(8),
 p(9),
 p(10),
 p(11).trim.toInt,
 p(12).trim.toInt,
 p(13).trim.toInt,
 p(14),
 p(15)
 ))

 val adultDataFrame = sqlContext.createDataFrame(rowRDD, schema)

我们已经看过子查询。现在,我想考虑表连接。下一个示例将使用刚刚创建的索引。它使用它来连接两个派生表。这个示例有点牵强,因为它连接了来自相同基础表的两个数据集,但你明白我的意思。两个派生表被创建为子查询,并在一个公共索引列上连接。

现在,表连接的 SQL 如下。从临时表adult创建了两个派生表,分别称为t1t2作为子查询。新的行索引列称为idx已被用来连接表t1t2中的数据。主要的SELECT语句从复合数据集中输出所有七列。我添加了一个LIMIT子句来最小化数据输出:

 val selectClause = "SELECT t1.idx,age,education,occupation,workclass,race,gender FROM "
 val tableClause1 = " ( SELECT idx,age,education,occupation FROM adult) t1 JOIN "
 val tableClause2 = " ( SELECT idx,workclass,race,gender FROM adult) t2 "
 val joinClause = " ON (t1.idx=t2.idx) "
 val limitClause = " LIMIT 10"

 val resRDD = sqlContext.sql( selectClause +
 tableClause1 + tableClause2 +
 joinClause   + limitClause
 )

 resRDD.map(t => t(0) + " " + t(1) + " " + t(2) + " " +
 t(3) + " " + t(4) + " " + t(5) + " " + t(6)
 )
 .collect().foreach(println)

请注意,在主要的SELECT语句中,我必须定义索引列来自哪里,所以我使用了t1.idx。所有其他列都是唯一的t1t2数据集,所以我不需要使用别名来引用它们(即t1.age)。因此,现在输出的数据如下:

33 45  Bachelors  Exec-managerial  Private  White  Male
233 25  Some-college  Adm-clerical  Private  White  Male
433 40  Bachelors  Prof-specialty  Self-emp-not-inc  White  Female
633 43  Some-college  Craft-repair  Private  White  Male
833 26  Some-college  Handlers-cleaners  Private  White  Male
1033 27  Some-college  Sales  Private  White  Male
1233 27  Bachelors  Adm-clerical  Private  White  Female
1433 32  Assoc-voc  Sales  Private  White  Male
1633 40  Assoc-acdm  Adm-clerical  State-gov  White  Male
1833 46  Some-college  Prof-specialty  Local-gov  White  Male

这给出了 Apache Spark 中基于 SQL 的功能的一些想法,但如果我发现需要的方法不可用怎么办?也许我需要一个新函数。这就是用户定义的函数UDFs)有用的地方。我将在下一节中介绍它们。

用户定义的函数

为了在 Scala 中创建一些用户定义的函数,我需要检查之前的成年人数据集中的数据。我计划创建一个 UDF,用于枚举教育列,以便我可以将列转换为整数值。如果我需要将数据用于机器学习,并创建一个 LabelPoint 结构,这将非常有用。所使用的向量,代表每条记录,需要是数值型的。我将首先确定存在哪种唯一的教育值,然后创建一个函数来枚举它们,最后在 SQL 中使用它。

我已经创建了一些 Scala 代码来显示教育值的排序列表。DISTINCT关键字确保每个值只有一个实例。我已经选择数据作为子表,使用一个名为edu_dist的别名来确保ORDER BY子句起作用:

 val selectClause = "SELECT t1.edu_dist FROM "
 val tableClause  = " ( SELECT DISTINCT education AS edu_dist FROM adult ) t1 "
 val orderClause  = " ORDER BY t1.edu_dist "

 val resRDD = sqlContext.sql( selectClause + tableClause  + orderClause )

 resRDD.map(t => t(0)).collect().foreach(println)

数据如下。我已经删除了一些值以节省空间,但你明白我的意思:

 10th
 11th
 12th
 1st-4th
 ………..
 Preschool
 Prof-school
 Some-college

我在 Scala 中定义了一个方法,接受基于字符串的教育值,并返回代表它的枚举整数值。如果没有识别到值,则返回一个名为9999的特殊值:

 def enumEdu( education:String ) : Int =
 {
 var enumval = 9999

 if ( education == "10th" )         { enumval = 0 }
 else if ( education == "11th" )         { enumval = 1 }
 else if ( education == "12th" )         { enumval = 2 }
 else if ( education == "1st-4th" )      { enumval = 3 }
 else if ( education == "5th-6th" )      { enumval = 4 }
 else if ( education == "7th-8th" )      { enumval = 5 }
 else if ( education == "9th" )          { enumval = 6 }
 else if ( education == "Assoc-acdm" )   { enumval = 7 }
 else if ( education == "Assoc-voc" )    { enumval = 8 }
 else if ( education == "Bachelors" )    { enumval = 9 }
 else if ( education == "Doctorate" )    { enumval = 10 }
 else if ( education == "HS-grad" )      { enumval = 11 }
 else if ( education == "Masters" )      { enumval = 12 }
 else if ( education == "Preschool" )    { enumval = 13 }
 else if ( education == "Prof-school" )  { enumval = 14 }
 else if ( education == "Some-college" ) { enumval = 15 }

 return enumval
 }

现在,我可以使用 Scala 中的 SQL 上下文注册此函数,以便在 SQL 语句中使用:

 sqlContext.udf.register( "enumEdu", enumEdu _ )

然后,SQL 和 Scala 代码用于枚举数据如下。新注册的名为enumEdu的函数在SELECT语句中使用。它以教育类型作为参数,并返回整数枚举。此值形成的列被别名为idx

 val selectClause = "SELECT enumEdu(t1.edu_dist) as idx,t1.edu_dist FROM "
 val tableClause  = " ( SELECT DISTINCT education AS edu_dist FROM adult ) t1 "
 val orderClause  = " ORDER BY t1.edu_dist "

 val resRDD = sqlContext.sql( selectClause + tableClause  + orderClause )

 resRDD.map(t => t(0) + " " + t(1) ).collect().foreach(println)

结果数据输出,作为教育值及其枚举的列表,如下所示:

0  10th
1  11th
2  12th
3  1st-4th
4  5th-6th
5  7th-8th
6  9th
7  Assoc-acdm
8  Assoc-voc
9  Bachelors
10  Doctorate
11  HS-grad
12  Masters
13  Preschool
14  Prof-school
15  Some-college

另一个示例函数名为ageBracket,它接受成年人的整数年龄值,并返回一个枚举的年龄段:

 def ageBracket( age:Int ) : Int =
 {
 var bracket = 9999

 if ( age >= 0  && age < 20  ) { bracket = 0 }
 else if ( age >= 20 && age < 40  ) { bracket = 1 }
 else if ( age >= 40 && age < 60  ) { bracket = 2 }
 else if ( age >= 60 && age < 80  ) { bracket = 3 }
 else if ( age >= 80 && age < 100 ) { bracket = 4 }
 else if ( age > 100 )              { bracket = 5 }

 return bracket
 }

再次,使用 SQL 上下文注册函数,以便在 SQL 语句中使用:

 sqlContext.udf.register( "ageBracket", ageBracket _ )

然后,基于 Scala 的 SQL 使用它从成年人数据集中选择年龄、年龄段和教育值:

 val selectClause = "SELECT age, ageBracket(age) as bracket,education FROM "
 val tableClause  = " adult "
 val limitClause  = " LIMIT 10 "

 val resRDD = sqlContext.sql( selectClause + tableClause  +
 limitClause )

 resRDD.map(t => t(0) + " " + t(1) + " " + t(2) ).collect().foreach(println)

然后,由于我使用了LIMIT子句将输出限制为 10 行,因此生成的数据如下:

39 1  Bachelors
50 2  Bachelors
38 1  HS-grad
53 2  11th
28 1  Bachelors
37 1  Masters
49 2  9th
52 2  HS-grad
31 1  Masters
42 2  Bachelors

还可以在 SQL 中定义函数,通过 SQL 上下文在 UDF 注册期间内联使用。以下示例定义了一个名为dblAge的函数,它只是将成年人的年龄乘以二。注册如下。它接受整数参数(age),并返回两倍的值:

 sqlContext.udf.register( "dblAge", (a:Int) => 2*a )

并且使用它的 SQL 现在选择ageage值的两倍,称为dblAge(age)

 val selectClause = "SELECT age,dblAge(age) FROM "
 val tableClause  = " adult "
 val limitClause  = " LIMIT 10 "

 val resRDD = sqlContext.sql( selectClause + tableClause  + limitClause )

 resRDD.map(t => t(0) + " " + t(1) ).collect().foreach(println)

现在,输出数据的两列包含年龄及其加倍值,看起来是这样的:

39 78
50 100
38 76
53 106
28 56
37 74
49 98
52 104
31 62
42 84

到目前为止,已经检查了 DataFrame、SQL 和用户定义函数,但是如果像我一样使用 Hadoop 堆栈集群,并且有 Apache Hive 可用,会怎么样呢?到目前为止我定义的adult表是一个临时表,但是如果我使用 Apache Spark SQL 访问 Hive,我可以访问静态数据库表。下一节将检查执行此操作所需的步骤。

使用 Hive

如果您有低延迟要求和多用户的商业智能类型工作负载,那么您可能考虑使用 Impala 来访问数据库。Apache Spark 在 Hive 上用于批处理和 ETL 链。本节将用于展示如何连接 Spark 到 Hive,以及如何使用此配置。首先,我将开发一个使用本地 Hive 元数据存储的应用程序,并展示它不会在 Hive 本身存储和持久化表数据。然后,我将设置 Apache Spark 连接到 Hive 元数据服务器,并在 Hive 中存储表和数据。我将从本地元数据服务器开始。

本地 Hive 元数据服务器

以下示例 Scala 代码显示了如何使用 Apache Spark 创建 Hive 上下文,并创建基于 Hive 的表。首先导入了 Spark 配置、上下文、SQL 和 Hive 类。然后,定义了一个名为hive_ex1的对象类和主方法。定义了应用程序名称,并创建了一个 Spark 配置对象。然后从配置对象创建了 Spark 上下文:

import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.sql._
import org.apache.spark.sql.hive.HiveContext

object hive_ex1 {

 def main(args: Array[String]) {

 val appName = "Hive Spark Ex 1"
 val conf    = new SparkConf()

 conf.setAppName(appName)

 val sc = new SparkContext(conf)

接下来,我从 Spark 上下文中创建一个新的 Hive 上下文,并导入 Hive implicits 和 Hive 上下文 SQL。implicits允许进行隐式转换,而 SQL 包含允许我运行基于 Hive 上下文的 SQL:

 val hiveContext = new HiveContext(sc)

 import hiveContext.implicits._
 import hiveContext.sql

下一个语句在 Hive 中创建了一个名为adult2的空表。您将会在本章中已经使用过的 adult 数据中识别出模式:

 hiveContext.sql( " 
 CREATE TABLE IF NOT EXISTS adult2
 (
 idx             INT,
 age             INT,
 workclass       STRING,
 fnlwgt          INT,
 education       STRING,
 educationnum    INT,
 maritalstatus   STRING,
 occupation      STRING,
 relationship    STRING,
 race            STRING,
 gender          STRING,
 capitalgain     INT,
 capitalloss     INT,
 nativecountry   STRING,
 income          STRING
 )

 ")

接下来,通过COUNT(*)从名为adult2的表中获取行计数,并打印输出值:

 val resRDD = hiveContext.sql("SELECT COUNT(*) FROM adult2")

 resRDD.map(t => "Count : " + t(0) ).collect().foreach(println)

如预期的那样,表中没有行。

Count : 0

在 Apache Spark Hive 中也可以创建基于 Hive 的外部表。以下的 HDFS 文件列表显示了名为adult.train.data2的 CSV 文件存在于名为/data/spark/hive的 HDFS 目录中,并且包含数据:

[hadoop@hc2nn hive]$ hdfs dfs -ls /data/spark/hive
Found 1 items
-rw-r--r--   3 hadoop supergroup    4171350 2015-06-24 15:18 /data/spark/hive/adult.train.data2

现在,我调整我的基于 Scala 的 Hive SQL 以创建一个名为adult3的外部表(如果不存在),该表与先前表具有相同的结构。在此表创建语句中的行格式指定逗号作为行列分隔符,这是 CSV 数据所期望的。此语句中的位置选项指定了 HDFS 上的/data/spark/hive目录作为数据的位置。因此,在此位置上可以有多个文件在 HDFS 上,用于填充此表。每个文件都需要具有与此表结构匹配的相同数据结构:

 hiveContext.sql("

 CREATE EXTERNAL TABLE IF NOT EXISTS adult3
 (
 idx             INT,
 age             INT,
 workclass       STRING,
 fnlwgt          INT,
 education       STRING,
 educationnum    INT,
 maritalstatus   STRING,
 occupation      STRING,
 relationship    STRING,
 race            STRING,
 gender          STRING,
 capitalgain     INT,
 capitalloss     INT,
 nativecountry   STRING,
 income          STRING
 )
 ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
 LOCATION '/data/spark/hive'

 ")

然后对adult3表进行行计数,并打印计数结果:

 val resRDD = hiveContext.sql("SELECT COUNT(*) FROM adult3")

 resRDD.map(t => "Count : " + t(0) ).collect().foreach(println)

如您所见,表现在包含大约 32,000 行。由于这是一个外部表,基于 HDFS 的数据并没有被移动,行计算是从底层基于 CSV 的数据中推导出来的。

Count : 32561

我意识到我想要从外部的adult3表中剥离维度数据。毕竟,Hive 是一个数据仓库,因此在使用基于原始 CSV 数据的一般 ETL 链的一部分时,会从数据中剥离维度和对象,并创建新的表。如果考虑教育维度,并尝试确定存在哪些唯一值,那么例如,SQL 将如下所示:

 val resRDD = hiveContext.sql("

 SELECT DISTINCT education AS edu FROM adult3
 ORDER BY edu

 ")

 resRDD.map(t => t(0) ).collect().foreach(println)

有序数据与本章早期使用 Spark SQL 推导出的值匹配:

 10th
 11th
 12th
 1st-4th
 5th-6th
 7th-8th
 9th
 Assoc-acdm
 Assoc-voc
 Bachelors
 Doctorate
 HS-grad
 Masters
 Preschool
 Prof-school
 Some-college

这很有用,但如果我想创建维度值,然后为以前的教育维度值分配整数索引值怎么办。例如,10th将是011th将是1。我已经在 HDFS 上为教育维度设置了一个维度 CSV 文件,如下所示。内容只包含唯一值的列表和一个索引:

[hadoop@hc2nn hive]$ hdfs dfs -ls /data/spark/dim1/
Found 1 items
-rw-r--r--   3 hadoop supergroup        174 2015-06-25 14:08 /data/spark/dim1/education.csv
[hadoop@hc2nn hive]$ hdfs dfs -cat /data/spark/dim1/education.csv
1,10th
2,11th
3,12th

现在,我可以在我的 Apache 应用程序中运行一些 Hive QL 来创建一个教育维度表。首先,如果教育表已经存在,我会删除它,然后通过解析 HDFS CSV 文件来创建表:

 hiveContext.sql("  DROP TABLE IF EXISTS education ")
 hiveContext.sql("

 CREATE TABLE IF NOT EXISTS  education
 (
 idx        INT,
 name       STRING
 )
 ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
 LOCATION '/data/spark/dim1/'
 ")

然后我可以选择新的教育表的内容,以确保它看起来是正确的。

val resRDD = hiveContext.sql(" SELECT * FROM education ")
resRDD.map( t => t(0)+" "+t(1) ).collect().foreach(println)

这给出了预期的索引列表和教育维度值:

1 10th
2 11th
3 12th
………
16 Some-college

因此,我已经开始了 ETL 管道的开端。原始 CSV 数据被用作外部表,然后创建了维度表,然后可以用来将原始数据中的维度转换为数字索引。我现在已经成功创建了一个 Spark 应用程序,它使用 Hive 上下文连接到 Hive Metastore 服务器,这使我能够创建和填充表。

我在我的 Linux 服务器上安装了 Hadoop 堆栈 Cloudera CDH 5.3。我正在写这本书时使用它来访问 HDFS,并且我还安装并运行了 Hive 和 Hue(CDH 安装信息可以在 Cloudera 网站cloudera.com/content/cloudera/en/documentation.html找到)。当我检查 HDFS 中的adult3表时,它应该已经创建在/user/hive/warehouse下,我看到了以下内容:

[hadoop@hc2nn hive]$ hdfs dfs -ls /user/hive/warehouse/adult3
ls: `/user/hive/warehouse/adult3': No such file or directory

基于 Hive 的表并不存在于 Hive 的预期位置。我可以通过检查 Hue Metastore 管理器来确认这一点,以查看默认数据库中存在哪些表。以下图表显示了我的默认数据库目前是空的。我已经添加了红线,以表明我目前正在查看默认数据库,并且没有数据。显然,当我运行基于 Apache Spark 的应用程序时,使用 Hive 上下文,我是连接到 Hive Metastore 服务器的。我知道这是因为日志表明了这一点,而且以这种方式创建的表在重新启动 Apache Spark 时会持久存在。

本地 Hive Metastore 服务器

刚刚运行的应用程序中的 Hive 上下文已经使用了本地 Hive Metastore 服务器,并将数据存储在本地位置;实际上,在这种情况下是在 HDFS 上的/tmp下。我现在想要使用基于 Hive 的 Metastore 服务器,这样我就可以直接在 Hive 中创建表和数据。接下来的部分将展示如何实现这一点。

基于 Hive 的 Metastore 服务器

我已经提到我正在使用 Cloudera 的 CDH 5.3 Hadoop 堆栈。我正在运行 Hive、HDFS、Hue 和 Zookeeper。我正在使用安装在/usr/local/spark下的 Apache Spark 1.3.1,以便创建和运行应用程序(我知道 CDH 5.3 发布了 Spark 1.2,但我想在这种情况下使用 Spark 1.3.x 中可用的 DataFrames)。

配置 Apache Spark 连接到 Hive 的第一件事是将名为hive-site.xml的 Hive 配置文件放入所有安装了 Spark 的服务器上的 Spark 配置目录中:

[hadoop@hc2nn bin]# cp /var/run/cloudera-scm-agent/process/1237-hive-HIVEMETASTORE/hive-site.xml /usr/local/spark/conf

然后,鉴于我已经通过 CDH Manager 安装了 Apache Hive 以便使用 PostgreSQL,我需要为 Spark 安装一个 PostgreSQL 连接器 JAR,否则它将不知道如何连接到 Hive,并且会出现类似这样的错误:

15/06/25 16:32:24 WARN DataNucleus.Connection: BoneCP specified but not present in CLASSPATH (s)
Caused by: java.lang.RuntimeException: Unable to instantiate org.apache.hadoop.hive.metastore.
Caused by: java.lang.reflect.InvocationTargetException
Caused by: javax.jdo.JDOFatalInternalException: Error creating transactional connection factor
Caused by: org.datanucleus.exceptions.NucleusException: Attempt to invoke the "dbcp-builtin" pnectionPool gave an 
error : The specified datastore driver ("org.postgresql.Driver") was not f. Please check your CLASSPATH
specification, and the name of the driver.
Caused by: org.datanucleus.store.rdbms.connectionpool.DatastoreDriverNotFoundException: The spver
("org.postgresql.Driver") was not found in the CLASSPATH. Please check your CLASSPATH specme of the driver.

我已经将错误消息简化为只包含相关部分,否则它将非常长。我已经确定了我安装的 PostgreSQL 的版本,如下所示。从 Cloudera 基于包的 jar 文件中确定为 9.0 版本:

[root@hc2nn jars]# pwd ; ls postgresql*
/opt/cloudera/parcels/CDH/jars
postgresql-9.0-801.jdbc4.jar

接下来,我使用jdbc.postgresql.org/网站下载必要的 PostgreSQL 连接器库。我已确定我的 Java 版本为 1.7,如下所示,这会影响要使用的库的版本:

[hadoop@hc2nn spark]$ java -version
java version "1.7.0_75"
OpenJDK Runtime Environment (rhel-2.5.4.0.el6_6-x86_64 u75-b13)
OpenJDK 64-Bit Server VM (build 24.75-b04, mixed mode)

该网站表示,如果您使用的是 Java 1.7 或 1.8,则应该使用该库的 JDBC41 版本。因此,我已经获取了postgresql-9.4-1201.jdbc41.jar文件。下一步是将此文件复制到 Apache Spark 安装的lib目录中,如下所示:

[hadoop@hc2nn lib]$ pwd ; ls -l postgresql*
/usr/local/spark/lib
-rw-r--r-- 1 hadoop hadoop 648487 Jun 26 13:20 postgresql-9.4-1201.jdbc41.jar

现在,必须将 PostgreSQL 库添加到 Spark 的CLASSPATH中,方法是在 Spark 的bin目录中的名为compute-classpath.sh的文件中添加一个条目,如下所示:

[hadoop@hc2nn bin]$ pwd ; tail compute-classpath.sh
/usr/local/spark/bin

# add postgresql connector to classpath
appendToClasspath "${assembly_folder}"/postgresql-9.4-1201.jdbc41.jar

echo "$CLASSPATH"

在我的情况下,我遇到了有关 CDH 5.3 Hive 和 Apache Spark 之间的 Hive 版本错误,如下所示。我认为版本如此接近,以至于我应该能够忽略这个错误:

Caused by: MetaException(message:Hive Schema version 0.13.1aa does not match metastore's schema version 0.13.0

Metastore is not upgraded or corrupt)

在这种情况下,我决定在我的 Spark 版本的hive-site.xml文件中关闭模式验证。这必须在该文件的所有基于 Spark 的实例中完成,然后重新启动 Spark。更改如下所示;值设置为false

 <property>
 <name>hive.metastore.schema.verification</name>
 <value>false</value>
 </property>

现在,当我运行与上一节相同的基于应用程序的 SQL 集时,我可以在 Apache Hive 默认数据库中创建对象。首先,我将使用基于 Spark 的 Hive 上下文创建名为adult2的空表:

 hiveContext.sql( "

 CREATE TABLE IF NOT EXISTS adult2
 (
 idx             INT,
 age             INT,
 workclass       STRING,
 fnlwgt          INT,
 education       STRING,
 educationnum    INT,
 maritalstatus   STRING,
 occupation      STRING,
 relationship    STRING,
 race            STRING,
 gender          STRING,
 capitalgain     INT,
 capitalloss     INT,
 nativecountry   STRING,
 income          STRING
 )

 ")

如您所见,当我运行应用程序并检查 Hue 元数据浏览器时,表adult2现在已经存在:

基于 Hive 的元数据服务器

我之前展示了表条目,并通过选择称为adult2的表条目在 Hue 默认数据库浏览器中获得了其结构:

基于 Hive 的元数据服务器

现在,可以执行基于 Spark 的 Hive QL 的外部表adult3,并从 Hue 确认数据访问。在最后一节中,必要的 Hive QL 如下:

 hiveContext.sql("

 CREATE EXTERNAL TABLE IF NOT EXISTS adult3
 (
 idx             INT,
 age             INT,
 workclass       STRING,
 fnlwgt          INT,
 education       STRING,
 educationnum    INT,
 maritalstatus   STRING,
 occupation      STRING,
 relationship    STRING,
 race            STRING,
 gender          STRING,
 capitalgain     INT,
 capitalloss     INT,
 nativecountry   STRING,
 income          STRING
 )
 ROW FORMAT DELIMITED FIELDS TERMINATED BY ','
 LOCATION '/data/spark/hive'

 ")

现在您可以看到,基于 Hive 的名为adult3的表已经由 Spark 创建在默认数据库中。下图再次生成自 Hue 元数据浏览器:

基于 Hive 的元数据服务器

以下 Hive QL 已从 Hue Hive 查询编辑器执行。它显示adult3表可以从 Hive 访问。我限制了行数以使图像可呈现。我不担心数据,只关心我能否访问它:

基于 Hive 的元数据服务器

我将在本节中提到的最后一件事对于在 Spark 中使用 Hive QL 对 Hive 进行操作将非常有用,那就是用户定义的函数或 UDF。例如,我将考虑row_sequence函数,该函数在以下基于 Scala 的代码中使用:

hiveContext.sql("

ADD JAR /opt/cloudera/parcels/CDH-5.3.3-1.cdh5.3.3.p0.5/jars/hive-contrib-0.13.1-cdh5.3.3.jar

 ")

hiveContext.sql("

CREATE TEMPORARY FUNCTION row_sequence as 'org.apache.hadoop.hive.contrib.udf.UDFRowSequence';

 ")

 val resRDD = hiveContext.sql("

 SELECT row_sequence(),t1.edu FROM
 ( SELECT DISTINCT education AS edu FROM adult3 ) t1
 ORDER BY t1.edu

 ")

通过ADD JAR命令可以将现有的或您自己的基于 JAR 的库添加到 Spark Hive 会话中。然后,可以使用基于包的类名将该库中的功能注册为临时函数,并在 Hive QL 语句中将新函数名称合并。

本章已成功将基于 Apache Spark 的应用程序连接到 Hive,并对 Hive 运行 Hive QL,以便表和数据更改在 Hive 中持久存在。但为什么这很重要呢?嗯,Spark 是一种内存并行处理系统。它的处理速度比基于 Hadoop 的 Map Reduce 快一个数量级。Apache Spark 现在可以作为处理引擎使用,而 Hive 数据仓库可以用于存储。快速的基于内存的 Spark 处理速度与 Hive 中可用的大数据规模结构化数据仓库存储相结合。

总结

本章开始时解释了 Spark SQL 上下文和文件 I/O 方法。然后展示了可以操作基于 Spark 和 HDFS 的数据,既可以使用类似 SQL 的方法和 DataFrames,也可以通过注册临时表和 Spark SQL。接下来,介绍了用户定义的函数,以展示 Spark SQL 的功能可以通过创建新函数来扩展以满足您的需求,将它们注册为 UDF,然后在 SQL 中调用它们来处理数据。

最后,Hive 上下文被引入用于在 Apache Spark 中使用。请记住,Spark 中的 Hive 上下文提供了 SQL 上下文功能的超集。我知道随着时间的推移,SQL 上下文将被扩展以匹配 Hive 上下文的功能。在 Spark 中使用 Hive 上下文进行 Hive QL 数据处理时,使用了本地 Hive 和基于 Hive 的 Metastore 服务器。我认为后者的配置更好,因为表被创建,数据更改会持久保存在您的 Hive 实例中。

在我的案例中,我使用的是 Cloudera CDH 5.3,其中使用了 Hive 0.13、PostgreSQL、ZooKeeper 和 Hue。我还使用了 Apache Spark 版本 1.3.1。我向您展示的配置设置纯粹是针对这个配置的。如果您想使用 MySQL,例如,您需要研究必要的更改。一个好的起点可能是<user@spark.apache.org>邮件列表。

最后,我想说 Apache Spark Hive 上下文配置,使用基于 Hive 的存储,非常有用。它允许您将 Hive 用作大数据规模的数据仓库,使用 Apache Spark 进行快速的内存处理。它不仅提供了使用基于 Spark 的模块(MLlib、SQL、GraphX 和 Stream)操纵数据的能力,还提供了使用其他基于 Hadoop 的工具,使得创建 ETL 链更加容易。

下一章将研究 Spark 图处理模块 GraphX,还将调查 Neo4J 图数据库和 MazeRunner 应用程序。

第五章:Apache Spark GraphX

在本章中,我想要研究 Apache Spark GraphX 模块和图处理。我还想简要介绍一下图数据库 Neo4j。因此,本章将涵盖以下主题:

  • GraphX 编码

  • Neo4j 的 Mazerunner

GraphX 编码部分使用 Scala 编写,将提供一系列图编码示例。Kenny Bastani 在实验性产品 Mazerunner 上的工作,我也将进行审查,将这两个主题结合在一个实际示例中。它提供了一个基于 Docker 的示例原型,用于在 Apache Spark GraphX 和 Neo4j 存储之间复制数据。

在 Scala 中编写代码使用 Spark GraphX 模块之前,我认为提供一个关于图处理实际上是什么的概述会很有用。下一节将使用一些简单的图示例进行简要介绍。

概览

图可以被视为一种数据结构,它由一组顶点和连接它们的边组成。图中的顶点或节点可以是对象,也可以是人,而边缘则是它们之间的关系。边缘可以是有方向的,意味着关系是从一个节点到下一个节点的。例如,节点 A 是节点 B 的父亲。

在下面的图表中,圆圈代表顶点或节点(AD),而粗线代表边缘或它们之间的关系(E1E6)。每个节点或边缘可能具有属性,这些值由相关的灰色方块(P1P7)表示。

因此,如果一个图代表了寻路的实际路线图,那么边缘可能代表次要道路或高速公路。节点将是高速公路交叉口或道路交叉口。节点和边缘的属性可能是道路类型、速度限制、距离、成本和网格位置。

有许多类型的图实现,但一些例子包括欺诈建模、金融货币交易建模、社交建模(如 Facebook 上的朋友关系)、地图处理、网络处理和页面排名。

概览

前面的图表显示了一个带有相关属性的图的通用示例。它还显示了边缘关系可以是有方向的,也就是说,E2边缘是从节点B到节点C的。然而,下面的例子使用了家庭成员及其之间的关系来创建一个图。请注意,两个节点或顶点之间可以有多条边。例如,MikeSarah之间的夫妻关系。此外,一个节点或边上可能有多个属性。

概览

因此,在前面的例子中,Sister属性是从节点 6 Flo到节点 1 Mike的。这些都是简单的图,用来解释图的结构和元素性质。真实的图应用可能会达到极大的规模,并且需要分布式处理和存储来使它们能够被操作。Facebook 能够处理包含超过 1 万亿边的图,使用Apache Giraph(来源:Avery Ching-Facebook)。Giraph 是用于图处理的 Apache Hadoop 生态系统工具,它在历史上基于 Map Reduce 进行处理,但现在使用 TinkerPop,这将在第六章中介绍,基于图的存储。尽管本书集中讨论 Apache Spark,但边的数量提供了一个非常令人印象深刻的指标,显示了图可以达到的规模。

在下一节中,我将使用 Scala 来研究 Apache Spark GraphX 模块的使用。

GraphX 编码

本节将使用上一节中展示的家庭关系图数据样本,使用 Scala 中的 Apache Spark GraphX 编程来进行分析。这些数据将存储在 HDFS 上,并将作为顶点和边的列表进行访问。尽管这个数据集很小,但是用这种方式构建的图可能非常大。我使用 HDFS 进行存储,因为如果你的图扩展到大数据规模,那么你将需要某种类型的分布式和冗余存储。正如本章所示的例子,这可能是 HDFS。使用 Apache Spark SQL 模块,存储也可以是 Apache Hive;详情请参见第四章,“Apache Spark SQL”。

环境

我使用了服务器hc2nn上的 hadoop Linux 账户来开发基于 Scala 的 GraphX 代码。SBT 编译的结构遵循与之前示例相同的模式,代码树存在于名为graphx的子目录中,其中有一个名为graph.sbt的 SBT 配置文件:

[hadoop@hc2nn graphx]$ pwd
/home/hadoop/spark/graphx

[hadoop@hc2nn graphx]$ ls
 src   graph.sbt          project     target

源代码如预期的那样位于此级别的子树下,名为src/main/scala,包含五个代码示例:

[hadoop@hc2nn scala]$ pwd
/home/hadoop/spark/graphx/src/main/scala

[hadoop@hc2nn scala]$ ls
graph1.scala  graph2.scala  graph3.scala  graph4.scala  graph5.scala

在每个基于图的示例中,Scala 文件使用相同的代码从 HDFS 加载数据,并创建图;但是,每个文件提供了基于 GraphX 的图处理的不同方面。由于本章使用了不同的 Spark 模块,sbt配置文件graph.sbt已经更改以支持这项工作:

[hadoop@hc2nn graphx]$ more graph.sbt

name := "Graph X"

version := "1.0"

scalaVersion := "2.10.4"

libraryDependencies += "org.apache.hadoop" % "hadoop-client" % "2.3.0"

libraryDependencies += "org.apache.spark" %% "spark-core"  % "1.0.0"

libraryDependencies += "org.apache.spark" %% "spark-graphx" % "1.0.0"

// If using CDH, also add Cloudera repo
resolvers += "Cloudera Repository" at https://repository.cloudera.com/artifactory/cloudera-repos/

graph.sbt文件的内容如前所示,通过 Linux 的more命令。这里只有两个变化需要注意——名称的值已更改以表示内容。更重要的是,Spark GraphX 1.0.0 库已添加为库依赖项。

两个数据文件已放置在 HDFS 的/data/spark/graphx/目录下。它们包含将用于本节的顶点和边的数据。如 Hadoop 文件系统的ls命令所示,文件名分别为graph1_edges.cvsgraph1_vertex.csv

[hadoop@hc2nn scala]$ hdfs dfs -ls /data/spark/graphx
Found 2 items
-rw-r--r--   3 hadoop supergroup        129 2015-03-01 13:52 /data/spark/graphx/graph1_edges.csv
-rw-r--r--   3 hadoop supergroup         59 2015-03-01 13:52 /data/spark/graphx/graph1_vertex.csv

下面显示的“顶点”文件,通过 Hadoop 文件系统的cat命令,只包含六行,表示上一节中使用的图。每个顶点代表一个人,具有顶点 ID 号、姓名和年龄值:

[hadoop@hc2nn scala]$ hdfs dfs -cat /data/spark/graphx/graph1_vertex.csv
1,Mike,48
2,Sarah,45
3,John,25
4,Jim,53
5,Kate,22
6,Flo,52

边文件包含一组有向边值,形式为源顶点 ID、目标顶点 ID 和关系。因此,记录一形成了FloMike之间的姐妹关系:

[hadoop@hc2nn scala]$  hdfs dfs -cat /data/spark/graphx/graph1_edges.csv
6,1,Sister
1,2,Husband
2,1,Wife
5,1,Daughter
5,2,Daughter
3,1,Son
3,2,Son
4,1,Friend
1,5,Father
1,3,Father
2,5,Mother
2,3,Mother

在解释了 sbt 环境和基于 HDFS 的数据之后,我们现在准备检查一些 GraphX 代码示例。与之前的示例一样,代码可以从graphx子目录编译和打包。这将创建一个名为graph-x_2.10-1.0.jar的 JAR 文件,从中可以运行示例应用程序:

[hadoop@hc2nn graphx]$ pwd
/home/hadoop/spark/graphx

[hadoop@hc2nn graphx]$  sbt package

Loading /usr/share/sbt/bin/sbt-launch-lib.bash
[info] Set current project to Graph X (in build file:/home/hadoop/spark/graphx/)
[info] Compiling 5 Scala sources to /home/hadoop/spark/graphx/target/scala-2.10/classes...
[info] Packaging /home/hadoop/spark/graphx/target/scala-2.10/graph-x_2.10-1.0.jar ...
[info] Done packaging.
[success] Total time: 30 s, completed Mar 3, 2015 5:27:10 PM

创建图

本节将解释通用的 Scala 代码,直到从基于 HDFS 的数据创建 GraphX 图为止。这将节省时间,因为相同的代码在每个示例中都被重用。一旦这一点得到解释,我将集中在每个代码示例中的实际基于图的操作上。

通用代码从导入 Spark 上下文、graphx 和 RDD 功能开始,以便在 Scala 代码中使用:

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf

import org.apache.spark.graphx._
import org.apache.spark.rdd.RDD

然后,定义一个应用程序,它扩展了App类,并且每个示例的应用程序名称从graph1更改为graph5。在使用spark-submit运行应用程序时将使用此应用程序名称:

object graph1 extends App
{

数据文件是根据 HDFS 服务器和端口、它们在 HDFS 下的路径和它们的文件名来定义的。如前所述,有两个包含“顶点”和“边”信息的数据文件:

  val hdfsServer = "hdfs://hc2nn.semtech-solutions.co.nz:8020"
  val hdfsPath   = "/data/spark/graphx/"
  val vertexFile = hdfsServer + hdfsPath + "graph1_vertex.csv"
  val edgeFile   = hdfsServer + hdfsPath + "graph1_edges.csv"

定义了 Spark Master URL,以及应用程序名称,当应用程序运行时将出现在 Spark 用户界面中。创建了一个新的 Spark 配置对象,并将 URL 和名称分配给它:

  val sparkMaster = "spark://hc2nn.semtech-solutions.co.nz:7077"
  val appName = "Graph 1"
  val conf = new SparkConf()
  conf.setMaster(sparkMaster)
  conf.setAppName(appName)

使用刚刚定义的配置创建了一个新的 Spark 上下文:

  val sparkCxt = new SparkContext(conf)

基于 HDFS 文件的顶点信息然后使用sparkCxt.textFile方法加载到名为vertices的基于 RDD 的结构中。数据存储为长整型VertexId和字符串表示人的姓名和年龄。数据行按逗号拆分,因为这是基于 CSV 的数据:

  val vertices: RDD[(VertexId, (String, String))] =
      sparkCxt.textFile(vertexFile).map { line =>
        val fields = line.split(",")
        ( fields(0).toLong, ( fields(1), fields(2) ) )
  }

同样,基于 HDFS 的边缘数据加载到名为edges的基于 RDD 的数据结构中。基于 CSV 的数据再次按逗号值拆分。前两个数据值转换为长整型值,因为它们代表源和目标顶点 ID。表示边关系的最终值保留为字符串。请注意,RDD 结构 edges 中的每个记录现在实际上是一个Edge记录:

  val edges: RDD[Edge[String]] =
      sparkCxt.textFile(edgeFile).map { line =>
        val fields = line.split(",")
        Edge(fields(0).toLong, fields(1).toLong, fields(2))
  }

在连接或顶点缺失的情况下定义了一个默认值,然后从基于 RDD 的结构verticesedgesdefault记录构建图:

  val default = ("Unknown", "Missing")
  val graph = Graph(vertices, edges, default)

这创建了一个名为graph的基于 GraphX 的结构,现在可以用于每个示例。请记住,尽管这些数据样本很小,但您可以使用这种方法创建非常大的图形。许多这些算法都是迭代应用,例如 PageRank 和 Triangle Count,因此程序将生成许多迭代的 Spark 作业。

示例 1 - 计数

图已加载,我们知道数据文件中的数据量,但是实际图中的顶点和边的数据内容如何?通过使用顶点和边计数函数,可以很容易地提取这些信息,如下所示:

  println( "vertices : " + graph.vertices.count )
  println( "edges    : " + graph.edges.count )

使用先前创建的示例名称和 JAR 文件运行graph1示例将提供计数信息。提供主 URL 以连接到 Spark 集群,并为执行器内存和总执行器核心提供一些默认参数:

spark-submit \
  --class graph1 \
  --master spark://hc2nn.semtech-solutions.co.nz:7077  \
  --executor-memory 700M \
  --total-executor-cores 100 \
 /home/hadoop/spark/graphx/target/scala-2.10/graph-x_2.10-1.0.jar

名为graph1的 Spark 集群作业提供了以下输出,这是预期的,也与数据文件匹配:

vertices : 6
edges    : 12

示例 2 - 过滤

如果我们需要从主图创建一个子图,并按照人的年龄或关系进行筛选,会发生什么?第二个示例 Scala 文件graph2中的示例代码显示了如何做到这一点:

  val c1 = graph.vertices.filter { case (id, (name, age)) => age.toLong > 40 }.count

  val c2 = graph.edges.filter { case Edge(from, to, property)
    => property == "Father" | property == "Mother" }.count

  println( "Vertices count : " + c1 )
  println( "Edges    count : " + c2 )

两个示例计数是从主图创建的。第一个筛选基于年龄的顶点,只取那些年龄大于 40 岁的人。请注意,存储为字符串的“年龄”值已转换为长整型进行比较。前面的第二个示例筛选了“母亲”或“父亲”的关系属性的边。创建了两个计数值:c1c2,并按照 Spark 输出显示在这里打印出来:

Vertices count : 4
Edges    count : 4

示例 3 - PageRank

PageRank 算法为图中的每个顶点提供了一个排名值。它假设连接到最多边的顶点是最重要的。搜索引擎使用 PageRank 为网页搜索期间的页面显示提供排序:

  val tolerance = 0.0001
  val ranking = graph.pageRank(tolerance).vertices
  val rankByPerson = vertices.join(ranking).map {
    case (id, ( (person,age) , rank )) => (rank, id, person)
  }

前面的示例代码创建了一个tolerance值,并使用它调用了图的pageRank方法。然后将顶点排名为一个新值排名。为了使排名更有意义,排名值与原始顶点 RDD 连接。然后,rankByPerson值包含排名、顶点 ID 和人的姓名。

PageRank 结果存储在rankByPerson中,然后使用 case 语句逐条打印记录内容,并使用格式语句打印内容。我这样做是因为我想定义排名值的格式可能会有所不同:

  rankByPerson.collect().foreach {
    case (rank, id, person) =>
      println ( f"Rank $rank%1.2f id $id person $person")
  }

应用程序的输出如下所示。预期的是,MikeSarah具有最高的排名,因为他们有最多的关系:

Rank 0.15 id 4 person Jim
Rank 0.15 id 6 person Flo
Rank 1.62 id 2 person Sarah
Rank 1.82 id 1 person Mike
Rank 1.13 id 3 person John
Rank 1.13 id 5 person Kate

示例 4 - 三角形计数

三角形计数算法提供了与该顶点相关的三角形数量的基于顶点的计数。例如,顶点Mike(1)连接到Kate(5),后者连接到Sarah(2);Sarah连接到Mike(1),因此形成了一个三角形。这对于路由查找可能很有用,需要为路由规划生成最小的无三角形的生成树图。

执行三角形计数并打印的代码很简单,如下所示。对图顶点执行triangleCount方法。结果保存在值tCount中,然后打印出来:

  val tCount = graph.triangleCount().vertices
  println( tCount.collect().mkString("\n") )

应用作业的结果显示,称为Flo(4)和Jim(6)的顶点没有三角形,而Mike(1)和Sarah(2)有最多的三角形,正如预期的那样,因为它们有最多的关系:

(4,0)
(6,0)
(2,4)
(1,4)
(3,2)
(5,2)

示例 5 - 连通组件

当从数据创建一个大图时,它可能包含未连接的子图,也就是说,彼此隔离并且之间没有桥接或连接边的子图。该算法提供了这种连接性的度量。根据您的处理方式,知道所有顶点是否连接可能很重要。

对于这个示例的 Scala 代码调用了两个图方法:connectedComponentsstronglyConnectedComponents。强方法需要一个最大迭代计数,已设置为1000。这些计数作用于图的顶点:

  val iterations = 1000
  val connected  = graph.connectedComponents().vertices
  val connectedS = graph.stronglyConnectedComponents(iterations).vertices

然后将顶点计数与原始顶点记录连接起来,以便将连接计数与顶点信息(例如人的姓名)关联起来。

  val connByPerson = vertices.join(connected).map {
    case (id, ( (person,age) , conn )) => (conn, id, person)
  }

  val connByPersonS = vertices.join(connectedS).map {
    case (id, ( (person,age) , conn )) => (conn, id, person)
  }
The results are then output using a case statement, and formatted printing:
  connByPerson.collect().foreach {
    case (conn, id, person) =>
      println ( f"Weak $conn  $id $person" )
  }

connectedComponents算法所预期的那样,结果显示每个顶点只有一个组件。这意味着所有顶点都是单个图的成员,就像本章前面显示的图表一样:

Weak 1  4 Jim
Weak 1  6 Flo
Weak 1  2 Sarah
Weak 1  1 Mike
Weak 1  3 John
Weak 1  5 Kate

stronglyConnectedComponents方法提供了图中连接性的度量,考虑了它们之间关系的方向。stronglyConnectedComponents算法的结果如下:

  connByPersonS.collect().foreach {
    case (conn, id, person) =>
      println ( f"Strong $conn  $id $person" )
  }

您可能会注意到从图中,关系SisterFriend是从顶点Flo(6)和Jim(4)到Mike(1)的边和顶点数据如下所示:

6,1,Sister
4,1,Friend

1,Mike,48
4,Jim,53
6,Flo,52

因此,强方法的输出显示,对于大多数顶点,第二列中的1表示只有一个图组件。然而,由于它们关系的方向,顶点46是不可达的,因此它们有一个顶点 ID 而不是组件 ID:

Strong 4  4 Jim
Strong 6  6 Flo
Strong 1  2 Sarah
Strong 1  1 Mike
Strong 1  3 John
Strong 1  5 Kate

Neo4j 的 Mazerunner

在前面的部分中,您已经学会了如何在 Scala 中编写 Apache Spark graphx 代码来处理基于 HDFS 的图形数据。您已经能够执行基于图形的算法,例如 PageRank 和三角形计数。然而,这种方法有一个限制。Spark 没有存储功能,并且将基于图形的数据存储在 HDFS 上的平面文件中不允许您在其存储位置对其进行操作。例如,如果您的数据存储在关系数据库中,您可以使用 SQL 在原地对其进行查询。Neo4j 等数据库是图数据库。这意味着它们的存储机制和数据访问语言作用于图形。在本节中,我想看一下 Kenny Bastani 创建的 Mazerunner,它是一个 GraphX Neo4j 处理原型。

以下图描述了 Mazerunner 架构。它显示了 Neo4j 中的数据被导出到 HDFS,并通过通知过程由 GraphX 处理。然后将 GraphX 数据更新保存回 HDFS 作为键值更新列表。然后将这些更改传播到 Neo4j 进行存储。此原型架构中的算法可以通过基于 Rest 的 HTTP URL 访问,稍后将显示。这里的重点是,算法可以通过 graphx 中的处理运行,但数据更改可以通过 Neo4j 数据库 cypher 语言查询进行检查。Kenny 的工作和更多细节可以在以下网址找到:www.kennybastani.com/2014/11/using-apache-spark-and-neo4j-for-big.html

本节将专门解释 Mazerunner 架构,并将通过示例展示如何使用。该架构提供了一个基于 GraphX 处理的独特示例,结合了基于图的存储。

Neo4j 的 Mazerunner

安装 Docker

安装 Mazerunner 示例代码的过程在github.com/kbastani/neo4j-mazerunner中有描述。

我使用了 64 位 Linux Centos 6.5 机器hc1r1m1进行安装。Mazerunner 示例使用 Docker 工具,在此示例中创建了运行 HDFS、Neo4j 和 Mazerunner 的虚拟容器,占用空间很小。首先,我必须安装 Docker。我已经使用 Linux root 用户通过yum命令完成了这一点。第一条命令安装了docker-io模块(docker 名称已经被另一个应用程序用于 CentOS 6.5):

[root@hc1r1m1 bin]# yum -y install docker-io

我需要启用public_ol6_latest存储库,并安装device-mapper-event-libs包,因为我发现我当前安装的 lib-device-mapper 没有导出 Docker 需要的 Base 符号。我以root身份执行了以下命令:

[root@hc1r1m1 ~]# yum-config-manager --enable public_ol6_latest
[root@hc1r1m1 ~]# yum install device-mapper-event-libs

我遇到的实际错误如下:

/usr/bin/docker: relocation error: /usr/bin/docker: symbol dm_task_get_info_with_deferred_remove, version Base not defined in file libdevmapper.so.1.02 with link time reference

然后我可以通过以下调用检查 Docker 的版本号,以确保 Docker 可以运行:

[root@hc1r1m1 ~]# docker version
Client version: 1.4.1
Client API version: 1.16
Go version (client): go1.3.3
Git commit (client): 5bc2ff8/1.4.1
OS/Arch (client): linux/amd64
Server version: 1.4.1
Server API version: 1.16
Go version (server): go1.3.3
Git commit (server): 5bc2ff8/1.4.1

我可以使用以下服务命令启动 Linux docker 服务。我还可以使用以下chkconfig命令强制 Docker 在 Linux 服务器启动时启动:

[root@hc1r1m1 bin]# service docker start
[root@hc1r1m1 bin]# chkconfig docker on

然后可以下载三个 Docker 镜像(HDFS,Mazerunner 和 Neo4j)。它们很大,所以可能需要一些时间:

[root@hc1r1m1 ~]# docker pull sequenceiq/hadoop-docker:2.4.1
Status: Downloaded newer image for sequenceiq/hadoop-docker:2.4.1

[root@hc1r1m1 ~]# docker pull kbastani/docker-neo4j:latest
Status: Downloaded newer image for kbastani/docker-neo4j:latest

[root@hc1r1m1 ~]# docker pull kbastani/neo4j-graph-analytics:latest
Status: Downloaded newer image for kbastani/neo4j-graph-analytics:latest

下载完成后,Docker 容器可以按顺序启动;HDFS,Mazerunner,然后 Neo4j。将加载默认的 Neo4j 电影数据库,并使用这些数据运行 Mazerunner 算法。HDFS 容器的启动如下:

[root@hc1r1m1 ~]# docker run -i -t --name hdfs sequenceiq/hadoop-docker:2.4.1 /etc/bootstrap.sh –bash

Starting sshd:                                [  OK  ]
Starting namenodes on [26d939395e84]
26d939395e84: starting namenode, logging to /usr/local/hadoop/logs/hadoop-root-namenode-26d939395e84.out
localhost: starting datanode, logging to /usr/local/hadoop/logs/hadoop-root-datanode-26d939395e84.out
Starting secondary namenodes [0.0.0.0]
0.0.0.0: starting secondarynamenode, logging to /usr/local/hadoop/logs/hadoop-root-secondarynamenode-26d939395e84.out
starting yarn daemons
starting resourcemanager, logging to /usr/local/hadoop/logs/yarn--resourcemanager-26d939395e84.out
localhost: starting nodemanager, logging to /usr/local/hadoop/logs/yarn-root-nodemanager-26d939395e84.out

Mazerunner 服务容器的启动如下:

[root@hc1r1m1 ~]# docker run -i -t --name mazerunner --link hdfs:hdfs kbastani/neo4j-graph-analytics

输出很长,所以我不会在这里全部包含,但你不会看到任何错误。还有一行,说明安装正在等待消息:

[*] Waiting for messages. To exit press CTRL+C

为了启动 Neo4j 容器,我需要安装程序为我创建一个新的 Neo4j 数据库,因为这是第一次安装。否则在重新启动时,我只需提供数据库目录的路径。使用link命令,Neo4j 容器链接到 HDFS 和 Mazerunner 容器:

[root@hc1r1m1 ~]# docker run -d -P -v /home/hadoop/neo4j/data:/opt/data --name graphdb --link mazerunner:mazerunner --link hdfs:hdfs kbastani/docker-neo4j

通过检查neo4j/data路径,我现在可以看到已经创建了一个名为graph.db的数据库目录:

[root@hc1r1m1 data]# pwd
/home/hadoop/neo4j/data

[root@hc1r1m1 data]# ls
graph.db

然后我可以使用以下docker inspect命令,该命令提供了基于容器的 IP 地址和 Docker 基础的 Neo4j 容器。inspect命令提供了我需要访问 Neo4j 容器的本地 IP 地址。curl命令连同端口号(我从 Kenny 的网站上知道)默认为7474,显示 Rest 接口正在运行:

[root@hc1r1m1 data]# docker inspect --format="{{.NetworkSettings.IPAddress}}" graphdb
172.17.0.5

[root@hc1r1m1 data]# curl  172.17.0.5:7474
{
 "management" : "http://172.17.0.5:7474/db/manage/",
 "data" : "http://172.17.0.5:7474/db/data/"
}

Neo4j 浏览器

本节中的其余工作现在将使用 Neo4j 浏览器 URL 进行,如下所示:

http://172.17.0.5:7474/browser

这是一个基于本地 Docker 的 IP 地址,将可以从hc1r1m1服务器访问。如果没有进一步的网络配置,它将不会在本地局域网的其他地方可见。

这将显示默认的 Neo4j 浏览器页面。可以通过点击这里的电影链接,选择 Cypher 查询并执行来安装电影图。

Neo4j 浏览器

然后可以使用 Cypher 查询来查询数据,这将在下一章中更深入地探讨。以下图表与它们相关的 Cypher 查询一起提供,以显示数据可以作为可视化显示的图形进行访问。第一个图表显示了一个简单的人到电影关系,关系细节显示在连接的边上。

Neo4j 浏览器

第二个图表作为 Neo4j 强大性能的视觉示例,展示了一个更复杂的 Cypher 查询和生成的图表。该图表表示包含 135 个节点和 180 个关系。在处理方面,这些数字相对较小,但很明显图表变得复杂。

Neo4j 浏览器

以下图表展示了通过 HTTP Rest URL 调用 Mazerunner 示例算法。调用由要调用的算法和它将在图中操作的属性定义:

http://localhost:7474/service/mazerunner/analysis/{algorithm}/{attribute}

例如,如下一节将展示的,可以使用通用 URL 来运行 PageRank 算法,设置algorithm=pagerank。该算法将通过设置attribute=FOLLOWS来操作follows关系。下一节将展示如何运行每个 Mazerunner 算法以及 Cypher 输出的示例。

Mazerunner 算法

本节展示了如何使用上一节中显示的基于 Rest 的 HTTP URL 运行 Mazerunner 示例算法。这一章中已经检查并编码了许多这些算法。请记住,本节中发生的有趣事情是数据从 Neo4j 开始,经过 Spark 和 GraphX 处理,然后更新回 Neo4j。看起来很简单,但是有底层的过程在进行所有的工作。在每个示例中,通过 Cypher 查询来询问算法已经添加到图中的属性。因此,每个示例不是关于查询,而是数据更新到 Neo4j 已经发生。

PageRank 算法

第一个调用显示了 PageRank 算法和 PageRank 属性被添加到电影图中。与以前一样,PageRank 算法根据顶点的边连接数量给出一个排名。在这种情况下,它使用FOLLOWS关系进行处理。

PageRank 算法

以下图像显示了 PageRank 算法结果的截图。图像顶部的文本(以MATCH开头)显示了 Cypher 查询,证明了 PageRank 属性已被添加到图中。

PageRank 算法

接近度中心性算法

接近度算法试图确定图中最重要的顶点。在这种情况下,closeness属性已被添加到图中。

接近度中心性算法

以下图像显示了接近度算法结果的截图。图像顶部的文本(以MATCH开头)显示了 Cypher 查询,证明了closeness_centrality属性已被添加到图中。请注意,此 Cypher 查询中使用了一个名为closeness的别名,表示closeness_centrality属性,因此输出更具可读性。

接近度中心性算法

三角形计数算法

triangle_count算法已被用于计算与顶点相关的三角形数量。使用了FOLLOWS关系,并且triangle_count属性已被添加到图中。

三角形计数算法

以下图片显示了三角形算法结果的屏幕截图。图像顶部的文本(以MATCH开头)显示了 Cypher 查询,证明了triangle_count属性已被添加到图中。请注意,在此 Cypher 查询中使用了一个名为tcount的别名,表示triangle_count属性,因此输出更加可呈现。

三角形计数算法

连接组件算法

连接组件算法是衡量图形数据中存在多少实际组件的一种方法。例如,数据可能包含两个子图,它们之间没有路径。在这种情况下,connected_components属性已被添加到图中。

连接组件算法

以下图片显示了连接组件算法结果的屏幕截图。图像顶部的文本(以MATCH开头)显示了 Cypher 查询,证明了connected_components属性已被添加到图中。请注意,在此 Cypher 查询中使用了一个名为ccomp的别名,表示connected_components属性,因此输出更加可呈现。

连接组件算法

强连接组件算法

强连接组件算法与连接组件算法非常相似。使用方向性的FOLLOWS关系从图形数据创建子图。创建多个子图,直到使用所有图形组件。这些子图形成了强连接组件。如此所见,strongly_connected_components属性已被添加到图中:

强连接组件算法

以下图片显示了强连接组件算法结果的屏幕截图。图像顶部的文本(以MATCH开头)显示了 Cypher 查询,证明了strongly_connected_components连接组件属性已被添加到图中。请注意,在此 Cypher 查询中使用了一个名为sccomp的别名,表示strongly_connected_components属性,因此输出更加可呈现。

强连接组件算法

总结

本章已经通过示例展示了如何使用基于 Scala 的代码调用 Apache Spark 中的 GraphX 算法。之所以使用 Scala,是因为开发示例所需的代码更少,节省时间。可以使用基于 Scala 的 shell,并且可以将代码编译成 Spark 应用程序。本章提供了使用 SBT 工具进行应用程序编译和配置的示例。本章的配置和代码示例也将随书提供下载。

最后,介绍了 Mazerunner 示例架构(由 Kenny Bastani 在 Neo 期间开发)用于 Neo4j 和 Apache Spark。为什么 Mazerunner 很重要?它提供了一个示例,说明了图形数据库可以用于图形存储,而 Apache Spark 用于图形处理。我并不是建议目前在生产场景中使用 Mazerunner。显然,还需要做更多工作,使这种架构准备好发布。然而,与分布式环境中的基于图形处理相关联的基于图形存储,提供了使用 Neo4j 的 Cypher 等查询语言来查询数据的选项。

希望你觉得这一章很有用。下一章将更深入地探讨基于图的存储。你现在可以深入了解更多 GraphX 编码,尝试运行提供的示例,并尝试修改代码,以便熟悉开发过程。

第六章:基于图形的存储

使用 Apache Spark 和特别是 GraphX 进行处理提供了使用基于内存的集群的实时图形处理的能力。然而,Apache Spark 并不提供存储;基于图形的数据必须来自某个地方,并且在处理之后,可能会需要存储。在本章中,我将以 Titan 图形数据库为例,研究基于图形的存储。本章将涵盖以下主题:

  • Titan 概述

  • TinkerPop 概述

  • 安装 Titan

  • 使用 HBase 与 Titan

  • 使用 Cassandra 的 Titan

  • 使用 Spark 与 Titan

这个处理领域的年轻意味着 Apache Spark 和基于图形的存储系统 Titan 之间的存储集成还不够成熟。

在上一章中,我们研究了 Neo4j Mazerunner 架构,展示了基于 Spark 的事务如何被复制到 Neo4j。本章讨论 Titan 并不是因为它今天展示的功能,而是因为它与 Apache Spark 一起在图形存储领域所提供的未来前景。

Titan

Titan 是由 Aurelius(thinkaurelius.com/)开发的图形数据库。应用程序源代码和二进制文件可以从 GitHub(thinkaurelius.github.io/titan/)下载,该位置还包含 Titan 文档。Titan 已经作为 Apache 2 许可证的开源应用程序发布。在撰写本书时,Aurelius 已被 DataStax 收购,尽管 Titan 的发布应该会继续。

Titan 提供了许多存储选项,但我只会集中在两个上面,即 HBase——Hadoop NoSQL 数据库和 Cassandra——非 Hadoop NoSQL 数据库。使用这些底层存储机制,Titan 能够在大数据范围内提供基于图形的存储。

基于 TinkerPop3 的 Titan 0.9.0-M2 版本于 2015 年 6 月发布,这将使其与 Apache Spark 更好地集成(TinkerPop 将在下一节中解释)。我将在本章中使用这个版本。Titan 现在使用 TinkerPop 进行图形操作。这个 Titan 版本是一个实验性的开发版本,但希望未来的版本能够巩固 Titan 的功能。

本章集中讨论 Titan 数据库而不是其他图形数据库,比如 Neo4j,因为 Titan 可以使用基于 Hadoop 的存储。此外,Titan 在与 Apache Spark 集成方面提供了未来的前景,用于大数据规模的内存图形处理。下图显示了本章讨论的架构。虚线表示直接 Spark 数据库访问,而实线表示 Spark 通过 Titan 类访问数据。

Titan

Spark 接口目前还没有正式存在(只在 M2 开发版本中可用),但这只是为了参考而添加的。尽管 Titan 提供了使用 Oracle 进行存储的选项,但本章不会涉及。我将首先研究 Titan 与 HBase 和 Cassandra 的架构,然后考虑 Apache Spark 的集成。在考虑(分布式)HBase 时,还需要 ZooKeeper 进行集成。鉴于我正在使用现有的 CDH5 集群,HBase 和 ZooKeeper 已经安装好。

TinkerPop

TinkerPop,截至 2015 年 7 月目前版本为 3,是一个 Apache 孵化器项目,可以在tinkerpop.incubator.apache.org/找到。它使得图形数据库(如 Titan)和图形分析系统(如 Giraph)可以将其作为图形处理的子系统使用,而不是创建自己的图形处理模块。

TinkerPop

前面的图表(从 TinkerPop 网站借来)显示了 TinkerPop 架构。蓝色层显示了核心 TinkerPop API,为图、顶点和边处理提供了图处理 API。供应商 API框显示了供应商将实现以整合其系统的 API。图表显示有两种可能的 API:一种用于OLTP数据库系统,另一种用于OLAP分析系统。

图表还显示,Gremlin语言用于为 TinkerPop 和 Titan 创建和管理图。最后,Gremlin 服务器位于架构的顶部,并允许集成到像 Ganglia 这样的监控系统。

安装 Titan

由于本章需要使用 Titan,我现在将安装它,并展示如何获取、安装和配置它。我已经下载了最新的预构建版本(0.9.0-M2)的 Titan:s3.thinkaurelius.com/downloads/titan/titan-0.9.0-M2-hadoop1.zip

我已将压缩版本下载到临时目录,如下所示。执行以下步骤,确保 Titan 在集群中的每个节点上都安装了:

[[hadoop@hc2nn tmp]$ ls -lh titan-0.9.0-M2-hadoop1.zip
-rw-r--r-- 1 hadoop hadoop 153M Jul 22 15:13 titan-0.9.0-M2-hadoop1.zip

使用 Linux 的解压命令,解压缩压缩的 Titan 发行文件:

[hadoop@hc2nn tmp]$ unzip titan-0.9.0-M2-hadoop1.zip

[hadoop@hc2nn tmp]$ ls -l
total 155752
drwxr-xr-x 10 hadoop hadoop      4096 Jun  9 00:56 titan-0.9.0-M2-hadoop1
-rw-r--r--  1 hadoop hadoop 159482381 Jul 22 15:13 titan-0.9.0-M2-hadoop1.zip

现在,使用 Linux 的su(切换用户)命令切换到root账户,并将安装移到/usr/local/位置。更改安装文件和组成员身份为hadoop用户,并创建一个名为titan的符号链接,以便将当前的 Titan 版本简化为路径/usr/local/titan

[hadoop@hc2nn ~]$ su –
[root@hc2nn ~]# cd /home/hadoop/tmp
[root@hc2nn titan]# mv titan-0.9.0-M2-hadoop1 /usr/local
[root@hc2nn titan]# cd /usr/local
[root@hc2nn local]# chown -R hadoop:hadoop titan-0.9.0-M2-hadoop1
[root@hc2nn local]# ln -s titan-0.9.0-M2-hadoop1 titan
[root@hc2nn local]# ls -ld *titan*
lrwxrwxrwx  1 root   root     19 Mar 13 14:10 titan -> titan-0.9.0-M2-hadoop1
drwxr-xr-x 10 hadoop hadoop 4096 Feb 14 13:30 titan-0.9.0-M2-hadoop1

使用稍后将演示的 Titan Gremlin shell,现在可以使用 Titan。这个版本的 Titan 需要 Java 8;确保您已经安装了它。

带有 HBase 的 Titan

如前图所示,HBase 依赖于 ZooKeeper。鉴于我在 CDH5 集群上有一个正常运行的 ZooKeeper 仲裁(运行在hc2r1m2hc2r1m3hc2r1m4节点上),我只需要确保 HBase 在我的 Hadoop 集群上安装并正常运行。

HBase 集群

我将使用 Cloudera CDH 集群管理器安装分布式版本的 HBase。使用管理器控制台,安装 HBase 是一项简单的任务。唯一需要决定的是在集群上放置 HBase 服务器的位置。下图显示了 CDH HBase 安装的按主机查看表单。HBase 组件显示在右侧作为已添加角色

我选择将 HBase 区域服务器(RS)添加到hc2r1m2hc2r1m3hc2r1m4节点上。我在hc2r1m1主机上安装了 HBase 主服务器(M)、HBase REST 服务器(HBREST)和 HBase Thrift 服务器(HBTS)。

HBase 集群

我过去曾手动安装和配置过许多基于 Hadoop 的组件,我发现这种简单的基于管理器的安装和配置组件的方法既快速又可靠。这节省了我时间,让我可以集中精力处理其他系统,比如 Titan。

安装了 HBase,并且已经从 CDH 管理器控制台启动,需要检查以确保它正常工作。我将使用下面显示的 HBase shell 命令来执行此操作:

[hadoop@hc2r1m2 ~]$ hbase shell
Version 0.98.6-cdh5.3.2, rUnknown, Tue Feb 24 12:56:59 PST 2015
hbase(main):001:0>

如前面的命令所示,我以 Linux 用户hadoop身份运行 HBase shell。已安装 HBase 版本 0.98.6;在开始使用 Titan 时,这个版本号将变得重要:

hbase(main):001:0> create 'table2', 'cf1'
hbase(main):002:0> put 'table2', 'row1', 'cf1:1', 'value1'
hbase(main):003:0> put 'table2', 'row2', 'cf1:1', 'value2'

我已经创建了一个名为table2的简单表,列族为cf1。然后我添加了两行,每行有两个不同的值。这个表是从hc2r1m2节点创建的,现在将从 HBase 集群中的另一个名为hc2r1m4的节点进行检查:

[hadoop@hc2r1m4 ~]$ hbase shell

hbase(main):001:0> scan 'table2'

ROW                     COLUMN+CELL
 row1                   column=cf1:1, timestamp=1437968514021, value=value1
 row2                   column=cf1:1, timestamp=1437968520664, value=value2
2 row(s) in 0.3870 seconds

如您所见,从不同的主机可以看到table2中的两行数据,因此 HBase 已安装并正常工作。现在是时候尝试使用 HBase 和 Titan Gremlin shell 在 Titan 中创建图了。

Gremlin HBase 脚本

我已经检查了我的 Java 版本,以确保我使用的是 8 版本,否则 Titan 0.9.0-M2 将无法工作:

[hadoop@hc2r1m2 ~]$ java -version
openjdk version "1.8.0_51"

如果您没有正确设置 Java 版本,您将会遇到这样的错误,直到您谷歌它们,它们似乎没有意义:

Exception in thread "main" java.lang.UnsupportedClassVersionError: org/apache/tinkerpop/gremlin/groovy/plugin/RemoteAcceptor :
Unsupported major.minor version 52.0

交互式 Titan Gremlin shell 可以在 Titan 安装的 bin 目录中找到,如下所示。一旦启动,它会提供一个 Gremlin 提示:

[hadoop@hc2r1m2 bin]$ pwd
/usr/local/titan/

[hadoop@hc2r1m2 titan]$ bin/gremlin.sh
gremlin>

以下脚本将使用 Gremlin shell 输入。脚本的第一部分定义了存储(HBase)的配置,使用的 ZooKeeper 服务器,ZooKeeper 端口号以及要使用的 HBase 表名:

hBaseConf = new BaseConfiguration();
hBaseConf.setProperty("storage.backend","hbase");
hBaseConf.setProperty("storage.hostname","hc2r1m2,hc2r1m3,hc2r1m4");
hBaseConf.setProperty("storage.hbase.ext.hbase.zookeeper.property.clientPort","2181")
hBaseConf.setProperty("storage.hbase.table","titan")

titanGraph = TitanFactory.open(hBaseConf);

下一部分定义了要使用管理系统创建的图的通用顶点属性的名称和年龄。然后提交管理系统的更改:

manageSys = titanGraph.openManagement();
nameProp = manageSys.makePropertyKey('name').dataType(String.class).make();
ageProp  = manageSys.makePropertyKey('age').dataType(String.class).make();
manageSys.buildIndex('nameIdx',Vertex.class).addKey(nameProp).buildCompositeIndex();
manageSys.buildIndex('ageIdx',Vertex.class).addKey(ageProp).buildCompositeIndex();

manageSys.commit();

现在,将六个顶点添加到图中。每个顶点都被赋予一个数字标签来表示其身份。每个顶点都被赋予年龄和姓名值:

v1=titanGraph.addVertex(label, '1');
v1.property('name', 'Mike');
v1.property('age', '48');

v2=titanGraph.addVertex(label, '2');
v2.property('name', 'Sarah');
v2.property('age', '45');

v3=titanGraph.addVertex(label, '3');
v3.property('name', 'John');
v3.property('age', '25');

v4=titanGraph.addVertex(label, '4');
v4.property('name', 'Jim');
v4.property('age', '53');

v5=titanGraph.addVertex(label, '5');
v5.property('name', 'Kate');
v5.property('age', '22');

v6=titanGraph.addVertex(label, '6');
v6.property('name', 'Flo');
v6.property('age', '52');

最后,图的边被添加以将顶点连接在一起。每条边都有一个关系值。一旦创建,更改就会被提交以将它们存储到 Titan,因此也存储到 HBase:

v6.addEdge("Sister", v1)
v1.addEdge("Husband", v2)
v2.addEdge("Wife", v1)
v5.addEdge("Daughter", v1)
v5.addEdge("Daughter", v2)
v3.addEdge("Son", v1)
v3.addEdge("Son", v2)
v4.addEdge("Friend", v1)
v1.addEdge("Father", v5)
v1.addEdge("Father", v3)
v2.addEdge("Mother", v5)
v2.addEdge("Mother", v3)

titanGraph.tx().commit();

这导致了一个简单的基于人的图,如下图所示,这也是在上一章中使用的:

The Gremlin HBase script

然后可以在 Titan 中使用 Gremlin shell 测试这个图,使用与之前类似的脚本。只需在gremlin>提示符下输入以下脚本,就像之前展示的那样。它使用相同的六行来创建titanGraph配置,但然后创建了一个图遍历变量g

hBaseConf = new BaseConfiguration();
hBaseConf.setProperty("storage.backend","hbase");
hBaseConf.setProperty("storage.hostname","hc2r1m2,hc2r1m3,hc2r1m4");
hBaseConf.setProperty("storage.hbase.ext.hbase.zookeeper.property.clientPort","2181")
hBaseConf.setProperty("storage.hbase.table","titan")

titanGraph = TitanFactory.open(hBaseConf);

gremlin> g = titanGraph.traversal()

现在,图遍历变量可以用来检查图的内容。使用ValueMap选项,可以搜索名为MikeFlo的图节点。它们已经成功找到了:

gremlin> g.V().has('name','Mike').valueMap();
==>[name:[Mike], age:[48]]

gremlin> g.V().has('name','Flo').valueMap();
==>[name:[Flo], age:[52]]

因此,使用 Gremlin shell 在 Titan 中创建并检查了图,但我们也可以使用 HBase shell 检查 HBase 中的存储,并检查 Titan 表的内容。以下扫描显示表存在,并包含此小图的72行数据:

[hadoop@hc2r1m2 ~]$ hbase shell
hbase(main):002:0> scan 'titan'
72 row(s) in 0.8310 seconds

现在图已经创建,并且我确信它已经存储在 HBase 中,我将尝试使用 apache Spark 访问数据。我已经在所有节点上启动了 Apache Spark,如前一章所示。这将是从 Apache Spark 1.3 直接访问 HBase 存储。我目前不打算使用 Titan 来解释存储在 HBase 中的图。

Spark on HBase

为了从 Spark 访问 HBase,我将使用 Cloudera 的SparkOnHBase模块,可以从github.com/cloudera-labs/SparkOnHBase下载。

下载的文件是以压缩格式的,需要解压。我使用 Linux unzip 命令在临时目录中完成了这个操作:

[hadoop@hc2r1m2 tmp]$ ls -l SparkOnHBase-cdh5-0.0.2.zip
-rw-r--r-- 1 hadoop hadoop 370439 Jul 27 13:39 SparkOnHBase-cdh5-0.0.2.zip

[hadoop@hc2r1m2 tmp]$ unzip SparkOnHBase-cdh5-0.0.2.zip

[hadoop@hc2r1m2 tmp]$ ls
SparkOnHBase-cdh5-0.0.2  SparkOnHBase-cdh5-0.0.2.zip

然后,我进入解压后的模块,并使用 Maven 命令mvn来构建 JAR 文件:

[hadoop@hc2r1m2 tmp]$ cd SparkOnHBase-cdh5-0.0.2
[hadoop@hc2r1m2 SparkOnHBase-cdh5-0.0.2]$ mvn clean package

[INFO] -----------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] -----------------------------------------------------------
[INFO] Total time: 13:17 min
[INFO] Finished at: 2015-07-27T14:05:55+12:00
[INFO] Final Memory: 50M/191M
[INFO] -----------------------------------------------------------

最后,我将构建的组件移动到我的开发区域,以保持整洁,这样我就可以在我的 Spark HBase 代码中使用这个模块:

[hadoop@hc2r1m2 SparkOnHBase-cdh5-0.0.2]$ cd ..
[hadoop@hc2r1m2 tmp]$ mv SparkOnHBase-cdh5-0.0.2 /home/hadoop/spark

使用 Spark 访问 HBase

与以前的章节一样,我将使用 SBT 和 Scala 将基于 Spark 的脚本编译成应用程序。然后,我将使用 spark-submit 在 Spark 集群上运行这些应用程序。我的 SBT 配置文件如下所示。它包含了 Hadoop、Spark 和 HBase 库:

[hadoop@hc2r1m2 titan_hbase]$ pwd
/home/hadoop/spark/titan_hbase

[hadoop@hc2r1m2 titan_hbase]$ more titan.sbt
name := "T i t a n"
version := "1.0"
scalaVersion := "2.10.4"

libraryDependencies += "org.apache.hadoop" % "hadoop-client" % "2.3.0"
libraryDependencies += "org.apache.spark" %% "spark-core"  % "1.3.1"
libraryDependencies += "com.cloudera.spark" % "hbase"   % "5-0.0.2" from "file:///home/hadoop/spark/SparkOnHBase-cdh5-0.0.2/target/SparkHBase.jar"
libraryDependencies += "org.apache.hadoop.hbase" % "client"   % "5-0.0.2" from "file:///home/hadoop/spark/SparkOnHBase-cdh5-0.0.2/target/SparkHBase.jar"
resolvers += "Cloudera Repository" at "https://repository.cloudera.com/artifactory/clouder
a-repos/"

请注意,我正在hc2r1m2服务器上运行此应用程序,使用 Linuxhadoop帐户,在/home/hadoop/spark/titan_hbase目录下。我创建了一个名为run_titan.bash.hbase的 Bash shell 脚本,允许我运行在src/main/scala子目录下创建和编译的任何应用程序:

[hadoop@hc2r1m2 titan_hbase]$ pwd ; more run_titan.bash.hbase
/home/hadoop/spark/titan_hbase

#!/bin/bash

SPARK_HOME=/usr/local/spark
SPARK_BIN=$SPARK_HOME/bin
SPARK_SBIN=$SPARK_HOME/sbin

JAR_PATH=/home/hadoop/spark/titan_hbase/target/scala-2.10/t-i-t-a-n_2.10-1.0.jar
CLASS_VAL=$1

CDH_JAR_HOME=/opt/cloudera/parcels/CDH/lib/hbase/
CONN_HOME=/home/hadoop/spark/SparkOnHBase-cdh5-0.0.2/target/

HBASE_JAR1=$CDH_JAR_HOME/hbase-common-0.98.6-cdh5.3.3.jar
HBASE_JAR2=$CONN_HOME/SparkHBase.jar

cd $SPARK_BIN

./spark-submit \
 --jars $HBASE_JAR1 \
 --jars $HBASE_JAR2 \
 --class $CLASS_VAL \
 --master spark://hc2nn.semtech-solutions.co.nz:7077  \
 --executor-memory 100M \
 --total-executor-cores 50 \
 $JAR_PATH

Bash 脚本保存在相同的titan_hbase目录中,并接受应用程序类名的单个参数。spark-submit调用的参数与先前的示例相同。在这种情况下,在src/main/scala下只有一个脚本,名为spark3_hbase2.scala

[hadoop@hc2r1m2 scala]$ pwd
/home/hadoop/spark/titan_hbase/src/main/scala

[hadoop@hc2r1m2 scala]$ ls
spark3_hbase2.scala

Scala 脚本首先定义了应用程序类所属的包名称。然后导入了 Spark、Hadoop 和 HBase 类:

package nz.co.semtechsolutions

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf

import org.apache.hadoop.hbase._
import org.apache.hadoop.fs.Path
import com.cloudera.spark.hbase.HBaseContext
import org.apache.hadoop.hbase.client.Scan

应用程序类名也被定义,以及主方法。然后根据应用程序名称和 Spark URL 创建一个配置对象。最后,从配置创建一个 Spark 上下文:

object spark3_hbase2
{

  def main(args: Array[String]) {

    val sparkMaster = "spark://hc2nn.semtech-solutions.co.nz:7077"
    val appName = "Spark HBase 2"
    val conf = new SparkConf()

    conf.setMaster(sparkMaster)
    conf.setAppName(appName)

    val sparkCxt = new SparkContext(conf)

接下来,创建一个 HBase 配置对象,并添加一个基于 Cloudera CDH hbase-site.xml文件的资源:

    val jobConf = HBaseConfiguration.create()

    val hbasePath="/opt/cloudera/parcels/CDH/etc/hbase/conf.dist/"

    jobConf.addResource(new Path(hbasePath+"hbase-site.xml"))

使用 Spark 上下文和 HBase 配置对象创建一个 HBase 上下文对象。还定义了扫描和缓存配置:

    val hbaseContext = new HBaseContext(sparkCxt, jobConf)

    var scan = new Scan()
    scan.setCaching(100)

最后,使用hbaseRDD HBase 上下文方法和扫描对象检索了 HBase Titan表中的数据。打印了 RDD 计数,然后关闭了脚本:

    var hbaseRdd = hbaseContext.hbaseRDD("titan", scan)

    println( "Rows in Titan hbase table : " + hbaseRdd.count() )

    println( " >>>>> Script Finished <<<<< " )

  } // end main

} // end spark3_hbase2

我只打印了检索到的数据计数,因为 Titan 以 GZ 格式压缩数据。因此,直接尝试操纵它将没有多大意义。

使用run_titan.bash.hbase脚本运行名为spark3_hbase2的 Spark 应用程序。它输出了一个 RDD 行计数为72,与先前找到的 Titan 表行计数相匹配。这证明 Apache Spark 已能够访问原始的 Titan HBase 存储的图形数据,但 Spark 尚未使用 Titan 库来访问 Titan 数据作为图形。这将在稍后讨论。以下是代码:

[hadoop@hc2r1m2 titan_hbase]$ ./run_titan.bash.hbase nz.co.semtechsolutions.spark3_hbase2

Rows in Titan hbase table : 72
 >>>>> Script Finished <<<<<

使用 Cassandra 的 Titan

在本节中,Cassandra NoSQL 数据库将作为 Titan 的存储机制。尽管它不使用 Hadoop,但它本身是一个大规模的基于集群的数据库,并且可以扩展到非常大的集群规模。本节将遵循相同的流程。与 HBase 一样,将创建一个图,并使用 Titan Gremlin shell 将其存储在 Cassandra 中。然后将使用 Gremlin 进行检查,并在 Cassandra 中检查存储的数据。然后将从 Spark 中访问原始的 Titan Cassandra 图形数据。因此,第一步是在集群中的每个节点上安装 Cassandra。

安装 Cassandra

创建一个允许使用 Linux 的yum命令安装 DataStax Cassandra 社区版本的 repo 文件。这将需要 root 访问权限,因此使用su命令切换用户到 root。在所有节点上安装 Cassandra:

[hadoop@hc2nn lib]$ su -
[root@hc2nn ~]# vi /etc/yum.repos.d/datastax.repo

[datastax]
name= DataStax Repo for Apache Cassandra
baseurl=http://rpm.datastax.com/community
enabled=1
gpgcheck=0

现在,在集群中的每个节点上使用 Linux 的yum命令安装 Cassandra:

[root@hc2nn ~]# yum -y install dsc20-2.0.13-1 cassandra20-2.0.13-1

通过修改cassandra.yaml文件,在/etc/cassandra/conf下设置 Cassandra 配置:

[root@hc2nn ~]# cd /etc/cassandra/conf   ; vi cassandra.yaml

我已经做了以下更改,以指定我的集群名称、服务器种子 IP 地址、RPC 地址和 snitch 值。种子节点是其他节点首先尝试连接的节点。在这种情况下,NameNode(103)和 node2(108)被用作seeds。snitch 方法管理网络拓扑和路由:

cluster_name: 'Cluster1'
seeds: "192.168.1.103,192.168.1.108"
listen_address:
rpc_address: 0.0.0.0
endpoint_snitch: GossipingPropertyFileSnitch

现在可以作为 root 在每个节点上启动 Cassandra,使用 service 命令:

[root@hc2nn ~]# service cassandra start

日志文件可以在/var/log/cassandra下找到,数据存储在/var/lib/cassandra下。nodetool命令可以在任何 Cassandra 节点上使用,以检查 Cassandra 集群的状态:

[root@hc2nn cassandra]# nodetool status
Datacenter: DC1
===============
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
--  Address        Load       Tokens  Owns (effective)  Host ID Rack
UN  192.168.1.105  63.96 KB   256     37.2%             f230c5d7-ff6f-43e7-821d-c7ae2b5141d3  RAC1
UN  192.168.1.110  45.86 KB   256     39.9%             fc1d80fe-6c2d-467d-9034-96a1f203c20d  RAC1
UN  192.168.1.109  45.9 KB    256     40.9%             daadf2ee-f8c2-4177-ae72-683e39fd1ea0  RAC1
UN  192.168.1.108  50.44 KB   256     40.5%             b9d796c0-5893-46bc-8e3c-187a524b1f5a  RAC1
UN  192.168.1.103  70.68 KB   256     41.5%             53c2eebd-
a66c-4a65-b026-96e232846243  RAC1

Cassandra CQL shell 命令称为cqlsh,可用于访问集群并创建对象。接下来调用 shell,它显示 Cassandra 版本 2.0.13 已安装:

[hadoop@hc2nn ~]$ cqlsh
Connected to Cluster1 at localhost:9160.
[cqlsh 4.1.1 | Cassandra 2.0.13 | CQL spec 3.1.1 | Thrift protocol 19.39.0]
Use HELP for help.
cqlsh>

Cassandra 查询语言接下来显示了一个名为keyspace1的键空间,通过 CQL shell 创建和使用:

cqlsh> CREATE KEYSPACE keyspace1 WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };

cqlsh> USE keyspace1;

cqlsh:keyspace1> SELECT * FROM system.schema_keyspaces;

 keyspace_name | durable_writes | strategy_class                              | strategy_options
--------------+------+---------------------------------------------+----------------------------
 keyspace1  | True | org.apache.cassandra.locator.SimpleStrategy | {"replication_factor":"1"}
 system  | True |  org.apache.cassandra.locator.LocalStrategy |                         {}
system_traces | True | org.apache.cassandra.locator.SimpleStrategy | {"replication_factor":"2"}

由于 Cassandra 已安装并运行,现在是时候使用 Cassandra 创建 Titan 图形存储。这将在下一节中使用 Titan Gremlin shell 来解决。它将遵循之前 HBase 部分的相同格式。

Gremlin Cassandra 脚本

与之前的 Gremlin 脚本一样,这个 Cassandra 版本创建了相同的简单图。这个脚本的不同之处在于配置。后端存储类型被定义为 Cassandra,主机名被定义为 Cassandra 种子节点。指定了 key space 和端口号,最后创建了图:

cassConf = new BaseConfiguration();
cassConf.setProperty("storage.backend","cassandra");
cassConf.setProperty("storage.hostname","hc2nn,hc2r1m2");
cassConf.setProperty("storage.port","9160")
cassConf.setProperty("storage.keyspace","titan")
titanGraph = TitanFactory.open(cassConf);

从这一点开始,脚本与之前的 HBase 示例相同,所以我不会重复它。这个脚本将作为cassandra_create.bash在下载包中提供。可以在 Gremlin shell 中使用之前的配置进行相同的检查以检查数据。这返回与之前检查相同的结果,证明图已经被存储:

gremlin> g = titanGraph.traversal()

gremlin> g.V().has('name','Mike').valueMap();
==>[name:[Mike], age:[48]]

gremlin> g.V().has('name','Flo').valueMap();
==>[name:[Flo], age:[52]]

通过使用 Cassandra CQL shell 和 Titan keyspace,可以看到在 Cassandra 中已经创建了许多 Titan 表:

[hadoop@hc2nn ~]$ cqlsh
cqlsh> use titan;
cqlsh:titan> describe tables;
edgestore        graphindex        system_properties systemlog  txlog
edgestore_lock_  graphindex_lock_  system_properties_lock_  titan_ids

还可以看到数据存在于 Cassandra 的edgestore表中:

cqlsh:titan> select * from edgestore;
 key                | column1            | value
--------------------+--------------------+------------------------------------------------
 0x0000000000004815 |               0x02 |                                     0x00011ee0
 0x0000000000004815 |             0x10c0 |                           0xa0727425536fee1ec0
.......
 0x0000000000001005 |             0x10c8 |                       0x00800512644c1b149004a0
 0x0000000000001005 | 0x30c9801009800c20 |   0x000101143c01023b0101696e6465782d706ff30200

这向我保证了在 Gremlin shell 中已经创建了 Titan 图,并且存储在 Cassandra 中。现在,我将尝试从 Spark 中访问数据。

Spark Cassandra 连接器

为了从 Spark 访问 Cassandra,我将下载 DataStax Spark Cassandra 连接器和驱动程序库。关于这方面的信息和版本匹配可以在mvnrepository.com/artifact/com.datastax.spark/找到。

这个 URL 的版本兼容性部分显示了应该与每个 Cassandra 和 Spark 版本一起使用的 Cassandra 连接器版本。版本表显示连接器版本应该与正在使用的 Spark 版本匹配。下一个 URL 允许在mvnrepository.com/artifact/com.datastax.spark/spark-cassandra-connector_2.10找到这些库。

通过上面的 URL,并选择一个库版本,你将看到与该库相关的编译依赖关系表,其中指示了你需要的所有其他依赖库及其版本。以下库是与 Spark 1.3.1 一起使用所需的。如果你使用前面的 URL,你将看到每个 Spark 版本应该使用哪个版本的 Cassandra 连接器库。你还将看到 Cassandra 连接器依赖的库。请小心选择所需的库版本:

[hadoop@hc2r1m2 titan_cass]$ pwd ; ls *.jar
/home/hadoop/spark/titan_cass

spark-cassandra-connector_2.10-1.3.0-M1.jar
cassandra-driver-core-2.1.5.jar
cassandra-thrift-2.1.3.jar
libthrift-0.9.2.jar
cassandra-clientutil-2.1.3.jar
guava-14.0.1.jar
joda-time-2.3.jar
joda-convert-1.2.jar

使用 Spark 访问 Cassandra

现在我已经准备好了 Cassandra 连接器库和所有的依赖关系,我可以开始考虑连接到 Cassandra 所需的 Scala 代码。首先要做的事情是设置 SBT 构建配置文件,因为我使用 SBT 作为开发工具。我的配置文件看起来像这样:

[hadoop@hc2r1m2 titan_cass]$ pwd ; more titan.sbt
/home/hadoop/spark/titan_cass

name := "Spark Cass"
version := "1.0"
scalaVersion := "2.10.4"
libraryDependencies += "org.apache.hadoop" % "hadoop-client" % "2.3.0"
libraryDependencies += "org.apache.spark" %% "spark-core"  % "1.3.1"
libraryDependencies += "com.datastax.spark" % "spark-cassandra-connector"  % "1.3.0-M1" fr
om "file:///home/hadoop/spark/titan_cass/spark-cassandra-connector_2.10-1.3.0-M1.jar"
libraryDependencies += "com.datastax.cassandra" % "cassandra-driver-core"  % "2.1.5" from
"file:///home/hadoop/spark/titan_cass/cassandra-driver-core-2.1.5.jar"
libraryDependencies += "org.joda"  % "time" % "2.3" from "file:///home/hadoop/spark/titan_
cass/joda-time-2.3.jar"
libraryDependencies += "org.apache.cassandra" % "thrift" % "2.1.3" from "file:///home/hado
op/spark/titan_cass/cassandra-thrift-2.1.3.jar"
libraryDependencies += "com.google.common" % "collect" % "14.0.1" from "file:///home/hadoo
p/spark/titan_cass/guava-14.0.1.jar
resolvers += "Cloudera Repository" at "https://repository.cloudera.com/artifactory/clouder
a-repos/"

Cassandra 连接器示例的 Scala 脚本,名为spark3_cass.scala,现在看起来像以下代码。首先,定义了包名。然后,为 Spark 和 Cassandra 连接器导入了类。接下来,定义了对象应用类spark3_cass ID,以及主方法:

package nz.co.semtechsolutions

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf

import com.datastax.spark.connector._

object spark3_cass
{

  def main(args: Array[String]) {

使用 Spark URL 和应用程序名称创建了一个 Spark 配置对象。将 Cassandra 连接主机添加到配置中。然后,使用配置对象创建了 Spark 上下文:

    val sparkMaster = "spark://hc2nn.semtech-solutions.co.nz:7077"
    val appName = "Spark Cass 1"
    val conf = new SparkConf()

    conf.setMaster(sparkMaster)
    conf.setAppName(appName)

    conf.set("spark.cassandra.connection.host", "hc2r1m2")

    val sparkCxt = new SparkContext(conf)

要检查的 Cassandra keyspace和表名已经定义。然后,使用名为cassandraTable的 Spark 上下文方法连接到 Cassandra,并获取edgestore表的内容作为 RDD。然后打印出这个 RDD 的大小,脚本退出。我们暂时不会查看这些数据,因为只需要证明可以连接到 Cassandra:

    val keySpace =  "titan"
    val tableName = "edgestore"

    val cassRDD = sparkCxt.cassandraTable( keySpace, tableName )

    println( "Cassandra Table Rows : " + cassRDD.count )

    println( " >>>>> Script Finished <<<<< " )

  } // end main

} // end spark3_cass

与之前的示例一样,Spark 的submit命令已放置在一个名为run_titan.bash.cass的 Bash 脚本中。下面显示的脚本看起来与已经使用的许多其他脚本类似。这里需要注意的是有一个 JARs 选项,列出了所有在运行时可用的 JAR 文件。这个选项中 JAR 文件的顺序已经确定,以避免类异常错误:

[hadoop@hc2r1m2 titan_cass]$ more run_titan.bash

#!/bin/bash

SPARK_HOME=/usr/local/spark
SPARK_BIN=$SPARK_HOME/bin
SPARK_SBIN=$SPARK_HOME/sbin

JAR_PATH=/home/hadoop/spark/titan_cass/target/scala-2.10/spark-cass_2.10-1.0.jar
CLASS_VAL=$1

CASS_HOME=/home/hadoop/spark/titan_cass/

CASS_JAR1=$CASS_HOME/spark-cassandra-connector_2.10-1.3.0-M1.jar
CASS_JAR2=$CASS_HOME/cassandra-driver-core-2.1.5.jar
CASS_JAR3=$CASS_HOME/cassandra-thrift-2.1.3.jar
CASS_JAR4=$CASS_HOME/libthrift-0.9.2.jar
CASS_JAR5=$CASS_HOME/cassandra-clientutil-2.1.3.jar
CASS_JAR6=$CASS_HOME/guava-14.0.1.jar
CASS_JAR7=$CASS_HOME/joda-time-2.3.jar
CASS_JAR8=$CASS_HOME/joda-convert-1.2.jar

cd $SPARK_BIN

./spark-submit \
 --jars $CASS_JAR8,$CASS_JAR7,$CASS_JAR5,$CASS_JAR4,$CASS_JAR3,$CASS_JAR6,$CASS_JAR2,$CASS_JAR1 \
 --class $CLASS_VAL \
 --master spark://hc2nn.semtech-solutions.co.nz:7077  \
 --executor-memory 100M \
 --total-executor-cores 50 \
 $JAR_PATH

此应用程序是通过之前的 Bash 脚本调用的。它连接到 Cassandra,选择数据,并返回基于 Cassandra 表数据的计数为218行。

[hadoop@hc2r1m2 titan_cass]$ ./run_titan.bash.cass nz.co.semtechsolutions.spark3_cass

Cassandra Table Rows : 218
 >>>>> Script Finished <<<<<

这证明了可以从 Apache Spark 访问基于原始 Cassandra 的 Titan 表数据。然而,与 HBase 示例一样,这是基于原始表的 Titan 数据,而不是 Titan 图形中的数据。下一步将是使用 Apache Spark 作为 Titan 数据库的处理引擎。这将在下一节中进行讨论。

使用 Spark 访问 Titan

到目前为止,在本章中,已经安装了 Titan 0.9.0-M2,并成功使用 HBase 和 Cassandra 作为后端存储选项创建了图形。这些图形是使用基于 Gremlin 的脚本创建的。在本节中,将使用属性文件通过 Gremlin 脚本来处理基于 Titan 的图形,使用相同的两个后端存储选项 HBase 和 Cassandra。

以下图表基于本章前面的 TinkerPop3 图表,展示了本节中使用的架构。我简化了图表,但基本上与之前的 TinkerPop 版本相同。我只是通过图形计算机 API 添加了到 Apache Spark 的链接。我还通过 Titan 供应商 API 添加了 HBase 和 Cassandra 存储。当然,HBase 的分布式安装同时使用 Zookeeper 进行配置,使用 HDFS 进行存储。

Titan 使用 TinkerPop 的 Hadoop-Gremlin 包进行图处理 OLAP 过程。文档部分的链接可以在这里找到:s3.thinkaurelius.com/docs/titan/0.9.0-M2/titan-hadoop-tp3.html

本节将展示如何使用 Bash shell、Groovy 和属性文件来配置和运行基于 Titan Spark 的作业。它将展示配置作业的不同方法,并展示管理日志以启用错误跟踪的方法。还将描述属性文件的不同配置,以便访问 HBase、Cassandra 和 Linux 文件系统。

请记住,本章基于的 Titan 0.9.0-M2 版本是一个开发版本。这是一个原型版本,尚未准备好投入生产。我假设随着未来 Titan 版本的推出,Titan 与 Spark 之间的链接将更加完善和稳定。目前,本节中的工作仅用于演示目的,考虑到 Titan 版本的性质。

使用 Spark 访问 Titan

在下一节中,我将解释使用 Gremlin 和 Groovy 脚本,然后转向使用 Cassandra 和 HBase 作为存储选项将 Titan 连接到 Spark。

Gremlin 和 Groovy

用于执行 Groovy 命令的 Gremlin shell 可以以多种方式使用。第一种使用方法只涉及启动 Gremlin shell 以用作交互式会话。只需执行以下命令:

cd $TITAN_HOME/bin ; ./ gremlin.sh

这将启动会话,并自动设置所需的插件,如 TinkerPop 和 Titan(见下文)。显然,之前的TITAN_HOME变量用于指示所讨论的 bin 目录位于您的 Titan 安装(TITAN_HOME)目录中:

plugin activated: tinkerpop.server
plugin activated: tinkerpop.utilities
plugin activated: tinkerpop.hadoop
plugin activated: tinkerpop.tinkergraph
plugin activated: aurelius.titan

然后它会提供一个 Gremlin shell 提示符,您可以在其中交互式地执行对 Titan 数据库的 shell 命令。此 shell 对于测试脚本和针对 Titan 数据库运行临时命令非常有用。

gremlin>

第二种方法是在调用gremlin.sh命令时将您的 Groovy 命令嵌入到脚本中。在这个例子中,EOF 标记之间的 Groovy 命令被传送到 Gremlin shell 中。当最后一个 Groovy 命令执行完毕时,Gremlin shell 将终止。当您仍希望使用 Gremlin shell 的自动化环境设置,但仍希望能够快速重新执行脚本时,这是很有用的。这段代码片段是从 Bash shell 脚本中执行的,如下一个例子所示。以下脚本使用titan.sh脚本来管理 Gremlin 服务器:

#!/bin/bash

TITAN_HOME=/usr/local/titan/

cd $TITAN_HOME

bin/titan.sh start

bin/gremlin.sh   <<  EOF

 t = TitanFactory.open('cassandra.properties')
 GraphOfTheGodsFactory.load(t)
 t.close()
EOF

bin/titan.sh stop

第三种方法涉及将 Groovy 命令移动到一个单独的 Groovy 文件中,并使用 Gremlin shell 的-e选项来执行该文件。这种方法为错误跟踪提供了额外的日志选项,但意味着在为 Groovy 脚本设置 Gremlin 环境时需要采取额外的步骤:

#!/bin/bash

TITAN_HOME=/usr/local/titan/
SCRIPTS_HOME=/home/hadoop/spark/gremlin
GREMLIN_LOG_FILE=$TITAN_HOME/log/gremlin_console.log

GROOVY_SCRIPT=$1

export GREMLIN_LOG_LEVEL="DEBUG"

cd $TITAN_HOME

bin/titan.sh start

bin/gremlin.sh -e  $SCRIPTS_HOME/$GROOVY_SCRIPT  > $GREMLIN_LOG_FILE 2>&1

bin/titan.sh stop

因此,这个脚本定义了 Gremlin 日志级别,可以设置为不同的日志级别以获取有关问题的额外信息,即 INFO、WARN 和 DEBUG。它还将脚本输出重定向到日志文件(GREMLIN_LOG_FILE),并将错误重定向到同一个日志文件(2>&1)。这样做的好处是可以持续监视日志文件,并提供会话的永久记录。要执行的 Groovy 脚本名称然后作为参数($1)传递给封装的 Bash shell 脚本。

正如我之前提到的,以这种方式调用的 Groovy 脚本需要额外的环境配置,以设置 Gremlin 会话,与之前的 Gremlin 会话选项相比。例如,需要导入将要使用的必要的 TinkerPop 和 Aurelius 类:

import com.thinkaurelius.titan.core.*
import com.thinkaurelius.titan.core.titan.*
import org.apache.tinkerpop.gremlin.*

在描述了启动 Gremlin shell 会话和运行 Groovy 脚本所需的脚本和配置选项之后,从现在开始我将集中讨论 Groovy 脚本和配置 Gremlin 会话所需的属性文件。

TinkerPop 的 Hadoop Gremlin

正如前面在本节中已经提到的,Titan 中的 TinkerPop Hadoop Gremlin 包将用于调用 Apache Spark 作为处理引擎(Hadoop Giraph 也可以用于处理)。链接s3.thinkaurelius.com/docs/titan/0.9.0-M2/titan-hadoop-tp3.html提供了 Hadoop Gremlin 的文档;请记住,这个 TinkerPop 包仍在开发中,可能会有所改变。

在这一点上,我将检查一个属性文件,该文件可用于将 Cassandra 作为 Titan 的存储后端进行连接。它包含了用于 Cassandra、Apache Spark 和 Hadoop Gremlin 配置的部分。我的 Cassandra 属性文件名为cassandra.properties,内容如下(以井号(#)开头的行是注释):

####################################
# Storage details
####################################
storage.backend=cassandra
storage.hostname=hc2r1m2
storage.port=9160
storage.cassandra.keyspace=dead
cassandra.input.partitioner.class=org.apache.cassandra.dht.Murmur3Partitioner

前面基于 Cassandra 的属性描述了 Cassandra 主机和端口。这就是存储后端类型为 Cassandra 的原因,要使用的 Cassandra keyspace称为dead(代表感激的死者——在本例中将使用的数据)。请记住,Cassandra 表是在 keyspaces 中分组的。前面的partitioner类定义了将用于对 Cassandra 数据进行分区的 Cassandra 类。Apache Spark 配置部分包含主 URL、执行器内存和要使用的数据serializer类:

####################################
# Spark
####################################
spark.master=spark://hc2nn.semtech-solutions.co.nz:6077
spark.executor.memory=400M
spark.serializer=org.apache.spark.serializer.KryoSerializer

最后,这里显示了属性文件中 Hadoop Gremlin 部分,该部分定义了用于图形和非图形输入和输出的类。它还定义了数据输入和输出位置,以及用于缓存 JAR 文件和推导内存的标志。

####################################
# Hadoop Gremlin
####################################
gremlin.graph=org.apache.tinkerpop.gremlin.hadoop.structure.HadoopGraph
gremlin.hadoop.graphInputFormat=com.thinkaurelius.titan.hadoop.formats.cassandra.CassandraInputFormat
gremlin.hadoop.graphOutputFormat=org.apache.tinkerpop.gremlin.hadoop.structure.io.gryo.GryoOutputFormat
gremlin.hadoop.memoryOutputFormat=org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat

gremlin.hadoop.deriveMemory=false
gremlin.hadoop.jarsInDistributedCache=true
gremlin.hadoop.inputLocation=none
gremlin.hadoop.outputLocation=output

蓝图是 TinkerPop 属性图模型接口。Titan 发布了自己的蓝图实现,所以在前面的属性中,你会看到gremlin.graph而不是blueprints.graph。这定义了用于定义应该使用的图的类。如果省略了这个选项,那么图类型将默认为以下内容:

com.thinkaurelius.titan.core.TitanFactory

CassandraInputFormat类定义了数据是从 Cassandra 数据库中检索出来的。图输出序列化类被定义为GryoOutputFormat。内存输出格式类被定义为使用 Hadoop Map Reduce 类SequenceFileOutputFormat

jarsInDistributedCache值已被定义为 true,以便将 JAR 文件复制到内存中,使 Apache Spark 能够使用它们。如果有更多时间,我会研究使 Titan 类对 Spark 可见的方法,以避免过多的内存使用。

鉴于 TinkerPop Hadoop Gremlin 模块目前仅作为开发原型版本发布,目前文档很少。编码示例非常有限,似乎也没有文档描述之前的每个属性。

在我深入探讨 Groovy 脚本示例之前,我想向您展示一种使用配置对象配置 Groovy 作业的替代方法。

替代 Groovy 配置

可以使用BaseConfiguration方法创建配置对象。在这个例子中,我创建了一个名为cassConf的 Cassandra 配置:

cassConf = new BaseConfiguration();

cassConf.setProperty("storage.backend","cassandra");
cassConf.setProperty("storage.hostname","hc2r1m2");
cassConf.setProperty("storage.port","9160")
cassConf.setProperty("storage.cassandra.keyspace","titan")

titanGraph = TitanFactory.open(cassConf);

然后使用setProperty方法来定义 Cassandra 连接属性,如后端类型、主机、端口和keyspace。最后,使用 open 方法创建了一个名为titanGraph的 Titan 图。稍后将会展示,Titan 图可以使用配置对象或属性文件路径来创建。已设置的属性与之前描述的 Cassandra 属性文件中定义的属性相匹配。

接下来的几节将展示如何创建和遍历图。它们将展示如何使用 Cassandra、HBase 和文件系统进行存储。鉴于我已经花了很多篇幅描述 Bash 脚本和属性文件,我只会描述每个实例中需要更改的属性。我还将在每个实例中提供简单的 Groovy 脚本片段。

使用 Cassandra

名为cassandra.properties的基于 Cassandra 的属性文件已经在前面描述过,所以我不会在这里重复细节。这个示例 Groovy 脚本创建了一个示例图,并将其存储在 Cassandra 中。它已经使用EOF来将脚本传输到 Gremlin shell 执行:

t1 = TitanFactory.open('/home/hadoop/spark/gremlin/cassandra.properties')
GraphOfTheGodsFactory.load(t1)

t1.traversal().V().count()

t1.traversal().V().valueMap()

t1.close()

使用TitanFactory.open方法和 Cassandra 属性文件创建了一个 Titan 图。它被称为t1。上帝之图,一个提供给 Titan 的示例图,已经被加载到图t1中,使用了GraphOfTheGodsFactory.load方法。然后生成了顶点的计数(V())以及ValueMap来显示图的内容。输出如下:

==>12

==>[name:[jupiter], age:[5000]]
==>[name:[hydra]]
==>[name:[nemean]]
==>[name:[tartarus]]
==>[name:[saturn], age:[10000]]
==>[name:[sky]]
==>[name:[pluto], age:[4000]]
==>[name:[alcmene], age:[45]]
==>[name:[hercules], age:[30]]
==>[name:[sea]]
==>[name:[cerberus]]
==>[name:[neptune], age:[4500]]

因此,图中有 12 个顶点,每个顶点都有一个在前面数据中显示的名称和年龄元素。成功创建了一个图后,现在可以配置之前的图遍历 Gremlin 命令以使用 Apache Spark 进行处理。只需在遍历命令中指定SparkGraphComputer即可实现。有关架构细节,请参见本章顶部的完整TinkerPop图。执行此命令时,您将在 Spark 集群用户界面上看到任务出现:

t1.traversal(computer(SparkGraphComputer)).V().count()

使用 HBase

在使用 HBase 时,需要更改属性文件。以下数值取自我的hbase.properties文件:

gremlin.hadoop.graphInputFormat=com.thinkaurelius.titan.hadoop.formats.hbase.HBaseInputFormat

input.conf.storage.backend=hbase
input.conf.storage.hostname=hc2r1m2
input.conf.storage.port=2181
input.conf.storage.hbase.table=titan
input.conf.storage.hbase.ext.zookeeper.znode.parent=/hbase

请记住,HBase 使用 Zookeeper 进行配置。因此,连接的端口号和服务器现在变成了zookeeper服务器和zookeeper主端口 2181。在 Zookeeper 中,znode父值也被定义为顶级节点/hbase。当然,后端类型现在被定义为hbase

此外,GraphInputFormat类已更改为HBaseInputFormat,以描述 HBase 作为输入源。现在可以使用此属性文件创建 Titan 图,就像上一节所示的那样。我不会在这里重复图的创建,因为它与上一节相同。接下来,我将转向文件系统存储。

使用文件系统

为了运行这个例子,我使用了一个基本的 Gremlin shell(bin/gremlin.sh)。在 Titan 发布的数据目录中,有许多可以加载以创建图形的示例数据文件格式。在这个例子中,我将使用名为grateful-dead.kryo的文件。因此,这次数据将直接从文件加载到图形中,而不需要指定存储后端,比如 Cassandra。我将使用的属性文件只包含以下条目:

gremlin.graph=org.apache.tinkerpop.gremlin.hadoop.structure.HadoopGraph
gremlin.hadoop.graphInputFormat=org.apache.tinkerpop.gremlin.hadoop.structure.io.gryo.GryoInputFormat
gremlin.hadoop.graphOutputFormat=org.apache.tinkerpop.gremlin.hadoop.structure.io.gryo.GryoOutputFormat
gremlin.hadoop.jarsInDistributedCache=true
gremlin.hadoop.deriveMemory=true

gremlin.hadoop.inputLocation=/usr/local/titan/data/grateful-dead.kryo
gremlin.hadoop.outputLocation=output

再次,它使用了 Hadoop Gremlin 包,但这次图形输入和输出格式被定义为GryoInputFormatGryoOutputFormat。输入位置被指定为实际的基于kyro的文件。因此,输入和输出的源是文件。现在,Groovy 脚本看起来像这样。首先,使用属性文件创建图形。然后创建图形遍历,以便我们可以计算顶点并查看结构:

graph = GraphFactory.open('/home/hadoop/spark/gremlin/hadoop-gryo.properties')
g1 = graph.traversal()

接下来,执行了一个顶点计数,显示有 800 多个顶点;最后,一个值映射显示了数据的结构,我显然剪辑了一些以节省空间。但你可以看到歌曲名称、类型和表演细节:

g1.V().count()
==>808
g1.V().valueMap()
==>[name:[MIGHT AS WELL], songType:[original], performances:[111]]
==>[name:[BROWN EYED WOMEN], songType:[original], performances:[347]]

这给您一个关于可用功能的基本概念。我相信,如果您搜索网络,您会发现更复杂的使用 Spark 与 Titan 的方法。以这个为例:

r = graph.compute(SparkGraphComputer.class).program(PageRankVertexProgram.build().create()).submit().get()

前面的例子指定了使用SparkGraphComputer类的 compute 方法。它还展示了如何使用 Titan 提供的页面排名顶点程序来执行程序方法。这将通过为每个顶点添加页面排名来修改您的图形。我提供这个作为一个例子,因为我不确定它现在是否适用于 Spark。

总结

本章介绍了 Aurelius 的 Titan 图形数据库。它展示了如何在 Linux 集群上安装和配置它。使用 Titan Gremlin shell 示例,图形已经被创建,并存储在 HBase 和 Cassandra NoSQL 数据库中。所需的 Titan 存储选项将取决于您的项目需求;HBase 基于 HDFS 的存储或 Cassandra 非 HDFS 的存储。本章还表明,您可以交互地使用 Gremlin shell 开发图形脚本,并使用 Bash shell 脚本运行带有关联日志的定期作业。

提供了简单的 Spark Scala 代码,显示了 Apache Spark 可以访问 Titan 在 HBase 和 Cassandra 上创建的基础表。这是通过使用 Cloudera 提供的数据库连接器模块(用于 HBase)和 DataStax(用于 Cassandra)实现的。所有示例代码和构建脚本都已经描述,并附有示例输出。我包含了这个基于 Scala 的部分,以向您展示可以在 Scala 中访问基于图形的数据。前面的部分从 Gremlin shell 处理数据,并使用 Spark 作为处理后端。这一部分将 Spark 作为主要处理引擎,并从 Spark 访问 Titan 数据。如果 Gremlin shell 不适合您的需求,您可以考虑这种方法。随着 Titan 的成熟,您可以通过 Scala 以不同的方式将 Titan 与 Spark 集成。

最后,Titan 的 Gremlin shell 已经与 Apache Spark 一起使用,演示了创建和访问基于 Titan 的图形的简单方法。为此,数据已存储在文件系统、Cassandra 和 HBase 上。

通过以下网址,Aurelius 和 Gremlin 用户可以使用 Google 群组:groups.google.com/forum/#!forum/aureliusgraphsgroups.google.com/forum/#!forum/gremlin-users

尽管 Titan 社区似乎比其他 Apache 项目要小,帖子数量可能有些少,很难得到回复。

今年,创建了 Cassandra 的 DataStax 收购了创建 Titan 的 Aurelius。Titan 的创建者现在参与开发 DataStax 的 DSE 图数据库,这可能会对 Titan 的发展产生影响。话虽如此,0.9.x Titan 版本已经发布,预计会有 1.0 版本的发布。

因此,通过一个使用 Scala 和 Gremlin 的示例展示了 Titan 功能的一部分后,我将在此结束本章。我想展示基于 Spark 的图处理和图存储系统的配对。我喜欢开源系统的开发速度和可访问性。我并不是说 Titan 就是适合你的数据库,但它是一个很好的例子。如果它的未来能够得到保证,并且其社区不断壮大,那么随着其成熟,它可能会成为一个有价值的资源。

请注意,本章中使用了两个版本的 Spark:1.3 和 1.2.1。较早的版本是必需的,因为显然它是唯一与 Titan 的SparkGraphComputer兼容的版本,因此避免了 Kyro 序列化错误。

在下一章中,将从h2o.ai/ H2O 产品的角度,研究对 Apache Spark MLlib 机器学习库的扩展。将使用 Scala 开发一个基于神经网络的深度学习示例,以展示其潜在功能。

第七章:用 H2O 扩展 Spark

H2O 是一个由h2o.ai/开发的开源系统,用于机器学习。它提供了丰富的机器学习算法和基于 Web 的数据处理用户界面。它提供了使用多种语言开发的能力:Java、Scala、Python 和 R。它还具有与 Spark、HDFS、Amazon S3、SQL 和 NoSQL 数据库进行接口的能力。本章将集中讨论 H2O 与 Apache Spark 的集成,使用 H2O 的Sparkling Water组件。将使用 Scala 开发一个简单的示例,基于真实数据创建一个深度学习模型。本章将:

  • 检查 H2O 功能

  • 考虑必要的 Spark H2O 环境

  • 检查 Sparkling Water 架构

  • 介绍并使用 H2O Flow 界面

  • 通过示例介绍深度学习

  • 考虑性能调优

  • 检查数据质量

下一步将概述 H2O 功能和本章中将使用的 Sparkling Water 架构。

概述

由于本章只能检查和使用 H2O 功能的一小部分,我认为提供一个功能区域列表将是有用的。此列表取自h2o.ai/网站的h2o.ai/product/algorithms/,基于数据整理、建模和对结果模型进行评分:

过程 模型 评分工具
数据概要分析 广义线性模型(GLM) 预测
摘要统计 决策树 混淆矩阵
聚合、过滤、分箱和派生列 梯度提升(GBM) AUC
切片、对数变换和匿名化 K 均值 命中率
变量创建 异常检测 PCA 得分
PCA 深度学习 多模型评分
训练和验证抽样计划 朴素贝叶斯
网格搜索

下一节将解释本章中 Spark 和 H2O 示例使用的环境,并解释遇到的一些问题。

处理环境

如果你们中有人查看过我的基于 Web 的博客,或者阅读过我的第一本书《大数据简化》,你会发现我对大数据集成和大数据工具的连接很感兴趣。这些系统都不是独立存在的。数据将从上游开始,在 Spark 加上 H2O 中进行处理,然后结果将被存储,或者移动到 ETL 链中的下一步。根据这个想法,在这个示例中,我将使用 Cloudera CDH HDFS 进行存储,并从那里获取我的数据。我也可以很容易地使用 S3、SQL 或 NoSQL 数据库。

在开始本章的开发工作时,我安装并使用了 Cloudera CDH 4.1.3 集群。我还安装了各种 Spark 版本,并可供使用。它们如下:

  • 将 Spark 1.0 安装为 CentOS 服务

  • 下载并安装的 Spark 1.2 二进制文件

  • 从源快照构建的 Spark 1.3

我认为我会进行实验,看看哪些 Spark 和 Hadoop 的组合可以一起工作。我在h2o-release.s3.amazonaws.com/sparkling-water/master/98/index.html下载了 Sparkling Water 的 0.2.12-95 版本。我发现 1.0 版本的 Spark 与 H2O 一起工作,但缺少 Spark 库。许多基于 Sparkling Water 的示例中使用的一些功能是可用的。Spark 版本 1.2 和 1.3 导致出现以下错误:

15/04/25 17:43:06 ERROR netty.NettyTransport: failed to bind to /192.168.1.103:0, shutting down Netty transport
15/04/25 17:43:06 WARN util.Utils: Service 'sparkDriver' could not bind on port 0\. Attempting port 1.

尽管 Spark 中正确配置了主端口号,但没有被识别,因此 H2O 应用无法连接到 Spark。在与 H2O 的工作人员讨论了这个问题后,我决定升级到 H2O 认证版本的 Hadoop 和 Spark。应该使用的推荐系统版本可在h2o.ai/product/recommended-systems-for-h2o/上找到。

我使用 Cloudera Manager 界面的包管理页面将我的 CDH 集群从版本 5.1.3 升级到版本 5.3。这自动提供了 Spark 1.2——这个版本已经集成到 CDH 集群中。这解决了所有与 H2O 相关的问题,并为我提供了一个经过 H2O 认证的 Hadoop 和 Spark 环境。

安装 H2O

为了完整起见,我将向您展示如何下载、安装和使用 H2O。尽管我最终选择了版本 0.2.12-95,但我首先下载并使用了 0.2.12-92。本节基于早期的安装,但用于获取软件的方法是相同的。下载链接会随时间变化,因此请在h2o.ai/download/上关注 Sparkling Water 下载选项。

这将获取压缩的 Sparkling Water 发布,如下所示的 CentOS Linux 长文件列表:

[hadoop@hc2r1m2 h2o]$ pwd ; ls -l
/home/hadoop/h2o
total 15892
-rw-r--r-- 1 hadoop hadoop 16272364 Apr 11 12:37 sparkling-water-0.2.12-92.zip

这个压缩的发布文件使用 Linux 的unzip命令解压,得到一个 Sparkling Water 发布文件树:

[hadoop@hc2r1m2 h2o]$ unzip sparkling-water-0.2.12-92.zip

[hadoop@hc2r1m2 h2o]$ ls -d sparkling-water*
sparkling-water-0.2.12-92  sparkling-water-0.2.12-92.zip

我已将发布树移动到/usr/local/目录下,使用 root 账户,并创建了一个名为h2o的简单符号链接到发布版本。这意味着我的基于 H2O 的构建可以引用这个链接,并且不需要随着新版本的 Sparkling Water 的获取而更改。我还使用 Linux 的chmod命令确保我的开发账户 hadoop 可以访问发布版本。

[hadoop@hc2r1m2 h2o]$ su -
[root@hc2r1m2 ~]# cd /home/hadoop/h2o
[root@hc2r1m2 h2o]# mv sparkling-water-0.2.12-92 /usr/local
[root@hc2r1m2 h2o]# cd /usr/local

[root@hc2r1m2 local]# chown -R hadoop:hadoop sparkling-water-0.2.12-92
[root@hc2r1m2 local]#  ln –s sparkling-water-0.2.12-92 h2o

[root@hc2r1m2 local]# ls –lrt  | grep sparkling
total 52
drwxr-xr-x   6 hadoop hadoop 4096 Mar 28 02:27 sparkling-water-0.2.12-92
lrwxrwxrwx   1 root   root     25 Apr 11 12:43 h2o -> sparkling-water-0.2.12-92

发布已安装在我的 Hadoop CDH 集群的所有节点上。

构建环境

从过去的例子中,您会知道我偏爱 SBT 作为开发 Scala 源代码示例的构建工具。我已在 Linux CentOS 6.5 服务器上使用 hadoop 开发账户创建了一个名为hc2r1m2的开发环境。开发目录名为h2o_spark_1_2

[hadoop@hc2r1m2 h2o_spark_1_2]$ pwd
/home/hadoop/spark/h2o_spark_1_2

我的 SBT 构建配置文件名为h2o.sbt,位于这里;它包含以下内容:

[hadoop@hc2r1m2 h2o_spark_1_2]$ more h2o.sbt

name := "H 2 O"

version := "1.0"

scalaVersion := "2.10.4"

libraryDependencies += "org.apache.hadoop" % "hadoop-client" % "2.3.0"

libraryDependencies += "org.apache.spark" % "spark-core"  % "1.2.0" from "file:///opt/cloudera/parcels/CDH-5.3.3-1.cdh5.3.3.p0.5/jars/spark-assembly-1.2.0-cdh5.3.3-hadoop2.5.0-cdh5.3.3.jar"

libraryDependencies += "org.apache.spark" % "mllib"  % "1.2.0" from "file:///opt/cloudera/parcels/CDH-5.3-1.cdh5.3.3.p0.5/jars/spark-assembly-1.2.0-cdh5.3.3-hadoop2.5.0-cdh5.3.3.jar"

libraryDependencies += "org.apache.spark" % "sql"  % "1.2.0" from "file:///opt/cloudera/parcels/CDH-5.3.3-1.cdh5.3.3.p0.5/jars/spark-assembly-1.2.0-cdh5.3.3-hadoop2.5.0-cdh5.3.3.jar"

libraryDependencies += "org.apache.spark" % "h2o"  % "0.2.12-95" from "file:///usr/local/h2o/assembly/build/libs/sparkling-water-assembly-0.2.12-95-all.jar"

libraryDependencies += "hex.deeplearning" % "DeepLearningModel"  % "0.2.12-95" from "file:///usr/local/h2o/assembly/build/libs/sparkling-water-assembly-0.2.12-95-all.jar"

libraryDependencies += "hex" % "ModelMetricsBinomial"  % "0.2.12-95" from "file:///usr/local/h2o/assembly/build/libs/sparkling-water-assembly-0.2.12-95-all.jar"

libraryDependencies += "water" % "Key"  % "0.2.12-95" from "file:///usr/local/h2o/assembly/build/libs/sparkling-water-assembly-0.2.12-95-all.jar"

libraryDependencies += "water" % "fvec"  % "0.2.12-95" from "file:///usr/local/h2o/assembly/build/libs/sparkling-water-assembly-0.2.12-95-all.jar"

我在之前的章节中提供了 SBT 配置示例,所以我不会在这里逐行详细介绍。我使用基于文件的 URL 来定义库依赖,并从 Cloudera parcel 路径获取 CDH 安装的 Hadoop JAR 文件。Sparkling Water JAR 路径被定义为/usr/local/h2o/,这刚刚创建。

我在这个开发目录中使用一个名为run_h2o.bash的 Bash 脚本来执行基于 H2O 的示例代码。它将应用程序类名作为参数,并如下所示:

[hadoop@hc2r1m2 h2o_spark_1_2]$ more run_h2o.bash

#!/bin/bash

SPARK_HOME=/opt/cloudera/parcels/CDH
SPARK_LIB=$SPARK_HOME/lib
SPARK_BIN=$SPARK_HOME/bin
SPARK_SBIN=$SPARK_HOME/sbin
SPARK_JAR=$SPARK_LIB/spark-assembly-1.2.0-cdh5.3.3-hadoop2.5.0-cdh5.3.3.jar

H2O_PATH=/usr/local/h2o/assembly/build/libs
H2O_JAR=$H2O_PATH/sparkling-water-assembly-0.2.12-95-all.jar

PATH=$SPARK_BIN:$PATH
PATH=$SPARK_SBIN:$PATH
export PATH

cd $SPARK_BIN

./spark-submit \
 --class $1 \
 --master spark://hc2nn.semtech-solutions.co.nz:7077  \
 --executor-memory 85m \
 --total-executor-cores 50 \
 --jars $H2O_JAR \
 /home/hadoop/spark/h2o_spark_1_2/target/scala-2.10/h-2-o_2.10-1.0.jar

这个 Spark 应用程序提交的示例已经涵盖过了,所以我不会详细介绍。将执行器内存设置为正确的值对避免内存不足问题和性能问题至关重要。这将在性能调优部分进行讨论。

与之前的例子一样,应用 Scala 代码位于development目录级别下的src/main/scala子目录中。下一节将检查 Apache Spark 和 H2O 的架构。

架构

本节中的图表来自h2o.ai/网站,网址为[h2o.ai/blog/2014/09/how-sparkling-water-brings-h2o-to-spark/](http:// http://h2o.ai/blog/2014/09/how-sparkling-water-brings-h2o-to-spark/),以清晰地描述 H2O Sparkling Water 如何扩展 Apache Spark 的功能。H2O 和 Spark 都是开源系统。Spark MLlib 包含大量功能,而 H2O 通过一系列额外的功能扩展了这一点,包括深度学习。它提供了用于转换(转换)、建模和评分数据的工具。它还提供了一个基于 Web 的用户界面进行交互。

下一个图表,来自h2o.ai/,显示了 H2O 如何与 Spark 集成。正如我们已经知道的,Spark 有主服务器和工作服务器;工作服务器创建执行器来执行实际工作。运行基于 Sparkling water 的应用程序发生以下步骤:

  1. Spark 的submit命令将闪亮的水 JAR 发送到 Spark 主服务器。

  2. Spark 主服务器启动工作服务器,并分发 JAR 文件。

  3. Spark 工作程序启动执行器 JVM 来执行工作。

  4. Spark 执行器启动 H2O 实例。

H2O 实例嵌入了 Executor JVM,因此它与 Spark 共享 JVM 堆空间。当所有 H2O 实例都启动时,H2O 形成一个集群,然后 H2O 流 Web 界面可用。

架构

上图解释了 H2O 如何适应 Apache Spark 架构,以及它是如何启动的,但是数据共享呢?数据如何在 Spark 和 H2O 之间传递?下图解释了这一点:

架构

为 H2O 和 Sparkling Water 创建了一个新的 H2O RDD 数据结构。它是一个层,位于 H2O 框架的顶部,其中的每一列代表一个数据项,并且独立压缩以提供最佳的压缩比。

在本章后面呈现的深度学习示例中,您将看到已经从 Spark 模式 RDD 和列数据项隐式创建了一个数据框,并且收入已被枚举。我现在不会详细解释这一点,因为稍后会解释,但这是上述架构的一个实际示例:

  val testFrame:DataFrame = schemaRddTest
  testFrame.replace( testFrame.find("income"), testFrame.vec("income").toEnum)

在本章中将处理的基于 Scala 的示例中,将发生以下操作:

  1. 数据来自 HDFS,并存储在 Spark RDD 中。

  2. Spark SQL 用于过滤数据。

  3. Spark 模式 RDD 转换为 H2O RDD。

  4. 基于 H2O 的处理和建模正在进行。

  5. 结果被传递回 Spark 进行准确性检查。

到目前为止,已经检查了 H2O 的一般架构,并且已经获取了用于使用的产品。已经解释了开发环境,并且已经考虑了 H2O 和 Spark 集成的过程。现在,是时候深入了解 H2O 的实际用法了。不过,首先必须获取一些真实世界的数据用于建模。

数据来源

自从我已经在第二章中使用了人工神经网络ANN)功能,Apache Spark MLlib,来对图像进行分类,似乎只有使用 H2O 深度学习来对本章中的数据进行分类才合适。为了做到这一点,我需要获取适合分类的数据集。我需要包含图像标签的图像数据,或者包含向量和标签的数据,以便我可以强制 H2O 使用其分类算法。

MNIST 测试和训练图像数据来自ann.lecun.com/exdb/mnist/。它包含 50,000 个训练行和 10,000 个测试行。它包含数字 0 到 9 的数字图像和相关标签。

在撰写本文时,我无法使用这些数据,因为 H2O Sparkling water 中存在一个 bug,限制了记录大小为 128 个元素。MNIST 数据的记录大小为28 x 28 + 1,包括图像和标签:

15/05/14 14:05:27 WARN TaskSetManager: Lost task 0.0 in stage 9.0 (TID 256, hc2r1m4.semtech-solutions.co.nz): java.lang.ArrayIndexOutOfBoundsException: -128

在您阅读此文时,这个问题应该已经得到解决并发布,但在短期内,我从www.cs.toronto.edu/~delve/data/datasets.html获取了另一个名为 income 的数据集,其中包含了加拿大雇员的收入数据。以下信息显示了属性和数据量。它还显示了数据中的列列表和一行样本数据:

Number of attributes: 16
Number of cases: 45,225

age workclass fnlwgt education educational-num marital-status occupation relationship race gender capital-gain capital-loss hours-per-week native-country income

39, State-gov, 77516, Bachelors, 13, Never-married, Adm-clerical, Not-in-family, White, Male, 2174, 0, 40, United-States, <=50K

我将枚举数据中的最后一列——收入等级,所以<=50k将枚举为0。这将允许我强制 H2O 深度学习算法进行分类而不是回归。我还将使用 Spark SQL 来限制数据列,并过滤数据。

数据质量在创建本章描述的基于 H2O 的示例时至关重要。下一节将探讨可以采取的步骤来改善数据质量,从而节省时间。

数据质量

当我将 HDFS 中的 CSV 数据文件导入到我的 Spark Scala H2O 示例代码时,我可以过滤传入的数据。以下示例代码包含两行过滤器;第一行检查数据行是否为空,而第二行检查每个数据行中的最后一列(收入)是否为空:

val testRDD  = rawTestData
  .filter(!_.isEmpty)
  .map(_.split(","))
  .filter( rawRow => ! rawRow(14).trim.isEmpty )

我还需要清理原始数据。有两个数据集,一个用于训练,一个用于测试。训练和测试数据必须具备以下特点:

  • 相同数量的列

  • 相同的数据类型

  • 代码中必须允许空值

  • 枚举类型的值必须匹配——尤其是标签

我遇到了与枚举标签列收入及其包含的值相关的错误。我发现我的测试数据集行以句点字符“。”结尾。处理时,这导致训练和测试数据的值在枚举时不匹配。

因此,我认为应该花费时间和精力来保障数据质量,作为训练和测试机器学习功能的预备步骤,以免浪费时间和产生额外成本。

性能调优

如果在 Spark 网络用户界面中看到以下错误,就需要监控 Spark 应用程序错误和标准输出日志:

05-15 13:55:38.176 192.168.1.105:54321   6375   Thread-10 ERRR: Out of Memory and no swap space left from hc2r1m1.semtech-solutions.co.nz/192.168.1.105:54321

如果您遇到应用执行器似乎没有响应的情况,可能需要调整执行器内存。如果您在执行器日志中看到以下错误,就需要这样做:

05-19 13:46:57.300 192.168.1.105:54321   10044  Thread-11 WARN: Unblock allocations; cache emptied but memory is low:  OOM but cache is emptied:  MEM_MAX = 89.5 MB, DESIRED_CACHE = 96.4 MB, CACHE = N/A, POJO = N/A, this request bytes = 36.4 MB

这可能会导致循环,因为应用程序请求的内存超过了可用内存,因此会等待下一次迭代重试。应用程序似乎会挂起,直到执行器被终止,并在备用节点上重新执行任务。由于这些问题,短任务的运行时间可能会大大延长。

监控 Spark 日志以查找这些类型的错误。在前面的示例中,更改spark-submit命令中的执行器内存设置可以消除错误,并大大减少运行时间。所请求的内存值已经降低到低于可用内存的水平。

 --executor-memory 85m

深度学习

神经网络在第二章中介绍,Apache Spark MLlib。本章在此基础上介绍了深度学习,它使用深度神经网络。这些是功能丰富的神经网络,包含额外的隐藏层,因此它们提取数据特征的能力增强。这些网络通常是前馈网络,其中特征特性是输入到输入层神经元的输入。然后这些神经元激活并将激活传播到隐藏层神经元,最终到输出层,应该呈现特征标签值。然后通过网络(至少在反向传播中)传播输出中的错误,调整神经元连接权重矩阵,以便在训练期间减少分类错误。

深度学习

H2O 手册中描述的前面的示例图显示了一个深度学习网络,左侧有四个输入神经元,中间有两个隐藏层,右侧有两个输出神经元。箭头显示了神经元之间的连接以及激活通过网络的方向。

这些网络功能丰富,因为它们提供以下选项:

  • 多种训练算法

  • 自动网络配置

  • 能够配置许多选项

  • 结构

隐藏层结构

  • 训练

学习率、退火和动量

因此,在对深度学习进行简要介绍之后,现在是时候看一些基于 Scala 的示例代码了。H2O 提供了大量的功能;构建和运行网络所需的类已经为您开发好了。您只需要做以下事情:

  • 准备数据和参数

  • 创建和训练模型

  • 使用第二个数据集验证模型

  • 对验证数据集输出进行评分

在评分模型时,您必须希望以百分比形式获得高值。您的模型必须能够准确预测和分类您的数据。

示例代码 - 收入

本节将使用之前的加拿大收入数据源,检查基于 Scala 的 H2O Sparkling Water 深度学习示例。首先,导入了 Spark(ContextConfmllibRDD)和 H2O(h2odeeplearningwater)类:

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf

import hex.deeplearning.{DeepLearningModel, DeepLearning}
import hex.deeplearning.DeepLearningModel.DeepLearningParameters
import org.apache.spark.h2o._
import org.apache.spark.mllib
import org.apache.spark.mllib.feature.{IDFModel, IDF, HashingTF}
import org.apache.spark.rdd.RDD
import water.Key

接下来定义了一个名为h2o_spark_dl2的应用程序类,创建了主 URL,然后基于此 URL 创建了一个配置对象和应用程序名称。然后使用配置对象创建 Spark 上下文:

object h2o_spark_dl2  extends App
{
  val sparkMaster = "spark://hc2nn.semtech-solutions.co.nz:7077"
  val appName = "Spark h2o ex1"
  val conf = new SparkConf()

  conf.setMaster(sparkMaster)
  conf.setAppName(appName)

  val sparkCxt = new SparkContext(conf)

从 Spark 上下文创建 H2O 上下文,还有一个 SQL 上下文:

  import org.apache.spark.h2o._
  implicit val h2oContext = new org.apache.spark.h2o.H2OContext(sparkCxt).start()

  import h2oContext._
  import org.apache.spark.sql._

  implicit val sqlContext = new SQLContext(sparkCxt)

使用openFlow命令启动 H2O Flow 用户界面:

  import sqlContext._
  openFlow

现在定义了数据文件的训练和测试(在 HDFS 上)使用服务器 URL、路径和文件名:

  val server    = "hdfs://hc2nn.semtech-solutions.co.nz:8020"
  val path      = "/data/spark/h2o/"

  val train_csv =  server + path + "adult.train.data" // 32,562 rows
  val test_csv  =  server + path + "adult.test.data"  // 16,283 rows

使用 Spark 上下文的textFile方法加载基于 CSV 的训练和测试数据:

  val rawTrainData = sparkCxt.textFile(train_csv)
  val rawTestData  = sparkCxt.textFile(test_csv)

现在,模式是根据属性字符串定义的。然后,通过使用一系列StructField,基于每一列拆分字符串,创建了一个模式变量。数据类型保留为字符串,true 值允许数据中的空值:

  val schemaString = "age workclass fnlwgt education “ + 
“educationalnum maritalstatus " + "occupation relationship race 
gender “ + “capitalgain capitalloss " + hoursperweek nativecountry income"

  val schema = StructType( schemaString.split(" ")
      .map(fieldName => StructField(fieldName, StringType, true)))

原始 CSV 行“训练”和测试数据现在通过逗号分割成列。数据被过滤以确保最后一列(“收入”)不为空。实际数据行是从原始 CSV 数据中的十五个(0-14)修剪的元素创建的。训练和测试数据集都经过处理:

  val trainRDD  = rawTrainData
         .filter(!_.isEmpty)
         .map(_.split(","))
         .filter( rawRow => ! rawRow(14).trim.isEmpty )
         .map(rawRow => Row(
               rawRow(0).toString.trim,  rawRow(1).toString.trim,
               rawRow(2).toString.trim,  rawRow(3).toString.trim,
               rawRow(4).toString.trim,  rawRow(5).toString.trim,
               rawRow(6).toString.trim,  rawRow(7).toString.trim,
               rawRow(8).toString.trim,  rawRow(9).toString.trim,
               rawRow(10).toString.trim, rawRow(11).toString.trim,
               rawRow(12).toString.trim, rawRow(13).toString.trim,
               rawRow(14).toString.trim
                           )
             )

  val testRDD  = rawTestData
         .filter(!_.isEmpty)
         .map(_.split(","))
         .filter( rawRow => ! rawRow(14).trim.isEmpty )
         .map(rawRow => Row(
               rawRow(0).toString.trim,  rawRow(1).toString.trim,
               rawRow(2).toString.trim,  rawRow(3).toString.trim,
               rawRow(4).toString.trim,  rawRow(5).toString.trim,
               rawRow(6).toString.trim,  rawRow(7).toString.trim,
               rawRow(8).toString.trim,  rawRow(9).toString.trim,
               rawRow(10).toString.trim, rawRow(11).toString.trim,
               rawRow(12).toString.trim, rawRow(13).toString.trim,
               rawRow(14).toString.trim
                           )
             )

现在使用 Spark 上下文的applySchema方法,为训练和测试数据集创建了 Spark Schema RDD 变量:

  val trainSchemaRDD = sqlContext.applySchema(trainRDD, schema)
  val testSchemaRDD  = sqlContext.applySchema(testRDD,  schema)

为训练和测试数据创建临时表:

  trainSchemaRDD.registerTempTable("trainingTable")
  testSchemaRDD.registerTempTable("testingTable")

现在,对这些临时表运行 SQL,既可以过滤列的数量,也可以潜在地限制数据。我可以添加WHERELIMIT子句。这是一个有用的方法,使我能够操纵基于列和行的数据:

  val schemaRddTrain = sqlContext.sql(
    """SELECT
         |age,workclass,education,maritalstatus,
         |occupation,relationship,race,
         |gender,hoursperweek,nativecountry,income
         |FROM trainingTable """.stripMargin)

  val schemaRddTest = sqlContext.sql(
    """SELECT
         |age,workclass,education,maritalstatus,
         |occupation,relationship,race,
         |gender,hoursperweek,nativecountry,income
         |FROM testingTable """.stripMargin)

现在从数据中创建了 H2O 数据框。每个数据集中的最后一列(收入)是枚举的,因为这是将用于数据的深度学习标签的列。此外,枚举此列会强制深度学习模型进行分类而不是回归:

  val trainFrame:DataFrame = schemaRddTrain
  trainFrame.replace( trainFrame.find("income"),        trainFrame.vec("income").toEnum)
  trainFrame.update(null)

  val testFrame:DataFrame = schemaRddTest
  testFrame.replace( testFrame.find("income"),        testFrame.vec("income").toEnum)
  testFrame.update(null)

现在保存了枚举结果数据收入列,以便可以使用该列中的值对测试模型预测值进行评分:

  val testResArray = schemaRddTest.collect()
  val sizeResults  = testResArray.length
  var resArray     = new ArrayDouble

  for ( i <- 0 to ( resArray.length - 1)) {
     resArray(i) = testFrame.vec("income").at(i)
  }

现在,深度学习模型参数已经设置好,包括迭代次数(或迭代次数)-用于训练和验证的数据集以及标签列收入,这将用于对数据进行分类。此外,我们选择使用变量重要性来确定数据中哪些数据列最重要。然后创建深度学习模型:

  val dlParams = new DeepLearningParameters()

  dlParams._epochs               = 100
  dlParams._train                = trainFrame
  dlParams._valid                = testFrame
  dlParams._response_column      = 'income
  dlParams._variable_importances = true
  val dl = new DeepLearning(dlParams)
  val dlModel = dl.trainModel.get

然后对模型进行针对测试数据集的评分,进行预测,这些收入预测值与先前存储的枚举测试数据收入值进行比较。最后,从测试数据中输出准确率百分比:

  val testH2oPredict  = dlModel.score(schemaRddTest )('predict)
  val testPredictions  = toRDDDoubleHolder
          .collect.map(_.result.getOrElse(Double.NaN))
  var resAccuracy = 0
  for ( i <- 0 to ( resArray.length - 1)) {
    if (  resArray(i) == testPredictions(i) )
      resAccuracy = resAccuracy + 1
  }

  println()
  println( ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" )
  println( ">>>>>> Model Test Accuracy = "
       + 100*resAccuracy / resArray.length  + " % " )
  println( ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" )
  println()

在最后一步中,应用程序被停止,通过shutdown调用终止 H2O 功能,然后停止 Spark 上下文:

  water.H2O.shutdown()
  sparkCxt.stop()

  println( " >>>>> Script Finished <<<<< " )

} // end application

基于训练数据集的 32,000 条记录和测试数据集的 16,000 条收入记录,这个深度学习模型非常准确。它达到了83%的准确度水平,这对于几行代码、小数据集和仅 100 个迭代次数来说是令人印象深刻的,如运行输出所示:

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>> Model Test Accuracy = 83 %
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

在下一节中,我将检查处理 MNIST 数据所需的一些编码,尽管由于编码时的 H2O 限制,该示例无法完成。

示例代码-MNIST

由于 MNIST 图像数据记录非常庞大,在创建 Spark SQL 模式和处理数据记录时会出现问题。此数据中的记录以 CSV 格式形成,并由 28 x 28 数字图像组成。然后,每行以图像的标签值终止。我通过定义一个函数来创建表示记录的模式字符串,然后调用它来创建我的模式:

  def getSchema(): String = {

    var schema = ""
    val limit = 28*28

    for (i <- 1 to limit){
      schema += "P" + i.toString + " "
    }
    schema += "Label"

    schema // return value
  }

  val schemaString = getSchema()
  val schema = StructType( schemaString.split(" ")
      .map(fieldName => StructField(fieldName, IntegerType, false)))

与先前的示例一样,可以采用与深度学习相同的一般方法来处理数据,除了实际处理原始 CSV 数据。有太多列需要单独处理,并且它们都需要转换为整数以表示它们的数据类型。可以通过两种方式之一来完成。在第一个示例中,可以使用var args来处理行中的所有元素:

val trainRDD  = rawTrainData.map( rawRow => Row( rawRow.split(",").map(_.toInt): _* ))

第二个示例使用fromSeq方法来处理行元素:

  val trainRDD  = rawTrainData.map(rawRow => Row.fromSeq(rawRow.split(",") .map(_.toInt)))

在下一节中,将检查 H2O Flow 用户界面,以了解如何使用它来监视 H2O 并处理数据。

H2O 流

H2O Flow 是 H2O 的基于 Web 的开源用户界面,并且由于它与 Spark 一起使用,因此也可以使用 Sparkling Water。这是一个完全功能的 H2O Web 界面,用于监视 H2O Sparkling Water 集群和作业,以及操作数据和训练模型。我已经创建了一些简单的示例代码来启动 H2O 界面。与之前基于 Scala 的代码示例一样,我所需要做的就是创建一个 Spark,一个 H2O 上下文,然后调用openFlow命令,这将启动 Flow 界面。

以下 Scala 代码示例仅导入了用于 Spark 上下文、配置和 H2O 的类。然后根据应用程序名称和 Spark 集群 URL 定义配置。然后使用配置对象创建 Spark 上下文:

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf
import org.apache.spark.h2o._

object h2o_spark_ex2  extends App
{
  val sparkMaster = "spark://hc2nn.semtech-solutions.co.nz:7077"
  val appName = "Spark h2o ex2"
  val conf = new SparkConf()

  conf.setMaster(sparkMaster)
  conf.setAppName(appName)

  val sparkCxt = new SparkContext(conf)

然后创建了一个 H2O 上下文,并使用 Spark 上下文启动了它。导入了 H2O 上下文类,并使用openFlow命令启动了 Flow 用户界面:

  implicit val h2oContext = new org.apache.spark.h2o.H2OContext(sparkCxt).start()

  import h2oContext._

  // Open H2O UI

  openFlow

请注意,为了让我能够使用 Flow 应用程序,我已经注释掉了 H2O 关闭和 Spark 上下文停止选项。我通常不会这样做,但我想让这个应用程序长时间运行,这样我就有足够的时间使用界面:

  // shutdown h20

//  water.H2O.shutdown()
//  sparkCxt.stop()

  println( " >>>>> Script Finished <<<<< " )

} // end application

我使用我的 Bash 脚本run_h2o.bash,并将应用程序类名称为h2o_spark_ex2作为参数。这个脚本包含对spark-submit命令的调用,它将执行编译后的应用程序:

[hadoop@hc2r1m2 h2o_spark_1_2]$ ./run_h2o.bash h2o_spark_ex2

当应用程序运行时,它会列出 H2O 集群的状态,并提供一个 URL,通过该 URL 可以访问 H2O Flow 浏览器:

15/05/20 13:00:21 INFO H2OContext: Sparkling Water started, status of context:
Sparkling Water Context:
 * number of executors: 4
 * list of used executors:
 (executorId, host, port)
 ------------------------
 (1,hc2r1m4.semtech-solutions.co.nz,54321)
 (3,hc2r1m2.semtech-solutions.co.nz,54321)
 (0,hc2r1m3.semtech-solutions.co.nz,54321)
 (2,hc2r1m1.semtech-solutions.co.nz,54321)
 ------------------------

 Open H2O Flow in browser: http://192.168.1.108:54323 (CMD + click in Mac OSX)

前面的例子表明,我可以使用主机 IP 地址192.168.1.108上的端口号54323访问 H2O 界面。我可以简单地检查我的主机文件,确认主机名是hc2r1m2

[hadoop@hc2nn ~]$ cat /etc/hosts | grep hc2
192.168.1.103 hc2nn.semtech-solutions.co.nz   hc2nn
192.168.1.105 hc2r1m1.semtech-solutions.co.nz   hc2r1m1
192.168.1.108 hc2r1m2.semtech-solutions.co.nz   hc2r1m2
192.168.1.109 hc2r1m3.semtech-solutions.co.nz   hc2r1m3
192.168.1.110 hc2r1m4.semtech-solutions.co.nz   hc2r1m4

因此,我可以使用hc2r1m2:54323的 URL 访问界面。下面的截图显示了 Flow 界面没有加载数据。页面顶部有数据处理和管理菜单选项和按钮。右侧有帮助选项,让您可以更多地了解 H2O:

H2O Flow

以下截图更详细地显示了菜单选项和按钮。在接下来的章节中,我将使用一个实际的例子来解释其中一些选项,但在本章中没有足够的空间来涵盖所有的功能。请查看h2o.ai/网站,详细了解 Flow 应用程序,可在h2o.ai/product/flow/找到:

H2O Flow

更详细地说,前面的菜单选项和按钮允许您管理您的 H2O Spark 集群,并操纵您希望处理的数据。下面的截图显示了可用的帮助选项的重新格式化列表,这样,如果遇到问题,您可以在同一个界面上调查解决问题:

H2O Flow

如果我使用菜单选项Admin | Cluster Status,我将获得以下截图,显示了每个集群服务器的内存、磁盘、负载和核心状态。这是一个有用的快照,为我提供了状态的彩色指示:

H2O Flow

菜单选项Admin | Jobs提供了当前集群作业的详细信息,包括开始、结束和运行时间,以及状态。单击作业名称会提供更多详细信息,包括数据处理细节和估计的运行时间,这是很有用的。此外,如果选择Refresh按钮,显示将持续刷新,直到取消选择为止:

H2O Flow

Admin | Water Meter选项提供了集群中每个节点的 CPU 使用情况的可视化显示。如下截图所示,我的仪表显示我的集群处于空闲状态:

H2O Flow

使用菜单选项Flow | Upload File,我已经上传了之前基于 Scala 的深度学习示例中使用的一些训练数据。数据已加载到数据预览窗格中;我可以看到数据的样本已经组织成单元格。还对数据类型进行了准确的猜测,这样我就可以看到哪些列可以被列举。如果我想考虑分类,这是很有用的:

H2O Flow

加载完数据后,我现在看到了一个Frame显示,它让我能够查看、检查、构建模型、创建预测或下载数据。数据显示了最小值、最大值和平均值等信息。它显示了数据类型、标签和零数据计数,如下截图所示:

H2O Flow

我认为基于这些数据创建深度学习分类模型,以比较基于 Scala 的方法和 H2O 用户界面会很有用。使用查看和检查选项,可以直观地交互式地检查数据,并创建与数据相关的图表。例如,使用先前的检查选项,然后选择绘制列选项,我能够创建一个数据标签与列数据中零计数的图表。以下截图显示了结果:

H2O Flow

通过选择构建模型选项,会提供一个菜单选项,让我选择模型类型。我将选择深度学习,因为我已经知道这些数据适合这种分类方法。先前基于 Scala 的模型的准确度达到了 83%:

H2O Flow

我选择了深度学习选项。选择了这个选项后,我可以设置模型参数,如训练和验证数据集,以及选择模型应该使用的数据列(显然,两个数据集应该包含相同的列)。以下截图显示了被选择的数据集和模型列:

H2O Flow

有大量基本和高级模型选项可供选择。其中一些显示在以下截图中。我已将响应列设置为 15 作为收入列。我还设置了VARIABLE_IMPORTANCES选项。请注意,我不需要枚举响应列,因为它已经自动完成了:

H2O Flow

还要注意,迭代选项设置为100与之前一样。此外,隐藏层的200,200表示网络有两个隐藏层,每个隐藏层有 200 个神经元。选择构建模型选项会根据这些参数创建模型。以下截图显示了正在训练的模型,包括训练时间的估计和迄今为止处理的数据的指示。

H2O Flow

一旦训练完成,查看模型会显示训练和验证指标,以及重要训练参数的列表:

H2O Flow

选择预测选项可以指定另一个验证数据集。使用新数据集选择预测选项会导致已经训练的模型针对新的测试数据集进行验证:

H2O Flow

选择预测选项会导致深度学习模型和数据集的预测细节显示如下截图所示:

H2O Flow

前面的截图显示了测试数据框架和模型类别,以及 AUC、GINI 和 MSE 的验证统计数据。

AUC 值,即曲线下面积,与 ROC 曲线相关,ROC 曲线也显示在以下截图中。TPR 表示真正率,FPR 表示假正率。AUC 是一个准确度的度量,值为 1 表示完美。因此,蓝线显示的准确度比红线高:

H2O Flow

这个界面中有很多功能,我没有解释,但我希望我已经让您感受到了它的强大和潜力。您可以使用这个界面来检查数据,并在尝试开发代码之前创建报告,或者作为一个独立的应用程序来深入研究您的数据。

摘要

当我检查 Apache Hadoop 和 Spark 时,我的持续主题是,这些系统都不是独立的。它们需要集成在一起形成基于 ETL 的处理系统。数据需要在 Spark 中进行源和处理,然后传递到 ETL 链中的下一个链接,或者存储起来。我希望本章已经向您展示了,Spark 功能可以通过额外的库和 H2O 等系统进行扩展。

尽管 Apache Spark MLlib(机器学习库)具有许多功能,但 H2O Sparkling Water 和 Flow web 界面的组合提供了额外丰富的数据分析建模选项。使用 Flow,您还可以直观、交互式地处理数据。希望本章能向您展示,尽管无法涵盖 H2O 提供的所有内容,但 Spark 和 H2O 的组合扩大了您的数据处理可能性。

希望您觉得本章内容有用。作为下一步,您可以考虑查看h2o.ai/网站或 H2O Google 小组,该小组可在groups.google.com/forum/#!forum/h2ostream上找到。

下一章将审查基于 Spark 的服务databricks.com/,该服务将在云中使用 Amazon AWS 存储来创建 Spark 集群。

第八章:Spark Databricks

创建大数据分析集群,导入数据,创建 ETL 流以清洗和处理数据是困难且昂贵的。Databricks 的目标是降低复杂性,使集群创建和数据处理过程更加简单。他们创建了一个基于 Apache Spark 的云平台,自动化了集群创建,并简化了数据导入、处理和可视化。目前,存储基于 AWS,但未来他们计划扩展到其他云提供商。

设计 Apache Spark 的同一批人参与了 Databricks 系统。在撰写本书时,该服务只能通过注册访问。我获得了 30 天的试用期。在接下来的两章中,我将检查该服务及其组件,并提供一些示例代码来展示其工作原理。本章将涵盖以下主题:

  • 安装 Databricks

  • AWS 配置

  • 帐户管理

  • 菜单系统

  • 笔记本和文件夹

  • 通过库导入作业

  • 开发环境

  • Databricks 表

  • Databricks DbUtils 包

鉴于本书以静态格式提供,完全检查流式等功能将会很困难。

概述

Databricks 服务,可在databricks.com/网站上获得,基于集群的概念。这类似于 Spark 集群,在之前的章节中已经进行了检查和使用。它包含一个主节点、工作节点和执行器。但是,集群的配置和大小是自动化的,取决于您指定的内存量。诸如安全性、隔离、进程监控和资源管理等功能都会自动为您管理。如果您有一个短时间内需要使用 200GB 内存的基于 Spark 的集群,这项服务可以动态创建它,并处理您的数据。处理完成后,您可以终止集群以减少成本。

在集群中,引入了笔记本的概念,以及一个位置供您创建脚本和运行程序。可以在笔记本中创建基于 Scala、Python 或 SQL 的文件夹。可以创建作业来执行功能,并可以从笔记本代码或导入的库中调用。笔记本可以调用笔记本功能。此外,还提供了根据时间或事件安排作业的功能。

这为您提供了 Databricks 服务提供的感觉。接下来的章节将解释每个引入的主要项目。请记住,这里呈现的内容是新的并且正在发展。此外,我在这个演示中使用了 AWS US East (North Virginia)地区,因为亚洲悉尼地区目前存在限制,导致 Databricks 安装失败。

安装 Databricks

为了创建这个演示,我使用了 AWS 提供的一年免费访问,该访问可在aws.amazon.com/free/上获得。这有一些限制,比如 5GB 的 S3 存储和 750 小时的 Amazon Elastic Compute Cloud (EC2),但它让我以较低成本访问并减少了我的整体 EC2 成本。AWS 账户提供以下内容:

  • 帐户 ID

  • 一个访问密钥 ID

  • 一个秘密访问密钥

这些信息项目被 Databricks 用来访问您的 AWS 存储,安装 Databricks 系统,并创建您指定的集群组件。从安装开始,您就开始产生 AWS EC2 成本,因为 Databricks 系统使用至少两个运行实例而没有任何集群。一旦您成功输入了 AWS 和计费信息,您将被提示启动 Databricks 云。

安装 Databricks

完成这些操作后,您将获得一个 URL 来访问您的云、一个管理员账户和密码。这将允许您访问 Databricks 基于 Web 的用户界面,如下面的截图所示:

安装 Databricks

这是欢迎界面。它显示了图像顶部的菜单栏,从左到右依次包括菜单、搜索、帮助和账户图标。在使用系统时,还可能有一个显示最近活动的时钟图标。通过这个单一界面,您可以在创建自己的集群和代码之前搜索帮助屏幕和使用示例。

AWS 计费

请注意,一旦安装了 Databricks 系统,您将开始产生 AWS EC2 存储成本。Databricks 试图通过保持 EC2 资源活动来最小化您的成本,以便进行完整的计费周期。例如,如果终止 Databricks 集群,基于集群的 EC2 实例仍将存在于 AWS 为其计费的一个小时内。通过这种方式,如果您创建一个新的集群,Databricks 可以重用它们。下面的截图显示,尽管我正在使用一个免费的 AWS 账户,并且我已经仔细减少了我的资源使用,但我在短时间内产生了 AWS EC2 成本:

AWS 计费

您需要了解您创建的 Databricks 集群,并了解,当它们存在并被使用时,将产生 AWS 成本。只保留您真正需要的集群,并终止其他任何集群。

为了检查 Databricks 数据导入功能,我还创建了一个 AWS S3 存储桶,并将数据文件上传到其中。这将在本章后面进行解释。

Databricks 菜单

通过选择 Databricks Web 界面上的左上角菜单图标,可以展开菜单系统。下面的截图显示了顶级菜单选项,以及工作区选项,展开到/folder1/folder2/的文件夹层次结构。最后,它显示了可以在folder2上执行的操作,即创建一个笔记本、创建一个仪表板等。

Databricks 菜单

所有这些操作将在以后的章节中扩展。下一节将介绍账户管理,然后转到集群。

账户管理

在 Databricks 中,账户管理非常简化。有一个默认的管理员账户,可以创建后续账户,但您需要知道管理员密码才能这样做。密码需要超过八个字符;它们应该包含至少一个数字、一个大写字母和一个非字母数字字符。账户选项可以从右上角的菜单选项中访问,如下面的截图所示:

账户管理

这也允许用户注销。通过选择账户设置,您可以更改密码。通过选择账户菜单选项,将生成一个账户列表。在那里,您将找到一个添加账户的选项,并且每个账户行都可以通过每个账户行上的X选项进行删除,如下面的截图所示:

账户管理

还可以从账户列表重置账户密码。选择添加账户选项会创建一个新的账户窗口,需要一个电子邮件地址、全名、管理员密码和用户密码。因此,如果您想创建一个新用户,您需要知道您的 Databricks 实例管理员密码。您还必须遵循新密码的规则,如下所示:

  • 至少八个字符

  • 必须包含 0-9 范围内的至少一个数字

  • 必须包含 A-Z 范围内的至少一个大写字母

  • 必须包含至少一个非字母数字字符:!@#$%账户管理

下一节将介绍集群菜单选项,并使您能够管理自己的 Databricks Spark 集群。

集群管理

选择集群菜单选项会提供您当前的 Databricks 集群及其状态的列表。当然,当前您还没有。选择添加集群选项允许您创建一个。请注意,您指定的内存量决定了您的集群的大小。创建具有单个主节点和工作节点的集群需要至少 54GB。对于每增加的 54GB,将添加一个工作节点。

集群管理

下面的截图是一个连接的图像,显示了一个名为semclust1的新集群正在创建中,处于Pending状态。在Pending状态下,集群没有仪表板,集群节点也无法访问。

集群管理

创建后,集群内存会被列出,并且其状态会从Pending变为Running。默认情况下会自动附加一个仪表板,并且可以访问 Spark 主节点和工作节点用户界面。这里需要注意的是,Databricks 会自动启动和管理集群进程。在显示的右侧还有一个Option列,提供了配置重启终止集群的能力,如下面的截图所示:

集群管理

通过重新配置集群,可以改变其大小。通过增加内存,可以增加工作节点。下面的截图显示了一个集群,创建时默认大小为 54GB,其内存扩展到了108GB。

集群管理

终止集群会将其删除,无法恢复。因此,您需要确保删除是正确的操作。在终止实际发生之前,Databricks 会提示您确认您的操作。

集群管理

创建和终止集群都需要时间。在终止期间,集群会被标记为橙色横幅,并显示终止状态,如下面的截图所示:

集群管理

请注意,前面截图中的集群类型显示为按需。创建集群时,可以选择一个名为使用竞价实例创建竞价集群的复选框。这些集群比按需集群更便宜,因为它们出价更低的 AWS 竞价。但是,它们启动可能比按需集群慢。

Spark 用户界面与您在非 Databricks Spark 集群上期望的一样。您可以检查工作节点、执行器、配置和日志文件。创建集群时,它们将被添加到您的集群列表中。其中一个集群将被用作运行仪表板的集群。可以通过使用创建仪表板集群选项来更改这一点。当您向集群添加库和笔记本时,集群详细信息条目将更新为添加的数量。

我现在唯一想说的关于 Databricks Spark 用户界面选项,因为它很熟悉,就是它显示了使用的 Spark 版本。下面的截图从主用户界面中提取,显示了正在使用的 Spark 版本(1.3.0)非常新。在撰写本文时,最新的 Apache Spark 版本是 1.3.1,日期为 2015 年 4 月 17 日。

集群管理

下一节将介绍 Databricks 笔记本和文件夹——如何创建它们以及它们的用途。

笔记本和文件夹

笔记本是一种特殊类型的 Databricks 文件夹,可用于创建 Spark 脚本。笔记本可以调用笔记本脚本来创建功能层次结构。创建时,必须指定笔记本的类型(Python、Scala 或 SQL),然后可以指定集群可以运行笔记本功能。下面的截图显示了笔记本的创建。

笔记本和文件夹

请注意,笔记本会话右侧的菜单选项允许更改笔记本的类型。下面的示例显示了 Python 笔记本可以更改为ScalaSQLMarkdown

笔记本和文件夹

请注意,Scala 笔记本无法更改为 Python,Python 笔记本也无法更改为 Scala。Python、Scala 和 SQL 这些术语作为开发语言是众所周知的,然而,Markdown是新的。Markdown 允许从文本中的格式化命令创建格式化文档。可以在forums.databricks.com/static/markdown/help.html找到一个简单的参考。

这意味着在创建脚本时,格式化的注释可以添加到笔记本会话中。笔记本进一步细分为单元格,其中包含要执行的命令。可以通过悬停在左上角并将其拖放到位置来在笔记本中移动单元格。可以在笔记本中的单元格列表中插入新单元格。

此外,在 Scala 或 Python 笔记本单元格中使用%sql命令允许使用 SQL 语法。通常,Shift + Enter的组合会导致笔记本或文件夹中的文本块被执行。使用%md命令允许在单元格内添加 Markdown 注释。还可以向笔记本单元格添加注释。在笔记本单元格的右上部分显示的菜单选项显示了注释以及最小化和最大化选项:

笔记本和文件夹

多个基于 Web 的会话可以共享一个笔记本。在笔记本中发生的操作将被填充到查看它的每个 Web 界面中。此外,Markdown 和注释选项可用于启用用户之间的通信,以帮助分布式组之间的交互式数据调查。

笔记本和文件夹

上面的屏幕截图显示了notebook1的笔记本会话的标题。它显示了笔记本名称和类型(Scala)。它还显示了将笔记本锁定以使其只读的选项,以及将其从其集群中分离的选项。下面的屏幕截图显示了在笔记本工作区内创建文件夹的过程:

笔记本和文件夹

工作区主菜单选项的下拉菜单中,可以创建一个文件夹,例如folder1。稍后的部分将描述此菜单中的其他选项。创建并选择后,从名为folder1的新文件夹的下拉菜单中,显示了与其关联的操作,如下面的屏幕截图所示:

笔记本和文件夹

因此,文件夹可以导出为 DBC 存档。它可以被锁定,或者克隆以创建副本。也可以重命名或删除。可以将项目导入其中;例如,稍后将通过示例解释文件。还可以在其中创建新的笔记本、仪表板、库和文件夹。

与文件夹一样,笔记本也有一组可能的操作。下面的屏幕截图显示了通过下拉菜单可用的操作,用于名为notebook1的笔记本,它当前附加到名为semclust1的运行集群。可以重命名、删除、锁定或克隆笔记本。还可以将其从当前集群中分离,或者如果它被分离,则可以附加它。还可以将笔记本导出到文件或 DBC 存档。

笔记本和文件夹

从文件夹导入选项,文件可以导入到文件夹中。下面的屏幕截图显示了如果选择此选项将调用的文件拖放选项窗口。可以将文件拖放到本地服务器上的上传窗格上,也可以单击该窗格以打开导航浏览器,以搜索要上传的文件。

笔记本和文件夹

需要上传的文件需要是特定类型。以下截图显示了支持的文件类型。这是从文件浏览器中浏览要上传的文件时拍摄的截图。这也是有道理的。支持的文件类型包括 Scala、SQL 和 Python;以及 DBC 存档和 JAR 文件库。

笔记本和文件夹

在离开这一部分之前,还应该注意到,可以拖放笔记本和文件夹来改变它们的位置。下一节将通过简单的示例来检查 Databricks 作业和库。

工作和图书馆

在 Databricks 中,可以导入 JAR 库并在集群上运行其中的类。我将创建一个非常简单的 Scala 代码片段,以在我的 Centos Linux 服务器上本地打印出斐波那契数列的前 100 个元素作为BigInt值。我将使用 SBT 将我的类编译成一个 JAR 文件,在本地运行以检查结果,然后在我的 Databricks 集群上运行以比较结果。代码如下所示:

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
import org.apache.spark.SparkConf

object db_ex1  extends App
{
  val appName = "Databricks example 1"
  val conf = new SparkConf()

  conf.setAppName(appName)

  val sparkCxt = new SparkContext(conf)

  var seed1:BigInt = 1
  var seed2:BigInt = 1
  val limit = 100
  var resultStr = seed1 + " " + seed2 + " "

  for( i <- 1 to limit ){

    val fib:BigInt = seed1 + seed2
    resultStr += fib.toString + " "

    seed1 = seed2
    seed2 = fib
  }

  println()
  println( "Result : " + resultStr )
  println()

  sparkCxt.stop()

} // end application

并不是最优雅的代码片段,也不是创建斐波那契数列的最佳方式,但我只是想要一个用于 Databricks 的示例 JAR 和类。在本地运行时,我得到了前 100 个项,如下所示(我已剪辑了这些数据以节省空间):

Result : 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 46368 75025 121393 196418 317811 514229 832040 1346269 2178309 3524578 5702887 9227465 14930352 24157817 39088169 63245986 102334155 165580141 267914296 433494437 701408733 1134903170 1836311903 2971215073 4807526976 7778742049 12586269025 20365011074 32951280099 53316291173

4660046610375530309 7540113804746346429 12200160415121876738 19740274219868223167 31940434634990099905 51680708854858323072 83621143489848422977 135301852344706746049 218922995834555169026 354224848179261915075 573147844013817084101 927372692193078999176

已创建的库名为data-bricks_2.10-1.0.jar。从我的文件夹菜单中,我可以使用下拉菜单选项创建一个新的库。这允许我指定库源为一个 JAR 文件,命名新库,并从我的本地服务器加载库 JAR 文件。以下截图显示了这个过程的一个例子:

工作和图书馆

创建库后,可以使用附加选项将其附加到名为semclust1的集群,即我的 Databricks 集群。以下截图显示了正在附加新库的过程:

工作和图书馆

在下面的例子中,通过在任务项目上选择jar选项创建了一个名为job2的作业。对于该作业,已加载了相同的 JAR 文件,并将类db_ex1分配到库中运行。集群已被指定为按需,这意味着将自动创建一个集群来运行作业。活动运行部分显示了作业在以下截图中的运行情况:

工作和图书馆

运行后,作业将移至显示的已完成运行部分。对于相同的作业,以下截图显示了它运行了47秒,是手动启动的,并且成功了。

工作和图书馆

通过在前面的截图中选择名为Run 1的运行,可以查看运行输出。以下截图显示了与本地运行相同的结果,显示了来自我的本地服务器执行的结果。我已剪辑输出文本以使其在此页面上呈现和阅读,但您可以看到输出是相同的。

工作和图书馆

因此,即使从这个非常简单的例子中,很明显可以远程开发应用程序,并将它们作为 JAR 文件加载到 Databricks 集群中以执行。然而,每次在 AWS EC2 存储上创建 Databricks 集群时,Spark URL 都会发生变化,因此应用程序不应该硬编码诸如 Spark 主 URL 之类的细节。Databricks 将自动设置 Spark URL。

以这种方式运行 JAR 文件类时,也可以定义类参数。作业可以被安排在特定时间运行,或定期运行。还可以指定作业超时和警报电子邮件地址。

开发环境

已经证明可以在 Scala、Python 或 SQL 的笔记本中创建脚本,但也可以使用诸如 IntelliJ 或 Eclipse 之类的 IDE 来开发代码。通过在开发环境中安装 SBT 插件,可以为 Databricks 环境开发代码。在我写这本书的时候,Databricks 的当前版本是 1.3.2d。在起始页面的新功能下的发布说明链接中包含了 IDE 集成的链接,即https://dbc-xxxxxxx-xxxx.cloud.databricks.com/#shell/1547

URL 将采用这种形式,以dbc开头的部分将更改以匹配您将创建的 Databricks 云的 URL。我不会在这里展开,而是留给您去调查。在下一节中,我将调查 Databricks 表数据处理功能。

Databricks 表

Databricks 的菜单选项允许您以表格形式存储数据,并附带模式。菜单选项允许您创建表格,并刷新表格列表,如下面的截图所示:

Databricks 表

数据导入

您可以通过数据导入创建表,并同时指定列名和类型的表结构。如果要导入的数据具有标题,则可以从中获取列名,尽管所有列类型都被假定为字符串。下面的截图显示了在创建表时可用的数据导入选项和表单的连接视图。导入文件位置选项包括S3DBFSJDBC文件

数据导入

前面的截图显示了选择了S3。为了浏览我的S3存储桶以将文件导入表中,我需要输入AWS Key IDSecret Access KeyAWS S3 Bucket Name。然后,我可以浏览、选择文件,并通过预览创建表。在下面的截图中,我选择了文件选项:

数据导入

我可以将要导入的文件拖放到下面截图中的上传框中,或者单击框以浏览本地服务器以选择要上传的文件。选择文件后,可以定义数据列分隔符,以及数据是否包含标题行。可以预览数据,并更改列名和数据类型。还可以指定新表名和文件类型。下面的截图显示了加载示例文件数据以创建名为shuttle的表:

数据导入

创建后,菜单表列表可以刷新,并且可以查看表模式以确认列名和类型。通过这种方式,还可以预览表数据的样本。现在可以从 SQL 会话中查看和访问表。下面的截图显示了使用show tables命令可见shuttle表:

数据导入

一旦导入,此表中的数据也可以通过 SQL 会话访问。下面的截图显示了一个简单的 SQL 会话语句,显示了从新的shuttle表中提取的数据:

数据导入

因此,这提供了从各种数据源导入多个表格,并创建复杂模式以通过列和行过滤和连接数据的手段,就像在传统的关系数据库中一样。它提供了一种熟悉的大数据处理方法。

本节描述了可以通过数据导入创建表的过程,但是如何通过编程方式创建表,或者创建外部对象作为表呢?接下来的部分将提供这种表管理方法的示例。

外部表

Databricks 允许您针对外部资源(如 AWS S3 文件或本地文件系统文件)创建表。在本节中,我将针对基于 S3 的存储桶、路径和一组文件创建外部表。我还将检查 AWS 中所需的权限和使用的访问策略。以下截图显示了一个名为dbawss3test2的 AWS S3 存储桶的创建。已授予所有人访问列表的权限。我并不建议您这样做,但请确保您的组可以访问您的存储桶。

外部表

此外,还添加了一个策略以帮助访问。在这种情况下,匿名用户已被授予对存储桶和子内容的只读访问权限。您可以创建一个更复杂的策略,以限制对您的组和各种文件的访问。以下截图显示了新策略:

外部表

有了访问策略和使用正确访问策略创建的存储桶,我现在可以创建文件夹并上传文件以供 Databricks 外部表使用。如下截图所示,我已经做到了。上传的文件以 CSV 文件格式有十列:

外部表

现在,AWS S3 资源已设置好,需要将其挂载到 Databricks,如下面基于 Scala 的示例所示。出于安全目的,我已从脚本中删除了我的 AWS 和秘密密钥。您的挂载目录将需要以/mnt和任何/字符开头,并且您的秘密密钥值将需要替换为%2F。使用dbutils.fs类来创建挂载,代码在一秒内执行,如下结果所示:

外部表

现在,可以使用基于笔记本的 SQL 会话针对此挂载路径和其中包含的文件创建外部表,如下截图所示。名为s3test1的表已针对挂载目录包含的文件创建,并指定逗号作为分隔符,以解析基于 CSV 的内容。

外部表

菜单选项现在显示s3test1表存在,如下截图所示。因此,应该可以针对此表运行一些 SQL:

外部表

我在基于 SQL 的笔记本会话中运行了一个SELECT语句,使用COUNT(*)函数从外部表中获取行数,如下截图所示。可以看到表包含14500行。

外部表

我现在将向基于 S3 的文件夹添加另一个文件。在这种情况下,它只是第一个文件的 CSV 格式副本,因此外部表中的行数应该加倍。以下截图显示了添加的文件:

外部表

对外部表运行相同的SELECT语句确实提供了29000行的加倍行数。以下截图显示了 SQL 语句和输出:

外部表

因此,在 Databricks 内部很容易创建外部表,并对动态更改的内容运行 SQL。文件结构需要是统一的,如果使用 AWS,则必须定义 S3 存储桶访问权限。下一节将检查 Databricks 提供的 DbUtils 包。

DbUtils 包

之前基于 Scala 的脚本使用了 DbUtils 包,并在最后一节中创建了挂载点,只使用了该包的一小部分功能。在本节中,我想介绍一些 DbUtils 包和Databricks 文件系统DBFS)的更多功能。在连接到 Databricks 集群的笔记本中,可以调用 DbUtils 包中的帮助选项,以了解其结构和功能。正如下面的截图所示,在 Scala 笔记本中执行dbutils.fs.help()可以提供有关 fsutils、cache 和基于挂载的功能的帮助:

DbUtils 包

也可以获取关于单个函数的帮助,就像之前截图中的文本所示。下面的截图中的示例解释了cacheTable函数,提供了描述性文本和带有参数和返回类型的示例函数调用:

DbUtils 包

下一节将简要介绍 DBFS,然后继续检查更多的dbutils功能。

Databricks 文件系统

可以使用dbfs:/*形式的 URL 访问 DBFS,并使用dbutils.fs中可用的函数。

Databricks file system

之前的截图显示了使用ls函数检查/mnt文件系统,然后显示挂载目录——s3datas3data1。这些是在之前的 Scala S3 挂载示例中创建的目录。

Dbutils fsutils

dbutils包中的fsutils函数组包括cpheadmkdirsmvputrm等函数。之前显示的帮助调用可以提供更多关于它们的信息。您可以使用mkdirs调用在 DBFS 上创建一个目录,如下所示。请注意,我在这个会话中在dbfs:/下创建了许多名为data*的目录。下面的例子创建了一个名为data2的目录:

Dbutils fsutils

之前的截图通过执行ls显示了 DBFS 上已经存在许多默认目录。例如,参见以下内容:

  • /tmp是一个临时区域

  • /mnt是远程目录的挂载点,即 S3

  • /user是一个用户存储区域,目前包含 Hive

  • /mount是一个空目录

  • /FileStore是用于存储表、JAR 和作业 JAR 的存储区域

  • /databricks-datasets是 Databricks 提供的数据集

接下来显示的dbutils复制命令允许将文件复制到 DBFS 位置。在这个例子中,external1.txt文件已经被复制到/data2目录,如下面的截图所示:

Dbutils fsutils

head函数可用于从 DBFS 文件的开头返回前 maxBytes 个字符。下面的例子显示了external1.txt文件的格式。这很有用,因为它告诉我这是一个 CSV 文件,因此告诉我如何处理它。

Dbutils fsutils

也可以在 DBFS 内部移动文件。下面的截图显示了使用mv命令将external1.txt文件从data2目录移动到名为data1的目录。然后使用ls命令确认移动。

Dbutils fsutils

最后,使用 remove 函数(rm)来删除刚刚移动的名为external1.txt的文件。以下的ls函数调用显示,该文件不再存在于data1目录中,因为在函数输出中没有FileInfo记录:

Dbutils fsutils

DbUtils 缓存

在 DbUtils 中的缓存功能提供了缓存(和取消缓存)表和文件到 DBFS 的方法。实际上,表也被保存为文件到名为/FileStore的 DBFS 目录。下面的截图显示了缓存功能是可用的:

DbUtils 缓存

DbUtils 挂载

挂载功能允许您挂载远程文件系统,刷新挂载,显示挂载详细信息,并卸载特定的已挂载目录。在前几节中已经给出了 S3 挂载的示例,所以我在这里不会重复了。以下截图显示了mounts函数的输出。s3datas3data1挂载是我创建的。根目录和数据集的另外两个挂载已经存在。挂载按MountInfo对象的顺序列出。我重新排列了文本,使其更有意义,并更好地呈现在页面上。

DbUtils 挂载

总结

本章介绍了 Databricks。它展示了如何访问该服务,以及它如何使用 AWS 资源。请记住,未来,发明 Databricks 的人计划支持其他基于云的平台,如 Microsoft Azure。我认为介绍 Databricks 很重要,因为参与 Apache Spark 开发的人也参与了这个系统。自然的发展似乎是 Hadoop,Spark,然后是 Databricks。

我将在下一章继续对 Databricks 进行调查,因为重要的功能,如可视化,尚未被审查。此外,Databricks 术语中尚未介绍的主要 Spark 功能模块称为 GraphX,流式处理,MLlib 和 SQL。在 Databricks 中使用这些模块处理真实数据有多容易?继续阅读以了解更多。

第九章:Databricks 可视化

本章是在第八章Spark Databricks中完成的工作的基础上继续研究基于 Apache Spark 的服务的功能databricks.com/。尽管我在本章中将使用基于 Scala 的代码示例,但我希望集中在 Databricks 功能上,而不是传统的 Spark 处理模块:MLlib、GraphX、Streaming 和 SQL。本章将解释以下 Databricks 领域:

  • 使用仪表板的数据可视化

  • 基于 RDD 的报告

  • 基于数据流的报告

  • Databricks Rest 接口

  • 使用 Databricks 移动数据

因此,本章将审查 Databricks 中通过报告和仪表板进行数据分析可视化的功能。它还将检查 REST 接口,因为我认为它是远程访问和集成目的的有用工具。最后,它将检查将数据和库移动到 Databricks 云实例的选项。

数据可视化

Databricks 提供了访问 S3 和基于本地文件系统的文件的工具。它提供了将数据导入表格的能力,如已经显示的。在上一章中,原始数据被导入到航天飞机表中,以提供可以针对其运行 SQL 的表格数据,以针对行和列进行过滤,允许数据进行排序,然后进行聚合。这非常有用,但当图像和报告呈现可以更容易和直观地解释的信息时,我们仍然在查看原始数据输出。

Databricks 提供了一个可视化界面,基于您的 SQL 会话产生的表格结果数据。以下截图显示了一些已经运行的 SQL。生成的数据和数据下面的可视化下拉菜单显示了可能的选项。

数据可视化

这里有一系列的可视化选项,从更熟悉的柱状图饼图分位数箱线图。我将更改我的 SQL,以便获得更多绘制图形的选项,如下所示:

数据可视化

然后,在选择了可视化选项;柱状图后,我将选择绘图选项,这将允许我选择图形顶点的数据。它还将允许我选择要在其上进行数据列的数据列。以下截图显示了我选择的值。

数据可视化

绘图选项显示的所有字段部分显示了可以从 SQL 语句结果数据中用于图形显示的所有字段。部分定义了将形成图形轴的数据字段。系列分组字段允许我定义一个值,教育,进行数据透视。通过选择应用,我现在可以创建一个根据教育类型分组的工作类型的总余额图表,如下截图所示:

数据可视化

如果我是一名会计师,试图确定影响工资成本的因素,以及公司内成本最高的员工群体,那么我将看到上一个图表中的绿色峰值。它似乎表明具有高等教育的管理员工是数据中成本最高的群体。这可以通过更改 SQL 以过滤高等教育来确认,按余额降序排序结果,并创建一个新的柱状图。

数据可视化

显然,管理分组约为1400 万。将显示选项更改为饼图,将数据表示为饼图,具有清晰大小的分段和颜色,从视觉上清晰地呈现数据和最重要的项目。

数据可视化

我无法在这个小章节中检查所有的显示选项,但我想展示的是可以使用地理信息创建的世界地图图表。我已从download.geonames.org/export/dump/下载了Countries.zip文件。

这将提供一个约 281MB 压缩的庞大数据集,可用于创建新表。它显示为世界地图图表。我还获取了一个 ISO2 到 ISO3 的映射数据集,并将其存储在一个名为cmap的 Databricks 表中。这使我能够将数据中的 ISO2 国家代码(例如“AU”)转换为 ISO3 国家代码(例如“AUS”)(地图图表所需)。我们将用于地图图表的数据的第一列必须包含地理位置数据。在这种情况下,ISO 3 格式的国家代码。因此,从国家数据中,我将按 ISO3 代码为每个国家创建记录计数。还要确保正确设置绘图选项的键和值。我已将下载的基于国家的数据存储在一个名为geo1的表中。以下截图显示了使用的 SQL:

数据可视化

如前所示,这给出了两列数据,一个基于 ISO3 的值称为country,和一个称为value的数字计数。将显示选项设置为地图会创建一个彩色世界地图,如下截图所示:

数据可视化

这些图表展示了数据可以以各种形式进行视觉呈现,但如果需要为外部客户生成报告或需要仪表板怎么办?所有这些将在下一节中介绍。

仪表板

在本节中,我将使用上一节中创建的名为geo1的表中的数据进行地图显示。它被用来创建一个简单的仪表板,并将仪表板发布给外部客户。从工作区菜单中,我创建了一个名为dash1的新仪表板。如果我编辑此仪表板的控件选项卡,我可以开始输入 SQL,并创建图表,如下截图所示。每个图表都表示为一个视图,并可以通过 SQL 定义。它可以通过绘图选项调整大小和配置,就像每个图表一样。使用添加下拉菜单添加一个视图。以下截图显示view1已经创建,并添加到dash1view2正在被定义。

仪表板

一旦所有视图都被添加、定位和调整大小,可以选择编辑选项卡来呈现最终的仪表板。以下截图现在显示了名为dash1的最终仪表板,其中包含三种不同形式的图表和数据的部分:

仪表板

这对于展示数据非常有用,但这个仪表板是在 Databricks 云环境中。如果我想让客户看到呢?仪表板屏幕右上角有一个发布菜单选项,允许您发布仪表板。这将在新的公开发布的 URL 下显示仪表板,如下截图所示。请注意以下截图顶部的新 URL。您现在可以与客户分享此 URL 以呈现结果。还有定期更新显示以表示基础数据更新的选项。

仪表板

这给出了可用的显示选项的概念。到目前为止,所有创建的报告和仪表板都是基于 SQL 和返回的数据。在下一节中,我将展示可以使用基于 Scala 的 Spark RDD 和流数据以编程方式创建报告。

基于 RDD 的报告

以下基于 Scala 的示例使用了一个名为birdType的用户定义类类型,基于鸟的名称和遇到的数量。 创建了一个鸟类记录的 RDD,然后转换为数据框架。 然后显示数据框架。 Databricks 允许将显示的数据呈现为表格或使用绘图选项呈现为图形。 以下图片显示了使用的 Scala:

基于 RDD 的报告

这个 Scala 示例允许创建的条形图显示在以下截图中。 前面的 Scala 代码和下面的截图不如这个图表是通过数据框架以编程方式创建的这一事实重要:

基于 RDD 的报告

这打开了以编程方式从基于计算的数据源创建数据框架和临时表的可能性。 它还允许处理流数据,并使用仪表板的刷新功能,以不断呈现流数据的窗口。 下一节将介绍基于流的报告生成示例。

基于流的报告

在本节中,我将使用 Databricks 的能力上传基于 JAR 的库,以便我们可以运行基于 Twitter 的流式 Apache Spark 示例。 为了做到这一点,我必须首先在apps.twitter.com/上创建一个 Twitter 帐户和一个示例应用程序。

以下截图显示我创建了一个名为My example app的应用程序。 这是必要的,因为我需要创建必要的访问密钥和令牌来创建基于 Scala 的 Twitter feed。

基于流的报告

如果我现在选择应用程序名称,我可以看到应用程序详细信息。 这提供了一个菜单选项,该选项提供对应用程序详细信息、设置、访问令牌和权限的访问。 还有一个按钮,上面写着测试 OAuth,这使得将要创建的访问和令牌密钥可以进行测试。 以下截图显示了应用程序菜单选项:

基于流的报告

通过选择密钥和访问令牌菜单选项,可以为应用程序生成访问密钥和访问令牌。 在本节中,每个应用程序设置和令牌都有一个 API 密钥和一个秘密密钥。 在以下截图的表单顶部显示了消费者密钥和消费者秘钥(当然,出于安全原因,这些图像中的密钥和帐户详细信息已被删除)。

基于流的报告

在上一张截图中还有重新生成密钥和设置权限的选项。 下一张截图显示了应用程序访问令牌的详细信息。 有一个访问令牌和一个访问令牌秘钥。 还有重新生成值和撤销访问的选项:

基于流的报告

使用这四个字母数字值字符串,可以编写一个 Scala 示例来访问 Twitter 流。 需要的值如下:

  • 消费者密钥

  • 消费者秘钥

  • 访问令牌

  • 访问令牌秘钥

在以下代码示例中,出于安全原因,我将删除自己的密钥值。 您只需要添加自己的值即可使代码正常工作。 我已经开发了自己的库,并在本地运行代码以检查它是否能正常工作。 我在将其加载到 Databricks 之前就这样做了,以减少调试所需的时间和成本。 我的 Scala 代码示例如下。 首先,我定义一个包,导入 Spark 流和 Twitter 资源。 然后,我定义了一个名为twitter1的对象类,并创建了一个主函数:

package nz.co.semtechsolutions

import org.apache.spark._
import org.apache.spark.SparkContext._
import org.apache.spark.streaming._
import org.apache.spark.streaming.twitter._
import org.apache.spark.streaming.StreamingContext._
import org.apache.spark.sql._
import org.apache.spark.sql.types.{StructType,StructField,StringType}

object twitter1 {

  def main(args: Array[String]) {

接下来,我使用应用程序名称创建一个 Spark 配置对象。 我没有使用 Spark 主 URL,因为我将让spark-submit和 Databricks 分配默认 URL。 从这里,我将创建一个 Spark 上下文,并定义 Twitter 消费者和访问值:

    val appName = "Twitter example 1"
    val conf    = new SparkConf()

    conf.setAppName(appName)
    val sc = new SparkContext(conf)

    val consumerKey       = "QQpl8xx"
    val consumerSecret    = "0HFzxx"
    val accessToken       = "323xx"
    val accessTokenSecret = "Ilxx"

我使用System.setProperty调用设置了 Twitter 访问属性,并使用它来设置四个twitter4j oauth访问属性,使用之前生成的访问密钥:

    System.setProperty("twitter4j.oauth.consumerKey", consumerKey)
    System.setProperty("twitter4j.oauth.consumerSecret",
       consumerSecret)
    System.setProperty("twitter4j.oauth.accessToken", accessToken)
    System.setProperty("twitter4j.oauth.accessTokenSecret",
       accessTokenSecret)

从 Spark 上下文创建了一个流上下文,用于创建基于 Twitter 的 Spark DStream。流被空格分割以创建单词,并且通过以#开头的单词进行过滤,以选择哈希标签:

    val ssc    = new StreamingContext(sc, Seconds(5) )
    val stream = TwitterUtils.createStream(ssc,None)
       .window( Seconds(60) )

    // split out the hash tags from the stream

    val hashTags = stream.flatMap( status => status.getText.split(" ").filter(_.startsWith("#")))

下面用于获取单例 SQL 上下文的函数在本示例的末尾定义。因此,对于哈希标签流中的每个 RDD,都会创建一个单独的 SQL 上下文。这用于导入隐式,允许 RDD 通过toDF隐式转换为数据框。从每个rdd创建了一个名为dfHashTags的数据框,然后用它注册了一个临时表。然后我对表运行了一些 SQL 以获取行数。然后打印出行数。代码中的横幅只是用来在使用spark-submit时更容易查看输出结果:

hashTags.foreachRDD{ rdd =>

val sqlContext = SQLContextSingleton.getInstance(rdd.sparkContext)
import sqlContext.implicits._

val dfHashTags = rdd.map(hashT => hashRow(hashT) ).toDF()

dfHashTags.registerTempTable("tweets")

val tweetcount = sqlContext.sql("select count(*) from tweets")

println("\n============================================")
println(  "============================================\n")

println("Count of hash tags in stream table : "
   + tweetcount.toString )

tweetcount.map(c => "Count of hash tags in stream table : "
   + c(0).toString ).collect().foreach(println)

println("\n============================================")
println(  "============================================\n")

} // for each hash tags rdd

我还输出了当前推文流数据窗口中前五条推文的列表。你可能会认出以下代码示例。这是来自 GitHub 上 Spark 示例。同样,我使用了横幅来帮助输出结果的查看:

val topCounts60 = hashTags.map((_, 1))
   .reduceByKeyAndWindow(_ + _, Seconds(60))
.map{case (topic, count) => (count, topic)}
.transform(_.sortByKey(false))

topCounts60.foreachRDD(rdd => {

  val topList = rdd.take(5)

  println("\n===========================================")
  println(  "===========================================\n")
  println("\nPopular topics in last 60 seconds (%s total):"
     .format(rdd.count()))
  topList.foreach{case (count, tag) => println("%s (%s tweets)"
     .format(tag, count))}
  println("\n===========================================")
  println(  "==========================================\n")
})

然后,我使用 Spark 流上下文sscstartawaitTermination来启动应用程序,并保持其运行直到停止:

    ssc.start()
    ssc.awaitTermination()

  } // end main
} // end twitter1

最后,我已经定义了单例 SQL 上下文函数,并且为哈希标签数据流rdd中的每一行定义了dataframe case class

object SQLContextSingleton {
  @transient private var instance: SQLContext = null

  def getInstance(sparkContext: SparkContext):
    SQLContext = synchronized {
    if (instance == null) {
      instance = new SQLContext(sparkContext)
    }
    instance
  }
}
case class hashRow( hashTag: String)

我使用 SBT 编译了这个 Scala 应用程序代码,生成了一个名为data-bricks_2.10-1.0.jar的 JAR 文件。我的SBT文件如下:

[hadoop@hc2nn twitter1]$  cat twitter.sbt

name := "Databricks"
version := "1.0"
scalaVersion := "2.10.4"
libraryDependencies += "org.apache.spark" % "streaming" % "1.3.1" from "file:///usr/local/spark/lib/spark-assembly-1.3.1-hadoop2.3.0.jar"
libraryDependencies += "org.apache.spark" % "sql" % "1.3.1" from "file:///usr/local/spark/lib/spark-assembly-1.3.1-hadoop2.3.0.jar"
libraryDependencies += "org.apache.spark.streaming" % "twitter" % "1.3.1" from file:///usr/local/spark/lib/spark-examples-1.3.1-hadoop2.3.0.jar

我下载了正确版本的 Apache Spark 到我的集群上,以匹配 Databricks 当前使用的版本(1.3.1)。然后我在集群中的每个节点下安装了它,并以 spark 作为集群管理器在本地模式下运行。我的spark-submit脚本如下:

[hadoop@hc2nn twitter1]$ more run_twitter.bash
#!/bin/bash

SPARK_HOME=/usr/local/spark
SPARK_BIN=$SPARK_HOME/bin
SPARK_SBIN=$SPARK_HOME/sbin

JAR_PATH=/home/hadoop/spark/twitter1/target/scala-2.10/data-bricks_2.10-1.0.jar
CLASS_VAL=nz.co.semtechsolutions.twitter1

TWITTER_JAR=/usr/local/spark/lib/spark-examples-1.3.1-hadoop2.3.0.jar

cd $SPARK_BIN

./spark-submit \
 --class $CLASS_VAL \
 --master spark://hc2nn.semtech-solutions.co.nz:7077  \
 --executor-memory 100M \
 --total-executor-cores 50 \
 --jars $TWITTER_JAR \
 $JAR_PATH

我不会详细介绍,因为已经涵盖了很多次,除了注意现在类值是nz.co.semtechsolutions.twitter1。这是包类名,加上应用对象类名。所以,当我在本地运行时,我得到以下输出:

======================================
Count of hash tags in stream table : 707
======================================
Popular topics in last 60 seconds (704 total):
#KCAMÉXICO (139 tweets)
#BE3 (115 tweets)
#Fallout4 (98 tweets)
#OrianaSabatini (69 tweets)
#MartinaStoessel (61 tweets)
======================================

这告诉我应用程序库起作用了。它连接到 Twitter,创建数据流,能够将数据过滤为哈希标签,并使用数据创建临时表。因此,创建了一个用于 Twitter 数据流的 JAR 库,并证明它有效后,我现在可以将其加载到 Databricks 云上。以下截图显示了从 Databricks 云作业菜单创建了一个名为joblib1的作业。设置 Jar选项已用于上传刚刚创建的 JAR 库。已指定了到twitter1应用对象类的完整基于包的名称。

基于流的报告

以下截图显示了名为joblib1的作业,已准备就绪。基于 Spark 的集群将根据需要创建,一旦使用立即运行选项执行作业,将在活动运行部分下立即执行。虽然没有指定调度选项,但可以定义作业在特定日期和时间运行。

基于流的报告

我选择了立即运行选项来启动作业运行,如下截图所示。这显示现在有一个名为Run 1的活动运行。它已经运行了六秒。它是手动启动的,正在等待创建按需集群。通过选择运行名称Run 1,我可以查看有关作业的详细信息,特别是已记录的输出。

基于流的报告

以下截图显示了joblib1Run 1输出的示例。它显示了开始时间和持续时间,还显示了运行状态和作业详细信息,包括类和 JAR 文件。它本应该显示类参数,但在这种情况下没有。它还显示了 54GB 按需集群的详细信息。更重要的是,它显示了前五个推文哈希标签值的列表。

基于流的报告

以下截图显示了 Databricks 云实例中相同作业运行输出窗口。但这显示了来自 SQL count(*)的输出,显示了当前数据流推文窗口中临时表中的推文哈希标签数量。

基于流的报告

因此,这证明了我可以在本地创建一个应用程序库,使用基于 Twitter 的 Apache Spark 流处理,并将数据流转换为数据框架和临时表。它表明我可以通过在本地开发,然后将我的库移植到 Databricks 云来降低成本。我知道在这个例子中我既没有将临时表可视化,也没有将 DataFrame 可视化为 Databricks 图表,但时间不允许我这样做。另外,如果有时间,我会做的另一件事是在应用程序失败时进行检查点或定期保存流到文件。然而,这个主题在第三章中有所涵盖,Apache Spark Streaming中有一个例子,所以如果您感兴趣,可以在那里看一下。在下一节中,我将检查 Databricks REST API,它将允许您的外部应用程序与 Databricks 云实例更好地集成。

REST 接口

Databricks 为基于 Spark 集群的操作提供了 REST 接口。它允许集群管理、库管理、命令执行和上下文的执行。要能够访问 REST API,AWS EC2 基础的 Databricks 云中的实例必须能够访问端口34563。以下是尝试访问我 Databricks 云实例端口34563的 Telnet 命令。请注意,Telnet 尝试已成功:

[hadoop@hc2nn ~]$ telnet dbc-bff687af-08b7.cloud.databricks.com 34563
Trying 52.6.229.109...
Connected to dbc-bff687af-08b7.cloud.databricks.com.
Escape character is '^]'.

如果您没有收到 Telnet 会话,请通过<help@databricks.com>联系 Databricks。接下来的部分提供了访问 Databricks 云实例的 REST 接口示例。

配置

为了使用接口,我需要将我用于访问 Databricks 集群实例的 IP 地址加入白名单。这是我将运行 REST API 命令的机器的 IP 地址。通过将 IP 地址加入白名单,Databricks 可以确保每个 Databricks 云实例都有一个安全的用户访问列表。

我通过之前的帮助电子邮件地址联系了 Databricks 支持,但在您的云实例的工作区菜单中还有一个白名单 IP 指南:

工作区 | databricks_guide | DevOps 工具 | 白名单 IP

现在可以使用 Linux curl命令从 Linux 命令行向我的 Databricks 云实例提交 REST API 调用。下面显示了curl命令的示例通用形式,使用了我的 Databricks 云实例用户名、密码、云实例 URL、REST API 路径和参数。

Databricks 论坛和之前的帮助电子邮件地址可用于获取更多信息。接下来的部分将提供一些 REST API 的工作示例:

curl –u  '<user>:<paswd>' <dbc url> -d "<parameters>"

集群管理

您仍然需要从您的云实例用户界面创建 Databricks Spark 集群。列表 REST API 命令如下:

/api/1.0/clusters/list

它不需要任何参数。此命令将提供您的集群列表、它们的状态、IP 地址、名称以及它们运行的端口号。以下输出显示,集群semclust1处于挂起状态,正在创建过程中:

curl -u 'xxxx:yyyyy' 'https://dbc-bff687af-08b7.cloud.databricks.com:34563/api/1.0/clusters/list'

 [{"id":"0611-014057-waist9","name":"semclust1","status":"Pending","driverIp":"","jdbcPort":10000,"numWorkers":0}]

当集群可用时运行相同的 REST API 命令,显示名为semcust1的集群正在运行,并且有一个 worker:

[{"id":"0611-014057-waist9","name":"semclust1","status":"Running","driverIp":"10.0.196.161","jdbcPort":10000,"numWorkers":1}]

终止此集群,并创建一个名为semclust的新集群,将更改 REST API 调用的结果,如下所示:

curl -u 'xxxx:yyyy' 'https://dbc-bff687af-08b7.cloud.databricks.com:34563/api/1.0/clusters/list'

[{"id":"0611-023105-moms10","name":"semclust", "status":"Pending","driverIp":"","jdbcPort":10000,"numWorkers":0},
 {"id":"0611-014057-waist9","name":"semclust1","status":"Terminated","driverIp":"10.0.196.161","jdbcPort":10000,"numWorkers":1}]

执行上下文

使用这些 API 调用,您可以创建、显示或删除执行上下文。REST API 调用如下:

  • /api/1.0/contexts/create

  • /api/1.0/contexts/status

  • /api/1.0/contexts/destroy

在以下 REST API 调用示例中,通过curl提交,为标识为其集群 ID 的semclust创建了一个 Scala 上下文。

curl -u 'xxxx:yyyy' https://dbc-bff687af-08b7.cloud.databricks.com:34563/api/1.0/contexts/create -d "language=scala&clusterId=0611-023105-moms10"

返回的结果要么是错误,要么是上下文 ID。以下三个示例返回值显示了由无效 URL 引起的错误,以及两个成功调用返回的上下文 ID:

{"error":"ClusterNotFoundException: Cluster not found: semclust1"}
{"id":"8689178710930730361"}
{"id":"2876384417314129043"}

命令执行

这些命令允许您运行命令、列出命令状态、取消命令或显示命令的结果。REST API 调用如下:

  • /api/1.0/commands/execute

  • /api/1.0/commands/cancel

  • /api/1.0/commands/status

下面的示例显示了针对名为cmap的现有表运行的 SQL 语句。上下文必须存在,并且必须是 SQL 类型。参数已通过-d选项传递给 HTTP GET 调用。参数是语言、集群 ID、上下文 ID 和 SQL 命令。命令 ID 返回如下:

curl -u 'admin:FirmWare1$34' https://dbc-bff687af-08b7.cloud.databricks.com:34563/api/1.0/commands/execute -d
"language=sql&clusterId=0611-023105-moms10&contextId=7690632266172649068&command=select count(*) from cmap"

{"id":"d8ec4989557d4a4ea271d991a603a3af"}

REST API 还允许上传库到集群并检查它们的状态。REST API 调用路径如下:

  • /api/1.0/libraries/upload

  • /api/1.0/libraries/list

接下来给出了一个上传到名为semclust的集群实例的库的示例。通过-d选项将参数传递给 HTTP GET API 调用的语言、集群 ID、库名称和 URI。成功的调用将返回库的名称和 URI,如下所示:

curl -u 'xxxx:yyyy' https://dbc-bff687af-08b7.cloud.databricks.com:34563/api/1.0/libraries/upload
 -d "language=scala&clusterId=0611-023105-moms10&name=lib1&uri=file:///home/hadoop/spark/ann/target/scala-2.10/a-n-n_2.10-1.0.jar"

{"name":"lib1","uri":"file:///home/hadoop/spark/ann/target/scala-2.10/a-n-n_2.10-1.0.jar"}

请注意,此 REST API 可能会随内容和版本而更改,因此请在 Databricks 论坛中检查,并使用以前的帮助电子邮件地址与 Databricks 支持检查 API 详细信息。我认为,通过这些简单的示例调用,很明显这个 REST API 可以用于将 Databricks 与外部系统和 ETL 链集成。在下一节中,我将概述 Databricks 云内的数据移动。

数据移动

有关在 Databricks 中移动数据的一些方法已经在第八章 Spark Databricks和第九章 Databricks Visualization中进行了解释。我想在本节中概述所有可用的移动数据方法。我将研究表、工作区、作业和 Spark 代码的选项。

表数据

Databricks 云的表导入功能允许从 AWS S3存储桶、Databricks 文件系统DBFS)、通过 JDBC 以及从本地文件导入数据。本节概述了每种类型的导入,从S3开始。从 AWS S3导入表数据需要 AWS 密钥、AWS 秘钥和S3存储桶名称。以下屏幕截图显示了一个示例。我已经提供了一个S3存储桶创建的示例,包括添加访问策略,因此我不会再次介绍它。

表数据

一旦添加了表单详细信息,您就可以浏览您的S3存储桶以获取数据源。选择DBFS作为表数据源可以浏览您的DBFS文件夹和文件。选择数据源后,可以显示预览,如下面的屏幕截图所示:

表数据

选择JDBC作为表格数据源允许您指定远程 SQL 数据库作为数据源。只需添加一个访问URL用户名密码。还可以添加一些 SQL 来定义表和源列。还有一个通过添加属性按钮添加额外属性的选项,如下面的屏幕截图所示:

表格数据

选择文件选项以从文件中填充 Databricks 云实例表,创建一个下拉或浏览。此上传方法先前用于将基于 CSV 的数据上传到表中。一旦指定了数据源,就可以指定数据分隔符字符串或标题行,定义列名或列类型,并在创建表之前预览数据。

表格数据

文件夹导入

从工作区或文件夹下拉菜单中,可以导入项目。以下屏幕截图显示了导入项目菜单选项的复合图像:

文件夹导入

这将创建一个文件拖放或浏览窗口,当点击时,允许您浏览本地服务器以导入项目。选择“所有支持的类型”选项显示可以导入的项目可以是 JAR 文件、dbc 存档、Scala、Python 或 SQL 文件。

库导入

以下屏幕截图显示了来自 Workspace 和文件夹菜单选项的新库功能。这允许将外部创建和测试的库加载到您的 Databricks 云实例中。该库可以是 Java 或 Scala JAR 文件、Python Egg 或用于访问存储库的 Maven 坐标。在下面的屏幕截图中,正在从本地服务器通过浏览窗口选择一个 JAR 文件。本章中使用了此功能来测试基于流的 Scala 编程:

库导入

进一步阅读

在总结本章之前,也是 Databricks 云端使用 Apache Spark 的最后一章,我想提及一些关于 Apache Spark 和 Databricks 的额外信息资源。首先,有 Databricks 论坛可供访问:forums.databricks.com/,用于与databricks.com/的使用相关的问题和答案。此外,在您的 Databricks 实例中,在 Workspace 菜单选项下,将有一个包含许多有用信息的 Databricks 指南。Apache Spark 网站spark.apache.org/也包含许多有用信息,以及基于模块的 API 文档。最后,还有 Spark 邮件列表,<user@spark.apache.org>,提供了大量关于 Spark 使用信息和问题解决的信息。

摘要

第八章、Spark Databricks和第九章、Databricks 可视化,已经介绍了 Databricks 在云安装方面的情况,以及 Notebooks 和文件夹的使用。已经检查了帐户和集群管理。还检查了作业创建、远程库创建的概念以及导入。解释了 Databricks dbutils包的功能,以及 Databricks 文件系统在第八章、Spark Databricks中。还展示了表格和数据导入的示例,以便对数据集运行 SQL。

已经检查了数据可视化的概念,并创建了各种图表。已经创建了仪表板,以展示创建和共享这种数据呈现的简易性。通过示例展示了 Databricks REST 接口,作为远程使用 Databricks 云实例并将其与外部系统集成的辅助。最后,已经检查了关于工作区、文件夹和表的数据和库移动选项。

您可能会问为什么我要把两章内容都献给像 Databricks 这样的基于云的服务。原因是 Databricks 似乎是从 Apache Spark 发展而来的一个逻辑上的基于云的进展。它得到了最初开发 Apache Spark 的人的支持,尽管作为一个服务还处于初期阶段,可能会发生变化,但仍然能够提供基于 Spark 的云生产服务。这意味着一家希望使用 Spark 的公司可以使用 Databricks,并随着需求增长而扩展他们的云,并且可以访问动态的基于 Spark 的机器学习、图处理、SQL、流处理和可视化功能。

正如以往一样,这些 Databricks 章节只是触及了功能的表面。下一步将是自己创建一个 AWS 和 Databricks 账户,并使用这里提供的信息来获得实际经验。

由于这是最后一章,我将再次提供我的联系方式。我对人们如何使用 Apache Spark 感兴趣。我对您创建的集群规模以及您处理的数据感兴趣。您是将 Spark 作为处理引擎使用吗?还是在其上构建系统?您可以在 LinkedIn 上与我联系:linkedin.com/profile/view?id=73219349

您可以通过我的网站semtech-solutions.co.nz或最后通过电子邮件联系我:<info@semtech-solutions.co.nz>

最后,我在有空的时候会维护一个与开源软件相关的演示文稿列表。任何人都可以免费使用和下载它们。它们可以在 SlideShare 上找到:www.slideshare.net/mikejf12/presentations

如果您有任何具有挑战性的机会或问题,请随时使用上述联系方式与我联系。

posted @ 2024-05-21 12:53  绝不原创的飞龙  阅读(10)  评论(0编辑  收藏  举报