Hadoop-MapReduce

63 阅读26分钟

MapReduce概述

定义

  • MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。
  • MapReduce核心功能是将用户编写的业务逻辑代码自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。

优势劣势

优点

  1. 易于编程,用户只关心业务逻辑。实现框架的接口。
  2. 良好的扩展性,可动态增加服务器,解决计算资源不够的问题。
  3. 高容错性,任何一台机器挂掉,可以将任务转移到其它节点。
  4. 适合海量数据计算(TB/PB)几千台服务器共同计算。

缺点

  1. 不擅长实时计算。(像Mysql毫秒级计算)
  2. 不擅长流式计算。(Sparkstreaming flink)
  3. 不擅长DAG有向无环图计算。(spark)

MapReduce核心思想

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

MapReduce进程

  • 一个完整的MapReduce程序在分布式运行时有三类实例进程:
  1. MrAppMaster: 负责整个程序的过程调度以及状态协调
  2. MapTask: 负责Map阶段的整个数据处理流程。
  3. ReduceTask:负责Reduce阶段的整个数据处理流程。

常用数据序列化类型

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

MapReduce编程规范

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

Mapper阶段

  • 用户自定义的Mapper要继承自己的父类 extent Mapper<Object, Text, Text, IntWritable>
  • Mapper的输入数据是KV对的形式(KV的类型可自定义)(K是内容所在位置的偏移量, V是一行内容)
  • Mapper中的业务逻辑写在map()方法中
  • Mapper的输出数据是KV对的形式(KV的类型可自定义)
  • map()方法(MapTask进程)对每一个<K, V>调用一次 (按行处理)

Reduce阶段

  • 用户自定义的Reduce要继承自己的父类
  • Reduce的输入数据对应Mapper的输出数据类型,也是KV
  • Reduce的业务逻辑写在reduce()方法中
  • Reducetask进程对每一组相同k的<k, v>组调用一次reduce()方法 (有多少次key,进入多少次reduce)

Driver阶段

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

WordCount案例

需求分析

  • 按照MapReduce编程规范,分别编写Mapper,Reducer, Driver
  1. 输入数据 word.txt(包含一堆单词)

  2. 输出数据(一堆单次以及出现次数的KV对, 并按字典序排列)

  3. Mapper:

    3.1. 将MapTask传给我们的文本内容转为String

    3.2. 根据空格将这一行切分为单词

    3.3. 将单词输出为<单词,1>

  4. Reducer:

    4.1. 汇总各个key的个数

    4.2. 输出该key的总次数

  5. Driver

    5.1. 获取配置信息,获取Job对象实例

    5.2. 指定本程序的jar包所在的本地路径

    5.3. 关联Mapper/Reducer业务类

    5.4. 指定Mapper输出数据的kv类型

    5.5. 指定最终输出的数据的kv类型

    5.6. 指定job的输入原始文件所在目录

    5.7. 指定job的输出结果所在目录(不能事先存在)

    5.8. 提交作业

环境准备

<dependencies>
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-client</artifactId>
        <version>3.3.2</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>2.0.5</version>
    </dependency>
</dependencies>
<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>
# 在resource下新建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

package com.atguigu.mapreduce.wordcount;

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类型:InWritable
 */
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {

    private Text k = new Text();
    private IntWritable v = new IntWritable(1);

    @Override
    protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, 
        IntWritable>.Context context) throws IOException, InterruptedException {
        //1. 将Text value 转换为String类型
        String s = value.toString();

        //2. 切割s
        String[] words = s.split(" ");

        //3. 将key value写入Contex中
        for (String word : words) {
            k.set(word);
            context.write(k,v);
        }

    }
}

Reducer

package com.atguigu.mapreduce.wordcount;

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: 输出的值类型
 */
public class WordCoundReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    private IntWritable val = new IntWritable();
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, 
    Text, IntWritable>.Context context) throws IOException, InterruptedException {
        int sum = 0;
        //1. 累加求和
        for (IntWritable value : values) {
            sum += value.get();
        }

        //2. 输出
        val.set(sum);
        context.write(key, val);
    }
}

Driver

package com.atguigu.mapreduce.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.FileOutputStream;
import java.io.IOException;

public class WordCountDriver {
    public static void main(String[] args) throws IOException, InterruptedException, 
    ClassNotFoundException {
        //1. 获取配置信息,获取Job对象实例
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

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

        //3. 关联Mapper和Reducer的jar
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCoundReducer.class);

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

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

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

        //7. 提交作业(true,代表开启监控并打印日志)
        boolean b = job.waitForCompletion(true);

        System.exit(b ? 0 : 1);

    }
}

Hadoop序列化

序列化概述

什么是序列化

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

为什么要序列化

  • 序列化可以存储"活的"对象,可以将"活的"对象发送到远程计算机

为什么不用Java序列化

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

hadoop序列化特点

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

Demo

FlowBean

package com.atguigu.mapreduce.writable;

import org.apache.commons.math3.stat.descriptive.summary.Sum;
import org.apache.hadoop.io.Writable;

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

/**
 * 1、定义类实现writable接口
 * 2、重写序列化和反序列化函数
 * 3、重写空参构造
 * 4、重写toString方法
 */
public class FlowBean implements Writable {

    private long UpFlow;
    private long DownFlow;
    private long SumFlow;

    public long getUpFlow() {
        return UpFlow;
    }

    public void setUpFlow(long upFlow) {
        UpFlow = upFlow;
    }

    public long getDownFlow() {
        return DownFlow;
    }

    public void setDownFlow(long downFlow) {
        DownFlow = downFlow;
    }

    public long getSumFlow() {
        return SumFlow;
    }

    public void setSumFlow(long sumFlow) {
        SumFlow = sumFlow;
    }

    public void setSumFlow() {
        SumFlow = UpFlow + DownFlow;
    }

    public FlowBean() {
    }

    @Override
    public void write(DataOutput dataOutput) throws IOException {
        //实现序列化
        dataOutput.writeLong(UpFlow);
        dataOutput.writeLong(DownFlow);
        dataOutput.writeLong(SumFlow);
    }

    @Override
    public void readFields(DataInput dataInput) throws IOException {
        //实现反序列化
        UpFlow = dataInput.readLong();
        DownFlow = dataInput.readLong();
        SumFlow = dataInput.readLong();
    }

    @Override
    public String toString() {
        return UpFlow + "\t" + DownFlow + "\t" + SumFlow;
    }
}

FlowMapper

package com.atguigu.mapreduce.writable;

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 k = new Text();
    private FlowBean v = new FlowBean();

    @Override
    protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, 
    FlowBean>.Context context) throws IOException, InterruptedException {
        // 1. 获取一行数据
        // 7   13560436666    120.196.100.99    1116       954         200
        String[] split = value.toString().split("\t");

        // 2. 读取想要的信息
        String phone = split[1];
        String upFlow = split[split.length - 3];
        String downFlow = split[split.length - 2];

        //3. 封装kv
        k.set(phone);
        v.setUpFlow(Long.parseLong(upFlow));
        v.setDownFlow(Long.parseLong(downFlow));
        v.setSumFlow();

        //4.写入context
        context.write(k, v);
    }

}

FlowReducer

package com.atguigu.mapreduce.writable;

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, Reducer<Text, FlowBean, Text, 
    FlowBean>.Context context) throws IOException, InterruptedException {

        long sumUp = 0;
        long sumDown = 0;

        // 1. 遍历values,求和
        for (FlowBean value : values) {
            sumUp += value.getUpFlow();
            sumDown += value.getDownFlow();
        }

        //2. 将数据写入outV对象
        outV.setUpFlow(sumUp);
        outV.setDownFlow(sumDown);
        outV.setSumFlow();

        // 3. reduce输出结果
        context.write(key, outV);
    }
}

FlowDriver

package com.atguigu.mapreduce.writable;

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 FlowDeiver {
    public static void main(String[] args) throws IOException, InterruptedException, 
    ClassNotFoundException {
        //1. 设置job工作对象,配置conf
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
        //2. 设置工作的jar包
        job.setJarByClass(FlowDeiver.class);
        //3. 关联Mapper和Reducer
        job.setMapperClass(FlowMapper.class);
        job.setReducerClass(FlowReducer.class);
        //4. 设置Mapper输出的kv类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(FlowBean.class);

        //5. 设置最终输出的kv类型
        job.setOutputKeyClass(Text.class);
        job.setMapOutputValueClass(FlowBean.class);

        //6. 设置输入输出数据的路径
        FileInputFormat.setInputPaths(job, new 
        Path("D:\Code\hadoop\Input\phone_data"));
        FileOutputFormat.setOutputPath(job, new 
        Path("D:\Code\hadoop\Output\phone_data"));

        //7. 提交job

        boolean res = job.waitForCompletion(true);

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

MapReduce核心框架原理

  1. Maptask: Input(可自定义数据来源, 通过InputFormat向Mapper输入数据)
  2. Maptask: Mapper(自定义map操作, 数据流向Shuffle)
  3. Shuffle: 用于数据的排序,合并,分区等功能的实现,结束后,数据流向Reducer
  4. ReduceTask: Reducer(自定义reduce操作,通过OutputFormat输出数据)
  5. ReduceTask: Output(可自定义输出输出目标)

InputFormat数据输入

切片与MapTask并行度决定机制

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

Job提交流程

源码流程

  • job.submit()
  • 建立连接,判断是本地运行环境还是Yarn运行环境,创建相应的clientProtocolProvider与clientprotocol ( connect() )
  • 提交job ( submitter.submitJobInternal(Job.this, cluster); )
    • 校验输出路径是否合规
    • 创建给集群提交数据的Stag路径
    • 获取Jobid,并创建Job路径
    • 拷贝jar包到集群(本地模式不提交)
    • 计算切片,生成切片规划文件 FileInputFormat.getSplits()
    • 向Stag路径写XML配置文件
    • 提交Job,返回提交状态

FileInputFormat切片源码解析

  • 程序先找到数据存储的目录
  • 开始遍历处理(规划切片)目录下的每一个文件
  • 遍历第一个文件ss.txt
    • 获取文件大小fs.sizeOf(ss.txt)
    • 计算切片大小
    # 通常切片大小等于块大小,本地模式下块大小默认为32M
    computeSplitSize(Math.max(minSize, Math.min(maxSize, blocksize))) = blocksize = 
    128M
    
    • 默认情况下,切片大小等于块大小blocksize
    • 开始切,形成第一个切片,ss.txt——0:128M, 第二个切片ss.txt——128:256M, 第三个切片ss.txt——256:300M, (每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就被划分为一块切片)
    • 将切片信息写到一个切片规划文件(InputSplit)中
    • 整个切片的核心过程在getSplit()方法中完成
    • InputSplit只记录了切片的元数据信息,比如起始位置,长度以及所在的节点列表等。
  • 提交切片规划文件到YARN上,YARN上的MrAppmaster就可以根据切片规划文件计算开启MapTask个数
  • 备注:切片大小设置
    • maxsize(切片最大值):参数如果调的比blocksize小,则会让切片变小,而且就等于配置的这个参数的值
    • minsize(切片最小值):参数调的比blocksize大,则可以让切片变得比blockSize还大
  • 备注:获取切片信息API
//获取切片的文件名称
String name = inputSplit.getPath().getName();
//根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();

FileinputFormat实现类

  • 思考:在运行MapReduce程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。那么针对不同的数据类型,MapReduce是如何读取这些数据的?
  • FileInputFormat常见的接口实现包括:TextInputFormat、keyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定义InputFormat等。

CombineTextInputFormat切片机制

  • 应用场景
    • CombineTextInputFormat用于小文件过多的场景,它可以将小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。
  • 虚拟存储切片的最大值设置
    • CombineTextInputFormat.setMaxInputSplitSize(job, 4194304); //4m
    • 注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值
  • 切片机制
    • 生成切片的过程包括:虚拟存储过程和切片两部分
    • 虚拟存储过程:将大小小于切片最大值的文件单独划分一块,若大于切片最大值,将其等分,每份均小于切片最大值,如5.1M的文件,大于4M,将其等分为2.55M的两个文件
    • 切片过程:(a)判断虚拟存储文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片。(b)如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
  • 案例,wordCount中,在Driver类中设置切片机制为CombineTextInputFormat,并设置虚拟存储切片最大值
package com.atguigu.mapreduce.combineTextInputformat;

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, InterruptedException, 
    ClassNotFoundException {
        //1. 创建Job以及配置文件
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        //2. 指定程序运行的jar包
        job.setJarByClass(WordCountDriver.class);

        //3. 关联Mapper方法与Reducer方法
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);

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

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

        // 设置FileInputFormat类
        job.setInputFormatClass(CombineTextInputFormat.class);

        // 设置虚拟存储切片的最大值
        CombineTextInputFormat.setMaxInputSplitSize(job, 20971520);

        //6. 设置输入路径与输出路径
        FileInputFormat.setInputPaths(job, new 
        Path("D:\Code\hadoop\Input\smallFile"));
        FileOutputFormat.setOutputPath(job, new 
        Path("D:\Code\hadoop\Output\smallFiles"));

        //7. 提交程序,并开启监控,打印日志
        boolean b = job.waitForCompletion(true);

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

Shuffle

Shuffle机制

  • Map方法之后,Reduce方法之前的数据处理过程,被称之为Shuffle,其中可进行数据的排序,分区,压缩,合并等操作。

Shuffle工作过程

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

Partition分区

  • 问题引出:要求将统计结果按照条件输出到不同的文件中(分区)。比如:将统计结果按照手机归属地不同省份输出到不同文件中(分区)。
  • 默认Partitioner分区
# 默认分区是根据key的hashCode对ReduceTasks个数取模得到的。用户没办法控制哪个key存储到哪个分区.
public int getPartition(K key, V value,
                        int numReduceTasks) {
  return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
  • 自定义Partitioner步骤
# 1. 自定义类集成Partition,重写getPartition()方法
public class Custompartitioner extends Partitioner<Text, FlowBean> {

  @override
  public int getPartition(Text key, FlowBean value,
                          int numReduceTasks) {
      // 控制分区逻辑代码
      return partition;
  }

}
# 2. 在Job驱动中,设置自定义Partitioner
job.setPartitionerClass(CustomPartitioner.class);
# 3. 自定义Partition后,要根据自定义Partition的逻辑设置相应数量的ReduceTask
job.setNumReduceTasks(5);
  • 实例
    • Flowpartition
    package com.atguigu.mapreduce.partitioner2;
    
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Partitioner;
    
    public class FlowPartition extends Partitioner<Text, FlowBean> {
        @Override
        public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
    
            String s = text.toString().substring(0, 3);
            # 根据手机前三位进行分区,分区号必须从0开始且是连续的整数,否则不合法  
            if (s.equals("135")) return 0;
            else if (s.equals("136")) return 1;
            else if (s.equals("137")) return 2;
            else if (s.equals("138")) return 3;
            else return 4;
        }
    }
    
    • FlowDriver
    package com.atguigu.mapreduce.partitioner2;
    
    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, InterruptedException, 
        ClassNotFoundException {
            //1. 创建带配置文件的job
            Configuration conf = new Configuration();
            Job job = Job.getInstance(conf);
            //2. 指定关联的jar包
            job.setJarByClass(FlowDriver.class);
            //3. 指定map类与reduce类
            job.setMapperClass(FlowMapper.class);
            job.setReducerClass(FlowReducer.class);
    
            //4. 指定mapper的数据输出类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(FlowBean.class);
    
            //5. 指定程序最终的输出类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(FlowBean.class);
    
            //设置getPartition类
            job.setPartitionerClass(FlowPartition.class);
    
            //设置reduceTask数量
            job.setNumReduceTasks(5);
    
            //6. 指定输入输出数据路径
            FileInputFormat.setInputPaths(job, new Path("D:\Code\hadoop\Input\phone_data"));
            FileOutputFormat.setOutputPath(job, new Path("D:\Code\hadoop\Output\phone_data2"));
    
            //7. 提交job, 开启监控,打印日志
            boolean b = job.waitForCompletion(true);
    
            System.exit(b ? 0 : 1);
        }
    }
    
  • 分区总结
  • 如果Reducetask的数量 > getPartition的数量,则会多产生几个空的输出文件part-r-000xx;
  • 如果1 < Reducetask的数量 < getPartition的数量,则会有一部分数据无处安放,会Exception;
  • 如果Reducetask的数量 = 1, 则不管MapTask端输出多少个分区文件,最终结果都交给一个ReduceTask,最终也就只产生一个结果文件part-r-00000;
  • 分区号必须从0开始,逐一累加

WritableComparable排序

  • 概述
  • 排序是MapReduce框架中最重要的操作之一。
  • Maptesk和Reducetesk均会对数据按照key进行排序,该操作数据hadoop默认行为。任何应用程序中的数据均会被排序,不管逻辑上是否需要。
  • 默认按照字典顺序排序,且实现该排序方法是快速排序。
  • Maptask中,数据先放在环形缓冲区中,达到阈值后,对缓冲区中数据进行快排,并溢写到磁盘文件,数据处理完毕后,会对磁盘上的文件进行归并排序。
  • 对于ReduceTask,从每个MapTask上远程拷贝数据文件,若文件太大(超过一定听阈值),则溢写到磁盘文件,否则存储在内存中。若磁盘上文件数量达到一定阈值,会进行一次归并排序以生成一个更大的文件,如果内存中文件大小或者数据超过一定阈值,则进行归并后写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
  • 排序分类
  • 部分排序: 每个输出文件内部有序。
  • 全排序:最终输出结果只有一个,且文件内部有序。
  • 辅助排序(GroupingComparator分组): 在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到一个reduce方法时,可以采用分组排序。
  • 二次排序: 自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。
  • 全排序与二次排序实例
    • FlowBean
    package com.atguigu.mapreduce.partition3;
    
    import org.apache.hadoop.io.Writable;
    import org.apache.hadoop.io.WritableComparable;
    
    import java.io.DataInput;
    import java.io.DataOutput;
    import java.io.IOException;
    
    /**
     * 1. 定义类实现Writable接口
     * 2. 实现序列化和反序列化
     * 3. 重写空参构造函数
     * 4. 重写toString()方法
     */
    public class FlowBean implements WritableComparable<FlowBean> {
    
        int lowFlow;
    
        int upFLow;
        int sumFlow;
    
        public FlowBean() {
        }
    
        @Override
        public String toString() {
            return lowFlow + "\t" + upFLow + "\t" + sumFlow;
        }
    
        public int getLowFlow() {
            return lowFlow;
        }
    
        public void setLowFlow(int lowFlow) {
            this.lowFlow = lowFlow;
        }
    
        public int getUpFLow() {
            return upFLow;
        }
    
        public void setUpFLow(int upFLow) {
            this.upFLow = upFLow;
        }
    
        public int getSumFlow() {
            return sumFlow;
        }
    
        public void setSumFlow() {
            this.sumFlow = lowFlow + upFLow;
        }
    
        @Override
        public void write(DataOutput dataOutput) throws IOException {
            //实现序列化
            dataOutput.writeInt(lowFlow);
            dataOutput.writeInt(upFLow);
            dataOutput.writeInt(sumFlow);
        }
    
        @Override
        public void readFields(DataInput dataInput) throws IOException {
            //实现反序列化
            lowFlow = dataInput.readInt();
            upFLow = dataInput.readInt();
            sumFlow = dataInput.readInt();
        }
    
        @Override
        public int compareTo(FlowBean o) {
            //按照总流量进行逆序排列
            if (sumFlow < o.sumFlow) return 1;
            else if (sumFlow > o.sumFlow) return -1;
            else {
                //二次排序
                if (upFLow < o.upFLow) return -1;
                else if (upFLow > o.upFLow) return 1;
                else return 0;
            }
        }
    }
    
    • FlowMapper
    package com.atguigu.mapreduce.partition3;
    
    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, FlowBean, Text> {
    
    
        Text v = new Text();
        FlowBean k = new FlowBean();
    
        @Override
        protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, FlowBean, 
        Text>.Context context) throws IOException, InterruptedException {
            // 1. 获取一行数据
            // 13470253144 180    180    360
            String[] s = value.toString().split("\t");
    
            String phone = s[0];
            String upFlow = s[s.length - 3];
            String lowFlow = s[s.length - 2];
    
            v.set(phone);
            k.setLowFlow(Integer.parseInt(lowFlow));
            k.setUpFLow(Integer.parseInt(upFlow));
            k.setSumFlow();
    
            context.write(k, v);
        }
    }
    
    • FlowReducer
    package com.atguigu.mapreduce.partition3;
    
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Reducer;
    
    import java.io.IOException;
    
    public class FlowReducer extends Reducer<FlowBean, Text, Text, FlowBean> {
    
        FlowBean v = new FlowBean();
    
        @Override
        protected void reduce(FlowBean key, Iterable<Text> values, Reducer<FlowBean, Text, 
        Text, FlowBean>.Context context) throws IOException, InterruptedException {
            for (Text val: values) {
                context.write(val, key);
            }
        }
    }
    
    • FlowDriver
    package com.atguigu.mapreduce.partition3;
    
    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, InterruptedException, 
        ClassNotFoundException {
            //1. 创建带配置文件的job
            Configuration conf = new Configuration();
            Job job = Job.getInstance(conf);
            //2. 指定关联的jar包
            job.setJarByClass(FlowDriver.class);
            //3. 指定map类与reduce类
            job.setMapperClass(FlowMapper.class);
            job.setReducerClass(FlowReducer.class);
    
            //4. 指定mapper的数据输出类型
            job.setMapOutputKeyClass(FlowBean.class);
            job.setMapOutputValueClass(Text.class);
    
            //5. 指定程序最终的输出类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(FlowBean.class);
    
            //6. 指定输入输出数据路径
            FileInputFormat.setInputPaths(job, new Path("D:\Code\hadoop\Output\phone_data"));
            FileOutputFormat.setOutputPath(job, new Path("D:\Code\hadoop\Output\phone_data2"));
    
            //7. 提交job, 开启监控,打印日志
            boolean b = job.waitForCompletion(true);
    
            System.exit(b ? 0 : 1);
        }
    }
    
  • 区内排序(部分)实例, 在以上代码基础上添加分区代码,按照手机号码开头排序
    • FlowPartition
    package com.atguigu.mapreduce.partition3;
    
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Partitioner;
    
    public class FlowPartition extends Partitioner<FlowBean, Text> {
        @Override
        public int getPartition(FlowBean flowBean, Text text, int numPartitions) {
            String phoneHead = text.toString().substring(0, 3);
    
            if (phoneHead.equals("135")) return 0;
            else if (phoneHead.equals("136")) return 1;
            else if (phoneHead.equals("137")) return 2;
            else if (phoneHead.equals("139")) return 3;
            else return 4;
        }
    }
    
    • FlowDriver
    // 添加以下内容
    // 设置partition类
    job.setPartitionerClass(FlowPartition.class);
    
    // 设置ReduceTask的数量
    job.setNumReduceTasks(5);
    

Combiner合并

  • Combiner是MR程序中Mapper和Reducer之外的一种组件
  • Combiner组件的父类就是Reducer
  • Combiner和Reducer的区别在于运行的位置, Combiner是在每一个MapTask所在的节点运行, Reducer是接受全局所有Mapper的输出结果。
  • Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输流量。
  • Combiner能够应用的前提是不能影响最终的业务逻辑, 而且, Combiner的输出kv应该和Reducer的输入kv类型要对应起来。

输出数据OutputFormat

  • 自定义OutputFormat,需要继承FileOutputFormat类,并重写getRecordWriter方法,返回一个自定义的RedordWriter类型对象(该类需要继承RecordWriter类,并实现带参构造函数,以及write,close方法)
  • LogOutputFormat
public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {
    @Override
    public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws 
    IOException, InterruptedException {
        LogRecordWriter lrw = new LogRecordWriter(job);
        return lrw;
    }
}
  • LogRecoreWriter
public class LogRecordWriter extends RecordWriter<Text, NullWritable> {

    FSDataOutputStream atguiguout;
    FSDataOutputStream otherout;

    public LogRecordWriter(TaskAttemptContext job) {
        try {
            // 获取文件系统对象
            FileSystem fs = FileSystem.get(job.getConfiguration());

            // 用文件系统对象创建两个输出流对应不同的目录
            atguiguout = fs.create(new Path("D:\Code\hadoop\Output\net\atguigu.log"));
            otherout = fs.create(new Path("D:\Code\hadoop\Output\net\other.log"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void write(Text key, NullWritable value) throws IOException, InterruptedException {
        String net = key.toString();
        if (net.contains("atguigu")) {
            atguiguout.writeBytes(net + "\n");
        } else {
            otherout.writeBytes(net + "\n");
        }
    }

    @Override
    public void close(TaskAttemptContext context) throws IOException, InterruptedException {
        atguiguout.close();
        otherout.close();
    }
}
  • LogDriver
/ 指定FileOutputFormat类
job.setOutputFormatClass(LogOutputFormat.class);

//6. 设置输入和输出路径(在outputformat类中指定文件输出路径后, 此处设置输出路径用于输出success文件)
FileInputFormat.setInputPaths(job, new Path("D:\Code\hadoop\Input\net"));
FileOutputFormat.setOutputPath(job, new Path("D:\Code\hadoop\Output\net\success"));

MapReduce内核源码解析

ReduceTask并行度决定机制

  • MapTask并行度由切片个数决定,切片个数由输入文件和切片规则决定(最大值最小值,块大小)。
  • ReduceTask = 0, 表示没有Reduce阶段,输出文件个数和Map个数一致。
  • ReduceTask默认值就是1,所以输出文件个数为1。
  • 如果数据分布不均匀,就有可能在Reduce阶段产生数据倾斜。
  • ReduceTask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能由一个Reducetask。
  • 具体多少个ReduceTask,需要根据集群性能而定。
  • 如果分区数不是1,但ReduceTask为1,是否执行分区过程。答案是不执行分区过程。因为在MapTask源码中,执行分区的前提是判断ReduceNum个数是否大于1。不大于1肯定不执行。

Join

Reduce Join

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

代码实现

  • OrderBean
public class OrderBean implements WritableComparable<OrderBean> {
    String id;
    String pid;
    String pname;
    int amount;
    String pd;

    public OrderBean(){}

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getPid() {
        return pid;
    }

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

    public String getPname() {
        return pname;
    }

    public void setPname(String pname) {
        this.pname = pname;
    }

    public int getAmount() {
        return amount;
    }

    public void setAmount(int amount) {
        this.amount = amount;
    }

    public String getPd() {
        return pd;
    }

    public void setPd(String pd) {
        this.pd = pd;
    }

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

    @Override
    public void readFields(DataInput in) throws IOException {
        id = in.readUTF();
        pid = in.readUTF();
        pname = in.readUTF();
        amount = in.readInt();
        pd = in.readUTF();
    }

    @Override
    public int compareTo(OrderBean o) {
        int a = Integer.parseInt(id);
        int b = Integer.parseInt(o.id);
        return Integer.compare(b, a);
    }

    @Override
    public String toString() {
        return  id + "\t" + pname + "\t" + amount;
    }
}
  • OrderMapper
public class OrderMapper extends Mapper<LongWritable, Text, Text, OrderBean> {

    OrderBean orderBean = new OrderBean();
    Text k = new Text();
    private String[] strs;
    private String filename;
    private FileSplit split;


    @Override
    protected void setup(Mapper<LongWritable, Text, Text, OrderBean>.Context context) throws 
    IOException, InterruptedException {
        // 根据读入的文件名称判断该位orderbean填充哪些字段
        split = (FileSplit) context.getInputSplit();

        filename = split.getPath().getName();
    }

    @Override
    protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, 
    OrderBean>.Context context) throws IOException, InterruptedException {
        strs = value.toString().split("\t");

        if (filename.contains("order")) {
            // 1001    01 1
            orderBean.setId(strs[0]);
            orderBean.setPid(strs[1]);
            orderBean.setAmount(Integer.parseInt(strs[2]));
            orderBean.setPd("order");
            orderBean.setPname("");
            k.set(strs[1]);
        }
        else {
            // 01  小米
            orderBean.setPid(strs[0]);
            orderBean.setPname(strs[1]);
            orderBean.setPd("pd");
            orderBean.setAmount(0);
            orderBean.setId("");
            k.set(strs[0]);
        }

        context.write(k, orderBean);
    }
}
  • OrderReducer
public class OrderReducer extends Reducer<Text, OrderBean, OrderBean, NullWritable> {

    @Override
    protected void reduce(Text key, Iterable<OrderBean> values, Reducer<Text, OrderBean, 
    OrderBean, NullWritable>.Context context) throws IOException, InterruptedException {
        List<OrderBean> orders = new ArrayList<>();
        OrderBean pd = new OrderBean();
        // 循环遍历values,并判断数据来自哪张表, 来自order的数据进入集合orders,来自pd的数据存入pd对象中.
        for (OrderBean value : values) {
            if (value.pd.equals("order")) {
                // 由于在一个ReduceTask中,遍历values时的value会被一直复用,且ArrayList的add是浅拷
                // 贝,因此,添加进orders中的数据全部会指向新加入的数据,因此,此处需新建一个对象,再
                // 将其加入orders
                OrderBean tmpOrder = new OrderBean();
                try {
                    BeanUtils.copyProperties(tmpOrder, value);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                } catch (InvocationTargetException e) {
                    throw new RuntimeException(e);
                }
                orders.add(tmpOrder);
            }
            else {
                try {
                    BeanUtils.copyProperties(pd, value);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                } catch (InvocationTargetException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        for (OrderBean order : orders) {
            order.setPname(pd.getPname());
            context.write(order, NullWritable.get());
        }
    }
}
  • 存在问题
  • 在这种情况下,合并操作是在Reducer阶段完成的,Reducer端的处理压力太大,Map节点的运算负载很低,资源利用率不高,且在Reduce阶段极易产生数据倾斜。
  • 解决方案:Map端实现数据合并。

MapJoin

  • 使用场景: 适用于一张表十分小,一张表很大的场景。
  • 优点:解决在Reduce端处理过多的表产生的数据倾斜。在Map端缓存多张表,提前处理业务逻辑,增加Map端业务,减少Reduce端数据的压力,尽可能较少数据倾斜。
  • 具体办法: 采用DistributedCache;(1)在Mapper的setup阶段,将文件读取到缓存集合中。(2)在Driver驱动类中加载缓存
// 缓存普通文件到Task运行节点
job.addCacheFile(new URI("file///e:/cache/pd.txt"));
// 如果时集群运行,需要设置HDFS路径
job.addCacheFIle(new URI("hdfs://hadoop102:8082/cache/pd.txt"));
  • MapJoinDriver
//加载缓存数据
job.addCacheFile(new URI("file:///D:/Code/hadoop/Input/order/pd.txt"));
// Map端Join的逻辑不需要Reduce阶段,设置reduceTask的数量为0
job.setNumReduceTasks(0);
  • MapJoinMapper
public class MapJoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> {

    HashMap<String, String> pdmap = new HashMap<>();
    Text k = new Text();

    @Override
    protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws 
    IOException, InterruptedException {
        // 通过缓存文件得到小表数据pd.txt
        URI[] files = context.getCacheFiles();
        Path path = new Path(files[0]);

        // 获取文件系统对象,并开流
        FileSystem fileSystem = FileSystem.get(context.getConfiguration());
        FSDataInputStream open = fileSystem.open(path);

        // 通过包装流转换为reader, 方便按行读取
        BufferedReader reader = new BufferedReader(new InputStreamReader(open));

        String line;
        while (StringUtils.isNotEmpty(line = reader.readLine())) {
            String[] split = line.split("\t");
            pdmap.put(split[0], split[1]);
        }

        // 关流
         IOUtils.closeStream(reader);
//        reader.close();
    }

    @Override
    protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, 
    NullWritable>.Context context) throws IOException, InterruptedException {
        String[] split = value.toString().split("\t");
        String id = split[0];
        String pid = split[1];
        String amount = split[2];
        String pname = pdmap.get(pid);

        k.set(id + "\t" + pname + "\t" + amount);
        context.write(k, NullWritable.get());
    }
}

ETL数据清洗案例

  • ETL,抽取(Extract), 转换(Transform),加载(Load)。在运行核心业务MapReduce前,往往要对数据进行清洗,清洗掉不符合用户要求的数据。清洗过程往往只需要运行Mapper程序,不需要Reduce程序。

代码案例

public class WebLogMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
    @Override
    protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, 
    NullWritable>.Context context) throws IOException, InterruptedException {
        // 1. 获取一行日志
        String log = value.toString();
        // 2. 解析日志
        boolean res = parseLog(log, context);
        // 3. 日志不合法退出
        if (!res) return;
        // 4. 日志合法则写入
        context.write(value, NullWritable.get());
    }

    private boolean parseLog(String log, Mapper<LongWritable, Text, Text, NullWritable>.Context 
    context) {
        // 1.切割
        String[] s = log.split(" ");
        // 2.判断长度
        if (s.length > 11) return true;
        else return false;
    }

}

总结

InputFormat

  • 默认的是TextInputFormat, kv key偏移量, v 一行内容
  • 处理小文件CombinTextInputFormat 把多个文件合并到一起同一切片

Mapper

  • setup() 初始化, map() 用户的业务逻辑, cleanup() 关闭资源

分区

  • 默认分区HashPartitioner,默认按照key的hash值 % numreducetask个数
  • 自定义分区

排序

  • 部分排序: 每个输出的文件内部有序
  • 全排序: 一个reduce, 对所有的数据大排序
  • 二次排序: 自定义排序范畴, 实现WritableCompare接口,重写compareTo接口

Combiner

  • 前提:不影响最终的业务逻辑
  • 提前聚合, 在map端 =》 解决数据倾斜的一个办法

Reducer

  • 用户的业务逻辑 setup() 初始化, reduce() 用户的业务逻辑, cleanup()关闭资源

OutputFormat

  • 默认TextOutputFormat 按行输出到文件
  • 自定义

Hadoop压缩

概述

优缺点

  • 优点:以减少磁盘IO、减少磁盘存储空间
  • 缺点:增加CPU开销

压缩原则

  • 运算密集型job, 少用压缩
  • IO密集型Job, 多用压缩

MR支持的压缩编码

压缩算法对比介绍

压缩格式Hadoop自带?算法文件扩展名是否可切片换成压缩格式后,原来的程序是否需要修改
DEFAULT是,直接使用DRFAULT.default和文本处理一样,不需要修改
Gzip是,直接使用DRFAULT.gz和文本处理一样,不需要修改
bzip2是,直接使用bzip2.bz2和文本处理一样,不需要修改
LZO否,需要安装LZO.lzo需要建索引,还需指定输入格式
Snappy是,直接使用Snappy.snappy和文本处理一样,不需要修改

压缩性能比较

压缩算法原始文件大小压缩文件大小压缩速度解压速度
gzip8.3G1.8G17.5MB/s58MB/s
bzip28.3G1.1G2.4MB/s9.5MB/s
LZO8.3G2.9G49.3MB/s74.6MB/s
Snappy250MB/s500MB/s

压缩方式选择

Gzip压缩

  • 优点:压缩率比较高
  • 缺点: 不支持Split; 压缩/解压速度一般

Bzip2压缩

  • 优点:压缩率高,支持Split
  • 缺点:压缩/解压速度慢

Lzo压缩

  • 优点:压缩/解压速度比较快,支持Split
  • 缺点:压缩率一般,想支持切牌你需要额外创建索引

Snappy压缩

  • 优点: 压缩和解压速度快
  • 缺点: 不支持Split, 压缩率一般

压缩位置选择

  • 压缩可以在MapReduce作用的任意阶段启用
  • 在Map前,输入端采用压缩:无需指定使用的编码解码方式。Hadoop自动检查文件扩展名,若扩展名能够匹配,就会用恰当的编码方式对文件进行压缩和解压。 在企业开发中,需要考虑的因素:(1)数据量小于块大小,重点考虑压缩和解压缩比较快的LZO/Snappy。(2)如果数据量非常大,重点考虑支持切片的Bzip2和LZO。
  • Map到Reduce之间,Mapper输出采用压缩: 企业开发中如何选择,为了减少MapTask和ReduceTask之间的网络IO。重点考虑压缩和解压缩快的LZO、Snappy
  • Reduce之后的阶段,Reducer输出采用压缩: 看需求:(1)如果数据永久保存,考虑压缩率比较高的Bzip2和Gzip。(2)如果作为下一个MapReduce输入,需要考虑数据量和是否支持切片。

压缩参数配置

压缩格式对应的编码/解码器
DEFAULTorg.apache.hadoop.io.compress.DefaultCodec
gziporg.apache.hadoop.io.compress.GzipCodec
bzip2org.apache.hadoop.io.compress.BZip2Codec
LZOcom.hadoop.compression.lzo.LzopCodec
Snappyorg.apache.hadoop.io.compress.SnappyCodec
  • 在Hadoop中启用压缩,可配置如下参数
参数默认值阶段建议
io.compression.codecs  (在core-site.xml中配置)无,这个需要在命令行输入hadoop checknative查看输入压缩Hadoop使用文件扩展名判断是否支持某种编解码器
mapreduce.map.output.compress(在mapred-site.xml中配置)falsemapper输出这个参数设为true启用压缩
mapreduce.map.output.compress.codec(在mapred-site.xml中配置)org.apache.hadoop.io.compress.DefaultCodecmapper输出企业多使用LZO或Snappy编解码器在此阶段压缩数据
mapreduce.output.fileoutputformat.compress(在mapred-site.xml中配置)falsereducer输出这个参数设为true启用压缩
mapreduce.output.fileoutputformat.compress.codec(在mapred-site.xml中配置)org.apache.hadoop.io.compress.DefaultCodecreducer输出使用标准工具或者编解码器,如gzip和bzip2
  • Map端采用输出压缩
//开启Map端输出压缩
conf.setBoolean("mapreduce.map.output.compress", true);

/**
 * setClass的三个参数
 * 1. 参数名,表示要配置的参数名字
 * 2. 类类型,表示要配置的参数的值的类型
 * 3. 父类类型,表示要设置的配置参数的值所继承的父类类型
 */
//设置map端输出压缩方式, 
conf.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);
  • Reduce端采用输出压缩
// 方式1, 在job.getInstance(conf)前配置
//设置reduce端开启压缩
conf.set(FileOutputFormat.COMPRESS, "true");

// 设置reduce端压缩方式
conf.set("mapreduce.output.fileoutputformat.compress.codec", GzipCodec.class.getName());

//方式2
//设置reduce端压缩开启
FileOutputFormat.setCompressOutput(job, true);

//设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);
FileOutputFormat.setOutputCompressorClass(job, DefaultCodec.class);