五、MapReduce进阶编程
目录:
1.筛选日志文件并生成序列化文件
2.Hadoop Java API读取序列化日志文件
3.优化日志文件统计程序
4.Eclipse提交日志文件统计程序
5.小结
6.实训
7.小练习
任务背景:网站运营方又提出来新的需求,为了比较今年与去年同期的用户访问数据,要求分别统计出2016年1月与2月的用户访问次数,并输出到不同的目录中。在本章中,将引入一些高级的编程技巧,使得整体编程更加高效实用。
第一个任务:在大数据文件分析处理中,尤其是在处理逻辑比较复杂的情况下,要使用多个MapReduce程序来连续进行处理,就需要在HDFS上保存大量的中间结果。如何提高中间结果的存取效率,对于整个数据处理流程是很有意义的。Hadoop序列化具有紧凑、快速、可扩展以及互操作的特点,非常适合MapReduce任务的输入与输出格式。首要任务就是从原数据中筛选出1月与2月的数据,以序列化文件的格式存储,为后续的数据处理任务做准备。
第二个任务:简要了解javaAPI的基本操作和应用。通过JavaAPI对HDFS中的文件进行操作,它不但能够轻松处理各类常规的文件操作,而且还提供了多种文件类型接口,能够轻松处理文本、键值对、序列化等多种文件格式。
第三个任务:在实际任务中,数据结构与逻辑更为复杂,键或值可能是由多个元素组成的,那么就需要用户根据情况自定义键值对的类型。Map端的输出结果是经过网络传输到Reduce端的。当Map端的输出数据量特别大时,网络传输可能成为影响处理效率的一大因素。为了提高整体处理效率,Hadoop提供了用于优化组件Combiner与Partitioner,可以帮助数据在传到Reducer之前进行一系列的合并和分区处理。另外,Hadoop提供了执行MapReduce程序过程中的计数功能,用户也可以根据需要进行个性化的计数设置。现在要编程实现2016年1月与2月的用户访问次数统计,并在编程过程中使用自定义的键值对类型、组件Combiner与Partitioner、自定义计数器等模块,有利于对Hadoop编程有更加深刻的认识。
第四个任务:学会MapReduce任务在工作环境中的实际提交流程,就是在Eclipse中直接向Hadoop提交MapReduce任务,而且显示执行过程中的输出日志。
1.筛选日志文件并生成序列化文件
任务:以序列化文件的格式输出筛选的数据。
1.1 MapReduce输入格式
(1)Hadoop自带了多个输入格式,其中一个抽象类为FileInputFormat,所有操作文件的InputFormat类都是从它那里继承方法和属性。当启动hadoop时,FileInputFormat会得到一个路径参数,这个路径包含了所需要处理的文件,FileInputFormat会读取这个文件夹内的所有文件,然后会把这些文件拆分为一个或多个InputSplit。下图为InputFormat的类继承结构:
其中TextInputFormat是默认的inputformat。
(2)Hadoop的MapReduce不仅可以处理文本信息,还可以处理二进制格式的数据,二进制格式也成为序列化格式,Hadoop的序列化有以下特点:
①紧凑:高效使用存储空间 ②快速:读取数据的额外开销少
③可扩展:可透明的读取旧格式的数据 ④互操作:可以使用不同的语言读/写永久存储的数据
处理序列化数据需要使用SequenceFileInputFormat来作为MapReduce的输入格式。
(3)常用的inputformat的输入格式:
输入格式 |
描述 |
键类型 |
值类型 |
TextInputFormat |
默认格式,读取文件的行 |
行的字节偏移量(LongWriable) |
行的内容(Text) |
SequenceFileInputFormat |
Hadoop定义的高性能二进制格式 |
用户自定义 |
|
KeyValueInputFormat |
把行解析为键值对 |
第一个tab字符前的所有字符 |
行剩下的所有内容(Text) |
设置MapReduce输入格式,可在驱动类中使用Job对象的setInputFormat()方法。例如,我们要读取社交网站2016年用户登录的信息,要设定输入对象为TextInputFormat,可以在驱动类中设置以下代码:
job.setInputFormat(TextInputFormat.class)
由于TextInputFormat是默认的输入格式,所以当输入格式是TextInputFormat时,驱动类可以不设置输入格式。但是要使用其他的输入格式,就要在驱动类中设置输入格式。
1.2 MapReduce输出格式
(1)针对上一小节介绍的输入格式,Hadoop都有对应的输出格式。输出格式实际上是输入格式的逆过程,即把键值对写入HDFS的文件块内。下图为OutputFormat类的继承构造:
默认的输出格式是TextOutputFormat,它把每一条记录写为文本行。它的键和值可以是任意类型,因为TextOutputFormat调用toString()方法把他们转换为字符串。每个键值对由制表符进行分割,当然也可以通过设定mapreduce.output.textoutputformat.separator属性来改变默认的分隔符。
(2)下表列出了常用的输出格式类:
输出格式 |
描述 |
TextOutputFormat |
默认的输出格式,以“key value”的方式输出行 |
SequenceFileOutputFormat |
输出二进制文件,适合作为子MapReduce作业的输入 |
NullOutputFormat |
忽略收到的数据,即不做输出 |
如果作为后续MapReduce任务的输入,那么序列化输入是一种好的输入格式,因为它的格式紧凑,很容易被压缩。本节任务需要将筛选出来的数据以序列化的格式输出,只需要在驱动类中添加以下代码:
job.setOutputFormat(SequenceFileOutputFormat.class)
1.3 任务实现(完成前述第一个任务)
(1)实现步骤:
①以文本格式读取文件 ②在map函数里判断读取进来的数据是否是1月或2月的数据。若是,则将该条数据输出;若不是,则不输出
③以序列化的格式输出数据。本人无只需要Mapper类就可以完成,即Map端的输出可以直接输出到HDFS,因此本任务不必设置Reducer类,即在驱动类中设置Reducer的个数为0。
(2)①代码:
package test;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;;
public class SelectData {
public static class SelectDataMapper extends Mapper<LongWritable,Text,Text,Text>{
protected void map(LongWritable key,Text value,Mapper<LongWritable,Text,Text,Text>.Context context)
throws IOException,InterruptedException{
String[] val=value.toString().split(",");
//过滤选取1月份和2月份的数据
if(val[1].contains("2016-01") || val[1].contains("2016-02")) {
context.write(new Text(val[0]), new Text(val[1]));
}
}
}
public static void main(String[]args) throws IOException,
ClassNotFoundException,InterruptedException{
Configuration conf=new Configuration();
Job job=Job.getInstance(conf,"selectdata");
job.setJarByClass(SelectData.class);
job.setMapperClass(SelectDataMapper.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
job.setInputFormatClass(TextInputFormat.class);//设置输入格式
job.setOutputFormatClass(SequenceFileOutputFormat.class);//设置输出格式
job.setNumReduceTasks(0);//设置Reducer的任务数为0
FileInputFormat.addInputPath(job, new Path(args[0]));
FileSystem.get(conf).delete(new Path(args[1]),true);
FileOutputFormat.setOutputPath(job, new Path(args[1]));
System.err.println(job.waitForCompletion(true)?-1:1);
}
}
②执行并查看结果:
hadoop jar selectdata.jar test.SelectData /user/dftest/user_login.txt /user/dftest/Selectdata
③记事本打开是(记事本对数据进行了解析):
由 sublime打开是二进制文件:
序列化输出完成。
2.Hadoop Java API读取序列化日志文件
本节将使用Hadoop Java API的方式读取该序列化文件,并将读取的数据保存到本地文件系统中,查看内容是否为1月和2月的用户登录信息。
2.1 FileSystem API 管理文件
(1)FileSystem是一个通用的文件管理系统API,使用它的第一步是需要先获取它的一个实例,下面给出了几个获取FileSystem实例的静态方法:
public static FileSystem get(Configuration conf) throws IOException
public static FileSystem get(URI uri,Configuration conf) throws IOException
public static FileSystem get(URI uri,Configuration conf,String user) throws IOException
(2)Configuration 对象封装了客户端或服务器端的配置信息,下面说明以上三个方法:
①第一个方法返回了一个默认的文件系统,是在core-site.xml中通过fs.defaultFS来指定的,如果在core-site.xml中没有设置,则返回本地的文件系统。
②第二个方法是通过uri来指定要返回的文件系统。如果uri是以hdfs标识开头,那么久返回一个HDFS文件系统;如果uri中没有相应的标识,则返回本地文件系统。
③第三个方法返回文件系统的原理与②相同,但它同时又限定了该文件系统的用户,在这方面是很重要的。
通过查看FileSystem的API可以找到FileSystem类的相关方法。
(3)举例:
修饰符和类型 |
方法 |
abstract FileStatus[] |
listStatus(Path f) |
FileStatus[] |
listStatus(Path[] files) |
FileStatus[] |
listStatus(Path[] files,PathFilter filter) |
FileStatus[] |
listStatus(Path f,PathFilter filter) |
以上方法返回的是一个文件列表。
①列举文件夹示例代码:
//获取配置
Configuration conf = new Configuration();
conf.set("fs.defaultFS","172.16.29.76:8020");
//获取文件系统
FileSystem fs = FileSystem.get(conf);
//指定要查看的文件目录
Path path = new Path("/user/dftest");
//获取文件列表
FileStatus[] filesatus = fs.listStatus(path);
//遍历文件列表
for (FileStatus file: filestatus) {
//判断是否为文件夹
if( file.isDirectory() ){
System.out.printlin( file.getPath().toString() );
}
}
//关闭文件系统
fs.close();
首先要设置Configuration来获取集群配置,然后指定集群内的hdfs文件目录
②与列举目录下的文件夹方式类似,可以使用同样的方法去遍历一个文件夹下面的所有文件。代码如下:
//获取配置
Configuration conf = new Configuration();
conf.set("fs.defaultFS","172.16.29.76:8020");
//获取文件系统
FileSystem fs = FileSystem.get(conf);
//指定要查看的文件目录
Path path = new Path("/user/dftest");
//获取文件列表
FileStatus[] filesatus = fs.listStatus(path);
//遍历文件列表
for (FileStatus file: filestatus) {
//判断是否为文件夹
if( file.isFile() ){
System.out.printlin( file.getPath().toString() );
}
}
//关闭文件系统
fs.close();
(4)用FileSystem API 创建目录
修饰符和类型 |
方法 |
static boolean |
mkdirs(FileSystem fs,Path dir,FsPermission permission) |
boolean |
mkdirs(Path f) |
abstract boolean |
mkdirs(Path f,FsPermission permission) |
相关参数:fs:文件系统对象 dir:要创建的目录名称 permission:为该目录设置的权限
任务:使用mkdirs(Path f)方法在HDFS上创建目录/user/dftest/loginmessage
代码示例如下:
//获取配置
Configuration conf = new Configuration();
conf.set("fs.defaultFS","172.16.29.76:8020");
//获取文件系统
FileSystem fs = FileSystem.get(conf);
//声明要创建的文件目录
Path path = new Path("/user/dftest/loginmessage");
//调用mkdirs函数创建目录
fs.mkdirs(path);
//关闭文件系统
fs.close();
2.2 FileSystem API 操作文件
(1)删除文件:
这里主要介绍delete方法
修饰符和类型 |
方法 |
boolean |
delete(Path f) |
abstract boolean |
delete(Path f,boolean recursive) |
相关参数:f 要删除的文件路径 recursive 如果路径是一个目录且不为空,要把recursive设置为true,否则会报出异常。在如果是文件的话,此值true或false均可。
使用delete(Path f,boolean recursive)删除HDFS上的/user/dftest/user_login.txt文件,具体实现代码如下:
//获取配置
Configuration conf = new Configuration();
conf.set("fs.defaultFS","172.16.29.76:8020");
//获取文件系统
FileSystem fs = FileSystem.get(conf);
//声明要删除的文件或目录
Path path = new Path("/user/dftest/user_login.txt");
//调用mkdirs函数创建目录
fs.delete(path,true);
//关闭文件系统
fs.close();
(2)上传与下载文件:(示例代码用的方法是蓝色标注的方法,结构与上面的代码类似,只是注明参数即可)
①本地上传文件:
修饰符和类型 |
方法 |
void |
copyFromLocalFile(boolean delSrc,boolean overwrite,Path[] srcs,Path dst) |
void |
copyFromLocalFile(boolean delSrc,boolean overwrite,Path srcs,Path dst) |
void |
copyFromLocalFile(boolean delSrc,Path srcs,Path dst) |
void |
copyFromLocalFile(Path srcs,Path dst) |
②下载到本地:
修饰符和类型 |
方法 |
void |
copyToLocalFile(boolean delSrc,Path src,Path dst) |
void |
copyToLocalFile(boolean delSrc,Path src,Path dst,boolean useRawLocalFileSystem) |
void |
copyToLocalFile(Path src,Path dst) |
相关参数 :delSrc:是否删除源文件 overwrite:是否覆盖已经存在的文件 srcs:存储源文件目录的数组
dst:目标路径 src:源文件路径 useRawLocalFileSystem:是否使用原始文件系统作为本地文件系统
2.3 FileSystem API 读写数据
(1)查看文件内容
Hadoop Java API 提供了一个获取指定文件的数据字节流的方法——open()。该方法返回的是FSDataInputStream对象。
修饰符和类型 |
方法 |
FSDataInputStream |
open(Path f) |
abstract FSDataInputStream |
open(Path f,int bufferSize) |
相关参数:f :要打开的文件 bufferSize:要使用的缓冲区大小
示例:读取HDFS上的文件/user/dftest/loginmessage/user_login.txt的内容,具体实现代码如下:
//获取配置
Configuration conf = new Configuration();
//获取文件系统
FileSystem fs = FileSystem.get(conf);
//声明要查看的文件路径
Path path = new Path("/user/dftest/loginmessage/user_login.txt");
//获取指定文件的数据字节流
FSDataInputStream is = fs.open(path);
//读取文件内容并打印出来
BufferedReader br=new BufferedReader(new InputStreamReader(is,"utf-8"));
String line="";
while((line=br.readline())!=null){
System.out.println(line);
}
//关闭数据字节流
br.close();
is.close();
//关闭文件系统
fs.close();
(2)写入数据
与读取数据类似,写入数据的实现可以理解为读取数据的逆向过程。向HDFS写入数据首先需要创建一个文件,FileSystem类提供了多种方法来创建文件。最常用的就是调用以创建文件的Path对象为参数的create(Path f)方法。除了创建一个新文件来写入数据,还可以用append()方法向一个已存在的文件添加数据。
示例:在/user/dftest/loginmessage/user_login.txt目录下创建文件new_user_login.txt,读取该目录下的user_login.txt文件并写入新文件new_user_login.txt中,具体实现代码如下:
//获取配置
Configuration conf = new Configuration();
//获取文件系统
FileSystem fs = FileSystem.get(conf);
//声明要查看的文件路径
Path path = new Path("/user/dftest/loginmessage/user_login.txt");
//创建新文件
Path newpath=new Path("/user/root/loginmessage/new_user_login.txt");
fs.delet(newpath);
FSDataOutputStream os=fs.create(newPath);
//获取指定文件的数据字节流
FSDataInputStream is = fs.open(path);
//读取文件内容并写入到新文件
BufferedReader br=new BufferedReader(new InputStreamReader(is,"utf-8"));
BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(os,"utf-8"));
String line="";
while((line=br.readline())!=null){
bw.write(line);
bw.newLine();
}
//关闭数据字节流
bw.close();
os.close();
br.close();
is.close();
//关闭文件系统
fs.close();
2.4 任务实现(完成前述第二个任务)
(1) Hadoop Java API提供了读取HDFS上的文件的方法,当然也可以读取序列化文件。不同于普通问件的读取方法,读取序列化文件需要获取到SequenceFile.Reader对象。下表给出了该对象提供的几个重要方法。
方法 |
描述 |
getKeyClassName() |
返回序列化文件中的键类型 |
getValueClassName() |
返回序列化文件中的值类型 |
next(Writable key) |
读取该文件中的键,如果存在下一个键,则返回true,读到文件末尾则返回false |
next(Writable key,Writable value) |
读取文件中的键和值,如果存在下一个键值,则返回true,读到文件末尾返回false |
toString() |
返回文件的路径名称 |
(2) 读取序列化文件
main函数部分代码如下:
//获取配置
Configuration conf = new Configuration();
//获取文件系统
FileSystem fs = FileSystem.get(conf);
//获取SequenceFile.Reader对象
SequenceFile.Reader reader=new SequenceFile.Reader(fs,new Path("/user/dftest/Selectdata/part-m-00000"),conf);
//获取序列化文件中使用的键值类型
Text key = new Text();
Text value=new Text();
//读取的数据写入selectdata.txt文件
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("C:\Users\Admin\desktop\selectdata.txt",true)));
while(reader.next(key,value)){
out.write(key.toString()+" "+value.toString()+"
");
}
out.close();
reader.close();
读取结果如下(截取的部分数据):
3.优化日志文件统计程序
任务1中筛选了1.2月份的用户登录信息,并生成了序列化文件,本节任务就是使用MapReduce读取该序列化文件,统计在2016年1.2月份每天的登录次数,并且要求最终的输出结果根据月份分别保存到两个不同的文件中。同时要求分别统计输入记录中1月份和2月份的记录数以及输出结果中1月份和2月份的记录数。
3.1 自定义键值类型
在Hadoop中,mapper和reducer处理的都是键值对记录。Hadoop提供了很多键值对类型,如Text、IntWritable、LongWritable等。
下图为Hadoop内置的数据类型:
下面对常用的集中数据类型进行详细解析:
类型 |
解释 |
BooleanWritable |
标准布尔型,相当于java数据里面的boolean,当<key,value>中key或者value为布尔型时使用 |
ByteWritable |
单字节型,相当于Java数据类型里面的byte,当<key,value>中的key或者value为单字节类型时使用 |
DoubleWritable |
双精度浮点型,相当于Java数据类型里面的double,当<key,value>中的key或者value为double类型时使用 |
FloatWritable |
单精度浮点型,相当于Java数据类型里面的float,当<key,value>中的key或者value为单精度浮点类型时使用 |
IntWritable |
整型,相当于Java数据类型里面的int,当<key,value>中的key或者value为整型时使用 |
LongWritable |
长整型,相当于Java数据类型里面的Long,当<key,value>中的key或者value为长整型时使用 |
Text |
使用UTF-8格式存储文本,在java数据中主要针对String类型 |
NullWritable |
空值,当<key,value>中的key或value为空时使用 |
Hadoop内置的数据类型可以满足绝大多数需求。但有时,用户需要一些特殊的键值类型来满足业务需求,即自定义键值类型。
①自定义值类型必须实现Writable接口,接口Writable是一个简单高效的基于基本I/O的序列化接口对象,包含两个方法,即write(DataOutput out)与readFields(DataInput in),其功能分别是将数据写入指定的流中和从指定的流中读取数据。下表为对这两个方法的描述。自定义值类型必须实现这两个方法。
返回类型 |
方向和描述 |
void |
readFields(DataInput in),从in中反序列化该对象的字段 |
void |
write(DataOuput out),将该对象的字段序列化到out中 |
②自定义键类型必须实现WritableComparable接口。WritableComparable接口自身又实现了Wriable接口,所以Hadoop中的键也可以作为值使用,但是实现Writable接口不能作为键使用。WritableComparable接口中不仅有readFields(DataInput in)方法和write(DataOutput out)方法,还提供了一个compareTo(To)方法,该方法提供了3种返回类型,如下表:
返回类型 |
解释 |
负整数 |
表示小于被比较对象 |
0 |
表示等于被比较对象 |
正整数 |
表示大于被比较对象 |
无论是自定义键类型还是自定义值类型,自定义的类默认继承object类,而object类默认有一个toString方法,该方法返回的是对象的内存地址。但有时候用户想要看到的是该对象的具体内容,而不是内存地址,这个时候就需要重写toString方法。重写方法只需要返回自己想要的字符串即可。
③下面具体介绍如何完整地自定义一个键值类型:
本节任务是统计用户每天登录的次数,输入的数据包含两列信息,用户名和登录日期。Mapper输出的键是用户名及登录日期,输出的值是1;Reducer输出的键也是登录名和日期,输出的值是每个用户每天登录的次数。由此可以看出,Mapper和Reducer输出的值类型使用Hadoop内置的IntWritable类型即可,而键类型可以自己定义。定义一个MemberLogTime类,该类实现接口WritableComparable<MemberLogTime>,类中声明了两个对象,分别为用户名member_name和登录时间logTime。同时,该类实现了readFields(DataInput in)方法、Write(DataInput out)方法和compareTo(MemberLogTime o)。其中,compareTo(MemberLogTime o)方法是根据用户名进行排序的。最后还要重写toString方法,该方法返回用户名和登录时间的字符串格式。
自定义键类型代码如下所示:
package essential;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class MemberLogTime implements WritableComparable<MemberLogTime> {
private String member_name;
private String logTime;
public MemberLogTime(){
}
public MemberLogTime(String member_name,String logTime){
this.member_name=member_name;
this.logTime=logTime;
}
public String getLogTime() {
return logTime;
}
public void setLogTime(String logTime) {
this.logTime = logTime;
}
public String getMember_name() {
return member_name;
}
public void setMember_name(String member_name) {
this.member_name = member_name;
}
/*关键是对下面三个方法进行重写实现
*
* */
@Override
public void readFields(DataInput dataInput) throws IOException {
this.member_name=dataInput.readUTF();
this.logTime=dataInput.readUTF();
}
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(member_name);
dataOutput.writeUTF(logTime);
}
@Override
public int compareTo(MemberLogTime o) {
return this.getMember_name().compareTo(o.getMember_name());
}
public String toString(){
return this.member_name+","+this.logTime;
}
}
3.2 初步探索Combiner
为了提高MapReduce作业的工作效率,Hadoop允许用户声明一个Combiner。Combiner是运行在Map端的一个“迷你Reducer”过程,它只处理单台机器生成的数据。
(1)声明的Combiner继承的是Reducer,其方法原理和Reduce的实现原理基本相同。不同的是,Combiner操作发生在Map端,或者说Combiner运行在每一个运行Map任务的节点上。它会接收特定节点上的Map输出作为输入,对Map输入的数据先做一次合并,再把输出结果发送到Reducer。需要注意的是,combiner不影响程序的处理逻辑,只会影响处理效率。
所有节点的Mapper输出都会传送到Reducer,当数据集很大时,Reduce端也会接收大量的数据,这样无疑会增加Reducer的负担,影响Reducer的工作效率。当加入Combiner时,每个节点的Mapper输出现在Combiner上进行整合,Combiner先对Mapper输出进行计算,然后将计算结果传输给Reducer,这样Reducer端接收到的数据量就会大大减少,提高效率。
下图为有无Combiner的对比图:
值得一提的是:并非所有的MapReduce程序都可以加入Combiner。仅当Reducer输入的键值对类型与Reducer输出的键值对类型一样,并且计算逻辑不影响最终的结果时才可以在MapReduce程序中加入Combiner。例如,统计求和或者求最大值时可以使用Combiner,但是类似计算平均值时就不能使用Combiner。
(2)Combiner继承的是Reducer,所以声明Combiner类的继承必须继承Reducer,在Combiner类里面重写reduce方法。下面代码展示了统计社交网站2016年1月和2月用户每天登录该网站次数的Combiner代码。
package essential;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class LogCountCombiner extends Reducer <MemberLogTime, IntWritable,MemberLogTime,IntWritable>{
protected void reduce(MemberLogTime key,Iterable<IntWritable> value,
Reducer<MemberLogTime,IntWritable,MemberLogTime,IntWritable>.Context context)
throws IOException, InterruptedException {
int sum=0;
for(IntWritable iw:value){
sum+=iw.get();
}
context.write(key,new IntWritable(sum));
}
}
除了声明Combiner类外,还需要在驱动类里面配置Combiner类,如下所示:
job.setCombinerClass(LogCountCombiner.class);
有时候甚至不需要特意声明一个Combiner类。当Combiner与Reducer实现逻辑相同时,可以不用声明Combiner类,在驱动类里面添加以下代码即可:
job.setCombinerClass(LogCountReducer.class);
3.3 浅析Partitioner
(1)下面先给出MapReduce的执行过程
数据首先上传到HDFS并且被分成文件块,接着MapReduce框架根据输入的文件计算输入分片,每个输入分片对应一个Map任务。Map在读取分片数据之前,InputFormat会将分片中的每条记录解析成键值对格式供Map读取。Map的输出结果可能会先传送到Combiner进行合并,而Combiner的输出结果会被Partitioner均匀地分配到每个Reducer上,Reducer的输出结果又会通过OutputFormat解析成特定的格式存储到HDFS上。在这个过程中,Combiner和Partitioner并非必须的,Combiner和Partitioner的使用需要根据实际业务需求来定。
(2)下面对Partitioner进行详细介绍:
Partitioner组件的功能是让Map对key进行分区,从而将不同的key分发到不同的Reducer中进行处理。分区阶段发生在Map阶段之后,Reduce阶段之前,分区的数量等于Reducer的个数。Reducer的个数可以在驱动类里面通过job.setNumReduceTasks设置。在使用多个Reducer的情况下,需要一些方法来确保Mapper输出的键值对发送到正确的Reducer中。
Hadoop自带了一个默认的分区实现,即HashPartitioner。HashPartitioner的实现很简单,它继承了Partitioner<K2,V2>,并且重写了getPartition方法,该方法有三个参数,分别为Mapper输出的值value以及Reducer的个数numReduceTasks,默认numReduceTasks是1。HashPartitioner的getPartition方法实现是根据key的hash值除以2的31次方后取余数,用该余数再次除以Reducer的数量,再取余数,得到的结果就是这个key对应的Partition的编号。
一般情况下,MapReduce程序都会使用默认的HashPartitioner分区,但有时候用户会有一些特殊的需求,例如,统计某社交网站2016年1月和2月用户每天登录的次数,要求1月份的输出结果放到一个文件里,2月份的输出结果放在一个文件里。这个时候就需要自定义Partition来实现这个要求。
(3)下面开始实现自定义Partition
自定义Partitioner需要继承Partitioner<K2,V2>并且重写getPartition方法。如果最终的结果是要输出到多个文件中,只需要让getPartition方法按照一定的规则返回0,1,2,3等即可。如下代码所示,自定义Partitioner实现将社交网站用户每天登陆次数的统计结果根据不同的月份分发到不同的输出文件里。在getPartition的实现方法里,分别使用0、1与numPartitions相除取余。在本例中,可以取numPartitions为2,这样可以刚好把1、2月份分开。使用Partitioner还需要在驱动类里设置Partitioner类及Reducer个数,代码如下:
package essential;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.mapreduce.Partitioner;
public class LogCountPartitioner extends Partitioner<MemberLogTime, IntWritable> {
@Override
public int getPartition(MemberLogTime key, IntWritable value, int numberPartitions) {
String date=key.getLogTime();
if(date.contains("2016-01")){
return 0/numberPartitions;
}else {
return 1/numberPartitions;
}
}
}
在main函数实现设置Partitioner类和设置Reducer个数
job.setPartitionerClass(LogCountPartitioner.class);
job.setNumReduceTask(2);//这样就可以控制Reducer输出的文件及数据分区了
3.4 自定义计数器
在Hadoop的运行日志中可以获取到Map和Reduce的任务数、运行Map任务花费的时间、运行Reduce任务花费的时间,以及Map、Combiner和Reduce的输入输出记录等。这些信息都是Hadoop自带的计数器统计出来的。
(1)概述:计数器是Hadoop框架使用的一种对统计信息收集的手段,主要应用于对数据的控制及收集统计信息。计数器可以帮助程序设计人员手机某一类特定的信息数据。一般情况下,Hadoop将计数器分为五大类,如下表所示:
计数器 |
属性名 |
MapReduce任务计数器 |
org.apache.hadoop.mapreduce.TaskCounter |
文件系统计数器 |
org.apache.hadoop.mapreduce.FileSystemCounter |
输入文件计数器 |
org.apache.hadoop.mapreduce.lib.input.FileInputFormatCounter |
输出文件计数器 |
org.apache.hadoop.mapreduce.lib.input.FileOutputFormatCounter |
作业计数器 |
org.apache.hadoop.mapreduce.JobCounter |
任务计数器主要用于收集任务在运行时的任务信息,任务计数器可以被部署在各个节点上,并且统一传送到主节点进行汇集,如果一个任务最终失败,那么所有的计数器记录都会被重置,即所有的计数清零。只有当任务成功以后,计数器才会被记录。
(2)自定义计数器:
自定义计数器有两种类型:
①通过java枚举(enum)类型来定义,一个作业可以定义的枚举类型数量不限,各个枚举类型包含的字段数量也不限。枚举类型的名称即为组的名称,枚举类型的字段就是计数器的名称。
②使用动态计数器。
(3)实现自定义计数器:
本节任务使用的是社交网站1月份和2月份用户的登录信息,但是1月份和2月份的数据各有多少不知道,如果想要知道每个月份的数据记录数,可以通过自定义计数器来实现。首先在Mapper类中定义枚举类型,代码如下:
enum LogCounter{
January,
February
}
接着在map函数里调用Context类的getCounter方法,说明使用了枚举类型中的哪个计数器,还需要调用increment()方法进行计数的添加,代码如下:
if(logTime.contains("2016-01")){
context.getCounter(LogCounter.January).increment(1);
}else if(logTime.contains("2016-02")){
context.getCounter(LogCounter.February).increment(1);
}
注意:计数器不是只能在Mapper中添加,也可以在Reducer中添加,如果要统计每个月的输出结果记录数,则需要在Reducer中添加上面代码。
(4)另一种自定义计数器的方式是使用动态计数器。除了使用getCounter方法获取枚举中值的方式外,Context类中还有一个重载方法getCounter(String groupName,String countName),能够对当前计数器进行动态计数。例如对统计一月份和二月份用户每天登录次数的输出结果进行计数,则可以在reduce函数中添加代码:
if(key.getLogTime().contains("2016-01")){
context.getCounter("OutputCounter","JanuaryResult").increment(1);
}else if(key.getLogTime().contains("2016-02")){
context.getCounter("OutputCounter","FebruaryResult").increment(1);
}
3.5 任务实现(完成前述第三个任务)
经过前面的讨论探索,现在开始实现本节任务:通过MapReduce编程实现用户在2016年1月份和2月份每天登录次数的统计。实现该任务的具体步骤及代码如下:
自定义Mapper类:
package essential;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
//自定义实现Mapper类
//为了提高效率,在驱动类里面配置Combiner类
public class LogCountMapper extends Mapper<Text,Text,MemberLogTime, IntWritable> {
private MemberLogTime mt=new MemberLogTime();
private IntWritable one=new IntWritable(1);
enum LogCounter{
January,
February
}
protected void map(Text key,Text value,Mapper<Text,Text,MemberLogTime,IntWritable>.Context context) throws IOException, InterruptedException {
String member_name=key.toString();
String logTime=value.toString();
if(logTime.contains("2016-01")){
context.getCounter(LogCounter.January).increment(1);
}else if(logTime.contains("2016-02")){
context.getCounter(LogCounter.February).increment(1);
}
mt.setMember_name(member_name);
mt.setLogTime(logTime);
context.write(mt,one);
}
}
自定义Reducer类:
package essential;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
//自定义实现Reducer类
public class LogCountReducer extends Reducer<MemberLogTime, IntWritable,MemberLogTime,IntWritable> {
protected void reduce(MemberLogTime key,Iterable<IntWritable> value,Reducer<MemberLogTime, IntWritable,MemberLogTime,IntWritable>.Context context) throws IOException, InterruptedException {
if(key.getLogTime().contains("2016-01")){//此处就是使用了计数器
context.getCounter("OutPutCounter","JanuaryResult").increment(1);
}else if(key.getLogTime().contains("2016-02")){
context.getCounter("OutPutCounter","FebruaryResult").increment(1);
}
//下面才是reduce函数的主体
int sum=0;
for(IntWritable iw:value){
sum+=iw.get();
}
context.write(key,new IntWritable(sum));
}
}
驱动类:
package essential;
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.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
import java.util.Properties;
//编辑查找1、2月数据统计的驱动类
public class LogCount {
public static void main(String args[]) throws IOException, ClassNotFoundException, InterruptedException {
// TODO Auto-generated method stub
Configuration conf = new Configuration();
Properties properties = System.getProperties();
properties.setProperty("HADOOP_USER_NAME","root");
//计数器类在前面的自定义Mapper和Reducer已经写入
Job job = Job.getInstance(conf,"LogCount");
job.setJarByClass(LogCount.class);
job.setMapperClass(LogCountMapper.class);//设置自定义Mapper类
job.setMapOutputKeyClass(MemberLogTime.class);
job.setMapOutputValueClass(IntWritable.class);
job.setCombinerClass(LogCountCombiner.class);//设置自定义Combiner类
job.setReducerClass(LogCountReducer.class);//设置自定义Reducer类
job.setOutputKeyClass(MemberLogTime.class);
job.setOutputValueClass(IntWritable.class);
job.setPartitionerClass(LogCountPartitioner.class);//设置自定义分区类
job.setNumReduceTasks(2);
//job.setOutputFormatClass(GbkOutputFormat.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileSystem.get(conf).delete(new Path(args[1]),true);//防止文件目录重复
FileOutputFormat.setOutputPath(job, new Path(args[1]));
System.exit(job.waitForCompletion(true)?0:1);
}
}
4.Eclipse提交日志文件统计程序
打包并提交任务到Hadoop集群,比在集成开发工具上更具有高效性,大文件开发可使用。
5.小结
本章是MapReduce编程进阶,介绍的内容包括MapReduce的输入及输出格式、Hadoop Java API、自定义键值类型、Combiner组件、自定义计数器以及Eclipse提交MapReduce任务。其中,自定义键值类型、Combiner组件和Partitioner组件对程序的优化起到了举足轻重的作用,在一定程度上可以提高程序运行效率。
6.实训
实训目的:掌握MapReduce的Combiner的使用,掌握自定义数据类型,掌握自定义计数器,掌握MapReduce参数传递,掌握ToolRunner的使用和提交MapReduce任务
实训1.统计全球每年的最高气温和最低气温
1.训练要点:掌握Combiner的使用、掌握自定义数据类型
2.需求说明:
将压缩文件上传到linux本地目录,在该目录下解压所有文件,文件数据格式如下:
其中,YEARMODA对应年月日,TEMP对应温度,并且每列数据的分隔符空格数是不同的,这个在预处理数据时要注意。
创建一个文件temperaturedata.txt,在数据文件所在目录执行命令 sed -i '1d' 来删除所有文件的首行字段,然后执行cat * >>data.txt将所有的数据输入到data.txt中
(1)统计全球每年的最高气温和最低气温。
(2)MapReduce输出结果包含年份、最高气温、最低气温,并按照最高气温降序排序。如果最高气温相同,则按照最低气温升序排序。
(3)使用自定义数据类型。
(4)结合Combiner和自定义数据类型完成全球每年最高气温和最低气温的统计。
3.实现思路及步骤:
目的是选出每年的最高气温和最低气温。(因为是选择最大最小值,可以在Combiner阶段就开始选择最大最小值,以提高效率)
输出结果为包含年份+最高气温+最低气温,并按照最高气温降序排序。如果最高气温相同,则按照最低气温升序排序。
(1)自定义数据类型
相当于给此次任务设置一个实体类,作为基本数据类型。
自定义数据类型(重写值类型方法)YearMaxTandMinT继承Writable接口,在这个类里面定义出相关属性:year、Maxtemp、MinTemp以及set、get方法。
实现构造函数。
(2)自定义Mapper
命名为MaxTandMinTMapper,其主要功能是作映射,将year作为key,temp作为value输出。
(3)自定义Combiner
命名为MaxTandMinTCombiner,其主要功能是提前处理Map的一些数据,获取出年度最高和最低的温度,然后作为值输出。
(4)自定义Reducer
命名为MaxTandMinTReducer,其主要功能为对年份进行排序,排序依据如上
4.实现代码:
(1)自定义实体类:
package essential.Temperature;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
//对值类型进行重写,实现Writable类
public class YearMaxTandMinT implements Writable {
private String year;
private double Maxtemp;
private double MinTemp;
public YearMaxTandMinT(){
}
public YearMaxTandMinT(String year,double maxtemp,double mintemp){
this.year=year;
this.Maxtemp=maxtemp;
this.MinTemp=mintemp;
}
public String getYear() {
return year;
}
public void setYear(String year) {
this.year = year;
}
public double getMaxtemp() {
return Maxtemp;
}
public void setMaxtemp(double maxtemp) {
Maxtemp = maxtemp;
}
public double getMinTemp() {
return MinTemp;
}
public void setMinTemp(double minTemp) {
MinTemp = minTemp;
}
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(year);
dataOutput.writeDouble(Maxtemp);
dataOutput.writeDouble(MinTemp);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.Maxtemp=dataInput.readDouble();
this.MinTemp=dataInput.readDouble();
this.year=dataInput.readUTF();
}
public String toString(){
return "Year:"+year+" ;The MaxTemperature is "+Maxtemp+" and the MinTemperature is "+MinTemp;
}
public int Compare(YearMaxTandMinT otherymm){//设置比较两对象之间的大小关系,前者与后者比较,前者较大返回1,后者较大返回-1;用于比较年份时使用
if(Maxtemp>otherymm.getMaxtemp()){
return 1;
}else if(Maxtemp==otherymm.getMaxtemp()){
if(MinTemp<otherymm.getMinTemp()){
return 1;
}else if(MinTemp==otherymm.getMinTemp()){
return 0;
} else{
return -1;
}
}else{
return -1;
}
}
}
(2)自定义Mapper:
package essential.Temperature;
import org.apache.hadoop.io.DoubleWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
//对Mapper进行自定义
public class MaxTandMinTMapper extends Mapper<LongWritable,Text,Text, DoubleWritable> {
//map处理逻辑:主要功能是作映射,将year作为key,temp作为value输出。
public void map(LongWritable key,Text value,Mapper<LongWritable,Text,Text, DoubleWritable>.Context context) throws IOException, InterruptedException {
String[] datas=value.toString().split("\s+");//多空格切割字符串
String year=datas[2].substring(0,4);
double tempdata=Double.parseDouble(datas[3]);
context.write(new Text(year),new DoubleWritable(tempdata));
}
}
(3)自定义Combiner:
package essential.Temperature;
import org.apache.hadoop.io.DoubleWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
//此处的Combiner旨在完成排序筛选工作,选出气温的最大值和最小值,
//此处我我想错了,我想的是一步到位,其实应该是和Mapper的输出应该一致才对,输出的还是应该为年份+温度,只不过是一次性写两次,Combiner的键值对输入类型和输出类型应该是一致的
public class MaxTandMinTCombiner extends Reducer<Text, DoubleWritable,Text,DoubleWritable> {
public void reduce(Text key,Iterable<DoubleWritable> tempdatas,Reducer<Text, DoubleWritable,Text,DoubleWritable>.Context context) throws IOException, InterruptedException {
YearMaxTandMinT ymm=new YearMaxTandMinT();
double maxtemp=0;//初始化温度值
double mintemp=999;
for(DoubleWritable tempdata:tempdatas){//遍历shuffle阶段组合的数组,对key值对应的最大最小值进行更新操作
if(tempdata.get()>maxtemp){
maxtemp=tempdata.get();
}else if(tempdata.get()<mintemp){
mintemp=tempdata.get();
}
}
//ymm.setMaxtemp(maxtemp);
//ymm.setMinTemp(mintemp);
context.write(key,new DoubleWritable(maxtemp));
context.write(key,new DoubleWritable(mintemp));
}
}
(4)自定义Reducer:
package essential.Temperature;
import org.apache.hadoop.io.DoubleWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.LinkedList;
//Reducer是对有Combiner部分分类好的数据进行最后的整合处理,最终要将每一年的年份和最高最低温度进行整理
//并对所有年份进行比较,比较的标准为,先以最高温度为标准进行比较,如果最高温度相同,则以最低温度较高者排在前面
public class MaxTandMinTReducer extends Reducer<Text, DoubleWritable, NullWritable,Text> {
private LinkedList<YearMaxTandMinT> ymmlists=new LinkedList<YearMaxTandMinT>();//用于存储Reduce整合出来的ym
public void reduce(Text key,Iterable<DoubleWritable> temps,Reducer<Text,DoubleWritable, NullWritable,Text>.Context context) throws IOException, InterruptedException {
//先对相同的key的最高温和最低温进行整合
System.out.println("压根没有执行我吗");
YearMaxTandMinT ym=new YearMaxTandMinT();
ym.setYear(key.toString());
double yearmintemp=999;
double yearmaxtemp=0;
for(DoubleWritable temp:temps){
if(temp.get()<yearmintemp){
yearmintemp= temp.get();
}
if(temp.get()>yearmaxtemp){
yearmaxtemp=temp.get();
}
}
ym.setMinTemp(yearmintemp);
ym.setMaxtemp(yearmaxtemp);
//到这里就实现了年份+最高温+最低温的匹配
//然后进行年份的排序,在这里需要引入链表(使用单向链表也可以),因为要存储的数据很少,所以这种方式是可以的。
//链表内数据先以最高温度为标准进行比较,如果最高温度相同,则以最低温度较高者排在前面
if(ymmlists.size()==0){//如果得到的是第一条数据直接加入表格
ymmlists.add(ym);
}else {
if(ym.Compare(ymmlists.getFirst())>0){//表示该元素为现有列表中最大
ymmlists.add(0,ym);
}if(ym.Compare(ymmlists.getLast())<0){//表示该元素在现有列表中最小
ymmlists.add(ym);
}else{//表示在最大和最小之间
for(int index=0;index<ymmlists.size();index++){//实现在指定位置插入元素
//ymmlists.add(1,aaa);
int otherindex=index+1;
if(ym.Compare(ymmlists.get(index))<0 && ym.Compare(ymmlists.get(otherindex))>0){
ymmlists.add(otherindex,ym);
break;
}
}
}
}
String printstring="";
for(YearMaxTandMinT yt:ymmlists){
printstring=printstring+yt.toString()+"
";
System.out.println(printstring);
}
context.write(NullWritable.get(),new Text(printstring));
}
}
(5)驱动类编写:
package essential.Temperature;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.DoubleWritable;
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;
import java.util.Properties;
//实现MapReduce任务,编写驱动类
public class MaxTandMinT {
public static void main(String [] args) throws IOException, ClassNotFoundException, InterruptedException {
// TODO Auto-generated method stub
Configuration conf = new Configuration();
Properties properties = System.getProperties();
properties.setProperty("HADOOP_USER_NAME","root");
//计数器类在前面的自定义Mapper和Reducer已经写入
Job job = Job.getInstance(conf,"MaxTandMinT");
job.setJarByClass(MaxTandMinT.class);
job.setMapperClass(MaxTandMinTMapper.class);//设置自定义Mapper类
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(DoubleWritable.class);
job.setCombinerClass(MaxTandMinTCombiner.class);//设置自定义Combiner类
job.setReducerClass(MaxTandMinTReducer.class);//设置自定义Reducer类
job.setOutputKeyClass(NullWritable.class);
job.setOutputValueClass(Text.class);
//job.setPartitionerClass(LogCountPartitioner.class);//设置自定义分区类
job.setNumReduceTasks(1);
//job.setOutputFormatClass(GbkOutputFormat.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileSystem.get(conf).delete(new Path(args[1]),true);//防止文件目录重复
FileOutputFormat.setOutputPath(job, new Path(args[1]));
System.exit(job.waitForCompletion(true)?0:1);
}
}
5.实现结果:
实训2.筛选15~25℃之间的数据
训练要点:掌握MapReduce参数的传递、掌握自定义计数器、掌握ToolRunner的使用和提交MapReduce任务。
7.小练习
(1)下面属于Hadoop内置数据类型的是:D
A.IntegerWritable B.StringWritable C.ListWritable D.MapWritable
(2)关于自定义数据类型,下列说法正确的是:D
A.自定义数据类型必须继承Writable接口
B.自定义键类型需要继承Writable接口
C.自定义值类型需要继承WritableComparable接口
D.自定义数据类型必须实现readFileds(DataInput datainput)方法
(3)设置MapReduce参数传递的正确方式是:基于MapReduce的API
conf.set("argName",args[n])传递
(4)在Mapper类的setup函数中,下列( )方式可以用来获取参数值。
context.getConfiguration.get("argName")
既然Hadoop的配置类Configuration里面有根据属性名称获取参数值的方法,即返回类型为String类型的get(String name)方法。在编写MapReduce程序的时候,可以在setup方法中通过上下文对象Context中的getConfiguaration()方法来获取配置对象Configuaration,再调用Configuration里面的get(String name)方法获取这些参数值。
(5)MapReduce的输入默认格式为TextInputFormat,输出默认格式为TextOutputFormat
全部基本知识到这里基本就结束了,最后一章是一个网站项目,主要的学习目标是学习KNN算法,并使用MapReduce实现KNN算法。