Hadoop 初学者指南(二)
五、高级 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 联接执行上一节中说明的报告:
-
创建以下以制表符分隔的文件,并将其命名为
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 -
创建以下以制表符分隔的文件,并将其命名为
accounts.txt:001John AllenStandard2012-03-15 002Abigail SmithPremium2004-07-13 003April StevensStandard2010-12-20 004Nasser HafezPremium2001-04-23 -
将数据文件拷贝到 HDFS。
$ hadoop fs -mkdir sales $ hadoop fs -put sales.txt sales/sales.txt $ hadoop fs -mkdir accounts $ hadoop fs -put accounts/accounts.txt -
创建以下文件并将其命名为
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); } } -
编译文件并将其添加到 JAR 文件。
$ javac ReduceJoin.java $ jar -cvf join.jar *.class -
通过执行以下命令运行作业:
$ hadoop jar join.jarReduceJoin sales accounts outputs -
检查结果文件。
$ 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 作业非常相似。 这项工作有几个方面让它变得特别,并允许我们实现联接。
首先,该类有两个已定义的映射器。 正如我们以前看到的,作业可以在一个链中执行多个映射器;但在本例中,我们希望对每个输入位置应用不同的映射器。 因此,我们将销售和帐户数据定义到SalesRecordMapper和AccountRecordMapper类 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包中有像DataJoinMapperBase和TaggedMapOutput这样的类,它们提供了一种封装的方法来派生标签用于映射输出,并在还原器上对其进行处理。 这种机制意味着您不必像我们以前那样定义显式的 Tag 字符串,然后仔细解析在 Reducer 接收到的数据以确定数据来自哪个映射器;提供的类中有封装此功能的方法。
当使用数字或其他非文本数据时,此功能特别有价值。 要创建我们自己的显式标记(如上例所示),我们必须将整数等类型转换为字符串,以便添加所需的前缀标记。 这将比使用标准形式的数值类型并依赖额外的类来实现标记的效率更低。
该框架允许非常复杂的标签生成,以及我们之前没有实现的标签分组等概念。 使用这种机制需要额外的工作,包括覆盖其他方法和使用不同的映射基类。 对于像上一个示例中这样简单的连接,这个框架可能有些夸张,但是如果您发现自己实现了非常复杂的标记逻辑,那么它可能值得一看。
实现地图端连接
要在给定点连接到,我们必须有权访问该点每个数据集中的相应记录。 这就是 Reduce 端联接的简单性发挥作用的地方;尽管它会导致额外的网络流量开销,但根据定义处理它可以确保 Reducer 具有与联接键相关联的所有记录。
如果我们希望在映射器中执行连接,那么要使该条件成立就不那么容易了。 我们不能假设我们的输入数据的结构足够好,可以同时读取相关记录。 我们通常有两类方法:消除从多个外部源读取数据的需要,或者对数据进行预处理,使其可用于地图端连接。
使用分布式缓存
实现第一种方法的最简单方法是获取除一个数据集之外的所有数据集,并使其在我们在上一章中使用的分布式缓存中可用。 该方法可以用于多个数据源,但为简单起见,我们只讨论两个。
如果我们有一个较大的数据集和一个较小的数据集,例如前面的销售和客户信息,一种选择是将客户信息打包并将其推送到 Distributed Cache 中。 然后,每个映射器将该数据读取到一个高效的数据结构中,例如使用连接键作为散列键的散列表。 然后处理销售记录,并且在处理每个记录期间,可以从哈希表中检索所需的帐户信息。
这种机制非常有效,当一个较小的数据集可以很容易地装入内存时,这是一个很好的方法。 然而,我们并不总是那么幸运,有时最小的数据集仍然太大,无法复制到每台工人机器上并保存在内存中。
拥有围棋英雄-实现地图侧连接
以前面的销售/客户记录示例为例,使用 Distributed Cache 实现映射端联接。 如果将帐户记录加载到将帐户 ID 号映射到客户名称的哈希表中,则可以使用帐户 ID 检索客户名称。 在处理销售记录时,在映射器中执行此操作。
修剪数据以适合缓存
如果最小的数据集合仍然太大,无法在分布式缓存中使用,则不一定会丢失所有数据。 例如,我们前面的例子只从每个记录中提取了两个字段,而丢弃了作业不需要的其他字段。 在现实中,帐户将由许多属性描述,这种缩减将极大地限制数据大小。 Hadoop 可用的数据通常是完整的数据集,但我们需要的只是字段的一个子集。
因此,在这种情况下,可以从完整数据集中仅提取 MapReduce 作业期间需要的字段,并在这样做时创建足够小以用于缓存的修剪后的数据集。
备注
这是一个与底层的面向列的数据库非常相似的概念。 传统的关系数据库每次存储一行数据,这意味着需要读取整行才能提取单个列。 基于列的数据库改为单独存储每列,从而允许查询只读取它感兴趣的列。
如果您采用这种方法,您需要考虑将使用什么机制来生成数据子集,以及生成数据子集的频率。 显而易见的方法是编写另一个 MapReduce 作业,该作业执行必要的过滤,然后该输出在分布式缓存中用于后续作业。 如果较小的数据集只有很少的更改,那么您可以按计划生成修剪后的数据集;例如,每晚刷新它。 否则,您将需要创建两个 MapReduce 作业链:一个用于生成修剪后的数据集,另一个用于使用大集和分布式缓存中的数据执行连接操作。
使用数据表示而不是原始数据
有时,其中一个数据源不用于检索其他数据,而是用于派生一些事实,然后在决策过程中使用这些事实。 例如,我们可能希望筛选销售记录,以便仅提取发货地址位于特定区域的记录。
在这种情况下,我们可以将所需的数据大小减少到可能更容易放入缓存中的适用销售记录的列表。 我们可以再次将其存储为哈希表,在哈希表中,我们只是记录记录有效的事实,甚至可以使用排序列表或树之类的东西。 在我们可以接受一些假阳性的情况下,仍然保证没有假阴性,Bloom filter提供了一种非常紧凑的方式来表示这样的信息。
可以看出,应用这种方法来启用地图端连接需要创造力,而且在数据集的性质和手头的问题上需要相当大的运气。 但请记住,最好的关系数据库管理员会花费大量时间优化查询以删除不必要的数据处理;因此,询问您是否真的需要处理所有这些数据并不是一个坏主意。
使用多个映射器
从根本上说,之前的技术试图消除完全交叉数据集联接的需要。 但有时这是您必须要做的;您可能只是拥有非常大的数据集,而这些数据集不能以任何一种聪明的方式组合在一起。
org.apache.hadoop.mapreduce.lib.join包中有支持这种情况的类。 主要感兴趣的类是CompositeInputFormat,它应用一个用户定义的函数来组合来自多个数据源的记录。
这种方法的主要限制是,除了以相同的方式对数据源进行排序和分区之外,还必须已经基于公用键对数据源进行索引。 原因很简单:当从每个源读取数据时,框架需要知道每个位置是否存在给定键。 如果我们知道每个分区都经过排序并且包含相同的键范围,那么简单的迭代逻辑就可以完成所需的匹配。
这种情况显然不会偶然发生,因此您可能再次发现自己编写预处理作业来将所有输入数据源转换为正确的排序和分区结构。
备注
本讨论开始涉及分布式和并行连接算法;这两个主题都有广泛的学术和商业研究。 如果您对这些想法感兴趣,并且想要了解更多的基本理论,请到scholar.google.com上搜索。
加入或不加入...
在我们了解了 MapReduce 世界的连接之后,让我们回到最初的问题:您真的确定要这样做吗? 通常在相对容易实现但效率低下的减端连接和更高效但更复杂的映射端替代方案之间进行选择。 我们已经看到,连接确实可以在 MapReduce 中实现,但它们并不总是美观的。 这就是为什么我们建议使用 Hive 或 PIG 之类的东西,如果这些类型的问题构成了你的工作量的很大一部分。 显然,我们可以使用那些在幕后自行转换为 MapReduce 代码并直接实现映射端和 Reduce 端连接的工具,但对于此类工作负载,通常最好使用设计良好、优化良好的库,而不是构建自己的库。 毕竟,这就是您使用 Hadoop 而不是编写自己的分布式处理框架的原因!
图算法
任何优秀的计算机科学家都会告诉你,图形数据结构是最强大的工具之一。 许多复杂的系统最好用图形来表示,至少几十年(如果你对它有更多的数学知识,可以追溯到几个世纪)的知识体系提供了非常强大的算法来解决各种各样的图形问题。 但就其本质而言,图及其算法在 MapReduce 范例中通常是很难想象的。
图表 101
让我们后退一步,定义一些术语。 图是由节点(也称为顶点)组成的结构,这些节点(也称为顶点)通过称为边的链接相连。 根据图形的类型,边可以是双向的,也可以是单向的,并且可能具有与其关联的权重。 例如,城市道路网可以看作是一个图形,其中道路是边,交叉点和兴趣点是节点。 有些街道是单程的,有些不是,有些是收费的,有些是在一天中的某些时间封闭的,等等。
对于运输公司来说,通过优化从一个点到另一个点的路线可以赚很多钱。 不同的图形算法可以通过考虑诸如单行道之类的属性以及表示为使给定道路更具吸引力或更不吸引人的权重的其他成本来导出这样的路线。
举一个更新的例子,想一想 Facebook 等网站所普及的社交图,其中节点是人,边是他们之间的关系。
图表和 MapReduce-在某处匹配
图形看起来不像许多其他 MapReduce 问题的主要原因是图形处理的有状态特性,这可以在元素之间以及通常在为单一算法一起处理的大量节点之间的基于路径的关系中看到。 图形算法倾向于使用全局状态的概念来确定下一步要处理哪些元素,并在每一步修改这样的全局知识。
具体地说,大多数众所周知的算法通常以增量或重入的方式执行,构建表示已处理节点和挂起节点的结构,并在减少前者的同时处理后者。
另一方面,MapReduce 问题在概念上是无状态的,通常基于分而治之的方法,其中每个 Hadoop 工作线程主机处理一小部分数据,写出最终结果的一部分,其中总作业输出被视为这些较小输出的简单集合。 因此,在 Hadoop 中实现图形算法时,我们需要在无状态并行和分布式框架中表达从根本上是有状态的和概念性单线程的算法。 这就是挑战!
大多数众所周知的图算法都是基于对图的搜索或遍历,通常是为了找到节点之间的路由-通常按某种成本概念进行排序。 最基本的图遍历算法有深度优先搜索(DFS)和广度优先搜索(BFS)。 算法之间的区别在于节点相对于其邻居的处理顺序不同。
我们将介绍实现这种遍历的特殊形式的算法;对于图中给定的起始节点,确定它与图中所有其他节点之间的距离。
备注
可以看出,图形算法和理论领域是一个巨大的领域,我们在这里几乎没有触及到它的皮毛。 如果您想了解更多,图表上的维基百科条目是一个很好的起点;它可以在en.wikipedia.org/wiki/Graph_…找到。
表示图
我们面临的第一个问题是如何用一种我们可以使用 MapReduce 高效处理的方式来表示图形。 有几种众所周知的图表示,称为基于指针的、邻接矩阵和邻接列表。 在大多数实现中,这些表示通常假定单个进程空间具有整个图的全局视图;我们需要修改表示以允许在离散映射和 Reduce 任务中处理单个节点。
我们将在下面的示例中使用此处所示的图表。 该图表确实有一些额外的信息,稍后将对其进行解释。
我们的图非常简单;它只有 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 被标记为完成,几个节点(它的直接邻居)被标记为当前节点,其他几个节点被标记为挂起。 该算法的连续应用将看到所有节点移动到其最终状态;当遇到每个节点时,其邻居被带入处理流水线。 我们稍后会展示这一点。
行动时间-创建源代码
现在我们将查看实现图形遍历的源代码。 因为代码很长,我们将把它分成多个步骤;显然,它们都应该放在一个源文件中。
-
使用这些导入将以下内容创建为
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 { -
创建内部类以保存节点的面向对象表示:
// 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 ; } } -
为作业创建映射器。 映射器将为其输入创建一个新的
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())) ; } } } -
为作业创建减速机。 与映射器一样,这将读入节点的表示形式,并根据节点的状态提供不同的值作为输出。 基本方法是从输入中收集状态和距离列的最大值,并通过此方法收敛到最终解决方案。
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)) ; } } -
创建作业驱动程序:
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内部类允许从平面文件到对象表示的映射,该对象表示将被封装在一个在业务领域方面有意义的对象中。 这也使得映射器和缩减器逻辑更加清晰,因为对象属性之间的比较比与仅由绝对索引位置标识的字符串片段的比较在语义上更有意义。
行动时间-第一次运行
现在,让我们对图形的起始表示执行此算法的初始执行:
-
将先前创建的
graph.txt文件放入 HDFS:$ hadoop fs -mkdirgraphin $ hadoop fs -put graph.txtgraphin/graph.txt -
编译作业并创建 JAR 文件:
$ javac GraphPath.java $ jar -cvf graph.jar *.class -
执行 MapReduce 作业:
$ hadoop jar graph.jarGraphPathgraphingraphout1 -
检查输出文件:
$ 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 的邻居-被标记为当前正在处理
- 所有其他节点都处于挂起状态
我们的图表现在看起来如下图所示:
在给定算法的情况下,这是意料之中的;第一个节点已完成,其通过映射器提取的相邻节点正在进行中。 所有其他节点尚未开始处理。
行动时间-第二次运行
如果我们将这个表示作为作业的另一次运行的输入,我们预计节点 2、3 和 4 现在应该是完整的,并且它们的邻居现在处于当前状态。 让我们看看,执行以下步骤:
-
通过执行以下命令执行 MapReduce 作业:
$ hadoop jar graph.jarGraphPathgraphout1graphout2 -
检查输出文件:
$ 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 仍处于挂起状态,如下图中的所示:
如果我们再次运行该作业,我们应该预期节点 5 和 6 已经完成,并且任何未处理的邻居都将成为当前节点。
行动时间-第三次运行
让我们通过第三次运行该算法来验证该假设。
-
执行 MapReduce 作业:
$ hadoop jar graph.jarGraphPathgraphout2graphout3 -
检查输出文件:
$ 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 仍处于挂起状态,当前没有节点在处理中,如下图所示:
这种状态的原因是,虽然节点 7 具有到节点 6 的链路,但在相反方向上没有边。 因此,实际上无法从节点 1 到达节点 7。如果我们最后一次运行该算法,我们应该会看到该图没有变化。
行动时间-第四次也是最后一次运行
让我们执行第四次执行,以验证输出现在已达到其最终稳定状态。
-
执行 MapReduce 作业:
$ hadoop jar graph.jarGraphPathgraphout3graphout4 -
检查输出文件:
$ 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,因此它将保持挂起状态,并且永远不会被进一步处理。 因此,我们的图形保持不变,如下图所示:
我们没有构建到算法中的一件事就是了解终止条件;如果运行没有创建任何新的 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 上。 在这里,我们还很难轻松地表示复杂的数据类型;我们要么必须将它们扁平化为文本格式,要么必须编写跨两种二进制格式的转换器,这两种格式都不是一个有吸引力的选择。
候选技术
幸运的是,近年来发布了几种技术,它们解决了跨语言数据表示的问题。 它们是协议缓冲区(由谷歌创建并托管在code.google.com/p/protobuf)、Thrift(最初由 Facebook 创建,现在是thrift.apache.org的一个 Apache 项目), 和Avro(由 Hadoop 的原始创建者 Doug Cutting 创建)。 考虑到它的传统和与 Hadoop 的紧密集成,我们将使用 Avro 来探讨这个主题。 我们不会在本书中介绍 Thrift 或 Protocol Buffers,但它们都是可靠的技术;如果您对数据序列化这一主题感兴趣,请查看它们的主页以获取更多信息。
介绍 Avro
AVRO 的主页是avro.apache.org,它是一个绑定了许多编程语言的数据持久化框架。 它创建了一种既可压缩又可拆分的二进制结构化格式,这意味着可以有效地将其用作 MapReduce 作业的输入。
Avro 允许定义分层数据结构;例如,我们可以创建包含数组、枚举类型和子记录的记录。 我们可以用任何编程语言创建这些文件,在 Hadoop 中处理它们,然后用第三种语言读取结果。
我们将在接下来的小节中讨论语言独立性的这些方面,但这种表达复杂结构化类型的能力也非常有价值。 即使我们只使用 Java,我们也可以使用 Avro 来允许我们将复杂的数据结构传入和传出映射器和减法器。 甚至像图形节点这样的东西!
该行动了-获取并安装 Avro
让我们下载 Avro 并将其安装到我们的系统上。
-
从avro.apache.org/releases.ht…下载 AVRO 的最新稳定版本。
-
从paranamer.codehaus.org下载最新版本的 ParaNamer 库。
-
将类添加到 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 -
将 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} -
将新的 JAR 文件添加到 Hadoop
lib目录。$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 的依赖,我们从它的主页codehaus.org下载。
备注
在撰写本文时,ParaNamer 主页的下载链接已损坏;作为替代方案,请尝试以下链接:
下载这些 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 的跨语言功能。
-
添加
rubygems包:$ sudo apt-get install rubygems -
安装 Avro gem:
$ gem install avro -
将以下内容创建为
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 -
运行程序并创建数据文件:
$ ruby generate.rb
刚刚发生了什么?
在使用 Ruby 之前,我们确保在我们的 Ubuntu 主机上安装了rubygems包。 然后我们安装预先存在的用于 Ruby 的 Avro gem。 这提供了在 Ruby 语言中读写 avro 文件所需的库。
Ruby 脚本本身只是读取前面创建的模式,并创建一个包含 10 条测试记录的数据文件。 然后我们运行该程序来创建数据。
这不是 Ruby 教程,所以我将把 Ruby API 的分析留给读者作为练习;它的文档可以在rubygems.org/gems/avro找到。
Java 操作消耗 Avro 数据的时间
现在我们已经有了个 Avro 数据,现在让我们编写一些 Java 代码来使用它:
-
将以下内容创建为
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) ; } } } -
Compile and run the program:
$ javacInputRead.java $ java InputReadsightings.avro输出将如以下屏幕截图所示:
刚刚发生了什么?
我们创建了 Java 类InputRead,它接受作为命令行参数传递的文件名,并将其解析为 avro 数据文件。 当 Avro 从数据文件中读取时,每个单独的元素被称为数据,并且每个数据将遵循模式中定义的结构。
在本例中,我们不使用显式模式;相反,我们将每个数据读入GenericRecord类,然后通过按名称显式检索每个字段来从中提取每个字段。
GenericRecord类在 Avro 中是一个非常灵活的类;它可以用来包装任何记录结构,比如我们的 UFO 目击类型。 Avro 还支持基元类型(如整数、浮点数和布尔值)以及其他结构化类型(如数组和枚举)。 在这些示例中,我们将使用记录作为最常见的结构,但这只是为了方便起见。
在 MapReduce 中使用 Avro
Avro 对 MapReduce 的支持围绕着其他熟悉类的几个特定于 Avro 的变体,而我们通常期望 Hadoop 通过新的InputFormat和OutputFormat类支持新的数据文件格式,我们将使用AvroJob、AvroMapper和AvroReducer,而不是使用AvroJob、AvroMapper和AvroReducer,而不是。 AvroJob 希望将 Avro 数据文件作为其输入和输出,因此我们不指定输入和输出格式类型,而是使用输入和输出 Avro 模式的详细信息对其进行配置。
我们的映射器和减少器实现的主要区别在于使用的类型。 默认情况下,avro 只有一个输入和输出,而我们习惯了Mapper和Reducer类有一个键/值输入和一个键/值输出。 Avro 还引入了Pair类,它通常用于发出中间键/值数据。
Avro 还支持AvroKey和AvroValue,它们可以包装其他类型,但我们不会在下面的示例中使用它们。
在 MapReduce 中生成形状摘要的时间
在本节中,我们将编写一个映射器,该映射器接受我们先前定义的 UFO 目击记录作为输入。 它将输出形状和计数1,缩减器将采用此形状和计数记录,并生成一个新的结构化 Avro 数据文件类型,其中包含每个 UFO 形状的最终计数。 执行以下步骤:
-
将
sightings.avro文件复制到 HDFS。$ hadoopfs -mkdiravroin $ hadoopfs -put sightings.avroavroin/sightings.avro -
将以下内容创建为
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); } } -
编译并运行作业:
$ javacAvroMR.java $ jar -cvfavroufo.jar *.class ufo.avsc $ hadoop jar ~/classes/avroufo.jarAvroMRavroinavroout -
检查输出目录:
$ 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 -
将输出文件复制到本地文件系统:
$ hadoopfs -get /user/hadoop/avroout/part-00000.avroresult.avro
刚刚发生了什么?
我们创建了Job类并检查了它的各种组件。 Mapper和Reducer类中的实际逻辑相对简单:Mapper类只提取 Shape 列并发出计数为1的列;然后减法器计算每个 Shape 的条目总数。 有趣的方面是定义了Mapper和Reducer类的输入和输出类型,以及作业是如何配置的。
Mapper类的输入类型为GenericRecord,输出类型为Pair。 Reducer类具有对应的输入类型Pair和输出类型GenericRecord。
传递给Mapper类的GenericRecord类包装了一个数据,该数据是输入文件中表示的 UFO 目击记录。 这就是Mapper类能够按名称检索Shape字段的方式。
回想一下,GenericRecords可能是使用模式显式创建的,也可能不是使用模式显式创建的,在任何一种情况下,结构都可以从数据文件中确定。 对于Reducer类的GenericRecord输出,我们确实传递了一个模式,但使用了一种新的机制来创建它。
在前面提到的代码中,我们创建了额外的UFORecord类,并使用 Avro 反射在运行时动态生成其模式。 然后,我们可以使用此模式创建专门包装该特定记录类型的GenericRecord类。
在Mapper和Reducer类之间,我们使用 avroPair类型来保存键和值对。 这允许我们为Mapper和Reducer类表达与我们在第 2 章,启动并运行 Hadoop中的原始 wordcount 示例中使用的相同逻辑;Mapper 类为每个值发出单个计数,减法器将这些计数相加为每个形状的总计数。
除了Mapper和Reducer类的输入和输出之外,还有一些处理 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 将根据指定的架构验证输入和输出,因此存在一定程度的数据类型安全。 对于其他元素,比如设置Mapper和Reducer类,我们只需在 AvroJob 上设置这些元素,而不是设置更通用的类,一旦完成,MapReduce 框架就会正常运行。
这个示例也是我们第一次显式实现Tool接口。 在运行 Hadoop 命令行程序时,有一系列参数(如-D)在所有多个子命令中是通用的。 如果作业类实现了上一节提到的Tool接口,它将自动访问在命令行上传递的这些标准选项中的任何一个。 这是一种防止大量代码重复的有用机制。
该行动了-用 Ruby 检查输出数据
现在我们已经有了作业的输出数据,让我们使用 Ruby 再次检查它。
-
将以下内容创建为
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 -
检查创建的结果文件。
$ ruby read.rb blur 1 cylinder 1 diamond 2 formation 1 light 3 saucer 1
刚刚发生了什么?
和以前一样,我们不会分析 Ruby Avro API。 该示例创建了一个 Ruby 脚本,该脚本打开一个 avro 数据文件,遍历每个数据,并根据显式命名的字段显示它。 请注意,该脚本不能访问数据文件的架构;标题中的信息提供了足够的数据来检索每个字段。
行动时间-用 Java 检查输出数据
为了说明可以从多种语言访问数据,我们还使用 Java 显示作业输出。
-
将以下内容创建为
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) ; } } } -
编译并运行程序:
$ 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 进程:
-
Firstly, check on the original status of the cluster and check whether everything is healthy. We'll use the
dfsadmincommand 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 -
使用 DataNode 进程的进程 ID(PID)并终止它:
$ kill -9 1928 -
检查主机
$ jps 2085 TaskTracker上是否不再运行 DataNode 进程
-
使用
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) … -
要关注的关键线路是报告块、活动节点和每个节点的最后联系时间的线路。 一旦死节点的最后联系时间约为 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) … -
重复该过程,直到复制不足的数据块计数再次为
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。 我们将简要演练本练习,因为它与前面的操作时间部分非常相似:
-
重新启动失效的 DataNode 并监视群集,直到所有节点都标记为活动。
-
选择两个 DataNode,使用进程 ID,然后终止 DataNode 进程。
-
如前所述,等待大约 10 分钟,然后通过
dfsadmin主动监视群集状态,特别注意报告的复制不足数据块数量。 -
等待群集稳定下来,并产生类似于以下内容的输出:
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 的步骤:
-
使用以下命令重新启动所有节点:
$ start-all.sh -
等待 Hadoop
dfsadmin -report显示四个活动节点。 -
将测试文件的新副本放到 HDFS 上:
$ Hadoop fs -put file1.data file1.new -
登录到其中三台群集主机,并终止每台主机上的 DataNode 进程。
-
等待通常的 10 分钟,然后通过
dfsadmin开始监视群集,直到您得到类似以下报告丢失数据块的输出:… Under replicated blocks: 123 Blocks with corrupt replicas: 0 Missing blocks: 33 ------------------------------------------------- Datanodes available: 1 (4 total, 3 dead) … -
尝试从 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 -
使用
start-all.sh脚本重新启动失效节点:$ start-all.sh -
重复监视块的状态:
$ 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 -
等待,直到没有报告丢失块,然后将测试文件复制到本地文件系统:
$ Hadoop fs -get file1.new file1.new -
对此文件和原始文件执行 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 集群的运行状况。
执行以下步骤:
-
Ensure everything is running via the
start-all.shscript then point your browser at the MapReduce web UI. The page should look like the following screenshot: -
启动一个长期运行的 MapReduce 作业;具有较大值的示例 pi 估计器非常适用于此:
$ Hadoop jar Hadoop/Hadoop-examples-1.0.4.jar pi 2500 2500 -
现在登录到群集节点并使用
jps标识 TaskTracker 进程:$ jps 21822 TaskTracker 3918 Jps 3891 DataNode -
终止 TaskTracker 进程:
$ kill -9 21822 -
验证 TaskTracker 是否不再运行:
$jps 3918 Jps 3891 DataNode -
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:
-
在原始窗口中监视作业进度;作业应该正在进行,即使速度很慢。
-
重新启动死的 TaskTracker 进程:
$ start-all.sh -
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:
刚刚发生了什么?
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/stop和slaves.sh脚本等工具使用。 您不需要在每个节点上都保持更新,只需要在通常运行此类命令的主机上保持更新即可。 实际上,这可能是一个专用的头节点或运行 NameNode 或 JobTracker 进程的主机。 我们将在第 7 章、中探讨这些设置。
杀死群集主机
虽然 DataNode 和 TaskTracker 进程的故障影响不同,但每个单独的节点都相对不重要。 任何一个 TaskTracker 或 DataNode 的故障都不值得担心,只有当多个其他 TaskTracker 或 DataNode 失败时才会出现问题,特别是在快速接连失败的情况下。 但是我们只有一个 JobTracker 和 NameNode;让我们来看看当它们失败时会发生什么。
该行动了-干掉 JobTracker
我们将首先终止 JobTracker 进程,这应该会影响我们执行 MapReduce 作业的能力,但不会影响底层的 HDFS 文件系统。
-
登录到 JobTracker 主机并终止其进程。
-
尝试启动测试 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) … -
执行一些 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 作业执行:
备注
不要在具有重要运营意义的群集上尝试此操作。 虽然影响将是短暂的,但它实际上会在一段时间内杀死整个群集。
-
登录到 NameNode 主机并列出正在运行的进程:
$ jps 2372 SecondaryNameNode 2118 NameNode 2434 JobTracker 5153 Jps -
终止 NameNode 进程。 不用担心 Second DaryNameNode,它可以继续运行。
-
尝试访问 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. -
提交 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 … -
检查正在运行的个进程:
$ jps 2372 SecondaryNameNode 5253 Jps 2434 JobTracker Restart the NameNode $ start-all.sh -
访问 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 用两个新组件BackupNode和CheckpointNode替换了 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 看来已经停止进展会发生什么。
导致行动任务失败的时间
让我们导致任务失败;在此之前,我们需要修改默认超时:
-
将此配置属性添加到
mapred-site.xml:<property> <name>mapred.task.timeout</name> <value>30000</value> </property> -
现在我们将修改第 3 章,了解 MapReduce中的老朋友 Wordcount。 将
WordCount3.java复制到名为WordCountTimeout.java的新文件中,并添加以下导入:import java.util.concurrent.TimeUnit ; import org.apache.hadoop.fs.FileSystem ; import org.apache.hadoop.fs.FSDataOutputStream ; -
将
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); } } } -
更改类名后编译文件,将其压缩,然后在集群上执行:
$ 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 任务[t1mapred.max.tracker.failures:如果记录了如此多的单个任务失败,则作业将失败
所有这些的默认值都是 4。
备注
请注意,将mapred.tracker.max.failures设置为小于其他两个属性的值是没有意义的。
您考虑设置其中哪一个将取决于您的数据和作业的性质。 如果您的作业访问的外部资源偶尔会导致暂时性错误,则增加任务的重复失败次数可能会很有用。 但是,如果任务非常特定于数据,则这些属性可能不太适用,因为失败一次的任务将再次失败。 但是,请注意,默认值高于 1 确实有意义,因为在大型复杂系统中,各种瞬态故障总是可能发生的。
有一个围棋英雄--导致任务失败
修改 wordcount 示例;不是休眠,而是让它抛出一个基于随机数的 RuntimeException。 修改群集配置,并探索管理多少失败的任务将导致整个作业失败的配置属性之间的关系。
数据导致任务失败
我们将探索的最后类故障是与数据相关的故障类型。 这里,我们指的是由于给定记录的数据已损坏、使用了错误的数据类型或格式或各种相关问题而崩溃的任务。 我们指的是那些收到的数据与预期不同的情况。
通过代码处理脏数据
处理脏数据的一种方法是编写防御性处理数据的映射器和减少器。 因此,例如,如果映射器接收的值应该是逗号分隔的值列表,则在处理数据之前首先验证项数。 如果第一个值应该是整数的字符串表示形式,请确保转换为数值类型时具有可靠的错误处理和默认行为。
这种方法的问题在于,无论您多么小心,总会有一些奇怪的数据输入没有被考虑到。 您是否考虑过接收不同 Unicode 字符集的值? 如果有多个字符集、空值、结尾错误的字符串、错误编码的转义字符等怎么办?
如果输入到作业的数据是由您生成和/或控制的,那么这些可能性就不那么重要了。 但是,如果您正在处理从外部来源接收的数据,总会有令人惊讶的理由。
使用 Hadoop 的跳过模式
另一种方法是将 Hadoop 配置为以不同方式处理任务失败。 Hadoop 不会将失败的任务视为原子事件,而是可以尝试识别哪些记录可能导致了问题,并将它们排除在未来的任务执行之外。 该机制称为跳过模式。 如果您遇到各种各样的数据问题,其中围绕这些问题进行编码是不可取或不实用的,那么这会很有用。 或者,如果在您的工作中使用的是第三方库,而您可能没有源代码,那么您可能别无选择。
跳过模式目前仅适用于写入 API 0.20 之前版本的作业,这是另一个需要考虑的问题。
使用跳过模式处理脏数据的操作时间
让我们通过编写一个 MapReduce 作业来查看操作中的跳过模式,该作业接收导致其失败的数据:
-
将以下 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 -
运行脚本:
$ ruby gendata.rb -
检查生成的文件的大小及其行数:
$ ls -lh skipdata.txt -rw-rw-r-- 1 hadoop hadoop 29M 2011-12-17 01:53 skipdata.txt ~$ cat skipdata.txt | wc -l 2000015 -
将文件复制到 HDFS:
$ hadoop fs -put skipdata.txt skipdata.txt -
将以下属性定义添加到
mapred-site.xml:<property> <name>mapred.skip.map.max.skip.records</name> <value5</value> </property> -
检查为
mapred.max.map.task.failures设置的值,如果该值较低,则将其设置为20。 -
将以下 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); } } -
编译该文件并将其 JAR 到
skipdata.jar。 -
运行作业:
$ 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 ... -
检查作业输出文件的内容:
```scala
$ hadoop fs -cat output/part-00000
totalcount 2000000
```
11. 在输出目录中查找跳过的记录:
```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
…
```
12. Check the job details from the MapReduce UI to observe the recorded statistics as shown in the following screenshot:

刚刚发生了什么?
我们必须在这里做很多设置,所以让我们一步一步地来完成。
首先,我们需要将 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.xmlHadoop/src/hdfs/hdfs-default.xmlHadoop/src/mapred/mapred-default.xml
操作浏览默认属性的时间
幸运的是,XML 文档不是查看默认值的唯一方式;还有更具可读性的 HTML 版本,我们现在将快速了解一下。
这些文件不包括在 Hadoop 仅限二进制版本中;如果您正在使用该版本,还可以在 Hadoop 网站上找到这些文件。
-
Point your browser at the
docs/core-default.htmlfile within your Hadoop distribution directory and browse its contents. It should look like the next screenshot: -
现在,类似地,浏览以下其他文件:
Hadoop/docs/hdfs-default.htmlHadoop/docs/mapred-default.html
刚刚发生了什么?
如您所见,每个属性都有名称、默认值和简短描述。 您还会看到确实有非常多的属性。 现在不要期望了解所有这些内容,但一定要花点时间浏览一下,以了解 Hadoop 允许的定制类型。
其他属性元素
当我们之前在配置文件中设置了属性时,我们使用了以下形式的 XML 元素:
<property>
<name>the.property.name</name>
<value>The property value</value>
</property>
我们还可以添加另外两个可选的 XML 元素:description和final。 现在,使用这些附加元素的完整描述属性如下所示:
<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 地址到机架名称的映射。
如果未指定脚本,则每个节点将被报告为单个默认机架的成员。
行动时间-检查默认机架配置
让我们来看看如何在我们的集群中设置默认机架配置。
-
执行以下命令:
$ Hadoop fsck -rack -
结果应包括类似以下内容的输出:
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 还将删除多余的副本以将数据块返回到复制因素。
该行动了-添加机架感知脚本
我们可以通过创建派生每个主机的机架位置的脚本来增强默认的扁平机架配置。
-
在 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 -
使此脚本可执行。
$ chmod +x rack-script.sh -
将以下属性添加到 NameNode 主机上的
core-site.xml:<property> <name>topology.script.file.name</name> <value>/home/Hadoop/rack-script.sh</value> </property> -
重新启动 HDFS。
$ start-dfs.sh -
Check the filesystem via
fsck.$ Hadoop fsck –rack上述命令的输出如下图所示:
刚刚发生了什么?
我们首先创建了一个简单的脚本,该脚本为命名节点返回一个值,为所有其他节点返回一个默认值。 我们将其放在 NameNode 主机上,并将所需的配置属性添加到 NameNodecore-site.xml文件。
启动 HDFS 后,我们使用hadoop fsck报告文件系统,看到现在有了一个双机架群集。 有了这些知识,Hadoop 现在将采用更复杂的块放置策略,如前所述。
提示
使用外部主机文件
一种常见的方法是在 Unix 上保留一个类似于/etc/hosts文件的单独数据文件,并使用该文件指定 IP/机架映射,每行一个。 然后,该文件可以独立更新,并由机架识别脚本读取。
什么是商用硬件?
让我们回顾一下问题,即集群使用的主机的一般特征,以及它们看起来是更像一个商用白盒服务器,还是更像是为高端企业环境而构建的东西。
问题的一部分是“商品”是一个模棱两可的术语。 对于一家企业来说,看起来便宜而令人愉悦的东西,对另一家企业来说,可能看起来是奢侈的高端。 我们建议在选择硬件时考虑以下几点,然后对您的决定保持满意:
- 对于您的硬件,您是否为复制 Hadoop 的某些容错功能的可靠性功能支付了额外费用?
- 您为解决已确认的需求或风险而支付的高端硬件功能在您的环境中是否切合实际?
- 您是否已验证高端硬件的成本高于价格较低/可靠性较低的硬件?
弹出式测验-设置群集
问题 1.。 在为您的新 Hadoop 群集选择硬件时,以下哪项最重要?
- CPU 核心的数量及其速度。
- 物理内存量。
- 存储量。
- 存储的速度。
- 这取决于最有可能的工作负载。
Q2.。 为什么您可能不想在群集中使用网络存储?
- 因为它可能会引入新的单点故障。
- 因为考虑到 Hadoop 的容错能力,它很可能具有冗余和容错的方法,这可能是不必要的。
- 因为这样的单个设备的性能可能低于 Hadoop 同时使用多个本地磁盘的性能。
- 以上都是。
第三季度。 您将在群集上处理 10 TB 的数据。 您的主要 MapReduce 作业处理金融交易,使用它们生成行为和未来预测的统计模型。 以下哪种硬件选择会是您群集的首选?
- 20 台主机,每台配备快速双核处理器、4 GB 内存和一个 500 GB 磁盘驱动器。
- 30 台主机,每台配备快速双核处理器、8 GB 内存和两个 500 GB 磁盘驱动器。
- 30 台主机,每台配备快速四核处理器、8 GB 内存和一个 1 TB 磁盘驱动器。
- 40 台主机,每台配备 16 GB 内存、快速四核处理器和四个 1 TB 磁盘驱动器。
集群访问控制
一旦启动并运行了这个闪亮的新集群,您就需要考虑访问和安全问题。 谁可以访问群集上的数据-是否有您真的不想让整个用户群看到的敏感数据?
Hadoop 安全模型
直到最近,Hadoop 还拥有一个充其量可以被描述为“仅标记”的安全模型。 它将所有者和组与每个文件相关联,但是,正如我们将看到的,它几乎没有对给定的客户端连接进行验证。 强大的安全性不仅可以管理指定给文件的标记,还可以管理所有连接用户的身份。
行动时间-演示默认安全性
当我们以前显示了文件列表时,我们已经看到了它们的用户名和组名。 然而,我们还没有真正探索这意味着什么。 我们就这么做吧。
-
在 Hadoop 用户的主目录中创建一个测试文本文件。
$ echo "I can read this!" > security-test.txt $ hadoop fs -put security-test.txt security-test.txt -
Change the permissions on the file to be accessible only by the owner.
$ hadoop fs -chmod 700 security-test.txt $ hadoop fs -ls上述命令的输出如下图所示:
-
Confirm you can still read the file.
$ hadoop fs -cat security-test.txt您将在屏幕上看到以下行:
I can read this! -
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! -
从另一个节点注销。
$ exit -
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上述命令的输出如下图所示:
-
切换到该用户。
$ su garry -
尝试读取 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------- -
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上述命令的输出如以下截图所示:
-
Confirm this user can access the file.
```scala
$ hadoop/bin/hadoop fs -cat security-test.txt
```
您将在屏幕上看到以下行:
```scala
I can read this!
```
11. 返回到 Hadoop 用户。
```scala
$ exit
```
12. 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 fs和Hadoop jar等命令时找到它们。
此模型之所以有效,是因为只有一台主机用于与 Hadoop 交互;而且由于该主机由集群管理员控制,普通用户应该无法创建或访问其他用户帐户。
请记住,此方法不会提供安全性。 它在一个软件系统周围设置了一个硬外壳,以减少 Hadoop 安全模型被颠覆的方式。
管理 NameNode
让我们再做一些风险降低。 在第 6 章,当事情中断时,当我谈到运行 NameNode 的主机故障的潜在后果时,我可能吓到您了。 如果那一节没有吓到你,那就回去重读一遍--它应该吓到你的。 总结是,丢失 NameNode 可能会丢失集群上的每一条条数据。 这是因为 NameNode 写入一个名为fsimage的文件,该文件包含文件系统的所有元数据,并记录哪些块组成哪些文件。 如果 NameNode 主机丢失导致fsimage无法恢复,则所有 HDFS 数据也同样丢失。
为 fsimage 类配置多个位置
NameNode 可以配置为同时将fsimage写入多个位置。 这纯粹是一种冗余机制,相同的数据写入每个位置,并且不会尝试使用多个存储设备来提高性能。 相反,政策是fsimage的多个副本将更难丢失。
该行动了-添加额外的图像位置
现在,让我们将 NameNode 配置为同时写入fsimage的多个副本,以提供所需的数据弹性。 为此,我们需要一个 NFS 导出的目录。
-
确保群集已停止。
$ stopall.sh -
将以下属性添加到
Hadoop/conf/core-site.xml,将第二个路径修改为指向可以写入 NameNode 数据的附加副本的 NFS 挂载位置。<property> <name>dfs.name.dir</name> <value>${hadoop.tmp.dir}/dfs/name,/share/backup/namenode</value> </property> -
删除新添加目录的所有现有内容。
$ rm -f /share/backup/namenode -
启动群集。
$ start-all.sh -
通过对前面指定的两个文件运行
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.xml和hdfs-site.xml文件,将它们(理想情况下)放在 NFS 位置,然后更新它们以指向新主机。 每次修改当前配置文件时,请记住对这些副本进行相同的更改。 - 将
slaves文件从 NameNode 复制到新主机或 NFS 共享。 另外,一定要让它保持最新。 - 了解您将如何处理新主机中的后续故障。 您可能会以多快的速度修复或更换原来出现故障的主机? 在此期间,哪个主机将是 NameNode(和辅助 NameNode)的位置?
准备好的?。 那,我们做吧!
是时候交换到新的 NameNode 主机了
在下面的步骤中,我们将新配置文件保留在挂载到/share/backup的 NFS 共享上,并更改路径以匹配您拥有新文件的位置。 对 grep 也使用不同的字符串;我们使用我们知道的未与集群中的任何其他主机共享的 IP 地址的一部分。
-
登录到当前 NameNode 主机并关闭群集。
$ stop-all.sh -
停止运行 NameNode 的主机。
$ sudo poweroff -
登录到新的 NameNode 主机并确认新的配置文件具有正确的 NameNode 位置。
$ grep 110 /share/backup/*.xml -
在新主机上,首先复制
slaves文件。$ cp /share/backup/slaves Hadoop/conf -
现在复制更新后的配置文件。
$ cp /share/backup/*site.xml Hadoop/conf -
从本地文件系统中删除所有旧的 NameNode 数据。
$ rm -f /var/Hadoop/dfs/name/* -
将更新的配置文件复制到群集中的每个节点。
$ slaves.sh cp /share/backup/*site.xml Hadoop/conf -
确保每个节点现在都有指向新 NameNode 的配置文件。
$ slaves.sh grep 110 hadoop/conf/*site.xml -
启动群集。
$ start-all.sh -
从命令行检查 HDFS 是否运行正常。
```scala
$ Hadoop fs ls /
```
11. 验证是否可以从 Web 用户界面访问 HDFS。
刚刚发生了什么?
首先,我们关闭集群。 这有点不具代表性,因为大多数故障都会看到 NameNode 以一种不太友好的方式死去,但我们不想在本章后面讨论文件系统损坏的问题。
然后,我们关闭旧的 NameNode 主机。 虽然不是绝对必要的,但这是一种很好的方法,可以确保没有人访问旧主机,并且会让您对迁移进行得有多好有不正确的看法。
在跨文件复制之前,我们快速查看一下core-site.xml和hdfs-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.sh和stop-balancer.sh脚本启动和停止来处理这种情况。
何时重新平衡
Hadoop 没有任何自动警报,可以提醒您文件系统不平衡。 相反,您需要密切关注hadoop fsck和hadoop fsadmin报告的数据,并关注节点之间的不平衡。
实际上,这并不是您通常需要担心的问题,因为 Hadoop 非常擅长管理块放置,并且在添加新硬件或恢复故障节点服务时,您可能只需要考虑运行平衡器来消除严重的不平衡。 但是,为了保持最大的集群健康,让平衡器按计划(例如,每晚)运行以将块平衡保持在指定阈值内的情况并不少见。
MapReduce 管理
正如我们在前面的章中所看到的,MapReduce 框架通常比 HDFS 更能容忍问题和故障。 JobTracker 和 TaskTracker 没有要管理的持久数据,因此,MapReduce 的管理更多的是处理正在运行的作业和任务,而不是服务于框架本身。
命令行作业管理
hadoop job命令行工具是此作业管理的主要界面。 像往常一样,键入以下内容以获取使用摘要:
$ hadoop job --help
该命令的选项通常不言自明;除了检索作业历史记录的某些元素外,它还允许您启动、停止、列出和修改正在运行的作业。 在下一节中,我们将一起探讨其中几个子命令的用法,而不是分别研究每个子命令。
拥有 Go 英雄-命令行工作管理
MapReduce UI 还提供对这些功能子集的访问。 浏览用户界面,了解您可以在 Web 界面上执行哪些操作,以及不可以执行哪些操作。
作业优先级和调度
到目前为止,我们通常对集群运行单个作业并等待其完成。 这隐藏了一个事实,即默认情况下,Hadoop 将后续作业提交放入先进先出(FIFO)队列。 当一个作业完成时,Hadoop 只是开始执行队列中的下一个作业。 除非我们使用我们将在后面部分讨论的替代调度器之一,否则 FIFO 调度器会将整个集群专用于当前正在运行的唯一作业。
对于作业提交模式很少看到作业在队列中等待的小集群来说,这完全没有问题。 但是,如果作业经常在队列中等待,则可能会出现问题。 特别是,FIFO 模型没有考虑作业优先级或所需的资源。 长时间运行但低优先级的作业将在稍后提交的较快的高优先级作业之前执行。
为了解决这种情况,Hadoop 定义了五个作业优先级级别:VERY_HIGH、HIGH、NORMAL、LOW和VERY_LOW。 作业的默认优先级为NORMAL,但可以使用hadoop job -set-priority命令进行更改。
是时候采取行动了-更改作业优先级并终止作业
让我们通过动态更改作业优先级并观察终止作业的结果来探索作业优先级。
-
在群集上启动运行时间相对较长的作业。
$ hadoop jar hadoop-examples-1.0.4.jar pi 100 1000 -
打开另一个窗口并提交第二个作业。
$ hadoop jar hadoop-examples-1.0.4.jar wordcount test.txt out1 -
打开另一个窗口并提交第三个窗口。
$ hadoop jar hadoop-examples-1.0.4.jar wordcount test.txt out2 -
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 -
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 -
将上次提交的作业的优先级提高到
VERY_HIGH。$ Hadoop job -set-priority job_201201111540_0007 VERY_HIGH -
取消当前正在运行的作业。
$ Hadoop job -kill job_201201111540_0005 -
查看其他作业以查看哪些作业开始处理。
刚刚发生了什么?
我们在集群上启动了一个作业,然后将另外两个作业排队,使用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目录内的capacityScheduler和fairScheduler目录中以 JAR 文件的形式提供。 要启用调度程序,要么将其 JAR 添加到hadoop/lib目录,要么显式地将其放在类路径上。 请注意,每个调度程序都需要自己的一组属性来配置其使用情况。 有关更多详细信息,请参阅各自的文档。
何时使用替代调度程序
备用调度器非常有效,但在小型集群或那些不需要确保多个作业并发或执行晚到但优先级高的作业的集群上并不真正需要。 每个服务器都有多个配置参数,需要进行调整才能获得最佳的集群利用率。 但对于具有多个用户和不同作业优先级的任何大型群集,它们可能是必不可少的。
缩放
您有数据,并且有一个正在运行的 Hadoop 集群;现在,您获得了更多的前者,也需要更多的后者。 我们反复说过,Hadoop 是一个易于扩展的系统。 因此,让我们增加一些新的容量。
向本地 Hadoop 群集添加容量
希望在这一点上,您应该对向正在运行的集群添加另一个节点的想法感到非常不满意。 在第 6 章、中,当事情中断时,我们不断地终止和重新启动节点。 添加新节点实际上没有什么不同,您只需执行以下步骤:
- 在主机上安装 Hadoop。
- 设置第 2 章、设置和运行中所示的环境变量。
- 将配置文件复制到安装上的
conf目录。 - 将主机的 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 中数据的关系视图开始。