Hadoop-MapReduce

本文详细介绍了MapReduce的核心概念、优缺点、工作流程、编程规范和常见操作,如序列化、分区、排序和Combiner。MapReduce适用于大规模离线数据处理,通过Mapper和Reducer实现数据的预处理和聚合。文章还提供了WordCount和流量统计的实例,展示了如何编写Mapper、Reducer和Driver。此外,讨论了自定义序列化、分区和排序策略,以及MapReduce的优化技巧,如CombineTextInputFormat和自定义Partitioner。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

MapReduce概述

MapReduce定义

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

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

MapReduce优点

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

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

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

适合 PB 级以上海量数据的离线处理:可以实现上千台服务器集群并发工作,提供数据处理能力。

MapReduce缺点

不擅长实时计算:MapReduce无法像MySQL一样,在毫秒或者秒级内返回结果

不擅长流式计算:流式计算的输入数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化。 这是因为MapReduce自身的设计特点决定了数据源必须是静态的

不擅长DAG有向无环图计算:多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下,MapReduce并不是不能做,而是使用后,每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常的低下

MapReduce核心思想

image-20210713180306107

MapReduce完整进程

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

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

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

常用数据序列化类型

Java类型Hadoop Writable 类型
BooleanBooleanWritable
ByteByteWritable
IntIntWritable
FloatFloatWritable
LongLongWritable
DoubleDoubleWritable
StringText
MapMapWritable
ArrayArrayWritable
NullNullWritable

初识MapReduce之WordCount实操

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

MapReduce 编程规范

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

Mapper阶段

  • 用户自定义的Mapper要继承自己的父类
  • Mapper的输入数据是KV对的形式(KV的类型可自定义)
  • Mapper中的业务逻辑写在map()方法中
  • Mapper的输出数据是KV对的形式(KV的类型可自定义)
  • map()方法(MapTask进程)对每一个调用一次

Reducer阶段

  • 用户自定义的Reducer要继承自己的父类
  • Reducer的输入数据类型对应Mapper的输出数据类型,也是KV
  • Reducer的业务逻辑写在reduce()方法
  • ReduceTask进程对每一组相同k的组调用一次reduce()方法

Driver阶段

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

引入依赖

<dependencies>
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-client</artifactId>
        <version>3.1.3</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.30</version>
    </dependency>
</dependencies>

配置日志级别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

编写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;

/*
    KEYIN: map阶段输入的key的类型:LongWritable
    VALUEIN:map阶段输入value类型:Text
    KEYOUT:map阶段输出的Key类型:Text
    VALUEOUT:map阶段输出的value类型:IntWritable

    数据如下:
    czs czs
    xyx xyx
    tkk tkk
 */
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {

    private Text outK = new Text();
    private IntWritable outV = new IntWritable(1);

    /*
        LongWritable key:map阶段输入的key的类型
        Text values:map阶段输入value类型
        Context context:上下文对象,连接mapreduce和系统之间的消息传递
    */
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        // 获取一行
        String line = value.toString();

        // 切割数据
        /*
                        czs
            czs czs =>
                        czs
         */
        String[] words = line.split(" ");

        // 循环写出
        for (String word : words) {
            // 封装outK
            outK.set(word);
            // 写出
            context.write(outK, outV);
        }
    }
}

编写Reducer类

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

import java.io.IOException;

/*
    KEYIN: reduce阶段输入的key的类型:Text
    VALUEIN:reduce阶段输入value类型:IntWritable
    KEYOUT:reduce阶段输出的Key类型:Text
    VALUEOUT:reduce阶段输出的value类型:IntWritable
 */
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {

    private IntWritable outV = new IntWritable();

    /*
        Text key:reduce阶段输入的key的类型
        Iterable<IntWritable> values:reduce阶段输入value类型
        Context context:上下文对象,连接mapreduce和系统之间的消息传递
     */
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int sum = 0;
        /*
            czs,(1,1)
            xyx,(1,1)
            累加
         */
        for (IntWritable value : values) {
            sum += value.get();
        }

        // 写出
        outV.set(sum);
        context.write(key, outV);
    }
}

编写Driver驱动类

public class WordCountDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        // 1. 获取job
        Configuration configuration = new Configuration();
        Job job = Job.getInstance(configuration);

        // 2. 设置jar包路径
        job.setJarByClass(WordCountDriver.class);

        // 3. 关联mapper和reducer
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);

        // 4. 设置map输出的KV类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);

        // 5. 设置最终输出的KV类型
        job.setOutputKeyClass(Text.class);
        job.setOutputKeyClass(IntWritable.class);

        // 6. 设置输入路径和输出路径
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        // 7. 提交job
        /*
            verbose:是否监控并打印信息
         */
        boolean result = job.waitForCompletion(true);
        System.exit(result ? 0 : 1);
    }
}

本地测试

  • 需要首先配置好HADOOP_HOME变量以及Windows运行依赖
  • 如果报错java.lang.UnsatisfiedLinkError: boolean org.apache.hadoop.io.nativeio.NativeIO$Windows.access0(java.lang.String, int)错误,把Hadoop的bin目录下的文件hadoop.dll复制到windows/system32下

集群测试

  • 用 maven 打 jar 包,需要添加的打包插件依赖
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.1</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>
  • 调整输入路径和输出路径为自定义路径

image-20210713181719269

  • 启动集群,执行自定义wordcount程序,执行需要指定全路径包名

image-20210713181815466

image-20210713181907300

序列化概述

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

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

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

Hadoop 序列化特点

  • 紧凑:高效使用存储空间
  • 快速:读写数据的额外开销小
  • 互操作:支持多语言的交互

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

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

具体实现bean对象序列化步骤如下7步

第一步:必须实现Writable接口

public class MyBean implements Writable {}

第二步:反序列化时,需要反射调用空参构造函数,所以必须有空参构造函数

public FlowBean() {
super();
}

第三步:重写序列化方法

// 序列化
@Override
public void write(DataOutput out) throws IOException {
    out.writeLong("xxx");
}

第四步:重写反序列化方法

// 反序列化
@Override
public void readFields(DataInput in) throws IOException {
    this.xxx = in.readLong();
}

第五步:注意反序列化的顺序和序列化的顺序完全一致

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

第七步:如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口,因为MapReduce框中的Shuffle过程要求对key必须能排序

序列化实操

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

输入数据格式
7 13560436666 120.196.100.99 1116 954 200 id 
手机号码 网络 ip 上行流量 下行流量 网络状态码

期望输出数据格式
13560436666 1116 954 2070
手机号码 上行流量 下行流量 总流量

实现分析

image-20210713212515670

编写序列化Bean对象

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

    // 空参构造
    public FlowBean() {
        super();
    }

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

    }

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


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

编写Mapper类

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

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 {
        // 21  13568436656    192.168.100.18 www.alibaba.com    2481   24681  200

        // 获取一行
        String line = value.toString();

        // 切割
        // {21,13568436656,192.168.100.18,www.alibaba.com,2481,24681,200}
        String[] split = line.split("\t");

        // 抓取想要的数据
        // 13568436656 2481    24681  200
        String phone = split[1];
        String up = split[split.length - 3];
        String down = split[split.length - 2];

        // 封装
        outK.set(phone);
        outV.setUpFlow(Long.parseLong(up));
        outV.setDownFlow(Long.parseLong(down));
        outV.setSumFlow();

        // 写出
        context.write(outK, outV);
    }
}

编写Reducer类

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

public class FlowReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
    private FlowBean outV = new FlowBean();

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

        // 遍历集合累加值
        long totalUp = 0;
        long totalDown = 0;
        for (FlowBean value : values) {
            totalUp += value.getUpFlow();
            totalDown += value.getDownFlow();
        }
        // 封装
        outV.setUpFlow(totalUp);
        outV.setDownFlow(totalDown);
        outV.setSumFlow();

        // 写出
        context.write(key, outV);

    }
}

编写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;

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

        // 1. 获取job
        Configuration configuration = new Configuration();
        Job job = Job.getInstance(configuration);

        // 2. 设置jar包
        job.setJarByClass(FlowDriver.class);

        // 3. 关联 mapper reducer
        job.setMapperClass(FlowMapper.class);
        job.setReducerClass(FlowReducer.class);

        // 4. 设置mapper输出的key value类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(FlowBean.class);

        // 5. 设置最终数据输出的key value类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(FlowBean.class);

        // 6. 设置数据的输入输出路径
        FileInputFormat.setInputPaths(job, new Path("E:\\input\\inputflow"));
        FileOutputFormat.setOutputPath(job, new Path("E:\\output"));

        // 7. 提交job
        boolean result = job.waitForCompletion(true);
        System.exit(result ? 0 : 1);
    }
}

本地测试

image-20210713212848323

核心框架原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zv7wvK6f-1626361298971)(…/AppData/Roaming/Typora/typora-user-images/image-20210715111723932.png)]

MapTask的并行度决定Map阶段的任务处理并发度,进而影响到整个Job的处理速度

数据块:Block是HDFS物理上把数据分成一块一块。数据块是HDFS存储数据单位

数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。数据切片是MapReduce程序计算输入数据的单位,一个切片会对应启动一个MapTask

image-20210715111858768

InputFormat 数据输入

FileInputFormat 切片

image-20210715112150219

实现流程

  • 程序先找到数据存储的目录。
  • 开始遍历处理(规划切片)目录下的每一个文件
  • 获取文件大小
  • 计算切片大小
  • 默认情况下,切片大小=blocksize
  • 每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分一块切片
  • 将切片信息写到一个切片规划文件中
  • 整个切片的核心过程在getSplit()方法中完成
  • InputSplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等。
  • 提交切片规划文件到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数。

切片机制

  • 简单地按照文件的内容长度进行切片
  • 切片大小,默认等于Block大小
  • 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片

参数配置

Math.max(minSize, Math.min(maxSize, blockSize));
mapreduce.input.fileinputformat.split.minsize=1 默认值为1
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue
因此,默认情况下,切片大小=blocksize
  • maxsize(切片最大值):参数如果调得比blockSize小,则会让切片变小,而且就等于配置的这个参数的值
  • minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blockSize还大。

FileInputFormat

FileInputFormat常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、 NLineInputFormat、CombineTextInputFormat 和自定义InputFormat

TextInputFormat

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

CombineTextInputFormat

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

虚拟存储切片最大值设置CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m,切片最大值设置最好根据实际的小文件大小情况来设置具体的值

切片机制:当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现太小切片)

实操过程

在驱动类中添加Job的如下配置

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

image-20210715113128182

MapReduce工作流程

MapTask 工作机制

image-20210715220400021

Read 阶段:MapTask通过InputFormat获得的RecordReader,从输入InputSplit中解析出一个个key/value

Map 阶段:该节点主要是将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value

Collect收集阶段:在用户编写 map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。在该函数内部,它会将生成的key/value分区(调用 Partitioner),并写入一个环形内存缓冲区中

Spill 阶段:即“溢写”,当环形缓冲区满后,MapReduce 会将数据写到本地磁盘上, 生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作

溢写第一步:利用快速排序算法对缓存区内的数据进行排序,排序方式是先按照分区编号Partition进行排序,然后按照 key 进行排序这样经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序

溢写第二步:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N 表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作

溢写第三步:将分区数据的元信息写到内存索引数据结构SpillRecord 中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中

Merge 阶段:当所有数据处理完成后,MapTask对所有临时文件进行一次合并, 以确保最终只会生成一个数据文件

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

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

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

ReduceTask 工作机制

image-20210715220821685

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

Sort 阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照 MapReduce 语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop 采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此ReduceTask只需对所有数据进行一次归并排序即可

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

Partition 分区实操

自定义类继承Partitioner,重写getPartition()方法

public class ProvincePartitioner extends Partitioner<Text, FlowBean> {

    @Override
    public int getPartition(Text text, FlowBean flowBean, int i) {
        /*
            KEY var1:map的输出Key
            VALUE var2:map的输出value
            int var3
         */
        String phone = text.toString();

        int partition;
        if (phone.startsWith("136")) {
            partition = 0;
        } else if (phone.startsWith("137")) {
            partition = 1;
        } else if (phone.startsWith("138")) {
            partition = 2;
        } else if (phone.startsWith("139")) {
            partition = 3;
        } else {
            partition = 4;
        }
        return partition;
    }
}

设置自定义Partitioner并根据自定义Partitioner的逻辑设置相应数量的ReduceTask

// 设置自定义Partitioner
job.setPartitionerClass(ProvincePartitioner.class);
// 设置ReduceTask数量
job.setNumReduceTasks(5);

image-20210715114257302

image-20210715114320862

Partition 分区总结

  • 如果ReduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
  • 如果1<ReduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会Exception
  • 如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask,最终也就只会产生一个结果文件 part-r-00000;
  • 分区号必须从零开始,逐一累加。

WritableComparable 排序

排序是MapReduce框架中最重要的操作之一,MapTask和ReduceTask均会对数据按照key进行排序。该操作属于Hadoop的默认行为。**任何应用程序中的数据均会被排序,而不管逻辑上是否需要。**默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。

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

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

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

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

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

自定义WritableComparable 实操

需要实现WritableComparable接口重写compareTo方法实现排序

public class FlowBean implements WritableComparable<FlowBean> {
    ...
    @Override
    public int compareTo(FlowBean o) {
        if (this.sumFlow > o.sumFlow) {
            return -1;
        } else if (this.sumFlow < o.sumFlow) {
            return 1;
        } else {
            if (this.upFlow > o.upFlow) {
                return 1;
            } else if (this.upFlow < o.upFlow) {
                return -1;
            } else {
                return 0;
            }
        }
    }
}

WritableComparable 全排序

实现WritableComparable接口重写compareTo方法实现排序即可

WritableComparable 区内排序

第一步:自定义分区,继承Partitioner重写getPartition方法

第二步:实现WritableComparable接口重写compareTo方法实现排序

Combiner合并

Combiner是MR程序中Mapper和Reducer之外的一种组件

Combiner组件的父类就是Reducer

Combiner和Reducer的区别在于运行的位置:Combiner是在每一个MapTask所在的节点运行,Reducer是接收全局所有Mapper的输出结果

Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量

Combiner能够应用的前提是不能影响最终的业务逻辑,而且Combiner的输出kv应该跟Reducer的输入kv类型要对应起来

实现步骤

第一步:自定义一个Combiner继承Reducer,重写Reduce方法

第二步:在Job驱动类中设置job.setCombinerClass(xxx.class);

OutputFormat 数据输出

OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了OutputFormat 接口。默认输出格式为TextOutputFormat

可以自定义OutputFormat输出到MySQL/HBase/Elasticsearch等存储框架中

实现步骤

第一步:自定义一个类继承FileOutputFormat

第二步:改写RecordWriter,具体改写输出数据的方法write()

OutputFormat实操

实现:过滤输入的log日志,对于指定的输出到指定log文件,对于其他的输出到other.log文件

自定义一个类继承FileOutputFormat

public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {
    @Override
    public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
		//创建一个自定义的 RecordWriter 返回
        LogReducerWriter lrw = new LogReducerWriter(taskAttemptContext);
        return lrw;
    }

}

编写RecordWriter类

public class LogReducerWriter extends RecordWriter<Text, NullWritable> {

    private FSDataOutputStream log;
    private FSDataOutputStream otherOut;

    public LogReducerWriter(TaskAttemptContext job) {
        // 创建两条流 获取文件系统对象
        try {
            FileSystem fs = FileSystem.get(job.getConfiguration());
            log = fs.create(new Path("E:\\output\\hadoop.log"));
            otherOut = fs.create(new Path("E:\\output\\other.log"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void write(Text text, NullWritable nullWritable) throws IOException, InterruptedException {

        // 具体写 根据一行的 log 数据是否包含 baidu,判断两条输出流输出的内容
        String log = text.toString();
        if (log.contains("baidu")) {
            this.log.writeBytes(log + "\n");
        } else {
            this.otherOut.writeBytes(log + "\n");
        }

    }

    @Override
    public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
        // 关流
        IOUtils.closeStream(this.log);
        IOUtils.closeStream(this.otherOut);
    }
}

编写Driver类

//设置自定义的 outputformat
job.setOutputFormatClass(LogOutputFormat.class);
// 虽然自定义了outputformat但是因为outputformat继承自fileoutputformat而fileoutputformat要输出一个_SUCCESS 文件,所以在还得指定一个输出目录
FileOutputFormat.setOutputPath(job, new Path("D:\\logoutput"));

ReduceJoin

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

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

ReduceJoin实操

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

实操地址https://gitee.com/czshh0628/hadoop-learning/tree/master/src/main/java/czs/study/hadoop/mapreduce/reducejoin

MapJoin

MapJoin适用于一张表十分小、一张表很大的场景。

MapJoin实操

第一步:在Mapper的setup阶段,将文件读取到缓存集合中

第二步:在Driver驱动类中加载缓存

//缓存普通文件到 Task 运行节点。
job.addCacheFile(new URI("file:///e:/cache/pd.txt"));
//如果是集群运行,需要设置 HDFS 路径
job.addCacheFile(new URI("hdfs://hadoop102:8020/cache/pd.txt"));

image-20210715222534939

实操地址https://gitee.com/czshh0628/hadoop-learning/tree/master/src/main/java/czs/study/hadoop/mapreduce/mapjoin

MapReduce 开发总结

输入数据接口:InputFormat

TextInputFormat的功能逻辑是:一次读一行文本,然后将该行的起始偏移量作为 key,行内容作为 value 返回

CombineTextInputFormat 可以把多个小文件合并成一个切片处理,提高处理效率

逻辑处理接口:Mapper

根据业务需求实现其中三个方法:map() setup() cleanup ()

Partitioner 分区

有默认实现 HashPartitioner,逻辑是根据 key 的哈希值和 numReduces 来返回一个 分区号;key.hashCode()&Integer.MAXVALUE % numReduces,可以自定义分区

Comparable 排序

当用自定义的对象作为key来输出时,就必须要实现WritableComparable接口,重写其中的compareTo()方法。

部分排序:对最终输出的每一个文件进行内部排序

全排序:对所有数据进行排序,通常只有一个 Reduce

二次排序:排序的条件有两个

Combiner 合并

Combiner 合并可以提高程序执行效率,减少IO传输。但是使用时必须不能影响原有的业务处理结果

逻辑处理接口:Reducer

根据业务需求实现其中三个方法:reduce() setup() cleanup ()

输出数据接口:OutputFormat

默认实现类是TextOutputFormat,功能逻辑是:将每一个KV对,向目标文本文件输出一行,还可以自定义 OutputFormat

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值