Hadoop(四) MapReduce 原理

目录

 MapReduce的核心思想

Hadoop MapReduce

InputFormat

TextInputFormat

KeyValueTextInputFormat

NLineInputFormat

自定义InputFormat

Job提交过程

FileInputFormat切片大小的参数配置

获取切片信息API

MapTask的个数

Partitioner

默认是HashPartitioner

 

 自定义Partitioner

排序

Combiner

Reduce

Reduce的过程

OutputFormat

TextOutputFormat

SequenceFileOutputFormat

自定义OutputFormat

MapReduce开发

输入数据接口InputFormat

逻辑处理接口:Mapper

Partitioner分区

Comparable排序

Combiner合并

reduce端分组:Groupingcomparator

逻辑处理接口:Reducer

输出数据接口:OutputFormat


 MapReduce的核心思想

上面的图片展示了四个阶段:Input, Map, Reduce, Output,这四个阶段就是MapReduce的核心过程

Input/Output只是任何系统都有的阶段,现只看Map/Reduce

Map阶段将输入的文件按照业务规则解析为一个个的k-v对,Reduce阶段在Map计算输出的k-v对上进行继续计算,

比方说汇总这些k-v对,计算出有多少个这样的k-v对。它所做的事就是把一件分为多个阶段来做,把一个大的问题分成很多的

小问题,可以将mapreduce看作是一种设计模式,一种解决解决某些场景问题的编程方法。比如在python中就提供了mapreuce

的编程模式

# map/filter 的使用
# 对传入的参数,每一个应用传入的运算
def fun1(x):
    return x*2 + 5
list1 = [4, 8, 9, 1, 3]
list2 = map(fun1, list1)

# reduce的应用,reduce函数需要单独导入
# 化简函数
from functools import reduce

f = lambda x, y : x + y

total = reduce(f, list2)

如果说这个问题不是很大,那么利用这个模式在一台计算机上就可以解决这个问题了。

但如果这个如果很大,我们还是要使用这个mapreduce模式,那么这时我们就需要使用很多台计算机去解决这个问题了,

这就要使用到Hadoop MapReduce(分布式的计算框架)

 

Hadoop MapReduce

Hadoop MapReduce是一个分布式的计算框架,它的核心功能还是利用mapreduce的编程模式计算解决计算的问题。

但是因为它是分布式的,因而引入很多的辅助功能去解决因为分布式所带来的问题,比如文件的存储,网络I/O通信等。

具体过程如下

InputFormat

public abstract class InputFormat<K, V> {

  /** 
   * Logically split the set of input files for the job.  
   * 
   * <p>Each {@link InputSplit} is then assigned to an individual {@link Mapper}
   * for processing.</p>
   *
   * <p><i>Note</i>: The split is a <i>logical</i> split of the inputs and the
   * input files are not physically split into chunks. For e.g. a split could
   * be <i>&lt;input-file-path, start, offset&gt;</i> tuple. The InputFormat
   * also creates the {@link RecordReader} to read the {@link InputSplit}.
   * 
   * @param context job configuration.
   * @return an array of {@link InputSplit}s for the job.
   */
  public abstract 
    List<InputSplit> getSplits(JobContext context
                               ) throws IOException, InterruptedException;
  
  /**
   * Create a record reader for a given split. The framework will call
   * {@link RecordReader#initialize(InputSplit, TaskAttemptContext)} before
   * the split is used.
   * @param split the split to be read
   * @param context the information about the task
   * @return a new record reader
   * @throws IOException
   * @throws InterruptedException
   */
  public abstract 
    RecordReader<K,V> createRecordReader(InputSplit split,
                                         TaskAttemptContext context
                                        ) throws IOException, 
                                                 InterruptedException;

}

 InputFormat 常见的接口实现类

TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定义InputFormat等

 

TextInputFormat

TextInputFormat 是默认的 InputFormat,每条记录是一行输入。键是LongWritable 类型,存储该行在整个文件中的字节偏移量。 值是这行的内容,不包括任何行终止符(换行符合回车符),它被打包成一个 Text 对象

 

KeyValueTextInputFormat

每一行均为一条记录, 被分隔符(缺省是tab(\t))分割为key(Text),value(Text)。

可以通过 mapreduce.input.keyvaluelinerecordreader.key.value,separator属性来设定分隔符。 它的默认值是一个制表符

 

NLineInputFormat

通过 TextInputFormat 和 KeyValueTextInputFormat,每个 Mapper 收到的输入行数不同。行数取决于输入分片的大小和行的长度。 如果希望 Mapper 收到固定行数的输入,需要将 NLineInputFormat 作为 InputFormat。与 TextInputFormat 一样, 键是文件中行的字节偏移量,值是行本身。N 是每个 Mapper 收到的输入行数。N 设置为1(默认值)时,每个 Mapper 正好收到一行输入。 mapreduce.input.lineinputformat.linespermap 属性实现 N 值的设定

 

自定义InputFormat

1-自定义一个类继承FileInputFormat

2-改写RecordReader,实现一次读取一个完整文件封装为KV

3-在输出时使用SequenceFileOutPutFormat输出合并文件

 

Job提交过程

org.apache.hadoop.mapreduce.Job

      waitCompletion(true)

      submit() --> new JobSubmitter()

                      submitJobInternal()

                          //创建提交数据数据的临时目录

                          JobSubmissionFiles.getStagingDir(cluster, conf);

                                   
                        // 获取jobid,并创建job路径
                        JobID jobId = submitClient.getNewJobID();
                        job.setJobID(jobId);
                        Path submitJobDir = new Path(jobStagingArea, jobId.toString());
                        
                        //
                        copyAndConfigureFiles(job, submitJobDir);

                        // 计算切片,并形成逻辑切分计划
                        int maps = writeSplits(job, submitJobDir);
                          // FileInputFormat
                          List<InputSplit> splits = input.getSplits(job);
                          //计算切片大小
                          long splitSize = computeSplitSize(blockSize, minSize, maxSize);
                                           Math.max(minSize, Math.min(maxSize, blockSize));

                        //将job信息(切片计划xml)写入提交目录
                        writeConf(conf, submitJobFile);
                                                    
                        // 向yarn提交job
                        submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());

FileInputFormat切片大小的参数配置

在FileInputFormat中,计算切片大小的逻辑:Math.max(minSize, Math.min(maxSize, blockSize));

切片主要由这几个值来运算决定

mapreduce.input.fileinputformat.split.minsize=1 默认值为1

mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue

因此,默认情况下,切片大小=blocksize。

maxsize(切片最大值):参数如果调得比blocksize小,则会让切片变小,而且就等于配置的这个参数的值。

minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blocksize还大。

FileInputFormat在切分文件时是按文件来切分的,因此不论这个文件的大小,该文件都至少有一个split,但文件小时极易产生

小文件问题这些就会产生大量很小的split,可以通过预处理将小文件合并为大文件或使用CombineTextInputFormat

 

获取切片信息API

// 根据文件类型获取切片信息,可用于在map中判断读入的是哪个文件
FileSplit inputSplit = (FileSplit) context.getInputSplit();
// 获取切片的文件名称
String name = inputSplit.getPath().getName();

MapTask的个数

MapTask的个数由split的个数决定,因此要特别注意小文件问题

 

Partitioner

默认是HashPartitioner

public class HashPartitioner<K2, V2> implements Partitioner<K2, V2> {
    public HashPartitioner() {
    }

    public void configure(JobConf job) {
    }

    public int getPartition(K2 key, V2 value, int numReduceTasks) {
        return (key.hashCode() & 2147483647) % numReduceTasks;
    }
}

 

 自定义Partitioner

 */
public class YearPartitioner extends Partitioner<CompKey, NullWritable> {
    @Override
    /**
     * @param numPartitions the total number of partitions.
     */
    public int getPartition(CompKey compKey, NullWritable nullWritable, int numPartitions) {
        int year = compKey.getYear();
        if(year < 1993) {
            return 0;
        }
        else if(year < 1996) {
            return 1;
        }
        else {
            return 2;
        }
    }
}

在job驱动中,设置自定义partitioner:

job.setPartitionerClass(CustomPartitioner.class);

自定义partition后,要根据自定义partitioner的逻辑设置相应数量的reduce task

job.setNumReduceTasks(3);

注意:

如果reduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;

如果1<reduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会Exception;

如果reduceTask的数量=1,则不管mapTask端输出多少个分区文件,最终结果都交给这一个reduceTask,最终也就只会产生一个结果文件 part-r-00000;

示例

(1)job.setNumReduceTasks(1);会正常运行,只不过会产生一个输出文件

(2)job.setNumReduceTasks(2);会报错

(3)job.setNumReduceTasks(6);大于5,程序会正常运行,会产生空文件

 

排序

1.部分排序(分区内有序,这是mapreduce框架默认的)
        key自动排序


2.全排序
        自定义分区(在某个范围内的数据就放到一个分区内)
        对key的排序 
        

3.二次排序(定义组合key,  定义新的Map端排序,定义新的Reduce端的 GroupingComparator)
        对value的排序。
        将value整合到key

 

Combiner

可以简单的理解为是map端的reduce操作,使用的也是Reducer,可以减少map端输出文件的大小

class WordCountReducerWithCombiner1 extends Reducer<Text, IntWritable, Text, IntWritable> {
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int amount = 0;
        for(IntWritable iw : values) {
            amount += iw.get();
        }
        context.write(key, new IntWritable(amount));
    }
}

Reduce

 // reduceTask的个数可以在Driver中直接设置

 job.setNumReduceTasks(4);

注意

(1)reducetask=0 ,表示没有reduce阶段,输出文件个数和map个数一致。

(2)reducetask默认值就是1,所以输出文件个数为一个。

(3)如果数据分布不均匀,就有可能在reduce阶段产生数据倾斜

(4)reducetask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个reducetask。

(5)具体多少个reducetask,需要根据集群性能而定。

(6)如果分区数不是1,但是reducetask为1,是否执行分区过程。答案是:不执行分区过程。因为在maptask的源码中,执行分区的前提是先判断reduceNum个数是否大于1。不大于1肯定不执行

 

Reduce的过程

Copy阶段(shuffle 阶段)

ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。

Merge阶段

在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。

Sort阶段

按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。

Reduce阶段

reduce()函数将计算结果写到HDFS上。

 

OutputFormat

OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了 OutputFormat接口,常用实现类

TextOutputFormat

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

SequenceFileOutputFormat

SequenceFileOutputFormat将它的输出写为一个顺序文件。如果输出需要作为后续 MapReduce任务的输入,这便是一种好的输出格式,因为它的格式紧凑,很容易被压缩。

自定义OutputFormat

为了实现控制最终文件的输出路径,可以自定义OutputFormat。

要在一个mapreduce程序中根据数据的不同输出两类结果到不同目录,这类灵活的输出需求可以通过自定义outputformat来实现。

自定义OutputFormat步骤

(1)自定义一个类继承FileOutputFormat。

(2)改写recordwriter,具体改写输出数据的方法write()。

 

MapReduce开发

输入数据接口InputFormat

默认使用的实现类是:TextInputFormat

TextInputFormat的功能逻辑是:一次读一行文本,然后将该行的起始偏移量作为key,行内容作为value返回

CombineTextInputFormat可以把多个小文件合并成一个切片处理,提高处理效率。

用户还可以自定义InputFormat。

逻辑处理接口:Mapper

用户根据业务需求实现其中三个方法:map() setup() cleanup () 

Partitioner分区

有默认实现 HashPartitioner,逻辑是根据key的哈希值和numReduces来返回一个分区号;key.hashCode()&Integer.MAXVALUE % numReduces

如果业务上有特别的需求,可以自定义分区。 

Comparable排序

当我们用自定义的对象作为key来输出时,就必须要实现WritableComparable接口,重写其中的compareTo()方法。

部分排序:对最终输出的没一个文件进行内部排序。

全排序:对所有数据进行排序,通常只有一个Reduce。

二次排序:排序的条件有两个。 

Combiner合并

Combiner合并可以提高程序执行效率,减少io传输。但是使用时必须不能影响原有的业务处理结果。

reduce端分组:Groupingcomparator

reduceTask拿到输入数据(一个partition的所有数据)后,首先需要对数据进行分组,其分组的默认原则是key相同,然后对每一组kv数据调用一次reduce()方法,并且将这一组kv中的第一个kv的key作为参数传给reduce的key,将这一组数据的value的迭代器传给reduce()的values参数。

利用上述这个机制,我们可以实现一个高效的分组取最大值的逻辑。

自定义一个bean对象用来封装我们的数据,然后改写其compareTo方法产生倒序排序的效果。然后自定义一个Groupingcomparator,将bean对象的分组逻辑改成按照我们的业务分组id来分组(比如订单号)。这样,我们要取的最大值就是reduce()方法中传进来key。

逻辑处理接口:Reducer

用户根据业务需求实现其中三个方法: reduce() setup() cleanup ()

输出数据接口:OutputFormat

默认实现类是TextOutputFormat,功能逻辑是:将每一个KV对向目标文本文件中输出为一行。

自定义OutputFormat。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章