在一些特殊情况下,我们会自定义一些MapReduce中的组件来满足自己的需求,比如自定义的Partition就是很好的例子。
1.1 自定义InputFormat
在Hadoop系统中自带了一些常用的InputFormat,我们可直接使用,如下:
- FileInputFormat<K,V>这个是基本的文件输入父类。
- TextInputFormat<LongWritable,Text>这个是默认的数据格式类,我们一般编程,如果没有特别指定的话,一般都使用的是这个;key代表当前行数据距离文件开始的偏移量,value为当前行字符串。
- SequenceFileInputFormat<K,V>这个是序列文件输入格式,使用序列文件可以提高效率,但是不利于查看结果,建议在过程中使用序列文件,最后展示可以使用可视化输出。
- KeyValueTextInputFormat<Text,Text>这个是读取以Tab(也即是\t)分隔的数据,每行数据如果以\t分隔,那么使用这个读入,就可以自动把\t前面的当做key,后面的当做value。
- CombineFileInputFormat<K,V>合并大量小数据是使用,比如mapreduce已经实现了一个org.apache.hadoop.mapreduce.lib.input.CombineTextInputFormat
- MultipleInputs 多种输入,可以为每个输入指定逻辑处理的Mapper。
在这儿,作者就不一一介绍了,有兴趣可以自己去尝试每个类型。下面我会介绍如何通过FileInputFormat来实现自定义的InputFormat。
实现FileInputFormat类,必须要实现createRecordReader方法。代码如下:
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.FileInputFormat;
import java.io.IOException;
public class CustomInputFormat extends FileInputFormat<Text, Text> {
@Override
public RecordReader<Text,Text> createRecordReader(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
// 自定义代码
return new CustomReader();
}
}
实现createRecordReader的关键就是返回一个自定义的RecordReader,如何实现自定义的RecordReader,请看下面的代码:
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.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.FileSplit;
import org.apache.hadoop.util.LineReader;
import java.io.IOException;
public class CustomReader extends RecordReader<Text ,Text> {
private LineReader lr ;
private Text key = new Text();
private Text value = new Text();
private long start ;
private long end;
private long currentPos;
private Text line = new Text();
@Override
public void initialize(InputSplit inputSplit, TaskAttemptContext taskAttemptContext)
throws IOException {
//输入分片的文件对象
FileSplit split =(FileSplit) inputSplit;
Configuration conf = taskAttemptContext.getConfiguration();
Path path = split.getPath();
FileSystem fs = path.getFileSystem(conf);
FSDataInputStream is = fs.open(path);
lr = new LineReader(is,conf);
//分片,起始位置
start =split.getStart();
//分片,终点位置
end = start + split.getLength();
is.seek(start);
if(start != 0){
//如果不是开始的分片,默认跳过第一行
start += lr.readLine(new Text(),0,
(int)Math.min(Integer.MAX_VALUE, end-start));
}
currentPos = start;
}
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
//currentPos=end,会继续读取下一行,currentPos > end则返回false
if(currentPos > end){
return false;
}
// 读取一行数据
currentPos += lr.readLine(line);
if(line.getLength()==0){
return false;
}
if(line.toString().startsWith("ignore")){
currentPos += lr.readLine(line);
}
// 转化为word数组
String [] words = line.toString().split(",");
// 异常处理
if(words.length < 2){
System.err.println("line:"+line.toString()+".");
return false;
}
key.set(words[0]);
value.set(words[1]);
return true;
}
@Override
public Text getCurrentKey() throws IOException, InterruptedException {
return key;
}
@Override
public Text getCurrentValue() throws IOException, InterruptedException {
return value;
}
@Override
public float getProgress() throws IOException, InterruptedException {
if (start == end) {
return 0.0f;
} else {
return Math.min(1.0f, (currentPos - start) / (float) (end - start));
}
}
@Override
public void close() throws IOException {
lr.close();
}
}
上面的代码就是实现一个自定义的RecordReader,核心的方法initialize,nextKeyValue。initialize用于实现一些初始化操作,nextKeyValue主要实现读取下一行记录的,转化成<key,value>形式。
这个自定义的CustomInputFormat 实现的功能就是:如果一行记录开始“ignore”则跳过这行。定义完成后就可以像其他系统自带的InputFormat一样正常使用。
1.2 Partitioner和Combiner
除了自定义InputFormat外,常见的自定义的组件还有partitioner和combiner,其中partitioner我们已经在4.1章节中使用过了,它的作用是自定义数据记录的分区的方式,而不用系统自带的分区方式。Hadoop自带的分区实现是HashPartitioner,其实现如下:
/** Partition keys by their {@link Object#hashCode()}. */
public class HashPartitioner<K, V> extends Partitioner<K, V> {
/** Use {@link Object#hashCode()} to partition. */
public int getPartition(K key, V value,int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
实现原始是根据key的hashCode来和numReduceTasks取模得到分区的,所以分区依赖于具体的key。具体的自定义见4.1.3节的SalaryPartitioner.java实现这里不多举例子了。另外一个非常有用的特性就是Combiner,关于combiner有两点需要注意的。
第一,与mapper和reducer不同的是,combiner没有默认的实现,需要显式的设置在conf中才有作用。
第二,并不是所有的job都适用combiner,只有操作满足结合律的才可设置combiner。combine操作类似于:opt(opt(1, 2, 3), opt(4, 5, 6))。如果opt为求和、求最大值,求最小值的话,可以使用,但是如果是求中值的话,不适用。
combine其本质是reduce,只是执行的阶段不同。使用Combiner的原因,因为map输出的记录需要shuffle和跨机器网络传输,如果使用Combiner可以在Map端先对单个map输出的数据进行一个局部的聚合,可以减少网络传输的数据量,从而提高性能。
下面我们实现一个word count的combiner,主要就是对map端输出的word进行统计,代码如下:
public class WordcountCombiner extends Reducer<Text, IntWritable, Text, IntWritable>{
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
int count = 0;
for(IntWritable v :values){
//对同一个map输出的k,v对进行按k进行一次汇总。不同map的k,v汇总必须要用reduce方法
count += v.get();
}
context.write(key, new IntWritable(count));
}
}
在job定义中配置:
job.setCombinerClass(WordcountCombiner.class);
最终可以达到Map端聚合的效果,提高性能。
1.3 自定义输出RecordeWriter
我们有时也会对Reducer输出的自定义,比如需要把数据写入第三方的数据库,这个时候需要自定义MapReducer输出,这个功能也是经常使用的。
首先要定义一个OutputFormat需要实现三个方法:
RecordWriter getRecordWriter(TaskAttemptContext context);
void checkOutputSpecs(JobContext context);
OutputCommitter getOutputCommitter(TaskAttemptContext context);
1.getRecordWriter
用于返回一个RecordWriter的实例,Reduce任务在执行的时候就是利用这个实例来输出Key/Value的。(如果Job不需要Reduce,那么Map任务会直接使用这个实例来进行输出。
2. heckOutputSpecs
在JobClient提交Job之前被调用的(在使用InputFomat进行输入数据划分之前),用于检测Job的输出路径。比如,FileOutputFormat通过这个方法来确认在Job开始之前,Job的Output路径并不存在,然后该方法又会重新创建这个Output 路径。这样一来,就能确保Job结束后,Output路径下的东西就是且仅是该Job输出的。
3.getOutputCommitter
用于返回一个OutputCommitter的实例。OutputCommitter用于控制Job的输出环境,它有下面几个方法:
void setupJob(JobContext jobContext);
void commitJob(JobContext jobContext);
void abortJob(JobContext jobContext, JobStatus.State state);
void setupTask(TaskAttemptContext taskContext);
boolean needsTaskCommit(TaskAttemptContext taskContext);
void commitTask(TaskAttemptContext taskContext);
void abortTask(TaskAttemptContext taskContext);
其中每个方法的功能如下:
- setupJob - mkdir ${mapred.output.dir}/_temporary
- commitJob - touch ${mapred.output.dir}/_SUCCESS && rm -r ${mapred.output.dir}/_temporary
- abortJob - rm -r ${mapred.output.dir}/_temporary
- setupTask - <nothing>
- needsTaskCommit - test -d ${mapred.output.dir}/_temporary/_${TaskAttemptID}
- commitTask - mv ${mapred.output.dir}/_temporary/_${TaskAttemptID}/* ${mapred.output.dir}/
- abortTask - rm -r ${mapred.output.dir}/_temporary/_${TaskAttemptID}
自定义OutputFormat代码如下:
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.*;
import org.apache.hadoop.mapreduce.lib.output.FileOutputCommitter;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class DBOutputFormat extends OutputFormat<Text, Text> {
@Override
public RecordWriter<Text, Text> getRecordWriter(TaskAttemptContext taskAttemptContext)
throws IOException, InterruptedException {
return new SqlRecordWriter();
}
@Override
public void checkOutputSpecs(JobContext jobContext)
throws IOException, InterruptedException {
}
@Override
public OutputCommitter getOutputCommitter(TaskAttemptContext context)
throws IOException, InterruptedException {
// 文件committer
return new FileOutputCommitter (FileOutputFormat.getOutputPath(context), context);
}
}
SqlRecordWriter自定义实现,最主要的就是write()方法,在这个方法中,我们可以把自己的输出逻辑放到这个方法内,另外就是close()方法主要用于一些善后操作。具体的代码如下:
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import java.io.IOException;
import java.sql.SQLException;
public class SqlRecordWriter extends RecordWriter<Text, Text> {
@Override
public void write(Text key, Text value) throws IOException, InterruptedException {
String data=key.toString();
String num=value.toString();
Dao dao=new Dao();
try {
// 把结果输出到mysql中
dao.insertData(data, num);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
//关闭数据库连接
}
}
job定义中设置自定义的OutputFormat
job.setOutputFormatClass(DBOutputFormat.class);