并行编程框架MapRduce(下)

MapReduce解析

I/O序列化

序列化 serialization就是将结构化的对象转为字节流的过程,以便在网络上传输或者写入磁盘进行永久存储。
反序列化 deserialization是序列化的逆过程,将字节流转换回结构化对象。
序列化和反序列化的主要应用是进程间的通信和持久化存储。
在Hadoop集群中,多节点之间的通信时通过远程过程调用RPC协议完成的。RPC协议将消息序列化成二进制RPC对序列化有如下要求:
1)紧凑:紧凑格式能充分利用网络带宽
2)快速:进程间通信形成了分布式系统的骨架,所有需要尽量减少序列化和反序列化的性能开销
3)可拓展性:为了满足新的需求,通信协议在不断变化,在控制客户端和服务器的过程中,需要直接引入新的协议,因此序列化必须满足可拓展的要求。
4)支持互操作:对于某些系统来说,希望能支持以不同编程语言编写的客户端与服务器交互,所以需要设计一种特定的格式来满足这一需求。
Hadoop并没有采用Java的序列化机制,而是引入了Writable接口,建立了自己的序列化机制,具有紧凑、速度快的特点,但不太容易用Java以为的编程语言去扩展。

不用Java的序列化机制的原因是因为
java的序列化是一个重量级序列化框架,一个对象被序列化后,会会带很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输。所以,Hadoop自己开发了一套序列化机制。
常用数据序列化类型

Java类型Hadoop Writable类型
BooleanBooleanWritable
ByteBtyeWritable
IntIntWritable
FloatFloatWritable
LongLongWritable
DoubleDoubleWritable
StringText
MapMapWritable
ArrayArrayWritable

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)
布尔型BooleanWritable1
字节型ByteWritable1
短整型ShortWritable2
整形IntWritable4
整形(可变长度)VIntWritable1-5
浮点型FloatWritable4
长整型LongWritable8
长整型(可变长度)VlongWritable1-9
双精度浮点型DoubleWritable8

进行整数编码时,可以有两种选择:

  • 定长格式(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.sizeint1一个文件分片最小有效字节数
Mapred.max.split.sizelongLong.MAX_VALUE一个文件分片最大有效字节数
dfs.block.sizelong128MBHDFS中块的大小(字节)

根据上面三个属性的值,可以计算分片大小
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_RECORDSmap输入的记录数,读到一条记录,该计数器的值递增
MAP_OUTPUT_RECORDS作业中所有map产生的map输出记录数
MAP_OUTPUT_BYTESmap输出的字节数
SPLIT_RAW_BYTES由map读取的输入-分片对象的字节数
COMBINE_INPUT_RECORDScombine输入的记录数
COMBINE_OUTPUT_RECORDScombine输出的记录数
REDUCE_INPUT_GROUPSreduce输入的组
REDUCE_INPUT_RECORDS所有reducer已经处理的输入记录的个数
REDUCE_OUTPUT_RECORDS作业中所有map已经产生的reduce输出的字节数
SPILLED_RECORDS作业中所有map和reduce任务溢出到磁盘的记录数
SHUFFLED_MAPS由shuffle传输的map输出数
FAILED_SHUFFLEshuffle过程中,发生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聚集这些计数器,在作业结束时产生最终结果。下面自定义计数器,统计输入的无效数据。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值