续 Hadoop 中的大数据技术:MapReduce(1)
第3章 MapReduce框架原理
3.1 InputFormat 数据输入
3.1.1 切片与MapTask并行度决定机制
-
问题引出
-
MapTask的并行度决定了Map阶段的任务处理并发程度,从而影响整个Job的处理速度。
-
思考:对于1GB的数据,启动8个MapTask可以提高集群的并发处理能力。但对于1KB的数据,也启动8个MapTask,是否同样能提升集群性能?MapTask并行任务是否越多越好?哪些因素会影响MapTask并行度?
分析如下:
-
对于1KB的数据启动8个MapTask是否能提升集群性能?
- 答案是不能。对于如此小的数据量,启动多个MapTask不仅不会提升性能,反而会因为额外的管理开销(如启动MapTask、调度、数据传输等)而降低整体效率。在这种情况下,一个MapTask就足以处理全部数据。
-
MapTask并行任务是否越多越好?
- 并非越多越好。MapTask的数量需要根据实际数据量、集群资源状况、任务特性等因素综合考虑。过多的MapTask会导致不必要的资源浪费,例如更多的启动时间、调度延迟和管理开销。
-
哪些因素会影响MapTask并行度?
-
数据量:数据总量是决定MapTask数量的主要因素之一。通常情况下,数据量越大,可以分配的MapTask数量越多。
-
集群资源:集群可用的CPU核心数、内存等硬件资源限制了可以同时运行的MapTask数量。
-
数据分布:数据在HDFS上的分布方式也会影响MapTask的并行度。例如,如果数据分布在多个较小的文件中,可能会导致MapTask数量过多。
-
任务特性:某些任务可能需要更多的计算资源或者有特殊的执行要求,这也会影响MapTask的并行度。
-
配置参数 :Hadoop配置文件中的参数,如
mapreduce.job.split.metainfo.maxsize
、mapreduce.input.fileinputformat.split.minsize
等,可以用来控制输入切片的大小,间接影响MapTask的并行度。 -
InputFormat :不同的
InputFormat
实现可能有不同的切片策略,例如TextInputFormat
默认按文件切片,而CombineTextInputFormat
可以根据设置合并小文件。
-
-
-
-
MapTask并行度决定机制
- 数据块:Block是HDFS物理上把数据分割成的一系列块。数据块是HDFS存储数据的基本单位。
- 数据切片:数据切片是在逻辑层面对输入数据进行分片,并不会在磁盘上将其物理切分。数据切片是MapReduce程序处理输入数据的基本单位,一个切片通常对应一个MapTask。
3.1.2 Job提交流程源码
Job提交流程源码详解
waitForCompletion()
submit();
// 1建立连接
connect();
// 1)创建提交Job的代理
new Cluster(getConfiguration());
// (1)判断是本地运行环境还是yarn集群运行环境
initialize(jobTrackAddr, conf);
// 2 提交job
submitter.submitJobInternal(Job.this, cluster)
// 1)创建给集群提交数据的Stag路径
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
// 2)获取jobid ,并创建Job路径
JobID jobId = submitClient.getNewJobID();
// 3)拷贝jar包到集群
copyAndConfigureFiles(job, submitJobDir);
rUploader.uploadFiles(job, jobSubmitDir);
// 4)计算切片,生成切片规划文件
writeSplits(job, submitJobDir);
maps = writeNewSplits(job, jobSubmitDir);
input.getSplits(job);
// 5)向Stag路径写XML配置文件
writeConf(conf, submitJobFile);
conf.writeXml(out);
// 6)提交Job,返回提交状态
status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
3.1.3 FileInputFormat切片机制
FileInputFormat
的切片机制是 Hadoop MapReduce 中一个重要的组成部分,它决定了输入数据如何被分割为多个部分,以便可以并行处理。以下是 FileInputFormat
的切片机制的关键点:
1. 切片概念
- InputSplit:这是数据的一个逻辑划分,它可以被一个 Map 任务处理。
- FileSplit :这是
FileInputFormat
中的一种具体的InputSplit
实现,代表了一个文件的一部分。
2. 切片大小
- 默认情况下,切片的大小通常等于 HDFS 的块大小,但也可以通过配置进行调整。
- 可以通过以下配置来调整切片大小:
mapreduce.input.fileinputformat.split.minsize
:最小切片大小。mapreduce.input.fileinputformat.split.maxsize
:最大切片大小。mapreduce.input.fileinputformat.split.ignorebadlines
:是否忽略输入中的坏行。
3. 切片创建过程
- 获取输入文件 :首先,
FileInputFormat
会获取所有输入文件的列表。 - 计算切片大小:基于配置计算出切片的最大和最小大小。
- 分割文件:对于每个文件,如果其大小小于最小切片大小,则整个文件作为一个切片;如果文件大小大于最大切片大小,则将其分割为多个切片。
- 创建 FileSplit 对象 :为每个切片创建一个
FileSplit
对象,该对象包含了文件路径、偏移量和长度等信息。
4. 切片示例
假设有一个 HDFS 文件,大小为 16MB,而 HDFS 的默认块大小为 128MB。如果我们想要将此文件分割为多个切片,可以按照以下步骤操作:
-
获取配置 :获取
mapreduce.input.fileinputformat.split.minsize
和mapreduce.input.fileinputformat.split.maxsize
的值。 -
计算切片:假设最小切片大小为 4MB,最大切片大小为 16MB,则文件将被分割为 4 个切片,每个切片大小为 4MB。
-
创建切片 :为每个切片创建一个
FileSplit
对象。
代码示例
以下是 FileInputFormat
中 getSplits
方法的大致实现:
public List<InputSplit> getSplits(JobContext job) throws IOException {
// 获取所有输入文件
List<FileStatus> files = listStatus(job);
// 计算切片大小
long minSize = job.getConfiguration().getLong("mapreduce.input.fileinputformat.split.minsize", 0);
long maxSize = job.getConfiguration().getLong("mapreduce.input.fileinputformat.split.maxsize", Long.MAX_VALUE);
List<InputSplit> splits = new ArrayList<>();
for (FileStatus file : files) {
if (file.getLen() <= minSize) {
// 文件太小,直接作为一个切片
splits.add(new FileSplit(file.getPath(), 0, file.getLen(), file.getBlockLocations()));
} else {
// 文件较大,需要根据切片大小分割
long start = 0;
while (start < file.getLen()) {
long length = Math.min(maxSize, file.getLen() - start);
if (length > minSize) {
splits.add(new FileSplit(file.getPath(), start, length, file.getBlockLocations()));
}
start += length;
}
}
}
return splits;
}
3.1.4 TextInputFormat
- FileInputFormat实现类
- 在运行MapReduce程序时,需要处理各种格式的输入文件,如基于行的日志文件、二进制文件、数据库表等。针对不同的数据类型,MapReduce提供了多种InputFormat实现类,包括
TextInputFormat
、KeyValueTextInputFormat
、NLineInputFormat
、CombineTextInputFormat
等。
- 在运行MapReduce程序时,需要处理各种格式的输入文件,如基于行的日志文件、二进制文件、数据库表等。针对不同的数据类型,MapReduce提供了多种InputFormat实现类,包括
- TextInputFormat
TextInputFormat
是默认的FileInputFormat实现类,按行读取每条记录。键是该行在整个文件中的起始字节偏移量(LongWritable
类型),值是这一行的内容(Text
类型),不包含行终止符。- 示例:假设一个切片包含以下四条文本记录:
- Rich learning form
- Intelligent learning engine
- Learning more convenient
- From the real demand for more close to the enterprise
- 每条记录表示为以下键/值对:
- (0,Rich learning form)
- (20,Intelligent learning engine)
- (49,Learning more convenient)
- (74,From the real demand for more close to the enterprise)
3.1.5 CombineTextInputFormat切片机制
- 应用场景
CombineTextInputFormat
适用于小文件过多的场景,它可以将多个小文件逻辑上合并到一个切片中,由一个MapTask处理。
- 虚拟存储切片最大值设置
- 可以通过
CombineTextInputFormat.setMaxInputSplitSize(job, size)
来设置虚拟存储切片的最大值。设置值应根据实际情况确定。
- 可以通过
- 切片机制
- 生成切片过程分为虚拟存储过程和切片过程两部分。
- 虚拟存储过程:根据设置的切片最大值,将输入文件逻辑上划分为多个块。
- 切片过程:将虚拟存储的文件进一步组合成切片。
- 示例 :假设
setMaxInputSplitSize
值为4MB,输入文件大小为8.02MB,则首先逻辑上划分一个4MB的块。剩余的4.02MB若按4MB逻辑划分,则会得到一个小于4MB的切片,因此将其均分为两个2.01MB的块。
- 生成切片过程分为虚拟存储过程和切片过程两部分。
3.1.6 CombineTextInputFormat案例实操
CombineTextInputFormat
是 Hadoop 中一种特殊的 InputFormat
类,它主要用于处理大量的小文件。它的主要特点是能够合并多个小文件到一个较大的切片中,以减少 Map 任务的数量,从而提高 MapReduce 作业的效率。下面是关于 CombineTextInputFormat
切片机制的解析:
CombineTextInputFormat 概述
CombineTextInputFormat
是为了应对"大量小文件"场景设计的,这类场景下的问题在于过多的小文件会导致 Map 任务的数量增加,进而导致资源管理器(如 YARN)调度负担加重。通过合并小文件到更大的切片中,可以显著减少 Map 任务的数量。
切片机制
CombineTextInputFormat
的切片机制主要体现在它的 getSplits
方法上,该方法决定了如何将输入文件合并成切片。
代码解析
下面是 CombineTextInputFormat
的 getSplits
方法的大致实现逻辑:
public List<InputSplit> getSplits(JobContext job) throws IOException {
// 获取所有输入文件
List<FileStatus> files = listStatus(job);
// 计算切片大小
long minSize = job.getConfiguration().getLong("mapreduce.input.fileinputformat.split.minsize", 0);
long maxSplitSize = job.getConfiguration().getLong("mapreduce.input.fileinputformat.split.maxsize", Long.MAX_VALUE);
List<InputSplit> splits = new ArrayList<>();
long currentSplitSize = 0;
List<FileStatus> currentFiles = new ArrayList<>();
for (FileStatus file : files) {
if (currentSplitSize + file.getLen() <= maxSplitSize) {
// 文件可以添加到当前切片
currentSplitSize += file.getLen();
currentFiles.add(file);
} else {
// 当前切片已满,创建切片
addSplit(splits, currentFiles, currentSplitSize, minSize);
currentSplitSize = file.getLen();
currentFiles.clear();
currentFiles.add(file);
}
}
// 添加最后一个切片
addSplit(splits, currentFiles, currentSplitSize, minSize);
return splits;
}
private void addSplit(List<InputSplit> splits, List<FileStatus> files, long size, long minSize) {
if (size >= minSize) {
// 创建并添加一个新的 CombineFileSplit
Path[] paths = new Path[files.size()];
long[] lengths = new long[files.size()];
String[] racks = new String[files.size()];
for (int i = 0; i < files.size(); i++) {
FileStatus file = files.get(i);
paths[i] = file.getPath();
lengths[i] = file.getLen();
racks[i] = file.getReplication() > 0 ? NetworkTopology.DEFAULT_RACK : "";
}
splits.add(new CombineFileSplit(paths, lengths, racks, null));
}
}
关键点说明
- 合并文件 :
- 遍历所有输入文件,将文件添加到当前切片中,直到达到最大切片大小。
- 当达到最大切片大小时,创建一个
CombineFileSplit
并添加到切片列表中。
- 切片大小 :
- 最大切片大小通过
mapreduce.input.fileinputformat.split.maxsize
配置。 - 最小切片大小通过
mapreduce.input.fileinputformat.split.minsize
配置。
- 最大切片大小通过
- CombineFileSplit :
CombineFileSplit
是CombineTextInputFormat
中使用的切片类型,它包含了多个文件的信息。- 它包含了多个文件的路径、每个文件的长度、机架信息等。
结论
CombineTextInputFormat
通过合并多个小文件到一个较大的切片中,有效地减少了 Map 任务的数量,提高了 MapReduce 作业的执行效率。这种方法特别适用于大量小文件的场景。
3.2 MapReduce工作流程
- 上述流程概述了整个MapReduce的工作流程,其中Shuffle过程主要关注从MapTask到ReduceTask的数据传输过程,具体步骤如下:
- MapTask收集map()方法输出的键值对,并暂时存放在内存缓冲区中。
- 内存缓冲区中的数据定期溢写到本地磁盘文件中,可能产生多个文件。
- 多个溢出文件会被合并成较大的文件。
- 溢出和合并过程中,都会调用Partitioner进行分区和排序。
- ReduceTask根据自己的分区号,从各MapTask机器上拉取相应分区的数据。
- ReduceTask将同一分区的不同MapTask的结果文件进行合并排序。
- 完成合并后,Shuffle过程结束,随后进入ReduceTask的逻辑处理阶段(调用用户自定义的reduce()方法)。
- 注意事项
- Shuffle中的缓冲区大小会影响MapReduce程序的执行效率。原则上缓冲区越大,磁盘I/O次数越少,执行速度越快。
- 缓冲区大小可通过参数
mapreduce.task.io.sort.mb
调整,默认为100MB。
3.3 Shuffle 机制详解
3.3.1 Shuffle 机制概述
Shuffle 是 MapReduce 处理流程中的一个关键阶段,位于 Map 函数之后和 Reduce 函数之前。在这个阶段,Map 任务的输出数据会被排序、分区,并传输给 Reduce 任务进行进一步处理。
3.3.2 Partition 分区
分区是 Shuffle 阶段的重要组成部分,用于决定 Map 任务的输出数据应该发送给哪个 Reduce 任务处理。分区策略决定了数据如何分布到不同的 Reduce 任务中。
3.3.3 Partition 分区案例实操
1. 需求描述
根据手机号码的归属地(前三位号码),将统计结果输出到不同的文件中。具体来说,手机号码以 136、137、138、139 开头的分别输出到四个独立的文件中,其余号码则输出到同一个文件中。
2. 需求分析
为了满足上述需求,我们需要编写一个自定义的分区类,根据手机号码的前三位来确定数据应发送给哪个 Reduce 任务。
3. 自定义分区类
package com.lzl.mapreduce.partitioner;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
String phone = text.toString();
String prePhone = phone.substring(0, 3);
int partition;
switch (prePhone) {
case "136":
partition = 0;
break;
case "137":
partition = 1;
break;
case "138":
partition = 2;
break;
case "139":
partition = 3;
break;
default:
partition = 4;
break;
}
return partition;
}
}
4. 驱动函数中的配置
package com.lzl.mapreduce.partitioner;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class FlowDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(FlowDriver.class);
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
job.setPartitionerClass(ProvincePartitioner.class);
job.setNumReduceTasks(5);
FileInputFormat.setInputPaths(job, new Path("D:\\inputflow"));
FileOutputFormat.setOutputPath(job, new Path("D:\\partitionout"));
boolean success = job.waitForCompletion(true);
System.exit(success ? 0 : 1);
}
}
3.3.4 WritableComparable 排序
为了实现自定义排序,需要让 FlowBean
类实现 WritableComparable<FlowBean>
接口,并重写 compareTo
方法。这样可以按照总流量大小对数据进行排序。
@Override
public int compareTo(FlowBean bean) {
int result;
if (this.sumFlow > bean.getSumFlow()) {
result = -1;
} else if (this.sumFlow < bean.getSumFlow()) {
result = 1;
} else {
result = 0;
}
return result;
}
3.3.5 WritableComparable 全排序案例实操
需求说明
根据之前序列化案例的结果,对总流量进行倒序排序。
-
输入数据:
原始数据
1 13736230513 192.196.100.1 www.lzl.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.lzl.com 1527 2106 200 6 84188413 192.168.100.3 www.lzl.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.lzl.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
第一次处理后的数据
-
期望输出数据:
手机号码 上行流量 下行流量 总流量 13509468723 7335 110349 117684 13736230513 2481 24681 27162 13956435636 132 1512 1644 13846544121 264 0 264
需求分析
对总流量进行倒序排序,输出结果按总流量从高到低排列。
代码实现
-
FlowBean类:
package com.lzl.mapreduce.writablecompable; import org.apache.hadoop.io.WritableComparable; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; public class FlowBean implements WritableComparable<FlowBean> { private long upFlow; // 上行流量 private long downFlow; // 下行流量 private long sumFlow; // 总流量 public FlowBean() {} // Getters and Setters 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; } // Serialization and Deserialization @Override public void write(DataOutput out) throws IOException { out.writeLong(this.upFlow); out.writeLong(this.downFlow); out.writeLong(this.sumFlow); } @Override public void readFields(DataInput in) throws IOException { this.upFlow = in.readLong(); this.downFlow = in.readLong(); this.sumFlow = in.readLong(); } // To String method @Override public String toString() { return upFlow + "\t" + downFlow + "\t" + sumFlow; } // Comparison logic @Override public int compareTo(FlowBean other) { return Long.compare(other.sumFlow, this.sumFlow); // Inverted order for descending sort } }
-
FlowMapper类:
package com.lzl.mapreduce.writablecompable; 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> { private FlowBean outK = new FlowBean(); private Text outV = new Text(); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] fields = value.toString().split("\t"); outK.setUpFlow(Long.parseLong(fields[1])); outK.setDownFlow(Long.parseLong(fields[2])); outK.setSumFlow(); outV.set(fields[0]); context.write(outK, outV); } }
-
FlowReducer类:
package com.lzl.mapreduce.writablecompable; 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, Context context) throws IOException, InterruptedException { for (Text value : values) { context.write(value, key); } } }
-
FlowDriver类:
package com.lzl.mapreduce.writablecompable; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import java.io.IOException; public class FlowDriver { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { Configuration conf = new Configuration(); Job job = Job.getInstance(conf); job.setJarByClass(FlowDriver.class); job.setMapperClass(FlowMapper.class); job.setReducerClass(FlowReducer.class); job.setMapOutputKeyClass(FlowBean.class); job.setMapOutputValueClass(Text.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(FlowBean.class); FileInputFormat.addInputPath(job, new Path("D:\\inputflow2")); FileOutputFormat.setOutputPath(job, new Path("D:\\comparout")); boolean success = job.waitForCompletion(true); System.exit(success ? 0 : 1); } }
3.3.6 WritableComparable 区内排序案例实操
需求说明
要求每个省份手机号输出的文件中按照总流量内部排序。
需求分析
基于之前的全排序需求,增加自定义分区类,分区依据为手机号的前三位。
案例实操
-
ProvincePartitioner2类:
package com.lzl.mapreduce.partitionercompable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Partitioner; public class ProvincePartitioner2 extends Partitioner<FlowBean, Text> { @Override public int getPartition(FlowBean flowBean, Text text, int numPartitions) { String phone = text.toString(); String prePhone = phone.substring(0, 3); int partition; switch (prePhone) { case "136": partition = 0; break; case "137": partition = 1; break; case "138": partition = 2; break; case "139": partition = 3; break; default: partition = 4; break; } return partition; } }
-
FlowDriver类修改:
// 设置自定义分区器 job.setPartitionerClass(ProvincePartitioner2.class); // 设置ReduceTask的数量 job.setNumReduceTasks(5);
3.3.7 Combiner 合并
实现步骤
-
WordCountCombiner类:
package com.lzl.mapreduce.combiner; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer; import java.io.IOException; public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> { private IntWritable outV = new IntWritable(); @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int sum = 0; for (IntWritable value : values) { sum += value.get(); } outV.set(sum); context.write(key, outV); } }
-
WordcountDriver类:
// 指定Combiner类 job.setCombinerClass(WordCountCombiner.class);
3.3.8 Combiner 合并案例实操
需求说明
统计过程中对每一个MapTask的输出进行局部汇总,以减少网络传输量。
案例实操 - 方案一
-
WordCountCombiner类:
package com.lzl.mapreduce.combiner; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer; import java.io.IOException; public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> { private IntWritable outV = new IntWritable(); @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int sum = 0; for (IntWritable value : values) { sum += value.get(); } outV.set(sum); context.write(key, outV); } }
-
WordcountDriver类:
// 指定Combiner类 job.setCombinerClass(WordCountCombiner.class);
案例实操 - 方案二
-
将WordcountReducer作为Combiner
// 指定Combiner类 job.setCombinerClass(WordCountReducer.class);
运行程序后,您可以看到输出数据的网络传输量显著减少。
3.4 OutputFormat 数据输出
3.4.1 OutputFormat接口实现类
Hadoop MapReduce 提供了多种内置的 OutputFormat
实现类,这些类可以用来控制 MapReduce 作业如何将结果写入文件系统。下面是一些常用的 OutputFormat
实现类及其简要说明:
- TextOutputFormat
- 用途: 将结果以文本格式写入文件。
- 特点: 最常见的输出格式,适用于大多数场景。
- SequenceFileOutputFormat
- 用途: 将结果以 SequenceFile 格式写入文件。
- 特点: SequenceFile 是一种二进制格式,支持压缩,通常用于 Hadoop 内部数据交换。
- DBOutputFormat
- 用途: 将 MapReduce 结果直接写入关系型数据库。
- 特点: 可以将结果直接存储到数据库中,适合大数据量的导入。
- MultipleOutputs
- 用途: 允许将不同的键值对输出到不同的文件或目录。
- 特点 : 通过
MultipleOutputs
可以灵活地将数据按照某些规则输出到不同的文件中。
- LazyOutputFormat
- 用途: 延迟写入结果,直到作业完成。
- 特点: 在某些情况下可以提高性能,因为它减少了中间写入操作。
- FileOutputFormat
- 用途: 文件输出的基础类,其他特定的输出格式都是它的子类。
- 特点: 提供基本的文件输出功能,如设置输出路径等。
示例代码
下面是一个简单的示例,展示了如何使用 TextOutputFormat
和自定义 OutputFormat
类:
TextOutputFormat 示例
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
public class TextOutputFormatExample {
public static void main(String[] args) throws Exception {
Job job = Job.getInstance();
job.setJobName("TextOutputFormat Example");
// 设置Mapper和Reducer类
job.setMapperClass(TextOutputFormatExampleMapper.class);
job.setReducerClass(TextOutputFormatExampleReducer.class);
// 设置Map和Reduce输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 设置输出格式为TextOutputFormat
job.setOutputFormatClass(TextOutputFormat.class);
// 设置输入和输出路径
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 等待作业完成
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
自定义 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 CustomLogOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext taskAttemptContext)
throws IOException, InterruptedException {
// 创建自定义的RecordWriter实例
return new CustomLogRecordWriter(taskAttemptContext);
}
}
自定义RecordWriter 示例
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 CustomLogRecordWriter extends RecordWriter<Text, NullWritable> {
private FSDataOutputStream lzlOut;
private FSDataOutputStream otherOut;
public CustomLogRecordWriter(TaskAttemptContext job) {
try {
// 获取文件系统对象
FileSystem fs = FileSystem.get(job.getConfiguration());
// 创建两个输出流对应不同的文件
lzlOut = fs.create(new Path("e:/lzl.log"));
otherOut = fs.create(new Path("e:/other.log"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void write(Text key, NullWritable value) throws IOException, InterruptedException {
String log = key.toString();
// 根据日志内容决定输出到哪个文件
if (log.contains("lzl")) {
lzlOut.writeBytes(log + "\n");
} else {
otherOut.writeBytes(log + "\n");
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
// 关闭输出流
IOUtils.closeStream(lzlOut);
IOUtils.closeStream(otherOut);
}
}
请注意,在实际应用中,你需要将上述代码中的类名和包名替换为你自己的命名空间。此外,还需要编写相应的 Mapper 和 Reducer 类来配合自定义的 OutputFormat
使用。
3.4.2 自定义OutputFormat案例实操
需求
过滤输入的日志文件,将包含"lzl"的记录输出到e:/lzl.log
,不含"lzl"的记录输出到e:/other.log
。
-
输入数据:
http://www.baidu.com http://www.google.com http://cn.bing.com http://www.lzl.com http://www.sohu.com http://www.sina.com http://www.sin2a.com http://www.sin2desa.com http://www.sindsafa.com
-
期望输出数据:
e:/lzl.log
: 包含"lzl"的日志记录。e:/other.log
: 不含"lzl"的日志记录。
需求分析
设计一个自定义的OutputFormat来实现基于日志内容的条件性输出。
案例实操
-
LogMapper类:
package com.lzl.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, Context context) throws IOException, InterruptedException { // 直接输出每行日志数据 context.write(value, NullWritable.get()); } }
-
LogReducer类:
package com.lzl.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, Context context) throws IOException, InterruptedException { // 迭代输出所有相同的键值 for (NullWritable value : values) { context.write(key, NullWritable.get()); } } }
-
LogOutputFormat类:
package com.lzl.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 LogOutputFormat extends FileOutputFormat<Text, NullWritable> { @Override public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException { // 创建一个自定义的RecordWriter并返回 return new LogRecordWriter(job); } }
-
LogRecordWriter类:
package com.lzl.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 LogRecordWriter extends RecordWriter<Text, NullWritable> { private FSDataOutputStream lzlOut; private FSDataOutputStream otherOut; public LogRecordWriter(TaskAttemptContext job) { try { // 获取文件系统对象 FileSystem fs = FileSystem.get(job.getConfiguration()); // 创建两个输出流对应不同的文件 lzlOut = fs.create(new Path("d:/hadoop/lzl.log")); otherOut = fs.create(new Path("d:/hadoop/other.log")); } catch (IOException e) { e.printStackTrace(); } } @Override public void write(Text key, NullWritable value) throws IOException, InterruptedException { String log = key.toString(); // 根据日志内容决定输出到哪个文件 if (log.contains("lzl")) { lzlOut.writeBytes(log + "\n"); } else { otherOut.writeBytes(log + "\n"); } } @Override public void close(TaskAttemptContext context) throws IOException, InterruptedException { // 关闭输出流 IOUtils.closeStream(lzlOut); IOUtils.closeStream(otherOut); } }
-
LogDriver类:
package com.lzl.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, ClassNotFoundException, InterruptedException { Configuration conf = new Configuration(); Job job = Job.getInstance(conf); 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(LogOutputFormat.class); FileInputFormat.setInputPaths(job, new Path("D:\\input")); // 指定一个输出目录,即使它不会被直接使用 FileOutputFormat.setOutputPath(job, new Path("D:\\logoutput")); boolean success = job.waitForCompletion(true); System.exit(success ? 0 : 1); } }
3.5 MapReduce 内核源码解析
3.5.1 MapTask 工作机制
MapTask 的工作流程可以分为以下几个主要阶段:
- Read 阶段 :
- MapTask 通过 InputFormat 获得的 RecordReader 从输入 InputSplit 中解析出一个个 key/value 对。
- Map 阶段 :
- 解析出的 key/value 对被传给用户编写的 map() 函数进行处理,并产生一系列新的 key/value 对。
- Collect 收集阶段 :
- 在 map() 函数中,处理后的 key/value 对通过 OutputCollector.collect() 输出。在此函数内部,它会对生成的 key/value 进行分区(使用 Partitioner),并将它们写入一个环形内存缓冲区中。
- Spill 溢写阶段 :
- 当环形缓冲区满后,MapReduce 会将数据写到本地磁盘上,生成一个临时文件。在写入磁盘之前,会对数据进行排序,并可能进行合并、压缩等操作。
- 溢写阶段详情 :
- 利用快速排序算法对缓存区内的数据进行排序,首先按分区编号 Partition 排序,然后按 key 排序。
- 按照分区编号由小到大依次将每个分区中的数据写入本地临时文件 output/spillN.out(N 表示当前溢写次数)。如果配置了 Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。
- 将分区数据的元信息写入内存索引数据结构 SpillRecord 中,每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过 1MB,则将内存索引写入文件 output/spillN.out.index 中。
- Merge 合并阶段 :
- 当所有数据处理完成后,MapTask 对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。合并过程以分区为单位进行,采用多轮递归合并的方式,每轮合并 mapreduce.task.io.sort.factor(默认 10)个文件,直至最终得到一个大文件。
3.5.2 ReduceTask 工作机制
ReduceTask 的工作流程主要包括以下阶段:
- Copy 阶段 :
- ReduceTask 从各个 MapTask 上远程拷贝数据,并根据数据大小决定是否将其写入磁盘或放入内存。
- Sort 排序阶段 :
- 在远程拷贝数据的同时,ReduceTask 启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。ReduceTask 通过归并排序将 key 相同的数据聚集在一起。
- Reduce 阶段 :
- reduce() 函数处理聚集后的数据,并将结果写入 HDFS。
3.5.3 ReduceTask 并行度决定机制
-
设置 ReduceTask 并行度:
-
ReduceTask 的并行度可以通过手动设置来决定,例如:
job.setNumReduceTasks(4);
-
-
实验测试:
- 实验表明,在不同 ReduceTask 数量下,作业完成的时间有所不同。例如,在一个具有 16 个 Slave 节点的集群中,随着 ReduceTask 数量的增加,作业完成时间先减少后增加。
-
注意事项:
- 注意选择合适的 ReduceTask 数量,以达到最佳性能。
3.5.4 MapTask & ReduceTask 源码解析
MapTask 源码解析流程
================== MapTask ===================
context.write(k, NullWritable.get()); // 自定义 map 方法的写出,进入
output.write(key, value);
collector.collect(key, value, partitioner.getPartition(key, value, partitions)); // HashPartitioner() 分区器
collect() // map 端所有的 kv 全部写出后会走下面的 close 方法
close()
collector.flush() // 溢出刷写方法
sortAndSpill() // 溢写排序
sorter.sort() // 快速排序
0mergeParts(); // 合并文件
collector.close(); // 收集器关闭
ReduceTask 源码解析流程
================== ReduceTask ===================
if (isMapOrReduce())
initialize()
init(shuffleContext);
totalMaps = job.getNumMapTasks(); // ShuffleSchedulerImpl
merger = createMergeManager(context); // 合并方法
this.inMemoryMerger = createInMemoryMerger(); // 内存合并
this.onDiskMerger = new OnDiskMerger(this); // 磁盘合并
rIter = shuffleConsumerPlugin.run();
eventFetcher.start(); // 开始抓取数据
eventFetcher.shutDown(); // 抓取结束
copyPhase.complete(); // copy 阶段完成
taskStatus.setPhase(TaskStatus.Phase.SORT); // 开始排序阶段
sortPhase.complete(); // 排序阶段完成,即将进入 reduce 阶段
reduce() // reduce 阶段调用的就是我们自定义的 reduce 方法,会被调用多次
cleanup(context); // reduce 完成之前,会最后调用一次 Reducer 里面的 cleanup 方法
3.6 Join 应用
3.6.1 Reduce Join
Map 端的工作:
- 为来自不同表或文件的 key/value 对打上标签以区分不同来源的记录。
- 使用连接字段作为 key,其余部分和新增加的标志作为 value 输出。
Reduce 端的工作:
- 在 Reduce 端,以连接字段作为 key 的分组已经完成。
- 我们只需要在每个分组中将那些来源于不同文件的记录(已在 Map 阶段打上标志)分开,然后进行合并即可。
3.6.2 Reduce Join 案例实操
1. 需求
订单数据表 t_order:
id | pid | amount |
---|---|---|
1001 | 01 | 1 |
1002 | 02 | 2 |
1003 | 03 | 3 |
1004 | 01 | 4 |
1005 | 02 | 5 |
1006 | 03 | 6 |
商品信息表 t_product:
pid | pname |
---|---|
01 | 小米 |
02 | 华为 |
03 | 格力 |
目标:
- 将商品信息表中的数据根据商品 pid 合并到订单数据表中。
最终数据形式:
id | pname | amount |
---|---|---|
1001 | 小米 | 1 |
1004 | 小米 | 4 |
1002 | 华为 | 2 |
1005 | 华为 | 5 |
1003 | 格力 | 3 |
1006 | 格力 | 6 |
2. 需求分析
- 通过将关联条件作为 Map 输出的 key,将两表中满足 Join 条件的数据以及数据所来源的文件信息发送至同一个 ReduceTask。
- 在 Reduce 中进行数据的串联。
3. 代码实现
(1)创建商品和订单合并后的 TableBean 类
package com.lzl.mapreduce.reducejoin;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class TableBean implements Writable {
private String id; // 订单 ID
private String pid; // 产品 ID
private int amount; // 产品数量
private String pname; // 产品名称
private String flag; // 标志字段,用于区分数据来源
public TableBean() {}
// Getters and Setters...
@Override
public String toString() {
return id + "\t" + pname + "\t" + amount;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(id);
out.writeUTF(pid);
out.writeInt(amount);
out.writeUTF(pname);
out.writeUTF(flag);
}
@Override
public void readFields(DataInput in) throws IOException {
this.id = in.readUTF();
this.pid = in.readUTF();
this.amount = in.readInt();
this.pname = in.readUTF();
this.flag = in.readUTF();
}
}
(2)编写 TableMapper 类
package com.lzl.mapreduce.reducejoin;
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, Text, TableBean> {
private String filename;
private Text outK = new Text();
private TableBean outV = new TableBean();
@Override
protected void setup(Context context) throws IOException, InterruptedException {
InputSplit split = context.getInputSplit();
FileSplit fileSplit = (FileSplit) split;
filename = fileSplit.getPath().getName();
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString();
if (filename.contains("order")) { // 处理订单表
String[] split = line.split("\t");
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");
outK.set(split[0]);
outV.setId("");
outV.setPid(split[0]);
outV.setAmount(0);
outV.setPname(split[1]);
outV.setFlag("pd");
}
context.write(outK, outV);
}
}
(3)编写 TableReducer 类
package com.lzl.mapreduce.reducejoin;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
0import java.util.ArrayList;
public class TableReducer extends Reducer<Text, TableBean, TableBean, NullWritable> {
@Override
protected void reduce(Text key, Iterable<TableBean> values, Context context) throws IOException, InterruptedException {
ArrayList<TableBean> orderBeans = new ArrayList<>();
TableBean pdBean = new TableBean();
for (TableBean value : values) {
if ("order".equals(value.getFlag())) { // 处理订单表
TableBean tmpOrderBean = new TableBean();
try {
BeanUtils.copyProperties(tmpOrderBean, value);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
orderBeans.add(tmpOrderBean);
} else { // 处理商品表
try {
BeanUtils.copyProperties(pdBean, value);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
for (TableBean orderBean : orderBeans) {
orderBean.setPname(pdBean.getPname());
context.write(orderBean, NullWritable.get());
}
}
}
(4)编写 TableDriver 类
package com.lzl.mapreduce.reducejoin;
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 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("D:\\input"));
FileOutputFormat.setOutputPath(job, new Path("D:\\output"));
boolean success = job.waitForCompletion(true);
System.exit(success ? 0 : 1);
}
}
4. 测试
-
运行程序查看结果。
1004 小米 4 1001 小米 1 1005 华为 5 1002 华为 2 1006 格力 6 1003 格力 3
5. 总结
缺点:
- 合并操作在 Reduce 阶段完成,导致 Reduce 端处理压力过大,Map 节点的运算负载较低,资源利用率不高。
- 在 Reduce 阶段容易产生数据倾斜。
解决方案:
- 在 Map 端实现数据合并。
3.6.3 Map Join
使用场景
Map Join 适用于一张表非常小而另一张表很大的场景。
优点
考虑到在 Reduce 端处理过多的表容易产生数据倾斜,可以在 Map 端缓存多张表,提前处理业务逻辑,从而增加 Map 端的业务处理能力,减轻 Reduce 端的数据处理压力,尽可能减少数据倾斜的发生。
具体办法:采用 DistributedCache
-
在 Mapper 的 setup 阶段,将文件读取到缓存集合中。
-
在 Driver 驱动类中 加载缓存。
// 缓存普通文件到 Task 运行节点。
job.addCacheFile(new URI("file:///e:/cache/pd.txt"));
// 如果是集群运行,需要设置 HDFS 路径
job.addCacheFile(new URI("hdfs://hadoop102:8020/cache/pd.txt"));
Map Join 案例实操
1. 需求
订单数据表 t_order:
id | pid | amount |
---|---|---|
1001 | 01 | 1 |
1002 | 02 | 2 |
1003 | 03 | 3 |
1004 | 01 | 4 |
1005 | 02 | 5 |
1006 | 03 | 6 |
商品信息表 t_product:
pid | pname |
---|---|
01 | 小米 |
02 | 华为 |
03 | 格力 |
目标:
- 将商品信息表中的数据根据商品 pid 合并到订单数据表中。
最终数据形式:
id | pname | amount |
---|---|---|
1001 | 小米 | 1 |
1004 | 小米 | 4 |
1002 | 华为 | 2 |
1005 | 华为 | 5 |
1003 | 格力 | 3 |
1006 | 格力 | 6 |
2. 需求分析
Map Join 适用于关联表中有小表的情形。
3. 实现代码
(1)在 MapJoinDriver 驱动类中添加缓存文件
package com.lzl.mapreduce.mapjoin;
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;
import java.net.URI;
import java.net.URISyntaxException;
public class MapJoinDriver {
public static void main(String[] args) throws IOException, URISyntaxException, ClassNotFoundException, InterruptedException {
// 1 获取 job 信息
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 2 设置加载 jar 包路径
job.setJarByClass(MapJoinDriver.class);
// 3 关联 mapper
job.setMapperClass(MapJoinMapper.class);
// 4 设置 Map 输出 KV 类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
// 5 设置最终输出 KV 类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 加载缓存数据
job.addCacheFile(new URI("file:///D:/input/tablecache/pd.txt"));
// Map 端 Join 的逻辑不需要 Reduce 阶段,设置 reduceTask 数量为 0
job.setNumReduceTasks(0);
// 6 设置输入输出路径
FileInputFormat.setInputPaths(job, new Path("D:\\input"));
FileOutputFormat.setOutputPath(job, new Path("D:\\output"));
// 7 提交
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
(2)在 MapJoinMapper 类中的 setup 方法中读取缓存文件
package com.lzl.mapreduce.mapjoin;
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.IOUtils;
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.net.URI;
import java.util.HashMap;
import java.util.Map;
public class MapJoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
private Map<String, String> pdMap = new HashMap<>();
private Text text = new Text();
// 任务开始前将 pd 数据缓存进 pdMap
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 通过缓存文件得到小表数据 pd.txt
URI[] cacheFiles = context.getCacheFiles();
Path path = new Path(cacheFiles[0]);
// 获取文件系统对象,并开流
FileSystem fs = FileSystem.get(context.getConfiguration());
FSDataInputStream fis = fs.open(path);
// 通过包装流转换为 reader,方便按行读取
BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
// 逐行读取,按行处理
String line;
while (StringUtils.isNotEmpty(line = reader.readLine())) {
// 切割一行
// 01 小米
String[] split = line.split("\t");
pdMap.put(split[0], split[1]);
}
// 关流
IOUtils.closeStream(reader);
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 读取大表数据
// 1001 01 1
String[] fields = value.toString().split("\t");
// 通过大表每行数据的 pid,去 pdMap 里面取出 pname
String pname = pdMap.get(fields[1]);
// 将大表每行数据的 pid 替换为 pname
text.set(fields[0] + "\t" + pname + "\t" + fields[2]);
// 写出
context.write(text, NullWritable.get());
}
}
3.7 数据清洗(ETL)
3.7.1 ETL 定义
ETL 是英文 Extract-Transform-Load 的缩写,用来描述将数据从来源端经过抽取(Extract)、转换(Transform)、加载(Load)至目的端的过程。ETL 通常与数据仓库相关联,但其对象并不限于数据仓库。
3.7.2 需求
去除日志中字段个数小于等于 11 的日志。
-
输入数据
194.237.142.21 - - [18/Sep/2013:06:49:18 +0000] "GET /wp-content/uploads/2013/07/rstudio-git3.png HTTP/1.1" 304 0 "-" "Mozilla/4.0 (compatible;)" 183.49.46.228 - - [18/Sep/2013:06:49:23 +0000] "-" 400 0 "-" "-" 163.177.71.12 - - [18/Sep/2013:06:49:33 +0000] "HEAD / HTTP/1.1" 200 20 "-" "DNSPod-Monitor/1.0" 163.177.71.12 - - [18/Sep/2013:06:49:36 +0000] "HEAD / HTTP/1.1" 200 20 "-" "DNSPod-Monitor/1.0" 101.226.68.137 - - [18/Sep/2013:06:49:42 +0000] "HEAD / HTTP/1.1" 200 20 "-" "DNSPod-Monitor/1.0" 101.226.68.137 - - [18/Sep/2013:06:49:45 +0000] "HEAD / HTTP/1.1" 200 20 "-" "DNSPod-Monitor/1.0" 60.208.6.156 - - [18/Sep/2013:06:49:48 +0000] "GET /wp-content/uploads/2013/07/rcassandra.png HTTP/1.0" 200 185524 "http://cos.name/category/software/packages/" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.66 Safari/537.36" 222.68.172.190 - - [18/Sep/2013:06:49:57 +0000] "GET /images/my.jpg HTTP/1.1" 200 19939 "http://www.angularjs.cn/A00n" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.66 Safari/537.36" 222.68.172.190 - - [18/Sep/2013:06:50:08 +0000] "-" 400 0 "-" "-" 183.195.232.138 - - [18/Sep/2013:06:50:16 +0000] "HEAD / HTTP/1.1" 200 20 "-" "DNSPod-Monitor/1.0" 183.195.232.138 - - [18/Sep/2013:06:50:16 +0000] "HEAD / HTTP/1.1" 200 20 "-" "DNSPod-Monitor/1.0" 66.249.66.84 - - [18/Sep/2013:06:50:28 +0000] "GET /page/6/ HTTP/1.1" 200 27777 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" 221.130.41.168 - - [18/Sep/2013:06:50:37 +0000] "GET /feed/ HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.66 Safari/537.36" sa=t&rct=j&q=nodejs%20%E5%BC%82%E6%AD%A5%E5%B9%BF%E6%92%AD&source=web&cd=1&cad=rja&ved=0CCgQFjAA&url=%68%74%74%70%3a%2f%2f%62%6c%6f%67%2e%66%65%6e%73%2e%6d%65%2f%6e%6f%64%65%6a%73%2d%73%6f%63%6b%65%74%69%6f%2d%63%68%61%74%2f&ei=rko5UrylAefOiAe7_IGQBw&usg=AFQjCNG6YWoZsJ_bSj8kTnMHcH51hYQkAA&bvm=bv.52288139,d.aGc" "Mozilla/5.0 (Windows NT 5.1; rv:23.0) Gecko/20100101 Firefox/23.0" 58.215.204.118 - - [18/Sep/2013:06:51:41 +0000] "-" 400 0 "-" "-" 58.215.204.118 - - [18/Sep/2013:06:51:41 +0000] "-" 400 0 "-" "-" 58.215.204.118 - - [18/Sep/2013:06:51:41 +0000] "-" 400 0 "-" "-"
-
期望输出数据
每行字段长度都大于 11。
3.7.3 需求分析
需要在 Map 阶段对输入的数据根据规则进行过滤清洗。
3.7.4 实现代码
(1)编写 WebLogMapper 类
package com.lzl.mapreduce.weblog;
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, Context context) throws IOException, InterruptedException {
// 1 获取 1 行数据
String line = value.toString();
// 2 解析日志
boolean result = parseLog(line, context);
// 3 日志不合法退出
if (!result) {
return;
}
// 4 日志合法就直接写出
context.write(value, NullWritable.get());
}
// 2 封装解析日志的方法
private boolean parseLog(String line, Context context) {
// 1 截取
String[] fields = line.split(" ");
// 2 日志长度大于 11 的为合法
if (fields.length > 11) {
return true;
} else {
return false;
}
}
}
(2)编写 WebLogDriver 类
package com.lzl.mapreduce.weblog;
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 {
// 输入输出路径需要根据自己电脑上实际的输入输出路径设置
args = new String[]{"D:/input/inputlog", "D:/output1"};
// 1 获取 job 信息
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 2 加载 jar 包
job.setJarByClass(WebLogDriver.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);
}
}
3.8 MapReduce 开发总结
3.8.1 输入数据接口:InputFormat
- 默认使用的实现类 :
TextInputFormat
- 功能逻辑:一次读取一行文本,然后将该行的起始偏移量作为 key,行内容作为 value 返回。
- CombineTextInputFormat 可以把多个小文件合并成一个切片处理,提高处理效率。
3.8.2 逻辑处理接口:Mapper
- 用户根据业务需求实现其中三个方法:
map()
、setup()
、cleanup()
。
3.8.3 Partitioner 分区
- 默认实现 :
HashPartitioner
,逻辑是根据 key 的哈希值和numReduces
来返回一个分区号:key.hashCode() & Integer.MAX_VALUE % numReduces
。 - 自定义分区:如果业务上有特别的需求,可以自定义分区。
3.8.4 Comparable 排序
- 自定义对象作为 key :当使用自定义的对象作为 key 来输出时,必须实现
WritableComparable
接口,重写其中的compareTo()
方法。 - 部分排序:对最终输出的每一个文件进行内部排序。
- 全排序:对所有数据进行排序,通常只有一个 Reduce。
- 二次排序:排序的条件有两个。
3.8.5 Combiner 合并
- 提高程序执行效率:Combiner 合并可以提高程序执行效率,减少 IO 传输。
- 不影响原有业务处理结果:但是使用时必须确保不会影响原有的业务处理结果。
3.8.6 逻辑处理接口:Reducer
- 用户根据业务需求实现其中三个方法:
reduce()
、setup()
、cleanup()
。
3.8.7 输出数据接口:OutputFormat
- 默认实现类 :
TextOutputFormat
,功能逻辑是:将每一个 KV 对,向目标文本文件输出一行。 - 自定义 OutputFormat :用户还可以自定义
OutputFormat
。
第4章 Hadoop数据压缩
4.1 概述
4.1.1 压缩的好处与坏处
- 好处
- 减少磁盘I/O操作。
- 节省磁盘存储空间。
- 坏处
- 增加CPU负担。
4.1.2 压缩原则
- 运算密集型作业:减少使用压缩。
- I/O密集型作业:多使用压缩。
4.2 支持的压缩编码
4.2.1 压缩算法对比
压缩格式 | Hadoop自带 | 算法 | 文件扩展名 | 是否可切片 | 是否需修改程序 |
---|---|---|---|---|---|
DEFLATE | 是 | DEFLATE | .deflate | 否 | 不需要 |
Gzip | 是 | DEFLATE | .gz | 否 | 不需要 |
bzip2 | 是 | bzip2 | .bz2 | 是 | 不需要 |
LZO | 否 | LZO | .lzo | 是 | 需要 |
Snappy | 是 | Snappy | .snappy | 否 | 不需要 |
4.2.2 压缩性能对比
压缩算法 | 原始文件大小 | 压缩文件大小 | 压缩速度 | 解压速度 |
---|---|---|---|---|
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 |
Snappy | 8.3GB | 变化较大 | 250MB/s+ | 500MB/s+ |
4.3 压缩方式的选择
4.3.1 Gzip压缩
- 优点:较高的压缩比。
- 缺点:不支持切片;压缩/解压速度一般。
4.3.2 Bzip2压缩
- 优点:高压缩比;支持切片。
- 缺点:压缩/解压速度较慢。
4.3.3 LZO压缩
- 优点:压缩/解压速度快;支持切片。
- 缺点:压缩比一般;需要额外创建索引才能支持切片。
4.3.4 Snappy压缩
- 优点:压缩和解压缩速度快。
- 缺点:不支持切片;压缩比一般。
4.3.5 压缩位置选择
- 选择:根据作业特性,在不同阶段启用压缩。
4.4 压缩参数配置
4.4.1 编码/解码器
压缩格式 | 对应的编码/解码器 |
---|---|
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 |
4.4.2 启用压缩的配置
参数 | 默认值 | 阶段 | 建议 |
---|---|---|---|
io.compression.codecs |
无 | 输入压缩 | 使用文件扩展名判断 |
mapreduce.map.output.compress |
false |
Mapper输出 | 启用压缩 |
mapreduce.map.output.compress.codec |
org.apache.hadoop.io.compress.DefaultCodec |
Mapper输出 | 使用特定编解码器 |
mapreduce.output.fileoutputformat.compress |
false |
Reducer输出 | 启用压缩 |
mapreduce.output.fileoutputformat.compress.codec |
org.apache.hadoop.io.compress.DefaultCodec |
Reducer输出 | 使用特定编解码器 |
4.5 压缩实操案例
4.5.1 Map 输出端采用压缩
驱动程序
package com.lzl.mapreduce.compress;
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.io.compress.BZip2Codec;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class WordCountDriver {
public static void main(String[] args) throws Exception {
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);
job.setJarByClass(WordCountDriver.class);
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
Mapper
package com.lzl.mapreduce.compress;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
@Override
public void map(LongWritable key, Text value, Context context) throws Exception {
String[] words = value.toString().split("\\s+");
for (String w : words) {
word.set(w);
context.write(word, one);
}
}
}
Reducer
package com.lzl.mapreduce.compress;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable result = new IntWritable();
@Override
public void reduce(Text key, Iterable<IntWritable> values, Context context) throws Exception {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
context.write(key, result);
}
}
4.5.2 Reduce 输出端采用压缩
修改驱动程序
package com.lzl.mapreduce.compress;
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.io.compress.BZip2Codec;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
0import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class WordCountDriver {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(WordCountDriver.class);
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 设置reduce端输出压缩开启
FileOutputFormat.setCompressOutput(job, true);
// 设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
Mapper 和 Reducer 保持不变(详见4.5.1)
第5章 常见错误及解决方案
5.1 导入包错误
- 常见问题 :导入包错误,尤其是
Text
和CombineTextInputFormat
。
5.2 Mapper 输入参数错误
- 错误 :Mapper的第一个输入参数不能是
IntWritable
。 - 解决 :使用
LongWritable
或NullWritable
。
5.3 Partition 和 ReduceTask 数量不匹配
- 错误 :
java.lang.Exception: java.io.IOException: Illegal partition
。 - 解决:调整ReduceTask的数量。
5.4 Partition 不执行
- 条件:如果分区数不是1但ReduceTask数量为1,不会执行分区过程。
- 原因:MapTask源码中判断ReduceTask数量是否大于1来决定是否执行分区。
5.5 Windows 环境下的JAR包移植问题
-
错误:
Exception in thread "main" java.lang.UnsupportedClassVersionError: com/lzl/mapreduce/wordcount/WordCountDriver : Unsupported major.minor version 52.0
-
原因:Windows环境与Linux环境的JDK版本不一致。
-
解决:统一使用相同的JDK版本。
5.6 缓存文件找不到
- 错误:找不到缓存的小文件。
- 解决:检查路径是否正确;确保文件命名正确;尝试使用绝对路径。
5.7 类型转换异常
- 常见原因:驱动函数中设置Map输出和最终输出时出现错误。
- 解决:检查类型是否匹配;确认Map输出的key已排序。
5.8 HDFS 输入文件问题
- 错误:无法获取输入文件。
- 解决:不要将输入文件放置在HDFS集群的根目录下。
5.9 运行时异常
-
错误:
Exception in thread "main" java.lang.UnsatisfiedLinkError: org.apache.hadoop.io.nativeio.NativeIO$Windows.access0(Ljava/lang/String;I)Z at org.apache.hadoop.io.nativeio.NativeIO$Windows.access0(Native Method) at org.apache.hadoop.io.nativeio.NativeIO$Windows.access(NativeIO.java:609) at org.apache.hadoop.fs.FileUtil.canRead(FileUtil.java:977) java.io.IOException: Could not locate executable null\bin\winutils.exe in the Hadoop binaries. at org.apache.hadoop.util.Shell.getQualifiedBinPath(Shell.java:356) at org.apache.hadoop.util.Shell.getWinUtilsPath(Shell.java:371) at org.apache.hadoop.util.Shell.<clinit>(Shell.java:364)
-
解决 :复制
hadoop.dll
文件到C:\Windows\System32
;修改Hadoop源码。
5.10 自定义OutputFormat时关闭流资源
-
重要 :在
RecordWriter
中的close
方法必须关闭流资源。否则输出的文件内容中数据为空。 -
示例:
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
if (fileOutputStream != null) {
fileOutputStream.close();
}
}