文章目录
一、概述
MapReduce
是一个分布式运算程序的编程框架。这个框架提供的是一套对HDFS里面文件进行分析的编程思路,即Map
和Reduce
两步。通过MapReduce提供的接口,我们可以方便地编写实现一个分布式计算任务,MapReduce自带的组件会将我们的代码组装成一个分布式计算程序提交给Yarn
进行处理。
- 优点
易于编程、良好的扩展性,增加机器就能扩展计算能力、高容错性、适合海量数据的离线计算和批处理。 - 缺点
不擅长实时计算;
不擅长流式计算,即不适合处理动态数据;
不擅长GAD(有向图)计算,即多步计算,否则MR会产生大量磁盘IO
二、Hadoop序列化
(一)序列化定义
序列化是将内存中的对象,转换成字节序列(或其他数据传输协议)以便存储到磁盘(持久化)和网络传输。
(二)Hadoop序列化优点
编写的MR程序在传输数据时必然要将对象进行序列化和反序列化。然而,Java提供的序列化框架Serializable
是一个重量级框架,对象在序列化时会附带很多额外的信息(校验信息、Header、继承体等),序列化的对象需实现Serializable
接口并通过DataInputStream/DataOutputStream传输数据。
为此,Hadoop使用自己的序列化框架Writable
,提供了部分可序列化的基础类并支持自定义可序列化对象,常用的数据类型对应的Hadoop数据序列化类型如下:
Java类型 | Hadoop Writable类型 |
---|---|
Boolean | BooleanWritable |
Byte | ByteWritable |
Int | IntWritable |
Float | FloatWritable |
Long | LongWritable |
Double | DoubleWritable |
String | Text |
Map | MapWritable |
Array | ArrayWritable |
(三)自定义bean对象实现序列化接口(Writable)
具体实现bean对象序列化步骤如下:
(1)必须实现Writable接口
(2)反序列化时,需要反射调用空参构造函数,所以必须有空参构造
public FlowBean() {
super();
}
(3)重写序列化方法
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
(4)重写反序列化方法
@Override
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}
(5)注意反序列化的顺序和序列化的顺序完全一致
(6)要想把结果显示在文件中,需要重写toString()
(TextOutputFormat特性决定),可用”\t”分开,方便后续用。
(7)如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口,重写compareTo()方法,因为MapReduce框架中的Shuffle
过程要求对key必须能排序。
@Override
public int compareTo(FlowBean o) {
// 倒序排列,从大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
三、WordCount案例
- 创建Maven工程
- 添加依赖
- pom.xml
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.7.2</version>
</dependency>
</dependencies>
- 编写Mapper类、Reducer类和Driver驱动类
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
public class WordcountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
Text k = new Text();
IntWritable v = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1 获取一行
String line = value.toString();
// 2 切割
String[] words = line.split(" ");
// 3 输出
for (String word : words) {
k.set(word);
context.write(k, v);
}
}
}
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
public class WordcountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
int sum;
IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {
// 1 累加求和
sum = 0;
for (IntWritable count : values) {
sum += count.get();
}
// 2 输出
v.set(sum);
context.write(key,v);
}
}
import java.io.IOException;
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 {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// 1 获取配置信息以及封装任务
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 2 设置jar加载路径
job.setJarByClass(WordcountDriver.class);
// 3 设置map和reduce类
job.setMapperClass(WordcountMapper.class);
job.setReducerClass(WordcountReducer.class);
// 4 设置map输出
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5 设置最终输出kv类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 6 设置输入和输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7 提交
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
在Hadoop集群上运行MR程序,需将程序打包成jar包。
添加maven依赖(注意修改其中Driver类的全类名):
- pom.xml
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin </artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.bessen.mapreduce.WordCountDriver</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
然后导出jar包:
在Hadoop集群上运行WordCount程序:
$ hadoop jar MyWordCount.jar com.bessen.mapreduce.WordCountDriver /input /output
四、MapReduce工作流程
一个MapReduce程序就是一个job
,而Driver
类相当于Yarn的客户端,Driver将MR程序的各项配置和数据切片信息提交给Yarn,Yarn会创建一个MR appmaster,负责整个job的资源调度。一个job包括map
和reduce
两步,具体流程如下。
(一)Map阶段详细流程
- 准备数据
- 切片
切片数决定了MapTask任务数量,一般默认切片大小为数据块大小,切片机制和文件分块机制类似。默认由FileInputFormat
负责切片,为避免小文件问题导致切片过多,可以换成CombineTextInputFormat
- 提交job信息
Driver向Yarn提交job的各类配置和切片信息,Yarn生成对应的Mr appmaster管理该job后续的一切。 - 根据切片数生成对应个数的
MapTask
- 读入数据
MR默认使用TextInputFormat
,按行的方式读取数据,该行在整个文件的起始偏移量为键,整行数据(不包括终止符)为值,也可以在Driver类中配置使用其他方式读取。 - 循环对每行数据进行map逻辑运算
- 收集器(
OutputCollector
)将输出的键值对写入环形缓冲区
(该环形缓冲区位于内存,用于过渡,一侧存储元数据,包括索引、分区、k、v起始位置,另一侧存储实际的k、v数据,环形缓冲区默认大小为100M,占用80%后溢写到磁盘。) - 持续对环形缓冲区溢写的小部分数据进行分区以及分区内排序
- 分区、排序后的数据溢写到磁盘
- 对磁盘的数据进一步归并(
Merge
)排序(毕竟之前每次只排序一小部分数据),归并其实就是继续完成之前没做完的分区和区内排序工作。 Combiner
合并
这一步只有在Driver类中配置过的情况下才会进行。Reduce阶段是收集所有Map输出后进行合并操作,而Combiner相当于将合并提前,即对单个MapTask的数据进行提前合并。
(二)Reduce阶段流程
12. 根据分区数决定ReduceTask
的个数
ReduceTask个数也可以自己设置,在Driver类中添加:
// 默认值是1,手动设置为3
job.setNumReduceTasks(3);
- 各ReduceTask收集数据,将所有对应分区的数据下载到磁盘,合并文件并排序
- 循环对key相同的每组数据进行reduce逻辑运算
- 通过OutputFormat(默认
TextOutputFormat
)写出键值对。
五、InputFormat和OutputFormat
MapReduce框架的输入输出采用键值对的格式,这其中就涉及到了框架自带的InputFormat和OutputFormat,根据不同需求对key和value的选择,可以使用MR自带的几个实现类,也可以自定义InputFormat和OutputFormat.
(一)几种FileInputFormat的子类
- TextInputFormat
默认使用的类,按行读取数据,该行在整个文件的起始字节偏移量为键,LongWritable类型,该行内容为值,不包括终止符(回车和换行符),Text类型。 - KeyValueTextInputFormat
按分隔符将一行分隔为key、value,设置使用该类读取数据,需在Driver类中添加:
// 设置切割符
configuration.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR,"\t");
// 设置输入格式
job.setInputFormatClass(KeyValueTextInputFormat.class);
- NLineInputFormat
使用NLineInputFormat时,切片机制将改变为N行一个切片。数据读取方法与TextInputFormat保持一致。在Driver类中设置方法如下:
// 设置3行为一个切片(InputSplit)
NLineInputFormat.setNumLinesPerSplit(job, 3);
// 设置输入格式
job.setInputFormatClass(NLineInputFormat.class);
- 自定义InputFromat
步骤:
- 自定义RecordReader类继承RecordReader
- 自定义InputFromat类继承FileInputFromat
- 在输出时使用SequenceFileOutputFormat输出合并文件
- 在Driver类中设置:
// 7设置输入的inputFormat
job.setInputFormatClass(MyFileInputformat.class);
// 设置输出的outputFormat
job.setOutputFormatClass(SequenceFileOutputFormat.class);
(二)几种FileOutputFormat的子类
MapReduce计算任务最终得到的是键值对类型的数据,在Reducer类里我们直接写一个write()方法进行输出。
context.write(key, value);
但这些数据最终是要输出到磁盘文件里面的,背后负责写出数据的就是OutputFormat。
- TextOutputFormat
MR默认使用的是TextOutputFormat
,会直接调用key和value的toString()方法,把输出的一条条数据写成文本行,格式如下:
- SequenceFileOutputFormat
MR还提供了另一个子类SequenceFileOutputFormat
,该类输出的格式紧凑,容易压缩,一般使用这个类时,输出作为下一个MapReduce任务的输入。 - 自定义OutputFormat
如果我们不喜欢TextOutputFormat提供的输出格式,而是想要输出其他的格式,例如对数据简单处理、将结果输出到数据库等,我们也可以自定义OutputFormat,其实就是自己写个类定义输出规则,告诉MapReduce拿到最终key和value后怎么操作。具体步骤如下:
- 自定义一个类继承
RecordWriter
,重写其中的构造方法和write()、close()方法。 - 自定义一个类继承
FileOutputFormat
,重写getRecordWriter()方法
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
// 返回一个RecordWriter
return new MyRecordWriter(job);
}
- 在Driver类中添加:
// 要将自定义的输出格式组件设置到job中
job.setOutputFormatClass(MyOutputFormat.class);
六、Shuffle机制
在前面的MapReduce工作流程中,每个MapTask,在map之后的写入环形缓冲区、分区、区内排序、溢写磁盘、归并、(合并)、压缩的整个过程称为Shuffle(洗牌)。
(一)分区(Partition)
分区数直接决定了ReduceTask个数,MapReduce默认使用HashPartitioner
进行分区,分区个数默认为1,通常我们需要自定义分区。
自定义分区步骤如下:
自定义一个类继承Partitioner,重写getPartition()方法。
public class MyPartitioner extends Partitioner<Text, TEXT> {
@Override
public int getPartition(Text key, Text value, int numPartitions) {
// 分区逻辑
// ...
return partition;
}
}
在Driver中设置:
job.setPartitionerClass(MyPartitioner.class);
//根据分区逻辑设置ReduceTask数量
job.setNumReduceTasks(3);
(二)排序
Map和Reduce阶段都要进行排序,如MapTask每次溢写磁盘前的区内排序、Map阶段最终的归并排序、ReduceTask从各MapTask节点获取输出后进行的归并排序等。
MapReduce默认对key
进行字典排序。为此,如果想要自定义排序,就需要让key对应的Bean对象具备排序功能,具体过程是:
让自定义的Bean对象实现WritableComparable
接口并重写compareTo
方法。
@Override
public int compareTo(FlowBean bean) {
int result;
// 按照sumFlow大小,倒序排列
if (sumFlow > bean.getSumFlow()) {
result = -1;
}else if (sumFlow < bean.getSumFlow()) {
result = 1;
}else {
result = 0;
}
return result;
}
(三)合并(Combiner)
ReduceTask需要收集所有MapTask节点对应分区的输出,进行汇总操作。如果Map阶段输出的数据量很大,必然会耗费大量的IO和网络传输资源。
如果任务在Map阶段进行提前汇总不会影响最终结果,则可以使用Combiner进行合并,事实上,Combiner组件的父类就是Reducer。
自定义Combiner的步骤:
- 自定义一个Combiner继承Reducer,重写Reduce方法
public class WordcountCombiner extends Reducer<Text, IntWritable, Text,IntWritable>{
@Override
protected void reduce(Text key, Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {
// 1 汇总操作
int count = 0;
for(IntWritable v :values){
count += v.get();
}
// 2 写出
context.write(key, new IntWritable(count));
}
}
- 在Driver驱动类中设置:
job.setCombinerClass(WordcountCombiner.class);
(四)分组排序/辅助排序(GroupingComparator)
Reduce阶段,相同key的数据会进入同一个reduce方法进行汇总,但在很多情况下,我们需要让相同组的数据进入一个reduce方法,同一组的数据key不一定相同,这时候就需要在reduce前添加分组排序。
分组排序的过程是按照key对输入Reduce阶段的数据进行分组,分组后,同一组的数据进入同一个reduce方法,设置如下:
- 自定义类继承WritableComparator,重写compare()方法并创建一个构造方法将比较对象的类传给父类
protected OrderGroupingComparator() {
super(OrderBean.class, true);
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
// 比较的业务逻辑,a、b同一组时返回值为0
// ...
return result;
}
- 在Driver类中设置
// 设置reduce端的分组
job.setGroupingComparatorClass(MyComparator.class);
七、Reduce Join和Map Join
其实,Map Join和Reduce Join没有什么新的东西,不像排序、分组、合并这些有MR框架提供的接口或父类,前面的内容是整个MR框架的基础,Map Join和Reduce Join只是两种实现join的逻辑思路。
首先回顾join,如果表的设计过程合乎规范,需要join的两张表中,一张表的外键应该是另一张表的主键,如下图所示:
(一)Reduce Join
Reduce Join的思路是在Map阶段的setup()
初始化方法中先区分是哪张表,并在map方法里面打上标签。Map阶段的输出以外键B为key,这样就可以在Reduce阶段将两张表属性B相同的数据汇总到一起进行join操作了。
- 在Mapper类的setup()方法中获取表名:
String name;
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 1 获取输入文件切片
FileSplit split = (FileSplit) context.getInputSplit();
// 2 获取输入文件名称
name = split.getPath().getName();
}
(二)Map Join
如果两张表中有一个是小表,那么可以直接让每一个MapTask都缓存一份小表到内存中,这样一来,在Map阶段就能直接完成join,也就不需要后面的Shuffle和Reduce过程了。
- 在Driver类中设置缓存文件路径并设置reduceTask个数为0:
// 设置缓存数据路径,HDFS路径为hdfs://
job.addCacheFile(new URI("file:///缓存文件路径"));
// 设置reduceTask数量为0,取消reduce阶段
job.setNumReduceTasks(0);
- 在Mapper类的setup()方法中缓存小表到HashMap:
Map<String, String> hashMap = new HashMap<>();
@Override
protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
// 1 获取第0个缓存的文件
URI[] cacheFiles = context.getCacheFiles();
String path = cacheFiles[0].getPath().toString();
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path), "UTF-8"));
String line;
while(StringUtils.isNotEmpty(line = reader.readLine())){
// 2 切割
String[] fields = line.split("\t");
// 3 缓存数据到集合
hashMap.put(fields[0], fields[1]);
}
// 4 关流
reader.close();
}