横向扩展
前面介绍了MapReduce针对少量输入数据是如何工作的,现在我们开始鸟瞰整个系统以及有大量输入数据时数据是如何处理的。为了简单起见,到目前为止,我们的例子都只是用了本地文件系统中的文件。然而,为了实现横向扩展(scaling out),我们需要把数据存储在分布式文件系统中,一般为HDFS (详见第3章),由此允许Hadoop将MapReduce 计算移到存储有部分数据的各台机器上。下面我们看看具体过程。
数据流
首先定义一些术语。MapReduce作业(job) 是客户端需要执行的一个工作单元:它包括输入数据、MapReduce程序和配置信息。Hadoop将作业分成若干个小任务 (task)来执行,其中包括两类任务:map任务和reduce任务。
有两类节点控制着作业执行过程:一个jobtracker及一系列tasktracker。jobtracker通过调度tasktracker上运行的任务,来协调所有运行在系统上的作业。tasktracker在运行任务的同时将运行进度报告发送给jobtracker,jobtracker由此记录每项作业任务的整体进度情况。如果其中一个任务失败,jobtracker可以在另外一个tasktracker节点上重新调度该任务。
Hadoop将MapReduce的输入数据划分成等长的小数据块,称为输入分片(input split)或简称分片。Hadoop为每个分片构建一个map任务,并由该任务来运行用户自定义的map 函数从而处理分片中的每条记录。
拥有许多分片,意味着处理每个分片所需要的时间少于处理整个输入数据所花的时间。因此,如果我们并行处理每个分片,且每个分片数据比较小,那么整个处理过程将获得更好的负载平衡,因为一台较快的计算机能够处理的数据分片比一台较慢的计算机更多,且成一定的比例。即使使用相同的机器,处理失败的作业或其他同时运行的作业也能够实现负载平衡,并且如果分片被切分得更细,负载平衡的质量会更好。
另一方面,如果分片切分得太小,那么管理分片的总时间和构建map 任务的总时间将决定着作业的整个执行时间。对于大多数作业来说,一个合理的分片大小趋向于HDFS的一个块的大小,默认是64 MB,不过可以针对集群调整这个默认值,在新建所有文件或新建每个文件时具体指定即可。
Hadoop在存储有输入数据(HDFS中的数据)的节点上运行map任务,可以获得最佳性能。这就是所谓的数据本地化优化(data locality optimization)。现在我们应该清楚为什么最佳分片的大小应该与块大小相同:因为它是确保可以存储在单个节点上的最大输入块的大小。如果分片跨越两个数据块,那么对于任何一个HDFS 节点,基本上都不可能同时存储这两个数据块,因此分片中的部分数据需要通过网络传输到map任务节点。与使用本地数据运行整个map任务相比,这种方法显然效率更低。
map任务将其输出写入本地硬盘,而非HDFS。这是为什么?因为map的输出是中间结果:该中间结果由reduce任务处理后才产生最终输出结果,而且一旦作业完成,map的输出结果可以被删除。因此,如果把它存储在HDFS中并实现备份,难免有些小题大做。如果该节点上运行的map任务在将map 中间结果传送给reduce 任务之前失败,Hadoop将在另一个节点上重新运行这个map任务以再次构建map中间结果。
reduce任务并不具备数据本地化的优势——单个reduce任务的输入通常来自于所有mapper的输出。在本例中,我们仅有一个reduce 任务,其输入是所有map任务的输出。因此,排过序的map输出需通过网络传输发送到运行reduce 任务的节点。数据在reduce端合并,然后由用户定义的reduce 函数处理。reduce的输出通常存储在HDFS中以实现可靠存储。如第3章所述,对于每个reduce 输出的HDFS块,第一个复本存储在本地节点上,其他复本存储在其他机架节点中。因此,reduce的输出写入HDFS确实需要占用网络带宽,但这与正常的HDFS流水线写入的消耗一样。
一个reduce任务的完整数据流如图2-2所示。虚线框表示节点,虚线箭头表示节点内部的数据传输,而实线箭头表示节点之间的数据传输。
reduce任务的数量并非由输入数据的大小决定,而是特别指定的。第191页的“默认的MapReduce作业”小节将介绍如何为指定的作业选择reduce任务的数量。
如有多个reduce任务,则每个map任务都会对其输出进行分区(partition),即为每个reduce任务建一个分区。每个分区有许多键(及其对应值),但每个键对应的键/值对记录都在同一分区中。分区由用户定义的分区函数控制,但通常用默认的分区器(partitioner,文中有时也称“分区函数”)通过哈希函数来分区,这种方法很高效。
一般情况下,多个reduce任务的数据流如图2-3所示。该图清楚地表明了为什么map任务和reduce任务之间的数据流称为shuffle(混洗),因为每个reduce 任务的输入都来自许多map任务。混洗一般比此图所示的更复杂,并且调整混洗参数对作业总执行时间会有非常大的影响,详情参见第177页的“混洗和排序”小节。
![横向扩展](https://i-blog.csdnimg.cn/blog_migrate/2556c09fcef9189ade127a76adaeae77.jpeg)
最后,也有可能没有任何reduce任务。当数据处理可以完全并行时,即无需混洗,可能会出现无reduce任务的情况(示例参见第211页的“NLineInputFormat”小节)。在这种情况下,唯一的非本地节点数据传输是map任务将结果写入HDFS(参见图2-4)。
combiner
集群上的可用带宽限制了MapReduce作业的数量,因此最重要的一点是尽量避免map任务和reduce任务之间的数据传输。Hadoop允许用户针对map任务的输出指定一个合并函数(文中有时也称作combiner,就像mapper和reducer一样——译者注)——合并函数的输出作为reduce函数的输入。由于合并函数是一个优化方案,所以Hadoop无法确定针对map任务输出中任一条记录需要调用多少次合并函数(如果需要)。换言之,不管调用合并函数多少次,0次、1次或多次,reducer的输出结果都应一致。
![横向扩展](https://i-blog.csdnimg.cn/blog_migrate/a1215cc0b8c76089894979af1029bd00.jpeg)
合并函数的规则限定了可以使用的函数类型。这里最好通过一个例子来说明。依旧假设以前计算最高气温的例子,1950年的读数由两个map任务处理(因为它们在不同的分片中)。假设第一个map 的输出如下:
(1950, 0)
(1950, 20)
(1950, 10)
第二个map的输出如下:
(1950, 25)
(1950, 15)
reduce函数被调用时,输入如下:
(1950, [0, 20, 10, 25, 15])
因为25为该列数据中最大的,所以其输出如下:
(1950, 25)
我们可以像使用reduce函数那样,使用合并函数找出每个map任务输出结果中的最高气温。如此一来,reduce函数调用时将被传入以下数据:
(1950, [20, 25])
reduce输出的结果和以前一样。更简单地说,我们可以通过下面的表达式来说明气温数值上的函数调用:
max(0, 20, 10, 25, 15) = max(max(0, 20, 10), max(25, 15)) = max(20, 25) = 25
并非所有函数都具有该属性。[1]例如,如果我们计算平均气温,便不能用平均数作为combiner,因为
mean(0, 20, 10, 25, 15) = 14
而combiner不能取代reduce函数:
mean(mean(0, 20, 10), mean(25, 15)) = mean(10, 20) = 15
为什么呢?我们仍然需要reduce函数来处理不同map输出中具有相同键的记录。但它能有效减少map和reduce之间的数据传输量,在MapReduce作业中使用combiner,是需要慎重考虑的。
指定一个合并函数
让我们回到Java MapReduce 程序,合并函数是通过reducer接口来定义的,并且该例中,它的实现与MaxTemperatureReducer中的reduce函数相同。唯一需要做的修改是在JobConf中设置combiner类(见例2-7)。
例2-7. 使用合并函数快速找出最高气温
public class MaxTemperatureWithCombiner {
public static void main(String[] args) throws IOException {
if (args.length != 2) {
System.err.println("Usage: MaxTemperatureWithCombiner " +
"");
System.exit(-1);
}
JobConf conf = new JobConf(MaxTemperatureWithCombiner.class);
conf.setJobName("Max temperature");
FileInputFormat.addInputPath(conf, new Path(args[0]));
FileOutputFormat.setOutputPath(conf, new Path(args[1]));
conf.setMapperClass(MaxTemperatureMapper.class);
conf.setCombinerClass(MaxTemperatureReducer.class);
conf.setReducerClass(MaxTemperatureReducer.class);
conf.setOutputKeyClass(Text.class);
conf.setOutputValueClass(IntWritable.class);
JobClient.runJob(conf);
}
}
[1] 在Gray等人1995年发表的论文“Data Cube: A Relational Aggregation Operator Generalizing Group-By, Cross-Tab, and Sub-Totals”中,具有该属性的函数被称为是“分布式的”。