Streaming那一套就先不管了,提交作业部分的代码肯定是一样的,只不过客户端提交的方式不一样。
很多人都从wordCount看起,看吧,我擦。
Configuration conf = new Configuration();
String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
if (otherArgs.length != 2) {
System.err.println("Usage: wordcount <in> <out>");
System.exit(2);
}
Job job = new Job(conf, "word count");
job.setJarByClass(WordCount.class);
job.setMapperClass(TokenizerMapper.class);
job.setCombinerClass(IntSumReducer.class);
job.setReducerClass(IntSumReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
main方法如上,一般的都是先搞一个jobconf对象,所谓jobconf对象就是一个专门管理作业配置文件的对象,一般都是继承Configured类,这里就不多废话了。
然后配置好mapper,reducer,输入输出数据类型,数据输入输出路径等等,就调用submit方法或者runjob,这样就得到一个RunningJob对象,作业的执行过程可以通过这个runningJob对象得到管理。但是这里好像又包装了几层,进去waitForCompletion看一下
public boolean waitForCompletion(boolean verbose
) throws IOException, InterruptedException,
ClassNotFoundException {
if (state == JobState.DEFINE) {
submit();
}
if (verbose) {
jobClient.monitorAndPrintJob(conf, info);
} else {
info.waitForCompletion();
}
return isSuccessful();
}
这里还是有submit,直接进去看看吧,下边的就不管了,是对作业运行过程的监控管理。
public void submit() throws IOException, InterruptedException,
ClassNotFoundException {
ensureState(JobState.DEFINE);
setUseNewAPI();//估计1.0.3API有一些变化,先不管了
// Connect to the JobTracker and submit the job
connect();
info = jobClient.submitJobInternal(conf);
super.setJobID(info.getID());
state = JobState.RUNNING;
}
首先确定作业的状态,作业看起来有两种状态一种叫define状态,就是没开始执行吧,一种叫running,就是正在运行,先这么理解吧,擦。
下一步就是connect,什么叫connect,connect谁?其实就是JobClient的初始化过程,同时让去连接jobtracker,abaci中就是去找metamaster。
关键代码看下哈:
public void init(JobConf conf) throws IOException {
String tracker = conf.get("mapred.job.tracker", "local");//看到没有根据mapred.job.tracker 找到jt,还有本地模式?
tasklogtimeout = conf.getInt(
TASKLOG_PULL_TIMEOUT_KEY, DEFAULT_TASKLOG_TIMEOUT);
this.ugi = UserGroupInformation.getCurrentUser();
if ("local".equals(tracker)) {
conf.setNumMapTasks(1);
this.jobSubmitClient = new LocalJobRunner(conf);
} else {
this.jobSubmitClient = createRPCProxy(JobTracker.getAddress(conf), conf);//建立rpc代理,不说了,so easy?
}
}
接着提交代码往下看,connect之后,开始submitJobInternal
进去主要看下关键代码片段吧
1、
Path jobStagingArea = JobSubmissionFiles.getStagingDir(JobClient.this,
jobCopy);
得到staging目录,我擦,什么是staging目录?通过追踪可以发现,所谓staging目录实际上是
<name>mapreduce.jobtracker.staging.root.dir</name>配置的目录下,比如/user下,那么staging目录就是/user/username/.staging/,也就是各个用户“家”目录下的.staging 目录,这里没有就创建它,至于这个目录干啥用,先不管。但是猜测可能就是用户作业信息存放的地方,tt正是从这里拿数据执行任务。
2、
JobID jobId = jobSubmitClient.getNewJobId();
拿到jobId,jt给的,咋生成的?看JobID构造方法,
public synchronized JobID getNewJobId() throws IOException {
return new JobID(getTrackerIdentifier(), nextJobId++);
}
public static final String JOBID_REGEX =
JOB + SEPARATOR + "[0-9]+" + SEPARATOR + "[0-9]+";
合法的格式是:job_一个或多个数字_一个或多个数字
平常我们看见的jobID一般都是job_201310141042_0036这样的,中间的数字好像有时间的特征,
通过追踪,可以看到trackerIdentifier是这么得来的,的确是日期,格式是年月日时分
private static SimpleDateFormat getDateFormat() {
return new SimpleDateFormat("yyyyMMddHHmm");
}
private static String generateNewIdentifier() {
return getDateFormat().format(new Date());
}
private int nextJobId = 1;初始1 ,那说明,作业号是递增,对后面的数字位数并没有限制。是从1递增的,也可以看做是jobtracker从启动之后接受提交作业的总数,int类型我擦,最大20+亿个吧。先不管了,这样jobID就有了。
3、在用户的staging目录构建一个以作业号命名的目录。叫做提交作业目录,对,staging就是用户的提交作业目录的地方。mapreduce.job.dir就是tt用于拿作业信息的地方。
Path submitJobDir = new Path(jobStagingArea, jobId.toString());
jobCopy.set("mapreduce.job.dir", submitJobDir.toString());
4、什么叫做copyAndConfigureFiles(jobCopy, submitJobDir)?
private void copyAndConfigureFiles(JobConf job, Path jobSubmitDir)
throws IOException, InterruptedException {
short replication = (short)job.getInt("mapred.submit.replication", 10);
copyAndConfigureFiles(job, jobSubmitDir, replication);
// Set the working directory
if (job.getWorkingDirectory() == null) {
job.setWorkingDirectory(fs.getWorkingDirectory());
}
}
这段代码首先获取submit的副本数?什么叫副本数,作业提交的过程中需要放到submitdir一些作业task运行相关的数据,供tt去获取,提高一定的副本数能让这个过程更快,如果只有一个副本,我擦,很多tt都向一台机器拿数据,这台机器就成了热点,所以配置了这个副本数,集群实际配置是16个。这个数字是否合适,回头再看。
下边的copyAndConfigureFiles比较复杂,就不全部贴出来了,分析代码片段如下:
a、先要根据配置确保submitdir的存在,比如用户a,那就得保证/user/a/.staging/目录准备好,然后会创建三个目录
public static Path getJobDistCacheFiles(Path jobSubmitDir) {
return new Path(jobSubmitDir, "files");
}
public static Path getJobDistCacheArchives(Path jobSubmitDir) {
return new Path(jobSubmitDir, "archives");
}
public static Path getJobDistCacheLibjars(Path jobSubmitDir) {
return new Path(jobSubmitDir, "libjars");
}
files,archives,libjars干啥用的,继续往下看就知道了。
b、创建好作业的这三个目录,就开始往里扔数据了。
还记得streamming提交作业的时候不,用-file 参数指定要分发的东西,比如我的mapper是个sh map.sh,就是说让tt在本机上执行map.sh这个脚本,但是你如果不用-file指定这个map.sh,作业就没法跑,因为这个map.sh是你自己知道,tt不知道啊,必须用-file指定分发的文件位置,就是这个map.sh的位置,这里就是把你指定的这个文件拷到集群上的刚才说到的files目录里,并且按照16副本保存。同时做好cache,要不然到时候再找建立对这个集群文件的链接又费时间,具体怎么cache的,先不管了。
同样的,存档、还有以来的jar文件放到对应的archives和libjars目录中。
c、如果作业本身是java写的,一定要打成jar包,在提交的过程中指定你的jar包在哪里,所以下面的一步就是找到用户提交指定的jar包。然后找到这个jar包,传到集群上去,这个名字也是固定的,就是/user/username/.staging/jobid/job.jar,即统一都叫job.jar,设置副本数啥的就不多说了。
以上的目录文件权限配置咱就不管了,重要的是分析清楚作业提交的流程。注意,如果你没给你的作业起名,在C这一步就会用你打的作业的jar包的名字作为作业的名字。
再回头看copyAndConfigureFiles,啥意思,就是从本地往集群拷贝吧?什么叫ConfigureFiles就是设置文件权限、副本数以及cache住这些文件路径吧?
当然整个Configure的过程也是比较重要的,对于解决实际问题应该有很多帮助,但是这里就不再详述了,继续往下吧。
以上都是给作业运行需要的文件创建好集群上保存这些东西的地方,同时把文件传到集群上去,并设置副本数,有了submit目录,还要有一个工作目录,工作目录也是配置的:
就是mapred.working.dir参数指定的hdfs的目录,如果没有配的话,工作目录会跑到用户的家目录下。
5、到目前为止,作业的所有配置信息还都保存在一个jobconf对象里,当然这里还想还有个JobContext对象,不管它是啥,总之作业配置信息还没有落地,因此下面就是把作业配置信息写到作业配置文件中,就是打开作业的运行状态页面看到的job.xml文件。
return new Path(jobSubmitDir, "job.xml");
看下面的代码片段,作业配置信息是怎么写的:
int reduces = jobCopy.getNumReduceTasks();//先获取用户配置了多少个reduce任务
InetAddress ip = InetAddress.getLocalHost();
if (ip != null) {
job.setJobSubmitHostAddress(ip.getHostAddress());
job.setJobSubmitHostName(ip.getHostName());
}//哪个机器提交的这个作业
JobContext context = new JobContext(jobCopy, jobId);//记录了运行作业所需材料存储的目录的位置信息等。
// Check the output specification
if (reduces == 0 ? jobCopy.getUseNewMapper() :
jobCopy.getUseNewReducer()) {//注意这里,如果reduce个数为0,就是纯map作业,比如distcp,那么这里因为版本的变化,确定要不要使用新api
org.apache.hadoop.mapreduce.OutputFormat<?,?> output =
ReflectionUtils.newInstance(context.getOutputFormatClass(),
jobCopy);
output.checkOutputSpecs(context);//检查输出目录是否已经配置,是不是存在,以上都不对的话,就会抛出异常。
} else {
jobCopy.getOutputFormat().checkOutputSpecs(fs, jobCopy);
}//这上边这几段代码先不管它接着往下看。
jobCopy = (JobConf)context.getConfiguration();
// Create the splits for the job
FileSystem fs = submitJobDir.getFileSystem(jobCopy);
LOG.debug("Creating splits at " + fs.makeQualified(submitJobDir));
int maps = writeSplits(context, submitJobDir);//开始写分片信息,写分片的方式有新旧之分,
jobCopy.setNumMapTasks(maps);
// write "queue admins of the queue to which job is being submitted"
// to job file.
String queue = jobCopy.getQueueName();
AccessControlList acl = jobSubmitClient.getQueueAdmins(queue);
jobCopy.set(QueueManager.toFullPropertyName(queue,
QueueACL.ADMINISTER_JOBS.getAclName()), acl.getACLString());
// Write job file to JobTracker's fs
FSDataOutputStream out =
FileSystem.create(fs, submitJobFile,
new FsPermission(JobSubmissionFiles.JOB_FILE_PERMISSION));
try {
jobCopy.writeXml(out);
} finally {
out.close();
}
注意在写job.xml配置文件之前就要先对输入数据分片,什么是分片?比如你要运算的输入数据有2个T,我日你不分片怎么让任务执行?mapred的原理就是让多个tt去处理一部分数据然后把结果汇总起来,所以必须得分片。到了1.0.3版本分片有了新的方法,但是你仍然可以选择旧的方式,下面看看怎么分吧(old方式):
private int writeOldSplits(JobConf job, Path jobSubmitDir)
throws IOException {
org.apache.hadoop.mapred.InputSplit[] splits =
job.getInputFormat().getSplits(job, job.getNumMapTasks());//从作业配置文件拿用户配置的map数,如果没配置默认是1
先要getSplits,我日,因为输入文件可以用多种格式,文本,文件啊,二进制啊,都行,但是你要怎么分割,只有知道数据格式的人才知道怎么分吧?对吧,所以实现了inputFormat的类一定要提供自己的分片方案,以FileInputFormat为例,看看咋分的:
public InputSplit[] getSplits(JobConf job, int numSplits)
throws IOException {
FileStatus[] files = listStatus(job);//进去看就知道,先要根据你作业初始化的时候指定的inputpath找到你要输入的文件,拿到文件的状态。
//基本上,输入文件一定在hdfs上。
// Save the number of input files in the job-conf
job.setLong(NUM_INPUT_FILES, files.length);//有多少个输入文件不需要用户自己指定,系统这就给你算出来了。
long totalSize = 0; // compute total size
for (FileStatus file: files) { // check we have valid files
if (file.isDir()) {
throw new IOException("Not a file: "+ file.getPath());
}
totalSize += file.getLen();
}//计算输入文件的大小总和,也就是输入数据总量,如果输入路径里边有目录,我擦,那是不允许的。
long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);//numSplits就是传进来的map的个数,这是用户指定的,缺省是1,如果配置成0,我日那就是整片了。作为这里的分片个数,如果没指定就整片搞了。goalSize就是每片的大小。
long minSize = Math.max(job.getLong("mapred.min.split.size", 1),
minSplitSize);
//片也不能忒小,所以default应该会给个最小片,集群给的是0,对于fileInputFormat类型,minSplitSize固定等于1,没有地方再去set它。
// generate splits
ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);//准备好东西盛啊
NetworkTopology clusterMap = new NetworkTopology();//我日这个集群的拓扑逻辑图,这个东西比较经典,回头再去专门分析它
for (FileStatus file: files) {//对输入的每一个文件进行如下处理
Path path = file.getPath();
FileSystem fs = path.getFileSystem(job);
long length = file.getLen();
BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0, length);//拿到这个文件的块位置信息。
if ((length != 0) && isSplitable(fs, path)) { //FileInputFormat这种输入格式写死了一定可以切分,isSplitable总返回true。
long blockSize = file.getBlockSize();
long splitSize = computeSplitSize(goalSize, minSize, blockSize);
//确定最后的切片大小,介于块大小和最小片大小之间。
long bytesRemaining = length;
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {//这个SPLIT_SLOP=1.1,啥意思,就是说,如果切完之后剩余1.1个切片量的时候就别切了,实际上不会那么精准,肯定是能切到最后不足一个分片就停下了。
String[] splitHosts = getSplitHosts(blkLocations,
length-bytesRemaining, splitSize, clusterMap);//这个很有意思,回头专门研究一下,也就是说知道了文件的offset,有了dn的拓扑图以及块位置信息,你要给我找到分片在哪些机器上,所以从这里就能看出来,分片分得啥,其实就是逻辑上的分片,分得是机器列表,是数据的地理信息的分片。
splits.add(new FileSplit(path, length-bytesRemaining, splitSize,
splitHosts));//盛下一个个的分片。
bytesRemaining -= splitSize;
}
if (bytesRemaining != 0) {
splits.add(new FileSplit(path, length-bytesRemaining, bytesRemaining,
blkLocations[blkLocations.length-1].getHosts()));
}//把最后那个不足一个分片大小的剩下的数据放进去吧。
} else if (length != 0) {
String[] splitHosts = getSplitHosts(blkLocations,0,length,clusterMap);
splits.add(new FileSplit(path, 0, length, splitHosts));
} else {
//Create empty hosts array for zero length files
splits.add(new FileSplit(path, 0, length, new String[0]));
}
}//这些else就不管了,不分或者怎样,先随他去吧
LOG.debug("Total # of splits: " + splits.size());
return splits.toArray(new FileSplit[splits.size()]);
}
关于如何确定最终的切片大小:
protected long computeSplitSize(long goalSize, long minSize,
long blockSize) {
return Math.max(minSize, Math.min(goalSize, blockSize));
//分块大小与文件块大小中较小的取出来,跟集群配置的允许的最小切片比较,取较大的。
//
}
啥意思?也就是说你根据输入数据总量和map数计算出来的切片大小不能超过一个块大小,超过了切片大小就按集群块大小来实施切分。太小了也不行不能低于允许的最下片。
因此分片大小就这样被限制在块大小和最小值之间了。片最大就是hdfs的标准一块!当然这是FileInputFormat这么规定的,其它的切片方案咋整的,那不一定。
至于为啥这个分片大小要这么取,是值得探讨的,如果说map数非常大,导致分片正好处于这个区间内,但是小于块大小,那一个文件切分的话会发生什么?这些问题留到专门的一篇中去介绍吧。先继续看作业的提交流程。
再回到writeOldSplits方法中,拿到各个分片的host信息等之后,对分片进行排序,排序的方案是实际分得结果里各个分片的byte数,虽然之前指定了切片大小,但是因为切片有可能比块大小小,所以有时候会把整个块给一个切片,这种情况也不是没有,另外在每个机器上块大小也不是绝对等于配置的256M或者128M的,所以造成这里需要排序。
public static void createSplitFiles(Path jobSubmitDir,
Configuration conf, FileSystem fs,
org.apache.hadoop.mapred.InputSplit[] splits)
throws IOException {
FSDataOutputStream out = createFile(fs,
JobSubmissionFiles.getJobSplitFile(jobSubmitDir), conf);//往/user/username/.staging/jobid/job.split写切分元数据,叫做SplitMetaInfo
SplitMetaInfo[] info = writeOldSplits(splits, out, conf);
out.close();
writeJobSplitMetaInfo(fs,JobSubmissionFiles.getJobSplitMetaFile(jobSubmitDir),
new FsPermission(JobSubmissionFiles.JOB_FILE_PERMISSION), splitVersion,
info);
}
看上面,切片信息都拿到了,现在需要把这些信息都写到staging目录里,因为tt要从这里拿嘛。
public static Path getJobSplitFile(Path jobSubmissionDir) {
return new Path(jobSubmissionDir, "job.split");
}
首先split目录在/user/username/.staging/jobid/下面,这个文件叫做job.split,
文件切分的元数据到底是啥,写了啥东西,这里也不讲了,回头单独发一篇。
6、根据实际切片数,重置map数。
int maps = writeSplits(context, submitJobDir);
jobCopy.setNumMapTasks(maps);
7、还要知道是哪个队列,我擦,队列在公平调度算法中基本上没啥用了,唉,回头再说队列的问题吧,反正这里就完成了job.xml的写入
String queue = jobCopy.getQueueName();
AccessControlList acl = jobSubmitClient.getQueueAdmins(queue);
jobCopy.set(QueueManager.toFullPropertyName(queue,
QueueACL.ADMINISTER_JOBS.getAclName()), acl.getACLString());
// Write job file to JobTracker's fs
FSDataOutputStream out =
FileSystem.create(fs, submitJobFile,
new FsPermission(JobSubmissionFiles.JOB_FILE_PERMISSION));
try {
jobCopy.writeXml(out);
} finally {
out.close();
}
8、下面开始真正向jobtracker提交作业了
public JobStatus submitJob(JobID jobId, String jobSubmitDir, Credentials ts)
告诉jt我的作业号,实际上是jt你妈的给我的,然后告诉你我的作业的信息存哪了,告诉你我的身份,我是尼玛合法用户。
synchronized (this) {
if (jobs.containsKey(jobId)) {
// job already running, don't start twice
return jobs.get(jobId).getStatus();
}
jobInfo = new JobInfo(jobId, new Text(ugi.getShortUserName()),
new Path(jobSubmitDir));
}
看上面,jobtracker先检查这个jobid是不是已经存在了,jt有个乔布斯(jobs)替他管理者所有正在处理的作业,结构就是jobid做key,value是JobInProcess。
如果已经有了就告你这个作业号的状态,我擦,什么情况下会重复提交相同id的作业?
如果没有就构造一个JobInfo来描述你的这个请求。很简单不说了这个JobInfo,就是id,用户,作业提交信息(staging)的路径...
JobInProgress job = null;
try {
job = new JobInProgress(this, this.conf, jobInfo, 0, ts);
} catch (Exception e) {
throw new IOException(e);
}
接下来,看上面,构造JobInProgress,至于这个东西是啥,比较麻烦,也是回头专门再看吧。
但你知道这是个描述正在处理的作业的结构就行了。
再往下就是检查内存,这部分也后面单独讲。
还有就是恢复作业的处理,这部分代码目前好像没有做处理,先不讲了。
JobStatus status;
try {
status = addJob(jobId, job);
} catch (IOException ioe) {
LOG.info("Job " + jobId + " submission failed!", ioe);
status = job.getStatus();
status.setFailureInfo(StringUtils.stringifyException(ioe));
failJob(job);
throw ioe;
}
return status;
}
看上面,就是把作业加到可调度的作业集合中,拿到作业状态JobStatus。
private synchronized JobStatus addJob(JobID jobId, JobInProgress job)
throws IOException {
totalSubmissions++;
synchronized (jobs) {
synchronized (taskScheduler) {
jobs.put(job.getProfile().getJobID(), job);
for (JobInProgressListener listener : jobInProgressListeners) {
listener.jobAdded(job);
}
}
}
myInstrumentation.submitJob(job.getJobConf(), jobId);
job.getQueueMetrics().submitJob(job.getJobConf(), jobId);
LOG.info("Job " + jobId + " added successfully for user '"
+ job.getJobConf().getUser() + "' to queue '"
+ job.getJobConf().getQueueName() + "'");
AuditLogger.logSuccess(job.getUser(),
Operation.SUBMIT_JOB.name(), jobId.toString());
return job.getStatus();
}
看上面,totalSubmissions++,说明jobtracker弄了一个全局计数器,记录总提交得作业数
然后同步乔布斯,还记得乔布斯吗?
Map<JobID, JobInProgress> jobs =
Collections.synchronizedMap(new TreeMap<JobID, JobInProgress>());
同步作业调度器,把这个新加的作业塞进去。
JobInProgressListener就属于作业调度器的范畴了,至于JobTrackerInstrumentation,不用管它,貌似是想做metric或者类似日志的东西,但是其实啥也没干,所有不管它了。
到这里基本上可以对作业提交总结一下:
1、作业提交首先要配置好你的作业,map数,reduce数,输入输出文件路径,输入输出文件格式类等,还要给作业起名。
2、作业staging阶段,所谓staging就是在hdfs上为这个作业弄个submit目录,将要分发的文件啊,压缩文件啊,还有依赖的jar包啊,put到集群上去。
然后还有job.jar(作业本身打的jar包,如果是java作业的话)。
3、对输入数据进行切分,怎么切要根据输入数据的格式,这个不同的有不同的getsplits的方式,但是切分的实质是找到分片对应的数据所在的位置,分片大小和重置map数就不多说了。
4、将切分的元数据写到job.split文件中。
5、注意,切分完了重置map数之后才开始写job.xml
6、作业提交给jobtracker,实际上是把新的jobInprogress塞到jobs的过程,这个jobs就是被作业调度器调度的对象。
作业提交实际上只是提交,只是把一切都准备好,弄个描述这个作业的对象交给乔布斯保存,或者叫cache就完了。
而作业得到执行或者调度,就不是客户端能控制的啦,而是作业调度器要干的了...