MapReduce高級應用——全排序和二次排序

前言

儘管現在MapReduce程序在日常開發中已經代碼編寫已經很少了,但作爲大數據Hadoop的三大板塊之一,他內在的許多思想也是很多後續框架的基礎鋪墊。本篇博客,南國重點回顧一下MR中的排序相關知識點。網上關於這個知識點 可能已經有很多的知識介紹,本來不打算寫這篇博客。最近一段時間終於抽空看了Hadoop權威指南的大部分內容。於是,本篇博客 南國試着從面試回顧的角度去編寫這篇博客。
話不多說,乾貨送上~

排序

在默認情況下,MapReduce根據輸入記錄的鍵對數據集進行排序。
在這裏插入圖片描述
但一些時候,我們需要根據實際的應用場景,對數據進行一些更爲複雜的排序。
例如:全排序和輔助排序(也成爲二次排序)。

全排序

讓MapReduce產生一個全局排序的文件:

  1. 最簡單的方法是隻使用一個分區(partition),這種在處理小規模文件時還行。但是在處理大型文件是效率極低,所有的數據都發送到一個Reduce進行排序,這樣不能充分利用集羣的計算資源,而且在數據量很大的情況下,很有可能會出現OOM問題。

  2. 首先創建一系列排好序的文件,其次串聯這些文件,最後生成一個全局排序的文件。它主要的思路使用一個partitioner來描述輸出的全局排序。該方案的重點在於分區方法,默認情況下根據hash值進行分區(默認的分區函數是HashPartitioner,其實現的原理是計算map輸出key的 hashCode ,然後對Reduce個數 求餘,餘數相同的 key 都會發送到同一個Reduce);還可以根據用戶自定義partitioner(自定義一個類並且繼承partitioner類,重寫器getpartition方法)
    這裏我舉個簡單例子:

//Partition做分區  
		public static class Partition extends Partitioner<Text,LongWritable> {

		@Override
		public int getPartition(Text key, LongWritable value, int num) {
			// TODO Auto-generated method stub
			if(key.toString().equals("apple")){
				return 0;
			}
			if(key.toString().equals("xiaomi")){
				return 1;
			}
			if(key.toString().equals("huawei")){
				return 2;
			}
			return 3;
		}					
	}
class GlobalSortPartitioner  extends Partitioner<Text,LongWritable> implements Configurable {
    private Configuration configuration = null;
    private int indexRange = 0;

    public int getPartition(Text text, LongWritable longWritable, int numPartitions) {
        //假如取值範圍等於26的話,那麼就意味着只需要根據第一個字母來劃分索引
        int index = 0;
        if(indexRange==26){
            index = text.toString().toCharArray()[0]-'a';
        }else if(indexRange == 26*26 ){
            //這裏就是需要根據前兩個字母進行劃分索引了
            char[] chars = text.toString().toCharArray();
            if (chars.length==1){
                index = (chars[0]-'a')*26;
            }
            index = (chars[0]-'a')*26+(chars[1]-'a');
        }
        int perReducerCount = indexRange/numPartitions;
        if(indexRange<numPartitions){
            return numPartitions;
        }

        for(int i = 0;i<numPartitions;i++){
            int min = i*perReducerCount;
            int max = (i+1)*perReducerCount-1;
            if(index>=min && index<=max){
                return i;
            }
        }
        //這裏我們採用的是第一種不太科學的方法
        return numPartitions-1;

    }

    public void setConf(Configuration conf) {
        this.configuration = conf;
        indexRange = configuration.getInt("key.indexRange",26*26);
    }

    public Configuration getConf() {
        return configuration;
    }
}
  1. 使用TotalOrderPartitioner進行全排序
    Hadoop 內置還有個名爲 TotalOrderPartitioner 的分區實現類,它解決全排序的問題。其主要做的事 實際上和 上面介紹的第二種分區實現類很類似,也就是根據Key的分界點將不同的Key發送到相應的分區。例如,下面的demo中使用了:
    //設置分區文件, TotalOrderPartitioner必須指定分區文件
    Path partitionFile = new Path( “_partitions”);
    TotalOrderPartitioner.setPartitionFile(conf, partitionFile);
public class TotalSort {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {

        //access hdfs's user
        System.setProperty("HADOOP_USER_NAME","root");

        Configuration conf = new Configuration();
        conf.set("mapred.jar", "D:\\MyDemo\\MapReduce\\Sort\\out\\artifacts\\TotalSort\\TotalSort.jar");

        FileSystem fs = FileSystem.get(conf);

        /*RandomSampler 參數說明
        * @param freq Probability with which a key will be chosen.
        * @param numSamples Total number of samples to obtain from all selected splits.
        * @param maxSplitsSampled The maximum number of splits to examine.
        */
        InputSampler.RandomSampler<Text, Text> sampler = new InputSampler.RandomSampler<>(0.1, 10, 10);

        //設置分區文件, TotalOrderPartitioner必須指定分區文件
        Path partitionFile = new Path( "_partitions");
        TotalOrderPartitioner.setPartitionFile(conf, partitionFile);

        Job job = Job.getInstance(conf);
        job.setJarByClass(TotalSort.class);
        job.setInputFormatClass(KeyValueTextInputFormat.class); //數據文件默認以\t分割
        job.setMapperClass(Mapper.class);
        job.setReducerClass(Reducer.class);
        job.setNumReduceTasks(4);  //設置reduce任務個數,分區文件以reduce個數爲基準,拆分成n段

        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(Text.class);

        job.setPartitionerClass(TotalOrderPartitioner.class);

        FileInputFormat.addInputPath(job, new Path("/test/sort"));

        Path path = new Path("/test/wc/output");

        if(fs.exists(path))//如果目錄存在,則刪除目錄
        {
            fs.delete(path,true);
        }
        FileOutputFormat.setOutputPath(job, path);

        //將隨機抽樣數據寫入分區文件
        InputSampler.writePartitionFile(job, sampler);

        boolean b = job.waitForCompletion(true);
        if(b)
        {
            System.out.println("OK");
        }

    }
}

輔助排序(二次排序)

二次排序在Hadoop面試 特別是MapReduce中 高頻的面試題目了。當數據本身是具有兩個維度的,我們對Key排序的同時還需要對Value進行排序。

二次排序的原理

  1. Map起始階段
    在Map階段,使用job.setInputFormatClass()定義的InputFormat,將輸入的數據集分割成小數據塊split,同時InputFormat提供一個RecordReader的實現。在這裏我們使用的是TextInputFormat,該行在整個作業中的字節偏移量作爲Key,這一行的文本作爲Value。這就是自定 Mapper的輸入是<LongWritable,Text> 的原因。然後調用自定義Mapper的map方法,將一個個<LongWritable,Text>鍵值對輸入給Mapper的map方法
    注意:很多都將行號作爲key,實際上這是不準確的。在《Hadoop權威指南》中提到
    在這裏插入圖片描述在這裏插入圖片描述

  2. Map最後階段
    在Map階段的最後,會先調用job.setPartitionerClass()對這個Mapper的輸出結果進行分區,每個分區映射到一個Reducer。每個分區內又調用job.setSortComparatorClass()設置的Key比較函數類排序。可以看到,這本身就是一個二次排序。如果沒有通過job.setSortComparatorClass()設置 Key比較函數類,則使用Key實現的compareTo()方法

  3. Reduce階段
    在Reduce階段,reduce()方法接受所有映射到這個Reduce的map輸出後,也會調用job.setSortComparatorClass()方法設置的Key比較函數類,對所有數據進行排序。然後開始構造一個Key對應的Value迭代器。這時就要用到分組使用 job.setGroupingComparatorClass()方法設置分組函數類。只要這個比較器比較的兩個Key相同,它們就屬於同一組,它們的 Value放在一個Value迭代器,而這個迭代器的Key使用屬於同一個組的所有Key的第一個Key。最後就是進入Reducer的 reduce()方法,reduce()方法的輸入是所有的Key和它的Value迭代器,同樣注意輸入與輸出的類型必須與自定義的Reducer中聲明的一致

二次排序的實現流程:

  1. 自定義key,假設數據集是整形類型的二維數據。這裏我們構建IntPair類型表示組合key。
package com.xjh.sort_twice;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

import org.apache.hadoop.io.WritableComparable;
/**
 * 自定義key排序
 * 在mr中,所有的key是需要被比較和排序的,並且是二次,先根據partitioner,再根據大小。而本例中也是要比較兩次。
 * 先按照第一字段排序,然後再對第一字段相同的按照第二字段排序。
 * 根據這一點,我們可以構造一個複合類IntPair,他有兩個字段,先利用分區對第一字段排序,再利用分區內的比較對第二字段排序
 * @author xjh
 *
 */
//自己定義的InPair類,實現WritableComparator
public class IntPair implements WritableComparable<IntPair>{
	int left;
	int right;
	
	public void set(int left, int right) {
		// TODO Auto-generated method stub
		this.left = left;
		this.right = right;
	}
	public int getLeft() {
		return left;
	}

	public int getRight() {
		return right;
	}
	
	//反序列化,從流中讀進二進制轉換成IntPair
	@Override
	public void readFields(DataInput in) throws IOException {
		// TODO Auto-generated method stub
		this.left = in.readInt();
		this.right = in.readInt();
	}
	//序列化,將IntPair轉換成二進制輸出
	@Override
	public void write(DataOutput out) throws IOException {
		// TODO Auto-generated method stub
		out.writeInt(left);
		out.writeInt(right);
	}
	
	/*
	 * 爲什麼要重寫equal方法?
	 * 因爲Object的equal方法默認是兩個對象的引用的比較,意思就是指向同一內存,地址則相等,否則不相等;
	 * 如果你現在需要利用對象裏面的值來判斷是否相等,則重載equal方法。
	 */
	@Override
	public boolean equals(Object obj) {
		// TODO Auto-generated method stub
		if(obj == null)
			return false;
		if(this == obj)
			return true;
		if (obj instanceof IntPair){
			IntPair r = (IntPair) obj;
			return r.left == left && r.right==right;
		}
		else{
			return false;
		}
			
	}
	
	/*
	 * 重寫equal 的同時爲什麼必須重寫hashcode? 
	 * hashCode是編譯器爲不同對象產生的不同整數,根據equal方法的定義:如果兩個對象是相等(equal)的,那麼兩個對象調用 hashCode必須產生相同的整數結果,
	 * 即:equal爲true,hashCode必須爲true,equal爲false,hashCode也必須 爲false,所以必須重寫hashCode來保證與equal同步。 
	 */
	@Override
	public int hashCode() {
		// TODO Auto-generated method stub
		return left*157 +right;
	}
	
	//實現key的比較
	@Override
	public int compareTo(IntPair o) {
		// TODO Auto-generated method stub
		if(left != o.left)
			return left<o.left? -1:1;
		else if (right != o.right)
			return right<o.right? -1:1;
		else
			return 0;
	}	
}
  1. 自定義分區。自定義分區函數類FirstPartitioner,是對組合key的第一次比較,完成對所有key的排序。
public static class MyPartitioner extends Partitioner<IntPair, IntWritable>{
		@Override
		public int getPartition(IntPair key, IntWritable value, int numOfReduce) {
			// TODO Auto-generated method stub
			return Math.abs(key.getLeft()*127) % numOfReduce;
		}
	}

並在main()函數中Job指定:job.setPartitionerClass(MyPartitioner.class);

  1. 自定義 SortComparator 實現 IntPair 類中的first和second排序。這裏我們在IntPair類已經實現了compareTo()方法實現。
  2. 自定義 GroupingComparator 類,實現分區內的數據分組。
/**
	 * 在分組比較的時候,只比較原來的key,而不是組合key。
	 */
	public static class MyGroupParator implements RawComparator<IntPair>{

		@Override
		public int compare(IntPair o1 , IntPair o2) {
			// TODO Auto-generated method stub
			int l = o1.getLeft();
			int r = o2.getRight();
			return l == r ? 0:(l<r ?-1:1);
		}
		//一個字節一個字節的比,直到找到一個不相同的字節,然後比這個字節的大小作爲兩個字節流的大小比較結果。
		@Override
		public int compare(byte[] b1, int l1, int r1, byte[] b2,int l2, int r2) {
			// TODO Auto-generated method stub
            return WritableComparator.compareBytes(b1, l1, Integer.SIZE/8, b2, l2, Integer.SIZE/8);
		}
		
	}

參考資料:
1.MapReduce二次排序

發佈了65 篇原創文章 · 獲贊 20 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章