一、MapReduce介绍
1.1、MapReduce计算思想
举个例子,要统计扑克牌的黑桃个数,最直接的办法是一张一张检查然后进行统计。利用MapReduce的计算方法,可以优化为:
- 把牌分配给在座所有玩家
- 每个玩家自己检查有多少张黑桃,然后把黑桃数目上报
- 把所有玩家的数字进行累加,得到最终的结果
总结:这个采取的就是分治的思想
1.2、分布式计算
- 对每个节点上的数据进行局部计算
- 对每个节点上面计算的局部结果进行最终全局汇总
1.3、MapReduce原理剖析
- MapReduce是一种分布式计算模型,是Google提出来的,主要用于搜索领域,解决海量数据的计算问题。
- MapReduce是分布式运行的,由两个阶段组成:Map和Reduce
在这map就是对数据进行局部汇总,reduce就是对局部数据进行最终汇总。
二、MapReduce
mapreduce主要分为两大步骤 map和reduce,map和reduce在代码层面对应的就是两个类,map对应的是mapper类,reduce对应的是reducer类,下面我们就来根据一个案例具体分析一下这两个步骤 假设我们有一个文件,文件里面有两行内容
hello world
say hello
我们想要统计文件中每个单词出现的总次数
2.1、Map阶段
-
框架会把输入文件(夹)划分为很多InputSplit,这里的inputsplit就是前面我们所说的split【对文件进行逻辑划分产生的】,默认情况下,每个HDFS的Block对应一个InputSplit。再通过RecordReader类,把每个InputSplit解析成一个一个的<k1,v1>。默认情况下,每一行数据,都会被解析成一个<k1,v1>这里的k1是指每一行的起始偏移量,v1代表的是那一行内容,所以,针对文件中的数据,经过map处理之后的结果是这样的。
<0,hello world> 第一行是从0开始 <12,say hello> 第二行是从12开始 -
框架调用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> -
框架对map函数输出的<k2,v2>进行分区。不同分区中的<k2,v2>由不同的reduce task处理,默认只有1个分区,所以所有的数据都在一个分区,最后只会产生一个reduce task。
咱们在这所说的单词计数,其实就是把每个单词出现的次数进行汇总即可,需要进行全局的汇总,不需要进行分区,所以一个redeuce任务就可以搞定
<hello,1> <world,1> <say,1> <hello,1> -
框架对每个分区中的数据,都会按照k2进行排序、分组。分组指的是相同k2的v2分成一个组
先按照k2排序(这里按照英文单词排序) <hello,1> <hello,1> <say,1> <world,1> 再进行分组 <hello,{1,1}> <say,{1}> <world,{1}> -
在map阶段,框架可以选择执行Combiner过程(可选)
Combiner可以翻译为规约,规约是什么意思呢? 在刚才的例子中,咱们最终是要在reduce端计算单词出现的总次数的,所以其实是可以在map端提前执行reduce的计算逻辑,先对在map端对单词出现的次 数进行局部求和操作,这样就可以减少map端到reduce端数据传输的大小,这就是规约的好处,当然了,并不是所有场景都可以使用规约,针对求平均值之类的操作就不能使用规约了,否则最终计算的结果就不准确了。
Combiner一个可选步骤,默认这个步骤是不执行的
-
框架会把map task输出的<k2,v2>写入到linux 的磁盘文件中。至此,整个map阶段执行结束
注意:
MapReduce程序是由map和reduce这两个阶段组成的,但是reduce阶段不是必须的,也就是说有的mapreduce任务只有map阶段,为什么会有这种任务呢?
是这样的,咱们前面说过,其实reduce主要是做最终聚合的,如果我们这个需求是不需要聚合操作直接对数据做过滤处理就行了,那也就意味着数据经过map阶段处理完就结束了,所以如果reduce阶段不存在的话,map的结果是可以直接保存到HDFS中的
注意,如果没有reduce阶段,其实map阶段只需要执行到第二步就可以,第二步执行完成以后,结果就可以直接输出到HDFS了。
2.2、Reduce阶段
-
框架对多个map任务的输出,按照不同的分区,通过网络copy到不同的reduce节点。这个过程称作shuffle
针对我们这个需求,只有一个分区,所以把数据拷贝到reduce端之后还是老样子
<hello,{1,1}> <say,{1}> <world,{1}> -
框架对reduce端接收的相同分区的<k2,v2>数据进行合并、排序、分组。reduce端接收到的是多个map的输出,对多个map任务中相同分区的数据进行合并、排序、分组 注意:
之前在map中已经做了排序 分组,这边也做这些操作 重复吗?
不重复,因为map端是局部的操作、 reduce端是全局的操作。之前是每个map任务内进行排序,是有序的,但是多个map任务之间就是无序的了
不过针对我们这个需求只有一个map任务一个分区,所以最终的结果还是老样子
<hello,{1,1}> <say,{1}> <world,{1}> -
框架调用Reducer类中的reduce方法,reduce方法的输入是<k2,{v2}>,输出是<k3,v3>。一个<k2,{v2}>调用一次reduce函数。程序员需要覆盖reduce函数,实现具体的业务逻辑。
那我们在这里就需要在reduce函数中实现最终的聚合计算操作了,将相同k2的{v2}累加求和,然后再转化为k3,v3写出去,在这里最终会调用三次reduce函数
<hello,2> <say,1> <world,1> -
框架把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
结果出现这个如下图所示