文章目录
- MapRdeuce的执行逻辑图
- Client概述
- Split 分片
- 分片的目的
- 分片的大小
- 为什么分片的大小最好是趋向于HDFS的一个块的大小
- 源码分析
MapRdeuce的执行逻辑图
一个MapReduce作业是客户端需要执行的一个工作单元:它包括输入数据,MapReduce程序和配置信息。Hadoop将作业分为若干个task来执行,其中主要包括两类:map任务和reduce任务。这些任务运行在集群的节点上,并通过YARN进行调度。一个完整的MapReduce程序由client,map,reduce这三大块组成。
Client概述
- 检查作业的输入、输出的配置是否完整
- 完成Input Splits(输入分片)的划分与计算
- 复制作业的jar包、配置文件到分布式文件系统HDFS的目录中
- 提交作业到JobTracker,同时监控作业的状态
Split 分片
Hadoop将MapReduce的输入数据划分成为等长的的小数据块,称为输入分片(Input Split),如上图所示。Hadoop为每个分片构建一个map任务,并由改任务来运行用户自定义的map函数,从而处理分片中的每一条记录。
分片的目的
将一个大的数据文件切分成多个等长的小的分片,意味着处理每个分片的时间要小于处理整个输入数据所花费的时间。因此,将一个较大的数据输入文件,切分成多个较小的分片,并且并行的处理每个分片,将大大的缩短我们处理整个数据输入的时间。
分片的大小
如果分片太大,以为着分片的数量减少,处理单个分片的时间也相应较长,不能合理的发挥出集群的并行处理能力。如果分片足够小,理论上来说整个过程将获得更好的负载均衡,因为一台较快的计算机能处理的分片数据比一台较慢的计算机更多,所以随着分片被切分的更细,负载均衡的质量会更高。
但是,如果分片切分的太细,那么管理分片的总时间和构建map任务的总时间又会增加,从而使整个作业的执行时间增加。所以对于大多数作业来说,一个合理的分片大小趋向于HDFS的一个块的大小,默认是128M。当然这个值也可以调整。
为什么分片的大小最好是趋向于HDFS的一个块的大小
- Hadoop会尽可能的在存储有输入数据文件的节点上运行map任务,这样就可以获取到最佳性能,因为它无需使用宝贵的集群带宽资源,这就是数据本地化优化
- 但是有时候对于一个map任务的输入分片来说,存储该分片的HDFS数据块副本的所有节点有可能正在运行其他的map任务,没有空闲的资源来运行map任务处理这一分片,这时hadoop需要找到一个空闲的节点来运行该map分片任务,这将导致该分片需要从存储数据块的节点网络传输到现在的节点,从而增加网络资源的消耗和等待的时长(仅仅是非常偶然的情况)。所以如果一个分片跨越了两个数据块,那么对于任何一个HDFS节点来说,基本上都不可能同时存储这两个数据块,因此对于分片中的部分数据就需要通过网络传输到map任务运行的节点,从而降低了整个数据处理的效率。所以一个合理的分片大小就是和HDFS的数据块的大小趋同。
TIPS:切片并不是将文件真的切分成多个部分,只是逻辑上描述一个数据范围的机制
源码分析
一个完整的mapreduce作业的客户端的主要做的事情:
- 检查作业的输入、输出的配置是否完整
- 完成Input Splits(输入分片)的划分与计算
- 复制作业的jar包、配置文件到分布式文件系统HDFS的目录中
- 提交作业到JobTracker,同时监控作业的状态
在一个job中,作业的执行或提交一般调用如下两个方法,这是MapReduce作业的入口
//执行作业
job.waitForCompletion(true)
//或者调用job.sumbit()
job.submit();
无论是上面哪个方法,最终都会调用到org.apache.hadoop.mapreduce.Job类中的submit方法,主要源码如下:
public void submit()
throws IOException, InterruptedException, ClassNotFoundException {
ensureState(JobState.DEFINE);
setUseNewAPI();
connect();
final JobSubmitter submitter =
getJobSubmitter(cluster.getFileSystem(), cluster.getClient());
status = ugi.doAs(new PrivilegedExceptionAction<JobStatus>() {
public JobStatus run() throws IOException, InterruptedException,
ClassNotFoundException {
return submitter.submitJobInternal(Job.this, cluster);
}
});
state = JobState.RUNNING;
LOG.info("The url to track the job: " + getTrackingURL());
}
在这个方法中构建了一个JobSubmitter,由这个对象来执行核心的操作,调用JobSubmitter的submitJobInternal方法执行分片计算,复制配置文件、Jar包到HDFS,以及提交作业。
JobStatus submitJobInternal(Job job, Cluster cluster)
throws ClassNotFoundException, InterruptedException, IOException {
//验证作业的输入、输出等配置项
checkSpecs(job);
Path submitJobDir = new Path(jobStagingArea, jobId.toString());
JobStatus status = null;
//创建作业的分片
LOG.debug("Creating splits at " + jtFs.makeQualified(submitJobDir));
int maps = writeSplits(job, submitJobDir);
conf.setInt(MRJobConfig.NUM_MAPS, maps);
LOG.info("number of splits:" + maps);
//提交作业的配置文件
writeConf(conf, submitJobFile);
//
// 提交作业
//
printTokens(jobId, job.getCredentials());
status = submitClient.submitJob(
jobId, submitJobDir.toString(), job.getCredentials());
}
writeSplits 最终会调用到FileInputFormat类的getSplits方法:
- 该方法会为每一个输入文件进行分片计算操作。
- hadoop默认的分片最小值是1,可以通过FileInputFormat.setMinInputSplitSize(job,6000);方法进行设置;
- hadoop默认的分片最大值是Long的最大值,可以通过FileInputFormat.setMaxInputSplitSize(job,60000);进行设置
- 由如下代码可知:如果用户不指定分片的大小,那么默认的大小就是文件块的大小
- 分片信息由:文件的路径,起始偏移量,分片大小,文件块所在的主机列表,和文件块的副本所在的主机列表
public List<InputSplit> getSplits(JobContext job) throws IOException {
StopWatch sw = new StopWatch().start();
//分片的最小值:默认是1
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
//分片的最大值:默认是Long的最大值,可以由参数“mapreduce.input.fileinputformat.split.maxsize”进行设置
long maxSize = getMaxSplitSize(job);
// generate splits
List<InputSplit> splits = new ArrayList<InputSplit>();
List<FileStatus> files = listStatus(job);
boolean ignoreDirs = !getInputDirRecursive(job)
&& job.getConfiguration().getBoolean(INPUT_DIR_NONRECURSIVE_IGNORE_SUBDIRS, false);
for (FileStatus file: files) {
if (ignoreDirs && file.isDirectory()) {
continue;
}
Path path = file.getPath();
long length = file.getLen();
if (length != 0) {
BlockLocation[] blkLocations;
if (file instanceof LocatedFileStatus) {
blkLocations = ((LocatedFileStatus) file).getBlockLocations();
} else {
FileSystem fs = path.getFileSystem(job.getConfiguration());
blkLocations = fs.getFileBlockLocations(file, 0, length);
}
if (isSplitable(job, path)) {
//获取文件的块的大小
long blockSize = file.getBlockSize();
//比较块大小,设置的最小分片值,最大的分片值,取Math.max(minSize, Math.min(maxSize, blockSize));最大值
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
long bytesRemaining = length;
//进行分片操作,计算出每一个分片的起始位置
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
//构建一个分片,分片信息由:文件的路径,起始偏移量,分片大小,文件块所在的主机列表,和文件块的副本所在的主机列表
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
bytesRemaining -= splitSize;
}
//并取出每一个分片所在的文件路径,块所在的机器列表
if (bytesRemaining != 0) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
}
} else { // not splitable
if (LOG.isDebugEnabled()) {
// Log only if the file is big enough to be splitted
if (length > Math.min(file.getBlockSize(), minSize)) {
LOG.debug("File is not splittable so no parallelization "
+ "is possible: " + file.getPath());
}
}
splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(),
blkLocations[0].getCachedHosts()));
}
} else {
//Create empty hosts array for zero length files
splits.add(makeSplit(path, 0, length, new String[0]));
}
}
// Save the number of input files for metrics/loadgen
job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
sw.stop();
if (LOG.isDebugEnabled()) {
LOG.debug("Total # of splits generated by getSplits: " + splits.size()
+ ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
}
return splits;
}