背景
無論是 hdfs 存儲文件還是 mapreduce 處理文件,對於小文件的存儲和處理都會影響效率,在實際工作中又難免面臨處理大量小文件的場景(比方說用 flume 實時採集日誌,日誌是由用戶發送請求而產生的,用戶發送請求的頻率不是固定的,有的時候頻繁請求,有的時候請求數就比較少,flume 採集數據的配置是每隔固定的一段時間產生一個文件,所以就導致在有些時間段會難免產生大量的小文件)。
在 d 盤的 input 目錄創建三個文件:
one.txt:
I love Beijign
I love China
Beijing is the capital of China
tow.txt:
I love Yantai
I love ShanDong
three.txt
I love Hangzhou
I love Shenzhen
分析
小文件的優化有如下幾種方式:
-
在數據採集階段,就將小文件或小批數據先合併成大文件再上傳到 HDFS,即在 Flume 採集的時候進行相應文件大小的配置。
-
在業務處理前,使用 mapreduce 程序對 HDFS 上的小文件先進行合併,再做後續的業務處理(當然,也可以使用 Java IO 流處理一下)。
-
在業務處理時,採用 CombineTextInputFormat 將多個小文件合併成一個切片,再處理以調高效率。詳見 案例四。
本例中使用第二種方式:通過自定義 InputFormat,RecordReader,指定輸出的OutputFormat 類型爲 SequenceFileOutputFormat 的方式來將多個小文件合併成一個大文件。
知識點:自定義 InputFormat,自定義 RecordReader。
實現
因爲 InputFormat 讀取文件輸入靠的是 RecordReader 來完成的,所以我們需要先創建 RecordReader。
1.自定義RecordReader
自定義的 RecordReader 需要繼承 RecordReader,泛型的類型爲 map 端輸入的 key 和 value 的類型,本例中我們的目的是合併文件,所以把文件的內容以字節序列的形式從 value 接收進來就可以,key 設爲 NullWritable 類型。
默認的 TextInputFormat 的 key 的類型是 LongWritable,表示當前所讀取到的字節的偏移量(相對於整篇文章),value 的類型是 Text,表示的是這一行文本的內容,大家可以回過頭去看之前的詞頻統計案例,就可以理解爲什麼 map 的輸入的 key 的類型是 LongWritable,輸入的 value 的類型是 Text 了。
需要重寫 6 個方法:
-
initialize:初始化RecordReader,如果在構造函數中進行了初始化,該方法可以爲空。
-
nextKeyValue:判斷當前文件是否還有下一個 key/value。
-
getCurrentKey:獲取當前讀取到的 key。
-
getCurrentValue:獲取當前讀取到的 value。
-
getProgress:返回的是一個[0.0, 1.0]之間的小數,表示讀取進度,1表示讀取完成。
-
close:關閉資源。
package top9_inputformat;
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.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
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.FileSplit;
import java.io.IOException;
/**
* @author 曲健磊
* @date 2019-09-18 10:52:32
* @description 用於讀取切片中的數據
*/
public class WholeRecordReader extends RecordReader<NullWritable, BytesWritable> {
private Configuration configuration;
private FileSplit split;
private boolean processed = false;
private BytesWritable value = new BytesWritable();
@Override
public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
// 接收讀取到的切片信息以及配置信息
this.split = (FileSplit) split;
configuration = context.getConfiguration();
}
// InputFormat會爲每一個輸入文件創建一個RecordReader
// 每一個RecordReader循環調用nextKeyValue方法讀取改文件所產生的所有切片
// 在本例中每個文件將會調用兩次nextKeyValue方法:
// 第一次:讀取該文件中的所有內容放入緩存把processed標記置爲true
// 第二次:標記爲true,結束方法(可在nextKeyValue方法內打斷點調試)
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
// 在讀取每個文件中的數據的時候判斷是否存在下一個key/value,如果存在返回true,否則返回false
if (!processed) {
// 1.定義緩存區
byte[] contents = new byte[(int)split.getLength()];
FileSystem fs = null;
FSDataInputStream fis = null;
try {
// 2.獲取文件系統
Path path = split.getPath();
fs = path.getFileSystem(configuration);
// 3.讀取數據
fis = fs.open(path);
// 4.讀取文件內容進緩衝區
IOUtils.readFully(fis, contents, 0, contents.length);
// 5.將數據保存到 value 中
value.set(contents, 0, contents.length);
} catch (Exception e) {
} finally {
IOUtils.closeStream(fis);
}
processed = true;
return true;
}
return false;
}
@Override
public NullWritable getCurrentKey() throws IOException, InterruptedException {
// 獲取當前讀取到的數據的key
return NullWritable.get();
}
@Override
public BytesWritable getCurrentValue() throws IOException, InterruptedException {
// 獲取當前讀取到的數據的value
return value;
}
@Override
public float getProgress() throws IOException, InterruptedException {
// 獲取當前進度信息
return processed ? 1 : 0;
}
@Override
public void close() throws IOException {}
}
2.自定義InputFormat
key 和 value 的類型仍然爲 NullWritable,BytesWritable,表示 map 的輸入的 key 和value 的類型。
package top9_inputformat;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import java.io.IOException;
/**
* @author 曲健磊
* @date 2019-09-18 10:43:20
* @description 自定義的InputFormat,用於讀取輸入文件
*/
public class WholeFileInputFormat extends FileInputFormat<NullWritable, BytesWritable> {
@Override
protected boolean isSplitable(JobContext context, Path filename) {
// FileInputFormat用isSplitable方法來指定對應的文件是否支持數據的切分,默認情況下都是支持的,也就是true
// 返回false表示不可以切分,不可以劃分成多個切片,也就是說只有一個切片
return false;
}
@Override
public RecordReader<NullWritable, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
// 用來創建RecordReader讀取切片中的數據
WholeRecordReader recordReader = new WholeRecordReader();
// 初始化RecordReader
recordReader.initialize(split, context);
return recordReader;
}
}
3.Mapper:
package top9_inputformat;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
/**
* @author 曲健磊
* @date 2019-09-18 11:22:30
*/
public class SequenceFileMapper extends Mapper<NullWritable, BytesWritable, Text, BytesWritable> {
Text k = new Text();
// mapper初始化
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 1.獲取文件切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();
// 2.獲取切片文件名稱
String name = inputSplit.getPath().toString();
// 3.設置map輸出的key的值
k.set(name);
}
@Override
protected void map(NullWritable key, BytesWritable value, Context context) throws IOException, InterruptedException {
context.write(k, value);
}
}
4.Reducer:
package top9_inputformat;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* @author 曲健磊
* @date 2019-09-18 11:28:29
*/
public class SequenceFileReducer extends Reducer<Text, BytesWritable, Text, BytesWritable> {
@Override
protected void reduce(Text key, Iterable<BytesWritable> values, Context context) throws IOException, InterruptedException {
context.write(key, values.iterator().next());
}
}
5.Driver:
package top9_inputformat;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;
/**
* @author 曲健磊
* @date 2019-09-18 11:29:45
*/
public class SequenceFileDriver {
public static void main(String[] args) throws Exception {
args = new String[]{"d:/input", "d:/output"};
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(SequenceFileDriver.class);
job.setMapperClass(SequenceFileMapper.class);
job.setReducerClass(SequenceFileReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(BytesWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(BytesWritable.class);
// 設置輸入的inputFormat
job.setInputFormatClass(WholeFileInputFormat.class);
// 設置輸出的OutputFormat,輸出字節序列
job.setOutputFormatClass(SequenceFileOutputFormat.class);
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
job.waitForCompletion(true);
}
}
程序運行結果如下:
可以大體看出是把三個文件合併到了一起,實現了需求,那麼如何讀取這種 sequence file 呢?
敬請期待(偷笑.gif)!
關注我的微信公衆號(曲健磊的個人隨筆),觀看更多精彩內容: