MapReduce解析
I/O序列化
序列化 serialization就是将结构化的对象转为字节流的过程,以便在网络上传输或者写入磁盘进行永久存储。
反序列化 deserialization是序列化的逆过程,将字节流转换回结构化对象。
序列化和反序列化的主要应用是进程间的通信和持久化存储。
在Hadoop集群中,多节点之间的通信时通过远程过程调用RPC协议完成的。RPC协议将消息序列化成二进制RPC对序列化有如下要求:
1)紧凑:紧凑格式能充分利用网络带宽
2)快速:进程间通信形成了分布式系统的骨架,所有需要尽量减少序列化和反序列化的性能开销
3)可拓展性:为了满足新的需求,通信协议在不断变化,在控制客户端和服务器的过程中,需要直接引入新的协议,因此序列化必须满足可拓展的要求。
4)支持互操作:对于某些系统来说,希望能支持以不同编程语言编写的客户端与服务器交互,所以需要设计一种特定的格式来满足这一需求。
Hadoop并没有采用Java的序列化机制,而是引入了Writable接口,建立了自己的序列化机制,具有紧凑、速度快的特点,但不太容易用Java以为的编程语言去扩展。
不用Java的序列化机制的原因是因为
java的序列化是一个重量级序列化框架,一个对象被序列化后,会会带很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输。所以,Hadoop自己开发了一套序列化机制。
常用数据序列化类型
Java类型 | Hadoop Writable类型 |
---|---|
Boolean | BooleanWritable |
Byte | BtyeWritable |
Int | IntWritable |
Float | FloatWritable |
Long | LongWritable |
Double | DoubleWritable |
String | Text |
Map | MapWritable |
Array | ArrayWritable |
Writable接口
Writable接口
Writable接口是基于DataInput和DataOutput实现的序列化协议。MapReduce中的key和value必须是可序列化的,也就是说,key和value要求是实现了Writable接口的对象。key还要求必须实现WritableComparable接口,以便进行排序。
package org.apache.hadoop.io;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.classification.InterfaceAudience.Public;
import org.apache.hadoop.classification.InterfaceStability.Stable;
@Public
@Stable
public interface Writable {
void write(DataOutput out) throws IOException;
void readFields(DataInput in) throws IOException;
}
write方法的功能是将对象写入二进制流Dataoutput,readFields的功能是从二进制流DataInput读取对象。参数中的DataInput和DataOutput是java.io包中定义的接口,分别用来表示二进制流的输入和输出。
WritableComparable接口
WritableComparable接口继承自Writable接口和java.lang.Comparable接口,是可序列化并且可比较的接口
package org.apache.hadoop.io;
import org.apache.hadoop.classification.InterfaceAudience.Public;
import org.apache.hadoop.classification.InterfaceStability.Stable;
@Public
@Stable
public interface WritableComparable<T> extends Writable, Comparable<T> {
}
由于继承自Writable,所以WritableComparable是可序列化的,需要实现write()和readFields()这两个序列化和反序列化方法。WritableComparable由于继承了Comparable接口,所以其也是可比较的,还需要compareTo()方法。
package java.lang;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
WritableComparator类
Java的RewComparator接口允许比较从流中读取的未被反序列化为对象的记录,省去创建对象的所有开销,从而更有效率。WritableComparator是继承自WritableComparable类的RewComparator类的通用实现,提供了两个重要的功能。
1)提供了对原始compare()方法的一个默认实现,该方法能够反序列化在流中比较的对象,调用对象的compare()方法进行比较。
2)它充当RawCompatator实例的一个工厂(已注册Writable的实现)。
例如,获取一个IntWritable的comparator,可以直接调用其get方法。
RawComparator<IntWritable>comparator=WritableComparator.get(IntWritable.class);
这个comparator可以比较两个IntWritable对象
package com.ex.mapreduce.wordcount;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.RawComparator;
import org.apache.hadoop.io.WritableComparator;
public class Text {
public static void main(String[] args){
WritableComparator comparator= WritableComparator.get(IntWritable.class);
IntWritable w1=new IntWritable(163);
IntWritable w2=new IntWritable(67);
System.out.println(comparator.compare(w1,w2));
}
}
这里因为163大于67所以返回1。
compatator直接比较序列化后的对象的实现代码,不需要再将数据流反序列化为对象,从而避免了额外的开销。
package com.ex.mapreduce.wordcount;
import com.fasterxml.jackson.databind.ObjectWriter;
import org.apache.commons.lang.SerializationUtils;
import org.apache.hadoop.io.*;
import org.apache.hadoop.io.Text;
import org.apache.zookeeper.server.util.SerializeUtils;
import java.io.*;
public class TextWritable {
public static void byteCompareTest() throws IOException {
WritableComparator comparator= WritableComparator.get(IntWritable.class);
IntWritable w1=new IntWritable(163);
IntWritable w2=new IntWritable(67);
byte[] b1= serialize(w1);
byte[] b2=serialize(w2);
System.out.println(comparator.compare(b1,0,b1.length,b2,0,b2.length));
}
public static byte[] serialize(Writable writable) throws IOException{
ByteArrayOutputStream out=new ByteArrayOutputStream();
DataOutputStream dataOut=new DataOutputStream(out);
writable.write(dataOut);
dataOut.close();
return out.toByteArray();
}
//反序列化
public static byte[] deserialize(Writable writable,byte[]bytes) throws IOException{
ByteArrayInputStream in=new ByteArrayInputStream(bytes);
DataInputStream dataIn=new DataInputStream(in);
writable.readFields(dataIn);
dataIn.close();
return bytes;
}
}
WritableComparable排序(接口)
首先,WritableComparable是Hadoop的排序方式之一,而排序是MapReduce框架中最重要的操作之一,它就是用来给数据排序的(按照Key排好),常发生在MapTask与ReduceTask的传输过程中(就是数据从map方法写到reduce方法之间)
任何应用程序中的数据均会被排序,不管逻辑上是否需要,都排序
MapTask和ReduceTask均会对数据进行排序,此操作属于Hadoop的默认行为
默认行为是按照字典顺序排序,且实现该排序的方法是快速排序,例如环形缓冲区中将数据写入分区后会进行区内的局部排序,使用的就是快排。
WritableComparator
它是用来给Key分组的
它在ReduceTask中进行,默认的类型是GroupingComparator也可以自定义
WritableComparator为辅助排序手段提供基础(继承它),用来应对不同的业务需求。
比如GroupingComparator会ReduceTask将文件写入磁盘并排序后按照Key进行分组,判断下一个key是否相同,将同组的key传给reduce()执行。
Writable封装类
Java基本类型的Writable封装器
Writable类提供了除char类型以外的Java基本类型的封装。所有的类都可通过get()和set()方法进行读取或者存储封装的值。
Java基本类型 | Writable类 | 序列化大小(byte) |
---|---|---|
布尔型 | BooleanWritable | 1 |
字节型 | ByteWritable | 1 |
短整型 | ShortWritable | 2 |
整形 | IntWritable | 4 |
整形(可变长度) | VIntWritable | 1-5 |
浮点型 | FloatWritable | 4 |
长整型 | LongWritable | 8 |
长整型(可变长度) | VlongWritable | 1-9 |
双精度浮点型 | DoubleWritable | 8 |
进行整数编码时,可以有两种选择:
- 定长格式(IntWritable和LongWritable)
- 变长格式(VIntWritable和VlongWritable)
定长格式适合对整个值域空间中分布均匀的数值进行编码,例如阿希方法等。
对于数值变量分布不均匀的,采用变长格式更加节省空间。
Text类型
Text类使用修订的标准UTF-8编码来存储文本,它提供了在字节级别上序列化、反序列化和比较文本的方法,基本可以看作Java的String类的Writable封装。Text类的前1-4个字节,采用变长整形来存储字符串编码中所需要的字节数,所以Text类型的最大存储为2G。Text可以方便地其他更够理解UTF-8编码地工具交互。Text类与JavaString 类在检索、Unicode和迭代等方面是有差别地。
1)charAt方法
int charAt(int position)
Text的chatAt返回的是一个表示Unicode编码位置的整型值。当position不在范围之内时返回-1。
2)find方法
int find(String what)
int find(String what,int start)
Text类型中的find方法返回字节偏移量,当查找内容不存在时返回-1。而String类的indexOf方法返回char编码单元中的索引位置。
3)set方法
void set(String string)
void set(byte[] utf8)
给Text对象设置值
4)decode方法
static String decode(byte[] utf8)
static String decode(byte[] utf8,int start,int length)
static String decode(byte[] utf8,int start,int length,boolean replace)
decode方法的功能是将UTF-8编码的字节数组转化为字符串
5)encode方法
static ByteBuffer encode(String string)
static ByteBuffer encode(String string,boolean replace)
将字符串转换为UTF-8编码的字节数组
6)String toString()
返回值对应的字符串。Text类不像Java String类有丰富的字符串操作API,在很多情况下需要将Text对象转化成String对象,这可以通过调用toString()方法来实现。
7)byte[] getBytes()
返回对应的字节数组。
8)int getLength()
返回字节数组里字节的数量。
Text方法的使用示例
public static void textTest(){
Text text=new Text("\u0041\u00DF\u6C49");
System.out.println(text.toString());
System.out.println(text.getLength());
System.out.println(text.charAt(0));
System.out.println(text.charAt(1));
System.out.println(text.charAt(2));
System.out.println(text.charAt(3));
System.out.println(text.find("\u00DF"));
System.out.println(text.find("\u6C49"));
}
字符串对应的UTF-8编码长度6(6=1+2+3)。text.charAt(0)、text.charAt(1)和text.charAt(3)返回所在位置字符的Unicode编码的整型值,分别是65,223和27721。由于text.charAt(2)指定的位置2错误,所以返回-1。text.find("\u00DF")返回字符\u00DF所在位置偏移量1,而text.find("\u6C49")偏移量为3。
ByteWritable
ByteWritable是对二进制数组的封装。它的序列化格式以一个4个字节的整数作为开始,表示数据的长度,也就是字符串本身。例如,长度为3的字节数组包含7、8、9,则其序列化形式为一个4字节的整数(00000003)和随后3个字节(07、08、09)。
ByteWritable是可变的,其值可以使用set(byte[] newData,int offset,int length)方法修改;toString()方法转换为十六进制并以空格分开;可以通过setCapacity()方法设置容量;getLength()方法返回对象的实际数据长度;getBytes().length返回字节数组的长度,即该对象的当前容量。
public static void byteTest(){
BytesWritable b=new BytesWritable("ABCD".getBytes());
System.out.println(b.toString());
System.out.println(b.getLength());
System.out.println(b.getBytes().length);
b.setCapacity(10);
System.out.println(b.toString());
System.out.println(b.getLength());
System.out.println(b.getBytes().length);
b.setCapacity(2);
System.out.println(b.toString());
System.out.println(b.getLength());
System.out.println(b.getBytes().length);
}
NullWritable
NullWritable的序列化长度为0,不包含任何字符,它仅仅充当占位符的角色,不会从数据流中读取和写出数据。例如,Map或Reduce阶段的输出,当key或value不需要输出时,就可以将其设置为NullWritable,一般调用NullWritable.get()来获取NullWritable类的实例。
ObjectWritable和GenericWritable
当一个字段中包含多种类型的数据时,就可以采用ObjectWritable进行封装,ObjectWritable是对String、Enum、Writable、null等类型的一种通用封装。ObjectWritable在RPC中用于方法的参数和返回类型的封装和解封类。
public static void objectTest(){
ObjectWritable o= new ObjectWritable("ABCD");
System.out.println(o.toString());
Class c=o.getDeclaredClass();
System.out.println(c);
String s=(String) o.get();
System.out.println(s);
}
ObjectWritable每次进行序列化时,要写入封装类型的名称以及保存封装之前的类型,这样势必会占用很大的空间,造成资源浪费。针对这种不足,MapReduce又提供了GenericWritable类。它的机制是当封装类型数量比较小,并且可以提取知道的情况下,可以使用静态类型的数组,通过使用对序列化后的类型的引用来提升性能。GenericWritable的用法是,继承这个类,然后把要输出value的Writable类型加进它的Class静态变量里。
public static void genericWritable(){
MyGenericWritable m=new MyGenericWritable(new Text("ABCD"));
System.out.println(m.toString());
Class c=m.get().getClass();
System.out.println(c);
Text t=(Text)m.get();
System.out.println(t);
}
Writable集合类
在org.apache.hadoop.io包中含有6个Writable集合类,分别是ArrayWritable,TwoDArrayWritable、ArrayPrimitiveWritable、MapWritable、SortedMapWritable以及EnumMapWritable。
ArrayWritable表示数组的Writable类型,TwoDArrayWritable表示二维数组的Writable类型。这两个类中所包含的元素必须是同一类型。数组的类型可以在构造方法里设置
public static void arrayWritable(){
ArrayWritable a=new ArrayWritable(Text.class);
a.set(new Writable[]{new Text("ABCD"),new Text("1234")});
for(Writable t:a.get()){
System.out.println(t);
}
System.out.println(a.toStrings());
}
public static void twoDArrayWritable(){
TwoDArrayWritable t=new TwoDArrayWritable(Text.class);
t.set(new Writable[][]{{new Text("ABCD"),new Text("EFG")},{new Text("1234"),new Text("567")}});
for(Writable[] one:t.get()){
for(Writable a:one){
System.out.print(a.toString()+' ');
}
System.out.println();
}
System.out.println(t.toString());
}
ArrayWritable和TwoDArrayWritable这两个类都有get()、set()和toArray()方法。toArray()方法用于创建数组的浅副本,不会为每个数组元素产生新的对象,也不构成底层数组的副本。
ArrayPrimtiveWritable是一个封装类,是对Java基本类型的数组(int[]、long[]等)的Writable类型的封装。调用set方法时,可以识别组件类型,无须像ArrayWritable那样通过继承该类来设置类型。
public static void arrayPrimitiveWritableText(){
ArrayPrimitiveWritable a=new ArrayPrimitiveWritable();
int[] i={123,456};
a.set(i);
for(Object l:(int[])a.get()){
System.out.println(l);
}
}
MapWritable实现了java.util.Map<Writable,Writable>,SortedMapWritable实现了java.util.SortedMap<WritableComparable,Writable>。上述每个<key,value>字段使用的类型是相应字段序列化的一部分。
public static void mapWritableTest(){
MapWritable m=new MapWritable();
HashMap<Text,IntWritable> h=new HashMap<Text,IntWritable>();
h.put(new Text("123"),new IntWritable(123));
System.out.println(h.keySet());
m.putAll(h);
m.put(new Text("456"),new IntWritable(456));
System.out.println(m.keySet());
}
SequenceFile和MapFile
SequenceFile
SequenceFile是用来存储二进制的键值对的一种平面文本存储文件,在SequenceFile中,每个键值对都被视为一条记录,每条记录是可序列化的字符数组。
SequenceFile存储格式
一个SequenceFile由Header后跟多条记录组成,多条记录后有同步标记Sync。
Header组成部分依次是,version(SequenceFile文件的前三个字母为SEQ,然后是1个字节的版本号),KeyClassName(Key是类名),valueClass(值得类名),compression/compressed(指定键值对是否压缩得布尔值),BlockCompression/blockcompressed(指定键值对是否进行块压缩得布尔值),Compressioncodec/compressclass(启动压缩时指定键值对编解码器得类),metadata/meta(用户操作文件得元数据信息)以及同步标记Sync(用于快速定位到记录得边界)。
每条Record使用键值对得方式进行存储。Record在是否启用压缩及使用不同压缩格式时,Record存储格式是不同得。在SequenceFile中,由CompressionType指定压缩状态,SequenceFile提供了三种格式得压缩状态,分别是Uncompressed、RecordCompressWriter和BlockCompressWriter。
Uncompressed
Uncompressed即未进行压缩得状态
Record由4部分组成Recordlength(记录长度,占4个字节),Keylength(key长度,占4个字节),Key(键)和Value(值)。
RecordCompressWriter
RecordCompress即记录压缩,对每一条记录得value值进行了压缩,key不压缩。
记录得组成与无压缩基本相同,Recordlength(记录长度,占4个字节),Keylength(key长度,占4个字节)。不同的是,值是经定义在Header的编码器来压缩的。
BlockCompressWriter
BlockCompress即块压缩,将一连串的Record组织在一起,一次压缩多个记录,而不是在记录级别压缩,因此压缩程度更容易达到理想状态,一般优先选择。Block大小的最小值由io.seqfile.compress.blocksize
属性指定,默认值是1 000 000字节。
BlockCompress格式结构组成中依次为,块中记录数、每条记录Key长度的集合、每条记录Key值得集合、每条记录Value长度得集合、每条记录Value值得集合。
写入SequenceFile
在进行SequenceFile的写操作时,使用createWriter()方法来返回一个SequenceFile.Writer()实例,在返回SequenceFile.Writer之后,即可以使用append()方法来写入键值对,并在结束后调用close()方法关闭数据流。值得注意的是,键和值并不一定是Writable类型,任何经过Serialzetion类实现序列化和反序列化的类型都可以使用
SequenceFile的写操作
package com.ex.mapreduce.wordcount;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.SequenceFile;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.compress.DefaultCodec;
import java.io.IOException;
import java.net.URI;
public class SequenceFileTest {
private static final String[] data={"one,a,cat","two,b,dog","three,c,pig","four,d,bear"};
public static void sequenceFileWriteTest() throws IOException {
String uri1="file:///D:/Non.txt";
Configuration conf=new Configuration();
FileSystem fs1=FileSystem.get(URI.create(uri1),conf);
Path path1=new Path(uri1);
SequenceFile.Writer writer1=null;
String uri2="file:///D:/Block.txt";
FileSystem fs2=FileSystem.get(URI.create(uri2),conf);
Path path2=new Path(uri2);
SequenceFile.Writer writer2=null;
String uri3="file:///D:/Record.txt";
FileSystem fs3=FileSystem.get(URI.create(uri3),conf);
Path path3=new Path(uri3);
SequenceFile.Writer writer3=null;
IntWritable key=new IntWritable();
Text value=new Text();
try{
writer1=SequenceFile.createWriter(fs1,conf,path1,IntWritable.class,Text.class);
writer2=SequenceFile.createWriter(fs2,conf,path2,IntWritable.class,Text.class,SequenceFile.CompressionType.BLOCK,new DefaultCodec());
writer3=SequenceFile.createWriter(fs3,conf,path3,IntWritable.class,Text.class,SequenceFile.CompressionType.RECORD,new DefaultCodec());
for(int i=0;i<80;i++){
key.set(80-i);
value.set(data[i%data.length]);
writer1.append(key,value);
writer2.append(key,value);
writer3.append(key,value);
}
}finally {
IOUtils.closeStream(writer1);
IOUtils.closeStream(writer2);
IOUtils.closeStream(writer3);
}
System.out.println("write success");
}
}
可以看到Block压缩的效率比Record明显更高,仅占用了377字节,之所以有这么高的压缩效率,是因为我们的键值对重复率非常高,可以高效的压缩。
因为我们value是一个非常简单的字符串,所以压缩并没有起到作用。
SequenceFile的读操作
public static void sequenceFileReadTest() throws IOException {
String uri="file:///D:/Non.txt";
Configuration conf=new Configuration();
FileSystem fs=FileSystem.get(URI.create(uri),conf);
Path path=new Path(uri);
SequenceFile.Reader reader=null;
try{
reader=new SequenceFile.Reader(fs,path,conf);
Writable key=(Writable) ReflectionUtils.newInstance(reader.getKeyClass(),conf);
Writable value=(Writable)ReflectionUtils.newInstance(reader.getValueClass(),conf);
long position=reader.getPosition();
while(reader.next(key,value)){
String syncSeen=reader.syncSeen()?"*":"";
System.out.printf("[%s%s]\t%s\t%s\n",position,syncSeen,key,value);
position=reader.getPosition();
}
}finally {
IOUtils.closeStream(reader);
}
System.out.println("read success");
}
MapFile
MapFile是排序后的SequenceFile,MapFile由Data和Index两部分组成。其中Index是文件索引,用于记录每个Record的key值以及Record在文件中的偏移位置,Data文件则用来存储索引对应的数据。在进行MapFile读取时,首先将Index文件读入内存,接着对内存中的索引进行查找,找到索引的键并找到所对应的值,最后从Data文件中读取相应的数据。MapFile文件为了保证key-value的有序,在每一次写入时对key-value进行检查,当写入的key-value不符合顺序时对key-value进行检查,当写入的key-value不符合顺序时就会报错。
MapFile的读写操作与SequenceFile的读写操作十分相似。在进行MapFile的写操作时,首先需要新建一个MapFile.Writer实例,然后调用append()方法顺序写入文件内容。键必须是WritableComparable类型的实例,值必须是Writable类型的实例。
在进行MapFile的读操作时,则需要首先创建一个MapFile.Reader实例,然后调用next()方法循环读取,直到读取完毕。
public static void mapFileWriterTest() throws IOException {
String uri="file:///D:/Map";
Configuration conf=new Configuration();
FileSystem fs=FileSystem.get(conf);
IntWritable key=new IntWritable();
Text value=new Text();
MapFile.Writer writer=null;
writer = new MapFile.Writer(fs.getConf(),new Path(uri),MapFile.Writer.keyClass(key.getClass()),MapFile.Writer.valueClass(value.getClass()));
key.set(1);
value.set("1");
writer.append(key,value);
key.set(2);
value.set("2");
writer.append(key,value);
IOUtils.closeStream(writer);
System.out.println("write success");
}
public static void mapFileReadTest() throws IOException {
String uri="file:///D:/Map";
Configuration conf=new Configuration();
FileSystem fs=FileSystem.get(URI.create(uri),conf);
IntWritable key=new IntWritable();
Text value=new Text();
MapFile.Reader reader=null;
try {
reader=new MapFile.Reader(fs,uri,conf);
while(reader.next(key,value)){
System.out.printf("%s\t%s\n",key,value);
}
}finally {
IOUtils.closeStream(reader);
}
System.out.println("read success");
}
MapReduce的输入和输出
输入分片InputSplit
InputSplit表示由单个Map任务处理的数据,每个Map任务处理一个InputSplit。每个InputSplit会被划分为若干记录,每个记录就是一个键值对,Map逐条处理每条记录。输入分片只是一种逻辑上的概念,没有对文件进行物理切割。
package org.apache.hadoop.mapred;
import java.io.IOException;
import org.apache.hadoop.classification.InterfaceAudience.Public;
import org.apache.hadoop.classification.InterfaceStability.Stable;
import org.apache.hadoop.io.Writable;
@Public
@Stable
public interface InputSplit extends Writable {
long getLength() throws IOException;
String[] getLocations() throws IOException;
}
InputSplit包含以字节为单位的分片长度信息,还包含一组存储位置信息(即一组主机名)。InputSplit不包含数据本身,只含有指向数据的引入,由一个字节为单位的长度和一组存储位置组成。MapReduce计算框架根据存储位置的信息,将Map任务尽可能的放置在分片数据的附近。同时,MapReduce会根据分片的大小对其进行排序,优先处理较大的分片,从而优化运行时间。
InputSplit类有三个子类继承:
FileSplit(文件输入分片)、CombineFileSplit(多文件输入分片)、DBInputSplit(数据块输入分片)。
FileSplit是默认的InputSplit。
InputFormat类
InputFormat负责产生输入分片InputSplit,并将它们分隔成记录,InputFormat的作用:
1)验证作用的输入规范。
2)将输入文件切分成逻辑的InputSplit,然后将每个分片分配给一个单独的Mapper处理。
3)提供RecordReader实现,用于从逻辑InputSplit中收集输入记录,以供Mapper处理。
package org.apache.hadoop.mapred;
import java.io.IOException;
import org.apache.hadoop.classification.InterfaceAudience.Public;
import org.apache.hadoop.classification.InterfaceStability.Stable;
@Public
@Stable
public interface InputFormat<K, V> {
InputSplit[] getSplits(JobConf var1, int var2) throws IOException;
RecordReader<K, V> getRecordReader(InputSplit var1, JobConf var2, Reporter var3) throws IOException;
}
首先客户端使用getSplits()方法计算分片,提交到JobTracker。JobTracker使用其存储位置信息来调度Map任务,从而TaskTracker处理这些分片。在TaskTracker上,Map任务把输入分片传给InputFormat的getRecordReader()方法来获得该分片的RecordReader。RecordReader就像记录上的迭代器,Map任务用RecordReader来生成记录的<key,value>键值对,然后再传递给map()方法进行处理。
文件输入
FileInputFormat类
FileInputFormat是基于文件为数据源的InputFormat实现的基类,其主要实现两个功能,一个是指出作业输入文件的位置,另一个是输入文件生成分片的实现代码端,即它为各种InputFormat提供了一个统一的getSplits实现,而把InputSplit切割成记录的功能交由其子类完成。
FileInputFormat提供了如下4种静态方法来设定作业的输入路径。这对MapReduce作业指定的输入文件提供了很强的灵活性。输入路径指定方法如下
1)public static void addInputPath(Job job,Path path);
为作业添加一个path
2)public static void addInputPaths(JobConf conf, String commaSeparatedPaths)
为作业添加多个路径,多个路径间以逗号隔开。
3)public static void setInputPaths(JobConf conf, Path... inputPaths)
将路径以数组的形式设置为作业的输入。
4)public static void setInputPaths(JobConf conf, String commaSeparatedPaths)
将以逗号分隔的路径设置为作业的输入列表。
一条路径可以表示一个文件、一个目录或者一个目录的集合。当输入路径是目录时表示包含目录下的所有文件。但是当目录下又包含子目录时,会把子目录当作文件来处理,这时MapReduce会报错。还可以设置一个过滤器,根据命名格式来限定选择目录下的文件。
FileInputFormat类文件的切分与节点选择
getSplits(JobContext)方法主要实现文件切分以及数据位置的选择,文件切分主要确定InputSplis的个数以及每个InputSplit所对应的数据段。对于一组文件,FileInputFormat只分隔大文件,这里的"大"指的是文件超过HDFS块的大小。分片值得大小通常可以通过设置Hadoop得属性进行改变。
Hadoop中分片属性
属性名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
Mapred.min.split.size | int | 1 | 一个文件分片最小有效字节数 |
Mapred.max.split.size | long | Long.MAX_VALUE | 一个文件分片最大有效字节数 |
dfs.block.size | long | 128MB | HDFS中块的大小(字节) |
根据上面三个属性的值,可以计算分片大小
max(minmumSize,min(maxmumSize,blockSize))
首先从最大分片和Block之间选择一个比较小的,再与最小分片相比较,选择一个大的,默认情况下有。
minmumSize<blockSize<maxmumSize
一般splitSize与blockSize相同,这是Hadoop推荐和默认选项,当然也可与blockSize不同。
一旦确定splitSize的值之后,FileInputFormat将文件一次切分成大小为splitSize的输入分片,最后剩下不足splitSize的数据块将单独成为一个InputSplit。
当系统中存在大量小文件时,由于不够一个数据块的大小,FileInputFormat把每个小文件分别独立作为一个InputSplit,并分配一个Map任务进行处理,这样会导致效率降低。通常使用SequenceFile将这些小文件合并成一个或多个大文件,再进行处理。如果系统中已经存在很多小文件,可以通过CombinerFileInputFormat把多个小文件打包成一个大文件,以使Map任务处理更多的数据,从而提高效率。
文本输入
TextInputFormat
TextInputFormat是默认InputFormat,主要用来处理文本数据,他将输入文件的每一行作为单独的一个记录。键key存储的是该行在整个文件中的字节偏移量。值value是文本的内容,但不包含终止符(换行符或回车符),它被打包成一个Text对象。
下面的文本文件会被作为一个分片,这个分片包含了3条记录。
Higher Education Press
I love China
Hello world
没一条记录表示以下键值对
(0,Higher Education Press)
(23,I love China)
(36,Hello world)
由上可知,键存储的是字节偏移量,值存储的是文本内容。需要提醒一点,键不是行号。每行在文件中的偏移量是可以在分片内单独确定的,每个分片都知道上一个分片的大小,只需要加到分片内的偏移量上,就可以获取每行在整个文件中的偏移量了。
KeyValueTextInputFormat
KeyValueTextInputFormat与TextInputFormat一样用来处理纯文本文件,每一行作为一条记录。通常,文件的每一行是一个键值对,键与值之间用分隔符隔开,默认采用制表符作为分隔符。属性mapreduce.input.keyvaluelinerecordreader.key.value.separator也可以设置分隔符。
a Higher Education Press
b I love China
c Hello world
该文件数据会切分为1个输入分片,包含3个记录。KeyValueTextInputFormat会将这些记录转换为
(a,Higher Education Press)
(b,I love China)
(c,Hello world)
NLineInputFormat
NLineInputFormat采用按行数而不是按文件大小切分文件的方法。NLineInputFormat设置每个mapper处理文件的行数,与TextInputFormat一样,键是文件中的行的字节偏移量。N是mapper收到的行数,当N设为1时,每个mapper正好收到一行的输入,用户可以通过mapreduce.input.lineinputformat.linespermap属性来修改N的数值。
Higher Education Press
I love China
Hello world
当N为2时,则每个输入分片就包含两行,由于共有3行文本,因此一个mapper收到两行输入:
(0,Higher Education Press)
(23,I love China)
//另一个mapper收到一行
(36,Hello world)
二进制输入
Hadoop的MapReduce不仅仅只是处理文本信息,还可以处理二进制格式的数据。
SequenceFileInputFormat类
SequenceFile是Hadoop为二进制类型的key/value存储提供的一种文件格式。它含有同步点,读取器可以从文件的任意一点与记录边界进行同步。SequenceFile还支持压缩。SequenceFile可以采用一些序列化技术来存储任意类型。SequenceFile也可以作为小文件的容器,将若干小文件打包成一个SequenceFile,这样效率更高。
如果要用SequenceFile作为MapReduce的输入,采用SequenceFileInputFormat非常合适。键和值由SequenceFile决定,所以只需要保证与Mapper类定义的输入类型匹配即可。例如,如果输入文件中键的格式是IntWritable,值是Text,则mapper的格式为Mapper<IntWritable,Text,K,V>K和V是输入的键和值的类型。
SequenceFileAsTextInputFormat类
SequenceFileAsTextInputFormat是SequenceFileInputFormat的变体,是将SequenceFile中的键和值解析成Text对象,这个转换通过在键和值上调用toString()方法来完成。
SequenceFileAsBinaryInputFormat
SequenceFileAsBinaryInputFormat是SequenceFileInputFormat的变体,是将SequenceFile中的键和值作为二进制对象。它们被封装为BytesWritable对象,因此应用程序可以任意地解释这些字节数组。
多路输入和数据库输入
多路输入
虽然MapReduce作业可以包含多个输入文件,但这些文件都必须由同一个InputFormat和同一个Mapper处理。一个作业一般包含多个输入文件,这些文件的数据格式可能有多种,这种情况下,可以采用MultipleInputs类进行处理。MultipleInput能够为每条输入路径独立指定InputFormat和Mapper,但是要求这些Mapper的输出类型相同。
MultipleInputs使用方法示例如下
MultipleInputs.addInputPath(job,path1,TextInputFormat.class,Mapper1.class);
MultipleInputs.addInputPath(job,path2,TextInputFormat.class,Mapper2.class);
MultipleInputs.addInputPath(job,path2,TextInputFormat.class,Mapper3.class);
MultipleInputs类还有一个重载的addInputPath方法(),没有Mapper参数,其定义如下
public static void addInputPath(Job job, Path path, Class<? extends InputFormat> inputFormatClass)
它适用于作业有多种输入数据格式,但只有一个Mapper的情况。由于没有Mapper参数,需要通过job的setMapper()方法来指定Mapper。
数据库输入
针对数据库类型的输入数据,采用DBInputFormat类。DBInputFormat可以使用JDBC从关系型数据库中读取数据。由于没有采用任何共享机制,当有多个Mapper去连接数据库时,有可能导致数据库压力过大,造成崩溃,因此DBInputFormat类通常用于加载小量的数据集。
OutputFormat类
OutputFormat描述了MapReduce作业的输出规范。
package org.apache.hadoop.mapreduce;
import java.io.IOException;
import org.apache.hadoop.classification.InterfaceAudience.Public;
import org.apache.hadoop.classification.InterfaceStability.Stable;
@Public
@Stable
public abstract class OutputFormat<K, V> {
public OutputFormat() {
}
public abstract RecordWriter<K, V> getRecordWriter(TaskAttemptContext var1) throws IOException, InterruptedException;
public abstract void checkOutputSpecs(JobContext var1) throws IOException, InterruptedException;
public abstract OutputCommitter getOutputCommitter(TaskAttemptContext var1) throws IOException, InterruptedException;
}
getRecordWriter()方法返回RecordWriter,RecordWritable负责键值对的写入。checkOutputSpecs()方法检查输出参数是否规范,一般检查输出目录是否已经存在,如果存在就会报错。getOutputCommitter()方法返回OutputCommitter。OutputCommitter负责对任务的输出进行管理,包括初始化临时文件,任务完成后清理临时目录、临时文件等。
FileOutFormat基类需要提供所有基于文件的OutputFormat实现的公共功能,主要功能包含以下两方面。
1)实现checkOutputSpecs接口。检查用户配置的输出目录是否存在,如果存在则抛出异常。
2)处理side-effect file。side-effect file的典型应用为推测执行任务,是为防止"慢任务“降低计算性能而启动的一种推测执行任务。FileOutFormat会为每个Task的数据创建一个side-effect file,并将产生的数据临时写入该文件,等Task完成之后,再将结果移动到最终的输出目录。
文本输出
TextOutputFormat是默认的输出格式,将每条记录写为文本行。由于其可以通过调用toString()方法将任意数据类型转换为字符串,因此其键和值的数据类型可以是任意形式。
每个键值默认由制表符分隔,可以通过属性mapreduce.output.textoutputformat.seqarator设置分隔符。
如果键或值需要省略(不输出),可以在参数的对应位置上使用NullWritable来实现。如果key和value都设为NullWritable表示无输出。
另外NullOutputFormat也是继承自OutputFormat类的一个抽象类,他会消耗掉所有输出,并将输出赋值为null,即什么也不输出。
package org.apache.hadoop.mapred.lib;
import org.apache.hadoop.classification.InterfaceAudience.Public;
import org.apache.hadoop.classification.InterfaceStability.Stable;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapred.OutputFormat;
import org.apache.hadoop.mapred.RecordWriter;
import org.apache.hadoop.mapred.Reporter;
import org.apache.hadoop.util.Progressable;
@Public
@Stable
public class NullOutputFormat<K, V> implements OutputFormat<K, V> {
public NullOutputFormat() {
}
public RecordWriter<K, V> getRecordWriter(FileSystem ignored, JobConf job, String name, Progressable progress) {
return new RecordWriter<K, V>() {
public void write(K key, V value) {
}
public void close(Reporter reporter) {
}
};
}
public void checkOutputSpecs(FileSystem ignored, JobConf job) {
}
}
二进制输出
SequenceFileOutputFormat
SequenceFileOutputFormat将它的输出写到SequenceFile。由于SequenceFile格式紧凑,很容易被压缩,因此如果输出需要作为后续MapReduce作业的输入,这便是一种很好的输出格式。
SequenceFileAsBinaryOutputFormat
SequenceFileAsBinaryOutputFormat与SequenceFileAsBinaryInputFormat相对应,功能是将键值对作为二进制格式写到一个SequenceFile容器中。
MapFileOutputFormat
MapFileOutputFormat以MapFile作为输出。由于MapFile的键是有序的,所以需要进行额外的限制来保证Reduce输出键的有序性。
多个输出
在默认情况下,当作业完成之后会将产生的文件放置到输出目录下,每个Reducer产生一个输出文件,并且以文件分区号命名,如part-r-00000、part-r-00001等。有时可能需要对输出的文件名进行控制,或让每个Reducer输出多个文件,这时,就需要使用MapReduce提供的MultipleOutputs类。
MultipleOutputs类可以将数据输出到多个文件,文件名源于输出的键和值或者任意字符串。这允许每个Reducer创建多个文件。name-m-nnnnn形式的文件名用于Map输出,name-r-nnnnn形式的文件名用于Reduce输出,其中name是由程序设定的任意名字,nnnnn是一个指明块号的整数(从0开始)。通过块号,可以保证在有相同名字情况下,从不同块输出不会造成冲突。
MapReduce任务相关类
Mapper类
每个Map任务就是一个Java进程,用来将HDFS中的文件记录解析成键值对。Mapper接收键值对<key1,value1>
形式数据,经过处理,再以新的键值对<key2,value2>
的形式输出。
在编写MapReduce程序时,Map任务一般需要继承Mapper类,Mapper类源码位于包org.apache.hadoop.mapreduce
中源码如下。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.apache.hadoop.mapreduce;
import java.io.IOException;
import org.apache.hadoop.classification.InterfaceAudience.Public;
import org.apache.hadoop.classification.InterfaceStability.Stable;
@Public
@Stable
public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
public Mapper() {
}
protected void setup(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
} //为map方法提供预处理功能,在任务开始时调用一次
protected void map(KEYIN key, VALUEIN value, Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
context.write(key, value);
}
protected void cleanup(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
} //进行扫尾工作,在任务结束时调用一次
public void run(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
this.setup(context);
try {
while(context.nextKeyValue()) {
this.map(context.getCurrentKey(), context.getCurrentValue(), context);
}
} finally {
this.cleanup(context);
}
}
public abstract class Context implements MapContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
public Context() {
}
}
}
Mapper类中有4个泛型,分别是KEYIN、VALUEIN、KEYOUT和VALUEOUT。KEYIN、VALUEIN表示输入数据<key,value>
的类型。KEYOUT、VALUEOUT表示输出数据<key,value>
的类型。
setup方法
setup()在Mapper类实例化时将调用一次,且只调用一次,一般开发者不需要重写此方法。setup()方法做一些初始化相关的工作。例如,程序需要时可以在setup()中读入一个全局参数,或装入一个文件,或完成作业的配置信息,或连接数据库等。
例如,读取Configuration的变量rnums
context.getConfigration().get("rnums");
map方法
MapReduce框架会通过InputFormat中RecordReader从InputSplit获取一个个键值对Key/value,并交给map()方法进行处理。输入参数key和value分别是对应的键和值,context是环境对象,或称上下文。使用context的write(key,value)方法作为Map任务的输出。
cleanup方法
Mapper通过继承Closeable接口获得close方法,用户通过实现该方法对Mapper进行清理工作,比如关闭setup()方法中打开的文件或尽力的数据库连接。也可以在cleanup()方法中对map()的处理结果进行进一步的处理,然后再提交<key,value>
输出。cleanup()在Task任务销毁前仅执行一次,在默认情况下cleanup()方法不需要重写。
run方法
MapReduce框架从自定义的Mapper类反射产生的实例的run()方法开始Map任务的执行。从上面run方法代码可以看出,run()方法提供了setup()->map()->cleanup()的执行模板。在while循环程序中,通过context.nextKeyValue()方法依次从context中取出一个键值对,然后交给map()方法执行。取完数据之后context.nextKeyValue()方法返回false,退出循环。
setup()方法在所有map()方法运行之前执行,cleanup()方法在所有map()方法运行之后执行。一般不需要重写run()方法,除非要重新定义Map任务的执行流程。
Partitioner类
Mapper输出的键值对<key,value>
需要送到指定的Reduce节点上进一步处理。<key,value>
键值对到某个Reducer的分配规则是由Partitioner类制定的。Partitoner抽象类位于包"org.apache.hadoop.mapreduce"中
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.apache.hadoop.mapreduce;
import org.apache.hadoop.classification.InterfaceAudience.Public;
import org.apache.hadoop.classification.InterfaceStability.Stable;
@Public
@Stable
public abstract class Partitioner<KEY, VALUE> {
public Partitioner() {
}
public abstract int getPartition(KEY var1, VALUE var2, int var3);
}
其中key和value为Shuffle传输中的<key,value>
,numPartitions为分区的总数。
MapReduce框架在包“org.apache.hadoop.mapreduce.lib.partition”中提供了4种分区模式,分别是BinaryPartitioner、HashPartitioner、KeyFieldBasePartitioner和TotalOrderPartitioner。
在Hadoop种,系统默认使用的是HashPartitioner,通过对key取hash值并按Reducer数目取模来确定相应的Reduce节点。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.apache.hadoop.mapreduce.lib.partition;
import org.apache.hadoop.classification.InterfaceAudience.Public;
import org.apache.hadoop.classification.InterfaceStability.Stable;
import org.apache.hadoop.mapreduce.Partitioner;
@Public
@Stable
public class HashPartitioner<K, V> extends Partitioner<K, V> {
public HashPartitioner() {
}
public int getPartition(K key, V value, int numReduceTasks) {
return (key.hashCode() & 2147483647) % numReduceTasks;
}
}
这里key.hashCode()&2147483647
是为了保证int值的第一位是0,即保证hashCode为负数。
如果系统提供的分区不能满足具体应用的需求,用户可以自定义分区。自定义分区需要继承Partitioner类,重写getPartitioner()方法和configure()方法。getPartitioner()方法用于确定Reduce目标节点,configure()方法使用Hadoop Job Configuration来配置所使用的Partitioner类。
package com.ex.mapreduce.partitioner;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapreduce.Partitioner;
public class MyPartitioner extends Partitioner<String,String> {
public int getPartition(String key, String value, int numPartitions) {
return (Integer.parseInt(key)&Integer.MAX_VALUE)%numPartitions;
}
public void configure(JobConf job){
}
}
还需再main方法中加入
job.setPartitionerClass(MyPartitioner.class);
Sort类
排序(sort)是Shuffle过程中的核心过程之一。在MapReduce排序过程中,如果key为IntWritable类型,那么按照数字大小对key排序;如果key为Text类型,则按照字典顺序对字符串排序。MapReduce默认是进行排序的,用户不能控制是否进行排序。
MapReduce默认按升序排序,但用户可以控制排序的规则,称之为自定义排序,其目的是满足某些特定业务需求。由于Hadoop自带的Writable类都是WritableComparable的实现类,WritableComparable类又同时继承了Writable和Comparable接口,因此WritableComparable的实现类都可以通过compareTo方法进行比较。自定义排序只需要继承WritableComparartor类,重写其compare()方法即可。
另外,还须在main方法中加入
job.setSortComparatorClass(MySort.class);
按数字大小对key降序排序
package com.ex.mapreduce.sort;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
public class MySort extends WritableComparator {
public MySort() {
super(IntWritable.class, true);
}
public int compare(WritableComparable a, WritableComparable b) {
IntWritable v1 = (IntWritable) a;
IntWritable v2 = (IntWritable) b;
return v2.compareTo(v1);
}
}
二次排序sort实现
package com.ex.mapreduce.sort;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.WritableComparator;
public class SecondarySort extends WritableComparator {
public SecondarySort() {
super(Text.class, true);
}
public int compare(WritableComparator a, WritableComparator b) {
if (Integer.parseInt(a.toString().split(" ")[0]) == Integer.parseInt(b.toString().split(" ")[0])) {
if (Integer.parseInt(a.toString().split(" ")[1]) > Integer.parseInt(b.toString().split(" ")[1]))
return 1;
else
return -1;
} else if (Integer.parseInt(a.toString().split(" ")[0]) > Integer.parseInt(b.toString().split(" ")[0]))
return 1;
else
return -1;
}
}
Combiner类
MapReduce框架使用Mapper将数据处理成一个个的<key,value>
键值对,经过Shuffle,然后使用Reducer处理数据并最终输出。虽然简单有效,但是如果待处理的数据量大,那么网络传输量也是巨大的。如果只是对数据求最大值,那么很明显Shuffle只需要输出它所知道的最大值(当前最大值)即可,不必传输过多数据。这样不仅可以减轻网络压力,还可以大幅度提高程序运行效率。MapReduce的Combiner类就起到这样的作用。
例如,WordCount案例,Mapper到Reducer的传输过程中,有许多相同key的键值对都需要通过RPC传输给Reducer进行计算,那么就可以利用Combiner,在传输给Reducer之前,在Mapper本地,把相同key对应的value值进行求和,这样可以减少网络数据传输量,又不影响最终计算结果。
package com.ex.mapreduce.combiner;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int count = 0;
for (IntWritable v : values) {
count += v.get();
}
context.write(key, new IntWritable(count));
}
}
在main()中设置Combiner对应的类
job.setCombinerClass(WordCountCombiner.class);
仔细观察发现WordCountbiner类与WordCountReducer类代码完全一样,所以对于WordCount这种业务需求的作业,完全没有必要编写Combiner类,在job对象setCombinerClass()方法中,直接调用已经编写好的WordCountReducer类就可以了。
由于Combiner继承自Reducer的实现类,所以从本质上讲,Combiner是处理数据量小的Reducer。由于Combiner的输出作为Reducer的输入,Combiner输出键值对<key,value>
的类型必须与Reducer输入键值对<key,value>
的数据类型相一致,而且不能影响Reducer的处理逻辑和最终结果。一般Combiner可以用在求最大值、最小值、求和等满足结合律和交换律的计算场景中,其他计算场景需要仔细考虑,认真设计算法。
Reduce类
Reduce任务接收Shuffle的输出作为输入数据,形式为<key,value-list>
,经过规约处理后,将结果写入到HDFS中。Reduce类位于包org.apache.hadoop.mapreduce
中
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.apache.hadoop.mapreduce;
import java.io.IOException;
import java.util.Iterator;
import org.apache.hadoop.classification.InterfaceAudience.Public;
import org.apache.hadoop.classification.InterfaceStability.Stable;
import org.apache.hadoop.mapreduce.ReduceContext.ValueIterator;
import org.apache.hadoop.mapreduce.task.annotation.Checkpointable;
@Checkpointable
@Public
@Stable
public class Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
public Reducer() {
}
protected void setup(Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
} //为reduce方法提供预处理功能,在任务开始时调用一次
protected void reduce(KEYIN key, Iterable<VALUEIN> values, Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
Iterator var4 = values.iterator();
while(var4.hasNext()) {
VALUEIN value = var4.next();
context.write(key, value);
}
}
protected void cleanup(Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
} //进行清理工作,在任务结束时调用一次
public void run(Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
this.setup(context);
try {
while(context.nextKey()) {
this.reduce(context.getCurrentKey(), context.getValues(), context);
Iterator<VALUEIN> iter = context.getValues().iterator();
if (iter instanceof ValueIterator) {
((ValueIterator)iter).resetBackupStore();
}
}
} finally {
this.cleanup(context);
}
}
public abstract class Context implements ReduceContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
public Context() {
}
}
}
Reducer类中有4个泛型,分别时KEYIN, VALUEIN, KEYOUT, VALUEOUT。KEYIN, VALUEIN表示Mapper输出数据<key,value>
的类型,也就是说Reducer的输入类型要与Mapper的输出类型相匹配。KEYOUT, VALUEOUT表示Reducer输出数据<key,value>
的类型。
setup方法
Reducer的setup()方法与Mapper的setup()方法类似,都是在执行任务之前调用一次,一般做一些初始化工作。通常不需要重写此方法。
cleanup方法
该方法在Reducer执行之后执行一次,主要做一些清理工作。
reducer方法
Reduce任务最核心的方法。Context时Reducer基类的内部类,继承自ReducerContext类,经过Reducer处理后,可以使用context.writer(key,value)来输出结果。
run方法
与Mapper的run()方法功能类似,用于控制Reduce任务的执行流程,默认是先执行一次setup()方法,接着执行循环,从context中依次取出每个键值对,交给reduce()方法处理。当所有的键值对取出完毕后结束循环。最后调用执行一次cleanup()方法。一般不需要编写run()方法,除非想控制Reduce任务的执行流程来做一些特殊的处理。
分组
reduce()方法是按照组为操作对象进行统计的,每个reduce方法每次只能对相同key所对应的值进行计算。在MapReduce的默认分组规则中,也是基于key进行的,会将相同key的value放到一个集合中去,因此reduce方法每次接收的是一组具有相同key值得键值对。为了满足特殊的要求,也可以自定义分组器,让某些不同key得键值对共同调用同一个reduce()方法。MapReduce提供了job.setGroupingComparatorClass(cls)
来使用自定义分组器,其中cls表示自定义分组得类。自定义分组需要继承WritableComparator,实现compare()方法,具有相同返回值的键值对调用同一个reduce()方法。
package com.ex.mapreduce.Group;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
public class MyGroup extends WritableComparator {
public MyGroup() {
super(IntWritable.class, true);
}
public int compare(WritableComparable a, WritableComparable b) {
IntWritable o1 = (IntWritable) a;
IntWritable o2 = (IntWritable) b;
if (o1.get() % 2 == 0) {
return 0;
} else {
return 1;
}
}
}
在上述代码中,首先利用构造方法,将key值指定为IntWritable类型。然后通过compare()方法,将key值为偶数的分为一组,返回值为0;将key值为奇数的分为另一组,返回值为1。这样reduce()方法每次接收的是一组key值全为0或者全为1的键值对。
最后,在main()方法中设置分组对应的类MyGroup
job.setGroupingComparatorClass(MyGroup.class);
MapReduce编程实例
选择操作
常见的关系代数运算包括选择、投影、并、交、差以及自然连接等操作,都可以非常容易利用MapReduce实现。
任务描述
一张学生信息表,每一列分别表示学号、姓名、性别和年龄。要求查询年龄大于18岁的学生信息。
student.txt
数据间用Tab分割。
15001 李勇 男 20
15002 李晨 女 19
15003 王敏 女 18
15004 张立 男 18
设计思路
对一个集合进行选择操作,可以在Map阶段对每个记录进行判断,将满足条件的记录输出即可,不需要编写Reduce端的代码。
程序代码
只需要编写Map类代码,由于Reduce端不需要编写代码,在MapReduce运行过程中,会生成一个系统自带的Reduce方法。这个Reduce是为了保持框架的完整性自动调用的,系统直接把Map端的输出作为整个程序的输出结果。
Map.java
package com.ex.mapreduce.exmaple;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class Map extends Mapper<LongWritable, Text,Text, NullWritable> {
public void map(LongWritable key ,Text value, Context context) throws IOException, InterruptedException {
String line=value.toString();
String[] f=line.split("\t");
int sage=Integer.parseInt(f[3]);
if(sage>18)
context.write(new Text(line),NullWritable.get());
}
}
package com.ex.mapreduce.exmaple;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
import java.io.IOException;
public class Driver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Configuration configuration = new Configuration();
String[] otherArgs = new GenericOptionsParser(configuration, args).getRemainingArgs();
Job job = Job.getInstance(configuration);
job.setJarByClass(Driver.class);
FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
job.setMapperClass(Map.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
运行结果
差运算
任务描述
两个集合分别存放在文件studentA和studentB,现在求studentA-studentB的结果。
1)studentA数据
15001 李勇 男 20
15002 李晨 女 19
15003 王敏 女 18
15004 张立 男 18
2)studentB数据
15001 李勇 男 20
15002 刘晨 女 19
15003 王敏 女 18
15004 孙俪 男 21
设计思路
计算studentA和studentB的差集,即找出在studentA中存在而在studentB中不存在的记录。
在Map阶段,对于studentA和studentB中每条记录以记录为键,而值分别用A和B进行区分。例如,studentA和键值对为<15001 李勇 男 20,A
,对应于studentB中的键值对<15001 李勇 男 20,B>
。
在Reduce端对<key,<vlaue-list>>
进行处理,如果<value-list>
里含有A且不含有B,key就是想要的记录,直接将key输出就可以了。
程序代码
1)Map端
SubstractMapper
package com.ex.mapreduce.chapter04_2;
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;
import java.io.IOException;
public class SubstracMapper extends Mapper<Object, Text,Text,Text> {
protected void map(Object key,Text value,Context context) throws IOException, InterruptedException {
InputSplit inputSplit=(InputSplit)context.getInputSplit();
String filename=((FileSplit)inputSplit).getPath().getName();
if(filename.contains("studentA"))
context.write(value,new Text("A"));
else
context.write(value,new Text("B"));
}
}
2)Reduce端
package com.ex.mapreduce.chapter04_2;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.ArrayList;
public class SubstractReducer extends Reducer<Text,Text,Text, NullWritable> {
public void reduce(Text key,Iterable<Text>values,Context context) throws IOException, InterruptedException {
ArrayList<String> al=new ArrayList<String>();
for(Text text:values)
al.add(text.toString());
if(al.contains("A")&&(!al.contains("B")))
context.write(key,NullWritable.get());
}
}
package com.ex.mapreduce.chapter04_2;
import com.ex.mapreduce.MapReduceUtils;
import com.ex.mapreduce.exmaple.Map;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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.util.GenericOptionsParser;
import java.io.File;
import java.io.IOException;
public class Driver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Configuration configuration = new Configuration();
String[] otherArgs = new GenericOptionsParser(configuration, args).getRemainingArgs();
Job job = Job.getInstance(configuration);
job.setJarByClass(com.ex.mapreduce.chapter04_2.Driver.class);
MapReduceUtils.checkArgs(job,otherArgs);
job.setMapperClass(SubstracMapper.class);
job.setReducerClass(SubstractReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Text.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
剩余例子略。
计数器
MapReduce计数器可以理解为简易的日志,它主要用来记录Job的执行进度和状态。在程序中通过设置计数器,可以获取作业执行进度的变化情况。以统计数据集中无效记录的数目的任务为例,如果发现无效记录的比例相当高,那么需要了解为何存在如此多的无效记录。计数器是收集作业统计信息的有效手段之一,用于质量控制或应用级统计,还可以辅助诊断系统故障。MapReduce自带了许多默认的计数器,用来描述多项指标,这些内置计数器被划分为若干个组。
组别 | 类别 |
---|---|
MapReduce任务计数器 | org.apache.hadoop.mapreduce.TaskCounter |
文件系统计数器 | org.apache.hadoop.mapreduce.FileSystemCounter |
FileInputFormat计数器 | org.apache.hadoop.mapreduce.lib.input.FileInputFormatCounter |
FileOutputFormat计数器 | org.apache.hadoop.mapreduce.lib.output.FileOutputFormatCounter |
作业计数器 | org.apache.hadoop.mapreduce.JobCounter |
任务计数器
任务计数器辅助采集任务执行过程中任务的相关信息,每个作业的所有任务的结构都会被聚集起来。任务计数器由其关联的任务维护,并定期发送Application Master。任务计数器的值每次都是完整传输的,因此可以避免由于消息丢失而引发的错误。内置的任务计数器包括MaoReduce任务计数器、文件系统任务计数器、FileInputFormat任务计数器、FileOutputFormat任务计数器。
内置的MapReduce任务计数器
计数器名称 | 说明 |
---|---|
MAP_INPUT_RECORDS | map输入的记录数,读到一条记录,该计数器的值递增 |
MAP_OUTPUT_RECORDS | 作业中所有map产生的map输出记录数 |
MAP_OUTPUT_BYTES | map输出的字节数 |
SPLIT_RAW_BYTES | 由map读取的输入-分片对象的字节数 |
COMBINE_INPUT_RECORDS | combine输入的记录数 |
COMBINE_OUTPUT_RECORDS | combine输出的记录数 |
REDUCE_INPUT_GROUPS | reduce输入的组 |
REDUCE_INPUT_RECORDS | 所有reducer已经处理的输入记录的个数 |
REDUCE_OUTPUT_RECORDS | 作业中所有map已经产生的reduce输出的字节数 |
SPILLED_RECORDS | 作业中所有map和reduce任务溢出到磁盘的记录数 |
SHUFFLED_MAPS | 由shuffle传输的map输出数 |
FAILED_SHUFFLE | shuffle过程中,发生map输出副本错误的次数 |
MERGED_MAP_OUTPUTS | 在reduce端被合并的map输出数 |
CPU_MILLISECONDS | 一个任务的总CPU时间,以毫秒为单位 |
PHYSICAL_MEMORY_BYTES | 一个任务所用的物理内存,以字节数为单位 |
内置的文件系统任务计数器
计数器名称 | 说明 |
---|---|
BYTES_READ | 文件系统的读字节数,各个文件系统分别对应一个计数器 |
BYTES_WRITTEN | 文件系统的写字节数 |
READ_OPS | 文件系统读操作(例如,open操作、file status操作)的数量 |
LARGE_READ_OPS | 文件系统大规模读操作(例如,对一个大容量目录进行list操作)的数量 |
WRITE_OPS | 文件系统写操作(例如,create操作、append操作)的数量 |
内置的FileInputFormat任务计数器
计数器名称 | 说明 |
---|---|
BYTES_READ | 由map任务通过FileInputFormat读取的字节数 |
内置的FileOutFormat任务计数器
计数器名称 | 说明 |
---|---|
BYTES_WRITTEN | 由map任务或reduce任务通过FileOutputFormat写的字节数 |
作业计数器
作业计数器由Application Master维护,都是作业级别的统计量,这些计数器的值不会随着任务的运行而改变。
计数器名称 | 说明 |
---|---|
TOTAL_LAUNCHED_MAPS | 启动的reduce任务数,包括以“推测执行”方式启动的任务 |
TOTAL_LAUNCHED_REDUCES | 启动的map任务数,包括以“推测执行”方式启动的任务 |
NUM_FAILED_MAPS | 失败的map任务数 |
NUM_FAILED_REDUCES | 失败的reduce任务数 |
NUM_KILLED_MAPS | 被终止的map任务数 |
NUM_KILLED_REDUCES | 被终止的reduce任务数 |
DATA_LOCAL_MAPS | 与输入数据在同一节点上的map任务数 |
RACK_LOCAL_MAPS | 与输入数据在同一机架范围内但不在同一几点上的map任务数 |
OTHER_LOCAL_MAPS | 与输入数据不在同一机架范围内的map任务总运行时间 |
MILLIS_MAPS | 包括以推测执行方法启动的map任务的总运行时间 |
MILLIS_REDUCES | 包括以推测执行方法启动的reduce任务的总运行时间 |
自定义计数器
MapReduce运行用户可以自定义计数器,计数器的值在mapper或reducer中增加。计数器由一个Java枚举类型来定义,方法对计算器进行分组。一个作业可以定义的枚举类型数量不限,每个枚举类型所包含的字段数量也不限。枚举类型的名称为计数器组的名称,枚举类型的字段为计数器的名称。MapReduce框架将跨所有的map和reduce聚集这些计数器,在作业结束时产生最终结果。下面自定义计数器,统计输入的无效数据。