hadoop中mapreduce作业执行过程分析--local模式

本文详细剖析了Hadoop中Job提交的过程,包括JobClient的初始化、Staging目录的生成及异常处理、JobID的获取、输入分片的创建等关键步骤。特别针对在Windows环境下运行Hadoop时可能遇到的问题进行了说明。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写完回头看看,发现好像有点儿乱,对不住各位看官啦吐舌头,限于个人水平,有错误的地方请大家予以回复纠正。

 

环境:主机WIN7+Eclipse4.3,hadoop源码导入eclipse,虚拟机中运行Ubuntu13.04,伪分布式模式运行hadoop。在eclipse中调试wordcount,为了防止出现Failed to set permissions of path的异常,将org.apache.hadoop.fs.FileUtil类中checkReturnValue方法中的源码注释掉!注意:在主机eclipse中调试,实际上是使用单机模式跑job,与分布式环境的作业运行流程不完全相同,故本篇所讲内容仅具有参考意义。

 

        入口是job.waitForCompletion方法,进入该方法后,会执行Job类中的submit方法,该方法主要有下面两行代码:

    connect();
    info = jobClient.submitJobInternal(conf);

        我们从这里分两条路,为了方便梳理,将代码按照执行顺序进行编号:

        1.先看connect()方法,该方法中会new一个JobClient对象,在JobClient的构造函数中会去调用JobClient中的init方法,该方法的内容如下所示:

public void init(JobConf conf) throws IOException {
    String tracker = conf.get("mapred.job.tracker", "local");
    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.rpcJobSubmitClient = 
          createRPCProxy(JobTracker.getAddress(conf), conf);
      this.jobSubmitClient = createProxy(this.rpcJobSubmitClient, conf);
    }        
  }

因为我这里没有对mapred.job.tracker没有做特别的设置,所以得到的tracker应该是local,这种情况下会去创建一个LocalJobRunner对象。(注意:可以在conf中将mapred.job.tracker设置为hadoop中jobtracker的URL,这种情况会执行else语句块的内容,是会在hadoop中运行该job的,但是也会遇到一些错误,这里先不做介绍啦。)LocalJobRunner对象实现了JobSubmissionProtocol接口,我们知道hadoop中主要使用RPC进行通讯,此接口就是client与JobTracker对象通讯的协议,后面代码中的jobClient对象其实就是通过其内置的jobSubmitClient与JobTracker之间进行交互的。

        2.接下来我们再来看jobClient.submitJobInternal(conf)这段代码。该方法里面的代码相对来说比较复杂:

        2.1.首先会调用JobSubmissionFiles.getStagingDir()方法,该方法内部主要有两块儿代码,先看第一个:

Path stagingArea = client.getStagingAreaDir();

该方法内部最终调用的实际上是LocalJobRunner类中的getStagingAreaDir方法,代码如下:

Path stagingRootDir = 
      new Path(conf.get("mapreduce.jobtracker.staging.root.dir",
        "/tmp/hadoop/mapred/staging"));
    UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
    String user;
    randid = rand.nextInt(Integer.MAX_VALUE);
    if (ugi != null) {
      user = ugi.getShortUserName() + randid;
    } else {
      user = "dummy" + randid;
    }
    return fs.makeQualified(new Path(stagingRootDir, user+"/.staging")).toString();

从这个方法里我们看以看到staging root dir的生成,该目录主要用来存放job的相关资源(比如我们写的代码),由于此处我所使用的是LocalJobRunner,所以此PATH是在我们本地文件系统中生成的(上面最后一行代码),并且我们可以看到,Staging的根目录路径为${mapreduce.jobtracker.staging.root.dir}/${username}随机数/.staging。

        我们再来看JobSubmissionFiles.getStagingDir()方法中的另一块代码,这里有一个在windows中跑job的时候常遇到的错误。由于上面代码中的user中含有一个随机数,所以理论上绝大多数情况下我们都需要在staging root dir下面新建一个由用户名和随机数构成的目录,所以,我们会执行

fs.mkdirs(stagingArea, new FsPermission(JOB_DIR_PERMISSION));

 最终我们实际上执行的是RowLocalFileSystem类中的mkdirs(Path f, FsPermission permission)方法,该方法中又会去调用setPermission(f, permission)方法,此方法进一步会调用org.apache.hadoop.fs.FileUtil类的setPermission方法,最终又调用了FileUtil类中的checkReturnValue方法,该方法会抛出一个异常:

//if (!rv) {
//      throw new IOException("Failed to set permissions of path: " + p + 
//                            " to " + String.format("%04o", permission.toShort()));
//}

我们前面没有提到rv,这个变量是代表staging目录的File试图去设置linux文件系统中的read权限的结果,但是在windows中这样的操作是失败的,于是rv接收到的值就是false,所以一旦进入checkReturnValue,就会抛出上面的异常。由于事先已将该段代码注释掉了,所以程序可以继续运行下去,可以顺利的退出JobSubmissionFiles.getStagingDir()方法。

        2.2 JobID jobId = jobSubmitClient.getNewJobId(),这行代码会执行LocalJobRunner类中的getNewJobId方法,该方法中会创建一个JobID对象并返回。接下来执行

copyAndConfigureFiles(jobCopy, submitJobDir);
此方法会将job的配置信息和需要的资源文件(比如files、archives,libjars)拷贝到submitJobDir(staging root dir目录下面一个以JOBID命名的),如果是运行在hadoop环境中,还会把这些文件加入到DistributedCache中。
        2.3 接下来会检查输出路径:
          if (reduces == 0 ? jobCopy.getUseNewMapper() : 
            jobCopy.getUseNewReducer()) {
            org.apache.hadoop.mapreduce.OutputFormat<?,?> output =
              ReflectionUtils.newInstance(context.getOutputFormatClass(),
                  jobCopy);
            output.checkOutputSpecs(context);
          } else {
            jobCopy.getOutputFormat().checkOutputSpecs(fs, jobCopy);
          }
          
        2.4 如果输出路径不存在,就会接着往下运行创建inputSplits 
 FileSystem fs = submitJobDir.getFileSystem(jobCopy);
 LOG.debug("Creating splits at " + fs.makeQualified(submitJobDir));
 int maps = writeSplits(context, submitJobDir);
 jobCopy.setNumMapTasks(maps);
        2.4.1 这段代码中主要来看一下writeSplits方法,该方法内部会去判断是否使用的是新的Mapper,如果是,就进一度调用JobClient中的writeNewSplits方法,否则就调用writeOldSplits。我使用的是新的API,所以就进入了writeNewSplits方法,该方法的代码如下:
    Configuration conf = job.getConfiguration();
    InputFormat<?, ?> input =
      ReflectionUtils.newInstance(job.getInputFormatClass(), conf);

    List<InputSplit> splits = input.getSplits(job);
    T[] array = (T[]) splits.toArray(new InputSplit[splits.size()]);

    // 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);
    return array.length;
首先,通过反射得到InputFormat对象,调用其getSplits方法得到输入分片的集合,然后对分片进行排序,SlitComparator是按照分片的大小降序排列的。 之后调用JobSplitWriter.createSplitFiles方法去创建输入分片(位于前面的submitJobDir目录下job.split和job.splitmetainfo以及对应的crc校验文件)。
        2.5 这个操作结束后,我们就可以退回到Jobclient类的submitJobInternal 方法中啦。接下来的一行代码
jobCopy.setNumMapTasks(maps);
会把返回的分片的数目设为map task的数目。
        2.6 之后会创建一个FSDataOutputStream对象,向jobSubmitFile中写入当前job的所有配置信息。
        2.7 接下来就是真正的提交job的操作,具体代码如下
status = jobSubmitClient.submitJob(
              jobId, submitJobDir.toString(), jobCopy.getCredentials());
由于我们是在本地运行的job,所以会调用LocalJobRunner类中的submitJob,如果是在hadoop集群中运行,则会调用JobTracker的submitJob方法。相对于JobTracker,LocalJobRunner类中的submitJob方法非常简单
    Job job = new Job(jobid, jobSubmitDir);
    job.job.setCredentials(credentials);
    return job.status;
        2.7.1 进入Job的构造函数 ,大部分代码都是比较简单的,先来看下面这几行代码
      this.trackerDistributedCacheManager =
        new TrackerDistributedCacheManager(conf, taskController);
      this.taskDistributedCacheManager =
        trackerDistributedCacheManager.newTaskDistributedCacheManager(
            jobid, conf);
      taskDistributedCacheManager.setupCache(conf, "archive", "archive");
这里先创建了一个TrackerDistributedCacheManager对象,然后该对象又创建了一个TaskDistributedCacheManager对象,DistributedCache的具体工作流程可以参考 这篇bog。之后,会更新(重新写入配置信息)job的xml配置文件。
        2.7.2 之后就是启动一个新线程执行Job。也就是会去执行Job类中的run方法,下面我们来分析一下run方法的执行过程。
        2.7.2.1 run方法中的代码比较多,挑几个重要地方来看吧。首先看下
Map<TaskAttemptID, MapOutputFile> mapOutputFiles =
            Collections.synchronizedMap(new HashMap<TaskAttemptID, MapOutputFile>());
List<MapTaskRunnable> taskRunnables = getMapTaskRunnables(taskSplitMetaInfos,
            jobId, mapOutputFiles);
这段代码会创建一个map task线程的集合,然后放入线程池中执行,map的输出结果文件放在mapOutputFiles中。
        2.7.2.2 接下来再来看这行代码
        TaskAttemptID reduceId = 
          new TaskAttemptID(new TaskID(jobId, false, 0), 0);
从这行代码开始,下面的就是Reduce的执行部分。TaskAttemptID是什么东东呢,先来看TaskAttempt吧, TaskAttempt可以理解为一个task,map task或reduce task, 这里对应的是一个reduce的id。
        2.7.2.3 然后,创建一个reducetask,并对其进行设置
ReduceTask reduce =
                new ReduceTask(systemJobFile.toString(), reduceId, 0, mapIds.size(),
                    1);
reduce.setUser(UserGroupInformation.getCurrentUser().getShortUserName());
JobConf localConf = new JobConf(job);
localConf.set("mapreduce.jobtracker.address", "local"); 
然后会将map的输出文件交给reduce,
          for (int i = 0; i < mapIds.size(); i++) {
              if (!this.isInterrupted()) {
                TaskAttemptID mapId = mapIds.get(i);
                Path mapOut = mapOutputFiles.get(mapId).getOutputFile();
                MapOutputFile localOutputFile = new MapOutputFile();
                localOutputFile.setConf(localConf);
                Path reduceIn =
                  localOutputFile.getInputFileForWrite(mapId.getTaskID(),
                        localFs.getFileStatus(mapOut).getLen());
                if (!localFs.mkdirs(reduceIn.getParent())) {
                  throw new IOException("Mkdirs failed to create "
                      + reduceIn.getParent().toString());
                }
                if (!localFs.rename(mapOut, reduceIn))
                  throw new IOException("Couldn't rename " + mapOut);
              } else {
                throw new InterruptedException();
              }
            }
之后会调用该reduce的run方法运行reducer
 reduce.run(localConf, this);
由于在local模式下,reduce的数量只可以是0或1,所以reduce的运行还是比较简单的、具体可以看run方法内部的代码。 
 
<think>我们正在解决Hadoop2.7.7运行MapReduce示例程序时出现的"找不到或无法加载主类"错误。根据引用[1]和[2],用户尝试运行示例程序(如pi或grep)时遇到问题。这个错误通常是由于Hadoop类路径配置不正确或示例JAR文件路径错误引起的。解决方案步骤:1.**确认JAR文件路径**:首先,确保你使用的JAR文件路径正确。在Hadoop2.7.7中,示例JAR文件通常位于:`$HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jar`使用绝对路径来运行命令,例如:```bashhadoopjar/path/to/hadoop/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jarpi1010```2.**设置HADOOP_CLASSPATH**:如果类路径没有正确设置,Hadoop可能无法找到主类。可以尝试设置`HADOOP_CLASSPATH`环境变量,包含示例JAR文件所在的目录:```bashexportHADOOP_CLASSPATH=$HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jar```3.**检查Hadoop配置文件**:确保`mapred-site.xml`中配置了正确的框架名称(YARN):```xml<configuration><property><name>mapreduce.framework.name</name><value>yarn</value></property></configuration>```4.**重新初始化环境变量**:有时环境变量可能没有正确加载,尝试重新加载环境变量:```bashsource$HADOOP_HOME/etc/hadoop/hadoop-env.sh```5.**检查JAVA_HOME设置**:在`hadoop-env.sh`中确认`JAVA_HOME`设置正确(引用[3][4]):```bashexportJAVA_HOME=/usr/local/src/jdk1.8.0_381#替换为你的实际路径```6.**使用完整的类名**:如果上述方法无效,可以尝试使用完整的类名运行。例如,计算pi的类名为`org.apache.hadoop.examples.Pi`:```bashhadoopjar/path/to/hadoop-mapreduce-examples-2.7.7.jarorg.apache.hadoop.examples.Pi1010```7.**检查Hadoop服务状态**:确保HDFS和YARN服务已经启动:```bashstart-dfs.shstart-yarn.sh```8.**检查JAR文件完整性**:确认JAR文件没有损坏。可以通过列出JAR文件内容来检查:```bashjartf/path/to/hadoop-mapreduce-examples-2.7.7.jar|grepPi.class```应该能看到`org/apache/hadoop/examples/Pi.class`。9.**检查用户权限**:确保当前用户有权限执行Hadoop命令和访问JAR文件。如果以上步骤仍无法解决问题,请检查Hadoop日志(位于`$HADOOP_HOME/logs/`)以获取更详细的错误信息。相关问题:1.Hadoop中如何正确配置类路径?2.如何诊断Hadoop作业提交失败的原因?3.HadoopMapReduce示例程序有哪些常用示例?4.如何修复Hadoop中的类加载问题?</think>### 解决 Hadoop 2.7.7 MapReduce "找不到或无法加载主类" 错误 #### 🔍 问题原因分析 该错误通常由以下原因引起: 1. **JAR文件路径错误**:Hadoop无法定位示例程序JAR文件[^1][^2] 2. **类路径配置缺失**:Hadoop未正确加载MapReduce依赖库 3. **环境变量未生效**:`HADOOP_CLASSPATH`未包含必要路径 4. **JAR文件损坏**:下载或传输过程中文件损坏 #### ✅ 解决方案步骤 ##### 1. 验证JAR文件路径 ```bash # 检查示例JAR是否存在(注意版本号) ls $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jar # 正确执行命令格式(使用绝对路径) hadoop jar /path/to/hadoop/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jar pi 10 10 ``` ##### 2. 配置全局类路径 编辑 `hadoop-env.sh` 添加类路径: ```bash vim $HADOOP_HOME/etc/hadoop/hadoop-env.sh ``` 添加以下配置(根据实际路径调整): ```bash export HADOOP_CLASSPATH=$HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jar export HADOOP_CLASSPATH=$HADOOP_CLASSPATH:$HADOOP_HOME/share/hadoop/mapreduce/*:$HADOOP_HOME/share/hadoop/common/* ``` ##### 3. 修复依赖库路径 在 `mapred-site.xml` 添加库路径配置: ```xml <property> <name>mapreduce.application.classpath</name> <value> $HADOOP_HOME/share/hadoop/mapreduce/*, $HADOOP_HOME/share/hadoop/mapreduce/lib/*, $HADOOP_HOME/share/hadoop/common/*, $HADOOP_HOME/share/hadoop/common/lib/* </value> </property> ``` ##### 4. 验证JAR文件完整性 ```bash # 检查文件MD5(应与官方一致) md5sum $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jar # 重新下载损坏的JAR(从官方镜像) wget https://archive.apache.org/dist/hadoop/common/hadoop-2.7.7/hadoop-2.7.7.tar.gz tar -xzf hadoop-2.7.7.tar.gz --strip-components=2 hadoop-2.7.7/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jar ``` ##### 5. 使用完整类名执行 ```bash hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jar \ org.apache.hadoop.examples.Pi 10 10 ``` ##### 6. 检查服务状态 确保HDFS和YARN已启动: ```bash # 检查NameNode hdfs dfsadmin -report # 检查YARN资源管理器 yarn node -list ``` #### ⚠️ 注意事项 1. **权限问题**:确保执行用户对JAR文件有读取权限 2. **配置同步**:在集群环境下同步所有节点的配置文件 3. **环境变量生效**:修改后执行 `source $HADOOP_HOME/etc/hadoop/hadoop-env.sh` 4. **版本一致性**:确认命令中的JAR版本号与实际完全匹配 > 通过上述步骤可解决99%的类加载错误。若仍失败,检查 `$HADOOP_HOME/logs/userlogs/` 中的详细错误日志。 --- ### 相关问题 1. **如何验证Hadoop集群的MapReduce功能是否正常?** > 探讨MapReduce健康检查的标准流程 2. **Hadoop类路径冲突有哪些典型表现?如何解决?** > 分析类加载冲突的诊断方法与解决策略 3. **Hadoop 2.x与3.x的MapReduce API有何兼容性差异?** > 对比版本间API变化及迁移注意事项 4. **如何为自定义MapReduce作业配置依赖库?** > 解析作业依赖管理的三种实现方式 5. **Hadoop环境变量配置的最佳实践是什么?** > 探讨全局配置与作业级配置的优先级管理 [^1]: Hadoop示例程序执行方法 [^2]: MapReduce作业运行错误分析 [^3]: Hadoop类路径配置规范 [^4]: JAR文件完整性验证流程 [^5]: Hadoop服务状态检查方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值