MapReduce概述

 

单词计数案例

需求

在一堆给定的文本文件中统计输出每一个单词出现的总次数

环境准备

在 /opt/test 目录下创建一个文件 wordcount.txt ,里面键入几个单词,并用空格分隔开

Java实现

package com.zyd;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author: ZYD
 * @Date: 2021/7/30 下午 21:28
 */

/**
 * 如果使用Java实现这个单词计数存在的问题:
 * 1. 操作的数据是HDFS分布式集群,可能这个文件有几个G的大小,如果使用Java,那么我们必须把这些数据全部拉取到JVM内存中去运行,可能会导致Java内存崩溃
 * 2. 如果我们不把这些数据全部拉取到本地操作,那么可能会把这个Java程序运行在不同的电脑上执行,只需要处理当前机器上的block块的数据即可
 *      但是这样也会存在问题:
 *          Java代码需要分布式的运行在不同的电脑上,那么处理结束之后将面临如何将处理结果汇总起来、如何把控每一个节点上程序有没有运行结束这些问题
 */
public class WordCountJava {
    public static void main(String[] args) {
        // 1. 连接HDFS集群
        Configuration conf = new Configuration();
        FileSystem fs = null;
        try {
            fs = FileSystem.get(new URI("hdfs://192.168.218.55:9000"), conf, "root");
            /**
             * 2. 读取文件数据 ----- IO流
             * 四个抽象父类:
             * 字节流:InputStream、OutputStream
             * 字符流:Reader、Writer
             */
            // 创建单词计数文件的输入流
            FSDataInputStream open = fs.open(new Path("/test/wordcount.txt"));
            // 字节转字符流
            InputStreamReader isr = new InputStreamReader(open);
            BufferedReader br = new BufferedReader(isr);
            String line;
            // 准备一个map集合存放数据,数据格式:单词,单词出现的次数
            Map<String, Integer> map = new HashMap<>();
            // 通过BufferedReader字符缓冲流的readline方法依次读取一行数据
            // 将每一行数据按空格分隔,统计次数
            /**
             * 为什么要用(line = br.readLine()) != null 而不用下面的方法?
             * 之所以使用(line = br.readLine()) != null判断,是因为readline()方法的特性:
             * readline是用来判断当前行有没有数据,如果有数据的话,那么将这一行的数据赋值给一个String类型的变量,然后将这个指针下移
             * 用下面的方法的话,意味着调用了两次readline方法,再调用这个readline方法的时候,他返回的就不是当前行的数据了,而是下一行的数据
             * 此时也就代表 line 这个字符串里存储的是第二行的数据
             */
            while (((line = br.readLine()) != null)) {
//            while (br.readLine() != null) {
//                line = br.readLine();
                System.out.println(line);
                String[] words = line.split(" ");
                /**
                 * 如果单词还没有在map集合添加,name在map集合加入 word,1
                 * 如果出现,那么在map集合添加 word,以前的次数+1
                 */
                for (String word: words) {
                    if (map.get(word) == null) {
                        map.put(word, 1);
                    } else {
                        map.put(word, map.get(word) + 1);
                    }
                }
            }
            System.out.println(map);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fs != null) {
                try {
                    fs.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Java操作注意事项

  • (面试题)通过代码的编写可以发现,在写while循环时,使用了(line = br.readLine()) != null 判断,而不是while (br.readLine() != null) {line = br.readLine(); ……},这是为什么呢?

原因:之所以使用(line = br.readLine()) != null判断,是因为readline()方法的特性:
* readline是用来判断当前行有没有数据,如果有数据的话,那么将这一行的数据赋值给一个String类型的变量,然后将这个指针下移
* 用下面的方法的话,意味着调用了两次readline方法,再调用这个readline方法的时候,他返回的就不是当前行的数据了,而是下一行的数据
* 此时也就代表 line 这个字符串里存储的是第二行的数据

  • 使用Java实现这个单词计数存在的问题:
    1. 操作的数据是HDFS分布式集群,可能这个文件有几个G的大小,如果使用Java,那么我们必须把这些数据全部拉取到JVM内存中去运行,可能会导致Java内存崩溃
    2. 如果我们不把这些数据全部拉取到本地操作,那么可能会把这个Java程序运行在不同的电脑上执行,只需要处理当前机器上的block块的数据即可。但是这样也会存在问题:Java代码需要分布式的运行在不同的电脑上,那么处理结束之后将面临如何将处理结果汇总起来、如何把控每一个节点上程序有没有运行结束这些问题

MapReduce实现

编写MR程序过程:

  • 编写Mapper阶段:继承Mapper类,定义好数据的输入和输出类型
  • 编写Reducer阶段:继承Reducer类,定义好数据的输入和输出类型
  • 编写Driver类:程序的运行入口,将Mapper阶段和Reducer阶段关联圈起来,并且定义输入文件和输出文件地址

源代码:

1. WordCountMapper.java

package MapReduce;

/**
 * @Author: ZYD
 * @Date: 2021/8/1 下午 22:44
 */

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

/**
 * 第一步:继承Mapper类(定义数据的输入格式和数据的输出形式)
 *      如果要使用MR程序,我们要指定数据输入的kye-value键值对类型
 *      同时要指定数据输出的key-value键值对类型
 *  Mapper阶段----一行数据执行一次Mapper
 *  Mapper阶段的输入key-value键值对格式很固定:LongWritable Text
 *      其中LongWritable 是 long 基本数据类型的hadoop序列化的类,一般情况下Map阶段的key代表的是文件的偏移量----理解为行号
 *      Text 是String类型的hadoop序列化类,代表的是每一行的数据
 *
 */
public class WordCountMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
    /**
     * mapTask的核心处理逻辑:
     *      每一行文件数据需要走一个map方法
     * @param key ----- Mapper阶段输入的key值-----行号
     * @param value -----Mapper阶段输入的value值-----当前行对应的字符串数据
     * @param context ------上下文对象,主要功能是为了实现将Map阶段的数据输入到Reduce阶段
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        // 第一步:将每一行数据value按照空格切割,然后将每一个得到的单词当做 key,1当做value,将他们输出到Reduce阶段
        String line = value.toString();
        String[] words = line.split(" ");
        for (String word: words) {
            // 将每一行数据空格切割后的单词以单词为key,1为value输出到reduce阶段
            // 到了reduce阶段,会根据key值将value给聚合起来
            context.write(new Text(word), new LongWritable(1));
        }
    }
}

2. WordCountReducer.java

package MapReduce;

/**
 * @Author: ZYD
 * @Date: 2021/8/1 下午 23:01
 */

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;
import java.util.Iterator;

/**
 * 继承Reducer类
 * Reducer类也粗腰指定输入的key-value的数据类型,也需要指定输出的key-value数据类型
 *
 * 此时Reducer阶段的输入的key-value键值对类型不能随便指定,它应该是Mapper阶段的输出数据类型
 */
public class WordCountReduce extends Reducer<Text, LongWritable, Text, LongWritable> {
    /**
     * reduceTask核心处理业务逻辑的方法
     *      他是每一组key值相同的数据执行一次
     * @param key ------ map阶段输出的key值 ---- 单词
     * @param values ------ values是一个类似于集合的数据,里面放的是key相同的数据的所有value值的集合
     * @param context ------ 上下文对象,将结果以<key, value>键值对的形式输出到最终的结果文件中
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
        /**
         * 思路:将values中的数据累加起来 ,这样的话单词对应出现的次数就确定了
         */
        Iterator<LongWritable> iterator = values.iterator();
        long result = 0L;
        while (iterator.hasNext()) {
            LongWritable next = iterator.next();
            result += next.get();
        }
        context.write(key, new LongWritable(result));
    }
}

3. WordCountDriver.java

package MapReduce;

/**
 * @Author: ZYD
 * @Date: 2021/8/1 下午 23:01
 */

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;
import java.util.Iterator;

/**
 * 继承Reducer类
 * Reducer类也粗腰指定输入的key-value的数据类型,也需要指定输出的key-value数据类型
 *
 * 此时Reducer阶段的输入的key-value键值对类型不能随便指定,它应该是Mapper阶段的输出数据类型
 */
public class WordCountReduce extends Reducer<Text, LongWritable, Text, LongWritable> {
    /**
     * reduceTask核心处理业务逻辑的方法
     *      他是每一组key值相同的数据执行一次
     * @param key ------ map阶段输出的key值 ---- 单词
     * @param values ------ values是一个类似于集合的数据,里面放的是key相同的数据的所有value值的集合
     * @param context ------ 上下文对象,将结果以<key, value>键值对的形式输出到最终的结果文件中
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
        /**
         * 思路:将values中的数据累加起来 ,这样的话单词对应出现的次数就确定了
         */
        Iterator<LongWritable> iterator = values.iterator();
        long result = 0L;
        while (iterator.hasNext()) {
            LongWritable next = iterator.next();
            result += next.get();
        }
        context.write(key, new LongWritable(result));
    }
}

为什么要使用MapReduce

  1. 海量数据在单机上处理因为硬件资源限制,无法胜任
  2. 而一旦将单机版程序(普通程序)扩展到集群在分布式运行,将极大增加程序的复杂度和开发难度
  3. 引入MapReduce框架(本身就是分布式)后,开发人员可以将绝大部分工作集中在业务逻辑的开发上,而将分布式计算中的复杂性交由框架来处理

分布式方案考虑的问题:

  1. 运行逻辑要不要先分后合(先把代码运行在不同的服务器上,最终将结果合并起来)
  2. 程序如何分配运算任务(切片)
  3. 两个阶段的程序如何启动?如何协调?
  4. 整个程序运行过程中的监控?容错?重试?

分布式方案需要考虑很多问题,但是我们可以将分布式程序中的公共功能封装成框架,让开发人员将精力集中在业务逻辑上,而MapReduce就是这样一个分布式程序的通用框架

MapReduce核心思想-----分而治之,先分后合

分布式的运算程序需要分成至少两个阶段:

  1. 将程序分布在不同的节点(电脑)上,将不同节点的程序相互运算出结果(完全并行运行),互不相干 -------- maptask阶段
  2. 并行运行,这一阶段数据需要依赖于上一阶段的并发实例的输出(即maptask阶段全部执行完毕才可以开始这一阶段)-------- reducetask阶段'

MapReduce编程模型只能包含一个map阶段和一个reduce阶段,如果用户业务逻辑非常复杂,那就只能使用多个MapReduce程序串行运行

Notes:Reduce Task的个数由业务来决定'

MapReduce进程

一个完整的mapreduce程序在分布式运行时有三类实例进程:

  1. MrAppMaster:负责整个程序的过程调度及状态协调(YARN),是程序的管理者,管理Map和Reduce任务
  2. MapTask:负责map阶段的整个数据处理流程(只分,不合),将程序运行在不同节点上,每个节点只负责一部分数据, 并行运行,互不干扰
  3. ReduceTask:负责reduce阶段的整个数据处理流程,将Mapper阶段处理完成的数据合并起来处理,也可以有多个reduce,并且多个reduce之间是并行运行,互不干扰的,但是reduce的执行需要依赖map阶段的数据

MapReduce编程规范(八股文)

用户编写的程序分成三个部分:Mapper,Reducer,Driver(提交运行mr程序的客户端)------三步编程法

Mapper阶段----编写Mapper类,即MapTask任务

  • 用户自定义的Mapper要继承自己的父类
  • Mapper的输入数据是KV对的形式(KV的类型可自定义)
  • Mapper中的业务逻辑写在map()方法中
  • Mapper的输出数据是KV对的形式(KV的类型可自定义)
  • map()方法(maptask进程)对每一个<K,V>调用一次(即一行调用一次map方法

Reducer阶段----编写Reducer类,即ReduceTask任务

  • 用户自定义的Reducer要继承自己的父类
  • Reducer的输入数据类型对应Mapper的输出数据类型,也是KV
  • Reducer的业务逻辑写在reduce()方法中
  • Reducetask进程对每一组相同k的<k,v>组调用一次reduce()方法(一个key调用一次reduce方法)

Driver阶段----关联MapTask和ReduceTask任务,并且提交运行

整个程序需要一个Drvier来进行提交,提交的是一个描述了各种必要信息的job对象

Notes:

  1. MapReduce程序处理的数据大部分都是HDFS上的文件数据
  2. Map阶段和Reduce阶段数据都需要通过key-value键值对形式进行输入和输出
  3. 一般情况下Map阶段的输出的key-value的键值对就是Reducer阶段输出的key-value键值对
  4. 一般情况下一个MapReduce程序只能有一个Map阶段和一个Reduce阶段,如果程序复杂,需要编写多个MR程序串行运行

MapReduce程序运行流程

 

 

 

       首先将存储在文件中的数据分片(假设分为三片),之后三个maptask同时作业,当所有maptask任务完成后,启动相应数量的ReduceTask,并告知ReduceTask处理数据的范围(数据分区)

 

1)     在MapReduce程序读取文件的输入目录上存放相应的文件。客户端程序在submit()方法执行前,获取待处理的数据信息,然后根据集群中参数的配置形成一个任务分配规划。

 

2)     客户端提交job.split、jar包、job.xml等文件给yarn,yarn中的resourcemanager启动MRAppMaster。

 

3)     MRAppMaster启动后根据本次job的描述信息,计算出需要的maptask实例数量,然后向集群申请机器启动相应数量的maptask进程。

 

4)     maptask利用客户指定的inputformat来读取数据,形成输入KV对。

 

5)     maptask将输入KV对传递给客户定义的map()方法,做逻辑运算

 

6)     map()运算完毕后将KV对收集到maptask缓存。

 

7)     maptask缓存中的KV对按照K分区排序后不断写到磁盘文件

 

8)     MRAppMaster监控到所有maptask进程任务完成之后,会根据客户指定的参数启动相应数量的reducetask进程,并告知reducetask进程要处理的数据分区。

 

9)     Reducetask进程启动之后,根据MRAppMaster告知的待处理数据所在位置,从若干台maptask运行所在机器上获取到若干个maptask输出结果文件,并在本地进行重新归并排序,然后按照相同key的KV为一个组,调用客户定义的reduce()方法进行逻辑运算。

 

10)  Reducetask运算完毕后,调用客户指定的outputformat将结果数据输出到外部存储。

 

posted @ 2021-08-05 12:14  张涨涨  阅读(86)  评论(0编辑  收藏  举报
Live2D