Hadoop CombineFileInputFormat实现原理及源码分析

Hadoop适用于少量的大文件场景,而不是大量的小文件场景(这里的小文件通常指文件大小显著小于HDFS Block Size的文件),其主要原因是因为FileInputFormat在为这些小文件生成切片的时候,会为每一个小文件生成一个切片,如果小文件数目众多,会导致大量的Map Task,每个Map Task仅仅处理很少量的数据(即一个小文件的全部数据),这种情况会带来很大的运行开销且耗时较长。
 
CombineFileInputFormat设计目的就是用来应对小文件的场景,FileInputFormat为每一个小文件生成一个切片,而CombineFileInputFormat会将许多小文件“打包”为一个切片,使得每一个Map Task可以处理更多的数据。更为关键的是CombineFileInputFormat在决定将哪些小文件的数据块“打包”为一个切片时会充分考虑数据本地性(节点本地性、机器本地性)的特性,因此不会在运行时间和网络带宽方面来很大的开销。
 
不光是小文件的场景,在处理大文件时CombineFileInputFormat也有优势,原因在于它会将同一节点上的多个数据块形成一个切片,对Data-Local Map Tasks可能带来一定的优化。
 
思路
 
CombineFileInputFormat有三个重要的属性:
 
 
maxSplitSize:切片大小最大值,可通过属性mapreduce.input.fileinputformat.split.maxsize或CombineFileInputFormat实例方法setMaxSplitSize()进行设置;
 
minSplitSizeNode:同一节点的数据块形成切片时,切片大小的最小值,可通过属性mapreduce.input.fileinputformat.split.minsize.per.node或CombineFileInputFormat实例方法setMinSplitSizeNode()进行设置;
 
minSplitSizeRack:同一机架的数据块形成切片时,切片大小的最小值,可通过属性mapreduce.input.fileinputformat.split.minsize.per.rack或CombineFileInputFormat实例方法setMinSplitSizeRack()进行设置;
 
 
CombineFileInputFormat有一个“过滤池”的概念,pools中保存着多个“过滤池”实例(实际就是MultiPathFilter实例,该实例由多个PathFilter实例构成),一个切片中的数据块只可能来自于同一个过滤池,但可以来自同一个过滤池中的不同文件。
 
注:“过滤池”的设计目的还不是很明白,欢迎大家交流,貌似Hive有相关的应用。
 
getSplits():
 
 
step 1
 
获取输入路径中的所有文件;
 
step 2
 
迭代为每个过滤池中的文件生成切片;
 
     step 2.1
     
     获取满足当前过滤池实例的所有文件myPaths;
 
     step 2.2
 
     为mypaths中的文件生成切片;
 
step 3
 
为不属于任何过滤池的文件生成切片。
 
无论是满足某过滤池实例条件的文件还是不属于任何过滤池的文件,可以笼统地理解为“一批文件”,getMoreSplits()就是为这一批文件生成切片的。
 
 
step 1
 
前面提到,CombineFileInputFormat在将小文件“打包”为一个切片的时候,会考虑数据本地性(节点本地性、数据本地性),而且一个切片涉及到多个小文件,因此此处定义了三个对应关系:
 
rackToBlocks:机架和数据块的对应关系;
blockToNodes:数据块和节点的对应关系;
nodeToBlocks:节点和数据块的对应关系。
 
此外还应注意到比较重要的两个类:
 
OneFileInfo:代表一个文件;
OneBlockInfo:代表一个文件中的数据块,注意这个地方的数据块是“逻辑”上的数据块,不能直接对应HDFS Block,后续会说原因。
 
step 2
 
迭代这“一批文件”,为每一个文件构建OneFileInfo对象(由OneFileInfo构造函数完成),OneFileInfo对象本身并没有多少实际作用,它的意义是在构建过程中维护了上述三个对应关系的信息。
 
其中有一段代码是为这个文件生成对应的OneBlockInfo对象(多个),如下:
 
 
可以看出,对于每一个数据块(由locations[i]表示)而言,构建OneBlockInfo的逻辑是一个循环的过程,根据maxSize(maxSplitSize)的值不同就有可以产生多个OneBlockInfo对象,这也是“逻辑块”的原因。
 
迭代完成之后,我们即可以认为数据块、节点、机架相互之间的对应关系已经建立完毕,接下来可以根据这些信息生成切片。
 
step 3 
 
切片的形成过程(createSplits())如下:
 
(1)不断迭代节点列表,逐个节点(数据块)形成切片(Local Split);对于每一个节点而言,遍历并累加这个节点上的数据块,
     a.如果maxSplitSize != 0且累加的数据块大小大于或等于maxSize,则将这些数据块形成一个切片,继续下一个节点(为了达到多个节点之间分布切片的目的);
     b.如果maxSplitSize == 0,则上述累加的数据块可能为该节点的全部数据块,也可能是节点在迭代过程中剩余的数据块,这些数据块根据条件的不同可能全部形成一个切片,或者全部留作后续处理;
 
(2)不断迭代机架列表,逐个机架(数据块)形成切片(Rack Split);对于每一个机架而言,处理流程与(1)类同,仅判断条件时策略不同;
     
(3)遍历并累加剩余数据块,如果maxSplitSize != 0且累积的数据块大小大于或等于maxSplitSize,则将这些数据块形成一个切片;
 
(4)剩余数据块形成一个切片。
 
该部分代码比较冗长,建议读者自行阅读,核心逻辑比较简单:优先将一个节点上的数据块形成切片(同时兼顾切片分布问题),次之将一个机架的数据块形成切片,最后将剩余数据块形成切片。
posted on 2015-02-09 21:17  非著名野生程序员  阅读(2297)  评论(0编辑  收藏  举报