MapReduce 是一个分布式运算程序的编程框架,是用户开发 “ 基于 Hadoop 的数据分析应用 ” 的核心框架
MapReduce 核心功能是将 用户编写的业务逻辑代码 和 自带默认组件 整合成一个完整的分布式运算程序,并发运行在一个 Hadoop 集群上
MapReduce 易于编程
它简单的实现一些接口,就可以完成一个分布式程序, 这个分布式程序可以分布到大量廉价的 PC 机器上运行。也就是说你写一个分布式程序,跟写一个简单的串行程序是一模一样的。就是因为这个特点使得 MapReduce 编程变得非常流行
良好的扩展性
当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力
高容错性
MapReduce 设计的初衷就是使程序能够部署在廉价的 PC 机器上,这就要求它具有很高的容错性。比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败,而且这个过程不需要人工参与,而完全是由 Hadoop 内部完成的
适合 PB 级以上海量数据的离线处理
可以实现上千台服务器集群并发工作,提供数据处理能力
不擅长实时计算
MapReduce 无法像 MySQL 一样,在毫秒或者秒级内返回结果
不擅长流式计算
流式计算的输入数据是动态的,而 MapReduce 的输入数据集是静态的,不能动态变化。
这是因为 MapReduce 自身的设计特点决定了数据源必须是静态的
Spark Streaming , Flink 擅长流式计算
不擅长 DAG(有向无环图)计算
多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下,MapReduce 并不是不能做,而是使用后, 每个 MapReduce 作业的输出结果都会写入到磁盘,会造成大量的磁盘 IO,导致性能非常的低下
Spark 擅长内存迭代
需求:统计其中每一个单词出现的总次数(查询结果:a-p一个文件, q-z一个文件)
分布式的运算程序分成至少 2 个阶段
MapTask 并发实例,完全并行运行,互不相干
ReduceTask 并发实例互不相干,但是他们的数据依赖于上一个阶段的所有 MapTask 并发实例的输出
MapReduce 编程模型只能包含一个 Map 阶段和一个 Reduce 阶段,如果用户的业务逻辑非常复杂,那就只能多个 MapReduce 程序,串行运行
一个完整的 MapReduce 程序在分布式运行时有三类实例进程 :
MrAppMaster:负责整个程序的过程调度及状态协调
MapTask:负责 Map 阶段的整个数据处理流程
ReduceTask:负责 Reduce 阶段的整个数据处理流程
采用反编译工具反编译源码,发现 WordCount 案例有 Map 类、 Reduce 类和驱动类。 且数据的类型是 Hadoop 自身封装的序列化类型
/opt/module/hadoop-3.1.3/share/hadoop/mapreduce
下载 反编译 jd-gui.exe
官方代码 :
package org.apache.hadoop.examples;
import java.io.IOException;
import java.io.PrintStream;
import java.util.StringTokenizer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Mapper.Context;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.Reducer.Context;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.GenericOptionsParser;
public class WordCount {
public static void main(String[] args) throws Exception{
Configuration conf = new Configuration();
String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
if (otherArgs.length < 2) {
System.err.println("Usage: wordcount [...] " );
System.exit(2);
}
Job job = Job.getInstance(conf, "word count");
job.setJarByClass(WordCount.class);
job.setMapperClass(TokenizerMapper.class);
job.setCombinerClass(IntSumReducer.class);
job.setReducerClass(IntSumReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
for (int i = 0; i < otherArgs.length - 1; i++) {
FileInputFormat.addInputPath(job, new Path(otherArgs[i]));
}
FileOutputFormat.setOutputPath(job, new Path(otherArgs[(otherArgs.length - 1)]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
public static class IntSumReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable result = new IntWritable();
public void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
this.result.set(sum);
context.write(key, this.result);
}
}
public static class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable> {
private static final IntWritable one = new IntWritable(1);
private Text word = new Text();
public void map(Object key, Text value, Mapper<Object, Text, Text, IntWritable>.Context context) throws IOException, InterruptedException {
StringTokenizer itr = new StringTokenizer(value.toString());
while (itr.hasMoreTokens()) {
this.word.set(itr.nextToken());
context.write(this.word, one);
}
}
}
}
Java 类型 | Hadoop Writable 类型 |
---|---|
Boolean | BooleanWritable |
Byte | ByteWritable |
Int | IntWritable |
Float | FloatWritable |
Long | LongWritable |
Double | DoubleWritable |
String | Text |
Map | MapWritable |
Array | ArrayWritable |
Null | NullWritable |
用户编写的程序分成三个部分:
// TokenizerMapper 继承Mapper
// 输入数据 : Object, Text
// 输出数据 : Text, IntWritable
public static class TokenizerMapper extends Mapper<Object, Text, Text, IntWritable> {
private static final IntWritable one = new IntWritable(1);
private Text word = new Text();
// 业务逻辑
public void map(Object key, Text value, Mapper<Object, Text, Text, IntWritable>.Context context) throws IOException, InterruptedException {
StringTokenizer itr = new StringTokenizer(value.toString());
while (itr.hasMoreTokens()) {
this.word.set(itr.nextToken());
context.write(this.word, one);
}
}
}
// IntSumReducer 继承 Reducer
//
public static class IntSumReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable result = new IntWritable();
public void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
this.result.set(sum);
context.write(key, this.result);
}
}
相当于 YARN 集群的客户端, 用于提交我们整个程序到 YARN 集群, 提交的是封装了 MapReduce 程序相关运行参数的 job 对象
在给定的文本文件中统计输出每一个单词出现的总次数
输入数据 :
输出数据 :
按照 MapReduce 编程规范, 分别写 :
Mapper
Reducer
Driver
创建 maven 工程, MapReduceDemo
pom.xml :
<dependencies>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-clientartifactId>
<version>3.1.3version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-mapreduce-client-appartifactId>
<version>3.1.3version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-yarn-server-resourcemanagerartifactId>
<version>3.1.3version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>1.7.30version>
dependency>
dependencies>
src/main/resources 目录下,新建一个文件,命名为 log4j.properties
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
package com.cpucode.mapreduce.wordcount;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/**
* KEYIN, map阶段输入的key的类型:LongWritable
* VALUEIN,map阶段输入value类型:Text
* KEYOUT,map阶段输出的Key类型:Text
* VALUEOUT,map阶段输出的value类型:IntWritable
*
* @author : cpucode
* @date : 2021/11/19 13:37
* @github : https://github.com/CPU-Code
* @csdn : https://blog.csdn.net/qq_44226094
*/
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private Text outK = new Text();
private IntWritable outV = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1 获取一行
String line = value.toString();
// 2 切割
String[] words = line.split(" ");
// 3 循环写出
for (String word : words){
// 封装outk
outK.set(word);
// 写出
context.write(outK, outV);
}
}
}
package com.cpucode.mapreduce.wordcount;
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
int sum;
IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
// 1 累加求和
sum = 0;
for (IntWritable count : values) {
sum += count.get();
}
// 2 输出
v.set(sum);
context.write(key,v);
}
}
package com.cpucode.mapreduce.wordcount;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
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;
public class WordCountDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// 1 获取配置信息以及获取 job 对象
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 2 关联本 Driver 程序的 jar
job.setJarByClass(WordCountDriver.class);
// 3 关联 Mapper 和 Reducer 的 jar
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
// 4 设置 Mapper 输出的 kv 类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5 设置最终输出 kv 类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 6 设置输入和输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7 提交 job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
集群上测试
用 maven 打 jar 包, 需要添加的打包插件依赖
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-pluginartifactId>
<version>3.6.1version>
<configuration>
<source>1.8source>
<target>1.8target>
configuration>
plugin>
<plugin>
<artifactId>maven-assembly-pluginartifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependenciesdescriptorRef>
descriptorRefs>
configuration>
<executions>
<execution>
<id>make-assemblyid>
<phase>packagephase>
<goals>
<goal>singlegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
注意:如果工程上显示红叉。 在项目上右键 -> maven -> Reimport 刷新即可
将程序打成 jar 包
修改不带依赖的 jar 包名称为 wc.jar,并拷贝该 jar 包到 Hadoop 集群的 /opt/module/hadoop-3.1.3 路径
上传
启动 Hadoop 集群
myhadoop.sh start
执行 WordCount 程序
hadoop jar mc.jar com.cpucode.mapreduce.wordcountargs.WordCountDriver /input /output
序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输
反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象。
一般来说,“活的”对象只生存在内存里,关机断电就没有了。而且“活的”对象只能由本地的进程使用,不能被发送到网络上的另外一台计算机。 然而序列化可以存储“活的”对象,可以将“活的”对象发送到远程计算机。
Java 的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息, Header,继承体系等),不便于在网络中高效传输。所以,Hadoop 自己开发了一套序列化机制(Writable)
企业开发中往往常用的基本序列化类型不能满足所有需求, 比如在 Hadoop 框架内部传递一个 bean 对象, 那么该对象就需要实现序列化接口
具体实现 bean 对象序列化步骤如下 :
必须实现 Writable 接口
反序列化时,需要反射调用空参构造函数,所以必须有空参构造
public FlowBean() {
super();
}
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
@Override
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}
注意反序列化的顺序和序列化的顺序完全一致
要想把结果显示在文件中,需要重写 toString(),可用 " \t " 分开,方便后续用
如果需要将自定义的 bean 放在 key 中传输,则还需要实现 Comparable 接口,因为 MapReduce 框中的 Shuffle 过程要求对 key 必须能排序
@Override
public int compareTo(FlowBean o) {
// 倒序排列,从大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
统计每一个手机号耗费的总上行流量、 总下行流量、总流量
7 | 13588886666 | 120.134.158.99 | 1524 | 1000 | 200 |
---|---|---|---|---|---|
id | 手机号码 | 网络 ip | 上行流量 | 下行流量 | 网络状态码 |
Map阶段
Reduce阶段
package com.cpucode.mapreduce.writable;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
//1 继承 Writable 接口
public class FlowBean implements Writable {
private long upFlow; //上行流量
private long downFlow; //下行流量
private long sumFlow; //总流量
//2 提供无参构造
public FlowBean() {
}
//3 提供三个参数的 getter 和 setter 方法
public long getUpFlow() {
return upFlow;
}
public void setUpFlow(long upFlow) {
this.upFlow = upFlow;
}
public long getDownFlow() {
return downFlow;
}
public void setDownFlow(long downFlow) {
this.downFlow = downFlow;
}
public long getSumFlow() {
return sumFlow;
}
public void setSumFlow(long sumFlow) {
this.sumFlow = sumFlow;
}
public void setSumFlow() {
this.sumFlow = this.upFlow + this.downFlow;
}
//4 实现序列化和反序列化方法,注意顺序一定要保持一致
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeLong(upFlow);
dataOutput.writeLong(downFlow);
dataOutput.writeLong(sumFlow);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.upFlow = dataInput.readLong();
this.downFlow = dataInput.readLong();
this.sumFlow = dataInput.readLong();
}
//5 重写 ToString
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow;
}
}
package com.cpucode.mapreduce.writable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class FlowMapper extends Mapper<LongWritable, Text, Text, FlowBean>{
private Text outK = new Text();
private FlowBean outV = new FlowBean();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1 获取一行数据,转成字符串
String line = value.toString();
//2 切割数据
String[] split = line.split("\t");
//3 抓取我们需要的数据:手机号,上行流量,下行流量
String phone = split[1];
String up = split[split.length - 3];
String down = split[split.length - 2];
//4 封装 outK outV
outK.set(phone);
outV.setUpFlow(Long.parseLong(up));
outV.setDownFlow(Long.parseLong(down));
outV.setSumFlow();
//5 写出 outK outV
context.write(outK, outV);
}
}
package com.atguigu.mapreduce.writable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class FlowReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
private FlowBean outV = new FlowBean();
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
long totalUp = 0;
long totalDown = 0;
//1 遍历 values,将其中的上行流量,下行流量分别累加
for (FlowBean flowBean : values) {
totalUp += flowBean.getUpFlow();
totalDown += flowBean.getDownFlow();
}
//2 封装 outKV
outV.setUpFlow(totalUp);
outV.setDownFlow(totalDown);
outV.setSumFlow();
//3 写出 outK outV
context.write(key,outV);
}
}
package com.cpucode.mapreduce.writable;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
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;
import java.io.IOException;
public class FlowDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//1 获取 job 对象
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2 关联本 Driver 类
job.setJarByClass(FlowDriver.class);
//3 关联 Mapper 和 Reducer
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
//4 设置 Map 端输出 KV 类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
//5 设置程序最终输出的 KV 类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//6 设置程序的输入输出路径
FileInputFormat.setInputPaths(job, new Path("D:\\inputflow"));
FileOutputFormat.setOutputPath(job, new Path("D:\\flowoutput"));
//7 提交 Job
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
MapTask 的并行度决定 Map 阶段的任务处理并发度,进而影响到整个 Job 的处理速度
假设:
1G 的数据, 启动 8 个 MapTask, 可以提高集群的并发处理能力
那么 1K 的数据,也启动 8 个 MapTask,会提高集群性能吗?
MapTask 并行任务是否越多越好呢? 哪些因素影响了 MapTask 并行度
数据块: Block 是 HDFS 物理上把数据分成一块一块。 数据块是 HDFS 存储数据单位
数据切片: 数据切片只是在逻辑上对输入进行分片, 并不会在磁盘上将其切分成片进行存储。数据切片是 MapReduce 程序计算输入数据的单位,一个切片会对应启动一个 MapTask
假设切片大小设置为100M
假设切片大小设置为128M
一个 Job 的 Map 阶段并行度由客户端在提交Job时的切片数决定
每一个 Split 切片分配一个 MapTask 并行实例处理
默认情况下,切片大小 = BlockSize
切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
waitForCompletion()
submit();
// 1 建立连接
connect();
// 1)创建提交 Job 的代理
new Cluster(getConfiguration());
// (1)判断是本地运行环境还是 yarn 集群运行环境
initialize(jobTrackAddr, conf);
// 2 提交 job
submitter.submitJobInternal(Job.this, cluster);
// 1)创建给集群提交数据的 Stag 路径
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
// 2)获取 jobid ,并创建 Job 路径
JobID jobId = submitClient.getNewJobID();
// 3)拷贝 jar 包到集群
copyAndConfigureFiles(job, submitJobDir);
rUploader.uploadFiles(job, jobSubmitDir);
// 4)计算切片,生成切片规划文件
writeSplits(job, submitJobDir);
maps = writeNewSplits(job, jobSubmitDir);
input.getSplits(job);
// 5)向 Stag 路径写 XML 配置文件
writeConf(conf, submitJobFile);
conf.writeXml(out);
// 6)提交 Job,返回提交状态
status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
Map 方法之后, Reduce 方法之前的数据处理过程称之为 Shuffle。
问题引出
要求将统计结果按照条件输出到不同文件中( 分区) 。 比如:将统计结果按照手机
归属地不同省份输出到不同文件中( 分区)
将统计结果按照手机归属地不同省份输出到不同文件中(分区)
(1)输入数据
phone_data .txt
(2)期望输出数据
手机号 136、 137、 138、 139 开头都分别放到一个独立的 4 个文件中,其他开头的放到
一个文件中。
排序是MapReduce框架中最重要的操作之一。
MapTask和ReduceTask均会对数据按照key进行排序。 该操作属于
Hadoop的默认行为。 任何应用程序中的数据均会被排序, 而不管逻辑上是
否需要。
默认排序是按照字典顺序排序, 且实现该排序的方法是快速排序。
对于MapTask, 它会将处理的结果暂时放到环形缓冲区中, 当环形缓冲区使
用率达到一定阈值后, 再对缓冲区中的数据进行一次快速排序, 并将这些有序数
据溢写到磁盘上, 而当数据处理完毕后, 它会对磁盘上所有文件进行归并排序。
对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大
小超过一定阈值, 则溢写磁盘上, 否则存储在内存中。如果磁盘上文件数目达到
一定阈值,则进行一次归并排序以生成一个更大文件;如果内存中文件大小或者
数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完
毕后, ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
1)部分排序
MapReduce根据输入记录的键对数据集排序。 保证输出的每个文件内部有序。
( 2)全排序
最终输出结果只有一个文件, 且文件内部有序。 实现方式是只设置一个ReduceTask。 但该方法在
处理大型文件时效率极低, 因为一台机器处理所有文件, 完全丧失了MapReduce所提供的并行架构。
( 3) 辅助排序: ( GroupingComparator分组)
在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部
字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序。
( 4)二次排序
在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。
需求
根据案例 2.3 序列化案例产生的结果再次对总流量进行倒序排序。
(1)输入数据
原始数据 第一次处理后的数据
phone_data .txt part-r-00000
(2)期望输出数据
需求
要求每个省份手机号输出的文件中按照总流量内部排序。
2) 需求分析
基于前一个需求, 增加自定义分区类, 分区按照省份手机号设置。
Combiner是MR程序中Mapper和Reducer之外的一种组件。
( 2) Combiner组件的父类就是Reducer。
( 3) Combiner和Reducer的区别在于运行的位置
Combiner是在每一个MapTask所在的节点运行;
( 4) Combiner的意义就是对每一个MapTask的输出进行局部汇总, 以减小网络传输量。
( 5) Combiner能够应用的前提是不能影响最终的业务逻辑, 而且, Combiner的输出kv
应该跟Reducer的输入kv类型要对应起来。
统计过程中对每一个 MapTask 的输出进行局部汇总,以减小网络传输量即采用
Combiner 功能。
OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了 OutputFormat
接口。下面我们介绍几种常见的OutputFormat实现类。
OutputFormat实现类
3.1 应用场景:
例如:输出数据到MySQL/HBase/Elasticsearch等存储框架中。
3.2 自定义OutputFormat步骤
➢ 自定义一个类继承FileOutputFormat。
➢ 改写RecordWriter,具体改写输出数据的方法write()。
过滤输入的 log 日志,包含 atguigu 的网站输出到 e:/atguigu.log,不包含 atguigu 的网站
Read 阶段: MapTask 通过 InputFormat 获得的 RecordReader, 从输入 InputSplit 中
解析出一个个 key/value。
(2) Map 阶段:该节点主要是将解析出的 key/value 交给用户编写 map()函数处理,并
产生一系列新的 key/value。
( 3) Collect 收集阶段:在用户编写 map()函数中,当数据处理完成后,一般会调用
OutputCollector.collect()输出结果。在该函数内部,它会将生成的 key/value 分区( 调用
Partitioner) , 并写入一个环形内存缓冲区中。
(4) Spill 阶段:即“溢写”, 当环形缓冲区满后, MapReduce 会将数据写到本地磁盘上,
生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排
序,并在必要时对数据进行合并、 压缩等操作。
溢写阶段详情:
步骤 1: 利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号
Partition 进行排序,然后按照 key 进行排序。这样, 经过排序后,数据以分区为单位聚集在
一起,且同一分区内所有数据按照 key 有序。
步骤 2: 按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文
件 output/spillN.out(N 表示当前溢写次数)中。如果用户设置了 Combiner,则写入文件之
前,对每个分区中的数据进行一次聚集操作。
步骤 3: 将分区数据的元信息写到内存索引数据结构 SpillRecord 中,其中每个分区的元
信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大
小超过 1MB,则将内存索引写到文件 output/spillN.out.index 中。
(5) Merge 阶段:当所有数据处理完成后, MapTask 对所有临时文件进行一次合并,
以确保最终只会生成一个数据文件。
当所有数据处理完后, MapTask 会将所有临时文件合并成一个大文件, 并保存到文件
output/file.out 中,同时生成相应的索引文件 output/file.out.index。
在进行文件合并过程中, MapTask 以分区为单位进行合并。对于某个分区, 它将采用多
轮递归合并的方式。 每轮合并 mapreduce.task.io.sort.factor(默认 10) 个文件,并将产生的文
件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。
让每个 MapTask 最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量
小文件产生的随机读取带来的开销。
Copy 阶段: ReduceTask 从各个 MapTask 上远程拷贝一片数据,并针对某一片数
据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
(2) Sort 阶段:在远程拷贝数据的同时, ReduceTask 启动了两个后台线程对内存和磁
盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照 MapReduce 语义,用
户编写 reduce()函数输入数据是按 key 进行聚集的一组数据。 为了将 key 相同的数据聚在一
起, Hadoop 采用了基于排序的策略。由于各个 MapTask 已经实现对自己的处理结果进行了
局部排序,因此, ReduceTask 只需对所有数据进行一次归并排序即可。
(3) Reduce 阶段: reduce()函数将计算结果写到 HDFS 上
回顾: MapTask 并行度由切片个数决定,切片个数由输入文件和切片规则决定。
思考: ReduceTask 并行度由谁决定?
1) 设置 ReduceTask 并行度(个数)
ReduceTask 的并行度同样影响整个 Job 的执行并发度和执行效率,但与 MapTask 的并
发数由切片数决定不同, ReduceTask 数量的决定是可以直接手动设置:
// 默认值是 1,手动设置为 4
job.setNumReduceTasks(4);
2) 实验:测试 ReduceTask 多少合适
(1)实验环境: 1 个 Master 节点, 16 个 Slave 节点: CPU:8GHZ,内存: 2G
(2)实验结论
ReduceTask=0, 表示没有Reduce阶段, 输出文件个数和Map个数一致。
( 2) ReduceTask默认值就是1, 所以输出文件个数为一个。
( 3) 如果数据分布不均匀, 就有可能在Reduce阶段产生数据倾斜
( 4) ReduceTask数量并不是任意设置, 还要考虑业务逻辑需求, 有些情况下, 需要计算全
局汇总结果, 就只能有1个ReduceTask。
( 5) 具体多少个ReduceTask, 需要根据集群性能而定。
( 6) 如果分区数不是1, 但是ReduceTask为1, 是否执行分区过程。 答案是:不执行分区过
程。 因为在MapTask的源码中, 执行分区的前提是先判断ReduceNum个数是否大于1。 不大于1
肯定不执行
context.write(k, NullWritable.get()); //自定义的 map 方法的写出,进入
output.write(key, value);
//MapTask727 行,收集方法,进入两次
collector.collect(key, value,partitioner.getPartition(key, value, partitions));
HashPartitioner(); //默认分区器
collect() //MapTask1082 行 map 端所有的 kv 全部写出后会走下面的 close 方法
close() //MapTask732 行
collector.flush() // 溢出刷写方法, MapTask735 行, 提前打个断点,进入
sortAndSpill() //溢写排序, MapTask1505 行,进入
sorter.sort() QuickSort //溢写排序方法, MapTask1625 行,进入
mergeParts(); //合并文件, MapTask1527 行,进入
if (isMapOrReduce()) //reduceTask324 行, 提前打断点
initialize() // reduceTask333 行,进入
init(shuffleContext); // reduceTask375 行,走到这需要先给下面的打断点
totalMaps = job.getNumMapTasks(); // ShuffleSchedulerImpl 第 120 行,提前打断点
merger = createMergeManager(context); //合并方法, Shuffle 第 80 行
// MergeManagerImpl 第 232 235 行,提前打断点
this.inMemoryMerger = createInMemoryMerger(); //内存合并
this.onDiskMerger = new OnDiskMerger(this); //磁盘合并
rIter = shuffleConsumerPlugin.run();
eventFetcher.start(); //开始抓取数据, Shuffle 第 107 行,提前打断点
eventFetcher.shutDown(); //抓取结束, Shuffle 第 141 行,提前打断点
copyPhase.complete(); //copy 阶段完成, Shuffle 第 151 行
taskStatus.setPhase(TaskStatus.Phase.SORT); //开始排序阶段, Shuffle 第 152 行
sortPhase.complete(); //排序阶段完成,即将进入 reduce 阶段 reduceTask382 行
reduce(); //reduce 阶段调用的就是我们自定义的 reduce 方法,会被调用多次
cleanup(context); //reduce 完成之前,会最后调用一次 Reducer 里面的 cleanup 方法
Map 端的主要工作:为来自不同表或文件的 key/value 对, 打标签以区别不同来源的记录。然后用连接字段作为 key,其余部分和新加的标志作为 value,最后进行输出。
Reduce 端的主要工作:在 Reduce 端以连接字段作为 key 的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在 Map 阶段已经打标志) 分开,最后进行合并就 ok
ETL,是英文 Extract-Transform-Load 的缩写,用来描述将数据从来源端经过抽取(Extract)、转换(Transform)、加载(Load)至目的端的过程。 ETL 一词较常用在数据仓库,但其对象并不限于数据仓库
在运行核心业务 MapReduce 程序之前,往往要先对数据进行清洗,清理掉不符合用户要求的数据。 清理的过程往往只需要运行 Mapper 程序,不需要运行 Reduce 程序
输入数据接口: InputFormat
(1) 默认使用的实现类是: TextInputFormat
(2) TextInputFormat 的功能逻辑是:一次读一行文本,然后将该行的起始偏移量作为
key,行内容作为 value 返回。
(3) CombineTextInputFormat 可以把多个小文件合并成一个切片处理,提高处理效率。
2) 逻辑处理接口: Mapper
用户根据业务需求实现其中三个方法: map() setup() cleanup ()
3) Partitioner 分区
(1)有默认实现 HashPartitioner,逻辑是根据 key 的哈希值和 numReduces 来返回一个
分区号; key.hashCode()&Integer.MAXVALUE % numReduces
(2)如果业务上有特别的需求,可以自定义分区。
4) Comparable 排序
(1)当我们用自定义的对象作为 key 来输出时,就必须要实现 WritableComparable 接
口,重写其中的 compareTo()方法。
(2) 部分排序:对最终输出的每一个文件进行内部排序。
(3)全排序:对所有数据进行排序,通常只有一个 Reduce。
(4)二次排序:排序的条件有两个。
5) Combiner 合并
Combiner 合并可以提高程序执行效率,减少 IO 传输。但是使用时必须不能影响原有的业务处理结果。
6) 逻辑处理接口: Reducer
用户根据业务需求实现其中三个方法: reduce() setup() cleanup ()
7) 输出数据接口: OutputFormat
(1)默认实现类是 TextOutputFormat,功能逻辑是:将每一个 KV 对,向目标文本文件
输出一行。
(2)用户还可以自定义 OutputFormat。
1)压缩的好处和坏处
压缩的优点: 以减少磁盘 IO、减少磁盘存储空间。
压缩的缺点:增加 CPU 开销。
2) 压缩原则
(1)运算密集型的 Job,少用压缩
(2) IO 密集型的 Job,多用压缩
压缩格式 | Hadoop 自带? | 算法 | 文件扩展名 | 是否可切片 | 换成压缩格式后,原来的程序是否需要修改 |
---|---|---|---|---|---|
DEFLATE | 使用 | DEFLATE | .deflate | 否 | 和文本处理一样,不需要修改 |
Gzip | 使用 | DEFLATE | .gz | 是 | 和文本处理一样,不需要修改 |
bzip2 | 使用 | bzip2 | .bz2 | 是 | 和文本处理一样,不需要修改 |
LZO | 安装 | LZO | .lzo | 是 | 需要建索引,还需要指定输入格式 |
Snappy | 使用 | Snappy | .snappy | 否 | 和文本处理一样,不需要修改 |
压缩算法 | 原始文件大小 | 压缩文件大小 | 压缩速度 | 解压速度 |
---|---|---|---|---|
压缩方式选择时重点考虑: 压缩/解压缩速度、压缩率(压缩后存储大小)、压缩后是否可以支持切片
为了支持多种压缩/解压缩算法, Hadoop 引入了编码/解码器
要在 Hadoop 中启用压缩,可以配置如下参数
即使你的 MapReduce 的输入输出文件都是未压缩的文件,你仍然可以对 Map 任务的中
间结果输出做压缩,因为它要写在硬盘并且通过网络传输到 Reduce 节点,对其压缩可以提
高很多性能,这些工作只要设置两个属性即可,我们来看下代码怎么设置