MapReduce概述
MapReduce定义
MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。
MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。
MapReduce优缺点
优点
- MapReduce易于编程
只需要简单实现接口,就可以完成一个分布式程序,程序可以部署到大量廉价的PC机器上运行。 - 良好的扩展性
当计算机资源不足的时候,可以通过简单的增加机器的方式来提高计算能力。 - 高容错性
MapReduce设计的初衷就是使程序能够部署在廉价的PC机器上,这就要求它具有很高的容错性。当一台机器出现故障的时候,任务会转移到另一个结点上计算,这是由Hadoop内部完成的。 - 适合PB级以上海量数据的离线处理
可以实现上千台服务器集群并发工作,提供数据处理能力。
缺点
- 不擅长实时计算
MapReduce无法像MySQL一样,在毫秒或者秒级内返回结果。 - 不擅长流式计算
流式计算的输入数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化,MapReduce自身的设计特点决定了数据源必须是静态的。 - 不擅长DAG(有向无环图)计算
多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。每个应用处理完,都会将结果输出到硬盘,造成大量IO操作,性能低下。
MapReduce核心思想
- 分布式计算分为两个阶段:Map和Reduce
- 第一阶段MapTask并发实例
- 第二阶段ReduceTask并发实例
- MapReduce模型只能包含一个Map阶段和一个Reduce阶段,如果有多个业务,就需要写多个MapReduce串行执行
MapReduce进程
一个完整的MapReduce程序在分布式运行时有三类实例进程:
MrAppMaster:负责整个程序的过程调度及状态协调
MapTask:负责Map阶段的整个数据处理流程
ReduceTask:负责Reduce阶段的整个数据处理流程
官方WordCount源码
package org.apache.hadoop.examples;
import java.io.IOException;
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.Reducer;
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 class TokenizerMapper
extends Mapper<Object, Text, Text, IntWritable>{
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
public void map(Object key, Text value, Context context
) throws IOException, InterruptedException {
StringTokenizer itr = new StringTokenizer(value.toString());
while (itr.hasMoreTokens()) {
word.set(itr.nextToken());
context.write(word, one);
}
}
}
public static class IntSumReducer
extends Reducer<Text,IntWritable,Text,IntWritable> {
private IntWritable result = new IntWritable();
public void reduce(Text key, Iterable<IntWritable> values,
Context context
) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
context.write(key, result);
}
}
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);
}
}
常用数据序列化类型
Java类型 | Hadoop Writable类型 |
---|---|
Boolean | BooleanWritable |
Byte | ByteWritable |
Int | IntWritable |
Float | FloatWritable |
Long | LongWritable |
Double | DoubleWritable |
String | Text |
Map | MapWritable |
Array | ArrayWritable |
Null | NullWritable |
只有String对应Text,其他的就是在基本类型后面加上“Writable”即可。 |
MapReduce编程规范
分为3个阶段:Mapper阶段,Reducer阶段,Driver阶段。
Mapper阶段:
- 用户自定义Mapper类继承父类
- Mapper的输入是K-V的形式,K-V可以自定义
- 将业务方法写在map()方法中
- Mapper的输出同样是K-V的形式,K-V可以自定义
- map()方法(MapTask进程)对每一个<K,V>调用一次
Reducer阶段:
- 用户自定义Reducer类继承父类
- Reducer输出类型对应Mapper的输入类型,是K-V的形式
- 将业务方法写在reduce()方法中
- reduce()方法(ReduceTask进程)对每一组相同K的<K,V>组调用一次
Driver阶段:
相当于YARN集群客户端,用于提交整个程序到YARN集群,提交的是封装了MapReduce程序相关运行参数的job对象
WordCount案例实操
按照MapReduce的编程规范,分别编写Mapper,Reducer,Driver。对一组输入数据做WordCount统计。
新建一个Maven项目,pom.xml添加如下依赖:
<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>
添加config.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类。
package com.demo.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;
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
Text text = new Text();
IntWritable intWritable = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, IntWritable>.Context context) throws IOException, InterruptedException {
String line = value.toString();// 获取一行
String[] words = line.split(" ");// 切割
for (String word : words) {// 输出
text.set(word);
context.write(text, intWritable);
}
}
}
编写Reducer类。
package com.demo.mapreduce.wordcount;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
int sum;
IntWritable intWritable = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
sum = 0;
for (IntWritable value : values) {// 累加求和
sum += value.get();
}
intWritable.set(sum);
context.write(key, intWritable);// 输出
}
}
编写Driver类。
package com.demo.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 {
Configuration configuration = new Configuration();// 获取配置信息
Job job = Job.getInstance(configuration);// 获取Job
job.setJarByClass(WordCountDriver.class);// 关联本Driver的jar
job.setMapperClass(WordCountMapper.class);// 关联Mapper的jar
job.setReducerClass(WordCountReducer.class);// 关联Reducer的jar
job.setMapOutputKeyClass(Text.class);// 设置Mapper输出的K类型
job.setMapOutputValueClass(IntWritable.class);// 设置Mapper输出的V类型
FileInputFormat.setInputPaths(job, new Path(args[0]));// 设置输入路径
FileOutputFormat.setOutputPath(job, new Path(args[1]));// 设置输出路径
System.exit(job.waitForCompletion(true) ? 0 : 1);// 提交Job
}
}
首先,我们先准备一个用于统计wordCount的输入文件input.txt。然后在idea的运行界面,将inputPath和outputPath通过Program arguments传进去,注意这里的outputPath必须是一个不存在的目录,如果目录存在会报错。运行之后,在outputPath中可以看到part-r-00000文件,用文本工具打开,就可以看到wordCount的结果了,到此,本地运行wordCount就完成了,下面对项目打包,再部署到集群环境进行测试。
在pom.xml里加入打包的插件,使用mvn package
命令打包。
<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>
打包后,可以看到demo-1.0-SNAPSHOT.jar和demo-1.0-SNAPSHOT-jar-with-dependencies.jar两个文件,它们的大小是不一样的,一个是带依赖的,一个是不带依赖的,这里我们选择demo-1.0-SNAPSHOT-jar-with-dependencies.jar上传到集群hadoop102结点的/opt/module/hadoop-3.1.3/目录下。访问hadoop102的HDFS目录,查看wcinput下的输入文件是否存在,wcoutput目录是否存在,准备好wcinput下的输入文件,删除wcoutput目录。启动集群并执行任务:
[root@hadoop102 hadoop-3.1.3]sbin/start-dfs.sh
[root@hadoop103 hadoop-3.1.3]$ sbin/start-yarn.sh
[root@hadoop102 hadoop-3.1.3]hadoop jar demo-1.0-SNAPSHOT-jar-with-dependencies.jar com.demo.mapreduce.wordcount.WordCountDriver /wcinput/word.txt /wcoutput
其中demo-1.0-SNAPSHOT-jar-with-dependencies.jar
是jar包的名字,com.demo.mapreduce.wordcount.WordCountDriver
是Driver类的名字,/wcinput/word.txt
是输入,/wcoutput
是输出。在执行同时,可以在hadoop103的任务目录看到任务的执行情况。
Hadoop序列化
序列化概述
序列化的意思是:把内存中的对象转换成字节序列(或其他数据传输协议),便于存储到磁盘(持久化)和网络传输。
反序列化的意思是:把收到的字节序列(或其他数据传输协议)或磁盘持久化的数据,转换成内存中的对象。
通常,对象只存在于内存中,断电就会丢失,为了能够保存或者传输这些对象,所以需要序列化操作。
Hadoop认为Java自带的序列化框架(Serializable)比较重,一个对象被序列化后,会携带很多额外的信息(校验信息、Header、继承体系等),不便于在网络中传输,所以Hadoop实现了自己的一套序列化(Writable)方案。
Hadoop序列化的特点:
- 紧凑:高效使用存储空间
- 快速:读写数据开销小
- 互操作:支持多语言交互
自定义Bean实现Writable序列化接口
常用的基本类型通常不能满足业务需求,所以需要自定义的Bean也可以实现序列化,需要满足下面的7个要求:
- 必须实现Writable接口
- 反序列化时候,需要反射调用空参构造函数,必须有一个空参构造
- 重写序列化方法
write()
- 重写反序列化方法
readFields()
- 反序列化的顺序要和序列化的顺序保持一致
- 如果希望把Bean对象的内容显示在文件中,需要重写
toString()
方法,把需要展示的字段输出出来 - 如果需要将自定义的Bean对象放在key上传输,还需要实现Comparable接口,因为MapReduce在Shuffle阶段需要根据key进行排序
序列化案例
现在有一个需求:给定一些数据,按照手机号统计每个手机号的上传总流量、下载总流量、总流量。数据是一行一行的,每一行包括手机号、上传流量、下载流量等信息。于是Map操作就是将这些信息根据手机号进行分组,Reduce操作就是将分组的数据求和。
FlowBean.java
package com.demo.mapreduce.writable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
@Getter// get方法
@Setter// set方法
@NoArgsConstructor// 无参构造器
@AllArgsConstructor// 全部参数构造器
public class FlowBean implements Writable {
private long upFlow;
private long downFlow;
private long sumFlow;
@Override
public String toString() {
return "FlowBean{" +
"upFlow=" + upFlow +
", downFlow=" + downFlow +
", sumFlow=" + sumFlow +
'}';
}
/**
* 序列化方法
*/
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeLong(upFlow);
dataOutput.writeLong(downFlow);
dataOutput.writeLong(sumFlow);
}
/**
* 反序列化方法
* 反序列化是按顺序取值,所以一定要按照序列化的顺序来read,才能拿到正确的值
*/
@Override
public void readFields(DataInput dataInput) throws IOException {
this.upFlow = dataInput.readLong();
this.downFlow = dataInput.readLong();
this.sumFlow = dataInput.readLong();
}
}
FlowMapper.java
package com.demo.mapreduce.writable;
import cn.hutool.core.convert.Convert;
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> {
Text text;
FlowBean flowBean;
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException {
String line = value.toString();// 读取一行
String[] values = line.split(" ");// 切割
String phone = values[0];// 取值
Long upFlow = Convert.toLong(values[1]);// 取值
Long downFlow = Convert.toLong(values[2]);// 取值
text = new Text(phone);// 封装K
flowBean = new FlowBean(upFlow, downFlow, upFlow + downFlow);// 封装V
context.write(text, flowBean);// 写出KV
}
}
FlowReducer.java
package com.demo.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> {
FlowBean flowBean;
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Reducer<Text, FlowBean, Text, FlowBean>.Context context) throws IOException, InterruptedException {
long totalUp = 0;// 初始化
long totalDown = 0;// 初始化
for (FlowBean flowBean : values) {// 同一个手机号码的数据进行累加
totalUp += flowBean.getUpFlow();
totalDown += flowBean.getDownFlow();
}
flowBean = new FlowBean(totalUp, totalDown, totalUp + totalDown);// 封装V
context.write(key, flowBean);// 写出KV
}
}
FlowDriver.java
package com.demo.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 {
Configuration configuration = new Configuration();// 获取配置信息
Job job = Job.getInstance(configuration);// 获取Job
job.setJarByClass(FlowDriver.class);// 关联本Driver的jar
job.setMapperClass(FlowMapper.class);// 关联Mapper的jar
job.setReducerClass(FlowReducer.class);// 关联Reducer的jar
job.setMapOutputKeyClass(Text.class);// 设置Mapper输出的K类型
job.setMapOutputValueClass(FlowBean.class);// 设置Mapper输出的V类型
FileInputFormat.setInputPaths(job, new Path(args[0]));// 设置输入路径
FileOutputFormat.setOutputPath(job, new Path(args[1]));// 设置输出路径
System.exit(job.waitForCompletion(true) ? 0 : 1);// 提交Job
}
}
MapReduce框架原理
MapReduce的会经历Input→Mapper→Reducer→Output这几个阶段。
InputFormat数据输入
切片与MapTask并行度决定机制
对于一个大的数据集,MapReduce会把数据进行切片,每个切片作为一个MapTask,于是切片数=MapTask数。
数据块:Block是HDFS物理存储的分块,数据块是HDFS存储数据单位。
数据切片:切片是逻辑上对输入数据进行分片,这些信息在磁盘上的存储方式不会改变,数据切片是MapReduce的计算输入数据的单位,一个切片对应一个MapTask。
- 一个Job的Map阶段并行度由客户端在提交Job时候的切片数决定
- MapReduce为每一个切片分配一个MapTask
- 默认情况下,切片大小=块大小=128MB
- 切片时,不考虑整体,而是以文件为单位进行切片,依次对每个文件进行切片
Job提交流程源码和切片源码详解
Job提交流程
job.waitForCompletion(true);// WordCountDriver.java中的job提交
public boolean waitForCompletion(boolean verbose) throws IOException, InterruptedException, ClassNotFoundException {
if (this.state == Job.JobState.DEFINE) {
this.submit();// 提交方法
}
if (verbose) {// 监控代码
this.monitorAndPrintJob();
} else {
int completionPollIntervalMillis = getCompletionPollInterval(this.cluster.getConf());
while(!this.isComplete()) {
try {
Thread.sleep((long)completionPollIntervalMillis);
} catch (InterruptedException var4) {
}
}
}
return this.isSuccessful();
}
public void submit() throws IOException, InterruptedException, ClassNotFoundException {
this.ensureState(Job.JobState.DEFINE);// 检查当前job的状态
this.setUseNewAPI();// 处理新旧API兼容问题
this.connect();// 链接客户端(可能是YARN客户端,有可能是本地客户端)
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 {
return submitter.submitJobInternal(Job.this, Job.this.cluster);// 提交job的核心代码
}
});
this.state = Job.JobState.RUNNING;// 标记job为运行中
LOG.info("The url to track the job: " + this.getTrackingURL());
}
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 {
// 通过job的configuration创建一个连接,new方法会调用initialize()方法
return new Cluster(Job.this.getConfiguration());
}
});
}
}
private void initialize(InetSocketAddress jobTrackAddr, Configuration conf) throws IOException {
this.initProviderList();// 初始化生产者List
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());
}
// 此时的providerList里有YarnClientProtocolProvider和LocalClientProtocolProvider
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;
}
}
JobStatus submitJobInternal(Job job, Cluster cluster) throws ClassNotFoundException, InterruptedException, IOException {
this.checkSpecs(job);// 检查job的输出路径
Configuration conf = job.getConfiguration();
addMRFrameworkToDistributedCache(conf);
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 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);// 提交目录配置,集群模式需要上传jar包,从这个方法上传
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);// MapTask数目
LOG.info("number of splits:" + maps);// split分片数目
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());// 提交job
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;
}
FileInputFormat切片源码解析
private int writeSplits(JobContext job, Path jobSubmitDir) throws IOException, InterruptedException, ClassNotFoundException {
JobConf jConf = (JobConf)job.getConfiguration();
int maps;
if (jConf.getUseNewMapper()) {
maps = this.writeNewSplits(job, jobSubmitDir);
} else {
maps = this.writeOldSplits(jConf, jobSubmitDir);
}
return maps;
}
private <T extends InputSplit> int writeNewSplits(JobContext job, Path jobSubmitDir) throws IOException, InterruptedException, ClassNotFoundException {
Configuration conf = job.getConfiguration();
InputFormat<?, ?> input = (InputFormat)ReflectionUtils.newInstance(job.getInputFormatClass(), conf);
List<InputSplit> splits = input.getSplits(job);// 切片方法
T[] array = (InputSplit[])((InputSplit[])splits.toArray(new InputSplit[splits.size()]));
Arrays.sort(array, new JobSubmitter.SplitComparator());
JobSplitWriter.createSplitFiles(jobSubmitDir, conf, jobSubmitDir.getFileSystem(conf), array);// 将分片信息写到临时文件中去
return array.length;
}
public List<InputSplit> getSplits(JobContext job) throws IOException {
StopWatch sw = (new StopWatch()).start();
long minSize = Math.max(this.getFormatMinSplitSize(), getMinSplitSize(job));
long maxSize = getMaxSplitSize(job);
List<InputSplit> splits = new ArrayList();
List<FileStatus> 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) {// 剩余内容呢÷分片大小的值,如果超过1.1,将剩余内容切一片
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]));
}
}
}
}
}
FileInputFormat
切片机制:
- 按照文件的内容长度进行切片
- 切片大小,默认等于blockSize
- 切片时候不考虑数据集整体,针对每一个文件进行切片
和切片有关的公式:
Math.max(minSize, Math.min(maxSize, blockSize));
mapreduce.input.fileinputformat.split.minsize = 1;// 默认值为1
mapreduce.input.fileinputformat.split.maxsize = Long.MAXValue;// 默认值Long.MAXValue
maxsize(切片最大值):如果调的比blockSize小,会让切片变小,等于配置的参数值
minsize(切片最小值):如果调的比blockSize大,会让切片变得比blockSize还大
String name = inputSplit.getPath().getName();// 获取切片文件名称
FileSplit inputSplit = (FileSplit)context.getInputSplit();// 根据文件类型获取切片信息
TextInputFormat
TextInputFormat是FileInputFormat的默认实现类,按行读取每条记录,键存储该行在文件中的起始字节偏移量,是LongWritable类型,值是这行的内容,不包含终止符,是Text类型。
CombineTextInputFormat
在实际的应用场景下,会碰到多个小文件的情况,此时,如果还按照默认的FileInputFormat来切割,会产生大量的MapTask,效率极其低下,使用CombineTextInputFormat可以解决这个问题,它可以将多个小文件从逻辑上划分到一个切片中,这样多个小文件就可以交给一个MapTask来处理了。
可以通过CombineTextInputFormat.setMatInputSplitSize(job, size);
方法来指定最大虚拟存储切片。
生成切片的过程分为:虚拟存储过程和切片过程两部分。
虚拟存储过程:
将输入目录下的所有文件依次与maxInputSplitSize
进行对比,如果不大于maxInputSplitSize
,从逻辑上划分为一块,如果大于maxInputSplitSize
且大于2 × maxInputSplitSize
,以maxInputSplitSize
切割为一块,比较剩余部分,当maxInputSplitSize
< 剩余部分大小 < 2 × maxInputSplitSize
的时候,将剩余部分平均切成2份。
切片过程:
判断虚拟存储文件大小是否大于maxInputSplitSize
,如果大于等于maxInputSplitSize
,单独形成一个切片。
如果不大于maxInputSplitSize
,则和下一个虚拟存储文件共同合成一个切片。
CombineTextInputFormat案例
回到WordCountDriver.java
,如果要采用CombineTextInputFormat,需要添加如下代码:
job.setInputFormatClass(CombineTextInputFormat.class);// 如果不设置InputFormat,默认采用TextInputFormat.class
CombineTextInputFormat.setMaxInputSplitSize(job, 4 * 1024 * 1024);// 虚拟存储切片最大值设置为4MB
MapReduce工作流程
- 加载待处理文本
- 对待处理文本进行分片
- 提交分片信息、数据信息、配置信息
- 计算出MapTask的数量
- 默认使用TextInputFormat进行读取
- 将读取到的内容通过Mapper的
map()
方法进行逻辑运算 - 将计算后的结果和索引输出到环形缓冲区,环形缓冲区分为两部分,一部分是索引,一部分是数据。这里在写数据的时候,写到80%后反向
- 进行分区和排序
- 将环形缓冲区中的数据写到文件中,分区内的数据是有序的
- 对文件执行Merge排序进行合并
- 所有MapTask任务完成后,启动相应数量的ReduceTask,告知ReduceTask处理数据范围(数据分区)
- ReduceTask拉取数据到本地磁盘
- 归并排序合并文件
- 每次读取一组数据
- 将相同key的合并为一组
- 默认使用TextOutputFormat进行输出,将结果写回到硬盘上
其中第7步到第16步是Shuffle流程,具体如下:
- MapTask收集我们的
map()
方法输出的kv对,放到内存缓冲区中 - 从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
- 多个溢出文件会被合并成大的溢出文件
- 在溢出过程及合并的过程中,都要调用Partitioner进行分区和针对key进行排序
- ReduceTask根据自己的分区号,去各个MapTask机器上取相应的结果分区数据
- ReduceTask会抓取到同一个分区的来自不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)
- 合并成大文件后,Shuffle的过程也就结束了,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对Group,调用用户自定义的
reduce()
方法)
注意:
- Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区 越大,磁盘io的次数越少,执行速度就越快
- 缓冲区的大小可以通过参数调整,参数:
mapreduce.task.io.sort.mb
默认100M
Shuffle机制
Shuffle机制
在Map方法之后,Reduce方法之前的数据处理过程叫Shuffle。
- map方法后,将kv写入环形缓冲区(默认100M),环形缓冲区分为两部分,一份存储索引,一部分存储数据
- 在写数据的时候,写到存储区域的80%的时候,反向写入,此时触发溢写,也就是将环形缓冲区内的数据写到磁盘
- 写入磁盘的时候进行快速排序
- 磁盘不同分区之间进行归并排序,形成一个有序的数据
- 将分区的内容进行转移,优先转移到内存,如果内存不够,溢出到磁盘进行Reduce操作
Partition分区
如果需要将结果输出到多个文件(分区)中,需要怎么控制呢?如果不做设置,默认只有一个分区文件,默认的分区是HashPartitioner,它是根据key的hashCode值对ReduceTasks个数取模得到,无法自定义控制key存储到哪个分区。
public class HashPartitioner<K, V> extends Partitioner<K, V> {
public HashPartitioner() {
}
public int getPartition(K key, V value, int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
自定义Partitioner步骤:
- 自定义分区类并继承Partitioner类实现
getPartition()
方法,在getPartition()
方法里,对key进行判断,返回不同的partition - 设置job对应的PartitionerClass:
job.setPartitionerClass(CustomPartitioner.class);
- 自定义Partitioner后,需要根据自定义Partitioner里的逻辑设置相应数量的ReduceTask数量:
job.setNumReduceTasks(5);
- 如果ReduceTask数量>
getPartition()
的结果数,会多产生几个空的输出文件part-r-000xx - 如果ReduceTask数量<
getPartition()
的结果数,有一部分数据无处安放,会报异常 - 如果ReduceTask数量=1,不管MapTask输出多少分区文件,这些分区文件都会交给一个ReduceTask来执行,最终也就只会产生一个结果文件
- 分区号必须从0开始,依次累加1
Partition分区案例实操
将Flow案例复制出来一份进行修改,Flow案例是统计手机号和流量的,新增一个CustomPartitioner.java类用于自定义分区。
package com.demo.mapreduce.partitioner;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
public class CustomPartitioner extends Partitioner<Text, FlowBean> {
/**
* 这里的text是手机号
* 根据手机号的最后一位是奇数还是偶数进行分区
* 结果会输出两个分区文件
*/
@Override
public int getPartition(Text text, FlowBean flowBean, int i) {
long value = Long.parseLong(text.toString());
return (int) (value % 2);
}
}
在FlowDriver.java里指定自定义分区类和ReduceTask的数量。
job.setPartitionerClass(CustomPartitioner.class);// 设置自定义分区类
job.setNumReduceTasks(2);// 设置ReduceTask的数量
WritableComparable排序
排序是MapReduce框架中一个重要操作。MapTask和ReduceTask都会对数据按照key进行排序,这是Hadoop的默认行为,所以Key必须comparable。默认的排序规则是按照字典序,使用的排序算法是快速排序。
对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。
对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中。如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
排序分类:
- 部分排序:MapReduce根据输入记录的键对数据集排序,保证输出的每个文件内部有序
- 全排序:最终输出只有一个文件,文件内部有序。只设置一个ReduceTask。但是这种方法在处理大型文件的时候,效率非常低
- 辅助排序(GroupingCompartor分组):在Reduce端对key进行分组。在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序
- 二次排序:在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。
bean对象作为key传输的时候,自定义排序是通过实现WritableComparable接口的compareTo()
实现的。
WritableComparable排序案例实操(全排序)
需求:对Flow案例产生的结果,对总流量进行倒序排列。
因为需要排序的字段在value里,所以我们需要互换key和value的位置,并自定义key的排序规则,注意下面的代码里,key和value的位置,和Flow案例对比,是互换的。
FlowBean.java
package com.demo.mapreduce.writableComparable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
@Getter// get方法
@Setter// set方法
@NoArgsConstructor// 无参构造器
@AllArgsConstructor// 全部参数构造器
// 实现WritableComparable的compareTo()方法就可以自定义排序了
public class FlowBean implements WritableComparable<FlowBean> {
private long upFlow;
private long downFlow;
private long sumFlow;
@Override
public String toString() {
return "FlowBean{" +
"upFlow=" + upFlow +
", downFlow=" + downFlow +
", sumFlow=" + sumFlow +
'}';
}
/**
* 序列化方法
*/
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeLong(upFlow);
dataOutput.writeLong(downFlow);
dataOutput.writeLong(sumFlow);
}
/**
* 反序列化方法
* 反序列化是按顺序取值,所以一定要按照序列化的顺序来read,才能拿到正确的值
*/
@Override
public void readFields(DataInput dataInput) throws IOException {
this.upFlow = dataInput.readLong();
this.downFlow = dataInput.readLong();
this.sumFlow = dataInput.readLong();
}
/**
* 自定义排序规则
* 按照FlowBean的sumFlow字段值倒序
*/
@Override
public int compareTo(FlowBean o) {
return Long.compare(o.getSumFlow(), this.getSumFlow());
}
}
FlowDriver.java
package com.demo.mapreduce.writableComparable;
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 {
Configuration configuration = new Configuration();// 获取配置信息
Job job = Job.getInstance(configuration);// 获取Job
job.setJarByClass(FlowDriver.class);// 关联本Driver的jar
job.setMapperClass(FlowMapper.class);// 关联Mapper的jar
job.setReducerClass(FlowReducer.class);// 关联Reducer的jar
job.setMapOutputKeyClass(FlowBean.class);// 设置Mapper输出的K类型
job.setMapOutputValueClass(Text.class);// 设置Mapper输出的V类型
FileInputFormat.setInputPaths(job, new Path(args[0]));// 设置输入路径
FileOutputFormat.setOutputPath(job, new Path(args[1]));// 设置输出路径
System.exit(job.waitForCompletion(true) ? 0 : 1);// 提交Job
}
}
FlowMapper.java
package com.demo.mapreduce.writableComparable;
import cn.hutool.core.convert.Convert;
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, FlowBean, Text> {
Text text;
FlowBean flowBean;
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, FlowBean, Text>.Context context) throws IOException, InterruptedException {
String line = value.toString();// 读取一行
String[] values = line.split(" ");// 切割
String phone = values[0];// 取值
Long upFlow = Convert.toLong(values[1]);// 取值
Long downFlow = Convert.toLong(values[2]);// 取值
flowBean = new FlowBean(upFlow, downFlow, upFlow + downFlow);// 封装K
text = new Text(phone);// 封装V
context.write(flowBean, text);// 写出KV
}
}
FlowReducer.java
package com.demo.mapreduce.writableComparable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
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 text : values) {
context.write(text, key);
}
}
}
如果总流量相同的情况下,需要根据上行流量或下行流量排序,这就是二次排序了,只需要在compareTo()
方法里写好排序依据即可。
WritableComparable排序案例实操(区内排序)
在上一个例子里,新增一个CustomPartitioner,注意这里的k-v,要和Map的返回值的k-v对应上。
package com.demo.mapreduce.partitioner;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
public class CustomPartitioner extends Partitioner<Text, FlowBean> {
/**
* 这里的text是手机号
* 根据手机号的最后一位是奇数还是偶数进行分区
* 结果会输出两个分区文件
*/
@Override
public int getPartition(Text text, FlowBean flowBean, int i) {
long value = Long.parseLong(text.toString());
return (int) (value % 2);
}
}
另外还需要在Driver里指定PartitionerClass和NumReduceTasks,就可以实现分区并且区内有序的效果了。
Combiner合并
Combiner是MapReduce程序中除了Mapper和Reducer之外的一种组件。Combiner的父类是Reducer。Combiner和Reducer的区别在于:Combiner运行在每一个MapTask节点上,Reducer运行在MapTask之后。Combiner存在的意义是:对每一个MapTask的输出进行局部汇总,这样可以减少网络传输。比如传递10000个(key,1)和1个(key,10000)相比,肯定是后者网络传输更小。但是Combiner并不适用于所有场景,Combiner的输出kv需要和Reducer的输入kv对应起来。
如果一个任务是求平均值,Mapper后Combiner进行合并,求得(3+5+7)/3=5,另一个求得(2+6)/2=4,不使用Combiner的时候,求得(3+5+7+2+6)/5=23/5≠(5+4)/2=9/2,因此Combiner并不适用于所有场景。
在wordcount的基础上,实现自定义Combiner。
package com.demo.mapreduce.wordcount;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class CustomCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
IntWritable 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();
}
intWritable = new IntWritable(sum);
context.write(key, intWritable);
}
}
在Job驱动类中,设置CombinerClass。
job.setCombinerClass(CustomCombiner.class);// 设置自定义Combiner类
通过运行日志可以看出来Combiner起作用了,可以观察到Combine input records
和Combine output records
的值不再是0了。
Combiner合并案例实操还是以wordcount为例。有两种方式
一种是自定义WordCountCombiner继承Reducer,在WordCountCombiner里对单词进行汇总,然后再交给Reducer处理。
另一种是将WordCountReducer作为Combiner配置在job的CombinerClass处。
仔细观察CustomCombiner.java
和WordCountReducer.java
,可以发现,它们两个的代码几乎是一样的,所以第二种方式更加简单。
OutputFormat数据输出
OutputFormat接口实现类
OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了OutputFormat接口,默认的输出是TextOutputFormat,除此之外,常见的还有FileOutputFormat和DBOutputFormat等。
我们可以自定义OutputFormat,重写write()
方法实现自定义输出数据。
自定义OutputFormat案例实操
这里有一些日志,我们需要对日志进行分类,并输出到不同的文件中,前面学的partition可以实现,这里再介绍一种方式。具体需求是:需要将日志中包含atguigu
关键字的输出到一个文件中,其余的输出到另一个文件中。
LogMapper.java
package com.demo.mapreduce.outputformat;
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;
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 {
context.write(value, NullWritable.get());
}
}
LogReducer.java
package com.demo.mapreduce.outputformat;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
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 {
// 因为key相同的数据会被放在value中,为了避免丢失数据,所以这里使用for循环遍历values,将每个数据都写出
for (NullWritable value : values) {
context.write(key, NullWritable.get());
}
}
}
CustomOutputFormat.java
package com.demo.mapreduce.outputformat;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class CustomOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
return new CustomRecordWriter(taskAttemptContext);
}
}
CustomRecordWriter.java
package com.demo.mapreduce.outputformat;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import java.io.IOException;
public class CustomRecordWriter extends RecordWriter<Text, NullWritable> {
private FSDataOutputStream atguiguFSDataOutputStream;
private FSDataOutputStream otherFSDataOutputStream;
public CustomRecordWriter(TaskAttemptContext taskAttemptContext) {
try {
// 获取文件系统对象
FileSystem fileSystem = FileSystem.get(taskAttemptContext.getConfiguration());
// 使用文件系统对象创建两个输出流
atguiguFSDataOutputStream = fileSystem.create(new Path("E:\\Hadoop\\output\\atguigu.log"));
otherFSDataOutputStream = fileSystem.create(new Path("E:\\Hadoop\\output\\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")) {
atguiguFSDataOutputStream.writeBytes(log + "\n");
} else {
otherFSDataOutputStream.writeBytes(log + "\n");
}
}
@Override
public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
IOUtils.closeStreams(atguiguFSDataOutputStream, otherFSDataOutputStream);
}
}
LogDriver.java
package com.demo.mapreduce.outputformat;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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 LogDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
job.setJarByClass(LogDriver.class);
job.setMapperClass(LogMapper.class);
job.setReducerClass(LogReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 设置自定义的OutputFormat类
job.setOutputFormatClass(CustomOutputFormat.class);
FileInputFormat.setInputPaths(job, new Path(args[0]));
// 虽然我们自定义了OutputFormat,但是因为我们的OutputFormat继承自FileOutputFormat
// 但是FileOutputFormat需要指定一个路径,用于输出一个_SUCCESS 文件,所以在这需要指定一个输出目录
FileOutputFormat.setOutputPath(job, new Path(args[1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
MapReduce内核源码解析
MapTask工作机制
MapTask的5个阶段:
- Read阶段:MapTask通过InputFormat获取RecordReader,从输入InputSplit解析出key-value
- Map阶段:将解析出的key-value交给用户编写的
map()
函数处理,此时会产生一些新的key-value - Collect阶段:在用户编写的
map()
函数中,数据处理完成后会调用OutputCollector.collect()
输出结果,函数内会将生成的key-value分区(调用Partitioner),写入环形缓冲区中 - Spill阶段:当环形缓冲区满了之后,MapReduce会将数据写到本地磁盘,生成一个临时文件,在写出之前会对数据进行一次本地排序,在必要的时候还会对数据进行合并、压缩等
- Merge阶段:当所有数据处理完成后,MapTask对所有临时文件进行一次合并, 以确保最终只会生成一个数据文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index。在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并mapreduce.task.io.sort.factor(默认 10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销
Spill阶段步骤:
- 利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排序,然后按照key进行排序。经过排序后,数据以分区为单位聚集在 一起,且同一分区内所有数据按照key有序
- 按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N 表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作
- 将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中
ReduceTask工作机制
- Copy阶段:ReduceTask从各个MapTask拉取一份数据,对于每份数据,如果大小超过阈值,写到磁盘,否则直接放到内存中
- Sort阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照MapReduce语义,用户编写
reduce()
函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可 - Reduce阶段:
reduce()
函数将计算结果写到HDFS上
ReduceTask并行度决定机制
MapTask的并行度由切片数决定,切片数由输入文件和切片规则决定。ReduceTask的并行度由setNumReduceTasks()
决定,默认值是1。但是并不是ReduceTask越多越好,ReduceTask的个数跟数据量有关系:设置过少的ReduceTask,并行效果不能表现出来,设置过多的ReduceTask,系统开销会非常大。
- ReduceTask=0表示没有Reduce阶段,输出文件个数和Map个数相同
- ReduceTask默认值为1,输出文件个数为1
- 如果数据分布不均匀,可能在Reduce阶段产生数据倾斜
- ReduceTask的数量并不是任意设置,要根据具体业务进行分析,如果需要计算全局汇总结果,只能设置一个ReduceTask
- ReduceTask需要根据集群的性能确定
- 如果分区数不是1,但是ReduceTask是1,是不执行分区过程的,在MapTask的源码中,执行分区之前会先判断ReduceNum是否大于1,如果大于1,执行分区,否则,使用内部类分区
org.apache.hadoop.mapred.MapTask.java
// 分别设置NumReduceTask的个数是1和大于1
// 以Debug模式启动,在Mapper里的context.write(key, value);上打断点,每次都执行Force Step Into,就可以看到效果了
private class NewOutputCollector<K, V> extends RecordWriter<K, V> {
private final MapOutputCollector<K, V> collector;
private final Partitioner<K, V> partitioner;
private final int partitions;
NewOutputCollector(JobContext jobContext, JobConf job, TaskUmbilicalProtocol umbilical, TaskReporter reporter) throws IOException, ClassNotFoundException {
this.collector = MapTask.this.createSortingCollector(job, reporter);
this.partitions = jobContext.getNumReduceTasks();
if (this.partitions > 1) {
this.partitioner = (Partitioner)ReflectionUtils.newInstance(jobContext.getPartitionerClass(), job);
} else {
this.partitioner = new Partitioner<K, V>() {
public int getPartition(K key, V value, int numPartitions) {
return NewOutputCollector.this.partitions - 1;
}
};
}
}
public void write(K key, V value) throws IOException, InterruptedException {
this.collector.collect(key, value, this.partitioner.getPartition(key, value, this.partitions));
}
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
try {
this.collector.flush();
} catch (ClassNotFoundException var3) {
throw new IOException("can't find class ", var3);
}
this.collector.close();
}
}
如果NumReduceTask是1,会执行上述代码的第13行,否则会执行第9行,也就是跳到我们自定义的Partitioner里。
MapTask & ReduceTask源码解析
我们选择带有partitioner的demo进行debug,在关键地方会写出汉字注释。
MapTask源码解析流程
context.write(text, flowBean);// 自定义Mapper的map()方法
// WrappedMapper.java
public void write(KEYOUT key, VALUEOUT value) throws IOException, InterruptedException {
mapContext.write(key, value);// write()方法
}
// TaskInputOutputContextImpl.java
public void write(KEYOUT key, VALUEOUT value) throws IOException, InterruptedException {
output.write(key, value);// write()方法
}
// MapTask.java
public void write(K key, V value) throws IOException, InterruptedException {
// 调用collect方法,收集key,value数据
// 注意这里的getPartition()方法,默认采用HashPartitioner,因为我们在driver里指定了CustomPartitioner,所以会走到自定义的Partitioner里,根据我们自定义的规则进行分区
collector.collect(key, value, partitioner.getPartition(key, value, partitions));
}
// MapTask.java
// 此方法执行完,一行记录的处理就完成了
public synchronized void collect(K key, V value, final int partition) throws IOException {
reporter.progress();
if (key.getClass() != keyClass) {// 校验key的类型
throw new IOException("Type mismatch in key from map: expected " + keyClass.getName() + ", received " + key.getClass().getName());
}
if (value.getClass() != valClass) {// 校验value的类型
throw new IOException("Type mismatch in value from map: expected " + valClass.getName() + ", received " + value.getClass().getName());
}
if (partition < 0 || partition >= partitions) {
throw new IOException("Illegal partition for " + key + " (" + partition + ")");
}
checkSpillException();
bufferRemaining -= METASIZE;
if (bufferRemaining <= 0) {
// start spill if the thread is not running and the soft limit has been reached
spillLock.lock();
try {
do {
if (!spillInProgress) {
// 计算数据k-v和索引k-v的位置
final int kvbidx = 4 * kvindex;
final int kvbend = 4 * kvend;
// serialized, unspilled bytes always lie between kvindex and bufindex, crossing the equator. Note that any void space created by a reset must be included in "used" bytes
final int bUsed = distanceTo(kvbidx, bufindex);
// 判断当前存储是否到达缓冲区的80%,softLimit = (int)(kvbuffer.length * spillper),其中spillper默认值是0.8
final boolean bufsoftlimit = bUsed >= softLimit;
if ((kvbend + METASIZE) % kvbuffer.length != equator - (equator % METASIZE)) {
// spill finished, reclaim space
resetSpill();
bufferRemaining = Math.min(distanceTo(bufindex, kvbidx) - 2 * METASIZE, softLimit - bUsed) - METASIZE;
continue;
} else if (bufsoftlimit && kvindex != kvend) {
// spill records, if any collected; check latter, as it may be possible for metadata alignment to hit spill pcnt
startSpill();// 开始溢写
final int avgRec = (int) (mapOutputByteCounter.getCounter() / mapOutputRecordCounter.getCounter());
// leave at least half the split buffer for serialization data ensure that kvindex >= bufindex
final int distkvi = distanceTo(bufindex, kvbidx);
final int newPos = (bufindex + Math.max(2 * METASIZE - 1, Math.min(distkvi / 2, distkvi / (METASIZE + avgRec) * METASIZE))) % kvbuffer.length;
setEquator(newPos);
bufmark = bufindex = newPos;
final int serBound = 4 * kvend;
// bytes remaining before the lock must be held and limits checked is the minimum of three arcs: the metadata space, the serialization space, and the soft limit
bufferRemaining = Math.min(
// metadata max
distanceTo(bufend, newPos),
Math.min(
// serialization max
distanceTo(newPos, serBound),
// soft limit
softLimit)) - 2 * METASIZE;
}
}
} while (false);
} finally {
spillLock.unlock();
}
}
try {
// serialize key bytes into buffer
// map和reduce可能在不同的机器上,所以k-v需要跨节点传输,分别对key-value进行序列化
int keystart = bufindex;
keySerializer.serialize(key);
if (bufindex < keystart) {
// wrapped the key; must make contiguous
bb.shiftBufferedKey();
keystart = 0;
}
// serialize value bytes into buffer
final int valstart = bufindex;
valSerializer.serialize(value);
// It's possible for records to have zero length, i.e. the serializer will perform no writes. To ensure that the boundary conditions are
// checked and that the kvindex invariant is maintained, perform a zero-length write into the buffer. The logic monitoring this could be
// moved into collect, but this is cleaner and inexpensive. For now, it is acceptable.
bb.write(b0, 0, 0);
// the record must be marked after the preceding write, as the metadata for this record are not yet written
int valend = bb.markRecord();
mapOutputRecordCounter.increment(1);
mapOutputByteCounter.increment(distanceTo(keystart, valend, bufvoid));
// write accounting info
kvmeta.put(kvindex + PARTITION, partition);
kvmeta.put(kvindex + KEYSTART, keystart);
kvmeta.put(kvindex + VALSTART, valstart);
kvmeta.put(kvindex + VALLEN, distanceTo(valstart, valend));
// advance kvindex
kvindex = (kvindex - NMETA + kvmeta.capacity()) % kvmeta.capacity();
} catch (MapTask.MapBufferTooSmallException e) {
LOG.info("Record too large for in-memory buffer: " + e.getMessage());
spillSingleRecord(key, value, partition);
mapOutputRecordCounter.increment(1);
return;
}
}
// Mapper.java
public void run(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
this.setup(context);
try {
while(context.nextKeyValue()) {
// 调用我们自己写的CustomMapper.java的map()方法
this.map(context.getCurrentKey(), context.getCurrentValue(), context);
}
} finally {// 当所有的数据都处理完,执行cleanup()方法
this.cleanup(context);
}
}
// MapTask.java的runNewMapper()方法
try {
input.initialize(split, mapperContext);
mapper.run(mapperContext);
mapPhase.complete();// 标记map阶段执行完成
setPhase(TaskStatus.Phase.SORT);
statusUpdate(umbilical);
input.close();
input = null;
output.close(mapperContext);// 执行collector.flush();将数据刷出去
output = null;
} finally {
closeQuietly(input);
closeQuietly(output, mapperContext);
}
// MapTask.java
public void close(TaskAttemptContext context) throws IOException,InterruptedException {
try {
collector.flush();// 执行flush()方法,此时会进行快排
} catch (ClassNotFoundException cnf) {
throw new IOException("can't find class ", cnf);
}
collector.close();// collector关闭,map阶段结束
}
// MapTask.java的flush()方法
sortAndSpill();// 排序和溢写
...
mergeParts();// 对多个溢写文件进行归并排序合并,归并结束后,会产生两个文件,一个是数据文件,一个是索引文件,后面reduce拉取文件的时候,根据索引文件从数据文件中拉锯数据
ReduceTask源码解析流程
// ReduceTask.java的run()方法
if (isMapOrReduce()) {
copyPhase = getProgress().addPhase("copy");// 标记reduce阶段的copy流程
sortPhase = getProgress().addPhase("sort");// 标记reduce阶段的sort流程
reducePhase = getProgress().addPhase("reduce");// 标记reduce阶段的reduce流程
}
// start thread that will handle communication with parent
TaskReporter reporter = startReporter(umbilical);
boolean useNewApi = job.getUseNewReducer();
initialize(job, getJobID(), reporter, useNewApi);// 初始化工作,主要关注OutputFormat的初始化即可,默认的OutputFormat是TextOutputFormat
...
ShuffleConsumerPlugin.Context shuffleContext =
new ShuffleConsumerPlugin.Context(getTaskID(), job, FileSystem.getLocal(job), umbilical,
super.lDirAlloc, reporter, codec,
combinerClass, combineCollector,
spilledRecordsCounter, reduceCombineInputCounter,
shuffledMapsCounter,
reduceShuffleBytes, failedShuffleCounter,
mergedMapOutputsCounter,
taskStatus, copyPhase, sortPhase, this,
mapOutputFile, localMapFiles);
// 初始化,会创建一个ShuffleScheduler的实现类和MergeManager的实现类,其中MergeManager实现类初始化的时候,会调用createInMemoryMerger()和new OnDiskMerger(this)方法准备内存区域和磁盘区域,用于存放拉取过来的数据信息
shuffleConsumerPlugin.init(shuffleContext);
// 开始抓取数据方法,run()方法执行完毕后,copy阶段完成,开启sort阶段
rIter = shuffleConsumerPlugin.run();
// free up the data structures
mapOutputFilesOnDisk.clear();
// 标记sort阶段完成
sortPhase.complete(); // sort is complete
// 标记进入reduce阶段
setPhase(TaskStatus.Phase.REDUCE);
statusUpdate(umbilical);
Class keyClass = job.getMapOutputKeyClass();
Class valueClass = job.getMapOutputValueClass();
RawComparator comparator = job.getOutputValueGroupingComparator();
if (useNewApi) {
// 调用runNewReducer()方法,方法里会调用reducer.run(reducerContext);方法
// Reducer.java的run()方法会调用我们自定义的CustomReducer.java的reduce()方法
runNewReducer(job, umbilical, reporter, rIter, comparator,
keyClass, valueClass);
} else {
runOldReducer(job, umbilical, reporter, rIter, comparator,
keyClass, valueClass);
}
shuffleConsumerPlugin.close();
done(umbilical, reporter);
// CustomReducer.java的reduce()方法
context.write(key, flowBean);
// 继续向里走,走到ReduceTask.java的write()方法
public void write(K key, V value) throws IOException, InterruptedException {
long bytesOutPrev = getOutputBytes(fsStats);
real.write(key,value);// 写key,value数据到内存或磁盘
long bytesOutCurr = getOutputBytes(fsStats);
fileOutputByteCounter.increment(bytesOutCurr - bytesOutPrev);
outputRecordCounter.increment(1);
}
Join应用
Reduce Join
回想数据库中的join操作,可以通过join操作,将两个表按照某个值进行关联,从而获取有关联的数据,在reduce里,也可以实现这个功能。
将数据库的两个表想象成两个文件,每个文件里一行是一条记录,一条记录有一些属性。
Map端:读取不同文件中的数据,并对每条数据打标签,用于表示这条数据来源于哪个文件,使用连接的字段作为key,其余部分作为value进行输出。
Reduce端:每个reduce()
方法接收到的数据已经按照key进行分组了,我们只需要遍历这些数据,根据map阶段打的标记将数据分开,最后进行合并即可。
Reduce Join案例实操
需求:
订单数据表:t_order
id | produceId | amount |
---|---|---|
1001 | 1 | 1 |
1002 | 2 | 2 |
1003 | 3 | 3 |
1004 | 1 | 4 |
1005 | 2 | 5 |
1006 | 3 | 6 |
商品信息表:t_product | ||
productId | productName | |
:-------: | :---------: | |
1 | 小米 | |
2 | 华为 | |
3 | 苹果 | |
最终的数据格式:t_result | ||
id | productName | amount |
:–: | :---------: | :----: |
1001 | 小米 | 1 |
1002 | 小米 | 4 |
1003 | 华为 | 2 |
1004 | 华为 | 5 |
1005 | 苹果 | 3 |
1006 | 苹果 | 6 |
通过将关联条件作为Map输出的key,将两表满足Join条件的数据并携带数据所来源的文件信息,发往同一个ReduceTask,在Reduce中进行数据的关联。 | ||
![]() | ||
TableBean.java |
package com.demo.mapreduce.reducejoin;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
@Getter// get方法
@Setter// set方法
@NoArgsConstructor// 无参构造器
@AllArgsConstructor// 全部参数构造器
public class TableBean implements Writable {
private Integer id;// id
private Integer productId;// 商品id
private Integer amount;// 商品数量
private String productName;// 商品名称
private String flag;// 数据来源标记
/**
* 只输出我们关心的属性
*/
@Override
public String toString() {
return "TableBean{" +
"id=" + id +
", amount=" + amount +
", productName='" + productName + '\'' +
'}';
}
/**
* 序列化方法
*/
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeInt(id);
dataOutput.writeInt(productId);
dataOutput.writeInt(amount);
dataOutput.writeUTF(productName);
dataOutput.writeUTF(flag);
}
/**
* 反序列化方法
*/
@Override
public void readFields(DataInput dataInput) throws IOException {
id = dataInput.readInt();
productId = dataInput.readInt();
amount = dataInput.readInt();
productName = dataInput.readUTF();
flag = dataInput.readUTF();
}
}
TableMapper.java
package com.demo.mapreduce.reducejoin;
import cn.hutool.core.convert.Convert;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
public class TableMapper extends Mapper<LongWritable, Text, IntWritable, TableBean> {
private String fileName;
private IntWritable intWritable;
private TableBean tableBean;
/**
* 对数据进行初步处理
*/
@Override
protected void setup(Mapper<LongWritable, Text, IntWritable, TableBean>.Context context) throws IOException, InterruptedException {
InputSplit inputSplit = context.getInputSplit();
FileSplit fileSplit = (FileSplit) inputSplit;
fileName = fileSplit.getPath().getName();
intWritable = new IntWritable();
tableBean = new TableBean();
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, IntWritable, TableBean>.Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] split = line.split("\t");
if (fileName.contains("order")) {// 订单表
// 封装key
intWritable.set(Convert.toInt(split[1]));
// 封装value
tableBean.setId(Convert.toInt(split[0]));
tableBean.setProductId(Convert.toInt(split[1]));
tableBean.setAmount(Convert.toInt(split[2]));
tableBean.setProductName("");
tableBean.setFlag("order");
} else {// 商品表
// 封装key
intWritable.set(Convert.toInt(split[0]));
// 封装value
tableBean.setId(0);
tableBean.setProductId(Convert.toInt(split[0]));
tableBean.setAmount(0);
tableBean.setProductName(split[1]);
tableBean.setFlag("product");
}
context.write(intWritable, tableBean);
}
}
TableReducer.java
package com.demo.mapreduce.reducejoin;
import cn.hutool.core.bean.BeanUtil;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class TableReducer extends Reducer<IntWritable, TableBean, TableBean, NullWritable> {
@Override
protected void reduce(IntWritable key, Iterable<TableBean> values, Reducer<IntWritable, TableBean, TableBean, NullWritable>.Context context) throws IOException, InterruptedException {
TableBean tableBean = new TableBean();
List<TableBean> orderList = new ArrayList<>();
TableBean tempTableBean;
for (TableBean value : values) {
if ("order".equals(value.getFlag())) {// 订单表
// 在MR里,value是一个引用,add()进去的是引用,并非对象,下次循环的时候,value就变了,orderList里的值也会变
// orderList.add(value);// 错误的写法
tempTableBean = new TableBean();// 创建一个临时的TableBean用于接收
BeanUtil.copyProperties(value, tempTableBean);// 将属性拷贝到tempTableBean上
orderList.add(tempTableBean);// 添加到集合
} else {// 商品表
BeanUtil.copyProperties(value, tableBean);// 将属性拷贝到tableBean上
}
}
// 遍历orderList,填充productName
for (TableBean order : orderList) {
order.setProductName(tableBean.getProductName());
context.write(order, NullWritable.get());
}
}
}
TableDriver.java
package com.demo.mapreduce.reducejoin;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
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 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(IntWritable.class);
job.setMapOutputValueClass(TableBean.class);
job.setOutputKeyClass(TableBean.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
这种方式是有缺点的,合并操作是放在Reducer处理,Map阶段运算效率很低,Reducer阶段运算时间很长,而且会在Reducer端产生数据倾斜问题,为了解决这个问题,引出MapJoin。
Map Join
MapJoin的应用场景:一张或几张小表和一张大表。
MapJoin的做法:在Map端将小表缓存到内存,在Map阶段处理大表的时候,去内存查小表将需要的数据带过来,省略Reducer阶段。在Mapper的setup()
方法中将小表缓存到内存中。
Map Join案例实操
TableBean.java不变,创建TableMapper.java和TableDriver.java。
TableMapper.java
package com.demo.mapreduce.mapjoin;
import cn.hutool.core.convert.Convert;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
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.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class TableMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
private Map<Integer, String> map;
private Text text;
@Override
protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
map = new HashMap<>();
text = new Text();
Path path = new Path(context.getCacheFiles()[0]);// 读取缓存文件
FileSystem fileSystem = FileSystem.get(context.getConfiguration());
FSDataInputStream fsDataInputStream = fileSystem.open(path);// 获取文件输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(fsDataInputStream, StandardCharsets.UTF_8));// 通过包装流转化为reader,方便按行读取
String line;
String[] split;
while (StringUtils.isNotBlank(line = bufferedReader.readLine())) {
split = line.split("\t");
map.put(Convert.toInt(split[0]), split[1]);// 将缓存中内容放到内存,方便后面map()方法使用
}
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] split = line.split("\t");
TableBean tableBean = new TableBean();
tableBean.setId(Convert.toInt(split[0]));
tableBean.setProductName(map.get(Convert.toInt(split[1])));// 直接从map中取需要的信息
tableBean.setAmount(Convert.toInt(split[2]));
text.set(tableBean.toString());
context.write(text, NullWritable.get());
}
}
TableDriver.java
package com.demo.mapreduce.mapjoin;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
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;
import java.net.URI;
import java.net.URISyntaxException;
public class TableDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException, URISyntaxException {
Job job = Job.getInstance(new Configuration());
job.setJarByClass(TableDriver.class);
job.setMapperClass(TableMapper.class);
// job.setReducerClass(TableReducer.class);// 不需要指定Reducer了
job.setMapOutputKeyClass(IntWritable.class);
job.setMapOutputValueClass(TableBean.class);
job.setOutputKeyClass(TableBean.class);
job.setOutputValueClass(NullWritable.class);
URI uri = new URI("file:///E:/Hadoop/input/cache/product.txt");
job.addCacheFile(uri);// 设置缓存文件位置
job.setNumReduceTasks(0);// 设置Reducer的数量是0
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
数据清洗(ETL)
ETL是Extract Transform Load的缩写,用于描述数据经过抽取(Extract)、转换(Transform)、加载(Load)的过程。ETL常用语数据仓库。
在运行MapReduce之前,往往需要对数据进行清洗,清洗掉不合法的数据,整个过程只需要在Mapper阶段运行,不需要Reducer参与。
现在有一个需求,需要对a.log做处理,过滤掉字段长度小于10的数据。
WebLogMapper.java
package com.demo.mapreduce.etl;
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;
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();// 读取一行日志信息
boolean result = clear(line);// 数据清洗
if (!result) {// 数据不合法
return;
}
context.write(value, NullWritable.get());// 数据合法
}
private boolean clear(String line) {
return line.split(" ").length >= 10;
}
}
WebLogDriver.java
package com.demo.mapreduce.etl;
import com.demo.mapreduce.outputformat.LogDriver;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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 WebLogDriver {
public static void main(String[] args) throws Exception {
Job job = Job.getInstance(new Configuration());
job.setJarByClass(LogDriver.class);
job.setMapperClass(WebLogMapper.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
job.setNumReduceTasks(0);
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
MapReduce开发总结
- 输入数据的接口:InputFormat
默认实现类是TextInputFormat,每次读取一行文本,将偏移量作为key,行内容作为value;还有CombineTextInputFormat,可以将多个小文件合并成一个大文件进行切片处理,通过降低IO次数来提高效率 - 逻辑处理接口:Mapper
用户需要根据业务实现三个方法:setup()
、map()
、cleanup()
- Partitioner分区
分区的默认实现是HashPartitioner,根据key的hash值和numReduceTasks来返回一个分区号:key.hashCode() & Integer.MAX_VALUE % numReduceTasks
- Comparable排序
当自定义对象作为key用来输出的时候,这个对象需要实现WritableComparable接口,重写其中的compareTo()
方法;部分排序:对输出的每个文件进行内部排序;全排序:需要对所有数据进行排序,此时只能有一个Reducer,否则无法保证最终排序效果;二次排序:排序条件有多个,当第一个条件相当的时候,继续比较下一个条件 - Combiner合并
Combiner可以提高程序的执行效率,减少IO的传输,使用不能影响原有业务处理 - 逻辑处理接口:Reducer
用户需要根据业务实现三个方法:setup()
、map()
、cleanup()
- 输出数据接口:OutputFormat
默认实现是TextOutputFormat,将每一个kv对向目标文件输出一行,用户可以自定义OutputFormat
Hadoop数据压缩
概述
数据压缩可以减少磁盘IO,减少磁盘存储空间,但是会增加CPU开销,在压缩和解压的时候,需要CPU做运算。
对于运算密集型的Job,少用压缩;对于IO密集型的Job,多用压缩。
MR支持的压缩编码
压缩算法比较
压缩格式 | Hadoop自带 | 算法 | 文件扩展名 | 是否可切片 | 程序是否需要修改 |
---|---|---|---|---|---|
DEFLATE | 是 | DEFLATE | .deflate | 否 | 否 |
Gzip | 是 | DEFLATE | .gz | 否 | 否 |
Bzip2 | 是 | bzip2 | bz2 | 是 | 否 |
LZO | 否 | LZO | .lzo | 是 | 需要建索引、指定输入格式 |
Snappy | 是 | Snappy | .snappy | 否 | 否 |
压缩性能比较 | |||||
压缩算法 | 原文件 | 压缩后 | 压缩速度 | 解压速度 | |
:------: | :----: | :----: | :------: | :------: | |
Gzip | 8.3GB | 1.8GB | 17.5MB/s | 58MB/s | |
Bzip2 | 8.3GB | 1.1GB | 2.4MB/s | 9.5MB/s | |
LZO | 8.3GB | 2.9GB | 49.3MB/s | 74.6MB/s |
压缩方式选择
算法 | 优点 | 缺点 |
---|---|---|
Gzip | 压缩率较高 | 不支持Split,压缩/解压速度一般 |
Bzip2 | 压缩率高,支持Split | 压缩/解压速度慢 |
LZO | 压缩/解压速度较快、支持Split | 压缩率一般,需要额外创建索引 |
Snappy | 压缩/解压速度一般 | 不支持和Split、压缩率一般 |
在MapReduce的流程中,可以在3个地方进行压缩:Mapper输入端压缩、Mapper输出端压缩、Reducer输出端压缩。 | ||
Mapper输入端压缩:无需指定压缩格式,Hadoop根据扩展名自动匹配压缩和解压。如果数据量小于块大小,不用考虑Split,采用压缩/解压速度快的LZO、Snappy | ||
Mapper输出端压缩:减少MapTask和ReduceTask之间网络IO,考虑压缩快的LZO、Snappy | ||
Reducer输出端压缩:如果需要永久保存数据,考虑压缩比较高的Bzip2和Gzip,如果数据作为下一个Mapper的输入,需要考虑是否支持Split |
压缩参数设置
压缩格式 | 编码/解码器 |
---|---|
DEFLATE | org.apache.hadoop.io.compress.DefaultCodec |
Gzip | org.apache.hadoop.io.compress.GzipCodec |
Bzip2 | org.apache.hadoop.io.compress.BZip2Codec |
LZO | org.apache.hadoop.io.compress.LzoCodec |
Snappy | org.apache.hadoop.io.compress.SnappyCodec |
参数 | 默认值 | 阶段 | 建议 |
---|---|---|---|
io.compression.codecs(在core-site.xml中配置) | 无,在命令行使用hadoop checknative查看 | 输入压缩 | Hadoop采用文件扩展名判断是否支持某种压缩 |
mapreduce.map.output.compress(在mapred-site.xml中配置) | false | mapper输出 | 参数设置为true启用压缩 |
mapreduce.map.output.compr ess.codec(在mapredsite.xml中配置) | org.apache.hadoop.io.com press.DefaultCodec | mapper输出 | 企业多使用LZO或Snappy进行压缩 |
mapreduce.output.fileoutpu tformat.compress(在mapred-site.xml中配置) | false | reducer输出 | 参数设置为true启用压缩 |
mapreduce.output.fileoutpu tformat.compress.codec(在mapred-site.xml中配置) | org.apache.hadoop.io.com press.DefaultCodec | reducer输出 | 使用标准工具或者编解码器,如Gzip和Bzip2 |
压缩实操案例
Map输出端采用压缩
Driver.java里加上如下代码,其他代码不变。
// 开启 map 端输出压缩
conf.setBoolean("mapreduce.map.output.compress", true);
// 设置 map 端输出压缩方式
conf.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);
Reduce输出端采用压缩
Driver.java里加上如下代码,其他代码不变。
// 设置 reduce 端输出压缩开启
FileOutputFormat.setCompressOutput(job, true);
// 设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
// FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);
// FileOutputFormat.setOutputCompressorClass(job, DefaultCodec.class);
常见错误及解决方案
- 导包错误
- Mapper第一个参数必须是LongWritable或者NullWritable,不能是IntWritable,会报一个类型转换异常
- java.lang.Exception: java.io.IOException: Illegal partition for 13926435656 (4)表明Partition和ReduceTask没对上,需要调整ReduceTask个数
- 分区数不是1,ReduceTask是1,不执行分区过程,在MapTask里,优先判断ReduceTask是否大于1
- Windows和Linux使用的JDK环境不一致可能产生问题
- 在缓存小文件的时候,要指定文件到具体的文件名
- 如果报类型转换异常,需要检查下Map输出和最终输出;Map输出的key如果没有排序也会报这个错
- 集群环境下,文件不能放到HDFS集群目录
- 自定义OutputFormat时,在RecordWriter中
close()
方法必须关闭流,否则输出文件数据为空