一、MapReduce的概念
Hadoop的三大组件:HDFS、Yarn、MapReduce
HDFS:解决的是分布式存储的问题。
MapReduce: 解决的是计算问题。
Yarn: 计算的时候,使用的资源如何协调(Windows操作系统)
2004年,谷歌发表了一篇名为《MapReduce》的论文,主要介绍了如何在分布式的存储系统上对数据进行高效率的计算。2005年,Nutch团队使用Java语言实现了这个技术,并命名为MapReduce。时至今日,MapReduce是Apache Hadoop的核心模块之一,是运行在HDFS上的分布式运算程序的编程框架,用于大规模数据集(大于1TB)的并行运算。其中的概念,"Map(映射)"和"Reduce(归约)"
reduce 其实就是合并的意思。
数据量如果太大,计算是代码随着数据走。
mapReduce的优缺点:
Servlet(淘汰了) --> SpringMVC
优点:
1、易于编程
代码写起来有固定的格式,编写难度非常的小,号称是八股文【固定写法】。
2、良好的扩展性
代码的计算资源不够了,可以直接拓展几台即可解决
3、高容出错
如果负责计算的电脑挂掉了,直接可以将任务转移到其他电脑上,任务不会执行失败的。
4、非常适合大数据集的计算(PB级以上) 1P=1024T
缺点:
1、不适合做实时计算
mapreduce一个任务就要跑很长时间,不利于实时。不能做到秒级或者毫秒级的计算。
mapreduce 属于离线的技术。
2、不适合做流式计算
数据因为都是静态的,不是边产生数据,边计算。
固定计算:数据量是固定的,给了1T 就计算。
3、不适合做有向图(DAG)计算
多个应用程序之间有依赖关系,后一个程序需要依赖前面的程序的结果。这种场景就称之为有向图,mapreduce是不适合的。
总结:MapReduce只能做离线的数据分析,并且计算速度比较慢。
我们之前接触过:运行过一个WordCount 案例。
二、MapReduce案例--WordCount
1、新建maven项目,并且导入包
<!--指定代码编译的版本-->
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>3.3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-client -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>3.3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-hdfs -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>3.3.1</version>
</dependency>
</dependencies>
补充Maven的支持:
2、创建一些数据
在项目的根路径下,创建一个文件夹,mr01 ,在mr01文件夹下,再创建数据的来源文件input文件夹,
在input文件夹下面,新建file,a.txt, b.txt, c.txt
a.txt
hello bigdata hello 1999 hello beijing hello
world hello hello java good
b.txt
hello gaoxinqu hello bingbing
hello chenchen hello
ACMilan hello china
c.txt
hello hadoop hello java hello storm hello spark hello redis hello zookeeper
hello hive hello hbase hello flume
3、编写代码
1)编写Map代码
package com.bigdata;
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;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/8/1 9:35
* @Version 1.0
*
* Mapper中的四个泛型跟什么照应:
* 1、LongWritable 行偏移量,一般都是LongWritable ,这一行的数据是从第几个字符开始计算的,因为数据量很多这个值也会很大,所以使用Long
* 2、Text 指的是这一行数据
* 3、Text Map任务输出的Key值的类型 单词
* 4、IntWritable Map任务输出的Key值的类型 1
*
*/
public class WordCountMapper extends Mapper<LongWritable, Text,Text, IntWritable> {
// Ctrl + o 可以展示哪些方法可以重写
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, IntWritable>.Context context) throws IOException, InterruptedException {
// key 值 指的是行偏移量
// value 指的是 这一行数据
//hello bigdata hello 1999 hello beijing hello
String line = value.toString();
// [hello,qianfeng,hello,1999,hello,beijing,hello]
String[] arr = line.split("\\s+");
// hello-> 1,bigdata->1,hello->1,1999->1,hello->1,beijing->1,hello->1
for (String word: arr) {
context.write(new Text(word),new IntWritable(1));
}
// fori 循环
/*for (int i = 0; i < arr.length ; i++) {
context.write(new Text(arr[i]),new IntWritable(1));
}*/
}
}
2)编写Reduce代码
package com.bigdata;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* reduce 是用来合并的
* reduce四个泛型:
* 前两个,跟map的输出类型一样
* 后面两个泛型:reduce端的输出类型
* hello 5
* world 2
* ...
*/
public class WordCountReducer extends Reducer<Text, IntWritable,Text, IntWritable> {
// reduce 这个方法,有多少个key值,就会调用多少次
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
// reduce 拿到的数据是什么样的呢 hello [1,1,1,1,1] world [1,1]
// 数据不是你想像的这个样子 hello 1 hello 1 hello 1
int count = 0;
// 第一种写法
for (IntWritable num : values) {
int i = num.get();
count = count + i;
}
// 第二种写法
/*Iterator<IntWritable> iterator = values.iterator();
while(iterator.hasNext()){
int i = iterator.next().get();
count = count + i;
}*/
// hello 5
context.write(key,new IntWritable(count));
}
}
3)编写测试代码
package com.bigdata;
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;
public class WordCountDriver {
// 这个类就是把 Mapper和 Reducer 放在一起执行的
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
// 使用本地的文件系统,而不是hdfs
configuration.set("fs.defaultFS","file:///");
// 使用本地的资源(CPU,内存等), 也可以使用yarn平台跑任务
configuration.set("mapreduce.framework.name","local");
Job job = Job.getInstance(configuration, "wordCount单词统计");
// 指定 map
job.setMapperClass(WordCountMapper.class);
// hello 1
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 自定义分区器的使用
job.setPartitionerClass(WordCountPartitioner.class);
// 设置reduceTask的数量
// reduce的数量决定了reduceTask的任务数量,每一个任务,结束后都会产生一个文件 part-r-xxxxx
// 结论:reduceTask的数量可以和分区数量不一致,但是没有意义,一般两者保持一致。
job.setNumReduceTasks(5);
// 指定 reduce
job.setReducerClass(WordCountReducer.class);
// hello 5
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 此处也可以使用绝对路径
FileInputFormat.setInputPaths(job,"../WordCount/mr01/input/");
FileOutputFormat.setOutputPath(job,new Path("../WordCount/mr01/output2"));
boolean result = job.waitForCompletion(true);
// 返回结果如果为true表示任务成功了,正常退出,否则非正常退出
System.exit(result?0:-1);
}
}
4、遇到的错误:
1、输出路径已经存在
2、出现了警告
这个项目中需要log4j的配置,你没有给,所有会出这个警告(可以处理也不可以不处理)
但是如果遇到某个错误,不出结果,也不报错,这个时候就需要程序打印详细的日志查看。
需要在项目中添加一个log4j.properties
# Global logging configuration
# Debug info warn error
log4j.rootLogger=DEBUG, stdout
# MyBatis logging configuration...
log4j.logger.org.mybatis.example.BlogMapper=TRACE
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
如果觉得日志中出现的这个错误非常的难受,可以进行解决:
解决这个问题的办法:
第一步:导入包:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.0.6</version>
</dependency>
第二步:resources文件夹下引入一个logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="CONSOLE_LOG_PATTERN"
value="%date{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) ${PID:- }--%magenta([%15.15(%thread)]) %cyan(%-50.50(%logger{50})) : %msg %n"/>
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<charset>UTF-8</charset>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="CONSOLE_APPENDER"/>
</root>
</configuration>
3、如果出现如下问题,如何解决?
Exception in thread "main" java.lang.UnsatisfiedLinkError: org.apache.hadoop.io.nativeio.NativeIO$Windows.access0(Ljava/lang/String;I)Z
at org.apache.hadoop.io.nativeio.NativeIO$Windows.access0(Native Method)
at org.apache.hadoop.io.nativeio.NativeIO$Windows.access(NativeIO.java:793)
at org.apache.hadoop.fs.FileUtil.canRead(FileUtil.java:1215)
at org.apache.hadoop.fs.FileUtil.list(FileUtil.java:1420)
at org.apache.hadoop.fs.RawLocalFileSystem.listStatus(RawLocalFileSystem.java:601)
at org.apache.hadoop.fs.FileSystem.listStatus(FileSystem.java:1972)
at org.apache.hadoop.fs.FileSystem.listStatus(FileSystem.java:2014)
at org.apache.hadoop.fs.FileSystem$4.<init>(FileSystem.java:2180)
at org.apache.hadoop.fs.FileSystem.listLocatedStatus(FileSystem.java:2179)
at org.apache.hadoop.fs.ChecksumFileSystem.listLocatedStatus(ChecksumFileSystem.java:783)
at org.apache.hadoop.mapreduce.lib.input.FileInputFormat.singleThreadedListStatus(FileInputFormat.java:320)
at org.apache.hadoop.mapreduce.lib.input.FileInputFormat.listStatus(FileInputFormat.java:279)
at org.apache.hadoop.mapreduce.lib.input.FileInputFormat.getSplits(FileInputFormat.java:404)
at org.apache.hadoop.mapreduce.JobSubmitter.writeNewSplits(JobSubmitter.java:310)
at org.apache.hadoop.mapreduce.JobSubmitter.writeSplits(JobSubmitter.java:327)
at org.apache.hadoop.mapreduce.JobSubmitter.submitJobInternal(JobSubmitter.java:200)
at org.apache.hadoop.mapreduce.Job$11.run(Job.java:1571)
at org.apache.hadoop.mapreduce.Job$11.run(Job.java:1568)
at java.security.AccessController.doPrivileged(Native Method)
at javax.security.auth.Subject.doAs(Subject.java:422)
at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1878)
at org.apache.hadoop.mapreduce.Job.submit(Job.java:1568)
at org.apache.hadoop.mapreduce.Job.waitForCompletion(Job.java:1589)
at mr02.WordCountDriver.main(WordCountDriver.java:58)
如果之前在windows中的hadoop中拷贝过hadoop.dll等文件,是不会出现这个问题的。需要进行安装包的补丁配置。
解决方案:
将之前的 hadoop.dll 文件拷贝在 C:/Windows/System32文件夹下一份,可以不重启试试,假如不管用,重启一下。
三、案例升级一下
需求:不仅单词统计,还需要将a-p 的单词存放在一起,q-z的单词存放在一起,其他单词存放在另一个文件中。
如果要完成以上的需求:就需要引入新的组件Partitioner。
1、编写代码
package com.bigdata;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/8/1 11:22
* @Version 1.0
*
* Map任务 --> Partitioner --> Reducer
* Partitioner 其实就是Map端的输出
*/
public class WordCountPartitioner extends Partitioner<Text, IntWritable> {
// 分区的区号,一定是从0开始的,中间不能断 0 1 2 3 4..
@Override
public int getPartition(Text text, IntWritable intWritable, int i) {
// text就是一个单词
String letter = text.toString();
char c = letter.charAt(0);
if(c>='a' && c <='p'){
return 0;
}else if(c>='q' && c <='z'){
return 1;
}else{
return 2;
}
}
}
2、使用分区代码
package com.bigdata;
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;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/8/1 10:02
* @Version 1.0
*/
public class WordCountDriver2 {
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
// 使用本地的文件系统,而不是hdfs
configuration.set("fs.defaultFS","file:///");
// 使用本地的资源(CPU,内存等), 也可以使用yarn平台跑任务
configuration.set("mapreduce.framework.name","local");
Job job = Job.getInstance(configuration, "单词统计WordCount");
// map任务的设置
job.setMapperClass(WordCountMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 指定分区的类是哪一个
job.setPartitionerClass(WordCountPartitioner.class);
// 还要执行 reduce的数量 因为一个reduce 就会产生一个结果文件
job.setNumReduceTasks(3);
// reduce任务的设置
job.setReducerClass(WordCountReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 设置要统计的数据的路径,结果输出路径
FileInputFormat.setInputPaths(job,new Path("mr01/input"));
// ouput文件夹一定不要出现,否则会报错
FileOutputFormat.setOutputPath(job,new Path("mr01/output2"));
// 等待任务执行结束
boolean b = job.waitForCompletion(true);
// 此处是一定要退出虚拟机的
System.exit(b ? 0:-1);
}
}
如果分区文件,指定的是3个分区,reduce的任务设置的是1个,结果就是产生1个输出文件,三个分区的结果在一个文件中。
如果是3个分区,reduce的任务设置的是5个,输出结果就是5个文件,其中三个是有值的,后面两个是没有值的空文件。
综上所述:分区的数量,一定要跟reduce的任务数量一致,效率是最高的。
四、序列化
啥是序列化?
jvm中的一个对象,不是类,假如你想把一个对象,保存到磁盘上,必须序列化,你把文件中的对象进行恢复,是不是的反序列化。
假如你想把对象发送给另一个服务器,需要通过网络传输,也必须序列化,到另一侧要反序列化。
大数据中,需要用到序列化,因为数据在传输过程中,需要用到。
说到序列化,我们想到了Java的序列化。一个类实现了Serializable 接口即可。
Java对象什么时候需要序列化?
1)需要保存到本地的时候
2)需要在网络之间传输的时候
package com.bigdata;
import java.io.Serializable;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/8/1 11:43
* @Version 1.0
*/
public class User implements Serializable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
大数据技术Hadoop并没有采用java的序列化机制,而是自己又整了一套自己的序列化机制。为什么?
Java的序列化携带的信息太多了,文件太大了,不便于在网络之间传输。
User 使用Java --> 100KB
User 使用大数据的序列化 --> 5KB
大数据采用的序列化机制是 Writable 接口。
为什么非得序列化呢?因为需要在网路之间传输。
java的八大基本数据类型: byte short int long float double char boolean
只需要记住:String --> Text即可。null --> NullWritable,仅仅是为了在某个地方占位,符合语法而已。
1、测试java序列化 VS Hadoop序列化大小比较
java序列化
package com.bigdata;
import java.io.Serializable;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/8/1 11:43
* @Version 1.0
*/
public class User implements Serializable {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Hadoop的序列化
package com.bigdata;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/8/1 14:06
* @Version 1.0
*/
public class UserWritable implements Writable {
private String name;
private int age;
public UserWritable(String name, int age) {
this.name = name;
this.age = age;
}
// 序列化
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
}
// 反序列化
@Override
public void readFields(DataInput in) throws IOException {
// 进行反序列化的时候,读取的顺序一定要跟序列化的时候的顺序一致,否则报错
name = in.readUTF();
age = in.readInt();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
使用对象流对比
package com.bigdata;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/8/1 14:11
* @Version 1.0
*/
public class TestXLH {
public static void main(String[] args) throws Exception {
User user = new User("zhangsan",20);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/user1.txt"));
objectOutputStream.writeObject(user);
objectOutputStream.close();
UserWritable user2 = new UserWritable("zhangsan",20);
ObjectOutputStream objectOutputStream2 = new ObjectOutputStream(new FileOutputStream("D:/user2.txt"));
// 此时是序列化对象去write 对象流,此处需要注意
user2.write(objectOutputStream2);
objectOutputStream2.close();
}
}
java序列化的结果:
hadoop序列化的结果:
2、自定义序列化案例--手机流量统计
需求:假定拿到了一些关于手机流量的日志文件,统计每个手机号码的上行流量,下行流量,以及总流量。
读懂日志文件:
1363157995033 15920133257 5C-0E-8B-C7-BA-20:CMCC 120.197.40.4 sug.so.360.cn 信息安全 20 20 3156 2936 200
第二列是手机号码 15920133257
倒数第三列是上行流量 3156
倒数第二列是下行流量 2936
思路:
map任务:
15989002119 1232 3456
15989002119 2343 34343
......
13726238888 3243 23432
13726238888 4343 4343
reduce 任务:
15989002119 对相同的手机号码进行合并
Key: 15989002119 values: [{15989002119,34343,34343},{15989002119,8989,34343}....]
此处:手机号码,上行流量,下行流量,总流量
此时就需要自定义数据类型了。
统计的结果:
代码演示:
package com.bigdata.phoneflow;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/8/1 14:29
* @Version 1.0
*/
// 自定义一个数据类型
public class PhoneFlowWritable implements Writable {
private String phone;
private int upFlow;
private int downFlow;
// 此处需要指定一个空参数的构造方法,否则报错:
// java.lang.NoSuchMethodException: com.bigdata.phoneflow.PhoneFlowWritable.<init>()
public PhoneFlowWritable() {
}
public PhoneFlowWritable(String phone, int upFlow, int downFlow) {
this.phone = phone;
this.upFlow = upFlow;
this.downFlow = downFlow;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public int getUpFlow() {
return upFlow;
}
public void setUpFlow(int upFlow) {
this.upFlow = upFlow;
}
public int getDownFlow() {
return downFlow;
}
public void setDownFlow(int downFlow) {
this.downFlow = downFlow;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(phone);
out.writeInt(upFlow);
out.writeInt(downFlow);
}
@Override
public void readFields(DataInput in) throws IOException {
phone = in.readUTF();
upFlow = in.readInt();
downFlow = in.readInt();
}
}
package com.bigdata.phoneflow;
import com.bigdata.WordCountMapper;
import com.bigdata.WordCountPartitioner;
import com.bigdata.WordCountReducer;
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;
import java.util.Iterator;
/**
* @Author laoyan
* @Description TODO
* @Date 2022/8/1 14:33
* @Version 1.0
*/
class PhoneFlowMapper extends Mapper<LongWritable, Text,Text,PhoneFlowWritable> {
// 将每一句话,都变为 手机号码 --> PhoneFlowWritable对象
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, PhoneFlowWritable>.Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] arr = line.split("\\s+");// \s 表示一个空白符,+ 表示出现一次到多次
String phone = arr[1];
int upFlow = Integer.parseInt(arr[arr.length-3]);
int downFlow = Integer.parseInt(arr[arr.length-2]);
PhoneFlowWritable phoneFlowWritable = new PhoneFlowWritable(phone, upFlow, downFlow);
context.write(new Text(phone) ,phoneFlowWritable);
}
}
// 手机号 --> 流量数据PhoneFlowWritable 手机号码 --> 统计的结果
class PhoneFlowReducer extends Reducer<Text,PhoneFlowWritable,Text,Text> {
@Override
protected void reduce(Text key, Iterable<PhoneFlowWritable> values, Reducer<Text, PhoneFlowWritable, Text, Text>.Context context) throws IOException, InterruptedException {
int upFlowNum = 0;
int downFlowNum = 0;
Iterator<PhoneFlowWritable> iterator = values.iterator();
while(iterator.hasNext()){
PhoneFlowWritable phoneFlowWritable = iterator.next();
upFlowNum += phoneFlowWritable.getUpFlow();
downFlowNum += phoneFlowWritable.getDownFlow();
}
StringBuffer sb = new StringBuffer();
sb.append("手机号"+key+"流量统计:");
sb.append("上行流量是:"+upFlowNum);
sb.append("下行流量是:"+downFlowNum);
sb.append("总的流量是:"+(upFlowNum + downFlowNum));
context.write(key,new Text(sb.toString()));
}
}
public class PhoneFlowDriver {
public static void main(String[] args) throws Exception{
Configuration configuration = new Configuration();
// 使用本地的文件系统,而不是hdfs
configuration.set("fs.defaultFS","file:///");
// 使用本地的资源(CPU,内存等), 也可以使用yarn平台跑任务
configuration.set("mapreduce.framework.name","local");
Job job = Job.getInstance(configuration, "手机流量统计");
// map任务的设置
job.setMapperClass(PhoneFlowMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(PhoneFlowWritable.class);
// reduce任务的设置
job.setReducerClass(PhoneFlowReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
// 设置要统计的数据的路径,结果输出路径
FileInputFormat.setInputPaths(job,new Path("mr01/phoneFlow/input"));
// ouput文件夹一定不要出现,否则会报错
FileOutputFormat.setOutputPath(job,new Path("mr01/phoneFlow/output"));
// 等待任务执行结束
boolean b = job.waitForCompletion(true);
// 此处是一定要退出虚拟机的
System.exit(b ? 0:-1);
}
}
五、关于片和块
假如我现在500M这样的数据,如何存储?
500M = 128M + 128M + 128M + 116M 分为四个块进行存储。
计算的时候,是按照片儿计算的,而不是块儿。
块是物理概念,一个块就是128M ,妥妥的,毋庸置疑。
片是逻辑概念,一个片大约等于一个块。
假如我现在需要计算一个300M的文件,这个时候启动多少个MapTask任务?答案是有多少个片儿,就启动多少个任务。
一个片儿约等于 一个块,但是最大可以 128M*1.1倍= 140.8
300M
128M 启动一个Map任务进行读取
172M 172M 和 128M * 1.1 =140.8M 进行比较,如果大于 ,继续进行切割
128M 启动一个任务Map任务
剩余44M 剩余的44M 和 128M*1.1倍比较,小于这个值,剩余的44M 就单独起一个Map任务
300m的数据,分给了3个MapTask任务进行处理。
如果是260M的数据,由多少个Map任务处理?
128M 第一个任务
132M 跟 128M * 1.1 进行比较,发现小于这个值,直接一个Map任务搞定,不在启动第三个任务了。
比如班里的同学一起搬砖,每人规定搬3块,假定砖还剩4块,到某个同学了,他就直接搬完即可,没必要让另一个同学因为一块砖,而专门跑一趟。
1、什么是片,什么是块?
块是物理概念,片是逻辑概念。一般片 = 块的,但是到最后一次的时候,有可能片> 块,但是绝对不能超过块的1.1倍。
2、mapreduce 启动多少个MapTask任务?
跟片有关系,有多少个片,就启动多少个map任务。
六、MapReduce的原理
简单版本:
AppMaster: 整个Job任务的核心协调工具
MapTask: 主要用于Map任务的执行
ReduceTask: 主要用于Reduce任务的执行
一个任务提交Job --> AppMaster(项目经理)--> 根据切片的数量统计出需要多少个MapTask任务 --> 向ResourceManager(Yarn平台的老大)索要资源 --> 执行Map任务,先读取一个分片的数据,传递给map方法。--> map 方法不断的溢写 --> reduce 方法 --> 将统计的结果存放在磁盘上。
分开讲解版:
MapTask执行阶段:
1. maptask调用FileInputFormat的getRecordReader读取分片数据
2. 每行数据读取一次,返回一个(K,V)对,K是offset(偏移量),V是一行数据
3. 将k-v对交给MapTask处理
4. 每对k-v调用一次map(K,V,context)方法,然后context.write(k,v)
5. 写出的数据交给收集器OutputCollector.collector()处理
6. 将数据写入环形缓冲区,并记录写入的起始偏移量,终止偏移量,环形缓冲区默认大小100M
7. 默认写到80%的时候要溢写到磁盘,溢写磁盘的过程中数据继续写入剩余20%
8. 溢写磁盘之前要先进行分区然后分区内进行排序
9. 默认的分区规则是hashpatitioner,即key的 hash%reduceNum
所有的mapreduce,其实都用到了分区,如果不写,使用的是默认的分区。
job.setNumReduceTask(3);
10. 默认的排序规则是key的字典顺序,使用的是快速排序
11. 溢写会形成多个文件,在maptask读取完一个分片数据后,先将环形缓冲区数据刷写到磁盘
12. 将数据多个溢写文件进行合并,分区内排序(外部排序===》归并排序)
关于9 的再次解释:
ReduceTask的执行流程:
1. 数据按照分区规则发送到reducetask
2. reducetask将来自多个maptask的数据进行合并,排序(外部排序===》归并排序)
3. 按照key相同分组
4. 一组数据调用一次reduce(k,iterable<v>values,context)
5. 处理后的数据交由reducetask
6. reducetask调用FileOutputFormat组件
7. FileOutputFormat组件中的write方法将数据写出。
总结
ReduceTask任务的数量是由谁决定的?
job.setNumReduceTasks(5);
是指定的,设置的几个就执行几个。
这个值不能瞎设置,要参考分区数量,加入有三个分区,ReduceTask任务就需要指定为3个。
七、Shuffle 过程【核心】
MapReduce的Shuffle过程指的是MapTask的后半程,以及ReduceTask的前半程,共同组成的。
从MapTask中的map方法结束,到ReduceTask中的reduce方法开始,这个中间的部分就是Shuffle。是MapReduce的核心,心脏。
map端:
1、map中的context.write方法,对外写出的时候,其实是写入到了一个环形缓冲区内(内存形式的),这个环形缓冲区大小是100M,可以通过参数设置。如果里面的数据大于80M,就开始溢写(从内存中将数据写入到磁盘上)。溢写的文件存放地址可以设置。
2、在溢写过程中,环形缓冲区不会停止工作,是会利用剩余的20%继续存入环形缓冲区的。除非是环形缓冲区的内存满了,map任务就被阻塞了。
在溢写出来的文件中,是排过序的,排序规则:快速排序算法。在排序之前,会根据分区的算法,对数据进行分区。是在内存中,先分区,在每一个分区中再排序,接着溢写到磁盘上的。
3、溢写出来的小文件需要合并为一个大文件,因为每一个MapTask只能有一份数据。就将相同的分区文件合并,并且排序(此处是归并排序)。每次合并的时候是10个小文件合并为一个大文件,进行多次合并,最终每一个分区的文件只能有一份。
假如100个小文件,需要合并几次呢?
100 每10分合并一次,第一轮:100个文件合并为了10个文件,这10个文件又合并为一个大文件,总共合并了11次。
4、将内存中的数据,溢写到磁盘上,还可以指定是否需要压缩,以及压缩的算法是什么。
reduce端:
1、reduce端根据不同的分区,拉取每个服务器上的相同的分区的数据。
reduce任务有少量复制线程,因此能够并行取得map输出。默认值是5个线程,但这个默认值可以修改设置mapreduce.reduce.shuffle. parallelcopies 属性即可。
2、如果map上的数据非常的小,该数据会拉取到reduce端的内存中,如果数据量比较大,直接拉取到reduce端的硬盘上。
环形缓冲区:
环形缓冲区,其实是一个数组,将数组分为两部分,分割的这个点就称之为轴心。存储KV键值对,是从左到右,类似于顺时针,因为每一个KV键值对都有对应的元数据。元数据是从轴心开始,从右向左执行。
当两者数据占用空间达到80%的时候,需要清理数组,清理完之后,轴心发生了变化。
KV键值对的元数据,是占用16个空间的(每四个是一组,共计4组)
前面四个第一组::表示Value的起始位置,第二组:Key值的起始位置,第三组:分区信息,第四组:val的长度。这些内容称之为KV键值对的meta数据(元数据)。
八、Combiner
这个Combiner是一个优化的代码,对于我们最终的结果没有任何的影响。
map端产生的数据,会被拉去到reduce端进行合并,有可能map端产生的数据非常的大,不便于在网络间传输,那么有没有办法可以缩小map端的数据呢?
之前: java 1 java 1 java 1 传递给reduce
现在: java 3 传递给reduce
Combiner其实就是运行在mapTask中的reducer。 Reducer其实就是合并代码的。Combiner是作用在Map端的。
这个结果不是最终的结果,而是一个临时的小统计。 最终reduce是会将所有的map结果再次进行汇总才是我们最终想要的统计结果。
Combiner 只能用于对统计结果没有影响的场景下。
一般只用于 统计之和,统计最大值最小值的场景下。统计平均值等情况是不能用的。
在代码中如何使用?
Combiner起作用的地方:
Combiner 其实作用于两个地方,一个是环形缓冲区溢写磁盘的时候,除了分区,排序之外,还可以做合并操作,将内存中的 hello 1 hello 1 hello 1 会合并为 hello 3
第二个位置是小文件合并为MapTask的大文件的时候,会将多个 hello 的值相加 hello 19,但是这个不是最终的答案,最终答案是将多个MapTask任务中的hello 进行合并才是最终的结果。
九、需要记忆的内容:
1. 从map函数输出到reduce函数接受输入数据,这个过程称之为shuffle.
2. map函数的输出,存储环形缓冲区(默认大小100M,阈值80M)
环形缓冲区:其实是一个字节数组kvbuffer. 这个字节数组,由两部分组成,一部分顺时针存储KV键值对,一部分逆时针存储 KV键值对的元数据。
3. 当达到阈值80%时,准备溢写到本地磁盘(因为是中间数据,因此没有必要存储在HDFS上)。在溢写前要进行对元数据分区(partition)整理,然后进行排序(quick sort,通过元数据找到出key,同一分区的所有key进行排序,排序完,元数据就已经有序了,在溢写时,按照元数据的顺序寻找原始数据进行溢写)
4. 如果有必要,可以在排序后,溢写前调用combiner函数进行运算,来达到减少数据的目的
5. 溢写文件有可能产生多个,然后对这多个溢写文件进行再次合并(也要进行分区和排序)。当溢写个数>=3时,可以再次调用combiner函数来减少数据。如果溢写个数<3时,默认不会调用combiner函数。
6. 合并的最终溢写文件可以使用压缩技术来达到节省磁盘空间和减少向reduce阶段传输数据的目的。(存储在本地磁盘中)
7. Reduce阶段通过HTTP写抓取属于自己的分区的所有map的输出数据(默认线程数是5,因此可以并发抓取)。
8. 抓取到的数据存在内存中,如果数据量大,当达到本地内存的阈值时会进行溢写操作,在溢写前会进行合并和排序(排序阶段),然后写到磁盘中,
9. 溢写文件可能会产生多个,因此在进入reduce之前会再次合并(合并因子是10),最后一次合并要满足10这个因子,同时输入给reduce函数,而不是产生合并文件。reduce函数输出数据会直接存储在HDFS上。