一、MapReduce简单概述
在Hadoop中有两个核心的模块,一个是大数据量文件的存储HDFS,另一个是能够做快速的数据分析,则为MapReduce。
百度百科介绍:
二、MapReduce的特点
它适合做“离线”(存储在本地)的海量数据计算,通常计算的数据量在PB级别或者ZB级别
MapReduce的主要特点如下:
- 易于编程
- 扩展性良好
- 高容错性
三、MapReduce的应用场景
MapReduce对于离线的大数据量分析计算还是需要一定时间的,不能实时做计算分析,它的典型应用有以下一些:
- 简单的数据统计,比如网站的pv、uv
- 搜索引擎建立索引
- 在搜索引擎中统计最流行的搜索词
- 统计搜索的词频率
- 复杂的数据分析算法实现
MapReduce也有不适用的的场景
1. 实时计算
2. 流式计算(MapReduce对于数据集要求是静态的,不能是动态的)
- DAG计算,当多个应用程序存在依赖关系,且后一个应用的输入来自于前一个输出这个情况是不适合用MapReduce的
四、MapReduce是如何运行的(通过实例来理解这个过程)
单词统计,分析的过程如下图所示
图中主要分为五个过程
Split:把大文件进行切割成多份
Map:分别针对切出来的小文件,解析出每一个字符并在后面记上数字1
Shuffle:每一份中的字符进行分组到一起并做统计
Reduce:把对应的字符进行累加
Final result:输出最终的结果
上面我们总结出了大致的执行过程,接下来看看具体的执行过程如何
1. 把数据切割成数据片段
2. 数据片段以key和value的形式被读进来处理,默认情况是以行的下标作为key,行的内容做为value
3. 数据传入Map中进行处理(处理逻辑由用户定义),在Map中处理完成后还是以key和value的形式输出
4. 输出的数据给到Shuffle,它进行数据的排序合并等操作,但是它不会修改传入的数据,这个时候还是key2和value
5. 数据接下来传给Reduce进行处理,它处理完后,生成key3和value3
6. Reduce处理完的数据会被写到HDFS的某个目录中
接下来针对 Split、Map、Reduce及Shuffle做分别的简单介绍
1. Split(MapReduce的文件切片)
第一个问题,它按多大的大小进行切片?
默认与block对应,这个大小也可以由用户自行控制,MapReduce的Split大小计算公式如下: max(min.split, min(max.split, block))
max.split = totalSize/numSplit(totalSize:文件大小;numSplit:用户设定的map task的个数,默认为1)
min.split = InputSplit的最小值(可以在配置文件中进行配置参数,mapred.min.split.size,在不配置时默认为1B,block是HDFS中块的大小)
2. Map过程与Reduce过程
Map和Reduce的实现逻辑都是由程序员完成的.
Map个数与Split的个数对应起来,一个Split切片对应一个Map任务
Reduce的默认数是1,这个是可以自行设置的。
注意:一个程序可能只有一个Map任务却没有Reduce任务,也可能是多个MapReduce程序串连起来,比如,第一个MapReduce的输出结果作为第二个MapReduce的输入,第二个MapReduce的输出又是第三个MapReduce的输入,最终所有的MapReduce完成才完成一个任务。
3. Shuffle过程
它又叫“洗牌”,它起到了连接Map任务与Reduce任务的作用。
Shuffle分为两个部分,一部分在Map端,另一部分在Reduce端
Map处理后的数据会以key、value的形式存在缓冲区中,这个缓冲区大小是128MB,当这个缓冲区快要溢出时(默认为80%),会把数据写到磁盘中生成文件,这个过程叫做溢写操作。
溢写磁盘的工作由一个线程来完成,溢写之前包括Partition(分区)、Sort(排序)都有默认的实现
Partition:默认按hash值%reduce数量进行分区
Sort:默认按字符顺序进行排序
在完成溢写后,在磁盘上会生成多个文件,多个文件会通过merge线程完成文件合并
合并完成后的数据(以key和value的形式存在),会基于Partition被发送到不同的Reduce上
五、MapReduce实例
实例一:对文件中的单词进行统计,字符之间使用空格进行分隔
相关的JAR包
hadoop-3.1.3\share\hadoop\common 下的jar及hadoop-3.1.3\share\hadoop\common\lib下的jar包
hadoop-3.1.3\share\hadoop\mapreduce下的jar及hadoop-3.1.3\share\hadoop\mapreduce\lib下的jar包
hadoop-3.1.3\share\hadoop\hdfs下及hadoop-3.1.3\share\hadoop\hdfs\lib下的jar包
一个完整的MapReduce分为两个部分:一个是Mapper;另一个是Reducer
Mapper
它是用来对文件进行切割并且循环输出给到Reducer
public class WordCountMapper extends Mapper<LongWritable,Text,Text,IntWritable>{
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//使用空隔进行分词
String[] str = value.toString().split(" ");
//for循环处理
for(int i = 0; i < str.length; i++){
//new Text,new IntWritable进行可序列化
context.write(new Text(str[i]),new IntWritable(1));
}
}
}
Readucer
它将从Map转入的词汇进行分组合并,并通过文本和单词统计量的方式输出
public class WordCountReducer 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 i:values){
sum = sum+i.get();
}
context.write(key,new IntWritable(sum));
}
}
主方法所在类
主方法中用来连接HDFS、要读取的文件及处理后的文件在HDFS中的路径。同时需要指明我们所要进行Map和Reduce过程的类
public class MRRunJob {
public static void main(String[] args) {
Configuration conf = new Configuration();
//NameNode入口
conf.set("fs.defaultFS","hdfs://192.168.2.4:8020");
Job job = null;
try {
job = Job.getInstance(conf,"mywc");
} catch (IOException e) {
e.printStackTrace();
}
//主类
job.setJarByClass(MRRunJob.class);
//Mapper类
job.setMapperClass(WordCountMapper.class);
//Reducer类
job.setReducerClass(WordCountReducer.class);
//Map输出的value类型
job.setOutputKeyClass(Text.class);
//Map输出的value类型
job.setOutputValueClass(IntWritable.class);
try {
//读取文件位置
job.setWorkingDirectory(new Path("/"));
System.out.println(job.getWorkingDirectory());
FileInputFormat.addInputPath(job,new Path("/usr/input/data/wc/"));
//处理完成后数据存储的位置(注意:如果输出文件夹存在则会报错)
FileOutputFormat.setOutputPath(job,new Path("/usr/output/data/wc/"));
job.waitForCompletion(true);
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意:上面的类一定是要在HDFS中的路径是否则程序会报错提示找不到文件位置。切记!!
实例二:数据清洗ETL实例(对时间进行格式化处理)
Mapper
public class ETLMapper extends Mapper<LongWritable,Text,Text,NullWritable> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//使用,号对文本内容进行分隔
String[] strArray = value.toString().split(",");
String strContent = "";
for(int i=0;i<strArray.length;i++){
//把下标为第三个数据转换为yyyy-MM-dd HH:mm:ss的时间格式
if(i == 3){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//把原数据的秒转为毫秒
String str = sdf.format(Long.parseLong(strArray[i]+"000"));
strContent = strContent + str + ",";
} else {
strContent = strContent + strArray[i] + ",";
}
}
context.write(new Text(strContent),NullWritable.get());
}
}
Reducer
public class ETLReducer extends Reducer<Text,NullWritable,NullWritable,Text> {
@Override
protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
//把Mapper传过来的内容写入磁盘
context.write(NullWritable.get(),key);
}
}
主方法
public class MRRunJob {
public static void main(String[] args) {
Configuration conf = new Configuration();
conf.set("fs.defaultFS","hdfs://192.168.2.4:8020");
//初始化fs
FileSystem fs = null;
try {
fs = FileSystem.get(conf);
} catch (IOException e) {
e.printStackTrace();
}
Job job = null;
try {
job = Job.getInstance(conf,"mywc");
} catch (IOException e) {
e.printStackTrace();
}
//主方法
job.setJarByClass(MRRunJob.class);
//Mapper方法
job.setMapperClass(ETLMapper.class);
//Reducer方法
job.setReducerClass(ETLReducer.class);
//Map输出的key类型
job.setOutputKeyClass(Text.class);
//Map输出的value类型
job.setOutputValueClass(NullWritable.class);
try {
//读取文件位置
Path inputPath = new Path("/usr/input/data/etl/");
//如果这个文件不存在则新增
if(!fs.exists(inputPath)){
fs.mkdirs(inputPath);
}
//需要把本地的文件上传到HDFS
Path src = new Path("D:\\IdeaProjects\\MapReduceTest\\ETLDemo\\etl01.txt");
fs.copyFromLocalFile(src,inputPath);
FileInputFormat.addInputPath(job,inputPath);
//处理完成后文件输出位置
Path outputPath = new Path("/usr/output/data/etl01/");
//如果这个输出的目录存在则删除
if(fs.exists(outputPath)){
fs.delete(outputPath,true);
}
FileOutputFormat.setOutputPath(job,outputPath);
boolean f = job.waitForCompletion(true);
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意:如果在window环境中需要运行成功的话,需要修改原码 NativeIO.java
把这个源码复制到自己的工程当中,并修改如下内容:
把原来的return注释掉,直接reurn true即可
如果不做这个操作,则在运行时会报错: org.apache.hadoop.io.nativeio.NativeIO$Windows.access0(Ljava/lang/String;I)Z