大数据开发MapReduce(第八篇)

1,400 阅读8分钟

一、MapReduce介绍

1.1、MapReduce计算思想

举个例子,要统计扑克牌的黑桃个数,最直接的办法是一张一张检查然后进行统计。利用MapReduce的计算方法,可以优化为:

  1. 把牌分配给在座所有玩家
  2. 每个玩家自己检查有多少张黑桃,然后把黑桃数目上报
  3. 把所有玩家的数字进行累加,得到最终的结果

总结:这个采取的就是分治的思想

1.2、分布式计算
  1. 对每个节点上的数据进行局部计算
  2. 对每个节点上面计算的局部结果进行最终全局汇总
1.3、MapReduce原理剖析
  1. MapReduce是一种分布式计算模型,是Google提出来的,主要用于搜索领域,解决海量数据的计算问题。
  2. MapReduce是分布式运行的,由两个阶段组成:Map和Reduce

在这map就是对数据进行局部汇总,reduce就是对局部数据进行最终汇总。

image-20220905232945761

二、MapReduce

mapreduce主要分为两大步骤 map和reduce,map和reduce在代码层面对应的就是两个类,map对应的是mapper类,reduce对应的是reducer类,下面我们就来根据一个案例具体分析一下这两个步骤 假设我们有一个文件,文件里面有两行内容

hello world
say hello

我们想要统计文件中每个单词出现的总次数

2.1、Map阶段
  1. 框架会把输入文件(夹)划分为很多InputSplit,这里的inputsplit就是前面我们所说的split【对文件进行逻辑划分产生的】,默认情况下,每个HDFS的Block对应一个InputSplit。再通过RecordReader类,把每个InputSplit解析成一个一个的<k1,v1>。默认情况下,每一行数据,都会被解析成一个<k1,v1>这里的k1是指每一行的起始偏移量,v1代表的是那一行内容,所以,针对文件中的数据,经过map处理之后的结果是这样的。

    <0,hello world> 第一行是从0开始
    <12,say hello>  第二行是从12开始
    
  2. 框架调用Mapper类中的map(…)函数,map函数的输入是<k1,v1>,输出是<k2,v2>。一个InputSplit对应一个Map Task

    注意:

    默认情况下,这个Mapper类里面的Map函数是没有实现,需要自己动手实现

    因为我们需要统计文件中每个单词出现的总次数,所以需要先把每一行内容中的单词切开,然后记录出现次数为1,这个逻辑就需要我们在map函数中实现了。

    针对<0,hello world>执行这个步骤2的结果是
    <hello,1>
    <world,1>
    针对<12,say hello>执行这个步骤2的结果是
    <say,1>
    <hello,1>
    
  3. 框架对map函数输出的<k2,v2>进行分区。不同分区中的<k2,v2>由不同的reduce task处理,默认只有1个分区,所以所有的数据都在一个分区,最后只会产生一个reduce task。

    咱们在这所说的单词计数,其实就是把每个单词出现的次数进行汇总即可,需要进行全局的汇总,不需要进行分区,所以一个redeuce任务就可以搞定

    <hello,1>
    <world,1>
    <say,1>
    <hello,1>
    
  4. 框架对每个分区中的数据,都会按照k2进行排序、分组。分组指的是相同k2的v2分成一个组

    先按照k2排序(这里按照英文单词排序)
    <hello,1>
    <hello,1>
    <say,1>
    <world,1>
    ​
    再进行分组
    <hello,{1,1}>
    <say,{1}>
    <world,{1}>
    
  5. 在map阶段,框架可以选择执行Combiner过程(可选)

    Combiner可以翻译为规约,规约是什么意思呢? 在刚才的例子中,咱们最终是要在reduce端计算单词出现的总次数的,所以其实是可以在map端提前执行reduce的计算逻辑,先对在map端对单词出现的次 数进行局部求和操作,这样就可以减少map端到reduce端数据传输的大小,这就是规约的好处,当然了,并不是所有场景都可以使用规约,针对求平均值之类的操作就不能使用规约了,否则最终计算的结果就不准确了。

    Combiner一个可选步骤,默认这个步骤是不执行的

  6. 框架会把map task输出的<k2,v2>写入到linux 的磁盘文件中。至此,整个map阶段执行结束

注意:

MapReduce程序是由map和reduce这两个阶段组成的,但是reduce阶段不是必须的,也就是说有的mapreduce任务只有map阶段,为什么会有这种任务呢?

是这样的,咱们前面说过,其实reduce主要是做最终聚合的,如果我们这个需求是不需要聚合操作直接对数据做过滤处理就行了,那也就意味着数据经过map阶段处理完就结束了,所以如果reduce阶段不存在的话,map的结果是可以直接保存到HDFS中的

注意,如果没有reduce阶段,其实map阶段只需要执行到第二步就可以,第二步执行完成以后,结果就可以直接输出到HDFS了。

2.2、Reduce阶段
  1. 框架对多个map任务的输出,按照不同的分区,通过网络copy到不同的reduce节点。这个过程称作shuffle

    针对我们这个需求,只有一个分区,所以把数据拷贝到reduce端之后还是老样子

    <hello,{1,1}>
    <say,{1}>
    <world,{1}>
    
  2. 框架对reduce端接收的相同分区的<k2,v2>数据进行合并、排序、分组。reduce端接收到的是多个map的输出,对多个map任务中相同分区的数据进行合并、排序、分组 注意:

    之前在map中已经做了排序 分组,这边也做这些操作 重复吗?

    不重复,因为map端是局部的操作reduce端是全局的操作。之前是每个map任务内进行排序,是有序的,但是多个map任务之间就是无序的了

    不过针对我们这个需求只有一个map任务一个分区,所以最终的结果还是老样子

    <hello,{1,1}>
    <say,{1}>
    <world,{1}>
    
  3. 框架调用Reducer类中的reduce方法,reduce方法的输入是<k2,{v2}>,输出是<k3,v3>。一个<k2,{v2}>调用一次reduce函数。程序员需要覆盖reduce函数,实现具体的业务逻辑。

    那我们在这里就需要在reduce函数中实现最终的聚合计算操作了,将相同k2的{v2}累加求和,然后再转化为k3,v3写出去,在这里最终会调用三次reduce函数

    <hello,2>
    <say,1>
    <world,1>
    
  4. 框架把reduce的输出结果保存到HDFS中。至此整个reduce阶段结束。

    hello 2
    say 1
    world 1
    

三、WordCount具体分析

3.1、编写map代码
package com.strivelearn.hadoop.hdfs.wordcount;
​
import java.io.IOException;
​
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
​
/**
 * key1 value1 key2 value2
 * key1 指的是文件每行的偏移量是数字型
 * value1 指的是文件的每个文字内容,是文本类型
 *
 * key2 是得到每个单词是文本类型
 * value2 是得到每个单词的汇总个数
 * @author xys
 * @version CustomerMapper.java, 2022年09月06日
 */
public class CustomerMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
    /**
     * Called once for each key/value pair in the input split. Most applications
     * should override this, but the default is the identity function.
     *
     * @param key
     * @param value
     * @param context
     */
    @Override
    protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, LongWritable>.Context context) throws IOException, InterruptedException {
        //key代表的每一行数据的行首偏移量,value代表的是每一行的内容
        //对获取到的每一行数据进行切割,把单词切割出来
        String[] words = value.toString().split(" ");
        //迭代切割出来的单词数据
        for (String word : words) {
            //把迭代出来的单词封装成<k2,v2>的形式
            Text k2 = new Text(word);
            LongWritable v2 = new LongWritable(1);
            //把<k2,v2>写出去
            context.write(k2, v2);
        }
    }
}
3.2、编写reduce代码
package com.strivelearn.hadoop.hdfs.wordcount;
​
import java.io.IOException;
​
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
​
/**
 * key2 是得到每个单词是文本类型
 * value2 是得到每个单词的汇总个数
 *
 * key3 是最终得到的单词文本
 * value3 是最终的累加的结果
 *
 * @author xys
 * @version CustomerReduce.java, 2022年09月06日
 */
public class CustomerReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
    /**
     * 针对<k2,{v2...}>的数据进行累加求和,并且最终把数据转成k3,v3写出去
     * This method is called once for each key. Most applications will define
     * their reduce class by overriding this method. The default implementation
     * is an identity function.
     *
     * @param key
     * @param values
     * @param context
     */
    @Override
    protected void reduce(Text key, Iterable<LongWritable> values, Reducer<Text, LongWritable, Text, LongWritable>.Context context) throws IOException, InterruptedException {
        //创建一个sum变量,保存v2的和
        long sum = 0;
        //对v2的数据进行累加求和
        for (LongWritable v2 : values) {
            sum += v2.get();
        }
​
        //组装k3 v3
        Text k3 = key;
        LongWritable v3 = new LongWritable(sum);
        //把结果写出去
        context.write(k3, v3);
    }
}
3.3、编写main代码
package com.strivelearn.hadoop.hdfs.wordcount;
​
import java.io.IOException;
​
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
​
/**
 * 需求:读取hdfs上的hello.txt文件,计算文件中每个单词出现的总次数
 * hello.txt的内容如下:
 * hello world
 * say hello
 *
 * 最终展示结果如下:
 * hello 2
 * world 1
 * say 1
 *
 * @author xys
 * @version WordCount.java, 2022年09月06日
 */
public class WordCountMain {
    public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
        //args的参数1为输入路径,参数2为输出路径
        if (args.length != 2) {
            //如果传递的参数不够,程序直接退出
            System.exit(100);
        }
​
        //指定Job需要配置的参数
        Configuration configuration = new Configuration();
        //创建一个Job
        Job job = Job.getInstance(configuration);
​
        //注意。这行必须设置,否则在集群中执行的时候是找不到WordCountMain这个类的
        job.setJarByClass(WordCountMain.class);
​
        //指定输入路径(可以是文件,也可以是目录)
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        //指定输出路径(只能指定一个不存在的目录)
        FileOutputFormat.setOutputPath(job, new Path(args[1]));
​
        //指定map相关的代码
        job.setMapperClass(CustomerMapper.class);
        //指定k2的类型
        job.setMapOutputKeyClass(Text.class);
        //指定v2的类型
        job.setMapOutputValueClass(LongWritable.class);
​
        //指定reduce相关的代码
        job.setReducerClass(CustomerReducer.class);
        //指定k3的类型
        job.setOutputKeyClass(Text.class);
        //指定v3的类型
        job.setOutputValueClass(LongWritable.class);
​
        //提交Job
        job.waitForCompletion(true);
    }
}
3.4、maven打包
<build>
    <plugins>
        <!-- compiler插件, 设定JDK版本 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.10.1</version>
            <configuration>
                <encoding>UTF-8</encoding>
                <source>1.8</source>
                <target>1.8</target>
                <showWarnings>true</showWarnings>
            </configuration>
        </plugin>
        <plugin>
            <!--打包插件-->
            <artifactId>maven-assembly-plugin</artifactId>
            <configuration>
                <descriptorRefs>
                    <!--可以打带依赖的插件-->
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
                <archive>
                    <manifest>
                        <!--指定入口类,也可以为空-->
                        <mainClass></mainClass>
                    </manifest>
                </archive>
            </configuration>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>single</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
3.5、上传到hadoop服务器

重点配置下:yarn-site.xml

<configuration>
    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
    </property>
    <property>
        <name>yarn.nodemanager.env-whitelist</name>
        <value>JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASS</value>
    </property>
    <property>
        <name>yarn.application.classpath</name>
        <value>/root/software/hadoop-3.3.4/etc/hadoop:/root/software/hadoop-3.3.4/share/hadoop/common/lib/*:/root/software/hadoop-3.3.4/share/hadoop/common/*:/root/software/hadoop-3.3.4/share/hadoop/hdfs:/root/software/hadoop-3.3.4/share/hadoop/hdfs/lib/*:/root/software/hadoop-3.3.4/share/hadoop/hdfs/*:/root/software/hadoop-3.3.4/share/hadoop/mapreduce/*:/root/software/hadoop-3.3.4/share/hadoop/yarn:/root/software/hadoop-3.3.4/share/hadoop/yarn/lib/*:/root/software/hadoop-3.3.4/share/hadoop/yarn/*</value>
    </property>
</configuration>

mapred-site.xml

<configuration>
    <property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
    </property>
    <property>
        <name>yarn.app.mapreduce.am.env</name>
        <value>HADOOP_MAPRED_HOME=/root/software/hadoop-3.3.4</value>
    </property>
    <property>
        <name>mapreduce.map.env</name>
        <value>HADOOP_MAPRED_HOME=/root/software/hadoop-3.3.4</value>
    </property>
    <property>
        <name>mapreduce.reduce.env</name>
        <value>HADOOP_MAPRED_HOME=/root/software/hadoop-3.3.4</value>
    </property>
</configuration>

运行命令:

hadoop jar hdfs-1.0-SNAPSHOT-jar-with-dependencies.jar com.strivelearn.hadoop.hdfs.wordcount.WordCountMain /hello.txt /out

还有一点就是程序的pom.xml文件里面。hadoop、log4j的依赖scope设置为provied

结果出现这个如下图所示

image-20220911155409161