mapreduce 全排序和二次排序及mapreduce过程讲解

mapreduce 全排序和二次排序

发现网上很多二次排序的例子没有实现全排序。参考了网上全排序和二次排序写了本篇的程序。
本篇也讲解了mapreduce的过程

版本:hadoop2.7.x

需求: 对如下的文本进行排序,文本已经放在了hdfs上

[root@XXX]# hdfs dfs -cat /XXX/input/sortdata
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/usr/lib/hadoop/lib/slf4j-log4j12-1.7.10.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/usr/lib/guardian-plugins/lib/slf4j-log4j12-1.7.7.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
19/01/28 16:52:14 INFO util.KerberosUtil: Using principal pattern: HTTP/_HOST
10 1
10 3
10 2
9 1
9 2
9 2
3 1
3 3
3 2
4 1
4 2
4 3
5 1
5 2
5 3
6 1
6 2
6 3
7 1
7 3
7 2
8 1
8 2
8 3
1 1
1 2
1 4
1 2
2 1
2 2
2 5
2 4
2 3

运行mr程序

hadoop jar mysort.jar p1.sort.SecondarySort /XXX/input /XXX/output

运行成功后hdfs上生成三个文本:part-r-00000,part-r-00001,part-r-00002,这是mr程序默认生成的文本名,从0开始,有几个reduce任务就生成几个文本。这里生成三个,是因为我在程序中设置3个reduce任务,具体看最后代码。

[root@XXX]# hdfs dfs -ls /XXX/output
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/usr/lib/hadoop/lib/slf4j-log4j12-1.7.10.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/usr/lib/guardian-plugins/lib/slf4j-log4j12-1.7.7.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
19/01/28 14:19:28 INFO util.KerberosUtil: Using principal pattern: HTTP/_HOST
Found 4 items
-rw-r--r--   3 XXX superuser          0 2019-01-28 14:10 /XXX/output/_SUCCESS
-rw-r--r--   3 XXX superuser         48 2019-01-28 14:10 /XXX/output/part-r-00000
-rw-r--r--   3 XXX superuser         36 2019-01-28 14:10 /XXX/output/part-r-00001
-rw-r--r--   3 XXX superuser         51 2019-01-28 14:10 /XXX/output/part-r-00002

查看这三个part文本,可看到实现了二次排序和全排序,文本内实现了二次排序,文本间实现了全排序。part-r-00000文本里面是最小的数,也就是文本编号越大,里面的数就越大。

[root@XXX]# hdfs dfs -cat /XXX/output/part-r-00000
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/usr/lib/hadoop/lib/slf4j-log4j12-1.7.10.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/usr/lib/guardian-plugins/lib/slf4j-log4j12-1.7.7.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
19/01/28 16:52:45 INFO util.KerberosUtil: Using principal pattern: HTTP/_HOST
1	1
1	2
1	2
1	4
2	1
2	2
2	3
2	4
2	5
3	1
3	2
3	3
[root@XXX]# hdfs dfs -cat /XXX/output/part-r-00001
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/usr/lib/hadoop/lib/slf4j-log4j12-1.7.10.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/usr/lib/guardian-plugins/lib/slf4j-log4j12-1.7.7.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
19/01/28 16:52:51 INFO util.KerberosUtil: Using principal pattern: HTTP/_HOST
4	1
4	2
4	3
5	1
5	2
5	3
6	1
6	2
6	3
[root@XXX]# hdfs dfs -cat /XXX/output/part-r-00002
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/usr/lib/hadoop/lib/slf4j-log4j12-1.7.10.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/usr/lib/guardian-plugins/lib/slf4j-log4j12-1.7.7.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
19/01/28 16:52:59 INFO util.KerberosUtil: Using principal pattern: HTTP/_HOST
7	1
7	2
7	3
8	1
8	2
8	3
9	1
9	2
9	2
10	1
10	2
10	3

程序

IntPair类

这个类的代码网上也有很多,我也是参考网上的。自定义类型需要继承WritableComparable接口,我们平时用的IntWritable,Text这些类也是继承WritableComparable这个接口的,有兴趣自己看一下源码。主要是要实现这个接口的比较大小的方法,用来比较对象的大小。
二次排序中,我们的数据格式是这样的{num1,num2},所以要定义一个这样的类,先比较num1大小,再比较num2的大小。要自定义接口中的compareTo方法.

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.WritableComparable;

public class IntPair implements WritableComparable{
    private int first;
    private int second;
    
    public IntPair(){
    }
    public IntPair(int left, int right){
        set(left, right);
    }
    public void set(int left, int right){
        first = left;
        second = right;
    }
    
    @Override
    public void readFields(DataInput in) throws IOException{
        first = in.readInt();
        second = in.readInt();
    }
    
    @Override
    public void write(DataOutput out) throws IOException{
        out.writeInt(first);
        out.writeInt(second);
    }
    
    @Override
    public int compareTo(IntPair o)
    {
        if (first != o.first){
            return first < o.first ? -1 : 1;
        }else if (second != o.second){
            return second < o.second ? -1 : 1;
        }else{
            return 0;
        }
    }
    
    @Override
    public int hashCode(){
        return first * 157 + second;
    }
    
    @Override
    public boolean equals(Object right){
        if (right == null)
            return false;
        if (this == right)
            return true;
        if (right instanceof IntPair){
            IntPair r = (IntPair) right;
            return r.first == first && r.second == second;
        }else{
            return false;
        }
    }
    
    public int getFirst(){
        return first;
    }
    
    public int getSecond(){
        return second;
    }
}

main主程序

一共要写map,partition,group,reduce这四处代码。
先介绍一下partition和group的作用。
1.partition:产生reduce编号,然后数据就能发送到对应编号的recduce任务中,编号从0开始。如果reduce的编号为N,结果就会生成part-r-0000N文本(上面生成了part-r-00000,part-r-00001,part-r-00002,说明有三个reduce,编号分别为0,1,2)。
partition默认是hash来生成编号,比如我们在程序中设置reduce任务数据为10, job.setNumReduceTasks(10),那么hash值就是0到9,partition方法计算某个数的hash值,然后发送到对应编号的reduce中。
默认的partition不满足我们需求,我们要实现全排序,也就是最小的一批数要发送到编号为0的reduce中,最大一批数发送到编号最大的reduce中。
我要排序的文本中第一列最大的数是10,最小的是1,所以我设置了3个reduce任务。把1,2,3发送到编号为0的reduce中;4,5,6发送到编号为1的reduce;7,8,9,10发送到编号为2的reduce中。
我们只需要继承Partitioner类,改写getPartition这个方法,这个方法要return一个整数,这个整数就是reduce的编号,看我程序中先获得最大最小值,然后三等分,比较大小后return 0,1,2这三个编号。

partiton中获取最大最小值也有些技巧,我这里是要事先知道最大最小值的,也就是我知道原文本中第一列最大的数是10,最小的是1,然后通过job的conf配置set最大最小两个参数,map和reduce任务中的context可以获取conf,然后获取最大最小值,这也是在mapreduce中共享公共参数的一种方法(利用configuration配置)。我改写了map类中的setup方法(这个方法大家一定要记住,文章最后会介绍),获取conf中的最大最小值,然后赋值给两个全局变量max和min,这样在partition中读取这两个全局变量就行了。
如果我们事先不能知道最大最小值,可以先写一个mapreduce任务求出最大最小值,reduce把最大最小值输出到hdfs上。

2.group:对分到同一reduce中的数据进行分组。
group有一个compare方法,相等的对象分到同一组。
先以wordcount程序为例说明一下默认的分组规则(还不懂wordcount过程的同学先网上看一下它的例子)。比如map方法输出5个单词{hello,1},{hi,1},{hello,1},{hi,1},{hi,1}。partiion中按默认的hash产生reduce编号,假设hello和hi的hash值一样,那么hello和hi分给了同一编号的reduce。reduce最终会接收到这样的数{hello,[1,1]},{hi,[1,1,1]},多个1合在了一起形成一个列表。{hello,[1,1]}就对应reduce方法中的key和values。
public void reduce(Text key, Iterable values, Context context),我们一般在reduce方法循环读取values中的值,是因为默认的group把key相同的value合并到了一起。
所以group的作用就是把两个{hello,1}分到同一组,合并到一起形成{hello,[1,1]},最后传给reduce。group默认key值一样就分到同一组,两个hello是一样的所以分到同一组

默认的group不满足我们要求,我们的key是自定义的IntPair,它的格式是(num1,num2),我们需要num1一样就分到同一组。比如map输出3个数据{(1,4),4},{(1,5),5},{(1,6),6},我们希望reduce得到{(1,4),[4,5,6]},也就是把num1相同的value合并在一起传给reduce。看我group代码中compare方法中只判断第一个数是否相等。

最后还有一个小问题,可能有人会问上面例子为什么reduce得到{(1,4),[4,5,6]},而不是{(1,5),[4,5,6]},{(1,6),[4,5,6]},[4,5,6]为什么是排好序的。或者问wordcount中的两个{hello,1},{hello,1},合并成{hello,[1,1]},合并后的hello是合并前的哪个hello,[1,1]这两个1分别对应合并前的哪个1?
其实很简单,就是group拿到的数是按key排好序的(这是mapreduce中默认会按key排序),所在group就把第一个key作为输出中的key,也就是(1,4),输出的[4,5,6]自然也是排好序的,是按我定义的IntPair类中的compareTo进行排序的,先比较num1,再比较num2。所以group拿到的数据是{(1,4),4},{(1,5),5},{(1,6),6}这样排好序的,然后按序合并后传给reduce

总结一下mapreduce的过程:map->combine->partitiion->sort->group->reduce
sort是在分区内根据key进行排序,排序方法是key中的compareTo方法,如果我们自定义key类型,一定要改写这个方法,所以group拿到的数是排好序的,最终传给reduce中的key也是排好序的,一般reduce输出的结果也是按key排好序的,但是reduce之间没有排好序,这也是全排序中要自定义partition,把最小的一批数据发送到编号为0的reduce,以此类推。

还有一个combine,combine的作用就是在map端对输出先做一次合并,以减少传输到reducer的数据量,试想一下wordcount例子,如果有3个map,每个map输出1万个(hello,1),最终reduce得到{hello,[1,1,1,1,1…1]},values有3万个1。有了combine可以先把map结果进行合并,比如把1万个(hello,1)合并成(hello,10000),最终reduce得到(hello,[10000,10000,10000]}
job.setCombinerClass(),这样设置combiner类,combine可以看成map本地的reduce,自定义combine类和自定义reduce一样继承Reducer类。wordcount中可以把combiner设置成reduce类,因为它们逻辑是一样的。
注意本篇的程序没有用到combine,下面是最终的主类

import java.io.IOException;
import java.util.StringTokenizer;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Partitioner;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.security.UserGroupInformation;


@SuppressWarnings("deprecation")
public class SecondarySort {
	public static int max=Integer.MIN_VALUE;
	public static int min=Integer.MAX_VALUE;

    public static class Map extends Mapper {
    

  	  public void setup(Context context)throws IOException, InterruptedException {
        	         Configuration conf = context.getConfiguration();
        	         max =Integer.parseInt(conf.get("max"));
        	         min =Integer.parseInt(conf.get("min"));

        }   
  	  
        public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            String line = value.toString();
            StringTokenizer tokenizer = new StringTokenizer(line);
            int left = 0;
            int right = 0;
            if (tokenizer.hasMoreTokens()) {
                left = Integer.parseInt(tokenizer.nextToken());
                if (tokenizer.hasMoreTokens()){
                    right = Integer.parseInt(tokenizer.nextToken());
                }

                context.write(new IntPair(left, right), new IntWritable(right));
            }
        }
        
        public void clean(Context context)
				throws IOException, InterruptedException {
			// TODO Auto-generated method stub			
		}
    }
    
    /*
     * 自定义分区函数类FirstPartitioner,根据 IntPair中的first实现分区
     */
    public static class FirstPartitioner extends Partitioner{  	
        @Override
        public int getPartition(IntPair key, IntWritable value,int numPartitions){
            int k =(max-min)/3;
            int one =min+k;
            int two=one+k;
            		
            int f=key.getFirst();
            if(f=one&&f {  
        public void reduce(IntPair key, Iterable values, Context context) throws IOException, InterruptedException {
            for (IntWritable val : values) {
                context.write(new Text(Integer.toString(key.getFirst())), val);
            }
        }
    }
    
    public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
        // 读取配置文件
        Configuration conf = new Configuration();
        conf.set("max", "10");
        conf.set("min", "1");
        
        //我的hadoop集群开了kerberos安全,需要进行认证
        System.setProperty("java.security.krb5.conf","path_to/krb5.conf");
        conf.set("hadoop.security.authentication", "kerberos");
        conf.set("dfs.namenode.kerberos.principal", "hdfs/XXX@XXXX");
        UserGroupInformation.setConfiguration(conf);
        UserGroupInformation.loginUserFromKeytab("XXX@XXXX", "path_to/XXX.keytab");
                      
        Job job = new Job(conf, "secondarysort");
        // 设置主类    
        job.setJarByClass(SecondarySort.class);
             
        // 输入路径
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        // 输出路径
        FileOutputFormat.setOutputPath(job, new Path(args[1]));
        
        // Mapper
        job.setMapperClass(Map.class);
        // Reducer
        job.setReducerClass(Reduce.class);
        
        // 分区函数
        job.setPartitionerClass(FirstPartitioner.class);
        
        // 分组函数
        job.setGroupingComparatorClass(GroupingComparator.class);
        
        //设置reduce个数,因为parttitoin中return 0,1,2三个数字,所以如果reduce数设置大于3,多出的reduce会在hdfs上生成空文本
        //比如设置为4,最终part-r-00003文本是空的,part-r-00000,1,2中有数据。如果设置小于3,运行程序会报错。
        job.setNumReduceTasks(3);
        // map输出key类型
        job.setMapOutputKeyClass(IntPair.class);
        // map输出value类型
        job.setMapOutputValueClass(IntWritable.class);
        
        // reduce输出key类型
        job.setOutputKeyClass(Text.class);
        // reduce输出value类型
        job.setOutputValueClass(IntWritable.class);
        
        // 输入格式
        job.setInputFormatClass(TextInputFormat.class);
        // 输出格式
        job.setOutputFormatClass(TextOutputFormat.class);
        
        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }
}

setup()和clean()
我们一般会改写map和reduce方法,但是Mapper类和Reducer类中还有setup,clean方法,这两个方法只在map/reduce执行前后调用一次,也就是map端执行过程是这样的:开始执行setup()一次->循环执行map()多次->最后执行一次clean();reduce端执行过程是这样的:开始执行setup()一次->循环执行reduce()多次->最后执行一次clean()。
从setup和clean的名字就知道它们作用,setup一般用来初始化一些参数(或者叫作资源),clean清除这些参数或资源。setup/clean方法和map/reduce方法一样,都有context对象,它们都可以调用context.write()。
举例说明一个优化wordcount的map,和上面说的combine作用类似,在map端先对结果合并。
先定义一个全局变量hashmap,把每个单词和计数先写入这个hashmap,最后在clean方法中调用context.write()把hashmap结果输出,注意map方法中没有调用context.write()。

public static class Map extends Mapper {
		
		  private String word = null;
	      public HashMap hashmap=new HashMap<>();
	      
		  public void setup(Context context)throws IOException, InterruptedException {
	      	         Configuration conf = context.getConfiguration();
	      	         //初始化 hashmap

	      }   
		  
	      public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
	          
	          String line = value.toString();  //每行句子
	          StringTokenizer tokenizer = new StringTokenizer(line);
	                      
	           while (tokenizer.hasMoreTokens()) {       	   
	                word=tokenizer.nextToken();
	                if(hashmap.containsKey(word)){
	                	int v=hashmap.get(word);
	                	hashmap.put(word, v+1);
	                }else{
	                	hashmap.put(word,1);	                	
	                }
	                 
	            }
	      }
	      
	      public void clean(Context context)throws IOException, InterruptedException {

	    	  for(String key:hashmap.keySet())
	          {
	    		  int v =hashmap.get(key);
	           context.write(new Text(key), new IntWritable(v));
	          }
				//清除hashmap
			}
	  }

你可能感兴趣的:(mapreduce 全排序和二次排序及mapreduce过程讲解)