1. MapReduce介绍
1.1 MapReduce的基本思想
MapReduce思想在生活中处处可见。或多或少都曾接触过这种思想。MapReduce的思想核心是“分而治之”,适用于大量复杂的任务处理场景(大规模数据处理场景)。
MapReduce分为Map和Reduce两个阶段,Map负责“分”,即把复杂的任务分解为若干个“简单的任务”来并行处理,这些小任务可以并行计算,彼此间几乎没有依赖关系。Reduce负责“合”,即对map阶段的结果进行全局汇总。MapReduce运行在yarn集群
下图用介绍了类似MapReduce思想的一个实现过程
在实际的MapReduce执行过程中,可能有多个Reduce
1.2 MapReduce的设计
MapReduce是一个分布式运算程序的编程框架,核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在Hadoop集群上。
MapReduce设计并提供了统一的计算框架,为程序员隐藏了绝大多数系统层面的处理细节。为程序员提供一个抽象和高层的编程接口和框架。程序员仅需要关心其应用层的具体计算问题,仅需编写少量的处理应用本身计算问题的程序代码。如何具体完成这个并行计算任务所相关的诸多系统层细节被隐藏起来,交给计算框架去处理:
Map和Reduce为程序员提供了一个清晰的操作接口抽象描述。MapReduce中定义了如下的Map和Reduce两个抽象的编程接口,由用户去编程实现.Map和Reduce,MapReduce处理的数据类型是**<key,value>键值对**。
- Map:
(k1; v1) → [(k2; v2)]
- Reduce:
(k2; [v2]) → [(k3; v3)]
1.3 MapReduce在yarn集群上的执行流程
-
MRAppMaster 负责整个程序的过程调度及状态协调
-
MapTask负责map阶段的整个数据处理流程
-
ReduceTask负责reduce阶段的整个数据处理流程
2. MapReduce编程规范
MapReduce 的开发一共有八个步骤, 其中 Map 阶段分为 2 个步骤,Shuffle 阶段 4 个步骤,Reduce 阶段分为 2 个步骤
Map 阶段 2 个步骤
- 设置 InputFormat 类, 将数据切分为 Key-Value**(K1和V1)** 对, 输入到第二步
- 自定义 Map 逻辑, 将第一步的结果转换成另外的 Key-Value(K2和V2) 对, 输出结果
Shuffle 阶段 4 个步骤
- 对输出的 Key-Value 对进行分区
- 对不同分区的数据按照相同的 Key 排序
- 对数据进行分组, 相同 Key 的 Value 放入一个集合中
- (可选) 对分组过的数据初步规约, 降低数据的网络拷贝
Reduce 阶段 2 个步骤
- 对多个 Map 任务的结果进行排序以及合并, 编写 Reduce 函数实现自己的逻辑, 对输入的 Key-Value 进行处理, 转为新的 Key-Value(K3和V3)输出
- 设置 OutputFormat 处理并保存 Reduce 输出的 Key-Value 数据
3.MapReduce 分区
在 MapReduce 中, 通过我们指定分区, 会将同一个分区的数据发送到同一个 Reduce 当中进行处理
Reduce默认分区只有一个,但是在实际应用中可能有多个结果文件输出,因此往往需要重新设置分区,分区的简单流程如下图所示
4. MapReduce中的计数器
计数器是收集作业统计信息的有效手段之一,用于质量控制或应用级统计。计数器还可辅助诊断系统故障。如果需要将日志信息传输到 map 或 reduce 任务, 更好的方法通常是看能否用一个计数器值来记录某一特定事件的发生。对于大型分布式作业而言,使用计数器更为方便。除了因为获取计数器值比输出日志更方便,还有根据计数器值统计特定事件的发生次数要比分析一堆日志文件容易得多。
- hadoop内置计数器列表
MapReduce任务计数器 | org.apache.hadoop.mapreduce.TaskCounter |
---|---|
文件系统计数器 | org.apache.hadoop.mapreduce.FileSystemCounter |
FileInputFormat计数器 | org.apache.hadoop.mapreduce.lib.input.FileInputFormatCounter |
FileOutputFormat计数器 | org.apache.hadoop.mapreduce.lib.output.FileOutputFormatCounter |
作业计数器 | org.apache.hadoop.mapreduce.JobCounter |
每次mapreduce执行完成之后,我们都会看到一些日志记录出来,其中包括一些重要的日志记录
-
除内置计数器之外,我们还可以自定义计数器
-
通过context上下文对象,在map端使用计数器进行统计
public class PartitionMapper extends Mapper<LongWritable,Text,Text,NullWritable>{ //map方法将K1和V1转为K2和V2 @Override protected void map(LongWritable key, Text value, Context context) throws Exception{ Counter counter = context.getCounter("MR_COUNT", "MyRecordCounter"); counter.increment(1L); context.write(value,NullWritable.get()); } }
-
通过enum枚举类型来定义计数器,例如统计reduce端数据的输入的key有多少个
public class PartitionerReducer extends Reducer<Text,NullWritable,Text,NullWritable> { public static enum Counter{ MY_REDUCE_INPUT_RECORDS,MY_REDUCE_INPUT_BYTES } @Override protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException { context.getCounter(Counter.MY_REDUCE_INPUT_RECORDS).increment(1L); context.write(key, NullWritable.get()); } }
-
-
序列化 (Serialization) 是指把结构化对象转化为字节流
-
反序列化 (Deserialization) 是序列化的逆过程. 把字节流转为结构化对象. 当要在进程间传递对象或持久化对象的时候, 就需要序列化对象成字节流, 反之当要将接收到或从磁盘读取的字节流转换为对象, 就要进行反序列化
-
Java 的序列化 (Serializable) 是一个重量级序列化框架, 一个对象被序列化后, 会附带很多额外的信息 (各种校验信息, header, 继承体系等), 不便于在网络中高效传输. 所以, Hadoop 自己开发了一套序列化机制(Writable), 精简高效. 不用像 Java 对象类一样传输多层的父子关系, 需要哪个属性就传输哪个属性值, 大大的减少网络传输的开销
-
Writable 是 Hadoop 的序列化接口. 一个类要支持可序列化只需实现这个接口即可
-
另外 Writable 有一个子接口是 WritableComparable, WritableComparable 是既可实现序列化, 也可以对key进行比较, 我们这里可以通过自定义 Key 实现 WritableComparable 来实现我们的排序功能
5. MapReduce的排序
数据格式如下
a 1
a 9
b 3
a 7
b 8
b 10
a 5
要求:
- 第一列按照字典顺序进行排列
- 第一列相同的时候, 第二列按照升序进行排列
排序发生在Shuffle阶段,需要重新定义比较器
Step 1. 自定义类型和比较器
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class MySort implements WritableComparable<MySort>{
// 组合key,第一部分是我们第一列,第二部分是我们第二列
private String first;
private int second;
public MySort() {
}
public MySort(String first, int second) {
this.set(first, second);
}
// 重写toString方法方便查看
@Override
public String toString() {
return "PairWritable{" +
"first='" + first + '\'' +
", second=" + second +
'}';
}
// 重写序列化方法
@Override
public void write(DataOutput output) throws IOException {
output.writeUTF(first);
output.writeInt(second);
}
// 重新反序列化
@Override
public void readFields(DataInput input) throws IOException {
this.first = input.readUTF();
this.second = input.readInt();
}
// 重写比较器
public int compareTo(MySort o) {
// 每次比较都是调用该方法的对象与传递的参数进行比较,
// 说白了就是第一行与第二行比较完了之后的结果与第三行比较,
// 得出来的结果再去与第四行比较,依次类推
// 第一个字段进行对比
int comp = this.first.compareTo(o.first);
if (comp != 0) {
return comp;
} else { // 若第一个字段相等,则比较第二个字段
return Integer.valueOf(this.second).compareTo(
Integer.valueOf(o.getSecond()));
}
}
public int getSecond() {
return second;
}
public void setSecond(int second) {
this.second = second;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public void set(String first, int second) {
this.first = first;
this.second = second;
}
}
Step 2. Mapper
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/*
K1:字段偏移量
V1:Text(a 3, b 7....)
K2: Mysort(a 3)
V2:NullWritable
*/
public class MyMapper extends Mapper<LongWritable, Text, MySort, NullWritable> {
@Override
public void map(LongWritable key, Text value, Context context) throws InterruptedException, IOException {
// 将V1拆分,并封装到Mysort中得到K2
String lineValue = value.toString();
String[] strs = lineValue.split("\t");
//设置组合key和value ==> <(key,value),value>
MySort mySort = new MySort();
mySort.setFirst(strs[0]);
mySort.setSecond(Integer.parseInt(strs[1]));
//将K2和V2写入上下文
context.write(mySort, NullWritable.get());
}
}
Step 3. Reducer
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class MyReduce extends Reducer<MySort, NullWritable, MySort,NullWritable> {
private Text outPutKey = new Text();
@Override
public void reduce(MySort key, Iterable<NullWritable> values, Context context) throws InterruptedException, IOException {
context.write(key, NullWritable.get());
}
}
Step 4. Main 入口
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
public class JobMain extends Configured implements Tool {
@Override
public int run(String[] args) throws Exception {
//1:创建job对象
Job job = Job.getInstance(super.getConf(), "mapreduce_sort");
//打包到集群上面运行时候,必须要添加以下配置,指定程序的main函数
job.setJarByClass(JobMain.class);
//2:配置job任务(八个步骤)
//第一步:设置输入类和输入的路径
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job, new Path("hdfs://hadoop1:8020/sort"));
// TextInputFormat.addInputPath(job, new Path("file:///E:\\atest\\sort_input"));
//第二步: 设置Mapper类和数据类型
job.setMapperClass(MyMapper.class);
job.setMapOutputKeyClass(MySort.class);
job.setMapOutputValueClass(NullWritable.class);
//第三,四,五,六
//第七步:设置Reducer类和类型
job.setReducerClass(MyReduce.class);
job.setOutputKeyClass(MySort.class);
job.setOutputValueClass(NullWritable.class);
//第八步: 设置输出类和输出的路径
job.setOutputFormatClass(TextOutputFormat.class);
TextOutputFormat.setOutputPath(job, new Path("hdfs://hadoop1:8020/sort_out"));
// TextOutputFormat.setOutputPath(job, new Path("file:///E:\\sort_out"));
//3:等待任务结束
boolean bl = job.waitForCompletion(true);
return bl?0:1;
}
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
//启动job任务
int run = ToolRunner.run(configuration, new JobMain(), args);
System.exit(run);
}
}
public class JobMain extends Configured implements Tool {
@Override
public int run(String[] args) throws Exception {
//1:创建job对象
Job job = Job.getInstance(super.getConf(), "mapreduce_sort");
//2:配置job任务(八个步骤)
//第一步:设置输入类和输入的路径
job.setInputFormatClass(TextInputFormat.class);
///TextInputFormat.addInputPath(job, new Path("hdfs://node01:8020/input/sort_input"));
TextInputFormat.addInputPath(job, new Path("file:///D:\\input\\sort_input"));
//第二步: 设置Mapper类和数据类型
job.setMapperClass(SortMapper.class);
job.setMapOutputKeyClass(SortBean.class);
job.setMapOutputValueClass(NullWritable.class);
//第三,四,五,六
//第七步:设置Reducer类和类型
job.setReducerClass(SortReducer.class);
job.setOutputKeyClass(SortBean.class);
job.setOutputValueClass(NullWritable.class);
//第八步: 设置输出类和输出的路径
job.setOutputFormatClass(TextOutputFormat.class);
//TextOutputFormat.setOutputPath(job, new Path("hdfs://node01:8020/out/sort_out"));
TextOutputFormat.setOutputPath(job, new Path("file:///D:\\out\\sort_out"));
//3:等待任务结束
boolean bl = job.waitForCompletion(true);
return bl?0:1;
}
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
//启动job任务
int run = ToolRunner.run(configuration, new JobMain(), args);
System.exit(run);
}
}
6. MapReduce的规约(Combiner)
Combiner 的作用就是对 map 端的输出先做一次合并,以减少在 map 和 reduce 节点之间的数据传输量,以提高网络IO 性能,是 MapReduce 的一种优化手段之一
- combiner 是 MR 程序中 Mapper 和 Reducer 之外的一种组件
- combiner 组件的父类就是 Reducer
- combiner 和 reducer 的区别在于运行的位置
- Combiner 是在每一个 maptask 所在的节点运行
- Reducer 是接收全局所有 Mapper 的输出结果
- combiner 的意义就是对每一个 maptask 的输出进行局部汇总,以减小网络传输量
来源:CSDN
作者:冥更
链接:https://blog.csdn.net/qq_24852439/article/details/104259895