使用hadoop jar執行mapreduce任務時首先從hdfs中讀取數據將這些數據解析爲inputsplit,然後再將inputsplit中的內容解析爲一個一個的<k,v>鍵值對,這個過程就是有InputFormat的子類完成的。之前在MR例子中有一段代碼job.setInputFormatClass(TextInputFormat.class);就是指定TextInputFormat來完成這項工作,這個類是hadoop默認的其實可以不寫。
InputFormat是一個抽象類,類中有兩個抽象方法List<InputSplit> getSplits和RecordReader<K,V>createRecordReader,getSplit負責將hdfs數據解析爲InputSplit,createRecordReader負責將每個InputSplit中的每一行解析爲<k,v>鍵值對。
TextInputFormat
getSplits
FileInputFormat繼承了InputFormat並實現了getSplits方法。
主要完成的功能是:
根據路徑解析hdfs數據,判斷文件是否可以被切分。
計算splitSize,默認等於blockSize,128M
獲取每一個hdfs對象並進行遍歷並將結果放入List<InputSplit>中返回。
Hadoop中一個block對應一個inputsplit,一個inputsplit對應一個map任務。
注意:
hadoop不會對小於128M的文件進行切分,例如一個文件1G那就是8個map任務,如果有1000個100kb的文件則對應1000個map任務,這樣會造成效率下降。所以MapReduce不適合處理小文件。
如果inputsplit和blocksize不一樣比如大於,那麼在解析爲inputsplit時一個block就不夠用,此時框架就會去別的節點上讀取數據來構造inputsplit,這樣會產生網絡消耗影響效率。
createRecordReader
TextInputFormat繼承了FileInputFormat並實現了createRecordReader方法。此方法的返回值是抽象類RecordReader,而最終返回的是LineRecordReader,LineRecordReader實現了RecordReader並在實現的抽象方法中完成解析。
主要完成的功能是:
在initialize方法中獲取FileSplit對象並讀取每一行內容。
獲取<k,v>鍵值對作爲map任務的入參再調用map任務。
框架每獲取一個<k,v>就會調用一次map任務。
至此我們可以通過下圖大概瞭解一下這幾個類的關係
NlineInputFormat
Hadoop中默認是一個block一個inputsplit,但是在代碼中可以指定其他的inputFormat子類,NLineInputFormat可以設置指定文件中多少行爲一個inputsplit,下面的代碼指定每3行一個inputsplit。
org.apache.hadoop.mapreduce.lib.input.NLineInputFormat;
job.setInputFormatClass(NLineInputFormat.class);
NLineInputFormat.setNumLinesPerSplit(job,3);
KeyValueTextInputFormat
記錄中有製表符(tab),以第一個製表符爲分隔符,前面的作爲key後面的作爲value,若無製表符則全部爲key,value爲空。
job.setInputFormatClass(KeyValueTextInputFormat.class);
同時也可以指定其他的字符串爲分隔符
conf.setStrings(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR,",");
CombineFileInputFormat
以之前提到的wordcount實例中需要統計單詞出現的次數輸入類使用的是TextInputFormat,但是如果我有許多的小文件那麼在執行mapreduce時split的數量就會很多。
如下圖,hdfs上有4個文件對應的split數量也爲4,map任務也爲4
CombineFileInputFormat這個輸入類可以合併小文件,下面來看一個例子。
package mapreduce;
import java.io.IOException;
import java.net.URI;
import org.apache.hadoop.conf.Configuration;
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.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.CombineFileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.CombineFileRecordReader;
import org.apache.hadoop.mapreduce.lib.input.CombineFileSplit;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import org.apache.hadoop.mapreduce.lib.input.LineRecordReader;
//import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.ReflectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* hdfs上的hello中的內容爲
tiger pig
pig cat dog
dog bird cat
tiger house
bus bike bus car
* @author think
*
*/
public class WordCount {
public static void main(String[] args) throws Exception {
String inPath = args[0];
Path outPath = new Path(args[1]);
//1:hdfs configuration,get SystemFile Object
Configuration conf = new Configuration();
URI uri = new URI("/");// URI uri = new URI("hdfs://192.168.79.128:9000/");
FileSystem fileSystem = FileSystem.get(uri, conf);
if (fileSystem.exists(outPath)) {
fileSystem.delete(outPath, true);
}
// 2:job object
String jobName = WordCount.class.getName();
Job job = Job.getInstance(conf, jobName);
job.setJarByClass(WordCount.class);
// 3:輸入路徑
FileInputFormat.setInputPaths(job, inPath);
// 4:指定inputFormat的子類,可選,默認是TextInputFormat
//job.setInputFormatClass(TextInputFormat.class);
job.setInputFormatClass(CombineSmallFileInputFormat.class);
// 5:指定mapper類,指定mapper的輸出<k2,v2>類型
job.setMapperClass(MapTask.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
// 6:指定reduce類,指定reduce的輸出<k3,v3>類型
job.setReducerClass(ReduceTask.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
// 7:指定輸出路徑
FileOutputFormat.setOutputPath(job, outPath);
// 8:指定outputformat子類
job.setOutputFormatClass(TextOutputFormat.class);
// 9:提交yarn執行
job.waitForCompletion(true);
}
/**
* Map 任務
* @author think
* LongWritable, Text, Text, LongWritable這4個參數依次代表map任務的輸入鍵值對<k1,v1>和輸出鍵值對<k2,v2>
*/
public static class MapTask extends Mapper<LongWritable, Text, Text, LongWritable>
{
Logger logger = LoggerFactory.getLogger(WordCount.class);
Text k2 = new Text();
LongWritable v2 = new LongWritable();
/**
* 重寫map方法
* context是一個mapper的內部類
*/
@Override
protected void map(LongWritable key, Text value,
Mapper<LongWritable, Text, Text, LongWritable>.Context context)
throws IOException, InterruptedException {
//1:key爲內容的字節序數,value爲內容
String content = value.toString();
System.out.println("內容:" + key.get() + " ," + content);
logger.info("內容:" + key.get() + " ," + content);
String[] arrs = content.split(",");
for(String word : arrs)
{
k2.set(word);
v2.set(1);
context.write(k2, v2);
logger.info("map:" + k2.toString() + "," + v2);
}
}
}
/**
* Reduce 任務
* @author think
* Text, LongWritable, Text, LongWritable這4個參數依次代表reduce任務的輸入鍵值對<k2,v2s>和輸出鍵值對<k3,v3>
*/
public static class ReduceTask extends Reducer<Text, LongWritable, Text, LongWritable>
{
LongWritable v3 = new LongWritable();
@Override
protected void reduce(Text k2, Iterable<LongWritable> v2s,
Reducer<Text, LongWritable, Text, LongWritable>.Context content)
throws IOException, InterruptedException {
System.out.println("k2:" + k2.toString());
long sum = 0;
for(LongWritable v2 : v2s)
{
System.out.println("v2:" + v2);
sum += v2.get();
}
v3.set(sum);
content.write(k2, v3);
System.out.println("k3,v3:" + k2.toString() + "," + v3);
}
}
/**
* 自定義處理小文件的mapreduce輸入類
* @author think
*
*/
public static class CombineSmallFileInputFormat extends CombineFileInputFormat<LongWritable, Text>{
/**
* createRecordReader創建一個讀取器,實現RecordReader方法
* <LongWritable, Text>是map任務的輸入參數,和之前的一樣。入參感覺要隨map任務的業務而定
* 返回值是CombineFileRecordReader對象實例
* 這個對象繼承了RecordReader
* 生成實例需要三個參數
* 第一個需要強轉成CombineFileSplit
* 第二個是上下文
* 第三個是我們自定義的一個類,這個類必須繼承RecordReader
*/
@Override
public RecordReader<LongWritable, Text> createRecordReader(
InputSplit split, TaskAttemptContext context)
throws IOException {
return new CombineFileRecordReader((CombineFileSplit)split, context, CombineSmallFileRecordReader.class);
}
}
/**
* 繼承RecordReader類的<k,v>和上面一樣都是<LongWritable, Text>
* 實現RecordReader方法
* @author think
*
*/
public static class CombineSmallFileRecordReader extends RecordReader<LongWritable, Text> {
private LineRecordReader lrr;
/**
* 在解析多個小文件時,每個小文件都會調用上面的
* return new CombineFileRecordReader((CombineFileSplit)split, context, CombineSmallFileRecordReader.class);
* 所以構造函數的第三個參數index就是每個小文件的序號,比如第一個,第二個......
*
*
* @param split
* @param context
* @param index 文件的序號
* @throws IOException
* @throws Interrupted Exception
*/
public CombineSmallFileRecordReader(CombineFileSplit split, TaskAttemptContext context, Integer index) throws IOException, InterruptedException
{
//1.通過反射機制實例化lrr
this.lrr = ReflectionUtils.newInstance(LineRecordReader.class, context.getConfiguration());
//2.爲初始化方法構造參數
/**
* 參數fileSplit就是我們處理的衆多小文件(word,word1..)所以是FileSplit,我們需要自行構造
* 4個參數分別是路徑信息(file),起始位置(偏移量start),長度(length),所在位置hosts,我們需要構建這4個參數
* 4個參數均從split中獲取,index是文件的序號
*/
Path file = split.getPath(index);
long start = split.getOffset(index);
long length = split.getLength(index);
String[] hosts = split.getLocations();
InputSplit fileSplit = new FileSplit(file, start, length, hosts);
//3.調用初始化方法
this.lrr.initialize(fileSplit, context);
}
@Override
public void initialize(InputSplit split, TaskAttemptContext context)
throws IOException, InterruptedException {
// TODO Auto-generated method stub
}
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
return lrr.nextKeyValue();
}
@Override
public LongWritable getCurrentKey() throws IOException,
InterruptedException {
return lrr.getCurrentKey();
}
@Override
public Text getCurrentValue() throws IOException, InterruptedException {
return lrr.getCurrentValue();
}
@Override
public float getProgress() throws IOException, InterruptedException {
return lrr.getProgress();
}
@Override
public void close() throws IOException {
lrr.close();
}
}
}
下圖中顯示的是日誌,可以看到相比於輸入類使用TextInputFormat,使用CombineFileInputFormat的split和map任務數量都要少,之間也應該更快。CombineFileInputFormat合併了小文件。