Hadoop由HDFS分布式存储、MapReduce分布式计算、Yarn资源调度三部分组成。
reduce阶段有一个关键的函数reduce()函数
此函数的输入也是键值对(即map的输出(kv对))
输出也是一系列键值对,结果最终写入HDFS
以MapReduce的词频统计为例:统计一批英文文章当中,每个单词出现的总次数。
MapReduce编程中,key有特殊的作用:
①数据中,若要针对某个值进行分组、聚合时,需将此值作为MR中的reduce的输入的key
如当前的词频统计例子,按单词进行分组,每组中对出现次数做聚合(计算总和);所以需要将每个单词作为reduce输入的key,MapReduce框架自动按照单词分组,进而求出每组即每个单词的总次数
②另外,key还具有可排序的特性,因为MR中的key类需要实现WritableComparable接口;而此接口又继承Comparable接口(可查看源码)
MR编程时,要充分利用以上两点;结合实际业务需求,设置合适的key
<properties>
<cdh.version>2.6.0-cdh5.14.2cdh.version>
properties>
<repositories>
<repository>
<id>clouderaid>
<url>https://repository.cloudera.com/artifactory/cloudera-repos/url>
repository>
repositories>
<dependencies>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-clientartifactId>
<version>2.6.0-mr1-cdh5.14.2version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-commonartifactId>
<version>${cdh.version}version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-hdfsartifactId>
<version>${cdh.version}version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-mapreduce-client-coreartifactId>
<version>${cdh.version}version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.11version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.testnggroupId>
<artifactId>testngartifactId>
<version>RELEASEversion>
<scope>testscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.38version>
<scope>compilescope>
dependency>
dependencies>
创建包com.zsc.wordcount
在包中创建自定义mapper类、自定义reducer类、包含main类
2.4.1 Mapper代码
package com.zsc.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的四个泛型分别表示
* 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
//取得当前行的数据
String line = value.toString();
//按照\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));
}
}
}
2.4.2 Reducer代码
package com.zsc.wordcount;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
*
* Reducer的四个泛型分别表示
* 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));// 输出最终结果
};
}
2.4.3 Main程序入口
package com.zsc.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();
//调用getInstance方法,生成job实例
Job job = Job.getInstance(configuration, WordCountMain.class.getSimpleName());
//设置job的jar包,如果参数指定的类包含在一个jar包中,则此jar包作为job的jar包; 参数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);
}
}
应用:mapreduce在企业中,可以用于对海量数据的数据清洗;当然,随着新一代大数据框架的出现,也可以使用spark、flink等框架,做数据清洗
日志格式:每行记录有6个字段;分别表示时间datetime、用户ID userid、新闻搜索关键字searchkwd、当前记录在返回列表中的序号retorder、用户点击链接的顺序cliorder、点击的URL连接cli-url
MapReduce程序一般分为map阶段,将任务分而治之;reduce阶段,将map阶段的结果进行聚合
但有些mapreduce应用不需要数据聚合的操作,也就是说不需要reduce阶段。即编程时,不需要编写自定义的reducer类;在main()中调用job.setNumReduceTasks(0)设置。本例的数据清洗就是属于此种情况
map方法的逻辑:取得每一行数据,与每条记录的固定格式比对,是否符合:
3.4.1 Mapper类
package com.zsc.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);
}
}
}
}
①reduce 0%,job就已经successfully,表示此MR程序没有reduce阶段
②DataCleaning是自定义计数器组名;damagedRecord是自定义的计数器;值为6,表示有6条损坏记录
图中part-m-00000中的m表示,此文件是由map任务生成
使用MR编程,统计sogou日志数据中,每个用户搜索的次数,结果写入HDFS
package com.zsc.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 {
/**
*
* @param args C:\test\datacleanresult c:\test\usersearch
* @throws IOException
* @throws ClassNotFoundException
* @throws InterruptedException
*/
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, 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);
}
}
}
4.4.1 结果
C:\test\datacleanresult c:\test\usersearch
shuffle主要指的是map端的输出作为reduce端输入的过程
分区使用了分区器,默认分区器是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;
}
}
reduce task会在每个map task运行完成后,通过HTTP获得map task输出中,属于自己的分区数据(许多kv对)
如果map输出数据比较小,先保存在reduce的jvm内存中,否则直接写入reduce磁盘
一旦内存缓冲区达到阈值(默认0.66)或map输出数的阈值(默认1000),则触发归并merge,结果写到本地磁盘
若MR编程指定了combine,在归并过程中会执行combine操作
随着溢出写的文件的增多,后台线程会将它们合并大的、排好序的文件
reduce task将所有map task复制完后,将合并磁盘上所有的溢出文件
默认一次合并10个
最后一批合并,部分数据来自内存,部分来自磁盘上的文件
进入“归并、排序、分组阶段”
每组数据调用一次reduce方法