MHuiG

「看庭前花开花落,望天上云卷云舒」

MapReduce 运行机制

# 大数据处理技术 - Hadoop-MapReduce 的运行机制

## Partitioner

在 MapReduce 中，通过我们指定分区，会将同一个分区的数据发送到同一个 reduce 当中进行处理，例如我们为了数据的统计，我们可以把一批类似的数据发送到同一个 reduce 当中去，在同一个 reduce 当中统计相同类型的数据，就可以实现类似数据的分区，统计等
说白了就是相同类型的数据，送到一起去处理，在 reduce 当中默认分区只有 `1` 个。
MapReduce 当中的分区类图

需求：将以下数据进行分开处理
详细数据参见 partition.csv 这个文本文件，其中第五个字段表示开奖结果数值，现在需求将 15 以上的结果以及 15 以下的结果进行分开成两个文件进行保存

注意：分区的案例，只能打成 jar 包发布到集群上面去运行，本地模式已经不能正常运行了

### 第一步：定义 mapper

我们这里的 mapper 程序不做任何逻辑，也不对 key，与 value 做任何改变，只是接收我们的数据，然后往下发送

    public class MyMapper extends Mapper<LongWritable,Text,Text,NullWritable>{
      @Override
      protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        context.write(value,NullWritable.get());
      }
    }

### 第二步：定义 reducer 逻辑

我们的 reducer 也不做任何处理，将我们的数据原封不动的输出即可

    public class MyReducer extends Reducer<Text,NullWritable,Text,NullWritable> {
      @Override
      protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
        context.write(key,NullWritable.get());
      }
    }

### 第三步：自定义 partitioner

    /**
    * 这里的输入类型与我们 map 阶段的输出类型相同
    */
    public class MyPartitioner extends Partitioner<Text,NullWritable>{
      /**
      * 返回值表示我们的数据要去到哪个分区
      * 返回值只是一个分区的标记，标记所有相同的数据去到指定的分区
      */
      @Override
      public int getPartition(Text text, NullWritable nullWritable, int i) {
        String result = text.toString().split("\t")[5];
        System.out.println(result);
        if (Integer.parseInt(result) > 15){
          return 1;
        }else{
          return 0;
        }
      }
    }

### 第四步：程序 main 函数入口

    public class PartitionMain extends Configured implements Tool {
      public static void main(String[] args) throws Exception{
        int run = ToolRunner.run(new Configuration(), new PartitionMain(), args);
        System.exit(run);
      }
      @Override
      public int run(String[] args) throws Exception {
        Job job = Job.getInstance(super.getConf(),PartitionMain.class.getSimpleName());
        job.setJarByClass(PartitionMain.class);
        job.setInputFormatClass(TextInputFormat.class);
        job.setOutputFormatClass(TextOutputFormat.class);
        TextInputFormat.addInputPath(job,new Path("hdfs://192.168.52.100:8020/partitioner"));
        TextOutputFormat.setOutputPath(job,new Path("hdfs://192.168.52.100:8020/outpartition"));
        job.setMapperClass(MyMapper.class);
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(NullWritable.class);
        job.setOutputKeyClass(Text.class);
        job.setMapOutputValueClass(NullWritable.class);
        job.setReducerClass(MyReducer.class);
        /**
        * 设置我们的分区类，以及我们的 reducetask 的个数，注意
        reduceTask 的个数一定要与我们的
        * 分区数保持一致
        */
        job.setPartitionerClass(MyPartitioner.class);
        job.setNumReduceTasks(2);
        boolean b = job.waitForCompletion(true);
        return b?0:1;
      }
    }

## MapTask

整个 Map 阶段流程大体如上图所示。
简单概述：
`inputFile` 通过 `split` 被逻辑切分为多个 split 文件，通过 `Record` 按行读取内容给 `map`（用户自己实现的）进行处理，数据被 `map` 处理结束之后交给 `OutputCollector` 收集器，对其结果 `key` 进行分区（默认使用 `hash` 分区），然后写入 `buffer`，每个 `map task` 都有一个内存缓冲区，存储着 `map` 的输出结果，当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘，当整个 `map task` 结束后再对磁盘中这个 `map task` 产生的所有临时文件做合并，生成最终的正式输出文件，然后等待 `reduce task` 来拉数据。

详细步骤：

1、 首先，读取数据组件 `InputFormat` （默认 `TextInputFormat` ）会通过 `getSplits` 方法对输入目录中文件进行逻辑切片规划得到 splits，有多少个 split 就对应启动多少个 MapTask 。split 与 block 的对应关系默认是一对一。

2、 将输入文件切分为 splits 之后，由 `RecordReader` 对象（默认 `LineRecordReader` ）进行读取，以 \n 作为分隔符，读取一行数据，返回 `<key，value>`。`Key` 表示每行首字符偏移值，`value` 表示这一行文本内容。

3、 读取 split 返回 `<key,value>`，进入用户自己继承的 Mapper 类中，执行用户重写的 map 函数。`RecordReader` 读取一行这里调用一次。

4、 map 逻辑完之后，将 map 的每条结果通过 context.write 进行 collect 数据收集。在 collect 中，会先对其进行分区处理，默认使用 `HashPartitioner`。MapReduce 提供 `Partitioner` 接口，它的作用就是根据 key 或 value 及 reduce 的数量来决定当前的这对输出数据最终应该交由哪个 reduce task 处理。默认对 keyhash 后再以 reduce task 数量取模。默认的取模方式只是为了平均 reduce 的处理能力，如果用户自己对 Partitioner 有需求，可以订制并设置到 job 上。

5、接下来，会将数据写入内存，内存中这片区域叫做环形缓冲区，缓冲区的作用是批量收集 map 结果，减少磁盘 IO 的影响。我们的 key/value 对以及 Partition 的结果都会被写入缓冲区。当然写入之前，key 与 value 值都会被序列化成字节数组。

环形缓冲区其实是一个数组，数组中存放着 key、value 的序列化数据和 key、value 的元数据信息，包括 partition、key 的起始位置、value 的起始位置以及 value 的长度。环形结构是一个抽象概念。

缓冲区是有大小限制，默认是 100MB。当 map task 的输出结果很多时，就可能会撑爆内存，所以需要在一定条件下将缓冲区中的数据临时写入磁盘，然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为 Spill，中文可译为溢写。这个溢写是由单独线程来完成，不影响往缓冲区写 map 结果的线程。溢写线程启动时不应该阻止 map 的结果输出，所以整个缓冲区有个溢写的比例 spill.percent。这个比例默认是 `0.8`，也就是当缓冲区的数据已经达到阈值（`buffer size * spill percent = 100MB * 0.8 = 80MB`），溢写线程启动，锁定这 80MB 的内存，执行溢写过程。`Map task` 的输出结果还可以往剩下的 20MB 内存中写，互不影响。

6、当溢写线程启动后，需要对这 `80MB` 空间内的 `key` 做 排序 `Sort`。排序是 MapReduce 模型默认的行为，这里的排序也是对序列化的字节做的排序。

如果 job 设置过 `Combiner`，那么现在就是使用 Combiner 的时候了。将有相同 key 的 `key/value` 对的 value 加起来，减少溢写到磁盘的数据量。`Combiner` 会优化 MapReduce 的中间结果，所以它在整个模型中会多次使用。

那哪些场景才能使用 Combiner 呢？从这里分析，Combiner 的输出是 Reducer 的输入，Combiner 绝不能改变最终的计算结果。Combiner 只应该用于那种 `Reduce` 的输入 `key/value` 与输出 `key/value` 类型完全一致，且不影响最终结果的场景。比如累加，最大值等。Combiner 的使用一定得慎重，如果用好，它对 job 执行效率有帮助，反之会影响 reduce 的最终结果。

7、合并溢写文件： 每次溢写会在磁盘上生成一个临时文件（写之前判断是否有 `combiner`），如果 map 的输出结果真的很大，有多次这样的溢写发生，磁盘上相应的就会有多个临时文件存在。当整个数据处理结束之后开始对磁盘中的临时文件进行 merge 合并，因为最终的文件只有一个，写入磁盘，并且为这个文件提供了一个索引文件，以记录每个 reduce 对应数据的偏移量。至此 map 整个阶段结束。

mapTask 的一些基础设置配置（mapred-site.xml 当中）：
http://archive.cloudera.com/cdh5/cdh/5/hadoop-2.6.0-cdh5.14.0/hadoopmapreduce-client/hadoop-mapreduce-client-core/mapred-default.xml

设置一：设置环型缓冲区的内存值大小（默认设置如下）

`mapreduce.task.io.sort.mb 100`

设置二：设置溢写百分比（默认设置如下）

`mapreduce.map.sort.spill.percent 0.80`

设置三：设置溢写数据目录（默认设置）

`mapreduce.cluster.local.dir ${hadoop.tmp.dir}/mapred/local`

设置四：设置一次最多合并多少个溢写文件（默认设置如下）

`mapreduce.task.io.sort.factor 10`

## ReduceTask

`Reduce` 大致分为 `copy`、`sort`、`reduce` 三个阶段，重点在前两个阶段。
`copy` 阶段包含一个 eventFetcher 来获取已完成的 map 列表，由 Fetcher 线程去 copy 数据，在此过程中会启动两个 merge 线 程 ， 分 别 为 `inMemoryMerger` 和 `onDiskMerger`，分别将内存中的数据 merge 到磁盘和将磁盘中的数据进行 merge。
待数据 copy 完成之后，copy 阶段就完成了，开始进行 `sort` 阶段，sort 阶段主要是执行 finalMerge 操作，纯粹的 sort 阶段。
完成之后就是 `reduce` 阶段，调用用户定义的 reduce 函数进行处理。

详细步骤：

1、Copy 阶段，简单地拉取数据。Reduce 进程启动一些数据 copy 线程 (Fetcher)，通过 HTTP 方式请求 maptask 获取属于自己的文件。
2、Merge 阶段。这里的 merge 如 map 端的 merge 动作，只是数组中存放的是不同 map 端 copy 来的数值。Copy 过来的数据会先放入内存缓冲区中，这里的缓冲区大小要比 map 端的更为灵活。merge 有三种形式：内存到内存；内存到磁盘；磁盘到磁盘。默认情况下第一种形式不启用。当内存中的数据量到达一定阈值，就启动内存到磁盘的 merge。与 map 端类似，这也是溢写的过程，这个过程中如果你设置有 Combiner，也是会启用的，然后在磁盘中生成了众多的溢写文件。第二种 merge 方式一直在运行，直到没有 map 端的数据时才结束，然后启动第三种磁盘到磁盘的 merge 方式生成最终的文件。
3、合并排序。把分散的数据合并成一个大的数据后，还会再对合并后的数据排序。
4、对排序后的键值对调用 reduce 方法，键相等的键值对调用一次 reduce 方法，每次调用会产生零个或者多个键值对，最后把这些输出的键值对写入到 HDFS 文件中。

## Shuffle

`map` 阶段处理的数据如何传递给 `reduce` 阶段，是 `MapReduce` 框架中最关键的一个流程，这个流程就叫 `shuffle`。
`shuffle`: 洗牌、发牌 ——（核心机制：数据分区，排序，分组，规约，合并等过程）。

`shuffle` 是 `Mapreduce` 的核心，它分布在 `Mapreduce` 的 `map` 阶段和 `reduce` 阶段。一般把从 `Map` 产生输出开始到 `Reduce` 取得数据作为输入之前的过程称作 `shuffle`。

1. Collect 阶段：将 `MapTask` 的结果输出到默认大小为 `100M` 的环形缓冲区，保存的是 `key/value`，`Partition` 分区信息等。
2. Spill 阶段：当内存中的数据量达到一定的阀值的时候，就会将数据写入本地磁盘，在将数据写入磁盘之前需要对数据进行一次排序的操作，如果配置了 `combiner`，还会将有相同分区号和 `key` 的数据进行排序。
3. Merge 阶段：把所有溢出的临时文件进行一次合并操作，以确保一个 `MapTask` 最终只产生一个中间数据文件。
4. Copy 阶段：`ReduceTask` 启动 `Fetcher` 线程到已经完成 `MapTask` 的节点上复制一份属于自己的数据，这些数据默认会保存在内存的缓冲区中，当内存的缓冲区达到一定的阀值的时候，就会将数据写到磁盘之上。
5. Merge 阶段：在 `ReduceTask` 远程复制数据的同时，会在后台开启两个线程对内存到本地的数据文件进行合并操作。
6. Sort 阶段：在对数据进行合并的同时，会进行排序操作，由于 `MapTask` 阶段已经对数据进行了局部的排序，`ReduceTask` 只需保证 `Copy` 的数据的最终整体有效性即可。

`Shuffle` 中的缓冲区大小会影响到 `mapreduce` 程序的执行效率，原则上说，缓冲区越大，磁盘 io 的次数越少，执行速度就越快缓冲区的大小可以通过参数调整，参数：`mapreduce.task.io.sort.mb` 默认 `100M`

大数据处理技术-Hadoop-MapReduce

MapReduce 大数据处理技术 - Hadoop-MapReduce 理解 MapReduce 思想MapReduce 思想在生活中处处可见。或多或少都曾接触过这种思想。MapRe...

大数据处理技术-Hadoop-MapReduce编程模型-WordCount实例分析

MapReduce WordCount 大数据处理技术 - Hadoop-MapReduce 编程模型 - WordCount 实例分析 WordCount 示例编写需求：在一堆给定的文...

留言区

Notes
Hadoop
本文目录
<pjax>
</pjax>
<pjax></pjax>
<pjax> Are you Robot?</pjax>