十三、MapReduce中的OutputFormat

       既然有InputFormat,那么自然就会有OutputFormat,本文主要介绍MapReduce中的OutputFormat。关注专栏《破茧成蝶——大数据篇》查看相关系列的文章~


目录

一、MapReduce的工作机制 

1.1 MapTask的工作机制

1.2 ReduceTask工作机制

二、MapReduce中的OutputFormat

2.1 常见的OutputFormat实现类

2.1.1 TextOutputFormat

2.1.2 SequenceFileOutputFormat

2.2 自定义OutputFormat实例

2.2.1 需求与数据

2.2.2 编写Bean类

2.2.3 编写Mapper类

2.2.4 编写RecordWriter类

2.2.5 编写OutputFormat类

2.2.6 编写Reducer类

2.2.7 编写Driver驱动类

2.2.8 测试


 

一、MapReduce的工作机制 

       在介绍OutputFormat之前,我们首先来看下MapTask和ReduceTask的工作机制。

1.1 MapTask的工作机制

       (1)Read阶段:MapTask通过用户编写的RecordReader,从输入InputSplit中解析出一个个key/value。(2)Map阶段:该节点主要是将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value。(3)Collect收集阶段:在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。在该函数内部,它会将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区中。(4)Spill阶段:即“溢写”,当环形缓冲区满后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。(5)Combine阶段:当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index。在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并io.sort.factor(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。

       溢写阶段的详细步骤:1、利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。2、按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。3、将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中。

1.2 ReduceTask工作机制

       (1)Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。(2)Merge阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。(3)Sort阶段:按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。(4)Reduce阶段:reduce()函数将计算结果写到HDFS上。

       注意事项:

       1、ReduceTask=0表示没有Reduce阶段,输出文件个数和Map个数一致。

       2、ReduceTask默认值就是1,所以输出文件个数为一个。

       3、如果数据分布不均,就可能会在Reduce端产生数据倾斜。

       4、ReduceTask的数量并不是任意设置,还要考虑具体的业务场景。

       5、要根据集群的性能来设置有多少个ReduceTask。

       6、如果分区不是1,但是ReduceTask为1,此时是不执行分区过程的。因为在MapTask的源码中,执行分区的前提是先判断ReduceNum是否大于1,如果不大于1,肯定不会执行。

二、MapReduce中的OutputFormat

2.1 常见的OutputFormat实现类

       OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了OutputFormat接口。以下是常见的几种OutputFormat实现类。

2.1.1 TextOutputFormat

       TextOutputFormat是默认的输出格式,他把每条记录写为文本行。他的键和值可以是任意类型,因为TextOutputFormat调用toString()方法把他们转换为字符串。

2.1.2 SequenceFileOutputFormat

       将SequenceFileOutputFormat输出作为后续MapReduce任务的输入,这是一种很好的输出格式,因为他的格式紧凑,很容易被压缩。

2.2 自定义OutputFormat实例

       自定义OutputFormat是本文的重点,因为有时候为了满足不同的业务场景,必须得自定义OutputFormat,我们通过一个案例来看一下。

2.2.1 需求与数据

       先来看下数据:

十三、MapReduce中的OutputFormat_第1张图片

       数据还是我们前面例子中用到的Nginx的日志数据,上图中的各个字段分别表示:时间、版本、客户端ip、访问路径、状态、域名、服务端ip、size、响应时间。现在想要将访问路径中为“/iclock/getrequest”路径的输出到一个文件中,其余的访问路径放到另外一个文件中。

2.2.2 编写Bean类

package com.xzw.hadoop.mapreduce.outputformat;

import org.apache.hadoop.io.Writable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

/**
 * @author: xzw
 * @create_date: 2020/8/10 14:02
 * @desc: 时间、版本、客户端ip、访问路径、状态、域名、服务端ip、size、响应时间
 * @modifier:
 * @modified_date:
 * @desc:
 */
public class LogBean implements Writable {
    private String date;
    private String version;
    private String clientIP;
    private String url;
    private String status;
    private String domainName;
    private String serverIP;
    private String size;
    private String responseDate;

    public LogBean() {
    }

    public void set(String date, String version, String clientIP, String url, String status, String domainName,
                    String serverIP, String size, String responseDate) {
        this.date = date;
        this.version = version;
        this.clientIP = clientIP;
        this.url = url;
        this.status = status;
        this.domainName = domainName;
        this.serverIP = serverIP;
        this.size = size;
        this.responseDate = responseDate;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public String getClientIP() {
        return clientIP;
    }

    public void setClientIP(String clientIP) {
        this.clientIP = clientIP;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public String getDomainName() {
        return domainName;
    }

    public void setDomainName(String domainName) {
        this.domainName = domainName;
    }

    public String getServerIP() {
        return serverIP;
    }

    public void setServerIP(String serverIP) {
        this.serverIP = serverIP;
    }

    public String getSize() {
        return size;
    }

    public void setSize(String size) {
        this.size = size;
    }

    public String getResponseDate() {
        return responseDate;
    }

    public void setResponseDate(String responseDate) {
        this.responseDate = responseDate;
    }

    @Override
    public String toString() {
        return date + '\t' + version + '\t' + clientIP + '\t' + url + '\t' + status + '\t' + domainName + '\t'
                + serverIP + '\t' + size + '\t' + responseDate;
    }

    /**
     * 序列化方法
     *
     * @param dataOutput
     * @throws IOException
     */
    @Override
    public void write(DataOutput dataOutput) throws IOException {
        dataOutput.writeUTF(date);
        dataOutput.writeUTF(version);
        dataOutput.writeUTF(clientIP);
        dataOutput.writeUTF(url);
        dataOutput.writeUTF(status);
        dataOutput.writeUTF(domainName);
        dataOutput.writeUTF(serverIP);
        dataOutput.writeUTF(size);
        dataOutput.writeUTF(responseDate);
    }

    /**
     * 反序列化方法
     *
     * @param dataInput
     * @throws IOException
     */
    @Override
    public void readFields(DataInput dataInput) throws IOException {
        this.date = dataInput.readUTF();
        this.version = dataInput.readUTF();
        this.clientIP = dataInput.readUTF();
        this.url = dataInput.readUTF();
        this.status = dataInput.readUTF();
        this.domainName = dataInput.readUTF();
        this.serverIP = dataInput.readUTF();
        this.size = dataInput.readUTF();
        this.responseDate = dataInput.readUTF();
    }
}

2.2.3 编写Mapper类

package com.xzw.hadoop.mapreduce.outputformat;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

/**
 * @author: xzw
 * @create_date: 2020/8/10 13:59
 * @desc:
 * @modifier:
 * @modified_date:
 * @desc:
 */
public class LogMapper extends Mapper {
    private Text k = new Text();
    private LogBean v = new LogBean();

    @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、获取对应的数据
        String date = fields[0];
        String version = fields[1];
        String clientIP = fields[2];
        String url = fields[3];
        String status = fields[4];
        String domainName = fields[5];
        String serverIP = fields[6];
        String size = fields[7];
        String responseDate = fields[8];

        //4、封装数据
        k.set(url);
        v.set(date, version, clientIP, url, status, domainName, serverIP, size, responseDate);

        //5、写出
        context.write(k, v);
    }
}

2.2.4 编写RecordWriter类

package com.xzw.hadoop.mapreduce.outputformat;

import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author: xzw
 * @create_date: 2020/8/10 14:23
 * @desc:
 * @modifier:
 * @modified_date:
 * @desc:
 */
public class LogRecordWriter extends RecordWriter {

    private FSDataOutputStream getrequest;
    private FSDataOutputStream others;

    public LogRecordWriter(TaskAttemptContext job) throws IOException {
        //1、获取文件系统
        FileSystem fs;
        fs = FileSystem.get(job.getConfiguration());

        //2、创建输出流
        String outDir = job.getConfiguration().get(FileOutputFormat.OUTDIR);
        getrequest = fs.create(new Path(outDir + "/getrequest.txt"));
        others = fs.create(new Path(outDir + "/others.txt"));
    }

    @Override
    public void write(Text key, LogBean value) throws IOException, InterruptedException {
        //判断路径是否是/iclock/getrequest,然后将value输出到不同的文件
        String k = key.toString() + "\n";
        if (k.contains("getrequest")) {
            getrequest.write(value.toString().getBytes());
            getrequest.write("\n".getBytes());
        } else {
            others.write(value.toString().getBytes());
            others.write("\n".getBytes());
        }
    }

    @Override
    public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
        //关闭资源
        IOUtils.closeStream(getrequest);
        IOUtils.closeStream(others);
    }
}

2.2.5 编写OutputFormat类

package com.xzw.hadoop.mapreduce.outputformat;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author: xzw
 * @create_date: 2020/8/10 14:44
 * @desc:
 * @modifier:
 * @modified_date:
 * @desc:
 */
public class LogOutputFormat extends FileOutputFormat {
    @Override
    public RecordWriter getRecordWriter(TaskAttemptContext job) throws IOException,
            InterruptedException {
        return new LogRecordWriter(job);
    }
}

2.2.6 编写Reducer类

package com.xzw.hadoop.mapreduce.outputformat;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * @author: xzw
 * @create_date: 2020/8/10 14:20
 * @desc:
 * @modifier:
 * @modified_date:
 * @desc:
 */
public class LogReducer extends Reducer {
    @Override
    protected void reduce(Text key, Iterable values, Context context) throws IOException,
            InterruptedException {
        for (LogBean value: values) {
            context.write(key, value);
        }
    }
}

2.2.7 编写Driver驱动类

package com.xzw.hadoop.mapreduce.outputformat;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
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 java.io.IOException;

/**
 * @author: xzw
 * @create_date: 2020/8/10 14:54
 * @desc:
 * @modifier:
 * @modified_date:
 * @desc:
 */
public class LogDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        //输入输出路径
        args = new String[]{"e:/input/nginx_log", "e:/output"};

        Job job = Job.getInstance(new Configuration());

        job.setJarByClass(LogDriver.class);

        job.setMapperClass(LogMapper.class);
        job.setReducerClass(LogReducer.class);

        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(LogBean.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(LogBean.class);

        job.setOutputFormatClass(LogOutputFormat.class);

        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        boolean b = job.waitForCompletion(true);
        System.exit(b ? 0 : 1);
    }
}

2.2.8 测试

       测试结果如下所示:

十三、MapReduce中的OutputFormat_第2张图片

       getrequest.txt内容如下:

十三、MapReduce中的OutputFormat_第3张图片

       others.txt内容如下:

十三、MapReduce中的OutputFormat_第4张图片

 

 

你可能感兴趣的:(破茧成蝶——大数据篇,MapReduce,hadoop,Reducer)