Hadoop 3.1.3 (MapReduce)

Hadoop(MapReduce)

MapReduce概述

MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。

MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。

MapReduce优缺点

优点:

1.MapReduce 易于编程

它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的PC机器上运行。也就是说你写一个分布式程序,跟写一个简单的串行程序是一模一样的。就是因为这个特点使得MapReduce编程变得非常流行。

2.良好的扩展性

当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力。

3.高容错性

MapReduce设计的初衷就是使程序能够部署在廉价的PC机器上,这就要求它具有很高的容错性。比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败,而且这个过程不需要人工参与,而完全是由Hadoop内部完成的。

4.适合PB级以上海量数据的离线处理

可以实现上千台服务器集群并发工作,提供数据处理能力。

缺点:

  1. 不擅长实时计算
    MapReduce无法像MySQL一样,在毫秒或者秒级内返回结果。
  2. 不擅长流式计算
    流式计算的输入数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化。这是因为MapReduce自身的设计特点决定了数据源必须是静态的。
  3. 不擅长DAG(有向图)计算
    多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下,MapReduce并不是不能做,而是使用后,每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常的低下。
MapReduce核心思想

截屏2020-04-14下午7.45.33

1)分布式的运算程序往往需要分成至少2个阶段。

2)第一个阶段的MapTask并发实例,完全并行运行,互不相干。

3)第二个阶段的ReduceTask并发实例互不相干,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出。

4)MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行。

总结:分析WordCount数据流走向深入理解MapReduce核心思想。

MapReduce进程

一个完整的MapReduce程序在分布式运行时有三类实例进程:

1)MrAppMaster:负责整个程序的过程调度及状态协调。

2)MapTask:负责Map阶段的整个数据处理流程。

3)ReduceTask:负责Reduce阶段的整个数据处理流程。

官方WordCount源码

截屏2020-04-14下午7.54.04

常用数据序列化类型
Java类型Hadoop Writable类型
BooleanBooleanWritable
ByteByteWritable
IntIntWritable
FloatFloatWritable
LongLongWritable
DoubleDoubleWritable
StringText
MapMapWritable
ArrayArrayWritable
MapReduce编程规范

用户编写的程序分成三个部分:Mapper、Reducer和Driver。

1.Mapper阶段

(1)用户自定义的Mapper要继承自己的父类

(2)Mapper的输入数据是KV对的形式(KV的类型可自定义)

(3)Mapper中的业务逻辑写在map()方法中

(4)Mapper的输出数据是KV对的形式(KV的类型可自定义)

(5)map()方法(MapTask进程)对每一个<K,V>调用一次

2.Reducer阶段

(1)用户自定义的Reducer要继承自己的父类

(2)Reducer的输入数据类型对应Mapper的输出数据类型,也是KV

(3)Reducer的业务逻辑写在reduce()方法中

(4)ReduceTask进程对每一组相同k的<k,v>组调用一次reduce()方法

3.Driver阶段

相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是封装了MapReduce程序相关运行参数的job对象

WordCount案例实操

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

环境准备

(1)创建maven工程

截屏2020-04-14下午8.44.50

(2)在pom.xml文件中添加如下依赖

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.12.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-client</artifactId>
        <version>3.1.3</version>
    </dependency>
</dependencies>

(2)在项目的src/main/resources目录下,新建一个文件,命名为“log4j2.xml”

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="error" strict="true" name="XMLConfig">
    <Appenders>
        <!-- 类型名为Console,名称为必须属性 -->
        <Appender type="Console" name="STDOUT">
            <!-- 布局为PatternLayout的方式,
            输出样式为[INFO] [2018-01-22 17:34:01][org.test.Console]I'm here -->
            <Layout type="PatternLayout"
                    pattern="[%p] [%d{yyyy-MM-dd HH:mm:ss}][%c{10}]%m%n" />
        </Appender>

    </Appenders>

    <Loggers>
        <!-- 可加性为false -->
        <Logger name="test" level="info" additivity="false">
            <AppenderRef ref="STDOUT" />
        </Logger>

        <!-- root loggerConfig设置 -->
        <Root level="info">
            <AppenderRef ref="STDOUT" />
        </Root>
    </Loggers>

</Configuration>

编写程序

Mapper类:
setup()方法: 在每个MapTask中只会在Task开始运行时被调用一次.

Called once at the beginning of the task .

map()方法: 一个切片中输入的每一个kv对会调用一次map方法。

Called once for each key/value pair in the input split. Most applications should override this, but the default is the identity function.

cleanup()方法:在每个MapTask中只会在Task结束前被调用一次

Called once at the end of the task.

run()方法: 负责控制Mapper的执行过程.

Reducer类:

setup()方法: 在每个ReduceTask开始运行时会调用一次该方法.

Called once at the start of the task.

reduce()方法: 为同一个key(map端输出的数据可能有相同key的多个kv对,称之为一组kv)执行一次reduce方法。

This method is called once for each key. Most applications will define their reduce class by overriding this method. The default implementation is an identity function.

cleanup()方法:在每个ReduceTask结束时会调用一次该方法

Called once at the end of the task.

run()方法: 负责控制Reducer的执行过程。

需求:自定义编写wordcount

(1)编写Mapper类

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

/**
 * 继承Mapper类,指定4个泛型——2组kv对
 * 一组输入数据的kv 另外一组输出的kv
 * <KEYIN, VALUEIN, KEYOUT, VALUEOUT>
 * KEYIN:LongWritable,表示文件中读取数据的偏移量(读到了什么位置,下一次从哪个位置读取)
 * VALUEIN:TEST,表示从文件中读取的一行数据
 * <p>
 * KEYOUT:Test,表示一个单词
 * VALUEOUT:IntWritable ,表示单词出现了一次
 *
 * @author Vanas
 * @create 2020-04-14 3:22 下午
 */

public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
    IntWritable outV = new IntWritable(1);
    Text outK = new Text();

    /**
     *
     * @param key 输入的key(keyin 位置)
     * @param value 输入的v,文件中读取的一行内容
     * @param context 负责调度Mapper运行
     * @throws IOException
     * @throws InterruptedException
     */

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//      1。 一行数据转换成String
        String line = value.toString();
//      2.使用空格切分数据
        String[] splits = line.split(" ");
//      3.迭代splits数组 ,将每个单词处理为kv写出去
        for (String word : splits) {
            outK.set(word);
            context.write(outK, outV);
        }
    }
}

(2)编写Reducer类

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * <KEYIN, VALUEIN, KEYOUT, VALUEOUT>
 * KEYIN: Text,Mapper输出的key ,单词
 * VALUEIN:IntWritable,Mapper输出的v,单词出现的次数
 * <p>
 * KEYOUT:Text,单词
 * VALUEOUT: IntWritable,单词出现的总次数
 *
 * @author Vanas
 * @create 2020-04-14 3:23 下午
 */
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {

    IntWritable outV = new IntWritable();

    /**
     * @param key     输入的key,一个单词
     * @param values  表示封装了当前key对应所有的value的一个迭代器对象
     * @param context 负责调度Reducer运行
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
//      1.迭代values将当前key对应的所有value累加
        int sum = 0;
        for (IntWritable value : values) {
            sum += value.get();
        }
//      2.写出 将累加的结果sum封装到outV中
        outV.set(sum);

        context.write(key, outV);
    }
}

(3)编写Driver驱动类

本地测试
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-14 3:23 下午
 */
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.关联jar
        job.setJarByClass(WordCountDriver.class);
//        3.关联Mapper 和 Reduce类
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);
//        4.设置Mapper的输出key和value的类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);
//        5.设置最终输出key和value的类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);
//        6.设置输入和输出路径
        FileInputFormat.setInputPaths(job, new Path("/Users/vanas/Desktop/hello.txt"));
        FileOutputFormat.setOutputPath(job, new Path("/Users/vanas/Desktop/output"));
//        7.提交job
        job.waitForCompletion(true);

    }
}

截屏2020-04-14下午8.45.56

截屏2020-04-14下午8.50.43

集群上测试

用maven打jar包

截屏2020-04-15上午11.19.05

//        6。设置输入和输出路径
//        FileInputFormat.setInputPaths(job, new Path("/Users/vanas/Desktop/hello.txt"));
//        FileOutputFormat.setOutputPath(job, new Path("/Users/vanas/Desktop/output"));

        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

(1)将程序打成jar包,然后拷贝到Hadoop集群中

(2)启动Hadoop集群

(3)执行WordCount程序

rz上传jar包和hello.txt文件

[vanas@hadoop130 hadoop-3.1.3]$ rz
[vanas@hadoop130 hadoop-3.1.3]$ hdfs dfs -mkdir /input
[vanas@hadoop130 hadoop-3.1.3]$ hdfs dfs -put hello.txt /input/
[vanas@hadoop130 hadoop-3.1.3]$  hadoop jar MapReduceDemo-1.0-SNAPSHOT.jar com.atguigu.mr.wordcount.WordCountDriver /input /output

截屏2020-04-15上午11.12.41

在Mac上向集群提交任务

(1)添加必要配置信息

主要是configuration配置、关联jar包、设置输入输出路径

package com.atguigu.mr.wordcount;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-14 3:23 下午
 */
public class WordCountDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//        1创建Job对象
//        获取配置信息以及封装任务
        Configuration conf = new Configuration();
//        设置HDFS NameNode的地址
        conf.set("fs.defaultFS", "hdfs://hadoop130:9820");
        // 指定MapReduce运行在Yarn上
        conf.set("mapreduce.framework.name", "yarn");
        //指定Yarn resourcemanager的位置
        conf.set("yarn.resourcemanager.hostname", "hadoop133");
        // 指定mapreduce可以在远程集群运行
        conf.set("mapreduce.app-submission.cross-platform", "true");

        Job job = Job.getInstance(conf);
//        2。关联jar
        job.setJar("/Users/vanas/Desktop/BigData/MapReduceDemo/target/MapReduceDemo-1.0-SNAPSHOT.jar");
//        job.setJarByClass(WordCountDriver.class);

//        3。关联Mapper 和 Reduce类
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);
//        4。设置Mapper的输出key和value的类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);
//        5设置最终输出key和value的类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);
//        6。设置输入和输出路径
//        FileInputFormat.setInputPaths(job, new Path("/Users/vanas/Desktop/hello.txt"));
//        FileOutputFormat.setOutputPath(job, new Path("/Users/vanas/Desktop/output"));

        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));
//        7。提交job
        job.waitForCompletion(true);

    }
}

(3)编辑任务配置

截屏2020-04-15上午11.37.16

run

截屏2020-04-15上午11.37.58

截屏2020-04-15上午11.43.35

Hadoop序列化

序列化概述

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

反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象。

为什么要序列化?

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

为什么不用Java的序列化?

Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输。所以,Hadoop自己开发了一套序列化机制(Writable)。

Hadoop序列化特点:

(1)紧凑 :高效使用存储空间

(2)快速:读写数据的额外开销小

(3)可扩展:随着通信协议的升级而可升级

(4)互操作:支持多语言的交互

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

在企业开发中往往常用的基本序列化类型不能满足所有需求,比如在Hadoop框架内部传递一个bean对象,那么该对象就需要实现序列化接口。

Hadoop序列化:
Writable接口:
write(DataOutput out) : 对象的序列化
readFields(DataInput in) :对象的反序列化

序列化步骤:

1. 实现Writable接口
 2. 在类中提供无参数构造器, 反序列化时会反射调用.
 3. write方法中写出的fields顺序与 readFields读取的fields顺序要一致.
 4. 一般也会重写toString()方法, 比较简单的重写就是将各个fields通过\t分隔打印.
    Reduce输出的kv中如果包含当前类的对象, 会调用toString进行打印.
    例如: Person 
          xxxReducer extends Reducer<keyIn,valueIn, Person ,valueOut>

序列化案例分析:
使用手机号做为key, 保证相同手机号的数据最终能进入到一个reduce方法, 实现流量的汇总.
使用一个bean对象作为value, bean对象是对某个手机号的上行,下行,总流量的封装.

如果在mr过程中,使用了一个bean对象作为k或者v , 要保证bean对象是支持序列化和反序列化的.

序列化案例实操

需求

统计每一个手机号耗费的总上行流量、下行流量、总流量

截屏2020-04-15上午11.55.03

编写MapReduce程序

(1)编写流量统计的Bean对象

注意包装类的使用

设置get、set方法

重写toString

提供空参构造器(好习惯)

实现序列化(序列化与反序列化 注意顺序一致)

import org.apache.hadoop.io.Writable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

/**
 * 每个手机号的 上行 下行 总流量 的封装
 * @author Vanas
 * @create 2020-04-15 10:27 上午
 */
public class FlowBean implements Writable {

    private Long upFlow;    //上行流量
    private Long downFlow;  //下行流量
    private Long sumFlow;   //总流量


    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 String toString() {
        return upFlow + "\t" + downFlow + "\t" + sumFlow;
    }

    public FlowBean() {
    }

    //  序列化
    public void write(DataOutput out) throws IOException {
        out.writeLong(upFlow);
        out.writeLong(downFlow);
        out.writeLong(sumFlow);
    }

    //  反序列化
    public void readFields(DataInput in) throws IOException {
        this.upFlow = in.readLong();
        this.downFlow = in.readLong();
        this.sumFlow = in.readLong();
    }
}

(2)编写Mapper类

需要重载FlowBean类中setSumFlow()方法,为了方便

注意sum写在up与down之后

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-15 10:29 上午
 */
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[] splits = line.split("\t");
//      3.封装key
        outK.set(splits[1]);
//      4.封装value
        outV.setUpFlow(Long.parseLong(splits[splits.length - 3]));
        outV.setDownFlow(Long.parseLong(splits[splits.length - 2]));
        outV.setSumFlow();
//      5.写出
        context.write(outK, outV);

    }
}

(3)编写Reducer类

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-15 10:29 上午
 */
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 totalUpFlow = 0;
//        总下行
        long totalDownFlow = 0;
//        总流量
        long totalSumFlow = 0;
//        处理一个手机号的总上行 总下行 总流量
        for (FlowBean flowBean : values) {
            totalUpFlow += flowBean.getUpFlow();
            totalDownFlow += flowBean.getDownFlow();
            totalSumFlow += flowBean.getSumFlow();

        }
//        封装value
        outV.setUpFlow(totalUpFlow);
        outV.setDownFlow(totalDownFlow);
        outV.setSumFlow(totalSumFlow);
//        写出
        context.write(key,outV);
    }
}

(4)编写Driver驱动类

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-15 10:30 上午
 */
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。关联jar
        job.setJarByClass(FlowDriver.class);

//        3。关联Mapper reducer
        job.setMapperClass(FlowMapper.class);
        job.setReducerClass(FlowReducer.class);
//        4。设置map输出的 k vleix
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(FlowBean.class);
//        5。设置最终输出的 k v
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(FlowBean.class);
//        6。设置输入和输出路径
        FileInputFormat.setInputPaths(job, new Path("/Users/vanas/Desktop/phone_data.txt"));
        FileOutputFormat.setOutputPath(job, new Path("/Users/vanas/Desktop/output"));
//        7。提交job
        job.waitForCompletion(true);
    }
}

phone_data.txt 和输出结果

截屏2020-04-15下午12.45.09

截屏2020-04-15下午12.46.35

注意要删除已存在的Output目录 否则会报异常

MR程序输出路径存在异常:
Exception in thread “main” org.apache.hadoop.mapred.FileAlreadyExistsException: Output directory file:/D:/output already exists

MapReduce框架原理

MapReduce的大致流程:

Map --> shuffule -->reduce

MapReduce的详细流程:
map --> sort --> copy --> sort --> reduce

MapTask

截屏2020-04-15下午6.06.01

ReduceTask

截屏2020-04-15下午6.06.53

截屏2020-04-15下午2.07.22

InputFormat: 负责Map端数据的输入
重要的方法:
getSplits(): 生成切片信息。
createRecordReader(): 负责输入数据的读取处理
子抽象类: FileInputFormat
getSplits(): 做了具体的实现. Hadoop默认的切片规则。
具体实现类:
TextInputFormat : Hadoop默认使用的InputFormat
默认使用FileInputFormat中的getSplits方法来生成切片信息。
使用LineRecordReader来读取数据,就是一行一行读取.
CombineFileInputFormat
NLineInputFormat
KeyValueTextInputFormat

切片与MapTask并行度决定机制

切片个数与MapTask的个数的关系:

分析的过程:

1. 有多少个切片就需要启动多少个MapTask
2. 理论情况下,MapTask个数越多,并行度越高. 但是也要考虑每个MapTask要处理合适量级的数据.
3. 到底启动多少个MapTask,需要通过切片来进行量化.
4. 默认情况下, 切片的个数与 实际的数据量和 块大小的设置有关系.
 默认情况下,切片的大小与 块的大小是一样的。也就意味着,有多少个块,就会生成多少个切片.
 当然,Hadoop也支持我们通过配置的方式来改变切片的大小以决定生成多少个切片.

实际的结论:

1.  数据块:    HDFS存储数据时,会按照配置文件中的块大小将数据从物理上切块.
  数据切片:  进行MR计算时,  从逻辑上按照切片大小生成切片信息.
           	      实际生成的每个切片就是记录一下读文件时从哪里读到哪里.(读取数据的范围)
  1. 有多少个切片就需要启动多少个MapTask
  2. 切片时不考虑数据集的整体,针对单个文件单独切片.

MapTask并行度决定机制

数据块:Block是HDFS物理上把数据分成一块一块。

数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行

截屏2020-04-15下午2.44.17

切片的源码: FileInputFormat类中:

  /** 
   * Generate the list of files and make them into FileSplits.
   * @param job the job context
   * @throws IOException
   */
  public List<InputSplit> getSplits(JobContext job) throws IOException {
    StopWatch sw = new StopWatch().start();
    long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));//1
    //minSize ==> mapreduce.input.fileinputformat.split.minsize
    long maxSize = getMaxSplitSize(job);//Long.MaxValue
    //maxSize ==> mapreduce.input.fileinputformat.split.maxsize

    // generate splits
    List<InputSplit> splits = new ArrayList<InputSplit>();
    List<FileStatus> files = listStatus(job);

    boolean ignoreDirs = !getInputDirRecursive(job)
      && job.getConfiguration().getBoolean(INPUT_DIR_NONRECURSIVE_IGNORE_SUBDIRS, false);
    //循环每个文件,为每个文件单独生成切片.
    for (FileStatus file: files) {
      if (ignoreDirs && file.isDirectory()) {
        continue;
      }
      Path path = file.getPath();
      long length = file.getLen();
      if (length != 0) {
        BlockLocation[] blkLocations;
        if (file instanceof LocatedFileStatus) {
          blkLocations = ((LocatedFileStatus) file).getBlockLocations();
        } else {
          FileSystem fs = path.getFileSystem(job.getConfiguration());
          blkLocations = fs.getFileBlockLocations(file, 0, length);
        }
        if (isSplitable(job, path)) {
          long blockSize = file.getBlockSize();
          //计算切片的大小
          long splitSize = computeSplitSize(blockSize, minSize, maxSize);
					//return Math.max(minSize, Math.min(maxSize, blockSize));
          //minSize 改大 maxSize 改小
          
          long bytesRemaining = length;
          // 当前文件剩余的大小 除以  切片大小 >1.1 ,继续切片,否则,剩余的大小生成一个切片。
	  			// 避免数据倾斜问题
          while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {//1.1
            int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
            splits.add(makeSplit(path, length-bytesRemaining, splitSize,
                        blkLocations[blkIndex].getHosts(),
                        blkLocations[blkIndex].getCachedHosts()));
            bytesRemaining -= splitSize;
          }

          if (bytesRemaining != 0) {
            int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
            splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,
                       blkLocations[blkIndex].getHosts(),
                       blkLocations[blkIndex].getCachedHosts()));
          }
        } else { // not splitable
          if (LOG.isDebugEnabled()) {
            // Log only if the file is big enough to be splitted
            if (length > Math.min(file.getBlockSize(), minSize)) {
              LOG.debug("File is not splittable so no parallelization "
                  + "is possible: " + file.getPath());
            }
          }
          splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(),
                      blkLocations[0].getCachedHosts()));
        }
      } else { 
        //Create empty hosts array for zero length files
        splits.add(makeSplit(path, 0, length, new String[0]));
      }
    }
    // Save the number of input files for metrics/loadgen
    job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
    sw.stop();
    if (LOG.isDebugEnabled()) {
      LOG.debug("Total # of splits generated by getSplits: " + splits.size()
          + ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
    }
    //返回切片信息的集合
    return splits;
  }

Debug查看

截屏2020-04-15下午6.57.15

提取到的信息:

  1. 本地的块大小默认是32M

  2. long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job)); // 1
    // minSize ==> mapreduce.input.fileinputformat.split.minsize 通过该参数改变minSize
    long maxSize = getMaxSplitSize(job); // Long.MAX_VALUE
    // maxSize ==> mapreduce.input.fileinputformat.split.maxsize 通过该参数改变maxSize

  3. long splitSize = computeSplitSize(blockSize, minSize, maxSize);
    // return Math.max(minSize, Math.min(maxSize, blockSize)); 计算切片大小

  4. 一个切片信息: file:/D:/input/inputflow/phone_data.txt:0+1178
    意思就是读取/D:/input/inputflow/phone_data.txt的 0~1178 范围的数据.

CombineTextInputFormat案例实操

CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。

切片机制

生成切片过程包括:虚拟存储过程和切片过程二部分

截屏2020-04-15下午7.43.02

默认是TextInputForma,一个文件切一片 4个文件就是4片

截屏2020-04-15下午7.24.00

使用CombineFileInputFormat ,设置大小为4M

//        使用CombineTextInputFormat
        job.setInputFormatClass(CombineTextInputFormat.class);
        CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);//4M 33554432 32M

截屏2020-04-15下午7.29.45

NLineInputFormat使用案例

默认切一片

需求

对每个单词进行个数统计,要求根据每个输入文件的行数来规定输出多少个切片。此案例要求每三行放入一个切片中。

使用NLineInputFormat 11行切4片

//        使用NLineInputFormat
        NLineInputFormat.setNumLinesPerSplit(job, 3);
        job.setInputFormatClass(NLineInputFormat.class);

截屏2020-04-15下午7.49.36

KeyValueTextInputFormat使用案例

需求

统计输入文件中每一行的第一个单词相同的行数。

Mapper

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-15 4:36 下午
 */
public class KVMapper extends Mapper<Text, Text, Text, IntWritable> {
    IntWritable outV = new IntWritable(1);

    @Override
    protected void map(Text key, Text value, Context context) throws IOException, InterruptedException {
//        读到的一行数据会按照预先设置的分割符进行切分,左边作为key 右边为Value 进入到mapper中
        context.write(key, outV);

    }
}

Reducer

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-15 4:37 下午
 */
public class KVReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    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);
    }
}

Driver

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.KeyValueTextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-15 4:37 下午
 */
public class KVDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//        1创建Job对象
//        获取配置信息以及封装任务
        Configuration conf = new Configuration();
        conf.set("mapreduce.input.keyvaluelinerecordreader.key.value.separator"," ");
        Job job = Job.getInstance(conf);
//        2。关联jar
        job.setJarByClass(KVDriver.class);

//        3。关联Mapper 和 Reduce类
        job.setMapperClass(KVMapper.class);
        job.setReducerClass(KVReducer.class);
//        4。设置Mapper的输出key和value的类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);
//        5设置最终输出key和value的类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);
//        设置使用KeyValueTextInputFormat
        job.setInputFormatClass(KeyValueTextInputFormat.class);

//        6。设置输入和输出路径
        FileInputFormat.setInputPaths(job, new Path("/Users/vanas/Desktop/kv.txt"));
        FileOutputFormat.setOutputPath(job, new Path("/Users/vanas/Desktop/output"));

//        7。提交job
        job.waitForCompletion(true);

    }
}

自定义InputFormat

在企业开发中,Hadoop框架自带的InputFormat类型不能满足所有应用场景,需要自定义InputFormat来解决实际问题。

自定义InputFormat步骤如下:

(1)自定义一个类继承FileInputFormat。

(2)改写RecordReader,实现一次读取一个完整文件封装为KV。

(3)在输出时使用SequenceFileOutPutFormat输出合并文件。

Shuffle机制

MapReduce的过程:
Map – > shuffle --> reduce

map --> sort --> copy --> sort -->reduce

Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle

截屏2020-04-17上午9.02.38

Partition分区

分区:

  1. 默认的分区器  HashPartitioner   , 根据key的值对reduceTask的个数进行取余操作来计算分区.
public int getPartition(K key, V value,int numReduceTasks) {
		return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
  2. 如何获取分区器对象
 NewOutputCollector(org.apache.hadoop.mapreduce.JobContext jobContext,
                   JobConf job,
                   TaskUmbilicalProtocol umbilical,
                   TaskReporter reporter
                   ) throws IOException, ClassNotFoundException {
  collector = createSortingCollector(job, reporter);
  partitions = jobContext.getNumReduceTasks();  // 获取reduceTask的个数 , 在driver中设置的
  if (partitions > 1) {
    //通过反射的方式创建分区器
    partitioner = (org.apache.hadoop.mapreduce.Partitioner<K,V>)
      ReflectionUtils.newInstance(jobContext.getPartitionerClass(), job);
  } else {
    partitioner = new org.apache.hadoop.mapreduce.Partitioner<K,V>() {
      @Override
      public int getPartition(K key, V value, int numPartitions) {
        return partitions - 1;   //返回固定的分区号  0
      }
    };
  }
}

public Class<? extends Partitioner<?,?>> getPartitionerClass() 
 throws ClassNotFoundException {
return (Class<? extends Partitioner<?,?>>) 
  //获取 PARTITIONER_CLASS_ATTR 保存的分区器对象,如果没有,则默认使用HashPartitioner
  conf.getClass(PARTITIONER_CLASS_ATTR, HashPartitioner.class);
 }

  1. 写出kv要计算kv对应的分区号
@Override
public void write(K key, V value) throws IOException, InterruptedException {
  	// kv被收集到缓冲区时,要先把分区号计算出来。
 	 collector.collect(key, value,partitioner.getPartition(key, value, partitions));
}
  1. 自定义分区: 继承Partitioner类. 重写getPartition方法。

a. 在Driver中设置使用的分区类, 并设置对应的ReduceTask个数.
业务决定将来有多少个分区,但是分区必须要由ReduceTask来控制。
b. 分区数要按照实际分区器中业务逻辑决定的分区数来来设置,
如果ReduceTask的个数设置为1 ,则自定义的分区器无法使用。
如果ReducceTask的个数设置的比实际的分区数少,报错。
如果ReduceTask的个数设置的比实际的分区数多,不报错,但会有reduceTask没有数据可处理。

Partition分区案例实操

需求:根据手机号前三位分区

基于FlowBean

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;

/**
 * @author Vanas
 * @create 2020-04-17 10:18 上午
 */
public class PhoneNumPartitioner extends Partitioner<Text,FlowBean> {
    /**
     * 需求:136、137、138、139 其他
     * @param text
     * @param flowBean
     * @param numPartitions
     * @return
     */
    public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
        int partitioner;
        String key = text.toString();
        if (key.startsWith("136")){
            partitioner = 0;
        }else if (key.startsWith("137")){
            partitioner = 1;
        }else if (key.startsWith("138")){
            partitioner = 2;
        }else if (key.startsWith("139")){
            partitioner = 3;
        }else{
            partitioner = 4;
        }
        return partitioner;
    }
}

在提交job前设置分区

//        设置分区器
        job.setPartitionerClass(PhoneNumPartitioner.class);
//        设置ReduceTask的个数 为PhoneNumParititioner类中逻辑决定分区数
        job.setNumReduceTasks(5);

截屏2020-04-17下午1.32.26

例2:

需求根据a-p,q-z 分区

基于WordCount

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;

/**
 * @author Vanas
 * @create 2020-04-17 1:09 下午
 */
public class WordCountPartitioner extends Partitioner<Text, IntWritable> {


    public int getPartition(Text text, IntWritable intWritable, int numPartitions) {
        int partitioner;
        String key = text.toString();

        if (key.charAt(0) > 'a' && key.charAt(0) < 'p') {
            partitioner = 0;
        } else {
            partitioner = 1;
        }
        return partitioner;
    }
}
        job.setPartitionerClass(WordCountPartitioner.class);
        job.setNumReduceTasks(2);

截屏2020-04-17下午1.28.13

分区总结

(1)如果ReduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;

(2)如果1<ReduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会Exception;

(3)如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask,最终也就只会产生一个结果文件 part-r-00000;

(4)分区号必须从零开始,逐一累加。

例如:假设自定义分区数为5,则

(1)job.setNumReduceTasks(1); 会正常运行,只不过会产生一个输出文件

(2)job.setNumReduceTasks(2); 会报错

(3)job.setNumReduceTasks(6); 大于5,程序会正常运行,会产生空文件

WritableComparable排序

排序是MapReduce框架中最重要的操作之一。

MapTask和ReduceTask均会对数据按照key进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。

默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。

对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。

对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中。如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。

排序:

    1. JavaBean类实现WritableComparable接口, 然后重写compareTo()方法。
       . 排序分: 全排序  区内排序  
               3. 排序是Hadoop默认的行为,排序时只针对于key进行排序。 
排序的分类

(1)部分排序

MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序。

(2)全排序

最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。

(3)辅助排序:(GroupingComparator分组)

在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序。

(4)二次排序
在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。

自定义排序WritableComparable

bean对象做为key传输,需要实现WritableComparable接口重写compareTo方法,就可以实现排序

WritableComparable排序案例实操(全排序)

需求:按总流量降序排列

Bean

import org.apache.hadoop.io.WritableComparable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-17 10:47 上午
 */
public class FlowBean implements WritableComparable<FlowBean> {
    private Long upFlow;
    private Long downFlow;
    private Long sumFlow;

    public FlowBean() {
    }

    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 String toString() {
        return upFlow + "\t" + downFlow + "\t" + sumFlow;
    }


    public void write(DataOutput out) throws IOException {
        out.writeLong(upFlow);
        out.writeLong(downFlow);
        out.writeLong(sumFlow);
    }

    public void readFields(DataInput in) throws IOException {
        upFlow = in.readLong();
        downFlow = in.readLong();
        sumFlow = in.readLong();
    }

    public int compareTo(FlowBean o) {
        return -this.sumFlow.compareTo(o.getSumFlow());


    }
}

Mapper

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-17 10:47 上午
 */
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 {
//        处理一行数据
        String line = value.toString();
        String[] splits = line.split("\t");

//      封装
        outV.set(splits[1]);

        outK.setUpFlow(Long.parseLong(splits[splits.length-3]));
        outK.setDownFlow(Long.parseLong(splits[splits.length-2]));
        outK.setSumFlow();


        context.write(outK,outV);
    }
}

Reduce

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-17 10:47 上午
 */
public class FlowReducer extends Reducer<FlowBean, Text,Text,FlowBean> {

    @Override
    protected void reduce(FlowBean key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
//    注意不同的手机号但总流量相同会组成一组kv  进入一个reduce方法
//        手机号相同,但总流量不同情况,会被分开处理
//        直接迭代values,将每个手机号的数据直接写出
        for (Text value : values) {
            context.write(value,key);
        }

    }
}

Driver

package com.atguigu.mr.writablecomparable;


import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-17 11:10 上午
 */
public class FlowDriver {
    public static void main(String[] args) throws InterruptedException, IOException, ClassNotFoundException {
        //        1.创建job对象
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

//        2。关联jar
        job.setJarByClass(FlowDriver.class);

//        3。关联Mapper reducer
        job.setMapperClass(FlowMapper.class);
        job.setReducerClass(FlowReducer.class);
//        4。设置map输出的 k v
        job.setMapOutputKeyClass(FlowBean.class);
        job.setMapOutputValueClass(Text.class);
//        5。设置最终输出的 k v
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(FlowBean.class);
//        6设置输入和输出路径
        FileInputFormat.setInputPaths(job, new Path("/Users/vanas/Desktop/phone_data.txt"));
        FileOutputFormat.setOutputPath(job, new Path("/Users/vanas/Desktop/output2"));
        //        提交job
        job.waitForCompletion(true);
    }
}

WritableComparable排序案例实操(区内排序)

需求:按总流量降序排列,并按号码前3位分区

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;



/**
 * @author Vanas
 * @create 2020-04-17 12:38 下午
 */
public class PhoneNumPartitioner extends Partitioner<FlowBean, Text> {

    public int getPartition(FlowBean flowBean, Text text, int numPartitions) {
        int partitioner;
        String phoneNum = text.toString();
        if (phoneNum.startsWith("136")) {
            partitioner = 0;
        } else if (phoneNum.startsWith("137")) {
            partitioner = 1;
        } else if (phoneNum.startsWith("138")) {
            partitioner = 2;
        } else if (phoneNum.startsWith("139")) {
            partitioner = 3;
        } else {
            partitioner = 4;
        }
        return partitioner;
    }
}

Driver

job前设置分区

        job.setPartitionerClass(PhoneNumPartitioner.class);
        job.setNumReduceTasks(5);

截屏2020-04-17下午1.47.49

Combiner合并

Combiner:

    1. 是Mappper和Reduce之外的组件。
       . Combiner是在每个MapTask中对数据做局部的汇总。 减少数据的传输量以及Reducer计算的数据量.
               3. Combiner的父类也是Reducer.但是Combiner是在Map阶段的MapTask中执行的。

截屏2020-04-17下午1.49.22

自定义Combiner实现步骤

(a)自定义一个Combiner继承Reducer,重写Reduce方法

(b)在Job驱动类中设置:

Combiner合并案例实操

需求:

统计过程中对每一个MapTask的输出进行局部汇总,以减小网络传输量即采用Combiner功能。

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**还是继承Reducer 但是是在MapTask里运行
 * @author Vanas
 * @create 2020-04-17 11:47 上午
 */
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);
    }
}

Driver:

 job.setCombinerClass(WordCountCombiner.class);

e

截屏2020-04-17下午1.51.43

GroupingComparator分组(辅助排序)

对Reduce阶段的数据根据某一个或几个字段进行分组

分组排序步骤:

(1)自定义类继承WritableComparator

(2)重写compare()方法

(3)创建一个构造将比较对象的类传给父类

WritableComparator : 比较

  private static final ConcurrentHashMap<Class, WritableComparator> comparators 
          = new ConcurrentHashMap<Class, WritableComparator>(); // registry
       //存储Hadoop自身序列化类型及对应的分组比较器。   

实际在Hadoop自身的序列化类型中,都已经定义好了对应的分组比较器.
例如: 在Text类中:

  /** 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);
    }
  }

在运行MR程序时,hadoop会将自身的序列化类型及对应的分组比较器注册到WritableComparator中的comparators这个Map中.

  static {
    // register this comparator
    WritableComparator.define(Text.class, new Comparator());
  }

----->

  /** Register an optimized comparator for a {@link WritableComparable}
   * implementation. Comparators registered with this method must be
   * thread-safe. */
  public static void define(Class c, WritableComparator comparator) {
    comparators.put(c, comparator);
  }

在mr中如何获取当前key类型对应的分组比较器

在MapTask中:

comparator = job.getOutputKeyComparator();

  /**
   * Get the {@link RawComparator} comparator used to compare keys.
   * 
   * @return the {@link RawComparator} comparator used to compare keys.
   */
  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);
  }
    

  /** Get a comparator for a {@link WritableComparable} implementation. */
  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;
  }

自定义比较器对象: 继承WritableComparator,提供构造方法,重写compre方法.

//4个方法 重写这个就行
public int compare(WritableComparable a, WritableComparable b) {
   return a.compareTo(b);
}

//如果一个key没有提供对应的比较器对象,默认会使用key对应的类中的 compareTo方法来进行分组比较.   
GroupingComparator分组案例实操

需求

有如下订单数据

订单id商品id成交金额
0000001Pdt_01222.8
Pdt_0233.8
0000002Pdt_03522.8
Pdt_04122.4
Pdt_05722.4
0000003Pdt_06232.8
Pdt_0233.8

现在需要求出每一个订单中最贵的商品。

需求分析

(1)利用“订单id和成交金额”作为key,可以将Map阶段读取到的所有订单数据按照id升序排序,如果id相同再按照金额降序排序,发送到Reduce。

(2)在Reduce端利用groupingComparator将订单id相同的kv聚合成组,然后取第一个即是该订单中最贵商品。

截屏2020-04-17下午8.17.43

代码实现

Bean

import org.apache.hadoop.io.WritableComparable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-17 3:22 下午
 */
public class OrderBean implements WritableComparable<OrderBean> {
    private String orderId;
    private Double price;

    public String getOrderId() {
        return orderId;
    }

    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public OrderBean() {
    }

    @Override
    public String toString() {
        return orderId + "\t" + price;
    }
//	比较规则:按照订单id升序,价格降序 ,注意负号
    public int compareTo(OrderBean o) {

        return this.orderId.compareTo(o.getOrderId()) == 0 ? -this.price.compareTo(o.getPrice()) : this.orderId.compareTo(o.getOrderId());

    }

    public void write(DataOutput out) throws IOException {
        out.writeUTF(orderId);
        out.writeDouble(price);
    }

    public void readFields(DataInput in) throws IOException {
        orderId = in.readUTF();
        price = in.readDouble();
    }
}

Mapper

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-17 3:22 下午
 */
public class OrderMapper extends Mapper<LongWritable, Text, OrderBean, NullWritable> {
    private OrderBean outK = new OrderBean();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String line = value.toString();
        String[] splits = line.split("\t");


        outK.setOrderId(splits[0]);
        outK.setPrice(Double.parseDouble(splits[2]));


        context.write(outK, NullWritable.get());
    }
}

Comparator

为了分到一个组 所以有了分组比较器

import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;

/** OrderBean 对象分组比较器
 * @author Vanas
 * @create 2020-04-17 3:33 下午
 */
public class OrderComparator extends WritableComparator {

    public OrderComparator() {
        super(OrderBean.class, true);

    }

    /**
     * 比较规则:比较id
     *
     * @param a
     * @param b
     * @return
     */
    @Override
    public int compare(WritableComparable a, WritableComparable b) {
        OrderBean aBean = (OrderBean) a;
        OrderBean bBean = (OrderBean) b;

        return aBean.getOrderId().compareTo(bBean.getOrderId());

    }
}

Reducer

import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-17 3:22 下午
 */
public class OrderReduce extends Reducer<OrderBean, NullWritable,OrderBean,NullWritable> {


    @Override
    protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
//        保证都是排好序的

//        直接将第一个数据即可
//        context.write(key,NullWritable.get());
        context.write(key,values.iterator().next());
    }
}


Driver

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-17 3:39 下午
 */
public class OrderDriver {

    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        //        1创建Job对象
//        获取配置信息以及封装任务
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
//        2。关联jar
        job.setJarByClass(OrderDriver.class);
//        3。关联Mapper 和 Reduce类
        job.setMapperClass(OrderMapper.class);
        job.setReducerClass(OrderReduce.class);
//        4。设置Mapper的输出key和value的类型
        job.setMapOutputKeyClass(OrderBean.class);
        job.setMapOutputValueClass(NullWritable.class);
//        5设置最终输出key和value的类型
        job.setOutputKeyClass(OrderBean.class);
        job.setOutputValueClass(NullWritable.class);
//        6。设置输入和输出路径
        FileInputFormat.setInputPaths(job, new Path("/Users/vanas/Desktop/order.txt"));
        FileOutputFormat.setOutputPath(job, new Path("/Users/vanas/Desktop/output"));

        job.setGroupingComparatorClass(OrderComparator.class);
//        7。提交job
        job.waitForCompletion(true);
    }
}

OutputFormat数据输出

OutputFormat: 负责Reduce端数据的输出
getRecordWriter(): 获取RecordWriter对象,负责数据的输出
checkOutputSpecs(): 检查输出路径

子抽象类:
FileOutputFormat:
checkOutputSpecs(): 对该方法做了默认的实现.

具体实现类:
TextOutputFormat : hadoop默认使用的OutputFormat
SequenceFileOutputFormat : kv格式的二进制文件.

OutputFormat接口实现类

OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了 OutputFormat接口。下面我们介绍几种常见的OutputFormat实现类。

1.文本输出TextOutputFormat

默认的输出格式是TextOutputFormat,它把每条记录写为文本行。它的键和值可以是任意类型,因为TextOutputFormat调用toString()方法把它们转换为字符串。

2.SequenceFileOutputFormat

将SequenceFileOutputFormat输出作为后续 MapReduce任务的输入,这便是一种好的输出格式,因为它的格式紧凑,很容易被压缩。

3.自定义OutputFormat

根据用户需求,自定义实现输出。

自定义OutputFormat

1.使用场景

为了实现控制最终文件的输出路径和输出格式,可以自定义OutputFormat。

例如:要在一个MapReduce程序中根据数据的不同输出两类结果到不同目录,这类灵活的输出需求可以通过自定义OutputFormat来实现。

2.自定义OutputFormat步骤

(1)自定义一个类继承FileOutputFormat。

(2)改写RecordWriter,具体改写输出数据的方法write()。

自定义OutputFormat案例实操

需求

过滤输入的log日志,包含atguigu的网站输出到e:/atguigu.log,不包含atguigu的网站输出到e:/other.log。

截屏2020-04-18上午1.48.43

Mapper

package com.atguigu.mr.outputFormat;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-17 4:23 下午
 */
public class OutputFormatMapper extends Mapper<LongWritable, Text, Text, NullWritable> {

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        context.write(value, NullWritable.get());
    }
}

Reducer

package com.atguigu.mr.outputFormat;

import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-17 4:24 下午
 */
public class OutputFormatReducer extends Reducer<Text, NullWritable, Text, NullWritable> {

    @Override
    protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
//   迭代values直接写出
        for (NullWritable value : values) {
            context.write(key, value);
        }
    }
}

import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-17 4:23 下午
 */
public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {


    public RecordWriter getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
        return new LogRecordWriter(job);
    }
}
import jdk.nashorn.internal.ir.IfNode;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-17 4:30 下午
 */
public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
    private String baiduPath = "/Users/vanas/Desktop/baidu.log";
    private String otherPath = "/Users/vanas/Desktop/other.log";
    private FSDataOutputStream baiduOut;
    private FSDataOutputStream otherOut;
    private TaskAttemptContext context;

    public LogRecordWriter(TaskAttemptContext context) throws IOException {
        this.context = context;
//        获取文件系统对象
        FileSystem fs = FileSystem.get(context.getConfiguration());
//        获取流
        baiduOut = fs.create(new Path(baiduPath));
        otherOut = fs.create(new Path(otherPath));
    }

    /**
     * 需求 baidu 可写到 baidu.log
     * 其他写在 other.log
     *
     * @param key
     * @param value
     * @throws IOException
     * @throws InterruptedException
     */

    public void write(Text key, NullWritable value) throws IOException, InterruptedException {

//        判断数据,使用不同的流数据写出
        String log = key.toString();
        if (log.contains("baidu")) {

            baiduOut.writeBytes(log);
        } else {
            otherOut.writeBytes(log + "\n\r");
        }

    }

    public void close(TaskAttemptContext context) throws IOException, InterruptedException {
        baiduOut.close();
        otherOut.close();
    }
}

Driver

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-17 4:40 下午
 */
public class OutputFormatDriver {

    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
//        2。关联jar
        job.setJarByClass(OutputFormatDriver.class);

//        3。关联Mapper 和 Reduce类
        job.setMapperClass(OutputFormatMapper.class);
        job.setReducerClass(OutputFormatReducer.class);
//        4。设置Mapper的输出key和value的类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(NullWritable.class);
//        5设置最终输出key和value的类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(NullWritable.class);
//        6。设置输入和输出路径
        FileInputFormat.setInputPaths(job, new Path("/Users/vanas/Desktop/log.txt"));
        FileOutputFormat.setOutputPath(job, new Path("/Users/vanas/Desktop/output1"));

        job.setOutputFormatClass(LogOutputFormat.class);
//        7。提交job
        job.waitForCompletion(true);
    }
}

截屏2020-04-17下午10.46.01

    ensureState(JobState.DEFINE);
    connect();

MR源码解读

一、 Job提交的流程
1. job.waitForCompletion(true); 在Driver中提交job
 1) sumbit() 提交
	(1) connect():
	    <1>  return new Cluster(getConfiguration());initialize(jobTrackAddr, conf);  
	          for (ClientProtocolProvider provider : frameworkLoader) {
              localProviderList.add(provider);
            }
		   // 通过YarnClientProtocolProvider | LocalClientProtocolProvider  根据配置文件的参数信息
		   // 获取当前job需要执行到本地还是Yarn
		   //最终:LocalClientProtocolProvider  ==> LocalJobRunner

截屏2020-04-18下午7.17.51

(2) return submitter.submitJobInternal(Job.this, cluster); 提交job
	    
	    <1> . checkSpecs(job); 检查job的输出路径。

截屏2020-04-18下午7.34.38

截屏2020-04-18下午7.33.46

	    <2> . Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf); 
	          生成Job提交的临时目录
	          /tmp/hadoop/mapred/staging/vanas1533793569/.staging
	          /private/tmp/hadoop/mapred/staging/vanas1533793569.staging

截屏2020-04-18下午7.38.23

	    <3> . JobID jobId = submitClient.getNewJobID();  //为当前Job生成Id
	    <4> . Path submitJobDir = new Path(jobStagingArea, jobId.toString()); // Job的提交路径 d:/tmp/hadoop/mapred/staging/Administrator1777320722/.staging/job_local1777320722_0001
	    <5> . copyAndConfigureFiles(job, submitJobDir);
		  ① rUploader.uploadResources(job, jobSubmitDir);
		     [1] uploadResourcesInternal(job, submitJobDir);  
		         {1}.submitJobDir = jtFs.makeQualified(submitJobDir); 
			     mkdirs(jtFs, submitJobDir, mapredSysPerms);
			    // 创建Job的提交路径 路径+jobid

截屏2020-04-18下午8.06.45

截屏2020-04-18下午8.11.33

	   <6> . int maps = writeSplits(job, submitJobDir); //生成切片信息 ,并返回切片的个数
	   <7> . conf.setInt(MRJobConfig.NUM_MAPS, maps);  //通过切片的个数设置MapTask的个数
	   <8> . writeConf(conf, submitJobFile);  //将当前Job相关的配置信息写到job提交路径下 

	         路径下:  job.split  job.splitmetainfo  job.xml    xxx.jar
	        
           <9> .status = submitClient.submitJob(
				 jobId, submitJobDir.toString(), job.getCredentials());
                //真正提交Job

	   <10> .  jtFs.delete(submitJobDir, true);  //等job执行完成后,删除Job的临时工作目录的内容

截屏2020-04-18下午8.17.35

本地没有jar包,集群就会有jar包

二、 MapTask的工作机制
1. 从Job提交流程的(2)--><9> 进去 
   Job job = new Job(JobID.downgrade(jobid), jobSubmitDir);  //构造真正执行的Job , LocalJobRunnber$Job
2. LocalJobRunnber$Job 的run()方法
   1)  TaskSplitMetaInfo[] taskSplitMetaInfos = 
          SplitMetaInfoReader.readSplitMetaInfo(jobId, localFs, conf, systemJobDir);
       // 读取job.splitmetainfo
   2)   int numReduceTasks = job.getNumReduceTasks();  // 获取ReduceTask个数

   3)  List<RunnableWithThrowable> mapRunnables = getMapTaskRunnables(
            taskSplitMetaInfos, jobId, mapOutputFiles); 
       // 根据切片的个数, 创建执行MapTask的 MapTaskRunnable
   4)   ExecutorService mapService = createMapExecutor();  // 创建线程池
       
   5)	runTasks(mapRunnables, mapService, "map");   //执行 MapTaskRunnable
	
   6)   因为Runnable提交给线程池执行,接下来会执行MapTaskRunnable的run方法。

   7)   执行 LocalJobRunner$Job$MapTaskRunnable 的run()方法.
        
	(1)  MapTask map = new MapTask(systemJobFile.toString(), mapId, taskId,
            info.getSplitIndex(), 1);   //创建MapTask对象

	(2)   map.run(localConf, Job.this);  //执行MapTask中的run方法
	      
	      <1> .runNewMapper(job, splitMetaInfo, umbilical, reporter); 
	          
		   ①  org.apache.hadoop.mapreduce.TaskAttemptContext taskContext =  JobContextImpl
		   ②  org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE> mapper =  WordConutMapper
		   ③  org.apache.hadoop.mapreduce.InputFormat<INKEY,INVALUE> inputFormat = TextInputFormat
		   ④  split = getSplitDetails(new Path(splitIndex.getSplitLocation()),
				splitIndex.getStartOffset());   // 重构切片对象
                       切片对象的信息 : file:/D:/input/inputWord/JaneEyre.txt:0+36306679

       ⑤  org.apache.hadoop.mapreduce.RecordReader<INKEY,INVALUE> input = MapTask$NetTrackingRecordReader

		   ⑥   output = new NewOutputCollector(taskContext, job, umbilical, reporter);  //构造缓冲区对象
			
			 [1] collector = createSortingCollector(job, reporter);  //获取缓冲区对象
			     MapTask$MapOutputBuffer

			    {1} . collector.init(context);  //初始化缓冲区对象
			           
				  1>>.final float spillper =
					job.getFloat(JobContext.MAP_SORT_SPILL_PERCENT, (float)0.8);
                                      // 溢写百分比  0.8
				  2>>.final int sortmb = job.getInt(MRJobConfig.IO_SORT_MB,
						 MRJobConfig.DEFAULT_IO_SORT_MB);    
				      // 缓冲区大小  100M

			    3>>.sorter = ReflectionUtils.newInstance(job.getClass(
						   MRJobConfig.MAP_SORT_CLASS, QuickSort.class,
						   IndexedSorter.class), job);
				     // 排序对象
				     // 排序使用的是快排,并且基于索引排序。
          4>> . // k/v serialization  // kv序列化
				  5>> . // output counters    // 计数器
				  6>> .  // compression       //  压缩
				  7>> .  // combiner          //  combiner

		  ⑦  mapper.run(mapperContext);    // 执行WordCountMapper中的run方法。 实际执行的是
		      WordCountMapper继承的Mapper中的run方法。
		      
		      [1] . 在Mapper中的run方法中 
		             map(context.getCurrentKey(), context.getCurrentValue(), context);
			     执行到WordCountMapper中的map方法。
		      [2] . 在WordCountMapper中的map方法中将kv写出
		            context.write(outK,outV);
三、 Shuffle流程(溢写,归并)
1. map中的kv持续往 缓冲区写, 会达到溢写条件,发生溢写,最后发生归并。
2. map中的 context.write(k,v) 
   1) . mapContext.write(key, value); 
       (1).  output.write(key, value);
           <1> collector.collect(key, value,
                        partitioner.getPartition(key, value, partitions));   
               // 将map写出的kv 计算好分区后,收集到缓冲区中。
           <2> . 当满足溢写条件后 ,开始发生溢写
	        			 startSpill();
               ① spillReady.signal(); //线程间通信,通知溢写线程开始溢写

	      			 ② 溢写线程调用 sortAndSpill() 方法发生溢写操作

	      			 ③ final SpillRecord spillRec = new SpillRecord(partitions);
                  final Path filename =
                  mapOutputFile.getSpillFileForWrite(numSpills, size);
                  out = rfs.create(filename)

		 						 //根据分区的个数,创建溢写文件,
		  /tmp/hadoop-Administrator/mapred/local/localRunner/Administrator/jobcache/job_local277309786_0001/attempt_local277309786_0001_m_000000_0/output/spill0.out

               ④ sorter.sort(MapOutputBuffer.this, mstart, mend, reporter);
								// 溢写前先排序

               ⑤ writer.close(); 通过writer进行溢写,溢写完成后,关闭流,可以查看磁盘中的溢写文件

               ⑥ if (totalIndexCacheMemory >= indexCacheMemoryLimit)  
                // create spill index file
                Path indexFilename =
                mapOutputFile.getSpillIndexFileForWrite(numSpills, partitions
                
	         			// 判断索引使用的内存空间是否超过限制的大小,如果超过也需要溢写到磁盘

               ⑦  map持续往缓冲区写,达到溢写条件,就继续溢写 ........ 可能整个过程中发生N次溢写。

               ⑦  MapTask中的runNewMapper 中 output.close(mapperContext); 
						// 假如上一次溢写完后,剩余进入的到缓冲区的数据没有达到溢写条件,那么当map中的所有的数据
            // 都已经处理完后,在关闭output时,会把缓冲区中的数据刷到磁盘中(其实就是没有达到溢写条件的数据也要写到磁盘)
                                                        
                  [1] collector.flush();  //刷写

                      {1} . sortAndSpill(); 通过溢写的方法进行剩余数据的刷写

                      {2} . 最后一次刷写完后,磁盘中会有N个溢写文件 
                                   spill0.out  spill1.out .... spillN.out

                      {3} . 归并 mergeParts();

                            >>1.  for(int i = 0; i < numSpills; i++) {
                            filename[i] = mapOutputFile.getSpillFile(i);
                            finalOutFileSize += rfs.getFileStatus(filename[i]).getLen();
                            }
                            //根据溢写的次数,得到要归并多少个溢写文件

                            >>2.  Path finalOutputFile =
					  mapOutputFile.getOutputFileForWrite(finalOutFileSize);

			          /tmp/hadoop-Administrator/mapred/local/localRunner/Administrator/jobcache/job_local1987086776_0001/attempt_local1987086776_0001_m_000000_0/output/file.out
			          
				  Path finalIndexFile =
					 mapOutputFile.getOutputIndexFileForWrite(finalIndexFileSize); 

                                  /tmp/hadoop-Administrator/mapred/local/localRunner/Administrator/jobcache/job_local1987086776_0001/attempt_local1987086776_0001_m_000000_0/output/file.out.index
                                 //生成最终存储数据的两个文件

                           >>3.  for (int parts = 0; parts < partitions; parts++) {
			        // 按照分区的, 进行归并。
                           
			  									 >>4 .awKeyValueIterator kvIter = Merger.merge(job, rfs,
					keyClass, valClass, codec,segmentList, mergeFactor,
					new Path(mapId.toString()),
					job.getOutputKeyComparator(), reporter, sortSegments,
					null, spilledRecordsCounter, sortPhase.phase(),TaskType.MAP);
				//归并操作
                          >>5 Writer<K, V> writer =
				 new Writer<K, V>(job, finalPartitionOut, keyClass, valClass, codec,
                               spilledRecordsCounter);
			       //通过writer写归并后的数据到磁盘
                          >>6 . if (combinerRunner == null || numSpills < minSpillsForCombine) {
					Merger.writeFile(kvIter, writer, reporter, job);
				 } else {
				 combineCollector.setWriter(writer);
				 combinerRunner.combine(kvIter, combineCollector);
				 }

				 //在归并时,如果有combine,且溢写的次数大于等于minSpillsForCombine的值3,会使用Combine
                          >>7.  for(int i = 0; i < numSpills; i++) {
					rfs.delete(filename[i],true);
				 }
				//归并完后,将溢写的文件删除
			 									  >> 8.  最后在磁盘中存储map处理完后的数据,等待reduce的拷贝。
			        									file.out  file.out.index  
		
四、 ReduceTask工作机制
1.在LocalJobRunner$Job中的run()方法中
	try {
          if (numReduceTasks > 0) {
	    //根据reduceTask的个数,创建对应个数的LocalJobRunner$Job$ReduceTaskRunnable
            List<RunnableWithThrowable> reduceRunnables = getReduceTaskRunnables(
                jobId, mapOutputFiles);
	   // 线程池
            ExecutorService reduceService = createReduceExecutor();
	    //将 ReduceTaskRunnable提交给线程池执行
            runTasks(reduceRunnables, reduceService, "reduce");
          }
    
1) . 执行	LocalJobRunner$Job$ReduceTaskRunnable 中的run方法
        
	(1) . ReduceTask reduce = new ReduceTask(systemJobFile.toString(),
              reduceId, taskId, mapIds.size(), 1); 
	      //创建ReduceTask对象
  (2) . reduce.run(localConf, Job.this); // 执行ReduceTask的run方法

	      <1> . runNewReducer(job, umbilical, reporter, rIter, comparator, 
                    keyClass, valueClass);
		    
		    [1] . org.apache.hadoop.mapreduce.TaskAttemptContext taskContext = TaskAttemptContextImpl
		    [2] . org.apache.hadoop.mapreduce.Reducer<INKEY,INVALUE,OUTKEY,OUTVALUE> reducer = WordCountReducer
		    [3] . org.apache.hadoop.mapreduce.RecordWriter<OUTKEY,OUTVALUE> trackedRW = ReduceTask$NewTrackingRecordWriter
		    [4] . reducer.run(reducerContext); 
		          //执行WordCountReducer的run方法 ,实际执行的是WordCountReducer继承的Reducer类中的run方法.

			  {1} .reduce(context.getCurrentKey(), context.getValues(), context); 
			       //执行到WordCountReducer中的 reduce方法.

        {2} . context.write(k,v) 将处理完的kv写出.

			  >>1 . reduceContext.write(key, value); 
				>>2 . output.write(key, value);
				>>3 . real.write(key,value);  // 通过RecordWriter将kv写出
				>>4 . out.write(NEWLINE);  //通过输出流将数据写到结果文件中

注意: 我们是在本地进行调试,所以N个MapTask 和 N个 ReduceTask没有并行的效果。
但是如果在集群上,N个 MapTask 和 N 个ReduceTask 是并行运行.

MapReduce工作流程

截屏2020-04-18下午8.59.46

截屏2020-04-18下午8.59.57

流程详解

上面的流程是整个MapReduce最全工作流程,但是Shuffle过程只是从第7步开始到第16步结束,具体Shuffle过程详解,如下:

1)MapTask收集我们的map()方法输出的kv对,放到内存缓冲区中

2)从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件

3)多个溢出文件会被合并成大的溢出文件

4)在溢出过程及合并的过程中,都要调用Partitioner进行分区和针对key进行排序

5)ReduceTask根据自己的分区号,去各个MapTask机器上取相应的结果分区数据

6)ReduceTask会取到同一个分区的来自不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)

7)合并成大文件后,Shuffle的过程也就结束了,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对Group,调用用户自定义的reduce()方法)

3.注意

Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。

缓冲区的大小可以通过参数调整,参数:io.sort.mb默认100M。

4.源码解析流程

context.write(k, NullWritable.get());
		output.write(key, value);
				collector.collect(key, value,partitioner.getPartition(key, value, partitions));
						HashPartitioner();
				collect()
						close()
								collect.flush()
										sortAndSpill()
												sort()   QuickSort
										mergeParts();
								collector.close();

MapTask工作机制

截屏2020-04-18下午9.00.13

(1)Read阶段:MapTask通过用户编写的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)Combine阶段:当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。

当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index。

在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并io.sort.factor(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。

让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。

ReduceTask工作机制

截屏2020-04-18下午9.00.24

(1)Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。

(2)Merge阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。

(3)Sort阶段:按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。

(4)Reduce阶段:reduce()函数将计算结果写到HDFS上。

设置ReduceTask并行度(个数)

ReduceTask的并行度同样影响整个Job的执行并发度和执行效率,但与MapTask的并发数由切片数决定不同,ReduceTask数量的决定是可以直接手动设置:

// 默认值是1,手动设置为4

job.setNumReduceTasks(4);

截屏2020-04-18下午11.00.13

Join多种应用

Reduce Join

Map端的主要工作:为来自不同表或文件的key/value对,打标签以区别不同来源的记录。然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出。

Reduce端的主要工作:在Reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在Map阶段已经打标志)分开,最后进行合并就ok了。

Reduce Join案例实操

需求:

通过将关联条件作为Map输出的key,将两表满足Join条件的数据并携带数据所来源的文件信息,发往同一个ReduceTask,在Reduce中进行数据的串联

截屏2020-04-18下午9.37.29

Bean

import org.apache.hadoop.io.Writable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

/**
 * 封装order.txt 和pd.txt的字段信息
 *
 * @author Vanas
 * @create 2020-04-18 2:33 下午
 */
public class OrderBean implements Writable {
    private String orderId;
    private String pid;
    private Integer amount;
    private String pname;

    private String flag; //标记数据来自哪个文件

    public OrderBean() {
    }

    public String getOrderId() {
        return orderId;
    }

    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }

    public String getPid() {
        return pid;
    }

    public void setPid(String pid) {
        this.pid = pid;
    }

    public Integer getAmount() {
        return amount;
    }

    public void setAmount(Integer 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;
    }


    public void write(DataOutput out) throws IOException {
        out.writeUTF(orderId);
        out.writeUTF(pid);
        out.writeInt(amount);
        out.writeUTF(pname);
        out.writeUTF(flag);

    }

    public void readFields(DataInput in) throws IOException {
        this.orderId = in.readUTF();
        this.pid = in.readUTF();
        this.amount = in.readInt();
        this.pname = in.readUTF();
        this.flag = in.readUTF();
    }

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

Mapper

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-18 2:42 下午
 */
public class ReduceJoinMapper extends Mapper<LongWritable, Text, Text, OrderBean> {

    private String currentSplitFileName;
    private OrderBean OutV = new OrderBean();
    private Text OutK = new Text();

    /**
     * 在MapTask开始时执行一次
     * 获取当前处理切片对应的文件是哪个
     *
     * @param context
     * @throws IOException
     * @throws InterruptedException
     */

    @Override
    protected void setup(Context context) throws IOException, InterruptedException {
//        获取当前切片对象
        InputSplit inputSplit = context.getInputSplit();
//        转换成FileSplit
        FileSplit currentSplit = (FileSplit) inputSplit;
//        获取当前处理的文件名
        currentSplitFileName = currentSplit.getPath().getName();

    }

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//        处理一行数据
        String line = value.toString();
        String[] splits = line.split("\t");


        if (currentSplitFileName.contains("order")) {
//            数据来源与order.txt
//            封装key
            OutK.set(splits[1]);
//             封装V
            OutV.setOrderId(splits[0]);
            OutV.setPid(splits[1]);
            OutV.setAmount(Integer.parseInt(splits[2]));
            OutV.setPname("");
            OutV.setFlag("order");
        } else {

//            数据来源pd.txt
//            封装K
            OutK.set(splits[0]);
//            封装v
            OutV.setPid(splits[0]);
            OutV.setPname(splits[1]);
            OutV.setOrderId("");
            OutV.setAmount(0);
            OutV.setFlag("pd");
        }


        context.write(OutK, OutV);


    }
}

Reducer

import org.apache.commons.beanutils.BeanUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

/**
 * @author Vanas
 * @create 2020-04-18 2:42 下午
 */
public class ReduceJoinReducer extends Reducer<Text, OrderBean, OrderBean, NullWritable> {
//  定义存储Order数据的OrderBean集合
    List<OrderBean> orders = new ArrayList<OrderBean>();
//  定义OrderBean,粗处pd的数据
    OrderBean pdBean = new OrderBean();


    @Override
    protected void reduce(Text key, Iterable<OrderBean> values, Context context) throws IOException, InterruptedException {
//        pid相同会进入一个方法
//        思路;将所有order的数据全部获取到,保存到一个集合中,把pd的数据获取到,保存在对象中
//        迭代保存order数据的集合,获取到每个order数据的orderBean对象,把pd对象中的pname设置到每个order数据的orderbean对象中
        for (OrderBean orderBean : values) {
            if ("order".equals(orderBean.getFlag())) {
//                深拷贝
                OrderBean currentOrderbean = new OrderBean();
                try {
                    BeanUtils.copyProperties(currentOrderbean, orderBean);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
                orders.add(currentOrderbean);
            } else {
//                pd
                try {
                    BeanUtils.copyProperties(pdBean, orderBean);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }

            }
        }
        for (OrderBean orderBean : orders) {
            orderBean.setPname(pdBean.getPname());
            context.write(orderBean, NullWritable.get());
        }
//        清空集合
        orders.clear();

    }
}

Driver

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-18 2:42 下午
 */
public class ReducerJoinDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
//        2。关联jar
        job.setJarByClass(ReducerJoinDriver.class);
//        3。关联Mapper 和 Reduce类
        job.setMapperClass(ReduceJoinMapper.class);
        job.setReducerClass(ReduceJoinReducer.class);
//        4。设置Mapper的输出key和value的类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(OrderBean.class);
//        5设置最终输出key和value的类型
        job.setOutputKeyClass(OrderBean.class);
        job.setOutputValueClass(NullWritable.class);
//        6。设置输入和输出路径
        FileInputFormat.setInputPaths(job, new Path("/Users/vanas/Desktop/input/join"));
        FileOutputFormat.setOutputPath(job, new Path("/Users/vanas/Desktop/output1"));

//        7。提交job
        job.waitForCompletion(true);
    }
}

截屏2020-04-18下午9.26.28

总结:

缺点:这种方式中,合并的操作是在Reduce阶段完成,Reduce端的处理压力太大,Map节点的运算负载则很低,资源利用率不高,且在Reduce阶段极易产生数据倾斜。

解决方案:Map端实现数据合并

Map Join

使用场景

Map Join适用于一张表十分小、一张表很大的场景

优点

思考:在Reduce端处理过多的表,非常容易产生数据倾斜。怎么办?

在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜。

具体办法:采用DistributedCache

(1)在Mapper的setup阶段,将文件读取到缓存集合中。

(2)在驱动函数中加载缓存。// 缓存普通文件到Task运行节点。

Map Join案例实操

需求分析

MapJoin适用于关联表中有小表的情形

截屏2020-04-18下午9.42.36

Mapper

import com.sun.org.apache.bcel.internal.generic.NEW;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;

import java.util.HashMap;
import java.util.Map;

/**
 * 小表文件加载到内存中,接下来每读取一条大表数据,就与内存中的小表的数据进行join,join完后直接写出
 *
 * @author Vanas
 * @create 2020-04-18 3:41 下午
 */
public class MapperJoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
    private Map<String, String> pdMap = new HashMap<String, String>();
    private Text outK = new Text();

    @Override
    protected void setup(Context context) throws IOException, InterruptedException {
//        小表数据加载内存中
//      获取在driver中设置的缓存文件
        URI[] cacheFiles = context.getCacheFiles();
        URI currentCacheFile = cacheFiles[0];
//        读取文件
        FileSystem fs = FileSystem.get(context.getConfiguration());
//        获取输入流
        FSDataInputStream in = fs.open(new Path(currentCacheFile.getPath()));
//          一次读取文件中一行数据
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        String line;
        while ((line = reader.readLine()) != null) {
//            切割
            String[] split = line.split("\t");
            pdMap.put(split[0], split[1]);
        }


    }

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//       读大数据
        String line = value.toString();
        String[] splits = line.split("\t");
        String currentPname = pdMap.get(splits[1]);
        String resultLine = splits[0] + "\t" + currentPname + "\t" + splits[2];
        outK.set(resultLine);

        context.write(outK, NullWritable.get());

    }
}

Driver

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

/**
 * @author Vanas
 * @create 2020-04-18 3:41 下午
 */
public class MapperJoinDriver {
    public static void main(String[] args) throws IOException, URISyntaxException, ClassNotFoundException, InterruptedException {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

//      设置缓存文件  可以设置多个
        job.addCacheFile(new URI("file:///Users/vanas/Desktop/input/join/pd.txt"));

        job.setJarByClass(MapperJoinDriver.class);
        job.setMapperClass(MapperJoinMapper.class);

        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(NullWritable.class);

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

//        设置ReduceTask的个数为0
        job.setNumReduceTasks(0);
        FileInputFormat.setInputPaths(job,new Path("/Users/vanas/Desktop/input/join/order.txt"));
        FileOutputFormat.setOutputPath(job,new Path("/Users/vanas/Desktop/output"));
        job.waitForCompletion(true);


    }
}

计数器应用

Hadoop为每个作业维护若干内置计数器,以描述多项指标。例如,某些计数器记录已处理的字节数和记录数,使用户可监控已处理的输入数据量和已产生的输出数据量。

多线程使用较多,单线程使用Debug

第二种方式用的多

截屏2020-04-18下午10.17.10

 context.getCounter("Map Join","map").increment(1);
 context.getCounter("Map Join","setup").increment(1);

数据清洗(ETL)

在运行核心业务MapReduce程序之前,往往要先对数据进行清洗,清理掉不符合用户要求的数据。清理的过程往往只需要运行Mapper程序,不需要运行Reduce程序。

企业中清洗相对复杂,如下

定义一个bean,用来记录日志数据中的各数据字段

public class LogBean {
	private String remote_addr;// 记录客户端的ip地址
	private String remote_user;// 记录客户端用户名称,忽略属性"-"
	private String time_local;// 记录访问时间与时区
	private String request;// 记录请求的url与http协议
	private String status;// 记录请求状态;成功是200
	private String body_bytes_sent;// 记录发送给客户端文件主体内容大小
	private String http_referer;// 用来记录从那个页面链接访问过来的
	private String http_user_agent;// 记录客户浏览器的相关信息

	private boolean valid = true;// 判断数据是否合法

	public String getRemote_addr() {
		return remote_addr;
	}

	public void setRemote_addr(String remote_addr) {
		this.remote_addr = remote_addr;
	}

	public String getRemote_user() {
		return remote_user;
	}

	public void setRemote_user(String remote_user) {
		this.remote_user = remote_user;
	}

	public String getTime_local() {
		return time_local;
	}

	public void setTime_local(String time_local) {
		this.time_local = time_local;
	}

	public String getRequest() {
		return request;
	}

	public void setRequest(String request) {
		this.request = request;
	}

	public String getStatus() {
		return status;
	}

	public void setStatus(String status) {
		this.status = status;
	}

	public String getBody_bytes_sent() {
		return body_bytes_sent;
	}

	public void setBody_bytes_sent(String body_bytes_sent) {
		this.body_bytes_sent = body_bytes_sent;
	}

	public String getHttp_referer() {
		return http_referer;
	}

	public void setHttp_referer(String http_referer) {
		this.http_referer = http_referer;
	}

	public String getHttp_user_agent() {
		return http_user_agent;
	}

	public void setHttp_user_agent(String http_user_agent) {
		this.http_user_agent = http_user_agent;
	}

	public boolean isValid() {
		return valid;
	}

	public void setValid(boolean valid) {
		this.valid = valid;
	}

	@Override
	public String toString() {

		StringBuilder sb = new StringBuilder();
		sb.append(this.valid);
		sb.append("\001").append(this.remote_addr);
		sb.append("\001").append(this.remote_user);
		sb.append("\001").append(this.time_local);
		sb.append("\001").append(this.request);
		sb.append("\001").append(this.status);
		sb.append("\001").append(this.body_bytes_sent);
		sb.append("\001").append(this.http_referer);
		sb.append("\001").append(this.http_user_agent);
		
		return sb.toString();
	}
}
import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

public class LogMapper extends Mapper<LongWritable, Text, Text, NullWritable>{
	Text k = new Text();
	
	@Override
	protected void map(LongWritable key, Text value, Context context)	throws IOException, InterruptedException {

		// 1 获取1行
		String line = value.toString();
		
		// 2 解析日志是否合法
		LogBean bean = parseLog(line);
		
		if (!bean.isValid()) {
			return;
		}
		
		k.set(bean.toString());
		
		// 3 输出
		context.write(k, NullWritable.get());
	}

	// 解析日志
	private LogBean parseLog(String line) {

		LogBean logBean = new LogBean();
		
		// 1 截取
		String[] fields = line.split(" ");
		
		if (fields.length > 11) {

			// 2封装数据
			logBean.setRemote_addr(fields[0]);
			logBean.setRemote_user(fields[1]);
			logBean.setTime_local(fields[3].substring(1));
			logBean.setRequest(fields[6]);
			logBean.setStatus(fields[8]);
			logBean.setBody_bytes_sent(fields[9]);
			logBean.setHttp_referer(fields[10]);
			
			if (fields.length > 12) {
				logBean.setHttp_user_agent(fields[11] + " "+ fields[12]);
			}else {
				logBean.setHttp_user_agent(fields[11]);
			}
			
			// 大于400,HTTP错误
			if (Integer.parseInt(logBean.getStatus()) >= 400) {
				logBean.setValid(false);
			}
		}else {
			logBean.setValid(false);
		}
		
		return logBean;
	}
}

MapReduce扩展案例

倒排索引案例(多job串联)

需求:

截屏2020-04-19下午9.12.27

Mapper-1

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;

import java.io.IOException;

/**
 * 第一次处理
 *
 * @author Vanas
 * @create 2020-04-19 4:55 下午
 */
public class OneIndexMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
    String name;
    Text outK = new Text();
    IntWritable outV = new IntWritable();

    @Override
    protected void setup(Context context){

//        获取文件名称
        FileSplit split = (FileSplit) context.getInputSplit();
        name = split.getPath().getName();

    }

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String line = value.toString();
        String[] splits = line.split(" ");
        for (String word : splits) {
            outK.set(word + "--" + name);
            outV.set(1);
            context.write(outK, outV);
        }


    }
}

Reducer-1

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import sun.tools.jconsole.OutputViewer;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-19 4:55 下午
 */
public class OneIndexReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    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);
    }
}

Driver-1

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-19 4:56 下午
 */
public class OneIndexDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        job.setJarByClass(OneIndexDriver.class);

        job.setMapperClass(OneIndexMapper.class);
        job.setReducerClass(OneIndexReducer.class);

        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);

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

        FileInputFormat.setInputPaths(job,new Path("/Users/vanas/Desktop/input/test1"));
        FileOutputFormat.setOutputPath(job,new Path("/Users/vanas/Desktop/output"));

        job.waitForCompletion(true);
    }
}

Mapper-2

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-19 5:47 下午
 */
public class TwoIndexMapper extends Mapper<LongWritable, Text, Text, Text> {
    Text OutK = new Text();
    Text OutV = new Text();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String line = value.toString();
        String[] splits = line.split("--");
        OutK.set(splits[0]);

        String[] split = splits[1].split("\t");
        OutV.set(split[0] + "-->" + split[1] + " ");

        context.write(OutK, OutV);


    }
}

Reducer-2

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-19 5:47 下午
 */
public class TwoIndexReducer extends Reducer<Text, Text, Text, Text> {


    Text outV = new Text();

    @Override
    protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
        String str = "";
        for (Text value : values) {
            str += value;
        }
        outV.set(str);

        context.write(key, outV);
    }
}

Driver-2

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;


import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-19 5:47 下午
 */
public class TwoIndexDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        job.setJarByClass(TwoIndexDriver.class);


        job.setMapperClass(TwoIndexMapper.class);
        job.setReducerClass(TwoIndexReducer.class);

        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(Text.class);

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

        FileInputFormat.setInputPaths(job,new Path("/Users/vanas/Desktop/output/part-r-00000"));
        FileOutputFormat.setOutputPath(job,new Path("/Users/vanas/Desktop/output1"));

        job.waitForCompletion(true);
    }
}

TopN案例

需求:

截屏2020-04-19下午9.15.34

Bean

import org.apache.hadoop.io.WritableComparable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-19 6:22 下午
 */
public class FlowBean implements WritableComparable<FlowBean> {
    private long phone;
    private long upFlow;
    private long downFlow;
    private long sumFlow;

    public FlowBean() {
    }

    public long getPhone() {
        return phone;
    }

    public void setPhone(long phone) {
        this.phone = phone;
    }

    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;
    }


    @Override
    public String toString() {
        return phone + "\t" + upFlow + "\t" + downFlow + "\t" + sumFlow;
    }

    public int compareTo(FlowBean o) {
        return -(int) (this.sumFlow - o.sumFlow);

    }

    public void write(DataOutput out) throws IOException {
        out.writeLong(phone);
        out.writeLong(upFlow);
        out.writeLong(downFlow);
        out.writeLong(sumFlow);

    }

    public void readFields(DataInput in) throws IOException {
        phone = in.readLong();
        upFlow = in.readLong();
        downFlow = in.readLong();
        sumFlow = in.readLong();
    }
}

Mapper

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-19 6:22 下午
 */
public class TopNMapper extends Mapper<LongWritable, Text, FlowBean, NullWritable> {
    FlowBean outK = new FlowBean();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String line = value.toString();
        String[] split = line.split("\t");
        outK.setPhone(Long.parseLong(split[0]));
        outK.setUpFlow(Long.parseLong(split[1]));
        outK.setDownFlow(Long.parseLong(split[2]));
        outK.setSumFlow(Long.parseLong(split[3]));

        context.write(outK, NullWritable.get());
    }
}

Reducer

import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-19 6:22 下午
 */
public class TopNReducer extends Reducer<FlowBean, NullWritable, FlowBean, NullWritable> {
    int i = 0;

    @Override
    protected void reduce(FlowBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
        while (i < 10) {

            context.write(key, NullWritable.get());
            i++;
            break;
        }
    }
}

Driver

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-19 6:23 下午
 */
public class TopNDriver {
    public static void main(String[] args) throws InterruptedException, IOException, ClassNotFoundException {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
        job.setJarByClass(TopNDriver.class);

        job.setMapperClass(TopNMapper.class);
        job.setReducerClass(TopNReducer.class);

        job.setMapOutputKeyClass(FlowBean.class);
        job.setMapOutputValueClass(NullWritable.class);

        job.setOutputKeyClass(FlowBean.class);
        job.setOutputValueClass(NullWritable.class);


        FileInputFormat.setInputPaths(job, new Path("/Users/vanas/Desktop/input/top10.txt"));
        FileOutputFormat.setOutputPath(job, new Path("/Users/vanas/Desktop/output2"));

        job.waitForCompletion(true);

    }
}

找博客共同好友案例

需求:

以下是博客的好友列表数据,冒号前是一个用户,冒号后是该用户的所有好友(数据中的好友关系是单向的)

求出哪些人两两之间有共同好友,及他俩的共同好友都有谁?

需求分析

先求出A、B、C、….等是谁的好友

Mapper-1

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

/**
 * xx是谁的好友
 *
 * @author Vanas
 * @create 2020-04-19 7:43 下午
 */
public class OneShareFriendsMapper extends Mapper<LongWritable, Text, Text, Text> {

    Text outK = new Text();
    Text outV = new Text();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String line = value.toString();
        String[] splits = line.split(":");

        String person = splits[0];
        String[] friends = splits[1].split(",");
        outV.set(person);
        for (String friend : friends) {
            outK.set(friend);
            context.write(outK, outV);

        }

    }

}

Reducer-1

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-19 7:44 下午
 */
public class OneShareFriendsReducer extends Reducer<Text,Text,Text,Text> {
    Text outV = new Text();
    @Override
    protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {

        StringBuffer str = new StringBuffer();

        for (Text value : values) {
            str.append(value).append(",");
        }
        outV.set(str.toString());
        context.write(key,outV);

    }
}

Driver-1

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-19 7:44 下午
 */
public class OneShareFriendsDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        job.setJarByClass(OneShareFriendsDriver.class);

        job.setMapperClass(OneShareFriendsMapper.class);
        job.setReducerClass(OneShareFriendsReducer.class);

        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(Text.class);

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

        FileInputFormat.setInputPaths(job, new Path("/Users/vanas/Desktop/input/friends.txt"));
        FileOutputFormat.setOutputPath(job, new Path("/Users/vanas/Desktop/output3"));

        job.waitForCompletion(true);


    }
}

Mapper-2

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;
import java.util.Arrays;

/**
 * @author Vanas
 * @create 2020-04-19 8:41 下午
 */
public class TwoShareFriendsMapper extends Mapper<LongWritable, Text, Text, Text> {

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String line = value.toString();
        String[] friend_persons = line.split("\t");

        String friend = friend_persons[0];
        String[] persons = friend_persons[1].split(",");

        Arrays.sort(persons);

        for (int i = 0; i < persons.length - 1; i++) {
            for (int j = i + 1; j < persons.length; j++) {
                // 发出 <人-人,好友> ,这样,相同的“人-人”对的所有好友就会到同1个reduce中去
                context.write(new Text(persons[i] + "-" + persons[j]), new Text(friend));
            }
        }

    }
}

Reducer-2

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-19 8:41 下午
 */
public class TwoShareFriendsReducer extends Reducer<Text, Text, Text, Text> {

    @Override
    protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
        StringBuffer sb = new StringBuffer();

        for (Text friend : values) {
            sb.append(friend).append(" ");
        }

        context.write(key, new Text(sb.toString()));
    }
}

Driver-2

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

/**
 * @author Vanas
 * @create 2020-04-19 8:42 下午
 */
public class TwoShareFriendsDriver {

    public static void main(String[] args) throws InterruptedException, IOException, ClassNotFoundException {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
        job.setJarByClass(TwoShareFriendsDriver.class);

        job.setMapperClass(TwoShareFriendsMapper.class);
        job.setReducerClass(TwoShareFriendsReducer.class);
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(Text.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(Text.class);

        FileInputFormat.setInputPaths(job, new Path("/Users/vanas/Desktop/output3/part-r-00000"));
        FileOutputFormat.setOutputPath(job, new Path("/Users/vanas/Desktop/output4"));


        job.waitForCompletion(true);

    }
}

截屏2020-04-19下午9.31.56

注意split切分时后面有换行

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值