MapReduce-基础入门必知必会

211 阅读17分钟

本文内容均来自于尚硅谷Hadoop3.0课程,感兴趣的同学可以在B站找到官方的视频教程哈

1、概述

1.1、MapReduce定义

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

1.2、优缺点

  • 优点

    • 易于编程。程序员只需关心业务代码
    • 良好扩展性。可以动态增减服务器,解决资源问题
    • 高容错性。支持failover
    • 适合海量数据计算(TB/PB)。几千台服务器共同计算
  • 缺点

    • 不擅长实时计算。
    • 不擅长流式计算。
    • 不擅长DAG有向无环图计算。

1.3、核心编程思想

MR = MapTask + ReduceTask

MapTask 拆分文本、ReduceTask统计词频

1.4、MR进程

一个MR程序在分布式运行时有三类实例进程:

  • MrAppMaster:负责整个程序的过程调度以及状态协调
  • MapTask:负责Map阶段的数据处理过程
  • ReduceTask:负责Reduce阶段的数据处理过程

1.5、MR的编程规范

1.5.1、Mapper阶段

  • 用户自定义的Mapper要继承Mapper类,重写map方法,map方法对每一个KV调用一次。
public static class MyMapper extends Mapper<inputK, inputV, outputK, outputV> {
    @Override
    public void map(){}
}

1.5.2、Reduce阶段

  • 自定义Reduce继承Reduce类,重写reduce方法,reduce方法对每一个相同K的KV调用一次(有多少个唯一的K,调用多少次reduce)。
public static class MyReduce extends Reduce<inputK, inputV, outputK, outputV> {
    @Override
    public void reduce(){}
}

1.5.3、Driver阶段

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

1.6、MR的HelloWorld:WordCount

整个WordCount的代码比较简单,注释也非常清楚,就直接贴代码了。

针对的计算文本为:

atguigu atguigu
ss ss
cls cls
Jiao
banzhang
xue
hadoop

期望输出的结果为:

atguigu 2
banzhang 1
cls 2
hadoop 1
Jiao 1
ss 2
xue 1
  • Mapper
/**
 * KEYIN, map阶段输入的key的类型:LongWritable
 * VALUEIN,map阶段输入value类型:Text
 * KEYOUT,map阶段输出的Key类型:Text
 * VALUEOUT,map阶段输出的value类型:IntWritable
 */
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
    private Text outK = new Text();
    private IntWritable outV = new IntWritable(1);
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        // 1 获取一行
        // atguigu atguigu
        String line = value.toString();
        // 2 切割
        // atguigu
        // atguigu
        String[] words = line.split(" ");
        // 3 循环写出
        for (String word : words) {
            // 封装outk
            outK.set(word);
            // 写出
            context.write(outK, outV);
        }
    }
}
  • Reduce
/**
 * 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();
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int sum = 0;
        // atguigu, (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 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 设置map输出的kv类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);

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

        // 6 设置输入路径和输出路径
        FileInputFormat.setInputPaths(job, new Path("D:\input\inputword"));
        FileOutputFormat.setOutputPath(job, new Path("D:\hadoop\output888"));

        // 7 提交job
        boolean result = job.waitForCompletion(true);

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

1.7、MR的开发流程

  • 首先在本地编写Map、Reduce、Driver等代码,并且在本地测试成功
  • 将本地代码打成jar包,上传到HDFS集群
  • 在HDFS集群上运行jar包,执行命令

2、MR序列化

Hadoop的序列化比Java更加轻量

2.1、自定义序列化步骤

  • 必须实现Writable接口
  • 反序列化时,需要反射调用空参构造函数,所以必须有空参构造器
  • 重写序列化方法write()
  • 重写反序列化方法readFields()
  • 注意反序列化顺序和序列化顺序保持一致
  • 想要把结果显示在文件中,需要重写toString(),可用\t分开方便后续使用
  • 如果自定义的bean想要作为key序列化传输,还必须实现Comparable接口,因为MR中的Shuffle过程要求对key必须能够排序

2.2、自定义序列化bean实操MR案例

输入数据格式:

1    13560436666    120.196.100.99    1116    954    200
2    ...
3    ...
id   手机号码        网络IP            上行流量  下行流量 总流量

期望的输出数据格式:

13560436666    1116    954    2070
...
...
手机号码        上行流量 下行流量 总流量

分析:

在map中对每行数据进行split,然后抽取手机号码、上行流量、下行流量。将手机号码作为key,上行流量、下行流量、上行流量+下行流量作为map或者自定义bean。

在reduce中,对相同的key进行上行流量、下行流量、总流量的汇总相加。

  • 自定义bean
/**
 * 1、定义类实现writable接口
 * 2、重写序列化和反序列化方法
 * 3、重写空参构造
 * 4、toString方法
 */
public class FlowBean implements Writable {
    private long upFlow; // 上行流量
    private long downFlow; // 下行流量
    private long sumFlow; // 总流量

    // 空参构造
    public FlowBean() {
    }
    // getter/setter省略...

    @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
public class FlowMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
    private Text outK = new Text();
    private FlowBean outV = new FlowBean();
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        // 1 获取一行
        // 1  13736230513  192.196.100.1  www.atguigu.com  2481  24681  200
        String line = value.toString();
        // 2 切割
        // 1,13736230513,192.196.100.1,www.atguigu.com,2481,24681,200   7 - 3= 4
        // 2  13846544121  192.196.100.2      264  0  200  6 - 3 = 3
        String[] split = line.split("\t");
        // 3 抓取想要的数据
        // 手机号:13736230513
        // 上行流量和下行流量:2481,24681
        String phone = split[1];
        String up = split[split.length - 3];
        String down  = split[split.length - 2];
        // 4封装
        outK.set(phone);
        outV.setUpFlow(Long.parseLong(up));
        outV.setDownFlow(Long.parseLong(down));
        outV.setSumFlow();
        // 5 写出
        context.write(outK, outV);
    }
}
  • reducer
public class FlowReducer extends Reducer<Text, FlowBean,Text, FlowBean> {
    private FlowBean outV = new FlowBean();
    @Override
    protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
        // 1 遍历集合累加值
        long totalUp = 0;
        long totaldown = 0;
        for (FlowBean value : values) {
            totalUp += value.getUpFlow();
            totaldown += value.getDownFlow();
        }
        // 2 封装outk, outv
        outV.setUpFlow(totalUp);
        outV.setDownFlow(totaldown);
        outV.setSumFlow();
        // 3 写出
        context.write(key, outV);
    }
}
  • Driver
public class FlowDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        // 1 获取job
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
        // 2 设置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("D:\input\inputflow"));
        FileOutputFormat.setOutputPath(job, new Path("D:\hadoop\output4"));
        // 7 提交job
        boolean result = job.waitForCompletion(true);
        System.exit(result ? 0 : 1);
    }
}

3、MR框架原理

image.png

3.1、InputFormat数据输入

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

  • 问题提出

MapTask的并行度决定Map阶段的任务处理并发度,进而影响到整个Job的处理速度。但MapTask越多越好嘛?会不会存在数据量本身就非常小,开启一个MapTask的成本远高于处理数据的成本?

  • MapTask并行度决定机制

首先来理解几个概念:

数据块(物理概念上的分块):Block是HDFS物理上把数据分成一块一块、数据块是HDFS存储数据单位

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

image (1).png

3.1.2、Job提交和切片生成主要流程

  • 建立连接

    • 创建提交job的代理
    • 判断是本地运行环境还是Yarn集群运行环境
  • 提交job

    • 创建给集群提交数据的stag路径
    • 获取jobId,创建job路径
    • 拷贝jar包到集群,如果本地运行则不需要
    • 计算切片,生成切片规划文件
    • 向stag路径中写XML配置文件,后续MR将根据该配置文件计算
    • 提交job,返回提交状态

image (2).png

3.1.3、切片流程

  • 程序先找到数据存储的目录。

  • 开始遍历处理(规划切片)目录下的每一个文件

  • 遍历第一个文件ss.txt(每个文件单独切片

    • 获取文件大小fs.sizeOf(ss.txt)
    • 计算切片大小:computeSplitSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M
    • 默认情况下,切片大小=blocksize
    • 开始切,形成第1个切片ss.txt - 0:128M;第2个切片ss.txt一128:256M;第3个切片ss.txt一256M:300M
    • 每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分一块切片
    • 将切片信息写到一个切片规划文件中
    • 整个切片的核心过程在getSplit()方法中完成
    • InputSplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等。(逻辑切片记录)
  • 提交切片规划文件到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数。

3.1.4、FileInputFormat切片机制

  • 切片机制

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

    • 输入两个文件:file1.txt-320M;file2.txt-10M
    • 切片后:file1.txt.split1-0~128;file1.txt.split2-128~256;file1.txt.split3-256~320;file2.txt.split1-0~10M
  • 切片计算公式

    • 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(切片最小值):参数调的比blockSizez大,则可以让切片变得比blockSize还大。

3.1.5、TextInputFormat

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

3.1.6、CombineTextInputFormat

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

  • 切片机制

image (3).png

3.2、MR的工作流程

image (4).png

image (5).png

  • 准备待处理的文件

  • 客户端提交任务前,对待处理文件进行切片分析,生成逻辑切片以及切片规划文件

  • 客户端提交job.split(任务切片)、jar包(自定义MR的业务逻辑代码)、job.xml(MapTask、Reducer规划文件),提交给Yarn

  • Yarn根据规划文件计算MapTask,启动MrAppMaster,MrAppMaster根据job.split开启MapTask。

  • MapTask使用InputFormat(默认TextInputFormat)读取文件信息

  • MapTask将读取的内容交给Mapper(一般来说是业务方自定义Mapper),通过map函数处理业务逻辑

  • 之后使用OutputCollector输出

    • OutputCollector是一个环形缓冲区,一半内存存放数据,一般内存存放数据的索引(分区信息、key偏移,value偏移)
    • 环形缓冲区默认100M,内存写满80%后反向写。写满80%后,此时线程会从100%处反向写数据,而另一个异步线程将缓冲区数据0~80%的数据写入磁盘。如果正向写线程慢于反向写线程,为了不覆盖原数据,反向写线程会阻塞。
    • 写入磁盘前,会对数据进行分区快排(对索引进行排序)
    • 将缓冲区排序后的数据写入磁盘(此时数据是分区且有序的)
    • 对相同分区的数据进行归并排序
    • 将相同key的数据进行聚合,<a, 1><c, 1> 和 <a, 1><c, 2> → <a, 1><c, 1><c, 2>
  • MapTask任务完成后,启动相应数量的ReduceTask,告知ReduceTask处理相应分区数据

  • RecudeTask向MapTask拉取数据,进行归并排序

  • 一次性去读相同key的一组,然后传给reduce方法(即业务方自己实现的reduce逻辑)

  • 处理完成后,使用OutputFormat(默认TestOutputFormat)写出数据

3.3、Shuffle机制

map方法处理之后,reduce方法处理之前的过程称之为Shuffle。

image (6).png

  • 为数据标记分区,然后进入环形缓冲区
  • 对key的索引按照字典顺序快排
  • combine数据,即按照相同的key聚合value
  • 归并排序、压缩数据、写入磁盘
  • ReduceTask拉取数据到内存,如果内存不够写磁盘
  • 对内存和磁盘数据进行归并排序
  • 按照相同的key进行分组

3.3.1、分区Partition

如果ReduceTask只有一个,那么没有分区只说,最后数据都会进入到一个文件当中。

如果存在多个ReduceTask,每个ReduceTask会将自己处理的数据写入到一个文件当中,此时我们可以指定Partition的分区策略,让不同ReduceTask接收不同数据,写入不同文件。

例如:要求将统计结果按照条件输出到不同文件中(分区)。比如:将统计结果按照手机归属地不同省份输出到不同文件中(分区)

默认的分区规则是HashPartitioner((key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;

  • 自定义分区
public class ProvincePartitioner extends Partitioner<K, V> {
    @Override
    public int getPartition(K k, V v, int numPartitions) {...}
}

job.setpartitionerclass (ProvincePartitioner.class);
job.setNumReduceTasks(5);
  • 分区小结

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

3.3.2、排序

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

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

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

  • 排序分类

    • 部分排序(分区内排序):MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序
    • 全排序:最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。
    • 辅助排序:在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部 字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序。
    • 二次排序:在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。
  • 自定义排序

public class FlowBean implements WritableComparable<FlowBean> {
    private long upFlow; // 上行流量
    private long downFlow; // 下行流量
    private long sumFlow; // 总流量

    // 空参构造
    public FlowBean() {
    }
    // getter/setter

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

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

3.3.3、Combiner

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

  • Combiner组件的父类就是Reducer。

  • Combiner和Reducer的区别在于运行的位置

    • Combiner是在每一个MapTask所在的节点运行;
    • Reducer是接收全局所有Mapper的输出结果;
  • Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量。

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

自定义Combiner

public class WordCountCombiner extends Reducer<Text, IntWritable,Text, IntWritable> {
    private IntWritable outV = new IntWritable();
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int sum =0;
        for (IntWritable value : values) {
            sum += value.get();
        }
        outV.set(sum);
        context.write(key,outV);
    }
}

job.setCombinerClass(WordCountReducer.class);

注意,如果reduce阶段被删除了,那么Shuffle阶段将不会执行,因此设置的Combiner将也不起作用。

其实直接使用自定义的Reducer也能够实现相应逻辑。

3.4、OutputFormat

默认使用TextOutputFormat

自定义OutputFormat

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

public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
    private  FSDataOutputStream atguiguOut;
    private  FSDataOutputStream otherOut;
    public LogRecordWriter(TaskAttemptContext job) {
        // 创建两条流
        try {
            FileSystem fs = FileSystem.get(job.getConfiguration());
            atguiguOut = fs.create(new Path("D:\hadoop\atguigu.log"));
            otherOut = fs.create(new Path("D:\hadoop\other.log"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void write(Text key, NullWritable value) throws IOException, InterruptedException {
        String log = key.toString();
        // 具体写
        if (log.contains("atguigu")){
            atguiguOut.writeBytes(log+"\n");
        }else {
            otherOut.writeBytes(log+"\n");
        }
    }
    @Override
    public void close(TaskAttemptContext context) throws IOException, InterruptedException {
        // 关流
        IOUtils.closeStream(atguiguOut);
        IOUtils.closeStream(otherOut);
    }
}


//设置自定义的outputformat
job.setOutputFormatClass(LogOutputFormat.class);

3.5、MR核心工作机制

3.5.1、MapTask工作机制

MapTask分为5个阶段:Read阶段、Map阶段、Collect阶段、溢写阶段、Merge阶段

image (7).png

3.5.2、Reduce工作机制

ReduceTask分为3个阶段:Copy阶段、Sort阶段、Reduce阶段

image (8).png

3.5.3、ReduceTask并行度选择

  • ReduceTask=0,表示没有Reduce阶段,输出文件个数和Map个数一致。即MapTask执行完后直接输出文件。
  • ReduceTask默认值就是1,所以输出文件个数为一个。(同时在这个单个文件上全排序,数据量非常大的情况下耗时、耗性能)
  • 如果数据分布不均匀,就有可能在Reduce阶段产生数据倾斜。(部分ReduceTask执行非常快、部分ReduceTask执行非常慢)
  • ReduceTask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有l个ReduceTask。
  • 具体多少个ReduceTask,需要根据集群性能而定。
  • 如果分区数不是1,但是ReduceTask为1,是否执行分区过程。答案是:不执行分区过程。因为在MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1。不大于1肯定不执行。

3.6、Join多种应用

从视频教程上来看,无论是MapJoin还是ReduceJoin,其实都是针对不同业务需求(例如将不同文件的数据合并输出至同一文件当中,这些文件的内容格式不同,需要提取相应的字段输出)设计的不同MapReduce逻辑处理,并没有类似MapTask、ReduceTask之类的原生组件。

这里便不再记录啥内容了,整体流程和MR-WordCount几乎一致。

3.7、ETL数据清理

ETL,是英文Extract-Transform-Load的缩写,用来描述将数据从来源端经过抽取(Extract)、转换(Transform)、加载(Load)至目的端的过程。ETL一词较常用在数据仓库,但其对象并不限于数据仓库。

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

使用MR作为ETL工具其实也是针对业务的特殊处理,可以使用MapTask对输入数据进行过滤,仅将符合业务逻辑处理的数据传输至下一个MR处理阶段。

public class WebLogMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        // 1 获取一行
        String line = value.toString();
        // 2 ETL
        boolean result = parseLog(line, context);
        if (!result){
            return;
        }
        // 3 写出
        context.write(value, NullWritable.get());
    }
    private boolean parseLog(String line, Context context) {
        // 切割
        // 1.206.126.5 - - [19/Sep/2013:05:41:41 +0000] "-" 400 0 "-" "-"
        String[] fields = line.split(" ");
        // 2 判断一下日志的长度是否大于11
        if (fields.length > 11){
            return true;
        }else {
            return false;
        }
    }
}

3.8、小结

  • InputFormat

    • 默认的是TextInputformat。kv:key(偏移量);v(一行内容)
    • 处理小文件:CombineTextInputFormat,把多个文件合并到一起统一切片
  • Mapper

    • setup()初始化;map()用户的业务逻辑;clearup()关闭资源
  • 分区

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

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

    • 前提:不影响最终的业务逻辑(求和、求平均值)。提前聚合map=>解决数据倾斜的一个方法
  • Reducer

    • setup()初始化;reduce()用户的业务逻辑;clearup()关闭资源
  • OutputFormat

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

4、数据压缩

4.1、Hadoop压缩概述

优点:减少磁盘IO、存储空间

缺点:增加CPU开销

压缩原则:

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

4.2、MR支持的压缩编码

  • 压缩算法对比
压缩格式Hadoop自带?算法文件扩展名是否可切片换成压缩格式后,原来的程序是否需要修改
DEFLATE是,直接使用DEFLATE.deflate和文本处理一样,不需要修改
Gzipe是,直接使用DEFLATE.gz和文本处理一样,不需要修改
bzip2是,直接使用bzip2.bz2和文本处理一样,不需要修改
LZO否,需要安装LZO.lzo需要建索引,还需要指定输入格式
Snappy是,直接使用Snappy.snappy和文本处理一样,不需要修改
  • 压缩算法性能比较
压缩算法原始文件大小压缩文件大小压缩速度解压速度
gzip8.3GB1.8GB17.5MB/s58MB/s
bzip28.3GB1.1GB2.4MB/s9.5MB/s
LZO8.3GB2.9GB49.3MB/s74.6MB/s

4.3、压缩方式选择

压缩方式选择时重点考虑:压缩/解压缩速度、压缩率(压缩后存储大小)、压缩后是否可以支持切片。

4.4、MR可压缩点

4.4.1、输入端压缩

无须指定显示指定编解码方式,Hadoop自动检测文件扩展名,选择合适的编码方式进行压缩/解压缩。

考虑因素:

  • 数据量小于块大小,重点考虑压缩速率,LZO/Snappy
  • 数据量非常大,考虑bz2、LZO

4.4.2、Mapper输出压缩

为了减少网络IO,直接考虑压缩/解压缩速率,LZO或者Snappy

4.4.3、Reducer输出压缩

根据需求指定:如果数据保存,那么可以选择压缩率高的算法;如果传递下一个MR,并且想要提高效率,选择压缩速度快的。

4.5、开启某种压缩方式

主要设置某些配置变量

// 1 获取配置
Configuration conf = new Configuration();
// 开启map端输出压缩
conf.setBoolean("mapreduce.map.output.compress", true);
// 设置map端输出压缩方式
conf.setClass("mapreduce.map.output.compress.codec", SnappyCodec.class, CompressionCodec.class);

// 设置reduce端输出压缩开启
FileOutputFormat.setCompressOutput(job, true);
// 设置压缩的方式
// FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
// FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);
FileOutputFormat.setOutputCompressorClass(job, DefaultCodec.class);