目录
MapReduce的核心思想
上面的图片展示了四个阶段:Input, Map, Reduce, Output,这四个阶段就是MapReduce的核心过程
Input/Output只是任何系统都有的阶段,现只看Map/Reduce
Map阶段将输入的文件按照业务规则解析为一个个的k-v对,Reduce阶段在Map计算输出的k-v对上进行继续计算,
比方说汇总这些k-v对,计算出有多少个这样的k-v对。它所做的事就是把一件分为多个阶段来做,把一个大的问题分成很多的
小问题,可以将mapreduce看作是一种设计模式,一种解决解决某些场景问题的编程方法。比如在python中就提供了mapreuce
的编程模式
# map/filter 的使用
# 对传入的参数,每一个应用传入的运算
def fun1(x):
return x*2 + 5
list1 = [4, 8, 9, 1, 3]
list2 = map(fun1, list1)
# reduce的应用,reduce函数需要单独导入
# 化简函数
from functools import reduce
f = lambda x, y : x + y
total = reduce(f, list2)
如果说这个问题不是很大,那么利用这个模式在一台计算机上就可以解决这个问题了。
但如果这个如果很大,我们还是要使用这个mapreduce模式,那么这时我们就需要使用很多台计算机去解决这个问题了,
这就要使用到Hadoop MapReduce(分布式的计算框架)
Hadoop MapReduce
Hadoop MapReduce是一个分布式的计算框架,它的核心功能还是利用mapreduce的编程模式计算解决计算的问题。
但是因为它是分布式的,因而引入很多的辅助功能去解决因为分布式所带来的问题,比如文件的存储,网络I/O通信等。
具体过程如下
InputFormat
public abstract class InputFormat<K, V> {
/**
* Logically split the set of input files for the job.
*
* <p>Each {@link InputSplit} is then assigned to an individual {@link Mapper}
* for processing.</p>
*
* <p><i>Note</i>: The split is a <i>logical</i> split of the inputs and the
* input files are not physically split into chunks. For e.g. a split could
* be <i><input-file-path, start, offset></i> tuple. The InputFormat
* also creates the {@link RecordReader} to read the {@link InputSplit}.
*
* @param context job configuration.
* @return an array of {@link InputSplit}s for the job.
*/
public abstract
List<InputSplit> getSplits(JobContext context
) throws IOException, InterruptedException;
/**
* Create a record reader for a given split. The framework will call
* {@link RecordReader#initialize(InputSplit, TaskAttemptContext)} before
* the split is used.
* @param split the split to be read
* @param context the information about the task
* @return a new record reader
* @throws IOException
* @throws InterruptedException
*/
public abstract
RecordReader<K,V> createRecordReader(InputSplit split,
TaskAttemptContext context
) throws IOException,
InterruptedException;
}
InputFormat 常见的接口实现类
TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定义InputFormat等
TextInputFormat
TextInputFormat 是默认的 InputFormat,每条记录是一行输入。键是LongWritable 类型,存储该行在整个文件中的字节偏移量。 值是这行的内容,不包括任何行终止符(换行符合回车符),它被打包成一个 Text 对象
KeyValueTextInputFormat
每一行均为一条记录, 被分隔符(缺省是tab(\t))分割为key(Text),value(Text)。
可以通过 mapreduce.input.keyvaluelinerecordreader.key.value,separator属性来设定分隔符。 它的默认值是一个制表符
NLineInputFormat
通过 TextInputFormat 和 KeyValueTextInputFormat,每个 Mapper 收到的输入行数不同。行数取决于输入分片的大小和行的长度。 如果希望 Mapper 收到固定行数的输入,需要将 NLineInputFormat 作为 InputFormat。与 TextInputFormat 一样, 键是文件中行的字节偏移量,值是行本身。N 是每个 Mapper 收到的输入行数。N 设置为1(默认值)时,每个 Mapper 正好收到一行输入。 mapreduce.input.lineinputformat.linespermap 属性实现 N 值的设定
自定义InputFormat
1-自定义一个类继承FileInputFormat
2-改写RecordReader,实现一次读取一个完整文件封装为KV
3-在输出时使用SequenceFileOutPutFormat输出合并文件
Job提交过程
org.apache.hadoop.mapreduce.Job
waitCompletion(true)
submit() --> new JobSubmitter()
submitJobInternal()
//创建提交数据数据的临时目录
JobSubmissionFiles.getStagingDir(cluster, conf);
// 获取jobid,并创建job路径
JobID jobId = submitClient.getNewJobID();
job.setJobID(jobId);
Path submitJobDir = new Path(jobStagingArea, jobId.toString());
//
copyAndConfigureFiles(job, submitJobDir);
// 计算切片,并形成逻辑切分计划
int maps = writeSplits(job, submitJobDir);
// FileInputFormat
List<InputSplit> splits = input.getSplits(job);
//计算切片大小
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
Math.max(minSize, Math.min(maxSize, blockSize));
//将job信息(切片计划xml)写入提交目录
writeConf(conf, submitJobFile);
// 向yarn提交job
submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
FileInputFormat切片大小的参数配置
在FileInputFormat中,计算切片大小的逻辑: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还大。
FileInputFormat在切分文件时是按文件来切分的,因此不论这个文件的大小,该文件都至少有一个split,但文件小时极易产生
小文件问题这些就会产生大量很小的split,可以通过预处理将小文件合并为大文件或使用CombineTextInputFormat
获取切片信息API
// 根据文件类型获取切片信息,可用于在map中判断读入的是哪个文件
FileSplit inputSplit = (FileSplit) context.getInputSplit();
// 获取切片的文件名称
String name = inputSplit.getPath().getName();
MapTask的个数
MapTask的个数由split的个数决定,因此要特别注意小文件问题
Partitioner
默认是HashPartitioner
public class HashPartitioner<K2, V2> implements Partitioner<K2, V2> {
public HashPartitioner() {
}
public void configure(JobConf job) {
}
public int getPartition(K2 key, V2 value, int numReduceTasks) {
return (key.hashCode() & 2147483647) % numReduceTasks;
}
}
自定义Partitioner
*/
public class YearPartitioner extends Partitioner<CompKey, NullWritable> {
@Override
/**
* @param numPartitions the total number of partitions.
*/
public int getPartition(CompKey compKey, NullWritable nullWritable, int numPartitions) {
int year = compKey.getYear();
if(year < 1993) {
return 0;
}
else if(year < 1996) {
return 1;
}
else {
return 2;
}
}
}
在job驱动中,设置自定义partitioner:
job.setPartitionerClass(CustomPartitioner.class);
自定义partition后,要根据自定义partitioner的逻辑设置相应数量的reduce task
job.setNumReduceTasks(3);
注意:
如果reduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
如果1<reduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会Exception;
如果reduceTask的数量=1,则不管mapTask端输出多少个分区文件,最终结果都交给这一个reduceTask,最终也就只会产生一个结果文件 part-r-00000;
示例
(1)job.setNumReduceTasks(1);会正常运行,只不过会产生一个输出文件
(2)job.setNumReduceTasks(2);会报错
(3)job.setNumReduceTasks(6);大于5,程序会正常运行,会产生空文件
排序
1.部分排序(分区内有序,这是mapreduce框架默认的)
key自动排序
2.全排序
自定义分区(在某个范围内的数据就放到一个分区内)
对key的排序
3.二次排序(定义组合key, 定义新的Map端排序,定义新的Reduce端的 GroupingComparator)
对value的排序。
将value整合到key
Combiner
可以简单的理解为是map端的reduce操作,使用的也是Reducer,可以减少map端输出文件的大小
class WordCountReducerWithCombiner1 extends Reducer<Text, IntWritable, Text, IntWritable> {
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int amount = 0;
for(IntWritable iw : values) {
amount += iw.get();
}
context.write(key, new IntWritable(amount));
}
}
Reduce
// reduceTask的个数可以在Driver中直接设置
job.setNumReduceTasks(4);
注意
(1)reducetask=0 ,表示没有reduce阶段,输出文件个数和map个数一致。
(2)reducetask默认值就是1,所以输出文件个数为一个。
(3)如果数据分布不均匀,就有可能在reduce阶段产生数据倾斜
(4)reducetask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个reducetask。
(5)具体多少个reducetask,需要根据集群性能而定。
(6)如果分区数不是1,但是reducetask为1,是否执行分区过程。答案是:不执行分区过程。因为在maptask的源码中,执行分区的前提是先判断reduceNum个数是否大于1。不大于1肯定不执行
Reduce的过程
Copy阶段(shuffle 阶段)
ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
Merge阶段
在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。
Sort阶段
按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。
Reduce阶段
reduce()函数将计算结果写到HDFS上。
OutputFormat
OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了 OutputFormat接口,常用实现类
TextOutputFormat
默认的输出格式是TextOutputFormat,它把每条记录写为文本行。它的键和值可以是任意类型,因为TextOutputFormat调用toString()方法把它们转换为字符串。
SequenceFileOutputFormat
SequenceFileOutputFormat将它的输出写为一个顺序文件。如果输出需要作为后续 MapReduce任务的输入,这便是一种好的输出格式,因为它的格式紧凑,很容易被压缩。
自定义OutputFormat
为了实现控制最终文件的输出路径,可以自定义OutputFormat。
要在一个mapreduce程序中根据数据的不同输出两类结果到不同目录,这类灵活的输出需求可以通过自定义outputformat来实现。
自定义OutputFormat步骤
(1)自定义一个类继承FileOutputFormat。
(2)改写recordwriter,具体改写输出数据的方法write()。
MapReduce开发
输入数据接口InputFormat
默认使用的实现类是:TextInputFormat
TextInputFormat的功能逻辑是:一次读一行文本,然后将该行的起始偏移量作为key,行内容作为value返回
CombineTextInputFormat可以把多个小文件合并成一个切片处理,提高处理效率。
用户还可以自定义InputFormat。
逻辑处理接口:Mapper
用户根据业务需求实现其中三个方法:map() setup() cleanup ()
Partitioner分区
有默认实现 HashPartitioner,逻辑是根据key的哈希值和numReduces来返回一个分区号;key.hashCode()&Integer.MAXVALUE % numReduces
如果业务上有特别的需求,可以自定义分区。
Comparable排序
当我们用自定义的对象作为key来输出时,就必须要实现WritableComparable接口,重写其中的compareTo()方法。
部分排序:对最终输出的没一个文件进行内部排序。
全排序:对所有数据进行排序,通常只有一个Reduce。
二次排序:排序的条件有两个。
Combiner合并
Combiner合并可以提高程序执行效率,减少io传输。但是使用时必须不能影响原有的业务处理结果。
reduce端分组:Groupingcomparator
reduceTask拿到输入数据(一个partition的所有数据)后,首先需要对数据进行分组,其分组的默认原则是key相同,然后对每一组kv数据调用一次reduce()方法,并且将这一组kv中的第一个kv的key作为参数传给reduce的key,将这一组数据的value的迭代器传给reduce()的values参数。
利用上述这个机制,我们可以实现一个高效的分组取最大值的逻辑。
自定义一个bean对象用来封装我们的数据,然后改写其compareTo方法产生倒序排序的效果。然后自定义一个Groupingcomparator,将bean对象的分组逻辑改成按照我们的业务分组id来分组(比如订单号)。这样,我们要取的最大值就是reduce()方法中传进来key。
逻辑处理接口:Reducer
用户根据业务需求实现其中三个方法: reduce() setup() cleanup ()
输出数据接口:OutputFormat
默认实现类是TextOutputFormat,功能逻辑是:将每一个KV对向目标文本文件中输出为一行。
自定义OutputFormat。