大数据Hadoop-MapReduce学习之旅第二篇

508 阅读8分钟

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」。

一、Hadoop 序列化

1、序列化概述

1.1、什么是序列化

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

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

1.2、为什么要序列化

一般来说,“活的”对象只生存在内存里,关机断电就没有了。而且“活的”对象只能由本地的进程使用,不能被发送到网络上的另外一台计算机。 然而序列化可以存储“活的”对象,可以将“活的”对象发送到远程计算机。

1.3、为什么不用Java的序列化

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

1.4、Hadoop序列化特点

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

2、自定义Bean对象实现序列化

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

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

  1. 必须实现Writable接口
  2. 反序列化时,需要反射调用空参构造函数,所以必须有空参构造
  3. 重写序列化方法
  4. 重写反序列化方法
  5. 注意反序列化的顺序和序列化的顺序完全一致
  6. 要想把结果显示在文件中,需要重写toString(),可用"\t"分开,方便后续用。
  7. 如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口,因为MapReduce框中的Shuffle过程要求对key必须能排序。

3、序列化实操案例

  1. 需求

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

    输入数据

    image.png

    输入数据格式

    image.png

    期望输出数据格式

    image.png

  2. 需求分析

    image.png

  3. 编写MapReduce程序

    • 编写流量统计的Bean对象

      public class FlowBean implements Writable {
          private long upFlow; // 上行流量
          private long downFlow; // 下行流量
          private long sumFlow; // 总流量
      
          // 空参构造
          public FlowBean() {
          }
      
          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;
          }
      
          @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 获取一行
              String line = value.toString();
              // 2 切割
              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);
          }
      }
      

二、MapReduce 框架原理

image.png

1、InputFormat 数据输入

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

  • 问题引出

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

    思考:1G的数据,启动8个MapTask,可以提高集群的并发处理能力。那么1K的数据,也启动8个MapTask,会提高集群性能吗?MapTask并行任务是否越多越好呢?哪些因素影响了MapTask并行度?

  • MapTask并行度决定机制

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

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

    image.png

1.2、Job提交流程源码和切片源码详解

  • Job提交流程源码详解

    waitForCompletion()
    
    submit();
    
    // 1建立连接
    
    connect();
    
    // 1)创建提交Job的代理
    
    new Cluster(getConfiguration());
    
    // (1)判断是本地运行环境还是yarn集群运行环境
    
    initialize(jobTrackAddr, conf);
    
    // 2 提交job
    
    submitter.submitJobInternal(Job.this, cluster)
    
    // 1)创建给集群提交数据的Stag路径
    
    Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
    
    // 2)获取jobid ,并创建Job路径
    
    JobID jobId = submitClient.getNewJobID();
    
    // 3)拷贝jar包到集群
    
    copyAndConfigureFiles(job, submitJobDir);
    
    rUploader.uploadFiles(job, jobSubmitDir);
    
    // 4)计算切片,生成切片规划文件
    
    writeSplits(job, submitJobDir);
    
    maps = writeNewSplits(job, jobSubmitDir);
    
    input.getSplits(job);
    
    // 5)向Stag路径写XML配置文件
    
    writeConf(conf, submitJobFile);
    
    conf.writeXml(out);
    
    // 6)提交Job,返回提交状态
    
    status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
    

    image.png

  • FileInputFormat切片源码解析(input.getSplits(job))

    image.png

1.3、FileInputFormat切片机制

image.png

image.png

1.4、TextInputFormat

  • FileInputFormat实现类

    思考:在运行MapReduce程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。那么,针对不同的数据类型,MapReduce是如何读取这些数据的呢?

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

  • TextInputFormat

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

    以下是一个示例,比如,一个分片包含了如下4条文本记录。

    image.png

1.5、CombineTextInputFormat切片机制

框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。

  • 应用场景

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

  • 虚拟存储切片最大值设置

    CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m

    注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。

  • 切片机制

    生成切片过程包括:虚拟存储过程和切片过程二部分。

    image.png

1.6、CombineTextInputFormat案例实操

  • 需求

    将输入的大量小文件合并成一个切片统一处理。

    image.png

  • 实现过程

    • 不做任何处理,运行WordCount案例程序,观察切片个数为4。

      number of splits:4

    • 在WordcountDriver中增加如下代码,运行程序,并观察运行的切片个数为3。

      • 驱动类中添加代码如下:
      // 如果不设置InputFormat,它默认用的是TextInputFormat.class
      job.setInputFormatClass(CombineTextInputFormat.class);
      //虚拟存储切片最大值设置4m
      CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);
      
      • 运行如果为3个切片。

        number of splits:3

    • 在WordcountDriver中增加如下代码,运行程序,并观察运行的切片个数为1。

      • 驱动中添加代码如下:

        // 如果不设置InputFormat,它默认用的是TextInputFormat.class
        job.setInputFormatClass(CombineTextInputFormat.class);
        //虚拟存储切片最大值设置20m
        CombineTextInputFormat.setMaxInputSplitSize(job, 20971520);
        
      • 运行如果为1个切片

        number of splits:1

2、MapReduce 工作流程

2.1、MapReduce 详细工作流程一

image.png

2.2、MapReduce 详细工作流程二

image.png

上面的流程是整个MapReduce最全工作流程,但是Shuffle过程只是从第7步开始到第16步结束,具体Shuffle过程详解,如下:

(1)MapTask收集我们的map()方法输出的kv对,放到内存缓冲区中

(2)从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件

(3)多个溢出文件会被合并成大的溢出文件

(4)在溢出过程及合并的过程中,都要调用Partitioner进行分区和针对key进行排序

(5)ReduceTask根据自己的分区号,去各个MapTask机器上取相应的结果分区数据

(6)ReduceTask会抓取到同一个分区的来自不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)

(7)合并成大文件后,Shuffle的过程也就结束了,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对Group,调用用户自定义的reduce()方法)

注意:

(1)Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。

(2)缓冲区的大小可以通过参数调整,参数:mapreduce.task.io.sort.mb默认100M。

三、友情链接

大数据Hadoop-MapReduce之MapReduce概述