MapReduce核心原理(下)

238 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第30天,点击查看活动详情

MapReduce 读取和输出数据

InputFormat

运行 MapReduce 程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。那么,针对不同的数据类型,MapReduce 是如何读取这些数据的呢?

InputFormat 是 MapReduce 框架用来读取数据的类。InputFormat 常用子类:

  • TextInputFormat(普通文本文件,MR 框架默认的读取实现类)
  • KeyValueTextInputFormat(读取一行文本数据按照指定分隔符,把数据封装为 kv 类型)
  • NLineInputFormat(读取数据按照行数进行划分分片)
  • CombineTextInputFormat(合并小文件,避免启动过多 MapTask 任务)
  • 自定义 InputFormat

1. CombineTextInputFormat 案例

MR 框架默认的 TextInputFormat 切片机制按文件划分切片,文件无论多小,都是单独一个切片,然后由一个 MapTask 处理,如果有大量小文件,就对应生成并启动大量的 MapTask,就会浪费很多初始化资源、启动回收等阶段。

CombineTextInputFormat 用于小文件过多的场景,它可以将多个小文件从逻辑上划分成一个切片,这样多个小文件可以交给一个 MapTask 处理,提高资源利用率。

使用方式:

// 如果不设置InputFormat,它默认用的是TextInputFormat.class
 job.setInputFormatClass(CombineTextInputFormat.class);
 //虚拟存储切片最大值设置4m
 CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);
  • CombineTextInputFormat 切片原理

假设设置 setMaxInputSplitSize 值为 4M,有四个小文件:1.txt -->2M ;2.txt-->7M;3.txt-->0.3M;4.txt--->8.2M

虚拟存储过程:

把输入目录下所有文件大小,依次和设置的 setMaxInputSplitSize 值进行比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值 2 倍,此时将文件均分成 2 个虚拟存储块(防止出现太小切片)。比如如 setMaxInputSplitSize 值为 4M,输入文件大小为 8.02M,则先逻辑上分出一个 4M 的块。剩余的大小为 4.02M,如果按照 4M 逻辑划分,就会出现 0.02M 的非常小的虚拟存储文件,所以将剩余的 4.02M 文件切分成(2.01M 和 2.01M)两个文件。

  • 2M,一个块

  • 7M,大于 4 但是不大于 4 的 2 倍,则分为两块,一块 3.5M

切片过程:

  • 判断虚拟存储的文件大小是否大于 setMaxInputSplitSize 值,大于等于则单独形成一个切片

  • 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。

  • 按照之前输入文件:那 4 个文件经过虚拟存储过程后,有 7 个文件块:2M、3.5M、3.5M、0.3M、4M、2.1M、2.1M

  • 合并之后最终形成 3 个切片:(2+3.5)M、(3.5+0.3+4)M、(2.1+2.1)M

2. 自定义 InputFormat

无论 HDFS 还是 MapReduce,在处理小文件时效率都非常低,但又难免面临处理大量小文件的场景,此时,就需要有相应解决方案。可以自定义 InputFormat 实现小文件的合并。

案例实战

需求:

将多个小文件合并成一个 SequenceFile 文件(SequenceFile 文件是 Hadoop 用来存储二进制形式的 key-value 对的文件格式),SequenceFile 里面存储着多个文件,存储的形式为文件路径+名称为 key,文件内容为 value。

实现思路:

  1. 定义一个类继承 FileInputFormat
  2. 重写 isSplitable()指定为不可切分,重写 createRecordReader()方法,创建自己的 RecorderReader 对象
  3. 改变默认读取数据方式,实现一次读取一个完整文件作为 kv 输出
  4. Driver 指定使用自定义 InputFormat

代码参考:

public class CustomFileInputFormat extends FileInputFormat<Text, BytesWritable> {

    @Override
    protected boolean isSplitable(JobContext context, Path filename) {
        return false;
    }

    @Override
    public RecordReader<Text, BytesWritable> createRecordReader(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
        CustomRecordReader reader = new CustomRecordReader();
        reader.initialize(inputSplit, taskAttemptContext);
        return reader;
    }
}
public class CustomRecordReader extends RecordReader <Text, BytesWritable> {

    private Configuration conf;
    private FileSplit split;
    private boolean isProgress=true;

    private BytesWritable value = new BytesWritable();

    private Text key = new Text();

    @Override
    public void initialize(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
        this.split = (FileSplit) inputSplit;
        this.conf = taskAttemptContext.getConfiguration();
    }

    @Override
    public boolean nextKeyValue() throws IOException, InterruptedException {
        if(isProgress){
            FSDataInputStream fis = null;
            try {
                //定义缓存区
                byte[] contents = new byte[(int) split.getLength()];
                //获取文件系统
                Path path = split.getPath();
                FileSystem fs = path.getFileSystem(conf);
                //读取数据
                fis = fs.open(path);
                //读取文件内容到缓存区
                IOUtils.readFully(fis,contents,0,contents.length);
                //输出文件内容
                value.set(contents,0,contents.length);
                //获取文件路径
                String name = split.getPath().toString();

                key.set(name);
            } finally {
                IOUtils.closeStream(fis);
            }
            isProgress = false;
            return true;
        }
        return false;
    }

    @Override
    public Text getCurrentKey() throws IOException, InterruptedException {
        return key;
    }

    @Override
    public BytesWritable getCurrentValue() throws IOException, InterruptedException {
        return value;
    }

    @Override
    public float getProgress() throws IOException, InterruptedException {
        return 0;
    }

    @Override
    public void close() throws IOException {

    }
}

在 driver 里设置 inputFormatclass

job.setInputFormatClass(CustomFileInputFormat.class);

OutputFormat

OutputFormat:是 MapReduce 输出数据的基类,所有 MapReduce 的数据输出都实现了 OutputFormat 抽象类。下面介绍几种常见的 OutputFormat 子类

  • TextOutputFormat

默认的输出格式是 TextOutputFormat,它把每条记录写为文本行。

  • SequenceFileOutputFormat

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

自定义 OutputFormat

案例实战

需求:

需要一个 MapReduce 程序根据奇偶数把结果输出到不同目录。

1
2
3
4
5
6
7
8
9
10

实现思路:

  • 自定义一个类继承 FileOutPutFormat
  • 改写 RecordWriter,重写 write 方法

代码参考:

public class CustomOutputFormat extends FileOutputFormat<Text, NullWritable> {

    @Override
    public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext context) throws IOException, InterruptedException {
        FileSystem fs = FileSystem.get(context.getConfiguration());
        FSDataOutputStream oddOut = fs.create(new Path("e:/odd.log"));
        FSDataOutputStream eventOut = fs.create(new Path("e:/event.log"));
        return new CustomWriter(oddOut, eventOut);
    }
}

public class CustomWriter extends RecordWriter<Text, NullWritable> {

    private FSDataOutputStream oddOut;
    private FSDataOutputStream evenOut;

    public CustomWriter(FSDataOutputStream oddOut, FSDataOutputStream evenOut) {
        this.oddOut = oddOut;
        this.evenOut = evenOut;
    }

    @Override
    public void write(Text text, NullWritable nullWritable) throws IOException, InterruptedException {
        Integer number = Integer.valueOf(text.toString());
        System.out.println(text.toString());
        if(number%2==0){
            evenOut.write(text.toString().getBytes());
            evenOut.write("\r\n".getBytes());
        }else {
            oddOut.write(text.toString().getBytes());
            oddOut.write("\r\n".getBytes());
        }
    }

    @Override
    public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
        IOUtils.closeStream(oddOut);
        IOUtils.closeStream(evenOut);
    }
}

设置 outputFormat 类

job.setOutputFormatClass(CustomOutputFormat.class);