Hadoop3-大数据分析-全-

Hadoop3 大数据分析(全)

原文:Big Data Analytics with Hadoop 3

协议:CC BY-NC-SA 4.0

零、前言

Apache Hadoop 是最受欢迎的大数据处理平台,可以与许多其他大数据工具相结合,构建强大的分析解决方案。借助 Hadoop 3 的大数据分析向您展示了如何做到这一点,通过实际示例来深入了解该软件及其优势。

一旦您浏览了 Hadoop 3 的最新功能,您将了解 HDFS、MapReduce 和 Yarn,以及它们如何实现更快、更高效的大数据处理。然后,您将继续学习如何将 Hadoop 与开源工具(如 Python 和 R)集成,以分析和可视化数据,并对大数据进行统计计算。当您熟悉所有这些内容时,您将探索如何将 Hadoop 3 与 Apache Spark 和 Apache Flink 一起用于实时数据分析和流处理。除此之外,您还将了解如何使用 Hadoop 在云中构建分析解决方案,以及如何使用实际用例通过端到端管道执行大数据分析。

到本书结束时,您将非常熟悉 Hadoop 生态系统的分析能力。您将能够构建强大的解决方案来执行大数据分析,并轻松获得见解。

这本书是给谁的

如果您希望使用 Hadoop 3 的强大功能为您的企业或业务构建高性能分析解决方案,或者您刚刚接触大数据分析,那么借助 Hadoop 3 进行大数据分析非常适合您。需要对 Java 编程语言有基本的了解。

这本书涵盖了什么

第一章Hadoop 简介,向大家介绍 Hadoop 的世界及其核心组件,即 HDFS 和 MapReduce。

第 2 章大数据分析概述介绍了检查大型数据集以发现数据模式、生成报告和收集有价值见解的过程。

第三章利用 MapReduce 进行大数据处理介绍了 MapReduce 的概念,这是大多数大数据计算/处理系统背后的基本概念。

第 4 章用 Python 和 Hadoop 进行科学计算和大数据分析,介绍 Python 以及借助 Python 包使用 Hadoop 进行大数据分析。

第五章利用 R 和 Hadoop 进行统计大数据计算,介绍 R,演示如何利用 R 利用 Hadoop 对大数据进行统计计算。

第 6 章使用 Apache Spark 进行批量分析,向您介绍 Apache Spark,并演示如何基于批量处理模型将 Spark 用于大数据分析。

第 7 章使用 Apache Spark 进行实时分析,介绍了 Apache Spark 的流处理模型,并演示了如何构建基于流的实时分析应用。

第 8 章使用 Apache Flink 进行批处理分析,介绍 Apache Flink 以及如何基于批处理模型将其用于大数据分析。

第 9 章使用 Apache Flink 进行流处理,向您介绍使用 Flink 的 DataStream APIs 和流处理。Flink 将用于接收和处理实时事件流,并将聚合和结果存储在 Hadoop 集群中。

第十章可视化大数据,用 Tableau 等各种工具和技术为大家介绍数据可视化的世界。

第 11 章云计算介绍,介绍云计算以及 IaaS、PaaS、SaaS 等各种概念。您还将一窥顶级云提供商。

第 12 章使用亚马逊 Web 服务,向您介绍 AWS 以及 AWS 中的各种服务,这些服务对于使用弹性地图缩减 ( EMR )在 AWS 云中设置 Hadoop 集群执行大数据分析非常有用。

充分利用这本书

这些例子已经使用 Scala、Java、R 和 Python 在 64 位 Linux 上实现。您还需要或准备在您的机器上安装以下软件(最好是最新版本):

  • Spark 2.3.0(或更高版本)
  • Hadoop 3.1(或更高版本)
  • 大概有 1.4 个
  • Java (JDK 和 JRE) 1.8+
  • Scala 2.11.x(或更高版本)
  • Python 2.7+/3.4+
  • R 3.1+和 RStudio 1.0.143(或更高)
  • 日食火星或想法智能(最新)

关于操作系统:Linux 发行版更好(包括 Debian、Ubuntu、Fedora、RHEL 和 CentOS),更具体地说,例如,关于 Ubuntu,建议安装完整的 14.04 (LTS) 64 位(或更高版本)、VMWare player 12 或 Virtual box。您也可以在 Windows (XP/7/8/10)或 macOS X (10.4.7+)上运行代码。

关于硬件配置:处理器酷睿 i3、酷睿 i5(推荐)~酷睿 i7(为获得最佳效果)。然而,多核处理将提供更快的数据处理和可扩展性。独立模式下至少 8 GB 内存(推荐)。单个虚拟机至少有 32 GB 内存,群集至少有 32gb 内存。足够运行繁重作业的存储空间(取决于您要处理的数据集大小),最好至少有 50 GB 的可用磁盘存储空间(用于单机和 SQL 仓库)。

下载示例代码文件

你可以从你在www.packtpub.com的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册将文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packtpub.com
  2. 选择“支持”选项卡。
  3. 点击代码下载和勘误表。
  4. 在搜索框中输入图书的名称,并按照屏幕指示进行操作。

下载文件后,请确保使用最新版本的解压缩文件夹:

  • 视窗系统的 WinRAR/7-Zip
  • zipeg/izp/un ARX for MAC
  • 适用于 Linux 的 7-Zip/PeaZip

这本书的代码包也托管在 GitHub 上,网址为 https://GitHub . com/PacktPublishing/Big-Data-Analytics-with-Hadoop-3。如果代码有更新,它将在现有的 GitHub 存储库中更新。

我们还有来自丰富的图书和视频目录的其他代码包,可在【https://github.com/PacktPublishing/】获得。看看他们!

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。你可以在这里下载:http://www . packtpub . com/sites/default/files/downloads/bigdatanalytics with Hadoop 3 _ color images . pdf

使用的约定

本书通篇使用了许多文本约定。

CodeInText:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“这个文件temperatures.csv是可以下载的,一旦下载,你可以通过运行命令将其移动到hdfs,如下面的代码所示。”

代码块设置如下:

hdfs dfs -copyFromLocal temperatures.csv /user/normal

当我们希望将您的注意力吸引到代码块的特定部分时,相关的行或项目以粗体显示:

Map-Reduce Framework -- output average temperature per city name
    Map input records=35
 Map output records=33
    Map output bytes=208
    Map output materialized bytes=286

任何命令行输入或输出都编写如下:

$ ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa
$ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
$ chmod 0600 ~/.ssh/authorized_keys

粗体:表示一个新的术语,一个重要的单词,或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。下面是一个例子:“单击数据节点选项卡显示所有节点。”

Warnings or important notes appear like this. Tips and tricks appear like this.

取得联系

我们随时欢迎读者的反馈。

综合反馈:发邮件feedback@packtpub.com并在邮件主题中提及书名。如果您对本书的任何方面有疑问,请发电子邮件至questions@packtpub.com

勘误表:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问www.packtpub.com/submit-errata,选择您的图书,点击勘误表提交链接,并输入详细信息。

盗版:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com联系我们,并提供材料链接。

如果你有兴趣成为一名作者:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问authors.packtpub.com

复习

请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家!

更多关于 Packt 的信息,请访问packtpub.com

一、Hadoop 简介

本章向读者介绍了 Hadoop 的世界和 Hadoop 的核心组件,即 Hadoop 分布式文件系统 ( HDFS )和 MapReduce。我们将从介绍 Hadoop 3 版本中的变化和新功能开始。特别是,我们将讨论 HDFS 和的新特性,以及对客户端应用的更改。此外,我们还将在本地安装一个 Hadoop 集群,并展示新功能,如擦除编码** ( EC )和时间轴服务。快速说明一下,第 10 章可视化大数据向您展示了如何在 AWS 中创建 Hadoop 集群。**

简而言之,本章将涵盖以下主题:

  • HDFS

    • 高可用性
    • 数据节点内平衡器
    • 欧盟委员会(European Commission)
    • 端口映射
  • MapReduce

    • 任务级优化
  • 故事

    • 机会集装箱
    • 时间轴服务 v.2
    • 码头集装箱化
  • 其他变化

  • Hadoop 3.1 的安装

    • HDFS
    • 故事
    • 欧盟委员会(European Commission)
    • 时间轴服务 v.2

Hadoop 分布式文件系统

HDFS 是一个用 Java 实现的基于软件的文件系统,它位于本地文件系统之上。HDFS 背后的主要概念是,它将文件划分为块(通常为 128 MB),而不是将文件作为一个整体来处理。这允许许多功能,例如分发、复制、故障恢复,更重要的是使用多台机器对数据块进行分布式处理。数据块大小可以是 64 MB、128 MB、256 MB 或 512 MB,无论哪种大小都可以。对于具有 128 兆字节块的 1 GB 文件,将有 1024 兆字节/128 兆字节等于 8 个块。如果考虑三倍的复制因子,则为 24 个数据块。HDFS 提供具有容错和故障恢复的分布式存储系统。HDFS 有两个主要组成部分:名为“T1”的节点和“T2”数据节点“T3”。名称节点包含文件系统所有内容的所有元数据:文件名、文件权限和每个文件的每个块的位置,因此它是 HDFS 最重要的机器。数据节点连接到名称节点,并将数据块存储在 HDFS。他们依赖名称节点来获取文件系统中所有关于内容的元数据信息。如果名称节点没有任何信息,数据节点将无法向任何想要读取/写入 HDFS 的客户端提供信息。

名称节点和数据节点进程可以在一台机器上运行;但是,一般来说,HDFS 集群由运行名称节点进程的专用服务器和运行数据节点进程的数千台机器组成。为了能够访问存储在名称节点中的内容信息,它将整个元数据结构存储在内存中。它通过跟踪数据块的复制因子,确保不会因机器故障而导致数据丢失。由于这是单点故障,为了降低因名称节点故障而导致数据丢失的风险,可以使用辅助名称节点来生成主名称节点内存结构的快照。

数据节点具有很大的存储容量,与名称节点不同,如果数据节点出现故障,HDFS 将继续正常运行。当数据节点出现故障时,名称节点会自动处理故障数据节点中所有数据块的复制,并确保复制得到备份。由于名称节点知道复制块的所有位置,因此连接到群集的任何客户端都能够很少甚至没有中断地继续进行。

In order to make sure that each block meets the minimum required replication factor, the NameNode replicates the lost blocks.

下图描述了文件到名称节点中的块的映射,以及块及其副本在数据节点中的存储:

如上图所示,自 Hadoop 开始以来,名称节点一直是单点故障。

高可用性

在 Hadoop 1.x 和 Hadoop 2.x 中,名称节点的丢失都会导致集群崩溃。在 Hadoop 1.x 中,没有简单的恢复方法,而 Hadoop 2.x 引入了高可用性(主动-被动设置)来帮助从名称节点故障中恢复。

下图显示了高可用性的工作原理:

在 Hadoop 3.x 中,您可以有两个被动名称节点和一个主动节点,以及五个日志节点,以帮助从灾难性故障中恢复:

  • 名称节点机器:运行活动和备用名称节点的机器。它们应该具有彼此等效的硬件,以及在非高可用性集群中使用的硬件。

  • 日志节点机器:运行日志节点的机器。JournalNode 守护程序相对较轻,因此这些守护程序可以合理地与其他 Hadoop 守护程序放在一起,例如 NameNodes、作业跟踪器或 Yarn资源管理器

数据节点内平衡器

HDFS 有一种方法可以在数据节点之间平衡数据块,但在具有多个硬盘的同一数据节点内部却没有这种平衡。因此,12 轴数据节点可能会有失衡的物理磁盘。但是为什么这对性能有影响呢?嗯,由于磁盘不平衡,数据节点级别的数据块可能与其他数据节点相同,但由于磁盘不平衡,读/写会有偏差。因此,Hadoop 3.x 引入了节点内平衡器来平衡每个数据节点内的物理磁盘,以减少数据的偏斜。

这增加了群集上运行的任何进程执行的读写操作,例如映射器缩减器

擦除编码

自从 Hadoop 诞生以来,HDFS 一直是它的基本组件。在 Hadoop 1.x 和 Hadoop 2.x 中,典型的 HDFS 安装使用三倍的复制因子。

与默认的复制因子 3 相比,EC 可能是 HDFS 近年来最大的变化,通过将复制因子从 3 降低到约 1.4,从根本上使许多数据集的容量翻倍。现在让我们了解一下电子商务是怎么回事。

电子商务是一种数据保护方法,其中数据被分解成片段,扩展,用冗余数据段编码,并存储在一组不同的位置或存储器中。如果在此过程中的某个时刻,数据因损坏而丢失,则可以使用存储在其他地方的信息来重建数据。虽然电子商务的中央处理器更密集,但这大大减少了可靠存储大量数据所需的存储空间(HDFS)。HDFS 使用复制来提供可靠的存储,这很昂贵,通常需要存储三份数据拷贝,因此会造成 200%的存储空间开销。

端口号

在 Hadoop 3.x 中,各种服务的许多端口都已更改。

以前,多个 Hadoop 服务的默认端口在 Linux 临时端口范围内(32768–61000)。这表明在启动时,服务有时会由于冲突而无法绑定到端口和另一个应用。

这些冲突端口已移出短暂范围,影响了名称节点、辅助名称节点、数据节点和 KMS。

更改如下:

  • 名称节点端口 : 50470 → 9871、50070 → 9870 和 8020 → 9820
  • 辅助名称节点端口 : 50091 → 9869 和 50090 → 9868
  • 数据节点端口 : 5 0020 → 9867、50010 → 9866、50475 → 9865 和 50075 → 9864

MapReduce 框架

理解这个概念的一个简单方法是,想象你和你的朋友想把成堆的水果分类放入盒子里。为此,你要给每个人分配一个任务,让他们检查一篮子生水果(全部混在一起),然后把水果分成不同的盒子。然后每个人做同样的任务,用这篮子水果把水果分成不同的种类。最后,你会从你所有的朋友那里得到很多盒水果。然后,你可以分配一个小组,把同一种水果放在一个盒子里,称重,密封盒子进行运输。展示 MapReduce 框架工作原理的一个经典示例是字数统计示例。以下是处理输入数据的各个阶段,首先将输入拆分到多个工作节点,然后最终生成输出,即字数统计:

MapReduce 框架由一个资源管理器和多个节点管理器组成(通常,节点管理器与 HDFS 的数据节点共存)。

任务级本机优化

MapReduce 增加了对地图输出收集器的本机实现的支持。这种新的支持可以使性能提高大约 30%或更多,尤其是对于洗牌密集型作业。

原生库将通过Pnative自动构建。用户可以通过设置mapreduce.job.map.output.collector.class=org.apache.hadoop.mapred逐个工作地选择新的收集器。
nativetask.NativeMapOutputCollectorDelegator在他们的岗位配置中。

基本思想是能够添加一个NativeMapOutputCollector,以便处理映射器发出的键/值对。因此sortspillIFile序列化都可以在本机代码中完成。初步测试(在 Xeon E5410、jdk6u24 上)显示了以下有希望的结果:

  • sort比 Java 快 3-10 倍左右(仅支持二进制字符串比较)
  • IFile序列化速度比 Java 快 3 倍左右:每秒约 500 MB。如果使用 CRC32C 硬件,在每秒 1 GB 或更高的范围内,事情会变得更快
  • 合并代码尚未完成,因此测试使用了足够的io.sort.mb来防止中间溢出

故事

当应用想要运行时,客户端启动 ApplicationMaster,然后 application master 与 ResourceManager 协商,以容器的形式获取集群中的资源。容器表示在单个节点上分配的用于运行任务和进程的 CPU(核心)和内存。容器由节点管理器监督,由资源管理器调度。

容器示例:

  • 一个内核和 4 GB 内存
  • 两个内核和 6 GB 内存
  • 四个内核和 20 GB 内存

一些容器被指定为映射器,另一些被指定为缩减器;所有这些都由 ApplicationMaster 与 ResourceManager 协同工作。这个框架叫做Yarn:

使用 Yarn,几个不同的应用可以请求和执行容器上的任务,共享集群资源非常好。然而,随着集群规模的增长以及应用和需求的变化,资源利用的效率会随着时间的推移而降低。

机会集装箱

机会容器可以被传输到节点管理器,即使它们在特定时间的执行不能立即开始,不像 Yarn 容器,当且仅当存在未分配的资源时,Yarn 容器才在节点中被调度。

在这些类型的场景中,机会容器将在节点管理器中排队,直到所需的资源可用。这些容器的最终目标是提高集群资源利用率,进而提高任务吞吐量。

容器执行的类型

容器有两种类型,如下所示:

  • 保证容器:这些容器对应现有的 Yarn 容器。它们由容量调度程序分配。当且仅当有资源可用于立即开始执行它们时,它们才被传输到节点。
  • 机会容器:与保证容器不同,在这种情况下,我们不能保证一旦它们被分派到一个节点,就会有资源可用来开始它们的执行。相反,它们将在节点管理器中排队,直到资源变得可用。

Yarn 时间线服务 v.2

Yarn 时间线服务 v.2 解决了以下两大挑战:

  • 增强时间轴服务的可扩展性和可靠性
  • 通过引入流和聚合来提高可用性

增强可扩展性和可靠性

版本 2 采用了更具可扩展性的分布式写入器架构和后端存储,与 v.1 相反,v . 1 没有在小集群之外扩展,因为它使用了写入器/读取器架构和后端存储的单个实例。

由于 Apache HBase 甚至可以很好地扩展到更大的集群,并继续保持良好的读写响应时间,v.2 更喜欢选择它作为主要后端存储。

可用性改进

很多时候,用户更感兴趣的是在流级别获得的信息,或者是 Yarn 应用的逻辑组。因此,启动一系列的 Yarn 应用来完成一个逻辑工作流会更方便。

为了实现这一点,v.2 支持流的概念,并在流级别聚合度量。

体系结构

Yarn 时间线服务 v.2 使用一组收集器(写入器)将数据写入后端
存储器。收集器是分布式的,并且与它们专用的应用主设备位于同一位置。除了资源管理器时间线收集器之外,属于该应用的所有数据都被发送到应用层
时间线收集器。

对于给定的应用,应用主可以将应用的数据写入位于同一位置的时间线收集器(这是本版本中的一个 NM 辅助服务)。此外,运行应用容器的其他节点的
节点管理器也将
数据写入运行应用主节点的节点上的时间线收集器。

资源管理器还维护自己的时间线收集器。它只发出 Yarn 一般的
生命周期事件,以保持合理的写入量。

时间轴读取器是独立于时间轴收集器的独立守护程序,它们
专门用于通过 REST API 提供查询:

下图从高层次说明了设计:

其他变化

Hadoop 3 还有其他变化,主要是为了更容易维护和操作。特别是,命令行工具已经进行了改进,以更好地满足运营团队的需求。

最低要求的 Java 版本

所有 Hadoop JARs 现在都编译成运行时版本的 Java 8。因此,仍在使用 Java 7 或更低版本的用户必须升级到 Java 8。

Shell 脚本重写

Hadoop 外壳脚本已经被重写,以修复许多长期存在的错误,并包含一些新功能。

发行说明中记录了不兼容的更改。你可以在 https://issues.apache.org/jira/browse/HADOOP-9902 找到他们。

更多详细信息可在https://Hadoop . Apache . org/docs/r 3 . 0 . 0/Hadoop-project-dist/Hadoop-common/Unixshellguide . html上查阅。出现在https://Hadoop . Apache . org/docs/r 3 . 0 . 0/Hadoop-project-dist/Hadoop-common/unixshellapi . html上的文档将吸引超级用户,因为它描述了大多数新功能,尤其是那些与可扩展性相关的功能。

阴影客户端 JARs

新增hadoop-client-api``hadoop-client-runtime神器,参考https://issues.apache.org/jira/browse/HADOOP-11804。这些工件将 Hadoop 的依赖关系隐藏在一个 JAR 中。因此,它避免了将 Hadoop 的依赖关系泄漏到应用的类路径中。

Hadoop 现在还支持与微软 Azure 数据湖和阿里云对象存储系统集成,作为 Hadoop 兼容文件系统的替代方案。

安装 Hadoop 3

在本节中,我们将了解如何在本地机器上安装单节点 Hadoop 3 集群。为此,我们将遵循在https://Hadoop . Apache . org/docs/current/Hadoop-project-dist/Hadoop-common/single cluster . html中给出的文档。

本文档详细描述了如何安装和配置单节点 Hadoop 设置,以便使用 Hadoop MapReduce 和 HDFS 快速执行简单操作。

先决条件

必须安装 Java 8 才能运行 Hadoop。如果你的机器上没有 Java 8,那么你可以下载安装 Java 8:https://www.java.com/en/download/

当您在浏览器中打开下载链接时,以下内容将出现在您的屏幕上:

下载

使用以下链接下载 Hadoop 3.1 版本:http://Apache . spinellistions . com/Hadoop/common/Hadoop-3 . 1 . 0/

以下截图是在浏览器中打开下载链接时显示的页面:

当您在浏览器中获得此页面时,只需将hadoop-3.1.0.tar.gz文件下载到您的本地机器上。

装置

执行以下步骤在您的计算机上安装单节点 Hadoop 集群:

  1. 使用以下命令提取下载的文件:
tar -xvzf hadoop-3.1.0.tar.gz
  1. 提取 Hadoop 二进制文件后,只需运行以下命令来测试 Hadoop 二进制文件,并确保二进制文件在我们的本地机器上工作:
cd hadoop-3.1.0

mkdir input

cp etc/hadoop/*.xml input

bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.0.jar grep input output 'dfs[a-z.]+'

cat output/*

如果一切按预期运行,您将看到一个显示一些输出的输出目录,这表明示例命令是有效的。

A typical error at this point will be missing Java. You might want to check and see if you have Java installed on your machine and the JAVA_HOME environment variable set correctly.

设置无密码 ssh

现在,通过运行一个简单的命令,检查是否可以在没有密码的情况下将ssh切换到localhost,如下所示:

$ ssh localhost

如果没有密码就无法从ssh切换到localhost,请执行以下命令:

$ ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa
$ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
$ chmod 0600 ~/.ssh/authorized_keys

设置名称节点

对配置文件etc/hadoop/core-site.xml进行以下更改:

<configuration>
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://localhost:9000</value>
    </property>
</configuration>

对配置文件etc/hadoop/hdfs-site.xml进行以下更改:

<configuration>
    <property>
        <name>dfs.replication</name>
        <value>1</value>
    </property>
        <name>dfs.name.dir</name>
        <value><YOURDIRECTORY>/hadoop-3.1.0/dfs/name</value>
    </property>
</configuration>

从 HDFS 开始

按照所示步骤启动 HDFS(名称节点和数据节点):

  1. 格式化文件系统:
$ ./bin/hdfs namenode -format
  1. 启动名称节点守护程序和数据节点守护程序:
$ ./sbin/start-dfs.sh

Hadoop 守护程序日志输出被写入$HADOOP_LOG_DIR目录(默认为$HADOOP_HOME/logs)。

  1. 浏览名称节点的网页界面;默认情况下,它在http://localhost:9870/可用。
  2. 创建执行 MapReduce 作业所需的 HDFS 目录:
$ ./bin/hdfs dfs -mkdir /user 
$ ./bin/hdfs dfs -mkdir /user/<username>
  1. 完成后,使用以下命令停止守护程序:
$ ./sbin/stop-dfs.sh
  1. 打开浏览器查看你本地的 Hadoop,可以在浏览器中作为http://localhost:9870/启动。以下是 HDFS 安装的样子:

  1. 单击数据节点选项卡会显示节点,如下图所示:

Figure: Screenshot showing the nodes in the Datanodes tab

  1. 单击日志将显示集群中的各种日志,如下图所示:

  1. 如下图所示,您还可以查看集群组件的各种 JVM 指标:

  1. 如下图所示,您也可以检查配置。这是查看整个配置和所有默认设置的好地方:

  1. 您还可以浏览新安装的集群的文件系统,如下图所示:

Figure: Screenshot showing the Browse Directory and how you can browse the filesystem in you newly installed cluster

在这一点上,我们都应该能够看到和使用一个基本的 HDFS 集群。但这只是一个包含一些目录和文件的 HDFS 文件系统。我们还需要一个作业/任务调度服务来实际使用集群来满足计算需求,而不仅仅是存储。

设置 Yarn 服务

在本节中,我们将设置一个 Yarn 服务,并启动运行和操作 Yarn 集群所需的组件:

  1. 启动资源管理器守护程序和节点管理器守护程序:
$ sbin/start-yarn.sh
  1. 浏览资源管理器的网页界面;默认情况下,它在:http://localhost:8088/可用

  2. 运行 MapReduce 作业

  3. 完成后,使用以下命令停止守护程序:

$ sbin/stop-yarn.sh

以下是 Yarn 资源管理器,您可以通过将网址http://localhost:8088/放入浏览器来查看:

Figure: Screenshot of YARN ResouceManager

下面是一个视图,显示了集群中的资源队列以及所有正在运行的应用。这也是您可以查看和监控正在运行的作业的地方:

Figure: Screenshot of queues of resources in the cluster

此时,我们应该能够看到运行 Hadoop 3.1.0 的本地集群中正在运行的 SHART 服务。接下来,我们将看看 Hadoop 3.x 中的一些新功能。

擦除编码

EC 是 Hadoop 3.x 中的一个关键变化,与早期版本相比,HDFS 利用率
效率有了显著提高,在早期版本中,例如 3 的复制因子导致了
各种数据的宝贵集群文件系统的巨大浪费,无论这些数据对手头的任务有多重要。

可以使用策略设置电子商务,并将策略分配给 HDFS 的目录。为此,HDFS 提供了一个 ec 子命令来执行与 EC 相关的管理命令:

hdfs ec [generic options]
    [-setPolicy -path <path> [-policy <policyName>] [-replicate]]
    [-getPolicy -path <path>]
    [-unsetPolicy -path <path>]
    [-listPolicies]
    [-addPolicies -policyFile <file>]
    [-listCodecs]
    [-enablePolicy -policy <policyName>]
    [-disablePolicy -policy <policyName>]
    [-help [cmd ...]]

以下是每个命令的详细信息:

  • [-setPolicy -path <path> [-policy <policyName>] [-replicate]]:在指定路径的目录上设置 EC 策略。
    • path:HDFS 的一个目录。这是一个强制参数。设置策略只影响新创建的文件,不影响现有文件。
    • policyName:用于该
      目录下文件的欧共体政策。如果设置了dfs.namenode.ec.system.default.policy配置,该参数可以省略。路径的电子商务策略将在配置中使用默认值进行设置。
    • -replicate:对目录应用特殊的复制策略,强制目录采用 3x 复制方案。
    • -replicate and -policy <policyName>:这些是可选参数。不能同时指定它们。
  • [-getPolicy -path <path>]:获取指定路径的文件或目录
    的 EC 策略的详细信息。
  • [-unsetPolicy -path <path>]:取消对目录中setPolicy的前一次调用所设置的 EC 策略。如果目录从祖先目录继承了 EC 策略,unsetPolicy是一个禁止操作。在没有显式策略设置的目录上取消设置策略不会返回错误。
  • [-listPolicies]:列出所有(启用、禁用和删除)在 HDFS 注册的欧共体政策
    。只有启用的策略适合与setPolicy命令一起使用。
  • [-addPolicies -policyFile <file>]:添加欧共体政策列表。示例策略文件请参考
    etc/hadoop/user_ec_policies.xml.template
    最大单元大小在属性
    dfs.namenode.ec.policies.max.cellsize中定义,默认值为 4 MB。
    目前 HDFS 允许用户总共添加 64 个策略,添加的策略 ID 在 64 到 127 之间。如果已经添加了 64 个策略,添加策略将失败。
  • [-listCodecs]:获取系统中支持的 EC 编解码器和编码器列表。
    编码器是编解码器的一种实现。一个编解码器可以有不同的实现,因此有不同的编码器。编解码器的编码器列在秋季
    倒序中。
  • [-removePolicy -policy <policyName>]:删除欧共体政策
  • [-enablePolicy -policy <policyName>]:启用欧共体政策
  • [-disablePolicy -policy <policyName>]:禁用欧共体政策

通过使用-listPolicies,您可以列出当前在您的集群
中设置的所有 EC 策略,以及这些策略的状态,无论它们是ENABLED还是DISABLED:

Lets test out EC in our cluster. First we will create directories in the HDFS shown as follows:

./bin/hdfs dfs -mkdir /user/normal
./bin/hdfs dfs -mkdir /user/ec

创建两个目录后,您可以在任何路径上设置策略:

./bin/hdfs ec -setPolicy -path /user/ec -policy RS-6-3-1024k
Set RS-6-3-1024k erasure coding policy on /user/ec

现在将任何内容复制到/user/ec文件夹中都会落入新设置的policy中。

键入如下所示的命令进行测试:

./bin/hdfs dfs -copyFromLocal ~/Documents/OnlineRetail.csv /user/ec

下面的截图显示了复制的结果,正如预期的那样,系统会抱怨,因为我们的本地系统上没有足够的集群来实现 EC。但这应该能让我们了解需要什么,以及它会是什么样子:

数据节点内平衡器

虽然 HDFS 一直有一个很好的功能,即在集群中的数据节点之间平衡数据,但这通常会导致数据节点内的磁盘倾斜。例如,如果您有四个磁盘,两个磁盘可能会占用大部分数据,另外两个磁盘可能利用率不足。考虑到物理磁盘(比如 7,200 或 10,000 rpm)的读/写速度较慢,这种数据倾斜会导致性能不佳。使用节点内平衡器,我们可以在磁盘之间重新平衡数据。

运行以下示例中显示的命令,在数据节点上调用磁盘平衡:

./bin/hdfs diskbalancer -plan 10.0.0.103

以下是磁盘平衡器命令的输出:

安装 Yarn 时间线服务 v.2

Yarn 时间线服务 v.2 部分所述, v.2 始终选择 Apache HbSe 作为主要后备存储,因为 Apache HbSe 甚至可以很好地扩展到更大的集群,并继续保持良好的读写响应时间。

为时间轴服务 v.2 准备存储需要执行几个步骤:

  1. 设置 HBase 集群
  2. 启用协处理器
  3. 为时间轴服务 v.2 创建模式

以下各节将更详细地解释每个步骤。

设置 HBase 群集

第一步包括选择一个 Apache HBase 集群用作存储集群。时间轴服务 v.2 支持的 Apache HBase 版本是 1.2.6。1.0.x 版本不再与时间轴服务 v.2 一起工作。HBase 的更高版本尚未与时间轴服务一起测试。

HBase 的简单部署

如果您想要为 Apache HBase 集群创建一个简单的部署配置文件,其中数据
加载量很小,但数据需要跨节点来来往往保持不变,那么您可以
考虑独立 HBase 而不是 HDFS 部署模式。

http://mirror.cogentco.com/pub/apache/hbase/1.2.6/

以下截图是 HBase 1.2.6 的下载链接:

下载hbase-1.2.6-bin.tar.gz到你的本地机器。然后提取 HBase
二进制文件:

tar -xvzf hbase-1.2.6-bin.tar.gz

以下是提取的糖化血红蛋白的内容:

这是独立的 HBase 设置的一个有用的变体,它让所有的 HBase 守护程序在一个 JVM 中运行,但是它不是持久保存在本地文件系统中,而是持久保存在 HDFS 实例中。向复制数据的 HDFS 进行写入可确保数据在节点来来往往时保持不变。要配置这个独立变体,编辑您的hbasesite.xml设置hbase.rootdir指向您的 HDFS 实例中的一个目录,然后将hbase.cluster.distributed设置为false

下面是我们安装的本地集群的hbase-site.xmlhdfs端口9000作为一个属性。如果不考虑这一点,将不会安装 HBase 集群。

<configuration>
    <property>
        <name>hbase.rootdir</name>
        <value>hdfs://localhost:9000/hbase</value>
    </property>
    <property>
        <name>hbase.cluster.distributed</name>
        <value>false</value>
    </property>
</configuration>

下一步是启动 HBase。我们将通过使用start-hbase.sh脚本来做到这一点:

./bin/start-hbase.sh

下面的截图显示了我们刚刚安装的 HBase 集群:

以下屏幕截图显示了显示各种组件版本的 HBase 群集设置的更多属性:

Figure: Screenshot of attributes of the HBase cluster setup and the versions of different components

一旦准备好使用 Apache HBase 集群,请执行以下部分中的步骤。

启用协处理器

在这个版本中,协处理器是动态加载的。

将时间轴服务.jar复制到 HDFS,在那里 HBase 可以加载它。它是在模式创建器中创建 flowrun 表所必需的。默认的 HDFS 位置是/hbase/coprocessor

例如:

hadoop fs -mkdir /hbase/coprocessor hadoop fs -put hadoop-yarn-server-timelineservice-hbase-3.0.0-alpha1-SNAPSHOT.jar /hbase/coprocessor/hadoop-yarn-server-timelineservice.jar

为了将罐子放在 HDFS 的不同位置,还存在一个名为yarn.timeline-service.hbase.coprocessor.jar.hdfs.location的 Yarn 配置设置,如下所示:

<property>
  <name>yarn.timeline-service.hbase.coprocessor.jar.hdfs.location</name>
  <value>/custom/hdfs/path/jarName</value>
</property>

使用模式创建工具创建时间线服务模式。为了实现这一点,我们还需要确保所有的 JARs 都被正确找到:

export HADOOP_CLASSPATH=$HADOOP_CLASSPATH:/Users/sridharalla/hbase-1.2.6/lib/:/Users/sridharalla/hadoop-3.1.0/share/hadoop/yarn/timelineservice/

更正类路径后,我们可以使用一个简单的命令创建 HBase 模式/表,如下所示:

./bin/hadoop org.apache.hadoop.yarn.server.timelineservice.storage.TimelineSchemaCreator -create -skipExistingTable

以下是根据前面的命令创建的 HBase 架构:

启用时间轴服务 v.2

以下是启动时间轴服务 v.2 的基本配置:

<property>
  <name>yarn.timeline-service.version</name>
  <value>2.0f</value>
</property>

<property>
  <name>yarn.timeline-service.enabled</name>
  <value>true</value>
</property>

<property>
  <name>yarn.nodemanager.aux-services</name>
  <value>mapreduce_shuffle,timeline_collector</value>
</property>

<property>
  <name>yarn.nodemanager.aux-services.timeline_collector.class</name>
  <value>org.apache.hadoop.yarn.server.timelineservice.collector.PerNodeTimelineCollectorsAuxService</value>
</property>

<property>
  <description> This setting indicates if the yarn system metrics is published by RM and NM by on the timeline service. </description>
  <name>yarn.system-metrics-publisher.enabled</name>
  <value>true</value>
</property>

<property>
  <description>This setting is to indicate if the yarn container events are published by RM to the timeline service or not. This configuration is for ATS V2\. </description>
  <name>yarn.rm.system-metrics-publisher.emit-container-events</name>
  <value>true</value>
</property>

此外,将hbase-site.xml配置文件添加到客户端 Hadoop 集群配置中,以便它可以将数据写入您正在使用的 Apache HBase 集群,或者将yarn.timeline-service.hbase.configuration.file设置为指向hbase-site.xml的文件 URL,用于将数据写入 HBase 的相同目的,例如:

<property>
  <description>This is an Optional URL to an hbase-site.xml configuration file. It is to be used to connect to the timeline-service hbase cluster. If it is empty or not specified, the HBase configuration will be loaded from the classpath. Else, they will override those from the ones present on the classpath. </description>
  <name>yarn.timeline-service.hbase.configuration.file</name>
  <value>file:/etc/hbase/hbase-site.xml</value>
</property>

运行时间轴服务 v.2

重新启动资源管理器和节点管理器以获取新配置。收集器以嵌入式方式在资源管理器和节点管理器中启动。

时间轴服务读取器是一个独立的 Yarn 守护程序,可以使用以下语法启动:

$ yarn-daemon.sh start timelinereader

启用 MapReduce 写入时间线服务 v.2

要将 MapReduce 框架数据写入时间轴服务 v.2,请在mapred-site.xml中启用以下配置:

<property>
  <name>mapreduce.job.emit-timeline-data</name>
  <value>true</value>
</property>

时间线服务仍在发展中,所以您应该尝试它,只是为了测试功能,而不是在生产中,并等待更广泛采用的版本,它应该很快就会推出。

摘要

在本章中,我们讨论了 Hadoop 3.x 中的新功能,以及它如何提高 Hadoop 2.x 的可靠性和性能。我们还介绍了在本地机器上安装独立的 Hadoop 集群。

在下一章中,我们将一窥大数据分析的世界。

二、大数据分析概述

在这一章中,我们将讨论大数据分析,从一般观点开始,然后深入探讨用于深入了解数据的一些常见技术。本章向读者介绍了检查大型数据集以发现数据模式、生成报告和收集有价值见解的过程。我们将特别关注大数据的七个方面。我们还将学习数据分析和大数据;我们将看到大数据提供的挑战以及如何在分布式计算中应对这些挑战,并研究使用 Hive 和 Tableau 展示最常用技术的方法。

简而言之,本章将涵盖以下主题:

  • 数据分析导论
  • 大数据介绍
  • 使用 Apache Hadoop 的分布式计算
  • MapReduce 框架
  • 储备
  • ApacheSpark

数据分析导论

数据分析是在检查数据时应用定性和定量技术的过程,目的是提供有价值的见解。利用各种技术和概念,数据分析可以提供探索数据探索数据分析 ( EDA )以及得出数据结论验证数据分析 ( CDA )的手段。EDA 和 CDA 是数据分析的基本概念,理解两者之间的区别很重要。

EDA 涉及用于探索数据的方法、工具和技术,旨在发现数据中的模式和数据各种元素之间的关系。CDA 涉及用于根据假设和统计技术,或对数据的简单观察,为特定问题提供见解或结论的方法、工具和技术。

在数据分析过程中

一旦数据被认为是现成的,数据科学家就可以使用统计方法(如 SAS)对其进行分析和探索。数据治理也成为确保正确收集和保护数据的一个因素。另一个不太为人所知的角色是数据管理员,他专门负责将数据理解为字节;确切地说,它来自哪里,所有发生的转换,以及业务真正需要从数据的列或字段中得到什么。

企业中的不同实体可能以不同的方式处理地址,例如:

123 N Main St vs 123 North Main Street.

但是,我们的分析依赖于获得正确的地址字段,否则提到的两个地址将被认为是不同的,我们的分析将不会具有相同的准确性。

分析过程从基于分析师可能需要的数据仓库的数据收集开始,收集组织中的各种数据(销售、营销、员工、薪资、人力资源等)。数据管理员和治理团队在这里非常重要,以确保收集到正确的数据,并且任何被视为机密或私人的信息不会被意外导出,即使最终用户都是员工。社会安全号码 ( 社会安全号码)或完整地址可能不是一个好主意,因为这可能会给组织带来很多问题。

必须建立数据质量流程,以确保正在收集和设计的数据是正确的,并且符合数据科学家的需求。在此阶段,主要目标是发现并修复可能影响分析需求准确性的数据质量问题。常见的技术是对数据进行分析,清理数据以确保数据集中的信息一致,并删除任何错误和重复记录。

因此,分析应用可以通过几个学科、团队和技能集来实现。分析应用可以用来生成报告,一直到自动触发业务操作。例如,您可以简单地创建每日销售报告,每天早上 8 点通过电子邮件发送给所有经理。但是,您也可以与业务流程管理应用或一些定制的股票交易应用集成,以采取行动,如购买、出售或股票市场活动警报。你也可以考虑接收新闻文章或社交媒体信息,以进一步影响将要做出的决定。

数据可视化是数据分析的一个重要部分,当您查看大量指标和计算时,很难理解数字。相反,人们越来越依赖商业智能 ( BI )工具,如 Tableau、QlikView 等来探索和分析数据。当然,大规模的可视化,如显示全国所有优步汽车或显示纽约市供水的热图,需要更多定制应用或专门工具来构建。

管理和分析数据一直是所有行业中许多不同规模组织面临的挑战。企业一直在努力寻找一种实用的方法来获取关于客户、产品和服务的信息。当公司只有少数顾客购买他们的一些商品时,这并没有那么困难。这并不是什么大的挑战。但随着时间的推移,市场上的公司开始增长。事情变得更加复杂。现在,我们有品牌信息和社交媒体。我们有通过互联网买卖的东西。我们需要拿出不同的解决方案。随着网络开发、组织、定价、社交网络和细分,我们正在处理大量不同的数据,这些数据在处理、管理、组织和试图从数据中获得一些洞察力时带来了更多的复杂性。

大数据介绍

推特、脸书、亚马逊、威瑞森、梅西百货和全食超市都是使用数据分析来运营业务的公司,许多决策都基于数据分析。想想他们正在收集什么样的数据,他们可能会收集多少数据,然后他们可能会如何使用这些数据。

让我们看看前面看到的杂货店例子;如果商店开始扩大业务,建立数百家商店,会怎么样?自然,销售交易必须以比单一商店大几百倍的规模收集和存储。但是,再也没有企业独立运作了。外面有很多信息,从当地新闻、推文、Yelp 评论、客户投诉、调查活动、来自其他商店的竞争、当地不断变化的人口统计或经济等等。所有这些附加数据都有助于更好地理解客户行为和收入模型。

例如,如果我们看到对商店停车设施的负面情绪越来越多,那么我们可以对此进行分析,并采取纠正措施,例如验证停车或与城市公共交通部门谈判,以提供更频繁的火车或公共汽车,从而更好地到达。数据的数量和种类越来越多,虽然它提供了更好的分析,但也给试图存储、处理和分析所有数据的业务信息技术组织带来了挑战。事实上,看到大量的数据并不少见。

每天,我们都会创建超过 2500 万字节的数据(2 EB),据估计,仅在过去几年中,就有超过 90%的数据被生成:

1 KB = 1024 Bytes
1 MB = 1024 KB
1 GB = 1024 MB
1 TB = 1024 GB ~ 1,000,000 MB
1 PB = 1024 TB ~ 1,000,000 GB ~ 1,000,000,000 MB
1 EB = 1024 PB ~ 1,000,000 TB ~ 1,000,000,000 GB ~ 1,000,000,000,000 MB

自 20 世纪 90 年代以来的如此大量的数据,以及理解和理解数据的需要,产生了大数据这个术语。

2001 年,时任咨询公司 Meta Group Inc(被 Gartner 收购)分析师的道格·兰尼(Doug Laney)提出了三个 Vs(即多样性、速度和成交量)的概念。现在,我们指的是四个 Vs,而不是三个 Vs,三个 Vs 加上了数据的准确性

以下是大数据的四个 Vs,用来描述大数据的属性。

各种数据

数据可以从许多来源获得,例如天气传感器、汽车传感器、人口普查数据、脸书更新、推文、交易、销售和营销。数据格式既有结构化的,也有非结构化的。数据类型也可以不同,二进制、文本、JSON 和 XML。变化真的开始触及数据深度的表面。

数据速度

数据可以来自数据仓库、批处理模式文件档案、近乎实时的更新,或者来自您刚刚预订的优步之旅的即时实时更新。速度是指创建数据的速度越来越快,以及关系数据库处理、存储和分析数据的速度越来越快。

数据量

数据可以收集和存储一小时、一天、一个月、一年或十年。对于许多公司来说,数据量正在增长到 100 倍。量是指数据的规模,这是大数据变大的部分原因。

数据的准确性

可以对数据进行分析,以获得可操作的见解,但是由于跨数据源分析了如此多的所有类型的数据,因此很难确保正确性和准确性证明。

以下是大数据的四个方面:

为了理解所有数据并将数据分析应用于大数据,我们需要扩展数据分析的概念,以更大的规模运营,处理大数据的四个方面。这不仅改变了用于分析数据的工具、技术和方法,也改变了我们处理问题的方式。如果在 1999 年,一个 SQL 数据库被用于一个业务中的数据,为了现在处理相同业务的数据,我们将需要一个分布式 SQL 数据库,它是可扩展的,并且适应大数据空间的细微差别。

前面描述的四个虚拟环境已经不足以涵盖大数据分析的能力和需求,因此,如今经常听到的是七个虚拟环境而不是四个虚拟环境

数据的可变性

可变性是指其含义不断变化的数据。很多时候,组织需要开发复杂的程序,以便能够理解程序中的上下文并解码它们的确切含义。

形象化

当您需要在数据处理后以可读和可访问的方式呈现数据时,可视化就会出现在图片中。

价值

大数据很大,并且每天都在增加,但是数据也很混乱、嘈杂,并且不断变化。它有多种格式可供所有人使用,没有分析和可视化就无法使用。

使用 Apache Hadoop 的分布式计算

我们被智能冰箱、智能手表、电话、平板电脑、笔记本电脑、机场自助服务亭、自动取款机等设备包围着,在这些设备的帮助下,我们现在能够做几年前无法想象的事情。我们已经习惯了 Instagram、Snapchat、Gmail、脸书、Twitter 和 Pinterest 等应用,几乎不可能有一天不访问这些应用。今天,云计算向我们介绍了以下概念:

  • 基础设施即服务
  • 平台即服务
  • 软件即服务

幕后是高度可扩展的分布式计算世界,这使得存储和处理 Petabytes ( PB )数据成为可能:

  • 1 EB = 1024 PB(5000 万部蓝光电影)
  • 1 PB = 1024 TB (50,000 部蓝光电影)
  • 1 TB = 1024 GB (50 部蓝光电影)

一部电影一张蓝光光盘的平均大小约为 20 GB。

现在,分布式计算的范例实际上并不是一个真正的新课题,几十年来一直以某种形式或形式被追求,主要是在研究机构以及一些商业产品公司。大规模并行处理 ( MPP )是几十年前在海洋学、地震监测和太空探索等几个领域使用的范例。Teradata 等多家公司也实施了 MPP 平台,并提供商业产品和应用。

最终,谷歌和亚马逊等科技公司将可扩展分布式计算这一小众领域推向了新的进化阶段,最终导致伯克利大学创建了 Apache Spark。谷歌发表了一篇关于 MapReduce 以及谷歌文件系统 ( GFS )的论文,将分布式计算的原理带到了每个人的使用中。当然,这需要归功于道格·卡特,他实现了谷歌白皮书中给出的概念,并向世界介绍了 Hadoop。Apache Hadoop 框架是一个用 Java 编写的开源软件框架。框架提供的两个主要领域是存储和处理。对于存储,Apache Hadoop 框架使用基于 2003 年 10 月发布的 GFS 论文的 Hadoop 分布式文件系统 ( HDFS )。对于处理或计算,框架依赖于 MapReduce,这是基于 2004 年 12 月发布的一篇关于 MapReduce 的谷歌论文 MapReduce 框架从 V1(基于 JobTracker 和 TaskTracker)发展到 V2(基于 YARN)。

MapReduce 框架

MapReduce 是一个用于在 Hadoop 集群中计算大量数据的框架。MapReduce 使用 Yarn 来调度映射器和 Reduce 作为任务,使用容器。

下图显示了一个计算单词频率的 MapReduce 作业示例:

MapReduce 与 YARN 紧密合作,规划作业和作业中的各种任务,向集群管理器(资源管理器)请求计算资源,在集群的计算资源上调度任务的执行,然后执行执行计划。使用 MapReduce,您可以读写不同格式的许多不同类型的文件,并以分布式方式执行非常复杂的计算。我们将在下一章的 MapReduce 框架中看到更多这方面的内容。

储备

Hive 在 MapReduce 框架上提供了一个 SQL 层抽象,并做了一些优化。这是必要的,因为使用 MapReduce 框架编写代码很复杂。例如,对特定文件中的记录进行简单的计数至少需要几十行代码,这对任何人来说都是无效的。Hive 通过将来自 SQL 语句的逻辑封装到 MapReduce 框架代码中来抽象 MapReduce 代码,该框架代码在后端自动生成和执行。这为那些需要花更多时间处理有用数据的人节省了大量时间,而不是为每一项需要执行的任务和每一项作为工作一部分的计算进行锅炉板编码:

Hive 不是为在线事务处理而设计的,不提供实时查询和行级更新。

在本节中,我们将了解 Hive 以及如何使用它来执行分析,https://hive.apache.org/downloads.html:

点击下载链接,查看各种可下载文件,如下图所示:

下载并提取 Hive 二进制文件

在本节中,我们将提取下载的二进制文件,然后配置 Hive 二进制文件以开始一切:

tar -xvzf apache-hive-2.3.3-bin.tar.gz

一旦提取了 Hive 包,执行以下操作创建hive-site.xml:

cd apache-hive-2.3.3-bin
vi conf/hive-site.xml

在属性列表的顶部,粘贴以下内容:

<property>
 <name>system:java.io.tmpdir</name>
 <value>/tmp/hive/java</value>
</property>

hive-site.xml底部添加以下属性:

<property>
 <name>hive.metastore.local</name>
 <value>TRUE</value>

</property>
<property>
 <name>hive.metastore.warehouse.dir</name>
 <value>/usr/hive/warehouse</value>
 </property>

之后,使用 Hadoop 命令,创建hive所需的 HDFS 路径:

cd hadoop-3.1.0
./bin/hadoop fs -mkdir -p /usr/hive/warehouse
./bin/hadoop fs -chmod g+w /usr/hive/warehouse

安装 Derby

Hive 通过利用 MapReduce 框架工作,并使用表和模式为后台运行的 MapReduce 作业创建映射器和缩减器。为了维护关于数据的元数据,Hive 使用了 Derby 这种易于使用的数据库。在本节中,我们将了解如何安装 Derby,以便在我们的 Hive 安装中使用,https://db.apache.org/derby/derby_downloads.html:

  1. 使用命令提取 Derby,如以下代码所示:
tar -xvzf db-derby-10.14.1.0-bin.tar.gz
  1. 然后,将目录改为derby,创建一个名为data的目录。事实上,有几个命令要运行,所以我们将在下面的代码中列出所有这些命令:
export HIVE_HOME=<YOURDIRECTORY>/apache-hive-2.3.3-bin
export HADOOP_HOME=<YOURDIRECTORY>/hadoop-3.1.0
export DERBY_HOME=<YOURDIRECTORY>/db-derby-10.14.1.0-bin
export PATH=$PATH:$HADOOP_HOME/bin:$HIVE_HOME/bin:$DERBY_HOME/bin
mkdir $DERBY_HOME/data
cp $DERBY_HOME/lib/derbyclient.jar $HIVE_HOME/lib
cp $DERBY_HOME/lib/derbytools.jar $HIVE_HOME/lib
  1. 现在,使用一个简单的命令启动 Derby 服务器,如以下代码所示:
nohup startNetworkServer -h 0.0.0.0 
  1. 完成后,您必须创建并初始化derby实例:
schematool -dbType derby -initSchema --verbose
  1. 现在,您可以打开hive控制台了:
hive

使用 Hive

与关系数据仓库相反,嵌套数据模型具有复杂的类型,如数组、映射和结构。我们可以用PARTITIONED BY子句根据一个或多个列的值来划分表。此外,可以使用CLUSTERED BY列对表或分区进行分桶,并且可以通过SORT BY列在该桶内对数据进行排序:

  • :非常类似于 RDBMS 表,包含行和表。
  • 分区:配置单元表可以有多个分区。它们也被映射到子目录和文件系统。
  • :数据在 Hive 中也可以分桶。它们可以作为文件存储在底层文件系统的分区中。

Hive 查询语言提供了基本的类似 SQL 的操作。以下是 HQL 可以轻松完成的几项任务:

  • 创建和管理表和分区
  • 支持各种关系、算术和逻辑运算符
  • 评估函数
  • 将表的内容下载到本地目录,或将查询结果下载到 HDFS 目录

创建数据库

我们首先必须创建一个数据库来保存 Hive 中创建的所有表。这一步很简单,与大多数其他数据库相似:

create database mydb;

以下是显示查询执行的hive控制台:

然后,我们开始使用刚刚创建的数据库来创建数据库所需的表,如下所示:

use mydb;

以下是显示查询执行的hive控制台:

创建表格

一旦我们创建了一个数据库,我们就可以在数据库中创建一个表了。表的创建在语法上类似于大多数关系数据库管理系统(数据库系统,如 Oracle、MySQL):

create external table OnlineRetail (
 InvoiceNo string,
 StockCode string,
 Description string,
 Quantity integer,
 InvoiceDate string,
 UnitPrice float,
 CustomerID string,
 Country string
 ) ROW FORMAT DELIMITED
 FIELDS TERMINATED BY ','
 LOCATION '/user/normal';

以下是hive控制台及其外观:

我们将不讨论查询语句的语法,而是讨论如何使用 stinger 计划显著提高查询性能,如下所示:

select count(*) from OnlineRetail;

以下是显示查询执行的hive控制台:

SELECT 语句语法

以下是 Hive 的SELECT语句的语法:

SELECT [ALL | DISTINCT] select_expr, select_expr, ...
FROM table_reference
[WHERE where_condition]
[GROUP BY col_list]
[HAVING having_condition]
[CLUSTER BY col_list | [DISTRIBUTE BY col_list] [SORT BY col_list]]
[LIMIT number]
;

SELECT是 HiveQL 中的投影算子。要点是:

  • SELECT扫描由FROM子句指定的表格
  • WHERE给出过滤什么的条件
  • GROUP BY给出了指定如何聚合记录的列列表
  • CLUSTER BYDISTRIBUTE BYSORT BY指定排序顺序和算法
  • LIMIT指定要检索多少条记录:
Select Description, count(*) as c from OnlineRetail group By Description order by c DESC limit 5;

以下是显示查询执行的hive控制台:

select * from OnlineRetail limit 5;

以下是显示查询执行的hive控制台:

select lower(description), quantity from OnlineRetail limit 5;

以下是显示查询执行的hive控制台:

where 子句

一个WHERE子句用于通过使用谓词操作符和逻辑操作符来过滤结果集,其帮助如下:

  • 谓词运算符列表
  • 逻辑运算符列表
  • 功能列表

下面是使用WHERE子句的一个例子:

select * from OnlineRetail where Description='WHITE METAL LANTERN' limit 5;

以下是显示查询执行的hive控制台:

下面的查询向我们展示了如何使用group by子句:

select Description, count(*) from OnlineRetail group by Description limit 5;

以下是显示查询执行的hive控制台:

以下查询是使用group by子句并指定条件来过滤借助having子句获得的结果的示例:

select Description, count(*) as cnt from OnlineRetail group by Description having cnt> 100 limit 5;

以下是显示查询执行的hive控制台:

下面的查询是使用 group by 子句的另一个示例,使用 having 子句过滤结果,并使用order by子句对我们的结果进行排序,这里使用DESC:

select Description, count(*) as cnt from OnlineRetail group by Description having cnt> 100 order by cnt DESC limit 5;

以下是显示查询执行的hive控制台:

插入语句语法

Hive 的INSERT语句语法如下:

-- append new rows to tablename1
INSERT INTO TABLE tablename1 select_statement1 FROM from_statement; 

-- replace contents of tablename1
INSERT OVERWRITE TABLE tablename1 select_statement1 FROM from_statement; 

-- more complex example using WITH clause
WITH tablename1 AS (select_statement1 FROM from_statement) INSERT [OVERWRITE/INTO] TABLE tablename2 select_statement2 FROM tablename1;

原始类型

类型与表中的列相关联。让我们看看下表中支持的类型及其描述:

| 类型 | 描述 |
| 整数 |

  • TINYINT: an integer of 1 byte.
  • : an integer of 2 bytes.
  • : an integer of 4 bytes.

|
| 布尔型 |

  • BOOLEAN : TRUE / FALSE

|
| 浮点数 |

  • FLOAT: Single precision
  • DOUBLE: Double precision

|
| 定点数字 |

  • DECIMAL: user-defined fixed-point value of scale and precision.

|
| 字符串类型 |

  • STRING: the character sequence in the specified character set.
  • VARCHAR: A character sequence with the maximum length in the specified character set.
  • CHAR: A character sequence of defined length in a specified character set.

|
| 日期和时间类型 |

  • TIMESTAMP: A specific time point, accurate to nanoseconds.
  • DATE: A date.

|
| 二元类型 |

  • BINARY: byte sequence

|

复杂类型

我们可以借助以下内容从基元和其他复合类型构建复杂类型:

  • 结构:使用点(.)符号可以访问类型中的元素。
  • 映射(键值元组):使用['element name']符号访问元素。
  • 数组(可索引列表):数组中的元素必须是同一类型。您可以使用[n]符号访问元素,其中 n 是数组的索引(从零开始)。

内置运算符和函数

下列运算符和函数不一定是最新的。(Hive 操作符和 UDF 有更多当前信息)。在 Beeline 或 Hive CLI 中,使用以下命令显示最新的文档:

SHOW FUNCTIONS;
DESCRIBE FUNCTION <function_name>;
DESCRIBE FUNCTION EXTENDED <function_name>;

所有 Hive 关键字都不区分大小写,包括 Hive 运算符和函数的名称。

内置运算符

关系运算符:根据操作数之间的比较是否成立,以下运算符比较传递的操作数并生成TRUEFALSE值:

| 运算符 | 类型 | 描述 |
| A = B | 所有基本类型 | TRUE如果表达式A相当于表达式B;否则FALSE |
| A != B | 所有基本类型 | TRUE如果表情A不是相当于表情B;否则FALSE |
| A < B | 所有基本类型 | TRUE如果表达式A小于表达式B;否则FALSE |
| A <= B | 所有基本类型 | TRUE如果表达式A小于或等于表达式B;否则FALSE |
| A > B | 所有基本类型 | TRUE如果表达式A大于表达式B;否则FALSE |
| A >= B | 所有基本类型 | 如果表达式A大于或等于表达式B,则为FALSE |
| A IS NULL | 所有类型 | 如果表达式A的计算结果为NULL,则为TRUE,否则为FALSE |
| A IS NOT NULL | 所有类型 | 如果表达式A的计算结果为NULL,则为FALSE,否则为TRUE |
| A LIKE B | 用线串 | TRUE如果字符串A与 SQL 简单正则表达式B匹配,则为FALSE。比较是逐个字符进行的。B中的_字符匹配A中的任意字符(类似于 posix 正则表达式中的.),B中的%字符匹配A中任意数量的字符(类似于 posix 正则表达式中的.*)。例如,foobar LIKE foo的评估结果为FALSE,其中作为foobar LIKE foo___的评估结果为TRUELIKE foo%的评估结果也是如此。要逃离%请使用\ ( %匹配一个%字符)。如果数据中包含分号,并且您想要搜索它,则需要对其进行转义;columnValue LIKE a\;b |
| A RLIKE B | 用线串 | NULL如果ABNULLTRUE如果A的任何(可能为空)子串与 Java 正则表达式B匹配(参见 Java 正则表达式语法),则为FALSE。例如,'foobar' rlike 'foo'评估为TRUE'foobar' rlike '^f.*r$'也是如此。 |
| A REGEXP B | 用线串 | 与RLIKE相同 |

算术运算符:以下运算符支持对操作数进行各种常见的算术运算。它们都返回数字类型:

| 操作员 | 类型 | 描述 |
| A + B | 所有数字类型 | 给出AB相加的结果。例如,结果的类型与操作数类型的公共父类型(在类型层次结构中)相同,因为每个整数都是浮点数。因此,float 是一个包含类型的整数,因此 float 和int上的+运算符将导致一个 float。 |
| A - B | 所有数字类型 | 给出A减去B的结果。结果的类型与操作数类型的公共父类型(在类型层次结构中)相同。 |
| A * B | 所有数字类型 | 给出AB相乘的结果。结果的类型与操作数类型的公共父类型(在类型层次结构中)相同。请注意,如果乘法导致溢出,则必须将其中一个运算符转换为类型层次结构中较高的类型。 |
| A / B | 所有数字类型 | 给出A除以B的结果。结果的类型与操作数类型的公共父类型(在类型层次结构中)相同。如果操作数是整数类型,那么结果就是除法的商。 |
| A % B | 所有数字类型 | 给出A除以B得到的提醒。结果的类型与操作数类型的公共父类型(在类型层次结构中)相同。 |
| A & B | 所有数字类型 | 给出AB的按位AND的结果。结果的类型与操作数类型的公共父类型(在类型层次结构中)相同。 |
| A &#124; B | 所有数字类型 | 给出AB的逐位OR结果。结果的类型与操作数类型的公共父级(在类型层次结构中)相同。 |
| A ^ B | 所有数字类型 | 给出AB的按位XOR的结果。结果的类型与操作数类型的公共父类型(在类型层次结构中)相同。 |
| ~A | 所有数字类型 | 给出A的按位NOT的结果。结果的类型与A的类型相同。 |

逻辑运算符:以下运算符支持创建逻辑表达式。根据操作数的布尔值,它们都返回布尔值TRUEFALSE:

| 操作员 | 类型 | 描述 |
| A AND B | 布尔代数学体系的 | 如果AB都是TRUE,则为真,否则为FALSE |
| A && B | 布尔代数学体系的 | 与A AND B相同 |
| A OR B | 布尔代数学体系的 | 如果AB或两者都是TRUE,则为真,否则为FALSE |
| A &#124;&#124; B | 布尔代数学体系的 | 与AB相同 |
| NOT A | 布尔代数学体系的 | TRUE如果AFALSE,否则FALSE |
| !A | 布尔代数学体系的 | 与NOT A相同 |

复杂类型上的运算符:以下运算符提供了访问复杂类型中元素的机制:

| 操作员 | 类型 | 描述 |
| A[n] | A是一个数组n是一个int | 返回数组A中第 n 元素。第一个元素的索引为 0,例如,如果A是由['foo', 'bar']组成的数组,那么A[0]返回'foo',而A[1]返回'bar' |
| M[key] | MMap<KV>键有类型K | 返回与地图中的键对应的值。例如,如果M是由
('f' -> 'foo', 'b' -> 'bar', 'all' -> 'foobar')组成的地图,则M['all']返回'foobar'。 |
| S.x | S是一个结构 | 返回Sx字段,例如,结构 foobar (int foo, int bar) foobar。foo返回存储在structfoo字段中的整数。 |

内置功能

Hive 支持以下内置功能:

| 数据类型 | 功能 | 描述 |
| BIGINT | round(double a) | 返回双倍的舍入BIGINT值。 |
| BIGINT | floor(double a) | 返回等于或小于两倍的最大BIGINT值。 |
| BIGINT | ceil(double a) | 返回等于或大于两倍的最小值BIGINT。 |
| double | rand(), rand(int seed) | 返回一个随机数(逐行变化)。指定种子将确保生成的随机数序列是确定性的。 |
| string | concat(string A, string B,...) | 返回在A后连接B得到的字符串。例如,concat('foo', 'bar')导致'foobar'。该函数接受任意数量的参数,并返回所有参数的串联。 |
| string | substr(string A, int start) | 返回从开始位置开始到字符串A结束的A子字符串。例如,substr('foobar', 4)导致'bar'。 |
| string | substr(string A, int start, int length) | 返回从给定长度的起始位置开始的A的子串,例如
substr('foobar', 4, 2)结果为'ba'。 |
| string | upper(string A) | 返回将A的所有字符转换为大写后得到的字符串,例如,upper('fOoBaR')结果为'FOOBAR'。 |
| string | ucase(string A) | 和鞋面一样。 |
| string | lower(string A) | 返回将B的所有字符转换为小写后得到的字符串,例如,lower('fOoBaR')结果为'foobar'。 |
| string | lcase(string A) | 和下面一样。 |
| string | trim(string A) | 返回从A两端修剪空格得到的字符串,例如trim('foobar ')得到'foobar'。 |
| string | ltrim(string A) | 返回从A开始(左手边)修剪空格后得到的字符串。例如,ltrim(' foobar ')导致'foobar '。 |
| string | rtrim(string A) | 返回从A的末端(右手边)修剪空格得到的字符串。例如,rtrim(' foobar')导致'foobar'。 |
| string | regexp_replace(string A, string B, string C) | 返回将B中与 Java 正则表达式语法(参见 Java 正则表达式语法)匹配的所有子字符串替换为C后得到的字符串。例如,regexp_replace('foobar', 'oo&#124;ar', )返回'fb'。 |
| int | size(Map<K.V>) | 返回地图类型中的元素数量。 |
| int | size(Array<T>) | 返回数组类型中的元素数量。 |
| value of <type> | cast(<expr> as <type>) | 将表达式expr的结果转换为<type>,例如,cast('1' as BIGINT)将字符串'1'转换为其整数表示。如果转换不成功,则返回null。 |
| string | from_unixtime(int unixtime) | 将 UNIX 纪元(1970-01-01 00:00:00 UTC)的秒数转换为以1970-01-01 00:00:00格式表示当前系统时区中该时刻时间戳的字符串。 |
| string | to_date(string timestamp) | 返回时间戳的日期部分string: to_date("1970-01-01 00:00:00") = "1970-01-01"。 |
| int | year(string date) | 返回日期或时间戳的年份部分string: year("1970-01-01 00:00:00") = 1970, year("1970-01-01") = 1970。 |
| int | month(string date) | 返回日期或时间戳的月份部分string: month("1970-11-01 00:00:00") = 11, month("1970-11-01") = 11。 |
| int | day(string date) | 返回日期或时间戳的日期部分string: day("1970-11-01 00:00:00") = 1, day("1970-11-01") = 1。 |
| string | get_json_object(string json_string, string path) | 根据指定的json路径从json字符串中提取json对象,并返回提取的.json对象的json字符串。如果输入的json字符串无效,将返回null。 |

Hive 中支持以下内置聚合函数:

| 数据类型 | 功能 | 描述 |
| BIGINT | count(*), count(expr), count(DISTINCT expr[, expr_.]) | count(*)–返回检索到的行的总数,包括包含NULL值的行;count(expr) –返回提供的表达式为非NULL的行数;count(DISTINCT expr[, expr])–返回所提供表达式唯一且非NULL的行数。 |
| DOUBLE | sum(col), sum(DISTINCT col) | 返回组中元素的总和或组中列的不同值的总和。 |
| DOUBLE | avg(col), avg(DISTINCT col) | 返回组中元素的平均值或组中列的不同值的平均值。 |
| DOUBLE | min(col) | 返回组中列的最小值。 |
| DOUBLE | max(col) | 返回组中列的最大值。 |

语言能力

Hive 的 SQL 提供了以下可以在表或分区上工作的基本 SQL 操作:

  • 借助WHERE子句过滤表格中的行
  • 使用SELECT子句从表中选择某些列
  • 在两个表之间执行相等连接
  • 为存储在表中的数据评估多个group by列上的聚合
  • 将查询结果存储到另一个表中
  • 将表的内容下载到本地(例如,nfs)目录
  • 将查询结果存储在hadoop dfs目录中
  • 管理表和分区(创建、删除和更改)
  • 为自定义映射/缩减作业插入所选语言的自定义脚本

检索信息的备忘单

下表向我们展示了如何检索一些常用函数的信息:

| 功能 | 鼠标 |
| 检索信息(常规) | SELECT from_columns FROM table WHERE conditions; |
| 检索所有值 | SELECT * FROM table; |
| 检索某些值 | SELECT * FROM table WHERE rec_name = "value"; |
| 用多个条件检索 | SELECT * FROM TABLE WHERE rec1 = "value1" AND rec2 = "value2"; |
| 检索特定列 | SELECT column_name FROM table; |
| 检索唯一输出 | SELECT DISTINCT column_name FROM table; |
| 整理 | SELECT col1, col2 FROM table ORDER BY col2; |
| 反向排序 | SELECT col1, col2 FROM table ORDER BY col2 DESC; |
| 数乌鸦 | SELECT COUNT(*) FROM table; |
| 带计数的分组 | SELECT owner, COUNT(*) FROM table GROUP BY owner; |
| 最大值 | SELECT MAX(col_name) AS label FROM table; |
| 从多个表中选择(使用别名w/"AS"连接同一个表) | SELECT pet.name, comment FROM pet JOIN event ON (pet.name = event.name) |

ApacheSpark

Apache Spark 是一个跨不同工作负载和平台的统一分布式计算引擎。Spark 可以连接到不同的平台,并使用各种范例(如 Spark Streaming、Spark ML、Spark SQL 和 Spark Graphx)处理不同的数据工作负载。

Apache Spark 是一个快速的内存数据处理引擎,具有优雅而富有表现力的开发 API,允许数据工作者高效地执行需要快速交互访问数据集的流式机器学习或 SQL 工作负载。

建立在核心之上的额外库允许工作负载用于流、SQL、图形处理和机器学习。例如,SparkML 是为数据科学设计的,它的抽象使数据科学变得更容易。

Spark 提供实时流、查询、机器学习和图形处理。在 Apache Spark 之前,我们必须针对不同类型的工作负载使用不同的技术。一个用于批处理分析,一个用于交互式查询,一个用于实时流处理,另一个用于机器学习算法。然而,Apache Spark 只需使用 Apache Spark 就可以完成所有这些任务,而不是使用不总是集成的多种技术。

使用 Apache Spark,可以处理所有类型的工作负载,Spark 还支持 Scala、Java、R 和 Python 作为编写客户端程序的手段。

Apache Spark 是一个开源分布式计算引擎,与 MapReduce 范式相比,它具有关键优势:

  • 尽可能使用内存处理
  • 用于批处理、实时工作负载的通用引擎
  • 与 Yarn 和介子兼容
  • 与 HBase、Cassandra、MongoDB、HDFS、亚马逊 S3 以及其他文件系统和数据源集成良好

Spark 早在 2009 年就在伯克利创建了,是构建 Mesos 项目的结果,Mesos 是一个支持不同类型集群计算系统的集群管理框架。

Hadoop 和 Apache Spark 都是流行的大数据框架,但它们并不真正服务于相同的目的。而 Hadoop 提供的是分布式存储和 MapReduce 分布式计算框架,而 Spark 则是在其他技术提供的分布式数据存储上运行的数据处理框架。

Spark is generally a lot faster than MapReduce because of the way it processes data. MapReduce operates on splits using disk operations, Spark operates on the dataset much more efficiently than MapReduce with the main reason behind the performance improvement of Apache Spark being the efficient off-heap in-memory processing rather than solely relying on disk-based computations.

如果您的数据操作和报告需求大部分是静态的,MapReduce 的处理风格就足够了,使用批处理来实现您的目的也是可以的,但是如果您需要对流式数据或多级处理逻辑中所需的处理需求进行分析,您可能想使用 Spark。

以下是 Apache Spark 堆栈:

使用 Tableau 实现可视化

无论我们使用哪种方法对大数据进行分布式计算,如果没有 Tableau 等工具的帮助,都很难理解数据的含义,Tableau 可以提供易于理解的数据可视化。

我们可以使用很多工具来做可视化,比如 Cognos、Tableau、Zoom data、KineticaDB、Python Matplotlib、R + Shiny、JavaScript 等等。我们将在第 10 章可视化大数据中更详细地介绍可视化。

下面是 Tableau 中的一个简单的水平条形图:

Figure: Screenshot showing a simple horizontal bar chart in Tableau

以下是 Tableau 中数据的地理空间视图:

Figure: Screenshot of a geospatial view of data in Tableau

摘要

在本章中,我们讨论了大数据分析、大数据分析的各种概念,以及大数据的七个方面—容量、速度、准确性、多样性、价值、愿景和可视化。我们还研究了一些有助于执行分析的技术,例如 Hive 和 Tableau。

在下一章中,我们将探索 MapReduce 的世界以及执行分布式计算时最常用的模式。

三、MapReduce 大数据处理

这一章将把我们在书中学到的一切都放入一个实际的用例中,构建一个端到端的管道来执行大数据分析。

简而言之,本章将涵盖以下主题:

  • MapReduce 框架
  • MapReduce 作业类型:
    • 单一映射程序作业
    • 单映射器缩减器作业
    • 多个映射器减速器作业
  • MapReduce 模式:
    • 聚合模式
    • 过滤模式
    • 连接模式

MapReduce 框架

MapReduce 是一个用于在 Hadoop 集群中计算大量数据的框架。MapReduce 使用 Yarn 来调度映射器和 Reduce 作为任务,使用容器。MapReduce 框架使您能够编写分布式应用,以可靠和容错的方式处理文件系统中的大量数据,例如 Hadoop 分布式文件系统 ( HDFS )。当您想要使用 MapReduce 框架来处理数据时,它通过创建作业来工作,然后在框架上运行作业来执行所需的任务。MapReduce 作业通常通过将输入数据拆分到工作节点,以并行方式运行映射器任务来工作。

此时,发生的任何故障,无论是 HDFS 级别的故障还是映射器任务的故障,都会被自动处理,以实现容错。一旦映射器完成,结果将通过网络复制到运行减速器任务的其他机器上。

下图显示了使用 MapReduce 作业统计单词频率的示例:

MapReduce 使用 Yarn 作为资源管理器,如下图所示:

MapReduce 这个术语实际上是指 Hadoop 程序执行的两个独立且不同的任务。第一个是 map 作业,它获取一组数据,并将其转换为另一组数据,其中各个元素被分解为元组(键/值对)。

reduce 作业将来自地图的输出作为输入,并将这些数据元组组合成更小的元组集。正如名称 MapReduce 的序列所暗示的,Reduce 作业总是在 map 作业之后执行。

MapReduce 作业的输入是分布在 HDFS 的数据存储中的一组文件。在 Hadoop 中,这些文件是用输入格式分割的,输入格式定义了如何将文件分割成输入分割。输入拆分是一个面向字节的文件块视图,由映射任务加载。Hadoop 中的每个映射任务都分为以下几个阶段:记录读取器、映射器、组合器和分割器。地图任务的输出,称为中间键和值,被发送到减速器。缩减任务分为以下几个阶段:洗牌、排序、缩减和输出格式。运行地图任务的节点最好位于数据所在的节点上。这样,数据通常不必在网络上移动,并且可以在本地计算机上计算。

在本章中,我们将研究不同的用例,以及如何使用 MapReduce 作业来产生所需的输出;为此,我们将使用一个简单的数据集。

资料组

第一个数据集是包含城市IDCity名称的城市表:

Id,City
1,Boston
2,New York
3,Chicago
4,Philadelphia
5,San Francisco
7,Las Vegas

该文件cities.csv可以下载,一旦下载,您可以通过运行命令将其移动到hdfs中,如以下代码所示:

hdfs dfs -copyFromLocal cities.csv /user/normal

第二个数据集是一个城市的每日温度测量数据,它包含测量的Date、城市ID和特定城市特定日期的Temperature:

Date,Id,Temperature
2018-01-01,1,21
2018-01-01,2,22
2018-01-01,3,23
2018-01-01,4,24
2018-01-01,5,25
2018-01-01,6,22
2018-01-02,1,23
2018-01-02,2,24
2018-01-02,3,25

该文件temperatures.csv可以下载,一旦下载,您可以通过运行命令将其移动到hdfs中,如以下代码所示:

hdfs dfs -copyFromLocal temperatures.csv /user/normal

以下是 MapReduce 程序的编程组件:

记录阅读器

输入阅读器将输入分成适当大小的分割(实际上,通常为 64 MB 到 128 MB),框架为每个映射函数分配一个分割。输入读取器从稳定存储(通常是分布式文件系统)中读取数据,并生成键/值对。

A common example will read a directory full of text files and return each line as a record.

记录读取器将输入格式生成的输入拆分转换为记录。记录读取器的目的是将数据解析成记录,而不是解析记录本身。它以键/值对的形式将数据传递给映射器。通常,上下文中的关键字是位置信息,值是组成记录的数据块。定制记录阅读器不在本书的讨论范围之内。我们通常假设您有一个合适的数据记录阅读器。LineRecordReader 是TextInputFormat提供的默认 RecordReader,它将输入文件的每一行都视为新值;相关的关键字是字节偏移量。LineRecordReader 总是跳过拆分中的第一行(或其一部分),如果它不是第一个拆分的话。它在末尾的分割边界后读取一行(如果数据可用,那么它不是最后一次分割)。

地图

map函数取一系列键/值对,分别处理,生成零个或多个输出键/值对。地图的输入和输出类型可以(通常)互不相同。

如果应用正在进行字数统计,map功能会将该行分解成单词,并为每个单词输出一个键/值对。每个输出对将包含作为关键字的单词和作为值的行中该单词的实例数。

在映射器中,对来自记录读取器的每个键/值对执行代码,以产生零个或多个新的键/值对,称为映射器的中间输出(也由键/值对组成)。决定每条记录的键和值与 MapReduce 作业完成的任务直接相关。关键是数据将被分组在什么上,而值是数据的一部分,将在减速器中使用,以生成必要的输出。模式中讨论的关键项目之一是不同类型的用例如何决定特定的键/值逻辑。事实上,这个逻辑的语义是 MapReduce 设计模式之间的一个关键区别。

组合器

如果每个映射器的每个输出都直接发送到每个缩减器,这将消耗大量的资源和时间。组合器是可选的本地化缩减器,可以在映射阶段对数据进行分组。它从映射器中获取中间键,并应用用户提供的方法在该映射器的小范围内聚合值。例如,因为聚合的计数是每个部分的计数之和,所以您可以生成一个中间计数,然后对这些中间计数求和以获得最终结果。在许多情况下,这大大减少了必须通过网络传输的数据量。例如,如果我们查看城市和温度的数据集,发送(波士顿,66)比通过网络发送(波士顿,20)、(波士顿,25)、(波士顿,21)需要更少的字节。组合器通常能带来显著的性能提升,而且没有任何负面影响。

我们将指出哪些模式受益于使用组合器,哪些模式不能使用组合器。组合器不能保证执行,因此它不能成为整个算法的一部分。

瓜分者ˌ分割者

分割器从映射器(或者组合器,如果正在使用的话)获取中间键/值对,并将它们分割成碎片,每个缩减器一个碎片。

应用的partition功能将每个map功能输出分配给特定的减速器,以便进行分割。partition功能,给定键和减压器数量,返回所需减压器的索引。

一个典型的缺省值是散列密钥,并使用hash值对减数器的数量进行模块计算:

partitionId = hash(key) % R, where R is number of Reducers

重要的是选择一个partition函数,该函数为每个分片提供近似均匀的数据分布,以实现负载平衡;否则,MapReduce 操作可能会被延迟,等待慢速的 Reduce 完成(也就是说,reduce 会分配更大份额的倾斜数据)。

在映射和缩减阶段之间,数据被混洗(并行排序,然后在节点之间交换),以便将数据从产生它们的映射节点移动到将在其中进行缩减的分片。根据网络带宽、中央处理器速度、产生的数据以及映射和减少计算所需的时间,混洗有时会比计算时间长。

默认情况下,分区器计算每个对象的哈希代码,通常是 md5 校验和。然后,它将键空间随机均匀地分布在缩减器上,但仍然确保不同映射器中具有相同值的键最终位于相同的缩减器上。可以通过排序等操作自定义分区器的默认行为。对于每个映射任务,分区数据被写入本地文件系统,并等待由相应的缩减器提取。

洗牌和排序

一旦映射器完成了输入数据处理(本质上,拆分数据并生成键/值对),输出就必须分布在集群中,以启动缩减任务。因此,缩减任务从洗牌和排序步骤开始,获取所有映射器和后续分区器写入的输出文件,并将它们下载到运行缩减任务的本地计算机。然后,这些单独的数据片段按键分类到一个更大的键/值对列表中。这种排序的目的是将等价的键组合在一起,这样它们的值就可以在 reduce 任务中轻松迭代。该框架自动处理一切,自定义代码能够控制键的排序和分组方式。

减少

减速器以分组数据为输入,每个按键分组运行一次reduce功能。该函数被传递给键和与该键相关联的所有值的迭代器。正如我们将在许多模式中看到的,在这个函数中可以发生广泛的处理。数据可以通过多种方式进行聚合、过滤和组合。一旦完成reduce功能,它会将零个或多个键/值对发送到最后一步,即输出格式。与map功能一样,reduce功能将因工作而异,因为它是解决方案中的核心逻辑。减速器可以有很多定制,包括将输出写入 HDFS,输出到弹性搜索索引,输出到关系数据库管理系统或 NoSQL,如卡珊德拉、HBase 等。

输出格式

输出格式翻译来自reduce函数的最终键/值对,并由记录写入器将其写入文件。默认情况下,它会用制表符分隔键和值,并用换行符分隔记录。这通常可以定制,以提供更丰富的输出格式,但最终,无论格式如何,数据都会被写入 HDFS。默认情况下,不仅支持写入 HDFS,还支持输出到弹性搜索索引、输出到关系数据库管理系统或 NoSQL,如卡珊德拉、糖化血红蛋白等。

MapReduce 作业类型

MapReduce 作业可以用多种方式编写,具体取决于期望的结果。MapReduce 作业的基本结构如下:

import java.io.IOException;
import java.util.StringTokenizer;
import java.util.Map;
import java.util.HashMap;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
import org.apache.commons.lang.StringEscapeUtils;

public class EnglishWordCounter {
public static class WordMapper
extends Mapper<Object, Text, Text, IntWritable> {
...
}
public static class CountReducer
extends Reducer<Text, IntWritable, Text, IntWritable> {
...
}

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

驱动程序的目的是编排作业。main的前几行都是关于解析命令行参数的。然后,我们开始设置作业对象,告诉它使用什么类进行计算,以及使用什么输入路径和输出路径。

让我们看看Mapper代码,它只是标记输入字符串,并将每个单词作为映射器的输出写入:

public static class WordMapper
extends Mapper<Object, Text, Text, IntWritable> {
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
public void map(Object key, Text value, Context context)
throws IOException, InterruptedException {
// Grab the "Text" field, since that is what we are counting over
String txt = value.toString()
StringTokenizer itr = new StringTokenizer(txt);
while (itr.hasMoreTokens()) {
word.set(itr.nextToken());
context.write(word, one);
}
}
}

最后是减速器代码,比较简单。reduce函数每个键组调用一次;在这种情况下,每个单词。我们将迭代这些值,这些值将是数字,并取一个连续的和。这个累计和的最终值将是以下值的总和:

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

有几种基本类型的 MapReduce 作业,如以下几点所示。

单一映射程序作业

单一映射器作业用于转换用例。如果我们只想更改数据的格式,比如某种转换,那么就使用这种模式:

| 方案 | 有些城市有简称,如波士顿、纽约等等 |
| 映射(键、值) | 关键词:城市名称值:短名称→如果城市是波士顿/波士顿,则转换为 BOS 否则,如果城市是纽约,则转换为纽约 |

现在,让我们看一个完整的仅单一映射程序作业的示例。为此,我们将简单地尝试从前面看到的temperature.csv文件中输出 cityID 和温度。

以下是代码:

package io.somethinglikethis;

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

public class SingleMapper
{
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        Job job = new Job(conf, "City Temperature Job");
        job.setMapperClass(TemperatureMapper.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        FileInputFormat.addInputPath(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }

    /*
    Date,Id,Temperature
    2018-01-01,1,21
    2018-01-01,2,22
    */
    private static class TemperatureMapper
            extends Mapper<Object, Text, Text, IntWritable> {

        public void map(Object key, Text value, Context context)
                throws IOException, InterruptedException {
            String txt = value.toString();
            String[] tokens = txt.split(",");
            String date = tokens[0];
            String id = tokens[1].trim();
            String temperature = tokens[2].trim();
            if (temperature.compareTo("Temperature") != 0)
                context.write(new Text(id), new IntWritable(Integer.parseInt(temperature)));
        }
    }

}

要执行这个作业,您必须使用您最喜欢的编辑器创建一个 Maven 项目,并编辑pom.xml使其看起来像下面的代码:

<?xml version="1.0" encoding="UTF-8"?>

<project  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <packaging>jar</packaging>
  <groupId>io.somethinglikethis</groupId>
  <artifactId>mapreduce</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>mapreduce</name>
  <!-- FIXME change it to the project's website -->
  <url>http://somethinglikethis.io</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-mapreduce-client-core</artifactId>
      <version>3.1.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-client</artifactId>
      <version>3.1.0</version>
    </dependency>
  </dependencies>
  <build>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-shade-plugin</artifactId>
          <version>3.1.1</version>
          <executions>
              <execution>
                  <phase>package</phase>
                  <goals>
                      <goal>shade</goal>
                  </goals>
              </execution>
          </executions>
            <configuration>
                <finalName>uber-${project.artifactId}-${project.version}</finalName>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                </transformers>
                <filters>
                    <filter>
                        <artifact>*:*</artifact>
                        <excludes>
                            <exclude>META-INF/*.SF</exclude>
                            <exclude>META-INF/*.DSA</exclude>
                            <exclude>META-INF/*.RSA</exclude>
                            <exclude>META-INF/LICENSE*</exclude>
                            <exclude>license/*</exclude>
                        </excludes>
                    </filter>
                </filters>
            </configuration>
        </plugin>
      </plugins>
  </build>
</project>

一旦你有了代码,你可以使用 Maven 来构建阴影/脂肪.jar,如下所示:

Moogie:mapreduce sridharalla$ mvn clean compile package
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building mapreduce 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ mapreduce ---
[INFO] Deleting /Users/sridharalla/git/mapreduce/target
.......
............

你应该在目标目录中看到一个uber-mapreduce-1.0-SNAPSHOT.jar;现在我们已经准备好执行任务了。

Make sure that the local Hadoop cluster, as seen in Chapter 1Introduction to Hadoop, is started, and that you are able to browse to http://localhost:9870.

为了执行该作业,我们将使用 Hadoop 二进制文件和我们之前构建的 fat .jar,如以下代码所示:

export PATH=$PATH:/Users/sridharalla/hadoop-3.1.0/bin
hdfs dfs -chmod -R 777 /user/normal

现在,运行命令,如以下代码所示:

hadoop jar target/uber-mapreduce-1.0-SNAPSHOT.jar io.somethinglikethis.SingleMapper /user/normal/temperatures.csv /user/normal/output/SingleMapper

作业将运行,您应该能够看到如下代码所示的输出:

Moogie:target sridharalla$ hadoop jar uber-mapreduce-1.0-SNAPSHOT.jar io.somethinglikethis.SingleMapper /user/normal/temperatures.csv /user/normal/output/SingleMapper
2018-05-20 18:38:01,399 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
2018-05-20 18:38:02,248 INFO impl.MetricsConfig: loaded properties from hadoop-metrics2.properties
......

特别注意输出计数器:

Map-Reduce Framework
 Map input records=28
 Map output records=27
 Map output bytes=162
 Map output materialized bytes=222
 Input split bytes=115
 Combine input records=0
 Combine output records=0
 Reduce input groups=6
 Reduce shuffle bytes=222
 Reduce input records=27
 Reduce output records=27
 Spilled Records=54
 Shuffled Maps =1
 Failed Shuffles=0
 Merged Map outputs=1
 GC time elapsed (ms)=13
 Total committed heap usage (bytes)=1084227584

这表明映射器输出了 27 条记录,并且没有缩减器动作,所有输入记录都是以 1:1 的方式输出的。您可以使用 HDFS 浏览器检查这一点,只需使用http://localhost:9870并跳转到/user/normal/output下显示的输出目录,如下图所示:

Figure: Screenshot showing how to check output from output directory

现在找到SingleMapper文件夹,进入这个目录,如下图截图所示:

Figure: Screenshot showing SingleMapper folder

深入到这个SingleMapper文件夹:

Figure: Screenshot showing further down in the SingleMapper folder

最后,点击如下截图所示的part-r-00000文件:

Figure: Screenshot showing the file to be selected

您将看到一个显示文件属性的屏幕,如下图所示:

Figure: screenshot showing the file properties

使用前面截图中的 head/tail 选项,您可以查看文件的内容,如下图所示:

Figure: Screenshot showing content of the file

这将SingleMapper作业的输出显示为简单地写入每行的城市标识和温度,无需任何计算。

You can also use the command line to view the contents of output hdfs dfs -cat /user/normal/output/SingleMapper/part-r-00000.

输出文件内容如以下代码所示:

1 25
1 21
1 23
1 19
1 23
2 20
2 22
2 27
2 24
2 26
3 21
3 25
3 22
3 25
3 23
4 21
4 26
4 23
4 24
4 22
5 18
5 24
5 22
5 25
5 24
6 22
6 22

SingleMapper作业执行到此结束,输出如预期。

单一映射器缩减器作业

单一映射器缩减器作业用于聚合用例。如果我们想做一些聚合,比如计数,按键,那么使用这个模式:

| 方案 | 计算城市的总/平均温度 |
| 映射(键、值) | 关键:城市价值:它们的温度 |
| 减少 | 按城市分组,取每个城市的平均温度 |

现在让我们看一个完整的仅映射器缩减器作业的例子。为此,我们将简单地尝试从前面看到的temperature.csv文件中输出 cityID 和平均温度。

以下是代码:

package io.somethinglikethis;

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

import java.io.IOException;

public class SingleMapperReducer
{
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        Job job = new Job(conf, "City Temperature Job");
        job.setMapperClass(TemperatureMapper.class);
        job.setReducerClass(TemperatureReducer.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        FileInputFormat.addInputPath(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }

    /*
    Date,Id,Temperature
    2018-01-01,1,21
    2018-01-01,2,22
    */
    private static class TemperatureMapper
            extends Mapper<Object, Text, Text, IntWritable> {

        public void map(Object key, Text value, Context context)
                throws IOException, InterruptedException {
            String txt = value.toString();
            String[] tokens = txt.split(",");
            String date = tokens[0];
            String id = tokens[1].trim();
            String temperature = tokens[2].trim();
            if (temperature.compareTo("Temperature") != 0)
                context.write(new Text(id), new IntWritable(Integer.parseInt(temperature)));
        }
    }

    private static class TemperatureReducer
            extends Reducer<Text, IntWritable, Text, IntWritable> {
        private IntWritable result = new IntWritable();
        public void reduce(Text key, Iterable<IntWritable> values,
                           Context context) throws IOException, InterruptedException {
            int sum = 0;
            int n = 0;
            for (IntWritable val : values) {
                sum += val.get();
                n +=1;
            }
            result.set(sum/n);
            context.write(key, result);
        }
    }
}

现在,运行以下命令:

hadoop jar target/uber-mapreduce-1.0-SNAPSHOT.jar io.somethinglikethis.SingleMapperReducer /user/normal/temperatures.csv /user/normal/output/SingleMapperReducer

作业将运行,您应该能够看到输出,如下面显示输出计数器的代码所示:

Map-Reduce Framework
    Map input records=28
    Map output records=27
    Map output bytes=162
    Map output materialized bytes=222
    Input split bytes=115
    Combine input records=0
 Combine output records=0
    Reduce input groups=6
    Reduce shuffle bytes=222
    Reduce input records=27
 Reduce output records=6
    Spilled Records=54
    Shuffled Maps =1
    Failed Shuffles=0
    Merged Map outputs=1
    GC time elapsed (ms)=12
    Total committed heap usage (bytes)=1080557568

这表明映射器输出了 27 条记录,减速器输出了 6 条记录。您可以使用 HDFS 浏览器检查这一点,只需使用http://localhost:9870并跳转到/user/normal/output下显示的输出目录,如下图所示:

Figure: screenshot showing how to check output in the output directory

现在,找到SingleMapperReducer文件夹,进入这个目录,然后像在单一映射器部分一样向下钻取;然后使用前面截图中的 head/tail 选项,可以查看文件的内容,如下图所示:

这显示了SingleMapperReducer作业的输出,写入每行的城市标识和每个城市标识的平均温度。

You can also use command line to view the contents of output hdfs dfs -cat /user/normal/output/SingleMapperReducer/part-r-00000.

输出文件内容如以下代码所示:

1 22
2 23
3 23
4 23
5 22
6 22

SingleMapperReducer作业执行到此结束,输出如预期。

多映射器减速器作业

在连接用例中使用了多个 mappers reducer 作业。在这种设计模式中,我们的输入取自多个输入文件,以产生连接/聚合输出:

| 方案 | 我们必须找到整个城市的平均温度,但是我们有两个不同模式的文件,一个用于城市,另一个用于温度。输入文件 1 城市标识到名称输入文件 2 每个城市每天的温度 |
| 映射(键、值) | 地图 1(用于输入 1)我们需要编写一个程序来根据城市标识,写下名字然后准备密钥/值对(城市标识、名称)地图 2(用于输入 2)我们需要编写一个程序,根据城市 ID,写温度然后准备密钥/值对(城市标识、温度) |
| 减少 | 按城市 ID 分组取每个城市名称的平均温度。 |

现在让我们来看一个完整的单个映射器缩减器作业的例子。为此,我们将简单地尝试从前面看到的temperature.csv文件中输出 cityID 和平均温度。

以下是代码:

package io.somethinglikethis;

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

import java.io.IOException;

public class MultipleMappersReducer
{
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        Job job = new Job(conf, "City Temperature Job");
        job.setMapperClass(TemperatureMapper.class);
        MultipleInputs.addInputPath(job, new Path(args[0]), TextInputFormat.class, CityMapper.class);
        MultipleInputs.addInputPath(job, new Path(args[1]), TextInputFormat.class, TemperatureMapper.class);

        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(Text.class);
        job.setReducerClass(TemperatureReducer.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        FileOutputFormat.setOutputPath(job, new Path(args[2]));

        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }

    /*
    Id,City
    1,Boston
    2,New York
    */
    private static class CityMapper

            extends Mapper<Object, Text, Text, Text> {

        public void map(Object key, Text value, Context context)
                throws IOException, InterruptedException {
            String txt = value.toString();
            String[] tokens = txt.split(",");
            String id = tokens[0].trim();
            String name = tokens[1].trim();
            if (name.compareTo("City") != 0)
                context.write(new Text(id), new Text(name));
        }
    }

    /*
    Date,Id,Temperature
    2018-01-01,1,21
    2018-01-01,2,22
    */
    private static class TemperatureMapper
            extends Mapper<Object, Text, Text, Text> {

        public void map(Object key, Text value, Context context)
                throws IOException, InterruptedException {
            String txt = value.toString();
            String[] tokens = txt.split(",");
            String date = tokens[0];
            String id = tokens[1].trim();
            String temperature = tokens[2].trim();
            if (temperature.compareTo("Temperature") != 0)
                context.write(new Text(id), new Text(temperature));
        }
    }

    private static class TemperatureReducer
            extends Reducer<Text, Text, Text, IntWritable> {
        private IntWritable result = new IntWritable();
        private Text cityName = new Text("Unknown");
        public void reduce(Text key, Iterable<Text> values,
                           Context context) throws IOException, InterruptedException {
            int sum = 0;
            int n = 0;

            cityName = new Text("city-"+key.toString());

            for (Text val : values) {
                String strVal = val.toString();
                if (strVal.length() <=3)
                {
                    sum += Integer.parseInt(strVal);
                    n +=1;
                } else {
                    cityName = new Text(strVal);
                }
            }
            if (n==0) n = 1;
            result.set(sum/n);
            context.write(cityName, result);
        }
    }
}

现在,运行命令,如以下代码所示:

hadoop jar target/uber-mapreduce-1.0-SNAPSHOT.jar io.somethinglikethis.MultipleMappersReducer /user/normal/cities.csv /user/normal/temperatures.csv /user/normal/output/MultipleMappersReducer

作业将运行,您应该能够看到如下输出计数器所示的输出:

Map-Reduce Framework -- mapper for temperature.csv
    Map input records=28
 Map output records=27
    Map output bytes=135
    Map output materialized bytes=195
    Input split bytes=286
    Combine input records=0
    Spilled Records=27
    Failed Shuffles=0
    Merged Map outputs=0
    GC time elapsed (ms)=0
    Total committed heap usage (bytes)=430964736

Map-Reduce Framework.  -- mapper for cities.csv
    Map input records=7
 Map output records=6
    Map output bytes=73
    Map output materialized bytes=91
    Input split bytes=273
    Combine input records=0
    Spilled Records=6
    Failed Shuffles=0
    Merged Map outputs=0
    GC time elapsed (ms)=10
    Total committed heap usage (bytes)=657457152

Map-Reduce Framework -- output average temperature per city name
    Map input records=35
 Map output records=33
    Map output bytes=208
    Map output materialized bytes=286
    Input split bytes=559
    Combine input records=0
    Combine output records=0
    Reduce input groups=7
    Reduce shuffle bytes=286
    Reduce input records=33
 Reduce output records=7
    Spilled Records=66
    Shuffled Maps =2
    Failed Shuffles=0
    Merged Map outputs=2
    GC time elapsed (ms)=10
    Total committed heap usage (bytes)=1745879040

这表明一个映射器输出了 27 条记录,Mapper2 输出了 6 条记录,reducer 输出了 7 条记录。您可以使用 HDFS 浏览器检查这一点,只需使用http://localhost:9870并跳转到/user/normal/output下显示的输出目录,如下图所示:

Figure: Check output in output directory

现在找到进入目录的MultipleMappersReducer文件夹,然后像在单一映射器部分一样向下钻取;然后,使用前面截图中的 head/tail 选项,可以查看文件的内容,如下图所示:

Figure: Content of the file

这将MultipleMappersReducer作业的输出显示为城市名称和每个城市的平均温度。如果一个城市标识在temperature.csv中没有相应的温度记录,平均值显示为 0。同样,如果一个城市 ID 在cities.csv中没有名字,那么这个城市的名字显示为 city-N

You can also use the command line to view the contents of output hdfs dfs -cat /user/normal/output/MultipleMappersReducer/part-r-00000.

输出文件内容如以下代码所示:

Boston 22
New York 23
Chicago 23
Philadelphia 23
San Francisco 22
city-6 22  //city ID 6 has no name in cities.csv only temperature measurements
Las Vegas 0 // city of Las vegas has no temperature measurements in temperature.csv

MultipleMappersReducer作业执行到此结束,输出如预期。

SingleMapperCombinerReducer 作业

SingleMapperReducer作业用于聚合用例。组合器,也称为半减速器,是一个可选类,通过接受来自 map 类的输入,然后将输出键/值对传递给减速器类来操作。组合器的目的是减少减速器的工作量;

在 MapReduce 程序中,25%的工作是在地图阶段完成的,也称为数据准备阶段,并行工作。同时 75%的工作是在阶段完成的,这个阶段被称为计算阶段,并不是平行的。因此,它比地图阶段慢。为了减少时间,可以在合并器阶段完成缩减阶段的一些工作。

例如,如果我们有一个组合器,那么我们将从映射器发送(波士顿,66),映射器将(波士顿,22),(波士顿,24),(波士顿,20)视为输入记录,而不是通过网络发送三个单独的密钥/密钥对记录。

方案

有几个城市,每个城市提供一个日温度,我们要计算这个城市的平均工资。但是,计算平均值有一定的规则。计算每个城市的城市总数后,我们可以计算每个城市的平均温度:

| 输入文件(几个文件) | 地图(平行)(,值=名称) | 合并(平行) | 减速器****(不平行) | 输出 |
| 城市 | 1 <10,20,25,45,15,45,25,20>2 <10,30,20,25,35> | 1 <250,20>2 <120,10> | one 波士顿,< 250,20,155,10,90,90,30>Two 纽约,< 120,10,175,10,135,10,110,10,130,10> | 波士顿<645>纽约<720> |
| 城市 | 12 | 12 | | |

现在,让我们来看看SingleMapperCombinerReducer作业的完整示例。为此,我们将简单地尝试从前面看到的temperature.csv文件中输出城市标识和平均温度。

以下是代码:

package io.somethinglikethis;

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

import java.io.IOException;

public class SingleMapperCombinerReducer
{
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        Job job = new Job(conf, "City Temperature Job");
        job.setMapperClass(TemperatureMapper.class);
        job.setCombinerClass(TemperatureReducer.class);
        job.setReducerClass(TemperatureReducer.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        FileInputFormat.addInputPath(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }

    /*
    Date,Id,Temperature
    2018-01-01,1,21
    2018-01-01,2,22
    */
    private static class TemperatureMapper
            extends Mapper<Object, Text, Text, IntWritable> {

        public void map(Object key, Text value, Context context)
                throws IOException, InterruptedException {
            String txt = value.toString();
            String[] tokens = txt.split(",");
            String date = tokens[0];
            String id = tokens[1].trim();
            String temperature = tokens[2].trim();
            if (temperature.compareTo("Temperature") != 0)
                context.write(new Text(id), new IntWritable(Integer.parseInt(temperature)));
        }
    }

    private static class TemperatureReducer
            extends Reducer<Text, IntWritable, Text, IntWritable> {
        private IntWritable result = new IntWritable();
        public void reduce(Text key, Iterable<IntWritable> values,
                           Context context) throws IOException, InterruptedException {
            int sum = 0;
            int n = 0;
            for (IntWritable val : values) {
                sum += val.get();
                n +=1;
            }
            result.set(sum/n);
            context.write(key, result);
        }
    }
}

现在,运行命令,如以下代码所示:

hadoop jar target/uber-mapreduce-1.0-SNAPSHOT.jar io.somethinglikethis.SingleMapperCombinerReducer /user/normal/temperatures.csv /user/normal/output/SingleMapperCombinerReducer

作业将运行,您应该能够看到如下输出计数器所示的输出:

Map-Reduce Framework
    Map input records=28
 Map output records=27
    Map output bytes=162
    Map output materialized bytes=54
    Input split bytes=115
    Combine input records=27
 Combine output records=6
    Reduce input groups=6
    Reduce shuffle bytes=54
    Reduce input records=6
 Reduce output records=6
    Spilled Records=12
    Shuffled Maps =1
    Failed Shuffles=0
    Merged Map outputs=1
    GC time elapsed (ms)=11
    Total committed heap usage (bytes)=1077936128

这表明映射器输出了 27 条记录,减速器输出了 6 条记录。但是,请注意,现在有一个合并器,它接收 27 个输入记录,输出 6 个记录,通过减少从映射器到缩减器的记录混洗,清楚地展示了性能增益。您只需使用http://localhost:9870并跳转到/user/normal/output下显示的输出目录,就可以使用 HDFS 浏览器对此进行检查,如下图所示:

现在找到SingleMapperCombinerReducer文件夹,进入这个目录,然后像前面的单一映射器部分一样向下钻取,然后使用前面屏幕中的头/尾选项,您可以查看文件的内容,如下图所示:

Figure: Check output in output directory

这会将SingleMapperCombinerReducer作业的输出显示为写入每行的城市标识和每个城市标识的平均温度:

Figure: Screenshot showing output of the SingleMapperCombinerReducer You can also use command line to view contents of output hdfs dfs -cat /user/normal/output/SingleMapperCombinerReducer/part-r-00000.

输出文件内容如以下代码所示:

1 22
2 23
3 23
4 23
5 22
6 22

SingleMapperCombinerReducer作业执行到此结束,输出如预期。

接下来,我们将研究编写 MapReduce 作业时使用的模式的更多细节。

MapReduce 模式

MapReduce 模式是一个模板,用于解决 MapReduce 常见的数据操作问题。模式并不特定于某个领域,例如文本处理或图形分析,但它是解决问题的通用方法。使用设计模式就是使用可靠的设计原则来构建更好的软件。

多年来,设计模式一直让开发人员的生活变得更加轻松。它们是以可重用和通用的方式解决问题的工具,因此开发人员可以花更少的时间来弄清楚他们将如何克服一个障碍并继续下一个障碍。

聚合模式

这一章着重于设计模式,这些模式产生了数据的顶层、汇总视图,因此您可以从单独查看一组本地化的记录中获得无法获得的见解。聚合或汇总、分析都是将相似的数据分组在一起,然后执行操作,例如计算统计数据、构建索引或简单计数。

本章中的模式是数字汇总、倒排索引和计数器计数:

聚合模式是一种用于计算数据的聚合统计值的通用模式,下面将详细讨论。在编写代码之前,正确使用组合器并理解正在执行的计算非常重要。基本上,逻辑是通过一个关键字段将记录组合在一起,并计算每个组的数字聚合。

当下列两个条件都成立时,可以使用聚合或数字汇总:

  • 你在处理数字数据或计数
  • 数据可以按特定字段分组

各城市的平均温度

应用输出记录中的每个城市作为关键字,每个温度作为值,从而按城市分组。然后,减少阶段将整数相加,并输出每个唯一的城市的平均温度。

记录计数

一种非常常见的汇总方法是获取按关键字分组的记录计数,并可能细分为每日、每周和每月计数。

最小/最大/计数

这是一种分析方法,用于确定特定事件的最小值、最大值和计数,例如第一次对城市进行采样、最后一次对城市进行采样以及在此期间测量温度的次数。如果您只对其中一个感兴趣,您不必同时收集所有这三个聚合,或者这里列出的任何其他用例。

平均/中位数/标准偏差

这类似于最小/最大/计数,但不像实现那样简单,因为这些操作是不相关的。组合器可以用于这三种情况,但是需要一种比重用缩减器实现更复杂的方法。

计算给定字段的最小、最大和计数的最小、最大和计数示例都是数字摘要模式的优秀应用。

SingleMapperReducer job seen earlier is a good example for aggregation pattern.

根据用例,可以定制聚合模式来生成预期的输出。

过滤模式

也称为转换模式,过滤模式查找数据的子集,无论它是小的,如前 10 名列表,还是大的,如重复数据消除的结果:

本章介绍了四种模式:过滤、布隆过滤、十大模式和独特模式。

作为最基本的模式,过滤作为其他一些模式的抽象模式。过滤只是单独评估每条记录,并根据某些条件决定它应该留下还是离开。过滤掉不感兴趣的记录,保留感兴趣的记录。考虑一个取一条记录并返回布尔值truefalse的评估函数f。如果此功能返回true,保留记录;否则,扔掉它。

The SingleMapper job seen earlier is a good example of a filtering patterns.

根据用例,可以定制一个转换模式来生成预期的输出。

连接模式

数据无处不在,虽然它本身非常有价值,但当我们开始一起分析这些集合时,我们可以发现有趣的关系。这就是连接模式发挥作用的地方。联接可用于用较小的引用集丰富数据,也可用于筛选或选择某种特殊列表中的记录。

要理解这些模式及其实现,您应该参考本章前面的MultipleMappersReducer作业。

缩写代码如下所示,显示了两个 mappers 和一个 reducer 类:

public class MultipleMappersReducer
{
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        Job job = new Job(conf, "City Temperature Job");
        job.setMapperClass(TemperatureMapper.class);
        MultipleInputs.addInputPath(job, new Path(args[0]), TextInputFormat.class, CityMapper.class);
        MultipleInputs.addInputPath(job, new Path(args[1]), TextInputFormat.class, TemperatureMapper.class);

        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(Text.class);
        job.setReducerClass(TemperatureReducer.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        FileOutputFormat.setOutputPath(job, new Path(args[2]));

        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }

    /*
    Id,City
    1,Boston
    2,New York
    */
    private static class CityMapper

            extends Mapper<Object, Text, Text, Text> {

        public void map(Object key, Text value, Context context)
                throws IOException, InterruptedException {
            String txt = value.toString();
            String[] tokens = txt.split(",");
            String id = tokens[0].trim();
            String name = tokens[1].trim();
            if (name.compareTo("City") != 0)
                context.write(new Text(id), new Text(name));
        }
    }

    /*
    Date,Id,Temperature
    2018-01-01,1,21
    2018-01-01,2,22
    */
    private static class TemperatureMapper
            extends Mapper<Object, Text, Text, Text> {

        public void map(Object key, Text value, Context context)
                throws IOException, InterruptedException {
            String txt = value.toString();
            String[] tokens = txt.split(",");
            String date = tokens[0];
            String id = tokens[1].trim();
            String temperature = tokens[2].trim();
            if (temperature.compareTo("Temperature") != 0)
                context.write(new Text(id), new Text(temperature));
        }
    }

    private static class TemperatureReducer
            extends Reducer<Text, Text, Text, IntWritable> {
        private IntWritable result = new IntWritable();
        private Text cityName = new Text("Unknown");
        public void reduce(Text key, Iterable<Text> values,
                           Context context) throws IOException, InterruptedException {
            int sum = 0;
            int n = 0;

            cityName = new Text("city-"+key.toString());

            for (Text val : values) {
                String strVal = val.toString();
                if (strVal.length() <=3)
                {
                    sum += Integer.parseInt(strVal);
                    n +=1;
                } else {
                    cityName = new Text(strVal);
                }
            }
            if (n==0) n = 1;
            result.set(sum/n);
            context.write(cityName, result);
        }
    }
}

该作业的输出显示在以下代码中:

Boston 22
New York 23
Chicago 23
Philadelphia 23
San Francisco 22
city-6 22 //city ID 6 has no name in cities.csv only temperature measurements
Las Vegas 0 // city of Las vegas has no temperature measurements in temperature.csv

内部连接

内部联接要求左右表具有相同的列。如果在左侧或右侧有重复或多个键的副本,连接将很快变成一种笛卡儿连接,要比设计正确的情况花费更长的时间来完成,以最小化多个键:

只有当城市标识同时具有以下代码所示的两个记录时,我们才会考虑城市和温度:

private static class InnerJoinReducer
        extends Reducer<Text, Text, Text, IntWritable> {
    private IntWritable result = new IntWritable();
    private Text cityName = new Text("Unknown");
    public void reduce(Text key, Iterable<Text> values,
                       Context context) throws IOException, InterruptedException {
        int sum = 0;
        int n = 0;

        for (Text val : values) {
            String strVal = val.toString();
            if (strVal.length() <=3)
            {
                sum += Integer.parseInt(strVal);
                n +=1;
            } else {
                cityName = new Text(strVal);
            }
        }
        if (n!=0 && cityName.toString().compareTo("Unknown") !=0) {
 result.set(sum / n);
 context.write(cityName, result);
 }
    }
}

输出将如以下代码所示(没有 city-6 或 Las Vegas,如前面原始输出所示):

Boston 22
New York 23
Chicago 23
Philadelphia 23
San Francisco 22

左反连接

左反连接只给出基于左侧表的那些行,这些行不在右侧表中。当您希望仅在右表中没有行时保留左表中的行时,请使用此选项。这提供了非常好的性能,因为只充分考虑了一个表,并且只检查了另一个表的连接条件:

如果 cityID 只有、名称而没有温度记录,我们将考虑城市和温度,如以下代码所示:

private static class LeftAntiJoinReducer
        extends Reducer<Text, Text, Text, IntWritable> {
    private IntWritable result = new IntWritable();
    private Text cityName = new Text("Unknown");
    public void reduce(Text key, Iterable<Text> values,
                       Context context) throws IOException, InterruptedException {
        int sum = 0;
        int n = 0;

        for (Text val : values) {
            String strVal = val.toString();
            if (strVal.length() <=3)
            {
                sum += Integer.parseInt(strVal);
                n +=1;
            } else {
                cityName = new Text(strVal);
            }
        }
        if (n==0 ) {
            if (n==0) n=1;
 result.set(sum / n);
 context.write(cityName, result);
 }
    }
}

输出将如以下代码所示:

Las Vegas 0 // city of Las vegas has no temperature measurements in temperature.csv

左外连接

左外联接给出了左侧表中的所有行,以及两个表共有的行(内联接)。如果在几乎没有共同点的表上使用,会导致非常大的结果,从而降低性能:

只有当城市标识既有记录又只有城市标识在cities.csv时,我们才会考虑城市和温度,如下代码所示:

private static class LeftOuterJoinReducer
        extends Reducer<Text, Text, Text, IntWritable> {
    private IntWritable result = new IntWritable();
    private Text cityName = new Text("Unknown");
    public void reduce(Text key, Iterable<Text> values,
                       Context context) throws IOException, InterruptedException {
        int sum = 0;
        int n = 0;

        for (Text val : values) {
            String strVal = val.toString();
            if (strVal.length() <=3)
            {
                sum += Integer.parseInt(strVal);
                n +=1;
            } else {
                cityName = new Text(strVal);
            }
        }
        if (cityName.toString().compareTo("Unknown") !=0)) {
            if (n==0) n = 1;
 result.set(sum / n);
 context.write(cityName, result);
 }
    }
}

输出显示在以下代码中:

Boston 22
New York 23
Chicago 23
Philadelphia 23
San Francisco 22
Las Vegas 0 // city of Las vegas has no temperature measurements in temperature.csv

右外连接

右外部联接给出右侧表中的所有行,以及左侧和右侧的公共行(内部联接)。使用它可以获得右表中的所有行,以及左表和右表中的行。如果不在左边,填写NULL。这里的性能类似于上表中提到的左外部连接:

只有当 cityID 既有记录又有温度测量值时,我们才会考虑城市和温度,如以下代码所示:

private static class RightOuterJoinReducer
        extends Reducer<Text, Text, Text, IntWritable> {
    private IntWritable result = new IntWritable();
    private Text cityName = new Text("Unknown");
    public void reduce(Text key, Iterable<Text> values,
                       Context context) throws IOException, InterruptedException {
        int sum = 0;
        int n = 0;

        for (Text val : values) {
            String strVal = val.toString();
            if (strVal.length() <=3)
            {
                sum += Integer.parseInt(strVal);
                n +=1;
            } else {
                cityName = new Text(strVal);
            }
        }
       if (n !=0) {
 result.set(sum / n);
 context.write(cityName, result);
 }
     }
}

输出如下:

Boston 22
New York 23
Chicago 23
Philadelphia 23
San Francisco 22
city-6 22 //city ID 6 has no name in cities.csv only temperature measurements

完全外部连接

完全外部联接给出联接子句左侧和右侧表中的所有(匹配和不匹配)行。当我们想要保留两个表中的所有行时,我们使用这个。当其中一个表匹配时,完全外部联接返回所有行。如果在几乎没有共同点的表上使用,可能会导致非常大的结果,从而降低性能:

只有当 cityID 同时具有这两个记录,或者它存在于其中一个表中时,我们才会考虑城市和温度,如下面的代码所示:

private static class FullOuterJoinReducer
        extends Reducer<Text, Text, Text, IntWritable> {
    private IntWritable result = new IntWritable();
    private Text cityName = new Text("Unknown");
    public void reduce(Text key, Iterable<Text> values,
                       Context context) throws IOException, InterruptedException {
        int sum = 0;
        int n = 0;

        for (Text val : values) {
            String strVal = val.toString();
            if (strVal.length() <=3)
            {
                sum += Integer.parseInt(strVal);
                n +=1;
            } else {
                cityName = new Text(strVal);
            }
        }
        if (n==0) n = 1;
 result.set(sum/n);
 context.write(cityName, result);
    }
}

输出如下:

Boston 22
New York 23
Chicago 23
Philadelphia 23
San Francisco 22
city-6 22 //city ID 6 has no name in cities.csv only temperature measurements
Las Vegas 0 // city of Las vegas has no temperature measurements in temperature.csv

左半连接

左半连接仅给出左侧表中的行,如果且仅当它们存在于右侧表中。当且仅当在右表中找到行时,使用此选项从左表中获取行。这与上一节中看到的左反连接相反。它不包括右侧值。它提供了非常好的性能,因为只充分考虑了一个表,并且只检查了另一个表的连接条件:

这类似于左外连接,只是我们将只从cities.csv输出左表记录。

交叉连接

交叉连接将左边的每一行与右边的每一行进行匹配,生成笛卡尔叉积。这需要谨慎使用,因为它是性能最差的联接,只能在特定的用例中使用:

这将输出所有城市的所有温度,生成 6×6 个记录(36 个输出记录)。通常不使用这种连接,因为输出可能非常大,而且在大多数情况下没有那么有用。

因此,我们可以使用多重映射方法实现不同的连接。

The multiple mappers reducer job seen earlier is a good example of a join pattern.

根据用例,可以定制连接模式来生成预期的输出。

摘要

在本章中,我们讨论了 MapReduce 框架、MapReduce 框架的各种组件以及 MapReduce 范式中的各种模式,这些模式可用于设计和开发 MapReduce 代码以满足特定的目标。

在下一章中,我们将了解 Python 语言,以及如何使用它对大数据进行分析。

四、基于 Python 和 Hadoop 的科学计算和大数据分析

在本章中,我们将介绍 Python 以及使用 Hadoop 和 Python 包分析大数据。我们将看到一个基本的 Python 安装,打开一个 Jupyter 笔记本,并通过一些例子进行工作。

简而言之,本章将涵盖以下主题:

  • 安装:
    • 下载并安装 Python
    • 下载并安装 Anaconda
    • 安装 Jupyter 笔记本
  • 数据分析

装置

在本节中,我们将了解使用 Python 解释器安装和设置 Jupyter Notebook 以执行数据分析所涉及的步骤。

安装标准 Python

用你的网络浏览器去http://www.python.org/download/的 Python 下载页面。Python 在 Windows、macOS 和 Linux 上受支持,您会发现不同的安装:

单击下载页面时,您将看到以下屏幕:

如果您点击一个特定的版本,如 3.6.5,那么您将进入一个不同的页面,如下图所示:

您可以阅读发行说明,然后通过向下滚动页面继续下载 Python 版本,如下图所示:

单击适合您的操作系统的正确版本并下载安装程序。下载完成后,在您的计算机上安装 Python。

安装蟒蛇

标准的 Python 安装有局限性,所以你必须安装 Jupyter、其他包、pip等等,才能让安装生产为你做好准备。Anaconda 是一个强调科学的一体化安装程序:它包括 Python、标准库和许多有用的第三方库。

使用浏览器,输入网址https://www.anaconda.com/download/–这将带您进入 Anaconda 下载页面,如下图所示:

下载适合您平台的 Anaconda 版本,然后按照网页https://docs.anaconda.com/anaconda/install/上的说明进行安装。

安装完成后,您应该可以打开 Anaconda Navigator(在 Windows 上,这是在“开始”菜单中,在 Mac 上,您可以简单地搜索)。

On Linux, typically you have to use the command line to launch Jupyter Notebook.

例如,在苹果电脑上,出现了如下截图所示的 Anaconda 导航器:

如果您使用的是 Anaconda Navigator,只需点击 Jupyter 笔记本启动按钮即可启动 Jupyter,如下图截图所示:

使用 Conda

到目前为止,Conda 命令行是成功设置 Python 安装最有用、最易于使用的工具。Conda 支持多个可以共存的环境,因此您可以设置 Python 2.7 环境和 Python 3.6 环境。如果你对深度学习感兴趣,你可以将 TensorFlow 设置为一个独立的环境,等等。

You can download and install conda by browsing to the URL https://conda.io/docs/user-guide/install/index.html.

下面的截图是 Conda 安装页面:

从链接下载 Conda 后,按照说明在您的机器上安装 Conda,如下图所示:

在命令行上输入conda list会显示所有安装的软件包。这将帮助您了解安装了哪些版本的软件包:

使用conda安装包装很容易。就像conda install <package name>一样简单。

例如,键入:

conda install scikit-learn

更重要的是,conda install Jupyter安装 Jupyter 笔记本,需要很多其他的包:

让我们尝试另一个重要的包:

conda install pandas

其他重要的包有:

conda install scikit-learn
conda install matplotlib
conda install seaborn

除了conda安装,我们还需要安装软件包来访问 HDFS (Hadoop)和打开文件(拼花格式):

pip install hdfs
pip install pyarrow

Jupyter 笔记本配置可以通过运行如下命令来生成:

[root@4b726275a804 /]# jupyter notebook --generate-config
 Writing default config to: /root/.jupyter/jupyter_notebook_config.py

Jupyter 需要身份验证,默认情况下这是一个令牌。但是,如果您想要创建基于密码的身份验证,那么只需运行下面代码中显示的命令来设置密码:

[root@4b726275a804 /]# jupyter notebook password
 Enter password:
 Verify password:
 [NotebookPasswordApp] Wrote hashed password to /root/.jupyter/jupyter_notebook_config.json

现在,我们已经准备好启动笔记本,因此键入以下命令:

jupyter notebook --allow-root --no-browser --ip=* --port=8888

以下是运行上述命令时的控制台:

当您打开浏览器并输入localhost:8888时,浏览器将打开登录屏幕,然后您必须输入前面步骤中设置的密码:

一旦提供了密码,Jupyter 笔记本门户就会打开,显示任何现有的笔记本。在这种情况下,我们没有以前的笔记本,所以下一步是创建一个。单击新建,然后为您的新笔记本选择 Python 2:

下面是一个新的笔记本,您现在可以在其中键入一些测试代码,如下图所示:

现在我们已经安装了 Python 和 Jupyter Notebook,我们准备使用 Notebooks 和 Python 语言进行数据分析。在下一节中,我们将深入研究可以进行的不同类型的数据分析。

数据分析

从随书提供的链接下载OnlineRetail.csv。然后,您可以使用熊猫加载文件。

以下是使用 Pandas 读取本地文件的简单方法:

import pandas as pd
path = '/Users/sridharalla/Documents/OnlineRetail.csv'
df = pd.read_csv(path)

然而,由于我们是在 Hadoop 集群中分析数据,我们应该使用hdfs而不是本地系统。以下是如何将hdfs文件加载到pandas数据帧的示例:

import pandas as pd
from hdfs import InsecureClient
client_hdfs = InsecureClient('http://localhost:9870')
with client_hdfs.read('/user/normal/OnlineRetail.csv', encoding = 'utf-8') as reader:
 df = pd.read_csv(reader,index_col=0)

下面是下面一行代码的作用:

df.head(3)

您将获得以下结果:

基本上,它显示了数据框中的前三个条目。

我们现在可以用数据做实验。输入以下内容:

len(df)

这将输出以下内容:

65499

这仅仅意味着数据帧的长度或大小。它告诉我们整个文件中有65,499个条目。

现在这样做:

df2 = df.loc[df.UnitPrice > 3.0]
df2.head(3)

我们定义了一个名为df2的新数据框,并将其设置为单价大于 3 的原始数据框中的所有条目。

然后,我们告诉它显示前三个条目,如下图所示:

以下代码行选择单价高于3.0的指数,并将其描述设置为Miscellaneous。然后显示前三项:

df.loc[df.UnitPrice > 3.0, ['Description']] = 'Miscellaneous'
df.head(3)

这就是结果:

如您所见,条目 2(索引为 1)的描述被更改为Miscellaneous,因为它的单价是 3.39 美元(正如我们之前指定的,这已经超过了 3 美元)。

代码行输出索引为 2 的数据:

df.loc[2]

输出如下:

最后,我们可以创建一个数量列的图,如下代码所示:

df['Quantity'].plot()

还有很多功能需要探索。

这里有一个使用.append()函数的例子。

我们定义了一个新的df对象df3,并将其设置为等于df的前 10 行加上df的第 200–209 行。换句话说,我们将第 200-209 行追加到df的第 0-9 行:

df3 = df[0:10].append(df[200:210])
df3

这是结果输出:

现在,假设您只关心几列,即库存代码数量发票日期单价。我们可以定义一个新的DataFrame对象,只包含数据中的那些列:

df4 = pd.DataFrame(df, columns=['StockCode', 'Quantity', 'InvoiceDate', 'UnitPrice']
df4.head(3)

这是以下结果:

熊猫提供了不同的方式来组合数据。更具体地说,我们可以合并连接加入,以及追加。我们已经介绍了 append,所以现在我们来看看连接数据。

看看这个代码块:

d1 = df[0:10]
d2 = df[10:20]

d3 = pd.concat([d1, d2])
d3

基本上,我们将d1设置为一个包含df前 10 个指数的DataFrame对象。然后,我们将d2设置为df的下十个指数。最后,我们将d3设置为d1d2的串联。这是它们连接后的结果:

我们可以做得更多。我们可以指定按键,这样可以更容易区分d1d2。看看下面的代码行:

d3 = pd.concat([d1, d2], keys=['d1', 'd2'])

如您所见,区分这两个数据集要容易得多。我们可以随意调用这些键,甚至像 xy 这样的简单键也可以。如果我们有三个数据集d1d2和一些d3,我们可以说键是( xyz ),这样我们就可以区分所有三个数据集。

现在,我们继续讨论不同列的连接。默认情况下,concat()功能使用外部连接。这意味着它组合了所有的列。想想 A 和 B 两组,其中 A 组包含所有属于d1的列名,B 组包含所有属于d2的列名。如果我们使用前面使用的代码行连接d1d2,我们将看到的列由 A 和 b 的并集表示

我们也可以指定要使用内部连接,用 A 和 b 的交集表示,看看下面几行代码:

d4 = pd.DataFrame(df, columns=['InvoiceNo', 'StockCode', 'Description'])[0:10]
d5 = pd.DataFrame(df, columns=['StockCode', 'Description', 'Quantity'])[0:10]

pd.concat([d4, d5])

如您所见,它使用了所有的列标签。

请记住,默认情况下,concat()使用外部连接。所以,说pd.concat([d4, d5])和说:

pd.concat([d4, d5], join='outer')

现在,我们使用内部连接。保持其他一切不变,但改变对concat()函数的调用。请看下面一行代码:

pd.concat([d4, d5], join='inner')

现在应该输出:

可以看到,这次我们只有d4d5共有的列标签。同样,我们可以添加键,以便更容易区分表中的两个数据集。

合并稍微复杂一些。这一次,您可以在外部联接、内部联接、左侧联接和右侧联接之间进行选择,还可以选择要合并的列。

让我们继续修改我们最初对d4d5的定义:

d4 = pd.DataFrame(df, columns=['InvoiceNo', 'StockCode', 'Description'])[0:11]
d5 = pd.DataFrame(df, columns=['StockCode', 'Description', 'Quantity'])[10:20]

你在d4定义的末尾看到的括号意味着我们将按照定义获取该特定DataFrame的前 11 个元素。d5 定义末尾的括号表示我们将元素 10 到 20 放入d5,而不是整个元素。

值得注意的是,他们将有一个重叠的元素,这将很快发挥作用。

首先从merge功能开始。让我们对d4d5进行左连接合并:

pd.merge(d4, d5, how='left')

这样做是使用了对d4d5中左侧数据框的所有列,并在此基础上添加了d5的列。如您所见,由于我们定义d5包含元素 10 到 20,因此没有从索引 0 到 10 的数量值。然而,由于元素 11 同时在d5d4中,我们在数量下看到了该元素的数据值。

同样,我们可以对右连接做同样的事情:

pd.merge(d4, d5, how='right')

现在,它使用d5的列标签以及d5的数据(从元素 10 到 20)。如您所见,索引 0 处的数据是与d4共享的,因此它在这个特定的表中完成。这是因为元素编号 11(索引 10)与d5(索引 10)的第一个元素重叠。

现在我们做内部连接:

pd.merge(d4, d5, how='inner')

内部连接意味着它只包含两个数据框共有的元素。在这种情况下,显示的元素是元素编号 11,索引 10 在df中。因为它存在于d4d5中,所以它既有发票号的数据,也有数量的数据(因为发票号的数据在d4中,而数量的数据在d5中)。

现在,我们将进行外部连接:

pd.merge(d4, d5, how='outer')

如您所见,外部连接意味着它包括所有列(列在d4d5中的并集)。

任何不存在的数据值都被标记为 NaN。例如d5中没有标注 InvoiceNo 的列,所以那里所有的数据值都显示为 NaN。

现在,让我们谈谈加入一个专栏。我们可以在函数调用中引入一个新参数on=。以下是股票代码栏的合并示例:

pd.merge(d4, d5, on='StockCode', how='left')

该图类似于我们使用左连接合并d4d5时生成的表格。但是,例外的是由于说明d4d5共有的一列,所以增加了两者,但分别用 _x_y 来区分。

正如你在最后一个条目中看到的,它被d4d5共享,所以 Description_xDescription_y 是相同的。

请记住,我们只能输入两个数据框共有的列名。所以,我们可以做股票代码或者描述来合并。

如果我们合并到描述上,看起来就是这样:

pd.merge(d4, d5, on='Description', how='left')

再次,通过添加 _x_y 分别表示d4d5来区分它们共享的列。

我们实际上可以传入一个列名列表,而不是一个列名。所以,现在我们有:

pd.merge(d4, d5, on=['StockCode', 'Description'], how='left')

然而,在这种情况下,我们可以看到,这是同一个表:

pd.merge(d4, d5, how='left')

这是因为在这种特殊情况下,我们传入的列表包含了他们共享的所有列名。如果他们共享三列,而我们只传入两列,情况就不是这样了。

为了说明这一点,假设这样:

d4 = pd.DataFrame(df, columns=['InvoiceNo', 'StockCode', 'Description', 'UnitPrice'])[0:11]
d5 = pd.DataFrame(df, columns=['StockCode', 'Description', 'Quantity', 'UnitPrice'])[10:20]

现在,让我们再试一次:

pd.merge(d4, d5, on=['StockCode', 'Description'], how='left')

所以,现在我们的桌子看起来像:

我们还可以指定希望所有的列都存在,即使是共享的列。

考虑一下:

pd.merge(d4, d5, left_index = True, right_index=True, how='outer')

您可以指定所需的任何连接类型,它仍会显示所有列。但是,在本例中,它将使用外部联接:

现在,我们可以进入join()功能。需要注意的一点是,如果两个数据框共享一个列名,它将不允许我们连接它们。所以,以下是不允许的:

d4 = pd.DataFrame(df, columns=['StockCode', 'Description', 'UnitPrice'])[0:11]
d5 = pd.DataFrame(df, columns=[ 'Description', 'Quantity', 'InvoiceNo'])[10:20]
d4.join(d5)

否则,会导致错误。

现在,看看下面几行代码:

d4 = pd.DataFrame(df, columns=['StockCode', 'UnitPrice'])[0:11]
d5 = pd.DataFrame(df, columns=[ 'Description', 'Quantity'])[10:20]
d4.join(d5)

这将产生这个表:

所以取d4 表,从d5添加列和对应的数据。由于d5没有从指数 0 到 9 的描述或数量数据,它们都显示为 NaN。由于d5d4都共享索引 10 的数据,因此该元素的所有数据都显示在相应的列中。

我们也可以反过来加入他们:

d4 = pd.DataFrame(df, columns=['StockCode', 'UnitPrice'])[0:11]
d5 = pd.DataFrame(df, columns=[ 'Description', 'Quantity'])[10:20]
d5.join(d4)

这是同样的逻辑,除了d4的列和相应的数据被添加到d5的表中。

接下来,我们可以使用combine_first()来组合数据。

请看下面的代码:

d6 = pd.DataFrame.copy(df)[0:5]
d7 = pd.DataFrame.copy(df)[2:8]

d6.loc[3, ['Quantity']] = 110
d6.loc[4, ['Quantity']] = 110

d7.loc[3, ['Quantity']] = 210
d7.loc[4, ['Quantity']] = 210
pd.concat([d6, d7], keys=['d6', 'd7'])

pd.DataFrame之后添加的.copy确保我们复制了原始的df,而不是编辑原始的df本身。这样,d6将指数34的数量改为110应该不会影响d7,反之亦然。请记住,如果您传入要选择的列列表,这将不起作用,因此您不能有类似以下的内容:

pd.DataFrame(df, columns=['Quantity', 'UnitPrice'])

运行前面的代码后,这就是结果表:

注意d6d7都有共同的元素,即索引为 2 到 4 的元素。

现在,看看这段代码:

d6.combine_first(d7)

这样做是把d7的数据和d6的数据结合起来,但是优先选择d6。请记住,我们在d6中将指数 3 和 4 的数量设置为110。如您所见,d6的数据保存在两个数据集有共同索引的地方。现在看看这一行代码:

d7.combine_first(d6)

现在你会看到,当两个元素有共同的索引时(在索引 3 和 4 处),保留d7 的数据。

您也可以使用value_counts()获得选择类别中每个值的出现次数。看看这段代码:

pd.value_counts(df['Country'])

在合并过程中需要考虑的一件事是,您可能会遇到重复的数据值。要解决这些问题,请使用.drop_duplicates()

考虑一下:

d1 = pd.DataFrame(df, columns = ['InvoiceNo', 'StockCode', 'Description'])[0:100]
d2 = pd.DataFrame(df, columns = ['Description', 'InvoiceDate', 'Quantity'])[0:100]

pd.merge(d1, d2)

如果我们一直滚动到底部:

如您所见,有许多重复的数据条目。要全部移除,我们可以使用drop_duplicates()。此外,我们可以指定可以使用哪些列数据来确定哪些条目是要删除的重复条目。例如,我们可以使用StockCode删除所有重复的条目,假设每个项目都有一个唯一的股票代码。我们还可以假设每个项目都有一个唯一的描述,并以这种方式删除项目。现在看看这段代码:

d1 = pd.DataFrame(df, columns = ['InvoiceNo', 'StockCode', 'Description'])[0:100]
d2 = pd.DataFrame(df, columns = ['Description', 'InvoiceDate', 'Quantity'])[0:100]

pd.merge(d1, d2).drop_duplicates(['StockCode'])

如果我们滚动到底部:

您将看到许多重复条目被删除。我们也可以通过DescriptionStockCodeDescription,它会产生同样的结果。

你会注意到指数到处都是。我们可以用reset_index()来修复。请看下面的代码:

d1 = pd.DataFrame(df, columns = ['InvoiceNo', 'StockCode', 'Description'])[0:100]
d2 = pd.DataFrame(df, columns = ['Description', 'InvoiceDate', 'Quantity'])[0:100]

d3 = pd.merge(d1, d2).drop_duplicates(['StockCode'])
d3.reset_index()

这就是它的样子:

显然,这不是你想要的。是的,它重置了索引,但是它添加了旧索引作为列。有一个简单的方法,那就是引入一个新的参数。现在,看看这段代码:

d3.reset_index(drop=True)

好多了。默认情况下,drop=False,所以如果不希望旧索引作为新列添加到数据中,那么记得设置drop=True

你可能还记得之前的.plot()功能。您可以使用它来帮助可视化数据帧,尤其是在数据帧很大的情况下。

这里有一个涉及单个列的例子:

d8 = pd.DataFrame(df, columns=['Quantity'])[0:100]
d8.plot()

这里,只选择前 100 个元素,以使图形不那么拥挤,并更好地说明示例。

现在,你将拥有:

现在,假设您希望显示多个列。请看以下内容:

d8 = pd.DataFrame(df, columns=['Quantity', 'UnitPrice'])[0:100]
d8.plot()

请记住,它不会绘制描述等定性数据列,只会绘制数量单价等可以绘制的东西。

摘要

在本章中,我们已经讨论了 Python 以及如何使用 Python 使用 Jupyter Notebook 执行数据分析。我们还研究了使用 Python 可以完成的几种不同的操作。

在下一章中,我们将研究另一种流行的分析语言 R,以及如何使用 R 来执行数据分析。

五、基于 R 和 Hadoop 的统计大数据计算

本章介绍了 R 以及如何使用 R 使用 Hadoop 对大数据进行统计计算。我们将看到从工作站上的开源 R 到像 Revolution R Enterprise 这样的并行化商业产品的各种选择,以及介于两者之间的许多其他选择。在这两个极端之间是一系列具有独特能力的选项:扩展数据、性能、功能和易用性。因此,正确的选择取决于您的数据大小、预算、技能、耐心和治理限制。

在本章中,我们将总结使用纯开源 r 的替代方案及其一些优势。此外,我们将描述通过结合开源和商业技术来实现更大规模、速度、稳定性和易开发性的选项。

简而言之,本章将涵盖以下主题:

  • 将 R 与 Hadoop 集成介绍
  • R 与 Hadoop 的集成方法
  • 用 R 进行数据分析

介绍

这一章是为了帮助目前对 Hadoop 不熟悉的 R 用户理解和选择要评估的解决方案而写的。和大多数开源的东西一样,首先考虑的当然是货币。不是一直都是吗?好消息是有多种免费的替代方案,并且在各种开源项目中正在开发额外的功能。

我们通常会看到使用完全开源的堆栈构建 R 和 Hadoop 集成的四个选项:

  • 在工作站上安装 R 并连接到 Hadoop 中的数据
  • 在共享服务器上安装 R 并连接到 Hadoop
  • 利用旋转打开
  • 使用 RMR2 在 MapReduce 内部执行 R

让我们在以下几节中详细介绍每个选项。

在工作站上安装 R 并连接到 Hadoop 中的数据

这种基线方法的最大优势是简单和成本。免费的。端到端免费。生活中还有什么?通过以开源形式提供的包 Revolution,包括rhdfsrhbase,R 用户可以直接从 Hadoop 中的hdfs文件系统和hbase数据库子系统中获取数据。这两个连接器都是 Revolution 创建和维护的 RHadoop 包的一部分,是首选。

还有其他选择。RHive 包直接从 R 执行 Hive 的 HQL(类似 SQL 的查询语言),并提供从 Hive 检索元数据的功能,如数据库名、表名、列名等。尤其是rhive包的优势在于,它的数据操作需要将一些工作下推到 Hadoop 中,避免了数据移动和大速度提升的并行操作。类似的下推也可以通过rhbase实现。然而,两者都不是特别丰富的环境,复杂的分析问题总是会暴露出一些能力差距。

除了有限的下推能力,R 最擅长处理从hdfshbasehive采集的适度数据;这样,当前的 R 用户就可以快速上手 Hadoop。

在共享服务器上安装 R 并连接到 Hadoop

一旦你厌倦了笔记本电脑上的内存障碍,显而易见的下一条路就是共享服务器。有了今天的技术,你只需花几千美元就可以装备一台强大的服务器,并在几个用户之间轻松共享。当使用具有 256 GB 或 512 GB 内存的窗口或 Linux 时,R 可以用来分析高达数百千兆字节的文件,尽管没有您希望的那么快。

与选项一一样,共享服务器上的 R 也可以利用rhbaserhive包的下推功能来实现并行性并避免数据移动。然而,与工作站一样,rhiverhbase的下推功能有限。

当然,虽然大量内存可以防止可怕的内存耗尽,但它对计算性能影响不大,并且依赖于分享在幼儿园学到的(或者可能没有学到的)技能。出于这些原因,可以认为共享服务器是工作站上 R 的一个很好的补充,但不是完全的替代品。

利用旋转打开

用 R 发行版Revolution R Open(RRO)取代 R 的 CRAN 下载,性能进一步提升。RRO 和 R 本身一样,是开源的,100% R,免费下载。它使用英特尔数学内核库加速数学计算,并且 100%兼容 CRAN 和其他存储库中的算法,如 BioConductor。不需要对 R 脚本进行任何更改,对于大量使用某些数学和线性代数原语的脚本,MKL 库提供的加速从可以忽略到一个数量级不等。如果你用语言做数学运算,你可以预期 RRO 可以让你的平均成绩翻倍。与选项一和选项二一样,RRO 可以与rhdfs等连接器一起使用,它可以通过rhbaserhive将工作连接并下推到 Hadoop 中。

使用 RMR2 在 MapReduce 内部执行 R

一旦你发现你的问题集太大,或者你的耐心在工作站或服务器上被耗尽,并且rhbaserhive下推的限制阻碍了进展,你就准备好在 Hadoop 内部运行 R 了。

开源 RHadoop 项目包括rhdfsrhbaseplyrmr,还有一个名为rmr2的包,可以让 R 用户使用 R 函数构建 Hadoop MapReduce 操作。使用映射器,R 函数被应用于组成一个hdfs文件、hbase表或其他数据集的所有数据块;结果可以发送到一个减速器,也是一个 R 函数,用于聚合或分析。所有的工作都是在 Hadoop 内部进行的,但内置于 R 中。让我们明确一点:将 R 函数应用于每个hdfs文件段是加速计算的好方法。但在很大程度上,避免移动数据才是真正强调性能的原因。为此,rmr2对 Hadoop 节点上的数据应用 R 函数,而不是将数据移动到 R 所在的位置。

虽然rmr2给出了本质上无限的能力,但作为数据科学家或统计学家,你的想法很快就会转向在大数据集上用 R 计算整个算法。以这种方式使用rmr2使 R 程序员的开发变得复杂,因为他或她必须编写所需算法的整个逻辑或修改现有的 CRAN 算法。然后,他/她必须验证算法是否准确,是否反映了预期的数学结果,并为诸如数据缺失等各种情况编写代码。

rmr2需要你自己编码来管理并行化。对于数据转换操作、聚合等来说,这可能是微不足道的,如果您试图在大型数据集上训练预测模型或构建分类器,这可能是相当繁琐的。虽然rmr2可能比其他方法更繁琐,但这并不是站不住脚的,大多数 R 程序员会发现rmr2比求助于基于 Java 的 Hadoop mappers 和 reducers 开发要容易得多。虽然有些乏味,但它:

  • 是完全开源的
  • 有助于并行化计算以处理更大的数据集
  • 跳过痛苦的数据移动
  • 被广泛使用,所以你会发现有帮助
  • 是免费的

rmr2不是该类别中的唯一选项;一个类似的名为rhipe的包也在那里,并提供类似的功能。rhipehttps://www.rhipe.com/download-confirmation/有描述,可从 GitHub 下载。

纯开源选项的总结和展望

在 Hadoop 中使用 R 的基于开源的选项的范围正在扩大。例如,Apache Spark 社区正在通过可预见的名称 SparkR 快速改进 R 集成。如今,SparkR 提供了从 R 到 Spark 的访问,就像今天rmr2rhipe为 Hadoop MapReduce 所做的那样。

我们预计,在未来,SparkR 团队将增加对 Spark 的 MLlib 机器学习算法库的支持,直接从 r 提供执行。可用性日期尚未广泛发布。

也许最令人兴奋的观察是,R 已经成为平台厂商的桌赌注。我们在 Cloudera、Hortonworks、MapR 和其他公司的合作伙伴,以及数据库供应商和其他公司,都敏锐地意识到了 R 在庞大且不断增长的数据科学社区中的主导地位,以及 R 作为从构建在 Hadoop 之上的新兴数据存储库中获取见解和价值的一种手段的重要性。

在随后的文章中,我将回顾通过将范围从仅开源解决方案扩展到像 Hadoop 的 Revolution R Enterprise 这样的解决方案,为 R 用户创造更高性能、简单性、可移植性和可扩展性的选项。

r 是一个惊人的数据科学编程工具,可以对模型进行统计数据分析,并将分析结果转换成彩色图形。毫无疑问,R 是统计学家、数据科学家、数据分析师和数据架构师最喜欢的编程工具,但在处理大型数据集时,它却有所欠缺。R 编程语言的一个主要缺点是所有对象都被加载到单机的主内存中。以千兆字节为单位的大型数据集无法加载到内存中;这也是 Hadoop 与 R 集成是理想解决方案的时候。为了适应 R 编程语言的内存、单机限制,数据科学家不得不将他们的数据分析限制在大数据集的数据样本上。当处理大数据时,R 编程语言的这种局限性是一个主要障碍。由于 R 的可伸缩性不是很高,核心 R 引擎只能处理有限的数据。

相反,像 Hadoop 这样的分布式处理框架对于大型数据集(petabyte 范围)上的复杂操作和任务是可扩展的,但不具备强大的统计分析能力。由于 Hadoop 是一个流行的大数据处理框架,将 R 与 Hadoop 集成是下一个合乎逻辑的步骤。在 Hadoop 上使用 R 将提供一个高度可扩展的数据分析平台,该平台可以根据数据集的大小进行扩展。将 Hadoop 与 R 集成可以让数据科学家在大型数据集上并行运行 R,因为 R 语言中的数据科学库都不能在大于内存的数据集上工作。借助 R 和 Hadoop 的大数据分析与商品硬件集群在垂直扩展方面提供的成本价值回报相竞争。

R 与 Hadoop 的集成方法

使用 Hadoop 的数据分析师或数据科学家可能有他们用于数据处理的 R 包或 R 脚本。为了在 Hadoop 中使用这些 R 脚本或 R 包,他们需要用 Java 编程语言或任何其他实现 Hadoop MapReduce 的语言重写这些 R 脚本。这是一个繁重的过程,可能会导致不必要的错误。为了将 Hadoop 与 R 编程语言集成,我们需要使用一个已经为 R 编写的软件,数据存储在 Hadoop 的分布式存储中。有许多使用 R 语言来执行大型计算的解决方案,但所有这些解决方案都要求在将数据分发到计算节点之前将其加载到内存中。这不是大型数据集的理想解决方案。以下是一些常用的将 Hadoop 与 R 集成的方法,以最大限度地利用 R 对大型数据集的分析能力。

RHADOOP–在工作站上安装 R 并连接到 HADOOP 中的数据

最常用的将 R 编程语言与 Hadoop 集成的开源分析解决方案是 RHadoop 。由 Revolution analytics 开发的 RHadoop 允许用户直接从 HBase 数据库子系统和 HDFS 文件系统中摄取数据。RHadoop 包是在 Hadoop 上使用 R 的最佳解决方案,因为它简单且具有成本优势。RHadoop 是五个不同包的集合,允许 Hadoop 用户使用 R 编程语言管理和分析数据。RHadoop 包与开源 Hadoop 兼容,也与流行的 Hadoop 发行版 Cloudera、Hortonworks 和 MapR 兼容:

  • rhbase:rhbase包使用一个节俭服务器为 R 内的 HBase 提供数据库管理功能。此包需要安装在将运行 R 客户端的节点上。使用rhbase,数据工程师和数据科学家可以从 r
  • rhdfs:rhdfs包为 R 程序员提供了与 Hadoop 分布式文件系统的连接,以便他们读取、写入或修改存储在 Hadoop HDFS 中的数据。
  • plyrmr:这个包支持对 Hadoop 管理的大数据集进行数据操作。plyrmr ( plyr用于 MapReduce)提供流行包中存在的数据操作操作,如reshape2plyr。这个包依赖 Hadoop MapReduce 来执行操作,但抽象了大部分 MapReduce 细节。
  • ravro:这个包允许用户从本地和 HDFS 文件系统读写 Avro 文件。
  • rmr2(在 Hadoop MapReduce 内部执行 R):使用这个包,R 程序员可以对一个 Hadoop 集群中存储的数据进行统计分析。使用rmr2将 R 与 Hadoop 集成可能是一个麻烦的过程,但是许多 R 程序员发现使用rmr2比依赖基于 Java 的 Hadoop 映射器和减压器要容易得多。rmr2可能有点乏味,但它消除了数据移动,并有助于处理大型数据集的并行计算。

RHIPE–在 Hadoop MapReduce 中执行 R

R 和 Hadoop 集成编程环境(RHIPE) 是一个 R 库,允许用户在 R 编程语言内运行 Hadoop MapReduce 作业。R 程序员只需要编写 R Map 和 R Reduce 函数,RHIPE 库会进行传递,调用相应的 Hadoop Map 和 Hadoop Reduce 任务。RHIPE 使用协议缓冲编码方案来传输映射和减少输入。与其他并行 R 包相比,使用 RHIPE 的优势在于它与 Hadoop 集成良好,并提供了一种在机器集群中使用 HDFS 的数据分发方案,该方案提供了容错能力并优化了处理器的使用。

R 和 Hadoop 流

Hadoop Streaming API 允许用户使用任何可执行脚本运行 Hadoop MapReduce 作业,该脚本从标准输入中读取数据,并将数据作为映射器或缩减器写入标准输出。因此,Hadoop 流应用编程接口可以在映射或缩减阶段与 R 编程脚本一起使用。这种集成 R 和 Hadoop 的方法不需要任何客户端集成,因为流作业是通过 Hadoop 命令行启动的。提交的 MapReduce 作业将通过 UNIX 标准流和序列化进行数据转换,以确保 Java 投诉输入到 Hadoop,而与程序员提供的输入脚本的语言无关。

你认为 R 与 Hadoop 集成的最佳方式是什么?

RHIVE–在工作站上安装 R 并连接到 Hadoop 中的数据

如果您希望从 R 接口启动 Hive 查询,那么 r Hive 是一个定位包,具有从 Apache Hive 中检索元数据(如数据库名、列名和表名)的功能。RHIVE 通过用 R 语言函数扩展 HiveQL,为存储在 Hadoop 中的数据提供了丰富的 R 编程语言可用的统计库和算法。RHIVE 函数允许用户将 R 统计学习模型应用于已经使用 Apache Hive 编目的 Hadoop 集群中存储的数据。使用 RHIVE 进行 Hadoop R 集成的优势在于,由于数据操作被下推到 Hadoop 中,因此可以并行化操作,避免数据移动。

ORCH–面向 Hadoop 的甲骨文连接器

ORCH 可用于非 Oracle Hadoop 集群或任何其他 Oracle 大数据设备。Mappers 和 Reduce 是用 R 编写的,MapReduce 作业是通过高级接口从 R 环境中执行的。有了 ORCH for R Hadoop 集成,R 程序员不必学习一门新的编程语言(如 Java)就能了解 Hadoop 环境的细节,如 Hadoop 集群硬件或软件。ORCH 连接器还允许用户通过相同的函数调用在本地测试 MapReduce 程序的能力,这要在它们部署到 Hadoop 集群之前很久。

使用 R 和 Hadoop 执行大数据分析的开源选项的数量在不断增加,但对于简单的 Hadoop MapReduce 作业,R 和 Hadoop Streaming 仍然被证明是最佳解决方案。R 和 Hadoop 的结合是从事大数据工作的专业人员的必备工具包,可结合您所需的性能、可扩展性和灵活性创建快速预测分析。

大多数 Hadoop 用户声称,使用 R 的优势在于其用于统计和数据可视化的详尽的数据科学库列表。然而,R 中的数据科学库本质上是非分布式的,这使得数据检索成为一件耗时的事情。这是 R 编程语言的一个内在限制,但是如果我们忽略它,那么 R 和 Hadoop 一起可以让大数据分析成为一种享受!

数据分析

r 允许我们进行各种各样的数据分析。我们用 Python 中的pandas所做的一切,我们也可以用 R 来做。

看看下面的代码:

df = read.csv(file=file.choose(), header=T, fill=T, sep=",", stringsAsFactors=F)

file.choose()表示将有一个新窗口,允许您选择要打开的数据文件。header=T表示会读取表头。fill=T表示它将为任何未定义或缺失的数据值填写 NaN。最后,sep=","表示知道如何区分.csv文件中不同的数据值。在这种情况下,它们都用逗号隔开。stringsAsFactors告诉它把所有的字符串值都当成字符串,而不是因子。这允许我们稍后替换数据中的值。

现在,你应该看到这个:

Figure: Screenshot of output you will obtain

进入。如果您在 Windows 上,应该会看到类似这样的内容:

无论操作系统如何,您都应该会看到一个窗口,允许您选择文件。接下来,您应该会看到:

如果你向右看,你会看到一个名为df的新字段。如果你点击它,你可以看到它的内容:

现在,我们已经创建了一个数据框架,我们可以开始一些分析。

我们可以获得一些关于行数和列数的信息,以及数据框的长度和列名。请看下面几行代码及其各自的输出:

> is.data.frame(df)
[1] TRUE
> ncol(df)
[1] 8
> length(df)
[1] 8
> nrow(df)
[1] 27080
> names(df)
[1] "InvoiceNo" "StockCode" "Description" "Quantity" "InvoiceDate"
    "UnitPrice" "CustomerID" "Country"
> colnames(df)
[1] "InvoiceNo" "StockCode" "Description" "Quantity" "InvoiceDate"
    "UnitPrice" "CustomerID" "Country"

现在,我们可以继续创建数据子集。看看这段代码:

d1 = df[1:3]

这就是它的结果:

所以基本上,我们选择了第 1、2、3 列作为d1的数据集。除了我们想要的列之外,我们还可以选择我们想要的行。让我们重新定义d1:

d1 = df[1:10, c(1:3)]

我们还可以访问数据框的单个列。看看这个:

v1 = df[[3]]

这会将整列数据分配给v1。现在,让我们进入v1的前五个要素:

v1[1:5]

我们也可以这样做:

v2 = df$Description
v2[1:5]

假设我们知道一个特定的数据值,我们甚至可以访问每个单独的行。这里,我们使用股票代码:

d1[d1$StockCode == "85123A", ]

我们可以访问我们想要的特定行:

d1 = df[1:10, c(1:8)]
d1[2, c(1:8)]

类似于 Python 中的.head()函数,r 中有一个head()函数,看看这段代码:

head(df)

我们可以添加另一个参数来选择要显示的行数。假设我们要显示前10行。下面是代码:

head(df, 10)

我们可以有一个负数作为第二个参数。请看以下内容:

head(d1, -2)

同样,我们可以使用tail()显示最后的 n 行。请看以下内容:

tail(d1, 4)

我们也可以有一个负数作为第二个参数,就像head()一样。看看这一行代码:

tail(d1, -2)

这会显示 nrow(d1) + n 行,其中 n 是传递到tail()函数的参数:

我们可以对一个栏目做一些基本的统计分析。但是,我们必须先转换数据。我们可以做min()max()mean()等等。看看这个:

min(as.numeric(df$UnitPrice))
[1] 0
min(df$UnitPrice)
[1] 0

as.numeric()表示任何字符串形式的数据值都将被转换为数字。在这种情况下,它们都不是字符串值,否则您会在0中看到min(df$UnitPrice)结果:

max(df$UnitPrice)
[1] 16888.02
mean(df$UnitPrice)
[1] 5.857586
median(df$UnitPrice)
[1] 2.51
quantile(df$UnitPrice)

我们可以在这里添加另一个参数来自定义我们想要的百分比值:

quantile(df$UnitPrice, c(0, .1, .5, .9)

sd(df$UnitPrice)

这告诉我们df$UnitPrice的标准差。我们还可以找到方差:

var(df$UnitPrice)

range(df$UnitPrice)

我们还可以得到一个五位数的总结,它告诉我们最小值、第一个分位数、中间值(也是 50%标记)、第三个分位数(75%标记)和最大值:

fivenum(df$UnitPrice)

我们还可以绘制一列选择。看看这个:

plot(df$UnitPrice)

我们可以有不同类型的情节。我们可以引入另一个参数来指定我们想要的绘图类型。请看下面几行代码及其结果图:

plot(df$UnitPrice, type="p")

如你所见,它和我们之前看到的图是一样的。但是,图表有点拥挤,所以我们使用一个较小的范围:

d1 = df[0:30, c(1:8)]
plot(d1$UnitPrice)

让我们更简单地重新定义d1使其只有UnitPrice列:

d1 = d1$UnitPrice
plot(d1, type="p")

该图应该与前一个图相同。

现在,让我们继续:

plot(d1, type="l")

这是d1的线图:

plot(d1, type="b")

这是d1的组合线图和点图。然而,它们并不相互重叠:

plot(d1, type="c")

该图仅是我们之前看到的type="b"组合图中的线条图:

plot(d1, type="o")

这是d1的一个过奖图。这意味着线图和点图相互重叠:

plot(d1, type="h")

这是d1的直方图:

plot(d1, type="s")

这是一个阶梯图:

plot(d1, type="S")

两个图的区别在于第一步图,其中type="s"是图先水平后垂直的地方。第二步图有type="S",先垂直移动后水平移动。通过看图表可以看出这种差异。

我们还可以使用其他参数,例如:

#Note: these are parameters, not individual lines of code.

#The title of the graph
main="Title" 

#Subtitle for the graph
sub="title"

#Label for the x-axis
xlab="X Axis"

#Label for the y-axis
ylab="Y Axis"

#The aspect ratio between y and x.
asp=1

现在举个例子:

plot(d1, type="h", main="Graph of Unit Prices vs Index", sub ="First 30 Rows", xlab = "Row Index", ylab="Prices", asp=1.4)

要将两个不同的数据帧添加在一起,我们使用rbind()

请看下面的代码:

d2 = df[0:10, c(1:8)]
d3 = df[21:30, c(1:8)]
d4 = rbind(d2, d3)

这是d2:

这是d3:

现在,这是d4:

需要注意的一点是,所有传入rbind()的数据帧必须有相同的列。顺序不重要。

我们也可以合并两个数据帧。

看看这段代码:

d2 = df[0:11, c("InvoiceNo", "StockCode", "Description")]
d3 = df[11:20, c("StockCode", "Description", "Quantity")]
d4 = merge(d2, d3)

这是d2:

这是d3:

这是d4:

所以默认情况下,merge()使用内部连接。

现在,让我们看看外部连接:

d4 = merge(d2, d3, all=T)

这是左外连接:

d4 = merge(d2, d3, all.x=T)

这是右外连接:

d4 = merge(d2, d3, all.y=T)

最后,交叉连接:

d4 = merge(d2, d3, by=NULL)

就像在熊猫中一样,我们可以用by=来指定两个数据项之间的一个.x.y,而不是_x_y。请看以下内容:

d4 = merge(d2, d3, by="StockCode", all=T)

这是StockCode列上的外部连接。

这就是结果:

我们可以随时记录下所有的命令,以防万一。执行以下代码保存命令日志:

savehistory(file="logname.Rhistory")

要加载历史记录:

loadhistory(file="logname.Rhistory")

如果您想查看您的历史记录,只需执行以下操作:

history()

我们可以检查数据,看看是否有空白数据。看看代码:

colSums(is.na(df))

现在,让我们再重复一遍。回想一下,当我们合并两个数据帧时,有些数据值是 NaN:

d2 = df[0:11, c("InvoiceNo", "StockCode", "Description")]
d3 = df[11:20, c("StockCode", "Description", "Quantity")]

现在,让我们对它们进行外部合并:

d4 = merge(d2, d3, all=T)

现在,让我们试试这一行代码:

colSums(is.na(d4))

我们也可以替换数据中的值。

现在,假设您想将价格大于 3 的每件商品的描述更改为"Miscellaneous"。看看这个示例代码:

d1 = df[0:30, c(1:8)]

现在,看看这个:

d1[d1$UnitPrice > 3, "Description"] <- "Miscellaneous"

现在我们看到单价大于三的东西都有"Miscellaneous"的描述。

我们可以使用除>以外的其他运算符,也可以替换其他列中的值。

这里还有一个例子。

假设发票号为536365的每一项实际上都来自United States

现在,由于它们都共享相同的发票号和发票日期,我们可以使用其中任何一个来选择所需的行:

d1[d1$InvoiceNo == 536365, "Country"] = "United States"

注意这次我们用=代替了<-。在这种情况下,它们都在分配一些东西,所以任何一个都可以使用。

摘要

在本章中,我们讨论了如何使用 R 来执行数据分析。我们还描述了集成 R 和 Hadoop 的不同选项。

在下一章中,我们将了解 Apache Spark,以及如何基于批处理模型将其用于大数据分析。

六、Apache Spark 批处理分析

在本章中,您将了解 Apache Spark 以及如何将其用于基于批处理模型的大数据分析。Spark SQL 是 Spark Core 之上的一个组件,可用于查询结构化数据。它正在成为事实上的工具,取代 Hive 成为 Hadoop 上批处理分析的选择。

此外,您将学习如何使用 Spark 来分析结构化数据(非结构化数据,如包含任意文本的文档,或必须转换为结构化形式的其他格式)。我们将在这里看到数据框架/数据集是如何成为基石的,以及 SparkSQL 的 API 如何使查询结构化数据变得简单而健壮。

我们还将介绍数据集,并了解数据集、数据框架和关系数据库之间的区别。简而言之,本章将涵盖以下主题:

  • 迷你图 SQL 和数据框
  • 数据框架和 SQL 应用编程接口
  • 数据框模式
  • 数据集和编码器
  • 加载和保存数据
  • 聚集
  • 连接

迷你图 SQL 和数据框

在 Apache Spark 之前,Apache Hive 是任何人想要对大量数据运行类似于 SQL 的查询时的首选技术。Apache Hive 本质上是将一个 SQL 查询翻译成 MapReduce,就像逻辑自动使对大数据执行多种分析变得非常容易,而无需实际学习用 Java 和 Scala 编写复杂的代码。

随着 Apache Spark 的出现,我们如何在大数据规模上执行分析发生了范式转变。Spark SQL 在 Apache Spark 的分布式计算能力之上提供了一个类似 SQL 的层,使用起来相当简单。事实上,Spark SQL 可以用作在线分析处理数据库。Spark SQL 的工作原理是将类似 SQL 的语句解析成抽象语法树 ( AST ),随后将该计划转换为逻辑计划,然后将逻辑计划优化为可执行的物理计划,如下图所示:

最终的执行使用底层的数据框架应用编程接口,通过简单地使用类似于 SQL 的接口,而不是学习所有的内部,任何人都可以非常容易地使用数据框架应用编程接口。由于本书深入探讨了各种应用编程接口的技术细节,我们将主要介绍数据框架应用编程接口,在一些地方展示了 Spark SQL 应用编程接口,以对比使用这些应用编程接口的不同方式。因此,数据框架应用编程接口是 Spark SQL 下面的底层。在本章中,我们将向您展示如何使用各种技术创建数据框,包括 SQL 查询和对数据框执行操作。

数据框架是对弹性分布式数据集 ( RDD )的抽象,处理使用催化剂优化器优化的更高级功能,并且通过钨计划也是高性能的。

自成立以来,钨项目一直是 Spark 执行引擎的最大变化。它的主要焦点在于提高 Spark 应用的 CPU 和内存效率。该项目包括三项举措:

  • 内存管理和二进制处理
  • 缓存感知计算
  • 代码生成

For more information, you can check out https://databricks.com/blog/2015/04/28/project-tungsten-bringing-spark-closer-to-bare-metal.html.

您可以将数据集视为 RDD 上的一个高效表,它具有高度优化的数据二进制表示。二进制表示是使用编码器实现的,编码器将各种对象序列化为二进制结构,性能比 RDD 表示好得多。因为数据框在内部使用 RDD,所以数据框/数据集也像 RDD 一样分布,因此也是一个分布式数据集。显然,这也意味着数据集是不可变的。

以下是数据的二进制表示的说明:

数据集是在 Spark 1.6 中添加的,并提供了在数据框之上进行强类型化的优势。事实上,由于 Spark 2.0,数据框只是数据集的别名。

http://spark.apache.org/sql/ defines the DataFrame type as a Dataset[Row], which means that most of the APIs will work well with both dataset and DataFrame.type DataFrame = Dataset[Row].

数据框在概念上类似于关系数据库中的表。因此,数据帧包含多行数据,每行由几列组成。我们需要记住的第一件事是,就像关系数据库一样,数据帧也是不可变的。数据帧不可变的这一特性意味着每次转换或操作都会创建一个新的数据帧。

让我们从更多地研究数据帧以及它们与关系数据库有什么不同开始。如前所述,RDDs 代表了 Apache Spark 中数据操作的低级 API。数据框架是在关系数据库之上创建的,以抽象关系数据库的低级内部工作方式,并公开更易于使用的高级应用编程接口,并提供大量现成的功能。DataFrame 是按照 Python pandas 包、R 语言、Julia 语言等类似概念创建的。

正如我们之前提到的,数据框架将 SQL 代码和特定于域的语言表达式转换成优化的执行计划,在 Spark Core APIs 之上运行,以便 SQL 语句执行各种各样的操作。数据框支持许多不同类型的输入数据源和许多类型的操作。这包括所有类型的 SQL 操作,如连接、分组依据、聚合和窗口函数。

Spark SQL 也非常类似于 Hive 查询语言,由于 Spark 为 Apache Hive 提供了一个自然的适配器,所以一直在 Apache Hive 工作的用户可以轻松地将自己的知识转移并应用到 Spark SQL 中,从而最大限度地减少转换时间。如前所述,数据帧本质上依赖于表的概念。

该表的操作方式与 Apache Hive 的工作方式非常相似。事实上,Apache Spark 中对表的许多操作类似于 Apache Hive 处理表和对表进行操作的方式。一旦您有了一个作为数据框的表,数据框就可以注册为一个表,并且您可以使用 Spark SQL 语句代替数据框 API 来操作数据。

数据框架取决于催化剂优化器和钨性能的提高,所以让我们简单地研究一下催化剂优化器是如何工作的。catalyst 优化器根据输入的 SQL 创建一个解析的逻辑计划,然后通过查看 SQL 语句中使用的各种属性和列来分析逻辑计划。一旦分析的逻辑计划被创建,catalyst 优化器通过组合几个操作并重新安排逻辑以获得更好的性能来进一步尝试优化计划。

In order to understand the catalyst optimizer, think about it as a common sense logic optimizer which can reorder operations such as filters and transformations, sometimes grouping several operations into one so as to minimize the amount of data that is shuffled across the worker nodes. For example, the catalyst optimizer may decide to broadcast the smaller datasets when performing joint operations between different datasets. Use explain to look at the execution plan of any DataFrame. The catalyst optimizer also computes statistics of the DataFrames columns and partitions improving the speed of execution.

例如,如果数据分区上有转换和过滤器,那么我们过滤数据和应用转换的顺序对操作的整体性能非常重要。作为所有优化的结果,生成优化的逻辑计划,然后将其转换为物理计划。

显然,几个物理计划可以执行相同的 SQL 语句并生成相同的结果。成本优化逻辑基于成本优化和估计来确定和挑选好的物理计划。与之前的版本(如 Spark 1.6 或更早版本)相比,Spark 2.x 提供了显著的性能提升,钨性能的提升是其背后的另一个关键因素。

钨实现了对内存管理和其他性能改进的全面检修。最重要的内存管理改进使用对象的二进制编码,并在堆外和堆内内存中引用它们。因此,通过使用二进制编码机制对所有对象进行编码,钨允许使用办公室堆内存。二进制编码对象占用的内存少得多。

项目钨也提高洗牌性能。数据通常通过DataFrameReader加载到数据框中,数据通过DataFrameWriter从数据框中保存。

数据框架应用编程接口和 SQL 应用编程接口

数据框可以通过几种方式创建;其中一些如下:

  • 执行 SQL 查询,加载外部数据,如 Parquet、JSON、CSV、Text、Hive、JDBC 等
  • 将关系数据库转换为数据帧
  • 加载一个 CSV 文件

我们将在这里看一下statesPopulation.csv,然后我们将它作为数据帧加载。

CSV 包含 2010 年至 2016 年美国各州人口的以下格式:

| 状态 | | 人口 |
| 亚拉巴马州 | Two thousand and ten | 47,85,492 |
| 阿拉斯加 | Two thousand and ten | Seven hundred and fourteen thousand and thirty-one |
| 亚利桑那州 | Two thousand and ten | 64,08,312 |
| 阿肯色州 | Two thousand and ten | Two million nine hundred and twenty-one thousand nine hundred and ninety-five |
| 加利福尼亚 | Two thousand and ten | Thirty-seven million three hundred and thirty-two thousand six hundred and eighty-five |

由于这个 CSV 有一个头,我们可以使用它来快速加载到带有隐式模式检测的数据帧中:

scala> val statesDF = spark.read.option("header",
"true").option("inferschema", "true").option("sep",
",").csv("statesPopulation.csv")
statesDF: org.apache.spark.sql.DataFrame = [State: string, Year: int ... 1
more field]

一旦我们加载了数据帧,就可以检查它的模式:

scala> statesDF.printSchema
root
|-- State: string (nullable = true)
|-- Year: integer (nullable = true)
|-- Population: integer (nullable = true)

option("header", "true").option("inferschema", "true").option("sep", ",") tells Spark that the CSV has a header; a comma separator is used to separate the fields/columns and also that schema can be inferred implicitly.

DataFrame 的工作原理是解析逻辑计划、分析逻辑计划、优化计划,然后最终执行物理执行计划。

使用数据框上的解释显示执行计划:

scala> statesDF.explain(true)
== Parsed Logical Plan ==
Relation[State#0,Year#1,Population#2] csv
== Analyzed Logical Plan ==
State: string, Year: int, Population: int
Relation[State#0,Year#1,Population#2] csv
== Optimized Logical Plan ==
Relation[State#0,Year#1,Population#2] csv
== Physical Plan ==
*FileScan csv [State#0,Year#1,Population#2] Batched: false, Format: CSV,
Location: InMemoryFileIndex[file:/Users/salla/states.csv],
PartitionFilters: [], PushedFilters: [], ReadSchema:
struct<State:string,Year:int,Population:int>

数据框也可以注册为表名(如下所示),这样就可以像关系数据库一样键入 SQL 语句:

scala> statesDF.createOrReplaceTempView("states")

一旦我们将数据框作为结构化数据框或表,我们就可以运行命令对数据进行操作:

scala> statesDF.show(5)
scala> spark.sql("select * from states limit 5").show
+----------+----+----------+
| State|Year|Population|
+----------+----+----------+
| Alabama|2010| 4785492|
| Alaska|2010| 714031|
| Arizona|2010| 6408312|
| Arkansas|2010| 2921995|
|California|2010| 37332685|
+----------+----+----------+

如果您在前面的代码中看到,我们已经编写了一个类似于 SQL 的语句,并使用spark.sql API 执行了它。

Note that the Spark SQL is simply converted to the DataFrame API for execution and the SQL is only a DSL for ease of use.

使用数据框上的sort操作,可以按任意列对数据框中的行进行排序。我们看到使用Population列进行降序排序的效果如下。这些行由Population按降序排列:

scala> statesDF.sort(col("Population").desc).show(5)
scala> spark.sql("select * from states order by Population desc limit
5").show
+----------+----+----------+
| State|Year|Population|
 +----------+----+----------+
|California|2016| 39250017|
|California|2015| 38993940|
|California|2014| 38680810|
|California|2013| 38335203|
|California|2012| 38011074|
+----------+----+----------+

使用groupBy我们可以按任意列对数据帧进行分组。以下是通过State对行进行分组,然后对每个State累加Population计数的代码:

scala> statesDF.groupBy("State").sum("Population").show(5)
scala> spark.sql("select State, sum(Population) 
from states group by State
limit 5").show
+---------+---------------+
| State|sum(Population)|
+---------+---------------+
| Utah| 20333580|
| Hawaii| 9810173|
|Minnesota| 37914011|
| Ohio| 81020539|
| Arkansas| 20703849|
+---------+---------------+

使用agg操作,您可以对数据框的列执行许多不同的操作,例如查找列的minmaxavg。您还可以同时执行该操作并重命名该列,以适合您的用例:

scala>
statesDF.groupBy("State").agg(sum("Population").alias("Total")).show(5)
scala> spark.sql("select State, sum(Population) as Total from states group
by State limit 5").show
+---------+--------+
| State| Total|
+---------+--------+
| Utah|20333580|
| Hawaii| 9810173|
|Minnesota|37914011|
| Ohio|81020539|
| Arkansas|20703849|
+---------+--------+

自然,逻辑越复杂,执行计划也就越复杂。让我们看看groupByagg API 调用的前一个操作的计划,以便更好地理解幕后到底发生了什么。以下是显示group by条款执行计划和每个State人口总和的代码:

scala>
statesDF.groupBy("State").agg(sum("Population").alias("Total")).explain(true)
== Parsed Logical Plan ==
'Aggregate [State#0], [State#0, sum('Population) AS Total#31886]
+- Relation[State#0,Year#1,Population#2] csv
== Analyzed Logical Plan ==
State: string, Total: bigint
Aggregate [State#0], [State#0, sum(cast(Population#2 as bigint)) AS
Total#31886L]
+- Relation[State#0,Year#1,Population#2] csv
== Optimized Logical Plan ==
Aggregate [State#0], [State#0, sum(cast(Population#2 as bigint)) AS
Total#31886L]
+- Project [State#0, Population#2]
+- Relation[State#0,Year#1,Population#2] csv
== Physical Plan ==
*HashAggregate(keys=[State#0], functions=[sum(cast(Population#2 as
bigint))], output=[State#0, Total#31886L])
+- Exchange hashpartitioning(State#0, 200)
+- *HashAggregate(keys=[State#0], functions=[partial_sum(cast(Population#2
as bigint))], output=[State#0, sum#31892L])
+- *FileScan csv [State#0,Population#2] Batched: false, Format: CSV,
Location: InMemoryFileIndex[file:/Users/salla/states.csv],
PartitionFilters: [], PushedFilters: [], ReadSchema:
struct<State:string,Population:int>

数据框操作可以很好地链接在一起,这样执行就可以利用成本优化(钨性能改进和催化剂优化器一起工作)。我们还可以在一条语句中将操作链接在一起,如下所示,其中我们不仅按State列对数据进行分组,然后对Population值求和,还按求和列对数据帧进行排序:

scala>
statesDF.groupBy("State").agg(sum("Population").alias("Total")).sort(col("Total").desc).show(5)
scala> spark.sql("select State, sum(Population) as Total from states group
by State order by Total desc limit 5").show
+----------+---------+
| State| Total|
+----------+---------+
|California|268280590
| Texas|185672865|
| Florida|137618322|
| New York|137409471|
| Illinois| 89960023|
+----------+---------+

前面的链式操作由多个转换和动作组成,
可以使用下图可视化:

也可以同时创建多个聚合,如下所示:

scala> statesDF.groupBy("State").agg(
min("Population").alias("minTotal"),
max("Population").alias("maxTotal"),
avg("Population").alias("avgTotal"))
.sort(col("minTotal").desc).show(5)
scala> spark.sql("select State, min(Population) as minTotal,
max(Population) as maxTotal, avg(Population) as avgTotal from states group
by State order by minTotal desc limit 5").show
+----------+--------+--------+--------------------+
| State|minTotal|maxTotal| avgTotal|
+----------+--------+--------+--------------------+
|California|37332685|39250017|3.8325798571428575E7|
| Texas|25244310|27862596| 2.6524695E7|
| New York|19402640|19747183| 1.962992442857143E7|
| Florida|18849098|20612439|1.9659760285714287E7|
| Illinois|12801539|12879505|1.2851431857142856E7|
+----------+--------+--------+--------------------+

中心

为了创建更适合执行多个汇总和聚合的不同视图,转换表的最佳方法之一是旋转。我们可以通过取一个列的值并使每个值成为一个实际的列来实现这一点。

让我们借助一个例子更好地理解这一点。我们将按Year旋转数据帧的行,并检查结果。我们现在获得的结果描述了来自Year列的值,每个值都形成了一个新列。这样做的最终结果是,我们可以使用Year创建的年度列进行汇总和汇总,而不仅仅是查看年度列:

scala> statesDF.groupBy("State").pivot("Year").sum("Population").show(5)
+---------+--------+--------+--------+--------+--------+--------+--------+
| State| 2010| 2011| 2012| 2013| 2014| 2015| 2016|
+---------+--------+--------+--------+--------+--------+--------+--------+
| Utah| 2775326| 2816124| 2855782| 2902663| 2941836| 2990632| 3051217|
| Hawaii| 1363945| 1377864| 1391820| 1406481| 1416349| 1425157| 1428557|
|Minnesota| 5311147| 5348562| 5380285| 5418521| 5453109| 5482435| 5519952|
| Ohio|11540983|11544824|11550839|11570022|11594408|11605090|11614373|
| Arkansas| 2921995| 2939493| 2950685| 2958663| 2966912| 2977853| 2988248|
+---------+--------+--------+--------+--------+--------+--------+--------+

过滤

数据框也支持筛选,可以通过筛选数据框行来生成新的数据框。Filter实现了一个非常重要的数据转换,将数据框架缩小到我们的用例。让我们看一下数据帧过滤的执行计划,只考虑California的状态:

scala> statesDF.filter("State == 'California'").explain(true)
== Parsed Logical Plan ==
'Filter ('State = California)
+- Relation[State#0,Year#1,Population#2] csv
== Analyzed Logical Plan ==
State: string, Year: int, Population: int
Filter (State#0 = California)
+- Relation[State#0,Year#1,Population#2] csv
== Optimized Logical Plan ==
Filter (isnotnull(State#0) && (State#0 = California))
+- Relation[State#0,Year#1,Population#2] csv
== Physical Plan ==
*Project [State#0, Year#1, Population#2]
+- *Filter (isnotnull(State#0) && (State#0 = California))
+- *FileScan csv [State#0,Year#1,Population#2] Batched: false, Format:
CSV, Location: InMemoryFileIndex[file:/Users/salla/states.csv],
PartitionFilters: [], PushedFilters: [IsNotNull(State),
EqualTo(State,California)], ReadSchema:
struct<State:string,Year:int,Population:int>

现在我们已经看到了执行计划,现在让我们执行filter命令如下:

scala> statesDF.filter("State == 'California'").show
+----------+----+----------+
| State|Year|Population|
+----------+----+----------+
|California|2010| 37332685|
|California|2011| 37676861|
|California|2012| 38011074|
|California|2013| 38335203|
|California|2014| 38680810|
|California|2015| 38993940|
|California|2016| 39250017|
+----------+----+----------+

用户定义的函数

用户定义函数 ( UDFs )定义了新的基于列的函数,扩展了 Spark SQL 的功能。当 Spark 中的内置函数无法处理我们的需求时,创建 UDF 会有所帮助。

udf() internally calls a case class UserDefinedFunction which in turn calls ScalaUDF internally.

让我们来看一个简单地将State列值转换为大写的 UDF 的例子。首先,我们在 Scala 中创建我们需要的函数,如以下代码片段所示:

import org.apache.spark.sql.functions._
scala> val toUpper: String => String = _.toUpperCase
toUpper: String => String = <function1>

然后我们必须将创建的函数封装在udf中,以创建 UDF:

scala> val toUpperUDF = udf(toUpper)
toUpperUDF: org.apache.spark.sql.expressions.UserDefinedFunction =
UserDefinedFunction(<function1>,StringType,Some(List(StringType)))

现在我们已经创建了udf,我们可以使用它将State列转换为大写:

scala> statesDF.withColumn("StateUpperCase",
toUpperUDF(col("State"))).show(5)
+----------+----+----------+--------------+
| State|Year|Population|StateUpperCase|
+----------+----+----------+--------------+
| Alabama|2010| 4785492| ALABAMA|
| Alaska|2010| 714031| ALASKA|
| Arizona|2010| 6408312| ARIZONA|
| Arkansas|2010| 2921995| ARKANSAS|
|California|2010| 37332685| CALIFORNIA|
+----------+----+----------+--------------+

模式-数据结构

模式是对数据结构的描述,可以是隐式的,也可以是显式的。将现有关系数据库转换为数据集有两种主要方法,因为数据框架在内部基于 RDDs 它们如下:

  • 用反射来推断 RDD 的图式
  • 通过一个编程接口,在这个接口的帮助下,您可以获取一个现有的 RDD 并呈现一个模式,从而将 RDD 转换为一个具有模式的数据集

隐式模式

让我们看一个例子,将一个逗号分隔值 ( CSV )文件加载到一个数据帧中。每当文本文件包含标题时,读取应用编程接口就可以通过读取标题行来推断模式。我们还可以选择指定用于拆分文本文件行的分隔符。

我们从标题行读取csv推断模式,并使用逗号(,)作为分隔符。我们还展示了使用schema命令和printSchema命令来验证输入文件的模式:

scala> val statesDF = spark.read.option("header", "true")
 .option("inferschema", "true")
 .option("sep", ",")
 .csv("statesPopulation.csv")
statesDF: org.apache.spark.sql.DataFrame = [State: string, Year: int ... 1
more field]
scala> statesDF.schema
res92: org.apache.spark.sql.types.StructType = StructType(
StructField(State,StringType,true),
StructField(Year,IntegerType,true),
StructField(Population,IntegerType,true))
scala> statesDF.printSchema
root
|-- State: string (nullable = true)
|-- Year: integer (nullable = true)
|-- Population: integer (nullable = true)

显式模式

使用StructType描述模式,T0 是StructField对象的集合。

StructType and StructField belong to the 
org.apache.spark.sql.types package. DataTypes such as IntegerType and StringType also belong to the org.apache.spark.sql.types package.

使用这些导入,我们可以定义一个自定义的显式模式。

首先,导入必要的类:

scala> import org.apache.spark.sql.types.{StructType, IntegerType,
StringType}
import org.apache.spark.sql.types.{StructType, IntegerType, StringType}

定义带有两个列/字段和一个后跟字符串的整数的模式:

scala> val schema = new StructType().add("i", IntegerType).add("s",
StringType)
schema: org.apache.spark.sql.types.StructType =
StructType(StructField(i,IntegerType,true), StructField(s,StringType,true))

很容易打印刚刚创建的schema:

scala> schema.printTreeString
root
|-- i: integer (nullable = true)
|-- s: string (nullable = true)

还有一个打印 JSON 的选项,如下,使用prettyJson功能:

scala> schema.prettyJson
res85: String =
{
"type" : "struct",
"fields" : [ {
"name" : "i",
"type" : "integer",
"nullable" : true,
"metadata" : { }
}, {
"name" : "s",
"type" : "string",
"nullable" : true,
"metadata" : { }
} ]
}

Spark SQL 的所有数据类型都位于包org.apache.spark.sql.types中。

您可以通过以下方式访问它们:

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

编码器

Spark 2.x 支持为复杂数据类型定义模式的不同方式。首先,让我们看一个简单的例子。Encoders必须使用导入语句导入,以便您使用Encoders:

import org.apache.spark.sql.Encoders

让我们看一个简单的例子,将元组定义为数据集 API 中使用的数据类型:

scala> Encoders.product[(Integer, String)].schema.printTreeString
root
|-- _1: integer (nullable = true)
|-- _2: string (nullable = true)

前面的代码看起来总是很复杂,所以我们也可以为我们的需求定义一个case class,然后使用它。

我们可以用两个字段定义一个案例class Record,一个Integer和一个String:

scala> case class Record(i: Integer, s: String)
defined class Record

使用Encoders我们可以很容易地在case class之上创建一个模式,从而允许我们轻松地使用各种应用编程接口:

scala> Encoders.product[Record].schema.printTreeString
root
|-- i: integer (nullable = true)
|-- s: string (nullable = true)

Spark SQL 的所有数据类型都位于包org.apache.spark.sql.types中。

您可以通过以下方式访问它们:

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

您应该在代码中使用DataTypes对象来创建复杂的Spark SQL类型,如数组或映射,如下所示:

scala> import org.apache.spark.sql.types.DataTypes
import org.apache.spark.sql.types.DataTypes
scala> val arrayType = DataTypes.createArrayType(IntegerType)
arrayType: org.apache.spark.sql.types.ArrayType =
ArrayType(IntegerType,true)

以下是 SparkSQL APIs 支持的数据类型:

| 数据类型 | Scala 中的值类型 | API 访问
或创建数据类型
|
| ByteType | Byte | ByteType |
| ShortType | Short | ShortType |
| IntegerType | Int | IntegerType |
| LongType | Long | LongType |
| FloatType | Float | FloatType |
| DoubleType | Double | DoubleType |
| DecimalType  | java.math.BigDecimal | DecimalType |
| StringType | String  | StringType |
| BinaryType | Array[Byte]  | BinaryType |
| BooleanType | Boolean | BooleanType |
| TimestampType | java.sql.Timestamp | TimestampType |
| DateType | java.sql.Date  | DateType |
| ArrayType | scala.collection.Seq | ArrayType(elementType, [containsNull]) |
| MapType | scala.collection.Map | MapType(keyType, valueType,
[valueContainsNull])Note:默认valueContainsNulltrue。 |
| StructType | org.apache.spark.sql.Row | StructType(fields).Note:菲尔兹是StructFieldsSeq。此外,不允许两个字段同名。 |

正在加载数据集

Spark SQL 可以通过DataFrameReader界面从文件、Hive 表、JDBC 数据库等外部存储系统读取数据。

API 调用的格式为spark.read.inputtype:

  • 镶木地板
  • 战斗支援车
  • Hive 表
  • JDBC
  • 妖魔
  • 文本
  • JSON

让我们看几个将 CSV 文件读入数据帧的简单例子:

scala> val statesPopulationDF = spark.read.option("header",
"true").option("inferschema", "true").option("sep",
",").csv("statesPopulation.csv")
statesPopulationDF: org.apache.spark.sql.DataFrame = [State: string, Year:
int ... 1 more field]
scala> val statesTaxRatesDF = spark.read.option("header",
"true").option("inferschema", "true").option("sep",
",").csv("statesTaxRates.csv")
statesTaxRatesDF: org.apache.spark.sql.DataFrame = [State: string, TaxRate:
double]

保存数据集

Spark SQL 可以通过DataFrameWriter界面将数据保存到文件、Hive 表和 JDBC 数据库等外部存储系统中。

API 调用的格式为dataframe.write.outputtype:

  • 镶木地板
  • 妖魔
  • 文本
  • Hive 表
  • JSON
  • 战斗支援车
  • JDBC

让我们看几个将数据帧写入或保存到 CSV 文件的例子:

scala> statesPopulationDF.write.option("header",
"true").csv("statesPopulation_dup.csv")
scala> statesTaxRatesDF.write.option("header",
"true").csv("statesTaxRates_dup.csv")

聚集

聚合是根据条件将数据收集在一起并对数据进行分析的方法。聚合对于理解各种大小的数据非常重要,因为仅仅拥有原始数据记录对于大多数用例来说并不那么有用。

Imagine a table containing one temperature measurement per day for every city in the world for five years.

例如,如果您看到下表,然后看到相同数据的聚合视图,那么很明显,仅仅原始记录并不能帮助您理解数据。下表显示了原始数据:

| 城市 | 日期 | 温度 |
| 波士顿 | 12/23/2016 | Thirty-two |
| 纽约 | 12/24/2016 | Thirty-six |
| 波士顿 | 12/24/2016 | Thirty |
| 费城 | 12/25/2016 | Thirty-four |
| 波士顿 | 12/25/2016 | Twenty-eight |

下图是每个城市的平均温度:

| 城市 | 平均T2气温 |
| 波士顿 | 30 - (32 + 30 + 28)/3 |
| 纽约 | Thirty-six |
| 费城 | Thirty-four |

聚合函数

可以在org.apache.spark.sql.functions包中找到的函数的帮助下执行聚合。除此之外,还可以创建自定义聚合函数,也称为用户自定义聚合函数 ( UDAF )。

Each grouping operation returns a RelationalGroupedDataset on which aggregations can be specified.

我们将加载示例数据来说明本节中所有不同类型的聚合函数:

val statesPopulationDF = spark.read.option("header", "true").
 option("inferschema", "true").
 option("sep", ",").csv("statesPopulation.csv")

数数

Count 是最基本的聚合函数,它只计算指定列的行数。countDistinctcount的延伸;它还消除了重复。

count API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def count(columnName: String): TypedColumn[Any, Long]
 Aggregate function: returns the number of items in a group.
def count(e: Column): Column
 Aggregate function: returns the number of items in a group.
def countDistinct(columnName: String, columnNames: String*): Column
 Aggregate function: returns the number of distinct items in a group.
def countDistinct(expr: Column, exprs: Column*): Column
 Aggregate function: returns the number of distinct items in a group.

让我们看一下在数据框中调用countcountDistinct来打印行数的一些例子:

import org.apache.spark.sql.functions._
scala> statesPopulationDF.select(col("*")).agg(count("State")).show
scala> statesPopulationDF.select(count("State")).show
+------------+
|count(State)|
+------------+
| 350|
+------------+
scala> statesPopulationDF.select(col("*")).agg(countDistinct("State")).show
scala> statesPopulationDF.select(countDistinct("State")).show
+---------------------+
|count(DISTINCT State)|
+---------------------+
| 50|

第一

获取RelationalGroupedDataset中的第一条记录。

first API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def first(columnName: String): Column
 Aggregate function: returns the first value of a column in a group.
def first(e: Column): Column
 Aggregate function: returns the first value in a group.
def first(columnName: String, ignoreNulls: Boolean): Column
 Aggregate function: returns the first value of a column in a group.
def first(e: Column, ignoreNulls: Boolean): Column 
 Aggregate function: returns the first value in a group.

让我们看一下在数据帧上调用 first 来输出第一行的例子:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.select(first("State")).show
+-------------------+
|first(State, false)|
+-------------------+
| Alabama|
+-------------------+

最后的

获取RelationalGroupedDataset中的最后一条记录。

last API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def last(columnName: String): Column
 Aggregate function: returns the last value of the column in a group.
def last(e: Column): Column
 Aggregate function: returns the last value in a group.
def last(columnName: String, ignoreNulls: Boolean): Column
 Aggregate function: returns the last value of the column in a group.
def last(e: Column, ignoreNulls: Boolean): Column
 Aggregate function: returns the last value in a group.

让我们看一下调用 DataFrame 上的 last 来输出最后一行的例子:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.select(last("State")).show
 +------------------+
 |last(State, false)|
 +------------------+
 | Wyoming|
 +------------------+

近似 _ 计数 _ 独特

如果您需要不同记录的近似计数,近似不同计数是一种更快的方法,而不是执行通常需要大量洗牌和其他操作的精确计数。

approx_count_distinct API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def approx_count_distinct(columnName: String, rsd: Double): Column
 Aggregate function: returns the approximate number of distinct items in a
 group.
def approx_count_distinct(e: Column, rsd: Double): Column
 Aggregate function: returns the approximate number of distinct items in a group.
def approx_count_distinct(columnName: String): Column
 Aggregate function: returns the approximate number of distinct items in a group.
def approx_count_distinct(e: Column): Column
 Aggregate function: returns the approximate number of distinct items in a group.

让我们看一下在数据帧上调用approx_count_distinct来打印数据帧的大概计数的例子:

import org.apache.spark.sql.functions._
 scala>
 statesPopulationDF.select(col("*")).agg(approx_count_distinct("State")).show
 +----------------------------+
 |approx_count_distinct(State)|
 +----------------------------+
 | 48|
 +----------------------------+
 scala> statesPopulationDF.select(approx_count_distinct("State", 0.2)).show
 +----------------------------+
 |approx_count_distinct(State)|
 +----------------------------+
 | 49|
 +----------------------------+

min是数据框中某一列的最小列值。min的一个例子就是如果要查一个城市的最低气温。

min API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def min(columnName: String): Column
 Aggregate function: returns the minimum value of the column in a group.
def min(e: Column): Column
 Aggregate function: returns the minimum value of the expression in a group.

让我们看一下在数据帧上调用min来打印最小值Population的例子:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.select(min("Population")).show
 +---------------+
 |min(Population)|
 +---------------+
 | 564513|
+---------------+

最大

max是数据框中某一列的最大列值。这方面的一个例子是,如果你想检查一个城市的最高温度。

max API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def max(columnName: String): Column
 Aggregate function: returns the maximum value of the column in a group.
def max(e: Column): Column
 Aggregate function: returns the maximum value of the expression in a group.

让我们看一下在数据框上调用max打印最大值Population的例子:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.select(max("Population")).show
+---------------+
 |max(Population)|
 +---------------+
 | 39250017|
 +---------------+

平均值

这些值的平均值是通过将这些值相加并除以值的数量来计算的。

The average of 123 is (1 + 2 + 3) / 3 = 6/3 = z.

avg API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def avg(columnName: String): Column
 Aggregate function: returns the average of the values in a group.
def avg(e: Column): Column
 Aggregate function: returns the average of the values in a group.

让我们看一下在数据框上调用avg来打印平均人口的例子:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.select(avg("Population")).show
 +-----------------+
 | avg(Population)|
 +-----------------+
 |6253399.371428572|
 +----------------+

总和

计算列值的总和。可选地,sumDistinct只能用于累加不同的值。

sum API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def sum(columnName: String): Column
 Aggregate function: returns the sum of all values in the given column.
def sum(e: Column): Column
 Aggregate function: returns the sum of all values in the expression.
def sumDistinct(columnName: String): Column
 Aggregate function: returns the sum of distinct values in the expression
def sumDistinct(e: Column): Column
 Aggregate function: returns the sum of distinct values in the expression.

让我们看一下在数据帧上调用 sum 来打印 sum(total)Population的例子:

import org.apache.spark.sql.functions._
scala> statesPopulationDF.select(sum("Population")).show
 +---------------+
 |sum(Population)|
 +---------------+
 | 2188689780|
 +---------------+

峭度

kurtosis是一种量化分布形状差异的方法,在平均值和方差方面看起来非常相似,但实际上是不同的。

kurtosis API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def kurtosis(columnName: String): Column
 Aggregate function: returns the kurtosis of the values in a group.
def kurtosis(e: Column): Column
 Aggregate function: returns the kurtosis of the values in a group.

让我们看一个在Population列的数据框上调用kurtosis的例子:

import org.apache.spark.sql.functions._
scala> statesPopulationDF.select(kurtosis("Population")).show
 +--------------------+
 |kurtosis(Population)|
 +--------------------+
 | 7.727421920829375|
 +--------------------+

歪斜

skewness测量数据中的值围绕平均值或平均值的不对称性。

skewness API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def skewness(columnName: String): Column
 Aggregate function: returns the skewness of the values in a group.
def skewness(e: Column): Column
 Aggregate function: returns the skewness of the values in a group.

让我们看一下在Population列的数据框上调用skewness的例子:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.select(skewness("Population")).show
 +--------------------+
 |skewness(Population)|
 +--------------------+
 | 2.5675329049100024|
 +--------------------+

差异

方差是每个值与平均值的平方差的平均值。

var API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def var_pop(columnName: String): Column
 Aggregate function: returns the population variance of the values in a group.
def var_pop(e: Column): Column
 Aggregate function: returns the population variance of the values in a group.
def var_samp(columnName: String): Column
 Aggregate function: returns the unbiased variance of the values in a group.
def var_samp(e: Column): Column
 Aggregate function: returns the unbiased variance of the values in a group.

现在,让我们看看在测量Population方差的数据帧上调用var_pop的例子:

import org.apache.spark.sql.functions._
scala> statesPopulationDF.select(var_pop("Population")).show
 +--------------------+
 | var_pop(Population)|
 +--------------------+
 |4.948359064356177E13|
 +--------------------+

标准偏差

标准差是方差的平方根(见上一节)。

stddev API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def stddev(columnName: String): Column
 Aggregate function: alias for stddev_samp.
def stddev(e: Column): Column
 Aggregate function: alias for stddev_samp.
def stddev_pop(columnName: String): Column
 Aggregate function: returns the population standard deviation of the
 expression in a group.
def stddev_pop(e: Column): Column
 Aggregate function: returns the population standard deviation of the
 expression in a group.
def stddev_samp(columnName: String): Column
 Aggregate function: returns the sample standard deviation of the expression in a group.
def stddev_samp(e: Column): Column
Aggregate function: returns the sample standard deviation of the expression in a group.

我们来看一个在数据框上调用stddev的例子,打印Population的标准
偏差:

import org.apache.spark.sql.functions._
scala> statesPopulationDF.select(stddev("Population")).show
 +-----------------------+
 |stddev_samp(Population)|
 +-----------------------+
 | 7044528.191173398|
 +-----------------------+

协方差

协方差是两个随机变量联合可变性的度量。

covar API 有如下几种实现方式。具体使用的应用编程接口取决于具体的用例:

def covar_pop(columnName1: String, columnName2: String): Column
 Aggregate function: returns the population covariance for two columns.
def covar_pop(column1: Column, column2: Column): Column
 Aggregate function: returns the population covariance for two columns.
def covar_samp(columnName1: String, columnName2: String): Column
 Aggregate function: returns the sample covariance for two columns.
def covar_samp(column1: Column, column2: Column): Column
 Aggregate function: returns the sample covariance for two columns.

让我们看一个在数据帧上调用covar_pop来计算YearPopulation列之间协方差的例子:

import org.apache.spark.sql.functions._
scala> statesPopulationDF.select(covar_pop("Year", "Population")).show
 +---------------------------+
 |covar_pop(Year, Population)|
 +---------------------------+
 | 183977.56000006935|
 +---------------------------+

群组依据

数据分析中常见的任务是将数据分成不同的类别,然后对结果数据组进行计算。

让我们在数据框上运行groupBy功能,打印每个State的聚合计数:

scala> statesPopulationDF.groupBy("State").count.show(5)
 +---------+-----+
| State|count|
 +---------+-----+
 | Utah| 7|
 | Hawaii| 7|
 |Minnesota| 7|
 | Ohio| 7|
 | Arkansas| 7|
 +---------+-----+

您也可以groupBy然后应用之前看到的任何聚合函数,如minmaxavgstddev等:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.groupBy("State").agg(min("Population"),
 avg("Population")).show(5)
+---------+---------------+--------------------+
 | State|min(Population)| avg(Population)|
 +---------+---------------+--------------------+
 | Utah| 2775326| 2904797.1428571427|
 | Hawaii| 1363945| 1401453.2857142857|
 |Minnesota| 5311147| 5416287.285714285|
 | Ohio| 11540983|1.1574362714285715E7|
 | Arkansas| 2921995| 2957692.714285714|
 +---------+---------------+--------------------+

到达

Rollup 是用于执行分层或嵌套计算的多维聚合。例如,如果我们想要显示每个StateYear组以及每个State的记录数量(汇总所有年份以给出每个State的总计,而不考虑Year,我们可以如下使用rollup:

scala> statesPopulationDF.rollup("State", "Year").count.show(5)
 +------------+----+-----+
 | State|Year|count|
 +------------+----+-----+
 |South Dakota|2010| 1|
 | New York|2012| 1|
 | California|2014| 1|
 | Wyoming|2014| 1|
 | Hawaii|null| 7|
 +------------+----+-----+

立方

Cube是一个多维聚合,用于执行分层或嵌套计算,就像rollup一样,不同的是cube对所有维度执行相同的操作。例如,如果我们想要显示每个StateYear组以及每个State的记录数量(汇总一整年以给出每个State的总计,与Year无关),我们可以使用cube如下:

scala> statesPopulationDF.cube("State", "Year").count.show(5)
 +------------+----+-----+
 | State|Year|count|
 +------------+----+-----+
 |South Dakota|2010| 1|
 | New York|2012| 1|
 | null|2014| 50|
 | Wyoming|2014| 1|
 | Hawaii|null| 7|
 +------------+----+-----+

窗口功能

窗口函数允许您在一个数据窗口上执行聚合,而不是在整个数据或某些筛选数据上执行聚合。此类窗口函数的用例有:

  • 累计总和
  • 同一个键的上一个值的增量
  • 加权移动平均线

您可以通过执行简单的计算,指定一个窗口查看三行 T-1TT+1 。您还可以在最近/最近的 10 个值上指定一个窗口:

Window规范的 API 需要三个属性,partitionBy()orderBy()rowsBetween()partitionBy按照partitionBy()的规定将数据分块到分区/组中。orderBy()用于对每个数据分区内的数据进行排序。

rowsBetween()指定执行计算的窗框或滑动窗口的跨度。

要试用 Windows 功能,需要某些软件包。可以使用导入指令导入必要的包,如下所示:

import org.apache.spark.sql.expressions.Window
import org.apache.spark.sql.functions.col
import org.apache.spark.sql.functions.max

现在我们准备写一些代码来学习Window函数。让我们为按Population排序和按State分区的分区创建一个窗口规范。此外,指定我们要将当前行之前的所有行视为Window的一部分:

val windowSpec = Window
 .partitionBy("State")
 .orderBy(col("Population").desc)
 .rowsBetween(Window.unboundedPreceding, Window.currentRow)

计算超过Window规格的等级。只要在指定的Window范围内,结果将是添加到每行的等级(行号)。在这个例子中,我们选择按State进行划分,然后按降序对每个State的行进行排序。因此,每个State行都分配有自己的等级编号:

import org.apache.spark.sql.functions._
 scala> statesPopulationDF.select(col("State"), col("Year"),
 max("Population").over(windowSpec), rank().over(windowSpec)).sort("State",
 "Year").show(10)
 +-------+----+-------------------------------------------------------------
 -----------------------------------------------------------------+---------
 ---------------------------------------------------------------------------
 ---------------------------------+
| State|Year|max(Population) OVER (PARTITION BY State ORDER BY Population
 DESC NULLS LAST ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)|RANK()
 OVER (PARTITION BY State ORDER BY Population DESC NULLS LAST ROWS BETWEEN
 UNBOUNDED PRECEDING AND CURRENT ROW)|
 +-------+----+-------------------------------------------------------------
 -----------------------------------------------------------------+---------
 ---------------------------------------------------------------------------
 ---------------------------------+
|Alabama|2010| 4863300| 6|
 |Alabama|2011| 4863300| 7|
 |Alabama|2012| 4863300| 5|
 |Alabama|2013| 4863300| 4|
 |Alabama|2014| 4863300| 3|

奈尔斯

ntiles是一种流行的窗口聚合,通常用于将输入数据集划分为 n 个部分。

例如,如果我们想将statesPopulationDF划分为State(窗口说明如前所示),按人口排序,然后分成两部分,我们可以在windowspec上使用ntile:

import org.apache.spark.sql.functions._
scala> statesPopulationDF.select(col("State"), col("Year"),
 ntile(2).over(windowSpec), rank().over(windowSpec)).sort("State",
 "Year").show(10)
+-------+----+-------------------------------------------------------------
 ----------------------------------------------------------+----------------
 ---------------------------------------------------------------------------
 --------------------------+
| State|Year|ntile(2) OVER (PARTITION BY State ORDER BY Population DESC
 NULLS LAST ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)|RANK() OVER
 (PARTITION BY State ORDER BY Population DESC NULLS LAST ROWS BETWEEN
 UNBOUNDED PRECEDING AND CURRENT ROW)|
 +-------+----+-------------------------------------------------------------
 ----------------------------------------------------------+----------------
 ---------------------------------------------------------------------------
 --------------------------+
 |Alabama|2010| 2| 6|
 |Alabama|2011| 2| 7|
 |Alabama|2012| 2| 5|
 |Alabama|2013| 1| 4|
 |Alabama|2014| 1| 3|
 |Alabama|2015| 1| 2|
 |Alabama|2016| 1| 1|
 | Alaska|2010| 2| 7|
 | Alaska|2011| 2| 6|
 | Alaska|2012| 2| 5|
 +-------+----+-------------------------------------------------------------
 ----------------------------------------------------------+----------------
 --------------------------------------------------------------

如前所示,我们使用Window功能和ntile()一起将每个State的行分成两个相等的部分。

A popular use of this function is to compute decile used in data science models.

连接

在传统数据库中,连接用于将一个事务表与另一个查找表连接起来,以生成更完整的视图。例如,如果您有一个按客户标识排序的在线交易表和另一个包含客户城市和客户标识的表,则可以使用联接生成按城市排序的交易报告。

交易表:本表有三栏,客户编号采购项目,客户支付项目金额:

| 客户名称 | 采购项目 | 支付的价格 |
| one | 耳机 | Twenty-five |
| Two | 看 | Twenty |
| three | 键盘 | Twenty |
| one | 老鼠 | Ten |
| four | 电缆 | Ten |
| three | 耳机 | Thirty |

客户信息表:该表有两列客户信息和客户居住的城市:

| 客户编号 | 城市 |
| one | 波士顿 |
| Two | 纽约 |
| three | 费城 |
| four | 波士顿 |

将交易表与客户信息表连接起来将生成如下视图:

| 客户编号 | 采购项目 | 支付的价格 | 城市 |
| one | 双耳式耳机 | Twenty-five | 波士顿 |
| Two | 看 | One hundred | 纽约 |
| three | 键盘 | Twenty | 费城 |
| one | 老鼠 | Ten | 波士顿 |
| four | 电缆 | Ten | 波士顿 |
| three | 耳机 | Thirty | 费城 |

现在,我们可以使用这个连接的视图来生成按City排序的Total销售价格报告:

| 城市 | #项 | 销售总价 |
| 波士顿 | three | Forty-five |
| 费城 | Two | Fifty |
| 纽约 | one | One hundred |

连接是 Spark SQL 的一个重要功能,因为它们使您能够将两个数据集结合在一起,如前所述。当然,Spark 不仅仅意味着生成一些报告,还用于以 Peta 字节规模处理数据,以处理实时流用例、机器学习算法或简单分析。为了实现这些目标,Spark 提供了所需的 API 函数。

两个数据集之间的典型连接使用左右数据集的一个或多个键进行,然后将键集上的条件表达式计算为布尔表达式。如果布尔表达式的结果返回true,则连接成功,否则连接的数据帧将不包含相应的连接。join应用编程接口有六种不同的实现:

join(right: Dataset[_]): DataFrame
 Condition-less inner join
 join(right: Dataset[_], usingColumn: String): DataFrame
 Inner join with a single column
 join(right: Dataset[_], usingColumns: Seq[String]): DataFrame
 Inner join with multiple columns
 join(right: Dataset[_], usingColumns: Seq[String], joinType: String):
 DataFrame
Join with multiple columns and a join type (inner, outer,....)
 join(right: Dataset[_], joinExprs: Column): DataFrame
 Inner Join using a join expression
join(right: Dataset[_], joinExprs: Column, joinType: String): DataFrame
 Join using a Join expression and a join type (inner, outer, ...)

我们将使用其中一个 API 来了解如何使用joinAPI;但是,您可以根据用例选择使用其他 API:

def join(right: Dataset[_], joinExprs: Column, joinType: String):
 DataFrame
Join with another DataFrame using the given join expression
 right: Right side of the join.
joinExprs: Join expression.
 joinType : Type of join to perform. Default is inner join
// Scala:
 import org.apache.spark.sql.functions._
 import spark.implicits._
 df1.join(df2, $"df1Key" === $"df2Key", "outer")

请注意,在接下来的几节中将详细介绍连接。

连接的内部工作方式

Join 通过使用多个执行器对数据帧的分区进行操作来工作。但是,实际操作和后续性能取决于连接的类型和要连接的数据集的性质。在下一节中,我们将研究不同类型的连接。

随机加入

两个大数据集之间的连接包括无序连接,其中左右数据集的分区分布在执行器中。混洗是昂贵的,分析逻辑以确保分区和混洗的分布是最佳的是很重要的。

以下是 shuffle join 内部工作原理的说明:

广播加入

一个大数据集和一个小数据集之间的连接是通过将小数据集广播给从左数据集开始存在分区的所有执行器来实现的,这种连接称为广播连接

以下是广播连接如何在内部工作的说明:

连接类型

下表列出了不同类型的联接。这一点很重要,因为连接两个数据集时所做的选择对输出和性能都有很大影响:

| 连接类型 | 描述 |
| 内部的 | 内部连接从左到右比较每一行,并且仅当左右数据集都具有非NULL值时,才组合匹配的行对。 |
| 外侧,全外侧,全外侧 | 完全外部联接给出了左侧和右侧表中的所有行。如果我们想保留两个表中的所有行,我们使用完全外部连接。当其中一个表匹配时,完全外部联接返回所有行 |
| 左反 | 左反连接只给出基于左侧表的那些行,这些行不在右侧表中。 |
| 左,左外侧 | 左外连接给出了左中的所有行加上左和右的公共行(内连接)。如果不正确,填写NULL。 |
| 左半 | 左半连接根据右侧的存在只给出左侧的行。不包括右侧的值。 |
| 右,右外侧 | 右外连接给出了右中的所有行加上左和右的公共行(内连接)。如果不在左边,填写NULL。 |

我们将通过使用示例数据集来研究不同的连接类型是如何工作的:

scala> val statesPopulationDF = spark.read.option("header",
 "true").option("inferschema", "true").option("sep",
 ",").csv("statesPopulation.csv")
 statesPopulationDF: org.apache.spark.sql.DataFrame = [State: string, Year:
 int ... 1 more field]
scala> val statesTaxRatesDF = spark.read.option("header",
 "true").option("inferschema", "true").option("sep",
 ",").csv("statesTaxRates.csv")
 statesTaxRatesDF: org.apache.spark.sql.DataFrame = [State: string, TaxRate:
 double]
scala> statesPopulationDF.count
 res21: Long = 357
scala> statesTaxRatesDF.count
 res32: Long = 47
%sql
 statesPopulationDF.createOrReplaceTempView("statesPopulationDF")
 statesTaxRatesDF.createOrReplaceTempView("statesTaxRatesDF")

内部连接

当两个数据集中的State都不为NULL时,内部连接会产生来自statesPopulationDFstatesTaxRatesDF的行:

通过State列连接两个数据集,如下所示:

val joinDF = statesPopulationDF.join(statesTaxRatesDF,
 statesPopulationDF("State") === statesTaxRatesDF("State"), "inner")
%sql
 val joinDF = spark.sql("SELECT * FROM statesPopulationDF INNER JOIN
 statesTaxRatesDF ON statesPopulationDF.State = statesTaxRatesDF.State")
scala> joinDF.count
 res22: Long = 329
scala> joinDF.show
 +--------------------+----+----------+--------------------+-------+
 | State|Year|Population| State|TaxRate|
+--------------------+----+----------+--------------------+-------+
 | Alabama|2010| 4785492| Alabama| 4.0|
 | Arizona|2010| 6408312| Arizona| 5.6|
 | Arkansas|2010| 2921995| Arkansas| 6.5|
 | California|2010| 37332685| California| 7.5|
 | Colorado|2010| 5048644| Colorado| 2.9|
 | Connecticut|2010| 3579899| Connecticut| 6.35|

你可以在joinDF上运行explain()查看执行计划:

scala> joinDF.explain
 == Physical Plan ==
*BroadcastHashJoin [State#570], [State#577], Inner, BuildRight
 :- *Project [State#570, Year#571, Population#572]
 : +- *Filter isnotnull(State#570)
 : +- *FileScan csv [State#570,Year#571,Population#572] Batched: false,
Format: CSV, Location: InMemoryFileIndex[file:/Users/salla/spark-2.1.0-binhadoop2.7/
 statesPopulation.csv], PartitionFilters: [], PushedFilters:
 [IsNotNull(State)], ReadSchema:
 struct<State:string,Year:int,Population:int>
 +- BroadcastExchange HashedRelationBroadcastMode(List(input[0, string,
 true]))
 +- *Project [State#577, TaxRate#578]
 +- *Filter isnotnull(State#577)
 +- *FileScan csv [State#577,TaxRate#578] Batched: false, Format: CSV,
Location: InMemoryFileIndex[file:/Users/salla/spark-2.1.0-binhadoop2.7/
 statesTaxRates.csv], PartitionFilters: [], PushedFilters:[IsNotNull(State)], ReadSchema: struct<State:string,TaxRate:double>

左外连接

左外连接导致从statesPopulationDF开始的所有行,包括在statesPopulationDFstatesTaxRatesDF中常见的任何行:

通过State列连接两个数据集,如下所示:

val joinDF = statesPopulationDF.join(statesTaxRatesDF,
 statesPopulationDF("State") === statesTaxRatesDF("State"), "leftouter")
%sql
 val joinDF = spark.sql("SELECT * FROM statesPopulationDF LEFT OUTER JOIN
statesTaxRatesDF ON statesPopulationDF.State = statesTaxRatesDF.State")
 scala> joinDF.count
 res22: Long = 357
 scala> joinDF.show(5)
 +----------+----+----------+----------+-------+
 | State|Year|Population| State|TaxRate|
 +----------+----+----------+----------+-------+
 | Alabama|2010| 4785492| Alabama| 4.0|
 | Alaska|2010| 714031| null| null|
 | Arizona|2010| 6408312| Arizona| 5.6|
 | Arkansas|2010| 2921995| Arkansas| 6.5|
 |California|2010| 37332685|California| 7.5|
 +----------+----+----------+----------+-------+

右外连接

右外连接导致从statesTaxRatesDF开始的所有行,包括在statesPopulationDFstatesTaxRatesDF中常见的行:

通过State列连接两个数据集,如下所示:

val joinDF = statesPopulationDF.join(statesTaxRatesDF,
 statesPopulationDF("State") === statesTaxRatesDF("State"), "rightouter")
%sql
 val joinDF = spark.sql("SELECT * FROM statesPopulationDF RIGHT OUTER JOIN
 statesTaxRatesDF ON statesPopulationDF.State = statesTaxRatesDF.State")
scala> joinDF.count
 res22: Long = 323
scala> joinDF.show
 +--------------------+----+----------+--------------------+-------+
 | State|Year|Population| State|TaxRate|
 +--------------------+----+----------+--------------------+-------+
 | Colorado|2011| 5118360| Colorado| 2.9|
 | Colorado|2010| 5048644| Colorado| 2.9|
 | null|null| null|Connecticut| 6.35|
 | Florida|2016| 20612439| Florida| 6.0|
 | Florida|2015| 20244914| Florida| 6.0|
 | Florida|2014| 19888741| Florida| 6.0|

外部连接

外部连接导致从statesPopulationDFstatesTaxRatesDF的所有行:

通过State列连接两个数据集,如下所示:

val joinDF = statesPopulationDF.join(statesTaxRatesDF,
 statesPopulationDF("State") === statesTaxRatesDF("State"), "fullouter")
%sql
 val joinDF = spark.sql("SELECT * FROM statesPopulationDF FULL OUTER JOIN
 statesTaxRatesDF ON statesPopulationDF.State = statesTaxRatesDF.State")
scala> joinDF.count
 res22: Long = 351
scala> joinDF.show
 +--------------------+----+----------+--------------------+-------+
 | State|Year|Population| State|TaxRate|
 +--------------------+----+----------+--------------------+-------+
 | Delaware|2010| 899816| null| null|
 | Delaware|2011| 907924| null| null|
 | West Virginia|2010| 1854230| West Virginia| 6.0|
 | West Virginia|2011| 1854972| West Virginia| 6.0|
 | Missouri|2010| 5996118| Missouri| 4.225|
 | null|null| null| Connecticut| 6.35|

左反连接

当且仅当statesTaxRatesDF中没有对应的行时,左反连接仅导致从statesPopulationDF开始的行:

通过State列连接两个数据集,如下所示:

val joinDF = statesPopulationDF.join(statesTaxRatesDF,
 statesPopulationDF("State") === statesTaxRatesDF("State"), "leftanti")
 %sql
 val joinDF = spark.sql("SELECT * FROM statesPopulationDF LEFT ANTI JOIN
 statesTaxRatesDF ON statesPopulationDF.State = statesTaxRatesDF.State")
scala> joinDF.count
res22: Long = 28
 scala> joinDF.show(5)
 +--------+----+----------+
 | State|Year|Population|
 +--------+----+----------+
 | Alaska|2010| 714031|
 |Delaware|2010| 899816|
 | Montana|2010| 990641|
 | Oregon|2010| 3838048|
 | Alaska|2011| 722713|
 +--------+----+----------+

左半连接

当且仅当在statesTaxRatesDF中有对应的行时,左半连接导致仅从statesPopulationDF开始的行:

通过State列连接两个数据集,如下所示:

val joinDF = statesPopulationDF.join(statesTaxRatesDF,
 statesPopulationDF("State") === statesTaxRatesDF("State"), "leftsemi")
 %sql

val joinDF = spark.sql("SELECT * FROM statesPopulationDF LEFT SEMI JOIN
 statesTaxRatesDF ON statesPopulationDF.State = statesTaxRatesDF.State")
scala> joinDF.count
res22: Long = 322
 scala> joinDF.show(5)
 +----------+----+----------+
 | State|Year|Population|
 +----------+----+----------+
 | Alabama|2010| 4785492|
 | Arizona|2010| 6408312|
 | Arkansas|2010| 2921995|
 |California|2010| 37332685|
 | Colorado|2010| 5048644|
 +----------+----+----------+

交叉连接

交叉连接将左边的每一行与右边的每一行进行匹配,生成笛卡尔叉积:

通过State列连接两个数据集,如下所示:

scala> val joinDF=statesPopulationDF.crossJoin(statesTaxRatesDF)
 joinDF: org.apache.spark.sql.DataFrame = [State: string, Year: int ... 3
 more fields]
%sql
val joinDF = spark.sql("SELECT * FROM statesPopulationDF CROSS JOIN
 statesTaxRatesDF")
 scala> joinDF.count
res46: Long = 16450
 scala> joinDF.show(10)
 +-------+----+----------+-----------+-------+
 | State|Year|Population| State|TaxRate|
 +-------+----+----------+-----------+-------+
 |Alabama|2010| 4785492| Alabama| 4.0|
 |Alabama|2010| 4785492| Arizona| 5.6|
 |Alabama|2010| 4785492| Arkansas| 6.5|
 |Alabama|2010| 4785492| California| 7.5|
 |Alabama|2010| 4785492| Colorado| 2.9|
 |Alabama|2010| 4785492|Connecticut| 6.35|
 |Alabama|2010| 4785492| Florida| 6.0|
 |Alabama|2010| 4785492| Georgia| 4.0|
 |Alabama|2010| 4785492| Hawaii| 4.0|
 |Alabama|2010| 4785492| Idaho| 6.0|
 +-------+----+----------+-----------+-------+

You can also use join with cross joinType instead of calling the cross join API: statesPopulationDF.join(statesTaxRatesDF, statesPopulationDF("State").isNotNull, "cross").count.

加入对性能的影响

选择的连接类型直接影响连接的性能。这是因为联接需要在执行器之间进行数据洗牌来执行任务,因此在使用联接时需要考虑不同的联接,甚至联接的顺序。下面是一个在编写连接代码时可以用作参考的表:

| 连接类型 | 性能注意事项和提示 |
| 内部的 | 内部联接要求左右表具有相同的列。如果您在左侧或右侧有重复或多个键的副本,连接将很快变成某种笛卡尔连接,比正确设计以最小化多个键花费更长的时间来完成。 |
| 跨过 | 交叉连接将左边的每一行与右边的每一行进行匹配,生成笛卡尔叉积。由于这是性能最差的连接,因此应谨慎使用,仅在特定用例中使用。 |
| 外侧,全外侧,全外侧 | 完全外部联接给出联接子句左侧和右侧表中的所有(匹配和不匹配)行。当我们想要保留两个表中的所有行时,我们使用完全外部连接。当其中一个表匹配时,完全外部联接返回所有行。如果在几乎没有共同点的表上使用,可能会导致非常大的结果,从而降低性能。 |
| 左反 | 左反连接只给出基于左侧表的那些行,这些行不在右侧表中。当我们希望只保留左表中的行而不保留右表中的行时,请使用此选项。非常好的性能,因为只充分考虑了一个表,并且只检查了另一个表的连接条件。 |
| 左,左外侧 | 左外连接给出了左侧表中的所有行,以及两个表共有的行(内连接)。如果在几乎没有共同点的表上使用,会导致非常大的结果,从而降低性能。 |
| 左半 | 当且仅当左侧表中的行存在于右侧表中时,左侧半联接才给出左侧表中的行。当且仅当行在右表中找到时,使用此选项从左表中获取行。这与上面看到的 leftanti join 相反。不包括右侧值。非常好的性能,因为只充分考虑了一个表,并且只检查了另一个表的连接条件。 |
| 右,右外侧 | 右外连接给出了右侧表中的所有行以及左侧和右侧的公共行(内连接)。使用它可以获得右表中的所有行以及左表和右表中的行。如果不在左边,填写NULL。性能类似于本表前面提到的左外连接。 |

摘要

在本章中,我们讨论了数据框的起源,以及 Spark SQL 如何在数据框之上提供 SQL 接口。数据帧的强大之处在于,与最初基于 RDD 的计算相比,执行时间减少了。拥有这样一个功能强大的层和一个简单的类似于 SQL 的接口使它变得更加强大。我们还研究了创建和操作数据帧的各种 API,并深入挖掘了聚合的复杂特性,包括groupByWindowrollupcubes。最后,我们还研究了连接数据集的概念以及各种可能的连接类型,如内部、外部、交叉等。

我们将通过 Apache Spark 在第 7 章实时分析中探索令人兴奋的实时数据处理和分析世界。

七、Apache Spark 实时分析

在本章中,我们将介绍 Apache Spark 的流处理模型,并向您展示如何构建基于流的实时分析应用。本章将重点介绍 Spark Streaming,并向您展示如何使用 Spark API 处理数据流。

更具体地说,读者将学习如何处理推特的推文,以及如何以几种方式处理实时数据流。基本上,本章将集中讨论以下内容:

  • 流媒体简介
  • Spark 流
  • 离散化流
  • 有状态和无状态转换
  • 检查点
  • 与其他流媒体平台(如 Apache Kafka)一起运营
  • 结构化流

流动

在现代世界,越来越多的人通过互联网相互联系。随着智能手机的出现,这一趋势飞速发展。如今,智能手机可以用来做很多事情,比如查看社交媒体、在线点餐、在线叫出租车。我们发现自己比以往任何时候都更加依赖互联网,未来我们只会变得更加依赖互联网。随着这一发展,数据生成量大幅增加。随着互联网开始繁荣,数据处理的本质发生了变化。任何时候通过手机访问某个应用或服务,都会进行实时数据处理。因为应用的质量关系重大,公司被迫改进数据处理,随着改进而来的是范式转变。目前正在研究和使用的一个范例是在高端基础设施上建立高度可扩展的实时(或尽可能接近实时)处理引擎的想法。它必须迅速发挥作用,接受变化和失败。基本上,数据处理必须尽可能接近实时,没有中断。

大多数被监控的系统会产生大量的数据,这些数据是不确定但连续的事件流。与任何其他数据处理系统一样,收集、存储和处理数据的挑战依然存在。实时需求导致了额外的复杂性。为了收集和处理这些不确定的流,需要一个高度可扩展的架构,并且存在许多这样的系统的迭代,例如弗林克、AMQ、Storm 和 Spark。更新、更现代化的系统非常高效和灵活,这意味着公司可以比以往任何时候都更容易、更高效地实现目标。这些新的技术发展允许来自各种来源的数据消费,以及数据处理和使用。所有这些都有最小的延迟。

当你用智能手机点披萨时,你可以用信用卡付款,然后直接把披萨送到你的地址。有些公交系统可以让你在地图上实时追踪个别公交车,如果你需要等公交车,可以用智能手机找附近的星巴克喝杯咖啡。

通过查看预计到达时间,你可以对去机场的行程做出明智的决定。如果汽车到达的时间可能对你的飞行计划不利,你可以取消你的旅程,坐附近的出租车。如果交通堵塞导致无法准时到达机场,你可以重新安排或取消航班。

要了解所有这些数据是如何实时处理的,我们必须首先了解流架构的基础知识。对于实时流体系结构来说,高速收集大量数据至关重要,但也要确保数据得到处理。

下面是一个通用的流处理系统,生产者将事件放入消息传递系统,而消费者则从消息传递系统中读取:

实时流数据可以根据以下三种范例进行处理:

  • 至少一次处理
  • 最多一次处理
  • 一次性处理

一次处理是最理想的情况,但在各种场景下很难实现。在实现这种标准非常复杂的情况下,我们必须在一次性处理的属性上做出妥协。

至少一次处理

在至少处理一次的范例中,最后接收到的事件的位置只有在它被处理之后才被保存,并且结果被存储在某个地方。在失败的情况下,消费者仍然能够读取和重新处理旧事件。但是,不能假设接收到的事件从未处理过或部分处理过,因此在再次调用前一个事件后,可能会出现结果重复。当名字说数据已经被处理至少一次时,这就是它的意思。

这个范例对于任何更新跑马灯或仪表以显示当前值的应用都是理想的。但是,总和、计数器或任何依赖于这些聚合类型的准确性的东西对于至少一次处理来说并不理想,主要是因为重复的事件会导致不正确的结果。

顺序如下:

  • 保存结果
  • 保存偏移量

下图显示了至少一次的处理范例:

最多一次处理

在这个范例中,最后一个事件的位置在实际处理之前被保存,结果被存储在某个地方。如果出现故障和消费者重启,旧事件将不会被再次读取。然而,可能会有事件丢失的可能性,因为我们不能假设所有接收到的事件都已处理,因此永远无法再次检索到这些事件。当范例声明事件要么不处理,要么最多处理一次时,这就是范例的含义。它非常适合需要更新跑马灯或仪表以显示当前值的情况。此外,如果准确性不是强制性的,或者如果需要所有事件,任何聚合(如累计总和或计数器)也可能起作用。任何丢失的事件都会导致结果丢失或不正确。

顺序如下:

  • 保存结果
  • 保存偏移量

如果出现故障,消费者最终会重新启动,那么每个事件都会有偏移量(前提是它们都在故障发生之前处理),但是事件可能会丢失,如下图所示:

一次性处理

这个范例类似于至少一次处理。它只保存实际处理后收到的最后一个事件,并将结果存储在某个地方,以便在失败和消费者重启的情况下,可以重新读取和重新处理旧事件。然而,存在潜在重复的原因,因为不能假设所有事件要么没有处理,要么只是部分处理。与至少一次处理不同,任何重复的事件都会被丢弃而不被处理,从而导致只进行一次处理。

这非常适合精度很重要的任何应用,例如涉及聚合的应用,如精确计数器,或者任何只需要处理一次事件而不会丢失的应用。

顺序如下:

  • 保存结果
  • 保存偏移量

以下是消费者在失败后重新启动时发生的情况。事件已经被处理,但是偏移没有被保存,如下图所示:

一次加工如何复制?涉及两个过程:

  • 幂等更新
  • 事务性更新

Spark Streaming 在 Spark 2.0 及更高版本中实现了结构流,支持一次处理。本章稍后将介绍结构流。

在幂等更新中,根据生成的唯一密钥或标识保存结果。在复制的情况下,生成的密钥或标识将已经在结果中(例如,数据库),因此消费者可以在不更新结果的情况下移除复制。然而,这可能会变得复杂,因为为每个事件生成唯一的密钥并不是一项简单的任务,因为在消费者端需要额外的处理。此外,对于结果和偏移,数据库可以是独立的。

在事务更新中,结果是分批保存的,需要开始一个事务并提交一个事务,因此在提交的情况下,事件将被成功处理。如果出现重复,可以在不更新结果的情况下删除它们。然而,这些比幂等更新更复杂,因为现在需要存储事务数据。另一个缺点是,对于结果和偏移,数据库可能需要保持不变。

Decisions on using at-least-once processing or at-most-once processing should be made after looking into the use case that you are trying to build, to keep a reasonable level of accuracy and performance.

Spark 流

Spark Streaming 并不是第一个流架构。随着时间的推移,已经开发了多种技术来满足各种实时处理需求。最早流行的流处理器技术之一是推特 Storm,它被用于许多企业。Spark 包括流媒体库,该库已发展成为当今使用最广泛的技术。这主要是因为与所有其他技术相比,Spark Streaming 具有一些显著的优势,最重要的是它将 Spark Streaming APIs 集成到其核心 API 中。不仅如此,Spark Streaming 还与 Spark ML 和 Spark SQL 以及 GraphX 集成在一起。由于所有这些集成,Spark 是一种强大而通用的流媒体技术。

注意https://Spark . Apache . org/docs/2 . 1 . 0/Streaming-programming-guide . html有更多关于 Spark Streaming Flink、Heron (Twitter Storm 的继承者)、Samza 及其各种功能的信息;例如,他们处理事件的能力,同时最大限度地减少延迟。但是,Spark Streaming 会消耗数据并在微批处理中进行处理。这些微贴片的大小至少为 500 毫秒。

Apache Apex, Flink, Samza, Heron, Gearpump, and other new technologies are all competitors of Spark Streaming in some cases. Spark Streaming, will not be the right fit if you need true, event-by-event processing.

Spark Streaming 的工作原理是,按照用户的配置,在特定的时间间隔内创建一批事件,并在另一个指定的时间间隔内交付这些事件进行处理。

Spark Streaming 支持多个输入源,并且可以将结果写入多个接收器:

SparkContext类似,Spark 流包含一个StreamingContext,这是流发生的主要入口点。StreamingConext依赖于SparkContext,而SparkContext实际上可以直接用于流媒体任务。StreamingContextSparkContext相似,不同之处在于StreamingContext要求程序对配料间隔的时间间隔/持续时间进行规定,范围从分钟到毫秒:

The SparkContext is the main point of entry. The StreamingContext reuses the logic that is part of SparkContext (task scheduling and resource management).

StreamingContext

作为流媒体的主要入口点,StreamingContext处理流媒体应用的动作,包括 RDD 的检查点和转换。

创建流上下文

新的StreamingContext可以通过以下几种方式之一创建:

  • 使用现有的SparkContext创建StreamingContext:
StreamingContext(sparkContext: SparkContext, batchDuration: Duration)
scala> val ssc = new StreamingContext(sc, Seconds(10))
  • 通过提供新配置所需的配置来创建StreamingContext:
StreamingContext(conf: SparkConf, batchDuration: Duration)
scala> val conf = newSparkConf().setMaster("local[1]").setAppName("TextStreams")
scala> val ssc = new StreamingContext(conf, Seconds(10))
  • getOrCreate功能用于从前一个检查点数据段重新创建一个StreamingContext,或者创建一个新的StreamingContext。如果数据不存在,则调用提供的creatingFunc创建StreamingContext,如下所示:
def getOrCreate(
checkpointPath: String,
creatingFunc: () => StreamingContext,
hadoopConf: Configuration = SparkHadoopUtil.get.conf,
createOnError: Boolean = false
): StreamingContext

开始流上下文

通过开始执行使用StreamingContext定义的流来启动流应用:

def start(): Unit
scala> ssc.start()

停止流上下文

StreamingContext停止时,所有处理停止。您需要创建一个新的StreamingContext,并且您必须调用start()来重新启动应用。有两种有用的 API 可以停止流处理:

  • 使用以下命令立即停止流执行(这不会等待接收到的数据被处理):
def stop(stopSparkContext: Boolean)
scala> ssc.stop(false)
  • 使用以下选项停止流的执行,并允许处理接收到的数据:
def stop(stopSparkContext: Boolean, stopGracefully: Boolean)
scala> ssc.stop(true, true)

输入流

有几种类型的输入流,都需要创建StreamingContext,如下节所示。

接收流

输入流由任何用户实现的接收器创建。它是可定制的:

更多详情请访问http://spark . Apache . org/docs/latest/streaming-custom-receiver . html:

API declaration for receiverStream:
def receiverStream[T]: ClassTag](receiver: Receiver[T]):
ReceiverInputDStream[T]

socketTextStream

socketTextStream使用 TCP 源hostname:port创建输入流。数据通过 TCP 套接字接收,接收到的字节被解释为 UTF8,用\n定界符行编码:

def socketTextStream(hostname: String, port: Int,
storageLevel: StorageLevel = StorageLevel.MEMORY_AND_DISK_SER_2):
ReceiverInputDStream[String]

rawSocketStream

rawSocketStream使用网络源hostname:port创建输入流。这是接收数据最有效的方法:

def rawSocketStream[T: ClassTag](hostname: String, port: Int,
storageLevel: StorageLevel = StorageLevel.MEMORY_AND_DISK_SER_2):
ReceiverInputDStream[T]

fileStream

fileStream创建一个监控 Hadoop 兼容文件系统的输入流。它使用给定的键值类型和输入格式读取新文件。任何以.开头的文件名都会被忽略。调用原子文件重命名功能,以.开头的文件名被重命名为可用的文件名,该文件名可由fileStream拾取并处理其内容:

def fileStream[K: ClassTag, V: ClassTag, F <: NewInputFormat[K, V]:
ClassTag] (directory: String): InputDStream[(K, V)]

文本文件流

textFileStream命令创建一个监控 Hadoop 兼容文件系统的输入流。它读取新文件,作为文本文件,键为Longwritable,值为text,输入格式为TextInputFormat。任何以.开头的文件都会被忽略:

def textFileStream(directory: String): Dstream[String]

二进制记录流

使用binaryRecordsStream,创建一个监控 Hadoop 兼容文件系统的输入流。任何以.开头的文件名都会被忽略:

def binaryRecordsStream(directory: String, recordLength: Int):
Dstream[Array[Byte]]

queueStream

使用queueStream,从 rdd 队列中创建输入流。在每个批处理中,队列返回的一个或所有 rdd 都会被处理:

def queueStream[T: ClassTag](queue: Queue[RDD[T]], oneAtATime: Boolean =
true): InputDStream[T]

文本文件流示例

以下是使用textFileStream方法的 Spark 流的示例。一个StreamingContext由 Spark 壳SparkContext ( sc)创建,间隔10秒。textFileStream将启动,然后将监控名为streamfiles的目录,并处理在该目录中找到的任何新文件。在本例中,将打印 RDD 中的元素数量:

scala> import org.apache.spark._
scala> import org.apache.spark.streaming._
scala> val ssc = new StreamingContext(sc, Seconds(10))
scala> val filestream = ssc.textFileStream("streamfiles")
scala> filestream.foreachRDD(rdd => {println(rdd.count())})
scala> ssc.start

twitterStream 示例

以下是如何使用 Spark Streaming 处理推特推文的另一个示例:

  1. 打开一个终端,将目录改为spark-2.1.1-bin-hadoop2.7

  2. spark-2.1.1-bin-hadoop2.7文件夹下创建一个名为streamouts的文件夹,其中安装了 Spark。Streamouts对象将收集推文,并在应用运行时将其转换为文本文件。

  3. 将这些 jar 下载到目录中:http://central . maven . org/maven 2/org/Apache/bahir/spark-streaming-Twitter _ 2.11/2 . 1 . 0/spark-streaming-Twitter _ 2.11-2 . 1 . 0 . jarhttp://central . maven . org/maven 2/org/twiter 4j/twiter 4j-core/4 . 0 . 6/twiter 4j-core-4 . 0 . 6 . jar【T3

  4. 启动spark-shell和 Twitter 集成所需的所有 JARs,这里指定为./bin/spakr-shell –jars twitter4j-stream-4.0.6.jartwitter4j-core-4.0.6.jarspark-streaming-twitter_2.11-2.1.0.jar

  5. 现在可以编写示例代码了。下面是测试推特事件处理的代码:

import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.streaming.twitter._
import twitter4j.auth.OAuthAuthorization
import twitter4j.conf.ConfigurationBuilder
//you can replace the next 4 settings with your own twitter account
settings.
System.setProperty("twitter4j.oauth.consumerKey",
"8wVysSpBc0LGzbwKMRh8hldSm")
System.setProperty("twitter4j.oauth.consumerSecret",
"FpV5MUDWliR6sInqIYIdkKMQEKaAUHdGJkEb4MVhDkh7dXtXPZ")
System.setProperty("twitter4j.oauth.accessToken", "817207925756358656-
yR0JR92VBdA2rBbgJaF7PYREbiV8VZq")
System.setProperty("twitter4j.oauth.accessTokenSecret",
"JsiVkUItwWCGyOLQEtnRpEhbXyZS9jNSzcMtycn68aBaS")
val ssc = new StreamingContext(sc, Seconds(10))
val twitterStream = TwitterUtils.createStream(ssc, None)
twitterStream.saveAsTextFiles("streamouts/tweets", "txt")
ssc.start()

streamouts文件夹现在将包含几个推文输出文本文件。这些可以被打开和检查,以确保它们包含推文。

离散化流

离散流 ( 数据流)是 Spark 流所基于的抽象。每个
数据流被表示为一个 rdd 序列,每个 rdd 在特定的时间
间隔被创建。然后,可以使用类似于
这样的概念作为基于有向循环图的执行计划(DAG),类似于常规 RDD 来处理数据流。就像常规的 RDD 处理一样,
作为执行计划一部分的任何转换和动作都在数据流的情况下处理,如下图所示:

数据流根据时间间隔将很长的数据流分成更小的数据块,并将每个数据块作为 RDD 进行处理。这些微批次是独立处理的,每个微批次都是无状态的。假设批处理间隔为 5 秒钟,事件被实时消耗,并且微批处理作为 RDD 进行进一步处理。关于 Spark Streaming 需要注意的一点是,用于处理微批处理事件的 API 调用被集成到 Spark APIs 中,以便与架构的其余部分集成。每当创建一个微批处理时,它就变成一个 RDD,允许使用如下图所示的 Spark APIs 进行无缝处理:

DStream类类似于以下示例:

class DStream[T: ClassTag] (var ssc: StreamingContext)
//hashmap of RDDs in the DStream
var generatedRDDs = new HashMap[Time, RDD[T]]()

在这个例子中,创建了一个StreamingContext,它每五秒钟生成一个微批处理,以创建一个类似于 Spark 核心应用编程接口 RDD 的 RDD。数据流中的这些关系数据库可以像任何其他关系数据库一样进行处理。构建流应用的步骤如下:

  1. SparkContext创建StreamingContext
  2. 从流中创建DStream
  3. 上下文提供了可以应用于关系数据库的转换和操作。
  4. 通过调用StreamingContext上的start启动流媒体应用。Spark 流应用实时处理消费和处理过程。

No further operations may be added once the Spark Streaming application is started. A stopped StreamingContext cannot be restarted ,either, and a new StreamingContext will have to be made.

以下是如何创建访问推特的流式作业的示例:

  1. SparkContext创建StreamingContext:
scala> val ssc = new StreamingContext(sc, Seconds(5))
ssc: org.apache.spark.streaming.StreamingContext =
org.apache.spark.streaming.StreamingContext@8ea5756
  1. StreamingContext创建一个DStream:
scala> val twitterStream = TwitterUtils.createStream(ssc, None)
twitterStream:
org.apache.spark.streaming.dstream.ReceiverInputDStream[twitter4j.Status] =
org.apache.spark.streaming.twitter.TwitterInputDStream@46219d14
  1. 提供适用于每个 RDD 的转换和操作:
val aggStream = twitterStream
.flatMap(x => x.getText.split(" ")).filter(_.startsWith("#"))
.map(x => (x, 1))
.reduceByKey(_ + _)
  1. 通过调用StreamingContext上的start启动流媒体应用:
ssc.start()
//to stop just call stop on the StreamingContext
ssc.stop(false)

步骤 2 中,我们创建了一个ReceiverInputDStream类型的DSTream。这是一个抽象类,它将任何必须在工作节点上启动接收器的InputDStream定义为能够接收外部数据。

这个例子展示了我们从推特流接收到的信息:

class InputDStream[T: ClassTag](_ssc: StreamingContext) extends
DStream[T](_ssc)
class ReceiverInputDStream[T: ClassTag](_ssc: StreamingContext) extends
InputDStream[T](_ssc)

twitterStream上运行一个变换flatMap(),将产生一个FlatMappedDStream,如下代码所示:

scala> val wordStream = twitterStream.flatMap(x => x.getText().split(" "))
wordStream: org.apache.spark.streaming.dstream.DStream[String] = org.apache.spark.streaming.dstream.FlatMappedDStream@1ed2dbd5

转换

数据流上的转换类似于适用于 Spark 核心 RDD 的转换。
数据流由 RDD 组成,因此对每个 RDD 进行转换,为每个 RDD 生成一个转换后的 RDD,从而创建一个转换后的数据流。每个转换
创建一个指定的数据流派生类。

有许多为某个功能而构建的数据流类;使用不同的 DStream 派生类实现地图转换、窗口函数、缩减动作和不同的InputStream类型。

下表展示了可能的转换类型:

| 转化 | 表示 |
| map(func) | 对数据流的每个元素应用transformation函数,并返回一个新的数据流。 |
| filter(func) | 过滤掉DStream的记录,返回一个新的数据流。 |
| repartition(numPartitions) | 创建更多或更少的分区来重新分布数据以改变并行度。 |
| union(otherStream) | 合并两个源数据流中的元素,并返回一个新数据流。 |
| count() | 通过计算源数据流的每个 RDD 中的元素数量,返回一个新的数据流。 |
| reduce(func) | 通过对源数据流的每个元素应用reduce函数,返回一个新的数据流。 |
| countByValue() | 计算每个Key的频率,并返回一个新的(Key, Long)对数据流。 |
| reduceByKey(func, [numTasks]) | 在源数据流的 RDDs 中按Key聚合数据,并返回一个新的(Key, Value)对数据流。 |
| join(otherStream, [numTasks]) | 连接两个(K, V)(K, W)对数据流,并返回一个新的(K, (V, W))对数据流,合并两个数据流的值。 |
| cogroup(otherStream, [numTasks]) | 当在由(K, V)(K, W)对组成的数据流上调用时,cogroup转换将返回由(K, Seq(V), Seq(W))元组组成的新数据流。 |
| transform(func) | 对源数据流的每个 RDD 应用一个转换函数,并返回一个新的数据流。 |
| updateStateByKey(func) | 通过对每个键的先前状态和新值应用给定函数来更新该键的状态。通常用于维护状态机。 |

Windows 操作

Spark 流允许窗口处理,这使您能够在事件的滑动窗口上应用转换。此滑动窗口是在指定的时间间隔内创建的。

每次窗口滑过数据流时,落入窗口
规范的源数据流将被合并以创建一个窗口数据流,如下图所示。窗口必须有两个指定的参数:

  • 窗口长度–指定考虑的间隔长度
  • 滑动间隔–创建窗口的间隔

The window length and the sliding interval are required to be a multiple of the block interval.

以下是一些常见转换的表格:

| 转化 | 表示 |
| window(windowLength, slideInterval) | 在源数据流上创建一个窗口,并将其作为新数据流返回。 |
| countByWindow(windowLength, slideInterval) | 通过应用滑动窗口返回数据流中的元素计数。 |
| reduceByWindow(func, windowLength, slideInterval) | 在创建一个windowLength 长度的滑动窗口后,通过对源数据流的每个元素应用reduce函数返回一个新的数据流。 |
| reduceByKeyAndWindow(func, windowLength,``slideInterval, [numTasks]) | 在应用于源数据流 RDDs 的窗口中,按Key聚合数据,并返回一个新的(Key, Value)对数据流。计算由func函数提供。 |
| reduceByKeyAndWindow(func, invFunc,``windowLength, slideInterval, [numTasks]) | aggrega–窗口 w 应用于源数据流的 RDDs 并返回新数据流对的时间间隔。前一个函数和这个函数的主要区别是invFunc,它提供了在滑动窗口开始时要进行的计算。 |
| countByValueAndWindow(windowLength,``slideInterval, [numTasks]) | 计算每个Key的频率,并在指定的滑动窗口内返回一个新的(Key, Long)对数据流。 |

让我们重温一下推特流的例子。现在的目标是每 5 秒钟打印一次推文中最常用的五个单词,15 秒钟的窗口每 10 秒钟滑动一次。要运行该代码,请执行以下步骤:

  1. 打开一个终端,将目录改为spark-2.1.1-bin-hadoop2.7

  2. 在安装 Spark 的spark-2.1.1-bin-hadoop2.7文件夹下创建一个名为streamouts的文件夹。运行应用后,streamouts文件夹将包含所有推文到文本的文件。

  3. 下载以下 JARs,放入目录:http://central . maven . org/maven 2/org/Apache/bahir/spark-streaming-Twitter _ 2.11/2 . 1 . 0/spark-streaming-Twitter _ 2.11-2 . 1 . 0 . jarhttp://central . maven . org/maven 2/org/Twitter 4j/Twitter 4j-core/4 . 0 . 6/Twitter 4j-core-4 . 0。

  4. 启动带有集成指定推文所需的 JARs 的 Spark 外壳;./bin/spark-shell --jars twitter4j-stream-4.0.6.jar,twitter4j- core-4.0.6.jar,spark-streaming-twitter_2.11-2.1.0.jar

  5. 以下是一些测试推文处理的示例代码:

import org.apache.log4j.Logger
import org.apache.log4j.Level
Logger.getLogger("org").setLevel(Level.OFF)
import java.util.Date
import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.streaming.twitter._
import twitter4j.auth.OAuthAuthorization
import twitter4j.conf.ConfigurationBuilder
System.setProperty("twitter4j.oauth.consumerKey","8wVysSpBc0LGzbwKMRh8hldSm")
System.setProperty("twitter4j.oauth.consumerSecret",
"FpV5MUDWliR6sInqIYIdkKMQEKaAUHdGJkEb4MVhDkh7dXtXPZ")
System.setProperty("twitter4j.oauth.accessToken",
"817207925756358656-yR0JR92VBdA2rBbgJaF7PYREbiV8VZq")
System.setProperty("twitter4j.oauth.accessTokenSecret",
"JsiVkUItwWCGyOLQEtnRpEhbXyZS9jNSzcMtycn68aBaS")

val ssc = new StreamingContext(sc, Seconds(5))
val twitterStream = TwitterUtils.createStream(ssc, None)
val aggStream = twitterStream
.flatMap(x => x.getText.split(" "))
.filter(_.startsWith("#"))
.map(x => (x, 1))
.reduceByKeyAndWindow(_ + _, _ - _, Seconds(15), Seconds(10), 5)

ssc.checkpoint("checkpoints")
aggStream.checkpoint(Seconds(10))
aggStream.foreachRDD((rdd, time) => {
val count = rdd.count()
if (count > 0) {
val dt = new Date(time.milliseconds)
println(s"\n\n$dt rddCount = $count\nTop 5 words\n")
val top5 = rdd.sortBy(_._2, ascending = false).take(5)
top5.foreach {
case (word, count) =>
println(s"[$word] - $count")
}}})
ssc.start
//wait 60 seconds
ss.stop(false)
The output is shown on the console every 15 seconds, looking like the following:
Mon May 29 02:44:50 EDT 2017 rddCount = 1453
Top 5 words
[#RT] - 64
[#de] - 24
[#a] - 15
[#to] - 15
[#the] - 13
Mon May 29 02:45:00 EDT 2017 rddCount = 3312
Top 5 words
[#RT] - 161
[#df] - 47
[#a] - 35
[#the] - 29
[#to] – 29

有状态/无状态转换

Spark Streaming 使用数据流的概念,数据流基本上是关系数据库的微数据块。我们还看到了一些可以应用于数据流的转换。数据流转换可以分为两种类型:无状态转换和有状态转换。

在无状态转换中,每个数据微批次是否被处理不依赖于之前的数据批次,因此每个批次都完全独立于之前的数据批次。

在有状态转换中,每个数据微批次是否被处理部分或全部取决于之前的数据批次,因此每个批次都会考虑之前发生了什么,并在处理时使用这些信息。

无状态转换

通过对数据流中的每个 RDD 应用转换,一个数据流被转换成另一个数据流,如下图所示。例如map()flatMap()union()join()reduceBykey()

有状态转换

有状态转换应用于数据流,但是它们依赖于先前的处理状态。例如countByValueAndWindow()reduceByKeyAndWindow()mapWithState()updateStateByKey()。根据定义,所有基于窗口的转换都是有状态的;我们必须跟踪窗口长度和数据流的滑动间隔。

检查点

由于预计实时流应用将长时间运行,同时保持对故障的弹性,Spark 流实现了一种称为检查点的机制。该机制跟踪足够的信息,以便能够从任何故障中恢复。有两种类型的数据检查点:

  • 元数据检查点
  • 数据检查点

通过调用StreamingContext上的checkpoint()启用检查点:

def checkpoint(directory: String)

这将指定存储检查点数据的目录。请注意,这必须是容错的文件系统,例如 HDFS。

一旦设置了检查点的目录,就可以根据时间间隔将任何数据流检查到其中。再来看一下推特的例子,每 10 秒钟可以检查一次每个数据流:

val ssc = new StreamingContext(sc, Seconds(5))
val twitterStream = TwitterUtils.createStream(ssc, None)
val wordStream = twitterStream.flatMap(x => x.getText().split(" "))
val aggStream = twitterStream
.flatMap(x => x.getText.split(" ")).filter(_.startsWith("#"))
.map(x => (x, 1))
.reduceByKeyAndWindow(_ + _, _ - _, Seconds(15), Seconds(10), 5)
ssc.checkpoint("checkpoints")
aggStream.checkpoint(Seconds(10))
wordStream.checkpoint(Seconds(10))

元数据检查点

元数据检查点保存定义流操作的信息,这些流操作由到达 HDFS 的 DAG 表示。这可用于在发生故障时恢复 DAG,从而允许应用重新启动。然后,驱动程序重新启动并从 HDFS 读取所有元数据,重建 DAG,同时恢复崩溃前的操作状态。

数据检查点

数据检查点将 RDDs 保存到 HDFS。在流式应用出现故障的情况下,可以恢复 RDDs,并且处理可以从停止的地方继续。在数据检查点的情况下,恢复不仅是好的,而且当 rdd 由于缓存清理或执行器丢失而丢失时,它也有所帮助。现在,任何生成的 rdd 都不需要等待 DAG 谱系中的父 rdd 被重新处理。

对于具有以下要求的任何应用,都必须启用检查点:

  • 应用状态转换。如果使用updateStateBykey()reduceByKeyAndWindow()(以及它们的反函数),那么必须给出检查点目录,以便发生 RDD 检查点。
  • 运行应用时从驱动程序故障中恢复。元数据检查点有助于恢复进度信息。

如果没有状态转换,那么应用可以在没有启用检查点的情况下运行。

There could be a loss of received, but not yet processed, data.

需要注意的是,RDD 检查点意味着将每个 RDD 存储起来。这将增加具有 rdd 检查点的批次的处理时间。因此,必须设置和调整检查点间隔,以免影响性能,这在处理实时处理的预期时很重要。

微小的批处理大小(例如 1 秒)意味着检查点出现得过于频繁,这可能会降低操作吞吐量。相反,不经常检查点会导致任务大小增加,由于大量排队数据而导致处理延迟。

需要 RDD 检查点的状态转换的默认检查点间隔至少为 10 秒。一个好的设置是 5 到 10 个数据流滑动间隔的检查点间隔。

驱动程序故障恢复

借助StreamingContext.getOrCreate(),我们可以实现驾驶员故障恢复。如前所述,这将从已经存在的检查点初始化StreamingContext,或者创建一个新的检查点。

我们不会实现一个名为createStreamContext 0的函数,它会创建一个StreamingContext并设置 DStreams 来解释推文并生成前五个最常用的哈希表,每 15 秒使用一个窗口。我们将调用getOrCreate()而不是调用createStreamContext()然后调用ssc.start(),这样如果存在检查点,那么StreamingContext将从checkpoint Directory中的数据重新创建。如果没有这样的目录,或者应用是第一次运行,那么createStreamContext()将被调用:

val ssc = StreamingContext.getOrCreate(checkpointDirectory,
createStreamContext _)

下面的代码显示了如何定义函数,以及如何调用getOrCreate():


val checkpointDirectory = "checkpoints"
//Creating and setting up a new StreamingContext
def createStreamContext(): StreamingContext = {
val ssc = new StreamingContext(sc, Seconds(5))
val twitterStream = TwitterUtils.createStream(ssc, None)
val wordStream = twitterStream.flatMap(x => x.getText().split(" "))
val aggStream = twitterStream
.flatMap(x => x.getText.split(" ")).filter(_.startsWith("#"))
.map(x => (x, 1))
.reduceByKeyAndWindow(_ + _, _ - _, Seconds(15), Seconds(10), 5)
ssc.checkpoint(checkpointDirectory)
aggStream.checkpoint(Seconds(10))
wordStream.checkpoint(Seconds(10))
aggStream.foreachRDD((rdd, time) => {
val count = rdd.count()
if (count > 0) {
val dt = new Date(time.milliseconds)
println(s"\n\n$dt rddCount = $count\nTop 5 words\n")
val top10 = rdd.sortBy(_._2, ascending = false).take(5)
top10.foreach {
case (word, count) =>
println(s"[$word] - $count")
}
}
})
ssc
}
//Retrieve StreamingContext from checkpoint data or create a new one
val ssc = StreamingContext.getOrCreate(checkpointDirectory,
createStreamContext _)

与流媒体平台的互操作性(Apache Kafka)

Spark Streaming 与目前最流行的消息平台 Apache Kafka 集成良好。这种集成有几种方法,随着时间的推移,这种机制在性能和可靠性方面有所改进。

主要有三种方法:

  • 基于接收者的方法
  • 直接流方法
  • 结构化流

基于接收器

Spark 和卡夫卡的第一次融合是基于接收者的融合。在基于接收者的方法中,驱动程序启动执行器上的接收者,然后执行器使用来自卡夫卡代理的高级应用编程接口提取数据。由于事件是从卡夫卡经纪人那里提取的,接收者将偏移量更新到动物园管理员中,这也是卡夫卡集群所使用的。这里重要的方面是使用提前写日志 ( WAL ,这是接收器从卡夫卡收集数据时写入的内容。如果出现问题,执行者和接收者必须重新启动或丢失,可以利用 WAL 恢复事件并处理它们。因此,这种基于日志的设计有助于提供耐用性和一致性。

事件的输入数据流由每个接收者从卡夫卡主题创建,同时它向动物园管理员查询卡夫卡主题、经纪人和偏移量。登录的、运行的接收器使并行变得复杂,因为随着应用的扩展,工作负载将无法正确分配。另一个问题是对 HDFS 的依赖,以及写操作重复。还需要关于一次处理范例的可靠性,因为只有幂等方法会起作用。事务方法在基于接收者的方法中不起作用,因为没有办法从 Zookeeper 或 HDFS 位置访问偏移范围。基于接收者的方法也更通用,因为它适用于任何消息传递系统,如下图所示:

可以通过调用createStream()应用编程接口来创建基于接收器的流:

def createStream(
ssc: StreamingContext,
// StreamingContext object
zkQuorum: String,
//Zookeeper quorum (hostname:port,hostname:port,..)
groupId: String,
//Group id for the consumer
topics: Map[String, Int],
//Map of (topic_name to numPartitions) to consume
storageLevel: StorageLevel = StorageLevel.MEMORY_AND_DISK_SER_2
//Storage level to use for storing the received objects
(default: StorageLevel.MEMORY_AND_DISK_SER_2)
): ReceiverInputDStream[(String, String)]
//DStream of (Kafka message key,
Kafka message value)

创建基于接收者的流,从卡夫卡经纪人那里提取消息的一个例子如下:

val topicMap = topics.split(",").map((_, numThreads.toInt)).toMap
val lines = KafkaUtils.createStream(ssc, zkQuorum, group,
topicMap).map(_._2)

直接流

可以创建一个不使用接收器直接从 Kafka 代理获取消息的输入流,这样可以确保每个 Kafka 消息只包含在转换中一次,如下图所示:

直接流的属性如下:

  • 无接收器:不使用接收器,直接查询卡夫卡。
  • 偏移量:不使用 Zookeeper 存储偏移量,任何消耗的偏移量都由流本身跟踪。每个批次中使用的偏移量可以从生成的 rdd 中访问。
  • 故障恢复:必须启用流上下文中的检查点,才能从驱动程序故障中恢复。
  • 端到端示意图:流保证所有的记录都被接收和转换一次,但不保证转换后的数据被输出一次。

可以使用卡夫卡实用程序createDirectStream()应用编程接口如下创建一个直接流:

def createDirectStream[
K: ClassTag,
//K type of Kafka message key
V: ClassTag,
//V type of Kafka message value
KD <: Decoder[K]: ClassTag,
//KD type of Kafka message key decoder
VD <: Decoder[V]: ClassTag,
//VD type of Kafka message value decoder
R: ClassTag
//R type returned by messageHandler
](
ssc: StreamingContext,
//StreamingContext object
kafkaParams: Map[String, String],
/*
kafkaParams Kafka <a
href="http://kafka.apache.org/documentation.html#configuration">
configuration parameters</a>. Requires "metadata.broker.list" or
"bootstrap.servers"
to be set with Kafka broker(s) (NOT Zookeeper servers) specified in
host1:port1,host2:port2 form.
*/
fromOffsets: Map[TopicAndPartition, Long],
//fromOffsets Per-
topic/partition Kafka offsets defining the (inclusive) starting point of
the stream
messageHandler: MessageAndMetadata[K, V] => R
//messageHandler Function
for translating each message and metadata into the desired type
): InputDStream[R]
//DStream of R

下面是一个直接流的例子,它从卡夫卡的主题中提取数据来创建数据流:

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

直接流应用编程接口只能与卡夫卡一起使用,所以它不是通用的。

结构化流

结构化流是 Apache Spark 2.0+的新版本,目前仍处于开发的 alpha 阶段。在下一节中,将详细介绍如何使用结构化流,并举例说明。也可以参考https://spark . Apache . org/docs/latest/Structured-Streaming-kafka-integration . html了解更多关于结构化流中 Kafka 集成的信息。

下面的代码片段显示了如何在结构化流中使用卡夫卡源流的示例:

val ds1 = spark
.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "host1:port1,host2:port2")
.option("subscribe", "topic1")
.load()
ds1.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
.as[(String, String)]

以下是如何使用卡夫卡源而不是源流的示例(如果您想采用批处理分析方法):

val ds1 = spark
.read
.format("kafka")
.option("kafka.bootstrap.servers", "host1:port1,host2:port2")
.option("subscribe", "topic1")
.load()
ds1.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
.as[(String, String)]

深入了解结构化流

结构化流是一个容错、可扩展的流处理引擎,构建在 Spark SQL 引擎之上。但是,结构化流允许您在接收的数据中指定事件时间,以便自动处理任何延迟的数据。需要注意的一点是,在 Spark 2.1 中,结构化流仍处于其阿尔法阶段,并且 API 被标记为实验性的。详情可参考https://spark . Apache . org/docs/latest/structured-streaming-programming-guide . html

结构化流背后的驱动思想是将数据流视为不断被添加的无界表。然后,计算和 SQL 查询可以应用于该表,这通常可以通过批处理数据来完成。例如,Spark SQL 查询将处理无界表。随着数据流随时间的变化,将处理越来越多的数据来生成结果表。该表可以写入外部接收器,称为输出

我们现在来看一个通过监听本地主机端口9999的输入来创建结构化流查询的例子。在 Linux 或 macOS 上,很容易在端口9999上启动服务器:

nc -lk 9999

下面是一个通过调用 SparkSession 的readStream应用编程接口创建inputStream的例子,然后从行中提取这些单词。然后,对单词进行分组,并对出现的单词进行计数,最后将结果写入输出流:

//Creating stream reading from localhost 999
val inputLines = spark.readStream
.format("socket")
.option("host", "localhost")
.option("port", 9999)
.load()
inputLines: org.apache.spark.sql.DataFrame = [value: string]
// Splitting the inputLines into words
val words = inputLines.as[String].flatMap(_.split(" "))
words: org.apache.spark.sql.Dataset[String] = [value: string]
// Generating running word count
val wordCounts = words.groupBy("value").count()
wordCounts: org.apache.spark.sql.DataFrame = [value: string, count: bigint]
val query = wordCounts.writeStream
.outputMode("complete")
.format("console")

query:
org.apache.spark.sql.streaming.DataStreamWriter[org.apache.spark.sql.Row] =
org.apache.spark.sql.streaming.DataStreamWriter@4823f4d0
query.start()

只要在终端中键入单词,查询就会更新,并通过在控制台上打印来继续生成结果:

scala> -------------------------------------------
Batch: 0
-------------------------------------------
+-----+-----+
|value|count|
+-----+-----+
| dog| 1|
+-----+-----+
-------------------------------------------
Batch: 1
-------------------------------------------
+-----+-----+
|value|count|
+-----+-----+
| dog| 1|
| cat| 1|
+-----+-----+
scala> -------------------------------------------
Batch: 2
-------------------------------------------
+-----+-----+
|value|count|
+-----+-----+
| dog| 2|
| cat| 1|
+-----+-----+

处理事件时间和延迟日期

事件时间是数据内部的时间。Spark Streaming 过去将时间定义为用于数据流目的的接收时间,但是对于许多需要事件时间的应用来说,这是不够的。例如,如果您要求一个标签每分钟在推文中出现的次数,那么您将需要数据生成的时间,而不是 Spark 收到事件的时间。

以下是前面结构化流示例的扩展,在服务器端口9999上侦听。Timestamp现在作为输入数据的一部分被启用,所以现在,我们可以在无界表上执行窗口操作:

import java.sql.Timestamp
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions._
// Creating DataFrame that represent the stream of input lines from connection
to host:port
val inputLines = spark.readStream
.format("socket")
.option("host", "localhost")
.option("port", 9999)
.option("includeTimestamp", true)
.load()
// Splitting the lines into words, retaining timestamps
val words = inputLines.as[(String, Timestamp)].flatMap(line =>
line._1.split(" ").map(word => (word, line._2))
).toDF("word", "timestamp")
// Grouping the data by window and word and computing the count of each
val windowedCounts = words.withWatermark("timestamp", "10 seconds")
.groupBy(
window($"timestamp", "10 seconds", "10 seconds"), $"word"
).count().orderBy("window")
// Begin executing the query which will print the windowed word counts to the
console
val query = windowedCounts.writeStream.outputMode("complete")
.format("console")
.option("truncate", "false")

query.start()
query.awaitTermination()

容错语义

一次性范例在使用外部数据库/存储来维护偏移量的传统流中是复杂的。结构化流仍在变化,在它被广泛使用之前,有几个挑战需要克服。

摘要

在这一章的过程中,涵盖了流处理系统、Spark Streaming、Apache Spark 中的数据流、数据流、DAG 和数据流谱系以及转换和动作的概念。此外,还介绍了窗口流处理和使用 Spark Streaming 处理推特推文的实际例子。然后,针对卡夫卡,介绍了基于接收者的和直接流的数据消费方式,最后,介绍了最新发展的结构化流技术。目前,它旨在解决当前的许多挑战,例如容错、在流中使用一次语义,以及简化与消息传递系统(如卡夫卡)的集成,同时保持与其他输入流类型集成的灵活性和可扩展性。

在下一章中,我们将探讨 Apache Flink,它是 Spark 作为计算平台的关键挑战者。

八、Apache Flink 批处理分析

本章将向读者介绍 Apache Flink,说明如何基于批处理模型使用 Flink 进行大数据分析。我们将研究数据集 API,它提供了对大数据进行批量分析的简单易用的方法。

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

  • Apache 弗林克简介
  • 安装 Flink
  • 使用 Scala 外壳
  • 使用 Flink 集群用户界面
  • 使用 Flink 进行批量分析

Apache 弗林克简介

Flink 是一个用于分布式流处理的开源框架,具有以下特性:

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

以下是官方文档的截图,显示了如何使用 Apache Flink:

查看 Apache Flink 框架的另一种方式如下图所示:

所有 Flink 程序都是延迟执行的,当程序的主方法被执行时,数据加载和转换不会直接发生。相反,每个操作都被创建并添加到程序的计划中。当执行被执行环境上的execute()调用显式触发时,操作实际上被执行。程序是在本地执行还是在集群上执行取决于执行环境的类型。惰性评估允许您构建复杂的程序,Flink 将其作为一个整体规划的单元来执行。

Flink 程序看起来像是转换数据集合的常规程序。每个程序都由相同的基本部分组成:

  1. 获得执行环境
  2. 加载初始数据
  3. 指定此数据的转换、聚合和连接
  4. 指定将计算结果放在哪里
  5. 触发程序执行

无界数据集的连续处理

在详细介绍 Apache Flink 之前,让我们从更高的层次来回顾一下您在处理数据时可能遇到的数据集类型,以及您可以选择处理的执行模型类型。这两种想法经常被混为一谈;了解是什么让他们与众不同将是有益的。

首先,有两种类型的数据集:

  • 无界:连续添加到的无限数据集
  • 有界的:有限的、不变的数据集

许多传统上被认为是有界或批处理的真实数据集实际上是无界数据集。无论数据是存储在 HDFS 的一系列目录中,还是存储在基于日志的系统中,例如 Apache Kafka ,都是如此。

无界数据集的一些示例包括但不限于以下内容:

  • 终端用户与移动或网络应用交互
  • 提供测量的物理传感器
  • 金融市场
  • 机器日志数据

其次,就像两种类型的数据集一样,也有两种类型的执行模型:

  • :只要产生数据,就连续执行的处理
  • 批处理:在有限的时间内执行并运行至完全的处理,完成后释放计算资源

用任一类型的执行模型来处理任一类型的数据集是可能的,尽管不一定是最佳的。例如,尽管存在开窗、状态管理和无序数据的潜在问题,批处理执行长期以来一直应用于无界数据集。

Flink 依赖于流执行模型,这是处理无界数据集的直观方式:流执行是连续处理连续产生的数据。数据集类型和执行模型类型之间的一致性在准确性和性能方面提供了许多优势。

流动模型和有界数据集

在 Apache Flink 中,您可以使用数据流应用编程接口来处理无界数据,也可以使用数据集应用编程接口来处理有界数据。Flink 使有界和无界数据集之间的关系变得非常自然。有界数据集可以简单地视为无界数据集的特例,因此可以将所有相同的概念应用于两种类型的数据集。

有界数据集在 Flink 内部作为有限流处理,Flink 管理有界数据集和无界数据集的方式只有一些细微的区别。因此,可以使用 Flink 来处理有界和无界数据,两个 API 都运行在同一个分布式流执行引擎上:一个简单而强大的架构。

安装 Flink

在本节中,我们将下载并安装 Apache Flink。

Flink 在 Linux、OS X 和 Windows 上运行。为了能够运行 Flink,唯一的要求是有一个运行良好的 Java 7.x(或更高版本)安装。如果您正在使用 windows,请查看https://ci . Apache . org/project/flink/Flink-docs-release-1.4/start/Flink _ on _ Windows . html的《Windows 上的 Flink》指南,其中介绍了如何在 Windows 上运行 Flink 进行本地设置。

您可以通过发出以下命令来检查您的 Java 版本:

java -version

如果您有 Java 8,输出将如下所示:

java version "1.8.0_111"
Java(TM) SE Runtime Environment (build 1.8.0_111-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.111-b14, mixed mode)

下载 Flink

https://flink.apache.org/downloads.html下载与您的平台相关的 Apache Flink 二进制文件:

Figure: Screenshot showing Apache Flink libraries

点击下载下载 Hadoop 版本。您将在浏览器中看到下载页面,如下图所示:

Figure: Screenshot showing Hadoop version to be downloaded

在这种情况下,我下载了flink-1.4.2-bin-hadoop28-scala_2.11.tgz,这是可用的最新版本。

下载后,提取二进制文件。在苹果电脑或 Linux 机器上,您可以使用tar命令:

安装 Flink

首先,将目录更改为提取 Apache Flink 的位置:

cd flink-1.4.2

您将看到以下内容:

启动本地 Flink 集群

只需在bin文件夹中使用以下脚本,即可启动本地集群:

./bin/start-local.sh

运行脚本后,您应该会看到集群已经启动。

http://localhost:8081检查作业管理器的网络前端,确保一切正常运行。网络前端应该报告一个可用的任务管理器实例:

您也可以通过检查logs目录中的日志文件来验证系统是否正在运行:

tail log/flink-*-jobmanager-*.log

要使用 Scala 外壳,请输入以下代码:

./bin/start-scala-shell.sh remote localhost 6123

要加载数据,请输入以下代码:

val dataSet = benv.readTextFile("OnlineRetail.csv")
dataSet.count()

您可以使用以下代码打印数据集的前五行。结果显示在代码后面的屏幕截图中:

dataSet
.first(5)
.print()

您可以使用map()执行简单的转换:

dataSet
.map(x => x.split(",")(2))
.first(5)
.print()

dataSet
.flatMap(x => x.split(","))
.map(x=> (x,1))
.groupBy(0)
.sum(1)
.first(10)
.print()

使用 Flink 集群用户界面

使用 Flink 集群用户界面,您可以了解和监控集群中运行的内容,并深入挖掘各种作业和任务。您可以监控作业状态、取消作业或调试作业的任何问题。通过查看日志,您还可以诊断代码的问题,并修复它们。

以下是已完成作业的列表:

您可以深入查看任何特定作业,以查看有关作业执行的更多详细信息:

Figure: Drilling down a particular job to see job's execution

您可以查看工作的时间线以获得更多详细信息:

Figure: Screenshot to see Timeline of a job

下面的屏幕截图显示了任务管理器选项卡,显示了所有的任务管理器。这有助于您了解任务经理的数量和状态:

也可以查看日志,如下图截图所示:

“指标”选项卡为您提供了内存和 CPU 资源的详细信息:

Figure: Screenshot showing details of the memory and CPU resources in Metrics tab

您也可以提交 JARs 作为作业,而不是在 Scala shell 中编写所有内容,如前所述:

批量分析

Apache Flink 中的批处理分析与流分析非常相似,Flink 使用相同的应用编程接口处理两种类型的分析。这提供了很大的灵活性,并允许在不同类型的分析中重用代码。

在这一节中,我们将看看我们正在使用的样本数据的一些分析工作。我们还将装载cities.csvtemperature.csv进行更多的连接操作。

正在读取文件

Flink 附带了几种内置格式,可以从常见的文件格式创建数据集。它们中的许多在执行环境上都有快捷方式。

基于文件

可以使用下面列出的 API 读取基于文件的源:

  • readTextFile(path) / TextInputFormat:逐行读取文件并以字符串形式返回。
  • readTextFileWithValue(path) / TextValueInputFormat:逐行读取文件并将其作为StringValues返回。StringValues是可变字符串。
  • readCsvFile(path) / CsvInputFormat:解析逗号(或其他字符)分隔字段的文件。返回元组、事例类对象或 POJOs 的数据集。支持基本的 Java 类型及其对应的Value字段类型。
  • readFileOfPrimitives(path, delimiter) / PrimitiveInputFormat:使用给定的分隔符解析新行(或另一个字符序列)定界的基本数据类型的文件,如StringInteger
  • readHadoopFile(FileInputFormat, Key, Value, path) / FileInputFormat:创建一个JobConf,用指定的FileInputFormatKey类和Value类从指定的路径读取文件,并作为Tuple2<Key, Value>.返回
  • readSequenceFile(Key, Value, path) / SequenceFileInputFormat:创建一个JobConf,从指定路径读取文件,类型为SequenceFileInputFormatKey类、Value类,并将其作为Tuple2<Key, Value>返回。

基于集合

基于集合(数据结构,如列表、数组等)的源可以使用下面列出的 API 读取:

  • fromCollection(Seq):从Seq创建数据集。集合中的所有元素必须属于同一类型。
  • fromCollection(Iterator):从Iterator创建数据集。该类指定迭代器返回的元素的数据类型。
  • fromElements(elements: _*):根据给定的对象序列创建数据集。所有对象必须属于同一类型。
  • fromParallelCollection(SplittableIterator):从迭代器并行创建数据集。该类指定迭代器返回的元素的数据类型。
  • generateSequence(from, to):并行生成给定区间内的数字序列。

一般的

可以使用下面列出的应用编程接口读取通用(定制)源:

  • readFile(inputFormat, path) / FileInputFormat:接受文件输入格式
  • createInput(inputFormat) / InputFormat:接受通用输入格式

我们将看看其中一个应用编程接口readTextFile()。使用此 API 读取文件会导致将文件(本地文本文件、hdfs 文件、Amazon s3 文件等)加载到 DataSet 中。该数据集包含正在加载的数据的分区位置,因此能够支持数据的 TBs。

让我们加载如下代码所示的示例OnlineRetail.csv:

val dataSet = benv.readTextFile("OnlineRetail.csv")
dataSet.first(10).print()

这将在加载后打印数据集的内容,如以下代码所示:

InvoiceNo,StockCode,Description,Quantity,InvoiceDate,UnitPrice,CustomerID,Country
 536365,85123A,WHITE HANGING HEART T-LIGHT HOLDER,6,12/1/10 8:26,2.55,17850,United Kingdom
 536365,71053,WHITE METAL LANTERN,6,12/1/10 8:26,3.39,17850,United Kingdom
 536365,84406B,CREAM CUPID HEARTS COAT HANGER,8,12/1/10 8:26,2.75,17850,United Kingdom
 536365,84029G,KNITTED UNION FLAG HOT WATER BOTTLE,6,12/1/10 8:26,3.39,17850,United Kingdom
 536365,84029E,RED WOOLLY HOTTIE WHITE HEART.,6,12/1/10 8:26,3.39,17850,United Kingdom
 536365,22752,SET 7 BABUSHKA NESTING BOXES,2,12/1/10 8:26,7.65,17850,United Kingdom
 536365,21730,GLASS STAR FROSTED T-LIGHT HOLDER,6,12/1/10 8:26,4.25,17850,United Kingdom
 536366,22633,HAND WARMER UNION JACK,6,12/1/10 8:28,1.85,17850,United Kingdom
 536366,22632,HAND WARMER RED POLKA DOT,6,12/1/10 8:28,1.85,17850,United Kingdom

如果您注意到前面的示例,您会发现第一行实际上是标题行,因此在任何分析中都没有用。您可以使用filter()功能过滤掉一行或多行。

以下是文件的加载和第一行的删除,并返回一个数据集:

val dataSet =benv
    .readTextFile("OnlineRetail.csv")
    .filter(!_.startsWith("InvoiceNo"))
dataSet.first(10).print()

这将在加载后打印数据集的内容,如以下代码所示:

 536365,85123A,WHITE HANGING HEART T-LIGHT HOLDER,6,12/1/10 8:26,2.55,17850,United Kingdom
 536365,71053,WHITE METAL LANTERN,6,12/1/10 8:26,3.39,17850,United Kingdom
 536365,84406B,CREAM CUPID HEARTS COAT HANGER,8,12/1/10 8:26,2.75,17850,United Kingdom
 536365,84029G,KNITTED UNION FLAG HOT WATER BOTTLE,6,12/1/10 8:26,3.39,17850,United Kingdom
 536365,84029E,RED WOOLLY HOTTIE WHITE HEART.,6,12/1/10 8:26,3.39,17850,United Kingdom
 536365,22752,SET 7 BABUSHKA NESTING BOXES,2,12/1/10 8:26,7.65,17850,United Kingdom
 536365,21730,GLASS STAR FROSTED T-LIGHT HOLDER,6,12/1/10 8:26,4.25,17850,United Kingdom
 536366,22633,HAND WARMER UNION JACK,6,12/1/10 8:28,1.85,17850,United Kingdom
 536366,22632,HAND WARMER RED POLKA DOT,6,12/1/10 8:28,1.85,17850,United Kingdom
 536367,84879,ASSORTED COLOUR BIRD ORNAMENT,32,12/1/10 8:34,1.69,13047,United Kingdom

显然这次我们没有看到标题行:

InvoiceNo,StockCode,Description,Quantity,InvoiceDate,UnitPrice,CustomerID,Country

我们现在将研究可以在加载的数据集上执行的更多操作。

转换

转换通过将转换逻辑应用于原始数据集的每一行,将数据集更改为新数据集。例如,如果我们想从输入中删除第一个标题行,那么我们可以使用filter()操作来完成。

下面是两个filter()操作的应用,首先删除标题,然后确保每行有正确的列数,在这种情况下正好是 8:

val dataSet = benv.readTextFile("OnlineRetail.csv")
    .filter(!_.startsWith("InvoiceNo"))
    .filter(_.split(",").length == 8)

dataSet.map(x => x.split(",")(2))
    .first(10).print()

这将在加载后打印数据集的内容,如以下代码所示:

 WHITE HANGING HEART T-LIGHT HOLDER
 WHITE METAL LANTERN
 CREAM CUPID HEARTS COAT HANGER
 KNITTED UNION FLAG HOT WATER BOTTLE
 RED WOOLLY HOTTIE WHITE HEART.
 SET 7 BABUSHKA NESTING BOXES
 GLASS STAR FROSTED T-LIGHT HOLDER
 HAND WARMER UNION JACK
 HAND WARMER RED POLKA DOT

同样,您可以打印数据集中的数量列:

dataSet.map(x => x.split(",")(3))
    .first(10).print()

这将在加载后打印数据集的内容,如以下代码所示:

 6
 6
 8
 6
 6
 2
 6
 6
 6

同样,您可以从数据集中打印描述和数量列的元组:

dataSet.map(x => (x.split(",")(2), x.split(",")(3).toInt))
    .first(10).print()

这将在加载后打印数据集的内容,如以下代码所示:

 (WHITE HANGING HEART T-LIGHT HOLDER,6)
 (WHITE METAL LANTERN,6)
 (CREAM CUPID HEARTS COAT HANGER,8)
 (KNITTED UNION FLAG HOT WATER BOTTLE,6)
 (RED WOOLLY HOTTIE WHITE HEART.,6)
 (SET 7 BABUSHKA NESTING BOXES,2)
 (GLASS STAR FROSTED T-LIGHT HOLDER,6)
 (HAND WARMER UNION JACK,6)
 (HAND WARMER RED POLKA DOT,6)

本节简要概述了可用的转换,可在https://ci . Apache . org/project/flink/flink-docs-release-1.4/dev/batch/dataset _ transformations . html中找到:

转换 描述
地图 获取一个元素并生成一个元素。
data.map { x => x.toInt }

|
| 平面地图 | 获取一个元素并生成零个、一个或多个元素。

data.flatMap { str => str.split(" ") }

|
| 地图分区 | 在单个函数调用中转换并行分区。该函数将分区作为迭代器,可以产生任意数量的结果值。每个分区中的元素数量取决于并行度和以前的操作。

data.mapPartition { in => in map { (_, 1) } }

|
| 过滤器 | 为每个元素计算一个布尔函数,并保留那些该函数返回 true 的元素。
重要:系统假设函数不修改应用谓词的元素。违反这个假设会导致不正确的结果。

data.filter { _ > 1000 }

|
| 减少 | 通过将两个元素重复组合成一个元素,将一组元素组合成一个元素。Reduce 可以应用于完整数据集,也可以应用于分组数据集。

data.reduce { _ + _ }

|
| ReduceGroup | 将一组元素组合成一个或多个元素。reduceGroup可以应用于完整数据集,也可以应用于分组数据集。

data.reduceGroup { elements => elements.sum }

|
| 总计 | 将一组值聚合成一个值。聚合函数可以被认为是内置的 reduce 函数。聚合可以应用于完整数据集,也可以应用于分组数据集。

val input: DataSet[(Int, String, Double)] = // [...]
val output: DataSet[(Int, String, Double)] = input.aggregate(SUM, 0).aggregate(MIN, 2)

您还可以对最小值、最大值和总和聚合使用简写语法。

val input: DataSet[(Int, String, Double)] = // [...]
val output: DataSet[(Int, String, Double)] = input.sum(0).min(2)

|
| 明显的 | 返回数据集的不同元素。它从输入数据集中删除与元素的所有字段或字段子集相关的重复条目。

data.distinct()

|
| 加入 | 通过创建键上相等的所有元素对来连接两个数据集。可选地使用JoinFunction将一对元素变成单个元素,或者使用FlatJoinFunction将一对元素变成任意多个(包括无)元素。

// In this case tuple fields are used as keys. "0" is the join field on the first tuple
// "1" is the join field on the second tuple.
val result = input1.join(input2).where(0).equalTo(1)

您可以通过连接提示指定运行时执行连接的方式。提示描述了连接是通过分区还是广播发生的,以及它是使用基于排序的算法还是基于哈希的算法。有关可能的提示列表和示例,请参考位于[的转换指南。如果没有指定提示,系统将尝试估计输入大小,并根据这些估计选择最佳策略。

// This executes a join by broadcasting the first data set
// using a hash table for the broadcast data
val result = input1.join(input2, JoinHint.BROADCAST_HASH_FIRST)
                   .where(0).equalTo(1)

请注意,连接转换仅适用于等连接。其他连接类型需要使用外部连接或 CoGroup 来表示。](https://ci.apache.org/projects/flink/flink-docs-release-1.4/dev/batch/dataset_transformations.html#join-algorithm-hints) |
| 外部连接 | 对两个数据集执行左、右或完全外部联接。外部联接类似于常规(内部)联接,创建所有键上相等的元素对。此外,如果在另一侧找不到匹配的键,则保留外侧的记录(满时为左、右或两者)。匹配的元素对(或者一个元素和另一个输入的空值)被赋予一个JoinFunction以将该元素对转变为单个元素,或者赋予一个FlatJoinFunction以将该元素对转变为任意多个(包括无)元素。

val joined = left.leftOuterJoin(right).where(0).equalTo(1) {
   (left, right) =>
     val a = if (left == null) "none" else left._1
     (a, right)
  }

|
| 你有吗 | 缩减操作的二维变体。将一个或多个字段上的每个输入分组,然后加入这些组。每对组调用一次转换函数。参见https://ci . Apache . org/projects/flink/flink-docs-release-1.4/dev/API _ concepts . html # specification-keys的 keys 部分,了解如何定义 coGroup keys。

data1.coGroup(data2).where(0).equalTo(1)

|
| 十字架 | 构建两个输入的笛卡尔乘积(叉积),创建所有元素对。可选地使用CrossFunction将一对元素变成单个元素

val data1: DataSet[Int] = // [...]
val data2: DataSet[String] = // [...]
val result: DataSet[(Int, String)] = data1.cross(data2)

注意:Cross 可能是一个非常计算密集型的操作,甚至会对大型计算集群构成挑战!建议使用crossWithTiny()crossWithHuge()提示系统数据集大小。 |
| 联盟 | 产生两个数据集的并集。

data.union(data2)

|
| 为…修复平衡 | 均匀地重新平衡数据集的并行分区,以消除数据倾斜。只有类似地图的转换可以遵循重新平衡转换。

val data1: DataSet[Int] = // [...]
val result: DataSet[(Int, String)] = data1.rebalance().map(...)

|
| 哈希分区 | 对给定键上的数据集进行哈希分区。按键可以指定为位置按键、表达式按键和按键选择器功能。

val in: DataSet[(Int, String)] = // [...]
val result = in.partitionByHash(0).mapPartition { ... }

|
| 范围分区 | 范围-在给定的键上划分数据集。按键可以指定为位置按键、表达式按键和按键选择器功能。

val in: DataSet[(Int, String)] = // [...]
val result = in.partitionByRange(0).mapPartition { ... }

|
| 自定义分区 | 手动指定数据分区。
注意:此方法仅适用于单字段键。

val in: DataSet[(Int, String)] = // [...]
val result = in
  .partitionCustom(partitioner: Partitioner[K], key)

|
| 分类分区 | 以指定的顺序对指定字段上的数据集的所有分区进行本地排序。字段可以指定为元组位置或字段表达式。对多个字段的排序是通过链接sortPartition()调用来完成的。

val in: DataSet[(Int, String)] = // [...]
val result = in.sortPartition(1, Order.ASCENDING).mapPartition { ... }

|
| 第一个 n | 返回数据集的前 n 个(任意)元素。First-n 可以应用于常规数据集、分组数据集或分组排序数据集。分组关键字可以被指定为关键字选择器函数、元组位置或事例类字段。

val in: DataSet[(Int, String)] = // [...]
// regular data set
val result1 = in.first(3)
// grouped data set
val result2 = in.groupBy(0).first(3)
// grouped-sorted data set
val result3 = in.groupBy(0).sortGroup(1, Order.ASCENDING).first(3)

|

群组依据

groupBy操作有助于通过一些列聚合数据集的行。groupBy()获取用于汇总行的列索引。

按照Description的命令分组,打印前 10 条记录。

dataSet.map(x => (x.split(",")(2), x.split(",")(3).toInt))
    .groupBy(0)
    .first(10).print()

一旦加载,这将打印数据集的内容,如下所示:

 (WOODLAND DESIGN COTTON TOTE BAG,1)
 (WOODLAND DESIGN COTTON TOTE BAG,1)
 (WOODLAND DESIGN COTTON TOTE BAG,6)
 (WOODLAND DESIGN COTTON TOTE BAG,1)
 (WOODLAND DESIGN COTTON TOTE BAG,2)
 (WOODLAND DESIGN COTTON TOTE BAG,1)
 (WOODLAND DESIGN COTTON TOTE BAG,6)
 (WOODLAND DESIGN COTTON TOTE BAG,1)
 (WOODLAND DESIGN COTTON TOTE BAG,1)
 (WOODLAND DESIGN COTTON TOTE BAG,12)
 (WOODLAND PARTY BAG + STICKER SET,2)
 (WOODLAND PARTY BAG + STICKER SET,16)
 (WOODLAND PARTY BAG + STICKER SET,1)
 (WOODLAND PARTY BAG + STICKER SET,8)
 (WOODLAND PARTY BAG + STICKER SET,4)

groupBy()原料药定义如下:

/**
 * Groups a {@link Tuple} {@link DataSet} using field position keys.
 *
 * <p><b>Note: Field position keys only be specified for Tuple DataSets.</b>
 *
 * <p>The field position keys specify the fields of Tuples on which the DataSet is grouped.
 * This method returns an {@link UnsortedGrouping} on which one of the following grouping transformation
 * can be applied.
 * <ul>
 * <li>{@link UnsortedGrouping#sortGroup(int, org.apache.flink.api.common.operators.Order)} to get a {@link SortedGrouping}.
 * <li>{@link UnsortedGrouping#aggregate(Aggregations, int)} to apply an Aggregate transformation.
 * <li>{@link UnsortedGrouping#reduce(org.apache.flink.api.common.functions.ReduceFunction)} to apply a Reduce transformation.
 * <li>{@link UnsortedGrouping#reduceGroup(org.apache.flink.api.common.functions.GroupReduceFunction)} to apply a GroupReduce transformation.
 * </ul>
 *
 * @param fields One or more field positions on which the DataSet will be grouped.
 * @return A Grouping on which a transformation needs to be applied to obtain a transformed DataSet.
 *
 * @see Tuple
 * @see UnsortedGrouping
 * @see AggregateOperator
 * @see ReduceOperator
 * @see org.apache.flink.api.java.operators.GroupReduceOperator
 * @see DataSet
 */
 public UnsortedGrouping<T> groupBy(int... fields) {
 return new UnsortedGrouping<>(this, new Keys.ExpressionKeys<>(fields, getType()));
 }

聚合

在通过一些列应用groupBy()之后,聚合操作将逻辑应用于数据集的分组行。groupBy()获取用于聚合行的列的索引,聚合操作获取要聚合的列的索引。

按照Description对命令分组,并为每个描述添加Quantities,然后打印前 10 条记录。

dataSet.map(x => (x.split(",")(2), x.split(",")(3).toInt))
    .groupBy(0)
    .sum(1)
    .first(10).print()

这将在加载后打印数据集的内容,如以下代码所示:

 (,-2117)
 (*Boombox Ipod Classic,1)
 (*USB Office Mirror Ball,2)
 (10 COLOUR SPACEBOY PEN,823)
 (12 COLOURED PARTY BALLOONS,102)
 (12 DAISY PEGS IN WOOD BOX,62)
 (12 EGG HOUSE PAINTED WOOD,16)
 (12 IVORY ROSE PEG PLACE SETTINGS,80)
 (12 MESSAGE CARDS WITH ENVELOPES,238)
 (12 PENCIL SMALL TUBE WOODLAND,444)

按照Description对命令分组,为每个Description加上Quantities,然后打印最大Quantity的顶部Description:

dataSet.map(x => (x.split(",")(2), x.split(",")(3).toInt))
    .groupBy(0)
    .sum(1)
    .max(1)
    .first(10).print()

这将在加载后打印数据集的内容,如以下代码所示:

(reverse 21/5/10 adjustment,8189)

按照Description对命令分组,并为每个Description加上Quantities,然后打印最少的Quantity的顶部Description:

dataSet.map(x => (x.split(",")(2), x.split(",")(3).toInt))
    .groupBy(0)
    .sum(1)
    .min(1)
    .first(10).print()

这将在加载后打印数据集的内容,如以下代码所示:

(reverse 21/5/10 adjustment,-7005)

sum()原料药定义如下:

// private helper that allows to set a different call location name
 private AggregateOperator<T> aggregate(Aggregations agg, int field, String callLocationName) {
 return new AggregateOperator<T>(this, agg, field, callLocationName);
 }
/**
 * Syntactic sugar for aggregate (SUM, field).
 * @param field The index of the Tuple field on which the aggregation function is applied.
 * @return An AggregateOperator that represents the summed DataSet.
 *
 * @see org.apache.flink.api.java.operators.AggregateOperator
 */
 public AggregateOperator<T> sum (int field) {
 return this.aggregate (Aggregations.SUM, field, Utils.getCallLocationName());
 }

连接

val cities = benv.readTextFile("cities.csv")

Id,City
1,Boston
2,New York
3,Chicago
4,Philadelphia
5,San Francisco
7,Las Vegas
val temp = benv.readTextFile("temperatures.csv")

Date,Id,Temperature
2018-01-01,1,21
2018-01-01,2,22
2018-01-01,3,23
2018-01-01,4,24
2018-01-01,5,25
2018-01-01,6,22
2018-01-02,1,23
2018-01-02,2,24
2018-01-02,3,25

现在让我们将 cities.csv 和 temperatures.csv 加载到 DataSets 中,并删除标题。

 val cities = benv.readTextFile("cities.csv")
    .filter(!_.contains("Id,"))
val temp = benv.readTextFile("temperatures.csv")
    .filter(!_.contains("Id,"))

然后我们将数据集转换为元组数据集。第一个数据集即城市数据集将产生<cityId, cityName>元组。第二个数据集是温度数据集,将产生<cityId, temperature>元组。

val cities2 = cities.map(x => (x.split(",")(0), x.split(",")(1)))
cities2.first(10).print()
val temp2 = temp.map(x => (x.split(",")(1), x.split(",")(2)))
temp2.first(10).print()

内部连接

内部联接要求左右表具有相同的列。如果在左侧或右侧有重复或多个键的副本,连接将很快变成某种笛卡尔连接,比正确设计以最小化多个键花费更长的时间来完成:

现在,我们准备执行内部连接来连接元组的两个数据集,如以下代码所示:

cities2.join(temp2)
 .where(0)
 .equalTo(0)
 .first(10).print()

该作业的输出如下所示,显示了两个数据集中的元组,其中 cityID 存在于两个数据集中:

 ((1,Boston),(1,21))
 ((2,New York),(2,22))
 ((3,Chicago),(3,23))
 ((4,Philadelphia),(4,24))
 ((5,San Francisco),(5,25))
 ((1,Boston),(1,23))
 ((2,New York),(2,24))
 ((3,Chicago),(3,25))
 ((4,Philadelphia),(4,26))
 ((5,San Francisco),(5,18))

现在,如果我们应用聚合并将每个城市的温度相加,我们将得到每个城市的总温度。您可以通过编写如下代码所示的代码来实现这一点:

cities2
    .join(temp2)
    .where(0)
    .equalTo(0)
    .map(x=> (x._1._2, x._2._2.toInt))
    .groupBy(0)
    .sum(1)
    .first(10).print()

这显示了以下结果:

(Boston,111)
(Chicago,116)
(New York,119)
(Philadelphia,116)
(San Francisco,113)

该工作可以在 flink UI 中看到:

join()原料药定义如下:


 /**
 * Initiates a Join transformation.
 *
 * <p>A Join transformation joins the elements of two
 * {@link DataSet DataSets} on key equality and provides multiple ways to combine
 * joining elements into one DataSet.
 *
 * <p>This method returns a {@link JoinOperatorSets} on which one of the {@code where} methods
 * can be called to define the join key of the first joining (i.e., this) DataSet.
 *
 * @param other The other DataSet with which this DataSet is joined.
 * @return A JoinOperatorSets to continue the definition of the Join transformation.
 *
 * @see JoinOperatorSets
 * @see DataSet
 */
 public <R> JoinOperatorSets<T, R> join(DataSet<R> other) {
 return new JoinOperatorSets<>(this, other);
 }

左外连接

左外连接给出了左侧表中的所有行,以及两个表共有的行(内连接)。如果在几乎没有共同点的表上使用,会导致非常大的结果,从而降低性能:

现在,我们准备执行左外连接来连接元组的两个数据集,如以下代码所示:

cities2
    .leftOuterJoin(temp2)
    .where(0)
    .equalTo(0) {
        (x,y) => (x, if (y==null) (x._1,0) else (x._1, y._2.toInt))
    }
    .map(x=> (x._1._2, x._2._2.toInt))
    .groupBy(0)
    .sum(1)
    .first(10).print()

该作业的输出如下所示,显示了两个数据集中的元组,其中 cityID 存在于左侧或两个数据集中:

(Boston,111)
(Chicago,116)
(Las Vegas,0)   // Las vegas has no records in temperatures DataSet so is assigned 0
(New York,119)
(Philadelphia,116)
(San Francisco,113)

该工作可以在 flink UI 中看到:

leftOuterJoin()原料药定义如下:

/**
 * Initiates a Left Outer Join transformation.
 *
 * <p>An Outer Join transformation joins two elements of two
 * {@link DataSet DataSets} on key equality and provides multiple ways to combine
 * joining elements into one DataSet.
 *
 * <p>Elements of the <b>left</b> DataSet (i.e. {@code this}) that do not have a matching
 * element on the other side are joined with {@code null} and emitted to the
 * resulting DataSet.
 *
 * @param other The other DataSet with which this DataSet is joined.
 * @return A JoinOperatorSet to continue the definition of the Join transformation.
 *
 * @see org.apache.flink.api.java.operators.join.JoinOperatorSetsBase
 * @see DataSet
 */
 public <R> JoinOperatorSetsBase<T, R> leftOuterJoin(DataSet<R> other) {
 return new JoinOperatorSetsBase<>(this, other, JoinHint.OPTIMIZER_CHOOSES, JoinType.LEFT_OUTER);
 }

右外连接

右外连接给出了右侧表中的所有行以及左侧和右侧的公共行(内连接)。使用它可以获得右表中的所有行以及左表和右表中的行。如果不在左边,填写NULL。性能类似于本表前面提到的左外连接:

现在,我们准备执行右外连接来连接元组的两个数据集,如以下代码所示:

cities2
    .rightOuterJoin(temp2)
    .where(0)
    .equalTo(0) {
        (x,y) => (if (x==null) (y._1,"unknown") else (y._1, x._2), y)
    }
    .map(x=> (x._1._2, x._2._2.toInt))
    .groupBy(0)
    .sum(1)
    .first(10).print()

该作业的输出如下所示,显示了两个数据集中的元组,其中 cityID 存在于右侧或两个数据集中:

(Boston,111)
(Chicago,116)
(New York,119)
(Philadelphia,116)
(San Francisco,113)
(unknown,44) . // note that only right hand side temperatures DataSet has id 6 which is not in cities DataSet

该工作可以在 flink UI 中看到:

rightOuterJoin()原料药定义如下:

/**
 * Initiates a Right Outer Join transformation.
 *
 * <p>An Outer Join transformation joins two elements of two
 * {@link DataSet DataSets} on key equality and provides multiple ways to combine
 * joining elements into one DataSet.
 *
 * <p>Elements of the <b>right</b> DataSet (i.e. {@code other}) that do not have a matching
 * element on {@code this} side are joined with {@code null} and emitted to the
 * resulting DataSet.
 *
 * @param other The other DataSet with which this DataSet is joined.
 * @return A JoinOperatorSet to continue the definition of the Join transformation.
 *
 * @see org.apache.flink.api.java.operators.join.JoinOperatorSetsBase
 * @see DataSet
 */
 public <R> JoinOperatorSetsBase<T, R> rightOuterJoin(DataSet<R> other) {
 return new JoinOperatorSetsBase<>(this, other, JoinHint.OPTIMIZER_CHOOSES, JoinType.RIGHT_OUTER);
 }

完全外部连接

完全外部联接给出联接子句左侧和右侧表中的所有(匹配和不匹配)行。当我们想要保留两个表中的所有行时,我们使用完全外部连接。当其中一个表匹配时,完全外部联接返回所有行。如果在几乎没有共同点的表上使用,可能会导致非常大的结果,从而降低性能:

现在,我们准备执行完整的外部连接来连接元组的两个数据集,如以下代码所示:

cities2
    .fullOuterJoin(temp2)
    .where(0)
    .equalTo(0) {
        (x,y) => (if (x==null) (y._1,"unknown") else (x._1, x._2), 
                if (y==null) (x._1,0) else (y._1, y._2.toInt))
    }
    .map(x=> (x._1._2, x._2._2.toInt))
    .groupBy(0)
    .sum(1)
    .first(10).print()

该作业的输出如下所示,显示了两个数据集中的元组,其中 cityID 存在于一个或两个数据集中:

(Boston,111)
(Chicago,116)
(Las Vegas,0) // Las vegas has no records in temperatures DataSet so is assigned 0
(New York,119)
(Philadelphia,116)
(San Francisco,113)
(unknown,44) // note that only right hand side temperatures DataSet has id 6 which is not in cities DataSet

该工作可以在 flink UI 中看到:

fullOuterJoin()原料药定义如下:

/**
 * Initiates a Full Outer Join transformation.
 *
 * <p>An Outer Join transformation joins two elements of two
 * {@link DataSet DataSets} on key equality and provides multiple ways to combine
 * joining elements into one DataSet.
 *
 * <p>Elements of <b>both</b> DataSets that do not have a matching
 * element on the opposing side are joined with {@code null} and emitted to the
 * resulting DataSet.
 *
 * @param other The other DataSet with which this DataSet is joined.
 * @return A JoinOperatorSet to continue the definition of the Join transformation.
 *
 * @see org.apache.flink.api.java.operators.join.JoinOperatorSetsBase
 * @see DataSet
 */
 public <R> JoinOperatorSetsBase<T, R> fullOuterJoin(DataSet<R> other) {
 return new JoinOperatorSetsBase<>(this, other, JoinHint.OPTIMIZER_CHOOSES, JoinType.FULL_OUTER);
 }

写入文件

数据接收器使用数据集,并用于存储或返回它们。使用输出格式描述数据接收器操作。Flink 附带了各种内置的输出格式,这些格式封装在对数据集的操作之后:

  • writeAsText() / TextOutputFormat:将元素以字符串形式逐行写入。通过调用每个元素的toString()方法获得字符串。
  • writeAsCsv(...) / CsvOutputFormat:将元组写成逗号分隔的值文件。行和字段分隔符是可配置的。每个字段的值来自对象的toString()方法。
  • print() / printToErr():在标准输出/标准误差流上打印每个元素的toString()值。
  • write() / FileOutputFormat:自定义文件输出的方法和基类。支持自定义对象到字节的转换。
  • output() / OutputFormat:最通用的输出方法,用于非基于文件的数据接收器(例如将结果存储在数据库中)。

让我们使用writeAsText()将城市和温度的内部连接结果写入文件。

No output will be seen until you call benv.execute().

首先,为城市和温度的内部连接创建一个数据集:

val results = cities2
    .join(temp2)
    .where(0)
    .equalTo(0)
    .map(x=> (x._1._2, x._2._2.toInt))
    .groupBy(0)
    .sum(1)

然后在结果数据集上调用writeAsText(),在数据链上调用execute(),如下代码所示:

results.writeAsText("file:///Users/sridharalla/flink-1.4.2/results.txt").setParallelism(1)
benv.execute()

如果您打开刚刚创建的文件,您将看到连接操作的结果,如以下代码所示:

(Boston,111)
(Chicago,116)
(New York,119)
(Philadelphia,116)
(San Francisco,113)

该工作可以在 flink UI 中看到:

摘要

在本章中,我们讨论了 Apache Flink 以及如何使用 Flink 对大量数据执行批处理分析。我们探索了弗林克和弗林克的内部运作。然后,我们加载并分析执行转换和聚合操作的数据。然后我们探讨了如何对大数据执行 Join 操作。

在下一章中,我们将讨论使用 Apache Flink 的实时分析。

九、Apache Flink 流处理

在这一章中,我们将研究使用 Apache Flink 的流处理,以及该框架如何在数据到达时立即用于处理数据,以构建令人兴奋的实时应用。我们将从数据流应用编程接口开始,看看可以执行的各种操作。

我们将关注以下内容:

  • 使用 DataStream 应用编程接口进行数据处理
  • 转换
  • 聚集
  • 窗户
  • 物理分区
  • 改比例
  • 数据接收器
  • 事件时间和水印
  • 卡夫卡连接器
  • Twitter 连接器
  • Elasticsearch Connector
  • 卡珊德拉连接器

流执行模型介绍

Flink 是一个用于分布式流处理的开源框架,它:

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

下图是流处理的一般视图:

Flink 的许多功能(状态管理、处理无序数据、灵活的窗口)对于在无界数据集上计算精确结果至关重要,并且由 Flink 的流执行模型实现:

  • Flink 保证有状态计算只有一次语义。有状态意味着应用可以维护一段时间内处理过的数据的聚合或汇总,而 Flink 的检查点机制确保了在发生故障时应用状态的一次语义:

  • Flink 支持流处理和带有事件时间语义的窗口。事件时间可以轻松计算出事件无序到达和事件可能延迟到达的流的准确结果:

  • 除了数据驱动窗口之外,Flink 还支持基于时间、计数或会话的灵活窗口。Windows 可以通过灵活的触发条件进行定制,以支持复杂的流模式。Flink 的窗口技术可以模拟数据生成环境的真实情况:

  • Flink 的容错是轻量级的,允许系统保持高吞吐率,同时提供一次性一致性保证。Flink 从故障中恢复,零数据丢失,而可靠性和延迟之间的平衡可以忽略不计:

  • Flink 能够实现高吞吐量和低延迟(快速处理大量数据)。
  • Flink 的保存点提供了一种状态版本机制,使更新应用或重新处理历史数据成为可能,而不会丢失状态,停机时间也很少。
  • Flink 旨在运行在具有数千个节点的大规模集群上,除了独立的集群模式之外,Flink 还提供了对 Yarn 和 Mesos 的支持。

使用 DataStream 应用编程接口进行数据处理

拥有强大的分析功能来处理实时数据至关重要。这对于数据驱动的域更为重要。Flink 使您能够使用其 DataStream 应用编程接口进行实时分析。这个流式数据处理应用编程接口帮助您迎合物联网 ( 物联网)应用,并实时或接近实时地存储、处理和分析数据。

在接下来的几节中,让我们检查与数据流应用编程接口相关的每个元素:

  • 执行环境
  • 数据源
  • 转换
  • 数据接收器
  • 连接器

执行环境

要编写一个 Flink 程序,需要一个执行环境。您可以使用现有环境或创建新环境。

根据您的需求,Flink 允许您使用现有的 Flink 环境、创建本地环境或创建远程环境。

根据您的要求,使用getExecutionEnvironment()命令完成不同的任务:

  • 为了在集成开发环境的本地环境中执行,它会启动一个本地执行环境
  • 为了执行 JAR,Flink 集群管理器以分布式方式执行程序
  • 要创建您自己的本地或远程环境,您可以使用诸如createLocalEnvironment()createRemoteEnvironment等方法(字符串主机、int 端口、字符串和.jar文件)

数据源

Flink 从不同的来源获得数据。它有许多内置的源函数来无缝地获取数据。Flink 中几个预先实现的数据源功能简化了数据来源。Flink 还允许您在现有函数不足以进行数据来源时编写自定义数据源函数。

这里记录了 DataStream API:https://ci . Apache . org/project/flink/flink-docs-release-1.4/dev/DataStream _ API . html

以下是 Flink 中一些现有的数据源函数:

  • 基于套接字的数据来源
  • 基于文件的数据来源

基于套接字的

数据流应用编程接口使您能够从套接字读取数据。查看下面这段代码,了解流应用编程接口的简单说明:

// Data type for words with count
case class WordWithCount(word: String, count: Long)
// get input data by connecting to the socket
val text = senv.socketTextStream("127.0.0.1", 9000, '\n')
// parse the data, group it, window it, and aggregate the counts
val windowCounts = text
 .flatMap { w => w.split("\\s") }
 .map { w => WordWithCount(w, 1) }
 .keyBy("word")
 .timeWindow(Time.seconds(5), Time.seconds(1))
 .sum("count")
// print the results with a single thread, rather than in parallel
windowCounts.print().setParallelism(1)
senv.execute("Socket Window WordCount")

前面的代码连接到本地主机上的端口9000,接收和处理文本,将字符串拆分成单个单词(用空格分隔)。然后,代码在5秒的窗口中统计单词的频率并打印出来。

为了运行这个例子,我们将使用 Flink 的 Scala 外壳:

现在,在任何 Linux 系统上启动一个运行nc的本地服务器,如下所示:

现在,运行 shell 中的代码连接到端口9000并监听数据:

您现在可以在 web 控制台中看到作业正在运行:

您可以深入了解这些任务:

Figure: Screenshot showing a view of the tasks

如果您开始在nc服务器控制台中键入文本,您将开始在log文件夹中看到输出。

在我的例子中,我看到一个taskmanagerlog文件:

tail -f log/flink-sridharalla-taskmanager-1-Moogie.local.out

以下是您在跟踪log文件时会看到的内容:

现在我们已经看到了运行的示例代码,让我们看看套接字流的 API。

在应用编程接口中指定从套接字读取数据的主机和端口:

socketTextStream(hostName, port);

您也可以指定分隔符:

socketTextStream(hostName,port,delimiter)

您还可以指定应用编程接口必须从套接字获取数据的最大次数:

socketTextStream(hostName,port,delimiter, maxRetry)

基于文件

使用 Flink 中基于文件的源函数从文件源中流式传输数据。使用readTextFile(String path)从指定文件中流式传输数据。默认情况下,字符串路径具有默认值TextInputFormat。这意味着它逐行读取文本和字符串。

如果文件格式不同于文本,请使用以下功能指定格式:

readFile(FileInputFormat<Out> inputFormat, String path)

使用readFileStream()功能,Flink 可以在文件流产生时读取文件流:

readFileStream(String filePath, long intervalMillis, FileMonitoringFunction.WatchType watchType)

指定文件路径、轮询文件路径的轮询间隔以及监视类型。手表类型有三种:

  • FileMonitoringFunction.WatchType.ONLY_NEW_FILES:用于只处理新文件
  • FileMonitoringFunction.WatchType.PROCESS_ONLY_APPENDED:用于只处理文件的附加内容
  • FileMonitoringFunction.WatchType.REPROCESS_WITH_APPENDED:不仅用来重新处理文件的追加内容,也用来重新处理文件中之前的内容

如果文件不是文本文件,则可以使用此功能定义文件输入格式:

readFile(fileInputFormat, path, watchType, interval, pathFilter, typeInfo)

此命令将读取文件任务分为两个子任务:

  • 一个子任务只监控基于指定WatchType的文件路径
  • 第二子任务并行执行实际的文件读取

监控文件路径的子任务是非并行子任务。它根据轮询间隔连续扫描文件路径,报告要处理的文件,分割文件,并将分割分配给相应的下游线程。

转换

数据转换将数据流从一种形式转换为另一种形式。输入可以是一个或多个数据流,输出可以是零,也可以是一个或多个数据流。在接下来的部分中,让我们检查不同的转换。

地图

这是最简单的转换之一,其中输入是一个数据流,输出也是一个数据流:

在 Java 中:

inputStream.map(new MapFunction<Integer, Integer>() {
@Override
public Integer map(Integer value) throws Exception {
return 5 * value;
}
});

在斯卡拉:

inputStream.map { x => x * 5 }

平面地图

flatMap将一条记录作为输入,并给出零条、一条或多条记录的输出:

在 Java 中:

inputStream.flatMap(new FlatMapFunction<String, String>() {
@Override
public void flatMap(String value, Collector<String> out)
throws Exception {
    for(String word: value.split(" ")){
        out.collect(word);
    }
});

在斯卡拉:

inputStream.flatMap { str => str.split(" ") }

过滤器

filter功能评估条件,并根据满足的条件给出记录作为输出:

The filter function can output zero records also.

在 Java 中:

inputStream.filter(new FilterFunction<Integer>() {
@Override
    public boolean filter(Integer value) throws Exception {
        return value != 1;
    }
});

在斯卡拉:

inputStream.filter { _ != 1 }

键比

keyBy根据密钥对流进行逻辑分区。它使用hash函数来划分流。它返回KeyedDataStream:

在 Java 中:

inputStream.keyBy("someKey");

在斯卡拉:

inputStream.keyBy("someKey")

减少

reduce通过将最后一个减少的值减少当前值来推出KeyedDataStream。下面的代码做一个KeyedDataStream的和减:

在 Java 中:

keyedInputStream. reduce(new ReduceFunction<Integer>() {
@Override
    public Integer reduce(Integer value1, Integer value2)
        throws Exception {
            return value1 + value2;
        }
});

在斯卡拉:

keyedInputStream. reduce { _ + _ }

折叠

fold通过将最后一个文件夹的流与当前记录相结合,推出KeyedDataStream。它会发回数据流:

在 Java 中:

keyedInputStream keyedStream.fold("Start", new FoldFunction<Integer, String>() {
@Override
    public String fold(String current, Integer value) {
        return current + "=" + value;
    }
});

在斯卡拉:

keyedInputStream.fold("Start")((str, i) => { str + "=" + i })

前面的函数应用于(1,2,3,4,5)流时,将发出一个流

像这样:Start=1=2=3=4=5

聚集

数据流应用编程接口支持各种聚合,如minmaxsum等。这些功能可以在KeyedDataStream上应用,以获得滚动聚合:

在 Java 中:

keyedInputStream.sum(0)
keyedInputStream.sum("key")
keyedInputStream.min(0)
keyedInputStream.min("key")
keyedInputStream.max(0)
keyedInputStream.max("key")
keyedInputStream.minBy(0)
keyedInputStream.minBy("key")
keyedInputStream.maxBy(0)
keyedInputStream.maxBy("key")

在斯卡拉:

keyedInputStream.sum(0)
keyedInputStream.sum("key")
keyedInputStream.min(0)
keyedInputStream.min("key")
keyedInputStream.max(0)
keyedInputStream.max("key")
keyedInputStream.minBy(0)
keyedInputStream.minBy("key")
keyedInputStream.maxBy(0)
keyedInputStream.maxBy("key")

maxmaxBy的区别在于max返回一个流中的最大值,而maxBy返回一个有最大值的键。这同样适用于minminBy

窗户

window功能允许按时间或其他条件对现有的KeyedDataStreams进行分组。以下转换以10秒的时间窗口发出记录组:

在 Java 中:

inputStream.keyBy(0).window(TumblingEventTimeWindows.of(Time.seconds(10)));

在斯卡拉:

inputStream.keyBy(0).window(TumblingEventTimeWindows.of(Time.seconds(10)))

Flink 定义了称为窗口的数据切片来处理潜在的无限数据流。

这有助于使用转换处理数据块。要对流进行开窗,请分配一个可以进行分发的密钥和一个描述对开窗流执行哪些转换的函数。

要将流分割成窗口,可以使用预先实现的 Flink 窗口分配器。使用滚动窗口、滑动窗口、全局窗口和会话窗口等选项。

Flink 还允许你通过扩展WindowAssigner类来编写自定义窗口分配器。

让我们在以下几节中研究这些分配器是如何工作的。

全局窗口

除非由触发器指定,否则全局窗口是永不结束的窗口。一般来说,在这种情况下,每个元素被分配给一个单独的每键全局窗口。如果未指定任何触发器,则不会触发任何计算。

翻滚的窗户

翻转窗口是固定长度的窗口,并且不重叠。使用滚动窗口在特定时间计算元素。例如,10 分钟的翻转窗口可用于计算 10 分钟内发生的一组事件。

推拉窗

滑动窗口类似于翻滚窗口,只是它们是重叠的。它们是固定长度的窗口,通过用户给定的窗口滑动参数与前面的窗口重叠。

使用这个窗口来计算在特定时间范围内发生的一组事件。

会话窗口

当必须根据输入数据决定窗口边界时,会话窗口非常有用。会话窗口允许窗口开始时间和窗口大小的灵活性。

提供会话间隙配置参数,该参数指示在认为会话已关闭之前等待的持续时间。

windowsll

windowAll功能允许对常规数据流进行分组。这通常是非并行数据转换,因为它运行在非分区数据流上:

在 Java 中:

inputStream.windowAll(TumblingEventTimeWindows.of(Time.seconds(10)));

在斯卡拉:

inputStream.windowAll(TumblingEventTimeWindows.of(Time.seconds(10)))

类似于常规的数据流函数,我们也有窗口数据流函数。唯一不同的是,它们处理的是窗口数据流。因此,窗口缩小的工作方式类似于reduce函数,窗口折叠的工作方式类似于fold函数,并且还有聚合。

联盟

union函数执行两个或多个数据流的合并。它并行组合数据流。如果将一个流与其自身组合,它会输出每个记录两次:

在 Java 中:

inputStream. union(inputStream1, inputStream2, ...);

在斯卡拉:

inputStream. union(inputStream1, inputStream2, ...)

窗口连接

通过公共窗口中的一些键连接两个数据流。以下示例显示了两个流在5秒窗口中的连接,其中第一个流的第一个属性的连接条件等于另一个流的第二个属性:

在 Java 中:

inputStream. join(inputStream1)
.where(0).equalTo(1)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.apply (new JoinFunction () {...});

在斯卡拉:

inputStream. join(inputStream1)
.where(0).equalTo(1)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.apply { ... }

使分离

使用此功能,根据标准将流split分成两个或多个流。当您获得混合流并且您可能想要单独处理数据时,这尤其有用:

在 Java 中:

SplitStream<Integer> split = inputStream.split(new OutputSelector<Integer>() {
@Override
public Iterable<String> select(Integer value) {
List<String> output = new ArrayList<String>();
if (value % 2 == 0) {
output.add("even");
}
else {
output.add("odd");
}
return output;
}
});

在斯卡拉:

val split = inputStream.split( (num: Int) =>(num % 2) match {
    case 0 => List("even")
    case 1 => List("odd")
})

挑选

使用此功能从分割流中选择特定的流:

在 Java 中:

SplitStream<Integer> split;
DataStream<Integer> even = split.select("even");
DataStream<Integer> odd = split.select("odd");
DataStream<Integer> all = split.select("even","odd");

在斯卡拉:

val even = split select "even"
val odd = split select "odd"
val all = split.select("even","odd")

项目

使用project功能从事件流中选择属性子集,并且仅将选定的元素发送到下一个处理流:

在 Java 中:

DataStream<Tuple4<Integer, Double, String, String>> in = // [...]
DataStream<Tuple2<String, String>> out = in.project(3,2);

在斯卡拉:

val in : DataStream[(Int,Double,String)] = // [...]
val out = in.project(3,2)

前面的函数从给定的记录中选择属性号23。以下是示例输入和输出记录:

(1,10.0, A, B )=> (B,A)
(2,20.0, C, D )=> (D,C)

物理分区

使用 Flink,您可以对流数据进行物理分区。您还可以选择提供自定义分区。让我们在以下几节中研究不同类型的分区。

自定义分区

如前所述,您可以提供分区器的自定义实现:

在 Java 中:

inputStream.partitionCustom(partitioner, "someKey");
inputStream.partitionCustom(partitioner, 0);

在斯卡拉:

inputStream.partitionCustom(partitioner, "someKey")
inputStream.partitionCustom(partitioner, 0)

编写自定义分区器时,请确保实现了有效的hash函数。

随机分区

随机分区以均匀的方式随机划分数据流:

在 Java 中:

inputStream.shuffle();

在斯卡拉:

inputStream.shuffle()

重新平衡分区

这种类型的分区有助于均匀分布数据。它使用循环方法进行分发。当数据有偏差时,这种类型的分区是很好的:

在 Java 中:

inputStream.rebalance();

在斯卡拉:

inputStream.rebalance()

改比例

重新缩放用于跨操作分布数据,对数据子集执行转换,并将它们组合在一起。这种重新平衡仅发生在单个节点上,因此不需要任何跨网络的数据传输:

在 Java 中:

inputStream.rescale();

在斯卡拉:

inputStream.rescale()

广播

广播将所有记录分发到每个分区。这有助于将每个元素分布到所有分区:

在 Java 中:

inputStream.broadcast();

在斯卡拉:

inputStream.broadcast()Data Sinks

一旦数据转换完成,您必须保存结果。以下是保存结果的一些 Flink 选项:

  • writeAsText():以字符串形式一次写入一行记录。
  • writeAsCsV():将元组写成逗号分隔的值文件。还可以配置行和字段分隔符。
  • print() / printErr():将记录写入标准输出。您也可以选择写入标准错误。
  • writeUsingOutputFormat():也可以提供自定义输出格式。定义自定义格式时,扩展OutputFormat,负责序列化和反序列化。
  • writeToSocket() : Flink 也支持将数据写入特定的套接字。定义SerializationSchema进行适当的序列化和格式化。

事件时间和水印

Flink 流媒体应用编程接口的灵感来自谷歌数据流模型。这个 API 支持不同的时间概念。以下是您可以在流环境中捕获时间的三个常见位置:

  • 事件时间:事件时间是指事件在其产生设备上发生的时间。例如,在物联网项目中,它可以是传感器捕捉读数的时间。一般来说,这些事件时间在进入 Flink 之前需要嵌入到记录中。在时间处理过程中,这些时间戳被提取出来并考虑用于开窗。事件时间处理可用于无序事件。
  • 处理时间:处理时间是执行数据流处理的机器时间。处理时间窗口只考虑事件被处理的时间戳。处理时间是流处理的最简单方式,因为它不需要处理机器和生产机器之间的任何同步。在分布式异步环境处理中,时间不提供确定性,因为它取决于记录在系统中流动的速度。
  • 摄入时间:摄入时间是特定事件进入 Flink 的时间。所有基于时间的操作都引用这个时间戳。摄取时间是一个比处理更昂贵的操作,但会产生可预测的结果。摄取时间程序不能处理任何无序事件,因为它只在事件进入 Flink 系统后才分配时间戳。

以下示例显示了如何设置事件时间和水印。在摄取时间和处理时间的情况下,只需分配时间特征,水印生成就会自动完成。下面是这个的代码片段:

在 Java 中:

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
//or
env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime);

在斯卡拉:

val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)
//or
env.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime)

对于事件时间流程序,指定分配水印和时间戳的方式。分配水印和时间戳有两种方式:

  • 直接从数据源属性
  • 使用时间戳受理人

要使用事件时间流,请按如下方式分配时间特性:

在 Java 中:

final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime;

在斯卡拉:

val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

在源中存储记录时,最好总是存储事件时间。Flink 还支持一些预定义的时间戳提取器和水印生成器。

连接器

Apache Flink 支持各种连接器,允许跨各种技术进行数据读/写。

卡夫卡连接器

Kafka 是一个发布-订阅分布式消息队列系统,允许用户发布消息到某个主题。然后将这些内容分发给该主题的订阅者。Flink 提供了将卡夫卡消费者定义为 Flink 流中的数据源的选项。要使用 Flink Kafka 连接器,必须使用特定的 JAR 文件。

使用以下 Maven 依赖项来使用连接器。例如,对于卡夫卡 0.9 版本,在pom.xml中添加以下依赖项:

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka-0.9_2.11/artifactId>
    <version>1.1.4</version>
</dependency>

现在,让我们看看如何使用卡夫卡消费者作为卡夫卡的来源:

在 Java 中:

Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "localhost:9092");
properties.setProperty("group.id", "test");
DataStream<String> input = env.addSource(new
FlinkKafkaConsumer09<String>("mytopic", new SimpleStringSchema(), properties));

在斯卡拉:

val properties = new Properties();
properties.setProperty("bootstrap.servers", "localhost:9092");
// only required for Kafka 0.8
properties.setProperty("zookeeper.connect", "localhost:2181");
properties.setProperty("group.id", "test");
stream = env
.addSource(new FlinkKafkaConsumer09[String]("mytopic", new
SimpleStringSchema(), properties))
.print

在前面的代码中,我们首先设置了 Kafka 主机以及 zookeeper 主机和端口的属性。然后,我们指定了主题名称,在本例中为mytopic。因此,如果任何消息发布到mytopic主题,它们将由 Flink 流处理。

如果以不同的格式获取数据,还可以为反序列化指定自定义模式。默认情况下,Flink 支持字符串和 JSON 反序列化程序。要启用容错,请在 Flink 中启用检查点。弗林克定期拍摄该州的快照。如果出现故障,它会恢复到最后一个检查点并重新开始处理。你也可以把卡夫卡制作人定义为一个 Flume。这将数据写入了卡夫卡的主题。要将数据写入卡夫卡的主题:

在 Java 中:

stream.addSink(new FlinkKafkaProducer09[String]("localhost:9092", "mytopic", new SimpleStringSchema()))

在斯卡拉:

stream.addSink(new FlinkKafkaProducer09<String>("localhost:9092", "mytopic", new SimpleStringSchema()));

Twitter 连接器

随着社交媒体和社交网站日益强大,能够从推特获取数据并进行处理变得至关重要。Twitter 数据可以用来对各种产品、服务、应用等做情感分析。

Flink 提供了 Twitter 连接器作为一个数据源。要使用连接器,请使用您的 Twitter 帐户创建一个 Twitter 应用,并生成连接器要使用的身份验证密钥。

Twitter 连接器可以使用 Java 或 Scala API。生成令牌后,您可以编写一个程序从 Twitter 获取数据,如下所示:

  1. 首先,添加一个 Maven 依赖项:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-twitter_2.11/artifactId>
<version>1.1.4</version>
</dependency>
  1. 接下来,添加 Twitter 作为数据源:

在 Java 中:

Properties props = new Properties();
props.setProperty(TwitterSource.CONSUMER_KEY, "");
props.setProperty(TwitterSource.CONSUMER_SECRET, "");
props.setProperty(TwitterSource.TOKEN, "");
props.setProperty(TwitterSource.TOKEN_SECRET, "");
DataStream<String> streamSource = env.addSource(new TwitterSource(props));

在斯卡拉:

val props = new Properties();
props.setProperty(TwitterSource.CONSUMER_KEY, "");
props.setProperty(TwitterSource.CONSUMER_SECRET, "");
props.setProperty(TwitterSource.TOKEN, "");
props.setProperty(TwitterSource.TOKEN_SECRET, "");
DataStream<String> streamSource = env.addSource(new TwitterSource(props));

在前面的代码中,我们首先为获得的令牌设置属性,然后添加TwitterSource。如果给定的信息是正确的,开始从推特获取数据。TwitterSource以 JSON 字符串格式发出数据。推特 JSON 示例如下所示:

{
...
"text": ""Loyalty 3.0: How to Revolutionize Customer &amp; Employee
Engagement with Big Data &amp; #Gamification" can be ordered here:
http://t.co/1XhqyaNjuR",
"geo": null,
"retweeted": false,
"in_reply_to_screen_name": null,
"possibly_sensitive": false,
"truncated": false,
"lang": "en",
"hashtags": [{
"text": "Gamification",
"indices": [90,
103]
}],
},
"in_reply_to_status_id_str": null,
"id": 330094515484508160
...
}

TwitterSource提供各种StatusesSampleEndpoint,返回一组随机推文。如果需要添加一些过滤器,又不想使用默认端点,可以实现TwitterSource.EndpointInitializer界面。

一旦你从推特上获取数据,你就可以处理、存储或分析数据。

兔子 MQ 连接器

RabbitMQ 是一个广泛使用的分布式高性能消息队列系统。它被用作高吞吐量操作的消息传递系统。它允许您创建分布式消息队列,并在队列中包含发布者和订阅者。更多关于 RabbitMQ 的信息,请访问https://www.rabbitmq.com/

Flink 支持从 RabbitMQ 获取和发布数据。它提供了一个连接器,可以作为数据流的数据源。

要使 RabbitMQ 连接器正常工作,您必须提供以下信息:

  • rabbtmq:主机、端口、用户凭证等配置。
  • 队列:您希望订阅的 RabbitMQ 队列名称。
  • 关联 ID:这是一个 RabbitMQ 特性,用于在分布式世界中通过唯一的 ID 关联请求和响应。Flink RabbitMQ 连接器提供了一个接口,根据您是否使用它,将其设置为truefalse
  • 反序列化模式:rabbtmq 以序列化的方式存储和传输数据,避免网络流量。因此,当收到消息时,订户知道如何反序列化消息。Flink 连接器为我们提供了一些默认的反序列化程序,比如字符串反序列化程序。

RabbitMQ 源为我们提供了以下关于流交付的选项:

  • 恰好一次:使用 Rabbtmq 相关标识和 Flink 检查点机制处理 Rabbtmq 事务
  • 至少一次:当启用了 Flink 检查点但没有设置 RabbitMQ 相关标识时

RabbitMQ 自动提交模式没有强有力的交付保证。

现在让我们编写一个代码来让这个连接器工作。像其他连接器一样,向代码中添加一个 Maven 依赖项:

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-rabbitmq_2.11/artifactId>
<version>1.1.4</version>
</dependency>

下面的代码片段展示了如何在 Java 中使用 RabbitMQ 连接器:

//Configurations
RMQConnectionConfig connectionConfig = new RMQConnectionConfig.Builder()
.setHost(<host>).setPort(<port>).setUserName(..)
.setPassword(..).setVirtualHost("/").build();

//Get Data Stream without correlation ids
DataStream<String> streamWO = env.addSource(new
RMQSource<String>(connectionConfig, "my-queue", new SimpleStringSchema()))
.print

//Get Data Stream with correlation ids
DataStream<String> streamW = env.addSource(new
RMQSource<String>(connectionConfig, "my-queue", true, new
SimpleStringSchema()))
.print

同样,在 Scala 中,代码可以编写如下:

val connectionConfig = new RMQConnectionConfig.Builder()
.setHost(<host>).setPort(<port>).setUserName(..)
.setPassword(..).setVirtualHost("/").build()
streamsWOIds = env.addSource(new RMQSource[String](connectionConfig, " my-queue", new SimpleStringSchema))
.print
streamsWIds = env.addSource(new RMQSource[String](connectionConfig, "my-queue", true, new SimpleStringSchema))
.print

您也可以使用 RabbitMQ 连接器作为 Flink 接收器。

要将进程发送回不同的 RabbitMQ 队列,请提供三个重要配置:

  • rabbitmq 配置
  • 队列名称–将处理后的数据发送回哪里
  • 序列化模式–Rabbtmq 将数据转换为字节的模式

下面是用 Java 编写的示例代码,展示了如何将这个连接器用作 Flink 接收器:

RMQConnectionConfig connectionConfig = new RMQConnectionConfig.Builder()
.setHost(<host>).setPort(<port>).setUserName(..)
.setPassword(..).setVirtualHost("/").build();
stream.addSink(new RMQSink<String>(connectionConfig, "target-queue", new StringToByteSerializer()));

同样的事情也可以在 Scala 中完成:

val connectionConfig = new RMQConnectionConfig.Builder()
.setHost(<host>).setPort(<port>).setUserName(..)
.setPassword(..).setVirtualHost("/").build()
stream.addSink(new RMQSink[String](connectionConfig, "target-queue", new StringToByteSerializer

Elasticsearch Connector

Elasticsearch 是一个分布式、低延迟的全文搜索引擎系统,它允许您对自己选择的文档进行索引,然后对文档集进行全文搜索。要了解更多关于弹性搜索的信息,请参见https://www.elastic.co

在许多情况下,您可能希望使用 Flink 处理数据,然后将其存储在弹性搜索中。为此,Flink 支持弹性搜索连接器。到目前为止,Elasticsearch 已经发布了两个主要版本。弗林克支持他们两个。对于 Elasticsearch 1.x,需要添加以下 Maven 依赖项:

<dependency>
\<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-elasticsearch_2.11</artifactId>
<version>1.1.4</version>
</dependency>

Flink 连接器提供了一个将数据写入弹性搜索的接收器。它使用两种方法连接到弹性搜索:

  • 嵌入式节点模式:在嵌入式节点模式下,接收器使用 BulkProcessor 将文档发送到 ElasticSearch。您可以配置在将文档发送到弹性搜索之前要缓冲多少请求。以下是代码片段:
DataStream<String> input = ...;
Map<String, String> config = Maps.newHashMap();
config.put("bulk.flush.max.actions", "1");
config.put("cluster.name", "cluster-name");
input.addSink(new ElasticsearchSink<>(config, new
IndexRequestBuilder<String>() {
@Override
public IndexRequest createIndexRequest(String element, RuntimeContext ctx) {
    Map<String, Object> json = new HashMap<>();
    json.put("data", element);
    return Requests.indexRequest()
    .index("my-index")
    .type("my-type")
    .source(json);
}
}));

在前面的代码片段中,我们创建了一个哈希映射,其中包含一些配置,例如集群名称以及在发送请求之前要缓冲多少文档。然后,我们将接收器添加到流中,指定要存储的索引、类型和文档。同样,Scala 中的代码如下:

val input: DataStream[String] = ...
val config = new util.HashMap[String, String]
config.put("bulk.flush.max.actions", "1")
config.put("cluster.name", "cluster-name")
text.addSink(new ElasticsearchSink(config, new IndexRequestBuilder[String]
{
    override def createIndexRequest(element: String, ctx: RuntimeContext):
    IndexRequest = {
        val json = new util.HashMap[String, AnyRef]
        json.put("data", element)
        Requests.indexRequest.index("my-index").`type`("my-type").source(json)
    }
}))
  • 传输客户端模式:弹性搜索允许通过端口9300上的传输客户端进行连接。Flink 支持通过其连接器使用这些连接。在配置中指定集群中存在的所有弹性搜索节点。以下是 Java 中的代码片段:
DataStream<String> input = ...;
Map<String, String> config = Maps.newHashMap();
config.put("bulk.flush.max.actions", "1");
config.put("cluster.name", "cluster-name");
List<TransportAddress> transports = new ArrayList<String>();
transports.add(new InetSocketTransportAddress("es-node-1", 9300));
transports.add(new InetSocketTransportAddress("es-node-2", 9300));
transports.add(new InetSocketTransportAddress("es-node-3", 9300));
input.addSink(new ElasticsearchSink<>(config, transports, new
IndexRequestBuilder<String>() {
@Override
public IndexRequest createIndexRequest(String element, RuntimeContext ctx) {
Map<String, Object> json = new HashMap<>();
json.put("data", element);
return Requests.indexRequest()
.index("my-index")
.type("my-type")
.source(json);
}
}));

在这里,我们还提供了关于集群名称、节点、端口、批量发送的最大请求数等详细信息。Scala 中类似的代码可以编写如下:

val input: DataStream[String] = ...
val config = new util.HashMap[String, String]
config.put("bulk.flush.max.actions", "1")
config.put("cluster.name", "cluster-name")
val transports = new ArrayList[String]
transports.add(new InetSocketTransportAddress("es-node-1", 9300))
transports.add(new InetSocketTransportAddress("es-node-2", 9300))
transports.add(new InetSocketTransportAddress("es-node-3", 9300))
text.addSink(new ElasticsearchSink(config, transports, new
IndexRequestBuilder[String] {
override def createIndexRequest(element: String, ctx: RuntimeContext):
IndexRequest = {
val json = new util.HashMap[String, AnyRef]
json.put("data", element)
Requests.indexRequest.index("my-index").`type`("my-type").source(json)
}
}))

卡珊德拉连接器

Cassandra 是一个分布式、低延迟的 NoSQL 数据库。这是一个基于键值的数据库。许多高吞吐量应用使用 Cassandra 作为它们的主数据库。Cassandra 采用分布式集群模式,没有主从架构。任何节点都可以支持读写。更多关于卡珊德拉的信息,请访问http://cassandra.apache.org

Apache Flink 提供了一个连接器,可以将数据写入 Cassandra。在许多应用中,人们可能希望在 Cassandra 中存储来自 Flink 的流数据。

像其他连接器一样,为了获得这一点,我们需要将其作为 Maven 依赖项添加:

<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-cassandra_2.11</artifactId>
<version>1.1.4</version>
</dependency>

添加依赖项后,添加 Cassandra 接收器及其配置,如下所示:

在 Java 中:

CassandraSink.addSink(input)
.setQuery("INSERT INTO cep.events (id, message) values (?, ?);")
.setClusterBuilder(new ClusterBuilder() {
@Override
public Cluster buildCluster(Cluster.Builder builder) {
return builder.addContactPoint("127.0.0.1").build();
}
})
.build()

在斯卡拉:

前面的代码将数据流写入名为事件的表中。该表需要一个事件标识和一条消息:

CassandraSink.addSink(input)
.setQuery("INSERT INTO cep.events (id, message) values (?, ?);")
.setClusterBuilder(new ClusterBuilder() {
@Override
public Cluster buildCluster(Cluster.Builder builder) {
return builder.addContactPoint("127.0.0.1").build();
}
)
.build();

摘要

在这一章中,我们了解了 Flink 最强大的 API——DataStream API;数据源、转换和接收器如何协同工作;以及关于各种技术连接器,比如 Elasticsearch、Cassandra、Kafka、RabbitMQ 等等。在本章中,我们还讨论了使用 Apache Flink 的流处理。

在下一章中,我们将转换话题,看看可视化数据最令人兴奋的领域之一。

十、可视化大数据

本章探讨了大数据处理和分析中最重要的活动之一,即创建数据和见解的强大可视化。我们倾向于理解任何图形,而不是文本或数字。在分析过程中,你需要不断地理解数据,并操纵其用法和解释;如果您能够可视化数据,而不是从表、列或文本文件中读取数据,那么这将会容易得多。当您使用了我们迄今为止看到的许多分析数据和生成见解的方法之一(例如通过 Python、R、Spark、Flink、Hive、MapReduce 等)时,任何试图理解见解的人都会希望理解数据上下文中的见解。出于这个目的,你也需要一些绘画作品。

简而言之,本章将涵盖以下主题:

  • 介绍
  • 活人画
  • 图表类型
  • 使用 Python
  • 使用 R
  • 数据可视化工具

介绍

数据可视化是最有价值的手段之一,通过它我们可以理解大数据,从而使它对大多数人更有用。数据可视化在很大程度上取决于用例。图表是数据的可视化表示。它们提供了一种强大的手段,以大多数人认为更容易理解的方式总结和呈现数据。图表使我们能够看到一些数据的主要特征。它们不仅使我们能够呈现一项研究的数字结果,而且还提供了数据的形状和模式,这对数据分析和决策至关重要。在开发数据可视化时,您需要牢记许多关键注意事项:

  • 哪种类型的数据使用哪种类型的图形表示
  • 如何设计允许交互功能的可视化方法
  • 如何以图形方式搜索和修改数据集
  • 如何区分数据和由此产生的见解
  • 如何开发可随着大数据规模的数据增长而扩展的可视化方法
  • 如何解决延迟问题,以便在可视化数据时没有明显的延迟
  • 如何优化高速或流式数据的设计以显示实时可视化效果
  • 如何可视化数据库中的数据
  • 如何在内存中可视化数据

有许多不同的方式来可视化数据。下图显示了一些示例来描述图表类型的选择如何改变可视化的使用和效果:

以下是可视化的更多示例:

Figure: Screenshot showing some more examples of visualization

活人画

在本节中,我们将设置 Tableau,这是一个非常流行的可视化工具。为此,我们可以简单地下载 Tableau 的试用版,并将其安装在我们的本地机器上。你可以在 https://www.tableau.com/找到 Tableau。

下面的截图显示了 Tableau 的下载链接:

一旦您安装了试用版(或者如果您已经有了许可的副本),您就可以开始一些基本的可视化练习了。

以下是 Tableau 发布的截图,您将在其中看到各种数据来源:

让我们从打开文件OnlineRetail.csv开始。以下是空白工作表的屏幕截图:

选择Quantity为一列,可以看到一条一条的条形图,如下图截图所示:

选择Description作为一行,查看显示每个项目数量的条形图,如下所示:

您可以应用过滤器来消除负数量值,如下图所示:

你会看到任意数值列的取值范围,如Quantity:

现在,您可以选择Quantity值的有效范围,如下所示:

如下图所示,现在只显示正值:

可以通过Quantity对图表进行排序,这样就可以看到顶部Quantity最大的项目Descriptions,如下图截图所示:

创建新工作表,如下所示:

与上一张工作表类似,选择DescriptionQuantity,如下图所示:

您可以从右侧窗格中选择不同的图表类型;选择包装气泡:

尝试选择树形图作为图表类型,如下所示:

您可以更改图表的颜色和其他属性,如下图所示:

很容易排除任何行/列或值/数据点:

您还可以创建包含多个工作表的仪表板,如下所示:

创建一些其他图表类型(比如折线图),如下图所示:

将新工作表添加到仪表板,如下图所示:

图表类型

图表可以有很多种形式;然而,有一些共同的特征为图表提供了从数据中提取意义的能力。通常,图表中的数据是用图形表示的,因为人类通常能够比文本更快地从图片中推断出意义。文本通常仅用于注释数据。

图形中文本最重要的用途之一是标题。图形的标题通常出现在主图形的上方,并提供图形中数据所指内容的简洁描述。数据中的尺寸通常显示在轴上。如果使用水平轴和垂直轴,它们通常分别称为 x 轴和 y 轴。每个轴都有一个刻度,由周期刻度表示,通常伴有数字或分类指示。每个轴通常还会在其外部或旁边显示一个标签,简要描述所表示的尺寸。如果刻度是数字的,标签通常以括号中的刻度单位作为后缀。在图表中,可能会出现一个线条网格来帮助数据的视觉对齐。网格可以通过在规则或显著的刻度上视觉强调线条来增强。强调的线被称为主网格线,其余的是次网格线。

图表的数据可以以各种格式出现,并且可以包括描述与图表中的指示位置相关联的数据的单独文本标签。数据可以显示为点或形状,连接或不连接,以及颜色和图案的任意组合。推论或兴趣点可以直接覆盖在图上,以进一步帮助信息提取。

当图表中出现的数据包含多个变量时,图表可能包含一个图例(也称为)。图例包含图表中出现的变量列表及其外观示例。该信息允许在图表中识别每个变量的数据。

折线图

折线图允许观察一个或几个变量在一段时间内的行为,并识别趋势。在传统商业智能中,折线图可以显示过去 12 个月的销售、利润和收入发展。当使用大数据时,公司可以使用这种可视化技术来跟踪按周计算的产品购买总量、按月计算的销售办公室平均订单数等等。

以下截图是折线图的示例:

圆形分格统计图表

饼图显示了整体的组成部分。同时处理传统数据和大数据的公司可能会使用这种技术来查看客户细分或市场份额。区别在于这些公司获取原始数据进行分析的来源。

以下是饼图的示例:

条形图

条形图允许比较不同变量的值。在传统的商业智能中,公司可以按类别分析他们的销售,按渠道分析营销推广的成本,等等。在分析大数据时,公司可以按小时查看客户参与度、销售数据等。

垂直条形图的示例如下:

以下屏幕截图是水平条形图的示例:

热图

热图使用颜色来表示数据。用户可能会在 Excel 中遇到一个热图,用绿色突出表现最好的分支机构的销售额,用红色突出表现最差的分支机构的销售额。如果零售商有兴趣了解商店中最常光顾的过道,他们也会使用销售区域的热图。在这种情况下,零售商将分析大数据,例如来自视频监控系统的数据:

Some really cool visualizations can be seen at https://blog.hubspot.com/marketing/great-data-visualization-examples and also at http://www.mastersindatascience.org/blog/10-cool-big-data-visualizations/.

可视化本身就是一门艺术,每个用例都需要关注正在可视化的内容,从图表类型、数据点数量、元素颜色等等开始。

使用 Python 可视化数据

Python 提供了许多大数据分析以及数据绘图和可视化的广泛功能。

Analyzing and Visualizing Big Data using Python is covered in Chapter 4, Scientific Computing and Big Data Analysis with Python and Hadoop.

这里有一个使用 Python 的例子,涉及一个单独的列:

d8 = pd.DataFrame(df, columns=['Quantity'])[0:100]
d8.plot()

这里,只选择前 100 个元素,以使图形不那么拥挤,并更好地说明示例。

现在,你将拥有:

假设您希望显示多列。请看下面的代码:

d8 = pd.DataFrame(df, columns=['Quantity', 'UnitPrice'])[0:100]
d8.plot()

只要记住它不会绘制Description这样的定性数据列,只会绘制可以绘制的东西,比如QuantityUnitPrice

用 R 可视化数据

r 为大数据分析以及数据绘图和可视化提供了许多广泛的功能。

Analyzing and Visualizing Big Data using R is covered in Chapter 5, Statistical Big Data Computing with R and Hadoop.

使用 R,我们还可以绘制一列选择。看看这个:

plot(df$UnitPrice)

plot(d1, type="b")

大数据可视化工具

对大数据工具市场的快速调查揭示了包括微软、思爱普、IBM 和 SAS 在内的大公司的存在。但是有很多专业软件供应商提供领先的大数据可视化工具,其中包括 Tableau、Qlik 和 TIBCO。领先的数据可视化产品包括以下公司提供的产品:

摘要

在本章中,我们讨论了可视化的力量以及良好可视化实践背后的各种概念。在下一章中,我们将了解云计算的力量,以及它如何改变大数据和大数据分析的格局。

十一、云计算简介

本章介绍云计算、基础设施即服务 ( IaaS )、平台即服务 ( PaaS )和软件即服务 ( SaaS )的概念。还简要讨论了顶级云提供商。

简而言之,本章将涵盖以下主题:

  • 云计算基础知识
  • 概念和术语
  • 目标和益处
  • 风险和挑战
  • 角色和界限
  • 云特征
  • 云交付模型
  • 云部署模型

无论您运行的应用能够在数百万移动用户之间共享照片,还是支持企业的关键运营,云服务平台都可以快速访问灵活且低成本的信息技术资源。借助云计算,您不需要在管理硬件上进行大量投资。相反,您可以调配合适的计算资源来支持您的想法或管理您的信息技术部门的运营。您可以立即访问所需的资源,并只根据使用情况付费。

云计算提供了一种通过互联网访问服务器、存储、数据库和一系列应用服务的简单方法。像亚马逊网络服务 ( AWS )这样的云服务平台拥有并维护这些应用服务所需的网络连接硬件,同时您可以使用网络应用使用您需要的东西。

概念和术语

本节介绍云的基本概念及其构件。

云是指一个独特的信息技术环境,它是为远程配置可扩展和可测量的信息技术资源而设计的。该术语起源于对互联网的隐喻,用来描述提供对一组分散的信息技术资源的远程访问的网络网络。在云计算成为正式的信息技术领域之前,云符号通常用于在各种规范和基于网络的架构的主流文档中表示互联网。

信息技术资源

信息技术资源是与信息技术相关的物理或虚拟工件,可以是基于软件的,如虚拟服务器或自定义软件程序,也可以是基于硬件的,如物理服务器或网络设备。

内部

作为一个独特的可远程访问的环境,云代表了一种部署信息技术资源的选择。在组织边界内的传统 IT 企业中托管的 IT 资源(不具体表示云)被认为位于 IT 企业的场所或内部(内部意味着位于非基于云的受控 IT 环境的场所)。该术语用于将信息技术资源定义为基于云的替代物。内部部署的信息技术资源不能基于云,反之亦然。

云消费者和云提供商

提供基于云的信息技术资源的实体是云提供商。使用基于云的信息技术资源的实体是云消费者

缩放比例

扩展表示信息技术资源处理使用需求的能力。

以下各节描述了缩放的类型。

缩放类型

  • 水平缩放:向外缩放和向内缩放
  • 垂直缩放:向上缩放和向下缩放

水平缩放

分配或释放同类型的 IT 资源称为水平伸缩。资源的横向分配称为向外扩展,资源的横向释放称为向外扩展。水平扩展是云环境中常见的扩展形式。

垂直缩放

当一个现有的 IT 资源被另一个更高或更低容量的资源替代时,就会发生垂直扩展。将一个 IT 资源替换为另一个容量更高的资源被称为向上扩展,将一个 IT 资源替换为一个容量更低的资源被称为向下扩展。由于更换造成的停机时间,垂直扩展在云环境中不太常见。

云服务

尽管云是一个可远程访问的环境,但并非云内的所有信息技术资源都可以远程访问。例如,部署在云中的数据库或物理服务器只能由同一云中的其他信息技术资源访问。可以专门部署具有已发布的应用编程接口的软件程序,以支持远程客户端的访问。

云服务是可以使用云远程访问的任何信息技术资源。与其他包含服务技术的信息技术领域不同,例如面向服务的体系结构 ( SOA ),云计算环境中的术语服务是广义的。云服务可以作为简单的基于网络的软件程序存在,具有使用消息协议调用的技术界面,或者作为管理工具或更大环境的远程访问点。

云服务消费者

云服务消费者是软件程序在运行时访问云服务时承担的临时角色。

云服务消费者的常见类型可以包括能够通过已发布的服务合同远程访问云服务的软件程序和服务,以及运行能够远程访问作为云服务可用的其他信息技术资源的软件的工作站、笔记本电脑和移动设备。

目标和益处

与批发商类似,公共云提供商的业务模式基于大规模获取信息技术资源,这些资源以有吸引力的价格提供给云消费者。这有助于组织在没有任何基础架构成本的情况下访问强大的基础架构。

投资基于云的信息技术资源最常见的经济理由是减少初始信息技术投资,如硬件、软件购买和拥有成本。云的度量使用特性代表一个特性集,它允许度量的运营支出(与业务绩效直接相关)取代预期的资本支出。这也称为比例成本

成本的降低让企业从小处着手,根据需要增加 IT 资源配置。此外,较低的初始支出允许资本被重新导向核心业务投资。降低成本的机会来自主要云提供商对大规模数据中心的部署和运营。此类数据中心通常位于能够以较低成本获得房地产、信息技术专业人员和网络带宽的区域,从而实现更高的运营成本节约。

同样的原理也适用于操作系统、中间件或平台软件以及应用软件。汇集的信息技术资源可以由多个云消费者共享,从而提高或优化利用率。通过使用经验证的做法来优化云架构、管理和治理,可以进一步降低运营成本和效率低下。

云消费者的优势:

  • 短期按需访问现收现付计算资源(如按小时计算的处理器),并在不需要时释放这些计算资源
  • 对无限计算资源的访问,这些资源按需提供,无需准备资源调配
  • 在初级水平上添加或删除 IT 资源的能力,例如以单个千兆字节的增量修改可用存储磁盘空间
  • 基础架构的基础架构抽象,以便应用不会被锁定在设备或位置上,并且可以在需要时轻松移动

例如,一家拥有大量以批处理为中心的任务的公司可以像他们的应用软件一样快速地完成这些任务。使用 100 台服务器 1 小时的成本与使用 1 台服务器 100 小时的成本相同。在不需要大量初始投资来创建大规模计算基础架构的情况下,实现这种信息技术资源的弹性非常有吸引力。

尽管云计算的好处显而易见,但实际的经济性可能很难计算和评估。继续采用云计算战略的决定将不仅仅是简单地比较租赁成本和购买成本。

增强的可扩展性

通过提供信息技术资源池,以及旨在共同利用这些资源的工具和技术,云可以根据需要或使用云消费者的直接配置,立即动态地将信息技术资源分配给云消费者。这使云消费者能够自动或手动扩展其基于云的信息技术资源,以适应处理波动和峰值。同样,基于云的信息技术资源可以随着处理需求的减少而释放(自动或手动)。

云固有的内置特性为信息技术资源提供了灵活的可扩展性,这与前面提到的成比例的成本优势直接相关。除了自动缩减扩展带来的明显财务收益之外,IT 资源始终满足和满足不可预测的使用需求的能力避免了在达到使用阈值时可能发生的潜在业务损失。

提高可用性和可靠性

信息技术资源的可用性和可靠性与有形的业务利益直接相关。停机限制了信息技术资源为其客户开放业务的时间,从而限制了其使用和创收潜力。在大量使用期间,没有立即纠正的运行时故障可能会产生更大的影响。不仅信息技术资源无法响应客户的请求,而且其意外故障也会降低客户的整体信心。

典型云环境的一个标志是其固有的能力,即提供广泛的支持,以提高基于云的信息技术资源的可用性,从而最大限度地减少甚至消除中断,并提高其可靠性,从而最大限度地减少运行时故障条件的影响。

具体来说:

  • 可用性提高的信息技术资源可以在更长的时间内访问(例如,24 小时中的 22 小时)。云提供商通常提供弹性的信息技术资源,能够保证高水平的可用性。
  • 可靠性提高的信息技术资源能够更好地避免异常情况并从中恢复。云环境的模块化体系结构提供了广泛的故障转移支持,从而提高了可靠性。

组织在考虑租赁基于云的服务和信息技术资源时,仔细检查云提供商提供的服务级别协议非常重要。尽管许多云环境能够提供非常高的可用性和可靠性,但归根结底是服务级别协议中做出的保证,这些保证通常代表了他们的实际合同义务。

风险和挑战

介绍并研究了几个最关键的云计算挑战,主要涉及使用公共云中的信息技术资源的云消费者。

安全漏洞增加

将业务数据迁移到云意味着数据安全的责任将由云提供商分担。远程使用信息技术资源需要云消费者扩大信任边界,以包括外部云。由于第三方云提供商通常会在经济实惠或方便的地理位置建立数据中心,因此很难正确解决多区域合规性和法律问题。当由公共云托管时,云消费者通常不知道其信息技术资源和数据的物理位置。对于某些组织来说,这可能会对指定数据隐私和存储策略的行业或政府法规造成严重的法律问题。

多重边界的存在使得很难建立跨越这种信任边界而不引入漏洞的可行安全架构,除非云消费者和云提供商碰巧支持相同或兼容的安全框架。使用公共云实现这样的兼容性并不容易。

信任边界重叠的另一个后果与云提供商对云消费者数据的特权访问有关。数据的安全程度现在受到云消费者和云提供商应用的安全控制和策略的限制。此外,由于基于云的信息技术资源通常是共享的,因此不同云消费者的信任边界可能会重叠。

信任边界的重叠和数据暴露的增加会为恶意的云消费者(人工和自动化)提供更大的机会来攻击信息技术资源并窃取或损坏业务数据。想象一个场景,两个访问相同云服务的组织需要将各自的信任边界扩展到云,导致信任边界重叠。对于云提供商来说,提供满足云服务消费者安全需求的安全机制可能是一项挑战。

运营治理控制减少

云消费者通常被分配了比内部 IT 资源更低的治理控制级别。这可能会引入与云提供商如何运营其云相关的风险,以及云和云消费者之间通信所需的外部连接。

云提供商之间的可移植性有限

由于云计算行业缺乏既定的行业标准,公共云通常在不同程度上是专有的。对于拥有依赖于这些专有环境的定制解决方案的云消费者来说,从一个云提供商转移到另一个云提供商可能很有挑战性。

角色和界限

根据组织和人员与云及其托管的信息技术资源的关系和/或交互方式,他们可以承担不同类型的预定义角色。每个即将加入的角色都参与并履行与基于云的活动相关的职责。以下部分定义了这些角色,并确定了它们的主要交互。

云提供商

提供基于云的信息技术资源的组织是云提供商。当承担云提供商的角色时,组织负责根据商定的服务级别协议条款向云消费者提供云服务。云提供商还承担任何必要的管理和行政职责,以确保整个云基础架构的平稳持续运行。

云提供商通常拥有可供云消费者租赁的信息技术资源;但是,一些云提供商也转售从其他云提供商租赁的 IT 资源。

云消费者

一个云消费者是一个组织(或一个人),它与云提供商有正式的合同或安排来使用云提供商提供的信息技术资源。具体来说,云消费者使用云服务消费者来访问云服务。

云服务所有者

合法拥有云服务的个人或组织称为云服务所有者。云服务所有者可以是云消费者,也可以是拥有云服务所在云的云提供商。

云资源管理员

云资源管理员是负责管理基于云的信息技术资源(包括云服务)的个人或组织。云资源管理员可以是(或属于)云服务所在云的云消费者或云提供商。或者,它可以是(或属于)签约管理基于云的信息技术资源的第三方组织。

其他角色

NIST 云计算参考体系结构定义了以下补充角色:

  • 云审计员:对云环境进行独立评估的第三方(通常是认证的)承担云审计员的角色。与此角色相关的典型职责包括评估安全控制、隐私影响和性能。云审计员角色的主要目的是提供对云环境的公正评估(以及可能的认可),以帮助加强云消费者和云提供商之间的信任关系。
  • 云经纪人:这个角色由一方承担,该方负责管理和协商云消费者和云提供商之间的云服务使用。云经纪人提供的中介服务包括服务中介、聚合和套利。
  • 云运营商:负责在云消费者和云提供商之间提供有线级连接的一方承担云运营商的角色。这一角色通常由网络和电信提供商承担。

组织边界

组织边界代表组织拥有和管理的一组信息技术资源的物理边界。

信任边界

当组织承担云消费者的角色来访问基于云的信息技术资源时,它需要将其信任扩展到组织的物理边界之外,以包括云环境的一部分。

云特征

信息技术环境需要一组特定的特征,以便能够以有效的方式远程配置可扩展和可测量的信息技术资源。这些特征需要存在到有意义的程度,信息技术环境才能被视为有效的云。

以下六个特定特征是大多数云环境共有的:

  • 按需使用
  • 无处不在的访问
  • 多租户(和资源池)
  • 弹性
  • 计量使用
  • 跳回

按需使用

云消费者可以单方面访问基于云的信息技术资源,让云消费者可以自由地自行调配这些信息技术资源。配置完成后,可以自动使用自行调配的信息技术资源,从而减少云消费者或云提供商的人力投入。这导致按需使用环境。也称为按需自助使用,这一特性支持主流 Clouds 中基于服务和使用驱动的功能。

无处不在的访问

无处不在的访问代表云服务被广泛访问的能力。为云服务建立无处不在的访问可能需要对一系列设备、传输协议、接口和安全技术的支持。启用这种级别的访问通常要求云服务架构针对不同云服务消费者的特定需求进行定制。

多租户(和资源池)

一个软件程序的特点是它的一个实例能够为不同的用户(租户)服务,因此每个用户都是相互隔离的,这种特点被称为多租户。云提供商通过使用经常依赖虚拟化技术的多租户模式,将其 IT 资源集中起来为多个云服务消费者提供服务。通过使用多租户技术,可以根据云服务消费者的需求动态分配和重新分配信息技术资源。

弹性

弹性是云的自动化能力,可根据运行时条件的需要或云消费者或云提供商的预先确定透明地扩展信息技术资源。弹性通常被认为是采用云计算的核心理由,主要是因为它与投资减少和成比例的成本收益密切相关。拥有大量信息技术资源的云提供商可以提供最大范围的弹性。

计量使用

衡量的使用特征代表云平台跟踪其信息技术资源使用情况的能力,主要是由云消费者跟踪。根据所测量的内容,云提供商只能根据实际使用的信息技术资源和/或获得信息技术资源访问权限的时间框架向云消费者收费。在这种情况下,计量使用与按需特性密切相关。

跳回

弹性计算是一种故障转移形式,它将信息技术资源的冗余实施分布在各个物理位置。可以预先配置信息技术资源,这样,如果一个资源不足,处理将自动移交给另一个冗余实现。在云计算中,弹性的特征可以指同一云中(但在不同的物理位置)或跨多个云的冗余信息技术资源。

云交付模型

云交付模型代表了由云提供商提供的特定的、预先打包的信息技术资源组合。三种常见的云交付模型已经广泛建立并正式化:

  • IaaS
  • PaaS
  • SaaS

基础设施即服务

IaaS 交付模型代表了一个独立的信息技术环境,包括以基础设施为中心的信息技术资源,这些资源可以使用基于云服务的界面和工具来访问和管理。这种环境可以包括硬件、网络、连接、操作系统和其他原始信息技术资源。与传统的托管或外包环境不同,借助 IaaS,IT 资源通常被虚拟化并打包成包,以简化基础架构的运行时扩展和定制。

IaaS 环境的一般目的是为云消费者提供对其配置和使用的高度控制和责任。IaaS 提供的信息技术资源通常没有预先配置,将管理责任直接放在云消费者身上。这种模式由云消费者使用,他们需要对他们打算创建的基于云的环境进行高度控制。

有时,云提供商会与其他云提供商签订 IaaS 产品合同,以扩展他们自己的云环境。不同云提供商提供的 IaaS 产品所提供的信息技术资源的类型和品牌可能会有所不同。通过 IaaS 环境提供的 IT 资源通常是作为新初始化的虚拟实例提供的。虚拟服务器是典型 IaaS 环境中的核心和主要 IT 资源。

平台即服务

平台即服务交付模型代表了一个预定义的、随时可用的环境,通常由已经部署和配置的信息技术资源组成。具体来说,平台即服务依赖于(主要由)现成环境的使用,该环境建立了一套预打包的产品和工具,用于支持定制应用的整个交付生命周期。

云消费者使用并投资于平台即服务环境的常见原因:

  • 出于可扩展性和经济性的目的,云消费者希望将内部部署环境扩展到云中
  • 云消费者使用现成的环境来完全替代内部环境
  • 云消费者希望成为云提供商,并部署自己的云服务供其他外部云消费者使用

通过在现成的平台中工作,云消费者可以免除设置和维护使用 IaaS 模型提供的基础架构 IT 资源的管理负担。

软件即服务

定位为共享云服务并作为产品或通用工具提供的软件程序代表了 SaaS 产品的典型特征。SaaS 交付模式通常用于使一系列云消费者广泛获得可重用的云服务(通常是商业上的)。围绕 SaaS 产品存在一个完整的市场,可以租赁和用于不同的目的,并使用不同的条款。

云消费者通常被授予对 SaaS 实施非常有限的管理控制权。它通常由云提供商提供,但也可以由承担云服务所有者角色的任何实体合法拥有。例如,在使用和使用 PaaS 环境时充当云消费者的组织可以构建云服务,并决定将其部署在与 SaaS 产品相同的环境中。由于基于 SaaS 的云服务可供在使用云服务时充当云消费者的其他组织使用,因此同一组织实际上承担了云提供商的角色。

结合云交付模型

三个基本的云交付模型组成了一个自然的供应层次结构,允许探索模型组合应用的机会。接下来的章节将简要强调与两种常见组合相关的注意事项。

IaaS + PaaS

平台即服务环境将建立在与物理和虚拟服务器以及 IaaS 环境中提供的其他 IT 资源相当的底层基础架构之上。

国际会计准则+部分会计准则+ SaaS

所有三种云交付模式都可以结合起来,建立相互依赖的信息技术资源层。例如,通过添加到前面的分层体系结构中,云消费者组织可以使用 PaaS 环境提供的现成环境来开发和部署自己的 SaaS 云服务,然后将其作为商业产品提供。

以下是 IaaS、PaaS 和 SaaS 中的所有层:

云部署模型

云部署模型代表特定类型的云环境,主要通过所有权、规模和访问权限来区分。

以下部分描述了四种常见的云部署模型:

  • 公共云
  • 社区云
  • 私有云
  • 混合云

公共云

公共云是由第三方云提供商拥有的可公开访问的云环境。公共云上的信息技术资源通常使用前面描述的云交付模型进行调配,通常以一定的成本提供给云消费者,或者使用其他途径(如广告)进行商业化。

云提供商负责创建和持续维护公共云及其信息技术资源。在接下来的章节中探讨的许多场景和架构都涉及公共云以及使用公共云的信息技术资源的提供者和消费者之间的关系。

社区云

社区云类似于公共云,只是它的访问仅限于特定的云消费者社区。社区云可能由社区成员共同拥有,也可能由第三方云提供商拥有,该提供商为公共云提供有限的访问权限。社区的成员云消费者通常分担定义和发展社区云的责任。

社区成员并不一定能保证访问或控制云的所有信息技术资源。除非得到社区的允许,否则社区之外的各方通常不会被授予访问权限。

私有云

私有云由单个组织拥有。私有云使组织能够使用云计算技术作为一种手段,集中组织的不同部分、位置或部门对信息技术资源的访问。当私有云作为受控环境存在时,在第 3 章利用 MapReduce 进行大数据处理风险和挑战一节中描述的问题往往不适用。

私有云的使用可以改变组织和信任边界的定义和应用方式。私有云环境的实际管理可能由内部或外包人员执行。

使用私有云,同一组织在技术上既是云消费者,也是云提供商。为了区分这些角色:

  • 一个单独的组织部门通常承担调配云的责任(因此承担云提供商的角色)
  • 需要访问私有云的部门承担云消费者的角色

A Cloud service consumer in the organization's on-premise environment accesses a Cloud service hosted on the same organization's private Cloud using a virtual private network.

在私有云环境中,正确使用内部部署和基于云这两个术语非常重要。即使私有云可能实际驻留在组织的场所,但只要云消费者可以远程访问它所承载的信息技术资源,它仍然被视为基于云的。因此,作为云消费者的部门在私有云之外托管的信息技术资源被视为与基于私有云的信息技术资源相关的本地资源。

混合云

混合云是由两个或多个不同的云部署模型组成的云环境。例如,云消费者可能会选择将处理敏感数据的云服务部署到私有云,将其他不太敏感的云服务部署到公共云。这种组合的结果是一种混合部署模式。

此处显示的是一个使用混合云架构的组织,该架构同时利用了私有云和公共云:

由于云环境中的潜在差异以及管理责任通常由私有云提供商和公共云提供商分担的事实,混合部署体系结构的创建和维护可能非常复杂和具有挑战性。

摘要

在本章中,我们讨论了云计算以及用于理解和实施云计算的关键术语。

在下一章中,我们将探索亚马逊最受欢迎的云提供商之一:AWS。

十二、使用亚马逊网络服务

本章向您介绍了 AWS 及其服务的概念,当您在 AWS 云中设置 Hadoop 集群时,这些概念对于使用弹性 MapReduce ( EMR )执行大数据分析非常有用。我们将查看 AWS 提供的关键组件和服务,并了解如何利用 AWS 组件和服务提供的各种功能。

简而言之,本章将涵盖以下主题:

  • 亚马逊弹性计算云
  • 从一个 AMI 启动多个实例
  • 什么是 AWS Lambda?
  • 亚马逊 S3 简介
  • 亚马逊 DynamoDB
  • 亚马逊人体运动数据流
  • AWS 胶水
  • 亚马逊 EMR

亚马逊弹性计算云

亚马逊弹性计算云 ( 亚马逊 EC2 )是一种在云上提供安全、可调整计算能力的网络服务。它旨在使网络规模的云计算对开发人员来说更容易。

亚马逊 EC2 的简单网络服务界面让您可以轻松获得和配置容量。它为您提供了对计算资源的完全控制,并让您使用亚马逊的计算环境。Amazon EC2 将获取和引导新服务器实例所需的时间减少到几分钟,使您能够随着计算需求的变化快速扩展容量(向上和向下)。亚马逊 EC2 允许您节省计算成本,因为您只需为实际使用的容量付费。亚马逊 EC2 为开发人员提供了构建抗故障应用的工具,并将它们与常见的故障场景隔离开来。

弹性网络规模计算

亚马逊 EC2 使您能够在几分钟内增加或减少容量。您可以同时委托一个或多个服务器实例。您也可以使用亚马逊 EC2 自动扩展来保持您的 EC2 车队的可用性,并根据您的需求自动上下扩展您的车队,以最大限度地提高性能并降低成本。要扩展多个服务,您可以使用 AWS 自动扩展

完全控制操作

您可以完全控制您的实例,包括根访问,并且能够像处理任何机器一样与它们进行交互。您可以停止任何实例,同时保留引导分区上的数据,然后使用 web 服务 API 重新启动同一个实例。实例可以使用 web 服务 API 远程重启,并且您还可以访问它们的控制台输出。

灵活的云托管服务

您可以选择多种实例类型、操作系统和软件包。Amazon EC2 允许您选择内存、CPU、实例存储的配置,以及最适合您选择的操作系统和应用的引导分区大小。例如,操作系统的选择包括许多 Linux 发行版和微软视窗服务器。

综合

Amazon EC2 集成了大多数 AWS 服务,如Amazon Simple Storage Service(Amazon S3)Amazon****关系数据库服务 ( Amazon RDS )和 Amazon 虚拟私有云(Amazon****VPC),为大范围的计算、查询处理和云存储提供完整、安全的解决方案

高可靠性

亚马逊 EC2 提供了一个高度可靠的环境,可以快速、可预测地委托更换实例。该服务在亚马逊成熟的网络基础设施和数据中心内运行。亚马逊 EC2 服务级别协议 ( 服务级别协议)为每个亚马逊 EC2 地区提供 99.99%的可用性。

安全

AWS 的云安全是最高优先级。作为 AWS 客户,您将受益于专为满足对安全性最敏感的组织的需求而构建的数据中心和网络架构。亚马逊 EC2 与亚马逊 VPC 合作,为您的计算资源提供安全和强大的网络功能。

便宜的

亚马逊 EC2 向你传递了亚马逊规模带来的财务收益。您为实际消耗的计算能力支付的费率非常低。详见亚马逊 EC2 的实例购买选项:https://aws.amazon.com/ec2/pricing/

易于启动

有几种方法可以开始使用亚马逊 EC2。您可以使用 AWS 管理控制台、AWS 命令行工具,这些工具可通过命令行界面或使用 AWS 软件开发工具包访问。AWS 易于启动和操作。要了解更多信息,请访问我们位于https://aws.amazon.com/getting-started/tutorials/的教程。

实例和亚马逊机器图像

一个亚马逊机器映像 ( AMI )是一个包含软件配置(例如,操作系统、应用服务器和应用)的模板。从 AMI 中,您启动一个实例,它是 AMI 的副本,在云中作为虚拟服务器运行。您可以启动一个 AMI 的多个实例,如下图所示:

启动一个 AMI 的多个实例

您的实例会一直运行,直到您停止或终止它们,或者直到它们失败。如果一个实例失败,您可以从 AMI 启动一个新实例。

例子

您可以从单个 AMI 启动不同类型的实例。实例类型本质上决定了用于实例的主机的硬件。每种实例类型都提供不同的计算和内存能力。根据计划在实例上运行的应用或软件所需的内存量和计算能力选择实例类型。有关每个 Amazon EC2 实例类型的硬件规格的更多信息,请参见此链接https://aws.amazon.com/ec2/instance-types/处的 Amazon EC2 实例。

启动一个实例后,它看起来像一个传统的主机,您可以像任何计算机一样与它交互。您可以完全控制您的实例;可以使用sudo运行需要 root 权限的命令。

埃米斯

亚马逊网络服务 ( AWS )发布了许多包含公共使用的通用软件配置的 AMIs。此外,AWS 开发人员社区的成员已经发布了他们自己的定制 AMIs。您也可以创建自己的自定义 AMI 或 AMIs 这样做使您能够快速轻松地启动拥有所需一切的新实例。例如,如果您的应用是网站或 web 服务,您的 AMI 可能包括 web 服务器、相关的静态内容和动态页面的代码。因此,从这个 AMI 启动一个实例后,您的 web 服务器就会启动,您的应用就可以接受请求了。

所有 AMI 都被分类为由亚马逊 EBS 支持,这意味着从 AMI 启动的实例的根设备是亚马逊 EBS 卷,或者由实例存储支持,这意味着从 AMI 启动的实例的根设备是从存储在亚马逊 S3 的模板创建的实例存储卷。

区域和可用性区域

亚马逊 EC2 托管在全球多个地点。这些位置由区域和可用性区域组成。每个地区都是一个独立的地理区域。每个区域都有多个独立的位置,称为可用性区域。Amazon EC2 为您提供了在多个位置放置实例和数据等资源的能力。资源不会跨地区复制,除非您特别这样做。

亚马逊运营着最先进、高度可用的数据中心。虽然很少发生,但可能会发生影响同一位置实例可用性的故障。如果您将所有实例托管在受此类故障影响的单个位置,则没有一个实例可用。

区域和可用性区域概念

每个地区都完全独立。每个可用性区域都是隔离的,但是一个区域中的可用性区域通过低延迟链路连接在一起。下图说明了区域和可用性区域之间的关系:

地区

每个亚马逊 EC2 区域都被设计成与其他亚马逊 EC2 区域完全隔离。这实现了最大可能的容错性和稳定性。

当您查看资源时,您只会看到与您指定的区域相关联的资源。这是因为区域是相互隔离的,我们不会跨区域自动复制资源。

可用性区域

启动实例时,您可以选择可用性区域或为您分配一个可用性区域。如果您将实例分布在多个可用性区域,而一个实例出现故障,您可以设计应用,以便另一个可用性区域中的实例可以处理请求。

您还可以使用弹性 IP 地址,通过将地址快速重新映射到另一个可用性区域中的实例,来屏蔽一个可用性区域中实例的故障。更多信息见弹性 IP 地址此链接为https://docs . AWS . Amazon . com/AWSEC2/latest/user guide/Elastic-IP-address-EIP . html

可用区域

您的帐户决定了您可以使用的地区。例如,一个 AWS 帐户提供多个区域,这样您就可以在满足您要求的位置启动 Amazon EC2 实例。例如,您可能希望在欧洲启动实例,以更接近您的欧洲客户或满足法律要求。

AWS 政府云(美国)帐户仅提供对 AWS 政府云(美国)区域的访问。更多信息见 AWS GovCloud(美国)地区

亚马逊 AWS(中国)帐户仅提供中国(北京)地区的访问权限。

下表列出了 AWS 帐户提供的区域:

| 地区名称 | 地区 | 端点 | 草案 |
| 美国东部(俄亥俄州) | 美国东部 2 | rds.us-east-2.amazonaws.com | HTTPS |
| 美国东部(北弗吉尼亚) | 美国东部 1 | rds.us-east-1.amazonaws.com | HTTPS |
| 美国西部(北加利福尼亚) | 美国-西部-1 | rds.us-west-1.amazonaws.com | HTTPS |
| 美国西部(俄勒冈州) | 美国西部 2 | rds.us-west-2.amazonaws.com | HTTPS |
| 亚太地区(东京) | AP-东北-1 | rds.ap-northeast-1.amazonaws.com | HTTPS |
| 亚太地区(首尔) | AP-东北-2 | rds.ap-northeast-2.amazonaws.com | HTTPS |
| 亚太地区(大阪-本地) | AP-东北-3 | rds.ap-northeast-3.amazonaws.com | HTTPS |
| 亚太地区(孟买) | ap-south-1 | rds.ap-south-1.amazonaws.com | HTTPS |
| 亚太地区(新加坡) | AP-东南-1 | rds.ap-southeast-1.amazonaws.com | HTTPS |
| 亚太地区(悉尼) | AP-东南-2 | rds.ap-southeast-2.amazonaws.com | HTTPS |
| 加拿大(中部) | ca-中央-1 | rds.ca-central-1.amazonaws.com | HTTPS |
| 中国北京 | cn-north-1 | rds.cn-north-1.amazonaws.com.cn | HTTPS |
| 中国(宁夏) | cn-西北-1 | rds.cn-northwest-1.amazonaws.com.cn | HTTPS |
| 我(法兰克福) | 我-中央-1 | rds.eu-central-1.amazonaws.com | HTTPS |
| 欧盟(爱尔兰) | 我-西-1 | rds.eu-west-1.amazonaws.com | HTTPS |
| 欧盟(伦敦) | 我-西-2 | rds.eu-west-2.amazonaws.com | HTTPS |
| 我(巴黎) | 我-西-3 | rds.eu-west-3.amazonaws.com | HTTPS |
| 南美洲(圣保罗) | sa-east-1 | rds.sa-east-1.amazonaws.com | HTTPS |

区域和端点

使用命令行界面或应用编程接口操作处理实例时,必须指定其区域端点。有关亚马逊 EC2 区域和端点的更多信息,请参见此链接的区域和端点:https://docs.aws.amazon.com/general/latest/gr/rande.html

实例类型

启动实例时,您指定的实例类型决定了用于实例的主机的硬件。每种实例类型都提供不同的计算、内存和存储能力,并根据这些能力分组到一个实例系列中。根据您计划在实例上运行的应用或软件的要求,选择实例类型。

标签基础知识

标签使您能够以不同的方式对您的 AWS 资源进行分类,例如,按目的、所有者或环境。当您有许多相同类型的资源时,这很有用—您可以根据分配给特定资源的标签快速识别该资源。每个标记由一个键和一个可选值组成,这两个值都是您定义的。例如,您可以为您的帐户的 Amazon EC2 实例定义一组标签,帮助您跟踪每个实例的所有者和堆栈级别。

Amazon EC2 密钥对

亚马逊 EC2 使用公钥密码对登录信息进行加密和解密。公钥加密使用公钥加密一段数据,如密码,然后收件人使用私钥解密数据。公钥和私钥被称为密钥对

针对 Linux 实例的 Amazon EC2 安全组

安全组充当虚拟防火墙,控制一个或多个实例的流量。启动实例时,您可以将一个或多个安全组与该实例相关联。您可以向每个安全组添加允许流量进出其关联实例的规则。您可以随时修改安全组的规则;短时间后,新规则将自动应用于与安全组关联的所有实例。当您决定是否允许流量到达某个实例时,您可以评估与该实例关联的所有安全组的所有规则。

弹性 IP 地址

弹性 IP 地址是为动态云计算设计的静态 IPv4 地址。弹性 IP 地址与您的 AWS 帐户相关联。使用弹性 IP 地址,您可以通过快速将地址重新映射到帐户中的另一个实例来掩盖实例或软件的故障。

亚马逊 EC2 和亚马逊虚拟私有云

亚马逊 VPC 使您能够在 AWS 云中自己的逻辑隔离区域定义一个虚拟网络,称为 VPC。您可以将 AWS 资源(如实例)启动到您的 VPC 中。您的 VPC 非常类似于传统的网络,您可以在自己的数据中心运行,具有使用 AWS 可扩展基础架构的优势。您可以配置您的 VPC,或选择其 IP 地址范围,创建子网,并配置路由表、网络网关和安全设置。您可以将 VPC 的实例连接到互联网。您可以将您的 VPC 连接到您自己的公司数据中心,使 AWS 云成为您的数据中心的扩展。为了保护每个子网中的资源,您可以使用多个安全层,包括安全组和网络访问控制列表。更多信息,请参见https://aws.amazon.com/documentation/vpc/亚马逊 VPC 用户指南

亚马逊弹性积木商店

亚马逊 弹性块存储 ( 亚马逊 EBS )提供块级存储卷,供 EC2 实例使用。EBS 卷是高度可用和可靠的存储卷,可以连接到同一可用性区域中的任何正在运行的实例。连接到 EC2 实例的 EBS 卷作为独立于实例生命周期的存储卷公开。有了亚马逊 EBS,你只需为你使用的东西付费。有关亚马逊电子商务服务定价的更多信息,请参见亚马逊电子商务服务页面https://aws.amazon.com/ebs/预计成本部分。

当数据必须能够快速访问并且需要长期持久性时,建议使用亚马逊 EBS。EBS 卷特别适合用作文件系统、数据库或任何需要精细更新和访问原始、未格式化的块级存储的应用的主存储。Amazon EBS 非常适合依赖随机读写的数据库式应用,也非常适合执行长时间连续读写的吞吐量密集型应用。

Amazon EC2 实例存储

实例存储为实例提供临时块级存储。该存储位于物理连接到主机的磁盘上。实例存储非常适合于经常变化的信息的临时存储,如缓冲区、缓存、暂存数据和其他临时内容,或者适合于跨一系列实例复制的数据,如负载平衡的 web 服务器池。

实例存储由一个或多个作为块设备公开的实例存储卷组成。实例存储的大小以及可用设备的数量因实例类型而异。虽然实例存储专用于特定实例,但磁盘子系统在主机上的实例之间共享。

什么是 AWS Lambda?

AWS Lambda 是一种计算服务,它允许您运行代码,而无需配置或管理服务器。AWS Lambda 仅在需要时执行您的代码,并自动扩展,从每天几个请求扩展到每秒几千个请求。您只需为消耗的计算时间付费,当您的代码没有运行时,无需付费。使用 AWS Lambda,您可以为几乎任何类型的应用或后端服务运行代码,所有这些都无需管理。AWS Lambda 在高可用性计算基础架构上运行您的代码,并执行计算资源的所有管理,包括服务器和操作系统维护、容量调配和自动扩展、代码监控和日志记录。你所需要做的就是用 AWS Lambda 支持的语言之一提供你的代码(目前是 Node.js、Java、C#、Go 和 Python)。

您可以使用 AWS Lambda 运行您的代码来响应事件,例如对亚马逊 S3 桶或亚马逊 DynamoDB 表中的数据的更改;使用亚马逊应用编程接口网关运行您的代码来响应 HTTP 请求;或者使用使用 AWS SDKs 进行的 API 调用来调用您的代码。有了这些功能,您可以使用 Lambda 轻松地为 Amazon S3 和 Amazon DynamoDB 等 AWS 服务构建数据处理触发器,以处理存储在 Kinesis 中的流数据,或者创建自己的后端,在 AWS 规模上运行,提供卓越的性能和必要的系统安全性。

您还可以构建由事件触发的函数组成的无服务器应用,并使用 AWS 代码管道AWS 代码构建自动部署它们。有关更多信息,请参见部署基于 Lambda 的应用

什么时候应该用 AWS Lambda?

AWS Lambda 是许多应用场景的理想计算平台,前提是您可以用 AWS Lambda 支持的语言(即 Node.js、Java、Go、C#和 Python)编写应用代码,并在 AWS Lambda 标准运行时环境和 Lambda 提供的资源内运行。

亚马逊 S3 简介

亚马逊 S3 在全球最大的云基础设施上运行,从头开始构建,以实现 99.99999999999%耐用性的客户承诺。数据自动分布在一个自动气象站区域内至少三个地理位置分开的物理设施上,亚马逊 S3 也可以自动将数据复制到任何其他自动气象站区域。

https://aws.amazon.com/了解更多关于 AWS 全球云基础设施的信息。

亚马逊 S3 入门

亚马逊 S3 是互联网的存储。您可以使用亚马逊 S3 随时随地存储和检索任意数量的数据。您可以使用 AWS 管理控制台完成这些任务,这是一个简单直观的网络界面。本指南向您介绍了亚马逊 S3 以及如何使用 AWS 管理控制台来管理亚马逊 S3 提供的存储空间。

如今的公司需要能够轻松、安全地大规模收集、存储和分析他们的数据。亚马逊 S3 是一种对象存储,旨在存储和检索来自任何地方的任何数量的数据——网站和移动应用、企业应用以及来自物联网传感器或设备的数据,并存储每个行业市场领导者使用的数百万个应用的数据。S3 提供了全面的安全和合规能力,甚至可以满足最严格的法规要求。它让客户能够灵活地管理数据,以实现成本优化、访问控制和法规遵从性。S3 提供就地查询功能,允许您直接对 S3 的静态数据进行强大的分析。亚马逊 S3 是支持度最高的存储平台,拥有最大的独立软件开发商解决方案生态系统和系统集成商合作伙伴。

全面的安全和合规能力

亚马逊 S3 是唯一支持三种不同加密形式的云存储平台。S3 提供了与 AWS 云跟踪的复杂集成,以记录、监控和保留存储应用编程接口调用活动进行审计。亚马逊 S3 是唯一一个拥有亚马逊 Macie 的云存储平台,它使用机器学习来自动发现、分类和保护 AWS 中的敏感数据。S3 支持安全标准和合规认证,包括 PCI-DSS、HIPAA/HITECH、FedRAMP、欧盟数据保护指令和 FISMA,帮助满足全球几乎每个监管机构的合规要求。

https://aws.amazon.com/security/了解更多安全信息。

https://aws.amazon.com/compliance/了解更多合规信息。

查询到位

亚马逊 S3 允许您对数据运行复杂的大数据分析,而无需将数据转移到单独的分析系统中。亚马逊雅典娜为任何懂 SQL 的人提供了对大量非结构化数据的按需查询访问。亚马逊红移光谱让你运行跨越你的数据仓库和 S3 的查询。只有 AWS 提供了 Amazon S3 Select(目前处于测试预览阶段),这是一种仅从 S3 对象中检索所需数据子集的方法,可以将经常从 S3 访问数据的大多数应用的性能提高高达 400%。

https://AWS . Amazon . com/blogs/AWS/Amazon-红移-光谱-exabyte-scale-in-query-of-S3-data/了解更多关于就地查询的信息。

灵活管理

亚马逊 S3 提供最灵活的存储管理和管理功能。存储管理员可以对数据使用趋势进行分类、报告和可视化,以降低成本并提高服务级别。可以使用唯一的、可定制的元数据来标记对象,以便客户可以针对每个工作负载分别查看和控制存储消耗、成本和安全性。S3 清单功能提供有关对象及其元数据的计划报告,用于维护、法规遵从性或分析操作。S3 还可以分析对象访问模式,以构建自动化分层、删除和保留的生命周期策略。由于亚马逊 S3 与 AWS Lambda 合作,客户可以记录活动、定义警报和调用工作流,而无需任何额外的基础架构。

https://aws.amazon.com/s3/了解更多关于 S3 存储管理的信息。

拥有最大生态系统的最受支持的平台

除了与大多数 AWS 服务集成之外,亚马逊 S3 生态系统还包括多个咨询系统集成商和独立软件供应商合作伙伴,每个月都有更多合作伙伴加入。AWS 市场提供 35 个类别和 3,500 多个软件清单,来自 1,100 多家独立软件开发商,这些软件预先配置为部署在 AWS 云上。 AWS 合作伙伴网络 ( APN )合作伙伴已经调整了他们的服务和软件,以便与 S3 合作提供备份和恢复、归档和灾难恢复等解决方案。

https://aws.amazon.com/backup-recovery/partner-solutions/了解更多关于 AWS 存储合作伙伴的信息。

轻松灵活的数据传输

您可以从众多选项中选择,将您的数据传输到(或传输出)亚马逊 S3。S3 简单可靠的 API 使得在互联网上传输数据变得很容易。亚马逊 S3 传输加速非常适合跨大地理距离的数据上传。AWS 直连为使用专用网络连接将大量数据移动到 AWS 提供了一致的高带宽和低延迟数据传输。您可以使用 AWS 雪球和 AWS 雪球边缘设备进行千兆字节级的数据传输,或者使用 AWS 雪地摩托进行更大的数据集。 AWS 存储网关为您提供了一个物理或虚拟数据传输设备,可在内部使用,轻松地将卷或文件移动到 AWS 云中。

https://aws.amazon.com/cloud-migration/了解更多关于云数据迁移的信息。

备份和恢复

亚马逊 S3 为备份和归档您的关键数据提供了一个高度耐用、可扩展和安全的目的地。您可以使用 S3 的版本控制功能来保护您存储的数据。您还可以定义生命周期规则,将不常使用的数据迁移到 S3 标准非频繁访问,并将对象集归档到亚马逊冰川。

https://aws.amazon.com/backup-restore/了解更多关于备份和恢复的信息。

数据存档

亚马逊 S3 和亚马逊冰川提供了一系列存储类别,以满足受监管行业的法规遵从性归档需求,或者满足需要快速、不经常访问归档数据的组织的活动归档需求。亚马逊冰川保险库锁提供一次写入多次读取 ( WORM )存储,以满足记录保留的合规性要求。生命周期策略使数据从亚马逊 S3 转移到亚马逊冰川变得简单,有助于基于客户定义的策略自动进行转移。

https://aws.amazon.com/archive/了解更多关于数据归档的信息。

数据湖和大数据分析

无论您存储的是医药或金融数据,还是照片和视频等多媒体文件,亚马逊 S3 都可以作为您的大数据分析数据湖。AWS 提供全面的服务组合,通过降低成本、扩展以满足需求和提高创新速度来帮助您管理大数据。

更多关于数据湖和大数据分析的信息,请访问https://aws . Amazon . com/blogs/big-data/介绍 AWS 上的数据湖解决方案/

混合云存储

AWS 存储网关帮助您构建混合云存储,利用亚马逊 S3 的耐用性和规模来扩展您现有的本地存储环境。使用它将工作负载从您的站点爆发到云中进行处理,然后将结果带回来。将主存储中较冷或价值较低的数据分层到云中,以降低成本并扩大您的内部投资。或者,作为备份或迁移项目的一部分,简单地使用它将数据增量移动到 S3。

https://aws.amazon.com/enterprise/hybrid/了解更多关于混合云存储的信息。

云原生应用数据

亚马逊 S3 提供高性能、高可用性的存储,使其易于扩展和维护经济高效的移动和基于互联网的快速运行的应用。借助 S3,您可以添加任意数量的内容,并从任何地方访问这些内容,因此您可以更快地部署应用并接触更多客户。

灾难恢复

亚马逊 S3 安全的全球基础设施提供了强大的灾难恢复解决方案,旨在提供卓越的数据保护。跨区域复制 ( CRR )会自动将每个 S3 对象复制到位于不同 AWS 区域的目标存储桶。

https://aws.amazon.com/disaster-recovery/了解更多关于灾难恢复的信息。

亚马逊 DynamoDB

Amazon DynamoDB 是一个完全托管的 NoSQL 数据库服务,提供快速和可预测的性能以及无缝的可扩展性。DynamoDB 让您卸下操作和扩展分布式数据库的管理负担,这样您就不必担心硬件配置、设置和配置、复制、软件修补或集群扩展。此外,DynamoDB 提供静态加密,这消除了保护敏感数据的操作负担和复杂性。更多信息,请参见休息时的亚马逊 DynamoDB 加密**https://docs . AWS . Amazon . com/Amazon DynamoDB/latest/developer guide/encryptiontrest . html

使用 DynamoDB,您可以创建数据库表来存储和检索任意数量的数据,并为任意级别的请求流量提供服务。您可以在不停机或不降低性能的情况下扩大或缩小表的吞吐量,并使用 AWS 管理控制台来监控资源利用率和性能指标。

亚马逊 DynamoDB 提供按需备份功能。它允许您创建表的完整备份,以便长期保留和存档,满足法规遵从性需求。有关更多信息,请参见动态数据库的按需备份和恢复

DynamoDB 允许您自动从表中删除过期的项目,以帮助您减少存储使用和存储不再相关的数据的成本。更多信息见生存时间

DynamoDB 会自动将表的数据和流量分散到足够数量的服务器上,以满足您的吞吐量和存储需求,同时保持一致和快速的性能。您的所有数据都存储在固态硬盘 ( 固态硬盘)上,并自动跨 AWS 区域的多个可用性区域进行复制,从而提供内置的高可用性和数据持久性。您可以使用全局表来保持 AWS 区域之间的电动数据库表同步。有关更多信息,请参见全球表格

亚马逊人体运动数据流

您可以使用亚马逊驱动数据流实时收集和处理大数据流记录。您将创建数据处理应用,称为亚马逊驱动数据流应用。典型的亚马逊驱动程序数据流应用从驱动程序数据流中读取数据作为数据记录。这些应用可以使用驱动程序客户端库,并且可以在亚马逊 EC2 实例上运行。处理后的记录可以发送到仪表板,用于生成警报、动态更改定价和广告策略,或者将数据发送到各种其他 AWS 服务。有关驱动程序数据流功能和定价的信息,请参见亚马逊驱动程序数据流。

除了亚马逊驱动数据消防软管,驱动数据流是驱动流数据平台的一部分。有关更多信息,请参见亚马逊驱动数据消防软管开发人员指南。有关 AWS 大数据解决方案的更多信息,请参见大数据。有关 AWS 流数据解决方案的更多信息,请参见什么是流数据?

我能用驱动力数据流做什么?

您可以使用驱动数据流进行快速和连续的数据获取和聚合。使用的数据类型包括信息技术基础设施日志数据、应用日志、社交媒体、市场数据馈送和网络点击流数据。因为数据获取和处理的响应时间是实时的,所以处理通常是轻量级的。

以下是使用驱动程序数据流的典型场景。

加速日志和数据馈送的获取和处理

您可以让生产者将数据直接推送到流中。例如,推送系统和应用日志,几秒钟后就可以进行处理。如果前端或应用服务器出现故障,这可以防止日志数据丢失。驱动数据流提供了加速的数据输入,因为在提交数据输入之前,您不会在服务器上对数据进行批处理。

实时指标和报告

您可以使用收集到驱动程序数据流中的数据进行简单的数据分析和实时报告。例如,当数据流入时,您的数据处理应用可以处理系统和应用日志的度量和报告,而不是等待接收批量数据。

实时数据分析

这结合了并行处理的能力和实时数据的价值。例如,您可以实时处理网站点击流,然后使用并行运行的多个不同的驱动数据流应用来分析网站可用性。

复杂流处理

您可以创建亚马逊驱动数据流应用和数据流的有向无环图 ( DAGs )。这通常包括将来自多个亚马逊驱动数据流应用的数据放入另一个流中,由不同的亚马逊驱动数据流应用进行下游处理。

使用驱动程序数据流的好处

虽然您可以使用运动学数据流来解决各种流数据问题,但一个常见的用途是实时聚合数据,然后将数据加载到数据仓库或地图缩减集群中。

为了确保持久性和弹性,数据被放入驱动程序数据流中。记录被放入流中的时间和它可以被检索的时间之间的延迟(放入-获取延迟 ) 小于 1 秒;亚马逊驱动数据流应用可以在添加数据后几乎立即开始消费数据流中的数据。驱动数据流的托管服务方面减轻了您创建和运行数据输入管道的操作负担。您可以创建流式地图缩减类型的应用,并且动态数据流的弹性使您能够向上或向下扩展数据流,这样您就永远不会在数据记录到期之前丢失它们。

多个亚马逊驱动数据流应用可以从一个流中消费数据,因此多个操作(如归档和处理)可以同时独立进行。例如,两个应用可以从同一个流中读取数据。第一个应用计算正在运行的聚合并更新一个 DynamoDB 表,第二个应用将数据压缩并归档到一个数据存储中,如亚马逊 S3。然后仪表板读取带有运行聚合的动态数据库表,以获得最新的报告。

AWS 胶水

AWS Glue 是一个完全托管的提取、转换和加载 ( ETL )服务,它使您的数据分类、清理、丰富和在各种数据存储之间可靠移动变得简单且经济高效。AWS Glue 由一个名为 AWS Glue 数据目录的中央数据存储库、一个自动生成 Python 代码的 ETL 引擎和一个灵活的调度程序组成,该调度程序处理依赖项解析、作业监控和失败时的作业重试/重试。AWS Glue 是无服务器的,因此没有基础架构可以设置或管理。

使用 AWS Glue 控制台发现数据、转换数据,并使其可用于搜索和查询。控制台调用底层服务来协调转换数据所需的工作。您还可以使用 AWS 胶水应用编程接口操作来与 AWS 胶水服务接口。使用熟悉的开发环境编辑、调试和测试您的 Python 或 Scala Apache Spark ETL 代码。

什么时候应该用 AWS 胶水?

您可以使用 AWS Glue 构建一个数据仓库来组织、清理、验证和格式化数据。您可以将 AWS 云数据转换并移动到您的数据存储中。您还可以将来自不同来源的数据加载到数据仓库中,以便进行定期报告和分析。通过将其存储在数据仓库中,您可以集成来自业务不同部分的信息,并为决策提供一个通用的数据来源。

当您构建数据仓库时,AWS Glue 简化了许多任务:

  • 发现关于数据存储的元数据并将其编目到一个中央目录中。
  • 您可以处理半结构化数据,如点击流或流程日志。
  • 使用计划的爬网程序中的表定义填充 AWS 粘附数据目录。
  • 爬虫调用分类器逻辑来推断数据的模式、格式和数据类型。这些元数据作为表格存储在 AWS 粘合数据目录中,并在您的 ETL 作业的创作过程中使用。
  • 生成 ETL 脚本来转换、展平和丰富从源到目标的数据。
  • 检测模式更改并根据您的偏好进行调整。
  • 根据计划或事件触发您的 ETL 作业。您可以自动启动作业,将数据移动到数据仓库中。触发器可用于创建作业之间的依赖流。
  • 收集运行时指标来监控数据仓库的活动。
  • 自动处理错误并重试。
  • 根据需要扩展资源,以运行您的作业。

当您对您的亚马逊 S3 数据湖运行无服务器查询时,您可以使用 AWS Glue。AWS Glue 可以对你的亚马逊 S3 数据进行编目,让你可以用亚马逊雅典娜和亚马逊红移光谱进行查询。使用爬虫,您的元数据与底层数据保持同步。雅典娜和红移光谱可以直接查询你的亚马逊 S3 数据湖使用 AWS 胶水数据目录。使用 AWS Glue,您可以通过一个统一的界面访问和分析数据,而无需将其加载到多个数据孤岛中。

您可以使用 AWS 胶水创建事件驱动的 ETL 管道。通过从 AWS Lambda 函数调用 AWS Glue ETL 作业,您可以在亚马逊 S3 有新数据时立即运行 ETL 作业。您也可以在 AWS 粘合数据目录中注册这个新数据集,作为您的 ETL 作业的一部分。

您可以使用 AWS Glue 来了解您的数据资产。您可以使用各种 AWS 服务存储数据,并且仍然可以使用 AWS 胶水数据目录维护数据的统一视图。查看数据目录以快速搜索和发现您拥有的数据集,并在一个中央存储库中维护相关元数据。数据目录还可以作为您的外部 Apache Hive Metastore 的替代插件。

亚马逊 EMR

Amazon EMR 是一个托管集群平台,它简化了在 AWS 上运行大数据框架,如 Apache Hadoop 和 Apache Spark,以处理和分析海量数据。通过使用这些框架和相关的开源项目,如 Apache Hive 和 Apache Pig,您可以为分析目的和商业智能工作负载处理数据。您还可以使用亚马逊电子病历将大量数据移入和移出其他 AWS 数据存储和数据库,例如亚马逊 S3 和亚马逊 DynamoDB。

亚马逊 EMR 提供了一个易于管理、快速且经济高效的 Hadoop 框架,以便跨可动态扩展的亚马逊 EC2 实例处理大量数据。您还可以在亚马逊 EMR 中运行其他流行的分布式框架,如 Apache Spark、HBase、Presto 和 Flink,并与其他 AWS 数据存储中的数据进行交互,如亚马逊 S3 和亚马逊 DynamoDB。

亚马逊 EMR 安全可靠地处理大量大数据用例,包括日志分析、网络索引、数据转换(ETL)、机器学习、金融分析、科学模拟和生物信息学。

实用的 AWS EMR 集群

在本练习中,您需要使用aws.amazon.com创建一个 AWS 帐户。

You will be charged to create and use an EMR cluster so please make sure you are OK with spending money on the cluster (typically $10 a day) and also terminate the cluster as soon as you are done.

登录后,您将看到如下屏幕截图所示的屏幕:

Figure: Screenshot of the screen that will appear after logging into your AWS account

通过选择 EMR 作为服务,您将进入如下屏幕截图所示的屏幕:

您可以通过选择各种选项来创建 EMR 集群,如下图所示:

对于 EMR 来说,密钥对是必不可少的,因此您可以打开一个新的选项卡并转到 AWS 控制台中的 EC2 服务:

以下是 EC2 仪表板:

通过选择左窗格中的“密钥对”选项,在 EC2 仪表板中创建新的密钥对:

以下是如何命名密钥对:

Make sure you copy the key pair, as you will not be able to do so at a later time

这是密钥对,您可以保存起来以备后用:

Figure: Screenshot showing the key pair that can be saved for later use

现在,使用您刚才生成的密钥对继续操作:

选择密钥对后,现在可以创建集群:

EMR cluster creation takes about 10 minutes.

这是 EMR 集群创建屏幕:

这是“摘要”选项卡,显示集群详细信息:

这是硬件选项卡,显示集群硬件:

这是“事件”选项卡,显示集群事件:

由于安全设置,您将无法访问 EMR 群集。因此,您必须打开端口以便从外部访问,然后才能探索 EMR 集群的 HDFS 和 Yarn 服务。

Make sure you don't use this insecure EMR cluster for practical purposes. This is just to be used to understand EMR.

这些是群集的安全组,显示在 EC2 仪表板中:

Figure: Screenshot showing security groups for the cluster

编辑两个安全组,并允许来自源 0.0.0.0/0 的所有 TCP 流量,如下图所示:

Figure: Screenshot showing how to edit the two security groups

现在,查看 EMR 主 IP 地址(公共),然后使用该地址访问 Yarn 服务http://EMR_MASTER_IP:8088/cluster

这是资源管理器:

这是资源管理器的队列:

也可以使用相同的 IP 地址http://<EMR-MASTER-IP>:50070访问 HDFS。

这里显示的是 HDFS 门户网站:

这些是 EMR 群集中的数据节点:

Figure: Screenshot showing datanodes in the EMR cluster

这是显示文件系统中目录和文件的 HDFS 浏览器:

我们已经演示了如何轻松地在 AWS 中旋转 EMR 集群。

Please make sure you terminate the EMR cluster at this point.

摘要

在本章中,我们讨论了 AWS 作为云计算需求的云提供商。

在下一章中,我们将把所有内容放在一起,以了解实现构建实用的大数据分析实践的业务目标需要什么。

posted @ 2025-10-01 11:29  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报