目录
准备:
-
准备3节点hadoop集群
-
windows下hadoop环境配置好
-
windows下安装IDEA编程工具
-
安装maven并配置环境变量
目标
-
理解MapReduce编程模型
-
描述MR的shuffle全流程(面试)
-
搭建MAVEN工程,统计词频,并提交集群运行,查看结果
-
利用搜狗数据,找出所有独立的uid并写入HDFS
-
利用搜狗数据,找出所有独立的uid出现次数,并写入HDFS,并要求使用Map端的Combine操作
-
谈谈什么是数据倾斜,什么情况会造成数据倾斜?(面试)
-
对MR数据倾斜,如何解决?(面试)
一、MapReduce基本原理
1.1 基本概念
-
1、MapReduce是采用一种分而治之的思想设计出来的分布式计算框
-
分而治之:比如一复杂、计算量大、耗时长的的任务,暂且称为“大任务”;此时使用单台服务器无法计算或较短时间内计算出结果时,可将此大任务切分成一个个小的任务,小任务分别在不同的服务器上并行的执行,最终再汇总每个小任务的结果。
-
-
2、MapReduce由两个阶段组成:
-
Map阶段(切分成一个个小的任务)
-
Reduce阶段(汇总小任务的结果)
-
-
3、MR输入的文件有几个block,就会生成几个map任务
-
4、MR的reduce任务的个数,由程序中编程指定:job.setNumReduceTasks(4)
-
5、map任务:
-
map任务一次读取block的一行数据,以kv对的形式输入map()
-
map()的输出作为reduce()的输入
-
-
6、reduce任务:
-
reduce任务通过网络将各map任务输出结果中,属于自己的数据取过来
-
key相同的键值对作为一组,调用一次reduce()
-
reduce任务生成一个结果文件
-
文件写入HDFS
-
以MapReduce的词频统计为例:统计一批英文文章当中,每个单词出现的总次数
假设MR的输入文件“Gone With The Wind”有三个block;block1、block2、block3;
MR编程时,每个block对应一个分片split,每一个split对应一个map任务(map task);
如图共3个map任务(map1、map2、map3);这3个任务的逻辑一样,所以以第一个map任务(map1)为例分析;
reduce任务个数,由自己写的程序编程指定,如job.setNumReduceTasks(4)指定reduce任务是4个(reduce1、reduce2、reduce3、reduce4)
1.2、Map流程
(1)input split
map1读取block1的数据;一次读取block1的一行数据,产生键值对(key/value),作为map()的参数传入。
(2)map()函数:(自定义)
- 假设当前所读行是第一行,将当前所读行的行首相对于当前block开始处的字节偏移量作为key(0)
- 当前行的内容作为value(Dear Bear River),将value当前行内容按空格切分,得到三个单词Dear | Bear | River
- 将每个单词变成键值对,输出出去(Dear, 1) | (Bear, 1) | (River, 1);
- block的第一行的数据被处理完后,接着处理第二行;逻辑同上
- 当map任务将当前block中所有的数据全部处理完后,此map任务即运行结束
(3)存数据:(shuffle)
结果最终写入map任务所在节点的本地磁盘中(内里还有细节,讲到shuffle时,再细细展开)
1.3、Reduce流程
(1)拉取:(shuffle)
-
map1任务完成后,reduce1通过网络,连接到map1,将map1输出结果中属于reduce1的分区的数据,通过网络获取到reduce1端(拷贝阶段),同样也如此连接到map2、map3获取结果
-
最终reduce1端获得4个(Dear, 1)键值对;由于key键相同,它们分到同一组;
-
4个(Dear, 1)键值对,转换成[Dear, Iterable(1, 1, 1, )],作为参数传入reduce()
(2)reduce()
-
在reduce()内部,计算Dear的总数为4,并将(Dear, 4)作为键值对输出
(3)写入HDFS
每个reduce任务最终输出文件(内里还有细节,讲到shuffle时,再细细展开),文件写入到HDFS.
1.4、其他说明:
1、Map任务数 = block数
2、Reduce任务数 = 自定义
3、key有特殊的作用
-
数据中,若要针对某个值进行分组、聚合时,需将此值作为MR中的reduce的输入的key
-
如当前的词频统计例子,按单词进行分组,每组中对出现次数做聚合(汇总计算总和);所以需要将每个单词作为reduce输入的key,MapReduce框架自动按照单词分组,进而求出每组即每个单词的总次数
-
另外,key还具有可排序的特性,因为MR中的key类需要实现WritableComparable接口;而此接口又继承Comparable接口(可查看源码)
二、 Shuffle(重点 )
2.1、 shuffle简图
-
shuffle主要指的是map端的输出作为reduce端输入的过程
-
为了确保每个reducer的的输入都是按键排序的,系统执行排序的过程,即将map task的输出通过一定规则传给reduce task,这个过程成为shuffle。
Shuffle阶段一部分是在map task 中进行的, 这里称为Map shuffle , 还有一部分是在reduce task 中进行的, 这里称为Reduce shffle。
2.2、 shuffle细节图
-
分区用到了分区器,默认分区器是HashPartitioner,源码:
public class HashPartitioner<K2, V2> implements Partitioner<K2, V2> {
public void configure(JobConf job) {}
/** Use {@link Object#hashCode()} to partition. */
public int getPartition(K2 key, V2 value,
int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
2.3、 map端的shuffle
-
(1)环形内存缓冲:
-
每个map任务都有一个对应的环形内存缓冲区;map()输出kv对,先写入到环形缓冲区(默认大小100M),当内容占据80%缓冲区空间后,由一个后台线程将缓冲区中的数据溢出写到一个磁盘文件。
-
在溢出写的过程中,map任务可以继续向环形缓冲区写入数据;但是若写入速度大于溢出写的速度,最终造成100m占满后,map任务会暂停向环形缓冲区中写数据的过程;只执行溢出写的过程;直到环形缓冲区的数据全部溢出写到磁盘,才恢复向缓冲区写入
-
-
(2)后台线程溢写磁盘过程,
-
(1)分区:先对每个溢写的kv对做分区;分区的个数由MR程序的reduce任务数决定;默认使用HashPartitioner计算当前kv对属于哪个分区;计算公式:(key.hashCode() & Integer.MAX_VALUE) % numReduceTasks
-
(2)排序: 每个分区中,根据kv对的key做内存中排序;
-
(3)combine(可选):若设置了map端本地聚合combiner,则对每个分区中,排好序的数据做combine操作;
-
(4)压缩(可选):若设置了对map输出压缩的功能,会对溢写数据压缩
-
(3)说明:
-
随着不断的向环形缓冲区中写入数据,会多次触发溢写(每当环形缓冲区写满100m),本地磁盘最终会生成多个溢出文件
-
合并溢写文件:在map task完成之前,所有溢出文件会被合并成一个大的溢出文件;且是已分区、已排序的输出文件
-
小细节:
-
在合并溢写文件时,如果至少有3个溢写文件,并且设置了map端combine的话,会在合并的过程中触发combine操作;
-
但是若只有2个或1个溢写文件,则不触发combine操作(因为combine操作,本质上是一个reduce,需要启动JVM虚拟机,有一定的开销)
-
2.4、reduce端的shuffle
-
(1)拉取:
-
reduce task会在每个map task运行完成后,通过HTTP获得map task输出中,属于自己的分区数据(许多kv对)
-
如果map输出数据比较小,先保存在reduce的jvm内存中,否则直接写入reduce磁盘
-
-
(2)归并merge:
-
一旦内存缓冲区达到阈值(默认0.66)或map输出数的阈值(默认1000),则触发归并merge,结果写到本地磁盘
-
-
(3)combine(可选):若MR编程指定了combine,在归并过程中会执行combine操作
(4)说明:
-
随着溢出写的文件的增多,后台线程会将它们合并大的、排好序的文件
-
reduce task将所有map task复制完后,将合并磁盘上所有的溢出文件
-
默认一次合并10个
-
最后一批合并,部分数据来自内存,部分来自磁盘上的文件
-
进入“归并、排序、分组阶段”
-
每组数据调用一次reduce方法
三、MR编程—词频统计
-
创建包com.kaikeba.hadoop.wordcount
-
在包中创建自定义mapper类、自定义reducer类、包含main类
1、main
3.2、map
3.3、Reduce
package com.kaikeba.hadoop.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;
/**
*
* MapReduce程序入口
* 注意:
* 导包时,不要导错了;
* 另外,map\reduce相关的类,使用mapreduce包下的,是新API,如org.apache.hadoop.mapreduce.Job;;
*/
public class WordCountMain {
//若在IDEA中本地执行MR程序,需要将mapred-site.xml中的mapreduce.framework.name值修改成local
//参数 c:/test/README.txt c:/test/wc
public static void main(String[] args) throws IOException,
ClassNotFoundException, InterruptedException {
//判断一下,输入参数是否是两个,分别表示输入路径、输出路径
if (args.length != 2 || args == null) {
System.out.println("please input Path!");
System.exit(0);
}
Configuration configuration = new Configuration();
//configuration.set("mapreduce.framework.name","local");
//告诉程序,要运行的jar包在哪
//configuration.set("mapreduce.job.jar","/home/hadoop/IdeaProjects/Hadoop/target/com.kaikeba.hadoop-1.0-SNAPSHOT.jar");
//调用getInstance方法,生成job实例
Job job = Job.getInstance(configuration, WordCountMain.class.getSimpleName());
//设置job的jar包,如果参数指定的类包含在一个jar包中,则此jar包作为job的jar包; 参数class跟主类在一个工程即可;一般设置成主类
// job.setJarByClass(WordCountMain.class);
job.setJarByClass(WordCountMain.class);
//通过job设置输入/输出格式
//MR的默认输入格式是TextInputFormat,输出格式是TextOutputFormat;所以下两行可以注释掉
// job.setInputFormatClass(TextInputFormat.class);
// job.setOutputFormatClass(TextOutputFormat.class);
//设置输入/输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//设置处理Map阶段的自定义的类
job.setMapperClass(WordCountMap.class);
//设置map combine类,减少网路传出量
job.setCombinerClass(WordCountReduce.class);
//设置处理Reduce阶段的自定义的类
job.setReducerClass(WordCountReduce.class);
//注意:如果map、reduce的输出的kv对类型一致,直接设置reduce的输出的kv对就行;如果不一样,需要分别设置map, reduce的输出的kv类型
//注意:此处设置的map输出的key/value类型,一定要与自定义map类输出的kv对类型一致;否则程序运行报错
// job.setMapOutputKeyClass(Text.class);
// job.setMapOutputValueClass(IntWritable.class);
//设置reduce task最终输出key/value的类型
//注意:此处设置的reduce输出的key/value类型,一定要与自定义reduce类输出的kv对类型一致;否则程序运行报错
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 提交作业
job.waitForCompletion(true);
}
}
package com.kaikeba.hadoop.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;
/**
* 类Mapper<LongWritable, Text, Text, IntWritable>的四个泛型分别表示
* map方法的输入的键的类型kin、值的类型vin;输出的键的类型kout、输出的值的类型vout
* kin指的是当前所读行行首相对于split分片开头的字节偏移量,所以是long类型,对应序列化类型LongWritable
* vin指的是当前所读行,类型是String,对应序列化类型Text
* kout根据需求,输出键指的是单词,类型是String,对应序列化类型是Text
* vout根据需求,输出值指的是单词的个数,1,类型是int,对应序列化类型是IntWritable
*
*/
public class WordCountMap extends Mapper<LongWritable, Text, Text, IntWritable> {
/**
* 处理分片split中的每一行的数据;针对每行数据,会调用一次map方法
* 在一次map方法调用时,从一行数据中,获得一个个单词word,再将每个单词word变成键值对形式(word, 1)输出出去
* 输出的值最终写到本地磁盘中
* @param key 当前所读行行首相对于split分片开头的字节偏移量
* @param value 当前所读行
* @param context
* @throws IOException
* @throws InterruptedException
*/
public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
//当前行的示例数据(单词间空格分割):Dear Bear River
// 1、取得当前行的数据
String line = value.toString();
// 2、按照\t进行分割,得到当前行所有单词
String[] words = line.split("\t");
for (String word : words) {
//将每个单词word变成键值对形式(word, 1)输出出去
//同样,输出前,要将kout, vout包装成对应的可序列化类型,如String对应Text,int对应IntWritable
context.write(new Text(word), new IntWritable(1));
}
}
}
package com.kaikeba.hadoop.wordcount;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
*
* Reducer<Text, IntWritable, Text, IntWritable>的四个泛型分别表示
* reduce方法的输入的键的类型kin、输入值的类型vin;输出的键的类型kout、输出的值的类型vout
* 注意:因为map的输出作为reduce的输入,所以此处的kin、vin类型分别与map的输出的键类型、值类型相同
* kout根据需求,输出键指的是单词,类型是String,对应序列化类型是Text
* vout根据需求,输出值指的是每个单词的总个数,类型是int,对应序列化类型是IntWritable
*
*/
public class WordCountReduce extends Reducer<Text, IntWritable, Text, IntWritable> {
/**
*
* key相同的一组kv对,会调用一次reduce方法
* 如reduce task汇聚了众多的键值对,有key是hello的键值对,也有key是spark的键值对,如下
* (hello, 1)
* (hello, 1)
* (hello, 1)
* (hello, 1)
* ...
* (spark, 1)
* (spark, 1)
* (spark, 1)
*
* 其中,key是hello的键值对被分成一组;merge成[hello, Iterable(1,1,1,1)],调用一次reduce方法
* 同样,key是spark的键值对被分成一组;merge成[spark, Iterable(1,1,1)],再调用一次reduce方法
*
* @param key 当前组的key
* @param values 当前组中,所有value组成的可迭代集和
* @param context reduce上下文环境对象
* @throws IOException
* @throws InterruptedException
*/
public void reduce(Text key, Iterable<IntWritable> values,
Context context) throws IOException, InterruptedException {
//定义变量,用于累计当前单词出现的次数
int sum = 0;
for (IntWritable count : values) {
//从count中获得值,累加到sum中
sum += count.get();
}
//将单词、单词次数,分别作为键值对,输出
context.write(key, new IntWritable(sum));// 输出最终结果
};
}
3.4、编写运行步骤
1、创建MAVEN工程
所有编程操作,在hadoop集群某节点的IDEA中完成
-
使用IDEA创建maven工程
-
pom文件参考提供的pom.xml,主要用到的dependencies有
<properties>
<cdh.version>2.6.0-cdh5.14.2</cdh.version>
</properties>
<repositories>
<repository>
<id>cloudera</id>
<url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.6.0-mr1-cdh5.14.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>${cdh.version}</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>${cdh.version}</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-core</artifactId>
<version>${cdh.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
<scope>compile</scope>
</dependency>
</dependencies>
2、代码编写
3、本地运行
在windows的IDEA中运行
初次运行WordCountMain,先设置main方法参数,根据图示操作即可
查看结果
①成功标识文件
②结果文件
4、集群运行
-
用maven插件打jar包;①点击Maven,②双击package打包
控制台打印结果;①表示打包成功;②是生成的jar所在路径
-
将jar包上传到node01用户主目录 /home/hadoop下
-
用hadoop jar命令运行mr程序
[hadoop@node01 ~]$ cd
[hadoop@node01 ~]$ hadoop jar com.kaikeba.hadoop-1.0-SNAPSHOT.jar com.kaikeba.hadoop.wordcount.WordCountMain /README.txt /wordcount01
说明:
com.kaikeba.hadoop-1.0-SNAPSHOT.jar是jar包名
com.kaikeba.hadoop.wordcount.WordCountMain是包含main方法的类的全限定名
/NOTICE.txt和/wordcount是main方法的两个参数,表示输入路径、输出路径
确认结果
[hadoop@node01 ~]$ hadoop fs -ls /wordcount01
四、MR编程-数据清洗
mapreduce在企业中,可以用于对海量数据的数据清洗;
当然,随着新一代大数据框架的出现,也可以使用spark、flink等框架,做数据清洗
4.1、需求
-
现有一批日志文件,日志来源于用户使用搜狗搜索引擎搜索新闻,并点击查看搜索结果过程;但是,日志中有一些记录损坏,现需要使用MapReduce来将这些损坏记录(如记录中少字段、多字段)从日志文件中删除,此过程就是传说中的数据清洗。并且在清洗时,要统计损坏的记录数。
-
MapReduce程序一般分为:map阶段,将任务分而治之;reduce阶段,将map阶段的结果进行聚合;而,有些mapreduce应用不需要数据聚合的操作,也就是说不需要reduce阶段。即编程时,不需要编写自定义的reducer类;在main()中调用job.setNumReduceTasks(0)设置。而本例的数据清洗就是属于此种情况。
-
日志格式:每行记录有6个字段;分别表示时间datetime、用户ID userid、新闻搜索关键字searchkwd、当前记录在返回列表中的序号retorder、用户点击链接的顺序cliorder、点击的URL连接cliurl
4.2、逻辑分析
-
统计损坏的记录数,可自定义map方法中,使用自定义计数器的方式进行;默认reduce阶段。
-
map方法的逻辑:取得每一行数据,与每条记录的固定格式比对,是否符合;若符合,则是完好的记录;否则是损坏的记录。并对自定义计数器累加。
4.4、MR代码
若要集群运行,需先将sogou.50w.utf8上传到HDFS根目录
[hadoop@node01 soft]$ pwd
/kkb/soft
[hadoop@node01 soft]$ hadoop fs -put sogou.50w.utf8 /
package com.kaikeba.hadoop.dataclean;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Counter;
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.output.FileOutputFormat;
import java.io.IOException;
/**
*
* 现对sogou日志数据,做数据清洗;将不符合格式要求的数据删除
* 每行记录有6个字段;
* 分别表示时间datetime、用户ID userid、新闻搜索关键字searchkwd、当前记录在返回列表中的序号retorder、用户点击链接的顺序cliorder、点击的URL连接cliurl
* 日志样本:
* 20111230111308 0bf5778fc7ba35e657ee88b25984c6e9 nba直播 4 1 http://www.hoopchina.com/tv
*
*/
public class DataClean {
/**
*
* 基本上大部分MR程序的main方法逻辑,大同小异;将其他MR程序的main方法代码拷贝过来,稍做修改即可
* 实际开发中,也会有很多的复制、粘贴、修改
*
* 注意:若要IDEA中,本地运行MR程序,需要将resources/mapred-site.xml中的mapreduce.framework.name属性值,设置成local
* @param args
*/
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//判断一下,输入参数是否是两个,分别表示输入路径、输出路径
if (args.length != 2 || args == null) {
System.out.println("please input Path!");
System.exit(0);
}
Configuration configuration = new Configuration();
//调用getInstance方法,生成job实例
Job job = Job.getInstance(configuration, DataClean.class.getSimpleName());
//设置jar包,参数是包含main方法的类
job.setJarByClass(DataClean.class);
//设置输入/输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//设置处理Map阶段的自定义的类
job.setMapperClass(DataCleanMapper.class);
//注意:此处设置的map输出的key/value类型,一定要与自定义map类输出的kv对类型一致;否则程序运行报错
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
//注意:因为不需要reduce聚合阶段,所以,需要显示设置reduce task个数是0
job.setNumReduceTasks(0);
// 提交作业
job.waitForCompletion(true);
}
/**
*
* 自定义mapper类
* 注意:若自定义的mapper类,与main方法在同一个类中,需要将自定义mapper类,声明成static的
*/
public static class DataCleanMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
//为了提高程序的效率,避免创建大量短周期的对象,出发频繁GC;此处生成一个对象,共用
NullWritable nullValue = NullWritable.get();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//自定义计数器,用于记录残缺记录数
Counter counter = context.getCounter("DataCleaning", "damagedRecord");
//获得当前行数据
//样例数据:20111230111645 169796ae819ae8b32668662bb99b6c2d 塘承高速公路规划线路图 1 1 http://auto.ifeng.com/roll/20111212/729164.shtml
String line = value.toString();
//将行数据按照记录中,字段分隔符切分
String[] fields = line.split("\t");
//判断字段数组长度,是否为6
if(fields.length != 6) {
//若不是,则不输出,并递增自定义计数器
counter.increment(1L);
} else {
//若是6,则原样输出
context.write(value, nullValue);
}
}
}
}
五、MapReduce编程—用户搜索次数
5.1 需求
-
使用MR编程,统计sogou日志数据中,每个用户搜索的次数;结果写入HDFS
-
数据来源自“MapReduce编程:数据清洗”中的输出结果
-
仍然是sogou日志数据;不再赘述
5.2 分析
-
MR中key的作用
-
MR编程时,若要针对某个值对数据进行分组、聚合时,需要将每个单词作为reduce输入的key,如当前的词频统计例子,从而按照单词分组,进而求出每组即每个单词的总次数
-
那么此例也是类似的。
-
统计每个用户的搜索次数,将userid放到reduce输入的key的位置;
-
对userid进行分组,进而统计每个用户的搜索次数
-
package com.kaikeba.hadoop.searchcount;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
/**
*
* 本MR示例,用于统计每个用户搜索并查看URL链接的次数
*/
public class UserSearchCount {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//判断一下,输入参数是否是两个,分别表示输入路径、输出路径
if (args.length != 2 || args == null) {
System.out.println("please input Path!");
System.exit(0);
}
Configuration configuration = new Configuration();
//configuration.set("mapreduce.job.jar","/home/hadoop/IdeaProjects/Hadoop/target/com.kaikeba.hadoop-1.0-SNAPSHOT.jar");
//调用getInstance方法,生成job实例
Job job = Job.getInstance(configuration, UserSearchCount.class.getSimpleName());
//设置jar包,参数是包含main方法的类
job.setJarByClass(UserSearchCount.class);
//通过job设置输入/输出格式
//MR的默认输入格式是TextInputFormat,输出格式是TextOutputFormat;所以下两行可以注释掉
// job.setInputFormatClass(TextInputFormat.class);
// job.setOutputFormatClass(TextOutputFormat.class);
//设置输入/输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//设置处理Map阶段的自定义的类
job.setMapperClass(SearchCountMapper.class);
//设置map combine类,减少网路传出量
//job.setCombinerClass(WordCountReduce.class);
//设置处理Reduce阶段的自定义的类
job.setReducerClass(SearchCountReducer.class);
//如果map、reduce的输出的kv对类型一致,直接设置reduce的输出的kv对就行;如果不一样,需要分别设置map, reduce的输出的kv类型
//注意:此处设置的map输出的key/value类型,一定要与自定义map类输出的kv对类型一致;否则程序运行报错
// job.setMapOutputKeyClass(Text.class);
// job.setMapOutputValueClass(IntWritable.class);
//设置reduce task最终输出key/value的类型
//注意:此处设置的reduce输出的key/value类型,一定要与自定义reduce类输出的kv对类型一致;否则程序运行报错
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//提交作业
job.waitForCompletion(true);
}
public static class SearchCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
//定义共用的对象,减少GC压力
Text userIdKOut = new Text();
IntWritable vOut = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//获得当前行的数据
//样例数据:20111230111645 169796ae819ae8b32668662bb99b6c2d 塘承高速公路规划线路图 1 1 http://auto.ifeng.com/roll/20111212/729164.shtml
String line = value.toString();
//切分,获得各字段组成的数组
String[] fields = line.split("\t");
//因为要统计每个user搜索并查看URL的次数,所以将userid放到输出key的位置
//注意:MR编程中,根据业务需求设计key是很重要的能力
String userid = fields[1];
//设置输出的key的值
userIdKOut.set(userid);
//输出结果
context.write(userIdKOut, vOut);
}
}
public static class SearchCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
//定义共用的对象,减少GC压力
IntWritable totalNumVOut = new 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();
}
//设置当前user搜索并查看总次数
totalNumVOut.set(sum);
context.write(key, totalNumVOut);
}
}
}