3.1.3 Hadoop,MapReduce, 序列化,M-R运行原理/并行度,Shuffle机制,分区与task数量,Combiner预聚合,全排序,分区排序,输入输出数据模式设置,压缩机制

目录

第六部分 MapReduce编程框架

第 1 节 MapReduce思想

第 2 节 官⽅WordCount案例源码解析

Hadoop序列化

Java基本类型与Hadoop常⽤序列化类型

第 3 节 MapReduce编程规范及示例编写

3.1 Mapper类

3.2 Reducer类

3.3 Driver阶段

3.4 WordCount代码实现

3.4.2 具体步骤

第 4 节 序列化Writable接⼝

4.1 实现Writable序列化步骤

4.2 Writable接⼝案例

第 5 节 MapReduce原理分析

5.1 MapTask运⾏机制详解

5.2 MapTask的并⾏度

 切⽚机制源码阅读

5.3 ReduceTask ⼯作机制

5.4 ReduceTask并⾏度

5.5 Shuffle机制

5.5.1 MapReduce的分区与reduceTask的数量

5.5.2 MapReduce中的Combiner

5.6 MapReduce中的排序

5.6.1 WritableComparable  全排序 

5.6.2 GroupingComparator  分区排序(默认的分区规则,区内有序) 面试题

5.7 MapReduce读取和输出数据

5.7.1 InputFormat

5.7.2 OutputFormat

5.8 shuffle阶段数据的压缩机制

5.8.1 hadoop当中⽀持的压缩算法

5.8.2 压缩位置

5.8.3 压缩配置⽅式

5.8.4 压缩案例


第六部分 MapReduce编程框架

第 1 节 MapReduce思想

MapReduce思想在⽣活中处处可⻅。我们或多或少都曾接触过这种思想。MapReduce的思想核⼼是分⽽治之,充分利⽤了并⾏处理的优势。

即使是发布过论⽂实现分布式计算的⾕歌也只是实现了这种思想,⽽不是⾃⼰原创。

MapReduce任务过程是分为两个处理阶段:

Map阶段:Map阶段的主要作⽤是“分”,即把复杂的任务分解为若⼲个“简单的任务”来并⾏处理。
Map阶段的这些任务可以并⾏计算,彼此间没有依赖关系。

Reduce阶段:Reduce阶段的主要作⽤是“合”,即对map阶段的结果进⾏全局汇总。

再次理解MapReduce的思想

第 2 节 官⽅WordCount案例源码解析

经过查看分析官⽅WordCount案例源码我们发现⼀个统计单词数量的MapReduce程序的代码由三个部分组成,
Mapper类
Reducer类
运⾏作业的代码(Driver)

Mapper类继承了org.apache.hadoop.mapreduce.Mapper类重写了其中的map⽅法,Reducer类继承了org.apache.hadoop.mapreduce.Reducer类重写了其中的reduce⽅法。

重写的Map⽅法作⽤:map⽅法其中的逻辑就是⽤户希望mr程序map阶段如何处理的逻辑;

重写的Reduce⽅法作⽤:reduce⽅法其中的逻辑是⽤户希望mr程序reduce阶段如何处理的逻辑;

Hadoop序列化

为什么进⾏序列化?
序列化主要是我们通过⽹络通信传输数据时或者把对象持久化到⽂件,需要把对象序列化成⼆进制的结构。
观察源码时发现⾃定义Mapper类与⾃定义Reducer类都有泛型类型约束,⽐如⾃定义Mapper有四个形参类型,但是形参类型并不是常⻅的java基本类型。

为什么Hadoop要选择建⽴⾃⼰的序列化格式⽽不使⽤java⾃带serializable?
序列化在分布式程序中⾮常重要,在Hadoop中,集群中多个节点的进程间的通信是通过RPC(远程过程调⽤:Remote Procedure Call)实现;RPC将消息序列化成⼆进制流发送到远程节点,远程节点再将接收到的⼆进制数据反序列化为原始的消息,因此RPC往往追求如下特点:

紧凑:数据更紧凑,能充分利⽤⽹络带宽资源
快速:序列化和反序列化的性能开销更低

Hadoop使⽤的是⾃⼰的序列化格式Writable,它⽐java的序列化serialization更紧凑速度更快。⼀个对象使⽤Serializable序列化后,会携带很多额外信息⽐如校验信息,Header,继承体系等。

Java基本类型与Hadoop常⽤序列化类型

第 3 节 MapReduce编程规范及示例编写

3.1 Mapper类

⽤户⾃定义⼀个Mapper类继承Hadoop的Mapper类
Mapper的输⼊数据是KV对的形式(类型可以⾃定义)
Map阶段的业务逻辑定义在map()⽅法中
Mapper的输出数据是KV对的形式(类型可以⾃定义)
注意:map()⽅法是对输⼊的⼀个KV对调⽤⼀次!!

3.2 Reducer类

⽤户⾃定义Reducer类要继承Hadoop的Reducer类
Reducer的输⼊数据类型对应Mapper的输出数据类型(KV对)
Reducer的业务逻辑写在reduce()⽅法中
Reduce()⽅法是对相同K的⼀组KV对调⽤执⾏⼀次

3.3 Driver阶段

创建提交YARN集群运⾏的Job对象,其中封装了MapReduce程序运⾏所需要的相关参数⼊输⼊数据路径,输出数据路径等,也相当于是⼀个YARN集群的客户端,主要作⽤就是提交我们MapReduce程序运⾏。

3.4 WordCount代码实现

3.4.1 需求

在给定的⽂本⽂件中统计输出每⼀个单词出现的总次数
输⼊数据:wc.txt;
输出:

apache 2
clickhouse 2
hadoop 1
mapreduce 1
spark 2
xiaoming 1

3.4.2 具体步骤

按照MapReduce编程规范,分别编写Mapper,Reducer,Driver。

1. 新建maven⼯程

 导⼊hadoop依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.lagou.hadoop</groupId>
    <artifactId>wordcount</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.8.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-common</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-hdfs</artifactId>
            <version>2.9.2</version>
        </dependency>
    </dependencies>

    <!--maven打包插件 -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-assembly-plugin </artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>

                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>


</project>

注意:以上依赖第⼀次需要联⽹下载!!

添加log4j.properties

log4j.rootLogger=info, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n

2. 整体思路梳理(仿照源码)

Map阶段:
1. map()⽅法中把传⼊的数据转为String类型
2. 根据空格切分出单词
3. 输出<单词,1>

Reduce阶段:
1. 汇总各个key(单词)的个数,遍历value数据进⾏累加
2. 输出key的总数

Driver
1. 获取配置⽂件对象,获取job对象实例
2. 指定程序jar的本地路径
3. 指定Mapper/Reducer类
4. 指定Mapper输出的kv数据类型
5. 指定最终输出的kv数据类型
6. 指定job处理的原始数据路径
7. 指定job输出结果路径
8. 提交作业

3. 编写Mapper类

package com.lagou.mr.wc;

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;

//需求:单词计数
//1 继承Mapper类
//2 Mapper类的泛型参数:共4个,2对kv
//    第一对kv: map输入参数类型
//       LongWritable, Text-->文本偏移量(后面不会用到),一行文本内容
//    第二队kv:map输出参数类型
//       Text, IntWritable-->单词,1
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
    //3 重写Mapper类的map方法
    /*
    1 接收到文本内容,转为String类型
    2 按照空格进行切分
    3 输出<单词,1>
     */

    //提升为全局变量,避免每次执行map方法都执行此操作
    final Text word = new Text();
    final IntWritable one = new IntWritable(1);

    // LongWritable, Text-->文本偏移量,一行文本内容,map方法的输入参数,一行文本就调用一次map方法
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//        1 接收到文本内容,转为String类型
        final String str = value.toString();
//        2 按照空格进行切分
        final String[] words = str.split(" ");
//        3 输出<单词,1>

        //遍历数据
        for (String s : words) {
            word.set(s);
            context.write(word, one);
        }

    }
}

继承的Mapper类型选择新版本API:

4. 编写Reducer类

package com.lagou.mr.wc;

import com.sun.org.apache.bcel.internal.generic.NEW;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

//继承的Reducer类有四个泛型参数,2对kv
//第一对kv:类型要与Mapper输出类型一致:Text, IntWritable
//第二对kv:自己设计决定输出的结果数据是什么类型:Text, IntWritable
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {

    //1 重写reduce方法

    // Text key:map方法输出的key,本案例中就是单词,
    // Iterable<IntWritable> values:一组key相同的kv的value组成的集合
    /*
        假设map方法:hello 1;hello 1;hello 1
        reduce的key和value是什么?
        key:hello,
        values:<1,1,1>

        假设map方法输出:hello 1;hello 1;hello 1,hadoop 1, mapreduce 1,hadoop 1
        reduce的key和value是什么?
        reduce方法何时调用:一组key相同的kv中的value组成集合然后调用一次reduce方法
        第一次:key:hello ,values:<1,1,1>
        第二次:key:hadoop ,values<1,1>
        第三次:key:mapreduce ,values<1>
     */
    IntWritable total = new IntWritable();

    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        //2 遍历key对应的values,然后累加结果
        int sum = 0;
        for (IntWritable value : values) {
            int i = value.get();
            sum += 1;
        }
        // 3 直接输出当前key对应的sum值,结果就是单词出现的总次数
        total.set(sum);
        context.write(key, total);
    }
}

选择继承的Reducer类


5. 编写Driver驱动类

package com.lagou.mr.wc;

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.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

//封装任务并提交运行
public class WordCountDriver {

    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        /*
        1. 获取配置文件对象,获取job对象实例
        2. 指定程序jar的本地路径
        3. 指定Mapper/Reducer类
        4. 指定Mapper输出的kv数据类型
        5. 指定最终输出的kv数据类型
        6. 指定job处理的原始数据路径
        7. 指定job输出结果路径
        8. 提交作业
         */
//        1. 获取配置文件对象,获取job对象实例
        final Configuration conf = new Configuration();
        final Job job = Job.getInstance(conf, "WordCountDriver");

//        2. 指定程序jar的本地路径
        job.setJarByClass(WordCountDriver.class);

//        3. 指定Mapper/Reducer类
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);

//        4. 指定Mapper输出的kv数据类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);

//        5. 指定最终输出的kv数据类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

//        6. 指定job处理的原始数据路径
        FileInputFormat.setInputPaths(job, new Path(args[0])); //指定读取数据的原始路径

//        7. 指定job输出结果路径
        FileOutputFormat.setOutputPath(job, new Path(args[1])); //指定结果数据输出路径

//        8. 提交作业
        final boolean flag = job.waitForCompletion(true);
        //jvm退出:正常退出0,非0值则是错误退出
        System.exit(flag ? 0 : 1);
    }
}

本次运行中出现了java.lang.UnsatisfiedLinkError: org.apache.hadoop.io.nativeio.NativeIO$Windows.access0(Ljava/lang/String;I)Z 异常, 解决方法: hadoop的bin文件夹, system32文件夹中可能缺少 hadoop.dll 文件, 补充对应版本

6. 运⾏任务

 本地模式

直接Idea中运⾏驱动类即可

idea运⾏需要传⼊参数:

选择editconfiguration

在program arguments设置参数

运⾏结束,去到输出结果路径查看结果


注意本地idea运⾏mr任务与集群没有任何关系,没有提交任务到yarn集群,是在本地使⽤多线程⽅式模拟的mr的运⾏。


 Yarn集群模式

把程序打成jar包,改名为wc.jar;上传到Hadoop集群
选择合适的Jar包


准备原始数据⽂件,上传到HDFS的路径,不能是本地路径,因为跨节点运⾏⽆法获取数据!!

启动Hadoop集群(Hdfs,Yarn)

 使⽤Hadoop 命令提交任务运⾏

hadoop jar wc.jar com.lagou.wordcount.WordcountDriver /user/lagou/input /user/lagou/output

Yarn集群任务运⾏成功展示图

第 4 节 序列化Writable接⼝

基本序列化类型往往不能满⾜所有需求,⽐如在Hadoop框架内部传递⼀个⾃定义bean对象,那么该对象就需要实现Writable序列化接⼝。

4.1 实现Writable序列化步骤

1. 必须实现Writable接⼝

2. 反序列化时,需要反射调⽤空参构造函数,所以必须有空参构造

public CustomBean() {

     super();
}

3. 重写序列化⽅法

@Override
public void write(DataOutput out) throws IOException {

  ....
}

4. 重写反序列化⽅法

@Override
public void readFields(DataInput in) throws IOException {

      ....
}

5. 反序列化的字段顺序和序列化字段的顺序必须完全⼀致
6. ⽅便展示结果数据,需要重写bean对象的toString()⽅法,可以⾃定义分隔符
7. 如果⾃定义Bean对象需要放在Mapper输出KV中的K,则该对象还需实现Comparable接⼝,因为因为MapReduce框中的Shuffle过程要求对key必须能排序!!

排序内容专⻔案例讲解!!

@Override
public int compareTo(CustomBean o) {

      // ⾃定义排序规则
      return this.num > o.getNum() ? -1 : 1;
}

4.2 Writable接⼝案例

1. 需求

统计每台智能⾳箱设备内容播放时⻓
原始⽇志格式

  001     001577c3     kar_890809       120.196.100.99     1116                 954               200    
⽇志id     设备id    appkey(合作硬件⼚商)     ⽹络ip         ⾃有内容时⻓(秒)      第三⽅内容时⻓(秒)     ⽹络状态码

输出结果

001577c3     11160                 9540             20700
设备id     ⾃有内容时⻓(秒)     第三⽅内容时⻓(秒)         总时⻓

mr编程技巧总结

map 方法输出的kv以及具体类型如何确定?   结合业务设计Map输出的key和v,key相同则去往同⼀个reducer调用reduce方法

reduce方法中输入参数kv, key: map方法输出的某个key, value: 是一个集合, 其中数据都来自map输出的kv, 且是key相同的所有value的集合

思路分析

Map:

① 读取一行数据, 拆分出设备ID

② 抽取出自有内容时长, 第三方时长, 设备id

③ 输出: key:设备id,  value: 封装bean对象, 携带自由时长, 第三方时长, id

④ 自定义bean对象作为value输出, 需要实现Writable接口

Reduce

① 遍历迭代器, 累计时长, 直接输出即可


2. 编写MapReduce程序

1. 创建SpeakBean对象

package com.lagou.mr.speak;

import org.apache.hadoop.io.Writable;

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

// 这个类型是map输出kv中value的类型,需要实现writable序列化接口
public class SpeakBean implements Writable {

    // 定义属性
    private Long selfDuration;      //自有内容时长
    private Long thirdPartDuration; //第三方内容时长
    private String deviceId;        //设备id
    private Long sumDuration;       //总时长

    // 准备一个空参构造
    public SpeakBean() {
    }

    // 序列化方法: 就是把内容输出到网络或者文本中, 本质就是流
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeLong(selfDuration);
        out.writeLong(thirdPartDuration);
        out.writeUTF(deviceId);
        out.writeLong(sumDuration);
    }

    //有参构造 
    public SpeakBean(Long selfDuration, Long thirdPartDuration, String deviceId) {
        this.selfDuration = selfDuration;
        this.thirdPartDuration = thirdPartDuration;
        this.deviceId = deviceId;
        this.sumDuration = this.selfDuration + this.thirdPartDuration;
    }

    //反序列化方法
    @Override
    public void readFields(DataInput in) throws IOException {
        this.selfDuration = in.readLong();      //自有时长
        this.thirdPartDuration = in.readLong(); //第三方时长
        this.deviceId = in.readUTF();           //设备id
        this.sumDuration = in.readLong();       //总时长
    }
    
    //为了方便观察数据,重写toString()方法
    @Override
    public String toString() {
        return
                selfDuration +
                        "\t" + thirdPartDuration +
                        "\t" + deviceId + "\t" + sumDuration;
    }

    ...
}

2. 编写Mapper类

package com.lagou.mr.speak;

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

import java.io.IOException;

//四个参数:分为两对kv
//  第一对kv:map输入参数的kv类型; k-->一行文本偏移量,    v--> 一行文本内容
//  第二对kv:map输出参数kv类型;  k-->map输出的key类型,  v--> map输出的value类型
public class SpeakMapper extends Mapper<LongWritable, Text, Text, SpeakBean> {
    /*
    1 转换接收到的text数据为String
    2 按照制表符进行切分;得到自有内容时长,第三方内容时长,设备id,封装为SpeakBean
    3 直接输出:k-->设备id,value-->speakbean
     */
    
    Text device_id = new Text();
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//        1 转换接收到的text数据为String
        final String line = value.toString();
        
//        2 按照制表符进行切分;得到自有内容时长,第三方内容时长,设备id,封装为SpeakBean
        final String[] fields = line.split("\t");
        //自有内容时长
        String selfDuration = fields[fields.length - 3];
        //第三方内容时长
        String thirdPartDuration = fields[fields.length - 2];
        //设备id
        String deviceId = fields[1];
        // 下面的Duration是String类型, 因此需要用parseLong转成Long类型
        final SpeakBean bean = new SpeakBean(Long.parseLong(selfDuration), Long.parseLong(thirdPartDuration), deviceId);
        
//        3 直接输出:k-->设备id,value-->speakbean
        device_id.set(deviceId);
        context.write(device_id, bean);
    }
}

3. 编写Reducer

package com.lagou.mr.speak;

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

import java.io.IOException;

public class SpeakReducer extends Reducer<Text, SpeakBean, Text, SpeakBean> {

    @Override
    protected void reduce(Text key, Iterable<SpeakBean> values, Context context) throws IOException, InterruptedException {

        //定义时长累加的初始值
        Long self_duration = 0L;
        Long third_part_duration = 0L;

        //reduce方法的key:map输出的某一个key
        //reduce方法的value:map输出的kv对中相同key的value组成的一个集合
        //reduce 逻辑:遍历迭代器累加时长即可
        for (SpeakBean bean : values) {
            final Long selfDuration = bean.getSelfDuration();
            final Long thirdPartDuration = bean.getThirdPartDuration();
            self_duration += selfDuration;
            third_part_duration += thirdPartDuration;
        }
        //输出,封装成一个bean对象输出
        final SpeakBean bean = new SpeakBean(self_duration, third_part_duration, key.toString());
        context.write(key, bean);
    }
}

4. 编写驱动

package com.lagou.mr.speak;


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;

public class SpeakDriver {

    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {

        final Configuration conf = new Configuration();
        final Job job = Job.getInstance(conf, "speakDriver");
        //设置jar包本地路径
        job.setJarByClass(SpeakDriver.class);
        //使用的mapper和reducer
        job.setMapperClass(SpeakMapper.class);
        job.setReducerClass(SpeakReducer.class);
        //map的输出kv类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(SpeakBean.class);
        //设置reduce输出
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(SpeakBean.class);
        //读取的数据路径
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));
        //提交任务
        final boolean flag = job.waitForCompletion(true);
        System.exit(flag ? 0 : 1);
    }
}

mr编程技巧总结

结合业务设计Map输出的key和v,利⽤key相同则去往同⼀个reduce的特点!!

map()⽅法中获取到只是⼀⾏⽂本数据尽量不做聚合运算

reduce()⽅法的参数要清楚含义

第 5 节 MapReduce原理分析

5.1 MapTask运⾏机制详解

MapTask流程

详细步骤:

1. ⾸先,读取数据组件InputFormat(默认TextInputFormat)会通过getSplits⽅法对输⼊⽬录中⽂件进⾏逻辑切⽚规划得到splits,有多少个split就对应启动多少个MapTask。split与block的对应关系默认是⼀对⼀。

2. 将输⼊⽂件切分为splits之后,由RecordReader对象(默认LineRecordReader)进⾏读取,以\n作为分隔符,读取⼀⾏数据,返回<key,value>。Key表示每⾏⾸字符偏移值,value表示这⼀⾏⽂本内容。

3. 读取split返回<key,value>,进⼊⽤户⾃⼰继承的Mapper类中,执⾏⽤户重写的map函数。RecordReader读取⼀⾏这⾥调⽤⼀次。

4. map逻辑完之后,将map的每条结果通过context.write进⾏collect数据收集。在collect中,会先对其进⾏分区处理,默认使⽤HashPartitioner。
MapReduce提供Partitioner接⼝,它的作⽤就是根据key或value及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理。默认对key hash后再以reduce task数量取模。默认的取模⽅式只是为了平均reduce的处理能⼒,如果⽤户⾃⼰对Partitioner有需求,可以订制并设置到job上。

5. 接下来,会将数据写⼊内存,内存中这⽚区域叫做环形缓冲区,缓冲区的作⽤是批量收集map结果,减少磁盘IO的影响。我们的key/value对以及Partition的结果都会被写⼊缓冲区。当然写⼊之前,key与value值都会被序列化成字节数组。
环形缓冲区其实是⼀个数组,数组中存放着key、value的序列化数据和key、value的元数据信息,包括partition、key的起始位置、value的起始位置以及value的⻓度。环形结构是⼀个抽象概念。
缓冲区是有⼤⼩限制,默认是100MB。当map task的输出结果很多时,就可能会撑爆内存,所以需要在⼀定条件下将缓冲区中的数据临时写⼊磁盘,然后重新利⽤这块缓冲区。这个从内存往磁盘写数据的过程被称为Spill,中⽂可译为溢写。这个溢写是由单ᇿ线程来完成,不影响往缓冲区写map结果的线程。溢写线程启动时不应该阻⽌map的结果输出,所以整个缓冲区有个溢写的⽐例spill.percent。这个⽐例默认是0.8,也就是当缓冲区的数据已经达到阈值(buffer size * spill
percent = 100MB * 0.8 = 80MB),溢写线程启动,锁定这80MB的内存,执⾏溢写过程。Maptask的输出结果还可以往剩下的20MB内存中写,互不影响.

6、当溢写线程启动后,需要对这80MB空间内的key做排序(Sort)。排序是MapReduce模型默认的⾏为!
如果job设置过Combiner,那么现在就是使⽤Combiner的时候了。将有相同key的key/value对的value加起来,减少溢写到磁盘的数据量。Combiner会优化MapReduce的中间结果,所以它在整个模型中会多次使⽤。

那哪些场景才能使⽤Combiner呢?从这⾥分析,Combiner的输出是Reducer的输⼊,Combiner绝不能改变最终的计算结果。Combiner只应该⽤于那种Reduce的输⼊key/value与输出key/value类型完全⼀致,且不影响最终结果的场景。⽐如累加,最⼤值等。Combiner的使⽤⼀定得慎重,如果⽤好,它对job执⾏效率有帮助,反之会影响reduce的最终结果。

7. 合并溢写⽂件:每次溢写会在磁盘上⽣成⼀个临时⽂件(写之前判断是否有combiner),如果map的输出结果真的很⼤,有多次这样的溢写发⽣,磁盘上相应的就会有多个临时⽂件存在。当整个数据处理结束之后开始对磁盘中的临时⽂件进⾏merge合并,因为最终的⽂件只有⼀个,写⼊磁盘,并且为这个⽂件提供了⼀个索引⽂件,以记录每个reduce对应数据的偏移量。

⾄此map整个阶段结束!!

MapTask的⼀些配置


官⽅参考地址

https://hadoop.apache.org/docs/r2.9.2/hadoop-mapreduce-client/hadoopmapreduce-client-core/mapred-default.x

Maptask阶段所有的排序都是针对key进行排序

5.2 MapTask的并⾏度

1. MapTask并⾏度思考

MapTask的并⾏度决定Map阶段的任务处理并发度,从⽽影响到整个Job的处理速度。

思考:MapTask并⾏任务是否越多越好呢?哪些因素影响了MapTask并⾏度?

2. MapTask并⾏度决定机制

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

切⽚:数据切⽚只是在逻辑上对输⼊进⾏分⽚,并不会在磁盘上将其切分成⽚进⾏存储。一个分片)(split)对应一个MapTask任务

split大小默认等于block的大小, splitSize=blockSize!!

问题: a文件300M, b文件 100M, 两个文件都存入hdfs, 并作为某个mr任务的输入数据, 则这个mr任务的split, 以及MapTask的并行度是多少?

切片的计算方式:  按照文件逐个计算

a 文件:  0-128M, 128-256M, 256-300M

b 文件: 0-100M

总共是4个split, MapTask并行度=4 !!

在大数据分布式计算框架中, 移动计算也不要移动数据!

 切⽚机制源码阅读

计算切片大小的源码, 默认就是128M

 MapTask并行度不是越多越好,  如果一个文件仅仅比128M大一点点 (<1.1倍), 也被当成一个split对待, 而不会切分成多个split

MR狂框架在并行运算同时也会消耗更多资源,并行度越高, 资源消耗也越高, 假设129M文件被分为两个切片, 一个128M, 一个1M, 对于1M切片的MapTask来说, 太浪费资源.

129M的文件, 在HDFS存储时, 会不会被切成两块呢?

切片源码截图

5.3 ReduceTask ⼯作机制

Reduce⼤致分为copy、sort、reduce三个阶段,重点在前两个阶段。copy阶段包含⼀个eventFetcher来获取已完成的map列表,由Fetcher线程去copy数据,在此过程中会启动两个merge线程,分别为inMemoryMerger和onDiskMerger,分别将内存中的数据merge到磁盘和将磁盘中的数据进⾏merge。待数据copy完成之后,copy阶段就完成了,开始进⾏sort阶段,sort阶段主要是执⾏finalMerge操作,纯粹的sort阶段,完成之后就是reduce阶段,调⽤⽤户定义的reduce函数进⾏处理。

详细步骤

Copy阶段,简单地拉取数据, 复制map端输出的。Reduce进程启动⼀些数据copy线程(Fetcher),通过HTTP⽅式请求maptask获取属于⾃⼰的⽂件。

Merge阶段。这⾥的merge如map端的merge动作,只是数组中存放的是不同map端copy来的数值。Copy过来的数据会先放⼊内存缓冲区中,这⾥的缓冲区⼤⼩要⽐map端的更为灵活。merge有三种形式:内存到内存;内存到磁盘;磁盘到磁盘。默认情况下第⼀种形式不启⽤。当内存中的数据量到达⼀定阈值,就启动内存到磁盘的merge。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启⽤的,然后在磁盘中⽣成了众多的溢写⽂件。第⼆种
merge⽅式⼀直在运⾏,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge⽅式⽣成最终的⽂件。

sort阶段, 合并排序。把分散的数据合并成⼀个⼤的数据后,还会再对合并后的数据排序。对排序后的键值对调⽤reduce⽅法,键相等的键值对调⽤⼀次reduce⽅法,每次调⽤会产⽣零个或者多个键值对,最后把这些输出的键值对写⼊到HDFS⽂件中。

5.4 ReduceTask并⾏度

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

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

job.setNumReduceTasks(4);

注意事项
1. ReduceTask=0,表示没有Reduce阶段,输出⽂件数和MapTask数量保持⼀致;
2. ReduceTask数量不设置默认就是⼀个,输出⽂件数量为1个;
3. 如果数据分布不均匀,可能在Reduce阶段产⽣数据倾斜; 数据倾斜就是某个reduceTask处理的数据量远大于其他节点.

5.5 Shuffle机制

map阶段处理的数据如何传递给reduce阶段,是MapReduce框架中最关键的⼀个流程,这个流程就叫shuffle。

shuffle: 洗牌、发牌——(核⼼机制:数据分区,排序,分组,combine,合并等过程)

5.5.1 MapReduce的分区与reduceTask的数量

在MapReduce中,通过我们指定分区,会将同⼀个分区的数据发送到同⼀个reduce当中进⾏处理(默认是key相同去往同个分区),例如我们为了数据的统计,我们可以把⼀批类似的数据发送到同⼀个reduce当中去,在同⼀个reduce当中统计相同类型的数据,如何才能保证相同key的数据去往同个reduce呢?只需要保证相同key的数据分发到同个分区即可。结合以上原理分析我们知道MR程序shuffle机制默认就是这种规则

1. 分区源码

翻阅源码验证以上规则,MR程序默认使⽤的HashPartitioner,保证了相同的key去往同个分区!!

2. ⾃定义分区
实际⽣产中需求变化多端,默认分区规则往往不能满⾜需求,需要结合业务逻辑来灵活控制分区规则以及分区数量!!

如何制定⾃⼰需要的分区规则?

具体步骤
1. ⾃定义类继承Partitioner,重写getPartition()⽅法
2. 在Driver驱动中,指定使⽤⾃定义Partitioner
3. 在Driver驱动中,要根据⾃定义Partitioner的逻辑设置相应数量的ReduceTask数量。

需求 按照不同的appkey把记录输出到不同的分区中

原始⽇志格式

001     001577c3     kar              120.196.100.99     1116           954               200
⽇志id    设备id   appkey(合作硬件⼚商)     ⽹络ip         ⾃有内容时⻓(秒)   第三⽅内容时⻓(秒)    ⽹络状态码

输出结果

根据appkey把不同⼚商的⽇志数据分别输出到不同的文件中

需求分析
⾯对业务需求,结合mr的特点,来设计map输出的kv,以及reduce输出的kv数据。
⼀个ReduceTask对应⼀个输出⽂件,因为在shuffle机制中每个reduceTask拉取的都是某⼀个分区的数据,⼀个分区对应⼀个输出⽂件。

结合appkey的前缀相同的特点,同时不能使⽤默认分区规则,⽽是使⽤⾃定义分区器,只要appkey前缀相同则数据进⼊同个分区。

整体思路
Mapper
1. 读取⼀⾏⽂本,按照制表符切分
2. 解析出appkey字段,其余数据封装为PartitionBean对象(实现序列化Writable接⼝)
3. 设计map()输出的kv,key-->appkey(依靠该字段完成分区),PartitionBean对象作为Value输出

Partition
⾃定义分区器,实现按照appkey字段的前缀来区分所属分区

Reduce
1. reduce()正常输出即可,⽆需进⾏聚合操作

Driver
1. 在原先设置job属性的同时增加设置使⽤⾃定义分区器
2. 注意设置ReduceTask的数量(与分区数量保持⼀致)

Mapper

package com.lagou.mr.partition;

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

import java.io.IOException;

/*
    1. 读取一行文本,按照制表符切分

    2. 解析出appkey字段,其余数据封装为PartitionBean对象(实现序列化Writable接口)

    3. 设计map()输出的kv,key-->appkey(依靠该字段完成分区),PartitionBean对象作为Value输出
 */
public class PartitionMapper extends Mapper<LongWritable, Text, Text, PartitionBean> {

    final PartitionBean bean = new PartitionBean();
    final Text k = new Text();

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

        bean.setId(fields[0]);
        bean.setDeviceId(fields[1]);
        bean.setAppkey(fields[2]);
        bean.setIp(fields[3]);
        bean.setSelfDuration(Long.parseLong(fields[4]));
        bean.setThirdPartDuration(Long.parseLong(fields[5]));
        bean.setStatus(fields[6]);

        k.set(appkey);
        context.write(k, bean); //shuffle开始时会根据k的hashcode值进行分区,但是结合我们自己的业务,默认hash分区方式不能满足需求
    }
}

PartitionBean

package com.lagou.mr.partition;

import org.apache.hadoop.io.Writable;

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

public class PartitionBean implements Writable {

    //定义属性
    private String id;              //日志id
    private String deviceId;        //设备id
    private String appkey;          //appkey厂商id
    private String ip;              //ip地址
    private Long selfDuration;      //自有内容播放时长
    private Long thirdPartDuration; //第三方内容时长
    private String status;          //状态码
    
    //准备一个空参构造
    public PartitionBean() {
    }

    public PartitionBean(String id, String deviceId, String appkey, String ip, Long selfDuration, Long thirdPartDuration, String status) {
        this.id = id;
        this.deviceId = deviceId;
        this.appkey = appkey;
        this.ip = ip;
        this.selfDuration = selfDuration;
        this.thirdPartDuration = thirdPartDuration;
        this.status = status;
    }

    //序列化方法
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeUTF(id);
        out.writeUTF(deviceId);
        out.writeUTF(appkey);
        out.writeUTF(ip);
        out.writeLong(selfDuration);
        out.writeLong(thirdPartDuration);
        out.writeUTF(status);

    }

    //反序列化方法  要求序列化与反序列化字段顺序要保持一致
    @Override
    public void readFields(DataInput in) throws IOException {
        this.id = in.readUTF();
        this.deviceId = in.readUTF();
        this.appkey = in.readUTF();
        this.ip = in.readUTF();
        this.selfDuration = in.readLong();
        this.thirdPartDuration = in.readLong();
        this.status = in.readUTF();
    }

    //方便文本中的数据易于观察,重写toString()方法
    @Override
    public String toString() {
        return id + '\t' +
                "\t" + deviceId + '\t' + appkey + '\t' +
                ip + '\t' +
                selfDuration +
                "\t" + thirdPartDuration +
                "\t" + status;
    }

    ....
}

CustomPartitioner

package com.lagou.mr.partition;

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

//Partitioner分区器的泛型是map输出的kv类型
public class CustomPartitioner extends Partitioner<Text, PartitionBean> {

    @Override
    public int getPartition(Text text, PartitionBean partitionBean, int numPartitions) {
        int partition = 0;

        if (text.toString().equals("kar")) {
            //只需要保证满足此if条件的数据获得同个分区编号集合
            partition = 0;
        } else if (text.toString().equals("pandora")) {
            partition = 1;
        } else {
            partition = 2;
        }
        return partition;
    }
}

PartitionReducer

package com.lagou.mr.partition;

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

import java.io.IOException;

//reduce输入类型:Text,PartitionBean,输出:Text,PartitionBean
public class PartitionReducer extends Reducer<Text, PartitionBean, Text, PartitionBean> {
    @Override
    protected void reduce(Text key, Iterable<PartitionBean> values, Context context) throws IOException, InterruptedException {
        //无需聚合运算,只需要进行输出即可
        for (PartitionBean bean : values) {
            context.write(key, bean);
        }
    }
}

PartitionDriver

package com.lagou.mr.partition;

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;

public class PartitionDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {

        //1 获取配置文件
        final Configuration conf = new Configuration();

        //2 获取job实例
        final Job job = Job.getInstance(conf);

        //3 设置任务相关参数
        job.setJarByClass(PartitionDriver.class);
        job.setMapperClass(PartitionMapper.class);
        job.setReducerClass(PartitionReducer.class);

        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(PartitionBean.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(PartitionBean.class);

        // 4 设置使用自定义分区器
        job.setPartitionerClass(CustomPartitioner.class);

        //5 指定reducetask的数量与分区数量保持一致,分区数量是3
        job.setNumReduceTasks(3);

        //reducetask不设置默认是1个, 如果分区数量不⽌1个,但是reduceTask数量是1个,此时只会输出⼀个⽂件。

        //如果reduceTask数量⼤于分区数量,但是输出多个空⽂件
        //job.setNumReduceTasks(5);

        //如果reduceTask数量⼩于分区数量,有可能会报错。
        //job.setNumReduceTasks(2);

        // 6 指定输入和输出数据路径
        FileInputFormat.setInputPaths(job, new Path("d:/speak.data"));
        FileOutputFormat.setOutputPath(job, new Path("d:/parition/out"));

        // 7 提交任务
        final boolean flag = job.waitForCompletion(true);

        System.exit(flag ? 0 : 1);
    }
}

总结
1. ⾃定义分区器时最好保证分区数量与reduceTask数量保持⼀致;
2. 如果分区数量不⽌1个,但是reduceTask数量1个,此时只会输出⼀个⽂件。
3. 如果reduceTask数量⼤于分区数量,但是输出多个空⽂件
4. 如果reduceTask数量⼩于分区数量,有可能会报错。

5.5.2 MapReduce中的Combiner

combiner运⾏机制:

1. Combiner是MR程序中Mapper和Reducer之外的⼀种组件
2. Combiner组件的⽗类就是Reducer
3. Combiner和reducer的区别在于运⾏的位置
4. Combiner是在每⼀个maptask所在的节点运⾏;
5. Combiner的意义就是对每⼀个maptask的输出进⾏局部汇总,以减⼩⽹络传输量。
6. Combiner能够应⽤的前提是不能影响最终的业务逻辑,此外,Combiner的输出kv应该跟reducer的输⼊kv类型要对应起来。

举例说明

假设⼀个计算平均值的MR任务

Map阶段
2个MapTask

MapTask1输出数据:10,5,15 如果使⽤Combiner:(10+5+15)/3=10

MapTask2输出数据:2,6 如果使⽤Combiner:(2+6)/2=4

Reduce阶段汇总
(10+4)/2=7

⽽正确结果应该是
(10+5+15+2+6)/5=7.6

⾃定义Combiner实现步骤

⾃定义⼀个Combiner继承Reducer,重写Reduce⽅法
在驱动(Driver)设置使⽤Combiner(默认是不适⽤Combiner组件)1

1. 改造WordCount程序

package com.lagou.mr.wc;

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

import java.io.IOException;

//combiner组件的输入和输出类型与map()方法保持一致
public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
    final IntWritable total = new IntWritable();

    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int num = 0;
        //进行局部汇总,逻辑是与reduce方法保持一致
        for (IntWritable value : values) {
            final int i = value.get();
            num += i;
        }
        total.set(num);
        //输出单词,累加结果
        context.write(key, total);
    }
}

在驱动(Driver)设置使⽤Combiner

package com.lagou.mr.wc;

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.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

//封装任务并提交运行
public class WordCountDriver {




    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        /*
        1. 获取配置文件对象,获取job对象实例
        2. 指定程序jar的本地路径
        3. 指定Mapper/Reducer类
        4. 指定Mapper输出的kv数据类型
        5. 指定最终输出的kv数据类型
        6. 指定job处理的原始数据路径
        7. 指定job输出结果路径
        8. 提交作业
         */
//        1. 获取配置文件对象,获取job对象实例
        final Configuration conf = new Configuration();
        final Job job = Job.getInstance(conf, "WordCountDriver");

//        2. 指定程序jar的本地路径
        job.setJarByClass(WordCountDriver.class);

//        3. 指定Mapper/Reducer类
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);

//        4. 指定Mapper输出的kv数据类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);

//        5. 指定最终输出的kv数据类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        //5.1 设置使用combiner组件
        job.setCombinerClass(WordCountCombiner.class);
        //job.setCombinerClass(WordCountReducer.class);  //直接使用Reducer作为Combiner组件来使用是可以的!!
        
//        6. 指定job处理的原始数据路径
        FileInputFormat.setInputPaths(job, new Path(args[0])); //指定读取数据的原始路径
//        7. 指定job输出结果路径
        FileOutputFormat.setOutputPath(job, new Path(args[1])); //指定结果数据输出路径
//        8. 提交作业
        final boolean flag = job.waitForCompletion(true);
        //jvm退出:正常退出0,非0值则是错误退出
        System.exit(flag ? 0 : 1);

    }
}

验证结果

观察程序运⾏⽇志

如果直接使⽤WordCountReducer作为Combiner使⽤是否可以?

直接使⽤Reducer作为Combiner组件来使⽤是可以的

5.6 MapReduce中的排序

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

MapTask和ReduceTask均会对数据按照key进⾏排序。该操作属于Hadoop的默认⾏为。任何应⽤程序中的数据均会被排序,⽽不管逻辑.上是否需要。默认排序是按照字典顺序排序,且实现该排序的⽅法是快速排序

MapTask

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

ReduceTask

当所有数据拷⻉完毕后,ReduceTask统-对内存和磁盘上的所有数据进⾏⼀次归并排序。

1. 部分排序. MapReduce根据输⼊记录的键对数据集排序。保证输出的每个⽂件内部有序。
2. 全排序 最终输出结果只有⼀个⽂件,且⽂件内部有序。实现⽅式是只设置一个ReduceTask。但该⽅法在处理⼤型⽂件时效率极低,因为一台机器处理所有⽂件,完全丧失了MapReduce所提供的并⾏架构。
3. 辅助排序: ( GroupingComparator分组) 在Reduce端对key进⾏分组。应⽤于:在接收的key为bean对象时,想让⼀个或⼏个字段相同(全部字段⽐较不相同)的key进⼊到同⼀个reduce⽅法时,可以采⽤分组排序。
4. ⼆次排序. 在⾃定义排序过程中,如果compareTo中的判断条件为两个即为⼆次排序。

5.6.1 WritableComparable  全排序 

Bean对象如果作为Map输出的key时,需要实现WritableComparable接⼝并重写compareTo⽅法指定排序规则

基于统计的播放时⻓案例的输出结果对总时⻓进⾏排序
实现全局排序只能设置⼀个ReduceTask!!
播放时⻓案例输出结果

00fdaf3 33180 33420 00fdaf3 66600
00wersa4 30689 35191 00wersa4 65880
0a0fe2 43085 44254 0a0fe2 87339
0ad0s7 31702 29183 0ad0s7 60885
0sfs01 31883 29101 0sfs01 60984
a00df6s 33239 36882 a00df6s 70121
adfd00fd5 30727 31491 adfd00fd5 62218

需求分析
如何设计map()⽅法输出的key,value
MR框架中shuffle阶段的排序是默认⾏为,不管你是否需要都会进⾏排序。所以可以直接让MR来自动排序
key:  把所有字段封装成为⼀个bean对象,并且指定bean对象作为key输出,如果作为key输出,需要实现排序接⼝,指定⾃⼰的排序规则→ 按照总时长排序

具体步骤

Mapper
1. 读取结果⽂件,按照制表符进⾏切分
2. 解析出相应字段封装为SpeakBean
3. SpeakBean实现WritableComparable接⼝重写compareTo⽅法
4. map()⽅法输出kv; key-->SpeakBean, value-->NullWritable.get()  不需要value内容

Reducer
1. 循环遍历输出

Mapper代码

package com.lagou.mr.sort;

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.awt.image.BandCombineOp;
import java.io.IOException;

// 不需要value的输出, 故设置成NullWritable
public class SortMapper extends Mapper<LongWritable, Text, SpeakBean, NullWritable> {
    final SpeakBean bean = new SpeakBean();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

        //1 读取一行文本,转为字符串,切分
        final String[] fields = value.toString().split("\t");

        //2 解析出各个字段封装成SpeakBean对象
        bean.setDeviceId(fields[0]);
        bean.setSelfDrutation(Long.parseLong(fields[1]));
        bean.setThirdPartDuration(Long.parseLong(fields[2]));
        bean.setSumDuration(Long.parseLong(fields[4]));

        //3 SpeakBean作为key输出
        context.write(bean, NullWritable.get());
    }
}

Reducer

package com.lagou.mr.sort;

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

import java.io.IOException;

public class SortReducer extends Reducer<SpeakBean, NullWritable, SpeakBean, NullWritable> {

    //reduce方法的调用是相同key的value组成一个集合调用一次

    /*
        java中如何判断两个对象是否相等?
        根据equals方法,比较还是地址值
     */
    @Override
    protected void reduce(SpeakBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {

        //讨论按照总流量排序这件事情,还需要在reduce端处理吗?因为之前已经利用mr的shuffle对数据进行了排序

        //为了避免前面compareTo方法中, 总流量相等被当成对象相等,而合并了key,所以遍历values可以获取每个key(bean对象)
        for (NullWritable value : values) { //遍历value同时,key也会随着遍历。
            context.write(key, value);
        }
    }
}

Bean对象实现WritableComparable接⼝

package com.lagou.mr.sort;

import org.apache.hadoop.io.WritableComparable;

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

//因为这个类的实例对象要作为map输出的key,所以要实现writablecomparalbe接口
public class SpeakBean implements WritableComparable<SpeakBean> {

    //定义属性
    private Long selfDrutation;     //自有内容播放时长
    private Long thirdPartDuration; //第三方内容播放时长
    private String deviceId;        //设备id
    private Long sumDuration;       //总时长

    //准备空参构造方法
    public SpeakBean() {
    }

    public SpeakBean(Long selfDrutation, Long thirdPartDuration, String deviceId, Long sumDuration) {
        this.selfDrutation = selfDrutation;
        this.thirdPartDuration = thirdPartDuration;
        this.deviceId = deviceId;
        this.sumDuration = sumDuration;
    }

    //序列化方法
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeLong(selfDrutation);
        out.writeLong(thirdPartDuration);
        out.writeUTF(deviceId);
        out.writeLong(sumDuration);
    }

    //反序列化方法
    @Override
    public void readFields(DataInput in) throws IOException {
        this.selfDrutation = in.readLong();
        this.thirdPartDuration = in.readLong();
        this.deviceId = in.readUTF();
        this.sumDuration = in.readLong();
    }

    //指定排序规则,我们希望按照总时长进行排序
    @Override
    public int compareTo(SpeakBean o) {  //返回值三种:0:相等 1:小于 -1:大于
        System.out.println("compareTo 方法执行了。。。");
        //指定按照bean对象的总时长字段的值进行比较
        if (this.sumDuration > o.sumDuration) {
            return -1;
        } else if (this.sumDuration < o.sumDuration) {
            return 1;
        } else {
            return 0; //加入第二个判断条件,二次排序
        }
    }

    @Override
    public boolean equals(Object o) {
        System.out.println("equals方法执行了。。。");
        return super.equals(o);
    }

    @Override
    public int hashCode() {
        return Objects.hash(getSelfDrutation(), getThirdPartDuration(), getDeviceId(), getSumDuration());
    }

    @Override
    public String toString() {
        return selfDrutation +
                "\t" + thirdPartDuration +
                "\t" + deviceId + '\t' +
                sumDuration
                ;
    }

        ...
}

 Driver

package com.lagou.mr.sort;

import com.lagou.mr.wc.WordCountDriver;
import com.lagou.mr.wc.WordCountMapper;
import com.lagou.mr.wc.WordCountReducer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
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;

public class SortDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
         /*
            1. 获取配置文件对象,获取job对象实例
            2. 指定程序jar的本地路径
            3. 指定Mapper/Reducer类
            4. 指定Mapper输出的kv数据类型
            5. 指定最终输出的kv数据类型
            6. 指定job处理的原始数据路径
            7. 指定job输出结果路径
            8. 提交作业
         */
//        1. 获取配置文件对象,获取job对象实例
        final Configuration conf = new Configuration();

        final Job job = Job.getInstance(conf, "SortDriver");
//        2. 指定程序jar的本地路径
        job.setJarByClass(SortDriver.class);
//        3. 指定Mapper/Reducer类
        job.setMapperClass(SortMapper.class);
        job.setReducerClass(SortReducer.class);
//        4. 指定Mapper输出的kv数据类型
        job.setMapOutputKeyClass(SpeakBean.class);
        job.setMapOutputValueClass(NullWritable.class);
//        5. 指定最终输出的kv数据类型
        job.setOutputKeyClass(SpeakBean.class);
        job.setOutputValueClass(NullWritable.class);

        //指定reduceTask的数量,不指定默认是1个
        job.setNumReduceTasks(1);

//        6. 指定job处理的原始数据路径
        //import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
        //import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
        FileInputFormat.setInputPaths(job, new Path("D:\\speak_output")); //指定读取数据的原始路径
//        7. 指定job输出结果路径
        FileOutputFormat.setOutputPath(job, new Path("d:\\speak\\sortout")); //指定结果数据输出路径
//        8. 提交作业
        final boolean flag = job.waitForCompletion(true);
        //jvm退出:正常退出0,非0值则是错误退出
        System.exit(flag ? 0 : 1);
    }
}

总结
1. ⾃定义对象作为Map的key输出时,需要实现WritableComparable接⼝,排序:重写compareTo()⽅法,序列以及反序列化⽅法
2. 再次理解reduce()⽅法的参数;reduce()⽅法是map输出的kv中key相同的kv中的v组成⼀个集合调⽤⼀次reduce()⽅法,选择遍历values得到所有的key.
3. 默认reduceTask数量是1个;
4. 对于全局排序需要保证只有⼀个reduceTask!!

5.6.2 GroupingComparator  分区排序(默认的分区规则,区内有序) 面试题

GroupingComparator是mapreduce当中reduce端的⼀个功能组件,主要的作⽤是决定哪些数据作为⼀组,调⽤⼀次reduce的逻辑,默认是每个不同的key,作为多个不同的组,每个组调⽤⼀次reduce逻辑,我们可以⾃定义

GroupingComparator实现不同的key作为同⼀个组,调⽤⼀次reduce逻辑。

1. 需求

原始数据

需要求出每⼀个订单中成交⾦额最⼤的⼀笔交易。

 2. 实现思路

Mapper
读取⼀⾏⽂本数据,切分出每个字段;
订单id和⾦额封装为⼀个Bean对象,Bean对象的排序规则指定为先按照订单Id排序 (视为相同的key),订单Id相等再按照⾦额降序排;
map()⽅法输出kv;    key-->bean对象, value-->NullWritable.get();

Shuffle
指定分区器,保证相同订单id的数据去往同个分区(⾃定义分区器)
指定GroupingComparator,分组规则指定只要订单Id相等则认为属于同⼀组;

Reduce
每个reduce()⽅法写出⼀组key的第⼀个

参考代码

OrderBean

OrderBean定义两个字段,⼀个字段是orderId,第⼆个字段是⾦额(注意⾦额⼀定要使⽤
Double或者DoubleWritable类型,否则没法按照⾦额顺序排序)
排序规则指定为先按照订单Id排序,订单Id相等再按照⾦额降序排!!

package com.lagou.mr.group;

import org.apache.hadoop.io.WritableComparable;

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

public class OrderBean implements WritableComparable<OrderBean> {

    private String orderId; //订单id
    private Double price;   //金额

    public OrderBean(String orderId, Double price) {
        this.orderId = orderId;
        this.price = price;
    }

    public OrderBean() {
    }

    //指定排序规则,先按照订单id比较再按照金额比较,按照金额降序排
    @Override
    public int compareTo(OrderBean o) {
        int res = this.orderId.compareTo(o.getOrderId()); //0 1 -1
        if (res == 0) {
            //订单id相同,比较金额
            res = - this.price.compareTo(o.getPrice());

        }
        return res;
    }

    //序列化
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeUTF(orderId);
        out.writeDouble(price);
    }

    //反序列化
    @Override
    public void readFields(DataInput in) throws IOException {
        this.orderId = in.readUTF();
        this.price = in.readDouble();
    }

    //重写toString()
    @Override
    public String toString() {
        return orderId + '\t' +
                price
                ;
    }

     ....
}

⾃定义分区器
保证ID相同的订单去往同个分区最终去往同⼀个Reduce中

package com.lagou.mr.group;

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

public class CustomPartitioner extends Partitioner<OrderBean, NullWritable> {
    @Override
    public int getPartition(OrderBean orderBean, NullWritable nullWritable, int numPartitions) {
        
        //希望订单id相同的数据进入同个分区
        return (orderBean.getOrderId().hashCode() & Integer.MAX_VALUE) % numPartitions;
    }
}

⾃定义GroupingComparator
保证id相同的订单进⼊⼀个分组中,进⼊分组的数据已经是按照⾦额降序排序。reduce()⽅法取出第⼀个即是⾦额最⾼的交易

package com.lagou.mr.group;


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

public class CustomGroupingComparator extends WritableComparator {

    public CustomGroupingComparator() {
        //注册自定义的GroupingComparator接受OrderBean对象
        super(OrderBean.class, true);
    }

    //重写其中的compare方法,通过这个方法来让mr接受orderid相等则两个对象相等的规则,key相等

    @Override
    public int compare(WritableComparable a, WritableComparable b) { //a 和b是orderbean的对象
        //比较两个对象的orderid
        final OrderBean o1 = (OrderBean) a;
        final OrderBean o2 = (OrderBean) b;
        return o1.getOrderId().compareTo(o2.getOrderId()); // 0 1 -1
    }
}

Mapper

package com.lagou.mr.group;

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;

public class GroupMapper extends Mapper<LongWritable, Text, OrderBean, NullWritable> {
    OrderBean bean = new OrderBean();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        final String[] fields = value.toString().split("\t");
        //订单id与jine封装为一个orderBean
        bean.setOrderId(fields[0]);
        bean.setPrice(Double.parseDouble(fields[2]));
        context.write(bean, NullWritable.get());
    }
}

Reducer

package com.lagou.mr.group;

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

import java.io.IOException;

public class GroupReducer extends Reducer<OrderBean, NullWritable, OrderBean, NullWritable> {

    //key:reduce方法的key注意是一组相同key的kv的第一个key 作为传入reduce方法的key,因为我们已经指定了排序的规则
    //  按照金额降序排列,则第一个key就是金额最大的交易数据
    //value:一组相同key的kv对中v的集合
    //  对于如何判断key是否相同,自定义对象是需要我们指定一个规则,这个规则通过Groupingcomaprator来指定
    @Override
    protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
        //直接输出key就是金额最大的交易
        context.write(key, NullWritable.get());
    }
}

Driver

package com.lagou.mr.group;

import com.lagou.mr.wc.WordCountDriver;
import com.lagou.mr.wc.WordCountMapper;
import com.lagou.mr.wc.WordCountReducer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
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;

public class GroupDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
         /*
        1. 获取配置文件对象,获取job对象实例
        2. 指定程序jar的本地路径
        3. 指定Mapper/Reducer类
        4. 指定Mapper输出的kv数据类型
        5. 指定最终输出的kv数据类型
        6. 指定job处理的原始数据路径
        7. 指定job输出结果路径
        8. 提交作业
         */
//        1. 获取配置文件对象,获取job对象实例
        final Configuration conf = new Configuration();

        final Job job = Job.getInstance(conf, "GroupDriver");
//        2. 指定程序jar的本地路径
        job.setJarByClass(GroupDriver.class);
//        3. 指定Mapper/Reducer类
        job.setMapperClass(GroupMapper.class);
        job.setReducerClass(GroupReducer.class);
//        4. 指定Mapper输出的kv数据类型
        job.setMapOutputKeyClass(OrderBean.class);
        job.setMapOutputValueClass(NullWritable.class);
//        5. 指定最终输出的kv数据类型
        job.setOutputKeyClass(OrderBean.class);
        job.setOutputValueClass(NullWritable.class);

        //指定分区器
        job.setPartitionerClass(CustomPartitioner.class);
        //指定使用groupingcomparator
        job.setGroupingComparatorClass(CustomGroupingComparator.class);
        FileInputFormat.setInputPaths(job, new Path("D:\\lagou\\课件\\正式班\\大数据正式班第一阶段模块一\\模块一\\资料\\data\\data\\GroupingComparator")); //指定读取数据的原始路径
//        7. 指定job输出结果路径
        FileOutputFormat.setOutputPath(job, new Path("d:\\group\\out")); //指定结果数据输出路径

        //指定reducetask的数量,不要使用默认的一个,分区效果不明显
        job.setNumReduceTasks(2);
//        8. 提交作业
        final boolean flag = job.waitForCompletion(true);
        //jvm退出:正常退出0,非0值则是错误退出
        System.exit(flag ? 0 : 1);

    }
}

5.7 MapReduce读取和输出数据

5.7.1 InputFormat

运⾏MapReduce程序时,输⼊的⽂件格式包括:基于⾏的⽇志⽂件、⼆进制格式⽂件、数据库表等。那么,针对不同的数据类型,MapReduce是如何读取这些数据的呢?
InputFormat是MapReduce框架⽤来读取数据的类。

InputFormat常⻅⼦类包括:
TextInputFormat (普通⽂本⽂件,MR框架默认的读取实现类型)
KeyValueTextInputFormat(读取⼀⾏⽂本数据按照指定分隔符,把数据封装为kv类型)
NLineInputF ormat(读取数据按照⾏数进⾏划分分⽚split)
CombineTextInputFormat(合并⼩⽂件,避免启动过多MapTask任务)
⾃定义InputFormat

1. CombineTextInputFormat

MR框架默认的TextInputFormat切⽚机制按⽂件划分切⽚,⽂件⽆论多⼩,都是单ᇿ⼀个切⽚,然后由⼀个MapTask处理,如果有⼤量⼩⽂件,就对应的会⽣成并启动⼤量的 MapTask,⽽每个MapTask处理的数据量很⼩⼤量时间浪费在初始化资源启动收回等阶段,这种⽅式导致资源利⽤率不⾼。

CombineTextInputFormat⽤于⼩⽂件过多的场景,它可以将多个⼩⽂件从逻辑上划分成⼀个切⽚split,这样多个⼩⽂件就可以交给⼀个MapTask处理,提⾼资源利⽤率。

需求
将输⼊数据中的多个⼩⽂件合并为⼀个切⽚处理
运⾏WordCount案例,准备多个⼩⽂件
具体使⽤⽅式

 // 如果不设置InputFormat,它默认⽤的是TextInputFormat.class
  job.setInputFormatClass(CombineTextInputFormat.class);
  
  //虚拟存储切⽚最⼤值设置4m
  CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);

验证切⽚数量的变化!!
CombineTextInputFormat切⽚原理
切⽚⽣成过程分为两部分:虚拟存储过程和切⽚过程
假设设置setMaxInputSplitSize值为4M
四个⼩⽂件:1.txt -->2M ;2.txt-->7M;3.txt-->0.3M;4.txt--->8.2M

虚拟存储过程:

把输⼊⽬录下所有⽂件⼤⼩,依次和设置的setMaxInputSplitSize值进⾏⽐较,

如果不⼤于设置的最⼤值,逻辑上划分⼀个块。

如果输⼊⽂件⼤于设置的最⼤值且⼤于两倍,那么以最⼤值切割⼀块;

当剩余数据⼤⼩超过设置的最⼤值且不⼤于最⼤值2倍,此时将⽂件均分成2个虚拟存储块(防⽌出现太⼩切⽚)。

⽐如setMaxInputSplitSize值为4M,输⼊⽂件⼤⼩为8.02M,则先逻辑上分出⼀个4M的块。剩余的⼤⼩为4.02M,如果按照4M逻辑划分,就会出现0.02M的⾮常⼩的虚拟存储⽂件,所以将剩余的4.02M⽂件切分成(2.01M和2.01M)两个⽂件

1.txt-->2M;2M<4M;⼀个块;
2.txt-->7M;7M>4M,但是不⼤于两倍,均匀分成两块;两块:每块3.5M;
3.txt-->0.3M;0.3<4M ,0.3M<4M ,⼀个块
4.txt-->8.2M;⼤于最⼤值且⼤于两倍;⼀个4M的块,剩余4.2M分成两块,每块2.1M

所有块信息:
2M,3.5M,3.5M,0.3M,4M,2.1M,2.1M 共7个虚拟存储块。

切⽚过程

判断虚拟存储的⽂件⼤⼩是否⼤于setMaxInputSplitSize值,⼤于等于则独形成⼀个切⽚。
如果不⼤于则跟下⼀个虚拟存储⽂件进⾏合并,共同形成⼀个切⽚。
按照之前输⼊⽂件:有4个⼩⽂件⼤⼩分别为2M、7M、0.3M以及8.2M这四个⼩⽂件,
则虚拟存储之后形成7个⽂件块,⼤⼩分别为: 2M,3.5M,3.5M,0.3M,4M,2.1M,2.1M
最终会形成3个切⽚,⼤⼩分别为: (2+3.5)M,(3.5+0.3+4)M,(2.1+2.1)M

注意:虚拟存储切⽚最⼤值设置最好根据实际的⼩⽂件⼤⼩情况来设置具体的值。

2. ⾃定义InputFormat

HDFS还是MapReduce,在处理⼩⽂件时效率都⾮常低,但⼜难免⾯临处理⼤量⼩⽂件的场景,此时,就需要有相应解决⽅案。可以⾃定义InputFormat实现⼩⽂件的合并。

需求
将多个⼩⽂件合并成⼀个SequenceFile⽂件(SequenceFile⽂件是Hadoop⽤来存储⼆进制形式的key-value对的⽂件格式),SequenceFile⾥⾯存储着多个⽂件,存储的形式为⽂件路径+名称为key,⽂件内容为value。

结果
得到⼀个合并了多个⼩⽂件的SequenceFile⽂件

整体思路
1. 定义⼀个类继承FileInputFormat
2. 重写isSplitable() 指定为不可切分;重写createRecordReader()⽅法,创建⾃⼰的

RecorderReader对象
3. 改变默认读取数据⽅式,实现⼀次读取⼀个完整⽂件作为kv输出;
4. Driver指定使⽤的InputFormat类型

代码参考
⾃定义InputFormat

package com.lagou.mr.sequence;

import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;

import java.io.IOException;

// 自定义inputformat读取多个小文件合并为一个SequenceFile文件
// SequenceFile文件中以kv形式存储文件,key--》文件路径+文件名称,value-->文件的整个内容

// TextInputFormat中泛型是LongWritable:文本的偏移量, Text:一行文本内容;指明当前inputformat的输出数据类型
// 自定义inputformat:key-->文件路径+名称,value-->整个文件内容
public class CustomInputFormat extends FileInputFormat<Text, BytesWritable> {

    //重写是否可切分
    @Override
    protected boolean isSplitable(JobContext context, Path filename) {
        //对于当前需求,不需要把文件切分,保证一个切片就是一个文件
        return false;
    }

    //recordReader就是用来读取数据的对象
    @Override
    public RecordReader<Text, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
        CustomRecordReader recordReader = new CustomRecordReader();
        //调用recordReader的初始化方法
        recordReader.initialize(split, context);
        return recordReader;
    }
}

⾃定义RecordReader

package com.lagou.mr.sequence;


import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;

import java.io.IOException;

//负责读取数据,一次读取整个文件内容,封装成kv输出
public class CustomRecordReader extends RecordReader<Text, BytesWritable> {

    private FileSplit split;
    //hadoop配置文件对象
    private Configuration conf;


    //定义key,value的成员变量
    private Text key = new Text();
    private BytesWritable value = new BytesWritable();

    //初始化方法,把切片以及上下文提升为全局
    @Override
    public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
        this.split = (FileSplit) split;
        conf = context.getConfiguration();
    }


    private Boolean flag = true;

    //用来读取数据的方法
    @Override
    public boolean nextKeyValue() throws IOException, InterruptedException {

        //对于当前split来说只需要读取一次即可,因为一次就把整个文件全部读取了。

        if (flag) {
            //准备一个数组存放读取到的数据,数据大小是多少?
            byte[] content = new byte[(int) split.getLength()];
            final Path path = split.getPath();//获取切片的path信息
            final FileSystem fs = path.getFileSystem(conf);//获取到文件系统对象

            final FSDataInputStream fis = fs.open(path); //获取到输入流

            IOUtils.readFully(fis, content, 0, content.length); //读取数据并把数据放入byte[]
            //封装key和value
            key.set(path.toString());
            value.set(content, 0, content.length);

            IOUtils.closeStream(fis);
            //把再次读取的开关置为false
            flag = false;
            return true;
        }

        return false;
    }

    //获取到key
    @Override
    public Text getCurrentKey() throws IOException, InterruptedException {
        return key;
    }

    //获取到value
    @Override
    public BytesWritable getCurrentValue() throws IOException, InterruptedException {
        return value;
    }

    //获取进度
    @Override
    public float getProgress() throws IOException, InterruptedException {
        return 0;
    }

    //关闭资源
    @Override
    public void close() throws IOException {

    }
}

Mapper

package com.lagou.mr.sequence;

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

import java.io.IOException;

//1 text:代表的是一个文件的path+名称,BytesWritable:一个文件的内容
public class SequcenMapper extends Mapper<Text, BytesWritable, Text, BytesWritable> {

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

Reducer

package com.lagou.mr.sequence;

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

import java.io.IOException;

public class SequcenReducer extends Reducer<Text, BytesWritable, Text, BytesWritable> {
    @Override
    protected void reduce(Text key, Iterable<BytesWritable> values, Context context) throws IOException, InterruptedException {
        //输出value值(文件内容),只获取其中第一个即可(只有一个)
        context.write(key, values.iterator().next());
    }
}

Driver

package com.lagou.mr.sequence;

import com.lagou.mr.wc.WordCountDriver;
import com.lagou.mr.wc.WordCountMapper;
import com.lagou.mr.wc.WordCountReducer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
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.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

public class SequenceDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
             /*
        1. 获取配置文件对象,获取job对象实例
        2. 指定程序jar的本地路径
        3. 指定Mapper/Reducer类
        4. 指定Mapper输出的kv数据类型
        5. 指定最终输出的kv数据类型
        6. 指定job处理的原始数据路径
        7. 指定job输出结果路径
        8. 提交作业
         */
//        1. 获取配置文件对象,获取job对象实例
        final Configuration conf = new Configuration();

        final Job job = Job.getInstance(conf, "SequenceDriver");
//        2. 指定程序jar的本地路径
        job.setJarByClass(SequenceDriver.class);
//        3. 指定Mapper/Reducer类
        job.setMapperClass(SequcenMapper.class);
        job.setReducerClass(SequcenReducer.class);
//        4. 指定Mapper输出的kv数据类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(BytesWritable.class);
//        5. 指定最终输出的kv数据类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(BytesWritable.class);


        //设置使用自定义InputFormat读取数据
        job.setInputFormatClass(CustomInputFormat.class);
        FileInputFormat.setInputPaths(job, new Path("E:\\teach\\hadoop框架\\资料\\data\\sequencefile小文件")); //指定读取数据的原始路径
//        7. 指定job输出结果路径
        FileOutputFormat.setOutputPath(job, new Path("E:\\sequence\\out")); //指定结果数据输出路径
//        8. 提交作业
        final boolean flag = job.waitForCompletion(true);
        //jvm退出:正常退出0,非0值则是错误退出
        System.exit(flag ? 0 : 1);
    }
}

验证输出结果


5.7.2 OutputFormat

OutputFormat:是MapReduce输出数据的基类,所有MapReduce的数据输出都实现了OutputFormat抽象类。下⾯我们介绍⼏种常⻅的OutputFormat⼦类

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

SequenceFileOutputForma
将SequenceFileOutputFormat输出作为后续MapReduce任务的输⼊,这是⼀种好的输出格式,因为它的格式紧凑很容易被

⾃定义OutputFormat

需求分析
要在⼀个MapReduce程序中根据数据的不同输出两类结果到不同⽬录,这类输出需求可以通过⾃定义OutputFormat来实现。

实现步骤
1. ⾃定义⼀个类继承FileOutputFormat。
2. 改写RecordWriter,改写输出数据的⽅法write()。

需求
⽹络请求⽇志数据

  http://www.baidu.com
  http://www.google.com
  http://cn.bing.com
  http://www.lagou.com
  http://www.sohu.com
  http://www.sina.com
  http://www.sin2a.com
  http://www.sin2desa.com
  http://www.sindsafa.com

输出结果
lagou.log

http://www.lagou.com

other.log

  http://cn.bing.com
  http://www.baidu.com
  http://www.google.com
  http://www.sin2a.com
  http://www.sin2desa.com
  http://www.sina.com
  http://www.sindsafa.com
  http://www.sohu.com


参考代码
Mapper

package com.lagou.mr.output;

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;

public class OutputMapper 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.lagou.mr.output;

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


import java.io.IOException;

public class OutputReducer extends Reducer<Text, NullWritable, Text, NullWritable> {
    @Override
    protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {

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

OutputFormat

package com.lagou.mr.output;

import org.apache.hadoop.conf.Configuration;
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 org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;


public class CustomOutputFormat extends FileOutputFormat<Text, NullWritable> {
    //写出数据的对象
    @Override
    public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext context) throws IOException, InterruptedException {
        //定义写出数据的路径信息,并获取到输出流传入writer对象中
        final Configuration conf = context.getConfiguration();
        final FileSystem fs = FileSystem.get(conf);
        //定义输出的路径
        final FSDataOutputStream lagouOut = fs.create(new Path("e:/lagou.log"));
        final FSDataOutputStream otherOut = fs.create(new Path("e:/other.log"));
        CustomWriter customWriter = new CustomWriter(lagouOut, otherOut);
        return customWriter;
    }
}

RecordWriter

package com.lagou.mr.output;

import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.io.IOUtils;
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;


public class CustomWriter extends RecordWriter<Text, NullWritable> {
    //定义成员变量
    private FSDataOutputStream lagouOut;
    private FSDataOutputStream otherOut;

    //定义构造方法接收两个输出流


    public CustomWriter(FSDataOutputStream lagouOut, FSDataOutputStream otherOut) {
        this.lagouOut = lagouOut;
        this.otherOut = otherOut;
    }

    //写出数据的逻辑,控制写出的路径
    @Override
    public void write(Text key, NullWritable value) throws IOException, InterruptedException {
        //写出数据需要输出流
        final String line = key.toString();
        if (line.contains("lagou")) {
            lagouOut.write(line.getBytes());
            lagouOut.write("\r\n".getBytes());
        } else {
            otherOut.write(line.getBytes());
            otherOut.write("\r\n".getBytes());
        }
    }

    //关闭,释放资源
    @Override
    public void close(TaskAttemptContext context) throws IOException, InterruptedException {

        IOUtils.closeStream(lagouOut);
        IOUtils.closeStream(otherOut);
    }
}

Driver

package com.lagou.mr.output;

import com.lagou.mr.wc.WordCountDriver;
import com.lagou.mr.wc.WordCountMapper;
import com.lagou.mr.wc.WordCountReducer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
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.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

public class OutputDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
              /*
        1. 获取配置文件对象,获取job对象实例
        2. 指定程序jar的本地路径
        3. 指定Mapper/Reducer类
        4. 指定Mapper输出的kv数据类型
        5. 指定最终输出的kv数据类型
        6. 指定job处理的原始数据路径
        7. 指定job输出结果路径
        8. 提交作业
         */
//        1. 获取配置文件对象,获取job对象实例
        final Configuration conf = new Configuration();

        final Job job = Job.getInstance(conf, "OutputDriver");
//        2. 指定程序jar的本地路径
        job.setJarByClass(OutputDriver.class);
//        3. 指定Mapper/Reducer类
        job.setMapperClass(OutputMapper.class);
        job.setReducerClass(OutputReducer.class);
//        4. 指定Mapper输出的kv数据类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(NullWritable.class);
//        5. 指定最终输出的kv数据类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(NullWritable.class);

        //指定使用自定义outputformat
        job.setOutputFormatClass(CustomOutputFormat.class);
        FileInputFormat.setInputPaths(job, new Path("E:\\teach\\hadoop框架\\资料\\data\\click_log")); //指定读取数据的原始路径
//        7. 指定job输出结果路径,因为mr默认要输出一个success等标识文件
        FileOutputFormat.setOutputPath(job, new Path("E:\\clicklog\\out")); //指定结果数据输出路径
//        8. 提交作业
        final boolean flag = job.waitForCompletion(true);
        //jvm退出:正常退出0,非0值则是错误退出
        System.exit(flag ? 0 : 1);
    }
}

验证结果是否已把数据分别输出到不同的⽬录中!!

5.8 shuffle阶段数据的压缩机制

5.8.1 hadoop当中⽀持的压缩算法

数据压缩有两⼤好处,节约磁盘空间,加速数据在⽹络和磁盘上的传输!!
我们可以使⽤bin/hadoop checknative 来查看我们编译之后的hadoop⽀持的各种压缩,如果出现openssl为false,那么就在线安装⼀下依赖包!!

安装openssl

yum install -y openssl-devel

为了⽀持多种压缩/解压缩算法,Hadoop引⼊了编码/解码器

常⻅压缩⽅式对⽐

5.8.2 压缩位置

Map输⼊端压缩
此处使⽤压缩⽂件作为Map的输⼊数据,⽆需显示指定编解码⽅式,Hadoop会⾃动检查⽂件扩展名,如果压缩⽅式能够匹配,Hadoop就会选择合适的编解码⽅式对⽂件进⾏压缩和解压

Map输出端压缩
Shuffle是Hadoop MR过程中资源消耗最多的阶段,如果有数据量过⼤造成⽹络传输速度缓慢,可以考虑使⽤压缩

Reduce端输出压缩
输出的结果数据使⽤压缩能够减少存储的数据量,降低所需磁盘的空间,并且作为第⼆个MR的输⼊时可以复⽤压缩。

5.8.3 压缩配置⽅式

1. 在驱动代码中通过Configuration直接设置使⽤的压缩⽅式,可以开启Map输出和Reduce输出压缩

Configuration configuration = new Configuration();

设置map阶段压缩
configuration.set("mapreduce.map.output.compress","true");
configuration.set("mapreduce.map.output.compress.codec","org.apache.hadoop.io.compress.SnappyCodec");

设置reduce阶段的压缩
configuration.set("mapreduce.output.fileoutputformat.compress","true");
configuration.set("mapreduce.output.fileoutputformat.compress.type","RECORD");
configuration.set("mapreduce.output.fileoutputformat.compress.codec","org.apache.hadoop.io.compress.SnappyCodec");

2. 配置mapred-site.xml(修改后分发到集群其它节点,重启Hadoop集群),此种⽅式对运⾏在集群的
所有MR任务都会执⾏压缩。

<property> 
      <name>mapreduce.output.fileoutputformat.compress</name>
      <value>true</value>
</property>

<property> 
      <name>mapreduce.output.fileoutputformat.compress.type</name>
      <value>RECORD</value>
</property>

<property> 
      <name>mapreduce.output.fileoutputformat.compress.codec</name>
      <value>org.apache.hadoop.io.compress.SnappyCodec</value>
</property>

5.8.4 压缩案例

需求
使⽤snappy压缩⽅式压缩WordCount案例的输出结果数据
具体实现
在驱动代码中添加压缩配置

//针对reduce端输出使用snappy压缩
configuration.set("mapreduce.output.fileoutputformat.compress","true");
configuration.set("mapreduce.output.fileoutputformat.compress.type","RECORD");
configuration.set("mapreduce.output.fileoutputformat.compress.codec","org.apache.hadoop.io.compress.SnappyCodec");

重新打成jar包,提交集群运⾏,验证输出结果是否已进⾏了snappy压缩!!

猜你喜欢

转载自blog.csdn.net/chengh1993/article/details/111658321