Mapreduce学习基础

一、Mapreduce的基础

1.1 为什么要学习Mapreduce

1. 单机资源受限,比如CPU,磁盘
2. 分布式计算的程序的复杂度特别高,难度大

mapreduce就是解决以上问题的:  
    1. 利用集群的所有cpu,所有内存,所有磁盘
    2. mapreduce就将公共的功能的开发封装成了框架,不需要开发人员操心,开发人员只需要关注具体的业务逻辑

1.2 Mapreduce的简介

1.2.1 简介

1. mapreduce是hadoop的三大重要模块之一
2. mapreduce是一个并发的计算和分析框架,用于计算和分析分布式文件系统上的大数据集。
3. 将计算划分为两个阶段:一个map(映射)阶段,一个reduce(归约)阶段
4. 该框架的开发灵感来源于google的《mapreduce》论文
5. 方便开发人员在不会分布式计算的情况下,开发和运行相关计算程序。

1.2.2 优缺点

1. 优点
	- 适合离线数据处理
	- mapreduce编程简单
	- 扩展性良好
	- 高容错性
2. 缺点
	- 不适合实时计算(实时计算:毫秒级别/秒级别,离线计算:秒级别以上)
	- 不适合流式计算(mapreduce处理的数据是静态的,不是流式的数据)
	- 不适合DAG(有向图)计算

1.3 Mapreduce的核心思想(重点)

简单的一句话概括:“移动计算而非移动数据”。

整理:
程序员将自己写好的业务逻辑代码和mr自带的一些组件打包成计算程序,移动到有数据存储的节点上,这样可以利用多节点的cpu的并发能力,提高计算效率(怎么提高的?一减少数据移动的开销,二利用了并发计算原理)
mapreduce是分为两个阶段,map阶段处理的是块文件(原始文件),计算后的结果存储本地磁盘,reduce阶段要跨节点fetch属于自己要处理的数据,计算后的结果存储到hdfs上(当然也可以存储到客户端所在的本地磁盘)

1.4 Mapreduce的阶段介绍(重点)

1.4.1 Map阶段

map阶段处理的是原始数据,也就是块文件(处理的是本存储节点上的数据)。会将处理的块文件,以切片的形式进行逻辑运算。通过映射关系进行一一映射。map阶段会有多个mapTask,这些任务并发运行,互不干扰
默认情况下,按行进行映射成键值对,
原始块文件
   |
   |
K1,V1(有N个kv对,K1是行偏移量,v1是行记录,也就是行内容)
   |
   |
  map方法
   |
   | 每一对k1v1都会调用一次map方法,在map方法里进行处理,形成K2V2 
   |
 K2,V2  (存储到本地磁盘)

1.4.2 Reduce阶段

reduce阶段处理的是map阶段计算出来的数据(临时数据),reduce阶段也会有多个reduceTask,并发运行,互不干扰。reduce处理的数据通常都是要跨节点fetch属于自己处理的数据。

fetch属于自己的一堆K2,v2,先形成<K2,<v2,v2,v2>>
         |
         |
       reduce方法
         |
	     |  同一个k2调用一次reduce方法,在reduce方法里进行处理,形成K3,v3
	     |
       K3,V3(存储到HDFS上)

1.5 Mapreduce的编程模型

1.5.1 自定义Mapper类型

1. 自定义类名,继承Mapper类型
2. 定义K1,V1,K2,V2的泛型
3. 重写map方法

1.5.2 自定义Reducer类型

1. 自定义类名,继承Reducer类型
2. 定义K2,V2,K3,V3的泛型
3. 重写reduce方法

1.5.3 自定义Driver类型

1. 获取job对象
2. 指定驱动类型
3. 指定Mapper类型和Reducer类型
4. 指定map阶段的K2,V2类型
5. 指定reduce阶段的K3,V3类型
6. 指定分区的数量.....
7. 指定要统计的文件的输入路径
8. 指定要输出的位置路径
9. 提交程序

1.6 Mapreduce的入门案例演示(重点,绘图)

1.6.1 案例简介:wordcount程序

wordcount:使用mr思想对多个文件,或者是多个块进行统计单词的出现频率

1.6.2 图解

在这里插入图片描述

1.6.4 wordcount案例代码步骤如下:

步骤1:创建项目,导入jar包依赖

<dependencies>
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-common</artifactId>
        <version>2.7.6</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-client -->
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-client</artifactId>
        <version>2.7.6</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-hdfs -->
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-hdfs</artifactId>
        <version>2.7.6</version>
    </dependency>
</dependencies>

步骤2:编写mapper类型

package com.qianfeng.mr.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;

/**
 * 1: 自定义类名,继承Mapper类型
 * 2: 定义K1,V1,K2,V2的泛型
 *       K1  行偏移量  整形   LongWritable     K1和V1是底层默认的类型,不能随便改
 *       V1  行内容    字符串  Text
 *       K2   单词     字符串  Text
 *       V2   数字1    整形   IntWritable
 * 3: 重写map方法
 */
public class MyMapper extends Mapper<LongWritable, Text,Text, IntWritable> {
    
    
    Text k2 = new Text();
    IntWritable v2 = new IntWritable(1);
    /**
     *
     * @param key    就是k1    单词频率统计时,行偏移量没有任何用处,无需操心
     * @param value   就是v1   只需要对v1做截断处理即可
     * @param context    上下文,该对象提供了输出k2,v2的方法
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
    
    
        //1:将value转成java的String类型
        String lineRecord = value.toString();
        //2: 使用空格切分成数组
        String[] words = lineRecord.split(" ");
        //3:遍历数组,将元素作为K2,  数字1作为V2写出去
        for (String word : words) {
    
    
            //将word转成Text类型
            k2.set(word);
            //写出去
            context.write(k2,v2);
        }
    }
}

步骤3:编写reducer类型

package com.qianfeng.mr.wordcount;

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

import java.io.IOException;

/**
 * 1: 自定义类名,继承Reducer类型
 * 2: 规定k2,v2,k3,v3的泛型
 *       k2,v2和map阶段的输出数据的泛型一致
 *       k3:  单词    Text
 *       V3:  数字    IntWritable
 * 3: 重写reduce方法
 */
public class MyReducer extends Reducer<Text, IntWritable,Text,IntWritable> {
    
    
    /**
     *      <hello,<1,1,1,1,1,1>>
     * @param key   就是k2
     * @param values   同一个k2的多个v2
     * @param context  上下文
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
    
    
         //1: 将values的多个1进行累加
        int sum = 0;
        for (IntWritable value : values) {
    
    
            //累加
            sum+=value.get();
        }
        //累加的结果sum就是v3, 转成IntWritable
        IntWritable v3 = new IntWritable(sum);
        //k2,就是k3,写出去
        context.write(key,v3);
    }
}

步骤4:编写driver类型

package com.qianfeng.mr.wordcount;

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

import java.io.IOException;


public class MyDriver {
    
    
    public static void main(String[] args) {
    
    

        try {
    
    
            Configuration configuration = new Configuration();
            Job job = Job.getInstance(configuration);
            //设置驱动类
            job.setJarByClass(MyDriver.class);
            //设置mapper和reducer类型
            job.setMapperClass(MyMapper.class);
            job.setReducerClass(MyReducer.class);
            //设置k2,v2,k3,v3的泛型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(IntWritable.class);
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
            //设置分区数量,不设置默认为1
            job.setNumReduceTasks(1);
            //设置输入和输出路径
            Path inputPath = new Path("D:/test");
            FileInputFormat.addInputPath(job,inputPath);
            FileOutputFormat.setOutputPath(job,new Path("D:/output"));

            //提交等待完成
            boolean flag = job.waitForCompletion(true);

            System.exit(flag?0:1);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

1.7 Partitioner组件的应用(重点)

1.7.1 reduceTask数量的研究

1. reduceTask的数量由分区数量决定
2. 不是每一个分区都有数据的
3. 如果自定义分区器了,那么reduceTask的数量不能小于分区器里的分区数量,否则会报错
   但是设置为1除外,原因,1是默认,底层源码在发现是1时,不会执行自定义分区器。

1.7.2 分区器的简介与自定义

mapreduce的默认分区器是HashPartitioner。 逻辑如下:调用k2的hash值与int的最大值做位运算再与numReduceTasks做取模运算。

public class HashPartitioner<K, V> extends Partitioner<K, V> {

  /** Use {@link Object#hashCode()} to partition. */
  public int getPartition(K key, V value,
                          int numReduceTasks) {
    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
  }

}

所以说,如果想要控制如何分区,可以自定义分区器,那么如何自定义分区器,可以参考HashPartitioner

案例演示

package com.qianfeng.mr.wordcount;

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

/**
 * 如何自定义分区器:
 * 1: 继承org.apache.hadoop.mapreduce.Partitioner
 * 2: 规定k2,v2的泛型
 * 3: 重写getPartition方法
 *     注意:该方法的返回值是一个int,表示分区号,是一个从0开始的自然数,多个分数号不能间断。必须是连续的
 *
 *
 *   需求: 将要统计的wordcount分为三个区:
 *         aA-nN为0分区的数据
 *         oO-zZ为1分区的数据
 *         其他为2分区的数据
 */
public class MyPartitioner extends Partitioner<Text, IntWritable> {
    
    
    @Override
    public int getPartition(Text text, IntWritable intWritable, int numPartitions) {
    
    
        //将k2变成字符串
        String k2 = text.toString();
        String first = k2.charAt(0)+"";
        if(first.matches("[A-Na-n]")){
    
    
            return 0;
        }else if(first.matches("[o-zO-Z]")) {
    
    
            return 1;
        }else{
    
    
            return 2;
        }
    }
}

1.8 IDE运行Mapreduce的几种模式(熟悉)

1.8.1 说明

集成开发工具(IDE)运行mapreduce的模式有三种,分别是
1. 在本地运行计算本地的文件,
2. 在本地运行计算HDFS上的文件,
3. 将计算程序远程提交到HDFS上计算HDFS上的文件。

1.8.2 本地运行计算本地的文件

本地指的是IDE所在的操作系统环境,计算的是本地文件系统中的文件。

原理如下:
- Configuration对象会读取四个默认的配置文件。
- fs.defaultFS的值为file:///,获取的是本地文件系统对象。
- mapreduce.framework.name的值为local

所以,在Driver中的输入路径和输出路径指定为本地的路径即可

1.8.3 本地运行计算HDFS上的文件

本地指的是IDE所在的操作系统环境,计算的是HDFS中的文件。

原理如下:
- 既然想要获取HDFS上的文件,那么fs.defaultFS的值为hdfs://qianfeng01:8020,也就是要获取分布式文件系统对象
- 是访问HDFS,将对应的文件读取到本地内存进行计算,计算结果存储到HDFS上(因为还是使用mapreudce.framework.name的默认值local).

1.8.4 本地远程提交计算程序到HDFS上,计算HDFS上的文件

原理:
- 移动计算而非移动数据
- 使用的是HDFS集群的设置,mapreduce.framework.name的值是yarn
- IDE需要有权限访问HDFS,IDE需要设置跨平台操作
   conf.set("mapreduce.app-submission.cross-platform", "true");
   System.setProperty("HADOOP_USER_NAME", "root");
   
步骤:
1. 将计算程序打包成jar文件,放入到classpath下: add as Library
2. 将集群的mapred-site.xml和yarn-site.xml文件导入到项目的resouces目录(注意重启的问题)
3. 如果报: 无任务控制....等字样,说明没有设置跨平台属性
4. 程序中的路径要使用hdfs上的路径。   

1.8.5 扩展:

将程序打成jar包,上传到hdfs上,使用hadoop jar指令来运行程序

注意:杀死yarn上的任务指令

]# yarn application -kill appId

appID就是网址8088上的ID

二、Hadoop序列化机制

2.1 序列化的概念和应用领域

-- 序列化:
	将内存中的对象转成二进制的字节序列形式
-- 反序列化:
	讲二进制形式的字节序列转成内存中的对象
-- 应用领域
   (1) 持久化到磁盘上保存
   (2) 网络传输

2.2 Hadoop与java序列化机制的比较

2.2.1 java序列化机制

是一个重量级的序列化机制,在序列化过程中,除了属性的值外,还会涉及到继承体系,类的结构,header等这些额外的,因此在网络传输过程中,信息体积大,速度相对慢等特点

2.2.2 hadoop序列化机制

因为hadoop一定会涉及到网络传输,因此hadoop序列化的要求应该是体积小,速度快,占用带宽小。  所以有以下特征:

1. 紧凑:  数据在序列化时比较紧凑,节省带宽资源
2. 快速:  在序列化和反序列化时,要尽可能的节省资源开销(磁盘,cpu,内存)
3. 可扩展: 要适应市场新的变化。扩展性要好。
4. 支持互操作:能支持不同语言写的客户端和服务端进行交互;

需要注意的是: MapReduce的key和value,都必须是可序列化的。而针对于key而言,是数据排序的关键字,因此还需要提供比较接口:WritableComparable

2.3 Hadoop的常用类型

java的常用的数据类型对应的hadoop数据序列化类型

Java类型 Hadoop Writable类型 释义
boolean BooleanWritable 标准布尔型数值
byte ByteWritable 单字节数值
int IntWritable 整型数值
float FloatWritable 单精度数
long LongWritable 长整型数值
double DoubleWritable 双精度数
string Text 使用UTF8格式存储的文本
map MapWritable 以键值对的形式存储Writable类型的数据
array ArrayWritable 以数组的形式存储Writable类型的数据
null NullWritable 当<key,value>中的key或value为空时使用

NullWritable说明:

在这里插入图片描述

2.4 序列化接口和排序接口(重点理解)

2.4.1 序列化接口:Writable

该接口只有两个抽象方法,分别是序列化方法和反序列化方法。

public interface Writable {
    
    
 //序列化方法,将对象序列化到out流中
  void write(DataOutput out) throws IOException;
  //反序列化方法,从in流中读取信息转成对象。
  void readFields(DataInput in) throws IOException;
}

2.4.2 排序接口:WritableComparable

1. java的排序接口,代码如下
public interface Comparable<T> {
    
    
    public int compareTo(T o);
}

2. hadoop的排序接口,继承了hadoop的序列化接口,同时继承了java的排序接口
public interface WritableComparable<T> extends Writable, Comparable<T> {
    
    
}

2.5 如何自定义Hadoop类型

因为hadoop的类型一定会进行网络传输,比如在reduceTask抓取的时候,还有在环形缓冲区的时候要比较,排序等。因此hadoop的类型,一定要实现序列化接口和排序接口。

方式1:  可以单独实现Writable和Comparable接口
方式2:  可以直接实现WritableComparable接口

步骤如下:
1. 定义类型,使用上述的某一种方式
2. 提供属性信息
3. 提供无参和全参构造器
4. 提供get/set方法
5. 提供toString方法
6. 重写比较方法compareTo(T t)
7. 重写序列化方法
8. 重写反序列化方法

1)案例演示:自定义hadoop的student类型

package com.qf.mapreduce.writable;

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

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

/**
 * 自定义一个hadoop类型,   方式1:实现两个接口,一个是Writable 一个是Comparable
 *                       方式2:直接实现WriableComparable接口
 */
public class Student implements WritableComparable<Student> {
    
    
    private int stuid;
    private String name;
    private int age;
    private String gender;
    //如果是hadoop类型的属性,需要直接初始化,避免反序列化时出现空指针异常
    private Text girlFriend = new Text();

    /**
     * 无参构造器必须要提供,因为在hadoop底层机制中会涉及到反序列化,而反序列化时框架会自动调用无参构造器
     */
    public Student(){
    
    

    }

    public Student(int stuid, String name, int age, String gender,Text girlFriend) {
    
    
        this.stuid = stuid;
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.girlFriend = girlFriend;
    }

    public int getStuid() {
    
    
        return stuid;
    }

    public void setStuid(int stuid) {
    
    
        this.stuid = stuid;
    }

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }

    public int getAge() {
    
    
        return age;
    }

    public void setAge(int age) {
    
    
        this.age = age;
    }

    public String getGender() {
    
    
        return gender;
    }

    public void setGender(String gender) {
    
    
        this.gender = gender;
    }
    public Text getGirlFriend(){
    
    
        return girlFriend;
    }
    public void setGirlFriend(Text girlFriend){
    
    
        this.girlFriend = girlFriend;
    }

    /**
     * 重写toString方法
     * @param
     * @return
     */
    public String toString(){
    
    
        return  stuid+"\t"+name+"\t"+age+"\t"+gender;
    }

    /**
     * 自定义排序规则:
     *    0:表示两个对象在排序上是相同的
     *    -1: this比o小
     *    1:  this比o大
     *
     *    排序规则: 如果想要升序排序,逻辑应该写成 this-o   注意是相关属性比较
     *             如果想要降序排序,逻辑应该写成 -(this-o)   注意是相关属性比较
     *
     *   自定义规则,按照年龄降序
     *            如果年龄相同,则按照学号升序排序
     * @param
     * @return
     */
    @Override
    public int compareTo(Student o) {
    
    
        int result =  o.age - this.age;
        if(result == 0){
    
    
            result = this.stuid - o.stuid;
        }
        return result;
    }

    /**
     * 序列化方式,作用是将对象的属性序列化成字节数组,
     *   如果属性是java类型,只需要调用输出流的相应方式即可
     *   如果属性是hadoop类型,只需要调用对象的write方法即可
     * @param out
     * @throws IOException
     */
    @Override
    public void write(DataOutput out) throws IOException {
    
    
        out.writeInt(stuid);
        out.writeUTF(name);
        out.writeInt(age);
        out.writeUTF(gender);
        girlFriend.write(out);
    }

    /**
     * 反序列化方法: 作用是将输入流里的数据读出来给属性赋值
     *    如果属性是java类型,只需要调用输入流的相应方法即可
     *    如果属性是hadoop类型,只需要调用对象的readFields方法即可
     *
     *
     *    注意:反序列化时的顺序必须和序列化的顺序保持一致。
     *
     * @param in
     * @throws IOException
     */
    @Override
    public void readFields(DataInput in) throws IOException {
    
    
        stuid = in.readInt();
        name = in.readUTF();
        age = in.readInt();
        gender = in.readUTF();

        girlFriend.readFields(in);
    }
}

2)扩展:java和hadoop的序列化的比较案例

java的student

package com.qf.mapreduce.wordcount;

import java.io.Serializable;

public class Student implements Serializable {
    
    
        private int stuid;
        private String name;
        private int age;
        private String gender;
        public Student(){
    
    }
        public Student(int stuid, String name, int age, String gender) {
    
    
            this.stuid = stuid;
            this.name = name;
            this.age = age;
            this.gender = gender;
        }

        public int getStuid() {
    
    
            return stuid;
        }

        public void setStuid(int stuid) {
    
    
            this.stuid = stuid;
        }

        public String getName() {
    
    
            return name;
        }

        public void setName(String name) {
    
    
            this.name = name;
        }

        public int getAge() {
    
    
            return age;
        }

        public void setAge(int age) {
    
    
            this.age = age;
        }

        public String getGender() {
    
    
            return gender;
        }

        public void setGender(String gender) {
    
    
            this.gender = gender;
        }
        public String toString(){
    
    
            return  stuid+"\t"+name+"\t"+age+"\t"+gender;
        }
    }

测试代码:

public class WritableAndSerializable {
    
    
    public static void main(String[] args) throws IOException {
    
    
        //先序列化java类型的对象
        ObjectOutputStream oos = 
            new ObjectOutputStream(new FileOutputStream("D:/javastudent.txt"));
        Student student = new Student(1001,"michael",23,"女");
        oos.writeObject(student);
        oos.close();
        //序列化hadoop类型
        com.qf.mapreduce.writable.Student student1 = new com.qf.mapreduce.writable.Student(1001,"michael",23,"女",new Text("高圆圆"));
        DataOutputStream out = new DataOutputStream(new FileOutputStream("D:/hadoopstudent.txt"));
        student1.write(out);
        out.close();
    }
}

2.6 案例之手机流量的统计(熟悉)

2.6.1 程序需求分析:

统计每个手机的总上行流量,总下行流量,总流量

2.6.2 数据准备

参考文件:HTTP_20130313143750.dat

2.6.3 代码如下:

步骤1:设计FlowBean类型

package com.qianfeng.mr.flow;

import org.apache.hadoop.io.WritableComparable;

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

/**
 * 步骤1:自定义一个hadoop类型,用于存储某一个手机号的信息
 * 1: 手机号
 * 2: 上行流量
 * 3: 下行流量
 * 4: 总流量
 *
 * 步骤2: 实现序列化排序接口,重写方法
 */
public class FlowBean implements WritableComparable<FlowBean> {
    
    
    private String phoneNum;
    private long upFlow;
    private long downFlow;
    private long totalFlow;

    public FlowBean(){
    
    }

    public FlowBean(String phoneNum, long upFlow, long downFlow, long totalFlow) {
    
    
        this.phoneNum = phoneNum;
        this.upFlow = upFlow;
        this.downFlow = downFlow;
        this.totalFlow = totalFlow;
    }

    public String getPhoneNum() {
    
    
        return phoneNum;
    }

    public void setPhoneNum(String phoneNum) {
    
    
        this.phoneNum = phoneNum;
    }

    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 getTotalFlow() {
    
    
        return upFlow+downFlow;
    }

    public void setTotalFlow(long totalFlow) {
    
    
        this.totalFlow = totalFlow;
    }

    @Override
    public void write(DataOutput out) throws IOException {
    
    
        out.writeUTF(phoneNum);
        out.writeLong(upFlow);
        out.writeLong(downFlow);
        out.writeLong(totalFlow);
    }

    @Override
    public void readFields(DataInput in) throws IOException {
    
    
        phoneNum = in.readUTF();
        upFlow = in.readLong();
        downFlow = in.readLong();
        totalFlow = in.readLong();
    }

    @Override
    public int compareTo(FlowBean o) {
    
    
        return 0;
    }
}

步骤2:设计FlowMapper

package com.qianfeng.mr.flow;

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

import java.io.IOException;

/**
 *   K1是行偏移量
 *   V1是行内容
 *   K2想要手机号
 *   V2想要上行,下行,总流量,所以是FlowBean类型
 */
public class FlowMapper extends Mapper<LongWritable, Text,LongWritable,FlowBean> {
    
    
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
    
    
        String line = value.toString();
        String[] content = line.split("\t");
        //获取手机号部分
        String phoneNum = content[1];
        LongWritable phoneK2 = new LongWritable(Long.parseLong(phoneNum));
        //获取上行,下行
        String upflow = content[content.length-3];
        String downflow = content[content.length-2];
        FlowBean v2 = new FlowBean(phoneNum,Long.parseLong(upflow),Long.parseLong(downflow));

        context.write(phoneK2,v2);

    }
}

步骤3:设计FlowReducer

package com.qianfeng.mr.flow;

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

import java.io.IOException;

/**
 *   137...5566        <123   234   357>     <111   222   333>
 */
public class FlowReducer extends Reducer<LongWritable,FlowBean,LongWritable,FlowBean> {
    
    
    @Override
    protected void reduce(LongWritable key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
    
    
        long upFlow = 0;
        long downflow = 0;
        for (FlowBean value : values) {
    
    
            upFlow += value.getUpFlow();
            downflow+= value.getDownFlow();
        }
        //再次封装成一个新的flowBean对象
        FlowBean v3 = new FlowBean(key.get()+"",upFlow,downflow);
        //写出去
        context.write(key,v3);
    }
}

步骤4:设计FlowDriver

package com.qianfeng.mr.flow;

import com.qianfeng.mr.wordcount.MyMapper;
import com.qianfeng.mr.wordcount.MyPartitioner;
import com.qianfeng.mr.wordcount.MyReducer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
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;


public class FlowDriver {
    
    
    public static void main(String[] args) {
    
    

        try {
    
    
            Configuration configuration = new Configuration();
            Job job = Job.getInstance(configuration);
            //设置驱动类
            job.setJarByClass(FlowDriver.class);
            //设置mapper和reducer类型
            job.setMapperClass(FlowMapper.class);
            job.setReducerClass(FlowReducer.class);
            //设置k2,v2的泛型   ,  如果k2和k3相同,那么可以不指定k2    如果v2和v3相同,那么可以不指定v2
            job.setMapOutputKeyClass(LongWritable.class);
            job.setMapOutputValueClass(FlowBean.class);
            // 设置,k3,v3的泛型
            job.setOutputKeyClass(LongWritable.class);
            job.setOutputValueClass(FlowBean.class);
            //设置分区数量,不设置默认为1
            job.setNumReduceTasks(1);
            //设置输入和输出路径
            Path inputPath = new Path("D:\\academia\\The teaching material\\The required data\\data-mr\\flow");
            FileInputFormat.addInputPath(job,inputPath);
            Path outPath = new Path("D:/output");
            FileSystem fs = FileSystem.get(configuration);
            if(fs.exists(outPath)){
    
    
                fs.delete(outPath,true);
            }
            FileOutputFormat.setOutputPath(job,outPath);

            //提交等待完成
            System.exit(job.waitForCompletion(true)?0:1);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

三、MapReduce的进阶

3.1 Mapreduce运行流程概述(熟悉)

##1. 整个mr运行过程中有三大类进程:
	(1)MRAPPMaster: 负责整个作业的资源调度和状态协调。每一个mr程序在启动时都会启动一个MRAPPMaster
	(2)MapTask:负责Map阶段的数据处理流程(是原始块文件的处理)
	(3)ReduceTask:负责Reduce阶段的数据处理流程(是Map阶段产生的临时数据)
##2 整体流程如下:
	(1)mr作业提交时,会为这个作业启动一个AppMaster进程,主类就是MRAPPMaster。
	(2)启动后会根据job上的描述信息(输入规则和输入路径)计算inputSplit(输入分片),从而决定MapTask的数量
	(3)AppMaster进程向resourcemanager为MapTask申请资源container(cpu,内存,磁盘)
	(4)MapTask运行时:
	  -- 根据输入规则(FileInputFormat)和记录阅读器(RecordReader)以及分片信息读取块文件,形成K1V1
	  -- K1V1作为map函数的输入数据,进入map函数,经过逻辑处理,形成K2V2
	  -- K2V2被写入到环形缓冲区,当达到阈值时,会进行溢写(Flush)成磁盘上文件,溢写前分区和排序。
	(5)AppMaster在监控MapTask运行5%时,会为ReduceTask申请资源,会根据用户指定的参数来启动相应的reduceTask进程,并告知reduceTask需要处理的数据范围
	(6)ReduceTask运行时:
		reduceTask会根据Appmaster告知自己的待处理的数据范围,从MapTask产生的临时数据中拉取属于自己的数据,并进行"归并排序",然后相同的key化为一组<K2,<ListV2>>,每一组调用一次reduce函数,经过逻辑处理后,形成K3V3,根据输出规则将数据写出到目的地。

3.2 运行流程之分片机制(重点)

3.2.1 输入分片的概念

MapReduce在进行作业提交时,会预先对将要分析的原始数据进行划分处理,形成一个个等长的逻辑数据对象,称之为输入分片(inputSplit),简称“分片”。MapReduce为每一个分片构建一个单独的MapTask,并由该任务来运行用户自定义的map方法,从而处理分片中的每一条记录。

a文件  1025MB  ---  9块,最后一块是1MB


分析:分片就是记录MapTask要处理的是哪一个块,通过属性记录,因此称之为逻辑数据。

扩展:FileSplit的源码解析

public class FileSplit extends InputSplit implements Writable {
    
    
  private Path file;     //记录分片对应的文件名
  private long start;    //记录该分片要处理的块文件的开始偏移量
  private long length;   //记录该分片要处理的数据大小
  private String[] hosts; // 记录该分片要处理的数据的位置。
  private SplitLocationInfo[] hostInfos;   //记录的是分片的位置信息

  public FileSplit() {
    
    }
    //........
}    


a文件  1025MB  ---  9块,最后一块是1MB
    
    
    块大小是固定的,抉择分片的大小
    
    start   0
    length  = 258MB
    
    start = 258

3.2.2 分片大小如何选择

说明:分片大小指的是length属性。

#1. 分片数量越多,优势如下:
    -- 处理分片对应的原始数据所花的时间更少,也就是小于一个分片处理整个文件的时间。
    -- 每一个分片对应一个MapTask,MapTask是并行运行的,效率高
    -- 分片越多,负载均衡就越好。
    -- 计算机硬件越好,处理速度越快,就可以腾出时间,计算其他任务。
#2. 分片太小的缺点:
	如果分片太小,管理分片的总时间和构建map任务的总时间将决定作业的整个执行时间
#3. 分片太大的缺点:	
	如果分片跨越两个数据块,那么分片的部分数据需要通过网络传输到map任务运行的节点,占用网络带宽,效率更低
#4 得出结论:
	因此最佳分片大小应该和HDFS上的块大小一致。hadoop2.x默认128M.

3.2.3 源码解析:

FileInputFormat、TextInputFormat、LineRecordReader这三个类的源码,参考视频

3.2.4 分片机制的总结

1)分片大小参数
//计算实际分片大小

long splitSize = computeSplitSize(blockSize, minSize, maxSize);

protected long computeSplitSize(long blockSize, long minSize,
                                long maxSize) {
    
    
    return Math.max(minSize, Math.min(maxSize, blockSize));
}

通过分析源码,在FileInputFormat中,计算切片大小的逻辑:Math.max(minSize, Math.min(maxSize, blockSize)); 切片主要由这几个值来运算决定

参数 默认值 属性
minsize 1 mapreduce.input.fileinputformat.split.minsize
maxsize Long.MAXVALUE mapreduce.input.fileinputformat.split.maxsize
blocksize 块大小 dfs.blocksize:

可以看出,就是取minsize、maxsize、blocksize三者的中间的那个值。

--1. 将maxsize调得比blocksize小,则会让切片变小,而且就等于配置的这个参数的值.
--2. 将minsize调得比blockSize大,则会让切片变得比blocksize还大
--3. 但是,不论怎么调参数,都不能让多个小文件"划入"一个split
2)分片创建过程总结
1. 获取文件位置及大小
2. 判断文件是否可以分片(压缩格式有的可以进行分片,有的不可以 bzip2支持切分,LZO须提供索引才支持)
3. 获取分片的大小
4. 剩余文件的大小/分片大小>1.1时,循环执行封装分片信息的方法,具体如下:
   封装一个分片信息(包含文件的路径,分片的起始偏移量,要处理的大小,分片包含的块的信息,分片中包含的块存在哪儿些机器上)

5. 剩余文件的大小/分片大小<=1.1且不等于0时,封装一个分片信息(包含文件的路径,分片的起始偏移量,要处理
   的大小,分片包含的块的信息,分片中包含的块存在哪儿些机器上)

分片的注意事项:1.1倍的冗余。

3)分片细节问题总结

如果有多个分片

真正读取原始数据(块文件)
- 除了第一个分片之外,剩下的分片在读取数据时,都会舍弃块文件的第一行
- 所有的分片在读取块文件时,都会多读一行,确保该分片对应的数据已经读取完毕

4)分片与块的区别

看完源码就知道了

1. 分片是逻辑数据,记录的是要处理的物理块的信息而已
    path,start,length,hosts
2. 块是物理的,是真实存储在文件系统上的原始数据文件。
   本质:  逻辑数据
          物理数据
   出现的时机:  
              mapreduce程序进行计算时,才会遇到分片概念
              hdfs的存储才会涉及到分块的概念

3.3 运行流程之MapTask(重点)

3.3.1 MapTask的整体概述

1. maptask调用FileInputFormat的createRecordReader通过分片数据来读取原始数据    
2. 会调用nextKeyValue方法获取每行数据,然后返回一个(K,V)对,K是offset,V是一行数据
3. 将k-v对交给Map函数进行处理
4. 每对k-v调用一次map(K,V,context)方法,经过处理,使用context.write(k,v)
5. 写出的数据交给收集器OutputCollector.collect()处理
6. 将数据写入环形缓冲区,并记录写入的起始偏移量,终止偏移量,环形缓冲区默认大小100M
7. 默认写到80%的时候要溢写到磁盘,溢写磁盘的过程中数据继续写入剩余20%
8. 溢写磁盘之前要先进行分区然后分区内进行排序
9. 默认的分区规则是hashpatitioner,即key的hash%reduceNum
10. 默认的排序规则是key的字典顺序,使用的是快速排序
11. 溢写会形成多个文件,在maptask读取完一个分片数据后,会将环形缓冲区数据刷写到磁盘
12. 将数据多个溢写文件进行合并,分区内排序(外部排序===》归并排序)

3.4 运行流程之ReduceTask(重点)

1. reduceTask启动后,开始使用线程fetch属于自己分区的map端产生的临时数据。
2. fetch过来的K2,v2,在内存中会进行归并排序。
3. reduceTask会调用分组器将内存中的k2,v2按照不同的k2来分组。
4. 每一组的<k2,list<v2>>调用一次reduce方法,经过处理,使用context.write(k,v)写出去。
5. 写出去的位置由FileOutputFormat的参数决定。

3.5 Shuffle流程(重点)

3.5.1 概念:

从map阶段的map函数输出开始,到reduce阶段的reduce函数接受数据为止,这个过程称之为shuffle阶段。
整个shuffle阶段可以划分为map端的shuffle和reduce端的shuffle.

3.5.2 map端shuffle

1. map函数的输出数据K2,v2进入环形缓冲区,环形缓冲区默认大小100MB,阈值80MB。
2. 当数据存储达到阈值时,会启动一个线程将该数据溢写出去,剩下的20M会继续写入,当20M写满,80M还未溢写完,则maptask出现阻塞。
3. 在溢写前,会先调用分区器按key分区,然后同一分区的数据进行按key排序,排序算法为QuickSort,规则为字典排序。(注意:分区和排序都是对KV对的元数据进行的)
4. 溢写时会产生临时文件,按照分区号从小到大将每一个分区的数据溢写到文件中(注意:写出时是通过排好序的元数据找到原始数据,写出去)。
5. 在排序后,溢写前,可以调用combiner函数来减少数据的溢写,目的减少磁盘IO.
6. 如果溢写文件数量有多个的话,会进行再次合并,合并成一个最终的临时文件,合并的时候使用的是归并算法。
7. 如果溢写文件数据至少3个,则在合并排序后,会再次调用combiner函数,来减少最终临时文件的大小。如果数量低于3个,则没有必要调用combiner。
8. 为了减少临时文件到reduce端的网络IO,建议将临时文件压缩,再进行传输。

3.5.3 reduce端shuffle

1. 当某一个mapTask结束后,reduceTask会利用线程开始fetch属于自己要处理的分区的数据,线程默认是5个,线程数量是针对于每一个节点来说的,线程通过HTTP协议抓取数据。
2. 如果抓过来的数据量过小,直接在对应的jvm的内存中进行归并排序,输入给reduce函数
3. 如果数据量过大,则会直接拷贝到所在的本地磁盘上,然后将多个文件合并成较大的文件,合并因子是10,合并时采用的算法是归并算法。(细节:最后一次合并一定要满足10这个因子,而且不会溢写成文件,直接输入给reduce).
4. 在归并算法合并时,如果map端的数据是压缩的,那么要在reduceTask的内存中解压缩再合并。
5. reduce处理后将数据存储到HDFS上。

3.6 Combiner函数

3.6.1 为什么要用Combiner函数

因为mapreduce在运行作业时,会涉及到磁盘IO和网络IO。而集群中资源都有限(磁盘空间,带宽)。因此在作业期间能减少磁盘IO和网络IO,是最优的。而Combiner函数就可以帮助用户来做到这点。

3.6.2 Combiner函数的特点

1. 本质就是运行在各个阶段的排序后的reduce函数。可以在map阶段,也可以在reduce阶段。
2. 是mapreduce作业的一个组件。
3. 父类是Reducer类型。
4. 减少IO
5. 应该在不影响结果的前提下来使用。
6. 使用job.setCombinerClass(.....)来设置


注意:编写程序时要考虑combiner类的reduce函数的泛型问题
     因为该reduce函数的输入数据:是map函数的输出数据
                     输出数据: 是要被reduceTask的reduce函数的,也就不是K3,V3还是K2,V2
    

3.7 MapReduce优化参数

参考文档和视频解说

四、YARN资源管理器

4.1 MapReduce1.x版本的简介

参考文档

4.2 YARN的简介与设计思想(重点)

4.2.1 YARN的简介

1. Apache YARN(yet another resource Negotiator)是 Hadoop框架的资源管理系统,
2. 是Hadoop的三大核心模块之一。
3. 由于yarn的通用性比较好,所以除了mapreduce外,还可以在其上运行其他计算框架,比如spark,tez

扩展:有些软件是运行在mr,spark之上的,比如pig,hive等。

4.2.2 YARN的设计思想

yarn的设计思想是将资源管理和作业调度以及监视功能设计成独立的守护进程。一个全局的资源管理器ResourceManager,多个节点管理器NodeManager,针对于每一个提交的应用程序有一个applicationMaster进程。
资源管理器和节点管理器是长期运行的守护进程。

每个进程的作用如下:

resouceManager:   
		负责整个集群的资源管理和分配,内部有一个scheduler(资源调度器)。
nodeManager: 
		节点管理器,每个机器上只有一个。管理和监视本节点上的资源,并向resouceManager汇报
container(容器):
		指的就是节点上资源,比如cpu,内存,磁盘,网络等
applicationmaster:  
		简称appmaster, 一个应用程序对应一个appmaster, 应用程序结束,appmaster释放资源,并停止
		appmaster在工作时,负责向resourcemanager申请作业所需要的资源,并与nodemanager共同负
		责作业的运行

4.3 YARN的配置说明

4.4 YARN的job提交流程(重点)

4.5 YARN的三种调度器(熟悉)

2 Combiner函数的特点

1. 本质就是运行在各个阶段的排序后的reduce函数。可以在map阶段,也可以在reduce阶段。
2. 是mapreduce作业的一个组件。
3. 父类是Reducer类型。
4. 减少IO
5. 应该在不影响结果的前提下来使用。
6. 使用job.setCombinerClass(.....)来设置


注意:编写程序时要考虑combiner类的reduce函数的泛型问题
     因为该reduce函数的输入数据:是map函数的输出数据
                     输出数据: 是要被reduceTask的reduce函数的,也就不是K3,V3还是K2,V2
    

3.7 MapReduce优化参数

参考文档和视频解说

四、YARN资源管理器

4.1 MapReduce1.x版本的简介

参考文档

4.2 YARN的简介与设计思想(重点)

4.2.1 YARN的简介

1. Apache YARN(yet another resource Negotiator)是 Hadoop框架的资源管理系统,
2. 是Hadoop的三大核心模块之一。
3. 由于yarn的通用性比较好,所以除了mapreduce外,还可以在其上运行其他计算框架,比如spark,tez

扩展:有些软件是运行在mr,spark之上的,比如pig,hive等。

4.2.2 YARN的设计思想

yarn的设计思想是将资源管理和作业调度以及监视功能设计成独立的守护进程。一个全局的资源管理器ResourceManager,多个节点管理器NodeManager,针对于每一个提交的应用程序有一个applicationMaster进程。
资源管理器和节点管理器是长期运行的守护进程。

每个进程的作用如下:

resouceManager:   
		负责整个集群的资源管理和分配,内部有一个scheduler(资源调度器)。
nodeManager: 
		节点管理器,每个机器上只有一个。管理和监视本节点上的资源,并向resouceManager汇报
container(容器):
		指的就是节点上资源,比如cpu,内存,磁盘,网络等
applicationmaster:  
		简称appmaster, 一个应用程序对应一个appmaster, 应用程序结束,appmaster释放资源,并停止
		appmaster在工作时,负责向resourcemanager申请作业所需要的资源,并与nodemanager共同负
		责作业的运行

4.3 YARN的配置说明

4.4 YARN的job提交流程(重点)

4.5 YARN的三种调度器(熟悉)

猜你喜欢

转载自blog.csdn.net/weixin_45682261/article/details/125117673