MapReduce 浅析 (2)

本文目录

1 示例代码

2 In-Mapper Combining

3 Combiner

4 Partitioner

5 Comparator

6 完整架构图

7 相关文章


 

1 示例代码

示例代码出自《MapReduce 浅析 (1)》,本文将基于该示例代码进行分析。

import java.io.IOException;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

public class NumberCount {

    /**
     * Mapper     : 学生A、学生B、学生C
     * Object     : 输入数据 Key 类型
     * Text       : 输入数据 Value 类型
     * Text       : 输出数据 Key 类型(输出至 Reducer 的 Key 类型)
     * IntWritable: 输出数据 Value 类型(输出至 Reducer 的 Value 类型)
     */
    public static class StudentMapper extends Mapper<Object, Text, Text, IntWritable> {

        /**
         * Mapper Task: 统计各自内容中,每个数字出现的次数。
         * key        : 输入数据 Key (源自 Hadoop 分派的数据)
         * value      : 输入数据 Value (源自 Hadoop 分派的数据)
         * context    : 输出对象 Context (用于输出数据至 Reducer 的对象)
         */
        public void map(Object key, Text value, Context context) throws IOException, InterruptedException {

            // 用数组记录数字[0-9]个数
            int[] numberCount = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

            // 将输入字符串中的非数字内容替换为空
            String strNum = value.toString().replaceAll("[^0-9]", "");

            // 遍历替换后的字符串中的每个字符
            for (int index = 0; index < strNum.length(); index++) {
                int arrayIndex = strNum.charAt(index) - 48;
                numberCount[arrayIndex]++;
            }

            // 将统计结果以键值对<数字,个数>的形式提交
            for (int index = 0; index < numberCount.length; index++) {
                context.write(new Text(String.valueOf(index)), new IntWritable(numberCount[index]));
            }
        }
    }

    /**
     * Reducer    : 学生甲、学生乙
     * Text       : 输入数据 Key 类型(对应 Mapper 输出 Key 类型)
     * IntWritable: 输入数据 Value 类型(对应 Mapper 输出 Value 类型)
     * Text       : 输出数据 Key 类型 (输出最终数据的 Key 类型)
     * IntWritable: 输出数据 Value 类型 (输出最终数据的 Value 类型)
     */
    public static class StudentReducer extends Reducer<Text, IntWritable, Text, IntWritable> {

        /**
         * Reducer Task: 汇总来自Mapper的数据。
         * key         : 输入数据 Key (源自 Mapper 输出的 Key)
         * value       : 输入数据 Value (源自 Mapper 输出的一组 Value)
         * context     : 输出对象 Context (用于输出最终数据)
         */
        public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {

            IntWritable count = new IntWritable();

            // 汇总每个 Key(数字) 对应的 Value(个数) 值之和
            int sum = 0;
            for (IntWritable val : values) {
   			    sum += val.get();
            }
            count.set(sum);

            // 将汇总结果提交
            context.write(key, count);
        }
    }

    /**
     * 入口函数
     */
    public static void main(String[] args) throws Exception {

        Configuration conf = new Configuration();

        // 创建 Job
        Job job = new Job(conf, "Number Count");

        // 设置 Mapper
        job.setMapperClass(StudentMapper.class);
        // 设置 Reducer
        job.setReducerClass(StudentReducer.class);

        // 设置 Mapper 的输出 Key 类型
        job.setOutputKeyClass(Text.class);
        // 设置 Mapper 的输出 Value 类型
        job.setOutputValueClass(IntWritable.class);

        // 设置 Data 输入路径
        FileInputFormat.addInputPath(job, new Path("/user/hadoop/inputDemo"));
        // 设置 Result 输出路径
        FileOutputFormat.setOutputPath(job, new Path("/user/hadoop/output"));

        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }
}

 

2 In-Mapper Combining

在 Job 开始执行时,每个 Mapper 节点会创建一个 StudentMapper 类对象,用于处理各自的 InputSplit 数据。

每次读取一行数据,然后调用一次 map 方法,最后,调用 context.write() 方法,将处理结果提交至 Reducer 节点。

假设:某 Mapper 节点的 InputSplit 共10行字符串,每次调用 map 方法提交10个键值对。

最终,将有100个键值对需要从 Mapper 节点传输至 Reducer 节点。

为了减少键值对传输的数量,我们将 numberCount 改为全局变量,并在 Mapper 节点中使用 cleanup() 方法。

public static class StudentMapper extends Mapper<Object, Text, Text, IntWritable> {

    // 全局变量:用数组记录数字[0-9]个数
    private int[] numberCount = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

    public void map(Object key, Text value, Context context) throws IOException, InterruptedException {

        // 将输入字符串中的非数字内容替换为空
        String strNum = value.toString().replaceAll("[^0-9]", "");

        // 遍历替换后的字符串中的每个字符
        for (int index = 0; index < strNum.length(); index++) {
            int arrayIndex = strNum.charAt(index) - 48;
            numberCount[arrayIndex]++;
        }
    }

    /**
     * 介绍:当前节点的所有 map 任务完成后,最终调用一次 cleanup() 方法。
     * 当处理完所有 InputSplit 之后,最终统一提交 numberCount 中的数据至 Reducer 节点。
     */
    public void cleanup(Context context) throws IOException, InterruptedException {

        // 将统计结果以键值对<数字,个数>的形式提交
        for (int index = 0; index < numberCount.length; index++) {
            context.write(new Text(String.valueOf(index)), new IntWritable(numberCount[index]));
        }
    }
}

使用 In-Mapper Combining 设计后,最终每个 Mapper 节点只会传输10个键值对至 Reducer 节点。

提示:该设计需要注意 Mapper 节点的内存使用情况,避免由于本地缓存数据不断增多而导致的内存溢出情况。

 

3 Combiner

在 MapReduce 中,Mapper 节点将处理后的数据提交至 Reducer 进行归纳汇总。

为了减少二者之间的键值对传输数量,我们使用了 In-Mapper Combining 设计,除此之外,我们还可以通过 Combiner 来达到这一目的。

Combiner 与 Mapper 处于同一节点(同一主机),可以将 Combiner 理解为 Mapper 节点内部的 Reducer。

当 Mapper 节点处理完数据后,Combiner 先进行一次归纳汇总,将可以合并的键值对进行合并,然后再提交至 Reducer 节点,从而减少键值对的传输量。

若 Combiner 的处理规则与 Reducer 的处理规则完全一致,则可以直接将 Reducer 类作为 Combiner 添加至 Job 中。

若二者的处理规则不同,则需要重新定义一个 Combiner 类(同样继承Reducer),然后将其作为 Combiner 添加至 Job 中。

public class NumberCount {

    /**
     * 入口函数
     */
    public static void main(String[] args) throws Exception {

        Configuration conf = new Configuration();

        // 创建 Job
        Job job = new Job(conf, "Number Count");

        // 此处省略其它代码

        // 设置 Combiner (规则相同,直接将 Reducer 类作为 Combiner 使用)
        job.setCombinerClass(StudentReducer.class);

        // 此处省略其它代码
    }
}

 

4 Partitioner

Partitioner 是 "Shuffle and Sort" 层的重要部分,用于分割中间数据(Mapper提交的数据),将中间数据映射至相应的 Reducer 节点,即决定每个中间数据应该由哪个 Reducer 来处理。

public static class NumberCount {

    /**
     * Partitioner类
     */
    public static class StudentPartitioner extends Partitioner<Text, Text> {

        /**
         * key            : Mapper 输出的 Key
         * value          : Mapper 输出的 Value
         * numReduceTasks : Reducer 的数量
         */
        @Override
        public int getPartition(Text key, IntWritable value, int numReduceTasks) {

            // 将 Key 值转为 int 类型
            int number = Integer.parseInt(key.toString().trim());

            // 未使用 Reducer 情况
            if(numReduceTasks == 0)
            {
                return 0;
            }

            // 根据 number 值,决定对应的 Reducer 编号。
            if (number <= 1) {
                return 0;                   // 数字0-1的数据交由0号 Reducer 处理
            } else if (number <= 3) {
                return 1 % numReduceTasks;  // 数字2-3的数据交由(1 % numReduceTasks)号 Reducer 处理
            } else if (number <= 5) {
                return 2 % numReduceTasks;  // 数字4-5的数据交由(2 % numReduceTasks)号 Reducer 处理
            } else if (number <= 7) {
                return 3 % numReduceTasks;  // 数字6-7的数据交由(3 % numReduceTasks)号 Reducer 处理
            } else {
                return 4 % numReduceTasks;  // 数字8-9的数据交由(4 % numReduceTasks)号 Reducer 处理
            }
        }
    }

    /**
     * 入口函数
     */
    public static void main(String[] args) throws Exception {

        Configuration conf = new Configuration();

        // 创建 Job
        Job job = new Job(conf, "Number Count");

        // 此处省略其它代码

        // 设置 Partitioner
        job.setPartitionerClass(StudentPartitioner.class);

        // 此处省略其它代码
    }
}

说明:百分号(%)表示取余操作。

若 Reducer 数量为1个,则 1 % numReduceTasks = 1 % 1 = 0,即交由0号 Reducer 处理;

若 Reducer 数量为2个,则 1 % numReduceTasks = 1 % 2 = 1,即交由1号 Reducer 处理;

若 Reducer 数量为3个,则 1 % numReduceTasks = 1 % 3 = 1,即交由1号 Reducer 处理;

若 Reducer 数量为3个,则 2 % numReduceTasks = 2 % 3 = 2,即交由2号 Reducer 处理;

若 Reducer 数量为3个,则 3 % numReduceTasks = 3 % 3 = 0,即交由0号 Reducer 处理;

 

5 Comparator

Comparator 也是 "Shuffle and Sort" 层的重要部分,用于对 Mapper 的输出数据,按照 Key 值进行排序,即决定到达 Reducer 的顺序。

在 Mapper 节点输出的数据,会根据 Comparator 规则,进行一次本地排序;

然后,在 Reducer 节点接收数据时,也会根据 Comparator 规则,对来自不同 Mapper 节点的数据,再进行一次整体排序。

public static class NumberCount {

    /**
     * Comparator类
     */
    public static class StudentComparator extends WritableComparator {

        @Override
        public DistrictComparator() {
            // Mapper 输出的 Key 为 Text 类型,即对 Text 类型数据进行排序
            super(Text.class, true);
        }

        /*
         * 实现倒序排序
         */
        public int compare(WritableComparable a, WritableComparable b) {
            int key1 = Integer.parseInt(((Text) a).toString());
            int key2 = Integer.parseInt(((Text) b).toString());
			
            if (key1 < key2)
                return 1;
            else if (key1 > key2)
                return -1;
            else
                return 0;
        }
    }

    /**
     * 入口函数
     */
    public static void main(String[] args) throws Exception {

        Configuration conf = new Configuration();

        // 创建 Job
        Job job = new Job(conf, "Number Count");

        // 此处省略其它代码

        // 设置 Comparator
        job.setSortComparatorClass(StudentComparator.class);

        // 此处省略其它代码
    }
}

 

6 完整架构图

 

7 相关文章

《MapReduce 浅析 (1)》

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值