Mapreduce工作流程按照时间顺序包括:
输入分片(input split)、
map阶段、
combiner阶段、
shuffle阶段和
reduce阶段。
1.输入分片(input split):在进行map计算之前,mapreduce会根据输入文件计算输入分片(input split),每个输入分片(input split)针对一个map任务
输入分片(input split)存储的并非数据本身,而是一个分片长度和一个记录数据的位置的数组,输入分片(input split)往往和hdfs的block(块)关系很密切
假如我们设定hdfs的块的大小是64mb,如果我们输入有三个文件,大小分别是3mb、65mb和127mb,那么mapreduce会把3mb文件分为一个输入分片(input split),65mb则是两个输入分片(input split)而127mb也是两个输入分片(input split)
即我们如果在map计算前做输入分片调整,例如合并小文件,那么就会有5个map任务将执行,而且每个map执行的数据大小不均,这个也是mapreduce优化计算的一个关键点。
2.map阶段:
程序员编写好的map函数了,因此map函数效率相对好控制,而且一般map操作都是本地化操作也就是在数据存储节点上进行;
3.combiner阶段:combiner阶段是程序员可以选择的,combiner其实也是一种reduce操作,因此我们看见WordCount类里是用reduce进行加载的。
Combiner是一个本地化的reduce操作,它是map运算的后续操作,主要是在map计算出中间文件前做一个简单的合并重复key值的操作,例如我们对文件里的单词频率做统计,map计算时候如果碰到一个hadoop的单词就会记录为1,但是这篇文章里hadoop可能会出现n多次,那么map输出文件冗余就会很多,因此在reduce计算前对相同的key做一个合并操作,那么文件会变小,这样就提高了宽带的传输效率,毕竟hadoop计算力宽带资源往往是计算的瓶颈也是最为宝贵的资源,但是combiner操作是有风险的,使用它的原则是combiner的输入不会影响到reduce计算的最终输入,
例如:如果计算只是求总数,最大值,最小值可以使用combiner,但是做平均值计算使用combiner的话,最终的reduce计算结果就会出错。
4.shuffle阶段:将map的输出作为reduce的输入的过程就是shuffle了
5.reduce阶段
和map函数一样也是程序员编写的,最终结果是存储在hdfs上的
InputSplit
InputSplit是指分片,在MapReduce当中作业中,作为map task最小输入单位。分片是基于文件基础上出来的而来的概念,通俗的理解一个文件可以切分为多少个片段,每个片段包括了<文件名,开始位置,长度,位于哪些主机>等信息。在MapTask拿到这些分片后,会知道从哪开始读取数据。
Job提交时如何获取到InputSplit
以org.apache.hadoop.mapred包中的FileInputFormat为例(因为该类作为其他文件类型的基类),内部实现了如何获取分片,通过分析代码,以便知晓文件是如何被切片的。
- public InputSplit[] getSplits(JobConf job, int numSplits)
- throwsIOException {
-
- FileStatus[] files = listStatus(job);
-
-
- job.setLong(NUM_INPUT_FILES, files.length);
-
- longtotalSize = 0;
- for(FileStatus file: files) {
- if(file.isDir()) {
- throw new IOException("Not a file: "+ file.getPath());
- }
- totalSize += file.getLen();
- }
-
-
- longgoalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
-
- longminSize = Math.max(job.getLong("mapred.min.split.size", 1),
- minSplitSize);
-
-
- ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);
- NetworkTopology clusterMap = new NetworkTopology();
-
-
- for(FileStatus file: files) {
- Path path = file.getPath();
- FileSystem fs = path.getFileSystem(job);
- longlength = file.getLen();
-
- BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0,length);
-
- if((length != 0) && isSplitable(fs, path)) {
- long blockSize = file.getBlockSize();
-
- long splitSize = computeSplitSize(goalSize,minSize, blockSize);
-
- long bytesRemaining = length;
-
- while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
-
- String[] splitHosts =getSplitHosts(blkLocations,
- length-bytesRemaining, splitSize,clusterMap);
- splits.add(new FileSplit(path,length-bytesRemaining, splitSize,
- splitHosts));
- bytesRemaining -= splitSize;
- }
-
-
- if (bytesRemaining != 0) {
- splits.add(new FileSplit(path,length-bytesRemaining, bytesRemaining,
- blkLocations[blkLocations.length-1].getHosts()));
- }
- } elseif(length != 0) {
-
- String[] splitHosts = getSplitHosts(blkLocations,0,length,clusterMap);
- splits.add(new FileSplit(path, 0, length, splitHosts));
- } else{
-
-
- splits.add(new FileSplit(path, 0, length, new String[0]));
- }
- }
- LOG.debug("Total # of splits: "+ splits.size());
- returnsplits.toArray(newFileSplit[splits.size()]);
- }
通过上述分析,可以知道我们指定一个目录作为job的输入源时,用户指定的MapTask的个数,以及文件总长度,块大小,以及用户指定的最小分片长度会影响到最后可以产生多少个分片,也就是这个Job最后需要执行多少次MapTask。
同时,还可以得知,一个分片是不会跨越两个文件的;一个空的文件也会占用到一个分片;不是每个分片都是等长的;以及一个分片可以跨一个大文件中连续的多个block。
主机列表是什么,如何选择
InputSplit作为一个分片,所包含的的信息中有主机列表这一信息,这不是说这个分片就在这个主机列表上,这是错误的理解。主机列表是指做task的时候,JobTracker会把Task发送到主机列表所在的节点上,由该节点来执行task。
在上面我们已经得出过结论“一个分片可以有多个block”,那么这种这情况下,主机列表就不会覆盖所有block所对应的主机信息,而是根据一种算法来:通过将机架和数据节点引入进来,形成网络拓扑;机架对应的信息中会存储这个机架有这个分片的多少数据量,数据节点对应的节点信息中会存储这个节点有这个分片的多少数据量。根据机架和数据节点这两个信息来排序,会选择出机架列表里包含的了最多数据量的机架,在该机架内选择包含了最多的数据量的数据节点。如果第一个机架的主机列表数量不够,则再从第二个机架内选择数据节点。通过这种形式来选择出最合理的主机列表信息。
另外对应的,如果一个分片只包含一个block,那么就没有上述这么复杂的情况,只要将这个块对应的信息(BlockLocation)中的主机列表信息返回即可。
下面我们来实际分析代码,会通过注释来解释关键的步骤。
- protected String[] getSplitHosts(BlockLocation[] blkLocations,
- longoffset, longsplitSize, NetworkTopology clusterMap)
- throwsIOException {
-
-
- intstartIndex = getBlockIndex(blkLocations, offset);
-
-
- longbytesInThisBlock = blkLocations[startIndex].getOffset() +
- blkLocations[startIndex].getLength() - offset;
-
-
-
-
- if(bytesInThisBlock >= splitSize) {
- returnblkLocations[startIndex].getHosts();
- }
-
-
- longbytesInFirstBlock = bytesInThisBlock;
- intindex = startIndex + 1;
- splitSize -= bytesInThisBlock;
-
-
- while(splitSize > 0) {
- bytesInThisBlock =
- Math.min(splitSize,blkLocations[index++].getLength());
- splitSize -= bytesInThisBlock;
- }
-
- longbytesInLastBlock = bytesInThisBlock;
- intendIndex = index - 1;
-
-
-
-
-
-
-
-
-
- Map <Node,NodeInfo> hostsMap = new IdentityHashMap<Node,NodeInfo>();
- Map <Node,NodeInfo> racksMap = new IdentityHashMap<Node,NodeInfo>();
- String [] allTopos = new String[0];
-
-
-
-
-
- for(index = startIndex; index <= endIndex; index++) {
-
-
-
- if(index == startIndex) {
- bytesInThisBlock = bytesInFirstBlock;
- }
- elseif(index == endIndex) {
- bytesInThisBlock = bytesInLastBlock;
- }
- else{
- bytesInThisBlock =blkLocations[index].getLength();
- }
-
-
-
- allTopos = blkLocations[index].getTopologyPaths();
-
-
-
- if(allTopos.length== 0) {
- allTopos = fakeRacks(blkLocations,index);
- }
-
-
-
-
-
-
-
- for(String topo: allTopos) {
-
- Node node, parentNode;
- NodeInfo nodeInfo, parentNodeInfo;
-
- node = clusterMap.getNode(topo);
-
- if (node == null) {
- node = new NodeBase(topo);
- clusterMap.add(node);
- }
-
- nodeInfo = hostsMap.get(node);
-
-
-
- if (nodeInfo == null) {
- nodeInfo = new NodeInfo(node);
- hostsMap.put(node,nodeInfo);
- parentNode = node.getParent();
- parentNodeInfo =racksMap.get(parentNode);
- if (parentNodeInfo == null) {
- parentNodeInfo = new NodeInfo(parentNode);
- racksMap.put(parentNode,parentNodeInfo);
- }
- parentNodeInfo.addLeaf(nodeInfo);
- }
- else {
- nodeInfo = hostsMap.get(node);
- parentNode = node.getParent();
- parentNodeInfo =racksMap.get(parentNode);
- }
-
-
- nodeInfo.addValue(index,bytesInThisBlock);
-
- parentNodeInfo.addValue(index,bytesInThisBlock);
-
- }
-
- }
-
- returnidentifyHosts(allTopos.length, racksMap);
- }
-
-
-
-
- privateString[] identifyHosts(int replicationFactor,
- Map<Node,NodeInfo> racksMap) {
-
- String [] retVal = new String[replicationFactor];
-
- List <NodeInfo> rackList = new LinkedList<NodeInfo>();
-
- rackList.addAll(racksMap.values());
-
-
-
- sortInDescendingOrder(rackList);
-
- booleandone = false;
- intindex = 0;
-
-
-
-
- for(NodeInfo ni: rackList) {
-
- Set<NodeInfo> hostSet= ni.getLeaves();
-
- List<NodeInfo>hostList = new LinkedList<NodeInfo>();
- hostList.addAll(hostSet);
-
-
- sortInDescendingOrder(hostList);
-
-
- for(NodeInfo host: hostList) {
-
- retVal[index++] = host.node.getName().split(":")[0];
- if (index == replicationFactor) {
- done = true;
- break;
- }
- }
-
- if(done == true){
- break;
- }
- }
- returnretVal;
- }
通过上述选择主机的算法,我们可以知道,当一个分片包含的多个block的时候,总会从其他节点读取数据,也就是做不到所有的计算都是本地化。为了发挥计算本地化性能,应该尽量使InputSplit大小与块大小相当。
在旧版的接口中,InputSplit的大小会受maptask个数,和split参数的影响,需要具体情况具体调整。在新版的接口中,这个比较容易控制,因为不受maptask的影响,InputSplit大小计算公式如下: splitSize=max("mapred.min.split.size",min("mapred.max.split.size",blockSize))
两个参数都取默认配置的时候,分片大小就是blockSize
最顶级是InputFormat抽象类 ,该类定义了两个方法,方法getSplits是负责切分输入文件(们)的,把很多的输入文件(们)切分成很多的输入分片,切分规则就是在这里定义的,每个InputSplit对应一个Mapper任务。方法createRecordReader是负责把getSplits生成的很多InputSplit解析成键值对的。InputFormat中的方法getSplits负责拆分输入文件的,方法createRecordReader负责把拆分的数据解析成键值对。这是从两个不同的角度设定的方法,职责不同,功能不同。方法getSplits的返回值是InputSplit的集合。抽象类InputSplit表示每个Mapper任务将要处理的数据,是二进制的,
根据要处理的数据类型不同,InputSplit也有不同的含义,因此会有很多子类。
方法createRecordReader的返回值是一个可以处理键值对的RecordReader。抽象类RecordReader是就是把InputSplit的数据解析成键值对的,提供了解析规则和读取键值对的方法,
根据数据不同,解析规则也会不同,因此会有很多子类。
根据解析的数据不同,InputFormat的子类有DBInputFormat、DelegatingInputFormat和FileInputFormat。其中,DBInputFormat是专门用于加载数据库中的数据的,如mysql、oracle等;FileInputFormat是专门用于处理文件中的数据的;DelegatingInputFormat是把其他各种InputFormat组成到一起的委托接口。从功能可以看出,FileInputFormat是用途最广的,其次是DBInputFormat,再次是DelegatingInputFormat。
FileInputFormat
该类是专门处理文件的,该类提供了如何计算输入分片(InputSplit)的方法。每个InputSplit对应一个运行的Mapper任务,因此InputSplit的数量决定了运行的Mapper任务数量,InputSplit的大小决定了每个Mapper任务处理数据量的大小。在这里,使用的是InputSplit的子类FileSplit。类FileSplit是专门用于处理输入文件的,会按照固定的字节数对每个输入文件进行拆分,拆分成很多的FileSplit。
根据文件的不同,对FileSplit解析成键值对的方式也有很多。因此,FileInputFormat会有很多的子类,包括TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineFileInputFormat和SequenceFileInputFormat。
输入格式化类的作用有两个:
- 设定Mapper任务数量——通过getSplits方法实现;
- 把输入文件的内容解析成键值对,供map函数调用——通过createRecordReader方法实现