Hadoop的JVM重用机制和小文件解决

Hadoop的JVM重用机制和小文件解决

一、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里的配置示例:

	<!-- 开启uber模式(针对小作业的优化) --> 
	<property> 
		<name>mapreduce.job.ubertask.enable</name> 
		<value>true</value> 
	</property> 
	<!-- 配置启动uber模式的最大map数 --> 
	<property> 
		<name>mapreduce.job.ubertask.maxmaps</name> 
		<value>9</value> 
	</property> 
	<!-- 配置启动uber模式的最大reduce数 --> 
	<property> 
		<name>mapreduce.job.ubertask.maxreduces</name> 
		<value>1</value> 
	</property> 

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<LongWritable,BytesWritable>{
	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&&currentIndex< 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<LongWritable, BytesWritable>{
	@Override
	public RecordReader<LongWritable, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException {
		CombineFileSplit combineFileSplit = (CombineFileSplit) split;
		//这里比较重要的是,一定要通过CombineFileRecordReader来创建一个RecordReader
		//而且它的构造方法的参数必须是上面的定义的类型和顺序。
		//构造方法包含3个参数:第一个是CombineFileSplit类型,第二个是TaskAttemptContext类型,第三个是Class<? extends RecordReader>类型。
		CombineFileRecordReader<LongWritable, BytesWritable> recordReader =new CombineFileRecordReader<LongWritable, BytesWritable>(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<LongWritable, BytesWritable, Text,Text>{
	private Text file=new Text();
	@Override
	Mapper<LongWritable, BytesWritable, Text, Text>.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);
	}
}

猜你喜欢

转载自blog.csdn.net/qq_38019655/article/details/83819132