Hadoop之TeraSort学习笔记

    TeraSort源码包含很多个java文件,其中可以分为三个部分:TeraGen, TeraSort和TeraValidate。

    TeraGen负责生成排序所需的随机数据,TeraValidate用来验证排序结果。

    而与TeraSort排序相关的java文件有TeraSort.java, TeraInputFormat.java, TeraOutputFormat.java, TeraScheduler.java。

    编译完源代码之后,设置运行参数,用TeraGen生成500M的数据,然后运行TeraSort(代码中添加Job.setNumReduceTasks(10),即设定Reduce Task的数量为10, 否则默认为1),会发现在进入Map阶段之前有下列几行输出,我就从这几句输出入手来学习这几个java文件。

    Spent 251ms computing base-splits.    (1)

    Spent 27ms computing TeraScheduler splits.    (2)

    Computing input splits took 280ms    (3)

    Sampling 10 splits of 15    (4)

    Making 10 from 100000 sampled records    (5)

    Computing parititions took 1216ms    (6)

    Spent 1534ms computing partitions.    (7)

    接下来就按照这几句输出的顺序看起,

    先看TeraSort的main方法:

int res = ToolRunner.run(new Configuration(), new TeraSort(), args);
System.exit(res);

    Ctrl点进run这个方法里面看到:

public static int run(Configuration conf, Tool tool, String[] args) 
    throws Exception{
    if(conf == null) {
      conf = new Configuration();
    }
    GenericOptionsParser parser = new GenericOptionsParser(conf, args);
    //set the configuration back, so that Tool can configure itself
    tool.setConf(conf);
    
    //get the args w/o generic hadoop args
    String[] toolArgs = parser.getRemainingArgs();
    return tool.run(toolArgs);
  }

    即上面new TeraSort()对应下面的参数Tool tool,而下面方法return tool.run,也就是运行TeraSort得到其返回值,TeraSort运行完成返回1, 否则返回0,所以在完成时System.exit(res)退出。

    然后开始看TeraSort.java的run方法,一路各种set之后有一句关键的    

TeraInputFormat.writePartitionFile(job, partitionFile);

    即调用了TeraInputFormat里的writePartitionFile方法,于是转向该方法,看到

final TeraInputFormat inFormat = new TeraInputFormat();
final TextSampler sampler = new TextSampler();
int partitions = job.getNumReduceTasks();
long sampleSize = conf.getLong(SAMPLE_SIZE, 100000);
final List<InputSplit> splits = inFormat.getSplits(job);

    最后一句即获取输入文件的Splits,我们知道Map Task就是对输入文件的Split进行处理,那我们应该怎么getSplits呢?

    进入该方法,我们看到:

t1 = System.currentTimeMillis();
lastContext = job;
lastResult = super.getSplits(job);
t2 = System.currentTimeMillis();
System.out.println("Spent " + (t2 - t1) + "ms computing base-splits.");
if (job.getConfiguration().getBoolean(TeraScheduler.USE, true)) {
  TeraScheduler scheduler = new TeraScheduler(
    lastResult.toArray(new FileSplit[0]), job.getConfiguration());
  lastResult = scheduler.getNewFileSplits();
  t3 = System.currentTimeMillis(); 
  System.out.println("Spent " + (t3 - t2) + "ms computing TeraScheduler splits.");
}

    我们看到原来TeraInputFormat并没有重写切片的方法,而是继承了父类FileInputFormat里的getSplits方法获取切片,即super.getSplits(job); 接下来我们就看到了开头所提到的七句输出里面的前两句,证实了代码的执行顺序和我们学习的顺序是一样的,那么下面的这个TeraScheduler又是个什么东西呢?阅读它的代码看得好像不是很明白它是做什么的,那我们来看它的功能的介绍:

    Solve the schedule and modify the FileSplit array to reflect the new schedule. It will move placed splits to front and unplacable splits to the end.

    原来是对文件的输入切片进行处理从而优化调度的呀!那它相比默认的(或没有)调度方法有什么好处呢?通过Stack Overflaw上网友的解答可以知道,这种调度方法可以:

    1、make sort local as much as possible;

    2、distribute the work evenly across machine.

    知道了这个我们回到TeraInputFormat.java里面的writePartitionFIle方法里继续往下看:

final List<InputSplit> splits = inFormat.getSplits(job);
long t2 = System.currentTimeMillis();
System.out.println("Computing input splits took " + (t2 - t1) + "ms");
int samples = Math.min(conf.getInt(NUM_PARTITIONS, 10), splits.size());
System.out.println("Sampling " + samples + " splits of " + splits.size());
final long recordsPerSample = sampleSize / samples;
final int sampleStep = splits.size() / samples;

    可以看到在getSplits工作结束以后,输出了我们开头提到的第三和第四句话,第三句话即表明我们获取这些输入文件的切片花了多长时间,而第四句输出表明我们从这些分片(splits.size)中采样了多少个样本(samples),那splits.size等于多少呢?我们点开父类的getSplits方法可以看到这几行代码:

long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
long maxSize = getMaxSplitSize(job);
blkLocations = fs.getFileBlockLocations(file, 0, length);
long splitSize = computeSplitSize(blockSize, minSize, maxSize);

    也就是说它是由这几个参数共同决定的,这几个参数可以在hadoop的配置文件里进行设置,所以分片的数量也会有所不同,我们这里是500M数据有15个分片,而它采样了其中的10个作为样本,为什么是10个呢?答案在这里:

int samples = Math.min(conf.getInt(NUM_PARTITIONS, 10), splits.size());

    我们的Reduce Task数量设置的是10, 而Partition数量与Reduce Task数量是相同的,所以samples取10和15的最小值,即10.

    这些工作完成之后,就开始通过SamplerThreadGroup进行第一次采样操作,为什么说是第一次呢?因为还有第二次,哈哈哈哈哈,我们往下看到第一次采样之后:

for(Text split : sampler.createPartitions(partitions)) {
      split.write(writer);
}

    这个循环就完成了二次采样,即从一次采样得到的100000(默认值)个样本中二次采集10个(等于Partition数量)样本,我们看到它这里调用了createParitions方法,我们跟过去瞧一眼:

Text[] createPartitions(int numPartitions) {
      int numRecords = records.size();
      System.out.println("Making " + numPartitions + " from " + numRecords + 
                         " sampled records");
      if (numPartitions > numRecords) {
        throw new IllegalArgumentException
          ("Requested more partitions than input keys (" + numPartitions +
           " > " + numRecords + ")");
      }
      new QuickSort().sort(this, 0, records.size());
      float stepSize = numRecords / (float) numPartitions;
      Text[] result = new Text[numPartitions-1];
      for(int i=1; i < numPartitions; ++i) {
        result[i-1] = records.get(Math.round(stepSize * i));
      }
      return result;
    }
  }

    果不其然,按照顺序我们看到了开头的第五句输出,这里的records.size就等于100000,,即第一次采样得到的样本大小,我们可以看到这个方法最后返回的是含有9条抽样数据的数组,为什么是9条呢?因为我们设定了10个Partition所需要的分割点数目就等于Partition数目减一,OK,这就完成了二次采样,我们返回到刚才的writePartitionFile方法接着看,

writer.close();
long t3 = System.currentTimeMillis();
System.out.println("Computing parititions took " + (t3 - t2) + "ms");

    第六句输出出现了,即两次采样过程所花费的时间,到这里我们writePartitionFIle的部分就结束啦,回归到TeraSort.java部分继续看!

long end = System.currentTimeMillis();
System.out.println("Spent " + (end - start) + "ms computing partitions.");
job.setPartitionerClass(TotalOrderPartitioner.class);

    最后一句输出get!也就是完成writePartitionFile所花费的总时间。

    这些分片啊、调度啊、采样啊等一系列准备工作结束以后就进入我们的mapreduce阶段啦,我们看到它设置TotalOrderPartitioner.class作为它的Partitioner方法,而细心的我们可以看到其实TeraSort中定义了两个Partitioner,其中一个是我们看到的TotalOrderPartitioner,另一个是没什么存在感的SimplePartitioner,这个SimplePartitioner有什么用处呢?其实并没有什么用处,因为它仅仅是对key的前缀进行简单的处理,并不能实现负载的相对均衡,所以一般情况下useSimplePartitioer = false,即使用TotalOrderPartitioner来作为Partition方法,那么这个方法都做了些什么呢?

    阅读代码我们知道:TeraInputFormat将采样得到的9个cut point存入了_partition.lst这个文件当中,而TotalOrderPartitioner就先从这个lst中读出这些cut point,然后根据这些cut point构造一棵三层的Trie,即字典树,例子如下:

Hadoop之TeraSort学习笔记_第1张图片

    当key中的前缀字母小于等于某一个节点时,它就被标记为该节点所对应的Partition,从而保证了Partition间的全局有序(Total Order),那它究竟有没有传说中的效果呢?我们在buildTrie-----getPartition方法中加入

BufferedWriter bf;
try {
    bf = new BufferedWriter(new FileWriter(file,true));
    bf.append(String.valueOf(trie.findPartition(key)));
    bf.newLine();
    bf.close();
    } catch (IOException e) {
    	  e.printStackTrace();
}

    这样就可以把500万条记录所分配的Partition的序号输出到我们指定的文件中去,之后用matlab中的函数进行个数统计,因为我们设定了10个Reduce Task,所以它们的序号就是0--9,下面的数据就是0--9分别Partition记录的个数及比例:

个数:

      501736

      510323

      501848

      502895

      499158

      495228

      494499

      496448

      502608

      495257

比例:

      10.0347

      10.2065

      10.0370

      10.0579

       9.9832

       9.9046

       9.8900

       9.9290

      10.0522

       9.9051

    可以明显的看到,相当的均匀!

你可能感兴趣的:(TeraSort,Hadoop;)