Reduce端分组排序源码分析

一、流程源码分析

​ 在xxxJobRunner类中有一个内部类ReduceTaskRunnable,该类是一个专门执行ReduceTask任务的线程类,其中的run方法就是开始执行reduce任务的方法,在run方法的内部,以下代码标志reduce任务开始执行:

ReduceTask reduce = new ReduceTask(systemJobFile.toString(),
              reduceId, taskId, mapIds.size(), 1);
	…………
reduce.run(localConf, Job.this);

​ 查看reduce.run内部逻辑,关键在于执行以下代码(该run方法是ReduceTask类中的):

if (useNewApi) {
  runNewReducer(job, umbilical, reporter, rIter, comparator, 
                keyClass, valueClass);
} else {
  runOldReducer(job, umbilical, reporter, rIter, comparator, 
                keyClass, valueClass);
}

​ 执行的是runNewReducer方法,继续查看它的逻辑,其中以下方法就是将分好组的数据一组一组的送入我们自定义的reduce方法中:

reducer.run(reducerContext);

​ 该方法的底层逻辑就是Reducer类的run方法,如下所示:

public void run(Context context) throws IOException, InterruptedException {
  setup(context);
  try {
    while (context.nextKey()) {
      reduce(context.getCurrentKey(), context.getValues(), context);
      // If a back up store is used, reset it
      Iterator<VALUEIN> iter = context.getValues().iterator();
      if(iter instanceof ReduceContext.ValueIterator) {
        ((ReduceContext.ValueIterator<VALUEIN>)iter).resetBackupStore();        
      }
    }
  } finally {
    cleanup(context);
  }
}

​ 在该方法中,通过while循环遍历上下文对象中的key,执行的是nextKey方法,那么继续查看nextKey方法的逻辑是什么样的:

/** Start processing next unique key. */
public boolean nextKey() throws IOException,InterruptedException {
  while (hasMore && nextKeyIsSame) {
    nextKeyValue();
  }
  if (hasMore) {
    if (inputKeyCounter != null) {
      inputKeyCounter.increment(1);
    }
    return nextKeyValue();
  } else {
    return false;
  }
}

​ 可以看到该方法首先会以while循环的方式调用nextKeyValue方法,循环的条件是"hasMore && nextKeyIsSame",即是否还有数据以及下一个数据的key是否和上一个的相同,如果这两个条件都满足了,那么就会一直循环执行nextKeyValue方法,那么继续查看nextKeyValue方法的源码:

/**
 * Advance to the next key/value pair.
 */
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
  if (!hasMore) {
    key = null;
    value = null;
    return false;
  }
  firstValue = !nextKeyIsSame;
  DataInputBuffer nextKey = input.getKey();
  currentRawKey.set(nextKey.getData(), nextKey.getPosition(), 
                    nextKey.getLength() - nextKey.getPosition());
  buffer.reset(currentRawKey.getBytes(), 0, currentRawKey.getLength());
  key = keyDeserializer.deserialize(key);
  DataInputBuffer nextVal = input.getValue();
  buffer.reset(nextVal.getData(), nextVal.getPosition(), nextVal.getLength()
      - nextVal.getPosition());
  value = valueDeserializer.deserialize(value);

  currentKeyLength = nextKey.getLength() - nextKey.getPosition();
  currentValueLength = nextVal.getLength() - nextVal.getPosition();

  if (isMarked) {
    backupStore.write(nextKey, nextVal);
  }

  hasMore = input.next();
  if (hasMore) {
    nextKey = input.getKey();
    nextKeyIsSame = comparator.compare(currentRawKey.getBytes(), 0, 
                                   currentRawKey.getLength(),
                                   nextKey.getData(),
                                   nextKey.getPosition(),
                                   nextKey.getLength() - nextKey.getPosition()
                                       ) == 0;
  } else {
    nextKeyIsSame = false;
  }
  inputValueCounter.increment(1);
  return true;
}

​ 在该方法中,首先会进行合法性判断,然后判断当前这个键值对中的值是不是当前key所代表的组的第一个值,接着就是将当前的数据存入上下文对象中。

​ 其中继续向下有一段比较重要的代码涉及到了分组排序的操作,即:

hasMore = input.next();
if (hasMore) {
  nextKey = input.getKey();
  nextKeyIsSame = comparator.compare(currentRawKey.getBytes(), 0, 
                                 currentRawKey.getLength(),
                                 nextKey.getData(),
                                 nextKey.getPosition(),
                                 nextKey.getLength() - nextKey.getPosition()
                                     ) == 0;
} else {
  nextKeyIsSame = false;
}

​ nextKeyIsSame的值就是依据分组排序的结果来进行的。此处调用的comparator.compare方法,具体实现在于传入的比较器对象中实现的compare方法,那么这个比较器对象是在哪里确定的呢?本着一种不见棺材不落泪,不到南墙不回头的精神继续查看源码:

​ comparator在Reduce上下文对象ReduceContextImpl中是一个成员变量,其具体的类型是:

private RawComparator<KEYIN> comparator;

​ 它的赋值操作是在ReduceContextImpl的构造方法中完成的,所以要想看谁对它赋的值,需要看谁声明了这个上下文对象,继续看:

​ 在Task类中的createReduceContext方法中声明了ReduceContextImpl,并进行了初始化

protected static <INKEY,INVALUE,OUTKEY,OUTVALUE> 
org.apache.hadoop.mapreduce.Reducer<INKEY,INVALUE,OUTKEY,OUTVALUE>.Context
createReduceContext(
    				…………
                    RawComparator<INKEY> comparator,
                    Class<INKEY> keyClass, Class<INVALUE> valueClass
) throws IOException, InterruptedException {
  org.apache.hadoop.mapreduce.ReduceContext<INKEY, INVALUE, OUTKEY, OUTVALUE> 
  reduceContext = 
    new ReduceContextImpl<INKEY, INVALUE, OUTKEY, OUTVALUE>(
      													…………
                                                            comparator, 
                                                           	…………);
	……
  return reducerContext;
}

​ 继续查看谁调用了createReduceContext方法,发现在ReduceTask类中的runNewReducer方法中调用了该方法,并传入了比较器对象:

org.apache.hadoop.mapreduce.Reducer.Context 
     reducerContext = createReduceContext(reducer, job, getTaskID(),
                                           rIter, reduceInputKeyCounter, 
                                           reduceInputValueCounter, 
                                           trackedRW,
                                           committer,
                                           reporter, comparator, keyClass,
                                           valueClass);

​ createReduceContext方法是在reduce.run之前执行的,可见在reduce任务真正开始之前,需要进行一系列的准备工作,其中就包括比较器对象的准备。

​ 而此处的comparator对象,也是传入Reducer方法的一个参数,继续查看是谁向runNewReducer方法中传入了comparator:

​ 在ReduceTask类的run方法中,先是获取用户为当前job定义的比较器,以用于将输入到reduce的key进行分组,然后将这个比较器传入到runNewReducer方法中:

RawComparator comparator = job.getOutputValueGroupingComparator();

if (useNewApi) {
  runNewReducer(job, umbilical, reporter, rIter, comparator, 
                keyClass, valueClass);
} else {
  runOldReducer(job, umbilical, reporter, rIter, comparator, 
                keyClass, valueClass);
}

​ 查看JobConf类中的getOutputValueGroupingComparator方法:

/** 
 * Get the user defined {@link WritableComparable} comparator for 
 * grouping keys of inputs to the reduce.
 * 
 * @return comparator set by the user for grouping values.
 * @see #setOutputValueGroupingComparator(Class) for details.
 */
public RawComparator getOutputValueGroupingComparator() {
  Class<? extends RawComparator> theClass = getClass(
    JobContext.GROUP_COMPARATOR_CLASS, null, RawComparator.class);
  if (theClass == null) {
    return getOutputKeyComparator();
  }
  
  return ReflectionUtils.newInstance(theClass, this);
}

​ 在该方法内部首先会尝试获取JobContext.GROUP_COMPARATOR_CLASS这个属性值,如果我们自定义了一个比较器的类,并在Driver类中设置了该属性的值,那么设置的值就作为比较器对象,否则就返回默认值null。

​ 如果theClass的值为null,那么会执行getOutputKeyComparator方法:

public RawComparator getOutputKeyComparator() {
  Class<? extends RawComparator> theClass = getClass(
    JobContext.KEY_COMPARATOR, null, RawComparator.class);
  if (theClass != null)
    return ReflectionUtils.newInstance(theClass, this);
  return WritableComparator.get(getMapOutputKeyClass().asSubclass(WritableComparable.class), this);
}

​ getOutputKeyComparator方法内部也是,如果设置了KEY_COMPARATOR属性值的值就创建这个属性值对象,否者就返回在Driver类中设置的Key的OutputKeyClass的对象。getMapOutputKeyClass返回的是map输出数据的key类型,如果没有设置,会使用最终输出数据的key类型返回。

public Class<?> getMapOutputKeyClass() {
  Class<?> retv = getClass(JobContext.MAP_OUTPUT_KEY_CLASS, null, Object.class);
  if (retv == null) {
    retv = getOutputKeyClass();
  }
  return retv;
}

​ 还有必须要注意的是,map输出的数据的key类型必须是WritableComparable类的子类,也就是说必须继承了WritableComparable这个类,这也就是说为什么当我们自定义的bean对象作为key的时候,一定要继承WritableComparable类了。

​ 继续查看WritableComparator.get方法,

public static WritableComparator get(
    Class<? extends WritableComparable> c, Configuration conf) {
  WritableComparator comparator = comparators.get(c);
  if (comparator == null) {
    // force the static initializers to run
    forceInit(c);
    // look to see if it is defined now
    comparator = comparators.get(c);
    // if not, use the generic one
    if (comparator == null) {
      comparator = new WritableComparator(c, conf, true);
    }
  }
  // Newly passed Configuration objects should be used.
  ReflectionUtils.setConf(comparator, conf);
  return comparator;
}

​ 该方法首先会执行comparators.get(c)方法获取c所映射的值,如果c没有映射的值,那么get方法就返回null,否则就返回它所映射的比较器对象。

​ 如果我们自定义的key类型没有所映射的值,那么返回null,此时就会使用反射技术创建一个comparator实例,这个实例也就是在后面Reduce端遍历key的时候,使用的comparator实例。

​ 如果key的类型Text,那么是可以找到对应映射的value,也就是有对应的比较器对象,那么直接返回即可。

​ 此时我们就搞清楚了,Reduce端是如何分组的,它需要借助一个key的比较器,然后在遍历数据的时候进行key的比较,如果key相同那么就是一组的,否则就不属于同一组。其实从更加准确的角度来讲,将数据的key传入到比较器的compare方法中,compare方法如果返回的结果是0,那么成员变量nextKeyIsSame的值就是true,说明当前一条记录的key与下一条记录的key是相同的,它们属于同一组,如果不为0,nextKeyIsSame的值就为false。

​ 所以,如果我们想要将一些key不完全相同的数据放入同一组,就可以自定义一个bean对象,这个bean对象要继承WritableComparable类,然后实现compareTo方法,在compareTo方法中可以定义哪些key是一组的,哪些不是。还有一种可以自定义分组的方式就是自定义一个类继承WritableComparator类,然后重写该类的compare方法,compare方法就是分组的逻辑,最后在Drive人类中设置一下这个分组排序类即可。

job.setGroupingComparatorClass(xxx.class)

​ 查看这段设置代码的源码:

public void setOutputValueGroupingComparator(
    Class<? extends RawComparator> theClass) {
  setClass(JobContext.GROUP_COMPARATOR_CLASS,
           theClass, RawComparator.class);
}

​ 可以看出,这个设置也正是对应了上面的JobConf类中的getOutputValueGroupingComparator方法中首先要检测获取的JobContext.GROUP_COMPARATOR_CLASS属性值。如果该属性值在Driver中设置了,那么就直接得到比较器对象,无需后面的利用数据的key的类型对象得到比较器对象了。

​ 这也就是说为何要在Reduce端,对从各个MapTask拉取的数据进行一次全局的排序,这样也是为了方便将同一组的数据放入一个reduce方法中。

二、key的相关数据类型源码分析

1、Text

​ 下面来查看一下Text数据类型的与比较器相关部分的代码:

​ 如果map输出的key是Text类型,那么调用的comparator.compare方法实际就是Text类中的compare方法:

/** A WritableComparator optimized for Text keys. */
public static class Comparator extends WritableComparator {
  public Comparator() {
    super(Text.class);
  }

  @Override
  public int compare(byte[] b1, int s1, int l1,
                     byte[] b2, int s2, int l2) {
    int n1 = WritableUtils.decodeVIntSize(b1[s1]);
    int n2 = WritableUtils.decodeVIntSize(b2[s2]);
    return compareBytes(b1, s1+n1, l1-n1, b2, s2+n2, l2-n2);
  }
}

​ compareBytes方法是WritableComparator中的一个静态方法,可见对于Text类型(二进制数据)的key,其排序规则是按照字典序来排的:

/** Lexicographic order of binary data. */
public static int compareBytes(byte[] b1, int s1, int l1,
                               byte[] b2, int s2, int l2) {
  return FastByteComparisons.compareTo(b1, s1, l1, b2, s2, l2);
}
public static int compareTo(byte[] b1, int s1, int l1, byte[] b2, int s2,
    int l2) {
  return LexicographicalComparerHolder.BEST_COMPARER.compareTo(
      b1, s1, l1, b2, s2, l2);
}

2、IntWritable

/** A Comparator optimized for IntWritable. */ 
public static class Comparator extends WritableComparator {
  public Comparator() {
    super(IntWritable.class);
  }
  
  @Override
  public int compare(byte[] b1, int s1, int l1,
                     byte[] b2, int s2, int l2) {
    int thisValue = readInt(b1, s1);
    int thatValue = readInt(b2, s2);
    return (thisValue<thatValue ? -1 : (thisValue==thatValue ? 0 : 1));
  }
}

3、自定义bean对象

​ 对于自定义的bean对象,reduce端在对数据进行分组的时候,会先依据bean创建一个比较器,当执行到comparator.compare方法时,此时首先调用的就是WritableComparator自带的compare方法,即下面源码中的第一个compare方法,在这个compare方法的最后返回的是compare(key1, key2)的执行结果。

@Override
public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) {
  try {
    buffer.reset(b1, s1, l1);                   // parse key1
    key1.readFields(buffer);
    
    buffer.reset(b2, s2, l2);                   // parse key2
    key2.readFields(buffer);
    
    buffer.reset(null, 0, 0);                   // clean up reference
  } catch (IOException e) {
    throw new RuntimeException(e);
  }
  
  return compare(key1, key2);                   // compare them
}

/** Compare two WritableComparables.
 *
 * <p> The default implementation uses the natural ordering, calling {@link
 * Comparable#compareTo(Object)}. */
@SuppressWarnings("unchecked")
public int compare(WritableComparable a, WritableComparable b) {
  return a.compareTo(b);
}

​ 而compare(key1, key2)就是上面源码中的第二个compare方法,它的参数是两个WritableComparable类型的对象。其内部调用的正是我们自定义的bean对象重写的compareTo方法。

三、自定义WritableComparator

​ 当然自定义分组,也可以通过自定义一个类继承WritableComparator来实现。继承这个类,需要将分组的逻辑重写在compare方法中,然后在Driver类中进行设置。

​ 而且一定需要注意的是,一定要写一个构造方法,在这个构造方法中调用一下父类构造方法,并向父类构造方法传入我们数据的key的类型。如下面所示。

public class GroupComparator extends WritableComparator {

    public GroupComparator() {
        super(OrderBean.class,true);
    }

    @Override
    public int compare(WritableComparable a, WritableComparable b) {

        OrderBean abean = (OrderBean)a;
        OrderBean bbean = (OrderBean)b;

        int result;
        if (abean.getId() > bbean.getId()){
            result = 1;
        }else if (abean.getId() < bbean.getId()){
            result = -1;
        }else {
            result = 0;
        }
        return result;
    }
}
posted @ 2021-02-17 16:20  有心有梦  阅读(228)  评论(0编辑  收藏  举报