Hadoop 学习笔记之 MapReduce
目录
3、自定义 bean 对象实现序列化接口(Writable)
一、MapReduce 概述
1、MapReduce 定义
MapReduce 是一个分布式运算程序的编程框架,是用户开发 "基于 Hadoop 的数据分析应用" 的核心框架。
MapReduce 核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。
2、MapReduce 优缺点
1)优点:
- MapReduce 易于编程:它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的PC机器上运行。
- 良好的扩展性:当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力。
- 高容错性:比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败,而且这个过程不需要人工参与,而完全是由 Hadoop 内部完成的。
- 适合PB级以上海量数据的离线处理:可以实现上千台服务器集群并发工作,提供数据处理能力。
2)缺点:
- 不擅长实时计算:MapReduce 无法像 MySQL 一样,在毫秒或者秒级内返回结果。
- 不擅长流式计算:MapReduce 的输入数据集是静态的,不能动态变化。
- 不擅长DAG(有向无环图)计算:每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常的低下。
3、MapReduce核心思想
- 分布式的运算程序往往需要分成至少2个阶段。
- 第一个阶段的 MapTask 并发实例,完全并行运行,互不相干。
- 第二个阶段的 ReduceTask 并发实例互不相干,但是他们的数据依赖于上一个阶段的所有 MapTask 并发实例的输出。
- MapReduce 编程模型只能包含一个 Map 阶段和一个 Reduce 阶段,如果用户的业务逻辑非常复杂,那就只能多个 MapReduce 程序,串行运行。
4、MapReduce进程
一个完整的 MapReduce 程序在分布式运行时有三类实例进程:
- MrAppMaster:负责整个程序的过程调度及状态协调。
- MapTask:负责Map阶段的整个数据处理流程。
- ReduceTask:负责Reduce阶段的整个数据处理流程。
5、常用数据序列化类型
- Java类型 Hadoop Writable类型
- Boolean BooleanWritable
- Byte ByteWritable
- Int IntWritable
- Float FloatWritable
- Long LongWritable
- Double DoubleWritable
- String Text
- Map MapWritable
- Array ArrayWritable
- Null NullWritabl
6、MapReduce编程规范
用户编写的程序分成三个部分:Mapper、Reducer 和 Driver。
1)Mapper阶段
- 用户自定义的 Mapper 要继承自己的父类
- Mapper 的输入数据是 KV 对的形式(KV 的类型可自定义)
- Mapper中的业务逻辑写在 map() 方法中
- Mapper的输出数据是 KV 对的形式(KV 的类型可自定义)
- map() 方法(MapTask进程)对每一个<K,V>调用一次
2)Reducer阶段
- 用户自定义的 Reducer 要继承自己的父类
- Reducer 的输入数据类型对应 Mapper 的输出数据类型,也是 KV
- Reducer 的业务逻辑写在 reduce() 方法中
- ReduceTask 进程对每一组相同 k 的 <k,v> 组调用一次 reduce() 方法
3)Driver 阶段
相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是封装了MapReduce程序相关运行参数的job对象
二、Hadoop序列化
1、序列化概述
序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输。
反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象。
2、Hadoop序列化特点:
(1)紧凑 :高效使用存储空间。
(2)快速:读写数据的额外开销小。
(3)互操作:支持多语言的交互
3、自定义 bean 对象实现序列化接口(Writable)
- 必须实现 Writable 接口
- 反序列化时,需要反射调用空参构造函数,所以必须有空参构造
- 重写序列化方法
- 重写反序列化方法
- 注意反序列化的顺序和序列化的顺序完全一致
- 要想把结果显示在文件中,需要重写 toString(),可用"\t"分开,方便后续用。
- 如果需要将自定义的 bean 放在 key 中传输,则还需要实现 Comparable 接口,因为 MapReduce 框中的 Shuffle 过程要求对 key 必须能排序。
三、MapReduce框架原理
1、InputFormat数据输入
1)切片与MapTask并行度决定机制
(MapTask 并行度由切片个数决定,切片个数由输入文件和切片规则决定)
MapTask 的并行度决定 Map 阶段的任务处理并发度,进而影响到整个 Job 的处理速度。
MapTask 并行度决定机制:
- 数据块:Block 是 HDFS 物理上把数据分成一块一块。数据块是 HDFS 存储数据单位。
- 数据切片:数据切片只是在逻辑上对输入数据进行分片,并不会在磁盘上将其切分成片进行存储。数据切片是 MapReduce 程序计算输入数据的单位,一个切片会对应启动一个MapTask。
- 一个 Job 的 Map 阶段并行度由客户端在提交 Job 时的切片数决定
- 每一个 Split 切片分配一个 MapTask 并行实例处理
- 默认情况下,切片大小 = BlockSize
- 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
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());
FileInputFormat切片源码解析(input.getSplits(job)
1、程序先找到你数据存储的目录
2、开始遍历处理(规划切片)目录下的每一个文件
3、遍历第一个文件 xx.txt(300M)
a)获取文件大小fs.sizeOf(xx.txt)
b)计算切片大小
computeSplitSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M
c)默认情况下,切片大小 = blocksize
d)开始切,形成第1个切片:xx.txt—0:128M 第2个切片xx.txt—128:256M 第3个切片xx.txt—256M:300M(每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分一块切片)
e)将切片信息写到一个切片规划文件中
f)整个切片的核心过程在getSplit()方法中完成
g)InputSplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等。
4、提交切片规划文件到 YARN 上,YARN 上的 MrAppMaster 就可以根据切片规划文件计算开启 MapTask 个数。
3)FileInputFormat 切片机制
- 简单地按照文件的内容长度进行切片
- 切片大小,默认等于 Block 大小
- 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
源码中计算切片大小的公式
Math.max(minSize, Math.min(maxSize, blockSize));
mapreduce.input.fileinputformat.split.minsize=1 默认值为1
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值 Long.MAXValue
因此,默认情况下,切片大小=blocksize。
切片大小设置
maxsize(切片最大值):参数如果调得比blockSize小,则会让切片变小,而且就等于配置的这个参数的值。
minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blockSize还大。
获取切片信息API
// 获取切片的文件名称
String name = inputSplit.getPath().getName();
// 根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();
4)FileInputFormat实现类
TextInputFormat
TextInputFormat 是默认的 FileInputFormat 实现类。按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量,LongWritable 类型。值是这行的内容,不包括任何行终止符(换行符和回车符),Text 类型。
CombineTextInputFormat
( job.setInputFormatClass(CombineTextInputFormat.class) )
CombineTextInputFormat 用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。
虚拟存储切片最大值设置
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值
切片机制
生成切片过程包括:虚拟存储过程和切片过程二部分
(1)虚拟存储过程:
将输入目录下所有文件大小,依次和设置的 setMaxInputSplitSize 值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现太小切片)。
eg:
setMaxInputSplitSize 值为4M,输入文件大小为8.02M,则先逻辑上分成一个4M。剩余的大小为4.02M,如果按照4M逻辑划分,就会出现0.02M的小的虚拟存储文件,所以将剩余的4.02M文件切分成(2.01M和2.01M)两个文件。
(2)切片过程:
判断虚拟存储的文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片。
如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
eg:
有4个小文件大小分别为1.7M、5.1M、3.4M以及6.8M这四个小文件,则虚拟存储之后形成6个文件块,大小分别为:
1.7M,(2.55M、2.55M),3.4M以及(3.4M、3.4M)
最终会形成3个切片,大小分别为:
(1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M
2、MapReduce工作流程
分片 → Map → Shuffle → Reduce → 输出
1)作业提交与初始化(客户端 → YARN)
- 客户端提交 Job:封装 Jar、配置、输入 / 输出路径、Mapper/Reducer 类,上传资源到 HDFS,向 ResourceManager(RM) 申请作业。
- RM 启动 ApplicationMaster(AM):RM 分配 Container 启动 AM(MRAppMaster),AM 负责后续任务调度。
- 数据分片(InputSplit):AM 读取 HDFS 输入目录,按 128MB(默认) 切分文件为 Split,每个 Split 对应 1 个 MapTask;分片时尽量数据本地化(任务调度到数据所在节点)。
2)Map 阶段(并行执行 MapTask)
每个 MapTask 处理 1 个 Split,流程:
- Read 读取:
InputFormat(默认 TextInputFormat)+RecordReader把 Split 转为<K1, V1>(行偏移量→行文本)。 - Map 处理:执行用户定义的
map(),输出中间结果<K2, V2>(如单词→1)。 - Collect 收集(环形缓冲区):中间结果写入内存环形缓冲区(默认 100MB),满 80% 溢写到本地磁盘;溢写前分区(Partition)+ 排序(Sort)+ 合并(Combiner,可选)。
- 分区:按
Partitioner(默认 Hash)把 K2 映射到 Reduce 分区,确保同 Key 到同一 Reduce。 - 排序:按 K2 字典序排序。
- Combiner:本地预聚合(如单词计数先本地求和),减少网络传输。
- 分区:按
3)Reduce 阶段(并行执行 ReduceTask)
- Reduce 处理:对每个唯一 Key,调用用户定义的
reduce(K2, Iterable<V2>, context),执行聚合逻辑(如求和、求平均),输出<K3, V3>。 - Write 输出:
OutputFormat+RecordWriter把结果写入 HDFS 输出目录,生成最终结果文件。
3、Shuffle 机制
Map 方法之后,Reduce 方法之前的数据处理过程称之为 Shuffle。
具体Shuffle过程详解,如下:
(1)MapTask收集我们的map()方法输出的 kv 对,放到内存缓冲区中
(2)从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
(3)多个溢出文件会被合并成大的溢出文件
(4)在溢出过程及合并的过程中,都要调用 Partitioner 进行分区和针对 key 进行排序
(5)ReduceTask 根据自己的分区号,去各个 MapTask 机器上取相应的结果分区数据
(6)ReduceTask 会抓取到同一个分区的来自不同 MapTask 的结果文件,ReduceTask 会将这些文件再进行合并(归并排序)
(7)合并成大文件后,Shuffle 的过程也就结束了,后面进入 ReduceTask 的逻辑运算过程(从文件中取出一个一个的键值对 Group,调用用户自定义的reduce()方法)
注意:
(1)Shuffle 中的缓冲区大小会影响到 MapReduce 程序的执行效率,原则上说,缓冲区越大,磁盘 io 的次数越少,执行速度就越快。
(2)缓冲区的大小可以通过参数调整,参数:mapreduce.task.io.sort.mb默认100M。
1)Partition分区
默认分区:默认分区是根据 key 的 hashCode 对 ReduceTasks 个数取模得到的。用户没法控制哪个key存储到哪个分区。
自定义分区:自定义类继承 Partitioner,重写 getPartition() 方法;在 Job 驱动中,设置自定义Partitioner (job.setPartitionerClass(partitionerName.class));自定义 Partition 后,要根据自定义Partitioner 的逻辑设置相应数量的 ReduceTask(job.setNumReduceTasks(x))。
分区总结:
- · 如果 ReduceTask 的数量 > getPartition 的结果数,则会多产生几个空的输出文件part-r-000xx
- · 如果1 < ReduceTask 的数量 < getPartition 的结果数,则有一部分分区数据无处安放,会Exception
- · 如果 ReduceTask 的数量 = 1,则不管 MapTask 端输出多少个分区文件,最终结果都交给这一个 ReduceTask ,最终也就只会产生一个结果文件 part-r-00000
- · 分区号必须从零开始,逐一累加
2)WritableComparable排序
默认排序:
排序是MapReduce框架中最重要的操作之一
MapTask 和 ReduceTask 均会对数据按照 key 进行排序。该操作属于 Hadoop 的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要
默认排序是按照字典顺序排序,且实现该排序的方法是快速排序
MapTask:它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。
ReduceTask:它从每个 MapTask 上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中。如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask 统一对内存和磁盘上的所有数据进行一次归并排序。
排序分类:
- 部分排序:MapReduce 根据输入记录的键对数据集排序。保证输出的每个文件内部有序。
- 全排序:最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。
- 辅助排序(GroupingComparator分组):在 Reduce 端对 key 进行分组。应用于:在接收的 key 为 bean 对象时,想让一个或几个字段相同(全部字段比较不相同)的 key 进入到同一个 reduce 方法时,可以采用分组排序。
- 二次排序:在自定义排序过程中,如果 compareTo中 的判断条件为两个即为二次排序。
自定义排序: bean 对象做为 key 传输,需要实现 WritableComparable 接口重写 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)Combiner合并
- Combiner 是 MR 程序中 Mapper 和 Reducer 之外的一种组件。
- Combiner 组件的父类就是 Reducer。
- Combiner 和 Reducer 的区别在于运行的位置:Combiner 是在每一个 MapTask 所在的节点运行;Reducer 是接收全局所有 Mapper 的输出结果;
- Combiner 的意义就是对每一个 MapTask 的输出进行局部汇总,以减小网络传输量。
- Combiner 能够应用的前提是不能影响最终的业务逻辑,而且,Combiner 的输出 kv 应该跟 Reducer 的输入kv类型要对应起来。
自定义Combiner:自定义一个Combiner继承Reducer,重写Reduce方法
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);
}
}
在Job驱动类中设置
job.setCombinerClass(WordCountCombiner.class);
4、OutputFormat 数据输出
OutputFormat 是 MapReduce 输出的基类,所有实现 MapReduce 输出都实现了 OutputFormat 接口。默认输出格式 TextOutputFormat
自定义 OutputFormat
- 应用场景:输出数据到 MySQL/HBase/Elasticsearch 等存储框架中
- 步骤:自定义一个类继承 FileOutputFormat;改写 RecordWriter,具体改写输出数据的方法write()
eg:过滤输入的log日志,包含 keyword 的内容输出到 d:/hadoop/keyword.log,不包含 keyword 的内容输出到 d:/hadoop/other.log
LogOutputFormat 类
public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
//创建一个自定义的RecordWriter返回
LogRecordWriter logRecordWriter = new LogRecordWriter(job);
return logRecordWriter;
}
}
LogRecordWriter 类
public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
private FSDataOutputStream keywordOut;
private FSDataOutputStream otherOut;
public LogRecordWriter(TaskAttemptContext job) {
try {
//获取文件系统对象
FileSystem fs = FileSystem.get(job.getConfiguration());
//用文件系统对象创建两个输出流对应不同的目录
keywordOut = fs.create(new Path("d:/hadoop/keyword.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();
//根据一行的log数据是否包含atguigu,判断两条输出流输出的内容
if (log.contains("keyword")) {
keywordOut.writeBytes(log + "\n");
} else {
otherOut.writeBytes(log + "\n");
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
//关流
IOUtils.closeStream(keywordOut);
IOUtils.closeStream(otherOut);
}
}
LogDriver类
//设置自定义的outputformat
job.setOutputFormatClass(LogOutputFormat.class);
FileInputFormat.setInputPaths(job, new Path("D:\\input"));
//虽然我们自定义了outputformat,但是因为我们的outputformat继承自fileoutputformat
//而fileoutputformat要输出一个_SUCCESS文件,所以在这还得指定一个输出目录
FileOutputFormat.setOutputPath(job, new Path("D:\\logoutput"));
5、MapReduce内核源码解析
1)MapTask工作机制
- Read 阶段:MapTask 通过 InputFormat 获得的 RecordReader,从输入 InputSplit 中解析出一个个 key/value。
- Map 阶段:该节点主要是将解析出的 key/value 交给用户编写 map() 函数处理,并产生一系列新的 key/value。
- Collect 收集阶段:在用户编写 map() 函数中,当数据处理完成后,一般会调用 OutputCollector.collect()输出结果。在该函数内部,它会将生成的 key/value 分区(调用Partitioner),并写入一个环形内存缓冲区中。
- Spill 阶段:即“溢写”,当环形缓冲区满后,MapReduce 会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。
- Merge 阶段:当所有数据处理完成后,MapTask 对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。
所有数据处理完后,MapTask 会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件 output/file.out.index。
在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并mapreduce.task.io.sort.factor(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。
让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。
溢写阶段详情:
- 利用快速排序算法对缓存区内的数据进行排序,排序方式是先按照分区编号 Partition 进行排序,然后按照 key 进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照 key 有序。
- 按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件 output/spillN.out(N表示当前溢写次数)中。如果用户设置了 Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。
- 将分区数据的元信息写到内存索引数据结构 SpillRecord 中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件 output/spillN.out.index 中。
2)ReduceTask工作机制
- Copy 阶段:ReduceTask 从各个 MapTask 上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
- Sort 阶段:在远程拷贝数据的同时,ReduceTask 启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照 MapReduce 语义,用户编写 reduce()函数输入数据是按 key 进行聚集的一组数据。为了将 key 相同的数据聚在一起,Hadoop 采用了基于排序的策略。由于各个 MapTask 已经实现对自己的处理结果进行了局部排序,因此,ReduceTask 只需对所有数据进行一次归并排序即可。
- Reduce 阶段:reduce() 函数将计算结果写到HDFS上。
3)ReduceTask并行度决定机制
ReduceTask 的并行度同样影响整个 Job 的执行并发度和执行效率,但与 MapTask 的并发数由切片数决定不同,ReduceTask 数量的决定是可以直接手动设置
// 默认值是1,手动设置为4
job.setNumReduceTasks(4);
注意事项:
- ReduceTask = 0,表示没有 Reduce 阶段,输出文件个数和 Map 个数一致。
- ReduceTask 默认值就是1,所以输出文件个数为一个。
- 如果数据分布不均匀,就有可能在 Reduce 阶段产生数据倾斜
- ReduceTask 数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个 ReduceTask。
- 具体多少个 ReduceTask,需要根据集群性能而定。
- 如果分区数不是1,但是 ReduceTask 为1,不执行分区过程。因为在MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1。不大于1肯定不执行。
6、Join应用
1)Reduce Join
Map 端的主要工作:为来自不同表或文件的 key/value 对,打标签以区别不同来源的记录。然后用连接字段作为 key,其余部分和新加的标志作为 value,最后进行输出。
Reduce 端的主要工作:在 Reduce 端以连接字段作为 key 的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在 Map 阶段已经打标志)分开,最后进行合并就ok了。
缺点:这种方式中,合并的操作是在Reduce阶段完成,Reduce端的处理压力太大,Map节点的运算负载则很低,资源利用率不高,且在Reduce阶段极易产生数据倾斜。
2)Map Join
使用场景:Map Join 适用于一张表十分小、一张表很大的场景。
优点:在 Map 端缓存多张表,提前处理业务逻辑,这样增加 Map 端业务,减少 Reduce 端数据的压力,尽可能的减少数据倾斜。
具体方法:
- 在Mapper的setup阶段,将文件读取到缓存集合中。
- 在Driver驱动类中加载缓存。
//缓存普通文件到Task运行节点。
job.addCacheFile(new URI("file:///e:/cache/pd.txt"));
//如果是集群运行,需要设置HDFS路径
job.addCacheFile(new URI("hdfs://hadoop102:8020/cache/pd.txt"));
7、MapReduce开发总结
- 输入数据接口:InputFormat
- 逻辑处理接口:Mapper
- Partitioner分区
- Comparable排序
- Combiner合并
- 逻辑处理接口:Reducer
- 输出数据接口:OutputFormat
四、Hadoop数据压缩
1、概述
1)优缺点
- 压缩的优点:以减少磁盘IO、减少磁盘存储空间。
- 压缩的缺点:增加CPU开销。
2)压缩原则
- 运算密集型的Job,少用压缩
- IO密集型的Job,多用压缩
2、MR支持的压缩编码
1)压缩算法对比介绍

-
2)压缩性能的比较

3、压缩方式选择
压缩方式选择时重点考虑:压缩/解压缩速度、压缩率(压缩后存储大小)、压缩后是否可以支持切片。
1)Gzip压缩
优点:压缩率比较高;
缺点:不支持Split;压缩/解压速度一般;
2) Bzip2压缩
优点:压缩率高;支持Split;
缺点:压缩/解压速度慢。
3)Lzo压缩
优点:压缩/解压速度比较快;支持Split;
缺点:压缩率一般;想支持切片需要额外创建索引。
4)Snappy压缩
优点:压缩和解压缩速度快;
缺点:不支持Split;压缩率一般;
5)压缩位置选择
压缩可以在MapReduce作用的任意阶段启用
-
输入端采用压缩:
数据量小于块大小,重点考虑压缩和解压缩速度比较快的 LZO/Snappy
数据量非常大,重点考虑支持切片的 Bzip2 和 LZOMapper输出采用压缩:
为了减少MapTask和ReduceTask之间的网络IO。重点考虑压缩和解压缩快的LZO、Snappy。Reducer输出采用压缩:
如果数据永久保存,考虑压缩率比较高的Bzip2和Gzip。
如果作为下一个MapReduce输入,需要考虑数据量和是否支持切片。
4、压缩参数配置
为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器
| 压缩格式 | 对应的编码/解码器 |
| DEFLATE | org.apache.hadoop.io.compress.DefaultCodec |
| gzip | org.apache.hadoop.io.compress.GzipCodec |
| bzip2 | org.apache.hadoop.io.compress.BZip2Codec |
| LZO | com.hadoop.compression.lzo.LzopCodec |
| Snappy | org.apache.hadoop.io.compress.SnappyCodec |
要在Hadoop中启用压缩,可以配置如下参数

更多推荐
所有评论(0)