Hadoop-初学者指南-全-

Hadoop 初学者指南(全)

原文:Hadoop beginner's guide

协议:CC BY-NC-SA 4.0

零、前言

本书旨在帮助您理解 Hadoop,并使用它来解决您的大数据问题。 现在是使用 Hadoop 等数据处理技术的好时机。 对大型数据集应用复杂分析的能力--曾经是大公司和政府机构的专利--现在可以通过免费的开源软件(OSS)实现。

但由于这一领域似乎很复杂,变化的速度也很快,掌握基础知识可能会有点令人望而生畏。 这就是这本书的用武之地,让您了解 Hadoop 是什么,它是如何工作的,以及您现在如何使用它从数据中提取价值。

除了对核心 Hadoop 的解释之外,我们还花了几章来探索使用 Hadoop 或与之集成的其他技术。 我们的目标不仅是让您了解 Hadoop 是什么,还希望您了解如何将其作为更广泛的技术基础设施的一部分来使用。

一种补充技术是使用云计算,特别是亚马逊 Web 服务提供的服务。 在整本书中,我们将向您展示如何使用这些服务来托管您的 Hadoop 工作负载,从而说明您不仅可以处理大量数据,而且实际上不需要购买任何物理硬件就可以做到这一点。

这本书涵盖了哪些内容

本书由三个主要部分组成:第 1 章到第 5 章,介绍 Hadoop 的核心及其工作原理;第 6 章和第 7 章,介绍 Hadoop 更具操作性的方面;第 8 章到第 11 章,介绍 Hadoop 与其他产品和技术的使用。

第 1 章概述了使 Hadoop 和云计算成为当今如此重要技术的趋势。

第 2 章启动和运行 Hadoop*将带您完成本地 Hadoop 集群的初始设置和一些演示作业的运行。 作为比较,同样的工作也在托管的 Hadoop Amazon 服务上执行。

第 3 章了解 MapReduce,深入到 Hadoop 的工作中,展示了 MapReduce 作业是如何执行的,并展示了如何使用 Java API 编写应用。

第 4 章开发 MapReduce 程序以一个中等大小的数据集为例,演示在决定如何处理和分析新数据源时提供帮助的技术。

第 5 章高级 MapReduce 技术介绍了几种更复杂的方法来应用 MapReduce 来解决似乎不一定立即适用于 Hadoop 处理模型的问题。

第 6 章当事情中断时,详细分析了 Hadoop 大肆吹嘘的高可用性和容错性,并看到它通过终止进程和故意使用损坏的数据故意造成严重破坏是多么好。

第 7 章保持事物运行从更具操作性的角度看待 Hadoop,对于那些需要管理 Hadoop 集群的人来说最有用。 除了演示一些最佳实践外,它还描述了如何为最糟糕的操作灾难做好准备,以便您可以在晚上睡觉。

第 8 章使用配置单元的数据关系视图介绍了 Apache 配置单元,它允许使用类似 SQL 的语法查询 Hadoop 数据。

第 9 章使用关系数据库探讨了 Hadoop 如何与现有数据库集成,特别是如何将数据从一个数据库移动到另一个数据库。

第 10 章使用 Flume进行数据收集,展示了如何使用 Apache Flume 从多个来源收集数据并将其传送到 Hadoop 等目的地。

第 11 章下一步的方向以更广泛的 Hadoop 生态系统概述作为本书的结束语,重点介绍了其他可能感兴趣的产品和技术。 此外,它还给出了一些关于如何参与 Hadoop 社区和获得帮助的想法。

这本书你需要什么

当我们讨论本书中使用的各种与 Hadoop 相关的软件包时,我们将描述每一章的特定要求。 但是,您通常需要在某个地方运行 Hadoop 集群。

在最简单的情况下,一台基于 Linux 的机器将为您提供一个平台来探索本书中几乎所有的练习。 我们假设您有 Ubuntu 的最新发行版,但只要您熟悉命令行 Linux,任何现代发行版都足够了。

后面章节中的一些示例确实需要多台机器才能查看工作情况,因此您需要访问至少四台这样的主机。 虚拟机是完全可以接受的;它们不适合生产,但很适合学习和探索。

由于我们在本书中还探讨了 Amazon Web 服务,因此您可以在 EC2 实例上运行所有示例,并且我们将在整本书中查看 AWS 的其他一些更特定于 Hadoop 的用法。 任何人都可以使用 AWS 服务,但您需要信用卡才能注册!

这本书是给谁看的

我们假设您阅读这本书是因为您想在实践层面上更多地了解 Hadoop;主要受众是那些具有软件开发经验但之前没有接触过 Hadoop 或类似大数据技术的人。

对于想知道如何编写 MapReduce 应用的开发人员,我们假设您熟悉 Java 程序的编写,并且熟悉 Unix 命令行界面。 我们还将向您展示几个 Ruby 程序,但这些程序通常只是为了演示语言独立性,您不需要是 Ruby 专家。

对于架构师和系统管理员来说,这本书在解释 Hadoop 是如何工作的、它在更广泛的体系结构中的位置,以及如何在操作上管理它方面也提供了重要的价值。 在第 4 章开发 MapReduce 程序第 5 章高级 MapReduce 技术中的一些较复杂的技术可能对此读者不太直接感兴趣。

公约

在这本书中,你会发现几个标题经常出现。

为了给出如何完成某一过程或任务的明确指示,我们使用:

行动方向时间到了

  1. 操作 1
  2. 行动 2
  3. 行动 3

说明通常需要一些额外的解释才能说得通,所以后面跟着:

刚刚发生了什么?

此标题说明您刚刚完成的任务或指令的工作方式。

您还可以在书中找到其他一些学习辅助工具,包括:

弹出式测验-标题

这些是简短的多项选择题,旨在帮助你测试自己的理解力。

有一个加油英雄-标题

这些设置了实际的挑战,并给你提供了尝试你所学到的东西的想法。

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

文本中的代码如下所示:“您可能注意到,我们使用 Unix 命令rm而不是 DOSdel命令来删除Drush目录。”

代码块设置如下:

# * Fine Tuning
#
key_buffer = 16M
key_buffer_size = 32M
max_allowed_packet = 16M
thread_stack = 512K
thread_cache_size = 8
max_connections = 300

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

# * Fine Tuning
#
key_buffer = 16M
key_buffer_size = 32M
max_allowed_packet = 16M
thread_stack = 512K
thread_cache_size = 8
max_connections = 300

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

cd /ProgramData/Propeople
rm -r Drush
git clone --branch master http://git.drupal.org/project/drush.git

新术语重要单词以粗体显示。 例如,您在屏幕、菜单或对话框中看到的文字会出现在文本中,如下所示:“在选择目的地位置屏幕上,单击下一步接受默认目的地。”

备注

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

提示

提示和技巧如下所示。

读者反馈

欢迎读者的反馈。 让我们知道你对这本书的看法-你喜欢什么或不喜欢什么。 读者反馈对于我们开发真正能让您获得最大收益的图书非常重要。

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

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

客户支持

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

下载示例代码

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

勘误表

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

盗版

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

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

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

问题

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

一、说明这一切是怎么回事

这本书是关于 Hadoop 的,这是一个用于大规模数据处理的开源框架。 在我们详细介绍这项技术及其在后面章节中的用法之前,花一点时间探索导致 Hadoop 的创建及其巨大成功的趋势是很重要的。

Hadoop 不是在真空中创建的;相反,它的存在是因为创建和使用的数据量呈爆炸式增长,而且这种数据洪流不仅出现在大型跨国公司身上,还出现在小型初创公司身上。 与此同时,其他趋势也改变了软件和系统的部署方式,与更传统的基础设施一起使用云资源,甚至优先使用云资源。

本章将探讨其中的一些趋势,并详细解释 Hadoop 试图解决的具体问题以及影响其设计的驱动因素。

在本章的其余部分中,我们将:

  • 了解大数据革命
  • 了解 Hadoop 是什么以及它如何从数据中提取价值
  • 了解云计算并了解亚马逊 Web 服务提供了什么
  • 看看大数据处理和云计算的结合有多强大
  • 了解本书其余部分涵盖的主题的概述

所以让我们开始吧!

大数据处理

环顾一下我们今天拥有的技术,很容易得出这样的结论:一切都是关于数据的。 作为消费者,我们对富媒体的胃口越来越大,无论是我们看的电影,还是我们创建和上传的图片和视频。 我们还经常不假思索地在网络上留下一条数据的踪迹,当我们执行日常生活中的行为时。

不仅生成的数据量在增加,而且增长速度也在加快。 从电子邮件到 Facebook 帖子,从购买历史到网络链接,到处都有不断增长的大型数据集。 挑战在于从这些数据中提取最有价值的方面;有时这意味着特定的数据元素,而在其他时候,重点是识别数据片段之间的趋势和关系。

在幕后发生了一种微妙的变化,这一切都是关于以越来越有意义的方式使用数据。 大公司已经意识到数据的价值已经有一段时间了,并一直在利用它来改善他们向客户(即我们)提供的服务。 考虑一下谷歌如何展示与我们的网络冲浪相关的广告,或者亚马逊或 Netflix 如何推荐通常与我们的品味和兴趣非常匹配的新产品或标题。

数据的值

如果大规模数据处理不能带来可观的投资回报或竞争优势,这些公司就不会投资。 大数据有几个主要方面值得重视:

  • 有些问题只有在被问到足够大的数据集时才会给出价值。 在没有其他因素的情况下,根据另一个人的喜好推荐一部电影不太可能非常准确。 将人数增加到 100 人,机会略有增加。 利用 1000 万其他人的观看历史记录,发现可用于给出相关建议的模式的机会大大提高。
  • 与以前的解决方案相比,大数据工具通常能够以更大的规模和更低的成本处理数据。 因此,通常可以执行以前昂贵得令人望而却步的数据处理任务。
  • 大规模数据处理的成本不仅仅是财务费用;延迟也是一个关键因素。 一个系统可能能够处理抛给它的尽可能多的数据,但是如果平均处理时间是以周为单位来衡量的,那么它可能就没有什么用处了。 大数据工具允许在控制处理时间的同时增加数据量,通常是通过将增加的数据量与额外的硬件相匹配来实现的。
  • 以前关于数据库应该是什么样子或者它的数据应该如何构造的假设可能需要重新考虑,以满足最大的数据问题的需要。
  • 与上述几点相结合,足够大的数据集和灵活的工具可以回答以前想象不到的问题。

从历史上看,面向少数人而不是多数人

上一节讨论的例子通常以大型搜索引擎和在线公司的创新形式出现。 这延续了一种更为古老的趋势,即处理大型数据集是一项昂贵而复杂的任务,中小型组织负担不起。

同样,更广泛的数据挖掘方法已经存在很长时间了,但在最大的公司和政府机构之外,从来没有真正成为一种实用的工具。

这种情况可能令人遗憾,但大多数较小的组织并不处于劣势,因为它们很少能够获得需要这种投资的大量数据。

然而,数据的增长不再局限于大型公司;许多中小型公司-更不用说一些个人-发现自己收集的数据越来越多,他们怀疑这些数据可能有一些他们想要释放的价值。

在理解如何实现这一点之前,重要的是要了解一些更广泛的历史趋势,它们为今天的 Hadoop 等系统奠定了基础。

经典数据处理系统

大数据挖掘系统稀少且昂贵的根本原因是,扩展一个系统以处理大型数据集非常困难;正如我们将看到的,它传统上受限于可以内置到一台计算机中的处理能力。

但是,随着数据大小的增加,有两种扩展系统的主要方法,通常称为向上扩展-和向外扩展。

_

在大多数企业中,数据处理通常是在价格高得惊人的大型计算机上执行的。 随着数据大小的增长,方法是移动到更大的服务器或存储阵列。 通过一个有效的架构--即使是今天,正如我们将在本章后面描述的那样--这种硬件的成本可以很容易地以数十万美元或数百万美元来衡量。

简单向上扩展的优势在于,架构不会因为增长而发生重大变化。 虽然使用了较大的组件,但基本关系(例如,个数据库服务器和存储阵列)保持不变。 对于商业数据库引擎等应用,软件处理利用可用硬件的复杂性,但从理论上讲,通过将相同的软件迁移到越来越大的服务器上可以实现更大的规模。 但请注意,将软件迁移到越来越多的处理器上的难度绝不是微不足道的;此外,单个主机的大小也有实际限制,因此在某些情况下,向上扩展不能再进一步扩展。

任何规模的单一架构的承诺也是不切实际的。 设计一个纵向扩展系统来处理 1TB、100TB 和 1PB 等大小的数据集,在概念上可能会应用相同组件的更大版本,但随着规模的增加,其连接的复杂性可能会从廉价商品到定制硬件有所不同。

横向扩展的早期方法

横向扩展方法将处理分散到越来越多的机器上,而不是将系统扩展到越来越大的硬件上。 如果数据集翻了一番,只需使用两台服务器,而不是一台双倍大小的服务器。 如果再翻一番,就移到四台主机上。

这种方法的明显好处是,购买成本仍然比扩大规模低得多。 当人们试图购买更大的机器时,服务器硬件成本往往会急剧增加,虽然一台主机的价格可能为 5000 美元,但一台处理能力是其十倍的主机的价格可能是其一百倍。 缺点是,我们需要制定策略,将我们的数据处理分散到一组服务器上,而历史上用于此目的的工具已被证明是复杂的。

因此,部署横向扩展解决方案需要大量的工程工作;系统开发人员通常需要手工制作用于数据分区和重组的机制,更不用说跨群集安排工作和处理单个机器故障的逻辑了。

限制因素

在大型企业、政府和学术界之外,这些扩大规模和横向扩展的传统方法并未被广泛采用。 购买成本往往很高,开发和管理系统的努力也是如此。 仅这些因素就让许多小企业望而却步。 此外,随着时间的推移,这些方法本身也有几个明显的弱点:

  • 随着横向扩展系统变得越来越大,或者随着纵向扩展系统处理多个 CPU,系统中并发的复杂性带来的困难变得非常严重。 有效利用多个主机或 CPU 是一项非常困难的任务,实施必要的策略以在所需工作负载的整个执行过程中保持效率可能需要付出巨大的努力。
  • 硬件的进步--通常以摩尔定律的形式表述--已经开始突显系统能力的差异。 CPU 能力的增长速度远远快于网络或磁盘速度;CPU 周期曾经是系统中最有价值的资源,但今天,这种情况已经不再存在。 与 20 年前相比,现代 CPU 可能能够执行数百万倍的操作,而内存和硬盘速度只增加了数千倍甚至数百倍。 构建一个 CPU 能力如此强大的现代系统是相当容易的,以至于存储系统根本不能以足够快的速度向其提供数据,从而使 CPU 保持忙碌。

一种不同的方法

从前面的场景来看,有许多技术已经被成功地用于减轻将数据处理系统扩展到大数据所需的大规模的痛苦。

所有道路都会导致横向扩展

正如刚才所暗示的,采取扩大规模的方法进行扩展并不是一种无限制的策略。 可以从主流硬件供应商那里购买的单个服务器的大小是有限制的,即使是更多的小众玩家也不能提供任意大的服务器。 在某一时刻,工作负载将增加到超出单个整体纵向扩展服务器的容量,那又如何呢? 不幸的是,最好的方法是使用两台大型服务器,而不是一台。 然后,稍后,三,四,以此类推。 或者,换句话说,纵向扩展架构的自然趋势是-在极端情况下-在组合中添加横向扩展策略。 虽然这提供了这两种方法的一些优点,但它也增加了成本和缺点;这种混合体系结构需要两者,而不是非常昂贵的硬件或需要手动开发跨集群逻辑。

由于这种最终趋势和纵向扩展体系结构的总体成本概况,它们很少用于大数据处理领域,而横向扩展体系结构是事实上的标准。

提示

如果您的问题空间涉及具有强大内部交叉引用和事务完整性需求的数据工作负载,大型纵向扩展关系数据库仍然可能是一个很好的选择。

不共享任何内容

任何有孩子的人都会花相当多的时间教小孩子分享是件好事。 这一原则不适用于数据处理系统,而且这一思想既适用于数据,也适用于硬件。

尤其是横向扩展体系结构的概念性视图显示了各个主机,每个主机处理整个数据集的一个子集,以产生其最终结果的一部分。 现实很少如此直截了当。 相反,主机之间可能需要相互通信,或者多个主机可能需要某些数据。 这些额外的依赖关系会给系统带来两方面的负面影响:瓶颈和增加的故障风险。

如果系统中的每个计算都需要一条数据或单个服务器,则在相互竞争的客户端访问公共数据或主机时,存在争用和延迟的可能性。 例如,如果在具有 25 台主机的系统中,只有一台主机必须由所有其他主机访问,则系统整体性能将受该关键主机的功能限制。

更糟糕的是,如果保存关键数据的这个“热”服务器或存储系统出现故障,整个工作负载将崩溃成堆。 早期的集群解决方案经常显示出这种风险;即使工作负载是跨服务器场处理的,它们通常使用共享存储系统来保存所有数据。

系统的各个组件应该尽可能独立,而不是共享资源,无论其他组件是否被复杂的工作所束缚或正在经历故障,每个组件都可以继续工作。

预计失败

在前面的原则中隐含的意思是,将尽可能独立地投入更多的硬件来解决这个问题。 这只有在系统构建时预期单个组件会出现故障(通常是有规律的且时间不方便)时才能实现。

备注

您会经常听到“五个九”这样的术语(指的是 99.999%的正常运行时间或可用性)。 虽然这绝对是同类中最好的可用性,但重要的是要认识到,由许多此类设备组成的系统的总体可靠性可能会有很大差异,这取决于系统是否能够容忍单个组件故障。

假设一台服务器具有 99%的可靠性,并且系统需要五台这样的主机才能运行。 系统可用性为 0.990.990.990.990.99,相当于 95%的可用性。 但如果单个服务器的评级只有 95%,系统可靠性就会下降到只有 76%。

相反,如果您构建的系统在任何给定时间只需要五台主机中的一台正常工作,则系统可用性将达到五个九的范围。 考虑与每个组件的关键程度相关的系统正常运行时间有助于将重点放在系统可用性可能达到的水平上。

提示

如果 99%的可用性这样的数字对您来说有点抽象,请考虑一下在给定时间段内意味着多少停机时间。 例如,99%的可用性相当于每年的停机时间略高于 3.5 天或每月停机 7 小时。 听起来还能达到 99%吗?

这种拥抱失败的方式往往是大数据系统最难让新手充分领会的方面之一。 这也是该方法与纵向扩展架构的最大不同之处。 大型纵向扩展服务器成本高的主要原因之一是用于减轻组件故障影响的工作量。 即使是低端服务器也可能有冗余电源,但在一个大的铁盒中,您会看到 CPU 安装在卡上,这些卡跨多个底板连接到内存和存储系统组。 大型钢铁供应商经常走极端来展示他们的系统有多么有弹性,他们做了一切事情,从服务器运行时拔出部分服务器,到实际向服务器开枪。 但是,如果系统的构建方式不是将每一次失败都视为一场需要缓解的危机,而是将其降为无关紧要的,那么就会出现一个非常不同的架构。

智能软件,哑巴硬件

如果我们希望看到硬件集群以尽可能灵活的方式使用,为多个并行工作流提供托管,答案是将智能推向软件,而不是硬件。

在此模型中,硬件被视为一组资源,将硬件分配给特定工作负载的责任交给软件层。 这允许硬件是通用的,因此获得起来既容易又便宜,并且有效使用硬件的功能转移到软件上,而软件是关于有效执行该任务的知识所在。

移动处理,而不是数据

假设您有一个非常大的数据集,比如说 1000TB(即 1PB),并且您需要对数据集中的每个数据执行一组四个操作。 让我们看看实现系统来解决这个问题的不同方式。

传统的大型纵向扩展解决方案将看到一台巨型服务器连接到同样令人印象深刻的存储系统,几乎可以肯定地使用光纤通道等技术来最大化存储带宽。 系统将执行该任务,但会受到 I/O 限制;即使是高端存储交换机也会限制将数据传送到主机的速度。

或者,以前集群技术的处理方法可能会看到一个由 1,000 台机器组成的集群,每台机器都有 1TB 的数据,分为四个象限,每个象限负责执行其中一个操作。 然后,集群管理软件将协调数据在集群中的移动,以确保每一块都接受所有四个处理步骤。 由于每条数据可以在其所在的主机上执行一个步骤,因此它将需要将数据流式传输到其他三个象限,因此我们实际上消耗了 3 PB 的网络带宽来执行处理。

请记住,处理能力的增长速度快于网络或磁盘技术,那么这些真的是解决问题的最佳方法吗? 最近的经验表明答案是否定的,另一种方法是避免移动数据,而是移动处理。 使用刚才提到的集群,但不要将其划分为象限;相反,让 1000 个节点中的每个节点对本地保存的数据执行所有四个处理阶段。 如果幸运的话,您只需从磁盘流式传输数据一次,而通过网络传输的只有程序二进制文件和状态报告,这两者与实际数据集相比都相形见绌。

如果 1,000 个节点的群集听起来大得离谱,请考虑一下大数据解决方案所使用的一些现代服务器外形规格。 它们看到的是单个主机,每个主机中有多达 12 个 1 TB 或 2 TB 的磁盘。 因为现代处理器有多个核心,所以可以构建一个具有 1 PB 存储空间的 50 节点集群,同时仍然有一个 CPU 核心专门处理来自每个单独磁盘的数据流。

构建应用,而不是基础设施

在考虑上一节中的场景时,很多人都会关注数据移动和处理的问题。 但是,任何曾经构建过这样的系统的人都会知道,作业调度、错误处理和协调等不太明显的元素才是真正的魔力所在。

如果我们必须实现用于确定在哪里执行处理、执行处理并将所有子结果合并到整体结果中的机制,我们就不会从旧模型中获得太多好处。 在那里,我们需要显式地管理数据分区;我们只是在交换一个难题和另一个难题。

这涉及到最新的趋势,我们将在这里重点介绍:一个透明地处理大部分集群机制并允许开发人员从业务问题的角度进行思考的系统。 框架提供了定义良好的接口,这些接口抽象了所有这些复杂性-智能软件-在此基础上可以构建特定于业务领域的应用,从而提供了开发人员和系统效率的最佳组合。

Hadoop

深思熟虑(或怀疑)的读者了解到前面的方法都是 Hadoop 的关键方面时,不会感到惊讶。 但是我们仍然没有真正回答 Hadoop 到底是什么的问题。

谢谢,谷歌

这一切都始于谷歌,它在 2003 年和 2004 年发布了两篇描述谷歌技术的学术论文:Google 文件系统(gfs)(http://research.google.com/archive/gfs.html)和 MapReduce(http://research.google.com/archive/mapreduce.html)。 这两者共同提供了一个以高效方式大规模处理数据的平台。

谢谢,道格

与此同时,Doug Cutting 正在开发 Nutch 开源网络搜索引擎。 他一直在研究系统中的元素,这些元素在 Google GFS 和 MapReduce 论文发表后引起了强烈共鸣。 Doug 开始了这些 Google 系统的实现工作,Hadoop 很快就诞生了,最初是 Lucene 的一个子项目,很快就成为了 Apache 开源基金会中自己的顶级项目。 因此,Hadoop 的核心是一个开源平台,它同时提供 MapReduce 和 GFS 技术的实现,并允许跨低成本商用硬件集群处理非常大的数据集。

谢谢,雅虎

雅虎在 2006 年聘请了 Doug Cutting,并很快成为 Hadoop 项目最著名的支持者之一。 除了经常宣传一些世界上最大的 Hadoop 部署外,雅虎还允许 Doug 和其他工程师在受雇期间为 Hadoop 做出贡献;雅虎还贡献了一些内部开发的 Hadoop 改进和扩展。 虽然道格现在已经转向 Cloudera(另一家支持 Hadoop 社区的知名初创公司),雅虎 Hadoop 团队的大部分成员也被剥离出来,成立了一家名为 Hortonworks 的初创公司,但雅虎仍然是 Hadoop 的主要贡献者。

Hadoop 的部分内容

顶层 Hadoop 项目有许多组件子项目,我们将在本书中讨论其中几个,但主要的两个是Hadoop 分布式文件系统(HDFS)和 MapReduce。 这些都是 Google 自己的 GFS 和 MapReduce 的直接实现。 我们将对两者进行更详细的讨论,但目前,最好将 HDFS 和 MapReduce 视为一对互补但截然不同的技术。

HDFS是一个文件系统,它可以通过跨主机群集向外扩展来存储非常大的数据集。 它具有特定的设计和性能特征;尤其是,它针对吞吐量而不是延迟进行了优化,并且通过复制而不是冗余来实现高可用性。

MapReduce是一种数据处理范例,它指定数据将如何从其两个阶段(称为映射和还原)输入和输出,然后将其应用于任意大的数据集。 MapReduce 与 HDFS 紧密集成,确保 MapReduce 任务尽可能直接在保存所需数据的 HDFS 节点上运行。

通用构建块

HDFS 和 MapReduce 都展示了上一节中描述的几个体系结构原则。 特别是:

  • 两者都设计为在商用(即中低规格)服务器集群上运行
  • 两者都通过添加更多服务器来扩展容量(横向扩展)
  • 两者都有识别和解决故障的机制
  • 两者都透明地提供许多服务,使用户能够专注于手头的问题
  • 两者都有一个体系结构,其中软件群集位于物理服务器上,并控制系统执行的所有方面

HDFS

HDFS 是一个不同于您以前可能遇到的大多数文件系统的文件系统。 它不是兼容 POSIX 的文件系统,这基本上意味着它不能提供与常规文件系统相同的保证。 它也是分布式文件系统,这意味着它将存储分布在多个节点上;在一些历史技术中,缺乏这样高效的分布式文件系统是一个限制因素。 主要功能包括:

  • HDFS 以块为单位存储文件,通常大小至少为 64MB,远远大于大多数文件系统中 4-32KB 的大小。
  • HDFS 针对延迟吞吐量进行了优化;它在流式传输大文件的读取请求方面非常高效,但在许多小文件的寻道请求方面效率很低。
  • HDFS 针对通常为一次写入和多次读取类型的工作负载进行了优化。
  • 每个存储节点都运行一个称为 DataNode 的进程,该进程管理该主机上的数据块,这些数据块由在单独主机上运行的主 NameNode 进程协调。
  • HDFS 使用复制,而不是通过在磁盘阵列中设置物理冗余或类似策略来处理磁盘故障。 组成文件的每个数据块都存储在群集内的多个节点上,HDFS NameNode 会持续监视每个 DataNode 发送的报告,以确保故障没有使任何数据块低于所需的复制系数。 如果确实发生这种情况,它会计划在群集中添加另一个拷贝。

MapReduce

虽然 MapReduce 作为一种相对较新的技术,它建立在数学和计算机科学的许多基础工作的基础上,特别是寻求表达运算的方法,然后这些运算将应用于一组数据中的每个元素。 实际上,称为mapreduce的各个函数概念直接来自函数式编程语言,它们被应用于输入数据列表。

另一个关键的基本概念是“分而治之”,即将一个问题分解成多个单独的子任务。 当子任务并行执行时,这种方法会变得更加强大;在理想情况下,1000 分钟的任务可以在 1 分钟内被 1000 个并行子任务处理。

MapReduce是一个基于这些原则的处理范例;它提供了一系列从源数据集到结果数据集的转换。 在最简单的情况下,输入数据被馈送到map函数,结果临时数据被馈送到reduce函数。 开发人员只定义数据转换;Hadoop 的 MapReduce 作业管理如何将这些转换并行应用到集群中的数据的过程。 尽管潜在的想法可能并不新颖,但 Hadoop 的一个主要优势在于它如何将这些原则整合到一个可访问和精心设计的平台中。

与传统的关系型数据库不同,传统的关系型数据库需要具有定义良好的模式的结构化数据,MapReduce 和 Hadoop 在半结构化或非结构化数据上工作得最好。 与符合严格模式的数据不同,要求将数据作为一系列键值对提供给map函数。 map函数的输出是一组其他键值对,reduce函数执行聚合以收集最终结果集。

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

最后一点可能是 Hadoop 最重要的方面。 该平台负责跨数据执行处理的各个方面。 在用户定义了作业的关键标准之后,其他所有事情都将由系统负责。 重要的是,从数据大小的角度来看,相同的 MapReduce 作业可以应用于托管在任何大小的集群上的任何大小的数据集。 如果数据大小为 1 GB,并且位于单个主机上,Hadoop 将相应地安排处理。 即使数据是 1PB 大小,并且托管在 1000 台机器上,它仍然会这样做,决定如何最好地利用所有主机来最有效地执行工作。 从用户的角度来看,数据和集群的实际大小是透明的,除了影响处理作业所需的时间外,它们不会改变用户与 Hadoop 交互的方式。

更好的结合在一起

我们可以欣赏 HDFS 和 MapReduce 各自的优点,但当它们组合在一起时,功能会更强大。 HDFS 可以在没有 MapReduce 的情况下使用,因为它本质上是一个大规模的数据存储平台。 尽管 MapReduce 可以从非 HDFS 源读取数据,但其处理性质与 HDFS 非常一致,因此将两者结合使用是迄今为止最常见的用例。

在执行 MapReduce 作业时,Hadoop 需要决定在哪里执行代码以最有效地处理数据集。 如果 MapReduce 群集主机都从单个存储主机或阵列提取其数据,这在很大程度上无关紧要,因为存储系统是共享资源,会导致争用。 但是,如果存储系统是 HDFS,则它允许 MapReduce 在保存感兴趣的数据的节点上执行数据处理,这建立在移动数据处理的成本低于数据本身的原则上。

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

回想一下我们前面的示例,即如何处理分布在 1000 台服务器上的 1PB 数据上的四步任务。 MapReduce 模型将(以某种简化和理想化的方式)对 HDFS 中驻留的主机上的每段数据执行map函数中的处理,然后重用reduce函数中的集群,将各个结果收集到最终结果集中。

Hadoop 的部分挑战在于将整个问题分解为mapreduce函数的最佳组合。 只有当四阶段处理链可以依次独立地应用于每个数据元素时,上述方法才有效。 正如我们将在后面章节中看到的,答案有时是使用多个 MapReduce 作业,其中一个作业的输出是下一个作业的输入。

公共架构

如前所述,HDFS 和 MapReduce 都是显示共同特征的软件集群:

  • 每个节点都遵循一个体系结构,其中一个工作节点集群由一个特殊的主/协调器节点管理
  • 在每种情况下,主服务器(HDFS 的 NameNode 和 MapReduce 的 JobTracker)通过移动数据块或重新调度失败的工作来监视群集的运行状况并处理故障
  • 每台服务器(HDFS 的 DataNode 和 MapReduce 的 TaskTracker)上的进程负责在物理主机上执行工作,接收来自 NameNode 或 JobTracker 的指令,并向其报告运行状况/进度状态

作为次要术语,我们通常使用术语主机服务器来指代托管 Hadoop 各种组件的物理硬件。 术语节点将指包括群集一部分的软件组件。

它有什么好处,有什么不好

与任何工具一样,了解 Hadoop 何时适合所讨论的问题非常重要。 本书的大部分内容将基于前面关于处理大数据量的广泛概述来强调其优势,但在不是最佳选择的早期阶段也要开始欣赏它,这一点很重要。

在 Hadoop 中做出的体系结构选择使其成为今天灵活且可伸缩的数据处理平台。 但是,与大多数架构或设计选择一样,必须理解一些后果。 其中最主要的一点是 Hadoop 是一个批处理系统。 当您在大型数据集上执行作业时,框架将一直搅动,直到最终结果准备就绪。 有了大型集群,即使是庞大的数据集也可以相对快速地生成答案,但事实仍然是,生成答案的速度不够快,无法为不耐烦的用户提供服务。 因此,Hadoop 本身并不能很好地适用于低延迟查询,比如在网站、实时系统或类似问题域中接收到的查询。

当 Hadoop 在大型数据集上运行作业时,设置作业、确定在每个节点上运行哪些任务以及所需的所有其他内务管理活动的开销只占总执行时间的一小部分。 但是,对于小数据集上的作业,存在执行开销,这意味着即使是简单的 MapReduce 作业也可能至少需要 10 秒。

备注

更广泛的 Hadoop 家族的另一个成员是HBase,它是另一项 Google 技术的开源实现。 这在 Hadoop 之上提供了一个(非关系)数据库,该数据库使用各种方法来支持低延迟查询。

但谷歌(Google)和雅虎(Yahoo)不都是这种计算方法的最坚定支持者吗?它们不都是关于响应时间至关重要的网站吗? 答案是肯定的,它突出了如何将 Hadoop 整合到任何组织或活动中,或者如何将其与其他技术结合使用以发挥各自优势的一个重要方面。 在一篇论文(http://research.google.com/archive/googlecluster.html)中,谷歌概述了他们当时是如何利用 MapReduce 的;在网络爬虫检索到更新的网页数据后,MapReduce 处理了庞大的数据集,并由此产生了一批 MySQL 服务器用于服务最终用户搜索请求的 Web 索引。

使用 Amazon Web 服务的云计算

我们将在本书中探讨的另一个技术领域是云计算,它是由 Amazon Web Services 提供的几个产品的形式。 但首先,我们需要消除围绕云计算的一些炒作和热词。

云层太多

云计算已经成为一个过度使用的术语,可以说,过度使用云计算可能会让它变得毫无意义。 因此,在这本书中,当我们使用这个术语时,让我们明确我们的意思-并关心什么。 这主要有两个方面:一种新的架构选项和一种不同的成本方法。

第三条路

我们已经讨论过将纵向扩展和横向扩展作为扩展数据处理系统的选项。 但到目前为止,我们的讨论都理所当然地认为,实现这两种选择的物理硬件将由进行系统开发的组织购买、拥有、托管和管理。 我们关心的云计算增加了第三种方法:将您的应用放到云中,让提供商处理可伸缩性问题。

当然,事情并不总是那么简单。 但对于许多云服务来说,这种模式确实是一场革命性的变革。 您可以根据一些已发布的指导方针或接口开发软件,然后将其部署到云平台上,并允许它根据需求扩展服务,当然这是有成本的。 但考虑到制造扩展系统通常涉及的成本,这往往是一个令人信服的命题。

不同类型的成本

云计算的这种方法也改变了系统硬件的支付方式。 通过分流基础设施成本,所有用户都能从云提供商实现的规模经济中受益,因为云提供商将其平台构建到能够承载数千或数百万个客户端的规模。 作为用户,您不仅可以让其他人担心诸如扩展等棘手的工程问题,而且您还可以根据需要为容量付费,而不必根据可能的最大工作负载调整系统的大小。 相反,您可以获得灵活性的好处,并根据工作负载需求使用更多或更少的资源。

有一个例子可以帮助说明这一点。 许多公司的财务部门都会在月底运行工作量,以生成税收和薪资数据,而且通常在年末会出现规模大得多的数据处理。 如果让您设计这样的系统,您会购买多少硬件? 如果您只购买了足够处理日常工作的东西,系统可能会在月底遇到困难,在年终处理结束时可能会遇到真正的麻烦。 如果您针对月末工作负载进行扩展,则系统在一年中的大部分时间都会有空闲容量,并且可能在执行年终处理时仍会遇到问题。 如果您针对年终工作负载进行调整,则系统将有大量容量在今年剩余时间处于闲置状态。 考虑到硬件的购买成本,再加上托管和运行成本-服务器的用电量可能占其生命周期成本的很大一部分-基本上你是在浪费大量的钱。

云计算的按需服务特性使您可以在很小的硬件占用空间上启动应用,然后随着时间的推移进行扩展和缩减。 在按使用付费模式下,您的成本随利用率而变化,您有能力处理工作负载,而无需购买足够的硬件来应对高峰。

这个模型的一个更微妙的方面是,这极大地降低了组织推出在线服务的入门成本。 我们都知道,一个新的热门服务如果不能满足需求,出现性能问题,就很难恢复势头和用户兴趣。 例如,在 2000 年,一个希望成功发布的组织需要在发布当天准备好足够的容量,以满足他们希望但不确定预期的用户流量的巨大激增。 考虑到地理位置的成本,在一次产品发布上花费数百万美元是很容易的。

今天,有了云计算,最初的基础设施成本可能会低至每月几十美元或数百美元,而这只会在流量需求时增加。

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

Amazon Web Services(AWS)是由 Amazon 提供的一组此类云计算服务。 在本书中,我们将使用其中的几项服务。

弹性计算云(EC2)

亚马逊的弹性计算云(EC2)位于http://aws.amazon.com/ec2/,基本上是随需应变服务器。 注册 AWS 和 EC2 后,只需信用卡详细信息即可访问专用虚拟机,在我们的服务器上轻松运行各种操作系统,包括 Windows 和许多 Linux 版本。

需要更多服务器吗? 开始更多。 需要更强大的服务器吗? 更改为所提供的较高规格(和成本)类型之一。 除此之外,EC2 还提供一整套免费服务,包括负载均衡器、静态 IP 地址、高性能附加虚拟磁盘驱动器等。

简单存储服务(S3)

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

弹性 MapReduce(EMR)

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

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

这本书涵盖了哪些内容

在这本书中,我们将学习如何编写 MapReduce 程序来执行一些重要的数据处理,以及如何在本地管理和 AWS 托管的 Hadoop 集群上运行这些程序。

我们不仅将 Hadoop 视为执行 MapReduce 处理的引擎,还将探索 Hadoop 功能如何适应组织的基础设施和系统的其余部分。 我们将介绍一些集成的共同点,比如在 Hadoop 和关系数据库之间获取数据,以及如何使 Hadoop 看起来更像这样的关系数据库。

一种双重方式

在本书中,我们的讨论不会局限于 Amazon EC2 上托管的 EMR 或 Hadoop;除了展示如何通过 EMR 将处理推到云中之外,我们还将讨论本地 Hadoop 集群(在 Ubuntu Linux 上)的构建和管理。

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

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

摘要

在本章中,我们学到了很多关于大数据、Hadoop 和云计算的知识。

具体地说,我们介绍了大数据的出现,以及数据处理方法和系统架构的变化如何使几乎所有以前昂贵的组织技术都可以使用。

我们还回顾了 Hadoop 的历史,以及它如何在众多趋势的基础上提供可扩展到海量的灵活而强大的数据处理平台。 我们还研究了云计算如何提供另一种系统架构方法,这种方法交换了高额的前期成本和对现收现用模式的直接物理责任,并依赖云提供商进行硬件配置、管理和扩展。 我们还了解了什么是 Amazon Web Services,以及其 Elastic MapReduce 服务如何利用其他 AWS 服务在云中提供 Hadoop。

我们还讨论了本书的目的及其在本地管理和 AWS 托管的 Hadoop 集群上的探索方法。

既然我们已经介绍了基础知识,并且知道了这项技术的来源和它的好处,我们就需要动手并让它运行起来,这就是我们将在第 2 章让 Hadoop 启动并运行中要做的事情。

二、启动和运行 Hadoop

既然我们已经探讨了大规模数据处理带来的机遇和挑战,以及为什么 Hadoop 是一个令人信服的选择,是时候让它建立并运行起来了。

在本章中,我们将执行以下操作:

  • 了解如何在本地 Ubuntu 主机上安装和运行 Hadoop
  • 运行一些示例 Hadoop 程序并熟悉系统
  • 设置使用 EMR 等 Amazon Web 服务产品所需的帐户
  • 在 Elastic MapReduce 上创建按需 Hadoop 集群
  • 了解本地 Hadoop 群集和托管 Hadoop 群集之间的主要区别

本地 Ubuntu 主机上的 Hadoop

为了在云之外探索 Hadoop,我们将使用一个或多个 Ubuntu 主机进行示例。 一台机器(无论是物理计算机还是虚拟机)就足以运行 Hadoop 的所有部分并探索 MapReduce。 然而,生产集群很可能会涉及更多的机器,因此即使在多台主机上部署一个开发 Hadoop 集群也会是一种不错的体验。 但是,对于入门来说,一台主机就足够了。

我们讨论的任何东西都不是 Ubuntu 独有的,Hadoop 应该可以在任何 Linux 发行版上运行。 显然,如果您使用的不是 Ubuntu 发行版,那么您可能必须改变环境的配置方式,但差别应该很小。

其他操作系统

Hadoop 确实可以在其他平台上很好地运行。 Windows 和 MacOSX 是开发人员的热门选择。 Windows 仅支持作为开发平台,而 Mac OS X 则完全不受正式支持。

如果您选择使用这样的平台,一般情况将类似于其他 Linux 发行版;如何在两个平台上使用 Hadoop 的所有方面都是相同的,但是您需要使用特定于操作系统的机制来设置环境变量和类似的任务。 Hadoop 常见问题包含一些关于替代平台的信息,如果您正在考虑这样的方法,应该是您的第一站。 Hadoop 常见问题解答可以在http://wiki.apache.org/hadoop/FAQ上找到。

行动时间-检查先决条件

Hadoop 是用 Java 编写的,因此您需要在 Ubuntu 主机上安装最新的Java 开发工具包(JDK)。 执行以下步骤以检查前提条件:

  1. 首先,打开终端并键入以下命令,查看已有的内容:

    $ javac
    $ java -version
    
    
  2. 如果这两个命令中的任何一个出现no such file or directory或类似的错误,或者如果后者提到“Open JDK”,那么您很可能需要下载完整的 JDK。 请从http://www.oracle.com/technetwork/java/javase/downloads/index.html的 Oracle 下载页面获取此文档;您应该可以获得最新版本。

  3. 安装 Java 后,将JDK/bin目录添加到您的 PATH 中,并使用以下命令设置JAVA_HOME环境变量,这些命令针对您的特定 Java 版本进行了修改:

    $ export JAVA_HOME=/opt/jdk1.6.0_24
    $ export PATH=$JAVA_HOME/bin:${PATH}
    
    

刚刚发生了什么?

这些步骤确保安装了正确版本的 Java,并且可以从命令行使用,而不必使用冗长的路径名来引用安装位置。

请记住,上述命令仅影响当前运行的 shell,在您注销、关闭 shell 或重新引导后,设置将丢失。 为了确保相同的设置始终可用,您可以将这些文件添加到您选择的 Shell 的启动文件中,例如,在 Bash Shell 的.bash_profile文件或 TCSH 的.cshrc文件中。

我喜欢的另一种方法是将所有必需的配置设置放入一个独立文件中,然后从命令行显式调用;例如:

$ source Hadoop_config.sh

这项技术允许您将多个安装文件放在同一个帐户中,而不会使 Shell 启动过于复杂;更不用说,几个应用所需的配置实际上可能不兼容。 只要记住在每次会话开始时加载文件就可以了!

设置 Hadoop

对于新手来说,Hadoop 最令人困惑的方面之一是它的各种组件、项目、子项目以及它们之间的相互关系。 事实是,这些都是随着时间的推移而演变的,这并没有让理解这一切的任务变得更容易。 不过,现在转到http://hadoop.apache.org,您会看到上面提到了三个重要的项目:

  • 常见的 / 共同的 / 普通的 / 通常的
  • HDFS
  • MapReduce

第 1 章中的解释应该对后两个问题很熟悉,公共项目包括一组帮助 Hadoop 产品在现实世界中工作的库和工具。 就目前而言,重要的是标准 Hadoop 发行版捆绑了所有这三个项目的最新版本,而这三个项目的组合正是您需要开始使用的。

版本说明

Hadoop 在从 0.19 到 0.20 版本的过渡中经历了重大变化,最引人注目的是迁移到一组用于开发 MapReduce 应用的新 API。 我们将在本书中主要使用新的 API,尽管我们在后面的章节中确实包括了一些旧的 API 的例子,因为并不是所有现有的功能都已经移植到新的 API 中。

当 0.20 分支被重命名为 1.0 时,Hadoop 版本控制也变得复杂起来。 0.22 和 0.23 分支仍然存在,实际上包含了 1.0 分支中没有包含的特性。 在撰写本文时,随着 1.1 和 2.0 分支被用于未来的开发版本,事情变得更加清晰。 由于大多数现有系统和第三方工具都是针对 0.20 分支构建的,因此我们将使用 Hadoop1.0 作为本书中的示例。

行动时间-下载 Hadoop

执行以下步骤下载 Hadoop:

  1. 访问 Hadoop 下载页面http://hadoop.apache.org/common/releases.html,检索 1.0.x 分支的最新稳定版本;在撰写本文时,版本是 1.0.4。

  2. 系统将要求您选择一个本地镜像;之后,您需要下载名称为hadoop-1.0.4``-bin.tar.gz的文件。

  3. 使用以下命令将此文件复制到您希望安装 Hadoop 的目录(例如,/usr/local):

    $ cp Hadoop-1.0.4.bin.tar.gz /usr/local
    
    
  4. 使用以下命令解压缩文件:

    $ tar –xf hadoop-1.0.4-bin.tar.gz
    
    
  5. 向 Hadoop 安装目录添加一个方便的符号链接。

    $ ln -s /usr/local/hadoop-1.0.4 /opt/hadoop
    
    
  6. 现在,您需要将 Hadoop 二进制目录添加到您的 PATH 中,并设置HADOOP_HOME环境变量,就像我们之前对 Java 所做的那样。

    $ export HADOOP_HOME=/usr/local/Hadoop
    $ export PATH=$HADOOP_HOME/bin:$PATH
    
    
  7. 进入 Hadoop 安装中的conf目录并编辑Hadoop-env.sh文件。 搜索JAVA_HOME并取消该行的注释,将位置修改为指向您的 JDK 安装,如前所述。

刚刚发生了什么?

这些步骤确保了 Hadoop 已安装并且可以从命令行使用。 通过设置 PATH 和配置变量,我们可以使用 Hadoop 命令行工具。 修改 Hadoop 配置文件是与主机设置集成所需的唯一设置更改。

如前所述,您应该将导出命令放入 shell 启动文件或在会话开始时指定的独立配置脚本中。

不要担心这里的一些细节;我们将在稍后介绍 Hadoop 的设置和使用。

操作时间-设置 SSH

执行以下步骤设置 SSH:

  1. 使用以下命令创建新的 OpenSSL 密钥对:

    $ ssh-keygen
    Generating public/private rsa key pair.
    Enter file in which to save the key (/home/hadoop/.ssh/id_rsa): 
    Created directory '/home/hadoop/.ssh'.
    Enter passphrase (empty for no passphrase): 
    Enter same passphrase again: 
    Your identification has been saved in /home/hadoop/.ssh/id_rsa.
    Your public key has been saved in /home/hadoop/.ssh/id_rsa.pub.
    …
    
    
  2. 使用以下命令将新公钥复制到授权密钥列表中:

    $ cp .ssh/id _rsa.pub  .ssh/authorized_keys 
    
    
  3. 将连接到本地主机。

    $ ssh localhost
    The authenticity of host 'localhost (127.0.0.1)' can't be established.
    RSA key fingerprint is b6:0c:bd:57:32:b6:66:7c:33:7b:62:92:61:fd:ca:2a.
    Are you sure you want to continue connecting (yes/no)? yes
    Warning: Permanently added 'localhost' (RSA) to the list of known hosts.
    
    
  4. 确认无密码 SSH 工作正常。

    $ ssh localhost
    $ ssh localhost
    
    

刚刚发生了什么?

因为 Hadoop 需要在一台或多台机器上的多个进程之间进行通信,所以我们需要确保我们用于 Hadoop 的用户可以连接到每个所需的主机,而不需要密码。 我们通过创建具有空密码的安全外壳(SSH)密钥对来实现这一点。 我们使用ssh-keygen命令启动此过程并接受提供的默认值。

创建密钥对后,我们需要将新的公钥添加到存储的可信密钥列表中;这意味着当尝试连接到这台计算机时,公钥将是可信的。 这样做之后,我们使用ssh命令连接到本地计算机,应该会收到一条有关信任主机证书的警告,如图所示。 确认这一点后,我们应该能够在没有进一步密码或提示的情况下进行连接。

备注

请注意,当我们稍后使用完全分布式集群时,我们需要确保 Hadoop 用户帐户在集群中的每台主机上都设置了相同的密钥。

配置和运行 Hadoop

到目前为止,这个都非常简单,只需要下载和系统管理。 现在我们可以直接处理 Hadoop 了。 终于来了! 我们将运行一个快速示例来展示 Hadoop 的运行情况。 还需要执行其他配置和设置,但下一步将有助于确保到目前为止安装和配置正确。

行动时间-使用 Hadoop 计算 PI

现在,我们将使用一个示例 Hadoop 程序来计算 PI 的值。 目前,这主要是为了验证安装,并展示执行 MapReduce 作业有多快。 假设HADOOP_HOME/bin目录位于您的路径中,请键入以下命令:

$ Hadoop jar hadoop/hadoop-examples-1.0.4.jar  pi 4 1000
Number of Maps  = 4
Samples per Map = 1000
Wrote input for Map #0
Wrote input for Map #1
Wrote input for Map #2
Wrote input for Map #3
Starting Job
12/10/26 22:56:11 INFO jvm.JvmMetrics: Initializing JVM Metrics with processName=JobTracker, sessionId=
12/10/26 22:56:11 INFO mapred.FileInputFormat: Total input paths to process : 4
12/10/26 22:56:12 INFO mapred.JobClient: Running job: job_local_0001
12/10/26 22:56:12 INFO mapred.FileInputFormat: Total input paths to process : 4
12/10/26 22:56:12 INFO mapred.MapTask: numReduceTasks: 1
…
12/10/26 22:56:14 INFO mapred.JobClient:  map 100% reduce 100%
12/10/26 22:56:14 INFO mapred.JobClient: Job complete: job_local_0001
12/10/26 22:56:14 INFO mapred.JobClient: Counters: 13
12/10/26 22:56:14 INFO mapred.JobClient:   FileSystemCounters
…
Job Finished in 2.904 seconds
Estimated value of Pi is 3.14000000000000000000
$

刚刚发生了什么?

这里有很多信息;当您在屏幕上获得完整输出时更是如此。 现在,让我们解开基础知识,在本书后面部分之前不要担心 Hadoop 的状态输出。 首先要澄清的是一些术语;每个 Hadoop 程序都作为一个作业运行,该作业创建多个任务来完成其工作。

查看输出,我们可以看到它大致分为三个部分:

  • 这项工作的开始
  • 作业执行时的状态
  • 作业的输出

在我们的示例中,我们可以看到作业创建了四个任务来计算 PI,而整个作业结果将是这些子结果的组合。 这个模式听起来应该与我们在第 1 章中遇到的相似;该模型用于将较大的作业拆分成较小的部分,然后将结果组合在一起。

大部分输出将在作业执行时显示,并提供显示进度的状态消息。 成功完成后,作业将打印出许多计数器和其他统计数据。 前面的示例实际上是不寻常的,因为很少在控制台上看到 MapReduce 作业的结果。 这不是 Hadoop 的限制,而是因为处理大型数据集的作业通常会产生大量的输出数据,而这些数据不太适合在屏幕上进行简单的回显。

祝贺您首次成功执行 MapReduce 作业!

三种模式

为了让在 Hadoop 上运行,我们回避了一个重要问题:我们应该在哪种模式下运行 Hadoop? 有三种可能会改变各种 Hadoop 组件的执行位置。 回想一下,HDFS 由单个 NameNode 组成,该 NameNode 充当集群协调器,并且是存储数据的一个或多个 DataNode 的主节点。 对于 MapReduce,JobTracker 是集群主机,它协调一个或多个 TaskTracker 进程执行的工作。 Hadoop 模式按如下方式部署这些组件:

  • 本地独立模式:如果与前面的 PI 示例一样,不配置任何其他内容,则这是默认模式。 在这种模式下,Hadoop 的所有组件(如 NameNode、DataNode、JobTracker 和 TaskTracker)都在单个 Java 进程中运行。
  • 伪分布式模式:在此模式下,将为每个 Hadoop 组件生成一个单独的 JVM,它们通过网络套接字进行通信,从而有效地在单个主机上提供一个功能齐全的微集群。
  • 完全分布式模式:在这种模式下,Hadoop 分布在台机器上,其中一些机器是通用工作者,另一些机器则是组件的专用主机,比如 NameNode 和 JobTracker。

每种模式都有其优点和缺点。 完全分布式模式显然是唯一可以跨机器集群扩展 Hadoop 的模式,但它需要更多的配置工作,更不用说机器集群了。 本地或独立模式最容易设置,但您与其交互的方式与完全分布式模式不同。 在本书中,我们通常更喜欢伪分布式模式,即使在单个主机上使用示例时也是如此,因为伪分布式模式中完成的所有操作几乎与它在更大的集群上的工作方式相同。

操作时间-配置伪分布式模式

看看 Hadoop 发行版中的conf目录。 有很多配置文件,但我们需要修改的是core-site.xmlhdfs-site.xmlmapred-site.xml

  1. core-site.xml修改为如下代码:

    <?xml version="1.0"?>
    <?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
    
    <!-- Put site-specific property overrides in this file. -->
    
    <configuration>
    <property>
    <name>fs.default.name</name>
    <value>hdfs://localhost:9000</value>
    </property>
    </configuration>
    
  2. hdfs-site.xml修改为如下代码:

    <?xml version="1.0"?>
    <?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
    
    <!-- Put site-specific property overrides in this file. -->
    
    <configuration>
    <property>
    <name>dfs.replication</name>
    <value>1</value>
    </property>
    </configuration>
    
  3. mapred-site.xml修改为,如以下代码所示:

    <?xml version="1.0"?>
    <?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
    
    <!-- Put site-specific property overrides in this file. -->
    
    <configuration>
    <property>
    <name>mapred.job.tracker</name>
    <value>localhost:9001</value>
    </property>
    </configuration>
    

刚刚发生了什么?

首先要注意的是这些配置文件的一般格式。 它们显然是 XML,并且在单个配置元素中包含多个属性规范。

属性规范始终包含 name 和 value 元素,并且可能包含前面代码中未显示的可选注释。

我们在这里设置了三个配置变量:

  • 变量dfs.default.name保存 NameNode 的位置,HDFS 和 MapReduce 组件都需要它,这解释了为什么它在core-site.xml而不是hdfs-site.xml中。
  • 变量dfs.replication指定每个 HDFS 块应该复制多少次。 回想一下第 1 章的全部内容,HDFS 通过确保将每个文件系统数据块复制到多个不同的主机(通常是 3 个)来处理故障。由于我们在伪分布式模式下只有一个主机和一个 DataNode,因此我们将此值更改为1
    ** 变量mapred.job.tracker保存 JobTracker 的位置,就像dfs.default.name保存 NameNode 的位置一样。 因为只有 MapReduce 组件需要知道这个位置,所以它在mapred-site.xml中。*

*### 备注

当然,您可以自由更改所使用的端口号,尽管 9000 和 9001 是 Hadoop 中的常见约定。

NameNode 和 JobTracker 的网络地址指定实际系统请求应该定向到的端口。 这些都不是面向用户的位置,所以不用费心把你的网络浏览器指向它们。 我们很快就会看到一些 Web 界面。

配置基本目录并格式化文件系统

如果选择伪分布式或完全分布式模式,则在启动第一个 Hadoop 集群之前,需要执行两个步骤。

  1. 设置存储 Hadoop 文件的基本目录。
  2. 格式化 HDFS 文件系统。

备注

准确地说,我们不需要更改默认目录;但是,正如后面看到的,现在考虑一下是一件好事。

执行操作的时间-更改基本 HDFS 目录

让我们首先设置基本目录,该目录指定 Hadoop 将在其下保存所有数据的本地文件系统上的位置。 执行以下步骤:

  1. 创建 Hadoop 将在其中存储其数据的目录:

    $ mkdir /var/lib/hadoop
    
    
  2. 确保该目录可由任何用户写入:

    $ chmod 777 /var/lib/hadoop
    
    
  3. 再次修改core-site.xml以添加以下属性:

    <property>
    <name>hadoop.tmp.dir</name>
    <value>/var/lib/hadoop</value>
    </property>
    

刚刚发生了什么?

由于我们将在 Hadoop 中存储数据,并且所有各种组件都在本地主机上运行,因此这些数据需要存储在本地文件系统的某个位置。 无论采用哪种模式,Hadoop 默认使用hadoop.tmp.dir属性作为写入所有文件和数据的基目录。

例如,MapReduce 使用此基本目录下的/mapred目录;HDFS 使用/dfs。 危险在于,hadoop.tmp.dir的默认值是/tmp,一些 Linux 发行版在每次重新启动时都会删除/tmp的内容。 因此,显式声明将数据保存在何处更为安全。

操作时间-格式化 NameNode

在首次以伪分布式或完全分布式模式启动 Hadoop 之前,我们需要格式化它将使用的 HDFS 文件系统。 键入以下内容:

$  hadoop namenode -format

此命令的输出应如下所示:

$ hadoop namenode -format
12/10/26 22:45:25 INFO namenode.NameNode: STARTUP_MSG: 
/************************************************************
STARTUP_MSG: Starting NameNode
STARTUP_MSG:   host = vm193/10.0.0.193
STARTUP_MSG:   args = [-format]
…
12/10/26 22:45:25 INFO namenode.FSNamesystem: fsOwner=hadoop,hadoop
12/10/26 22:45:25 INFO namenode.FSNamesystem: supergroup=supergroup
12/10/26 22:45:25 INFO namenode.FSNamesystem: isPermissionEnabled=true
12/10/26 22:45:25 INFO common.Storage: Image file of size 96 saved in 0 seconds.
12/10/26 22:45:25 INFO common.Storage: Storage directory /var/lib/hadoop-hadoop/dfs/name has been successfully formatted.
12/10/26 22:45:26 INFO namenode.NameNode: SHUTDOWN_MSG: 
/************************************************************
SHUTDOWN_MSG: Shutting down NameNode at vm193/10.0.0.193
$ 

刚刚发生了什么?

这不是一个非常令人兴奋的输出,因为该步骤只是为我们将来使用 HDFS 提供支持。 但是,它确实帮助我们将 HDFS 看作一个文件系统;就像任何操作系统上的任何新存储设备一样,我们需要格式化设备才能使用它。 HDFS 也是如此;最初,文件系统数据有一个默认位置,但没有对应的文件系统索引的实际数据。

备注

每次都这么做!

如果您使用 Hadoop 的经历与我类似,那么在设置新安装时会经常犯一系列简单的错误。 很容易忘记 NameNode 的格式化,然后在尝试第一个 Hadoop 活动时收到一系列失败消息。

但是只做一次!

格式化 NameNode 的命令可以执行多次,但这样做将销毁所有现有的文件系统数据。 它只能在 Hadoop 集群关闭时执行,有时您会想要这样做,但在大多数其他情况下,它是不可撤销地删除 HDFS 上的每一条数据的快捷方法;在大型集群上需要的时间要长得多。 所以要小心!

启动和使用 Hadoop

在完成所有配置和设置之后,现在让我们启动我们的集群,并实际对其执行一些操作。

启动 Hadoop 行动的时间到了

与 Hadoop 的本地模式不同,在本地模式下,所有组件仅在提交的作业的生命周期内运行,而在 Hadoop 的伪分布式或完全分布式模式下,集群组件作为长时间运行的进程存在。 在使用 HDFS 或 MapReduce 之前,我们需要启动所需的组件。 键入以下命令;输出应如下所示,其中命令包含在以$为前缀的行中:

  1. 键入第一个命令:

    $ start-dfs.sh
    starting namenode, logging to /home/hadoop/hadoop/bin/../logs/hadoop-hadoop-namenode-vm193.out
    localhost: starting datanode, logging to /home/hadoop/hadoop/bin/../logs/hadoop-hadoop-datanode-vm193.out
    localhost: starting secondarynamenode, logging to /home/hadoop/hadoop/bin/../logs/hadoop-hadoop-secondarynamenode-vm193.out
    
    
  2. 键入第二个命令:

    $ jps
    9550 DataNode
    9687 Jps
    9638 SecondaryNameNode
    9471 NameNode
    
    
  3. 键入第三个命令:

    $ hadoop dfs -ls /
    Found 2 items
    drwxr-xr-x   - hadoop supergroup          0 2012-10-26 23:03 /tmp
    drwxr-xr-x   - hadoop supergroup          0 2012-10-26 23:06 /user
    
    
  4. 键入第四个命令:

    $ start-mapred.sh 
    starting jobtracker, logging to /home/hadoop/hadoop/bin/../logs/hadoop-hadoop-jobtracker-vm193.out
    localhost: starting tasktracker, logging to /home/hadoop/hadoop/bin/../logs/hadoop-hadoop-tasktracker-vm193.out
    
    
  5. 键入第五个命令:

    $ jps
    9550 DataNode
    9877 TaskTracker
    9638 SecondaryNameNode
    9471 NameNode
    9798 JobTracker
    9913 Jps
    
    

刚刚发生了什么?

顾名思义,start-dfs.sh命令启动 HDFS 所需的组件。 这是用于管理文件系统的 NameNode 和用于保存数据的单个 DataNode。 Second daryNameNode 是一个可用性辅助工具,我们将在后面的章节中讨论。

在启动这些组件之后,我们使用 JDK 的jps实用程序来查看哪些 Java 进程正在运行,如果输出看起来不错,那么我们就可以使用 Hadoop 的dfs实用程序来列出 HDFS 文件系统的根目录。

在此之后,我们使用start-mapred.sh启动 MapReduce 组件-这次是 JobTracker 和单个 TaskTracker-然后再次使用jps验证结果。

还有一个组合的start-all.sh文件,我们将在稍后阶段使用它,但在早期,执行两个阶段的启动会更容易地验证集群配置,这是很有用的。

行动时间-使用 HDFS

如前面的示例所示,HDFS 有一个看起来很熟悉的界面,它允许我们使用与 Unix 中的命令类似的命令来操作文件系统上的文件和目录。 让我们通过键入以下命令来试用一下:

键入以下命令:

$ hadoop -mkdir /user
$ hadoop -mkdir /user/hadoop
$ hadoop fs -ls /user
Found 1 items
drwxr-xr-x   - hadoop supergroup          0 2012-10-26 23:09 /user/Hadoop
$ echo "This is a test." >> test.txt
$ cat test.txt
This is a test.
$ hadoop dfs -copyFromLocal test.txt  .
$ hadoop dfs -ls
Found 1 items
-rw-r--r--   1 hadoop supergroup         16 2012-10-26 23:19/user/hadoop/test.txt
$ hadoop dfs -cat test.txt
This is a test.
$ rm test.txt 
$ hadoop dfs -cat test.txt
This is a test.
$ hadoop fs -copyToLocal test.txt
$ cat test.txt
This is a test.

刚刚发生了什么?

本例显示了 Hadoop 实用程序的fs子命令的用法。 请注意,dfsfs命令都是等效的)。 与大多数文件系统一样,Hadoop 为每个用户提供了主目录的概念。 这些主目录存储在 HDFS 上的/user目录下,如果主目录尚不存在,则在进一步操作之前先创建它。

然后,我们在本地文件系统上创建一个简单的文本文件,并使用copyFromLocal命令将其复制到 HDFS,然后使用-ls-cat实用程序检查其存在和内容。 可以看到,用户主目录别名为.,因为在 Unix 中,假定未指定路径的-ls命令引用该位置,而相对路径(不是以/开头)将从该位置开始。

然后,我们从本地文件系统中删除该文件,使用-copyToLocal命令将其从 HDFS 复制回来,并使用本地cat实用程序检查其内容。

备注

如上例所示,混合使用 HDFS 和本地文件系统命令是一种强大的组合,可以非常容易地在用于本地文件系统的 HDFS 命令上执行,反之亦然。 所以要小心,特别是在删除的时候。

还有其他 HDFS 操作命令;有关详细列表,请尝试Hadoop fs -help

行动字数时间到了,MapReduce 的 Hello World

随着时间的推移,许多应用()都获得了一个规范的示例,这是任何初学者指南都不应该缺少的。 对于 Hadoop,这是 wordcount-与 Hadoop 捆绑在一起的一个示例,用于统计输入文本文件中的单词频率。

  1. 首先执行以下命令:

    $ hadoop dfs -mkdir data
    $ hadoop dfs -cp test.txt data
    $ hadoop dfs -ls data
    Found 1 items
    -rw-r--r--   1 hadoop supergroup         16 2012-10-26 23:20 /user/hadoop/data/test.txt
    
    
  2. 现在执行以下命令:

    $ Hadoop Hadoop/hadoop-examples-1.0.4.jar  wordcount data out
    12/10/26 23:22:49 INFO input.FileInputFormat: Total input paths to process : 1
    12/10/26 23:22:50 INFO mapred.JobClient: Running job: job_201210262315_0002
    12/10/26 23:22:51 INFO mapred.JobClient:  map 0% reduce 0%
    12/10/26 23:23:03 INFO mapred.JobClient:  map 100% reduce 0%
    12/10/26 23:23:15 INFO mapred.JobClient:  map 100% reduce 100%
    12/10/26 23:23:17 INFO mapred.JobClient: Job complete: job_201210262315_0002
    12/10/26 23:23:17 INFO mapred.JobClient: Counters: 17
    12/10/26 23:23:17 INFO mapred.JobClient:   Job Counters 
    12/10/26 23:23:17 INFO mapred.JobClient:     Launched reduce tasks=1
    12/10/26 23:23:17 INFO mapred.JobClient:     Launched map tasks=1
    12/10/26 23:23:17 INFO mapred.JobClient:     Data-local map tasks=1
    12/10/26 23:23:17 INFO mapred.JobClient:   FileSystemCounters
    12/10/26 23:23:17 INFO mapred.JobClient:     FILE_BYTES_READ=46
    12/10/26 23:23:17 INFO mapred.JobClient:     HDFS_BYTES_READ=16
    12/10/26 23:23:17 INFO mapred.JobClient:     FILE_BYTES_WRITTEN=124
    12/10/26 23:23:17 INFO mapred.JobClient:     HDFS_BYTES_WRITTEN=24
    12/10/26 23:23:17 INFO mapred.JobClient:   Map-Reduce Framework
    12/10/26 23:23:17 INFO mapred.JobClient:     Reduce input groups=4
    12/10/26 23:23:17 INFO mapred.JobClient:     Combine output records=4
    12/10/26 23:23:17 INFO mapred.JobClient:     Map input records=1
    12/10/26 23:23:17 INFO mapred.JobClient:     Reduce shuffle bytes=46
    12/10/26 23:23:17 INFO mapred.JobClient:     Reduce output records=4
    12/10/26 23:23:17 INFO mapred.JobClient:     Spilled Records=8
    12/10/26 23:23:17 INFO mapred.JobClient:     Map output bytes=32
    12/10/26 23:23:17 INFO mapred.JobClient:     Combine input records=4
    12/10/26 23:23:17 INFO mapred.JobClient:     Map output records=4
    12/10/26 23:23:17 INFO mapred.JobClient:     Reduce input records=4
    
    
  3. 执行以下命令:

    $ hadoop fs -ls out
    Found 2 items
    drwxr-xr-x   - hadoop supergroup          0 2012-10-26 23:22 /user/hadoop/out/_logs
    -rw-r--r--   1 hadoop supergroup         24 2012-10-26 23:23 /user/hadoop/out/part-r-00000
    
    
  4. 现在执行此命令:

    $ hadoop fs -cat out/part-0-00000
    This  1
    a  1
    is  1
    test.  1
    
    

刚刚发生了什么?

我们在这里做了三件事,具体如下:

  • 已将先前创建的文本文件移动到 HDFS 上的新目录中
  • 运行示例 WordCount 作业,将此新目录和不存在的输出目录指定为参数
  • 使用fs实用程序检查 MapReduce 作业的输出

正如我们前面所说的,伪分布式模式有更多的 Java 进程,因此作业输出比独立 PI 短得多似乎很奇怪。 原因是本地独立模式将有关每个单独任务执行的信息打印到屏幕上,而在其他模式中,此信息仅写入正在运行的主机上的日志文件。

输出目录是由 Hadoop 自己创建的,实际的结果文件遵循这里所示的 part-nnnnn约定;尽管给出了我们的设置,但只有一个结果文件。 我们使用fs -cat命令检查该文件,结果与预期一致。

备注

如果将现有目录指定为 Hadoop 作业的输出源,则该作业将无法运行,并将抛出异常,抱怨已存在的目录。 如果希望 Hadoop 将输出存储到某个目录,则该目录不能存在。 将其视为一种安全机制,可以阻止 Hadoop 重写以前有价值的作业运行以及您经常忘记确定的内容。 如果您有信心,您可以重写此行为,我们稍后将看到这一点。

PI 和 Wordcount 程序只是 Hadoop 附带的一些示例。 下面是如何获取所有这些内容的列表。 看看你能不能弄清楚其中的一些。

$ hadoop jar hadoop/hadoop-examples-1.0.4.jar 

在更大的正文上做个围棋英雄字数

运行像 Hadoop 这样复杂的框架,使用 5 个离散的 Java 进程来计算单行文本文件中的单词并不令人印象深刻。 强大的功能来自这样一个事实,即我们可以使用完全相同的程序对更大的文件,甚至是分布在多节点 Hadoop 集群中的海量文本语料库运行 WordCount。 如果我们有这样的设置,我们将通过运行程序并简单地指定源和输出数据的目录位置来执行与刚才完全相同的命令。

找到一个大型在线文本文件-http://www.gutenberg.org处的 Project Gutenberg 是一个很好的起点-并通过将其复制到 HDFS 并执行 wordcount 示例来对其运行 wordcount。 输出可能与您预期的不同,因为在大量文本中,需要解决脏数据、标点符号和格式化问题。 考虑如何改进字数统计;我们将在下一章研究如何将其扩展为更复杂的处理链。

从浏览器监控 Hadoop

到目前为止,我们一直依赖命令行工具和直接命令输出来查看我们的系统在做什么。 Hadoop 提供了两个您应该熟悉的 Web 界面,一个用于 HDFS,另一个用于 MapReduce。 这两个工具在伪分布式模式下都很有用,并且在完全分布式安装时都是关键工具。

HDFS Web 用户界面

将您的 Web 浏览器指向运行 Hadoop 的主机上的端口 50030。 默认情况下,本地主机和任何其他可以访问网络的计算机都应该可以使用 Web 接口。 以下是示例屏幕截图:

The HDFS web UI

这里发生了很多事情,但紧随其后的关键数据告诉我们集群中的节点数、文件系统大小、已用空间和链接,以便深入了解更多信息,甚至浏览文件系统。

花点时间操作这个界面;它需要熟悉。 对于多节点群集,有关活动节点和死节点的信息以及有关其状态历史的详细信息对于调试群集问题至关重要。

MapReduce Web 用户界面

默认情况下,JobTracker UI 在端口 50070 上可用,并且适用前面所述的相同访问规则。 以下是示例屏幕截图:

The MapReduce web UI

这比 HDFS 接口更复杂! 除了活节点数/死节点数的类似计数外,还有自启动以来执行的作业数的历史记录及其单个任务计数的细目。

正在执行的作业和历史作业列表是获取更多信息的入口;对于每个作业,我们可以访问每个节点上的每个任务尝试的历史记录,并访问日志以获取详细信息。 现在,我们将介绍使用任何分布式系统时最痛苦的部分之一:调试。 这可能真的很难。

假设您有一个由 100 台机器组成的集群,试图处理一个海量数据集,其中完整的作业需要每台主机执行数百个 map 和 Reduce 任务。 如果作业开始运行非常慢或明显失败,问题所在并不总是显而易见。 查看 MapReduce web 用户界面可能是第一站,因为它为调查运行和历史作业的健康状况提供了丰富的起点。

使用弹性 MapReduce

我们现在将转向云中的 Hadoop,这是 Amazon Web Services 提供的 Elastic MapReduce 服务。 访问 EMR 的方式有多种,但现在我们将重点放在提供的 Web 控制台上,将 Hadoop 的完整点击式方法与前面的命令行驱动示例进行对比。

在 Amazon Web Services 中设置帐户

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

创建 AWS 帐户

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

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

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

注册必要的服务

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

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

备注

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

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

使用管理控制台对电子病历进行操作字数统计的时间

让我们使用提供的一些示例代码直接跳到关于 EMR 的示例。 执行以下步骤:

  1. Browse to http://aws.amazon.com, go to Developers | AWS Management Console, and then click on the Sign in to the AWS Console button. The default view should look like the following screenshot. If it does not, click on Amazon S3 from within the console.

    Time for action – WordCount on EMR using the management console

  2. 如前面的屏幕截图所示,单击创建存储桶按钮,然后输入新存储桶的名称。 存储桶名称在所有 AWS 用户中必须是全局唯一的,因此不要期望mybuckets3test等明显的存储桶名称可用。

  3. Click on the Region drop-down menu and select the geographic area nearest to you.

    Time for action – WordCount on EMR using the management console

  4. Click on the Elastic MapReduce link and click on the Create a new Job Flow button. You should see a screen like the following screenshot:

    Time for action – WordCount on EMR using the management console

  5. You should now see a screen like the preceding screenshot. Select the Run a sample application radio button and the Word Count (Streaming) menu item from the sample application drop-down box and click on the Continue button.

    Time for action – WordCount on EMR using the management console

  6. The next screen, shown in the preceding screenshot, allows us to specify the location of the output produced by running the job. In the edit box for the output location, enter the name of the bucket created in step 1 (garryt1use is the bucket we are using here); then click on the Continue button.

    Time for action – WordCount on EMR using the management console

  7. The next screenshot shows the page where we can modify the number and size of the virtual hosts utilized by our job. Confirm that the instance type for each combo box is Small (m1.small), and the number of nodes for the Core group is 2 and for the Task group it is 0. Then click on the Continue button.

    Time for action – WordCount on EMR using the management console

  8. This next screenshot involves options we will not be using in this example. For the Amazon EC2 key pair field, select the Proceed without key pair menu item and click on the No radio button for the Enable Debugging field. Ensure that the Keep Alive radio button is set to No and click on the Continue button.

    Time for action – WordCount on EMR using the management console

  9. The next screen, shown in the preceding screenshot, is one we will not be doing much with right now. Confirm that the Proceed with no Bootstrap Actions radio button is selected and click on the Continue button.

    Time for action – WordCount on EMR using the management console

  10. Confirm the job flow specifications are as expected and click on the Create Job Flow button. Then click on the View my Job Flows and check status buttons. This will give a list of your job flows; you can filter to show only running or completed jobs. The default is to show all, as in the example shown in the following screenshot:

![Time for action – WordCount on EMR using the management console](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/hadoop-begin-guide/img/7300_02_11.jpg)
  1. Occasionally hit the Refresh button until the status of the listed job, Running or Starting, changes to Complete; then click its checkbox to see details of the job flow, as shown in the following screenshot:
![Time for action – WordCount on EMR using the management console](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/hadoop-begin-guide/img/7300_02_12.jpg)
  1. Click the S3 tab and select the bucket you created for the output location. You will see it has a single entry called wordcount, which is a directory. Right-click on that and select Open. Then do the same until you see a list of actual files following the familiar Hadoop part-nnnnn naming scheme, as shown in the following screenshot:
![Time for action – WordCount on EMR using the management console](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/hadoop-begin-guide/img/7300_02_13.jpg)

右键单击**Part-00000**并打开它。 它应该看起来像这样:

```scala
a              14716
aa             52
aakar          3
aargau         3
abad           3
abandoned      46
abandonment    6
abate          9
abauj          3
abbassid       4
abbes          3
abbl           3
…
```

这种类型的输出看起来熟悉吗?

刚刚发生了什么?

第一步是处理 S3,而不是 EMR。 S3 是一种可伸缩的存储服务,允许您将文件(称为对象)存储在称为存储桶的容器中,并通过存储桶和对象键(即名称)访问对象。 该模型类似于文件系统的使用,尽管存在潜在的差异,但在本书中它们不太可能是重要的。

S3 是放置想要在 EMR 中处理的 MapReduce 程序和源数据的地方,也是存储 EMR Hadoop 作业的输出和日志的地方。 有太多第三方工具可以访问 S3,但这里我们使用的是 AWS 管理控制台,这是大多数 AWS 服务的浏览器界面。

虽然我们建议您为 S3 选择最近的地理区域,但这不是必需的;非美国位置通常会为距离他们较近的客户提供更好的延迟,但它们的成本也往往略高。 在考虑所有这些因素之后,您需要决定在哪里托管数据和应用。

在创建 S3 存储桶之后,我们移动到 EMR 控制台并创建了一个新的作业流。 此术语在电子病历中用于指代数据处理任务。 正如我们将看到的,这可以是一次性交易,其中底层 Hadoop 集群是按需创建和销毁的,也可以是在其上执行多个作业的长期运行的集群。

我们保留了默认的作业流名称,然后选择使用示例应用,在本例中是 wordcount 的 Python 实现。 术语 Hadoop Streaming 指的是一种允许使用脚本语言编写映射和减少任务的机制,但其功能与我们之前使用的 Java 字数相同。

指定作业流的表单需要源数据、程序、映射和还原类的位置,以及输出数据的所需位置。 对于我们刚才看到的示例,大多数字段都是预先填充的;可以看到,与从命令行运行本地 Hadoop 时所需的内容有明显的相似之处。

通过不选择Keep Alive选项,我们选择了专门为执行此作业而创建的 Hadoop 集群,然后销毁该集群。 这样的群集将具有更长的启动时间,但会将成本降至最低。 如果您选择保持作业流处于活动状态,您将看到额外的作业执行得更快,因为您不必等待集群启动。 但是,在您显式终止作业流之前,您将被收取底层 EC2 资源的费用。

确认之后,我们不需要添加任何额外的引导选项;我们选择了要部署到 Hadoop 集群中的主机数量和类型。 EMR 区分三个不同的主机组:

  • 主组:这是托管 NameNode 和 JobTracker 的控制节点。 这些只有 1 个。
  • 核心组:这些节点同时运行 HDFS DataNodes 和 MapReduce TaskTracker。 主机数量是可配置的。
  • 任务组:这些主机不保存 HDFS 数据,但运行 TaskTracker,可以提供更强的处理能力。 主机数量是可配置的。

主机类型指的是不同级别的硬件功能,详细信息可以在 EC2 页面上找到。 较大的主机功能更强大,但成本更高。 目前,默认情况下,一个作业流中的主机总数必须在 20 个或更少,尽管亚马逊有一个简单的表单来请求更高的限制。

确认后,一切正常-我们启动作业流并在控制台上监视它,直到状态更改为已完成。 此时,我们返回 S3,查看我们指定为输出目的地的存储桶内部,并检查我们的 wordcount 作业的输出,它看起来应该与本地 Hadoop wordcount 的输出非常相似。

一个显而易见的问题是,源数据从何而来? 这是我们在创建过程中看到的作业流规范中预先填充的字段之一。 对于非持久性作业流,最常见的模型是从指定的 S3 源位置读取源数据,并将结果数据写入指定的结果 S3 存储桶。

就是这样! AWS 管理控制台允许从浏览器对 S3 和 EMR 等服务进行细粒度控制。 只需一个浏览器和一张信用卡,我们就可以启动 Hadoop 作业来处理数据,而不必担心安装、运行或管理 Hadoop 的任何技术问题。

来个围棋英雄--其他电子病历示例应用

EMR 提供了其他几个示例应用。 为什么不试一试呢?

电子病历的其他使用方式

尽管 AWS 管理控制台是一款功能强大且令人印象深刻的工具,但它并不总是我们想要访问 S3 和运行 EMR 作业的方式。 与所有 AWS 服务一样,可以使用编程工具和命令行工具来使用这些服务。

AWS 凭据

但是,在使用编程工具或命令行工具之前,我们需要了解帐户持有人如何验证 AWS 发出此类请求。 由於这些都是收费的服务,我们真的不希望别人代我们提出要求。 请注意,由于我们在前面的示例中使用我们的 AWS 帐户直接登录到 AWS 管理控制台,因此我们不必担心这一点。

每个 AWS 帐户都有几个在访问各种服务时使用的标识符:

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

如果这听起来令人困惑,那是因为事实的确如此。 至少一开始是这样。 然而,在使用工具访问 AWS 服务时,通常只需先将正确的凭据添加到已配置的文件中,然后一切就可以正常工作了。 但是,如果您确实决定探索编程工具或命令行工具,那么花点时间阅读每个服务的文档以了解其安全性是如何工作的,这将是值得的。

电子病历命令行工具

在本书中,我们不会对 S3 和 EMR 执行任何无法从 AWS 管理控制台执行的操作。 但是,在处理运营工作负载、希望集成到其他工作流中或自动化服务访问时,基于浏览器的工具无论功能有多强大,都不合适。 使用服务的直接编程接口提供了最精细的控制,但需要最大的努力。

Amazon 为许多服务提供了一组命令行工具,这些工具提供了自动访问 AWS 服务的有用方式,从而最大限度地减少了所需的开发量。 如果您想要一个更基于 CLI 的 EMR 界面,但还不想编写定制代码,那么从 EMR 主页链接的 Elastic MapReduce 命令行工具值得一看。

AWS 生态系统

每个 AWS 服务还拥有过多的第三方工具、服务和库,这些工具、服务和库可以提供不同的服务访问方式、提供其他功能或提供新的实用程序。 请查看位于http://aws.amazon.com/developertools的开发人员工具中心,作为起点。

本地 Hadoop 与 EMR Hadoop 的比较

在我们第一次体验了本地 Hadoop 集群及其在 EMR 中的等价物之后,这是我们考虑两种方法差异的好时机。

很明显,关键区别并不在于功能;如果我们想要的只是一个运行 MapReduce 作业的环境,那么这两种方法都完全适合。 相反,区别特征围绕着我们在第 1 章中提到的一个主题,即您是否更喜欢涉及前期基础设施成本和持续维护工作的成本模型,而不是维护负担更低、概念上具有无限可伸缩性的现收现付模式。 除了成本决定之外,还有几件事需要牢记:

** EMR 支持特定版本的 Hadoop,并有随时间升级的策略。 如果您需要特定的版本,特别是在发布后立即需要最新最好的版本,那么在 EMR 上线之前的延迟可能是不可接受的。

  • 您可以启动一个持久的 EMR 作业流,并像对待本地 Hadoop 集群一样对待它,登录到托管节点并调整它们的配置。 如果您发现自己在这么做,那么是否真的需要这种级别的控制值得一问,如果需要,是否会阻止您获得迁移到 EMR 所带来的所有成本模型好处?
  • 如果这确实归结为成本考虑,请记住将本地群集的所有隐藏成本考虑在内,这些成本经常被遗忘。 考虑一下电力、空间、冷却和设施的成本。 更不用说管理开销了,如果事情在凌晨开始中断,这可能不是微不足道的。

摘要

我们在本章中介绍了很多关于 Hadoop 集群的启动和运行以及在其上执行 MapReduce 程序的内容。

具体地说,我们介绍了在本地 Ubuntu 主机上运行 Hadoop 的前提条件。 我们还了解了如何在独立或伪分布式模式下安装和配置本地 Hadoop 集群。 然后,我们了解了如何访问 HDFS 文件系统和提交 MapReduce 作业。 然后,我们继续学习访问 Elastic MapReduce 和其他 AWS 服务需要哪些帐户。

我们了解了如何使用 AWS 管理控制台浏览和创建 S3 存储桶和对象,以及如何创建作业流并使用它在 EMR 托管的 Hadoop 群集上执行 MapReduce 作业。 我们还讨论了访问 AWS 服务的其他方式,并研究了本地 Hadoop 和 EMR 托管 Hadoop 之间的差异。

既然我们已经了解了如何在本地或在 EMR 上运行 Hadoop,我们就可以开始编写我们自己的 MapReduce 程序了,这将是下一章的主题。**

三、了解 MapReduce

前两章讨论了 Hadoop 允许我们解决的问题,并提供了一些运行示例 MapReduce 作业的实践经验。 有了这个基础,我们现在将更深入一些。

在本章中,我们将介绍:

  • 了解键/值对如何成为 Hadoop 任务的基础
  • 了解 MapReduce 作业的各个阶段
  • 详细检查 MAP、RECESS 和可选组合阶段的工作原理
  • 查看用于 Hadoop 的 Java API,并使用它开发一些简单的 MapReduce 作业
  • 了解 Hadoop 输入和输出

键/值对

第 1 章的全部内容开始,我们一直在讨论以键/值对的形式处理和提供输出的操作,但没有解释原因。 现在是解决这个问题的时候了。

*## 这意味着什么

首先,我们将通过强调 Java 标准库中的类似概念来阐明我们所说的键/值对是什么意思。 java.util.Map接口是常用类的父类,比如HashMap,甚至是原始的Hashtable类(通过一些库的向后重新设计)。

对于任何 JavaMap对象,其内容是从指定类型的给定键到潜在不同类型的相关值的一组映射。 例如,HashMap对象可以包含从人名(String)到其生日(Date)的映射。

在 Hadoop 上下文中,我们指的是还包含与关联值相关的键的数据。 该数据以这样一种方式存储,即可以跨一组键对数据集中的各种值进行排序和重新排列。 如果我们使用键/值数据,那么提出如下问题将是有意义的:

  • 给定键在数据集中是否有映射?
  • 与给定键关联的值是什么?
  • 全套钥匙是什么?

回想上一章的字数。 我们稍后将对其进行更详细的介绍,但是该程序的输出显然是一组键/值关系;对于每个单词(键),都有其出现次数的计数(值)。 考虑这个简单的例子,键/值数据的一些重要特性将变得显而易见,如下所示:

  • 键必须是唯一的,但值不必是
  • 每个值都必须与一个键相关联,但是一个键可以没有值(尽管在本例中不是这样)
  • 仔细定义键很重要;决定计数是否区分大小写将产生不同的结果

备注

请注意,我们需要仔细定义这里的键是唯一的是什么意思。 这并不意味着键只出现一次;在我们的数据集中,我们可能会看到一个键出现多次,正如我们将看到的,MapReduce 模型有一个阶段,其中与每个键相关联的所有值都被收集在一起。 键的唯一性保证了如果我们将任何给定键的每个值收集在一起,结果将是从键的单个实例到以这种方式映射的每个值的关联,并且不会遗漏任何值。

为什么选择键/值数据?

使用键/值数据作为 MapReduce 操作的基础,可以实现一个功能强大的编程模型,该模型具有惊人的广泛适用性,这可以从 Hadoop 和 MapReduce 在各种行业和问题场景中的采用中看出。 许多数据本质上要么是键/值,要么可以用这种方式表示。 它是一个简单的模型,具有广泛的适用性和足够直接的语义,根据它定义的程序可以由 Hadoop 这样的框架应用。

当然,数据模型本身并不是使 Hadoop 变得有用的唯一因素;它的真正威力在于它如何使用并行执行技术,并在第 1 章中讨论了它到底是什么。 我们可以拥有大量的主机,在这些主机上我们可以存储和执行数据,甚至可以使用一个框架来管理将较大的任务划分为较小的块,以及将部分结果组合成整体答案。 但是我们需要这个框架来为我们提供一种表达问题的方式,而不需要我们成为执行机制方面的专家;我们希望表达数据所需的转换,然后让框架来完成其余的工作。 MapReduce 及其键/值接口提供了这样一个抽象级别,程序员只需指定这些转换,Hadoop 就可以处理将其应用于任意大型数据集的复杂过程。

一些真实世界的例子

为了使不那么抽象,让我们考虑一些实际数据,它是键/值对:

  • 地址簿将姓名(关键字)与联系信息(值)相关联
  • 银行帐户使用帐号(密钥)与帐户详细信息(值)相关联
  • 一本书的索引将一个单词(关键字)与它出现的页面(值)联系起来
  • 在计算机文件系统中,文件名(键)允许访问任何类型的数据,如文本、图像和声音(值)

这些示例有意扩大范围,以帮助和鼓励您认为键/值数据不是仅在高端数据挖掘中使用的受限制的模型,而是我们周围的一个非常常见的模型。

如果这对 Hadoop 不重要,我们就不会进行这个讨论。 底线是,如果数据可以表示为键/值对,那么它就可以由 MapReduce 处理。

MapReduce 作为一系列键/值转换

您可能已经见过中键/值转换方面描述的 MapReduce,特别是看起来像这样的令人生畏的 MapReduce:

{K1,V1} -> {K2, List<V2>} -> {K3,V3}

我们现在能够理解这意味着什么:

  • MapReduce 作业的map方法的输入是一系列键/值对,我们称之为K1V1
  • map方法的输出(因此也是reduce方法的输入)是一系列键和相关的值列表,称为K2V2。 请注意,每个映射器只输出一系列单独的键/值输出;在shuffle方法中,这些输出组合为键和值列表。
  • MapReduce 作业的最终输出是另一系列键/值对,称为K3V3

这组键/值对不必不同;很可能输入(比方说)姓名和联系方式,然后输出相同的内容,可能会使用一些中间格式来整理信息。 在我们接下来探索用于 MapReduce 的 Java API 时,请记住这个三阶段模型。 我们将首先介绍您需要的 API 的主要部分,然后系统地检查 MapReduce 作业的执行情况。

弹出测验-键/值对

问题 1.。 键/值对的概念是…

  1. 由 Hadoop 创建并特定于 Hadoop 的东西。
  2. 一种表达我们经常看到但没有想到的关系的方式。
  3. 来自计算机科学的学术概念。

Q2.。 用户名/密码组合是密钥/值数据的示例吗?

  1. 是的,这是一个很明显的例子,一个值与另一个值相关联。
  2. 不,密码更像是用户名的属性,没有索引型关系。
  3. 我们通常不会这样认为,但 Hadoop 仍然可以将一系列用户名/密码组合处理为键/值对。

Hadoop Java API for MapReduce

Hadoop 在其 0.20 版本中经历了重大的 API 更改,这是我们在本书中使用的 1.0 版本中的主要接口。 尽管之前的 API 当然是有效的,但社区觉得它在某些方面很笨拙和不必要的复杂。

新的 API(有时通常称为上下文对象)是 Java MapReduce 开发的未来,因此我们将在本书中尽可能地使用它。 请注意,注意:0.20 之前的 MapReduce 库的某些部分尚未移植到新 API,因此当我们需要检查其中任何一个时,我们将使用旧接口。

0.20 MapReduce Java API

以上版本的 MapReduce API 的 0.20 和在org.apache.hadoop.mapreduce包或其子包中包含大多数关键类和接口。

在大多数情况下,MapReduce 作业的实现将提供该包中的MapperReducer基类的特定于作业的子类。

备注

我们将坚持使用常用的K1/K2/K3/等术语,尽管最近 Hadoop API 在某些地方使用了KEYIN/VALUEINKEYOUT/VALUEOUT等术语。 目前,我们将继续使用K1/K2/K3,因为它有助于理解端到端数据流。

Mapper 类

这是 Hadoop 提供的基础Mapper类的简化视图。 对于我们自己的映射器实现,我们将子类这个基类并覆盖指定的方法,如下所示:

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

尽管 Java 泛型的使用一开始可能会让这看起来有点不透明,但实际上并没有那么多事情发生。 该类是根据键/值输入和输出类型定义的,然后map方法在其参数中接受输入键/值对。 另一个参数是Context类的实例,它提供了与 Hadoop 框架通信的各种机制,其中之一是输出mapreduce方法的结果。

提示

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

有时可能需要重写三个附加方法。

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

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

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

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

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

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

提示

下载示例代码

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

Reducer 类

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

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

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

该类还有setupruncleanup方法,它们的默认实现与相似,Mapper类可以选择性地覆盖:

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

此方法在将任何键/值列表呈现给reduce方法之前调用一次。 默认的实现不执行任何操作。

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

此方法在所有键/值列表都已呈现给reduce方法之后调用一次。 默认实现不执行任何操作。

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

此方法控制 JVM 中处理任务的总体流程。 默认实现在为提供给Reducer类的所有键/值重复调用reduce方法之前调用setup方法,然后最后调用cleanup方法。

驱动程序类

尽管我们的映射器和 Reducer 实现是我们执行 MapReduce 作业所需的全部,但是还需要另外一段代码:与 Hadoop 框架通信并指定运行 MapReduce 作业所需的配置元素的驱动程序。 这涉及到一些方面,比如告诉 Hadoop 使用哪个MapperReducer类、在哪里查找输入数据以及以什么格式查找输入数据、在哪里放置输出数据以及如何格式化输出数据。 还可以设置多种其他配置选项,我们将在本书中看到这些选项。

没有默认的父驱动程序类作为子类;驱动程序逻辑通常存在于为封装 MapReduce 作业而编写的类的 Main 方法中。 请看下面的代码片段作为示例驱动程序。 不要担心每一行是如何工作的,尽管您应该能够大致了解每行都在做什么:

public class ExampleDriver
{
...
public static void main(String[] args) throws Exception
{
// Create a Configuration object that is used to set other options
    Configuration conf = new Configuration() ;
// 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);
}
}}

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

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

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

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

编写 MapReduce 程序

我们使用和谈论单词计数已经有很长一段时间了;让我们实际编写一个实现,编译并运行它,然后探索一些修改。

执行操作的时间-设置类路径

要编译任何与 Hadoop 相关的代码,我们需要引用标准的 Hadoop 捆绑类。

Hadoop-1.0.4.core.jar文件从发行版添加到 Java 类路径,如下所示:

$ export CLASSPATH=.:${HADOOP_HOME}/Hadoop-1.0.4.core.jar:${CLASSPATH}

刚刚发生了什么?

这会将Hadoop-1.0.4.core.jar文件显式添加到类路径中,与当前目录和 CLASSPATH 环境变量以前的内容并列。

同样,最好将其放入您的 shell 启动文件或一个独立的源文件中。

备注

稍后,我们还需要在类路径上拥有许多随 Hadoop 一起提供的第三方库,实现这一点有一条捷径。 目前,显式添加核心 JAR 文件就足够了。

动作执行字数时间

我们已经在第 2 章启动和运行 Hadoop中看到了 Wordcount 示例程序的使用。 现在,我们将通过执行以下步骤来探索我们自己的 Java 实现:

  1. WordCount1.java文件中输入以下代码:

    Import java.io.* ;
    import org.apache.hadoop.conf.Configuration ;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.IntWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.mapreduce.Mapper;
    import org.apache.hadoop.mapreduce.Reducer;
    import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; 
    
    public class WordCount1 
    {
    
        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 static void main(String[] args) throws Exception {
            Configuration conf = new Configuration();
            Job job = new Job(conf, "word count");
            job.setJarByClass(WordCount1.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]));
            System.exit(job.waitForCompletion(true) ? 0 : 1);
        }
    }
    
  2. 现在通过执行以下命令编译它:

    $ javac WordCount1.java
    
    

刚刚发生了什么?

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

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

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

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

我们在前面说过,减法器的输入是一个键和相应的值列表,在mapreduce方法之间有一些魔力,可以收集每个键的值来帮助实现这一点,我们现在不会描述这一点。 Hadoop 为每个键执行一次 Reducer,前面的 Reducer 实现只计算Iterable对象中的数字,并以<word, count>的形式给出每个单词的输出。 这是我们的K3/V3值。

看看我们的mapperreducer类的签名:WordCountMapper类给出IntWritableText作为输入,给出TextIntWritable作为输出。 WordCountReducer类将TextIntWritable作为输入和输出。 这也是一种非常常见的模式,在这种模式下,map方法对键和值执行反转,而是发出一系列数据对,Reducer 对这些数据对执行聚合。

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

执行操作的时间-构建一个 JAR 文件

在 Hadoop 中运行作业之前,我们必须将所需的类文件收集到单个 JAR 文件中,然后提交给系统。

从生成的类文件创建一个 JAR 文件。

$ jar cvf wc1.jar WordCount1*class

刚刚发生了什么?

在提交到 Hadoop 之前,我们必须始终将我们的类文件打包到 JAR 文件中,无论它是本地的还是 Elastic MapReduce 上的。

提示

注意 JAR 命令和文件路径。 如果将子目录中的文件包括在 JAR 文件类中,则该类可能不会以预期的路径存储。 在使用所有源数据都被编译的 Catch-all 类目录时,这种情况尤其常见。 编写脚本以切换到目录、将所需文件转换为 JAR 文件并将 JAR 文件移动到所需位置可能很有用。

在本地 Hadoop 群集上运行操作字数的时间

现在我们已经生成了类文件并将它们收集到一个 JAR 文件中,我们可以通过执行以下步骤来运行应用:

  1. 将新的 JAR 文件提交给 Hadoop 以供执行。

    $ hadoop jar wc1.jar WordCount1 test.txt output
    
    
  2. 如果成功,您应该看到输出与我们在上一章中运行 Hadoop 提供的示例字数计数时获得的输出非常相似。 检查输出文件,应如下所示:

    $ Hadoop fs –cat output/part-r-00000
    This 1
    yes 1
    a 1
    is 2
    test 1
    this 1
    
    

刚刚发生了什么?

这是我们第一次在自己的代码中使用 Hadoop jar 命令。 有四个论点:

  1. JAR 文件的名称。
  2. JAR 文件中驱动程序类的名称。
  3. 输入文件在 HDFS 上的位置(在本例中是对/user/Hadoop home文件夹的相对引用)。
  4. 输出文件夹的所需位置(同样是相对路径)。

提示

只有在 JAR 文件清单中未指定主类(如本例所示)时,才需要驱动程序类的名称。

电子病历动作运行字数时间

现在我们将向您展示如何在 EMR 上运行相同的 JAR 文件。 一如既往地记住,这是要花钱的!

  1. 转到位于http://aws.amazon.com/console的亚马逊网络服务控制台,登录,然后选择S3

  2. 您需要两个存储桶:一个用于存放 JAR 文件,另一个用于作业输出。 您可以使用现有的存储桶,也可以创建新的存储桶。

  3. 打开要存储作业文件的存储桶,单击Upload,然后添加前面创建的wc1.jar文件。

  4. 返回控制台主页,然后通过选择Elastic MapReduce转到控制台的 EMR 部分。

  5. Click on the Create a New Job Flow button and you'll see a familiar screen as shown in the following screenshot:

    Time for action – running WordCount on EMR

  6. 之前,我们使用了一个示例应用;要运行我们的代码,我们需要执行不同的步骤。 首先,选择运行您自己的应用单选按钮。

  7. 选择作业类型组合框中,选择自定义 JAR

  8. Click on the Continue button and you'll see a new form, as shown in the following screenshot:

    Time for action – running WordCount on EMR

现在我们指定作业的参数。 在我们上传的 JAR 文件中,我们的代码--特别是驱动程序类--指定了诸如MapperReducer类等方面。

我们需要提供的是 JAR 文件的路径以及作业的输入和输出路径。 在JAR Location字段中,输入您上传 JAR 文件的位置。 如果 JAR 文件名为wc1.jar,并且您将其上传到名为mybucket的存储桶中,则路径将为mybucket/wc1.jar

JAR 参数字段中,您需要输入主类的名称以及作业的输入和输出位置。 对于S3上的文件,我们可以使用s3://bucketname/objectname形式的 URL。 单击继续,将出现熟悉的屏幕以指定作业流的虚拟机,如以下屏幕截图所示:

Time for action – running WordCount on EMR

现在继续执行作业流设置和执行,就像我们在第 2 章启动并运行 Hadoop中所做的那样。

刚刚发生了什么?

这里的重要经验是,我们可以在 EMR 中重用在本地 Hadoop 集群上编写的代码,并将其用于本地 Hadoop 集群。 此外,除了前几个步骤之外,无论要执行的作业代码的来源是什么,EMR 控制台的大部分都是相同的。

在本章的其余部分中,我们将不会显式地展示在 EMR 上执行的代码,而是更多地关注本地集群,因为在 EMR 上运行 JAR 文件非常容易。

0.20 版本之前的 Java MapReduce API

我们在本书中的首选适用于 MapReduce Java API 的 0.20 和更高版本,但我们需要快速了解一下较旧的 API,原因有两个:

  1. 许多在线示例和其他参考资料都是为较旧的 API 编写的。
  2. MapReduce 框架中的几个方面还没有移植到新的 API,我们需要使用旧的 API 来探索它们。

旧版 API 的类主要在org.apache.hadoop.mapred包中找到。

新的 API 类使用具体的MapperReducer类,而旧的 API 将此职责分散到抽象类和接口上。

Mapper类的实现将继承抽象MapReduceBase类并实现Mapper接口,而自定义的Reducer类将继承相同的MapReduceBase抽象类但实现Reducer接口。

我们不会详细探究MapReduceBase,因为它的功能涉及作业设置和配置,而这并不是理解MapReduce模型的真正核心。 但是 0.20 之前的MapperReducer的接口值得一看:

public interface Mapper<K1, V1, K2, V2>
{
void map( K1 key, V1 value, OutputCollector< K2, V2> output, Reporter reporter) throws IOException ;
}

public interface Reducer<K2, V2, K3, V3>
{
void reduce( K2 key, Iterator<V2> values, 
OutputCollector<K3, V3> output, Reporter reporter) 
throws IOException ;
}

这里有几点需要理解:

  • OutputCollector类的泛型参数更明确地显示了方法的结果是如何显示为输出的。
  • 旧的 API 使用OutputCollector类实现此目的,并使用Reporter类将状态和指标信息写入 Hadoop 框架。 0.20API 在Context类中结合了这些职责。
  • Reducer接口使用Iterator对象而不是Iterable对象;这一点有所改变,因为后者使用 Java 来处理每种语法,从而使代码更加简洁。
  • mapreduce方法都不能在旧 API 中抛出InterruptedException

如您所见,API 之间的更改改变了 MapReduce 程序的编写方式,但不改变映射器或减少器的用途或职责。 除非需要,否则不要觉得有必要成为这两个 API 的专家;熟悉这两个 API 中的任何一个应该会让您了解本书的其余部分。

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

我们不必总是从头开始编写我们自己的MapperReducer类。 Hadoop 提供了几个常见的MapperReducer实现,可以在我们的工作中使用。 如果我们不覆盖新 API 中的MapperReducer类中的任何方法,则默认实现是 IdentityMapperReducer类,它们只是不加更改地输出输入。

请注意,随着时间的推移,可能会添加更多这样的预写MapperReducer实现,目前新 API 没有旧 API 那么多。

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

  • InverseMapper:此输出(value,key)
  • TokenCounterMapper:统计每行输入中的离散令牌数

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

  • IntSumReducer:这将输出每个键的整数值列表的总和
  • LongSumReducer:这将输出每个键的长值列表的总和

是时候行动了--用简单的方法数字数

让我们回顾一下 wordcount,但这一次使用一些预定义的mapreduce实现:

  1. 创建包含以下代码的新WordCountPredefined.java文件:

    import org.apache.hadoop.conf.Configuration ;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.IntWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    import org.apache.hadoop.mapreduce.lib.map.TokenCounterMapper ;
    import org.apache.hadoop.mapreduce.lib.reduce.IntSumReducer ;
    
    public class WordCountPredefined
    {   
        public static void main(String[] args) throws Exception
        {
            Configuration conf = new Configuration();
            Job job = new Job(conf, "word count1");
            job.setJarByClass(WordCountPredefined.class);
            job.setMapperClass(TokenCounterMapper.class);
            job.setReducerClass(IntSumReducer.class);
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
            FileInputFormat.addInputPath(job, new Path(args[0]));
            FileOutputFormat.setOutputPath(job, new Path(args[1]));
            System.exit(job.waitForCompletion(true) ? 0 : 1);
        }
    }
    
  2. 现在编译、创建 JAR 文件,并像以前一样运行它。

  3. 如果您希望使用相同的位置,请不要忘记在运行作业之前删除输出目录。 例如,使用hadoop fs -rmr输出。

刚刚发生了什么?

考虑到在 MapReduce 世界中普遍存在的 Wordcount 示例,预定义的MapperReducer实现一起实现整个 Wordcount 解决方案可能并不完全令人惊讶。 TokenCounterMapper类简单地将每个输入行分成一系列(token, 1)对,IntSumReducer类通过对每个键的值数求和来提供最终计数。

这里有两件重要的事情值得欣赏:

  • 尽管 Wordcount 无疑是这些实现的灵感来源,但它们绝不是特定于它的,可以广泛应用
  • 这种拥有可重用的映射器和减少器实现的模型是需要记住的一件事,特别是考虑到新的 MapReduce 作业实现的最佳起点通常是现有的

浏览一系列字数统计

为了更详细地探索映射器和 Reducer 之间的关系,并公开 Hadoop 的一些内部工作,我们现在将了解 Wordcount(或者实际上任何 MapReduce 作业)是如何执行的。

启动

驱动程序中对Job.waitForCompletion()的调用是所有操作开始的地方。 该驱动程序是在我们的本地机器上运行的唯一一段代码,该调用启动了与 JobTracker 的通信。 请记住,JobTracker 负责作业调度和执行的所有方面,因此在执行任何与作业管理相关的任务时,它成为我们的主要界面。 JobTracker 代表我们与 NameNode 通信,并管理与存储在 HDFS 上的数据相关的所有交互。

拆分输入

这些交互中的第一个发生在 JobTracker 查看输入数据并确定如何将其分配给映射任务时。 回想一下,HDFS 文件通常被分割成至少 64MB 的块,JobTracker 会将每个块分配给一个映射任务。

当然,我们的字数统计示例使用了很少的数据量,这些数据完全在单个块内。 假设有一个更大的输入文件(以 TB 为单位),那么拆分模型就更有意义了。 文件的每个段-或者在 MapReduce 术语中称为Split-由一个映射任务唯一地处理。

一旦计算出拆分,JobTracker 就会将拆分和包含MapperReducer类的 JAR 文件放入 HDFS 上特定于作业的目录中,该目录的路径将在任务启动时传递给每个任务。

任务分配

一旦 JobTracker 确定需要多少映射任务,它就会查看群集中的主机数量、有多少 TaskTracker 在工作,以及每个任务可以同时执行多少映射任务(用户可定义的配置变量)。 JobTracker 还会查看各个输入数据块在群集中的位置,并尝试定义一个执行计划,以最大限度地减少 TaskTracker 处理位于同一物理主机上的拆分/数据块的情况,如果失败,它将处理同一硬件机架中的至少一个数据块。

这种数据局部性优化是 Hadoop 能够高效处理如此大型数据集的一个重要原因。 还请记住,默认情况下,每个数据块跨三个不同的主机进行复制,因此生成任务/主机计划以查看大多数数据块在本地处理的可能性比最初看起来要高。

任务启动

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

如果集群有足够的容量一次执行所有映射任务,那么它们都将被启动,并被赋予要处理的拆分和作业 JAR 文件的引用。 然后,每个 TaskTracker 将拆分复制到本地文件系统。

如果任务数量超过集群容量,JobTracker 将保留挂起任务队列,并在节点完成初始分配的 MAP 任务时将其分配给节点。

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

持续的 JobTracker 监控

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

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

第 2 章启动和运行 Hadoop中,我们的单词数输入是一个简单的一行文本文件。 在本演练的其余部分,我们假设它是一个同样普通的两行文本文件:

This is a test
Yes this is

Driver 类通过使用 TextInputFormat 指定输入文件的格式和结构,因此 Hadoop 知道将其视为以行号为键、以行内容为值的文本。 因此,将为映射器的两次调用提供以下输入:

1 This is a test
2 Yes it is.

映射器执行

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

然后,对于每个单独的单词,映射器都会发出一个由实际单词本身和值 1 组成的键。

提示

我们添加了几个我们将在这里提到的优化,但在这一点上不要太担心它们。 您将看到,我们不是每次都创建包含值 1 的IntWritable对象,而是将其创建为静态变量,并在每次调用中重用它。 类似地,我们使用单个Text对象,并在每次执行该方法时重置其内容。 这样做的原因是,尽管它对我们的小输入文件帮助不大,但在处理庞大的数据集时,可能会看到映射器被调用数千次或数百万次。 如果每次调用都可能为键和值输出创建一个新对象,这将成为一个资源问题,并可能由于垃圾收集而导致更频繁的暂停。 我们使用这个值,并且知道Context.write方法不会改变它。

映射器输出和减少输入

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

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

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

分区

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

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

备注

我们将在后面的章节中讨论容错问题,但在这一点上,一个明显的问题是,如果减速器发生故障,这个计算会发生什么情况。 答案是,JobTracker 将确保重新执行任何失败的 Reduce 任务,可能是在不同的节点上,因此暂时性故障不会成为问题。 更严重的问题,如数据敏感错误或拆分中严重损坏的数据,除非采取某些步骤,否则将导致整个作业失败。

可选分区函数

在中,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的自定义子类提供特定于应用的分区逻辑。 例如,如果在应用标准散列函数时数据提供了非常不均匀的分布,则尤其如此。

减速器输入

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

减速器执行

我们的WordCountReducer类非常简单;对于每个单词,它只计算数组中元素的数量,并为每个单词发出最终的(Word, count)输出。

提示

我们不担心任何形式的优化,以避免在这里创建过多的对象。 Reduce 调用的数量通常小于映射器的数量,因此开销不是问题。 但是,如果您发现自己的性能要求非常苛刻,请随意这样做。

对于我们在样例输入上调用 wordcount 的,除了一个单词之外,所有单词在值列表中只有一个值;is有两个值。

备注

请注意,单词thisThis具有离散计数,因为我们没有尝试忽略区分大小写。 类似地,以句点结束每个句子会使is停止计数为 2,因为is将不同于is.。 在处理文本数据(如大写、标点符号、连字符、分页和其他方面)时一定要小心,因为它们可能会歪曲数据的感知方式。 在这种情况下,通常会有一个前身 MapReduce 作业对数据集应用标准化或清理策略。

减速机输出

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

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

此数据将输出到驱动程序中指定的输出目录中的分区文件,该目录将使用指定的 OutputFormat 实现进行格式化。 每个 Reduce 任务都写入一个文件名为part-r-nnnnn的文件,其中nnnnn00000开始并递增。 当然,这就是我们在第 2 章让 Hadoop 启动并运行中看到的;希望part前缀现在更有意义一些。

停机

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

仅此而已!

正如您所看到的,每个 MapReduce 程序都位于 Hadoop 提供的大量机器之上,所提供的草图在很多方面都是一种简化。 和以前一样,对于这样一个小的例子来说,其中的很多内容都没有太大的价值,但是不要忘记,我们可以使用相同的软件和映射器/减少器实现来对一个庞大的集群(无论是本地集群还是 EMR 集群)中的更大的数据集进行字数统计。 Hadoop 在这一点上为您做的工作是巨大的,这使得对这样的数据集执行数据分析成为可能;否则,手动实现代码的分发、同步和并行化的工作将是巨大的。

除了组合器…。 也许吧

前面省略了一个额外的、可选的步骤。 Hadoop 允许使用组合器类在还原器检索输出之前对map方法的输出执行一些早期排序。

为什么要有合并器?

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

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

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

使用合并器进行动作词计数的时间

让我们在第一个单词计数示例中添加一个合并器。 事实上,让我们使用我们的减速器作为合并器。 由于合并器必须与减法器具有相同的接口,这是您经常看到的,不过请注意,减法器中涉及的处理类型将决定它是否是合并器的真正候选者;我们稍后将讨论这一点。 因为我们要计算单词的出现次数,所以可以对 map 节点进行部分计数,并将这些小计传递给减法器。

  1. WordCount1.java复制到WordCount2.java并更改驱动程序类,以便在MapperReducer类的定义之间添加以下行:

            job.setCombinerClass(WordCountReducer.class);
    
  2. 同样,将类名更改为WordCount2,然后编译它。

    $ javac WordCount2.java
    
  3. 创建 JAR 文件。

    $ jar cvf wc2.jar WordCount2*class
    
  4. 在 Hadoop 上运行作业。

    $ hadoop jar wc2.jar WordCount2 test.txt output
    
  5. 检查输出。

    $ hadoop fs -cat output/part-r-00000
    

刚刚发生了什么?

此输出可能与您预期的不同,因为单词is的值现在被错误地指定为 1 而不是 2。

问题在于合并器和减速器将如何相互作用。 提供给减法器的值以前是(is, 1, 1),现在是(is, 2),因为我们的组合器对每个单词的元素数进行了自己的求和。 但是,我们的减少器并不查看Iterable对象中的实际值,它只是计算有多少个值。

何时可以将减速机用作组合器

编写组合器时需要小心。 请记住,Hadoop 不保证它可以应用于映射输出的次数,它可以是 0、1 或更多。 因此,能够以这种方式有效地应用由组合器执行的操作是至关重要的。 诸如求和、加法等分布式操作通常是安全的,但如前所述,请确保 Reduce 逻辑不会做出可能破坏此属性的隐式假设。

使用组合器操作固定字数的时间

让我们对 wordcount 进行必要的修改,以正确使用组合器。

WordCount2.java复制到名为WordCount3.java的新文件中,并按如下方式更改reduce方法:

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

还要记住将类名更改为WordCount3,然后编译、创建 JAR 文件,然后像以前一样运行作业。

刚刚发生了什么?

现在的产量与预期不谋而合。 组合器的任何映射端调用都会成功执行,并且减法器正确地生成总输出值。

提示

如果将原来的 Reducer 用作合并器,并将新的 Reduce 实现用作 Reducer,这会起作用吗? 答案是否定的,尽管我们的测试示例不会演示它。 由于组合器可能会在地图输出数据上被多次调用,因此如果数据集足够大,地图输出中将出现相同的错误,但由于输入大小较小,此处不会出现相同的错误。 从根本上说,最初的还原器是不正确的,但这并不是立竿见影的;要小心这些微妙的逻辑缺陷。 这类问题可能真的很难调试,因为代码将可靠地在包含数据集的子集的开发箱上工作,而在更大的操作集群上失败。 仔细设计您的组合器类,不要依赖于只处理一小部分数据样本的测试。

重用是您的朋友

在上一节中,我们获取了现有的作业类文件并对其进行了更改。 这是一个非常常见的 Hadoop 开发工作流的小示例;使用现有作业文件作为新作业文件的起点。 即使实际的映射器和减少器逻辑非常不同,接受现有的工作通常也是省时的,因为这可以帮助您记住映射器、减少器和驱动程序实现的所有必需元素。

弹出式测验-MapReduce 机制

问题 1.。 对于 MapReduce 作业,您总是需要指定什么?

  1. 映射器和缩减器的类。
  2. 映射器、缩减器和合并器的类。
  3. 映射器、缩减器、分割器和合并器的类。
  4. 无;所有类都有默认实现。

Q2.。 一个组合器要执行多少次?

  1. 至少一次。
  2. 零次或一次。
  3. 零次、一次或多次。
  4. 它是可配置的。

第三季度。 您有一个映射器,它为每个关键点生成一个整数值和以下一组 Reduce 操作:

  • 减法器 A:输出整数值集合的总和。
  • 减法器 B:输出该组值中的最大值。
  • 减数 C:输出值集合的平均值。
  • 缩减器 D:输出集合中最大值和最小值之间的差值。

这些 Reduce 操作中的哪些可以安全地用作组合器?

  1. 他们所有人。
  2. A 和 B。
  3. A、B 和 D。
  4. C 和 D。
  5. 他们一个也没有。

Hadoop 特定的数据类型

到目前为止点,我们已经美化了用作 map 和 Reduce 类的输入和输出的实际数据类型。 我们现在来看一看。

Writable 和 WritableCompanable 接口

如果您浏览 Hadoop API 中的org.apache.hadoop.io包,您将看到一些熟悉的类,如TextIntWritable,以及其他带有Writable后缀的类。

该软件包还包含如下指定的Writable接口:

import java.io.DataInput ;
import java.io.DataOutput ;
import java.io.IOException ;

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

此接口的主要用途是提供在通过网络传递数据或从磁盘读取和写入数据时对数据进行序列化和反序列化的机制。 要用作映射器或减少器(即V1V2V3)的值输入或输出值的每个数据类型都必须实现此接口。

要用作键的数据(K1K2K3)有更严格的要求:除了Writable之外,它还必须提供标准 JavaComparable接口的实现。 它有以下规格:

public interface Comparable
{
public int compareTO( Object obj) ;
}

Compare 方法返回-101,具体取决于所比较的对象是小于、等于还是大于当前对象。

作为一个方便的接口,Hadoop 在org.apache.hadoop.io包中提供了WritableComparable接口。

public interface WritableComparable extends Writable, Comparable
{}

介绍包装器类

幸运的是,您不必从头开始;正如您已经看到的,Hadoop 提供了包装 Java 原语类型并实现WritableComparable的类。 它们在org.apache.hadoop.io包中提供。

原始包装类

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

  • 布尔可写
  • 可写字节
  • 可双写
  • FloatWritable
  • IntWritable
  • 可长写入
  • VIntWritable-可变长度整数类型
  • VLongWritable-可变长度的长类型

数组包装类

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

  • ArrayWritable
  • TwoDArrayWritable

地图包装类

这些类允许将java.util.Map接口的实现用作键或值。 请注意,它们被定义为Map<Writable, Writable>,并有效地管理一定程度的内部运行时类型检查。 这确实意味着编译类型检查被削弱了,所以要小心。

  • AbstractMapWritable:这是其他具体Writable映射实现的基类
  • MapWritable:这是将Writable键映射到Writable值的通用映射
  • SortedMapWritable:此是也实现SortedMap接口的MapWritable类的专门化

该执行操作了-使用可写包装类

让我们编写一个类来显示其中一些包装类的运行情况:

  1. 将以下内容创建为WritablesTest.java

    import org.apache.hadoop.io.* ;
    import java.util.* ;
    
    public class WritablesTest
    {
        public static class IntArrayWritable extends ArrayWritable
        {
            public IntArrayWritable()
            {
                super(IntWritable.class) ;
            }
        }
    
        public static void main(String[] args)
        {
    System.out.println("*** Primitive Writables ***") ;
            BooleanWritable bool1 = new BooleanWritable(true) ;
            ByteWritable byte1 = new ByteWritable( (byte)3) ;
            System.out.printf("Boolean:%s Byte:%d\n", bool1, byte1.get()) ;
    
            IntWritable i1 = new IntWritable(5) ;
            IntWritable i2 = new IntWritable( 17) ;
            System.out.printf("I1:%d I2:%d\n", i1.get(), i2.get()) ;
            i1.set(i2.get()) ;
            System.out.printf("I1:%d I2:%d\n", i1.get(), i2.get()) ;
            Integer i3 = new Integer( 23) ;
            i1.set( i3) ;
            System.out.printf("I1:%d I2:%d\n", i1.get(), i2.get()) ;
    
    System.out.println("*** Array Writables ***") ;       
            ArrayWritable a = new ArrayWritable( IntWritable.class) ;
            a.set( new IntWritable[]{ new IntWritable(1), new IntWritable(3), new IntWritable(5)}) ;
    
            IntWritable[] values = (IntWritable[])a.get() ;
    
            for (IntWritable i: values)
            System.out.println(i) ;
    
            IntArrayWritable ia = new IntArrayWritable() ;
            ia.set( new IntWritable[]{ new IntWritable(1), new IntWritable(3), new IntWritable(5)}) ;
    
            IntWritable[] ivalues = (IntWritable[])ia.get() ;
    
            ia.set(new LongWritable[]{new LongWritable(1000l)}) ;
    
    System.out.println("*** Map Writables ***") ;       
            MapWritable m = new MapWritable() ;
            IntWritable key1 = new IntWritable(5) ;
            NullWritable value1 = NullWritable.get() ;
            m.put(key1, value1) ;
            System.out.println(m.containsKey(key1)) ;
            System.out.println(m.get(key1)) ;
            m.put(new LongWritable(1000000000), key1) ;
            Set<Writable> keys = m.keySet() ;
    
            for(Writable w: keys)
            System.out.println(w.getClass()) ;
        }
    }
    
  2. 编译和运行类,您应该会得到以下输出:

    *** Primitive Writables ***
    Boolean:true Byte:3
    I1:5 I2:17
    I1:17 I2:17
    I1:23 I2:17
    *** Array Writables ***
    1
    3
    5
    *** Map Writables ***
    true
    (null)
    class org.apache.hadoop.io.LongWritable
    class org.apache.hadoop.io.IntWritable
    
    

刚刚发生了什么?

这个输出在很大程度上应该是不言而喻的。 我们创建各种Writable包装器对象,并显示它们的一般用法。 有几个关键点:

  • 如前所述,除了Writable本身之外没有类型安全。 因此,可以有一个包含多种类型的数组或映射,如前所述。
  • 例如,我们可以通过向IntWritable上需要int变量的方法提供一个Integer对象来使用自动装箱。
  • 内部类演示了如果要将ArrayWritable类用作reduce函数的输入需要什么;必须定义具有这种默认构造函数的子类。

其他包装类

  • CompressedWritable:此是一个基类,允许在显式访问其属性之前保持压缩状态的大型对象
  • ObjectWritable:此是通用的通用对象包装器
  • NullWritable:this 是空值的单个对象表示形式
  • VersionedWritable:此是一个基本实现,允许可写类随时间跟踪版本

来个围棋英雄--玩写字游戏

编写一个使用NullWritableObjectWritable类的类,其方式与前面的示例相同。

自己做

正如您从WritableComparable接口中看到的,所需的方法非常简单;如果您希望在 MapReduce 作业中将您自己的自定义类用作键或值,请不要害怕添加此功能。

输入/输出

我们已经多次提到驱动程序类的一个方面,但没有详细解释:输入到 MapReduce 作业和从 MapReduce 作业输出的数据的格式和结构。

文件、拆分和记录

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

InputFormat 和 RecordReader

Hadoop 的第一个职责是InputFormatorg.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:此用于纯文本文件

提示

0.20 之前的 API 在org.apache.hadoop.mapred包中定义了额外的 InputFormats。

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

Hadoop 提供的 RecordReader

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

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

同样,0.20 之前的 API 在org.apache.hadoop.mapred包中还有额外的RecordReader类,比如KeyValueRecordReader,这些类还没有移植到新的 API 中。

OutputFormat 和 RecordWriter

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

提示

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

Hadoop 提供的 OutputFormat

org.apache.hadoop.mapreduce.output包中提供了以下个个 OutputFormats:

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

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

别忘了序列文件

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

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

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

摘要

我们在本章已经涵盖了很多内容,现在我们有了更详细地研究 MapReduce 的基础。 具体地说,我们了解了键/值对是如何成为非常适合 MapReduce 处理的广泛适用的数据模型的。 我们还学习了如何使用 Java API 的 0.20 和更高版本编写映射器和减少器实现。

然后,我们了解了 MapReduce 作业是如何处理的,以及mapreduce方法是如何通过重要的协调和任务调度机制绑定在一起的。 我们还看到了某些 MapReduce 作业如何需要以自定义分区程序或组合器的形式进行专门化。

我们还了解了 Hadoop 如何向文件系统读取数据以及如何从文件系统读取数据。 它使用InputFormatOutputFormat的概念将文件作为一个整体处理,使用RecordReaderRecordWriter将格式与键/值对相互转换。

有了这些知识,我们现在将进入下一章的案例研究,它展示了处理大数据集的 MapReduce 应用的持续开发和增强。*

四、开发 MapReduce 程序

既然我们已经探索了 MapReduce 技术,我们将在本章研究如何使用它。 具体地说,我们将使用一个更丰富的数据集,并研究如何使用 MapReduce 提供的工具来分析它。

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

  • Hadoop 流媒体及其应用
  • 不明飞行物目击数据集
  • 使用流作为开发/调试工具
  • 在单个作业中使用多个映射器
  • 在群集中高效共享实用程序文件和数据
  • 报告作业和任务状态以及对调试有用的日志信息

在本章中,目标是介绍有关如何分析新数据集的具体工具和想法。 我们将从查看如何使用脚本编程语言来帮助 MapReduce 进行原型开发和初始分析开始。 尽管在上一章中学习 Java API 并立即迁移到不同的语言似乎有些奇怪,但我们的目标是让您了解解决问题的不同方法。 正如许多作业使用 Java API 以外的其他方式实现没有什么意义一样,在其他情况下,使用另一种方法是最合适的。 把这些技术看作是你工具带的新补充,有了这些经验,你就会更容易地知道哪种技术最适合给定的场景。

在 Hadoop 中使用 Java 以外的语言

我们在前面已经提到过,MapReduce 程序不必用 Java 编写。 大多数程序都是用 Java 编写的,但是您可能想要或需要用另一种语言编写地图和减少任务,这有几个原因。 也许您有现有的代码可以利用,或者需要使用第三方二进制文件-原因是多种多样且合理的。

Hadoop 提供了许多机制来帮助非 Java 开发,其中最主要的是Hadoop 管道,它为 Hadoop 提供了本机 C++接口,以及Hadoop Streaming,它允许任何使用标准输入和输出的程序用于映射和缩减任务。 在本章中,我们将大量使用 Hadoop 流。

Hadoop 流的工作原理

使用 MapReduce Java API,map 和 Reduce 任务都提供了包含任务功能的方法的实现。 这些方法将输入作为方法参数接收到任务,然后通过Context对象输出结果。 这是一个清晰且类型安全的接口,但根据定义是特定于 Java 的。

Hadoop 流媒体采用了一种不同的方法。 使用流,您可以编写一个映射任务,该任务从标准输入读取其输入,一次一行,并将其结果的输出提供给标准输出。 然后,Reduce 任务也执行同样的操作,同样只对其数据流使用标准输入和输出。

从标准输入和输出读取和写入的任何程序都可以在流中使用,比如编译的二进制文件、UnixShell 脚本或用 Ruby 或 Python 等动态语言编写的程序。

为什么要使用 Hadoop 流

流媒体最大的优势是,与使用 Java 相比,它可以让你更快地尝试想法并进行迭代。 您只需编写脚本并将它们作为参数传递到流 JAR 文件,而不是编译/JAR/提交循环。 特别是在对新数据集进行初步分析或尝试新想法时,这可以显著加快开发速度。

关于动态语言和静态语言的经典争论平衡了快速开发与运行时性能和类型检查的好处。 使用流媒体时,这些动态缺点也适用。 因此,我们倾向于使用流式处理进行前期分析,使用 Java 实现将在生产集群上执行的作业。

在本章中,我们将使用 Ruby 作为流媒体示例,但这是个人喜好。 如果您更喜欢 shell 脚本或另一种语言(如 Python),那么可以利用这个机会将这里使用的脚本转换为您选择的语言。

操作时间-使用流实现字数计数

让我们再次鞭打 wordcount 的死马,并通过执行以下步骤使用流实现它:

  1. 将以下文件保存到wcmapper.rb

    #/bin/env ruby
    
    while line = gets
        words = line.split("\t")
        words.each{ |word| puts word.strip+"\t1"}}
    end
    
  2. 通过执行以下命令使文件成为可执行文件:

    $ chmod +x wcmapper.rb
    
    
  3. 将以下文件保存到wcreducer.rb

    #!/usr/bin/env ruby
    
    current = nil
    count = 0
    
    while line = gets
        word, counter = line.split("\t")
    
        if word == current
            count = count+1
        else
            puts current+"\t"+count.to_s if current
            current = word
            count = 1
        end
    end
    puts current+"\t"+count.to_s
    
  4. 通过执行以下命令使文件成为可执行文件:

    $ chmod +x wcreducer.rb
    
    
  5. 使用上一章中的数据文件将脚本作为流作业执行:

    $ hadoop jar hadoop/contrib/streaming/hadoop-streaming-1.0.3.jar 
    -file wcmapper.rb -mapper wcmapper.rb -file wcreducer.rb 
    -reducer wcreducer.rb -input test.txt -output output
    packageJobJar: [wcmapper.rb, wcreducer.rb, /tmp/hadoop-hadoop/hadoop-unjar1531650352198893161/] [] /tmp/streamjob937274081293220534.jar tmpDir=null
    12/02/05 12:43:53 INFO mapred.FileInputFormat: Total input paths to process : 1
    12/02/05 12:43:53 INFO streaming.StreamJob: getLocalDirs(): [/var/hadoop/mapred/local]
    12/02/05 12:43:53 INFO streaming.StreamJob: Running job: job_201202051234_0005
    …
    12/02/05 12:44:01 INFO streaming.StreamJob:  map 100%  reduce 0%
    12/02/05 12:44:13 INFO streaming.StreamJob:  map 100%  reduce 100%
    12/02/05 12:44:16 INFO streaming.StreamJob: Job complete: job_201202051234_0005
    12/02/05 12:44:16 INFO streaming.StreamJob: Output: wcoutput
    
    
  6. 检查结果文件:

    $ hadoop fs -cat output/part-00000
    
    

刚刚发生了什么?

忽略 Ruby 的细节。 如果你不懂这种语言,在这里就不重要了。

首先,我们创建了将成为我们的映射器的脚本。 它使用gets函数从标准输入中读取一行,将其拆分成单词,然后使用puts函数将单词和值1写入标准输出。 然后,我们将该文件设为可执行文件。

由于我们将在下一节描述的原因,我们的减速器稍微复杂一些。 但是,它执行我们预期的工作,它统计每个单词的出现次数,从标准输入读取,并将输出作为最终值提供给标准输出。 同样,我们确保将该文件设置为可执行文件。

请注意,在这两种情况下,我们都隐式使用前面章节中讨论的 Hadoop 输入和输出格式。 它是处理源文件的TextInputFormat属性,并将每一行一次提供给映射脚本。 相反,TextOutputFormat属性将确保 Reduce 任务的输出也被正确写入为文本数据。 如果需要,我们当然可以修改这些内容。

接下来,我们通过上一节中显示的相当繁琐的命令行将流作业提交给 Hadoop。 每个文件需要指定两次的原因是,任何在每个节点上不可用的文件都必须由 Hadoop 打包并跨群集传送,这需要通过-file选项指定。 然后,我们还需要告诉 Hadoop 哪个脚本执行映射器和减速器角色。

最后,我们查看了作业的输出,它应该与前面基于 Java 的 WordCount 实现相同

使用流式处理时作业的差异

流式字数映射器看起来比 Java 版本简单得多,但是缩减器似乎有更多的逻辑。 为什么? 原因是当我们使用流时,Hadoop 和我们的任务之间的隐含契约会发生变化。

在 Java 中,我们知道我们的map()方法将为每个输入键/值对调用一次,而我们的reduce()方法将为每个键及其值集调用。

对于流,我们不再有mapreduce方法的概念,而是编写了处理接收的数据流的脚本。 这改变了我们编写减速器的方式。 在 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 不同。

这应该解释了 Ruby Reducer 中使用的机制;它首先为当前单词设置空默认值;然后在读取每一行之后,它确定这是否是 Current 键的另一个值,如果是,则递增计数。 如果不是,则前一个键将没有更多的值,其最终输出将被发送到标准输出,并重新开始对新单词的计数。

在阅读了前面几章中关于 Hadoop 为我们做了这么多事情有多么棒之后,这看起来可能要复杂得多,但在您编写了几个流媒体缩减器之后,它实际上并没有乍看起来那么糟糕。 还要记住,Hadoop 仍然管理对各个映射任务的拆分分配,以及将给定键的值发送到同一减法器的必要协调。 可以通过配置设置修改此行为,以更改映射器和减少器的数量,就像 Java API 一样。

分析大型数据集

有了用 Java 和流编写 MapReduce 作业的能力,我们现在将探索一个比我们以前看到的更重要的数据集。 在接下来的小节中,我们将尝试展示如何进行此类分析,以及 Hadoop 允许您对大型数据集提出的问题类型。

获取 UFO 目击数据集

我们将使用超过 60,000 次 UFO 目击事件的公共领域数据集。 这是由信息黑猩猩在http://www.infochimps.com/datasets/60000-documented-ufo-sightings-with-text-descriptions-and-metada托管的。

你需要注册一个免费的信息黑猩猩账户才能下载一份数据。

该数据包括一系列 UFO 目击记录,其字段如下:

  1. 目击日期:此字段给出目击不明飞行物的日期。
  2. 记录日期:此字段给出目击报告的日期,通常与目击日期不同。
  3. 位置:此字段提供目击发生的位置。
  4. 形状:此字段提供 UFO 形状的简要摘要,例如,菱形、灯光、圆柱体。
  5. 持续时间:此字段给出视线持续的持续时间。
  6. 描述:此字段提供目击的自由文本详细信息。

下载后,您会发现数据有几种格式。 我们将使用.tsv(制表符分隔值)版本。

体验数据集

当面对一个新的数据集时,通常很难感觉到所涉及数据的性质、广度和质量。 有几个问题,它们的答案将影响您处理后续分析的方式,特别是:

  • 数据集有多大?
  • 这些记录有多完整?
  • 记录与预期格式的匹配程度如何?

第一个是一个简单的规模问题;我们谈论的是数百、数千、数百万还是更多的记录? 第二个问题是问记录有多完整。 如果您希望每条记录都有 10 个字段(如果这是结构化或半结构化数据),那么有多少关键字段填充了数据? 最后一个问题在这一点上展开,记录与您对格式和表示的期望匹配程度如何?

行动时间-汇总 UFO 数据

现在我们有了个数据,让我们初步总结一下它的大小以及有多少条记录可能不完整:

  1. 将 HDFS 上的 UFO制表符分隔值(TSV)文件另存为ufo.tsv,将以下文件保存到summarymapper.rb

    #!/usr/bin/env ruby
    
    while line = gets
        puts "total\t1"
        parts = line.split("\t")
        puts "badline\t1" if parts.size != 6
        puts "sighted\t1" if !parts[0].empty?
        puts "recorded\t1" if !parts[1].empty?
        puts "location\t1" if !parts[2].empty?
        puts "shape\t1" if !parts[3].empty?
        puts "duration\t1" if !parts[4].empty?
        puts "description\t1" if !parts[5].empty?
    end
    
  2. 通过执行以下命令使文件成为可执行文件:

    $ chmod +x summarymapper.rb
    
    
  3. 使用流式处理执行作业,如下所示:

    $ hadoop jar hadoop/contrib/streaming/hadoop-streaming-1.0.3.jar 
    -file summarymapper.rb -mapper summarymapper.rb -file wcreducer.rb -reducer wcreducer.rb -input ufo.tsv -output ufosummary
    
    
  4. 取数汇总数据:

    $ hadoop fs -cat ufosummary/part-0000
    
    

刚刚发生了什么?

请记住,我们的 UFO 目击应该有六个字段,如前所述。 它们如下所列:

  • 目击日期
  • 报告目击事件的日期
  • 目击地点
  • 对象的形状
  • 目击的持续时间
  • 活动的免费文本说明

映射器检查文件并计算记录总数,此外还识别可能不完整的记录。

我们只需记录在处理文件时遇到了多少不同的记录,就可以生成总计数。 我们通过标记不包含恰好六个字段或至少有一个字段为空值的记录来识别潜在的不完整记录。

因此,映射器的实现读取每一行,并在遍历文件时执行三项操作:

  • 它提供要在处理的记录总数中递增的令牌的输出
  • 它在制表符边界上拆分记录,并记录没有产生六个字段值的任何线的出现情况
  • 对于它报告的六个预期字段中的每一个,当显示的值不是空字符串时,即该字段中有数据,尽管这实际上并不说明该数据的质量

我们有意编写此映射器以生成(token,``count)形式的输出。 这样做可以让我们使用前面实现中的现有字数减减器作为此作业的减字器。 当然还有更高效的实现,但由于这项工作不太可能频繁执行,所以这种便利是值得的。

在撰写本文时,这项工作的结果如下:

badline324
description61372
duration58961
location61377
recorded61377
shape58855
sighted61377
total61377

我们从这些数字中看到,我们有 61,300 项记录。 所有这些都提供了“目击日期”、“报告日期”和“位置”字段的值。 大约 58,000-59,000 条记录具有形状值和持续时间值,并且几乎所有记录都有描述。

在制表符上拆分时,发现有 372 行没有恰好有 6 个字段。 但是,由于只有 5 条记录没有描述价值,这表明坏记录通常有太多的选项卡,而不是太少。 当然,我们可以更改我们的地图绘制程序来收集有关这一事实的详细信息。 这很可能是由于自由文本描述中使用了制表符,因此目前我们将进行分析,预计大多数记录都会正确放置所有六个字段的值,但不会对每条记录中的其他制表符做出任何假设。

检查 UFO 形状

在这些报告中的所有字段中,形状是我们最感兴趣的,因为它可以提供一些有趣的数据分组方式,具体取决于我们在该字段中拥有的信息类型。

行动时间-汇总形状数据

正如我们之前提供的总体 UFO 数据集的汇总一样,现在让我们对为 UFO 形状提供的数据进行更有针对性的汇总:

  1. 将以下内容保存到shapemapper.rb

    #!/usr/bin/env ruby
    
    while line = gets  
        parts = line.split("\t")    
        if parts.size == 6        
            shape = parts[3].strip     
            puts shape+"\t1" if !shape.empty?   
        end     
    end     
    
  2. 使文件成为可执行文件:

    $ chmod +x shapemapper.rb
    
    
  3. 使用 Wordcount Reducer:

    $ hadoop jar hadoop/contrib/streaming/hadoop-streaming-1.0.3.jarr --file shapemapper.rb -mapper shapemapper.rb -file wcreducer.rb -reducer wcreducer.rb -input ufo.tsv -output shapes
    
    

    再次执行作业

  4. 检索形状信息:

    $ hadoop fs -cat shapes/part-00000 
    
    

刚刚发生了什么?

我们这里的地图绘制程序非常简单。 它将每条记录分解成其组成字段,丢弃恰好没有 6 个字段的任何记录,并给出一个计数器作为任何非空形状值的输出。

出于我们这里的目的,我们很乐意忽略任何与我们期望的规范不完全匹配的记录。 也许唯一的 UFO 目击记录将一劳永逸地证明这一点,但即便如此,它也不太可能对我们的分析产生太大影响。 在决定如此轻易地丢弃某些记录之前,请考虑一下单个记录的潜在价值。 如果您主要在关注趋势的大型聚合上工作,那么单个记录可能无关紧要。 但在单个值可能对分析产生实质性影响或必须考虑的情况下,尝试更保守地解析和恢复而不是丢弃可能是最好的方法。 我们将在第 6 章中更多地讨论这种权衡

在将地图程序设置为可执行文件并运行我们生成的作业的常规程序之后,报告了显示 29 种不同 UFO 形状的数据。 以下是出于空间原因以紧凑形式列出的一些示例输出:

changed1 changing1533 chevron758 cigar1774
circle5250 cone265 crescent2 cross177
cylinder981 delta8 diamond909 disk4798
dome1 egg661 fireball3437 flare1
flash988 formation1775 hexagon1 light12140
other4574 oval2859 pyramid1 rectangle957
round2 sphere3614 teardrop592 triangle6036
unknown4459

正如我们所看到的,在目击频率上有很大的差异。 有些像金字塔这样的形状只出现一次,而光占所有报告形状的五分之一以上。 考虑到许多不明飞行物目击事件都发生在夜间,人们可能会争辩说,对光的描述并不是特别有用或具体,当结合其他和未知的值时,我们会看到,在我们报告的 58000 个形状中,大约有 21000 可能实际上没有任何用处。 因为我们不会跑出去做额外的研究,所以这不是很重要,但重要的是开始从这些方面考虑你的数据。 即使是这些类型的汇总分析也可以开始洞察数据的性质,并指出可能的分析质量。 例如,在报告形状的情况下,我们已经发现,在我们的 61000 次目击中,只有 58000 次报告了形状,其中 21000 次的价值令人怀疑。 我们已经确定,我们的 61000 个样本集只提供了 37000 个我们可能能够使用的形状报告。 如果您的分析基于最小数量的样本,请始终确保预先进行此类总结,以确定数据集是否真的能满足您的需求。

行动时间-目击持续时间与 UFO 形状的关联

让我们对此形状数据做一点更详细的分析。 我们想知道目击持续时间与报告的形状之间是否有关联。 也许雪茄形状的不明飞行物停留的时间比其他的要长,或者队形出现的时间总是准确的。

  1. 将以下内容保存到shapetimemapper.rb

    #!/usr/bin/env ruby
    
    pattern = Regexp.new /\d* ?((min)|(sec))/
    
    while line = gets
    parts = line.split("\t")
    if parts.size == 6
    shape = parts[3].strip
    duration = parts[4].strip.downcase
    if !shape.empty? && !duration.empty?
    match = pattern.match(duration)
    time = /\d*/.match(match[0])[0]
    unit = match[1]
    time = Integer(time)
    time = time * 60 if unit == "min"
    puts shape+"\t"+time.to_s
    end
    end
    end
    
  2. 通过执行以下命令使文件成为可执行文件:

    $ chmod +x shapetimemapper.rb
    
    
  3. 将以下内容保存到shapetimereducer.rb

    #!/usr/bin/env ruby
    
    current = nil
    min = 0
    max = 0
    mean = 0
    total = 0
    count = 0
    
    while line = gets
    word, time = line.split("\t")
    time = Integer(time)
    
    if word == current
    count = count+1
    total = total+time
    min = time if time < min
    max = time if time > max
    else
    puts current+"\t"+min.to_s+" "+max.to_s+" "+(total/count).to_s if current
    current = word
    count = 1
    total = time
    min = time
    max = time
    end
    end
    puts current+"\t"+min.to_s+" "+max.to_s+" "+(total/count).to_s
    
  4. 通过执行以下命令使文件可执行:

    $ chmod +x shapetimereducer.rb
    
    
  5. 运行作业:

    $ hadoop jar hadoop/contrib/streaminghHadoop-streaming-1.0.3.jar -file shapetimemapper.rb -mapper shapetimemapper.rb -file shapetimereducer.rb -reducer shapetimereducer.rb -input ufo.tsv -output shapetime
    
    
  6. 检索结果:

    $ hadoop fs -cat shapetime/part-00000
    
    

刚刚发生了什么?

由于持续时间字段的性质,这里的映射器比前面的示例稍微复杂一些。 快速查看一些示例记录,我们发现值如下所示:

15 seconds
2 minutes
2 min
2minutes
5-10 seconds

换句话说,存在范围和绝对值的混合,时间单位的不同格式和不一致的术语。 同样,为简单起见,我们决定对数据进行有限的解释;如果有,我们将取绝对值,如果没有,则取范围的上半部分。 我们假设字符串minsec将出现在时间单位中,并将所有计时转换为秒。 使用一些正则表达式魔术,我们将持续时间字段解压成这些部分并进行转换。 再次注意,我们只是丢弃任何不能按预期工作的记录,这可能并不总是合适的。

减法器遵循与我们前面的示例相同的模式,从缺省键开始,然后读取值,直到遇到新的键。 在本例中,我们希望捕获每个形状的最小值、最大值和平均值,因此使用大量变量来跟踪所需的数据。

请记住,流缩减器需要处理分组到其关联键中的一系列值,并且必须识别新行何时具有更改的键,从而指示已处理的前一个键的最后一个值。 相反,Java Reducer 会更简单,因为它在每次执行中只处理单个键的值。

在使这两个文件成为可执行文件之后,我们运行该作业并获得以下结果,其中我们删除了所有少于 10 次的形状,并再次出于空间原因使输出更加紧凑。 每个形状的数字分别是最小值、最大值和平均值:

changing0 5400 670 chevron0 3600 333
cigar0 5400 370 circle0 7200 423
cone0 4500 498 cross2 3600 460
cylinder0 5760 380 diamond0 7800 519
disk0 5400 449 egg0 5400 383
fireball0 5400 236 flash0 7200 303
formation0 5400 434 light0 9000 462
other0 5400 418 oval0 5400 405
rectangle0 4200 352 sphere0 14400 396
teardrop0 2700 335 triangle0 18000 375
unknown0 6000 470

令人惊讶的是,在所有形状类型中,平均目击持续时间的变化相对较小;大多数形状的平均值在 350 秒到 430 秒之间。 有趣的是,我们还看到火球的平均持续时间最短,可变物体的平均持续时间最长,这两者都有一定的直觉意义。 根据定义,火球不会是一个持久的现象,一个多变的物体需要很长的时间才能被注意到它的变化。

在 Hadoop 外部使用流式脚本

最后一个示例及其更复杂的映射器和减少器很好地说明了流如何以另一种方式帮助 MapReduce 开发;您可以在 Hadoop 之外执行脚本。

在 MapReduce 开发期间,拥有用于测试代码的生产数据样本通常是很好的做法。 但是,当这是在 HDFS 上,并且您正在编写 Java map 和 Reduce 任务时,可能很难调试问题或改进复杂的逻辑。 使用从命令行读取输入的 map 和 Reduce 任务,您可以直接针对一些数据运行它们,以获得对结果的快速反馈。 如果您有一个提供 Hadoop 集成的开发环境,或者在独立模式下使用 Hadoop,那么问题就会最小化;只需记住,流确实让您能够在 Hadoop 之外尝试脚本;它可能有一天会有用。

在开发这些脚本时,作者注意到他的 UFO 数据文件中的最后一组记录的数据比文件开头的记录具有更好的结构化方式。 因此,要在映射器上进行快速测试,只需:

$ tail ufo.tsv | shapetimemapper.rb

这一原则可以应用于整个工作流,以执行 map 和 Reduce 脚本。

操作时间-从命令行执行形状/时间分析

如何进行这种本地命令行分析可能不是很明显,所以让我们来看一个例子。

使用本地文件系统上的 UFO 数据文件,执行以下命令:

$ cat ufo.tsv | shapetimemapper.rb | sort| shapetimereducer.rb

刚刚发生了什么?

使用单一的 unix 命令行,我们生成了与之前完整的 MapReduce 作业相同的输出。 如果您查看一下命令行的功能,就会发现这是有意义的。

首先,将输入文件(一次一行)发送到映射器。 它的输出通过 Unix 排序实用程序传递,并且这个排序后的输出一次传递一行给减法器。 当然,这是我们的一般 MapReduce 作业工作流的一个非常简化的表示。

那么,一个显而易见的问题是,如果我们可以在命令行进行等价的分析,那么我们为什么还要费心使用 Hadoop 呢? 答案当然是我们的老朋友,Scale。 这种简单的方法对于 UFO 目击这样的文件很有效,虽然不是微不足道的,但只有 71MB 大小。 将其放在上下文中,我们可以在单个现代磁盘驱动器上保存此数据集的数千个副本。

那么,如果数据集的大小改为 71 GB,甚至是 71TB,又会怎样呢? 在后一种情况下,至少我们必须将数据分布在多个主机上,然后决定如何拆分数据、合并部分答案,并在此过程中处理不可避免的故障。 换句话说,我们需要类似 Hadoop 的东西。

但是,不要忽视使用这样的命令行工具,在 MapReduce 开发过程中应该很好地使用这些方法。

Java 形状和位置分析

让我们返回到 Java MapReduce API,并考虑对报告中的形状和位置数据进行一些分析。

但是,在开始编写代码之前,让我们先考虑一下我们是如何处理该数据集的每个字段的分析的。 以前的映射器有一个共同的模式:

  • 丢弃确定为损坏的记录
  • 处理有效记录以提取感兴趣的字段
  • 输出我们关心的记录数据的表示形式

现在,如果我们要编写 Java 映射器来分析位置,然后可能还要分析目击和报告时间列,我们将遵循类似的模式。 那么,我们可以避免任何随之而来的代码重复吗?

通过使用org.apache.hadoop.mapred.lib.ChainMapper,答案是肯定的。 此类提供了一种顺序执行多个映射器的方法,最终映射器的输出将传递给减少器。 ChainMapper不仅适用于这种类型的数据清理;在分析特定作业时,在应用 Reducer 之前执行多个映射类型任务并不少见。

此方法的一个示例是编写一个验证映射器,该映射器可供所有未来的现场分析作业使用。 该映射器将丢弃被认为已损坏的行,只将有效行传递给实际的业务逻辑映射器,该映射器现在可以专注于分析数据,而不是担心粗略级别的验证。

这里的另一种方法是在丢弃无效记录的自定义InputFormat类中进行验证;哪种方法最有意义取决于您的特定情况。

链中的每个映射器都在单个 JVM 中执行,因此不必担心使用多个映射器会增加我们的文件系统 I/O 负载。

行动时间-使用 ChainMapper 进行现场验证/分析

让我们使用这个原则并使用ChainMapper类来帮助我们在工作中提供一些记录验证:

  1. 将以下类创建为UFORecordValidationMapper.java

    import java.io.IOException;
    
    import org.apache.hadoop.io.* ;
    import org.apache.hadoop.mapred.* ;
    import org.apache.hadoop.mapred.lib.* ;
    
    public class UFORecordValidationMapper extends MapReduceBase
    implements Mapper<LongWritable, Text, LongWritable, Text>
    {
    
        public void map(LongWritable key, Text value,
            OutputCollector<LongWritable, Text> output,
            Reporter reporter) throws IOException
    {
    String line = value.toString();
            if (validate(line))
                output.collect(key, value);
        }
    
            private boolean validate(String str)
            {
                String[] parts = str.split("\t") ;
    
                if (parts.length != 6)
                return false ;
    
                return true ;
            }
        }
    
  2. 将创建为UFOLocation.java

    import java.io.IOException;
    import java.util.Iterator ;
    import java.util.regex.* ;
    
    import org.apache.hadoop.conf.* ;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.* ;
    import org.apache.hadoop.mapred.* ;
    import org.apache.hadoop.mapred.lib.* ;
    
    public class UFOLocation
    {
    
        public static class MapClass extends MapReduceBase
    implements Mapper<LongWritable, Text, Text, LongWritable>
    {
    
    private final static LongWritable one = new LongWritable(1);
    private static Pattern locationPattern = Pattern.compile(
    "[a-zA-Z]{2}[^a-zA-Z]*$") ;
    
    public void map(LongWritable key, Text value,
    OutputCollector<Text, LongWritable> output,
    Reporter reporter) throws IOException
    {
    String line = value.toString();
            String[] fields = line.split("\t") ;
            String location = fields[2].trim() ;
            if (location.length() >= 2)
            {
    
                Matcher matcher = locationPattern.matcher(location) ;
                if (matcher.find() )
                {
                    int start = matcher.start() ;
                    String state = location.substring(start,start+2);
    
                    output.collect(new Text(state.toUpperCase()), 
                           One);
                }
            }
        }
    }
    
    public static void main(String[] args) throws Exception
    {
        Configuration config = new Configuration() ;
    JobConf conf = new JobConf(config, UFOLocation.class);
    conf.setJobName("UFOLocation");
    
    conf.setOutputKeyClass(Text.class);
    conf.setOutputValueClass(LongWritable.class);
    
    JobConf mapconf1 = new JobConf(false) ;
    ChainMapper.addMapper( conf, UFORecordValidationMapper.class,                  
    LongWritable.class, Text.class, LongWritable.class, 
    Text.class, true, mapconf1) ;
    
    JobConf mapconf2 = new JobConf(false) ;
    ChainMapper.addMapper( conf, MapClass.class, 
    LongWritable.class, Text.class, 
    Text.class, LongWritable.class, true, mapconf2) ;
    conf.setMapperClass(ChainMapper.class);
    conf.setCombinerClass(LongSumReducer.class);
    conf.setReducerClass(LongSumReducer.class);
    
    FileInputFormat.setInputPaths(conf,args[0]) ;
    FileOutputFormat.setOutputPath(conf, new Path(args[1])) ;
    
    JobClient.runJob(conf);
    }
    }
    
  3. 编译两个文件:

    $ javac UFORecordValidationMapper.java UFOLocation.java
    
    
  4. 将类文件打包并将作业提交给 Hadoop:

    $ Hadoop jar ufo.jar UFOLocation ufo.tsv output
    
    
  5. 将输出文件复制到本地文件系统并进行检查:

    $ Hadoop fs -get output/part-00000 locations.txt
    $ more locations.txt
    

刚刚发生了什么?

这里发生了相当多的事情,所以让我们一次看一段。

第一个映射器是我们的简单验证映射器。 该类遵循与标准 MapReduce API 相同的接口,map方法只是返回实用程序验证方法的结果。 我们将其拆分到一个单独的方法中,以突出映射器的功能,但是检查很容易就在 mainmap方法本身中进行。 为简单起见,我们坚持以前的验证策略,即查找字段的数量,并丢弃没有恰好分成六个制表符分隔字段的行。

请注意,不幸的是,ChainMapper类是最后要迁移到上下文对象 API 的组件之一,从 Hadoop1.0 开始,它只能与较旧的 API 一起使用。 它仍然是一个有效的概念和有用的工具,但在 Hadoop2.0 之前,它将最终迁移到org.apache.hadoop.mapreduce.lib.chain包中,它目前的使用需要旧的方法。

另一个文件包含 Main 方法中的另一个映射器实现和更新的驱动程序。 地图绘制程序在 UFO 目击报告中的位置字段末尾查找由两个字母组成的序列。 从对数据的一些手动检查中可以明显看出,大多数位置字段的格式为city, state,其中使用标准的两个字符的缩写表示州。

但是,有些记录会添加尾随的圆括号、句点或其他标点符号。 其他一些根本不是这种格式。 出于我们的目的,我们很乐意丢弃这些记录,并将重点放在那些具有我们要查找的尾随两个字符的州缩写的记录上。

Map 方法使用另一个正则表达式从 Location 字段中提取它,并以缩写的大写形式和一个简单的计数给出输出。

作业的驱动程序更改最多,因为涉及单个map类的先前配置被替换为对ChainMapper类的多个调用。

一般模型是为每个映射器创建一个新的配置对象,然后将映射器及其输入和输出的规范以及对整个作业配置对象的引用一起添加到ChainMapper类。

请注意,这两个映射器具有不同的签名。 两者都输入类型为LongWritable的键和类型为Text的值,它们也是UFORecordValidationMapper的输出类型。 然而,UFOLocationMapper输出的是文本类型的键和LongWritable类型的值,反之亦然。

这里重要的是将来自链中最终映射器的输入(UFOLocationMapper)与reduce类期望的输入(LongSumReducer)进行匹配。 使用ChainMapper类时,只要满足以下条件,链中的映射器可以具有不同的输入和输出:

  • 对于除最终映射器之外的所有映射器,每个映射器输出都与链中后续映射器的输入匹配
  • 对于最终映射器,其输出与减少器的输入匹配

我们编译这些类,并将它们放入相同的 JAR 文件中。 这是我们第一次将来自多个 Java 源文件的输出捆绑在一起。 正如预期的那样,这里没有什么神奇之处;JAR 文件、路径和类名的常规规则适用。 因为在本例中,我们的两个类都在同一个包中,所以我们不必担心在驱动程序类文件中需要额外的导入。

然后,我们运行 MapReduce 作业并检查输出,这并不完全像预期的那样。

来个围棋英雄

使用 Java API 和前面的 ChainMapper 示例重新实现以前用 Ruby 编写的映射器,这些映射器生成形状、频率和持续时间报告。

缩写过多

以下是上一个作业的结果文件中的前几个条目:

AB      286
AD      6
AE      7
AI      6
AK      234
AL      548
AM      22
AN      161
…

该文件有 186 个不同的双字符条目。 显然,我们从位置域提取最终字符有向图的方法不够健壮。

我们的数据存在许多问题,在手动分析源文件后,这些问题变得非常明显:

  • 国家缩略语的大写不一致
  • 有相当数量的目击事件来自美国以外,尽管它们可能遵循类似的(city, area)模式,但这个缩写并不是我们预期的 50 个缩写之一
  • 有些字段根本不遵循该模式,但我们的正则表达式仍会捕获这些字段

我们需要过滤这些结果,理想的做法是将美国记录归一化为正确的州输出,并将其他所有数据收集到更广泛的类别中。

要执行这项任务,我们需要向映射器添加一些关于有效的美国州缩写是什么的概念。 当然,我们可以将其硬编码到映射器中,但这似乎不正确。 虽然我们目前将所有非美国目击事件作为一个单一类别对待,但我们可能希望随着时间的推移延长这一类别,或许还可以按国家进行分类。 如果我们硬编码缩写,则每次都需要重新编译我们的映射器。

使用分布式缓存

Hadoop 为我们提供了另一种机制来实现跨作业中的所有任务共享引用数据的目标,即分布式缓存。 这可用于有效地使映射或减少任务使用的个公共只读文件可用于所有节点。 文件可以是文本数据,就像本例中一样,但也可以是其他 JAR、二进制数据或归档;任何事情都是可能的。

要分发的文件放在 HDFS 上,并添加到作业驱动程序中的 DistributedCache。 Hadoop 在作业执行之前将文件复制到每个节点的本地文件系统,这意味着每个任务都可以本地访问这些文件。

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

行动时间-使用分布式缓存改善位置输出

现在,让我们使用分布式缓存在整个集群中共享美国州名称和缩写的列表:

  1. 在本地文件系统上创建名为states.txt的数据文件。 它应该将州缩写和全名制表符分开,每行一个。 或从本书主页检索该文件。 该文件的开头应如下所示:

    AL      Alabama
    AK      Alaska
    AZ      Arizona
    AR      Arkansas
    CA      California
    
    …
    
  2. 将文件放在 HDFS 上:

    $ hadoop fs -put states.txt states.txt
    
    
  3. 将前面的个UFOLocation.java文件复制到 UFOLocation2.java 文件,并通过添加以下导入语句进行更改:

    import java.io.* ;
    import java.net.* ;
    import java.util.* ;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.filecache.DistributedCache ;
    
  4. 在设置作业名称后,将以下行添加到驱动程序主方法:

    DistributedCache.addCacheFile(new URI ("/user/hadoop/states.txt"), conf) ;
    
  5. 按如下方式替换map类:

        public static class MapClass extends MapReduceBase
    implements Mapper<LongWritable, Text, Text, LongWritable>
        {
    
            private final static LongWritable one = new LongWritable(1);
            private static Pattern locationPattern = Pattern.compile(
    "[a-zA-Z]{2}[^a-zA-Z]*$") ;
            private Map<String, String> stateNames ;
    
            @Override
            public void configure( JobConf job)
            {
                try
                {
                    Path[] cacheFiles = DistributedCache.getLocalCacheFiles(job) ;
                    setupStateMap( cacheFiles[0].toString()) ;
                } catch (IOException e) 
    {
    System.err.println("Error reading state file.") ;
                        System.exit(1) ;
    }
            }
    
            private void setupStateMap(String filename) 
    throws IOException
            {
                Map<String, String> states = new HashMap<String, 
    String>() ;
                BufferedReader reader = new BufferedReader( new FileReader(filename)) ;
                String line = reader.readLine() ;
                while (line != null)
                {
                    String[] split = line.split("\t") ;
                    states.put(split[0], split[1]) ;
                    line = reader.readLine() ;
                }
    
                stateNames = states ;
            }
    
            public void map(LongWritable key, Text value,
                OutputCollector<Text, LongWritable> output,
                Reporter reporter) throws IOException
            {
                String line = value.toString();
            String[] fields = line.split("\t") ;
            String location = fields[2].trim() ;
            if (location.length() >= 2)
            {
    
                Matcher matcher = locationPattern.matcher(location) ;
                if (matcher.find() )
                {
                    int start = matcher.start() ;
                    String state = location.substring(start, start+2) ;
    
                    output.collect(newText(lookupState(state.toUpperCase())), one);
                }
            }
        }
    
        private String lookupState( String state)
        {
            String fullName = stateNames.get(state) ;
    
            return fullName == null? "Other": fullName ;
            }
    }
    
  6. 编译这些类并将作业提交给 Hadoop。 然后检索结果文件。

刚刚发生了什么?

我们首先创建了将在作业中使用的查找文件,并将其放在 HDFS 上。 要添加到分布式缓存的文件最初必须复制到 HDFS 文件系统。

创建新作业文件后,我们添加了所需的类导入。 然后,我们修改了 Driver 类,将每个节点上要添加到 DistributedCache 的文件添加到其中。 可以通过多种方式指定文件名,但最简单的方式是使用 HDFS 上文件位置的绝对路径。

我们的映射器类有很多变化。 我们添加了一个被覆盖的configure方法,该方法用于填充一个映射,该映射将用于将州缩写与其全名相关联。

configure方法在任务启动时调用,默认实现不执行任何操作。 在我们的覆盖版本中,我们检索已添加到分布式缓存的文件数组。 因为我们知道缓存中只有一个文件,所以使用该数组中的第一个索引是安全的,并将其传递给一个utility方法,该方法解析该文件并使用其内容填充州缩写查找映射。 请注意,一旦检索到文件引用,我们就可以使用标准 Java I/O 类访问该文件;它毕竟只是本地文件系统上的一个文件。

我们添加另一个方法来执行查找,该方法获取从 Location 字段提取的字符串,如果匹配则返回州的全名,否则返回字符串Other。 这是在通过OutputCollector类写入映射结果之前调用的。

此作业的结果应与以下数据类似:

Alabama548
Alaska234
Arizona2097
Arkansas534
California7679
…
Other4531…
…

这很好用,但我们在此过程中丢失了一些信息。 在我们的验证映射器中,我们只需删除不符合我们的六个字段标准的任何行。 虽然我们不关心单个丢失的记录,但我们可能会关心丢失的记录数量是否很多。 目前,我们确定这一点的唯一方法是将每个识别的州的记录数相加,然后从文件中的总记录数中减去。 我们还可以尝试让这些数据流经作业的其余部分,以收集在一个特殊的简化密钥中,但这似乎也是错误的。 幸运的是,还有更好的办法。

计数器、状态和其他输出

在每个 MapReducejob 结束时,我们会看到与计数器相关的输出,如下所示:

12/02/12 06:28:51 INFO mapred.JobClient: Counters: 22
12/02/12 06:28:51 INFO mapred.JobClient:   Job Counters 
12/02/12 06:28:51 INFO mapred.JobClient:     Launched reduce tasks=1
12/02/12 06:28:51 INFO mapred.JobClient:     Launched map tasks=18
12/02/12 06:28:51 INFO mapred.JobClient:     Data-local map tasks=18
12/02/12 06:28:51 INFO mapred.JobClient:   SkippingTaskCounters
12/02/12 06:28:51 INFO mapred.JobClient:     MapProcessedRecords=61393
…

可以添加用户定义的计数器,这些计数器同样将从所有任务中聚合,并在最终输出和 MapReduce web UI 中报告。

执行操作的时间-创建计数器、任务状态和写入日志输出

我们将修改我们的UFORecordValidationMapper以报告有关跳过记录的统计信息,并突出显示用于记录有关作业信息的其他一些工具:

  1. 创建以下内容作为UFOCountingRecordValidationMapper.java文件:

    import java.io.IOException;
    
    import org.apache.hadoop.io.* ;
    import org.apache.hadoop.mapred.* ;
    import org.apache.hadoop.mapred.lib.* ;
    
    public class UFOCountingRecordValidationMapper extends MapReduceBase
    implements Mapper<LongWritable, Text, LongWritable, Text>
    {
    
        public enum LineCounters
        {
            BAD_LINES,
            TOO_MANY_TABS,
            TOO_FEW_TABS
        } ;
    
        public void map(LongWritable key, Text value,
            OutputCollector<LongWritable, Text> output,
            Reporter reporter) throws IOException
        {
            String line = value.toString();
    
            if (validate(line, reporter))
    Output.collect(key, value);
        }
    
        private boolean validate(String str, Reporter reporter)
        {
            String[] parts = str.split("\t") ;
    
            if (parts.length != 6)
            {
                if (parts.length < 6)
                {
    reporter.incrCounter(LineCounters.TOO_FEW_TABS, 1) ;
                }
                else
                {
                    reporter.incrCounter(LineCounters.TOO_MANY_TABS, 1) ;
                }
    
                reporter.incrCounter(LineCounters.BAD_LINES, 1) ;
    
    if((reporter.getCounter(
    LineCounters.BAD_LINES).getCounter()%10)
    == 0)
                {
                    reporter.setStatus("Got 10 bad lines.") ;
                    System.err.println("Read another 10 bad lines.") ;
                }
    
                return false ;
            }
            return true ;
        }
            }
    
  2. 复制UFOLocation2.java文件作为UFOLocation3.java文件,以使用此新映射器而不是UFORecordValidationMapper

    …
            JobConf mapconf1 = new JobConf(false) ;
            ChainMapper.addMapper( conf, 
    UFOCountingRecordValidationMapper.class,
                LongWritable.class, Text.class, LongWritable.class, 
    Text.class,
                true, mapconf1) ;
    
  3. 编译文件,将它们打包,然后将作业提交给 Hadoop:

    …
    12/02/12 06:28:51 INFO mapred.JobClient: Counters: 22
    12/02/12 06:28:51 INFO mapred.JobClient:   UFOCountingRecordValidationMapper$LineCounters
    12/02/12 06:28:51 INFO mapred.JobClient:     TOO_MANY_TABS=324
    12/02/12 06:28:51 INFO mapred.JobClient:     BAD_LINES=326
    12/02/12 06:28:51 INFO mapred.JobClient:     TOO_FEW_TABS=2
    12/02/12 06:28:51 INFO mapred.JobClient:   Job Counters 
    
    
  4. Use a web browser to go to the MapReduce web UI (remember by default it is on port 50030 on the JobTracker host). Select the job at the bottom of the Completed Jobs list and you should see a screen similar to the following screenshot:

    Time for action – creating counters, task states, and writing log output

  5. Click on the link to the map tasks and you should see an overview screen like the following screenshot:

    Time for action – creating counters, task states, and writing log output

  6. For one of the tasks with our custom status message, click on the link to its counters. This should give a screen similar to the one shown as follows:

    Time for action – creating counters, task states, and writing log output

  7. Go back to the task list and click on the task ID to get the task overview similar to the following screenshot:

    Time for action – creating counters, task states, and writing log output

  8. Under the Task Logs column are options for the amount of data to be displayed. Click on All and the following screenshot should be displayed:

    Time for action – creating counters, task states, and writing log output

  9. 现在登录到其中一个任务节点,查看存储在hadoop/logs/userlogs下的文件。 每个任务尝试都有一个目录,每个目录中都有几个文件;要查找的文件是stderr

刚刚发生了什么?

为了添加新的计数器,我们需要做的第一件事是创建一个标准的 Java 枚举来容纳它们。 在本例中,我们创建了 Hadoop 认为的名为LineCounters的计数器组,其中有三个计数器表示坏行的总数,而更细粒度的计数器表示字段太少或太多的行数。 这就是创建一组新计数器所需做的全部工作;定义枚举,一旦开始设置计数器值,框架将自动理解它们。

要添加到计数器,我们只需通过Reporter对象将其递增,在这里的每种情况下,我们都会在每次遇到坏行时添加一行,一行的字段少于六个,另一行的字段多于六个。

我们还检索任务的BAD_LINE计数器,如果它是 10 的倍数,则执行以下操作:

  • 设置任务状态以反映此事实
  • 使用标准的 JavaSystem.err.println机制向stderr编写类似的消息

然后,我们转到 MapReduce UI,验证是否可以在作业概述中看到计数器总数,以及在任务列表中看到带有自定义状态消息的任务。

然后,我们浏览 Web 用户界面,查看单个作业的计数器,然后在我们看到的任务的详细信息页面下,我们可以单击该任务的日志文件。

然后,我们查看了其中一个节点,发现 Hadoop 还捕获了文件系统{HADOOP_HOME}/logs/userlogs目录下的目录中每个任务的日志。 在每个任务尝试的子目录下,有用于标准流和常规任务日志的文件。 正如您将看到的,繁忙的节点最终可能会有大量的任务日志目录,并且识别感兴趣的任务目录并不总是那么容易。 事实证明,Web 界面对这些数据的查看效率更高。

提示

如果您使用的是 Hadoopcontext对象 API,则可以通过Context.getCounter().increment()方法访问计数器。

信息太多了!

在不太担心如何从我们的工作中获得地位和其他信息之后,我们似乎突然有了太多令人困惑的选择。 事实是,尤其是在运行完全分布式集群时,确实无法避免数据可能分布在每个节点上的事实。 使用 Java 代码,我们不能像使用 Ruby 流任务那样,在命令行上轻松地模拟它的用法;因此,需要仔细考虑在运行时需要哪些信息。 这应包括有关一般作业操作(附加统计数据)以及可能需要进一步调查的问题指标的详细信息。

计数器、任务状态消息和老式的 Java 日志记录可以协同工作。 如果存在您关心的情况,请将其设置为将记录每次发生的计数器,并考虑设置遇到该情况的任务的状态消息。 如果有特定数据,则将其写入stderr。 由于计数器非常容易看到,因此如果发生感兴趣的情况,您可以很快知道作业完成后的情况。 从这里,您可以转到 web 用户界面,并一目了然地看到遇到这种情况的所有任务。 在这里,您可以单击查看任务的更详细日志。

事实上,您不需要等到作业完成;计数器和任务状态消息会随着作业的进行而在 Web 用户界面中更新,因此您可以在计数器或任务状态消息提醒您注意此情况时立即开始调查。 这在运行时间很长的作业中特别有用,因为错误可能会导致您中止作业。

摘要

本章介绍了 MapReduce 作业的开发,重点介绍了您可能经常遇到的一些问题和方法。 特别是,我们了解了 Hadoop Streaming 如何提供一种使用脚本语言编写映射和减少任务的方法,以及使用 Streaming 如何成为作业原型和初始数据分析早期阶段的有效工具。

我们还了解到,使用脚本语言编写任务可以提供使用命令行工具直接测试和调试代码的额外好处。 在 Java API 中,我们看到了ChainMapper类,它提供了一种将复杂的地图任务分解成一系列更小、更集中的任务的有效方法。

然后,我们了解了分布式缓存如何提供在所有节点之间高效共享数据的机制。 它将文件从 HDFS 复制到每个节点上的本地文件系统,从而提供对数据的本地访问。 我们还了解了如何通过为计数器组定义 Java 枚举并使用框架方法递增其值来添加作业计数器,以及如何组合使用计数器、任务状态消息和调试日志来开发高效的作业分析工作流。

我们希望您在开发 MapReduce 作业时会经常遇到这些技术和想法。 在下一章中,我们将探索一系列更高级的技术,这些技术不太经常遇到,但当它们出现时,它们是无价的。

五、高级 MapReduce 技术

既然我们已经了解了 MapReduce 的基本原理及其用法的一些细节,接下来是研究 MapReduce 中涉及的更多技术和概念的时候了。 本章将介绍以下主题:

  • 对数据执行连接
  • 在 MapReduce 中实现图形算法
  • 如何以独立于语言的方式表示复杂数据类型

在此过程中,我们将使用案例研究作为示例,以突出其他方面,如提示和技巧,并确定最佳实践的一些领域。

简单、高级、介于两者之间

在章节标题中加入这个词“高级”有点危险,因为复杂性是一个主观概念。 所以让我们非常清楚这里讨论的材料。 我们丝毫不认为这是精炼智慧的巅峰,否则这些智慧将需要数年时间才能获得。 相反,我们也不认为本章介绍的一些技术和问题会出现在对 Hadoop 世界不熟悉的人身上。

因此,出于本章的目的,我们使用“高级”一词来涵盖您在最初的几天或几周内看不到的东西,或者如果您看到了也不一定会欣赏的东西。 这些技术既能为特定问题提供特定的解决方案,又能突出显示使用标准 Hadoop 和相关 API 来解决显然不适合 MapReduce 处理模型的问题的方法。 在此过程中,我们还将指出一些替代方法,这些方法我们在这里没有实现,但它们可能是进一步研究的有用资源。

我们的第一个案例研究是后一种情况的一个非常常见的示例;在 MapReduce 中执行联接类型的操作。

加入

很少有问题使用单一数据集。 在许多情况下,有一些简单的方法可以消除在 MapReduce 框架内尝试和处理大量离散但相关的数据集的需要。

当然,这里的类比与关系数据库中的JOIN概念类似。 将数据分割成多个表,然后使用将表连接在一起的 SQL 语句从多个源检索数据,这是非常自然的。 典型的例子是,主表只有特定事实的 ID 号,而针对其他表的联接用于提取有关唯一 ID 引用的信息的数据。

当这不是一个好主意的时候

在 MapReduce 中实现联接是可能的。 事实上,正如我们将看到的,问题与其说是做这件事的能力,不如说是在众多潜在战略中选择哪种战略。

但是,MapReduce 连接通常很难编写,而且很容易使效率低下。 无论使用 Hadoop 的时间长短,您都会遇到需要这样做的情况。 但是,如果您非常频繁地需要执行 MapReduce 连接,那么您可能需要问问自己,您的数据结构是否良好,本质上是否比您最初假设的更具关联性。 如果是这样的话,您可能需要考虑Apache Have(第 8 章关于数据与配置单元的关系视图的主要主题)或Apache Pig(在同一章中简要提到)。 两者都在 Hadoop 之上提供了额外的层,允许用高级语言表示数据处理操作;在配置单元中,通过 SQL 的变体。

地图侧连接与减少侧连接

请注意,在 Hadoop 中有两种连接数据的基本方法,它们的名称取决于作业执行中连接发生的位置。 在这两种情况下,我们都需要将多个数据流放在一起,并通过某些逻辑执行连接。 这两种方法之间的基本区别在于多个数据流是在映射器函数中组合还是在减少器函数中组合。

映射端联接,顾名思义是,将数据流读入映射器,并使用映射器函数中的逻辑来执行联接。 映射端联接的最大优势在于,通过在映射器中执行所有联接(更重要的是减少数据量),传输到 Reduce 阶段的数据量大大减少。 地图端连接的缺点是,您要么需要找到一种方法来确保其中一个数据源非常小,要么需要定义作业输入以遵循非常具体的标准。 通常,要做到这一点,唯一的方法是使用另一个 MapReduce 作业对数据进行预处理,该作业的唯一目的是使数据为地图端连接做好准备。

相反,Reduce-Side Join在不执行任何连接逻辑的情况下通过映射阶段处理多个数据流,并在 Reduce 阶段进行连接。 这种方法的潜在缺点是,来自每个源的所有数据都被拉过混洗阶段,并被传递到还原器,在那里,大部分数据可能会被联接操作丢弃。 对于大型数据集,这可能会成为非常重要的开销。

Reduce-Side Join 的主要优势是它的简单性;您在很大程度上负责作业的结构,并且为相关数据集定义 Reduce-Side JOIN 方法通常非常简单。 让我们来看一个例子。

匹配客户和销售信息

许多公司的常见情况是销售记录与客户数据分开保存。 当然,这两者之间存在关系;通常,销售记录包含执行销售的用户帐户的唯一 ID。

在 Hadoop 世界中,这些数据文件由两种类型的数据文件表示:一种包含用户 ID 和销售信息的记录,另一种包含每个用户帐户的完整数据。

频繁的任务需要使用这两个来源的数据的报告;例如,我们希望看到每个用户的销售总数和总价值,但不希望将其与匿名 ID 号关联,而是与姓名关联。 当客户服务代表希望呼叫最频繁的客户(来自销售记录的数据),但又希望能够用名字而不只是一个号码来指代该人时,这可能是很有价值的。

使用 MultipleInput 进行操作减少端连接的时间

通过执行以下步骤,我们可以使用 Reduce-Side 联接执行上一节中说明的报告:

  1. 创建以下以制表符分隔的文件,并将其命名为sales.txt

    00135.992012-03-15
    00212.492004-07-02
    00413.422005-12-20
    003499.992010-12-20
    00178.952012-04-02
    00221.992006-11-30
    00293.452008-09-10
    0019.992012-05-17
    
  2. 创建以下以制表符分隔的文件,并将其命名为accounts.txt

    001John AllenStandard2012-03-15
    002Abigail SmithPremium2004-07-13
    003April StevensStandard2010-12-20
    004Nasser HafezPremium2001-04-23
    
  3. 将数据文件拷贝到 HDFS。

    $ hadoop fs -mkdir sales
    $ hadoop fs -put sales.txt sales/sales.txt
    $ hadoop fs -mkdir accounts
    $ hadoop fs -put accounts/accounts.txt
    
    
  4. 创建以下文件并将其命名为ReduceJoin.java

    import java.io.* ;
    
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.*;
    import org.apache.hadoop.mapreduce.lib.input.*;
    import org.apache.hadoop.mapreduce.lib.input.*;
    
    public class ReduceJoin
    {
    
        public static class SalesRecordMapper
    extends Mapper<Object, Text, Text, Text>
    {
    
            public void map(Object key, Text value, Context context)
    throws IOException, InterruptedException
            {
                String record = value.toString() ;
                String[] parts = record.split("\t") ;
    
                context.write(new Text(parts[0]), new 
    Text("sales\t"+parts[1])) ;
            }
        }
    
        public static class AccountRecordMapper
    extends Mapper<Object, Text, Text, Text>
    {
            public void map(Object key, Text value, Context context)
    throws IOException, InterruptedException
            {
                String record = value.toString() ;
                String[] parts = record.split("\t") ;
    
                context.write(new Text(parts[0]), new 
    Text("accounts\t"+parts[1])) ;
           }
        }
    
        public static class ReduceJoinReducer
        extends Reducer<Text, Text, Text, Text>
        {
    
            public void reduce(Text key, Iterable<Text> values,
                Context context)
                throws IOException, InterruptedException
            {
                String name = "" ;
    double total = 0.0 ;
                int count = 0 ;
    
                for(Text t: values)
                {
                    String parts[] = t.toString().split("\t") ;
    
                    if (parts[0].equals("sales"))
                    {
                        count++ ;
                        total+= Float.parseFloat(parts[1]) ;
                    }
                    else if (parts[0].equals("accounts"))
                    {
                        name = parts[1] ;
                    }
                }
    
                String str = String.format("%d\t%f", count, total) ;
                context.write(new Text(name), new Text(str)) ;
            }
        }
    
        public static void main(String[] args) throws Exception 
    {
            Configuration conf = new Configuration();
            Job job = new Job(conf, "Reduce-side join");
            job.setJarByClass(ReduceJoin.class);
            job.setReducerClass(ReduceJoinReducer.class);
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(Text.class);
    MultipleInputs.addInputPath(job, new Path(args[0]), 
    TextInputFormat.class, SalesRecordMapper.class) ;
    MultipleInputs.addInputPath(job, new Path(args[1]), 
    TextInputFormat.class, AccountRecordMapper.class) ;
            Path outputPath = new Path(args[2]);
            FileOutputFormat.setOutputPath(job, outputPath);
    outputPath.getFileSystem(conf).delete(outputPath);
    
            System.exit(job.waitForCompletion(true) ? 0 : 1);
        }
    }
    
  5. 编译文件并将其添加到 JAR 文件。

    $ javac ReduceJoin.java
    $ jar -cvf join.jar *.class
    
    
  6. 通过执行以下命令运行作业:

    $ hadoop jar join.jarReduceJoin sales accounts outputs
    
    
  7. 检查结果文件。

    $ hadoop fs -cat /user/garry/outputs/part-r-00000
    John Allen3124.929998
    Abigail Smith3127.929996
    April Stevens1499.989990
    Nasser Hafez113.420000
    
    

刚刚发生了什么?

首先,我们创建了要在本例中使用的数据文件。 我们创建了两个较小的数据集,因为这样可以更容易地跟踪结果输出。 我们定义的第一个数据集是具有四列的帐户详细信息,如下所示:

  • 帐户 ID
  • 客户端名称
  • 帐户类型
  • 开户日期

然后,我们创建了一个包含三列的销售记录:

  • 购买者的帐户 ID
  • 这笔交易的价值
  • 销售日期

当然,真实的帐目和销售记录中的字段会比这里提到的字段多得多。 创建文件后,我们将它们放到 HDFS 上。

然后,我们创建了ReduceJoin.java文件,该文件看起来与我们以前使用的 MapReduce 作业非常相似。 这项工作有几个方面让它变得特别,并允许我们实现联接。

首先,该类有两个已定义的映射器。 正如我们以前看到的,作业可以在一个链中执行多个映射器;但在本例中,我们希望对每个输入位置应用不同的映射器。 因此,我们将销售和帐户数据定义到SalesRecordMapperAccountRecordMapper类 ES 中。 我们使用了org.apache.hadoop.mapreduce.lib.io包中的MultipleInputs类,如下所示:

MultipleInputs.addInputPath(job, new Path(args[0]), 
TextInputFormat.class, SalesRecordMapper.class) ;
MultipleInputs.addInputPath(job, new Path(args[1]), 
TextInputFormat.class, AccountRecordMapper.class) ;

如您所见,与前面添加单个输入位置的示例不同,MultipleInputs类允许我们添加多个源,并将每个源与不同的输入格式和映射器相关联。

映射器非常简单;SalesRecordMapper类发出形式<account number>, <sales value>的输出,而AccountRecordMapper类发出形式<account number>, <client name>的输出。 因此,我们将每个销售的订单值和客户名称传递到将进行实际联接的 Reducer。

请注意,这两个映射器实际上发出的值都超过了所需的值。 SalesRecordMapper类以sales作为其值输出的前缀,而AccountRecordMapper类使用标记account

如果我们看看减速器,就会明白为什么会这样。 Reducer 检索给定键的每条记录,但是如果没有这些显式标记,我们就不知道给定值是来自 Sales 还是 Account 映射器,因此无法理解如何处理数据值。

因此,ReduceJoinReducer类根据它们来自哪个映射器,以不同方式处理Iterator对象中的值。 AccountRecordMapper类中的值(应该只有一个)用于填充最终输出中的客户端名称。 对于每个销售记录-可能是多个,因为大多数客户购买的商品不止一个-订单总数被算作总价值。 因此,减少器的输出是账户持有人姓名的关键字,以及包含订单数和总订单值的值字符串。

我们编译并执行该类;注意我们如何提供表示两个输入目录和单个输出源的三个参数。 由于MultipleInputs类的配置方式,我们还必须确保以正确的顺序指定目录;没有动态机制来确定哪种类型的文件位于哪个位置。

执行后,我们检查输出文件并确认它确实如预期的那样包含指定客户端的总数。

DataJoinMapper 和 TaggedMapperOutput

有一种方法可以以更复杂和面向对象的方式实现减少端联接。 在org.apache.hadoop.contrib.join包中有像DataJoinMapperBaseTaggedMapOutput这样的类,它们提供了一种封装的方法来派生标签用于映射输出,并在还原器上对其进行处理。 这种机制意味着您不必像我们以前那样定义显式的 Tag 字符串,然后仔细解析在 Reducer 接收到的数据以确定数据来自哪个映射器;提供的类中有封装此功能的方法。

当使用数字或其他非文本数据时,此功能特别有价值。 要创建我们自己的显式标记(如上例所示),我们必须将整数等类型转换为字符串,以便添加所需的前缀标记。 这将比使用标准形式的数值类型并依赖额外的类来实现标记的效率更低。

该框架允许非常复杂的标签生成,以及我们之前没有实现的标签分组等概念。 使用这种机制需要额外的工作,包括覆盖其他方法和使用不同的映射基类。 对于像上一个示例中这样简单的连接,这个框架可能有些夸张,但是如果您发现自己实现了非常复杂的标记逻辑,那么它可能值得一看。

实现地图端连接

要在给定点连接到,我们必须有权访问该点每个数据集中的相应记录。 这就是 Reduce 端联接的简单性发挥作用的地方;尽管它会导致额外的网络流量开销,但根据定义处理它可以确保 Reducer 具有与联接键相关联的所有记录。

如果我们希望在映射器中执行连接,那么要使该条件成立就不那么容易了。 我们不能假设我们的输入数据的结构足够好,可以同时读取相关记录。 我们通常有两类方法:消除从多个外部源读取数据的需要,或者对数据进行预处理,使其可用于地图端连接。

使用分布式缓存

实现第一种方法的最简单方法是获取除一个数据集之外的所有数据集,并使其在我们在上一章中使用的分布式缓存中可用。 该方法可以用于多个数据源,但为简单起见,我们只讨论两个。

如果我们有一个较大的数据集和一个较小的数据集,例如前面的销售和客户信息,一种选择是将客户信息打包并将其推送到 Distributed Cache 中。 然后,每个映射器将该数据读取到一个高效的数据结构中,例如使用连接键作为散列键的散列表。 然后处理销售记录,并且在处理每个记录期间,可以从哈希表中检索所需的帐户信息。

这种机制非常有效,当一个较小的数据集可以很容易地装入内存时,这是一个很好的方法。 然而,我们并不总是那么幸运,有时最小的数据集仍然太大,无法复制到每台工人机器上并保存在内存中。

拥有围棋英雄-实现地图侧连接

以前面的销售/客户记录示例为例,使用 Distributed Cache 实现映射端联接。 如果将帐户记录加载到将帐户 ID 号映射到客户名称的哈希表中,则可以使用帐户 ID 检索客户名称。 在处理销售记录时,在映射器中执行此操作。

修剪数据以适合缓存

如果最小的数据集合仍然太大,无法在分布式缓存中使用,则不一定会丢失所有数据。 例如,我们前面的例子只从每个记录中提取了两个字段,而丢弃了作业不需要的其他字段。 在现实中,帐户将由许多属性描述,这种缩减将极大地限制数据大小。 Hadoop 可用的数据通常是完整的数据集,但我们需要的只是字段的一个子集。

因此,在这种情况下,可以从完整数据集中仅提取 MapReduce 作业期间需要的字段,并在这样做时创建足够小以用于缓存的修剪后的数据集。

备注

这是一个与底层的面向列的数据库非常相似的概念。 传统的关系数据库每次存储一行数据,这意味着需要读取整行才能提取单个列。 基于列的数据库改为单独存储每列,从而允许查询只读取它感兴趣的列。

如果您采用这种方法,您需要考虑将使用什么机制来生成数据子集,以及生成数据子集的频率。 显而易见的方法是编写另一个 MapReduce 作业,该作业执行必要的过滤,然后该输出在分布式缓存中用于后续作业。 如果较小的数据集只有很少的更改,那么您可以按计划生成修剪后的数据集;例如,每晚刷新它。 否则,您将需要创建两个 MapReduce 作业链:一个用于生成修剪后的数据集,另一个用于使用大集和分布式缓存中的数据执行连接操作。

使用数据表示而不是原始数据

有时,其中一个数据源不用于检索其他数据,而是用于派生一些事实,然后在决策过程中使用这些事实。 例如,我们可能希望筛选销售记录,以便仅提取发货地址位于特定区域的记录。

在这种情况下,我们可以将所需的数据大小减少到可能更容易放入缓存中的适用销售记录的列表。 我们可以再次将其存储为哈希表,在哈希表中,我们只是记录记录有效的事实,甚至可以使用排序列表或树之类的东西。 在我们可以接受一些假阳性的情况下,仍然保证没有假阴性,Bloom filter提供了一种非常紧凑的方式来表示这样的信息。

可以看出,应用这种方法来启用地图端连接需要创造力,而且在数据集的性质和手头的问题上需要相当大的运气。 但请记住,最好的关系数据库管理员会花费大量时间优化查询以删除不必要的数据处理;因此,询问您是否真的需要处理所有这些数据并不是一个坏主意。

使用多个映射器

从根本上说,之前的技术试图消除完全交叉数据集联接的需要。 但有时这是您必须要做的;您可能只是拥有非常大的数据集,而这些数据集不能以任何一种聪明的方式组合在一起。

org.apache.hadoop.mapreduce.lib.join包中有支持这种情况的类。 主要感兴趣的类是CompositeInputFormat,它应用一个用户定义的函数来组合来自多个数据源的记录。

这种方法的主要限制是,除了以相同的方式对数据源进行排序和分区之外,还必须已经基于公用键对数据源进行索引。 原因很简单:当从每个源读取数据时,框架需要知道每个位置是否存在给定键。 如果我们知道每个分区都经过排序并且包含相同的键范围,那么简单的迭代逻辑就可以完成所需的匹配。

这种情况显然不会偶然发生,因此您可能再次发现自己编写预处理作业来将所有输入数据源转换为正确的排序和分区结构。

备注

本讨论开始涉及分布式和并行连接算法;这两个主题都有广泛的学术和商业研究。 如果您对这些想法感兴趣,并且想要了解更多的基本理论,请到http://scholar.google.com上搜索。

加入或不加入...

在我们了解了 MapReduce 世界的连接之后,让我们回到最初的问题:您真的确定要这样做吗? 通常在相对容易实现但效率低下的减端连接和更高效但更复杂的映射端替代方案之间进行选择。 我们已经看到,连接确实可以在 MapReduce 中实现,但它们并不总是美观的。 这就是为什么我们建议使用 Hive 或 PIG 之类的东西,如果这些类型的问题构成了你的工作量的很大一部分。 显然,我们可以使用那些在幕后自行转换为 MapReduce 代码并直接实现映射端和 Reduce 端连接的工具,但对于此类工作负载,通常最好使用设计良好、优化良好的库,而不是构建自己的库。 毕竟,这就是您使用 Hadoop 而不是编写自己的分布式处理框架的原因!

图算法

任何优秀的计算机科学家都会告诉你,图形数据结构是最强大的工具之一。 许多复杂的系统最好用图形来表示,至少几十年(如果你对它有更多的数学知识,可以追溯到几个世纪)的知识体系提供了非常强大的算法来解决各种各样的图形问题。 但就其本质而言,图及其算法在 MapReduce 范例中通常是很难想象的。

图表 101

让我们后退一步,定义一些术语。 图是由节点(也称为顶点)组成的结构,这些节点(也称为顶点)通过称为的链接相连。 根据图形的类型,边可以是双向的,也可以是单向的,并且可能具有与其关联的权重。 例如,城市道路网可以看作是一个图形,其中道路是边,交叉点和兴趣点是节点。 有些街道是单程的,有些不是,有些是收费的,有些是在一天中的某些时间封闭的,等等。

对于运输公司来说,通过优化从一个点到另一个点的路线可以赚很多钱。 不同的图形算法可以通过考虑诸如单行道之类的属性以及表示为使给定道路更具吸引力或更不吸引人的权重的其他成本来导出这样的路线。

举一个更新的例子,想一想 Facebook 等网站所普及的社交图,其中节点是人,边是他们之间的关系。

图表和 MapReduce-在某处匹配

图形看起来不像许多其他 MapReduce 问题的主要原因是图形处理的有状态特性,这可以在元素之间以及通常在为单一算法一起处理的大量节点之间的基于路径的关系中看到。 图形算法倾向于使用全局状态的概念来确定下一步要处理哪些元素,并在每一步修改这样的全局知识。

具体地说,大多数众所周知的算法通常以增量或重入的方式执行,构建表示已处理节点和挂起节点的结构,并在减少前者的同时处理后者。

另一方面,MapReduce 问题在概念上是无状态的,通常基于分而治之的方法,其中每个 Hadoop 工作线程主机处理一小部分数据,写出最终结果的一部分,其中总作业输出被视为这些较小输出的简单集合。 因此,在 Hadoop 中实现图形算法时,我们需要在无状态并行和分布式框架中表达从根本上是有状态的和概念性单线程的算法。 这就是挑战!

大多数众所周知的图算法都是基于对图的搜索或遍历,通常是为了找到节点之间的路由-通常按某种成本概念进行排序。 最基本的图遍历算法有深度优先搜索(DFS)和广度优先搜索(BFS)。 算法之间的区别在于节点相对于其邻居的处理顺序不同。

我们将介绍实现这种遍历的特殊形式的算法;对于图中给定的起始节点,确定它与图中所有其他节点之间的距离。

备注

可以看出,图形算法和理论领域是一个巨大的领域,我们在这里几乎没有触及到它的皮毛。 如果您想了解更多,图表上的维基百科条目是一个很好的起点;它可以在http://en.wikipedia.org/wiki/Graph_(abstract_data_type)找到。

表示图

我们面临的第一个问题是如何用一种我们可以使用 MapReduce 高效处理的方式来表示图形。 有几种众所周知的图表示,称为基于指针的、邻接矩阵和邻接列表。 在大多数实现中,这些表示通常假定单个进程空间具有整个图的全局视图;我们需要修改表示以允许在离散映射和 Reduce 任务中处理单个节点。

我们将在下面的示例中使用此处所示的图表。 该图表确实有一些额外的信息,稍后将对其进行解释。

Representing a graph

我们的图非常简单;它只有 7 个节点,除了一条边之外,所有的边都是双向的。 我们还使用标准图形算法中使用的常用着色技术,如下所示:

  • 白色节点尚未处理
  • 当前正在处理灰色节点
  • 已处理黑色节点

当我们在以下步骤中处理图形时,我们预计会看到节点经过这些阶段。

动作时间-表示图形

让我们定义将在以下示例中使用的图形的文本表示形式。

将以下内容创建为graph.txt

12,3,40C
21,4
31,5,6
41,2
53,6
63,5
76

刚刚发生了什么?

我们定义了一个文件结构来表示我们的图形,这在一定程度上基于邻接列表方法。 我们假设每个节点都有一个唯一的 ID,文件结构有四个字段,如下所示:

  • 节点 ID
  • 以逗号分隔的邻居列表
  • 到起始节点的距离
  • 节点状态

在初始表示中,只有起始节点具有第三列和第四列的值:它与自身的距离是 0,它的状态是“C”,这将在后面解释。

我们的图是有向图-更正式地称为有向图-也就是说,如果节点 1 将节点 2 列为邻居,则如果节点 2 也将节点 1 列为其邻居,则只有一条返回路径。 我们在图形表示中看到了这一点,除了一条边之外,所有边的两端都有一个箭头。

算法概述

因为该算法和相应的 MapReduce 作业非常复杂,所以我们将在显示代码之前对其进行解释,然后在稍后的使用中进行演示。

根据前面的表示,我们将定义一个 MapReduce 作业,该作业将多次执行以获得最终输出;该作业的给定执行的输入将是上一次执行的输出。

根据上一节中描述的颜色代码,我们将为节点定义三种状态:

  • Pending:节点尚待处理,处于默认状态(白色)
  • 当前正在处理:节点正在处理中(灰色)
  • 完成:节点的最终距离已确定(黑色)

映射器

映射器将读入图形的当前表示,并按如下方式处理每个节点:

  • 如果该节点被标记为完成,它将提供不带任何更改的输出。
  • 如果节点被标记为当前正在处理,则其状态将更改为 Done,并在不进行其他更改的情况下提供输出。 它的每个邻居按照当前记录给出输出,其距离递增 1,但是没有邻居;例如,节点 1 不知道节点 2 的邻居。
  • 如果节点被标记为挂起,则其状态将更改为当前正在处理,并且它会在不做进一步更改的情况下提供输出。

减速机

减法器将接收每个节点 ID 的一条或多条记录,并将它们的值合并到该阶段的最终输出节点记录中。

减速器的通用算法如下:

  • 完成记录是最终输出,不会执行值的进一步处理
  • 对于其他节点,通过获取邻居列表、找到该节点的位置以及最高距离和状态来构建最终输出

迭代应用

如果我们应用一次这个算法,我们将使节点 1 被标记为完成,几个节点(它的直接邻居)被标记为当前节点,其他几个节点被标记为挂起。 该算法的连续应用将看到所有节点移动到其最终状态;当遇到每个节点时,其邻居被带入处理流水线。 我们稍后会展示这一点。

行动时间-创建源代码

现在我们将查看实现图形遍历的源代码。 因为代码很长,我们将把它分成多个步骤;显然,它们都应该放在一个源文件中。

  1. 使用这些导入将以下内容创建为GraphPath.java

    import java.io.* ;
    
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.mapreduce.*;
    import org.apache.hadoop.mapreduce.lib.input.*;
    import org.apache.hadoop.mapreduce.lib.output.*;
    
    public class GraphPath
    {
    
  2. 创建内部类以保存节点的面向对象表示:

    // Inner class to represent a node
        public static class Node
        {
    // The integer node id
            private String id ;
    // The ids of all nodes this node has a path to
            private String neighbours ;
    // The distance of this node to the starting node
            private int distance ;
    // The current node state
            private String state ;
    
    // Parse the text file representation into a Node object
            Node( Text t)
            {
                String[] parts = t.toString().split("\t") ;
    this.id = parts[0] ;
    this.neighbours = parts[1] ;
                if (parts.length<3 || parts[2].equals(""))
    this.distance = -1 ;
                else
    this.distance = Integer.parseInt(parts[2]) ;
    
                if (parts.length< 4 || parts[3].equals(""))
    this.stae = "P" ;
                else
    this.state = parts[3] ;
            }
    
    // Create a node from a key and value object pair
            Node(Text key, Text value)
            {
                this(new Text(key.toString()+"\t"+value.toString())) ;
            }
    
            Public String getId()
            {return this.id ;
            }
    
            public String getNeighbours()
            {
                return this.neighbours ;
            }
    
            public int getDistance()
            {
                return this.distance ;
            }
    
            public String getState()
            {
                return this.state ;
            }
        }
    
  3. 为作业创建映射器。 映射器将为其输入创建一个新的Node对象,然后对其进行检查,并根据其状态进行适当的处理。

        public static class GraphPathMapper
    extends Mapper<Object, Text, Text, Text>
    {
    
           public void map(Object key, Text value, Context context)
    throws IOException, InterruptedException 
    {
             Node n = new Node(value) ;
    
             if (n.getState().equals("C"))
             {
    //  Output the node with its state changed to Done
                context.write(new Text(n.getId()), new Text(n.getNeighbours()+"\t"+n.getDistance()+"\t"+"D")) ;
    
                    for (String neighbour:n.getNeighbours().split(","))
                    {
    // Output each neighbour as a Currently processing node
    // Increment the distance by 1; it is one link further away
                        context.write(new Text(neighbour), new 
    Text("\t"+(n.getDistance()+1)+"\tC")) ;
                    }
                }
                else
                {
    // Output a pending node unchanged
                    context.write(new Text(n.getId()), new 
    Text(n.getNeighbours()+"\t"+n.getDistance()
    +"\t"+n.getState())) ;
                }
    
            }
        }
    
  4. 为作业创建减速机。 与映射器一样,这将读入节点的表示形式,并根据节点的状态提供不同的值作为输出。 基本方法是从输入中收集状态和距离列的最大值,并通过此方法收敛到最终解决方案。

        public static class GraphPathReducer
    extends Reducer<Text, Text, Text, Text>
    {
    
            public void reduce(Text key, Iterable<Text> values,
                Context context)
                throws IOException, InterruptedException 
    {
    // Set some default values for the final output
                String neighbours = null ;
    int distance = -1 ;
    String state = "P" ;
    
                for(Text t: values)
    {
                    Node n = new Node(key, t) ;
    
                    if (n.getState().equals("D"))
                    {
    // A done node should be the final output; ignore the remaining 
    // values
    neighbours = n.getNeighbours() ;
                        distance = n.getDistance() ;
                        state = n.getState() ;
                        break ;
                    }
    
    // Select the list of neighbours when found                
                    if (n.getNeighbours() != null)
    neighbours = n.getNeighbours() ;
    
    // Select the largest distance
                    if (n.getDistance() > distance)
    distance = n.getDistance() ;
    
    // Select the highest remaining state
                    if (n.getState().equals("D") || 
    (n.getState().equals("C") &&state.equals("P")))
    state=n.getState() ;
                }
    
    // Output a new node representation from the collected parts        
                context.write(key, new 
    Text(neighbours+"\t"+distance+"\t"+state)) ;
            }
        }
    
  5. 创建作业驱动程序:

        public static void main(String[] args) throws Exception 
    {
            Configuration conf = new Configuration();
            Job job = new Job(conf, "graph path");
            job.setJarByClass(GraphPath.class);
            job.setMapperClass(GraphPathMapper.class);
            job.setReducerClass(GraphPathReducer.class);
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(Text.class);
            FileInputFormat.addInputPath(job, new Path(args[0]));
            FileOutputFormat.setOutputPath(job, new Path(args[1]));
            System.exit(job.waitForCompletion(true) ? 0 : 1);
        }
    }
    

刚刚发生了什么?

这里的作业实现了前面描述的算法,我们将在下面几节中执行该算法。 作业设置非常标准,除了算法定义之外,这里唯一的新功能是使用内部类来表示节点。

映射器或缩减器的输入通常是更复杂结构或对象的展平表示。 我们可以只使用该表示,但在这种情况下,这将导致映射器和减简器主体充满文本和字符串操作代码,从而使实际算法变得模糊。

使用Node内部类允许从平面文件到对象表示的映射,该对象表示将被封装在一个在业务领域方面有意义的对象中。 这也使得映射器和缩减器逻辑更加清晰,因为对象属性之间的比较比与仅由绝对索引位置标识的字符串片段的比较在语义上更有意义。

行动时间-第一次运行

现在,让我们对图形的起始表示执行此算法的初始执行:

  1. 将先前创建的graph.txt文件放入 HDFS:

    $ hadoop fs -mkdirgraphin
    $ hadoop fs -put graph.txtgraphin/graph.txt
    
    
  2. 编译作业并创建 JAR 文件:

    $ javac GraphPath.java
    $ jar -cvf graph.jar *.class
    
    
  3. 执行 MapReduce 作业:

    $ hadoop jar graph.jarGraphPathgraphingraphout1
    
    
  4. 检查输出文件:

    $ hadoop fs –cat /home/user/hadoop/graphout1/part-r00000
    12,3,40D
    21,41C
    31,5,61C
    41,21C
    53,6-1P
    63,5-1P
    76-1P
    
    

刚刚发生了什么?

将源文件放到 HDFS 上并创建作业 JAR 文件后,我们在 Hadoop 中执行该作业。 图形的输出表示形式显示了一些更改,如下所示:

  • 节点 1 现在被标记为完成;它与自身的距离显然为 0
  • 节点 2、3 和 4-节点 1 的邻居-被标记为当前正在处理
  • 所有其他节点都处于挂起状态

我们的图表现在看起来如下图所示:

What just happened?

在给定算法的情况下,这是意料之中的;第一个节点已完成,其通过映射器提取的相邻节点正在进行中。 所有其他节点尚未开始处理。

行动时间-第二次运行

如果我们将这个表示作为作业的另一次运行的输入,我们预计节点 2、3 和 4 现在应该是完整的,并且它们的邻居现在处于当前状态。 让我们看看,执行以下步骤:

  1. 通过执行以下命令执行 MapReduce 作业:

    $ hadoop jar graph.jarGraphPathgraphout1graphout2
    
    
  2. 检查输出文件:

    $ hadoop fs -cat /home/user/hadoop/graphout2/part-r000000
    12,3,40D
    21,41D
    31,5,61D
    41,21D
    53,62C
    63,52C
    76-1P
    
    

刚刚发生了什么?

不出所料,节点 1 到 4 已完成,节点 5 和 6 正在进行中,节点 7 仍处于挂起状态,如下图中的所示:

What just happened?

如果我们再次运行该作业,我们应该预期节点 5 和 6 已经完成,并且任何未处理的邻居都将成为当前节点。

行动时间-第三次运行

让我们通过第三次运行该算法来验证该假设。

  1. 执行 MapReduce 作业:

    $ hadoop jar graph.jarGraphPathgraphout2graphout3
    
    
  2. 检查输出文件:

    $ hadoop fs -cat /user/hadoop/graphout3/part-r-00000
    12,3,40D
    21,41D
    31,5,61D
    41,21D
    53,62D
    63,52D
    76-1P
    
    

刚刚发生了什么?

我们现在看到节点 1 到节点 6 是完整的。 但节点 7 仍处于挂起状态,当前没有节点在处理中,如下图所示:

What just happened?

这种状态的原因是,虽然节点 7 具有到节点 6 的链路,但在相反方向上没有边。 因此,实际上无法从节点 1 到达节点 7。如果我们最后一次运行该算法,我们应该会看到该图没有变化。

行动时间-第四次也是最后一次运行

让我们执行第四次执行,以验证输出现在已达到其最终稳定状态。

  1. 执行 MapReduce 作业:

    $ hadoop jar graph.jarGraphPathgraphout3graphout4
    
    
  2. 检查输出文件:

    $ hadoop fs -cat /user/hadoop/graphout4/part-r-00000
    12,3,40D
    21,41D
    31,5,61D
    41,21D
    53,62D
    63,52D
    76-1P
    
    

刚刚发生了什么?

输出与预期不谋而合;由于节点 1 或其任何邻居都无法到达节点 7,因此它将保持挂起状态,并且永远不会被进一步处理。 因此,我们的图形保持不变,如下图所示:

What just happened?

我们没有构建到算法中的一件事就是了解终止条件;如果运行没有创建任何新的 D 或 C 节点,那么这个过程就完成了。

我们在这里使用的机制是手动的,也就是说,我们通过检查知道图形表示已经达到了它的最终稳定状态。 不过,有几种方法可以通过编程实现这一点。 在后面的章节中,我们将讨论自定义作业计数器;例如,我们可以在每次创建新的 D 或 C 节点时递增计数器,并且只有在运行后该计数器大于零时才重新执行作业。

运行多个作业

前面的算法是我们第一次显式使用一个 MapReduce 作业的输出作为另一个作业的输入。 在大多数情况下,作业是不同的;但是,正如我们已经看到的,重复应用一种算法直到输出达到稳定状态是有价值的。

关于图形的最后思考

对于熟悉图形算法的任何人来说,前面的过程似乎非常陌生。 这仅仅是因为我们将有状态且可能递归的全局和可重入算法实现为一系列连续的无状态 MapReduce 作业。 重要的事实不在于所使用的特定算法;教训在于我们如何获取平面文本结构和一系列 MapReduce 作业,并由此实现类似于图形遍历的东西。 您可能会遇到一些问题,这些问题一开始似乎无法在 MapReduce 范例中实现;请考虑这里使用的一些技术,并记住许多算法都可以在 MapReduce 中建模。 它们看起来可能与传统方法有很大不同,但目标是正确的输出,而不是已知算法的实现。

使用独立于语言的数据结构

经常有人批评 Hadoop,社区一直在努力解决这一问题,那就是它非常以 Java 为中心。 指责一个完全用 Java 实现的项目是以 Java 为中心似乎有些奇怪,但这是从客户的角度考虑的。

我们已经展示了 Hadoop Streaming 如何允许使用脚本语言来实现映射和减少任务,以及管道如何为 C++提供类似的机制。 但是,Hadoop MapReduce 支持的输入格式的性质仍然是 Java 独有的。 最有效的格式是 SequenceFile,这是一种支持压缩的二进制可拆分容器。 但是,SequenceFiles 只有一个 Java API;它们不能用任何其他语言写入或读取。

我们可以让一个外部进程创建数据,以便将其摄取到 Hadoop 中以进行 MapReduce 处理,而最好的方法是将其简单地作为文本类型的输出,或者进行一些预处理以将输出格式转换为 SequenceFiles,然后将其推送到 HDFS 上。 在这里,我们还很难轻松地表示复杂的数据类型;我们要么必须将它们扁平化为文本格式,要么必须编写跨两种二进制格式的转换器,这两种格式都不是一个有吸引力的选择。

候选技术

幸运的是,近年来发布了几种技术,它们解决了跨语言数据表示的问题。 它们是协议缓冲区(由谷歌创建并托管在http://code.google.com/p/protobuf)、Thrift(最初由 Facebook 创建,现在是http://thrift.apache.org的一个 Apache 项目), 和Avro(由 Hadoop 的原始创建者 Doug Cutting 创建)。 考虑到它的传统和与 Hadoop 的紧密集成,我们将使用 Avro 来探讨这个主题。 我们不会在本书中介绍 Thrift 或 Protocol Buffers,但它们都是可靠的技术;如果您对数据序列化这一主题感兴趣,请查看它们的主页以获取更多信息。

介绍 Avro

AVRO 的主页是http://avro.apache.org,它是一个绑定了许多编程语言的数据持久化框架。 它创建了一种既可压缩又可拆分的二进制结构化格式,这意味着可以有效地将其用作 MapReduce 作业的输入。

Avro 允许定义分层数据结构;例如,我们可以创建包含数组、枚举类型和子记录的记录。 我们可以用任何编程语言创建这些文件,在 Hadoop 中处理它们,然后用第三种语言读取结果。

我们将在接下来的小节中讨论语言独立性的这些方面,但这种表达复杂结构化类型的能力也非常有价值。 即使我们只使用 Java,我们也可以使用 Avro 来允许我们将复杂的数据结构传入和传出映射器和减法器。 甚至像图形节点这样的东西!

该行动了-获取并安装 Avro

让我们下载 Avro 并将其安装到我们的系统上。

  1. http://avro.apache.org/releases.html下载 AVRO 的最新稳定版本。

  2. http://paranamer.codehaus.org下载最新版本的 ParaNamer 库。

  3. 将类添加到 Java 编译器使用的构建类路径中。

    $ export CLASSPATH=avro-1.7.2.jar:${CLASSPATH}
    $ export CLASSPATH=avro-mapred-1.7.2.jar:${CLASSPATH}
    $ export CLASSPATH=paranamer-2.5.jar:${CLASSPATH
    
    
  4. 将 Hadoop 发行版中现有的 JAR 文件添加到build类路径。

    Export CLASSPATH=${HADOOP_HOME}/lib/Jackson-core-asl-1.8.jar:${CLASSPATH}
    Export CLASSPATH=${HADOOP_HOME}/lib/Jackson-mapred-asl-1.8.jar:${CLASSPATH}
    Export CLASSPATH=${HADOOP_HOME}/lib/commons-cli-1.2.jar:${CLASSPATH}
    
    
  5. 将新的 JAR 文件添加到 Hadooplib目录。

    $cpavro-1.7.2.jar ${HADOOP_HOME}/lib
    $cpavro-1.7.2.jar ${HADOOP_HOME}/lib
    $cpavro-mapred-1.7.2.jar ${HADOOP_HOME}/lib
    
    

刚刚发生了什么?

设置 Avro 有点复杂;与我们将要使用的其他 Apache 工具相比,它是一个新得多的项目,因此它需要多次下载 tarball。

我们从 Apache 网站下载 avro 和 avro 映射的 JAR 文件。 还有一个对 ParaNamer 的依赖,我们从它的主页http://codehaus.org下载。

备注

在撰写本文时,ParaNamer 主页的下载链接已损坏;作为替代方案,请尝试以下链接:

http://search.maven.org/remotecontent?filepath=com/thoughtworks/paranamer/paranamer/2.5/paranamer-2.5.jar

下载这些 JAR 文件后,我们需要将它们添加到我们的环境使用的类路径中;主要用于 Java 编译器。 我们添加这些文件,但我们还需要将 Hadoop 附带的几个包添加到build类路径中,因为它们是编译和运行 Avro 代码所必需的。

最后,我们将三个新的 JAR 文件复制到集群中每个主机上的 Hadooplib目录中,以使类在运行时可用于 map 和 Reduce 任务。 我们可以通过其他机制分发这些 JAR 文件,但这是最直接的方法。

Avro 和模式

与 Thrift 和 Protocol Buffers 等工具相比,Avro 的一个优势在于它接近描述 Avro 数据文件的模式。 虽然其他工具总是要求模式作为不同的资源可用,但 AVRO 数据文件将模式编码在它们的头中,这允许代码解析文件,而不需要看到单独的模式文件。

Avro 支持(但不需要)生成针对特定数据架构定制的代码的代码生成。 这是一种优化,在可能的情况下是有价值的,但不是必需的。

因此,我们可以编写一系列从未实际使用数据文件模式的 Avro 示例,但我们将只对流程的一部分执行此操作。 在下面的示例中,我们将定义一个模式,该模式表示我们以前使用的 UFO 目击记录的简化版本。

执行操作的时间-定义模式

现在让我们在单个 Avro 模式文件中创建这个简化 UFO 模式。

将以下内容创建为ufo.avsc

{ "type": "record",
  "name": "UFO_Sighting_Record",
  "fields" : [
    {"name": "sighting_date", "type": "string"},
    {"name": "city", "type": "string"},
    {"name": "shape", "type": ["null", "string"]}, 
    {"name": "duration", "type": "float"}
] 
}

刚刚发生了什么?

可以看到,Avro 在其模式中使用 JSON,这些模式通常使用.avsc扩展名保存。 我们在这里为具有四个字段的格式创建模式,如下所示:

  • 字符串类型的SISTETING_DATE字段,用于保存格式为yyyy-mm-dd的日期
  • 字符串类型的City字段,它将包含目击事件发生的城市名称
  • Shape字段,字符串类型的可选字段,表示 UFO 的形状
  • 持续时间字段以分数分钟为单位表示观察持续时间

定义了模式之后,我们现在将创建一些示例数据。

该行动了-使用 Ruby 创建源 Avro 数据

让我们使用 Ruby 创建示例数据,以演示 Avro 的跨语言功能。

  1. 添加rubygems包:

    $ sudo apt-get install rubygems
    
    
  2. 安装 Avro gem:

    $ gem install avro
    
    
  3. 将以下内容创建为generate.rb

    require 'rubygems'
    require 'avro'
    
    file = File.open('sightings.avro', 'wb')
    schema = Avro::Schema.parse(
    File.open("ufo.avsc", "rb").read)
    
    writer = Avro::IO::DatumWriter.new(schema)
    dw = Avro::DataFile::Writer.new(file, writer, schema)
    dw<< {"sighting_date" => "2012-01-12", "city" => "Boston", "shape" => "diamond", "duration" => 3.5}
    dw<< {"sighting_date" => "2011-06-13", "city" => "London", "shape" => "light", "duration" => 13}
    dw<< {"sighting_date" => "1999-12-31", "city" => "New York", "shape" => "light", "duration" => 0.25}
    dw<< {"sighting_date" => "2001-08-23", "city" => "Las Vegas", "shape" => "cylinder", "duration" => 1.2}
    dw<< {"sighting_date" => "1975-11-09", "city" => "Miami", "duration" => 5}
    dw<< {"sighting_date" => "2003-02-27", "city" => "Paris", "shape" => "light", "duration" => 0.5}
    dw<< {"sighting_date" => "2007-04-12", "city" => "Dallas", "shape" => "diamond", "duration" => 3.5}
    dw<< {"sighting_date" => "2009-10-10", "city" => "Milan", "shape" => "formation", "duration" => 0}
    dw<< {"sighting_date" => "2012-04-10", "city" => "Amsterdam", "shape" => "blur", "duration" => 6}
    dw<< {"sighting_date" => "2006-06-15", "city" => "Minneapolis", "shape" => "saucer", "duration" => 0.25}
    dw.close
    
  4. 运行程序并创建数据文件:

    $ ruby generate.rb
    
    

刚刚发生了什么?

在使用 Ruby 之前,我们确保在我们的 Ubuntu 主机上安装了rubygems包。 然后我们安装预先存在的用于 Ruby 的 Avro gem。 这提供了在 Ruby 语言中读写 avro 文件所需的库。

Ruby 脚本本身只是读取前面创建的模式,并创建一个包含 10 条测试记录的数据文件。 然后我们运行该程序来创建数据。

这不是 Ruby 教程,所以我将把 Ruby API 的分析留给读者作为练习;它的文档可以在http://rubygems.org/gems/avro找到。

Java 操作消耗 Avro 数据的时间

现在我们已经有了个 Avro 数据,现在让我们编写一些 Java 代码来使用它:

  1. 将以下内容创建为InputRead.java

    import java.io.File;
    import java.io.IOException;
    
    import org.apache.avro.file.DataFileReader;
    import org.apache.avro.generic.GenericData;
    import org.apache.avro. generic.GenericDatumReader;
    import org.apache.avro.generic.GenericRecord;
    import org.apache.avro.io.DatumReader;
    
    public class InputRead
    {
        public static void main(String[] args) throws IOException
        {
            String filename = args[0] ;
    
            File file=new File(filename) ;
    DatumReader<GenericRecord> reader= new 
    GenericDatumReader<GenericRecord>();
    DataFileReader<GenericRecord>dataFileReader=new 
    DataFileReader<GenericRecord>(file,reader);
    
            while (dataFileReader.hasNext())
            {
    GenericRecord result=dataFileReader.next();
                String output = String.format("%s %s %s %f",
    result.get("sighting_date"), result.get("city"), 
    result.get("shape"), result.get("duration")) ;
    System.out.println(output) ;
            }
        }
    }
    
  2. Compile and run the program:

    $ javacInputRead.java
    $ java InputReadsightings.avro
    
    

    输出将如以下屏幕截图所示:

    Time for action – consuming the Avro data with Java

刚刚发生了什么?

我们创建了 Java 类InputRead,它接受作为命令行参数传递的文件名,并将其解析为 avro 数据文件。 当 Avro 从数据文件中读取时,每个单独的元素被称为数据,并且每个数据将遵循模式中定义的结构。

在本例中,我们不使用显式模式;相反,我们将每个数据读入GenericRecord类,然后通过按名称显式检索每个字段来从中提取每个字段。

GenericRecord类在 Avro 中是一个非常灵活的类;它可以用来包装任何记录结构,比如我们的 UFO 目击类型。 Avro 还支持基元类型(如整数、浮点数和布尔值)以及其他结构化类型(如数组和枚举)。 在这些示例中,我们将使用记录作为最常见的结构,但这只是为了方便起见。

在 MapReduce 中使用 Avro

Avro 对 MapReduce 的支持围绕着其他熟悉类的几个特定于 Avro 的变体,而我们通常期望 Hadoop 通过新的InputFormatOutputFormat类支持新的数据文件格式,我们将使用AvroJobAvroMapperAvroReducer,而不是使用AvroJobAvroMapperAvroReducer,而不是。 AvroJob 希望将 Avro 数据文件作为其输入和输出,因此我们不指定输入和输出格式类型,而是使用输入和输出 Avro 模式的详细信息对其进行配置。

我们的映射器和减少器实现的主要区别在于使用的类型。 默认情况下,avro 只有一个输入和输出,而我们习惯了MapperReducer类有一个键/值输入和一个键/值输出。 Avro 还引入了Pair类,它通常用于发出中间键/值数据。

Avro 还支持AvroKeyAvroValue,它们可以包装其他类型,但我们不会在下面的示例中使用它们。

在 MapReduce 中生成形状摘要的时间

在本节中,我们将编写一个映射器,该映射器接受我们先前定义的 UFO 目击记录作为输入。 它将输出形状和计数1,缩减器将采用此形状和计数记录,并生成一个新的结构化 Avro 数据文件类型,其中包含每个 UFO 形状的最终计数。 执行以下步骤:

  1. sightings.avro文件复制到 HDFS。

    $ hadoopfs -mkdiravroin
    $ hadoopfs -put sightings.avroavroin/sightings.avro
    
    
  2. 将以下内容创建为AvroMR.java

    import java.io.IOException;
    import org.apache.avro.Schema;
    import org.apache.avro.generic.*;
    import org.apache.avro.Schema.Type;
    import org.apache.avro.mapred.*;
    import org.apache.avro.reflect.ReflectData;
    import org.apache.avro.util.Utf8;
    import org.apache.hadoop.conf.*;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.mapred.*;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.io.* ;
    import org.apache.hadoop.util.*;
    
    // Output record definition
    class UFORecord
    {
    UFORecord()
        {
        }
    
        public String shape ;
        public long count ;
    }
    
    public class AvroMR extends Configured  implements Tool
    {
    // Create schema for map output
        public static final Schema PAIR_SCHEMA =         
    Pair.getPairSchema(Schema.create(Schema.Type.STRING), 
    Schema.create(Schema.Type.LONG));
    // Create schema for reduce output
        public final static Schema OUTPUT_SCHEMA = 
    ReflectData.get().getSchema(UFORecord.class);
    
        @Override
        public int run(String[] args) throws Exception
        {
    JobConfconf = new JobConf(getConf(), getClass());
    conf.setJobName("UFO count");
    
            String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
            if (otherArgs.length != 2)
            {
    System.err.println("Usage: avro UFO counter <in><out>");
    System.exit(2);
    
            }
    
    FileInputFormat.addInputPath(conf, new Path(otherArgs[0]));
            Path outputPath = new Path(otherArgs[1]);
    FileOutputFormat.setOutputPath(conf, outputPath);
    outputPath.getFileSystem(conf).delete(outputPath);
            Schema input_schema = 
    Schema.parse(getClass().getResourceAsStream("ufo.avsc"));
    AvroJob.setInputSchema(conf, input_schema);
    AvroJob.setMapOutputSchema(conf,           
    Pair.getPairSchema(Schema.create(Schema.Type.STRING), 
    Schema.create(Schema.Type.LONG)));
    
    AvroJob.setOutputSchema(conf, OUTPUT_SCHEMA);
    AvroJob.setMapperClass(conf, AvroRecordMapper.class);
    AvroJob.setReducerClass(conf, AvroRecordReducer.class);
    conf.setInputFormat(AvroInputFormat.class) ;
    JobClient.runJob(conf);
    
            return 0 ;
        }
    
        public static class AvroRecordMapper extends 
    AvroMapper<GenericRecord, Pair<Utf8, Long>>
        {
            @Override
            public void map(GenericRecord in, AvroCollector<Pair<Utf8, 
    Long>> collector, Reporter reporter) throws IOException
            {
                Pair<Utf8,Long> p = new Pair<Utf8,Long>(PAIR_SCHEMA) ;
    Utf8 shape = (Utf8)in.get("shape") ;
                if (shape != null)
                {
    p.set(shape, 1L) ;
    collector.collect(p);
                }
            }
        }
    
        public static class AvroRecordReducer extends AvroReducer<Utf8, 
    Long, GenericRecord>
        {
            public void reduce(Utf8 key, Iterable<Long> values, 
    AvroCollector<GenericRecord> collector,  
                Reporter reporter) throws IOException
            {
                long sum = 0;
                for (Long val : values)
                {
                    sum += val;
                }
    
    GenericRecord value = new 
    GenericData.Record(OUTPUT_SCHEMA);
    value.put("shape", key);
    value.put("count", sum);
    
    collector.collect(value);
            }
        }
    
        public static void main(String[] args) throws Exception
        {
    int res = ToolRunner.run(new Configuration(), new AvroMR(),
    args);
    System.exit(res);
        } 
    }
    
  3. 编译并运行作业:

    $ javacAvroMR.java
    $ jar -cvfavroufo.jar *.class ufo.avsc
     $ hadoop jar ~/classes/avroufo.jarAvroMRavroinavroout
    
    
  4. 检查输出目录:

    $ hadoopfs -lsavroout
    Found 3 items
    -rw-r--r--   1 … /user/hadoop/avroout/_SUCCESS
    drwxr-xr-x   - hadoopsupergroup          0 … /user/hadoop/avroout/_logs
    -rw-r--r--   1 …  /user/hadoop/avroout/part-00000.avro
    
    
  5. 将输出文件复制到本地文件系统:

    $ hadoopfs -get /user/hadoop/avroout/part-00000.avroresult.avro
    
    

刚刚发生了什么?

我们创建了Job类并检查了它的各种组件。 MapperReducer类中的实际逻辑相对简单:Mapper类只提取 Shape 列并发出计数为1的列;然后减法器计算每个 Shape 的条目总数。 有趣的方面是定义了MapperReducer类的输入和输出类型,以及作业是如何配置的。

Mapper类的输入类型为GenericRecord,输出类型为PairReducer类具有对应的输入类型Pair和输出类型GenericRecord

传递给Mapper类的GenericRecord类包装了一个数据,该数据是输入文件中表示的 UFO 目击记录。 这就是Mapper类能够按名称检索Shape字段的方式。

回想一下,GenericRecords可能是使用模式显式创建的,也可能不是使用模式显式创建的,在任何一种情况下,结构都可以从数据文件中确定。 对于Reducer类的GenericRecord输出,我们确实传递了一个模式,但使用了一种新的机制来创建它。

在前面提到的代码中,我们创建了额外的UFORecord类,并使用 Avro 反射在运行时动态生成其模式。 然后,我们可以使用此模式创建专门包装该特定记录类型的GenericRecord类。

MapperReducer类之间,我们使用 avroPair类型来保存键和值对。 这允许我们为MapperReducer类表达与我们在第 2 章启动并运行 Hadoop中的原始 wordcount 示例中使用的相同逻辑;Mapper 类为每个值发出单个计数,减法器将这些计数相加为每个形状的总计数。

除了MapperReducer类的输入和输出之外,还有一些处理 Avro 数据的作业所特有的配置:

Schema input_schema = Schema.parse(getClass().getResourceAsStream("ufo.avsc")) ;
AvroJob.setInputSchema(conf, input_schema);
AvroJob.setMapOutputSchema(conf,           Pair.getPairSchema(Schema.create(Schema.Type.STRING), Schema.create(Schema.Type.LONG)));

AvroJob.setOutputSchema(conf, OUTPUT_SCHEMA);
AvroJob.setMapperClass(conf, AvroRecordMapper.class);
AvroJob.setReducerClass(conf, AvroRecordReducer.class);

这些配置元素向 Avro 演示了模式定义的重要性;尽管我们可以没有它,但我们必须设置预期的输入和输出模式类型。 Avro 将根据指定的架构验证输入和输出,因此存在一定程度的数据类型安全。 对于其他元素,比如设置MapperReducer类,我们只需在 AvroJob 上设置这些元素,而不是设置更通用的类,一旦完成,MapReduce 框架就会正常运行。

这个示例也是我们第一次显式实现Tool接口。 在运行 Hadoop 命令行程序时,有一系列参数(如-D)在所有多个子命令中是通用的。 如果作业类实现了上一节提到的Tool接口,它将自动访问在命令行上传递的这些标准选项中的任何一个。 这是一种防止大量代码重复的有用机制。

该行动了-用 Ruby 检查输出数据

现在我们已经有了作业的输出数据,让我们使用 Ruby 再次检查它。

  1. 将以下内容创建为read.rb

    require 'rubygems'
    require 'avro'
    
    file = File.open('res.avro', 'rb')
    reader = Avro::IO::DatumReader.new()
    dr = Avro::DataFile::Reader.new(file, reader)
    
    dr.each {|record|  
    print record["shape"]," ",record["count"],"\n"
    }
    dr.close
    
  2. 检查创建的结果文件。

    $ ruby read.rb
    blur 1
    cylinder 1
    diamond 2
    formation 1
    light 3
    saucer 1
    
    

刚刚发生了什么?

和以前一样,我们不会分析 Ruby Avro API。 该示例创建了一个 Ruby 脚本,该脚本打开一个 avro 数据文件,遍历每个数据,并根据显式命名的字段显示它。 请注意,该脚本不能访问数据文件的架构;标题中的信息提供了足够的数据来检索每个字段。

行动时间-用 Java 检查输出数据

为了说明可以从多种语言访问数据,我们还使用 Java 显示作业输出。

  1. 将以下内容创建为OutputRead.java

    import java.io.File;
    import java.io.IOException;
    
    import org.apache.avro.file.DataFileReader;
    import org.apache.avro.generic.GenericData;
    import org.apache.avro. generic.GenericDatumReader;
    import org.apache.avro.generic.GenericRecord;
    import org.apache.avro.io.DatumReader;
    
    public class OutputRead
    {
        public static void main(String[] args) throws IOException
        {
            String filename = args[0] ;
    
            File file=new File(filename) ;
    DatumReader<GenericRecord> reader= new 
    GenericDatumReader<GenericRecord>();
    DataFileReader<GenericRecord>dataFileReader=new 
    DataFileReader<GenericRecord>(file,reader);
    
            while (dataFileReader.hasNext())
            {
    GenericRecord result=dataFileReader.next();
                String output = String.format("%s %d",
    result.get("shape"), result.get("count")) ;
    System.out.println(output) ;
            }
        }
    }
    
  2. 编译并运行程序:

    $ javacOutputResult.java
    $ java OutputResultresult.avro
    blur 1
    cylinder 1
    diamond 2
    formation 1
    light 3
    saucer 1
    
    

刚刚发生了什么?

我们添加了这个示例,以显示多种语言正在读取的 Avro 数据。 代码与前面的InputRead类非常相似;唯一的区别是命名字段用于显示从数据文件中读取的每个数据。

在 Avro 中玩一玩英雄图

正如前面提到的,我们努力在我们的GraphPath类中降低与表示相关的复杂性。 但是,对于与文本和对象的平面行之间的映射,管理这些转换需要额外的开销。

凭借对嵌套复杂类型的支持,Avro 可以本机支持更接近运行时对象的节点表示。 修改GraphPath类作业,以读取图形表示并将其写入由每个节点的数据组成的 Avro 数据文件。 下面的示例架构可能是一个很好的起点,但您可以随时对其进行改进:

{ "type": "record",
  "name": "Graph_representation",
  "fields" : [
{"name": "node_id", "type": "int"},
    {"name": "neighbors", "type": "array", "items:"int" },
    {"name": "distance", "type": "int"},
  {"name": "status", "type": "enum", 
"symbols": ["PENDING", "CURRENT", "DONE"
},]
] 
}

Avro 继续前进

我们在本案例研究中没有介绍 Avro 的许多特性。 我们只关注它作为静态数据表示的价值。 它还可以在远程过程调用(RPC)框架中使用,并且可以选择性地用作 Hadoop2.0 中的默认 RPC 格式。 我们没有使用 Avro 的代码生成工具来生成更加专注于领域的 API。 我们也没有讨论诸如 Avro 支持模式演变的能力等问题,例如,允许在不使旧数据无效或中断现有客户端的情况下将新字段添加到最近的记录中。 这是一项你很有可能在未来看到更多的技术。

摘要

本章使用了三个案例研究来突出 Hadoop 及其更广泛的生态系统的一些更高级的方面。 特别地,我们介绍了联接类型问题的性质及其出现的位置,如何相对轻松地实现减少端联接但会降低效率,以及如何通过将数据推送到分布式缓存来使用优化来避免映射端的完全联接。

然后,我们了解了如何实现完整的映射端联接,但需要大量的输入数据处理;如果联接是经常遇到的用例,应该如何研究其他工具(如配置单元和 Pig);以及如何考虑图形等复杂类型,以及如何以可以在 MapReduce 中使用的方式表示它们。

我们还了解了将图形算法分解为多阶段 MapReduce 作业的技术、独立于语言的数据类型的重要性、如何将 Avro 用于语言独立以及复杂的 Java 使用的类型,以及对 MapReduce API 的 Avro 扩展(允许将结构化类型用作 MapReduce 作业的输入和输出)。

现在,我们对 Hadoop MapReduce 框架的编程方面的介绍到此为止。 我们现在将在接下来的两章中继续探索如何管理和扩展 Hadoop 环境。

六、当事情崩溃时

Hadoop 的主要承诺之一是对失败的恢复能力,以及在失败发生时能够幸免于难的能力。 容忍失败将是本章的重点。

我们将特别介绍以下主题:

  • Hadoop 如何处理 DataNodes 和 TaskTracker 的故障
  • Hadoop 如何处理 NameNode 和 JobTracker 的故障
  • 硬件故障对 Hadoop 的影响
  • 如何处理由软件错误导致的任务失败
  • 脏数据如何导致任务失败以及如何处理

在此过程中,我们将加深对 Hadoop 各个组件如何组合在一起的理解,并确定一些最佳实践领域。

故障

对于许多技术,在出现问题时要采取的步骤在很多文档中很少涉及,而且通常只被视为专家感兴趣的主题。 有了 Hadoop,它变得更加突出;Hadoop 的大部分架构和设计都基于在故障频繁且意料之中的环境中执行。

拥抱失败

近年来,一种与传统心态不同的心态被称为拥抱失败。 与其希望失败不会发生,不如接受这样的事实:失败会发生,并且知道当失败发生时,您的系统和进程将如何响应。

或者至少不要害怕

这可能有点牵强,因此,我们在本章的目标是让您对系统中的故障感到更舒服。 我们将杀死正在运行的集群的进程,故意导致软件失败,将坏数据推入我们的作业,通常会造成尽可能多的中断。

不要在家里尝试这个

通常,当试图破坏系统时,测试实例会被滥用,从而使操作系统不受中断的影响。 我们不主张对可操作的 Hadoop 集群执行本章中给出的操作,但事实是,除了一两个非常具体的情况外,您可以这样做。 我们的目标是了解各种类型的故障的影响,以便当它们确实发生在业务关键型系统上时,您将知道它是否是一个问题。 幸运的是,大多数案例都是由 Hadoop 为您处理的。

故障类型

我们将故障大致分为以下五种类型:

  • 节点出现故障,即 DataNode 或 TaskTracker 进程
  • 群集的主进程(即 NameNode 或 JobTracker 进程)出现故障
  • 硬件故障,即主机崩溃、硬盘故障等
  • 由于软件错误,MapReduce 作业中的单个任务失败
  • MapReduce 作业中的单个任务因数据问题而失败

在接下来的部分中,我们将依次探讨其中的每一个。

Hadoop 节点故障

我们将探讨的第一类故障是单个 DataNode 和 TaskTracker 进程的意外终止。 考虑到 Hadoop 声称通过其商用硬件的故障生存来管理系统可用性,我们可以预期这一领域将非常稳固。 事实上,随着集群发展到成百上千台主机,单个节点的故障可能会变得相当常见。

在我们开始杀戮之前,让我们先介绍一个新工具并正确设置集群。

dfsadmin 命令

作为经常查看 HDFS Web 用户界面以确定群集状态的替代工具,我们将使用dfsadmin命令行工具:

$ Hadoop dfsadmin 

这将给出该命令可以采用的各种选项的列表;出于我们的目的,我们将使用-report选项。 这提供了整个群集状态的概述,包括已配置的容量、节点和文件,以及有关每个已配置节点的具体详细信息。

群集设置、测试文件和块大小

以下活动需要一个完全分布式集群;请参阅本书前面给出的设置说明。 下面的屏幕截图和示例使用一个集群,其中一个主机用于 JobTracker 和 NameNode,四个从节点用于运行 DataNode 和 TaskTracker 进程。

提示

请记住,您不需要为每个节点配备物理硬件,我们的群集使用虚拟机。

在正常使用中,64 MB 是 Hadoop 群集通常配置的块大小。 出于我们的测试目的,这非常不方便,因为我们需要相当大的文件才能在我们的多节点集群中获得有意义的块计数。

我们可以做的是减小配置的块大小;在本例中,我们将使用 4MB。 对 Hadoopconf目录中的hdfs-site.xml文件进行以下修改:

<property>
<name>dfs.block.size</name>
<value>4194304</value>
;</property>
<property>
<name>dfs.namenode.logging.level</name>
<value>all</value>
</property>

第一个属性对块大小进行必要的更改,第二个属性增加 NameNode 日志记录级别,以使某些块操作更可见。

备注

这两个设置都适用于此测试设置,但在生产集群上很少看到。 尽管如果正在调查一个特别困难的问题,可能需要更高的 NameNode 日志记录,但您不太可能想要一个小到 4MB 的块大小。 虽然较小的块大小可以很好地工作,但它会影响 Hadoop 的效率。

我们还需要一个大小合理的测试文件,它将由多个 4MB 的块组成。 我们实际上不会使用文件的内容,因此文件类型无关紧要。 但是,对于以下部分,您应该尽可能将最大的文件复制到 HDFS 上。 我们使用的是 CD ISO 映像:

$ Hadoop fs –put cd.iso file1.data

容错和弹性 MapReduce

本书中的示例是针对本地 Hadoop 集群的,因为这样可以使一些故障模式细节更加明确。 EMR 提供与本地集群完全相同的容错能力,因此这里描述的故障场景同样适用于本地 Hadoop 集群和 EMR 托管的集群。

操作时间-终止 DataNode 进程

首先,我们将终止一个 DataNode。 回想一下,DataNode 进程在 HDFS 集群中的每台主机上运行,负责管理 HDFS 文件系统中的块。 由于 Hadoop 在默认情况下对数据块使用复制系数 3,因此我们预计单个 DataNode 故障不会对可用性产生直接影响,而是会导致某些数据块暂时低于复制阈值。 执行以下步骤以终止 DataNode 进程:

  1. Firstly, check on the original status of the cluster and check whether everything is healthy. We'll use the dfsadmin command for this:

    $ Hadoop dfsadmin -report
    Configured Capacity: 81376493568 (75.79 GB)
    Present Capacity: 61117323920 (56.92 GB)
    DFS Remaining: 59576766464 (55.49 GB)
    DFS Used: 1540557456 (1.43 GB)
    DFS Used%: 2.52%
    Under replicated blocks: 0
    Blocks with corrupt replicas: 0
    Missing blocks: 0
    -------------------------------------------------
    Datanodes available: 4 (4 total, 0 dead)
    Name: 10.0.0.102:50010
    Decommission Status : Normal
    Configured Capacity: 20344123392 (18.95 GB)
    DFS Used: 403606906 (384.91 MB)
    Non DFS Used: 5063119494 (4.72 GB)
    DFS Remaining: 14877396992(13.86 GB)
    DFS Used%: 1.98%
    DFS Remaining%: 73.13%
    Last contact: Sun Dec 04 15:16:27 PST 2011
    …
    
    

    现在登录到个节点,并使用jps命令确定 DataNode 进程的进程 ID:

    $ jps
    2085 TaskTracker
    2109 Jps
    1928 DataNode
    
    
  2. 使用 DataNode 进程的进程 ID(PID)并终止它:

    $ kill -9  1928
    
    
  3. 检查主机

    $ jps
    2085 TaskTracker
    
    

    上是否不再运行 DataNode 进程

  4. 使用dfsadmin命令再次检查群集的状态:

    $ Hadoop dfsadmin -report
    Configured Capacity: 81376493568 (75.79 GB)
    Present Capacity: 61117323920 (56.92 GB)
    DFS Remaining: 59576766464 (55.49 GB)
    DFS Used: 1540557456 (1.43 GB)
    DFS Used%: 2.52%
    Under replicated blocks: 0
    Blocks with corrupt replicas: 0
    Missing blocks: 0
    -------------------------------------------------
    Datanodes available: 4 (4 total, 0 dead)
    …
    
    
  5. 要关注的关键线路是报告块、活动节点和每个节点的最后联系时间的线路。 一旦死节点的最后联系时间约为 10 分钟,请更频繁地使用该命令,直到块和活动节点值更改:

    $ Hadoop dfsadmin -report
    Configured Capacity: 61032370176 (56.84 GB)
    Present Capacity: 46030327050 (42.87 GB)
    DFS Remaining: 44520288256 (41.46 GB)
    DFS Used: 1510038794 (1.41 GB)
    DFS Used%: 3.28%
    Under replicated blocks: 12
    Blocks with corrupt replicas: 0
    Missing blocks: 0
    -------------------------------------------------
    Datanodes available: 3 (4 total, 1 dead)
    …
    
    
  6. 重复该过程,直到复制不足的数据块计数再次为0

    $ Hadoop dfsadmin -report
    …
    Under replicated blocks: 0
    Blocks with corrupt replicas: 0
    Missing blocks: 0
    -------------------------------------------------
    Datanodes available: 3 (4 total, 1 dead)
    …
    
    

刚刚发生了什么?

高层的描述非常简单;Hadoop 认识到节点丢失,并解决了这个问题。 然而,为了实现这一目标,还有相当多的工作要做。

当我们终止 DataNode 进程时,作为读/写操作的一部分,该主机上的进程不再可用于服务或接收数据块。 然而,我们当时实际上并没有访问文件系统,那么 NameNode 进程是如何知道这个特定的 DataNode 是死的呢?

NameNode 和 DataNode 通信

答案在于 NameNode 和 DataNode 进程之间的持续通信,我们已经提到过一两次,但从未真正解释过。 这是通过来自 DataNode 的一系列持续不断的心跳消息来实现的,这些消息报告了它的当前状态和它持有的块。 作为回报,NameNode 向 DataNode 发出指令,例如创建新文件的通知或从另一个节点检索块的指令。

这一切都是在 NameNode 进程启动并开始从 DataNode 接收状态消息时开始的。 回想一下,每个 DataNode 都知道其 NameNode 的位置,并将持续发送状态报告。 这些消息列出了每个 DataNode 保存的块,NameNode 可以由此构建一个完整的映射,使其能够将文件和目录与组成它们的块以及存储它们的节点相关联。

NameNode 进程监视它最后一次从每个 DataNode 接收心跳的时间,在达到阈值之后,它会假定 DataNode 不再起作用,并将其标记为失效。

备注

认为 DataNode 已死的确切阈值不能配置为单个 HDFS 属性。 相反,它是根据其他几个属性(如定义心跳间隔)计算得出的。 正如我们稍后将看到的,在 MapReduce 世界中事情会稍微简单一些,因为 TaskTracker 的超时是由单个配置属性控制的。

一旦某个 DataNode 被标记为失效,NameNode 进程就会确定该节点上保留的、现在已低于其复制目标的块。 在默认情况下,停用节点上保存的每个数据块将是三个复制副本之一,因此该节点保存其复制副本的每个数据块现在在群集上只有两个复制副本。

在前面的示例中,我们捕获了 12 个数据块仍未充分复制时的状态,即它们在整个群集中没有足够的复制副本来满足复制目标。 当 NameNode 进程确定复制不足的数据块时,它会分配其他 DataNode 从现有复制副本所在的主机复制这些数据块。 在这种情况下,我们只需重新复制数量非常少的数据块;在实时群集中,当受影响的数据块达到其复制系数时,节点故障可能会导致一段时间的高网络流量。

请注意,如果故障节点返回到群集,我们会遇到数据块的副本数量超过所需的个的情况;在这种情况下,NameNode 进程将发送指令以删除多余的副本。 要删除的特定副本是随机选择的,因此结果将是返回的节点最终将保留其某些块并删除其他块。

有一个围棋英雄-NameNode 日志挖掘

我们将 NameNode 进程配置为记录其所有活动。 查看这些非常详细的日志,并尝试识别正在发送的复制请求。

最终输出显示复制不足的数据块复制到活动节点后的状态。 群集仅剩三个活动节点,但没有复制不足的数据块。

提示

重新启动所有主机上的失效节点的快速方法是使用start-all.sh脚本。 它将尝试启动所有内容,但它足够智能,可以检测正在运行的服务,这意味着您可以重新启动失效的节点,而不会有重复的风险。

行动时间-行动中的复制因素

让我们重复前面的过程,但这一次,从我们的四个集群中删除两个 DataNode。 我们将简要演练本练习,因为它与前面的操作时间部分非常相似:

  1. 重新启动失效的 DataNode 并监视群集,直到所有节点都标记为活动。

  2. 选择两个 DataNode,使用进程 ID,然后终止 DataNode 进程。

  3. 如前所述,等待大约 10 分钟,然后通过dfsadmin主动监视群集状态,特别注意报告的复制不足数据块数量。

  4. 等待群集稳定下来,并产生类似于以下内容的输出:

    Configured Capacity: 61032370176 (56.84 GB)
    Present Capacity: 45842373555 (42.69 GB)
    DFS Remaining: 44294680576 (41.25 GB)
    DFS Used: 1547692979 (1.44 GB)
    DFS Used%: 3.38%
    Under replicated blocks: 125
    Blocks with corrupt replicas: 0
    Missing blocks: 0
    -------------------------------------------------
    Datanodes available: 2 (4 total, 2 dead)
    …
    
    

刚刚发生了什么?

这与以前的过程相同;不同之处在于,由于两个 DataNode 故障,明显有更多数据块低于复制因子,其中许多数据块只剩下一个剩余的副本。 因此,您应该会看到,报告的复制不足数据块数量在最初增加时会有更多活动,因为节点会出现故障,然后随着重新复制的发生而下降。 这些事件也可以在 NameNode 日志中看到。

请注意,尽管 Hadoop 可以使用重新复制将只有一个剩余副本的数据块增加到两个副本,但这仍会使数据块处于复制不足状态。 由于群集中只有两个活动节点,现在任何数据块都不可能满足默认复制目标 3 个。

由于空间原因,我们一直在截断dfsadmin输出;特别是,我们一直在省略每个节点的报告信息。 但是,让我们通过前面的阶段来看一下集群中的第一个节点。 在我们开始终止任何 DataNode 之前,它报告了以下内容:

Name: 10.0.0.101:50010
Decommission Status : Normal
Configured Capacity: 20344123392 (18.95 GB)
DFS Used: 399379827 (380.88 MB)
Non DFS Used: 5064258189 (4.72 GB)
DFS Remaining: 14880485376(13.86 GB)
DFS Used%: 1.96%
DFS Remaining%: 73.14%
Last contact: Sun Dec 04 15:16:27 PST 2011

在单个 DataNode 被终止并根据需要重新复制所有数据块后,它报告了以下信息:

Name: 10.0.0.101:50010
Decommission Status : Normal
Configured Capacity: 20344123392 (18.95 GB)
DFS Used: 515236022 (491.37 MB)
Non DFS Used: 5016289098 (4.67 GB)
DFS Remaining: 14812598272(13.8 GB)
DFS Used%: 2.53%
DFS Remaining%: 72.81%
Last contact: Sun Dec 04 15:31:22 PST 2011

需要注意的是,节点上的本地 DFS 存储增加了。 这不应该令人惊讶。 对于死节点,群集中的其他节点需要添加一些额外的数据块副本,这将转化为每个数据块上更高的存储利用率。

最后,以下是其他两个 DataNode 被终止后该节点的报告:

Name: 10.0.0.101:50010
Decommission Status : Normal
Configured Capacity: 20344123392 (18.95 GB)
DFS Used: 514289664 (490.46 MB)
Non DFS Used: 5063868416 (4.72 GB)
DFS Remaining: 14765965312(13.75 GB)
DFS Used%: 2.53%
DFS Remaining%: 72.58%
Last contact: Sun Dec 04 15:43:47 PST 2011

有了两个死节点,剩余的活动节点似乎应该消耗更多的本地存储空间,但事实并非如此,这又是复制因素的自然结果。

如果我们有四个节点,复制系数为 3,则每个数据块在群集中的三个活动节点上都有一个副本。 如果一个节点死了,驻留在其他节点上的数据块不会受到影响,但是在死节点上有副本的任何块都需要创建一个新的副本。 但是,由于只有三个活动节点,每个节点将保存每个数据块的副本。 如果第二个节点出现故障,这种情况将导致数据块复制不足,Hadoop 没有地方放置额外的副本。 由于剩余的两个节点都已拥有每个数据块的副本,因此它们的存储利用率不会增加。

采取行动的时间-故意导致块丢失

下一步应该很明显;让我们快速连续地杀死三个 DataNode。

提示

这是我们提到的第一个活动,您确实不应该在生产集群上执行这些活动。 虽然如果正确遵循这些步骤不会丢失数据,但有一段时间现有数据不可用。

以下是快速连续终止三个 DataNode 的步骤:

  1. 使用以下命令重新启动所有节点:

    $ start-all.sh
    
    
  2. 等待 Hadoopdfsadmin -report显示四个活动节点。

  3. 将测试文件的新副本放到 HDFS 上:

    $ Hadoop fs -put file1.data file1.new
    
    
  4. 登录到其中三台群集主机,并终止每台主机上的 DataNode 进程。

  5. 等待通常的 10 分钟,然后通过dfsadmin开始监视群集,直到您得到类似以下报告丢失数据块的输出:

    …
    Under replicated blocks: 123
    Blocks with corrupt replicas: 0
    Missing blocks: 33
    -------------------------------------------------
    Datanodes available: 1 (4 total, 3 dead)
    …
    
    
  6. 尝试从 HDFS 检索测试文件:

    $ hadoop fs -get file1.new  file1.new
    11/12/04 16:18:05 INFO hdfs.DFSClient: No node available for block: blk_1691554429626293399_1003 file=/user/hadoop/file1.new
    11/12/04 16:18:05 INFO hdfs.DFSClient: Could not obtain block blk_1691554429626293399_1003 from any node:  java.io.IOException: No live nodes contain current block
    …
    get: Could not obtain block: blk_1691554429626293399_1003 file=/user/hadoop/file1.new
    
    
  7. 使用start-all.sh脚本重新启动失效节点:

    $ start-all.sh
    
    
  8. 重复监视块的状态:

    $ Hadoop dfsadmin -report | grep -i blocks
    Under replicated blockss: 69
    Blocks with corrupt replicas: 0
    Missing blocks: 35
    $ Hadoop dfsadmin -report | grep -i blocks
    Under replicated blockss: 0
    Blocks with corrupt replicas: 0
    Missing blocks: 30
    
    
  9. 等待,直到没有报告丢失块,然后将测试文件复制到本地文件系统:

    $ Hadoop fs -get file1.new file1.new
    
    
  10. 对此文件和原始文件执行 MD5 检查:

```scala
$ md5sum file1.*
f1f30b26b40f8302150bc2a494c1961d  file1.data
f1f30b26b40f8302150bc2a494c1961d  file1.new

```

刚刚发生了什么?

在重新启动被杀死的节点之后,我们再次将测试文件复制到 HDFS 上。 这并不是绝对必要的,因为我们可以使用现有的文件,但由于副本的混洗,干净的副本会给出最具代表性的结果。

然后,我们像以前一样杀死了三个个 DataNode,并等待 HDFS 响应。 与前面的示例不同,杀死这些节点意味着可以肯定的是,某些块将在被杀死的节点上拥有它们的所有副本。 正如我们所看到的,这正是结果;剩余的单节点群集显示了 100 多个复制不足的数据块(显然只剩下一个复制副本),但也有 33 个数据块丢失。

谈论块有点抽象,所以我们尝试检索我们的测试文件,我们知道,该文件实际上有 33 个洞。 尝试访问该文件失败,因为 Hadoop 找不到传送该文件所需的丢失数据块。

然后,我们重新启动所有节点,并再次尝试检索该文件。 这一次成功了,但我们采取了额外的预防措施,对文件执行 MD5 密码检查,以确认它与原始文件按位相同-确实如此。

这一点很重要:虽然节点故障可能会导致数据不可用,但如果节点恢复,可能不会永久丢失数据。

数据可能丢失的时间

不要从这个例子中假定在 Hadoop 集群中不可能丢失数据。 一般情况下,这是非常困难的,但灾难往往有以错误的方式袭击的习惯。

如上例所示,多个节点的并行故障等于或大于复制系数,有可能导致块丢失。 在我们的示例中,四个集群中有三个死节点,可能性很高;在 1000 个集群中,这个几率要低得多,但仍然不是零。 随着群集大小的增加,故障率也会增加,在狭窄的时间窗口内出现三个节点故障的可能性越来越小。 相反,影响也会降低,但快速的多个故障总是会带来数据丢失的风险。

另一个更隐蔽的问题是反复出现或部分故障,例如,当整个群集的电源问题导致节点崩溃和重启时。 Hadoop 最终可能会追逐复制目标,不断要求恢复的主机复制复制不足的数据块,还可能会看到它们在任务中途失败。 这样的一系列事件还可能增加数据丢失的可能性。

最后,不要忘记人的因素。 当用户意外删除文件或目录时,让复制系数等于群集的大小(确保每个数据块都在每个节点上)对您没有帮助。

总结说,由于系统故障而丢失数据的可能性很小,但通过几乎不可避免的人为操作是可能的。 复制不是备份的完全替代方案;请确保您了解所处理数据的重要性以及此处讨论的丢失类型的影响。

备注

Hadoop 集群中最灾难性的损失实际上是由 NameNode 故障和文件系统损坏造成的;我们将在下一章详细讨论这个主题。

阻止损坏

来自每个 DataNode 的报告还包括损坏块的计数,我们没有提到这一点。 首次存储数据块时,还会有一个隐藏文件写入同一 HDFS 目录,其中包含该数据块的加密校验和。 默认情况下,块内的每个 512 字节区块都有一个校验和。

每当任何客户端读取数据块时,它都会检索校验和列表,并将这些校验和与它对已读取的块数据生成的校验和进行比较。 如果校验和不匹配,则该特定 DataNode 上的数据块将被标记为损坏,并且客户端将检索不同的复制副本。 在获知损坏的数据块后,NameNode 将计划从现有的未损坏的副本之一制作新的副本。

如果这种情况似乎不太可能发生,请考虑单个主机上的故障内存、磁盘驱动器、存储控制器或许多问题可能会导致块在存储或读取时最初被写入时损坏。 这些都是罕见事件,在持有同一数据块副本的所有 DataNode 上发生相同损坏的可能性变得异常渺茫。 但是,请记住,如前所述,复制不是备份的完全替代方案,如果您需要 100%的数据可用性,则可能需要考虑群集外备份。

操作时间-终止 TaskTracker 进程

我们已经充分滥用了 HDFS 及其 DataNode;现在让我们看看杀死一些 TaskTracker 进程会对 MapReduce 造成什么损害。

虽然有一个mradmin命令,但它不会给出我们在 HDFS 中习惯的那种状态报告。 因此,我们将使用 MapReduce web UI(默认情况下位于 JobTracker 主机的端口 50070 上)来监控 MapReduce 集群的运行状况。

执行以下步骤:

  1. Ensure everything is running via the start-all.sh script then point your browser at the MapReduce web UI. The page should look like the following screenshot:

    Time for action – killing a TaskTracker process

  2. 启动一个长期运行的 MapReduce 作业;具有较大值的示例 pi 估计器非常适用于此:

    $ Hadoop jar Hadoop/Hadoop-examples-1.0.4.jar pi 2500 2500
    
    
  3. 现在登录到群集节点并使用jps标识 TaskTracker 进程:

    $ jps
    21822 TaskTracker
    3918 Jps
    3891 DataNode
    
    
  4. 终止 TaskTracker 进程:

    $ kill -9 21822
    
    
  5. 验证 TaskTracker 是否不再运行:

    $jps
    3918 Jps
    3891 DataNode
    
    
  6. Go back to the MapReduce web UI and after 10 minutes you should see that the number of nodes and available map/reduce slots change as shown in the following screenshot:

    Time for action – killing a TaskTracker process

  7. 在原始窗口中监视作业进度;作业应该正在进行,即使速度很慢。

  8. 重新启动死的 TaskTracker 进程:

    $ start-all.sh
    
    
  9. Monitor the MapReduce web UI. After a little time the number of nodes should be back to its original number as shown in the following screenshot:

    Time for action – killing a TaskTracker process

刚刚发生了什么?

MapReduce Web 界面提供了大量关于集群及其执行的作业的信息。 对于我们这里的兴趣而言,重要的数据是集群摘要,它显示了当前正在执行的映射和减少任务的数量、提交的作业总数、节点数量及其映射和减少的容量,最后是任何列入黑名单的节点。

JobTracker 进程与 TaskTracker 进程的关系与 NameNode 和 DataNode 之间的关系大不相同,但使用了类似的心跳/监视机制。

TaskTracker 进程经常向 JobTracker 发送心跳信号,但它们包含分配的任务和可用容量的进度报告,而不是数据块运行状况的状态报告。 每个节点都有可配置数量的映射和还原任务槽(每个任务槽的默认值是两个),这就是为什么我们在第一个 Web UI 屏幕截图中看到四个节点和八个映射和还原任务槽的原因。

当我们终止 TaskTracker 进程时,它的心跳不足由 JobTracker 进程测量,在可配置的时间后,节点被假定为死机,我们看到 Web 用户界面中反映的群集容量减少。

提示

TaskTracker 进程被视为已死的超时由在mapred-site.xml中配置的mapred.tasktracker.expiry.interval属性修改。

当 TaskTracker 进程被标记为失效时,JobTracker 进程还会将其正在进行的任务视为失败,并将其重新分配给集群中的其他节点。 我们通过观察作业在节点被杀死的情况下成功进行来隐含地看到这一点。

重新启动 TaskTracker 进程后,它会向 JobTracker 发送心跳,JobTracker 将其标记为活动状态,并将其重新集成到 MapReduce 集群中。 我们可以从集群节点和任务槽容量恢复到它们的原始值看到这一点,如最后的屏幕截图所示。

比较 DataNode 和 TaskTracker 故障

我们不会使用 TaskTracker 执行类似的两三个节点终止活动,因为任务执行架构使得单个 TaskTracker 故障相对不重要。 由于 TaskTracker 进程处于 JobTracker 的控制和协调之下,因此它们各自的故障除了降低集群执行能力外,不会产生任何直接影响。 如果 TaskTracker 实例失败,JobTracker 将简单地将失败的任务调度到集群中健康的 TaskTracker 进程上。 JobTracker 可以自由地重新调度群集周围的任务,因为 TaskTracker 在概念上是无状态的;单个故障不会影响作业的其他部分。

相比之下,丢失 DataNode(本质上是有状态的)可能会影响 HDFS 上保存的持久数据,从而可能使其不可用。

这突出了各种节点的性质以及它们与整个 Hadoop 框架的关系。 DataNode 管理数据,而 TaskTracker 读取和写入数据。 每个 TaskTracker 的灾难性故障仍然会给我们留下一个功能完全正常的 HDFS;类似的 NameNode 进程故障会留下一个有效无用的活动 MapReduce 集群(除非它被配置为使用不同的存储系统)。

永久性故障

到目前为止,我们的恢复场景假定故障节点可以在同一物理主机上重新启动。 但是,如果由于主机出现严重故障而无法恢复,该怎么办? 答案很简单;您可以从从服务器的文件中删除主机,Hadoop 将不再尝试在该主机上启动 DataNode 或 TaskTracker。 相反,如果您获得了具有不同主机名的替换计算机,请将此新主机添加到同一文件中并运行start-all.sh

备注

请注意,从文件仅由start/stopslaves.sh脚本等工具使用。 您不需要在每个节点上都保持更新,只需要在通常运行此类命令的主机上保持更新即可。 实际上,这可能是一个专用的头节点或运行 NameNode 或 JobTracker 进程的主机。 我们将在第 7 章中探讨这些设置

杀死群集主机

虽然 DataNode 和 TaskTracker 进程的故障影响不同,但每个单独的节点都相对不重要。 任何一个 TaskTracker 或 DataNode 的故障都不值得担心,只有当多个其他 TaskTracker 或 DataNode 失败时才会出现问题,特别是在快速接连失败的情况下。 但是我们只有一个 JobTracker 和 NameNode;让我们来看看当它们失败时会发生什么。

该行动了-干掉 JobTracker

我们将首先终止 JobTracker 进程,这应该会影响我们执行 MapReduce 作业的能力,但不会影响底层的 HDFS 文件系统。

  1. 登录到 JobTracker 主机并终止其进程。

  2. 尝试启动测试 MapReduce 作业,如 PI 或 Wordcount:

    $ Hadoop jar wc.jar WordCount3 test.txt output
    Starting Job
    11/12/11 16:03:29 INFO ipc.Client: Retrying connect to server: /10.0.0.100:9001\. Already tried 0 time(s).
    11/12/11 16:03:30 INFO ipc.Client: Retrying connect to server: /10.0.0.100:9001\. Already tried 1 time(s).
    …
    11/12/11 16:03:38 INFO ipc.Client: Retrying connect to server: /10.0.0.100:9001\. Already tried 9 time(s).
    java.net.ConnectException: Call to /10.0.0.100:9001 failed on connection exception: java.net.ConnectException: Connection refused
     at org.apache.hadoop.ipc.Client.wrapException(Client.java:767)
     at org.apache.hadoop.ipc.Client.call(Client.java:743)
     at org.apache.hadoop.ipc.RPC$Invoker.invoke(RPC.java:220)
    …
    
    
  3. 执行一些 HDFS 操作:

    $ hadoop fs -ls /
    Found 2 items
    drwxr-xr-x   - hadoop supergroup          0 2011-12-11 19:19 /user
    drwxr-xr-x   - hadoop supergroup          0 2011-12-04 20:38 /var
    $ hadoop fs -cat test.txt
    This is a test file
    
    

刚刚发生了什么?

在终止 JobTracker 进程之后,我们尝试启动 MapReduce 作业。 从第 2 章启动并运行 Hadoop的演练中,我们知道启动作业的机器上的客户端尝试与 JobTracker 进程通信以启动作业调度活动。 但在这种情况下,没有运行 JobTracker,此通信没有发生,作业失败。

然后,我们执行了几个 HDFS 操作来突出显示上一节中的要点;不起作用的 MapReduce 集群不会直接影响 HDFS,它仍然可用于所有客户端和操作。

启动更换工单跟踪器

MapReduce 集群的恢复也非常简单。 重新启动 JobTracker 进程后,所有后续 MapReduce 作业都将成功处理。

请注意,当 JobTracker 被终止时,所有正在运行的作业都会丢失,需要重新启动。 注意 HDFS 上的临时文件和目录;许多 MapReduce 作业将临时数据写入 HDFS,这些数据通常在作业完成时被清除。 失败的作业-尤其是由于 JobTracker 失败而失败的作业-可能会留下这些数据,这可能需要手动清理。

拥有一位围棋英雄-将 JobTracker 移动到新主机

但是,如果运行 JobTracker 进程的主机出现致命的硬件故障并且无法恢复,会发生什么情况呢? 在这种情况下,您需要在另一台主机上启动新的 JobTracker 进程。 这需要所有节点使用新位置更新其mapred-site.xml文件,并重新启动群集。 尝尝这个!。 我们将在下一章详细讨论这个问题。

该采取行动了-终止 NameNode 进程

现在让我们终止 NameNode 进程,该进程将直接阻止我们访问 HDFS,进而阻止 MapReduce 作业执行:

备注

不要在具有重要运营意义的群集上尝试此操作。 虽然影响将是短暂的,但它实际上会在一段时间内杀死整个群集。

  1. 登录到 NameNode 主机并列出正在运行的进程:

    $ jps
    2372 SecondaryNameNode
    2118 NameNode
    2434 JobTracker
    5153 Jps
    
    
  2. 终止 NameNode 进程。 不用担心 Second DaryNameNode,它可以继续运行。

  3. 尝试访问 HDFS 文件系统:

    $ hadoop fs -ls /
    11/12/13 16:00:05 INFO ipc.Client: Retrying connect to server: /10.0.0.100:9000\. Already tried 0 time(s).
    11/12/13 16:00:06 INFO ipc.Client: Retrying connect to server: /10.0.0.100:9000\. Already tried 1 time(s).
    11/12/13 16:00:07 INFO ipc.Client: Retrying connect to server: /10.0.0.100:9000\. Already tried 2 time(s).
    11/12/13 16:00:08 INFO ipc.Client: Retrying connect to server: /10.0.0.100:9000\. Already tried 3 time(s).
    11/12/13 16:00:09 INFO ipc.Client: Retrying connect to server: /10.0.0.100:9000\. Already tried 4 
    time(s).
    …
    Bad connection to FS. command aborted.
    
    
  4. 提交 MapReduce 作业:

    $ hadoop jar hadoop/hadoop-examples-1.0.4.jar  pi 10 100
    Number of Maps  = 10
    Samples per Map = 100
    11/12/13 16:00:35 INFO ipc.Client: Retrying connect to server: /10.0.0.100:9000\. Already tried 0 time(s).
    11/12/13 16:00:36 INFO ipc.Client: Retrying connect to server: /10.0.0.100:9000\. Already tried 1 time(s).
    11/12/13 16:00:37 INFO ipc.Client: Retrying connect to server: /10.0.0.100:9000\. Already tried 2 time(s).
    …
    java.lang.RuntimeException: java.net.ConnectException: Call to /10.0.0.100:9000 failed on connection exception: java.net.ConnectException: Connection refused
     at org.apache.hadoop.mapred.JobConf.getWorkingDirectory(JobConf.java:371)
     at org.apache.hadoop.mapred.FileInputFormat.setInputPaths(FileInputFormat.java:309)
    …
    Caused by: java.net.ConnectException: Call to /10.0.0.100:9000 failed on connection exception: java.net.ConnectException: Connection refused
    …
    
    
  5. 检查正在运行的个进程:

    $ jps
    2372 SecondaryNameNode
    5253 Jps
    2434 JobTracker
    Restart the NameNode
    $ start-all.sh
    
    
  6. 访问 HDFS:

    $ Hadoop fs -ls /
    Found 2 items
    drwxr-xr-x   - hadoop supergroup          0 2011-12-16 16:18 /user 
    drwxr-xr-x   - hadoop supergroup          0 2011-12-16 16:23 /var 
    
    

刚刚发生了什么?

我们终止了 NameNode 进程,并尝试访问 HDFS 文件系统。 这当然失败了;没有 NameNode,就没有服务器接收我们的文件系统命令。

然后,我们尝试提交 MapReduce 作业,但也失败了。 从简化的异常堆栈跟踪中可以看到,在尝试设置作业数据的输入路径时,JobTracker 还尝试连接到 NameNode,但失败了。

然后,我们确认 JobTracker 进程是健康的,正是 NameNode 的不可用导致 MapReduce 任务失败。

最后,我们重新启动 NameNode 并确认可以再次访问 HDFS 文件系统。

启动替换 NameNode

到目前为止,MapReduce 和 HDFS 集群之间存在差异,了解到在另一台主机上重新启动新的 NameNode 并不像移动 JobTracker 那么简单就不足为奇了。 更直截了当地说,由于硬件故障而不得不移动 NameNode 可能是使用 Hadoop 集群所能遇到的最严重的危机。 除非你做了仔细的准备,否则丢失所有数据的可能性很高。

这是一个相当不错的陈述,我们需要探索 NameNode 进程的性质来理解为什么会出现这种情况。

更详细地介绍 NameNode 的角色

到目前为止,我们已经谈到 NameNode 进程作为 DataNode 进程和负责确保遵守配置参数(如块复制值)的服务之间的协调器。 这是一组重要的任务,但也非常注重操作。 NameNode 进程还负责管理 HDFS 文件系统元数据;一个很好的类比是将其视为持有传统文件系统中的文件分配表的等价物。

文件系统、文件、数据块和节点

在访问 HDFS 时,您很少关心数据块。 您希望访问文件系统中某个位置的给定文件。 为此,NameNode 进程需要维护大量信息:

  • 实际的文件系统内容、所有文件的名称及其包含的目录
  • 有关每个元素的其他元数据,例如大小、所有权和复制系数
  • 哪些数据块保存每个文件的数据的映射
  • 群集中的哪些节点保存哪些数据块的映射,以及每个数据块的当前复制状态

前面的个点之外的所有点都是持久性数据,在 NameNode 进程重新启动后必须维护这些数据。

群集中最重要的一条数据-FIMAGE

NameNode 进程将两个数据结构存储到磁盘,即fsimage文件和对其进行更改的编辑日志。 fsimage文件包含上一节提到的关键文件系统属性;文件系统上每个文件和目录的名称和详细信息,以及与每个文件和目录对应的块的映射。

如果fsimage文件丢失,您有一系列节点保存数据块,而不知道哪些块对应于文件的哪个部分。 事实上,您甚至不知道首先应该构造哪些文件。 丢失fsimage文件会留下所有文件系统数据,但实际上会使其变得毫无用处。

fsimage文件在启动时由 NameNode 进程读取,出于性能原因在内存中保存和操作。 为了避免丢失对文件系统的更改,所做的任何修改都会在 NameNode 的正常运行时间内写入编辑日志。 下次重新启动时,它会在启动时查找此日志,并使用它来更新fsimage文件,然后将该文件读入内存。

备注

这个过程可以通过使用 Second daryNameNode 进行优化,我们稍后会提到这一点。

数据节点启动

当 DataNode 进程启动时,它通过向 NameNode 进程报告其持有的块来开始其心跳进程。 正如本章前面所解释的,这就是 NameNode 进程如何知道应该使用哪个节点来为给定块的请求提供服务。 如果 NameNode 进程本身重新启动,它将使用所有 DataNode 进程重新建立检测信号来构建块到节点的映射。

由于 DataNode 进程可能进出集群,因此持久存储此映射几乎没有什么用处,因为磁盘上的状态通常会与当前实际情况不符。 这就是 NameNode 进程不会持久化在哪些节点上保存哪些块的位置的原因。

安全模式

如果您在启动 HDFS 群集后不久查看 HDFS Web UI 或dfsadmin的输出,您将看到群集处于安全模式的引用,以及在它离开安全模式之前报告的数据块所需阈值。 这是正在工作的 DataNode 块报告机制。

作为额外的保护措施,NameNode 进程将 HDFS 文件系统保持为只读模式,直到它确认给定百分比的块达到其复制阈值。 在通常情况下,这只需要报告所有 DataNode 进程,但如果有些进程失败,NameNode 进程将需要安排一些重新复制,然后才能退出安全模式。

Second daryNameNode

Hadoop 中最不幸的命名实体是Second daryNameNode。 当人们第一次了解到关键的fsimage文件时,这个称为 Second daryNameNode 的东西开始听起来像是一个有用的缓解方法。 是否如其名称所示,在另一台主机上运行的 NameNode 进程的第二个副本可以在主要主机出现故障时接管? 没有,Second daryNameNode 有一个非常特定的角色;它定期读取fsimage文件的状态,编辑日志,并写出应用了日志更改的更新后的fsimage文件。 这在 NameNode 启动方面节省了大量时间。 如果 NameNode 进程已经运行了很长一段时间,那么编辑日志将非常庞大,将所有更改应用到存储在磁盘上的旧fsimage文件状态将需要很长时间(很容易就是几个小时)。 Second daryNameNode 有助于更快地启动。

那么,当 NameNode 进程出现严重故障时该怎么办呢?

说别惊慌会有帮助吗? 有解决 NameNode 故障的方法,这是一个如此重要的主题,我们将在下一章中用完整的一节来介绍它。 但目前的要点是,您可以将 NameNode 进程配置为写入其fsimage文件,并将日志编辑到多个位置。 通常,网络文件系统被添加为第二个位置,以确保在 NameNode 主机之外有fsimage文件的副本。

但是,在新主机上移动到新 NameNode 进程的过程需要手动操作,并且在您执行此操作之前,Hadoop 集群将处于停滞状态。 这是您想要有一个过程的东西,并且您已经尝试过了(成功!)。 在测试场景中。 你真的不想在你的运营集群崩溃、你的 CEO 对你大喊大叫、公司亏损的时候学习如何做到这一点。

BackupNode/CheckpointNode 和 NameNode HA

Hadoop 0.22 用两个新组件BackupNodeCheckpointNode替换了 SecdaryNameNode。 后者实际上是一个重命名的 Second DaryNameNode;它负责在常规检查点更新fsimage文件,以减少 NameNode 启动时间。

不过,BackupNode 离实现 NameNode 全功能热备份的目标又近了一步。 它从 NameNode 接收持续不断的文件系统更新流,其内存中的状态在任何时间点都是最新的,当前状态保存在主 NameNode 中。 如果 NameNode 失效,BackupNode 将更有能力作为新的 NameNode 投入使用。 该过程不是自动的,需要手动干预和重新启动集群,但它减轻了 NameNode 故障带来的一些痛苦。

请记住,Hadoop 1.0 是 0.20 版分支的延续,因此它不包含前面提到的特性。

Hadoop2.0 将把这些扩展带到下一个逻辑步骤:从当前主 NameNode 到最新备份 NameNode 的全自动 NameNode 故障转移。 此 NameNode高可用性(HA)是对 Hadoop 体系结构要求最长的更改之一,完成后将是一个受欢迎的补充。

硬件故障

当我们早先终止各种 Hadoop 组件时,我们-在大多数情况下-使用 Hadoop 进程的终止作为托管物理硬件故障的代理。 根据经验,如果没有一些底层主机问题导致问题,Hadoop 进程失败的情况非常少见。

主机故障

主机的实际故障是要考虑的最简单的情况。 机器可能会因严重的硬件问题(CPU 故障、电源故障、风扇卡住等)而出现故障,从而导致主机上运行的 Hadoop 进程突然出现故障。 系统级软件中的严重错误(内核死机、I/O 锁定等)也可能产生同样的影响。

一般来说,如果故障导致主机崩溃、重新启动或在一段时间内无法访问,我们可以预期 Hadoop 的行为与本章中所演示的一样。

主机损坏

一个更隐蔽的问题是主机看起来正常工作,但实际上正在产生损坏的结果。 这种情况的示例可能是导致数据损坏的内存故障或导致磁盘上的数据损坏的磁盘扇区错误。

对于 HDFS,这是我们前面讨论的损坏块的状态报告发挥作用的地方。

对于 MapReduce,没有等效的机制。 与大多数其他软件一样,TaskTracker 依赖于主机正确写入和读取的数据,并且无法检测到任务执行或混洗阶段的损坏。

相关故障的风险

有一种现象,大多数人直到它咬了他们才会考虑;有时失败的原因也会导致后续的失败,极大地增加了遇到数据丢失情况的机会。

例如,我曾经在一个使用四个网络设备的系统上工作。 其中一个失败了,没有人关心它;毕竟还有三个设备。 直到他们在 18 小时内全部失败。 原来它们都是一批有问题的硬盘。

它不一定要如此奇特;更常见的原因将是共享服务或设施中的故障。 网络交换机可能会出现故障,配电可能会出现峰值,空调可能会出现故障,设备机架可能会短路。 正如我们将在下一章中看到的那样,Hadoop 不会将块分配到随机位置,它积极寻求采用一种放置策略,以防止共享服务中出现此类故障。

我们又在谈论不太可能的情况,最常见的情况是,一个失败的东道主就是这样,而不是失败危机冰山一角。 但是,切记永远不要忽视不太可能的情况,特别是在集群规模逐渐扩大的情况下。

软件导致任务失败

如前所述,Hadoop 进程本身崩溃或自发失败的情况实际上相对较少。 在实践中,您可能会看到更多由任务导致的故障,即您在集群上执行的映射或 Reduce 任务中的错误。

运行缓慢的任务失败

我们将首先看看如果任务挂起或在 Hadoop 看来已经停止进展会发生什么。

导致行动任务失败的时间

让我们导致任务失败;在此之前,我们需要修改默认超时:

  1. 将此配置属性添加到mapred-site.xml

    <property>
    <name>mapred.task.timeout</name>
    <value>30000</value>
    </property>
    
  2. 现在我们将修改第 3 章了解 MapReduce中的老朋友 Wordcount。 将WordCount3.java复制到名为WordCountTimeout.java的新文件中,并添加以下导入:

    import java.util.concurrent.TimeUnit ;
    import org.apache.hadoop.fs.FileSystem ;
    import org.apache.hadoop.fs.FSDataOutputStream ;
    
  3. map方法替换为以下方法:

        public void map(Object key, Text value, Context context
                        ) throws IOException, InterruptedException {
    String lockfile = "/user/hadoop/hdfs.lock" ;
      Configuration config = new Configuration() ;  
    FileSystem hdfs = FileSystem.get(config) ;  
    Path path = new Path(lockfile) ;  
    if (!hdfs.exists(path))
    {
    byte[] bytes = "A lockfile".getBytes() ;
      FSDataOutputStream out = hdfs.create(path) ;  
    out.write(bytes, 0, bytes.length);
    out.close() ;
    TimeUnit.SECONDS.sleep(100) ;
    }
    
    String[] words = value.toString().split(" ") ;
    
    for (String str: words)
    {
            word.set(str);
            context.write(word, one);
    
        }
        }
      }
    
  4. 更改类名后编译文件,将其压缩,然后在集群上执行:

    $ Hadoop jar wc.jar WordCountTimeout test.txt output
    …
    11/12/11 19:19:51 INFO mapred.JobClient:  map 50% reduce 0%
    11/12/11 19:20:25 INFO mapred.JobClient:  map 0% reduce 0%
    11/12/11 19:20:27 INFO mapred.JobClient: Task Id : attempt_201112111821_0004_m_000000_0, Status : FAILED
    Task attempt_201112111821_0004_m_000000_0 failed to report status for 32 seconds. Killing!
    11/12/11 19:20:31 INFO mapred.JobClient:  map 100% reduce 0%
    11/12/11 19:20:43 INFO mapred.JobClient:  map 100% reduce 100%
    11/12/11 19:20:45 INFO mapred.JobClient: Job complete: job_201112111821_0004
    11/12/11 19:20:45 INFO mapred.JobClient: Counters: 18
    11/12/11 19:20:45 INFO mapred.JobClient:   Job Counters
    …
    
    

刚刚发生了什么?

我们首先修改了一个默认的 Hadoop 属性,该属性管理一个任务在 Hadoop 框架考虑终止之前可以看起来毫无进展的时间。

然后,我们修改了 WordCount3,添加了一些使任务休眠 100 秒的逻辑。 我们在 HDFS 上使用了一个锁定文件,以确保只有一个任务实例休眠。 如果我们在映射操作中只有 SLEEP 语句而不进行任何检查,那么每个映射器都会超时,作业将会失败。

围棋英雄-HDFS 编程访问

我们在本书中说过我们不会真正处理对 HDFS 的编程访问。 不过,请看一下我们在这里所做的工作,并浏览这些类的 Javadoc。 您会发现该接口在很大程度上遵循访问标准 Java 文件系统的模式。

然后,我们编译、打包类,并在集群上执行作业。 第一个任务进入休眠状态,在超过我们设置的阈值(以毫秒为单位指定值)之后,Hadoop 将终止该任务,并重新调度另一个映射器来处理分配给失败任务的拆分。

Hadoop 对运行缓慢任务的处理

Hadoop 在这里需要执行平衡操作。 它想要终止那些停滞不前或由于其他原因运行异常缓慢的任务;但有时复杂的任务只是需要很长时间。 如果任务依赖于任何外部资源来完成其执行,则情况尤其如此。

Hadoop 在决定任务的空闲/静止/停滞时间时,会从任务中寻找进展的证据。 通常情况下,这可能是:

  • 正在发出结果
  • 正在将值写入计数器
  • 明确报告进度

对于后者,Hadoop 提供了Progressable接口,其中包含一个感兴趣的方法:

Public void progress() ;

Context类实现此接口,因此任何映射器或减少器都可以调用context.progress()来表明它是活动的并继续处理。

投机性执行

通常,MapReduce 作业将由许多离散映射和减少任务执行组成。 当在群集上运行时,配置错误或故障的主机确实存在风险,可能会导致其任务的运行速度比其他主机慢得多。

为了解决这个问题,Hadoop 将在映射或减少阶段接近尾声时在整个集群中分配重复的映射或减少任务。 这种推测性的任务执行旨在防止一两个运行缓慢的任务对整个作业执行时间造成重大影响。

Hadoop 对失败任务的处理

任务不会只是挂起;有时它们会显式抛出异常、中止或以其他方式停止执行,而不是像前面提到的那样安静。

Hadoop 有三个配置属性,它们控制如何响应任务失败,都是在mapred-site.xml中设置的:

  • mapred.map.max.attempts:在导致作业失败之前,给定的映射任务将重试多次
  • mapred.reduce.max.attempts:在导致作业失败之前,将多次重试给定的 Reduce 任务[t1
  • mapred.max.tracker.failures:如果记录了如此多的单个任务失败,则作业将失败

所有这些的默认值都是 4。

备注

请注意,将mapred.tracker.max.failures设置为小于其他两个属性的值是没有意义的。

您考虑设置其中哪一个将取决于您的数据和作业的性质。 如果您的作业访问的外部资源偶尔会导致暂时性错误,则增加任务的重复失败次数可能会很有用。 但是,如果任务非常特定于数据,则这些属性可能不太适用,因为失败一次的任务将再次失败。 但是,请注意,默认值高于 1 确实有意义,因为在大型复杂系统中,各种瞬态故障总是可能发生的。

有一个围棋英雄--导致任务失败

修改 wordcount 示例;不是休眠,而是让它抛出一个基于随机数的 RuntimeException。 修改群集配置,并探索管理多少失败的任务将导致整个作业失败的配置属性之间的关系。

数据导致任务失败

我们将探索的最后类故障是与数据相关的故障类型。 这里,我们指的是由于给定记录的数据已损坏、使用了错误的数据类型或格式或各种相关问题而崩溃的任务。 我们指的是那些收到的数据与预期不同的情况。

通过代码处理脏数据

处理脏数据的一种方法是编写防御性处理数据的映射器和减少器。 因此,例如,如果映射器接收的值应该是逗号分隔的值列表,则在处理数据之前首先验证项数。 如果第一个值应该是整数的字符串表示形式,请确保转换为数值类型时具有可靠的错误处理和默认行为。

这种方法的问题在于,无论您多么小心,总会有一些奇怪的数据输入没有被考虑到。 您是否考虑过接收不同 Unicode 字符集的值? 如果有多个字符集、空值、结尾错误的字符串、错误编码的转义字符等怎么办?

如果输入到作业的数据是由您生成和/或控制的,那么这些可能性就不那么重要了。 但是,如果您正在处理从外部来源接收的数据,总会有令人惊讶的理由。

使用 Hadoop 的跳过模式

另一种方法是将 Hadoop 配置为以不同方式处理任务失败。 Hadoop 不会将失败的任务视为原子事件,而是可以尝试识别哪些记录可能导致了问题,并将它们排除在未来的任务执行之外。 该机制称为跳过模式。 如果您遇到各种各样的数据问题,其中围绕这些问题进行编码是不可取或不实用的,那么这会很有用。 或者,如果在您的工作中使用的是第三方库,而您可能没有源代码,那么您可能别无选择。

跳过模式目前仅适用于写入 API 0.20 之前版本的作业,这是另一个需要考虑的问题。

使用跳过模式处理脏数据的操作时间

让我们通过编写一个 MapReduce 作业来查看操作中的跳过模式,该作业接收导致其失败的数据:

  1. 将以下 Ruby 脚本另存为gendata.rb

    File.open("skipdata.txt", "w") do |file|
      3.times do
        500000.times{file.write("A valid record\n")}
        5.times{file.write("skiptext\n")}
      end
      500000.times{file.write("A valid record\n")}
    End
    
  2. 运行脚本:

    $ ruby gendata.rb 
    
    
  3. 检查生成的文件的大小及其行数:

    $ ls -lh skipdata.txt
    -rw-rw-r-- 1 hadoop hadoop 29M 2011-12-17 01:53 skipdata.txt
    ~$ cat skipdata.txt | wc -l
    2000015
    
    
  4. 将文件复制到 HDFS:

    $ hadoop fs -put skipdata.txt skipdata.txt
    
    
  5. 将以下属性定义添加到mapred-site.xml

    <property>
    <name>mapred.skip.map.max.skip.records</name>
    <value5</value>
    </property>
    
  6. 检查为mapred.max.map.task.failures设置的值,如果该值较低,则将其设置为20

  7. 将以下 Java 文件另存为SkipData.java

    import java.io.IOException;
    
    import org.apache.hadoop.conf.* ;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.* ;
    import org.apache.hadoop.mapred.* ;
    import org.apache.hadoop.mapred.lib.* ;
    
    public class SkipData
    {
    
        public static class MapClass extends MapReduceBase
        implements Mapper<LongWritable, Text, Text, LongWritable>
        {
    
            private final static LongWritable one = new LongWritable(1);
            private Text word = new Text("totalcount");
    
            public void map(LongWritable key, Text value,
                OutputCollector<Text, LongWritable> output,
                    Reporter reporter) throws IOException
                    {
                        String line = value.toString();
    
                    if (line.equals("skiptext"))
                    throw new RuntimeException("Found skiptext") ;
                    output.collect(word, one);
                }
            }
    
            public static void main(String[] args) throws Exception
            {
                Configuration config = new Configuration() ;
                JobConf conf = new JobConf(config, SkipData.class);
                conf.setJobName("SkipData");
    
                conf.setOutputKeyClass(Text.class);
                conf.setOutputValueClass(LongWritable.class);
    
                conf.setMapperClass(MapClass.class);
                conf.setCombinerClass(LongSumReducer.class);
                conf.setReducerClass(LongSumReducer.class);
    
                FileInputFormat.setInputPaths(conf,args[0]) ;
                FileOutputFormat.setOutputPath(conf, new Path(args[1])) ;
    
                JobClient.runJob(conf);
            }
        }
    
  8. 编译该文件并将其 JAR 到skipdata.jar

  9. 运行作业:

    $ hadoop jar skip.jar SkipData skipdata.txt output
    …
    11/12/16 17:59:07 INFO mapred.JobClient:  map 45% reduce 8%
    11/12/16 17:59:08 INFO mapred.JobClient: Task Id : attempt_201112161623_0014_m_000003_0, Status : FAILED
    java.lang.RuntimeException: Found skiptext
     at SkipData$MapClass.map(SkipData.java:26)
     at SkipData$MapClass.map(SkipData.java:12)
     at org.apache.hadoop.mapred.MapRunner.run(MapRunner.java:50)
     at org.apache.hadoop.mapred.MapTask.runOldMapper(MapTask.java:358)
     at org.apache.hadoop.mapred.MapTask.run(MapTask.java:307)
     at org.apache.hadoop.mapred.Child.main(Child.java:170)
    11/12/16 17:59:11 INFO mapred.JobClient:  map 42% reduce 8%
    ...
    11/12/16 18:01:26 INFO mapred.JobClient:  map 70% reduce 16%
    11/12/16 18:01:35 INFO mapred.JobClient:  map 71% reduce 16%
    11/12/16 18:01:43 INFO mapred.JobClient: Task Id : attempt_201111161623_0014_m_000003_2, Status : FAILED
    java.lang.RuntimeException: Found skiptext
    ...
    11/12/16 18:12:44 INFO mapred.JobClient:  map 99% reduce 29%
    11/12/16 18:12:50 INFO mapred.JobClient:  map 100% reduce 29%
    11/12/16 18:13:00 INFO mapred.JobClient:  map 100% reduce 100%
    11/12/16 18:13:02 INFO mapred.JobClient: Job complete: job_201112161623_0014
    ...
    
    
  10. 检查作业输出文件的内容:

```scala
$ hadoop fs -cat output/part-00000
totalcount  2000000

```
  1. 在输出目录中查找跳过的记录:
```scala
$ hadoop fs -ls output/_logs/skip
Found 15 items
-rw-r--r--   3 hadoop supergroup        203 2011-12-16 18:05 /user/hadoop/output/_logs/skip/attempt_201112161623_0014_m_000001_3
-rw-r--r--   3 hadoop supergroup        211 2011-12-16 18:06 /user/hadoop/output/_logs/skip/attempt_201112161623_0014_m_000001_4
…

```
  1. Check the job details from the MapReduce UI to observe the recorded statistics as shown in the following screenshot:
![Time for action – handling dirty data by using skip mode](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/hadoop-begin-guide/img/7300_06_04.jpg)

刚刚发生了什么?

我们必须在这里做很多设置,所以让我们一步一步地来完成。

首先,我们需要将 Hadoop 配置为使用跳过模式;默认情况下禁用该模式。 Key Configuration 属性被设置为5,这意味着我们不希望框架跳过任何大于此数字的记录集。 请注意,这包括无效记录,通过将此属性设置为0(默认值),Hadoop 将不会进入跳过模式。

我们还会检查以确保 Hadoop 配置了足够高的重复任务尝试失败阈值,我们稍后将对此进行解释。

接下来,我们需要一个可以用来模拟脏数据的测试文件。 我们编写了一个简单的 Ruby 脚本,该脚本生成了一个包含 200 万行的文件,我们会将其视为有效的,并在该文件中散布着三组五条错误记录。 我们运行此脚本并确认生成的文件确实有 2,000,015 行。 然后将该文件放到 HDFS 上,作为作业输入。

然后,我们编写了一个简单的 MapReduce 作业,该作业可以有效地计算有效记录的数量。 每次该行将输入读取为有效文本时,我们将在聚合为最终总数的基础上再发出 1 的计数。 当遇到无效行时,映射器会抛出异常而失败。

然后,我们编译该文件,将其压缩,然后运行作业。 作业需要一段时间才能运行,从作业状态的摘录中可以看出,它遵循一种我们以前从未见过的模式。 MAP 进度计数器会增加,但当任务失败时,进度会回落,然后再次开始增加。 这是正在运行的跳过模式。

每次将键/值对传递给映射器时,Hadoop 默认情况下会递增一个计数器,使其能够跟踪导致故障的记录。

提示

如果您的 map 或 Reduce 任务通过机制处理其输入,而不是直接通过 map 或 Reduce 方法的参数接收所有数据(例如,从异步进程或缓存),则需要确保手动显式更新此计数器。

当任务失败时,Hadoop 会在同一块上重试该任务,但会尝试解决无效记录。 通过二分搜索方法,框架会对数据执行重试,直到跳过的记录数不大于我们之前配置的最大值(即 5 条)。当框架寻找要跳过的最佳批次时,此过程确实需要多次任务重试和失败,这就是为什么我们必须确保框架配置为能够容忍超过正常数量的重复任务失败。

我们看着作业在这个来回的过程中继续进行,并在完成时检查输出文件的内容。 这显示了 2,000,000 条已处理的记录,这是我们输入文件中正确的有效记录数。 Hadoop 成功地仅跳过了三组(每组五个)无效记录。

然后,我们查看了作业输出目录中的_logs目录,发现有一个包含跳过记录的序列文件的跳过目录。

最后,我们查看了 MapReduce web 用户界面以查看总体作业状态,其中包括在跳过模式下处理的记录数和跳过的记录数。 请注意,失败任务的总数是 22 个,这大于我们的失败映射尝试阈值,但这个数字是多个任务的总失败数。

跳过或不跳过...

跳过模式可能非常有效,但正如我们在前面看到的,Hadoop 必须确定跳过哪个记录范围会导致性能损失。 我们的测试文件实际上对 Hadoop 非常有帮助;坏记录被很好地分成三组,只占整个数据集的很小一部分。 如果输入数据中有更多无效记录,并且它们在整个文件中分布得更广,那么更有效的方法可能是使用前身 MapReduce 作业来过滤掉所有无效记录。

这就是为什么我们提出了编写代码来处理坏数据和连续使用跳过模式的主题。 这两种都是你应该在你的工具带上掌握的有效技术。 当哪种方法是最佳方法时,没有单一的答案,在做出决定之前,您需要考虑输入数据、性能要求和硬编码机会。

摘要

我们在这一章中造成了很多破坏,我希望您永远不必在运行 Hadoop 集群的一天内处理如此多的故障。 从这次经历中有一些关键的学习要点。

通常,组件故障在 Hadoop 中并不可怕。 特别是在大型集群中,某些组件或主机出现故障是相当常见的,Hadoop 就是为处理这种情况而设计的。 HDFS 负责存储数据,它主动管理每个数据块的复制,并计划在 DataNode 进程停止时创建新副本。

MapReduce 对 TaskTracker 故障有一种无状态的方法,一般情况下,如果一个作业失败,它只需调度重复的作业。 它还可以这样做,以防止行为不端的主人拖慢整个工作。

HDFS 和 MapReduce 主节点的故障是更严重的故障。 特别是,NameNode 进程保存关键的文件系统数据,您必须积极确保已将其设置为允许新的 NameNode 进程接管。

通常,硬件故障看起来与以前的进程故障非常相似,但始终要注意相关故障的可能性。 如果任务因软件错误而失败,Hadoop 将在可配置的阈值内重试。 使用跳过模式可以解决与数据相关的错误,尽管这会带来性能损失。

现在我们知道了如何处理集群中的故障,我们将在下一章讨论更广泛的集群设置、运行状况和维护问题。

七、保持运转

拥有 Hadoop 集群并不完全是编写有趣的程序来进行智能数据分析。 您还需要维护集群,使其保持调优,并准备好执行您想要的数据处理。

在本章中,我们将介绍:

  • 有关 Hadoop 配置属性的详细信息
  • 如何为您的群集选择硬件
  • Hadoop 安全性的工作原理
  • 管理 NameNode
  • 管理 HDFS
  • 管理 MapReduce
  • 扩展群集

虽然这些主题侧重于操作,但它们确实给了我们一个机会来探索 Hadoop 的一些我们以前没有研究过的方面。 因此,即使您不亲自管理集群,这里也应该有对您有用的信息。

关于电子病历的说明

使用云服务(如 Amazon Web Services 提供的云服务)的主要好处之一是,大部分维护开销由服务提供商承担。 Elastic MapReduce 可以创建绑定到单个任务(非持久作业流)执行的 Hadoop 集群,或者允许长期运行的集群可用于多个作业(持久作业流)。 当使用非持久作业流时,底层 Hadoop 集群如何配置和运行的实际机制在很大程度上对用户是不可见的。 因此,使用非持久工作流的用户将不需要考虑本章中的许多主题。 如果您在持续的工作流程中使用电子病历,许多主题(但不是所有主题)都会变得相关。

在本章中,我们将概括介绍本地 Hadoop 群集。 如果需要重新配置持久作业流,请使用相同的 Hadoop 属性,但请按照第 3 章编写 MapReduce 作业中所述进行设置。

Hadoop 配置属性

在我们看运行集群之前,让我们先来讨论一下 Hadoop 的配置属性。 在此过程中,我们一直在介绍其中的许多内容,还有几个额外的要点值得考虑。

默认值

对于 Hadoop 新用户来说,最令人费解的事情之一是大量的配置属性。 它们来自哪里,它们的含义是什么,它们的默认值是什么?

如果您拥有完整的 Hadoop 发行版-即,不仅仅是二进制发行版-以下 XML 文件将回答您的问题:

  • Hadoop/src/core/core-default.xml
  • Hadoop/src/hdfs/hdfs-default.xml
  • Hadoop/src/mapred/mapred-default.xml

操作浏览默认属性的时间

幸运的是,XML 文档不是查看默认值的唯一方式;还有更具可读性的 HTML 版本,我们现在将快速了解一下。

这些文件不包括在 Hadoop 仅限二进制版本中;如果您正在使用该版本,还可以在 Hadoop 网站上找到这些文件。

  1. Point your browser at the docs/core-default.html file within your Hadoop distribution directory and browse its contents. It should look like the next screenshot:

    Time for action – browsing default properties

  2. 现在,类似地,浏览以下其他文件:

    • Hadoop/docs/hdfs-default.html
    • Hadoop/docs/mapred-default.html

刚刚发生了什么?

如您所见,每个属性都有名称、默认值和简短描述。 您还会看到确实有非常多的属性。 现在不要期望了解所有这些内容,但一定要花点时间浏览一下,以了解 Hadoop 允许的定制类型。

其他属性元素

当我们之前在配置文件中设置了属性时,我们使用了以下形式的 XML 元素:

<property>
<name>the.property.name</name>
<value>The property value</value>
</property>

我们还可以添加另外两个可选的 XML 元素:descriptionfinal。 现在,使用这些附加元素的完整描述属性如下所示:

<property>
<name>the.property.name</name>
<value>The default property value</value>
<description>A textual description of the property</description>
<final>Boolean</final>
</property>

Description 元素是不言而喻的,它提供了我们在前面的 HTML 文件中看到的每个属性的描述性文本的位置。

final属性的含义与 Java 中的类似:标记为final的任何属性不能被任何其他文件中的值或其他方式覆盖;我们很快就会看到这一点。 对于出于性能、完整性、安全性或其他原因而希望强制实施群集范围值的属性,请使用此选项。

默认存储位置

您将看到修改 Hadoop 在本地磁盘和 HDFS 上存储数据的位置的属性。 有一个属性用作许多其他hadoop.tmp.dir的基础,它是所有 Hadoop 文件的根位置,其缺省值是/tmp

不幸的是,许多 Linux 发行版-包括 Ubuntu-被配置为在每次重新引导时删除该目录的内容。 这意味着如果您不覆盖此属性,您将在下次主机重新启动时丢失所有 HDFS 数据。 因此,在core-site.xml中设置如下内容是值得的:

<property>
<name>hadoop.tmp.dir</name>
<value>/var/lib/hadoop</value>
</property>

请记住,要确保启动 Hadoop 的用户可以写入该位置,并且目录所在的磁盘有足够的空间。 正如您稍后将看到的,还有许多其他属性允许更精细地控制特定类型数据的存储位置。

设置属性的位置

我们之前已经使用配置文件为 Hadoop 属性指定了新值。 这很好,但如果我们试图为某个属性找到最佳值或正在执行需要特殊处理的作业,则会产生开销。

可以使用JobConf类以编程方式设置正在执行的作业的配置属性。 支持两种类型的方法,第一种是专门用于设置特定属性的方法,比如我们已经看到的用于设置作业名称、输入和输出格式等的方法。 还有一些方法可以设置属性,例如作业的 MAP 和 Reduce 任务的首选数量。

此外,还有一组泛型方法,如下所示:

  • Void set(String key, String value);
  • Void setIfUnset(String key, String value);
  • Void setBoolean( String key, Boolean value);
  • Void setInt(String key, int value);

这些方法更加灵活,不需要为我们希望修改的每个属性创建特定的方法。 但是,它们也会丢失编译时检查,这意味着您可以使用无效的属性名称或为属性分配错误的类型,并且只能在运行时才能发现。

备注

这种以编程方式和在配置文件中设置属性值的能力是能够将属性标记为final的重要原因。 对于您不希望任何已提交作业能够覆盖它们的属性,请在主配置文件中将其设置为最终属性。

设置群集

在我们看如何保持集群运行之前,让我们先来看看设置集群的一些方面。

有多少台主机?

当考虑一个新的 Hadoop 集群时,首要问题之一是从多大容量开始。 我们知道,随着需求的增长,我们可以添加更多节点,但我们也希望以一种轻松增长的方式开始。

这里确实没有明确的答案,因为这在很大程度上取决于要处理的数据集的大小和要执行的作业的复杂性。 唯一近乎绝对的说法是,如果您希望复制因子为n,则至少应该有那么多节点。 但请记住,节点会出现故障,如果您的节点数量与默认的复制因子相同,则任何单个故障都会将数据块推入复制不足状态。 在具有数十个或数百个节点的大多数集群中,这不是问题;但对于复制系数为 3 的非常小的集群,最安全的方法是 5 节点集群。

计算节点上的可用空间

所需节点数量的一个明显起点是查看要在集群上处理的数据集的大小。 如果您的主机具有 2 TB 的磁盘空间和 10 TB 的数据集,那么很可能会认为 5 个节点是所需的最低数量。

这是不正确的,因为它忽略了复制因素和对临时空间的需求。 回想一下,映射器的输出被写入本地磁盘,以便由还原器检索。 我们需要考虑到这种重要的磁盘使用情况。

一个不错的经验法则是假设复制系数为 3,剩余空间的 25%应计为临时空间。 使用这些假设,我们的 2 TB 节点上的 10 TB 数据集所需的群集计算如下:

  • Divide the total storage space on a node by the replication factor:

    2 TB/3=666 GB

  • Reduce this figure by 25 percent to account for temp space:

    666 GB*0.75=500 GB

  • 因此,每个 2 TB 节点大约有 500 GB(0.5 TB)的可用空间

  • Divide the data set size by this figure:

    10 TB/500 GB=20

因此,我们的 10 TB 数据集可能至少需要 20 个节点的群集,是我们天真估计的四倍。

这种需要比预期更多的节点的模式并不少见,在考虑您希望主机达到多高规格时应该记住这一点;请参阅本章后面的调整硬件一节。

主节点的位置

下一个问题是 NameNode、JobTracker 和 Second daryNameNode 将位于何处。 我们已经看到,DataNode 可以与 NameNode 运行在同一主机上,TaskTracker 可以与 JobTracker 共存,但对于生产集群来说,这不太可能是一个很好的设置。

正如我们将看到的,NameNode 和 Second daryNameNode 有一些特定的资源需求,任何影响它们性能的东西都可能会降低整个集群操作的速度。

理想的情况是将 NameNode、JobTracker 和 Second daryNameNode 放在它们自己的专用主机上。 但是,对于非常小的群集,这将导致硬件占用空间的显著增加,而不一定会获得全部好处。

如果可能,第一步应该是将 NameNode、JobTracker 和 Second daryNameNode 分离到没有运行任何 DataNode 或 TaskTracker 进程的单个专用主机上。 随着群集的不断增长,您可以添加额外的服务器主机,然后将 NameNode 移到自己的主机上,从而保持 JobTracker 和 Second DaryNameNode 位于同一位置。 最后,随着集群进一步发展,迁移到完全分离将是有意义的。

备注

正如在保持事物运行中所讨论的,Hadoop 2.0 将辅助 NameNode 拆分为备份 NameNode 和检查点 NameNode。 最佳实践仍在发展中,但目标是为 NameNode 和至少一个备份 NameNode 各有一台专用主机似乎是明智的。

调整硬件大小

要存储的数据量不是关于节点要使用的硬件规格的唯一考虑因素。 相反,您必须考虑可用的处理能力、内存、存储类型和网络。

关于为 Hadoop 集群选择硬件的文章已经很多了,再说一次,没有一个单一的答案可以适用于所有情况。 最大的变量是将在数据上执行的 MapReduce 任务的类型,特别是它们是否受 CPU、内存、I/O 或其他因素的限制。

处理器/内存/存储比

考虑这一点的一个好方法是从 CPU/内存/存储比的角度来看待潜在的硬件。 因此,例如,具有 8 GB 内存和 2 TB 存储的四核主机可以被视为每 1 TB 存储具有两个核心和 4 GB 内存。

然后看看您将要运行的 MapReduce 作业的类型,这个比率看起来合适吗? 换句话说,您的工作负载是否按比例需要更多的这些资源,或者更平衡的配置就足够了吗?

当然,这是通过建立原型和收集度量来评估的最佳方法,但这并不总是可能的。 如果不是,考虑一下这项工作的哪个部分是最昂贵的。 例如,我们看到的一些作业是 I/O 绑定的,从磁盘读取数据,执行简单的转换,然后将结果写回磁盘。 如果这是我们工作负载的典型情况,我们可能会使用具有更多存储的硬件-特别是当它由多个磁盘提供以增加 I/O 时-而使用更少的 CPU 和内存。

相反,执行非常繁重的数字处理的作业将需要更多的 CPU,而那些创建或使用大型数据结构的作业将从内存中受益。

从限制因素的角度来考虑这一点。 如果您的作业正在运行,它是受 CPU 限制(处理器满负荷运行;内存和 I/O 为备用)、内存为限制(物理内存已满并交换到磁盘;CPU 和 I/O 为备用)还是 I/O 为限制(CPU 和内存为备用,但数据以最大可能的速度从磁盘读取/写入)? 你能买到能放松这一限制的硬件吗?

这当然是一个无限的过程,因为一旦你放松了一个界限,另一个界限就会显露出来。 因此,请始终记住,我们的想法是获得一个在您可能的使用场景上下文中有意义的性能配置文件。

如果你真的不知道你工作的绩效特点怎么办? 理想情况下,试着找出答案,在你拥有的任何硬件上做一些原型测试,并用它来指导你的决定。 但是,如果连这都不可能,您将不得不进行配置并试用。 请记住,Hadoop 支持异构硬件-尽管拥有统一的规范最终会让您的工作更轻松-因此,请将集群构建到尽可能小的大小并评估硬件。 利用这些知识为未来有关额外购买主机或升级现有机群的决策提供信息。

电子病历作为原型平台

回想一下,当我们在 Elastic MapReduce 上配置作业时,我们选择了主节点和数据/任务节点的硬件类型。 如果您计划在 EMR 上运行作业,您有一个内置的功能来调整此配置,以找到价格和执行速度的最佳硬件规格组合。

但是,即使您不打算全职使用 EMR,它也可以是一个有价值的原型平台。 如果您正在调整集群大小,但不知道作业的性能特征,请考虑 EMR 上的一些原型以获得更好的洞察力。 虽然您最终可能会在您没有计划的 EMR 服务上花钱,但这可能比发现您为集群购买了完全不合适的硬件的成本要低得多。

特殊节点要求

并非所有主机都有相同的硬件要求。 具体地说,NameNode 的主机看起来可能与托管 DataNodes 和 TaskTracker 的主机截然不同。

回想一下,NameNode 保存 HDFS 文件系统的内存表示,以及文件、目录、块、节点和各种元数据之间的关系。 这意味着 NameNode 往往受内存限制,可能比任何其他主机需要更大的内存,特别是对于非常大的集群或具有大量文件的主机。 虽然 16 GB 可能是 DataNodes/TaskTracker 的常见内存大小,但 NameNode 主机拥有 64 GB 或更多内存的情况并不少见。 如果 NameNode 耗尽了物理内存并开始使用交换空间,则对群集性能的影响可能会很严重。

然而,虽然 64 GB 的物理内存很大,但对于现代存储来说很小,而且鉴于文件系统映像是 NameNode 存储的唯一数据,我们不需要 DataNode 主机上常见的海量存储。 我们更关心 NameNode 的可靠性,因此很可能在冗余配置中有多个磁盘。 因此,NameNode 主机将受益于多个小型驱动器(用于冗余),而不是大型驱动器。

因此,总体而言,NameNode 主机看起来可能与集群中的其他主机非常不同;这就是为什么我们早先建议在预算/空间允许的情况下尽快将 NameNode 移动到它自己的主机,因为这样更容易满足其独特的硬件要求。

备注

Second daryNameNode(或 Hadoop 2.0 中的 CheckpointNameNode 和 BackupNameNode)与 NameNode 具有相同的硬件要求。 您可以在一个更通用的主机上以辅助容量运行它,但如果由于主要硬件故障而需要切换并将其设置为 NameNode,您可能会遇到麻烦。

存储类型

虽然您会发现对前面关于处理器、内存和存储容量(或 I/O)的相对重要性的一些观点有强烈的看法,但这些论点通常是基于应用要求以及硬件特征和度量的。 然而,一旦我们开始讨论要使用的存储类型,就很容易陷入火焰战的局面,在那里你会发现非常根深蒂固的观点。

商品级存储与企业级存储

第一个论证将是关于使用针对商品/消费者细分市场的硬盘驱动器还是针对企业客户的硬盘驱动器最有意义。 前者(主要是 SATA 磁盘)更大、更便宜、速度更慢,平均无故障时间(MTBF)的报价较低。 企业磁盘将使用 SAS 或光纤通道等技术,总体上将更小、更昂贵、更快,并且具有更高的报价 MTBF 数字。

单磁盘与 RAID

下一个问题将是关于磁盘是如何配置的。 企业级的方法是使用廉价磁盘冗余阵列(RAID)将多个磁盘分组到单个逻辑存储设备中,该设备可以安静地承受一个或多个磁盘故障。 这随之而来的是总体容量损失的代价以及对实现的读/写速率的影响。

另一种方法是独立处理每个磁盘,以最大限度地提高总存储和聚合 I/O,代价是单个磁盘故障导致主机宕机。

寻找平衡点

Hadoop 架构在很多方面都是,其前提是硬件将出现故障。 从这个角度来看,可以争辩说没有必要使用任何传统的以企业为重点的存储功能。 取而代之的是,使用许多大而便宜的磁盘来最大化总存储,并并行地从它们读取和写入,以同样地提高 I/O 吞吐量。 单个磁盘故障可能会导致主机出现故障,但正如我们所看到的,群集将解决此故障。

这是一个完全有效的论点,在许多情况下完全有道理。 然而,这一论点忽略了让主机重新投入服务的成本。 如果您的群集位于隔壁房间,并且您有一架备用磁盘,则主机恢复可能会是一项快速、无痛苦且成本低廉的任务。 但是,如果您的集群是由商业配置机构托管的,则任何实际操作的维护成本都可能要高得多。 如果您使用的是完全托管的服务器,而您必须向提供商支付维护任务的费用,情况就更是如此。 在这种情况下,使用 RAID 带来的额外成本以及减少的容量和 I/O 可能是有意义的。

Колибри网络存储

有一件事几乎永远不会有意义,那就是将网络存储用于您的主群集存储。 无论是通过存储区域网络(SAN)进行块存储,还是通过网络文件系统(NFS)或类似协议进行基于文件的存储,这些方法通过引入不必要的瓶颈和额外的共享设备来约束 Hadoop,从而对故障产生重大影响。

然而,有时您可能会因为非技术原因而被迫使用这样的东西。 这并不是说它不起作用,只是它改变了 Hadoop 在速度和容错性方面的执行方式,所以请确保您了解如果发生这种情况的后果。

Hadoop 网络配置

Hadoop 对网络设备的支持不如对存储的支持复杂,因此与 CPU、内存和存储设置相比,您需要选择的硬件更少。 归根结底,Hadoop 目前只能支持一个网络设备,例如,不能使用主机上的所有 4 Gb 以太网连接来实现 4 Gb 的总吞吐量。 如果您需要的网络吞吐量大于单个千兆位端口所提供的吞吐量,则除非您的硬件或操作系统可以将多个端口作为单个设备提供给 Hadoop,否则唯一的选择就是使用 10 千兆位以太网设备。

块的放置方式

我们已经讨论了很多关于使用复制实现冗余的 HDFS,但是还没有探索 Hadoop 如何选择将数据块的副本放置在哪里。

在大多数传统服务器群中,各种主机(以及网络和其他设备)安装在垂直堆叠设备的标准大小机架中。 每个机架通常都有一个为其供电的公共配电装置,并且通常有一个网络交换机作为更广泛的网络与机架中所有主机之间的接口。

在此设置下,我们可以确定三种主要的故障类型:

  • 影响单个主机的故障(例如,CPU/内存/磁盘/主板故障)
  • 影响单个机架的故障(例如,电源装置或交换机故障)
  • 影响整个群集的因素(例如,更大的电源/网络故障、冷却/环境中断)

备注

请记住,Hadoop 目前不支持分布在多个数据中心的集群,因此第三种类型故障的实例很可能会导致集群崩溃。

默认情况下,Hadoop 会将每个节点视为位于同一物理机架中。 这意味着任何一对主机之间的带宽和延迟大致相等,并且每个节点与任何其他节点遭受相关故障的可能性相同。

机架感知

但是,如果您确实具有多机架设置,或者其他配置使前面的假设无效,则可以为每个节点添加向 Hadoop 报告其机架 ID 的功能,Hadoop 随后会在放置副本时考虑这一点。

在这样的设置中,Hadoop 尝试将节点的第一个副本放在给定主机上,第二个副本放在同一机架中的另一个主机上,第三个副本放在不同机架中的主机上。

此策略在性能和可用性之间提供了良好的平衡。 当机架包含自己的网络交换机时,机架内主机之间的通信延迟通常低于与外部主机之间的通信延迟。 此策略在一个机架内放置两个副本,以确保这些副本的最大写入速度,但在机架外保留一个副本,以便在机架故障时提供冗余。

机架感知脚本

如果设置了topology.script.file.name属性并指向文件系统上的可执行脚本,NameNode 将使用它来确定每个主机的机架。

请注意,需要设置该属性,并且脚本只需要存在于 NameNode 主机上。

NameNode 将向脚本传递它发现的每个节点的 IP 地址,因此脚本负责从节点 IP 地址到机架名称的映射。

如果未指定脚本,则每个节点将被报告为单个默认机架的成员。

行动时间-检查默认机架配置

让我们来看看如何在我们的集群中设置默认机架配置。

  1. 执行以下命令:

    $ Hadoop fsck -rack
    
    
  2. 结果应包括类似以下内容的输出:

    Default replication factor:    3
    Average block replication:     3.3045976
    Corrupt blocks:                0
    Missing replicas:              18 (0.5217391 %)
    Number of data-nodes:          4
    Number of racks:               1
    The filesystem under path '/' is HEALTHY
    
    

刚刚发生了什么?

这里对使用的工具及其输出都很感兴趣。 该工具是Hadoop fsck,可用于检查和修复文件系统问题。 可以看到,这包括一些与我们的老朋友hadoop dfsadmin没有什么不同的信息,尽管该工具更关注每个节点的详细状态,而hadoop fsck报告整个文件系统的内部结构。

它报告的内容之一是集群中的机架总数,如前面的输出所示,其值为1,与预期不谋而合。

备注

此命令是在最近用于某些 HDFS 弹性测试的群集上执行的。 这解释了平均数据块复制和复制不足数据块的数字。

如果某个数据块因主机临时故障而导致复制副本数量超过所需数量,则恢复服务的主机会将该数据块置于最小复制系数之上。 除了确保数据块已添加副本以满足复制因素外,Hadoop 还将删除多余的副本以将数据块返回到复制因素。

该行动了-添加机架感知脚本

我们可以通过创建派生每个主机的机架位置的脚本来增强默认的扁平机架配置。

  1. 在 NameNode 主机上的 Hadoop 用户主目录中创建名为rack-script.sh的脚本,其中包含以下文本。 请记住将 IP 地址更改为其中一个 HDFS 节点。

    #!/bin/bash
    
    if [ $1 = "10.0.0.101" ]; then
        echo -n "/rack1 "
    else
        echo -n "/default-rack "
    fi
    
  2. 使此脚本可执行。

    $ chmod +x rack-script.sh
    
    
  3. 将以下属性添加到 NameNode 主机上的core-site.xml

    <property>
    <name>topology.script.file.name</name>
    <value>/home/Hadoop/rack-script.sh</value>
    </property>
    
  4. 重新启动 HDFS。

    $ start-dfs.sh
    
    
  5. Check the filesystem via fsck.

    $ Hadoop fsck –rack
    
    

    上述命令的输出如下图所示:

    Time for action – adding a rack awareness script

刚刚发生了什么?

我们首先创建了一个简单的脚本,该脚本为命名节点返回一个值,为所有其他节点返回一个默认值。 我们将其放在 NameNode 主机上,并将所需的配置属性添加到 NameNodecore-site.xml文件。

启动 HDFS 后,我们使用hadoop fsck报告文件系统,看到现在有了一个双机架群集。 有了这些知识,Hadoop 现在将采用更复杂的块放置策略,如前所述。

提示

使用外部主机文件

一种常见的方法是在 Unix 上保留一个类似于/etc/hosts文件的单独数据文件,并使用该文件指定 IP/机架映射,每行一个。 然后,该文件可以独立更新,并由机架识别脚本读取。

什么是商用硬件?

让我们回顾一下问题,即集群使用的主机的一般特征,以及它们看起来是更像一个商用白盒服务器,还是更像是为高端企业环境而构建的东西。

问题的一部分是“商品”是一个模棱两可的术语。 对于一家企业来说,看起来便宜而令人愉悦的东西,对另一家企业来说,可能看起来是奢侈的高端。 我们建议在选择硬件时考虑以下几点,然后对您的决定保持满意:

  • 对于您的硬件,您是否为复制 Hadoop 的某些容错功能的可靠性功能支付了额外费用?
  • 您为解决已确认的需求或风险而支付的高端硬件功能在您的环境中是否切合实际?
  • 您是否已验证高端硬件的成本高于价格较低/可靠性较低的硬件?

弹出式测验-设置群集

问题 1.。 在为您的新 Hadoop 群集选择硬件时,以下哪项最重要?

  1. CPU 核心的数量及其速度。
  2. 物理内存量。
  3. 存储量。
  4. 存储的速度。
  5. 这取决于最有可能的工作负载。

Q2.。 为什么您可能不想在群集中使用网络存储?

  1. 因为它可能会引入新的单点故障。
  2. 因为考虑到 Hadoop 的容错能力,它很可能具有冗余和容错的方法,这可能是不必要的。
  3. 因为这样的单个设备的性能可能低于 Hadoop 同时使用多个本地磁盘的性能。
  4. 以上都是。

第三季度。 您将在群集上处理 10 TB 的数据。 您的主要 MapReduce 作业处理金融交易,使用它们生成行为和未来预测的统计模型。 以下哪种硬件选择会是您群集的首选?

  1. 20 台主机,每台配备快速双核处理器、4 GB 内存和一个 500 GB 磁盘驱动器。
  2. 30 台主机,每台配备快速双核处理器、8 GB 内存和两个 500 GB 磁盘驱动器。
  3. 30 台主机,每台配备快速四核处理器、8 GB 内存和一个 1 TB 磁盘驱动器。
  4. 40 台主机,每台配备 16 GB 内存、快速四核处理器和四个 1 TB 磁盘驱动器。

集群访问控制

一旦启动并运行了这个闪亮的新集群,您就需要考虑访问和安全问题。 谁可以访问群集上的数据-是否有您真的不想让整个用户群看到的敏感数据?

Hadoop 安全模型

直到最近,Hadoop 还拥有一个充其量可以被描述为“仅标记”的安全模型。 它将所有者和组与每个文件相关联,但是,正如我们将看到的,它几乎没有对给定的客户端连接进行验证。 强大的安全性不仅可以管理指定给文件的标记,还可以管理所有连接用户的身份。

行动时间-演示默认安全性

当我们以前显示了文件列表时,我们已经看到了它们的用户名和组名。 然而,我们还没有真正探索这意味着什么。 我们就这么做吧。

  1. 在 Hadoop 用户的主目录中创建一个测试文本文件。

    $ echo "I can read this!" >  security-test.txt 
    $ hadoop fs -put security-test.txt  security-test.txt 
    
    
  2. Change the permissions on the file to be accessible only by the owner.

    $ hadoop fs -chmod 700 security-test.txt 
    $ hadoop fs -ls
    
    

    上述命令的输出如下图所示:

    Time for action – demonstrating the default security

  3. Confirm you can still read the file.

    $ hadoop fs -cat security-test.txt 
    
    

    您将在屏幕上看到以下行:

    I can read this!
    
    
  4. Connect to another node in the cluster and try to read the file from there.

    $ ssh node2
    $ hadoop fs -cat security-test.txt 
    
    

    您将在屏幕上看到以下行:

    I can read this!
    
    
  5. 从另一个节点注销。

    $ exit
    
    
  6. Create a home directory for another user and give them ownership.

    $ hadoop m[Kfs -mkdir /user/garry
    $ hadoop fs -chown garry /user/garry
    $ hadoop fs -ls /user
    
    

    上述命令的输出如下图所示:

    Time for action – demonstrating the default security

  7. 切换到该用户。

    $ su garry
    
    
  8. 尝试读取 Hadoop 用户主目录中的测试文件。

    $ hadoop/bin/hadoop fs -cat /user/hadoop/security-test.txt
    cat: org.apache.hadoop.security.AccessControlException: Permission denied: user=garry, access=READ, inode="security-test.txt":hadoop:supergroup:rw-------
    
    
  9. Place a copy of the file in this user's home directory and again make it accessible only by the owner.

    $ Hadoop/bin/Hadoop fs -put security-test.txt security-test.txt
    $ Hadoop/bin/Hadoop fs -chmod 700 security-test.txt
    $ hadoop/bin/hadoop fs -ls 
    
    

    上述命令的输出如以下截图所示:

    Time for action – demonstrating the default security

  10. Confirm this user can access the file.

```scala
$ hadoop/bin/hadoop fs -cat security-test.txt 

```

您将在屏幕上看到以下行:

```scala
I can read this!

```
  1. 返回到 Hadoop 用户。
```scala
$ exit

```
  1. Try and read the file in the other user's home directory.
```scala
$ hadoop fs -cat /user/garry/security-test.txt

```

您将在屏幕上看到以下行:

```scala
I can read this!

```

刚刚发生了什么?

我们首先使用 Hadoop 用户在 HDFS 上的主目录中创建一个测试文件。 我们对hadoop fs使用了-chmod选项,这是我们以前从未见过的。 这与标准 Unixchmod工具非常相似,后者为文件所有者、组成员和所有用户提供不同级别的读/写/执行访问权限。

然后,我们转到另一台主机,再次以 hadoop 用户的身份尝试访问该文件。 不足为奇的是,这种做法奏效了。 但是为什么呢? Hadoop 对允许其访问该文件的 Hadoop 用户了解多少?

为了探索这一点,我们随后在 HDFS 上创建了另一个主目录(您可以使用您有权访问的主机上的任何其他帐户),并通过使用hadoop fs-chown选项授予它所有权。 这看起来应该再次类似于标准 Unix-chown。 然后,我们切换到该用户并尝试读取存储在 Hadoop 用户主目录中的文件。 此操作失败,出现前面显示的安全异常,这也是我们所预期的。 我们再次将一个测试文件复制到该用户的主目录中,并使其仅供所有者访问。

但是,我们随后切换回 Hadoop 用户,并尝试访问另一个帐户主目录中的文件,从而搅乱了局面,令人惊讶的是,这一切都奏效了。

用户标识

谜题第一部分的答案是 Hadoop 使用执行 HDFS 命令的用户的 Unix ID 作为 HDFS 上的用户标识。 因此,名为alice的用户执行的任何命令都将使用名为alice的所有者创建文件,并且只能读取或写入该用户具有正确访问权限的文件。

有安全意识的人会意识到,要访问 Hadoop 群集,只需在任何可以连接到该群集的主机上创建一个与现有 HDFS 用户同名的用户即可。 因此,例如,在前面的示例中,在可以访问 NameNode 的任何主机上创建的名为hadoop的任何用户都可以读取用户hadoop可以访问的所有文件,这实际上比看起来更糟糕。

超级用户

在上一步中,Hadoop 用户访问了另一个用户的文件。 Hadoop 将启动集群的用户 ID 视为超级用户,并为其提供各种权限,例如读取、写入和修改 HDFS 上的任何文件的能力。 有安全意识的人会意识到在 Hadoop 管理员控制之外的主机上随机创建名为hadoop的用户的风险更大。

更精细的访问控制

上述情况导致 Hadoop 从一开始就存在安全问题。 然而,社区并没有停滞不前,经过大量工作,Hadoop 的最新版本支持更精细、更强大的安全模型。

为了避免依赖简单的用户 ID,开发人员需要从某个地方了解用户身份,因此选择了与之集成的 Kerberos 系统。 这确实需要建立和维护本书讨论范围之外的服务,但是如果这种安全性对您很重要,请参考 Hadoop 文档。 请注意,此支持确实允许与第三方身份系统(如 Microsoft Active Directory)集成,因此功能相当强大。

通过物理访问控制绕过安全模型

如果 Kerberos 的负担太重,或者安全性是可有可无的,而不是绝对的,那么有一些方法可以降低风险。 我最喜欢的一种方式是将整个集群置于具有严格访问控制的防火墙之后。 特别是,只允许从将被视为簇头节点且所有用户都连接到的单个主机访问 NameNode 和 JobTracker 服务。

提示

从非群集主机访问 Hadoop

Hadoop 无需在主机上运行,即可使用命令行工具访问 HDFS 并运行 MapReduce 作业。 只要主机上安装了 Hadoop,并且其配置文件具有正确的 NameNode 和 JobTracker 位置,就可以在调用Hadoop fsHadoop jar等命令时找到它们。

此模型之所以有效,是因为只有一台主机用于与 Hadoop 交互;而且由于该主机由集群管理员控制,普通用户应该无法创建或访问其他用户帐户。

请记住,此方法不会提供安全性。 它在一个软件系统周围设置了一个硬外壳,以减少 Hadoop 安全模型被颠覆的方式。

管理 NameNode

让我们再做一些风险降低。 在第 6 章当事情中断时,当我谈到运行 NameNode 的主机故障的潜在后果时,我可能吓到您了。 如果那一节没有吓到你,那就回去重读一遍--它应该吓到你的。 总结是,丢失 NameNode 可能会丢失集群上的每一条条数据。 这是因为 NameNode 写入一个名为fsimage的文件,该文件包含文件系统的所有元数据,并记录哪些块组成哪些文件。 如果 NameNode 主机丢失导致fsimage无法恢复,则所有 HDFS 数据也同样丢失。

为 fsimage 类配置多个位置

NameNode 可以配置为同时将fsimage写入多个位置。 这纯粹是一种冗余机制,相同的数据写入每个位置,并且不会尝试使用多个存储设备来提高性能。 相反,政策是fsimage的多个副本将更难丢失。

该行动了-添加额外的图像位置

现在,让我们将 NameNode 配置为同时写入fsimage的多个副本,以提供所需的数据弹性。 为此,我们需要一个 NFS 导出的目录。

  1. 确保群集已停止。

    $ stopall.sh
    
    
  2. 将以下属性添加到Hadoop/conf/core-site.xml,将第二个路径修改为指向可以写入 NameNode 数据的附加副本的 NFS 挂载位置。

    <property>
    <name>dfs.name.dir</name>
    <value>${hadoop.tmp.dir}/dfs/name,/share/backup/namenode</value>
    </property>
    
  3. 删除新添加目录的所有现有内容。

    $ rm -f /share/backup/namenode
    
    
  4. 启动群集。

    $ start-all.sh
    
    
  5. 通过对前面指定的两个文件运行md5sum命令(根据您配置的位置更改以下代码),验证fsimage是否写入了这两个指定位置:

    $ md5sum /var/hadoop/dfs/name/image/fsimage
    a25432981b0ecd6b70da647e9b94304a  /var/hadoop/dfs/name/image/fsimage
    $ md5sum /share/backup/namenode/image/fsimage
    a25432981b0ecd6b70da647e9b94304a  /share/backup/namenode/image/fsimage
    
    

刚刚发生了什么?

首先,我们确保集群已停止;尽管正在运行的集群不会重新读取对核心配置文件的更改,但这是一个好习惯,以防 Hadoop 中添加了该功能。

然后,我们向集群配置添加了一个新属性,为data.name.dir属性指定值。 此属性获取逗号分隔值的列表,并将fsimage写入每个位置。 注意前面讨论的hadoop.tmp.dir属性是如何被取消引用的,这在使用 Unix 变量时可以看到。 此语法允许我们将属性值基于其他属性,并在更新父属性时继承更改。

提示

不要忘记所有必需的位置

此属性的默认值为${Hadoop.tmp.dir}/dfs/name。 添加附加值时,请记住也要显式添加缺省值,如前所示。 否则,该属性将仅使用单个新值。

在启动群集之前,我们确保新目录存在并且为空。 如果目录不存在,NameNode 将无法按预期启动。 但是,如果该目录以前用于存储 NameNode 数据,Hadoop 也将无法启动,因为它将识别两个目录包含不同的 NameNode 数据,并且不知道哪个目录是正确的。

这里要小心! 特别是当您尝试各种 NameNode 数据位置或在节点之间来回交换时;您真的不希望意外删除错误目录中的内容。

在启动 HDFS 集群之后,我们等待片刻,然后使用 MD5 加密校验和来验证两个位置是否包含相同的fsimage

传真复印件的写入位置

建议至少将fsimage写入两个位置,其中一个应该是远程(如 NFS)文件系统,如上例所示。 fsimage仅定期更新,因此文件系统不需要高性能。

在前面关于硬件选择的讨论中,我们提到了 NameNode 主机的其他注意事项。 由于fsimage的重要性,确保将其写入多个磁盘并可能投资于可靠性更高的磁盘,甚至将fsimage写入 RAID 阵列可能是有用的。 如果主机出现故障,使用写入远程文件系统的副本将是最简单的选择;但万一也遇到问题,最好选择从故障主机中取出另一个磁盘,然后在另一个主机上使用它来恢复数据。

交换到另一台 NameNode 主机

我们已确保将fsimage写入多个位置,这是管理到不同 NameNode 主机的交换的一个最重要的前提条件。 现在我们需要真正做到这一点。

这是您确实不应该在生产集群上执行的操作。 在第一次尝试的时候绝对不是,但即使在那之后,这也不是一个没有风险的过程。 但一定要在其他集群上练习,了解一下当灾难来袭时你会做些什么。

在灾难来临前做好准备

当您需要恢复生产群集时,您不会希望第一次探索此主题。 有几件事要提前做,这样灾难恢复就不那么痛苦了,更不用说可能了:

  • 确保 NameNode 将fsimage写入多个位置,如前所述。
  • 确定哪个主机将成为新的 NameNode 位置。 如果这是当前用于 DataNode 和 TaskTracker 的主机,请确保它具有托管 NameNode 所需的正确硬件,并且由于失去这些工作进程而导致的群集性能降低不会太大。
  • 复制core-site.xmlhdfs-site.xml文件,将它们(理想情况下)放在 NFS 位置,然后更新它们以指向新主机。 每次修改当前配置文件时,请记住对这些副本进行相同的更改。
  • slaves文件从 NameNode 复制到新主机或 NFS 共享。 另外,一定要让它保持最新。
  • 了解您将如何处理新主机中的后续故障。 您可能会以多快的速度修复或更换原来出现故障的主机? 在此期间,哪个主机将是 NameNode(和辅助 NameNode)的位置?

准备好的?。 那,我们做吧!

是时候交换到新的 NameNode 主机了

在下面的步骤中,我们将新配置文件保留在挂载到/share/backup的 NFS 共享上,并更改路径以匹配您拥有新文件的位置。 对 grep 也使用不同的字符串;我们使用我们知道的未与集群中的任何其他主机共享的 IP 地址的一部分。

  1. 登录到当前 NameNode 主机并关闭群集。

    $ stop-all.sh
    
    
  2. 停止运行 NameNode 的主机。

    $ sudo poweroff
    
    
  3. 登录到新的 NameNode 主机并确认新的配置文件具有正确的 NameNode 位置。

    $ grep 110 /share/backup/*.xml
    
    
  4. 在新主机上,首先复制slaves文件。

    $ cp /share/backup/slaves Hadoop/conf
    
    
  5. 现在复制更新后的配置文件。

    $ cp /share/backup/*site.xml Hadoop/conf
    
    
  6. 从本地文件系统中删除所有旧的 NameNode 数据。

    $ rm -f /var/Hadoop/dfs/name/*
    
    
  7. 将更新的配置文件复制到群集中的每个节点。

    $ slaves.sh cp /share/backup/*site.xml Hadoop/conf
    
    
  8. 确保每个节点现在都有指向新 NameNode 的配置文件。

    $ slaves.sh grep 110 hadoop/conf/*site.xml
    
    
  9. 启动群集。

    $ start-all.sh
    
    
  10. 从命令行检查 HDFS 是否运行正常。

```scala
$ Hadoop fs ls /

```
  1. 验证是否可以从 Web 用户界面访问 HDFS。

刚刚发生了什么?

首先,我们关闭集群。 这有点不具代表性,因为大多数故障都会看到 NameNode 以一种不太友好的方式死去,但我们不想在本章后面讨论文件系统损坏的问题。

然后,我们关闭旧的 NameNode 主机。 虽然不是绝对必要的,但这是一种很好的方法,可以确保没有人访问旧主机,并且会让您对迁移进行得有多好有不正确的看法。

在跨文件复制之前,我们快速查看一下core-site.xmlhdfs-site.xml,以确保为core-site.xml中的fs.default.dir属性指定了正确的值。

然后,我们准备新主机,首先复制slaves配置文件和集群配置文件,然后从本地目录中删除所有旧的 NameNode 数据。 有关在此步骤中非常小心的信息,请参阅前面的步骤。

接下来,我们使用slaves.sh脚本让集群中的每台主机复制新的配置文件。 我们知道我们的新 NameNode 主机是唯一一个 IP 地址为 110 的主机,因此我们在文件中对其进行 grep,以确保所有主机都是最新的(显然,您的系统需要使用不同的模式)。

在这个阶段,一切都应该很好;我们启动集群,并通过命令行工具和 UI 进行访问,以确认它正在按预期运行。

先别急着庆祝!

请记住,即使成功迁移到新的 NameNode,也还没有完全完成。 您事先决定了如何处理 Second DaryNameNode,以及如果新迁移的主机发生故障,哪个主机将成为新的指定 NameNode 主机。 要为此做好准备,你需要再次检查前面提到的“做好准备”清单,并采取适当的行动。

备注

不要忘记考虑相关故障的可能性。 调查 NameNode 主机故障的原因,以防这是更大问题的开始。

MapReduce 怎么样?

我们没有提到移动 JobTracker,因为这是一个痛苦得多的过程,如第 6 章中所示。 如果您的 NameNode 和 JobTracker 在同一台主机上运行,则需要修改前面的方法,同时保留mapred-site.xml的新副本,该副本在mapred.job.tracker属性中包含新主机的位置。

来个围棋英雄-换到新的 NameNode 主机

执行 NameNode 和 JobTracker 从一台主机到另一台主机的迁移。

管理 HDFS

正如我们在第 6 章中看到的在节点中断时,Hadoop 会自动管理许多在更传统的文件系统上耗费大量精力的可用性问题。 然而,有些事情我们仍然需要意识到。

数据写入位置

正如 NameNode 可以有多个存储通过dfs.name.dir属性指定的fsimage的位置一样,我们前面已经研究过,有一个类似的属性,称为dfs.data.dir,它允许 HDFS 使用主机上的多个数据位置,我们现在来看一下。

这是一种有用的机制,其工作方式与 NameNode 属性非常不同。 如果在dfs.data.dir中指定了多个目录,Hadoop 会将这些目录视为一系列可以并行使用的独立位置。 如果您在文件系统上的不同位置安装了多个物理磁盘或其他存储设备,这将非常有用。 Hadoop 将智能地使用这些多个设备,不仅最大化总存储容量,而且通过跨位置平衡读写来获得最大吞吐量。 正如在存储类型部分中提到的,这是一种以单个磁盘故障导致整个主机故障为代价来最大化这些因素的方法。

使用平衡器

Hadoop 努力工作,以最大化性能和冗余的方式将数据块放在 HDFS 上。 但是,在某些情况下,群集可能会变得不平衡,各个节点上保存的数据之间会有很大差异。 导致这种情况的典型情况是将新节点添加到群集中。 默认情况下,Hadoop 会将新节点视为与所有其他节点一起放置块的候选节点,这意味着它将在相当长的一段时间内保持较低的利用率。 已经停止服务或以其他方式遭受问题的节点也可能比它们的对等节点收集的块数量更少。

Hadoop 包含一个称为平衡器的工具,分别由start-balancer.shstop-balancer.sh脚本启动和停止来处理这种情况。

何时重新平衡

Hadoop 没有任何自动警报,可以提醒您文件系统不平衡。 相反,您需要密切关注hadoop fsckhadoop fsadmin报告的数据,并关注节点之间的不平衡。

实际上,这并不是您通常需要担心的问题,因为 Hadoop 非常擅长管理块放置,并且在添加新硬件或恢复故障节点服务时,您可能只需要考虑运行平衡器来消除严重的不平衡。 但是,为了保持最大的集群健康,让平衡器按计划(例如,每晚)运行以将块平衡保持在指定阈值内的情况并不少见。

MapReduce 管理

正如我们在前面的章中所看到的,MapReduce 框架通常比 HDFS 更能容忍问题和故障。 JobTracker 和 TaskTracker 没有要管理的持久数据,因此,MapReduce 的管理更多的是处理正在运行的作业和任务,而不是服务于框架本身。

命令行作业管理

hadoop job命令行工具是此作业管理的主要界面。 像往常一样,键入以下内容以获取使用摘要:

$ hadoop job --help

该命令的选项通常不言自明;除了检索作业历史记录的某些元素外,它还允许您启动、停止、列出和修改正在运行的作业。 在下一节中,我们将一起探讨其中几个子命令的用法,而不是分别研究每个子命令。

拥有 Go 英雄-命令行工作管理

MapReduce UI 还提供对这些功能子集的访问。 浏览用户界面,了解您可以在 Web 界面上执行哪些操作,以及不可以执行哪些操作。

作业优先级和调度

到目前为止,我们通常对集群运行单个作业并等待其完成。 这隐藏了一个事实,即默认情况下,Hadoop 将后续作业提交放入先进先出(FIFO)队列。 当一个作业完成时,Hadoop 只是开始执行队列中的下一个作业。 除非我们使用我们将在后面部分讨论的替代调度器之一,否则 FIFO 调度器会将整个集群专用于当前正在运行的唯一作业。

对于作业提交模式很少看到作业在队列中等待的小集群来说,这完全没有问题。 但是,如果作业经常在队列中等待,则可能会出现问题。 特别是,FIFO 模型没有考虑作业优先级或所需的资源。 长时间运行但低优先级的作业将在稍后提交的较快的高优先级作业之前执行。

为了解决这种情况,Hadoop 定义了五个作业优先级级别:VERY_HIGHHIGHNORMALLOWVERY_LOW。 作业的默认优先级为NORMAL,但可以使用hadoop job -set-priority命令进行更改。

是时候采取行动了-更改作业优先级并终止作业

让我们通过动态更改作业优先级并观察终止作业的结果来探索作业优先级。

  1. 在群集上启动运行时间相对较长的作业。

    $ hadoop jar hadoop-examples-1.0.4.jar pi 100 1000
    
    
  2. 打开另一个窗口并提交第二个作业。

    $ hadoop jar hadoop-examples-1.0.4.jar wordcount test.txt out1
    
    
  3. 打开另一个窗口并提交第三个窗口。

    $ hadoop jar hadoop-examples-1.0.4.jar wordcount test.txt out2
    
    
  4. List the running jobs.

    $ Hadoop job -list
    
    

    您将在屏幕上看到以下行:

    3 jobs currently running
    JobId  State  StartTime  UserName  Priority  SchedulingInfo
    job_201201111540_0005  1  1326325810671  hadoop  NORMAL  NA
    job_201201111540_0006  1  1326325938781  hadoop  NORMAL  NA
    job_201201111540_0007  1  1326325961700  hadoop  NORMAL  NA
    
    
  5. Check the status of the running job.

    $ Hadoop job -status job_201201111540_0005
    
    

    您将在屏幕上看到以下行:

    Job: job_201201111540_0005
    file: hdfs://head:9000/var/hadoop/mapred/system/job_201201111540_0005/job.xml
    tracking URL: http://head:50030/jobdetails.jsp?jobid=job_201201111540_000
    map() completion: 1.0
    reduce() completion: 0.32666665
    Counters: 18
    
    
  6. 将上次提交的作业的优先级提高到VERY_HIGH

    $ Hadoop job -set-priority job_201201111540_0007 VERY_HIGH
    
    
  7. 取消当前正在运行的作业。

    $ Hadoop job -kill job_201201111540_0005
    
    
  8. 查看其他作业以查看哪些作业开始处理。

刚刚发生了什么?

我们在集群上启动了一个作业,然后将另外两个作业排队,使用hadoop job -list确认排队的作业按预期顺序排列。 hadoop job -list all命令将列出已完成的作业和当前作业,hadoop job -history将允许我们更详细地检查作业及其任务。 为了确认提交的作业正在运行,除了作业计数器之外,我们还使用hadoop job -status获取作业的当前映射和减少任务完成状态。

然后,我们使用hadoop job -set-priority提高队列中当前最后一个作业的优先级。

在使用hadoop job -kill中止当前运行的作业之后,我们确认了下一个执行的优先级较高的作业,即使队列中剩余的作业是预先提交的。

备用调度器

手动修改 FIFO 队列中的作业优先级确实有效,但它需要主动监视和管理作业队列。 如果我们考虑这个问题,我们会遇到这种困难的原因是 Hadoop 将整个集群专用于正在执行的每个作业。

Hadoop 提供了两个额外的作业调度器,它们采用不同的方法,并在多个并发执行的作业之间共享集群。 还有一个插件机制,可以用来添加额外的调度器。 请注意,这种类型的资源共享是概念上简单但实际上非常复杂的问题之一,也是许多学术研究的领域。 我们的目标是在遵守相对优先级概念的同时,不仅在某个时间点,而且在更长的时间内最大限度地分配资源。

容量调度器

Capacity Scheduler使用向其提交作业的多个作业队列(可以对其应用访问控制),每个作业队列都分配有一部分集群资源。 例如,您可以让一个队列用于分配 90%的群集的大型长期运行作业,另一个队列用于分配剩余 10%的较小的高优先级作业。 如果两个队列都提交了作业,将按此比例分配集群资源。

但是,如果一个队列为空,而另一个队列有作业要执行,则 Capacity Scheduler 会将空队列的容量临时分配给忙碌的队列。 一旦作业提交到空队列,它将在当前运行的任务完成执行时重新获得其容量。 该方法在期望的资源分配和防止长时间未使用的容量之间提供了合理的平衡。

虽然默认情况下禁用了 Capacity Scheduler,但它支持每个队列中的作业优先级。 如果高优先级作业是在低优先级作业之后提交的,则在容量可用时,其任务将优先于其他作业进行调度。

公平调度器

Fair Scheduler将集群分割成作业提交到的池;用户和池之间通常存在关联。 虽然默认情况下每个池都会获得相等的群集份额,但可以对此进行修改。

在每个池中,默认模式是在提交到该池的所有作业之间共享该池。 因此,如果集群被分成 Alice 和 Bob 的池,这两个池分别提交三个作业,那么集群将并行执行所有六个作业。 可以对池中运行的并发作业数量设置总限制,因为同时运行太多作业可能会产生大量临时数据,并提供整体效率低下的处理。

与 Capacity Scheduler 一样,如果一个池为空,公平调度器将向其他池过度分配群集容量,然后在池接收作业时回收该容量。 它还支持池中的作业优先级,以便优先调度高优先级作业的任务,而不是低优先级作业的任务。

启用备用调度程序

每个备用调度器在 Hadoop 安装的contrib目录内的capacitySchedulerfairScheduler目录中以 JAR 文件的形式提供。 要启用调度程序,要么将其 JAR 添加到hadoop/lib目录,要么显式地将其放在类路径上。 请注意,每个调度程序都需要自己的一组属性来配置其使用情况。 有关更多详细信息,请参阅各自的文档。

何时使用替代调度程序

备用调度器非常有效,但在小型集群或那些不需要确保多个作业并发或执行晚到但优先级高的作业的集群上并不真正需要。 每个服务器都有多个配置参数,需要进行调整才能获得最佳的集群利用率。 但对于具有多个用户和不同作业优先级的任何大型群集,它们可能是必不可少的。

缩放

您有数据,并且有一个正在运行的 Hadoop 集群;现在,您获得了更多的前者,也需要更多的后者。 我们反复说过,Hadoop 是一个易于扩展的系统。 因此,让我们增加一些新的容量。

向本地 Hadoop 群集添加容量

希望在这一点上,您应该对向正在运行的集群添加另一个节点的想法感到非常不满意。 在第 6 章中,当事情中断时,我们不断地终止和重新启动节点。 添加新节点实际上没有什么不同,您只需执行以下步骤:

  1. 在主机上安装 Hadoop。
  2. 设置第 2 章设置和运行中所示的环境变量。
  3. 将配置文件复制到安装上的conf目录。
  4. 将主机的 DNS 名称或 IP 地址添加到通常从其运行命令(如slaves.sh或群集启动/停止脚本)的节点上的slaves文件。

就这样!

有围棋英雄-添加节点和运行平衡器

尝试添加新节点的过程,然后检查 HDFS 的状态。 如果不平衡,用平衡器来修理。 为了帮助最大化效果,在添加新节点之前,请确保 HDFS 上有合理数量的数据。

向电子病历工作流添加容量

如果您正在使用 Elastic MapReduce,对于非持久性的集群,伸缩的概念并不总是适用。 由于您指定了每次设置作业流时所需的主机数量和类型,因此只需确保群集大小适合于要执行的作业。

展开正在运行的作业流

但是,有时您可能需要更快地完成一个长期运行的作业。 在这种情况下,您可以向正在运行的作业流中添加更多节点。 回想一下,EMR 有三种不同类型的节点:NameNode 和 JobTracker 的主节点、HDFS 的核心节点和 MapReduce 工作者的任务节点。 在这种情况下,您可以添加其他任务节点来帮助处理 MapReduce 作业。

另一个场景是,您定义了一个作业流,其中包含一系列 MapReduce 作业,而不是只有一个。 电子病历现在允许在这样一系列步骤之间修改作业流。 这样做的好处是,每个作业都有一个定制的硬件配置,可以更好地控制性能与成本之间的平衡。

EMR 的规范模型是作业流从 S3 提取其源数据,在临时 EMR Hadoop 集群上处理该数据,然后将结果写回 S3。 但是,如果您有一个需要频繁处理的非常大的数据集,那么来回复制数据可能会变得太耗时。 在这种情况下可以采用的另一种模型是在作业流中使用持久性 Hadoop 集群,该作业流的大小已经有足够的核心节点来在 HDFS 上存储所需的数据。 在执行处理时,如前所示,通过向作业流分配更多任务节点来增加容量。

备注

这些调整运行作业流大小的任务目前无法从 AWS 控制台获得,需要通过 API 或命令行工具执行。

摘要

本章介绍了如何构建、维护和扩展 Hadoop 群集。 特别是,我们了解了在哪里可以找到 Hadoop 配置属性的默认值,以及如何在每个作业级别以编程方式设置它们。 我们了解了如何为群集选择硬件,以及在承诺购买之前了解您可能的工作负载的价值,以及 Hadoop 如何通过机架感知利用主机的物理位置感知来优化其数据块放置策略。

然后,我们了解了默认 Hadoop 安全模型是如何工作的,它的弱点以及如何缓解它们,如何降低我们在中介绍的 NameNode 故障风险,以及如何在灾难来袭时切换到新的 NameNode 主机。 我们了解了有关数据块副本放置的更多信息,了解了群集如何变得不平衡,以及如果不平衡该怎么办。

我们还了解了 MapReduce 作业调度的 Hadoop 模型,了解了作业优先级如何修改行为、Capacity Scheduler 和 Fair Scheduler 如何提供更复杂的方式来跨多个并发作业提交管理集群资源,以及如何使用新容量扩展集群。

本书对核心 Hadoop 的探索到此结束。 在接下来的章节中,我们将介绍构建在 Hadoop 之上的其他系统和工具,以提供更复杂的数据视图以及与其他系统的集成。 我们将通过使用配置单元从 HDFS 中数据的关系视图开始。

八、Hive 数据关系视图

MapReduce 是一个强大的范例,它支持复杂的数据处理,可以揭示有价值的见解。 然而,在将处理分析分解为一系列映射和减少步骤的模型方面,它确实需要不同的思维方式以及一些培训和经验。 有几个构建在 Hadoop 之上的产品可以为 HDFS 中保存的数据提供更高级别或更熟悉的视图。 本章将介绍其中最流行的工具之一配置单元

在本章中,我们将介绍:

  • Hive 是什么?您可能想要使用它的原因
  • 如何安装和配置配置单元
  • 使用配置单元对 UFO 数据集进行类 SQL 分析
  • 配置单元如何近似关系数据库的常见功能,如联接和视图
  • 如何在非常大的数据集上高效地使用配置单元
  • 配置单元如何允许将用户定义的函数合并到其查询中
  • Hive 如何补充另一个常用工具--PIG

Hive 概述

配置单元是一个数据仓库,它使用 MapReduce 分析存储在 HDFS 上的数据。 特别是,它提供了一种名为HiveQL的查询语言,与常见的结构化查询语言(SQL)标准非常相似。

为什么使用 Hive?

开发 MapReduce 程序中,我们介绍了 Hadoop 流,并解释了流的一大好处是它如何在 MapReduce 作业的开发中实现更快的周转。 Hive 将这一点更进了一步。 它提供了一种基于行业标准 SQL 的查询语言,而不是提供一种更快速地开发 MAP 和 Reduce 任务的方法。 HIVE 接受这些 HiveQL 语句,并立即自动将查询转换为一个或多个 MapReduce 作业。 然后,它执行整个 MapReduce 程序并将结果返回给用户。 Hadoop 流减少了所需的代码/编译/提交周期,而配置单元则完全删除了它,只需要组合 HiveQL 语句。

这种与 Hadoop 的接口不仅加快了从数据分析中产生结果所需的时间,而且大大拓宽了可以使用 Hadoop 和 MapReduce 的用户范围。 任何熟悉 SQL 的人都可以使用配置单元,而不需要软件开发技能。

这些属性的组合是,配置单元通常用作业务和数据分析师对存储在 HDFS 上的数据执行即席查询的工具。 直接使用 MapReduce 需要在执行作业之前编写 map 和 Reduce 任务,这意味着从设想可能的查询到其执行有必要的延迟。 有了 Hive,数据分析师可以在不需要软件开发人员持续参与的情况下改进 HiveQL 查询。 当然,也有操作和实践上的限制(一个写得不好的查询无论采用哪种技术都是低效的),但总的原则是令人信服的。

谢谢,Facebook!

正如我们早些时候感谢 Google、Yahoo!和 Doug Cutting 为 Hadoop 以及激发其灵感的技术所做的贡献一样,现在我们必须直接感谢 Facebook。

HIVE 是由 Facebook 数据团队开发的,在内部使用后,它被贡献给了 Apache 软件基金会,并作为开源软件免费提供。 它的主页是http://hive.apache.org

设置配置单元

在本节中,我们将演练下载、安装和配置配置单元的操作。

必备条件

与 Hadoop 不同,没有配置单元主机、从属或节点。 HIVE 作为客户端应用运行,该应用处理 HiveQL 查询,将其转换为 MapReduce 作业,并将其提交到 Hadoop 群集。

虽然有一种适合小型作业和开发使用的模式,但通常情况是,配置单元需要一个现有的运行正常的 Hadoop 集群。

就像其他 Hadoop 客户端不需要在实际群集节点上执行一样,配置单元可以在满足以下条件的任何主机上执行:

  • 主机上安装了 Hadoop(即使没有进程在运行)
  • 设置了HADOOP_HOME环境变量并指向 Hadoop 安装位置
  • ${HADOOP_HOME}/bin目录添加到系统或用户路径

获取 Hive

您应该从http://hive.apache.org/releases.html下载最新的稳定配置单元版本。

位于Hadoop的配置单元入门指南将提供有关版本兼容性的建议,但作为一般原则,您应该期待最新稳定版本的配置单元、http://cwiki.apache.org/confluence/display/Hive/GettingStarted 和 JAVA 能够协同工作。

行动时间-安装 Hive

现在让我们设置 Hive,这样我们就可以开始使用它了。

  1. 下载最新稳定版本的配置单元,并将其移动到您希望安装的位置:

    $ mv hive-0.8.1.tar.gz /usr/local
    
    
  2. 解压缩包:

    $ tar –xzf hive-0.8.1.tar.gz
    
    
  3. HIVE_HOME变量设置为安装目录:

    $ export HIVE_HOME=/usr/local/hive
    
    
  4. 将配置单元主目录添加到 PATH 变量:

    $ export PATH=${HIVE_HOME}/bin:${PATH}
    
    
  5. 在 HDFS 上创建配置单元所需的目录:

    $ hadoop fs -mkdir /tmp
    $ hadoop fs -mkdir /user/hive/warehouse
    
    
  6. 使这两个目录组都可写:

    $ hadoop fs -chmod g+w /tmp
    $ hadoop fs -chmod g+w /user/hive/warehouse
    
    
  7. Try to start Hive:

    $ hive
    
    

    您将收到以下响应:

    Logging initialized using configuration in jar:file:/opt/hive-0.8.1/lib/hive-common-0.8.1.jar!/hive-log4j.properties
    Hive history file=/tmp/hadoop/hive_job_log_hadoop_201203031500_480385673.txt
    hive>
    
    
  8. 退出配置单元交互式外壳:

    $ hive> quit;
    
    

刚刚发生了什么?

下载最新的稳定配置单元版本后,我们将其复制到所需位置并解压缩存档文件。 这创建了一个目录hive-<version>

类似地,正如我们之前定义的HADOOP_HOME并将安装中的bin目录添加到 PATH 变量,然后我们对 HIVE_HOME 及其 bin 目录执行了类似的操作。

备注

请记住,为了避免每次登录时都必须设置这些变量,请将它们添加到您的 Shell 登录脚本中,或者添加到您希望使用配置单元时获取的单独配置脚本中。

然后,我们在 HDFS 上创建了配置单元需要的两个目录,并更改了它们的属性,使它们可以分组写入。 默认情况下,配置单元将在/tmp目录中写入查询执行期间创建的临时数据,并将输出数据放在此位置。 /user/hive/warehouse目录是配置单元存储写入其表的数据的位置。

在所有这些设置之后,我们运行hive命令,成功的安装将产生类似于上面提到的输出。 运行不带参数的hive命令进入交互式 shell;hive>提示符类似于关系数据库交互工具中熟悉的sql>mysql>提示符。

然后,我们通过键入quit;退出交互式 shell。 请注意尾随的分号;。 如前所述,HiveQL 非常类似于 SQL,并且遵循所有命令必须以分号结束的约定。 按不带分号的Enter将允许在后续行上继续执行命令。

使用配置单元

通过我们的配置单元安装,我们现在将导入和分析第 4 章开发 MapReduce 程序中介绍的 UFO 数据集。

将任何新数据导入配置单元时,通常有三个阶段的流程:

  1. 创建要向其中导入数据的表的规范。
  2. 将数据导入到创建的表中。
  3. 对该表执行 HiveQL 查询。

对于那些有关系数据库经验的人来说,这个过程应该非常熟悉。 HIVE 提供了数据的结构化查询视图,要实现这一点,我们必须首先定义表列的规范,并将数据导入到表中,然后才能执行任何查询。

备注

我们假定您对 SQL 有一定的熟悉程度,并且将更多地关注如何使用配置单元完成工作,而不是详细解释特定的 SQL 构造。 SQL 参考对于那些对该语言不太熟悉的人来说可能很方便,但我们将确保您知道每条语句的作用,即使细节需要更深入的 SQL 知识。

操作时间-为 UFO 数据创建表

执行下面的步骤,为 UFO 数据创建表:

  1. 启动配置单元交互式外壳:

    $ hive
    
    
  2. Create a table for the UFO data set, splitting the statement across multiple lines for easy readability:

    hive> CREATE TABLE ufodata(sighted STRING, reported STRING, sighting_location STRING,    > shape STRING, duration STRING, 
    description STRING COMMENT 'Free text description') 
    COMMENT 'The UFO data set.' ;
    
    

    完成后,您应该看到以下几行:

    OK
    Time taken: 0.238 seconds
    
    
  3. List all existing tables:

    hive> show tables;
    
    

    您将收到以下输出:

    OK
    ufodata
    Time taken: 0.156 seconds
    
    
  4. Show tables matching a regular expression:

    hive> show tables '.*data';
    
    

    您将收到以下输出:

    OK
    ufodata
    Time taken: 0.065 seconds
    
    
  5. Validate the table specification:

    hive> describe ufodata;
    
    

    您将收到以下输出:

    OK
    sighted  string 
    reported  string 
    sighting_location  string 
    shape  string 
    duration  string 
    description  string  Free text description
    Time taken: 0.086 seconds
    
    
  6. Display a more detailed description of the table:

    hive> describe extended ufodata;
    
    

    您将收到以下输出:

    OK
    sighted  string 
    reported  string 
    …
    Detailed Table Information  Table(tableName:ufodata, dbName:default, owner:hadoop, createTime:1330818664, lastAccessTime:0, retention:0, 
    …
    …location:hdfs://head:9000/user/hive/warehouse/ufodata, inputFormat:org.apache.hadoop.mapred.TextInputFormat, outputFormat:org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat, compressed:false, numBuckets:-1, 
    
    

刚刚发生了什么?

启动交互式配置单元解释器后,我们使用CREATE TABLE命令定义 UFO 数据表的结构。 与标准 SQL 一样,这要求表中的每一列都有一个名称和数据类型。 HiveQL 还为每列和整个表提供了可选的注释,如前面所示,我们在其中添加了一列和一张表注释。

对于 UFO 数据,我们使用STRING作为数据类型;HiveQL 和 SQL 一样,支持多种数据类型:

  • 布尔类型BOOLEAN
  • 整数类型TINYINTINTBIGINT
  • 浮点类型FLOATDOUBLE
  • 文本类型STRING

在创建表之后,我们使用SHOW TABLES语句来验证表是否已创建。 此命令列出所有表格,在本例中,我们的新 UFO 表格是系统中唯一的表格。

然后,我们在SHOW TABLES上使用一个变量,该变量接受一个可选的 Java 正则表达式来匹配表名。 在这种情况下,输出与前面的命令相同,但在包含大量表的系统中-尤其是当您不知道确切名称时-这个变体可能非常有用。

备注

我们已经看到该表存在,但是我们还没有验证它是否正确创建。 接下来,我们使用DESCRIBE TABLE命令来显示命名表的规范。 我们看到所有内容都是指定的(尽管注意,此命令没有显示表注释),然后使用DESCRIBE TABLE EXTENDED变体获取有关表的更多信息。

虽然有几个有趣的地方,但我们省略了大部分最终输出。 注意:输入格式指定为TextInputFormat;默认情况下,配置单元将假定插入到表中的任何 HDFS 文件都存储为文本文件。

我们还看到,表数据将存储在前面创建的/user/hive/warehouseHDFS 目录下的一个目录中。

提示

关于区分大小写的说明:

与 SQL 一样,HiveQL 在关键字、列或表名方面不区分大小写。 按照惯例,SQL 语句使用大写作为 SQL 语言关键字,当在文件中使用 HiveQL 时,我们通常会遵循这一点,如下所示。 然而,在输入交互式命令时,我们会经常选择阻力最小的行,并使用小写。

动作时间-插入 UFO 数据

现在我们已经创建了一个表,让我们将 UFO 数据加载到其中。

  1. 将 UFO 数据文件复制到 HDFS:

    $ hadoop fs -put ufo.tsv /tmp/ufo.tsv
    
    
  2. Confirm that the file was copied:

    $ hadoop fs -ls /tmp
    
    

    您将收到以下响应:

    Found 2 items
    drwxrwxr-x   - hadoop supergroup          0 … 14:52 /tmp/hive-hadoop
    -rw-r--r--   3 hadoop supergroup   75342464 2012-03-03 16:01 /tmp/ufo.tsv
    
    
  3. 进入 Hive 互动外壳:

    $ hive
    
    
  4. Load the data from the previously copied file into the ufodata table:

    hive> LOAD DATA INPATH '/tmp/ufo.tsv' OVERWRITE INTO TABLE ufodata;
    
    

    您将收到以下响应:

    Loading data to table default.ufodata
    Deleted hdfs://head:9000/user/hive/warehouse/ufodata
    OK
    Time taken: 5.494 seconds
    
    
  5. 退出配置单元外壳:

    hive> quit;
    
    
  6. Check the location from which we copied the data file:

    $ hadoop fs -ls /tmp
    
    

    您将收到以下响应:

    Found 1 items
    drwxrwxr-x   - hadoop supergroup          0 … 16:10 /tmp/hive-hadoop
    
    

刚刚发生了什么?

我们首先将之前在第 4 章开发 MapReduce 程序中使用的 UFO 目击事件的制表符分隔文件复制到 HDFS 上。 在 HDFS 上验证了该文件是否存在之后,我们启动了配置单元交互式 shell,并使用LOAD DATA命令将该文件加载到ufodata表中。

因为我们使用的是 HDFS 上已有的文件,所以路径仅由INPATH指定。 我们可以使用LOCAL INPATH直接从本地文件系统上的文件加载(不需要以前的显式 HDFS 副本)。

我们指定了OVERWRITE语句,该语句将在加载新数据之前删除表中的所有现有数据。 这显然应该小心使用,从命令的输出可以看出,使用OVERWRITE删除了保存表数据的目录。

注意:该命令的执行时间仅为 5 秒多一点,比将 UFO 数据文件复制到 HDFS 所需的时间要长得多。

备注

虽然我们在本例中指定了一个显式文件,但通过将目录指定为INPATH位置,可以使用一条语句加载多个文件;在这种情况下,目录中的所有文件都将加载到表中。

退出配置单元外壳后,我们再次查看将数据文件复制到的目录,发现它已经不在那里了。 如果LOAD语句被赋予 HDFS 上数据的路径,它不会简单地将其复制到/user/hive/datawarehouse中,而是会将其移动到那里。 如果要分析其他应用使用的 HDFS 上的数据,请创建副本或使用稍后介绍的EXTERNAL机制。

验证数据

现在我们已经将数据加载到表中,最好执行一些快速验证查询,以确认一切都符合预期。 有时,我们的初始表定义被证明是不正确的。

执行操作的时间-验证表

执行某些初始验证的最简单方法是执行一些摘要查询来验证导入。 这类似于我们在第 4 章开发 MapReduce 程序中使用 Hadoop 流的活动类型。

  1. Instead of using the Hive shell, pass the following HiveQL to the hive command-line tool to count the number of entries in the table:

    $ hive -e "select count(*) from ufodata;"
    
    

    您将收到以下响应:

    Total MapReduce jobs = 1
    Launching Job 1 out of 1
    …
    Hadoop job information for Stage-1: number of mappers: 1; number of reducers: 1
    2012-03-03 16:15:15,510 Stage-1 map = 0%,  reduce = 0%
    2012-03-03 16:15:21,552 Stage-1 map = 100%,  reduce = 0%
    2012-03-03 16:15:30,622 Stage-1 map = 100%,  reduce = 100%
    Ended Job = job_201202281524_0006
    MapReduce Jobs Launched: 
    Job 0: Map: 1  Reduce: 1   HDFS Read: 75416209 HDFS Write: 6 SUCESS
    Total MapReduce CPU Time Spent: 0 msec
    OK
    61393
    Time taken: 28.218 seconds
    
    
  2. Display a sample of five values for the sighted column:

    $ hive -e  "select sighted from ufodata limit 5;"
    
    

    您将收到以下响应:

    Total MapReduce jobs = 1
    Launching Job 1 out of 1
    …
    OK
    19951009  19951009   Iowa City, IA      Man repts. witnessing &quot;flash, followed by a classic UFO, w/ a tailfin at back.&quot; Red color on top half of tailfin. Became triangular.
    19951010  19951011   Milwaukee, WI    2 min.  Man  on Hwy 43 SW of Milwaukee sees large, bright blue light streak by his car, descend, turn, cross road ahead, strobe. Bizarre!
    19950101  19950103   Shelton, WA      Telephoned Report:CA woman visiting daughter witness discs and triangular ships over Squaxin Island in Puget Sound. Dramatic.  Written report, with illustrations, submitted to NUFORC.
    19950510  19950510   Columbia, MO    2 min.  Man repts. son&apos;s bizarre sighting of small humanoid creature in back yard.  Reptd. in Acteon Journal, St. Louis UFO newsletter.
    19950611  19950614   Seattle, WA      Anonymous caller repts. sighting 4 ufo&apos;s in NNE sky, 45 deg. above horizon.  (No other facts reptd.  No return tel. #.)
    Time taken: 11.693 seconds
    
    

刚刚发生了什么?

在本例中,我们使用hive -e命令将 HiveQL 直接传递给配置单元工具,而不是使用交互式 shell。 交互式外壳在执行一系列配置单元操作时非常有用。 对于简单的语句,使用此方法并将查询字符串直接传递给命令行工具通常更为方便。 这也表明可以像任何其他 Unix 工具一样从脚本中调用配置单元。

备注

使用hive –e时,HiveQL 字符串没有必要以分号结尾,但如果您和我一样,这个习惯很难改掉。 如果要在单个字符串中包含多个命令,显然必须用分号分隔。

第一个查询的结果是 61393 条,与我们以前使用直接 MapReduce 分析 UFO 数据集时看到的记录数量相同。 这告诉我们整个数据集确实加载到了表中。

然后,我们执行第二个查询来选择表中第一列的五个值,该查询应该返回一个包含五个日期的列表。 但是,输出反而包括已加载到第一列中的整个记录。

问题是,尽管我们依赖于配置单元将数据文件加载为文本文件,但我们没有考虑列之间的分隔符。 我们的文件是制表符分隔的,但是默认情况下,配置单元希望它的输入文件具有由 ASCII 代码 00(CONTROL-A)分隔的字段。

执行操作的时间-使用正确的列分隔符重新定义表格

让我们按如下方式修改我们的表规范:

  1. 将以下文件创建为commands.hql

    DROP TABLE ufodata ;
    CREATE TABLE ufodata(sighted string, reported string, sighting_location string,
    shape string, duration string, description string)
    ROW FORMAT DELIMITED
    FIELDS TERMINATED BY '\t' ;
    LOAD DATA INPATH '/tmp/ufo.tsv' OVERWRITE INTO TABLE ufodata ;
    
  2. 将数据文件复制到 HDFS:

    $ hadoop fs -put ufo.tsv /tmp/ufo.tsv
    
    
  3. Execute the HiveQL script:

    $ hive -f commands.hql 
    
    

    您将收到以下响应:

    OK
    Time taken: 5.821 seconds
    OK
    Time taken: 0.248 seconds
    Loading data to table default.ufodata
    Deleted hdfs://head:9000/user/hive/warehouse/ufodata
    OK
    Time taken: 0.285 seconds
    
    
  4. Validate the number of rows in the table:

    $ hive -e "select count(*) from ufodata;"
    
    

    您将收到以下响应:

    OK
    61393
    Time taken: 28.077 seconds
    
    
  5. Validate the contents of the reported column:

    $ hive -e "select reported from ufodata limit 5"
    
    

    您将收到以下响应:

    OK
    19951009
    19951011
    19950103
    19950510
    19950614
    Time taken: 14.852 seconds
    
    

刚刚发生了什么?

在本例中,我们介绍了第三种调用 HiveQL 命令的方法。 除了使用交互式外壳或将查询字符串传递给配置单元工具之外,我们还可以让配置单元读取并执行包含一系列 HiveQL 语句的文件的内容。

我们首先创建了这样一个文件,该文件删除旧的表,创建一个新的表,然后将数据文件加载到其中。

与表规范的主要区别是ROW FORMATFIELDS TERMINATED BY语句。 我们需要这两个命令,因为第一个命令告诉配置单元该行包含多个分隔字段,而第二个命令指定实际的分隔符。 从这里可以看到,我们既可以使用显式的 ASCII 码,也可以使用通用的标记,比如\tfor tab。

备注

请小心使用分隔符规格,因为它必须精确且区分大小写。 不要像我最近那样,因为不小心写了\T而不是\t而浪费了几个小时。

在运行脚本之前,我们再次将数据文件复制到 HDFS 上-前一个副本被DELETE语句删除-然后使用hive -f执行 HiveQL 文件。

与前面一样,我们然后执行两个简单的SELECT语句,首先计算表中的行数,然后从指定的列中提取少量行的特定值。

正如预期的那样,总行计数与以前相同,但是第二条语句现在生成看起来正确的数据,显示行现在被正确地拆分成它们的组成字段。

Hive 表--是真的还是假的?

如果仔细观察前面示例中各种命令所花费的时间,您会发现一种乍一看可能很奇怪的模式。 将数据加载到表中所需的时间与创建表规范的时间差不多,但即使是简单地计算所有 ROW 语句的时间也要长得多。 输出还显示,表创建和数据加载实际上并不会导致执行 MapReduce 作业,这就解释了执行时间非常短的原因。

将数据加载到配置单元表中时,该过程可能与传统关系数据库的预期过程不同。 尽管配置单元将数据文件复制到其工作目录中,但它实际上并不会在此时将输入数据处理成行。 相反,它所做的是围绕数据创建元数据,然后由后续 HiveQL 查询使用。

因此,CREATE TABLELOAD DATA语句本身并不真正创建具体的表数据,而是生成在配置单元生成 MapReduce 作业以访问表中概念性存储的数据时将使用的元数据。

执行操作的时间-从现有文件创建表

到目前为止,我们已经将数据直接从配置单元有效控制的文件加载到配置单元中。 但是,也可以创建表格来模拟配置单元外部文件中保存的数据。 当我们希望能够对外部应用写入和管理的数据或需要保存在配置单元仓库目录之外的目录中的数据执行配置单元处理时,这会很有用。 这样的文件不会移动到配置单元仓库目录,也不会在删除表时删除。

  1. 将以下内容保存到名为states.hql的文件中:

    CREATE EXTERNAL TABLE states(abbreviation string, full_name string)
    ROW FORMAT DELIMITED
    FIELDS TERMINATED BY '\t'
    LOCATION '/tmp/states' ;
    
  2. Copy the data file onto HDFS and confirm its presence afterwards:

    $ hadoop fs -put states.txt /tmp/states/states.txt
    $ hadoop fs -ls /tmp/states
    
    

    您将收到以下响应:

    Found 1 items
    -rw-r--r--   3 hadoop supergroup        654 2012-03-03 16:54 /tmp/states/states.txt
    
    
  3. Execute the HiveQL script:

    $ hive -f states.hql
    
    

    您将收到以下响应:

    Logging initialized using configuration in jar:file:/opt/hive-0.8.1/lib/hive-common-0.8.1.jar!/hive-log4j.properties
    Hive history file=/tmp/hadoop/hive_job_log_hadoop_201203031655_1132553792.txt
    OK
    Time taken: 3.954 seconds
    OK
    Time taken: 0.594 seconds
    
    
  4. Check the source data file:

    $ hadoop fs -ls /tmp/states
    
    

    您将收到以下响应:

    Found 1 items
    -rw-r--r--   3 hadoop supergroup        654 … /tmp/states/states.txt
    
    
  5. Execute a sample query against the table:

    $ hive -e "select full_name from states where abbreviation like 'CA'"
    
    

    您将收到以下响应:

    Logging initialized using configuration in jar:file:/opt/hive-0.8.1/lib/hive-common-0.8.1.jar!/hive-log4j.properties
    Hive history file=/tmp/hadoop/hive_job_log_hadoop_201203031655_410945775.txt
    Total MapReduce jobs = 1
    ...
    OK
    California
    Time taken: 15.75 seconds
    
    

刚刚发生了什么?

用于创建外部表的 HiveQL 语句与我们之前使用的CREATE TABLE的形式仅略有不同。 关键字EXTERNAL指定表存在于配置单元不控制的资源中,LOCATION子句指定要在何处找到源文件或目录。

创建 HiveQL 脚本后,我们将源文件复制到 HDFS。 对于该表,我们使用了开发 MapReduce 程序中的数据文件,该文件将美国各州映射到它们常见的两个字母的缩写。

在确认文件位于 HDFS 上的预期位置后,我们执行查询以创建表,并再次检查源文件。 与以前将源文件移动到/user/hive/warehouse目录的表创建不同,states.txt文件仍然位于它被复制到的 HDFS 位置。

最后,我们对该表执行查询,以确认它已填充源数据,预期结果证实了这一点。 这突出了这种形式的CREATE TABLE的另一个不同之处;对于前面的非外部表,表创建语句不会将任何数据摄取到表中,后续的LOAD DATA或(稍后我们将看到)INSERT语句执行实际的表填充。 使用包含LOCATION规范的表定义,我们可以在单个语句中创建表和获取数据。

我们现在在 Hive 中有两个表;较大的表包含 UFO 目击数据,较小的表将美国各州的缩写映射到它们的全名。 使用第二个表中的数据来丰富前一个表中的 Location 列不是一个有用的组合吗?

操作时间-执行联接

联接是 SQL 中非常常用的一种工具,尽管对于新手来说有时看起来有点吓人。 本质上,JOIN允许基于条件语句将多个表中的行逻辑组合在一起。 HIVE 对联接有丰富的支持,我们现在将研究这一点。

  1. 将以下内容创建为join.hql

    SELECT t1.sighted, t2.full_name
    FROM ufodata t1 JOIN states t2
    ON (LOWER(t2.abbreviation) = LOWER(SUBSTR( t1.sighting_location, (LENGTH(t1.sighting_location)-1)))) 
    LIMIT 5 ;
    
  2. Execute the query:

    $ hive -f join.hql
    
    

    您将收到以下响应:

    OK
    20060930  Alaska
    20051018  Alaska
    20050707  Alaska
    20100112  Alaska
    20100625  Alaska
    Time taken: 33.255 seconds
    
    

刚刚发生了什么?

实际的join查询相对简单;我们希望提取一系列记录的目击日期和位置,但我们希望将其映射到州全名,而不是原始位置字段。 我们创建的 HiveQL 文件执行这样的查询。 联接本身由标准的JOIN关键字指定,匹配条件包含在ON子句中。

配置单元的限制使情况变得复杂,因为它只支持等联接,即 ON 子句包含相等检查的连接。 不可能使用>?<等运算符或我们更愿意在这里使用的LIKE关键字来指定联接条件。

相反,因此,我们有机会引入几个配置单元的内置函数,特别是那些将字符串转换为小写(LOWER)、从字符串中提取子字符串(SUBSTR)以及返回字符串中的字符数(LENGTH)的函数。

我们知道,大多数位置条目的格式为“City,State_abbreitation(城市,州 _ 缩写)”。 因此,我们使用SUBSTR提取字符串中倒数第三个和第二个字符,使用length计算索引。 我们通过LOWER将州缩写和提取的字符串都转换为小写,因为我们不能假设视觉表中的所有条目都将正确地使用统一的大写。

执行该脚本后,我们将获得预期的输出样本行,其中确实包括目击日期和州全名,而不是缩写。

请注意,使用了LIMIT子句,该子句仅限制查询将返回多少输出行。 这也表明 HiveQL 与 SQL 方言最相似,比如 MySQL 等开源数据库中的方言。

此示例显示了内部联接;配置单元还支持左和右外联接以及左半联接。 在配置单元中使用联接有许多微妙之处(例如前面提到的等值联接限制),如果您可能要使用联接,尤其是在使用非常大的表时,您真的应该通读一下配置单元主页上的文档。

备注

这不仅仅是对配置单元的批评;联接是非常强大的工具,但可以公平地说,编写糟糕的联接或在忽视关键约束的情况下创建的联接比任何其他类型的 SQL 查询都导致更多的关系数据库陷入停滞。

有一个围棋英雄-改进连接以使用正则表达式

除了我们之前使用的字符串函数,配置单元还具有RLIKEREGEXP_EXTRACT等函数,这些函数直接支持类似 Java 的正则表达式操作。 使用正则表达式重写前面的联接规范,以创建更准确、更优雅的联接语句。

配置单元和 SQL 视图

配置单元支持的另一个强大的 SQL 特性是视图。 当逻辑表的内容由SELECT语句指定,并且随后可以针对底层数据的这个动态视图(因此而得名)执行后续查询时,这些方法非常有用。

使用视图执行操作的时间

我们可以使用视图来隐藏底层查询复杂性,例如前面的连接示例。 现在让我们创建一个视图来实现这一点。

  1. 将以下内容创建为view.hql

    CREATE VIEW IF NOT EXISTS usa_sightings (sighted, reported, shape, state)
    AS select t1.sighted, t1.reported, t1.shape, t2.full_name
    FROM ufodata t1 JOIN states t2
    ON (LOWER(t2.abbreviation) = LOWER(substr( t1.sighting_location, (LENGTH(t1.sighting_location)-1)))) ;
    
  2. Execute the script:

    $ hive -f view.hql
    
    

    您将收到以下响应:

    Logging initialized using configuration in jar:file:/opt/hive-0.8.1/lib/hive-common-0.8.1.jar!/hive-log4j.properties
    Hive history file=/tmp/hadoop/hive_job_log_hadoop_201203040557_1017700649.txt
    OK
    Time taken: 5.135 seconds
    
    
  3. Execute the script again:

    $ hive -f view.hql
    
    

    您将收到以下响应:

    Logging initialized using configuration in jar:file:/opt/hive-0.8.1/lib/hive-common-0.8.1.jar!/hive-log4j.properties
    Hive history file=/tmp/hadoop/hive_job_log_hadoop_201203040557_851275946.txt
    OK
    Time taken: 4.828 seconds
    
    
  4. Execute a test query against the view:

    $ hive -e "select count(state) from usa_sightings where state = 'California'"
    
    

    您将收到以下响应:

    Logging initialized using configuration in jar:file:/opt/hive-0.8.1/lib/hive-common-0.8.1.jar!/hive-log4j.properties
    Hive history file=/tmp/hadoop/hive_job_log_hadoop_201203040558_1729315866.txt
    Total MapReduce jobs = 2
    Launching Job 1 out of 2
    …
    2012-03-04 05:58:12,991 Stage-1 map = 0%,  reduce = 0%
    2012-03-04 05:58:16,021 Stage-1 map = 50%,  reduce = 0%
    2012-03-04 05:58:18,046 Stage-1 map = 100%,  reduce = 0%
    2012-03-04 05:58:24,092 Stage-1 map = 100%,  reduce = 100%
    Ended Job = job_201203040432_0027
    Launching Job 2 out of 2
    …
    2012-03-04 05:58:33,650 Stage-2 map = 0%,  reduce = 0%
    2012-03-04 05:58:36,673 Stage-2 map = 100%,  reduce = 0%
    2012-03-04 05:58:45,730 Stage-2 map = 100%,  reduce = 100%
    Ended Job = job_201203040432_0028
    MapReduce Jobs Launched: 
    Job 0: Map: 2  Reduce: 1   HDFS Read: 75416863 HDFS Write: 116 SUCESS
    Job 1: Map: 1  Reduce: 1   HDFS Read: 304 HDFS Write: 5 SUCESS
    Total MapReduce CPU Time Spent: 0 msec.
    OK
    7599
    Time taken: 47.03 seconds
    
    
  5. Delete the view:

    $ hive -e "drop view usa_sightings"
    
    

    您将在屏幕上收到以下输出:

    OK
    Time taken: 5.298 seconds
    
    

刚刚发生了什么?

我们首先使用CREATE VIEW语句创建视图。 这类似于CREATE TABLE,但有两个主要区别:

  • 列定义仅包括名称作为类型,该类型将由基础查询确定
  • AS子句指定将用于生成视图的SELECT语句

我们使用前面的 JOIN 语句来生成视图,因此实际上我们正在创建一个将 Location 字段标准化为完整状态名称的表,而不直接要求用户处理如何执行规范化。

可选的IF NOT EXISTS子句(也可以与CREATE TABLE一起使用)意味着配置单元将忽略创建视图的重复尝试。 如果没有这个子句,重复尝试创建视图将生成错误,这并不总是所需的行为。

然后,我们执行该脚本两次,以创建视图并演示包含IF NOT EXISTS子句可以防止我们想要的错误。

创建视图后,我们随后对其执行查询(在本例中),以简单地计算有多少目击事件发生在加利福尼亚州。 我们之前所有生成 MapReduce 作业的配置单元语句都只生成了一个;针对我们的视图的这个查询需要两个链接的 MapReduce 作业。 查看查询和视图规范,这并不令人惊讶;不难想象第一个 MapReduce 作业及其输出将如何实现视图,并将其输出提供给作为第二个作业执行的后续计数查询。 因此,您还会看到这个分两个阶段的作业比我们之前的任何查询花费的时间都要长得多。

Hive 实际上比这更聪明。 如果外部查询可以合并到视图创建中,那么配置单元将只生成并执行一个 MapReduce 作业。 考虑到手工开发一系列合作的 MapReduce 作业所花费的时间,这是一个很好的例子,说明了 Hive 可以提供的好处。 尽管手写的 MapReduce 作业(或一系列作业)可能效率更高,但配置单元是一个很好的工具,可以首先确定哪些作业是有用的。 与其花一天时间开发 MapReduce 作业来得出相同的结论,不如运行一个较慢的配置单元查询来确定一个想法没有期望的那么有用。

我们已经提到,视图可以隐藏底层的复杂性;这通常意味着执行视图本质上很慢。 对于大规模生产工作负载,您需要优化 SQL,并可能完全删除视图。

在运行查询之后,我们通过DROP VIEW语句删除视图,这再次证明了 HiveQL(和 SQL)处理表和视图的相似性。

在配置单元中处理脏数据

您中的观察者可能会注意到,此查询报告的加利福尼亚目击事件数量与我们在第 4 章开发 MapReduce 程序中生成的数量不同。 为什么?

回想一下,在第 4 章开发 MapReduce 程序中运行 Hadoop Streaming 或 Java MapReduce 作业之前,我们有一种机制可以忽略格式错误的输入行。 然后,在处理数据时,我们使用更精确的正则表达式从 Location 字段中提取两个字母的州缩写。 然而,在 Hive 中,我们没有做这样的预处理,而是依靠相当粗糙的机制来提取缩写。

对于后者,我们可以使用前面提到的一些支持正则表达式的配置单元函数,但对于前者,我们最多只能在许多查询中添加复杂的 VALIDATIONWHERE子句。

一种常见的模式是在将数据导入配置单元之前对其进行预处理,因此,例如,在本例中,我们可以运行一个 MapReduce 作业来删除输入文件中的所有格式错误的记录,而另一个作业则提前执行位置字段的规范化。

来个围棋英雄--来吧!

编写 MapReduce 作业(可能是一个或两个)来对输入数据进行预处理,并生成更适合直接导入到配置单元的清理文件。 然后编写执行作业的脚本,创建配置单元表,并将新文件导入到表中。 这也将展示 Hadoop 和 Have 在一起是多么容易和强大的脚本化。

操作导出查询输出的时间

我们之前已经将大量数据加载到配置单元中,或者提取非常少量的数据作为查询结果。 我们还可以导出大型结果集;让我们看一个示例。

  1. 重新创建以前使用的视图:

    $ hive -f view.hql
    
    
  2. 将以下文件创建为export.hql

    INSERT OVERWRITE DIRECTORY '/tmp/out'
    SELECT reported, shape, state
    FROM usa_sightings
    WHERE state = 'California' ;
    
  3. Execute the script:

    $ hive -f export.hql
    
    

    您将收到以下响应:

    2012-03-04 06:20:44,571 Stage-1 map = 100%,  reduce = 100%
    Ended Job = job_201203040432_0029
    Moving data to: /tmp/out
    7599 Rows loaded to /tmp/out
    MapReduce Jobs Launched: 
    Job 0: Map: 2  Reduce: 1   HDFS Read: 75416863 HDFS Write: 210901 SUCESS
    Total MapReduce CPU Time Spent: 0 msec
    OK
    Time taken: 46.669 seconds
    
    
  4. Look in the specified output directory:

    $ hadoop fs -ls /tmp/out
    
    

    您将收到以下响应:

    Found 1 items
    -rw-r--r--   3 hadoop supergroup     210901 … /tmp/out/000000_1
    
    
  5. Examine the output file:

    $ hadoop fs -cat /tmp/out/000000_1 | head
    
    

    您将在屏幕上收到以下输出:

    20021014_ light_California
    20050224_ other_California
    20021001_ egg_California
    20030527_ sphere_California
    20050813_ light_California
    20040701_ other_California
    20031007_ light_California
    
    

刚刚发生了什么?

重用前一个视图之后,我们使用INSERT OVERWRITE DIRECTORY命令创建了 HiveQL 脚本。 顾名思义,这会将后续语句的结果放到指定位置。 OVERWRITE修饰符也是可选的,它简单地确定是否要首先删除该位置中的任何现有内容。 INSERT命令后跟一条SELECT语句,该语句生成要写入输出位置的数据。 在本例中,我们使用以前创建的视图上的查询,您会记得该查询构建在连接之上,演示了这里的查询如何可以任意复杂。

当输出数据要写入运行配置单元命令而不是 HDFS 的主机的本地文件系统时,还有一个额外的可选LOCAL修饰符。

当我们运行脚本时,MapReduce 的输出基本上与我们预期的一样,但增加了一行,说明已将多少行导出到指定的输出位置。

在运行脚本之后,我们检查输出目录,看看结果文件是否在那里,当我们查看它时,内容与我们预期的一样。

备注

正如配置单元用于输入中的文本文件的默认分隔符是 ASCII 代码 0001(‘\a’)一样,它也将其用作输出文件的默认分隔符,如上例所示。

INSERT命令还可以用对其他表的查询结果填充一个表,我们将研究下一个表。 首先,我们需要解释一个我们将同时使用的概念。

对表进行分区

我们在前面提到过,编写得不好的连接有很长一段不好的历史,会导致关系数据库花费大量时间来处理不必要的工作。 执行全表扫描(访问表中的每一行)而不是使用允许直接访问感兴趣的行的索引的查询可以讲述类似的悲惨故事。

对于存储在 HDFS 上并映射到配置单元表的数据,默认情况下几乎需要全表扫描。 由于没有办法将数据分割成更有组织的结构,允许处理只应用于感兴趣的数据子集,Hive 被迫处理整个数据集。 对于我们大约 70MB 的 UFO 文件,这真的不是问题,因为我们看到文件处理只需要几十秒。 然而,如果它大一千倍呢?

与传统关系数据库一样,配置单元允许根据虚拟列的值对表进行分区,然后将这些值用于查询谓词中。

具体地说,在创建表时,它可以有一个或多个分区列,并且在将数据加载到表中时,这些列的指定值将确定将数据写入的分区。

对于每天接收大量数据的表,最常见的分区策略是将分区列设置为日期。 然后,可以将将来的查询限制为仅处理包含在特定分区中的数据。 在幕后,配置单元将每个分区存储在它自己的目录和文件中,这样它就可以只在感兴趣的数据上应用 MapReduce 作业。 通过使用多个分区列,可以创建丰富的分层结构,并且对于具有仅需要较小数据子集的查询的大型表,花一些时间决定最佳分区策略是值得的。

对于我们的 UFO 数据集,我们将使用目击年份作为分割值,但是我们必须使用一些不太常见的特性来实现它。 所以,在介绍完之后,现在让我们来做一些分区!

该行动了-制作一张分区的不明飞行物目录表

我们将为 UFO 数据创建一个新表,以演示分区的有效性。

  1. 将以下查询另存为createpartition.hql

    CREATE TABLE partufo(sighted string, reported string, sighting_location string,shape string, duration string, description string) 
    PARTITIONED BY (year string)
    ROW FORMAT DELIMITED
    FIELDS TERMINATED BY '\t' ;
    
  2. 将以下查询另存为insertpartition.hql

    SET hive.exec.dynamic.partition=true ;
    SET hive.exec.dynamic.partition.mode=nonstrict ;
    
    INSERT OVERWRITE TABLE partufo partition (year)
    SELECT sighted, reported, sighting_location, shape, duration, description,
    SUBSTR(TRIM(sighted), 1,4)  FROM ufodata ;
    
  3. Create the partitioned table:

    $ hive -f createpartition.hql 
    
    

    您将收到以下响应:

    Logging initialized using configuration in jar:file:/opt/hive-0.8.1/lib/hive-common-0.8.1.jar!/hive-log4j.properties
    Hive history file=/tmp/hadoop/hive_job_log_hadoop_201203101838_17331656.txt
    OK
    Time taken: 4.754 seconds
    
    
  4. 检查创建的表:

    OK
    sighted  string 
    reported  string 
    sighting_location  string 
    shape  string 
    duration  string 
    description  string 
    year  string 
    Time taken: 4.704 seconds
    
    
  5. Populate the table:

    $ hive -f insertpartition.hql 
    
    

    您将在屏幕上看到以下行:

    Total MapReduce jobs = 2
    …
    …
    Ended Job = job_201203040432_0041
    Ended Job = 994255701, job is filtered out (removed at runtime).
    Moving data to: hdfs://head:9000/tmp/hive-hadoop/hive_2012-03-10_18-38-36_380_1188564613139061024/-ext-10000
    Loading data to table default.partufo partition (year=null)
    Loading partition {year=1977}
    Loading partition {year=1880}
    Loading partition {year=1975}
    Loading partition {year=2007}
    Loading partition {year=1957}
    …
    Table default.partufo stats: [num_partitions: 100, num_files: 100, num_rows: 0, total_size: 74751215, raw_data_size: 0]
    61393 Rows loaded to partufo
    …
    OK
    Time taken: 46.285 seconds
    
    
  6. Execute a count command against a partition:

    $ hive –e "select count(*)from partufo where year  = '1989'"
    
    

    您将收到以下响应:

    OK
    249
    Time taken: 26.56 seconds
    
    
  7. Execute a similar query on the non-partitioned table:

    $ hive –e "select count(*) from ufodata where sighted like '1989%'"
    
    

    您将收到以下响应:

    OK
    249
    Time taken: 28.61 seconds
    
    
  8. List the contents of the Hive directory housing the partitioned table:

    $ Hadoop fs –ls /user/hive/warehouse/partufo
    
    

    您将收到以下响应:

    Found 100 items
    drwxr-xr-x   - hadoop supergroup          0 2012-03-10 18:38 /user/hive/warehouse/partufo/year=0000
    drwxr-xr-x   - hadoop supergroup          0 2012-03-10 18:38 /user/hive/warehouse/partufo/year=1400
    drwxr-xr-x   - hadoop supergroup          0 2012-03-10 18:38 /user/hive/warehouse/partufo/year=1762
    drwxr-xr-x   - hadoop supergroup          0 2012-03-10 18:38 /user/hive/warehouse/partufo/year=1790
    drwxr-xr-x   - hadoop supergroup          0 2012-03-10 18:38 /user/hive/warehouse/partufo/year=1860
    drwxr-xr-x   - hadoop supergroup          0 2012-03-10 18:38 /user/hive/warehouse/partufo/year=1864
    drwxr-xr-x   - hadoop supergroup          0 2012-03-10 18:38 /user/hive/warehouse/partufo/year=1865
    
    

刚刚发生了什么?

我们为这个示例创建了两个 HiveQL 脚本。 其中第一个用于创建新的分区表。 正如我们所看到的,它看起来与前面的CREATE TABLE语句非常相似;不同之处在于附加的PARTITIONED BY子句。

在我们执行这个脚本之后,我们描述了该表,看到,从 HiveQL 的角度来看,该表看起来就像前面的ufodata表,但是增加了一列年份的内容。 这允许在WHERE子句中指定条件时将该列视为任何其他列,即使该列数据实际上并不存在于磁盘数据文件中。

接下来,我们执行第二个脚本,该脚本执行将数据实际加载到分区表中。 这里有几件值得注意的事情。

首先,我们看到INSERT命令可以用于表,就像我们之前对目录所做的那样。 INSERT语句指定数据的去向,随后的SELECT语句从现有的表或视图中收集所需的数据。

这里使用的分区机制利用了配置单元中一个相对较新的功能,即动态分区。 在大多数情况下,此语句中的 PARTITION 子句将包括 Year 列的显式值。 但是,尽管如果我们将一天的数据上载到每日分区中,这将会起作用,但是它不适合我们的数据文件类型,在我们的数据文件中,应该将各种行插入到各种分区中。 通过简单地指定不带值的列名,分区名将由SELECT语句返回的 Year 列的值自动生成。

这有望解释SELECT语句中奇怪的最后一个子句;在指定了ufodata中的所有标准列之后,我们添加了一个规范,用于提取包含视线列的前四个字符的字符串。 请记住,因为分区表将 Year 分区列视为第七列,这意味着我们将看到的字符串的 Year 组件分配给每行中的 Year 列。 因此,每一行都被插入到与其目击年份相关联的分区中。

为了证明这是按预期工作的,我们然后执行两个查询;一个计算分区表中 1989 年的分区中的所有记录,另一个计算ufodata中以字符串“1989”开头的记录,也就是以前用于动态创建分区的组件。

可以看到,这两个查询返回相同的结果,验证了我们的分区策略是否按预期工作。 我们还注意到分区查询比其他查询稍微快一些,尽管速度不是很快。 这很可能是因为 MapReduce 启动时间主导了我们相对较少的数据集的处理。

最后,我们查看配置单元存储分区表数据的目录,发现 100 个动态生成的分区中的每一个确实都有一个目录。 现在,每当我们表达引用特定分区的 HiveQL 语句时,配置单元都可以通过只处理在适当分区目录中找到的数据来执行显著的优化。

扣件、群集和排序...。 哦天啊!

我们在这里不会详细探讨它,但是分层分区列并不是配置单元如何优化数据子集内的数据访问模式的全部内容。 在分区内,配置单元提供了一种机制,可以使用指定的CLUSTER BY列上的散列函数将行进一步收集到个存储桶中。 在存储桶中,可以使用指定的SORT BY列按排序顺序保存行。 例如,我们可以根据不明飞行物的形状和根据发现日期排序的每个桶中的数据来存储数据。

这些特性不一定是您在使用配置单元的第一天就需要使用的特性,但是如果您发现自己使用的数据集越来越大,那么考虑这种类型的优化可能会大大缩短查询处理时间。

自定义函数

配置单元提供了机制,让您可以将自定义代码直接挂接到 HiveQL 执行中。 这可以是添加新的库函数的形式,也可以是指定配置单元转换的形式,其工作方式与 Hadoop 流非常相似。 我们将在本节中查看用户定义的函数,因为它们是您最有可能在早期需要添加自定义代码的地方。 配置单元转换是一种稍微复杂一些的机制,通过它可以添加由配置单元运行时调用的自定义映射和减少类。 如果你感兴趣的是变换,那么在 Hive 维基上有很好的文档记录。

操作时间-添加新的用户定义函数(UDF)

让我们展示如何通过新的 UDF 创建和调用一些自定义 Java 代码。

  1. 将以下代码另存为City.java

    package com.kycorsystems ;
    
    import java.util.regex.Matcher ;
    import java.util.regex.Pattern ;
    import org.apache.hadoop.hive.ql.exec.UDF ;
    import org.apache.hadoop.io.Text ;
    
    public class City extends UDF
    {
        private static Pattern pattern = Pattern.compile(
            "[a-zA-z]+?[\\. ]*[a-zA-z]+?[\\, ][^a-zA-Z]") ;
    
        public Text evaluate( final Text str)
        {
            Text result ;
            String location = str.toString().trim() ;
            Matcher matcher = pattern.matcher(location) ;
    
            if (matcher.find())
            {
                result = new Text(                 location.substring(matcher.start(), matcher.end()-2)) ;
            }
            else
            {
                result = new Text("Unknown") ;
            }        
            return result ;
        }
    }
    
  2. 编译此文件:

    $ javac -cp hive/lib/hive-exec-0.8.1.jar:hadoop/hadoop-1.0.4-core.jar  -d . City.java
    
    
  3. Package the generated class file into a JAR file:

    $ jar cvf city.jar com
    
    

    您将收到以下响应:

    added manifest
    adding: com/(in = 0) (out= 0)(stored 0%)
    adding: com/kycorsystems/(in = 0) (out= 0)(stored 0%)
    adding: com/kycorsystems/City.class(in = 1101) (out= 647)(deflated 41%)
    
    
  4. 启动交互式配置单元外壳:

    $ hive
    
    
  5. Add the new JAR file to the Hive classpath:

    hive> add jar city.jar;
    
    

    您将收到以下响应:

    Added city.jar to class path
    Added resource: city.jar
    
    
  6. Confirm that the JAR file was added:

    hive> list jars;
    
    

    您将收到以下响应:

    file:/opt/hive-0.8.1/lib/hive-builtins-0.8.1.jar
    city.jar
    
    
  7. Register the new code with a function name:

    hive> create temporary function city as 'com.kycorsystems.City' ;
    
    

    您将收到以下响应:

    OK
    Time taken: 0.277 seconds
    
    
  8. Execute a query using the new function:

    hive> select city(sighting_location), count(*) as total
    > from partufo
    > where year = '1999'
    > group by city(sighting_location)
    > having  total > 15 ;
    
    

    您将收到以下响应:

    Total MapReduce jobs = 1
    Launching Job 1 out of 1
    …
    OK
    Chicago  19
    Las Vegas  19
    Phoenix  19
    Portland  17
    San Diego  18
    Seattle  26
    Unknown  34
    Time taken: 29.055 seconds
    
    

刚刚发生了什么?

我们编写的 Java 类扩展了基础org.apache.hadoop.hive.exec.ql.UDF(用户定义函数)类。 在这个类中,我们定义了一个方法,用于在给定位置字符串的情况下返回城市名称,该字符串遵循我们前面看到的一般模式。

UDF 实际上并不基于类型定义一系列Evaluate方法;相反,您可以使用任意参数和返回类型自由添加自己的方法。 HIVE 使用 Java 反射来选择正确的求值方法,如果您需要更细粒度的选择,您可以开发实现UDFMethodResolver接口的您自己的实用程序类。

这里使用的正则表达式有点笨拙;我们希望提取城市的名称,假设后跟一个州缩写。 但是,名称的描述方式和对多个单词名称的处理方式不一致,这就给我们带来了前面看到的正则表达式。 除此之外,这门课非常简单。

我们编译City.java文件,同时从配置单元和 Hadoop 添加必要的 JAR。

备注

当然,请记住,如果您使用的是 Hadoop 和 Have 的相同版本,则特定的 JAR 文件名可能不同。

然后,我们将生成的类文件捆绑到一个 JAR 中,并启动配置单元交互外壳。

创建 JAR 之后,我们需要配置配置单元以使用它。 这是一个分两步走的过程。 首先,我们使用add jar命令将新的 JAR 文件添加到配置单元使用的类路径中。 这样做之后,我们使用list jars命令确认我们的新 JAR 已经在系统中注册。

添加 JAR 只是告诉配置单元存在一些代码,并没有说明我们希望如何在 HiveQL 语句中引用该函数。 CREATE FUNCTION命令实现了这一点-将一个函数名(在本例中为city)与提供实现的完全限定的 Java 类(在本例中为com.kycorsystems.City)关联起来。

将 JAR 文件添加到类路径并创建函数后,我们现在可以在 HiveQL 语句中引用city()函数。

接下来,我们运行了一个示例查询,演示了新函数的运行情况。 回到分区的不明飞行物目击事件表,我们认为,在每个人都在为千禧年末日做准备的时候,看看 UFO 目击事件最多发生在哪里会很有趣。

从 HiveQL 语句可以看出,我们可以像使用任何其他函数一样使用我们的新函数,事实上,了解哪些函数是内置函数、哪些函数是 UDF 的唯一方法就是熟悉标准配置单元函数库。

结果显示,目击事件主要集中在美国西北部和西南部,芝加哥是唯一例外。 然而,我们确实得到了相当多的Unknown结果,这需要进一步的分析来确定这是由于美国以外的地方造成的,还是我们需要进一步改进我们的正则表达式。

预处理或不预处理...

让我们再来回顾一下前面的主题;在将数据导入到配置单元之前,可能需要将数据预处理为更干净的形式。 从前面的示例可以看出,我们可以通过一系列 UDF 动态执行类似的处理。 例如,我们可以添加名为statecountry的函数,这些函数从位置观察字符串中提取或推断出进一步的地区和国家分量。 关于哪种方法是最好的,很少有具体的规则,但一些指导方针可能会有所帮助。

如果就像这里的情况一样,除了提取不同的组件之外,我们不太可能实际处理完整的位置字符串,那么预处理可能更有意义。 我们可以将其规范化为更可预测的格式,甚至将其分成单独的城市/地区/国家列,而不是在每次访问列时都执行昂贵的文本处理。

但是,如果 HiveQL 中的列通常以其原始形式使用,而额外的处理是例外情况,那么在整个数据集中执行昂贵的处理步骤可能没有什么好处。

使用对您的数据和工作负载最有意义的策略。 请记住,UDF 不仅仅用于这种类型的文本处理,它们还可以用来封装您希望应用于表中数据的任何类型的逻辑。

Hive 对 PIG

在 Internet 上搜索有关 Hive 的文章,很快就会发现许多将 Hive 与另一个名为Pig的 Apache 项目进行比较的文章。 围绕这一比较的一些最常见的问题是,为什么两者都存在,什么时候使用一种比另一种更好,以及在酒吧里穿项目 T 恤时,哪种会让你看起来更酷。

这两个项目之间的重叠之处在于,hive 希望为数据提供一个熟悉的类似 SQL 的界面,而 Pig 使用一种名为Pig 拉丁语的语言来指定数据流管道。 就像配置单元将 HiveQL 转换成 MapReduce 然后执行一样,Pig 从 Pig 的拉丁脚本执行类似的 MapReduce 代码生成。

HiveQL 和 Pig 拉丁语之间最大的区别是对作业将如何执行表示的控制量。 与 SQL 一样,HiveQL 指定要做什么,但几乎没有说明如何实际构建实现。 HiveQL 查询规划器负责确定执行 HiveQL 命令的特定部分的顺序、评估函数的顺序,等等。 这些决策是由配置单元在运行时做出的,类似于传统的关系数据库查询规划器,这也是 Pig 拉丁语操作的级别。

这两种方法都不需要编写原始 MapReduce 代码;它们提供的抽象有所不同。

Hive 和 PIG 的选择将取决于你的需要。 如果拥有熟悉的数据 SQL 接口对于使 Hadoop 中的数据可供更广泛的受众使用很重要,那么配置单元是显而易见的选择。 相反,如果您的人员从数据管道的角度考虑问题,并且需要对作业的执行方式进行更细粒度的控制,那么 Pig 可能更合适。 Hive 和 Pig 项目正在寻求更紧密的集成,因此希望错误的竞争感将会减少,相反,这两个项目将被视为减少执行 MapReduce 作业所需的 Hadoop 知识的互补方式。

我们没有讲到的是

在这个配置单元概述中,我们介绍了它的安装和设置、表、视图和联接的创建和操作。 我们研究了如何将数据移入和移出 Hive,如何优化数据处理,并探索了 Hive 的几个内置函数。

在现实中,我们仅仅触及了皮毛。 除了对前面的主题和各种相关概念进行更深入的介绍外,我们甚至没有触及这样的主题,例如MetaStore(配置单元存储其配置和元数据)或SerDe(序列化/反序列化)对象,这些对象可用于从更复杂的文件格式(如 JSON)读取数据。

HIVE 是一个非常丰富的工具,具有许多强大而复杂的功能。 如果您认为 Hive 对您有价值,则建议您在浏览完本章中的示例后,花些时间阅读 Hive 网站上的文档。 在那里,您还可以找到指向用户邮件列表的链接,这是一个很好的信息和帮助来源。

亚马逊网络服务上的 Hive

Elastic MapReduce 通过一些特定机制为配置单元提供了重要支持,以帮助其与其他 AWS 服务集成。

电子病历动作运行 UFO 分析时间

让我们通过在该平台上进行一些 UFO 分析,来探索 EMR 与 Hive 的使用。

  1. http://aws.amazon.com/console登录到亚马逊网络服务管理控制台。

  2. 电子病历上的每个配置单元作业流都从 S3 存储桶运行,我们需要选择要用于此目的的存储桶。 选择S3查看与您的帐户关联的存储桶列表,然后选择要从中运行示例的存储桶,在下面的示例中,我们选择了名为 garryt1use 的存储桶。

  3. Use the web interface to create three directories called ufodata, ufoout, and ufologs within that bucket. The resulting list of the bucket's contents should look like the following screenshot:

    Time for action – running UFO analysis on EMR

  4. 双击ufodata目录上的将其打开,并在其中创建名为ufostates的两个子目录。

  5. Create the following as s3test.hql, click on the Upload link within the ufodata directory, and follow the prompts to upload the file:

    CREATE EXTERNAL TABLE IF NOT EXISTS ufodata(sighted string, reported string, sighting_location string,
    shape string, duration string, description string)
    ROW FORMAT DELIMITED
    FIELDS TERMINATED BY '\t' 
    LOCATION '${INPUT}/ufo' ;
    
    CREATE EXTERNAL TABLE IF NOT EXISTS states(abbreviation string, full_name string)
    ROW FORMAT DELIMITED
    FIELDS TERMINATED BY '\t'
    LOCATION '${INPUT}/states' ;
    
    CREATE VIEW IF NOT EXISTS usa_sightings (sighted, reported, shape, state)
    AS SELECT t1.sighted, t1.reported, t1.shape, t2.full_name
    FROM ufodata t1 JOIN states t2
    ON (LOWER(t2.abbreviation) = LOWER(SUBSTR( t1.sighting_location, (LENGTH(t1.sighting_location)-1)))) ;
    
    CREATE EXTERNAL TABLE IF NOT EXISTS state_results ( reported string, shape string, state string)
    ROW FORMAT DELIMITED
    FFIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n'
    STORED AS TEXTFILE
    LOCATION '${OUTPUT}/states' ;
    
    INSERT OVERWRITE TABLE state_results
    SELECT reported, shape, state
    FROM usa_sightings
    WHERE state = 'California' ;
    

    现在,ufodata的内容应如以下屏幕截图所示:

    Time for action – running UFO analysis on EMR

  6. Double-click the states directory to open it and into this, upload the states.txt file used earlier. The directory should now look like the following screenshot:

    Time for action – running UFO analysis on EMR

  7. 单击文件列表顶部的ufodata组件以返回到此目录。

  8. Double-click on the ufo directory to open it and into this, upload the ufo.tsv file used earlier. The directory should now look like the following screenshot:

    Time for action – running UFO analysis on EMR

  9. Now select Elastic MapReduce and click on Create a New Job Flow. Then select the option Run your own application and select a Hive application, as shown in the following screenshot:

    Time for action – running UFO analysis on EMR

  10. Click on Continue and then fill in the required details for the Hive job flow. Use the following screenshot as a guide, but remember to change the bucket name (the first component in the s3:// URLs) to the bucket you set up before:

![Time for action – running UFO analysis on EMR](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/hadoop-begin-guide/img/7300_08_06.jpg)
  1. Click on Continue, review the number and the type of hosts to be used, and then click on Continue once again. Then fill in the name of the directory for the logs, as shown in the following screenshot:
![Time for action – running UFO analysis on EMR](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/hadoop-begin-guide/img/7300_08_07.jpg)
  1. 单击继续。 然后,在剩余的作业创建过程中执行与相同的操作,本例中没有其他需要更改的默认选项。 最后,启动作业流并从管理控制台监视其进度。
  2. 作业成功完成后,返回S3并双击ufoout目录。 其中应该有一个名为states的目录,还有一个名为0000000的文件。 双击以下载该文件,并验证其内容是否如下所示:
```scala
20021014   light   California
20050224   other   California
20021001   egg     California
20030527   sphere  California
```

刚刚发生了什么?

在实际执行 EMR 作业流之前,我们需要在前面的示例中进行一些设置。 首先,我们使用 S3Web 界面为我们的工作准备目录结构。 我们创建了三个主要目录来保存输入数据,一个目录用于写入结果,一个目录用于 EMR 放置作业流执行的日志。

HiveQL 脚本是对本章前面使用的几个配置单元命令的修改。 它为 UFO 目击数据和州名称以及连接它们的视图创建表。 然后,它创建一个没有源数据的新表,并使用INSERT OVERWRITE TABLE用查询结果填充该表。

该脚本的独特之处在于我们为每个表指定LOCATION子句的方式。 对于输入表,我们使用相对于名为INPUT的变量的路径,并对结果表的OUTPUT变量执行同样的操作。

请注意,EMR 中的配置单元要求表数据的位置是目录,而不是文件。 这就是我们之前为上传特定源文件的每个表创建子目录的原因,而不是使用数据文件本身的直接路径指定表。

在我们的 S3 存储桶中设置了所需的文件和目录结构后,我们转到 EMR Web 控制台并开始作业流创建过程。

在指定我们希望使用我们自己的程序并且它将是一个配置单元应用之后,我们在一个屏幕上填写了我们的工作流所需的关键数据:

  • HiveQL 脚本本身的位置
  • 包含输入数据的目录
  • 要用于输出数据的目录

HiveQL 脚本的路径是显式路径,不需要任何解释。 但是,重要的是要了解如何将其他值映射到我们的配置单元脚本中使用的变量。

输入路径的值可以作为INPUT变量提供给配置单元脚本,这就是我们将包含 UFO 目击数据的目录指定为${INPUT}/ufo的方式。 同样,此表单中指定的输出值将用作配置单元脚本中的OUTPUT变量。

我们没有对默认主机设置进行任何更改,它将是一个小主机和两个小核心节点。 在下一个屏幕上,我们添加了希望 EMR 写入作业流执行生成的日志的位置。

虽然是可选的,但捕获这些日志很有用,特别是在运行新脚本的早期阶段,尽管 S3 存储显然是有成本的。 EMR 还可以将索引日志数据写入SimpleDB(另一项 AWS 服务),但我们在这里没有展示这一点。

在完成作业流定义之后,我们启动它,在成功执行之后,转到 S3 界面浏览到输出位置,其中很高兴地包含了我们期望的数据。

使用交互式工作流进行开发

在开发要在 EMR 上执行的新配置单元脚本时,以前的批处理作业执行不太合适。 在作业流创建和执行之间通常有几分钟的延迟,如果作业失败,则会产生几个小时的 EC2 实例时间成本(部分时间四舍五入)。

我们可以在交互模式下启动配置单元作业流,而不是选择创建 EMR 作业流的选项来运行配置单元脚本(如前面的示例所示)。 这可以有效地启动 Hadoop 集群,而不需要命名脚本。 然后,您可以以 hadoop 用户的身份通过 SSH 连接到主节点,您将发现已在其中安装和配置了配置单元。 在此环境中进行脚本开发,然后根据需要设置批处理脚本作业流以在生产中自动执行脚本,效率要高得多。

拥有围棋英雄-使用交互式电子病历群集

在电子病历中启动交互式配置单元作业流。 您需要已向 EC2 注册 SSH 凭据,才能连接到主节点。 直接从主节点运行前面的脚本,记住要将适当的变量传递给脚本。

与其他 AWS 产品集成

对于本地 Hadoop/Have 安装,数据存储位置的问题通常归结为 HDFS 或本地文件系统。 正如我们之前看到的,EMR 中的配置单元提供了另一种选择,它支持数据驻留在 S3 中的外部表。

另一个具有类似支持的 aws 服务是DynamoDB(位于NoSQL),这是一个托管在云中的 http://aws.amazon.com/dynamodb 数据库解决方案。 EMR 中的配置单元作业流可以声明从 DynamoDB 读取数据或将其用作查询输出目标的外部表。

这是一个非常强大的模型,因为它允许使用配置单元来处理和组合来自多个源的数据,而将数据从一个系统映射到配置单元表的机制是透明的。 它还允许将配置单元用作将数据从一个系统移动到另一个系统的机制。 从现有商店频繁地将数据放入此类托管服务的行为是采用的主要障碍。

摘要

我们已经在本章中了解了配置单元,并了解了它如何提供使用关系数据库的任何人都会熟悉的许多工具和功能。 它不需要开发 MapReduce 应用,而是将 Hadoop 的强大功能提供给更广泛的社区。

具体地说,我们下载并安装了 hive,了解到它是一个客户端应用,将其 HiveQL 语言转换为 MapReduce 代码,然后提交给 Hadoop 集群。 我们探索了配置单元用于创建表和对这些表运行查询的机制。 我们了解了配置单元如何支持各种底层数据文件格式和结构,以及如何修改这些选项。

我们还认识到,配置单元表在很大程度上是一个逻辑结构,在幕后,表上的所有类似 SQL 的操作实际上都是由 HDFS 文件上的 MapReduce 作业执行的。 然后,我们了解了配置单元如何支持强大的特性,如联接和视图,以及如何对表进行分区以帮助高效执行查询。

我们使用配置单元将查询结果输出到 HDFS 上的文件,并了解 Elastic MapReduce 如何支持配置单元,其中交互式作业流可用于开发新的配置单元应用,然后以批处理模式自动运行。

正如我们在本书中多次提到的,Hive 看起来像一个关系数据库,但实际上并不是。 然而,在许多情况下,您会发现现有的关系数据库是您需要集成到的更广泛基础设施的一部分。 执行这种集成以及如何在这些不同类型的数据源之间移动数据将是下一章的主题。

九、使用关系数据库

正如我们在上一章中看到的,配置单元是一个很棒的工具,它提供了存储在 Hadoop 中的数据的类似于关系数据库的视图。 然而,归根结底,它并不是真正的关系数据库。 它没有完全实现 SQL 标准,其性能和规模特征与传统关系数据库有很大不同(不是更好或更差,只是不同)。

在许多情况下,您会发现 Hadoop 集群与关系数据库一起使用(而不是替代)。 通常,业务流需要将数据从一个商店移动到另一个商店;我们现在将探索这种集成。

在本章中,我们将:

  • 确定一些常见的 Hadoop/RDBMS 使用情形
  • 了解如何将数据从 RDBMS 移动到 HDFS 和配置单元
  • 使用 Sqoop 作为此类问题的更好解决方案
  • 将带有导出的数据从 Hadoop 移动到 RDBMS
  • 最后,我们将讨论如何将其应用于 AWS

公共数据路径

回到第 1 章关于的全部内容,我们谈到了我们认为会引起很大争议的人为选择:使用 Hadoop 或传统的关系数据库。 正如那里解释的那样,我们的论点是,要重点关注的是为手头的任务确定正确的工具,这可能会导致使用多种技术的情况。 值得看几个具体的例子来说明这一观点。

Hadoop 作为归档存储

当 RDBMS 用作主数据存储库时,经常会出现规模和数据保留问题。 随着新数据量的增加,如何处理较旧且价值较低的数据?

传统上,有两种主要方法来解决这种情况:

  • 对 RDBMS 进行分区,以提高较新数据的性能;有时,该技术允许将较旧的数据存储在速度较慢且成本较低的存储系统上
  • 将数据归档到磁带或其他脱机存储

这两种方法都是有效的,两者之间的决定通常取决于是否需要较旧的数据才能及时访问。 这是两种极端情况,前者以复杂性和基础设施费用为代价最大化访问,而后者降低成本但使数据更难访问。

最近看到的模型是将最新的数据保存在关系数据库中,而将较旧的数据推送到 Hadoop 中。 这可以作为结构化文件放到 HDFS 上,也可以放到配置单元中以保留 RDBMS 接口。 这两全其美,允许通过高速、低延迟的 SQL 查询访问较低的数据量和较新的数据,而从 Hadoop 访问的归档数据量要大得多。 因此,数据对于需要两种访问类型中的任何一种的用例仍然可用;对于需要跨最新数据和归档数据的任何查询,这在需要额外集成的平台上都是必需的。

由于 Hadoop 的可伸缩性,此模型提供了巨大的未来增长潜力;我们知道,我们可以继续增加存储的归档数据量,同时保留对其运行分析的能力。

Hadoop 作为预处理步骤

在我们的 Hive 讨论中,我们多次强调了一些预处理工作对处理或清理数据非常有用的机会。 不幸的事实是,在许多(大多数?)。 在大数据的情况下,来自多个来源的大量数据意味着脏数据只是给定的。 尽管大多数 MapReduce 作业只需要处理全部数据的一个子集,但我们仍然会在整个数据集中发现不完整或损坏的数据。 正如配置单元可以从预处理数据中获益一样,传统的关系数据库也可以。

Hadoop 在这里可以是一个很棒的工具;它可以从多个源提取数据,将它们组合以进行必要的转换,并在将数据插入到关系数据库之前进行清理。

Hadoop 作为数据输入工具

Hadoop 不仅有的价值,还因为它使数据变得更好,并且非常适合被吸收到关系数据库中。 除了这些任务之外,Hadoop 还可以用于生成额外的数据集或数据视图,然后从关系数据库提供这些数据集或数据视图。 这里的常见模式指的是这样的情况:我们不仅希望显示帐户的主要数据,还希望在其旁边显示从帐户历史记录生成的次要数据。 这些观点可以是对前几个月各类支出交易的汇总。 这些数据保存在 Hadoop 中,可以从 Hadoop 中生成实际摘要,这些摘要可能会被推回到数据库中,以便更快地显示。

蛇吃自己的尾巴

实际情况往往比这些明确定义的情况更复杂,Hadoop 和关系数据库之间的数据流使用圆圈和圆弧而不是一条直线来描述的情况并不少见。 例如,Hadoop 群集可以对数据执行预处理步骤,这些数据随后被摄取到 RDBMS 中,然后接收用于构建聚合的频繁事务转储,这些事务转储被发送回数据库。 然后,一旦数据超过某个阈值,它就会从数据库中删除,但会保留在 Hadoop 中以备存档之用。

无论情况如何,能够将数据从 Hadoop 获取到关系数据库并返回是将 Hadoop 集成到 IT 基础设施中的一个关键方面。 那么,让我们看看怎么做。

设置 MySQL

在从关系数据库读取和写入数据之前,我们需要一个正在运行的关系数据库。 我们将在本章中使用 MySQL,因为它是免费且广泛可用的,而且许多开发人员在其职业生涯的某个时候都使用过它。 当然,您可以使用任何可用的 JDBC 驱动程序的 RDBMS,但如果这样做,则需要修改本章中需要与数据库服务器直接交互的方面。

行动时间-安装和设置 MySQL

让我们安装 MySQL 并配置基本数据库和访问权限。

  1. 在 Ubuntu 主机上,使用apt-get

    $ apt-get update
    $ apt-get install mysql-server
    
    

    安装 MySQL

  2. 按照提示操作,并在系统提示时选择合适的超级用户密码。

  3. 安装后,连接到 MySQL 服务器:

    $ mysql -h localhost -u root -p
    
    
  4. 提示时输入根密码:

    Welcome to the MySQL monitor.  Commands end with ; or \g.
    Your MySQL connection id is 40
    …
    Mysql>
    
    
  5. Create a new database to use for the examples in this chapter:

    Mysql> create database hadooptest;
    
    

    您将收到以下响应:

    Query OK, 1 row affected (0.00 sec)
    
    
  6. Create a user account with full access to the database:

    Mysql>  grant all on hadooptest.* to 'hadoopuser'@'%' identified by 'password';
    
    

    您将收到以下响应:

    Query OK, 0 rows affected (0.01 sec)
    
    
  7. Reload the user privileges to have the user changes take effect:

    Mysql> flush privileges;
    
    

    您将收到以下响应:

    Query OK, 0 rows affected (0.01 sec)
    
    
  8. Log out as root:

    mysql> quit;
    
    

    您将收到以下响应:

    Bye
    
    
  9. 以新创建的用户身份登录,在系统提示时输入密码:

    $ mysql -u hadoopuser -p
    
    
  10. 更改到新创建的数据库:

```scala
mysql> use hadooptest;

```
  1. Create a test table, drop it to confirm the user has the privileges in this database, and then log out:
```scala
mysql> create table tabletest(id int);
mysql> drop table tabletest;
mysql> quit;

```

![Time for action – installing and setting up MySQL](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/hadoop-begin-guide/img/7300_09_01.jpg)

刚刚发生了什么?

由于像apt这样的包管理器的奇迹,安装像 MySQL 这样的复杂软件真的很容易。 我们只是使用标准过程来安装一个包;在 Ubuntu(实际上还有大多数其他发行版)下,请求 MySQL 的主服务器包将带来所有需要的依赖项以及客户端包。

在安装过程中,系统将提示您输入数据库的超级用户密码。 即使这是一个测试数据库实例,没有人会使用,并且没有有价值的数据,也请给 root 用户一个强密码。 使用弱根密码是一种坏习惯,我们不想鼓励这种做法。

安装 MySQL 之后,我们使用mysql命令行实用程序连接到数据库。 这需要一系列选项,但我们将使用以下选项:

  • -h:此选项用于指定数据库的主机名(如果未指定,则假定为本地计算机)
  • -u:此选项用于连接的用户名(默认为当前 Linux 用户)
  • -p:此选项用于提示输入用户密码

MySQL 有多个数据库的概念,每个数据库都是表的集合分组。 每个表都需要与一个数据库相关联。 MySQL 有几个内置数据库,但是我们使用CREATE DATABASE语句来创建一个名为hadooptest的新数据库,以便我们稍后进行工作。

除非明确授予请求用户执行操作所需的权限,否则 MySQL 拒绝执行操作的连接/请求。 我们不想以 root 用户的身份执行所有操作(这是一种糟糕的做法,而且非常危险,因为 root 可以修改/删除所有内容),因此我们使用GRANT语句创建了一个名为hadoopuser的新用户。

我们使用的GRANT语句实际上做了三件不同的事情:

  • 创建hadoopuser帐户
  • 设置hadoopuser密码;我们将其设置为password,显然您永远不应该这样做;选择容易记忆的密码
  • 授予hadoopuserhadooptest数据库及其所有表的所有权限

我们发出FLUSH PRIVILEGES命令以使这些更改生效,然后我们以 root 用户身份注销并以新用户身份连接以检查所有操作是否正常。

这里的USE语句有点多余。 将来,我们可以将数据库名称添加到mysql命令行工具中,以自动更改为该数据库。

以新用户身份连接是一个好兆头,但为了获得充分的信心,我们在hadooptest数据库中创建了一个新表,然后删除它。 此处的成功表明hadoopuser确实拥有修改数据库所需的权限。

有必要这么难吗?

我们在这里可能有点谨慎,因为我们检查了整个过程的每一步。 然而,我在过去发现,细微的拼写错误,特别是在GRANT语句中,可能会导致以后很难诊断的问题。 为了继续我们的偏执,让我们对默认的 MySQL 配置做一个更改,我们现在还不需要这样做,但是如果我们不这样做,我们以后会后悔的。

对于任何生产数据库,您当然不会有从书中键入的安全敏感语句,比如GRANT。 请参阅数据库文档以了解用户帐户和权限。

该采取行动了-将 MySQL 配置为允许远程连接

我们需要更改常见的默认 MySQL 行为,这将阻止我们从其他主机访问数据库。

  1. 在您喜欢的文本编辑器中编辑/etc/mysql/my.cnf并查找该行:

    bind-address = 127.0.0.1
    
  2. 将其更改为:

    # bind-address = 127.0.0.1
    
  3. 重新启动 MySQL:

    $ restart mysql
    
    

刚刚发生了什么?

大多数开箱即用的 MySQL 配置仅允许从运行服务器的同一主机进行访问。 从安全角度来看,这绝对是正确的默认设置。 但是,例如,如果您启动了试图访问该主机上的数据库的 MapReduce 作业,也可能会造成真正的混乱。 您可能会看到作业因连接错误而失败。 如果发生这种情况,您将在主机上启动mysql命令行客户端;这将成功。 然后,您可能会编写一个快速 JDBC 客户端来测试连通性。 这也会奏效。 只有当您从 Hadoop 工作节点之一尝试这些步骤时,问题才会显现出来。 是的,这在过去已经咬了我好几次了!

前面的更改告诉 MySQL 绑定到所有可用接口,从而可以从远程客户端访问。

进行更改后,我们需要重新启动服务器。 在 Ubuntu11.10 中,许多服务脚本已经移植到Upstart框架,我们可以直接使用方便的restart命令。

如果您使用的是 Ubuntu 之外的发行版--甚至可能是不同版本的 Ubuntu--全局 MySQL 配置文件可能位于不同的位置;例如,CentOS 和 Red Hat Enterprise Linux 上的/etc/my.cnf

在生产中不要这样做!

或者至少在不考虑后果的情况下不会这么做。 在前面的示例中,我们给新用户提供了一个非常糟糕的密码;请不要这样做。 但是,如果您随后使数据库在整个网络上可用,则尤其不要做这样的事情。 是的,这是一个没有有价值数据的测试数据库,但令人惊讶的是,有这么多测试数据库存活了很长一段时间,并且开始变得越来越关键。 完成后,您会记得删除具有弱密码的用户吗?

说教说得够多了。 数据库需要数据。 让我们向本章将使用的hadooptest数据库添加一个表。

该行动了-设置员工数据库

如果没有 Employee 表的例子,对数据库的讨论就不完整,所以我们将遵循传统从这里开始。

  1. 使用以下条目创建名为employees.tsv的制表符分隔文件:

    Alice  Engineering  50000  2009-03-12
    BobSales  35000  2011-10-01
    Camille  Marketing  40000  2003-04-20
    David  Executive  75000  2001-03-20
    Erica  Support  34000  2011-07-07
    
  2. 连接到 MySQL 服务器:

    $ mysql -u hadoopuser -p hadooptest
    
    
  3. 创建表:

    Mysql> create table employees(
    first_name varchar(10) primary key,
    dept varchar(15),
    salary int,
    start_date date
    ) ;
    
    
  4. Load the data from the file into the database:

    mysql> load data local infile '/home/garry/employees.tsv'
     -> into table employees
     -> fields terminated by '\t' lines terminated by '\n' ;
    
    

    Time for action – setting up the employee database

刚刚发生了什么?

这是相当标准的数据库内容。 我们创建了一个制表符分隔的数据文件,在数据库中创建表,然后使用LOAD DATA LOCAL INFILE语句将数据导入到表中。

我们在这里使用的是一组非常小的数据,因为它实际上仅用于说明目的。

注意数据文件访问权限

不要省略LOAD DATA语句中的LOCAL部分;这样做会看到 MySQL 以 MySQL 用户的身份尝试并加载文件,这通常会导致访问问题。

将数据导入 Hadoop

既然我们已经投入了所有的前期工作,让我们来看看将数据从 MySQL 转移到 Hadoop 的方法。

使用 MySQL 工具和手动导入

将数据导出到 Hadoop 的最简单方法是使用现有的命令行工具和语句。 要导出整个表(或者实际上是整个数据库),MySQL 提供了mysqldump实用程序。 要进行更精确的导出,我们可以使用以下形式的SELECT语句:

SELECT col1, col2 from table
INTO OUTFILE '/tmp/out.csv'
FIELDS TERMINATED by ',', LINES TERMINATED BY '\n';

一旦有了导出文件,我们就可以使用hadoop fs -put将其移动到 HDFS 中,或者通过上一章中讨论的方法将其移动到配置单元中。

让 Go 成为英雄-将 EMPLOYEE 表导出到 HDFS

我们不希望本章将变成 MySQL 教程,因此请查找mysqldump实用程序的语法,并使用它或SELECT … INTO OUTFILE语句将 EMPLOYEE 表导出为以制表符分隔的文件,然后将其复制到 HDFS。

从映射器访问数据库

对于我们这个简单的示例,前面的方法很好,但是如果您需要导出更大的数据集,特别是如果要由 MapReduce 作业处理这些数据,那该怎么办呢?

最明显的方法是在 MapReduce 输入作业中直接进行 JDBC 访问,该作业从数据库中提取数据并将其写入 HDFS,为进一步的处理做好准备。

这是一种有效的技术,但也有一些不太明显的问题。

您需要小心您对数据库施加了多少负载。 将这类作业放到一个非常大的集群上可能会非常迅速地融化数据库,因为成百上千的映射器试图同时打开连接并读取同一个表。 最简单的访问模式也可能是每行一个查询,这样就无法使用更高效的大容量访问语句。 即使数据库可以承担负载,数据库网络连接也很有可能很快成为瓶颈。

要在所有映射器中有效地并行化查询,您需要一种策略来将表划分为每个映射器将检索的段。 然后,您需要确定每个映射器如何传递其段参数。

如果检索到的段很大,除非您显式报告进度,否则很可能最终会出现由 Hadoop 框架终止的长时间运行的任务。

对于一项概念上简单的任务来说,这实际上是相当多的工作。 为此,使用现有的工具不是更好吗? 确实有这样一个工具,我们将在本章的其余部分中使用,它就是 Sqoop。

更好的方式--引入 Sqoop

是由 Cloudera(Hadoop),一家公司创建的,该公司除了提供自己的 http://www.cloudera.com 发行版打包外,还提供大量与 Hadoop 相关的服务,这一点我们将在第 11 章下一步去哪里中讨论。

除了提供这个打包的 Hadoop 产品之外,该公司还创建了许多工具供社区使用,Sqoop 就是其中之一。 它的工作就是做我们需要的事情,在 Hadoop 和关系数据库之间复制数据。 虽然它最初是由 Cloudera 开发的,但它已经向 Apache 软件基金会捐款,它的主页是http://sqoop.apache.org

行动时间-下载和配置 Sqoop

让我们下载并安装并配置 Sqoop。

  1. 转到 Sqoop 主页,选择不低于 1.4.1 的最稳定版本的链接,并将其与您正在使用的 Hadoop 版本进行匹配。 下载该文件。

  2. 将检索到的文件复制到您希望将其安装在系统上的位置;然后将其解压缩:

    $mv sqoop-1.4.1-incubating__hadoop-1.0.0.tar.gz_ /usr/local
    $ cd /usr/local
    $ tar –xzf sqoop-1.4.1-incubating__hadoop-1.0.0.tar.gz_
    
    
  3. 创建符号链接:

    $ ln -s sqoop-1.4.1-incubating__hadoop-1.0.0 sqoop
    
    
  4. 更新您的环境:

    $ export SQOOP_HOME=/usr/local/sqoop
    $ export PATH=${SQOOP_HOME}/bin:${PATH}
    
    
  5. 为您的数据库下载 jdbc 驱动程序;对于 mysql,我们可以在http://dev.mysql.com/downloads/connector/j/5.0.html找到它。

  6. 将下载的 JAR 文件复制到 Sqooplib目录:

    $ cp mysql-connector-java-5.0.8-bin.jar /opt/sqoop/lib
    
    
  7. Test Sqoop:

    $ sqoop help
    
    

    您将看到以下输出:

    usage: sqoop COMMAND [ARGS]
    Available commands:
     codegen            Generate code to interact with database records
    …
     version            Display version information
    See 'sqoop help COMMAND' for information on a specific command.
    
    

刚刚发生了什么?

Sqoop 是一个非常简单的安装工具。 从 Sqoop 主页下载所需版本后-小心选择与我们的 Hadoop 版本匹配的版本-我们复制并解压缩文件。

同样,我们需要设置一个环境变量,并将 Sqoopbin目录添加到我们的路径中,这样我们就可以直接在 Shell 中设置这些变量,或者像前面一样,将这些步骤添加到我们可以在开发会话之前获取的配置文件中。

Sqoop 需要访问数据库的 JDBC 驱动程序;对于我们来说,我们下载了 MySQL 连接器并将其复制到 Sqooplib目录中。 对于最流行的数据库,这是 Sqoop 所需的所有配置;如果您想要使用一些新奇的东西,请参考 Sqoop 文档。

在此最小安装之后,我们执行了sqoop命令行实用程序以验证它是否正常工作。

备注

您可能会看到来自 Sqoop 的警告消息,告诉您尚未定义HBASE_HOME等其他变量。 因为我们在这本书中不讨论 HBase,所以我们不需要这个设置,并且将在我们的屏幕截图中省略这样的警告。

Sqoop 和 Hadoop 版本

我们对之前要检索的 Sqoop 版本非常具体;比以前的软件下载更加具体。 在 1.4.1 之前的 Sqoop 版本中,依赖于仅在 Cloudera Hadoop 发行版或 0.21 之后的 Hadoop 版本中提供的核心 Hadoop 类之一上的附加方法。

不幸的是,Hadoop1.0 实际上是 0.20 分支的延续,这意味着 Sqoop 1.3 可以与 Hadoop 0.21 一起工作,但不能与 0.20 或 1.0 一起工作。 为了避免此版本混淆,我们建议使用版本 1.4.1 或更高版本,这样可以消除依赖关系。

不需要额外的 MySQL 配置;如前所述,我们可以通过使用 Sqoop 发现服务器是否没有配置为允许远程客户端。

Sqoop 和 HDFS

我们可以执行的最简单的导入是将数据库表中的数据转储到 HDFS 上的结构化文件。 就这么办吧。

执行操作的时间-将数据从 MySQL 导出到 HDFS

在这里,我们将使用一个简单的示例,在该示例中,我们只需从单个 MySQL 表中提取所有数据,并将其写入 HDFS 上的单个文件。

  1. Run Sqoop to export data from MySQL onto HDFS:

    $ sqoop import --connect jdbc:mysql://10.0.0.100/hadooptest 
    --username hadoopuser \ > --password password --table employees
    
    

    Time for action – exporting data from MySQL to HDFS

  2. Examine the output directory:

    $ hadoop fs -ls employees
    
    

    您将收到以下响应:

    Found 6 items
    -rw-r--r--   3 hadoop supergroup          0 2012-05-21 04:10 /user/hadoop/employees/_SUCCESS
    drwxr-xr-x   - hadoop supergroup          0 2012-05-21 04:10 /user/hadoop/employees/_logs
    -rw-r--r--   3 … /user/hadoop/employees/part-m-00000
    -rw-r--r--   3 … /user/hadoop/employees/part-m-00001
    -rw-r--r--   3 … /user/hadoop/employees/part-m-00002
    -rw-r--r--   3 … /user/hadoop/employees/part-m-00003
    
    
  3. Display one of the result files:

    $ hadoop fs -cat /user/hadoop/employees/part-m-00001
    
    

    您将看到以下输出:

    Bob,Sales,35000,2011-10-01
    Camille,Marketing,40000,2003-04-20
    
    

刚刚发生了什么?

我们不需要任何前言;这里只需要一条 Sqoop 语句。 可以看到,Sqoop 命令行有很多选项;让我们一次只解压一个选项。

Sqoop 中的第一个选项是要执行的任务类型;在本例中,我们希望将数据从关系源导入 Hadoop。 --connect选项指定数据库的 JDBC URI,标准格式为jdbc:<driver>://<host>/<database>。 显然,您需要将 IP 或主机名更改为运行数据库的服务器。

我们使用--username--password选项指定这些属性,最后使用--table指示我们希望从哪个表检索数据。 就是这样! Sqoop 会做剩下的事情。

Sqoop 输出相对冗长,但一定要阅读它,因为它可以很好地了解到底发生了什么。

备注

但是,重复执行 Sqoop 可能会包含关于已存在的生成文件的嵌套错误。 暂时忽略这一点。

首先,在前面的步骤中,我们看到 Sqoop 告诉我们不要使用--password选项,因为它本质上是不安全的。 Sqoop 有一个替代的-P命令,它会提示输入密码;我们将在以后的示例中使用该命令。

我们还收到关于使用文本主键列的警告,这是一个非常糟糕的主意;稍后将对此进行详细介绍。

然而,在所有设置和警告之后,我们看到 Sqoop 执行了一个 MapReduce 作业并成功完成了它。

默认情况下,Sqoop 将输出文件放入运行作业的用户的主目录中的一个目录中。 这些文件将位于与源表同名的目录中。 为了验证这一点,我们使用hadoop fs -ls检查了这个目录,并确认它包含几个文件,很可能比我们预期的要多,考虑到这么小的表。 请注意,我们在这里略微缩写了输出,以使其适合一行。

然后,我们检查了其中一个输出文件,发现了产生多个文件的原因;即使表很小,它仍然被分割到多个映射器,因此也就是输出文件。 默认情况下,Sqoop 使用四个映射任务。 在这种情况下,这看起来可能有点奇怪,但通常的情况是导入的数据要大得多。 考虑到希望将数据复制到 HDFS 上,该数据很可能是未来 MapReduce 作业的源,因此多个文件使非常有意义。

映射器和主键列

我们故意使用员工数据集中的文本主键列来设置这种情况。 实际上,主键更有可能是一个自动递增的数字雇员 ID。然而,这个选择突出了 Sqoop 处理表格的方式及其对主键的使用的本质。

Sqoop 使用主键列来确定如何在其映射器之间分割源数据。 但是,正如前面的警告所述,这意味着我们依赖基于字符串的比较,并且在大小写意义不完美的环境中,结果可能是不正确的。 理想情况是按照建议使用数值列。

或者,也可以使用-m选项控制映射器的数量。 如果我们使用-m 1,将只有一个映射器,并且不会尝试对主键列进行分区。 对于像我们这样的小数据集,我们也可以这样做,以确保单个输出文件。

这不仅仅是一个选项;如果您尝试从没有主键的表导入,Sqoop 将失败,并显示一个错误,指出从这样的表导入的唯一方法是显式设置单个映射器。

其他选项

在导入数据时,不要认为 Sqoop 要么全有要么什么都不是。 Sqoop 还有其他几个选项来指定、限制和更改从数据库提取的数据。 我们将在下面讨论配置单元的部分中对其进行说明,但请记住,在导出到 HDFS 时也可以使用大多数配置单元。

Sqoop 的架构

现在我们已经看到了 Sqoop 的运行,有必要花一些时间来澄清它的架构,看看它是如何工作的。 在几种方式上,Sqoop 与 Hadoop 的交互方式与 Have 非常相似;两者都是创建一个或多个 MapReduce 作业来执行任务的单一客户端程序。

Sqoop 没有任何服务器进程;我们运行的命令行客户端就是它的全部。 但是,因为它可以根据手头的特定任务定制生成的 MapReduce 代码,所以它往往可以非常高效地利用 Hadoop。

前面在主键上拆分源 RDBMS 表的示例就是一个很好的例子。 Sqoop 知道将在 MapReduce 作业中配置的映射器的数量(如前所述,缺省值为 4),因此它可以对源表进行智能分区。

如果我们假设一个表有 100 万条记录和 4 个映射器,那么每个映射器将处理 25 万条记录。 凭借对主键列的了解,Sqoop 可以创建四条 SQL 语句来检索数据,每条语句都使用所需的主键列范围作为警告。 在最简单的情况下,这可以像在第一条语句中添加类似WHERE id BETWEEN 1 and 250000的内容,并对其他语句使用不同的id范围一样简单。

当从 Hadoop 导出数据时,我们将看到相反的行为,因为 Sqoop 再次并行跨多个映射器的数据检索,并优化将该数据插入到关系数据库中。 然而,所有这些智能都被推入在 Hadoop 上执行的 MapReduce 作业中;Sqoop 命令行客户端的工作是尽可能高效地生成这些代码,然后在处理发生时避开它。

使用 Sqoop 将数据导入配置单元

Sqoop 有与配置单元的重要集成,允许它将数据从关系源导入到新的或现有的配置单元表中。 有多种方法可以定制此流程,但同样,让我们从简单的案例开始。

执行操作的时间-将数据从 MySQL 导出到配置单元

对于本例,我们将将单个 MySQL 表中的所有数据导出到配置单元中对应命名的表中。 您需要按照上一章中的详细说明安装和配置配置单元。

  1. Delete the output directory created in the previous section:

    $ hadoop fs -rmr employees
    
    

    您将收到以下响应:

    Deleted hdfs://head:9000/user/hadoop/employees
    
    
  2. Confirm Hive doesn't already contain an employees table:

    $ hive -e "show tables like 'employees'"
    
    

    您将收到以下响应:

    OK
    Time taken: 2.318 seconds
    
    
  3. Perform the Sqoop import:

    $ sqoop import --connect jdbc:mysql://10.0.0.100/hadooptest 
    --username hadoopuser -P
    --table employees --hive-import --hive-table employees 
    
    

    Time for action – exporting data from MySQL into Hive

  4. Check the contents in Hive:

    $ hive -e "select * from employees"
    
    

    您将收到以下响应:

    OK
    Alice  Engineering  50000  2009-03-12
    Camille  Marketing  40000  2003-04-20
    David  Executive  75000  2001-03-20
    Erica  Support  34000  2011-07-07
    Time taken: 2.739 seconds
    
    
  5. Examine the created table in Hive:

    $ hive -e "describe employees"
    
    

    您将收到以下响应:

    OK
    first_name  string 
    dept  string 
    salary  int 
    start_date  string 
    Time taken: 2.553 seconds
    
    

刚刚发生了什么?

同样,我们使用带有两个新选项的 Sqoop 命令,--hive-import告诉 Sqoop 最终目的地是配置单元而不是 HDFS,--hive-table指定配置单元中我们希望将数据导入到的表名。

实际上,如果配置单元表与--table选项指定的源表相同,则不需要指定配置单元表的名称。 但是,它确实让事情变得更加明确,所以我们通常会将其包括在内。

和前面一样,一定要阅读完整的 Sqoop 输出,因为它提供了对正在发生的事情的很好的洞察力,但是最后几行突出显示了成功地导入到新的 hive 表中。

我们看到 Sqoop 从 MySQL 检索 5 行,然后经历将它们复制到 HDFS 并导入到配置单元的各个阶段。 接下来我们将讨论警告重新类型转换。

在 Sqoop 完成这个过程之后,我们使用配置单元从新的配置单元表中检索数据,并确认它是我们所期望的。 然后,我们检查创建的表的定义。

在这一点上,我们确实看到了一件奇怪的事情:start_date列被赋予了一个类型字符串,尽管它最初是 MySQL 中的 SQLDATE类型。

我们在 Sqoop 执行过程中看到的警告解释了这种情况:

12/05/23 13:06:33 WARN hive.TableDefWriter: Column start_date had to be cast to a less precise type in Hive

这是因为配置单元不支持除TIMESTAMP以外的任何时态数据类型。 在导入的数据属于另一种类型(与日期或时间相关)的情况下,Sqoop 会将其转换为字符串。 稍后我们将研究处理这种情况的方法。

此示例非常常见,但我们并不总是希望将整个表导入到配置单元中。 有时,我们希望只包含特定的列或应用谓词来减少所选项目的数量。 Sqoop 允许我们两者兼而有之。

采取行动的时间-更有选择性的进口

让我们通过执行受条件表达式限制的导入来看看这是如何工作的。

  1. Delete any existing employee import directory:

    $ hadoop fs -rmr employees
    
    

    您将收到以下响应:

    Deleted hdfs://head:9000/user/hadoop/employees
    
    
  2. Import selected columns with a predicate:

    sqoop import --connect jdbc:mysql://10.0.0.100/hadooptest 
    --username hadoopuser -P
    --table employees --columns first_name,salary
     --where "salary > 45000" 
    --hive-import --hive-table salary 
    
    

    您将收到以下响应:

    12/05/23 15:02:03 INFO hive.HiveImport: Hive import complete.
    
    
  3. Examine the created table:

    $ hive -e "describe salary"
    
    

    您将收到以下响应:

    OK
    first_name  string 
    salary  int 
    Time taken: 2.57 seconds
    
    
  4. Examine the imported data:

    $ hive -e "select * from salary"
    
    

    您将看到以下输出:

    OK
    Alice  50000
    David  75000
    Time taken: 2.754 seconds
    
    

刚刚发生了什么?

这一次,我们的 Sqoop 命令首先添加了--columns选项,该选项指定要在导入中包括哪些列。 这是一个逗号分隔的列表。

我们还使用了--where选项,该选项允许应用于用于从数据库提取数据的 SQL 的WHERE子句的自由文本规范。

这些选项的组合是,我们的 Sqoop 命令应该只导入薪水大于WHERE子句中指定的阈值的人的姓名和薪水。

我们执行该命令,看到它成功完成,然后检查在配置单元中创建的表。 我们看到它确实只包含指定的列,然后显示表内容以验证 WHERE 谓词是否也被正确应用。

数据类型问题

第八章与配置单元的数据关系视图中,我们提到了配置单元并不支持所有常见的 SQL 数据类型。 特别是DATEDATETIME类型目前还没有实现,尽管它们确实作为已确定的配置单元问题存在;因此,希望将来会添加它们。 我们在本章早些时候看到了这一影响,这是我们第一次导入 Hive。 尽管 MySQL 中的start_date列是DATE类型,但 Sqoop 导入标记了一个转换警告,并且配置单元中的结果列是STRING类型。

Sqoop 有一个在这里很有用的选项,即我们可以使用--map-column-hive显式地告诉 Sqoop 如何在生成的配置单元表中创建列。

操作时间-使用类型映射

让我们使用类型映射来改进数据导入。

  1. 删除任何现有输出目录:

    $ hadoop fs -rmr employees
    
    
  2. Execute Sqoop with an explicit type mapping:

    sqoop import --connect jdbc:mysql://10.0.0.100/hadooptest --username hadoopuser 
    -P --table employees 
    --hive-import --hive-table employees 
    --map-column-hive start_date=timestamp
    
    

    您将收到以下响应:

    12/05/23 14:53:38 INFO hive.HiveImport: Hive import complete.
    
    
  3. Examine the created table definition:

    $ hive -e "describe employees"
    
    

    您将收到以下响应:

    OK
    first_name  string 
    dept  string 
    salary  int 
    start_date  timestamp 
    Time taken: 2.547 seconds
    
    
  4. Examine the imported data:

    $ hive -e "select * from employees";
    
    

    您将收到以下响应:

    OK
    Failed with exception java.io.IOException:java.lang.IllegalArgumentException: Timestamp format must be yyyy-mm-dd hh:mm:ss[.fffffffff]
    Time taken: 2.73 seconds
    
    

刚刚发生了什么?

这里的 Sqoop 命令行类似于原始的配置单元导入,只是增加了列映射规范。 我们指定start_date列应该是TIMESTAMP类型,并且我们可以添加其他规范。 该选项接受此类映射的逗号分隔列表。

在确认 Sqoop 成功执行之后,我们检查了创建的配置单元表,并验证确实应用了映射,并且start_date列的类型为TIMESTAMP

然后,我们尝试从表中检索数据,但没有成功,收到关于类型格式不匹配的错误。

仔细想想,这并不令人意外。 尽管我们将所需的列类型指定为TIMESTAMP,但是从 MySQL 导入的实际数据的类型是DATE,它不包含时间戳中所需的时间部分。 这是一个重要的教训。 确保类型映射正确只是难题的一部分;我们还必须确保数据对于指定的列类型有效。

执行操作的时间-从原始查询导入数据

让我们看一个导入的示例,其中使用原始 SQL 语句来选择要导入的数据。

  1. 删除任何现有输出目录:

    $ hadoop fs –rmr employees
    
    
  2. 删除任何现有配置单元员工表:

    $ hive -e 'drop table employees'
    
    
  3. 使用显式查询导入数据:

    sqoop import --connect jdbc:mysql://10.0.0.100/hadooptest 
    --username hadoopuser -P
    --target-dir employees 
    --query 'select first_name, dept, salary, 
    timestamp(start_date) as start_date from employees where $CONDITIONS' 
    --hive-import --hive-table employees 
    --map-column-hive start_date=timestamp -m 1
    
    
  4. Examine the created table:

    $ hive -e "describe employees"
    
    

    您将收到以下响应:

    OK
    first_name  string 
    dept  string 
    salary  int 
    start_date  timestamp 
    Time taken: 2.591 seconds
    
    
  5. Examine the data:

    $ hive -e "select * from employees"
    
    

    您将收到以下响应:

    OK
    Alice  Engineering  50000  2009-03-12 00:00:00
    BobSales  35000  2011-10-01 00:00:00
    Camille  Marketing  40000  2003-04-20 00:00:00
    David  Executive  75000  2001-03-20 00:00:00
    Erica  Support  34000  2011-07-07 00:00:00
    Time taken: 2.709 seconds
    
    

刚刚发生了什么?

为了实现我们的目标,我们使用了一种非常不同的 Sqoop 导入形式。 这里我们使用--query选项来定义显式 SQL 语句,而不是指定所需的表,然后让 Sqoop 导入所有列或指定的子集。

在语句中,我们选择了源表中的所有列,但应用timestamp()函数将start_date列转换为正确的类型。 (请注意,此函数仅向日期添加一个00:00时间元素)。 我们为该函数的结果设置别名,这使我们可以在类型映射选项中对其进行命名。

因为我们没有--table选项,所以我们必须添加--target-dir来告诉 Sqoop 它应该在 HDFS 上创建的目录的名称。

SQL 中的WHERE子句是 Sqoop 所需的,尽管我们实际上并没有使用它。 没有--table选项不仅会降低 Sqoop 自动生成导出目录名称的能力,还意味着 Sqoop 不知道从哪里检索数据,因此不知道如何跨多个映射器划分数据。 $CONDITIONS变量与--where选项结合使用;指定后者为 Sqoop 提供了对表进行适当分区所需的信息。

我们在这里采用了一种不同的方法,而是将映射器的数量显式设置为1,这样就不需要显式分区子句了。

执行 Sqoop 之后,我们检查配置单元中的表定义,与前面一样,它具有所有列的正确数据类型。 然后我们查看数据,现在成功了,start_date列数据被适当地转换为TIMESTAMP值。

备注

当我们在Sqoop 和 HDFS部分提到 Sqoop 提供了限制从数据库提取的数据的机制时,我们指的是querywherecolumns选项。 请注意,无论目的地如何,任何 Sqoop 导入都可以使用它们。

来个围棋英雄

虽然对于这么小的数据集来说确实不需要它,但$CONDITIONS变量是一个重要的工具。 修改前面的 Sqoop 语句以使用具有显式分区语句的多个映射器。

Sqoop 和 Hive 分区

与配置单元的数据关系视图中,我们谈到了很多关于配置单元分区的内容,并强调了它们在允许对非常大的表进行查询优化方面的重要性。 好消息是 Sqoop 可以支持配置单元分区;坏消息是支持不完整。

要将数据从关系数据库导入分区配置单元表,我们使用--hive-partition-key选项指定分区列,使用--hive-partition-value选项指定此 Sqoop 命令将向其中导入数据的分区的值。

这很好,但确实需要将每个 Sqoop 语句导入到单个配置单元分区中;目前不支持配置单元自动分区。 相反,如果要将数据集导入到表中的多个分区中,则需要使用单独的 Sqoop 语句插入到每个分区中。

字段和行终止符

到目前为止,我们一直在隐含地依赖于一些默认值,但现在应该讨论它们。 我们的原始文本文件是用制表符分隔的,但您可能已经注意到,我们导出到 HDFS 的数据是用逗号分隔的。 如果您查看/user/hive/warehouse/employees下的文件(请记住,这是 HDFS 上配置单元保存其源文件的默认位置),记录将使用 ASCII 代码 001 作为分隔符。 怎么一回事?

在第一个实例中,我们让 Sqoop 使用其缺省值,在本例中,这意味着使用逗号分隔字段,并使用\n记录。 但是,当 Sqoop 导入配置单元时,它使用配置单元默认值,包括使用 001 代码(^A)分隔字段。

我们可以使用以下 Sqoop 选项显式设置分隔符:

  • fields-terminated-by:这是字段之间的分隔符
  • lines-terminated-by:行终止符
  • escaped-by:用于转义字符(例如,)
  • enclosed-by:将字段括起来的字符(例如,“)
  • optionally-enclosed-by:类似于前面的选项,但不是必填项
  • mysql-delimiters:使用 MySQL 默认值的快捷方式

这看起来可能有点吓人,但它并不像术语可能暗示的那样晦涩难懂,并且具有 SQL 经验的人应该熟悉其中的概念和语法。 前几个选项非常不言自明;当谈到封闭字符和可选封闭字符时,它就变得不那么清楚了。

这实际上是关于(通常是自由格式)数据,其中给定域可能包括具有特殊含义的字符。 例如,逗号分隔的文件中包含逗号的字符串列。 在这种情况下,我们可以用引号将字符串列括起来,以允许在字段中使用逗号。 如果所有字段都需要这样的封闭字符,我们将使用第一个表单;如果只有一个字段子集需要这样的表单,则可以将其指定为可选。

从 Hadoop 获取数据

我们说 Hadoop 和关系数据库之间的数据流很少是线性的单向过程。 事实上,在 Hadoop 中处理数据,然后将其插入到关系数据库中的情况可以说是更常见的情况。 我们现在就来探索这一点。

从减速机内写入数据

考虑如何将 MapReduce 作业的输出复制到关系数据库中,我们发现了与查看数据导入 Hadoop 问题时类似的注意事项。

最明显的方法是修改减法器,为每个键及其关联值生成输出,然后通过 JDBC 将它们直接插入到数据库中。 我们不必像导入情况那样担心源列分区,但仍然需要考虑我们对数据库施加了多少负载,以及我们是否需要考虑长期运行任务的超时。 此外,与映射器的情况一样,这种方法倾向于对数据库执行许多单个查询,这通常比批量操作的效率低得多。

从减速器写入 SQL 导入文件

通常,更好的方法不是像前面的示例那样绕过通常的 MapReduce 生成输出文件的情况,而是利用它。

所有关系数据库都能够通过自定义工具或使用LOAD DATA语句从源文件获取数据。 因此,在 Reducer 中,我们可以修改数据输出,使其更容易被吸收到我们的关系目标中。 这样就不需要考虑减速器给数据库带来负载或如何处理长期运行的任务等问题,但它确实需要在我们的 MapReduce 作业之外进行第二步。

更好的方式-再次 Sqoop

如果你看过 Sqoop 内置帮助或其在线文档的输出,了解到 Sqoop 也可以成为我们从 Hadoop 导出数据的首选工具,这可能不会令人惊讶--如果你看过 Sqoop 内置帮助或其在线文档的话。

开始行动-将数据从 Hadoop 导入 MySQL

让我们通过将数据从 HDFS 文件导入到 MySQL 表中来演示这一点。

  1. 使用以下条目创建名为newemployees.tsv的制表符分隔文件:

    Frances  Operations  34000  2012-03-01
    Greg  Engineering  60000  2003-11-18
    Harry  Intern  22000  2012-05-15
    Iris  Executive  80000  2001-04-08
    Jan  Support  28500  2009-03-30
    
  2. 在 HDFS 上创建一个新目录,并将文件复制到其中:

    $hadoop fs -mkdir edata
    $ hadoop fs -put newemployees.tsv edata/newemployees.tsv
    
    
  3. Confirm the current number of records in the employee table:

    $ echo "select count(*) from employees" | 
    mysql –u hadoopuser –p hadooptest
    
    

    您将收到以下响应:

    Enter password: 
    count(*)
    5
    
    
  4. Run a Sqoop export:

    $ sqoop export --connect jdbc:mysql://10.0.0.100/hadooptest 
    --username hadoopuser  -P --table employees 
    --export-dir edata --input-fields-terminated-by '\t'
    
    

    您将收到以下响应:

    12/05/27 07:52:22 INFO mapreduce.ExportJobBase: Exported 5 records.
    
    
  5. Check the number of records in the table after the export:

    Echo "select count(*) from employees" 
    | mysql -u hadoopuser -p hadooptest
    
    

    您将收到以下响应:

    Enter password: 
    count(*)
    10
    
    
  6. Check the data:

    $ echo "select * from employees" 
    | mysql -u hadoopuser -p hadooptest
    
    

    您将收到以下响应:

    Enter password: 
    first_name  dept  salary  start_date
    Alice  Engineering  50000  2009-03-12
    …
    Frances  Operations  34000  2012-03-01
    Greg  Engineering  60000  2003-11-18
    Harry  Intern  22000  2012-05-15
    Iris  Executive  80000  2001-04-08
    Jan  Support  28500  2009-03-30
    
    

刚刚发生了什么?

我们首先创建了一个数据文件,其中包含另外五名员工的信息。 我们在 HDFS 上为数据创建了一个目录,并将新文件复制到该目录中。

在运行导出之前,我们确认 MySQL 中的表只包含最初的五名员工。

Sqoop 命令的结构与以前类似,最大的变化是使用了export命令。 顾名思义,Sqoop 将导出数据从 Hadoop 导出到关系数据库。

我们使用了几个与前面类似的选项,主要用于指定数据库连接、连接所需的用户名和密码以及要向其中插入数据的表。

因为我们要从 HDFS 导出数据,所以需要指定包含要导出的所有文件的位置,这是通过--export-dir选项完成的。 目录中包含的所有文件都将被导出;它们不需要位于单个文件中;Sqoop 将包括其 MapReduce 作业中的所有文件。 默认情况下,Sqoop 使用四个映射器;如果您有大量的文件,增加这个数字可能会更有效;不过,一定要进行测试,以确保数据库上的负载保持在可控范围内。

传递给 Sqoop 的最后一个选项指定了源文件中使用的字段终止符,在本例中是制表符。 确保数据文件的格式正确是您的责任;Sqoop 将假定每个记录中的元素数量与表中的列相同(尽管 NULL 是可以接受的),并由指定的字段分隔符分隔。

在观看 Sqoop 命令成功完成之后,我们看到它报告它导出了 5 条记录。 我们使用mysql工具检查数据库中现在的行数,然后查看数据以确认新员工加入了我们的老朋友。

Sqoop 导入和导出之间的差异

虽然在概念上和命令行调用中类似,但 Sqoop 导入和导出之间有许多重要的区别值得研究。

首先,Sqoop 导入可以假设更多关于正在处理的数据的信息;通过显式命名的表或添加的谓词,有很多关于数据结构和类型的信息。 但是,Sqoop 导出仅提供源文件的位置以及用于分隔和括起字段和记录的字符。 虽然 Sqoop 导入到配置单元可以根据提供的表名和结构自动创建新表,但是 Sqoop 导出必须导入到关系数据库中的现有表中。

尽管我们前面的日期和时间戳演示显示了一些尖锐的边缘,但是 Sqoop 导入也能够确定源数据是否符合定义的列类型;否则,数据将不可能插入到数据库中。 同样,Sqoop 导出只能有效地访问字符字段,而不了解真实数据类型。 如果您拥有非常干净和格式良好的数据,这可能永远不重要,但对于我们其他人来说,将需要考虑数据导出和类型转换,特别是在 NULL 和默认值方面。 Sqoop 文档详细介绍了这些选项,值得一读。

插入与更新

我们前面的示例非常简单;我们添加了一组完整的新数据,它们可以愉快地与表的现有内容共存。 默认情况下,Sqoop 导出执行一系列附加操作,将每条记录作为表中的新行添加。

但是,如果我们稍后想要更新数据,例如,我们的员工在年底加薪时,该怎么办? 由于数据库表将first_name定义为主键,任何插入与现有员工同名的新行的尝试都将失败,并导致主键约束失败。

在这种情况下,我们可以设置 Sqoop--update-key选项来指定主键,Sqoop 将基于该键(可以是逗号分隔的键列表)生成UPDATE语句,而不是添加新行的INSERT语句。

备注

在此模式下,任何与现有键值不匹配的记录都将被静默忽略,如果一条语句更新多行,Sqoop 将不会标记错误。

如果我们还想要为不存在的数据添加新行的更新选项,我们可以将--update-mode选项设置为allowinsert

来个围棋英雄

创建另一个数据文件,其中包含三名新员工以及两名现有员工的更新工资。 在导入模式下使用 Sqoop 既可以添加新员工,也可以应用所需的更新。

Sqoop 和配置单元导出

鉴于前面的示例,了解到 Sqoop 目前没有任何直接支持将配置单元表导出到关系数据库中可能并不令人惊讶。 更准确地说,我们之前使用的--hive-import选项没有显式的等价物。

但是,在某些情况下,我们可以解决此问题。 如果配置单元表格以文本格式存储其数据,我们可以将 Sqoop 指向 HDFS 上表格数据文件的位置。 在表引用外部数据的情况下,这可能很简单,但是一旦我们开始看到带有复杂分区的 Hive 表,目录结构就会变得更加复杂。

HIVE 还可以将表存储为 Binary SequenceFiles,当前的限制是 Sqoop 不能透明地从以这种格式存储的表导出。

操作时间-将配置单元数据导入 MySQL

不管这些限制如何,让我们演示一下,在正确的情况下,我们可以使用 Sqoop 直接导出存储在配置单元中的数据。

  1. Remove any existing data in the employee table:

    $ echo "truncate employees" | mysql –u hadoopuser –p hadooptest
    
    

    您将收到以下响应:

    Query OK, 0 rows affected (0.01 sec)
    
    
  2. Check the contents of the Hive warehouse for the employee table:

    $ hadoop fs –ls /user/hive/warehouse/employees
    
    

    您将收到以下响应:

    Found 1 items
    … /user/hive/warehouse/employees/part-m-00000
    
    
  3. Perform the Sqoop export:

    sqoop export --connect jdbc:mysql://10.0.0.100/hadooptest 
    --username hadoopuser –P --table employees \
    --export-dir /user/hive/warehouse/employees 
    --input-fields-terminated-by '\001' 
    --input-lines-terminated-by '\n'
    
    

    Time for action – importing Hive data into MySQL

刚刚发生了什么?

首先,我们截断 MySQL 中的employees表以删除所有现有数据,然后确认 Employee 表数据就是我们预期的位置。

备注

请注意,Sqoop 还可能在此目录中创建一个后缀为_SUCCESS的空文件;如果存在该文件,则应在运行 Sqoop 导出之前将其删除。

Sqoopexport命令与以前一样;唯一的变化是不同的数据源位置以及添加了显式的字段和行终止符。 回想一下,默认情况下,配置单元的字段和行终止符分别使用 ASCII 代码 001 和\n(不过,还要回想一下,我们之前已经使用其他分隔符将文件导入到配置单元中,因此这是始终需要检查的内容)。

我们执行 Sqoop 命令,并在尝试创建java.sql.Date的实例时看到它因Java IllegalArgumentExceptions而失败。

我们现在遇到了与前面遇到的问题相反的问题;源 MySQL 表中的原始类型具有配置单元不支持的数据类型,并且我们转换了数据以匹配可用的TIMESTAMP类型。 但是,当再次导出数据时,我们现在尝试使用TIMESTAMP值创建DATE,如果不进行一些转换,这是不可能的。

这里的教训是,我们以前进行单向转换的方法只在数据只在一个方向流动的情况下有效。 只要我们需要双向数据传输,配置单元和关系存储之间不匹配的类型就会增加复杂性,并且需要插入转换例程。

执行操作的时间-修复映射并重新运行导出

然而,在本例中,让我们做一些可能更有意义的事情-修改 Employee 表的定义,使其在两个数据源中保持一致。

  1. 启动mysql实用程序:

    $ mysql -u hadoopuser -p hadooptest
    Enter password: 
    
    
  2. Change the type of the start_date column:

    mysql> alter table employees modify column start_date timestamp;
    
    

    您将收到以下响应:

    Query OK, 0 rows affected (0.02 sec)
    Records: 0  Duplicates: 0  Warnings: 0
    
    
  3. Display the table definition:

    mysql> describe employees; 
    
    

    Time for action – fixing the mapping and re-running the export

  4. 退出mysql工具:

    mysql> quit;
    
    
  5. Perform the Sqoop export:

    sqoop export --connect jdbc:mysql://10.0.0.100/hadooptest --username hadoopuser –P –table employees
    --export-dir /user/hive/warehouse/employees 
    --input-fields-terminated-by '\001' 
    --input-lines-terminated-by '\n'
    
    

    您将收到以下响应:

    12/05/27 09:17:39 INFO mapreduce.ExportJobBase: Exported 10 records.
    
    
  6. Check the number of records in the MySQL database:

    $ echo "select count(*) from employees" 
    | mysql -u hadoopuser -p hadooptest
    
    

    您将收到以下输出:

    Enter password: 
    count(*)
    10
    
    

刚刚发生了什么?

在尝试与上次相同的 Sqoop 导出之前,我们使用mysql工具连接到数据库并修改start_date列的类型。 当然,请注意,在生产系统上不应该随意进行这样的更改,但是考虑到我们目前有一个空的测试表,这里不存在任何问题。

进行更改后,我们重新运行了 Sqoop 导出,这一次成功了。

其他 Sqoop 功能

Sqoop 还有许多我们不会详细讨论的其他特性,但我们将重点介绍它们,以便感兴趣的读者可以在 Sqoop 文档中查找它们。

增量合并

我们使用的示例是要么全有要么全无的处理,在大多数情况下,在将数据导入空表时最有意义。 有个处理添加的机制,但是如果我们预见 Sqoop 执行持续的导入,那么一些额外的支持是可用的。

Sqoop 支持增量导入的概念,在这种情况下,导入任务由某个日期额外限定,并且该任务只处理晚于该日期的记录。 这允许构建包括 Sqoop 的长期运行工作流。

避免部分导出

我们已经看到在将数据从 Hadoop 导出到关系数据库时如何发生错误。 对我们来说,这不是一个大问题,因为这个问题导致所有导出的记录都失败了。 但是,只有部分导出失败并导致数据库中的数据部分提交的情况并不少见。

为了降低这种风险,Sqoop 允许使用临时表;它将所有数据加载到这个辅助表中,并且只有在成功插入所有数据之后,才在单个事务中执行移动到主表中的操作。 这对于容易失败的工作负载非常有用,但也有一些重要的限制,比如不能支持更新模式。 对于非常大的导入,还会对单个非常长时间运行的事务的 RDBMS 造成性能和负载影响。

作为代码生成器的 Sqoop

我们一直在忽略 Sqoop 处理过程中的错误,我们不久前很随意地忽略了这个错误--抛出的异常是因为 Sqoop 所需的生成代码已经存在。

在执行导入时,Sqoop 会生成 Java 类文件,这些文件提供了访问所创建文件中的字段和记录的编程方法。 Sqoop 在内部使用这些类,但也可以在 Sqoop 调用外部使用它们,实际上,Sqoopcodegen命令可以在导出任务外部重新生成类。

AWS 注意事项

到目前为止,我们没有在本章提到 AWS,因为 Sqoop 中没有任何内容支持或阻止在 AWS 上使用它。 我们可以在 EC2 主机上像在本地主机上一样轻松地运行 Sqoop,并且它可以访问手动创建的 Hadoop 群集,也可以选择访问 EMR 创建的运行配置单元的 Hadoop 群集。 考虑在 AWS 中使用时,唯一可能出现的问题是安全组访问,因为许多默认 EC2 配置不允许大多数关系数据库使用的端口上的流量(MySQL 默认为 3306)。 但是,这并不比我们的 Hadoop 集群和 MySQL 数据库位于防火墙或任何其他网络安全边界的不同侧更成问题。

考虑 RDS

还有一个我们以前没有提到的 AWS 服务现在确实值得介绍一下。 AmazonRelational Database Service(RDS)在云中提供托管的关系数据库,并提供 MySQL、Oracle 和 Microsoft SQL Server 选项。 RDS 允许从控制台或命令行工具启动实例,而不必担心数据库引擎的安装、配置和管理。 然后,您只需将数据库客户端工具指向数据库,并开始创建表和操作数据。

RDS 和 EMR 是一个强大的组合,它们提供的托管服务大大减轻了手动管理此类服务的痛苦。 如果您需要一个关系数据库,但又不想担心它的管理,那么 RDS 可能适合您。

如果您使用 EC2 主机在 S3 中生成数据或存储数据,RDS 和 EMR 组合可能会特别强大。 亚马逊的一般政策是,在一个地区内将数据从一项服务传输到另一项服务不收取任何费用。 因此,可以让一组 EC2 主机生成大量数据量,这些数据量被推送到 RDS 中的关系数据库中以进行查询访问,并存储在 EMR 中以进行存档和长期分析。 将数据放入存储和处理系统通常是一项具有技术挑战性的活动,如果需要跨商业网络链路移动数据,则很容易消耗大量费用。 构建在协作 AWS 服务(如 EC2、RDS 和 EMR)之上的架构可以最大限度地减少这两个问题。

摘要

在本章中,我们介绍了 Hadoop 和关系数据库的集成。 特别是,我们探索了最常见的用例,并看到 Hadoop 和关系数据库可以成为高度互补的技术。 我们考虑了将数据从关系数据库导出到 HDFS 文件的方法,并意识到诸如主键、列分区和长时间运行的任务等问题使其比乍看起来更难。

然后我们介绍了 Sqoop,这是一个现已捐赠给 Apache 软件基金会的 Cloudera 工具,它为此类数据迁移提供了一个框架。 我们使用 Sqoop 将数据从 MySQL 导入 HDFS,然后导入配置单元,强调了在执行此类任务时必须考虑的数据类型兼容性方面。 我们还使用 Sqoop 执行反向操作-将数据从 HDFS 复制到 MySQL 数据库-发现此路径比其他方向有更微妙的考虑,简要讨论了文件格式和更新与插入任务的问题,并引入了额外的 Sqoop 功能,如代码生成和增量合并。

关系数据库是大多数 IT 基础设施的重要(通常是关键)部分。 但是,它们并不是唯一的这样的组成部分。 Web 服务器和其他应用生成的海量日志文件越来越重要--通常是低调的。 下一章将展示 Hadoop 如何非常适合处理和存储此类数据。

十、使用 Flume 收集数据

在前两章中,我们看到了 Have 和 Sqoop 如何为 Hadoop 提供关系数据库接口,并允许它与“真实”数据库交换数据。 虽然这是一个非常常见的用例,但当然,我们可能希望将许多不同类型的数据源引入 Hadoop。

在本章中,我们将介绍:

  • Hadoop 中常用处理的数据概述
  • 将这些数据放入 Hadoop 的简单方法
  • Apache Flume 如何让这项任务变得容易得多
  • 简单到复杂的 Flume 设置的常用图案
  • 无论采用何种技术都需要考虑的常见问题,如数据生命周期

关于 AWS 的说明

本章讨论的 AW 比本书中的任何其他 AW 都要少。 事实上,在这一节之后我们甚至不会提到它。 亚马逊没有类似于 Flume 的服务,所以我们也没有特定于 AWS 的产品可以探索。 另一方面,在使用 Flume 时,无论是在本地主机上还是在 EC2 虚拟实例上,它的工作方式都完全相同。 因此,本章的其余部分不假定执行这些示例的环境;它们将在每个环境上执行相同的操作。

数据数据无处不在...

在讨论 Hadoop 与其他系统的集成时,很容易将其视为一对一模式。 数据来自一个系统,在 Hadoop 中处理,然后传递到第三个系统。

事情可能在第一天就是这样,但实际情况往往是一系列相互协作的组件,数据流在它们之间来回传递。 我们如何以可维护的方式构建这个复杂的网络是本章的重点。

数据类型

为了便于讨论,我们将数据分为两大类:

  • 网络流量、,其中数据由系统生成并通过网络连接发送
  • 文件数据,其中数据是由系统生成的,并写入文件系统上某处的文件

除了检索数据的方式外,我们不假设这些数据类别有任何不同。

让网络流量进入 Hadoop

当我们说网络数据时,我们指的是通过 HTTP 连接从 Web 服务器检索的信息、客户端应用拉取的数据库内容或通过数据总线发送的消息。 在每种情况下,数据都由客户端应用检索,该应用要么通过网络拉取数据,要么侦听数据到达。

备注

在以下几个示例中,我们将使用curl实用程序检索或发送网络数据。 确保它已安装在您的系统上,如果没有,请安装它。

该采取行动了-将 Web 服务器数据导入 Hadoop

让我们来看看我们如何简化地将数据从 Web 服务器复制到 HDFS。

  1. 将 NameNode Web 界面的文本检索到本地文件:

    $ curl localhost:50070 > web.txt
    
    
  2. Check the file size:

    $ ls -ldh web.txt 
    
    

    您将收到以下响应:

    -rw-r--r-- 1 hadoop hadoop 246 Aug 19 08:53 web.txt
    
    
  3. 将文件复制到 HDFS:

    $ hadoop fs -put web.txt web.txt
    
    
  4. Check the file on HDFS:

    $ hadoop fs -ls 
    
    

    您将收到以下响应:

    Found 1 items
    -rw-r--r--   1 hadoop supergroup        246 2012-08-19 08:53 /user/hadoop/web.txt
    
    

刚刚发生了什么?

这里有应该没有什么令人惊讶的地方。 我们使用curl实用程序从托管 NameNode Web 界面的嵌入式 Web 服务器检索网页,并将其保存到本地文件。 我们检查文件大小,将其复制到 HDFS,并验证文件是否已成功传输。

这里要注意的不是一系列操作--毕竟这只是我们自第 2 章启动并运行以来一直使用的hadoop fs命令的另一种用法--而是我们应该讨论使用的模式。

虽然我们想要的数据位于 Web 服务器中,并且可以通过 HTTP 协议进行访问,但开箱即用的 Hadoop 工具非常基于文件,并且不具备对此类远程信息源的任何内在支持。 这就是我们需要在将网络数据传输到 HDFS 之前将其复制到文件中的原因。

当然,我们可以通过第 3 章编写 MapReduce 作业中提到的编程接口将数据直接写入 HDFS,这样做效果很好。 然而,这需要我们开始为需要从中检索数据的每个不同的网络源编写自定义客户端。

来个围棋英雄

以编程方式检索数据并将其写入 HDFS 是一项非常强大的功能,值得探索一下。 非常流行的 HTTPJava 库是Apache****HTTPClient,位于http://hc.apache.org/httpcomponents-client-ga/index.htmlHTTP Components项目中。

使用 HTTPClient 和 Java HDFS 接口像以前一样检索网页并将其写入 HDFS。

将文件导入 Hadoop

我们前面的示例展示了将基于文件的数据导入 Hadoop 的最简单方法,以及标准命令行工具或编程 API 的使用。 这里没有什么可讨论的,因为这是我们在整本书中一直在讨论的一个主题。

隐藏问题

尽管前面的方法都很好,但有几个原因说明它们可能不适合生产使用。

将网络数据保存在网络上

我们将通过网络访问的数据复制到文件,然后再将其放到 HDFS 上的模型会对性能产生影响。 由于往返到磁盘(系统中最慢的部分)会增加延迟。 对于在一次调用中检索的大量数据来说,这可能不是问题-尽管磁盘空间可能会成为一个问题-但对于高速检索的少量数据,这可能会成为一个真正的问题。

Hadoop 依赖项

对于基于文件的方法,前面提到的模型中隐含着这样一点:我们可以访问文件的点必须能够访问 Hadoop 安装,并且必须配置为知道集群的位置。 这可能会在系统中增加额外的依赖关系;这可能会迫使我们将 Hadoop 添加到真正需要对其一无所知的主机。 我们可以通过使用 SFTP 等工具将文件检索到支持 Hadoop 的计算机,并从那里复制到 HDFS 来缓解此问题。

可靠性

请注意,在前面的方法中完全缺少错误处理。 我们使用的工具没有内置的重试机制,这意味着我们需要围绕每个数据检索包装一定程度的错误检测和重试逻辑。

重新创建控制盘

最后一点可能涉及到这些特殊方法的最大问题;很容易得到十几个不同的命令行工具和脚本字符串,每个字符串都执行非常相似的任务。 随着时间的推移,重复工作和更困难的错误跟踪方面的潜在成本可能会很大。

通用框架方法

在这一点上,任何有企业计算经验的人都会认为,这听起来像是使用某种类型的公共集成框架最好解决的问题。 这是完全正确的,并且确实是诸如Enterprise Application Integration(EAI)等领域所熟知的一般产品类型。

不过,我们需要的是一个框架,它能够识别 Hadoop,并且可以轻松地与 Hadoop(以及相关项目)集成,而不需要花费大量精力编写自定义适配器。 我们可以创建我们自己的,但是让我们看一下Apache Flume,它提供了我们需要的大部分内容。

Apache Flume 简介

Flume 在Hadoop找到,是另一个与 http://flume.apache.org 紧密集成的 Apache 项目,我们将在本章的剩余部分对其进行探讨。

在我们解释 Flume 可以做什么之前,让我们先弄清楚它不能做什么。 Flume 被描述为用于检索和分发日志的系统,这意味着面向行的文本数据。 它不是一个通用的数据分发平台;尤其是,不要指望使用它来检索或移动二进制数据。

但是,由于在 Hadoop 中处理的绝大多数数据都符合这一描述,因此 Flume 很可能会满足您的许多数据检索需求。

备注

Flume 也不是像我们在第 5 章高级 MapReduce 技术中使用的Avro那样的通用数据序列化框架,也不是ThriftProtocol Buffers等类似技术。 正如我们将看到的,Flume 对数据格式进行了假设,并且没有提供序列化这些格式以外的数据的方法。

Flume 提供了从多个源检索数据、将其传递到远程位置(可能是扇出或管道模型中的多个位置),然后将其传递到各种目的地的机制。 虽然它确实有一个允许开发自定义源和目标的编程 API,但是这个基础产品对许多最常见的场景都有内置支持。 让我们把它安装好,然后看一看。

关于版本化的说明

最近,Flume 经历了一些重大变化。 原始 Flume(现已重命名为Flume OG用于原始生成)将被Flume NG(下一代)取代。 虽然总体原则和功能非常相似,但实现方式却大相径庭。

因为 Flume NG 是未来的,我们将在本书中介绍它。 不过,在一段时间内,它将缺少更成熟的 Flume OG 的几个功能,所以如果您发现 Flume NG 没有满足的特定要求,那么可能值得看看 Flume OG。

行动时间-安装和配置 Flume

让我们下载并安装 Flume。

  1. http://flume.apache.org/检索最新的 Flume NG 二进制文件,并将其下载并保存到本地文件系统。

  2. 将文件移动到所需位置并解压缩:

    $ mv apache-flume-1.2.0-bin.tar.gz /opt
    $ tar -xzf /opt/apache-flume-1.2.0-bin.tar.gz
    
    
  3. 创建指向安装的符号链接:

    $ ln -s /opt/apache-flume-1.2.0 /opt/flume
    
    
  4. 定义FLUME_HOME环境变量:

    Export FLUME_HOME=/opt/flume
    
    
  5. 将 Flumebin目录添加到您的路径:

    Export PATH=${FLUME_HOME}/bin:${PATH}
    
    
  6. 验证是否设置了JAVA_HOME

    Echo ${JAVA_HOME}
    
    
  7. 验证 Hadoop 库是否位于类路径中:

    $ echo ${CLASSPATH}
    
    
  8. 创建将用作 Flumeconf目录的目录:

    $ mkdir /home/hadoop/flume/conf
    
    
  9. 将所需文件复制到conf目录:

    $ cp /opt/flume/conf/log4j.properties /home/hadoop/flume/conf
    $ cp /opt/flume/conf/flume-env.sh.sample /home/hadoop/flume/conf/flume-env.sh
    
    
  10. 编辑flume-env.sh并设置JAVA_HOME

刚刚发生了什么?

Flume 安装非常简单,并且与我们之前安装的工具具有类似的先决条件。

首先,我们检索了最新版本的 Flume NG(任何 1.2.x 或更高版本都可以),并将其保存到本地文件系统。 我们将其移动到所需位置,将其解压缩,并创建了指向该位置的方便符号链接。

我们需要定义FLUME_HOME环境变量,并将安装目录中的bin目录添加到类路径中。 如前所述,这可以直接在命令行上完成,也可以在方便的脚本中完成。

Flume 要求定义JAVA_HOME,我们证实了这一点。 它还需要 Hadoop 库,所以我们检查 Hadoop 类是否在类路径中。

虽然最后几个步骤将用于生产,但并不是严格意义上的演示所必需的。 Flume 查找配置目录,其中包含定义默认日志记录属性和环境设置变量(如JAVA_HOME)的文件。 我们发现 Flume 在正确设置此目录时的性能是最可预测的,因此我们现在就这样做了,不需要在很长时间内更改它。

我们假设/home/hadoop/flume是存储 Flume 配置和其他文件的工作目录;根据您的系统的需要更改此目录。

使用 Flume 捕获网络数据

现在我们已经安装了 Flume,让我们使用它来捕获一些网络数据。

在日志文件中捕获网络流量的操作时间

在第一个实例中,让我们使用一个简单的 Flume 配置,该配置将把网络数据捕获到主 Flume 日志文件中。

  1. 在您的 Flume 工作目录中创建以下文件agent1.conf

    agent1.sources = netsource
    agent1.sinks = logsink
    agent1.channels = memorychannel
    
    agent1.sources.netsource.type = netcat
    agent1.sources.netsource.bind = localhost
    agent1.sources.netsource.port = 3000
    
    agent1.sinks.logsink.type = logger
    
    agent1.channels.memorychannel.type = memory
    agent1.channels.memorychannel.capacity = 1000
    agent1.channels.memorychannel.transactionCapacity = 100
    
    agent1.sources.netsource.channels = memorychannel
    agent1.sinks.logsink.channel = memorychannel
    
  2. Start a Flume agent:

    $ flume-ng agent --conf conf --conf-file 10a.conf  --name agent1 
    
    

    上述命令的输出如下图所示:

    Time for action – capturing network traffic in a log file

  3. 在另一个窗口中,打开到本地主机上端口 3000 的 telnet 连接,然后键入一些文本:

    $ curl telnet://localhost:3000
    Hello
    OK
    Flume!
    OK
    
    
  4. 使用Ctrl+C关闭卷曲连接。

  5. Look at the Flume log file:

    $ tail flume.log
    
    

    您将收到以下响应:

    2012-08-19 00:37:32,702 INFO sink.LoggerSink: Event: { headers:{} body: 68 65 6C 6C 6F                                  Hello }
    2012-08-19 00:37:32,702 INFO sink.LoggerSink: Event: { headers:{} body: 6D 65                                           Flume }
    
    

刚刚发生了什么?

首先,我们在 Flume 工作目录中创建了一个 Flume 配置文件。 我们稍后将更详细地讨论这一点,但现在,请考虑 Flume 通过名为source的组件接收数据,并将其写入名为Sink的目的地。

在本例中,我们创建了一个Netcat源,它在端口上监听网络连接。 您可以看到,我们将其配置为绑定到本地计算机上的端口 3000。

配置的是类型logger,毫不奇怪,它会将其输出写入日志文件。 配置文件的其余部分定义名为agent1代理,它使用此源和接收器。

然后,我们使用flume-ng二进制文件启动 Flume 代理。 这是我们将用来启动所有 Flume 进程的工具。 请注意,我们为此命令提供了几个选项:

  • agent参数告诉 Flume 启动代理,该代理是数据移动中涉及的正在运行的 Flume 进程的通用名称
  • 如前所述,conf目录
  • 我们将要启动的进程的特定配置文件
  • 配置文件中代理的名称

代理将启动,且屏幕上不会显示进一步输出。 (显然,我们将在生产环境中的后台运行该过程。)

在另一个窗口中,我们使用curl实用程序打开到本地计算机端口 3000 的 telnet 连接。 打开此类会话的传统方式当然是 telnet 程序本身,但许多 Linux 发行版默认安装了 curl;几乎没有一个使用较旧的telnet实用程序。

我们在每行键入一个单词并按Enter,然后使用Ctrl+C命令终止会话。 最后,我们查看正在写入 Flume 工作目录的flume.log文件,并看到我们键入的每个单词都有一个条目。

将操作记录到控制台的时间

查看日志文件并不总是很方便,特别是在我们已经打开代理屏幕的情况下。 让我们修改代理,使其也将事件记录到屏幕上。

  1. Restart the Flume agent with an additional argument:

    $ flume-ng agent --conf conf --conf-file 10a.conf --name agent1 -Dflume.root.logger=INFO,console
    
    

    您将收到以下响应:

    Info: Sourcing environment configuration script /home/hadoop/flume/conf/flume-env.sh
    …
    org.apache.flume.node.Application --conf-file 10a.conf --name agent1
    2012-08-19 00:41:45,462 (main) [INFO - org.apache.flume.lifecycle.LifecycleSupervisor.start(LifecycleSupervisor.java:67)] Starting lifec
    
    
  2. 在另一个窗口中,通过 cURL:

    $ curl telnet://localhost:3000
    
    

    连接到服务器

  3. Type in Hello and Flume on separate lines, hit Ctrl + C, and then check the agent window:

    Time for action – logging to the console

刚刚发生了什么?

我们添加了这个示例,因为它在调试或创建新流时变得非常有用。

如上例所示,默认情况下,Flume 会将其日志写入文件系统上的一个文件。 更确切地说,这是在我们的conf目录中的 log4j 属性文件中指定的默认行为。 有时,我们希望获得更即时的反馈,而无需不断查看日志文件或更改属性文件。

通过在命令行上显式设置变量flume.root.logger,我们可以覆盖默认的记录器配置,并将输出直接发送到代理窗口。 记录器是标准的 log4j,因此支持DEBUGINFO等普通日志级别。

将网络数据写入日志文件

Flume 将其接收的数据写入日志文件的默认日志接收器行为有一些限制,特别是当我们想要在其他应用中使用捕获的数据时。 通过配置不同类型的接收器,我们可以将数据写入更多可使用的数据文件。

执行操作的时间-将命令的输出捕获到平面文件

让我们用的方法来演示这一点,同时演示一种新的源。

  1. 在 Flume 工作目录中创建以下文件agent2.conf

    agent2.sources = execsource
    agent2.sinks = filesink
    agent2.channels = filechannel
    
    agent2.sources.execsource.type = exec
    agent2.sources.execsource.command = cat /home/hadoop/message
    
    agent2.sinks.filesink.type = FILE_ROLL
    agent2.sinks.filesink.sink.directory = /home/hadoop/flume/files
    agent2.sinks.filesink.sink.rollInterval = 0
    
    agent2.channels.filechannel.type = file
    agent2.channels.filechannel.checkpointDir = /home/hadoop/flume/fc/checkpoint
    agent2.channels.filechannel.dataDirs = /home/hadoop/flume/fc/data
    
    agent2.sources.execsource.channels = filechannel
    agent2.sinks.filesink.channel = filechannel
    
  2. 在主目录中创建一个简单的测试文件:

    $ echo "Hello again Flume!" > /home/hadoop/message
    
    
  3. 启动代理:

    $ flume-ng agent --conf conf --conf-file agent2.conf --name agent2
    
    
  4. In another window, check file sink output directory:

    $ ls files
    $ cat files/*
    
    

    上述命令的输出如下图所示:

    Time for action – capturing the output of a command to a flat file

刚刚发生了什么?

前面的示例遵循与前面类似的模式。 我们为 Flume 代理创建了配置文件,运行了代理,然后确认它已经捕获了我们预期的数据。

这次我们使用了 EXEC 源和file_roll接收器。 顾名思义,前者在主机上执行命令并捕获其输出作为 Flume 代理的输入。 尽管在前一种情况下,该命令只执行一次,但这只是为了说明目的。 更常见的用法是使用产生持续数据流的命令。 请注意,可以将 EXEC 接收器配置为在命令终止时重新启动该命令。

代理的输出被写入配置文件中指定的文件。 默认情况下,Flume 每 30 秒轮换(滚动)一个新文件;我们禁用此功能是为了更容易跟踪单个文件中发生的事情。

我们看到该文件确实包含指定的exec命令的输出。

日志与文件

为什么 Flume 既有日志接收器又有文件接收器,这可能不是很明显。 从概念上讲,两者做的是一样的事情,那么有什么不同呢?

实际上,记录器接收器更像是一个调试工具。 它不仅记录源捕获的信息,还添加了大量附加元数据和事件。 然而,文件接收器记录的输入数据与接收到的数据完全一样,没有任何更改-尽管如果需要的话,这是可能的,正如我们稍后将看到的那样。

在大多数情况下,您会希望文件接收器捕获输入数据,但根据您的需要,日志也可能在非生产情况下有用。

执行操作的时间-在本地平面文件中捕获远程文件

让我们展示另一个将数据捕获到文件接收器的示例。 这一次,我们将使用另一个允许它从远程客户端接收数据的 Flume 功能。

  1. 在 Flume 工作目录中创建以下文件agent3.conf

    agent3.sources = avrosource
    agent3.sinks = filesink
    agent3.channels = jdbcchannel
    
    agent3.sources.avrosource.type = avro
    agent3.sources.avrosource.bind = localhost
    agent3.sources.avrosource.port = 4000
    agent3.sources.avrosource.threads = 5
    
    agent3.sinks.filesink.type = FILE_ROLL
    agent3.sinks.filesink.sink.directory = /home/hadoop/flume/files
    agent3.sinks.filesink.sink.rollInterval = 0
    
    agent3.channels.jdbcchannel.type = jdbc
    
    agent3.sources.avrosource.channels = jdbcchannel
    agent3.sinks.filesink.channel = jdbcchannel
    
  2. 将新测试文件创建为/home/hadoop/message2

    Hello from Avro!
    
  3. 启动 Flume 代理:

    $ flume-ng agent –conf conf –conf-file agent3.conf –name agent3 
    
    
  4. 在另一个窗口中,使用 Flume Avro 客户端将文件发送到代理:

    $ flume-ng avro-client -H localhost -p 4000 -F /home/hadoop/message
    
    
  5. As before, check the file in the configured output directory:

    $ cat files/*
    
    

    上述命令的输出如以下截图所示:

    Time for action – capturing a remote file in a local flat file

刚刚发生了什么?

与前面一样,我们创建了一个新的配置文件,这一次为代理使用了 avro 源。 回想一下第 5 章高级 MapReduce 技术,Avro 是一个数据序列化框架;也就是说,它管理数据在网络上从一个点到另一个点的打包和传输。 与 Netcat 源类似,Avro 源需要指定其网络设置的配置参数。 在本例中,它将侦听本地计算机上的端口 4000。 代理配置为像以前一样使用文件接收器,我们照常启动它。

Flume 既有 Avro 源,也有独立的 Avro 客户端。 后者可用于读取文件并将其发送到网络上任何位置的 Avro 源。 在我们的示例中,我们只使用本地机器,但请注意,avro 客户端需要它应该将文件发送到的 avro 源的显式主机名和端口。 因此,这不是一个限制;Avro 客户端可以将文件发送到网络上任何位置的监听 Flume Avro 源。

Avro 客户端读取文件,将其发送到代理,并将其写入文件接收器。 我们通过确认文件内容是否如预期的那样位于文件接收器位置来检查此行为。

源、汇和通道

我们在前面的示例中有意使用了各种源、汇和通道,只是为了说明如何混合和匹配它们。 然而,我们还没有对它们进行过详细的探索,特别是渠道。 现在让我们更深入地挖掘一下。

来源

我们查看了三个来源:Netcat、exec 和 Avro。 Flume NG 还支持序列生成器源(主要用于测试)以及读取syslogd数据的源的 TCP 和 UDP 变体。 每个源都配置在一个代理中,在接收到足够的数据以生成 Flume 事件后,它会将这个新创建的事件发送到源所连接的通道。 尽管源可能具有与其如何读取数据、转换事件和处理故障情况相关的逻辑,但源不知道如何存储事件。 源负责将事件传递到配置的通道,而事件处理的所有其他方面对源是不可见的。

Flume

除了我们之前使用的记录器和文件滚动槽,Flume 还支持 HDFS、HBase(两种类型)、Avro(用于代理链接)、NULL(用于测试)和 IRC(用于 Internet 中继聊天服务)的接收器。 接收器在概念上类似于源,但情况相反。

接收器等待从配置的通道接收事件,它对其内部工作原理一无所知。 接收时,接收器将事件的输出处理到其特定目标,管理有关超时、重试和轮换的所有问题。

通道

那么连接信源和信宿的神秘通道是什么呢? 正如前面的名称和配置条目所暗示的那样,它们是管理事件交付的通信和保留机制。

当我们定义一个源和一个接收器时,它们读取和写入数据的方式可能会有很大的不同。 例如,EXEC 源接收数据的速度可能比文件卷接收器写入数据的速度快得多,或者源可能有需要暂停写入的时间(例如,当滚动到新文件或处理系统 I/O 拥塞时)。 因此,通道需要在源和宿之间缓冲数据,以允许数据尽可能高效地流经代理。 这就是为什么我们的配置文件的通道配置部分包含容量等元素的原因。

存储器通道最容易理解,因为事件被从源存储器读取到存储器中,并在接收器能够接收它们时传递到接收器。 但是,如果代理进程在进程中途死亡(无论是由于软件还是硬件故障),则内存通道中当前的所有事件都将永远丢失。

我们还使用的文件JDBC通道提供事件的持久存储,以防止此类丢失。 从源读取事件后,文件通道将内容写入文件系统上的文件,该文件仅在成功传递到接收器后才会删除。 类似地,JDBC 通道使用嵌入式 Derby 数据库以可恢复的方式存储事件。

这是经典的性能与可靠性之间的权衡。 内存通道速度最快,但有数据丢失的风险。 文件和 JDBC 通道通常要慢得多,但可以有效地向接收器提供有保证的传输。 您选择哪个频道取决于应用的性质和每个事件的值。

备注

不要过于担心这种取舍;在现实世界中,答案通常是显而易见的。 此外,一定要仔细检查使用的信源和信宿的可靠性。 如果这些都是不可靠的,您无论如何都会丢弃事件,那么您会从持久通道中获益很多吗?

或者自己滚

不要觉得被现有的源、汇和渠道集合所限制。 Flume 提供了一个接口来定义您自己的实现。 此外,Flume OG 中存在的一些组件尚未整合到 Flume NG 中,但可能会在未来出现。

了解 Flume 配置文件

现在我们已经讨论了个源、接收器和通道,让我们更详细地看看前面的一个配置文件:

agent1.sources = netsource
agent1.sinks = logsink
agent1.channels = memorychannel

这些第一行命名代理,并定义与其关联的源、汇和通道。 每行可以有多个值;这些值以空格分隔:

agent1.sources.netsource.type = netcat
agent1.sources.netsource.bind = localhost
agent1.sources.netsource.port = 3000

这些行指定源的配置。 由于我们使用的是 Netcat 源,因此配置值指定它应该如何绑定到网络。 每种类型的源都有自己的配置变量。

agent1.sinks.logsink.type = logger

这指定要使用的接收器是通过命令行或 log4j 属性文件进一步配置的记录器接收器。

agent1.channels.memorychannel.type = memory
agent1.channels.memorychannel.capacity = 1000
agent1.channels.memorychannel.transactionCapacity = 100
These lines specify the channel to be used and then add the type specific configuration values.  In this case we are using the memory channel and we specify its capacity but – since it is non-persistent – no external storage mechanism.
agent1.sources.netsource.channels = memorychannel
agent1.sinks.logsink.channel = memorychannel

最后的行配置用于信源和信宿的通道。 虽然我们对不同的代理使用了不同的配置文件,但我们可以将所有元素放在单个配置文件中,因为各个代理名称提供了必要的分隔。 然而,这可能会产生一个非常冗长的文件,当您刚刚学习 Flume 时,它可能会有点吓人。 我们也可以在给定的代理中有多个流,例如,我们可以将前面的两个示例组合到单个配置文件和代理中。

来个围棋英雄

就这么做吧! 在包含以下内容的单个复合代理中创建一个配置文件,该文件指定前面示例中前面的agent1agent2的功能:

  • Netcat 源及其关联的记录器接收器
  • EXEC 源及其关联的文件接收器
  • 两个内存通道,分别用于前面提到的源/宿对

为了让您开始,下面是组件定义的外观:

agentx.sources = netsource execsource
agentx.sinks = logsink filesink
agentx.channels = memorychannel1 memorychannel2

这一切都是关于事件的

在我们尝试另一个例子之前,让我们再讨论一个定义。 究竟什么是事件?

请记住,Flume 是明确基于日志文件的,因此在大多数情况下,一个事件等同于一行文本,后跟一个换行符。 这就是我们在使用过的源和汇上看到的行为。

然而,情况并不总是如此,例如,UDP syslogd 源将接收到的每个数据包视为通过系统传递的单个事件。 然而,在使用这些接收器和源时,这些事件定义是不可更改的,例如,在读取文件时,我们别无选择,只能使用基于行的事件。

将网络流量写入 HDFS 的操作时间

在一本关于 Hadoop 的书中关于 Flume 的讨论到目前为止还没有实际使用过 Hadoop。 让我们通过 Flume 将数据写入 HDFS 来解决这个问题。

  1. 在 Flume 工作目录中创建以下文件agent4.conf

    agent4.sources = netsource
    agent4.sinks = hdfssink
    agent4.channels = memorychannel
    
    agent4.sources.netsource.type = netcat
    agent4.sources.netsource.bind = localhost
    agent4.sources.netsource.port = 3000
    
    agent4.sinks.hdfssink.type = hdfs
    agent4.sinks.hdfssink.hdfs.path = /flume
    agent4.sinks.hdfssink.hdfs.filePrefix = log
    agent4.sinks.hdfssink.hdfs.rollInterval = 0
    agent4.sinks.hdfssink.hdfs.rollCount = 3
    agent4.sinks.hdfssink.hdfs.fileType = DataStream
    
    agent4.channels.memorychannel.type = memory
    agent4.channels.memorychannel.capacity = 1000
    agent4.channels.memorychannel.transactionCapacity = 100
    
    agent4.sources.netsource.channels = memorychannel
    agent4.sinks.hdfssink.channel = memorychannel
    
  2. 启动代理:

    $ flume-ng agent –conf conf –conf-file agent4.conf –name agent4 
    
    
  3. 在另一个窗口中,打开 telnet 连接并向 Flume 发送七个事件:

    $ curl telnet://localhost:3000
    
    
  4. Check the contents of the directory specified in the Flume configuration file and then examine the file contents:

    $ hadoop fs -ls /flume
    $ hadoop fs –cat "/flume/*"
    
    

    前面命令的输出可以在下面的屏幕截图中显示:

    Time for action – writing network traffic onto HDFS

刚刚发生了什么?

这一次,我们将 Netcat 源与 HDFS 接收器配对。 从配置文件中可以看出,我们需要指定文件的位置、任何文件前缀以及从一个文件滚动到另一个文件的策略等方面。 在本例中,我们在/flume目录中指定了文件,每个文件都以log-开头,并且每个文件中最多有三个条目(显然,这样的低值仅用于测试)。

启动代理后,我们再次使用 cURL 向 Flume 发送 7 个单字事件。 然后,我们使用 Hadoop 命令行实用程序查看目录内容,并验证我们的输入数据是否正在写入 HDFS。

请注意,第三个 HDFS 文件的扩展名为.tmp。 请记住,我们为每个文件指定了三个条目,但只输入了七个值。 因此,我们填满了两个文件,开始了另一个文件。 Flume 为当前正在写入的文件赋予了.tmp扩展名,这使得在指定要通过 MapReduce 作业处理哪些文件时,很容易区分已完成的文件和正在进行的文件。

动作添加时间戳时间

我们在前面提到过,有一些机制可以让文件数据以稍微复杂的方式写入。 让我们做一些非常常见的事情,并使用动态创建的时间戳将我们的数据写入到一个目录中。

  1. 将以下配置文件创建为agent5.conf

    agent5.sources = netsource
    agent5.sinks = hdfssink
    agent5.channels = memorychannel
    
    agent5.sources.netsource.type = netcat
    agent5.sources.netsource.bind = localhost
    agent5.sources.netsource.port = 3000
    agent5.sources.netsource.interceptors = ts
    
    agent5.sources.netsource.interceptors.ts.type = org.apache.flume.interceptor.TimestampInterceptor$Builder
    
    agent5.sinks.hdfssink.type = hdfs
    agent5.sinks.hdfssink.hdfs.path = /flume-%Y-%m-%d
    agent5.sinks.hdfssink.hdfs.filePrefix = log-
    agent5.sinks.hdfssink.hdfs.rollInterval = 0
    agent5.sinks.hdfssink.hdfs.rollCount = 3
    agent5.sinks.hdfssink.hdfs.fileType = DataStream
    
    agent5.channels.memorychannel.type = memory
    agent5.channels.memorychannel.capacity = 1000
    agent5.channels.memorychannel.transactionCapacity = 100
    
    agent5.sources.netsource.channels = memorychannel
    agent5.sinks.hdfssink.channel = memorychannel
    
  2. 启动代理:

    $ flume-ng agent –conf conf –conf-file agent5.conf –name agent5
    
    
  3. 在另一个窗口中,打开 telnet 会话并向 Flume 发送七个事件:

    $ curl telnet://localhost:3000
    
    
  4. Check the directory name on HDFS and the files within it:

    $ hadoop fs -ls /
    
    

    上述代码的输出可以在下面的屏幕截图中显示:

    Time for action – adding timestamps

刚刚发生了什么?

我们对前面的配置文件进行了一些更改。 我们向 Netcat 源代码添加了interceptor规范,并将其实现类指定为TimestampInterceptor

Flume 拦截器是可以在事件从源传递到通道之前操作和修改事件的插件。 大多数拦截器要么向事件添加元数据(如本例所示),要么根据特定条件删除事件。 除了几个内置的拦截器之外,自然还有一种用于用户定义的拦截器的机制。

我们在这里使用了时间戳拦截器,它将读取事件时的 Unix 时间戳添加到事件元数据中。 这允许我们扩展要将事件写入其中的 HDFS 路径的定义。

虽然以前我们只是将所有事件写入/flume目录,但现在我们将路径指定为/flume-%Y-%m-%d。 在运行代理并将一些数据发送到 Flume 之后,我们查看了 HDFS,发现这些变量已经展开,为目录提供了年/月/日后缀。

HDFS 接收器支持许多其他变量,如源的主机名和允许精确分区到秒级别的附加时间变量。

这里的实用程序很简单;这种简单的机制可以提供自动分区,使数据管理变得更容易,但也为 MapReduce 作业提供了更简单的数据接口,而不是将所有事件写入一个随时间而变得庞大的单个目录中。 例如,如果您的大多数 MapReduce 作业都处理每小时一次的数据,那么让 Flume 将传入事件划分到每小时一次的目录中将使您的工作变得容易得多。

准确地说,通过 Flume 的事件添加了完整的 Unix 时间戳,即精确到最接近的秒。 在我们的示例中,我们在目录规范中仅使用与日期相关的变量,如果需要每小时或更细粒度的目录分区,则将使用与时间相关的变量。

备注

这里假设处理点的时间戳足以满足您的需要。 如果正在对文件进行批处理,然后将其送入 Flume,则文件内容的时间戳可能来自前一小时的时间戳,而不是它们被处理时的时间戳。 在这种情况下,您可以编写一个自定义拦截器来根据文件内容设置时间戳头。

到 Sqoop 或到 Flume...

一个明显的问题是,如果我们想要将关系数据库中的数据导出到 HDFS 上,那么 Sqoop 或 Flume 哪一个最合适。 我们已经看到了 Sqoop 如何执行这样的导出,我们可以使用 Flume 执行类似的操作,可以使用自定义源,甚至只需将对mysql命令的调用包装在 EXEC 源中即可。

一个很好的经验法则是查看数据类型,并询问它是日志数据还是其他更复杂的数据。

Flume 在很大程度上是为了处理日志数据而创建的,它在这方面非常出色。 但在大多数情况下,Flume 网络负责将事件从源传送到汇点,而不会对日志数据本身进行任何真正的转换。 如果您在多个关系数据库中有日志数据,那么 Flume 可能是一个很好的选择,尽管我会质疑使用数据库存储日志记录的长期可伸缩性。

非日志数据可能需要只有 Sqoop 才能执行的数据操作。 我们在上一章中使用 Sqoop 执行的许多转换,比如指定要检索的列的子集,实际上使用 Flume 是不可能的。 如果您正在处理需要单独字段处理的结构化数据,那么 Flume 本身也很可能不是理想的工具。 如果您想要直接集成 Hive,那么 Sqoop 是您唯一的选择。

当然,请记住,这些工具还可以在更复杂的工作流程中协同工作。 事件可以通过 Flume 收集到 HDFS 上,通过 MapReduce 进行处理,然后通过 Sqoop 导出到关系数据库中。

行动时间-多级 Flume 网络

让我们把前面提到的几个部分放在一起,看看一个 Flume 代理如何使用另一个代理作为它的接收器。

  1. 将以下文件创建为agent6.conf

    agent6.sources = avrosource
    agent6.sinks = avrosink
    agent6.channels = memorychannel
    
    agent6.sources.avrosource.type = avro
    agent6.sources.avrosource.bind = localhost
    agent6.sources.avrosource.port = 2000
    agent6.sources.avrosource.threads = 5
    
    agent6.sinks.avrosink.type = avro
    agent6.sinks.avrosink.hostname = localhost
    agent6.sinks.avrosink.port = 4000
    
    agent6.channels.memorychannel.type = memory
    agent6.channels.memorychannel.capacity = 1000
    agent6.channels.memorychannel.transactionCapacity = 100
    
    agent6.sources.avrosource.channels = memorychannel
    agent6.sinks.avrosink.channel = memorychannel
    
  2. 启动按照前面创建的agent3.conf文件配置的代理,即使用 avro 源和文件接收器:

    $ flume-ng client –conf conf –conf-file agent3.conf agent3 
    
    
  3. 在第二个窗口中,启动另一个代理;该代理配置有前面的文件:

    $ flume-ng client –conf conf –conf-file agent6.conf agent6
    
    
  4. 在第三个窗口中,使用 Avro 客户端将文件发送到每个 Flume 代理:

    $ flume-ng avro-client –H localhost –p 4000 –F /home/hadoop/message
    $ flume-ng avro-client -H localhost -p 2000 -F /home/hadoop/message2
    
    
  5. Check the output directory for files and examine the file present:

    Time for action – multi level Flume networks

刚刚发生了什么?

首先,我们定义了一个具有 Avro 信源和 Avro 信宿的新代理。 我们以前没有使用过这个接收器;这个接收器不是将事件写入本地位置或 HDFS,而是使用 avro 将事件发送到远程源。

我们先启动这个新代理的一个实例,然后再启动前面使用的agent3的一个实例。 回想一下,该代理有一个 avro 源和一个文件滚动接收器。 我们将第一个代理中的 Avro 接收器配置为指向第二个代理中的 Avro 接收器的主机和端口,并通过这样做来构建数据路由链。

在两个代理都运行的情况下,我们然后使用 avro 客户端向每个代理发送一个文件,并确认它们都出现在配置为agent3接收器目标的文件位置。

这不仅仅是技术能力本身。 此功能是允许 Flume 构建任意复杂的分布式事件收集网络的构建块。 不是每个代理的一个副本,而是每种类型的多个代理将事件馈送到链中的下一个链接,该链接充当事件聚合点。

对多个接收器进行动作写入的时间

我们需要最后一项功能来构建这样的网络,即可以写入多个接收器的代理。 让我们创建一个。

  1. 将以下配置文件创建为agent7.conf

    agent7.sources = netsource
    agent7.sinks = hdfssink filesink
    agent7.channels = memorychannel1 memorychannel2
    
    agent7.sources.netsource.type = netcat
    agent7.sources.netsource.bind = localhost
    agent7.sources.netsource.port = 3000
    agent7.sources.netsource.interceptors = ts
    
    agent7.sources.netsource.interceptors.ts.type = org.apache.flume.interceptor.TimestampInterceptor$Builder
    
    agent7.sinks.hdfssink.type = hdfs
    agent7.sinks.hdfssink.hdfs.path = /flume-%Y-%m-%d
    agent7.sinks.hdfssink.hdfs.filePrefix = log
    agent7.sinks.hdfssink.hdfs.rollInterval = 0
    agent7.sinks.hdfssink.hdfs.rollCount = 3
    agent7.sinks.hdfssink.hdfs.fileType = DataStream
    
    agent7.sinks.filesink.type = FILE_ROLL
    agent7.sinks.filesink.sink.directory = /home/hadoop/flume/files
    agent7.sinks.filesink.sink.rollInterval = 0
    
    agent7.channels.memorychannel1.type = memory
    agent7.channels.memorychannel1.capacity = 1000
    agent7.channels.memorychannel1.transactionCapacity = 100
    
    agent7.channels.memorychannel2.type = memory
    agent7.channels.memorychannel2.capacity = 1000
    agent7.channels.memorychannel2.transactionCapacity = 100
    
    agent7.sources.netsource.channels = memorychannel1 memorychannel2
    agent7.sinks.hdfssink.channel = memorychannel1
    agent7.sinks.filesink.channel = memorychannel2
    
    agent7.sources.netsource.selector.type = replicating
    
  2. 启动代理:

    $ flume-ng agent –conf conf –conf-file agent7.conf –name agent7 
    
    
  3. Open a telnet session and send an event to Flume:

    $ curl telnet://localhost:3000
    
    

    您将收到以下响应:

    Replicating!
    Check the contents of the HDFS and file sinks:
    $ cat files/*
    $ hdfs fs –cat "/flume-*/*"
    
    

    上述命令的输出如下图所示:

    Time for action – writing to multiple sinks

刚刚发生了什么?

我们创建了一个配置文件,其中包含单个 Netcat 源文件以及该文件和 HDFS 接收器。 我们配置了单独的内存通道,将源连接到两个接收器。

然后,我们将源选择器类型设置为replicating,这意味着事件将被发送到所有已配置的通道。

在正常启动代理并将事件发送到源之后,我们确认该事件确实写入了文件系统和 HDFS 接收器。

选择器复制和多路复用

源选择器有两种模式,复制(如我们在这里看到的)和多路复用。 多路复用源选择器将根据指定报头字段的值使用逻辑来确定事件应该发送到哪个通道。

处理接收器故障

由于接收器是个输出目的地,因此预计接收器可能会随着时间的推移出现故障或变得无响应。 与任何输入/输出设备一样,接收器可能已饱和、空间不足或脱机。

正如 Flume 将选择器与源关联以允许我们刚才看到的复制和多路复用行为一样,它还支持接收器处理器的概念。

定义了两个信宿处理器,即故障转移信宿处理器和负载平衡信宿处理器。

接收器处理器将接收器视为在一个组内,并根据它们的类型,在事件到达时做出不同的反应。 负载平衡接收器处理器使用循环或随机算法向接收器发送事件,每次发送一个事件,以选择下一步使用哪个接收器。 如果接收器出现故障,则会在另一个接收器上重试该事件,但出现故障的接收器仍保留在池中。

相反,故障转移接收器将接收器视为优先级列表,并且仅在其上面的接收器失败时才尝试较低优先级的接收器。 出现故障的接收器将从列表中删除,并且仅在随着后续故障而增加的冷静期后才会重试。

有一个围棋英雄-处理 Flume 故障

设置具有三个已配置 HDFS 接收器的 Flume 配置,每个接收器都写入 HDFS 上的不同位置。 使用负载平衡器接收器处理器确认事件已写入每个接收器,然后使用故障转移接收器处理器显示优先顺序。

您是否可以强制代理选择优先级最高的处理器以外的处理器?

接下来,世界

我们现在已经介绍了 Flume 的大部分关键功能。 正如前面提到的,Flume 是一个框架,应该仔细考虑这一点;Flume 的部署模型比我们看到的任何其他产品都灵活得多。

它通过相对较小的一组功能实现其灵活性;通过通道将源连接到汇点,以及允许多代理或多通道配置的多种变体。 这看起来可能不是很多,但请考虑一下,可以组合这些构造块来创建如下系统,其中多个 Web 服务器场将其日志馈送到中央 Hadoop 群集:

  • 每个场中的每个节点都运行一个代理,依次拉取每个本地日志文件。
  • 这些日志文件被发送到一个高度可用的聚合点,每个场中的一个聚合点还执行一些处理并向事件添加附加元数据,将事件分类为三种类型的记录。
  • 然后,这些一级聚合器将事件发送到访问 Hadoop 集群的一系列代理之一。 聚合器提供多个接入点,事件类型 1 和 2 被发送到第一个接入点,事件类型 3 被发送到第二个接入点。
  • 在最终聚合器中,它们将事件类型 1 和 2 写入 HDFS 上的不同位置,类型 2 也写入本地文件系统。 事件类型 3 直接写入 HBase。

如此简单的原语可以组合起来构建这样复杂的系统,这真是令人惊讶!

有一个围棋英雄-下一个,世界

作为一种思维实验,尝试完成前面的场景,并确定在流程中的每个步骤都需要什么样的 Flume 设置。

更大的图景

重要的是要认识到,从一个点到另一个点“简单地”获取数据很少是您考虑数据的范围。 最近,像数据生命周期管理这样的术语被广泛使用是有原因的。 让我们简要地看一下一些需要考虑的事情,最好是在数据泛滥整个系统之前。

数据生命周期

就数据生命周期而言,要问的主要问题是,您从数据存储中获得的价值将在多长时间内大于存储成本。 永久保存数据似乎很有吸引力,但随着时间的推移,保存越来越多数据的成本将会增加。 这些成本不仅仅是财务上的;许多系统的性能随着数据量的增加而下降。

这个问题不是--或者至少不应该是--由技术因素决定的。 相反,你需要企业的价值和成本成为驱动因素。 有时数据很快就变得一文不值,有时由于竞争或法律原因,企业无法将其删除。 确定位置并采取相应的行动。

当然,请记住,在保留或删除数据之间不是一个二元决策;您还可以跨存储层迁移数据,这些存储层的成本和性能会随着时间的推移而降低。

暂存数据

在这个过程的另一边,通常值得考虑如何将数据提供给处理平台(如 MapReduce)。 对于多个数据源,您通常最不希望的就是将所有数据都放在一个巨大的卷上。

正如我们在前面看到的,Flume 能够参数化它在 HDFS 上写入的位置,这是一个很好的工具来帮助解决这个问题。 但是,通常将此初始下载点视为在处理之前写入数据的临时中转区是很有用的。 在处理之后,可以将其移动到长期目录结构中。

排程

在流程中的许多点上,我们已经讨论过,有一种隐含的需求,即需要外部任务来做一些事情。 如前所述,我们希望 MapReduce 在 Flume 将文件写入 HDFS 后对其进行处理,但是该任务是如何调度的呢? 或者,我们如何管理源主机上的后处理、旧数据的存档或删除,甚至是日志文件的删除?

其中一些任务(例如后者)可能由 Linux 上的logrotate等现有系统管理,但其他任务可能需要构建。 像cron这样显而易见的工具可能已经足够好了,但是随着系统复杂性的增加,您可能需要研究更复杂的调度系统。 在下一章中,我们将简要介绍这样一个与 Hadoop 紧密集成的系统。

摘要

本章讨论了如何跨网络检索数据并使其可在 Hadoop 中处理的问题。 正如我们所看到的,这实际上是一个更普遍的挑战,尽管我们可能会使用特定于 Hadoop 的工具,如 Flume,但这些原则并不是唯一的。 特别是,我们概述了我们可能想要写入 Hadoop 的数据类型,通常将其归类为网络数据或文件数据。 我们探索了使用现有命令行工具进行此类检索的一些方法。 虽然功能强大,但这些方法缺乏复杂性,不适合扩展到更复杂的场景中。

我们将 Flume 视为定义和管理数据(特别是来自日志文件)路由和交付的灵活框架,并了解了 Flume 体系结构,该体系结构可以看到数据到达源,通过通道进行处理,然后写入接收器。

然后,我们探索了 Flume 的许多功能,例如如何使用不同类型的源、汇和通道。 我们了解了如何将简单的构建块组合成非常复杂的系统,最后介绍了一些关于数据管理的更一般的想法。

这就是本书的主要内容。 在下一章中,我们将勾勒出一些可能令人感兴趣的其他项目,并重点介绍一些让社区参与和获得支持的方法。

十一、下一步要去哪里

正如书名所示,本书旨在让 Hadoop 初学者深入了解该技术及其应用。 正如我们在多个场合看到的那样,Hadoop 生态系统有比核心产品本身更多的东西。 在本章中,我们将快速重点介绍一些可能感兴趣的领域。

在本章中,我们将讨论:

  • 我们在这本书中所涵盖的内容
  • 我们在这本书中没有涵盖的内容
  • 即将到来的 Hadoop 变化
  • 可供选择的 Hadoop 发行版
  • 其他重要的 Apache 项目
  • 可选编程抽象
  • 信息和帮助的来源

我们在这本书中做了什么,没有做什么

由于我们的重点是初学者,本书的目的是为您提供核心 Hadoop 概念和工具的坚实基础。 此外,我们还提供了一些其他工具的经验,帮助您将技术集成到您的基础设施中。

虽然 Hadoop 一开始只是一个核心产品,但公平地说,围绕 Hadoop 的生态系统在最近几年呈爆炸式增长。 该技术还有其他发行版本,其中一些提供了商业自定义扩展。 有太多的相关项目和工具构建在 Hadoop 之上,并为现有想法提供特定的功能或替代方法。 现在是参与 Hadoop 的一个非常令人兴奋的时刻;让我们快速了解一下外面的情况。

备注

当然,请注意,任何关于生态系统的概述都会受到作者兴趣和偏好的影响,而且在写出来的那一刻就已经过时了。 换句话说,任何时候都不要认为这就是所有可用的东西;把它看作是对胃口的刺激。

即将推出的 Hadoop 更改

在讨论其他 Hadoop 发行版之前,让我们先看一下 Hadoop 本身在不久的将来的一些变化。 我们已经讨论了 Hadoop2.0 中的 HDFS 更改,特别是新的 BackupNameNode 和 CheckpointNameNode 服务支持的 NameNode 的高可用性。 这对 Hadoop 来说是一项重要的功能,因为它将使 HDFS 更加健壮,极大地增强其企业凭证并简化群集操作。 NameNode HA 的影响很难被夸大;几乎可以肯定的是,它将成为几年后没有人能够记住我们如何生活的功能之一。

MapReduce 在这一切进行时并没有停滞不前,事实上,正在引入的变化可能不会产生太大的立竿见影的影响,但实际上要根本得多。

这些更改最初是在名称MapReduce 2.0MRV2下开发的。 然而,现在使用的名称是Yar(Yet Another Resource Neighter),这个名称更准确,因为更改更多的是关于 Hadoop 平台,而不是 MapReduce 本身。 YAIN 的目标是在 Hadoop 上构建一个框架,允许将集群资源分配给给定的应用,并且 MapReduce 只是这些应用中的一个。

如果您现在考虑 JobTracker,它负责两个完全不同的任务:管理给定 MapReduce 作业的进度(但也识别在任何时间点哪些集群资源可用)和将资源分配给作业的各个阶段。 YAY 将这些划分为不同的角色;全局ResourceManager使用每台主机上的 NodeManagers 来管理集群的资源;以及不同的ApplicationManager(第一个示例是 MapReduce),它与 ResourceManager 通信以获取其作业所需的资源。

Year 中的 MapReduce 接口将保持不变,因此从客户端的角度来看,所有现有代码仍将在新平台上运行。 但随着新的 ApplicationManager 的开发,我们将开始看到 Hadoop 更多地被用作支持多种类型处理模型的通用任务处理平台。 移植到 Yarn 的其他模型的早期示例是基于流的处理和在科学计算中广泛使用的消息传递接口(MPI)的端口。

备选分配

回到第 2 章启动并运行,我们转到 Hadoop 主页,从那里我们下载了安装包。 虽然看起来可能很奇怪,但这远不是获得 Hadoop 的唯一途径。 更奇怪的是,大多数生产部署都不使用 Apache Hadoop 发行版。

为什么选择替代分发?

Hadoop 是开放的源代码软件。 只要遵守管理 Hadoop 的 Apache 软件许可证,任何人都可以制作自己的软件版本。 创建替代发行版的主要原因有两个。

捆绑

一些提供商寻求构建一个预先捆绑的发行版,其中不仅包含 Hadoop,还包含其他项目,如 Have、HBase、Pig 等等。 尽管大多数项目的安装很少会有困难--除了 HBase,从历史上看,手工设置都比较困难--但可能会出现一些微妙的版本不兼容问题,直到某个特定的生产工作负载达到系统时才会出现。 捆绑发布可以提供一组预先集成的兼容版本,这些版本可以协同工作。

捆绑的发行版不仅可以在 tarball 文件中提供发行版,还可以在通过 RPM、YUM 或 APT 等包管理器轻松安装的包中提供发行版。

免费和商业扩展

作为一个拥有相对自由的发行许可的开源项目,创建者还可以自由地使用专有扩展来增强 Hadoop,这些扩展可以是免费的开源产品,也可以是商业产品。

这可能是一个有争议的问题,因为一些开源倡导者不喜欢任何成功的开源项目的商业化;对他们来说,商业实体似乎是在免费享用开源社区的成果,而不必为自己构建它。 其他人认为这是灵活的 Apache 许可的一个健康方面;基础产品永远是免费的,个人和公司可以选择是否使用商业扩展。 我们不会以任何一种方式做出判断,但请注意,这几乎是你肯定会遇到的争议。

考虑到存在其他发行版本的原因,让我们来看几个流行的例子。

适用于 Hadoop 的 Cloudera 发行版

使用最广泛的 Hadoop 发行版是 Hadoop 的Cloudera 发行版,称为CDH。 回想一下,Cloudera 是第一个创建 Sqoop 并将其贡献给开源社区的公司,现在 Doug Cutting 也在这里工作。

Cloudera 发行版可在Hadoop上获得,其中包含大量 http://www.cloudera.com/hadoop 产品,从 Hadoop 本身、Hive、Pig 和 HBase 到 SQOOP 和 Flume 等工具,再到 Mahout 和 Whir 等其他鲜为人知的产品。 我们稍后将讨论其中的一些内容。

CDH 有多种包格式可供选择,并以随时可用的方式部署软件。 例如,对于 NameNode、TaskTracker 等组件,基本 Hadoop 产品被分成不同的包,并且每个包都集成了标准的 Linux 服务基础设施。

CDH 是第一个广泛可用的替代发行版,其广泛的可用软件、经过验证的质量水平和免费成本使其成为非常受欢迎的选择。

除了培训、支持和咨询服务外,Cloudera 还提供额外的纯商业产品,如 Hadoop 管理工具。 详情请浏览公司网页。

Hortonworks 数据平台

2011 年,负责 Hadoop 如此多开发的雅虎部门被剥离出来,成立了一家名为Hortonworks的新公司。 他们还制作了自己的预集成 Hadoop 发行版,称为 TheHortonworks data Platfo****RM(HDP),可在http://hortonworks.com/products/hortonworksdataplatform/获得。

HDP 在概念上类似于 CDH,但这两种产品在侧重点上有所不同。 Hortonworks 强调 HDP 是完全开源的,包括管理工具。 他们还通过支持 Talend Open Studio 等工具将 HDP 定位为关键的集成平台。 Hortonworks 不提供商业软件;相反,它的商业模式侧重于为该平台提供专业服务和支持。

Cloudera 和 Hortonworks 都是风投支持的公司,拥有重要的工程专业知识;这两家公司都雇佣了 Hadoop 最多产的贡献者。 但是,底层技术是相同的 Apache 项目;不同之处在于它们的打包方式、使用的版本以及这些公司提供的附加增值产品。

MAPR

MapR Technologies提供了不同的类型的分发,尽管公司和分发通常简称为MapR。 在Hadoop提供,该发行版基于 http://www.mapr.com,但添加了许多更改和增强。

MapR 的一个主要关注点是性能和可用性,例如,它是第一个为 Hadoop NameNode 和 JobTracker 提供高可用性解决方案的发行版,您会记得(参见第 7 章保持事物运行)是核心 Hadoop 的一个重大弱点。 它还提供与 NFS 文件系统的本机集成,这使得处理现有数据变得容易得多;MapR 用完全兼容 POSIX 的文件系统取代了 HDFS,该文件系统可以轻松地远程挂载。

MapR 提供了其发行版的社区版和企业版;并不是所有的扩展都在免费产品中提供。 除了培训和咨询外,该公司还提供支持服务,作为企业产品订阅的一部分。

IBM InfoSphere Big Insight

我们将在这里提到的最后一个发行版来自 IBM。 IBM InfoSphere Big Insights发行版可以在Hadoop BigInsight上获得,并且(与 http://www-01.ibm.com/software/data/infosphere/一样)提供了对开源 Hadoop 核心的商业改进和扩展。

Big Insights 有两个版本,免费的 IBM InfoSphere Big Insights 基本版和商业的 IBM InfoSphere Big Insights 企业版。 大洞察力,大名鼎鼎! 基本版是一组增强的 Apache Hadoop 产品,添加了一些免费的管理和部署工具以及与其他 IBM 产品的集成。

EnterpriseEdition 实际上与 Basic Edition 有很大的不同;它更像是 Hadoop 之上的一层,实际上可以与 CDH 或 HDP 等其他发行版一起使用。 企业版提供了一系列数据可视化、业务分析和处理工具。 它还与其他 IBM 产品(如 InfoSphere Streams、DB2 和 GPFS)深度集成。

选择分配

可以看到,可用的发行版(我们没有涵盖所有发行版)的范围从方便的完全开源产品的打包和集成到它们上面的整个定制的集成和分析层。 不存在总体最佳分配;请仔细考虑您的需求并考虑替代方案。 由于所有以前的发行版都提供至少一个基本版本的免费下载,因此您也可以简单地尝试一下并体验一下这些选项。

其他 Apache 项目

无论您是使用捆绑的发行版还是坚持使用基本的 Apache Hadoop 下载,您都会遇到许多对其他相关 Apache 项目的引用。 我们已经在本书中介绍了 Hive、Sqoop 和 Flume;现在我们将重点介绍其他一些。

请注意,这篇报道试图指出亮点(从我的角度来看),并让人们品尝到可用项目的广泛类型。 就像以前一样,继续保持警惕;会有新的不断推出。

HBase

也许最受欢迎的 Apache Hadoop 相关项目是HBase;它的主页位于http://hbase.apache.org。 基于 Google 在一篇学术论文中公布的 Bigtable 数据存储模型(听起来熟悉吗?),HBase 是一个位于 HDFS 之上的非关系型数据存储。

MapReduce 和 Have 任务都专注于类似批处理的数据访问模式,而 HBase 则寻求提供非常低延迟的数据访问。 因此,与前面提到的技术不同,HBase 可以直接支持面向用户的服务。

HBase 数据模型不是我们在配置单元和所有其他 RDBMS 中看到的关系方法。 相反,它是一个键值、无模式的解决方案,采用面向列的数据视图;列可以在运行时添加,并取决于插入到 HBase 中的值。 因此,每个查找操作都非常快,因为它实际上是从行键到所需列的键值映射。 HBase 还将时间戳视为数据的另一个维度,因此可以直接从某个时间点检索数据。

数据模型非常强大,但并不适合所有用例,就像关系模型并不普遍适用一样。 但是,如果您需要存储在 Hadoop 中的大规模数据的结构化低延迟视图,HBase 绝对是您应该考虑的。

Oozie

我们已经说过很多次,Hadoop 集群不是生活在真空中,需要与其他系统集成,并集成到更广泛的工作流中。 Oozie可从Hadoop获得,它是一个专注于 http://oozie.apache.org 的工作流调度器,可以解决后一种情况。

在其最简单的形式中,Oozie 提供了基于基于时间的标准(例如,每小时执行一次)或数据可用性(例如,当新数据到达该位置时执行)来调度 MapReduce 作业执行的机制。 它允许规范能够描述完整的端到端流程的多阶段工作流。

除了简单的 MapReduce 作业,Oozie 还可以调度运行配置单元或 Pig 命令的作业,以及完全在 Hadoop 之外的任务(例如发送电子邮件、运行 shell 脚本或在远程主机上运行命令)。

构建工作流的方法有很多种; 常用的方法是使用Extract Transform 和****Load(ETL)工具,如Pentaho Kettle(http://kettle.pentaho.com)和Spring Batch(http://static.springsource.org/spring-batch)。 例如,它们确实包括一些 Hadoop 集成,但传统的专用工作流引擎可能不包括。 如果您正在构建具有重要 Hadoop 交互的工作流,并且没有必须与之集成的现有工作流工具,那么可以考虑使用 Oozie。

旋转

当希望使用 Amazon AWS 等云服务进行 Hadoop 部署时,与在 EC2 上设置自己的群集相比,使用更高级别的服务(如 ElasticMapReduce)通常要容易得多。 尽管有脚本可以提供帮助,但事实是在云基础设施上基于 Hadoop 的部署可能会带来开销。 这就是http://whir.apache.org中的 apachewhir的用武之地。

Whir 并不关注 Hadoop;它关注的是独立于供应商的云服务实例化,Hadoop 就是其中的一个例子。 WHIR 提供了一种编程方式,可以在云基础设施上指定和创建基于 Hadoop 的部署,为您处理所有底层服务方面。 它以独立于提供者的方式完成这项工作,这样,一旦您在 EC2 上启动,您就可以使用相同的代码在另一个提供者(如 Rackspace 或 Eucalyptus)上创建相同的设置。 这使得供应商锁定-通常是云部署的一个问题-不再是问题。

惠尔还没有完全做到这一点。 如今,它可以创建的服务有限,并且只支持单一提供商 AWS。 但是,如果您对轻松部署云感兴趣,那么值得关注它的进展。

Mahout

前面的项目都是通用的,因为它们提供了独立于任何应用领域的功能。 ApacheMahout位于http://mahout.apache.org,它是构建在 Hadoop 和 MapReduce 之上的机器学习算法库。

Hadoop 处理模型通常非常适合机器学习应用,因为机器学习应用的目标是从大数据集中提取价值和意义。 Manhout 提供了集群和推荐器等常见 ML 技术的实现。

如果您有大量数据,并且需要帮助找到关键模式、关系,或者只是大海捞针,Mahout 可能会提供帮助。

MRUnit

我们将提到的最终 Apache Hadoop 项目还突出显示了可用内容的广泛范围。 在很大程度上,如果您的 MapReduce 作业经常由于潜在的 bug 而失败,那么使用多少很酷的技术以及使用哪个发行版都无关紧要。 最近从http://mrunit.apache.org升级的 MRUnit 可以在这方面提供帮助。

开发 MapReduce 作业可能很困难,尤其是在早期,但测试和调试它们几乎总是很困难的。 MRUnit 采用其同名产品(如 JUnit 和 DBUnit)的单元测试模型,并提供一个框架来帮助编写和执行测试,从而帮助提高代码质量。 构建一个测试套件、集成自动化测试和构建工具,突然之间,所有那些您在编写非 MapReduce 代码时做梦也不会想要遵循的软件工程最佳实践也都在这里提供了。

如果您曾经编写过 MapReduce 作业,那么 MRUnit 可能会让您感兴趣。 依我拙见,这是一个非常重要的项目,请检查一下。

其他编程抽象

Hadoop 不只是通过附加功能来扩展;有一些工具可以提供完全不同的范例来编写用于在 Hadoop 中处理数据的代码。

PIG

我们在第 8 章A Relational View on Data with Have中提到了Pig(http://pig.apache.org),这里不再赘述。 请记住,它是可用的,如果您的流程或人员认为 Hadoop 流程的数据流定义比编写原始 MapReduce 代码或 HiveQL 脚本更直观或更适合,那么它可能会很有用。 请记住,主要区别在于 Pig 是一种命令式语言(它定义了流程将如何执行),而 Have 则更具声明性(定义了期望的结果,但不定义它们将如何产生)。

级联

Cascading不是一个 apache 项目,但它是开源的,可以从http://www.cascading.org获得。 虽然配置单元和 Pig 有效地定义了用来表示数据处理的不同语言,但级联提供了一组更高级别的抽象。

该模型不考虑多个 MapReduce 作业如何通过级联处理和共享数据,而是一个使用管道和多个连接器、分路器和类似构造的数据流。 这些都是以编程方式构建的(核心 API 最初是 Java,但还有许多其他语言绑定),级联管理集群上工作流的转换、部署和执行。

如果您想要一个更高级别的 MapReduce 接口,而 Pig 和 Have 的声明性样式不适合,那么级联的编程模型可能就是您想要的。

AWS 资源

许多 Hadoop 技术可以作为自我管理群集的一部分部署在 AWS 上。 但是,正如 Amazon 提供对 Elastic MapReduce 的支持一样,Elastic MapReduce 将 Hadoop 作为托管服务处理,还有一些其他服务值得一提。

电子病历上的 HBase

这本身并不是一个真正独特的服务,但就像 EMR 拥有对配置单元和 Pig 的原生支持一样,它现在也提供了对 HBase 集群的直接支持。 这是一个相对较新的功能,看看它在实践中的运行情况会很有趣;HBase 历来对网络和系统负载的质量非常敏感。

SimpleDB

Amazon SimpleDB(HBase)是一个提供类似 http://aws.amazon.com/simpledb 的数据模型的服务。 这实际上不是在 Hadoop 上实现的,但是我们将提到这项服务和下面的服务,因为如果您感兴趣的是类似 HBase 的数据模型,它们确实提供了值得考虑的托管替代方案。 这项服务已经有几年的历史了,并且非常成熟,有非常好理解的用例。

SimpleDB 确实有一些限制,特别是在表大小和需要手动对大型数据集进行分区方面,但是如果您需要在较小的卷上使用 HBase 类型的存储,那么它可能是一个很好的选择。 它也很容易设置,是尝试基于列的数据模型的一种很好的方式。

DynamoDB

来自 AWS 的较新的服务是DynamoDB,可从[http://aws.amazon.com/DynamoDB](http://aws.amazon.com/ dynamodb)获得。 尽管它的数据模型再次与 SimpleDB 和 HBase 非常相似,但它针对的是一种非常不同类型的应用。 SimpleDB 有相当丰富的搜索 API,但在大小方面非常有限,DynamoDB 提供了一个更受限制的 API,但具有几乎无限可伸缩性的服务保证。

DynamoDB 定价模型特别有趣;不是为托管服务的特定数量的服务器付费,而是分配一定的读/写容量,DynamoDB 管理满足该配置容量所需的资源。 这是一个有趣的发展,因为它是一个更纯粹的服务模型,其中交付所需性能的机制对服务用户是完全不透明的。 如果您需要比 SimpleDB 能够提供的数据存储规模大得多的数据存储,请考虑 DynamoDB,但一定要仔细考虑定价模型,因为调配过多的容量很快就会变得非常昂贵。

信息来源

你不仅仅需要新的技术和工具,不管它们有多酷。 有时,更有经验的人提供一点帮助就可以把你从困境中拉出来。 在这方面,您已经做了很好的介绍;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 用户组(Hug);大多数用户应该在http://wiki.apache.org/hadoop/HadoopUserGroups上列出。 这些公司倾向于安排半定期的聚会,包括高质量的演示、与志同道合的人讨论技术的能力,以及经常是披萨和饮料。

你住的地方附近没有拥抱吗? 考虑开一家吧!

会议

虽然 Hadoop 是一项相对较新的技术,但已经有了一些涉及开源、学术和商业世界的重要会议活动。 像Hadoop Summit这样的活动相当大;它和其他活动通过http://wiki.apache.org/hadoop/Conferences链接。

摘要

在本章中,我们简要介绍了更广泛的 Hadoop 生态系统。 我们了解了 Hadoop 中即将发生的变化,特别是 HDFS 高可用性和 YILE,为什么会存在其他 Hadoop 发行版以及一些更受欢迎的发行版,以及其他提供功能、扩展或 Hadoop 支持工具的 Apache 项目。

我们还研究了编写或创建 Hadoop 作业和信息源的替代方法,以及如何与其他爱好者联系。

现在去享受乐趣,创造一些令人惊叹的东西吧!

十二、附录 A:答案

第 3 章,了解 MapReduce

弹出测验-键/值对

| Q1 | 2 个 | | Q2 | 3. |

流行问答-通过字数统计

| Q1 | 1. | | Q2 | 3. | | Q3 | 2.缩减器 C 不能使用,因为如果发生这种减缩,最终的缩减器可能会从组合器接收一系列方法,而不知道使用了多少项来生成它们,这意味着不可能计算总平均值。 缩减器 D 是微妙的,因为选择最大值或最小值的单个任务可以安全地用作组合器操作。 但是,如果目标是确定每个关键字的最大值和最小值之间的总体方差,那么这是行不通的。 如果接收到最大键的组合器的值聚集在它周围,这将生成较小的结果;接收到最小值的组合器也是如此。 这些子范围孤立地没有什么价值,而且最终的减除器也不能构造所需的结果。 |

第 7 章,保持运行

弹出式测验-设置群集

| Q1 | 5.虽然有一些通用的指导原则是可能的,并且您可能需要概括您的集群是否将运行各种作业,但最佳选择取决于预期的工作负载。 | | Q2 | 4.网络存储有多种形式,但在许多情况下,您可能会发现由数百台主机组成的大型 Hadoop 集群依赖于单个(通常是一对)存储设备。 这向群集添加了一个新的故障场景,并且与许多其他场景相比,该场景的可能性较小。 当存储技术确实希望解决故障缓解问题时,通常是通过磁盘级冗余。 这些磁盘阵列可以是高性能的,但通常在读取或写入方面会有损失。 让 Hadoop 控制自己的故障处理,并允许完全并行访问相同数量的磁盘,可能会带来更高的整体性能。 | | Q3 | 3.可能吧! 我们建议避免使用第一种配置,因为虽然它的原始存储刚刚好,而且远未出现动力不足的情况,但这种设置很有可能不会提供多少增长空间。 数据量的增加将立即需要新的主机,而 MapReduce 作业的额外复杂性可能需要额外的处理器能力或内存。配置 B 和 C 看起来都不错,因为它们有多余的存储用于增长,并且为处理器和内存提供了类似的净空。 B 的磁盘 I/O 越高,C 的 CPU 性能越好。 由于主要工作涉及金融建模和预测,我们预计每项任务在 CPU 和内存需求方面都相当重要。 配置 B 可能具有更高的 I/O,但如果处理器以 100%的利用率运行,则很可能不会使用额外的磁盘吞吐量。 因此,处理器能力更强的主机可能更合适。配置 D 对任务来说已经足够了,我们选择它并不是出于这个原因;为什么要购买比我们知道需要的容量更多的容量呢? |
posted @ 2025-10-01 11:29  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报