2.1MapReduce输入

来源:互联网 发布:杭州工商大学网络教育 编辑:程序博客网 时间:2024/06/15 22:21

2.1MapReduce输入

  MapReduce作业依赖于Map阶段为它提供原始数据的输入,这个阶段提供了能获得的最大并行度,因此它的智能化对一个作业的提速至关重要。数据被分成块(chunk),然后Map任务对每块数据进行操作。每块数据被称为InputSplit。[//后面称分片]Map任务需要在每个InputSplit类上进行操作。还有其他两种类,InputFormat和RecordReader,在处理Hadoop作业的输入时,它们尤为重要。

2.1.1 InputFormat类

  Hadoop中一个MapReduce作业的输入数据的规格是通过InputFormat类及它的子类给出的。Hadoop在运行MapReduce程序时InputFormat家族的类主要承担以下功能:

  • 输入数据的有效性检测。例如,检查指定路径的文件是否存在。
  • 将输入数据切分成逻辑块(InputSplit),并把它们分配给对应的Map任务。也就是有多少个InputSplit,就得生成多少个Map任务。
  • 实例化一个能在每个InputSplit类上工作的RecordReader对象,并以键-值对方式生成数据。

  当需要从HDFS中获取输入时,会广泛使用FileInputFormat的派生类;子类DBInputFormat则是一个能从支持SQL的数据库读取数据的特殊类;CombineFileInputFormat则是一个直接派生于FileInputFormat的抽象子类,它能将多个文件合并到一个分片中。
InputFormat是抽象类,其继承子类层次图

  • InputFormat
    • DBInputFormat
    • EmptyInputFormat
    • FileInputFormat
      • TextInputFormat
      • CombineFileInputFormat
      • NLineInputFormat
      • SequenceFileInputFormat
      • KeyValueTextInputFormat
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;    }                                                                    

整个基于文件的InputFormat体系(FileInputFormat及其子类)的设计思路是,有公共基类FileInputFormat采用统一的方法对各种输入文件进行切分,比如按照某个固定大小等分,而由各个派生类自己提供机制将进一步解析InputSplit。对应的具体的实现是,基类FileInputFormat提供getSplits()实现,而派生类提供createRecordReader()实现。比如TextInputFormat 类是FileInputFormat 的默认实现,该输入格式主要针对的是文本类型的文件,文件被分割成多行,而且每一行都使用换行符(LF=10) 或者【Enter】键作为每一行的结束标识。该类主要重写了FileInputFormat中的createRecordReader ,其返回了LineRecordReader行记录读取器,该读取器用于从文件中读取一行,将这行文本在文件中的偏移量作为key,以这行文本的内容作为value,组成键值对。

2.1.2 InputSplit类

  抽象类InputSplit以及它的派生类从字节的层面展示输入数据,它有以下几个主要属性:

  • 输入文件名
  • 分片数据在文件中的偏移量(起始位置)
  • 分片数据的长度(以字节为单位)
  • 分片数据所在的节点的位置信息

  InputSplit有以下两个特点:

  • 逻辑分片:它只是在逻辑上对输入数据进行分片,并不会在磁盘上将其切分成分片进行存储。InputSplit只记录了分片的元数据信息,比如起始位置,长度以及所在的节点列表。
  • 可序列化:在hadoop中,对象序列化主要有两个作用:进程间通信和永久存储。此外,InputSplit支持序列化操作主要是为了进程间通信。作业被提交到JobTracker之前,Client会调用作业InputFormat中的getSplits函数,并将得到的InputSplit序列化到文件中。这样,当作业提交到JobTracker端对作业初始化时,可直接读取该文件,解析出所有InputSplit,并创建对应的Map Task。

  在HDFS中,当一个文件的大小少于HDFS的块容量时,每个文件都将创建一个InputSplit实例。例如,如果HDFS的块容量为128MB,任何小于128MB的文件都会拥有一个InputSplit实例。对于那些被分割成多个块的文件(文件的大小多于块的容量),将会使用一个更为复杂的公式来计算InputSplit的数量。一般情况下,InputSplit类受限于HDFS块容量的上限,除非最小的分片[//这里的分片应该时指一个InputSplit]也比块容量还大(这是很罕见的情况,并且可能导致数据本地化的问题)。

InputSplitSize=Max(minSplitSize,Min(blocksize,maxSplitSize))

  基于分片所在位置信息和资源的可用性,调度器将决定在哪个节点上为一个分片[//InputSplit]执行对应的Map任务,然后分片将与执行任务的节点进行通信。

  • InputSplit
    • FileSplit (文件输入分片)
    • CombineFileSplit(多文件输入分片)
    • DBInputSplit(数据块输入分片)

2.1.3 RecordReader类

  与InputSplit不同的是,RecordReader类将数据以一条条记录(record)的方式向Map传递。RecordReader在InputSplit类内部执行,并将数据以key-value的形式产生一条条的记录。RecordReader的边界会参考InputSplit的边界,但不是强制一致的。极端情况下,一个自定义的RecordReader类可以对整个文件进行读写(但我们不建议这么做)。大部分时候,在RecordReader类于InputSplit类重合的情况下,RecordReader类将对应一个InputSplit类,从而为Map任务提供完整的数据记录。

  通过FSDataInputStream的对象,可以对一个InputSplit类以字节的方式读取数据。虽然这种方式不会感知数据的位置信息,但是通常情况下,它仅从下一个分片中获取很少字节的数据,所以不会有很明显的负载过高的问题。当一条记录很大时,由于节点间要传输大量的数据,因此会对性能造成很大的影响。

  InputSplit的第一条记录和最后一条记录可能会被从中间切开。为了解决这种记录跨越InputSplit的读取问题,RecordReader规定每个InputSplit的第一条不完整记录划给前一个InputSplit处理。

2.1.4 Hadoop的“小文件”问题

  当输入文件明显小于HDFS的块的容量时,Hadoop会出现一个众所周知的“小文件”问题。小文件作为输入处理时,Hadoop将为每个文件创建一个Map任务,这将引入很高的任务注册开销。这些任务能够在几秒中内完成,然而产生任务和清理任务的时间要比执行任务的时间长得多。同时,每个文件在NameNode中大约要占据150字节的内存,如果大量的小文件存在,将使得这种对象的数量激增,严重影响NameNode的性能和可扩展性。读取大量小文件也是效率很低,因为有大量的磁盘寻道(seek)操作,并且需要跨越不同的DataNode去读取。

  不幸的是,小文件是现实存在的,但是我们可以采取如下策略处理小文件。

  • 在存储文件和执行文件之前,先执行预处理步骤,即把小文件合并成一个更大的文件。SequenceFile(序列文件)和TFile格式时比较受欢迎的将小文件合并为大文件的方法。另一个可选的方案是使用Hadoop Archive File(HAR),它能减轻NameNode的内存压力。HAR是基于HDFS的元文件系统(meta-filesystem)。
  • 使用CombineFileInputFormat将多个小文件合并到一个InputSplit中。同时也可以考虑用这个方法来提高处在相同节点或机架的数据的处理性能。由于这种方法没有改变NameNode中的文件数量,所以它不能减轻NameNode的内存需求量的压力。

  为了演示CombineFileInputFormat的功能,我们的数据集:https://archive.ics.uci.edu/ml/datasets/NSF+Research+Award+Abstracts+1990-2003 。尽管数据集有130000份,但是我们只考虑其中一个只有441份的子集。MapReduce Hadoop作业从数据集中逐行读取,并产生441个输入分片,然后把结果输出到标准输出(standard output)。在这个作业中,reduce任务的数量设置为0。

  Hadoop MapReduce作业的输入需要指定使用InputFormat,InputSplit和RecordReader类。在这个例子中,我们将441份文件合并为一个单独的分片。

  CombineFileInputFormat是一个抽象类,它能够帮助我们合并文件,从而指定输入。开发人员唯一需要重写(Override)的是createRecordReader()方法。这个方法实例化一个自定义的RecordReader类的对象来读取记录。CombineFileInputFormat类在getSplits()方法中返回一个CombineFileSplit分片对象。每个分片可能合并了来自不同文件的不同块。如果使用setMaxSplitSize()方法设置了分片的最大容量,本地节点的文件将会合并到一个分片中,本地剩余的块[//超过分片最大容量的那部分]将与来自同一机架的其他主机的块合并。然而,如果没有设置这个最大容量,合并操作不会在本地主机层面进行,它只会在同一机架内进行合并。如果将setMaxSplitSize()设置为HDFS的块容量,那是默认行为,也就是每个块对应一个分片。

package MasteringHadoop;import org.apache.hadoop.conf.Configuration;import org.apache.hadoop.fs.FSDataInputStream;import org.apache.hadoop.fs.FileSystem;import org.apache.hadoop.fs.Path;import org.apache.hadoop.io.LongWritable;import org.apache.hadoop.io.Text;import org.apache.hadoop.mapreduce.InputSplit;import org.apache.hadoop.mapreduce.RecordReader;import org.apache.hadoop.mapreduce.TaskAttemptContext;import org.apache.hadoop.mapreduce.lib.input.*;import org.apache.hadoop.util.LineReader;import java.io.IOException;public class MasteringHadoopCombineFileInputFormat extends CombineFileInputFormat<LongWritable, Text>{    @Override    public RecordReader<LongWritable, Text> createRecordReader(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException {        return new CombineFileRecordReader<LongWritable, Text>((CombineFileSplit) inputSplit, taskAttemptContext,  MasteringHadoopCombineFileRecordReader.class);    }    public static class MasteringHadoopCombineFileRecordReader extends RecordReader<LongWritable, Text>{        private LongWritable key;        private Text value;        private Path path;        private FileSystem fileSystem;        private LineReader lineReader;        private FSDataInputStream fsDataInputStream;        private Configuration configuration;        private int fileIndex;        private CombineFileSplit combineFileSplit;        private long start;        private long end;        public MasteringHadoopCombineFileRecordReader(CombineFileSplit inputSplit, TaskAttemptContext context, Integer index) throws IOException{            this.fileIndex = index;            this.combineFileSplit = inputSplit;            this.configuration = context.getConfiguration();            this.path = inputSplit.getPath(index);            this.fileSystem = this.path.getFileSystem(configuration);            this.fsDataInputStream = fileSystem.open(this.path);            this.lineReader = new LineReader(this.fsDataInputStream, this.configuration);            this.start = inputSplit.getOffset(index);            this.end = this.start + inputSplit.getLength(index);            this.key = new LongWritable(0);            this.value = new Text("");        }        @Override        public void initialize(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {            //Overloaded in the constructor.        }        @Override        public boolean nextKeyValue() throws IOException, InterruptedException {            int offset = 0;            boolean isKeyValueAvailable = true;            if(this.start < this.end){                offset = this.lineReader.readLine(this.value);                this.key.set(this.start);                this.start += offset;            }            if(offset == 0){                this.key.set(0);                this.value.set("");                isKeyValueAvailable = false;            }            return isKeyValueAvailable;        }        @Override        public LongWritable getCurrentKey() throws IOException, InterruptedException {            return key;        }        @Override        public Text getCurrentValue() throws IOException, InterruptedException {            return value;        }        @Override        public float getProgress() throws IOException, InterruptedException {            long splitStart = this.combineFileSplit.getOffset(fileIndex);            if(this.start < this.end){                return Math.min(1.0f, (this.start -  splitStart)/ (float) (this.end - splitStart));            }            return 0;        }        @Override        public void close() throws IOException {            if(lineReader != null){                lineReader.close();            }        }    }}

  代码演示了自定义的RecordReader类,创建这个类是为了从CombineFileSplit中返回记录。CombineFileSplit和FileSplit之间的不同点在于是否存在包含多个偏移量和长度的多个路径。自定义的RecordReader类会被分片中的每个文件[//每个InputSplit]调用,因此,自定义RecordReader类的构造函数必须有一个整型(integer)变量指明特定的文件正在用于产生记录。
MasteringHadoopCombineFileRecordReader类在MasteringHadoopCombineFileInputFormat类内部。

  下面代码段给出了Mapper类和驱动程序的实现。在驱动程序中最重要的一行代码就是设置job.setInputFormatClass(MasteringHadoop.MasteringHadoopCombineFileInputFormat.class)。当程序执行时,程序中分片是1,数据集的大小为5MB,而HDFS的块容量为128MB。

package MasteringHadoop;import org.apache.hadoop.conf.Configuration;import org.apache.hadoop.fs.Path;import org.apache.hadoop.mapreduce.*;import org.apache.hadoop.io.*;import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;import org.apache.hadoop.util.GenericOptionsParser;import java.io.IOException;public class CombineFilesMasteringHadoop {    public static class CombineFilesMapper extends  Mapper<LongWritable, Text, LongWritable, Text>{        @Override        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {            context.write(key, value);        }    }    public static void main(String args[]) throws IOException, InterruptedException, ClassNotFoundException{        GenericOptionsParser parser = new GenericOptionsParser(args);        Configuration config = parser.getConfiguration();        String[] remainingArgs = parser.getRemainingArgs();        Job job = Job.getInstance(config, "MasteringHadoop-CombineDemo");        job.setOutputKeyClass(LongWritable.class);        job.setOutputValueClass(Text.class);        job.setMapperClass(CombineFilesMapper.class);        job.setNumReduceTasks(0);        job.setInputFormatClass(MasteringHadoop.MasteringHadoopCombineFileInputFormat.class);        job.setOutputFormatClass(TextOutputFormat.class);        FileInputFormat.addInputPath(job, new Path(remainingArgs[0]));        TextOutputFormat.setOutputPath(job, new Path(remainingArgs[1]));        job.waitForCompletion(true);    }}

输入文件在grant-subset中
输出内容:

2017-11-14 16:25:20,866 INFO [org.apache.hadoop.mapreduce.lib.input.FileInputFormat] - Total input paths to process : 4412017-11-14 16:25:21,460 INFO [org.apache.hadoop.mapreduce.JobSubmitter] - number of splits:1

将job.setInputFormatClass(MasteringHadoop.MasteringHadoopCombineFileInputFormat.class)注释掉后的结果:

17/11/14 18:58:07 INFO input.FileInputFormat: Total input paths to process : 44117/11/14 18:58:16 INFO mapreduce.JobSubmitter: number of splits:441

2.1.5 输入过滤

  我们使用那441个文件演示过滤功能。我们需要处理的文件的文件名需要匹配一个特定的正则表达式,并且满足最小文件大小。这两个要求都有特定的作业参数,分别是filter.name和filter.min.size。实现时需要扩展Configured类,并向下面代码段那样实现PathFilter接口。Configured类是能够使用Configuration进行配置的基类,PathFilter接口包含了accept()方法,而在accept()方法的实现中,接受一个Path参数作为入参,并根据是否需要在输入中包含这个文件来决定返回true或者false。下面的代码段展示了这个类的主要实现:
MasteringHadoopPathFilter.java

package MasteringHadoop;import org.apache.hadoop.conf.Configuration;import org.apache.hadoop.conf.Configured;import org.apache.hadoop.fs.FileSystem;import org.apache.hadoop.fs.Path;import org.apache.hadoop.fs.PathFilter;import org.apache.hadoop.io.IntWritable;import org.apache.hadoop.io.LongWritable;import org.apache.hadoop.io.Text;import org.apache.hadoop.mapreduce.Job;import org.apache.hadoop.mapreduce.Mapper;import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;import org.apache.hadoop.util.GenericOptionsParser;import java.io.IOException;import java.util.regex.Matcher;import java.util.regex.Pattern;public class MasteringHadoopPathFilter {    public static class MasteringHadoopPathFilterMap extends Mapper<LongWritable, Text, LongWritable, Text> {        @Override        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {            context.write(key, value);        }    }    public static class MasteringHadoopPathAndSizeFilter extends Configured implements PathFilter {        private Configuration configuration;        private Pattern filePattern;        private long filterSize;        private FileSystem fileSystem;        @Override        public boolean accept(Path path){            boolean isFileAcceptable = true;            try{                if(fileSystem.isDirectory(path)){                    return true;                }                if(filePattern != null){                    Matcher m = filePattern.matcher(path.toString());                    isFileAcceptable = m.matches();                }                if(filterSize > 0){                    long actualFileSize = fileSystem.getFileStatus(path).getLen();                    if(actualFileSize > this.filterSize){                        isFileAcceptable &= true;                    }                    else{                        isFileAcceptable = false;                    }                }            }            catch(IOException ioException){                //Error handling goes here.            }            return isFileAcceptable;        }        @Override        public void setConf(Configuration conf){            this.configuration = conf;            if(this.configuration != null){                String filterRegex = this.configuration.get("filter.name");                if(filterRegex != null){                    this.filePattern = Pattern.compile(filterRegex);                }                String filterSizeString = this.configuration.get("filter.min.size");                if(filterSizeString != null){                    this.filterSize = Long.parseLong(filterSizeString);                }                try{                    this.fileSystem = FileSystem.get(this.configuration);                }                catch(IOException ioException){                    //Error handling                }            }        }    }    public static void main(String args[]) throws IOException, InterruptedException, ClassNotFoundException{        GenericOptionsParser parser = new GenericOptionsParser(args);        Configuration config = parser.getConfiguration();        String[] remainingArgs = parser.getRemainingArgs();        Job job = Job.getInstance(config, "MasteringHadoop-PathFilterDemo");        job.setOutputKeyClass(IntWritable.class);        job.setOutputValueClass(Text.class);        job.setMapperClass(MasteringHadoopPathFilterMap.class);        job.setNumReduceTasks(0);        job.setInputFormatClass(TextInputFormat.class);        job.setOutputFormatClass(TextOutputFormat.class);        FileInputFormat.setInputPathFilter(job, MasteringHadoopPathAndSizeFilter.class);        TextInputFormat.addInputPath(job, new Path(remainingArgs[0]));        TextOutputFormat.setOutputPath(job, new Path(remainingArgs[1]));        job.waitForCompletion(true);    }}

MasteringHadoopPathAndSizeFilter.setConf()方法是用来对Configuration的私有变量进行设置,并且读取它的任何属性。这驱动类(主类)中,需要使用下面的代码告诉作业过滤器的存在:

FileInputFormat.setInputPathFilter(job, MasteringHadoopPathAndSizeFilter.class);

MasteringHadoopPathAndSizeFilter.accept()方法为所有的目录返回true。当前目录的路径也是accept()方法的输入路径之一。它使用Java的正则表达式类(Pattern和Matches)决定是否存在匹配正则表达式的路径,并以此配置一个相应的布尔变量。进行二次检查以确定文件大小,并与过滤器设置的大小相比较。FileSystem类的对象暴露一个能返回FileStatus对象的getFileStatus()方法,FileStatus对象能够通过获取器(getter)检测对应文件的属性。

程序运行参数:

-D filter.name=.*a999645.* -D filter.min.size=2500 ./grant-subset ./output

执行结果:

2017-11-14 21:53:33,723 INFO [org.apache.hadoop.mapreduce.lib.input.FileInputFormat] - Total input paths to process : 32017-11-14 21:53:33,944 INFO [org.apache.hadoop.mapreduce.JobSubmitter] - number of splits:3
原创粉丝点击