[大数据]Hadoop(3)MapReduce(2)

3.MapReduce框架原理

MapReduce的数据流:
Map Reduce数据流

3.1 InputFormat数据输入

InputFormat:从文件中读取数据,转化成key-value格式。

MapReduce程序会设置输入格式,如job.setIntputFormatClass(KeyValueTextInputFormat.class),来保证输入文件按照我们想要的格式被读取。所有的输入格式继承与抽象类InputFormat,其子类有专门用于读取普通文件的各种InputFormat。
在这里插入图片描述
InputFormat解决的问题:
1)如何将数据分片 —— 由getSplit()来解决
2)如何读取数据中的分片 —— 由RecoderReader()来解决

InputFormat源码:

public abstract class InputFormat<K, V> {
    public InputFormat() {
    }

    public abstract List<InputSplit> getSplits(JobContext var1) throws IOException, InterruptedException;

    public abstract RecordReader<K, V> createRecordReader(InputSplit var1, TaskAttemptContext var2) throws IOException, InterruptedException;
}

getSplit()用来获取由输入文件计算出来的InputSplit,createRecoderReader()以k-v对的形式从InputSplit中读出来。

通过使用InputFormat,MapReduce框架可以做到:
1)验证作业的输入的正确性
2)将输入文件切分成逻辑的InputSplits,一个InputSplit将被分配给一个单独的Mapper task。
3)提供RecordReader的实现,这个RecordReader会从InputSplit中正确读出一条一条的K-V对供Mapper使用。

3.1.1 数据切片

MapTask的并行度决定Map阶段的任务处理并发度,进而影响到整个Job的处理速度。
数据块:Block是HDFS物理上把数据分成一块一块。
数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。

在进行切片之后每一个切片交由一个MapTask并行处理,由MapTask将切片转换为key-value值,之后交给Mapper。
在这里插入图片描述

3.1.2 InputSplit

InputSplit是一个抽象类,InputFormat通过getSplits()切分成的分片就存储在这个类中,它在逻辑上包含了提供给处理这个InputSplit的Mapper的所有K-V对。

InputSplit源码:

public abstract class InputSplit {
    public InputSplit() {
    }

    public abstract long getLength() throws IOException, InterruptedException;

    public abstract String[] getLocations() throws IOException, InterruptedException;

    @Evolving
    public SplitLocationInfo[] getLocationInfo() throws IOException {
        return null;
    }
}

1)getLength() 用来获取InputSplit的大小,以便对InputSplit进行排序。
2)getLocations() 则用来获取存储分片的位置列表,这些位置是本地的,不需要序列化。
3)getLocationInfo() 获取有关输入拆分存储在哪些节点,以及如何在每个位置存储信息。

3.1.3 RecoderReader

RecordReader是用来从一个输入分片中读取一个一个的K-V对的抽象类,我们可以将其看作是InputSplit上的迭代器。

RecordReader源码:

public abstract class RecordReader<KEYIN, VALUEIN> implements Closeable {
    public RecordReader() {
    }

    public abstract void initialize(InputSplit var1, TaskAttemptContext var2) throws IOException, InterruptedException;

    public abstract boolean nextKeyValue() throws IOException, InterruptedException;

    public abstract KEYIN getCurrentKey() throws IOException, InterruptedException;

    public abstract VALUEIN getCurrentValue() throws IOException, InterruptedException;

    public abstract float getProgress() throws IOException, InterruptedException;

    public abstract void close() throws IOException;
}

1)initialize() 初始化RecordReader。
2)nextKeyValue() RecordReader中最主要的方法,由它获取分片上的下一个K-V对。
3)getCurrentKey() 获取当前的Key。
4)getCurrentValue() 获取当前的Value。
5)getProgress() 记录RecordReader当前通过其数据的进展。
6)close() 关闭RecordReader。

3.1.4 Job提交流程(切片源码)

Job主要向临时文件夹提交 1)jar包,2)配置信息.xml,3)切片信息
Job提交流程源码

waitForCompletion()

submit();

// 1建立连接
	connect();	
		// 1)创建提交Job的代理
		new Cluster(getConfiguration());
			// (1)判断是本地yarn还是远程
			initialize(jobTrackAddr, conf); 

// 2 提交job
submitter.submitJobInternal(Job.this, cluster)
	// 1)创建给集群提交数据的Stag路径
	Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);

	// 2)获取jobid ,并创建Job路径
	JobID jobId = submitClient.getNewJobID();

	// 3)拷贝jar包到集群
copyAndConfigureFiles(job, submitJobDir);	
	rUploader.uploadFiles(job, jobSubmitDir);

// 4)计算切片,生成切片规划文件
writeSplits(job, submitJobDir);
		maps = writeNewSplits(job, jobSubmitDir);
		input.getSplits(job);

// 5)向Stag路径写XML配置文件
writeConf(conf, submitJobFile);
	conf.writeXml(out);

// 6)提交Job,返回提交状态
status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());

在这里插入图片描述

3.1.5 FileInputFormat

FileInputFormat是用来专门读取普通文件的FileInputFormat子类。

FileInputFormat切片机制:

1)简单的按照文件的内容长度进行切片。
2)切片大小,默认等于Block大小。
3)切片时不考虑数据集整体,而是逐个针对每一个文件单独切片。
在这里插入图片描述
FileInputFormat的getSplit()方法过程:
1)获取InputSplit的SplitSize。可以通过设置mapred.min.split.size和mapred.max.split.size来设置,默认情况下min为1,max为block的大小。
2)判断文件是否可以切分。比如,密码文件、压缩文件只能按照一个分片进行处理。
3)通过computeSplitSize计算出splitSize。计算方法是:Math.max(minSize, Math.min(maxSize, blockSize))。也就是保证在minSize和maxSize之间,且如果minSize<=blockSize<=maxSize,则设为blockSize。
4)通过add()将分片加入列表,该方法中通过makeSplit()实现逻辑块的切分。
5)分析文件长度不为0程序如何执行.

源码:

  public List<InputSplit> getSplits(JobContext job) throws IOException {
       StopWatch sw = (new StopWatch()).start();
       long minSize = Math.max(this.getFormatMinSplitSize(), getMinSplitSize(job));
       long maxSize = getMaxSplitSize(job);
       List<InputSplit> splits = new ArrayList();
       List<FileStatus> files = this.listStatus(job);
       boolean ignoreDirs = !getInputDirRecursive(job) && job.getConfiguration().getBoolean("mapreduce.input.fileinputformat.input.dir.nonrecursive.ignore.subdirs", false);
       Iterator var10 = files.iterator();

       while(true) {
           while(true) {
               while(true) {
                   FileStatus file;
                   do {
                       if (!var10.hasNext()) {
                           job.getConfiguration().setLong("mapreduce.input.fileinputformat.numinputfiles", (long)files.size());
                           sw.stop();
                           if (LOG.isDebugEnabled()) {
                               LOG.debug("Total # of splits generated by getSplits: " + splits.size() + ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
                           }

                           return splits;
                       }

                       file = (FileStatus)var10.next();
                   } while(ignoreDirs && file.isDirectory());

                   Path path = file.getPath();
                   long length = file.getLen();
                   if (length != 0L) {
                       BlockLocation[] blkLocations;
                       if (file instanceof LocatedFileStatus) {
                           blkLocations = ((LocatedFileStatus)file).getBlockLocations();
                       } else {
                           FileSystem fs = path.getFileSystem(job.getConfiguration());
                           blkLocations = fs.getFileBlockLocations(file, 0L, length);
                       }

                       if (this.isSplitable(job, path)) {
                           long blockSize = file.getBlockSize();
                           long splitSize = this.computeSplitSize(blockSize, minSize, maxSize);

                           long bytesRemaining;
                           int blkIndex;
                           for(bytesRemaining = length; (double)bytesRemaining / (double)splitSize > 1.1D; bytesRemaining -= splitSize) {
                               blkIndex = this.getBlockIndex(blkLocations, length - bytesRemaining);
                               splits.add(this.makeSplit(path, length - bytesRemaining, splitSize, blkLocations[blkIndex].getHosts(), blkLocations[blkIndex].getCachedHosts()));
                           }

                           if (bytesRemaining != 0L) {
                               blkIndex = this.getBlockIndex(blkLocations, length - bytesRemaining);
                               splits.add(this.makeSplit(path, length - bytesRemaining, bytesRemaining, blkLocations[blkIndex].getHosts(), blkLocations[blkIndex].getCachedHosts()));
                           }
                       } else {
                           if (LOG.isDebugEnabled() && length > Math.min(file.getBlockSize(), minSize)) {
                               LOG.debug("File is not splittable so no parallelization is possible: " + file.getPath());
                           }

                           splits.add(this.makeSplit(path, 0L, length, blkLocations[0].getHosts(), blkLocations[0].getCachedHosts()));
                       }
                   } else {
                       splits.add(this.makeSplit(path, 0L, length, new String[0]));
                   }
               }
           }
       }
   }

3.1.6 TextInputFormat

TextInputFormat是默认的FileInputFormat实现类,按照行读取每条记录,键是存储在整个文件中的起始字节偏移量,为LongWriteable类型,值是这一行的内容,不包括任何行终止符(换行符和回车符),为Text类型。
在这里插入图片描述
TextInputFormat中的createRecordReader()方法:

public RecordReader<LongWritable, Text> createRecordReader(InputSplit split, TaskAttemptContext context) {
       String delimiter = context.getConfiguration().get("textinputformat.record.delimiter");
       byte[] recordDelimiterBytes = null;
       if (null != delimiter) {
           recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);
       }

       return new LineRecordReader(recordDelimiterBytes);
   }

1)设置终止符。textinputformat.record.delimiter 指的是读取一行的数据的终止符,即遇到终止符时,这一行的读取结束。可以通过Configuration的set()方法来设置自定义的终止符,如果没有设置,那么Hadoop就采用以CR,LF或者CRLF作为终止符。
2)获取一个LineRecordReader对象,读取InputSplit。

接下来使用LineRecordReader中的nextkeyValue()方法给Key和Value赋值,由readLine()方法从输入流中读取给定文本,返回值为被读取字节的数量 newSize = in.readLine(value, maxLineLength, maxBytesToConsume(pos))。读取一行数据,将数据放入value中,返回值为被读取字节的长度,还包括新行(换行)。

public boolean nextKeyValue() throws IOException {
       if (this.key == null) {
           this.key = new LongWritable();
       }

       this.key.set(this.pos);
       if (this.value == null) {
           this.value = new Text();
       }

       int newSize = 0;

       while(this.getFilePosition() <= this.end || this.in.needAdditionalRecordAfterSplit()) {
           if (this.pos == 0L) {
               newSize = this.skipUtfByteOrderMark();
           } else {
               newSize = this.in.readLine(this.value, this.maxLineLength, this.maxBytesToConsume(this.pos));
               this.pos += (long)newSize;
           }

           if (newSize == 0 || newSize < this.maxLineLength) {
               break;
           }

           LOG.info("Skipped line of size " + newSize + " at pos " + (this.pos - (long)newSize));
       }

       if (newSize == 0) {
           this.key = null;
           this.value = null;
           return false;
       } else {
           return true;
       }
   }

接下来通过Mapper中的run()方法调用nextValue(),建立一个键值对。至此就可以将分割好的切片以Key-Value的形式提交给Mapper使用。

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);
       }

   }

3.1.7 CombineTextInputFormat

框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。
1)应用场景:
CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。
2)虚拟存储切片最大值设置
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。
3)切片机制
生成切片过程包括:虚拟存储过程和切片过程二部分。
(1)虚拟存储过程:
将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现太小切片)。
例如setMaxInputSplitSize值为4M,输入文件大小为8.02M,则先逻辑上分成一个4M。剩余的大小为4.02M,如果按照4M逻辑划分,就会出现0.02M的小的虚拟存储文件,所以将剩余的4.02M文件切分成(2.01M和2.01M)两个文件。
(2)切片过程:
(a)判断虚拟存储的文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片。
(b)如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
(c)测试举例:有4个小文件大小分别为1.7M、5.1M、3.4M以及6.8M这四个小文件,则虚拟存储之后形成6个文件块,大小分别为:
1.7M,(2.55M、2.55M),3.4M以及(3.4M、3.4M)
最终会形成3个切片,大小分别为:
(1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M
在这里插入图片描述

3.1.8 自定义InputFormat

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

即在切片之后将所有的文件以统一的方式读入一个二进制文件中。需要自己实现RecoderReader方法。

定义MyInputFormat类,继承FileInputFormat,重新实现RecordReader方法。返回一个自定义的RecordReader。

public class MyInputFormat extends FileInputFormat<Text, BytesWritable> {
    @Override
    /**
     * 返回一个自定义类型的RecordReader
     */
    public RecordReader<Text, BytesWritable> createRecordReader(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
        return new MyRecoderReader();
    }
}

实现MyRecoderReader类。

import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapred.FileSplit;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;

import java.io.IOException;

/**
 * 将整个文件转换成一组Key - Value 对
 */
public class MyRecoderReader extends RecordReader<Text, BytesWritable> {
    public boolean isRead = false;//表示文件是否读完了,默认为false,表示文件重来没读过
    private Text key = new Text();
    private BytesWritable value = new BytesWritable();
    private FSDataInputStream inputStream;
    private FileSplit fs;


    @Override
    public void initialize(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
        //开流
        FileSystem fileSystem = FileSystem.get(taskAttemptContext.getConfiguration());//从任务背景中获取Configuration
        fs = (FileSplit) inputSplit; //将InputSplit转换成子类
        FSDataInputStream inputStream = fileSystem.open(fs.getPath());//获取路径
    }

    @Override
    /**
     * 读取下一组KeyValue Pair 返回是否成功读取
     */
    public boolean nextKeyValue() throws IOException, InterruptedException {
        if (!isRead) {
            //读流读取这个文件
            //填充Key Value
            key.set(fs.getPath().toString());

            byte[] buffer = new byte[(int) fs.getLength()];//一次读完全部文件 以字节数组方式读取
            inputStream.read(buffer);
            value.set(buffer, 0, buffer.length);
            isRead = true;//标记文件读取结束
            return true;

        } else {
            return false;//文件已经读过
        }
    }

    @Override
    /**
     * 获取当前读到的key
     */
    public Text getCurrentKey() throws IOException, InterruptedException {
        return key;
    }

    @Override
    /**
     * 获取当前读到的Value
     */
    public BytesWritable getCurrentValue() throws IOException, InterruptedException {
        return value;
    }

    @Override
    /**
     * 显示读取的进度,返回0或者1
     */
    public float getProgress() throws IOException, InterruptedException {
        return isRead ? 1 : 0;
    }

    @Override
    public void close() throws IOException {
        //关流
        IOUtils.closeStream(inputStream);
    }
}

Mapper方法与Reducer都使用默认的,不需要重写。
MyInputDriver类:

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.mapred.SequenceFileOutputFormat;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;

import javax.xml.soap.Text;
import java.io.IOException;

public class MyInputDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        Job job = Job.getInstance(new Configuration());
        job.setJarByClass(MyInputDriver.class);
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(BytesWritable.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(BytesWritable.class);
        job.setInputFormatClass(MyInputFormat.class);//指定自己的InputFormat格式
        job.setOutputValueClass(SequenceFileOutputFormat.class);
        FileInputFormat.setInputPaths(job, new Path("d:/input"));
        FileOutputFormat.setOutputPath(job, new Path("d:/output"));
        boolean b = job.waitForCompletion(true);
        System.exit(b ? 0 : 1);
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值