一、hadoop2.0 uber功能
1) uber的原理:Yarn的默认配置会禁用uber组件,即不允许JVM重用。我们先看看在这种情况下,Yarn是如何执行一个MapReduce job的。首先,Resource Manager里的Applications Manager会为每一个application(比如一个用户提交的MapReduce Job)在NodeManager里面申请一个container,然后在该container里面启动一个Application Master。container在Yarn中是分配资源的容器(内存、cpu、硬盘等),它启动时便会相应启动一个JVM。此时,Application Master便陆续为application包含的每一个task(一个Map task或Reduce task)向Resource Manager申请一个container。等每得到一个container后,便要求该container所属的NodeManager将此container启动,然后就在这个container里面执行相应的task。等这个task执行完后,这个container便会被NodeManager收回,而container所拥有的JVM也相应地被退出。在这种情况下,可以看出每一个JVM仅会执行一Task, JVM并未被重用。
2)用户可以通过启用uber组件来允许JVM重用——即在同一个container里面依次执行多个task。在yarn-site.xml文件中,改变一下几个参数的配置即可启用uber的方法:参数| 默认值 | 描述- mapreduce.job.ubertask.enable | (false) | 是否启用user功能。如果启用了该功能,则会将一个“小的application”的所有子task在同一个JVM里面执行,达到JVM重用的目的。这个JVM便是负责该application的ApplicationMaster所用的JVM(运行在其container里)。那具体什么样的application算是“小的application"呢?下面几个参数便是用来定义何谓一个“小的application"- mapreduce.job.ubertask.maxmaps | 9 | map任务数的阀值,如果一个application包含的map数小于该值的定义,那么该application就会被认为是一个小的application。- mapreduce.job.ubertask.maxreduces | 1 | reduce任务数的阀值,如果一个application包含的reduce数小于该值的定义,那么该application就会被认为是一个小的application。不过目前Yarn不支持该值大于1的情况。- mapreduce.job.ubertask.maxbytes | | application的输入大小的阀值。默认为dfs.block.size的值。当实际的输入大小不超过该值的设定,便会认为该application为一个小的application。最后,我们来看当uber功能被启用的时候,Yarn是如何执行一个application的。首先,Resource Manager里的Applications Manager会为每一个application在NodeManager里面申请一个container,然后在该container里面启动一个Application Master。containe启动时便会相应启动一个JVM。此时,如果uber功能被启用,并且该application被认为是一个“小的application”,那么Application Master便会将该application包含的每一个task依次在这个container里的JVM里顺序执行,直到所有task被执行完。这样Application Master便不用再为每一个task向Resource Manager去申请一个单独的container,最终达到了 JVM重用(资源重用)的目的。
3)在yarn-site.xml里的配置示例:
mapreduce.job.ubertask.enable
true
mapreduce.job.ubertask.maxmaps
9
mapreduce.job.ubertask.maxreduces
1
2.0的uber模式开启之后,JVM重用也一定生效。生效的条件是Map任务数量小于配置的任务数量,则认为是一个小任务,如果是小任务则JVM生效。
比如配置的Map任务=9 ,实际的map=5。如果实际的12,则认为不是一个小任务,则不开启JVM重用机制。
补充:uber的规定,reduce的数量必须是1,如果reduce>1,则不认为是小任务。JVM重用机制,是针对任务为单位的,即不同Task是不能共用JVM重用机制的。使用JVM重用机制,一定要注意全局变量的使用问题。
二、Hadoop小文件问题
小文件的定义:小文件指的是那些size比HDFS 的block size(默认64M/1.0版本,128M/2.0版本)小的多的文件。如果在HDFS中存储海量的小文件,会产生很多问题。
大量小文件在HDFS中的问题:任何一个文件,目录和block,在HDFS中都会被表示为元数据信息,每一个元数据信息占用150 bytes的内存空间。所以,如果有10million个文件,每一个文件对应一个block,那么就将要消耗namenode 3G的内存来保存这些block的信息。如果规模再大一些,那么将会超出现阶段计算机硬件所能满足的极限。不仅如此,HDFS并不是为了有效的处理大量小文件而存在的。它主要是为了流式的访问大文件而设计的。对小文件的读取通常会造成大量从datanode到datanode的seeks和hopping来retrieve文件,而这样是非常的低效的一种访问方式。
大量小文件在mapreduce中的问题:Map tasks通常是每次处理一个block的input(默认使用FileInputFormat)。如果文件非常的
小,并且拥有大量的这种小文件,那么每一个map task都仅仅处理了非常小的input数据,并且会产生大量的map tasks,每一个map task都会消耗一定量的bookkeeping的资源。比较一个1GB的文件,默认block size为64M,和1Gb的文件,没一个文件100KB,那么后者没一个小文件使用一个map task,那么job的时间将会十倍甚至百倍慢于前者。
hadoop中有一些特性可以用来减轻这种问题:可以在一个JVM中允许task reuse,以支持在一个JVM中运行多个map task,以此来减少一些JVM的启动消耗。另一种方法是将多个小文件合成一个spilt,即用一个map任务来处理。
三、Hadoop小文件解决方案
在使用Hadoop处理海量小文件的应用场景中,如果你选择使用CombineFileInputFormat,而且你是第一次使用,可能你会感到有点迷惑。虽然,从这个处理方案的思想上很容易理解,但是可能会遇到这样那样的问题。使用CombineFileInputFormat作为Map任务的输入规格描述,首先需要实现一个自定义的RecordReader。CombineFileInputFormat的大致原理是,他会将输入多个数据文件(小文件)的元数据全部包装到CombineFileSplit类里面。也就是说,因为小文件的情况下,在HDFS中都是单Block的文件,即一个文件一个Block,一个CombineFileSplit包含了一组文件Block,包括每个文件的起始偏移(offset),长度(length),Block位置(localtions)等元数据。如果想要处理一个CombineFileSplit,很容易想到,对其包含的每个InputSplit(实际上这里面没有这个,你需要读取一个小文件块的时候,需要构造一个FileInputSplit对象)。在执行MapReduce任务的时候,需要读取文件的文本行(简单一点是文本行,也可能是其他格式数据)。那么对于CombineFileSplit来说,你需要处理其包含的小文件Block,就要对应设置一个RecordReader,才能正确读取文件数据内容。通常情况下,我们有一批小文件,格式通常是相同的,只需要在为CombineFileSplit实现一个RecordReader的时候,内置另一个用来读取小文件Block的RecordReader,这样就能保证读取CombineFileSplit内部聚积的小文件。
为CombineFileSplit实现一个RecordReader,并在内部使用Hadoop自带的
LineRecordReader来读取小文件的文本行数据代码实现:
import java.io.IOException;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.CombineFileSplit;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import org.apache.hadoop.mapreduce.lib.input.LineRecordReader;
public class CombineSmallfileRecordReader extends RecordReader{
private CombineFileSplit combineFileSplit;
private LineRecordReader lineRecordReader=new LineRecordReader();
private Path[] paths;
private int totalLength;
private int currentIndex;
private float currentProgress=0;
private LongWritable currentKey;
private BytesWritable currentValue=new BytesWritable();
public CombineSmallfileRecordReader(CombineFileSplit combineFileSplit,TaskAttemptContext context,Integer index) {
super();
this.combineFileSplit=combineFileSplit;
this.currentIndex=index;//当前要处理的小文件Block在CombineFileSpilt中的索引
}
@Override
public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
this.combineFileSplit = (CombineFileSplit) split;
// 处理CombineFileSplit中的一个小文件Block,因为使用LineRecordReader,需要构造一个FileSplit对象,然后才能够读取数据
FileSplit fileSplit =new FileSplit(combineFileSplit.getPath(currentIndex),
combineFileSplit.getOffset(currentIndex),
combineFileSplit.getLength(currentIndex),
combineFileSplit.getLocations());
lineRecordReader.initialize(fileSplit, context);
this.paths = combineFileSplit.getPaths();
totalLength = paths.length;
context.getConfiguration().set("map.input.file.name",combineFileSplit.getPath(currentIndex).getName());
}
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
if (currentIndex >=0&¤tIndex< totalLength){
return lineRecordReader.nextKeyValue();
}else{
return false;
}
}
@Override
public BytesWritable getCurrentValue() throws IOException,InterruptedException {
byte[] content = lineRecordReader.getCurrentValue().getBytes();
currentValue.set(content,0,content.length);
return currentValue;
}
@Override
public float getProgress() throws IOException, InterruptedException {
if(currentIndex >=0&& currentIndex < totalLength){
currentProgress = (float) currentIndex / totalLength;
return currentProgress;
}
return currentProgress;
}
@Override
public void close() throws IOException {
lineRecordReader.close();
}
如果存在这样的应用场景,你的小文件具有不同的格式,那么就需要考虑对不同类型的小文件,使用不同的内置RecordReader,具体逻辑也是在上面的类中实现。
我们已经为CombineFileSplit实现了一个RecordReader,然后需要在一个
CombineFileInputFormat中注入这个RecordReader类实现类
CombineSmallfileRecordReader的对象。这时,需要实现一个CombineFileInputFormat的子类,可以重写createRecordReader方法。我们实现的CombineSmallfileInputFormat,代码如下所示:
CombineSmallfileInputFormat类
import java.io.IOException;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.CombineFileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.CombineFileRecordReader;
import org.apache.hadoop.mapreduce.lib.input.CombineFileSplit;
public class CombineSmallfileInputFormat extends CombineFileInputFormat{
@Override
public RecordReader createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException {
CombineFileSplit combineFileSplit = (CombineFileSplit) split;
//这里比较重要的是,一定要通过CombineFileRecordReader来创建一个RecordReader
//而且它的构造方法的参数必须是上面的定义的类型和顺序。
//构造方法包含3个参数:第一个是CombineFileSplit类型,第二个是TaskAttemptContext类型,第三个是Class类型。
CombineFileRecordReader recordReader =new CombineFileRecordReader(combineFileSplit, context, CombineSmallfileRecordReader.class);
try {
recordReader.initialize(combineFileSplit, context);
} catch (InterruptedException e) {
e.printStackTrace();
}
return recordReader;
}
}
CombineSmallfileMapper类
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
public class CombineSmallfileMapper extends Mapper{
private Text file=new Text();
@Override
Mapper.Context context)throws IOException,InterruptedException {
String fileName=context.getConfiguration().get("map.input.file.name");
file.set(fileName);
context.write(file, new Text(new String(value.getBytes()).trim()));
}
}
CombineSmallfiles类
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;
public class CombineSmallfiles {
public static void main(String[] args) throws Exception {
Configuration conf=new Configuration();
Job job =Job.getInstance(conf);
job.setJarByClass(CombineSmallfiles.class);
job.setMapperClass(CombineSmallfileMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(BytesWritable.class);
job.setInputFormatClass(CombineSmallfileInputFormat.class);
FileInputFormat.setInputPaths(job, new
Path("hdfs://192.168.234.21:9000/score"));
FileOutputFormat.setOutputPath(job, new
Path("hdfs://192.168.234.21:9000/score/result"));
job.waitForCompletion(true);
}
}