一、自定义InputFormat
案例:小文件处理,需将多个小文件合并为一个SequenceFile文件,SequenceFile里面存储着多个文件,存储的形式为文件路径+名称为key,文件内容为value(BytesWritable)。
思路:
1、自定义一个InputFormat,继承FileInputFormat<NullWritable, BytesWritable>
(1)重写isSplitable()方法
@Override
protected boolean isSplitable(JobContext context, Path filename) {
return false;
}
(2)重写createRecordReader()方法,自定义RecordReader,继承RecordReader<NullWritable, BytesWritable>,并初始化
@Override
public RecordReader<NullWritable, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context)
throws IOException, InterruptedException {
// 1 定义一个自己的recordReader
WholeRecordReader recordReader = new WholeRecordReader();
// 2 初始化recordReader
recordReader.initialize(split, context);
return recordReader;
}
2、接上述(2)自定义RecordReader,重写initialize()、nextKeyValue()、getCurrentKey()、getCurrentValue()方法
import java.io.IOException;
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;
public class WholeRecordReader extends RecordReader<NullWritable, BytesWritable> {
private FileSplit split;
private Configuration configuration;
private BytesWritable value = new BytesWritable();
private boolean processed = false;
@Override
public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
// 获取传递过来的数据
this.split = (FileSplit) split;
configuration = context.getConfiguration();
}
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
if (!processed) {
// 1 定义缓存
byte[] contents = new byte[(int) split.getLength()];
// 2 获取文件系统
Path path = split.getPath();
FileSystem fs = path.getFileSystem(configuration);
// 3 读取内容
FSDataInputStream fis = null;
try {
// 3.1 打开输入流
fis = fs.open(path);
// 3.2 读取文件内容
IOUtils.readFully(fis, contents, 0, contents.length);
// 3.3 输出文件内容
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 {
return NullWritable.get();
}
@Override
public BytesWritable getCurrentValue() throws IOException, InterruptedException {
return value;
}
@Override
public float getProgress() throws IOException, InterruptedException {
return processed?1:0;
}
@Override
public void close() throws IOException {
}
}
3、自定义Mapper
★ 在hadoop的源码中,基类Mapper类和Reducer类中都是只包含四个方法:setup方法,cleanup方法,run方法,map方法。在run方法中调用了上面的三个方法:setup方法,map方法,cleanup方法。其中setup方法和cleanup方法默认是不做任何操作,且它们只被执行一次。但是setup方法一般会在map函数之前执行一些准备工作,如作业的一些配置信息等;cleanup方法则是在map方法运行完之后最后执行 的,该方法是完成一些结尾清理的工作,如:资源释放等。如果需要做一些配置和清理的工作,需要在Mapper/Reducer的子类中进行重写来实现相应的功能。map方法会在对应的子类中重新实现,就是我们自定义的map方法。该方法在一个while循环里面,表明该方法是执行很多次的。run方法就是每个maptask调用的方法。
这里,需要重写setup()方法和map()方法。
import java.io.IOException;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
/**
* 最终输出为SequenceFile类型,切片路径为key,文件内容为value
*/
public class WholeMapper extends Mapper<NullWritable, BytesWritable, Text, BytesWritable>{
Text k = new Text();
@Override
protected void setup( Context context) throws IOException, InterruptedException {
FileSplit split = (FileSplit) context.getInputSplit();
Path path = split.getPath();
k.set(path.toString());
}
@Override
protected void map(NullWritable key, BytesWritable value, Context context) throws IOException, InterruptedException {
context.write(k, value);
}
}
4、编写驱动程序
★ job.setOutputKeyClass和job.setOutputValueClas在默认情况下是同时设置map阶段和reduce阶段的输出,也就是说只有map和reduce输出是一样的时候才不会出问题。当map和reduce输出是不一样的时候就需要通过job.setMapOutputKeyClass和job.setMapOutputValueClas来设置map阶段的输出。
import java.io.IOException;
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;
public class WholeDriver {
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
job.setJarByClass(WholeDriver.class);
job.setInputFormatClass(WholeFileInputFormat.class);
job.setOutputFormatClass(SequenceFileOutputFormat.class);
job.setMapperClass(WholeMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(BytesWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(BytesWritable.class);
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
二、自定义Mapper
(见:一、3、自定义Mapper)
三、自定义Partitioner分区
Mapreduce中会将map输出的kv对,按照相同key分组,然后分发给不同的reducetask。默认的分发规则为:根据key的hashcode%reducetask数来分发。
1、自定义一个Partitioner继承Partitioner<K2,V2>
K2和V2 对应的是map输出kv类型,重写getPartition(K2 key, V2 value, int numPartitions) 方法。
2、在驱动函数中增加自定义数据分区设置和reduce task设置
// 8 指定自定义数据分区
job.setPartitionerClass(自定义的Partitioner.class);
// 9 同时指定相应数量的reduce task
job.setNumReduceTasks(reduce task的数量);
这里的reduce task的数量和分区数保持一致。
四、自定义Key.compareTo排序
需自定义bean对象,实现 WritableComparable 接口,重写compareTo方法。
★ 关于WritableComparable:
Interface WritableComparable
所有超级接口:
Comparable, Writable
所有已知的实现类:
BooleanWritable, BytesWritable, ByteWritable, DoubleWritable, FloatWritable, ID, ID, IntWritable, JobID, JobID, LongWritable, MD5Hash, NullWritable, Record, ShortWritable, TaskAttemptID, TaskAttemptID, TaskID, TaskID, Text, VIntWritable, VLongWritable
五、自定义Combiner
统计过程中对每一个maptask的输出进行局部汇总,以减小网络传输量即采用Combiner功能。Combiner运行在每一个MapTask中。
1、自定义一个Combiner类,继承Reducer<K1, V1, K1, V1>
K1和V1即为Map阶段的输出类型
2、驱动类中指定combiner
job.setCombinerClass(自定义的Combiner.class);
六、自定义Reducer
(继承Reducer)
七、自定义分组/GroupingComparator
和 “四、自定义Key.compareTo排序” 中 Map阶段自定义Bean的compareTo相对应,GroupingComparator为Reduce阶段的比较。
1、自定义一个GroupingComparator 继承 WritableComparator 类
★ 关于WritableComparator
所有实现的接口:
Comparator, Configurable, RawComparator
直接已知子类:
KeyFieldBasedComparator, RecordComparator
(1)定义空参构造
protected 自定义GroupingComparator() {
super(自定义Bean.class, true);
}
(2)重写compare(WritableComparable a, WritableComparable b)方法,比如将某个值一样的视为一组。
compare()返回值为int型,return one of -1, 0, or 1 according to whether the value of expression is negative, zero or positive。
八、map端表合并(Distributedcache)
适用于关联表中有小表的情形;
可以将小表分发到所有的map节点,这样,map节点就可以在本地对自己所读到的大表数据进行合并并输出最终结果,可以大大提高合并操作的并发度,加快处理速度。
1、先在驱动模块中添加缓存文件
// 加载缓存数据
job.addCacheFile(new URI("文件路径"));
2、读取缓存的文件数据,自定义Mapper
重写setup()方法(因为只需要获取一次文件)和map()方法,举例如下
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
public class DistributedCacheMapper extends Mapper<LongWritable, Text, Text, NullWritable>{
Map<String, String> pdMap = new HashMap<>();
@Override
protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.Context context)
throws IOException, InterruptedException {
// 1 获取缓存的文件
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("pd.txt")));
String line;
while(StringUtils.isNotEmpty(line = reader.readLine())){
// 2 切割
String[] fields = line.split("\t");
// 3 缓存数据到集合
pdMap.put(fields[0], fields[1]);
}
// 4 关流
reader.close();
}
Text k = new Text();
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
// 1 获取一行
String line = value.toString();
// 2 截取
String[] fields = line.split("\t");
// 3 获取订单id
String orderId = fields[1];
// 4 获取商品名称
String pdName = pdMap.get(orderId);
// 5 拼接
k.set(line + "\t"+ pdName);
// 6 写出
context.write(k, NullWritable.get());
}
}
九、自定义OutputFormat
1、自定义一个outputformat,继承 FileOutputFormat<K1, V1>
K1和V1是最终要输出的类型
同时,重写getRecordWriter(TaskAttemptContext job)方法,并返回一个自定义的RecordWriter
2、自定义RecordWriter,继承RecordWriter<K1, V1>
执行具体些数据的业务,如
import java.io.IOException;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
public class FilterRecordWriter extends RecordWriter<Text, NullWritable> {
FSDataOutputStream atguiguOut = null;
FSDataOutputStream otherOut = null;
public FilterRecordWriter(TaskAttemptContext job) {
// 1 获取文件系统
FileSystem fs;
try {
fs = FileSystem.get(job.getConfiguration());
// 2 创建输出文件路径
Path atguiguPath = new Path("e:/atguigu.log");
Path otherPath = new Path("e:/other.log");
// 3 创建输出流
atguiguOut = fs.create(atguiguPath);
otherOut = fs.create(otherPath);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void write(Text key, NullWritable value) throws IOException, InterruptedException {
// 判断是否包含“atguigu”输出到不同文件
if (key.toString().contains("atguigu")) {
atguiguOut.write(key.toString().getBytes());
} else {
otherOut.write(key.toString().getBytes());
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
// 关闭资源
if (atguiguOut != null) {
atguiguOut.close();
}
if (otherOut != null) {
otherOut.close();
}
}
}
3、编写Mapper和Reducer
4、编写Driver
// 要将自定义的输出格式组件设置到job中
job.setOutputFormatClass(自定义的OutputFormat.class);
需要注意,虽然我们自定义了outputformat,但是因为我们的outputformat继承自fileoutputformat,而fileoutputformat要输出一个_SUCCESS文件,所以,在这还得指定一个输出目录
FileOutputFormat.setOutputPath(job, new Path(args[1]));