7.1MR类型
7.2 输入格式
7.2.1输入分片与记录
InputFormat类的层次结构
- 每一个map操作只处理一个输入分片,并且一个一个地处理每条记录,也就是一个键值对。
- 在数据库中,一个输入分片可以是一个表的若干行,而一条记录就是这若干行中的一行。
public abstract class InputSplit {
public abstract long getLength() throws IOException, InterruptedException;
public abstract String[] getLocations() throws IOException, InterruptedException;
}
- inputsplit有长度和位置(主机名),长度用于将单元排序使得最大的单元能够最先的到处理,以提高效率(贪心近似算法)。
public abstract class InputFormat<K, V> {
public abstract List<InputSplit> getSplits(JobContext context) throws IOException, InterruptedException;
public abstract RecordReader<K, V> createRecordReader(InputSplit split, TaskAttemptContext context)
throws IOException, InterruptedException;
}
-
jobclient调用getsplits,返回分片,客户端将他们发送到jobtracker上,jobtracker会使用位置信息将他们调度到相应的tasktracker上执行。
-
在tasktracker上,map任务会将输入分片触底到inputformat的getRecordReader中获得RecordReader。
-
map任务会使用RecordReader来读取记录并且生成键值对,然后再传递给map函数。代码过程如下。
public void run(Context context) throws IOException, InterruptedException { setup(context); while (context.nextKeyValue()) { map(context.getCurrentKey(), context.getCurrentValue(), context); } cleanup(context); }
- 如果分片比hdfs的块大小大,就会导致有很多map操作不是本地的文件块。
CombineFileInputFormat用于处理MR小文件,FileInputFormat将每个文件分割成1个或者多个单元,而CombineFileInputFormat可以将多个文件打包到一个输入单元中,这样每次map操作就会有更多的数据来处理,而不是一次map处理一个小文件。
而且CombineFileInputFormat会考虑到节点和集群的位置信息以决定哪些文件应该被打包到一个单元,以避免远程传输太多的情况,所以原本MR的效率并不会下降。
- MR处理数据的最佳速度最好与数据在集群中出书的速度相同,而处理小文件将增加作业的seek操作数。
- 在HDFS集群中储存大量小文件将会加重名称节点的负担。
- 可以使用SequenceFile将这些小文件合并成一个或者多个大文件,将文件名作为键值,如果不需要键,可用NullWritable代替。值就可以是文件的内容。
避免输入文件不被分割:
-
增加最小分片大小,将它设置成大于要处理的文件大小,甚至是输入分片大小的上限。
-
继承FileInputFormat实体类,重载isSplitable方法,将它的返回值设置为false。
将整个文件作为一条记录处理
有些情况下,需要 mapper能够访问整个文件中的内容。对此,一部分是不对文件进行分割,另一部分则是需要一个 RecordReader来读取文件内容作为map操作的值,见例72的wholeFileInputFormat。
public class WholeFileInputFormat extends FileInputFormat<NullWritable, BytesWritable> {
@Override
protected boolean isSplitable(JobContext context, Path file) {
return false;
}
@Override
public RecordReader<NullWritable, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context)
throws IOException, InterruptedException {
WholeFileRecordReader reader = new WholeFileRecordReader();
reader.initialize(split, context);
return reader;
}
}
以上代码其中重载了isSplitable,估计在getSplits前会调用isSplitable,来评判是否要分片,false的话就只返回一个分片。
class WholeFileRecordReader extends RecordReader<NullWritable, BytesWritable> {
private FileSplit fileSplit;
private Configuration conf;
private BytesWritable value = new BytesWritable();
private boolean processed = false;
@Override
public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
this.fileSplit = (FileSplit) split;
this.conf = context.getConfiguration();
}
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
if (!processed) {
byte[] contents = 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, contents, 0, contents.length);
value.set(contents, 0, contents.length);
} finally {
IOUtils.closeStream(in);
}
processed = true;
return true;
}
return false;
}
@Override
public NullWritable getCurrentKey() throws IOException, InterruptedException {
return NullWritable.get();
}
@Override
public BytesWritable getCurrentValue() throws IOException, InterruptedException {
return value;
}
@Override
public float getProgress() throws IOException {
return processed ? 1.0f : 0.0f;
}
@Override
public void close() throws IOException {
// do nothing
}
}
上代码中新增了process来判断是否被处理过。
public class SmallFilesToSequenceFileConverter extends Configured implements Tool {
static class SequenceFileMapper extends Mapper<NullWritable, BytesWritable, Text, BytesWritable> {
private Text filenameKey;
@Override
protected void setup(Context context) throws IOException, InterruptedException {
InputSplit split = context.getInputSplit();
Path path = ((FileSplit) split).getPath();
filenameKey = new Text(path.toString());
}
@Override
protected void map(NullWritable key, BytesWritable value, Context context)
throws IOException, InterruptedException {
context.write(filenameKey, value);
}
}
@Override
public int run(String[] args) throws Exception {
Job job = JobBuilder.parseInputAndOutput(this, getConf(), args);
if (job == null) {
return -1;
}
job.setInputFormatClass(WholeFileInputFormat.class);
job.setOutputFormatClass(SequenceFileOutputFormat.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(BytesWritable.class);
job.setMapperClass(SequenceFileMapper.class);
return job.waitForCompletion(true) ? 0 : 1;
}
public static void main(String[] args) throws Exception {
int exitCode = ToolRunner.run(new SmallFilesToSequenceFileConverter(), args);
System.exit(exitCode);
}
}
该MR将小文件打包成SequenceFile,其中,键是文件名,值是文件内容。
usage
% hadoop jar hadoop-examples.jar SmallFilesToSequenceFileConverter \
-conf conf/hadoop-localhost.xml -D mapreduce.job.reduces=2 \
input/smallfiles output
使用了两个reduce,所以输出两个结果文件
% hadoop fs -conf conf/hadoop-localhost.xml -text output/part-r-00000
hdfs://localhost/user/tom/input/smallfiles/a 61 61 61 61 61 61 61 61 61 61
hdfs://localhost/user/tom/input/smallfiles/c 63 63 63 63 63 63 63 63 63 63
hdfs://localhost/user/tom/input/smallfiles/e
% hadoop fs -conf conf/hadoop-localhost.xml -text output/part-r-00001
hdfs://localhost/user/tom/input/smallfiles/b 62 62 62 62 62 62 62 62 62 62
hdfs://localhost/user/tom/input/smallfiles/d 64 64 64 64 64 64 64 64 64 64
hdfs://localhost/user/tom/input/smallfiles/f 66 66 66 66 66 66 66 66 66 66
7.2.2文本输入
TextInputFormat是默认的InputFormat,每一行数据都是一条记录。他的键是IntWritable类型的,储存该行在整个文件中的偏移量(单位为字节)。值就是行中的数据内容,不包括任何行终止符(换行符和回车符等),他是Text类型的。
TextInputFormat和HDFS块
7.2.4多种输入
MultipleInputs.addInputPath(job, ncdcInputPath,
TextInputFormat.class, MaxTemperatureMapper.class);
MultipleInputs.addInputPath(job, metOfficeInputPath,
TextInputFormat.class, MetOfficeMaxTemperatureMapper.class);
以上代码取代了对FileInputFormat.addInputPath()和conf.setMapperClass()的调用。
7.2.5 数据库格式的输入输出
DBInputFormat是一个使用JDBC并且从关系数据库中读取数据的一种输入格
式。由于它没有任何碎片技术,所以在访问数据库的时候必须非常小心,太多的
mapper可能会使数据库受不了。由于这个原因, DBInputFormat最好是在加载小量的数据集的时候用,可能在通过 MultipleInputs与来自HDFs的大数据集连接时使用。与之相对应的输出格式是 DBOutput Format,在将数据存入数据库时比较有用。
HBase中的 TableInputFormat可以让 MapReduce程序访问 HBase中表里的数据。同样, TableoutputFormat是可以让 MapReduce的输出写入 HBase。
7.3输出格式
7.3.3 多个输出
HashPartitioner可以适应任意数量的partitioner,并且能够较好的保证Partitioner之间是比较均匀的。
使用HashPartitioner的话,每个分区上包含多个气象站的数据。若要实现每个气象站一个输出文件,我们就需要让每个reducer写多个文件使用MultipleFileOutputFormat。
- 用MultipleFileOutpu类将整个数据集分割到以stationID命名的文件中。
public class PartitionByStationUsingMultipleOutputs extends Configured implements Tool {
static class StationMapper extends Mapper<LongWritable, Text, Text, Text> {
private NcdcRecordParser parser = new NcdcRecordParser();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
parser.parse(value);
context.write(new Text(parser.getStationId()), value);
}
}
static class MultipleOutputsReducer extends Reducer<Text, Text, NullWritable, Text> {
private MultipleOutputs<NullWritable, Text> multipleOutputs;
@Override
protected void setup(Context context) throws IOException, InterruptedException {
multipleOutputs = new MultipleOutputs<NullWritable, Text>(context);
}
@Override
protected void reduce(Text key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
for (Text value : values) {
multipleOutputs.write(NullWritable.get(), value, key.toString());
}
}
@Override
protected void cleanup(Context context) throws IOException, InterruptedException {
multipleOutputs.close();
}
}
@Override
public int run(String[] args) throws Exception {
Job job = JobBuilder.parseInputAndOutput(this, getConf(), args);
if (job == null) {
return -1;
}
job.setMapperClass(StationMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setReducerClass(MultipleOutputsReducer.class);
job.setOutputKeyClass(NullWritable.class);
return job.waitForCompletion(true) ? 0 : 1;
}
public static void main(String[] args) throws Exception {
int exitCode = ToolRunner.run(new PartitionByStationUsingMultipleOutputs(), args);
System.exit(exitCode);
}
}
StationMapper将stationId从记录中抽取出来作为键,使同一个气象站的数据被切分到同一个分区。