FileInputFormat类中split切分算法和host选择算法介绍

来源:互联网 发布:vip影视盒子源码 编辑:程序博客网 时间:2024/04/30 23:57

FileInputFormat类中split切分算法和host选择算法介绍

在hadoop源码的org.apache.hadoop.mapred包中,有个FileInputFormat类,这个类的主要作用是提供统一的getSplits函数。该函数实现中最核心的两个算法是split切分算法 和 host选择算法

1、什么是split?

它是在逻辑上对输入数据进行的分片,并不会在磁盘上将其切分成分片进行存储。每个split都作为一个独立单位分配给一个task去处理(这也是为什么要把输入切分成split的原因)。hadoop中用org.apache.hadoop.mapred.FileSplit类来封装分片,其定义如下(省略了成员方法):

public class FileSplit extends org.apache.hadoop.mapreduce.InputSplit implements InputSplit {

private Path file;  //split所在的文件(一个split一定只属于一个文件)
  
private long start;  //split在文件中的起始位置
  
private long length;  //split的长度
  
private String[] hosts;  //split所在的主机名称

......
}

注:FileSplit既继承于InputSplit类,又实现InputSplit接口。没错,在hadoop-1.0.0中,既有InputSplit类,在org.apache.hadoop.mapreduce中;又有InputSplit接口,在org.apache.hadoop.mapred中。

2、输入文件、split、block三者的关系。

我们可以用一张图来说明三者之间的关系,如下图:


蓝色部分可以看做是一个输入文件,它被划分成多个block,如:block1,block2,block3,block4,block5,等等,存储在HDFS系统上。每个block在HDFS上有三个备份(算上自己,总共三份),每个备份分布在不同的节点(节点可能是主机、机柜、数据中心)上,图中有5个节点,防止因某个节点宕机而丢失数据。同时,一个文件被分成多个split,如:split1,split2,split3,split4等等。

(1)split是文件在逻辑上的划分,是程序中的一个独立处理单位,每一个split分配给一个task去处理。在实际的存储系统中并没有按split去存储。

(2)block是文件在物理上的划分,HDFS系统上就是按照block来存储的。一个block的多个备份存储在不同的节点上。

(3)一个文件可能被划分成多个split,但一个split只可能属于一个文件(稍后代码中将会讲解)。比如:图中的文件至少包含4个split。

(4)一个split可能包含多个block,但一个block不一定只属于一个split。比如:split1完全包含block1,部分包含block2,;block2一部分属于split1,一部分属于split2.

3、split切分算法

这里的文件切分算法指的是将文件切分成split,不是block。文件切分算法主要用来确定 InputSplit的个数 以及 每个InputSplit对应的数据段。对于每个文件,由以下三个属性值确定其对应的InputSplit的个数。

  • goalSize :它是根据用户期望的InputSplit数目计算出来的,即totalSize/numSplits。其中,totalSize为输入文件(可能有多个)总大小;numSplits数用户设定的Map Task个数,默认情况下是1.
  • minSize :InputSplit的最小值,由配置参数mapred.min.split.size(在/conf/mapred-site.xml文件中配置)确定,默认是1(字节).
  • blockSize : 文件在HDFS中存储的block大小(在/conf/hdfs-site.xml文件中配置),不同文件可能不同,默认是64MB。
那么,splitSize = max{minSize, min{goalSize, blockSize}}.  一旦确定了splitSize值后,文件将被切分成大小为splitSize的InputSplit,最后剩下不足splitSize的数据块单独成为一个InputSplit。

下面我们来看看源代码。源代码(Hadoop-1.0.0)在org.apache.hadoop.mapred.FileInputFormat的getSplits方法中。

   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26    
//本文件代码在 org.apache.hadoop.mapred.FileInputFormat 中


private static final double SPLIT_SLOP = 1.1; // 切片系数

private long minSplitSize = 1;//最小split大小为1个字节



/**brief :将输入划分成多个split
*
* @param job : 作业,里面包含了作业运行时的一些信息,比如:输入文件信息
* @param numSplits : 用户给定的划分块数,即希望将文件划分成多少个split
*
* @return : 返回划分好的split数组
*/
public InputSplit[] getSplits(JobConf job, int numSplits) throws IOException {
FileStatus[] files = listStatus(job);//从job中获取输入文件状态信息
job.setLong(NUM_INPUT_FILES, files.length);//将输入文件个数保存到job中
long totalSize = 0;//用于保存所有文件大小的总和
//统计所有文件大小的总和
for (FileStatus file: files) {
if (file.isDir()) {//非法文件
throw new IOException("Not a file: "+ file.getPath());
}
totalSize += file.getLen();//累加
}
long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);//按用户要求划分输入,确定每个split的目标大小goalSize
long minSize = Math.max(job.getLong("mapred.min.split.size", 1),minSplitSize);//minSplitSize是FileInputFormat类的成员,默认值是1(字节)。
//job.getLong("mapred.min.split.size", 1)是获取配置文件中设置的值,若没有设置,则取1.
/*************************** 开始划分split ************************/
ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);//申请一个初始大小为numSplits的数组,来存放划分结果
NetworkTopology clusterMap = new NetworkTopology();//申请一个网络拓扑,用于划分过程中保存整个网络的拓扑结构
for (FileStatus file: files) {//对于每一个文件
Path path = file.getPath();//获取文件路径
FileSystem fs = path.getFileSystem(job);//获得hdfs文件系统中的路径信息
long length = file.getLen();//文件长度(字节数)
BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0, length);//获得此文件每个block所在位置(节点),可能存在于不同的节点上,所以是个数组
if ((length != 0) && isSplitable(fs, path)) {//文件长度不为0,且可分片
long blockSize = file.getBlockSize();//获得此文件在HDFS系统中的block大小
long splitSize = computeSplitSize(goalSize, minSize, blockSize);//计算分片的大小
long bytesRemaining = length;//文件剩余字节数
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {//文件剩余大小 大于 切片大小的1.1倍才会继续切片
String[] splitHosts = getSplitHosts(blkLocations, length-bytesRemaining, splitSize, clusterMap);//获得此split所在的主机位置
splits.add(new FileSplit(path, length-bytesRemaining, splitSize, splitHosts));//添加分片到结果集
//表示此文件(path指定)的此分片(length-bytesRemaining和splitSize指定)所在的hosts
bytesRemaining -= splitSize;//剩余大小
}
if (bytesRemaining != 0) {//将文件的最后一部分作为一个split
splits.add(new FileSplit(path, length-bytesRemaining, bytesRemaining, blkLocations[blkLocations.length-1].getHosts()));//添加split到结果集
}
}
else if (length != 0) {//文件不可分片,则将整个文件作为一个分片
String[] splitHosts = getSplitHosts(blkLocations,0,length,clusterMap);//获得此文件所在的主机位置
splits.add(new FileSplit(path, 0, length, splitHosts));//添加到结果集
} else {//文件长度为0,则生成一个空分片
splits.add(new FileSplit(path, 0, length, new String[0]));
}
}
LOG.debug("Total # of splits: " + splits.size());//记录调试信息日志
return splits.toArray(new FileSplit[splits.size()]);
}

/**brief :计算split大小
*
* @param goalSize : 用户希望的split大小
* @param minSize : 配置文件中的split最小大小
* @param blockSize : HDFS文件系统中的block大小
*/
protected long computeSplitSize(long goalSize, long minSize, long blockSize) {
return Math.max(minSize, Math.min(goalSize, blockSize));
}
代码中注释已经很详细了,下面简单说明几点:

(1)long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits); 根据用户的numSplits个数来确定split的目标大小;

(2)方法computeSplitSize就是用来计算split的最终大小的。

(3)下面这段代码意思是将最后不足splitSize的部分也作为一个分片:

if (bytesRemaining != 0) {//将文件的最后一部分作为一个分片
 splits.add(new FileSplit(path, length-bytesRemaining, bytesRemaining, 
blkLocations[blkLocations.length-1].getHosts()));
}

(4) for (FileStatus file: files) {....},这个for循环,是对每一个文件进行分片,那么就可以保证一个split只属于一个文件,不会属于多个文件

(5)NetworkTopology clusterMap = new NetworkTopology(); 不晓得为什么要定义这么一个对象。虽然作为参数传递给getSplitHosts方法,但在getSplits方法中,真没看出来它有什么作用。如果getSplitHosts方法要用的话,完全可以放在getSplitHosts中定义,为什么要放在getSplits方法中定义呢?


4、host选择算法

InputSplit的四个成员<file,start,length,hosts>,分别表示InputSplit所在的文件、起始位置、长度及所在的host列表。前三个很容易确定,host列表的选择比较困难。host列表的选择策略直接影响到运行过程中的任务本地性。我们都知道HDFS上的文件是以block存储的,一个文件对应的block可能分布在整个Hadoop集群上,而InputSplit的划分算法可能导致一个InputSplit对应多个block,这些block可能位于不同节点上,这使得Hadoop不可能完全实现数据的本地性

为此,Hadoop将数据本地性按代价划分成三个等级,分别是:node locality(主机本地性)、rack locality(机柜本地性)、data locality(数据中心本地性)。前两个等级在Hadoop都已实现,数据中心本地性暂时还未实现。在任务调度时,会依次考虑3种节点本地性,优先让空闲资源处理本节点上的数据,如果节点上没有可处理的数据,则处理同一个机柜上的数据,最坏的情况是处理其他机柜上的数据(当然必须在同一数据中心)。

虽然InputSplit对应的bloc可能位于多个节点上,但考虑到任务调度的效率,一般不会将所有节点放入InputSplit的hosts列表中,而是选择包含该InputSplit数据量最大的前几个节点(Hadoop限制最多选择10个,多余的会过滤掉),以作为任务调度时判断任务是否具有本地性的主要凭证。一个简单有效的启发式算法(一种可行的算法,并不是最优的是:首先按照rack包含的数据量对rack进行排序,然后在同一个rack内按node包含的数据量对node排序,最后取前N个node的host作为InputSplit的hosts列表,这里的N为block副本数。这样,当任务调度器调度Task时,只要将Task调度给位于hosts列表中的节点,就认为该Task满足本地性。

我们来看看源代码。源代码(Hadoop-1.0.0)在org.apache.hadoop.mapred.FileInputFormat的getSplitHosts方法中。

   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 136 237 238
//本文件代码在 org.apache.hadoop.mapred.FileInputFormat 中
/**
* This function identifies(识别) and returns the hosts that contribute
* most(最大贡献) for a given split. For calculating the contribution(贡献度), rack
* locality(机柜本地性) is treated on par with(和...一起) host locality(主机本地性), so hosts from racks
* that contribute the most are preferred over hosts on racks that
* contribute less(因此,贡献大的机柜的主机要优先于贡献低的机柜的主机)
*
* @param blkLocations : block位置list
* @param offset : split在文件中的偏移量
* @param splitSize : split大小
* @param clusterMap : 网络拓扑,传递进来的是一个空拓扑,在本方法中会进行填充
*
* @return : 返回对这个split贡献最大的N个主机的集合
*/
protected String[] getSplitHosts(BlockLocation[] blkLocations, long offset, long splitSize, NetworkTopology clusterMap) throws IOException {

int startIndex = getBlockIndex(blkLocations, offset);//此偏移量的split所对应的第一个block的位置

long bytesInThisBlock = blkLocations[startIndex].getOffset() + blkLocations[startIndex].getLength() - offset; //split在第一个block中的数据量(字节个数)

if (bytesInThisBlock >= splitSize) {//如果split的字节全在此block中,则直接返回此block所对应的主机集合
return blkLocations[startIndex].getHosts();
}

long bytesInFirstBlock = bytesInThisBlock;//在第一个block中的字节数
int index = startIndex + 1;//从第二个block开始遍历
splitSize -= bytesInThisBlock;//除了在第一个block中的字节数,剩下还有多少

while (splitSize > 0) {//找到最后那个block
bytesInThisBlock = Math.min(splitSize, blkLocations[index++].getLength());//取 剩余split大小 和 当前block大小 的较小值
splitSize -= bytesInThisBlock;//去除当前block的字节数
}

long bytesInLastBlock = bytesInThisBlock;//在最后那个block中的字节数
int endIndex = index - 1;//最后那个block的索引号

Map <Node,NodeInfo> hostsMap = new IdentityHashMap<Node,NodeInfo>();//主机map,用于 主机 到 主机信息 的映射
Map <Node,NodeInfo> racksMap = new IdentityHashMap<Node,NodeInfo>();//机柜map,用于 机柜 到 机柜信息 的映射
String [] allTopos = new String[0];//用于存放split的所有block拓扑(拓扑:路径信息)

for (index = startIndex; index <= endIndex; index++) {//遍历split的所有block

/** 确定split在当前block的字节数 **/
if (index == startIndex) {//在首个block中的字节数
bytesInThisBlock = bytesInFirstBlock;
}
else if (index == endIndex) {//在尾个block中的字节数
bytesInThisBlock = bytesInLastBlock;
}
else {
bytesInThisBlock = blkLocations[index].getLength();//等于当前block的大小
}
allTopos = blkLocations[index].getTopologyPaths();//当前block在网络中的拓扑(因为一个block存在多个备份,可能存在不同的位置上,所以返回的是一个数组)

if (allTopos.length == 0) {//如果没有拓扑信息可以利用,则默认生成一个虚拟拓扑
allTopos = fakeRacks(blkLocations, index);
}

for (String topo: allTopos) {//遍历每一个拓扑,计算每个主机、每个机柜上的含有此split的字节数量

Node node, parentNode;//一个Node可能是一个数据中心、机架、机器,见Node.java文件。这里,node为主机,parentNode为机架
NodeInfo nodeInfo, parentNodeInfo;//节点对应的信息

node = clusterMap.getNode(topo); //按照拓扑(如/dog/orange/hostname:port,其中dog为数据中心,orange为机架,hostname为主机名,port为端口号)得到相应的节点

if (node == null) {//如果node为空,则按照默认拓扑生成一个节点
node = new NodeBase(topo);//创建默认拓扑
clusterMap.add(node);//加入网络拓扑中
}
nodeInfo = hostsMap.get(node);//第一次get时都是null,因为hostsMap为空
if (nodeInfo == null) {
nodeInfo = new NodeInfo(node);//生成一个默认的节点信息
hostsMap.put(node,nodeInfo);//加入到map中
parentNode = node.getParent();//获得此节点的父节点
parentNodeInfo = racksMap.get(parentNode);
if (parentNodeInfo == null) {
parentNodeInfo = new NodeInfo(parentNode);//生成默认节点信息
racksMap.put(parentNode,parentNodeInfo);//加入到map中
}
parentNodeInfo.addLeaf(nodeInfo);//node为parentNode的一个叶子
}
else {//不是第一次get时
nodeInfo = hostsMap.get(node);
parentNode = node.getParent();
parentNodeInfo = racksMap.get(parentNode);
}

nodeInfo.addValue(index, bytesInThisBlock);//同一个主机上相同block,addValue只会累加一次,见188行
parentNodeInfo.addValue(index, bytesInThisBlock);//同一个机架上相同block,addValue只会累加一次,见188行

} // for all topos

} // for all indices

return identifyHosts(allTopos.length, racksMap);
}
/**brief :找出此offset从哪个block开始的
*
* @param blkLocations : 文件的block数组,即文件的所有block都存在这个数组中
* @param offset : split在文件中的偏移量
*
* @return : split在blkLocations中的起始索引号。如果未找到,抛出异常。
*/
protected int getBlockIndex(BlockLocation[] blkLocations, long offset) {
for (int i = 0 ; i < blkLocations.length; i++) {
//offset大于等于当前block的起点,且小于当前block起点+当前block长度,那么此split就是从当前block开始的
if ((blkLocations[i].getOffset() <= offset) && (offset < blkLocations[i].getOffset() + blkLocations[i].getLength())){
return i;
}
}
BlockLocation last = blkLocations[blkLocations.length -1];
long fileLength = last.getOffset() + last.getLength() -1;
throw new IllegalArgumentException("Offset " + offset + " is outside of file (0.." + fileLength + ")");//抛出异常
}

/**brief : 生成虚拟拓扑,即在主机前面加上默认的机柜名称
*
* @param blkLocations : 存放文件所有block的数组
* @param index : 相应的block索引号
*
* @return :虚拟拓扑
*/
private String[] fakeRacks(BlockLocation[] blkLocations, int index) throws IOException {
String[] allHosts = blkLocations[index].getHosts();//获得此block所在的主机
String[] allTopos = new String[allHosts.length];
for (int i = 0; i < allHosts.length; i++) {
allTopos[i] = NetworkTopology.DEFAULT_RACK + "/" + allHosts[i]; //DEFAULT_RACK = "/default-rack";
}
return allTopos;
}

/**brief :此方法在org.apache.hadoop.mapred.FileInputFormat.NodeInfo类中,是FileInputFormat的一个内部类。方法的作用是累加block的字节数
*
* @param blockIndex : block的索引号
* @param value : split在此block的字节数
*
*/
void addValue(int blockIndex, long value) {
if (blockIds.add(blockIndex) == true) {//根据返回值判断是否要累加value。当blockIndex不存在的时候,会返回true;
this.value += value; //若已存在blockIndex,则返回false。也就是说,同一个拓扑中的相同block,只会累加一次。
}
}
/**brief : 机架优先、节点次之的顺序,按数据量(字节数)从大到小 对拓扑排序。找出要求数目的主机
*
* @param replicationFactor : 规定数目的主机数
* @param racksMap : 一个map,存放着 机柜节点 到 节点信息 的映射
*
* @return : 按贡献值(字节数)从多到少排序后,返回所要找的replicationFactor个主机名
*/
private String[] identifyHosts(int replicationFactor, Map<Node,NodeInfo> racksMap) {
String [] retVal = new String[replicationFactor];//用于存放结果的数组
List <NodeInfo> rackList = new LinkedList<NodeInfo>(); //申请一个list,用于排序

rackList.addAll(racksMap.values());//将map的值放入list中,便于排序。 racksMap中存放的是机架上关于split的存储量

sortInDescendingOrder(rackList);//对机架排序,按照它们对split的字节数从多到少排序

boolean done = false;//标识变量,标识整个过程是否结束
int index = 0;//retVal数组的索引号

for (NodeInfo ni: rackList) {

Set<NodeInfo> hostSet = ni.getLeaves();//机架的叶子,其实就是主机,即获得此机架上的所有主机(存储着split的主机)

List<NodeInfo> hostList = new LinkedList<NodeInfo>();//用于排序
hostList.addAll(hostSet);//将所有主机添加到list中,便于排序

sortInDescendingOrder(hostList);//对本机架上的主机排序,按照它们的字节数从多到少排序

for (NodeInfo host: hostList) {
retVal[index++] = host.node.getName().split(":")[0];//从主机名中分离主机名,去除端口
if (index == replicationFactor) {//已找到足够多的主机
done = true;//标识任务完成
break;
}
}
if (done == true) {
break;
}
}
return retVal;
}
/**brief : 用于降序排序
*
* @param mylist : 需要排序的list
*/
private void sortInDescendingOrder(List<NodeInfo> mylist) {//降序排序

Collections.sort(mylist, new Comparator<NodeInfo> () {
public int compare(NodeInfo obj1, NodeInfo obj2) {

if (obj1 == null || obj2 == null)
return -1;

if (obj1.getValue() == obj2.getValue()) {
return 0;
}
else {
return ((obj1.getValue() < obj2.getValue()) ? 1 : -1);
}
}
}
);
}

代码中注释已经很详细了,下面简单说明几点:

(1)getBlockIndex(BlockLocation[] blkLocations, long offset)方法,这是获取split所在block的索引号。比如说:在第2节的图中,blkLocations={block1, block2, block3, block4, block5, ...},split2从block2的中间部位开始,所以返回的索引为1,即block2在blkLocations中的下标。

(2)hostsMap用来记录主机的一些信息,比如包含split的字节数;racksMap用来记录机柜的一些信息。在遍历完相应的block之后,hostsMap记录下了包含split数据的主机信息,racksMap记录下了包含split数据的机柜信息,那么这些信息(主要是包含split的字节数)就可以用于之后的排序。

(3)org.apache.hadoop.mapred.FileInputFormat.NodeInfo类(是个内部类)的addValue(...)方法,保证了同一个拓扑中的相同block,只会累加一次包含split的字节数。拓扑可以看做是一个绝对路径,比如:/dog/orange/hostname:port,其中dog为数据中心,orange为机架,hostname为主机名,port为端口号。 对于文件的一个block,可能多个主机上都存储着,因为不同的主机就是不同的拓扑,所以不同主机上的相同block都会参与排序,以选出距离本地最近的一个主机。

(4)identifyHosts(...)方法中,首先按机柜上的字节数(split在此机柜上的数据量)从多到少排序,然后在同一机柜的多个主机上按字节数(split在此主机上的数据量)从多到少排序,依次选出要求个数的主机。标识符done的作用是一旦找到要求个数的主机,立即退出循环。

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 手机视频缩略图加载失败怎么办 电脑突然卡住不动了怎么办 word文档被锁住了怎么办 手机qq磁盘已满怎么办 不小心格式化了硬盘怎么办 移动硬盘插上显示要格式化怎么办 微信网络特别慢怎么办 苹果六网速太慢怎么办 小米手机wifi网速慢怎么办 苹果8蜂窝上网慢怎么办 苹果6s4g网速慢怎么办 苹果7上网速度慢怎么办 银行转账到别人账户怎么办 银行转账转错账户怎么办 人已故欠的公款怎么办 论文抄了表格数据怎么办 电子转账转错了怎么办 苹果手机付款方式有问题怎么办 合同中付款方式错怎么办? 优步付款方式无效怎么办 工程付款方式变更没有合同怎么办 银行账号被锁了怎么办? 街电押金退不了怎么办 佣金宝账号忘了怎么办 如果汇款汇错了怎么办 手机汇款汇错了怎么办 汇款时少了数字怎么办 打过流脑后发烧怎么办 甲醛公司除完后怎么办 发票系统导出的xml 怎么办 新买的书包味道太大怎么办 alt+a截图热键冲突怎么办 白背心领发黄了怎么办 房屋装修后出现质量问题怎么办 华为p9手机音量小怎么办 华为畅享8玩游戏卡怎么办 华为畅享7玩游戏卡怎么办 华为p9升级以后屏幕失灵怎么办 荣耀9青春版玩游戏卡怎么办 华为p9屏幕不亮了怎么办 华为p9入水黑屏怎么办