easy-algorithm-interview-an.../bigdata/hadoop/hadoop mapper从源码开始 详解.md

9.1 KiB
Raw Blame History

hadoop的mapreduce计算框架中最重要的两个部分自然就是mapper跟reducer了。写了这么久的MR一直没有机会研究源码也挺遗憾的。趁着这波有一些要深入了解的需求加上周末的一些时间仔细阅读了一下mapper相关源码有了自己的一些小小心得权当笔记。写得不好或者有不对的地方请童鞋们指出

1.mapper源码

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.hadoop.mapreduce;

import java.io.IOException;

import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.io.RawComparator;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.mapreduce.task.MapContextImpl;

@InterfaceAudience.Public
@InterfaceStability.Stable
public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {

  /**
   * The <code>Context</code> passed on to the {@link Mapper} implementations.
   */
  public abstract class Context
    implements MapContext<KEYIN,VALUEIN,KEYOUT,VALUEOUT> {
  }
  
  /**
   * Called once at the beginning of the task.
   */
  protected void setup(Context context
                       ) throws IOException, InterruptedException {
    // NOTHING
  }

  /**
   * Called once for each key/value pair in the input split. Most applications
   * should override this, but the default is the identity function.
   */
  @SuppressWarnings("unchecked")
  protected void map(KEYIN key, VALUEIN value, 
                     Context context) throws IOException, InterruptedException {
    context.write((KEYOUT) key, (VALUEOUT) value);
  }

  /**
   * Called once at the end of the task.
   */
  protected void cleanup(Context context
                         ) throws IOException, InterruptedException {
    // NOTHING
  }
  
  /**
   * Expert users can override this method for more complete control over the
   * execution of the Mapper.
   * @param context
   * @throws IOException
   */
  public void run(Context context) throws IOException, InterruptedException {
    setup(context);
    try {
      while (context.nextKeyValue()) {
        map(context.getCurrentKey(), context.getCurrentValue(), context);
      }
    } finally {
      cleanup(context);
    }
  }
}

为了避免太过冗长,把源码里的一些注释以及说明拿掉了,后面再跟大家一一道来。

2.mapper类的基本结构

mapper类里的结构其实不太复杂总共四个方法一个抽象类具体的结构如下图所示
这里写图片描述

这几个方法的功能相对也很好理解setup()方法一般就是用来做一些在map真正开始干活之前的准备工作例如相关配置文件的读取参数的传递等clean()方法则是用来做一些擦屁股的活。当然,这两个函数在实际中也是可以不写的。如果你不用传参不用读配置,代码逻辑也相对简单没有要后续擦屁股的活,自然也就用不着这两函数。

既然是mapper类那map()方法肯定就是我们真正干活的地方了。
以mapper源码中的wordcount例子中的map方法为例

public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
    StringTokenizer itr = new StringTokenizer(value.toString());
    while (itr.hasMoreTokens()) {
      word.set(itr.nextToken());
      context.write(word, one);
     }
}

相信有点java基础的同学都能看懂上面这个方法。就是将输入的一行文本每次拆分成一个(word,one)的k,v对然后分发给reducer做进一步的处理。如果是wordcount那就是相同的word被分发到相同的reduce端然后做count操作。

run()方法则是驱动整个代码按照setup(),map(),cleanup()的流程正常工作的一个方法。里面的可配置项相对也比较多比较杂。后面有用的时候跟大家专门讲讲run()方法里的相关配置。

context是mapper里的一个内部类主要是为了在map任务或者reduce任务中跟踪task的相关状态。在mapper类中这个context就可以存储一些job conf有关的信息。在setup()方法中就可以用context读取相关的配置信息。这部分的源码还没有仔细研究如果有什么问题欢迎大家指出)

context是一个抽象类具体的继承层次关系如下图所示
这里写图片描述

3.mapper类的一些子类

mapper类其实类似于一个接口里面没有任何具体实现实际开发场景中肯定需要我们至少实现map()方法来满足业务需求。同时hadoop中也有一些mapper的子类具体有哪些请看下图
这里写图片描述

由图可知mapper一共有九个子类。我们挑其中的几个子类来稍做分析。

3.1 InverseMapper

首先上源码

@InterfaceAudience.Public
@InterfaceStability.Stable
public class InverseMapper<K, V> extends Mapper<K,V,V,K> {

  /** The inverse function.  Input keys and values are swapped.*/
  @Override
  public void map(K key, V value, Context context
                  ) throws IOException, InterruptedException {
    context.write(value, key);
  }
  
}

从InverseMapper的源码很容易看出他就做了一件很简单的事情重写map()方法将map阶段的k,v掉换了个然后输出(v,k)对。

3.2 TokenCounterMapper

public class TokenCounterMapper extends Mapper<Object, Text, Text, IntWritable>{
    
  private final static IntWritable one = new IntWritable(1);
  private Text word = new Text();
  
  @Override
  public void map(Object key, Text value, Context context
                  ) throws IOException, InterruptedException {
    StringTokenizer itr = new StringTokenizer(value.toString());
    while (itr.hasMoreTokens()) {
      word.set(itr.nextToken());
      context.write(word, one);
    }
  }
}

wordcount的mapper阶段实现不解释。

3.3 RegexMapper

public class RegexMapper<K> extends Mapper<K, Text, Text, LongWritable> {

  public static String PATTERN = "mapreduce.mapper.regex";
  public static String GROUP = "mapreduce.mapper.regexmapper..group";
  private Pattern pattern;
  private int group;

  public void setup(Context context) {
    Configuration conf = context.getConfiguration();
    pattern = Pattern.compile(conf.get(PATTERN));
    group = conf.getInt(GROUP, 0);
  }

  public void map(K key, Text value,
                  Context context)
    throws IOException, InterruptedException {
    String text = value.toString();
    Matcher matcher = pattern.matcher(text);
    while (matcher.find()) {
      context.write(new Text(matcher.group(group)), new LongWritable(1));
    }
  }
}

从源码比较容易看出这是正则版的wordcount。

4.mapper阶段流程小结

总结起来mapper阶段的工作流程如下
1.先把输入的文件,按照一定的标准和方法做切分(InputSplit)。这个切分的过程很关键因为MR的核心思想就是将一个巨大的任务切分成多个小任务分发给不同节点计算每一个输入片对应的就是一个mapper。如果切分没有做好后面的工作自然就无从谈起。后续有时间再专门阐述一下切分相关的具体细节。
2.对切分完的输入按照一定的规则解析成(k,v)对。
3.调用Mapper类中的map()方法,对第二步解析出来的(k,v)对进行操作。这也是我们需要实现真正逻辑的地方。每调用一次map()方法就会输出0个或1个或多个(k,v)对。
4.对第三步输出的(k,v)对进行partition 。partition是基于k进行的这样就保证相同的k落在同一个分区之中。
5.对第四步产生的(k,v)对排序。首先是按k排序如果k相同则按v排序。如果后续还有combiner阶段则继续进行combiner如果没有则直接将数据输出到磁盘。
6.combiner阶段。其实跟reduce的实现逻辑是一样的。比如在wordcount中reducer类都不用实现直接在run()方法中用job.setReducerClass(IntSumReducer.class)设置即可。