详细情况见:http://shitouer.cn/2013/02/hadoop-source-code-analyse-mapreduce-inputformat/
http://blog.sina.com.cn/s/blog_4a064aed01011lja.html
以下是对此2网址的学习:
InputFormat中的Splits集合的获取;
InputFormat是一个接口,该接口有2个成员函数;
public abstract class InputFormat<K, V> { /** * 仅仅是逻辑分片,并没有物理分片,所以每一个分片类似于这样一个元组 <input-file-path, start, offset> */ public abstract List<InputSplit> getSplits(JobContext context) throws IOException, InterruptedException; /** * Create a record reader for a given split. */ public abstract RecordReader<K, V> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException; }
FileInputFormat类如下
static void addInputPath(Job job, Path path) ; // Add a Path to the list of inputs for the map-reduce job. 增加一条输入路径 static void addInputPaths(Job job, String commaSeparatedPaths) ; //Add the given comma separated paths to the list of inputs for the map-reduce job. 批量增加 protected long computeSplitSize(long blockSize, long minSize, long maxSize); protected int getBlockIndex(BlockLocation[] blkLocations, long offset) ; protected long getFormatMinSplitSize(); // Get the lower bound on split size imposed by the format. 获取最小splitSize也就是一个split有多少个blocks的最小,系统默认为1; static PathFilter getInputPathFilter(JobContext context) ; // Get a PathFilter instance of the filter set for the input paths. 获取一个文件过滤器 static Path[] getInputPaths(JobContext context) ; // Get the list of input Paths for the map-reduce job. 获取输入文件路径集合 static long getMaxSplitSize(JobContext context) ; // Get the maximum split size. 获取最大的splitSize也就是一个split有多少个blocks的最大值; static long getMinSplitSize(JobContext job); //Get the minimum split size 获取最小splitSize也就是一个split有多少个blocks的最小值;人为设置的。默认为1; List<InputSplit> getSplits(JobContext job) ; // Generate the list of files and make them into FileSplits. 获取FileSplits protected boolean isSplitable(JobContext context, Path filename) ; //Is the given filename splitable? Usually, true, but if the file is stream compressed, it will not be. 判断能否被分 protected List<FileStatus> listStatus(JobContext job) ; //List input directories. 获取FileSplits的状态,描述类信息 static void setInputPathFilter(Job job, Class<? extends PathFilter> filter) ; // Set a PathFilter to be applied to the input paths for the map-reduce job.设置输入文件过滤器是对map-reduce job而言的 static void setInputPaths(Job job, Path... inputPaths) ; //Set the array of Paths as the list of inputs for the map-reduce job.连续 设置输入文件路径是对map-reduce job而言的 static void setInputPaths(Job job, String commaSeparatedPaths); // Sets the given comma separated paths as the list of inputs for the map-reduce job.隔离 设置输入文件路径是对map-reduce job而言的 static void setMaxInputSplitSize(Job job, long size) ; // Set the maximum split size 设置最大split size值 static void setMinInputSplitSize(Job job, long size) ; // Set the minimum input split size 设置最小split size值 FileInputFormat() ;//Constructor构造函数 RecordReader<K, V> createRecordReader(InputSplit split,TaskAttemptContext context);//构造一个RecordReader对象
而FileInputFormat是继承了InputFormat接口的类,故而它需要实现这两个函数;
对于第一个函数实现:
public List<InputSplit> getSplits(JobContext job) throws IOException { // 首先计算分片的最大和最小值。这两个值将会用来计算分片的大小。 // 由源码可知,这两个值可以通过mapred.min.split.size和mapred.max.split.size来设置 long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job)); long maxSize = getMaxSplitSize(job); // splits链表用来存储计算得到的输入分片结果 List<InputSplit> splits = new ArrayList<InputSplit>(); // files链表存储由listStatus()获取的输入文件列表,listStatus比较特殊,我们在下面详细研究 List<FileStatus> files = listStatus(job); for (FileStatus file : files) { Path path = file.getPath(); FileSystem fs = path.getFileSystem(job.getConfiguration()); long length = file.getLen(); // 获取该文件所有的block信息列表[hostname, offset, length] BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0, length); // 判断文件是否可分割,通常是可分割的,但如果文件是压缩的,将不可分割 // 是否分割可以自行重写FileInputFormat的isSplitable来控制 if ((length != 0) && isSplitable(job, path)) { long blockSize = file.getBlockSize(); // 计算分片大小 // 即 Math.max(minSize, Math.min(maxSize, blockSize)); // 也就是保证在minSize和maxSize之间,且如果minSize<=blockSize<=maxSize,则设为blockSize long splitSize = computeSplitSize(blockSize, minSize, maxSize); long bytesRemaining = length; // 循环分片。 // 当剩余数据与分片大小比值大于Split_Slop时,继续分片, 小于等于时,停止分片 while (((double) bytesRemaining) / splitSize > SPLIT_SLOP) { int blkIndex = getBlockIndex(blkLocations, length - bytesRemaining); splits.add(new FileSplit(path, length - bytesRemaining, splitSize, blkLocations[blkIndex].getHosts())); bytesRemaining -= splitSize; } // 处理余下的数据 if (bytesRemaining != 0) { splits.add(new FileSplit(path, length - bytesRemaining, bytesRemaining, blkLocations[blkLocations.length - 1].getHosts())); } } else if (length != 0) { // 不可split,整块返回 splits.add(new FileSplit(path, 0, length, blkLocations[0] .getHosts())); } else { // 对于长度为0的文件,创建空Hosts列表,返回 splits.add(new FileSplit(path, 0, length, new String[0])); } } // 设置输入文件数量 job.getConfiguration().setLong(NUM_INPUT_FILES, files.size()); return splits; }
实现FileSplits集合,需要计算出一个文件的splitSize大小,也就是一个split包括多少个blocks;
Math.max(minSize, Math.min(goalSize, blockSize));
minSize表示1个splits包括最小包括多少个blocks,默认是1;
goalSize=totalblocks/numSplits,其中numSplits和maptasks数相等(一个split和1个map对应),这个是认为建议的值,而totalblocks表示所有文件(输入为1个文件,或者多个文件)在物理上所需要的blocks数;
blocksize表示这个文件需要多少个blocksize来存放。
结论:
1. 一个split不会包含零点几或者几点几个Block,一定是包含大于等于1个整数个Block
2. 一个split不会包含两个File的Block,不会跨越File边界
3. split和Block的关系是一对多的关系
4. maptasks的个数最终决定于splits的长度
splits的loaction是取第一个block的地址信息,而不是所有block地址信息,因为有一个可以知道其他的;
详细程序过程已经注释了,这其中使用到了FlieSplit具体类,该类的构造主要是提供信息存储效果;
FileSplit类是继承InputSplit接口;主要带参构造函数和继承的两个方法,以及成员变量。
FlieSplit类如下
FlieSplit impement InputSize { private Path file; // Split所在的文件 private long start; // Split的起始位置 private long length; // Split的长度,getLength()会返回它 private String[] hosts; // Split所在的机器名称,getLocations()会返回它 long getLength() throws IOException { //通过这个函数返回length return length; } String[] getLocations() throws IOException { //........通过这个函数返回loaction
if(host!=null) return hosts;
else
return new String[]; } FlieSplit() { //构造空的对象 } FlieSplit(Path file,long start,long length,String[] hosts) { //......赋值即可 file=file; start=start; length=length; hosts=hosts; } ~FlieSplit() { } }
上述的FileInputFormat的实现了InputFormat中的第一个函数,与InputSplit接口的具体类FileSplit相对应了。
另外listStatus函数实现如下;这个方法是FileInputFormat的方法,不是继承的。
protected List<FileStatus> listStatus(JobContext job) throws IOException { // 省略部分代码... List<PathFilter> filters = new ArrayList<PathFilter>(); filters.add(hiddenFileFilter); PathFilter jobFilter = getInputPathFilter(job); if (jobFilter != null) { filters.add(jobFilter); } // 创建了一个MultiPathFilter,其内部包含了两个PathFilter // 一个为过滤隐藏文件的Filter,一个为用户自定义Filter(如果制定了) PathFilter inputFilter = new MultiPathFilter(filters); for (int i = 0; i < dirs.length; ++i) { Path p = dirs[i]; FileSystem fs = p.getFileSystem(job.getConfiguration()); FileStatus[] matches = fs.globStatus(p, inputFilter); if (matches == null) { errors.add(new IOException("Input path does not exist: " + p)); } else if (matches.length == 0) { errors.add(new IOException("Input Pattern " + p + " matches 0 files")); } else { for (FileStatus globStat : matches) { if (globStat.isDir()) { for (FileStatus stat : fs.listStatus( globStat.getPath(), inputFilter)) { result.add(stat); } } else { result.add(globStat); } } } } // 省略部分代码 }
在上述中得到了FileStatus类对象的集合;FileStatus类如下
Class FileStatus extends Object implement Comparable, Writable { prviate long length; prviate boolean isdir; prviate int block_replication; prviate long blocksize; prviate long modification_time,; prviate long access_time; prviate FsPermission permission; prviate String owner; prviate String group; prviate Path path; int compareTo(Object o) ; // Compare this object to another object 比较 boolean equals(Object o); // Compare if this object is equal to another object 是否相等 long getAccessTime() ; //Get the access time of the file. 获取access time long getBlockSize() ; // Get the block size of the file. 获取block size文件的blocks数 String getGroup() ; //Get the group associated with the file. 获取文件组 long getLen() ;//获取文件长度,单位是B; long getModificationTime() ; // Get the modification time of the file. 获取修改时间 String getOwner() ; //Get the owner of the file. 获取文件所属者 Path getPath() ;//获取文件路径 FsPermission getPermission() ; //Get FsPermission associated with the file. 不重要 short getReplication() ; // Get the replication factor of a file. 获取版本 int hashCode() ; //Returns a hash code value for the object, which is defined as the hash code of the path name. 哈希后 boolean isDir() ; // Is this a directory? 判断是不是路径 void readFields(DataInput in) ; //Deserialize the fields of this object from in. protected void setGroup(String group); //Sets group. protected void setOwner(String owner) ; //Sets owner. protected void setPermission(FsPermission permission) ; //Sets permission. void write(DataOutput out) ; //Serialize the fields of this object to out. FileStatus() ;//Constructor
FileStatus(long length, boolean isdir, int block_replication, long blocksize, long modification_time, long access_time, FsPermission permission, String owner, String group, Path path) ;//Constructor
FileStatus(long length, boolean isdir, int block_replication, long blocksize, long modification_time, Path path) ;//Constructor }
这里用到了isDir()是不是文件路径,不是路径是文件则直接添加了,该文件状态信息;若是路径则调用fs.listStatus方法将其路径下的所有文件添加到文件状态信息中。
这里又出现了PathFilter类,这些在网址1中有介绍,或者查看hadoop的api。
下面讲述第二个函数与与InputSplit接口的具体类FileSplit如何对应:
这个函数实现只需要在此种通过RecordReader接口下的具体类来构造对象,将其放回就可以得到RecordReader的具体类对象了。故而重点是用一个类去实现接口RecordReader;
首先介绍接口RecordReader
public abstract class RecordReader<KEYIN, VALUEIN> implements Closeable { /** * 由一个InputSplit初始化 */ public abstract void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException; /** * 顾名思义,读取分片下一个<key, value>对----最重要的部分 */ public abstract boolean nextKeyValue() throws IOException, InterruptedException; /** * Get the current key */ public abstract KEYIN getCurrentKey() throws IOException, InterruptedException; /** * Get the current value. */ public abstract VALUEIN getCurrentValue() throws IOException, InterruptedException; /** * 跟踪读取分片的进度 */ public abstract float getProgress() throws IOException, InterruptedException; /** * Close the record reader. */ public abstract void close() throws IOException; }
具体类是LineRecordReader类,下面主要是实现了initialize和nextKeyValue方法;其他省去了
public class LineRecordReader extends RecordReader<LongWritable, Text> { private CompressionCodecFactory compressionCodecs = null; private long start; private long pos; private long end; private LineReader in; private int maxLineLength; private LongWritable key = null; private Text value = null; // initialize函数即对LineRecordReader的一个初始化 // 主要是计算分片的始末位置,打开输入流以供读取K-V对,处理分片经过压缩的情况等 public void initialize(InputSplit genericSplit, TaskAttemptContext context) throws IOException { FileSplit split = (FileSplit) genericSplit; Configuration job = context.getConfiguration(); this.maxLineLength = job.getInt("mapred.linerecordreader.maxlength", Integer.MAX_VALUE); start = split.getStart(); end = start + split.getLength(); final Path file = split.getPath(); compressionCodecs = new CompressionCodecFactory(job); final CompressionCodec codec = compressionCodecs.getCodec(file); // 打开文件,并定位到分片读取的起始位置 FileSystem fs = file.getFileSystem(job); FSDataInputStream fileIn = fs.open(split.getPath()); boolean skipFirstLine = false; if (codec != null) { // 文件是压缩文件的话,直接打开文件 in = new LineReader(codec.createInputStream(fileIn), job); end = Long.MAX_VALUE; } else { // if (start != 0) { skipFirstLine = true; --start; // 定位到偏移位置,下次读取就会从便宜位置开始 fileIn.seek(start); } in = new LineReader(fileIn, job); } if (skipFirstLine) { // skip first line and re-establish "start". start += in.readLine(new Text(), 0, (int) Math.min((long) Integer.MAX_VALUE, end - start)); } this.pos = start; } public boolean nextKeyValue() throws IOException { if (key == null) { key = new LongWritable(); } key.set(pos);// key即为偏移量 if (value == null) { value = new Text(); } int newSize = 0; while (pos < end) { newSize = in.readLine(value, maxLineLength, Math.max((int) Math.min(Integer.MAX_VALUE, end - pos), maxLineLength)); // 读取的数据长度为0,则说明已读完 if (newSize == 0) { break; } pos += newSize; // 读取的数据长度小于最大行长度,也说明已读取完毕 if (newSize < maxLineLength) { break; } // 执行到此处,说明该行数据没读完,继续读入 } if (newSize == 0) { key = null; value = null; return false; } else { return true; } } // 省略了部分方法 }
程序通过job.setInputFormatClass(FileInputFormat.
class
);设置自己定义的FileInputFormat输入格式
在FileInputFormat类中有了具体的LineRecordReader类对象,RecordReader是如何被Mapreduce框架利用的呢?
首先看一下基类Mapper类
public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> { public class Context extends MapContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> { public Context(Configuration conf, TaskAttemptID taskid, RecordReader<KEYIN, VALUEIN> reader, RecordWriter<KEYOUT, VALUEOUT> writer, OutputCommitter committer, StatusReporter reporter, InputSplit split) throws IOException, InterruptedException { super(conf, taskid, reader, writer, committer, reporter, split);//调用父类构造函数,该类就是用来构造父类,没有新的方法 } } /** * 预处理,仅在map task启动时运行一次 */ protected void setup(Context context) throws IOException, InterruptedException { } /** * 对于InputSplit中的每一对<key, value>都会运行一次 */ @SuppressWarnings("unchecked") protected void map(KEYIN key, VALUEIN value, Context context) throws IOException, InterruptedException { context.write((KEYOUT) key, (VALUEOUT) value); } /** * 扫尾工作,比如关闭流等 */ protected void cleanup(Context context) throws IOException, InterruptedException { } /** * map task的驱动器 */ public void run(Context context) throws IOException, InterruptedException { setup(context); while (context.nextKeyValue()) { map(context.getCurrentKey(), context.getCurrentValue(), context); } cleanup(context); } }
核心是在于run函数,
-
run()方法首先调用setup()进行初始操作
-
然后循环对每个从context.nextKeyValue()获取的“K-V对”调用map()函数进行处理
-
最后调用cleanup()做最后的处理
Context extends MapContext表明Context的基类是MapContext
public class MapContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> extends TaskInputOutputContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> { private RecordReader<KEYIN, VALUEIN> reader; private InputSplit split; public MapContext(Configuration conf, TaskAttemptID taskid, RecordReader<KEYIN, VALUEIN> reader, RecordWriter<KEYOUT, VALUEOUT> writer, OutputCommitter committer, StatusReporter reporter, InputSplit split) { super(conf, taskid, writer, committer, reporter);//作用是调用父类的构造函数也就是TaskInputOutputContext构造函数完成父类构造 this.reader = reader; this.split = split; } /** * Get the input split for this map. */ public InputSplit getInputSplit() { return split; } @Override public KEYIN getCurrentKey() throws IOException, InterruptedException { return reader.getCurrentKey(); } @Override public VALUEIN getCurrentValue() throws IOException, InterruptedException { return reader.getCurrentValue(); } @Override public boolean nextKeyValue() throws IOException, InterruptedException { return reader.nextKeyValue(); } }
我们知道一般去重写Mapper类时一般是重写它的map函数,几乎不会对run函数重写;至于setup和cleanup这些只是调用一次对于一次任务而言;高手会重写run函数;一般人只是重写map函数。
做好自己的类后,通过job来set,将自己的类给job调用使用。
job.setMapperClass(TokenizerMapper.class);//设置自己的Mapper类
job.setInputFormatClass(FileInputFormat.
class
);
//设置自己的InputFormat类
那么关键的Mapper类中的run在哪里调用呢?又是如何传递关键参数context呢?
待续。。。。。。。。。。。