map函数处理的<k1,v1>从HDFS文件中获取流程源码解析

本文深入解析MapReduce任务执行过程中,如何通过FileInputFormat与TextInputFormat实现外部文件的切分与转换为<k1,v1>键值对形式。详细探讨了InputSplit与RecordReader的作用,以及它们在数据输入处理阶段的重要性。分析了hdfsblock与InputSplit的关系,解释了设计一个hdfsblock对应一个InputSplit的原因,以实现数据本地化的高效计算。

 

 

0 引子:

 

mapreduce在执行任务的时候,是如何将外部文件进行切分,并将这些文件转换成<k1,v1>键值对方式的

(还记得 map-reduce基本概念和wordcount解析 文章中提到的<k1,v1>概念吗?)

 

一些总结性的话:

a) recordreader + inputsplit是数据输入处理阶段非常重要的两个概念。

b) inputsplit: 对原始输入数据的封装,封装原始数据源,这个数据源可以是hdfs文件系统,也可是DB,或者数据流

c) recordreader: 把inputsplit的数据转换成<k1,v1>

d) inputsplit和hdfs的block之间的关系是什么?? 
 在hdfs看来,mapreduce就是hdfs的客户端,
mapreduce默认inputsplit大小和hdfs的一个block相等,
但是并不说
mapreduce的一个inputsplit就对应hdfs的一个block,
mapreduce是看不到hdfs体内的一个个的block的,  因此默认情况下,被处理目标文件有多少block,就会产生

多少inputsplit,也就会对应多少个map任务,即block和inputsplit只有这种对照关系。

 

为什么设计成一个hdfs block 对应一个 inputsplit:

核心就是数据本地化,在当前机器上计算的数据尽量不要传输

1个inputsplit>1个block时,比如1个inputsplit使用2个block时,hdfs的这两个block不一定都存储在同一个节点上,那么必然会产生网络传输,将需要的另一个block 传输到计算的这个节点上.

inputsplit<1个block时,那么一个block的数据中就必然有一部分没有被处理,必然就会被别的mapper处理,必然增大网络传输的概率.

 

 

 

 

 

 

 

 

看下图:

 

 

 

 

 

 

1 FileInputFormat代码简析, 回答如何将源文件逻辑分割

 

package org.apache.hadoop.mapreduce.lib.input


读取hdfs目录下的文件,后将文件便利,在执行getSplits方法时会发现,for循环内代码中,
每一个文件不论是1K还是1G
都会被处理封装成对应个数的inputsplit ,即一个文件对应一个或者多个split
而每一个split都会对应一个map进程
如果小文件太多,那么开启map进程太多 势必浪费资源,操作系统开启,执行,挂起一个进程是很消耗资源的

  /** 
   * Generate the list of files and make them into FileSplits.
   */ 
  public List<InputSplit> getSplits(JobContext job
                                    ) throws IOException {
    long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job)); 
    long maxSize = getMaxSplitSize(job);

    // generate splits
    List<InputSplit> splits = new ArrayList<InputSplit>();
	// hdfs//master:9000/hello 则files为hello   hdfs//master:9000/ 则files为目录下的所有文件
    List<FileStatus>files = listStatus(job); // 读取job中的输入路径,进入
 
    for (FileStatus file: files) {// 对目标路径下的文件/文件数 进行截取成 split操作, 每个文件至少对应一个切片,一个切片对应一个map
	//如果每个文件都是1M,那么这个文件就在切片时对应一个split 也就对应一个map操作,1T的文件就1T个map,这种计算要消费多大的资源,这就是小文件不适合hdfs的原因,
	//而如果64个1m文件合并后在处理,对应map就缩小了63倍,这样对程序性能影响有多少 可想而知
      Path path = file.getPath();// 获取文件路径
      FileSystem fs = path.getFileSystem(job.getConfiguration());// 获取hdfs操作
      long length = file.getLen();
      BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0, length);
      if ((length != 0) && isSplitable(job, path)) { // 当目标文件长度>0并且是可split时
        long blockSize = file.getBlockSize();// 文件块大小 默认是64M
        long splitSize = computeSplitSize(blockSize, minSize, maxSize);// 计算切片尺寸,进入此方法,可见结果为64M, 参数minSize值为1,maxSize为long的最大值
        如果想调整切片大小,需要重写方法 getMaxSplitSize    public static long getMinSplitSize( 两个方法
		
	
        long bytesRemaining = length;
        while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
          int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
          splits.add(new FileSplit(path, length-bytesRemaining, splitSize, 
                                   blkLocations[blkIndex].getHosts()));
          bytesRemaining -= splitSize;
        }
        
        if (bytesRemaining != 0) {
          splits.add(new FileSplit(path, length-bytesRemaining, bytesRemaining, 
                     blkLocations[blkLocations.length-1].getHosts()));
        }
      } else if (length != 0) { // 文件不可拆分
        splits.add(new FileSplit(path, 0, length, blkLocations[0].getHosts()));
      } else { // 文件长度=0
        //Create empty hosts array for zero length files
        splits.add(new FileSplit(path, 0, length, new String[0]));
      }
    }
    
    // Save the number of input files in the job-conf
    job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());

    LOG.debug("Total # of splits: " + splits.size());
    return splits;
  }



  
  如下: 
  protected List<FileStatus> listStatus(
   ....
   Path[] dirs = getInputPaths(job); // 进入   大概看下 )



仅仅是对1T数据按照64M一个个的做标记, 是逻辑切分,不是物理切分,仅仅记录原始数据位置和要处理的长度
类比于高速公路上的 37公里处, 70公里处的路标, 仅仅是个标志而已,实际上高速公路还是连在一起的
看下面的数据结构,如果是物理切分的话,其数据结构必然也会要有 private bytes[] buf; 这个属性
public class FileSplit extends InputSplit implements Writable {
  private Path file;
  private long start;
  private long length;
  private String[] hosts;

 。。。。
}

 

 

 

2 RecordReader 代码简析,回答如何将split转换成键值对:

 

package org.apache.hadoop.mapreduce.lib.input

 TextInputFormat.


  @Override
  public RecordReader<LongWritable, Text> 
    createRecordReader(InputSplit split,
                       TaskAttemptContext context) {
    return new LineRecordReader();  // 进入
  }
  }


  如下:
  /**
 * Treats keys as offset in file and value as line. 
 */
  LineRecordReader.java
  
  nextKeyValue()  调用一次  则当前 key  value 就被赋值到,  相当于迭代器
  
    public boolean nextKeyValue() throws IOException {
    if (key == null) {
      key = new LongWritable(); // 第一次调用时,key = 0  eg: hello.txt (大小10KB) 
    }
    key.set(pos);
    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) {
      newSize = in.readLine(value, maxLineLength,  //读取第一行时,将第一行内容写入到value内,并返回第一行读完后的位置
          Math.max(maxBytesToConsume(pos), maxLineLength));
      if (newSize == 0) {
        break;
      }
      pos += newSize; // 记录pos现在的位置,为读取第二行时, key.set(pos);做准备工作
      if (newSize < maxLineLength) {
        break;
      }
	  // 这样赋值对全局变量 key  value赋值完毕后,通过getCurrentKey  getCurrentValue 获取现在的值  


....}





看map如何调用写法:
  
  Mapper.java:
  
   public void run(Context context) throws IOException, InterruptedException {
    setup(context);
    while (context.nextKeyValue()) {// 不断context.nextKeyValue()进入方法后实际就是RecordReader.nextKeyValue()来不断将目标文件的行,行内容放在<k,v>
      map(context.getCurrentKey(), context.getCurrentValue(), context); //不断取值
    }
    cleanup(context);
  }

 

 

 

总结如下:

 



 

 

 

系统回答如下:

问:从源代码的角度分析map函数处理的<k1,v1>是如何从HDFS文件中获取的?
答:
1.从TextInputFormat入手分析,找到父类FileInputFormat,找到父类InputFormat。
  在InputFormat中找到2个方法,分别是getSplits(...)和createRecordReader(...)。
  通过注释知道getSplits(...)作用是把输入文件集合中的所有内容解析成一个个的InputSplits,每一个InputSplit对应一个mapper task。
  createRecordReader(...)作用是创建一个RecordReader的实现类。RecordReader作用是解析InputSplit产生一个个的<k,v>。
2.在FileInputFormat中找到getSplits(...)的实现。
  通过实现,获知
  (1)每个SplitSize的大小和默认的block大小一致,好处是满足数据本地性。
  (2)每个输入文件都会产生一个InputSplit,即使是空白文件,也会产生InputSPlit;
  如果一个文件非常大,那么会按照InputSplit大小,切分产生多个InputSplit。
3.在TextInputFormat中找到createRecordReader(...)的实现,在方法中找到了LineRecordReader。
  接下来分析LineRecordReader类。  
  在RecordReader类中,通过查看多个方法,知晓key、value作为类的属性存在的,且知道了nextKeyValue()方法的用法。  
  在LineRecordReader类中,重点分析了nextKeyValue(...)方法。在这个方法中,重点分析了newSize = in.readLine(value, maxLineLength, maxBytesToConsume(pos));
  在in.readLine(...)中,第一个形参存储被读取的行文本内容,返回值表示被读取内容的字节数。
  通过以上代码,分析了InputSplit中的内容是如何转化为一个个的<k,v>。
4.从Mapper类中进行分析,发现了setup()、cleanup()、map()、run()。
  在run()方法中,通过while,调用context.nextKeyValue(...)。
  进一步分析Context的接口类是org.apache.hadoop.mapreduce.lib.map.WrappedMapper.MapContext,MapContext调用了nextKeyValue(...)。最终找到了MapContext的实现了MapContextImpl类org.apache.hadoop.mapreduce.task.MapContextImpl。
  在这个类的构造方法中,发现传入了RecordReader的实现类。  

 

 

<think>嗯,用户这次想要了解MapReduce的介绍以及Map函数和Reduce任务的具体作用。结合用户提供的参考引用和我之前关于CAP定理的回答,需要特别注意技术细节的准确性和表述的清晰度。 用户提供的5个引用材料很有价值,其中[1][3][4][5]直接关于MapReduce核心机制,[2]则补充了Python中map/reduce的基础概念。需要重点整合这些信息:引用[1]定义框架概念,引用[3][5]详细描述Shuffle机制,引用[4]通过案例说明函数作用。 注意到用户特别询问"不同Map函数",这提示需要强调Map函数的可定制性。从引用[4]的案例看,同样的MapReduce框架可以通过替换map函数实现词频统计、数据清洗等不同功能。而Reduce任务的差异主要体现在聚合逻辑上,如求和/求平均等。 技术实现层面必须突出三点:一是Map阶段的并行拆分(引用[1][4]),二是Shuffle的"相同key聚合"核心作用(引用[3][5]),三是Reduce的分布式计算结果合并(引用[3])。流程图展示会很有效,但需注意纯文本表达的限制。 最后考虑延伸问题设计:用户似乎对底层机制感兴趣,所以准备补充Shuffle优化和容错机制的问题;同时提供CP/AP系统对比,延续之前CAP定理的讨论脉络。 </think>以下是对MapReduce框架的详细解析,重点阐述其核心思想、工作流程Map/Reduce函数的作用,结合技术文献进行说明: --- ### **MapReduce框架概述** MapReduce是一种分布式计算框架,核心思想是将大数据处理任务分解为 **并行化的Map阶段** 和 **聚合化的Reduce阶段**。其处理流程满足以下公式: $$ \text{Input} \xrightarrow{\text{Split}} \text{Map} \xrightarrow{\text{Shuffle}} \text{Reduce} \xrightarrow{\text{Merge}} \text{Output} $$ --- ### **Map函数的核心作用** Map任务对输入数据块进行**初步处理转换**,生成中间键值对: 1. **数据分块处理** 每个Map任务处理一个数据分片(如HDFS文件块),实现并行计算[^1][^4]。 2. **键值对转换** 用户自定义`map()`函数将输入数据转换为`<key, value>`中间结果: ```python # 示例:词频统计的Map函数 def map_func(document): for word in document.split(): yield (word, 1) # 输出(word, 1) ``` 3. **输出中间结果** 结果按key分区存储到本地磁盘,为Shuffle阶段做准备[^3][^5]。 --- ### **Reduce任务的核心作用** Reduce任务对Map输出的中间结果进行**跨节点聚合**: 1. **数据拉取排序** Reduce节点从所有Map节点拉取相同key的数据,并按key排序(Shuffle阶段)[^3][^5]。 2. **聚合计算** 用户自定义`reduce()`函数对同一key的value集合进行运算: ```python # 示例:词频统计的Reduce函数 def reduce_func(key, values): yield (key, sum(values)) # 输出(word, 总次数) ``` 3. **结果输出** 最终结果写入分布式存储系统(如HDFS)[^4]。 --- ### **关键机制:Shuffle过程** Shuffle是连接Map和Reduce的核心环节,实现: 1. **分区(Partitioning)** Map输出的`<k,v>`按key哈希分配到不同Reduce分区[^5]。 2. **排序(Sorting)** 每个分区内数据按键排序,便于Reduce聚合[^3]。 3. **数据传递** 通过HTTP协议将中间数据从Map节点传输到Reduce节点[^5]。 > **流程图示** > ``` > [Map Task 1] → (k1,v1) → │ > [Map Task 2] → (k2,v2) → ├─Shuffle─→ [Reduce Task] → (k1,sum) > [Map Task 3] → (k1,v3) → │ > ``` --- ### **Map/Reduce函数的设计差异** | **组件** | **输入** | **操作性质** | **输出** | **并行粒度** | |-----------|------------------------|-------------------|------------------------|--------------| | Map函数 | 数据分片(如文本行) | **无状态转换** | 中间键值对`<k,v>` | 分片内独立并行 | | Reduce函数| 同一key的所有value | **有状态聚合** | 最终结果`<k,聚合值>` | 跨分片全局聚合 | > **典型应用场景**: > - **Map**:数据清洗、特征提取、简单过滤 > - **Reduce**:求和/平均值统计、去重计数、关联分析[^4] --- ### 工程意义 1. **自动容错** Worker节点故障时,主节点自动重启任务[^1]。 2. **横向扩展性** 增加节点即可线性提升处理能力[^3]。 3. **抽象简化** 开发者只需关注Map/Reduce逻辑,无需处理分布式通信细节[^1][^4]。 --- ### 相关问题 1. Shuffle阶段的数据排序如何影响Reduce性能? 2. 哪些场景下MapReduce可能成为性能瓶颈? 3. 对比SparkMapReduce在Shuffle机制上的优化差异? > 参考资料: > [^1]: MapReduce核心概念 > [^3]: Reduce阶段工作机制 > [^4]: Map/Reduce函数作用案例 > [^5]: Shuffle机制解析
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值