MapReduce框架原理之(一)InputFormat数据输入

1 切片与MapTask并行度决定机制

  1. MapTask并行度决定机制
    (1)数据块:Block,在HDFS中,将数据物理上分成一块一块.
    (2)数据切片:数据切片只是在逻辑上对输入的数据进行分片,并不会在磁盘上真的将数据切成多片存储.
  2. 为什么要按默认块大小来切片:
    假设,我们现在有一个大小为300M的xx.iso文件
    (1)如果按照默认的Block大小(128M)来切分,可以分为3块:
    在这里插入图片描述
    这三块分别被存放在3个节点上,如下所示:这时前两个块都是满的
    在这里插入图片描述
    (2)如果按照均等分,即100M/块来切分,也可以分成abc三块:
    在这里插入图片描述
    但是切分之后,会出现如下状态:
    在这里插入图片描述
    由于HDFS的block默认是128M的大小,DataNode1中,的某个block中,0~100M的区间存储了切分出来的块a,而101M~128M的区间存放的是块b0~28M内容,块b的其余内容则储存在DataNode2中,同理,DataNode2中还要存放一部分块c的内容,DataNode3也要存放一部分块c的数据…
    这就是为什么,默认的,切片大小是HDFS的块大小.

2 FileInputFormat切片源码解析

通过观察源码(getSplits方法)可得到一些切片思路:

  1. 程序先找到(输入源)数据存储的目录.
  2. 遍历目录内所有文件(每个文件单独切片).
  3. 遍历第一个文件a.txt:
    (1)获取文件大小fs.sizeOf(a.txt)
    (2)计算切片大小
    // blocksize=128M
    computeSplitSize(Math.max(minSize,Math.min(maxSize,blocksize)))
    (3)默认情况下,切片大小等于blocksize
    (4)开始切片:
    0~128M为第一片,128~256M为第二片,256~300M为第三片
    每次切片前判断,size是否大于block的1.1倍,不大于则不切直接算作1片
    (5)将切片信息写到一个切片规划文件中
    (6)整个切片的核心过程在getSplit()方法中完成
    (7)InputSplit只记录了切片的元数据信息,比如起始位置,长度以及所在的节点列表等.
  4. 提交切片规划文件到YARN上,YARN上的MrAppMaster就可以根据切片规划文件开启对应数目的MapTask.

3 CombineTextInputFormat切片机制

框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下.

  1. 应用场景:
    CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件,从逻辑上规划到一个切片中,这样多个小文件就可以交给一个MapTask处理.
  2. 虚拟存储切片最大值设置:
    CombineTextInputFormat.setMapInputSplitSize(job, 4194304);//bytes 4m
    虚拟存储切片最大值最好按照实际文件大小来设置
  3. 切片机制:
    生成切片过程包括:虚拟存储过程和切片过程两部分
    (1)虚拟存储过程:
    将输入目录下所有文件大小,一次和设置的mapInputSplitSize进行比较,如果不大于设置的最大值,逻辑上划分为一块.如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块,当剩余的数据大小超过设置的最大值但小于最大值的2倍,此时文件将均分成2个虚拟存储块(防止出现太小的切片).
    例如,mapInputSplitSize值为4M,文件大小为8.02M,则逻辑上先分出一个4M的块,剩余的4.02M如果按照4M划分就还会出现0.02M的小文件,所以将剩余的文件切分成两个2.01M的文件.
    (2)切片过程:
    a)判断虚拟存储的文件是否大于mapInputSplitSize,大于则切为一片.
    b)如果不大于则继续跟下一个虚拟存储块进行合并,合并后继续进行上述判断.
    c)假设现在有4个小文件,大小分别为1.7M,5.1M,3.9M以及6.8M这4个小文件,则虚拟存储之后形成了6个文件块,大小分别为:
    1.7M,(2.55M,2.55M),3.9M以及(3.4M,3.4M)
    最后会行成3个切片,大小分别为:
    (1.7M + 2.55M),(2.55M + 3.9M),(3.4M+3.4M)
  4. FileInputFormat实现类

3.1 TextInputFormat

默认的FileInputFormat实现类.按行读取每条记录,键是该行在整个文件中的起始字节偏移量,LongWritable类型.值是这行的内容,不包括任何终止符(换行符和回车符),Text类型.
比如,一个切片包含了下面4行内容:

Rich learning form
Intelligent learning engine
Learning more convenient
From the real demand for more close to the enterprise

则进入到Mapper的每条记录会表示为以下K,V对的形式:

(0,Rich learning form)
(19,Intelligent learning engine)
(47,Learning more convenient)
(72,From the real demand for more close to the enterprise)

3.2 KeyValueTextInputFoamte

每一行均为一条记录,被分隔符分隔为Key,Value.可以通过在驱动类中设置
conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR, "\t");来设定分隔符.默认分隔符是tab(\t)

以下是一个示例:lineN和后面的内容以制表符"\t"分隔

line1	Rich learning form
line2	Intelligent learning engine
line3	Learning more convenient
line4	From the real demand for more close to the enterprise

每条记录表示为以下K,V对形式

(line1,Rich learning form)
(line2,Intelligent learning engine)
(line3,Learning more convenient)
(line4,From the real demand for more close to the enterprise)

3.3 NLineInputFormat

如果使用NLineInputFormat则代表,InputSplit不再按Block块去划分,而是按NlineInputFormat指定的行数N来划分.即输入文件的总行数 / N = 切片数, 如果不是整除,切片数 = 商 + 1.
示例数据:

Rich learning form
Intelligent learning engine
Learning more convenient
From the real demand for more close to the enterprise
// 设置每个切片InputSplit中划分三条记录
NLineInputFormat.setNumLinesPerSplit(job, 2);
          
// 使用NLineInputFormat处理记录数  
job.setInputFormatClass(NLineInputFormat.class); 

如果N为2,则表示2行以个切片,最终被切成2片,会开启2个MapTask
第一个切片:

(0,Rich learning form)
(19,Intelligent learning engine)

第二个切片:

(47,Learning more convenient)
(72,From the real demand for more close to the enterprise)

3.4 KeyValueTextInputFormat

这个比较简单,需要在Driver类中设置切割符,并且设置inputFormatClass为KeyValueTextInputFormat

// 设置切割符
conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR, " ");
// 设置输入格式
job.setInputFormatClass(KeyValueTextInputFormat.class);

4 自定义InputFormat

Hadoop框架自带的InputFormat类型不能满足所有的应用场景,很多时候需要自定义InputFormat来解决问题.
自定义InputFormat步骤如下:
(1)自定义的类需要继承FileInputFormat
(2)改写RecordReader,实现一次读取一个完整文件封装为K,V
(3)在输出时使用SequenceFileOutPutFormat输出合并文件

4.1 自定义InputFormat演示

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

  1. 自定义一个类继承FileInputFormat
    (1)重写isSplitable()方法,返回false不可切割.
    (2)重写createRecordReader()方法,创建自定义的RecordReader类对象,并初始化.
  2. 改写RecordReader,实现一次读取一个完整文件封装为K,V
    采用IO流,一次读取一个文件输出到value,isSplitable()方法返回false则表示文件不进行切片.
  3. 设置Driver
    假设我们的自定义InputFormat类命名为WholeFIleInputFormat,那么Driver中需要设置如下信息:
// 设置输入的inputFormat
job.setInputFormatClass(WholeFileInputFormat.class);
// 设置输出的outputFormat
job.setOutputFormatClass(SequenceFileOutputFormat.class);

4.2 代码实现

自定义InputFormat类

package inputformat;

import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;

import java.io.IOException;

public class WholeFileInputFormat extends FileInputFormat<Text, BytesWritable> {
    @Override
    protected boolean isSplitable(JobContext context, Path filename) {
        // return false表示不可切片,整个文件会当做一个完整的切片,交给同一个MapTask来处理
        return false;
    }

    @Override
    public RecordReader<Text, BytesWritable> createRecordReader(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
    	// 返回我们自定义的RecordReader类
        WholeFileRecordReader reader = new WholeFileRecordReader();
        return reader;
    }
}

自定义RecordReader

package inputformat;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;

import java.io.IOException;

public class WholeFileRecordReader extends RecordReader<Text, BytesWritable> {

    private FileSplit split;
    private Configuration conf;
    // 读取进度,要么未读0.0F,要么读完了整个文件1.0F
    private boolean isProgress = true;
    private Text k = new Text();
    private BytesWritable value = new BytesWritable();
    private FSDataInputStream fis = null;
    public WholeFileRecordReader() {}

    @Override
    public void initialize(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {

         // inputSplit : 就是包含切片数据的切片信息
         // 由于我们在WholeFIleInputFormat中重写了,isSplitable(),并且返回了false
         // 即代表对文件不进行切片,或者说整个文件切成一片.
         // 将其转换为FileSplit
        this.split = (FileSplit) inputSplit;
        this.conf = taskAttemptContext.getConfiguration();
    }

    @Override
    public boolean nextKeyValue() {
        if(isProgress) {
            // 1.定义缓存区
            byte[] contents = new byte[(int) split.getLength()];
            FileSystem fs = null;

            try{
                // 2.获取文件系统
                Path path = split.getPath();
                fs = path.getFileSystem(conf);

                // 3.开启输入流
                fis = fs.open(path);

                // 4.读取文件内容,读取后放入contents
                IOUtils.readFully(fis, contents, 0, contents.length);

                // 5.输出文件内容
                value.set(contents, 0, contents.length);

                // 6.获取文件路径+名称
                String name = path.toString();

                // 7.设置key的值
                k.set(name);
            } catch (Exception e) {
                e.printStackTrace();
            }
            isProgress = false;
            return true;
        }
        return false;
    }

    @Override
    public Text getCurrentKey() throws IOException, InterruptedException {
        return this.k;
    }

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

    @Override
    public float getProgress() throws IOException, InterruptedException {
        return this.isProgress ? 0.0F : 1.0F;
    }

    @Override
    public void close() throws IOException {
        IOUtils.closeStream(this.fis);
    }
}

Mapper

package inputformat;

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

import java.io.IOException;

public class WFMapper extends Mapper<Text, BytesWritable, Text, BytesWritable> {
    @Override
    protected void map(Text key, BytesWritable value, Context context) throws IOException, InterruptedException {
        super.map(key, value, context);
    }
}

Reducer

package inputformat;

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

import java.io.IOException;

public class WFReducer extends Reducer<Text, BytesWritable, Text, BytesWritable> {
    @Override
    protected void reduce(Text key, Iterable<BytesWritable> values, Context context) throws IOException, InterruptedException {
        super.reduce(key, values, context);
    }
}

Driver类

package inputformat;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;

import java.io.IOException;

public class WFDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
        // 输出目录存在则删除目录
        FileSystem fs = FileSystem.get(conf);
        Path oPath = new Path("i:\\output");
        if (fs.exists(oPath)) {
            fs.delete(oPath, true);
        }
        job.setJarByClass(WFDriver.class);
        job.setMapperClass(WFMapper.class);
        job.setReducerClass(WFReducer.class);

        // 设置map输出的key和value类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(BytesWritable.class);
        // 设置最终(reduce)输出的key和value类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(BytesWritable.class);

        // 设置inputformat类
        job.setInputFormatClass(WholeFileInputFormat.class);
        // 设置outputformat
        job.setOutputFormatClass(SequenceFileOutputFormat.class);

        WholeFileInputFormat.addInputPath(job, new Path("i:\\input"));

        SequenceFileOutputFormat.setOutputPath(job, oPath);

        boolean result = job.waitForCompletion(true);
        System.exit(result ? 0 : 1);
    }
}
发布了62 篇原创文章 · 获赞 3 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Leonardy/article/details/103945911