MapReduce概述
定义
MapReduce是一个分布式运算程序的编程框架
MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。
MapReduce优缺点
优点
- MapReduce易于编程,简单的实现一些接口,就可以完成一个分布式程序,可以分布到大量廉价的PC机器上运行
- 良好的扩展性,计算资源不能得到满足时,简单的增加机器扩展它的计算能力
- 高容错性,其中一台机器挂了,把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败
-
缺点
不擅长实时计算。无法像MySQL一样在毫秒或者秒级内返回结果
- 不擅长流式计算。MapReduce的输入数据集是静态的
- 不擅长DAG(有向无环图)计算
多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下,MapReduce并不是不能做,而是使用后,每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常的低下。
MapReduce核心思想

- 分布式的运算程序往往需要分成至少2个阶段。
- 第一个阶段的MapTask并发实例,完全并行运行,互不相干。
- 第二个阶段的ReduceTask并发实例互不相干,但是数据依赖于上一个阶段的所有MapTask并发实例的输出。
MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行。
MapReduce进程
MapReduce程序在分布式运行时有三类实例进程:
MrAppMaster:负责整个程序的过程调度及状态协调
- MapTask:负责Map阶段的整个数据处理流程
ReduceTask:负责Reduce阶段的整个数据处理流程
官方WordCount源码
Map类、Reduce类、驱动类
[qtbhy@hadoop102 mapreduce]$ pwd/opt/module/hadoop-3.1.3/share/hadoop/mapreduce
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 <in> [<in>...] <out>");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);}// <Text, IntWritable, Text, IntWritable>// Text, IntWritable输入数据的k-v;Text, IntWritable输出数据的k-vpublic 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);}}// <Object, Text, Text, IntWritable>// Object, Text输入数据的k-v;Text, IntWritable数据数据的k-vpublic 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 |
MapReduce编程规范
用户编写的程序分成三个部分:Mapper、Reducer和Driver
- Mapper阶段
- 用户自定义的Mapper要继承自己的父类
- Mapper的输入数据是KV对的形式(KV的类型可自定义)
- Mapper中的业务逻辑写在map()方法中
- Mapper的输出数据是KV对的形式(KV的类型可自定义)
- map()方法(MapTask进程)对每一个
调用一次
- Reducer阶段
- 用户自定义的Reducer要继承自己的父类
- Reducer的输入数据类型对应Mapper的输出数据类型,也是KV
- Reducer的业务逻辑写在reduce()方法中
- ReduceTask进程对每一组相同k的
组调用一次reduce()方法
- Driver阶段
相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是封装了MapReduce程序相关运行参数的job对象
WordCount案例
需求
需求分析
分别编写Mapper,Reducer,Driver
输入数据
at atsaa saaxx ccsjiaobanzhangxuehadoop
输出数据
at 2banzhang 1css 1hadoop 1jiao 1saa 2xue 1xx 1
Mapper
- 将MapTask传入的文本内容转换成String
at at
- 根据空格将这一行切分成单词
at
at
- 将单词输出为<单词,1>
at,1
at,1
- Reducer
- 汇总各个key的个数
at,1
at,1
- 输出该key的总次数
at,2
Driver
创建maven工程
添加依赖
<dependencies><dependency><groupId>org.apache.hadoop</groupId><artifactId>hadoop-client</artifactId><version>3.1.3</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>1.7.30</version></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编写程序
编写Mapper类
```java package com.example.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 */ public class WordCountMapper extends Mapper
{ private Text outK = new Text(); private IntWritable outV = new IntWritable(1); @Override protected void map(LongWritable key, Text value, Mapper .Context context) throws IOException, InterruptedException { // 1.获取一行 // at at String line = value.toString(); // 2.切割 // at // at String[] words = line.split(" "); // 3.循环写出 for (String word : words) { // 封装outK outK.set(word); // 写出 context.write(outK, outV); }编写Reducer类
```java package com.example.mapreduce.wordcount;
import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
- KEYIN,reduce阶段输入的key的类型——Text
- VALUEIN,reduce阶段输入value类型——IntWritable
- KEYOUT,reduce阶段输出的key的类型——Text
VALUEOUT,reduce阶段输出value类型——IntWritable */ public class WordCountReducer extends Reducer
{ private IntWritable outV = new IntWritable(); @Override protected void reduce(Text key, Iterable
values, Reducer .Context context) throws IOException, InterruptedException { int sum = 0; // at,(1,1) // 累加 for (IntWritable value : values) { sum += value.get(); } outV.set(sum); // 写出 context.write(key, outV);编写Driver驱动类
```java package com.example.mapreduce.wordcount;
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; import java.io.IOException;
public class WordCountDriver { public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException { // 1. 获取job Configuration conf = new Configuration(); Job job = Job.getInstance(conf);
// 2. 设置jar包路径
job.setJarByClass(WordCountDriver.class);
// 3. 关联mapper和reducer
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
// 4. 设置map输出的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("C:\\Users\\ace\\Desktop\\input"));
FileOutputFormat.setOutputPath(job, new Path("C:\\Users\\ace\\Desktop\\output"));
// 7. 提交job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
<a name="N2Ixz"></a>
### 本地测试

<a name="Sn3ex"></a>
### 集群测试
1. 添加maven打jar包的依赖
```xml
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
修改输入路径和输出路径为参数
// 6. 设置输入路径和输出路径 FileInputFormat.setInputPaths(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1]));打包

- 上传到虚拟机
运行
[qtbhy@hadoop102 hadoop-3.1.3]$ hadoop jar wc.jar com.example.mapreduce.wordcount.WordCountDriver /input /outputHadoop序列化
序列化
概念
序列化:把内存中的对象转换成字节序列或其他数据传输协议以便于存储到磁盘(持久化)和网络传输
反序列化:将收到字节序列(或其他数据传输协议)或者磁盘的持久化数据,转换成内存中的对象原因
序列化可以存储“活的”对象,可以将“活的”对象发送到远程计算机
为什么不用Java的序列化
Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输
特点
紧凑:高效使用存储空间
- 快速:读写数据的额外开销小
-
自定义bean对象实现序列化接口(Writable)
步骤
实现Writable接口
- 反序列化时,需要反射调用空参构造函数,必须有空参构造
- 重写序列化方法
- 重写反序列化方法
- 反序列化的顺序和序列化的顺序完全一致
- 重写toString()
如果需要将自定义的bean放在key中传输,还需要实现Comparable接口(MapReduce框中的Shuffle过程要求对key必须能排序)
实例
需求
统计每一个手机号耗费的总上行流量、总下行流量、总流量
输入数据
格式:id 手机号码 网络ip 上行流量 下行流量 网络状态码
1 13736230513 192.196.100.1 www.atguigu.com 2481 24681 200
2 13846544121 192.196.100.2 264 0 200
3 13956435636 192.196.100.3 132 1512 200
4 13966251146 192.168.100.1 240 0 404
5 18271575951 192.168.100.2 www.atguigu.com 1527 2106 200
6 84188413 192.168.100.3 www.atguigu.com 4116 1432 200
7 13590439668 192.168.100.4 1116 954 200
8 15910133277 192.168.100.5 www.hao123.com 3156 2936 200
9 13729199489 192.168.100.6 240 0 200
10 13630577991 192.168.100.7 www.shouhu.com 6960 690 200
11 15043685818 192.168.100.8 www.baidu.com 3659 3538 200
12 15959002129 192.168.100.9 www.atguigu.com 1938 180 500
13 13560439638 192.168.100.10 918 4938 200
14 13470253144 192.168.100.11 180 180 200
15 13682846555 192.168.100.12 www.qq.com 1938 2910 200
16 13992314666 192.168.100.13 www.gaga.com 3008 3720 200
17 13509468723 192.168.100.14 www.qinghua.com 7335 110349 404
18 18390173782 192.168.100.15 www.sogou.com 9531 2412 200
19 13975057813 192.168.100.16 www.baidu.com 11058 48243 200
20 13768778790 192.168.100.17 120 120 200
21 13568436656 192.168.100.18 www.alibaba.com 2481 24681 200
22 13568436656 192.168.100.19 1116 954 200
- 期望输出数据格式
编写程序
- 流量统计的Bean对象 ```java package com.example.mapreduce.writable;
import org.apache.hadoop.io.Writable;
import java.io.DataInput; import java.io.DataOutput; import java.io.IOException;
/**
- 定义类实现writable接口
- 重写序列化和反序列化方法
- 重写空参构造
toString方法 */ public class FlowBean implements Writable { private long upFlow; // 上行流量 private long downFlow; // 下行流量 private long sumFlow; // 总流量
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; }
// 空参构造 public FlowBean() { }
@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(); }
@Override public String toString() { return upFlow + “\t” + downFlow + “\t” + sumFlow; } } ```
- Mapper类 ```java package com.example.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
// 2. 切割
// 1,13736230513,192.196.100.1,www.atguigu.com,2481,24681,200
String[] split = line.split("\t");
// 3. 抓取想要的数据
// 手机号 上行流量 下行流量
String phone = split[1];
String up = split[split.length - 3];
String down = split[split.length - 2];
// 4. 封装
outK.set(phone);
outV.setUpFlow(Long.parseLong(up));
outV.setDownFlow(Long.parseLong(down));
outV.setSumFlow();
// 5. 写出
context.write(outK, outV);
}
}
3. Reducer类
```java
package com.example.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, Reducer<Text, FlowBean, Text, FlowBean>.Context context) throws IOException, InterruptedException {
// 1. 遍历集合累加值
long totalUp = 0;
long totalDown = 0;
for (FlowBean value : values) {
totalUp += value.getUpFlow();
totalDown += value.getDownFlow();
}
// 2. 封装outK,outV
outV.setUpFlow(totalUp);
outV.setDownFlow(totalDown);
outV.setSumFlow();
// 3. 写出
context.write(key, outV);
}
}
- Driver类 ```java package com.example.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, InterruptedException, ClassNotFoundException { // 1. 获取job Configuration conf = new Configuration(); Job job = Job.getInstance(conf);
// 2. 设置jar
job.setJarByClass(FlowDriver.class);
// 3. 关联mapper和reducer
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
// 4. 设置mapper 输出key和value类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
// 5. 设置最终数据输出的key和value类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
// 6. 设置数据的输入路径和输出路径
FileInputFormat.setInputPaths(job, new Path("C:\\Users\\ace\\Desktop\\input"));
FileOutputFormat.setOutputPath(job, new Path("C:\\Users\\ace\\Desktop\\output"));
// 7. 提交job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
<a name="qmwpG"></a>
### 测试

<a name="tU88t"></a>
# MapReduce框架原理

<a name="OAZjB"></a>
## 1. InputFormat数据输入
<a name="CRH35"></a>
### 切片与MapTask并行度决定机制
数据块:数据块是HDFS存储数据单位(Block)<br />数据切片:在逻辑上对输入进行分片,不会在磁盘上将其切分成片进行存储。<br /> 数据切片是MapReduce程序计算输入数据的单位,一个切片会对应启动一个MapTask<br />
<a name="tlwev"></a>
### Job提交流程源码和切片源码
<a name="Nfx0L"></a>
#### Job提交流程源码

```java
public void submit() throws IOException, InterruptedException, ClassNotFoundException {
// 状态
this.ensureState(Job.JobState.DEFINE);
// 解决新旧API兼容性问题
this.setUseNewAPI();
// 连接本地客户端
/*
private synchronized void connect() throws IOException, InterruptedException, ClassNotFoundException {
if (this.cluster == null) {
this.cluster = (Cluster)this.ugi.doAs(new PrivilegedExceptionAction<Cluster>() {
public Cluster run() throws IOException, InterruptedException, ClassNotFoundException {
return new Cluster(Job.this.getConfiguration());
}
});
}
}
*/
/*
public Cluster(InetSocketAddress jobTrackAddr, Configuration conf) throws IOException {
this.fs = null;
this.sysDir = null;
this.stagingAreaDir = null;
this.jobHistoryDir = null;
this.providerList = null;
this.conf = conf;
this.ugi = UserGroupInformation.getCurrentUser();
this.initialize(jobTrackAddr, conf);
}
*/
/*
private void initialize(InetSocketAddress jobTrackAddr, Configuration conf) throws IOException {
this.initProviderList();
IOException initEx = new IOException("Cannot initialize Cluster. Please check your configuration for mapreduce.framework.name and the correspond server addresses.");
if (jobTrackAddr != null) {
LOG.info("Initializing cluster for Job Tracker=" + jobTrackAddr.toString());
}
// 两个客户端 图
Iterator var4 = this.providerList.iterator();
while(var4.hasNext()) {
ClientProtocolProvider provider = (ClientProtocolProvider)var4.next();
LOG.debug("Trying ClientProtocolProvider : " + provider.getClass().getName());
ClientProtocol clientProtocol = null;
try {
if (jobTrackAddr == null) {
clientProtocol = provider.create(conf);
} else {
clientProtocol = provider.create(jobTrackAddr, conf);
}
if (clientProtocol != null) {
this.clientProtocolProvider = provider;
this.client = clientProtocol;
LOG.debug("Picked " + provider.getClass().getName() + " as the ClientProtocolProvider");
break;
}
LOG.debug("Cannot pick " + provider.getClass().getName() + " as the ClientProtocolProvider - returned null protocol");
} catch (Exception var9) {
String errMsg = "Failed to use " + provider.getClass().getName() + " due to error: ";
initEx.addSuppressed(new IOException(errMsg, var9));
LOG.info(errMsg, var9);
}
}
if (null == this.clientProtocolProvider || null == this.client) {
throw initEx;
}
}
*/
this.connect();
// 提交过程
/*
JobStatus submitJobInternal(Job job, Cluster cluster) throws ClassNotFoundException, InterruptedException, IOException {
this.checkSpecs(job); // 校验输出路径和输出路径不存在
Configuration conf = job.getConfiguration();
addMRFrameworkToDistributedCache(conf);
// jobStagingArea: file/tmp/hadoop/mapred/staging/ace1947161686/.staging
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
InetAddress ip = InetAddress.getLocalHost();
if (ip != null) {
this.submitHostAddress = ip.getHostAddress();
this.submitHostName = ip.getHostName();
conf.set("mapreduce.job.submithostname", this.submitHostName);
conf.set("mapreduce.job.submithostaddress", this.submitHostAddress);
}
// jobId:job_local1947161686_0001
JobID jobId = this.submitClient.getNewJobID();
job.setJobID(jobId);
Path submitJobDir = new Path(jobStagingArea, jobId.toString());
JobStatus status = null;
JobStatus var25;
try {
conf.set("mapreduce.job.user.name", UserGroupInformation.getCurrentUser().getShortUserName());
conf.set("hadoop.http.filter.initializers", "org.apache.hadoop.yarn.server.webproxy.amfilter.AmFilterInitializer");
conf.set("mapreduce.job.dir", submitJobDir.toString());
LOG.debug("Configuring job " + jobId + " with " + submitJobDir + " as the submit dir");
TokenCache.obtainTokensForNamenodes(job.getCredentials(), new Path[]{submitJobDir}, conf);
this.populateTokenCache(conf, job.getCredentials());
if (TokenCache.getShuffleSecretKey(job.getCredentials()) == null) {
KeyGenerator keyGen;
try {
keyGen = KeyGenerator.getInstance("HmacSHA1");
keyGen.init(64);
} catch (NoSuchAlgorithmException var20) {
throw new IOException("Error generating shuffle secret key", var20);
}
SecretKey shuffleKey = keyGen.generateKey();
TokenCache.setShuffleSecretKey(shuffleKey.getEncoded(), job.getCredentials());
}
if (CryptoUtils.isEncryptedSpillEnabled(conf)) {
conf.setInt("mapreduce.am.max-attempts", 1);
LOG.warn("Max job attempts set to 1 since encrypted intermediatedata spill is enabled");
}
this.copyAndConfigureFiles(job, submitJobDir);
Path submitJobFile = JobSubmissionFiles.getJobConfPath(submitJobDir);
LOG.debug("Creating splits at " + this.jtFs.makeQualified(submitJobDir));
int maps = this.writeSplits(job, submitJobDir);
conf.setInt("mapreduce.job.maps", maps);
LOG.info("number of splits:" + maps);
int maxMaps = conf.getInt("mapreduce.job.max.map", -1);
if (maxMaps >= 0 && maxMaps < maps) {
throw new IllegalArgumentException("The number of map tasks " + maps + " exceeded limit " + maxMaps);
}
String queue = conf.get("mapreduce.job.queuename", "default");
AccessControlList acl = this.submitClient.getQueueAdmins(queue);
conf.set(QueueManager.toFullPropertyName(queue, QueueACL.ADMINISTER_JOBS.getAclName()), acl.getAclString());
TokenCache.cleanUpTokenReferral(conf);
if (conf.getBoolean("mapreduce.job.token.tracking.ids.enabled", false)) {
ArrayList<String> trackingIds = new ArrayList();
Iterator var15 = job.getCredentials().getAllTokens().iterator();
while(var15.hasNext()) {
Token<? extends TokenIdentifier> t = (Token)var15.next();
trackingIds.add(t.decodeIdentifier().getTrackingId());
}
conf.setStrings("mapreduce.job.token.tracking.ids", (String[])trackingIds.toArray(new String[trackingIds.size()]));
}
ReservationId reservationId = job.getReservationId();
if (reservationId != null) {
conf.set("mapreduce.job.reservation.id", reservationId.toString());
}
this.writeConf(conf, submitJobFile);
this.printTokens(jobId, job.getCredentials());
status = this.submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
if (status == null) {
throw new IOException("Could not launch job");
}
var25 = status;
} finally {
if (status == null) {
LOG.info("Cleaning up the staging area " + submitJobDir);
if (this.jtFs != null && submitJobDir != null) {
this.jtFs.delete(submitJobDir, true);
}
}
}
return var25;
}
*/
final JobSubmitter submitter = this.getJobSubmitter(this.cluster.getFileSystem(), this.cluster.getClient());
this.status = (JobStatus)this.ugi.doAs(new PrivilegedExceptionAction<JobStatus>() {
public JobStatus run() throws IOException, InterruptedException, ClassNotFoundException {
/*
private void copyAndConfigureFiles(Job job, Path jobSubmitDir) throws IOException {
Configuration conf = job.getConfiguration();
boolean useWildcards = conf.getBoolean("mapreduce.client.libjars.wildcard", true);
JobResourceUploader rUploader = new JobResourceUploader(this.jtFs, useWildcards);
rUploader.uploadResources(job, jobSubmitDir);
job.getWorkingDirectory();
}
*/
this.copyAndConfigureFiles(job, submitJobDir);
Path submitJobFile = JobSubmissionFiles.getJobConfPath(submitJobDir);
LOG.debug("Creating splits at " + this.jtFs.makeQualified(submitJobDir));
int maps = this.writeSplits(job, submitJobDir);
conf.setInt("mapreduce.job.maps", maps);
LOG.info("number of splits:" + maps);
int maxMaps = conf.getInt("mapreduce.job.max.map", -1);
if (maxMaps >= 0 && maxMaps < maps) {
throw new IllegalArgumentException("The number of map tasks " + maps + " exceeded limit " + maxMaps);
}
String queue = conf.get("mapreduce.job.queuename", "default");
AccessControlList acl = this.submitClient.getQueueAdmins(queue);
conf.set(QueueManager.toFullPropertyName(queue, QueueACL.ADMINISTER_JOBS.getAclName()), acl.getAclString());
TokenCache.cleanUpTokenReferral(conf);
if (conf.getBoolean("mapreduce.job.token.tracking.ids.enabled", false)) {
ArrayList<String> trackingIds = new ArrayList();
Iterator var15 = job.getCredentials().getAllTokens().iterator();
while(var15.hasNext()) {
Token<? extends TokenIdentifier> t = (Token)var15.next();
trackingIds.add(t.decodeIdentifier().getTrackingId());
}
conf.setStrings("mapreduce.job.token.tracking.ids", (String[])trackingIds.toArray(new String[trackingIds.size()]));
}
ReservationId reservationId = job.getReservationId();
if (reservationId != null) {
conf.set("mapreduce.job.reservation.id", reservationId.toString());
}
this.writeConf(conf, submitJobFile);
this.printTokens(jobId, job.getCredentials());
status = this.submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
if (status == null) {
throw new IOException("Could not launch job");
}
var25 = status;
} finally {
if (status == null) {
LOG.info("Cleaning up the staging area " + submitJobDir);
if (this.jtFs != null && submitJobDir != null) {
this.jtFs.delete(submitJobDir, true);
}
}
}
return var25;
}
*/
return submitter.submitJobInternal(Job.this, Job.this.cluster);
}
});
this.state = Job.JobState.RUNNING;
LOG.info("The url to track the job: " + this.getTrackingURL());
}
切片源码
- 存储路径目录
- 遍历处理目录下的每一个文件
- 遍历文件xxx.txt
- 获取文件大小
- 计算切片大小,默认情况下,切片大小=blocksize
- 开始切片
- 切片1:0-128M
- 切片2:128-256M
- 切片3:256M-300M
每次切片都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分一块切片
- 将切片信息写到一个切片规划文件中
- 整个切片的核心过程在getSplit()方法中完成
InputSplit只记录了切片的元数据信息,如起始位置、长度以及所在的节点列表等
- 提交切片规划文件到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数
```java
public List
getSplits(JobContext job) throws IOException { StopWatch sw = (new StopWatch()).start(); long minSize = Math.max(this.getFormatMinSplitSize(), getMinSplitSize(job)); long maxSize = getMaxSplitSize(job); List splits = new ArrayList(); List files = this.listStatus(job); boolean ignoreDirs = !getInputDirRecursive(job) && job.getConfiguration().getBoolean(“mapreduce.input.fileinputformat.input.dir.nonrecursive.ignore.subdirs”, false); Iterator var10 = files.iterator();
while(true) { while(true) {
while(true) { FileStatus file; do { if (!var10.hasNext()) { job.getConfiguration().setLong("mapreduce.input.fileinputformat.numinputfiles", (long)files.size()); sw.stop(); if (LOG.isDebugEnabled()) { LOG.debug("Total # of splits generated by getSplits: " + splits.size() + ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS)); } return splits; } file = (FileStatus)var10.next(); } while(ignoreDirs && file.isDirectory()); Path path = file.getPath(); long length = file.getLen(); if (length != 0L) { BlockLocation[] blkLocations; if (file instanceof LocatedFileStatus) { blkLocations = ((LocatedFileStatus)file).getBlockLocations(); } else { FileSystem fs = path.getFileSystem(job.getConfiguration()); blkLocations = fs.getFileBlockLocations(file, 0L, length); } if (this.isSplitable(job, path)) { long blockSize = file.getBlockSize(); long splitSize = this.computeSplitSize(blockSize, minSize, maxSize); long bytesRemaining; int blkIndex; for(bytesRemaining = length; (double)bytesRemaining / (double)splitSize > 1.1D; bytesRemaining -= splitSize) { blkIndex = this.getBlockIndex(blkLocations, length - bytesRemaining); splits.add(this.makeSplit(path, length - bytesRemaining, splitSize, blkLocations[blkIndex].getHosts(), blkLocations[blkIndex].getCachedHosts())); } if (bytesRemaining != 0L) { blkIndex = this.getBlockIndex(blkLocations, length - bytesRemaining); splits.add(this.makeSplit(path, length - bytesRemaining, bytesRemaining, blkLocations[blkIndex].getHosts(), blkLocations[blkIndex].getCachedHosts())); } } else { if (LOG.isDebugEnabled() && length > Math.min(file.getBlockSize(), minSize)) { LOG.debug("File is not splittable so no parallelization is possible: " + file.getPath()); } splits.add(this.makeSplit(path, 0L, length, blkLocations[0].getHosts(), blkLocations[0].getCachedHosts())); } } else { splits.add(this.makeSplit(path, 0L, length, new String[0])); } }切片机制
- 提交切片规划文件到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数
```java
public List
- 简单地按照文件的内容长度进行切片
- 切片大小默认等于Block大小
- 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
- 举例,两个输入文件:
- file1.txt 320M
- file2.txt 10M
=>切片
- file1.txt.split1— 0~128
- file1.txt.split2— 128~256
- file1.txt.split3— 256~320
- file2.txt.split1— 0~10M
TextInputFormat
FileInputFormat实现类
FileInputFormat常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定义InputFormat等。TextInputFormat
TextInputFormat是默认的FileInputFormat实现类,按行读取每条记录。
键——存储该行在整个文件中的起始字节偏移量(LongWritable类)
值——这行的内容,不包括任何行终止符,即换行和回车(Text类型)CombineTextInputFormat切片机制
使用场景
框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。
用于小文件过多的场景,将多个小文件从逻辑上规划到一个切片中,多个小文件交给一个MapTask处理。虚拟存储切片最大值设置
```java // 如果不设置InputFormat,默认用TextInputFormat.class job.setInputFormatClass(CombineTextInputFormat.class);
// 虚拟存储切片最大值设置为4M CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);
<a name="HwpK0"></a>
#### 切片机制
1. 虚拟存储过程
将输入目录下所有文件大小依次和设置的setMaxInputSpliteSize值比较:
- 如果不大于设置的最大值,逻辑上划分一个块
- 如果输入文件大于设置的最大值且大于两倍,以最大值切割一块
- 当剩余数据大小超过设置的最大值且不大于最大值2倍,将文件均分为2个虚拟存储块(防止出现太小切片)
2. 切片过程
1. 判断学你存储的文件大小是否大于setMaxInputSpliteSize值,大于等于则单独形成一个切片
1. 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片

<a name="ZyMcW"></a>
#### 案例
1. 需求
将输入的小文件合并成一个切片统一处理<br />输入数据:<br /><br />期望:一个切片处理4个文件
2. 实现过程
1. 不做任何处理,运行WordCount,切片个数为4

2. 虚拟存储切片最大值设为4M
```java
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
// 虚拟存储切片最大值设置4m
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);

- 虚拟存储切片最大值设为20M
// 如果不设置InputFormat,它默认用的是TextInputFormat.class job.setInputFormatClass(CombineTextInputFormat.class); // 虚拟存储切片最大值设置20m CombineTextInputFormat.setMaxInputSplitSize(job, 20971520);
CombineTextInputFormat导入 org.apache.hadoop.mapreduce.lib.input.CombineTextInputFormat;
2. MapReduce工作流程


Shuffle过程(7-16):
- MapTask收集map()方法输出的kv对,放到内存缓冲区中
- 从内存缓冲区不断移除本地磁盘文件,可能会溢出多个文件
- 多个溢出文件会被合并成大的溢出文件
- 在溢出过程及合并的过程中,都要调用Partitioner进行分区和针对key进行排序
- ReduceTask根据自己的分区号,去各个MapTask机器上取相应的结果分区数据
- ReduceTask会抓取到同一个分区的来自不同MapTask的结果文件,将这些文件再进行合并(归并排序)
- 合并成大文件后,Shuffle的过程结束
- Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上,缓冲区越大,磁盘io的次数越少,执行速度越快
- 缓冲区大小可以通过参数mapreduce.task.io.sort.mb调整,默认100M
3. Shuffle机制
Shuffle机制
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle
Partition分区
需求:将统计结果按照条件输出到不同文件中(分区)
默认Partitioner分区:
public class HashPartitioner<K, V> extends Partitioner<K, V> {
public int getPartition(K key, V value, int numReduceTasks) {
return (key.hashCode() & 2147483647) % numReduceTasks;
}
}
自定义Partitioner步骤:
(1)自定义类继承Partitioner,重写getPartiton()方法
public class CustomPartitioner extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int i) {
// 分区代码逻辑
return partition;
}
}
(2)在Job驱动中,设置自定义Partitioner
job.setPartitionerClass(CustomPartitioner.class);
(3)自定义Partition后,要根据自定义Partitioner的逻辑设置相应数量的ReduceTask
job.setNumReduceTasks(5);
总结:
(1)如果ReduceTask的数量>getPartitioner的结果数,则会多产生几个空的输出文件part-r-000xx;
(2)如果1
(4)分区号必须从0开始累加
Partition分区案例
按照手机号136、137、139、139和其他开头分别放到5个独立的文件中
import com.example.mapreduce.writable.FlowBean;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
// Partitioner<Text, FlowBean>是map输出的k、v类型
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int i) {
// text:是手机号
String phone = text.toString();
String prePhone = phone.substring(0, 3);
int partition;
if("136".equals(prePhone)) {
partition = 0;
} else if ("137".equals(prePhone)) {
partition = 1;
} else if("138".equals(prePhone)) {
partition = 2;
} else if("139".equals(prePhone)) {
partition = 3;
} else {
partition = 4;
}
return partition;
}
}
public class FlowDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
// 1. 获取job
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 2. 设置jar
job.setJarByClass(FlowDriver.class);
// 3. 关联mapper和reducer
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
// 4. 设置mapper 输出key和value类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
// 5. 设置最终数据输出的key和value类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
// 指定自定义分区器
job.setPartitionerClass(ProvincePartitioner.class);
// 指定相应数量的ReduceTask
job.setNumReduceTasks(5);
// 6. 设置数据的输入路径和输出路径
FileInputFormat.setInputPaths(job, new Path("C:\\Users\\ace\\Desktop\\input111"));
FileOutputFormat.setOutputPath(job, new Path("C:\\Users\\ace\\Desktop\\output222"));
// 7. 提交job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
WritableComparable排序
MapTask和ReduceTask都会对数据按照key进行排序,默认按照字典顺序排序,实现方法是快速排序,是Hadoop的默认行为,不管逻辑上是否需要。
- 对于MapTask,会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上。当数据处理完毕后,会对磁盘上所有文件进行归并排序
- 对于ReduceTask,从每个MapTask上远程拷贝相应的数据文件
- 如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中
- 如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件
- 如果内存中文件大小或者数目超过一定阈值,进行一次合并后将数据溢写到磁盘上
- 当所有数据拷贝完毕后,ReduceTask同意对内存和磁盘上的所有数据进行一次归并排序
排序分类
(1)部分排序:MapReduce根据输入记录的键对数据集排序,保证输出的每个文件内部有序
(2)全排序:最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask,但大文件效率低
(3)辅助排序(GroupingComparator分组):在Reduce端对key进行分组,应用于在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序
(4)二次排序:在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序
WritableComparable排序案例实操(全排序)
需求:
- 输入数据
自定义序列化https://www.yuque.com/quantianbuhuoyue/bvs7lm/gv9682#ozpLD
处理的数据
期望输出:对总流量进行倒序排序
// 实现WritableComparable接口重写compareTo方法 public class FlowBean implements WritableComparable<FlowBean> { private long upFlow; // 上行流量 private long downFlow; // 下行流量 private long sumFlow; // 总流量 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; } // 空参构造 public FlowBean() { } @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(); } @Override public String toString() { return upFlow + "\t" + downFlow + "\t" + sumFlow; } @Override public int compareTo(FlowBean o) { // 总流量的倒叙排序 if(this.sumFlow > o.sumFlow) { return -1; } else if(this.sumFlow < o.sumFlow) { return 1; } else { return 0; } } }public class FlowMapper extends Mapper<LongWritable, Text, FlowBean, Text> { private FlowBean outK = new FlowBean(); private Text outV = new Text(); @Override protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, FlowBean, Text>.Context context) throws IOException, InterruptedException { // 获取一行 String line = value.toString(); // 切割 String[] split = line.split("\t"); // 封装 outV.set(split[0]); outK.setUpFlow(Long.parseLong(split[1])); outK.setDownFlow(Long.parseLong(split[2])); outK.setSumFlow(); // 写出 context.write(outK, outV); } }public class FlowReducer extends Reducer<FlowBean, Text, Text, FlowBean> { @Override protected void reduce(FlowBean key, Iterable<Text> values, Reducer<FlowBean, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException { for (Text value : values) { context.write(value, key); } } }public class FlowDriver { public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException { // 1. 获取job Configuration conf = new Configuration(); Job job = Job.getInstance(conf); // 2. 设置jar job.setJarByClass(FlowDriver.class); // 3. 关联mapper和reducer job.setMapperClass(FlowMapper.class); job.setReducerClass(FlowReducer.class); // 4. 设置mapper 输出key和value类型 job.setMapOutputKeyClass(FlowBean.class); job.setMapOutputValueClass(Text.class); // 5. 设置最终数据输出的key和value类型 job.setOutputKeyClass(Text.class); job.setOutputValueClass(FlowBean.class); // 6. 设置数据的输入路径和输出路径 FileInputFormat.setInputPaths(job, new Path("C:\\Users\\ace\\Desktop\\input222")); FileOutputFormat.setOutputPath(job, new Path("C:\\Users\\ace\\Desktop\\output2222")); // 7. 提交job boolean result = job.waitForCompletion(true); System.exit(result ? 0 : 1); } }WritableComparable排序案例实操(二次排序)
@Override public int compareTo(FlowBean o) { // 总流量的倒叙排序 if (this.sumFlow > o.sumFlow) { return -1; } else if (this.sumFlow < o.sumFlow) { return 1; } else { // 按照上行流量的正序排 if (this.upFlow > o.upFlow) { return 1; } else if (this.upFlow < o.upFlow) { return -1; } else { return 0; } } }WritableComparable排序案例实操(区内排序)
排序+分区
public class ProvincePartitioner extends Partitioner<FlowBean, Text> { @Override public int getPartition(FlowBean flowBean, Text text, int i) { String phone = text.toString(); String prePhone = phone.substring(0, 3); if ("136".equals(prePhone)) { return 0; } else if ("137".equals(prePhone)) { return 1; } else if ("138".equals(prePhone)) { return 2; } else if ("139".equals(prePhone)) { return 3; } else { return 4; } } }job.setPartitionerClass(ProvincePartitioner.class); job.setNumReduceTasks(5);Combiner合并
Combiner是MR程序中Mapper和Reducer之外的一种组件
- Combiner组件的父类就是Reducer
- Combiner和Reducer的区别在于运行的位置:
- Combiner是在每一个MapTask所在的节点运行
- Reducer是接收全局所有Mapper的输出结果
- Combiner对每一个MapTask的输出进行局部汇总,以减小网络传输量
Combiner能够应用的前提是不能影响最终的业务逻辑以及输出的kv应该跟Reducer的输入kv类型要对应起来
Combiner合并案例
需求:统计过程中对每一个MapTask的输出进行局部汇总,以减小网络传输量即采用Combiner功能
实现方案一:增加一个WordcountCombiner类继承Reducer
public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> { private IntWritable outV = new IntWritable(); @Override protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException { int sum = 0; for (IntWritable value : values) { sum += value.get(); } outV.set(sum); context.write(key, outV); } }job.setCombinerClass(WordCountCombiner.class);
方案二:将Reducer作为Combiner
job.setCombinerClass(WordCountReducer.class);如果没有Reduce阶段,Shuffle直接不执行 job.setNumReduceTasks(0);

4. OutputFormat数据输出
OutputFormat接口实现类
OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了OutputFormat接口,默认输出格式TextOutputFormat
自定义OutputFormat
应用场景
输出数据到MySQL/HBase/Elasticsearch等存储框架中
步骤
自定义一个类继承FileOutputFormat
改写RecordWriter,改写输出数据的方法write()
需求
过滤输入的log,包含atguigu的网站输出到athuigu.log,其他输出到other.log
编码
(1)编写LogMapper类
public class LogMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
// map阶段不做处理
context.write(value, NullWritable.get());
}
}
(2)编写LogReducer类
public class LogReducer extends Reducer<Text, NullWritable, Text, NullWritable> {
@Override
protected void reduce(Text key, Iterable<NullWritable> values, Reducer<Text, NullWritable, Text, NullWritable>.Context context) throws IOException, InterruptedException {
for (NullWritable value : values) {
context.write(key, NullWritable.get());
}
}
}
(3)自定义LogOutputFormat类
创建一个类LogRecordWriter继承RecordWriter,创建两个输入流
public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
LogRecordWriter lrw = new LogRecordWriter(taskAttemptContext);
return lrw;
}
}
public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
private FSDataOutputStream atOut;
private FSDataOutputStream otherOut;
public LogRecordWriter(TaskAttemptContext taskAttemptContext) {
// 创建两条流
try {
FileSystem fs = FileSystem.get(taskAttemptContext.getConfiguration());
atOut = fs.create(new Path("C:\\Users\\ace\\Desktop\\out\\atguigu.log"));
otherOut = fs.create(new Path("C:\\Users\\ace\\Desktop\\out\\other.log"));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void write(Text text, NullWritable nullWritable) throws IOException, InterruptedException {
String log = text.toString();
// 具体写
if(log.contains("atguigu")) {
atOut.writeBytes(log + "\n");
} else {
otherOut.writeBytes(log + "\n");
}
}
@Override
public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
// 关流
IOUtils.closeStream(atOut);
IOUtils.closeStream(otherOut);
}
}
(4)编写LogDriver类
public class LogDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
// 1. 获取job
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 2. 设置jar包路径
job.setJarByClass(LogDriver.class);
// 3. 关联mapper和reducer
job.setMapperClass(LogMapper.class);
job.setReducerClass(LogReducer.class);
// 4. 设置map输出的kv类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
// 5. 设置最终输出的kv类型
job.setOutputFormatClass(LogOutputFormat.class);
// 6. 设置输入路径和输出路径
FileInputFormat.setInputPaths(job, new Path("C:\\Users\\ace\\Desktop\\input"));
// 自定义outputformat继承自fileoutputformat,要输出一个_SUCCESS文件,要指定一个输出目录
FileOutputFormat.setOutputPath(job, new Path("C:\\Users\\ace\\Desktop\\out"));
// 7. 提交job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
5. MapReduce内核源码解析
MapTask工作机制

(1)Read阶段:MapTask通过InputFormat获得RecordReader,从输入InputSplit中解析出key-value
(2)Map阶段:该节点主要是将解析出的key-value交给用户编写的map()函数处理,并产生一系列新的key-value
(3)Collect阶段:在用户编写map()函数中。数据处理完成后,一般调用OutputCollector.collect()输出结果。在该函数内部,会将生成的key-value分区(调用Paetitioner),并写入一个环形内存缓冲区中
(4)Spill阶段:溢写,当环形缓冲区满后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。将数据写入本地磁盘之前,先要对数据进行一次本地排序,并可以对数据合并、压缩
- 利用快速排序对缓冲区内的数据进行排序,先按照分区编号Partitioner进行排序,然后按照key进行排序,经过排序后,数据以分区为单位聚集在一起,同一分区内所有数据按照key有序
- 按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作
- 将分区数据的元信息写到内存索引数据结构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最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。
ReduceTask工作机制

(1)Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
(2)Sort阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。
(3)Reduce阶段:reduce()函数将计算结果写到HDFS上。
ReduceTask并行度决定机制
设置ReduceTask并行度(个数)
job.setNumReduceTasks(4); // 默认是1
并行度机制
- ReduceTask=0,表示没有Reduce阶段,输出文件个数和Map个数一致
- ReduceTask默认值是1,输出文件个数为1
- 如果数据分布不均匀,可能在Reduce阶段产生数据倾斜
- ReduceTask数量不是任意设置,要考虑业务逻辑需求,如果需要计算全局汇总结果,就只能有一个ReduceTask
- 具体多少个ReduceTask,需要根据集群性能决定(实验)
- 如果分区数不是1,但是ReduceTask为1,是否执行分区过程?
不执行,MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1,不大于1不执行
6. Join多种应用
Reduce Join
Map端的主要工作
为来自不同表或文件的key-value对打标签以区别不同来源的记录,然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出。
Reduce端的主要工作
在Reduce端以连接字段作为key的分组以及完成,只需要在每一个分组当中将来源于不同文件的记录分开,最后进行合并。
实例
需求
order.txt | id | pid | amount | | —- | —- | —- | | 1001 | 01 | 1 | | 1002 | 02 | 2 | | 1003 | 03 | 3 | | 1004 | 01 | 4 | | 1005 | 02 | 5 | | 1006 | 03 | 6 |
pd.txt | pid | pname | | —- | —- | | 01 | 小米 | | 02 | 华为 | | 03 | 格力 |
根据pid合并:
| id | pname | amount |
|---|---|---|
| 1001 | 小米 | 1 |
| 1004 | 小米 | 4 |
| 1002 | 华为 | 2 |
| 1005 | 华为 | 5 |
| 1003 | 格力 | 3 |
| 1006 | 格力 | 6 |
需求分析
通过将关联条件作为Map输出的key,将两表满足Join条件的数据并携带数据所来源的文件信息,发往同一个ReduceTask,在Reduce中进行数据的串联
代码实现
public class TableBean implements Writable {
private String id; // 订单id
private String pid; // 商品id
private int amount; // 商品数量
private String pname; // 商品名称
private String flag; // 标记数据来源于哪个表
public TableBean() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPid() {
return pid;
}
public void setPid(String pid) {
this.pid = pid;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
public String getFlag() {
return flag;
}
public void setFlag(String flag) {
this.flag = flag;
}
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(id);
dataOutput.writeUTF(pid);
dataOutput.writeInt(amount);
dataOutput.writeUTF(pname);
dataOutput.writeUTF(flag);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.id = dataInput.readUTF();
this.pid = dataInput.readUTF();
this.amount = dataInput.readInt();
this.pname = dataInput.readUTF();
this.flag = dataInput.readUTF();
}
@Override
public String toString() {
return id + '\t' + pname + '\t' +amount;
}
}
public class TableMapper extends Mapper<LongWritable, Text, Text, TableBean> {
private String fileName;
private Text outK = new Text();
private TableBean outV = new TableBean();
@Override
protected void setup(Mapper<LongWritable, Text, Text, TableBean>.Context context) throws IOException, InterruptedException {
// 初始化 order pd
FileSplit split = (FileSplit) context.getInputSplit();
fileName = split.getPath().getName();
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, TableBean>.Context context) throws IOException, InterruptedException {
// 获取一行
String line = value.toString();
// 判断是哪个文件
if(fileName.contains("order")) {
// 处理的是订单表
String[] split = line.split("\t");
// 封装k v
outK.set(split[1]);
outV.setId(split[0]);
outV.setPid(split[1]);
outV.setAmount(Integer.parseInt(split[2]));
outV.setPname("");
outV.setFlag("order");
} else {
// 处理的是商品表
String[] split = line.split("\t");
// 封装k v
outK.set(split[0]);
outV.setId("");
outV.setPid(split[0]);
outV.setAmount(0);
outV.setPname(split[1]);
outV.setFlag("pd");
}
// 写出
context.write(outK, outV);
}
}
public class TableReducer extends Reducer<Text, TableBean, TableBean, NullWritable> {
@Override
protected void reduce(Text key, Iterable<TableBean> values, Reducer<Text, TableBean, TableBean, NullWritable>.Context context) throws IOException, InterruptedException {
ArrayList<TableBean> orderBeans = new ArrayList<>();
TableBean pdBean = new TableBean();
for (TableBean value : values) {
if ("order".equals(value.getFlag())) {
TableBean tmptableBean = new TableBean();
try {
BeanUtils.copyProperties(tmptableBean, value);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
orderBeans.add(tmptableBean);
} else {
try {
BeanUtils.copyProperties(pdBean, value);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
// 循环遍历orderBeans,复制pdName
for (TableBean orderBean : orderBeans) {
orderBean.setPname(pdBean.getPname());
context.write(orderBean, NullWritable.get());
}
}
}
public class TableDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Job job = Job.getInstance(new Configuration());
job.setJarByClass(TableDriver.class);
job.setMapperClass(TableMapper.class);
job.setReducerClass(TableReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(TableBean.class);
job.setOutputKeyClass(TableBean.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job, new Path("C:\\Users\\ace\\Desktop\\input"));
FileOutputFormat.setOutputPath(job, new Path("C:\\Users\\ace\\Desktop\\output"));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}

总结
- 缺点:这种方式中,合并的操作是在Reduce阶段完成,Reduce端的处理压力太大,Map节点的运算负载则很低,资源利用率不高,且在Reduce阶段极易产生数据倾斜。
-
7. 数据清洗(ETL)
是Extract-Transform-Load的缩写,用来描述将数据从来源端经过抽取、转换、加载至目的端的过程。
清洗过程往往只需要运行Mapper程序,不需要运行Reduce程序
需求
去除日志中字段个数小于等于11的字段
程序
在Map阶段对输入的数据根据规则进行过滤清洗public class WebLogDriver { public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException { args = new String[] { "C:\\Users\\ace\\Desktop\\input", "C:\\Users\\ace\\Desktop\\output" }; // 1 获取job信息 Configuration conf = new Configuration(); Job job = Job.getInstance(conf); // 2 加载jar包 job.setJarByClass(LogDriver.class); // 3 关联map job.setMapperClass(WebLogMapper.class); // 4 设置最终输出类型 job.setOutputKeyClass(Text.class); job.setOutputValueClass(NullWritable.class); // 设置reducetask个数为0 job.setNumReduceTasks(0); // 5 设置输入和输出路径 FileInputFormat.setInputPaths(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); // 6 提交 boolean b = job.waitForCompletion(true); System.exit(b ? 0 : 1); } }public class WebLogMapper extends Mapper<LongWritable, Text, Text, NullWritable> { @Override protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException { // 获取一行 String line = value.toString(); // ETL boolean result = parseLog(line, context); if(!result) { return; } // 写出 context.write(value, NullWritable.get()); } private boolean parseLog(String line, Context context) { // 切割 String[] fields = line.split(" "); // 判断日志长度是否大于11 if(fields.length > 11) { return true; } else { return false; } } }Hadoop数据压缩
1. 概述
优点:减少磁盘IO、减少磁盘存储空间,IO密集型Job,多用压缩
缺点:增加CPU开销,运算密集型Job,少用压缩2. MR支持的压缩编码
压缩算法对比介绍
| 压缩格式 | Hadoop自带? | 算法 | 文件扩展名 | 是否可切片 | 换成压缩格式后,原来的程序是否需要修改 | 优缺点 | | —- | —- | —- | —- | —- | —- | —- | | DEFLATE | 是,直接使用 | DEFLATE | .deflate | 否 | 和文本处理一样,不需要修改 | | | Gzip | 是,直接使用 | DEFLATE | .gz | 否 | 和文本处理一样,不需要修改 | 优点:压缩率比较高;
缺点:不支持Split;压缩/解压速度一般; | | bzip2 | 是,直接使用 | bzip2 | .bz2 | 是 | 和文本处理一样,不需要修改 | 优点:压缩率高;支持Split;
缺点:压缩/解压速度慢。 | | LZO | 否,需要安装 | LZO | .lzo | 是 | 需要建索引,还需要指定输入格式 | 优点:压缩/解压速度比较快;支持Split;
缺点:压缩率一般;想支持切片需要额外创建索引。 | | Snappy | 是,直接使用 | Snappy | .snappy | 否 | 和文本处理一样,不需要修改 | 优点:压缩和解压缩速度快;
缺点:不支持Split;压缩率一般; |
3. 压缩方式选择
压缩位置选择
可以在MapReduce作用的任意阶段
——>Map——>Reduce——>
| 输入端采用压缩 | 无需显示指定使用的编解码方式,Hadoop自动检查文件扩展名,如果扩展名能够匹配,就会采用恰当的编解码方式对文件进行压缩和解压 | - 数据量小于块大小,重点考虑压缩和解压缩速度比较快的LZO/Snappy - 数据量非常大,重点考虑支持切片的Bzip2和LZO |
|---|---|---|
| Mapper输出采用压缩 | 为了减少MapTask和ReduceTask之间的网络IO | 考虑压缩和解压缩快的LZO、Snappy |
| Reducer输出采用压缩 | - 数据永久保存,考虑压缩率比较高的Bzip2和Gzip - 作为下一个MapReduce输入,需要考虑数据量和是否支持切片 |
4. 压缩参数配置
为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器 | 压缩格式 | 对应的编码/解码器 | | —- | —- | | DEFLATE | org.apache.hadoop.io.compress.DefaultCodec | | gzip | org.apache.hadoop.io.compress.GzipCodec | | bzip2 | org.apache.hadoop.io.compress.BZip2Codec | | LZO | com.hadoop.compression.lzo.LzopCodec | | Snappy | org.apache.hadoop.io.compress.SnappyCodec |
在Hadoop中启用压缩,配置参数 | 阶段 | 参数 | 默认值 | 建议 | | —- | —- | —- | —- | | 输入压缩 | io.compression.codecs
(在core-site.xml中配置) | 无,这个需要在命令行输入hadoop checknative查看 | Hadoop使用文件扩展名判断是否支持某种编解码器 | | mapper输出 | mapreduce.map.output.compress
(在mapred-site.xml中配置) | false | 这个参数设为true启用压缩 | | | mapreduce.map.output.compress.codec
(在mapred-site.xml中配置) | org.apache.hadoop.io.compress.DefaultCodec | 企业多使用LZO或Snappy编解码器在此阶段压缩数据 | | reducer输出 | mapreduce.output.fileoutputformat.compress
(在mapred-site.xml中配置) | false | 这个参数设为true启用压缩 | | | mapreduce.output.fileoutputformat.compress.codec
(在mapred-site.xml中配置) | org.apache.hadoop.io.compress.DefaultCodec | 使用标准工具或者编解码器,如gzip和bzip2 |
5. 实例
Map输出端采用压缩
public class WordCountDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
// 1. 获取job
Configuration conf = new Configuration();
// 开启map端输出压缩
conf.setBoolean("mapreduce.map.output.compress", true);
//设置map端输出压缩方式
conf.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);
Job job = Job.getInstance(conf);
// 2. 设置jar包路径
job.setJarByClass(WordCountDriver.class);
// 3. 关联mapper和reducer
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
// 4. 设置map输出的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("C:\\Users\\ace\\Desktop\\input"));
FileOutputFormat.setOutputPath(job, new Path("C:\\Users\\ace\\Desktop\\output111"));
// 7. 提交job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
Reduce输出端采用压缩
public class WordCountDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
// 1. 获取job
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 2. 设置jar包路径
job.setJarByClass(WordCountDriver.class);
// 3. 关联mapper和reducer
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
// 4. 设置map输出的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("C:\\Users\\ace\\Desktop\\input"));
FileOutputFormat.setOutputPath(job, new Path("C:\\Users\\ace\\Desktop\\output111"));
// 设置reduce端输出压缩开启
FileOutputFormat.setCompressOutput(job, true);
// 设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
// 7. 提交job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}

