Hadoop之MapReduce

102 阅读26分钟

1. MapReduce概述

1.1 定义

MapReduce是一个分布式运算的变成框架,核心功能是将用户编写的业务逻辑代码和自带的默认组件整合成一个完整的分布式运算框架

1.2优缺点

优点:
    1.易于编程,只需要简单的实现一些接口就可以完成一个分布式程序
    2.良好的扩展性
    3.高容错性
    4.适合PB级以上的海量数据的离线处理
缺点:
    1.不擅长实时计算
    2.不擅长流式计算
    3.不擅长DAG(有向无环图)计算

1.3核心变成思想

分而治之,先分再和
1.分布式运行计算往往需要分成至少两个阶段
2.第一个阶段的MapTask并发实例,完全并行运行,互不相干
3.第二个阶段的ReduceTask并发实例互不相干,但它们的数据依赖于上一个阶段的所有MapReduce并发实例的输出
4.MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果业务逻辑复杂,只能多个MapReduce程序串行运行

1.4MapReduce进程

1.MrAppMaster: 负责整个程序的过程调度级状态协调
2.MapTask: 负责Map阶段的整个数据处理流程
3.ReduceTask: 负责Reduce阶段的整个数据处理流程

1.5 常用序列化类型

java类型Hadoop类型
BooleanBooleanWritable
ByteByteWritable
intIntWritable
FloatFloatWritable
LongLongWritable
DoubleDoubleWritable
StringText
MapMapWritable
ArrayArrayWritable
NullNullWritable

1.6 MapReduce编程规范

1. Mapper阶段
    用户自定义的Mapper要继承自己的父类
    Mapper的输入数据是KV对的形式
    Mapper中的业务逻辑卸载map()方法中
    Mapper的输出数据是KV对的形式
    map()方法(MapTask进程)对每一个<K,V>调用一次
2. Reduce阶段
    用户自定义的Reducer要继承自己的父类
    Reducer的输入类型要对应Mapper的输出类型
    Reducer中的业务逻辑卸载reduce()方法中
    ReduceTask进程对每一组相同<K,V>调用一次reduce()方法
3. Driver阶段
    相当于YARN集群的客户端,用于提交程序到YARN集群,提交的是封装了Map Reduce程序相关运行参数的job对象

1.7 WordCount案例

1.需求:在给定的文本文件中输出每一个单词出现的总次数

2.Map端

2.1 分析

 1. 将文本转换为String
 2. 根据空格切分单词
 3. 将单词输出为<单词,1>

2.2 代码

    public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
        Text k = new Text();
        IntWritable v = new IntWritable(1);
        @Override
        protected void map(LongWritable key, Text value, Context context)    throws IOException, InterruptedException {
            // 1 获取一行
            String line = value.toString();
            // 2 切割
            String[] words = line.split(" ");
            // 3 输出
            for (String word : words) {
                k.set(word);
                context.write(k, v);
            }
        }
    }

3.Reduce端

3.1 分析

 1.汇总各个key的个数
 2.输出该key的总次数

3.2 代码

public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
    int sum;
    IntWritable v = new IntWritable();
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {
        // 1 累加求和
        sum = 0;
        for (IntWritable count : values) {
            sum += count.get();
        }
        // 2 输出
         v.set(sum);
        context.write(key,v);
     }
}

4.Driver端

 public class WordCountDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
      // 1 获取配置信息以及获取job对象
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
        // 2 关联本Driver程序的jar
        job.setJarByClass(WordCountDriver.class);
        // 3 关联Mapper和Reducer的jar
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);
        // 4 设置Mapper输出的kv类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);
        // 5 设置最终输出kv类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);
        // 6 设置输入和输出路径
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));
        // 7 提交job
        boolean result = job.waitForCompletion(true);
        System.exit(result ? 0 : 1);
    }
}

2. Hadoop序列化

2.1 序列化概述

1. 什么是序列化
    序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输。 
    反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象。

2. 为什么要序列化
    一般来说,“活的”对象只生存在内存里,关机断电就没有了。而且“活的”对象只能由本地的进程使用,不能被发送到网络上的另外一台计算机。 然而序列化可以存储“活的”对象,可以将“活的”对象发送到远程计算机。

3.Hadoop序列化特点:
    1.紧凑:高效使用存储空间。
    2.快速:读写数据的额外开销小。
    3.互操作:支持多语言的交互

2.2 自定义bean对象实现序列化接口(Writable)

1.必须实现Writable接口
2.反序列化时,需要反射调用空参构造函数,所以必须有空参构造
3.重写序列化方法
    @Override
    public void write(DataOutput out) throws IOException {
    }

4.重写反序列化方法
    @Override
    public void readFields(DataInput in) throws IOException {
    }

5.注意反序列化的顺序和序列化的顺序完全一致

6.要想把结果显示在文件中,需要重写toString(),可用"\t"分开,方便后续用。

7.如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口,因为MapReduce框中的Shuffle过程要求对key必须能排序。详见后面排序案例。
    @Override
    public int compareTo(FlowBean o) {
        // 倒序排列,从大到小
        return this.sumFlow > o.getSumFlow() ? -1 : 1;
    }

2.3 案例

1.需求

1. 统计每一个手机号耗费的总上行流量、总下行流量、总流量
2. 输入格式:
    7  13560436666 120.196.100.99    1116           954      200
    id 手机号码    网络ip           上行流量       下行流量  网络状态码
3. 输出格式:
    13560436666 1116      954        2070
    手机号码     上行流量  下行流量   总流量
    

2. 需求分析

2.1 Map阶段

  1. 读取一行数据,切分数据
       7  13560436666 120.196.100.99  1116   954   200
       id 手机号码    网络ip           上行流量       下行流量  网络状态码
  2. 抽取手机号、上行流量、下行流量
      13560436666   1116   954
      手机号码     上行流量  下行流量
  3. 以手机号为key,bean对象为value输出即 context.write(手机号,bean)

2.2 Reduce阶段

  1. 累加上行流量和下行流量的到总流量

2.3 代码

2.3.1 编写流量统计的Bean对象

//1 继承Writable接口
public class FlowBean implements Writable {
    private long upFlow; //上行流量
    private long downFlow; //下行流量
    private long sumFlow; //总流量
    //2 提供无参构造
    public FlowBean() {
    }
    //3 提供三个参数的getter和setter方法
    public long getUpFlow() {
        return upFlow;
    }
    public void setUpFlow(long upFlow) {
        this.upFlow = upFlow;
    }
    public long getDownFlow() {
            return downFlow;
    }
    public void setDownFlow(long downFlow) {
            this.downFlow = downFlow;
    }
    public long getSumFlow() {
            return sumFlow;
    }
    public void setSumFlow(long sumFlow) {
            this.sumFlow = sumFlow;
    }
    public void setSumFlow() {
            this.sumFlow = this.upFlow + this.downFlow;
    }
    //4 实现序列化和反序列化方法,注意顺序一定要保持一致
    @Override
    public void write(DataOutput dataOutput) throws IOException {
            dataOutput.writeLong(upFlow);
            dataOutput.writeLong(downFlow);
            dataOutput.writeLong(sumFlow);
    }
    @Override
    public void readFields(DataInput dataInput) throws IOException {
            this.upFlow = dataInput.readLong();
            this.downFlow = dataInput.readLong();
            this.sumFlow = dataInput.readLong();
    }
    //5 重写ToString
    @Override
    public String toString() {
        return upFlow + "\t" + downFlow + "\t" + sumFlow;
    }
  }

2.3.2 编写Mapper类

public class FlowMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
    private Text outK = new Text();
    private FlowBean outV = new FlowBean();
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
    //1 获取一行数据,转成字符串
        String line = value.toString();
        //2 切割数据
        String[] split = line.split("\t");
        //3 抓取我们需要的数据:手机号,上行流量,下行流量
        String phone = split[1];
        String up = split[split.length - 3];
        String down = split[split.length - 2];
        //4 封装outK outV
        outK.set(phone);
        outK.set(phone);
        outV.setUpFlow(Long.parseLong(up));
        outV.setDownFlow(Long.parseLong(down));
        outV.setSumFlow();
        //5 写出outK outV
        context.write(outK, outV);
    }
}

2.3.3 编写Reducer类

public class FlowReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
        private FlowBean outV = new FlowBean();
        @Override
        protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
            long totalUp = 0;
            long totalDown = 0;
            //1 遍历values,将其中的上行流量,下行流量分别累加
            for (FlowBean flowBean : values) {
                totalUp += flowBean.getUpFlow();
                totalDown += flowBean.getDownFlow();
            }
            //2 封装outKV
            outV.setUpFlow(totalUp);
            outV.setDownFlow(totalDown);
            outV.setSumFlow();
            //3 写出outK outV
            context.write(key,outV);
        }
 }

2.3.4 编写Driver驱动类

public class FlowDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        //1 获取job对象
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
        //2 关联本Driver类
        job.setJarByClass(FlowDriver.class);
        //3 关联Mapper和Reducer
        job.setMapperClass(FlowMapper.class);
        job.setReducerClass(FlowReducer.class);
        //4 设置Map端输出KV类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(FlowBean.class);
        //5 设置程序最终输出的KV类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(FlowBean.class);
        //6 设置程序的输入输出路径
        FileInputFormat.setInputPaths(job, new Path("D:\\inputflow"));
        FileOutputFormat.setOutputPath(job, new Path("D:\\flowoutput"));
        //7 提交Job
        boolean b = job.waitForCompletion(true);
        System.exit(b ? 0 : 1);
    }
  }

3. MapReduce 框架原理

3.1 InputFormat数据输入

3.1.1 切片与MapTask并行度决定机制

1.MapTask并行度决定机制
   数据块:BlockHDFS物理上把数据分成一块一块,数据块是HDFS的存储数据单位
   数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。数据切片是MapReduce程序计算输入数据的单位,一个切片会对应启动一个MapTask。
   一个JobMap阶段有客户端在提交job时的切片数决定
   每一个Split切片分配一个MapTask进行并行实例处理
   默认情况下切片大小=BlockSize
   切片时不考虑数据集整体而是逐个对每一个文件单独切片

MapTask并行度决定机制.png

3.1.2 FileInputFormat切片机制

FileInputFormat接口的常见实现类:TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定义InputFormat等
流程:
    1. 程序先找到数据的存储目录
    2. 开始遍历处理(规划处理)目录下的每一个文件
    3. 遍历第一个文件
        a. 获取文件大小
        b. 计算切片大小 computeSplitSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M
        c. 默认情况下,切片大小等于blocksize
        d. 开始切片,形成第一个切片0-128M 第二个切片 128-256M ......
           (每次切片时都要判断切完剩下的部分是否大于块的1.1倍,不大于就划分为一块切片)
        e. 将切片信息写到一个切片规划文件中
        f. 整个切片的核心过程在getSplit方法中完成
    4. 提交切片规划文件到yarn上,yarn上的MrAppMaster根据切片规划文件计算开启MapTask个数
切片机制:
    1. 简单地按照文件的内容长度进行切片
    2. 切片大小默认等于Block大小
    3. 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
切片大小的参数配置
    1. 源码中计算切片大小的公式
        Math.max(minSize,Math.min(maxSize,blocksize))
        mapreduce.input.fileinputformat.split.minsize=1 //默认值为1
        mapreduce.input.fileinputformat.split.maxsize=LongMAXValue //默认值LongMAXValue
        因此默认情况下切片大小=blocksize
    2. 切片大小设置
        maxsize(切片最大值): 参数如果调的比blocksize小,则会让切片变小,而且就等于配置的这个参数值
        minsize(切片最小值): 参数调的比blocksize大则会让切片变得比blockSize大
    3. 获取切片信息API
        //获取切片的文件名称
        String name=inputSplit.getPath().getName();
        //根据文件类型获取切片信息
        FileSplit inputSplit=(FileSplit) context.getInputSplit();

3.1.3 TextInputFormat

   TextInputFormat是默认的FileInputFormat实现类,按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量, LongWritable类型。值是这行的内容,不包括任何行终止符(换行符和回车符),Text类型      

3.1.4 CombineTextInputFormat切片机制

  1. 应用场景
     CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑规划上规划到一个切片中,这样多个小文件就可以MapTask处理
  2. 虚拟存储切片最大值设置
     CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
     注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。
 3. 切片机制
    生成切片过程包括:虚拟存储过程和切片过程两个部分      
   

CombineTextInputFormat切片机制.png

1)虚拟存储过程:
        将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现太小切片)。
        例如setMaxInputSplitSize值为4M,输入文件大小为8.02M,则先逻辑上分一个4M。剩余的大小为4.02M,如果按照4M逻辑划分,就会出现0.02M的小的虚拟存储文件,所以将剩余的4.02M文件切分成(2.01M和2.01M)两个文件。
(2)切片过程:
    (a)判断虚拟存储的文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片。
    (b)如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
    (c)测试举例:有4个小文件大小分别为1.7M、5.1M、3.4M以及6.8M这四个小文件,则虚拟存储之后形成6个文件块,大小分别为:
    1.7M,(2.55M、2.55M),3.4M以及(3.4M、3.4M)
    最终会形成3个切片,大小分别为:
    (1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M 

3.2 MapReduce工作流程

3.2.1.工作流程:

MapReduce工作流程.png

3.3 Shuffle机制

3.3.1 Shuffle机制流程

shuffle机制.png

流程:
    (1)MapTask收集我们的map()方法输出的kv对,放到内存缓冲区中
    (2)从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
    (3)多个溢出文件会被合并成大的溢出文件
    (4)在溢出过程及合并的过程中,都要调用Partitioner进行分区和针对key进行排序
    (5)ReduceTask根据自己的分区号,去各个MapTask机器上取相应的结果分区数据
    (6)ReduceTask会抓取到同一个分区的来自不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)
    (7)合并成大文件后,Shuffle的过程也就结束了,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对Group,调用用户自定义的reduce()方法)
注意:
    (1)Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。
    (2)缓冲区的大小可以通过参数调整,参数:mapreduce.task.io.sort.mb默认100M。

3.3.2 Partition分区

  1. 默认Partition分区
      public class HashPartitioner<K,V> extends Partitioner<K,V>{
          public int getPartition(K key,V value, int numReduceTasks){
              return (key.hashCode() & Integer.MAX_VALUE)% numReduceTasks;
          }
      }
   默认分区是根据key的HashCode对ReduceTask个数取模得到的,用户没法控制哪个key存储到哪个分区
2.自定义Partitioner步骤
  (1). 自定义类继承Partitioner重写getPartition()方法
  (2). 在Job驱动中,设置自定义Partitioner
       job.setPartitionerClass(xxx.class)
  (3). 自定义Partition后要格局自定义Partitioner的逻辑设置相应数量的ReduceTask
       job.setNumReduceTask();
3. 分区总结
    (1) 如果ReduceTask的数量大于getPartition的结果数,则会多产生几个空的输出文件
    (2) 如果1<ReduceTask的数量<getPartition的结果数,则有一部分数据无处安放,会Exception
    (3) 如果ReduceTask的数量等于1,则不管MapTask端输出多少分区文件,最终结果都会交给这一个ReduceTask,只会产生一个结果文件
4. 案例
    4.1 需求:
        将统计结果按照手机归属地输出到不同省份
    4.2 代码
        自定义分区类实现Partitioner
        public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
            @Override
            public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
                //获取手机号前三位prePhone
                String phone = text.toString();
                String prePhone = phone.substring(0, 3);
                //定义一个分区号变量partition,根据prePhone设置分区号
                int partition;
                if("136".equals(prePhone)){
                    partition = 0;
                }else if("137".equals(prePhone)){
                    partition = 1;
                }else if("138".equals(prePhone)){
                    partition = 2;
                }else if("139".equals(prePhone)){
                    partition = 3;
                }else {
                    partition = 4;
                }
                //最后返回分区号partition
                return partition;
            }
    }
    
   驱动函数中增加自定义分区设置和ReduceTask设置
   public class FlowDriver {
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
            //1 获取job对象
            Configuration conf = new Configuration();
            Job job = Job.getInstance(conf);
            //2 关联本Driver
            job.setJarByClass(FlowDriver.class);
            //3 关联Mapper和Reducer
            job.setMapperClass(FlowMapper.class);
            job.setReducerClass(FlowReducer.class);
            //4 设置Map端输出数据的KV类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(FlowBean.class);
            //5 设置程序最终输出的KV类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(FlowBean.class);
            //8 指定自定义分区器            job.setPartitionerClass(ProvincePartitioner.class);
            //9 同时指定相应数量的ReduceTask
            job.setNumReduceTasks(5);
            //6 设置输入输出路径
            FileInputFormat.setInputPaths(job, new Path("D:\\inputflow"));
            FileOutputFormat.setOutputPath(job, new Path("D\\partitionout"));
            //7 提交Job
            boolean b = job.waitForCompletion(true);
            System.exit(b ? 0 : 1);
        }
}

流量统计Bean序列化对象
//1 继承Writable接口
public class FlowBean implements Writable { 
    //上行流量
    private long upFlow;
     //下行流量 
    private long downFlow;
   //总流量 
    private long sumFlow;
    //2 提供无参构造
    public FlowBean() { } 
    //3 提供三个参数的getter和setter方法
    public long getUpFlow() { 
        return upFlow;
    } 
    public void setUpFlow(long upFlow) {
        this.upFlow = upFlow; 
    } 
    public long getDownFlow() { 
        return downFlow; 
    }
    public void setDownFlow(long downFlow) {
        this.downFlow = downFlow;
   }
   public long getSumFlow() { 
       return sumFlow; 
   }
   public void setSumFlow(long sumFlow) {
       this.sumFlow = sumFlow; 
   } 
   public void setSumFlow() {
       this.sumFlow = this.upFlow + this.downFlow;
   }
   //4 实现序列化和反序列化方法,注意顺序一定要保持一致 
   @Override
   public void write(DataOutput dataOutput) throws IOException {
            dataOutput.writeLong(upFlow);
            dataOutput.writeLong(downFlow);
            dataOutput.writeLong(sumFlow);
    }
  @Override
   public void readFields(DataInput dataInput) throws IOException {
      this.upFlow = dataInput.readLong(); 
      this.downFlow = dataInput.readLong(); 
      this.sumFlow = dataInput.readLong();
  }
  //5 重写ToString
  @Override 
  public String toString() { 
      return upFlow + "\t" + downFlow + "\t" + sumFlow;
  }
}

Mapper类
public class FlowMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
    private Text outK = new Text();
    private FlowBean outV = new FlowBean();
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        //1 获取一行数据,转成字符串
        String line = value.toString();
        //2 切割数据
        String[] split = line.split("\t");
        //3 抓取我们需要的数据:手机号,上行流量,下行流量
        String phone = split[1];
        String up = split[split.length - 3];
        String down = split[split.length - 2];
        //4 封装outK outV outK.set(phone);
        outK.set(phone);
        outV.setUpFlow(Long.parseLong(up));
        outV.setDownFlow(Long.parseLong(down));
        outV.setSumFlow();
        //5 写出outK outV
        context.write(outK, outV);
     }
   }
   
Reucer类
public class FlowReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
    private FlowBean outV = new FlowBean();
    @Override
    protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException{
        long totalUp = 0;
        long totalDown = 0;
        //1 遍历values,将其中的上行流量,下行流量分别累加
        for (FlowBean flowBean : values) {
            totalUp += flowBean.getUpFlow();
            totalDown += flowBean.getDownFlow();
        }
        //2 封装outKV
        outV.setUpFlow(totalUp);
        outV.setDownFlow(totalDown);
        outV.setSumFlow();
        //3 写出outK outV
        context.write(key,outV);
    }
 }

3.3.3 WritableComparable排序

1. 排序概述
     MapTask和ReduceTask均会对数据按照key排序,默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。
     对于MapTask,它会将处理结果暂时放在环形缓冲区,当环形缓冲区的使用率达到一定阈值后在对环形缓冲区的数据进行一次快速排序,并将这些数据溢写到磁盘上,并对其进行归并排序
     对于ReduceTask,它从每个MapTask远程拷贝相应的数据文件,如果文件超过一定阈值则溢写到磁盘上,否则存在内存中,如果磁盘上的文件数目达到一定阈值则进行一次归并排序生成一个更大的文件,如果内存中的文件大小或数目超贵一定阈值则进行一次合并后溢写到磁盘上,当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的数据进行一次归并排序
2. 排序分类
    (1) 部分排序:MapReduce根据输入的键对数据集排序,保证输出的每个文件内部有序
    (2) 全排序: 最终输出结果只有一个文件,且文件内部有序,实现方式是只设置一个ReduceTask
    (3) 辅助排序: (GroupingComparator分组)
        在Reduce端对key进行分组,应用于在接收的key为bean对象时想让一个或几个字段相同的key进入同一个reduce方法
    (4) 二次排序:在自定义排序过程中如果compareTo中的判断条件为两个即为二次排序
3. 全排序案例
    (1)FlowBean对象在在需求1基础上增加了比较功能
        public class FlowBean implements WritableComparable<FlowBean>{
            private long upFlow; //上行流量
            private long downFlow; //下行流量
            private long sumFlow; //总流量
            //提供无参构造
            public FlowBean() {
            }
            //生成三个属性的getter和setter方法
            public long getUpFlow() {
                return upFlow;
            }
             public void setUpFlow(long upFlow) {
                this.upFlow = upFlow;
            }
            public long getDownFlow() {
                return downFlow;
            }
            public void setDownFlow(long downFlow) {
                this.downFlow = downFlow;
            }
             public long getSumFlow() {
                return sumFlow;
            }
            public void setSumFlow(long sumFlow) {
                this.sumFlow = sumFlow;
            }
            public void setSumFlow() {
                this.sumFlow = this.upFlow + this.downFlow;
            }
            //实现序列化和反序列化方法,注意顺序一定要一致
            @Override
            public void write(DataOutput out) throws IOException {
                out.writeLong(this.upFlow);
                out.writeLong(this.downFlow);
                out.writeLong(this.sumFlow);
            }
            
            @Override
            public void readFields(DataInput in) throws IOException {
                this.upFlow = in.readLong();
                this.downFlow = in.readLong();
                this.sumFlow = in.readLong();
            }
            
            //重写ToString,最后要输出FlowBean
            @Override
            public String toString() {
                return upFlow + "\t" + downFlow + "\t" + sumFlow;
            }
            
            @Override
            public int compareTo(FlowBean o) {
                //按照总流量比较,倒序排列
                if(this.sumFlow > o.sumFlow){
                    return -1;
                }else if(this.sumFlow < o.sumFlow){
                    return 1;
                }else {
                    return 0;
                }
            }
    }
    
Mapper类
    public class FlowMapper extends Mapper<LongWritable, Text, FlowBean, Text> {
        private FlowBean outK = new FlowBean();
        private Text outV = new Text();

        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            //1 获取一行数据
            String line = value.toString();
            //2 按照"\t",切割数据
            String[] split = line.split("\t");
            //3 封装outK outV
            outK.setUpFlow(Long.parseLong(split[1]));
            outK.setDownFlow(Long.parseLong(split[2]));
            outK.setSumFlow();
            outV.set(split[0]);
            //4 写出outK outV
            context.write(outK,outV);
        }
}

Reducer类
    public class FlowReducer extends Reducer<FlowBean, Text, Text, FlowBean> {
    
    @Override
    protected void reduce(FlowBean key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
        //遍历values集合,循环写出,避免总流量相同的情况
        for (Text value : values) {
            //调换KV位置,反向写出
            context.write(value,key);
        }
    }
}

Driver类
    public class FlowDriver {
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
            //1 获取job对象
            Configuration conf = new Configuration();
            Job job = Job.getInstance(conf);
            //2 关联本Driver类
            job.setJarByClass(FlowDriver.class);
            //3 关联Mapper和Reducer
            job.setMapperClass(FlowMapper.class);
            job.setReducerClass(FlowReducer.class);
            //4 设置Map端输出数据的KV类型
            job.setMapOutputKeyClass(FlowBean.class);
            job.setMapOutputValueClass(Text.class);
            //5 设置程序最终输出的KV类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(FlowBean.class);
            //6 设置输入输出路径
            FileInputFormat.setInputPaths(job, new Path("D:\\inputflow2"));
            FileOutputFormat.setOutputPath(job, new Path("D:\\comparout"));
            //7 提交Job
            boolean b = job.waitForCompletion(true);
            System.exit(b ? 0 : 1);
        }
}

4. 区内排序案例
    (1)增加自定义分区类

public class ProvincePartitioner2 extends Partitioner<FlowBean, Text> {

    @Override
    public int getPartition(FlowBean flowBean, Text text, int numPartitions) {
        //获取手机号前三位
        String phone = text.toString();
        String prePhone = phone.substring(0, 3);
        //定义一个分区号变量partition,根据prePhone设置分区号
        int partition;
        if("136".equals(prePhone)){
            partition = 0;
        }else if("137".equals(prePhone)){
            partition = 1;
        }else if("138".equals(prePhone)){
            partition = 2;
        }else if("139".equals(prePhone)){
            partition = 3;
        }else {
            partition = 4;
        }
        //最后返回分区号partition
        return partition;
    }
}
(2)在驱动类中添加分区类
    // 设置自定义分区器
    job.setPartitionerClass(ProvincePartitioner2.class);
    // 设置对应的ReduceTask的个数
    job.setNumReduceTasks(5);

3.3.4 Combiner合并

   1. CombinerMR程序中MapperReducer之外的一种组件
   2. Combiner的父类就是Reducer
   3. CombinerReducer的区别在于运行的位置
       Combiner是在每一个MapTask所在节点运行
       Reducer是接收全局所有的Mapper的输出结果
   4. Combiner就是对每一个MapTask的输出进行局部汇总,以减少网络传输量
   5. Combiner能够应用的前提是不能影响最终的业务逻辑,而且Combiner的输出kv应该跟Reducer的输出kv类型要对应起来
   6. 自定义Combiner实现步骤
       1.继承Reducer重写Reduce方法
       public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
            private IntWritable outV = new IntWritable();
            @Override
            protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
                int sum = 0;
                for (IntWritable value : values) {
                    sum += value.get();
                }
                outV.set(sum);
                context.write(key,outV);
            }
      }
      2.Job驱动类中设置: 
        job.setCombinerClass(WordCountCombiner.class);
        

3.4 OutputFormat数据输出

3.4.1 自定义OutputFormat案例

   1. 需求:
       过滤输入的log日志,包含atguigu的网站输出到e:/atguigu.log,不包含atguigu的网站输出到e:/other.log
   2. 代码:
       (1)编写LogMapper类
        public class LogMapper extends Mapper<LongWritable, Text,Text, NullWritable> {
            @Override
            protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
                //不做任何处理,直接写出一行log数据
                context.write(value,NullWritable.get());
            }
        }
        (2)编写LogReducer类
            public class LogReducer extends Reducer<Text, NullWritable,Text, NullWritable> {
                @Override
                protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
                    // 防止有相同的数据,迭代写出
                    for (NullWritable value : values) {
                        context.write(key,NullWritable.get());
                    }
                }
            }
        (3)自定义一个LogOutputFormat类
            public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {
                @Override
                public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
                    //创建一个自定义的RecordWriter返回
                    LogRecordWriter logRecordWriter = new LogRecordWriter(job);
                    return logRecordWriter;
                }
            }
      (4)编写LogRecordWriter类
    public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
        private FSDataOutputStream atguiguOut;
        private FSDataOutputStream otherOut;
        public LogRecordWriter(TaskAttemptContext job) {
            try {
                //获取文件系统对象
                FileSystem fs = FileSystem.get(job.getConfiguration());
                //用文件系统对象创建两个输出流对应不同的目录
                atguiguOut = fs.create(new Path("d:/hadoop/atguigu.log"));
                otherOut = fs.create(new Path("d:/hadoop/other.log"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        @Override
        public void write(Text key, NullWritable value) throws IOException, InterruptedException {
            String log = key.toString();
            //根据一行的log数据是否包含atguigu,判断两条输出流输出的内容
            if (log.contains("atguigu")) {
                atguiguOut.writeBytes(log + "\n");
            } else {
                otherOut.writeBytes(log + "\n");
            }
        }
        @Override
        public void close(TaskAttemptContext context) throws IOException, InterruptedException {
            //关流
            IOUtils.closeStream(atguiguOut);
            IOUtils.closeStream(otherOut);
        }
    }
    
    (5)编写LogDriver类
    public class LogDriver {
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
            Configuration conf = new Configuration();
            Job job = Job.getInstance(conf);
            job.setJarByClass(LogDriver.class);
            job.setMapperClass(LogMapper.class);
            job.setReducerClass(LogReducer.class);
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(NullWritable.class);
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(NullWritable.class);
            //设置自定义的outputformat
            job.setOutputFormatClass(LogOutputFormat.class);
            FileInputFormat.setInputPaths(job, new Path("D:\\input"));
            //虽然我们自定义了outputformat,但是因为我们的outputformat继承自fileoutputformat
            //而fileoutputformat要输出一个_SUCCESS文件,所以在这还得指定一个输出目录
            FileOutputFormat.setOutputPath(job, new Path("D:\\logoutput"));
            boolean b = job.waitForCompletion(true);
            System.exit(b ? 0 : 1);
        }
    }

3.5 MapReduce内核解析

3.5.1 MapTask工作机制

MapTask工作机制.png

    (1)Read阶段:MapTask通过InputFormat获得的RecordReader,从输入InputSplit中解析出一个个key/value。
    (2)Map阶段:该节点主要是将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value。
    (3)Collect收集阶段:在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。在该函数内部,它会将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区中。
    (4)Spill阶段:即“溢写”,当环形缓冲区满后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。
       溢写阶段详情:
       步骤1:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。
       步骤2:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。
       步骤3:将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中。
    (5)Merge阶段:当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。
       当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index。
       在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并mapreduce.task.io.sort.factor(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。
       让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。

3.5.2 ReduceTask工作机制

ReduceTask工作机制.png

1)Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
(2)Sort阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。
(3)Reduce阶段:reduce()函数将计算结果写到HDFS上

3.5.3 ReduceTask并行度决定机制

1. 设置ReduceTask并行度(个数)
   ReduceTask的并行度同样影响整个Job的执行并发度和执行效率,但与MapTask的并发数由切片数决定不同,ReduceTask数量的决定是可以直接手动设置:    
   // 默认值是1,手动设置为4
   job.setNumReduceTasks(4);
2. 注意事项
   (1) ReduceTask=0,表示没有Reduce阶段,输出的文件个数和Map个数一致
   (2) ReduceTask默认值就是1,所以输出的个数为1
   (3) 如果数据分布不均匀就可能在Reduce阶段产生数据倾斜
   (4) ReduceTask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下需要计算全局汇总结果,就只能有1个ReduceTask
   (5) 具体多少个ReduceTask需要根据集群性能而定
   (6) 如果分区数不是1,但ReduceTask为1,是否执行分区过程:不执行,因为在MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1

3.6 Join应用

3.6.1 ReduceJoin

Map端的主要工作:为来自不同表或文件的key/value对,打标签以区别不同来源的记录。然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出。
Reduce端的主要工作:在Reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在Map阶段已经打标志)分开,最后进行合并就ok了。

3.6.2 ReduceJoin 案例

订单数据表t_order
idpidamount
1001011
1002022
1003033
1004014
1005025
1006036
商品信息表t_product
pidpname
01小米
02华为
03格力
需求分析:
    通过将关联条件作为Map输出的key,将两表满足Join条件的数据并携带数据所来源的文件信息,发往同一个ReduceTask,在Reduce中进行数据的串联。

ReduceJoin.png

代码实现
(1)创建商品和订单合并后的TableBean类
    public class TableBean implements Writable {
        private String id; //订单id
        private String pid; //产品id
        private int amount; //产品数量
        private String pname; //产品名称
        private String flag; //判断是order表还是pd表的标志字段
        public TableBean() {

        }
        public String getId() {
            return id;
        }
        public void setId(String id) {
            this.id = id;
        }
        public String getPid() {
            return pid;
        }
        public void setPid(String pid) {
            this.pid = pid;
        }
        public int getAmount() {
            return amount;
        }
        public void setAmount(int amount) {
            this.amount = amount;
        }
        public String getPname() {
            return pname;
        }
        public void setPname(String pname) {
            this.pname = pname;
        }
        public String getFlag() {
            return flag;
        }
        public void setFlag(String flag) {
            this.flag = flag;
        }

        @Override
        public String toString() {
            return id + "\t" + pname + "\t" + amount;
        }

        @Override
        public void write(DataOutput out) throws IOException {
            out.writeUTF(id);
            out.writeUTF(pid);
            out.writeInt(amount);
            out.writeUTF(pname);
            out.writeUTF(flag);
        }

        @Override
        public void readFields(DataInput in) throws IOException {
            this.id = in.readUTF();
            this.pid = in.readUTF();
            this.amount = in.readInt();
            this.pname = in.readUTF();
            this.flag = in.readUTF();
        }
}
(2)编写TableMapper类
    public class TableMapper extends Mapper<LongWritable,Text,Text,TableBean> {
        private String filename;
        private Text outK = new Text();
        private TableBean outV = new TableBean();
        @Override
        protected void setup(Context context) throws IOException, InterruptedException {
            //获取对应文件名称
            InputSplit split = context.getInputSplit();
            FileSplit fileSplit = (FileSplit) split;
            filename = fileSplit.getPath().getName();
        }
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            //获取一行
            String line = value.toString();
            //判断是哪个文件,然后针对文件进行不同的操作
            if(filename.contains("order")){//订单表的处理
                String[] split = line.split("\t");
                //封装outK
                outK.set(split[1]);
                //封装outV
                outV.setId(split[0]);
                outV.setPid(split[1]);                outV.setAmount(Integer.parseInt(split[2]));
                outV.setPname("");
                outV.setFlag("order");
            }else {//商品表的处理
                String[] split = line.split("\t");
                //封装outK
                outK.set(split[0]);
                //封装outV
                outV.setId("");
                outV.setPid(split[0]);
                outV.setAmount(0);
                outV.setPname(split[1]);
                outV.setFlag("pd");
            }
            //写出KV
            context.write(outK,outV);
        }
    }
    
   (3)编写TableReducer类
   
 public class TableReducer extends Reducer<Text,TableBean,TableBean, NullWritable> {
    @Override
    protected void reduce(Text key, Iterable<TableBean> values, Context context) throws IOException, InterruptedException {
        ArrayList<TableBean> orderBeans = new ArrayList<>();
        TableBean pdBean = new TableBean();
        for (TableBean value : values) {
            //判断数据来自哪个表
            if("order".equals(value.getFlag())){//订单表
              //创建一个临时TableBean对象接收value
                TableBean tmpOrderBean = new TableBean();
                try {
                    BeanUtils.copyProperties(tmpOrderBean,value);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
              //将临时TableBean对象添加到集合orderBeans
                orderBeans.add(tmpOrderBean);

            }else//商品表
                try {
                    BeanUtils.copyProperties(pdBean,value);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
        //遍历集合orderBeans,替换掉每个orderBean的pid为pname,然后写出
        for (TableBean orderBean : orderBeans) {
            orderBean.setPname(pdBean.getPname());
           //写出修改后的orderBean对象
            context.write(orderBean,NullWritable.get());
        }
    }
}
(4)编写TableDriver类

public class TableDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        Job job = Job.getInstance(new Configuration());
        job.setJarByClass(TableDriver.class);
        job.setMapperClass(TableMapper.class);
        job.setReducerClass(TableReducer.class);
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(TableBean.class);
        job.setOutputKeyClass(TableBean.class);
        job.setOutputValueClass(NullWritable.class);
        FileInputFormat.setInputPaths(job, new Path("D:\\input"));
        FileOutputFormat.setOutputPath(job, new Path("D:\\output"));   
        boolean b = job.waitForCompletion(true);
        System.exit(b ? 0 : 1);
    }
}
总结:
     缺点:这种方式中,合并的操作是在Reduce阶段完成,Reduce端的处理压力太大,Map节点的运算负载则很低,资源利用率不高,且在Reduce阶段极易产生数据倾斜。
     解决方案:Map端实现数据合并。

3.6.3 Map Join

1. 使用场景:
     Map Join适用于一张表十分小、一张表很大的场景。
2. 优点
        思考:在Reduce端处理过多的表,非常容易产生数据倾斜。怎么办?
        在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜。
3. 具体办法:采用DistributedCache1)在Mapper的setup阶段,将文件读取到缓存集合中。
           (2)在Driver驱动类中加载缓存。
            //缓存普通文件到Task运行节点。
            job.addCacheFile(new URI("file:///e:/cache/pd.txt"));
            //如果是集群运行,需要设置HDFS路径
            job.addCacheFile(new URI("hdfs://hadoop102:8020/cache/pd.txt"));

3.6.4 Map Join案例

需求分析
    MapJoin适用于关联表中有小表的情形。

DistributedCache.png

代码实现
(1)先在MapJoinDriver驱动类中添加缓存文件

public class MapJoinDriver {
    public static void main(String[] args) throws IOException, URISyntaxException, ClassNotFoundException, InterruptedException {
        // 1 获取job信息
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
        // 2 设置加载jar包路径
        job.setJarByClass(MapJoinDriver.class);
        // 3 关联mapper
        job.setMapperClass(MapJoinMapper.class);
        // 4 设置Map输出KV类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(NullWritable.class);
        // 5 设置最终输出KV类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(NullWritable.class);
        // 加载缓存数据
        job.addCacheFile(new URI("file:///D:/input/tablecache/pd.txt"));
        // Map端Join的逻辑不需要Reduce阶段,设置reduceTask数量为0
        job.setNumReduceTasks(0);
        // 6 设置输入输出路径
        FileInputFormat.setInputPaths(job, new Path("D:\\input"));
        FileOutputFormat.setOutputPath(job, new Path("D:\\output"));
        // 7 提交
        boolean b = job.waitForCompletion(true);
        System.exit(b ? 0 : 1);
    }
}

(2)在MapJoinMapper类中的setup方法中读取缓存文件
public class MapJoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
    private Map<String, String> pdMap = new HashMap<>();
    private Text text = new Text();
    //任务开始前将pd数据缓存进pdMap
    @Override
    protected void setup(Context context) throws IOException, InterruptedException {
        //通过缓存文件得到小表数据pd.txt
        URI[] cacheFiles = context.getCacheFiles();
        Path path = new Path(cacheFiles[0]);
        //获取文件系统对象,并开流
        FileSystem fs = FileSystem.get(context.getConfiguration());
        FSDataInputStream fis = fs.open(path);
        //通过包装流转换为reader,方便按行读取
        BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
        //逐行读取,按行处理
        String line;
        while (StringUtils.isNotEmpty(line = reader.readLine())) {
            //切割一行    
            //01  小米
            String[] split = line.split("\t");
            pdMap.put(split[0], split[1]);
        }
        //关流
        IOUtils.closeStream(reader);
    }

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        //读取大表数据    
        //1001   01  1
        String[] fields = value.toString().split("\t");
        //通过大表每行数据的pid,去pdMap里面取出pname
        String pname = pdMap.get(fields[1]);
        //将大表每行数据的pid替换为pname
        text.set(fields[0] + "\t" + pname + "\t" + fields[2]);
        //写出
        context.write(text,NullWritable.get());
    }
}

3.7 数据清洗(ETL)

“ETL,是英文Extract-Transform-Load的缩写,用来描述将数据从来源端经过抽取(Extract)、转换(Transform)、加载(Load)至目的端的过程。ETL一词较常用在数据仓库,但其对象并不限于数据仓库
在运行核心业务MapReduce程序之前,往往要先对数据进行清洗,清理掉不符合用户要求的数据。清理的过程往往只需要运行Mapper程序,不需要运行Reduce程序。
1.需求
    去除日志中字段个数小于等于11的日志。
2. 实现代码
(1)编写WebLogMapper类
public class WebLogMapper extends Mapper<LongWritable, Text, Text, NullWritable>{ 

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {     
        // 1 获取1行数据
        String line = value.toString();      
        // 2 解析日志
        boolean result = parseLog(line,context);       
        // 3 日志不合法退出
        if (!result) {
            return;
        }      
        // 4 日志合法就直接写出
        context.write(value, NullWritable.get());
    }
// 2 封装解析日志的方法
    private boolean parseLog(String line, Context context) {
        // 1 截取
        String[] fields = line.split(" ");      
        // 2 日志长度大于11的为合法
        if (fields.length > 11) {
            return true;
        }else {
            return false;
        }
    }
}
(2)编写WebLogDriver类
public class WebLogDriver {
    public static void main(String[] args) throws Exception {
       // 输入输出路径需要根据自己电脑上实际的输入输出路径设置
        args = new String[] { "D:/input/inputlog", "D:/output1" };
        // 1 获取job信息
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
        // 2 加载jar包
        job.setJarByClass(LogDriver.class);
        // 3 关联map
        job.setMapperClass(WebLogMapper.class);
        // 4 设置最终输出类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(NullWritable.class);
        // 设置reducetask个数为0
        job.setNumReduceTasks(0);
        // 5 设置输入和输出路径
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));
        // 6 提交
         boolean b = job.waitForCompletion(true);
         System.exit(b ? 0 : 1);
    }
}