0202-MapReduce
第一章 MapReduce概述
1.1 MapReduce定义
MapReduce是一个分布式运算程序的编程框架,是用户开发"基于Hadoop的数据分析应用"的核心框架
MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上.
1.2 MapReduce优缺点
1.2.1 优点
- 良好的扩展性
当计算资源不能得到满足时,可以通过简单的增加机器来扩展它的计算能力- 高容错性
一台机器挂了,它可以把上面的计算任务转移到另外一个台节点上运行,不至于这个任务运行失败.而且这个过程完全由Hadoop内部完成.- 适合PB级以上海量数据的离线处理
1.2.2 缺点
- 不擅长实时计算
- 不擅长流式计算
流式计算的数据数据是动态的,而MapReduce的输入数据集是静态的.MapReduce自身设计特点决定了数据源必须是静态的.- 不擅长DAG计算
多个应用程序存在依赖关系,后一个引用依赖前一个的输出.MapReduce只能将中间结果写入磁盘,再去从磁盘中读取,这样会造成大量的磁盘IO,性能非常低下.
1.3 MapReduce核心思想
- 分布式的运算程序往往需要分成至少2个阶段。
- 第一个阶段的MapTask并发实例,
完全并行
运行,互不相干。- 第二个阶段的ReduceTask并发实例
互不相干
,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出。
- MapReduce编程模型
只能包含一个Map阶段和一个Reduce阶段
,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行。
若干问题
- 文件如何切分
- 数据如何变成KV对进入MapTask
- MapTask的个数
- MapTask如何工作
- ReduceTask的个数
- ReduceTask如何工作
- 数据如何输出
1.4 MapReduce进程
一个完整的MapReduce程序在分布式运行时有三个实例进程:
- MrAppMaster
负责整个程序的过程调度及状态协调- MapTask
负责MAP阶段整个数据的处理流程- ReduceTask
负责Reduce阶段整个数据的处理流程
1.5 WordCount源码
1.6 常用数据序列化类型
Java类型 | Hadoop Writable类型 |
---|---|
Boolean | BooleanWritable |
Byte | ByteWritable |
Int | IntWritable |
Float | FloatWritable |
Long | LongWritable |
Double | DoubleWritable |
String | Text |
Map | MapWritable |
Array | ArrayWritable |
1.7 MapReduce编程规范
编写MapReduce程序分为三个部分: Mapper,Reducer,Driver
- Mapper阶段
(1)用户自定义的Mapper要继承自己的父类
(2)Mapper的输入数据是KV对的形式(KV的类型可自定义)
(3)Mapper中的业务逻辑写在map()方法中
(4)Mapper的输出数据是KV对的形式(KV的类型可自定义)
(5)map()方法(maptask进程)对每一个<K,V>调用一次
- Reduce阶段
(1)用户自定义的Reducer要继承自己的父类
(2)Reducer的输入数据类型对应Mapper的输出数据类型,也是KV
(3)Reducer的业务逻辑写在reduce()方法中
(4)Reducetask进程对每一组相同k的<k,v>组调用一次reduce()方法
- Driver阶段
相当于Yarn集群的客户端,用于提交整个程序到YARN集群,提交的是封装了MapReduce程序相关运行参数的job对象
1.8 WordCount案例实操
- 编写Mapper类
public class WordCountMapper extends Mapper<LongWritable, Text,Text, IntWritable> {
Text keyOut = new Text();
IntWritable valueOut = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1. 获取一行;
String line = value.toString();
// 2. 切割
String[] words = line.split(" ");
// 3.输出
for (String word : words) {
keyOut.set(word);
context.write(keyOut,valueOut);
}
}
}
- 编写Reducer
public class WordCountReducer extends Reducer<Text, IntWritable,Text, LongWritable> {
Text keyOut = new Text();
LongWritable valueOut = new LongWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
// 1. 累加求和
long sum = 0;
for (IntWritable value : values) {
sum = sum + value.get();
}
// 2. 输出
keyOut.set(key);
valueOut.set(sum);
context.write(keyOut,valueOut);
}
}
- 编写Driver
public class WordCountDriver {
public static void main(String[] args) throws Exception {
// 1 获取配置信息以及封装任务
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 2 设置jar加载路径
job.setJarByClass(WordCountDriver.class);
// 3 设置map和reduce类
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
// 4 设置map输出
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5 设置最终输出kv类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
// 6 设置输入和输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7 提交
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
第二章 Hadoop序列化
2.1 序列化概述
2.1.1 什么是序列化
- 序列化就是把内存中对象,转换成字节序列(或者其他数据传输协议) 以便于存储到磁盘(持久化) 和 网络传输
- 反序列化就是将收到字节序列(或者其他数据传输协议) 或者是磁盘的持久化数据,转换成内存中对象
2.1.2 为什么要序列化
一般来说, "活的"对象只有生存在内存里,关机断电就没有了. 而且"活的"对象只能由本地的进程使用, 不能发送到网络上的另外一台计算机. 然而序列化可以存储"活的"对象,可以将"活的"对象发送到远程计算机
整个MapReduce中的数据传递并不是以对象的方式进行传递的.因为大数据背景下,如果以对象的方式传递数据那么会非常消耗内存, 因此进行序列化之后, 再进行数据传递时 , 内存不够时, 可以将对象存储在磁盘上, 等到用到该对象的时候, 再从磁盘里面反序列化回来. .
2.1.3 为什么不用Java自带的序列化
Java的序列化是一个重量级序列化框架(Serializable), 一个对象被序列化后, 会附带很多额外的信息,不便于在网络中高效传输.
所以Hadoop自己开发了一套序列化机制(Writable)
2.2 自定义bean对象实现序列化接口
具体实现bean对象序列化步骤如下:
- 必须实现Writable接口
- 重写序列化方法
- 重写反序列化方法
- 注意序列化的顺序和反序列化的顺序完全一致
- 要想把结果显示在文件中, 重写toString()方法
- 如果需要将自定义的bean放在key中传输, 则还需要实现Comparable接口(直接实现WritableComparable接口即可).因为MapReduce中的shuffle阶段要求key是可排序的
public class FlowBean implements WritableComparable<FlowBean> {
private long upFlow;
private long downFlow;
private long sumFlow;
public long getUpFlow() {
return upFlow;
}
public void setUpFlow(long upFlow) {
this.upFlow = upFlow;
}
public long getDownFlow() {
return downFlow;
}
public void setDownFlow(long downFlow) {
this.downFlow = downFlow;
}
public long getSumFlow() {
return sumFlow;
}
public void setSumFlow(long sumFlow) {
this.sumFlow = sumFlow;
}
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}
public int compareTo(FlowBean o) {
// 从大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
@Override
public String toString() {
return "FlowBean{" +
"upFlow=" + upFlow +
", downFlow=" + downFlow +
", sumFlow=" + sumFlow +
'}';
}
}
2.3 序列化案例实操
第三章 MapReduce框架原理
3.0 MapReduce工作全流程
3.1 InputFormat数据输入
InputFormat 主要完成两件事:
- 数据切片
List< InputSplit > getSplits(JobContext context)- 将文件中的数据转换成KV对传递给MapTask
RecordReader<K,V> createRecordReader(InputSplit split,TaskAttemptContext context)
3.1.1 切片与MapTask并行度决定机制
两个概念
数据块: Block是HDFS物理上把数据分成一块一块
数据切片: 数据切片知识逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储
问题引出:
-
到底要启动几个MapTask ??
1G的数据,启动8个MapTask,可以提高集群的并发处理能力。 那么1K的数据,也启动8个MapTask,会提高集群性能吗? MapTask并行任务是否越多越好呢?哪些因素影响了MapTask并行度?
- 一个Job的Map阶段的并行度(启动几个MapTask)由客户端在提交Job时的切片数决定.
- 每一个Split切片分配一个MapTask并行实例处理
问题引出:
- 怎么切 ??
- 默认情况下,切片大小=BlockSize
- 切片时不考虑数据集整体,而是逐个针对每个文件单独切片.
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());
- FileInputFormat切片源码
public List<InputSplit> getSplits(JobContext job) throws IOException {
StopWatch sw = new StopWatch().start();
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
long maxSize = getMaxSplitSize(job);
// generate splits
List<InputSplit> splits = new ArrayList<InputSplit>();
List<FileStatus> files = listStatus(job);
// 1.遍历每一个文件
for (FileStatus file : files) {
Path path = file.getPath();
// 2.获取文件大小
long length = file.getLen();
if (length != 0) {
BlockLocation[] blkLocations;
if (file instanceof LocatedFileStatus) {
blkLocations = ((LocatedFileStatus) file).getBlockLocations();
} else {
FileSystem fs = path.getFileSystem(job.getConfiguration());
blkLocations = fs.getFileBlockLocations(file, 0, length);
}
if (isSplitable(job, path)) {
long blockSize = file.getBlockSize();
// 3. 计算切片大小 默认情况下 = blockSize
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
long bytesRemaining = length;
// 4. 开始切,第一个切片0-128M ,第二个切片128-256M
// 每次切片时,判断切完后剩下的部分是否大于切片的1.1倍,不大于就停止切分
while (((double) bytesRemaining) / splitSize > SPLIT_SLOP) {
int blkIndex = getBlockIndex(blkLocations, length - bytesRemaining);
splits.add(makeSplit(path, length - bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
bytesRemaining -= splitSize;
}
if (bytesRemaining != 0) {
int blkIndex = getBlockIndex(blkLocations, length - bytesRemaining);
splits.add(makeSplit(path, length - bytesRemaining, bytesRemaining,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
}
} else { // not splitable
splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(),
blkLocations[0].getCachedHosts()));
}
} else {
//Create empty hosts array for zero length files
splits.add(makeSplit(path, 0, length, new String[0]));
}
}
// Save the number of input files for metrics/loadgen
job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
sw.stop();
if (LOG.isDebugEnabled()) {
LOG.debug("Total # of splits generated by getSplits: " + splits.size()
+ ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
}
// 5. InputSplit只记录了切片的元数据信息,比如其实位置,长度以及所在节点的列表等.
return splits;
// 提交切片规划文件(job.split,job.xml)到YARN上,
// YARN上的MrAppMaster就可以根据切片规划文件计算需要开启的MapTask的个数
}
3.1.3 FileInputFormat
- FileInputFormat切片机制
- 简单地按照文件的内容长度进行切片
- 切片大小,默认等于BlockSize
切片大小设置(三者中间):
(1) maxsize参数如果调的比BlockSize小,则会让切片变小, 此时的切片大小就是maxsize
(2) minsize 参数如果调的比BlockSize大,则会让切片变大, 此时的切片大小就是minsize
- 当切完后剩下的文件大小大于切片大小的1.1 倍时再继续切分,否则不再切分(防止生成过多的小文件)
- 切片时不考虑数据集整体,而是针对每一个文件单独切片
- FileInputFormat的KV对的形成
- 数据的偏移量作为Key
- 每一行数据为作为Value
3.1.4 FileInputFormat的实现类
实现类 | 切片机制 | kv的形成 |
---|---|---|
TextInputFormat | 使用父类 | k:偏移量; v:这行数据 |
KeyValueTextInputFormat | 使用父类 | 每行数据被指定的分隔符(默认\t)分割成KV |
NLineInputFormat | 切片数=输入文件的总行数 / N | k:偏移量; v:这行数据 |
CombineFileInputFormat | (小文件过多的场景) |
代码中的使用:
job.setInputFormatClass(KeyValueTextInputFormat.class);
3.1.5 自定义InputFormat
需求:
将多个小文件合并成一个SequenceFile文件(SequenceFile文件是Hadoop用来存储二进制形式的key-value对的文件格式),SequenceFile里面存储着多个文件,存储的形式为文件路径+名称为key,文件内容为value。
步骤
- 自定义一个类继承FileInputFormat
- 改写RecordReader, 实现一次读取一个完整文件封装为KV
- 在输出是使用SequenceFileOutputFormat输出合并文件
具体实现:
- 自定义一个类继承FileInputFormat
(1) 重写isSplitable()方法, 返回false 表示不可切割
(2) 重写createRecordReader(),创建自定义的RecordReader对象,并初始化- 改写RecordReader,实现一次读取一个完整文件并封装为KV
(1) 采用IO流一次读取一个文件输出到Value中, 因为设置了不可切片, 最终把所有文件都封装到了value中.
(2) 获取文件路径信息+名称, 并设置key- 设置Driver
(1) job.setInputFormatClass(WholeFileInputFormat.class);
(2) job.setOutputFormatClass(SequenceFileOutputFormat.class);
上代码:
- 自定义InputFormat
public class WholeFileInputFormat extends FileInputFormat<Text, ByteWritable> {
@Override
protected boolean isSplitable(JobContext context, Path filename) {
return false;
}
public RecordReader<Text, ByteWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
WholeRecordReader recordReader = new WholeRecordReader();
recordReader.initialize(split, context);
return recordReader;
}
}
- 自定义RecordReader
public class WholeRecordReader extends RecordReader<Text, ByteWritable> {
private Configuration configuration;
private FileSplit split;
private boolean isProgress = true;
private BytesWritable value = new BytesWritable();
private Text key = new Text();
public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
this.split = (FileSplit) split;
configuration = context.getConfiguration();
}
public boolean nextKeyValue() throws IOException, InterruptedException {
if (isProgress) {
// 1. 定义缓冲区
byte[] contents = new byte[(int) split.getLength()];
FileSystem fs = null;
FSDataInputStream fis = null;
// 2. 获取文件系统
Path path = split.getPath();
fs = path.getFileSystem(configuration);
// 3. 读取数据
fis = fs.open(path);
// 4. 读取文件内容
IOUtils.readFully(fis, contents, 0, contents.length);
// 5. 输出文件内容
value.set(contents, 0, contents.length);
// 6. 获取文件路径及名称
String name = split.getPath().toString();
// 7. 设置输出的key值
key.set(name);
IOUtils.closeStream(fis);
isProgress = false;
return true;
}
return false;
}
public Text getCurrentKey() throws IOException, InterruptedException {
return null;
}
public ByteWritable getCurrentValue() throws IOException, InterruptedException {
return null;
}
public float getProgress() throws IOException, InterruptedException {
return 0;
}
public void close() throws IOException {
}
}
3.2 MapTask工作机制
3.2.1 Read阶段
MapTask通过用户编写的RecordReader,从输入InputSplit中解析出一个个Key/value
3.2.2 Map阶段
将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value。
3.2.3 Collect阶段
map() 函数中context.write(key,value) ------ 数据被写到哪里去了
在用户编写map()函数中,当数据处理完成后,一般会调用
OutputCollector.collect()
输出结果。在该函数内部,它会将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区
中。
3.2.4 Spill阶段
即“溢写”,当环形缓冲区满后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。
溢写详情:
步骤1:
利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。
步骤2:
按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。
步骤3:
将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中。
3.2.5 Combine阶段
当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。
文件合并细节
在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮归并排序的方式。每轮合并io.sort.factor(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。
让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。
3.3 Shuffle机制
3.3.1 认识shuffle
这个Shuffle 涉及三个阶段,从MapTask开始从前往后依次是:
(1) partitioner(可选)
(2) WritableComparable(必须)
(3) Combiner(可选)
为什么叫shuffle,为什么叫混洗
每个ReduceTask的输入都来及许多map任务
3.3.2 Partition分区
- 分区是干嘛的
给数据打标签,告诉他应该去往哪个reduceTask
- 什么时候会用到分区
要求将统计结果按照条件输出到不同文件中(分区)。比如:将统计结果按照手机归属地不同省份输出到不同文件中(分区)
- 默认partitioner分区实现
public class HashPartitioner<K, V> extends Partitioner<K, V> {
/** Use {@link Object#hashCode()} to partition. */
public int getPartition(K key, V value, int numReduceTasks) {
// & : 防止hashcode是负数
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
默认分区是根据key的hashCode对reduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区.如果采用默认的分区实现,即HashParitioner的话,那么 ReduceTask的个数就是分区个数.但是原则上,这两者是没有关系的.
- 自定义Partitioner步骤
(1) 自定义类继承Partitioner ,重写getPartition()方法
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
@Overrider
public int getPartition(Text key, FlowBean value, int numPartitions) {
// 1 获取电话号码的前三位
String preNum = key.toString().substring(0, 3);
int partition = 4;
// 2 判断是哪个省
if ("136".equals(preNum)) {
partition = 0;
}else if ("137".equals(preNum)) {
partition = 1;
}else if ("138".equals(preNum)) {
partition = 2;
}else if ("139".equals(preNum)) {
partition = 3;
}
return partition;
}
}
(2) 在job驱动中,设置自定义partitioner
job.setPartitionerClass(ProvincePartitioner.class)
(3) 自定义partition后,要根据自定义partitioner的逻辑设置相应数量的reduce task
job.setNumReduceTasks(5);
- 注意:
如果reduceTask的数量 > getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
如果1<reduceTask的数量 < getPartition的结果数,则有一部分分区数据无处安放,会Exception;
如果reduceTask的数量=1,则不管mapTask端输出多少个分区文件,最终结果都交给这一个reduceTask,最终也就只会产生一个结果文件 part-r-00000;
例如:假设自定义分区数为5,则
(1)job.setNumReduceTasks(1);会正常运行,只不过会产生一个输出文件
(2)job.setNumReduceTasks(2);会报错
(3)job.setNumReduceTasks(6);大于5,程序会正常运行,会产生空文件
- 可以看出, ReduceTask的数量并非由输入数据的大小决定,也不是由分区数量决定,相反是独立指定的.而独立指定的经验法则: ‘目标reducer保持在每个运行5分钟左右, 且产生至少一个HDFS块的输出比较合适’.
- 相比之下,MapTask的数量是由输入文件切片的数量决定的.
3.3.3 Partition案例实操
3.3.4 WritableComparable排序
- 排序概述
- 排序是MapReduce框架中最重要的操作之一。
Map Task和Reduce Task均会对数据(按照key)进行排序
。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。- 对于Map Task,它会将处理的结果暂时放到一个缓冲区中,当缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次排序,并将这些有序数据写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行一次合并,以将这些文件合并成一个大的有序文件。
- 对于Reduce Task,它从每个Map Task上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则放到磁盘上,否则放到内存中。如果磁盘上文件数目达到一定阈值,则进行一次合并以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据写到磁盘上。当所有数据拷贝完毕后,Reduce Task统一对内存和磁盘上的所有数据进行一次合并。
- 排序的分类
- 部分排序:
MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部排序。- 全排序: 如何用Hadoop产生一个全局排序的文件?最简单的方法是使用一个分区。但该方法在处理大型文件时效率极低,因为一台机器必须处理所有输出文件,从而完全丧失了MapReduce所提供的并行架构。
替代方案:首先创建一系列排好序的文件;其次,串联这些文件;最后,生成一个全局排序的文件。主要思路是使用一个分区来描述输出的全局排序。例如:可以为上述文件创建3个分区,在第一分区中,记录的单词首字母a-g,第二分区记录单词首字母h-n,
第三分区记录单词首字母o-z。- 辅助排序:(GroupingComparator分组) Mapreduce框架在记录到达reducer之前按键对记录排序,但键所对应的值并没有被排序。甚至在不同的执行轮次中,这些值的排序也不固定,因为它们来自不同的map任务且这些map任务在不同轮次中完成时间各不相同。一般来说,大多数MapReduce程序会避免让reduce函数依赖于值的排序。但是,有时也需要通过特定的方法对键进行排序和分组等以实现对值的排序。
- 自定义排序
bean对象最为key传输, 需要实现WritableComparable接口并重写compareTo方法
@Override
public int compareTo(FlowBean o) {
int result;
// 按照总流量大小,倒序排列
if (sumFlow > bean.getSumFlow()) {
result = -1;
}else if (sumFlow < bean.getSumFlow()) {
result = 1;
}else {
result = 0;
}
return result;
}
3.3.5 WritableComparable案例实操(全排序)
3.3.6 WritableComparable案例实操(分区内排序)
3.3.7 Combiner合并
- Combiner
- combiner 是MR程序中Mapper和Reducer之外的一种组件
- combiner组件的父类就是Reducer
- combiner和reducer的区别在于运行的位置
a. Combiner是在每一个MapTask所外节点运行
b. Reducer是接收全局所有的Mapper的输出结果- combiner的意义就是对每一个MapTask的输出进行局部汇总, 以减少网络传输量
- combiner能够应用的前提是不能影响最终的业务逻辑, 而且, combiner的输出kv应该跟Reducer的输入KV类型对应起来.
- 自定义Combiner
定义一个Combiner继承Reducer,重写reducer方法
public class WordcountCombiner extends Reducer<Text, IntWritable, Text,IntWritable>{
@Override
protected void reduce(Text key, Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {
// 1 汇总操作
int count = 0;
for(IntWritable v :values){
count += v.get();
}
// 2 写出
context.write(key, new IntWritable(count));
}
}
...
job.setCombinerClass(WordcountCombiner.class);
3.3.8 Combiner案例实操
3.3.9 GroupingComparator分组
针对Reducer端对key进行分组. 应用于: 在接收的key为bean对象时, 想
让一个或几个字段相同的key进入到同一个reduce方法时
, 可以采用分组排序.简单来说就是TOPN
所谓的分组
可以看出Reduce是先排序, 后分组的
要对Reduce阶段的数据根据一个或者几个字段进行分组
自定义分组器:
(1) 自定义类继承WritableComparator
public class OrderGroupingComparator extends WritableComparator {
}
(2) 重写compare()方法
@Override
public int compare(WritableComparable a, WritableComparable b) {
// 比较的业务逻辑
return result;
}
(3) 创建一个无参构造, 将比较对象的类传递给父类(反序列化时, 需要反射调用无参构造函数,如果没有,会报空指针异常)
protected OrderGroupingComparator() {
super(OrderBean.class, true);
}
// 无参构造的必要性
// 反序列化时, 需要反射调用无参构造函数,如果没有,会报空指针异常
3.3.10 GroupingComparator分组案例实操(TopN)
-
需求:
-
具体实现
(1)利用“订单id和成交金额”作为key,可以将Map阶段读取到的所有订单数据按照id升序排序,如果id相同再按照金额降序排序,发送到Reduce。
(2)在Reduce端利用groupingComparator将订单id相同的kv聚合成组,然后取第一个即是该订单中最贵商品。
- 上代码
- Bean
public class OrderBean implements WritableComparable<OrderBean> {
private String orderId;
private double price;
public OrderBean() {
super();
}
public OrderBean(String orderId, double price) {
this.orderId = orderId;
this.price = price;
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public int compareTo(OrderBean o) {
// 二次排序, 先按照订单号升序, 再按照价格降序
int compare = this.orderId.compareTo(o.orderId);
if (compare == 0) {
return Double.compare(o.price, this.price);
} else {
return compare;
}
}
public void write(DataOutput out) throws IOException {
out.writeUTF(orderId);
out.writeDouble(price);
}
public void readFields(DataInput in) throws IOException {
orderId = in.readUTF();
price = in.readDouble();
}
}
- Map
public class OrderMapper extends Mapper<LongWritable, Text, OrderBean, NullWritable> {
private OrderBean keyOut = new OrderBean();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] fields = line.split("\t");
keyOut.setOrderId(fields[0]);
keyOut.setPrice(Double.parseDouble(fields[2]));
context.write(keyOut, NullWritable.get());
}
}
- GroupingComparator
public class OrderComparator extends WritableComparator {
public OrderComparator() {
super(OrderBean.class, true);
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
OrderBean oa = (OrderBean) a;
OrderBean ob = (OrderBean) b;
// orderId相同就为一组
return oa.getOrderId().compareTo(ob.getOrderId());
}
}
- Reduce
public class OrderReducer extends Reducer<OrderBean, NullWritable, OrderBean, NullWritable> {
@Override
protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
context.write(key, NullWritable.get());
}
}
- Reduce取Top2
public class OrderReducer extends Reducer<OrderBean, NullWritable, OrderBean, NullWritable> {
@Override
protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
Iterator<NullWritable> iterator = values.iterator();
for (int i = 0; i < 2; i++) {
if (iterator.hasNext()) {
context.write(key, iterator.next());
}
}
}
}
- Driver
public class OrderDriver {
public static void main(String[] args) throws Exception {
// 1 获取配置信息
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 2 设置jar包加载路径
job.setJarByClass(OrderDriver.class);
// 3 加载map/reduce类
job.setMapperClass(OrderMapper.class);
job.setReducerClass(OrderReducer.class);
// 4 设置map输出数据key和value类型
job.setMapOutputKeyClass(OrderBean.class);
job.setMapOutputValueClass(NullWritable.class);
// 5 设置最终输出数据的key和value类型
job.setOutputKeyClass(OrderBean.class);
job.setOutputValueClass(NullWritable.class);
// 6 设置输入数据和输出数据路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 8 设置reduce端的分组
job.setGroupingComparatorClass(OrderComparator.class);
// 7 提交
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
3.4 ReduceTask工作机制
3.4.1 Copy阶段
ReduceTask从每个MapTask上远程拷贝一片数据, 并针对某一片数据, 如果其大小超过一定的阈值 , 则小道磁盘上, 否则直接放到内存中.
3.4.2 Merge阶段
在远程拷贝数据的同时, ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并, 以防止内存使用过多或磁盘上文件过多.
3.4.3 Sort阶段
按照MapReduce语义, 用户编写reduce()函数输入数据是按key进行聚集(分组)的一组数据. 为了将key相同的数据聚集在一起, Hadoop采用了基于排序的策略. 以排序的手段达到分组的目的. 由于各个MapTask已经实现对自己处理结果进行了局部排序, 因此, ReduceTask只需对所有数据进行一次归并排序即可.
3.4.4 Reduce阶段
reduce() 函数将计算结果写出
3.5 OutPutFormat数据输出
3.5.1 OutputFormat接口实现类
- 文本输出TextOutputFormat
- SequenceFileOutPutFormat
将SequenceFileOutputFormat输出作为后续MapReduce任务的输入, 这便是一种好的输出格式, 因为它的格式经凑, 容易被压缩 - 自定义OutputFormat
3.5.2 自定义OutputFormat
(1) 自定义一个类继承OutPutFormat
(2) 改写RecordWriter,具体改写输出数据的方法write()
3.6 Join多种应用
3.6.1 Reduce Join
- Map端的主要工作:
为来自不同表或文件的kv对,打标签以区别不同的来源的记录, 然后用连接字段作为key, 其余部分和新加的标志作为value, 最后进行输出- Reduce端的主要工作
在reduce端以连接字段作为key的分组已经完成, 我们只需要在每一个分组当中将那些来源于不同文件的记录(map端已经打标签了)分开,最后进行合并就ok了.
3.6.2 Reduce Join案例实操
-
需求
-
具体实现
-
上代码
- 创建商品和订合并后的Bean类
public class TableBean implements WritableComparable<TableBean> {
private String orderId;
private String pId;
private int amount;
private String pname;
private String flag;
public TableBean() {
super();
}
public TableBean(String orderId, String pId, int amount, String pname, String flag) {
super();
this.orderId = orderId;
this.pId = pId;
this.amount = amount;
this.pname = pname;
this.flag = flag;
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getpId() {
return pId;
}
public void setpId(String pId) {
this.pId = pId;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
public String getFlag() {
return flag;
}
public void setFlag(String flag) {
this.flag = flag;
}
public int compareTo(TableBean o) {
return this.pId.compareTo(o.getpId());
}
public void write(DataOutput out) throws IOException {
out.writeUTF(orderId);
out.writeUTF(pId);
out.writeInt(amount);
out.writeUTF(pname);
out.writeUTF(flag);
}
public void readFields(DataInput in) throws IOException {
this.orderId = in.readUTF();
this.pId = in.readUTF();
this.amount = in.readInt();
this.pname = in.readUTF();
this.flag = in.readUTF();
}
@Override
public String toString() {
return "TableBean{" +
"orderId='" + orderId + '\'' +
", pId='" + pId + '\'' +
", amount=" + amount +
", pname='" + pname + '\'' +
", flag='" + flag + '\'' +
'}';
}
}
- 编写TableMapper类
public class TableMapper extends Mapper<LongWritable, Text, Text, TableBean> {
String name;
TableBean bean = new TableBean();
Text keyOut = new Text();
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 1. 获取输入文件切片
FileSplit split = (FileSplit) context.getInputSplit();
// 2. 获取输入文件名称
name = split.getPath().getName();
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1. 获取输入数据
String line = value.toString();
// 2. 不同文件分别处理
if (name.startsWith("order")) {
// 订单表处理
// 2.1 切割
String[] fields = line.split("\t");
// 2.2 封装bean对象
bean.setOrderId(fields[0]);
bean.setpId(fields[1]);
bean.setAmount(Integer.parseInt(fields[2]));
bean.setPname("");
bean.setFlag("order");
keyOut.set(fields[1]);
} else {
// 产品表处理
// 2.3 切割
String[] fields = line.split("\t");
// 2.4 封装bean对象
bean.setpId(fields[0]);
bean.setPname(fields[1]);
bean.setFlag("pd");
bean.setAmount(0);
bean.setOrderId("");
keyOut.set(fields[0]);
}
// 3. 写出
context.write(keyOut, bean);
}
}
- 编写TableReducer类
public class TableRedcuer extends Reducer<Text, TableBean, TableBean, NullWritable> {
@Override
protected void reduce(Text key, Iterable<TableBean> values, Context context) throws IOException, InterruptedException {
// 1.准备好存储订单的集合
ArrayList<TableBean> orderBeans = new ArrayList<TableBean>();
// 2. 准备bean对象
TableBean pdBean = new TableBean();
for (TableBean bean : values) {
if ("order".equals(bean.getFlag())) {
// 订单表(一对多,多的一方)
TableBean orderBean = new TableBean();
try {
BeanUtils.copyProperties(orderBean, bean);
} catch (Exception e) {
e.printStackTrace();
}
orderBeans.add(orderBean);
} else {
// 产品表(一对多,一的一方)
try {
BeanUtils.copyProperties(pdBean, bean);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 3. 拼接表
for (TableBean orderBean : orderBeans) {
orderBean.setPname(pdBean.getPname());
// 4. 写出
context.write(orderBean, NullWritable.get());
}
}
}
ReduceJoin缺点及解决方案
- 缺点
这种方式中, 合并的操作是在Reduce阶段完成, Reduce端的处理压力太大, Map节点的运算负载则很低, 资源利于率不高, 且在reduce阶段极易产生数据倾斜- 解决方案
Map端实现数据合并
3.6.3 Map Join
- 使用场景
Map Join适用于小表Join 大表的场景- 优点
思考:在Reduce端处理过多的表,非常容易产生数据倾斜。怎么办?
在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜。- 具体办法:采用DistributedCache
(1)在Mapper的setup阶段,将文件读取到缓存集合中。
(2)在驱动函数中加载缓存。
// 缓存普通文件到Task运行节点。
job.addCacheFile(new URI(“file://e:/cache/pd.txt”));
3.6.4 Map Join案例实操
具体实现
上代码
- Mapper
public class DistributedCacheMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
Map<String, String> pdMap = new HashMap<String, String>();
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 1. 获取缓存的文件
URI[] cacheFiles = context.getCacheFiles();
String path = cacheFiles[0].getPath().toString();
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path), "UTF-8"));
String line;
while (StringUtils.isNotEmpty(line = reader.readLine())) {
// 2. 切割
String[] fields = line.split("\t");
// 3. 缓存数据到集合
pdMap.put(fields[0],fields[1]);
}
// 4. 关流
reader.close();
}
Text keyOut = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1. 获取大表的一行
String line = value.toString();
// 2. 截取大表信息
String[] fields = line.split("\t");
// 3. 获取产品ID
String pId = fields[1];
// 4. 获取商品名称(从缓存的小表中获取)
String pdName = pdMap.get(pId);
// 5. 拼接
keyOut.set(line + "\t" + pdName);
// 6. 写出
context.write(keyOut, NullWritable.get());
}
}
- Driver
public class DistributedCacheDriver {
public static void main(String[] args) throws Exception {
// 1 获取job信息
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 2 设置加载jar包路径
job.setJarByClass(DistributedCacheDriver.class);
// 3 关联map
job.setMapperClass(DistributedCacheMapper.class);
// 4 设置最终输出数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 5 设置输入输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 6 加载缓存数据(小文件)
job.addCacheFile(new URI("file:///e:/input/inputcache/pd.txt"));
// 7 Map端Join的逻辑不需要Reduce阶段,设置reduceTask数量为0
job.setNumReduceTasks(0);
// 8 提交
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
3.7 数据清洗(ETL)
在运行核心业务MapReduce程序之前,往往要先对数据进行清洗,清理掉不符合用户要求的数据。清理的过程往往只需要运行Mapper程序,不需要运行Reduce程序。
3.7.1 数据清洗案例实操-简单解析版
- 需求: 去除日志中字段长度小于等于11的日志。
public class LogMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
Text keyOut = new Text();
@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. 合格, 则写出
keyOut.set(line);
context.write(keyOut, NullWritable.get());
}
// 2. 具体解析日志的实现
private boolean parseLog(String line, Context context) {
// 1. 截取
String[] fields = line.split(" ");
// 2. 日志长度大于11的为合法
if (fields.length > 11) {
// 系统计数器
context.getCounter("map", "true").increment(1);
return true;
} else {
context.getCounter("map", "false").increment(1);
return false;
}
}
}
public class LogDriver {
public static void main(String[] args) throws Exception {
// 1 获取job信息
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 2 加载jar包
job.setJarByClass(LogDriver.class);
// 3 关联map
job.setMapperClass(LogMapper.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 提交
job.waitForCompletion(true);
}
}
3.8 MapReduce开发总结
- 输入接口: InputFormat
(1) 默认使用的实现类是: TextInputFormat
(2) TextInputFormat的功能逻辑是: 一次读一行文本, 然后将该行的起始偏移量作为key, 行内容作为value返回.
(3) KeyValueTextInputFormat每一行均为一个条记录, 被分隔符分隔为Key,value, 默认分隔符是tab
(4) NLineInputFormat按照指定的行数N来划分切片
(5) CombineTextInputFormat可以把多个小文件合并成一个切片处理, 提高处理效率
(6) 用户还可以自定义InputFormat
- 逻辑处理接口: Mapper
用户根据业务需求实现其中的三个方法: map(), setup(), cleanup()
- Partition分区
(1) 有默认的HashPartitioner, 逻辑是根据key的哈希值和numReduceTask 来返回一个分区号;
(2) 如果业务上有特殊需要, 可以自定义分区
- Comparable排序
(1) 当我们用自定义的对象作为key来输出时, 就必须要实现WritableComparable接口, 重写其中的compareTo方法
(2) 部分排序: 对最终输出的每一个文件进行内部排序
(3) 全排序: 对所有数据进行排序, 通常只有一个Reduce
(4) 二次排序: 排序的条件有两个
- Combiner合并
Combiner合并可以提高程序执行效率, 减少IO传输, 但是使用时必须不能影响原有的业务处理结果
- Reduce端分组----GroupingComparator
在Reduce端对key进行分组. 应用于: 在接收的key作为bean对象时 , 想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法是, 可以采用分组排序.
- 逻辑处理接口: Reducer
用户根据业务需求实现其中的三个方法: reduce(), setup() , cleanup()
- 输出接口: OutputFormat
(1) 默认实现类是TextOutputFormat, 功能逻辑是: 将每一个KV对, 向目标文件输出一行
(2) 将SequenceFileOutputFormat输出作为后续MapReduce任务的输入, 这便是一种好的输出格式.
(3) 用户还可以自定义OutputFormat
第四章 Hadoop数据压缩
4.1 概述
- 压缩概述
压缩技术能够有效减少底层存储系统(HDFS)读写字节数。压缩提高了网络带宽和磁盘空间的效率。在Hadood下,尤其是数据规模很大和工作负载密集的情况下,使用数据压缩显得非常重要。在这种情况下,I/O操作和网络数据传输要花大量的时间。还有,Shuffle与Merge过程同样也面临着巨大的I/O压力。
鉴于磁盘I/O和网络带宽是Hadoop的宝贵资源,数据压缩对于节省资源、最小化磁盘I/O和网络传输非常有帮助。可以在任意MapReduce阶段启用压缩.不过,尽管压缩与解压操作的CPU开销不高,其性能的提升和资源的节省并非没有代价。
- 压缩的策略
压缩mapreduce的一种优化策略:通过压缩编码对mapper或者reducer的输出进行压缩,以减少磁盘IO,提高MR程序运行速度(但相应增加了cpu运算负担)
注意:压缩特性运用得当能提高性能,但运用不当也可能降低性能
- 压缩的原则
(1)运算密集型的job,少用压缩
(2)IO密集型的job,多用压缩
4.2 MR支持的压缩编码
压缩格式 | hadoop自带? | 算法 | 文件扩展名 | 是否可切分 | 换成压缩格式后,原来的程序是否需要修改 |
---|---|---|---|---|---|
DEFLATE | 是,直接使用 | DEFLATE | .deflate | 否 | 和文本处理一样,不需要修改 |
Gzip | 是,直接使用 | DEFLATE | .gz | 否 | 和文本处理一样,不需要修改 |
bzip2 | 是,直接使用 | bzip2 | .bz2 | 是 | 和文本处理一样,不需要修改 |
LZO | 否,需要安装 | LZO | .lzo | 是 | 需要建索引,还需要指定输入格式 |
Snappy | 否,需要安装 | Snappy | .snappy | 否 | 和文本处理一样,不需要修改 |
4.3 压缩方式选择
4.3.1 Gzip
4.3.2 Bzip2
4.3.3 Lzo
4.3.4 Snappy
4.4 压缩位置选择
- 输入端采用压缩
在有大量数据并计划重复处理的情况下,应该考虑对输入进行压缩。然而,你无须显示指定使用的编解码方式。Hadoop自动检查文件扩展名,如果扩展名能够匹配,就会用恰当的编解码方式对文件进行压缩和解压。否则,Hadoop就不会使用任何编解码器。
- Mapper输出采用压缩
当map任务输出的中间数据量很大时,应考虑在此阶段采用压缩技术。这能显著改善内部数据Shuffle过程,而Shuffle过程在Hadoop处理过程中是资源消耗最多的环节。如果发现数据量大造成网络传输缓慢,应该考虑使用压缩技术。可用于压缩mapper输出的快速编解码器包括LZO、LZ4或者Snappy。
注:LZO是供Hadoop压缩数据用的通用压缩编解码器。其设计目标是达到与硬盘读取速度相当的压缩速度,因此速度是优先考虑的因素,而不是压缩率。与gzip编解码器相比,它的压缩速度是gzip的5倍,而解压速度是gzip的2倍。同一个文件用LZO压缩后比用gzip压缩后大50%,但比压缩前小25%~50%。这对改善性能非常有利,map阶段完成时间快4倍。
- Reducer输出采用压缩
在此阶段启用压缩技术能够减少要存储的数据量,因此降低所需的磁盘空间。当mapreduce作业形成作业链条时,因为第二个作业的输入也已压缩,所以启用压缩同样有效。