Hadoop学习(9)----MapReduce阶段的Split阶段

一、介绍

在上一届的信息中我们已经知道MapReduce框架,在处理过程中主要分为四个阶段:Split(分片)阶段、Map阶段、Shuffle(混排、重拍)阶段、Reduce阶段。接下来笔者将会分别详细介绍着四个阶段,也会加上Hadoop2.6.0的源码进行分析。从而加深读者对Split阶段的理解。

我们知道从文件开始读取,经过一些列处理后,文件数据将以<key,value>键值对的方式进入到Map阶段中,这就是Split的主要任务,下面我们详细介绍这一些类操作。

二、Split介绍

想要了解Split的过程,就必须要了解三个类:FileSplit.class、InputFormat.class和LineRecordReader.class

(1)org.apache.hadoop.mapreduce.lib.input.FileSplit


public class FileSplit extends InputSplit implements Writable {
  private Path file;
  private long start;
  private long length;
  private String[] hosts;
  private SplitLocationInfo[] hostInfos;

  public FileSplit() {}
  public FileSplit(Path file, long start, long length, String[] hosts) {
    this.file = file;
    this.start = start;
    this.length = length;
    this.hosts = hosts;
  }
 public FileSplit(Path file, long start, long length, String[] hosts,
     String[] inMemoryHosts) {
   this(file, start, length, hosts);
   hostInfos = new SplitLocationInfo[hosts.length];
   for (int i = 0; i < hosts.length; i++) {
     // because N will be tiny, scanning is probably faster than a HashSet
     boolean inMemory = false;
     for (String inMemoryHost : inMemoryHosts) {
       if (inMemoryHost.equals(hosts[i])) {
         inMemory = true;
         break;
       }
     }
     hostInfos[i] = new SplitLocationInfo(hosts[i], inMemory);
   }
 }
 
  /** The file containing this split's data. */
  public Path getPath() { return file; }
  
  /** The position of the first byte in the file to process. */
  public long getStart() { return start; }
  
  /** The number of bytes in the file to process. */
  @Override
  public long getLength() { return length; }

  @Override
  public String toString() { return file + ":" + start + "+" + length; }

  
  // Writable methods
  
  @Override
  public String[] getLocations() throws IOException {
    if (this.hosts == null) {
      return new String[]{};
    } else {
      return this.hosts;
    }
  }
  
  @Override
  @Evolving
  public SplitLocationInfo[] getLocationInfo() throws IOException {
    return hostInfos;
  }
}

这个类是从Hadoop2.6源代码中复制出来的,并且删除了一部分代码,从该类中我们看出,一个Split分片的内容主要包括Path file、Long start、Long length和String[] hosts。从这四个可以看出Split分片并没有实际的数据,分片其实只是对一个文件进行逻辑上的分片,数据还是按照Block的方式保存在HDFS中,而一个Split分片的主要记录了该分片是从文件的那个位置开始,长度是多少,这些数据的位置在哪里这些信息,在读取分片数据的时候,是根据FileSplit类中的信息去读取相应的Block的数据。这也是为什么分片最好和Block大小相同的原因,如果一个FileSplit的大小大于一个Block的大小,则该分片可能会需要从其他节点的Block读取数据,这样就会造成不必要的网络传输,导致处理时间增长。

(2)org.apache.hadoop.mapreduce.InputFormat

public abstract class InputFormat<K, V> {

  
  public abstract 
    List<InputSplit> getSplits(JobContext context
                               ) throws IOException, InterruptedException;
  
 
  public abstract 
    RecordReader<K,V> createRecordReader(InputSplit split,
                                         TaskAttemptContext context
                                        ) throws IOException, 
                                                 InterruptedException;

 }

我们在实现读取分片的过程就有这两个函数完成,首先我们先介绍getSplit()函数

public InputSplit[] getSplits(JobConf job, int numSplits)
    throws IOException {
    Stopwatch sw = new Stopwatch().start();
    FileStatus[] files = listStatus(job);//获取Job中所有文件是状态信息(filestatus类)
    
    job.setLong(NUM_INPUT_FILES, files.length);   //设置输入文件数量为文件状态的个数
    long totalSize = 0;                           // 计算job中所有文件的总大小
    for (FileStatus file: files) {                // 便利所有的文件,检查是否含有文件
      if (file.isDirectory()) {
        throw new IOException("Not a file: "+ file.getPath());
      }
      totalSize += file.getLen();
    }

    long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);//根据map数量,计算每个map的处理量
    long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
      FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize); //获取分片大小的最大值

    // generate splits
    ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);//根据map的数量创建分片数组
    NetworkTopology clusterMap = new NetworkTopology();
    for (FileStatus file: files) {    //对每一个文件进行便利
      Path path = file.getPath();    //获取当前文件的路径
      long length = file.getLen();   //获取当前文件的大小
      if (length != 0) {  //在文件不为空的情况下
        FileSystem fs = path.getFileSystem(job);   //创建FileSystem示例
        BlockLocation[] blkLocations;     //创建BlockLocation数组,准备记录该文件所有Block的位置
        if (file instanceof LocatedFileStatus) {   //整个判断语句就是用来获取该文件Block的位置信息
          blkLocations = ((LocatedFileStatus) file).getBlockLocations();
        } else {
          blkLocations = fs.getFileBlockLocations(file, 0, length);
        }
        if (isSplitable(fs, path)) {  //isSplitable为true时,表示一个分片中只允许一个文件,false相反
          long blockSize = file.getBlockSize(); //获取文件的Block的大小
          long splitSize = computeSplitSize(goalSize, minSize, blockSize);//计算一个分片的实际大小

          long bytesRemaining = length; //文件剩余大小
          while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) { //
            String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,
                length-bytesRemaining, splitSize, clusterMap); //根据Block位置信息、split大小等信息进行计算split的IP地址
            splits.add(makeSplit(path, length-bytesRemaining, splitSize, //将新产生的Split加入到split的List中
                splitHosts[0], splitHosts[1]));
            bytesRemaining -= splitSize; //减小文件所剩的大小
          }

          if (bytesRemaining != 0) {  //当文件还有剩余量时,经剩余的文件放入到新的分片中
            String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations, length
                - bytesRemaining, bytesRemaining, clusterMap);
            splits.add(makeSplit(path, length - bytesRemaining, bytesRemaining,
                splitHosts[0], splitHosts[1]));
          }
        } else { //isSplitable==false时,则一个文件可以不被分片,将文件放入到一个split中
          String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,0,length,clusterMap);//获取split所在的IP地址
          splits.add(makeSplit(path, 0, length, splitHosts[0], splitHosts[1])); //放入splits的List中
        }
      } else { 
        //Create empty hosts array for zero length files
        splits.add(makeSplit(path, 0, length, new String[0]));
      }
    }
    sw.stop();
    if (LOG.isDebugEnabled()) {
      LOG.debug("Total # of splits generated by getSplits: " + splits.size()
          + ", TimeTaken: " + sw.elapsedMillis());
    }
    return splits.toArray(new FileSplit[splits.size()]);
  }

通过getSplit()函数得到对文件的分片信息后,然后读取分片表示的数据,并生成<key,value>键值对送入到map端,而生成<key,value>键值对则是由createRecordReader()函数完成。

<span style="font-size:18px;"> public RecordReader<LongWritable, Text> 
    createRecordReader(InputSplit split,
                       TaskAttemptContext context) {
    String delimiter = context.getConfiguration().get(
        "textinputformat.record.delimiter"); //读取文件的默认分隔符
    byte[] recordDelimiterBytes = null;
    if (null != delimiter)
      recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);
    return new LineRecordReader(recordDelimiterBytes); //调用LineRecordReader方法,生成<key,vavle>键值对
 }</span>
这样通过InputFormat类中的两个方法,我们就完成了对文件的分片和读取<key,value>值。

(3)org.apache.hadoop.mapreduce.lib.input.LineRecordReader

<span style="font-size:18px;">public class LineRecordReader extends RecordReader<LongWritable, Text> {
  private static final Log LOG = LogFactory.getLog(LineRecordReader.class);
  public static final String MAX_LINE_LENGTH = 
    "mapreduce.input.linerecordreader.line.maxlength";

  private long start;
  private long pos;
  private long end;
  private SplitLineReader in;
  private FSDataInputStream fileIn;
  private Seekable filePosition;
  private int maxLineLength;
  private LongWritable key;
  private Text value;
  private boolean isCompressedInput;
  private Decompressor decompressor;
  private byte[] recordDelimiterBytes;

  public LineRecordReader() {
  }

  public LineRecordReader(byte[] recordDelimiter) {
    this.recordDelimiterBytes = recordDelimiter;
  }

  public void initialize(InputSplit genericSplit,
                         TaskAttemptContext context) throws IOException { //初始化信息
    FileSplit split = (FileSplit) genericSplit;
    Configuration job = context.getConfiguration();
    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);
    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 SplitLineReader(fileIn, job, this.recordDelimiterBytes);
      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;
  }
 
  public boolean nextKeyValue() throws IOException { //将split分片数据生成<key,value>键值对
    if (key == null) {
      key = new LongWritable();
    }
    key.set(pos); //将key进行赋值,赋值为所在文件的位置
    if (value == null) {
      value = new Text();
    }
    int newSize = 0;
    // We always read one extra line, which lies outside the upper
    // split limit i.e. (end - 1)
    while (getFilePosition() <= end || in.needAdditionalRecordAfterSplit()) { 
      if (pos == 0) {
        newSize = skipUtfByteOrderMark(); //生成一行数据的大小
      } else {
        newSize = in.readLine(value, maxLineLength, maxBytesToConsume(pos)); //读取一行一行数据作为value的值
        pos += newSize; //增加位置信息
      }

      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) {
      key = null;
      value = null;
      return false;
    } else {
      return true;
    }
  }
}</span>

当LineRecordReader方法每一次读取一行时,便执行一次nextkeyvalue方法,当成功生成一个<key,value>键值对后,nextkeyvalue方法返回true值,这是新得到的key和value存放在LineRecordReader对象中的key和value属性中,就可以进行读取了。当nextkeyvalue()方法将所有的数据读取结束后,就表示一个split中的所有数据被读取到map中。

三 总结

通过上述三个类以及相应的方法,试下了将数据从Block中读取,并生成<key,value>键值对过程:

getSplit()---->splits----->createRecordReader----->nextKeyValue()-----><key,value>键值对

当我们实现自己定制的Split方法时,通常是重写上面三个类中相应的方法,从而实现新的功能

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单项选择题 1. 下面哪个程序负责 HDFS 数据存储。   a)NameNode   b)Jobtracker   c)Datanode   d)secondaryNameNode   e)tasktracker 2. HDfS 中的 block 默认保存几份?   a)3 份   b)2 份   c)1 份   d)不确定 3. 下列哪个程序通常与 NameNode 在一个节点启动?   a)SecondaryNameNode   b)DataNode   c)TaskTracker   d)Jobtracker 4. Hadoop 作者 大数据面试题-全文共15页,当前为第1页。  a)Martin Fowler 大数据面试题-全文共15页,当前为第1页。   b)Kent Beck   c)Doug cutting 5. HDFS 默认 Block Size   a)32MB   b)64MB   c)128MB 6. 下列哪项通常是集群的最主要瓶颈   a)CPU   b)网络   c)磁盘   d)内存 7. 关于 SecondaryNameNode 哪项是正确的?   a)它是 NameNode 的热备   b)它对内存没有要求   c)它的目的是帮助 NameNode 合并编辑日志,减少 NameNode 启动时间   d)SecondaryNameNode 应与 NameNode 部署到一个节点 多选题 8. 下列哪项可以作为集群的管理工具 大数据面试题-全文共15页,当前为第2页。  a)Puppet 大数据面试题-全文共15页,当前为第2页。   b)Pdsh   c)Cloudera Manager   d)d)Zookeeper 9. 配置机架感知的下面哪项正确   a)如果一个机架出问题,不会影响数据读写   b)写入数据的时候会写到不同机架的 DataNode 中   c)MapReduce 会根据机架获取离自己比较近的网络数据 10. Client 端上传文件的时候下列哪项正确   a)数据经过 NameNode 传递给 DataNode   b)Client 端将文件切分为 Block,依次上传   c)Client 只上传数据到一台 DataNode,然后由 NameNode 负责 Block 复制工作 11. 下列哪个是 Hadoop 运行的模式   a)单机版   b)伪分布式   c)分布式 12. Cloudera 提供哪几种安装 CDH 的方法   a)Cloudera manager   b)Tar ball 大数据面试题-全文共15页,当前为第3页。  c)Yum d)Rpm 大数据面试题-全文共15页,当前为第3页。 判断题 13. Ganglia 不仅可以进行监控,也可以进行告警。( ) 14. Block Size 是不可以修改的。( ) 15. Nagios 不可以监控 Hadoop 集群,因为它不提供 Hadoop 支持。( ) 16. 如果 NameNode 意外终止,SecondaryNameNode 会接替它使集群继续工作。( ) 17. Cloudera CDH 是需要付费使用的。( ) 18. Hadoop 是 Java 开发的,所以 MapReduce 只支持 Java 语言编写。( ) 19. Hadoop 支持数据的随机读写。( ) 20. NameNode 负责管理 metadata,client 端每次读写请求,它都会从磁盘中读取或则会写入 metadata 信息并反馈 client 端。( ) 21. NameNode 本地磁盘保存了 Block 的位置信息。( ) 22. DataNode 通过长连接与 NameNode 保持通信。( ) 23. Hadoop 自身具有严格的权限管理和安全措施保障集群正常运行。( ) 24. Slave 节点要存储数据,所以它的磁盘越大越好。( ) 25. hadoop dfsadmin –report 命令用于检测 HDFS 损坏块。( ) 26. Hadoop 默认调度器策略为 FIFO( ) 大数据面试题-全文共15页,当前为第4页。27. 集群内每个节点都应该配 RAID,这样避免单磁盘损坏,影响整个节点运行。( ) 大数据面试题-全文共15页,当前为第4页。 28. 因为 HDFS 有多个副本,所以 NameNode 是不存在单点问题的。( ) 29. 每个 map 槽就是一个线程。( ) 30. Mapreduce 的 input split 就是一个 block。( ) 31. NameNode 的 Web UI 端口是 50030,它通过 jetty 启动的 Web 服务。( ) 32. Ha
TF-IDF(Term Frequency-Inverse Document Frequency)是一种用于信息检索和文本挖掘的常用技术,它能够评估一个词语在一个文档集合中的重要程度。 在Hadoop MapReduce框架下实现TF-IDF,需要完成以下步骤: 1. 计算每个文档中每个单词出现的次数(Term Frequency,即TF)。 2. 计算每个单词在整个文档集合中出现的文档数(Inverse Document Frequency,即IDF)。 3. 计算每个单词在每个文档中的TF-IDF值。 下面是一个基于Hadoop MapReduce实现TF-IDF的示例: 1. 计算每个文档中每个单词出现的次数 首先,我们需要将文档集合分成若干个小文件,每个小文件包含若干个文档。在Map阶段,我们需要将每个小文件中的每个文档转换成键值对形式,其中键为文档ID,值为文档内容。然后,在Reduce阶段,我们需要对每个文档进行分词,并计算每个单词在该文档中出现的次数。 Map阶段: ```java public class TFMapper extends Mapper<LongWritable, Text, Text, Text> { private Text docID = new Text(); private Text wordCount = new Text(); public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] parts = value.toString().split("\\t"); String docContent = parts[1]; String[] words = docContent.split(" "); Map<String, Integer> wordCounts = new HashMap<String, Integer>(); for (String word : words) { if (wordCounts.containsKey(word)) { wordCounts.put(word, wordCounts.get(word) + 1); } else { wordCounts.put(word, 1); } } for (String word : wordCounts.keySet()) { docID.set(parts[0]); wordCount.set(word + ":" + wordCounts.get(word)); context.write(docID, wordCount); } } } ``` Reduce阶段: ```java public class TFReducer extends Reducer<Text, Text, Text, Text> { private Text wordCount = new Text(); public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { Map<String, Integer> wordCounts = new HashMap<String, Integer>(); for (Text value : values) { String[] parts = value.toString().split(":"); String word = parts[0]; int count = Integer.parseInt(parts[1]); if (wordCounts.containsKey(word)) { wordCounts.put(word, wordCounts.get(word) + count); } else { wordCounts.put(word, count); } } StringBuilder sb = new StringBuilder(); for (String word : wordCounts.keySet()) { sb.append(word + ":" + wordCounts.get(word) + " "); } wordCount.set(sb.toString()); context.write(key, wordCount); } } ``` 2. 计算每个单词在整个文档集合中出现的文档数 在Map阶段,我们需要将每个文档中的单词转换成键值对形式,其中键为单词,值为文档ID。然后,在Reduce阶段,我们需要对每个单词进行统计,得到每个单词在多少个文档中出现过。 Map阶段: ```java public class IDFMapper extends Mapper<LongWritable, Text, Text, Text> { private Text word = new Text(); private Text docID = new Text(); public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] parts = value.toString().split("\\t"); String[] words = parts[1].split(" "); for (String w : words) { word.set(w); docID.set(parts[0]); context.write(word, docID); } } } ``` Reduce阶段: ```java public class IDFReducer extends Reducer<Text, Text, Text, DoubleWritable> { private DoubleWritable idf = new DoubleWritable(); public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { Set<String> docs = new HashSet<String>(); for (Text value : values) { docs.add(value.toString()); } double df = docs.size(); double N = context.getConfiguration().getLong("totalDocs", 1L); double idfValue = Math.log(N / df); idf.set(idfValue); context.write(key, idf); } } ``` 3. 计算每个单词在每个文档中的TF-IDF值 在Map阶段,我们需要将每个文档中的单词转换成键值对形式,其中键为文档ID和单词,值为单词在该文档中出现的次数和该单词的IDF值。然后,在Reduce阶段,我们需要对每个文档中的所有单词进行统计,得到每个单词在该文档中的TF-IDF值。 Map阶段: ```java public class TFIDFMapper extends Mapper<LongWritable, Text, Text, Text> { private Text docID = new Text(); private Text wordCountIDF = new Text(); public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] parts = value.toString().split("\\t"); String[] wordCounts = parts[1].split(" "); for (String wc : wordCounts) { String[] subParts = wc.split(":"); String word = subParts[0]; int count = Integer.parseInt(subParts[1]); double idf = Double.parseDouble(subParts[2]); docID.set(parts[0] + ":" + word); wordCountIDF.set(count + ":" + idf); context.write(docID, wordCountIDF); } } } ``` Reduce阶段: ```java public class TFIDFReducer extends Reducer<Text, Text, Text, DoubleWritable> { private DoubleWritable tfidf = new DoubleWritable(); public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { int count = 0; double idf = 0.0; for (Text value : values) { String[] parts = value.toString().split(":"); count += Integer.parseInt(parts[0]); idf = Double.parseDouble(parts[1]); } tfidf.set(count * idf); context.write(key, tfidf); } } ``` 最后,在Driver中将上述三个阶段串联起来,即可完成TF-IDF的计算。 ```java public class TFIDFDriver { public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); Job job1 = Job.getInstance(conf, "TF"); job1.setJarByClass(TFIDFDriver.class); job1.setInputFormatClass(TextInputFormat.class); job1.setOutputFormatClass(TextOutputFormat.class); job1.setMapperClass(TFMapper.class); job1.setCombinerClass(TFReducer.class); job1.setReducerClass(TFReducer.class); job1.setOutputKeyClass(Text.class); job1.setOutputValueClass(Text.class); FileInputFormat.addInputPath(job1, new Path(args[0])); FileOutputFormat.setOutputPath(job1, new Path(args[1])); job1.waitForCompletion(true); Job job2 = Job.getInstance(conf, "IDF"); job2.setJarByClass(TFIDFDriver.class); job2.setInputFormatClass(TextInputFormat.class); job2.setOutputFormatClass(TextOutputFormat.class); job2.setMapperClass(IDFMapper.class); job2.setReducerClass(IDFReducer.class); job2.setOutputKeyClass(Text.class); job2.setOutputValueClass(DoubleWritable.class); FileInputFormat.addInputPath(job2, new Path(args[1])); FileOutputFormat.setOutputPath(job2, new Path(args[2])); job2.getConfiguration().setLong("totalDocs", job2.getCounters().findCounter("org.apache.hadoop.mapred.Task$Counter", "MAP_INPUT_RECORDS").getValue()); job2.waitForCompletion(true); Job job3 = Job.getInstance(conf, "TF-IDF"); job3.setJarByClass(TFIDFDriver.class); job3.setInputFormatClass(TextInputFormat.class); job3.setOutputFormatClass(TextOutputFormat.class); job3.setMapperClass(TFIDFMapper.class); job3.setReducerClass(TFIDFReducer.class); job3.setOutputKeyClass(Text.class); job3.setOutputValueClass(DoubleWritable.class); FileInputFormat.addInputPath(job3, new Path(args[1])); FileOutputFormat.setOutputPath(job3, new Path(args[3])); job3.waitForCompletion(true); } } ``` 以上就是基于Hadoop MapReduce实现TF-IDF的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值