MapReduce概述

前言

mapreduce是hadoop的计算框架,它与hdfs关系紧密。可以说它的计算思想是基于分布式文件而设计的。

MR计算模型

MapReduce最早是由Google公司研究提出的一种面向大规模数据处理的并行计算模型和方法。也可以说它是:“分布式计算的始祖”。
在这里插入图片描述

计算流程是:输入分片 —> map阶段 —> combiner阶段(可选) —> shuffle阶段 —> reduce阶段

输入分片(input split)

在进行map计算之前,mapreduce会根据输入文件计算输入分片,每个输入分片针对一个map任务。

  • 关于 mapreduce分片算法为:
           假如我们设定hdfs的块的大小是64mb,输入有三个文件,大小分别是3mb、65mb和127mb,那么mapreduce会把3mb文件分为一个输入分片,65mb则是两个输入分片而127mb也是两个输入分片.换句话说我们可以通过合并小文件做输入分片调整,那么就不会有5个map任务,而且每个map执行的数据大小不均的情况发生。
  • 关于读取其它数据源:
           例如mysql,先将mysql数据读取到hdfs上,然后通过以上分片算法分片
map阶段

map操作都是本地化操作也就是在数据存储节点上进行,负责将当前存储节点上的数据,整理成K,V格式。要程序员编写。

K,V格式:

  • K:存储用于关键的字段信息(类似于sql中, join的条件字段)
  • V:其它数据信息 (类似于sql中,整行)
combiner阶段

combiner阶段是程序员可选择的,combiner其实也是一种reduce操作。它是map运算的后续操作,主要是在map计算出中间文件前做一个简单的合并重复key值的操作。例如我们对文件里的单词频率做统计,map计算时候如果碰到一个hadoop的单词就会记录为1,但是这篇文章里hadoop可能会出现n多次,那么map输出文件冗余就会很多,因此在reduce计算前对相同的key做一个合并操作,那么文件会变小,这样就提高了宽带的传输效率,毕竟hadoop计算力宽带资源往往是计算的瓶颈也是最为宝贵的资源,但是combiner操作是有风险的,使用它的原则是combiner的输入不会影响到reduce计算的最终输入,例如:如果计算只是求总数,最大值,最小值可以使用combiner,但是做平均值计算使用combiner的话,最终的reduce计算结果就会出错。

combine时一个本地化的reduce操作,对相同的key做一个合并操作,提高带宽的利用率

shuffle阶段

将map的输出作为reduce的输入的过程就是shuffle了,这个是mapreduce优化的重点地方。
在这里插入图片描述

  1. 每个 Map 任务的计算结果都会写入到本地文件系统

map写入磁盘的过程十分的复杂,内存开销是很大的,map在做输出时候会在内存里开启一个环形内存缓冲区,这个缓冲区专门用来输出的,默认大小是100mb,并且在配置文件里为这个缓冲区设定了一个阀值,默认是0.80,如果缓冲区的内存达到了阀值的80%时候,这个守护线程就会把内容写到磁盘上,这个过程叫spill

  1. 等Map任务快要计算完成的时候,MapReduce 计算框架会启动 shuffle 过程.在 Map 任务进程调用一个 Partitioner 接口,对 Map 产生的每个 <key, value> 进行 Reduce 分区选择,然后通过 HTTP 通信发送给对应的 Reduce 进程。
  • MapReduce 框架默认的 Partitioner 用 Key 的哈希值对 Reduce任务数量取模,相同的 Key 一定会落在相同的 Reduce 任务 ID
  • 如果reduce对顺序有要求,可以定义每个partition的边界,大的数据到一个Reduce,后序只要把各个reduce的结果相加就行。可能会导致每个partition上分配到的记录数相差很大,hadoop提供了采样器帮我们预估整个边界,以使数据的分配尽量平均。
  1. Reduce 任务进程对收到的数据进行排序和合并,相同的 Key 放在一起,组成一个 传递给 Reduce 执行

  2. 如果我们定义了combiner函数,那么排序前还会执行combiner操作。1步骤中的数据结点,也会进行2,3步骤的模拟。产生结果后再继续2,3步骤。

reduce阶段

针对shuffle阶段准备好的输入开始计算。

用MR实现left-join

我们要把数据库的数据存储和计算进行分离。假如计算引挚选用MR.怎么做呢?

Hive就是基于MR和HDFS思想实现的

假设对如下2张表做leftjoin

factory表:
factoryname                    addressed
Beijing Red Star                    1
Shenzhen Thunder                   3
Guangzhou Honda                    2
Beijing Rising                      1
Guangzhou Development Bank           2
Tencent                         3
Back of Beijing                    1

address表:
addressID    addressname
1            Beijing
2            Guangzhou
3            Shenzhen
4            Xian

取出两个表中共同列作为map中的key,同时需要标识每个列所在的表,供在reduce中拆分

//汇聚所有addressID相同的
protected void map(LongWritable key, Text value,Context context)
            throws IOException, InterruptedException {
        String path = ((FileSplit)context.getInputSplit()).getPath().getName();//获取文件名
        String line = value.toString();
        StringTokenizer st = new StringTokenizer(value.toString());
        String[] tmp = line.split("    +");
        if(tmp.length ==2){
            String first = tmp[0];
            String second = tmp[1];
            if(path.equals("factory")){
                if(first.equals("factoryname")) return;
                k.set(second);
                v.set(first+"1");
            }else if(path.equals("address")){
                if(second.equals("addressname")) return;
                k.set(first);
                v.set(second+"2");
            }
            context.write(k,v);
        }
    }

//以factory为主表拆分
protected void reduce(Text key, Iterable<Text> value,Context context)
            throws IOException, InterruptedException {
            List<String> factory = new ArrayList<String>();
            List<String> address = new ArrayList<String>();
            for(Text val : value){
                String str = val.toString();
                String stf = str.substring(str.length()-1);
                String con = str.substring(0,str.length()-1);
                int flag = Integer.parseInt(stf);
                if(flag == 1){
                    factory.add(con);
                }else if(flag ==2){
                    address.add(con);
                }
            }
            for(int i=0;i<factory.size();i++){
                k.set(factory.get(i));
                for(int j=0;j<address.size();j++){
                    v.set(address.get(j));
                    context.write(k, v);
                }
            }
    }    

再来聊combiner

在map端使用combiner合并数据可以减少需要通过网络io的数据,有效增加map reduce程序的运行效率。

combiner默认直接采用已有的reducer代码,而采用这种相同逻辑的combiner要求提前执行combiner程序,合并的数据不会影响到reducer端最终的合并。

如果上述“left join”案例使用combiner,将会改变reduce的输入。另结果异常。比如统计单词、求最大/小值等,这些程序的数据提前合并不会影响到reducer端的最终合并。再以比如求平均数为例子


public class AverageMapper extends Mapper<LongWritable, Text, Text, FloatWritable> {
    private Text text = new Text();
    private FloatWritable number = new FloatWritable();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String[] row = value.toString().split("\t");
        String student = row[0];
        String score = row[2];
        text.set(student);
        number.set(Float.parseFloat(score));
        context.write(text, number);
    }
}

public class AverageReducer extends Reducer<Text, FloatWritable, Text, FloatWritable> {
    private FloatWritable avg = new FloatWritable();

    @Override
    protected void reduce(Text key, Iterable<FloatWritable> values, Context context) throws IOException, InterruptedException {
        float sum = 0;
        int count = 0;
        for (FloatWritable value : values) {
            sum += value.get();
            count++;
        }
        avg.set(sum / count);
        context.write(key, avg);
    }
}

如果使用

job.setCombinerClass(AverageReducer.class);

最终的结果:可以发现最终结果比正确的平均值变小了。主要错误是在map端使用combiner程序进行提前聚合。

public class CombinerMapper extends Mapper<LongWritable, Text, Text, Text> {
    private Text student = new Text();
    private Text score = new Text();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String[] split = value.toString().split("\t");
        student.set(split[0]);
        score.set(split[2]);
        context.write(student, score);
    }
}

public class AverageCombiner extends Reducer<Text, Text, Text, Text> {
    private Text text = new Text();
    @Override
    protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
        int sum = 0;
        int count = 0;
        for (Text value : values) {
            sum += Integer.parseInt(value.toString());
            count++;
        }
        text.set("" + sum + "\t" + count);
        context.write(key, text);
    }
}

public class CombinerReducer extends Reducer<Text, Text, Text, FloatWritable> {
    private FloatWritable avg = new FloatWritable();

    @Override
    protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
        float sum = 0;
        float count = 0;
        for (Text value : values) {
            String[] split = value.toString().split("\t");
            sum += Integer.parseInt(split[0]);
            count += Integer.parseInt(split[1]);
        }
        avg.set(sum / count);
        context.write(key, avg);
    }
}

使用上述自定义combiner求平均值的mapreduce程序就能得到最大的结果。

Partition机制

除了上述自定义combiner方式,mapReduce还可以自定义分区 Partition.
mapreducer shuffle数据向reducer输出的时是根据HashPartitioner分区器来进行数据的分区的。

如果碰到:需要将相同号码段的手机号码放到同一个文件中,比如135开头的一个文件,136开头的文件

public class CusPartition extends Partitioner<Text, LongWritable> {
    @Override
    public int getPartition(Text key, LongWritable value, int numPartitions) {
        int partition = 3;
        // 获取key的前三位 135 -》0 136 -》1 137 -》2 138 -》 3
        String index = key.toString().substring(0, 3);
        if ("135".equals(index)){
            partition = 0 ;
        } else if ("136".equals(index)){
            partition = 1;
        } else if ("137".equals(index)){
            partition = 2;
        }
        return partition;
    }
}

可以通过

job.setPartitionerClass(CusPartition.class);

来设默认分区规则,来达到我们的效果

MR排序

排序是 MR 中非常重要的操作之一

  1. MapTask 和 ReduceTask 都会对数据按照 key 进行排序。该操作是默认行为。任何 MR 程序中数据均会被排序,而不看逻辑是否需要。
  2. MapTask 中,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率到一定的阈值,再对缓冲区数据进行一次快排,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。
  3. ReduceTask 中,它从每个 MapTask 上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写到磁盘上,否则储存在内存上。如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件。如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完后,ReduceTask 统一对内存和磁盘上的所有数据进行一次归并排序。

ps: 每一个ReduceTask生成一个文件,每一个MapTask对应一个文件

部分排序

MapReduce 根据输入记录的键对数据集排序,保证输出的每个文件内部有序。

// key需要实现 WritableComparable 接口重写 compareTo 方法,就可以实现部分排序

@Override
public int compareTo(FlowBean o) {

	int result;
		
	// 按照总流量大小,倒序排列
	if (sumFlow > bean.getSumFlow()) {
		result = -1;
	}else if (sumFlow < bean.getSumFlow()) {
		result = 1;
	}else {
		result = 0;
	}

	return result;
}
全局排序

只设置一个ReduceTask最终输出结果只要一个文件,且文件内部有序.

该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了 MapReduce 所提供的并行架构。

通过Partition + 部分排序的方式,生成多个有序文件。然后按分区顺序将文件合并。

辅助排序

在 Reduce 端对 key 进行分组。应用于:在接收的 key 为 bean 对象时,想让一个或几个字段相同(全部字段比较不相同)的 key 进入到同一个 reduce 方法时,可以采用分组排序。

// 实现WritableComparator接口,并通过以下函数设置
job.setGroupingComparatorClass(OrderGroupingComparator.class);
二次排序

对进入同一个reduce的 键 或键的部分 进行排序,即 map的排序不是我reduce输入想要的排序

job.setSortComparatorClass(SortComparator.class);

主要参考

《hadoop权威指南》
Mapreduce的排序(全局排序、分区加排序、Combiner优化)

MR – WritableComparable排序

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值