MapReduce笔记

MapReduce(分布式计算框架)

1 MapReduce引言

1.1 MapReduce概念

MapReduce:是Hadoop体系下的一种分布式计算框架(计算模型|编程模型)。简单的说,就是通过代码对存储在HDFS上的数据进行计算处理的框架。

大数据的出现带来了海量数据存储的问题外,同时还带来了海量数据的计算问题。当数据到达一定量级后,单机性能再好,也无法在人类可接收的范围内完成数据处理,此时就必须要使用分布式的计算框架,调度多台机器并行计算处理数据,提高数据的处理速度。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TIIIm6Up-1631064667569)(MapReduce笔记.assets/image-20210217233526993.png)]

分布式计算:

  • 将一个大的文件,分散到多个服务器上存储–分布式存储
  • 将一个大的计算任务,分散到多个服务器并行计算–分布式计算

1.2 MapReduce编程思想

MapReduce的核心思想:分而治之计算向数据移动

  1. 将要计算的大数据拆分成多个小的数据
  2. 将计算任务分配到数据所在的节点(计算向数据移动),对局部数据进行局部计算获取局部结果
  3. 将多个小任务的结果,进行合并汇总处理

总结:集合多台服务器的计算能力,并行处理,提高任务的处理速度。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FURwXjRm-1631064667571)(MapReduce笔记.assets/image-20210218145144056.png)]

MapReduce核心编程概念:

  • MapTask:局部的计算任务(也是可并行任务)
  • ReduceTask:汇总计算任务
  • Job:(任务,作业)一次完整的计算任务,由多个MapTask和ReduceTask组成

2 Yarn架构分析

Yarn:(Yet Another Resource Negotiator的缩写)是hadoop集群资源管理器系统,用来执行MapReduce分布式计算。

MapReduce是一套分布式计算框架,在运行时需要调度多台服务器的资源。而yarn则可以调度多个服务器的计算资源(cpu、内存等),为MapReduce提供运行所需的资源,一套运行环境。

Yarn的结构分析:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cf7I1dza-1631064667572)(MapReduce笔记.assets/image-20210715232933810.png)]

Yarn从整体上还是属于master/slave模型,主要依赖于三个组件来实现功能:

  • ResourceManager

    类似:管理者,只做管理,不负责具体任务

    1. Yarn集群的管理者(Master)

      监控管理集群中所有NodeManager的资源

    2. 任务调度

      1. 接收job任务
      2. 为job任务在集群中分配计算资源
  • NodeManager

    类似:具体干活的人

    1. Yarn集群的从机(Slave)

      管理本机的计算资源(cpu、内存、网络、硬盘)

      定期通过心跳向ResourceManager汇报节点资源状态

    2. 执行计算任务

      接收ResourceManager分配的具体计算任务

      ​ MapTask(局部计算任务)
      ​ ReduceTask(汇总计算任务)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cDb6UDg1-1631064667574)(MapReduce笔记.assets/image-20210218161327597.png)]

    注意:NodeManager和DataNode是同1个节点

  • ApplicationMaster

    类似:监工

    1. 监控Yarn集群每个计算任务(MapTask、ReduceTask)的运行状态
      任务名字 执行进度 结果 成功失败 消耗的资源
    2. 和ResourceManager通信,协调运行资源
      根据任务的执行情况,申请更多或者释放资源,甚至说任务失败重试。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P5kwyxVD-1631064667575)(MapReduce笔记.assets/image-20210218161409889.png)]

3 Yarn伪分布式环境安装

Hadoop包含HDFS和Yarn。

  1. 首先准备好HDFS环境

    #验证HDFS环境
    [root@hadoop10 hadoop-2.9.2]#start-dfs.sh
    
    #查看进程信息
    [root@hadoop10 hadoop-2.9.2]# jps
    10951 SecondaryNameNode
    10648 NameNode
    16539 Jps
    10748 DataNode
    
    #关闭HDFS
    [root@hadoop10 hadoop-2.9.2]# stop-dfs.sh 
    
  2. 配置Yarn

    Hadoop包含HDFS和Yarn,安装过Hadoop后直接配置Yarn即可,
    配置文件还是在hadoop文件夹/etc/hadoop目录里

    1. mapred-site.xml

      配置Hadoop的资源调用器为yarn。
      注意:需要将mapred-site.xml.template改名为mapred-site.xml

      <configuration>
              <!-- 配置MapReduce框架的资源调度器为yarn-->
              <property>
                      <name>mapreduce.framework.name</name>
                      <value>yarn</value>
              </property>
      </configuration>
      
    2. yarn-site.xml

      配置Yarn集群主机,ResourceManager的ip以及MapReduce的策略:mapreduce_shuffle

       <!--配置resourcemanager的主机ip-->
        <property>
            <name>yarn.resourcemanager.hostname</name>
            <value>hadoop10</value>
        </property>
      <!-- mapreduce计算服务方法。 -->
        <property>
            <name>yarn.nodemanager.aux-services</name>
            <value>mapreduce_shuffle</value>
        </property>
       
        <!--关闭物理内存检查(测试使用,实战不用)-->
        <property>
          <name>yarn.nodemanager.pmem-check-enabled</name>
          <value>false</value>
        </property>
        <!--关闭虚拟内存检查(测试使用,实战不用)-->
        <property>
          <name>yarn.nodemanager.vmem-check-enabled</name>
          <value>false</value>
        </property>
      
    3. slaves

      配置NodeManager的ip(也是DataNode的ip)

      hadoop10
      
  3. 启动HDFS和Yarn集群

    #启动HDFS集群
    start-dfs.sh
    #启动Yarn集群
    start-yarn.sh
    
    说明:stop-yarn.sh #关闭Yarn集群
    
  4. 验证

    jps[root@hadoop10 hadoop]# jps
    17570 Jps
    16984 NameNode			
    17530 NodeManager		#局部计算节点
    17085 DataNode
    17277 SecondaryNameNode
    17439 ResourceManager	#资源调度节点
    

    访问Yarn的web服务:http://resourcemanager所在节点ip:8088

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QpNzgCON-1631064667576)(MapReduce笔记.assets/image-20210218172651751.png)]

4 MapReduce编程

4.1 编程思路

案例需求:统计一个文件中所有单词出现的次数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gDa62S3h-1631064667577)(MapReduce笔记.assets/image-20210218210557553.png)]

  1. 将一个大文件拆分成多个小文件,并且每个文件的数据拆分成 <偏移量-行数据> 的键值对
  2. 在不同的NodeManager上将 <偏移量-行数据> 格式的键值对转换为 <单词-次数>的键值对
  3. MapTask阶段执行后,下载多个临时结果,分组合并成一个文件
  4. 然后汇总计算单词和次数的关系,将汇总结果输出到文件中

大体流程分析后,我们结合相关Java API再分析一下编程时需要关注的部分:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V4kCjrju-1631064667577)(MapReduce笔记.assets/image-20210218214340292.png)]

  1. MapTask阶段

    1. TextInputFormat:

      1. 将原有的数据拆分
      2. 辅助MapTask程序读取 <偏移量-行数据>的键值对数据
    2. Mapper:

      1. 程序员需要自定义局部计算(处理k-v的代码逻辑)

      2. map方法每调用1次处理一个键值对 ,将<偏移量-行数据>转换为<单词-次数1>的键值对

      3. MapTask不断读取文件,解析为 <偏移量-行数据>,然后不断调用map方法

        伪代码
            while(读取下一个kv){
                mapper.map(k,v,context);
            }
        

    说明:读取文件拆分,以及循环调用map方法已经在MapReduce框架中实现好,编程时在此阶段只需要关注如何定义map方法实现即可。

  2. ReduceTask阶段

    1. Reducer:

      1. 程序员需要自定义汇总计算的代码逻辑

      2. reduce方法每调用1次,处理分组合并后的一个键值对,将 <单词-[次数,次数]>转换为<单词-出现总次数>的键值对

      3. ReduceTask不断读取 k-vs,循环调用reduce方法

        while(读取下一个k-vs){
            reducer.reduce(k,vs);
        }
        
    2. TextOutputFormat:

      将reducer计算的k-v结果,写到文件中,并将文件传入到HDFS中

    说明:循环调用reduce方法以及输出到HDFS这部分已经在MapReduce框架中实现好,编程时在次阶段只需要关注如何定义reduce方法实现即可。

  3. Job:

    一个MapReduce计算任务就是一个Job。
    Job=MapTask+ReduceTask,编程时需要一个Job对象统领Mapper和Reducer。

4.2 第1个MapReduce程序(统计单词出现次数)

  1. 准备log4j.properties和MapReduce依赖

    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-client</artifactId>
        <version>2.9.2</version>
    </dependency>
    
  2. 编码

    1. Mapper开发

      public class WordCountJob {
          //注意:Mapper写成了静态内部类
          /*
              mapper的作用: <偏移量-行数据> ==> <单词-出现的次数(默认1)>
              Mapper的四个泛型按顺序:KEYIN、VALUEIN、KEYOUT、VALUEOUT
              KEYIN: 输入的键值对键的类型(偏移量的类型),在Java中是Long,Hadoop中使用LongWritable
              VALUEIN:输入的键值对值的类型(行数据的类型),在Java中是String,Hadoop中使用Text
              KEYOUT:输出结果的键的类型(单词的类型),在Java中是String,Hadoop中使用Text
              VALUEOUT:输出结果的值的类型(次数的类型),在Java中是Integer,Hadoop中使用IntWritable
           */
          public static class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
              @Override
              /*
                  执行时机:每读取文件的一行,调用1次map方法
                  key:行数据的偏移量 (这里没啥用)
                  value:一行的数据 (需要拆分成多个单词)
                  context:输出结果的工具
               */
              protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
                  String[] words = value.toString().trim().split(" ");
                  //拆分之后,得到的每个单词出现了1次
                  for (String word : words) {
                      context.write(new Text(word),new IntWritable(1));
                  }
              }
          }
      }
      
    2. Reducer开发

      /*
             reducer的作用: <单词-出现次数的列表> ==> <单词,总次数>
             Reducer的四个泛型按顺序:KEYIN、VALUEIN、KEYOUT、VALUEOUT
             KEYIN: 输入的键值对键的类型(单词的类型),在Java中是String,Hadoop中使用Text
             VALUEIN:输入的键值对值的类型(次数的类型),Java中是Integer,Hadoop中使用IntWritable
             KEYOUT:输出结果的键的类型(单词的类型),在Java中是String,Hadoop中使用Text
             VALUEOUT:输出结果的值的类型(总次数的类型),在Java中是Integer,Hadoop中使用IntWritable
          */
      public static class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
          @Override
          /*
                  key: 单词
                  values: 出现的次数的集合
                  context: 输出结果的工具
               */
          protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
              //累加values中的次数,即得到某一个单词的总次数
              int sum = 0;
              for (IntWritable value : values) {
                  sum += value.get();
              }
              context.write(key,new IntWritable(sum));
          }
      }
      
    3. Job开发

      //要求:继承Configured类 实现Tool接口,重写run方法
      public class WordCountJob extends Configured implements Tool {
          public static void main(String[] args) throws Exception {
              System.setProperty("HADOOP_USER_NAME", "root");//设置下用户名
              ToolRunner.run(new WordCountJob(),args);
          }
          @Override
          public int run(String[] strings) throws Exception {
              //1 初始化配置
              Configuration configuration = new Configuration();
              configuration.set("fs.defaultFS","hdfs://hadoop10:9000");
              //2 创建Job对象
              Job job = Job.getInstance(configuration);
              job.setJarByClass(WordCountJob.class);
              //3 组装Job:设置读取文件和输出结果的工具类型
              job.setInputFormatClass(TextInputFormat.class);
              job.setOutputFormatClass(TextOutputFormat.class);
              //4 组装Job:设置要处理的文件路径和输出结果的路径
              TextInputFormat.addInputPath(job,new Path("/baizhi/mapreduce/demo1/word.txt"));
              TextOutputFormat.setOutputPath(job,new Path("/baizhi/mapreduce/demo1/out"));//输出文件如果已经存在会报错
              //5 组装Job: 设置mapper和reducer的相关信息
              job.setMapperClass(WordCountMapper.class);
              job.setMapOutputKeyClass(Text.class);
              job.setMapOutputValueClass(IntWritable.class);
              job.setReducerClass(WordCountReducer.class);
              job.setOutputKeyClass(Text.class);
              job.setOutputValueClass(IntWritable.class);
              //6 启动Job
              boolean completion = job.waitForCompletion(true);//true:打印日志信息
              return completion ? 1:0;
          }
      ...
      }
      
  3. 查看结果

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XbVJBLtm-1631064667578)(MapReduce笔记.assets/image-20210218223406726.png)]

5 MR程序提交详解

5.1 Yarn本地提交MR程序

直接在idea中运行mapreduce程序,有一部分是在windows中运行,实际生产环境一定是完全linux环境,需要将程序打包后部署到linux中运行。

  1. 将Java程序打包 xxx.jar

    1. pom.xml配置打包方式和编解码集

      <packaging>jar</packaging>
      <properties>
          <project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
          <maven.compiler.source>1.8</maven.compiler.source>
          <maven.compiler.target>1.8</maven.compiler.target>
      </properties>
      
    2. 执行打包命令

      mvn clean package -DskipTests
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pC4oCppd-1631064667579)(MapReduce笔记.assets/image-20210219112152682.png)]

      也可以通过idea的maven工具完成

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5hARtHMq-1631064667579)(MapReduce笔记.assets/image-20210219171209213.png)]

  2. 将jar包上传到hadoop服务器上,在服务器上执行下jar包

    1. 找到生成的jar包(在项目的target目录下生成)

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nMAlH1xn-1631064667580)(MapReduce笔记.assets/image-20210219112456868.png)]

    2. 将jiar包上传到hadoop服务器上(直接使用xshell或者MobaXterm上传文件),然后执行

      hadoop jar  jar包名  job全类名
      示例:
       hadoop jar hadoop-test-1.0-SNAPSHOT.jar com.baizhi.mapreduce.WordCountJob
      

    补充:

    #1 设置Maven打包jar包的名字 pom.xml添加如下配置
    <build>
    	<finalName>wordcount</finalName>
    </build>
    
    #2  实战场景
      需求:MR处理商城平台,每天产生的商品日志信息,统计每天每个商品的访问次数? 
      场景:mr的jar程序,每天定时运行一次。
            
    定时器:每天半夜0:00开始执行
              hadoop jar /opt/app/wordcount.jar com.baizhi.mapreduce.WordCountJob
    

5.2 自动远程部署jar包

手动部署jar包到Hadoop服务器上比较麻烦,可以借助maven的插件,自动部署jar包。

  1. pom.xml添加插件的ssh扩展和wagon(货车)插件

    <build>
        <finalName>wordcount</finalName>
    
        <!-- 配置ssh扩展-->
        <extensions>
            <extension>
                <groupId>org.apache.maven.wagon</groupId>
                <artifactId>wagon-ssh</artifactId>
                <version>2.8</version>
            </extension>
        </extensions>
    
        <!-- 配置wagon的copy插件-->
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>wagon-maven-plugin</artifactId>
                <version>1.0</version>
                <configuration>
                    <!--上传的本地jar的位置(固定写法)-->
                    <fromFile>target/${project.build.finalName}.jar</fromFile>
                    <!--远程拷贝的地址 scp://用户名:密码@ip:/opt/app-->
                    <url>scp://root:root@hadoop10:/opt/app</url>
                </configuration>
            </plugin>
        </plugins>
    </build>
    
  2. 执行插件命令,上传文件

    mvn clean package #重新编译(不是必须)
    mvn wagon:upload-single #自动上传jar包到hadoop服务器
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k9ytA7Kk-1631064667580)(MapReduce笔记.assets/image-20210219170648205.png)]

    或者在idea的maven界面,单击插件的功能完成

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3XO29pzx-1631064667581)(MapReduce笔记.assets/image-20210219171320849.png)]

  3. 在Hadoop服务器上,运行jar包

    hadoop jar /opt/app/wordcount.jar com.baizhi.mapreduce.WordCountJob
    

5.3 Yarn添加JobHistroy日志聚合服务

JobHistoryServer:

  1. 负责将各个节点上的日志文件集中到HDFS中,便于管理。—日志聚合。
  2. 提供查看日志信息web管理界面。
  1. 修改yarn-site.xml

    <!-- 开启日志聚合:将各个节点上的日志文件集中到HDFS中,便于管理 -->
    <property>
      <name>yarn.log-aggregation-enable</name>
      <value>true</value>
    </property>
    <!-- 设置日志保存时间,单位秒 -->
    <property>
      <name>yarn.log-aggregation.retain-seconds</name>
      <value>106800</value>
    </property>
    
  2. 修改mapred-site.xml

<!-- 设置日志服务器的远程传输日志信息的端口和地址 -->
<property>
  <name>mapreduce.jobhistory.address</name>
  <value>hadoop10:10020</value>
</property>

<!-- 设置日志服务器的web访问的地址和端口 -->
<property>
  <name>mapreduce.jobhistory.webapp.address</name>
  <value>hadoop10:19888</value>
</property>
  1. 启动jobhistory(在日志服务器所在节点执行)

    # 启动
    [root@hadoop10 ~]# mr-jobhistory-daemon.sh start historyserver
    
    # 关闭
    [root@hadoop10 ~]# mr-jobhistory-daemon.sh stop historyserver 
    
  2. 验证:在windows中通过浏览器访问JobHistoryServer

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jw4jn7O2-1631064667581)(MapReduce笔记.assets/image-20210219225306642.png)]

6 Hadoop序列化和反序列化

6.1 概念

Hadoop序列化和反序列化:MapReduce流程中k-v对象数据在内存和磁盘中数据的过程。

序列化:对象数据从内存到输出保存到磁盘中的过程。
反序列化:从磁盘中读取文件的数据,在内存中恢复成对象的过程。
MapReduce流程中的key-value对象,需要反复在磁盘和内存中进行传输,要求Key-Value要序列化和反序列化。

Hadoop序列化的时机:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AcbdgwyV-1631064667582)(MapReduce笔记.assets/image-20210219220354222.png)]

6.2 Hadoop和Java序列化对比

Hadoop并没有使用Java序列化的方式,而是自定义了一套序列化方式。

Java序列化数据保存的信息比较全:包名、类型、继承关系等等,导致额外的带宽占用,降低传输效率。
Hadoop序列化:尽量序列化数据内容,数据紧凑,降低带宽占用,性能高。

Hadoop和Java序列化对比

区别Java序列化Hadoop序列化
特点信息量大:package名、class类名、父类、递归的父类信息、数据值等紧凑:仅序列化数据,大数据情况下减少数据带宽占用、提高传输性能。
实现方式实现Serializable接口实现Writable接口

Hadoop中内置的序列化类型

Java类型Hadop需要实现Writable
booleanBooleanWritable
byteByteWritable
shortShortWritable
intIntWritable
longLongWritable
floatFloatWritable
doubleDoubleWritable
StringText
arryArrayWritable
mapMapWritable
nullNullWritable

6.3 自定义序列化类型

自定义Hadoop序列化类型要求:

  1. 实现Writable接口
  2. 实现接口中的序列化和反序列化方法
public class DataFlowWritable implements Writable {
    private Integer up;
    private Integer down;

    @Override
    //序列化方法:通过DataOutput输出对象中的属性数据
    public void write(DataOutput dataOutput) throws IOException {
        /*
        序列化调用的方法,必须跟属性类型保持一致
            writeInt 输出int类型
            writeLong 输出long类型
            writeUTF 输出String类型
         */
        dataOutput.writeInt(up);
        dataOutput.writeInt(down);
    }

    @Override
    //反序列化方法:通过DataInput读取数据,为属性赋值
    public void readFields(DataInput dataInput) throws IOException {
        /*
          反序列化调用的方法,必须跟属性类型一致
          读取属性的顺序要跟输出的顺序一致
         */
        this.up = dataInput.readInt();
        this.down = dataInput.readInt();
    }

    public Integer getUp() {
        return up;
    }

    public void setUp(Integer up) {
        this.up = up;
    }

    public Integer getDown() {
        return down;
    }

    public void setDown(Integer down) {
        this.down = down;
    }

    @Override
    public String toString() {
        return "DataFlowWritable{" +
                "up=" + up +
                ", down=" + down +
                '}';
    }
}

注意:

  • 序列化和反序列化的属性顺序保持一致
  • 序列化方法和反序列化方法要跟属性类型保持一致
  • 反序列化后的值,必须赋值给当前对象的属性

7 案例:app流量分析

需求介绍:

手机使用APP的流量数据,每次手机上网记录一条信息。 需求:统计每个手机号的 上传总流量 下载总流量 总流量

数据格式说明:

# 案例数据
时间戳        手机号                  mac地址         ip地址          上传包 下载包  上传流量 下载流量  HTTP状态码
1363157985066 13726230503 00-FD-07-A4-72-B8:CMCC 120.196.100.82 24 27 2481 24681 200
1363157995052 13826544101 5C-0E-8B-C7-F1-E0:CMCC 120.197.40.4 4 0 264 0 200
1363157991076 13926435656 20-10-7A-28-CC-0A:CMCC 120.196.100.99 2 4 132 1512 200
1363154400022 13926251106 5C-0E-8B-8B-B1-50:CMCC 120.197.40.4 4 0 240 0 200
1363157985066 13726230503 00-FD-07-A4-72-B8:CMCC 120.196.100.82 24 27 2481 24681 200
1363157995052 13826544101 5C-0E-8B-C7-F1-E0:CMCC 120.197.40.4 4 0 264 0 200
1363157991076 13926435656 20-10-7A-28-CC-0A:CMCC 120.196.100.99 2 4 132 1512 200
1363154400022 13926251106 5C-0E-8B-8B-B1-50:CMCC 120.197.40.4 4 0 240 0 200

# 期望结果
13726230503   上传流量:4962  下载流量:49362  总数据流量:  54324
13826544101   上传流量:528  下载流量:0  总数据流量:  528
13926251106   上传流量:480  下载流量:0  总数据流量:  480
13926435656   上传流量:264  下载流量:3024  总数据流量:  3288

思路分析:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2GXylUeK-1631064667582)(MapReduce笔记.assets/image-20210228122550796.png)]

  1. 自定义PhoneFlowWritable封装上传流量和下载流量

    public static class PhoneFlowWritable implements Writable{
        private Integer up;
        private Integer down;
    
        @Override
        public void write(DataOutput dataOutput) throws IOException {
            dataOutput.writeInt(up);
            dataOutput.writeInt(down);
        }
    
        @Override
        public void readFields(DataInput dataInput) throws IOException {
            this.up = dataInput.readInt();
            this.down = dataInput.readInt();
        }
    
        public Integer getUp() {
            return up;
        }
    
        public void setUp(Integer up) {
            this.up = up;
        }
    
        public Integer getDown() {
            return down;
        }
    
        public void setDown(Integer down) {
            this.down = down;
        }
    
        @Override
        public String toString() {
            return "PhoneFlowWritable{" +
                "up=" + up +
                ", down=" + down +
                '}';
        }
    }
    
  2. Mapper开发

    public static class PhoneFlowMapper extends Mapper<LongWritable,Text, Text,PhoneFlowWritable>{
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            /*
                key: 偏移量
                value:一行的数据,对value拆分可以得到 手机号 上传流量 下载流量 等等数据
                 */
            String[] splits = value.toString().trim().split("\t");
            String phone = splits[1];
            String upFlowStr = splits[6];
            String downFlowStr = splits[7];
    
            PhoneFlowWritable flow = new PhoneFlowWritable();
            flow.setUp(Integer.parseInt(upFlowStr));
            flow.setDown(Integer.parseInt(downFlowStr));
    
            context.write(new Text(phone),flow);
        }
    }
    
  3. Reducer开发

    public static class PhoneFlowReducer extends Reducer<Text, PhoneFlowWritable, Text, Text> {
        @Override
        protected void reduce(Text key, Iterable<PhoneFlowWritable> values, Context context) throws IOException, InterruptedException {
            /*
                key:手机号
                values: 同1个手机号的所有上传下载流量
                 */
            // 遍历values统计一个手机号所有的上传和下载流量
            int upSum = 0;
            int downSum = 0;
            for (PhoneFlowWritable flow : values) {
                upSum += flow.getUp();
                downSum += flow.getDown();
            }
    
            context.write(key,new Text("上传流量:"+upSum+" 下载流量:"+downSum+" 总数据流量:"+(upSum+downSum)));
        }
    }
    
  4. Job开发

    public class PhoneFlowJob extends Configured implements Tool {
        public static void main(String[] args) throws Exception {
            System.setProperty("HADOOP_USER_NAME", "root");
            ToolRunner.run(new PhoneFlowJob(),args);
        }
    
        @Override
        public int run(String[] strings) throws Exception {
            // 1 初始化配置
            Configuration configuration = new Configuration();
            configuration.set("fs.defaultFS","hdfs://hadoop10:9000");
            //2 创建job
            Job job = Job.getInstance(configuration);
            job.setJarByClass(PhoneFlowJob.class);
            //3 组装job:设置读取数据和输出数据的工具
            job.setInputFormatClass(TextInputFormat.class);
            job.setOutputFormatClass(TextOutputFormat.class);
            //4 组装job:设置读取的文件路径和输出的结果路径
            TextInputFormat.addInputPath(job,new Path("/baizhi/mapreduce/demo2/phone.log"));
            TextOutputFormat.setOutputPath(job,new Path("/baizhi/mapreduce/demo2/out"));
            //5 组装job:设置mapper和reducer相关的信息
            job.setMapperClass(PhoneFlowMapper.class);
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(PhoneFlowWritable.class);
            job.setReducerClass(PhoneFlowReducer.class);
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(Text.class);
            //6 启动job
            return job.waitForCompletion(true) ? 1 : 0;
        }
        ...
    }
    

8 案例:数据清洗

8.1 实战案例

需求介绍

背景介绍:通常情况下,大数据平台获得原始数据文件中,存在大量无效数据和缺失数据,需要再第一时间,对数据进行清洗,获得符合后续处理需求的数据内容和格式

典型需求:对手机流量原始数据,将其中的手机号为"null"和不完整的数据去除

数据格式说明:

# 原数据
id             手机号        手机mac                 ip地址                  上传    下载  HTTP状态码
1363157985066  13726230503  00-FD-07-A4-72-B8:CMCC  120.196.100.82  24  27  2481  24681  200
1363157995052  13826544101  5C-0E-8B-C7-F1-E0:CMCC  120.197.40.4  4  0  264  0  200
1363157991076  13926435656  20-10-7A-28-CC-0A:CMCC  120.196.100.99  2  4  132  1512  200
1363154400022  13926251106  5C-0E-8B-8B-B1-50:CMCC  120.197.40.4  4  0  240  0  200
1363157985066  13726230503  00-FD-07-A4-72-B8:CMCC  120.196.100.82  24  27  2481  24681  200
1363157995052  13826544101  5C-0E-8B-C7-F1-E0:CMCC  120.197.40.4  4  0  264  0  200
1363157991076  13926435656  20-10-7A-28-CC-0A:CMCC  120.196.100.99  2  4  132  1512  200
1363154400022  13926251106  5C-0E-8B-8B-B1-50:CMCC  120.197.40.4  4  0  240  0  200
1363157995052  13826544109  5C-0E-8B-C7-F1-E0:CMCC  120.197.40.4  4  0
1363157995052  null  5C-0E-8B-C7-F1-E0:CMCC  120.197.40.4  4  0  240  0  200
1363157991076  13926435659  20-10-7A-28-CC-0A:CMCC  120.196.100.99  2  4  null  null  null

# 期望结果【删除其中手机号不符合要求、上传流量确实和下载流量缺失的数据,并仅保格式正确的数据。】
1363157985066  13726230503  00-FD-07-A4-72-B8:CMCC  120.196.100.82  24  27  2481  24681  200
1363157995052  13826544101  5C-0E-8B-C7-F1-E0:CMCC  120.197.40.4  4  0  264  0  200
1363157991076  13926435656  20-10-7A-28-CC-0A:CMCC  120.196.100.99  2  4  132  1512  200
1363154400022  13926251106  5C-0E-8B-8B-B1-50:CMCC  120.197.40.4  4  0  240  0  200
1363157985066  13726230503  00-FD-07-A4-72-B8:CMCC  120.196.100.82  24  27  2481  24681  200
...

思路分析:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sGmOFKaU-1631064667583)(MapReduce笔记.assets/image-20210228125748536.png)]

编码:

# 重点:
1.Map阶段将格式正确的数据做成key输出,而value则不需要输出任何数据(NullWritable2.MapReduce整个流程中可以取消reduce阶段的程序执行,map输出的会直接作为结果输出到HDFS文件中。
  
# 编码实现
1. 删除job中有关reducer的相关设置:reducer类和输出的key value类型。
2. 手动设置reducetask的个数为0
   job.setNumReduceTasks(0);//取消reducer
  1. Mapper开发

    public static class FlowDataMapper extends Mapper<LongWritable, Text, Text,NullWritable> {
            @Override
            protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
                /*
                value 一行数据,判断value数据格式是否正确,如果正确就输出,否则跳过
                 */
                if (check(value.toString().trim())) {
                    context.write(value,NullWritable.get());
                }
            }
    
            private boolean check(String data){
                // 如果拆分后元素个数少于9个,return false
                String[] splits = data.split(" ");
                if(splits.length < 9){
                    return false;
                }
    
                // 手机号为空,return false
                String phone = splits[1];
                if (phone.isEmpty() || phone.equalsIgnoreCase("null")) {
                    return false;
                }
    
                // 上传流量或者下载流量为空,return false
                String upFlow = splits[6];
                if (upFlow.isEmpty() || upFlow.equalsIgnoreCase("null")) {
                    return false;
                }
    
                String downFlow = splits[7];
                if (downFlow.isEmpty() || downFlow.equalsIgnoreCase("null")) {
                    return false;
                }
    
                return true;
            }
        }
    
  2. Job开发

    public class FlowDataCleanJob extends Configured implements Tool {
        public static void main(String[] args) throws Exception {
            System.setProperty("HADOOP_USER_NAME", "root");
            ToolRunner.run(new FlowDataCleanJob(),args);
        }
    
        @Override
        public int run(String[] strings) throws Exception {
            //1 初始化配置
            Configuration configuration = new Configuration();
            configuration.set("fs.defaultFS", "hdfs://hadoop10:9000");
            //2 创建Job
            Job job = Job.getInstance(configuration);
            job.setJarByClass(FlowDataCleanJob.class);
            //3 组装Job: 设置读取数据和数据结果的工具
            job.setInputFormatClass(TextInputFormat.class);
            job.setOutputFormatClass(TextOutputFormat.class);
            //4 组装Job: 设置读取的文件路径和输出的文件路径
            TextInputFormat.addInputPath(job,new Path("/baizhi/mapreduce/demo3/dataclean.log"));
            TextOutputFormat.setOutputPath(job,new Path("/baizhi/mapreduce/demo3/out"));
            //5 组装Job:设置Mapper相关的信息
            job.setMapperClass(FlowDataCleanMapper.class);
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(NullWritable.class);
            job.setNumReduceTasks(0);//不再执行reducer
            //6 启动Job
            return job.waitForCompletion(true) ? 1:0;
        }
        ...
    }
    

8.2 计数器Counter

计数器是MapReduce框架内置的日志工具,可以用于统计特定代码的执行次数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3DsXzlTO-1631064667583)(MapReduce笔记.assets/image-20210220225124056.png)]

用途:通常可以使用Counter统计自定义代码的执行次数

# 编码
  context.getCounter("自定义计数器组名","自定义计数器名").increment(1L); 

需求:

统计清洗的数据总条数、有效数据条数和无效数据条数

protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
    /*
            value 一行数据,判断value数据格式是否正确,如果正确就输出,否则跳过
             */
    context.getCounter("dataclean","全部数据").increment(1L);
    if (check(value.toString().trim())) {
        context.write(value,NullWritable.get());
        context.getCounter("dataclean","有效数据").increment(1L);
    }else{
        context.getCounter("dataclean","无效数据").increment(1L);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qZsfzv2Z-1631064667584)(MapReduce笔记.assets/image-20210221132413200.png)]

9 排序

9.1 默认排序

案例:斗鱼主播日志数据按照观众人数升序序排序

# 案例
用户id  观众人数
团团  300
小黑  200
哦吼  400
卢本伟  100
八戒  250
悟空  100
唐僧  100


# 期望结果
卢本伟  100
悟空  100
唐僧  100
小黑  200
八戒  250
团团  300
哦吼  400
  1. Mapper开发

    public static class AscSortMapper extends Mapper<LongWritable,Text, IntWritable, Text> {
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            /*
                value表示一行数据,比如: 团团 300 ,要输出的结果为 300 团团
                 */
            String[] splits = value.toString().split(" ");
            context.write(new IntWritable(Integer.parseInt(splits[1])),new Text(splits[0]));
        }
    }
    
  2. Reducer开发

    public static class AscSortReducer extends Reducer<IntWritable, Text, Text, IntWritable> {
            @Override
            protected void reduce(IntWritable key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
                for (Text value : values) {
                    context.write(value,key);
                }
            }
        }
    
  3. Job开发

    public class AscSortJob extends Configured implements Tool {
        public static void main(String[] args) throws Exception {
            System.setProperty("HADOOP_USER_NAME", "root");
            ToolRunner.run(new AscSortJob(),args);
        }
    
        @Override
        public int run(String[] strings) throws Exception {
            //1 初始化配置
            Configuration configuration = new Configuration();
            configuration.set("fs.defaultFS", "hdfs://hadoop10:9000");
            //2 创建Job
            Job job = Job.getInstance(configuration);
            job.setJarByClass(AscSortJob.class);
            //3 组装Job: 设置读取数据和输出结果的工具
            job.setInputFormatClass(TextInputFormat.class);
            job.setOutputFormatClass(TextOutputFormat.class);
            //4 组装Job: 设置读取和输出的文件路径
            TextInputFormat.addInputPath(job,new Path("/baizhi/mapreduce/demo4/sort.log"));
            TextOutputFormat.setOutputPath(job,new Path("/baizhi/mapreduce/demo4/out"));
            //5 组装Job:设置Mapper和Reducer的相关信息
            job.setMapperClass(AscSortMapper.class);
            job.setMapOutputKeyClass(IntWritable.class);
            job.setMapOutputValueClass(Text.class);
            job.setReducerClass(AscSortReducer.class);
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
            //6 启动Job
            return job.waitForCompletion(true) ? 1:0;
        }
        ...
    }
    

默认排序规则:

MapReduce流程中,默认根据mapper输出的Key-Value对,按照Key的大小升序排列

默认排序规则:
key如果是数字:从小到大排序
key如果是字符串:按照字典顺序排序(a<b<c)

流程分析:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-078AbqhO-1631064667584)(MapReduce笔记.assets/image-20210228161645680.png)]

默认排序时机:

Map阶段:map处理数据,写出到磁盘时会对数据进行排序输出(降低Reduce阶段的压力)

Reduce阶段:将来自不同MapTask的数据文件进行归并排序

9.2 自定义排序

案例:斗鱼主播日志数据按照观众人数降序排序

# 自定义排序
# 案例
团团  300
小黑  200
哦吼  400
卢本伟  100
八戒  250
悟空  100
唐僧  100


# 期望
哦吼  400
团团  300
八戒  250
小黑  200
卢本伟  100
悟空  100
唐僧  100

思路分析:

MapReduce框架会自动根据key进行排序,只需要修改Key的排序规则即可。
分析下IntWritable类的排序规则

public interface WritableComparable<T> extends Writable, Comparable<T> {
}

public class IntWritable implements WritableComparable<IntWritable> {
 private int value;
 ...
 public int compareTo(IntWritable o) {
         int thisValue = this.value;
         int thatValue = o.value;
         return thisValue < thatValue ? -1 : (thisValue == thatValue ? 0 : 1);
     }
}

通过分析,可知IntWritable实现Comparale接口,在compareTo方法中定义了排序规则。如果要重写Key的排序规则,就需要自定义Key类型,实现Comparable接口,重写compareTo方法。

compareTo规则:
要升序排列:this 和 that(形参接收) 比较,this<that 返回-1,否则返回0或1
要降序排列:this和 that(形参接收)比较,this<that 返回1,否则返回0或-1

编码:

/*
# 1. 需要自定义Mapper输出的key的类型,实现WritableComparable接口
# 2. 实现compareTo方法。
# 3. 补齐write和readFields的序列化相关方法 
*/
public static class WatcherWritable implements WritableComparable<WatcherWritable>{
        private int watcher;

        public WatcherWritable() {
        }

        public WatcherWritable(int watcher) {
            this.watcher = watcher;
        }

        public int getWatcher() {
            return watcher;
        }

        public void setWatcher(int watcher) {
            this.watcher = watcher;
        }

        @Override
        public String toString() {
            return Integer.toString(this.watcher);
        }

        @Override
        public int compareTo(WatcherWritable o) {
            if(this.watcher < o.watcher){
                return 1;
            }
            if(this.watcher == o.watcher){
                return 0;
            }
            return -1;
        }

        @Override
        public void write(DataOutput dataOutput) throws IOException {
            dataOutput.writeInt(this.watcher);
        }

        @Override
        public void readFields(DataInput dataInput) throws IOException {
            this.watcher = dataInput.readInt();
        }
    }

9.3 二次排序

案例:主播按照观众人数降序排序,如果观众人数相同,再按直播时长排序。

# 案例数据
用户id  观众人数  直播时长
团团  300  1000
小黑  200  2000
哦吼  400  7000
卢本伟  100  6000
八戒  250  5000
悟空  100  4000
唐僧  100  3000



# 期望结果
哦吼  400  7000
团团  300  1000
八戒  250  5000
小黑  200  2000
卢本伟  100  6000
悟空  100  4000
唐僧  100  3000

思路分析:

MapReduce根据Key类型的compareTo方法决定如何排序。此时,需要先根据观看人数再根据直播时长进行比较,可以定义Key类型,封装 观看人数和直播时长 为属性,然后在compareTo方法中定义比较逻辑

编码:

 public static class PlayWritable implements WritableComparable<PlayWritable>{
        private int viewer;
        private int length;
        public PlayWritable() {
        }

        public PlayWritable(int viewer, int length) {
            this.viewer = viewer;
            this.length = length;
        }

        public int getViewer() {
            return viewer;
        }

        public void setViewer(int viewer) {
            this.viewer = viewer;
        }

        public int getLength() {
            return length;
        }

        public void setLength(int length) {
            this.length = length;
        }

        @Override
        public String toString() {
            return this.viewer+ " "+this.length;
        }

        @Override
        public int compareTo(PlayWritable o) {
            if(o.viewer != this.viewer){
                return o.viewer - this.viewer;
            }
            return o.length - this.length;
        }

        @Override
        public void write(DataOutput dataOutput) throws IOException {
            dataOutput.writeInt(this.viewer);
            dataOutput.writeInt(this.length);
        }

        @Override
        public void readFields(DataInput dataInput) throws IOException {
            this.viewer = dataInput.readInt();
            this.length = dataInput.readInt();
        }
    }

总结:

# 总结MapReduce排序
1. 时机:
  ① MapTask阶段输出k-v后,局部排序[目的: 提前局部并行排序,提高排序速度,减轻reduce端排序压力]
  ② 在ReduceTask阶段的merge(分组)时候, 数据总体合并排序[归并排序,保证整体有序]
2. 排序依据:
  MR对Mapper输出的key进行排序。
3. 排序规则: 
  MR排序的时候,调用Key.compareTo()决定排序规则。
   
# MR框架内置
1. 排序(Key)
2. 分组merge(Key)

10 MapReduce组件详解和优化

10.1 MapTask阶段

10.1.1 FileInputFormat
# 1. 作用:
  ① 对原始的HDFS文件进行逻辑切分。
  ② 指定mapreduce读取文件路径。
  
# 2. API讲解
  ①. 指定一个输入文件
    FileInputFormat.addInputPath(job,new Path("/hdfs文件"));
  ②. 指定一个输入目录
    FileInputFormat.addInputPath(job,new Path("/hdfs目录"));
  ③. 指定多个输入文件
    job.setInputFormatClass(TextInputFormat.class);
    FileInputFormat.addInputPath(job,new Path("/hdfs/文件1.txt"));
    FileInputFormat.addInputPath(job,new Path("/hdfs/文件2.txt"));
    FileInputFormat.addInputPath(job,new Path("/hdfs/文件3.txt"));
10.1.2 Split(逻辑切片,一段任务描述信息)
# 1. Split概念
  MapReduce程序,FileInputFormat对HDFS源文件的逻辑拆分块。(并非真正的数据块)
  通俗:就是一个MapTask处理的数据量描述信息:
      start 从哪儿开始读数据
      length 当前MapTask读取多少数据。
      host 文件所在hdfs的节点位置。
# 2. 内容
  start起始位置
  length长度
  hosts 数据本身所在的datanode节点位置
  
# 3. 默认大小---TextInputFormat 默认设置
  默认 SplitSize == blockSize(实际大小)。 
  分析:过大或者过小,都会导致MapTask跨节点读取数据文件,导致数据传输速度降低。降低效率。
  结论:
    使得NodeManager中启动的MapTask尽可能读取本节点的数据,减少数据的跨网络传输。 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3SDKPDYU-1631064667585)(MapReduce笔记.assets/image-20210228194851505.png)]

为什么一般的SplitSize设置为BlockSize?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FfJLpXSi-1631064667585)(MapReduce笔记.assets/image-20210228195401315.png)]

尽可能的保证NodeManager在处理数据时直接从同1服务器中读取数据,避免跨节点读取数据。

11.1.3 MapTask并行度
# 为什么要并行MapTask ?
  利用多个服务器节点的资源,并行处理数据,提高数据处理速度。

# MapTask并行度决定因素
  Split的个数 ===> 启动MapTask个数

# MapTask并行度
  每个Split数据,启动一个MapTask程序。 
  MapTask启动个数 == Split个数。 

注意:

海量小文件,导致大量的block,导致大量的split,导致启动大量的MapTask,瞬间挤占服务器资源。 MapReduce不适合处理大量小文件数据。

# 场景:Hadoop不适合处理海量的小数据文件
原因:每个文件单独block拆分,海量小文件,导致海量小block,导致海量split,启动大量MapTask,占用过多内存空间。
解决:
   1. HDFS:在HDFS中将多个业务含义相同的数据文件合并成1个文件。
      hdfs dfs -getmerge /xxx.log  /本地目录
      
   2. MapReduce:干预Split 的计算规则,合并多个block为1个split。减少split个数,进而减少MapTask个数。降低服务器运行job的内存占用。
     使用CombineTextInputFormat

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5EdMM9mX-1631064667586)(MapReduce笔记.assets/image-20210228203553217.png)]

11.1.4 CombineTextInputFormat
# 1. 特点: 
    将多个小block合并成1个split处理,设置切片大小为10M。
  
# 2. 应用: 
    海量的小数据文件产生海量小block,合并成大的split,减少split数量,减少MapTask数量,提高MapReduce性能。
    
# 3. 代码:

    job.setInputFormatClass(CombineTextInputFormat.class);// 设置格式化输出类。
    
    CombineTextInputFormat.setMaxInputSplitSize(job,10485760);//10M,只要加起来不超过10M的block数据,都会合并成1个split处理。
    
    CombineTextInputFormat.addInputPath(job,new Path("/hdfs/目录"));//设置读取文件的路径

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-loxkbdo5-1631064667586)(MapReduce笔记.assets/image-20210228214122604.png)]

11.1.5 Combiner

案例:金融平台消费日志数据统计

# 数据说明:
  数据组成:每个月记录一个文件,文件中记录了用户的消费日志数据。
# 需求:统计每个用户的当月消费总金额?

案例数据:

# 测试案例(消费记录)
姓名  消费金额
张三  100
王五  200
张三  300
李四  300
李四  300
张三  400
王五  500
王五  500
张三  600

# 期望结果
李四  600
王五  1200
张三  1400

思路分析:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mf7DzEGD-1631064667587)(MapReduce笔记.assets/image-20210228220425935.png)]

编码:

  1. Mapper开发

     public static class ConsumptionStatistMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
            @Override
            protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
                String[] values = value.toString().split(" ");
    
                context.write(new Text(values[0]),new IntWritable(Integer.parseInt(values[1])));
            }
        }
    
  2. Reducer开发

    public static class ConsumptionStatistReducer extends Reducer<Text,IntWritable,Text,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();
                }
    
                context.write(key,new IntWritable(sum));
            }
        }
    
  3. Job开发

    public class ConsumptionStatisticsJob extends Configured implements Tool {
    
        public static void main(String[] args) throws Exception {
            ToolRunner.run(new ConsumptionStatisticsJob(),args);
        }
    
        @Override
        public int run(String[] strings) throws Exception {
    
            //1 初始化配置
            Configuration configuration = new Configuration();
            configuration.set("fs.defaultFS","hdfs://hadoop10:9000");
    
            //2 创建Job
            Job job = Job.getInstance(configuration);
    
            //3 设置原始数据类型
            job.setInputFormatClass(TextInputFormat.class);
            TextInputFormat.addInputPath(job,new Path("/a/b/consumption-statistics.txt"));
    
            //4 设置mapper类和map的输出类型
            job.setMapperClass(ConsumptionStatistMapper.class);
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(IntWritable.class);
    
            //5 设置reducer类和reduce输出类型
            job.setReducerClass(ConsumptionStatistReducer.class);
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
    
            //6 设置输出结果的输出路径
            job.setOutputFormatClass(TextOutputFormat.class);
            TextOutputFormat.setOutputPath(job,new Path("/a/b/out"));
    
            //7 设置要执行的job的类型
            job.setJarByClass(ConsumptionStatisticsJob.class);
    
            return job.waitForCompletion(true) ? 1:0;
        }
    
        public static class ConsumptionStatistMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
            @Override
            protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
                String[] values = value.toString().split(" ");
    
                context.write(new Text(values[0]),new IntWritable(Integer.parseInt(values[1])));
            }
        }
    
        public static class ConsumptionStatistReducer extends Reducer<Text,IntWritable,Text,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();
                }
    
                context.write(key,new IntWritable(sum));
            }
        }
    }
    

存在的问题:

# 问题:
  所有的累加的统计压力,都放在了Reduce一端。
  MapTask有多个,且并行,但计算压力太小。
  
# 解决思路:
  讲ReduceTask的部分计算压力前置到MapTask阶段 
  本质:对Mapper输出的key-value,执行 局部的Reduce操作(merge[排序 分组 合并],调用Reducer的reduce方法)

Combiner讲解

# 概念
  发生在MapTask阶段的局部ReduceTask操作。
# 发生时机
  Mapper输出key-value之后,数据在内存中经过 排序 分组 合并 并调用reducer.reduce方法,再输出到本地磁盘。
# 代码
  job.setCombinerClass(XxxxReducer.class);
# 场景:
  适合支持迭代的数据分析:求和、统计总数等。
  不适合:求平均值场景。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jhkw3KDu-1631064667587)(MapReduce笔记.assets/image-20210228221723379.png)]

编码:

  1. Mapper开发(和之前一样)

  2. Reducer开发(和之前一样)

  3. Job开发(比之前多了Combiner的设置)

    public class ConsumptionStatisticsJob extends Configured implements Tool {
    
        public static void main(String[] args) throws Exception {
            ToolRunner.run(new ConsumptionStatisticsJob(),args);
        }
    
        @Override
        public int run(String[] strings) throws Exception {
    
            //1 初始化配置
            Configuration configuration = new Configuration();
            configuration.set("fs.defaultFS","hdfs://hadoop10:9000");
    
            //2 创建Job
            Job job = Job.getInstance(configuration);
    
            //3 设置原始数据类型
            job.setInputFormatClass(TextInputFormat.class);
            TextInputFormat.addInputPath(job,new Path("/a/b/consumption-statistics.txt"));
    
            //4 设置mapper类和map的输出类型
            job.setMapperClass(ConsumptionStatistMapper.class);
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(IntWritable.class);
    
            //5 设置reducer类和reduce输出类型
            job.setReducerClass(ConsumptionStatistReducer.class);
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
    
            // 添加Combiner
            job.setCombinerClass(ConsumptionStatistReducer.class);
    
            //6 设置输出结果的输出路径
            job.setOutputFormatClass(TextOutputFormat.class);
            TextOutputFormat.setOutputPath(job,new Path("/a/b/out"));
    
            //7 设置要执行的job的类型
            job.setJarByClass(ConsumptionStatisticsJob.class);
    
            return job.waitForCompletion(true) ? 1:0;
        }
    
        //mapper 和 reducer 省略...
    }
    

10.2 ReduceTask阶段相关

10.2.1 ReduceTask并行度

案例:电商平台商品日志数据分析,统计每个商品的访问次数

数据:

# 测试案例:商品浏览日志
日期      域名          商品url            商品名       pid     驻留时间
2020年3月3日  www.baizhiedu.com  /product/detail/10001.html  iphoneSE  10001  30
2020年3月3日  www.baizhiedu.com  /product/detail/10001.html  iphoneSE  10001  60
2020年3月3日  www.baizhiedu.com  /product/detail/10001.html  iphoneSE  10001  100
2020年3月3日  www.baizhiedu.com  /product/detail/10002.html  xps15  10002  10
2020年3月3日  www.baizhiedu.com  /product/detail/10002.html  xps15  10002  20
2020年3月3日  www.baizhiedu.com  /product/detail/10004.html  iphoneX  10004  100
2020年3月3日  www.baizhiedu.com  /product/detail/10003.html  thinkpadx390  10003  200
2020年3月3日  www.baizhiedu.com  /product/detail/10004.html  iphoneX  10004  100
2020年3月3日  www.baizhiedu.com  /product/detail/10003.html  thinkpadx390  10003  100
2020年3月3日  www.baizhiedu.com  /product/detail/10001.html  iphoneSE  10001  120
2020年3月4日  www.baizhiedu.com  /product/detail/10001.html  iphoneSE  10001  200
2020年3月5日  www.baizhiedu.com  /product/detail/10001.html  iphoneSE  10001  25
2020年3月3日  www.baizhiedu.com  /product/detail/10004.html  iphoneX  10004  100
2020年3月3日  www.baizhiedu.com  /product/detail/10002.html  xps15  10002  20
2020年3月6日  www.baizhiedu.com  /product/detail/10001.html  iphoneSE  10001  20
2020年3月3日  www.baizhiedu.com  /product/detail/10004.html  iphoneX  10004  100

# 期望结果
pid    访问次数
10001  7
10002  3
10003  2
10004  4

思路分析:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tt73sKFO-1631064667588)(MapReduce笔记.assets/image-20210228224209911.png)]

ReduceTask并行度

# 1. 概念
  Reduce阶段ReduceTask程序可以设置多个。(默认是1个)
# 2. 特点
  1:提高Reduce端的程序个数,并行执行,提高执行速度。
  2:每个ReduceTask程序,对应一个输出结果文件。
# 3. 编码 
  job.setNumberReduceTasks(reduce数量); //设置ReduceTask个数,实际设置分区的个数。
  导致Reduce阶段的数据进行了分区。 

ReduceTask提高并行度,会导致Mapper输出的key-value进行分区操作。(上图)

目的:

  1. 提升效率:提前MapTask阶段分区,并行分流,效率高。且,避免到Reduce端merge时候,多余的比较操作,提升效率。
  2. 防止数据倾斜:利用多个ReduceTask计算节点,平分汇总阶段的数据计算压力。(数据计算压力的负载均衡)
10.2.2 分区

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7W75PSCq-1631064667588)(MapReduce笔记.assets/image-20210228225316914.png)]

 # 默认规则
  对mapper输出的key,进行分区号的计算。
  partitioner.getPartition(k,v)--看上图
 # HashParitioner
   
 # 默认分区规则好不好:
   1. 启动多个ReduceTask并行度,提升效率。--- 优点
   2. 防止ReduceTask阶段的数据倾斜,数据计算压力均衡。--- 优点

默认分区代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rhvfLd8p-1631064667589)(MapReduce笔记.assets/image-20210228225500438.png)]

自定义分区

案例:学生成绩统计分析

将学生成绩,按照各个成绩降序排序,各个科目成绩单独输出。

数据:

# 自定义partition
将下面数据分区处理:
姓名  科目 成绩
张三  语文  10
李四  数学  30
王五  语文  20
赵6  英语  40
张三  数学  50
李四  语文  10
张三  英语  70
李四  英语  80
王五  英语  45
王五  数学  10
赵6  数学  10
赵6  语文  100

思路:

# 自定义分区
1. 编写自定义分区类,继承Partitioner覆盖getPartition方法
  注意:分区号从0开始算。(示例代码在下面)

2. 给job注册分区类
   job.setPartitionerClass(自定义Partitioner.class);
   
3. 设置ReduceTask个数(开启分区)
   job.setNumReduceTasks(数字);//reduceTask数量要和分区数量一样。
public class SubjectPartitioner extends Partitioner<DescDoubleWritable,SubjectStudent> {
    /**
     * 根据 subject 计算对应的分区好
     * 结论:
     *     语文  -- 0
     *     数学  -- 1
     *     英语  -- 2
     *     其他  -- 3
     * @param descDoubleWritable
     * @param subjectStudent
     * @param i
     * @return
     */
    @Override
    public int getPartition(DescDoubleWritable descDoubleWritable, SubjectStudent subjectStudent, int i) {
        String subject = subjectStudent.getSubject();
        int partitionNum=0;
        switch (subject){
            case "语文":
                partitionNum = 0;
                break;
            case "数学":
                partitionNum = 1;
                break;
            case "英语":
                partitionNum = 2;
                break;
            default:
                partitionNum = 3;
                break;
        }
        return partitionNum;
    }
}

11 MapReduce工作原理讲解

11.1 环形缓冲区和Spill溢写

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ryvs1Kln-1631064667590)(MapReduce笔记.assets/image-20210307112749667.png)]

流程分析:

  1. mapper输出的k-v结果会先临时存放到一个大小为100MB的圆形缓冲区(mapreduce.task.io.sort.mb)
  2. 一旦缓冲区写满80%(mapreduce.map.sort.spill.percent),触发溢写线程(第2个线程)工作。将80%缓冲区内的内容根据分区排序后,写出到临时文件中。(如果发生多次溢写,会有多个临时文件)
  3. MapTask工作结束后,将不同的分区临时文件的内容进行归并排序,产生分区后的有序文件

环形缓冲区:

  1. 临时存放mapper输出的k-v结果

  2. 运行缓冲区大小默认100MB(可以配置修改)

  3. Mapper输出k-v,经过分区,得到分区号,不断写入环形缓冲区

  4. 一旦写满80%,触发第2个线程(溢写线程)

    读取80%范围内的数据,按照分区进行排序,写出到本地临时文件。

    每个分区1个临时文件,文件中key是有序的

溢写:

概念:单独一个线程SpillThread,负责溢写过程

  1. 从圆形缓冲区中读取k-v(带分区号)
  2. 根据分区号,对k-v做分区内排序
  3. 将排序后的k-v写出到本地磁盘文件中,文件按照分区号保存

溢写触发的时机:

  1. 圆形缓冲区写满80%,触发一次溢写
  2. MapTask的mapper处理完数据,即使没有达到80%,也会触发最后一次溢写

11.2 MapReduce排序次数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g6fKYm6q-1631064667590)(MapReduce笔记.assets/image-20210307120124899.png)]

第一次: MapTask阶段环形缓冲区开始spill溢写,缓冲区每次溢写,发生一轮排序。
第二次: Maptask多次溢写产生的多个溢写文件(单个文件每部k有序),要做归并排序,maptask每个分区内,只保留1个文件(key有序),分区内溢写文件的归并排序。
第三次: ReduceTask 汇总多个MapTask的(对应分区)结果文件,归并排序(合并排序)

11.3 Shuffle

Shuffle: MapReduce整体中的一个部分过程,K-V数据从MapTask的Mapper.map方法中离开,一直到ReduceTask的Reducer.reduce方法接收kv这个中间过程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2q8liT5m-1631064667591)(MapReduce笔记.assets/image-20210307121444693.png)]

Shuffle

  1. MapTask阶段

    ① mapper.map输出kv

    ② 分区: getPartion(k,v)计算分区号

    ③ 写入环形缓冲区。

    ④ 一旦达到溢写条件,溢写k-v-分区号 分区排序 溢写文件产生

    ⑤ 多个溢写文件归并排序,合并一个文件

  2. ReduceTask阶段

    ① 下载:每个ReduceTask按照分区号,从所有MapTask本地下载对应分区号的文件。

② 归并排序

③ 按照key分组

④ 将k相同的v合并在一个集合中。

⑤ 将k-vs传入Reducer.reduce(k,vs)处理。

12 MapReduce工作流程汇总

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mczjCYEd-1631064667591)(MapReduce笔记.assets/image-20210307193429209.png)]

  1. 对输入的文件分片(split)
  2. 每一个分片交由一个MapTask来处理
  3. Mapper将结果保存到内存缓冲区中(默认100MB),当缓冲区的内容到达阈值(默认80%),将缓冲区中的内容溢写到磁盘
  4. 溢写时会先进行分区排序,如果定义了Combiner的话,中间也会执行Combine操作,最终生成分区有序的文件
  5. 在Reduce流程中,首先相同分区的数据进入到同一个reduce,在这个过程中伴随着排序、合并
  6. 一个分区对应一个Reducer,有多少分区就有多少ReduceTask,最终通过reduce产生分区结果

13 源码分析

源码的价值:

# 简历
  阅读过MyBatis部分源码
  阅读过Spring的部分源码
  阅读过Mapreducede部分源码 
  
# 技术学习
  通过阅读源码,验证我对技术流程的理解
  通过阅读源码,加强我对于技术流程的理解。

13.1 Split拆分

Split拆分源码–任务提交 TextInputFormat--FileInputForamt.getSplits(job)

/** 
   * 对文件生成逻辑上的切片Split,对应InputSplit,多个split对应List集合。
   * @param job the job context
   * @throws IOException
   */
  public List<InputSplit> getSplits(JobContext job) throws IOException{
  // 最小值  1,本质上就是【mapreduce.input.fileinputformat.split.minsize】
  long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
    // 最大值 LongMax, 本质上对应:【mapreduce.input.fileinputformat.split.maxsize】
    long maxSize = getMaxSplitSize(job);
      ...
    // 创建空的InputSplit的List,一会切一个,放里面放一个Split信息。
    List<InputSplit> splits = new ArrayList<InputSplit>();
     ...
    // 获得blockSize=128MB
    long blockSize = file.getBlockSize();//128MB
    // 获得splitSize=128MB【计算方式:minSize blockSize maxSize 在三者取其中,可以通过调节参数,修改split的大小】
    long splitSize = computeSplitSize(blockSize, minSize, maxSize);
    
    // 循环条件:如果剩余的字节大小 > splitSize的1.1倍。
      while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
          // block的序号
          int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
          // 构造一个split(文件路径 start length host 内存host)
          splits.add(makeSplit(path, length-bytesRemaining, splitSize,
                               blkLocations[blkIndex].getHosts(),
                               blkLocations[blkIndex].getCachedHosts()));
          // 切一刀,减去当前split的字节数。
          bytesRemaining -= splitSize;
      }
  }

13.2 MapTask局部计算

回顾自定义Mapper

// 一个split切片处理,创建一个Mapper对象
class 自定义Mapper extends Mapper{
  public void run(Context context) throws IOException, InterruptedException{
    //1: 调用setup 一次。:一般用来覆盖后,天加初始化资源操作。---调用1次。
      setup(context);
       // 循环读取 行数据 k(偏移量)-v(行)
          while (context.nextKeyValue()) {
            //2: 每读1行,调用1次map方法。
            map(context.getCurrentKey(), context.getCurrentValue(), context);
          }
          //3: 调用cleanup一次:一般覆盖,重写一些释放资源的代码----调用1次。
        cleanup(context);
  }
  
  // 数据处理map方法:被子类覆盖。
  protected void map(KEYIN key, VALUEIN value, 
                     Context context) throws IOException, InterruptedException {
      //自定义实现
                     }
}

MapTask调用Mapper的run方法作为调用map的入口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vq8C1ymQ-1631064667592)(MapReduce笔记.assets/image-20210805224510875.png)]

1.3.3 ReduceTask汇总计算

自定义Reducer回顾

class 自定义Reducer extends Reducer { 
    public void run(Context context) throws IOException, InterruptedException {
        //1. 调用setup方法。 1次。
        setup(context);
        try {
            while (context.nextKey()) {
                // 2:循环读取一组k-vs,调用reduce方法处理:循环调用。
                reduce(context.getCurrentKey(), context.getValues(), context);
                // If a back up store is used, reset it
                Iterator<VALUEIN> iter = context.getValues().iterator();
                if(iter instanceof ReduceContext.ValueIterator) {
                    ((ReduceContext.ValueIterator<VALUEIN>)iter).resetBackupStore();        
                }
            }
        } finally {
            // 3:调用cleanup方法。 reduce方法之后调用一次。
            cleanup(context);
        }
        
    }
    
    // 数据处理reduce方法:汇总操作。
 	protected void reduce(KEYIN key, Iterable<VALUEIN> values, Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
       //自定义实现
    }
}

ReduceTask调用Reducer的run方法作为调用reduce的入口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kn3PV1iH-1631064667592)(MapReduce笔记.assets/image-20210805225514661.png)]

14 经典案例:TopN

需求:获得主播观众人数前3名的信息。

数据:

# 原始数据
主播id 观众人数  时长
团团  2345  1000
小黑  67123  2000
哦吼  3456  7000
卢本伟  912345  6000


八戒  1234  5000
悟空  456  4000
唐僧  123345  3000


# 期望结果
卢本伟  912345
唐僧    123345
小黑    67123

常规思路:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S0FtD9Ep-1631064667593)(MapReduce笔记.assets/image-20210307213549711.png)]

优化思路

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eBafBqkt-1631064667593)(MapReduce笔记.assets/image-20210307213928931.png)]

15 Yarn分布式安装

集群规划:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kiP7Vb12-1631064667594)(MapReduce笔记.assets/image-20210805205835409.png)]

安装步骤:

  1. 环境准备

    # 保证HDFS分布式环境已经正确搭建(在之前的HDFS集群基础上搭建Yarn集群)
    
    要求:查看hadoop11:50070.
      在datanode标签页看到3个正常的datanode节点信息
      
    # 关闭HDFS集群
    stop-dfs.sh
    
  2. 初始化yarn的配置

    1. 配置mapred-site.xml

      <!-- mapreduce使用的资源调度器 -->
      <property>
          <name>mapreduce.framework.name</name>
          <value>yarn</value>
      </property>  
      <!-- 设置日志服务器的远程传输日志信息的端口和地址 -->
      <property>
          <name>mapreduce.jobhistory.address</name>
          <value>hadoop10:10020</value>
      </property>
      
      <!-- 设置日志服务器的web访问的地址和端口 -->
      <property>
          <name>mapreduce.jobhistory.webapp.address</name>
          <value>hadoop10:19888</value>
      </property>
      
    2. 配置yarn-site.xml

      <property>
          <name>yarn.nodemanager.aux-services</name>
          <value>mapreduce_shuffle</value>
      </property>
      <!--配置resourcemanager的主机ip-->
      <property>
          <name>yarn.resourcemanager.hostname</name>
          <value>hadoop11</value>
      </property>
      <!-- 开启日志聚合:将各个节点上的日志文件集中到HDFS中,便于管理 -->
      <property>
          <name>yarn.log-aggregation-enable</name>
          <value>true</value>
      </property>
      <!-- 设置日志保存时间 -->
      <property>
          <name>yarn.log-aggregation.retain-seconds</name>
          <value>106800</value>
      </property>
      
    3. slaves

      # 配置NodeManager的ip,同时也是HDFS的DataNode
      hadoop11
      hadoop12
      hadoop13
      
    4. 远程复制配置文件到其它节点

      [root@hadoop11 hadoop]# scp mapred-site.xml root@hadoop12:/opt/installs/hadoop/etc/hadoop/
      [root@hadoop11 hadoop]# scp mapred-site.xml root@hadoop13:/opt/installs/hadoop/etc/hadoop/
      [root@hadoop11 hadoop]# scp yarn-site.xml root@hadoop12:/opt/installs/hadoop/etc/hadoop/
      [root@hadoop11 hadoop]# scp yarn-site.xml root@hadoop13:/opt/installs/hadoop/etc/hadoop/
      [root@hadoop11 hadoop]# scp slaves root@hadoop12:/opt/installs/hadoop/etc/hadoop/
      [root@hadoop11 hadoop]# scp slaves root@hadoop13:/opt/installs/hadoop/etc/hadoop/
      
  3. 启动yarn集群

    # 先启动hdfs集群:在NameNode所在机器上执行
    start-dfs.sh
    # 再启动yarn集群:在ResourceManager所在机器上执行
    start-yarn.sh
    # 最后启动历史日志服务器
    mr-jobhistory-daemon.sh start historyserver
    
    
    说明如果要关闭,就执行之前讲过的关闭hdfs和yarn的命令
    
  4. 验证

    # 访问yarn的资源调度器web网页。
      http://主节点ResourceManager节点的ip:8088 
     
    

程传输日志信息的端口和地址 -->

mapreduce.jobhistory.address
hadoop10:10020

  <!-- 设置日志服务器的web访问的地址和端口 -->
  <property>
      <name>mapreduce.jobhistory.webapp.address</name>
      <value>hadoop10:19888</value>
  </property>
  ```
  1. 配置yarn-site.xml

    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
    </property>
    <!--配置resourcemanager的主机ip-->
    <property>
        <name>yarn.resourcemanager.hostname</name>
        <value>hadoop11</value>
    </property>
    <!-- 开启日志聚合:将各个节点上的日志文件集中到HDFS中,便于管理 -->
    <property>
        <name>yarn.log-aggregation-enable</name>
        <value>true</value>
    </property>
    <!-- 设置日志保存时间 -->
    <property>
        <name>yarn.log-aggregation.retain-seconds</name>
        <value>106800</value>
    </property>
    
  2. slaves

    # 配置NodeManager的ip,同时也是HDFS的DataNode
    hadoop11
    hadoop12
    hadoop13
    
  3. 远程复制配置文件到其它节点

    [root@hadoop11 hadoop]# scp mapred-site.xml root@hadoop12:/opt/installs/hadoop/etc/hadoop/
    [root@hadoop11 hadoop]# scp mapred-site.xml root@hadoop13:/opt/installs/hadoop/etc/hadoop/
    [root@hadoop11 hadoop]# scp yarn-site.xml root@hadoop12:/opt/installs/hadoop/etc/hadoop/
    [root@hadoop11 hadoop]# scp yarn-site.xml root@hadoop13:/opt/installs/hadoop/etc/hadoop/
    [root@hadoop11 hadoop]# scp slaves root@hadoop12:/opt/installs/hadoop/etc/hadoop/
    [root@hadoop11 hadoop]# scp slaves root@hadoop13:/opt/installs/hadoop/etc/hadoop/
    
  4. 启动yarn集群

    # 先启动hdfs集群:在NameNode所在机器上执行
    start-dfs.sh
    # 再启动yarn集群:在ResourceManager所在机器上执行
    start-yarn.sh
    # 最后启动历史日志服务器
    mr-jobhistory-daemon.sh start historyserver
    
    
    说明如果要关闭,就执行之前讲过的关闭hdfs和yarn的命令
    
  5. 验证

    # 访问yarn的资源调度器web网页。
      http://主节点ResourceManager节点的ip:8088 
     
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值