Hadoop2-学习手册-一-

75 阅读1小时+

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

勘误表

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

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

盗版

在互联网上盗版版权材料是所有媒体持续存在的问题。 在 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)(research.google.com/archive/gfs…)和 MapReduce(research.google.com/archive/map…)。 这两者共同提供了一个以高效方式进行超大规模数据处理的平台。 谷歌采取了自己构建的方法,但他们没有针对一个特定的问题或数据集构建某种东西,而是创建了一个可以在其上实现多个处理应用的平台。 具体地说,他们利用了大量商用服务器,构建了 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(www.cloudera.com)、Hortonworks(www.hortonworks.com)和 MapR(www.mapr.com)是最先上市的,每种产品都有不同的方法和卖点。 Hortonworks 将自己定位为开源玩家;Cloudera 也致力于开源,但增加了配置和管理 Hadoop 的专有部分;MapR 提供了混合的开源/专有 Hadoop 发行版,其特征是专有的 NFS 层而不是 HDFS,并且专注于提供服务。

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

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

一种双重方式

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

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

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

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

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

简单存储服务(S3)

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

弹性 MapReduce(EMR)

亚马逊的 Elastic MapReduce 在Hadoop上找到了,基本上就是云中的 aws.amazon.com/elasticmapr… 使用多个界面(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),但在开发、自动化和以编程方式访问系统时,命令行仍然是最强大的工具。

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

Cloudera QuickStart 虚拟机

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

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

Amazon EMR

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

创建 AWS 帐户

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

备注

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

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

注册必要的服务

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

转到从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 中通常是最具成本效益的。 有关亚马逊工作站和电子病历的信息,请咨询aws.amazon.com/elasticmapr…

如何使用电子病历

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

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

AWS 凭据

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

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

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

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

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

AWS 命令行界面

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

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

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

$ pip install awscli

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

这也是通过遵循console.aws.amazon.com/ec2/home?re…提供的说明来设置 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指示 docs.aws.amazon.com/AWSEC2/late… 收集日志并将其存储在learninghadoop2S3 存储桶中。

备注

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

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

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

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

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

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

有关使用 AWS CLI 的更多信息,请参阅docs.aws.amazon.com/ElasticMapR…

运行示例

所有示例的源代码都可以在github.com/learninghad…上找到。

Gradle(Java/)脚本和配置用于编译大多数 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 生态系统的核心组件以及一些第三方工具和库,这些工具和库将使编写健壮的分布式代码成为一项可访问且可望令人愉快的任务。 阅读本书时,您将学习如何从大量结构化和非结构化数据中收集、处理、存储和提取信息。

我们将使用从推特的(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 公开的任何其他对象都在不断演变。 最新参考文献可在dev.twitter.com/docs/platfo…找到。

推特凭证

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

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

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

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

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

  • 访问令牌
  • 访问密码

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

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

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

Twitter credentials

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

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

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

Twitter credentials

使用 Python 进行编程访问

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

可以使用easy_installpip包管理器或通过克隆github.com/tweepy/twee…处的存储库来安装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的源代码,它可以在github.com/learninghad…中找到:

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 Trueif __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),因为它是 zookeeper.apache.org HA 实现的关键部分,我们将在本章中介绍它。 然而,它也被多个其他 Hadoop 组件和相关项目使用,所以我们将在本书中多次涉及到它。

ZooKeeper 最初是 HBase 的一个子组件,用于启用该服务的几个操作功能。 当构建任何复杂的分布式系统时,几乎总是需要一系列活动,而且这些活动总是很难正确进行。 这些活动包括处理共享锁、检测组件故障以及支持一组协作服务中的领导者选举等。 ZooKeeper 是作为协调服务创建的,它将提供一系列基本操作,HBase 可以根据这些操作实现这些类型的操作关键特性。 请注意,ZooKeeper 还从research.google.com/archive/chu…中描述的 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 接口相当低级,并且出现了一些高级接口,它们提供了更多从低级原语到应用级逻辑的映射。 策展人项目(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 抽象类的几个实现之一。 可以在hadoop.apache.org/docs/r2.5.0…中找到可用的文件系统列表。 下表总结了其中的一些文件系统,以及相应的 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(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 的更多信息,请参见www.cse.ohio-state.edu/hpcs/WWW/HT…

兽人

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

更多详细信息可以在docs.hortonworks.com/HDPDocument…上找到。

检察官办公室

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

_

Apache Avro(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 规范。 可以在github.com/learninghad…找到以下代码。

在下面的代码中,我们将使用 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.");
        }
    }

位于github.com/learninghad…,处的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 年发表的一篇论文(research.google.com/archive/map…)提出的处理数据的分而治之模型,并且在函数式编程和数据库研究方面都有基础。 名称本身指的是应用于所有输入数据的两个截然不同的步骤,一个是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-将所有内容组合在一起,并提供一个数据管道来清理和准备我们将提供给趋势主题和情绪分析模型的文本数据。

运行示例

本部分中描述的示例的完整源代码可以在github.com/learninghad…中找到。

在 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 统计数据集中出现的单词。 此示例的源代码可以在github.com/learninghad…中找到。 以下面的代码块为例:

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。 此示例的源代码可以在github.com/learninghad…中找到。

热门话题

#符号称为标签,用于标记推文中的关键字或主题。 它是由 Twitter 用户有机创建的,作为对邮件进行分类的一种方式。 推特搜索(可在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 出现的总数。

此示例的完整源代码可以在github.com/learninghad…中找到。

编译后的类将位于我们之前使用 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 库(code.google.com/p/guava-lib…)提供的数据结构--或者调整更新策略。

在驱动程序中,我们通过设置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

本例的源代码可以在github.com/learninghad…中找到。

标签的情感

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

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

备注

这些资源--以及更多的资源--已经由伊利诺伊大学芝加哥分校的刘兵教授的团队收集,并在刘兵、胡敏青和郑俊生等人身上使用。 “意见观察家:分析和比较网络上的意见。” 第 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));
        }
    }

此示例的完整源代码可以在github.com/learninghad…中找到。

运行上述代码后,使用以下命令执行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调用指定了在用作组合时适用于整个映射器管道的输出键/值类。

此示例的完整源代码可以在github.com/learninghad…中找到。

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

Tez

TEZ DAG 是 MapReduce 的推广

可以在github.com/apache/incu…中找到规范字数计算示例。

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)是一个引擎,用于通过标准 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)是一个流处理框架,由 samza.apache.org 开发并捐赠给 Apache Software Foundation。 Samza 处理概念上无限的数据流,应用将其视为一系列消息。

Samza 目前与 Apache Kafka(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,深入接近实时和迭代处理的领域。