Hadoop2-学习手册-全-

Hadoop2 学习手册(全)

原文:Learning Hadoop 2

协议:CC BY-NC-SA 4.0

零、前言

本书将带您亲身探索 Hadoop2 这个奇妙的世界及其快速发展的生态系统。 Hadoop2 建立在该平台早期版本的坚实基础上,允许在单个 Hadoop 集群上执行多个数据处理框架。

为了理解这一重大演变,我们将探索这些新模型是如何工作的,并展示它们在使用批处理、迭代和接近实时的算法处理大数据量方面的应用。

这本书涵盖了哪些内容

第 1 章简介介绍了 Hadoop 及其希望解决的大数据问题的背景。 我们还将重点介绍 Hadoop1 有待改进的领域。

第 2 章存储深入探讨 Hadoop 分布式文件系统,Hadoop 处理的大部分数据都存储在该系统中。 我们将研究 HDFS 的特殊特性,展示如何使用它,并讨论它在 Hadoop 2 中的改进。我们还介绍了 Hadoop 中的另一个存储系统 ZooKeeper,它的许多高可用性功能都依赖于它。

第 3 章处理-MapReduce 和 Beyond,首先讨论传统的 Hadoop 处理模型及其使用方法。 然后我们讨论 Hadoop2 如何将该平台推广到使用多种计算模型,MapReduce 只是其中之一。

第 4 章使用 Samza 进行实时计算更深入地介绍了 Hadoop 2 支持的这些替代处理模型之一。我们特别介绍了如何使用 Apache Samza 处理实时流数据。

第 5 章使用 Spark进行迭代计算,深入探讨了一种非常不同的替代处理模型。 在本章中,我们将介绍 Apache Spark 如何提供进行迭代处理的方法。

第 6 章使用 Pig进行数据分析,演示了 Apache Pig 如何通过提供描述数据流的语言使 MapReduce 的传统计算模型更易于使用。

第 7 章Hadoop 和 SQL介绍了熟悉的 SQL 语言是如何在 Hadoop 中存储的数据上实现的。 通过使用 Apache Have 并描述 Cloudera Impala 等替代方案,我们展示了如何使用现有技能和工具实现大数据处理。

第 8 章数据生命周期管理全面介绍了如何管理 Hadoop 中要处理的所有数据。 使用 Apache Oozie,我们将展示如何构建工作流来接收、处理和管理数据。

第 9 章简化开发重点介绍了一系列旨在帮助开发人员快速取得成果的工具。 通过使用 Hadoop Streaming、Apache Crunch 和 Kite,我们展示了如何使用正确的工具来加速开发循环,或者提供语义更丰富、样板更少的新 API。

第 10 章运行 Hadoop 集群介绍了 Hadoop 的操作方面。 通过关注开发人员感兴趣的领域,如集群管理、监控和安全性,本章将帮助您更好地与运营人员合作。

第 11 章下一步是什么,带您快速浏览了许多我们认为有用的其他项目和工具,但由于篇幅限制无法在本书中详细介绍。 我们还给出了一些关于在哪里找到更多信息来源以及如何与各种开放源码社区接触的建议。

这本书你需要什么

因为大多数人没有大量闲置的机器,所以我们在本书中的大多数示例中都使用 Cloudera QuickStart 虚拟机。 这是预装了完整 Hadoop 集群的所有组件的单机映像。 它可以在任何支持 VMware 或 VirtualBox 虚拟化技术的主机上运行。

我们还将探讨 Amazon Web Services 以及如何在 AWS Elastic MapReduce 服务上运行某些 Hadoop 技术。 AWS 服务可以通过 Web 浏览器或 Linux 命令行界面进行管理。

这本书是给谁看的

本书主要面向对学习如何使用 Hadoop 框架和相关组件解决实际问题感兴趣的应用和系统开发人员。 尽管我们用几种编程语言展示了示例,但坚实的 Java 基础是主要的先决条件。

数据工程师和架构师可能还会发现有关数据生命周期、文件格式和计算模型的材料很有用。

公约

在这本书中,你会发现许多区分不同信息的文本样式。 以下是这些风格的一些示例,并解释了它们的含义。

文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄如下所示:“如果类路径中不存在 avro 依赖项,我们需要在访问单个字段之前将Avro MapReduce.jar文件添加到我们的环境中。”

代码块设置如下:

topic_edges_grouped = FOREACH topic_edges_grouped {
  GENERATE
    group.topic_id as topic,
    group.source_id as source,
    topic_edges.(destination_id,w) as edges;
}

任何命令行输入或输出都如下所示:

$ hdfs dfs -put target/elephant-bird-pig-4.5.jar hdfs:///jar/
$ hdfs dfs –put target/elephant-bird-hadoop-compat-4.5.jar hdfs:///jar/
$ hdfs dfs –put elephant-bird-core-4.5.jar hdfs:///jar/ 

新术语重要单词以粗体显示。 您在屏幕、菜单或对话框中看到的文字会出现在文本中,如下所示:“填写表单后,我们需要审核并接受服务条款,然后单击页面左下角的创建应用按钮。”

备注

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

提示

提示和技巧如下所示。

读者反馈

欢迎读者的反馈。 让我们知道你对这本书的看法-你喜欢什么或不喜欢什么。 读者反馈对我们很重要,因为它可以帮助我们开发出真正能让您获得最大收益的图书。

要向我们发送一般反馈,只需发送电子邮件<[feedback@packtpub.com](mailto:feedback@packtpub.com)>,并在邮件主题中提及书名。

如果有一个您擅长的主题,并且您有兴趣撰写或投稿一本书,请参阅我们的作者指南,网址为www.Packtpub.com/Authors

客户支持

现在您已经成为 Packt 图书的拥有者,我们有很多东西可以帮助您从购买中获得最大价值。

下载示例代码

这本书的源代码可以在 giHub 的https://github.com/learninghadoop2/book-examples上找到。 作者将对此代码应用任何勘误表,并随着技术的发展使其保持最新。 此外,您还可以从您的帐户http://www.packtpub.com为您购买的所有 Packt Publishing 图书下载示例代码文件。 如果您在其他地方购买了本书,您可以访问http://www.packtpub.com/support并注册,以便将文件通过电子邮件直接发送给您。

勘误表

虽然我们已经竭尽全力确保内容的准确性,但错误还是会发生。 如果您在我们的一本书中发现错误--可能是文本或代码中的错误--如果您能向我们报告,我们将不胜感激。 通过这样做,您可以将其他读者从挫折中解救出来,并帮助我们改进本书的后续版本。 如果您发现任何勘误表,请访问http://www.packtpub.com/submit-errata,选择您的图书,单击勘误表提交表链接,然后输入勘误表的详细信息。 一旦您的勘误表被核实,您提交的勘误表将被接受,勘误表将被上传到我们的网站或添加到该书目勘误表部分下的任何现有勘误表列表中。

要查看之前提交的勘误表,请转到https://www.packtpub.com/books/content/support,并在搜索字段中输入图书名称。 所需信息将显示在勘误表部分下。

盗版

在互联网上盗版版权材料是所有媒体持续存在的问题。 在 Packt,我们非常重视版权和许可证的保护。 如果您在互联网上发现任何形式的非法复制我们的作品,请立即提供我们的位置、地址或网站名称,以便我们采取补救措施。

请拨打<[copyright@packtpub.com](mailto:copyright@packtpub.com)>与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们的作者方面的帮助,以及我们为您提供有价值内容的能力。

问题

如果您对本书的任何方面有任何问题,可以拨打<[questions@packtpub.com](mailto:questions@packtpub.com)>与我们联系,我们将尽最大努力解决。

一、引言

本书将教您如何使用最新版本的 Hadoop 构建令人惊叹的系统。 不过,在你改变世界之前,我们需要做一些基础工作,这就是本章的用武之地。

在本介绍性章节中,我们将介绍以下主题:

  • 简要回顾 Hadoop 的背景知识
  • Hadoop 演变一览
  • Hadoop 2 中的关键元素
  • 我们将在本书中使用的 Hadoop 发行版
  • 我们将在示例中使用的数据集

关于版本化的说明

在 Hadoop1 中,版本历史有点复杂,在 0.2x 范围内有多个分叉分支,这导致了奇怪的情况,在某些情况下,1.x 版本的特性可能比 0.23 版本少。 幸运的是,在版本 2 代码库中,这要简单得多,但是明确我们在本书中将使用哪个版本是很重要的。

Hadoop2.0 发布了 Alpha 和 Beta 版本,在此过程中引入了几个不兼容的更改。 特别值得一提的是,在测试版和最终发行版之间有一个重要的 API 稳定工作。

Hadoop 2.2.0 是 Hadoop2 代码库的第一个通用版本(GA),它的接口现在被宣布为稳定且向前兼容。 因此,我们将在本书中使用 2.2 版本的产品和界面。 虽然这些原则可以在 2.0 测试版上使用,但特别是在测试版中会出现 API 不兼容的情况。 这一点尤其重要,因为 MapReducev2 已经被几个发行版供应商移植到 Hadoop1,但这些产品是基于测试版而不是 GAAPI 的。 如果您正在使用这样的产品,那么您将会遇到这些不兼容的更改。 建议将基于 Hadoop 2.2 或更高版本的版本用于任何 Hadoop 2 工作负载的开发和生产部署。

Hadoop 的背景

我们假设大多数读者会对 Hadoop 略知一二,或者至少对大数据处理系统有所了解。 因此,我们不会在本书中给出 Hadoop 成功的原因或它帮助解决的问题类型的详细背景。 但是,特别是考虑到 Hadoop 2 和我们将在后面章节中使用的其他产品的某些方面,给出一个我们认为 Hadoop 如何适应技术环境以及我们认为它能带来最大好处的特定问题领域的草图是很有用的。

在古代,在“大数据”这个术语出现之前(大约相当于十年前),几乎没有选择来处理以 TB 或更大为单位的数据集。 一些商业数据库可以通过非常具体和昂贵的硬件设置扩展到这一级别,但所需的专业知识和资本支出使其成为只有最大的组织才能选择的选择。 或者,可以针对手头的具体问题构建一个自定义系统。 这受到了一些相同的问题(专业知识和成本)的影响,并增加了任何尖端系统固有的风险。 另一方面,如果成功构建了一个系统,它很可能非常符合需求。

很少有中小型公司担心这一领域,不仅是因为解决方案超出了他们的能力范围,而且他们通常也没有任何接近于需要此类解决方案的数据量。 随着生成超大型数据集的能力变得越来越普遍,处理这些数据的需求也越来越大。

尽管大数据变得更加民主化,不再是少数特权阶层的领地,但如果能让小公司负担得起数据处理系统,就需要进行重大的架构改革。 第一个重大变化是减少了系统所需的前期资本支出;这意味着没有高端硬件或昂贵的软件许可证。 以前,高端硬件通常在数量相对较少的超大型服务器和存储系统中使用,每个服务器和存储系统都有多种方法来避免硬件故障。 虽然令人印象深刻,但这类系统非常昂贵,而转移到更多低端服务器将是大幅降低新系统硬件成本的最快方式。 更多地转向商用硬件,而不是传统的企业级设备,也将意味着弹性和容错能力的降低。 这些责任需要由软件层承担。 更智能的软件,更愚蠢的硬件

谷歌在 2003 年开始了后来被称为 Hadoop 的变革,并在 2004 年发布了两篇学术论文,描述了Google 文件系统(gfs)(http://research.google.com/archive/gfs.html)和 MapReduce(http://research.google.com/archive/mapreduce.html)。 这两者共同提供了一个以高效方式进行超大规模数据处理的平台。 谷歌采取了自己构建的方法,但他们没有针对一个特定的问题或数据集构建某种东西,而是创建了一个可以在其上实现多个处理应用的平台。 具体地说,他们利用了大量商用服务器,构建了 GFS 和 MapReduce,这种方式假定硬件故障是司空见惯的,只是软件需要处理的事情。

与此同时,Doug Cutting 正在开发 Nutch 开源网络爬虫。 他正在研究系统中的元素,这些元素在 Google GFS 和 MapReduce 论文发表后引起了强烈共鸣。 Doug 开始致力于这些 Google 想法的开源实现,Hadoop 很快就诞生了,首先是作为 Lucene 的一个子项目,然后是它自己在 Apache Software Foundation 中的顶级项目。

雅虎!。 2006 年聘请了 Doug Cutting,并很快成为 Hadoop 项目最著名的支持者之一。 除了经常宣传一些世界上最大的 Hadoop 部署外,雅虎! 允许 Doug 和其他工程师在受雇于公司的同时为 Hadoop 做出贡献,更不用说回馈一些内部开发的 Hadoop 改进和扩展了。

Hadoop 组件

Bide Hadoop 伞形项目有许多组件子项目,我们将在本书中讨论其中的几个。 Hadoop 的核心提供两项服务:存储和计算。 典型的 Hadoop 工作流包括将数据加载到Hadoop 分布式文件系统(HDFS)和使用MapReduceAPI 或几个依赖 MapReduce 作为执行框架的工具进行处理。

Components of Hadoop

Hadoop 1:HDFS 和 MapReduce

这两层都是 Google 自己的 GFS 和 MapReduce 技术的直接实现。

通用构建块

HDFS 和 MapReduce 都展示了上一节中描述的几个体系结构原则。 具体地说,的共同原则如下:

  • 两者都设计为在商用(即中低规格)服务器集群上运行
  • 两者都通过添加更多服务器(横向扩展)来扩展其容量,而不是之前的使用更大硬件的模型(纵向扩展)
  • 两者都有识别和解决故障的机制
  • 两者都透明地提供大部分服务,使用户能够专注于手头的问题
  • 两者都有一个体系结构,其中软件集群位于物理服务器上,并管理应用负载平衡和容错等方面,而不依赖高端硬件来提供这些功能

存储

HDFS 是文件系统,尽管不是 POSIX 兼容的文件系统。 这基本上意味着它不会显示出与常规文件系统相同的特征。 具体地说,特征如下:

  • HDFS 将文件存储在大小通常至少为 64 MB 或(现在更常见)128 MB 的数据块中,远远大于大多数文件系统中 4-32 KB 的大小
  • HDFS 针对延迟吞吐量进行了优化;它在流式读取大文件时非常高效,但在查找许多小文件时效率很低
  • HDFS 针对通常为一次写入和多次读取的工作负载进行了优化
  • HDFS 使用复制,而不是通过在磁盘阵列中设置物理冗余或类似策略来处理磁盘故障。 组成文件的每个数据块都存储在集群内的多个节点上,名为 NameNode 的服务会持续监视,以确保故障不会使任何数据块低于所需的复制系数。 如果确实发生了这种情况,则它会计划在集群中创建另一个副本。

计算

MapReduce 是一个 API、一个执行引擎和一个处理范例;它提供了一系列从源数据集到结果数据集的转换。 在最简单的情况下,输入数据通过 MAP 函数馈送,生成的临时数据然后通过 Reduce 函数馈送。

MapReduce 最适用于半结构化或非结构化数据。 与符合严格模式的数据不同,我们的要求是可以将数据作为一系列键-值对提供给映射函数。 Map 函数的输出是一组其他键-值对,Reduce 函数执行聚合以收集最终结果集。

Hadoop 为映射和缩减阶段提供了标准规范(即接口),这些阶段的实现通常称为映射器和减少器。 典型的 MapReduce 应用将包含许多映射器和减法器,其中几个非常简单并不少见。 开发人员专注于表示源数据和结果数据之间的转换,Hadoop 框架管理作业执行和协调的所有方面。

更好的结合在一起

我们可以欣赏 HDFS 和 MapReduce 各自的优点,但当它们组合在一起时,功能会更强大。 它们可以单独使用,但当它们结合在一起时,它们可以发挥彼此的最大优点,这种紧密的互通是 Hadoop1 成功和被接受的主要因素。

在规划 MapReduce 作业时,Hadoop 需要决定在哪台主机上执行代码,以便最有效地处理数据集。 如果 MapReduce 集群主机全部从单个存储主机或阵列提取其数据,则这在很大程度上无关紧要,因为存储系统是共享资源,会导致争用。 如果存储系统更透明,并允许 MapReduce 更直接地操作其数据,那么就有机会在更接近数据的地方执行处理,这是建立在移动处理成本低于数据的原则上的。

Hadoop 最常见的部署模式是将 HDFS 和 MapReduce 集群部署在同一组服务器上。 每个包含数据的主机和用于管理数据的 HDFS 组件还托管一个 MapReduce 组件,该组件可以调度和执行数据处理。 当作业提交到 Hadoop 时,它可以使用局部性优化来尽可能多地在数据驻留的主机上调度数据,从而最大限度地减少网络流量并最大限度地提高性能。

Hadoop 2-有什么大不了的?

如果我们看看核心 Hadoop 分发版的两个主要组件,即存储和计算,我们会发现 Hadoop2 对每个组件都有非常不同的影响。 与 Hadoop 1 中的 HDFS 相比,Hadoop 2 中的 HDFS 主要是一个功能更丰富、弹性更强的产品,而对于 MapReduce,这些变化要深刻得多,实际上改变了人们对 Hadoop 作为处理平台的总体看法。 让我们先来看看 Hadoop2 中的 HDFS。

Hadoop 2 中的存储

我们将在第 2 章存储中更详细地讨论 HDFS 体系结构,但就目前而言,考虑主从模型就足够了。 从节点(称为 DataNode)保存实际文件系统数据。 具体地说,运行 DataNode 的每个主机通常都有一个或多个磁盘,将包含每个 HDFS 块的数据的文件写入到这些磁盘上。 DataNode 本身并不了解整个文件系统;它的角色是存储、服务和确保其负责的数据的完整性。

主节点(称为 NameNode)负责知道哪个 DataNode 持有哪个块,以及这些块是如何构成文件系统的。 当客户端查看文件系统并希望检索文件时,通过向 NameNode 发出请求来检索所需块的列表。

此模型运行良好,并已扩展到具有数万个节点的集群,例如 Yahoo! 因此,尽管 NameNode 是可伸缩的,但存在弹性风险;如果 NameNode 变得不可用,那么整个集群实际上是无用的。 无法执行 HDFS 操作,而且由于绝大多数安装使用 HDFS 作为服务(如 MapReduce)的存储层,因此即使它们仍在正常运行,它们也变得不可用。

更具灾难性的是,NameNode 将文件系统元数据存储到其本地文件系统上的一个持久文件中。 如果 NameNode 主机以此数据不可恢复的方式崩溃,则集群上的所有数据实际上都将永远丢失。 数据仍将存在于各种 DataNode 上,但哪些块包含哪些文件的映射将丢失。 这就是为什么在 Hadoop1 中,最佳实践是让 NameNode 将其文件系统元数据同步写入本地磁盘和至少一个远程网络卷(通常通过 NFS)。

第三方供应商已经提供了几个 NameNode高可用性(HA)解决方案,但核心 Hadoop 产品在版本 1 中没有提供这样的弹性。考虑到这种体系结构单点故障和数据丢失的风险,听到NameNode HA是 Hadoop 2 中 HDFS 的主要功能之一也就不足为奇了,我们将在。 该功能不仅提供了一个备用 NameNode,可以在活动 NameNode 出现故障时自动升级为所有请求提供服务,而且还为该机制之上的关键文件系统元数据构建了额外的弹性。

Hadoop2 中的 HDFS 仍然是一个非 POSIX 文件系统;它仍然具有非常大的块大小,并且仍然以延迟换取吞吐量。 但是,它现在确实具有一些功能,可以使其看起来更像传统文件系统。 特别是,Hadoop2 中的核心 HDFS 现在可以远程挂载为 NFS 卷。 这是另一个特性,以前是由第三方供应商作为专有功能提供的,但现在是主要的 Apache 代码库。

总体而言,Hadoop 2 中的 HDFS 弹性更强,可以更轻松地集成到现有工作流和流程中。 这是 Hadoop1 中产品的强大发展。

Hadoop 2 中的计算

HDFS2 上的工作是在 MapReduce 的方向明确之前开始的。 这很可能是因为像 NameNode HA 这样的特性是如此明显的路径,以至于社区知道要解决的最关键的领域。 然而,MapReduce 并没有一个类似的改进领域列表,这就是为什么当 MRv2 计划开始时,并不完全清楚它将走向何方。

也许 Hadoop1 中对 MapReduce 最频繁的批评是它的批处理模型不适合需要更快响应时间的问题领域。 例如,我们将在第 7 章Hadoop 和 SQL中讨论的 HIVE 提供了针对 HDFS 数据的类似 SQL 的接口,但在幕后,语句被转换为 MapReduce 作业,然后像其他作业一样执行。 许多其他产品和工具采取了类似的方法,提供了一个特定的面向用户的界面,隐藏了 MapReduce 翻译层。

尽管这种方法非常成功,并且已经构建了一些令人惊叹的产品,但在许多情况下,仍然存在不匹配的事实,因为所有这些接口(其中一些接口需要某种类型的响应)都在后台,在批处理平台上执行。 在寻求增强 MapReduce 时,可以对其进行改进,使其更适合这些用例,但根本的不匹配仍然存在。 这种情况导致 MRv2 计划的重点发生了重大变化;也许 MapReduce 本身不需要改变,但真正需要的是在 Hadoop 平台上启用不同的处理模型。 于是诞生了又一个资源谈判者(Yarn)。

看看 Hadoop1 中的 MapReduce,该产品实际上做了两件完全不同的事情:它提供了执行 MapReduce 计算的处理框架,但它也管理着计算在集群中的分配。 它不仅将数据定向到特定的 map 和 Reduce 任务以及在它们之间定向数据,而且还确定每个任务将在何处运行,并管理整个作业生命周期、监视每个任务和节点的运行状况、在任何任务失败时重新调度等等。

这不是一项微不足道的任务,工作负载的自动并行化一直是 Hadoop 的主要优势之一。 如果我们查看 Hadoop1 中的 MapReduce,我们会看到,在用户定义了作业的关键标准之后,其他所有事情都由系统负责。 重要的是,从规模的角度来看,相同的 MapReduce 作业可以应用于托管在任何大小的集群上的任何卷的数据集。 如果数据大小为 1 GB,并且位于单个主机上,则 Hadoop 将相应地安排处理。 如果数据的大小改为 1 PB,并且托管在 1,000 台机器上,那么它也会这样做。 从用户的角度来看,数据和集群的实际规模是透明的,除了影响处理作业所需的时间外,它不会更改与系统交互的界面。

在 Hadoop2 中,作业调度和资源管理的这一角色与执行实际应用的角色是分开的,并由 YAR 实现。

YAIN 负责管理集群资源,因此 MapReduce 作为应用运行在 YAR 框架之上。 Hadoop2 中的 MapReduce 接口与 Hadoop1 中的 MapReduce 接口在语义和实践上都是完全兼容的。 然而,在幕后,MapReduce 已经成为 Yarn 框架上的一个托管应用。

这种拆分的意义是,可以编写其他应用,提供更专注于实际问题领域的处理模型,并将所有资源管理和调度责任卸载给 YAR。 许多不同的执行引擎的最新版本已经移植到了 Yarn 上,无论是处于生产就绪状态还是实验状态,并且已经表明,该方法可以允许单个 Hadoop 集群运行从面向批处理的 MapReduce 作业到快速响应 SQL 查询到连续数据流,甚至可以从高性能计算(HPC)实现图形处理和消息传递接口(MPI)等模型。 下面的图显示了 Hadoop 2 的架构:

Computation in Hadoop 2

Hadoop 2

这就是为什么围绕 Hadoop2 的大部分关注和兴奋都集中在它上面的 Yarn 和框架上,比如 Apache Tez 和 Apache Spark。 有了 YAR,Hadoop 集群不再只是一个批处理引擎;它是一个单一平台,在该平台上可以将大量处理技术应用于存储在 HDFS 中的海量数据。 此外,应用可以基于这些计算范例和执行模型构建。

与实现某种牵引力的类比是将 Yarn 视为处理内核,在此基础上可以构建其他领域特定的应用。 我们将在本书中更详细地讨论 Yarn,特别是在第 3 章Processing-MapReduce and Beyond第 4 章使用 Samza进行实时计算,以及第 5 章使用 Spark 进行迭代计算

Apache Hadoop 的发行版

在 Hadoop 非常早期的日子里,安装(通常是从源代码构建)和管理每个组件及其依赖项的负担落在用户身上。 随着该系统变得越来越流行,第三方工具和库生态系统开始增长,安装和管理 Hadoop 部署的复杂性急剧增加,以至于围绕核心 Apache Hadoop 提供连贯的软件包、文档和培训已成为一种业务模式。 进入 Apache Hadoop 的发行版世界。

Hadoop 发行版在概念上类似于 Linux 发行版如何提供一组围绕公共核心的集成软件。 它们自己承担捆绑和打包软件的负担,并为用户提供安装、管理和部署 Apache Hadoop 以及选定数量的第三方库的简单方法。 具体地说,发行版提供了一系列经认证相互兼容的产品版本。 从历史上看,构建一个基于 Hadoop 的平台通常非常复杂,因为各种版本的相互依赖。

Cloudera(http://www.cloudera.com)、Hortonworks(http://www.hortonworks.com)和 MapR(http://www.mapr.com)是最先上市的,每种产品都有不同的方法和卖点。 Hortonworks 将自己定位为开源玩家;Cloudera 也致力于开源,但增加了配置和管理 Hadoop 的专有部分;MapR 提供了混合的开源/专有 Hadoop 发行版,其特征是专有的 NFS 层而不是 HDFS,并且专注于提供服务。

分发生态系统中的另一个强大参与者是 Amazon,它在Amazon Web Services(AWS)基础设施之上提供了名为Elastic MapReduce(EMR)的 Hadoop 版本。

随着 Hadoop2 的问世,可用于 Hadoop 的发行版数量急剧增加,远远超过了我们提到的四个发行版。 包含 Apache Hadoop 的软件产品列表可能不完整,请访问http://wiki.apache.org/hadoop/Distributions%20and%20Commercial%20Support

一种双重方式

在这本书中,除了展示如何通过 EMR 将处理推入云之外,我们还将讨论本地 Hadoop 集群的构建和管理。

这有两个原因:首先,尽管 EMR 使 Hadoop 更容易访问,但该技术的某些方面只有在手动管理集群时才会变得明显。 虽然也可以在更手动的模式下使用 EMR,但我们通常会使用本地集群进行此类探索。 其次,虽然这不一定是非此即彼的决定,但许多组织混合使用内部和云托管功能,有时是因为担心过度依赖单个外部提供商,但实际上,在本地容量上进行开发和小规模测试,然后将其按生产规模部署到云中通常比较方便。

在后面的几章中,我们将讨论与 Hadoop 集成的其他产品,我们将主要给出本地集群的示例,因为无论产品部署在哪里,它们的工作方式都没有区别。

AWS-亚马逊提供的按需基础设施

AWS 是亚马逊提供的一套云计算服务。 在本书中,我们将使用其中的几项服务。

简单存储服务(S3)

亚马逊的简单存储服务(S3)位于http://aws.amazon.com/s3/,是一个提供简单键值存储模型的存储服务。 使用 Web、命令行或编程界面创建对象(可以是从文本文件到图像再到 MP3 的任何对象),您可以基于分层模型存储和检索数据。 在此模型中,您将创建包含对象的存储桶。 每个存储桶都有一个唯一的标识符,并且在每个存储桶中,每个对象都是唯一命名的。 这一简单的策略实现了一项极其强大的服务,亚马逊对此完全负责(除了数据的可靠性和可用性之外,还负责服务扩展)。

弹性 MapReduce(EMR)

亚马逊的 Elastic MapReduce 在Hadoop上找到了,基本上就是云中的 http://aws.amazon.com/elasticmapreduce/。 使用多个界面(Web 控制台、CLI 或 API)中的任何,Hadoop 工作流都定义有所需的 Hadoop 主机数量和源数据位置等属性。 提供了实现 MapReduce 作业的 Hadoop 代码,并按下了虚拟 Go 按钮。

在其最令人印象深刻的模式下,EMR 可以从 S3 提取源数据,在它在 Amazon 的虚拟主机按需服务 EC2 上创建的 Hadoop 集群上处理这些数据,将结果推送回 S3,并终止 Hadoop 集群和托管它的 EC2 虚拟机。 当然,这些服务中的每一项都有成本(通常是按存储 GB 和服务器使用时间计算),但无需专用硬件即可访问如此强大的数据处理功能的能力是非常强大的。

入门

我们现在将描述本书中将使用的两个环境:Cloudera 的 QuickStart 虚拟机将是我们的参考系统,我们将在其上展示所有示例,但当在按需服务中运行示例有一些特别有价值的方面时,我们还将在 Amazon 的 EMR 上演示一些示例。

尽管提供的示例和代码旨在尽可能具有通用性和可移植性,但我们在讨论本地集群时,参考设置将是在 CentOS Linux 上运行的 Cloudera。

在很大程度上,我们将展示使用终端提示符或从终端提示符执行的示例。 尽管 Hadoop 的图形界面在过去几年中有了很大改进(例如,出色的色调和 Cloudera Manager),但在开发、自动化和以编程方式访问系统时,命令行仍然是最强大的工具。

本书中提供的所有示例和源代码都可以从https://github.com/learninghadoop2/book-examples下载。 此外,我们还有图书主页,我们将在http://learninghadoop2.com上发布更新和相关材料。

Cloudera QuickStart 虚拟机

Hadoop 发行版的优势之一是,它们让能够访问易于安装的打包软件。 Cloudera 更进一步,提供了其最新发行版的可免费下载的虚拟机实例,称为 CDH QuickStart VM,部署在 CentOS Linux 之上。

在本书的其余部分中,我们将使用 CDH5.0.0 VM 作为参考和基准系统来运行示例和源代码。 VM 的镜像可用于 Vmware(http://www.vmware.com/nl/products/player/)、KVM(http://www.linux-kvm.org/page/Main_Page)和 VirtualBox(https://www.virtualbox.org/)虚拟化系统。

Amazon EMR

在使用Elastic MapReduce之前,我们需要设置一个 AWS 帐户并将其注册到必要的服务。

创建 AWS 帐户

Amazon 已将其一般帐户与 AWS 集成,这意味着,如果您已经拥有任何亚马逊零售网站的帐户,则这是您使用 AWS 服务所需的唯一帐户。

备注

请注意,AWS 服务是有费用的;您需要一张与可以收费的账户相关联的活动信用卡。

如果您需要新的亚马逊帐户,请转到AWS,选择新建 http://aws.amazon.com 帐户,然后按照提示操作。 Amazon 为一些服务添加了一个免费级别,因此您可能会发现,在测试和探索的早期,您的许多活动都保持在免费级别内。 免费级别的范围一直在扩大,所以要确保你知道你将会和不会被收费。

注册必要的服务

一旦您拥有 Amazon 帐户,您将需要注册该帐户以使用所需的 AWS 服务,即、Simple Storage Service(S3)、Elastic Compute Cloud(EC2)和Elastic MapReduce。 只需注册任何 AWS 服务即可免费使用;该流程只需将该服务提供给您的帐户即可。

转到从http://aws.amazon.com链接的 S3、EC2 和 EMR 页面,单击每页上的Sign Up****按钮,然后按照提示操作。

**## 使用弹性 MapReduce

在 AWS 上创建了帐户并注册了所有必需的服务后,我们可以继续配置电子病历的编程访问权限。

启动并运行 Hadoop

备注

小心! 这可真花了不少钱啊!

在继续之前,了解使用 AWS 服务将会产生与您的 Amazon 帐户关联的信用卡上显示的费用,这一点至关重要。 大多数费用都很低,并且会随着基础设施使用量的增加而增加;在 S3 中存储 10 GB 数据的成本是 1 GB 的 10 倍,运行 20 个 EC2 实例的成本是单个 EC2 实例的 20 倍。 由于存在分层成本模型,因此实际成本往往在较高的水平上有较小的边际增长。 但在使用任何一项服务之前,您都应该仔细阅读每项服务的定价部分。 另请注意,目前从 AWS 服务(如 EC2 和 S3)传出的数据是收费的,但服务之间的数据传输是不收费的。 这意味着,仔细设计 AWS 的使用,通过尽可能多的数据处理将数据保留在 AWS 中通常是最具成本效益的。 有关亚马逊工作站和电子病历的信息,请咨询http://aws.amazon.com/elasticmapreduce/#pricing

如何使用电子病历

Amazon 为 EMR 提供 Web 和命令行界面。 这两个界面只是同一个系统的前端;使用命令行界面创建的集群可以使用 Web 工具进行检查和管理,反之亦然。

在很大程度上,我们将使用命令行工具以编程方式创建和管理集群,并在有意义的情况下使用 Web 界面。

AWS 凭据

在使用编程或命令行工具之前,我们需要了解帐户持有人如何向 AWS 进行身份验证以提出此类请求。

每个 AWS 帐户都有多个标识符,如下所示,可在访问各种服务时使用:

  • 帐户 ID:每个 AWS 帐户都有一个数字 ID。
  • 访问密钥:关联的访问密钥用于标识发出请求的帐户。
  • 秘密访问密钥:访问密钥的伙伴是秘密访问密钥。 访问密钥不是秘密,可以在服务请求中公开,但是秘密访问密钥是您用来验证自己是否为帐户所有者的密钥。 把它当做你的信用卡。
  • 密钥对:这些是用于登录 EC2 主机的密钥对。 可以在 EC2 内生成公钥/私钥对,也可以将外部生成的密钥导入系统。

用户凭据和权限通过名为Identity and Access Management(IAM)的 Web 服务进行管理,您需要注册该服务才能获得访问和密钥。

如果这听起来令人困惑,那是因为它确实如此,至少在一开始是这样。 使用工具访问 AWS 服务时,通常只需将正确的凭据添加到已配置的文件中,然后一切就可以正常工作了。 但是,如果您确实决定探索编程工具或命令行工具,那么花点时间阅读每个服务的文档以了解其安全性是如何工作的将是值得的。 有关创建 aws 帐户和获取访问凭证的更多信息,请参阅http://docs.aws.amazon.com/iam

AWS 命令行界面

每个 AWS 服务在历史上都有自己的命令行工具集。 不过,亚马逊最近创建了一个单一的、统一的命令行工具,允许访问大多数服务。 Amazon CLI 位于http://aws.amazon.com/cli

它可以从 tarball 安装,也可以通过pipeasy_install包管理器安装。

在 CDH QuickStart 虚拟机上,我们可以使用以下命令安装awscli

$ pip install awscli

为了访问 API,我们需要将软件配置为使用我们的访问密钥和密钥向 AWS 进行身份验证。

这也是通过遵循https://console.aws.amazon.com/ec2/home?region=us-east-1#c=EC2&s=KeyPair提供的说明来设置 EC2 密钥对的好时机。

虽然密钥对并不是运行 EMR 集群所必需的,但它将使我们能够远程登录到主节点并获得对集群的低级别访问。

以下命令将引导您完成一系列配置步骤,并将结果配置存储在.aws/credential文件中:

$ aws configure

配置 CLI 后,我们可以使用aws <service> <arguments>查询 AWS。 要创建和查询 S3 存储桶,请使用类似以下命令的命令。 请注意,S3 存储桶需要在所有 AWS 账户中具有全局唯一性,因此最常用的名称(如s3://mybucket)将不可用:

$ aws s3 mb s3://learninghadoop2
$ aws s3 ls

我们可以使用以下命令配置具有五个m1.xlarge个节点的 EMR 集群:

$ aws emr create-cluster --name "EMR cluster" \
--ami-version 3.2.0 \
--instance-type m1.xlarge  \
--instance-count 5 \
--log-uri s3://learninghadoop2/emr-logs

其中--ami-version是 Amazon Machine Image 模板(EMR)的 ID,--log-uri指示 http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html 收集日志并将其存储在learninghadoop2S3 存储桶中。

备注

如果在设置 AWS CLI 时未指定默认区域,则还必须使用--region 参数在 AWS CLI 中添加一个 EMR 命令;例如,运行--region eu-west-1以使用 EU 爱尔兰区域。 您可以在http://docs.aws.amazon.com/general/latest/gr/rande.html上找到所有可用 aws 区域的详细信息。

我们可以使用以下命令通过向正在运行的集群添加步骤来提交工作流:

$ aws emr add-steps --cluster-id <cluster> --steps <steps> 

要终止集群,请使用以下命令行:

$ aws emr terminate-clusters --cluster-id <cluster>

在后面的章节中,我们将向您展示如何添加执行 MapReduce 作业和 Pig 脚本的步骤。

有关使用 AWS CLI 的更多信息,请参阅http://docs.aws.amazon.com/ElasticMapReduce/latest/DeveloperGuide/emr-manage.html

运行示例

所有示例的源代码都可以在https://github.com/learninghadoop2/book-examples上找到。

Gradle(Java/)脚本和配置用于编译大多数 http://www.gradle.org 代码。 示例中包含的gradlew脚本将引导 Graotstrap,并使用它来获取依赖项并编译代码。

可以通过gradlew脚本调用jar任务来创建 JAR 文件,如下所示:

./gradlew jar

作业通常通过使用hadoop jar命令提交 JAR 文件来执行,如下所示:

$ hadoop jar example.jar <MainClass> [-libjars $LIBJARS] arg1 arg2 … argN

可选的-libjars参数指定要发送到远程节点的运行时第三方依赖项。

备注

我们将要使用的一些框架,比如 Apache Spark,都有自己的构建和包管理工具。 我们会为这些特别个案提供更多资料和资源。

copyJarGradle 任务可用于将第三方依存关系下载到build/libjars/<example>/lib,如下所示:

./gradlew copyJar

为方便起见,我们提供了一个fatJarGradle 任务,该任务将示例类及其依赖项捆绑到单个 JAR 文件中。 尽管支持使用–libjar而不鼓励使用这种方法,但在处理依赖关系问题时,它可能会派上用场。

以下命令将生成build/libs/<example>-all.jar

$ ./gradlew fatJar

使用 Hadoop 进行数据处理

在本书剩余的章中,我们将介绍 Hadoop 生态系统的核心组件以及一些第三方工具和库,这些工具和库将使编写健壮的分布式代码成为一项可访问且可望令人愉快的任务。 阅读本书时,您将学习如何从大量结构化和非结构化数据中收集、处理、存储和提取信息。

我们将使用从推特的(http://www.twitter.com)实时消防水龙带生成的数据集。 这种方法将允许我们在本地使用相对较小的数据集进行实验,一旦准备好,就可以将示例扩展到生产级数据大小。

为什么选择推特?

多亏了的编程 API,Twitter 提供了一种简单的方法来生成任意大小的数据集,并将它们注入到我们基于本地或云的 Hadoop 集群中。 除了绝对的大小之外,我们将使用的数据集还具有许多属性,这些属性适合几个有趣的数据建模和处理用例。

Twitter 数据具有以下属性:

  • 非结构化:每个状态更新都是一条文本消息,可以包含对媒体内容(如 URL 和图像)的引用
  • 结构化:tweet 是带时间戳的顺序记录
  • :诸如回复和提及等关系可以建模为交互网络
  • 地理位置:发布推文或用户居住的位置
  • 实时:Twitter 上生成的所有数据都可以通过实时消防软管获得

这些属性将反映在我们可以使用 Hadoop 构建的应用类型中。 这些例子包括情绪分析、社交网络和趋势分析。

构建我们的第一个数据集

Twitter 的服务条款禁止以任何形式重新分发用户生成的数据;因此,我们不能提供通用的数据集。 相反,我们将使用 Python 脚本以编程方式访问该平台,并创建从实况流收集的用户 tweet 的转储。

一个服务,多个接口

推特用户每天分享超过 2 亿条推文,也被称为状态更新。 该平台通过四种类型的 API 提供对这个数据库的访问,每种 API 代表 Twitter 的一个方面,旨在满足特定的使用案例,例如链接来自第三方来源(产品的 Twitter)的 Twitter 内容并与之交互、编程访问特定用户或站点的内容(REST)、跨用户或站点的时间轴的搜索功能(搜索)以及实时访问在 Twitter 网络上创建的所有内容(流)。

流 API 允许直接访问 Twitter 流、跟踪关键字、从特定地区检索带地理标记的 tweet 等等。 在本书中,我们将使用此 API 作为数据源来说明 Hadoop 的批处理和实时功能。 但是,我们不会与 API 本身交互;相反,我们将利用第三方库来卸载身份验证和连接管理等繁琐工作。

一条推特的解剖

调用实时 API 返回的每个 tweet 对象被表示为一个序列化的 JSON 字符串,除了文本消息外,该字符串还包含一组属性和元数据。 这些附加内容包括唯一标识推文的数字 ID、共享推文的位置、共享该推文的用户(用户对象)、是否被其他用户重新发布(转发)和多少次(转发次数)、机器检测到的文本的语言、该推文是否回复了某人以及如果是的话,用户和回复的推文 ID,等等。

Tweet 的结构以及 API 公开的任何其他对象都在不断演变。 最新参考文献可在https://dev.twitter.com/docs/platform-objects/tweets找到。

推特凭证

Twitter 利用 OAuth 协议对第三方软件对其平台的访问进行身份验证和授权。

应用通过外部渠道(例如 Web 表单)获得以下一对凭证:

  • 消费者密钥
  • 消费者秘密

消费者秘密永远不会直接传输给第三方,因为它被用来对每个请求进行签名。

用户通过一个三方流程授权应用访问服务,该流程一旦完成,将授予应用一个由以下内容组成的令牌:

  • 访问令牌
  • 访问密码

同样,对于消费者来说,访问秘密永远不会直接传输给第三方,而是用来对每个请求进行签名。

为了使用流 API,我们首先需要注册一个应用,并授予它对系统的编程访问权限。 如果您需要一个新的推特帐户,请进入https://twitter.com/signup的注册页面,并填写所需信息。 完成此步骤后,我们需要创建一个样例应用,该应用将代表我们访问 API 并授予它适当的授权权限。 我们将使用位于https://dev.twitter.com/apps的 Web 表单来完成此操作。

当创建一个新的应用时,我们被要求给它一个名称、一个描述和一个 URL。 下面的屏幕截图显示了名为Learning Hadoop 2 Book Dataset的示例应用的设置。 出于本书的目的,我们不需要指定有效的 URL,因此我们使用了占位符。

Twitter credentials

填写表单后,我们需要查看并接受服务条款,然后单击页面左下角的Create Application按钮。

现在,我们看到一个总结应用详细信息的页面,如下面的屏幕截图所示;身份验证和授权凭据可以在 OAuth 工具选项卡下找到。

我们终于准备好生成我们的第一个 Twitter 数据集。

Twitter credentials

使用 Python 进行编程访问

在本节中,我们将使用 Python 和位于https://github.com/tweepy/tweepytweepy库来收集 Twitter 的数据。 图书代码归档的ch1目录中的stream.py文件将监听器实例化到实时消防软管,获取数据样本,并将每个 tweet 的文本回显到标准输出。

可以使用easy_installpip包管理器或通过克隆https://github.com/tweepy/tweepy处的存储库来安装tweepy库。

在 CDH QuickStart 虚拟机上,我们可以使用以下命令行安装tweepy

$ pip install tweepy

当使用-j参数调用时,脚本将 JSON tweet 输出到标准输出;-t提取并打印文本字段。 我们指定使用–n <num tweets>打印多少条 tweet。 如果未指定–n,则脚本将无限期运行。 按Ctrl+C可终止执行。

脚本期望将 OAuth 凭据存储为 shell 环境变量;必须在执行stream.py的终端会话中设置以下凭据。

$ export TWITTER_CONSUMER_KEY="your_consumer_key"
$ export TWITTER_CONSUMER_SECRET="your_consumer_secret"
$ export TWITTER_ACCESS_KEY="your_access_key"
$ export TWITTER_ACCESS_SECRET="your_access_secret"

一旦安装了所需的依赖项并设置了 shell 环境中的 OAuth 数据,我们就可以按如下方式运行该程序:

$ python stream.py –t –n 1000 > tweets.txt

我们依靠 Linux 的 shell I/O 将带有stream.py>操作符的输出重定向到一个名为tweets.txt的文件。 如果一切都执行正确,您应该会看到一堵文字墙,其中每一行都是一条 tweet。

请注意,在本例中,我们根本没有使用 Hadoop。 在接下来的章节中,我们将展示如何将流 API 生成的数据集导入 Hadoop,并在本地集群和 Amazon EMR 上分析其内容。

现在,让我们看一下stream.py的源代码,它可以在https://github.com/learninghadoop2/book-examples/blob/master/ch1/stream.py中找到:

import tweepy
import os
import json
import argparse

consumer_key = os.environ['TWITTER_CONSUMER_KEY']
consumer_secret = os.environ['TWITTER_CONSUMER_SECRET']
access_key = os.environ['TWITTER_ACCESS_KEY']
access_secret = os.environ['TWITTER_ACCESS_SECRET']

class EchoStreamListener(tweepy.StreamListener):
    def __init__(self, api, dump_json=False, numtweets=0):
        self.api = api
        self.dump_json = dump_json
        self.count = 0
        self.limit = int(numtweets)
        super(tweepy.StreamListener, self).__init__()

    def on_data(self, tweet):
        tweet_data = json.loads(tweet)
        if 'text' in tweet_data:
            if self.dump_json:
                print tweet.rstrip()
            else:
                print tweet_data['text'].encode("utf-8").rstrip()

            self.count = self.count+1
            return False if self.count == self.limit else True

    def on_error(self, status_code):
        return True

    def on_timeout(self):
        return True
…
if __name__ == '__main__':
    parser = get_parser()
    args = parser.parse_args()

    auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
    auth.set_access_token(access_key, access_secret)
    api = tweepy.API(auth)
    sapi = tweepy.streaming.Stream(
        auth, EchoStreamListener(
            api=api, 
            dump_json=args.json, 
            numtweets=args.numtweets))
    sapi.sample()

首先,我们导入三个依赖项:tweepyosjson模块,它们随 Python 解释器版本 2.6 或更高版本一起提供。

然后我们定义一个类EchoStreamListener,它从tweepy继承并扩展StreamListener。 顾名思义,StreamListener监听实时流上发布的事件和 tweet,并执行相应的操作。

每当检测到新事件时,它都会触发对on_data()的调用。 在此方法中,我们从 tweet 对象中提取text字段,并使用 UTF-8 编码将其打印到标准输出。 或者,如果使用-j调用该脚本,我们将打印整个 JSON tweet。 执行脚本时,我们使用标识 Twitter 帐户的 OAuth 凭据实例化一个tweepy.OAuthHandler对象,然后使用该对象使用应用访问和密钥进行身份验证。 然后,我们使用auth对象创建tweepy.API类的实例(api)

在成功验证之后,我们告诉 Python 使用EchoStreamListener监听实时流上的事件。

发往statuses/sample端点的 http GET 请求由sample()执行。 该请求返回所有公共状态的随机样本。

备注

小心点! 默认情况下,sample()将无限期运行。 记住通过按Ctrl+C来显式终止方法调用。

摘要

本章对 Hadoop 的起源、演变以及为什么版本 2 的发布是如此重要的里程碑进行了旋风式的介绍。 我们还在书中描述了 Hadoop 发行版的新兴市场,以及我们将如何结合使用本地和云发行版。

最后,我们描述了如何设置后续章节中所需的软件、帐户和环境,并演示了如何从我们将用作示例的 Twitter 流中提取数据。

了解了这些背景知识后,我们现在将继续详细研究 Hadoop 中的存储层。**

二、存储

在上一章中概述完 Hadoop 之后,我们现在将开始更详细地研究它的各个组成部分。 在本章中,我们将从堆栈的概念底层开始:在 Hadoop 中存储数据的方法和机制。 我们将特别讨论以下主题:

  • 描述Hadoop 分布式文件系统(HDFS)的体系结构
  • 显示 Hadoop 2 中对 HDFS 进行了哪些增强
  • 了解如何使用命令行工具和 Java API 访问 HDFS
  • 简要描述 ZooKeeper-Hadoop 中的另一个(某种)文件系统
  • 在 Hadoop 中存储数据以及可用的文件格式的调查注意事项

第 3 章Processing-MapReduce 和 Beyond中,我们将描述 Hadoop 如何提供允许处理数据的框架。

HDFS 的内部工作原理

第 1 章简介中,我们对 HDFS 进行了非常高层次的概述;现在我们将更详细地探讨它。 正如那一章中提到的,HDFS 可以被视为一个文件系统,尽管它具有非常特定的性能特征和语义。 它由两个主服务器进程实现:NameNodeDataNodes,在主/从设置中配置。 如果将 NameNode 视为保存所有文件系统元数据,将 DataNode 视为保存实际文件系统数据(块),则这是一个很好的起点。 放到 HDFS 上的每个文件都将被拆分成多个块,这些块可能驻留在许多 DataNode 上,而 NameNode 了解如何组合这些块来构造文件。

集群启动

假设我们有一个先前关闭的 HDFS 集群,然后检查启动行为,让我们探索这些节点的各种职责以及它们之间的通信。

NameNode 启动

我们将首先考虑 NameNode 的启动(尽管对此没有实际的排序要求,我们这样做只是出于叙述原因)。 NameNode 实际上存储关于文件系统的两种类型的数据:

  • 文件系统的结构,即目录名、文件名、位置和属性
  • 构成文件系统上每个文件的数据块

此数据存储在 NameNode 启动时读取的文件中。 注意,NameNode 并不持久地存储存储在特定 DataNode 上的块的映射;我们将很快看到该信息是如何通信的。

因为 NameNode 依赖于文件系统的这种内存表示形式,所以与 DataNode 相比,它往往具有完全不同的硬件要求。 我们将在第 10 章运行 Hadoop 集群中更详细地探讨硬件选择;目前,只需记住 NameNode 往往非常需要内存。 这在具有许多(数百万或更多)文件的非常大的集群上尤其如此,特别是当这些文件具有非常长的名称时。 NameNode 上的这种伸缩限制还带来了一个额外的 Hadoop2 特性,我们不会详细介绍它:NameNode 联合,多个 NameNode(或 NameNode HA 对)协同工作,为整个文件系统提供总体元数据。

NameNode 写入的主文件称为fsimage;这是整个集群中最重要的一段数据,因为如果没有它,将丢失如何将所有数据块重构为可用的文件系统的知识。 该文件被读取到内存中,并且将来对文件系统的所有修改都将应用于该文件系统的该内存中表示。 NameNode 不会在运行后应用新更改时写出fsimage的新版本;相反,它会写入另一个名为edits的文件,该文件是自写入上一个版本的fsimage以来所做更改的列表。

NameNode 启动过程首先读取fsimage文件,然后读取edits文件,并将存储在edits文件中的所有更改应用于fsimage的内存副本。 然后,它将最新版本的fsimage文件写入磁盘,并准备好接收客户端请求。

数据节点启动

当数据节点启动时,它们首先对它们保存副本的块进行编目。 通常,这些块将简单地写为本地 DataNode 文件系统上的个文件。 DataNode 将执行一些数据块一致性检查,然后向 NameNode 报告其具有有效拷贝的数据块的列表。 这就是 NameNode 构造其所需的最终映射的方式-通过了解哪些块存储在哪些 DataNode 上。 一旦 DataNode 将自身注册到 NameNode,就会在节点之间发送一系列持续的心跳请求,以允许 NameNode 检测已关闭、变得不可访问或新进入集群的 DataNode。

数据块复制

HDFS 将每个数据块复制到多个 DataNode 上;默认复制系数为 3,但可以在每个文件级别进行配置。 HDFS 还可以配置为能够确定给定的 DataNode 是否位于同一物理硬件机架中。 在给定智能块放置和集群拓扑知识的情况下,HDFS 将尝试将第二个副本放在不同的主机上,但与第一个和第三个副本放在与第一个和第三个副本相同的设备机架中,放在机架外的主机上。 通过这种方式,系统可以在多达整个机架的设备出现故障时幸存下来,并且每个数据块仍至少有一个活动副本。 正如我们将在第 3 章Processing-MapReduce 以及之外看到的,有关块放置的知识还允许 Hadoop 将处理调度到尽可能接近每个块的副本,这可以极大地提高性能。

请记住,复制是一种恢复能力的策略,但不是一种备份机制;如果您在 HDFS 中控制了至关重要的数据,则需要考虑备份或其他可提供错误保护的方法,例如意外删除的文件,而复制无法防御这些错误。

当 NameNode 启动并从 DataNode 接收数据块报告时,它将保持安全模式,直到将可配置的数据块阈值(默认值为 99.9%)报告为实时。 在安全模式下,客户端不能对文件系统进行任何修改。

对 HDFS 文件系统的命令行访问

在 Hadoop 发行版中,有一个名为hdfs的命令行实用程序,它是从命令行与文件系统交互的主要方式。 在不带任何参数的情况下运行此命令,以查看各种可用的子命令。 不过,有很多种;有几种用于启动或停止各种 HDFS 组件。 hdfs命令的一般形式为:

hdfs <sub-command> <command> [arguments]

我们将在本书中使用的两个主要子命令是:

  • dfs:这是,用于一般文件系统访问和操作,包括读/写和访问文件和目录

  • dfsadmin:此用于文件系统的管理和维护。 不过,我们不会详细介绍此命令。 看一下-report命令,它列出了文件系统和所有 DataNode 的状态:

    $ hdfs dfsadmin -report
    

备注

请注意,dfsdfsadmin命令也可以与主要的 Hadoop 命令行实用程序一起使用,例如hadoop fs -ls /。 这是 Hadoop 早期版本中的方法,但现在已弃用,取而代之的是hdfs命令。

探索 HDFS 文件系统

运行以下以获取dfs子命令提供的可用命令列表:

$ hdfs dfs

从前面命令的输出中可以看出,其中许多命令看起来与标准的 Unix 文件系统命令相似,并且毫不奇怪,它们的工作方式与预期一致。 在我们的测试 VM 中,我们有一个名为cloudera的用户帐户。 使用此用户,我们可以按如下方式列出文件系统的根目录:

$ hdfs dfs -ls /
Found 7 items
drwxr-xr-x   - hbase hbase               0 2014-04-04 15:18 /hbase
drwxr-xr-x   - hdfs  supergroup          0 2014-10-21 13:16 /jar
drwxr-xr-x   - hdfs  supergroup          0 2014-10-15 15:26 /schema
drwxr-xr-x   - solr  solr                0 2014-04-04 15:16 /solr
drwxrwxrwt   - hdfs  supergroup          0 2014-11-12 11:29 /tmp
drwxr-xr-x   - hdfs  supergroup          0 2014-07-13 09:05 /user
drwxr-xr-x   - hdfs  supergroup          0 2014-04-04 15:15 /var

输出非常类似于 Unixls命令。 文件属性的工作原理与 Unix 文件系统上的user/group/world属性相同(如图所示,包括t粘性位)以及目录所有者、组和修改时间的详细信息。 组名和修改日期之间的列是大小;对于目录,此列为 0,但对于文件,将有一个值,我们将在下面的信息框后面的代码中看到:

备注

如果使用相对路径,则从用户的主目录获取这些路径。 如果没有主目录,我们可以使用以下命令创建它:

$ sudo -u hdfs hdfs dfs –mkdir /user/cloudera
$ sudo -u hdfs hdfs dfs –chown cloudera:cloudera /user/cloudera

mkdirchown步骤需要超级用户权限(sudo -u hdfs)。

$ hdfs dfs -mkdir testdir
$ hdfs dfs -ls
Found 1 items
drwxr-xr-x   - cloudera cloudera     0 2014-11-13 11:21 testdir

然后,我们可以创建一个文件,将其复制到 HDFS,并直接从其在 HDFS 上的位置读取其内容,如下所示:

$ echo "Hello world" > testfile.txt
$ hdfs dfs -put testfile.txt testdir

请注意,有一个名为-copyFromLocal的较旧命令,其工作方式与-put相同;您可能会在较旧的在线文档中看到它。 现在,运行以下命令并检查输出:

$ hdfs dfs -ls testdir
Found 1 items
-rw-r--r--   3 cloudera cloudera         12 2014-11-13 11:21 testdir/testfile.txt

请注意文件属性和所有者之间的新列;这是文件的复制系数。 现在,最后,运行以下命令:

$ hdfs dfs -tail testdir/testfile.txt
Hello world

其余的dfs子命令非常直观;可以随意使用。 我们将在本章后面探讨快照和对 HDFS 的编程访问。

保护文件系统元数据

由于fsimage文件对文件系统非常关键,因此它的丢失是灾难性的故障。 在 Hadoop1 中,NameNode 是单点故障,最佳实践是将 NameNode 配置为同步写入fsimage并将文件编辑到本地存储以及远程文件系统(通常是 NFS)上的至少一个其他位置。 在 NameNode 出现故障的情况下,可以使用文件系统元数据的此最新副本启动替换 NameNode。 然而,该过程需要大量的人工干预,并将导致集群完全不可用的一段时间。

辅助 NameNode 无法拯救

在 Hadoop1 的所有组件中,命名最不幸的组件是二级 NameNode,这并不是没有道理的,许多人期望它是某种备份或备用 NameNode。 不是这样的;相反,二级 NameNode 只负责定期读取fsimage的最新版本,并编辑文件并创建应用了未完成编辑的新的最新fsimage。 在繁忙的集群上,此检查点可以通过减少 NameNode 在能够为客户端提供服务之前必须应用的编辑次数来显著加快 NameNode 的重启速度。

在 Hadoop 2 中,命名更加清晰;有检查点节点(执行以前由辅助 NameNode 执行的角色)和 Backup NameNodes(保留文件系统元数据的本地最新副本),尽管将备份节点提升为主 NameNode 的过程仍然是一个多阶段的手动过程。

Hadoop 2 NameNode HA

然而,在大多数生产 Hadoop 2 集群中,使用完全高可用性(HA)解决方案比使用依赖检查点和备份节点更有意义。 尝试将 NameNode HA 与检查点和备份节点机制结合使用实际上是错误的。

其核心思想是在主动/被动集群中配置一对 NameNode(目前不支持超过两个)。 一个 NameNode 充当为所有客户端请求提供服务的实时主节点,第二个 NameNode 仍然准备好在主节点出现故障时接管。 特别是,Hadoop 2 HDFS 通过两种机制启用此 HA:

  • 为两个 NameNode 提供一致的文件系统视图
  • 为客户端始终连接到主 NameNode 提供了一种方法

保持 HA NameNodes 同步

实际上有两种机制使活动 NameNode 和备用 NameNode 保持文件系统视图的一致性:使用NFS共享或仲裁日志管理器(QJM)。

在 NFS 情况下,对外部远程 NFS 文件共享有一个明显的要求-请注意,在 Hadoop1 中,对于文件系统元数据的第二个副本,使用 NFS 是最佳实践,因此许多集群已经有了一个。 如果高可用性是一个问题,但是应该记住,使 NFS 高度可用通常需要高端且昂贵的硬件。 在 Hadoop2 中,HA 使用 NFS;但是,NFS 位置成为文件系统元数据的主要位置。 当活动 NameNode 将所有文件系统更改写入 NFS 共享时,备用节点会检测到这些更改并相应地更新其文件系统元数据副本。

QJM 机制使用外部服务(日志管理器)而不是文件系统。 日志管理器集群是在该数量的主机上运行的奇数个服务(3、5 和 7 是最常见的)。 对文件系统的所有更改都提交给 QJM 服务,只有当大多数 QJM 节点提交更改时,更改才被视为已提交。 备用 NameNode 从 QJM 服务接收更改更新,并使用此信息使其文件系统元数据副本保持最新。

QJM 机制不需要额外的硬件,因为检查点节点是轻量级的,并且可以与其他服务共存。 该模型中也没有单点故障。 因此,QJM HA 通常是首选选项。

在任何一种情况下,无论是在基于 NFS 的 HA 中还是在基于 QJM 的 HA 中,DataNode 都会向这两个 NameNode 发送块状态报告,以确保这两个 NameNode 都具有块到 DataNode 映射的最新信息。 请记住,此块分配信息不保存在fsimage/编辑数据中。

客户端配置

HDFS 集群的客户端大多不知道 NameNode HA 正在被使用这一事实。 配置文件需要包括两个 NameNode 的详细信息,但用于确定哪个是活动 NameNode 以及何时切换到备用 NameNode 的机制完全封装在客户端库中。 但基本概念是,与 Hadoop 1 中的显式 NameNode 主机不同,Hadoop 2 中的 HDFS 标识了 NameNode 的名称服务 ID,其中为 HA 定义了多个单独的 NameNode(每个 NameNode 都有自己的 NameNode ID)。 请注意,名称服务 ID 的概念也由 NameNode 联邦使用,我们在前面简要提到了这一点。

故障转移的工作原理

故障转移可以是手动的,也可以是自动的。 手动故障转移需要管理员触发将备用 NameNode 升级到当前活动 NameNode 的交换机。 尽管自动故障转移对维护系统可用性的影响最大,但在某些情况下,这可能并不总是可取的。 触发手动故障切换只需要运行几个命令,因此,即使在此模式下,故障切换也比 Hadoop 1 或 Hadoop 2 备份节点的情况容易得多,后者转换到新的 NameNode 需要大量手动工作。

无论故障转移是手动触发还是自动触发,它都有两个主要阶段:确认以前的主服务器不再为请求提供服务,以及将备用服务器提升为主服务器。

故障转移中最大的风险是存在两个 NameNode 都在为请求提供服务的时间段。 在这种情况下,可能会对两个 NameNode 上的文件系统进行冲突更改,或者它们可能不同步。 即使在使用 QJM(它只接受来自单个客户端的连接)的情况下这应该是不可能的,但过时的信息可能会被提供给客户端,然后客户端可能会尝试根据这些陈旧的元数据做出不正确的决定。 当然,如果之前的主 NameNode 在某种程度上行为不正确,这尤其有可能,这就是为什么首先需要确定故障转移的原因。

为了确保任何时候只有一个 NameNode 处于活动状态,需要使用隔离机制来验证现有的 NameNode 主服务器是否已关闭。 最简单的包含机制将尝试 ssh 进入 NameNode 主机并主动终止进程,尽管也可以执行自定义脚本,因此该机制非常灵活。 在隔离成功且系统已确认以前的主 NameNode 现已失效并已释放所有所需资源之前,故障转移将不会继续。

一旦隔离成功,备用 NameNode 将成为主 NameNode,如果 NFS 用于 HA,则备用 NameNode 将开始写入 NFS 挂载的fsimage并编辑日志;如果这是 HA 机制,则备用 NameNode 将成为 QJM 的单个客户端。

在讨论自动故障转移之前,我们需要稍微介绍一下用于启用此功能的另一个 Apache 项目。

Apache ZooKeeper-一种不同类型的文件系统

在 Hadoop 中,我们在讨论文件系统和数据存储时将主要讨论 HDFS。 但是,在几乎所有的 Hadoop2 安装中,还有另一个服务看起来有点像文件系统,但它提供了对分布式系统的正常运行至关重要的重要功能。 该服务是 Apache zooKeeper(HDFS),因为它是 http://zookeeper.apache.org HA 实现的关键部分,我们将在本章中介绍它。 然而,它也被多个其他 Hadoop 组件和相关项目使用,所以我们将在本书中多次涉及到它。

ZooKeeper 最初是 HBase 的一个子组件,用于启用该服务的几个操作功能。 当构建任何复杂的分布式系统时,几乎总是需要一系列活动,而且这些活动总是很难正确进行。 这些活动包括处理共享锁、检测组件故障以及支持一组协作服务中的领导者选举等。 ZooKeeper 是作为协调服务创建的,它将提供一系列基本操作,HBase 可以根据这些操作实现这些类型的操作关键特性。 请注意,ZooKeeper 还从http://research.google.com/archive/chubby-osdi06.pdf中描述的 Google Chubby 系统获得灵感。

ZooKeeper 以实例集群的形式运行,称为整体。 该集合提供了一种数据结构,它在某种程度上类似于文件系统。 结构中的每个位置都称为 Z 节点,可以像目录一样拥有子节点,也可以像文件一样拥有内容。 请注意,ZooKeeper 不适合存储非常大量的数据,默认情况下,Znode 中的最大数据量为 1MB。 在任何时间点,集合中的一台服务器都是主服务器,并做出有关客户端请求的所有决策。 围绕主控的责任有非常明确的规则,包括它必须确保只有在大多数合唱团成员提交更改时才提交请求,并且一旦提交,任何冲突的更改都会被拒绝。

您应该在 Cloudera 虚拟机中安装 ZooKeeper。 如果没有,请使用 Cloudera Manager 将其作为单个节点安装在主机上。 在生产系统中,ZooKeeper 具有关于绝对多数投票的非常特定的语义,因此有些逻辑只有在较大的集合中才有意义(3、5 或 7 个节点是最常见的大小)。

Cloudera VM 中有一个名为zookeeper-client的 ZooKeeper 命令行客户端;请注意,在普通的 ZooKeeper 发行版中,它被称为zkCli.sh。 如果不带参数运行它,它将连接到本地计算机上运行的 ZooKeeper 服务器。 在这里,您可以键入help来获取命令列表。

最感兴趣的命令将是createlsget。 顾名思义,它们创建一个 Znode,列出文件系统中特定位置的 ZNode,并获取存储在特定 Znode 的数据。 以下是一些用法示例。

  • 创建无数据的 Z 节点:

    $ create /zk-test '' 
    
    
  • 创建第一个 Znode 的子节点并在其中存储一些文本:

    $ create /zk-test/child1 'sampledata'
    
    
  • 检索与特定 Znode 关联的数据:

    $ get /zk-test/child1 
    
    

客户端还可以在给定的 Znode 上注册观察器-如果有问题的 Znode 发生更改,无论是其数据还是子节点被修改,都会发出警报。

这听起来可能不是很有用,但是 ZNode 还可以创建为顺序节点和临时节点,这就是神奇之处所在。

使用顺序 ZNode 实现分布式锁

如果在 CLI 中使用-s选项创建了 Znode,则它将被创建为顺序节点。 ZooKeeper 将为提供的名称添加一个 10 位整数后缀,该整数保证是唯一的,并且大于同一 Znode 的任何其他连续的子节点。 我们可以使用此机制来创建分布式锁。 ZooKeeper 本身并不持有实际的锁;客户端需要了解 ZooKeeper 中的特定状态对于它们到相关应用锁的映射意味着什么。

如果我们在/zk-lock创建一个(非顺序的)Znode,那么任何希望持有锁的客户端都将创建一个顺序的子节点。 例如,在第一种情况下,create -s /zk-lock/locknode命令可能会创建节点/zk-lock/locknode-0000000001,并为后续调用增加整数后缀。 当客户端在锁下创建 Z 节点时,它将检查其顺序节点是否具有最低整数后缀。 如果有,则将其视为拥有锁。 如果不是,那么它将需要等待,直到持有锁的节点被删除。 客户端通常会监视具有下一个最低后缀的节点,然后在该节点被删除时收到警报,表明它现在持有锁。

使用短暂的 ZNode 实现群组成员资格和领导人选举

在整个会话过程中,任何 ZooKeeper 客户端都会向服务器发送心跳信号,表明它处于活动状态。 对于我们到目前为止已经讨论过的 ZNode,我们可以说它们是持久的,并且将跨会话存活。 然而,我们可以将 Znode 创建为短暂的,这意味着一旦创建它的客户机断开连接或被 ZooKeeper 服务器检测到死亡,它就会消失。 在 CLI 中,通过向 CREATE 命令添加-e标志来创建临时 Znode。

临时 ZNodes 是在分布式系统中实现组成员发现的一种很好的机制。 对于任何节点可能在没有通知的情况下发生故障、加入和离开的系统来说,知道哪些节点在任何时间点都是活动的通常是一项困难的任务。 在 ZooKeeper 中,我们可以让每个节点在 ZooKeeper 文件系统中的某个位置创建一个临时 Znode,从而为此类发现提供基础。 ZNode 可以保存有关服务节点的数据,如主机名、IP 地址、端口号等。 要获得活动节点的列表,我们可以简单地列出父组 Znode 的子节点。 由于临时节点的性质,我们可以确信在任何时候检索到的活动节点列表都是最新的。

如果我们让个服务节点创建 Znode 子节点,这些子节点不仅是短暂的,而且是连续的,那么我们还可以为需要在任何时候拥有单个主节点的服务构建领导人选举机制。 锁的机制与此相同;客户端服务节点创建顺序的和短暂的 Z 节点,然后检查它是否具有最低序列号。 如果是这样的话,那它就是主人了。 如果不是,则它将在下一个最低顺序节点上注册观察器,以便在它可能成为主节点时收到警报。

Колибриобработает

org.apache.zookeeper.ZooKeeper类是访问 ZooKeeper 集合的主要编程客户端。 有关详细信息,请参阅 javadoc,但基本接口相对简单,与 CLI 中的命令明显一一对应。 例如:

  • create:等同于 CLIcreate
  • getChildren:等同于 CLIls
  • getData:等同于 CLIget

积木

正如所见,ZooKeeper 提供了少量定义良好的操作,这些操作具有非常强的语义保证,可以构建到更高级别的服务中,例如我们前面讨论的锁、组成员和领导人选举。 最好将 ZooKeeper 看作是对分布式系统至关重要的精心设计和可靠功能的工具包,这些功能可以在其上构建,而不必担心其实现的复杂性。 不过,提供的 ZooKeeper 接口相当低级,并且出现了一些高级接口,它们提供了更多从低级原语到应用级逻辑的映射。 策展人项目(http://curator.apache.org/)就是一个很好的例子。

ZooKeeper 在 Hadoop1 中使用得很少,但现在它非常普遍。 MapReduce 和 HDFS 都使用它来实现其 JobTracker 和 NameNode 组件的高可用性。 我们稍后将探讨的 HIVE 和 Impala 使用它在由多个并发作业访问的数据表上放置锁。 我们将在 Samza 的上下文中讨论的 Kafka 将 ZooKeeper 用于节点(Kafka 术语中的代理)、领导人选举和状态管理。

进一步阅读

我们没有详细描述 ZooKeeper,完全省略了一些方面,比如它将配额和访问控制列表应用于文件系统内的 ZNode 的能力,以及构建回调的机制。 我们在这里的目的是提供足够的细节,以便您对如何在本书中探讨的 Hadoop 服务中使用它有一些了解。 有关更多信息,请参阅项目主页。

自动 NameNode 故障转移

现在我们已经引入了 ZooKeeper,我们可以展示如何使用它来启用自动 NameNode故障转移。

Automatic NameNode Failover 向系统引入了两个新组件:ZooKeeper Quorum和在每个 NameNode 主机上运行的ZooKeeper Failover Controller(ZKFC)。 ZKFC 在 ZooKeeper 中创建一个短暂的 Znode,只要它检测到本地 NameNode 处于活动状态并正常工作,它就会一直持有该 Znode。 它通过不断向 NameNode 发送简单的健康检查请求来确定这一点,如果 NameNode 在短时间内未能正确响应,则 ZKFC 将假定 NameNode 已经失败。 如果 NameNode 机器崩溃或其他故障,ZooKeeper 中的 ZKFC 会话将关闭,短暂的 Znode 也将自动删除。

ZKFC 进程还在监视集群中其他 NameNode 的 ZNode。 如果备用 NameNode 主机上的 ZKFC 看到现有的主 Znode 消失,它将假定主 Znode 已出现故障,并将尝试故障转移。 它通过尝试获取 NameNode 的锁(通过 ZooKeeper 部分中描述的协议)来实现这一点,如果成功,它将通过前面描述的相同隔离/提升机制启动故障转移。

HDFS 快照

我们在前面提到过,仅使用 HDFS 复制不是合适的备份策略。 在 Hadoop2 文件系统中,添加了快照,这为 HDFS 带来了另一个级别的数据保护。

文件系统快照在各种技术中已经使用了一段时间。 其基本思想是可以查看文件系统在特定时间点的确切状态。 这是通过在制作快照时获取文件系统元数据的副本并使其可供将来查看来实现的。

当对文件系统进行更改时,任何会影响快照的更改都会被特殊处理。 例如,如果存在于快照中的文件被删除,则即使它将从文件系统的当前状态中移除,其元数据仍将保留在快照中,并且与其数据相关联的块将保留在文件系统中,尽管不能通过除快照之外的任何系统视图来访问。

举个例子可以说明这一点。 假设您有一个包含以下文件的文件系统:

/data1 (5 blocks)
/data2 (10 blocks)

您拍摄快照,然后删除文件/data2。 如果查看文件系统的当前状态,则只有/data1可见。 如果检查快照,您将看到这两个文件。 在幕后,所有 15 个块仍然存在,但只有那些与未删除的文件/data1相关联的块是当前文件系统的一部分。 仅当快照本身被删除时,才会释放文件/data2的数据块-快照是只读视图。

Hadoop2 中的快照可以在整个文件系统级别上应用,也可以仅在特定路径上应用。 路径需要设置为快照表格,请注意,如果路径的任何子路径或父路径本身都是快照表格,则不能有路径快照表格。

让我们根据前面创建的目录来举一个简单的例子来说明快照的用法。 我们将要说明的命令需要以超级用户权限执行,而超级用户权限可以通过sudo -u hdfs获得。

首先,使用hdfsCLI 实用程序的dfsadmin子命令启用目录快照,如下所示:

$ sudo -u hdfs hdfs dfsadmin -allowSnapshot \
/user/cloudera/testdir
Allowing snapshot on testdir succeeded

现在,我们创建快照并对其进行检查;可以通过 snapshottable 目录的.snapshot子目录访问快照。 请注意,.snapshot目录在目录的正常列表中不可见。 下面是我们如何创建快照并对其进行检查:

$ sudo -u hdfs hdfs dfs -createSnapshot \
/user/cloudera/testdir sn1
Created snapshot /user/cloudera/testdir/.snapshot/sn1

$ sudo -u hdfs hdfs dfs -ls \
/user/cloudera/testdir/.snapshot/sn1

Found 1 items -rw-r--r--   1 cloudera cloudera         12 2014-11-13 11:21 /user/cloudera/testdir/.snapshot/sn1/testfile.txt

现在,我们从主目录中删除测试文件,并验证它现在是否为空:

$ sudo -u hdfs hdfs dfs -rm \
/user/cloudera/testdir/testfile.txt
14/11/13 13:13:51 INFO fs.TrashPolicyDefault: Namenode trash configuration: Deletion interval = 1440 minutes, Emptier interval = 0 minutes. Moved: 'hdfs://localhost.localdomain:8020/user/cloudera/testdir/testfile.txt' to trash at: hdfs://localhost.localdomain:8020/user/hdfs/.Trash/Current
$ hdfs dfs -ls /user/cloudera/testdir
$

请注意提到的垃圾桶目录;默认情况下,HDFS 会将任何删除的文件复制到用户主目录中的.Trash目录中,这有助于防止手指滑倒。 这些文件可以通过hdfs dfs -expunge删除,或者默认情况下将在 7 天后自动清除。

现在,我们检查现在已删除的文件仍可用的快照:

$ hdfs dfs -ls testdir/.snapshot/sn1
Found 1 items drwxr-xr-x   - cloudera cloudera          0 2014-11-13 13:12 testdir/.snapshot/sn1
$ hdfs dfs -tail testdir/.snapshot/sn1/testfile.txt
Hello world

然后,我们可以删除快照,释放它持有的所有数据块,如下所示:

$ sudo -u hdfs hdfs dfs -deleteSnapshot \
/user/cloudera/testdir sn1 
$ hdfs dfs -ls testdir/.snapshot
$

可以看到,快照中的文件完全可供读取和复制,从而提供了对创建快照时文件系统的历史状态的访问。 每个目录最多可以有 65,535 个快照,HDFS 管理快照的方式对正常文件系统操作的影响非常高效。 它们是在任何可能产生负面影响的活动(例如尝试访问文件系统的应用的新版本)之前使用的一种很好的机制。 如果新软件损坏文件,则可以恢复目录的旧状态。 如果在一段时间的验证后软件被接受,则可以改为删除快照。

Hadoop 文件系统

在之前,我们将 HDFS 称为Hadoop 文件系统。 实际上,Hadoop 对文件系统有一个相当抽象的概念。 HDFS 只是org.apache.hadoop.fs.FileSystemJava 抽象类的几个实现之一。 可以在https://hadoop.apache.org/docs/r2.5.0/api/org/apache/hadoop/fs/FileSystem.html中找到可用的文件系统列表。 下表总结了其中的一些文件系统,以及相应的 URI 方案和 Java 实现类。

|

档案系统

|

URI 方案

|

Java 实现

|
| --- | --- | --- |
| 本地人 / 慢车 / 当地居民 / 本地新闻 | file | org.apache.hadoop.fs.LocalFileSystem |
| HDFS | hdfs | org.apache.hadoop.hdfs.DistributedFileSystem |
| S3(本地) | s3n | org.apache.hadoop.fs.s3native.NativeS3FileSystem |
| S3(基于数据块) | s3 | org.apache.hadoop.fs.s3.S3FileSystem |

存在 S3 文件系统的两种实现。 Native-s3n-用于读写常规文件。 使用s3n存储的数据可由任何工具访问,反之亦然,可用于读取其他 S3 工具生成的数据。 s3n无法处理大于 5TB 的文件或重命名操作。

与 HDFS 非常类似,基于块的 S3 文件系统以块为单位存储文件,并要求 S3 存储桶专用于文件系统。 存储在 S3 文件系统中的文件可以大于 5 TB,但它们不能与其他 S3 工具互操作。 此外,基于块的 S3 支持重命名操作。

Hadoop 接口

Hadoop 是用 Java 编写的,毫不奇怪,与系统的所有交互都是通过 Java API 进行的。 我们在前面的示例中通过hdfs命令使用的命令行界面是一个 Java 应用,它使用FileSystem类在可用的文件系统上执行输入/输出操作。

Колибриобработается

org.apache.hadoop.fs包提供的 Java API 公开了个 Apache Hadoop 文件系统。

org.apache.hadoop.fs.FileSystem是每个文件系统实现的抽象类,并提供与 Hadoop 中的数据交互的通用接口。 所有使用 HDFS 的代码都应该能够处理FileSystem对象。

Libhdfs

Libhdfs 是一个 C 库,尽管它的名字是,但它可以用于访问任何 Hadoop 文件系统,而不仅仅是 HDFS。 它是使用 Java Native Interface(JNI)编写的,模拟 Java 文件系统类。

节俭

Apache Thrift(http://thrift.apache.org)是一个框架,用于通过数据序列化和远程方法调用机制构建跨语言的软件。 在contrib中提供的 Hadoop Thrift API 将 Hadoop 文件系统公开为 Thrift 服务。 该接口使非 Java 代码能够轻松地访问存储在 Hadoop 文件系统中的数据。

除了上述接口之外,还有其他接口允许通过 HTTP 和 FTP(仅限 HDFS)以及 WebDAV 访问 Hadoop 文件系统。

管理和序列化数据

拥有文件系统固然不错,但我们还需要表示数据并将其存储在文件系统上的机制。 我们现在将探索其中的一些机制。

可写界面

对于我们开发人员来说,如果我们能够操作更高级别的数据类型,并让 Hadoop 负责将它们序列化为字节以写入文件系统并在从文件系统读取字节流时从字节流中重建所需的过程,这将是非常有用的。

org.apache.hadoop.io package包含 Writable 接口,该接口提供此机制,指定如下:

   public interface Writable
   {
   void write(DataOutput out) throws IOException ;
   void readFields(DataInput in) throws IOException ;
   }

此接口的主要用途是提供在通过网络传递数据或从磁盘读取和写入数据时对数据进行序列化和反序列化的机制。

当我们在后面的章节中探索 Hadoop 上的处理框架时,我们经常会看到要求数据参数是类型 Writable 的实例。 如果我们使用提供此接口的适当实现的数据结构,则 Hadoop 机制可以自动管理数据类型的序列化和反序列化,而不需要知道它表示什么或如何使用。

介绍包装器类

幸运的是,您不必从头开始构建您将使用的所有数据类型的可写变体。 Hadoop 提供了包装 Java 原语类型并实现 Writable 接口的类。 它们在org.apache.hadoop.io包中提供。

这些类在概念上类似于java.lang中的原始包装类,如 Integer 和 Long。 它们保存单个原始值,可以在构造时设置,也可以通过 setter 方法设置。 这些建议如下:

  • BooleanWritable
  • ByteWritable
  • DoubleWritable
  • FloatWritable
  • IntWritable
  • LongWritable
  • VIntWritable:可变长度整型
  • VLongWritable:可变长度长型
  • 还有一个文本,它对java.lang.String进行换行。

数组包装类

Hadoop 还提供了一些基于集合的包装类。 这些类为其他 Writable 对象数组提供了可写包装器。 例如,实例可以保存IntWritableDoubleWritable的数组,但不能保存原始 int 或 Float 类型的数组。 需要为所需的 Writable 类指定一个子类。 这些建议如下:

ArrayWritable
TwoDArrayWritable

可比较接口和可写可比较接口

当我们说包装类实现Writable时,我们有点不准确;它们实际上在org.apache.hadoop.io包中实现了一个名为WritableComparable的复合接口,该复合接口将Writable与标准的java.lang.Comparable接口结合起来:

   public interface WritableComparable extends Writable, Comparable
   {}

只有当我们在下一章探索 MapReduce 时,对Comparable的需求才会变得明显,但现在,只需记住包装器类提供了由 Hadoop 或其任何框架对其进行序列化和排序的机制。

存储数据

到目前为止,我们介绍了 HDFS 的体系结构,以及如何使用命令行工具和 Java API 以编程方式存储和检索数据。 在到目前为止看到的示例中,我们隐含地假设我们的数据存储为文本文件。 实际上,一些应用和数据集需要特殊的数据结构来保存文件内容。 多年来,创建文件格式既是为了满足 MapReduce 处理的要求(例如,我们希望数据是可拆分的),也是为了满足对结构化和非结构化数据建模的需要。 目前,很多注意力都集中在更好地捕捉关系数据存储和建模的用例上。 在本章的剩余部分,我们将介绍 Hadoop 生态系统中可用的一些流行的文件格式选择。

序列化和容器

在讨论文件格式时,我们假设有两种情况,如下所示:

  • 序列化:我们希望将在处理时生成和操作的数据结构编码为我们可以存储到文件中、传输并在稍后阶段检索并转换回以供进一步处理的格式
  • 容器:一旦数据被序列化为文件,容器就提供了将多个文件组合在一起并添加附加元数据的方法

压缩

在处理数据时,文件压缩通常可以显著节省存储文件所需的空间,以及跨网络和从/到本地磁盘的数据 I/O。

概括地说,使用处理框架时,压缩可以在处理管道中的三个点发生:

  • 要处理的输入文件
  • 处理完成后产生的输出文件
  • 管道内部生成的中间/临时文件

当我们在这些阶段中的任何一个阶段添加压缩时,我们就有机会大幅减少要读取或写入磁盘或通过网络的数据量。 这对于 MapReduce 这样的框架特别有用,例如,这些框架可以生成比输入或输出数据集更大的临时数据量。

Apache Hadoop 附带了许多压缩编解码器:gzip、bzip2、lzo、snappy-每个都有自己的折衷。 选择编解码器是经过深思熟虑的选择,应该既考虑正在处理的数据的类型,也考虑处理框架本身的性质。

除了一般的空间/时间权衡(其中最大的空间节省是以压缩和解压缩速度为代价(反之亦然)),我们还需要考虑存储在 HDFS 中的数据将由并行的分布式软件访问;其中一些软件还将增加其自身对文件格式的特殊要求。 例如,MapReduce 对可以拆分为有效子文件的文件最有效。

这可能会使决策复杂化,比如选择是否压缩以及在压缩时使用哪个编解码器,因为大多数压缩编解码器(如 gzip)不支持可拆分文件,而少数压缩编解码器(如 LZO)支持。

通用文件格式

第一类文件格式是那些通用的文件格式,可以应用于任何应用域,并且不对数据结构或访问模式进行任何假设。

  • text:在 HDFS 上存储数据的最简单方法是使用平面文件。 文本文件既可用于保存非结构化数据(网页或推文),也可用于保存结构化数据(长度为行的 CSV 文件)。 文本文件是可拆分的,但需要考虑如何处理文件中多个元素(例如,行)之间的边界。
  • SequenceFile:SequenceFile 是由二进制键/值对组成的平面数据结构,引入该结构是为了满足基于 MapReduce 的处理的特定要求。 在 MapReduce 中,它仍然作为一种输入/输出格式被广泛使用。 正如我们将在第 3 章Processing-MapReduce 和 Beyond中看到的,在内部,映射的临时输出使用 SequenceFile 存储。

SequenceFile 分别提供WriterReaderSorter类来写入、读取和排序数据。

根据使用的压缩机制,可以区分 SequenceFile 的三种变体:

  • 未压缩的键/值记录。
  • 记录压缩的键/值记录。 只有‘value’被压缩。
  • 阻止压缩的键/值记录。 键和值被收集在任意大小的块中,并分别压缩。

然而,在每种情况下,SequenceFile 都是可拆分的,这是它最大的优势之一。

面向列的数据格式

在关系数据库世界中,面向列的数据存储根据列组织和存储表;一般来说,每列的数据将存储在一起。 与大多数按行组织数据的关系型 DBMS 相比,这是一种显著不同的方法。 面向列的存储具有显著的性能优势;例如,如果查询只需要从包含数百列的非常宽的表中读取两列,则只访问所需的列数据文件。 传统的面向行的数据库必须读取需要数据的每一行的所有列。 这对在大量相似项上计算聚合函数的工作负载(例如数据仓库系统的典型 OLAP 工作负载)的影响最大。

第 7 章Hadoop 和 SQL中,我们将看到 Hadoop 如何成为数据仓库世界的 SQL 后端,这要归功于 Apache Have 和 Cloudera Impala 等项目。 作为向该领域扩展的一部分,已经开发了许多文件格式来满足关系建模和数据仓库需求。

RCFile、ORC 和 Parquet 是针对这些用例开发的三种最先进的面向列的文件格式。

RCFile

行列文件(RCFile)最初由 Facebook 开发,用作其 Hive 数据仓库系统的后端存储,该系统是第一个开源的主流 SQL-on-Hadoop 系统。

RCFile 的目标是提供以下功能:

  • 快速数据加载
  • 快速查询处理
  • 高效的存储利用率
  • 对动态工作负载的适应性

有关 RCFile 的更多信息,请参见http://www.cse.ohio-state.edu/hpcs/WWW/HTML/publications/abs11-4.html

兽人

优化的行列文件格式(ORC)旨在将 RCFile 的性能与 Avro 的灵活性相结合。 它主要用于 Apache Have,最初由 Hortonworks 开发,以克服其他可用文件格式的感知限制。

更多详细信息可以在http://docs.hortonworks.com/HDPDocuments/HDP2/HDP-2.0.0.2/ds_Hive/orcfile.html上找到。

检察官办公室

Parquet 发现于Cloudera,最初是 Cloudera、http://parquet.incubator.apache.org 和 Criteo 共同开发的,现在已捐赠给 Apache 软件基金会。 Parquet 的目标是为 Cloudera Impala 提供一种现代的、高性能的柱状文件格式。 与黑斑羚一样,镶木地板的灵感来自德雷梅尔的论文(http://research.google.com/pubs/pub36632.html)。 它允许复杂的嵌套数据结构,并允许在每列级别上进行高效编码。

_

Apache Avro(http://avro.apache.org)是一种面向模式的二进制数据序列化格式和文件容器。 在本书中,AVRO 将是我们首选的二进制数据格式。 它既是可拆分的,也是可压缩的,这使得它成为使用 MapReduce 等框架进行数据处理的有效格式。

然而,许多其他项目也有内置的特定 Avro 支持和集成,因此它的应用非常广泛。 当数据存储在 avro 文件中时,其架构(定义为 JSON 对象)与其一起存储。 文件可以稍后由第三方处理,而不需要事先知道数据是如何编码的。 这使得数据具有自描述性,并便于使用动态和脚本语言。 读取时模式模型还有助于提高 Avro 记录的存储效率,因为不需要对各个字段进行标记。

在后面的章节中,您将看到这些属性如何简化数据生命周期管理,并允许模式迁移等重要操作。

使用 Java API

现在,我们将演示如何使用 Java API 来解析 Avro 模式、读写 Avro 文件以及使用 Avro 的代码生成工具。 请注意,该格式本质上是独立于语言的;大多数语言都有 API,Java 创建的文件可以从任何其他语言无缝读取。

AVRO 模式被描述为 JSON 文档,并由org.apache.avro.Schema类表示。 为了演示用于操作 Avro 文档的 API,我们将向前看我们在第 7 章Hadoop 和 SQL中用于配置单元表的 Avro 规范。 可以在https://github.com/learninghadoop2/book-examples/blob/master/ch2/src/main/java/com/learninghadoop2/avro/AvroParse.java找到以下代码。

在下面的代码中,我们将使用 Avro Java API 创建一个包含 tweet 记录的 avro 文件,然后使用文件中的架构重新读取该文件,以提取存储记录的详细信息:

    public static void testGenericRecord() {
        try {
            Schema schema = new Schema.Parser()
   .parse(new File("tweets_avro.avsc"));
            GenericRecord tweet = new GenericData
   .Record(schema);

            tweet.put("text", "The generic tweet text");

            File file = new File("tweets.avro");
            DatumWriter<GenericRecord> datumWriter = 
               new GenericDatumWriter<>(schema);
            DataFileWriter<GenericRecord> fileWriter = 
               new DataFileWriter<>( datumWriter );

            fileWriter.create(schema, file);
            fileWriter.append(tweet);
            fileWriter.close();

            DatumReader<GenericRecord> datumReader = 
                new GenericDatumReader<>(schema);
            DataFileReader<GenericRecord> fileReader = 
                new DataFileReader(file, datumReader);
            GenericRecord genericTweet = null;

            while (fileReader.hasNext()) {
                genericTweet = (GenericRecord) fileReader
                    .next(genericTweet);

                for (Schema.Field field : 
                    genericTweet.getSchema().getFields()) {
                    Object val = genericTweet.get(field.name());

                    if (val != null) {
                        System.out.println(val);
                    }
                }

            }
        } catch (IOException ie) {
            System.out.println("Error parsing or writing file.");
        }
    }

位于https://github.com/learninghadoop2/book-examples/blob/master/ch2/tweets_avro.avsc,处的tweets_avro.avsc模式描述具有多个字段的 tweet。 要创建这种类型的 avro 对象,我们首先要解析架构文件。 然后,我们使用 Avro 的GenericRecord概念构建符合此模式的 Avro 文档。 在本例中,我们只设置一个属性-tweet 文本本身。

要写入这个包含单个对象的 avro 文件,我们将使用 avro 的 I/O 功能。 要读取该文件,我们不需要从模式开始,因为我们可以从从文件读取的GenericRecord中提取该模式。 然后,我们遍历架构结构,并基于发现的字段动态处理文档。 这一功能尤其强大,因为它是客户端保持独立于 Avro 模式以及它如何随时间发展的关键推动因素。

但是,如果我们事先有了模式文件,我们就可以使用 Avro 代码生成来创建一个定制类,使操作 Avro 记录变得容易得多。 要生成代码,我们将使用avro-tools.jar中的 Compile 类,向其传递模式文件的名称和所需的输出目录:

$ java -jar /opt/cloudera/parcels/CDH-5.0.0-1.cdh5.0.0.p0.47/lib/avro/avro-tools.jar compile schema tweets_avro.avsc src/main/java

该类将被放置在基于模式中定义的任何命名空间的目录结构中。 由于我们在com.learninghadoop2.avrotables名称空间中创建了此模式,因此我们可以看到以下内容:

$ ls src/main/java/com/learninghadoop2/avrotables/tweets_avro.java

通过这个类,让我们回顾一下 Avro 对象的创建和读写操作,如下所示:

    public static void testGeneratedCode() {
        tweets_avro tweet = new tweets_avro();
        tweet.setText("The code generated tweet text");

        try {
            File file = new File("tweets.avro");
            DatumWriter<tweets_avro> datumWriter = 
                new SpecificDatumWriter<>(tweets_avro.class);
            DataFileWriter<tweets_avro> fileWriter = 
                new DataFileWriter<>(datumWriter);

            fileWriter.create(tweet.getSchema(), file);
            fileWriter.append(tweet);
            fileWriter.close();

            DatumReader<tweets_avro> datumReader = 
                new SpecificDatumReader<>(tweets_avro.class);
            DataFileReader<tweets_avro> fileReader = 
                new DataFileReader<>(file, datumReader);

            while (fileReader.hasNext()) {
                tweet = fileReader.next(tweet);
                System.out.println(tweet.getText());
            }
        } catch (IOException ie) {
            System.out.println("Error in parsing or writingfiles.");
        }
    }

因为我们使用了代码生成,所以我们现在将 avroSpecificRecord机制与生成的表示域模型中的对象的类一起使用。 因此,我们可以直接实例化对象并通过熟悉的 get/set 方法访问其属性。

编写文件类似于前面执行的操作,不同之处在于我们使用特定的类,并在需要时直接从 tweet 对象检索模式。 类似地,通过创建特定类的实例并使用 get/set 方法可以简化读取。

摘要

本章简要介绍了 Hadoop 集群上的存储。 我们特别介绍了以下内容:

  • Hadoop 中使用的主要文件系统 HDFS 的高级体系结构
  • HDFS 如何在幕后工作,尤其是其实现可靠性的方法
  • Hadoop 2 如何显著增加了 HDFS,特别是以 NameNode HA 和文件系统快照的形式
  • ZooKeeper 是什么,Hadoop 如何使用它来启用 NameNode 自动故障切换等功能
  • 用于访问 HDFS 的命令行工具概述
  • Hadoop 中用于文件系统的 API 以及 HDFS 如何在代码级成为更灵活的文件系统抽象的一种实现
  • 如何将数据序列化到 Hadoop 文件系统,以及核心类中提供的一些支持
  • Hadoop 中最常存储数据的各种文件格式及其一些特定使用情形

在下一章中,我们将详细介绍 Hadoop 如何提供可用于处理存储在其中的数据的处理框架。

三、数据处理——MapReduce 及以后

在 Hadoop1 中,平台有两个明确的组件:用于数据存储的 HDFS 和用于数据处理的 MapReduce。 上一章描述了 Hadoop2 中 HDFS 的发展,本章我们将讨论数据处理。

与存储相比,Hadoop 2 中的处理情况发生了更大的变化,现在 Hadoop 作为一等公民支持多种处理模式。 在本章中,我们将探索 Hadoop2 中的 MapReduce 和其他计算模型。 我们将特别介绍以下内容:

  • 什么是 MapReduce 以及为其编写应用所需的 Java API
  • MapReduce 是如何在实践中实现的
  • Hadoop 如何将数据读入和读出其处理作业
  • Yar,Hadoop2 组件,允许在平台上进行 MapReduce 以外的处理
  • 几种在 Yarn 上实现的计算模型介绍

MapReduce

MapReduce 是 Hadoop1 中支持的主要处理模型。它遵循谷歌在 2006 年发表的一篇论文(http://research.google.com/archive/mapreduce.html)提出的处理数据的分而治之模型,并且在函数式编程和数据库研究方面都有基础。 名称本身指的是应用于所有输入数据的两个截然不同的步骤,一个是map函数,另一个是reduce函数。

每个 MapReduce 应用都是构建在这个非常简单的模型之上的一系列作业。 有时,整个应用可能需要多个作业,其中一个reduce阶段的输出是另一个map阶段的输入,有时可能有多个mapreduce函数,但核心概念保持不变。

我们将通过查看mapreduce函数的性质来介绍 MapReduce 模型,然后描述构建函数实现所需的 Java API。 在展示了一些示例之后,我们将演练 MapReduce 执行,以更深入地了解实际的 MapReduce 框架如何在运行时执行代码。

学习 MapReduce 模型可能有点违反直觉;通常很难理解非常简单的函数组合在一起时如何能够在巨大的数据集上提供非常丰富的处理。 但它确实起作用了,相信我们!

当我们探索mapreduce函数的性质时,可以将它们视为应用于从源数据集中检索的记录流。 我们稍后将描述这是如何发生的;现在,假设源数据被分成更小的块,每个块都被提供给 map 函数的一个专用实例。 每条记录都应用了映射功能,生成一组中间数据。 从该临时数据集中检索记录,并通过reduce函数将所有相关记录一起馈送。 所有记录集的reduce函数的最终输出是整个作业的总体结果。

从功能的角度来看,MapReduce 将数据结构从一个(键、值)对列表转换为另一个。 在映射阶段,数据从 HDFS 加载,函数并行应用于每个输入(键、值),新的(键、值)对列表为输出:

map(k1,v1) -> list(k2,v2)

然后,该框架从所有列表中收集具有相同密钥的所有对,并将它们分组在一起,为每个密钥创建一个组。 对每个组并行应用Reduce函数,进而生成一个值列表:

reduce(k2, list (v2)) → k3,list(v3)

然后,输出以以下方式写回 HDFS:

MapReduce

映射和减少阶段

到 MapReduce 的 Java API

MapReduce 的 Java API 由org.apache.hadoop.mapreduce包公开。 编写 MapReduce 程序的核心是对 Hadoop 提供的MapperReducer基类进行子类化,并用我们自己的实现覆盖map()reduce()方法。

Mapper 类

对于我们的自己的Mapper实现,我们将子类化为Mapper基类并覆盖map()方法,如下所示:

   class Mapper<K1, V1, K2, V2>
   {
         void map(K1 key, V1 value Mapper.Context context)
               throws IOException, InterruptedException
         ...
   }

根据键/值输入和输出类型定义类,然后map 方法将输入键/值对作为其参数。 另一个参数是Context类的实例,它提供了与 Hadoop 框架通信的各种机制,其中之一是输出mapreduce方法的结果。

请注意,map 方法仅引用 K1 和 V1 键/值对的单个实例。 这是 MapReduce 范例的一个关键方面,在 MapReduce 范例中,您可以编写处理单个记录的类,框架负责将庞大的数据集转换为键/值对流所需的所有工作。 您永远不需要编写映射或缩减类来尝试处理整个数据集。 Hadoop 还通过其InputFormatOutputFormat 类提供了机制,这些机制提供了通用文件格式的实现,并且同样消除了必须为除自定义文件类型之外的任何文件类型编写文件解析器的需要。

有时可能需要覆盖另外三种方法:

   protected void setup( Mapper.Context context)
         throws IOException, InterruptedException

在将任何键/值对呈现给 map 方法之前,将调用此方法一次。 默认实现不执行任何操作:

   protected void cleanup( Mapper.Context context)
         throws IOException, InterruptedException

在将所有键/值对呈现给 map 方法之后,将调用此方法一次。 默认实现不执行任何操作:

   protected void run( Mapper.Context context)
         throws IOException, InterruptedException

此方法控制 JVM 中任务处理的整体流程。 默认实现先调用 Setup 方法一次,然后为拆分中的每个键/值对重复调用 map 方法,然后最后调用 Cleanup 方法。

Reducer 类

Reducer基类的工作方式与Mapper类非常相似,通常只需要子类覆盖单个reduce()方法。 下面是精简的类定义:

   public class Reducer<K2, V2, K3, V3>
   {
      void reduce(K2 key, Iterable<V2> values,
         Reducer.Context context)
           throws IOException, InterruptedException
      ...
   }

同样,请注意在更广泛的数据流方面的类定义(reduce方法接受K2/V2作为输入,并提供K3/V3作为输出),而实际的reduce方法只接受一个键及其关联的值列表。 上下文对象也是输出方法结果的机制。

此类还具有与Mapper类类似的默认实现的 Setup、Run 和 Cleanup 方法,可以选择覆盖这些方法:

protected void setup(Reducer.Context context)
throws IOException, InterruptedException

在将任何键/值列表呈现给reduce方法之前,会调用一次setup()方法。 默认实现不执行任何操作:

protected void cleanup(Reducer.Context context)
throws IOException, InterruptedException

在将所有键/值列表呈现给reduce方法之后,调用一次cleanup()方法。 默认实现不执行任何操作:

protected void run(Reducer.Context context)
throws IOException, InterruptedException

run()方法控制 JVM 中处理任务的总体流程。 对于提供给Reducer类的尽可能多的键/值对,默认实现在重复且可能并发地调用reduce方法之前调用 setUp 方法,然后最后调用 Cleanup 方法。

驱动程序类

驱动程序类与 Hadoop 框架通信,并指定运行 MapReduce 作业所需的配置元素。 这涉及到一些方面,比如告诉 Hadoop 使用哪个MapperReducer类、在哪里查找输入数据以及以什么格式查找输入数据、在哪里放置输出数据以及如何格式化输出数据。

驱动程序逻辑通常存在于为封装 MapReduce 作业而编写的类的 Main 方法中。 子类没有默认的父驱动程序类:

public class ExampleDriver extends Configured implements Tool
   {
   ...
   public static void run(String[] args) throws Exception
   {
      // Create a Configuration object that is used to set other options
      Configuration conf = getConf();

      // Get command line arguments
      args = new GenericOptionsParser(conf, args)
      .getRemainingArgs();

      // Create the object representing the job
      Job job = new Job(conf, "ExampleJob");

      // Set the name of the main class in the job jarfile
      job.setJarByClass(ExampleDriver.class);
      // Set the mapper class
      job.setMapperClass(ExampleMapper.class);

      // Set the reducer class
      job.setReducerClass(ExampleReducer.class);

      // Set the types for the final output key and value
      job.setOutputKeyClass(Text.class);
      job.setOutputValueClass(IntWritable.class);

      // Set input and output file paths
      FileInputFormat.addInputPath(job, new Path(args[0]));
      FileOutputFormat.setOutputPath(job, new Path(args[1]));

      // Execute the job and wait for it to complete
      System.exit(job.waitForCompletion(true) ? 0 : 1);
   }

   public static void main(String[] args) throws Exception
   {
      int exitCode = ToolRunner.run(new ExampleDriver(), args);
      System.exit(exitCode);
    }
}

在前面的行代码中,org.apache.hadoop.util.Tool是用于处理命令行选项的界面。 实际的处理被委托给ToolRunner.run,它使用给定的 Configuration 运行Tool,用于获取和设置作业的配置选项。 通过子类化org.apache.hadoop.conf.Configured,我们可以通过 GenericOptionsParser从命令行选项直接设置Configuration对象。

考虑到我们之前关于作业的讨论,许多设置都涉及作业对象上的操作也就不足为奇了。 这包括设置作业名称和指定要用于映射器和减少器实现的类。

设置特定的输入/输出配置,最后,传递给 main 方法的参数用于指定作业的输入和输出位置。 这是你会经常看到的一种非常常见的模式。

配置选项有个默认值,我们在前面的类中隐式使用了其中一些。 最值得注意的是,我们没有提到输入文件的格式或如何编写输出文件。 这些是通过前面提到的InputFormatOutputFormat类定义的;我们稍后将详细探讨它们。 默认的输入和输出格式是适合我们示例的文本文件。 除了特别优化的二进制格式之外,还有多种在文本文件中表示格式的方式。

对于不太复杂的 MapReduce 作业,一种常见的模型是将MapperReducer类作为驱动程序中的内部类。 这允许将所有内容保存在单个文件中,从而简化了代码分发。

组合

Hadoop 允许使用组合器类在还原器检索输出之前对map方法的输出执行一些早期排序。

Hadoop 的大部分设计都是基于减少通常等同于磁盘和网络 I/O 的作业的昂贵部分。映射器的输出通常很大;看到它是原始输入大小的许多倍的情况并不少见。 Hadoop 确实允许配置选项来帮助降低减速器通过网络传输如此大的数据块的影响。 组合器采用了一种不同的方法,可以提前执行聚合,从而首先需要传输更少的数据。

组合器没有自己的接口;组合器必须具有与 Reducer 相同的签名,因此还可以从org.apache.hadoop.mapreduce包中派生 Reduce 类。 这样做的效果基本上是对指定给每个减少器的输出的映射器执行一个小型缩减。

Hadoop 不保证合并器是否会被执行。 有时,它可能根本不执行,而在其他时间,它可能会被使用一次、两次或多次,具体取决于映射器为每个减速器生成的输出文件的大小和数量。

分区

Reduce 接口的隐式保证之一是,单个 Reducer 将被赋予与给定键相关联的所有值。 在集群中运行多个 Reduce 任务的情况下,必须将每个映射器输出划分为发往每个 Reducer 的单独输出。 这些分区文件存储在本地节点文件系统上。

整个集群中的 Reduce 任务的数量不像映射器的数量那样动态,实际上,我们可以将该值指定为作业提交的一部分。 因此,Hadoop 知道完成这项工作需要多少减速器,并由此知道映射器输出应该拆分成多少个分区。

可选分区函数

在中,org.apache.hadoop.mapreduce包是Partitioner类,这是一个具有以下签名的抽象类:

public abstract class Partitioner<Key, Value>
{
  public abstract int getPartition(Key key, Value value, int numPartitions);
}

默认情况下,Hadoop 将使用散列输出键的策略来执行分区。 此功能由org.apache.hadoop.mapreduce.lib.partition包中的HashPartitioner类提供,但在某些情况下需要为Partitioner的自定义子类提供特定于应用的分区逻辑。 请注意,getPartition函数将键、值和分区数量作为参数,自定义分区逻辑可以使用这些参数。

例如,如果在应用标准散列函数时数据提供了非常不均匀的分布,则自定义分区策略将特别必要。 不均匀分区可能会导致某些任务必须执行比其他任务多得多的工作,从而导致整体作业执行时间更长。

Hadoop 提供的映射器和减少器实现

我们并不总是必须从头开始编写我们自己的 Mapper 和 Reducer 类。 Hadoop 提供了几个常见的 Mapper 和 Reducer 实现,可以在我们的工作中使用。 如果我们不覆盖 Mapper 和 Reducer 类中的任何方法,则默认实现是 Identity Mapper 和 Reducer 类,它们只是输出不变的输入。

映射器位于org.apache.hadoop.mapreduce.lib.mapper处的,包括以下内容:

  • InverseMapper:返回(value,key)作为输出,即输入键作为值输出,输入值作为键输出
  • TokenCounterMapper:统计每行输入中的离散令牌数
  • IdentityMapper:实现标识功能,将输入直接映射到输出

减速器位于org.apache.hadoop.mapreduce.lib.reduce,目前包括以下内容:

  • IntSumReducer:输出每个键的整数值列表的总和
  • LongSumReducer:输出每个键的长值列表的总和
  • IdentityReducer:实现标识功能,将输入直接映射到输出

共享参考数据

有时,我们可能希望跨任务共享数据。 对于实例,如果我们需要在 ID 到字符串转换表上执行查找操作,我们可能希望这样的数据源可以由映射器或缩减器访问。 一种简单的方法是将我们想要访问的数据存储在 HDFS 上,并使用文件系统 API 将其作为 Map 或 Reduce 步骤的一部分进行查询。

Hadoop 为我们提供了另一种机制来实现在作业中的所有任务之间共享引用数据的目标,即由org.apache.hadoop.mapreduce.filecache.DistributedCache类定义的分布式缓存。 这可用于有效地使mapreduce任务使用的公共只读文件对所有节点可用。

文件可以是本例中的文本数据,但也可以是其他 JAR、二进制数据或归档;任何事情都是可能的。 要分发的文件放在 HDFS 上,并添加到作业驱动程序中的 DistributedCache。 Hadoop 在作业执行之前将文件复制到每个节点的本地文件系统,这意味着每个任务都可以本地访问这些文件。

另一种选择是,将需要的文件捆绑到提交给 Hadoop 的作业 JAR 中。 这确实将数据绑定到作业 JAR,使得跨作业共享变得更加困难,并且需要在数据更改时重新构建 JAR。

编写 MapReduce 程序

在本章中,我们将关注批处理工作负载;给定一组历史数据,我们将查看该数据集的属性。 在第 4 章使用 Samza使用 Spark迭代计算中,我们将展示如何对实时收集的文本流执行类似类型的分析。

入门

在下面的示例中,我们将假设使用stream.py脚本收集 1,000 条 tweet 生成的数据集,如第 1 章简介中所示:

$ python stream.py –t –n 1000 > tweets.txt

然后,我们可以使用以下命令将数据集拷贝到 HDFS:

$ hdfs dfs -put tweets.txt <destination>

提示

请注意,到目前为止,我们只处理 tweet 的文本。 在本书的其余部分中,我们将扩展stream.py以 JSON 格式输出额外的 tweet 元数据。 在使用stream.py转储 TB 级的消息之前,请记住这一点。

我们的第一个 MapReduce 程序将是规范的单词计数示例。 本程序的一个变体将用于确定热门话题。 然后,我们将分析与主题相关的文本,以确定它表达的是“积极”情绪还是“负面”情绪。 最后,我们将使用 MapReduce 模式-ChainMapper-将所有内容组合在一起,并提供一个数据管道来清理和准备我们将提供给趋势主题和情绪分析模型的文本数据。

运行示例

本部分中描述的示例的完整源代码可以在https://github.com/learninghadoop2/book-examples/tree/master/ch3中找到。

在 Hadoop 中运行作业之前,我们必须编译代码并将所需的类文件收集到单个 JAR 文件中,然后提交给系统。 使用 Gradle,您可以使用以下命令构建所需的 JAR 文件:

$ ./gradlew jar

本地集群

使用 Hadoop 命令行实用程序的 jar 选项在 Hadoop 上执行作业。 要使用它,我们指定 JAR 文件的名称、其中的主类以及将传递给主类的任何参数,如以下命令所示:

$ hadoop jar <job jarfile> <main class> <argument 1> … <argument 2>

弹性 MapReduce

回想一下第 1 章简介,Elastic MapReduce 期望作业 JAR 文件及其输入数据位于 S3 存储桶中,反之则将其输出转储回 S3。

备注

小心:这会花钱的! 在本例中,我们将使用 EMR 可用的最小集群配置,即单节点集群

首先,我们将使用aws命令行实用程序将 tweet 数据集以及正面和负面单词列表复制到 S3:

$ aws s3 put tweets.txt s3://<bucket>/input
$ aws s3 put job.jar s3://<bucket>

通过将 JAR 文件上传到s3://<bucket>并使用 AWS CLI 添加CUSTOM_JAR步骤,我们可以使用 EMR 命令行工具执行作业,如下所示:

$ aws emr add-steps --cluster-id <cluster-id> --steps \
Type=CUSTOM_JAR,\
Name=CustomJAR,\
Jar=s3://<bucket>/job.jar,\
MainClass=<class name>,\
Args=arg1,arg2,…argN

这里,cluster-id是正在运行的 EMR 集群的 ID,<class name>是主类的完全限定名,arg1,arg2,…,argN是作业参数。

字数,MapReduce 的 Hello World

Wordcount 统计数据集中出现的单词。 此示例的源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch3/src/main/java/com/learninghadoop2/mapreduce/WordCount.java中找到。 以下面的代码块为例:

public class WordCount extends Configured implements Tool
{
    public static class WordCountMapper
            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 {
            String[] words = value.toString().split(" ") ;
            for (String str: words)
            {
                word.set(str);
                context.write(word, one);
            }
        }
    }
    public static class WordCountReducer
            extends Reducer<Text,IntWritable,Text,IntWritable> {
        public void reduce(Text key, Iterable<IntWritable> values,
                           Context context
        ) throws IOException, InterruptedException {
            int total = 0;
            for (IntWritable val : values) {
                total++ ;
            }
            context.write(key, new IntWritable(total));
        }
    }

    public int run(String[] args) throws Exception {
        Configuration conf = getConf();

        args = new GenericOptionsParser(conf, args)
        .getRemainingArgs();

        Job job = Job.getInstance(conf);

        job.setJarByClass(WordCount.class);
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

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

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

    public static void main(String[] args) throws Exception {
        int exitCode = ToolRunner.run(new WordCount(), args);
        System.exit(exitCode);
    }
}

这是我们的第一个个完整的 MapReduce 作业。 看看结构,您应该能认出我们前面讨论过的元素:整个Job类,在其 Main 方法中包含驱动程序配置,以及定义为静态嵌套类的 Mapper 和 Reducer 实现。

在下一节中,我们将更详细地演练 MapReduce 的机制,但现在,让我们看一下前面的代码,并考虑它是如何实现我们前面讨论的键/值转换的。

Mapper 类的输入可以说是最难理解的,因为实际上并没有使用键。 作业指定TextInputFormat作为输入数据的格式,默认情况下,这将向映射器传递数据,其中键是文件中的字节偏移量,值是该行的文本。 实际上,您可能从未真正看到过使用字节偏移键的映射器,但它是提供的。

映射器对输入源中的每一行文本执行一次,每次它获取该行并将其拆分成单词。 然后,它使用上下文对象输出(通常称为发出)表单的每个新键/值(word,1)。 这些是我们的K2/V2值。

我们在前面说过,减法器的输入是一个键和相应的值列表,在mapreduce方法之间发生了一些魔术,可以收集每个键的值来帮助实现这一点-称为无序阶段,我们现在不会对其进行描述。 Hadoop 为每个键执行一次 Reducer,前面的 Reducer 实现只是对 Iterable 对象中的数字进行计数,并以(word,count)的形式给出每个单词的输出。 这些是我们的 K3/V3 值。

看看我们的映射器和减法器类的签名:WordCountMapper类接受IntWritable和 text 作为输入,并提供 text 和IntWritable作为输出。 WordCountReducer类接受文本和IntWritable作为输入和输出。 这也是一种非常常见的模式,map 方法对键和值执行反转,而是发出一系列数据对,Reducer 对这些数据对执行聚合。

驱动程序在这里更有意义,因为我们有实际的参数值。 我们使用传递给类的参数来指定输入和输出位置。

使用以下命令运行作业:

$ hadoop jar build/libs/mapreduce-example.jar com.learninghadoop2.mapreduce.WordCount \
 twitter.txt output

使用下面的命令检查输出;实际的文件名可能不同,因此只需查看 HDFS 主目录中名为 output 的目录:

$ hdfs dfs -cat output/part-r-00000

单词共现

同时出现的单词可能是短语,而常见(频繁出现)的短语可能是重要的。 在自然语言处理中,共现术语列表称为 N-Gram。 N-gram 是文本分析的几种统计方法的基础。 我们将给出一个由两个术语(二元语法)组成的 N-Gram(分析应用中经常遇到的度量)的特殊情况的示例。

MapReduce 中一个天真的实现是 wordcount 的扩展,它发出一个由两个制表符分隔的单词组成的多字段键。

public class BiGramCount extends Configured implements Tool
{
   public static class BiGramMapper
           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 {
           String[] words = value.toString().split(" ");

           Text bigram = new Text();
           String prev = null;

           for (String s : words) {
               if (prev != null) {
                   bigram.set(prev + "\t+\t" + s);
                   context.write(bigram, one);
               }

               prev = s;
           }
       }
   }

    @Override
    public int run(String[] args) throws Exception {
         Configuration conf = getConf();

         args = new GenericOptionsParser(conf, args).getRemainingArgs();
         Job job = Job.getInstance(conf);
         job.setJarByClass(BiGramCount.class);
         job.setMapperClass(BiGramMapper.class);
         job.setReducerClass(IntSumReducer.class);
         job.setOutputKeyClass(Text.class);
         job.setOutputValueClass(IntWritable.class);
         FileInputFormat.addInputPath(job, new Path(args[0]));
         FileOutputFormat.setOutputPath(job, new Path(args[1]));
         return (job.waitForCompletion(true) ? 0 : 1);
    }

    public static void main(String[] args) throws Exception {
        int exitCode = ToolRunner.run(new BiGramCount(), args);
        System.exit(exitCode);
    }
}

在此作业中,我们用实现相同逻辑的org.apache.hadoop.mapreduce.lib.reduce.IntSumReducer替换WordCountReducer。 此示例的源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch3/src/main/java/com/learninghadoop2/mapreduce/BiGramCount.java中找到。

热门话题

#符号称为标签,用于标记推文中的关键字或主题。 它是由 Twitter 用户有机创建的,作为对邮件进行分类的一种方式。 推特搜索(可在https://twitter.com/search-home找到)普及了使用标签作为连接和查找与特定主题相关的内容以及谈论这些主题的人的方法。 通过计算在给定时间段内标签被提及的频率,我们可以确定哪些话题在社交网络中流行。

public class HashTagCount extends Configured implements Tool
{
    public static class HashTagCountMapper
            extends Mapper<Object, Text, Text, IntWritable>
    {
        private final static IntWritable one = new IntWritable(1);
        private Text word = new Text();

        private String hashtagRegExp =
"(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)";

        public void map(Object key, Text value, Context context)
                throws IOException, InterruptedException {
            String[] words = value.toString().split(" ") ;

            for (String str: words)
            {
                if (str.matches(hashtagRegExp)) {
                    word.set(str);
                    context.write(word, one);
                }
            }
        }
    }

    public int run(String[] args) throws Exception {
        Configuration conf = getConf();

        args = new GenericOptionsParser(conf, args)
        .getRemainingArgs();

        Job job = Job.getInstance(conf);

        job.setJarByClass(HashTagCount.class);
        job.setMapperClass(HashTagCountMapper.class);
        job.setCombinerClass(IntSumReducer.class);
        job.setReducerClass(IntSumReducer.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

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

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

    public static void main(String[] args) throws Exception {
        int exitCode = ToolRunner.run(new HashTagCount(), args);
        System.exit(exitCode);
    }
}

与 wordcount 示例一样,我们将映射器中的文本标记化。 我们使用正则表达式-hashtagRegExp-来检测 Twitter 文本中是否存在 hashtag,并在找到 hashtag 时发出 hashtag 和数字 1。 在 Reducer 步骤中,然后使用 IntSumReducer计算发出的 hashtag 出现的总数。

此示例的完整源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch3/src/main/java/com/learninghadoop2/mapreduce/HashTagCount.java中找到。

编译后的类将位于我们之前使用 Gradle 构建的 JAR 文件中,因此现在我们使用以下命令执行 HashTagCount:

$ hadoop jar build/libs/mapreduce-example.jar \
com.learninghadoop2.mapreduce.HashTagCount twitter.txt output

让我们像前面一样检查输出:

$ hdfs dfs -cat output/part-r-00000

您应该会看到类似以下内容的输出:

#whey         1
#willpower    1
#win          2
#winterblues  1
#winterstorm  1
#wipolitics   1
#women        6
#woodgrain    1

每一行都由一个标签和它在 twets 数据集中出现的次数组成。 如您所见,MapReduce 作业按键对结果进行排序。 如果我们想要找到提到最多的主题,我们需要对结果集进行排序。 天真的方法是对聚合值进行总排序,并选择前 10 名。

如果输出数据集很小,我们可以将其通过管道传输到标准输出,并使用sort实用程序对其进行排序:

$ hdfs dfs -cat output/part-r-00000 | sort -k2 -n -r | head -n 10

另一个解决方案是编写另一个 MapReduce 作业来遍历整个结果集并按值排序。 当数据变得很大时,这种类型的全局排序可能会变得相当昂贵。 在下一节中,我们将演示一种对聚合数据进行排序的高效设计模式

前 N 个模式

在 Top N 模式中,我们将数据排序在本地数据结构中。 每个映射器计算其拆分中前 N 条记录的列表,并将其列表发送到缩减器。 单个 Reducer 任务查找前 N 个全局记录。

我们将应用此设计模式来实现TopTenHashTag作业,该作业在我们的数据集中查找前十个主题。 该作业接受HashTagCount生成的输出数据作为输入,并返回十个最常提到的标签的列表。

TopTenMapper中,我们使用TreeMap来保持标签的排序列表(按升序排列)。 该映射的关键是出现的次数;该值是由个标签及其在map()中的频率.组成的制表符分隔的字符串,对于每个值,我们更新topN映射。 当 topN 有十个以上的项目时,我们删除最小的:

public static class TopTenMapper extends Mapper<Object, Text, 
  NullWritable, Text> {

  private TreeMap<Integer, Text> topN = new TreeMap<Integer, Text>();
  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 {

  String[] words = value.toString().split("\t") ;
  if (words.length < 2) {
    return;
  }
  topN.put(Integer.parseInt(words[1]), new Text(value));
  if (topN.size() > 10) {
    topN.remove(topN.firstKey());
  }
}

       @Override
       protected void cleanup(Context context) throws IOException, InterruptedException {
            for (Text t : topN.values()) {
                context.write(NullWritable.get(), t);
            }
        }
    }

我们不会在 map 函数中发出任何键/值。 我们实现了一个cleanup()方法,一旦映射器使用了它的所有输入,就会发出topN中的(hashtag,count)值。 我们使用NullWritable键,因为我们希望所有值都与同一键相关联,这样我们就可以在所有映射器的前 n 个列表上执行全局排序。 这意味着我们的工作将只执行一个减速器。

减法器实现的逻辑与我们在map()中的逻辑类似。 我们实例化TreeMap并使用它来保存前 10 个值的有序列表:

    public static class TopTenReducer extends
            Reducer<NullWritable, Text, NullWritable, Text> {

        private TreeMap<Integer, Text> topN = new TreeMap<Integer, Text>();

        @Override
        public void reduce(NullWritable key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
            for (Text value : values) {
                String[] words = value.toString().split("\t") ;

                topN.put(Integer.parseInt(words[1]),
                    new Text(value));

                if (topN.size() > 10) {
                    topN.remove(topN.firstKey());
                }
            }

            for (Text word : topN.descendingMap().values()) {
                context.write(NullWritable.get(), word);
            }
        }
    }

最后,我们按降序遍历topN以生成趋势主题列表。

备注

请注意,在此实现中,当调用topN.put()时,我们覆盖了在TreeMap中已经存在频率值的 hashtag。 根据用例的不同,建议使用不同的数据结构--比如 Guava 库(https://code.google.com/p/guava-libraries/)提供的数据结构--或者调整更新策略。

在驱动程序中,我们通过设置job.setNumReduceTasks(1)来强制执行单个减速器:

$ hadoop jar build/libs/mapreduce-example.jar \
com.learninghadoop2.mapreduce.TopTenHashTag \
output/part-r-00000 \
top-ten

我们可以查看前十名,列出热门话题:

$ hdfs dfs -cat top-ten/part-r-00000
#Stalker48      150
#gameinsight    55
#12M    52
#KCA    46
#LORDJASONJEROME        29
#Valencia       19
#LesAnges6      16
#VoteLuan       15
#hadoop2    12
#Gameinsight    11

本例的源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch3/src/main/java/com/learninghadoop2/mapreduce/TopTenHashTag.java中找到。

标签的情感

识别数据源中的主观信息的过程通常被称为情感分析。 在前面的示例中,我们展示了如何检测社交网络中的热门话题;现在我们将分析围绕这些话题分享的文本,以确定它们表达的是积极情绪还是负面情绪。

http://www.cs.uic.edu/~liub/FBS/opinion-lexicon-English.rar上可以找到英语的正面和负面单词列表--一个所谓的意见词典。

备注

这些资源--以及更多的资源--已经由伊利诺伊大学芝加哥分校的刘兵教授的团队收集,并在刘兵、胡敏青和郑俊生等人身上使用。 “意见观察家:分析和比较网络上的意见。” 第 14 届国际万维网会议论文集(WWW-2005),2005 年 5 月 10-14 日,日本千叶市

在本例中,我们将介绍一种词袋方法,尽管该方法本质上过于简单,但可以用作挖掘文本中观点的基线。 对于每条 tweet 和每个 hashtag,我们将计算正面或负面单词出现的次数,并根据文本长度对此计数进行归一化。

备注

词袋模型是自然语言处理和信息检索中用来表示文本文档的一种方法。 在该模型中,文本被表示为其单词的集合或包-具有多样性,而不考虑语法和形态属性,甚至不考虑词序。

使用以下命令行解压缩归档文件并将单词列表放入 HDFS 中:

$ hdfs dfs –put positive-words.txt <destination>
$ hdfs dfs –put negative-words.txt <destination>

在 Mapper 类中,我们将包含单词列表的两个对象定义为Set<String>positiveWordsnegativeWords

private Set<String> positiveWords =  null;
private Set<String> negativeWords = null;

我们覆盖映射器的默认setup()方法,以便使用我们在上一章中讨论的文件系统 API 从 HDFS 读取正面和负面单词的列表-由两个配置属性job.positivewords.pathjob.negativewords.path指定。 我们还可以使用 DistributedCache 在集群之间共享此数据。 Helper 方法parseWordsList读取单词列表、剥离注释并将单词加载到HashSet<String>中:

private HashSet<String> parseWordsList(FileSystem fs, Path wordsListPath)
{
    HashSet<String> words = new HashSet<String>();
    try {

        if (fs.exists(wordsListPath)) {
            FSDataInputStream fi = fs.open(wordsListPath);

            BufferedReader br =
new BufferedReader(new InputStreamReader(fi));
            String line = null;
            while ((line = br.readLine()) != null) {
                if (line.length() > 0 && !line.startsWith(BEGIN_COMMENT)) {
                    words.add(line);
                }
            }

            fi.close();
        }
    }
    catch (IOException e) {
        e.printStackTrace();
    }

    return words;
}  

在 Mapper 步骤中,我们为 tweet 中的每个标签发出 tweet 的总体感觉(简单地说,正向字数减去负向字数)和 tweet 的长度。

我们将在减法器中使用这些参数来计算按推文长度加权的总体情绪比率,以估计标签上的推文所表达的情绪,如下所示:

        public void map(Object key, Text value, Context context)
 throws IOException, InterruptedException {
            String[] words = value.toString().split(" ") ;
            Integer positiveCount = new Integer(0);
            Integer negativeCount = new Integer(0);

            Integer wordsCount = new Integer(0);

            for (String str: words)
            {
                if (str.matches(HASHTAG_PATTERN)) {
                    hashtags.add(str);
                }

                if (positiveWords.contains(str)) {
                    positiveCount += 1;
                } else if (negativeWords.contains(str)) {
                    negativeCount += 1;
                }

                wordsCount += 1;
            }

            Integer sentimentDifference = 0;
            if (wordsCount > 0) {
              sentimentDifference = positiveCount - negativeCount;
            }

            String stats ;
            for (String hashtag : hashtags) {
                word.set(hashtag);
                stats = String.format("%d %d", sentimentDifference, wordsCount);
                context.write(word, new Text(stats));
            }
        }
    }

在 Reducer 步骤中,我们将给予每个标签实例的情绪得分相加,并除以出现该标签的所有推文的总大小:

public static class HashTagSentimentReducer
            extends Reducer<Text,Text,Text,DoubleWritable> {
        public void reduce(Text key, Iterable<Text> values,
                           Context context
        ) throws IOException, InterruptedException {
            double totalDifference = 0;
            double totalWords = 0;
            for (Text val : values) {
                String[] parts = val.toString().split(" ") ;
                totalDifference += Double.parseDouble(parts[0]) ;
                totalWords += Double.parseDouble(parts[1]) ;
            }
            context.write(key,
new DoubleWritable(totalDifference/totalWords));
        }
    }

此示例的完整源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch3/src/main/java/com/learninghadoop2/mapreduce/HashTagSentiment.java中找到。

运行上述代码后,使用以下命令执行HashTagSentiment

$ hadoop jar build/libs/mapreduce-example.jar com.learninghadoop2.mapreduce.HashTagSentiment twitter.txt output-sentiment <positive words> <negative words>

您可以使用以下命令检查输出:

$ hdfs dfs -cat output-sentiment/part-r-00
000

您应该会看到类似于以下内容的输出:

#1068   0.011861271213042056
#10YearsOfLove  0.012285135487494233
#11     0.011941109121333999
#12     0.011938693593171155
#12F    0.012339242266249566
#12M    0.011864286953783268
#12MCalleEnPazYaTeVasNicolas

在前面的输出中,每行都由一个标签和与之相关的情感极性组成。 这个数字是启发式的,它告诉我们标签主要与正面(极性>0)还是负面(极性<0)情绪相关,以及这种情绪的程度-数字越高或越低,情绪越强烈。

使用链映射器清除文本

在到目前为止提供的示例中,我们忽略了几乎每个围绕文本处理构建的应用的一个关键步骤,即输入数据的规范化和清理。 此标准化步骤的三个常见组件为:

  • 将字母大小写更改为小写或大写
  • 拆除停工字眼
  • 茎 / 干 / 船首 / 血统

在本节中,我们将展示ChainMapper类(位于org.apache.hadoop.mapreduce.lib.chain.ChainMapper)如何允许我们顺序组合一系列映射器,以作为数据清理管道的第一步放在一起。 使用以下选项将映射器添加到配置的作业:

ChainMapper.addMapper(
JobConf job,
Class<? extends Mapper<K1,V1,K2,V2>> klass,
Class<? extends K1> inputKeyClass,
Class<? extends V1> inputValueClass,
Class<? extends K2> outputKeyClass,
Class<? extends V2> outputValueClass, JobConf mapperConf)

静态方法addMapper需要传递以下参数:

  • job:添加 Mapper 类的 JobConf
  • class:要添加的映射器类
  • inputKeyClass:映射器输入键类
  • inputValueClass:映射器输入值类
  • outputKeyClass:映射器输出键类
  • outputValueClass:映射器输出值类
  • mapperConf:具有 Mapper 类配置的 JobConf

在这个示例中,我们将处理上面列出的第一项:在计算每条 tweet 的情感之前,我们将其文本中出现的每个单词转换为小写。 这将允许我们通过忽略不同推文的大小写差异来更准确地确定标签的情绪。

首先,我们定义了一个新的映射器-LowerCaseMapper-它的map()函数在其输入值上调用 Java String 的toLowerCase() 方法,并发出大小写的文本:

public class LowerCaseMapper extends Mapper<LongWritable, Text, IntWritable, Text> {
    private Text lowercased = new Text();
    public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
        lowercased.set(value.toString().toLowerCase());
        context.write(new IntWritable(1), lowercased);
    }
}

HashTagSentimentChain驱动程序中,我们配置 Job 对象,以便将两个映射器链接在一起并执行:

public class HashTagSentimentChain
extends Configured implements Tool
{

    public int run(String[] args) throws Exception {
        Configuration conf = getConf();
        args = new GenericOptionsParser(conf,args).getRemainingArgs();

        // location (on hdfs) of the positive words list
        conf.set("job.positivewords.path", args[2]);
        conf.set("job.negativewords.path", args[3]);

        Job job = Job.getInstance(conf);
        job.setJarByClass(HashTagSentimentChain.class);

        Configuration lowerCaseMapperConf = new Configuration(false);
        ChainMapper.addMapper(job,
                LowerCaseMapper.class,
                LongWritable.class, Text.class,
                IntWritable.class, Text.class,
                lowerCaseMapperConf);

        Configuration hashTagSentimentConf = new Configuration(false);
        ChainMapper.addMapper(job,
                HashTagSentiment.HashTagSentimentMapper.class,
                IntWritable.class,
                Text.class, Text.class,
                Text.class,
                hashTagSentimentConf);
        job.setReducerClass(HashTagSentiment.HashTagSentimentReducer.class);

        job.setInputFormatClass(TextInputFormat.class);
        FileInputFormat.addInputPath(job, new Path(args[0]));

        job.setOutputFormatClass(TextOutputFormat.class);
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

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

    public static void main (String[] args) throws Exception {
        int exitCode = ToolRunner.run(
new HashTagSentimentChain(), args);
        System.exit(exitCode);
    }
}

在管道中调用LowerCaseMapperHashTagSentimentMapper类,其中第一个类的输出成为第二个类的输入。 最后一个映射器的输出将写入任务的输出。 此设计的直接好处是减少了磁盘 I/O 操作。 映射器不需要知道它们已被链接。 因此,可以重用可以在单个任务中组合的专用映射器。 请注意,此模式假设所有映射器和 Reduce 都使用匹配的输出和输入(键、值)对。 ChainMapper 本身不执行强制转换或转换。

最后,请注意,链中最后一个映射器的addMapper调用指定了在用作组合时适用于整个映射器管道的输出键/值类。

此示例的完整源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch3/src/main/java/com/learninghadoop2/mapreduce/HashTagSentimentChain.java中找到。

使用以下命令执行HashTagSentimentChain

$ hadoop jar build/libs/mapreduce-example.jar com.learninghadoop2.mapreduce.HashTagSentimentChain twitter.txt output <positive words> <negative words>

您应该会看到类似于上一个示例的输出。 请注意,这一次,每行的标签都是小写的。

浏览 MapReduce 作业的运行

为了更详细地探索映射器和 Reducer 之间的关系,并公开 Hadoop 的一些内部工作原理,我们现在将了解 MapReduce 作业是如何执行的。 这同样适用于 Hadoop1 中的 MapReduce 和 Hadoop2 中的 MapReduce,尽管后者是使用 Yarn 实现的,这一点我们将在本章后面讨论。 有关本节中描述的服务的其他信息,以及对 MapReduce 应用故障排除的建议,可以在第 10 章运行 Hadoop 集群中找到。

启动

该驱动程序是在我们的本地机器上运行的唯一一段代码,调用Job.waitForCompletion()将启动与 JobTracker 的通信,JobTracker 是 MapReduce 系统中的主节点。 JobTracker 负责作业调度和执行的所有方面,因此在执行任何与作业管理相关的任务时,它成为我们的主要界面。

为了共享集群上的资源,JobTracker 可以使用几种调度方法中的一种来处理传入的作业。 一般模型是具有多个队列,作业可以提交到这些队列,同时还有跨队列分配资源的策略。 这些策略最常用的实现是容量和公平调度程序。

JobTracker 代表我们与 NameNode 通信,并管理与存储在 HDFS 上的数据相关的所有交互。

拆分输入

这些交互中的第一个发生在 JobTracker 查看输入数据并确定如何将其分配给映射任务时。 回想一下,HDFS 文件通常被分割成至少 64MB 的块,JobTracker 会将每个块分配给一个映射任务。 当然,我们的字数统计示例使用了很少的数据量,这些数据完全在单个块内。 假设有一个更大的输入文件(以 TB 为单位),那么拆分模型就更有意义了。 文件的每个段(在 MapReduce 术语中称为拆分)都由一个映射任务唯一地处理。 一旦计算出拆分,JobTracker 就会将拆分和包含 Mapper 和 Reducer 类的 JAR 文件放入 HDFS 上特定于作业的目录中,该目录的路径将在任务启动时传递给每个任务。

任务分配

TaskTracker 服务负责分配资源、执行和跟踪节点上运行的 map 和 Reduce 任务的状态。 一旦 JobTracker 确定需要多少映射任务,它就会查看集群中的主机数量、有多少 TaskTracker 在工作,以及每个任务可以同时执行多少映射任务(用户可定义的配置变量)。 JobTracker 还会查看各个输入数据块在集群中的位置,并尝试定义一个执行计划,以最大化 TaskTracker 处理位于同一物理主机上的拆分/数据块的情况,或者,如果失败,它将处理同一硬件机架中的至少一个拆分/数据块。 这种数据局部性优化是 Hadoop 能够高效处理如此大型数据集的一个重要原因。 还请记住,默认情况下,每个数据块跨三个不同的主机进行复制,因此生成任务/主机计划以查看大多数数据块在本地处理的可能性比最初看起来要高。

任务启动

然后,每个 TaskTracker 启动一个单独的 Java 虚拟机来执行任务。 这确实增加了启动时间损失,但它将 TaskTracker 与行为不当的mapreduce任务引起的问题隔离开来,并且可以将其配置为在后续执行的任务之间共享。

如果集群有足够的容量一次执行所有映射任务,那么它们都将被启动,并被赋予要处理的拆分和作业 JAR 文件的引用。 如果任务数量超过集群容量,JobTracker 将保留挂起任务队列,并在节点完成初始分配的 MAP 任务时将其分配给节点。

现在我们可以查看 MAP 任务的执行数据了。 如果所有这些听起来像是大量的工作,那么它解释了为什么在运行任何 MapReduce 作业时,系统启动和执行所有这些步骤总是要花费大量的时间。

持续的 JobTracker 监控

JobTracker 现在不仅仅是停止工作,等待 TaskTracker 执行所有的映射器和减法器。 它不断地与 TaskTracker 交换心跳和状态信息,寻找进展或问题的证据。 它还从整个作业执行过程中的任务收集指标,其中一些指标由 Hadoop 提供,另一些指标由mapreduce任务的开发人员指定,尽管我们在本例中没有使用任何指标。

发帖主题:Re:Колибри0.7.0

驱动程序类使用TextInputFormat指定输入文件的格式和结构,由此,Hadoop 知道将其作为文本处理,字节偏移量作为键,行内容作为值。 假设我们的数据集包含以下文本:

This is a test
Yes it is

因此,映射器的两次调用将得到以下输出:

1 This is a test
2 Yes it is

映射器执行

由于作业的配置方式,映射器接收到的键/值对分别是行和行内容文件中的偏移量。 我们在WordCountMapper中实现的 map 方法丢弃了键,因为我们不关心每行在文件中出现的位置,并使用标准 Java String 类上的 Split 方法将提供的值拆分成单词。 请注意使用正则表达式或StringTokenizer类可以提供更好的标记化,但是对于我们的目的来说,这个简单的方法就足够了。 然后,对于每个单独的单词,映射器都会发出一个由实际单词本身和值 1 组成的键。

映射器输出和减速器输入

映射器的输出是一系列形式为(word,1)的对;在我们的示例中,这些对将是:

(This,1), (is, 1), (a, 1), (test, 1), (Yes, 1), (it, 1), (is, 1)

来自映射器的这些输出对不会直接传递到减速器。 映射和还原之间是混洗阶段,MapReduce 的大部分魔力都发生在这里。

减速器输入

Reducer TaskTracker 从 JobTracker 接收更新,这些更新告诉它集群中的哪些节点拥有需要由其本地reduce任务处理的map个输出分区。 然后,它从各个节点检索这些内容,并将它们合并到单个文件中,该文件将提供给reduce任务。

减速器执行

我们的WordCountReducer类非常简单;对于每个单词,它只计算数组中元素的数量,并为每个单词发出最终的(word,count)输出。 对于我们对样例输入调用 wordcount,除了一个单词之外,所有单词在值列表中只有一个值;有两个值。

减速机输出

因此,我们示例的组减速器输出为:

(This, 1), (is, 2), (a, 1), (test, 1), (Yes, 1), (it, 1)

此数据将输出到驱动程序中指定的输出目录中的分区文件,该目录将使用指定的 OutputFormat 实现进行格式化。 每个reduce任务写入一个文件名为part-r-nnnnn的文件,其中nnnnn00000开始并递增。

停机

一旦所有任务都成功完成,JobTracker 就会向客户端输出作业的最终状态,以及它在此过程中聚合的一些更重要的计数器的最终聚合。 完整的作业和任务历史记录可以在每个节点的日志目录中找到,或者更方便地通过 JobTracker web UI 获得;将浏览器指向 JobTracker 节点上的端口 50030。

输入/输出

我们已经讨论了关于文件作为作业启动的一部分被拆分以及拆分中的数据被发送到映射器实现的问题。 但是,这忽略了两个方面:如何将数据存储在文件中,以及如何将各个键和值传递给映射器结构。

InputFormat 和 RecordReader

Hadoop 有 InputFormat 的概念作为这些职责中的第一个。 org.apache.hadoop.mapreduce包中的 InputFormat 抽象类提供了两个方法,如以下代码所示:

public abstract class InputFormat<K, V>
{
    public abstract List<InputSplit> getSplits( JobContext context);
    RecordReader<K, V> createRecordReader(InputSplit split,
        TaskAttemptContext context) ;
}

这些方法显示 InputFormat 类的两个职责:

  • 提供有关如何将输入文件拆分为地图处理所需的拆分的详细信息
  • 创建将从拆分生成一系列键/值对的 RecordReader

RecordReader 类也是org.apache.hadoop.mapreduce包中的抽象类:

public abstract class RecordReader<Key, Value> implements Closeable
{
  public abstract void initialize(InputSplit split,
    TaskAttemptContext  context);
  public abstract boolean nextKeyValue()
    throws IOException, InterruptedException;
  public abstract Key getCurrentKey()
    throws IOException, InterruptedException;
  public abstract Value getCurrentValue()
    throws IOException, InterruptedException;
  public abstract float getProgress()
    throws IOException, InterruptedException;
  public abstract close() throws IOException;
}

为每个拆分创建一个RecordReader实例,并调用getNextKeyValue返回一个布尔值,指示是否有另一个键/值对可用,如果有,则使用getKeygetValue方法分别访问键和值。

因此,InputFormatRecordReader类的组合就是在任何类型的输入数据和 MapReduce 所需的键/值对之间桥接所需的全部内容。

Hadoop 提供的 InputFormat

org.apache.hadoop.mapreduce.lib.input包中有一些 Hadoop 提供的 InputFormat 实现:

  • FileInputFormat:是一个抽象基类,它可以是任何基于文件的输入的父类。
  • SequenceFileInputFormat:是一种高效的二进制文件格式,将在下一节中讨论。
  • TextInputFormat:用于纯文本文件。
  • KeyValueTextInputFormat:用于纯文本文件。 每行由一个分隔符字节分为键部分和值部分。

请注意,输入格式不限于从文件读取;FileInputFormat 本身就是 InputFormat 的子类。 Hadoop 可以使用不基于文件的数据作为 MapReduce 作业的输入;常见的源是关系数据库或面向列的数据库,如 Amazon DynamoDB 或 HBase。

Hadoop 提供的 RecordReader

Hadoop 提供了一些常见的RecordReader实现,也存在于org.apache.hadoop.mapreduce.lib.input包中:

  • LineRecordReader:实现是文本文件的默认RecordReader类,它将文件中的字节偏移量表示为键,将行内容表示为值
  • SequenceFileRecordReader:实现从二进制SequenceFile容器读取键/值

OutputFormat 和 RecordWriter

有一个类似的模式,用于编写由来自org.apache.hadoop.mapreduce包的OutputFormatRecordWriter的子类协调的作业输出。 我们在这里不会详细讨论这些内容,但是一般的方法是相似的,尽管 OutputFormat 确实有一个更复杂的 API,因为它有用于验证输出规范等任务的方法。

如果指定的输出目录已经存在,则此步骤会导致作业失败。 如果您想要不同的行为,则需要OutputFormat的子类来覆盖此方法。

Hadoop 提供的 OutputFormat

org.apache.hadoop.mapreduce.output包中提供了以下输出格式:

  • FileOutputFormat:是所有基于文件的 OutputFormats 的基类
  • NullOutputFormat:是一个虚拟实现,它丢弃输出,不向文件写入任何内容
  • SequenceFileOutputFormat:写入二进制序列文件格式
  • TextOutputFormat:写入纯文本文件

请注意,这些类将其所需的RecordWriter实现定义为静态嵌套类,因此没有单独提供RecordWriter实现。

序列文件

org.apache.hadoop.io包中的SequenceFile类提供了一种高效的二进制文件格式,该格式通常用作 MapReduce 作业的输出。 如果作业的输出被处理为另一个作业的输入,情况尤其如此。 序列文件有几个优点,如下所示:

  • 作为二进制文件,它们本质上比文本文件更紧凑
  • 此外,它们还支持可选压缩,也可以应用于不同级别,即压缩每条记录或整个拆分
  • 它们可以拆分并并行处理

最后一个特征很重要,因为大多数二进制格式(特别是那些压缩或加密的格式)不能拆分,必须作为单个线性数据流读取。 使用此类文件作为 MapReduce 作业的输入意味着将使用单个映射器来处理整个文件,这可能会导致较大的性能影响。 在这种情况下,最好使用可拆分格式,如 SequenceFile,或者,如果您无法避免接收其他格式的文件,请执行预处理步骤,将其转换为可拆分格式。 这将是一种权衡,因为转换将需要时间,但在许多情况下(特别是对于复杂的映射任务),通过增加并行性节省的时间将超过这一点。

Yarn

Yar 开始时是 MapReduce v2(MRv2)计划的一部分,但现在是 Hadoop 中的一个独立子项目(也就是说,它与 MapReduce 处于同一级别)。 它源于一种认识,即 Hadoop1 中的 MapReduce 将两个相关但不同的职责合并在一起:资源管理和应用执行。

尽管 MapReduce 模型在庞大的数据集上实现了以前无法想象的处理,但概念级别的 MapReduce 模型对性能和可伸缩性有影响。 MapReduce 模型中隐含的含义是,任何应用都只能由一系列基本上线性的 MapReduce 作业组成,每个作业都遵循一个或多个地图的模型,后面跟着一个或多个 Reduce。 该模型非常适合某些应用,但不是所有应用。 特别是,它不适合需要非常低延迟响应时间的工作负载;MapReduce 的启动时间以及有时冗长的作业链往往大大超出了面向用户的进程的容忍度。 人们还发现,对于更自然地被表示为任务的有向无环图(DAG)的作业来说,该模型的效率非常低,其中图上的节点是处理步骤,而边是数据流。 如果将应用作为 DAG 进行分析和执行,则应用可能在一个步骤中以跨处理步骤的高度并行性执行,但是当通过 MapReduce 镜头查看时,结果通常是一系列相互依赖的 MapReduce 作业效率低下。

许多项目都在 MapReduce 之上构建了不同类型的处理,虽然很多项目都非常成功(Apache Have 和 Pig 就是两个突出的例子),但 MapReduce 作为处理范例与 Hadoop1 中的作业调度机制的紧密结合使得任何新项目都很难针对其特定需求定制这些领域中的任何一个。

结果是又一个资源协商器(Yarn),它在 Hadoop 中提供了一个功能强大的作业调度机制,并为要在其中实现的不同处理模型提供了定义良好的接口。

Yarn 架构

要理解 Yarn 是如何工作的,重要的是不要再去想 MapReduce 以及它是如何处理数据的。 Year 本身并没有说明在其上运行的应用的性质,而是专注于为这些作业的调度和执行提供机制。 正如我们将看到的那样,Year 能够承载长时间运行的流处理或低延迟的面向用户的工作负载,就像它能够承载批处理工作负载一样,比如 MapReduce。

Yarn 的成分

YAYN 由两个主要组件组成,ResourceManager(RM)和NodeManager(NM),ResourceManager(RM)管理整个集群中的资源,在每台主机上运行并管理单个机器上的资源。 ResourceManager 和 NodeManager 处理容器的调度和管理,容器是专用于运行特定应用代码的内存、CPU 和 I/O 的抽象概念。 以 MapReduce 为例,当在 Yarn 上运行时,JobTracker 和每个 TaskTracker 都在各自的专用容器中运行。 但是请注意,在 YAR 中,每个 MapReduce 作业都有自己专用的 JobTracker;没有一个实例可以管理所有作业,就像在 Hadoop1 中一样。

YAY 本身只负责整个集群的任务调度;所有关于应用级进度、监控和容错的概念都在应用代码中处理。 这是一个非常明确的设计决策;通过使 Yarn 尽可能独立,它有一组非常明确的职责,并且不会人为地限制可以在 Yarn 上实现的应用类型。

作为所有集群资源的仲裁者,YAR 有能力将集群作为一个整体进行高效管理,而不会关注应用层的资源需求。 它有一个可插拔的调度策略,所提供的实现类似于现有的 Hadoop 容量和公平调度器。 Year 还将所有应用代码视为本质上不受信任的代码,所有应用管理和控制任务都在用户空间中执行。

Yarn 应用的解剖

提交的 Yarn 应用有两个组件:ApplicationMaster(AM),它协调整个应用流,以及将在工作节点上运行的代码的规范。 对于 MapReduce TOP YAR,JobTracker 实现 ApplicationMaster 功能,而 TaskTracker 是部署在 Worker 节点上的应用定制代码。

如上所述,应用管理、进度监控和容错的职责在 Yarn 中被推到了应用层面。 执行这些任务的是 ApplicationMaster;例如,Year 本身没有说明 ApplicationMaster 和 Worker 容器中运行的代码之间的通信机制。

这种通用性允许 Yarn 应用不被绑定到 Java 类。 ApplicationManager 可以改为请求 NodeManager 执行 shell 脚本、本机应用或在每个节点上可用的任何其他类型的处理。

Yarn 应用的生命周期

与 Hadoop1 中的 MapReduce 作业一样,客户端将 Yarn 应用提交到集群。 启动 Yarn 应用时,客户端首先调用 ResourceManager(更具体地说,是 ResourceManager 的 ApplicationManager 部分),并请求在其中执行 ApplicationMaster 的初始容器。 在大多数情况下,ApplicationMaster 将从集群中的托管容器运行,就像应用代码的其余部分一样。 ApplicationManager 与 ResourceManager 的另一个主要组件(调度器本身)通信,调度器本身负责管理整个集群中的所有资源。

ApplicationMaster 在提供的容器中启动,向 ResourceManager 注册自身,并开始协商其所需资源的过程。 ApplicationMaster 与 ResourceManager 通信,并请求它所需的容器。 所请求的容器的规格还可以包括附加信息,例如期望的集群内的位置和具体的资源要求,例如特定数量的内存或 CPU。

ResourceManager 向 ApplicationMaster 提供已分配给它的容器的详细信息,然后 ApplicationMaster 与 NodeManagers 通信,为每个容器启动特定于应用的任务。 这是通过向 NodeManager 提供要执行的应用的规范来实现的,如前所述,该规范可以是 JAR 文件、脚本、本地可执行文件的路径或 NodeManager 可以调用的任何其他内容。 每个 NodeManager 实例化应用代码的容器,并根据提供的规范启动应用。

容错和监控

从开始,行为在很大程度上是特定于应用的。 YAY 不会管理应用进度,但会执行一些正在进行的任务。 ResourceManager 中的 AMLivelinessMonitor 接收来自所有 ApplicationMaster 的心跳信号,如果它确定 ApplicationMaster 失败或停止工作,它将注销失败的 ApplicationMaster 并释放其分配的所有容器。 然后,ResourceManager 将重新调度应用可配置的次数。

除了这个过程之外,ResourceManager 内的 NMLivelinessMonitor 还接收来自 NodeManager 的心跳,并跟踪集群中每个 NodeManager 的运行状况。 与 ApplicationMaster 的健康监控类似,NodeManager 在默认时间(10 分钟)内未收到心跳信号后将被标记为已死,在此之后,所有已分配的容器都将被标记为已死,并且该节点将被排除在未来的资源分配之外。

同时,NodeManager 将主动监控每个已分配容器的资源利用率,并且对于那些不受硬限制限制的资源,将杀死超出其资源分配的容器。

在更高的级别,Yarn 调度器总是希望在所采用的共享策略的约束内最大化集群利用率。 与 Hadoop 1 一样,如果争用较少,这将允许低优先级应用使用更多集群资源,但如果提交较高优先级的应用,调度程序将抢占这些额外的容器(即,请求终止它们)。

应用级容错和进度监控的其余职责必须在应用代码中实现。 例如,对于在 Yarn 上的 MapReduce,所有任务调度和重试的管理都是在应用级别提供的,而不是由 Yarn 以任何方式提供的。

分层思考

这些最后的陈述可能表明,编写在 Yarn 上运行的应用是一项大量的工作,这是真的。 Yarn API 相当低级,对于大多数只想在数据上运行一些处理任务的开发人员来说,它可能会让人望而生畏。 如果我们所拥有的全部都是 Yarn,而每个新的 Hadoop 应用都必须实现自己的 ApplicationMaster,那么 Yarn 看起来就不会像现在这样有趣了。

使情况更好的是,通常情况下,要求不是实现每一个 Yarn 上的应用,而是将其用于数量较少的处理框架,这些框架提供了要实现的更友好的接口。 第一个是 MapReduce;由于它驻留在 Yarn 上,开发人员编写通常的mapreduce接口,并且基本上不了解 Yarn 机制。

但是在同一个集群上,另一个开发人员可能正在运行一个作业,该作业使用具有显著不同处理特征的不同框架,而 Swing 将同时管理这两个作业。

我们将更详细地介绍目前可用的几种 Yarn 处理模型,但它们涵盖了从批处理到低延迟查询、流和图形处理等各个方面。

然而,随着 Yarn 体验的增长,有许多举措可以使这些处理框架的开发变得更容易。 一方面,有更高级别的接口,如,如 Cloudera Kitten(https://github.com/cloudera/kitten)或 Apache Twill(http://twill.incubator.apache.org/),在 Yarn API 之上提供了更友好的抽象。 不过,也许更重要的开发模型是框架的出现,这些框架提供了更丰富的工具,可以更轻松地构建具有通用通用性能特征的应用。

执行模型

我们提到了不同的 Yarn 应用,它们具有不同的加工特性,但一个新兴的模式通常认为它们的执行模式是一个差异化的来源。 通过这种方式,我们参考了 Yarn 应用生命周期的管理方式,并确定了三种主要类型:按作业应用、按会话应用和始终在线应用。

批处理,例如在 Yarn 上的 MapReduce,可以看到 MapReduce 框架的生命周期与提交的应用的生命周期绑定在一起。 如果我们提交 MapReduce 作业,则执行该作业的 JobTracker 和 TaskTracker 是专门为该作业创建的,并在作业完成时终止。 这对于 Batch 来说很有效,但是如果我们希望提供一个交互性更强的模型,那么如果发出的每个命令都遭受这种惩罚,那么建立 Yarn 应用的启动开销及其所有资源分配都将严重影响用户体验。 在一个更具交互性(或基于会话)的生命周期中,将会看到 Yarn 应用启动,然后可以为许多提交的请求/命令提供服务。 只有在退出会话时,Yarn 应用才会终止。

最后,我们提出了长期运行的应用的概念,该应用独立于任何交互式输入来处理连续数据流。 因此,启动并持续处理通过某种外部机制检索的数据对于 Yarn 应用来说是最有意义的。 只有在显式关闭或出现异常情况时,应用才会退出。

现实世界中的 Yarn-MapReduce 之外的计算

前面的讨论有点抽象,因此在本节中,我们将探索几个现有的 Yarn 应用,看看它们是如何使用框架的,以及它们是如何提供广泛的处理能力的。 特别令人感兴趣的是,Yarn 框架如何采用截然不同的方法来进行资源管理、I/O 流水线和容错。

MapReduce 的问题

到目前为止,我们已经从 API 的角度研究了 MapReduce。 Hadoop 中的 MapReduce 不止于此;在 Hadoop2 之前,它是许多工具的默认执行引擎,其中包括配置单元和 Pig,我们将在本书后面更详细地讨论这些工具。 我们已经看到 MapReduce 应用实际上是一个作业链。 这正是框架最大的痛点和制约因素之一。 MapReduce 检查点数据到 HDFS 以进行进程内通信:

The problem with MapReduce

MapReduce 作业链

在每个reduce阶段结束时,输出被写入磁盘,以便可以由下一个作业的映射器加载,并将用作其输入。 这种 I/O 开销会带来延迟,特别是当我们的应用需要多次通过数据集(因此需要多次写入)时。 不幸的是,这种类型的迭代计算是许多分析应用的核心。

Apache Tez 和 Apache Spark 是通过推广 MapReduce 范例来解决此问题的两个框架。 在本节的其余部分中,我们将在 Apache Samza 之后简要讨论它们,Apache Samza 是一个采用完全不同的实时处理方法的框架。

同步,由 Elderman 更正@ELDER_MAN

TEZ(API)是一个低级 http://tez.apache.org 和 Execution 引擎,专注于提供低延迟处理,并被用作 Hive、Pig 和其他几个实现标准联接、过滤、合并和分组操作的框架的最新发展的基础。 TEZ 是微软在 2009 年的 Dryad 论文(http://research.microsoft.com/en-us/projects/dryad/)中提出的编程模型的实现和发展。 TEZ 是 MapReduce 作为数据流的概括,它致力于通过在队列上流水线 I/O 操作来实现快速、交互的计算,以实现进程内通信。 这避免了影响 MapReduce 的昂贵磁盘写入。 API 提供将作业之间的依赖关系表示为 DAG 的原语。 然后将完整的 DAG 提交给可以优化执行流的规划者。 上图中描述的相同应用将在 TEZ 中作为单个作业执行,将 I/O 从减速器流水线传输到减速器,而无需 HDFS 写入和映射器随后的读取。 在下图中可以看到一个例子:

Tez

TEZ DAG 是 MapReduce 的推广

可以在https://github.com/apache/incubator-tez/blob/master/tez-mapreduce-examples/src/main/java/org/apache/tez/mapreduce/examples/WordCount.java中找到规范字数计算示例。

DAG dag = new DAG("WordCount");
dag.addVertex(tokenizerVertex)
.addVertex(summerVertex)
.addEdge(new Edge(tokenizerVertex, summerVertex,
edgeConf.createDefaultEdgeProperty()));

尽管图形拓扑dag 可以用几行代码来表示,但是执行作业所需的样板是相当多的。 此代码处理许多低级调度和执行职责,包括容错。 当 tez 检测到失败的任务时,它会返回处理图,以找到重新执行失败任务的起点。

_

HIVE 0.13 是第一个使用 TEZ 作为其执行引擎的备受瞩目的项目。 我们将在第 7 章Hadoop 和 SQL中更详细地讨论 hive,但现在我们只会触及它是如何在 Yarn 上实现的。

HIVE(SQL)是一个引擎,用于通过标准 http://hive.apache.org 语法查询存储在 HDFS 上的数据。 它已经取得了巨大的成功,因为这种类型的功能极大地降低了在 Hadoop 中开始数据分析探索的障碍。

在 Hadoop1 中,配置单元别无选择,只能将其 SQL 语句实现为一系列 MapReduce 作业。 当 SQL 提交给配置单元时,它会在后台生成所需的 MapReduce 作业,并在集群上执行这些作业。 这种方法有两个主要缺点:每次启动都要付出相当大的代价,而受约束的 MapReduce 模型意味着看似简单的 SQL 语句通常会被转换成一系列冗长的多个依赖的 MapReduce 作业。 这是一个更自然地概念化为任务 DAG 的处理类型的示例,如本章前面所述。

虽然在 MapReduce 中执行配置单元会带来一些好处,但在 YAR 中,当使用 TEZ 完全重新实现项目时,主要的好处在配置单元 0.13 中。 通过利用专注于提供低延迟处理的 tez API,配置单元在使其代码库变得更简单的同时获得了更高的性能。

由于 TEZ 将其工作负载视为 DAG,从而为转换后的 SQL 查询提供了更好的匹配,因此 TEZ 上的配置单元可以将任何 SQL 语句作为单个作业来执行,最大限度地提高并行度。

TEZ 通过提供始终运行的服务来帮助配置单元支持交互式查询,而不是要求在每次提交 SQL 时从头开始实例化应用。 这一点很重要,因为尽管处理海量数据的查询只需要一些时间,但我们的目标是让配置单元不再是一个批处理工具,而是尽可能多地成为一个交互式工具。

==___ _

Spark(.apache.org)是一个处理框架,擅长迭代和接近实时的处理。 它由加州大学伯克利分校创建,已作为 Apache 项目捐赠。 Spark 提供了一种抽象,允许将 Hadoop 中的数据视为可对其执行一系列操作的分布式数据结构。 该框架基于从(Dryad)获得灵感的相同概念,但在允许在内存中保存和处理数据的作业方面表现出色,而且它可以非常高效地在整个集群中调度内存中数据集的处理。 Spark 自动控制整个集群的数据复制,确保分布式数据集的每个元素都保存在至少两台机器的内存中,并提供类似于 HDFS 的基于复制的容错功能。

Spark 最初是一个独立的系统,但从 0.8 版开始,它被移植到也可以在 Yarn 上运行。 Spark 特别有趣,因为虽然它的经典处理模型是面向批处理的,但是通过 Spark shell,它提供了一个交互前端,而 Spark Streaming 子项目也提供了近乎实时的数据流处理。 Spark 对于不同的人来说是不同的;它既是一个高级 API,也是一个执行引擎。 在写这篇文章的时候,Hive 和 PIG 到星火的港口正在进行中。

== _ Apache Samza

Samza(LinkedIn)是一个流处理框架,由 http://samza.apache.org 开发并捐赠给 Apache Software Foundation。 Samza 处理概念上无限的数据流,应用将其视为一系列消息。

Samza 目前与 Apache Kafka(http://kafka.apache.org)集成最紧密,尽管它确实有一个可插拔的架构。 Kafka 本身是一个消息传递系统,它擅长大数据量,并提供基于主题的抽象,类似于大多数其他消息传递平台,如 RabbitMQ。 发布者向主题发送消息,感兴趣的客户端在消息到达时使用来自主题的消息。 Kafka 有多个方面使其有别于其他消息平台,但对于这次讨论,最有趣的一点是 Kafka 将消息存储了一段时间,这允许主题中的消息可以回放。 主题跨多个主机进行分区,并且可以跨主机复制分区以防止节点故障。

Samza 基于流的概念构建其处理流,在使用 Kafka 时,流直接映射到 Kafka 分区。 典型的 Samza 作业可能会侦听传入消息的一个主题,执行一些转换,然后将输出写入另一个主题。 然后可以组合多个 Samza 作业以提供更复杂的处理结构。

作为一个 Yarn 应用,Samza ApplicationMaster 监视所有正在运行的 Samza 任务的运行状况。 如果任务失败,则在新容器中实例化替换任务。 Samza 通过让每个任务将其进度写入新的流(同样建模为 Kafka 主题)来实现容错,因此任何替换任务只需要从该检查点主题读取最新的任务状态,然后从最后处理的位置重放主消息主题。 Samza 还提供了对本地任务状态的支持,这对于连接和聚合类型的工作负载非常有用。 此本地状态再次建立在流抽象之上,因此本质上对主机故障具有弹性。

与 Yarn 无关的框架

一个有趣的点是,前面的两个项目(Samza 和 Spark)在 Yarn 上运行,但并不特定于 Yarn。 Spark 最初是一个独立的服务,已经实现了其他调度器,比如 Apache Mesos 或在 AmazonEC2 上运行。 虽然 Samza 现在只在 Yarn 上运行,但它的架构显然不是特定于 Yarn 的,而且还有关于在其他平台上提供实现的讨论。

如果将尽可能多的应用推入应用的 Yarn 模型通过实现复杂性有其缺点,那么这种解耦就是它的主要好处之一。 为使用 YAR 编写的应用不需要绑定到它;根据定义,实际应用逻辑和管理的所有功能都封装在应用代码中,并且独立于 YAR 或其他框架。 当然,这并不是说设计一个与调度器无关的应用是一项微不足道的任务,但现在它是一项容易处理的任务;情况绝对不是这样。

今日及以后的 Yarn

虽然 Yarn 已经用于生产(在 Yahoo! 特别值得一提的是)有一段时间,最终的 GA 版本直到 2012 年底才发布。 Yarn 的界面在开发周期的很晚之前也是相当流畅的。 因此,在 Hadoop2.2 中,完全向前兼容的 Yarn 仍然是相对较新的。

Yarn 今天功能齐全,未来的发展方向将是其现有能力的延伸。 其中最值得注意的可能是在更多维度上指定和控制容器资源的能力。 目前,只有位置、内存和 CPU 规格是可能的,这将扩展到存储和网络 I/O 等领域。

此外,ApplicationMaster 目前对集装箱是否放在同一位置的管理几乎没有控制权。 这里的细粒度控制将允许 ApplicationMaster 指定容器何时可以在同一节点上调度或何时不可以调度的策略。 此外,当前的资源分配模型是非常静态的,允许应用动态更改分配给正在运行的容器的资源将非常有用。

摘要

本章探讨了如何处理我们在上一章中讨论过的大量数据。 我们特别介绍了以下内容:

  • MapReduce 如何成为 Hadoop1 及其概念模型中唯一可用的处理模型
  • MapReduce 的 Java API,以及如何使用它构建一些示例,从 Twitter 标签的字数统计到情感分析
  • 详细介绍了 MapReduce 是如何在实践中实现的,并介绍了 MapReduce 作业的执行过程
  • Hadoop 如何存储数据以及表示输入和输出格式以及记录读取器和写入器所涉及的类
  • MapReduce 的局限性导致了 Yarn 的开发,为 Hadoop 平台上的多种计算模型打开了大门
  • Yarn 架构以及如何在其上构建应用

在接下来的两章中,我们将不再严格地进行批处理,而是使用本章介绍的两个 Yarn 托管框架,即 Samza 和 Spark,深入接近实时和迭代处理的领域。

四、使用 Samza 的实时计算

上一章讨论了 YAINE,并经常提到它在 Hadoop 平台上支持的传统批处理 MapReduce 之外的计算模型和处理框架的广度。 在本章和下一章中,我们将深入探讨两个这样的项目,即 Apache Samza 和 Apache Spark。 我们之所以选择这些框架,是因为它们演示了流和迭代处理的使用,并提供了有趣的机制来组合处理范例。 在本章中,我们将探讨 Samza,并涵盖以下主题:

  • Samza 是什么,以及它如何与 YAR 和其他项目(如 Apache Kafka)集成
  • Samza 如何为流处理提供基于回调的简单接口
  • Samza 如何将多个流处理作业组合到更复杂的工作流中
  • Samza 如何支持任务中的持久本地状态,以及这如何极大地丰富了它可以实现的功能

使用 Samza 进行流处理

为了探索纯流处理平台,我们将使用 Samza,它在https://samza.apache.org上提供了。 这里显示的代码已经在当前的 0.8 版本中进行了测试,随着项目的不断发展,我们将不断更新 GitHub 存储库。

Samza 是 LinkedIn 打造的,并于 2013 年 9 月捐赠给了 Apache 软件基金会(Apache Software Foundation)。 多年来,LinkedIn 已经建立了一个模型,将他们的大部分数据概念化为流,从这个模型中,他们看到了需要一个框架,该框架可以提供一种开发人员友好的机制来处理这些无处不在的数据流。

LinkedIn 的团队意识到,当涉及到数据处理时,很多注意力都集中在数据处理上,例如,RPC 工作负载通常作为延迟要求非常低的同步系统实施,或者在批处理系统中,作业的周期通常以小时来衡量。 介于两者之间的地区得到的支持相对较少,这是 Samza 的目标领域;它的大多数工作预计响应时间从几毫秒到几分钟不等。 他们还假设数据以理论上无限的连续消息流的形式到达。

Samza 的工作原理

在开放源码世界中有许多个流处理系统,比如 Storm(http://storm.apache.org),还有许多其他(主要是商业化的)工具,比如复杂事件处理(CEP)系统,它们也面向连续消息流的处理。 这些系统有许多相似之处,但也有一些主要的不同之处。

对于 Samza 来说,最重要的区别可能是它对消息传递的假设。 许多系统非常努力地减少每条消息的延迟,有时假设目标是尽可能快地将消息传入和传出系统。 Samza 的假设几乎是相反的;它的流是持久的和有弹性的,任何写入流的消息都可以在第一次到达后的一段时间内重新读取。 正如我们将看到的,这提供了相当大的容错能力。 Samza 也建立在这种模式的基础上,让它的每一项任务都能保持地方政府的弹性。

Samza 主要是用 Scala 实现的,尽管它的公共 API 是用 Java 编写的。 我们将在本章中展示 Java 示例,但是任何 JVM 语言都可以用来实现 Samza 应用。 我们将在下一章探索 Spark 时讨论 Scala。

Samza 高级架构

Samza 认为世界有三个主要层或组件:流层、执行层和处理层。

Samza high-level architecture

桑扎建筑

流层提供对数据流的访问,用于消费和发布。 执行层提供了运行 Samza 应用、分配 CPU 和内存等资源以及管理其生命周期的方法。 处理层是实际的 Samza 框架本身,其接口允许每条消息的功能。

Samza 提供了可插拔的接口来支持前两层,尽管当前的主要实现使用 Kafka 进行流式处理,而将 YAR 用于执行。 我们将在接下来的几节中进一步讨论这些问题。

桑扎最好的朋友--Apache·卡夫卡

Samza 本身不实现实际的消息流。 相反,它为消息系统提供接口,然后与集成。 默认流实现构建在Apache Kafka(http://kafka.apache.org)之上,这也是 LinkedIn 构建的消息传递系统,但现在是一个成功且被广泛采用的开源项目。

可以将 Kafka 视为类似 RabbitMQ 或 ActiveMQ 的消息代理,但如前所述,它将所有消息写入磁盘,并作为其设计的核心部分跨多个主机进行扩展。 Kafka 通过命名主题使用发布/订阅模型的概念,生产者向这些主题写入消息,消费者从这些主题读取消息。 它们的工作原理与任何其他消息传递系统中的主题非常相似。

因为 Kafka 将所有消息写入磁盘,所以它可能不像其他消息传递系统那样具有超低延迟的消息吞吐量,其他消息传递系统关注的是尽可能快地处理消息,而不是长期存储消息。 然而,Kafka 的伸缩性非常好,它重放消息流的能力非常有用。 例如,如果消费客户端出现故障,则它可以从已知良好的时间点重新读取消息,或者如果下游算法发生变化,则可以重放流量以利用新功能。

当跨主机伸缩时,Kafka 对主题进行分区,并支持分区复制以实现容错。 每个 Kafka 消息具有与该消息相关联的密钥,并且该密钥用于决定将给定消息发送到哪个分区。 这允许进行语义上有用的分区,例如,如果密钥是系统中的用户 ID,则给定用户的所有消息都将发送到同一分区。 Kafka 保证每个分区内的有序交付,这样读取分区的任何客户端都可以知道它们正在按照生产者写入消息的顺序接收该分区中每个键的所有消息。

Samza 定期写出位置的检查点,它已经读取了它正在使用的所有流。 这些检查点消息本身就是写入卡夫卡主题的。 因此,当 Samza 作业启动时,每个任务都可以重新读取其检查点流,以了解从流中的哪个位置开始处理消息。 这意味着 Kafka 实际上也起到了缓冲区的作用;如果 Samza 作业崩溃或被关闭进行升级,消息不会丢失。 相反,作业在重新启动时只会从最后一个检查点位置重新启动。 这种缓冲功能也很重要,因为它使多个 Samza 作业更容易作为复杂工作流的一部分运行。 当 Kafka 主题是作业之间的协调点时,一个作业可能会消耗另一个作业正在写入的主题;在这种情况下,Kafka 可以帮助解决由于任何给定作业运行速度慢于其他作业而导致的问题。 传统上,在由多个作业阶段组成的系统中,运行缓慢的作业造成的背压可能是一个真正的问题,但 Kafka 作为弹性缓冲区允许每个作业以其自己的速率进行读写。 请注意,这类似于多个协调 MapReduce 作业如何将 HDFS 用于类似目的。

Kafka 提供至少一次的消息传递语义,也就是说,写入 Kafka 的任何消息都将保证对特定分区的客户端可用。 但是,消息可能会在检查点之间进行处理;客户端可能会接收到重复的消息。 有特定于应用的机制可以缓解这一问题,Kafka 和 Samza 在他们的路线图上都有只有一次的语义,但就目前而言,这是你在设计工作时应该考虑的因素。

除了演示 Samza 之外,我们不会进一步解释卡夫卡。 如果你感兴趣,可以去看看它的网站和维基;那里有很多好的信息,包括一些优秀的论文和演示文稿。

Yarn 一体化

如前所述,正如 Samza 将 Kafka 用于其流层实现一样,它将 Yarn 用于执行层。 就像第 3 章Processing-MapReduce 及以后中描述的任何 Year 应用一样,Samza 既提供了控制整个作业生命周期的ApplicationMaster实现,也提供了在每个容器中执行的特定于 Samza 的功能(称为任务)的实现。 正如 Kafka 划分其主题一样,任务是 Samza 划分其处理的机制。 每个 Kafka 分区将由单个 Samza 任务读取。 如果 Samza 作业使用多个流,那么对于分配给它的每个流分区,给定任务将是该作业中的唯一使用者。

Samza 框架由每个作业配置告知该作业感兴趣的 Kafka 流,并且 Samza 不断轮询这些流以确定是否有新消息到达。 当有新消息可用时,Samza 任务调用用户定义的回调来处理该消息,该模型对于 MapReduce 开发人员来说不应该太陌生。 此方法在名为StreamTask的接口中定义,并具有以下签名:

public void process(IncomingMessageEnvelope envelope,
 MessageCollector collector, 
 TaskCoordinator coordinator)

这是每个 Samza 任务的核心,并定义要应用于接收到的消息的功能。 接收到的待处理消息被封装在IncomingMessageEnvelope中;输出消息可以写入到MessageCollector中,并且可以通过TaskCoordinator执行任务管理(如关机)。

如前所述,Samza 在底层 Kafka 主题中为每个分区创建一个任务实例。 每个 Yarn 容器将管理这些任务中的一个或多个。 因此,总体模型是 Samza Application Master 协调多个容器,每个容器负责一个或多个StreamTask实例。

独立模型

虽然我们将在本章中专门讨论 Kafka 和 Year 作为 Samza 的流和执行层的提供者,但重要的是要记住,核心 Samza 系统对流和执行系统都使用定义良好的接口。 有多个流源的实现(我们将在下一节中看到),除了 Yarn 支持之外,Samza 还附带了一个LocalJobRunner类。 这种运行任务的替代方法可以在 JVM 上执行进程中的StreamTask个实例,而不需要完整的 Yarn 集群,后者有时是一个有用的测试和调试工具。 还讨论了在其他集群管理器或虚拟化框架之上的 Samza 实现。

你好,桑扎!

由于并不是每个人都有 ZooKeeper、Kafka 和 Yarn 簇可以使用,Samza 团队已经创建了一个很好的方法来开始使用产品。 而不是仅仅拥有一个“你好世界”! 程序中,有一个名为 Hello Samza 的存储库,您可以通过在git://git.apache.org/samza-hello-samza.git上克隆该存储库来访问该存储库。

这将下载并安装专用的 ZooKeeper、Kafka 和 Snow 实例(Samza 的三个主要前提条件),从而创建一个完整的堆栈,您可以在上面提交 Samza 作业。

还有许多示例 Samza 作业处理来自 Wikipedia 编辑通知的数据。 查看位于http://samza.apache.org/startup/hello-samza/0.8/的页面,并按照那里给出的说明进行操作。 (在撰写本文时,Samza 仍然是一个相对年轻的项目,我们不愿包含有关示例的直接信息,这些信息可能会发生变化)。

对于本章中剩下的 Samza 示例,我们假设您要么使用 Hello Samza 包来提供必要的组件(ZooKeeper/Kafka/Year),要么已经集成了每个组件的其他实例。

本例有三个相互构建的不同 Samza 作业。 第一个读取维基百科的编辑,第二个解析这些记录,第三个根据处理后的记录生成统计数据。 我们将很快构建我们自己的多数据流工作流。

有趣的是这里的 WikipediaFeed 示例;它使用 Wikipedia 而不是 Kafka 作为其消息源。 具体地说,它提供了 SamzaSystemConsumer接口的另一个实现,以允许 Samza 从外部系统读取消息。 如前所述,Samza 并不依赖于 Kafka,并且,如本例所示,构建新的流实现不必针对通用的基础设施组件;它可以是非常特定于工作的,因为所需的工作量并不是很大。

提示

请注意,ZooKeeper 和 Kafka 的默认配置都会将系统数据写入到/tmp下的目录中,如果使用 Hello Samza,这将是您已经设置的配置。 如果您使用的 Linux 发行版会在重新引导时清除此目录的内容,请务必小心。 如果您计划执行任何重要的测试,那么最好重新配置这些组件以使用较少的临时位置。 更改每个服务的相关配置文件;它们位于hello-samza/deploy目录下的服务目录中。

构建推文解析作业

让我们构建我们自己的简单作业实现来显示所需的完整代码。 我们将使用 Twitter 流的解析作为本章中的示例,稍后将从我们的客户端设置一个管道,将来自 Twitter API 的消息消费到 Kafka 主题中。 因此,我们需要一个 Samza 任务来读取 JSON 消息流,提取实际的 tweet 文本,并将这些内容写入 tweet 主题。

以下是TwitterParseStreamTask.java中的主要代码,可从https://github.com/learninghadoop2/book-examples/blob/master/ch4/src/main/java/com/learninghadoop2/samza/tasks/TwitterParseStreamTask.java获得:

package com.learninghadoop2.samza.tasks;
public class TwitterParseStreamTask implements StreamTask {
    @Override
    public void process(IncomingMessageEnvelope envelope, MessageCollector collector, TaskCoordinator coordinator) {
        String msg = ((String) envelope.getMessage());

        try {
            JSONParser parser  = new JSONParser();
            Object     obj     = parser.parse(msg);
            JSONObject jsonObj = (JSONObject) obj;
            String     text    = (String) jsonObj.get("text");

            collector.send(new OutgoingMessageEnvelope(new SystemStream("kafka", "tweets-parsed"), text));
        } catch (ParseException pe) {}
    }
  }
}

代码在很大程度上是不言而喻的,但也有一些有趣的地方。 我们使用 JSON Simple(http://code.google.com/p/json-simple/)来满足相对简单的 JSON 解析需求;在本书后面我们也将使用它。

IncomingMessageEnvelope及其对应的OutputMessageEnvelope是与实际消息数据相关的主要结构。 除了消息有效负载之外,信封还将包含有关系统、主题名称和(可选)分区号的数据,以及其他元数据。 出于我们的目的,我们只从传入消息中提取消息体,并通过新的OutgoingMessageEnvelope将从中提取的 tweet 文本发送到名为kafka的系统中名为tweets-parsed的主题。 请注意小写的名称-我们稍后将对此进行解释。

IncomingMessageEnvelope中的消息类型为java.lang.Object。 Samza 目前不强制执行数据模型,因此没有强类型的消息体。 因此,在提取消息内容时,通常需要显式强制转换。 因为每个任务都需要知道它处理的流的预期消息格式,所以这并不是看起来那么奇怪。

配置文件

在前面的代码中,没有说明消息来自哪里;框架只是将它们呈现给StreamTask实现,但显然 Samza 需要知道从哪里获取消息。 每个作业都有一个配置文件来定义这一点和更多内容。 在https://github.com/learninghadoop2/book-examples/blob/master/ch4/src/main/resources/twitter-parser.properties处可以找到twitter-parse.properties

# Job
job.factory.class=org.apache.samza.job.yarn.YarnJobFactory
job.name=twitter-parser

# YARN
yarn.package.path=file:///home/gturkington/samza/build/distributions/learninghadoop2-0.1.tar.gz

# Task
task.class=com.learninghadoop2.samza.tasks.TwitterParseStreamTask
task.inputs=kafka.tweets
task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory
task.checkpoint.system=kafka

# Normally, this would be 3, but we have only one broker.
task.checkpoint.replication.factor=1

# Serializers
serializers.registry.string.class=org.apache.samza.serializers.StringSerdeFactory

# Systems
systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory
systems.kafka.streams.tweets.samza.msg.serde=string
systems.kafka.streams.tweets-parsed.samza.msg.serde=string
systems.kafka.consumer.zookeeper.connect=localhost:2181/
systems.kafka.consumer.auto.offset.reset=largest
systems.kafka.producer.metadata.broker.list=localhost:9092
systems.kafka.producer.producer.type=sync
systems.kafka.producer.batch.num.messages=1

这个看起来可能很多,但现在我们只考虑高级结构和一些关键设置。 作业部分将 Yarn 设置为执行框架(与本地作业运行器类相对),并为作业命名。 如果我们要运行同一作业的多个副本,我们还会为每个副本分配一个唯一的 ID。任务部分指定任务的实现类以及它应该接收消息的流的名称。 序列化程序告诉 Samza 如何在流中读取和写入消息,系统部分按名称定义系统,并将实现类与它们相关联。

在我们的示例中,我们只定义了一个名为kafka的系统,并且在前一个任务中发送消息时引用此系统。 请注意,这个名称是任意的,我们可以随心所欲地称呼它。 显然,为了清楚起见,用相同的名字来称呼卡夫卡系统是有意义的,但这只是一种惯例。 特别是,在处理彼此相似的多个系统时,有时甚至在配置文件的不同部分以不同方式对待同一系统时,有时需要指定不同的名称。

在本节中,我们还将指定要与任务使用的流相关联的 SerDe。 回想一下,Kafka 消息有一个正文和一个可选的键,用于确定将消息发送到哪个分区。 Samza 需要知道如何处理这些流的密钥和消息的内容。 Samza 支持将其视为原始字节或特定类型,如前面提到的字符串、整数和 JSON。

其余的配置将在每个作业之间基本保持不变,因为它包括 ZooKeeper 集合和 Kafka 集群的位置等内容,并指定如何设置流的检查点。 Samza 允许各种各样的定制,完整的配置选项在http://samza.apache.org/learn/documentation/0.8/jobs/configuration-table.html中有详细介绍。

将推特数据导入卡夫卡

在我们运行作业之前,我们确实需要向 Kafka 发送一些 t1 消息。 让我们创建一个新的 Kafka 主题,名为twets,我们将把 tweet 写入其中。

要执行此操作和其他与 Kafka 相关的操作,我们将使用位于 Kafka 发行版的bin目录中的命令行工具。 如果您从作为 Hello Samza 应用一部分创建的堆栈中运行作业,则为deploy/kafka/bin

kafka-topics.sh是一个通用工具,可用于创建、更新和描述主题。 它的大多数用法都需要参数来指定本地动物园管理员集群的位置、Kafka 代理存储其详细信息的位置以及要操作的主题的名称。 要创建新主题,请运行以下命令:

$ kafka-topics.sh  --zookeeper localhost:2181 --create –topic tweets --partitions 1 --replication-factor 1

这将创建一个名为twets的主题,并将其分区数和复制因子显式设置为 1。如果您在本地测试 VM 中运行 Kafka,这是合适的,但很明显,生产部署将具有更多分区以跨多个代理扩展负载,并且复制因子至少为 2 以提供容错。

使用kafka-topics.sh工具的列表选项仅显示系统中的主题,或使用describe获取有关特定主题的更多详细信息:

$ kafka-topics.sh  --zookeeper localhost:2181 --describe --topic tweets
Topic:tweets    PartitionCount:1    ReplicationFactor:1    Configs:
 Topic: tweets  Partition: 0    Leader: 0    Replicas: 0    Isr: 0

多个 0 可能会造成混淆,因为它们是标签而不是计数。 系统中的每个代理都有一个 ID,通常从 0 开始,每个主题中的分区也是如此。 前面的输出告诉我们,名为tweets的主题有一个 ID 为 0 的分区,充当该分区的领导者的代理是 Broker 0,并且此分区的同步副本(ISR)的集合也只有 Broker 0。 最后一个值在处理复制时特别重要。

我们将使用前面章节中的 Python 实用程序从 Twitter 提要中提取 JSON tweet,然后使用 Kafka CLI 消息生成器将消息写入 Kafka 主题。 这不是一种非常有效的做事方式,但它适合用于插图目的。 假设我们的 Python 脚本位于主目录中,从 kafkabin目录中运行以下命令:

$ python ~/stream.py –j | ./kafka-console-producer.sh  --broker-list localhost:9092 --topic tweets

它将无限期运行,因此请注意不要让它在磁盘空间较小的测试 VM 上运行一夜,而不是因为作者曾经做过这样的事情。

运行 Samza 作业

要运行 Samza 作业,我们需要将代码与执行它所需的 Samza 组件一起打包到一个.tar.gz存档中,该存档将由 Yar NodeManager 读取。 这是 Samza 任务配置文件中的yarn.file.package属性引用的文件。

在使用单节点 Hello Samza 时,我们只能使用文件系统上的绝对路径,如前面的配置示例所示。 对于较大的 Yarn 网格上的作业,最简单的方法是将包放到 HDFS 上,并通过hdfs://URI 或在 Web 服务器上引用它(Samza 提供了一种机制,允许 Yarn 通过 HTTP 读取文件)。

因为 Samza 有多个子组件,并且每个子组件都有自己的依赖项,所以整个 Yarn 包最终可能包含很多 JAR 文件(超过 100 个!)。 此外,您需要包含 Samza 任务的自定义代码以及 Samza 发行版中的一些脚本。 这不是手工可以做的事情。 在本章的示例代码中(可在Gradle找到),我们设置了一个示例结构来保存代码和配置文件,并通过 Gradle 提供了一些自动化功能,以构建必要的任务归档并启动任务。

当位于本书的 Samza 示例代码目录的根目录中时,执行以下命令以构建包含本章所有类一起编译并与所有其他必需文件捆绑在一起的单个文件存档:

$ ./gradlew targz

此 Gradle 任务不仅将在build/distributions目录中创建必要的.tar.gz 归档,还将在build/samza-package下存储归档的扩展版本。 这将非常有用,因为我们将使用存储在归档的bin目录中的 Samza 脚本将任务实际提交给 YAR。

所以现在,让我们来做我们的工作吧。 我们需要有两个文件路径:将作业提交给 YAR 的 Samzarun-job.sh脚本和作业的配置文件。 由于我们创建的作业包将所有已编译的任务捆绑在一起,因此通过使用在task.class属性中指定特定任务实现类的不同配置文件,我们告诉 Samza 要运行哪个任务。 要实际运行任务,我们可以从build/samza-archives下的分解项目归档中运行以下命令:

$ bin/run-job.sh  --config-factory=org.apache.samza.config.factories.PropertiesConfigFactory --config-path=]config/twitter-parser.properties

为方便起见,我们添加了一个 Gradle 任务来运行此作业:

$ ./gradlew runTwitterParser

要查看作业的输出,我们将使用 Kafka CLI 客户端消费消息:

$ ./kafka-console-consumer.sh –zookeeper localhost:2181 –topic tweets-parsed

您应该看到客户端上出现了连续的 tweet 流。

备注

请注意,我们没有显式创建名为 twets-parsed 的主题。 当生产者或消费者试图使用主题时,Kafka 可以允许动态创建主题。 但在许多情况下,默认的分区和复制值可能不合适,并且需要显式创建主题以确保正确定义这些关键主题属性。

Samza 和 HDFS

您可能已经注意到,我们刚刚在讨论 Samza 时第一次提到了 HDFS。 虽然 Samza 与 Yarn 紧密集成,但它没有与 HDFS 直接集成。 在逻辑层面上,Samza 的流实现系统(如 Kafka)提供的存储层通常由 HDFS 为传统 Hadoop 工作负载提供。 如前所述,在 Samza 体系结构的术语中,在这两个模型中,YAR 是执行层,而 Samza 对源和目标数据使用流层,而 MapReduce 等框架使用 HDFS。 这是一个很好的例子,说明了 YAR 如何启用其他计算模型,这些模型不仅处理数据的方式与面向批处理的 MapReduce 非常不同,而且还可以使用完全不同的存储系统来存储它们的源数据。

窗口函数

根据在个特定时间窗口内在流上接收到的消息来生成某些数据通常很有用。 例如,记录每分钟测量的前n属性值。 Samza 通过WindowableTask接口支持这一点,该接口有以下要实现的单一方法:

  public void window(MessageCollector collector, TaskCoordinator coordinator);

这应该类似于StreamTask接口中的process方法。 但是,因为该方法是按时间计划调用的,所以它的调用不与接收到的消息相关联。 但是,MessageCollectorTaskCoordinator参数仍然存在,因为大多数可窗口化任务将生成输出消息,并且可能还希望执行一些任务管理操作。

让我们以前面的任务为例,添加一个窗口函数,该函数将输出每个窗口化时间段内收到的 tweet 数量。 这是在https://github.com/learninghadoop2/book-examples/blob/master/ch4/src/main/java/com/learninghadoop2/samza/tasks/TwitterStatisticsStreamTask.java中找到的TwitterStatisticsStreamTask.java 的主类实现:

public class TwitterStatisticsStreamTask implements StreamTask, WindowableTask {
    private int tweets = 0;

    @Override
    public void process(IncomingMessageEnvelope envelope, MessageCollector collector, TaskCoordinator coordinator) {
        tweets++;
    }

    @Override
    public void window(MessageCollector collector, TaskCoordinator coordinator) {
        collector.send(new OutgoingMessageEnvelope(new SystemStream("kafka", "tweet-stats"), "" + tweets));

        // Reset counts after windowing.
        tweets = 0;
    }
}

TwitterStatisticsStreamTask类有一个名为tweets的私有成员变量,该变量被初始化为0,并在每次调用process方法时递增。 因此,我们知道,对于从底层流实现传递到任务的每条消息,该变量都将递增。 每个 Samza 容器都有一个在循环中运行的线程,该线程对容器内的所有任务执行进程和窗口方法。 这意味着我们不需要保护实例变量不受并发修改的影响;容器内的每个任务只有一个方法可以同时执行。

在我们的window方法中,我们向名为tweet-stats的新主题发送一条消息,然后重置tweets变量。 这非常简单,唯一缺少的部分是 Samza 如何知道何时调用window方法。 我们在配置文件中指定:

task.window.ms=5000

这告诉 Samza 每隔 5 秒对每个任务实例调用window方法。 要运行window任务,需要执行 Gradle 任务:

$ ./gradlew runTwitterStatistics

如果我们现在使用kafka-console-consumer.sh监听tweet-stats流,我们将看到以下输出:

Number of tweets: 5012
Number of tweets: 5398

备注

请注意,本文中的术语窗口指的是 Samza 在概念上将消息流分割成时间范围,并提供在每个范围边界执行处理的机制。 Samza 没有直接提供该术语在滑动窗口方面的其他用法的实现,在滑动窗口中,随着时间的推移保存和处理一系列值。 然而,可窗口任务界面确实提供了实现此类滑动窗口的管道。

多作业工作流

正如我们在 Hello Samza 示例中看到的,Samza 的一些真正功能来自多个作业的组合,我们将使用文本清理作业开始演示此功能。

在接下来的部分中,我们将通过将推文与一组英语正面和负面单词进行比较来执行推文情感分析。 然而,考虑到 Twitter 信息流是多么丰富的多语种,简单地将其应用于原始 Twitter 提要将会产生非常参差不齐的结果。 我们还需要考虑文本清理、大写、频繁收缩等问题。 正如任何使用过任何重要数据集的人都知道的那样,使数据适合处理的操作通常需要大量的工作(通常是大多数工作!)。 去吧。

因此,在我们尝试检测 tweet 情绪之前,让我们先进行一些简单的文本清理;特别是,我们将只选择英文 tweet,并且在将它们发送到新的输出流之前,我们将强制它们的文本为小写。

语言检测是一个难题,为此,我们将使用 Apache Tika 库的特性(http://tika.apache.org)。 Tika 提供了广泛的功能,可以从各种来源提取文本,然后从该文本中提取更多信息。 如果使用我们的 Gradle 脚本,则 Tika 依赖项已经指定,并将自动包含在生成的作业包中。 如果通过其他机制构建,您将需要从主页下载 Tika JAR 文件并将其添加到您的 Yarn 作业包中。 可以在https://github.com/learninghadoop2/book-examples/blob/master/ch4/src/main/java/com/learninghadoop2/samza/tasks/TextCleanupStreamTask.java找到以下代码TextCleanupStreamTask.java

public class TextCleanupStreamTask implements StreamTask {
    @Override
    public void process(IncomingMessageEnvelope envelope, MessageCollector collector, TaskCoordinator coordinator) {
        String rawtext = ((String) envelope.getMessage());

        if ("en".equals(detectLanguage(rawtext))) {
            collector.send(new OutgoingMessageEnvelope(new SystemStream("kafka", "english-tweets"),
                    rawtext.toLowerCase()));
        }
    }

    private String detectLanguage(String text) {
        LanguageIdentifier li = new LanguageIdentifier(text);

        return li.getLanguage();
    }
}

这项任务非常简单,这要归功于 Tika 执行的繁重任务。 我们创建一个实用方法来包装 TikaLanguageDetector的创建和使用,然后在process方法中对每个传入消息的消息体调用此方法。 仅当应用此实用程序方法的结果为"en"(即英语的两个字母代码)时,我们才写入输出流。

此任务的配置文件类似于前一个任务的配置文件,具有任务名称和实现类的特定值。 它在存储库中的名称为textcleanup.properties,位于https://github.com/learninghadoop2/book-examples/blob/master/ch4/src/main/resources/textcleanup.properties。 我们还需要指定输入流:

task.inputs=kafka.tweets-parsed

这一点很重要,因为我们需要此任务来解析在前面任务中提取的 tweet 文本,并避免重复最好封装在一个位置的 JSON 解析逻辑。 我们可以使用以下命令运行此任务:

$ ./gradlew runTextCleanup

现在,我们可以一起运行这三个任务;TwitterParseStreamTaskTwitterStatisticsStreamTask将使用原始 tweet 流,而TextCleanupStreamTask将使用TwitterParseStreamTask的输出。

Multijob workflows

流上的数据处理

推文情感分析

我们现在将实现一个任务来执行推文情感分析,类似于我们在上一章中使用 MapReduce 所做的操作。 这还将向我们展示 Samza 提供的一种有用机制:引导流。

引导流

一般来说,大多数流处理作业(在 Samza 或其他框架中)将开始处理启动后到达的消息,通常会忽略历史消息。 由于其可重放流的概念,Samza 没有这一限制。

在我们的情绪分析工作中,我们有两套参照词:积极词和消极词。 尽管到目前为止我们还没有展示它,但是 Samza 可以使用来自多个流的消息,底层机制将轮询所有命名流,并将它们的消息一次一个地提供给process方法。 因此,我们可以为肯定词和否定词创建流,并将数据集推送到这些流上。 乍一看,我们可以计划将这两个流回溯到最早的点,并在它们到达时阅读推文。 问题是,Samza 不能保证对来自多个流的消息进行排序,即使有一种机制可以给流更高的优先级,我们也不能假设在第一条 tweet 到达之前,所有否定和肯定的词都会被处理。

对于这类场景,Samza 有引导流的概念。 如果任务定义了任何引导流,那么它将从最早的偏移量开始读取这些流,直到它们被完全处理为止(从技术上讲,它将读取这些流,直到它们被赶上,因此发送到任一流的任何新词都将被无优先级地处理,并且将在 tweet 之间交错到达)。

现在,我们将创建一个名为TweetSentimentStreamTask的新作业,该作业读取两个引导流,将其内容收集到 HashMap 中,收集情绪趋势的运行计数,并使用window函数每隔一段时间输出此数据。 此代码可在https://github.com/learninghadoop2/book-examples/blob/master/ch4/src/main/java/com/learninghadoop2/samza/tasks/TwitterSentimentStreamTask.java中找到:

public class TwitterSentimentStreamTask implements StreamTask, WindowableTask {
    private Set<String>          positiveWords  = new HashSet<String>();
    private Set<String>          negativeWords  = new HashSet<String>();
    private int                  tweets         = 0;
    private int                  positiveTweets = 0;
    private int                  negativeTweets = 0;
    private int                  maxPositive    = 0;
    private int                  maxNegative    = 0;

    @Override
    public void process(IncomingMessageEnvelope envelope, MessageCollector collector, TaskCoordinator coordinator) {
        if ("positive-words".equals(envelope.getSystemStreamPartition().getStream())) {
            positiveWords.add(((String) envelope.getMessage()));
        } else if ("negative-words".equals(envelope.getSystemStreamPartition().getStream())) {
            negativeWords.add(((String) envelope.getMessage()));
        } else if ("english-tweets".equals(envelope.getSystemStreamPartition().getStream())) {
            tweets++;

            int    positive = 0;
            int    negative = 0;
            String words    = ((String) envelope.getMessage());

            for (String word : words.split(" ")) {
                if (positiveWords.contains(word)) {
                    positive++;
                } else if (negativeWords.contains(word)) {
                    negative++;
                }
            }

            if (positive > negative) {
                positiveTweets++;
            }

            if (negative > positive) {
                negativeTweets++;
            }

            if (positive > maxPositive) {
                maxPositive = positive;
            }

            if (negative > maxNegative) {
                maxNegative = negative;
            }
        }
    }

    @Override
    public void window(MessageCollector collector, TaskCoordinator coordinator) {
        String msg = String.format("Tweets: %d Positive: %d Negative: %d MaxPositive: %d MinPositive: %d", tweets, positiveTweets, negativeTweets, maxPositive, maxNegative);

        collector.send(new OutgoingMessageEnvelope(new SystemStream("kafka", "tweet-sentiment-stats"), msg));

        // Reset counts after windowing.
        tweets         = 0;
        positiveTweets = 0;
        negativeTweets = 0;
        maxPositive    = 0;
        maxNegative    = 0;
    }

}

在这个任务中,我们添加了一些私有成员变量,我们将使用这些变量来记录整个 tweet 的数量、正数和负数,以及在单个 tweet 中看到的最大正数和负数。

此任务消耗三个 Kafka 主题。 尽管我们将配置两个作为引导流,但它们仍然是从其中接收消息的完全相同类型的 Kafka 主题;引导流的唯一区别是我们告诉 Samza 使用 Kafka 的倒带功能来完全重新读取流中的每条消息。 对于另一条推文流,我们只是在新消息到达时开始阅读它们。

如前所述,如果一个任务订阅了多个流,则相同的process方法将从每个流接收消息。 这就是为什么我们使用envelope.getSystemStreamPartition().getStream()来提取每个给定消息的流名称,然后执行相应操作的原因。 如果消息来自任何一个引导流,我们就将其内容添加到适当的哈希图中。 我们将一条推特信息分解成其构成词,测试每个词的正面或负面情绪,然后相应地更新计数。 如您所见,此任务不会将收到的 tweet 输出到另一个主题。

因为我们不执行任何直接处理,所以这样做没有意义;任何其他希望使用消息的任务都可以直接订阅传入的 twets 流。 然而,一个可能的修改可能是为每个人写正面和负面情绪的推文到专门的推文中。

window方法输出一系列计数,然后重置变量(与以前一样)。 请注意,Samza 确实支持通过 JMX 直接公开指标,这可能更适合于这样的简单窗口示例。 但是,我们没有篇幅在本书中介绍该项目的这一方面。

要运行此作业,我们需要像往常一样设置作业和任务名称来修改配置文件,但现在还需要指定多个输入流:

task.inputs=kafka.english-tweets,kafka.positive-words,kafka.negative-words

然后,我们需要指定我们的两个流是引导流,应该从最早的偏移量开始读取。 具体地说,我们为流设置了三个属性。 我们说它们将被引导,即在其他流之前完全读取,这是通过指定每个流上的偏移量需要被重置到最旧(第一)位置来实现的:

systems.kafka.streams.positive-words.samza.bootstrap=true
systems.kafka.streams.positive-words.samza.reset.offset=true
systems.kafka.streams.positive-words.samza.offset.default=oldest

systems.kafka.streams.negative-words.samza.bootstrap=true
systems.kafka.streams.negative-words.samza.reset.offset=true
systems.kafka.streams.negative-words.samza.offset.default=oldest

我们可以使用以下命令运行此作业:

$ ./gradlew runTwitterSentiment

启动作业后,查看关于tweet-sentiment-stats主题的消息的输出。

情绪检测作业将在阅读任何新检测到的小写英文推文之前引导正面和负面词流。

有了情绪检测作业,我们现在可以可视化我们的四个协作作业,如下图所示:

Bootstrap streams

引导流和协作任务

提示

要正确运行作业,似乎有必要先启动 JSON 解析器作业,然后再启动清理作业,然后再启动情感作业,但事实并非如此。 任何未读的消息都会保留在 Kafka 中进行缓冲,因此多作业工作流中的作业以什么顺序启动并不重要。 当然,在开始接收数据之前,情感作业将输出 0 条 tweet 计数,但如果流作业在它所依赖的作业之前开始,则不会中断。

有状态任务

我们将探讨的 Samza 的最后一个方面是,它如何允许处理流分区的任务具有持久的本地状态。 在前面的示例中,我们使用私有变量来跟踪运行总数,但有时任务具有更丰富的本地状态会很有用。 例如,在两个流上执行逻辑联接,从一个流构建状态模型并将其与另一个流进行比较非常有用。

备注

请注意,Samza 可以利用其分区流的概念来极大地优化加入流的行为。 如果要加入的每个流使用相同的分区键(例如,用户 ID),则使用这些流的每个任务将跨所有流接收与每个 ID 关联的所有消息。

Samza 还有另一个抽象,类似于其管理其作业和实现其任务的框架的概念。 它定义了一个可以有多个具体实现的抽象键值存储。 Samza 将现有的开源项目用于磁盘实现,并从 v0.7 开始使用 LevelDB,从 v0.8 开始添加 RocksDB。 还有一个内存中存储,它不持久保存键值数据,但在测试或潜在的非常具体的生产工作负载中可能很有用。

每个任务都可以写入这个键值存储,并且 Samza 管理其对本地实现的持久性。 为了支持持久状态,存储还被建模为流,并且对存储的所有写入也被推送到流中。 如果任务失败,则在重新启动时,它可以通过重播支持主题中的消息来恢复其本地键值存储的状态。 这里一个明显的问题是需要重放的消息的数量;然而,例如,当使用 Kafka 时,它会用相同的密钥压缩消息,因此主题中只保留最新的更新。

我们将修改前面的 tweet 情绪示例,以添加在任何 tweet 中看到的最大正面和负面情绪的终生计数。 可以在https://github.com/learninghadoop2/book-examples/blob/master/ch4/src/main/java/com/learninghadoop2/samza/tasks/TwitterStatefulSentimentStreamTask.java找到以下代码TwitterStatefulSentimentStateTask.java。 请注意,process 方法与TwitterSentimentStateTask.java相同,因此出于空间原因,我们在这里省略了它:

public class TwitterStatefulSentimentStreamTask implements StreamTask, WindowableTask, InitableTask {
    private Set<String> positiveWords  = new HashSet<String>();
    private Set<String> negativeWords  = new HashSet<String>();
    private int tweets = 0;
    private int positiveTweets = 0;
    private int negativeTweets = 0;
    private int maxPositive = 0;
    private int maxNegative = 0;
    private KeyValueStore<String, Integer> store;

    @SuppressWarnings("unchecked")
    @Override
    public void init(Config config, TaskContext context) {
        this.store = (KeyValueStore<String, Integer>) context.getStore("tweet-store");
    }

    @Override
    public void process(IncomingMessageEnvelope envelope, MessageCollector collector, TaskCoordinator coordinator) {
...
    }

    @Override
    public void window(MessageCollector collector, TaskCoordinator coordinator) {
        Integer lifetimeMaxPositive = store.get("lifetimeMaxPositive");
        Integer lifetimeMaxNegative = store.get("lifetimeMaxNegative");

        if ((lifetimeMaxPositive == null) || (maxPositive > lifetimeMaxPositive)) {
            lifetimeMaxPositive = maxPositive;
            store.put("lifetimeMaxPositive", lifetimeMaxPositive);
        }

        if ((lifetimeMaxNegative == null) || (maxNegative > lifetimeMaxNegative)) {
            lifetimeMaxNegative = maxNegative;
            store.put("lifetimeMaxNegative", lifetimeMaxNegative);
        }

        String msg =
            String.format(
                "Tweets: %d Positive: %d Negative: %d MaxPositive: %d MaxNegative: %d LifetimeMaxPositive: %d LifetimeMaxNegative: %d",
                tweets, positiveTweets, negativeTweets, maxPositive, maxNegative, lifetimeMaxPositive,
                lifetimeMaxNegative);

        collector.send(new OutgoingMessageEnvelope(new SystemStream("kafka", "tweet-stateful-sentiment-stats"), msg));

        // Reset counts after windowing.
        tweets         = 0;
        positiveTweets = 0;
        negativeTweets = 0;
        maxPositive    = 0;
        maxNegative    = 0;
    }
}

这个类实现了一个名为InitableTask的新接口。 这只有一个名为init的方法,当任务需要在开始执行之前配置其配置的个方面时使用。 我们在这里使用init()方法创建KeyValueStore类的实例,并将其存储在私有成员变量中。

顾名思义,KeyValueStore提供了熟悉的put/get类型的接口。 在本例中,我们指定键的类型为字符串,值为整数。 在我们的window方法中,我们检索之前存储的最大正面和负面情绪的值,如果当前窗口中的计数更高,则相应地更新存储。 然后,我们像以前一样输出window方法的结果。

如您所见,用户不需要处理KeyValueStore实例的本地或远程持久性的细节;这些都由 Samza 处理。 该机制的效率还使任务易于处理以保存大量的本地状态,在长时间运行的聚合或流连接等情况下可能特别有价值。

作业的配置文件可以在https://github.com/learninghadoop2/book-examples/blob/master/ch4/src/main/resources/twitter-stateful-sentiment.properties中找到。 它需要添加几个条目,如下所示:

stores.tweet-store.factory=org.apache.samza.storage.kv.KeyValueStorageEngineFactory
stores.tweet-store.changelog=kafka.twitter-stats-state
stores.tweet-store.key.serde=string
stores.tweet-store.msg.serde=integer

第一行指定存储的实现类,第二行指定用于持久状态的 Kafka 主题,最后两行指定存储键和值的类型。

要运行此作业,请使用以下命令:

$ ./gradlew runTwitterStatefulSentiment

为方便起见,以下命令将启动四个作业:JSON 解析器、文本清理、统计作业和状态情感作业:

$ ./gradlew runTasks

Samza 是一个纯粹的流处理系统,它提供其存储层和执行层的可插拔实现。 最常用的插件是 YAR 和 Kafka,它们展示了 Samza 如何在使用完全不同的存储层的情况下与 Hadoop Year 紧密集成。 Samza 仍然是一个相对较新的项目,目前的功能只是设想的一个子集。 建议查阅其网页,以获取有关其当前状况的最新信息。

摘要

本章更多地关注在 Hadoop2 上可以做些什么,特别是 Yarn,而不是 Hadoop 内部的细节。 这几乎肯定是一件好事,因为它表明 Hadoop 正在实现其目标,即成为一个不再依赖于批处理的更灵活、更通用的数据处理平台。 特别是,我们重点介绍了 Samza 如何展示可以在 YAR 上实现的处理框架可以创新并支持与 Hadoop1 中提供的功能截然不同的功能。

特别是,我们看到 Samza 如何从批处理转向延迟范围的另一端,并在单个消息到达时支持按消息处理。

我们还了解了 Samza 如何提供 MapReduce 开发人员熟悉的回调机制,但将其用于非常不同的处理模型。 我们还讨论了 Samza 使用 YAIN 作为其主要执行框架的方式,以及它如何实现第 3 章Processing-MapReduce 以及之后中描述的模型。

在下一章中,我们将改变思路,探索 Apache Spark。 虽然它的数据模型与 Samza 非常不同,但我们将看到它也有一个支持实时数据流处理的扩展,包括 Kafka 集成选项。 然而,这两个项目是如此不同,以至于它们在竞争中更具互补性。

五、使用 Spark 的迭代计算

在上一章中,我们了解了 Samza 如何在 Hadoop 中实现近乎实时的流数据处理。 这与传统的 MapReduce 批处理模型相去甚远,但仍然符合提供定义良好的接口以实现业务逻辑任务的模型。 在这一章中,我们将探讨 Apache Spark,它既可以被视为构建应用的框架,也可以被视为本身的处理框架。 不仅应用构建在 Spark 之上,Hadoop 生态系统中的整个组件也在重新实现,以使用 Spark 作为其底层处理框架。 我们将特别介绍以下主题:

  • Spark 是什么?它的核心系统如何在 Yarn 上运行
  • Spark 提供的数据模型,可实现高度可伸缩和高效的数据处理
  • 其他 Spark 组件和相关项目的广度

需要注意的是,尽管 Spark 有自己的处理流数据的机制,但这只是 Spark 提供的一部分。 最好把它看作是一个更广泛的倡议。

==___ _

ApacheSpark(MapReduce)是一个基于 https://spark.apache.org/的泛化的数据处理框架。 它最初是由加州大学伯克利分校的 AMP 实验室开发的(https://amplab.cs.berkeley.edu/)。 与 TEZ 一样,Spark 充当一个执行引擎,将数据转换建模为 DAG,并努力消除 MapReduce 的 I/O 开销,以便在规模上执行迭代计算。 虽然 Tez 的主要目标是为 Hadoop 上的 MapReduce 提供一个更快的执行引擎,但是 Spark 已经被设计为一个独立的框架和一个用于应用开发的 API。 该系统设计用于执行通用内存数据处理、流工作流以及交互和迭代计算。

Spark 是在 Scala 中实现的,Scala 是一种用于 Java VM 的静态类型编程语言,除了 Scala 本身之外,它还公开了 Java 和 Python 的本机编程接口。 请注意,尽管 Java 代码可以直接调用 Scala 接口,但类型系统的某些方面使得此类代码相当笨拙,因此我们使用本机 Java API。

Scala 附带了一个类似于 Ruby 和 Python 的交互式 shell;这允许用户从解释器交互运行 Spark 来查询任何数据集。

Scala 解释器的操作方式是为用户键入的每一行编译一个类,将其加载到 JVM 中,并在其上调用一个函数。 此类包括一个单例对象,该对象包含该行上的变量或函数,并在初始化方法中运行该行的代码。 除了其丰富的编程接口之外,Spark 正逐渐成为一个执行引擎,Hadoop 生态系统的流行工具(如 Pig 和 Have)被移植到框架中。

使用工作集的集群计算

Spark 的架构以弹性分布式数据集(RDDS)的概念为中心,这是一组 Scala 对象的只读集合,这些 Scala 对象被分区到一组可以持久存储在内存中的机器上。 这一抽象是在 2012 年的一篇研究论文弹性分布式数据集:内存中集群计算的容错抽象中提出的,可在https://www.cs.berkeley.edu/~matei/papers/2012/nsdi_spark.pdf找到。

Spark 应用由一个驱动程序组成,该驱动程序在一群工作进程和长期进程上执行并行操作,这些进程可以通过调度作为并行任务运行的函数在内存中存储数据分区,如下图所示:

Cluster computing with working sets

星火集群体系结构

流程通过 SparkContext 实例进行协调。 SparkContext 连接到资源管理器(比如 Yarn),请求工作节点上的执行器,并发送要执行的任务。 执行器负责在本地运行任务和管理内存。

Spark 允许您使用称为共享变量的抽象在任务之间或任务与驱动程序之间共享变量。 Spark 支持两种类型的共享变量:广播变量和累加器,广播变量可用于在所有节点的内存中缓存值,累加器是附加变量,如计数器和总和。

弹性分布式数据集(RDDS)

RDD 存储在内存中,跨机器共享,并用于类似 MapReduce 的并行操作。 容错是通过世系的概念实现的:如果 RDD 的一个分区丢失,则 RDD 有足够的信息说明它是如何从其他 RDD 派生出来的,从而能够仅重建该分区。 RDD 可以通过四种方式构建:

  • 通过从存储在 HDFS 中的文件读取数据
  • 通过将 Scala 集合划分-并行化-将其划分为多个分区,然后将这些分区发送给工作者
  • 通过使用并行运算符转换现有 RDD
  • 通过更改现有 RDD 的持久性

当 RDDS 可以放入内存中并且可以跨操作缓存时,Spark 就会闪耀光芒。 API 公开了方法来持久化 RDDS,并允许几种持久化策略和存储级别,从而允许溢出到磁盘以及节省空间的二进制序列化。

操作

操作通过将函数传递给 Spark 来调用。 该系统根据函数式编程范例处理变量和副作用。 闭包可以引用创建它们的作用域中的变量。 操作的示例有count(返回数据集中的元素数量)和save(将数据集输出到存储)。 在 RDDS 上的其他并行操作包括:

  • map:将函数应用于数据集的每个元素
  • filter:根据用户提供的条件从数据集中选择元素
  • reduce:使用关联函数组合数据集元素
  • collect:将数据集的所有元素发送到驱动程序
  • foreach:通过用户提供的函数传递每个元素
  • groupByKey:按提供的键将项目分组在一起
  • sortByKey:按键对项目进行排序

部署

Spark 既可以在本地模式下运行,类似于 Hadoop 单节点设置,也可以在资源管理器上运行。 当前支持的资源管理器包括:

  • Spark 独立集群模式
  • 纺 Yarn / 奇谈 / 闲聊
  • ApacheMesos

Yarn 上的火花

为了在 Yarn 上部署 Spark,需要构建一个临时合并 JAR。 Spark 在 ResourceManager 中启动独立部署的集群的一个实例。 Cloudera 和 MapR 都将 Spark on Sink 作为其软件分发的一部分。 在撰写本文时,Spark 已经作为技术预览版提供给 Hortonworks 的 HDP(http://hortonworks.com/hadoop/spark/)。

EC2 上的火花

Spark 附带了位于ec2目录中的部署脚本spark-ec2。 此脚本自动在 EC2 实例集群上设置 Spark 和 HDFS。 要在 Amazon 云上启动 Spark 集群,请转到ec2目录并运行以下命令:

./spark-ec2 -k <keypair> -i <key-file> -s <num-slaves> launch <cluster-name>

这里,<keypair>是 EC2 密钥对的名称,<key-file>是密钥对的私钥文件,<num-slaves>是要启动的从节点数,<cluster-name>是要赋予集群的名称。 有关密钥对设置的更多详细信息,请参见第 1 章简介,并通过转到集群调度程序的 Web UI(脚本完成后将打印其地址)来验证集群调度程序是否已启动并看到所有从属程序。

您可以通过形式为s3n://<bucket>/path的 URI 指定 S3 中的路径作为输入。 您还需要设置 Amazon 安全凭据,方法是在执行程序之前设置环境变量AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY,或者通过SparkContext.hadoopConfiguration

Spark 入门

Spark 二进制文件和源代码可以在项目网站http://spark.apache.org/上找到。 以下部分中的示例使用 Cloudera CDH5.0 QuickStart VM 上从源代码构建的 Spark 1.1.0 进行了测试。

使用以下命令下载并解压缩gzip档案:

$ wget http://d3kbcqa49mib13.cloudfront.net/spark-1.1.0.tgz 
$ tar xvzf spark-1.1.0.tgz
$ cd spark-1.1.0

Spark 在 Scala2.10 上构建,并使用sbt(https://github.com/sbt/sbt)构建源代码核心和相关示例:

$ ./sbt/sbt -Dhadoop.version=2.2.0  -Pyarn  assembly

使用-Dhadoop.version=2.2.0-Pyarn选项,我们指示sbt针对 Hadoop 版本 2.2.0 或更高版本进行构建,并启用 Yarn 支持。

使用以下命令在独立模式下启动 Spark:

$ ./sbin/start-all.sh 

该命令将在spark://localhost:7077启动一个本地主实例以及一个工作节点。

可以在http://localhost:8080/访问到主节点的 Web 界面,如以下屏幕截图所示:

Getting started with Spark

主节点 Web 界面

Spark 可以通过spark-shell交互运行,spark-shell是 Scala shell 的修改版本。 作为第一个示例,我们将使用 Scala API 对我们在第 3 章处理-MapReduce 以及之后的中使用的 Twitter 数据集进行字数统计。

通过运行以下命令启动交互式spark-shell会话:

$ ./bin/spark-shell

外壳程序实例化一个SparkContext对象sc,该对象负责处理到工作进程的驱动程序连接。 我们将在本章后面描述它的语义。

为简单起见,让我们创建一个示例文本数据集,该数据集每行包含一个状态更新:

$ stream.py -t -n 1000 > sample.txt

然后,将其复制到 HDFS:

$ hdfs dfs -put sample.txt /tmp

spark-shell内,我们首先从样本数据创建一个RDD - file-:

val file = sc.textFile("/tmp/sample.txt")

然后,我们应用一系列转换来计算文件中出现的单词。 请注意,转换链counts的输出仍然是 RDD:

val counts = file.flatMap(line => line.split(" "))
.map(word => (word, 1))
.reduceByKey((m, n) => m + n)  

此转换链对应于我们熟悉的映射和缩减阶段。 在映射阶段,我们加载数据集的每一行(flatMap),将每条 tweet 标记为一个单词序列,计算每个单词(map)的出现次数,并发出(key, value)对。 在 Reduce 阶段,我们按键(word)和总和值(m, n)分组以获得字数。

最后,我们将前个元素counts.take(10)打印到控制台:

counts.take(10).foreach(println)

编写和运行独立应用

Spark 允许使用三个 API 编写独立应用:Scala、Java 和 Python。

_API 比例

Spark 驱动程序必须做的第一件事是创建一个SparkContext对象,该对象告诉 Spark 如何访问集群。 将类和隐式转换导入程序后,如下所示:

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

可以使用以下构造函数创建SparkContext对象:

new SparkContext(master, appName, [sparkHome]) 

也可以通过接受SparkConf对象的SparkContext(conf)创建它。

主参数是指定要连接到的集群 URI 的字符串(如spark://localhost:7077)或要在本地模式下运行的 local字符串。 appName术语是将在集群 Web 用户界面中显示的应用名称。

不可能覆盖默认的SparkContext类,也不可能在运行的 Spark shell 中创建新的类。 但是,可以使用MASTER环境变量指定上下文连接到哪个主服务器。 例如,要在四个内核上运行 spark-shell,请使用以下命令:

$ MASTER=local[4] ./bin/spark-shell 

Колибриобработает

org.apache.spark.api.java包向 Java 公开了 Scala 版本中所有可用的 Spark 特性。 Java API 有一个JavaSparkContext类,它返回org.apache.spark.api.java.JavaRDD的实例,并且使用 Java 集合而不是 Scala 集合。

Java 和 Scala API 之间有几个主要区别:

  • Java7 不支持匿名函数或一级函数;因此,必须通过扩展org.apache.spark.api.java.function.FunctionFunction2和其他类来实现函数。 从 Spark Version 1.0 开始,API 已经进行了重构,以支持 Java8lambda 表达式。 在 Java8 中,函数类可以替换为内联表达式,这些表达式充当匿名函数的简写。
  • RDD 方法返回 Java 集合
  • 键-值对在 Scala 中简称为(keyvalue),由scala.Tuple2类表示。
  • 为了维护类型安全,一些 RDD 和函数方法(如处理密钥对和 Double 的方法)被实现为专用的类。

Java 中的字数统计

examples/src/main/java/org/apache/spark/examples/JavaWordCount.java处的 Spark 源代码发行版中包含了一个 Java 中的字数计数示例。

首先,我们使用JavaSparkContext类创建上下文:

   JavaSparkContext sc = new JavaSparkContext(master, "JavaWordCount",
     System.getenv("SPARK_HOME"), JavaSparkContext.jarOfClass(JavaWordCount.class));

    JavaRDD<String> data = sc.textFile(infile, 1);
    JavaRDD<String> words = data.flatMap(new FlatMapFunction<String, String>() {
      @Override
      public Iterable<String> call(String s) {
        return Arrays.asList(s.split(" "));
      }
    });

    JavaPairRDD<String, Integer> ones = words.map(new PairFunction<String, String, Integer>() {
      @Override
      public Tuple2<String, Integer> call(String s) {
        return new Tuple2<String, Integer>(s, 1);
      }
    });

    JavaPairRDD<String, Integer> counts = ones.reduceByKey(new Function2<Integer, Integer, Integer>() {
      @Override
      public Integer call(Integer i1, Integer i2) {
        return i1 + i2;
      }
    });

然后,我们从 HDFS 位置infile构建 RDD。 在转换链的第一步中,我们对数据集中的每个 tweet 进行标记化,并返回一个单词列表。 我们使用JavaPairRDD<String, Integer>的实例来计算每个单词出现的次数。 最后,我们将 RDD 简化为一个新的JavaPairRDD<String, Integer>实例,该实例包含一个元组列表,每个表示一个单词及其在数据集中被发现的次数。

Python API

PySpark 需要 Python 版本 2.6 或更高版本。 RDDS 支持与 Scala 对应的相同方法,但采用 Python 函数并返回 Python 集合类型。 Lambda 语法(https://docs.python.org/2/reference/expressions.html)用于将函数传递给 RDDS。

pyspark中的字数与其对应的 Scala 中的字数相对相似:

tweets = sc.textFile("/tmp/sample.txt")
counts = tweets.flatMap(lambda tweet: tweet.split(' ')) \
                  .map(lambda word: (word, 1)) \
                  .reduceByKey(lambda m,n:m+n)

lambda构造在运行时创建匿名函数。 lambda tweet: tweet.split(' ')创建一个函数,该函数接受字符串tweet作为输入,并输出由空格分隔的字符串列表。 Spark 的flatMap将此函数应用于tweets数据集的每一行。 在map阶段,对于每个word标记,lambda word: (word, 1)返回指示数据集中出现单词的(word, 1)个元组。 在reduceByKey中,我们按关键字对这些元组进行分组,然后将这些值相加,以获得与lambda m,n:m+n的字数。

星火生态系统

Apache Spark 支持许多工具,既可以作为库,也可以作为执行引擎。

火花流

SparkStreaming(位于Scala)是 http://spark.apache.org/docs/latest/streaming-programming-guide.html API 的扩展,允许从 Kafka、Flume、Twitter、ZeroMQ 和 TCPSocket 等流获取数据。

Spark Streaming 接收实时输入数据流,并将数据分成批(任意大小的时间窗口),然后由 Spark 核心引擎处理,以批量生成最终结果流。 这种高级抽象称为 DStream(org.apache.spark.streaming.dstream.DStreams),并以 RDDS 序列的形式实现。 DStream 允许两种操作:转换输出操作。 转换作用于一个或多个 DStream 以创建新的 DStream。 作为转换链的一部分,数据可以持久化到存储层(HDFS)或输出通道。 火花流允许在数据的滑动窗口上进行转换。 基于窗口的操作需要指定两个参数:窗口长度、窗口持续时间和滑动间隔,即执行基于窗口的操作的间隔。

GraphX

GraphX(位于Pregel)是一种用于图计算的 API,它公开了一组用于面向图的计算的运算符和算法,以及 https://spark.apache.org/docs/latest/graphx-programming-guide.html 的优化变体。

帖子主题:Re:Колибри

MLlib(位于http://spark.apache.org/docs/latest/mllib-guide.html)提供常见的机器学习(ML)功能,包括测试和数据生成器。 MLlib 目前支持四种类型的算法:二进制分类、回归、聚类和协作过滤。

火花 SQL

Spark SQL 派生自 Shark,Shark 是使用 Spark 作为执行引擎的 Hive 数据仓库系统的实现。 我们将在第 7 章Hadoop 和 SQL中讨论配置单元。 使用 Spark SQL,可以将类似 SQL 的查询与 Scala 或 Python 代码混合使用。 查询返回的结果集本身就是 RDDS,因此,它们可以由 Spark 核心方法或 MLlib 和 GraphX 操作。

使用 Apache Spark 处理数据

在本节中,我们将使用 Scala API 实现第 3 章Processing-MapReduce 以及以外的示例。 我们将考虑批处理和实时处理场景。 我们将向您展示如何使用 Spark Streaming 来计算实时 Twitter 流的统计数据。

构建和运行示例

示例的 scala 源代码可以在https://github.com/learninghadoop2/book-examples/tree/master/ch5中找到。 我们将使用sbt来构建、管理和执行代码。

build.sbt文件控制代码基元数据和软件依赖关系;这些包括 Spark 链接到的 Scala 解释器的版本、用于解析隐式依赖关系的 Akka 包库的链接以及对 Spark 和 Hadoop 库的依赖关系。

所有示例的源代码都可以用以下命令编译:

$ sbt compile

或者,可以使用以下命令将其打包到 JAR 文件中:

$ sbt package

可以使用以下命令生成用于执行编译类的帮助器脚本:

$ sbt add-start-script-tasks
$ sbt start-script

可以按如下方式调用帮助器:

$ target/start <class name> <master> <param1> … <param n>

这里,<master>是主节点的 URI。 可以使用以下命令通过sbt调用交互式 Scala 会话:

$ sbt console

该控制台与 Spark 交互式 shell 不同;相反,它是执行代码的另一种方式。 为了在其中运行 Spark 代码,我们需要手动导入和实例化一个SparkContext对象。 本节中提供的所有示例都期望包含使用者密钥和机密以及访问令牌的twitter4j.properties文件位于调用sbtspark-shell的同一目录中:

oauth.consumerKey=
oauth.consumerSecret=
oauth.accessToken=
oauth.accessTokenSecret=

在 Yarn 上运行示例

要在 Yarn 网格上运行示例,我们首先使用以下命令构建一个 JAR 文件:

$ sbt package

然后,我们使用spark-submit命令将其发送到资源管理器:

./bin/spark-submit --class application.to.execute --master yarn-cluster [options] target/scala-2.10/chapter-4_2.10-1.0.jar [<param1> … <param n>]

与独立模式不同,我们不需要指定<master>URI。 在 YAR 中,ResourceManager 是从集群配置中选择的。 有关在 Yarn 中发射火花的更多信息,请参阅http://spark.apache.org/docs/latest/running-on-yarn.html

查找热门话题

与前面使用 Spark shell 的示例不同,我们将初始化SparkContext作为程序的一部分。 我们将三个参数传递给SparkContext构造函数:我们要使用的调度器类型、应用的名称和安装 Spark 的目录:

import org.apache.spark.SparkContext._
import org.apache.spark.SparkContext
import scala.util.matching.Regex

object HashtagCount {
  def main(args: Array[String]) {
[…]
  val sc = new SparkContext(master, 
"HashtagCount", 
System.getenv("SPARK_HOME"))

    val file = sc.textFile(inputFile)
    val pattern = new Regex("(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)")

    val counts = file.flatMap(line => 
      (pattern findAllIn line).toList)
        .map(word => (word, 1))
        .reduceByKey((m, n) => m + n)  

    counts.saveAsTextFile(outputPath)
  }
}

我们从 HDFS(InputFile)中存储的数据集创建初始 RDD,并应用与 Wordcount 示例类似的逻辑。

对于数据集中的每条 tweet,我们提取与 hashtag 模式(pattern findAllIn line).toArray匹配的字符串数组,并使用 map 操作符计算每个字符串的出现次数。 这将以元组列表的形式生成一个新的 RDD,格式为:

(word, 1), (word2, 1), (word, 1) 

最后,我们使用reduceByKey()方法将这个 RDD 的元素组合在一起。 我们使用saveAsTextFile将最后一步生成的 RDD 存储回 HDFS。

独立驱动程序的代码位于https://github.com/learninghadoop2/book-examples/blob/master/ch5/src/main/scala/com/learninghadoop2/spark/HashTagCount.scala

为主题分配情感

本例的源代码位于https://github.com/learninghadoop2/book-examples/blob/master/ch5/src/main/scala/com/learninghadoop2/spark/HashTagSentiment.scala,代码如下:

import org.apache.spark.SparkContext._
import org.apache.spark.SparkContext
import scala.util.matching.Regex
import scala.io.Source

object HashtagSentiment {
  def main(args: Array[String]) {
   […]
    val sc = new SparkContext(master, 
"HashtagSentiment", 
System.getenv("SPARK_HOME"))

    val file = sc.textFile(inputFile)

    val positive = Source.fromFile(positiveWordsPath)
      .getLines
      .filterNot(_ startsWith ";")
      .toSet
    val negative = Source.fromFile(negativeWordsPath)
      .getLines
      .filterNot(_ startsWith ";")
      .toSet

    val pattern = new Regex("(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)")
    val counts = file.flatMap(line => (pattern findAllIn line).map({
    word => (word, sentimentScore(line, positive, negative)) 
    })).reduceByKey({ (m, n) => (m._1 + n._1, m._2 + n._2) })

    val sentiment = counts.map({hashtagScore =>
    val hashtag = hashtagScore._1
    val score = hashtagScore._2
    val normalizedScore = score._1 / score._2
    (hashtag, normalizedScore)
    })

    sentiment.saveAsTextFile(outputPath)
  }
}

首先,我们将正面和负面单词列表读取到 ScalaSet对象中,并过滤掉注释(以;开头的字符串)。

当找到一个标签时,我们调用一个函数-sentimentScore-来估计该给定文本所表达的情感。 此函数实现的逻辑与我们在第 3 章处理-MapReduce 及之后中使用的逻辑相同,用于估计 tweet 的情绪。 它将 tweet 的文本str以及正面和负面单词列表作为Set[String]对象作为输入参数。 返回值是正负分数与 tweet 中的字数之差。 在 Spark 中,我们将此返回值表示为一对DoubleInteger对象:

def sentimentScore(str: String, positive: Set[String], 
         negative: Set[String]): (Double, Int) = {
   var positiveScore = 0; var negativeScore = 0;
    str.split("""\s+""").foreach { w =>
      if (positive.contains(w)) { positiveScore+=1; }
      if (negative.contains(w)) { negativeScore+=1; }
    } 
    ((positiveScore - negativeScore).toDouble, 
           str.split("""\s+""").length)
}

我们通过按键(Hashtag)聚合来减少映射输出。 在这个阶段,我们发出一个由标签、正负分数之差之和和每条 tweet 的字数组成的三元组。 我们使用额外的 MAP 步骤来归一化情感得分,并将得到的标签和情感对列表存储到 HDFS 中。

流上的数据处理

前面的示例可以很容易地调整为处理实时数据流。 在本节和下一节中,我们将使用spark-streaming-twitter对实时消防软管执行一些简单的分析任务:

  val window = 10
  val ssc = new StreamingContext(master, "TwitterStreamEcho", Seconds(window), System.getenv("SPARK_HOME"))

  val stream = TwitterUtils.createStream(ssc, auth)

  val tweets = stream.map(tweet => (tweet.getText()))
  tweets.print()

  ssc.start()
  ssc.awaitTermination()
}   

本例的 scala 源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch5/src/main/scala/com/learninghadoop2/spark/TwitterStreamEcho.scala中找到。

我们需要导入的两个关键包是:

import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.twitter._

我们使用一个 10 秒的窗口在本地集群上初始化一个新的StreamingContext ssc,并使用该上下文创建我们打印其文本的 tweet 的DStream条。

在成功执行后,Twitter 的实时消防软管将以 10 秒为一批的数据在终端中回放。 请注意,计算将无限期继续,但可以通过按Ctrl+C随时中断。

TwitterUtils对象是spark-streaming-twitter附带的Twitter4j库(http://twitter4j.org/en/index.html)的包装器。 成功调用TwitterUtils.createStream将返回Twitter4j个对象(TwitterInputDStream)的 DStream。 在前面的示例中,我们使用getText()方法提取 tweet 文本;但是,请注意,twitter4j对象公开了完整的 Twitter API。 例如,我们可以使用以下调用打印用户流:

val users = stream.map(tweet => (tweet.getUser().getId(), tweet.getUser().getName()))
users.print()

状态管理

Spark 流提供了一个特别的 DStream 来保持 RDD 中每个键的状态,并提供了updateStateByKey方法来改变状态。

我们可以重用批处理示例的代码来分配和更新流上的情感分数:

object StreamingHashTagSentiment {
[…]

    val counts = text.flatMap(line => (pattern findAllIn line)
      .toList
      .map(word => (word, sentimentScore(line, positive, negative))))
      .reduceByKey({ (m, n) => (m._1 + n._1, m._2 + n._2) })

    val sentiment = counts.map({hashtagScore =>
        val hashtag = hashtagScore._1
        val score = hashtagScore._2
        val normalizedScore = score._1 / score._2
        (hashtag, normalizedScore)
    })

    val stateDstream = sentiment
         .updateStateByKey[Double](updateFunc)

    stateDstream.print

    ssc.checkpoint("/tmp/checkpoint")
    ssc.start()
}

通过调用hashtagSentiment.updateStateByKey创建状态 DStream。

updateFunc函数实现状态突变逻辑,它是一段时间内情绪得分的累积和:

    val updateFunc = (values: Seq[Double], state: Option[Double]) => {
      val currentScore = values.sum

      val previousScore = state.getOrElse(0.0)

      Some( (currentScore + previousScore) * decayFactor)
    }   

decayFactor是一个常量值,小于或等于零,我们用它来随着时间的推移按比例减少分数。 直观地说,如果标签不再流行,这将使它们褪色。 Spark Streaming 将有状态操作的中间数据写入 HDFS,因此我们需要使用ssc.checkpoint检查点数据流上下文。

本例的源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch5/src/main/scala/com/learninghadoop2/spark/StreamingHashTagSentiment.scala找到。

使用 Spark SQL 进行数据分析

SPARKSQL 可以简化表示和操作结构化数据的任务。 我们将 JSON 文件加载到临时表中,并通过混合 SQL 语句和 Scala 代码来计算简单的统计数据:

object SparkJson {
   […]
   val file = sc.textFile(inputFile)

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

   val tweets = sqlContext.jsonFile(inFile)
   tweets.printSchema()

   // Register the SchemaRDD as a table
   tweets.registerTempTable("tweets")
   val text = sqlContext.sql("SELECT text, user.id FROM tweets")

   // Find the ten most popular hashtags
   val pattern = new Regex("(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)")

   val counts = text.flatMap(sqlRow => (pattern findAllIn sqlRow(0).toString).toList)
            .map(word => (word, 1))
            .reduceByKey( (m, n) => m+n)
   counts.registerTempTable("hashtag_frequency")

counts.printSchema

val top10 = sqlContext.sql("SELECT _1 as hashtag, _2 as frequency FROM hashtag_frequency order by frequency desc limit 10")

top10.foreach(println)
}

与前面的示例一样,我们实例化一个SparkContext sc并加载 JSON tweet 的数据集。 然后,我们基于现有的sc创建org.apache.spark.sql.SQLContext的实例。 import sqlContext._提供对sqlContext的所有函数和隐式约定的访问。 我们使用sqlContext.jsonFile加载 tweet 的 JSON 数据集。 得到的tweets对象是SchemaRDD的一个实例,它是 Spark SQL 引入的一种新的类型的 RDD。 SchemaRDD类在概念上类似于关系数据库中的表;它由Row对象和描述每个Row中内容的模式组成。 我们可以通过调用tweets.printSchema()来查看 tweet 的模式。 在我们能够使用 SQL 语句操作 tweet 之前,我们需要将SchemaRDD注册为SQLContext中的一个表。 然后,我们使用 SQL 查询提取 JSON tweet 的文本字段。 请注意,sqlContext.sql的输出再次是 RDD。 因此,我们可以使用 Spark 核心方法来操作它。 在我们的例子中,我们重用前面示例中使用的逻辑来提取 hashtag 并计算它们的出现次数。 最后,我们将结果 RDD 注册为表hashtag_frequency,并使用 SQL 查询按频率对标签进行排序。

此示例的源代码可在https://github.com/learninghadoop2/book-examples/blob/master/ch5/src/main/scala/com/learninghadoop2/spark/SparkJson.scala中找到。

数据流上的 SQL

在写入时,不能从StreamingContext对象直接实例化SQLContext。 但是,可以通过为给定流中的每个 RDD 注册SchemaRDD来查询 DStream:

object SqlOnStream {
[…]

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

    val gson = new Gson()

    val dstream = TwitterUtils
   .createStream(ssc, auth)
   .map(gson.toJson(_))

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

   dstream.foreachRDD( rdd => {
      rdd.foreach(println)
        val jsonRDD = sqlContext.jsonRDD(rdd)
        jsonRDD.registerTempTable("tweets")
        jsonRDD.printSchema 

         sqlContext.sql(query)
    })

    ssc.checkpoint("/tmp/checkpoint")
    ssc.start() 
    ssc.awaitTermination() 
}

为了让两者协同工作,我们首先创建一个SparkContext sc,我们使用它来初始化 aStreamingContext ssc和 asqlContext。 与前面的示例一样,我们使用TwitterUtils.createStream创建 DStream RDDdstream。 在本例中,我们使用 Google 的 gson JSON 解析器将每个twitter4j对象序列化为 JSON 字符串。 为了在流上执行 Spark SQL 查询,我们在dstream.foreachRDD循环中注册了一个SchemaRDD jsonRDD。 我们使用sqlContext.jsonRDD方法从一批 JSON tweet 创建 RDD。 此时,我们可以使用sqlContext.sql方法查询SchemaRDD

本例的源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch5/src/main/scala/com/learninghadoop2/spark/SqlOnStream.scala找到。

Samza 和 Spark Streaming 的比较

比较 Samza 和 Spark Streaming 有助于确定各自最适用的领域。 正如希望在本书中所阐明的那样,这些技术非常具有互补性。 尽管 Spark Streaming 可能看起来与 Samza 竞争,但我们觉得这两款产品在某些领域都提供了令人信服的优势。

当输入数据确实是离散事件流,并且您希望构建对这种类型的输入进行操作的处理时,Samza 就会大放异彩。 在 Kafka 上运行的 Samza 作业的延迟可能在毫秒量级。 这提供了一个专注于单个消息的编程模型,更适合真正接近实时的处理应用。 尽管它缺乏构建协作作业拓扑的支持,但其简单的模型允许构建类似的结构,或许更重要的是,它很容易推理。 它的分区和伸缩模型也注重简单性,这再次使 Samza 应用非常易于理解,并使其在处理像实时数据这样本质复杂的事情时具有显著优势。

Spark 不仅仅是一款流媒体产品。 它支持从现有数据集构建分布式数据结构,并使用强大的原语来操作这些结构,这使它能够在更高的粒度级别处理大型数据集。 Spark 生态系统中的其他产品在这个通用的批处理核心上构建额外的接口或抽象。 这与 Samza 的消息流模型非常不同。

当我们查看 Spark Streaming 时,也演示了此批处理模型;它将消息流分割为一系列 RDD,而不是按消息处理模型。 使用快速执行引擎,这意味着延迟低至 1 秒(http://www.cs.berkeley.edu/~matei/papers/2012/hotcloud_spark_streaming.pdf)。 对于希望以这种方式分析流的工作负载,这将比 Samza 的每消息模型更合适,后者需要额外的逻辑来提供这种窗口。

摘要

本章探讨了 Spark,并向您展示了它如何将迭代处理添加为一个新的丰富框架,可以在其上构建应用。 我们特别强调了:

  • 基于分布式数据结构的 Spark 处理模型及其如何实现高效的内存数据处理
  • 更广泛的 Spark 生态系统,以及如何在其上构建多个附加项目以进一步专门化计算模型

在下一章中,我们将探索 Apache Pig 及其编程语言 Pig 拉丁语。 我们将看到这个工具如何通过抽象掉 MapReduce 和 Spark 的一些复杂性来极大地简化 Hadoop 的软件开发。

六、使用 Apache Pig 的数据分析

在前面的章节中,我们探索了一些用于数据处理的 API。 MapReduce、Spark、Tez 和 Samza 是相当低级的,使用它们编写重要的业务逻辑通常需要大量的 Java 开发。 而且,不同的用户会有不同的需求。 对于分析师来说,编写 MapReduce 代码或构建输入和输出的 DAG 来回答一些简单的查询可能是不切实际的。 同时,软件工程师或研究人员可能希望在进入低级实现细节之前,使用高级抽象来构建想法和算法的原型。

在本章和下一章中,我们将探索一些工具,这些工具提供了一种使用更高级别的抽象在 HDFS 上处理数据的方法。 在本章中,我们将探讨 Apache Pig,特别是将涵盖以下主题:

  • 什么是 Apache Pig 及其提供的数据流模型
  • PIG 拉丁语的数据类型和函数
  • 如何使用自定义用户代码轻松增强 Pig
  • 我们如何使用 Pig 来分析 Twitter 流

小 PIG 一览

从历史上看,Pig 工具包由一个编译器组成,该编译器生成 MapReduce 程序,绑定它们的依赖项,并在 Hadoop 上执行它们。 PIG 作业在中用名为Pig Lat****的语言编写,可以交互方式和批处理方式执行。 此外,可以使用用 Java、Python、Ruby、Groovy 或 JavaScript 编写的用户定义函数(UDF)来扩展 Pig 拉丁语。

清管器使用案例包括以下内容:

  • 数据处理
  • 即席分析查询
  • 算法的快速原型设计
  • 提取变换加载管道

遵循我们在前几章中看到的趋势,Pig 正在向通用计算架构迈进。 从 0.13 版本开始,ExecutionEngine接口(org.apache.pig.backend.executionengine)充当 Pig 前端和后端之间的桥梁,允许在 MapReduce 以外的框架上编译和执行 Pig 拉丁文脚本。 在撰写本文时,版本 0.13 附带了MRExecutionEngine(org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MRExecutionEngine),并基于TEZ(org.apache.pig.backend.hadoop.executionengine.tez.*)开发了一个低延迟后端,预计版本 0.14(请参阅https://issues.apache.org/jira/browse/PIG-3446)中也将包含该版本(参见MRExecutionEngine(org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MRExecutionEngine))。 开发分支目前正在进行集成 Spark 的工作(参见https://issues.apache.org/jira/browse/PIG-4059)。

Pig0.13 为 MapReduce 后端提供了大量的性能增强,特别是减少小型作业延迟的两个特性:直接 HDFS 访问(https://issues.apache.org/jira/browse/PIG-3642)和自动本地模式(https://issues.apache.org/jira/browse/PIG-3463)。 默认情况下,直接 HDFS(opt.fetch属性)处于打开状态。 当在只包含LIMITFILTERUNIONSTREAMFOREACH运算符的简单(仅地图)脚本中执行DUMP时,从 HDFS 获取输入数据,并绕过 MapReduce 直接在 Pig 中执行查询。 使用 AUTO LOCAL(pig.auto.local.enabled属性),当数据大小小于pig.auto.local.input.maxbytes时,Pig 将在 Hadoop 本地模式下运行查询。 默认情况下,自动本地功能处于关闭状态。

如果两种模式都关闭,或者查询不符合任何一种条件,PIG 将启动 MapReduce 作业。 如果这两种模式都打开,Pig 将检查查询是否符合直接访问的条件,如果不符合,则回退到 AUTO LOCAL。 否则,它将在 MapReduce 上执行查询。

入门

我们将使用stream.py脚本选项来提取 JSON 数据并检索特定数量的 tweet;我们可以使用如下命令运行:

$ python stream.py -j -n 10000 > tweets.json

tweets.json文件的每一行将包含一个 JSON 字符串,表示一条 tweet。

请记住,Twitter API 凭据需要作为环境变量提供,或者硬编码在脚本本身中。

奔跑的 PIG

PIG 是一个工具,它翻译用 Pig 拉丁文编写的语句,并在独立模式下在单机上执行它们,或者在分布式模式下在完整的 Hadoop 集群上执行它们。 即使在后者中,Pig 的角色也是将 Pig 拉丁语语句转换为 MapReduce 作业,因此不需要安装额外的服务或守护程序。 它与其关联库一起用作命令行工具。

Cloudera CDH 附带 Apache Pig 版本 0.12。 或者,PIG 源代码和二进制分布可以在https://pig.apache.org/releases.html处获得。

正如预期的那样,MapReduce 模式需要访问 Hadoop 集群和 HDFS 安装。 MapReduce 模式是在命令行提示下运行 Pig 命令时执行的默认模式。 可以使用以下命令执行脚本:

$ pig -f <script>

可以使用-param <param>=<val>通过命令行传递参数,如下所示:

$ pig –param input=tweets.txt

也可以在param文件中指定参数,该文件可以使用-param_file <file>选项传递给 Pig。 可以指定多个文件。 如果一个参数在文件中出现多次,将使用最后一个值,并显示警告。 参数文件每行包含一个参数。 允许空行和注释(通过以#开头的行来指定)。 在 Pig 脚本中,参数的形式为$<parameter>。 可以使用default语句分配默认值:%default input tweets.json'default命令在 Grunt 会话中不起作用;我们将在下一节中讨论 Grunt。

在本地模式下,所有文件都使用本地主机和文件系统安装和运行。 使用-x标志指定本地模式:

$ pig -x local

在这两种执行模式下,Pig 程序既可以在交互式外壳中运行,也可以在批处理模式下运行。

Grunt-Pig 互动外壳

PIG 可以使用 Grunt shell 在交互模式下运行,当我们在终端提示符下使用pig命令时会调用该 shell。 在本章的其余部分中,我们将假设示例是在 Grunt 会话中执行的。 除了执行 Pig 拉丁语语句之外,Grunt 还提供了许多实用程序和对 shell 命令的访问:

  • fs:允许用户操作 Hadoop 文件系统对象,并具有与 Hadoop CLI 相同的语义
  • sh:通过操作系统外壳执行命令
  • exec:在交互式 Grunt 会话中启动 Pig 脚本
  • kill:终止 MapReduce 作业
  • help:打印所有可用命令的列表

弹性 MapReduce

通过使用--applications Name=Pig,Args=--version,<version>创建集群,可以在 EMR 上执行 PIG 脚本,如下所示:

$ aws emr create-cluster \
--name "Pig cluster" \
--ami-version <ami version> \
--instance-type <EC2 instance> \
--instance-count <number of nodes> \
--applications Name=Pig,Args=--version,<version>\
--log-uri <S3 bucket> \
--steps Type=PIG,\ 
Name="Pig script",\
Args=[-f,s3://<script location>,\
-p,input=<input param>,\
-p,output=<output param>]

前面的命令将提供一个新的 EMR 集群并执行s3://<script location>。 请注意,要执行的脚本以及输入(-p input)和输出(-p output)路径预计位于 S3 上。

作为创建新 EMR 集群的替代方法,可以使用以下命令将 Pig Steps 添加到已经实例化的 EMR 集群:

$ aws emr add-steps \
--cluster-id <cluster id>\
--steps Type=PIG,\ 
Name= "Other Pig script",\
Args=[-f,s3://<script location>,\
-p,input=<input param>,\
-p,output=<output param>]

在前面的命令中,<cluster id>是实例化集群的 ID。

还可以使用以下命令 ssh 进入主节点并在 Grunt 会话中运行 Pig 拉丁语语句:

$ aws emr ssh --cluster-id <cluster id> --key-pair-file <key pair>

Apache Pig 基础知识

编程 Apache Pig 的主要接口是 Pig 拉丁语,这是一种实现数据流范例思想的过程性语言。

PIG 拉丁语项目一般按如下方式组织:

  • LOAD语句从 HDFS 读取数据
  • 一系列语句聚合和操作数据
  • STORE语句将输出写入文件系统
  • 或者,DUMP语句将输出显示到终端

下面的示例显示了一系列语句,这些语句输出从 tweet 数据集中提取的按频率排序的前 10 个标签:

tweets = LOAD 'tweets.json' 
  USING JsonLoader('created_at:chararray, 
    id:long, 
    id_str:chararray, 
    text:chararray');

hashtags = FOREACH tweets {
  GENERATE FLATTEN(
    REGEX_EXTRACT(
      text, 
      '(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)', 1)
    ) as tag;
}

hashtags_grpd = GROUP hashtags BY tag;
hashtags_count = FOREACH hashtags_grpd {
  GENERATE 
    group, 
    COUNT(hashtags) as occurrencies; 
}
hashtags_count_sorted = ORDER hashtags_count BY occurrencies DESC;
top_10_hashtags = LIMIT hashtags_count_sorted 10;
DUMP top_10_hashtags;

首先,我们从 HDFS 加载tweets.json数据集,反序列化 JSON 文件,并将其映射到一个四列模式,该模式包含 tweet 的创建时间、数字和字符串形式的 ID 以及文本。 对于每条 tweet,我们使用正则表达式从其文本中提取标签。 我们根据标签进行聚合,统计出现的次数,并按频率排序。 最后,我们将排序记录限制为最频繁的 10 个标签。

Pig 编译器提取一系列类似于前一条语句的语句,将其转换为 MapReduce 作业,然后在 Hadoop 集群上执行。 计划器和优化器将解析对输入和输出关系的依赖关系,并在可能的情况下并行化语句的执行。

语句是使用 Pig 处理数据的构建块。 它们把一种关系作为输入,产生另一种关系作为输出。 在 Pig 拉丁语中,关系可以定义为一个由元组组成的包,这两种数据类型我们将在本章余下的部分中使用。

熟悉 SQL 和关系数据模型的用户可能会发现 Pig 拉丁语的语法有些熟悉。 虽然语法本身确实有相似之处,但 Pig 拉丁语实现了完全不同的计算模型。 PIG 拉丁语是过程性的,它指定要执行的实际数据转换,而 SQL 是声明性的,描述了问题的性质,但没有指定实际的运行时处理。 在组织数据方面,可以将关系视为关系数据库中的表,其中包中的元组对应于表中的行。 关系是无序的,因此很容易并行化,而且它们比关系表的约束更少。 PIG 关系可以包含具有不同字段数的元组,而具有相同字段计数的元组可以在相应位置具有不同类型的字段。

SQL 和 Pig 拉丁语采用的数据流模型之间的一个关键区别在于如何管理数据管道中的拆分。 在关系世界中,SQL 等声明性语言实现并执行将生成单个结果的查询。 数据流模型将数据转换视为图形,其中输入和输出是由操作符连接的节点。 例如,查询的中间步骤可能需要按多个键对输入进行分组,并产生多个输出(GROUP BY)。 PIG 具有内置机制,可以通过在输入可用时立即执行运算符来管理此类图中的多个数据流,并可能对每个流应用不同的运算符。 例如,Pig 的GROUP BY操作符实现使用了并行特性(http://pig.apache.org/docs/r0.12.0/perf.html#parallel),允许用户为生成的 MapReduce 作业增加 Reduce 任务的数量,从而提高并发性。 该属性的另一个副作用是,当多个运算符可以在同一程序中并行执行时,Pig 会这样做(有关 Pig 的多查询实现的更多细节可以在http://pig.apache.org/docs/r0.12.0/perf.html#multi-query-execution中找到)。 Pig 拉丁语的计算方法的另一个结果是,它允许在管道中的任何点上持久化数据。 它允许开发人员在必要时选择特定的运算符实现和执行计划,从而有效地覆盖优化器。

PIG 拉丁语允许甚至鼓励开发人员通过用户定义函数(UDF)以及利用 Hadoop 流将自己的代码插入到流水线中的几乎任何位置。 UDF 允许用户指定有关如何加载数据、如何存储数据以及如何处理数据的自定义业务逻辑,而流允许用户在数据流中的任何点启动可执行文件。

编程小 PIG

PIG 拉丁语附带了许多内置函数(eval、load/store、ath、string、Bag 和 tuple 函数)以及许多标量和复杂数据类型。 此外,Pig 允许通过 UDF 和 Java 方法的动态调用来扩展函数和数据类型。

PIG 数据类型

PIG 支持以下标量数据类型:

  • int:带符号的 32 位整数
  • long:带符号的 64 位整数
  • float:32 位浮点
  • double:64 位浮点
  • chararray:Unicode UTF-8 格式的字符数组(字符串)
  • bytearray:字节数组(BLOB)
  • boolean:布尔值
  • 发帖主题:Re:Колибри0.7.0
  • **T0*a Java BigInteger
  • *T0

PIG 支持以下复杂数据类型:

  • map:用[]括起来的关联数组,键和值用#分隔,项用,分隔
  • tuple:数据的有序列表,其中元素可以是由()括起来的任何标量或复杂类型,项由,分隔
  • bag:由{}包围并由,分隔的元组的无序集合

默认情况下,Pig 将数据视为非类型化数据。 用户可以在加载时声明数据类型,也可以在必要时手动强制转换。 如果数据类型未声明,但脚本隐式地将值视为特定类型,则 Pig 将假定该值属于该类型,并相应地进行强制转换。 包或元组的字段可以通过名称tuple.field或位置$<index>来引用。 PIG 从 0 开始计数,因此第一个元素将表示为$0

PIG 函数

内置函数是用 Java 实现的,它们试图遵循标准的 Java 约定。 不过,我们要紧记以下几点不同之处:

  • 函数名称区分大小写且大写
  • 如果结果值为 NULL、空或不是数字(NaN),则 Pig 返回 NULL
  • 如果 Pig 无法处理该表达式,则返回异常

所有内置函数的列表可以在http://pig.apache.org/docs/r0.12.0/func.html中找到。

加载/存储

加载/存储函数确定数据如何进出 Pig。 函数PigStorageTextLoaderBinStorage可分别用于读写 UTF-8 分隔文本、非结构化文本和二进制数据。 对压缩的支持由加载/存储功能决定。 函数PigStorageTextLoader支持读(加载)和写(存储)的 gzip 和 bzip2 压缩。 BinStorage函数不支持压缩。

从 0.12 版开始,Pig 内置支持通过AvroStorage(加载/存储)、JsonStorage(存储)和JsonLoader(加载)加载和存储 Avro 和 JSON 数据。 在撰写本文时,对 JSON 的支持在某种程度上仍然是有限的。 特别地,Pig 期望将数据的模式作为参数提供给JsonLoader/JsonStorage,或者它假定.pig_schema(由JsonStorage生成)存在于包含输入数据的目录中。 在实践中,这使得处理不是由 Pig 本身生成的 JSON 转储变得困难。

如下面的示例所示,我们可以使用JsonLoader加载 JSON 数据集:

tweets = LOAD 'tweets.json' USING JsonLoader(
'created_at:chararray,  
id:long, 
id_str:chararray, 
text:chararray,
source:chararray');

我们提供了一个模式,以便映射 JSON 对象created_ididid_strtextsource的前五个元素。 我们可以使用describe tweets查看 tweet 的模式,它返回以下内容:

 tweets: {created_at: chararray,id: long,id_str: chararray,text: chararray,source: chararray} 

评估

Eval 函数实现一组要应用于返回包或地图数据类型的表达式的操作。 表达式结果在函数上下文中求值。

  • AVG(expression):计算单列包中数值的平均值
  • COUNT(expression):对包中第一个位置中具有非空值的所有元素进行计数
  • COUNT_STAR(expression):对包中的所有元素进行计数
  • IsEmpty(expression):检查包或地图是否为空
  • MAX(expression)MIN(expression)SUM(expression):返回包中个元素的 max、min 或总和[t4
  • TOKENIZE(exp``ression):拆分字符串并输出一包单词

元组、包和映射函数

这些函数允许在包、元组和映射类型之间进行转换。 它们包括以下内容:

  • TOTUPLE(expression)TOMAP(expression)TOBAG(expression):这些将expression强制为元组、映射或包
  • TOP(n, column, relation):此函数返回元组包中的个顶部n个元组

数学、字符串和日期时间函数

PIG 公开由java.lang.Mathjava.lang.Stringjava.util.Date和 Joda-TimeDateTime类(位于http://www.joda.org/joda-time/)提供的个函数。

动态调用器

动态调用程序允许执行 Java 函数,而不必将它们包装在 UDF 中。 它们可用于满足以下条件的任何静态函数:

  • 不接受任何参数或接受具有这些相同类型的stringintlongdoublefloatarray的组合
  • 返回stringintlongdoublefloat

只有原语可以用于数字,而 Java 盒装类(如 Integer)不能用作参数。 根据返回类型,必须使用特定类型的调用器:InvokeForStringInvokeForIntInvokeForLongInvokeForDoubleInvokeForFloat。 有关动态调用器的更多详细信息可以在http://pig.apache.org/docs/r0.12.0/func.html#dynamic-invokers中找到。

从 0.9 版开始,Pig 拉丁语的预处理器支持宏扩展。 宏是使用DEFINE语句定义的:

DEFINE macro_name(param1, ..., paramN) RETURNS output_bag { 
  pig_latin_statements 
};

宏内联展开,其参数在{ }内的 Pig 拉丁块中引用。

宏输出关系在RETURNS语句(output_bag)中给出。 RETURNS void用于没有输出关系的宏。

我们可以定义一个宏来计算关系中的行数,如下所示:

DEFINE count_rows(X) RETURNS cnt { 
  grpd = group $X all; 
  $cnt = foreach grpd generate COUNT($X); 
};

我们可以在 Pig 脚本或 Grunt 会话中使用它来计算 tweet 的数量:

tweets_count = count_rows(tweets);
DUMP tweets_count;

宏允许我们将代码放在单独的文件中并在需要的地方导入,从而使脚本模块化。 例如,我们可以将count_rows保存在一个名为count_rows.macro的文件中,然后使用命令import 'count_rows.macro'导入它。

宏有许多限制;尤其是,宏中只允许使用 Pig 拉丁语语句。 不能使用REGISTER语句和 shell 命令,不允许使用 UDF,也不支持宏内的参数替换。

使用数据

PIG 拉丁语提供了许多关系运算符来组合函数并对数据应用转换。 数据管道中的典型操作包括过滤关系(FILTER)、基于键聚合输入(GROUP)、基于数据列生成转换(FOREACH)以及基于共享键的连接关系(JOIN)。

在接下来的几节中,我们将演示通过加载 JSON 数据生成的 tweet 数据集上的这些操作符。

过滤

FILTER运算符根据表达式从关系中选择元组,如下所示:

relation = FILTER relation BY expression;

我们可以使用此运算符过滤其文本与 hashtag 正则表达式匹配的 tweet,如下所示:

tweets_with_tag = FILTER tweets BY 
    (text 
       MATCHES '(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)'
);

聚合

GROUP运算符根据表达式或键将一个或多个关系中的数据分组在一起,如下所示:

relation = GROUP relation BY expression;

我们可以按source字段将推文分组到一个新的关系grpd中,如下所示:

grpd = GROUP tweets BY source;

通过将元组指定为键,可以对多个维度进行分组,如下所示:

grpd = GROUP tweets BY (created_at, source);

GROUP运算的结果是一个关系,它包括组表达式的每个唯一值一个元组。 此元组包含两个字段。 第一个字段被命名为group,并且与组密钥的类型相同。 第二个字段采用原始关系的名称,类型为 Bag。 这两个字段的名称都由系统生成。

使用关键字ALL,Pig 将聚合整个关系。 GROUP tweets ALL方案将聚合同一组中的所有元组。

如前所述,Pig 允许使用PARALLEL运算符显式处理GROUP运算符的并发级别:

grpd = GROUP tweets BY (created_at, id) PARALLEL 10;

在前面的示例中,编译器生成的 MapReduce 作业将运行 10 个并发 Reduce 任务。 PIG 对使用多少减速机有一个试探性的估计。 全局强制执行 Reduce 任务数量的另一种方法是使用set default_parallel <n>命令。

永远

FOREACH运算符对列应用函数,如下所示:

relation = FOREACH relation GENERATE transformation;

FOREACH的输出取决于应用的转换。

我们可以使用运算符投影包含标签的所有 tweet 的文本,如下所示:

 t = FOREACH tweets_with_tag GENERATE text;

我们还可以将函数应用于投影柱。 例如,我们可以使用REGEX_TOKENIZE函数将每条推文拆分成单词,如下所示:

t = FOREACH tweets_with_tag GENERATE FLATTEN(TOKENIZE(text)) as word;

FLATTEN修饰符进一步将TOKENIZE生成的包解套成词的元组。

加入

JOIN运算符基于公共字段值执行两个或多个关系的内部联接。 其语法如下:

relation = JOIN relation1 BY expression1, relation2 BY expression2;

我们可以使用联接操作来检测包含正面单词的 tweet,如下所示:

positive = LOAD 'positive-words.txt' USING PigStorage() as (w:chararray);

过滤掉评论,如下所示:

positive_words = FILTER positive BY NOT w MATCHES '^;.*';

positive_words是一包元组,每个元组包含一个单词。 然后,我们对 tweet 的文本进行标记化,并创建一个新的(id_str,word)元组包,如下所示:

id_words = FOREACH tweets {
   GENERATE 
      id_str, 
      FLATTEN(TOKENIZE(text)) as word;
}

我们在word字段上连接这两个关系,并获得包含一个或多个肯定词的所有 tweet 之间的关系,如下所示:

positive_tweets = JOIN positive_words BY w, id_words BY word;

在此语句中,我们在id_words.word是正词的条件下将positive_wordsid_words连接起来。 positive_tweets运算符是一个{w:chararray,id_str:chararray, word:chararray}形式的包,它包含符合联接条件的positive_wordsid_words的所有元素。

我们可以结合GROUPFOREACH运算符来计算每条 tweet 的正面单词数(至少包含一个正面单词)。 首先,我们将正面推文的关系按推文 ID 进行分组,然后统计每个 ID 在关系中出现的次数,如下所示:

grpd = GROUP positive_tweets BY id_str;
score = FOREACH grpd GENERATE FLATTEN(group), COUNT(positive_tweets);

JOIN操作符也可以利用并行化功能,如下所示:

positive_tweets = JOIN positive_words BY w, id_words BY word PARALLEL 10

前面的命令将执行具有 10 个减速器任务的联接。

可以使用USING关键字后跟专用联接的 ID 来指定操作员的行为。 有关更多详细信息,请访问http://pig.apache.org/docs/r0.12.0/perf.html#specialized-joins

扩展 PIG(自定义项)

函数几乎可以是 Pig 中每个运算符的一部分。 UDF 和内置函数之间有两个主要区别。 首先,需要使用REGISTER关键字注册 UDF,以便使它们可供 Pig 使用。 其次,它们在使用时需要合格。 PIG UDF 目前可以在 Java、Python、Ruby、JavaScript 和 Groovy 中实现。 对 Java 函数提供了最广泛的支持,这些函数允许您定制流程的所有部分,包括数据加载/存储、转换和聚合。 此外,Java 函数的效率也更高,因为它们是用与 Pig 相同的语言实现的,而且还支持其他接口,如 Algebraic 和 Acumulator 接口。 另一方面,Ruby 和 PythonAPI 允许更快速的原型化。

自定义函数与 Pig 环境的集成主要由以下两个语句REGISTERDEFINE管理:

  • REGISTER注册 JAR 文件,以便可以使用文件中的 UDF,如下所示:

    REGISTER 'piggybank.jar'
    
    
  • DEFINE创建函数或流命令的别名,如下所示:

    DEFINE MyFunction my.package.uri.MyFunction
    
    

Pig 的 0.12 版引入了 UDF 流,作为一种使用没有 JVM 实现的语言编写函数的机制。

贡献的自定义项

PIG 的代码 base 托管一个名为Piggybank的 UDF 存储库。 其他受欢迎的贡献存储库是Twitter 的 Elephant Bird(位于https://github.com/kevinweil/elephant-bird/)和Apache DataFu(位于http://datafu.incubator.apache.org/)。

存钱罐

存钱罐是 Pig 用户共享功能的地方。 共享的代码位于位于http://svn.apache.org/viewvc/pig/trunk/contrib/piggybank/java/src/main/java/org/apache/pig/piggybank/的官方 Pig Subversion 存储库中。 接口文档可以在Conrib部分下的http://pig.apache.org/docs/r0.12.0/api/找到。 存钱罐 UDF 可以通过从 Subversion 存储库签出并编译源代码或使用 Pig 二进制版本附带的 JAR 文件来获得。 在 Cloudera CDH 中,piggybank.jar/opt/cloudera/parcels/CDH/lib/pig/piggybank.jar可用。

象鸟

大象鸟是一个开源库,其中包含了 Hadoop 在 Twitter 生产中使用的所有东西。 该库包含许多序列化工具、自定义输入和输出格式、可写内容、Pig 加载/存储函数以及更多杂类。

大象鸟附带了一个极其灵活的 JSON 加载器函数,在撰写本文时,该函数是在 Pig 中操作 JSON 数据的首选资源。

= 0= Apache DataF

Apache DataFu Pig 收集许多由 LinkedIn 开发和贡献的分析函数。 这些功能包括统计和估计函数、包和集合运算、采样、散列和链接分析。

分析 Twitter 流

在下面的示例中,我们将使用 Elephant Bird 提供的 JsonLoader 实现来加载和操作 JSON 数据。 我们将使用 Pig 来探索推特元数据并分析数据集中的趋势。 最后,我们将用户之间的交互建模为一个图,并使用 Apache DataFu 来分析这个社交网络。

必备条件

下载elephant-bird-pig(http://central.maven.org/maven2/com/twitter/elephantbird/elephant-bird-pig/4.5/elephant-bird-pig-4.5.jar), elephant-bird-hadoop-compat(http://central.maven.org/maven2/com/twitter/elephantbird/elephant-bird-hadoop-compat/4.5/elephant-bird-hadoop-compat-4.5.jar),elephant-bird-core(http://central.maven.org/maven2/com/twitter/elephantbird/elephant-bird-core/4.5/elephant-bird-core-4.5.jar)JAR 文件。 从 Maven 中央存储库中,使用以下命令将它们复制到 HDFS:

$ hdfs dfs -put target/elephant-bird-pig-4.5.jar hdfs:///jar/
$ hdfs dfs –put target/elephant-bird-hadoop-compat-4.5.jar hdfs:///jar/
$ hdfs dfs –put elephant-bird-core-4.5.jar hdfs:///jar/ 

数据集探索

在深入研究数据集之前,我们需要注册对 Elephant Bird 和 DataFu 的依赖关系,如下所示:

REGISTER /opt/cloudera/parcels/CDH/lib/pig/datafu-1.1.0-cdh5.0.0.jar
REGISTER /opt/cloudera/parcels/CDH/lib/pig/lib/json-simple-1.1.jar
REGISTER hdfs:///jar/elephant-bird-pig-4.5.jar
REGISTER hdfs:///jar/elephant-bird-hadoop-compat-4.5.jar
REGISTER hdfs:///jar/elephant-bird-core-4.5.jar

然后,使用com.twitter.elephantbird.pig.load.JsonLoader加载 tweet 的 JSON 数据集,如下所示:

tweets = LOAD 'tweets.json' using  com.twitter.elephantbird.pig.load.JsonLoader('-nestedLoad');

com.twitter.elephantbird.pig.load.JsonLoader将输入文件的每一行解码为 JSON,并将结果值映射作为单元素元组传递给 Pig。 这样就可以访问 JSON 对象的元素,而不必预先指定模式。 参数–nestedLoad指示类加载嵌套数据结构。

推特元数据

在本章的剩余部分中,我们将使用来自 JSON 数据集的元数据来建模 tweet 流。 附加到 tweet 的元数据的一个例子是Place对象,它包含有关用户位置的地理信息。 Place包含描述其名称、ID、国家/地区、国家/地区代码等的字段。 完整的描述可以在https://dev.twitter.com/docs/platform-objects/places找到。

place = FOREACH tweets GENERATE (chararray)$0#'place' as place;

Entities 提供来自 tweet、URL、标签和提及的结构化数据等信息,而不必从文本中提取它们。 有关实体的描述,请参阅https://dev.twitter.com/docs/entities。 Hashtag 实体是从 tweet 中提取的标签数组。 每个实体都有以下两个属性:

  • text:是标签文本
  • 索引:是从中提取标签的字符位置

以下代码使用实体:

hashtags_bag = FOREACH tweets {
    GENERATE 
      FLATTEN($0#'entities'#'hashtags') as tag;
}

然后,我们将展平hashtags_bag以提取每个标签的文本:

hashtags = FOREACH hashtags_bag GENERATE tag#'text' as topic;

用户对象的实体包含出现在用户配置文件和说明字段中的信息。 我们可以通过推文地图中的user字段提取推文作者的 ID:

users = FOREACH tweets GENERATE $0#'user'#'id' as id;

数据准备

内置运算符SAMPLE从数据集中选择概率为pn元组的集合,如下所示:

sampled = SAMPLE tweets 0.01;

前面的命令将选择大约 1%的数据集。 假设SAMPLE是概率的(http://en.wikipedia.org/wiki/Bernoulli_sampling),则不能保证样本大小将是准确的。 此外,该函数使用替换进行采样,这意味着每个项可能出现多次。

Apache DataFu 实现了许多采样方法,用于具有精确样本大小且不需要替换的情况(SimpleRandomSampling)、使用替换进行采样(SimpleRandomSampleWithReplacementVoteSimpleRandomSampleWithReplacementElect)、当我们要考虑样本偏差时(WeightedRandomSampling),或者跨多个关系进行采样(SampleByKey)。

我们可以使用SimpleRandomSample创建数据集恰好 1%的样本,每个项目都有相同的被选中概率。

备注

实际保证的样本大小为个 ceil(pn)*,概率至少为 99%。

首先,我们将采样概率 0.01 传递给 UDF 构造函数:

DEFINE SRS datafu.pig.sampling.SimpleRandomSample('0.01');

以及使用(GROUP tweets ALL),创建的要采样的包:

sampled = FOREACH (GROUP tweets ALL) GENERATE FLATTEN(SRS(tweets));

SimpleRandomSampleUDF 选择而不替换,这意味着每个项目将只出现一次。

备注

使用哪种抽样方法取决于我们正在处理的数据、关于项目如何分布的假设、数据集的大小,以及我们实际想要实现的目标。 通常,当我们想要探索数据集来阐明假设时,SimpleRandomSample可能是一个很好的选择。 但是,在几个分析应用中,通常使用假定替换的方法(例如,Bootstrapping)。

请注意,在处理非常大的数据集时,带替换的采样和不带替换的采样的行为往往类似。 从数十亿个项目中选择一个项目两次的概率将很低。

前 n 个统计数据

我们可能首先要问的问题之一是,某些事情发生的频率有多高。 例如,我们可能希望根据提及次数创建前 10 个主题的直方图。 同样,我们可能希望找到排名前 50 的国家/地区或排名前 10 的用户。 在查看推文数据之前,我们将定义一个宏,以便可以将相同的选择逻辑应用于不同的项目集合:

DEFINE top_n(rel, col, n) 
  RETURNS top_n_items {
    grpd = GROUP $rel BY $col;
    cnt_items = FOREACH grpd 
        GENERATE FLATTEN(group), COUNT($rel) AS cnt;
    cnt_items_sorted = ORDER cnt_items BY cnt DESC;
    $top_n_items = LIMIT cnt_items_sorted $n;
  }

top_n方法将关系rel、要计数的列col和要返回的项数n作为参数。 在 Pig 拉丁语块中,我们首先按col中的项对rel进行分组,计算每个项的出现次数,对它们进行排序,然后选择最频繁的n

为了找到排名前 10 位的英语标签,我们按语言对它们进行过滤,并提取它们的文本:

tweets_en = FILTER tweets by $0#'lang' == 'en';
hashtags_bag = FOREACH tweets { 
    GENERATE
        FLATTEN($0#'entities'#'hashtags') AS tag;
}
hashtags = FOREACH hashtags_bag GENERATE tag#'text' AS tag;

并应用top_n宏:

top_10_hashtags = top_n(hashtags, tag, 10);

为了更好地描述什么是流行的,并使这些信息与用户更相关,我们可以深入数据集,查看每个地理位置的标签。

首先,我们生成(placehashtag)元组的包,如下所示:

hashtags_country_bag = FOREACH tweets generate {
    0#'place' as place, 
    FLATTEN($0#'entities'#'hashtags') as tag;
}

然后,我们提取国家代码和标签文本,如下所示:

hashtags_country = FOREACH hashtags_country_bag {
  GENERATE 
    place#'country_code' as co, 
    tag#'text' as tag;
}

然后,我们计算每个国家/地区代码和标签一起出现的次数,如下所示:

hashtags_country_frequency = FOREACH (GROUP hashtags_country ALL) {
  GENERATE 
    FLATTEN(group), 
    COUNT(hashtags_country) as count;
}

最后,我们使用TOP函数计算每个标签的前 10 个国家/地区,如下所示:

hashtags_country_regrouped= GROUP hashtags_country_frequency BY cnt; 
top_results = FOREACH hashtags_country_regrouped {
    result = TOP(10, 1, hashtags_country_frequency);
    GENERATE FLATTEN(result);
} 

TOP的参数是要返回的元组数、要比较的列以及包含该列的关系:

top_results = FOREACH D {
  result = TOP(10, 1, C);
  GENERATE FLATTEN(result);
}

本例的源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch6/topn.pig找到。

日期时间操作

JSON tweet 中的created_at字段为我们提供了关于 tweet 发布时间的时间戳信息。 不幸的是,它的格式与 Pig 的内置datetime类型不兼容。

储蓄罐通过org.apache.pig.piggybank.evaluation.datetime.convert中包含的大量时间操纵 UDF 来拯救我们。 其中之一是CustomFormatToISO,它将任意格式的时间戳转换为 ISO8601 日期时间字符串。

为了访问这些 UDF,我们首先需要注册piggybank.jar文件,如下所示:

REGISTER /opt/cloudera/parcels/CDH/lib/pig/piggybank.jar

为了使我们的代码不那么冗长,我们为CustomFormatToISO类的完全限定的 Java 名称创建一个别名:

DEFINE CustomFormatToISO org.apache.pig.piggybank.evaluation.datetime.convert.CustomFormatToISO();

通过了解如何操作时间戳,我们可以计算不同时间间隔的统计数据。 例如,我们可以查看每小时创建了多少条推文。 PIG 有一个内置的GetHour函数,可以从datetime类型中提取小时数。 为此,我们首先使用CustomFormatToISO将时间戳字符串转换为 ISO 8601,然后使用内置的ToDate函数将结果chararray转换为datetime,如下所示:

hourly_tweets = FOREACH tweets {
  GENERATE 
    GetHour(
      ToDate(
      CustomFormatToISO(
$0#'created_at', 'EEE MMMM d HH:mm:ss Z y')
      )
    ) as hour;
}

现在,只需按小时对hourly_tweets进行分组,然后按组生成推文计数,如下所示:

hourly_tweets_count =  FOREACH (GROUP hourly_tweets BY hour) { 
  GENERATE FLATTEN(group), COUNT(hourly_tweets);
}

会话

DataFu 的Sessionize类可以帮助我们更好地捕获随时间推移的用户活动。 会话表示用户在给定时间段内的活动。 例如,我们可以每隔 15 分钟查看每个用户的推文流,并测量这些会话以确定网络容量和用户活动:

DEFINE Sessionize datafu.pig.sessions.Sessionize('15m');
users_activity = FOREACH tweets {
      GENERATE 
        CustomFormatToISO($0#'created_at', 
                      'EEE MMMM d HH:mm:ss Z y') AS dt,
        (chararray)$0#'user'#'id' as user_id;
}
users_activity_sessionized = FOREACH 
    (GROUP users_activity BY user_id) {
    ordered = ORDER users_activity BY dt;
    GENERATE FLATTEN(Sessionize(ordered)) 
                    AS (dt, user_id, session_id);
}

user_activity只记录给定user_id发布状态更新的时间dt

Sessionize将会话超时和包作为输入。 输入包的第一个元素是 ISO 8601 时间戳,必须按此时间戳对包进行排序。 彼此相隔 15 分钟的事件将属于同一会话。

它返回带有新字段session_id的输入包,该字段唯一地标识一个会话。 使用这些数据,我们可以计算会话的长度和其他一些统计数据。 有关Sessionize用法的更多示例,请参见http://datafu.incubator.apache.org/docs/datafu/guide/sessions.html

捕获用户交互

在本章的剩余部分中,我们将研究如何从用户交互中捕获模式。 作为这个方向的第一步,我们将创建一个适合对社交网络建模的数据集。 该数据集将包含时间戳、tweet 的 ID、发布 tweet 的用户、她回复的用户和 tweet,以及 tweet 中的标签。

Twitter 将任何以@字符开头的消息视为回复(in_reply_to_status_id_str)。 这样的推文被解释为给那个人的直接信息。 将@字符放在推文中的任何其他位置都会被解释为提及('entities'#'user_mentions‘),而不是回复。 不同之处在于,提及的内容会立即广播给一个人的追随者,而回复则不会。 然而,回复被认为是提及的。

在处理个人身份信息时,如果不能完全删除 IP 地址、姓名和用户 ID 等敏感数据,最好将其匿名。 一种常用的技术涉及一个hash函数,该函数将我们想要匿名的数据作为输入,将与称为 SALT 的附加随机数据连接起来。 以下代码显示了此类匿名的示例:

DEFINE SHA datafu.pig.hash.SHA();
from_to_bag = FOREACH tweets {
  dt = $0#'created_at';
  user_id = (chararray)$0#'user'#'id';
  tweet_id = (chararray)$0#'id_str';
  reply_to_tweet = (chararray)$0#'in_reply_to_status_id_str';
  reply_to = (chararray)$0#'in_reply_to_user_id_str';
  place = $0#'place';
  topics = $0#'entities'#'hashtags';

  GENERATE
    CustomFormatToISO(dt, 'EEE MMMM d HH:mm:ss Z y') AS dt,
    SHA((chararray)CONCAT('SALT', user_id)) AS source,  
    SHA(((chararray)CONCAT('SALT', tweet_id))) AS tweet_id,
    ((reply_to_tweet IS NULL) 
         ? NULL 
         : SHA((chararray)CONCAT('SALT', reply_to_tweet))) 
               AS  reply_to_tweet_id,
    ((reply_to IS NULL) 
         ? NULL 
         : SHA((chararray)CONCAT('SALT', reply_to))) 
                AS destination,
    (chararray)place#'country_code' as country,
    FLATTEN(topics) AS topic;
}

-- extract the hashtag text
from_to = FOREACH from_to_bag { 
  GENERATE 
    dt, 
    tweet_id, 
    reply_to_tweet_id, 
    source, 
    destination, 
    country,
    (chararray)topic#'text' AS topic;
}

在本例中,我们使用CONCAT将一个(不是很随机的)SALT 字符串附加到个人数据。 然后,我们使用 DataFu 的SHA函数生成加盐 ID 的散列。 SHA函数要求其输入参数为非空。 我们使用if-then-else语句强制执行此条件。 在 PIG 拉丁语中,这表示为<condition is true> ? <true branch> : <false branch>。 如果字符串为空,则返回NULL,如果不为空,则返回加盐的散列。 为了使代码更具可读性,我们对 tweet JSON 字段使用别名,并在GENERATE块中引用它们。

链接分析

我们可以重新定义我们的方法来确定热门话题,以包括用户的反应。 第一种天真的方法可能是,如果一个话题导致的回复数量超过阈值,那么它就会被认为是重要的。

这种方法的一个问题是,tweet 生成的回复相对较少,因此生成的数据集的数量将会很低。 因此,需要非常大量的数据才能包含被回复的推文并产生任何结果。 在实践中,我们可能希望将此指标与其他指标(例如,提及)结合起来,以便执行更有意义的分析。

为了满足这个查询,我们将创建一个新的数据集,其中包括从 tweet 和用户回复的 tweet 中提取的 hashtag:

tweet_hashtag = FOREACH from_to GENERATE tweet_id, topic;
from_to_self_joined = JOIN from_to BY reply_to_tweet_id LEFT, 
tweet_hashtag BY tweet_id;

twitter_graph = FOREACH from_to_self_joined  { 
    GENERATE
        from_to::dt AS dt,
        from_to::tweet_id AS tweet_id,
        from_to::reply_to_tweet_id AS reply_to_tweet_id,
        from_to::source AS source,
        from_to::destination AS destination,
        from_to::topic AS topic,
        from_to::country AS country,
        tweet_hashtag::topic AS topic_replied;
}

请注意,Pig 不允许在同一关系上进行交叉联接,因此我们必须为联接的右侧创建tweet_hashtag。 在这里,我们使用::运算符来消除我们想要从哪个关系和列中选择记录的歧义。

同样,我们可以使用top_n宏按回复数量查找前 10 个主题:

top_10_topics = top_n(twitter_graph, topic_replied, 10);

数数东西只能带我们走到这一步。 我们可以使用 DataFu 在此数据集上计算更多描述性统计数据。 使用Quantile函数,我们可以计算标签反应数量的中位数、第 90 个、第 95 个和第 99 个百分位数,如下所示:

DEFINE Quantile datafu.pig.stats.Quantile('0.5','0.90','0.95','0.99');

由于 UDF 期望整数值的有序包作为输入,我们首先计算每个topic_replied条目的频率,如下所示。

topics_with_replies_grpd = GROUP twitter_graph BY topic_replied;
topics_with_replies_cnt = FOREACH topics_with_replies_grpd {
  GENERATE
COUNT(twitter_graph) as cnt;
}

然后,我们对频率包应用Quantile,如下所示:

quantiles = FOREACH (GROUP topics_with_replies_cnt ALL) {
    sorted = ORDER topics_with_replies_cnt BY cnt;
    GENERATE Quantile(sorted);
}

本例的源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch6/graph.pig找到。

有影响力的用户

我们将使用 GOOGLE 开发的网页排名算法 PageRank(http://ilpubs.stanford.edu:8090/422/1/1999-66.pdf)在上一节生成的推特图表中识别有影响力的用户。

这种类型的分析有许多用例,例如定向和上下文广告、推荐系统、垃圾邮件检测,以及明显测量网页的重要性。 研究论文wtf:在推特http://stanford.edu/~rezab/papers/wtf_overview.pdf找到的**要关注推特的服务人员中描述了一种类似的方法,推特使用这种方法来实现“关注谁”功能(WTF:The**Who to Follow Service)。

PageRank 非正式地根据链接到该页面的其他页面的重要性来确定该页面的重要性,并为其分配一个介于 0 和 1 之间的分数。PageRank 分数高表示有很多页面指向该页面。 直观地说,被高 PageRank 的页面链接是一种高质量的认可。 根据 Twitter 图,我们假设收到大量回复的用户在社交网络中很重要或有影响力。 在 Twitter 的例子中,我们考虑了 PageRank 的扩展定义,其中两个用户之间的链接由直接回复给出,并由消息中出现的任何最终标签进行标记。 启发式地,我们希望确定在给定主题上有影响力的用户。

在 DataFu 的实现中,每个图都表示为一个由(source, edges)个元组组成的包。 source元组是表示源节点的整数 ID。 边缘是一个由(destination, weight)个元组组成的袋子。 destination是表示目的节点的整数 ID。 weight是一个双精度数,表示边缘应该加权多少。 UDF 的输出是一个由(source, rank)对组成的包,其中rank是图中源用户的 PageRank 值。 请注意,我们将节点、边和图作为抽象概念来讨论。 在谷歌的例子中,节点是网页,边是从一个页面到另一个页面的链接,而图形是直接或间接连接的一组页面。

在我们的例子中,节点表示用户,边表示in_reply_to_user_id_str个提及,边由 tweet 中的标签标记。 PageRank 的输出应该建议哪些用户在给定的交互模式下对给定的主题有影响力。

在本节中,我们将编写一条管道,以实现以下目标:

  • 将数据表示为图形,其中每个节点都是用户,并用标签标记边
  • 将 ID 和散列标签映射到整数,以便 PageRank 可以使用它们
  • 应用 PageRank
  • 以可互操作的格式(AVRO)将结果存储到 HDFS 中

我们将图表示为(source, destination, topic)形式的元组包,其中每个元组表示节点之间的交互。 本例的源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch6/pagerank.pig找到。

我们将把用户和标签的文本映射到数字 ID。 我们使用 Java StringhashCode()方法执行此转换步骤,并将逻辑包装在EvalUDF 中。

备注

整数的大小实际上是图中节点和边数的上限。 对于生产代码,建议您使用更健壮的散列函数。

StringToInt类接受字符串作为输入,调用hashCode()方法,并将方法输出返回给 Pig。 自定义函数代码可在https://github.com/learninghadoop2/book-examples/blob/master/ch6/udf/com/learninghadoop2/pig/udf/StringToInt.java找到。

package com.learninghadoop2.pig.udf;
import java.io.IOException;
import org.apache.pig.EvalFunc;
import org.apache.pig.data.Tuple;

public class StringToInt extends EvalFunc<Integer> {
    public Integer exec(Tuple input) throws IOException {
        if (input == null || input.size() == 0)
            return null;
        try {
            String str = (String) input.get(0);
            return str.hashCode();
        } catch(Exception e) {
          throw 
             new IOException("Cannot convert String to Int", e);
        }
    }
}

我们扩展org.apache.pig.EvalFunc并覆盖exec方法,以在函数输入上返回str.hashCode()EvalFunc<Integer>类使用 UDF(Integer)的返回类型进行参数化。

接下来,我们编译类并将其归档到 JAR 中,如下所示:

$ javac -classpath /opt/cloudera/parcels/CDH/lib/pig/pig.jar:$(hadoop classpath) com/learninghadoop2/pig/udf/StringToInt.java
$ jar cvf myudfs-pig.jar com/learninghadoop2/pig/udf/StringToInt.class

我们现在可以在 Pig 中注册 UDF 并创建StringToInt的别名,如下所示:

REGISTER myudfs-pig.jar
DEFINE StringToInt com.learninghadoop2.pig.udf.StringToInt();

我们过滤掉没有destination和没有topic的推文,如下所示:

tweets_graph_filtered = FILTER twitter_graph by 
(destination IS NOT NULL) AND 
(topic IS NOT null);

然后,我们将sourcedestinationtopic转换为整数 ID:

from_to = foreach tweets_graph_filtered {
  GENERATE 
    StringToInt(source) as source_id, 
    StringToInt(destination) as destination_id, 
    StringToInt(topic) as topic_id;
}

一旦数据采用适当的格式,我们就可以重用 PageRank 的实现和 DataFu 提供的示例代码(位于https://github.com/apache/incubator-datafu/blob/master/datafu-pig/src/main/java/datafu/pig/linkanalysis/PageRank.java),如以下代码所示:

DEFINE PageRank datafu.pig.linkanalysis.PageRank('dangling_nodes','true');

我们首先创建一个包含(source_id, destination_id, topic_id)个元组的包,如下所示:

reply_to = group from_to by (source_id, destination_id, topic_id); 

我们统计每个元组的出现次数,即两个人谈论一个主题的次数,如下所示:

topic_edges = foreach reply_to {
  GENERATE flatten(group), ((double)COUNT(from_to.topic_id)) as w;
}

请记住,主题是我们图形的边;我们首先在源节点和主题边之间创建一个关联,如下所示:

topic_edges_grouped = GROUP topic_edges by (topic_id, source_id);

然后,我们对其进行重组,目的是添加目的节点和边权重,如下所示:

topic_edges_grouped = FOREACH topic_edges_grouped {
  GENERATE
    group.topic_id as topic,
    group.source_id as source,
    topic_edges.(destination_id,w) as edges;
}

创建 Twitter 图表后,我们将计算所有用户的 PageRank(source_id):

topic_rank = FOREACH (GROUP topic_edges_grouped BY topic) {
  GENERATE
    group as topic,
    FLATTEN(PageRank(topic_edges_grouped.(source,edges))) as (source,rank);
}
topic_rank = FOREACH topic_rank GENERATE topic, source, rank;

我们将结果以 Avro 格式存储在 HDFS 中。 如果类路径中不存在 avro 依赖项,那么在访问各个字段之前,我们需要将 avro MapReduce JAR 文件添加到我们的环境中。 在 Pig 中,例如在 Cloudera CDH5 虚拟机上:

REGISTER /opt/cloudera/parcels/CDH/lib/avro/avro.jar
REGISTER /opt/cloudera/parcels/CDH/lib/avro/avro-mapred-hadoop2.jar 
STORE topic_rank INTO 'replies-pagerank' using AvroStorage();    

备注

在最后这两个部分中,我们对 Twitter 图表可能是什么样子以及主题和用户交互的概念意味着什么做了一些隐含的假设。 考虑到我们提出的限制,我们分析的结果社交网络将相对较小,不一定代表整个 Twitter 社交网络。 不鼓励从该数据集中推断结果。 在实践中,要生成健壮的社会交互模型,还需要考虑许多其他因素。

摘要

在本章中,我们介绍了 Apache Pig,一个在 Hadoop 上进行大规模数据分析的平台。 我们特别讨论了以下主题:

  • Pig 的目标是以一种方式提供类似数据流的抽象,而不需要动手进行 MapReduce 开发
  • Pig 的数据处理方法与 SQL 相比如何?在 SQL 中,Pig 是过程性的,而 SQL 是声明性的
  • Pig 入门-这是一项简单的任务,因为它是一个生成自定义代码的库,不需要额外的服务
  • Pig 提供的数据类型、核心函数和扩展机制概述
  • 应用 Pig 详细分析 Twitter 数据集的示例,展示了它以非常简洁的方式表达复杂概念的能力
  • Piggybank、Elephant Bird 和 DataFu 等库如何为许多有用的预写 Pig 函数提供存储库
  • 在下一章中,我们将通过探索对 HDFS 中存储的数据显示类似 SQL 的抽象的工具来回顾 SQL 比较

七、Hadoop 和 SQL

MapReduce 是一个强大的范例,它支持复杂的数据处理,可以揭示有价值的见解。 然而,正如前面几章所讨论的,它确实需要不同的思维模式,以及在将处理分析分解为一系列映射和还原步骤的模型方面的一些培训和经验。 有几个构建在 Hadoop 之上的产品可以提供 HDFS 中保存的数据的更高级别或更熟悉的视图,Pig 就是一个非常流行的产品。 本章将探讨在 Hadoop 上实现的另一个最常见的抽象:SQL。

在本章中,我们将介绍以下主题:

  • Hadoop 上的 SQL 有哪些使用案例?为什么它如此受欢迎
  • HiveQL,Apache Have 引入的 SQL 方言
  • 使用 HiveQL 对 Twitter 数据集执行类似 SQL 的分析
  • HiveQL 如何近似关系数据库的常见功能,如连接和视图
  • HiveQL 如何允许将用户定义的函数合并到其查询中
  • Hadoop 上的 SQL 如何补充 Pig
  • 其他 SQL-on-Hadoop 产品(如 Impala)及其与配置单元的不同之处

为什么选择 Hadoop 上的 SQL

到目前为止,我们已经了解了如何使用 MapReduce API 编写 Hadoop 程序,以及 Pig 拉丁语如何通过 UDF 为自定义业务逻辑提供脚本抽象和包装器。 PIG 是一个非常强大的工具,但大多数开发人员或业务分析师并不熟悉其基于数据流的编程模型。 这类人浏览数据的传统选择工具是 SQL。

早在 2008 年,Facebook 就发布了 Have,这是第一个在 Hadoop 上广泛使用的 SQL 实现。

配置单元没有提供一种更快速地开发 map 和 Reduce 任务的方法,而是提供了HiveQL的实现,这是一种基于 SQL 的查询语言。 HIVE 接受 HiveQL 语句,并立即自动将查询转换为一个或多个 MapReduce 作业。 然后,它执行整个 MapReduce 程序并将结果返回给用户。

这个到 Hadoop 的接口不仅减少了从数据分析中产生结果所需的时间,而且还大大拓宽了谁可以使用 Hadoop 的网络。 任何熟悉 SQL 的人都可以使用配置单元,而不需要软件开发技能。

这些属性的组合是 HiveQL 经常被用作业务和数据分析师对存储在 HDFS 上的数据执行即席查询的工具。 有了 Hive,数据分析师可以在不需要软件开发人员参与的情况下改进查询。 就像 Pig 一样,配置单元还允许通过用户定义的函数来扩展 HiveQL,从而使基础 SQL 方言能够使用特定于业务的功能进行自定义。

其他基于 Hadoop 的 SQL 解决方案

虽然 Hive 是第一个引入并支持 HiveQL 的产品,但它不再是唯一的产品。 在本章的后面,我们还将讨论 Impala,它发布于 2013 年,已经是一个非常流行的工具,特别是对于低延迟查询。 还有其他的,但我们将主要讨论 Hive 和黑斑羚,因为它们是最成功的。

然而,在介绍 Hadoop 上 SQL 的核心特性和功能时,我们将给出使用配置单元的示例;尽管配置单元和 Impala 共享许多 SQL 特性,但它们也有许多不同之处。 我们不想不断地警告每个新功能,说明 Hive 与黑斑羚相比是如何支持这些新功能的。 我们通常会查看这两种产品共有的功能集的各个方面,但如果您同时使用这两种产品,阅读最新的发行说明以了解它们之间的区别是很重要的。

必备条件

在深入研究特定技术之前,让我们先生成一些数据,我们将在本章的示例中使用这些数据。 我们将创建以前的 Pig 脚本的修改版本作为此脚本的主要功能。 本章中的脚本假设以前使用的 Elephant Bird Jars 位于 HDFS 的/jar目录中。 完整源代码在https://github.com/learninghadoop2/book-examples/blob/master/ch7/extract_for_hive.pig,但extract_for_hive.pig的核心如下:

-- load JSON data
tweets = load '$inputDir' using  com.twitter.elephantbird.pig.load.JsonLoader('-nestedLoad');
-- Tweets
tweets_tsv = foreach tweets {
generate 
    (chararray)CustomFormatToISO($0#'created_at', 
'EEE MMMM d HH:mm:ss Z y') as dt, 
    (chararray)$0#'id_str', 
(chararray)$0#'text' as text, 
    (chararray)$0#'in_reply_to', 
(boolean)$0#'retweeted' as is_retweeted, 
(chararray)$0#'user'#'id_str' as user_id, (chararray)$0#'place'#'id' as place_id;
}
store tweets_tsv into '$outputDir/tweets' 
using PigStorage('\u0001');
-- Places
needed_fields = foreach tweets {
   generate 
(chararray)CustomFormatToISO($0#'created_at', 
'EEE MMMM d HH:mm:ss Z y') as dt, 
     (chararray)$0#'id_str' as id_str, 
$0#'place' as place;
}
place_fields = foreach needed_fields {
generate 
    (chararray)place#'id' as place_id, 
    (chararray)place#'country_code' as co, 
    (chararray)place#'country' as country, 
    (chararray)place#'name' as place_name, 
    (chararray)place#'full_name' as place_full_name, 
    (chararray)place#'place_type' as place_type;
}
filtered_places = filter place_fields by co != '';
unique_places = distinct filtered_places;
store unique_places into '$outputDir/places' 
using PigStorage('\u0001');

-- Users
users = foreach tweets {
   generate 
(chararray)CustomFormatToISO($0#'created_at', 
'EEE MMMM d HH:mm:ss Z y') as dt, 
(chararray)$0#'id_str' as id_str, 
$0#'user' as user;
}
user_fields = foreach users {
   generate 
    (chararray)CustomFormatToISO(user#'created_at', 
'EEE MMMM d HH:mm:ss Z y') as dt,
  (chararray)user#'id_str' as user_id, 
  (chararray)user#'location' as user_location, 
  (chararray)user#'name' as user_name, 
  (chararray)user#'description' as user_description, 
  (int)user#'followers_count' as followers_count, 
  (int)user#'friends_count' as friends_count, 
  (int)user#'favourites_count' as favourites_count, 
  (chararray)user#'screen_name' as screen_name, 
  (int)user#'listed_count' as listed_count;

}
unique_users = distinct user_fields;
store unique_users into '$outputDir/users' 
using PigStorage('\u0001');

按如下方式运行此脚本:

$ pig –f extract_for_hive.pig –param inputDir=<json input> -param outputDir=<output path>

前面的代码将数据写入 tweet、user 和 place 信息的三个单独的 TSV 文件中。 请注意,在store命令中,我们在调用PigStorage时传递一个参数。 这个参数将默认字段分隔符从制表符更改为 Unicode 值 U0001,或者您也可以使用Ctrl+C+A。 这通常用作配置单元表格中的分隔符,对我们特别有用,因为我们的推文数据可能包含其他字段中的制表符。

Hive 概述

现在,我们将展示如何将数据导入配置单元,并针对配置单元提供的表抽象表对数据运行查询。 在本例中以及本章的其余部分中,我们将假设查询被键入到 shell 中,可以通过执行hive命令调用这些查询。

最近,一个名为 Beeline 的客户端也出现了,并且很可能在不久的将来成为首选的 CLI 客户端。

将任何新数据导入配置单元时,通常有三个阶段的流程:

  • 创建要向其中导入数据的表的规范
  • 将数据导入到创建的表中
  • 对表执行 HiveQL 查询

大多数 HiveQL 语句直接类似于标准 SQL 中名称相似的语句。 在本章中,我们只假定您对 SQL 的了解不多,但是如果您需要复习一下,有很多很好的在线学习资源。

HIVE 提供了数据的结构化查询视图,要实现这一点,我们必须首先定义表列的规范,并将数据导入表中,然后才能执行任何查询。 表规范是使用CREATE语句生成的,该语句指定表名、表列的名称和类型以及有关表存储方式的一些元数据:

CREATE table tweets (
created_at string,
tweet_id string,
text string,
in_reply_to string,
retweeted boolean,
user_id string,
place_id string
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS TEXTFILE;

该语句创建一个由数据集中列的名称及其数据类型列表定义的新表tweets。 我们指定字段由 Unicode U0001 字符分隔,用于存储数据的格式为TEXTFILE

可以使用LOAD DATA语句从 HDFStweets/中的某个位置导入数据:

LOAD DATA INPATH 'tweets' OVERWRITE INTO TABLE tweets;

默认情况下,配置单元表格的数据存储在 HDFS 的/user/hive/warehouse下。 如果为LOAD语句指定了 HDFS 上数据的路径,则它不会简单地将数据复制到/user/hive/warehouse中,而是会将其移动到那里。 如果要分析其他应用使用的 HDFS 上的数据,请创建副本或使用稍后介绍的EXTERNAL机制。

将数据导入配置单元后,我们可以对其运行查询。 例如:

SELECT COUNT(*) FROM tweets;

前面的代码将返回数据集中存在的 tweet 总数。 HiveQL 与 SQL 一样,在关键字、列或表名方面不区分大小写。 按照惯例,SQL 语句使用大写作为 SQL 语言关键字,当在文件中使用 HiveQL 时,我们通常会遵循这一点,稍后将展示这一点。 然而,在输入交互式命令时,我们会经常选择阻力最小的行,并使用小写。

如果仔细观察前面示例中各种命令所花费的时间,您会注意到,将数据加载到表中所需的时间与创建表规范的时间差不多,但即使是简单地计算所有行的时间也要长得多。 输出还显示,表创建和数据加载实际上并不会导致执行 MapReduce 作业,这就解释了执行时间非常短的原因。

Hive 表的性质

尽管配置单元将数据文件复制到其工作目录中,但它在此时并不实际将输入数据处理成行。

CREATE TABLELOAD DATA语句本身并不真正创建具体的表数据;相反,它们生成的元数据将在配置单元生成 MapReduce 作业以访问表中概念上存储但实际驻留在 HDFS 上的数据时使用。 尽管 HiveQL 语句引用特定的表结构,但它由配置单元负责生成代码,将其正确映射到存储数据文件的实际磁盘格式。

这似乎表明配置单元不是真正的数据库;这是真的,事实并非如此。关系数据库需要在接收数据之前定义一个表模式,然后只接收符合该规范的数据,而配置单元要灵活得多。 配置单元表的不太具体的性质意味着模式可以基于数据已经到达时定义,而不是基于数据应该是如何的假设,这可能被证明是错误的。 尽管不管采用何种技术,可变的数据格式都很麻烦,但 Hive 模型在问题出现时(而不是在问题出现时)提供了额外的自由度。

Hive 架构

在版本 2 之前,Hadoop 主要是批处理系统。 正如我们在前几章中看到的,MapReduce 作业往往有很高的延迟和提交和调度带来的开销。 在内部,配置单元将 HiveQL 语句编译成 MapReduce 作业。 传统上,配置单元查询的特点是高延迟。 随着毒刺计划和 Hive 0.13 中引入的改进(我们将在后面讨论),这一点已经改变。

HIVE 作为一个客户端应用运行,该应用处理 HiveQL 查询,将其转换为 MapReduce 作业,然后将这些作业提交给 Hadoop 集群,或者提交给 Hadoop 1 中的原生 MapReduce,或者提交给 Hadoop 2 中在 YAR 上运行的 MapReduce Application Master。

无论采用哪种模型,配置单元都使用一个称为元存储的组件,在该组件中,它保存有关系统中定义的表的所有元数据。 具有讽刺意味的是,这些数据存储在专门用于 Hive 的关系数据库中。 在配置单元的最早版本中,所有客户端都直接与元存储通信,但这意味着配置单元 CLI 工具的每个用户都需要知道元存储用户名和密码。

HiveServer 的创建目的是充当远程客户端的入口点,远程客户端也可以充当单个访问控制点,并控制对底层元存储的所有访问。 由于 HiveServer 的限制,访问配置单元的最新方式是通过多客户端 HiveServer2。

备注

HiveServer2 引入了对其前身的许多改进,包括用户身份验证和对来自同一客户端的多个连接的支持。 有关的更多信息,请参见https://cwiki.apache.org/confluence/display/Hive/Setting+Up+HiveServer2

可以分别使用hive --service hiveserverhive --service hiveserver2命令手动执行HiveServerHiveServer2的实例。

在本章前面和后面看到的示例中,我们隐式使用 HiveServer 通过配置单元命令行工具提交查询。 HiveServer2 与 Beeline 一起提供。 出于兼容性和成熟性的原因,Beeline 相对较新,这两个工具都可以在 Cloudera 和大多数其他主要发行版上使用。 Beeline 客户端是核心 Apache Have 发行版的一部分,因此也是完全开源的。 可以使用以下命令在嵌入式版本中执行直线:

$ beeline -u jdbc:hive2://

数据类型

HiveQL 支持标准数据库系统提供的许多常见数据类型。 这些类型包括原语类型(如floatdoubleint,string)到到结构化集合类型,这些类型提供 SQL 类似于arraysstructsunions等类型(structs具有某些字段的选项)。 因为配置单元是用 Java 实现的,所以原语类型的行为将类似于它们的 Java 对应物。 我们可以将配置单元数据类型分为以下五大类:

  • 数值tinyintsmallintintbigintfloatdoubledecimal
  • 日期和时间timestampdate
  • 字符串stringvarcharchar
  • 集合arraymapstructuniontype
  • 其他booleanbinaryNULL

DDL 语句

HiveQL 提供了多个语句来创建、删除和更改数据库、表和视图。 CREATE DATABASE <name>语句创建具有给定名称的新数据库。 数据库表示包含表和视图元数据的命名空间。 如果存在多个数据库,则USE <database name>语句指定使用哪个数据库来查询表或创建新元数据。 如果未显式指定数据库,则配置单元将针对default数据库运行所有语句。 SHOW [DATABASES, TABLES, VIEWS]显示数据仓库中当前可用的数据库,以及当前使用的数据库中存在哪些表元数据和视图元数据:

CREATE DATABASE twitter;
SHOW databases;
USE twitter;
SHOW TABLES;

CREATE TABLE [IF NOT EXISTS] <name>语句创建具有给定名称的表。 正如前面提到的,真正创建的是表示表及其到 HDFS 上文件的映射的元数据,以及存储数据文件的目录。 如果已存在同名的表或视图,则配置单元将引发异常。

表名和列名都不区分大小写。 在较早版本的配置单元(0.12 和更早版本)中,表名和列名中只允许使用字母数字和下划线字符。 从配置单元 0.13 开始,系统支持列名中的 Unicode 字符。 保留字(如loadcreate)需要用反号(`字符)转义才能按字面处理。

关键字EXTERNAL指定表存在于配置单元无法控制的资源中,这是在基于 Hadoop 的Extract-Transform-Load(ETL)管道开始时从另一个源提取数据的有用机制。 LOCATION子句指定要在何处找到源文件(或目录)。 以下代码中使用了EXTERNAL关键字和LOCATION子句:

CREATE EXTERNAL TABLE tweets (
created_at string,
tweet_id string,
text string,
in_reply_to string,
retweeted boolean,
user_id string,
place_id string
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS TEXTFILE
LOCATION '${input}/tweets';

此表将在元存储中创建,但不会将数据复制到/user/hive/warehouse目录。

提示

请注意,配置单元没有主键或唯一标识符的概念。 在将数据加载到数据仓库之前,唯一性和数据规范化是需要解决的问题。

CREATE VIEW <view name> … AS SELECT语句创建具有给定名称的视图。 例如,我们可以创建一个视图来将转发与其他消息隔离开来,如下所示:

CREATE VIEW retweets 
COMMENT 'Tweets that have been retweeted'
AS SELECT * FROM tweets WHERE retweeted = true;

除非另有指定,否则列名派生自定义的SELECT语句。 配置单元当前不支持实例化视图。

DROP TABLEDROP VIEW语句删除给定表或视图的元数据和数据。 删除EXTERNAL表或视图时,只会删除元数据,实际数据文件不会受到影响。

配置单元允许通过ALTER TABLE语句更改表元数据,该语句可用于更改列类型、名称、位置和注释,或添加和替换列。

在添加列时,重要的是要记住,只有元数据会更改,而不是数据集本身。 这意味着,如果我们在表的中间添加旧文件中不存在的列,那么在从旧数据中进行选择时,我们可能会在错误的列中获得错误的值。 这是因为我们将使用新格式查看旧文件。 在讨论 Avro 时,我们将在数据生命周期管理中讨论数据和架构迁移。

同样,ALTER VIEW <view name> AS <select statement>会更改现有视图的定义。

文件格式和存储

配置单元表的底层数据文件与 HDFS 上的任何其他文件没有什么不同。 用户可以使用其他工具直接读取 Hive 表中的 HDFS 文件。 他们还可以使用其他工具写入 HDFS 文件,这些文件可以通过CREATE EXTERNAL TABLELOAD DATA INPATH加载到配置单元中。

配置单元使用the SerializerDeserializer类、SerDe 以及FileFormat来读写表行。 如果未指定ROW FORMAT或在CREATE TABLE语句中指定了ROW FORMAT DELIMITED,则使用本机 SerDe。 DELIMITED子句指示系统读取分隔文件。 可以使用ESCAPED BY子句对分隔符字符进行转义。

配置单元当前使用以下FileFormat类来读写 HDFS 文件:

  • TextInputFormatHiveIgnoreKeyTextOutputFormat:是否将以纯文本文件格式读取/写入数据
  • SequenceFileInputFormatSequenceFileOutputFormat:类以 HadoopSequenceFile格式读取/写入数据

此外,以下 SerDe 类可用于序列化和反序列化数据:

  • MetadataTypedColumnsetSerDe:将读/写分隔的记录,如 CSV 或制表符分隔的记录
  • ThriftSerDeDynamicSerDe:将读/写个节约对象

JSON

从版本 0.13 开始,配置单元将与本机org.apache.hive.hcatalog.data.JsonSerDe一起提供。 对于较旧版本的配置单元,hive-json-serde(位于https://github.com/rcongiu/Hive-JSON-Serde)无疑是功能最丰富的 JSON 序列化/反序列化模块之一。

我们可以使用任一模块加载 JSON tweet,而不需要任何预处理,只需定义与 JSON 文档内容匹配的配置单元模式。 在下面的示例中,我们使用配置单元-JSON-SERDE。

与任何第三方模块一样,我们使用以下代码将 SerDe JAR 加载到配置单元中:

ADD JAR JAR json-serde-1.3-jar-with-dependencies.jar;

然后,我们发出通常的CREATE语句,如下所示:

CREATE EXTERNAL TABLE tweets (
   contributors string,
   coordinates struct <
      coordinates: array <float>,
      type: string>,
   created_at string,
   entities struct <
      hashtags: array <struct <
            indices: array <tinyint>,
            text: string>>,
…
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
STORED AS TEXTFILE
LOCATION 'tweets';  

使用此 SerDe,我们可以将嵌套文档(如实体或用户)映射到structmap类型。 我们告诉配置单元存储在LOCATION 'tweets'中的数据是文本(STORED AS TEXTFILE),并且每一行都是一个 JSON 对象(ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe‘)。 在配置单元 0.13 和更高版本中,我们可以将此属性表示为ROW FORMAT SERDE 'org.apache.hive.hcatalog.data.JsonSerDe'

手动指定复杂文档的模式可能是一个乏味且容易出错的过程。 hive-json模块(位于https://github.com/hortonworks/hive-json)是分析大型文档并生成适当的配置单元模式的便捷实用程序。 根据文档集合的不同,可能需要进一步改进。

在我们的示例中,我们使用了用hive-json生成的模式,该模式将 twets JSON 映射到许多struct数据类型。 这允许我们使用方便的点符号来查询数据。 例如,我们可以使用以下代码提取用户对象的屏幕名称和描述字段:

SELECT user.screen_name, user.description FROM tweets_json LIMIT 10;

_

AvroSerde(https://cwiki.apache.org/confluence/display/Hive/AvroSerDe)允许我们以 Avro 格式读取和写入数据。 从 0.14 开始,可以使用STORED AS AVRO语句创建 Avro 支持的表,配置单元将负责为该表创建适当的 Avro 模式。 以前的配置单元版本稍微更冗长一些。

作为一个例子,让我们将我们在Data Analysis with Apache Pig中生成的 PageRank 数据集加载到配置单元中。 此数据集是使用 Pig 的AvroStorage类创建的,具有以下架构:

{
  "type":"record",
  "name":"record",
  "fields": [
    {"name":"topic","type":["null","int"]},
    {"name":"source","type":["null","int"]},
    {"name":"rank","type":["null","float"]}
  ]
}  

表结构被捕获在 Avro 记录中,该记录包含标题信息(名称和限定名称的可选名称空间)和字段数组。 每个字段都指定了其名称和类型以及可选的文档字符串。

对于少数字段,类型不是单个值,而是一对值,其中一个为 NULL。 这是一个 Avro 联合,这是处理可能具有空值的列的惯用方式。 Avro 将 null 指定为具体类型,并且需要以这种方式指定其他类型可能具有 null 值的任何位置。 当我们使用以下架构时,这将为我们透明地处理。

有了这个定义,我们现在可以创建一个配置单元表格,该表格使用此模式作为其表格规范,如下所示:

CREATE EXTERNAL TABLE tweets_pagerank
ROW FORMAT SERDE
  'org.apache.hadoop.hive.serde2.avro.AvroSerDe'
WITH SERDEPROPERTIES ('avro.schema.literal'='{
    "type":"record",
    "name":"record",
    "fields": [
        {"name":"topic","type":["null","int"]},
        {"name":"source","type":["null","int"]},
        {"name":"rank","type":["null","float"]}
    ]
}')
STORED AS INPUTFORMAT
  'org.apache.hadoop.hive.ql.io.avro.AvroContainerInputFormat'
OUTPUTFORMAT
  'org.apache.hadoop.hive.ql.io.avro.AvroContainerOutputFormat'
LOCATION '${data}/ch5-pagerank';

然后,查看配置单元中的以下表定义(另请注意,我们将在数据生命周期管理中介绍的 HCatalog 也支持此类定义):

DESCRIBE tweets_pagerank;
OK
topic                 int                   from deserializer   
source                int                   from deserializer   
rank                  float                 from deserializer  

在 DDL 中,我们告诉配置单元使用AvroContainerInputFormatAvroContainerOutputFormat以 Avro 格式存储数据。 每行都需要使用org.apache.hadoop.hive.serde2.avro.AvroSerDe进行序列化和反序列化。 表模式由配置单元从嵌入在avro.schema.literal中的 Avro 模式中推断出来。

或者,我们可以在 HDFS 上存储模式,并让配置单元读取它以确定表结构。 在名为pagerank.avsc的文件中创建前面的模式-这是 Avro 模式的标准文件扩展名。 然后将其放在 HDFS 上;我们更希望有一个公共位置来存放模式文件,比如/schema/avro。 最后,使用avro.schema.urlserDe 属性WITH SERDEPROPERTIES ('avro.schema.url'='hdfs://<namenode>/schema/avro/pagerank.avsc')定义表。

如果类路径中不存在 avro 依赖项,我们需要在访问单个字段之前将 avroMapReducejar 添加到我们的环境中。 在配置单元内的 Cloudera CDH5 虚拟机上:

ADD JAR /opt/cloudera/parcels/CDH/lib/avro/avro-mapred-hadoop2.jar; 

我们也可以像使用其他桌子一样使用这张桌子。 例如,我们可以查询数据来选择 PageRank 较高的用户和主题对:

SELECT source, topic from tweets_pagerank WHERE rank >= 0.9;

数据生命周期管理中,我们将看到 Avro 和avro.schema.url如何在支持模式迁移方面发挥重要作用。

柱状商店

配置单元还可以通过ORC(https://cwiki.apache.org/confluence/display/Hive/LanguageManual+ORC)和Parquet(https://cwiki.apache.org/confluence/display/Hive/Parquet)格式利用列式存储。

如果一个表定义了非常多的列,那么对于任何给定的查询来说,只处理这些列的一小部分是很常见的。 但即使在 SequenceFile 中,也会从磁盘读取、解压缩和处理每一整行及其所有列。 对于我们事先知道不感兴趣的数据,这会消耗大量系统资源。

传统的关系数据库也以行为单位存储数据,一种名为Columnar****的数据库将改为以列为重点。 在最简单的模型中,表中的每列都有一个文件,而不是每个表都有一个文件。 如果查询只需要访问总共有 100 列的表中的 5 列,则只会读取这 5 列的文件。 ORC 和 Parquet 都使用这一原则以及其他优化来实现更快的查询。

**## 查询

可以使用熟悉的SELECT … FROM语句查询表。 WHERE语句允许指定过滤条件,GROUP BY聚合记录,ORDER BY指定排序条件,LIMIT指定要检索的记录数。 聚合函数(如countsum)可以应用于聚合记录。 例如,下面的代码返回数据集中最多产的前 10 位用户:

SELECT user_id, COUNT(*) AS cnt FROM tweets GROUP BY user_id ORDER BY cnt DESC LIMIT 10

这将返回数据集中最多产的前 10 名用户:

2263949659 4
1332188053  4
959468857  3
1367752118  3
362562944  3
58646041  3
2375296688  3
1468188529  3
37114209  3
2385040940  3

我们可以通过设置以下内容来提高hive输出的可读性:

SET hive.cli.print.header=true;

这将指示hive(尽管不是beeline)打印列名作为输出的一部分。

提示

您可以将该命令添加到。 hiverc文件通常位于执行用户的主目录的根目录中,以便将其应用于所有hiveCLI 会话。

HiveQL 实现了一个JOIN运算符,它使我们能够将表组合在一起。 在先决条件部分中,我们为 User 和 Place 对象生成了单独的数据集。 现在让我们使用外部表将它们加载到配置单元中。

我们首先创建一个user表来存储用户数据,如下所示:

CREATE EXTERNAL TABLE user (
created_at string,
user_id string,
`location` string,
name string,
description string,
followers_count bigint,
friends_count bigint,
favourites_count bigint,
screen_name string,
listed_count bigint
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS TEXTFILE
LOCATION '${input}/users';

然后,我们创建一个place表来存储位置数据,如下所示:

CREATE EXTERNAL TABLE place (
place_id string,
country_code string,
country string,
`name` string,
full_name string,
place_type string
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS TEXTFILE
LOCATION '${input}/places';

我们可以使用JOIN运算符显示最多产的 10 个用户的名称,如下所示:

SELECT tweets.user_id, user.name, COUNT(tweets.user_id) AS cnt 
FROM tweets 
JOIN user ON user.user_id  = tweets.user_id
GROUP BY tweets.user_id, user.user_id, user.name 
ORDER BY cnt DESC LIMIT 10; 

提示

配置单元中仅支持相等、外连接和左(半)连接。

请注意,可能有个条目具有给定的用户 ID,但followers_countfriends_countfavourites_count列的值不同。 为了避免重复的条目,我们只对tweets表中的user_id进行计数。

我们可以重写前面的查询,如下所示:

SELECT tweets.user_id, u.name, COUNT(*) AS cnt 
FROM tweets 
join (SELECT user_id, name FROM user GROUP BY user_id, name) u
ON u.user_id = tweets.user_id
GROUP BY tweets.user_id, u.name 
ORDER BY cnt DESC LIMIT 10;   

我们不是直接连接user表,而是执行子查询,如下所示:

SELECT user_id, name FROM user GROUP BY user_id, name;

子查询提取唯一的用户 ID 和名称。 请注意,配置单元对子查询的支持有限,以往仅允许在SELECT语句的FROM子句中使用子查询。 HIVE 0.13 还在WHERE子句中添加了对子查询的有限支持。

HiveQL 是一种不断发展的丰富语言,对它的全面阐述超出了本章的范围。 其查询和动态链接库功能的描述可以在https://cwiki.apache.org/confluence/display/Hive/LanguageManual中找到。

针对给定工作负载构建配置单元表

通常,配置单元并不是单独使用的,而是在创建表时考虑到特定的工作负载或需要以适合包含在自动化流程中的方式调用。 我们现在将探讨其中的一些场景。

对表进行分区

对于列式文件格式,我们解释了在处理查询时尽早排除不需要的数据的好处。 在 SQL 中使用类似的概念已经有一段时间了:表分区。

创建分区表时,会将列指定为分区键。 然后将具有该键的所有值存储在一起。 在配置单元的例子中,每个分区键的不同子目录都是在 HDFS 上仓库位置的表目录下创建的。

了解分区列的基数很重要。 由于截然不同的值太少,由于文件仍然非常大,好处就会减少。 如果值太多,则查询可能需要扫描大量文件才能访问所有必需的数据。 也许最常见的分区键是基于日期的分区键。 例如,我们可以根据created_at列(即用户首次注册的日期)对前面的user表进行分区。 请注意,由于按定义对表进行分区会影响其文件结构,因此我们现在将该表创建为非外部表,如下所示:

CREATE TABLE partitioned_user (
created_at string,
user_id string,
`location` string,
name string,
description string,
followers_count bigint,
friends_count bigint,
favourites_count bigint,
screen_name string,
listed_count bigint
)  PARTITIONED BY (created_at_date string)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS TEXTFILE;

要将数据加载到分区中,我们可以显式地给出要向其中插入数据的分区的值,如下所示:

INSERT INTO TABLE partitioned_user
PARTITION( created_at_date = '2014-01-01')
SELECT 
created_at,
user_id,
location,
name,
description,
followers_count,
friends_count,
favourites_count,
screen_name,
listed_count
FROM user;

这充其量是冗长的,因为我们需要为每个分区键值编写一条语句;如果一条LOADINSERT语句包含多个分区的数据,那么它就不能工作。 HIVE 还有一个称为动态分区的功能,它可以在这方面为我们提供帮助。 我们设置了以下三个变量:

SET hive.exec.dynamic.partition = true;
SET hive.exec.dynamic.partition.mode = nonstrict;
SET hive.exec.max.dynamic.partitions.pernode=5000;

前两条语句使所有分区(nonstrict选项)都是动态的。 第三个允许在每个映射器和减少器节点上创建 5000 个不同的分区。

然后,我们只需使用要用作分区键的列名,配置单元将根据给定行的键的值将数据插入到分区中:

INSERT INTO TABLE partitioned_user
PARTITION( created_at_date )
SELECT 
created_at,
user_id,
location,
name,
description,
followers_count,
friends_count,
favourites_count,
screen_name,
listed_count,
to_date(created_at) as created_at_date
FROM user;

即使我们在这里只使用一个分区列,我们也可以通过多个列键对表进行分区;只需在PARTITIONED BY子句中将它们作为逗号分隔的列表。

请注意,分区键列需要作为用于插入到分区表中的任何语句的最后一列。 在前面的代码中,我们使用配置单元的to_date函数将created_at时间戳转换为YYYY-MM-DD格式的字符串。

分区数据在 HDFS 中存储为/path/to/warehouse/<database>/<table>/key=<value>。 在我们的示例中,partitioned_user表结构将类似于/user/hive/warehouse/default/partitioned_user/created_at=2014-04-01

如果数据直接添加到文件系统中(例如,通过某个第三方处理工具或通过hadoop fs -put),元存储将不会自动检测新分区。 用户将需要为每个新添加的分区手动运行如下所示的ALTER TABLE语句:

ALTER TABLE <table_name> ADD PARTITION <location>;

要为元存储中当前不存在的所有分区添加元数据,我们可以使用:MSCK REPAIR TABLE <table_name>;语句。 在 EMR 上,这相当于执行以下语句:

ALTER TABLE <table_name> RECOVER PARTITIONS; 

请注意,这两个语句也适用于EXTERNAL表。 在下一章中,我们将了解如何利用此模式创建灵活且可互操作的管道。

覆盖和更新数据

当我们需要更新表的一部分时,分区也很有用。 通常,以下形式的语句将替换目标表的所有数据:

INSERT OVERWRITE INTO <table>…

如果省略OVERWRITE,则每个INSERT语句都会向表中添加额外的数据。 有时,这是可取的,但通常情况下,被摄取到配置单元表中的源数据旨在完全更新数据的子集,并保持其余数据不受影响。

如果我们对表的分区执行INSERT OVERWRITE语句(或LOAD OVERWRITE语句),那么只有指定的分区会受到影响。 因此,如果我们要插入用户数据,并且只想影响源文件中包含数据的分区,我们可以通过在前面的INSERT语句中添加OVERWRITE关键字来实现这一点。

我们还可以向SELECT语句添加警告。 例如,假设我们只想更新特定月份的数据:

INSERT INTO TABLE partitioned_user
PARTITION (created_at_date)
SELECT created_at ,
user_id,
location,
name,
description,
followers_count,
friends_count,
favourites_count,
screen_name,
listed_count,
to_date(created_at) as created_at_date
FROM user 
WHERE to_date(created_at) BETWEEN '2014-03-01' and '2014-03-31';

扣合和排序

对表进行分区是一种结构,您可以通过在针对表的查询的WHERE子句中使用分区列(或多个列)来显式地利用该结构。 还有另一种称为 Baketing 的机制,它可以进一步分割表的存储方式,并以一种允许配置单元自身优化其内部查询计划以利用该结构的方式来实现这一点。

让我们创建 tweet 和用户表的分桶版本;请注意CREATE TABLE语句中的以下附加CLUSTER BYSORT BY语句:

CREATE table bucketed_tweets (
tweet_id string,
text string,
in_reply_to string,
retweeted boolean,
user_id string,
place_id string
)  PARTITIONED BY (created_at string)
CLUSTERED BY(user_ID) into 64 BUCKETS
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS TEXTFILE;

CREATE TABLE bucketed_user (
user_id string,
`location` string,
name string,
description string,
followers_count bigint,
friends_count bigint,
favourites_count bigint,
screen_name string,
listed_count bigint
)  PARTITIONED BY (created_at string)
CLUSTERED BY(user_ID) SORTED BY(name) into 64 BUCKETS
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS TEXTFILE;

请注意,我们将 twets 表更改为也要分区;您只能存储已分区的表。

就像我们在插入分区表时需要指定分区列一样,我们还必须注意确保插入到存储桶表中的数据被正确地聚集在一起。 我们通过在将数据插入到表中之前设置以下标志来实现这一点:

SET hive.enforce.bucketing=true;

与分区表一样,在使用LOAD DATA语句时不能应用 Baketing 函数;如果希望将外部数据加载到分时段的表中,请首先将其插入到临时表中,然后使用INSERT…SELECT…语法填充分时段的表。

将数据插入到存储桶表中时,将根据应用于CLUSTERED BY子句中指定的列的散列函数的结果将行分配给存储桶。

当我们需要连接两个存储桶类似的表时,创建表的最大优势之一就出现在上面的示例中。 因此,例如,任何以下形式的查询都将得到极大的改进:

SET hive.optimize.bucketmapjoin=true;
SELECT …
FROM bucketed_user u JOIN bucketed_tweet t
ON u.user_id = t.user_id;

由于联接是在用于存储表的列上执行的,配置单元可以优化处理量,因为它知道每个存储桶在两个表中包含相同的一组user_id列。 在确定要匹配的行时,只需要比较存储桶中的行,而不需要比较整个表。 这确实要求两个表都聚集在同一列上,并且存储桶编号要么相同,要么是另一个的倍数。 在后一种情况下,假设一个表聚集到 32 个桶中,另一个表聚集到 64 个桶中,用于将数据分配给桶的默认哈希函数的性质意味着第一个表中的桶 3 中的 ID 将覆盖第二个表中的桶 3 和 35 中的 ID。

采样数据

在使用配置单元对表中的数据进行采样的功能时,对表进行分段化也会有所帮助。 采样允许查询仅收集表中全部行的指定子集。 当您有一个非常大的表,并且具有适度一致的数据模式时,这很有用。 在这种情况下,将查询应用于一小部分数据会快得多,并且仍然会给出具有广泛代表性的结果。 当然,请注意,这只适用于您希望确定表特征(如数据中的模式范围)的查询;如果您试图计算任何内容,则需要将结果缩放到完整的表大小。

对于未分桶的表,您可以通过指定查询应仅应用于表的特定子集,采用与我们前面看到的类似的机制进行采样:

SELECT max(friends_count)
FROM user TABLESAMPLE(BUCKET 2 OUT OF 64 ON name);

在此查询中,配置单元将根据 Name 列将表中的行有效地散列到 64 个存储桶中。 然后,它将只使用第二个存储桶进行查询。 可以指定多个存储桶,如果给出RAND()作为ON子句,则整行都由 bakting 函数使用。

虽然成功,但效率很低,因为需要扫描整个表才能生成所需的数据子集。 如果我们对存储桶表进行采样,并确保采样的存储桶数等于或等于表中存储桶的倍数,则配置单元将只读取相关的存储桶。 例如:

SELECT MAX(friends_count)
FROM bucketed_user TABLESAMPLE(BUCKET 2 OUT OF 32 on user_id);

在前面针对bucketed_user表的查询中,该表是在user_id列上使用 64 个存储桶创建的,由于它使用同一列,因此采样将只读取所需的存储桶。 在本例中,这些将是来自每个分区的存储桶 2 和 34。

抽样的最终形式是块抽样。 在这种情况下,我们可以指定要采样的表的所需数量,而配置单元将使用近似值,只在 HDFS 上读取足够的源数据块来满足所需的大小。 目前,数据大小可以指定为表的百分比、绝对数据大小或行数(在每个块中)。 TABLESAMPLE的语法如下所示,每个拆分将分别对表的 0.5%、1 GB 的数据或 100 行进行采样:

TABLESAMPLE(0.5 PERCENT)
TABLESAMPLE(1G)
TABLESAMPLE(100 ROWS)

如果您对后一种采样形式感兴趣,请参考文档,因为支持的输入格式和文件格式有一些特定限制。

编写脚本

我们可以将配置单元命令放在一个文件中,并在hiveCLI 实用程序中使用-f选项运行它们:

$ cat show_tables.hql
show tables;
$ hive -f show_tables.hql 

我们可以通过hiveconf机制将 HiveQL 语句参数化。 这允许我们在使用环境变量时指定环境变量名,而不是在调用点指定环境变量名。 例如:

$ cat show_tables2.hql
show tables like '${hiveconf:TABLENAME}';
$ hive -hiveconf TABLENAME=user -f show_tables2.hql

也可以在配置单元脚本或交互式会话中设置该变量:

SET TABLE_NAME='user';

前面的hiveconf参数将在与配置单元配置选项相同的名称空间中添加任何新变量。 从配置单元 0.8 开始,有一个名为hivevar的类似选项,它可以将任何用户变量添加到不同的名称空间中。 使用hivevar时,前面的命令如下所示:

$ cat show_tables3.hql
show tables like '${hivevar:TABLENAME}';
$ hive -hivevar TABLENAME=user –f show_tables3.hql

或者我们可以交互地编写命令:

SET hivevar:TABLE_NAME='user';

配置单元和亚马逊网络服务

使用 Elastic MapReduce 作为 AWS Hadoop-on-Demand 服务,当然可以在 EMR 集群上运行配置单元。 但是也可以使用任何 Hadoop 集群中的 Amazon 存储服务,特别是 S3,无论是在 EMR 中还是您自己的本地集群中。

Hive 和 S3

正如在第 2 章存储中提到的,可以为 Hadoop 指定 HDFS 以外的默认文件系统,S3 是一个选项。 但是,它不一定是要么全有要么全无的事情;可以将特定的表存储在 S3 中。 这些表的数据将被检索到集群中进行处理,生成的任何数据都可以写入不同的 S3 位置(同一个表不能是单个查询的源和目标),也可以写入 HDFS。

我们可以使用如下命令获取推文数据的文件并将其放到 S3 中的某个位置:

$ aws s3 put tweets.tsv s3://<bucket-name>/tweets/

我们首先需要指定可以访问存储桶的访问密钥和秘密访问密钥。 这可以通过三种方式实现:

  • 在配置单元 CLI 中将fs.s3n.awsAccessKeyIdfs.s3n.awsSecretAccessKey设置为适当的值
  • hive-site.xml中设置相同的值,但请注意,这将 S3 的使用限制为单组凭据
  • 在表 URL 中显式指定表位置,即s3n://<access key>:<secret access key>@<bucket>/<path>

然后,我们可以创建一个引用此数据的表,如下所示:

CREATE table remote_tweets (
created_at string,
tweet_id string,
text string,
in_reply_to string,
retweeted boolean,
user_id string,
place_id string
)  CLUSTERED BY(user_ID) into 64 BUCKETS
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\t'
LOCATION 's3n://<bucket-name>/tweets'

这可能是将 S3 数据拉入本地 Hadoop 集群进行处理的一种非常有效的方式。

备注

为了在 S3 位置的 URI 中使用 AWS 凭据,无论参数如何传递,密钥和访问密钥不得包含/+=\字符。 如果需要,可以从位于https://console.aws.amazon.com/iam/的 IAM 控制台生成一组新的凭证。

从理论上讲,您可以将数据留在外部表中,并在需要时引用它,以避免 WAN 数据传输延迟(和成本),尽管将数据放入本地表并从那里进行未来处理通常是有意义的。 例如,如果表是分区的,那么您可能会发现自己每天都要检索一个新分区。

弹性 MapReduce 上的配置单元

在某种程度上,在 Amazon Elastic MapReduce 中使用配置单元与本章中讨论的所有内容一样,只需使用即可。 您可以创建持久集群,登录到主节点,并使用配置单元 CLI 创建表和提交查询。 执行所有这些操作将使用 EC2 实例上的本地存储来存储表数据。

毫不奇怪,EMR 集群上的作业也可以引用其数据存储在 S3(或 DynamoDB)上的表。 同样不足为奇的是,亚马逊对其版本的 Hive 进行了扩展,使这一切变得非常无缝。 在 EMR 作业中,从存储在 S3 中的表中提取数据、对其进行处理、将任何中间数据写入 EMR 本地存储,然后将输出结果写入 S3、DynamoDB 或不断增加的其他 AWS 服务列表中的一个,这非常简单。

前面提到的模式,即每天将新数据添加到表的新分区目录中,在 S3 中已被证明非常有效;它通常是大型和增量增长的数据集的存储位置选择。 使用 EMR 时存在语法差异;与前面提到的 MSCK 命令不同,使用添加到分区目录的新数据更新配置单元表的命令如下:

ALTER TABLE <table-name> RECOVER PARTITIONS;

有关http://docs.aws.amazon.com/ElasticMapReduce/latest/DeveloperGuide/emr-hive-additional-features.html的最新增强功能,请参阅电子病历文档。 此外,请参考更广泛的 EMR 文档。 特别是,与其他 AWS 服务的集成点是一个快速增长的领域。

扩展 HiveQL

HiveQL 语言可以通过插件和第三方函数进行扩展。 在配置单元中,有三种类型的函数,其特征在于它们作为输入和生成的行数:

  • 用户定义函数(UDF):是一次作用于一行的个更简单的函数。
  • 自定义聚合函数(UDAF):取多行作为输入,生成多行作为输出。 这些是与GROUP BY语句(类似于COUNT()AVG()MIN()MAX()等)一起使用的聚合函数。
  • User Defined Table Functions(UDTFs):接受个多行作为输入,并生成一个由多个行组成的逻辑表,该逻辑表可以在联接表达式中使用。

提示

这些 API 仅在 Java 中提供。 对于其他语言,可以使用TRANSFORMMAPREDUCE子句通过用户定义的脚本流式传输数据,这些子句充当 Hadoop 流功能的前端。

有两个 API 可用于编写 UDF。 一个简单的 APIorg.apache.hadoop.hive.ql.exec.UDF可用于获取和返回基本可写类型的函数。 在org.apache.hadoop.hive.ql.udf.generic.GenericUDF包中提供了更丰富的 API,它提供了对除 WRITABLE 之外的数据类型的支持。 现在我们将说明如何使用org.apache.hadoop.hive.ql.exec.UDF来实现类似于我们在使用 Spark迭代计算中使用的 String to ID 函数,以将标签映射到 Pig 中的整数。 使用此 API 构建 UDF 只需要扩展 UDF 类和编写evaluate()方法,如下所示:

public class StringToInt extends UDF {
    public Integer evaluate(Text input) {
        if (input == null)
            return null;

         String str = input.toString();
         return str.hashCode();
    }
}

该函数接受Text对象作为输入,并使用hashCode()方法将其映射为整数值。 此函数的源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch7/udf/com/learninghadoop2/hive/udf/StringToInt.java中找到。

提示

正如在第 6 章使用 Apache Pig 进行数据分析中所指出的,在生产中应该使用更健壮的散列函数。

我们编译该类并将其存档到 JAR 文件中,如下所示:

$ javac -classpath $(hadoop classpath):/opt/cloudera/parcels/CDH/lib/hive/lib/* com/learninghadoop2/hive/udf/StringToInt.java 
$ jar cvf myudfs-hive.jar com/learninghadoop2/hive/udf/StringToInt.class

在使用 UDF 之前,必须使用以下命令在配置单元中注册 UDF:

ADD JAR myudfs-hive.jar;
CREATE TEMPORARY FUNCTION string_to_int AS 'com.learninghadoop2.hive.udf.StringToInt'; 

ADD JAR语句将 JAR 文件添加到分布式缓存。 语句的作用是:在配置单元中注册一个实现给定 Java 类的函数。 一旦配置单元会话关闭,该函数将被删除。 从配置单元 0.13 开始,可以使用CREATE FUNCTION … 创建永久函数,其定义保存在元存储中。

注册后,可以像任何其他函数一样在查询中使用StringToInt。 在下面的示例中,我们首先通过应用regexp_extract从 tweet 文本中提取一个标签列表。 然后,我们使用string_to_int将每个标记映射到一个数字 ID:

SELECT unique_hashtags.hashtag, string_to_int(unique_hashtags.hashtag) AS tag_id FROM
    (
        SELECT regexp_extract(text, 
            '(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)') as hashtag  
        FROM tweets 
        GROUP BY regexp_extract(text, 
        '(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)')
) unique_hashtags GROUP BY unique_hashtags.hashtag, string_to_int(unique_hashtags.hashtag);

正如我们在上一章中所做的那样,我们可以使用前面的查询来创建查找表:

CREATE TABLE lookuptable (tag string, tag_id bigint);
INSERT OVERWRITE TABLE lookuptable 
SELECT unique_hashtags.hashtag, 
    string_to_int(unique_hashtags.hashtag) as tag_id
FROM 
  (
    SELECT regexp_extract(text, 
        '(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)') AS hashtag  
         FROM tweets 
         GROUP BY regexp_extract(text, 
            '(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)')
   ) unique_hashtags 
GROUP BY unique_hashtags.hashtag, string_to_int(unique_hashtags.hashtag);

可编程接口

除了hivebeeline命令行工具之外,还可以通过 JDBC 和 Thrift 编程接口向系统提交 HiveQL 查询。 对 ODBC 的支持捆绑在较旧版本的配置单元中,但从配置单元 0.12 开始,它需要从头开始构建。 有关这一过程的更多信息,请参见https://cwiki.apache.org/confluence/display/Hive/HiveODBC

JDBC

使用 JDBC API 编写的配置单元客户端看起来与为其他数据库系统(例如 MySQL)编写的客户端程序完全相同。 以下是使用 JDBC API 的配置单元客户端程序示例。 这个示例的源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch7/clients/com/learninghadoop2/hive/client/HiveJdbcClient.java中找到。

public class HiveJdbcClient {
     private static String driverName = " org.apache.hive.jdbc.HiveDriver";

     // connection string
     public static String URL = "jdbc:hive2://localhost:10000";

     // Show all tables in the default database
     public static String QUERY = "show tables";

     public static void main(String[] args) throws SQLException {
          try {
               Class.forName (driverName);
          } 
          catch (ClassNotFoundException e) {
               e.printStackTrace();
               System.exit(1);
          }
          Connection con = DriverManager.getConnection (URL);
          Statement stmt = con.createStatement();

          ResultSet resultSet = stmt.executeQuery(QUERY);
          while (resultSet.next()) {
               System.out.println(resultSet.getString(1));
          }
    }
}

URL部分是描述连接端点的 JDBC URI。 建立远程连接的格式为jdbc:hive2:<host>:<port>/<database>。 嵌入式模式下的连接可以通过不指定主机或端口(如jdbc:hive2://)来建立。

hivehive2是连接到HiveServerHiveServer2时要使用的驱动程序。 QUERY包含要执行的 HiveQL 查询。

提示

配置单元的 JDBC 接口仅公开默认数据库。 为了访问其他数据库,您需要在底层查询中使用<database>.<table>符号显式引用它们。

首先,我们加载HiveServer2JDBC 驱动程序org.apache.hive.jdbc.HiveDriver

提示

使用org.apache.hadoop.hive.jdbc.HiveDriver连接到 HiveServer。

然后,与任何其他 JDBC 程序一样,我们建立到URL的连接并使用它实例化一个Statement类。 我们在没有身份验证的情况下执行QUERY,并将输出数据集存储到ResultSet对象中。 最后,我们扫描resultSet并将其内容打印到命令行。

使用以下命令编译并执行该示例:

$ javac HiveJdbcClient.java
$ java -cp $(hadoop classpath):/opt/cloudera/parcels/CDH/lib/hive/lib/*:/opt/cloudera/parcels/CDH/lib/hive/lib/hive-jdbc.jar: com.learninghadoop2.hive.client.HiveJdbcClient

节俭

Thrift 提供对配置单元的低级访问,并且与 HiveServer 的 JDBC 实现相比具有许多优势。 首先,它允许来自同一客户端的多个连接,并且允许轻松使用 Java 以外的编程语言。 在 HiveServer2 中,这是一个不太常用的选项,但在兼容性方面仍然值得一提。 使用 Java API 实现的示例 Thrift 客户端可以在https://github.com/learninghadoop2/book-examples/blob/master/ch7/clients/com/learninghadoop2/hive/client/HiveThriftClient.java中找到。 此客户端可用于连接到 HiveServer,但由于协议差异,客户端不能与 HiveServer2 一起工作。

在本例中,我们定义了一个getClient()方法,该方法接受 HiveServer 服务的主机和端口作为输入,并返回org.apache.hadoop.hive.service.ThriftHive.Client的实例。

通过首先实例化到 HiveServer 服务的套接字连接org.apache.thrift.transport.TSocket,并指定协议org.apache.thrift.protocol.TBinaryProtocol来序列化和传输数据,从而获得客户端,如下所示:

        TSocket transport = new TSocket(host, port);
        transport.setTimeout(TIMEOUT);
        transport.open();
        TBinaryProtocol protocol = new TBinaryProtocol(transport);
        client = new ThriftHive.Client(protocol);

我们从 Main 方法调用getClient(),并使用客户端对端口11111上本地主机上运行的 HiveServer 实例执行查询,如下所示:

     public static void main(String[] args) throws Exception {
          Client client = getClient("localhost", 11111);
          client.execute("show tables");
          List<String> results = client.fetchAll();           
for (String result : results) {
System.out.println(result);           
} 
     }

确保 HiveServer 在端口11111上运行,如果没有,请使用以下命令启动实例:

$ sudo hive --service hiveserver -p 11111

使用以下命令编译并执行HiveThriftClient.java示例:

$ javac $(hadoop classpath):/opt/cloudera/parcels/CDH/lib/hive/lib/* com/learninghadoop2/hive/client/HiveThriftClient.java
$ java -cp $(hadoop classpath):/opt/cloudera/parcels/CDH/lib/hive/lib/*: com.learninghadoop2.hive.client.HiveThriftClient

Stinger 计划

从最早的版本开始,HIVE 就一直非常成功和强大,尤其是它能够在庞大的数据集上提供类似 SQL 的处理。 但其他技术并没有停滞不前,Hive 获得了相对缓慢的名声,特别是在大型任务的启动时间较长,以及无法对概念上简单的查询做出快速响应方面。

这些感知到的限制与其说是由于配置单元本身的原因,不如说是因为与实现 SQL 查询的其他方式相比,将 SQL 查询转换为 MapReduce 模型的效率非常低。 特别是在非常大的数据集方面,MapReduce 看到了大量的 I/O(因此花费了大量时间)写出一个 MapReduce 作业的结果,只是为了让另一个作业读取它们。 正如在第 3 章Processing-MapReduce 和 Beyond中所讨论的,这是 TEZ 设计中的主要驱动因素,它可以将 Hadoop 集群上的作业调度为任务图,而不需要在它们之间进行低效的读写。

以下是对 MapReduce 框架与 TEZ 的查询:

SELECT a.country, COUNT(b.place_id) FROM place a JOIN tweets b ON (a. place_id = b.place_id) GROUP BY a.country;

下图对比了 MapReduce 框架上前面查询的执行计划与 TEZ:

Stinger initiative

MapReduce 上的配置单元与 TEZ

在普通 MapReduce 中,为GROUP BYJOIN子句创建了两个作业。 第一个作业由一组 MapReduce 任务组成,这些任务从磁盘读取数据以执行分组。 还原器将中间结果写入磁盘,以便同步输出。 第二个作业中的映射器从磁盘读取中间结果以及从表 b 读取数据。然后将组合的数据集传递到还原器,在那里连接共享密钥。 如果我们执行一条ORDER BY语句,这将导致第三个作业和更多的 MapReduce 过程。 相同的查询由从磁盘读取数据的一组 Map 任务作为单个作业在 TEZ 上执行。 I/O 分组和连接在减速器之间以流水线方式进行。

除了这些架构限制外,围绕 SQL 语言支持的许多领域也可以提供更高的效率,2013 年初,Stinger 计划启动,明确目标是使 Hive 的速度提高 100 倍以上,并提供更丰富的 SQL 支持。 HIVE 0.13 拥有 Stinger 的三个阶段的所有功能,从而形成了更加完整的 SQL 方言。 此外,除了基于 MapReduce 的 YAINE 实现之外,TEZ 还作为一个执行框架提供,这比之前在 Hadoop1MapReduce 上的实现更高效。

使用 TEZ 作为执行引擎,配置单元不再局限于一系列线性 MapReduce 作业,而是可以构建一个处理图,其中任何给定的步骤都可以(例如)将结果流式传输到多个子步骤。

为了利用 TEZ 框架,有一个新的hive变量设置:

set hive.execution.engine=tez;

这个设置依赖于集群上安装的 tez;它的源代码形式可以从Cloudera获得,也可以在几个发行版中获得,不过在撰写本文时,还没有 http://tez.apache.org

另一个值是mr,它使用经典的 MapReduce 模型(在 Yarn 之上),因此可以在单个安装中与使用 TEZ 的配置单元的性能进行比较。

同步,由 Elderman 更正@ELDER_MAN

HIVE 不是唯一提供 SQL-on-Hadoop 功能的产品。 第二个使用最广泛的可能是黑斑羚,它于 2012 年底宣布,并于 2013 年春季发布。 虽然最初是在 Cloudera 内部开发的,但它的源代码会定期推送到开源 Git 存储库(https://github.com/cloudera/impala)。

黑斑羚是基于对 Hive 弱点的认识而创造出来的,正是这种认识导致了毒刺计划。

Impala 还从 Google Dremel(http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/36632.pdf)中获得了一些灵感,2009 年发表的一篇论文首次公开描述了这一点。 Dremel 是由 Google 构建的,目的是解决在非常大的数据集上进行非常快速的查询的需求与当时支撑 Hive 的现有 MapReduce 模型固有的高延迟之间的差距。 Dremel 是解决此问题的一种复杂方法,它不是在 MapReduce(如由 Have 实现的)之上构建缓解,而是创建一个访问存储在 HDFS 中的相同数据的新服务。 Dremel 还受益于大量工作,优化了数据的存储格式,使其更易于接受非常快的分析查询。

黑斑羚的架构

基本架构有三个主要组件:黑斑羚守护进程、状态存储和客户端。 最近的版本添加了改进服务的附加组件,但我们将重点放在高级体系结构上。

应该在 DataNode 进程管理 HDFS 数据的每个主机上运行 Impala 守护程序(impalad)。 请注意,impalad并不通过完整的 HDFS 文件系统 API 访问文件系统块;相反,它使用一种称为短路读取的特性来提高数据访问效率。

当客户端提交查询时,它可以向任何正在运行的impalad进程提交查询,而这个进程将成为该查询执行的协调器。 Impala 性能的关键方面在于,对于每个查询,它都会生成自定义本机代码,然后将这些代码推送到系统上的所有impalad进程并由其执行。 这个高度优化的代码在本地数据上执行查询,然后每个impalad将其结果集的子集返回给协调器节点,协调器节点执行最终的数据合并以产生最终结果。 任何使用过目前可用的(通常是商用且昂贵的)大规模并行处理(MPP)(用于这种类型的共享横向扩展体系结构的术语)数据仓库解决方案的人都应该熟悉这种类型的体系结构。 在集群运行时,状态存储守护进程确保每个impalad进程都知道所有其他进程,并提供整体集群健康状况的视图。

与 Hive 共存

Impala 作为一种较新的产品,往往具有更多受限的 SQL 数据类型集,并且比配置单元支持更受限的 SQL 方言。 然而,随着每个新版本的发布,它正在扩展这一支持。 请参阅 Impala 文档(http://www.cloudera.com/content/cloudera-content/cloudera-docs/CDH5/latest/Impala/impala.html),了解当前支持级别的概述。

Impala 支持配置单元使用的元数据存储机制,用于持久存储围绕其表结构和存储的元数据。 这意味着,在具有现有配置单元设置的集群上,应该可以立即使用 Impala,因为它将访问相同的元存储,从而提供对配置单元中可用的相同表的访问。

但需要注意的是,在组合的配置单元和黑斑羚环境中工作时,SQL 方言和数据类型的差异可能会导致意想不到的结果。 有些查询可能在其中一个上有效,但在另一个上无效,它们可能表现出非常不同的性能特征(稍后将对此进行详细介绍),或者它们实际上可能会给出不同的结果。 最后一点在使用浮点和双精度这样的数据类型时可能会变得明显,这些数据类型在底层系统中只是被区别对待(配置单元是在 Java 上实现的,而 Impala 是用 C++编写的)。

从 1.2 版开始,它支持用 C++和 Java 编写的个 UDF,尽管强烈推荐 C++作为更快的解决方案。 如果您希望在 Hive 和黑斑羚之间共享自定义功能,请记住这一点。

一种不同的哲学

当 Impala 第一次发布时,它最大的好处在于它真正实现了通常所说的思维速度分析。 可以足够快地返回查询,以至于分析师可以以完全交互的方式探索分析线索,而不必一次等待几分钟才能完成每个查询。 公平地说,大多数采用黑斑羚的人有时会对它的表现感到震惊,特别是与当时的 Hive 发货版本相比。

Impala 的关注点主要停留在这些较短的查询上,这确实对系统造成了一些限制。 黑斑羚往往会占用大量内存,因为它依赖内存中的处理来实现其大部分性能。 如果查询需要将数据集保存在内存中,而不是在执行节点上可用,那么在 2.0 之前的 Impala 版本中,该查询将简单地失败。

将 Stinger 上的工作与 Impala 进行比较,可以说 Impala 更加注重在支持交互式数据分析的更短(也可以说是更常见)的查询中出类拔萃。 许多商业智能工具和服务现在都经过认证,可以直接在 Impala 上运行。 毒刺计划在黑斑羚擅长的领域减少了让 Hive 变得同样快的努力,而是(在不同程度上)改进了 Hive 在所有工作负载下的速度。 Impala 仍在快速发展,Stinger 已经为 Hive 注入了额外的动力,因此明智的做法是同时考虑这两种产品,并确定哪种产品最能满足您的项目和工作流程的性能和功能要求。

还应该记住的是,影响黑斑羚和 Hive 方向的是竞争的商业压力。 Impala 是由 Cloudera 创建的,并且仍然由 Cloudera 驱动,Cloudera 是 Hadoop 发行版中最受欢迎的供应商。 Stinger 计划虽然得到了微软等多家公司的支持(是的,真的!)。 Intel 是由 Hortonworks 领导的,可能是 Hadoop 发行版的第二大供应商。 事实是,如果您使用的是 Hadoop 的 Cloudera 发行版,那么 Have 的一些核心特性可能会出现得较慢,而 Impala 将始终是最新的。 相反,如果你使用另一个发行版,你可能会得到最新的 Hive 版本,但它可能有一个更旧的 Impala,或者就像目前的情况一样,你可能需要自己下载并安装它。

前面提到的拼花和 ORC 文件格式也出现了类似的情况。 镶木地板是黑斑羚的首选,由 Cloudera 领导的一群公司开发,而 ORC 是 Hive 的首选,是 Hortonworks 的拥护者。

不幸的是,现实情况是,Cloudera 发行版对 Parquet 的支持通常很快,但在 Hortonworks 发行版中就不是那么快了,在 Hortonworks 发行版中,ORC 文件格式是首选格式。

这些主题有点令人担忧,因为虽然这一领域的竞争是一件好事,而且可以说,Impala 的宣布帮助激发了 Hive 社区的活力,但与过去不同的是,您选择的分发版本可能会对完全支持的工具和文件格式产生更大的影响,这一风险更大。 希望目前的情况只是我们在所有这些新的和改进的技术的开发周期中所处位置的产物,但是一定要根据您的 SQL-on-Hadoop 需求仔细考虑您的发行版选择。

Drill、Tajo 和 Beyond

您还应该考虑到 Hadoop 上的 SQL 不再只指配置单元或黑斑羚。 ApacheDrill(DREMEL)是 http://drill.apache.org 最先描述的 DREMEL 模型的一个更完整的实现。 尽管 Impala 跨 HDFS 数据实现了 Dremel 体系结构,但 Drill 希望跨多个数据源提供类似的功能。 它还处于早期阶段,但如果你的需求比 Hive 或黑斑羚提供的更广泛,它可能是值得考虑的。

TAJO(Hadoop)是另一个寻求成为 http://tajo.apache.org 数据上的完整数据仓库系统的 Apache 项目。 通过与 Impala 类似的体系结构,它提供了一个更丰富的系统,其中包含多个优化器和 ETL 工具等组件,这些组件在传统数据仓库中很常见,但在 Hadoop 世界中很少捆绑在一起。 它的用户基础要小得多,但已经被某些公司成功地使用了很长时间,如果您需要更全面的数据仓库解决方案,可能值得考虑。

其他产品也在这个领域涌现,做一些研究是个好主意。 Hive 和黑斑羚都是很棒的工具,但如果你发现它们不能满足你的需求,那就四处看看--其他的可能会。

摘要

在早期,Hadoop 有时被错误地视为最新的关系数据库杀手。 随着时间的推移,越来越明显的是,更明智的方法是将其视为 RDBMS 技术的补充,事实上,RDBMS 社区已经开发了 SQL 等工具,这些工具在 Hadoop 世界中也很有价值。

HiveQL 是 SQL 在 Hadoop 上的实现,是本章的重点。 关于 HiveQL 及其实现,我们讨论了以下主题:

  • HiveQL 如何在 HDFS 中存储的数据之上提供逻辑模型,这与预先强制实施表结构的关系数据库不同
  • HiveQL 如何支持许多标准 SQL 数据类型和命令,包括连接和视图
  • HiveQL 提供的类似 ETL 的特性,包括将数据导入到表中的能力,以及通过分区和类似机制优化表结构的能力
  • HiveQL 如何提供使用用户定义代码扩展其核心运算符集合的能力,以及这与 Pig UDF 机制有何不同
  • Hive 开发的近期历史,例如 Stinger 计划,见证了 Hive 过渡到使用 TEZ 的更新实现
  • 围绕 HiveQL 的更广泛的生态系统,现在包括 Impala、Tajo 和 Drill 等产品,以及这些产品如何专注于突出的特定领域

在 Pig and Have 中,我们引入了处理 MapReduce 数据的替代模型,但到目前为止,我们还没有研究另一个问题:需要什么方法和工具才能真正允许 Hadoop 中收集的海量数据集随着时间的推移保持有用和可管理? 在下一章中,我们将略微提升抽象层次,看看如何管理这一巨大数据资产的生命周期。**

八、数据生命周期管理

我们前面的章节非常关注技术,描述了特定的工具或技术以及如何使用它们。 在本章和下一章中,我们将采取更自上而下的方法,描述您可能遇到的问题空间,然后探索如何解决它。 我们将特别介绍以下主题:

  • 我们所说的术语数据生命周期管理的含义是什么
  • 为什么需要考虑数据生命周期管理
  • 可用于解决问题的工具类别
  • 如何使用这些工具构建 Twitter 情绪分析管道的前半部分

什么是数据生命周期管理

数据不只存在于某个时间点。 特别是对于长期运行的生产工作流,您可能会在 Hadoop 集群中获取大量数据。 需求很少会长时间保持不变,因此除了新逻辑之外,您可能还会看到该数据的格式发生了变化,或者需要使用多个数据源来提供在应用中处理的数据集。 我们使用术语数据生命周期管理来描述一种处理数据收集、存储和转换的方法,该方法可确保数据处于需要的位置,采用其需要的格式,并允许数据和系统随时间演变。

数据生命周期管理的重要性

如果构建数据处理应用,则根据定义,您依赖于处理的数据。 正如我们考虑应用和系统的可靠性一样,有必要确保数据也是生产就绪的。

在某些情况下,需要将数据吸收到 Hadoop 中。 它是企业的一部分,通常与外部系统有多个集成点。 如果来自这些系统的数据获取不可靠,则对处理该数据的作业的影响通常与重大系统故障一样具有破坏性。 数据接收成为本身的一个关键组件。 当我们说摄取需要可靠时,我们不仅仅是指数据正在到达;它还必须以一种可用的格式到达,并通过一种能够处理随时间演变的机制。

其中许多问题的问题在于,除非流量很大,系统很关键,而且任何问题的业务影响都不是微不足道的,否则它们不会以显著的方式出现。 对于不太关键的数据流有效的临时方法通常不会进行扩展,但在活动系统上进行替换会非常痛苦。

帮助工具

但是不要惊慌! 有许多类别的工具可以帮助解决数据生命周期管理问题。 在本章中,我们将提供以下三大类别的示例:

  • 编排服务:构建接收管道通常有多个独立的阶段,我们将使用编排工具来描述、执行和管理这些阶段
  • 连接器:鉴于与外部系统集成的重要性,我们将了解如何使用连接器来简化 Hadoop 存储提供的抽象
  • 文件格式:我们存储数据的方式会影响我们管理格式随时间演变的方式,有几种丰富的存储格式可以支持这一点

构建推文分析能力

在前面的章节中,我们使用了 Twitter 数据分析的各种实现来描述几个概念。 我们将把这一能力推向更深层次,并将其作为一个主要案例进行研究。

在本章中,我们将构建一条数据接收管道,构建一个在设计时考虑到可靠性和未来发展的生产就绪数据流。

我们将在本章中逐步构建管道。 在每个阶段,我们将强调哪些内容发生了变化,但不能在没有将章的大小增加两倍的情况下包含每个阶段的完整清单。 然而,本章的源代码包含了每一次迭代的全部内容。

获取推文数据

我们需要做的第一件事是获取实际的 tweet 数据。 与前面的示例一样,我们可以将-j-n参数传递给stream.py,以将 JSON tweet 转储到 stdout:

$ stream.py -j -n 10000 > tweets.json

因为我们有这个工具可以按需创建一批示例 tweet,所以我们可以通过定期运行此作业来开始我们的接收管道。 但如何做到呢?

介绍 Oozie

当然,我们可以将块放在一起,并使用类似 cron 的东西来进行简单的作业调度,但请记住,我们需要一个考虑到可靠性的接收管道。 因此,我们非常需要一种可以用来检测故障并以其他方式响应异常情况的调度工具。

我们将在这里使用的工具是 Oozie(Hadoop),这是一个关注 http://oozie.apache.org 生态系统的工作流引擎和调度器。

Oozie 提供了一种将工作流定义为一系列节点的方法,这些节点具有可配置的参数和从一个节点到下一个节点的受控转换。 它是作为 Cloudera QuickStart VM 的一部分安装的,主命令行客户机名为oozie,这并不奇怪。

备注

我们已经针对 Cloudera QuickStart VM 的 5.0 版本测试了本章中的工作流,在撰写 Oozie 的最新版本 5.1 时,它存在一些问题。 然而,我们的工作流中没有特定于版本的东西,因此它们应该与任何正确工作的 Ooziev4 实现兼容。

虽然 Oozie 功能强大且灵活,但它可能需要一点时间才能适应,所以我们将举几个例子,并描述我们在此过程中正在做些什么。

Oozie 工作流中最常见的节点是操作。 在动作节点中实际执行工作流的步骤;其他节点类型在决策、并行性和故障检测方面处理工作流的管理。 Oozie 可以执行多种类型的操作。 其中之一是 shell 操作,它可用于在系统上执行任何命令,例如本机二进制文件、shell 脚本或任何其他命令行实用程序。 让我们创建一个脚本来生成 tweet 文件,并将其复制到 HDFS:

set -e
source twitter.keys
python stream.py -j -n 500 > /tmp/tweets.out
hdfs dfs -put /tmp/tweets.out /tmp/tweets/tweets.out
rm -f /tmp/tweets.out

请注意,如果包含的任何命令失败,第一行将导致整个脚本失败。 我们使用环境文件为twitter.keys中的脚本提供 Twitter 密钥,其格式如下:

export TWITTER_CONSUMER_KEY=<value>
export TWITTER_CONSUMER_SECRET=<value>
export TWITTER_ACCESS_KEY=<value> 
export TWITTER_ACCESS_SECRET=<value>

Oozie 使用 XML 描述其工作流,通常存储在名为workflow.xml的文件中。 让我们来看看调用 shell 命令的 Oozie 工作流的定义。

Oozie 工作流的模式称为Workflow-app,我们可以为该工作流指定一个特定的名称。 在 CLI 或 Oozie Web 用户界面中查看作业历史记录时,这很有用。 在本书的示例中,我们将使用递增的版本号,以便更容易地分离源库中的迭代。 下面是我们为工作流应用指定特定名称的方式:

<workflow-app  name="v1">

Oozie 工作流由一系列相连的节点组成,每个节点代表流程中的一个步骤,并由工作流定义中的 XML 节点表示。 Oozie 有许多节点处理工作流从一个步骤到下一个步骤的过渡。 其中第一个节点是 Start 节点,它只说明要作为工作流一部分执行的第一个节点的名称,如下所示:

    <start to="fs-node"/>

然后,我们就有了命名开始节点的定义。 在本例中,它是一个动作节点,它是实际执行某些处理的大多数 Oozie 节点的泛型节点类型,如下所示:

    <action name="fs-node">

Action 是一个广泛的节点类别,然后我们通常会对它进行专门化,并针对此给定节点进行特定处理。 在本例中,我们使用 fs 节点类型,它允许我们执行文件系统操作:

    <fs>

我们希望确保要将 tweet 数据文件复制到的 HDFS 上的目录存在、为空,并且具有适当的权限。 为此,我们尝试删除目录(如果存在),然后创建它,最后应用所需的权限,如下所示:

    <delete path="${nameNode}/tmp/tweets"/>
    <mkdir path="${nameNode}/tmp/tweets"/>
    <chmod path="${nameNode}/tmp/tweets" permissions="777"/>
    </fs>

稍后我们将看到另一种设置目录的方法。 执行完节点的功能后,Oozie 需要知道如何继续工作流。 在大多数情况下,如果此节点成功,这将包括移动到另一个操作节点,否则将中止工作流。 这是由下面的元素指定的。 Ok 节点给出执行成功时要转换到的节点的名称;错误节点为失败情况命名目标节点。 下面是 OK 和 FAIL 节点的使用方式:

    <ok to="shell-node"/>
    <error to="fail"/>
    </action>
    <action name="shell-node">

第二个操作节点再次使用其特定的处理类型进行专门化;在本例中,我们有一个 shell 节点:

<shell >

然后,shell 操作将指定 Hadoop JobTracker 和 NameNode 位置。 请注意,实际值是由变量给出的;我们稍后将解释它们的来源。 JobTracker 和 NameNode 指定如下:

            <job-tracker>${jobTracker}</job-tracker>
            <name-node>${nameNode}</name-node>

正如在第 3 章Processing-MapReduce 和 Beyond中提到的,MapReduce 使用多个队列为不同的资源调度方法提供支持。 下一个元素指定工作流应该提交到的 MapReduce 队列:

             <configuration>
                <property>
                    <name>mapred.job.queue.name</name>
                    <value>${queueName}</value>
                </property>
             </configuration>

现在 shell 节点已完全配置,我们可以再次通过变量指定要调用的命令,如下所示:

              <exec>${EXEC}</exec>

Oozie 工作流的各个步骤作为 MapReduce 作业执行。 因此,此 shell 操作将作为特定 TaskTracker 上的特定任务实例执行。 因此,在执行操作之前,我们需要指定需要将哪些文件复制到 TaskTracker 计算机上的本地工作目录。 在本例中,我们需要复制主 shell 脚本、Python tweet 生成器和 Twitter 配置文件,如下所示:

<file>${workflowRoot}/${EXEC}</file>
<file>${workflowRoot}/twitter.keys</file>
<file>${workflowRoot}/stream.py</file>

关闭 shell 元素后,根据操作是否成功完成,我们再次指定要执行的操作。 由于 MapReduce 用于作业执行,因此根据定义,大多数节点类型都有内置的重试和恢复逻辑,尽管 shell 节点并非如此:

       </shell>
      <ok to="end"/>
      <error to="fail"/>
</action>

如果工作流失败,我们就在这种情况下终止它。 kill节点类型就是这样做的-终止工作流,使其无法继续执行任何进一步的步骤,通常会在此过程中记录错误消息。 下面是kill节点类型的使用方法:

<kill name="fail">
   <message>Shell action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]</message>
</kill>

另一方面,end节点只是暂停工作流,并将其记录为在 Oozie 中成功完成:

   <end name="end"/>
</workflow-app>

显而易见的问题是,前面的变量代表什么,它们从哪里获得具体的值。 前面的变量是通常称为 EL 的 Oozie 表达式语言的示例。

除了描述流中步骤的工作流定义文件(workflow.xml)之外,我们还需要创建一个配置文件,该文件为给定的工作流执行提供特定值。 这种功能和配置的分离使我们可以编写可在不同集群、不同文件位置或不同变量值上使用的工作流,而不必重新创建工作流本身。 按照惯例,此文件通常命名为job.properties。 对于前面的工作流,这里有一个示例job.properties文件。

首先,我们指定要向其提交工作流的 JobTracker、NameNode 和 MapReduce 队列的位置。 以下操作应该可以在 Cloudera 5.0 QuickStart VM 上运行,尽管在 5.1 版中,主机名已更改为quickstart.cloudera。 重要的是,指定的 NameNode 和 JobTracker 地址需要在 Oozie 白名单中-VM 上的本地服务是自动添加的:

jobTracker=localhost.localdomain:8032
nameNode=hdfs://localhost.localdomain:8020
queueName=default

接下来,我们为工作流定义和相关文件在 HDFS 文件系统上的位置设置一些值。 请注意,使用了表示运行作业的用户名的变量。 这允许将单个工作流应用于不同的路径,具体取决于提交用户,如下所示:

tasksRoot=book
workflowRoot=${nameNode}/user/${user.name}/${tasksRoot}/v1
oozie.wf.application.path=${nameNode}/user/${user.name}/${tasksRoot}/v1

接下来,我们将工作流中要执行的命令命名为${EXEC}

EXEC=gettweets.sh

更复杂的工作流将需要job.properties文件中的其他条目;前面的工作流非常简单。

oozie命令行工具需要知道 Oozie 服务器在哪里运行。 这可以作为参数添加到每个 Oozie shell 命令中,但很快就会变得很笨拙。 相反,您可以设置 shell 环境变量,如下所示:

$ export OOZIE_URL='http://localhost:11000/oozie'

完成所有这些工作之后,我们现在可以实际运行 Oozie 工作流了。 按照job.properties文件中的值中指定的方式在 HDFS 上创建一个目录。 在前面的命令中,我们将在 HDFS 上的主目录下将其创建为book/v1。 将stream.pygettweets.shtwitter.properties文件复制到该目录;这些文件是执行 shell 命令实际执行所需的文件。 然后,将workflow.xml文件添加到同一目录。

然后,要运行工作流,我们需要执行以下操作:

$ oozie job -run -config <path-to-job.properties>

如果提交成功,Oozie 会将作业名称打印到屏幕上。 您可以使用以下命令查看此工作流的当前状态:

$ oozie job -info <job-id>

您还可以检查作业的日志:

$ oozie job -log <job-id> 

此外,可以使用以下命令查看所有当前和最近的职务:

$ oozie jobs 

关于 HDFS 文件权限的说明

Shell 命令中有一个微妙的方面,可以捕捉粗心大意的人。 作为拥有fs节点的替代方案,我们可以在 shell 节点中包含一个准备元素,以便在文件系统上创建所需的目录。 它将如下所示:

<prepare>
     <mkdir path="${nameNode}/tmp/tweets"/>
</prepare>

准备阶段由提交工作流的用户执行,但由于实际脚本执行是在 Yarn 上执行的,因此通常以 Yarn 用户的身份执行。 您可能会遇到这样的问题:脚本生成 tweet,在 HDFS 上创建/tmp/tweets目录,但脚本随后无法拥有写入该目录的权限。 您可以通过更精确地分配权限来解决这个问题,或者,如前所述,您可以添加一个文件系统节点来封装所需的操作。 在本章中,我们将混合使用这两种技术;对于非 shell 节点,我们将使用 Prepare 元素,特别是当所需的目录仅由该节点操作时。 对于涉及 shell 节点的情况,或者创建的目录将跨多个节点使用的情况,我们将安全地使用更显式的fs节点。

让开发变得更容易

在开发期间管理 Oozie 作业的文件和资源有时会变得很笨拙。 有些文件需要在 HDFS 上,而有些文件需要在本地,对某些文件的更改需要对其他文件进行更改。 最简单的方法通常是开发或更改本地文件系统上的工作流目录的完整克隆,并将更改从那里推送到 HDFS 中名称相似的目录,当然,不要忘记要确保所有更改都在修订控制之下! 对于工作流的操作执行,job.properties文件是唯一需要位于本地文件系统上的文件,反之,所有其他文件都需要位于 HDFS 上。 请始终记住这一点:对工作流的本地副本进行更改、忘记将更改推送到 HDFS、然后弄不清工作流为什么没有反映这些更改太容易了。

提取数据并摄取到 Hive 中

有了个 HDFS 上的数据,我们现在可以为条 tweet 和用户提取单独的数据集,并像前几章一样放置数据。 我们可以重用extract_for_hive.pig将原始 tweet JSON 解析成单独的文件,再次将它们存储在 HDFS 上,然后执行一个配置单元步骤,该步骤将这些新文件摄取到配置单元表中,以获取 tweet、用户和地点。

要在 Oozie 中做到这一点,我们需要在工作流中添加两个新节点,第一步的 Pig 操作和第二步的 Have 操作。

对于我们的配置单元操作,我们只创建三个指向 Pig 生成的文件的外部表。 然后,这将允许我们遵循前面描述的摄取临时表或外部表的模型,并使用 HiveQLINSERT语句从临时表或外部表中插入可操作的(通常是分区的)表。 此create.hql脚本可以在https://github.com/learninghadoop2/book-examples/blob/master/ch8/v2/hive/create.hql中找到,但其形式如下所示:

CREATE DATABASE IF NOT EXISTS twttr ;
USE twttr;
DROP TABLE IF EXISTS tweets;
CREATE EXTERNAL TABLE tweets (
...
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS TEXTFILE
LOCATION '${ingestDir}/tweets';

DROP TABLE IF EXISTS user;
CREATE EXTERNAL TABLE user (
...
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS TEXTFILE
LOCATION '${ingestDir}/users';

DROP TABLE IF EXISTS place;
CREATE EXTERNAL TABLE place (
...
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\u0001'
STORED AS TEXTFILE
LOCATION '${ingestDir}/places';

请注意,每个表上的文件分隔符也被显式设置为与我们从 Pig 输出的内容相匹配。 除了之外,两个脚本中的位置都由变量指定,我们将在job.properties文件中为这些变量提供具体的值。

使用前面的语句,我们可以为在源代码中找到的工作流创建 Pig 节点,作为管道的 v2。 节点定义的大部分看起来与前面使用的 shell 节点类似,因为我们设置了相同的配置元素;还要注意我们使用prepare元素创建所需的输出目录。 我们可以为工作流创建 Pig 节点,如以下action所示:

<action name="pig-node">
   <pig>
       <job-tracker>${jobTracker}</job-tracker>
       <name-node>${nameNode}</name-node>
       <prepare>
           <delete path="${nameNode}/${outputDir}"/>
           <mkdir path="${nameNode}/${outputDir}"/>
       </prepare>
       <configuration>
           <property>
               <name>mapred.job.queue.name</name>
               <value>${queueName}</value>
           </property>
       </configuration>

与 shell 命令类似,我们需要告诉 Pig 操作实际 Pig 脚本的位置。 这在以下script元素中指定:

          <script>${workflowRoot}/pig/extract_for_hive.pig</script>

我们还需要修改用于调用 Pig 脚本的命令行,以添加个参数。 以下元素执行此操作;请注意构造模式,其中一个元素添加实际参数名称,下一个元素添加其值(我们将在下一节中看到另一种传递参数的机制):

       <argument>-param</argument>
       <argument>inputDir=${inputDir}</argument>
       <argument>-param</argument>
       <argument>outputDir=${outputDir}</argument>
  </pig>

由于我们要从此步骤移至配置单元节点,因此需要适当设置以下元素:

       <ok to="hive-node"/>
       <error to="fail"/>
   </action>

配置单元操作本身与前面的节点略有不同;尽管它以类似的方式启动,但它指定了特定于配置单元操作的命名空间,如下所示:

<action name="hive-node">
       <hive >
        <job-tracker>${jobTracker}</job-tracker>
        <name-node>${nameNode}</name-node>

配置单元操作需要配置单元本身使用的许多配置元素,并且在大多数情况下,我们将hive-site.xml文件复制到工作流目录并指定其位置,如以下 XML 所示;请注意,此机制不是配置单元特定的,也可用于自定义操作:

        <job-xml>${workflowRoot}/hive-site.xml</job-xml>

此外,我们可能需要覆盖某些 MapReduce 默认配置属性,如以下 XML 所示,其中我们指定作业应使用中间压缩:

        <configuration>
             <property>
                 <name>mapred.compress.map.output</name>
                 <value>true</value>
             </property>
        </configuration>

配置配置单元环境后,我们现在指定配置单元脚本的位置:

        <script>${workflowRoot}/hive/create.hql</script>

我们还让到提供将参数传递给配置单元脚本的机制。 但是,我们不是一次构建一个组件的命令行,而是添加将job.properties文件中的配置元素名称映射到配置单元脚本中指定的变量的param元素;Pig 操作也支持此机制:

        <param>dbName=${dbName}</param>
        <param>ingestDir=${ingestDir}</param>
   </hive>

然后,Hive 节点与其他节点一样关闭,如下所示:

     <ok to="end"/>
     <error to="fail"/>
</action>

我们现在需要将所有这些放在一起,以便在 Oozie 中运行多阶段工作流。 完整的workflow.xml文件可在https://github.com/learninghadoop2/book-examples/tree/master/ch8/v2中找到,工作流程如下图所示:

Extracting data and ingesting into Hive

数据接收工作流 v2

此工作流执行前面讨论的所有步骤;它生成 tweet 数据,通过 Pig 提取数据子集,然后将这些数据摄取到配置单元中。

关于工作流目录结构的说明

我们现在的工作流目录中有相当多的文件,最好采用一些结构和命名约定。 对于当前工作流,我们在 HDFS 上的目录如下所示:

/hive/
/hive/create.hql
/lib/
/pig/
/pig/extract_for_hive.pig
/scripts/
/scripts/gettweets.sh
/scripts/stream-json-batch.py
/scripts/twitter-keys
/hive-site.xml
/job.properties
/workflow.xml

我们遵循的模型是将配置文件保存在顶级目录中,但将与给定操作类型相关的文件保存在专用子目录中。 请注意,拥有一个lib目录是很有用的,即使它是空的,因为有些节点类型会查找它。

使用前面的结构,我们组合作业的job.properties文件现在如下所示:

jobTracker=localhost.localdomain:8032
nameNode=hdfs://localhost.localdomain:8020
queueName=default
tasksRoot=book

workflowRoot=${nameNode}/user/${user.name}/${tasksRoot}/v2
oozie.wf.application.path=${nameNode}/user/${user.name}/${tasksRoot}/v2
oozie.use.system.libpath=true
EXEC=gettweets.sh
inputDir=/tmp/tweets
outputDir=/tmp/tweetdata
ingestDir=/tmp/tweetdata
dbName=twttr

在前面的代码中,我们已经完全更新了workflow.xml定义,以包含到目前为止描述的所有步骤-包括创建所需目录的初始fs节点,而无需担心用户权限。

HCatalog 简介

如果我们看看我们当前的工作流程,我们如何使用 HDFS 作为 Pig 和 Have 之间的接口是低效的。 我们需要将 Pig 脚本的结果输出到 HDFS 上,然后配置单元脚本可以将其用作一些新表的位置。 这突出表明,将数据存储在配置单元中通常非常有用,但这是有限的,因为很少有工具(主要是配置单元)可以访问配置单元元存储,从而读取和写入此类数据。 仔细想想,Hive 有两个主要层:用于访问和操作其数据的工具,以及对该数据运行查询的执行框架。

Hive 的 HCatalog 子项目有效地提供了这些层中第一层的独立实现-访问和操作 Hive 转储中的数据的方法。 HCatalog 为其他工具(如 Pig 和 MapReduce)提供了本机读写存储在 HDFS 上的表结构数据的机制。

当然,请记住,数据以一种或另一种格式存储在 HDFS 上。 配置单元元存储提供了将这些文件抽象到配置单元熟悉的关系表结构中的模型。 因此,当我们说我们在 HCatalog 中存储数据时,我们真正的意思是我们在 HDFS 上存储数据,这样这些数据就可以通过配置单元元存储中指定的表结构公开。 相反,当我们提到配置单元数据时,我们真正指的是其元数据存储在配置单元元数据中,并且可以由任何元数据感知工具(如 HCatalog)访问的数据。

使用 HCatalog

HCatalog 命令行工具称为HCAT,它将预装在 Cloudera QuickStart VM 上-实际上,它与任何 0.11 以后的配置单元版本一起安装。

hcat实用程序没有交互模式,因此通常将其与显式命令行参数一起使用,或者将其指向命令文件,如下所示:

$ hcat –e "use default; show tables"
$ hcat –f commands.hql

尽管 HCAT 工具很有用并且可以合并到脚本中,但是对于我们这里的目的来说,HCatalog 更有趣的元素是它与 Pig 的集成。 HCatalog 定义了一个名为HCatLoader的新 Pig 加载器和一个名为HCatStorer的存储器。 顾名思义,它们允许 Pig 脚本直接读取或写入配置单元表。 我们可以使用此机制将 Oozie 工作流中以前的 Pig 和 Have 操作替换为一个基于 HCatalog 的 Pig 操作,该操作将 Pig 作业的输出直接写入到 Have 中的表中。

为清楚起见,我们将创建名为tweets_hcatplaces_hcatusers_hcat的新表,并将此数据插入其中;请注意,这些表不再是外部表:

CREATE TABLE tweets_hcat… 
CREATE TABLE places_hcat …
CREATE TABLE users_hcat …

请注意,如果脚本文件中包含这些命令,则可以使用 HCAT CLI 工具执行它们,如下所示:

$ hcat –f create.hql

然而,HCAT CLI 工具没有提供类似于配置单元 CLI 的交互式 shell。 我们现在可以使用前面的 Pig 脚本,只需要更改存储命令,用HCatStorer替换PigStorage。 因此,我们更新的 Pig 脚本extract_to_hcat.pig包括如下store命令:

store tweets_tsv into 'twttr.tweets_hcat' using org.apache.hive.hcatalog.pig.HCatStorer();

注意,HCatStorer类的包名有org.apache.hive.hcatalog前缀;当 HCatalog 在 Apache 孵化器中时,它使用org.apache.hcatalog作为它的包前缀。 这个旧的表单现在已经过时了,应该使用显式地将 HCatalog 显示为配置单元的子项目的新表单。

有了这个新的 Pig 脚本,我们现在可以使用 HCatalog 将以前的 Pig 和 Have 操作替换为更新后的 Pig 操作。 这还需要首次使用 oozie sharelib,我们将在下一节讨论这一点。 在我们的工作流定义中,此操作的pig元素的定义如以下 XML 所示,并且可以在源包中找到作为管道的 v3;在 v3 中,我们还添加了一个实用工具配置单元节点,以在 Pig 节点之前运行,以确保在执行需要它们的 Pig 脚本之前,所有必需的表都已存在。

<pig>
   <job-tracker>${jobTracker}</job-tracker>
   <name-node>${nameNode}</name-node>
   <job-xml>${workflowRoot}/hive-site.xml</job-xml>
    <configuration>
          <property>
              <name>mapred.job.queue.name</name>
              <value>${queueName}</value>
          </property>
          <property>
             <name>oozie.action.sharelib.for.pig</name>
             <value>pig,hcatalog</value>
          </property>
    </configuration>
    <script>${workflowRoot}/pig/extract_to_hcat.pig
    </script>
    <argument>-param</argument>
    <argument>inputDir=${inputDir}</argument>
</pig>

值得注意的两个更改是添加了对hive-site.xml文件的显式引用;这是 HCatalog 所必需的,以及新的配置元素,它告诉 Oozie 包含所需的HCatalogjar。

Oozie Sharelib

上一次添加的涉及到我们到目前为止还没有提到的 Oozie 的一个重要方面:Ooziesharelib。 当 Oozie 运行其所有不同的操作类型时,它需要多个 JAR 来访问 Hadoop 并调用各种工具,如 Have 和 Pig。 作为 Oozie 安装的一部分,已经在 HDFS 上放置了大量依赖 JAR,供 Oozie 及其各种操作类型使用:这就是 Ooziesharelib

对于 Oozie 的大多数用法,只要知道sharelib存在就足够了,通常在/user/oozie/share/lib on HDFS下,当需要添加一些显式配置值时(如前面的示例所示)。 当使用 Pig 操作时,Pig Jars 将被自动拾取,但是当 Pig 脚本使用诸如 HCatalog 之类的东西时,Oozie 将不会显式知道此依赖项。

Oozie CLI 允许操作sharelib,尽管需要这样做的场景超出了本书的范围。 不过,要查看 ooziesharelib中包含哪些组件,以下命令可能很有用:

$ oozie admin -shareliblist

以下命令可用于查看包含sharelib中特定组件的各个 JAR,在本例中为 HCatalog:

$ oozie admin -shareliblist hcat

这些命令可用于验证是否包含了所需的 JAR,以及查看正在使用的特定版本。

HCatalog 和分区表

如果您第二次重新运行上一个工作流,它将失败;深入查看日志,您将看到 HCatalog 抱怨它无法写入已包含数据的表。 这是 HCatalog 的当前限制;默认情况下,它将表和表中的分区视为不可变的。 另一方面,HIVE 将向表或分区添加新数据;它的默认表视图是可变的。

对配置单元和 HCatalog 即将进行的更改将看到一个新表属性的支持,该属性将在这两个工具中控制此行为;例如,添加到表定义中的以下内容将允许像现在的配置单元中支持的那样追加表格:

TBLPROPERTIES("immutable"="false")

不过,目前在配置单元和 HCatalog 的发货版本中不可用。 对于向表中添加越来越多数据的工作流来说,我们因此需要为每次新运行的工作流创建一个新分区。 我们在管道的 v4 中进行了这些更改,首先使用整数分区键重新创建表,如下所示:

CREATE  TABLE tweets_hcat (
…)
PARTITIONED BY (partition_key int)
ROW FORMAT DELIMITED
  FIELDS TERMINATED BY '\u0001'
STORED AS SEQUENCEFILE;

CREATE  TABLE `places_hcat`(
… )
partitioned by(partition_key int)
ROW FORMAT DELIMITED
  FIELDS TERMINATED BY '\u0001'
STORED AS SEQUENCEFILE
TBLPROPERTIES("immutable"="false") ;

CREATE  TABLE `users_hcat`(
…)
partitioned by(partition_key int)
ROW FORMAT DELIMITED
  FIELDS TERMINATED BY '\u0001'
STORED AS SEQUENCEFILE
TBLPROPERTIES("immutable"="false") ;

PigHCatStorer采用可选的分区定义,我们相应地修改了 Pig 脚本中的store语句;例如:

store tweets_tsv into 'twttr.tweets_hcat' 
using org.apache.hive.hcatalog.pig.HCatStorer(
'partition_key=$partitionKey');

然后,我们修改workflow.xml文件中的 Pig 操作,以包括此附加参数:

<script>${workflowRoot}/pig/extract_to_hcat.pig</script>
          <param>inputDir=${inputDir}</param>
          <param>partitionKey=${partitionKey}</param>

那么问题就是我们如何将此分区键传递给工作流。 我们可以在job.properties文件中指定它,但这样做会遇到在下一次重新运行时尝试写入现有分区的相同问题。

HCatalog and partitioned tables

摄取工作流 v4

目前,我们将把它作为显式参数传递给 Oozie CLI 的调用,稍后再探索更好的方法:

$ oozie job –run –config v4/job.properties –DpartitionKey=12345

备注

请注意,此行为的后果是使用相同参数重新运行 HCAT 工作流将失败。 在测试工作流或使用本书中的示例代码时,请注意这一点。

生成派生数据

既然我们已经建立了主数据管道,那么在添加每个新的个附加数据集之后,我们很可能希望采取一系列操作。 作为一个简单的例子,请注意,使用我们前面的机制将每组用户数据添加到单独的分区,users_hcat表将多次包含用户。 让我们为唯一用户创建一个新表,并在每次添加新用户数据时重新生成该表。

请注意,考虑到前面提到的 HCatalog 的限制,我们将使用一个配置单元操作来实现此目的,因为我们需要替换表中的数据。

首先,我们将为唯一用户信息创建一个新表,如下所示:

CREATE TABLE IF NOT EXISTS `unique_users`(
  `user_id` string ,
  `name` string ,
  `description` string ,
  `screen_name` string )
ROW FORMAT DELIMITED
  FIELDS TERMINATED BY '\t'
STORED AS sequencefile ;

在该表中,我们将只存储从不更改(ID)或很少更改(屏幕名称等)的用户属性。 然后,我们可以编写一条简单的配置单元语句,从完整的users_hcat表填充此表:

USE twttr;
INSERT OVERWRITE TABLE unique_users
SELECT DISTINCT user_id, name, description, screen_name
FROM users_hcat;

然后,我们可以在工作流中的上一个 Pig 节点之后添加一个附加的配置单元操作节点。 在执行此操作时,我们发现简单地给节点命名(如 hive-node)的模式不是一个好主意,因为我们现在有两个基于配置单元的节点。 在工作流的 v5 中,我们添加了此新节点,并将节点更改为更具描述性的名称:

Producing derived data

摄取工作流 v5

并行执行多个动作

我们的工作流有两种类型的活动:初始化文件系统和配置单元表的节点的初始设置,以及执行实际处理的功能节点。 如果我们看一下我们一直在使用的两个设置节点,很明显它们是完全不同的,并且不是相互依赖的。 因此,我们可以利用称为forkjoin节点的 Oozie 特性来并行执行这些操作。 现在,我们的workflow.xml文件的开头变为:

 <start to="setup-fork-node"/>

Ooziefork节点包含许多path元素,每个元素指定一个起始节点。 其中每一项都将并行推出:

<fork name="setup-fork-node">
   <path start="setup-filesystem-node" />
   <path start="create-tables-node" />
</fork>

指定的每个操作节点与我们以前使用的任何操作节点都没有什么不同。 动作节点可以链接到一系列其他节点;唯一的要求是每个并行的动作系列必须转换到与fork节点关联的join节点,如下所示:

    <action name="setup-filesystem-node">
…
        <ok to="setup-join-node"/>
        <error to="fail"/>
    </action>
    <action name="create-tables-node">
…
        <ok to="setup-join-node"/>
        <error to="fail"/>
    </action>

join节点本身充当协调点;任何已完成的工作流都将等待,直到fork节点中指定的所有路径都到达该点。 此时,工作流在join节点内指定的节点处继续。 下面是join节点的使用方法:

<join name="create-join-node" to="gettweets-node"/>

在前面的代码中,出于空间目的我们省略了操作定义,但完整的工作流定义在 V6 中:

Performing multiple actions in parallel

摄取工作流 v6

调用子工作流

尽管fork/join机制使并行操作的过程更加高效,但如果我们在主workflow.xml定义中包含它,它仍然会增加大量的冗长。 从概念上讲,我们有一系列操作来执行工作流所需的相关任务,但不一定是工作流的一部分。 对于这种情况和类似情况,Oozie 提供了调用子工作流的能力。 父工作流将执行子工作流并等待其完成,并能够将配置元素从一个工作流传递到另一个工作流。

子工作流本身将是一个完整的工作流,通常存储在 HDFS 上的一个目录中,该目录具有我们期望的工作流、主workflow.xml文件以及任何所需的配置单元、PIG 或类似文件的所有常见结构。

我们可以在 HDFS 上创建一个名为 Setup-Workflow 的新目录,并在其中创建文件系统和配置单元创建操作所需的文件。 子工作流配置文件如下所示:

<workflow-app  name="create-workflow">
    <start to="setup-fork-node"/>
    <fork name="setup-fork-node">
          <path start="setup-filesystem-node" />
      <path start="create-tables-node" />
    </fork>
    <action name="setup-filesystem-node">
    …
    </action>
    <action name="create-tables-node">
    …
    </action>
    <join name="create-join-node" to="end"/>
    <kill name="fail">
        <message>Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]</message>
    </kill>
    <end name="end"/>
</workflow-app>

定义了此子工作流后,我们将修改主工作流的第一个节点以使用子工作流节点,如下所示:

    <start to="create-subworkflow-node"/>
    <action name="create-subworkflow-node">
        <sub-workflow>
            <app-path>${subWorkflowRoot}</app-path>
            <propagate-configuration/>
        </sub-workflow>
        <ok to="gettweets-node"/>
        <error to="fail"/>
    </action>

我们将在父工作流的job.properties中指定subWorkflowPathpropagate-configuration元素将父工作流的配置传递给子工作流。

添加全局设置

通过将实用程序节点提取到子工作流中,我们可以显著减少主工作流定义中的混乱和复杂性。 在我们的接收管道的 v7 中,我们将进行一个额外的简化,并添加一个全局配置部分,如下所示:

<workflow-app  name="v7">
    <global>
            <job-tracker>${jobTracker}</job-tracker>
            <name-node>${nameNode}</name-node>
            <job-xml>${workflowRoot}/hive-site.xml</job-xml>
            <configuration>
                <property>
                    <name>mapred.job.queue.name</name>
                    <value>${queueName}</value>
                </property>
            </configuration>
</global>
<start to="create-subworkflow-node"/>

通过添加此全局配置节,我们无需在剩余工作流中的配置单元和 PIG 节点中指定任何这些值(请注意,当前外壳节点不支持全局配置机制)。 这可以极大地简化我们的一些节点;例如,我们的 Pig 节点现在如下所示:

<action name="hcat-ingest-node">
   <pig>
     <configuration>
       <property>
         <name>oozie.action.sharelib.for.pig</name>
         <value>pig,hcatalog</value>
         </property>
       </configuration>
       <script>${workflowRoot}/pig/extract_to_hcat.pig</script>
          <param>inputDir=${inputDir}</param>
          <param>dbName=${dbName}</param>
          <param>partitionKey=${partitionKey}</param>
   </pig>
   <ok to="derived-data-node"/>
   <error to="fail"/>
</action>

可以看到,我们可以添加额外的配置元素,或者实际上覆盖全局部分中指定的配置元素,从而产生更加清晰的操作定义,只关注特定于相关操作的信息。 我们的 Workflow v7 既添加了全局部分,又添加子工作流,这显著提高了工作流的可读性:

Adding global settings

摄取工作流 v7

外部数据的挑战

当我们依赖外部数据来驱动我们的应用时,我们隐含地依赖于该数据的质量和稳定性。 当然,对于任何数据都是如此,但是当数据是由我们无法控制的外部源生成时,风险很可能更高。 无论如何,当我们在这些数据馈送的基础上构建我们期望的可靠应用时,特别是当我们的数据量增长时,我们需要考虑如何降低这些风险。

数据验证

我们使用通用术语数据验证来指代确保传入数据符合我们的期望的行为,并潜在地应用标准化来相应地修改它,甚至删除格式错误或损坏的输入。 这实际上涉及的将是非常特定的应用。 在某些情况下,重要的是确保系统只接收符合给定的准确或干净定义的数据。 对于我们的 tweet 数据,我们不关心每一条记录,可以很容易地采用一种策略,比如删除在我们关心的特定字段中没有值的记录。 但是,对于其他应用,必须捕获每条输入记录,这可能会驱动逻辑的实现,以重新格式化每条记录,以确保其符合要求。 在其他情况下,只有正确的记录会被摄取,但其余的记录可能不会被丢弃,而是会存储在其他地方供以后分析。

底线是,试图定义一种通用的数据验证方法远远超出了本章的范围。

但是,我们可以提供一些想法,说明在管道中的什么位置合并各种类型的验证逻辑。

验证操作

执行任何必要的验证或清理的逻辑可以直接合并到其他操作中。 运行脚本以收集数据的外壳节点可以添加命令,以不同的方式处理格式错误的记录。 将数据加载到表中的 PIG 和 HIVE 操作可以对摄取执行筛选(在 Pig 中更容易完成),或者在将数据从摄取表复制到操作存储区时添加警告。

但是,有一种观点认为应该在工作流中添加一个验证节点,即使它最初并不执行实际的逻辑。 例如,这可能是一个 Pig 操作,它读取数据,应用验证,并将验证后的数据写入新位置以供后续节点读取。 这样做的好处是,我们可以在以后更新验证逻辑,而无需更改其他操作,这应该会降低意外破坏管道其余部分的风险,并使节点在职责方面的定义更加清晰。 这一思路的自然延伸是,新的验证子工作流很可能也是一个很好的模型,因为它不仅提供职责分离,而且使验证逻辑更易于测试和更新。

这种方法的明显缺点是,它增加了额外的处理和另一个读取数据并重新写入数据的周期。 当然,这直接违背了我们在考虑使用来自 Pig 的 HCatalog 时强调的优势之一。

最后,这将归结为在性能与工作流复杂性和可维护性之间的权衡。 在考虑如何执行验证以及这对您的工作流意味着什么时,请在决定实现之前考虑所有这些要素。

处理格式更改

我们不能仅仅因为我们有数据流入我们的系统并确信数据得到了充分的验证就宣布的胜利。 特别是当数据来自外部来源时,我们必须考虑数据的结构可能会随着时间的推移而发生变化。

请记住,像配置单元这样的系统仅在读取数据时才应用表架构。 这对于实现灵活的数据存储和获取是一个巨大的好处,但当获取的数据与针对其执行的查询不再匹配时,可能会导致面向用户的查询或工作负载突然失败。 在写入时应用模式的关系数据库甚至不允许将此类数据摄取到系统中。

处理对数据格式所做更改的明显方法是将现有数据重新处理为新格式。 虽然这在较小的数据集上是容易处理的,但在大型 Hadoop 集群中看到的那种卷上,它很快就变得不可行了。

使用 avro 处理模式演变

Avro 在与 Hive 的集成方面有一些功能,可以帮助我们解决这个问题。 如果我们将表作为 tweet 数据,我们可以用以下 avro 模式表示 tweet 记录的结构:

{
 "namespace": "com.learninghadoop2.avrotables",
 "type":"record",
 "name":"tweets_avro",
 "fields":[
   {"name": "created_at", "type": ["null" ,"string"]},
   {"name": "tweet_id_str", "type": ["null","string"]},
   {"name": "text","type":["null","string"]},
   {"name": "in_reply_to", "type": ["null","string"]},
   {"name": "is_retweeted", "type": ["null","string"]},
   {"name": "user_id", "type": ["null","string"]},
  {"name": "place_id", "type": ["null","string"]}
  ]
}

在名为tweets_avro.avsc的文件中创建前面的模式-这是 Avro 模式的标准文件扩展名。 然后,将其放在 HDFS 上;我们希望有一个公共位置来存放模式文件,比如/schema/avro

有了这个定义,我们现在可以创建一个配置单元表格,该表格使用此模式作为其表格规范,如下所示:

CREATE TABLE tweets_avro
PARTITIONED BY ( `partition_key` int)
ROW FORMAT SERDE
  'org.apache.hadoop.hive.serde2.avro.AvroSerDe'
WITH SERDEPROPERTIES (
'avro.schema.url'='hdfs://localhost.localdomain:8020/schema/avro/tweets_avro.avsc'
)
STORED AS INPUTFORMAT
  'org.apache.hadoop.hive.ql.io.avro.AvroContainerInputFormat'
OUTPUTFORMAT
  'org.apache.hadoop.hive.ql.io.avro.AvroContainerOutputFormat';

然后,从配置单元(或也支持此类定义的 HCatalog)中查看表定义:

describe tweets_avro
OK
created_at              string                  from deserializer
tweet_id_str            string                  from deserializer
text                    string                  from deserializer
in_reply_to             string                  from deserializer
is_retweeted            string                  from deserializer
user_id                 string                  from deserializer
place_id                string                  from deserializer
partition_key           int                   None

我们还可以像使用其他表一样使用该表,例如,将分区 3 中的数据从非 avro 表复制到 avro 表,如下所示:

SET hive.exec.dynamic.partition.mode=nonstrict
INSERT INTO TABLE tweets_avro
PARTITION (partition_key)
SELECT  FROM tweets_hcat

备注

与前面的示例一样,如果类路径中不存在 avro 依赖项,我们需要将 avro MapReduce JAR 添加到我们的环境中,然后才能从表中进行选择。

我们现在有了一个由 avro 模式指定的新 twets 表;到目前为止,它看起来和其他表一样。 但是,对于我们在本章中的目的而言,真正的好处在于我们可以如何使用 Avro 机制来处理模式演变。 让我们向表架构添加一个新字段,如下所示:

{
 "namespace": "com.learninghadoop2.avrotables",
 "type":"record",
 "name":"tweets_avro",
 "fields":[
   {"name": "created_at", "type": ["null" ,"string"]},
   {"name": "tweet_id_str", "type": ["null","string"]},
   {"name": "text","type":["null","string"]},
   {"name": "in_reply_to", "type": ["null","string"]},
   {"name": "is_retweeted", "type": ["null","string"]},
   {"name": "user_id", "type": ["null","string"]},
  {"name": "place_id", "type": ["null","string"]},
  {"name": "new_feature", "type": "string", "default": "wow!"}
  ]
}

有了这个新的模式,我们就可以验证表定义是否也已更新,如下所示:

describe tweets_avro;
OK
created_at              string                  from deserializer
tweet_id_str            string                  from deserializer
text                    string                  from deserializer
in_reply_to             string                  from deserializer
is_retweeted            string                  from deserializer
user_id                 string                  from deserializer
place_id                string                  from deserializer
new_feature             string                  from deserializer
partition_key           int                     None

在不添加任何新数据的情况下,我们可以在将返回现有数据默认值的新字段上运行查询,如下所示:

SELECT new_feature FROM tweets_avro LIMIT 5;
...
OK
wow!
wow!
wow!
wow!
wow!

更令人印象深刻的是,新列不需要添加到末尾;它可以在记录中的任何位置。 使用此机制,我们现在可以更新 Avro 模式以表示新的数据结构,并看到这些更改自动反映在配置单元表定义中。 任何引用新列的查询都将检索不存在该字段的所有现有数据的默认值。

请注意,我们在这里使用的默认机制是 Avro 的核心,并不特定于配置单元。 Avro 是一种非常强大和灵活的格式,在许多领域都有应用,绝对值得我们在这里进行更深入的研究。

从技术上讲,这为我们提供了向前兼容性。 我们可以对表模式进行更改,并使所有现有数据自动保持与新结构的兼容。但是,我们不能继续将旧格式的数据吸收到更新表中,因为该机制不提供向后兼容性:

INSERT INTO TABLE tweets_avro 
PARTITION (partition_key)
SELECT * FROM tweets_hcat;
FAILED: SemanticException [Error 10044]: Line 1:18 Cannot insert into target table because column number/types are different 'tweets_avro': Table insclause-0 has 8 columns, but query has 7 columns.

通过 Avro 支持模式演变,可以将数据更改作为正常业务的一部分来处理,而不是经常变成消防紧急情况。 但很明显,这不是免费的;仍然需要在管道中做出改变,并将这些改变投入生产。 但是,拥有提供前向兼容性的配置单元表确实允许以更易于管理的步骤执行该过程;否则,您将需要在管道的每个阶段同步更改。 如果更改是从接收到插入到 Avro 支持的配置单元表中,那么这些表的所有用户都可以保持不变(只要他们不做像select *这样的事情,这无论如何都是一个糟糕的主意),并继续对新数据运行现有查询。 然后,可以根据摄取机制的不同时间表更改这些应用。 在摄取管道的 V8 中,我们展示了如何充分使用 avro 表来实现所有现有功能。

备注

请注意,撰写本文时尚未发布的配置单元 0.14 可能会包含更多对 Avro 的内置支持,这可能会进一步简化模式演变的过程。 如果阅读本文时配置单元 0.14 可用,请务必查看最终实现。

关于使用 Avro 模式进化的最后思考

通过对 Avro 的讨论,我们已经触及了更广泛主题的某些方面,特别是更大范围的数据管理以及围绕数据版本控制和保留的策略。 这一领域的大部分内容都变得非常特定于一个组织,但这里有一些我们认为更广泛适用的临别想法。

仅进行附加更改

我们在前面的示例中讨论了添加列。 有时(尽管更少见),源数据会删除列,或者您发现不再需要新列。 Avro 并没有真正提供工具来帮助实现这一点,我们觉得这通常是不受欢迎的。 我们倾向于维护旧数据,而不是删除旧列,而不是在所有新数据中使用空列。 如果您控制数据格式,这将更容易管理;如果您正在接收外部源,则要遵循此方法,您将需要重新处理数据以删除旧列,或者更改接收机制以为所有新数据添加默认值。

显式管理架构版本

在前面的示例中,我们有一个模式文件,我们直接对其进行了更改。 这可能是一个非常糟糕的想法,因为它使我们无法跟踪随时间推移的模式更改。 除了将模式视为受版本控制的构件(您的模式也在 Git 中,不是吗?)。 使用显式版本标记每个模式通常很有用。 当传入数据也显式版本化时,这特别有用。 然后,您可以添加新文件并使用ALTER TABLE语句将配置单元表定义指向新架构,而不是覆盖现有架构文件。 当然,我们在这里假设您不能选择对具有不同格式的旧数据使用不同的查询。 尽管配置单元没有选择模式的自动机制,但在某些情况下,您可能可以手动进行控制,从而避开进化问题。

考虑模式分发

当使用模式文件时,请考虑如何将其分发给客户端。 如果像前面的示例一样,文件位于 HDFS 上,那么给它一个较高的复制因子可能是有意义的。 该文件将由查询表的每个 MapReduce 作业中的每个映射器检索。

还可以将 avro URL 指定为本地文件系统位置(file://),这对开发很有用,也可以作为 Web 资源(http://)。 尽管后者非常有用,因为它是一种将模式分发到非 Hadoop 客户端的便捷机制,但请记住,Web 服务器上的负载可能很高。 使用现代硬件和高效的 Web 服务器,这很可能不是一个大问题,但如果您有一个由数千台机器组成的集群,运行许多并行作业,其中每个映射器都需要访问 Web 服务器,那么请小心。

收集其他数据

许多数据处理系统没有单一的数据接收来源;通常,一个主要来源由其他次要来源丰富。 现在,我们将了解如何将此类参考数据的检索合并到我们的数据仓库中。

从高层次上讲,这个问题与我们检索原始 tweet 数据没有太大区别,因为我们希望从外部来源提取数据,可能对其进行一些处理,并将其存储在以后可以使用的地方。 但这确实突出了我们需要考虑的一个方面:我们真的想在每次接收新 tweet 时检索这些数据吗? 答案当然是否定的。 参考数据很少更改,我们可以轻松地获取它,而不是像新的 tweet 数据那样频繁。 这提出了一个我们到目前为止一直回避的问题:我们如何安排 Oozie 工作流?

安排工作流

到目前为止,我们已经从 CLI 按需运行所有 Oozie 工作流。 Oozie 还有一个调度程序,它可以定时启动作业,也可以在满足外部标准(如 HDFS 中出现的数据)时启动作业。 我们的工作流程很适合让我们的主推文管道运行,比如说,每 10 分钟运行一次,但参考数据每天只刷新一次。

提示

无论何时检索数据,都要仔细考虑如何处理执行删除/替换操作的数据集。 特别是,在检索和验证新数据之前不要执行删除操作;否则,在下一次检索成功之前,任何需要引用数据的作业都将失败。 将破坏性操作包括在仅在成功完成检索步骤后才触发的子工作流中可能是一个很好的选择。

Oozie 实际上定义了它可以运行的两种类型的应用:我们到目前为止已经使用的工作流和协调器,它们根据各种标准调度要执行的工作流。 协调器作业在概念上类似于我们的其他工作流;我们将 XML 配置文件推送到 HDFS 上,并在运行时使用参数化的属性文件对其进行配置。 此外,协调器作业具有从触发其执行的事件接收附加参数化的功能。

用一个例子来描述这可能是最好的。 比方说,我们希望像前面提到的那样创建一个协调器,该协调器每 10 分钟执行我们摄取工作流的 v7。 下面是coordinator.xml文件(协调器 XML 定义的标准名称):

<coordinator-app name="tweets-10min-coordinator"  frequency="${freq}" start="${startTime}" end="${endTime}"  timezone="UTC" >

协调器中的主要操作节点是工作流,我们需要为其指定其在 HDFS 上的根位置和所有必需的属性,如下所示:

    <action>
        <workflow>
           <app-path>${workflowPath}</app-path>
                <configuration>
                     <property>
                        <name>workflowRoot</name>
                        <value>${workflowRoot}</value>
                    </property>
…

我们还需要包括工作流中的任何操作或其触发的任何子工作流所需的任何属性;实际上,这意味着需要在此处包括要触发的任何工作流中存在的任何用户定义变量,如下所示:

                    <property>
                        <name>dbName</name>
                        <value>${dbName}</value>
                   </property>
                   <property>
                        <name>partitionKey</name><value>${coord:formatTime(coord:nominalTime(), 'yyyyMMddhhmm')}
                        </value>
                   </property>
                   <property>
                        <name>exec</name>
                        <value>gettweets.sh</value>
                   </property>
                   <property>
                        <name>inputDir</name>
                        <value>/tmp/tweets</value>
                   </property>
                   <property>
                        <name>subWorkflowRoot</name>
                        <value>${subWorkflowRoot}</value>
                   </property>
             </configuration>
          </workflow>
      </action>
</coordinator-app>

我们在前面的 XML 中使用了一些特定于协调器的特性。 请注意协调器开始和结束时间的详细说明以及频率(以分钟为单位)。 我们在这里使用最简单的形式;Oozie 也有一组函数来允许相当丰富的频率规格。

我们在定义partitionKey变量时使用了协调器 EL 函数。 早些时候,在从 CLI 运行工作流时,我们明确指定了这些,但提到了还有一种更好的方法-就是这样。 以下表达式生成包含年、月、日、小时和分钟的格式化输出:

${coord:formatTime(coord:nominalTime(), 'yyyyMMddhhmm')}

如果我们随后使用它作为分区键的值,我们可以确保每次工作流调用都能在我们的HCatalog表中正确地创建一个唯一的分区。

协调器作业的相应job.properties看起来与我们前面的配置文件非常相似,其中包含 NameNode 和类似变量的常用条目,以及特定于应用的变量的值,如dbName。 此外,我们还需要指定 HDFS 上协调器位置的根目录,如下所示:

oozie.coord.application.path=${nameNode}/user/${user.name}/${tasksRoot}/tweets_10min

请注意oozie.coord名称空间前缀,而不是之前使用的oozie.wf。 有了 HDFS 上的协调器定义,我们就可以将文件提交给 Oozie,就像之前的作业一样。 但在本例中,作业将仅在给定的时间段内运行。 具体地说,当系统时钟在startTimeendTime之间时,它将每五分钟运行一次(频率是可变的)。

我们已经在本章的源代码中包含了tweets_10min目录中的完整配置。

其他 Oozie 触发器

前面的协调器有一个非常简单的触发器;它在指定的时间范围内定期启动。 Oozie 还有一个称为 DataSets 的附加功能,它可以由新数据的可用性触发。

这并不是非常适合我们到目前为止定义我们的管道的方式,但是想象一下,我们的工作流不是将收集 tweet 作为第一步,而是一个外部系统在不断地将新的 tweet 文件推送到 HDFS 上。 可以将 Oozie 配置为根据目录模式查找新数据的存在,或者专门在 HDFS 上出现就绪文件时触发。 后一种配置提供了一种非常方便的机制来集成 MapReduce 作业的输出,默认情况下,MapReduce 作业会将_SUCCESS文件写入其输出目录。

Oozie 数据集可以说是整个系统中最强大的部分之一,由于空间原因,我们在这里不能公正地对待它们。 但我们强烈建议您参考 Oozie 主页以获取更多信息。

齐心协力

让我们回顾一下我们到目前为止已经讨论过的内容,以及我们如何使用 Oozie 构建一系列复杂的工作流,这些工作流通过组合所有讨论的技术来实现数据生命周期管理的方法。

首先,重要的是定义明确的职责,并使用良好的设计和关注点分离原则实现系统的各个部分。 通过应用这一点,我们最终得到了几个不同的工作流:

  • 确保环境(主要是 HDFS 和配置单元元数据)正确配置的子工作流
  • 用于执行数据验证的子工作流
  • 触发前两个子工作流,然后通过多步骤接收管道提取新数据的主工作流
  • 每 10 分钟执行上述工作流的协调员
  • 第二协调器,其摄取将对应用流水线有用的参考数据

我们还使用 Avro 模式定义所有表,并在任何可能的情况下使用它们来帮助管理模式演变和随时间变化的数据格式。

在本章的源代码中,我们将在工作流的最终版本中提供这些组件的完整源代码。

提供帮助的其他工具

尽管 Oozie 是一个非常强大的工具,但有时要正确编写工作流定义文件可能有些困难。 随着管道变得越来越庞大,管理复杂性成为一项挑战,即使将其良好的功能划分为多个工作流也是如此。 在更简单的层面上,XML 对于人类来说从来就不是一件有趣的事情! 有一些工具可以提供帮助。 自称为 Hadoop UI(http://gethue.com/)的工具 Hue 提供了一些图形工具来帮助编写、执行和管理 Oozie 工作流。 虽然功能强大,但 Hue 不是一个初学者工具;我们将在第 11 章下一步去哪里中更多地提到它。

一个名为 Falcon(http://falcon.incubator.apache.org)的新 apache 项目可能也会令人感兴趣。 Falcon 使用 Oozie 构建一系列更高级别的数据流和操作。 例如,Falcon 提供了实现和确保跨多个 Hadoop 集群进行跨站点复制的配方。 猎鹰团队正在开发更好的界面来构建他们的工作流程,所以这个项目可能很值得一看。

摘要

希望本章将数据生命周期管理的主题作为一个枯燥的抽象概念进行介绍。 我们讲了很多内容,特别是:

  • 数据生命周期管理的定义,以及它如何涵盖通常在大数据量中变得重要的许多问题和技术
  • 按照良好的数据生命周期管理原则构建数据接收管道的概念,然后可供更高级别的分析工具使用
  • Oozie 作为一个专注于 Hadoop 的工作流管理器,以及我们如何使用它将一系列操作组合到一个统一的工作流中
  • 各种 Oozie 工具,如子工作流、并行操作执行和全局变量,使我们能够将真正的设计原则应用到我们的工作流中
  • HCatalog 以及它如何为配置单元以外的工具提供读写表结构数据的方法;我们展示了它的巨大前景以及与 Pig 等工具的集成,但也强调了当前的一些弱点
  • Avro 是我们选择的处理模式随时间演变的工具
  • 使用 Oozie 协调器基于时间间隔或数据可用性构建计划的工作流,以推动多个摄取管道的执行
  • 其他一些工具可以使这些任务变得更容易,即色调和猎鹰

在下一章中,我们将介绍几种高级分析工具和框架,它们可以在接收管道中收集的数据基础上构建复杂的应用逻辑。

九、让开发变得更容易

在本章中,我们将介绍如何根据用例和最终目标,使用构建在 JavaAPI 之上的大量抽象和框架来简化 Hadoop 中的应用开发。 我们将特别了解以下主题:

  • 流 API 如何允许我们使用 Python 和 Ruby 等动态语言编写 MapReduce 作业
  • Apache Crunch 和 Kite Morphline 等框架如何允许我们使用更高级别的抽象来表示数据转换管道
  • Kite Data 是 Cloudera 开发的一个很有前途的框架,它如何为我们提供了应用设计模式和样板来简化 Hadoop 生态系统中不同组件的集成和互操作性的能力

选择框架

在前面的章中,我们了解了用于编写分布式应用的 MapReduce 和 Spark 编程 API。 虽然这些 API 非常强大和灵活,但它们具有一定程度的复杂性,可能需要大量的开发时间。

为了减少冗长,我们引入了 Pig 和 Have 框架,它们将特定于领域的语言 Pig 拉丁语和 Have QL 编译到许多 MapReduce 作业或 Spark DAG 中,有效地将 API 抽象出来。 这两种语言都可以使用 UDF 进行扩展,UDF 是将复杂逻辑映射到 Pig 和 Have 数据模型的一种方式。

当我们需要一定程度的灵活性和模块性时,事情可能会变得棘手。 根据用例和开发人员需求,Hadoop 生态系统提供了大量的 API、框架和库选择。 在本章中,我们将识别四类用户,并将其与以下相关工具进行匹配:

  • 希望避免使用 Java 而倾向于使用动态语言编写 MapReduce 作业脚本的开发人员,或者使用未在 JVM 上实现的语言的开发人员。 典型的用例是前期分析和快速原型制作:Hadoop Streaming
  • Java 开发人员,需要集成 Hadoop 生态系统的组件,并且可以受益于代码化的设计模式和样板:Kite Data
  • 希望使用熟悉的 API 编写模块化数据管道的 Java 开发人员:Apache Crunch
  • 更愿意配置数据转换链的开发人员。 例如,一个想要在 ETL 管道中嵌入现有代码的数据工程师:Kite Morphines

Hadoop 流

我们在前面已经提到过,MapReduce 程序不必用 Java 编写。 您可能想要或需要用另一种语言编写地图和减少任务,原因有几个。 也许您有现有的代码可以利用,或者需要使用第三方二进制文件-原因是多种多样且合理的。

Hadoop 提供了许多机制来帮助非 Java 开发,其中最主要的是 Hadoop 管道(提供本地 C++接口)和 Hadoop 流(允许任何使用标准输入和输出的程序用于映射和缩减任务)。 使用 MapReduce Java API,map 和 Reduce 任务都为包含任务功能的方法提供实现。 这些方法将输入作为方法参数接收到任务,然后通过Context对象输出结果。 这是一个清晰且类型安全的接口,但根据定义它是特定于 Java 的。

Hadoop 流媒体采用了一种不同的方法。 使用流,您可以编写一个映射任务,该任务从标准输入读取其输入,一次一行,并将其结果的输出提供给标准输出。 然后,Reduce 任务也执行同样的操作,同样只对其数据流使用标准输入和输出。

从标准输入和输出读取和写入的任何程序都可以在流中使用,比如编译的二进制文件、Unix shell 脚本或用动态语言(如 Python 或 Ruby)编写的程序。 流媒体的最大优势是,它可以让你尝试想法,并比使用 Java 更快地迭代它们。 您只需编写脚本并将它们作为参数传递到流 JAR 文件,而不是编译/JAR/提交循环。 特别是在对新数据集进行初步分析或尝试新想法时,这可以显著加快开发速度。

关于动态语言和静态语言的经典争论平衡了快速开发与运行时性能和类型检查的好处。 使用流式传输时,这些动态缺点也适用于。 因此,我们倾向于使用流式处理进行前期分析,使用 Java 实现将在生产集群上执行的作业。

Python 中的流式字数统计

我们将通过使用 Python 重新实现我们熟悉的字数统计示例来演示 Hadoop 流。 首先,我们创建一个脚本作为我们的映射器。 它使用for循环使用标准输入中的 UTF-8 编码文本行,将其拆分成单词,并使用print函数将每个单词写入标准输出,如下所示:

#!/bin/env python
import sys

for line in sys.stdin:
    # skip empty lines
    if line == '\n':
        continue

    # preserve utf-8 encoding
    try:
        line = line.encode('utf-8')
    except UnicodeDecodeError:
        continue
    # newline characters can appear within the text
    line = line.replace('\n', '')

    # lowercase and tokenize
    line = line.lower().split()

    for term in line:
        if not term:
          continue
        try:
            print(
                u"%s" % (
                    term.decode('utf-8')))
        except UnicodeEncodeError:
            continue

减法器统计标准输入中每个单词的出现次数,并将输出作为最终值提供给标准输出,如下所示:

#!/bin/env python
import sys

count = 1
current = None

for word in sys.stdin:
    word = word.strip()

    if word == current:
        count += 1
    else:
        if current:
            print "%s\t%s" % (current.decode('utf-8'), count)
        current = word
        count = 1
if current == word:
    print "%s\t%s" % (current.decode('utf-8'), count)

备注

在这两种情况下,我们都隐式使用前面章节中讨论的 Hadoop 输入和输出格式。 它是处理源文件的TextInputFormat,并将每一行一次提供给映射脚本。 相反,TextOutputFormat将确保 Reduce 任务的输出也被正确地写为文本。

map.pyreduce.py复制到 HDFS,然后使用前几章中的样本数据将脚本作为流作业执行,如下所示:

$ hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/hadoop-streaming.jar \
-file map.py \
-mapper "python map.py" \
-file reduce.py \
-reducer "python reduce.py" \
-input sample.txt \
-output output.txt 

备注

推文采用UTF-8编码。 确保相应地设置了PYTHONIOENCODING,以便在 UNIX 终端中通过管道传输数据:

$ export PYTHONIOENCODING='UTF-8'

可以从命令行提示符执行相同的代码:

$ cat sample.txt | python map.py| python reduce.py > out.txt

映射器和减速器代码可在https://github.com/learninghadoop2/book-examples/blob/master/ch9/streaming/wc/python/map.py中找到。

使用流式处理时作业的差异

在 Java 中,我们知道我们的map()方法将为每个输入键/值对调用一次,而我们的reduce()方法将为每个键及其值集调用。

对于流,我们不再有 map 或 Reduce 方法的概念;相反,我们编写了处理接收数据流的脚本。 这改变了我们编写减速器的方式。 在 Java 中,每个键的值分组是由 Hadoop 执行的;每次调用 Reduce 方法都会收到一个用制表符分隔的键及其所有值。 在流式处理中,Reduce 任务的每个实例每次都被赋予一个单独的未收集的值。

Hadoop 流确实会对键进行排序,例如,如果映射器发出以下数据:

First 1
Word 1
Word 1
A 1
First 1

流减少器将按以下顺序接收它:

A 1
First 1
First 1
Word 1
Word 1

Hadoop 仍然收集每个键的值,并确保每个键只传递给一个减法器。 换句话说,Reducer 获取多个键的所有值,并将它们分组在一起;但是,它们不会打包到 Reducer 的单独执行中,也就是每个键一个值,这与 Java API 不同。 由于 Hadoop 流使用stdinstdout通道在任务之间交换数据,因此调试和错误消息不应打印到标准输出。 在下面的示例中,我们将使用 Pythonlogging(https://docs.python.org/2/library/logging.html)包将警告语句记录到一个文件中。

查找文本中的重要单词

我们现在将实现一个度量Term Frequency-Inverse Document Frequency(TF-IDF),它将帮助我们根据单词在一组文档(在我们的例子中是 tweet)中出现的频率来确定它们的重要性。

直观地说,如果一个词经常出现在文档中,它就很重要,应该给它一个高分。 然而,如果一个词出现在许多文档中,我们应该用较低的分数来惩罚它,因为它是一个常见的词,而且它的出现频率并不是本文所独有的。

因此,许多文档中出现的常见单词(如The和表示的)将被缩小。 在一条推文中频繁出现的词语将被放大。 TF-IDF 的使用通常与其他度量和技术结合使用,包括停用词删除和文本分类。 请注意,此技术在处理较短的文档(如 tweet)时会有缺点。 在这种情况下,术语频率分量将趋向于变为 1。 相反,人们可以利用这一特性来检测离群值。

我们将在示例中使用的 TF-IDF 的定义如下:

tf = # of times term appears in a document (raw frequency)
idf = 1+log(#  of documents / # documents with term in it)
tf-idf = tf * idf

我们将使用三个 MapReduce 作业在 Python 中实现该算法:

  • 第一个计算词频
  • 第二个计算文档频率(IDF 的分母)
  • 第三个计算每条推文的 TF-IDF

计算词频

词频部分与字数统计示例非常相似。 主要区别在于,我们将使用多字段、制表符分隔的键来跟踪术语和文档 ID 的同时出现情况。 对于 JSON 格式的每个 tweet,映射器提取id_strtext字段,标记化text,并发出termdoc_id元组:

for tweet in sys.stdin:
    # skip empty lines
    if tweet == '\n':
        continue
    try:
        tweet = json.loads(tweet)
    except:
        logger.warn("Invalid input %s " % tweet)
        continue
    # In our example one tweet corresponds to one document.
    doc_id = tweet['id_str']
    if not doc_id:
        continue

    # preserve utf-8 encoding
    text = tweet['text'].encode('utf-8')
    # newline characters can appear within the text
    text = text.replace('\n', '')

    # lowercase and tokenize
    text = text.lower().split()

    for term in text:
        try:
            print(
                u"%s\t%s" % (
                    term.decode('utf-8'), doc_id.decode('utf-8'))
                )
        except UnicodeEncodeError:
            logger.warn("Invalid term %s " % term)

在减法器中,我们以制表符分隔的字符串形式发出文档中每个术语的频率:

freq = 1
cur_term, cur_doc_id = sys.stdin.readline().split()
for line in sys.stdin:
    line = line.strip()
    try:
        term, doc_id = line.split('\t')
    except:
        logger.warn("Invalid record %s " % line)

    # the key is a (doc_id, term) pair
    if (doc_id == cur_doc_id) and (term == cur_term):
        freq += 1

    else:
        print(
            u"%s\t%s\t%s" % (
                cur_term.decode('utf-8'), cur_doc_id.decode('utf-8'), freq))
        cur_doc_id = doc_id
        cur_term = term
        freq = 1

print(
    u"%s\t%s\t%s" % (
        cur_term.decode('utf-8'), cur_doc_id.decode('utf-8'), freq))

要使此实现正常工作,减速器输入按术语排序是至关重要的。 我们可以使用以下管道从命令行测试这两个脚本:

$ cat tweets.json  |  python map-tf.py  | sort -k1,2  | \
python reduce-tf.py

在命令行中,我们使用sort实用程序,而在 MapReduce 中,我们将使用org.apache.hadoop.mapreduce.lib.KeyFieldBasedComparator。 该比较器实现sort命令提供的功能子集。 特别地,可以使用–k<position>选项指定按字段排序。 要按术语(键的第一个字段)过滤,我们设置-D mapreduce.text.key.comparator.options=-k1

/usr/bin/hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/hadoop-streaming.jar \
-D map.output.key.field.separator=\t \
-D stream.num.map.output.key.fields=2 \
-Dmapreduce.output.key.comparator.class=\
org.apache.hadoop.mapreduce.lib.KeyFieldBasedComparator \
-D mapreduce.text.key.comparator.options=-k1,2 \
-input tweets.json \
-output /tmp/tf-out.tsv \
-file map-tf.py \
-mapper "python map-tf.py" \
-file reduce-tf.py \
-reducer "python reduce-tf.py" 

备注

我们在比较器选项中指定哪些字段属于键(用于混洗)。

映射器和减速器代码可在https://github.com/learninghadoop2/book-examples/blob/master/ch9/streaming/tf-idf/python/map-tf.py中找到。

计算单据频次

计算文档频率的主逻辑在减法器中,而映射器只是一个标识函数,它加载并输送 TF 作业的输出(按术语排序)。 在缩减器中,对于每个术语,我们计算它在所有文档中出现的次数。 对于每个术语,我们保留(termdoc_idtf)元组的缓冲区key_cache,当找到新的术语时,我们将缓冲区刷新为标准输出,以及累积的文档频率df

# Cache the (term,doc_id, tf) tuple. 
key_cache = []

line = sys.stdin.readline().strip()
cur_term, cur_doc_id, cur_tf = line.split('\t')
cur_tf = int(cur_tf)
cur_df = 1

for line in sys.stdin:
    line = line.strip()

    try:
        term, doc_id, tf = line.strip().split('\t')
        tf = int(tf)
    except:
        logger.warn("Invalid record: %s " % line)
        continue

    # term is the only key for this input
    if (term == cur_term):
        # increment document frequency
        cur_df += 1

        key_cache.append(
            u"%s\t%s\t%s" % (term.decode('utf-8'), doc_id.decode('utf-8'), tf))

    else:
        for key in key_cache:
            print("%s\t%s" % (key, cur_df))

        print (
            u"%s\t%s\t%s\t%s" % (
                cur_term.decode('utf-8'),
                cur_doc_id.decode('utf-8'),
                cur_tf, cur_df)
            )

        # flush the cache
        key_cache = []
        cur_doc_id = doc_id
        cur_term = term
        cur_tf = tf
        cur_df = 1

for key in key_cache:
    print(u"%s\t%s" % (key.decode('utf-8'), cur_df))
print(
    u"%s\t%s\t%s\t%s\n" % (
        cur_term.decode('utf-8'),
        cur_doc_id.decode('utf-8'),
        cur_tf, cur_df))

我们可以通过以下命令行测试脚本:

$ cat /tmp/tf-out.tsv  |  python map-df.py  | python reduce-df.py > /tmp/df-out.tsv

我们可以通过以下方式测试 Hadoop 流上的脚本:

/usr/bin/hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/hadoop-streaming.jar \
-D map.output.key.field.separator=\t \
-D stream.num.map.output.key.fields=3 \
-D mapreduce.output.key.comparator.class=\
org.apache.hadoop.mapreduce.lib.KeyFieldBasedComparator \
-D mapreduce.text.key.comparator.options=-k1 \
-input /tmp/tf-out.tsv/part-00000 \
-output /tmp/df-out.tsv \
-mapper org.apache.hadoop.mapred.lib.IdentityMapper \
-file reduce-df.py \
-reducer "python reduce-df.py"

在 Hadoop 上,我们使用org.apache.hadoop.mapred.lib.IdentityMapper,它提供与map-df.py脚本相同的逻辑。

映射器和减速器代码可在https://github.com/learninghadoop2/book-examples/blob/master/ch9/streaming/tf-idf/python/map-df.py中找到。

把它们放在一起-TF-IDF

要计算 TF-IDF,我们只需要一个使用上一步输出的映射器:

num_doc = sys.argv[1]

for line in sys.stdin:
    line = line.strip()

    try:
        term, doc_id, tf, df = line.split('\t')

        tf = float(tf)
        df = float(df)
        num_doc = float(num_doc)
    except:
        logger.warn("Invalid record %s" % line)

    # idf = num_doc / df
    tf_idf = tf * (1+math.log(num_doc / df))
    print("%s\t%s\t%s" % (term, doc_id, tf_idf))

集合中的文档数作为参数传递给tf-idf.py

/usr/bin/hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/hadoop-streaming.jar \
-D mapreduce.reduce.tasks=0 \
-input /tmp/df-out.tsv/part-00000 \
-output /tmp/tf-idf.out \
-file tf-idf.py \
-mapper "python tf-idf.py 15578"

要计算 tweet 总数,我们可以将catwcUnix 实用程序与 Hadoop 流结合使用:

/usr/bin/hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/hadoop-streaming.jar \
-input tweets.json \
-output tweets.cnt \
-mapper /bin/cat \
-reducer /usr/bin/wc

映射器源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch9/streaming/tf-idf/python/tf-idf.py找到。

套件数据

Kite SDK(Hadoop)是类、命令行工具和示例的集合,旨在简化在 http://www.kitesdk.org 之上构建应用的过程。

在本节中,我们将了解 Kite 的子项目 Kite Data 如何简化与 Hadoop 数据仓库的几个组件的集成。 风筝示例可以在https://github.com/kite-sdk/kite-examples找到。

在 Cloudera 的 QuickStart VM 上,可以在/opt/cloudera/parcels/CDH/lib/kite/找到 Kite Jars。

Kite 数据被组织在许多子项目中,我们将在下面的部分中描述其中一些子项目。

_

顾名思义,核心是数据模块中提供的所有功能的构建块。 它的主要抽象是数据集和存储库。

org.kitesdk.data.Dataset接口用于表示一组不可变的数据:

@Immutable
public interface Dataset<E> extends RefinableView<E> {
  String getName();
  DatasetDescriptor getDescriptor();
  Dataset<E> getPartition(PartitionKey key, boolean autoCreate);
  void dropPartition(PartitionKey key);
  Iterable<Dataset<E>> getPartitions();
  URI getUri();
}

每个数据集由org.kitesdk.data.DatasetDescriptor接口的名称和实例标识,即数据集的结构描述,并提供其模式(org.apache.avro.Schema)和分区策略。

Reader<E>接口的实现用于从底层存储系统读取数据并产生类型为E的反序列化实体。 newReader()方法可用于获取给定数据集的适当实现:

public interface DatasetReader<E> extends Iterator<E>, Iterable<E>, Closeable {
  void open();

  boolean hasNext();

  E next();
    void remove();
    void close();
    boolean isOpen();
}

DatasetReader的实例将提供读取和迭代数据流的方法。 类似地,org.kitesdk.data.DatasetWriter提供了将数据流写入Dataset对象的接口:

public interface DatasetWriter<E> extends Flushable, Closeable {
  void open();
  void write(E entity);
  void flush();
  void close();
  boolean isOpen();
}

与阅读器一样,编写器也是一次性使用的对象。 它们序列化类型为E的实体的实例,并将它们写入底层存储系统。 编写器通常不会直接实例化;相反,可以通过newWriter()工厂方法创建适当的实现。 DatasetWriter的实现将持有资源,直到调用close(),并期望调用方在写入器不再使用时调用finally块中的close()。 最后,请注意DatasetWriter的实现通常不是线程安全的。 从多个线程访问编写器的行为未定义。

数据集的一个特殊情况是View接口,如下所示:

public interface View<E> {
   Dataset<E> getDataset();
   DatasetReader<E> newReader();
   DatasetWriter<E> newWriter();
   boolean includes(E entity);
   public boolean deleteAll();
}

视图携带现有数据集的键和分区的子集;它们在概念上类似于关系模型中的“视图”概念。

View界面可以从数据范围或键范围创建,也可以作为其他视图之间的联合创建。

_Data HCatalog

Data HCatalog 是一个模块,可用于访问 HCatalog 存储库。 该模块的核心抽象是org.kitesdk.data.hcatalog.HCatalogAbstractDatasetRepository及其具体实现org.kitesdk.data.hcatalog.HCatalogDatasetRepository

它们描述了使用 HCatalog 管理存储的元数据和 HDFS 的DatasetRepository,如下所示:

public class HCatalogDatasetRepository extends HCatalogAbstractDatasetRepository {
   HCatalogDatasetRepository(Configuration conf) {
    super(conf, new HCatalogManagedMetadataProvider(conf));
  }
   HCatalogDatasetRepository(Configuration conf, MetadataProvider provider) {
    super(conf, provider);
  }
   public <E> Dataset<E> create(String name, DatasetDescriptor descriptor) {
    getMetadataProvider().create(name, descriptor);
    return load(name);
  }
   public boolean delete(String name) {
    return getMetadataProvider().delete(name);
  }
   public static class Builder {
   …
  }
}

备注

从 Kite 0.17 开始,Data HCatalog 已弃用,取而代之的是新的 Data Have 模块。

数据目录的位置由Hive/HCatalog选择(所谓的“托管表”),或者在创建该类的实例时通过在构造函数中提供文件系统和根目录(外部表)来指定。

_

kite-data-模块通过Dataset接口公开配置单元模式。 从 Kite 0.17 开始,此包将取代 Data HCatalog。

Колибрипрограмма数据映射还原

org.kitesdk.data.mapreduce包提供了接口,用于使用 MapReduce 对数据集进行读写操作。

同步,由长者更正

org.kitesdk.data.spark包提供了接口,用于使用 Apache Spark 对数据集进行读写操作。

_

org.kitesdk.data.crunch.CrunchDatasets包是一个帮助器类,用于将数据集和视图公开为 CrunchReadableSourceTarget类:

public class CrunchDatasets {
public static <E> ReadableSource<E> asSource(View<E> view, Class<E> type) {
    return new DatasetSourceTarget<E>(view, type);
  }
public static <E> ReadableSource<E> asSource(URI uri, Class<E> type) {
    return new DatasetSourceTarget<E>(uri, type);
  }
public static <E> ReadableSource<E> asSource(String uri, Class<E> type) {
    return asSource(URI.create(uri), type);
  }

public static <E> Target asTarget(View<E> view) {
    return new DatasetTarget<E>(view);
  }
 public static Target asTarget(String uri) {
    return asTarget(URI.create(uri));
  }
public static Target asTarget(URI uri) {
    return new DatasetTarget<Object>(uri);
  }
}

♫T0\ApacheCrunch

Apache Crunch(http://crunch.apache.org)是一个 Java 和 Scala 库,用于创建 MapReduce 作业的管道。 它基于谷歌的 FlumeJava(http://dl.acm.org/citation.cfm?id=1806638)论文和库。 该项目的目标是通过公开许多实现聚合、联接、过滤和排序记录等操作的模式,使熟悉 Java 编程语言的任何人都能尽可能简单地编写 MapReduce 作业。

与 Pig 等工具类似,Crunch 管道是通过组合不可变的分布式数据结构并在这些结构上运行所有处理操作来创建的;它们被表示和实现为用户定义的函数。 管道被编译成 MapReduce 作业的 DAG,其执行由库的规划者管理。 Crunch 允许我们编写迭代代码,并从映射和归约操作的角度抽象出思考的复杂性,同时避免了对诸如 Pig 拉丁语之类的特殊编程语言的需要。 此外,Crunch 提供了一个高度可定制的类型系统,允许我们处理和混合 Hadoop Writables、HBase 和 Avro 序列化对象。

FlumeJava 的主要假设是,MapReduce 对于几类问题来说是错误的抽象级别,这些问题的计算通常由多个链式作业组成。 出于性能原因,我们经常需要将逻辑上独立的操作(例如,过滤、投影、分组和其他转换)组合到单个物理 MapReduce 作业中。 这一方面对代码的可测试性也有影响。 虽然我们不会在本章中讨论这一方面,但我们鼓励读者通过参考 Crunch 的文档来深入了解它。

入门

QuickStart 虚拟机上已经安装了 Crash Jars。 默认情况下,JAR 位于/opt/cloudera/parcels/CDH/lib/crunch中。

或者,最近的 Crunch 库可以从https://crunch.apache.org/download.html、从 Maven Central 或特定于 Cloudera 的存储库下载。

概念

压缩管道由两个抽象组成:PCollectionPTable

PCollection<T>接口是类型为T的对象的分布式不可变集合。 PTable<Key, Value>接口是由Key类型的键和Value类型的值组成的分布式、不可变的哈希表(PCollection 的子接口),它公开了使用键-值对的方法。

这两个抽象支持以下四个基元操作:

  • parallelDo:将用户定义函数DoFn应用于给定的PCollection,并返回新的PCollection
  • union:将两个或多个PCollections合并为单个虚拟PCollection
  • groupByKey:按键对PTable的元素进行排序和分组
  • combineValues:聚合来自groupByKey操作的值

https://github.com/learninghadoop2/book-examples/blob/master/ch9/crunch/src/main/java/com/learninghadoop2/crunch/HashtagCount.java实现了一个 Crunch MapReduce 管道,该管道对出现的散列标签进行计数:

Pipeline pipeline = new MRPipeline(HashtagCount.class, getConf());

pipeline.enableDebug();

PCollection<String> lines = pipeline.readTextFile(args[0]);

PCollection<String> words = lines.parallelDo(new DoFn<String, String>() {
  public void process(String line, Emitter<String> emitter) {
    for (String word : line.split("\\s+")) {
        if (word.matches("(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)")) {
            emitter.emit(word);
        }
    }
  }
}, Writables.strings());

PTable<String, Long> counts = words.count();

pipeline.writeTextFile(counts, args[1]);
// Execute the pipeline as a MapReduce.
pipeline.done();

在本例中,我们首先创建一个MRPipeline管道,并使用它首先将用stream.py -t创建的sample.txt的内容读取到一个字符串集合中,其中该集合的每个元素代表一条 tweet。 我们使用tweet.split("\\s+")将每条 tweet 标记为单词,并发出与标签正则表达式匹配的每个单词,序列化为 Writable。 请注意,标记化和过滤操作由parallelDo调用创建的 MapReduce 作业并行执行。 我们创建了一个PTable,它将每个表示为字符串的 hashtag 与它在数据集中出现的次数关联起来。 最后,我们将PTable计数作为文本文件写入 HDFS。 使用pipeline.done()执行流水线。

要编译和执行管道,我们可以使用 Gradle 来管理所需的依赖项,如下所示:

$ ./gradlew jar
$ ./gradlew copyJars

将使用copyJars下载的 Crunch 和 Avro 依赖项添加到LIBJARS环境变量:

$ export CRUNCH_DEPS=build/libjars/crunch-example/lib
$ export LIBJARS=${LIBJARS},${CRUNCH_DEPS}/crunch-core-0.9.0-cdh5.0.3.jar,${CRUNCH_DEPS}/avro-1.7.5-cdh5.0.3.jar,${CRUNCH_DEPS}/avro-mapred-1.7.5-cdh5.0.3-hadoop2.jar

然后,在 Hadoop 上运行示例:

$ hadoop jar build/libs/crunch-example.jar \
com.learninghadoop2.crunch.HashtagCount \
tweets.json count-out \
-libjars $LIBJARS

数据序列化

框架的目标之一是使处理包含嵌套和重复数据结构(如协议缓冲区和 Thrift 记录)的复杂记录变得容易。

org.apache.crunch.types.PType接口定义了在 Crunch 管道中使用的数据类型与用于从 HDFS 读取数据/向 HDFS 写入数据的序列化和存储格式之间的映射。 每个PCollection都有一个关联的PType,它告诉 Crunch 如何读/写数据。

org.apache.crunch.types.PTypeFamily接口提供抽象工厂来实现共享相同序列化格式的PType实例。 目前,Crunch 支持两种类型的系列:一种基于 Writable 接口,另一种基于 Apache Avro。

备注

尽管 Crunch 允许在同一管道中混合和匹配使用PType的不同实例的PCollection接口,但每个PCollection接口的PType必须属于唯一的系列。 例如,不可能将键序列化为 Writable 并使用 avro 序列化其值的PTable

这两个类型族都支持一组通用的原语类型(字符串、长整型、整型、浮点型、双精度型、布尔型和字节),以及可以由其他PTypes构造的更复杂的PType接口。 其中包括其他PType的元组和集合。 一个特别重要、复杂的PTypetableOf,它确定paralleDo的返回类型是PCollection还是PTable

可以通过继承和扩展 Avro 和 Writable 族的内置内容来创建新的PTypes。 这需要实现 InputMapFn<S, T>和 Output MapFn<T, S>类。 我们为S是原始类型而T是新类型的实例实现PType

派生的PTypes可以在PTypes类中找到。 其中包括对协议缓冲区、Thrift 记录、Java Enums、BigInteger 和 UUID 的序列化支持。 我们在使用 Apache Pig 进行数据分析中讨论的 Elephant Bird 库包含其他示例。

数据处理模式

org.apache.crunch.lib为常见的数据操作操作实现了许多设计模式。

聚合和排序

org.apache.crunch.lib提供的大多数数据处理模式依赖于PTablegroupByKey方法。该方法有三种不同的重载形式:

  • groupByKey():让规划人员确定分区的数量
  • groupByKey(int numPartitions):用于设置开发者指定的分区数量
  • groupByKey(GroupingOptions options):允许我们指定用于混洗的自定义分区和比较器

org.apache.crunch.GroupingOptions类采用 Hadoop 的PartitionerRawComparator类的实例来实现自定义分区和排序操作。

groupByKey方法返回PGroupedTable的实例,PGroupedTable是 Crunch 对分组表格的表示。 它对应于 MapReduce 作业的混洗阶段的输出,并允许将值与combineValue方法组合。

org.apache.crunch.lib.Aggregate包公开了对PCollection实例执行简单聚合(count、max、top 和 length)的方法。

Sort 提供了一个 API 来对其内容实现Comparable接口的PCollectionPTable实例进行排序。

默认情况下,Crunch 使用一个缩减器对数据进行排序。 可以通过将所需的分区数传递给sort方法来修改此行为。 Sort.Order方法用信号表示应该进行排序的顺序。

下面是如何为集合指定不同的排序选项:

public static <T> PCollection<T> sort(PCollection<T> collection)
public static <T> PCollection<T> sort(PCollection<T> collection, Sort.Order order)
public static <T> PCollection<T> sort(PCollection<T> collection, int numReducers,                                       Sort.Order order)

下面是如何为表指定不同的排序选项:

public static <K,V> PTable<K,V> sort(PTable<K,V> table)

public static <K,V> PTable<K,V> sort(PTable<K,V> table, Sort.Order key)
public static <K,V> PTable<K,V> sort(PTable<K,V> table, int numReducers, Sort.Order key)

最后,sortPairs使用Sort.ColumnOrder中指定的列顺序对PCollection对进行排序:

sortPairs(PCollection<Pair<U,V>> collection, Sort.ColumnOrder... columnOrders)

连接数据

org.apache.crunch.lib.Join包是基于公共密钥加入PTables的 API。 支持以下四种联接操作:

  • fullJoin
  • join(默认为innerJoin)
  • leftJoin
  • rightJoin

这些方法具有共同的返回类型和签名。 作为参考,我们将描述实现内部联接的常用join方法:

public static <K,U,V> PTable<K,Pair<U,V>> join(PTable<K,U> left, PTable<K,V> right)

org.apache.crunch.lib.Join.JoinStrategy包提供了定义自定义联接策略的接口。 Crunch 的默认策略(defaultStrategy)是连接数据减少端。

管道实施和执行

Crunch 伴随着管道接口的三个实现而来。 本章隐含使用的最旧的是org.apache.crunch.impl.mr.MRPipeline,它使用 Hadoop 的 MapReduce 作为其执行引擎。 org.apache.crunch.impl.mem.MemPipeline允许在内存中执行所有操作,而不执行到磁盘的序列化。 Crunch 0.10 引入了org.apache.crunch.impl.spark.SparkPipeline,它编译并运行 Apache Spark 的 DAGPCollections

SparkPippeline

使用 SparkPipeline,Crunch 将的大部分执行任务委托给 Spark,并执行相对较少的计划任务,以下例外情况除外:

  • 多路输入
  • 多路输出
  • 数据序列化
  • 检查点设置

在撰写本文时,SparkPipeline 仍在大力开发中,可能无法处理标准 MRPipeline 的所有用例。 Crunch 社区正在积极工作,以确保两种实现之间的完全兼容性。

Pippeline

MemPipeline 在客户机上执行内存中的。 与 MRPipeline 不同,MemPipeline 不是显式创建的,而是通过调用静态方法MemPipeline.getInstance()引用的。 所有操作都在内存中,PTypes 的使用非常少。

压缩示例

现在我们将使用 Apache Crunch 以更模块化的方式重新实现到目前为止编写的一些 MapReduce 代码。

词语共现

第 3 章Processing-MapReduce and Beyond中,我们展示了一个 MapReduce 作业 BiGramCount,用于计算 tweet 中单词的共现次数。 同样的逻辑可以实现为DoFn。 使用 Crunch,我们可以使用复杂类型Pair<String, String>,而不是发出多字段键并在稍后阶段对其进行解析,如下所示:

class BiGram extends DoFn<String, Pair<String, String>> {
    @Override
    public void process(String tweet, 
Emitter<Pair<String, String>> emitter) {
        String[] words = tweet.split(" ") ;

        Text bigram = new Text();
        String prev = null;

        for (String s : words) {
          if (prev != null) {
              emitter.emit(Pair.of(prev, s));
            }       
            prev = s;
        }
    }   
}       

请注意,与 MapReduce 相比,BiGramCrunch 实现是一个独立的类,可以在任何其他代码库中轻松重用。 此示例的代码包含在https://github.com/learninghadoop2/book-examples/blob/master/ch9/crunch/src/main/java/com/learninghadoop2/crunch/DataPreparationPipeline.java中。

TF-IDF

我们可以使用MRPipeline实现 TF-IDF 作业链,如下所示:

public class CrunchTermFrequencyInvertedDocumentFrequency 
         extends Configured implements Tool, Serializable {

   private Long numDocs;

   @SuppressWarnings("deprecation")

   public static class TF {
        String term;
        String docId;
        int frequency;

        public TF() {}

        public TF(String term, 
               String docId, Integer frequency) {
           this.term = term;
           this.docId = docId;
           this.frequency = (int) frequency;

        }
   }

   public int run(String[] args) throws Exception {
       if(args.length != 2) {
         System.err.println();
         System.err.println("Usage: " + this.getClass().getName() + " [generic options] input output");

         return 1;
       }
       // Create an object to coordinate pipeline creation and execution.
       Pipeline pipeline = 
new MRPipeline(TermFrequencyInvertedDocumentFrequency.class, getConf());

       // enable debug options
       pipeline.enableDebug();

       // Reference a given text file as a collection of Strings.
       PCollection<String> tweets = pipeline.readTextFile(args[0]);
       numDocs = tweets.length().getValue();

       // We use Avro reflections to map the TF POJO to avsc 
       PTable<String, TF> tf = tweets.parallelDo(new TermFrequencyAvro(), Avros.tableOf(Avros.strings(), Avros.reflects(TF.class)));

       // Calculate DF
       PTable<String, Long> df = Aggregate.count(tf.parallelDo( new DocumentFrequencyString(), Avros.strings()));

       // Finally we calculate TF-IDF 
       PTable<String, Pair<TF, Long>> tfDf = Join.join(tf, df);
       PCollection<Tuple3<String, String, Double>> tfIdf = tfDf.parallelDo(new TermFrequencyInvertedDocumentFrequency(),
                Avros.triples(
                      Avros.strings(), 
                      Avros.strings(), 
                      Avros.doubles()));

       // Serialize as avro 
       tfIdf.write(To.avroFile(args[1]));

       // Execute the pipeline as a MapReduce.
       PipelineResult result = pipeline.done();
       return result.succeeded() ? 0 : 1;
   }
   …
}

与流式传输相比,我们在这里遵循的方法具有许多优势。 首先,我们不需要使用单独的脚本手动链接 MapReduce 作业。 这项任务是 Crunch 的主要目的。 其次,我们可以将度量的每个组件表示为不同的类,使其更容易在未来的应用中重用。

为了实现词频,我们创建了一个DoFn类,它接受 tweet 作为输入并发出Pair<String, TF>。 第一个元素是一个术语,第二个元素是将使用 avro 序列化的 POJO 类的实例。 TF部分包含三个变量: termdocumentIdfrequency。 在引用实现中,我们希望输入数据是我们反序列化和解析的 JSON 字符串。 我们还将标记化作为 Process 方法的一个子任务。

根据用例的不同,我们可以分别在和DoFns中抽象这两个操作,如下所示:

class TermFrequencyAvro extends DoFn<String,Pair<String, TF>> {
    public void process(String JSONTweet, 
Emitter<Pair <String, TF>> emitter) {
        Map<String, Integer> termCount = new HashMap<>();

        String tweet;
        String docId;

        JSONParser parser = new JSONParser();

        try {
            Object obj = parser.parse(JSONTweet);

            JSONObject jsonObject = (JSONObject) obj;

            tweet = (String) jsonObject.get("text");
            docId = (String) jsonObject.get("id_str");

            for (String term : tweet.split("\\s+")) {
                if (termCount.containsKey(term.toLowerCase())) {
                    termCount.put(term, 
termCount.get(term.toLowerCase()) + 1);
                } else {
                    termCount.put(term.toLowerCase(), 1);
                }
            }

            for (Entry<String, Integer> entry : termCount.entrySet()) {
                emitter.emit(Pair.of(entry.getKey(), new TF(entry.getKey(), docId, entry.getValue())));
            }
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
  }
}

文档频率很简单。 对于在项频率步骤中生成的每个Pair<String, TF>,我们发出项-该对的第一个元素。 我们汇总并计算得到的术语的PCollection,以获得文档频率,如下所示:

class DocumentFrequencyString extends DoFn<Pair<String, TF>, String> {
@Override
   public void process(Pair<String, TF> tfAvro,
      Emitter<String> emitter) {
      emitter.emit(tfAvro.first());
   }
}

最后,我们将共享密钥(Term)上的PTableTF 与PTableDF 连接起来,并将得到的Pair<String, Pair<TF, Long>>对象提供给TermFrequencyInvertedDocumentFrequency

对于每个术语和文档,我们计算 TF-IDF 并返回termdocIftfIdf三元组:

   class TermFrequencyInvertedDocumentFrequency extends MapFn<Pair<String, Pair<TF, Long>>, Tuple3<String, String, Double> >  {      
      @Override
      public Tuple3<String, String, Double> map(
            Pair<String, Pair<TF, Long>> input) {

         Pair<TF, Long> tfDf = input.second();
         Long df = tfDf.second();

         TF tf = tfDf.first();
         double idf = 1.0+Math.log(numDocs / df);
         double tfIdf = idf * tf.frequency;

         return  Tuple3.of(tf.term, tf.docId, tfIdf);
      }

   }   

我们使用MapFn,因为我们将为每个输入输出一条记录。 本例的源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch9/crunch/src/main/java/com/learninghadoop2/crunch/CrunchTermFrequencyInvertedDocumentFrequency.java找到。

可以使用以下命令编译和执行该示例:

$ ./gradlew jar
$ ./gradlew copyJars

如果尚未完成,请将使用copyJars下载的 Crunch 和 Avro 依存关系添加到LIBJARS环境变量,如下所示:

$ export CRUNCH_DEPS=build/libjars/crunch-example/lib
$ export LIBJARS=${LIBJARS},${CRUNCH_DEPS}/crunch-core-0.9.0-cdh5.0.3.jar,${CRUNCH_DEPS}/avro-1.7.5-cdh5.0.3.jar,${CRUNCH_DEPS}/avro-mapred-1.7.5-cdh5.0.3-hadoop2.jar

此外,将json-simpleJAR 添加到LIBJARS

$ export LIBJARS=${LIBJARS},${CRUNCH_DEPS}/json-simple-1.1.1.jar

最后,将CrunchTermFrequencyInvertedDocumentFrequency作为 MapReduce 作业运行,如下所示:

$ hadoop jar build/libs/crunch-example.jar \
com.learninghadoop2.crunch.CrunchTermFrequencyInvertedDocumentFrequency  \
-libjars ${LIBJARS} \
tweets.json tweets.avro-out

风筝睡眠线

Kite Morphines 是一个数据转换库,灵感来自 Unix 管道,最初是作为 Cloudera 搜索的一部分开发的。 变形线是内存中的转换命令链,它依赖插件结构来利用异构数据源。 它使用声明性命令对记录执行 ETL 操作。 命令在配置文件中定义,该文件稍后将提供给驱动程序类。

其目标是通过提供一个允许开发人员用一系列配置设置替换编程的库,使将 ETL 逻辑嵌入到任何 Java 代码库中成为一项微不足道的任务。

概念

形态线是围绕两个抽象构建的:CommandRecord

记录是org.kitesdk.morphline.api.Record接口的实现:

public final class Record {  
  private ArrayListMultimap<String, Object> fields;  
…
    private Record(ArrayListMultimap<String, Object> fields) {…}
  public ListMultimap<String, Object> getFields() {…}
  public List get(String key) {…}
  public void put(String key, Object value) {…}
   …
}

记录是一组个命名字段,其中每个字段都有一个包含一个或多个值的列表。 Record是在 Google Guava 的ListMultimapArrayListMultimap类之上实现的。 请注意,值可以是任何 Java 对象,字段可以是多值的,并且两条记录不需要使用公共字段名。 记录可以包含_attachment_body字段,该字段可以是java.io.InputStream或字节数组。

命令实现org.kitesdk.morphline.api.Command接口:

public interface Command {
   void notify(Record notification);
   boolean process(Record record);
   Command getParent();
}

命令将记录转换为零个或多个记录。 命令可以调用为读写操作以及添加或删除字段提供的Record实例上的方法。

命令被链接在一起,在变形线的每一步,父命令将记录发送给子命令,子命令继而处理这些记录。 父母和孩子之间使用两个通信通道(平面)交换信息;通知通过控制平面发送,记录通过数据平面发送。 记录由process()方法处理,该方法返回一个布尔值以指示是否应该继续进行变形线。

命令不是直接实例化的,而是通过实现org.kitesdk.morphline.api.CommandBuilder接口来实例化的:

public interface CommandBuilder {
   Collection<String> getNames();
   Command build(Config config, 
      Command parent, 
      Command child, 
      MorphlineContext context);
}

getNames方法返回可用于调用命令的名称。 支持多个名称以允许向后兼容名称更改。 build()方法创建并返回一个以给定的变形线配置为根的命令。

org.kitesdk.morphline.api.MorphlineContext接口允许将附加参数传递给所有 Morphline 命令。

条形态线的数据模型是按照源-管-宿模式构建的,在这种模式下,从源捕获数据,通过多个处理步骤通过管道传输数据,然后将其输出传送到宿中。

Morphline 命令

Kite Morphines 附带了许多默认命令,这些命令实现了常见序列化格式(纯文本、Avro、JSON)的数据转换。 当前可用的命令组织为变形线的子项目,包括:

  • kite-morphlines-core-stdio:将从二进制大型对象(BLOB)和文本读取数据
  • kite-morphlines-core-stdlib:包装用于数据操作和表示的 Java 数据类型
  • kite-morphlines-avro:是,用于序列化和反序列化 avro 格式的数据
  • kite-morphlines-json:将序列化和反序列化 JSON 格式的数据
  • kite-morphlines-hadoop-core:是否用于访问 HDFS
  • kite-morphlines-hadoop-parquet-avro:是,用于序列化和反序列化 Parquet 格式的数据
  • kite-morphlines-hadoop-sequencefile:用于序列化和反序列化 Sequencefile 格式的数据
  • kite-morphlines-hadoop-rcfile:使用序列化和反序列化 RC 文件格式的数据

所有可用命令的列表可在http://kitesdk.org/docs/0.17.0/kite-morphlines/morphlinesReferenceGuide.html中找到。

命令是通过在配置文件morphline.conf中声明一系列转换来定义的,然后由驱动程序编译并执行该配置文件。 例如,我们可以指定一个read_tweets变形行,它将加载存储为 JSON 数据的 tweet,使用 Jackson 序列化和反序列化它们,并通过组合org.kitesdk.morphline包中包含的默认readJsonhead命令打印前 10 个,如下所示:

morphlines : [{
  id : read_tweets
  importCommands : ["org.kitesdk.morphline.**"]

  commands : [{
    readJson {
      outputClass : com.fasterxml.jackson.databind.JsonNode
    }}
    {
      head { 
      limit : 10
    }}
  ]
}]

现在,我们将展示如何从独立的 Java 程序和 MapReduce 执行此变形线。

MorphlineDriver.java显示了如何使用嵌入到主机系统中的库。 我们在 main方法中执行的第一步是加载 Morphline 的 JSON 配置,构建一个MorphlineContext对象,并将其编译成Command的实例,该实例充当 Morphline 的起始节点。 请注意,Compiler.compile()接受一个finalChild参数;在本例中,它是RecordEmitter。 我们使用RecordEmitter作为形态线的接收器,要么将记录打印到 stdout,要么将其存储到 HDFS 中。 在MorphlineDriver示例中,我们使用org.kitesdk.morphline.base.Notifications以事务的方式管理和监视 Morphline 生命周期。

调用Notifications.notifyStartSession(morphline)将在通过调用Notifications.notifyBeginTransaction定义的事务内启动转换链。 成功后,我们使用Notifications.notifyShutdown(morphline)终止管道。 在失败的情况下,我们回滚事务Notifications.notifyRollbackTransaction(morphline),并将异常处理程序从变形行上下文传递到调用 Java 代码:

public class MorphlineDriver {
    private static final class RecordEmitter implements Command {
       private final Text line = new Text();

      @Override
      public Command getParent() {
         return null;
      }

      @Override
      public void notify(Record record) {

      }

      @Override
      public boolean process(Record record) {
         line.set(record.get("_attachment_body").toString());

         System.out.println(line);

         return true;
      }
       }  

   public static void main(String[] args) throws IOException {
       /* load a morphline conf and set it up */
       File morphlineFile = new File(args[0]);
       String morphlineId = args[1];
       MorphlineContext morphlineContext = new MorphlineContext.Builder().build();
       Command morphline = new Compiler().compile(morphlineFile, morphlineId, morphlineContext, new RecordEmitter());

       /* Prepare the morphline for execution
        * 
        * Notifications are sent through the communication channel  
        * */

       Notifications.notifyBeginTransaction(morphline);

       /* Note that we are using the local filesystem, not hdfs*/
       InputStream in = new BufferedInputStream(new FileInputStream(args[2]));

       /* fill in a record and pass  it over */
       Record record = new Record();
       record.put(Fields.ATTACHMENT_BODY, in); 

       try {

            Notifications.notifyStartSession(morphline);
            boolean success = morphline.process(record);
            if (!success) {
              System.out.println("Morphline failed to process record: " + record);
            }
        /* Commit the morphline */
       } catch (RuntimeException e) {
           Notifications.notifyRollbackTransaction(morphline);
           morphlineContext.getExceptionHandler().handleException(e, null);
         }
       finally {
            in.close();
        }

        /* shut it down */
        Notifications.notifyShutdown(morphline);     
    }
}

在本例中,我们将 JSON 格式的数据从本地文件系统加载到一个InputStream对象中,并使用它来初始化一个新的Record实例。 RecordEmitter类包含链的最后一个处理的记录实例,我们在该实例上提取_attachment_body并将其打印到标准输出。 MorphlineDriver的源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch9/kite/src/main/java/com/learninghadoop2/kite/morphlines/MorphlineDriver.java中找到。

使用 MapReduce 作业中相同的变形线非常简单。 在 Mapper 的设置阶段,我们构建一个包含实例化逻辑的上下文,而 map 方法设置Record对象并触发处理逻辑,如下所示:

public static class ReadTweets
        extends Mapper<Object, Text, Text, NullWritable> {
    private final Record record = new Record();
    private Command morphline;

    @Override
    protected void setup(Context context)
            throws IOException, InterruptedException {
        File morphlineConf = new File(context.getConfiguration()
                .get(MORPHLINE_CONF));
        String morphlineId = context.getConfiguration()
                .get(MORPHLINE_ID);
        MorphlineContext morphlineContext = 
new MorphlineContext.Builder()
                .build();

        morphline = new org.kitesdk.morphline.base.Compiler()
                .compile(morphlineConf,
                        morphlineId,
                        morphlineContext,
                        new RecordEmitter(context));
    }

    public void map(Object key, Text value, Context context)
            throws IOException, InterruptedException {
        record.put(Fields.ATTACHMENT_BODY,
                new ByteArrayInputStream(
value.toString().getBytes("UTF8")));
        if (!morphline.process(record)) {
              System.out.println(
"Morphline failed to process record: " + record);
        }

        record.removeAll(Fields.ATTACHMENT_BODY);
    }
}

在 MapReduce 代码中,我们修改了RecordEmitter以从后处理的记录中提取Fields有效负载,并将其存储到上下文中。 这允许我们通过在 MapReduce 配置样板中指定FileOutputFormat将数据写入 HDFS:

private static final class RecordEmitter implements Command {
    private final Text line = new Text();
    private final Mapper.Context context;

    private RecordEmitter(Mapper.Context context) {
        this.context = context;
    }

    @Override
    public void notify(Record notification) {
    }

    @Override
    public Command getParent() {
        return null;
    }

    @Override
    public boolean process(Record record) {
        line.set(record.get(Fields.ATTACHMENT_BODY).toString());
        try {
            context.write(line, null);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}   

请注意,我们现在可以通过修改morphline.conf来更改处理管道行为并添加进一步的数据转换,而无需明确更改实例化和处理逻辑。 MapReduce 驱动程序源代码可以在https://github.com/learninghadoop2/book-examples/blob/master/ch9/kite/src/main/java/com/learninghadoop2/kite/morphlines/MorphlineDriverMapReduce.java中找到。

这两个示例都可以使用以下命令从ch9/kite/编译:

$ ./gradlew jar
$ ./gradlew copyJar

我们将runtime依赖项添加到LIBJARS,如下所示

$ export KITE_DEPS=/home/cloudera/review/hadoop2book-private-reviews-gabriele-ch8/src/ch8/kite/build/libjars/kite-example/lib
export LIBJARS=${LIBJARS},${KITE_DEPS}/kite-morphlines-core-0.17.0.jar,${KITE_DEPS}/kite-morphlines-json-0.17.0.jar,${KITE_DEPS}/metrics-core-3.0.2.jar,${KITE_DEPS}/metrics-healthchecks-3.0.2.jar,${KITE_DEPS}/config-1.0.2.jar,${KITE_DEPS}/jackson-databind-2.3.1.jar,${KITE_DEPS}/jackson-core-2.3.1.jar,${KITE_DEPS}/jackson-annotations-2.3.0.jar

我们可以使用以下内容运行 MapReduce 驱动程序:

$ hadoop jar build/libs/kite-example.jar \
com.learninghadoop2.kite.morphlines.MorphlineDriverMapReduce \
-libjars ${LIBJARS} \
morphline.conf \
read_tweets \
tweets.json \
morphlines-out

可以使用以下命令执行 Java 独立驱动程序:

$ export CLASSPATH=${CLASSPATH}:${KITE_DEPS}/kite-morphlines-core-0.17.0.jar:${KITE_DEPS}/kite-morphlines-json-0.17.0.jar:${KITE_DEPS}/metrics-core-3.0.2.jar:${KITE_DEPS}/metrics-healthchecks-3.0.2.jar:${KITE_DEPS}/config-1.0.2.jar:${KITE_DEPS}/jackson-databind-2.3.1.jar:${KITE_DEPS}/jackson-core-2.3.1.jar:${KITE_DEPS}/jackson-annotations-2.3.0.jar:${KITE_DEPS}/slf4j-api-1.7.5.jar:${KITE_DEPS}/guava-11.0.2.jar:${KITE_DEPS}/hadoop-common-2.3.0-cdh5.0.3.jar
$ java -cp $CLASSPATH:./build/libs/kite-example.jar \
com.learninghadoop2.kite.morphlines.MorphlineDriver \
morphline.conf \
read_tweets tweets.json \
morphlines-out

摘要

在本章中,我们介绍了四个简化 Hadoop 开发的工具。 我们特别介绍了以下内容:

  • Hadoop Streaming 如何允许使用动态语言编写 MapReduce 作业
  • Kite Data 如何简化与异构数据源的接口
  • Apache Crunch 如何提供高级抽象来编写实现通用设计模式的 Spark 和 MapReduce 作业的管道
  • Morphline 如何允许我们声明命令和数据转换链,然后这些命令和数据转换可以嵌入到任何 Java 代码库中

第 10 章运行 Hadoop2 集群中,我们将把重点从软件开发领域转移到系统管理上。 我们将讨论如何设置、管理和扩展 Hadoop 集群,同时考虑到监控和安全性等方面。

十、运行 Hadoop 集群

在本章中,我们将稍微改变我们的关注点,看看在运行可操作的 Hadoop 集群时您将面临的一些注意事项。 我们将特别介绍以下主题:

  • 为什么开发人员应该关心操作,为什么 Hadoop 操作不同
  • 有关 Cloudera Manager 及其功能和限制的更多详细信息
  • 设计既可在物理硬件上使用又可在 EMR 上使用的集群
  • 保护 Hadoop 集群的安全
  • Hadoop 监控
  • 对在 Hadoop 上运行的应用问题进行故障排除

我是一名开发人员-我不关心操作!

在进一步讨论之前,我们需要解释一下为什么我们要在一本直接面向开发人员的书中加入一个关于系统操作的章节。 对于任何为更传统的平台(例如,Web 应用、数据库编程等)进行开发的人来说,规范很可能是在开发和运营之间进行非常清晰的划分。 第一组构建代码并将其打包,第二组控制和操作代码运行的环境。

近年来,DevOps 运动获得了发展势头,他们相信,如果这些孤岛被移除,团队之间的合作更加紧密,对每个人都是最好的。 当涉及到运行基于 Hadoop 的系统和服务时,我们相信这是绝对必要的。

Hadoop 和 DevOps 实践

尽管开发人员可以从概念上构建一个随时可以被遗忘的应用,但实际情况往往更加微妙。 在运行时分配给应用的资源数量很可能是开发人员希望影响的。 一旦应用开始运行,操作人员在尝试优化集群时可能需要深入了解应用。 这确实不像传统企业 IT 中看到的那样明确划分职责。 这可能是一件非常好的事情。

换句话说,开发人员需要更多地了解操作方面,操作人员也需要更多地了解开发人员在做什么。 因此,请将本章视为我们帮助您与运营人员进行这些讨论的贡献。 我们不打算在本章结束时让您成为 Hadoop 专家管理员;这本身就是一个专门的角色和技能集。 取而代之的是,我们将对您确实需要了解的问题进行一站式巡视,一旦您的应用在实时集群上运行,这将使您的工作变得更轻松。

根据这篇报道的性质,我们将涉及很多主题,而且只会略微深入;如果有更深层次的兴趣,我们会提供进一步调查的链接。 只要确保你的操作人员参与进来就行了!

Cloudera 管理器

在本书中,我们使用了Cloudera Hadoop Distribution(CDH)作为最常用的平台,它具有便捷的 QuickStart 虚拟机和强大的 Cloudera Manager 应用。 使用基于 Cloudera 的集群,Cloudera Manager 将成为(至少最初)您进入系统的主要界面,用于管理和监视集群,所以让我们来探索一下。

请注意,Cloudera Manager 拥有大量高质量的在线文档。 我们不会在这里重复此文档;相反,我们将尝试强调 Cloudera Manager 在您的开发和运营工作流中的位置,以及您可能想要接受它还是不想接受它。 Cloudera Manager 的最新版本和以前版本的文档可以通过主 Cloudera 文档页面http://www.cloudera.com/content/support/en/documentation.html访问。

付款或不付款

在对 Cloudera Manager 感到兴奋之前,有一点很重要,那就是查阅当前的文档,了解免费版本中有哪些功能可用,哪些功能需要订阅付费 Cloudera 产品。 如果你绝对想要付费版本提供的一些功能,但又不能或不想为订阅服务付费,那么 Cloudera Manager,甚至整个 Cloudera 发行版,可能都不太适合你。 我们将在第 11 章中回到这个主题。

使用 Cloudera Manager 进行集群管理

使用 QuickStart VM 不会很明显,但 Cloudera Manager 是用于管理集群中所有服务的主要工具。 如果您想启用一项新服务,您将使用 Cloudera Manager。 要更改配置,您需要 Cloudera Manager。 要升级到最新版本,您将再次需要 Cloudera Manager。

即使集群的主要管理是由操作人员处理的,作为开发人员,您可能仍然希望熟悉 Cloudera Manager 界面,以便查看集群到底是如何配置的。 如果您的作业运行缓慢,那么查看 Cloudera Manager 以了解当前的配置情况很可能是您的第一步。 Cloudera Manager Web 界面的默认端口为7180,因此主页通常通过 URL(如http://<hostname>:7180/cmf/home)连接,如以下屏幕截图所示:

Cluster management using Cloudera Manager

Cloudera Manager 主页

查看界面是值得的;但是,如果您要连接具有管理员权限的用户帐户,请小心!

单击Clusters链接,这将展开,给出当前由 Cloudera Manager 实例管理的集群的列表。 这应该告诉您单个 Cloudera Manager 实例可以管理多个集群。 这非常有用,特别是在开发和生产中有许多集群的情况下。

对于每个扩展的集群,将有当前在集群上运行的服务的列表。 单击一项服务,然后您将看到其他选项列表。 选择配置,您可以开始浏览该特定服务的详细配置。 单击Actions,您将获得一些特定于服务的选项;这通常包括停止、启动、重新启动和管理服务。

单击Hosts选项而不是Clusters,您可以开始深入查看 Cloudera Manager 管理的服务器,并从那里查看在每个服务器上部署了哪些服务组件。

Cloudera 管理器和其他管理工具

最后一条评论可能会提出一个问题:Cloudera Manager 如何与其他系统管理工具集成? 鉴于我们早先关于 DevOps 理念的重要性的评论,它与 DevOps 环境中受欢迎的工具的集成情况如何?

诚实的回答是:并不总是很好。 尽管主 Cloudera Manager 服务器本身可以由自动化工具(如 Pupet 或 Chef)管理,但有一个明确的假设是,Cloudera Manager 将控制 Cloudera Manager 在其集群中包含的所有主机上安装和配置 Cloudera Manager 所需的所有软件。 对于一些管理员来说,这使得 Cloudera Manager 背后的硬件看起来像一个大的黑匣子;他们可能控制基本操作系统的安装,但未来配置基线的管理完全由 Cloudera Manager 管理。 这里没有什么可做的;它就是这样-为了获得 Cloudera Manager 的好处,它会将自身作为一个新的管理系统添加到您的基础架构中,而这与您更广泛的环境的契合度将视具体情况而定。

使用 Cloudera Manager 进行监控

在系统监控方面也可以提出类似的观点,因为 Cloudera Manager 在概念上也是一个复制点。 但是开始在界面上单击,很快就会发现 Cloudera Manager 提供了一套极其丰富的工具来评估托管集群的运行状况和性能。

从绘制 Impala 查询的相对性能图,到显示 Yarn 应用的作业状态,再到提供存储在 HDFS 上的块的低级数据,所有这些都可以在一个界面中完成。 我们将在本章后面讨论 Hadoop 上的故障排除是如何具有挑战性的,但 Cloudera Manager 提供的单点可见性是评估集群运行状况或性能的一个很好的工具。 我们将在本章后面更详细地讨论监控。

查找配置文件

当运行由 Cloudera Manager 管理的集群时,首先面临的困惑之一就是试图查找该集群使用的配置文件。 在产品的普通 Apache 发行版中,比如核心 Hadoop,通常会有文件存储在/etc/hadoop中,类似地,/etc/hive存储在 hive 中,/etc/oozie存储在 oozie 中,依此类推。

然而,在 Cloudera Manager 管理的集群中,每次重新启动服务时都会重新生成配置文件,并且配置文件不是位于文件系统的/etc位置,而是位于/var/run/cloudera-scm-agent-process/<pid>-<task name>/,其中最后一个目录的名称可能是7007-yarn-NODEMANAGER。 对于任何习惯于使用早期 Hadoop 集群或其他不做此类操作的发行版的人来说,这可能看起来很奇怪。 但在 Cloudera Manager 控制的集群中,使用 Web 界面浏览配置通常比查找底层配置文件更容易。 哪种方法最好? 这有点哲理,每个团队都需要决定哪一个最适合他们。

Cloudera Manager API

我们只给出了 Cloudera Manager 的最高级别概述,在这样做的过程中,完全忽略了一个可能对某些组织非常有用的领域:Cloudera Manager 提供了一个允许将其功能集成到其他系统和工具中的 API。 如果您可能对此感兴趣,请参考文档。

Cloudera Manager 锁定

这就把我们带到了围绕 Cloudera Manager 的整个讨论中隐含的观点:它确实在一定程度上锁定了 Cloudera 及其发行版。 这种锁定可能只以某些方式存在;例如,代码应该可以跨集群移植,模数是关于不同底层版本的常见警告-但是集群本身可能不容易重新配置为使用不同的发行版。 假设切换发行版将是一个完全的删除/重新格式化/重新安装活动。

我们并不是说不要使用它,而是您需要注意 Cloudera Manager 的使用带来的锁定。 对于几乎没有专门的运营支持或现有基础设施的小型团队来说,Cloudera Manager 为您提供的重要功能可能会盖过这种锁定的影响。

对于较大的团队或在与现有工具和流程集成更重要的环境中工作的团队来说,决策可能不那么明确。 查看 Cloudera Manager,与您的运营人员讨论,确定最适合您的产品。

请注意,可以手动下载并安装 Cloudera 发行版的各种组件,而无需使用 Cloudera Manager 来管理集群及其主机。 对于一些用户来说,这可能是一个有吸引力的中间选择,因为可以使用 Cloudera 软件,但部署和管理可以内置到现有的部署和管理工具中。 这也可能是避免前面提到的付费 Cloudera 支持级别的额外费用的一种方式。

Ambari-开源替代方案

Ambari 是一个 Apache 项目(http://ambari.apache.org),从理论上讲,它提供了 Cloudera Manager 的开源替代方案。 它是 Hortonworks 发行版的管理控制台。 在撰写本文时,Hortonworks 的员工也是项目的绝大多数贡献者。

考虑到 Ambari 的开源特性,人们可以预料到,它依赖于其他开源产品,如 Pupet 和 Nagios,来提供对其托管集群的管理和监控。 它还具有类似于 Cloudera Manager 的高级功能,即安装、配置、管理和监视 Hadoop 集群以及其中的组件服务。

了解 Ambari 项目是件好事,因为选择不仅仅是完全锁定 Cloudera 和 Cloudera Manager,还是手动管理集群。 Ambari 提供了一个图形化的工具,随着它的成熟,它可能值得考虑,甚至可以参与进来。 在 HDP 集群上,可通过http://<hostname>:8080/#/main/dashboard访问与前面所示的 Cloudera Manager 主页相当的 Ambari UI,其屏幕截图如下所示:

Ambari – the open source alternative

安巴里

Hadoop 2 世界中的操作

正如在第 2 章Storage中提到的,Hadoop2 中对 HDFS 所做的一些最重要的更改涉及其容错性和与外部系统的更好集成。 这不仅仅是出于好奇,尤其是 NameNode 的高可用性特性,从 Hadoop 1 开始就对集群的管理产生了巨大的影响。在 2012 年左右糟糕的过去,Hadoop 集群的运营准备的很大一部分都是围绕 NameNode 故障的缓解和恢复过程来构建的。 如果在 Hadoop1 中 NameNode 死了,并且您没有 HDFSfsimage元数据文件的备份,那么您基本上就失去了对所有数据的访问权限。 如果元数据永久丢失,那么数据也会永久丢失。

Hadoop2 增加了内置的 NameNode HA 和使其工作的机制。 此外,还有一些组件,如进入 HDFS 的 NFS 网关,这使得它成为一个更加灵活的系统。 但这种额外的能力确实是以牺牲更多的活动部件为代价的。 要启用 NameNode HA,JournalManager 和 FailoverController 中还有其他组件,并且 NFS 网关需要特定于 Hadoop 的 portmap 和 nfsd 服务实现。

Hadoop2 现在还拥有个与外部服务的广泛的其他集成点,以及在其上运行的更广泛的应用和服务选择。 因此,从操作的角度看 Hadoop2 可能会很有用,因为它牺牲了 Hadoop1 的简单性,换取了额外的复杂性,从而提供了更强大的平台。

资源共享

在 Hadoop1 中,人们必须考虑资源共享的唯一时刻是考虑将哪个调度器用于 MapReduce JobTracker。 由于所有作业最终都被转换为 MapReduce 代码,因此在 MapReduce 级别拥有资源共享策略通常足以管理大量集群工作负载。

Hadoop 2 和 Yarn 改变了这一局面。 除了运行许多 MapReduce 作业之外,一个集群还可能在其他 YAR ApplicationMaster 之上运行许多其他应用。 TEZ 和 Spark 本身就是框架,它们在其提供的接口上运行额外的应用。

如果所有东西都在 Yarn 上运行,那么它提供了配置分配给应用的每个容器消耗的最大资源分配(在 CPU、内存和即将到来的 I/O 方面)的方法。 这里的主要目标是确保分配足够的资源以保持硬件的充分利用,而不会有未使用的容量或使其过载。

当非 Yarn 应用(如 Impala)在集群上运行并希望获取分配的容量片段(尤其是在 Impala 中的内存)时,事情会变得更加有趣。 比方说,如果您在相同的主机上以非 Yarn 模式运行 Spark,或者实际上任何其他分布式应用可能受益于 Hadoop 机器上的托管,也可能会发生这种情况。

基本上,在 Hadoop2 中,您需要更多地将集群看作一个多租户环境,需要更多地关注向各个租户分配资源。

这里确实没有什么灵丹妙药的建议;正确的配置将完全取决于托管的服务及其运行的工作负载。 这是另一个示例,您希望与运营团队密切合作,使用阈值执行一系列负载测试,以确定各种客户端的资源要求是什么,以及哪种方法可以提供最高的利用率和性能。 Cloudera 工程师的以下博客文章很好地概述了他们如何让 Impala 和 MapReduce 有效共存来解决这个问题:http://blog.cloudera.com/blog/2013/06/configuring-impala-and-mapreduce-for-multi-tenant-performance/

构建物理集群

在考虑硬件资源分配之前,有一个较小的要求:定义和选择用于集群的硬件。 在本节中,我们将讨论物理集群,并在下一节中继续讨论 Amazon EMR。

任何特定的硬件建议一经撰写就会过期。 我们建议仔细阅读各个 Hadoop 发行版供应商的网站,因为他们经常就当前推荐的配置撰写新文章。

我们不会告诉您需要多少内核或 GB 内存,而是从稍微高一点的级别来看硬件选择。 首先要意识到的是,运行 Hadoop 集群的主机很可能与企业的其他主机看起来非常不同。 Hadoop 针对低(ER)成本的硬件进行了优化,因此不会看到少量非常大的服务器,而应该看到更多具有更少企业可靠性功能的机器。 但不要认为 Hadoop 会在你身边的任何垃圾上运行得很好。 有可能,但最近典型 Hadoop 服务器的配置已经远离了低端市场,相反,最适合的似乎是中端服务器,在那里可以以较低的价格实现最大的核心/磁盘/内存。

与存储数据和执行应用逻辑的工作节点不同,对于运行 HDFS NameNode 或 Yar ResourceManager 等服务的主机,您还应该有不同的资源需求。 对于前者,通常对大量存储的需求要小得多,但通常需要更大的内存和可能更快的磁盘。

对于 Hadoop 工作节点,核心、内存和 I/O 这三个主要硬件类别之间的比率通常是最重要的。 这将直接为您做出有关工作负荷和资源分配的决策提供信息。

例如,许多工作负载往往会受到 I/O 限制,在主机上分配的容器数量是物理磁盘数量的许多倍,实际上可能会因为争用旋转磁盘而导致整体速度减慢。 在撰写本文时,当前的建议是 Yarn 容器的数量不超过磁盘数量的 1.8 倍。 如果您的工作负载是 I/O 受限的,那么您很可能会通过向集群添加更多主机来获得更好的性能,而不是尝试在当前主机上运行更多容器、更快的处理器或更多内存。

相反,如果您希望运行大量并发的 Impala、Spark 和其他需要大量内存的作业,那么内存可能很快就会成为压力最大的资源。 这就是为什么即使您可以从发行商那里获得通用集群的最新硬件建议,您仍然需要针对您的预期工作负载进行验证并进行相应的定制。 在小型测试集群上或在 EMR 上进行基准测试确实是不可替代的,它可以成为探索多个应用的资源需求的一个很好的平台,这些应用可以为硬件采购决策提供信息。 也许 EMR 可能是您的主要环境;如果是这样,我们将在后面的部分讨论这一点。

物理布局

如果您确实使用物理集群,您将需要考虑一些在 EMR 上基本上是透明的事情。

机架感知

对于集群来说,这些方面的第一个方面是构建机架感知,这些集群足够大,可以占用一个以上的数据中心空间。 如第 2 章存储中所述,当 HDFS 放置新文件的副本时,它会尝试将第二个副本放置在与第一个副本不同的主机上,并将第三个副本放置在多机架系统中不同的设备机架中。 此启发式方法旨在最大限度地提高恢复能力;即使整个设备机架发生故障,也至少有一个副本可用。 MapReduce 使用类似的逻辑来尝试获得更均衡的任务分布。

如果您不执行任何操作,则每台主机都将被指定为位于单个默认机架中。 但是,如果集群增长超过这一点,您将需要更新机架名称。

在幕后,Hadoop 通过执行用户提供的将节点主机名映射到机架名称的脚本来发现节点的机架。 Cloudera Manager 允许在给定主机上设置机架名称,然后在 Hadoop 调用其机架识别脚本时检索该名称。 要为主机设置机架,请单击Hosts->->Assign Rack,然后从 Cloudera Manager 主页分配机架。

发文:2013 年 2 月 10 日星期日晚上 11:00

如前所述,您的集群中可能有两种类型的硬件:运行工作器的机器和运行服务器的机器。 在部署物理集群时,您需要确定哪些服务以及这些服务的哪些子组件在哪些物理机上运行。

对于工作者来说,这通常非常简单;大多数(尽管不是全部)服务在所有工作者主机上都有一个工作者代理模型。 但是,对于主/服务器组件,需要稍微考虑一下。 如果您有三个主节点,那么如何扩展您的主 NameNode 和备用 NameNode:Yarn 资源管理器、可能的色调、几个配置单元服务器和一个 Oozie 管理器? 其中一些功能高度可用,而另一些则不是。 随着您向集群中添加越来越多的服务,您还将看到这个主服务列表大幅增长。

在理想情况下,每个服务主机可能有一台主机,但这只适用于非常大的集群;在较小的安装中,它的成本高得令人望而却步。 另外,它可能总是有点浪费。 这里也没有一成不变的规则,但一定要查看可用的硬件,并尝试将服务尽可能地分布在节点上。 例如,不要让两个 NameNode 有两个节点,然后将其他所有内容放在第三个节点上。 考虑单个主机故障的影响,并管理布局以将其降至最低。 随着集群跨多个设备机架扩展,还需要考虑如何在单机架故障中幸存下来。 Hadoop 本身对此很有帮助,因为 HDFS 将尝试确保每个数据块在至少两个机架上都有副本。 但是,例如,如果所有主节点都驻留在单个机架中,则会削弱这种类型的弹性。

升级服务

升级 Hadoop 历来是一项耗时且有一定风险的任务。 在手动部署的集群(即不受 Cloudera Manager 等工具管理的集群)上仍然是这种情况。

如果您使用的是 Cloudera Manager,那么它会将耗时的部分从活动中去掉,但不一定会带来风险。 任何升级都应始终被视为发生意外问题的可能性很高的活动,您应该安排足够的集群停机时间来应对这种意外的兴奋。 在测试集群上进行测试升级确实是无可替代的,这强调了将 Hadoop 视为环境的一个组件的重要性,该组件需要像其他组件一样被视为部署生命周期。

有时升级需要修改 HDFS 元数据,或者可能会影响文件系统。 当然,这才是真正的风险所在。 除了运行测试升级外,还要注意将 HDFS 设置为升级模式的功能,这将有效地创建升级前文件系统状态的快照,并将一直保留到升级完成。 此非常有用,因为即使是出现严重错误并损坏数据的升级也有可能完全回滚。

在电子病历上构建集群

Elastic MapReduce 是一种灵活的解决方案,根据需求和工作负载,可以与物理 Hadoop 集群相邻,也可以替换物理 Hadoop 集群。 正如我们到目前为止已经看到的,EMR 提供了预加载和配置了配置单元、流和 Pig 的集群,以及允许执行 MapReduce 应用的自定义 JAR 集群。

第二个要区分的是短暂生命周期和长期生命周期。 按需生成临时 EMR 集群;将数据加载到 S3 或 HDFS 中,执行一些处理工作流,存储输出结果,然后自动关闭集群。 工作流终止后,长期运行的集群将保持活动状态,并且集群仍可用于复制新数据和执行新工作流。 长时间运行的集群通常非常适合数据仓库或处理足够大的数据集,因此与临时实例相比,加载和处理数据的效率会很低。

在一份面向潜在用户的必读白皮书(可在https://media.amazonwebservices.com/AWS_Amazon_EMR_Best_Practices.pdf找到)中,亚马逊提供了一个启发式方法来估计哪种集群类型更适合使用,如下所示:

如果每天的作业数(设置集群的时间包括 Amazon S3 数据加载时间,如果使用 Amazon S3+数据处理时间)<24 小时,请考虑临时 Amazon EMR 集群或物理实例。 通过将-live 参数传递给 ElasticMapduce 命令来实例化长时间运行的实例,该命令启用了 Keep Alive 选项并禁用了自动终止。*

请注意,临时集群和长期运行的集群共享相同的属性和限制;尤其是,一旦集群关闭,HDFS 上的数据就不会持久化。

关于文件系统的注意事项

到目前为止,在我们的示例中,我们假设数据在 S3 中可用。 在本例中,存储桶作为s3n文件系统挂载在 EMR 中,并用作输入源和临时文件系统来存储计算中的中间数据。 在 S3 中,我们引入了潜在的 I/O 开销,读写等操作会触发GETPUT HTTP请求。

备注

请注意,EMR 不支持 S3 数据块存储。 S3 URI 映射到 S3n。

另一种选择是将数据加载到集群 HDFS 中,并从那里运行处理。 在这种情况下,我们确实有更快的 I/O 和数据局部性,但我们会失去持久性。 当集群关闭时,我们的数据就会消失。 根据经验,如果您正在运行临时集群,那么使用 S3 作为后端是有意义的。 在实践中,人们应该根据工作流特性进行监控和决策。 迭代的多遍 MapReduce 作业将极大地受益于 HDFS;有人可能会争辩说,对于这些类型的工作流,像 TEZ 或 Spark 这样的执行引擎会更合适。

将数据导入电子病历

将数据从 HDFS 复制到 S3 时,建议使用 s3Distcp(http://docs.aws.amazon.com/ElasticMapReduce/latest/DeveloperGuide/UsingEMR_s3distcp.html),而不是 Apache Distcp 或 Hadoop Distcp。 此方法也适用于在 EMR 内以及从 S3 到 HDFS 传输数据。 要将大量数据从本地磁盘移动到 S3,Amazon 建议使用 Jets3t 或 GNU 并行来并行化工作负载。 通常,重要的是要知道,对 S3 的 PUT 请求的上限是每个文件 5 GB。 要上传较大的文件,需要依赖分块上传(API),这是一种允许将大文件拆分成较小部分并在上传时重新组装的 https://aws.amazon.com/about-aws/whats-new/2010/11/10/Amazon-S3-Introducing-Multipart-Upload/。 也可以使用 AWS CLI 或流行的 S3CMD 实用程序等工具复制文件,但这些工具没有 AS s3Distcp 的并行优势。

EC2 实例和调整

EMR 集群的大小取决于数据集大小、文件和块的数量(确定拆分数量)和工作负载类型(尽量避免在任务内存耗尽时溢出到磁盘)。 根据经验,好的大小应该最大限度地提高并行度。 每个实例的映射器和减少器的数量以及每个 JVM 守护进程的堆大小通常由 EMR 在可用资源发生变化的情况下提供和调优集群时配置。

←T0 抯集群调整

除了前面针对在 EMR 上运行的集群的注释之外,在任何类型的集群上运行工作负载时,还需要记住一些一般想法。 当然,当在 EMR 之外运行时,这将更加明确,因为它通常抽象出一些细节。

JVM 注意事项

您应该运行 64 位版本的 JVM 并使用服务器模式。 这个可能需要更长的时间来生成优化的代码,但它也使用了更积极的策略,并将随着时间的推移重新优化代码。 这使得它更适合长期运行的服务,比如 Hadoop 进程。

确保为 JVM 分配足够的内存,以防止过度频繁的垃圾收集(GC)暂停。 并发标记和清除收集器是目前针对 Hadoop 测试和推荐最多的收集器。 自从 JDK7 引入以来,垃圾优先(G1)收集器已经成为许多其他工作负载的 GC 选项,因此值得关注推荐的最佳实践的发展。 这些选项可以在 Cloudera Manager 的每个服务的配置部分中配置为自定义 Java 参数。

小文件问题

在考虑服务协同定位时,您将考虑将堆分配给工作节点上的个 Java 进程。 但是关于 NameNode 有一个特殊的情况,您应该知道:小文件问题。

Hadoop 针对具有大块大小的超大型文件进行了优化。 但有时特定的工作负载或数据源会将许多小文件推送到 HDFS 上。 这很可能是次优的,因为它表明每次处理一个块的每个任务在完成之前只会读取少量数据,从而导致效率低下。

拥有许多小文件也会消耗更多的 NameNode 内存;它在内存中保存从文件到块的映射,因此保存每个文件和块的元数据。 如果文件数量和数据块数量快速增加,那么 NameNode 内存使用量也会增加。 这可能只影响系统的一个子集,因为在撰写本文时,1 GB 内存可以支持 200 万个文件或块,但是使用 2 或 4 GB 的默认堆大小,很容易达到这个限制。 如果 NameNode 需要开始非常积极地运行垃圾收集,或者最终耗尽内存,那么您的集群将非常不健康。 缓解方法是将更多堆分配给 JVM;较长期的方法是将许多小文件合并为数量较少的较大文件。 理想情况下,使用可拆分的压缩编解码器进行压缩。

映射和减少优化

映射器和减法器都提供了优化性能的区域;这里有几点需要考虑:

  • 映射器的数量取决于分割的数量。 当文件小于默认块大小或使用不可拆分格式压缩时,映射器的数量将等于文件的数量。 否则,映射器的数量由每个文件的总大小除以块大小得出。
  • 压缩映射器输出以减少对磁盘的写入并增加 I/O。LZO 是执行此任务的好格式。
  • 避免溢出到磁盘:映射器应该有足够的内存来保留尽可能多的数据。
  • 减速器数量:建议您使用的减速器数量少于减速器总容量(这样可以避免执行等待)。

安全性

一旦你构建了一个集群,你首先想到的就是如何保护它,对吗? 别担心,大多数人都不担心。但是,随着 Hadoop 从研究部门的内部分析转变为直接驱动关键系统,它不能被忽视太久。

保护 Hadoop 不是心血来潮或没有经过重大测试就能完成的事情。 我们不能就这一问题给出详细的建议,也不能强烈强调认真对待和妥善处理这一问题的必要性。 这可能会耗费时间,可能会花费金钱,但要权衡一下集群受损的成本。

安全性也是一个比 Hadoop 集群大得多的话题。 我们将探索 Hadoop 中提供的一些安全特性,但是您确实需要一个连贯的安全策略,这些离散的组件都适合这些安全策略。

Hadoop 安全模型的演变

在 Hadoop1 中,实际上没有安全保护,因为提供的安全模型有明显的攻击向量。 您用来连接到集群的 Unix 用户 ID 被认为是有效的,并且您拥有该用户的所有权限。 显然,这意味着在可以访问集群的主机上拥有管理访问权限的任何人都可以有效地模拟任何其他用户。

这导致了所谓的“头节点”访问模型的发展,根据该模型,Hadoop 集群与除头节点之外的所有主机隔离,所有对集群的访问都通过这个集中控制的节点进行中介。 对于缺乏真正的安全模型来说,这是一种有效的缓解措施,即使在使用更丰富的安全方案的情况下,这仍然是有用的。

超越基本授权

核心 Hadoop 增加了额外的安全功能,解决了之前的问题。 具体而言,它们涉及以下内容:

  • 集群可能要求用户通过 Kerberos 进行身份验证,并证明他们是他们所说的那个人。
  • 在安全模式下,集群还可以使用 Kerberos 进行所有节点到节点的通信,从而确保所有通信节点都经过身份验证,并防止恶意节点尝试加入集群。
  • 为了简化管理,可以将用户收集到组中,可以针对这些组定义数据访问权限。 这称为基于角色的访问控制(RBAC),它是拥有多个用户的安全集群的先决条件。 可以从公司系统(如 LDAP 或 Active Directory)检索用户-组映射。
  • HDFS 可以应用 ACL 来取代当前受 Unix 启发的所有者/组/世界模型。

这些功能为 Hadoop 提供了比过去更强大的安全态势,但是社区正在快速发展,并且出现了更多专门的 Apache 项目来解决特定的安全领域。

ApacheSentryHadoop是一个为 https://sentry.incubator.apache.org 数据和服务提供更细粒度授权的系统。 其他服务构建哨兵映射,例如,这不仅允许对特定的 HDFS 目录施加特定限制,而且还允许对实体(如配置单元表)施加特定限制。

Sentry 专注于为 Hadoop 安全性的内部细粒度方面提供更丰富的工具,而 Apache Knox(http://knox.apache.org)提供了到 Hadoop 的安全网关,该网关与外部身份管理系统集成,并提供访问控制机制来允许或禁止访问特定的 Hadoop 服务和操作。 它通过向 Hadoop 提供一个仅支持 REST 的接口并保护对此 API 的所有调用来实现这一点。

Hadoop 安全的未来

在 Hadoop 世界中还有许多其他的发展。 核心 Hadoop2.5 向 HDFS 添加了扩展的文件属性,可用作附加访问控制机制的基础。 未来的版本将包含更好地支持传输中和静态数据加密的功能,由英特尔(https://github.com/intel-hadoop/project-rhino/)领导的犀牛计划(Project Rhino)正在构建对文件系统加密模块、安全文件系统以及在某种程度上更全面的密钥管理基础设施的更丰富支持。

Hadoop 发行版供应商正在迅速采取行动,将这些功能添加到他们的发行版中,因此,如果您关心安全性(您关心的,不是吗!),那么请参考文档以了解您的发行版的最新版本。 新的安全功能正在添加,甚至是即时更新,而且在重大升级之前不会推迟。

使用安全集群的后果

在用现在可用的和即将到来的所有安全好处来取笑你之后,给你一些警告才是公平的。 安全性通常很难正确实现,错误地使用缺陷部署带来的安全感通常比知道自己没有安全性更糟糕。

然而,即使您操作正确,运行安全集群也会产生后果。 这无疑增加了管理员(通常也是用户)的工作难度,因此肯定会有开销。 特定的 Hadoop 工具和服务的工作方式也会有所不同,具体取决于集群上采用的安全性。

我们在数据生命周期管理中讨论了 Oozie,它在幕后使用自己的委派令牌。 这允许 Oozie 用户提交作业,然后代表最初提交的用户执行这些作业。 在只使用基本授权机制的集群中,这很容易配置,但在安全集群中使用 Oozie 需要向工作流定义和常规 Oozie 配置添加额外的逻辑。 对于 Hadoop 或 Oozie 来说,这不是问题;然而,与 Hadoop2 中 HDFS 更好的 HA 特性带来的额外复杂性类似,更好的安全机制只会带来您需要考虑的成本和后果。

监控

在本章的早些时候,我们讨论了 Cloudera Manager 作为可视化的监控工具,并暗示它也可以通过编程方式与其他监控系统集成。 但是,在将 Hadoop 插入任何监控框架之前,有必要考虑一下对 Hadoop 集群进行操作监控意味着什么。

Hadoop-故障无关紧要

传统的系统监控往往是一个相当二进制的工具;一般来说,要么某些东西在工作,要么不工作。主机是活的还是死的,Web 服务器是否响应。但在 Hadoop 世界里,事情有点不同;重要的是服务的可用性,即使特定的硬件或软件发生故障,这仍然可以被视为实时的。 如果单个工作节点出现故障,任何 Hadoop 集群都不会出现问题。 从 Hadoop2 开始,如果配置了 HA,甚至服务器进程(如 NameNode)的故障也不应该成为问题。 因此,对 Hadoop 的任何监视都需要考虑服务运行状况,而不是特定主机的运行状况,这一点应该不重要。 全天候寻呼机的操作人员不会高兴在凌晨 3 点被寻呼时发现 10,000 个集群中的一个工作节点出现故障。 的确,一旦集群的规模超过了某一点,单个硬件出现故障几乎是家常便饭。

监控集成

您将不会构建自己的监控工具;相反,您可能希望与现有工具和框架集成。 对于流行的开源监控工具,如 Nagios 和 Zabbix,有多个示例模板可以集成 Hadoop 的服务范围和特定于节点的指标。

这可以实现前面所暗示的那种分离;Yarn 资源管理器的故障将是一个高危急事件,很可能会导致向操作人员发送警报,但应该只捕获特定主机上的高负载,而不会导致警报被触发。? 这就提供了在发生不好的事情时触发警报的双重功能,此外,它还可以捕获和提供随时间推移深入研究系统数据以进行趋势分析所需的信息。

Cloudera Manager 提供了 REST 接口,这是另一个集成点,Nagios 等工具可以根据该接口集成和提取 Cloudera Manager 定义的服务级别指标,而不必定义自己的指标。

对于构建在 IBM Tivoli 或 HP OpenView 等框架之上的重量级企业监控基础设施,Cloudera Manager 还可以通过这些系统收集的 SNMP 陷阱传递事件。

应用级指标

有时,您可能还希望您的应用收集可以在系统内集中捕获的指标。 不同的计算模型实现这一点的机制会有所不同,但最著名的是 MapReduce 中可用的应用计数器。

当 MapReduce 作业完成时,它会输出许多计数器,这些计数器由系统在整个作业执行过程中收集,这些计数器处理映射任务的数量、写入的字节数、失败的任务等指标。 您还可以编写特定于应用的指标,这些指标将与系统计数器一起使用,并在整个 map/Reduce 执行过程中自动聚合。 首先定义一个 Java 枚举,并在其中命名您需要的指标,如下所示:

public enum AppMetrics{
  MAX_SEEN,
  MIN_SEEN,
  BAD_RECORDS 
};

然后,在 Map 或 Reduce 实现的 Map、Reduce、Setup 和 Cleanup 方法中,您可以执行类似以下操作来将计数器递增 1:

Context.getCounter(AppMetrics.BAD_RECORDS).increment(1);

有关该机制的更多详细信息,请参考org.apache.hadoop.mapreduce.Counter接口的 JavaDoc。

故障排除

监视和记录计数器或附加信息固然不错,但知道如何在排除应用故障时真正找到所需的信息可能会让人望而生畏。 在本节中,我们将了解 Hadoop 如何存储日志和系统信息。 我们可以区分三种类型的原木,如下所示:

  • Yarn 应用,包括 MapReduce 作业
  • 守护程序日志(NameNode 和 ResourceManager)
  • 记录非分布式工作负载的服务,例如,HiveServer2 记录到/var/log

除了这些日志类型之外,Hadoop 还在文件系统(存储可用性、复制系数和块数量)和系统级别公开了许多指标。 如前所述,Apache Ambari 和 Cloudera Manager 作为前端都做得很好,它们集中了对调试信息的访问。 但是,在幕后,每个服务要么记录到 HDFS,要么记录到单节点文件系统。 此外,YAR、MapReduce 和 HDFS 通过 Web 接口和编程 API 公开它们的日志文件和指标。

日志记录级别

默认情况下,Hadoop 将消息记录到 Log4j。 Log4j 是通过类路径中的log4j.properties配置的。 此文件定义记录的内容和使用的布局:

log4j.rootLogger=${root.logger}
root.logger=INFO,console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{2}: %m%n

缺省的根记录器是INFO,console,它将级别为INFO及以上的所有消息记录到控制台的stderr。 部署在 Hadoop 上的单个应用可以发布它们自己的log4j.properties,并根据需要设置它们发出的日志的级别和其他属性。

Hadoop 守护进程有一个网页可以获取和设置任何 Log4j 属性的日志级别。 此接口由每个服务 Web UI 中的/LogLevel端点公开。 要启用ResourceManager类的调试日志记录,我们将访问http://resourcemanagerhost:8088/LogLevel,屏幕截图如下所示:

Logging levels

获取并设置 ResourceManager 上的日志级别

或者,Yarndaemonlog <host:port>命令与service /LogLevel端点连接。 我们可以使用–getlevel <property>参数检查ResourceManager类的与mapreduce.map.log.level相关联的级别,如下所示:

$ hadoop daemonlog -getlevel localhost.localdomain:8088  mapreduce.map.log.level 
Connecting to http://localhost.localdomain:8088/logLevel?log=mapreduce.map.log.level Submitted Log Name: mapreduce.map.log.level Log Class: org.apache.commons.logging.impl.Log4JLogger Effective level: INFO 

可以使用-setlevel <property> <level>选项修改有效级别:

$ hadoop daemonlog -setlevel localhost.localdomain:8088  mapreduce.map.log.level  DEBUG
Connecting to http://localhost.localdomain:8088/logLevel?log=mapreduce.map.log.level&level=DEBUG
Submitted Log Name: mapreduce.map.log.level
Log Class: org.apache.commons.logging.impl.Log4JLogger
Submitted Level: DEBUG
Setting Level to DEBUG ...
Effective level: DEBUG

请注意,此设置将影响ResourceManager类生成的所有日志。 这包括系统生成的条目以及在 Yarn 上运行的应用生成的条目。

访问日志文件

根据分布情况,日志文件位置和命名约定可能会有所不同。 Apache Ambari 和 Cloudera Manager 集中访问服务和单个应用的日志文件。 在 Cloudera 的 QuickStart VM 上,可以在http://localhost.localdomain:7180/cmf/hardware/hosts/1/processes处找到当前运行的进程及其日志文件的链接、stderrstdout通道的概览,屏幕截图如下所示:

Access to logfiles

访问 Cloudera Manager 中的日志资源

Ambari 通过 HDP 沙盒上http://127.0.0.1:8080/#/main/services处的Services仪表板提供了类似的概览,屏幕截图如下所示:

Access to logfiles

访问 Apache Ambari 上的日志资源

非分布式日志通常位于每个集群节点的/var/log/<service>下。 Yarn 容器和 MRv2 原木的位置也取决于分布。 在 CDH5 上,这些资源在 HDFS 中的/tmp/logs/<user>下可用。

访问分布式日志的标准方式是通过命令行工具或使用服务 Web UI。

例如,该命令如下所示:

$ yarn application -list -appStates ALL 

前面的命令将列出所有正在运行和重试的 Yarn 应用。 任务列中的 URL 指向显示任务日志的 Web 界面,如下所示:

14/08/03 14:44:38 INFO client.RMProxy: Connecting to ResourceManager at localhost.localdomain/127.0.0.1:8032 Total number of applications (application-types: [] and states: [NEW, NEW_SAVING, SUBMITTED, ACCEPTED, RUNNING, FINISHED, FAILED, KILLED]):4                 Application-Id      Application-Name      Application-Type        User       Queue               State         Final-State         Progress                         Tracking-URL application_1405630696162_0002  PigLatin:DefaultJobName             MAPREDUCE    cloudera  root.cloudera            FINISHED           SUCCEEDED             100%  http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0002 application_1405630696162_0004  PigLatin:DefaultJobName             MAPREDUCE    cloudera  root.cloudera            FINISHED           SUCCEEDED             100%  http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0004 application_1405630696162_0003  PigLatin:DefaultJobName             MAPREDUCE    cloudera  root.cloudera            FINISHED           SUCCEEDED             100%  http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0003 application_1405630696162_0005  PigLatin:DefaultJobName             MAPREDUCE    cloudera  root.cloudera            FINISHED           SUCCEEDED             100%  http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0005 

例如,指向属于用户 Cloudera 的任务的链接http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0002是存储在hdfs:///tmp/logs/cloudera/logs/application_1405630696162_0002/下的内容的前端。

在以下部分中,我们将概述可用于不同服务的 UI。

备注

使用–log-uri s3://<bucket>选项配置 EMR 集群将确保 Hadoop 日志复制到s3://<bucket>位置。

ResourceManager、NodeManager 和 Application Manager

在 YAINE 上,ResourceManager web UI 提供 Hadoop 集群的信息和常规作业统计数据、正在运行/完成/失败的作业,以及作业历史日志文件。 默认情况下,UI 显示在http://<resourcemanagerhost>:8088/,可以在下面的屏幕截图中看到:

ResourceManager, NodeManager, and Application Manager

资源管理器

应用

在左侧栏上,可以查看感兴趣的应用状态:NEWSUBMITTEDACCEPTEDRUNNINGFINISHINGFINISHEDFAILEDKILLED。 根据应用状态,以下信息可用:

  • 应用 ID
  • 提交用户
  • 应用名称
  • 应用所在的调度程序队列
  • 开始/结束时间和状态
  • 链接到应用历史记录的跟踪 UI

此外,Cluster Metrics视图还提供以下信息:

  • 整体应用状态
  • 运行中的集装箱数量
  • 内存使用情况
  • 节点状态

个节点

Nodes视图是 NodeManager 服务菜单的前端,它显示有关节点正在运行的应用的运行状况和位置信息,如下所示:

Nodes

节点状态

集群的每个单独节点通过其自己的 UI 在主机级别公开更多信息和统计信息。 这些信息包括节点上运行的 Hadoop 版本、节点上有多少可用内存、节点状态以及正在运行的应用和容器列表,如以下屏幕截图所示:

Nodes

单节点信息

调度器

以下屏幕截图显示了 Scheduler 窗口:

Scheduler

计划安排者 / 调度机 / 调度程序 / 制表人

MapReduce

虽然 MapReducev1 和 MapReducev2 中提供了相同的信息和日志记录详细信息,但访问方式略有不同。

MapReduce v1

以下屏幕截图显示了 MapReduce JobTracker UI:

MapReduce v1

作业跟踪器 UI

作业跟踪器 UI 默认在http://<jobtracker>:50070中提供,它显示有关当前正在运行的所有作业以及停用的 MapReduce 作业的信息、集群资源和运行状况的摘要以及调度信息和完成百分比,如以下屏幕截图所示:

MapReduce v1

作业详细信息

对于每个正在运行和停用的作业,都有详细信息可用,包括其 ID、所有者、优先级、任务分配和映射器的任务启动。 单击jobid链接将进入作业详细信息页面-与mapred job –list命令显示的 URL 相同。 此资源提供有关 map 和 Reduce 任务的详细信息,以及作业、文件系统和 MapReduce 级别的常规计数器统计信息;其中包括使用的内存、读/写操作数以及读写字节数。

对于每个映射和减少操作,JobTracker 会显示总任务、挂起任务、正在运行任务、已完成任务和失败任务,如以下屏幕截图所示:

MapReduce v1

作业任务概述

单击工单表格中的链接将进入任务和任务尝试级别的进一步概述,如以下屏幕截图所示:

MapReduce v1

任务尝试次数

从最后一页开始,我们可以访问每个任务尝试的日志,包括每个单独 TaskTracker 主机上成功任务和失败/终止任务的日志。 此日志包含有关 MapReduce 作业状态的最精细信息,包括 Log4j 附加器的输出以及通过管道传输到stdoutstderr通道以及syslog的输出,如以下屏幕截图所示:

MapReduce v1

TaskTracker 日志

MapReduce v2(Yarn)

正如我们在第 3 章Processing-MapReduce 以及之后的中看到的,对于 YAIN,MapReduce 只是众多可以部署的处理框架之一。 回想一下前面的章节,JobTracker 和 TaskTracker 服务分别被 ResourceManager 和 NodeManager 取代。 因此,来自 YAR 的服务 UI 和日志文件都比 MapReducev1 更通用。

资源管理器中显示的application_1405630696162_0002名称对应于具有job_1405630696162_0002ID 的 MapReduce 作业。该应用 ID 属于在容器内运行的任务,单击它将显示 MapReduce 作业的概览,并允许从任一阶段向下钻取各个任务,直至到达单任务日志,如以下屏幕截图所示:

MapReduce v2 (YARN)

包含 MapReduce 作业的 Yarn 应用

作业历史服务器

Year 附带了一个 JobHistoryREST 服务,该服务公开有关已完成应用的详细信息。 目前,它只支持 MapReduce,并提供有关已完成作业的信息。 这包括提交作业的作业最终状态SUCCESSFULFAILED、MAP 和 Reduce 任务总数以及时间信息。

http://<jobhistoryhost>:19888/jobhistory提供了一个 UI,如以下截图所示:

JobHistory Server

作业历史记录界面

单击每个作业 ID 将转到 Yarn 应用屏幕截图中显示的 MapReduce 作业 UI。

NameNode 和 DataNode

Hadoop 分布式文件系统(HDFS)的 Web 界面通常显示有关 NameNode 本身以及文件系统的信息。

默认情况下位于http://<namenodehost>:50070/,如下图所示:

NameNode and DataNode

NameNode UI

概述菜单显示有关 DFS 容量和使用情况以及数据块池状态的 NameNode 信息,并提供 DataNode 运行状况和可用性状态的摘要。 此页中包含的信息在很大程度上等同于命令行提示符中显示的信息:

$ hdfs dfsadmin –report

DataNodes 菜单提供有关每个节点状态的更详细信息,并提供单个主机级别的深入查看,包括可用节点和已停用的节点,如以下屏幕截图所示:

NameNode and DataNode

数据节点 UI

摘要

这是围绕运行可操作 Hadoop 集群的考虑因素进行的短暂停留。 我们并没有试图将开发人员转变为管理员,但希望更广阔的视角能帮助您帮助您的运营人员。 我们特别讨论了以下主题:

  • Hadoop 如何天然地适合 DevOps 方法,因为它的多层复杂性意味着开发人员和运营人员之间不可能也不希望有实质性的知识差距
  • Cloudera Manager,以及它如何成为一款出色的管理和监控工具;不过,如果您有其他企业工具,并且存在供应商锁定风险,那么它可能会导致集成问题
  • Ambari,Cloudera Manager 的 Apache 开源替代品,以及如何在 Hortonworks 发行版中使用它
  • 如何考虑为物理 Hadoop 集群选择硬件,以及这如何自然地符合 Hadoop 2 世界中可能的多个工作负载如何在共享资源上和平共处的考虑因素
  • 启动和使用 EMR 集群的不同注意事项,以及这如何既是物理集群的附件,又是物理集群的替代方案
  • Hadoop 安全生态系统,它是一个发展非常迅速的领域,今天可用的功能比几年前要好得多,而且仍然有很多东西即将出现
  • 监控 Hadoop 集群,考虑在拥抱故障的 Hadoop 模型中哪些事件很重要,以及如何将这些警报和指标集成到其他企业监控框架中
  • 如何对 Hadoop 集群的问题进行故障排除,包括可能发生的情况以及如何找到信息为您的分析提供信息
  • 快速浏览 Hadoop 提供的各种 Web 用户界面,这些界面可以很好地概述系统中各个组件内发生的情况

这就是我们对 Hadoop 的深入讨论。 在最后一章中,我们将对更广泛的 Hadoop 生态系统表达一些想法,为书中没有机会介绍的有用和有趣的工具和产品提供一些指导,并建议如何参与社区。

十一、下一步要去哪里

在前面的章节中,我们研究了 Hadoop2 的许多部分以及它周围的生态系统。 然而,我们必然会受到页数的限制;有些方面我们没有尽可能深入,有些方面我们只是顺便提到,或者根本没有提到。

Hadoop 生态系统,包括发行版、Apache 和非 Apache 项目,现在是一个令人难以置信的充满活力和健康的地方。 在本章中,如果您愿意的话,我们希望用旅游指南来补充前面讨论的更详细的材料,以了解其他有趣的目的地。 在本章中,我们将讨论以下主题:

  • Hadoop 发行版
  • 其他重要的 Apache 和非 Apache 项目
  • 信息和帮助的来源

当然,请注意,任何关于生态系统的概述都会受到我们的兴趣和偏好的影响,而且从它写出来的那一刻起就已经过时了。 换句话说,千万不要认为这就是所有可用的东西,而应该把它看作是一种刺激食欲的行为。

备选分配

在本书中,我们通常使用 Hadoop 的 Cloudera 发行版,但试图尽可能保持覆盖率发行版的独立性。 我们在本书中也提到了Hortonworks Data Platform(HDP),但这些当然不是您可用的唯一分发选择。

在环顾四周之前,让我们考虑一下您是否需要分发。 完全可以访问 Apache 网站,下载您感兴趣的项目的源代码 tarball,然后一起构建它们。 但是,考虑到版本依赖关系,这可能会消耗比您预期更多的时间。 潜在的,更是如此。 此外,最终产品在操作部署和管理的工具或脚本方面可能会缺少一些改进。 对于大多数用户来说,这些方面就是采用现有 Hadoop 发行版是自然而然的选择的原因。

关于免费和商业扩展的说明--作为一个拥有相当自由许可的开源项目,发行版创建者也可以自由地使用专有扩展来增强 Hadoop,这些扩展可以是免费的开源产品,也可以是商业产品。

这可能是一个有争议的问题,因为一些开源倡导者不喜欢任何成功的开源项目的商业化;对他们来说,商业实体似乎是在免费享用开源社区的成果,而不必为自己构建它。 其他人认为这是灵活的 Apache 许可的一个健康方面;基础产品永远是免费的,个人和公司可以选择是否使用商业扩展。 我们不会给出任何一种判断,但请注意,这是你几乎肯定会遇到的另一个争议。

因此,您需要决定是否需要发行版,如果需要发行版,原因是什么,与滚动自己的发行版相比,哪些特定方面会给您带来最大的好处? 您是想要一个完全开源的产品,还是愿意为商业扩展付费? 考虑到这些问题,让我们来看看几个主要的发行版。

适用于 Hadoop 的 Cloudera 发行版

您将会熟悉 Cloudera 发行版(http://www.cloudera.com),因为它在本书中一直在使用。 CDH 是第一个广泛可用的替代发行版,其可用软件的广度、经过验证的质量水平以及其免费成本使其成为非常受欢迎的选择。

最近,Cloudera 一直在积极地将其添加到发行版中的产品扩展到核心 Hadoop 项目之外。 除了 Cloudera Manager 和 Impala(都是 Cloudera 开发的产品)之外,它还添加了其他工具,如 Cloudera search(基于 Apache Solr)和 Cloudera Navigator(一种数据治理解决方案)。 CDH 5 之前的版本更侧重于发行版的集成优势,而版本 5(可能还会更高)正在基础 Apache Hadoop 项目上添加越来越多的功能。

除了培训和咨询服务外,Cloudera 还为其产品提供商业支持。 详情可在公司网页上找到。

Hortonworks 数据平台

2011 年,雅虎! 负责 Hadoop 如此多开发的部门被剥离出来,成立了一家名为 Hortonworks 的新公司。 他们还制作了自己的预集成 Hadoop 发行版,名为Hortonworks data Platform(HDP),可在http://hortonworks.com/products/hortonworksdataplatform/上获得。

HDP 在概念上与 CDH 相似,但这两种产品在侧重点上有所不同。 Hortonworks 强调 HDP 是完全开源的,包括我们在第 10 章运行 Hadoop 集群中简要讨论的管理工具 Ambari。 他们还通过 HDP 对 Talend Open Studio 等工具的支持,将 HDP 定位为关键的集成平台。 Hortonworks 不提供专有软件;相反,它的商业模式侧重于为该平台提供专业服务和支持。

Cloudera 和 Hortonworks 都是风投支持的公司,拥有重要的工程专业知识;这两家公司都雇佣了 Hadoop 最多产的贡献者。 然而,底层技术由相同的 Apache 项目组成;不同的因素是它们的打包方式、使用的版本以及这些公司提供的额外增值产品。

MAPR

MapR Technologies 提供了一种不同的分发类型,尽管该公司和分发通常简称为 MapR。 Hadoop提供的发行版基于 http://www.mapr.com,但添加了许多更改和增强。

MapR 发行版的重点是性能和可用性。 例如,它是第一个为 Hadoop NameNode 和 JobTracker 提供高可用性解决方案的发行版,您会记得在第 2 章Storage中,它是核心 Hadoop 1 中的一个重大弱点。它还提供了早在 Hadoop 2 之前就与 NFS 文件系统的本地集成,这使得处理现有数据变得容易得多。 为了实现这些功能,MapR 用完全兼容 POSIX 的文件系统取代了 HDFS,该文件系统也没有 NameNode 功能,从而实现了一个没有主节点的真正分布式系统,并且声称硬件利用率比 Apache HDFS 高得多。

MapR 提供了其发行版的社区版和企业版;并不是所有的扩展都在免费产品中提供。 除了培训和咨询,该公司还提供支持服务,作为企业产品订阅的一部分。

和 REST…

Hadoop 发行版不只是年轻初创公司的领地,也不是一个静态的市场。 英特尔在 2014 年初之前一直有自己的发行版,当时它决定将自己的更改合并到 CDH 中。 IBM 有自己的发行版,名为 IBM Infsphere Big Insights,有免费版和商业版两种。 也有许多大企业推出自己的发行版的各种故事,其中一些是公开提供的,而另一些则不是。 有这么多高质量的发行版可供选择,您将不会缺少选择。

选择分配

这提出了一个问题:如何选择分发? 可以看到,可用的发行版(我们没有涵盖所有发行版)的范围从完全开源产品的方便打包和集成到它们上面的整个定制集成和分析层。 不存在总体最佳分布;请仔细考虑您的需求并考虑替代方案。 因为所有这些都提供至少一个基本版本的免费下载,所以简单地自己玩和体验这些选项是很好的。

其他计算框架

我们经常讨论,Yarn 给 Hadoop 平台带来的各种可能性。 我们详细介绍了两款新车型,Samza 和 Spark。 此外,其他更成熟的框架,如 Pig,也正在移植到该框架中。

为了更全面地了解本节中的情况,我们将通过提供一组计算模型来说明使用 Yarn 进行加工的可能性,这些计算模型目前正移植到 Yarn 之上的 Hadoop。

Apache 风暴

Storm(http://storm.apache.org)是一个(主要)用 Clojure 编程语言编写的分布式计算框架。 它使用定制的喷嘴和螺栓来定义信息源和操作,以允许对流数据进行分布式处理。 Storm 应用被设计为创建转换流的接口拓扑。 它提供与 MapReduce 作业类似的功能,不同之处在于拓扑理论上将无限期运行,直到手动终止。

虽然最初与 Hadoop 截然不同,但 Yahoo!正在开发 Yarn 端口。 可以在https://github.com/yahoo/storm-yarn找到。

== 060 _ Apache 吉拉夫

Giraph 起源于 Google 的 Pregel 文件(可以在http://kowshik.github.io/JPregel/pregel_paper.pdf找到)的开源实现。 Gigraph 和 Pregel 都受到了 Valiant 在 1990 年提出的分布式计算的批量同步并行(BSP)模型的启发。 Giraph 增加了几个特性,包括主计算、分片聚合器、面向边缘的输入和内核外计算。 Yarn 端口可在https://issues.apache.org/jira/browse/GIRAPH-13找到。

Apache Hama

HAMA 是一个顶级的 Apache 项目,与我们到目前为止遇到的其他方法一样,它的目标是解决 MapReduce 在迭代编程方面的弱点。 与前面提到的 Giraph 类似,HAMA 实现了 BSP 技术,并且受到了 Pregel 论文的很大启发。 Yarn 端口可以在https://issues.apache.org/jira/browse/HAMA-431处找到。

其他有趣的项目

无论您是使用捆绑的发行版还是坚持使用基础 Apache Hadoop 下载,您都会遇到许多对其他相关项目的引用。 我们在本书中已经介绍了其中的几个,如 Hive、Samza 和 Crunch;现在我们将重点介绍其他一些。

请注意,这篇报道试图指出亮点(从作者的角度),并让人品尝到可用的项目类型的广度。 正如前面提到的,保持警惕,因为将会有新的不断推出。

HBase

也许我们在这本书中没有介绍的最受欢迎的 Apache Hadoop 相关项目是 HBase(http://hbase.apache.org)。 基于 Google 在一篇学术论文中公布的 Bigtable 数据存储模型(听起来熟悉吗?),HBase 是一个位于 HDFS 之上的非关系型数据存储。

MapReduce 和 Have 都专注于类似批处理的数据访问模式,而 HBase 则寻求提供非常低延迟的数据访问。 因此,与上述技术不同,HBase 可以直接支持面向用户的服务。

HBase 数据模型不是在配置单元和所有其他 RDBMS 中使用的关系方法,它也没有提供关系存储认为理所当然的完整 ACID 保证。 相反,它是一个无键值模式的解决方案,采用面向列的数据视图;列可以在运行时添加,并取决于插入到 HBase 中的值。 因此,每个查找操作都非常快,因为它实际上是从行键到所需列的键值映射。 HBase 还将时间戳视为数据的另一个维度,因此可以直接从某个时间点检索数据。

数据模型非常强大,但并不适合所有用例,就像关系模型并不普遍适用一样。 但是,如果您需要存储在 Hadoop 中的大规模数据的结构化低延迟视图,那么 HBase 绝对是您应该考虑的。

Sqoop

第 7 章Hadoop 和 SQL中,我们研究了用于向 HDFS 上存储的数据提供类似关系的界面的工具。 通常,这类数据要么需要从现有的关系数据库中检索,要么需要将其处理的输出存储回去。

Apache Sqoop(Hadoop)提供了一种机制,用于声明性地指定关系数据库和 http://sqoop.apache.org 之间的数据移动。 它接受一个任务定义,并由此生成 MapReduce 作业来执行所需的数据检索或存储。 它还将生成代码来帮助操作带有自定义 Java 类的关系记录。 此外,它还可以与 HBase 和 HCATALOG/HIVE 集成,并提供了非常丰富的集成可能性。

在撰写本文时,Sqoop 略有变化。 它的原始版本 Sqoop1 是一个纯客户端应用。 与最初的配置单元命令行工具非常相似,Sqoop1 没有服务器,在客户机上生成所有代码。 不幸的是,这意味着每个客户端需要了解有关物理数据源的大量详细信息,包括确切的主机名以及身份验证凭据。

Sqoop 2 提供了一个集中的 Sqoop 服务器,该服务器封装了所有这些细节,并向连接的客户端提供各种配置的数据源。 这是一个优秀的模型,但在撰写本文时,社区的普遍建议是坚持使用 Sqoop1,直到新版本进一步发展。 如果您对此类型的工具感兴趣,请检查当前状态。

旋转

当希望使用 Amazon AWS 等云服务进行 Hadoop 部署时,与在 EC2 上设置您自己的集群相比,使用更高级别的服务(如 Elastic MapReduce)通常要容易得多。 尽管有脚本可以提供帮助,但事实是在云基础设施上基于 Hadoop 的部署可能会带来开销。 这就是 Apache whir(https://whirr.apache.org/)的用武之地。

Whir 并不关注 Hadoop;它关注的是独立于供应商的云服务实例化,Hadoop 就是其中的一个例子。 WHIR 的目标是提供一种程序化的方式来指定和创建云基础设施上基于 Hadoop 的部署,这种方式可以为您处理所有底层服务方面。 它以独立于提供者的方式完成这项工作,这样一旦您在 EC2 上启动,您就可以使用相同的代码在另一个提供者(如 Rightscale 或 Eucalyptus)上创建相同的设置。 这使得供应商锁定(通常是云部署中的一个问题)不那么成问题。

惠尔还没有完全做到这一点。 今天,它可以创建的服务和支持的提供商有限,然而,如果您对轻松部署云感兴趣,那么值得关注它的进展。

备注

如果您在 Amazon Web Services 上构建完整的基础设施,那么您可能会发现,云形成在很大程度上提供了定义应用需求的相同能力,尽管显然是以特定于 AWS 的方式。

Mahout

Apache Mahout(Hadoop)是用于在 http://mahout.apache.org/之上执行高级分析的分布式算法、Java 类和工具的集合。 与第 5 章使用 Spark迭代计算中简要提到的 Spark 的 MLLib 类似,Mahout 附带了许多用于常见用例的算法:推荐、集群、回归和功能工程。 虽然该系统专注于自然语言处理和文本挖掘任务,但其构建块(线性代数运算)适合应用于许多领域。 从 0.9 版开始,项目与 MapReduce 框架分离,转而采用更丰富的编程模型,如 Spark。 社区的最终目标是获得一个基于 Scala DSL 的平台无关库。

♫T0H♫色调

Hue(Cloudera)最初由 Cloudera 开发,并以“Hadoop 用户界面”的名称销售,它是一组应用,捆绑在一个通用的 Web 界面下,充当 Hadoop 生态系统的核心服务和许多组件的客户端:Hue(http://gethue.com/)是一组应用,它们捆绑在一个公共 Web 界面下,充当 Hadoop 生态系统的核心服务和许多组件的客户端:

Hue

配置单元的色调查询编辑器

色调利用了我们在前几章中讨论的许多工具,并提供了一个用于分析和可视化数据的集成界面。 有两个组件非常有趣。 一方面,有一个查询编辑器,允许用户创建和保存配置单元(或黑斑羚)查询,以 CSV 或 Microsoft Office Excel 格式导出结果集,以及在浏览器中绘制结果集。 该编辑器具有共享 HiveQL 和结果集的功能,从而促进了组织内的协作。 另一方面,有一个 Oozie 工作流和协调器编辑器,允许用户手动创建和部署 Oozie 作业,自动生成 XML 配置和样板。

Cloudera 和 Hortonworks 发行版都附带了 Hue,通常包括以下内容:

  • HDFS 的文件管理器
  • Yarn 作业浏览器(MapReduce)
  • Apache HBase 浏览器
  • Hive 矿藏探险家
  • 配置单元和黑斑羚的查询编辑器
  • “小 PIG”的脚本编辑器
  • MapReduce 和 Spark 的作业编辑器
  • Sqoop 2 作业的编辑器
  • Oozie 工作流编辑器和仪表板
  • Apache ZooKeeper 浏览器

最重要的是,Hue 是一个带有 SDK 的框架,其中包含许多用于开发与 Hadoop 交互的第三方应用的 Web 资产、API 和模式。

其他编程抽象

Hadoop 不仅仅是通过附加功能进行了扩展,还有一些工具可以提供完全不同的范例来编写用于在 Hadoop 中处理数据的代码。

级联

Cascading(Hadoop)由并发开发,并在 Apache 许可下开源,是一个流行的框架,它抽象了 http://www.cascading.org/的复杂性,并允许我们在 Hadoop 之上创建复杂的工作流。 级联作业可以编译为 MapReduce、TEZ 和 Spark,并在其上执行。 在概念上,该框架类似于 Apache Crunch,在第 9 章让开发更容易中涉及到,尽管实际上在数据抽象和最终目标方面有所不同。 级联采用元组数据模型(类似于 Pig),而不是任意对象,并鼓励用户依赖更高级别的 DSL、强大的内置类型和工具来操作数据。

简单地说,级联对于 Pig 拉丁语和 HiveQL 就像 Crunch 对于用户定义的函数一样。

与我们在第 9 章使开发更容易中看到的 Morphline 一样,级联数据模型遵循源-管道-接收器方法,从源捕获数据,通过多个处理步骤输送数据,然后将其输出交付到接收器,准备由另一个应用拾取。

级联鼓励开发人员用多种 JVM 语言编写代码。 该框架的端口支持 Python(PyCascading)、JRuby(Cascading.jruby)、Clojure(Cascalog)和 Scala(烫伤)。 尤其是卡斯卡洛格和烫伤,已经获得了很大的吸引力,并从它们自己的生态系统中繁殖出来。

级联擅长的一个领域是文档。 该项目提供了全面的 API javadoc、广泛的教程(http://www.cascading.org/documentation/tutorials/)和基于练习的交互式学习环境(https://github.com/Cascading/Impatient)。

级联的另一个强大卖点是它与第三方环境的集成。 Amazon EMR 支持级联作为一流的处理框架,并允许我们使用命令行和 Web 界面(http://docs.aws.amazon.com/ElasticMapReduce/latest/DeveloperGuide/CreateCascading.html)启动级联集群。 IntelliJ IDEA 和 Eclipse 集成开发环境都有 SDK 插件。 该框架的顶级项目之一是级联模式(Cascading Pattern),这是一组机器学习算法,它提供了一个实用程序,用于将预测模型标记语言(PMML)文档转换为 Apache Hadoop 上的应用,从而促进与流行的统计环境和科学工具(如 R(http://cran.r-project.org/web/packages/pmml/index.html)的互操作性。

AWS 资源

许多 Hadoop 技术可以作为自我管理集群的一部分部署在 AWS 上。 然而,正如 Amazon 提供对 Elastic MapReduce 的支持一样,Elastic MapReduce 将 Hadoop 作为托管服务处理,还有一些其他服务值得一提。

SimpleDB 和 DynamoDB

一段时间以来,AWS 一直提供 SimpleDB 作为托管服务,提供类 HBase 的数据模型。

然而,它在很大程度上已经被位于http://aws.amazon.com/dynamodb的 AWS 的较新服务 DynamoDB 所取代。 虽然它的数据模型与 SimpleDB 和 HBase 非常相似,但它针对的是一种非常不同类型的应用。 SimpleDB 有相当丰富的搜索 API,但在大小方面非常有限,DynamoDB 提供了一个更受限制但不断发展的 API,但具有几乎无限可伸缩性的服务保证。

DynamoDB 定价模型特别有趣;不是为托管服务的特定数量的服务器付费,而是为读写操作分配一定的容量,DynamoDB 管理满足该配置容量所需的资源。 这是一个有趣的发展,因为它是一个更纯粹的服务模型,其中交付所需性能的机制对服务用户是完全不透明的。 请看一下 DynamoDB,但如果您需要比 SimpleDB 能够提供的数据存储规模大得多的数据存储;但是,一定要仔细考虑定价模型,因为调配过多的容量很快就会变得非常昂贵。 亚马逊在以下网址为 DynamoDB 提供了一些良好的最佳实践,这些实践说明最小化服务成本可能会导致额外的应用层复杂性:http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/BestPractices.html

备注

当然,对 DynamoDB 和 SimpleDB 的讨论假设了一个非关系数据模型;云服务中的关系数据库有Amazon Relational Database Service(Amazon RDS)。

运动

就像 EMR 是由 Hadoop 托管的,DynamoDB 与托管的 HBase 有相似之处,看到 AWS 在 2013 年宣布推出 Kinesis(一种托管的流媒体数据服务)也就不足为奇了。 这可以在http://aws.amazon.com/kinesis找到,它的概念构建块与 Kafka 上的 Samza 堆栈非常相似。 Kinesis 将消息的分区视图作为数据流提供,并提供了一个 API,以便在消息到达时执行回调。 与大多数 AWS 服务一样,它与其他服务紧密集成,可以轻松地将数据传入和传出 S3 等位置。

♪嘿,♪

我们将提到的最后一个 AWS 服务是数据管道,它可以在http://aws.amazon.com/datapipeline找到。 顾名思义,它是一个用于构建涉及多个步骤、数据移动和转换的数据处理作业的框架。 它在概念上与 Oozie 有相当大的重叠,但有一些曲折。 首先,Data Pipeline 与许多其他 AWS 服务进行了预期的深度集成,可以轻松定义包含不同存储库(如 RDS、S3 和 DynamoDB)的数据工作流。 此外,Data Pipeline 确实能够集成安装在本地基础设施上的代理,为构建跨越 AWS 和内部部署环境的工作流提供了一条有趣的途径。

信息来源

你不仅仅需要新的技术和工具--即使它们很酷。 有时,来自更有经验的人的一点帮助就能把你从困境中拉出来。 在这方面,您已经做了很好的介绍,因为 Hadoop 社区在许多领域都非常强大。

源代码

有时很容易忽略,但 Hadoop 和所有其他 Apache 项目毕竟是完全开源的。 实际的源代码是有关系统如何工作的信息的最终来源(恕我直言,使用双关语)。 熟悉源代码并通过某些功能进行跟踪可能会提供大量信息。 更不用说当你遇到意想不到的行为时会有帮助。

邮件列表和论坛

本章列出的几乎所有个项目和服务都有个自己的邮件列表和/或论坛;请查看主页中的特定链接。 大多数发行版也有自己的论坛和其他机制来分享知识和从社区获得(非商业性)帮助。 此外,如果使用亚马逊,请务必访问https://forums.aws.amazon.com上的亚马逊开发者论坛。

永远记住仔细阅读发帖指南,了解期望的礼仪。 这些都是大量的信息来源;列表和论坛经常被特定项目的开发人员访问。 期待在 Hadoop 列表上看到核心 Hadoop 开发人员,在 Have 列表上看到 Have 开发人员,在 EMR 论坛上看到 EMR 开发人员,等等。

LinkedIn 群

在职业社交网络 LinkedIn 上有个个 Hadoop 和相关群组。 搜索您感兴趣的特定领域,但最好从http://www.linkedin.com/groups/Hadoop-Users-988957上的一般 Hadoop 用户组开始。

拥抱

如果您想要更多面对面的互动,请在您所在地区寻找Hadoop 用户组(拥抱),的大部分将在http://wiki.apache.org/hadoop/HadoopUserGroups列出。 这些公司倾向于安排半定期的聚会,包括高质量的演示、与志同道合的人讨论技术的能力,以及经常是披萨和饮料。

你住的地方附近没有拥抱吗? 考虑开一家吧。

会议

虽然有些行业需要几十年时间才能建立一个会议线路,但 Hadoop 已经有了一些涉及开源、学术和商业世界的重要会议活动。 像 Hadoop 峰会和 Strata 这样的活动是相当大的;这些和其他一些活动是从http://wiki.apache.org/hadoop/Conferences链接到的。

摘要

在本章中,我们简要介绍了更广泛的 Hadoop 生态系统,介绍了以下主题:

  • 为什么会存在其他 Hadoop 发行版以及一些比较流行的发行版
  • 其他提供功能、扩展或 Hadoop 支持工具的项目
  • 编写或创建 Hadoop 作业的其他方式
  • 信息来源以及如何与其他爱好者联系

现在,去享受乐趣,建造一些令人惊叹的东西吧!

posted @ 2025-10-01 11:29  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报