mapreduce job提交到yarn分析
相关类
Configuration
配置job,如果不配置则使用默认配置。
Job
封装了一个job运行的信息。
Cluster
代表本地连接ResourceManager和文件系统的一个对象;内部封装了JobRunner和运行时的文件系统信息;如果是本地模式则JobRunner为localJobRunner,和本地文件系统,集群模式为YarnRunner和hdfs。
ClientProtocolProvider
用于生成JobRunner;如果是本地模式则ClientProtocolProvider为LocalClientProtocolProvider,集群模式为YarnClientProtocolProvider。
ServiceLoader
用来加载ClientProtocolProvider。
InputFormat
对作业进行逻辑切片。
JobSubmitter
提交一个job;包括获取jobId,提交目录路径,上传作业运行的文件(jar,切片信息文件,job.xml)。
提交过程
新建作业
-
新建Configuration对象,通过该对象的set()方法,设置作业运行的配置参数和连接hadoop集群的所需参数。
-
新建Job对象,将上述Configuration对象传入Job对象中。
-
设置Job运行时参数,jar本地位置,map和reduce全类名,输入输出路径等等。
作业启动
- 通过调用Job对象的Job#waitForCompletion()或者Job#submit()方法启动作业。
Job#submit
-
首先确认作业状态为DEFINE;然后调用setUseNewAPI()判断是否使用新API,默认使用新的API。
-
调用Job#connect()连接集群;首先判断Cluster对象是否存在,如果不存在则最终调用Cluster#initialize()创建一个Cluster对象代表本地和集群的连接。
-
创建一个JobSubmitter对象用于提交作业。
-
使用上述JobSubmitter对象调用JobSubmitter#submitJobInternal()方法提交作业到集群。
Cluster#initialize()
-
通过foreach语句,使用ServiceLoader加载ClientProtocolProvider;首先ServiceLoader会创建一个LocalClientProtocolProvider对象。
-
调用ClientProtocolProvider#create()方法准备生成JobRunner对象.
-
判断是否Configuration对象中设置的mapreduce.framework.name是否为yarn,如果没有设置则该LocalClientProtocolProvider对象直接生成LocalJobRunner对象并返回;如果设置了,则返回null并进入下一次foreach。
-
第二次运行forench将会返回YarnClientProtocolProvider,调用ClientProtocolProvider#create()方法返回YarnRunner对象并且连接ResourceManager。
JobSubmitter#submitJobInternal()
- 首先会检查输出路径是否满足要求;判断参数mapreduce.application.framework.path是否配置(该参数见官方文档)。
- 向Yarn获取一个jobId,生成一个提交目录的路径;将job的jar文件和该文件的crc校验文件提交到提交目录。
- 调用JobSubmitter#writeSplits()方法,如果使用newAPI最终调用JobSubmitter#writeNewSplits(),向提交目录写入切片信息文件。
- 上述函数继续调用InputFormat#getSplits()获取切片信息,因为默认的InputFormat对象是TextInputFormat。所以下面分析TextInputFormat的getSplit()方法;TextInputFormat继承自FileInputFormat。
- 获取到切片信息后,会对切片按照size大小进行降序排列。
- 写出切片信息文件和job.xml到提交目录。
- 提交作业。
FileInputFormat#getSplit()
-
获取最小切片和最大切片大小(单位B),然后获取输入目录下的所有文件的信息。切片只是逻辑切片,通过切片文件保存关于源文件的分割信息,而不是直接对源文件进行切分。
-
遍历输入文件:
-
获取文件在对应文件系统的块信息。
-
调用TextInputFormat判断当前配置是否支持切片,如果未设置压缩器则支持切片,如果设置了,则判断该压缩器是否是 SplittableCompressionCodec的子类,是则可切片。
-
如果不可切片,将整个文件构造一个大的逻辑切片返回。
-
如果可以切片,则获取该文件系统的文件块大小;更据(文件块大小,设置的最小切片size,最大切片size)计算切片大小,计算公式 Math.max(minSize, Math.min(maxSize, blockSize))。
-
循环进行切片,每次都判断剩余部分除以切片size是否大于1.1,大于则循环继续;否则直接将剩余部分添加到切片信息中。
部分代码
Job#submit()
//确认job状态
ensureState(JobState.DEFINE);
//设置api
setUseNewAPI();
//--------------------------------连接集群----------------------------------------
connect();
//getClient()返回的是JobRunner对象
JobSubmitter submitter =
getJobSubmitter(cluster.getFileSystem(), cluster.getClient());
status = ugi.doAs(new PrivilegedExceptionAction<JobStatus>() {
public JobStatus run() throws IOException, InterruptedException,
ClassNotFoundException {
//----------------------------提交job-----------------------------------------
return submitter.submitJobInternal(Job.this, cluster);
}
});
//将job状态设置为运行
state = JobState.RUNNING;
Cluster#initialize()
//ClientProtocolProvider
for (ClientProtocolProvider provider : frameworkLoader) {
//本地模式返回LocalJobRunner
//集群模式返回YarnRunner
clientProtocol = provider.create(conf);
}
JobSubmitter#submitJobInternal()
//添加MapReduce框架存档的路径到conf中
addMRFrameworkToDistributedCache(conf);
//获取jobId
JobID jobId = submitClient.getNewJobID();
//从得到一个job提交的目录。
Path submitJobDir = new Path(jobStagingArea, jobId.toString());
//将job的jar文件和该文件的crc校验文件提交到提交目录。
copyAndConfigureFiles(job, submitJobDir);
//切片并写出切片信息文件到提交目录
int maps = writeSplits(job, submitJobDir);
//写出job.xml到提交目录
writeConf(conf, submitJobFile);
JobSubmitter#writeNewSplits()
//获取配置的InputFormat对象,默认为TextInputFormat
InputFormat<?, ?> input =
ReflectionUtils.newInstance(job.getInputFormatClass(), conf);
//获取切片信息。
List<InputSplit> splits = input.getSplits(job)
// sort the splits into order based on size, so that the biggest
// go first
Arrays.sort(array, new SplitComparator());
//将切片信息写出到提交目录
JobSplitWriter.createSplitFiles(jobSubmitDir, conf,
jobSubmitDir.getFileSystem(conf), array);
FileInputFormat#getSplit()
//----------------------------------------切片大小,单位是字节(B)----------------------------------------
long minSize = Math.max(getFormatMinSplitSize()/*long(1)*/, getMinSplitSize(job)/*job conf中可以设置最小切片大小,默认long(1)*/);
//job conf中可以设置最大切片大小,默认long型最大值
long maxSize = getMaxSplitSize(job);
//获取输入目录下的所有文件的信息。
List<FileStatus> files = listStatus(job);
//遍历文件
for (FileStatus file: files) {
//获取文件所在的块信息
if (file instanceof LocatedFileStatus) {
//从本地文件系统获取
blkLocations = ((LocatedFileStatus) file).getBlockLocations();
} else {
//从hdfs获取
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;
//写入切片信息
//写入时需要判断bytesRemaining/splitSize > SPLIT_SLOP
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP/*1.1*/) {
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
splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(),
blkLocations[0].getCachedHosts()));
}
TextInputFormat#isSplitable()
//根据文件后缀名获取对应的压缩器。该压缩器是map端输入压缩器。
final CompressionCodec codec =
new CompressionCodecFactory(context.getConfiguration()).getCodec(file);
if (null == codec) {
return true;
}
//返回该压缩器是否是SplittableCompressionCodec的子类,是则可以切片。
return codec instanceof SplittableCompressionCodec;
注意点总结
设置job切片大小的最小值和最大值
- 通过配置mapreduce.input.fileinputformat.split.minsize设置最小值,默认为1;mapreduce.input.fileinputformat.split.maxsize设置最大值,默认为long型最大值。
切片
- 切片只是逻辑切片,通过切片文件保存关于源文件的分割信息,而不是直接对源文件进行切分。
- 需要调用InputFormat#isSplitable()方法判断当前配置环境是否可以切片,在TextInputFormat#isSplitable()中,如果要能够切片,压缩器(该压缩器是map输入端压缩器)需要是SplittableCompressionCodec的子类;或者没有配置压缩器。
- 计算切片大小的公式为 Math.max(minSize, Math.min(maxSize, blockSize))。所以要调高splitSize需要调高minSize值>blockSize,要调低splitSize需要调低maxSize值<blockSize。
- 默认splitSize==blockSize。
- 循环切片:每次切片前当前文件剩余未处理大小除以splitSize大于1.1则对剩余部分进行切片(注意:虽然使用1.1而不是1进行判断,但是切片大小还是splitSize而不是splitSize*1.1),否则就直接将其添加到切片信息中。
- 会对切片按照size大小,降序排列。
staging目录文件的replication
- job.jar和job.split的replication等于参数mapreduce.client.submit.file.replication配置值;如果不配置该参数则默认时10.
- job.xml为hdfs设置的replication,默认是3