hadoop的源码分析--Map的input阶段

hadoop的体系十分庞大,这里对一些主要功能的细节做分析。

map阶段

input阶段

主要涉及的难点有 :

  1. 切片的划分
  2. 切片数据的读取(多余数据的剔除,和有效数据的拉取)
//MyWordCount.java
        job.waitForCompletion(true);

该方法会跳转到Job.class类中,

...
  public boolean waitForCompletion(boolean verbose
                                   ) throws IOException, InterruptedException,
                                            ClassNotFoundException {
    if (state == JobState.DEFINE) {
    //如果该Job 已经定义,则提交Job
      submit();// 从这里开始跳转到下一个方法
    }
    if (verbose) {
    // 如果开启 开启监视器
      monitorAndPrintJob();
    } else {
      // get the completion poll interval from the client.
      int completionPollIntervalMillis = 
        Job.getCompletionPollInterval(cluster.getConf());
      while (!isComplete()) {
        try {
          Thread.sleep(completionPollIntervalMillis);
        } catch (InterruptedException ie) {
        }
      }
    }//返回job执行成功!
    return isSuccessful();
  }
  ...

上面的submit()方法属于 Job.class 类

...
  public void submit() 
         throws IOException, InterruptedException, ClassNotFoundException {
    ensureState(JobState.DEFINE);
    setUseNewAPI();//使用新的API 
    connect();//做一些链接
    // 重点步骤 !!!定义一个submitter 对象 用于提交任务
    final JobSubmitter submitter = 
        getJobSubmitter(cluster.getFileSystem(), cluster.getClient());
    status = ugi.doAs(new PrivilegedExceptionAction<JobStatus>() {
      public JobStatus run() throws IOException, InterruptedException, 
      ClassNotFoundException {
      //getJobSubmitter 只是一个简单的构造方法,这里使用submitJobInternal()方法 提交一个任务。,下点submitJobInternal 方法。
        return submitter.submitJobInternal(Job.this, cluster);
      }
    });
    state = JobState.RUNNING;
    LOG.info("The url to track the job: " + getTrackingURL());
   }
 ...

下面查看 submitJobInternal()方法,此方法定义在submitJobInternal.class类

JobStatus submitJobInternal(Job job, Cluster cluster) 
  throws ClassNotFoundException, InterruptedException, IOException {

    //validate the jobs output specs 
    checkSpecs(job);

    Configuration conf = job.getConfiguration();
    //拿到配置信息,并将其添加到框架。
    addMRFrameworkToDistributedCache(conf);

    Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
    //configure the command line options correctly on the submitting dfs
    InetAddress ip = InetAddress.getLocalHost();
    if (ip != null) {
      submitHostAddress = ip.getHostAddress();
      submitHostName = ip.getHostName();
      conf.set(MRJobConfig.JOB_SUBMITHOST,submitHostName);
      conf.set(MRJobConfig.JOB_SUBMITHOSTADDR,submitHostAddress);
    }
    JobID jobId = submitClient.getNewJobID();
    job.setJobID(jobId);
    Path submitJobDir = new Path(jobStagingArea, jobId.toString());
    JobStatus status = null;
    try {
      conf.set(MRJobConfig.USER_NAME,
          UserGroupInformation.getCurrentUser().getShortUserName());
      conf.set("hadoop.http.filter.initializers", 
          "org.apache.hadoop.yarn.server.webproxy.amfilter.AmFilterInitializer");
      conf.set(MRJobConfig.MAPREDUCE_JOB_DIR, submitJobDir.toString());
      LOG.debug("Configuring job " + jobId + " with " + submitJobDir 
          + " as the submit dir");
      // get delegation token for the dir
      TokenCache.obtainTokensForNamenodes(job.getCredentials(),
          new Path[] { submitJobDir }, conf);
      
      populateTokenCache(conf, job.getCredentials());

      // generate a secret to authenticate shuffle transfers
      if (TokenCache.getShuffleSecretKey(job.getCredentials()) == null) {
        KeyGenerator keyGen;
        try {
          keyGen = KeyGenerator.getInstance(SHUFFLE_KEYGEN_ALGORITHM);
          keyGen.init(SHUFFLE_KEY_LENGTH);
        } catch (NoSuchAlgorithmException e) {
          throw new IOException("Error generating shuffle secret key", e);
        }
        SecretKey shuffleKey = keyGen.generateKey();
        TokenCache.setShuffleSecretKey(shuffleKey.getEncoded(),
            job.getCredentials());
      }
      if (CryptoUtils.isEncryptedSpillEnabled(conf)) {
        conf.setInt(MRJobConfig.MR_AM_MAX_ATTEMPTS, 1);
        LOG.warn("Max job attempts set to 1 since encrypted intermediate" +
                "data spill is enabled");
      }

      copyAndConfigureFiles(job, submitJobDir);

      Path submitJobFile = JobSubmissionFiles.getJobConfPath(submitJobDir);
      // 以上内容都是一些配置 信息 ,下面开始 重点的切片操作怕
      /*
      *
      *                           下面是数据的切片操作
      * 
      */
      // Create the splits for the job
      LOG.debug("Creating splits at " + jtFs.makeQualified(submitJobDir));
// 主要是查看 writeSplits()这个函数的处理内容.
      int maps = writeSplits(job, submitJobDir);
      conf.setInt(MRJobConfig.NUM_MAPS, maps);
      LOG.info("number of splits:" + maps);

      // write "queue admins of the queue to which job is being submitted"
      // to job file.
      String queue = conf.get(MRJobConfig.QUEUE_NAME,
          JobConf.DEFAULT_QUEUE_NAME);
      AccessControlList acl = submitClient.getQueueAdmins(queue);
      conf.set(toFullPropertyName(queue,
          QueueACL.ADMINISTER_JOBS.getAclName()), acl.getAclString());

      // removing jobtoken referrals before copying the jobconf to HDFS
      // as the tasks don't need this setting, actually they may break
      // because of it if present as the referral will point to a
      // different job.
      TokenCache.cleanUpTokenReferral(conf);

      if (conf.getBoolean(
          MRJobConfig.JOB_TOKEN_TRACKING_IDS_ENABLED,
          MRJobConfig.DEFAULT_JOB_TOKEN_TRACKING_IDS_ENABLED)) {
        // Add HDFS tracking ids
        ArrayList<String> trackingIds = new ArrayList<String>();
        for (Token<? extends TokenIdentifier> t :
            job.getCredentials().getAllTokens()) {
          trackingIds.add(t.decodeIdentifier().getTrackingId());
        }
        conf.setStrings(MRJobConfig.JOB_TOKEN_TRACKING_IDS,
            trackingIds.toArray(new String[trackingIds.size()]));
      }

      // Set reservation info if it exists
      ReservationId reservationId = job.getReservationId();
      if (reservationId != null) {
        conf.set(MRJobConfig.RESERVATION_ID, reservationId.toString());
      }

      // Write job file to submit dir
      writeConf(conf, submitJobFile);
      
      //
      // Now, actually submit the job (using the submit name)
      //
      printTokens(jobId, job.getCredentials());
      status = submitClient.submitJob(
          jobId, submitJobDir.toString(), job.getCredentials());
      if (status != null) {
        return status;
      } else {
        throw new IOException("Could not launch job");
      }
    } finally {
      if (status == null) {
        LOG.info("Cleaning up the staging area " + submitJobDir);
        if (jtFs != null && submitJobDir != null)
          jtFs.delete(submitJobDir, true);

      }
    }
  }
  

查看 writeSplits()这个函数的处理内容. 在JobSubmitter.class类中.

//这个类是一个套娃操作,就是判断下使用新旧API 并进行了一个重定向.主要查看writeNewSplits()方法
  private int writeSplits(org.apache.hadoop.mapreduce.JobContext job,
      Path jobSubmitDir) throws IOException,
      InterruptedException, ClassNotFoundException {
    JobConf jConf = (JobConf)job.getConfiguration();
    int maps;
    if (jConf.getUseNewMapper()) {
      maps = writeNewSplits(job, jobSubmitDir);
    } else {
      maps = writeOldSplits(jConf, jobSubmitDir);
    }
    return maps;
  }

这个类是一个套娃操作,就是判断下使用新旧API 并进行了一个重定向.主要查看writeNewSplits()方法.

private <T extends InputSplit>
  int writeNewSplits(JobContext job, Path jobSubmitDir) throws IOException,
      InterruptedException, ClassNotFoundException {
      //同样的,拿到一个配置文件 
    Configuration conf = job.getConfiguration();
    //然后使用反射工具,将配置文件反射成类,得到input对象  
    
    InputFormat<?, ?> input =
      ReflectionUtils.newInstance(job.getInputFormatClass(), conf);
    /*
    *
    *    下面单说一下,job.getXXXXX()方法 这是一个在 JobContext类中定义的一个抽象类 ,其具体内容如下
    *   public Class<? extends InputFormat<?,?>> getInputFormatClass() throws ClassNotFoundException;
    *        //Get the {@link Mapper} class for the job.
    *       //@return the {@link Mapper} class for the job.
    *   这个方法在 JobContextImpl类中实现 ,内容如下 
    *   public Class<? extends InputFormat<?,?>> getInputFormatClass()   throws ClassNotFoundException {
    *   //都是相同的操作,首先看用户是否配置了,如果没有配置就使用默认的 TextInputFormat.class
    *   return (Class<? extends InputFormat<?,?>>) conf.getClass(INPUT_FORMAT_CLASS_ATTR, TextInputFormat.class);}
    *  
    *    job.getxx() 类 都是 在jobcontext中抽象,在jobcontextimpl中实现.
    *
    */
    //下面进入核心方法 也就是 input 的getSplits(job)方法
    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;
  }

这个切片方法 getSplits() 在抽象在InputFormat.class中 ,内容如下

 public abstract 
    List<InputSplit> getSplits(JobContext context
                               ) throws IOException, InterruptedException;

但是它的实例化对象在 FileInputFormat.class 类中 .


 public List<InputSplit> getSplits(JobContext job) throws IOException {
    StopWatch sw = new StopWatch().start();//此处是一个计时器对象,用于记录程序执行了多久
    

    long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
    
    long maxSize = getMaxSplitSize(job);
    // 设置最大切片长度和最小切片长度 
    /*
    *   protected long getFormatMinSplitSize() { return 1;}
    * 
    *   getMinSplitSize(job)是获用户设置的切片大小 ,默认为1 
    * 
    *   public static long getMaxSplitSize(JobContext context) {
    *       默认的最大值 是long 类型吧的最大值
    *     return context.getConfiguration().getLong(SPLIT_MAXSIZE, Long.MAX_VALUE); }
    *
    */
    // generate splits 下面开始生成切片 .
    List<InputSplit> splits = new ArrayList<InputSplit>();//创建一个inputsplit类型的array
    List<FileStatus> files = listStatus(job);//listStatus 是获得job 的输入目录下的一级文件的清单
    for (FileStatus file: files) {//下面开始对每一个文件进行操作.
    //获得文件的地址和长度 
      Path path = file.getPath();
      long length = file.getLen();
      if (length != 0) {//如果文件大小不为0 ,对文件进行操作
        BlockLocation[] blkLocations;
        
        if (file instanceof LocatedFileStatus) { //使用instanceof 判断一个类是不是另一个类的实现类
          blkLocations = ((LocatedFileStatus) file).getBlockLocations();//如果是本地文件 则blocklocation 的数组 使用本地类的方式获得 
        } else {// 否则使用分布式文件系统的方式获得 .分区信息
          FileSystem fs = path.getFileSystem(job.getConfiguration());//使用job 的配置文件获得FileSystem对象 
          blkLocations = fs.getFileBlockLocations(file, 0, length);//获得该文件的分区信息   
        }
        if (isSplitable(job, path)) {//判断下该文件是否可以分片, 如果可分执行如下代码   .
          long blockSize = file.getBlockSize();//得到文件的block 块的大小  ,可能是默认的128M 也可能使用户自己设置的 
 		  long splitSize = computeSplitSize(blockSize, minSize, maxSize);
          // 知识点又来了 ,如何计算分片的大小呢 ,下面我们看一下 
          /*
          *  protected long computeSplitSize(long blockSize, long minSize,long maxSize) {
		  *		    return Math.max(minSize, Math.min(maxSize, blockSize));
  			}
          *     默认情况下 minsize 是1  maxsize 是一个超级大的数9223372036854775807 ,假设这里的blocksize 是128M 
          *     那么 min(maxsize,blocksize) 就是 blocksize  并且 max(minsize ,blocksize) 就是 blocksize  
          *     这就是所说的默认情况下 ,切片大小等于块大小 .
          * 	
 		  *	那么如何调整切片的大小呢-----(调大splitsize ,改大minsize)  (调小splitsize ,改小 maxsize)
          *
          */
         
          long bytesRemaining = length;//定义字节剩余长度变量,用于记录当前还有多少字节没有被分片.
          
          while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
          // 循环的退出条件是 bytesRemaining /splitSize > 1.1  注意这里SPLIT_SLOP 是静态值 1.1 
          // 也就是说 当剩余的字节不足一个分片时 退出循环 
          
            int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
            /*
            *
            *
            *叮咚 知识点又来了 ,如何找到blocklocation的索引呢 ? 还是去看看get函中怎么定义的吧
            *
            *  protected int getBlockIndex(BlockLocation[] blkLocations,  long offset) {
			*	   for (int i = 0 ; i < blkLocations.length; i++) {//遍历整个bolck块
      		*			// is the offset inside this block?
      		*	        // 如果offset 正好在一个分区内 ,则返回这个分区的索引,getoffset()得到一个块的起始位置,而 getoffset+length 整好是一个块的结束位置. 
   			*	 		if ((blkLocations[i].getOffset() <= offset) &&  
   			*	  		 (offset < blkLocations[i].getOffset() + blkLocations[i].getLength())){
     		*			   return i;
      		*			}
  			*	  }
  			*	// 如果for 循环结束,表示offset 已经越界了 ,下面我们求一下 最后一个块的结束位置.
    		*    BlockLocation last = blkLocations[blkLocations.length -1];
   			*	 long fileLength = last.getOffset() + last.getLength() -1;//得到最后一个块的位置.并抛出数组越界异常.
   			*	 throw new IllegalArgumentException("Offset " + offset +  " is outside of file (0.." +
            *                          fileLength + ")");
            * }
            *
            *
            *
            */
  			// makeSplit( 文件的路径,文件的开始位置,和结束位置,block块所在的主机,block块已经缓存的主机)
            splits.add(makeSplit(path, length-bytesRemaining, splitSize,
                        blkLocations[blkIndex].getHosts(),
                        blkLocations[blkIndex].getCachedHosts()));
            bytesRemaining -= splitSize;//将剩余文件,减去已经分片的字节数据
          }

          if (bytesRemaining != 0) {//判断 剩余自己是否为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()));
        }
        
      } else { //length的长度为零说明 这是一个空文件 ,则添加一条空字符记录到切片
        //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();//计时器结束 在LOG中打印执行的时间
    if (LOG.isDebugEnabled()) {
      LOG.debug("Total # of splits generated by getSplits: " + splits.size()
          + ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
    }
    return splits;
  }

到这里分片就结束了,返回到writeNewSplits()方法,将spilts 转化为array 并返回array的length .
也就是说返回了分片的个数.也就是map 的数量 代码如下

 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;

返回到submitJobInternal方法 将map的数量加入配置,启动相应的MapTask

      int maps = writeSplits(job, submitJobDir);
      conf.setInt(MRJobConfig.NUM_MAPS, maps);
      LOG.info("number of splits:" + maps);

其调用关系图,如下图所示:
[tupian.jpg]

MapTask 类解析

打开框架先找run()方法,找了run 在看try()。

 private <INKEY, INVALUE, OUTKEY, OUTVALUE> void runNewMapper(JobConf job, TaskSplitIndex splitIndex, TaskUmbilicalProtocol umbilical, TaskReporter reporter) throws IOException, ClassNotFoundException, InterruptedException {
        TaskAttemptContext taskContext = new TaskAttemptContextImpl(job, this.getTaskID(), reporter);
        Mapper<INKEY, INVALUE, OUTKEY, OUTVALUE> mapper = (Mapper)ReflectionUtils.newInstance(taskContext.getMapperClass(), job);
        //拿到mapper类 ,如果用户定义了,使用用户定义的,否则使用默认的Mapper类.
        InputFormat<INKEY, INVALUE> inputFormat = (InputFormat)ReflectionUtils.newInstance(taskContext.getInputFormatClass(), job);
        
        org.apache.hadoop.mapreduce.InputSplit split = null;
        split = (org.apache.hadoop.mapreduce.InputSplit)this.getSplitDetails(new Path(splitIndex.getSplitLocation()), splitIndex.getStartOffset());
        LOG.info("Processing split: " + split);
        org.apache.hadoop.mapreduce.RecordReader<INKEY, INVALUE> input = new MapTask.NewTrackingRecordReader(split, inputFormat, reporter, taskContext);
        job.setBoolean("mapreduce.job.skiprecords", this.isSkipping());
        RecordWriter output = null;
        if (job.getNumReduceTasks() == 0) {
            output = new MapTask.NewDirectOutputCollector(taskContext, job, umbilical, reporter);
        } else {
            output = new MapTask.NewOutputCollector(taskContext, job, umbilical, reporter);
        }

        MapContext<INKEY, INVALUE, OUTKEY, OUTVALUE> mapContext = new MapContextImpl(job, this.getTaskID(), input, (RecordWriter)output, this.committer, reporter, split);
        org.apache.hadoop.mapreduce.Mapper.Context mapperContext = (new WrappedMapper()).getMapContext(mapContext);
/*
*   重头戏来了 
*
*
*/
        try {
      
            input.initialize(split, mapperContext);
            //根据initialize()可知这是一个初始化  

/*
*定义在RecodeReader.class 中的一个抽象方法。
*  public abstract void initialize(InputSplit split TaskAttemptContext context) throws IOException, InterruptedException;
* 在LineRecodeReader.class类中实现 。
*/
            mapper.run(mapperContext);
            //这是 Mapper 类中的run方法,我们在下面的代码分析中,进行了Mapper类的分析.
            this.mapPhase.complete();//将map阶段设置未完成 .
            this.setPhase(Phase.SORT);//设置排序阶段开始
            this.statusUpdate(umbilical);//更新排序阶段
            input.close();
            input = null;
            ((RecordWriter)output).close(mapperContext);
            output = null;
        } finally {
            this.closeQuietly((org.apache.hadoop.mapreduce.RecordReader)input);
            this.closeQuietly((RecordWriter)output, mapperContext);
        }

    }

initialize()方法,在LineRecordReader.class 类中实现

public void initialize(InputSplit genericSplit,
                         TaskAttemptContext context) throws IOException {
    FileSplit split = (FileSplit) genericSplit;//拿到一个切片文件对象
    Configuration job = context.getConfiguration();// 通过context 获得配置文件。
    this.maxLineLength = job.getInt(MAX_LINE_LENGTH, Integer.MAX_VALUE);
    // 获得最大的行长度,用于判断一个文件是不是一行。★ 
    start = split.getStart();
    end = start + split.getLength();
   // 获得切片的开始位置和结束位置。
   
    final Path file = split.getPath();//拿到切片的路径 
    // open the file and seek to the start of the split
    final FileSystem fs = file.getFileSystem(job);
    fileIn = fs.open(file);//并在分布式文件系统中,拿到这个文件的内容。
    
    CompressionCodec codec = new CompressionCodecFactory(job).getCodec(file);
    //当在读取一个压缩文件的时候,可能并不知道压缩文件用的是哪种压缩算法,那么无法完成解压任务。在Hadoop中,CompressionCodecFactory通过使用其getCodec()方法,可以通过文件扩展名映射到一个与其对应的CompressionCodec类
    if (null!=codec) {//返回的类不为空 ,则表明读取的是一个压缩文件
      isCompressedInput = true;	//是一个压缩输入流
      decompressor = CodecPool.getDecompressor(codec);//拿到一个解压器文件 
      if (codec instanceof SplittableCompressionCodec) {// 如果得到的是一个可分片的压缩文件 ,执行以下操作 
      /*以下是读取压缩文件流 的方法 现在先不做具体讨论  */
        final SplitCompressionInputStream cIn =
          ((SplittableCompressionCodec)codec).createInputStream(
            fileIn, decompressor, start, end,
            SplittableCompressionCodec.READ_MODE.BYBLOCK);
        in = new CompressedSplitLineReader(cIn, job,
            this.recordDelimiterBytes);
        start = cIn.getAdjustedStart();
        end = cIn.getAdjustedEnd();
        filePosition = cIn;

      } else { // 如果是一个不可分片的压缩数据文件 ,
        in = new SplitLineReader(codec.createInputStream(fileIn,
            decompressor), job, this.recordDelimiterBytes);
        filePosition = fileIn;
      }
      
    } else {//如果不是一个压缩文件 ,直接进行寻址读取。
      fileIn.seek(start);//将文件指针 移动到数据开始位置
      //创建一个未压缩的切片,行读取器 
      in = new UncompressedSplitLineReader(
          fileIn, job, this.recordDelimiterBytes, split.getLength());
      filePosition = fileIn;
    }
    
    // If this is not the first split, we always throw away first record
    // because we always (except the last split) read one extra line in
    // next() method.
    if (start != 0) {
    // 如果不是首条记录,则把开头第一行数据丢弃 ,因为他们是上一行的剩余。
      start += in.readLine(new Text(), 0, maxBytesToConsume(start));
    }
    this.pos = start;//将当前位置的定义到开始位置。
  }

Mapper.class 类的分析
打开类先看run()方法

public void run(Context context) throws IOException, InterruptedException {
    setup(context);//这是一个初始化的函数
    try {
      while (context.nextKeyValue()) {
     // 我们只要关心map函数就可以了
        map(context.getCurrentKey(), context.getCurrentValue(), context);
      }
    } finally {
      cleanup(context);//这是一个结束清理的函数
    }
  }

........
// mappr中的原始函数 
  protected void map(KEYIN key, VALUEIN value, 
                     Context context) throws IOException, InterruptedException {
    context.write((KEYOUT) key, (VALUEOUT) value);
  }
  // my mapper 中重写的map方法
//    创建一个intWritable 对象,并将其初始化为1 ,用于记录单词出现的次数
   private final static IntWritable one = new IntWritable(1);
//   创建一个Text 对象用于记录单词的内容 ,所以我们Map输出的键值对是 <Text ,IntWritable >
   private Text word = new Text();
//   实现父类的方法
   public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
//       使用 StringTokenizer 进行分词操作,返回一个迭代器对象
       StringTokenizer itr = new StringTokenizer(value.toString());
     while (itr.hasMoreTokens()) {// 当迭代器不为空时,继续循环
       word.set(itr.nextToken());//将单词放入到word 实际上是放入一个数组中。
       context.write(word, one);

map中的两个参数分别时通过context.getCurrentKey()context.getCurrentValue() 得到的,但是这个两个方法主要受 context.nextKeyValue()方法控制,我们依次查看下这三个方法。

context.nextKeyValue() 方法

抽象在TaskInputOutputContext.class 中 ,实现在

//TaskInputOutputContext.class
  public boolean nextKeyValue() throws IOException, InterruptedException; 
// MapContexti=Impl.class
  public boolean nextKeyValue() throws IOException, InterruptedException {
    return reader.nextKeyValue();
  }

reader.nextKeyValue()方法抽象在 RecordReader中 实现在LineRecordReader.class


public boolean nextKeyValue() throws IOException {
    if (key == null) {//首次调用 key==null 创建一个长整形数 用来存放key的值 
      key = new LongWritable();
    }
    key.set(pos);
    /* 
    *   设置key的值,其中pos 在initialize()方法中已经初始化为  
    *    this.pos = start ;
    *    首次 start = 0 ;
    * 
    */
    if (value == null) { // 首次 value == null 所以会创建一个空的文本对象 
      value = new Text();
    }
    int newSize = 0;//我们总是多向后读一行 从而保证数据的完整性。现在还不知道new size 的意思 
    // We always read one extra line, which lies outside the upper
    // split limit i.e. (end - 1)
    
    while (getFilePosition() <= end || in.needAdditionalRecordAfterSplit()) {
    /*
    *    getFilePosition() 拿到pos 的当前位置 ,对压缩文件 和 非压缩文件有两种不同的策略
    *     
    *    needAdditionalRecordAfterSplit() 默认值返回false ,判断是不是有多余的数据要读取
    * 
    *
    */
      if (pos == 0) {// 如果是首次调用 则 pos == 0 ,这时候newsize = 跳过utf前置符号的位置
      // utf-8编码的TXT文件在第一行会自动添加一个编码标识符   
        newSize = skipUtfByteOrderMark();//跳过 Utf 字节顺序标记 ,在附录的时候做具体分析    
      } else {// 不是首次调用 就没有多余字符的风险了 ,读取一行 ,并返回 行的长度 。
        newSize = in.readLine(value, maxLineLength, maxBytesToConsume(pos));//maxBytesToConsume()也颇为复杂 ,附录中解释
        pos += newSize;
/* 
*  LineRecorder.class 
*  public int readLine(Text str, int maxLineLength, int maxBytesToConsume) throws IOException {
*     if (this.recordDelimiterBytes != null) {
*       return readCustomLine(str, maxLineLength, maxBytesToConsume);
*     } else {
*       return readDefaultLine(str, maxLineLength, maxBytesToConsume);
*     }
*   }
*
*/
      }

      if ((newSize == 0) || (newSize < maxLineLength)) {// 如果读到了空文件 或者 已经读完了所有文件,直接跳出循环
        break;
      }

      // line too long. try again
      LOG.info("Skipped line of size " + newSize + " at pos " + 
               (pos - newSize));
    }
    if (newSize == 0) {// 如果 newsize == 0 ,表示读取的是空文件 读取失败 重置 key value ,否则 返回true  此时这个切片的数据存储在 value 中
      key = null;
      value = null;
      return false;
    } else {
/*
*    key是数据的起始地址 
*    value 存放的是数据的内容 ,在这里将这两个数据设置好,然后 通过两个get方法读取。
*
*/
      return true;
    }
  }

在map()处理完的数据会调用 context.write(key,values) 函数将数据写入到缓存 。
点开该方法会发现,他是一个接口;

// TaskInputOutputContext.class 
  public void write(KEYOUT key, VALUEOUT value) 
      throws IOException, InterruptedException;

我们找一下他的子类实现,在这里他又调用了output对象的write方法

// TaskInputOutputContextimpl.class 
  public void write(KEYOUT key, VALUEOUT value
                    ) throws IOException, InterruptedException {
    output.write(key, value);
  }

我们继续打开看一下,又是一个抽象方法,下面继续点开

//Recordwriter.class
  public abstract void write(K key, V value
                             ) throws IOException, InterruptedException;
                          
  // 在MapTask类中,是NewOutputCollector 类中的方法。也就是将数据选择一个分区 partions 进行存入,下面开始看看output 功能。
    @Override
    public void write(K key, V value) throws IOException, InterruptedException {
      collector.collect(key, value,
                        partitioner.getPartition(key, value, partitions));
    }

在这里插入图片描述

附录

关于needAdditionalRecordAfterSplit() 方法的解析

//看名字就知道 ,填充缓冲区  
  protected int fillBuffer(InputStream in, byte[] buffer, boolean inDelimiter)throws IOException {
    int maxBytesToRead = buffer.length;//获得完整缓冲区长度。
    if (totalBytesRead < splitLength) {
      long leftBytesForSplit = splitLength - totalBytesRead;
      // check if leftBytesForSplit exceed Integer.MAX_VALUE
      if (leftBytesForSplit <= Integer.MAX_VALUE) {
        maxBytesToRead = Math.min(maxBytesToRead, (int)leftBytesForSplit);
      }
    }
    int bytesRead = in.read(buffer, 0, maxBytesToRead);
    // If the split ended in the middle of a record delimiter then we need
    // to read one additional record, as the consumer of the next split will
    // not recognize the partial delimiter as a record.
    // However if using the default delimiter and the next character is a
    // linefeed then next split will treat it as a delimiter all by itself
    // and the additional record read should not be performed.
    if (totalBytesRead == splitLength && inDelimiter && bytesRead > 0) {
      if (usingCRLF) {//判断是否使用默认的换行符分割 ; usingCRLF 表示 使用换行符 分割 。
        needAdditionalRecord = (buffer[0] != '\n');
      } else {
        needAdditionalRecord = true;
      }
    }
    if (bytesRead > 0) {
      totalBytesRead += bytesRead;
    }
    return bytesRead;
  }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值