一、 MapReduce入门
1、 MapReduce定义
Mapreduce是一个分布式运算程序的编程框架,是用户开发“基于hadoop的数据分析应用”的核心框架。
Mapreduce核心功能是将开发者编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个hadoop集群上。
2、 MapReduce优缺点
-
优点
(1) MapReduce 易于编程
简单地实现一些接口或者继承一些框架准备好的类,重写一些方法,就就能够与框架进行有机结合,完成一个分布式程序,并部署到大量廉价的PC机器上运行。就是因为这个特点使得MapReduce编程变得非常流行。
(2) 良好的扩展性
当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力,而不需要重新编写代码。
(3) 高容错性
MapReduce设计的初衷就是使程序能够部署在廉价的PC机器上,而廉价的机器性能差,运行时很容易出现错误,这就要求它具有很高的容错性。比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败,而且这个过程不需要人工参与,而完全是由Hadoop内部完成的。
(4) 适合PB级以上海量数据的离线处理
它适合离线处理而不适合在线处理。比如像毫秒级别的返回一个结果,MapReduce很难做到。 -
缺点
MapReduce不擅长做实时计算、流式计算、DAG(有向图)计算。
(1) 实时计算
这个是从返回结果的实效上来说的,MapReduce无法像MySQL一样,在毫秒或者秒级内返回结果。
(2) 流式计算
这个是从它能处理的数据上来说的,流式计算的输入数据是动态的,像河流一样,源源不断,而MapReduce能够处理的输入数据集是静态的,不能动态变化。在设计时,就没打算处理动态的数据。
(3) DAG(有向无环图)计算
多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下,MapReduce并不是不能做,而是使用后,每个MapReduce作业的输出结果都会写入到hdfs,还要从hdfs中读出,在大数据的场景下,更会造成大量的磁盘IO,导致性能非常的低下。
3、 MapReduce核心思想
1)分布式的运算程序往往需要分成至少2个阶段。
2)第一个阶段的map task并发实例,完全并行运行,互不相干。
3)第二个阶段的reduce task并发实例互不相干,但是他们的数据依赖于上一个阶段的所有map task并发实例的输出。
4)MapReduce编程模型只能包含一个map阶段和一个reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个mapreduce程序,串行运行。
4、 MapReduce进程(MR)
1)MrAppMaster:负责整个程序的过程调度及状态协调。
2)MapTask:负责map阶段的整个数据处理流程。
3)ReduceTask:负责reduce阶段的整个数据处理流程。
5、 MapReduce编程规范(八股文)
用户编写的程序分成三个部分:Mapper(用于生成maptask)、Reducer(用于生成reduce task)和Driver(用于将mapper和reduce组装起来并向yarn提交任务)。
6、 WordCount案例实操
1.需求
在给定的文本文件中统计输出每一个单词出现的总次数,注意,这里我们先不把结果分别放到两个文件里面,先放到一个文件里面。
按照mapreduce编程规范,分别编写Mapper,Reducer,Driver,如图所示:
导入相应的依赖坐标+日志添加
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.7.2</version>
</dependency>
</dependencies>
推荐一个插件,在package时可以打成两个jar包,一个包为常规包。一个将依赖jar包都打入,适合在没配置运行环境下使用。
正常打包都为常规包。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<minimizeJar>true</minimizeJar>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
在项目的src/main/resources目录下,新建一个文件,命名为“log4j.properties”,在文件中填入:
log4j.rootLogger=debug, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
5.编写程序
(1)编写mapper类
package com.bigdata.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;
//数据在mapreduce中传递的时候,都是以<key,value>键值对的形式,传递
//KEYIN VALUEIN 是指输入数据key value的类型
// keyin是指输入文件每一行开始的偏移量,可以理解为行号 ,用Longwritable表示
// VALUEIN是指输入文件每一行的内容,即字符串 ,用Text表示
//KEYOUT VALUEOUT 是指输出数据 key vlaue的类型
//KEYOUT 是指单词的类型,用Text表示
//VALUEOUT 是指单词出现的次数,用IntWritable
public class WordcountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
@Override//框架帮我们读取数据,每读取一行,调用一次map方法
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1.转成字符串
String str = value.toString();
//2.切割字符串
String[] s = str.split(" ");
//3.遍历成键值对
for (String ss:s){
//4.输出键值对
Text k = new Text(ss);
IntWritable v = new IntWritable(1);
context.write(k,v);//写到磁盘
}
}
}
(2)编写reducer类
package com.bigdata.wordcount;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
//KEYIN,VALUEIN 是指输入数据kv的类型 和mapper的keyout,valueout是对应的
//这里的keyin对应mapper的keyout,valuein对应mapper的valueout
//KEYOUT,VALUEOUT 是指输出数据的kv的类型,具体是什么需要根据自己的业务逻辑决定
//根据具体的逻辑,我们要统计每个单词出现的总次数
//可以让单词作为keyout,因此其类型 用Text表示
//让单词出现的总次数作为valueout 因此其类型 用Intwritable表示
public class WordcountReduce extends Reducer<Text, IntWritable, Text, IntWritable> {
//框架会把相同key的放到一组,对这一组key调用一次reduce方法
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
//统计key的总次数
int sum=0;
for (IntWritable v:values){
sum+=v.get();//get方法转为int类型
}
IntWritable v = new IntWritable(sum);
//写出键值对
context.write(key,v);
}
}
(3)编写驱动类
package com.bigdata.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 WordcountDriver{
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//1 创建一个配置对象
Configuration conf = new Configuration();
//2 通过配置对象获取一个job对象
Job job = Job.getInstance(conf);
//3 设置job的jar包
job.setJarByClass(WordcountDriver.class);
//4 设置job的额mapper类,reduce类
job.setMapperClass(WordcountMapper.class);
job.setReducerClass(WordcountReduce.class);
//5 设置mapper的keyout和valueout
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//6 设置最终输出数据的keyout,valueout
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//7 设置输入数据的路径和输出数据的路径
FileInputFormat.setInputPaths(job,new Path(args[0]));
//输出目录不能事先存在,框架会自行创建
FileOutputFormat.setOutputPath(job,new Path(args[1]));
//8 向yarn或本地yarn模拟器提交任务
boolean b = job.waitForCompletion(true);
System.out.println("是否成功:"+b);
}
}
路径设置,输入路径可以精确到文件,output目录不能事先存在,hadoop会为我们创建
.本地测试,点击run
.集群上测试
(1)将程序打成jar包,然后拷贝到hadoop集群中
(2)启动hadoop集群
(3)执行wordcount程序(输入输出操作的都是hdfs目录,非虚拟机目录)
[root@hadoop003 ~]# hadoop jar mapreduce-1.0.jar com.bigdata.wordcount.WordcountDriver /idea/input /idea/output
hadoop序列化
1、 序列化概述
1) 什么是序列化
序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储(持久化)和网络传输。
反序列化就是将收到字节序列(或其他数据传输协议)或者是硬盘的持久化数据,转换成内存中的对象。
2) 为什么要序列化
一般来说,“活的”对象只生存在内存里,关机断电就没有了。而且“活的”对象只能由本地的进程使用,不能被发送到网络上的另外一台计算机。 然而序列化可以存储“活的”对象,可以将“活的”对象发送到远程计算机。
3) 为什么不用Java的序列化 serilazable
Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,header,继承体系等),不便于在网络中高效传输。所以,Hadoop自己开发了一套序列化机制(Writable),特点如下:
(1) 紧凑
紧凑的格式能让我们充分利用网络带宽,而带宽是数据中心最稀缺的资源。
(2) 快速
进程通信形成了分布式系统的骨架,所以需要尽量减少序列化和反序列化的性能开销,这是基本的。
(3) 可扩展
协议为了满足新的需求变化,所以控制客户端和服务器过程中,需要直接引进相应的协议,这些是新协议,原序列化方式能支持新的协议报文。
(4) 互操作
能支持不同语言写的客户端和服务端进行交互。
2、 常用数据序列化类型
常用的数据类型对应的hadoop数据序列化类型
3、 自定义bean对象实现序列化接口(Writable)
Hadoop给我们准备的常用类型已经具备hadoop的序列化标准了,可以进行序列化传输,而我们自定义bean对象要想序列化传输,必须实现序列化接口,需要注意以下几项:
(1)必须实现Writable接口
(2)反序列化时,需要反射调用空参构造函数,所以必须有空参构造
(3)重写序列化方法,(注意序列化的字段的顺序必须与反序列化的顺序一致)
(4)重写反序列化方法
(5)注意反序列化的顺序和序列化的顺序完全一致
(6)要想把结果显示在文件中,需要重写toString(),可用”\t”分开,方便后续用。
三、 MapReduce框架原理
1、 MapReduce工作流程
1.流程示意图,如图所示:
MapReduce整体工作流程
Map详细工作流程
Reduce详细工作流程
2.流程详解
上面的流程是整个MapReduce最全工作流程,具体如下:
(1) 执行Driver的main方法,里面有个job.waitForCompletion(),在该方法里面完成任务的准备,主要包括将输入数据切片并将切片规划写到job.split文件里面,生成运行任务时的配置文件job.xml,将我们写的mapreduce程序打成jar包。准备好了之后,将任务提交给yarn的resourcemanager。
(2) Resourcemanager收到客户端提交的任务后,会选一个nodemanager,在其上边生成一个mapreduce application master进程,由该进程负责指挥调度刚刚提交的任务。
(3) Master会分析我们的任务,查看任务的切片数量,然后启动相应的map task的数量,假定有两个切片,那么就会生成2个map task。
(4) 开始执行所有的map task,这些map task 并行运行,互相独立,会执行我们在重写的Mapper类的map方法,我们在map方法中写入自己的业务逻辑。Map方法有两个变量即key和value,这两个变量由框架(也就是LineRecordReader类)帮我们从输入数据读取进来封装到这两个变量中,我们只需要拿过来即可,读取一行,调用一次map方法。
(5) 在map方法里面对输入数据key和value进行自己的业务逻辑处理后,要把我们的结果也要封装好key value的形式写出去写出到outputCollector即环形缓冲区。
(6) key-value键值对从map中写出后,在进入到环形缓冲区之前,会经过分区器,得到一个所属分区的区号标记,意味着该key-value以后将被溢写到哪个分区。
(7) 环形缓冲区是个位于内存的数据结构,默认大小是100M,可以在hadoop的配置文件中通过io.sort.mb参数修改,当来到环形缓冲区的数据越来越多,达到容量80%的时候,这些环形缓冲区的kv对会被溢写到磁盘上,但是并不是把所有的kv溢写到一个文件,而是根据每个kv的所属分区,溢写到多个分区文件。如果在溢写后,环形缓冲区的数据又要满了,还会溢写多次。另外每一次溢写的时候,会读所有的kv根据mapper的keyout排序后溢出。这种溢写我们称之为区内有序,也就是溢写后的kv数据既要分区也要排序。
(8) 等所有的kv都溢写完毕后,会形成多个分区小文件,还要对这些分区小文件进行归并排序,所谓归并排序就是把多个有序的小文件变为一个有序的大文件。这个map task执行完成后,会形成多个分区文件,每个文件都是排好续的。
(9) Map task执行完毕后,接下来开始执行reduce task。因为map task形成了两个分区文件,所有需要两个reduce task,我们先说第一个reduce task,第二个流程跟第一个类似。Reduce task 1先把两个map task机器上的分区1数据下载下来,如果内存放不下的话,把数据放到磁盘上。
(10) 然后对下载下来的多个分区1的数据进行归并排序,排好序之后,调用分组比较器将所有kv按照k进行分组,然后将每组kv准备好去调用一次reduce方法。
3.注意
Combiner 预合并优化。
正常流程就是我们所说的上边10步,我们也可以对map task的输出进行优化。假定map task归并排序后的分区1文件中有100个键值对<a,1>,每个键值对占用1个字节存储空间,那么100个<a,1>就会占用100字节。reduce task在执行的时候,需要把这100个字节拷贝到reduce task的机器。
那么,怎么使用combiner进行优化呢,可以在缓冲区溢写到磁盘进行,以及归并排序的时候,让每个map task进行预合并,也就是 把100个<a,1> 变成<a,100>,这样的话,只需要用到1个kv对,只需要占用1字节存储空间,reduce task在拷贝的时候,能大大减少网络传输量。
但是要注意有了combiner还需要reduce task 进行合并吗?是需要的,因为combiner只是每个map task的预合并,是局部合并,还需要reduce task进行全局合并。
Shuffle中的缓冲区大小会影响到mapreduce程序的执行效率,原则上说,缓冲区越大,磁盘IO的次数越少,执行速度就越快。
缓冲区的大小可以通过参数调整,参数:io.sort.mb 默认100M。
2、 InputFormat数据输入
我们在指定driver类的main方法向yarn提交任务时,需要提前把输入数据进行切片,也就是说任务分一分,默认的切片类是FileInputFormat。
1) FileInputFormat切片机制
2) CombineTextInputFormat切片机制
关于大量小文件的优化策略:
默认情况下TextInputformat对任务的切片机制是按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个maptask,这样如果有大量小文件,就会产生大量的maptask,处理效率极其低下。
具体实现步骤
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class)
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
3、 MapTask工作机制
- maptask并行度决定机制
maptask的并行度决定map阶段的任务处理并发度,进而影响到整个job的处理速度。那么,mapTask并行任务是否越多越好呢?
一个job的map阶段MapTask并行度(个数),由客户端提交job时的切片个数决定,
例如,切两个切片,就会生成两个map task,切3个切片,就会生成3个map task。如图所示:
默认情况下,切片大小=块大小,效率最高,如果以上图100M切片,100-200M的内容需要跨机器传输,效率不高。
4、ReduceTask工作机制
Reduce task的并行度,也就是同时开启几个reduce task。我们知道切片的数量决定了map task的数量,那么什么决定了reduce task的数量呢?
第一种情况:如果我们自己定义了分区器,我们能够确定自己的分区器能够形成几个物理分区,假如要生成5个分区,那么我们要在driver中显式的设置启动与分区数量相等的reduce 的数量。
job.setNumReduceTasks(5);
第二种情况:reducetask默认值就是1,所以输出文件个数为一个。
5、 Shuffle机制
1) Shuffle机制(洗牌)
Mapreduce确保每个reducer的输入都是按key排序的。系统执行排序的过程(即将mapper输出作为输入传给reducer 的这个过程)称为shuffle。
2) Partition分区
问题引出:要求将统计结果按照条件输出到不同文件中(分区)。比如:将统计结果按照手机归属地不同省份输出到不同文件中(分区)。
分区:把map任务输出的kv数据分别放到不同的分区文件,相同分区的数据由一个reduce task来处理,从而达到reduce 并行度,把结果写到不同文件的目的。
(1) 默认partition分区方式
public class HashPartitioner<K, V> extends Partitioner<K, V> {
public int getPartition(K key, V value, int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
默认分区是根据key的hashCode对reduceTasks个数取模得到的,其目的是尽可能均匀的把数据分布到各个文件,让每个reduce task处理的数据大致相等。用户没法控制哪个key存储到哪个分区。
(2) 自定义Partitioner步骤
(1) 自定义类例如CustomPartitioner,继承Partitioner类,重写getPartition()方法。在该方法中实现分区的逻辑,即怎样控制从mapper输出的这些kv被溢写到不同的区文件。
(2) 在job驱动中,设置自定义partitioner:
job.setPartitionerClass(CustomPartitioner.class);
(3) 自定义partition后,要根据自定义partitioner的逻辑设置相应数量的reduce task
job.setNumReduceTasks(5);
(3) 注意
分区器被调用的时机:分区器在mapper把kv写出之后,进入到环形缓冲区之前被调用,相当于给每个kv打上了一个区号标记,该标记觉得了该kv被环形缓冲区溢写到磁盘上的位置,例如某kv是1区的,就会被溢写到1区文件,是2区的就被溢写到2区文件。
reduceTask的个数决定了有几个文件!!
如果reduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
如果1<reduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会Exception;
如果reduceTask的数量=1,则不管mapTask端输出多少个分区文件,最终结果都交给这一个reduceTask,最终也就只会产生一个结果文件 part-r-00000;
例如:假设自定义分区数为5,则:
a) job.setNumReduceTasks(1);会正常运行,只不过会产生一个输出文件
b) job.setNumReduceTasks(2);会报错
c) job.setNumReduceTasks(6);大于5,程序会正常运行,会产生空文件
3) WritableComparable排序
排序是MapReduce框架中最重要的操作之一。Map Task和Reduce Task均会对数据(按照key)进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。默认排序是按照字典顺序排序。
排序是MapReduce框架中最重要的操作之一。Map Task和Reduce Task均会对数据(按照key)进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。默认排序是按照字典顺序排序。
对于Map Task,它会将处理的结果暂时放到一个缓冲区中,当缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次排序,并将这些有序数据写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行一次合并,以将这些文件合并成一个大的有序文件。
对于Reduce Task,它从每个Map Task上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则放到磁盘上,否则放到内存中。如果磁盘上文件数目达到一定阈值,则进行一次合并以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据写到磁盘上。当所有数据拷贝完毕后,Reduce Task统一对内存和磁盘上的所有数据进行一次合并。
自定义排序WritableComparable
要想利用框架提供的排序机制,需要做两步:
第一步:把需要排序的数据放到mapper的keyout的位置,这样框架才会对我们所有的kv数据按照key进行排序。如果该数据是个自定义的bean对象,则需要进行第二步。
第二步:告知框架按照bean的哪个属性排序,按照升序还是按照降序。也就是让自定义的类实现WritableComparable接口重写compareTo方法,让该方法返回-1、1、或者0,就可以实现排序。
案例
有如下学生数据,两列分别为学生姓名和总成绩,请重新按照总成绩降序排序并输出
bean
package com.bigdata.sortscore1;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* @creat 2020-08-01-18:33
*/
public class ScoreBean implements WritableComparable<ScoreBean> {
private String name;
private int score;
public ScoreBean() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(name);
dataOutput.writeInt(score);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
name=dataInput.readUTF();
score=dataInput.readInt();
}
@Override
public String toString() {
return
"name='" + name + '\'' +
", score=" + score ;
}
@Override
public int compareTo(ScoreBean o) {
int res=0;
if(this.getScore()>o.getScore()){
res=-1;
}else if(this.getScore()<o.getScore()){
res=1;
}
return res;
}
}
mapper
package com.bigdata.sortscore1;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/**
* @creat 2020-07-23-17:44
*/
public class ScoreSortMapper1 extends Mapper<LongWritable, Text,ScoreBean, NullWritable> {
ScoreBean k=new ScoreBean();
@Override//框架每读取一行,调用一次map方法
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//刘一水 114,对总成绩排序
//1.转成字符串
String str = value.toString();
//2.切割字符串
String[] s = str.split("\t");
//输出姓名,成绩 【刘能 80】
k.setName(s[0]);
k.setScore(Integer.parseInt(s[1]));
context.write(k,NullWritable.get());
}
}
reduce
package com.bigdata.sortscore1;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* @creat 2020-07-23-17:44
*/
public class ScoreSortReduce1 extends Reducer<ScoreBean, NullWritable,ScoreBean,NullWritable> {
@Override
protected void reduce(ScoreBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
for (NullWritable v:values){
context.write(key,v);
}
}
}
driver
package com.bigdata.sortscore1;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
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;
/**
* @creat 2020-07-23-17:45
*/
public class ScoreSortDriver1 {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//创建一个配置对象
Configuration conf = new Configuration();
//通过配置对象获取一个job对象
Job job = Job.getInstance(conf);
//设置job的jar包
job.setJarByClass(ScoreSortDriver1.class);
//设置job的额mapper类,reduce类
job.setMapperClass(ScoreSortMapper1.class);
job.setReducerClass(ScoreSortReduce1.class);
//设置mapper的keyout和valueout
job.setMapOutputKeyClass(ScoreBean.class);
job.setMapOutputValueClass(NullWritable.class);
//设置最终输出数据的keyout,valueout
job.setOutputKeyClass(ScoreBean.class);
job.setOutputValueClass(NullWritable.class);
//设置输入数据的路径和输出数据的路径
FileInputFormat.setInputPaths(job,new Path(args[0]));
//输出目录不能事先存在,框架会自行创建
FileOutputFormat.setOutputPath(job,new Path(args[1]));
//8 向yarn或本地yarn模拟器提交任务
boolean b = job.waitForCompletion(true);
System.out.println("是否成功:"+b);
}
}
4) Combiner合并
(1) combiner是MR程序中Mapper和Reducer之外的一种组件。
(2) combiner组件的父类就是Reducer。
(3) combiner和reducer的区别在于运行的位置:
Combiner是在每一个maptask所在的节点运行,用于局部合并。
Reducer是接收全局所有Mapper的输出结果,用于全局合并。
(4) combiner的意义就是对每一个maptask的输出进行局部汇总,以减小网络传输量。
(5) 自定义Combiner实现步骤:
a) 自定义一个combiner继承Reducer,重写reduce方法,描述预合并的逻辑。
b) 在job驱动类中设置:
job.setCombinerClass(XXX.class);
案例,统计每个单词出现的次数
方案一
a) 增加一个WordcountCombiner类继承Reducer。
package com.bigdata.wordcount;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class WordcountCombiner 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 = sum + value.get();
}
context.write(key,new IntWritable(sum));
}
}
b) 在WordcountDriver驱动类中指定combiner。
job.setCombinerClass(WordcountCombiner.class);
方案二
将WordcountReducer作为combiner在WordcountDriver驱动类中指定。
// 指定需要使用combiner,以及用哪个类作为combiner的逻辑
job.setCombinerClass(WordcountReducer.class);
5) GroupingComparator分组(辅助排序)
对reduce阶段的数据根据某一个或几个字段进行分组。
之前一直在说reduce方法是分组调用,只要一堆kv中的k相同,则这些kv被分到一组,这一组调用一次reduce方法,那么怎么认定key相同,我需要给框架一个标准,怎么给它呢?就是通过自定义一个分组比较器,重写compare方法,告知框架何为key相同。
分析
a) 利用“用户id、月份、电费”的bean作为key,可以将map阶段读取到的所有电费数据按照userid升序、电费降序排序,发送到reduce。
b) 在reduce端利用groupingcomparator将userid相同的kv聚合成组,然后取第一个即是最大值,如图所示
代码实现
a) 定义PowerBean
package com.bigdata.power;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class PowerBean implements WritableComparable<PowerBean>{
private int userid;
private String month;
private double fee;
public PowerBean() {
}
public int getUserid() {
return userid;
}
public void setUserid(int userid) {
this.userid = userid;
}
public String getMonth() {
return month;
}
public void setMonth(String month) {
this.month = month;
}
public double getFee() {
return fee;
}
public void setFee(double fee) {
this.fee = fee;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeInt(userid);
out.writeUTF(month);
out.writeDouble(fee);
}
@Override
public void readFields(DataInput in) throws IOException {
this.userid = in.readInt();
this.month = in.readUTF();
this.fee = in.readDouble();
}
@Override//先按照userid升序排序,如果userid相同,再按照电费降序排序
public int compareTo(PowerBean o) {
int res1 = 0;
if(this.getUserid() > o.getUserid()){
res1 = 1;
}else if(this.getUserid() < o.getUserid()){
res1 = -1;
} else {
int res2 = 0;
if(this.getFee() > o.getFee()){
res2 = -1;
}else if(this.getFee() < o.getFee()){
res2 = 1;
}
return res2;
}
return res1;
}
@Override
public String toString() {
return this.userid+"\t"+this.month+"\t"+this.fee;
}
}
b) 编写Mapper
package com.bigdata.power;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class PowerMapper extends Mapper<LongWritable,Text,PowerBean,NullWritable>{
PowerBean k = new PowerBean();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//5686621 201906 132.6
//1,读取每行数据,将每行数据转换为字符串
String line = value.toString();
//2 挑出所需数据,封装为kv,并写出
String[] split = line.split("\t");
k.setUserid(Integer.parseInt(split[0]));
k.setMonth(split[1]);
k.setFee(Double.parseDouble(split[2]));
// 3 将kv写出
context.write(k,NullWritable.get());
}
}
c) 编写GroupingComparator
package com.bigdata.power;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
//分组比较器的作用就是给出一个标准,该标准用于认定一堆kv的k是否相同,
//在本案例中,我们想要userid相同的数据进到一个组,因此 我们给出的标准就是,只要userid相同,就认为是key相同,这些kv就是一组
public class PowerGroupComparator extends WritableComparator{
public PowerGroupComparator(){
//让框架创建对象
super(PowerBean.class,true);
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
PowerBean aa = (PowerBean) a;
PowerBean bb = (PowerBean) b;
int res = 0;
if(aa.getUserid() > bb.getUserid()){
res = 1;
}else if (aa.getUserid() < bb.getUserid()){
res = -1;
}
System.out.println(aa.getUserid()+"-"+aa.getMonth()+"-"+aa.getFee()+"\t"+bb.getUserid()+"-"+bb.getMonth()+"-"+bb.getFee());
return res;
}
}
d) 编写Reducer
package com.bigdata.power;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class PowerReduce extends Reducer<PowerBean,NullWritable,PowerBean,NullWritable>{
@Override
protected void reduce(PowerBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
//因为进来的每组数据都是按照电费降序排好的,因此只需要写出第一条数据即可。
//<5686621 201912 144.6,null>
// <5686621 201906 132.6,null>
//<5686621 201907 22.5 ,null>
context.write(key,NullWritable.get());
}
}
e) 编写Driver
package com.bigdata.power;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class PowerDriver {
public static void main(String[] args) throws Exception {
// 1 创建一个配置对象
Configuration conf = new Configuration();
// 2 通过配置对象获取一个job对象
Job job = Job.getInstance(conf);
// 3 设置job的jar包
job.setJarByClass(PowerDriver.class);
// 4 设置job的mapper类,reduce类
job.setMapperClass(PowerMapper.class);
job.setReducerClass(PowerReduce.class);
// 5 设置mapper的keyout和valueout
job.setMapOutputKeyClass(PowerBean.class);
job.setMapOutputValueClass(NullWritable.class);
// 6 设置最终输出数据的keyout和valueout
job.setOutputKeyClass(PowerBean.class);
job.setOutputValueClass(NullWritable.class);
// 7 设置输入数据的路径和输出数据的路径
FileInputFormat.setInputPaths(job,new Path(args[0]));
//注意输出目录不能事先存在,必须设置一个不存在的目录,框架会自行创建,否则就会报错
FileOutputFormat.setOutputPath(job,new Path(args[1]));
job.setGroupingComparatorClass(PowerGroupComparator.class);
// 8 向yarn或者本地yarn模拟器提交任务
boolean res = job.waitForCompletion(true);
System.out.println("是否成功:"+res);
}
}