之前我们学习了MapReduce的执行过程,下面我们看一下MapReduce执行过程中输入和输出所涉及到的数据结构。
[b]输入格式:[/b]
通过之前的学习,我们知道在执行mapreduce之前,原始数据被分割成若干split,每个split作为一个map任务的输入,在map执行过程中split会被分解成一个个记录(key-value对),map会依次处理每一个记录。
split在hadoop中用接口InputSplit来表示,每个InputSplit的实例表示一个split。InputSplit有两个成员变量,分别表示这个split的长度和存储这个split的节点位置。split的处理方式是按贪心算法实现的,长度大小用来将split进行排序,使最大的split优先被处理;MapReduce利用存储位置成员变量来保证执行map任务的节点与split距离最近。
InputFormat负创建splits并且将split分解为各个记录。InputFormat接口中包含两个方法:JobClient调用getSplits来计算splits,计算splits后,客户端将他们发送给jobtracker,jobtracker利用splits中的存储节点位置信息来调度map任务。在tasktracker执行map任务过程中,会调用getRecordReader成员函数来获得记录,并对每个记录调用map方法。
[b]FileInputFormat:[/b]
FileInputFormat是所有以文件作为数据源的InputFormat实现的基类,FileInputFormat保存作为job输入的所有文件,并实现了对输入文件计算splits的方法。至于获得记录的方法是有不同的子类进行实现的。
FileInputFormat提供了一套addInputPath方法来设置job的输入路径,可以通过这些方法来构造一个路径的列表,具体的可参见api文档。
获得了输入文件后,FileInputFormat是怎样将他们划分成splits的呢?FileInputFormat只划分比HDFS block大的文件,所以FileInputFormat划分的结果是这个文件或者是这个文件中的一部分,如果一个文件的大小比block小,将不会被划分,这也是Hadoop处理大文件的效率要比处理很多小文件的效率高的原因。
当Hadoop处理很多小文件(文件大小小于hdfs block大小)的时候,由于FileInputFormat不会对小文件进行划分,所以每一个小文件都会被当做一个split并分配一个map任务,导致效率底下。比如一个1G的文件,会被划分成16个64MB的split,并分配16个map任务处理,而10000个100kb的文件会被10000个map任务处理。
有些时候我们需要将整个文件的内容作为map的输入,这时可以实现一个FileInputFormat,复写isSplittable函数禁止对输入数据进行分片。输入文件没有被分片,我们必须将整个文件的内容作为一个记录,这是我们需要实现一个RecodeReader来处理数据。
是不是有点晕的,还是看实际程序吧,这个例子是将6个小文件利用mapreduce的方法合并成一个大文件,并将这个文件以sequencefile的形式在HDFS中保存起来。代码如下:
WholeFileInputFormat类实现FileInputFormat,关闭分片功能,并返回新定义的WholeFileRecordReader.
package hadoop.chapter7;
import java.io.IOException;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapred.FileInputFormat;
import org.apache.hadoop.mapred.FileSplit;
import org.apache.hadoop.mapred.InputSplit;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapred.RecordReader;
import org.apache.hadoop.mapred.Reporter;
public class WholeFileInputFormat
extends FileInputFormat {
@Override
public RecordReader getRecordReader(
InputSplit split, JobConf job, Reporter reporter)
throws IOException {
// 返回自定义RecordReader
return new WholeFileRecordReader((FileSplit) split, job);
}
@Override
protected boolean isSplitable(FileSystem fs, Path filename) {
// 输入文件不分片
return false;
}
}
WholeFileRecordReader的实现:
package hadoop.chapter7;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapred.FileSplit;
import org.apache.hadoop.mapred.RecordReader;
public class WholeFileRecordReader
implements RecordReader {
private FileSplit fileSplit;
private Configuration conf;
private boolean processed = false;
public WholeFileRecordReader(FileSplit fileSplit, Configuration conf) {
this.fileSplit = fileSplit;
this.conf = conf;
}
@Override
public void close() throws IOException {
}
@Override
public NullWritable createKey() {
return NullWritable.get();
}
@Override
public BytesWritable createValue() {
return new BytesWritable();
}
@Override
public long getPos() throws IOException {
return processed ? fileSplit.getLength() : 0;
}
@Override
public float getProgress() throws IOException {
return processed ? 1.0f : 0.0f;
}
@Override
public boolean next(NullWritable key, BytesWritable value)
throws IOException {
if (!processed) {
byte[] content = new byte[(int) fileSplit.getLength()];
Path file = fileSplit.getPath();
FileSystem fs = file.getFileSystem(conf);
FSDataInputStream in = null;
try {
in = fs.open(file);
IOUtils.readFully(in, content, 0, content.length);
value.set(content, 0, content.length);
} finally {
IOUtils.closeStream(in);
}
processed = true;
return true;
}
return false;
}
}
WholeFileRecordReader中最重要的next函数将整个文件的内容读入设置到value中,这样map函数中获得的value就是整个文件的内容。
package hadoop.chapter7;
import java.io.IOException;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapred.FileInputFormat;
import org.apache.hadoop.mapred.FileOutputFormat;
import org.apache.hadoop.mapred.JobClient;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapred.MapReduceBase;
import org.apache.hadoop.mapred.Mapper;
import org.apache.hadoop.mapred.OutputCollector;
import org.apache.hadoop.mapred.Reporter;
import org.apache.hadoop.mapred.SequenceFileOutputFormat;
import org.apache.hadoop.mapred.lib.IdentityReducer;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
public class SmallFilesToSequenceFileConverter extends Configured
implements Tool {
static class SequenceMapper extends MapReduceBase
implements Mapper {
private JobConf conf;
@Override
public void configure(JobConf job) {
this.conf = job;
}
@Override
public void map(NullWritable key, BytesWritable value,
OutputCollector output, Reporter reporter)
throws IOException {
String filename = conf.get("map.input.file");
output.collect(new Text(filename), value);
}
}
@Override
public int run(String[] args) throws Exception {
JobConf conf = new JobConf(getConf(), getClass());
conf.setJobName("SmallFilesToSequenceFileConverter");
FileInputFormat.addInputPath(conf, new Path(args[0]));
FileOutputFormat.setOutputPath(conf, new Path(args[1]));
conf.setInputFormat(WholeFileInputFormat.class);
conf.setOutputFormat(SequenceFileOutputFormat.class);
conf.setMapperClass(SequenceMapper.class);
conf.setReducerClass(IdentityReducer.class);
conf.setOutputKeyClass(Text.class);
conf.setOutputValueClass(BytesWritable.class);
conf.set("mapred.reduce.tasks", "2");
JobClient.runJob(conf);
return 0;
}
public static void main(String[] args) {
try {
ToolRunner.run(new SmallFilesToSequenceFileConverter(), args);
} catch (Exception e) {
e.printStackTrace();
}
}
}
SmallFilesToSequenceFileConverter类中定义了mapper函数,并在run函数中我们用conf.set("mapred.reduce.tasks", "2");来设置reducer的个数为2,这样会用两个reducer来处理map的结果,会产生两个文件。
准备程序运行需要的数据,简历6个txt文件,内容分别是aaaa-zzzzz,然后上传到HDFS中:
[img]http://dl.iteye.com/upload/attachment/369158/34622253-bae0-3776-aa29-5a8533975464.png[/img]
运行程序后得到结果:
[img]http://dl.iteye.com/upload/attachment/369160/da659e50-6c4d-3b99-a929-0a852de673f7.png[/img]
由于我们设置了reduce结果是sequencefile格式,所以在控制台用hadoop fs -text查看结果:
[img]http://dl.iteye.com/upload/attachment/369162/ceb036de-546c-3c85-a242-1df2dba0d256.png[/img]
因为设置了reducer的数目为2,所以会产生两个文件,并且map结果是按key分组和排序的,所以我们可以得到上述结果。
FileInputFormat分析完毕,下面我们看一下Hadoop的各种输入格式:
1. TextInputformat
TextInputformat是默认的inputformat,对于输入文件,文件中每一行作为一个记录,他将每一行在文件中的起始偏移量作为key,每一行的内容作为value。为什么不用行数作为key呢?
注意split中只是保存了split在文件中的位置,map操作读取数据的时候还是会到HDFS中打开原始文件,然后定位到split所需的数据的位置。
所以在处理split的时候,打开文件后定位到split所在的位置,但是我们不知道这个split的第一个记录处在文件的第几行,我们也无法知道上一个split中存了多少行数据,所以不能确定这个记录是文件中的第几行。
然而我们会定位到split时我们可以获得第一个记录的偏移量,所以TextInputformat是以行的偏移量作为key。
2. key-value TextInputformat
当输入数据的每一行是两列,并用tab分离的形式的时候,key-value TextInputformat处理这种格式的文件非常适合。
3.NLineInputformat
NLineInputformat可以控制在每个split中数据的行数。
4.SequenceFileInputformat
当输入文件格式是sequencefile的时候,要使用SequenceFileInputformat。由于sequencefile都是以key和value的二进制形式存放的(注意hadoop类型的二进制的解释方式和原始二进制不一样,会多一些维护信息),所以这种情况下map的key和value的类型是由sequencefile决定的,所以必须保证map的输入类型与sequencefile一致。比如sequencefile中的内容key的类型是intwritable,value的类型是Text,那么在map的类型也必须是这两个。
5.sequencefileAsTextInputFormat
sequencefileAsTextInputFormat将sequencefile的key和value都转化成Text对象传入map中。
6.sequencefileAsBinaryInputFormat
他将sequencefile中的key和value都以原始二进制的形式封装在byteswritable对象中传给map,如何对二进制数据进行解释是map函数编写者的工作。
7.复合输入
MapReduce的输入格式可能不只一种,这时可以使用MutipleInputs来多次添加路径并制定输入格式。
[b]输出格式:[/b]
1.TextOutputformat
默认的输出格式,key和value中间值用tab隔开的。
2.SequenceFileOutputformat
将key和value以sequencefile格式输出。
3.sequencefileAsOutputFormat
将key和value以原始二进制的格式输出。
4.MapFileOutputFormat
将key和value写入MapFile中。由于MapFile中的key是有序的,所以写入的时候必须保证记录是按key值顺序写入的。
5.MultipleOutputFormat
默认情况下一个reducer会产生一个输出,但是有些时候我们想一个reducer产生多个输出,MultipleOutputFormat和MultipleOutputs可以实现这个功能。
MultipleOutputFormat允许reducer将输出写入多个文件,写入那个文件是由key和value决定的。MultipleOutputFormat提供了一些方法可以控制reducer输出目标文件的名字。MultipleOutputFormat是MultipleTextOutputFormat和MultipleSequenceOutputFormat两个类的父类。
例如可以写一个类,继承自MultipleTextOutputFormat并复写他的generateFileNameForKeyValue方法控制reduce输出目标的文件名。程序运行的时候不同的reducer输出结果的时候调用这个方法,这个方法根据要输出记录的信息产生文件名,reducer将数据写入相应文件中,这样就能是一个reducer向多个文件写入。利用这个特性我们就可以方便的进行结果的聚类了。
我们看下面例子:
package hadoop.chapter7_1;
import java.io.IOException;
import java.util.Iterator;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapred.FileInputFormat;
import org.apache.hadoop.mapred.FileOutputFormat;
import org.apache.hadoop.mapred.JobClient;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapred.MapReduceBase;
import org.apache.hadoop.mapred.Mapper;
import org.apache.hadoop.mapred.OutputCollector;
import org.apache.hadoop.mapred.Reducer;
import org.apache.hadoop.mapred.Reporter;
import org.apache.hadoop.mapred.SkipBadRecords;
import org.apache.hadoop.mapred.lib.MultipleTextOutputFormat;
public class PartitionByYear {
private static class MaxTemperatureMapper extends MapReduceBase
implements Mapper{
@Override
public void map(LongWritable key, Text value,
OutputCollector output, Reporter reporter)
throws IOException {
String line = value.toString();
String year = line.substring(14, 18);
output.collect(new Text(year), value);
}
}
private static class MaxTemperatureReducer extends MapReduceBase
implements Reducer{
@Override
public void reduce(Text key, Iterator values,
OutputCollector output, Reporter reporter)
throws IOException {
while (values.hasNext()) {
output.collect(key, values.next());
}
}
}
private static class StationYearMutipleTextOutputFormat extends
MultipleTextOutputFormat {
@Override
protected String generateFileNameForKeyValue(Text key, Text value,
String name) {
return key.toString();
}
}
public static void main(String[] args) throws IOException {
if (args.length != 2) {
System.err.println("");
System.exit(-1);
}
JobConf conf = new JobConf(PartitionByYear.class);
FileInputFormat.addInputPath(conf, new Path(args[0]));
FileOutputFormat.setOutputPath(conf, new Path(args[1]));
conf.setJobName("PartitionByYear");
conf.setOutputKeyClass(Text.class);
conf.setOutputValueClass(Text.class);
conf.setMapperClass(MaxTemperatureMapper.class);
conf.setReducerClass(MaxTemperatureReducer.class);
conf.setOutputFormat(StationYearMutipleTextOutputFormat.class);
JobClient.runJob(conf);
}
}
在这个程序中StationYearMutipleTextOutputFormat类复写了generateFileNameForKeyValue,使Reducer输出的时候会按key的值来选择文件。
我们用前面的气象站的数据进行测试,拷贝两年的数据到一个文件中,上传到HDFS中并命名为2.txt,从下图中可以看出这个文件包含两年的数据。
[img]http://dl.iteye.com/upload/attachment/369270/a0bdfd1b-f9f1-3409-9336-fda1d475c371.png[/img]
运行程序后会在output1目录中得到两个结果:
[img]http://dl.iteye.com/upload/attachment/369273/98f1dddb-059a-33d2-a8c9-d1702fc9ec42.png[/img]
可以看到reduce的结果输出是按照年份来分别输出到不同的文件中的。
6. MultipleOutputs
MultipleOutputFormat实际上是在reduce输出结果的时候指定文件名来实现多文件输出,而MultipleOutputs是在job指定的output基础上,新增额外的输出,他才是真正意义上的多文件输出。
MultipleOutputs分为两种情况:
·附加单个文件输出。有些时候我们在原有的结果输出基础上,还想输出一些其他的信息,这是就可以调用MultipleOutputs的addNamedOutput方法,并且outputformat格式及keyvalue类型都可以自定义的。
下面我们看一个例子,一个简单的投票统计程序。程序的输入是:
people1vote2
people2vote1
people3vote2
people4vote3
people5vote2
people6vote1
people7vote3
people8vote2
people9vote2
表示9个人分别为三个候选人投票的情况。保存文件为vote.txt并上传到HDFS中。
package hadoop.chapter7_1;
import java.io.IOException;
import java.util.Iterator;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapred.FileInputFormat;
import org.apache.hadoop.mapred.FileOutputFormat;
import org.apache.hadoop.mapred.JobClient;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapred.MapReduceBase;
import org.apache.hadoop.mapred.Mapper;
import org.apache.hadoop.mapred.OutputCollector;
import org.apache.hadoop.mapred.Reducer;
import org.apache.hadoop.mapred.Reporter;
import org.apache.hadoop.mapred.SequenceFileOutputFormat;
import org.apache.hadoop.mapred.TextOutputFormat;
import org.apache.hadoop.mapred.lib.IdentityReducer;
import org.apache.hadoop.mapred.lib.MultipleOutputs;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
public class PartitionByMultipleOutput extends Configured implements Tool {
private static class VoteMapper extends MapReduceBase implements
Mapper {
@Override
public void map(LongWritable key, Text value,
OutputCollector output, Reporter reporter)
throws IOException {
String line = value.toString();
String vote = line.substring(7);
String people = line.substring(0, 7);
output.collect(new Text(vote), new Text(people));
}
}
private static class VoteReducer extends MapReduceBase implements
Reducer {
private MultipleOutputs multipleOutputs;
@Override
public void configure(JobConf job) {
this.multipleOutputs = new MultipleOutputs(job);
}
@Override
public void close() throws IOException {
this.multipleOutputs.close();
}
@Override
public void reduce(Text key, Iterator values,
OutputCollector output, Reporter reporter)
throws IOException {
// 获取在配置job是注册的名字为vote的collector
OutputCollector collector = multipleOutputs.getCollector("vote",
reporter);
while (values.hasNext()) {
Text value = values.next();
output.collect(key, value);
collector.collect(key, value);
}
}
}
@Override
public int run(String[] args) throws Exception {
JobConf conf = new JobConf(getConf(), getClass());
conf.setJobName("vote");
FileInputFormat.addInputPath(conf, new Path(args[0]));
FileOutputFormat.setOutputPath(conf, new Path(args[1]));
conf.setOutputFormat(TextOutputFormat.class);
conf.setMapperClass(VoteMapper.class);
conf.setReducerClass(VoteReducer.class);
conf.setOutputKeyClass(Text.class);
conf.setOutputValueClass(Text.class);
// 附加文件输出,可以多次调用这个函数来附加多个文件
MultipleOutputs.addNamedOutput(conf, "vote", TextOutputFormat.class, Text.class, Text.class);
JobClient.runJob(conf);
return 0;
}
public static void main(String[] args) {
if (args.length != 2) {
System.err.println("参数错误");
System.exit(-1);
}
try {
ToolRunner.run(new PartitionByMultipleOutput(), args);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行程序,得到两个输出文件,都是投票的统计结果。
[img]http://dl.iteye.com/upload/attachment/369571/c22a66e6-4044-328c-b0f5-b7dd0cfffa6c.png[/img]
·附加多个文件:
调用MultipleOutputs的addMultiNameOutput方法可以附加多个文件,利用这个方法可以实现多文件分类输出。
在配置job的时候用addMultiNameOutput注册一个namedoutput,在reducer中用multipleOutputs.getCollector获取这个namedoutput的时候,第二个参数指明了按照什么进行分割附加输出,我们看下面的例子:
package hadoop.chapter7_1;
import java.io.IOException;
import java.util.Iterator;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapred.FileInputFormat;
import org.apache.hadoop.mapred.FileOutputFormat;
import org.apache.hadoop.mapred.JobClient;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapred.MapReduceBase;
import org.apache.hadoop.mapred.Mapper;
import org.apache.hadoop.mapred.OutputCollector;
import org.apache.hadoop.mapred.Reducer;
import org.apache.hadoop.mapred.Reporter;
import org.apache.hadoop.mapred.SequenceFileOutputFormat;
import org.apache.hadoop.mapred.TextOutputFormat;
import org.apache.hadoop.mapred.lib.IdentityReducer;
import org.apache.hadoop.mapred.lib.MultipleOutputs;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
public class PartitionByMultipleOutput_new extends Configured implements Tool {
private static class VoteMapper extends MapReduceBase implements
Mapper {
@Override
public void map(LongWritable key, Text value,
OutputCollector output, Reporter reporter)
throws IOException {
String line = value.toString();
String vote = line.substring(7);
String people = line.substring(0, 7);
output.collect(new Text(vote), new Text(people));
}
}
private static class VoteReducer extends MapReduceBase implements
Reducer {
private MultipleOutputs multipleOutputs;
@Override
public void configure(JobConf job) {
this.multipleOutputs = new MultipleOutputs(job);
}
@Override
public void close() throws IOException {
this.multipleOutputs.close();
}
@Override
public void reduce(Text key, Iterator values,
OutputCollector output, Reporter reporter)
throws IOException {
// 获取在配置job是注册的名字为vote的collector,第二个参数指明了按不同的key来分割附加输出
OutputCollector collector = multipleOutputs.getCollector("vote", key.toString(), reporter);
while (values.hasNext()) {
Text value = values.next();
output.collect(key, value);
collector.collect(key, value);
}
}
}
@Override
public int run(String[] args) throws Exception {
JobConf conf = new JobConf(getConf(), getClass());
conf.setJobName("vote");
FileInputFormat.addInputPath(conf, new Path(args[0]));
FileOutputFormat.setOutputPath(conf, new Path(args[1]));
conf.setOutputFormat(TextOutputFormat.class);
conf.setMapperClass(VoteMapper.class);
conf.setReducerClass(VoteReducer.class);
conf.setOutputKeyClass(Text.class);
conf.setOutputValueClass(Text.class);
MultipleOutputs.addMultiNamedOutput(conf, "vote", TextOutputFormat.class, Text.class, Text.class);
JobClient.runJob(conf);
return 0;
}
public static void main(String[] args) {
if (args.length != 2) {
System.err.println("参数错误");
System.exit(-1);
}
try {
ToolRunner.run(new PartitionByMultipleOutput_new(), args);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
我们制定按key对附加输出进行分割,运行程序可以得出结果如下:
[img]http://dl.iteye.com/upload/attachment/369587/db9cc1f9-9f7a-3eef-832c-5f0d23694d7f.png[/img]
在输出目录中得到了原始输出和三个附加输出文件。