Hadoop之MapReduce简介

1,026 阅读27分钟

MapReduce介绍

在这里我们先举个例子来介绍一下MapReduce
计算扑克牌中的黑桃个数
就是我们平时打牌时用的扑克牌,现在呢,有一摞牌,我想知道这摞牌中有多少张黑桃
最直接的方式是一张一张检查并且统计出有多少张是黑桃,但是这种方式的效率比较低,如果说这一摞牌 只有几十张也就无所谓了,如果这一摞拍有上千张呢?你一张一张去检查还不疯了?
这个时候我们可以使用MapReduce的计算方法
第一步:把这摞牌分配给在座的所有玩家
第二步:让每个玩家查一下自己手中的牌有多少张是黑桃,然后把这个数目汇报给你
第三步:你把所有玩家告诉你的数字加起来,得到最终的结果
之前是一张一张的串行计算,现在使用mapreduce是把数据分配给多个人,并行计算,每一个人获得一个局部聚合的临时结果,最终再统一汇总一下。
这样就可以快速得到答案了,这其实就是MapReduce的计算思想。
下面我们再通过具体的案例分析MapReduce的计算思想

分布式计算介绍

再举一个例子,就拿我们平时使用比较多的JDBC代码执行的流程来说。
我们自己写的JDBC代码是在一台机器上运行,mysql数据库是在另一台机器上运行。
正常情况下,我们通过jdbc代码去mysql中获取一条数据,速度还是很快的,
但是有一个极端情况,如果我们要获取的数据量很大,达到了几个G,甚至于几十G。
这个时候我们使用jdbc代码去拉取数据的时候,就会变得非常慢,
这个慢主要是由于两个方面造成的,
一个是磁盘io(会进行磁盘读写操作),
一个是网络io(网络传输)。
这两个里面其实最耗时的还是网络io,我们平时在两台电脑之间传输一个几十G的文件也需要很长时间
的,但是如果是使用U盘拷贝就很快了,所以可以看出来主要耗时的地方是在网络IO上面。
这种计算方式我们称之为移动数据 ,就是把mysql数据库中的数据移动到计算程序所在的机器上面

如果我们考虑把计算程序移动到mysql上面去执行,是不是就可以节省网络io了,是的!
这种方式称之为移动计算,就是把计算程序移动到数据所在的节点上面

移动数据是传统的计算方式,现在的一种新思路是移动计算。
如果我们数据量很大的话,我们的数据肯定是由很多个节点存储的,这个时候我们就可以把我们的程序代 码拷贝到对应的节点上面去执行,程序代码都是很小的,一般也就几十KB或者几百KB,加上外部依赖 包,最大也就几兆 ,甚至几十兆,但是我们需要计算的数据动辄都是几十G、几百G,他们两个之间的差 距不是一星半点啊
这样我们的代码就可以在每个数据节点上面执行了,但是这个代码只能计算当前节点上的数据的,如果我 们想要统计数据的总行数,这里每个数据节点上的代码只能计算当前节点上数据的行数,所以还的有一个 汇总程序,这样每个数据节点上面计算的临时结果就可以通过汇总程序得到最终的结果了。
此时汇总程序需要传递的数据量就很小了,只需要接收一个数字即可。
这个计算过程就是分布式计算,这个步骤分为两步

  • 第一步:对每个节点上面的数据进行局部计算
  • 第二步:对每个节点上面计算的局部结果进行最终全局汇总

MapReduce原理剖析

MapReduce是一种分布式计算模型,是Google提出来的,主要用于搜索领域,解决海量数据的计算问 题.
MapReduce是分布式运行的,由两个阶段组成:Map和Reduce,
Map阶段是一个独立的程序,在很多个节点同时运行,每个节点处理一部分数据。
Reduce阶段也是一个独立的程序,可以在一个或者多个节点同时运行,每个节点处理一部分数据【在这 我们先把reduce理解为一个单独的聚合程序即可】。
在这map就是对数据进行局部汇总,reduce就是对局部数据进行最终汇总。
结合到我们前面分析的统计黑桃的例子中,这里的map阶段就是指每个人统计自己手里的黑桃的个数, reduce就是对每个人统计的黑桃个数进行最终汇总
不知道大家有没有疑问,这两个阶段的名字为什么称为map和reduce?
这个就不用纠结了,作者总归要给他起一个名字的,就像我们平时看到的一个笑话,一个小孩问他爸爸, 井盖为什么是圆的? 他爸爸说,如果井盖是方的,你又要问我它为什么要是方的,它总归是要有一个形 状把。
在这我们再举一个例子,看这个图

这是一个Hadoop集群,一共5个节点
一个主节点,四个从节点
这里面我们只列出来了HDFS相关的进程信息
假设我们有一个512M的文件,这个文件会产生4个block块,假设这4个block块正好分别存储到了集群的 4个节点上,我们的计算程序会被分发到每一个数据所在的节点,然后开始执行计算,在map阶段,针对 每一个block块对应的数据都会产生一个map任务(这个map任务其实就是执行这个计算程序的),在这里 也就意味着会产生4个map任务并行执行,4个map阶段都执行完毕以后,会执行reduce阶段,在reduce 阶段中会对这4个map任务的输出数据进行汇总统计,得到最终的结果。

MapReduce原理剖析

下面看一个官方的mapreduce原理图
左下角是一个文件,文件最下面是几个block块,说明这个文件被切分成了这几个block块,文件上面是一 些split,注意,咱们前面说的每个block产生一个map任务,其实这是不严谨的,其实严谨一点来说的话 应该是一个split产生一个map任务。
那这里的block和split之间有什么关系吗? 我们来分析一下
block块是文件的物理切分,在磁盘上是真实存在的。是对文件的真正切分
而split是逻辑划分,不是对文件真正的切分,默认情况下我们可以认为一个split的大小和一个block的大 小是一样的,所以实际上是一个split会产生一个map task
这里面的map Task就是咱们前面说的map任务,看后面有一个reduce Task,reduce会把结果数据输出 到hdfs上,有几个reduce任务就会产生几个文件,这里有三个reduce任务,就产生了3个文件,咱们前 面分析的案例中只有一个reduce任务做全局汇总
注意看map的输入 输出 reduce的输入 输出
map的输入是k1,v1 输出是k2,v2
reduce的输入是k2,v2 输出是k3,v3 都是键值对的形式。
在这注意一下,为什么在这是1,2,3呢? 这个主要是为了区分数据,方便理解,没有其它含义,这是我们 人为定义的。

MapReduce之Map阶段

mapreduce主要分为两大步骤 map和reduce,map和reduce在代码层面对应的就是两个类,map对应 的是mapper类,reduce对应的是reducer类,下面我们就来根据一个案例具体分析一下这两个步骤
假设我们有一个文件,文件里面有两行内容
第一行是hello you
第二行是hello me
我们想统计文件中每个单词出现的总次数
首先是map阶段
第一步:框架会把输入文件(夹)划分为很多InputSplit,这里的inputsplit就是前面我们所说的split【对文件进行逻辑划分产生的】,默认情况下,每个HDFS的Block对应一个InputSplit。再通过RecordReader 类,把每个InputSplit解析成一个一个的<k1,v1>。默认情况下,每一行数据,都会被解析成一个<k1,v1> 这里的k1是指每一行的起始偏移量,v1代表的是那一行内容,
所以,针对文件中的数据,经过map处理之后的结果是这样的
<0,hello you>
<10,hello me>
注意:map第一次执行会产生<0,hello you>,第二次执行会产生<10,hello me>,并不是执行一次就获取到这两行结果了,因为每次只会读取一行数据,我在这里只是把这两行执行的最终结果都列出来了
第二步:框架调用Mapper类中的map(…)函数,map函数的输入是<k1,v1>,输出是<k2,v2>。一个因为我们需要统计文件中每个单词出现的总次数,所以需要先把每一行内容中的单词切开,然后记录出现次数为1,这个逻辑就需要我们在map函数中实现了
那针对<0,hello you>执行这个逻辑之后的结果就是
<hello,1>
<you,1>
针对<10,hello me>执行这个逻辑之后的结果是
<hello,1>
<me,1>
第三步:框架对map函数输出的<k2,v2>进行分区。不同分区中的<k2,v2>由不同的reduce task处理,默 认只有1个分区,所以所有的数据都在一个分区,最后只会产生一个reduce task。
经过这个步骤之后,数据没什么变化,如果有多个分区的话,需要把这些数据根据分区规则分开,在这里 默认只有1个分区。
<hello,1>
<you,1>
<hello,1>
<me,1>
咱们在这所说的单词计数,其实就是把每个单词出现的次数进行汇总即可,需要进行全局的汇总,不需要 进行分区,所以一个redeuce任务就可以搞定,
如果你的业务逻辑比较复杂,需要进行分区,那么就会产生多个reduce任务了,
那么这个时候,map任务输出的数据到底给哪个reduce使用?这个就需要划分一下,要不然就乱套了。
假设有两个reduce,map的输出到底给哪个reduce,如何分配,这是一个问题。
这个问题,由分区来完成。
map输出的那些数据到底给哪个reduce使用,这个就是分区干的事了。
第四步:框架对每个分区中的数据,都会按照k2进行排序、分组。分组指的是相同k2的v2分成一个组。 先按照k2排序
<hello,1>
<hello,1>
<me,1>
<you,1>
然后按照k2进行分组,把相同k2的v2分成一个组
<hello,{1,1}>
<me,{1}>
<you,{1}>
第五步:在map阶段,框架可以选择执行Combiner过程
Combiner可以翻译为规约,规约是什么意思呢? 在刚才的例子中,咱们最终是要在reduce端计算单词 出现的总次数的,所以其实是可以在map端提前执行reduce的计算逻辑,先对在map端对单词出现的次 数进行局部求和操作,这样就可以减少map端到reduce端数据传输的大小,这就是规约的好处,当然 了,并不是所有场景都可以使用规约,针对求平均值之类的操作就不能使用规约了,否则最终计算的结果 就不准确了。
Combiner一个可选步骤,默认这个步骤是不执行的。
第六步:框架会把map task输出的<k2,v2>写入到linux 的磁盘文件中
<hello,{1,1}>
<me,{1}>
<you,{1}>
至此,整个map阶段执行结束
最后注意一点:
MapReduce程序是由map和reduce这两个阶段组成的,但是reduce阶段不是必须的,也就是说有的 mapreduce任务只有map阶段,为什么会有这种任务呢?
是这样的,咱们前面说过,其实reduce主要是做最终聚合的,如果我们这个需求是不需要聚合操作,直接对数据做过滤处理就行了,那也就意味着数据经过map阶段处理完就结束了,所以如果reduce阶段不存在的话,map的结果是可以直接保存到HDFS中的
针对我们这个单词计数的需求是存在reduce阶段的,所以我们继续往下面分析。

MapReduce之Reduce阶段

第一步:框架对多个map任务的输出,按照不同的分区,通过网络copy到不同的reduce节点。这个过程称作shuffle
针对我们这个需求,只有一个分区,所以把数据拷贝到reduce端之后还是老样子
<hello,{1,1}>
<me,{1}>
<you,{1}>
第二步:框架对reduce端接收的相同分区的<k2,v2>数据进行合并、排序、分组。
reduce端接收到的是多个map的输出,对多个map任务中相同分区的数据进行合并 排序 分组 注意,之前在map中已经做了排序 分组,这边也做这些操作 重复吗?
不重复,因为map端是局部的操作 reduce端是全局的操作
之前是每个map任务内进行排序,是有序的,但是多个map任务之间就是无序的了。
不过针对我们这个需求只有一个map任务一个分区,所以最终的结果还是老样子
<hello,{1,1}>
<me,{1}>
<you,{1}>
第三步:框架调用Reducer类中的reduce方法,reduce方法的输入是<k2,{v2}>,输出是<k3,v3>。一个<k2,{v2}>调用一次reduce函数。程序员需要覆盖reduce函数,实现具体的业务逻辑。
那我们在这里就需要在reduce函数中实现最终的聚合计算操作了,将相同k2的{v2}累加求和,然后再转化为k3,v3写出去,在这里最终会调用三次reduce函数
<hello,2>
<me,1>
<you,1>
第四步:框架把reduce的输出结果保存到HDFS中。
hello 2
me 1
you 1
至此,整个reduce阶段结束。
接下来看这个图再重新梳理一下单词计数的执行流程

上面的是单个文件的执行流程,有一些现象看起来还是不明显 下面我们来看一个两个文件的执行流程

实战

前面我们通过理论层面详细分析了单词计数的执行流程,下面我们就来实际上手操作一下。
大致流程如下:
第一步:开发Map阶段代码
第二步:开发Reduce阶段代码
第三步:组装Job
在idea中创建WordCountJob类
添加注释,梳理一下需求:
需求:读取hdfs上的hello.txt文件,计算文件中每个单词出现的总次数
hello.txt文件内容如下:
hello you
hello me
最终需要的结果形式如下:
hello 2
me 1
you 1

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.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

public class WordCountJob {
    /**
     * 创建自定义mapper类
     */
    public static class MyMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
        /**
         * 需要实现map函数
         * 这个map函数就是可以接收k1,v1, 产生k2,v2
         *
         * @param k1
         * @param v1
         * @param context
         * @throws IOException
         * @throws InterruptedException
         */
        @Override
        protected void map(LongWritable k1, Text v1, Context context) throws IOException, InterruptedException {
            // k1代表的是每一行的行首偏移量,v1代表的是每一行内容
            // 对获取到的每一行数据进行切割,把单词切割出来
            String[] words = v1.toString().split(" ");
            for (String word : words) {
                // 迭代切割出来的单词数据
                Text k2 = new Text(word);
                LongWritable v2 = new LongWritable(1L);
                // 把<k2,v2>写出去 context.write(k2,v2);
                context.write(k2, v2);
            }
        }
    }

    /**
     * 创建自定义reducer类
     */
    public static class MyReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
        /**
         * 针对<k2,{v2……}>的数据进行累加求和,并且最终把数据转化为k3,v3写出去
         *
         * @param k2
         * @param v2s
         * @param context
         * @throws IOException
         * @throws InterruptedException
         */
        @Override
        protected void reduce(Text k2, Iterable<LongWritable> v2s, Context context) throws IOException, InterruptedException {
            long sum = 0L;
            for (LongWritable v2 : v2s) {
                sum += v2.get();
            }
            Text k3 = k2;
            LongWritable v3 = new LongWritable(sum);
            context.write(k3, v3);
        }
    }

    public static void main(String[] args) {
        try {
            if (args.length != 2) {
                // 如果传递的参数不够,程序直接退出
                System.exit(100);
            }
            // job需要的配置参数
            Configuration conf = new Configuration();
            // 创建一个job
            Job job = Job.getInstance(conf);
            // 注意:这一行必须设置,否则在集群中执行的是找不到WordCountJob这个类
            job.setJarByClass(WordCountJob.class);
            // 指定输入路径(可以是文件,也可以是目录)
            FileInputFormat.setInputPaths(job, new Path(args[0]));
            // 指定输出路径(只能指定一个不存在的目录)
            FileOutputFormat.setOutputPath(job, new Path(args[1]));
            // 指定map相关的代码
            job.setMapperClass(MyMapper.class);
            // 指定k2的类型
            job.setMapOutputKeyClass(Text.class);
            // 指定v2的类型
            job.setMapOutputValueClass(LongWritable.class);
            // 指定reduce相关的代码
            job.setReducerClass(MyReducer.class);
            // 指定k3的类型
            job.setOutputKeyClass(Text.class);
            // 指定v3的类型
            job.setOutputValueClass(LongWritable.class);
            job.waitForCompletion(true);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

现在代码开发完毕了,现在我们是把自定义的mapper类和reducer类都放到了这个WordCountJob类中,主要是为了在学习阶段看起来清晰一些,所有的代码都在一个类中,好找,其实我们完全可以把自定义的mapper类和reducer类单独提出去,定义为单独的类,是没有什么区别的。
ok,那代码开发好了以后想要执行,我们需要打jar包上传到集群上去执行,这个时候需要在pom文件中添加maven的编译打包插件。

<build>
        <!-- compiler插件, 设定JDK版本 -->
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</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>

注意了,这些添加完以后还有一个地方需要修改,需要在pom中的hadoop-client和log4j依赖中增加scope属性,值为provided,表示只在编译的时候使用这个依赖,在执行以及打包的时候都不使用,因为hadoop-client和log4j依赖在集群中都是有的,所以在打jar包的时候就不需要打进去了,如果我们使用到了集群中没有的第三方依赖包就不需要增加这个provided属性了,不增加provided就可以把对应的第三方依赖打进jar包里面了。

<dependencies>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
            <version>3.2.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.10</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.10</version>
            <scope>provided</scope>
        </dependency>
</dependencies>

添加好了以后就可以打包了,建议在windows的cmd命令行下cd到项目根目录,然后执行mvn编译打包命令,看到最后输出的BUILD SUCCESS就说明执行成功了

命令执行成功之后,就可以到target目录下获取对应的jar包了,需要使用jar-with-dependencies结尾的那个jar包。
D:\IdeaProjects\db_hadoop\target\db_hadoop-1.0-SNAPSHOT-jar-with-dependencies.jar
把这个jar包上传到集群的任意一台机器上面或者是hadoop客户端机器上都可以,只要这台机器可以和集群进行交互即可。
注意,这个jar包不能使用java -jar的方式执行,需要使用集群特有的执行方式
我把这个jar包上传到了bigdata01机器的/usr/local/hadoop-3.2.0目录下了,
在向集群中正式提交任务jar包之前需要先把测试数据准备好
在本地创建一个hello.txt文件,内容是

[root@bigdata01 hadoop-3.2.0]# vi hello.txt 
hello you
hello me

单词中间用空格隔开,因为我们在MapReduce代码中是使用空格进行切割单词的。
然后把hello.txt上传到hdfs的test目录下

[root@bigdata01 hadoop-3.2.0]# hdfs dfs -mkdir /test 
[root@bigdata01 hadoop-3.2.0]# hdfs dfs -put hello.txt /test 
[root@bigdata01 hadoop-3.2.0]# hdfs dfs -ls /test 
Found 1 items 
-rw-r--r-- 2 root supergroup 19 2020-04-22 11:16 /test/hello.txt

接下来就可以向集群提交MapReduce任务了
具体的命令是这样的

hadoop jar db_hadoop-1.0-SNAPSHOT-jar-with-dependencies.jar com.cjt.mr.WordCountJob /test/hello.txt /out
or
yarn jar db_hadoop-1.0-SNAPSHOT-jar-with-dependencies.jar com.cjt.mr.WordCountJob /test/hello.txt /out
  • hadoop:表示使用hadoop脚本提交任务,其实在这里使用yarn脚本也是可以的,从hadoop2开始支持使用yarn,不过也兼容hadoop1,也继续支持使用hadoop脚本,所以在这里使用哪个都可以,具体就看你个人的喜好了,我是习惯于使用hadoop脚本
  • jar:表示执行jar包
  • db_hadoop-1.0-SNAPSHOT-jar-with-dependencies.jar:指定具体的jar包路径信息
  • com.cjt.mr.WordCountJob:指定要执行的mapreduce代码的全路径
  • /test/hello.txt:指定mapreduce接收到的第一个参数,代表的是输入路径,这里的输入路径可以直接指定hello.txt的路径,也可以直接指定它的父目录,因为它的父目录里面也没有其它无关的文件,如果指定目录的话就意味着hdfs会读取这个目录下所有的文件,所以后期如果我们需要处理一批文件,那就可以把他们放到同一个目录里面,直接指定目录即可。
  • /out:指定mapreduce接收到的第二个参数,代表的是输出目录,这里的输出目录必须是不存在的,MapReduce程序在执行之前会检测这个输出目录,如果存在会报错,因为它每次执行任务都需要一个新的输出目录来存储结果数据

任务提交到集群上面之后,可以在shell窗口中看到如下日志信息,最终map执行到100%,reduce执行到100%,说明任务执行成功了。

2020-04-22 15:12:59,887 INFO mapreduce.Job: map 0% reduce 0% 
2020-04-22 15:13:08,050 INFO mapreduce.Job: map 100% reduce 0% 
2020-04-22 15:13:16,261 INFO mapreduce.Job: map 100% reduce 100%

当然了,也可以到web界面中查看任务执行情况。
访问 http://bigdata01:8088

那我们来查看一下任务输出的结果,

[root@bigdata01 hadoop-3.2.0]# hdfs dfs -ls /out 
Found 2 items 
-rw-r--r-- 2 root supergroup 0 2020-04-22 15:13 /out/_SUCCESS 
-rw-r--r-- 2 root supergroup 19 2020-04-22 15:13 /out/part-r-00000

在out输出目录中,_SUCCESS是一个标记文件,有这个文件表示这个任务执行成功了。 part-r-00000是具体的数据文件,如果有多个reduce任务会产生多个这种文件,多个文件的话会按照从0开始编号,00001,00002等等…

还要一点需要注意的,part后面的r表示这个结果文件是reduce步骤产生的,如果一个mapreduce只有map阶段没有reduce阶段,那么产生的结果文件是part-m-00000这样的。

MapReduce之任务日志查看

如果想要查看mapreduce任务执行过程产生的日志信息怎么办呢?
是不是在提交任务的时候直接在这个控制台上就能看到了?先不要着急,我们先在代码中增加一些日志信息,在实际工作中做调试的时候这个也是很有必要的
在自定义mapper类的map函数中增加一个输出,将k1,v1的值打印出来

  @Override
        protected void map(LongWritable k1, Text v1, Context context) throws IOException, InterruptedException {
            // k1代表的是每一行的行首偏移量,v1代表的是每一行内容
            // 对获取到的每一行数据进行切割,把单词切割出来
            String[] words = v1.toString().split(" ");
            System.out.println("<k1,v1>=<"+k1.get()+","+v1.toString()+">");
            for (String word : words) {
                // 迭代切割出来的单词数据
                Text k2 = new Text(word);
                LongWritable v2 = new LongWritable(1L);
                System.out.println("k2:"+word+"...v2:1");
                // 把<k2,v2>写出去;
                context.write(k2, v2);
            }
        }

在自定义reducer类中的reduce方法中增加一个输出,将k2,v2和k3,v3的值打印出来

  */
        @Override
        protected void reduce(Text k2, Iterable<LongWritable> v2s, Context context) throws IOException, InterruptedException {
            long sum = 0L;
            for (LongWritable v2 : v2s) {
                System.out.println("<k2,v2>=<"+k2.toString()+","+v2.get()+">");
                sum += v2.get();
            }
            Text k3 = k2;
            LongWritable v3 = new LongWritable(sum);
            System.out.println("<k3,v3>=<"+k3.toString()+","+v3.get()+">");
            context.write(k3, v3);
        }

重新在windows机器上打jar包,并把新的jar包上传到bigdata01机器的/usr/local/hadoop-3.2.0目录中 重新向集群提交任务,注意,针对输出目录,要么换一个新的不存在的目录,要么把之前的out目录删掉

hadoop jar db_hadoop-1.0-SNAPSHOT-jar-with-dependencies.jar com.cjt.mr.WordCountJob /test/hello.txt /out

等待任务执行结束,我们发现在控制台上是看不到任务中的日志信息的,为什么呢?因为我们在这相当于是通过一个客户端把任务提交到集群里面去执行了,所以日志是存在在集群里面的。想要查看需要需要到一个特殊的地方查看这些日志信息
先进入到yarn的web界面,访问8088端口,点击对应任务的history链接
http://bigdata01:8088/


注意了,在这里我们发现这个链接是打不来的,
这里有两个原因,第一个原因是没有windows的hosts文件中没有配置bigdata02和bigdata03这两个主机名和ip的映射关系,先去把这两个主机名配置到hosts文件里面,之前的bigdata01已经配置进去了。

192.168.182.100 bigdata01 
192.168.182.101 bigdata02 
192.168.182.102 bigdata03

第二个原因就是这里必须要启动historyserver进程才可以,并且还要开启日志聚合功能,才能在web界面上直接查看任务对应的日志信息,因为默认情况下任务的日志是散落在nodemanager节点上的,想要查看需要找到对应的nodemanager节点上去查看,这样就很不方便,通过日志聚合功能我们可以把之前本来散落在nodemanager节点上的日志统一收集到hdfs上的指定目录中,这样就可以在yarn的web界面中直接查看了

那我们就来开启日志聚合功能。开启日志聚合功能需要修改yarn-site.xml的配置,增加 yarn.log-aggregation-enable和yarn.log.server.url这两个参数

<property> 
    <name>yarn.log-aggregation-enable</name> 
    <value>true</value> 
</property> 
<property> 
    <name>yarn.log.server.url</name> 
    <value>http://bigdata01:19888/jobhistory/logs/</value> 
</property>

注意:修改这个配置想要生效需要重启集群。

[root@bigdata01 hadoop-3.2.0]# sbin/stop-all.sh 
[root@bigdata01 hadoop-3.2.0]# cd etc/hadoop/ 
[root@bigdata01 hadoop]# vi yarn-site.xml

启动historyserver进程,需要在集群的所有节点上都启动这个进程

[root@bigdata01 hadoop-3.2.0]# bin/mapred --daemon start historyserver 
[root@bigdata01 hadoop-3.2.0]# jps 
4232 SecondaryNameNode 
5192 JobHistoryServer 
4473 ResourceManager 
3966 NameNode 
5231 Jps 
[root@bigdata02 hadoop-3.2.0]# bin/mapred --daemon start historyserver 
[root@bigdata02 hadoop-3.2.0]# jps 
2904 Jps 
2523 NodeManager 
2844 JobHistoryServer 
2415 DataNode 
[root@bigdata03 hadoop-3.2.0]# bin/mapred --daemon start historyserver 
[root@bigdata03 hadoop-3.2.0]# jps 
3138 JobHistoryServer
2678 NodeManager 
2570 DataNode 
3198 Jps

重新再提交mapreduce任务
此时再进入yarn的8088界面,点击任务对应的history链接就可以打开了。
此时,点击对应map和reduce后面的链接就可以点进去查看日志信息了,点击map后面的数字1,可以进入如下界面

点击这个界面中的logs文字链接,可以查看详细的日志信息。
最终可以在界面中看到很多日志信息,我们刚才使用sout输出的日志信息需要到Log Type: stdout这里来查看,在这里可以看到,k1和v1的值

Log Type: stdout Log 
Upload Time: Fri Apr 24 15:33:58 +0800 2020 
Log Length: 103 
<k1,v1>=<0,hello you>
<k1,v1>=<10,hello me>

想要查看reduce输出的日志信息需要到reduce里面查看,操作流程是一样的,可以看到k2,v2和k3,v3的值
咱们刚才的输出是使用syout输出的,这个其实是不正规的,标准的日志写法是需要使用logger进行输出的

public static class MyMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
        Logger logger = LoggerFactory.getLogger(MyMapper.class);
        /**
         * 需要实现map函数
         * 这个map函数就是可以接收k1,v1, 产生k2,v2
         *
         * @param k1
         * @param v1
         * @param context
         * @throws IOException
         * @throws InterruptedException
         */
        @Override
        protected void map(LongWritable k1, Text v1, Context context) throws IOException, InterruptedException {
            // k1代表的是每一行的行首偏移量,v1代表的是每一行内容
            // 对获取到的每一行数据进行切割,把单词切割出来
            String[] words = v1.toString().split(" ");
            logger.info("<k1,v1>=<"+k1.get()+","+v1.toString()+">");
           // System.out.println("<k1,v1>=<"+k1.get()+","+v1.toString()+">");
            for (String word : words) {
                // 迭代切割出来的单词数据
                Text k2 = new Text(word);
                LongWritable v2 = new LongWritable(1L);
                logger.info("k2:"+word+"...v2:1");
               // System.out.println("k2:"+word+"...v2:1");
                // 把<k2,v2>写出去 context.write(k2,v2);
                context.write(k2, v2);
            }
        }
    }

    /**
     * 创建自定义reducer类
     */
    public static class MyReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
        Logger logger = LoggerFactory.getLogger(MyReducer.class);
        /**
         * 针对<k2,{v2……}>的数据进行累加求和,并且最终把数据转化为k3,v3写出去
         *
         * @param k2
         * @param v2s
         * @param context
         * @throws IOException
         * @throws InterruptedException
         */
        @Override
        protected void reduce(Text k2, Iterable<LongWritable> v2s, Context context) throws IOException, InterruptedException {
            long sum = 0L;
            for (LongWritable v2 : v2s) {
                logger.info("<k2,v2>=<"+k2.toString()+","+v2.get()+">");
                // System.out.println("<k2,v2>=<"+k2.toString()+","+v2.get()+">");
                sum += v2.get();
            }
            Text k3 = k2;
            LongWritable v3 = new LongWritable(sum);
            logger.info("<k3,v3>=<"+k3.toString()+","+v3.get()+">");
           // System.out.println("<k3,v3>=<"+k3.toString()+","+v3.get()+">");
            context.write(k3, v3);
        }
    }

重新编译打包上传,重新提交最新的jar包,这个时候再查看日志就需要到Log Type: syslog中查看日志了。

这是工作中比较常用的查看日志的方式,但是还有一种使用命令查看的方式,这种方式面试的时候一般喜欢问

[root@bigdata01 hadoop-3.2.0]# yarn logs -applicationId application_158771356

注意:后面指定的是任务id,任务id可以到yarn的web界面上查看。
执行这个命令可以看到很多的日志信息,我们通过grep筛选一下日志

[root@bigdata01 hadoop-3.2.0]# yarn logs -applicationId application_158771356 | grep k1,v1
<k1,v1>=<0,hello you> 
<k1,v1>=<10,hello me>

停止Hadoop集群中的任务

如果一个mapreduce任务处理的数据量比较大的话,这个任务会执行很长时间,可能几十分钟或者几个小时都有可能,假设一个场景,任务执行了一半了我们发现我们的代码写的有问题,需要修改代码重新提交执行,这个时候之前的任务就没有必要再执行了,没有任何意义了,最终的结果肯定是错误的,所以我们就想把它停掉,要不然会额外浪费集群的资源,如何停止呢?
我在提交任务的窗口中按ctrl+c是不是就可以停止?
注意了,不是这样的,我们前面说过,这个任务是提交到集群执行的,你在提交任务的窗口中执行ctrl+c对已经提交到集群中的任务是没有任何影响的。
我们可以验证一下,执行ctrl+c之后你再到yarn的8088界面查看,会发现任务依然存在。
所以需要使用hadoop集群的命令去停止正在运行的任务
使用yarn application -kill命令,后面指定任务id即可

[root@bigdata01 hadoop-3.2.0]# yarn application -kill application_15877135678

MapReduce程序扩展

咱们前面说过MapReduce任务是由map阶段和reduce阶段组成的
但是我们也说过,reduce阶段不是必须的,那也就意味着MapReduce程序可以只包含map阶段。
什么场景下会只需要map阶段呢?
当数据只需要进行普通的过滤、解析等操作,不需要进行聚合,这个时候就不需要使用reduce阶段了,
在代码层面该如何设置呢?
很简单,在组装Job的时候设置reduce的task数目为0就可以了。并且Reduce代码也不需要写了。

public static void main(String[] args) { 
	try { if(args.length!=2){ 
    // 如果传递的参数不够,程序直接退出 
   		 System.exit(100); 
    	}
    // job需要的配置参数 
    Configuration conf = new Configuration(); 
    // 创建一个job 
    Job job = Job.getInstance(conf); 
    // 注意:这一行必须设置,否则在集群中执行的是找不到WordCountJob这个类 		
    job.setJarByClass(WordCountJobNoReduce.class); 
    // 指定输入路径(可以是文件,也可以是目录) 
    FileInputFormat.setInputPaths(job,new Path(args[0])); 
    // 指定输出路径(只能指定一个不存在的目录) 
    FileOutputFormat.setOutputPath(job,new Path(args[1])); 
    // 指定map相关的代码 
    job.setMapperClass(MyMapper.class);
    // 指定k2的类型 
    job.setMapOutputKeyClass(Text.class); 
    // 指定v2的类型 
    job.setMapOutputValueClass(LongWritable.class); 
    //禁用reduce阶段 
    job.setNumReduceTasks(0); 
    // 提交job job.waitForCompletion(true); 
    }catch (Exception e){ 	
    	e.printStackTrace(); 
    } 
  }
}

重新编译,打包,上传到bigdata01机器上
然后将最新的任务提交到集群上面,注意修改入口类全类名
这里发现map执行到100%以后任务就执行成功了,reduce还是0%,因为就没有reduce阶段了。
查看输出结果,注意,这里的文件名就是part-m-00000了

[root@bigdata01 hadoop-3.2.0]# hdfs dfs -cat /out5/part-m-00000 
hello 1 
you 1 
hello 1
me 1