为了利用HADOOP提供的并行处理,我们需要把我们的查询表达成一个
MapReduce。在小规模的本地化测试后,我们可以在一个集群上运行它。
Map and Reduce
MapReduce把处理分成两个阶段:map阶段和reduce阶段。每一阶段都
有一个键值对作为输入和输出,键值的类型可以由程序员选择。程序员同时
指定两个函数:map函数和reduce函数。map阶段的输入是NCDC的原始
数据。我们选择文本输入格式,它把数据集中的每一行作为一个文本值给我
们。键是这行的起始位置相对于文件开头的偏移。在这个例子中,map函数
只是一个数据处理阶段,把数据设置成这个格式供reduce函数工作在上面:
找到每一年的最高温度。map函数也是删除错误记录的好地方:在这里我们把
缺失,可疑,或错误的数据过滤掉。
为了可视化map的工作,考虑下面输入数据的示例行(一些没用的列被删除
以适合页面大小,用省略号指出):
0067011990999991950051507004...9999999N9+00001+99999999999...
0043011990999991950051512004...9999999N9+00221+99999999999...
0043011990999991950051518004...9999999N9-00111+99999999999...
0043012650999991949032412004...0500001N9+01111+99999999999...
0043012650999991949032418004...0500001N9+00781+99999999999...
这些行作为键值对提供给map函数:
(0, 006701199099999<strong>1950</strong>051507004...9999999N9+<strong>00001</strong>+99999999999...)
(106, 004301199099999<strong>1950</strong>051512004...9999999N9+<strong>00221</strong>+99999999999...)
(212, 004301199099999<strong>1950</strong>051518004...9999999N9-<strong>00111</strong>+99999999999...)
(318, 004301265099999<strong>1949</strong>032412004...0500001N9+<strong>01111</strong>+99999999999...)
(424, 004301265099999<strong>1949</strong>032418004...0500001N9+<strong>00781</strong>+99999999999...)
键是文件中行的偏移,我们的map函数忽略它。map函数仅仅提取年分和气温(
粗体表示的内容),并把它做为输出(温度值被解读为整型):
(1950, 0)
(1950, 22)
(1950, −11)
(1949, 111)
(1949, 78)
map函数的输出在被送往reduce函数之前被MapReduce框架处理。这个处理包括
把键值对排序、分组。所以,继续我们的例子,我们reduce函数看到如下的输入:
(1949, [111, 78])
(1950, [0, 22, −11])
每一年与对应的气温计数一起。reduce函数目前要做的事情就是遍历这个列表并找到
最大的读数:
(1949, 111)
(1950, 22)
这是最终的输出:每一年全球的最高温度。
Figure2-1阐明了整个数据流程。在图表的底部是一个Unix管道,它模仿整个MapReduce
流,再接下来的HADOOP Straming一章中我们还会再次看到。
JAVA MapReduce
已经贯穿MapReduce程序的工作流程,下一步是如何通过代码来表达。我们需要三个东西:
map函数,reduce函数,以及一些代码来运行这个工作。map函数由Mapper类表现,它声明了
一个抽象的map()方法。例2-3是我们map函数的实现.例2-3:
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
public class MaxTemperatureMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private static final int MISSING = 9999;
@Override
public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String line = value.toString();
String year = line.substring(15, 19);
int airTemperature;
if (line.charAt(87) == '+') { // parseInt doesn't like leading plus signs
airTemperature = Integer.parseInt(line.substring(88, 92));
} else {
airTemperature = Integer.parseInt(line.substring(87, 92));
}
String quality = line.substring(92, 93);
if (airTemperature != MISSING && quality.matches("[01459]")) {
context.write(new Text(year), new IntWritable(airTemperature));
}
}
}
Mapper是一个泛型类,有四个形参分别表示map函数的输入键,输入值,输出键,输出值的类型。目前
的例子中,输入键类型是一个长整型的偏移量,输入值是一行的文本,输出键是一个年份,输出值是气温(
一个整型。不是使用java的内置类型,而是使用HADOOP提供的它自己的基本类型,它们优化了网络序列化。
可以在org.apache.hadoop.io包中找到。这里我们使用LongWritable,它对应java中的Long,Text(像java的String)
以及IntWritable(像java的Integer)。
传入map()方法一个键和一个值。我们把Text的值转成JAVA的String,然后使用它的substring()方法来提取
我们感兴趣的内容。
map()方法也提供了一个Context实例来写输出。既然如此,我们把年份做为一个Text对象,温度通过IntWritable
包装。我们仅仅在提供了温度并且质量码表明这个温度读取是正确的时候才写出一条记录。
reduce函数使用Reducer定义,如例2-4所示:
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
public class MaxTemperatureReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
@Override
public void reduce(Text key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
int maxValue = Integer.MIN_VALUE;
for (IntWritable value : values) {
maxValue = Math.max(maxValue, value.get());
}
context.write(key, new IntWritable(maxValue));
}
}
同样,四个形参用来指定输入输出类型,这次是为了reduce函数。reduce函数的图稿类型必须和map
函数的输出类型匹配:Text和IntWritable。在这种情况下,reduce函数的输出类型是Text和IntWritable,即
年份和它的最高温度,我们通过迭代所有的温度并把它与目前最高值做比较来得到这个最高温度。
第三段代码运行MapReduce工作,例2-5
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class MaxTemperature {
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println("Usage: MaxTemperature <input path> <output path>");
System.exit(-1);
}
Job job = new Job();
job.setJarByClass(MaxTemperature.class);
job.setJobName("Max temperature");
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
job.setMapperClass(MaxTemperatureMapper.class);
job.setReducerClass(MaxTemperatureReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
Job对象形成了job的规范,并且让你控制job如何运行。当我们在HADOOP集群上运行这个job时,
我们会把这些代码打包成一个JAR文件(HADOOP会把它分发到集群上)。不是显示定义JAR文件的
名称,而是用Job的setJarByClass()方法传入一个类,HADOOP会使用它来找到包含这个类的JAR文件
来定位。
构造好了一个Job对象,我们定义输入输出路径。输入路径通过调用FileInputFormat的addInputPath()
静态方法来指定,它可以是一个文件,目录(在这种情况下,输入由目录中的所有文件组成),或是一
个文件模式。顾名思义,addInputPath()方法可以被调用多次来使用多个路径的输入。
输出路径(只有一个)是通过调用FileOutputForamt的静态方法setOutputPath来定义的。它指定了
reduce的输出文件路径。在job运行之前这个路径不能存在,因为HADOOP会抱怨并不会运行这个job。
这个机制是为了防止数据丢失(一个job突然重写了另一个job的输出是很烦的)。
下一步,我们指定map和reduce的类型,通过setMapperClass()和setReducerClass()方法。
setOutputKeyClass()和setOutputValueClass()方法控制了reduce函数的输出类型,并且必须和reduce
产生的匹配。map的输出类型默认是相同的,所以如果mapper产生和reducer相同的类型则不需要设置(
我们的例子中是这样的)。尽管如此,如果它们不同,map的输出类型必须使用setMapOutputKeyClass()
和setMapOutputValueClass()方法来设置。
输入类型由输入格式控制,我们并没有显示设置它,因为我们使用默认的TextInputFormat。
设置完map函数和reduce函数之后,我们准备运行job。Job的waitForCompletion()方法提交这个job并
等待它完成。这个方法的唯一参数是一个标志,表明是否产生明细信息。当是true时,job把它的处理信息
写到控制台上。
waitForCompletion()方法的返回值是一个布尔类型,表示成功(true)或失败(false),我们把它转成
程序退出码0或1。
测试
写完MapReduce job之后,接下来就在一个小的数据集上运行测试并找到代码问题。首先,安装HADOOP
,采用单独模式(附录A指导如何做)。这种模式下HADOOP使用本地文件系统运行本地的job。然后,使用本书
的网站的指导来安装编译例子。
让我们开始测试它:
<strong>% export HADOOP_CLASSPATH=hadoop-examples.jar
% hadoop MaxTemperature input/ncdc/sample.txt output</strong>
14/09/16 09:48:39 WARN util.NativeCodeLoader: Unable to load native-hadoop
library for your platform... using builtin-java classes where applicable
14/09/16 09:48:40 WARN mapreduce.JobSubmitter: Hadoop command-line option
parsing not performed. Implement the Tool interface and execute your application
with ToolRunner to remedy this.
14/09/16 09:48:40 INFO input.FileInputFormat: Total input paths to process : 1
14/09/16 09:48:40 INFO mapreduce.JobSubmitter: number of splits:1
14/09/16 09:48:40 INFO mapreduce.JobSubmitter: Submitting tokens for job:
job_local26392882_0001
14/09/16 09:48:40 INFO mapreduce.Job: The url to track the job:
http://localhost:8080/
14/09/16 09:48:40 INFO mapreduce.Job: Running job: job_local26392882_0001
14/09/16 09:48:40 INFO mapred.LocalJobRunner: OutputCommitter set in config null
14/09/16 09:48:40 INFO mapred.LocalJobRunner: OutputCommitter is
org.apache.hadoop.mapreduce.lib.output.FileOutputCommitter
14/09/16 09:48:40 INFO mapred.LocalJobRunner: Waiting for map tasks
14/09/16 09:48:40 INFO mapred.LocalJobRunner: Starting task:
attempt_local26392882_0001_m_000000_0
14/09/16 09:48:40 INFO mapred.Task: Using ResourceCalculatorProcessTree : null
14/09/16 09:48:40 INFO mapred.LocalJobRunner:
14/09/16 09:48:40 INFO mapred.Task: Task:attempt_local26392882_0001_m_000000_0
is done. And is in the process of committing
14/09/16 09:48:40 INFO mapred.LocalJobRunner: map
14/09/16 09:48:40 INFO mapred.Task: Task 'attempt_local26392882_0001_m_000000_0'
done.
14/09/16 09:48:40 INFO mapred.LocalJobRunner: Finishing task:
attempt_local26392882_0001_m_000000_0
14/09/16 09:48:40 INFO mapred.LocalJobRunner: map task executor complete.
14/09/16 09:48:40 INFO mapred.LocalJobRunner: Waiting for reduce tasks
14/09/16 09:48:40 INFO mapred.LocalJobRunner: Starting task:
attempt_local26392882_0001_r_000000_0
14/09/16 09:48:40 INFO mapred.Task: Using ResourceCalculatorProcessTree : null
14/09/16 09:48:40 INFO mapred.LocalJobRunner: 1 / 1 copied.
14/09/16 09:48:40 INFO mapred.Merger: Merging 1 sorted segments
14/09/16 09:48:40 INFO mapred.Merger: Down to the last merge-pass, with 1
segments left of total size: 50 bytes
14/09/16 09:48:40 INFO mapred.Merger: Merging 1 sorted segments
14/09/16 09:48:40 INFO mapred.Merger: Down to the last merge-pass, with 1
segments left of total size: 50 bytes
14/09/16 09:48:40 INFO mapred.LocalJobRunner: 1 / 1 copied.
14/09/16 09:48:40 INFO mapred.Task: Task:attempt_local26392882_0001_r_000000_0
is done. And is in the process of committing
14/09/16 09:48:40 INFO mapred.LocalJobRunner: 1 / 1 copied.
14/09/16 09:48:40 INFO mapred.Task: Task attempt_local26392882_0001_r_000000_0
is allowed to commit now
14/09/16 09:48:40 INFO output.FileOutputCommitter: Saved output of task
'attempt...local26392882_0001_r_000000_0' to file:/Users/tom/book-workspace/
hadoop-book/output/_temporary/0/task_local26392882_0001_r_000000
14/09/16 09:48:40 INFO mapred.LocalJobRunner: reduce > reduce
14/09/16 09:48:40 INFO mapred.Task: Task 'attempt_local26392882_0001_r_000000_0'
done.
14/09/16 09:48:40 INFO mapred.LocalJobRunner: Finishing task:
attempt_local26392882_0001_r_000000_0
14/09/16 09:48:40 INFO mapred.LocalJobRunner: reduce task executor complete.
14/09/16 09:48:41 INFO mapreduce.Job: Job job_local26392882_0001 running in uber
mode : false
14/09/16 09:48:41 INFO mapreduce.Job: map 100% reduce 100%
14/09/16 09:48:41 INFO mapreduce.Job: Job job_local26392882_0001 completed
successfully
14/09/16 09:48:41 INFO mapreduce.Job: Counters: 30
File System Counters
FILE: Number of bytes read=377168
FILE: Number of bytes written=828464
FILE: Number of read operations=0
FILE: Number of large read operations=0
FILE: Number of write operations=0
Map-Reduce Framework
Map input records=5
Map output records=5
Map output bytes=45
Map output materialized bytes=61
Input split bytes=129
Combine input records=0
Combine output records=0
Reduce input groups=2
Reduce shuffle bytes=61
Reduce input records=5
Reduce output records=2
Spilled Records=10
Shuffled Maps =1
Failed Shuffles=0
Merged Map outputs=1
GC time elapsed (ms)=39
Total committed heap usage (bytes)=226754560
File Input Format Counters
Bytes Read=529
File Output Format Counters
Bytes Written=29
当hadoop命令被调用,并提供一个类名做为第一个参数,它开启一个JVM来
运行这个类。hadoop命令把HADOOP库(以及它的依赖)添加到classpath,
并使用HADOOP的配置。为了把应用的类添加到classpath,我们已经定义一个
环境变量名为HADOOP_CLASSPATH,hadoop脚本会使用它。
运行的job的输出提供了一个有用的信息。例如,我们可以看到这个job有一
个ID,值为job_local26392882_0001,它运行一个map任务和一个reduce任务(
IDS:attempt_local26392882_0001_m_000000_0 和attempt_local26392882_0001_r_000000_0)。
知道job和任务的ID在调试MapReduce job时是非常有用的。
输出的最后一部分,标题为“Counters",显示HADOOP运行每一个job的统计信息。
它对于确定数据处理总数是不是你的预期非常有用。例如,我们可以跟踪走过系统的
记录数量:5个map输入记录产生5个map输出记录(因为mapper为每一个输入记录
发出一个输出记录),然后,5个reduce输入记录分成两组(每一个键一组)产生两
个reduce输出记录。
输出被写到输出目录,每一个reducer包含一个输出文件。这个job只有一个reducer,所以
我们有一个单独的文件,名叫part-r-00000:
% cat output/part-r-00000
1949 111
1950 22
这个结果和我们早先手工处理的结果一致。也就是说,最高温度记录,1949年是11.1°C,
1950年是2.2°C。