MapReduce概述
Hadoop MapReduce 是一个分布式计算框架,用于编写批处理应用程序。编写好的程序可以提交到 Hadoop 集群上用于并行处理大规模的数据。可以处理像什么单词统计,手机号流量统计啊等。
适用场景
- 数据统计,如:网站的PV、UV统计
- 搜索引擎构建索引
- 海量数据查询
- 复杂数据分析算法实现
不适用场景
- OLAP
- 要求毫秒或秒级返回结果
- 流计算
- 流计算的输入数据集是动态的,而MapReduce是静态的
- DAG计算
- 多个任务之间存在依赖关系,后一个的输入是前一个的输出,构成有向无环图DAG
- 每个MapReduce作业的输出结果都会落盘,造成大量磁盘IO,导致性能非常低下
MapReduce执行过程
先看一个大致过程图:
1. map阶段
每个Mapper任务是一个java进程,它会读取HDFS中的文件,解析成很多的键值对,经过我们覆盖的map方法处理后,转换为很多的键值对再输出。整个Mapper任务的处理过程又可以分为以下几个阶段:
1) 输入切片
读取到hdfs的block后,需要对block进行切片(逻辑上切片)。默认是,一个block对应一个切片,且最后一个切片大小不大于block块大小的1.1倍 则规划为一个切片。切片的数量等于启动的MapTask任务的数量。切片是为了更好地利用集群资源,多台机器并发的去处理一个文件,提高工作效率。
切片的源码逻辑:
protected long computeSplitSize(long blockSize, long minSize, long maxSize) { // blockSize:默认大小是128M(dfs.blocksize) // minSize:默认是1byte(mapreduce.input.fileinputformat.split.minsize) 参数可配置 // maxSize:默认值是Long.MaxValue(mapreduce.input.fileinputformat.split.minsize)参数可配置 // 返回每个切片大小 return Math.max(minSize, Math.min(maxSize, blockSize)); }
每个切片大小逻辑:
- 当 blockSize 在minSize和maxSize之间时, 就取blockSize (所以默认blockSize)
- 当 maxSize < blockSize 时,取maxSize。
- 当 minSize > blockSize 时, 取minSize 。
- 当 minSize < maxSize 时,取 minSize (没人会这么配置)
实际使用中,maxSIze不用调整, 如果要想切片大于blockSize就 把minSize调大与blockSize , 小于blockSize的值就不要调了(一个block还要分几次读取,浪费)
为什么切片大小要默认等于block大小?
源文件300M,切片大小设置为100M,这并不是最优的设置。
我们知道 hadoop是以 块block 来存储文件的 ,请看下图:
一个300M文件,会以3个block来存储到集群上面(3个block可能在不同机器)。当设置切片大小为100M时,也就是说MapTask1会从block1上面读取100M,MapTask2从block1读取28M,然后跳转到block2读取剩下的72M (满足100M),这样会导致从不同的机器上获取,效率不高。
2) 输入格式
切片完成后,然后就对输入片中的记录按照一定的规则解析成键值对。切片输入文件的格式有:基于行的日志文件,二进制格式文件,数据库表等。那么针对不同的数据类型,MapReduce有不同的FileInputFormat接口实现类来读取这些数据,常见的包括:TextInputFormat、KeyValueTextInputForma、NLineInputFormat、CombineTextInputFormat和自定义InputFormat等。
有个默认格式(TextInputFormat )是把每一行文本内容解析成键值对。“键”是每一行的起始位置(单位是字节),“值”是本行的文本内容。
几种输入格式解析
- TextInputFormat:
框架默认输入格式, 按行读取每条记录。输出的键值对: key-该行在整个文件中的起始字节偏移量(LongWritable类型) , val-值是这行的内容,不包括任何行终止符(换行符和回车符),Text类型 。
- ConbineTextInputFormat:
框架默认的TextInputFormat切片机制是对任务按文件规划切开,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,会产生大量的MapTask
应用场景:ConbineTextInputFormat 用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件可以交给一个MapTask处理
虚拟存储切片最大值设置:
ConbineTextInputFormat.setMaxInputSplitSize(job, 4194304); //4M , 注意: 虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值
- KeyValueTextInputFormat:
每一行均为一条记录,被分隔符分割为key,value,可以通过在驱动类中设置conf.set(KeyValueLineRecorder.KEY_VALUE_SEPERATOR, "\t"); 来设定分隔符
默认分隔符是tab(\t)
- NLineInputFormat:
如果使用NLineInputFormat,代表每个map进程处理掉的InputSplit不再按照Block块去划分,而是按照NLineInputFormat指定的行数N来划分。
即输入文件的总行数/N = 切片数,如果不整除,切片数=商+1
3) map方法
然后调用Mapper类中的map方法。上面解析出来的每一个键值对,调用一次map方法。如果有1000个键值对,就会调用1000次map方法。每一次调用map方法会输出零个或者多个键值对。
4) 分区
接下来是按照一定的规则对上面map输出的键值对进行分区。分区是基于键进行的。比如我们的键表示省份(如北京、上海、山东等),那么就可以按照不同省份进行分区,同一个省份的键值对划分到一个区中。默认是只有一个区。分区的数量就是Reducer任务运行的数量。默认只有一个Reducer任务。通过我们指定分区, 会将同一个分区的数据发送到同一个 Reduce 当中进行处理 。
mapreduce中默认的 Partitioner 实现类是 HashPartitioner ,也就是对key(map输出的key)进行hash取模,源码:
public class HashPartitioner<K, V> extends Partitioner<K, V> { /** Use {@link Object#hashCode()} to partition. */ public int getPartition(K key, V value, int numReduceTasks) { // & Integer.MAX_VALUE 就是为了提高 key的特征位数,增加取模的随机性
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks; } }
5) 排序
此阶段是对每个分区中的键值对进行排序。首先,按照键进行排序,对于键相同的键值对,按照值进行排序。比如三个键值对<2,2>、<1,3>、<2,1>,键和值分别是整数。那么排序后的结果是<1,3>、<2,1>、<2,2>。
6) Combiner
Combine的作用是对单个Map的输出进行本地的部分聚合之后再将结果传递给Reduce,以减少网络中的IO开销和Reduce的压力,所以其实际上就是一个局部的Reduce。这个过程是可选的。
2. Reduce阶段
每个Reducer任务是一个java进程。Reducer任务接收Mapper任务的输出,归约处理后写入到HDFS中。主要是下面三个阶段:
- 第一阶段是Reducer任务会主动从Mapper任务复制其输出的键值对,Mapper任务可能会有很多,因此Reducer会复制多个Mapper的输出。
- 第二阶段是把复制到Reducer本地数据,全部进行合并,即把分散的数据合并成一个大的数据,再对合并后的数据排序。
- 第三阶段是对排序后的键值对调用reduce方法,键相等的键值对调用一次reduce方法,每次调用会产生零个或者多个键值对,最后把这些输出的键值对写入到HDFS文件中。
shuffle过程
上面过程只是讲解大概, 其实核心在于shuffle 。分为map阶段的shuffle 和reduce阶段的shuffle。 下图为 官方shuffle图:
Map阶段的shuffle
map阶段经过切片,读取输出格式,map方法等后会生成<k,v>键值对。这些输出会先存到到缓存里面,每个map任务(任务个数跟切片个数有关) 有一个环形缓冲区,用于存储前面的键值对,默认大小100MB(io.sort.mb属性),一旦达到阀值0.8(io.sort.spil l.percent),一个后台线程就把内容写到(spill)Linux本地磁盘中的指定目录(mapred.local.dir)下的新建的一个溢出写文件。当处理结束后,会把所有的溢写文件进行归并排序。
溢写磁盘前,要经过分区, 排序和combine等操作。首先是进行分区, 然后对不同的分区进行排序和combine ,这样就得到了 分区个数 排序好的且combine合并好的文件。最后 由Reduce任务去拉取。
Reduce阶段的shuffle
reducer通过http方式拉取对应分区的输出文件到缓存中,同样缓存占用到达一定阈值后会将数据写到磁盘中, 然后对文件合并,归并排序,根据key进行分组 。这样就得到了相同key的数据,最后经过reduce方法。输出到HDFS 。
到此我们大致 讲完了mapreduce的执行过程了。后面会对每个过程详细分析。