前言
MapReduce的源码分析是基于Hadoop1.2.1基础上进行的代码分析。
什么是InputSplit
InputSplit是指分片,在MapReduce当中作业中,作为map task最小输入单位。分片是基于文件基础上出来的而来的概念,通俗的理解一个文件可以切分为多少个片段,每个片段包括了<文件名,开始位置,长度,位于哪些主机>等信息。在MapTask拿到这些分片后,会知道从哪开始读取数据。Job提交时如何获取到InputSplit
以org.apache.hadoop.mapred包中的FileInputFormat为例(因为该类作为其他文件类型的基类),内部实现了如何获取分片,通过分析代码,以便知晓文件是如何被切片的。
public InputSplit[] getSplits(JobConf job, int numSplits)
throwsIOException {
//获取文件列表的状态,底层通过HDFS客户端的//DistributedFileSystem.getFileStatus获取到文件的状态(文件长度,访问时间,权限,块大小,副本数等信息)
FileStatus[] files = listStatus(job);
// 保存输入的文件的文件个数
job.setLong(NUM_INPUT_FILES, files.length);
//计算所有文件的总长度
longtotalSize = 0; // compute total size
for(FileStatus file: files) { // check we have valid files
if(file.isDir()) {
throw new IOException("Not a file: "+ file.getPath());
}
totalSize += file.getLen();
}
// 计算出目标长度,通过总长度和用户指定的map task的个数相除得到
longgoalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
// 获取用户配置文件中指定的最小split的长度,默认为1,如果不希望按默认计算出的大//小进行分片,则可以指定最小切分的大小,当这个值大于计算出的分片大小,则会以此为准。
longminSize = Math.max(job.getLong("mapred.min.split.size", 1),
minSplitSize);
// 保存后续生成的split
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();
// 获取到整个文件的所有block的位置信息
BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0,length);
// 文件长度不为0,且能被切分(二进制文件总是不允许切分)
if((length != 0) && isSplitable(fs, path)) {
long blockSize = file.getBlockSize();
//计算出当前文件需要按多长作为当前该文件切分的单位(一般为blockSize,当map task指定的多,则为goalSize,这需要按具体的参数)
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{
//文件长度为空的也会产生一个分片
//Create empty hosts array for zero length files
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 {
// 通过指定的偏移来确定在偏移是落在了第几个Block上
intstartIndex = getBlockIndex(blkLocations, offset);
// 计算出当前这个Block从偏移开始到块结束还有多少数据量
longbytesInThisBlock = blkLocations[startIndex].getOffset() +
blkLocations[startIndex].getLength() - offset;
// 如果这个块的剩余的数据量是大于一个分片的长度的,
// 则直接返回这个block所对应的主机列表。也就是一个分片不足一个block的情况
//If this is the only block, just return
if(bytesInThisBlock >= splitSize) {
returnblkLocations[startIndex].getHosts();
}
// 否则,说明了这个分片还会包含其他的block,因此需要算出除当前块外的分片长度
longbytesInFirstBlock = bytesInThisBlock;
intindex = startIndex + 1;
splitSize -= bytesInThisBlock;
// 计算出在最后一个块做这个分片占了多少长度的数据量。
while(splitSize > 0) {
bytesInThisBlock =
Math.min(splitSize,blkLocations[index++].getLength());
splitSize -= bytesInThisBlock;
}
longbytesInLastBlock = bytesInThisBlock;
intendIndex = index - 1;
//这是两个核心的结果,用于记录网络拓扑信息
//Node用来表示节点(如数据节点,机架)
//NodeInfo用来表示节点的信息,包含(叶子节点列表,blockId列表,数据长度)
//hostsMap会记录数据节点(简称节点,即Datanode)到对应的节点信息的关系
//在hostsMap记录的value中会记录数据节点包含了这个分片中的多少个块索引
//以及包含的这些block有多少数据是在这个分片中的。
//racksMap会记录机架到这个机架信息,在racksMap中会记录包括上述的数据节点
//所包含的的信息之外,还记录了有哪些数据节点属于这个机架
Map <Node,NodeInfo> hostsMap = new IdentityHashMap<Node,NodeInfo>();
Map <Node,NodeInfo> racksMap = new IdentityHashMap<Node,NodeInfo>();
String [] allTopos = new String[0];
// Build the hierarchy and aggregate thecontribution of
// bytes at each level. SeeTestGetSplitHosts.java
// 遍历这个分片所包含的的block,将block的拓扑信息和数据长度信息记录到
// hostsMap和racksMap中
for(index = startIndex; index <= endIndex; index++) {
// 确认block有多少数据是属于当前这个分片的
// Establish the bytes in this block
if(index == startIndex) {
bytesInThisBlock = bytesInFirstBlock;
}
elseif(index == endIndex) {
bytesInThisBlock = bytesInLastBlock;
}
else{
bytesInThisBlock =blkLocations[index].getLength();
}
// 获取block的拓扑信息,取得拓扑的路径
// 如["/rack1/node1","/rack1/node2","/rack2/node3"]
allTopos = blkLocations[index].getTopologyPaths();
// If no topology information is available,just
// prefix a fakeRack
if(allTopos.length== 0) {
allTopos = fakeRacks(blkLocations,index);
}
// NOTE: This code currently works only forone level of
// hierarchy (rack/host). However, it isrelatively easy
// to extend this to support aggregation atdifferent
// levels
// 遍历每个拓扑,将信息构建到hostsMap和racksMap
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);
} // for all topos
} // for all indices
// 真正开始按选择主机
returnidentifyHosts(allTopos.length, racksMap);
}
// 会选择出副本数的主机列表,即有副本数是3,则会返回3个主机的信息
// 选择的算法,是前面所说的先根据机架包含的数据量排序,再根据节点包含的数据量
// 进行排序,然后依次从高到底选出副本数个主机信息返回
privateString[] identifyHosts(int replicationFactor,
Map<Node,NodeInfo> racksMap) {
String [] retVal = new String[replicationFactor];
List <NodeInfo> rackList = new LinkedList<NodeInfo>();
rackList.addAll(racksMap.values());
//对所有机架按包含的数据量多少进行排序
// Sort the racks based on theircontribution to this split
sortInDescendingOrder(rackList);
booleandone = false;
intindex = 0;
//依次遍历这些机架,在机架内会按节点包含的数据量的多少进行排序
// Get the host list for all our aggregateditems, sort
// them and return the top entries
for(NodeInfo ni: rackList) {
Set<NodeInfo> hostSet= ni.getLeaves();
List<NodeInfo>hostList = new LinkedList<NodeInfo>();
hostList.addAll(hostSet);
// Sort the hosts in this rack based ontheir contribution
sortInDescendingOrder(hostList);
// 从按数据量的多少从高到底选择主机
for(NodeInfo host: hostList) {
// Strip out the port number from the host name
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